2026-05-17 00:12:40 +02:00
|
|
|
#pragma once
|
|
|
|
|
// Custom screen — not part of upstream UITask.cpp
|
|
|
|
|
// Included by UITask.cpp after SettingsScreen.h is defined.
|
|
|
|
|
|
feat(nav): navigate to / save a location shared in a message
Locations already travel as plain text ({loc} expands to "lat,lon"), so any
message carrying coordinates can now be acted on. In the fullscreen message
view, the context-menu (Hold Enter) gains Navigate and Save waypoint
entries whenever the message contains a coordinate — alongside Reply.
- geo::parseLatLon: shared scanner for an embedded "lat,lon" (the {loc}
format), with an optional [WAY] tag that supplies a label. Range-checked,
requires a decimal point to avoid matching plain integer pairs. Will also
back the waypoint-share feature later.
- QuickMsgScreen: Navigate opens NavView inline (To/Hdg/distance, honouring
the global Units setting) over the message, back returns to it; Save
waypoint stores the point (label from [WAY], else auto-named). Works in
both DM and channel histories, and on incoming or outgoing messages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:28:17 +02:00
|
|
|
#include "NavView.h" // navigate to a location shared inside a message
|
|
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
class QuickMsgScreen : public UIScreen {
|
|
|
|
|
UITask* _task;
|
|
|
|
|
|
|
|
|
|
enum Phase { MODE_SELECT, CONTACT_PICK, DM_HIST, MSG_PICK, CHANNEL_PICK, CHANNEL_HIST, KEYBOARD };
|
|
|
|
|
Phase _phase;
|
|
|
|
|
|
|
|
|
|
// MODE_SELECT
|
|
|
|
|
int _mode_sel; // 0=Direct, 1=Channel, 2=Room Servers
|
|
|
|
|
|
|
|
|
|
// CONTACT_PICK
|
|
|
|
|
int _contact_sel, _contact_scroll;
|
|
|
|
|
int _num_contacts;
|
|
|
|
|
uint16_t _sorted[MAX_CONTACTS];
|
|
|
|
|
ContactInfo _sel_contact;
|
|
|
|
|
bool _room_mode; // true = picking a room server, false = picking a DM contact
|
|
|
|
|
|
|
|
|
|
// CHANNEL_PICK
|
|
|
|
|
int _channel_sel, _channel_scroll;
|
|
|
|
|
int _num_channels;
|
|
|
|
|
uint8_t _channel_indices[MAX_GROUP_CHANNELS];
|
|
|
|
|
uint8_t _ch_unread[MAX_GROUP_CHANNELS];
|
|
|
|
|
int _sel_channel_idx;
|
|
|
|
|
bool _sending_to_channel;
|
|
|
|
|
|
|
|
|
|
// MSG_PICK (shared)
|
|
|
|
|
int _msg_sel, _msg_scroll;
|
|
|
|
|
int _active_msgs[QUICK_MSGS_MAX];
|
|
|
|
|
int _active_msg_count;
|
|
|
|
|
|
|
|
|
|
// CHANNEL_HIST
|
|
|
|
|
int _hist_sel, _hist_scroll;
|
|
|
|
|
FullscreenMsgView _fs;
|
|
|
|
|
int _unread_at_entry; // _ch_unread value when entering CHANNEL_HIST
|
|
|
|
|
int _viewing_max_seen; // highest _hist_sel reached in current session
|
2026-05-31 16:23:07 +02:00
|
|
|
// Shared ring for all channels combined. Sized to comfortably exceed
|
|
|
|
|
// typical inter-visit accumulation; addChannelMsg() decrements the
|
|
|
|
|
// matching _ch_unread when an entry is evicted so the badge can't claim
|
|
|
|
|
// unread messages that no longer exist in the ring.
|
|
|
|
|
static const int CH_HIST_MAX = 96;
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
// KEYBOARD
|
|
|
|
|
KeyboardWidget _kb;
|
|
|
|
|
|
2026-05-18 15:12:11 +02:00
|
|
|
// Context menu (opened by KEY_CONTEXT_MENU in CHANNEL_PICK / CONTACT_PICK / histories)
|
2026-05-17 00:12:40 +02:00
|
|
|
PopupMenu _ctx_menu;
|
|
|
|
|
bool _ctx_dirty;
|
|
|
|
|
char _ctx_notif_item[22];
|
|
|
|
|
char _ctx_melody_item[20];
|
2026-05-24 23:30:16 +02:00
|
|
|
char _ctx_pin_item[28]; // "Pin to dial" or "Unpin (slot N)"
|
2026-05-27 17:33:52 +02:00
|
|
|
char _ctx_ch_fav_item[12]; // "Fav" or "Unfav"
|
2026-05-24 23:30:16 +02:00
|
|
|
char _pin_slot_labels[NodePrefs::FAVOURITES_COUNT][22]; // per-slot picker labels
|
|
|
|
|
bool _pin_picker_active; // true while the slot-picker submenu is open
|
2026-05-25 07:58:45 +02:00
|
|
|
bool _dm_direct_entry; // entered DM_HIST via Favourites shortcut; CANCEL returns home
|
2026-05-18 18:18:45 +02:00
|
|
|
char _reply_prefix[36]; // "@[nick] " built when reply is triggered
|
2026-05-18 15:12:11 +02:00
|
|
|
bool _reply_mode; // true while composing a reply (prefix is prepended)
|
2026-05-17 00:12:40 +02:00
|
|
|
|
feat(nav): navigate to / save a location shared in a message
Locations already travel as plain text ({loc} expands to "lat,lon"), so any
message carrying coordinates can now be acted on. In the fullscreen message
view, the context-menu (Hold Enter) gains Navigate and Save waypoint
entries whenever the message contains a coordinate — alongside Reply.
- geo::parseLatLon: shared scanner for an embedded "lat,lon" (the {loc}
format), with an optional [WAY] tag that supplies a label. Range-checked,
requires a decimal point to avoid matching plain integer pairs. Will also
back the waypoint-share feature later.
- QuickMsgScreen: Navigate opens NavView inline (To/Hdg/distance, honouring
the global Units setting) over the message, back returns to it; Save
waypoint stores the point (label from [WAY], else auto-named). Works in
both DM and channel histories, and on incoming or outgoing messages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:28:17 +02:00
|
|
|
// Fullscreen-message context menu actions. Built per-message: Reply (when
|
|
|
|
|
// applicable) plus Navigate / Save waypoint when the message carries a
|
|
|
|
|
// location (a {loc} string or a [WAY] share). _fs_act maps each visible row
|
|
|
|
|
// back to an action so the index math survives the conditional layout.
|
|
|
|
|
enum FsAct : uint8_t { FS_REPLY, FS_NAV, FS_SAVE };
|
|
|
|
|
uint8_t _fs_act[3];
|
|
|
|
|
int _fs_act_n = 0;
|
|
|
|
|
// Inline navigate-to-location view layered over the fullscreen message.
|
|
|
|
|
bool _nav_active = false;
|
|
|
|
|
int32_t _nav_lat = 0, _nav_lon = 0;
|
|
|
|
|
char _nav_label[24] = "";
|
|
|
|
|
|
2026-06-04 11:38:12 +02:00
|
|
|
// Share-compose mode: launched from elsewhere (e.g. a waypoint) with a
|
|
|
|
|
// prepared message; the user picks a recipient and the text lands prefilled
|
|
|
|
|
// in the keyboard to confirm/edit before sending.
|
|
|
|
|
bool _share_mode = false;
|
|
|
|
|
char _share_text[160] = "";
|
|
|
|
|
|
2026-05-19 20:21:32 +02:00
|
|
|
struct ChHistEntry { uint8_t ch_idx; char text[140]; uint32_t timestamp; };
|
2026-05-17 00:12:40 +02:00
|
|
|
ChHistEntry _hist[CH_HIST_MAX];
|
|
|
|
|
int _hist_head, _hist_count;
|
|
|
|
|
|
|
|
|
|
// DM_HIST
|
2026-05-19 20:21:32 +02:00
|
|
|
struct DmHistEntry { uint8_t prefix[4]; uint8_t outgoing; char text[80]; uint32_t timestamp; };
|
2026-05-17 00:12:40 +02:00
|
|
|
static const int DM_HIST_MAX = 64;
|
|
|
|
|
DmHistEntry _dm_hist[DM_HIST_MAX];
|
|
|
|
|
int _dm_hist_head, _dm_hist_count;
|
|
|
|
|
int _dm_hist_sel, _dm_hist_scroll;
|
|
|
|
|
FullscreenMsgView _dm_fs;
|
|
|
|
|
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
int _visible = 4; // updated in render(); used by handleInput() for scroll clamping
|
|
|
|
|
int _hist_visible = 2; // updated in render(); for history list scroll clamping
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
void expandMsg(const char* tmpl, char* out, int out_len) const {
|
|
|
|
|
double lat = 0, lon = 0;
|
|
|
|
|
bool gps_valid = false;
|
|
|
|
|
#if ENV_INCLUDE_GPS == 1
|
|
|
|
|
LocationProvider* loc = sensors.getLocationProvider();
|
|
|
|
|
if (loc && loc->isValid()) {
|
|
|
|
|
lat = loc->getLatitude() / 1000000.0;
|
|
|
|
|
lon = loc->getLongitude() / 1000000.0;
|
|
|
|
|
gps_valid = true;
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
NodePrefs* np = _task->getNodePrefs();
|
|
|
|
|
float batt = (float)board.getBattMilliVolts() / 1000.0f;
|
|
|
|
|
::expandMsg(tmpl, out, out_len, lat, lon, gps_valid,
|
|
|
|
|
rtc_clock.getCurrentTime(),
|
|
|
|
|
np ? np->tz_offset_hours : 0,
|
|
|
|
|
&sensors, batt);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-21 16:37:25 +02:00
|
|
|
// Strip "@[nick] " reply prefix from a message body for compact list display.
|
|
|
|
|
static const char* skipReplyPrefix(const char* text) {
|
|
|
|
|
if (text[0] == '@' && text[1] == '[') {
|
|
|
|
|
const char* close = strchr(text + 2, ']');
|
2026-05-23 14:45:24 +02:00
|
|
|
if (close && close[1] == ' ' && close[2]) text = close + 2;
|
2026-05-21 16:37:25 +02:00
|
|
|
}
|
2026-05-23 14:45:24 +02:00
|
|
|
while (*text == '\n' || *text == '\r' || *text == ' ') text++;
|
2026-05-21 16:37:25 +02:00
|
|
|
return text;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 18:20:42 +02:00
|
|
|
// Build "@[nick] " prefix from a channel message text ("nick: body") into _reply_prefix.
|
2026-05-18 15:12:11 +02:00
|
|
|
// 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;
|
2026-05-18 18:18:45 +02:00
|
|
|
if (slen > 31) slen = 31;
|
|
|
|
|
snprintf(_reply_prefix, sizeof(_reply_prefix), "@[%.*s] ", slen, text);
|
2026-05-18 15:12:11 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void startReply(bool to_channel) {
|
|
|
|
|
_sending_to_channel = to_channel;
|
|
|
|
|
_reply_mode = true;
|
|
|
|
|
setupMsgPick();
|
|
|
|
|
_phase = MSG_PICK;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 11:38:12 +02:00
|
|
|
// Recipient chosen while sharing — open the keyboard with the prepared text.
|
|
|
|
|
void beginShareCompose(bool channel) {
|
|
|
|
|
_sending_to_channel = channel;
|
|
|
|
|
_reply_mode = false;
|
|
|
|
|
_kb.begin(_share_text);
|
|
|
|
|
_phase = KEYBOARD;
|
|
|
|
|
}
|
|
|
|
|
|
feat(nav): navigate to / save a location shared in a message
Locations already travel as plain text ({loc} expands to "lat,lon"), so any
message carrying coordinates can now be acted on. In the fullscreen message
view, the context-menu (Hold Enter) gains Navigate and Save waypoint
entries whenever the message contains a coordinate — alongside Reply.
- geo::parseLatLon: shared scanner for an embedded "lat,lon" (the {loc}
format), with an optional [WAY] tag that supplies a label. Range-checked,
requires a decimal point to avoid matching plain integer pairs. Will also
back the waypoint-share feature later.
- QuickMsgScreen: Navigate opens NavView inline (To/Hdg/distance, honouring
the global Units setting) over the message, back returns to it; Save
waypoint stores the point (label from [WAY], else auto-named). Works in
both DM and channel histories, and on incoming or outgoing messages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:28:17 +02:00
|
|
|
// Build the fullscreen-message options popup: Reply (if allowed) plus
|
|
|
|
|
// Navigate / Save waypoint when `body` carries a location. Opens _ctx_menu
|
|
|
|
|
// only when there's at least one action. Parses the location once here and
|
|
|
|
|
// stashes it for the action handler.
|
|
|
|
|
void buildFsMenu(const char* body, bool reply_allowed) {
|
|
|
|
|
bool has_loc = geo::parseLatLon(body, _nav_lat, _nav_lon, _nav_label, sizeof(_nav_label));
|
|
|
|
|
int n = (reply_allowed ? 1 : 0) + (has_loc ? 2 : 0);
|
|
|
|
|
if (n == 0) return;
|
|
|
|
|
_fs_act_n = 0;
|
|
|
|
|
_ctx_menu.begin("Options", n);
|
|
|
|
|
if (reply_allowed) { _ctx_menu.addItem("Reply"); _fs_act[_fs_act_n++] = FS_REPLY; }
|
|
|
|
|
if (has_loc) { _ctx_menu.addItem("Navigate"); _fs_act[_fs_act_n++] = FS_NAV;
|
|
|
|
|
_ctx_menu.addItem("Save waypoint"); _fs_act[_fs_act_n++] = FS_SAVE; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Dispatch the selected fullscreen-options row. `channel` picks which
|
|
|
|
|
// fullscreen view to close when starting a reply.
|
|
|
|
|
void dispatchFsAction(bool channel) {
|
|
|
|
|
FsAct a = (FsAct)_fs_act[(_ctx_menu._sel >= 0 && _ctx_menu._sel < _fs_act_n) ? _ctx_menu._sel : 0];
|
|
|
|
|
_ctx_menu.active = false;
|
|
|
|
|
if (a == FS_REPLY) {
|
|
|
|
|
(channel ? _fs : _dm_fs).active = false;
|
|
|
|
|
startReply(channel);
|
|
|
|
|
} else if (a == FS_NAV) {
|
|
|
|
|
_nav_active = true; // keep the message view active underneath
|
|
|
|
|
} else {
|
|
|
|
|
saveSharedWaypoint();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Save the location parsed from the open message as a waypoint. Uses the
|
|
|
|
|
// [WAY] label when present, else auto-names it like a manually-marked point.
|
|
|
|
|
void saveSharedWaypoint() {
|
|
|
|
|
if (_task->waypoints().full()) { _task->showAlert("Waypoints full", 1000); return; }
|
|
|
|
|
char label[WAYPOINT_LABEL_LEN];
|
|
|
|
|
if (_nav_label[0]) {
|
|
|
|
|
strncpy(label, _nav_label, sizeof(label) - 1);
|
|
|
|
|
label[sizeof(label) - 1] = '\0';
|
|
|
|
|
} else {
|
|
|
|
|
snprintf(label, sizeof(label), "WP%d", _task->waypoints().count() + 1);
|
|
|
|
|
}
|
|
|
|
|
if (_task->waypoints().add(_nav_lat, _nav_lon,
|
|
|
|
|
(uint32_t)rtc_clock.getCurrentTime(), label)) {
|
|
|
|
|
_task->saveWaypoints();
|
|
|
|
|
_task->showAlert("Waypoint saved", 800);
|
|
|
|
|
} else {
|
|
|
|
|
_task->showAlert("Waypoints full", 1000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void renderNav(DisplayDriver& display) {
|
|
|
|
|
int32_t mylat, mylon; bool have = _task->currentLocation(mylat, mylon);
|
|
|
|
|
int cog; bool cogv = _task->currentCourse(cog);
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
navview::draw(display, have, mylat, mylon, _nav_lat, _nav_lon,
|
|
|
|
|
_nav_label[0] ? _nav_label : "Msg loc", cogv, cog, p && p->units_imperial);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
void setupMsgPick() {
|
|
|
|
|
_msg_sel = _msg_scroll = 0;
|
|
|
|
|
_active_msg_count = 0;
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
if (p) {
|
|
|
|
|
for (int i = 0; i < QUICK_MSGS_MAX; i++) {
|
|
|
|
|
if (p->custom_msgs[i][0] != '\0')
|
|
|
|
|
_active_msgs[_active_msg_count++] = i;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
refactor(ui): apply code review fixes — bugs, risk, optimisations, cleanup
Fixes 7 bugs (timing UB on unsigned long, abs() no-op, XBM CRC PROGMEM
scan, CayenneLPP heap thrash ×2, UTF-8 reply prefix, lock-seq reset on
wake), 3 risk items (notif melody stop guard, bot trigger pre-lowercase,
lock-seq cleared on display wake), plus optimisations (fmtMsgAge/
getTextWidth caching, RTTTL builder consolidated to NodePrefs, HP_*
constants and homePageLabel table moved to NodePrefs, DataStore flat
lambda schema, translateUTF8Static factored out) and minor cleanup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:03:45 +02:00
|
|
|
static void fmtMsgAge(char* buf, int n, uint32_t timestamp, uint32_t now) {
|
2026-05-19 20:21:32 +02:00
|
|
|
if (timestamp == 0 || now < timestamp) { buf[0] = '\0'; return; }
|
|
|
|
|
uint32_t age = now - timestamp;
|
|
|
|
|
if (age < 60) snprintf(buf, n, "%us", age);
|
|
|
|
|
else if (age < 3600) snprintf(buf, n, "%um", age / 60);
|
|
|
|
|
else if (age < 86400) snprintf(buf, n, "%uh", age / 3600);
|
|
|
|
|
else snprintf(buf, n, ">1d");
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
// count history entries for a specific channel
|
|
|
|
|
int histCountForChannel(int ch_idx) const {
|
|
|
|
|
int n = 0;
|
|
|
|
|
for (int i = 0; i < _hist_count; i++) {
|
|
|
|
|
if (_hist[(_hist_head + i) % CH_HIST_MAX].ch_idx == (uint8_t)ch_idx) n++;
|
|
|
|
|
}
|
|
|
|
|
return n;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// get ring-buffer position of j-th history entry for channel (newest first)
|
|
|
|
|
int histEntryForChannel(int ch_idx, int j) const {
|
|
|
|
|
int n = 0;
|
|
|
|
|
for (int i = _hist_count - 1; i >= 0; i--) {
|
|
|
|
|
int pos = (_hist_head + i) % CH_HIST_MAX;
|
|
|
|
|
if (_hist[pos].ch_idx == (uint8_t)ch_idx) {
|
|
|
|
|
if (n == j) return pos;
|
|
|
|
|
n++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void storeDMMsg(const uint8_t* pub_key, bool outgoing, const char* text) {
|
|
|
|
|
int pos;
|
|
|
|
|
if (_dm_hist_count < DM_HIST_MAX) {
|
|
|
|
|
pos = (_dm_hist_head + _dm_hist_count) % DM_HIST_MAX;
|
|
|
|
|
_dm_hist_count++;
|
|
|
|
|
} else {
|
|
|
|
|
pos = _dm_hist_head;
|
|
|
|
|
_dm_hist_head = (_dm_hist_head + 1) % DM_HIST_MAX;
|
|
|
|
|
}
|
|
|
|
|
memcpy(_dm_hist[pos].prefix, pub_key, 4);
|
|
|
|
|
_dm_hist[pos].outgoing = outgoing ? 1 : 0;
|
2026-05-19 20:21:32 +02:00
|
|
|
_dm_hist[pos].timestamp = rtc_clock.getCurrentTime();
|
2026-05-17 00:12:40 +02:00
|
|
|
strncpy(_dm_hist[pos].text, text, sizeof(DmHistEntry::text) - 1);
|
|
|
|
|
_dm_hist[pos].text[sizeof(DmHistEntry::text) - 1] = '\0';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int dmHistCountForContact(const uint8_t* prefix) const {
|
|
|
|
|
int n = 0;
|
|
|
|
|
for (int i = 0; i < _dm_hist_count; i++)
|
|
|
|
|
if (memcmp(_dm_hist[(_dm_hist_head + i) % DM_HIST_MAX].prefix, prefix, 4) == 0) n++;
|
|
|
|
|
return n;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int dmHistEntryForContact(const uint8_t* prefix, int j) const { // j=0 = newest
|
|
|
|
|
int n = 0;
|
|
|
|
|
for (int i = _dm_hist_count - 1; i >= 0; i--) {
|
|
|
|
|
int pos = (_dm_hist_head + i) % DM_HIST_MAX;
|
|
|
|
|
if (memcmp(_dm_hist[pos].prefix, prefix, 4) == 0) {
|
|
|
|
|
if (n == j) return pos;
|
|
|
|
|
n++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void afterSend(bool ok, const char* msg) {
|
2026-05-18 15:12:11 +02:00
|
|
|
_reply_mode = false;
|
2026-06-04 11:38:12 +02:00
|
|
|
_share_mode = false;
|
2026-05-17 00:12:40 +02:00
|
|
|
if (ok && _sending_to_channel) {
|
|
|
|
|
_hist_sel = 0;
|
|
|
|
|
_hist_scroll = 0;
|
|
|
|
|
_phase = CHANNEL_HIST; // set before addChannelMsg so viewing=true, no unread bump
|
|
|
|
|
char entry[sizeof(ChHistEntry::text)];
|
|
|
|
|
snprintf(entry, sizeof(entry), "Me: %s", msg);
|
|
|
|
|
addChannelMsg(_sel_channel_idx, entry);
|
|
|
|
|
// After inserting sent msg at index 0, the unread index range is stale.
|
|
|
|
|
// User is active in this channel — treat as fully read.
|
|
|
|
|
_ch_unread[_sel_channel_idx] = 0;
|
|
|
|
|
_unread_at_entry = 0;
|
|
|
|
|
_viewing_max_seen = 0;
|
|
|
|
|
_task->showAlert("Sent!", 600);
|
|
|
|
|
} else if (ok) {
|
|
|
|
|
storeDMMsg(_sel_contact.id.pub_key, true, msg);
|
|
|
|
|
_dm_hist_sel = 0;
|
|
|
|
|
_dm_hist_scroll = 0;
|
|
|
|
|
_phase = DM_HIST;
|
|
|
|
|
_task->showAlert("Sent!", 600);
|
|
|
|
|
} else {
|
|
|
|
|
_task->showAlert("Send failed", 1500);
|
|
|
|
|
_task->gotoHomeScreen();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool sendText(const char* msg) {
|
|
|
|
|
if (_sending_to_channel) {
|
|
|
|
|
ChannelDetails ch;
|
|
|
|
|
if (!the_mesh.getChannel(_sel_channel_idx, ch)) return false;
|
|
|
|
|
return the_mesh.sendGroupMessage(rtc_clock.getCurrentTime(), ch.channel,
|
|
|
|
|
the_mesh.getNodeName(), msg, strlen(msg));
|
|
|
|
|
} else {
|
|
|
|
|
uint32_t expected_ack = 0, est_timeout = 0;
|
|
|
|
|
return the_mesh.sendMessage(_sel_contact, rtc_clock.getCurrentTime(), 0,
|
|
|
|
|
msg, expected_ack, est_timeout) > 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void buildContactList() {
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
ContactInfo c;
|
|
|
|
|
int total = the_mesh.getNumContacts();
|
|
|
|
|
_num_contacts = 0;
|
|
|
|
|
if (_room_mode) {
|
|
|
|
|
bool fav_only = (p && p->room_fav_only);
|
|
|
|
|
for (int i = 0; i < total; i++) {
|
|
|
|
|
if (!the_mesh.getContactByIdx(i, c) || c.type != ADV_TYPE_ROOM) continue;
|
|
|
|
|
if (fav_only && !(c.flags & 0x01)) continue;
|
|
|
|
|
_sorted[_num_contacts++] = i;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
bool show_all = (p && p->dm_show_all);
|
2026-05-24 20:07:46 +02:00
|
|
|
// Build _sorted and counts[] in one pass — avoids a second getContactByIdx loop.
|
|
|
|
|
int counts[MAX_CONTACTS];
|
2026-05-17 00:12:40 +02:00
|
|
|
for (int i = 0; i < total; i++) {
|
|
|
|
|
if (!the_mesh.getContactByIdx(i, c) || c.type != ADV_TYPE_CHAT) continue;
|
|
|
|
|
if (!show_all && !(c.flags & 0x01)) continue;
|
2026-05-24 20:07:46 +02:00
|
|
|
counts[_num_contacts] = dmHistCountForContact(c.id.pub_key);
|
2026-05-17 00:12:40 +02:00
|
|
|
_sorted[_num_contacts++] = i;
|
|
|
|
|
}
|
2026-05-23 09:19:27 +02:00
|
|
|
// Sort by message count descending; contacts with no messages keep original order.
|
|
|
|
|
for (int i = 1; i < _num_contacts; i++) {
|
|
|
|
|
if (counts[i] == 0) continue;
|
|
|
|
|
uint16_t key = _sorted[i]; int kc = counts[i];
|
|
|
|
|
int j = i;
|
|
|
|
|
while (j > 0 && counts[j-1] < kc) {
|
|
|
|
|
_sorted[j] = _sorted[j-1]; counts[j] = counts[j-1]; j--;
|
|
|
|
|
}
|
|
|
|
|
_sorted[j] = key; counts[j] = kc;
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void buildChannelList() {
|
2026-05-27 17:33:52 +02:00
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
bool fav_only = (p && p->ch_fav_only);
|
2026-05-17 00:12:40 +02:00
|
|
|
_num_channels = 0;
|
|
|
|
|
for (int i = 0; i < MAX_GROUP_CHANNELS; i++) {
|
|
|
|
|
ChannelDetails ch;
|
2026-05-27 17:33:52 +02:00
|
|
|
if (!the_mesh.getChannel(i, ch) || ch.name[0] == '\0') continue;
|
|
|
|
|
if (fav_only && !(p->ch_fav_bitmask & (1ULL << i))) continue;
|
|
|
|
|
_channel_indices[_num_channels++] = (uint8_t)i;
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns per-channel notification state: 0=follow global, 1=muted, 2=force-on
|
|
|
|
|
uint8_t chNotifState(uint8_t ch_idx) const {
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
if (!p) return 0;
|
|
|
|
|
uint64_t mask = 1ULL << ch_idx;
|
|
|
|
|
if (!(p->ch_notif_override & mask)) return 0;
|
|
|
|
|
return (p->ch_notif_muted & mask) ? 1 : 2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void setChNotifState(uint8_t ch_idx, uint8_t state) {
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
if (!p) return;
|
|
|
|
|
uint64_t mask = 1ULL << ch_idx;
|
|
|
|
|
if (state == 0) {
|
|
|
|
|
p->ch_notif_override &= ~mask;
|
|
|
|
|
p->ch_notif_muted &= ~mask;
|
|
|
|
|
} else if (state == 1) {
|
|
|
|
|
p->ch_notif_override |= mask;
|
|
|
|
|
p->ch_notif_muted |= mask;
|
|
|
|
|
} else {
|
|
|
|
|
p->ch_notif_override |= mask;
|
|
|
|
|
p->ch_notif_muted &= ~mask;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint8_t dmNotifState(const uint8_t* pub_key) const {
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
if (!p) return 0;
|
|
|
|
|
for (int i = 0; i < NodePrefs::DM_NOTIF_TABLE_MAX; i++)
|
|
|
|
|
if (p->dm_notif[i].state && memcmp(p->dm_notif[i].prefix, pub_key, 4) == 0)
|
|
|
|
|
return p->dm_notif[i].state;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void setDmNotifState(const uint8_t* pub_key, uint8_t state) {
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
if (!p) return;
|
|
|
|
|
for (int i = 0; i < NodePrefs::DM_NOTIF_TABLE_MAX; i++) {
|
|
|
|
|
if (p->dm_notif[i].state && memcmp(p->dm_notif[i].prefix, pub_key, 4) == 0) {
|
|
|
|
|
if (state == 0) { memset(&p->dm_notif[i], 0, sizeof(p->dm_notif[i])); }
|
|
|
|
|
else p->dm_notif[i].state = state;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (state == 0) return;
|
|
|
|
|
for (int i = 0; i < NodePrefs::DM_NOTIF_TABLE_MAX; i++) {
|
|
|
|
|
if (p->dm_notif[i].state == 0) {
|
|
|
|
|
memcpy(p->dm_notif[i].prefix, pub_key, 4);
|
|
|
|
|
p->dm_notif[i].state = state;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// table full — overwrite slot 0
|
|
|
|
|
memcpy(p->dm_notif[0].prefix, pub_key, 4);
|
|
|
|
|
p->dm_notif[0].state = state;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint8_t chNotifMelody(uint8_t ch_idx) const {
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
if (!p) return 0;
|
|
|
|
|
uint64_t mask = 1ULL << ch_idx;
|
|
|
|
|
if (!(p->ch_notif_melody_set & mask)) return 0;
|
|
|
|
|
return (p->ch_notif_melody_2 & mask) ? 2 : 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void setChNotifMelody(uint8_t ch_idx, uint8_t slot) {
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
if (!p) return;
|
|
|
|
|
uint64_t mask = 1ULL << ch_idx;
|
|
|
|
|
if (slot == 0) {
|
|
|
|
|
p->ch_notif_melody_set &= ~mask;
|
|
|
|
|
p->ch_notif_melody_2 &= ~mask;
|
|
|
|
|
} else if (slot == 1) {
|
|
|
|
|
p->ch_notif_melody_set |= mask;
|
|
|
|
|
p->ch_notif_melody_2 &= ~mask;
|
|
|
|
|
} else {
|
|
|
|
|
p->ch_notif_melody_set |= mask;
|
|
|
|
|
p->ch_notif_melody_2 |= mask;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint8_t dmMelodySlot(const uint8_t* pub_key) const {
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
if (!p) return 0;
|
|
|
|
|
for (int i = 0; i < NodePrefs::DM_MELODY_TABLE_MAX; i++)
|
|
|
|
|
if (p->dm_melody[i].slot && memcmp(p->dm_melody[i].prefix, pub_key, 4) == 0)
|
|
|
|
|
return p->dm_melody[i].slot;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void setDmMelody(const uint8_t* pub_key, uint8_t slot) {
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
if (!p) return;
|
|
|
|
|
for (int i = 0; i < NodePrefs::DM_MELODY_TABLE_MAX; i++) {
|
|
|
|
|
if (p->dm_melody[i].slot && memcmp(p->dm_melody[i].prefix, pub_key, 4) == 0) {
|
|
|
|
|
if (slot == 0) memset(&p->dm_melody[i], 0, sizeof(p->dm_melody[i]));
|
|
|
|
|
else p->dm_melody[i].slot = slot;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (slot == 0) return;
|
|
|
|
|
for (int i = 0; i < NodePrefs::DM_MELODY_TABLE_MAX; i++) {
|
|
|
|
|
if (p->dm_melody[i].slot == 0) {
|
|
|
|
|
memcpy(p->dm_melody[i].prefix, pub_key, 4);
|
|
|
|
|
p->dm_melody[i].slot = slot;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
memcpy(p->dm_melody[0].prefix, pub_key, 4);
|
|
|
|
|
p->dm_melody[0].slot = slot;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
QuickMsgScreen(UITask* task)
|
|
|
|
|
: _task(task), _phase(MODE_SELECT), _mode_sel(0),
|
|
|
|
|
_contact_sel(0), _contact_scroll(0), _num_contacts(0), _room_mode(false),
|
|
|
|
|
_channel_sel(0), _channel_scroll(0), _num_channels(0),
|
|
|
|
|
_sel_channel_idx(0), _sending_to_channel(false),
|
|
|
|
|
_msg_sel(0), _msg_scroll(0), _active_msg_count(0),
|
|
|
|
|
_hist_sel(0), _hist_scroll(0),
|
|
|
|
|
_unread_at_entry(0), _viewing_max_seen(0),
|
|
|
|
|
_hist_head(0), _hist_count(0),
|
|
|
|
|
_dm_hist_head(0), _dm_hist_count(0),
|
|
|
|
|
_dm_hist_sel(-1), _dm_hist_scroll(0),
|
2026-05-25 07:58:45 +02:00
|
|
|
_ctx_dirty(false), _pin_picker_active(false), _dm_direct_entry(false), _reply_mode(false) {
|
2026-05-17 00:12:40 +02:00
|
|
|
memset(_ch_unread, 0, sizeof(_ch_unread));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void addChannelMsg(uint8_t ch_idx, const char* text) {
|
2026-05-30 23:57:12 +02:00
|
|
|
// Guard against bogus channel indices (e.g. findChannelIdx() returned -1
|
|
|
|
|
// and was cast to uint8_t → 255). Storing such an entry would burn a ring
|
|
|
|
|
// slot for a message that no visible channel can ever surface.
|
|
|
|
|
if (ch_idx >= MAX_GROUP_CHANNELS) return;
|
2026-05-17 00:12:40 +02:00
|
|
|
int pos;
|
|
|
|
|
if (_hist_count < CH_HIST_MAX) {
|
|
|
|
|
pos = (_hist_head + _hist_count) % CH_HIST_MAX;
|
|
|
|
|
_hist_count++;
|
|
|
|
|
} else {
|
|
|
|
|
pos = _hist_head;
|
2026-05-31 16:23:07 +02:00
|
|
|
// Evicting the oldest entry — drop its share of the unread counter so
|
|
|
|
|
// the badge can't claim a message the ring no longer holds.
|
|
|
|
|
uint8_t evicted = _hist[pos].ch_idx;
|
|
|
|
|
if (evicted < MAX_GROUP_CHANNELS && _ch_unread[evicted] > 0) {
|
|
|
|
|
_ch_unread[evicted]--;
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
_hist_head = (_hist_head + 1) % CH_HIST_MAX;
|
|
|
|
|
}
|
|
|
|
|
_hist[pos].ch_idx = ch_idx;
|
2026-05-19 20:21:32 +02:00
|
|
|
_hist[pos].timestamp = rtc_clock.getCurrentTime();
|
2026-05-17 00:12:40 +02:00
|
|
|
strncpy(_hist[pos].text, text, sizeof(_hist[pos].text) - 1);
|
|
|
|
|
_hist[pos].text[sizeof(_hist[pos].text) - 1] = '\0';
|
|
|
|
|
|
2026-05-30 23:57:12 +02:00
|
|
|
bool viewing = (_phase == CHANNEL_HIST && _sel_channel_idx == (int)ch_idx);
|
|
|
|
|
if (!viewing && _ch_unread[ch_idx] < 99) _ch_unread[ch_idx]++;
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text) {
|
|
|
|
|
storeDMMsg(pub_key, outgoing, text);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int getDMUnreadTotal() const {
|
|
|
|
|
return _task->getDMUnreadTotal();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int getTotalChannelUnread() const {
|
|
|
|
|
int total = 0;
|
|
|
|
|
for (int i = 0; i < MAX_GROUP_CHANNELS; i++) total += _ch_unread[i];
|
|
|
|
|
return total;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void clearAllChannelUnread() {
|
|
|
|
|
memset(_ch_unread, 0, sizeof(_ch_unread));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void updateChannelUnread() {
|
|
|
|
|
if (_hist_sel < 0 || _sel_channel_idx < 0 || _sel_channel_idx >= MAX_GROUP_CHANNELS) return;
|
|
|
|
|
if (_hist_sel > _viewing_max_seen) _viewing_max_seen = _hist_sel;
|
|
|
|
|
// histEntryForChannel is newest-first: index 0 = newest (unread), higher = older.
|
|
|
|
|
// Each step down from 0 sees one more message; seen count = max_seen + 1.
|
|
|
|
|
int remaining = _unread_at_entry - (_viewing_max_seen + 1);
|
|
|
|
|
_ch_unread[_sel_channel_idx] = (uint8_t)(remaining > 0 ? remaining : 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void reset() {
|
|
|
|
|
_phase = MODE_SELECT;
|
|
|
|
|
_mode_sel = 0;
|
|
|
|
|
_contact_sel = _contact_scroll = 0;
|
|
|
|
|
_msg_sel = _msg_scroll = 0;
|
|
|
|
|
_channel_sel = _channel_scroll = 0;
|
|
|
|
|
_sending_to_channel = false;
|
|
|
|
|
|
|
|
|
|
_room_mode = false;
|
|
|
|
|
buildContactList();
|
|
|
|
|
buildChannelList();
|
|
|
|
|
|
|
|
|
|
_ctx_menu.active = false;
|
|
|
|
|
_ctx_dirty = false;
|
feat(nav): navigate to / save a location shared in a message
Locations already travel as plain text ({loc} expands to "lat,lon"), so any
message carrying coordinates can now be acted on. In the fullscreen message
view, the context-menu (Hold Enter) gains Navigate and Save waypoint
entries whenever the message contains a coordinate — alongside Reply.
- geo::parseLatLon: shared scanner for an embedded "lat,lon" (the {loc}
format), with an optional [WAY] tag that supplies a label. Range-checked,
requires a decimal point to avoid matching plain integer pairs. Will also
back the waypoint-share feature later.
- QuickMsgScreen: Navigate opens NavView inline (To/Hdg/distance, honouring
the global Units setting) over the message, back returns to it; Save
waypoint stores the point (label from [WAY], else auto-named). Works in
both DM and channel histories, and on incoming or outgoing messages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:28:17 +02:00
|
|
|
_nav_active = false;
|
2026-06-04 11:38:12 +02:00
|
|
|
_share_mode = false;
|
2026-05-24 23:30:16 +02:00
|
|
|
_pin_picker_active = false;
|
2026-05-25 07:58:45 +02:00
|
|
|
_dm_direct_entry = false;
|
2026-05-17 00:12:40 +02:00
|
|
|
_unread_at_entry = 0;
|
|
|
|
|
_viewing_max_seen = 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 08:08:12 +02:00
|
|
|
// Recent DM contacts, newest first, deduped. Resolves the 4-byte
|
|
|
|
|
// _dm_hist prefix to a 6-byte pub_key prefix by walking the contact
|
|
|
|
|
// list once per unique sender. Returns the number filled.
|
|
|
|
|
int getRecentDMContacts(uint8_t out[][NodePrefs::FAVOURITE_PREFIX_LEN], int max) const {
|
|
|
|
|
int n = 0;
|
|
|
|
|
for (int i = _dm_hist_count - 1; i >= 0 && n < max; i--) {
|
|
|
|
|
int pos = (_dm_hist_head + i) % DM_HIST_MAX;
|
|
|
|
|
const uint8_t* p4 = _dm_hist[pos].prefix;
|
|
|
|
|
// Skip if already collected.
|
|
|
|
|
bool dup = false;
|
|
|
|
|
for (int j = 0; j < n; j++) if (memcmp(out[j], p4, 4) == 0) { dup = true; break; }
|
|
|
|
|
if (dup) continue;
|
|
|
|
|
// Find a real contact whose pub_key starts with this 4-byte prefix.
|
|
|
|
|
for (int idx = 0; ; idx++) {
|
|
|
|
|
ContactInfo c;
|
|
|
|
|
if (!the_mesh.getContactByIdx(idx, c)) break;
|
|
|
|
|
if (memcmp(c.id.pub_key, p4, 4) == 0) {
|
|
|
|
|
memcpy(out[n], c.id.pub_key, NodePrefs::FAVOURITE_PREFIX_LEN);
|
|
|
|
|
n++;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return n;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 07:52:56 +02:00
|
|
|
// Jump straight into a contact's DM history (used by the Favourites dial).
|
2026-05-25 07:58:45 +02:00
|
|
|
// Caller must have already reset() the screen. Marks the entry so KEY_CANCEL
|
|
|
|
|
// from DM_HIST returns to the home screen instead of falling back through
|
|
|
|
|
// CONTACT_PICK → MODE_SELECT.
|
2026-06-04 11:38:12 +02:00
|
|
|
// Enter the screen pre-loaded to share `text` (e.g. a "[WAY]lat,lon label"
|
|
|
|
|
// waypoint). The user picks Direct/Channel then a recipient; selecting one
|
|
|
|
|
// jumps straight to the keyboard prefilled with the text (see beginShareCompose).
|
|
|
|
|
void startShare(const char* text) {
|
|
|
|
|
reset();
|
|
|
|
|
_share_mode = true;
|
|
|
|
|
strncpy(_share_text, text, sizeof(_share_text) - 1);
|
|
|
|
|
_share_text[sizeof(_share_text) - 1] = '\0';
|
|
|
|
|
_phase = MODE_SELECT;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 07:52:56 +02:00
|
|
|
void enterDM(const ContactInfo& ci) {
|
|
|
|
|
_sel_contact = ci;
|
|
|
|
|
_task->clearDMUnread(ci.id.pub_key);
|
|
|
|
|
_dm_hist_sel = -1;
|
|
|
|
|
_dm_hist_scroll = 0;
|
|
|
|
|
_dm_fs.active = false;
|
|
|
|
|
_room_mode = false;
|
|
|
|
|
_phase = DM_HIST;
|
2026-05-25 07:58:45 +02:00
|
|
|
_dm_direct_entry = true;
|
2026-05-25 07:52:56 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
int render(DisplayDriver& display) override {
|
|
|
|
|
display.setTextSize(1);
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
feat(nav): navigate to / save a location shared in a message
Locations already travel as plain text ({loc} expands to "lat,lon"), so any
message carrying coordinates can now be acted on. In the fullscreen message
view, the context-menu (Hold Enter) gains Navigate and Save waypoint
entries whenever the message contains a coordinate — alongside Reply.
- geo::parseLatLon: shared scanner for an embedded "lat,lon" (the {loc}
format), with an optional [WAY] tag that supplies a label. Range-checked,
requires a decimal point to avoid matching plain integer pairs. Will also
back the waypoint-share feature later.
- QuickMsgScreen: Navigate opens NavView inline (To/Hdg/distance, honouring
the global Units setting) over the message, back returns to it; Save
waypoint stores the point (label from [WAY], else auto-named). Works in
both DM and channel histories, and on incoming or outgoing messages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:28:17 +02:00
|
|
|
// Navigate-to-location view sits over everything else while active.
|
|
|
|
|
if (_nav_active) { renderNav(display); return 1000; }
|
|
|
|
|
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
int lh = display.getLineHeight();
|
|
|
|
|
int item_h = display.lineStep();
|
|
|
|
|
int start_y = display.listStart();
|
|
|
|
|
int cw = display.getCharWidth();
|
|
|
|
|
_visible = display.listVisible(item_h);
|
|
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
if (_phase == MODE_SELECT) {
|
|
|
|
|
display.drawTextCentered(display.width()/2, 0, "MESSAGE");
|
2026-05-22 23:13:27 +02:00
|
|
|
display.fillRect(0, display.headerH() - 1, display.width(), display.sepH());
|
2026-05-17 00:12:40 +02:00
|
|
|
const char* opts[] = { "Direct message", "Channels", "Room Servers" };
|
|
|
|
|
int badges[3] = {
|
|
|
|
|
getDMUnreadTotal(),
|
|
|
|
|
_task->getChannelUnreadCount(),
|
|
|
|
|
_task->getRoomUnreadCount()
|
|
|
|
|
};
|
|
|
|
|
for (int i = 0; i < 3; i++) {
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
int y = start_y + i * item_h;
|
2026-05-17 00:12:40 +02:00
|
|
|
bool sel = (i == _mode_sel);
|
refactor(ui): drawSelectionRow helper for the 5-line invert/restore pattern
Eight screens repeated the same if/else block to invert the row
background for selected items. Add DisplayDriver::drawSelectionRow that
sets LIGHT, optionally fills the rect, and leaves the colour as DARK
when sel (so the caller's text renders inverted). 61 lines removed
across SettingsScreen, KeyboardWidget (cells + special row), ToolsScreen,
DashboardConfigScreen, BotScreen, RingtoneEditorScreen (note slots +
menu), NearbyScreen, QuickMsgScreen (4 list views).
Card-style rows (QuickMsg hist, NearbyScreen discover, RingtoneEditor
notes display) keep their original drawRect/partial-fill outline for
unselected — they don't match the simple-invert pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:33:02 +02:00
|
|
|
display.drawSelectionRow(0, y - 1, display.width(), item_h - 1, sel);
|
2026-05-17 00:12:40 +02:00
|
|
|
display.setCursor(0, y);
|
|
|
|
|
display.print(sel ? ">" : " ");
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
display.setCursor(cw + 2, y);
|
2026-05-17 00:12:40 +02:00
|
|
|
display.print(opts[i]);
|
|
|
|
|
if (badges[i] > 0) {
|
|
|
|
|
char badge[5];
|
|
|
|
|
snprintf(badge, sizeof(badge), "%d", badges[i]);
|
|
|
|
|
int bw = display.getTextWidth(badge) + 2;
|
|
|
|
|
display.setCursor(display.width() - bw, y);
|
|
|
|
|
display.print(badge);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
2026-05-24 22:43:31 +02:00
|
|
|
if (_ctx_menu.active) _ctx_menu.render(display);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
} else if (_phase == CONTACT_PICK) {
|
|
|
|
|
display.drawTextCentered(display.width()/2, 0, _room_mode ? "SELECT ROOM" : "SELECT CONTACT");
|
2026-05-22 23:13:27 +02:00
|
|
|
display.fillRect(0, display.headerH() - 1, display.width(), display.sepH());
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
if (_num_contacts == 0) {
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
display.drawTextCentered(display.width()/2, display.height()/2, _room_mode ? "No room servers" : "No favourites");
|
2026-05-17 00:12:40 +02:00
|
|
|
return 5000;
|
|
|
|
|
}
|
|
|
|
|
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
for (int i = 0; i < _visible && (_contact_scroll+i) < _num_contacts; i++) {
|
2026-05-17 00:12:40 +02:00
|
|
|
int list_idx = _contact_scroll + i;
|
|
|
|
|
int mesh_idx = _sorted[list_idx];
|
|
|
|
|
bool sel = (list_idx == _contact_sel);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
int y = start_y + i * item_h;
|
2026-05-17 00:12:40 +02:00
|
|
|
|
refactor(ui): drawSelectionRow helper for the 5-line invert/restore pattern
Eight screens repeated the same if/else block to invert the row
background for selected items. Add DisplayDriver::drawSelectionRow that
sets LIGHT, optionally fills the rect, and leaves the colour as DARK
when sel (so the caller's text renders inverted). 61 lines removed
across SettingsScreen, KeyboardWidget (cells + special row), ToolsScreen,
DashboardConfigScreen, BotScreen, RingtoneEditorScreen (note slots +
menu), NearbyScreen, QuickMsgScreen (4 list views).
Card-style rows (QuickMsg hist, NearbyScreen discover, RingtoneEditor
notes display) keep their original drawRect/partial-fill outline for
unselected — they don't match the simple-invert pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:33:02 +02:00
|
|
|
display.drawSelectionRow(0, y - 1, display.width(), item_h - 1, sel);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
ContactInfo c;
|
|
|
|
|
if (the_mesh.getContactByIdx(mesh_idx, c)) {
|
|
|
|
|
display.setCursor(0, y);
|
|
|
|
|
display.print(sel ? ">" : " ");
|
|
|
|
|
char filtered[sizeof(c.name)];
|
|
|
|
|
display.translateUTF8ToBlocks(filtered, c.name, sizeof(filtered));
|
|
|
|
|
uint8_t dm_unread = _task->getDMUnread(c.id.pub_key);
|
|
|
|
|
char badge[5] = "";
|
|
|
|
|
int bw = 0;
|
|
|
|
|
if (dm_unread > 0) {
|
|
|
|
|
snprintf(badge, sizeof(badge), "%d", (int)dm_unread);
|
|
|
|
|
bw = display.getTextWidth(badge) + 2;
|
|
|
|
|
}
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
display.drawTextEllipsized(cw + 2, y, display.width() - cw - 2 - bw - 1, filtered);
|
2026-05-17 00:12:40 +02:00
|
|
|
if (dm_unread > 0) {
|
|
|
|
|
display.setCursor(display.width() - bw + 1, y);
|
|
|
|
|
display.print(badge);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
if (_contact_scroll > 0) { display.setCursor(display.width() - cw, start_y); display.print("^"); }
|
|
|
|
|
if (_contact_scroll + _visible < _num_contacts) { display.setCursor(display.width() - cw, start_y + (_visible-1)*item_h); display.print("v"); }
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
// Context menu overlay
|
|
|
|
|
if (_ctx_menu.active) _ctx_menu.render(display);
|
|
|
|
|
|
|
|
|
|
} else if (_phase == CHANNEL_PICK) {
|
|
|
|
|
display.drawTextCentered(display.width()/2, 0, "SELECT CHANNEL");
|
2026-05-22 23:13:27 +02:00
|
|
|
display.fillRect(0, display.headerH() - 1, display.width(), display.sepH());
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
if (_num_channels == 0) {
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
display.drawTextCentered(display.width()/2, display.height()/2, "No channels");
|
2026-05-17 00:12:40 +02:00
|
|
|
return 5000;
|
|
|
|
|
}
|
|
|
|
|
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
for (int i = 0; i < _visible && (_channel_scroll+i) < _num_channels; i++) {
|
2026-05-17 00:12:40 +02:00
|
|
|
int list_idx = _channel_scroll + i;
|
|
|
|
|
bool sel = (list_idx == _channel_sel);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
int y = start_y + i * item_h;
|
2026-05-17 00:12:40 +02:00
|
|
|
|
refactor(ui): drawSelectionRow helper for the 5-line invert/restore pattern
Eight screens repeated the same if/else block to invert the row
background for selected items. Add DisplayDriver::drawSelectionRow that
sets LIGHT, optionally fills the rect, and leaves the colour as DARK
when sel (so the caller's text renders inverted). 61 lines removed
across SettingsScreen, KeyboardWidget (cells + special row), ToolsScreen,
DashboardConfigScreen, BotScreen, RingtoneEditorScreen (note slots +
menu), NearbyScreen, QuickMsgScreen (4 list views).
Card-style rows (QuickMsg hist, NearbyScreen discover, RingtoneEditor
notes display) keep their original drawRect/partial-fill outline for
unselected — they don't match the simple-invert pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:33:02 +02:00
|
|
|
display.drawSelectionRow(0, y - 1, display.width(), item_h - 1, sel);
|
2026-05-17 00:12:40 +02:00
|
|
|
display.setCursor(0, y);
|
|
|
|
|
display.print(sel ? ">" : " ");
|
|
|
|
|
ChannelDetails ch;
|
|
|
|
|
if (the_mesh.getChannel(_channel_indices[list_idx], ch)) {
|
|
|
|
|
uint8_t unread = _ch_unread[_channel_indices[list_idx]];
|
|
|
|
|
char badge[5] = "";
|
|
|
|
|
int bw = 0;
|
|
|
|
|
if (unread > 0) {
|
|
|
|
|
snprintf(badge, sizeof(badge), "%d", (int)unread);
|
|
|
|
|
bw = display.getTextWidth(badge) + 2;
|
|
|
|
|
}
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
display.drawTextEllipsized(cw + 2, y, display.width() - cw - 4 - bw, ch.name);
|
2026-05-17 00:12:40 +02:00
|
|
|
if (unread > 0) {
|
|
|
|
|
display.setCursor(display.width() - bw, y);
|
|
|
|
|
display.print(badge);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
if (_channel_scroll > 0) { display.setCursor(display.width() - cw, start_y); display.print("^"); }
|
|
|
|
|
if (_channel_scroll + _visible < _num_channels) { display.setCursor(display.width() - cw, start_y + (_visible-1)*item_h); display.print("v"); }
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
// Context menu overlay
|
|
|
|
|
if (_ctx_menu.active) _ctx_menu.render(display);
|
|
|
|
|
|
|
|
|
|
} else if (_phase == DM_HIST) {
|
|
|
|
|
display.setTextSize(1);
|
|
|
|
|
char filtered_name[sizeof(_sel_contact.name)];
|
|
|
|
|
display.translateUTF8ToBlocks(filtered_name, _sel_contact.name, sizeof(filtered_name));
|
|
|
|
|
|
|
|
|
|
if (_dm_fs.active && _dm_hist_sel >= 0) {
|
|
|
|
|
int ring_pos = dmHistEntryForContact(_sel_contact.id.pub_key, _dm_hist_sel);
|
|
|
|
|
if (ring_pos >= 0) {
|
|
|
|
|
const DmHistEntry& e = _dm_hist[ring_pos];
|
|
|
|
|
const char* sender = e.outgoing ? "Me" : filtered_name;
|
|
|
|
|
int dm_count = dmHistCountForContact(_sel_contact.id.pub_key);
|
2026-05-18 16:11:25 +02:00
|
|
|
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;
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
return 500;
|
|
|
|
|
}
|
|
|
|
|
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
int hist_start_y = display.headerH();
|
2026-05-22 23:13:27 +02:00
|
|
|
int cby = display.height() - lh - 2;
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
char title[24];
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
snprintf(title, sizeof(title), "%.23s", filtered_name);
|
|
|
|
|
display.drawTextCentered(display.width()/2, 0, title);
|
2026-05-22 23:13:27 +02:00
|
|
|
display.fillRect(0, lh + 1, display.width(), display.sepH());
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
int dm_count = dmHistCountForContact(_sel_contact.id.pub_key);
|
refactor(ui): apply code review fixes — bugs, risk, optimisations, cleanup
Fixes 7 bugs (timing UB on unsigned long, abs() no-op, XBM CRC PROGMEM
scan, CayenneLPP heap thrash ×2, UTF-8 reply prefix, lock-seq reset on
wake), 3 risk items (notif melody stop guard, bot trigger pre-lowercase,
lock-seq cleared on display wake), plus optimisations (fmtMsgAge/
getTextWidth caching, RTTTL builder consolidated to NodePrefs, HP_*
constants and homePageLabel table moved to NodePrefs, DataStore flat
lambda schema, translateUTF8Static factored out) and minor cleanup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:03:45 +02:00
|
|
|
uint32_t now_ts = rtc_clock.getCurrentTime();
|
2026-05-17 00:12:40 +02:00
|
|
|
|
2026-05-27 23:47:30 +02:00
|
|
|
// Portrait e-ink (height > width): variable-height boxes that show the full
|
|
|
|
|
// wrapped message text. All other displays/orientations: compact 2-line boxes.
|
|
|
|
|
bool portrait_expand = (display.height() > display.width());
|
|
|
|
|
const int MAX_VIS_BOXES = 8;
|
|
|
|
|
int box_ys[MAX_VIS_BOXES], box_hs[MAX_VIS_BOXES], n_vis = 0;
|
|
|
|
|
{
|
|
|
|
|
const int fixed_bh = 2 * lh + 1;
|
|
|
|
|
int cur_y = hist_start_y;
|
|
|
|
|
for (int ii = 0; ii < MAX_VIS_BOXES && (_dm_hist_scroll + ii) < dm_count; ii++) {
|
|
|
|
|
int bh = fixed_bh;
|
|
|
|
|
if (portrait_expand) {
|
|
|
|
|
int rp = dmHistEntryForContact(_sel_contact.id.pub_key, _dm_hist_scroll + ii);
|
|
|
|
|
if (rp >= 0) {
|
|
|
|
|
char trans[160];
|
|
|
|
|
display.translateUTF8ToBlocks(trans, skipReplyPrefix(_dm_hist[rp].text), sizeof(trans));
|
|
|
|
|
char wl[8][FS_CHARS_MAX];
|
|
|
|
|
int nl = FullscreenMsgView::wrapLines(display, trans, display.width() - 6, wl, 8);
|
|
|
|
|
bh = (1 + (nl > 0 ? nl : 1)) * lh + 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (cur_y + bh > cby) break;
|
|
|
|
|
box_ys[n_vis] = cur_y; box_hs[n_vis] = bh; n_vis++;
|
|
|
|
|
cur_y += bh + (portrait_expand ? 2 : 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_hist_visible = n_vis;
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < n_vis && (_dm_hist_scroll + i) < dm_count; i++) {
|
2026-05-17 00:12:40 +02:00
|
|
|
int item = _dm_hist_scroll + i;
|
|
|
|
|
bool sel = (item == _dm_hist_sel);
|
2026-05-27 23:47:30 +02:00
|
|
|
int y = box_ys[i];
|
|
|
|
|
int bh = box_hs[i];
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
int ring_pos = dmHistEntryForContact(_sel_contact.id.pub_key, item);
|
|
|
|
|
if (ring_pos < 0) continue;
|
|
|
|
|
|
|
|
|
|
const DmHistEntry& e = _dm_hist[ring_pos];
|
|
|
|
|
const char* sender = e.outgoing ? "Me" : filtered_name;
|
|
|
|
|
|
refactor(ui): apply code review fixes — bugs, risk, optimisations, cleanup
Fixes 7 bugs (timing UB on unsigned long, abs() no-op, XBM CRC PROGMEM
scan, CayenneLPP heap thrash ×2, UTF-8 reply prefix, lock-seq reset on
wake), 3 risk items (notif melody stop guard, bot trigger pre-lowercase,
lock-seq cleared on display wake), plus optimisations (fmtMsgAge/
getTextWidth caching, RTTTL builder consolidated to NodePrefs, HP_*
constants and homePageLabel table moved to NodePrefs, DataStore flat
lambda schema, translateUTF8Static factored out) and minor cleanup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:03:45 +02:00
|
|
|
char age[6]; fmtMsgAge(age, sizeof(age), e.timestamp, now_ts);
|
2026-05-19 20:21:32 +02:00
|
|
|
int age_w = age[0] ? display.getTextWidth(age) + 3 : 0;
|
|
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
if (sel) {
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
2026-05-27 23:47:30 +02:00
|
|
|
display.fillRect(0, y, display.width(), bh);
|
2026-05-17 00:12:40 +02:00
|
|
|
display.setColor(DisplayDriver::DARK);
|
|
|
|
|
} else {
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
2026-05-27 23:47:30 +02:00
|
|
|
display.drawRect(0, y, display.width(), bh);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
display.fillRect(1, y + 1, display.width() - 2, lh);
|
2026-05-17 00:12:40 +02:00
|
|
|
display.setColor(DisplayDriver::DARK);
|
2026-05-27 23:47:30 +02:00
|
|
|
}
|
|
|
|
|
display.drawTextEllipsized(3, y + 1, display.width() - cw - 2 - age_w, sender);
|
|
|
|
|
if (age[0]) { display.setCursor(display.width() - age_w, y + 1); display.print(age); }
|
|
|
|
|
if (!sel) display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
if (portrait_expand) {
|
|
|
|
|
char trans[160];
|
|
|
|
|
display.translateUTF8ToBlocks(trans, skipReplyPrefix(e.text), sizeof(trans));
|
|
|
|
|
char wl[8][FS_CHARS_MAX];
|
|
|
|
|
int nl = FullscreenMsgView::wrapLines(display, trans, display.width() - 6, wl, 8);
|
|
|
|
|
for (int li = 0; li < nl; li++) { display.setCursor(3, y + (li + 1) * lh + 1); display.print(wl[li]); }
|
|
|
|
|
} else {
|
2026-05-23 14:45:24 +02:00
|
|
|
display.drawTextEllipsized(3, y + lh + 1, display.width() - cw - 2, skipReplyPrefix(e.text));
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (dm_count == 0) {
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
display.drawTextCentered(display.width()/2, display.height()/2, "No messages yet");
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
if (_dm_hist_scroll > 0) {
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
display.setCursor(display.width() - cw, hist_start_y + 1);
|
2026-05-17 00:12:40 +02:00
|
|
|
display.print("^");
|
|
|
|
|
}
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
if (_dm_hist_scroll + _hist_visible < dm_count) {
|
2026-05-27 23:47:30 +02:00
|
|
|
int arrow_y = (n_vis > 0) ? box_ys[n_vis - 1] + box_hs[n_vis - 1] - lh : hist_start_y;
|
|
|
|
|
display.setCursor(display.width() - cw, arrow_y);
|
2026-05-17 00:12:40 +02:00
|
|
|
display.print("v");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool compose_sel = (_dm_hist_sel == -1);
|
|
|
|
|
const char* ctxt = "[+ send]";
|
|
|
|
|
int ctw = display.getTextWidth(ctxt);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
int cbx = 1;
|
2026-05-17 00:12:40 +02:00
|
|
|
if (compose_sel) {
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
display.fillRect(cbx, cby - 1, ctw + 4, lh + 1);
|
2026-05-17 00:12:40 +02:00
|
|
|
display.setColor(DisplayDriver::DARK);
|
|
|
|
|
}
|
|
|
|
|
display.setCursor(cbx + 2, cby);
|
|
|
|
|
display.print(ctxt);
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
2026-05-18 16:11:25 +02:00
|
|
|
if (_ctx_menu.active) _ctx_menu.render(display);
|
2026-05-17 00:12:40 +02:00
|
|
|
return dm_count > 0 ? 500 : 2000;
|
|
|
|
|
|
|
|
|
|
} else if (_phase == CHANNEL_HIST) {
|
|
|
|
|
if (_fs.active && _hist_sel >= 0) {
|
|
|
|
|
int fs_hist_count = histCountForChannel(_sel_channel_idx);
|
|
|
|
|
int ring_pos = histEntryForChannel(_sel_channel_idx, _hist_sel);
|
|
|
|
|
if (ring_pos >= 0) {
|
|
|
|
|
const char* ftext = _hist[ring_pos].text;
|
|
|
|
|
const char* fsep = strstr(ftext, ": ");
|
|
|
|
|
char fsender[22], fmsg[140];
|
|
|
|
|
if (fsep) {
|
|
|
|
|
int nl = fsep - ftext; if (nl > 21) nl = 21;
|
|
|
|
|
strncpy(fsender, ftext, nl); fsender[nl] = '\0';
|
|
|
|
|
strncpy(fmsg, fsep + 2, sizeof(fmsg) - 1); fmsg[sizeof(fmsg)-1] = '\0';
|
|
|
|
|
} else {
|
2026-05-31 00:11:43 +02:00
|
|
|
strcpy(fsender, "?");
|
2026-05-17 00:12:40 +02:00
|
|
|
strncpy(fmsg, ftext, sizeof(fmsg) - 1); fmsg[sizeof(fmsg)-1] = '\0';
|
|
|
|
|
}
|
2026-05-18 16:11:25 +02:00
|
|
|
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;
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
return 2000;
|
|
|
|
|
}
|
|
|
|
|
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
int hist_start_y = display.headerH();
|
2026-05-22 23:13:27 +02:00
|
|
|
int cby = display.height() - lh - 2;
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
ChannelDetails ch;
|
|
|
|
|
the_mesh.getChannel(_sel_channel_idx, ch);
|
|
|
|
|
char title[24];
|
|
|
|
|
snprintf(title, sizeof(title), "%.23s", ch.name);
|
|
|
|
|
display.drawTextCentered(display.width()/2, 0, title);
|
2026-05-22 23:13:27 +02:00
|
|
|
display.fillRect(0, lh + 1, display.width(), display.sepH());
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
int ch_hist_count = histCountForChannel(_sel_channel_idx);
|
refactor(ui): apply code review fixes — bugs, risk, optimisations, cleanup
Fixes 7 bugs (timing UB on unsigned long, abs() no-op, XBM CRC PROGMEM
scan, CayenneLPP heap thrash ×2, UTF-8 reply prefix, lock-seq reset on
wake), 3 risk items (notif melody stop guard, bot trigger pre-lowercase,
lock-seq cleared on display wake), plus optimisations (fmtMsgAge/
getTextWidth caching, RTTTL builder consolidated to NodePrefs, HP_*
constants and homePageLabel table moved to NodePrefs, DataStore flat
lambda schema, translateUTF8Static factored out) and minor cleanup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:03:45 +02:00
|
|
|
uint32_t now_ts = rtc_clock.getCurrentTime();
|
2026-05-17 00:12:40 +02:00
|
|
|
|
2026-05-27 23:47:30 +02:00
|
|
|
// Portrait e-ink (height > width): variable-height boxes that show the full
|
|
|
|
|
// wrapped message text. All other displays/orientations: compact 2-line boxes.
|
|
|
|
|
bool portrait_expand = (display.height() > display.width());
|
|
|
|
|
const int MAX_VIS_BOXES = 8;
|
|
|
|
|
int box_ys[MAX_VIS_BOXES], box_hs[MAX_VIS_BOXES], n_vis = 0;
|
|
|
|
|
{
|
|
|
|
|
const int fixed_bh = 2 * lh + 1;
|
|
|
|
|
int cur_y = hist_start_y;
|
|
|
|
|
for (int ii = 0; ii < MAX_VIS_BOXES && (_hist_scroll + ii) < ch_hist_count; ii++) {
|
|
|
|
|
int bh = fixed_bh;
|
|
|
|
|
if (portrait_expand) {
|
|
|
|
|
int rp = histEntryForChannel(_sel_channel_idx, _hist_scroll + ii);
|
|
|
|
|
if (rp >= 0) {
|
|
|
|
|
const char* rtext = _hist[rp].text;
|
|
|
|
|
const char* rsep = strstr(rtext, ": ");
|
|
|
|
|
const char* rbody = rsep ? rsep + 2 : rtext;
|
|
|
|
|
char trans[160];
|
|
|
|
|
display.translateUTF8ToBlocks(trans, skipReplyPrefix(rbody), sizeof(trans));
|
|
|
|
|
char wl[8][FS_CHARS_MAX];
|
|
|
|
|
int nl = FullscreenMsgView::wrapLines(display, trans, display.width() - 6, wl, 8);
|
|
|
|
|
bh = (1 + (nl > 0 ? nl : 1)) * lh + 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (cur_y + bh > cby) break;
|
|
|
|
|
box_ys[n_vis] = cur_y; box_hs[n_vis] = bh; n_vis++;
|
|
|
|
|
cur_y += bh + (portrait_expand ? 2 : 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_hist_visible = n_vis;
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < n_vis && (_hist_scroll + i) < ch_hist_count; i++) {
|
2026-05-17 00:12:40 +02:00
|
|
|
int item = _hist_scroll + i;
|
|
|
|
|
bool sel = (item == _hist_sel);
|
2026-05-27 23:47:30 +02:00
|
|
|
int y = box_ys[i];
|
|
|
|
|
int bh = box_hs[i];
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
int ring_pos = histEntryForChannel(_sel_channel_idx, item);
|
|
|
|
|
if (ring_pos < 0) continue;
|
|
|
|
|
|
|
|
|
|
const char* text = _hist[ring_pos].text;
|
|
|
|
|
const char* sep = strstr(text, ": ");
|
2026-05-27 23:47:30 +02:00
|
|
|
char sender[22], msg_part[140];
|
2026-05-17 00:12:40 +02:00
|
|
|
if (sep) {
|
|
|
|
|
int nl = sep - text; if (nl > 21) nl = 21;
|
|
|
|
|
strncpy(sender, text, nl); sender[nl] = '\0';
|
|
|
|
|
strncpy(msg_part, sep + 2, sizeof(msg_part) - 1);
|
|
|
|
|
msg_part[sizeof(msg_part) - 1] = '\0';
|
|
|
|
|
} else {
|
2026-05-31 00:11:43 +02:00
|
|
|
strcpy(sender, "?");
|
2026-05-17 00:12:40 +02:00
|
|
|
strncpy(msg_part, text, sizeof(msg_part) - 1);
|
|
|
|
|
msg_part[sizeof(msg_part) - 1] = '\0';
|
|
|
|
|
}
|
|
|
|
|
|
refactor(ui): apply code review fixes — bugs, risk, optimisations, cleanup
Fixes 7 bugs (timing UB on unsigned long, abs() no-op, XBM CRC PROGMEM
scan, CayenneLPP heap thrash ×2, UTF-8 reply prefix, lock-seq reset on
wake), 3 risk items (notif melody stop guard, bot trigger pre-lowercase,
lock-seq cleared on display wake), plus optimisations (fmtMsgAge/
getTextWidth caching, RTTTL builder consolidated to NodePrefs, HP_*
constants and homePageLabel table moved to NodePrefs, DataStore flat
lambda schema, translateUTF8Static factored out) and minor cleanup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:03:45 +02:00
|
|
|
char age[6]; fmtMsgAge(age, sizeof(age), _hist[ring_pos].timestamp, now_ts);
|
2026-05-19 20:21:32 +02:00
|
|
|
int age_w = age[0] ? display.getTextWidth(age) + 3 : 0;
|
|
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
if (sel) {
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
2026-05-27 23:47:30 +02:00
|
|
|
display.fillRect(0, y, display.width(), bh);
|
2026-05-17 00:12:40 +02:00
|
|
|
display.setColor(DisplayDriver::DARK);
|
|
|
|
|
} else {
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
2026-05-27 23:47:30 +02:00
|
|
|
display.drawRect(0, y, display.width(), bh);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
display.fillRect(1, y + 1, display.width() - 2, lh);
|
2026-05-17 00:12:40 +02:00
|
|
|
display.setColor(DisplayDriver::DARK);
|
2026-05-27 23:47:30 +02:00
|
|
|
}
|
|
|
|
|
display.drawTextEllipsized(3, y + 1, display.width() - cw - 2 - age_w, sender);
|
|
|
|
|
if (age[0]) { display.setCursor(display.width() - age_w, y + 1); display.print(age); }
|
|
|
|
|
if (!sel) display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
if (portrait_expand) {
|
|
|
|
|
char trans[160];
|
|
|
|
|
display.translateUTF8ToBlocks(trans, skipReplyPrefix(msg_part), sizeof(trans));
|
|
|
|
|
char wl[8][FS_CHARS_MAX];
|
|
|
|
|
int nl = FullscreenMsgView::wrapLines(display, trans, display.width() - 6, wl, 8);
|
|
|
|
|
for (int li = 0; li < nl; li++) { display.setCursor(3, y + (li + 1) * lh + 1); display.print(wl[li]); }
|
|
|
|
|
} else {
|
2026-05-23 14:45:24 +02:00
|
|
|
display.drawTextEllipsized(3, y + lh + 1, display.width() - cw - 2, skipReplyPrefix(msg_part));
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ch_hist_count == 0) {
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
display.drawTextCentered(display.width()/2, display.height()/2, "No messages yet");
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// scroll hints
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
if (_hist_scroll > 0) {
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
display.setCursor(display.width() - cw, hist_start_y + 1);
|
2026-05-17 00:12:40 +02:00
|
|
|
display.print("^");
|
|
|
|
|
}
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
if (_hist_scroll + _hist_visible < ch_hist_count) {
|
2026-05-27 23:47:30 +02:00
|
|
|
int arrow_y = (n_vis > 0) ? box_ys[n_vis - 1] + box_hs[n_vis - 1] - lh : hist_start_y;
|
|
|
|
|
display.setCursor(display.width() - cw, arrow_y);
|
2026-05-17 00:12:40 +02:00
|
|
|
display.print("v");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// small compose button (bottom-left, always bordered, inverted when selected)
|
|
|
|
|
bool compose_sel = (_hist_sel == -1);
|
|
|
|
|
const char* ctxt = "[+ send]";
|
|
|
|
|
int ctw = display.getTextWidth(ctxt);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
int cbx = 1;
|
2026-05-17 00:12:40 +02:00
|
|
|
if (compose_sel) {
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
display.fillRect(cbx - 1, cby - 1, ctw + 4, lh + 2);
|
2026-05-17 00:12:40 +02:00
|
|
|
display.setColor(DisplayDriver::DARK);
|
|
|
|
|
} else {
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
display.drawRect(cbx - 1, cby - 1, ctw + 4, lh + 2);
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
display.setCursor(cbx + 1, cby);
|
|
|
|
|
display.print(ctxt);
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
2026-05-18 16:11:25 +02:00
|
|
|
if (_ctx_menu.active) _ctx_menu.render(display);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
} else if (_phase == KEYBOARD) {
|
|
|
|
|
return _kb.render(display);
|
|
|
|
|
|
|
|
|
|
} else { // MSG_PICK
|
|
|
|
|
char title[24];
|
2026-05-18 15:12:11 +02:00
|
|
|
if (_reply_mode) {
|
2026-05-18 18:18:45 +02:00
|
|
|
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);
|
2026-05-18 15:12:11 +02:00
|
|
|
} else if (_sending_to_channel) {
|
2026-05-17 00:12:40 +02:00
|
|
|
ChannelDetails ch;
|
|
|
|
|
the_mesh.getChannel(_sel_channel_idx, ch);
|
|
|
|
|
snprintf(title, sizeof(title), "%.23s", ch.name);
|
|
|
|
|
} else {
|
|
|
|
|
snprintf(title, sizeof(title), "TO:%.14s", _sel_contact.name);
|
|
|
|
|
}
|
|
|
|
|
display.drawTextCentered(display.width()/2, 0, title);
|
2026-05-22 23:13:27 +02:00
|
|
|
display.fillRect(0, display.headerH() - 1, display.width(), display.sepH());
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
int total_msg_items = 1 + _active_msg_count;
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
for (int i = 0; i < _visible && (_msg_scroll+i) < total_msg_items; i++) {
|
2026-05-17 00:12:40 +02:00
|
|
|
int idx = _msg_scroll + i;
|
|
|
|
|
bool sel = (idx == _msg_sel);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
int y = start_y + i * item_h;
|
2026-05-17 00:12:40 +02:00
|
|
|
|
refactor(ui): drawSelectionRow helper for the 5-line invert/restore pattern
Eight screens repeated the same if/else block to invert the row
background for selected items. Add DisplayDriver::drawSelectionRow that
sets LIGHT, optionally fills the rect, and leaves the colour as DARK
when sel (so the caller's text renders inverted). 61 lines removed
across SettingsScreen, KeyboardWidget (cells + special row), ToolsScreen,
DashboardConfigScreen, BotScreen, RingtoneEditorScreen (note slots +
menu), NearbyScreen, QuickMsgScreen (4 list views).
Card-style rows (QuickMsg hist, NearbyScreen discover, RingtoneEditor
notes display) keep their original drawRect/partial-fill outline for
unselected — they don't match the simple-invert pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:33:02 +02:00
|
|
|
display.drawSelectionRow(0, y - 1, display.width(), item_h - 1, sel);
|
2026-05-17 00:12:40 +02:00
|
|
|
display.setCursor(0, y);
|
|
|
|
|
display.print(sel ? ">" : " ");
|
|
|
|
|
|
|
|
|
|
if (idx == 0) {
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
display.setCursor(cw + 2, y);
|
2026-05-17 00:12:40 +02:00
|
|
|
display.print("Custom message...");
|
|
|
|
|
} else {
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
int slot = _active_msgs[idx - 1];
|
|
|
|
|
const char* tmpl = p ? p->custom_msgs[slot] : "";
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
display.drawTextEllipsized(cw + 2, y, display.width() - cw - 4, tmpl);
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
if (_msg_scroll > 0) { display.setCursor(display.width() - cw, start_y); display.print("^"); }
|
|
|
|
|
if (_msg_scroll + _visible < total_msg_items) { display.setCursor(display.width() - cw, start_y + (_visible-1)*item_h); display.print("v"); }
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
return 2000;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool handleInput(char c) override {
|
feat(nav): navigate to / save a location shared in a message
Locations already travel as plain text ({loc} expands to "lat,lon"), so any
message carrying coordinates can now be acted on. In the fullscreen message
view, the context-menu (Hold Enter) gains Navigate and Save waypoint
entries whenever the message contains a coordinate — alongside Reply.
- geo::parseLatLon: shared scanner for an embedded "lat,lon" (the {loc}
format), with an optional [WAY] tag that supplies a label. Range-checked,
requires a decimal point to avoid matching plain integer pairs. Will also
back the waypoint-share feature later.
- QuickMsgScreen: Navigate opens NavView inline (To/Hdg/distance, honouring
the global Units setting) over the message, back returns to it; Save
waypoint stores the point (label from [WAY], else auto-named). Works in
both DM and channel histories, and on incoming or outgoing messages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:28:17 +02:00
|
|
|
// Navigate view: any back key returns to the message it was opened from.
|
|
|
|
|
if (_nav_active) {
|
|
|
|
|
if (c == KEY_CANCEL || c == KEY_ENTER || c == KEY_CONTEXT_MENU) _nav_active = false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
if (_phase == MODE_SELECT) {
|
2026-05-24 22:43:31 +02:00
|
|
|
// Context menu (Mark-all-read) takes precedence while active.
|
|
|
|
|
if (_ctx_menu.active) {
|
|
|
|
|
auto res = _ctx_menu.handleInput(c);
|
|
|
|
|
if (res == PopupMenu::SELECTED) {
|
2026-05-24 22:46:54 +02:00
|
|
|
int cleared = 0;
|
2026-05-24 22:43:31 +02:00
|
|
|
if (_mode_sel == 0) {
|
2026-05-24 22:46:54 +02:00
|
|
|
cleared = getDMUnreadTotal();
|
2026-05-24 22:43:31 +02:00
|
|
|
_task->clearAllDMUnread();
|
|
|
|
|
} else if (_mode_sel == 1) {
|
2026-05-24 22:46:54 +02:00
|
|
|
cleared = _task->getChannelUnreadCount();
|
2026-05-24 22:43:31 +02:00
|
|
|
clearAllChannelUnread();
|
|
|
|
|
} else {
|
2026-05-24 22:46:54 +02:00
|
|
|
cleared = _task->getRoomUnreadCount();
|
2026-05-24 22:43:31 +02:00
|
|
|
_task->clearRoomUnread();
|
|
|
|
|
}
|
2026-05-24 22:46:54 +02:00
|
|
|
char msg[32];
|
|
|
|
|
snprintf(msg, sizeof(msg), "%d marked read", cleared);
|
|
|
|
|
_task->showAlert(msg, 800);
|
2026-05-24 22:43:31 +02:00
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
if (c == KEY_CANCEL) { _task->gotoHomeScreen(); return true; }
|
|
|
|
|
if (c == KEY_UP && _mode_sel > 0) { _mode_sel--; return true; }
|
|
|
|
|
if (c == KEY_DOWN && _mode_sel < 2) { _mode_sel++; return true; }
|
2026-05-24 22:43:31 +02:00
|
|
|
if (c == KEY_CONTEXT_MENU) {
|
|
|
|
|
// PopupMenu stores the title pointer verbatim — use static strings.
|
|
|
|
|
static const char* MODE_TITLES[] = { "DM options", "Channel options", "Room options" };
|
|
|
|
|
_ctx_menu.begin(MODE_TITLES[_mode_sel < 3 ? _mode_sel : 0], 1);
|
|
|
|
|
_ctx_menu.addItem("Mark all read");
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
if (c == KEY_ENTER) {
|
|
|
|
|
if (_mode_sel == 1) {
|
|
|
|
|
_phase = CHANNEL_PICK;
|
|
|
|
|
} else {
|
|
|
|
|
_room_mode = (_mode_sel == 2);
|
|
|
|
|
if (_room_mode) _task->clearRoomUnread();
|
|
|
|
|
buildContactList();
|
|
|
|
|
_contact_sel = _contact_scroll = 0;
|
|
|
|
|
_phase = CONTACT_PICK;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else if (_phase == CONTACT_PICK) {
|
|
|
|
|
// Context menu consumes all input while open
|
|
|
|
|
if (_ctx_menu.active) {
|
2026-05-29 00:25:02 +02:00
|
|
|
// LEFT/RIGHT cycle Notif/Melody in-place (menu stays open).
|
|
|
|
|
if (!_pin_picker_active && _num_contacts > 0) {
|
|
|
|
|
bool left = (c == KEY_LEFT || c == KEY_PREV);
|
|
|
|
|
bool right = (c == KEY_RIGHT || c == KEY_NEXT);
|
|
|
|
|
if (left || right) {
|
|
|
|
|
static const char* NOTIF_LABELS[] = { "default", "OFF", "ON" };
|
|
|
|
|
static const char* ML[] = { "global", "M1", "M2" };
|
|
|
|
|
ContactInfo ci;
|
|
|
|
|
if (the_mesh.getContactByIdx(_sorted[_contact_sel], ci)) {
|
|
|
|
|
int sel = _ctx_menu.selectedIndex();
|
|
|
|
|
if (sel == 1) {
|
|
|
|
|
uint8_t v = dmNotifState(ci.id.pub_key);
|
|
|
|
|
v = right ? (v + 1) % 3 : (v + 2) % 3;
|
|
|
|
|
setDmNotifState(ci.id.pub_key, v);
|
|
|
|
|
snprintf(_ctx_notif_item, sizeof(_ctx_notif_item), "Notif: %s", NOTIF_LABELS[v]);
|
|
|
|
|
_ctx_dirty = true;
|
|
|
|
|
} else if (sel == 2) {
|
|
|
|
|
uint8_t v = dmMelodySlot(ci.id.pub_key);
|
|
|
|
|
v = right ? (v + 1) % 3 : (v + 2) % 3;
|
|
|
|
|
setDmMelody(ci.id.pub_key, v);
|
|
|
|
|
snprintf(_ctx_melody_item, sizeof(_ctx_melody_item), "Melody: %s", ML[v]);
|
|
|
|
|
_ctx_dirty = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
auto res = _ctx_menu.handleInput(c);
|
2026-05-24 23:30:16 +02:00
|
|
|
if (_pin_picker_active) {
|
|
|
|
|
// Slot picker sub-menu: index 0..FAVOURITES_COUNT-1 maps directly to slot.
|
|
|
|
|
if (res == PopupMenu::SELECTED && _num_contacts > 0) {
|
|
|
|
|
ContactInfo ci;
|
|
|
|
|
if (the_mesh.getContactByIdx(_sorted[_contact_sel], ci)) {
|
|
|
|
|
int slot = _ctx_menu.selectedIndex();
|
|
|
|
|
_task->setFavouriteSlot(slot, ci.id.pub_key);
|
|
|
|
|
the_mesh.savePrefs();
|
|
|
|
|
char alert[24];
|
|
|
|
|
snprintf(alert, sizeof(alert), "Pinned to slot %d", slot + 1);
|
|
|
|
|
_task->showAlert(alert, 800);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (res != PopupMenu::NONE) _pin_picker_active = false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
if (res == PopupMenu::SELECTED && _num_contacts > 0) {
|
|
|
|
|
ContactInfo ci;
|
|
|
|
|
if (the_mesh.getContactByIdx(_sorted[_contact_sel], ci)) {
|
2026-05-29 00:25:02 +02:00
|
|
|
int sel = _ctx_menu.selectedIndex();
|
|
|
|
|
if (sel == 0) {
|
2026-05-17 00:12:40 +02:00
|
|
|
_task->clearDMUnread(ci.id.pub_key);
|
2026-05-29 00:25:02 +02:00
|
|
|
} else if (sel == 3) {
|
2026-05-24 23:30:16 +02:00
|
|
|
// Pin / Unpin
|
|
|
|
|
int pinned_slot = _task->findFavouriteSlot(ci.id.pub_key);
|
|
|
|
|
if (pinned_slot >= 0) {
|
|
|
|
|
_task->clearFavouriteSlot(pinned_slot);
|
|
|
|
|
the_mesh.savePrefs();
|
|
|
|
|
char alert[24];
|
|
|
|
|
snprintf(alert, sizeof(alert), "Unpinned (slot %d)", pinned_slot + 1);
|
|
|
|
|
_task->showAlert(alert, 800);
|
|
|
|
|
} else {
|
|
|
|
|
for (int s = 0; s < NodePrefs::FAVOURITES_COUNT; s++) {
|
|
|
|
|
if (_task->isFavouriteSlotEmpty(s)) {
|
|
|
|
|
snprintf(_pin_slot_labels[s], sizeof(_pin_slot_labels[s]), "Slot %d: empty", s + 1);
|
|
|
|
|
} else {
|
|
|
|
|
char nm[16] = "?";
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
if (p) {
|
|
|
|
|
const uint8_t* pfx = p->favourite_contacts[s];
|
|
|
|
|
for (int idx = 0; ; idx++) {
|
|
|
|
|
ContactInfo c2;
|
|
|
|
|
if (!the_mesh.getContactByIdx(idx, c2)) break;
|
|
|
|
|
if (memcmp(c2.id.pub_key, pfx, NodePrefs::FAVOURITE_PREFIX_LEN) == 0) {
|
|
|
|
|
DisplayDriver::translateUTF8Static(nm, c2.name, sizeof(nm));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
snprintf(_pin_slot_labels[s], sizeof(_pin_slot_labels[s]), "Slot %d: %s", s + 1, nm);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ctx_menu.begin("Pick slot", 3);
|
|
|
|
|
for (int s = 0; s < NodePrefs::FAVOURITES_COUNT; s++) _ctx_menu.addItem(_pin_slot_labels[s]);
|
|
|
|
|
_pin_picker_active = true;
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
2026-05-29 00:25:02 +02:00
|
|
|
// sel == 1 (Notif) and sel == 2 (Melody): already cycled via LEFT/RIGHT; ENTER just closes.
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (res != PopupMenu::NONE && _ctx_dirty) {
|
|
|
|
|
the_mesh.savePrefs(); _ctx_dirty = false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (c == KEY_CANCEL) { _room_mode = false; _phase = MODE_SELECT; return true; }
|
|
|
|
|
if (c == KEY_UP && _contact_sel > 0) {
|
|
|
|
|
_contact_sel--;
|
|
|
|
|
if (_contact_sel < _contact_scroll) _contact_scroll = _contact_sel;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (c == KEY_DOWN && _contact_sel < _num_contacts - 1) {
|
|
|
|
|
_contact_sel++;
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
if (_contact_sel >= _contact_scroll + _visible) _contact_scroll = _contact_sel - _visible + 1;
|
2026-05-17 00:12:40 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (c == KEY_ENTER && _num_contacts > 0) {
|
|
|
|
|
if (the_mesh.getContactByIdx(_sorted[_contact_sel], _sel_contact)) {
|
|
|
|
|
_task->clearDMUnread(_sel_contact.id.pub_key);
|
|
|
|
|
_dm_hist_sel = -1;
|
|
|
|
|
_dm_hist_scroll = 0;
|
|
|
|
|
_dm_fs.active = false;
|
|
|
|
|
_phase = DM_HIST;
|
2026-06-04 11:38:12 +02:00
|
|
|
if (_share_mode) beginShareCompose(false);
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (c == KEY_CONTEXT_MENU && _num_contacts > 0 && !_room_mode) {
|
|
|
|
|
static const char* NOTIF_LABELS[] = { "default", "OFF", "ON" };
|
|
|
|
|
ContactInfo ci;
|
|
|
|
|
the_mesh.getContactByIdx(_sorted[_contact_sel], ci);
|
|
|
|
|
snprintf(_ctx_notif_item, sizeof(_ctx_notif_item), "Notif: %s",
|
|
|
|
|
NOTIF_LABELS[dmNotifState(ci.id.pub_key)]);
|
|
|
|
|
{ static const char* ML[] = { "global", "M1", "M2" };
|
|
|
|
|
snprintf(_ctx_melody_item, sizeof(_ctx_melody_item), "Melody: %s",
|
|
|
|
|
ML[dmMelodySlot(ci.id.pub_key)]); }
|
2026-05-24 23:30:16 +02:00
|
|
|
int pinned_slot = _task->findFavouriteSlot(ci.id.pub_key);
|
|
|
|
|
if (pinned_slot >= 0) snprintf(_ctx_pin_item, sizeof(_ctx_pin_item), "Unpin (slot %d)", pinned_slot + 1);
|
|
|
|
|
else snprintf(_ctx_pin_item, sizeof(_ctx_pin_item), "Pin to dial");
|
2026-05-17 00:12:40 +02:00
|
|
|
_ctx_menu.begin("Contact options", 3);
|
|
|
|
|
_ctx_menu.addItem("Mark as read");
|
|
|
|
|
_ctx_menu.addItem(_ctx_notif_item);
|
|
|
|
|
_ctx_menu.addItem(_ctx_melody_item);
|
2026-05-24 23:30:16 +02:00
|
|
|
_ctx_menu.addItem(_ctx_pin_item);
|
2026-05-17 00:12:40 +02:00
|
|
|
_ctx_dirty = false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else if (_phase == CHANNEL_PICK) {
|
|
|
|
|
// Context menu consumes all input while open
|
|
|
|
|
if (_ctx_menu.active) {
|
2026-05-29 00:25:02 +02:00
|
|
|
// LEFT/RIGHT cycle Notif/Melody/Fav in-place (menu stays open).
|
|
|
|
|
if (_num_channels > 0) {
|
|
|
|
|
bool left = (c == KEY_LEFT || c == KEY_PREV);
|
|
|
|
|
bool right = (c == KEY_RIGHT || c == KEY_NEXT);
|
|
|
|
|
if (left || right) {
|
|
|
|
|
static const char* NOTIF_LABELS[] = { "default", "OFF", "ON" };
|
|
|
|
|
static const char* ML[] = { "global", "M1", "M2" };
|
|
|
|
|
uint8_t ch_idx = _channel_indices[_channel_sel];
|
|
|
|
|
int sel = _ctx_menu.selectedIndex();
|
|
|
|
|
if (sel == 1) {
|
|
|
|
|
uint8_t v = chNotifState(ch_idx);
|
|
|
|
|
v = right ? (v + 1) % 3 : (v + 2) % 3;
|
|
|
|
|
setChNotifState(ch_idx, v);
|
|
|
|
|
snprintf(_ctx_notif_item, sizeof(_ctx_notif_item), "Notif: %s", NOTIF_LABELS[v]);
|
|
|
|
|
_ctx_dirty = true;
|
|
|
|
|
} else if (sel == 2) {
|
|
|
|
|
uint8_t v = chNotifMelody(ch_idx);
|
|
|
|
|
v = right ? (v + 1) % 3 : (v + 2) % 3;
|
|
|
|
|
setChNotifMelody(ch_idx, v);
|
|
|
|
|
snprintf(_ctx_melody_item, sizeof(_ctx_melody_item), "Melody: %s", ML[v]);
|
|
|
|
|
_ctx_dirty = true;
|
|
|
|
|
} else if (sel == 3) {
|
|
|
|
|
NodePrefs* p2 = _task->getNodePrefs();
|
|
|
|
|
if (p2) {
|
|
|
|
|
p2->ch_fav_bitmask ^= (1ULL << ch_idx);
|
|
|
|
|
bool is_fav = (p2->ch_fav_bitmask & (1ULL << ch_idx));
|
|
|
|
|
snprintf(_ctx_ch_fav_item, sizeof(_ctx_ch_fav_item), is_fav ? "Fav: yes" : "Fav: no");
|
|
|
|
|
_ctx_dirty = true;
|
|
|
|
|
buildChannelList();
|
|
|
|
|
if (_channel_sel >= _num_channels) _channel_sel = _num_channels > 0 ? _num_channels - 1 : 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
auto res = _ctx_menu.handleInput(c);
|
|
|
|
|
if (res == PopupMenu::SELECTED && _num_channels > 0) {
|
|
|
|
|
uint8_t ch_idx = _channel_indices[_channel_sel];
|
2026-05-29 00:25:02 +02:00
|
|
|
int sel = _ctx_menu.selectedIndex();
|
|
|
|
|
if (sel == 0) {
|
2026-05-17 00:12:40 +02:00
|
|
|
_ch_unread[ch_idx] = 0;
|
|
|
|
|
}
|
2026-05-29 00:25:02 +02:00
|
|
|
// sel 1/2/3 already handled by LEFT/RIGHT; ENTER just closes.
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
if (res != PopupMenu::NONE && _ctx_dirty) {
|
|
|
|
|
the_mesh.savePrefs(); _ctx_dirty = false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (c == KEY_CANCEL) { _phase = MODE_SELECT; return true; }
|
|
|
|
|
if (c == KEY_UP && _channel_sel > 0) {
|
|
|
|
|
_channel_sel--;
|
|
|
|
|
if (_channel_sel < _channel_scroll) _channel_scroll = _channel_sel;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (c == KEY_DOWN && _channel_sel < _num_channels - 1) {
|
|
|
|
|
_channel_sel++;
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
if (_channel_sel >= _channel_scroll + _visible) _channel_scroll = _channel_sel - _visible + 1;
|
2026-05-17 00:12:40 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (c == KEY_ENTER && _num_channels > 0) {
|
|
|
|
|
_sel_channel_idx = _channel_indices[_channel_sel];
|
|
|
|
|
int hc = histCountForChannel(_sel_channel_idx);
|
|
|
|
|
_unread_at_entry = (int)_ch_unread[_sel_channel_idx];
|
|
|
|
|
_hist_scroll = 0;
|
|
|
|
|
_hist_sel = hc > 0 ? 0 : -1;
|
|
|
|
|
_viewing_max_seen = _hist_sel >= 0 ? _hist_sel : 0;
|
|
|
|
|
_phase = CHANNEL_HIST;
|
|
|
|
|
updateChannelUnread();
|
2026-06-04 11:38:12 +02:00
|
|
|
if (_share_mode) beginShareCompose(true);
|
2026-05-17 00:12:40 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (c == KEY_CONTEXT_MENU && _num_channels > 0) {
|
|
|
|
|
uint8_t ch_idx = _channel_indices[_channel_sel];
|
|
|
|
|
static const char* NOTIF_LABELS[] = { "default", "OFF", "ON" };
|
|
|
|
|
snprintf(_ctx_notif_item, sizeof(_ctx_notif_item), "Notif: %s",
|
|
|
|
|
NOTIF_LABELS[chNotifState(ch_idx)]);
|
|
|
|
|
{ static const char* ML[] = { "global", "M1", "M2" };
|
|
|
|
|
snprintf(_ctx_melody_item, sizeof(_ctx_melody_item), "Melody: %s",
|
|
|
|
|
ML[chNotifMelody(ch_idx)]); }
|
2026-05-27 17:33:52 +02:00
|
|
|
{ NodePrefs* p2 = _task->getNodePrefs();
|
|
|
|
|
bool is_fav = p2 && (p2->ch_fav_bitmask & (1ULL << ch_idx));
|
2026-05-27 23:47:30 +02:00
|
|
|
snprintf(_ctx_ch_fav_item, sizeof(_ctx_ch_fav_item), is_fav ? "Fav: yes" : "Fav: no"); }
|
2026-05-27 17:33:52 +02:00
|
|
|
_ctx_menu.begin("Channel options", 4);
|
2026-05-17 00:12:40 +02:00
|
|
|
_ctx_menu.addItem("Mark all read");
|
|
|
|
|
_ctx_menu.addItem(_ctx_notif_item);
|
|
|
|
|
_ctx_menu.addItem(_ctx_melody_item);
|
2026-05-27 17:33:52 +02:00
|
|
|
_ctx_menu.addItem(_ctx_ch_fav_item);
|
2026-05-17 00:12:40 +02:00
|
|
|
_ctx_dirty = false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else if (_phase == DM_HIST) {
|
|
|
|
|
int dm_count = dmHistCountForContact(_sel_contact.id.pub_key);
|
|
|
|
|
if (_dm_fs.active) {
|
2026-05-18 15:12:11 +02:00
|
|
|
if (_ctx_menu.active) {
|
|
|
|
|
auto res = _ctx_menu.handleInput(c);
|
|
|
|
|
if (res == PopupMenu::SELECTED) {
|
feat(nav): navigate to / save a location shared in a message
Locations already travel as plain text ({loc} expands to "lat,lon"), so any
message carrying coordinates can now be acted on. In the fullscreen message
view, the context-menu (Hold Enter) gains Navigate and Save waypoint
entries whenever the message contains a coordinate — alongside Reply.
- geo::parseLatLon: shared scanner for an embedded "lat,lon" (the {loc}
format), with an optional [WAY] tag that supplies a label. Range-checked,
requires a decimal point to avoid matching plain integer pairs. Will also
back the waypoint-share feature later.
- QuickMsgScreen: Navigate opens NavView inline (To/Hdg/distance, honouring
the global Units setting) over the message, back returns to it; Save
waypoint stores the point (label from [WAY], else auto-named). Works in
both DM and channel histories, and on incoming or outgoing messages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:28:17 +02:00
|
|
|
dispatchFsAction(false);
|
2026-05-18 15:12:11 +02:00
|
|
|
} else if (res != PopupMenu::NONE) {
|
|
|
|
|
_ctx_menu.active = false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
auto res = _dm_fs.handleInput(c);
|
|
|
|
|
if (res == FullscreenMsgView::PREV) {
|
|
|
|
|
if (_dm_hist_sel < dm_count - 1) { _dm_hist_sel++; _dm_fs.scroll = 0; }
|
|
|
|
|
} else if (res == FullscreenMsgView::NEXT) {
|
|
|
|
|
if (_dm_hist_sel > 0) { _dm_hist_sel--; _dm_fs.scroll = 0; }
|
|
|
|
|
} else if (res == FullscreenMsgView::CLOSE) {
|
|
|
|
|
_dm_fs.active = false;
|
2026-05-18 15:12:11 +02:00
|
|
|
} else if (res == FullscreenMsgView::REPLY) {
|
|
|
|
|
int ring_pos = dmHistEntryForContact(_sel_contact.id.pub_key, _dm_hist_sel);
|
feat(nav): navigate to / save a location shared in a message
Locations already travel as plain text ({loc} expands to "lat,lon"), so any
message carrying coordinates can now be acted on. In the fullscreen message
view, the context-menu (Hold Enter) gains Navigate and Save waypoint
entries whenever the message contains a coordinate — alongside Reply.
- geo::parseLatLon: shared scanner for an embedded "lat,lon" (the {loc}
format), with an optional [WAY] tag that supplies a label. Range-checked,
requires a decimal point to avoid matching plain integer pairs. Will also
back the waypoint-share feature later.
- QuickMsgScreen: Navigate opens NavView inline (To/Hdg/distance, honouring
the global Units setting) over the message, back returns to it; Save
waypoint stores the point (label from [WAY], else auto-named). Works in
both DM and channel histories, and on incoming or outgoing messages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:28:17 +02:00
|
|
|
if (ring_pos >= 0) {
|
|
|
|
|
bool reply_ok = !_dm_hist[ring_pos].outgoing;
|
|
|
|
|
if (reply_ok) {
|
|
|
|
|
char _tname[32]; DisplayDriver::translateUTF8Static(_tname, _sel_contact.name, sizeof(_tname));
|
|
|
|
|
snprintf(_reply_prefix, sizeof(_reply_prefix), "@[%.31s] ", _tname);
|
|
|
|
|
}
|
|
|
|
|
buildFsMenu(_dm_hist[ring_pos].text, reply_ok);
|
2026-05-18 15:12:11 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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;
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-05-25 07:58:45 +02:00
|
|
|
if (c == KEY_CANCEL) {
|
|
|
|
|
if (_dm_direct_entry) {
|
|
|
|
|
_dm_direct_entry = false;
|
|
|
|
|
_task->gotoHomeScreen();
|
|
|
|
|
} else {
|
|
|
|
|
_phase = CONTACT_PICK;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
if (c == KEY_UP) {
|
|
|
|
|
if (_dm_hist_sel > 0) {
|
|
|
|
|
_dm_hist_sel--;
|
|
|
|
|
if (_dm_hist_sel < _dm_hist_scroll) _dm_hist_scroll = _dm_hist_sel;
|
|
|
|
|
} else if (_dm_hist_sel == 0) {
|
|
|
|
|
_dm_hist_sel = -1;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (c == KEY_DOWN) {
|
|
|
|
|
if (_dm_hist_sel == -1 && dm_count > 0) { _dm_hist_sel = 0; _dm_hist_scroll = 0; }
|
|
|
|
|
else if (_dm_hist_sel >= 0 && _dm_hist_sel < dm_count - 1) {
|
|
|
|
|
_dm_hist_sel++;
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
if (_dm_hist_sel >= _dm_hist_scroll + _hist_visible)
|
|
|
|
|
_dm_hist_scroll = _dm_hist_sel - _hist_visible + 1;
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (c == KEY_ENTER) {
|
|
|
|
|
if (_dm_hist_sel >= 0) {
|
|
|
|
|
_dm_fs.begin();
|
|
|
|
|
} else {
|
|
|
|
|
_sending_to_channel = false;
|
|
|
|
|
setupMsgPick();
|
|
|
|
|
_phase = MSG_PICK;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-05-18 15:12:11 +02:00
|
|
|
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) {
|
refactor(ui): apply code review fixes — bugs, risk, optimisations, cleanup
Fixes 7 bugs (timing UB on unsigned long, abs() no-op, XBM CRC PROGMEM
scan, CayenneLPP heap thrash ×2, UTF-8 reply prefix, lock-seq reset on
wake), 3 risk items (notif melody stop guard, bot trigger pre-lowercase,
lock-seq cleared on display wake), plus optimisations (fmtMsgAge/
getTextWidth caching, RTTTL builder consolidated to NodePrefs, HP_*
constants and homePageLabel table moved to NodePrefs, DataStore flat
lambda schema, translateUTF8Static factored out) and minor cleanup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:03:45 +02:00
|
|
|
char _tname[32]; DisplayDriver::translateUTF8Static(_tname, _sel_contact.name, sizeof(_tname));
|
|
|
|
|
snprintf(_reply_prefix, sizeof(_reply_prefix), "@[%.31s] ", _tname);
|
2026-05-18 15:12:11 +02:00
|
|
|
_ctx_menu.begin("Options", 1);
|
|
|
|
|
_ctx_menu.addItem("Reply");
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
} else if (_phase == CHANNEL_HIST) {
|
|
|
|
|
int ch_hist_count = histCountForChannel(_sel_channel_idx);
|
|
|
|
|
if (_fs.active) {
|
2026-05-18 15:12:11 +02:00
|
|
|
if (_ctx_menu.active) {
|
|
|
|
|
auto res = _ctx_menu.handleInput(c);
|
|
|
|
|
if (res == PopupMenu::SELECTED) {
|
feat(nav): navigate to / save a location shared in a message
Locations already travel as plain text ({loc} expands to "lat,lon"), so any
message carrying coordinates can now be acted on. In the fullscreen message
view, the context-menu (Hold Enter) gains Navigate and Save waypoint
entries whenever the message contains a coordinate — alongside Reply.
- geo::parseLatLon: shared scanner for an embedded "lat,lon" (the {loc}
format), with an optional [WAY] tag that supplies a label. Range-checked,
requires a decimal point to avoid matching plain integer pairs. Will also
back the waypoint-share feature later.
- QuickMsgScreen: Navigate opens NavView inline (To/Hdg/distance, honouring
the global Units setting) over the message, back returns to it; Save
waypoint stores the point (label from [WAY], else auto-named). Works in
both DM and channel histories, and on incoming or outgoing messages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:28:17 +02:00
|
|
|
dispatchFsAction(true);
|
2026-05-18 15:12:11 +02:00
|
|
|
} else if (res != PopupMenu::NONE) {
|
|
|
|
|
_ctx_menu.active = false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
auto res = _fs.handleInput(c);
|
|
|
|
|
if (res == FullscreenMsgView::PREV) {
|
|
|
|
|
if (_hist_sel < ch_hist_count - 1) { _hist_sel++; _fs.scroll = 0; updateChannelUnread(); }
|
|
|
|
|
} else if (res == FullscreenMsgView::NEXT) {
|
|
|
|
|
if (_hist_sel > 0) { _hist_sel--; _fs.scroll = 0; updateChannelUnread(); }
|
|
|
|
|
} else if (res == FullscreenMsgView::CLOSE) {
|
|
|
|
|
_fs.active = false;
|
2026-05-18 15:12:11 +02:00
|
|
|
} else if (res == FullscreenMsgView::REPLY) {
|
|
|
|
|
int ring_pos = histEntryForChannel(_sel_channel_idx, _hist_sel);
|
feat(nav): navigate to / save a location shared in a message
Locations already travel as plain text ({loc} expands to "lat,lon"), so any
message carrying coordinates can now be acted on. In the fullscreen message
view, the context-menu (Hold Enter) gains Navigate and Save waypoint
entries whenever the message contains a coordinate — alongside Reply.
- geo::parseLatLon: shared scanner for an embedded "lat,lon" (the {loc}
format), with an optional [WAY] tag that supplies a label. Range-checked,
requires a decimal point to avoid matching plain integer pairs. Will also
back the waypoint-share feature later.
- QuickMsgScreen: Navigate opens NavView inline (To/Hdg/distance, honouring
the global Units setting) over the message, back returns to it; Save
waypoint stores the point (label from [WAY], else auto-named). Works in
both DM and channel histories, and on incoming or outgoing messages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:28:17 +02:00
|
|
|
if (ring_pos >= 0)
|
|
|
|
|
buildFsMenu(_hist[ring_pos].text, buildChannelReplyPrefix(_hist[ring_pos].text));
|
2026-05-18 15:12:11 +02:00
|
|
|
}
|
|
|
|
|
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;
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (c == KEY_CANCEL) { _phase = CHANNEL_PICK; return true; }
|
|
|
|
|
if (c == KEY_UP) {
|
|
|
|
|
if (_hist_sel > 0) { _hist_sel--; if (_hist_sel < _hist_scroll) _hist_scroll = _hist_sel; }
|
|
|
|
|
else if (_hist_sel == 0) _hist_sel = -1;
|
|
|
|
|
updateChannelUnread();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (c == KEY_DOWN) {
|
|
|
|
|
if (_hist_sel == -1 && ch_hist_count > 0) { _hist_sel = 0; _hist_scroll = 0; }
|
|
|
|
|
else if (_hist_sel >= 0 && _hist_sel < ch_hist_count - 1) {
|
|
|
|
|
_hist_sel++;
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
if (_hist_sel >= _hist_scroll + _hist_visible) _hist_scroll = _hist_sel - _hist_visible + 1;
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
updateChannelUnread();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (c == KEY_ENTER) {
|
|
|
|
|
if (_hist_sel >= 0) {
|
|
|
|
|
_fs.begin();
|
|
|
|
|
updateChannelUnread();
|
|
|
|
|
} else {
|
|
|
|
|
_sending_to_channel = true;
|
|
|
|
|
setupMsgPick();
|
|
|
|
|
_phase = MSG_PICK;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-05-18 15:12:11 +02:00
|
|
|
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;
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
} else if (_phase == KEYBOARD) {
|
|
|
|
|
auto res = _kb.handleInput(c);
|
|
|
|
|
if (res == KeyboardWidget::CANCELLED) {
|
2026-06-04 11:38:12 +02:00
|
|
|
if (_share_mode) { _share_mode = false; _task->gotoHomeScreen(); }
|
|
|
|
|
else { _phase = MSG_PICK; }
|
2026-05-17 00:12:40 +02:00
|
|
|
} else if (res == KeyboardWidget::DONE) {
|
2026-05-24 19:53:05 +02:00
|
|
|
int prefix_len = _reply_mode ? (int)strlen(_reply_prefix) : 0;
|
|
|
|
|
if (_kb.len > prefix_len) {
|
|
|
|
|
// Expand only the body — prefix "@[nick] " is preserved verbatim, so a nick
|
|
|
|
|
// that happens to contain a placeholder token isn't substituted.
|
2026-05-17 00:12:40 +02:00
|
|
|
char expanded[KB_MAX_LEN + 1];
|
2026-05-24 19:53:05 +02:00
|
|
|
if (prefix_len > 0) {
|
|
|
|
|
memcpy(expanded, _kb.buf, prefix_len);
|
|
|
|
|
expandMsg(_kb.buf + prefix_len, expanded + prefix_len, sizeof(expanded) - prefix_len);
|
|
|
|
|
} else {
|
|
|
|
|
expandMsg(_kb.buf, expanded, sizeof(expanded));
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
bool ok = sendText(expanded);
|
|
|
|
|
afterSend(ok, expanded);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
} else { // MSG_PICK
|
|
|
|
|
int total_msg_items = 1 + _active_msg_count;
|
|
|
|
|
if (c == KEY_CANCEL) {
|
2026-05-18 15:12:11 +02:00
|
|
|
_reply_mode = false;
|
2026-05-17 00:12:40 +02:00
|
|
|
_phase = _sending_to_channel ? CHANNEL_HIST : DM_HIST;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (c == KEY_UP && _msg_sel > 0) {
|
|
|
|
|
_msg_sel--;
|
|
|
|
|
if (_msg_sel < _msg_scroll) _msg_scroll = _msg_sel;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (c == KEY_DOWN && _msg_sel < total_msg_items - 1) {
|
|
|
|
|
_msg_sel++;
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
if (_msg_sel >= _msg_scroll + _visible) _msg_scroll = _msg_sel - _visible + 1;
|
2026-05-17 00:12:40 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (c == KEY_ENTER) {
|
|
|
|
|
if (_msg_sel == 0) {
|
2026-05-18 15:12:11 +02:00
|
|
|
_kb.begin(_reply_mode ? _reply_prefix : "");
|
2026-05-17 00:12:40 +02:00
|
|
|
kbAddSensorPlaceholders(_kb, &sensors);
|
|
|
|
|
_phase = KEYBOARD;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
int slot = _active_msgs[_msg_sel - 1];
|
|
|
|
|
const char* tmpl = p ? p->custom_msgs[slot] : "OK";
|
2026-05-18 09:53:36 +02:00
|
|
|
char msg[140];
|
2026-05-18 15:12:11 +02:00
|
|
|
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));
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
bool ok = sendText(msg);
|
|
|
|
|
afterSend(ok, msg);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
};
|