mirror of
https://github.com/ratspeak/ratdeck.git
synced 2026-05-15 11:55:13 +00:00
Distinguish sent vs delivered with check-mark indicators
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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <time.h>
|
||||
|
||||
LXMFManager* LXMFManager::_instance = nullptr;
|
||||
std::map<std::string, LXMFManager::PendingProof> 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());
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <functional>
|
||||
#include <deque>
|
||||
#include <set>
|
||||
#include <map>
|
||||
|
||||
class LXMFManager {
|
||||
public:
|
||||
@@ -53,5 +54,17 @@ private:
|
||||
std::set<std::string> _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<std::string, PendingProof> _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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ private:
|
||||
std::vector<LXMFMessage> _cachedMsgs;
|
||||
|
||||
void updateMessageStatus(int msgIdx, LXMFStatus status);
|
||||
static void applyStatusGlyph(lv_obj_t* lbl, LXMFStatus status);
|
||||
|
||||
// LVGL widgets
|
||||
lv_obj_t* _header = nullptr;
|
||||
|
||||
Reference in New Issue
Block a user