From 8f566732ed4dde44f4bf62373696c3197419a11c Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Mon, 18 May 2026 15:00:01 +0200 Subject: [PATCH 1/6] feat: show @recipient as 'To: nick' bar in fullscreen message view When a message starts with @nick, the fullscreen view expands the header to show a second row 'To: nick' below the sender name, and displays the message body without the @nick prefix. Co-Authored-By: Claude Sonnet 4.6 --- .../ui-new/FullscreenMsgView.h | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/examples/companion_radio/ui-new/FullscreenMsgView.h b/examples/companion_radio/ui-new/FullscreenMsgView.h index a5bfe99f..969b801f 100644 --- a/examples/companion_radio/ui-new/FullscreenMsgView.h +++ b/examples/companion_radio/ui-new/FullscreenMsgView.h @@ -42,35 +42,59 @@ struct FullscreenMsgView { int render(DisplayDriver& display, const char* sender, const char* text, bool has_prev, bool has_next) { display.setTextSize(1); + + // detect @recipient at start of message + char to_nick[32] = ""; + const char* body = text; + if (text[0] == '@') { + const char* sp = strchr(text, ' '); + if (sp && sp[1]) { + int len = (int)(sp - text) - 1; + if (len >= (int)sizeof(to_nick)) len = sizeof(to_nick) - 1; + memcpy(to_nick, text + 1, len); + to_nick[len] = '\0'; + body = sp + 1; + } + } + + const int header_h = to_nick[0] ? 20 : 10; + const int startY = header_h + 2; + const int visible = (display.height() - startY - FS_LINE_H) / FS_LINE_H; + display.setColor(DisplayDriver::LIGHT); - display.fillRect(0, 0, display.width(), 10); + display.fillRect(0, 0, display.width(), header_h); display.setColor(DisplayDriver::DARK); display.drawTextEllipsized(2, 1, display.width() - 4, sender); + if (to_nick[0]) { + char to_label[36]; + snprintf(to_label, sizeof(to_label), "To: %s", to_nick); + display.drawTextEllipsized(2, 11, display.width() - 4, to_label); + } display.setColor(DisplayDriver::LIGHT); char trans_text[512]; - display.translateUTF8ToBlocks(trans_text, text, sizeof(trans_text)); + display.translateUTF8ToBlocks(trans_text, body, sizeof(trans_text)); char lines[12][FS_CHARS + 1]; int lcount = wrapLines(trans_text, lines, 12); - int max_scroll = lcount > FS_VISIBLE ? lcount - FS_VISIBLE : 0; + int max_scroll = lcount > visible ? lcount - visible : 0; if (scroll > max_scroll) scroll = max_scroll; - for (int i = 0; i < FS_VISIBLE && (scroll + i) < lcount; i++) { - display.setCursor(0, FS_START_Y + i * FS_LINE_H); + for (int i = 0; i < visible && (scroll + i) < lcount; i++) { + display.setCursor(0, startY + i * FS_LINE_H); display.print(lines[scroll + i]); } if (scroll > 0) { display.setColor(DisplayDriver::DARK); - display.fillRect(display.width() - 6, FS_START_Y, 6, FS_LINE_H); + display.fillRect(display.width() - 6, startY, 6, FS_LINE_H); display.setColor(DisplayDriver::LIGHT); - display.setCursor(display.width() - 6, FS_START_Y); + display.setCursor(display.width() - 6, startY); display.print("^"); } if (scroll < max_scroll) { display.setColor(DisplayDriver::DARK); - display.fillRect(display.width() - 6, FS_START_Y + (FS_VISIBLE - 1) * FS_LINE_H, 6, FS_LINE_H); + display.fillRect(display.width() - 6, startY + (visible - 1) * FS_LINE_H, 6, FS_LINE_H); display.setColor(DisplayDriver::LIGHT); - display.setCursor(display.width() - 6, FS_START_Y + (FS_VISIBLE - 1) * FS_LINE_H); + display.setCursor(display.width() - 6, startY + (visible - 1) * FS_LINE_H); display.print("v"); } if (has_next) { From 6f7de3ed204d4dab209c9e2fa3a374f770b6f910 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Mon, 18 May 2026 15:12:11 +0200 Subject: [PATCH 2/6] feat: add reply action to channel and DM message history MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Long-press Enter (KEY_CONTEXT_MENU) on a selected message shows an 'Options / Reply' popup in both list and fullscreen views. Confirming opens MSG_PICK with 'RE: nick' title — the reply can be sent via keyboard or quick message templates, both automatically prefixed with '@nick '. - Channel: reply prefix extracted from 'sender: message' format - DM: reply available only on incoming messages, uses contact name - FullscreenMsgView: REPLY added to Result enum, KEY_CONTEXT_MENU handled - MSG_PICK: _reply_mode flag routes prefix through keyboard and templates - Guard: prevent sending empty reply (prefix-only keyboard input ignored) Co-Authored-By: Claude Sonnet 4.6 --- .../ui-new/FullscreenMsgView.h | 11 +- .../companion_radio/ui-new/QuickMsgScreen.h | 112 +++++++++++++++++- 2 files changed, 112 insertions(+), 11 deletions(-) diff --git a/examples/companion_radio/ui-new/FullscreenMsgView.h b/examples/companion_radio/ui-new/FullscreenMsgView.h index 969b801f..99582f12 100644 --- a/examples/companion_radio/ui-new/FullscreenMsgView.h +++ b/examples/companion_radio/ui-new/FullscreenMsgView.h @@ -14,7 +14,7 @@ struct FullscreenMsgView { FullscreenMsgView() : scroll(0), active(false) {} - enum Result { NONE, PREV, NEXT, CLOSE }; + enum Result { NONE, PREV, NEXT, CLOSE, REPLY }; void begin() { scroll = 0; active = true; } @@ -109,10 +109,11 @@ struct FullscreenMsgView { } Result handleInput(char c) { - if (c == KEY_UP) { if (scroll > 0) scroll--; return NONE; } - if (c == KEY_DOWN) { scroll++; return NONE; } - if (c == KEY_LEFT) return NEXT; - if (c == KEY_RIGHT) return PREV; + if (c == KEY_UP) { if (scroll > 0) scroll--; return NONE; } + if (c == KEY_DOWN) { scroll++; return NONE; } + if (c == KEY_LEFT) return NEXT; + if (c == KEY_RIGHT) return PREV; + if (c == KEY_CONTEXT_MENU) return REPLY; if (c == KEY_ENTER || c == KEY_CANCEL) return CLOSE; return NONE; } diff --git a/examples/companion_radio/ui-new/QuickMsgScreen.h b/examples/companion_radio/ui-new/QuickMsgScreen.h index e2230605..65803a0a 100644 --- a/examples/companion_radio/ui-new/QuickMsgScreen.h +++ b/examples/companion_radio/ui-new/QuickMsgScreen.h @@ -41,11 +41,13 @@ class QuickMsgScreen : public UIScreen { // KEYBOARD KeyboardWidget _kb; - // Context menu (opened by KEY_CONTEXT_MENU in CHANNEL_PICK / CONTACT_PICK) + // Context menu (opened by KEY_CONTEXT_MENU in CHANNEL_PICK / CONTACT_PICK / histories) PopupMenu _ctx_menu; bool _ctx_dirty; char _ctx_notif_item[22]; char _ctx_melody_item[20]; + char _reply_prefix[36]; // "@nick " built when reply is triggered + bool _reply_mode; // true while composing a reply (prefix is prepended) struct ChHistEntry { uint8_t ch_idx; char text[140]; }; ChHistEntry _hist[CH_HIST_MAX]; @@ -86,6 +88,25 @@ class QuickMsgScreen : public UIScreen { &sensors, batt); } + // Build "@nick " prefix from a channel message text ("nick: body") into _reply_prefix. + // Returns false if sender is "Me" (own message — no reply prefix needed). + bool buildChannelReplyPrefix(const char* text) { + const char* sep = strstr(text, ": "); + if (!sep) return false; + int slen = (int)(sep - text); + if (slen == 2 && strncmp(text, "Me", 2) == 0) return false; + if (slen > 32) slen = 32; + snprintf(_reply_prefix, sizeof(_reply_prefix), "@%.*s ", slen, text); + return true; + } + + void startReply(bool to_channel) { + _sending_to_channel = to_channel; + _reply_mode = true; + setupMsgPick(); + _phase = MSG_PICK; + } + void setupMsgPick() { _msg_sel = _msg_scroll = 0; _active_msg_count = 0; @@ -167,6 +188,7 @@ class QuickMsgScreen : public UIScreen { } void afterSend(bool ok, const char* msg) { + _reply_mode = false; if (ok && _sending_to_channel) { _hist_sel = 0; _hist_scroll = 0; @@ -361,7 +383,7 @@ public: _hist_head(0), _hist_count(0), _dm_hist_head(0), _dm_hist_count(0), _dm_hist_sel(-1), _dm_hist_scroll(0), - _ctx_dirty(false) { + _ctx_dirty(false), _reply_mode(false) { memset(_ch_unread, 0, sizeof(_ch_unread)); } @@ -751,7 +773,9 @@ public: } else { // MSG_PICK char title[24]; - if (_sending_to_channel) { + if (_reply_mode) { + snprintf(title, sizeof(title), "RE:%.20s", _reply_prefix + 1); // skip '@' + } else if (_sending_to_channel) { ChannelDetails ch; the_mesh.getChannel(_sel_channel_idx, ch); snprintf(title, sizeof(title), "%.23s", ch.name); @@ -938,6 +962,16 @@ public: } else if (_phase == DM_HIST) { int dm_count = dmHistCountForContact(_sel_contact.id.pub_key); if (_dm_fs.active) { + if (_ctx_menu.active) { + auto res = _ctx_menu.handleInput(c); + if (res == PopupMenu::SELECTED) { + _dm_fs.active = false; + startReply(false); + } else if (res != PopupMenu::NONE) { + _ctx_menu.active = false; + } + return true; + } auto res = _dm_fs.handleInput(c); if (res == FullscreenMsgView::PREV) { if (_dm_hist_sel < dm_count - 1) { _dm_hist_sel++; _dm_fs.scroll = 0; } @@ -945,6 +979,22 @@ public: if (_dm_hist_sel > 0) { _dm_hist_sel--; _dm_fs.scroll = 0; } } else if (res == FullscreenMsgView::CLOSE) { _dm_fs.active = false; + } else if (res == FullscreenMsgView::REPLY) { + int ring_pos = dmHistEntryForContact(_sel_contact.id.pub_key, _dm_hist_sel); + if (ring_pos >= 0 && !_dm_hist[ring_pos].outgoing) { + snprintf(_reply_prefix, sizeof(_reply_prefix), "@%.32s ", _sel_contact.name); + _ctx_menu.begin("Options", 1); + _ctx_menu.addItem("Reply"); + } + } + return true; + } + if (_ctx_menu.active) { + auto res = _ctx_menu.handleInput(c); + if (res == PopupMenu::SELECTED) { + startReply(false); + } else if (res != PopupMenu::NONE) { + _ctx_menu.active = false; } return true; } @@ -977,10 +1027,29 @@ public: } return true; } + if (c == KEY_CONTEXT_MENU && _dm_hist_sel >= 0) { + int ring_pos = dmHistEntryForContact(_sel_contact.id.pub_key, _dm_hist_sel); + if (ring_pos >= 0 && !_dm_hist[ring_pos].outgoing) { + snprintf(_reply_prefix, sizeof(_reply_prefix), "@%.32s ", _sel_contact.name); + _ctx_menu.begin("Options", 1); + _ctx_menu.addItem("Reply"); + } + return true; + } } else if (_phase == CHANNEL_HIST) { int ch_hist_count = histCountForChannel(_sel_channel_idx); if (_fs.active) { + if (_ctx_menu.active) { + auto res = _ctx_menu.handleInput(c); + if (res == PopupMenu::SELECTED) { + _fs.active = false; + startReply(true); + } else if (res != PopupMenu::NONE) { + _ctx_menu.active = false; + } + return true; + } auto res = _fs.handleInput(c); if (res == FullscreenMsgView::PREV) { if (_hist_sel < ch_hist_count - 1) { _hist_sel++; _fs.scroll = 0; updateChannelUnread(); } @@ -988,6 +1057,21 @@ public: if (_hist_sel > 0) { _hist_sel--; _fs.scroll = 0; updateChannelUnread(); } } else if (res == FullscreenMsgView::CLOSE) { _fs.active = false; + } else if (res == FullscreenMsgView::REPLY) { + int ring_pos = histEntryForChannel(_sel_channel_idx, _hist_sel); + if (ring_pos >= 0 && buildChannelReplyPrefix(_hist[ring_pos].text)) { + _ctx_menu.begin("Options", 1); + _ctx_menu.addItem("Reply"); + } + } + return true; + } + if (_ctx_menu.active) { + auto res = _ctx_menu.handleInput(c); + if (res == PopupMenu::SELECTED) { + startReply(true); + } else if (res != PopupMenu::NONE) { + _ctx_menu.active = false; } return true; } @@ -1018,13 +1102,22 @@ public: } return true; } + if (c == KEY_CONTEXT_MENU && _hist_sel >= 0) { + int ring_pos = histEntryForChannel(_sel_channel_idx, _hist_sel); + if (ring_pos >= 0 && buildChannelReplyPrefix(_hist[ring_pos].text)) { + _ctx_menu.begin("Options", 1); + _ctx_menu.addItem("Reply"); + } + return true; + } } else if (_phase == KEYBOARD) { auto res = _kb.handleInput(c); if (res == KeyboardWidget::CANCELLED) { _phase = MSG_PICK; } else if (res == KeyboardWidget::DONE) { - if (_kb.len > 0) { + int min_len = _reply_mode ? (int)strlen(_reply_prefix) : 0; + if (_kb.len > min_len) { char expanded[KB_MAX_LEN + 1]; expandMsg(_kb.buf, expanded, sizeof(expanded)); bool ok = sendText(expanded); @@ -1036,6 +1129,7 @@ public: } else { // MSG_PICK int total_msg_items = 1 + _active_msg_count; if (c == KEY_CANCEL) { + _reply_mode = false; _phase = _sending_to_channel ? CHANNEL_HIST : DM_HIST; return true; } @@ -1051,7 +1145,7 @@ public: } if (c == KEY_ENTER) { if (_msg_sel == 0) { - _kb.begin(); + _kb.begin(_reply_mode ? _reply_prefix : ""); kbAddSensorPlaceholders(_kb, &sensors); _phase = KEYBOARD; return true; @@ -1060,7 +1154,13 @@ public: int slot = _active_msgs[_msg_sel - 1]; const char* tmpl = p ? p->custom_msgs[slot] : "OK"; char msg[140]; - expandMsg(tmpl, msg, sizeof(msg)); + if (_reply_mode) { + char body[140]; + expandMsg(tmpl, body, sizeof(body)); + snprintf(msg, sizeof(msg), "%s%s", _reply_prefix, body); + } else { + expandMsg(tmpl, msg, sizeof(msg)); + } bool ok = sendText(msg); afterSend(ok, msg); return true; From b21bcb5ca4a50d2183a469b7412ea60209df933d Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Mon, 18 May 2026 16:11:25 +0200 Subject: [PATCH 3/6] fix: render reply popup in CHANNEL_HIST and DM_HIST phases _ctx_menu.render() was only called in CONTACT_PICK and CHANNEL_PICK, so the reply popup was active but invisible in history views. Added render calls for list view and fullscreen view in both DM_HIST and CHANNEL_HIST phases. Co-Authored-By: Claude Sonnet 4.6 --- .../companion_radio/ui-new/QuickMsgScreen.h | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/examples/companion_radio/ui-new/QuickMsgScreen.h b/examples/companion_radio/ui-new/QuickMsgScreen.h index 65803a0a..f2b61aa2 100644 --- a/examples/companion_radio/ui-new/QuickMsgScreen.h +++ b/examples/companion_radio/ui-new/QuickMsgScreen.h @@ -593,9 +593,11 @@ public: const DmHistEntry& e = _dm_hist[ring_pos]; const char* sender = e.outgoing ? "Me" : filtered_name; int dm_count = dmHistCountForContact(_sel_contact.id.pub_key); - return _dm_fs.render(display, sender, e.text, - _dm_hist_sel < dm_count - 1, - _dm_hist_sel > 0); + int ret = _dm_fs.render(display, sender, e.text, + _dm_hist_sel < dm_count - 1, + _dm_hist_sel > 0); + if (_ctx_menu.active) _ctx_menu.render(display); + return ret; } return 500; } @@ -662,6 +664,7 @@ public: display.setCursor(cbx + 2, cby); display.print(ctxt); display.setColor(DisplayDriver::LIGHT); + if (_ctx_menu.active) _ctx_menu.render(display); return dm_count > 0 ? 500 : 2000; } else if (_phase == CHANNEL_HIST) { @@ -680,9 +683,11 @@ public: strncpy(fsender, "?", sizeof(fsender)); strncpy(fmsg, ftext, sizeof(fmsg) - 1); fmsg[sizeof(fmsg)-1] = '\0'; } - return _fs.render(display, fsender, fmsg, - _hist_sel < fs_hist_count - 1, - _hist_sel > 0); + int ret = _fs.render(display, fsender, fmsg, + _hist_sel < fs_hist_count - 1, + _hist_sel > 0); + if (_ctx_menu.active) _ctx_menu.render(display); + return ret; } return 2000; } @@ -767,6 +772,7 @@ public: display.setCursor(cbx + 1, cby); display.print(ctxt); display.setColor(DisplayDriver::LIGHT); + if (_ctx_menu.active) _ctx_menu.render(display); } else if (_phase == KEYBOARD) { return _kb.render(display); From 34cfd27b8a1f2953a62366f3185c296a0688e377 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Mon, 18 May 2026 18:18:45 +0200 Subject: [PATCH 4/6] fix: use @[nick] format for replies and fix fullscreen message display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reply prefix changed to "@[nick] " format — bracket delimiter handles nicks with spaces unambiguously - FullscreenMsgView: parse "@[nick] body" to show "To: nick" header; fall back gracefully if format not matched - Reduce FS_CHARS 21→20 to prevent scroll arrows overlapping last char - Transliterate nick before displaying in "To:" header and "RE:" title so non-ASCII characters render correctly on the OLED font Co-Authored-By: Claude Sonnet 4.6 --- .../ui-new/FullscreenMsgView.h | 19 ++++++++++--------- .../companion_radio/ui-new/QuickMsgScreen.h | 17 ++++++++++++----- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/examples/companion_radio/ui-new/FullscreenMsgView.h b/examples/companion_radio/ui-new/FullscreenMsgView.h index 99582f12..ae32a24a 100644 --- a/examples/companion_radio/ui-new/FullscreenMsgView.h +++ b/examples/companion_radio/ui-new/FullscreenMsgView.h @@ -3,7 +3,7 @@ #include #include -static const int FS_CHARS = 21; +static const int FS_CHARS = 20; static const int FS_LINE_H = 8; static const int FS_START_Y = 12; static const int FS_VISIBLE = (64 - FS_START_Y) / FS_LINE_H; @@ -46,14 +46,14 @@ struct FullscreenMsgView { // detect @recipient at start of message char to_nick[32] = ""; const char* body = text; - if (text[0] == '@') { - const char* sp = strchr(text, ' '); - if (sp && sp[1]) { - int len = (int)(sp - text) - 1; + if (text[0] == '@' && text[1] == '[') { + const char* close = strchr(text + 2, ']'); + if (close && close[1] == ' ' && close[2]) { + int len = (int)(close - text) - 2; if (len >= (int)sizeof(to_nick)) len = sizeof(to_nick) - 1; - memcpy(to_nick, text + 1, len); + memcpy(to_nick, text + 2, len); to_nick[len] = '\0'; - body = sp + 1; + body = close + 2; } } @@ -66,8 +66,9 @@ struct FullscreenMsgView { display.setColor(DisplayDriver::DARK); display.drawTextEllipsized(2, 1, display.width() - 4, sender); if (to_nick[0]) { - char to_label[36]; - snprintf(to_label, sizeof(to_label), "To: %s", to_nick); + char trans_nick[32], to_label[36]; + display.translateUTF8ToBlocks(trans_nick, to_nick, sizeof(trans_nick)); + snprintf(to_label, sizeof(to_label), "To: %s", trans_nick); display.drawTextEllipsized(2, 11, display.width() - 4, to_label); } display.setColor(DisplayDriver::LIGHT); diff --git a/examples/companion_radio/ui-new/QuickMsgScreen.h b/examples/companion_radio/ui-new/QuickMsgScreen.h index f2b61aa2..13632482 100644 --- a/examples/companion_radio/ui-new/QuickMsgScreen.h +++ b/examples/companion_radio/ui-new/QuickMsgScreen.h @@ -46,7 +46,7 @@ class QuickMsgScreen : public UIScreen { bool _ctx_dirty; char _ctx_notif_item[22]; char _ctx_melody_item[20]; - char _reply_prefix[36]; // "@nick " built when reply is triggered + char _reply_prefix[36]; // "@[nick] " built when reply is triggered bool _reply_mode; // true while composing a reply (prefix is prepended) struct ChHistEntry { uint8_t ch_idx; char text[140]; }; @@ -96,7 +96,8 @@ class QuickMsgScreen : public UIScreen { int slen = (int)(sep - text); if (slen == 2 && strncmp(text, "Me", 2) == 0) return false; if (slen > 32) slen = 32; - snprintf(_reply_prefix, sizeof(_reply_prefix), "@%.*s ", slen, text); + if (slen > 31) slen = 31; + snprintf(_reply_prefix, sizeof(_reply_prefix), "@[%.*s] ", slen, text); return true; } @@ -780,7 +781,13 @@ public: } else { // MSG_PICK char title[24]; if (_reply_mode) { - snprintf(title, sizeof(title), "RE:%.20s", _reply_prefix + 1); // skip '@' + int rlen = (int)strlen(_reply_prefix) - 4; // exclude "@[" and "] " + if (rlen < 0) rlen = 0; + if (rlen > 20) rlen = 20; + char nick_raw[32], nick_trans[32]; + snprintf(nick_raw, sizeof(nick_raw), "%.*s", rlen, _reply_prefix + 2); + display.translateUTF8ToBlocks(nick_trans, nick_raw, sizeof(nick_trans)); + snprintf(title, sizeof(title), "RE:%s", nick_trans); } else if (_sending_to_channel) { ChannelDetails ch; the_mesh.getChannel(_sel_channel_idx, ch); @@ -988,7 +995,7 @@ public: } else if (res == FullscreenMsgView::REPLY) { int ring_pos = dmHistEntryForContact(_sel_contact.id.pub_key, _dm_hist_sel); if (ring_pos >= 0 && !_dm_hist[ring_pos].outgoing) { - snprintf(_reply_prefix, sizeof(_reply_prefix), "@%.32s ", _sel_contact.name); + snprintf(_reply_prefix, sizeof(_reply_prefix), "@[%.31s] ", _sel_contact.name); _ctx_menu.begin("Options", 1); _ctx_menu.addItem("Reply"); } @@ -1036,7 +1043,7 @@ public: if (c == KEY_CONTEXT_MENU && _dm_hist_sel >= 0) { int ring_pos = dmHistEntryForContact(_sel_contact.id.pub_key, _dm_hist_sel); if (ring_pos >= 0 && !_dm_hist[ring_pos].outgoing) { - snprintf(_reply_prefix, sizeof(_reply_prefix), "@%.32s ", _sel_contact.name); + snprintf(_reply_prefix, sizeof(_reply_prefix), "@[%.31s] ", _sel_contact.name); _ctx_menu.begin("Options", 1); _ctx_menu.addItem("Reply"); } From 0f5740f6b61877138f93ba628e7cb669e64a27eb Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Mon, 18 May 2026 18:20:42 +0200 Subject: [PATCH 5/6] fix: remove redundant slen clamp and update stale comment Co-Authored-By: Claude Sonnet 4.6 --- examples/companion_radio/ui-new/QuickMsgScreen.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/companion_radio/ui-new/QuickMsgScreen.h b/examples/companion_radio/ui-new/QuickMsgScreen.h index 13632482..ba7ec29e 100644 --- a/examples/companion_radio/ui-new/QuickMsgScreen.h +++ b/examples/companion_radio/ui-new/QuickMsgScreen.h @@ -88,14 +88,13 @@ class QuickMsgScreen : public UIScreen { &sensors, batt); } - // Build "@nick " prefix from a channel message text ("nick: body") into _reply_prefix. + // Build "@[nick] " prefix from a channel message text ("nick: body") into _reply_prefix. // Returns false if sender is "Me" (own message — no reply prefix needed). bool buildChannelReplyPrefix(const char* text) { const char* sep = strstr(text, ": "); if (!sep) return false; int slen = (int)(sep - text); if (slen == 2 && strncmp(text, "Me", 2) == 0) return false; - if (slen > 32) slen = 32; if (slen > 31) slen = 31; snprintf(_reply_prefix, sizeof(_reply_prefix), "@[%.*s] ", slen, text); return true; From 02fd79d5675a595313222ecb412c7a4b6780f0db Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Mon, 18 May 2026 22:33:04 +0200 Subject: [PATCH 6/6] docs: update README with reply feature and screen lock Co-Authored-By: Claude Sonnet 4.6 --- README.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 907d8a92..0fb15770 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ Join the discussion on offical MeshCore discord: https://discord.gg/sdhYArU2jr View and send messages using the on-screen keyboard or predefined quick replies. The keyboard supports placeholders that insert live sensor data — `{time}` and `{loc}` are always available; additional placeholders (`{temp}`, `{hum}`, `{pres}`, `{batt}`, `{alt}`, `{lux}`, `{co2}`) appear automatically for sensors that are connected and returning data. -Press Enter on a message to open it in fullscreen. Navigate between messages with left (newer) and right (older). +Press Enter on a message to open it in fullscreen. Navigate between messages with left (newer) and right (older). If the message is a reply addressed to someone (`@[nick]`), a **To: nick** bar is shown below the sender name and the body is displayed without the address prefix. -Hold Enter on a message or channel to open a context menu: change per-channel notification settings (mute, follow global, or force-on) and per-channel melody override (follow global, Melody 1, or Melody 2), or mark messages as read. +Hold Enter on a message to open a context menu. From the list or fullscreen view, select **Reply** to pre-fill the keyboard or a quick message with `@[nick]` so the recipient is clearly addressed. On channel or contact list entries, the context menu also lets you change per-channel notification settings (mute, follow global, or force-on), per-channel melody override (follow global, Melody 1, or Melody 2), or mark messages as read. ### Settings Screen @@ -34,6 +34,7 @@ All settings are saved to flash and restored on next boot. - **System** - Timezone (UTC offset in hours) - Low battery shutdown threshold + - Auto-lock — automatically locks the device when the display turns off - **GPS** - Position broadcast interval - **Contacts** @@ -48,6 +49,15 @@ A dedicated clock page on the home screen shows the current time and date, synch Up to three configurable data fields are displayed below the clock. Available fields: battery voltage, temperature, humidity, pressure, GPS coordinates, altitude, luminosity, CO₂, contact count, and total unread message count. +### Screen Lock + +Hold **Back** and press **Enter** three times to lock or unlock the device. While locked: + +- The display turns off and ignores incoming keypresses +- A brief button press shows the lock screen: current time, date, and up to two sensor values (reuses the Dashboard Config fields) +- A hint popup guides through the unlock sequence +- **Auto-lock** (configurable in Settings → System) locks automatically when the display turns off + ### Nearby Nodes Browse nodes that have recently advertised on the mesh. Filter by category (Favourites, All, Companion, Repeater, Room, Sensor). Select a node to see its coordinates, distance, bearing with cardinal direction, type, and last-heard time.