feat(mesh): mirror app-originated DM/channel sends into on-device history

CMD_SEND_TXT_MSG and CMD_SEND_CHANNEL_TXT_MSG (the phone app's send path)
transmitted over the mesh but never touched the device's own MessagesScreen
history, unlike a message composed on-device (MessagesScreen::afterSend) --
so a DM/channel post sent from the app was invisible if that same
conversation was later opened on the device's own screen. Both handlers now
also call into the same history-store entry points incoming messages use.

Also wires up delivery-status parity with an on-device send, not just the
raw text:
- Channels: arms the existing "relayed into mesh" repeater-echo tracker
  (trackRelaySend()/armChannelRelay()) on the new entry -- sendGroupMessage
  already runs that tracker regardless of who originated the send, this
  just attaches it to the right history entry. Required threading a ring
  position back out through AbstractUITask::addChannelMsg (now returns int)
  and a new armChannelRelay() passthrough.
- DMs: addDMMsg gained ack_tag/ack_deadline_ms/resends params (threaded
  through MessageHistory -> MessagesScreen -> AbstractUITask/UITask) so an
  app-sent DM gets the same pending -> \xe2\x9c\x93/\xe2\x9c\x97 status the on-device compose
  path shows. resends stays 0 deliberately: the app owns its own retry
  decision, so this only drives the on-screen status, never a second,
  independent auto-resend from the device itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-08-30 17:35:13 +02:00
co-authored by Claude Sonnet 5
parent 1bb28296c2
commit 389f3f7a36
6 changed files with 75 additions and 12 deletions
@@ -212,8 +212,13 @@ public:
_dm_hist[pos].resends_left = (outgoing && ack_tag) ? resends : 0;
}
// ack_tag/ack_deadline_ms/resends let an outgoing DM (e.g. one the phone app
// just sent via CMD_SEND_TXT_MSG) carry the same pending-ACK tracking a
// message composed on-device gets from storeDMMsg() directly — otherwise it
// shows with no delivery status at all. Unused (0) for incoming.
void addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text,
uint32_t sender_timestamp = 0) {
uint32_t sender_timestamp = 0, uint32_t ack_tag = 0,
uint32_t ack_deadline_ms = 0, uint8_t resends = 0) {
// Drop retried copies of an incoming DM: a resend reuses the sender's
// timestamp and text but carries a fresh packet hash, so the mesh dup-filter
// lets it through. Match on prefix + sender_timestamp + text to suppress it.
@@ -225,7 +230,7 @@ public:
return; // duplicate retry — already in history
}
}
storeDMMsg(pub_key, outgoing, text, 0, 0, outgoing ? 0 : sender_timestamp, 0);
storeDMMsg(pub_key, outgoing, text, ack_tag, ack_deadline_ms, sender_timestamp, resends);
}
int dmHistCountForContact(const uint8_t* prefix) const {
@@ -661,10 +661,12 @@ public:
return pos;
}
void markChannelRelayed(uint32_t seq) { _history.markChannelRelayed(seq); }
void armChannelRelay(int pos, uint32_t seq) { _history.armChannelRelay(pos, seq); }
void addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text,
uint32_t sender_timestamp = 0) {
uint32_t sender_timestamp = 0, uint32_t ack_tag = 0,
uint32_t ack_deadline_ms = 0, uint8_t resends = 0) {
bool viewing = (_phase == DM_HIST && memcmp(_sel_contact.id.pub_key, pub_key, 4) == 0);
_history.addDMMsg(pub_key, outgoing, text, sender_timestamp);
_history.addDMMsg(pub_key, outgoing, text, sender_timestamp, ack_tag, ack_deadline_ms, resends);
if (viewing && _dm_hist_sel > 0) { _dm_hist_sel++; _dm_hist_scroll++; } // see addChannelMsg
}
void markDmDelivered(uint32_t ack_crc) { _history.markDmDelivered(ack_crc); }
+9 -4
View File
@@ -1766,9 +1766,13 @@ int UITask::getRecentDMContacts(uint8_t out[][NodePrefs::FAVOURITE_PREFIX_LEN],
return ((MessagesScreen*)messages_screen)->getRecentDMContacts(out, max);
}
void UITask::addChannelMsg(uint8_t channel_idx, const char* text, uint32_t timestamp) {
int UITask::addChannelMsg(uint8_t channel_idx, const char* text, uint32_t timestamp) {
_last_notif_ch_idx = (int)channel_idx;
((MessagesScreen*)messages_screen)->addChannelMsg(channel_idx, text, timestamp);
return ((MessagesScreen*)messages_screen)->addChannelMsg(channel_idx, text, timestamp);
}
void UITask::armChannelRelay(int pos, uint32_t seq) {
((MessagesScreen*)messages_screen)->armChannelRelay(pos, seq);
}
int UITask::getChannelUnreadCount() const {
@@ -1801,8 +1805,9 @@ void UITask::onAdminReply(const uint8_t* pub_key, const char* text) {
_next_refresh = 0; // same reasoning as onRoomLoginResult above
}
void UITask::addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text, uint32_t sender_timestamp) {
((MessagesScreen*)messages_screen)->addDMMsg(pub_key, outgoing, text, sender_timestamp);
void UITask::addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text, uint32_t sender_timestamp,
uint32_t ack_tag, uint32_t ack_deadline_ms, uint8_t resends) {
((MessagesScreen*)messages_screen)->addDMMsg(pub_key, outgoing, text, sender_timestamp, ack_tag, ack_deadline_ms, resends);
}
int UITask::getDMUnreadTotal() const {
+4 -2
View File
@@ -385,8 +385,10 @@ public:
void stopMelody();
bool isMelodyPlaying();
void showAlert(const char* text, int duration_millis);
void addChannelMsg(uint8_t channel_idx, const char* text, uint32_t timestamp = 0) override;
void addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text, uint32_t sender_timestamp = 0) override;
int addChannelMsg(uint8_t channel_idx, const char* text, uint32_t timestamp = 0) override;
void armChannelRelay(int pos, uint32_t seq) override;
void addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text, uint32_t sender_timestamp = 0,
uint32_t ack_tag = 0, uint32_t ack_deadline_ms = 0, uint8_t resends = 0) override;
void onMsgAck(uint32_t ack_crc) override;
void onChannelRelayed(uint32_t seq) override;
void onRoomLoginResult(const uint8_t* pub_key, bool success, uint8_t permissions) override;