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.
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
static const int CH_HIST_MAX = 32;
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
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-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-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) {
|
|
|
|
|
int pos;
|
|
|
|
|
if (_hist_count < CH_HIST_MAX) {
|
|
|
|
|
pos = (_hist_head + _hist_count) % CH_HIST_MAX;
|
|
|
|
|
_hist_count++;
|
|
|
|
|
} else {
|
|
|
|
|
pos = _hist_head;
|
|
|
|
|
_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';
|
|
|
|
|
|
|
|
|
|
if (ch_idx < MAX_GROUP_CHANNELS) {
|
|
|
|
|
bool viewing = (_phase == CHANNEL_HIST && _sel_channel_idx == (int)ch_idx);
|
|
|
|
|
if (!viewing && _ch_unread[ch_idx] < 99) _ch_unread[ch_idx]++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
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-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);
|
|
|
|
|
|
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 {
|
|
|
|
|
strncpy(fsender, "?", sizeof(fsender));
|
|
|
|
|
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 {
|
|
|
|
|
strncpy(sender, "?", sizeof(sender));
|
|
|
|
|
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 {
|
|
|
|
|
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) {
|
|
|
|
|
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)) {
|
|
|
|
|
if (_ctx_menu.selectedIndex() == 0) {
|
|
|
|
|
_task->clearDMUnread(ci.id.pub_key);
|
|
|
|
|
} else if (_ctx_menu.selectedIndex() == 1) {
|
|
|
|
|
uint8_t nstate = dmNotifState(ci.id.pub_key);
|
|
|
|
|
setDmNotifState(ci.id.pub_key, (nstate + 1) % 3);
|
|
|
|
|
_ctx_dirty = true;
|
2026-05-24 23:30:16 +02:00
|
|
|
} else if (_ctx_menu.selectedIndex() == 2) {
|
2026-05-17 00:12:40 +02:00
|
|
|
uint8_t slot = dmMelodySlot(ci.id.pub_key);
|
|
|
|
|
setDmMelody(ci.id.pub_key, (slot + 1) % 3);
|
|
|
|
|
_ctx_dirty = true;
|
2026-05-24 23:30:16 +02:00
|
|
|
} else if (_ctx_menu.selectedIndex() == 3) {
|
|
|
|
|
// 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 {
|
|
|
|
|
// Open slot picker. Each label shows "Slot N: <name>" or "Slot N: empty".
|
|
|
|
|
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 {
|
|
|
|
|
// Resolve current occupant name by walking contacts.
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
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) {
|
|
|
|
|
auto res = _ctx_menu.handleInput(c);
|
|
|
|
|
if (res == PopupMenu::SELECTED && _num_channels > 0) {
|
|
|
|
|
uint8_t ch_idx = _channel_indices[_channel_sel];
|
|
|
|
|
if (_ctx_menu.selectedIndex() == 0) {
|
|
|
|
|
_ch_unread[ch_idx] = 0;
|
|
|
|
|
} else if (_ctx_menu.selectedIndex() == 1) {
|
|
|
|
|
uint8_t nstate = chNotifState(ch_idx);
|
|
|
|
|
setChNotifState(ch_idx, (nstate + 1) % 3);
|
|
|
|
|
_ctx_dirty = true;
|
2026-05-27 17:33:52 +02:00
|
|
|
} else if (_ctx_menu.selectedIndex() == 2) {
|
2026-05-17 00:12:40 +02:00
|
|
|
uint8_t slot = chNotifMelody(ch_idx);
|
|
|
|
|
setChNotifMelody(ch_idx, (slot + 1) % 3);
|
|
|
|
|
_ctx_dirty = true;
|
2026-05-27 17:33:52 +02:00
|
|
|
} else {
|
|
|
|
|
// Toggle favourite
|
|
|
|
|
NodePrefs* p2 = _task->getNodePrefs();
|
|
|
|
|
if (p2) {
|
|
|
|
|
p2->ch_fav_bitmask ^= (1ULL << ch_idx);
|
|
|
|
|
_ctx_dirty = true;
|
|
|
|
|
buildChannelList(); // refilter if ch_fav_only is active
|
|
|
|
|
if (_channel_sel >= _num_channels) _channel_sel = _num_channels > 0 ? _num_channels - 1 : 0;
|
|
|
|
|
}
|
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();
|
|
|
|
|
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) {
|
|
|
|
|
_dm_fs.active = false;
|
|
|
|
|
startReply(false);
|
|
|
|
|
} 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);
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
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) {
|
|
|
|
|
_fs.active = false;
|
|
|
|
|
startReply(true);
|
|
|
|
|
} 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);
|
|
|
|
|
if (ring_pos >= 0 && buildChannelReplyPrefix(_hist[ring_pos].text)) {
|
|
|
|
|
_ctx_menu.begin("Options", 1);
|
|
|
|
|
_ctx_menu.addItem("Reply");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (_ctx_menu.active) {
|
|
|
|
|
auto res = _ctx_menu.handleInput(c);
|
|
|
|
|
if (res == PopupMenu::SELECTED) {
|
|
|
|
|
startReply(true);
|
|
|
|
|
} else if (res != PopupMenu::NONE) {
|
|
|
|
|
_ctx_menu.active = false;
|
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) {
|
|
|
|
|
_phase = MSG_PICK;
|
|
|
|
|
} 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;
|
|
|
|
|
}
|
|
|
|
|
};
|