roomserver additions stage 2 and dm ui functionality updates

This commit is contained in:
pelgraine
2026-03-22 10:51:59 +11:00
parent c2840a43aa
commit 0bf2826110
6 changed files with 103 additions and 32 deletions
+7
View File
@@ -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);
+40 -11
View File
@@ -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);
+20 -10
View File
@@ -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 {
@@ -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;
+34 -11
View File
@@ -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;
}
+1
View File
@@ -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;