From 17f012b7776aaa75231f9fe4e00a9127f9a44e0b Mon Sep 17 00:00:00 2001 From: drkhsh Date: Mon, 27 Apr 2026 13:08:38 +0200 Subject: [PATCH] Distinguish sent vs delivered with check-mark indicators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously a single ASCII glyph collapsed SENT and DELIVERED. Wire PacketReceipt delivery callbacks through to sideband-style single and double checks. LXMFManager tracks outstanding receipts (opportunistic + link single-packet) by hash. On delivery: persist DELIVERED and fire _statusCb. On 60s timeout: drop the entry, leave at SENT — lack of proof over LoRa isn't proof of failure. Resource transfers stay at SENT; microReticulum's Transport skips receipts for resource packets and Link::start_resource_transfer exposes no concluded callback to hook. LvMessageView renders the glyph via applyStatusGlyph (Montserrat 12): REFRESH for in-flight, single OK for SENT, double OK for DELIVERED, WARNING for FAILED. Status callback matches by timestamp (~1s tolerance) so DELIVERED lands on the right bubble after later messages have already moved past QUEUED. --- lv_conf.h | 2 +- src/reticulum/LXMFManager.cpp | 47 ++++++++++++++++++++++-- src/reticulum/LXMFManager.h | 13 +++++++ src/ui/screens/LvMessageView.cpp | 63 +++++++++++++++++++------------- src/ui/screens/LvMessageView.h | 1 + 5 files changed, 97 insertions(+), 29 deletions(-) diff --git a/lv_conf.h b/lv_conf.h index 3287779..ead33c6 100644 --- a/lv_conf.h +++ b/lv_conf.h @@ -43,7 +43,7 @@ extern const lv_font_t lv_font_ratdeck_14; // Fonts - built-in (only 16 still used for titles; 10/12/14 replaced by custom ratdeck fonts) #define LV_FONT_MONTSERRAT_10 0 -#define LV_FONT_MONTSERRAT_12 0 +#define LV_FONT_MONTSERRAT_12 1 // Delivery-status check glyphs in LvMessageView #define LV_FONT_MONTSERRAT_14 0 #define LV_FONT_MONTSERRAT_16 1 #define LV_FONT_UNSCII_8 0 diff --git a/src/reticulum/LXMFManager.cpp b/src/reticulum/LXMFManager.cpp index 10692b4..ed0f83d 100644 --- a/src/reticulum/LXMFManager.cpp +++ b/src/reticulum/LXMFManager.cpp @@ -5,6 +5,7 @@ #include LXMFManager* LXMFManager::_instance = nullptr; +std::map LXMFManager::_pendingProofs; bool LXMFManager::begin(ReticulumManager* rns, MessageStore* store) { _rns = rns; _store = store; _instance = this; @@ -173,9 +174,14 @@ bool LXMFManager::sendDirect(LXMFMessage& msg) { (int)linkBytes.size(), msg.destHash.toHex().substr(0, 8).c_str()); RNS::Packet packet(_outLink, linkBytes); RNS::PacketReceipt receipt = packet.send(); - if (receipt) { sent = true; } + if (receipt) { sent = true; registerProofTracking(receipt, msg); } } else { - // Too large for single packet — use Resource transfer (chunked) + // Too large for single packet — use Resource transfer (chunked). + // No DELIVERED transition for this path: microReticulum's Transport + // skips receipt generation for RESOURCE-context packets, and + // Link::start_resource_transfer doesn't surface a concluded + // callback. Message stays at SENT. lxmf-py wires this via + // Resource.set_callback — port that when the lib exposes it. Serial.printf("[LXMF] sending via link resource: %d bytes to %s\n", (int)linkBytes.size(), msg.destHash.toHex().substr(0, 8).c_str()); if (_outLink.start_resource_transfer(linkBytes)) { @@ -200,7 +206,7 @@ bool LXMFManager::sendDirect(LXMFMessage& msg) { (int)payloadBytes.size(), outDest.hash().toHex().substr(0, 12).c_str()); RNS::Packet packet(outDest, payloadBytes); RNS::PacketReceipt receipt = packet.send(); - if (receipt) { sent = true; } + if (receipt) { sent = true; registerProofTracking(receipt, msg); } } else { // Too large for single frame — need link + resource transfer Serial.printf("[LXMF] Message needs link delivery (%d bytes > %d single-frame), retry %d\n", @@ -355,3 +361,38 @@ const ConversationSummary* LXMFManager::getConversationSummary(const std::string void LXMFManager::markRead(const std::string& peerHex) { if (_store) { _store->markConversationRead(peerHex); } } + +void LXMFManager::registerProofTracking(RNS::PacketReceipt& receipt, const LXMFMessage& msg) { + if (!receipt) return; + std::string rh = receipt.hash().toHex(); + _pendingProofs[rh] = { msg.destHash.toHex(), msg.timestamp }; + receipt.set_delivery_callback(&LXMFManager::onProofDelivered); + receipt.set_timeout(60); + receipt.set_timeout_callback(&LXMFManager::onProofTimeout); +} + +void LXMFManager::onProofDelivered(const RNS::PacketReceipt& r) { + if (!_instance) return; + std::string rh = r.hash().toHex(); + auto it = _pendingProofs.find(rh); + if (it == _pendingProofs.end()) return; + PendingProof p = it->second; + _pendingProofs.erase(it); + + if (_instance->_store) { + _instance->_store->updateMessageStatus(p.peerHex, p.timestamp, false, LXMFStatus::DELIVERED); + } + if (_instance->_statusCb) { + _instance->_statusCb(p.peerHex, p.timestamp, LXMFStatus::DELIVERED); + } + Serial.printf("[LXMF] DELIVERED proof for %s @ %.0f\n", + p.peerHex.substr(0, 12).c_str(), p.timestamp); +} + +void LXMFManager::onProofTimeout(const RNS::PacketReceipt& r) { + // No proof within window — leave status as SENT (over LoRa, lack of + // proof doesn't mean undelivered). Just clear the pending entry so the + // map doesn't leak. + if (!_instance) return; + _pendingProofs.erase(r.hash().toHex()); +} diff --git a/src/reticulum/LXMFManager.h b/src/reticulum/LXMFManager.h index 3ded508..99b622e 100644 --- a/src/reticulum/LXMFManager.h +++ b/src/reticulum/LXMFManager.h @@ -10,6 +10,7 @@ #include #include #include +#include class LXMFManager { public: @@ -53,5 +54,17 @@ private: std::set _seenMessageIds; static constexpr int MAX_SEEN_IDS = 100; + // Outstanding delivery proofs — keyed by receipt hash hex. The recipient + // returns a PROOF packet over RNS for opportunistic and link single + // packets; when it arrives, we flip the message to DELIVERED. + struct PendingProof { + std::string peerHex; + double timestamp; + }; + static std::map _pendingProofs; + static void onProofDelivered(const RNS::PacketReceipt& r); + static void onProofTimeout(const RNS::PacketReceipt& r); + void registerProofTracking(RNS::PacketReceipt& receipt, const LXMFMessage& msg); + static LXMFManager* _instance; }; diff --git a/src/ui/screens/LvMessageView.cpp b/src/ui/screens/LvMessageView.cpp index f10d561..d1154e0 100644 --- a/src/ui/screens/LvMessageView.cpp +++ b/src/ui/screens/LvMessageView.cpp @@ -123,13 +123,13 @@ void LvMessageView::onEnter() { if (_ui) _ui->lvTabBar().setUnreadCount(LvTabBar::TAB_MSGS, _lxmf->unreadCount()); // Register status callback — partial update without full rebuild std::string peer = _peerHex; - _lxmf->setStatusCallback([this, peer](const std::string& peerHex, double, LXMFStatus newStatus) { + _lxmf->setStatusCallback([this, peer](const std::string& peerHex, double ts, LXMFStatus newStatus) { if (peerHex != peer) return; for (int i = _cachedMsgs.size() - 1; i >= 0; i--) { - if (!_cachedMsgs[i].incoming && _cachedMsgs[i].status == LXMFStatus::QUEUED) { + if (!_cachedMsgs[i].incoming && fabs(_cachedMsgs[i].timestamp - ts) < 1.0) { _cachedMsgs[i].status = newStatus; updateMessageStatus(i, newStatus); - break; + return; } } }); @@ -246,20 +246,14 @@ void LvMessageView::appendMessage(const LXMFMessage& msg) { lv_obj_set_width(lbl, maxBubbleW - 14); lv_label_set_text(lbl, msg.content.c_str()); - // Status indicator for outgoing (tracked for partial updates) + // Status indicator for outgoing (tracked for partial updates). + // Glyph + colour come from applyStatusGlyph so single-check vs + // double-check rendering stays in one place. if (!msg.incoming) { - const char* ind = "~"; - uint32_t indColor = Theme::TEXT_MUTED; - if (msg.status == LXMFStatus::SENT || msg.status == LXMFStatus::DELIVERED) { - ind = "*"; indColor = Theme::SUCCESS; - } else if (msg.status == LXMFStatus::FAILED) { - ind = "!"; indColor = Theme::ERROR_CLR; - } lv_obj_t* statusLbl = lv_label_create(box); - lv_obj_set_style_text_font(statusLbl, &lv_font_ratdeck_10, 0); - lv_obj_set_style_text_color(statusLbl, lv_color_hex(indColor), 0); - lv_label_set_text(statusLbl, ind); - lv_obj_align(statusLbl, LV_ALIGN_BOTTOM_RIGHT, 0, 0); + lv_obj_set_style_text_font(statusLbl, &lv_font_montserrat_12, 0); + lv_obj_align(statusLbl, LV_ALIGN_BOTTOM_RIGHT, 0, 1); + applyStatusGlyph(statusLbl, msg.status); _statusLabels.push_back(statusLbl); _textLabels.push_back(lbl); } else { @@ -314,16 +308,7 @@ void LvMessageView::updateMessageStatus(int msgIdx, LXMFStatus status) { lv_obj_t* textLbl = _textLabels[msgIdx]; if (!statusLbl) return; // Incoming message, no status label - // Update status indicator - const char* ind = "~"; - uint32_t indColor = Theme::TEXT_MUTED; - if (status == LXMFStatus::SENT || status == LXMFStatus::DELIVERED) { - ind = "*"; indColor = Theme::SUCCESS; - } else if (status == LXMFStatus::FAILED) { - ind = "!"; indColor = Theme::ERROR_CLR; - } - lv_obj_set_style_text_color(statusLbl, lv_color_hex(indColor), 0); - lv_label_set_text(statusLbl, ind); + applyStatusGlyph(statusLbl, status); // Update text color to match status if (textLbl) { @@ -337,6 +322,34 @@ void LvMessageView::updateMessageStatus(int msgIdx, LXMFStatus status) { } } +void LvMessageView::applyStatusGlyph(lv_obj_t* lbl, LXMFStatus status) { + if (!lbl) return; + const char* glyph; + uint32_t color; + switch (status) { + case LXMFStatus::DELIVERED: + glyph = LV_SYMBOL_OK LV_SYMBOL_OK; // Double check + color = Theme::SUCCESS; + break; + case LXMFStatus::SENT: + glyph = LV_SYMBOL_OK; // Single check + color = Theme::TEXT_MUTED; + break; + case LXMFStatus::FAILED: + glyph = LV_SYMBOL_WARNING; + color = Theme::ERROR_CLR; + break; + case LXMFStatus::QUEUED: + case LXMFStatus::SENDING: + default: + glyph = LV_SYMBOL_REFRESH; // In-flight + color = Theme::TEXT_MUTED; + break; + } + lv_label_set_text(lbl, glyph); + lv_obj_set_style_text_color(lbl, lv_color_hex(color), 0); +} + void LvMessageView::sendCurrentMessage() { if (!_lxmf || _peerHex.empty() || _inputText.empty()) return; diff --git a/src/ui/screens/LvMessageView.h b/src/ui/screens/LvMessageView.h index d575883..6663f56 100644 --- a/src/ui/screens/LvMessageView.h +++ b/src/ui/screens/LvMessageView.h @@ -45,6 +45,7 @@ private: std::vector _cachedMsgs; void updateMessageStatus(int msgIdx, LXMFStatus status); + static void applyStatusGlyph(lv_obj_t* lbl, LXMFStatus status); // LVGL widgets lv_obj_t* _header = nullptr;