From 0bf2826110d173988b7a69748fbade7156170d1f Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Sun, 22 Mar 2026 10:51:59 +1100 Subject: [PATCH] roomserver additions stage 2 and dm ui functionality updates --- examples/companion_radio/MyMesh.cpp | 7 +++ examples/companion_radio/main.cpp | 51 +++++++++++++++---- .../companion_radio/ui-new/ChannelScreen.h | 30 +++++++---- .../ui-new/Repeateradminscreen.h | 1 + examples/companion_radio/ui-new/UITask.cpp | 45 ++++++++++++---- examples/companion_radio/ui-new/UITask.h | 1 + 6 files changed, 103 insertions(+), 32 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index cf4ed245..b3b12b89 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -737,6 +737,13 @@ bool MyMesh::uiLoginToRepeater(uint32_t contact_idx, const char* password, uint3 uint8_t save_path_len = recipient->out_path_len; recipient->out_path_len = OUT_PATH_UNKNOWN; + // For room servers: reset sync_since to zero so the server pushes ALL posts. + // The device has no persistent DM storage, so every session needs full history. + // sync_since naturally updates as messages arrive (BaseChatMesh::onPeerDataRecv). + if (recipient->type == ADV_TYPE_ROOM) { + recipient->sync_since = 0; + } + Serial.printf("[uiLogin] Sending login to '%s' (idx=%d, path was 0x%02X, now 0x%02X, hash_mode=%d)\n", recipient->name, contact_idx, save_path_len, recipient->out_path_len, _prefs.path_hash_mode); diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 3459c5cb..35cbb15f 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -2369,17 +2369,29 @@ void handleKeyboardInput() { if (key == '\r') { // Enter - send the message Serial.println("Compose: Enter pressed, sending..."); + bool composeWasSent = false; if (composePos > 0) { sendComposedMessage(); + composeWasSent = true; // sendComposedMessage shows its own alert } bool wasDM = composeDM; + int savedDMIdx = composeDMContactIdx; + char savedDMName[32]; + if (wasDM) strncpy(savedDMName, composeDMName, sizeof(savedDMName)); composeMode = false; emojiPickerMode = false; composeDM = false; composeDMContactIdx = -1; composeBuffer[0] = '\0'; composePos = 0; - if (wasDM) { + if (wasDM && savedDMIdx >= 0) { + // Return to DM conversation to see sent message + ChannelScreen* chScr = (ChannelScreen*)ui_task.getChannelScreen(); + uint8_t savedPerms = (chScr && chScr->isDMConversation()) ? chScr->getDMContactPerms() : 0; + ui_task.gotoDMConversation(savedDMName, savedDMIdx, savedPerms); + // Re-show alert after navigation (setCurrScreen clears prior alerts) + if (composeWasSent) ui_task.showAlert("DM sent!", 1500); + } else if (wasDM) { ui_task.gotoContactsScreen(); } else { ui_task.gotoChannelScreen(); @@ -2393,13 +2405,20 @@ void handleKeyboardInput() { // Shift+Backspace = Cancel (works anytime) Serial.println("Compose: Shift+Backspace, cancelling..."); bool wasDM = composeDM; + int savedDMIdx = composeDMContactIdx; + char savedDMName[32]; + if (wasDM) strncpy(savedDMName, composeDMName, sizeof(savedDMName)); composeMode = false; emojiPickerMode = false; composeDM = false; composeDMContactIdx = -1; composeBuffer[0] = '\0'; composePos = 0; - if (wasDM) { + if (wasDM && savedDMIdx >= 0) { + ChannelScreen* chScr = (ChannelScreen*)ui_task.getChannelScreen(); + uint8_t savedPerms = (chScr && chScr->isDMConversation()) ? chScr->getDMContactPerms() : 0; + ui_task.gotoDMConversation(savedDMName, savedDMIdx, savedPerms); + } else if (wasDM) { ui_task.gotoContactsScreen(); } else { ui_task.gotoChannelScreen(); @@ -2699,22 +2718,34 @@ void handleKeyboardInput() { RepeaterAdminScreen::AdminState astate = admin->getState(); bool shiftDel = (key == '\b' && keyboard.wasShiftConsumed()); + // Helper: exit admin — room servers go to DM conversation if logged in, otherwise contacts + auto exitAdmin = [&]() { + int cidx = admin->getContactIdx(); + uint8_t perms = admin->getPermissions() & 0x03; + ContactInfo ci; + if (cidx >= 0 && perms > 0 && the_mesh.getContactByIdx(cidx, ci) && ci.type == ADV_TYPE_ROOM) { + ui_task.gotoDMConversation(ci.name, cidx, perms); + Serial.printf("Nav: Admin -> conversation for %s\n", ci.name); + } else { + ui_task.gotoContactsScreen(); + Serial.println("Nav: Admin -> contacts"); + } + }; + // In password entry: Shift+Del exits, all other keys pass through normally if (astate == RepeaterAdminScreen::STATE_PASSWORD_ENTRY) { if (shiftDel) { - Serial.println("Nav: Back to contacts from admin login"); - ui_task.gotoContactsScreen(); + exitAdmin(); } else { ui_task.injectKey(key); } return; } - // In category menu (top level): Shift+Del exits to contacts, C opens compose + // In category menu (top level): Shift+Del exits, C opens compose if (astate == RepeaterAdminScreen::STATE_CATEGORY_MENU) { if (shiftDel) { - Serial.println("Nav: Back to contacts from admin menu"); - ui_task.gotoContactsScreen(); + exitAdmin(); return; } // C key: allow entering compose mode from admin menu @@ -3270,10 +3301,6 @@ void handleKeyboardInput() { // L = Login/Admin — from DM conversation, open repeater admin with auto-login if (ui_task.isOnChannelScreen()) { ChannelScreen* chScr = (ChannelScreen*)ui_task.getChannelScreen(); - Serial.printf("[L key] onChannelScreen=1, isDMConv=%d, perms=%d, cidx=%d\n", - chScr ? chScr->isDMConversation() : -1, - chScr ? chScr->getDMContactPerms() : -1, - chScr ? chScr->getDMContactIdx() : -1); if (chScr && chScr->isDMConversation() && chScr->getDMContactPerms() > 0) { int cidx = chScr->getDMContactIdx(); if (cidx >= 0) { @@ -3515,6 +3542,8 @@ void sendComposedMessage() { // Direct message to a specific contact if (composeDMContactIdx >= 0) { if (the_mesh.uiSendDirectMessage((uint32_t)composeDMContactIdx, utf8Buf)) { + // Add to channel screen so sent DM appears in conversation view + ui_task.addSentDM(composeDMName, the_mesh.getNodePrefs()->node_name, utf8Buf); ui_task.showAlert("DM sent!", 1500); } else { ui_task.showAlert("DM failed!", 1500); diff --git a/examples/companion_radio/ui-new/ChannelScreen.h b/examples/companion_radio/ui-new/ChannelScreen.h index 3d744242..d506c790 100644 --- a/examples/companion_radio/ui-new/ChannelScreen.h +++ b/examples/companion_radio/ui-new/ChannelScreen.h @@ -59,11 +59,19 @@ public: uint8_t path_len; uint8_t channel_idx; // Which channel this message belongs to int8_t snr; // Receive SNR × 4 (0 if locally sent or unknown) + uint32_t dm_peer_hash; // DM peer name hash (for conversation filtering) uint8_t path[MSG_PATH_MAX]; // Repeater hop hashes char text[CHANNEL_MSG_TEXT_LEN]; bool valid; }; + // Simple hash for DM peer matching + static uint32_t peerHash(const char* s) { + uint32_t h = 5381; + while (*s) { h = ((h << 5) + h) ^ (uint8_t)*s++; } + return h; + } + private: UITask* _task; mesh::RTCClock* _rtc; @@ -100,13 +108,10 @@ private: if (_viewChannelIdx != 0xFF) { return msg.channel_idx == _viewChannelIdx; } - // DM tab in conversation mode: filter by sender name + // DM tab in conversation mode: filter by peer hash if (!_dmInboxMode && _dmFilterName[0] != '\0') { if (msg.channel_idx != 0xFF) return false; - int nameLen = strlen(_dmFilterName); - if (strncmp(msg.text, _dmFilterName, nameLen) != 0) return false; - if (msg.text[nameLen] != ':') return false; - return true; + return msg.dm_peer_hash == peerHash(_dmFilterName); } // Inbox mode or no filter — match all DMs return msg.channel_idx == 0xFF; @@ -127,6 +132,7 @@ public: // Initialize all messages as invalid for (int i = 0; i < CHANNEL_MSG_HISTORY_SIZE; i++) { _messages[i].valid = false; + _messages[i].dm_peer_hash = 0; memset(_messages[i].path, 0, MSG_PATH_MAX); } // Initialize unread counts @@ -136,8 +142,9 @@ public: void setSDReady(bool ready) { _sdReady = ready; } // Add a new message to the history + // peer_name: for DMs, the contact this message belongs to (sender for received, recipient for sent) void addMessage(uint8_t channel_idx, uint8_t path_len, const char* sender, const char* text, - const uint8_t* path_bytes = nullptr, int8_t snr = 0) { + const uint8_t* path_bytes = nullptr, int8_t snr = 0, const char* peer_name = nullptr) { // Move to next slot in circular buffer _newestIdx = (_newestIdx + 1) % CHANNEL_MSG_HISTORY_SIZE; @@ -148,6 +155,13 @@ public: msg->snr = snr; msg->valid = true; + // Set DM peer hash for conversation filtering + if (channel_idx == 0xFF) { + msg->dm_peer_hash = peerHash(peer_name ? peer_name : sender); + } else { + msg->dm_peer_hash = 0; + } + // Store path hop hashes memset(msg->path, 0, MSG_PATH_MAX); if (path_bytes && path_len > 0 && path_len != 0xFF) { @@ -226,8 +240,6 @@ public: _dmContactIdx = contactIdx; _dmContactPerms = perms; _scrollPos = 0; - Serial.printf("[ChannelScreen] openConversation: name=%s, idx=%d, perms=%d\n", - contactName, contactIdx, perms); } int getDMContactIdx() const { return _dmContactIdx; } @@ -1376,8 +1388,6 @@ public: display.setCursor(display.width() - display.getTextWidth(rightText) - 2, footerY); display.print(rightText); } else if (_viewChannelIdx == 0xFF) { - Serial.printf("[DM Footer] perms=%d, contactIdx=%d, inboxMode=%d\n", - _dmContactPerms, _dmContactIdx, _dmInboxMode); if (_dmContactPerms > 0) { display.print("Q:Exit L:Admin"); } else { diff --git a/examples/companion_radio/ui-new/Repeateradminscreen.h b/examples/companion_radio/ui-new/Repeateradminscreen.h index dcf43193..41c37ef4 100644 --- a/examples/companion_radio/ui-new/Repeateradminscreen.h +++ b/examples/companion_radio/ui-new/Repeateradminscreen.h @@ -475,6 +475,7 @@ public: int getContactIdx() const { return _contactIdx; } AdminState getState() const { return _state; } + uint8_t getPermissions() const { return _permissions; } void onLoginResult(bool success, uint8_t permissions, uint32_t server_time) { _waitingForLogin = false; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 59a99a9e..3b363730 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1287,7 +1287,7 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i if (_dedup[i].name_hash == nameH && _dedup[i].text_hash == textH && (now - _dedup[i].millis) < MSG_DEDUP_WINDOW_MS) { // Duplicate — suppress UI notification but still queued for BLE sync - Serial.printf("[Dedup] Suppressed duplicate from %s\n", from_name); + Serial.println("[Dedup] Suppressed duplicate"); return; } } @@ -1963,12 +1963,26 @@ void UITask::onVKBSubmit() { case VKB_DM: { if (strlen(text) == 0) break; + bool dmSuccess = false; if (the_mesh.uiSendDirectMessage((uint32_t)idx, text)) { - showAlert("DM sent!", 1500); - } else { - showAlert("DM failed!", 1500); + // Add to channel screen so sent DM appears in conversation view + ContactInfo dmRecipient; + if (the_mesh.getContactByIdx(idx, dmRecipient)) { + addSentDM(dmRecipient.name, the_mesh.getNodePrefs()->node_name, text); + } + dmSuccess = true; } - if (_screenBeforeVKB) setCurrScreen(_screenBeforeVKB); + // Return to DM conversation if we have contact info + ContactInfo dmContact; + if (the_mesh.getContactByIdx(idx, dmContact)) { + ChannelScreen* cs = (ChannelScreen*)channel_screen; + uint8_t savedPerms = (cs && cs->isDMConversation()) ? cs->getDMContactPerms() : 0; + gotoDMConversation(dmContact.name, idx, savedPerms); + } else if (_screenBeforeVKB) { + setCurrScreen(_screenBeforeVKB); + } + // Show alert AFTER navigation (setCurrScreen clears prior alerts) + showAlert(dmSuccess ? "DM sent!" : "DM failed!", 1500); break; } case VKB_ADMIN_PASSWORD: { @@ -2219,11 +2233,14 @@ bool UITask::isHomeOnRecentPage() const { } void UITask::gotoChannelScreen() { - ((ChannelScreen *) channel_screen)->resetScroll(); + ChannelScreen* cs = (ChannelScreen*)channel_screen; + // If currently showing DM view, reset to channel 0 + if (cs->getViewChannelIdx() == 0xFF) { + cs->setViewChannelIdx(0); + } + cs->resetScroll(); // Mark the currently viewed channel as read - ((ChannelScreen *) channel_screen)->markChannelRead( - ((ChannelScreen *) channel_screen)->getViewChannelIdx() - ); + cs->markChannelRead(cs->getViewChannelIdx()); setCurrScreen(channel_screen); if (_display != NULL && !_display->isOn()) { _display->turnOn(); @@ -2362,6 +2379,14 @@ void UITask::addSentChannelMessage(uint8_t channel_idx, const char* sender, cons ((ChannelScreen *) channel_screen)->addMessage(channel_idx, 0, sender, formattedMsg); } +void UITask::addSentDM(const char* recipientName, const char* sender, const char* text) { + // Format as "Sender: message" and tag with recipient's peer hash + char formattedMsg[CHANNEL_MSG_TEXT_LEN]; + snprintf(formattedMsg, sizeof(formattedMsg), "%s: %s", sender, text); + ((ChannelScreen *) channel_screen)->addMessage(0xFF, 0, sender, formattedMsg, + nullptr, 0, recipientName); +} + void UITask::markChannelReadFromBLE(uint8_t channel_idx) { ((ChannelScreen *) channel_screen)->markChannelRead(channel_idx); // If clearing DMs, also zero all per-contact DM counts @@ -2505,8 +2530,6 @@ void UITask::onAdminLoginResult(bool success, uint8_t permissions, uint32_t serv ContactInfo contact; if (the_mesh.getContactByIdx(cidx, contact) && contact.type == ADV_TYPE_ROOM) { uint8_t maskedPerms = permissions & 0x03; - Serial.printf("[Admin] Room login (raw=%d, masked=%d) — opening conversation for %s\n", - permissions, maskedPerms, contact.name); gotoDMConversation(contact.name, cidx, maskedPerms); return; } diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 25aed75d..8d95cdbd 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -262,6 +262,7 @@ public: // Add a sent message to the channel screen history void addSentChannelMessage(uint8_t channel_idx, const char* sender, const char* text) override; + void addSentDM(const char* recipientName, const char* sender, const char* text); // Mark channel as read when BLE companion app syncs messages void markChannelReadFromBLE(uint8_t channel_idx) override;