2026-05-17 00:12:40 +02:00
|
|
|
#pragma once
|
|
|
|
|
// Custom screen — not part of upstream UITask.cpp
|
|
|
|
|
// Included by UITask.cpp after SettingsScreen.h is defined.
|
|
|
|
|
|
feat(nav): navigate to / save a location shared in a message
Locations already travel as plain text ({loc} expands to "lat,lon"), so any
message carrying coordinates can now be acted on. In the fullscreen message
view, the context-menu (Hold Enter) gains Navigate and Save waypoint
entries whenever the message contains a coordinate — alongside Reply.
- geo::parseLatLon: shared scanner for an embedded "lat,lon" (the {loc}
format), with an optional [WAY] tag that supplies a label. Range-checked,
requires a decimal point to avoid matching plain integer pairs. Will also
back the waypoint-share feature later.
- QuickMsgScreen: Navigate opens NavView inline (To/Hdg/distance, honouring
the global Units setting) over the message, back returns to it; Save
waypoint stores the point (label from [WAY], else auto-named). Works in
both DM and channel histories, and on incoming or outgoing messages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:28:17 +02:00
|
|
|
#include "NavView.h" // navigate to a location shared inside a message
|
2026-06-14 23:33:16 +02:00
|
|
|
#include "icons.h" // scalable mini-icons (delivery markers)
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
#include "ChannelsView.h" // on-device channel add/edit form (Channels tab)
|
feat(nav): navigate to / save a location shared in a message
Locations already travel as plain text ({loc} expands to "lat,lon"), so any
message carrying coordinates can now be acted on. In the fullscreen message
view, the context-menu (Hold Enter) gains Navigate and Save waypoint
entries whenever the message contains a coordinate — alongside Reply.
- geo::parseLatLon: shared scanner for an embedded "lat,lon" (the {loc}
format), with an optional [WAY] tag that supplies a label. Range-checked,
requires a decimal point to avoid matching plain integer pairs. Will also
back the waypoint-share feature later.
- QuickMsgScreen: Navigate opens NavView inline (To/Hdg/distance, honouring
the global Units setting) over the message, back returns to it; Save
waypoint stores the point (label from [WAY], else auto-named). Works in
both DM and channel histories, and on incoming or outgoing messages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:28:17 +02:00
|
|
|
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
class MessagesScreen : public UIScreen {
|
2026-05-17 00:12:40 +02:00
|
|
|
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
|
2026-06-26 10:55:24 +02:00
|
|
|
bool _login_mode; // true while KEYBOARD is collecting a room-login password
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
// CHANNEL_PICK
|
|
|
|
|
int _channel_sel, _channel_scroll;
|
|
|
|
|
int _num_channels;
|
|
|
|
|
uint8_t _channel_indices[MAX_GROUP_CHANNELS];
|
|
|
|
|
int _sel_channel_idx;
|
|
|
|
|
bool _sending_to_channel;
|
|
|
|
|
|
2026-06-14 23:33:16 +02:00
|
|
|
// Carries the just-sent DM's ACK tag + deadline + send timestamp from
|
|
|
|
|
// sendText() to afterSend().
|
|
|
|
|
uint32_t _last_ack_tag = 0;
|
|
|
|
|
uint32_t _last_ack_deadline_ms = 0;
|
|
|
|
|
uint32_t _last_send_ts = 0;
|
|
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
// MSG_PICK (shared)
|
|
|
|
|
int _msg_sel, _msg_scroll;
|
|
|
|
|
int _active_msgs[QUICK_MSGS_MAX];
|
|
|
|
|
int _active_msg_count;
|
|
|
|
|
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
// CHANNEL_HIST — selection + the unread "viewing session" bookkeeping. The
|
|
|
|
|
// history ring itself and the per-channel unread counters live in _history.
|
2026-05-17 00:12:40 +02:00
|
|
|
int _hist_sel, _hist_scroll;
|
|
|
|
|
FullscreenMsgView _fs;
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
int _unread_at_entry; // channel unread count when entering CHANNEL_HIST
|
2026-05-17 00:12:40 +02:00
|
|
|
int _viewing_max_seen; // highest _hist_sel reached in current session
|
|
|
|
|
|
|
|
|
|
// KEYBOARD
|
2026-06-08 20:42:18 +02:00
|
|
|
KeyboardWidget* _kb;
|
2026-05-17 00:12:40 +02:00
|
|
|
|
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;
|
2026-07-05 16:08:44 +02:00
|
|
|
// Channel the open channel-context menu acts on, frozen at open time. The
|
|
|
|
|
// Fav toggle can remove the highlighted channel from a fav-only list, so
|
|
|
|
|
// re-reading _channel_indices[_channel_sel] mid-interaction could silently
|
|
|
|
|
// retarget the menu at a different channel.
|
|
|
|
|
uint8_t _ctx_ch_idx = 0;
|
2026-05-17 00:12:40 +02:00
|
|
|
char _ctx_notif_item[22];
|
|
|
|
|
char _ctx_melody_item[20];
|
2026-05-24 23:30:16 +02:00
|
|
|
char _ctx_pin_item[28]; // "Pin to dial" or "Unpin (slot N)"
|
2026-05-27 17:33:52 +02:00
|
|
|
char _ctx_ch_fav_item[12]; // "Fav" or "Unfav"
|
2026-05-24 23:30:16 +02:00
|
|
|
char _pin_slot_labels[NodePrefs::FAVOURITES_COUNT][22]; // per-slot picker labels
|
|
|
|
|
bool _pin_picker_active; // true while the slot-picker submenu is open
|
2026-05-25 07:58:45 +02:00
|
|
|
bool _dm_direct_entry; // entered DM_HIST via Favourites shortcut; CANCEL returns home
|
2026-05-18 18:18:45 +02:00
|
|
|
char _reply_prefix[36]; // "@[nick] " built when reply is triggered
|
2026-05-18 15:12:11 +02:00
|
|
|
bool _reply_mode; // true while composing a reply (prefix is prepended)
|
2026-05-17 00:12:40 +02:00
|
|
|
|
feat(nav): navigate to / save a location shared in a message
Locations already travel as plain text ({loc} expands to "lat,lon"), so any
message carrying coordinates can now be acted on. In the fullscreen message
view, the context-menu (Hold Enter) gains Navigate and Save waypoint
entries whenever the message contains a coordinate — alongside Reply.
- geo::parseLatLon: shared scanner for an embedded "lat,lon" (the {loc}
format), with an optional [WAY] tag that supplies a label. Range-checked,
requires a decimal point to avoid matching plain integer pairs. Will also
back the waypoint-share feature later.
- QuickMsgScreen: Navigate opens NavView inline (To/Hdg/distance, honouring
the global Units setting) over the message, back returns to it; Save
waypoint stores the point (label from [WAY], else auto-named). Works in
both DM and channel histories, and on incoming or outgoing messages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:28:17 +02:00
|
|
|
// Fullscreen-message context menu actions. Built per-message: Reply (when
|
|
|
|
|
// applicable) plus Navigate / Save waypoint when the message carries a
|
|
|
|
|
// location (a {loc} string or a [WAY] share). _fs_act maps each visible row
|
|
|
|
|
// back to an action so the index math survives the conditional layout.
|
|
|
|
|
enum FsAct : uint8_t { FS_REPLY, FS_NAV, FS_SAVE };
|
|
|
|
|
uint8_t _fs_act[3];
|
|
|
|
|
int _fs_act_n = 0;
|
|
|
|
|
// Inline navigate-to-location view layered over the fullscreen message.
|
|
|
|
|
bool _nav_active = false;
|
|
|
|
|
int32_t _nav_lat = 0, _nav_lon = 0;
|
|
|
|
|
char _nav_label[24] = "";
|
|
|
|
|
|
2026-06-04 11:38:12 +02:00
|
|
|
// Share-compose mode: launched from elsewhere (e.g. a waypoint) with a
|
|
|
|
|
// prepared message; the user picks a recipient and the text lands prefilled
|
|
|
|
|
// in the keyboard to confirm/edit before sending.
|
|
|
|
|
bool _share_mode = false;
|
|
|
|
|
char _share_text[160] = "";
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
// Live-share target picker: reuse the recipient chooser to set the persistent
|
|
|
|
|
// auto-share target (channel / DM) instead of composing a message.
|
|
|
|
|
bool _pick_target = false;
|
feat(bot): room-server target, tab-carousel UI, per-target independence
Auto-Reply Bot gains a third target (room servers, alongside DM and
channel), new {name}/{hops} reply placeholders, and a DM all/favourites
allow-list. BotScreen is redesigned as a circular tab carousel (Direct /
Channel / Room / Other, same interaction as Nearby Nodes' filter tabs)
instead of one long scrolling list, which also exposed and fixed a
leftover coupling where Channel/Room trigger-replies and their !command
handling secretly depended on the DM tab's Enable/Commands toggles —
each target's Enable and Commands are now fully independent.
- NodePrefs: bot_room_enabled/prefix/trigger/reply, bot_dm_scope,
bot_commands_ch/bot_commands_room (bot_commands_enabled repurposed as
DM-only). SCHEMA_SENTINEL 0xC0DE001D -> 0xC0DE001F; sizeof unchanged
at 2712 (new bytes absorbed existing padding, verified via a
standalone host compile + offsetof check).
- DataStore: persists all new fields; seeds bot_commands_ch/room from
the old shared bot_commands_enabled on upgrade so existing
channel/room command behaviour isn't silently lost.
- MyMeshBot: tryBotReplyRoom/tryBotRoomCommand mirror the channel bot's
shape but post via sendMessage (room relays to members itself);
requires an existing login session with that room, same as a manual
post would. botDmSenderAllowed() gates DM trigger-reply/commands on
the favourites bit when bot_dm_scope=Fav.
- MsgExpand: {name}/{hops} as optional trailing params (default
nullptr/-1, no existing caller affected) — deliberately not exposed
on the general compose keyboard, only on bot Reply fields.
- QuickMsgScreen/UITask: room-target picker (mirrors the channel
picker), routing through the existing room-login prompt when there's
no saved password yet.
- BotScreen: tab carousel (LEFT/RIGHT switches tabs, UP/DOWN moves
rows, Enter is now the only way to change a value); Enable split out
of Channel/Room's combo row; Quiet Hours gained a stepper sub-mode;
Commands moved from a shared toggle into each tab.
- docs/tools_screen.md updated for the new tab layout and behaviour.
Not build-verified — no PlatformIO toolchain in this environment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 20:16:59 +02:00
|
|
|
// Bot channel picker: same idea, but jumps straight to CHANNEL_PICK (the
|
|
|
|
|
// bot only ever targets a channel, never a DM) to set bot_channel_idx.
|
|
|
|
|
bool _pick_bot_channel = false;
|
|
|
|
|
// Bot room picker: jumps straight to CONTACT_PICK with room_mode on (the
|
|
|
|
|
// bot's room target is always a room server) to set bot_room_prefix.
|
|
|
|
|
bool _pick_bot_room = false;
|
2026-06-04 11:38:12 +02:00
|
|
|
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
// The message-history rings (channel + DM), their per-entry delivery state and
|
|
|
|
|
// per-channel unread counters live in this store (see MessageHistory.h). The
|
|
|
|
|
// phase machine below keeps only the view state — selection, scroll, the
|
|
|
|
|
// fullscreen readers — and reaches entries through _history's accessors. The
|
|
|
|
|
// shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are file-
|
|
|
|
|
// scope, so they're still referred to unqualified throughout this screen.
|
|
|
|
|
MessageHistory _history;
|
|
|
|
|
|
|
|
|
|
// DM_HIST view state (the ring itself is in _history).
|
2026-05-17 00:12:40 +02:00
|
|
|
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 _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-06-16 09:32:09 +02:00
|
|
|
// Scrollbar metrics for a history list. The track is pinned to the full list
|
|
|
|
|
// area (top_y..cby) so it never resizes with content; only the thumb sizes and
|
|
|
|
|
// moves. `need` also drives the gutter reserve so wide message boxes don't
|
|
|
|
|
// reflow as messages arrive.
|
|
|
|
|
struct HistScroll { bool need; int reserve; long total_px, scroll_px; int view_px; };
|
|
|
|
|
|
|
|
|
|
// `getBody(idx)` returns the body text for list item idx (the part that wraps),
|
|
|
|
|
// or nullptr to fall back to a fixed 2-line box.
|
|
|
|
|
template <class GetBody>
|
|
|
|
|
HistScroll computeHistScroll(DisplayDriver& display, bool portrait, int count, int scroll,
|
|
|
|
|
int hist_start_y, int cby, int lh, GetBody getBody) {
|
|
|
|
|
HistScroll r{};
|
|
|
|
|
const int fixed_bh = 2 * lh + 1;
|
|
|
|
|
const int top_y = hist_start_y + 1;
|
|
|
|
|
r.view_px = cby - top_y; // fixed track height = full list area
|
|
|
|
|
if (r.view_px < 1) r.view_px = 1;
|
|
|
|
|
const int col = scrollIndicatorColWidth(display);
|
|
|
|
|
|
|
|
|
|
if (!portrait) { // uniform 2-line boxes → exact pixel math
|
|
|
|
|
const int box = fixed_bh + 1;
|
|
|
|
|
r.total_px = (long)count * box;
|
|
|
|
|
r.scroll_px = (long)scroll * box;
|
|
|
|
|
r.need = r.total_px > r.view_px;
|
|
|
|
|
r.reserve = r.need ? col : 0;
|
|
|
|
|
if (!r.need) r.scroll_px = 0;
|
|
|
|
|
return r;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const int sp = 2; // portrait inter-box spacing
|
|
|
|
|
auto boxH = [&](int idx, int rsv) -> int {
|
|
|
|
|
const char* body = getBody(idx);
|
|
|
|
|
if (!body) return fixed_bh;
|
|
|
|
|
display.translateUTF8ToBlocks(s_wrap_trans, skipReplyPrefix(body), sizeof(s_wrap_trans));
|
|
|
|
|
int nl = FullscreenMsgView::wrapLines(display, s_wrap_trans, display.width() - 6 - rsv, s_wrap_lines, 8);
|
|
|
|
|
return (1 + (nl > 0 ? nl : 1)) * lh + 1;
|
|
|
|
|
};
|
|
|
|
|
// Scrollbar-needed test at the WIDEST layout (reserve 0 → fewest wrap lines →
|
|
|
|
|
// shortest total). If even this overflows the list area the gutter is truly
|
|
|
|
|
// needed, so it stays put and can't flicker in/out as messages arrive.
|
|
|
|
|
int cur = hist_start_y, fit = 0;
|
|
|
|
|
for (int i = 0; i < count; i++) { int bh = boxH(i, 0); if (cur + bh > cby) break; fit++; cur += bh + sp; }
|
|
|
|
|
r.need = fit < count;
|
|
|
|
|
r.reserve = r.need ? col : 0;
|
|
|
|
|
if (r.need) { // sum real box heights at the final width
|
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
|
|
|
long ext = boxH(i, r.reserve) + sp;
|
|
|
|
|
r.total_px += ext;
|
|
|
|
|
if (i < scroll) r.scroll_px += ext;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return r;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-29 17:43:17 +02:00
|
|
|
// Strip the "@[nick] " reply prefix for compact list display (body only).
|
|
|
|
|
// Shares the one parser with the fullscreen view — see msgReplyBody().
|
|
|
|
|
static const char* skipReplyPrefix(const char* text) { return msgReplyBody(text); }
|
2026-05-21 16:37:25 +02:00
|
|
|
|
2026-06-22 18:31:47 +02:00
|
|
|
// Split a DM-history entry into the name to show as the author and the body to
|
|
|
|
|
// show beneath it. Room servers carry many guests, so incoming room posts are
|
|
|
|
|
// stored "Sender: text" (MyMesh::queueMessage); split that off so each line is
|
|
|
|
|
// attributed to its guest. Outgoing → "Me"; plain DMs (or no separator) keep
|
|
|
|
|
// the contact name and the text unchanged.
|
|
|
|
|
const char* dmDisplayParts(const DmHistEntry& e, bool is_room, const char* contact_name,
|
|
|
|
|
char* sender_buf, int sender_cap) const {
|
|
|
|
|
const char* body = skipReplyPrefix(e.text);
|
|
|
|
|
if (e.outgoing) {
|
|
|
|
|
strncpy(sender_buf, "Me", sender_cap - 1);
|
|
|
|
|
} else if (is_room) {
|
|
|
|
|
const char* sep = strstr(body, ": ");
|
|
|
|
|
if (sep) {
|
|
|
|
|
int nl = (int)(sep - body);
|
|
|
|
|
if (nl > sender_cap - 1) nl = sender_cap - 1;
|
|
|
|
|
strncpy(sender_buf, body, nl);
|
|
|
|
|
sender_buf[nl] = '\0';
|
|
|
|
|
return sep + 2; // body after "Sender: "
|
|
|
|
|
}
|
|
|
|
|
strncpy(sender_buf, contact_name, sender_cap - 1);
|
|
|
|
|
} else {
|
|
|
|
|
strncpy(sender_buf, contact_name, sender_cap - 1);
|
|
|
|
|
}
|
|
|
|
|
sender_buf[sender_cap - 1] = '\0';
|
|
|
|
|
return body;
|
|
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-28 12:27:16 +02:00
|
|
|
// Build "@[nick] " into _reply_prefix for a reply to a DM/room post, raw
|
|
|
|
|
// (UTF-8) so it goes out over the air intact — like buildChannelReplyPrefix,
|
|
|
|
|
// and unlike the old per-site code which ran the name through the lossy
|
|
|
|
|
// display transliterator. Addresses the same author the history shows
|
|
|
|
|
// (dmDisplayParts): in a room every post is stored "Author: text", so the
|
|
|
|
|
// addressee is that author, not the room server's own name (_sel_contact.name);
|
|
|
|
|
// a plain 1:1 DM uses the contact name. Caller ensures the post is incoming.
|
|
|
|
|
void buildDmReplyPrefix(const DmHistEntry& e) {
|
|
|
|
|
char nick[32];
|
|
|
|
|
dmDisplayParts(e, _sel_contact.type == ADV_TYPE_ROOM, _sel_contact.name, nick, sizeof(nick));
|
|
|
|
|
snprintf(_reply_prefix, sizeof(_reply_prefix), "@[%.31s] ", nick);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 15:12:11 +02:00
|
|
|
void startReply(bool to_channel) {
|
|
|
|
|
_sending_to_channel = to_channel;
|
|
|
|
|
_reply_mode = true;
|
|
|
|
|
setupMsgPick();
|
|
|
|
|
_phase = MSG_PICK;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 11:38:12 +02:00
|
|
|
// Recipient chosen while sharing — open the keyboard with the prepared text.
|
|
|
|
|
void beginShareCompose(bool channel) {
|
|
|
|
|
_sending_to_channel = channel;
|
|
|
|
|
_reply_mode = false;
|
2026-06-08 20:42:18 +02:00
|
|
|
_kb->begin(_share_text);
|
2026-06-04 11:38:12 +02:00
|
|
|
_phase = KEYBOARD;
|
|
|
|
|
}
|
|
|
|
|
|
feat(nav): navigate to / save a location shared in a message
Locations already travel as plain text ({loc} expands to "lat,lon"), so any
message carrying coordinates can now be acted on. In the fullscreen message
view, the context-menu (Hold Enter) gains Navigate and Save waypoint
entries whenever the message contains a coordinate — alongside Reply.
- geo::parseLatLon: shared scanner for an embedded "lat,lon" (the {loc}
format), with an optional [WAY] tag that supplies a label. Range-checked,
requires a decimal point to avoid matching plain integer pairs. Will also
back the waypoint-share feature later.
- QuickMsgScreen: Navigate opens NavView inline (To/Hdg/distance, honouring
the global Units setting) over the message, back returns to it; Save
waypoint stores the point (label from [WAY], else auto-named). Works in
both DM and channel histories, and on incoming or outgoing messages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:28:17 +02:00
|
|
|
// Build the fullscreen-message options popup: Reply (if allowed) plus
|
|
|
|
|
// Navigate / Save waypoint when `body` carries a location. Opens _ctx_menu
|
|
|
|
|
// only when there's at least one action. Parses the location once here and
|
|
|
|
|
// stashes it for the action handler.
|
|
|
|
|
void buildFsMenu(const char* body, bool reply_allowed) {
|
|
|
|
|
bool has_loc = geo::parseLatLon(body, _nav_lat, _nav_lon, _nav_label, sizeof(_nav_label));
|
|
|
|
|
int n = (reply_allowed ? 1 : 0) + (has_loc ? 2 : 0);
|
|
|
|
|
if (n == 0) return;
|
|
|
|
|
_fs_act_n = 0;
|
2026-07-10 16:01:14 +02:00
|
|
|
_ctx_menu.begin("Options", n);
|
feat(nav): navigate to / save a location shared in a message
Locations already travel as plain text ({loc} expands to "lat,lon"), so any
message carrying coordinates can now be acted on. In the fullscreen message
view, the context-menu (Hold Enter) gains Navigate and Save waypoint
entries whenever the message contains a coordinate — alongside Reply.
- geo::parseLatLon: shared scanner for an embedded "lat,lon" (the {loc}
format), with an optional [WAY] tag that supplies a label. Range-checked,
requires a decimal point to avoid matching plain integer pairs. Will also
back the waypoint-share feature later.
- QuickMsgScreen: Navigate opens NavView inline (To/Hdg/distance, honouring
the global Units setting) over the message, back returns to it; Save
waypoint stores the point (label from [WAY], else auto-named). Works in
both DM and channel histories, and on incoming or outgoing messages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:28:17 +02:00
|
|
|
if (reply_allowed) { _ctx_menu.addItem("Reply"); _fs_act[_fs_act_n++] = FS_REPLY; }
|
|
|
|
|
if (has_loc) { _ctx_menu.addItem("Navigate"); _fs_act[_fs_act_n++] = FS_NAV;
|
|
|
|
|
_ctx_menu.addItem("Save waypoint"); _fs_act[_fs_act_n++] = FS_SAVE; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Dispatch the selected fullscreen-options row. `channel` picks which
|
|
|
|
|
// fullscreen view to close when starting a reply.
|
|
|
|
|
void dispatchFsAction(bool channel) {
|
2026-06-14 13:37:03 +02:00
|
|
|
int csel = _ctx_menu.selectedIndex();
|
|
|
|
|
FsAct a = (FsAct)_fs_act[(csel >= 0 && csel < _fs_act_n) ? csel : 0];
|
feat(nav): navigate to / save a location shared in a message
Locations already travel as plain text ({loc} expands to "lat,lon"), so any
message carrying coordinates can now be acted on. In the fullscreen message
view, the context-menu (Hold Enter) gains Navigate and Save waypoint
entries whenever the message contains a coordinate — alongside Reply.
- geo::parseLatLon: shared scanner for an embedded "lat,lon" (the {loc}
format), with an optional [WAY] tag that supplies a label. Range-checked,
requires a decimal point to avoid matching plain integer pairs. Will also
back the waypoint-share feature later.
- QuickMsgScreen: Navigate opens NavView inline (To/Hdg/distance, honouring
the global Units setting) over the message, back returns to it; Save
waypoint stores the point (label from [WAY], else auto-named). Works in
both DM and channel histories, and on incoming or outgoing messages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:28:17 +02:00
|
|
|
_ctx_menu.active = false;
|
|
|
|
|
if (a == FS_REPLY) {
|
|
|
|
|
(channel ? _fs : _dm_fs).active = false;
|
|
|
|
|
startReply(channel);
|
|
|
|
|
} else if (a == FS_NAV) {
|
|
|
|
|
_nav_active = true; // keep the message view active underneath
|
|
|
|
|
} else {
|
|
|
|
|
saveSharedWaypoint();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Save the location parsed from the open message as a waypoint. Uses the
|
|
|
|
|
// [WAY] label when present, else auto-names it like a manually-marked point.
|
|
|
|
|
void saveSharedWaypoint() {
|
|
|
|
|
char label[WAYPOINT_LABEL_LEN];
|
|
|
|
|
if (_nav_label[0]) {
|
|
|
|
|
strncpy(label, _nav_label, sizeof(label) - 1);
|
|
|
|
|
label[sizeof(label) - 1] = '\0';
|
|
|
|
|
} else {
|
|
|
|
|
snprintf(label, sizeof(label), "WP%d", _task->waypoints().count() + 1);
|
|
|
|
|
}
|
2026-06-09 15:09:57 +02:00
|
|
|
_task->addWaypoint(_nav_lat, _nav_lon, label);
|
feat(nav): navigate to / save a location shared in a message
Locations already travel as plain text ({loc} expands to "lat,lon"), so any
message carrying coordinates can now be acted on. In the fullscreen message
view, the context-menu (Hold Enter) gains Navigate and Save waypoint
entries whenever the message contains a coordinate — alongside Reply.
- geo::parseLatLon: shared scanner for an embedded "lat,lon" (the {loc}
format), with an optional [WAY] tag that supplies a label. Range-checked,
requires a decimal point to avoid matching plain integer pairs. Will also
back the waypoint-share feature later.
- QuickMsgScreen: Navigate opens NavView inline (To/Hdg/distance, honouring
the global Units setting) over the message, back returns to it; Save
waypoint stores the point (label from [WAY], else auto-named). Works in
both DM and channel histories, and on incoming or outgoing messages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:28:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void renderNav(DisplayDriver& display) {
|
|
|
|
|
int32_t mylat, mylon; bool have = _task->currentLocation(mylat, mylon);
|
|
|
|
|
int cog; bool cogv = _task->currentCourse(cog);
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
navview::draw(display, have, mylat, mylon, _nav_lat, _nav_lon,
|
|
|
|
|
_nav_label[0] ? _nav_label : "Msg loc", cogv, cog, p && p->units_imperial);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
void setupMsgPick() {
|
|
|
|
|
_msg_sel = _msg_scroll = 0;
|
|
|
|
|
_active_msg_count = 0;
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
if (p) {
|
|
|
|
|
for (int i = 0; i < QUICK_MSGS_MAX; i++) {
|
|
|
|
|
if (p->custom_msgs[i][0] != '\0')
|
|
|
|
|
_active_msgs[_active_msg_count++] = i;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-14 23:33:16 +02:00
|
|
|
// Delivery marker, drawn with the current ink colour and auto-scaled to the
|
|
|
|
|
// font (see icons.h). Pending = a row of dots, one per send (so it grows with
|
|
|
|
|
// each auto-resend); delivered = ✓; failed = ✗; ACK_NONE = nothing.
|
|
|
|
|
static void drawAckGlyph(DisplayDriver& d, int x, int top_y, AckState s, int sends = 1) {
|
|
|
|
|
switch (s) {
|
|
|
|
|
case ACK_PENDING: miniIconDotRow(d, x, top_y, sends); break;
|
2026-06-15 09:42:29 +02:00
|
|
|
case ACK_OK: miniIconDraw(d, x, top_y, ICON_CHECK); break;
|
|
|
|
|
case ACK_FAIL: miniIconDraw(d, x, top_y, ICON_CROSS); break;
|
2026-06-14 23:33:16 +02:00
|
|
|
default: break; // ACK_NONE → nothing
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-29 23:05:11 +02:00
|
|
|
// Selection frame for one history message box, shared by the DM/room and
|
|
|
|
|
// channel history lists. Selected = solid fill; unselected = outline with a
|
|
|
|
|
// filled header strip. Leaves the ink DARK (for the sender row drawn next).
|
|
|
|
|
static void drawHistRowFrame(DisplayDriver& d, int y, int bh, int reserve, int lh, bool sel) {
|
|
|
|
|
d.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
if (sel) {
|
|
|
|
|
d.fillRect(0, y, d.width() - reserve, bh);
|
|
|
|
|
} else {
|
|
|
|
|
d.drawRect(0, y, d.width() - reserve, bh);
|
|
|
|
|
d.fillRect(1, y + 1, d.width() - 2 - reserve, lh);
|
|
|
|
|
}
|
|
|
|
|
d.setColor(DisplayDriver::DARK);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Bottom-left "[+ send]" compose button, shared by both history lists:
|
|
|
|
|
// bordered when idle, inverted (filled) when selected. Always reset to LIGHT
|
|
|
|
|
// first so it stays visible regardless of the ink the message loop left.
|
|
|
|
|
static void drawComposeButton(DisplayDriver& d, int cby, int lh, bool sel) {
|
|
|
|
|
const char* ctxt = "[+ send]";
|
|
|
|
|
int ctw = d.getTextWidth(ctxt);
|
|
|
|
|
d.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
if (sel) { d.fillRect(0, cby - 1, ctw + 4, lh + 2); d.setColor(DisplayDriver::DARK); }
|
|
|
|
|
else d.drawRect(0, cby - 1, ctw + 4, lh + 2);
|
|
|
|
|
d.setCursor(2, cby);
|
|
|
|
|
d.print(ctxt);
|
|
|
|
|
d.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
void afterSend(bool ok, const char* msg) {
|
2026-05-18 15:12:11 +02:00
|
|
|
_reply_mode = false;
|
2026-06-04 11:38:12 +02:00
|
|
|
_share_mode = false;
|
2026-05-17 00:12:40 +02:00
|
|
|
if (ok && _sending_to_channel) {
|
|
|
|
|
_hist_sel = 0;
|
|
|
|
|
_hist_scroll = 0;
|
|
|
|
|
_phase = CHANNEL_HIST; // set before addChannelMsg so viewing=true, no unread bump
|
|
|
|
|
char entry[sizeof(ChHistEntry::text)];
|
|
|
|
|
snprintf(entry, sizeof(entry), "Me: %s", msg);
|
2026-06-14 23:33:16 +02:00
|
|
|
int pos = addChannelMsg(_sel_channel_idx, entry);
|
|
|
|
|
// Arm the "relayed into mesh" marker on this exact entry — MyMesh tracked
|
|
|
|
|
// the flood it just originated and reports a heard repeater echo by seq.
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
if (pos >= 0) _history.armChannelRelay(pos, the_mesh.lastChannelRelaySeq());
|
2026-05-17 00:12:40 +02:00
|
|
|
// After inserting sent msg at index 0, the unread index range is stale.
|
|
|
|
|
// User is active in this channel — treat as fully read.
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
_history.setChUnread(_sel_channel_idx, 0);
|
2026-05-17 00:12:40 +02:00
|
|
|
_unread_at_entry = 0;
|
|
|
|
|
_viewing_max_seen = 0;
|
|
|
|
|
_task->showAlert("Sent!", 600);
|
|
|
|
|
} else if (ok) {
|
2026-06-14 23:33:16 +02:00
|
|
|
NodePrefs* np = _task->getNodePrefs();
|
|
|
|
|
uint8_t resends = np ? np->dm_resend_count : 0;
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
_history.storeDMMsg(_sel_contact.id.pub_key, true, msg, _last_ack_tag,
|
|
|
|
|
_last_ack_deadline_ms, _last_send_ts, resends);
|
2026-05-17 00:12:40 +02:00
|
|
|
_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) {
|
2026-06-14 23:33:16 +02:00
|
|
|
_last_ack_tag = 0;
|
|
|
|
|
_last_ack_deadline_ms = 0;
|
|
|
|
|
_last_send_ts = 0;
|
2026-05-17 00:12:40 +02:00
|
|
|
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 {
|
2026-06-14 23:33:16 +02:00
|
|
|
uint32_t send_ts = rtc_clock.getCurrentTime();
|
2026-05-17 00:12:40 +02:00
|
|
|
uint32_t expected_ack = 0, est_timeout = 0;
|
2026-06-14 23:33:16 +02:00
|
|
|
bool ok = the_mesh.sendMessage(_sel_contact, send_ts, 0,
|
|
|
|
|
msg, expected_ack, est_timeout) > 0;
|
|
|
|
|
if (ok && expected_ack) {
|
|
|
|
|
_last_send_ts = send_ts;
|
|
|
|
|
_last_ack_tag = expected_ack;
|
|
|
|
|
// Generous margin over the base estimate so a slow multi-hop ACK isn't
|
|
|
|
|
// prematurely shown as failed.
|
|
|
|
|
_last_ack_deadline_ms = millis() + est_timeout + 4000;
|
|
|
|
|
}
|
|
|
|
|
return ok;
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
counts[_num_contacts] = _history.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
|
2026-06-29 22:48:10 +02:00
|
|
|
// Per-contact/channel notification + melody overrides share two storage
|
|
|
|
|
// shapes, so the eight accessors below are thin wrappers over two primitives:
|
|
|
|
|
//
|
|
|
|
|
// • Channels — a 3-state packed into a pair of channel-index bitmasks: a
|
|
|
|
|
// "presence" mask (is there an override at all) + a "variant" mask. The two
|
|
|
|
|
// uses disagree on which state the variant bit means, so the caller passes
|
|
|
|
|
// the state value that corresponds to variant-set (v_set).
|
|
|
|
|
// • DMs — a small {prefix[4], value} table: find by 4-byte prefix, update or
|
|
|
|
|
// clear (clear frees the slot), else insert into the first free slot, else
|
|
|
|
|
// overwrite slot 0. value 0 == "no override" == empty slot.
|
|
|
|
|
|
|
|
|
|
static uint8_t maskPairGet(uint64_t presence, uint64_t variant, uint8_t idx,
|
|
|
|
|
uint8_t v_set, uint8_t v_clr) {
|
|
|
|
|
uint64_t m = 1ULL << idx;
|
|
|
|
|
if (!(presence & m)) return 0;
|
|
|
|
|
return (variant & m) ? v_set : v_clr;
|
|
|
|
|
}
|
|
|
|
|
static void maskPairSet(uint64_t& presence, uint64_t& variant, uint8_t idx,
|
|
|
|
|
uint8_t state, uint8_t v_set) {
|
|
|
|
|
uint64_t m = 1ULL << idx;
|
|
|
|
|
if (state == 0) { presence &= ~m; variant &= ~m; return; }
|
|
|
|
|
presence |= m;
|
|
|
|
|
if (state == v_set) variant |= m; else variant &= ~m;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template <class Entry>
|
|
|
|
|
static uint8_t prefTableGet(const Entry* tbl, int n, const uint8_t* pub_key,
|
|
|
|
|
uint8_t Entry::* val) {
|
|
|
|
|
for (int i = 0; i < n; i++)
|
|
|
|
|
if (tbl[i].*val && memcmp(tbl[i].prefix, pub_key, 4) == 0) return tbl[i].*val;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
template <class Entry>
|
|
|
|
|
static void prefTableSet(Entry* tbl, int n, const uint8_t* pub_key,
|
|
|
|
|
uint8_t Entry::* val, uint8_t v) {
|
|
|
|
|
for (int i = 0; i < n; i++)
|
|
|
|
|
if (tbl[i].*val && memcmp(tbl[i].prefix, pub_key, 4) == 0) {
|
|
|
|
|
if (v == 0) memset(&tbl[i], 0, sizeof(tbl[i])); else tbl[i].*val = v;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (v == 0) return;
|
|
|
|
|
for (int i = 0; i < n; i++)
|
|
|
|
|
if (tbl[i].*val == 0) { memcpy(tbl[i].prefix, pub_key, 4); tbl[i].*val = v; return; }
|
|
|
|
|
memcpy(tbl[0].prefix, pub_key, 4); tbl[0].*val = v; // table full — overwrite slot 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Channel notif: state 1 = muted (variant bit set), 2 = force-on (variant clear).
|
2026-05-17 00:12:40 +02:00
|
|
|
uint8_t chNotifState(uint8_t ch_idx) const {
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
2026-06-29 22:48:10 +02:00
|
|
|
return p ? maskPairGet(p->ch_notif_override, p->ch_notif_muted, ch_idx, 1, 2) : 0;
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
void setChNotifState(uint8_t ch_idx, uint8_t state) {
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
2026-06-29 22:48:10 +02:00
|
|
|
if (p) maskPairSet(p->ch_notif_override, p->ch_notif_muted, ch_idx, state, 1);
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint8_t dmNotifState(const uint8_t* pub_key) const {
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
2026-06-29 22:48:10 +02:00
|
|
|
return p ? prefTableGet(p->dm_notif, NodePrefs::DM_NOTIF_TABLE_MAX, pub_key,
|
|
|
|
|
&NodePrefs::DmNotifEntry::state) : 0;
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
void setDmNotifState(const uint8_t* pub_key, uint8_t state) {
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
2026-06-29 22:48:10 +02:00
|
|
|
if (p) prefTableSet(p->dm_notif, NodePrefs::DM_NOTIF_TABLE_MAX, pub_key,
|
|
|
|
|
&NodePrefs::DmNotifEntry::state, state);
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-29 22:48:10 +02:00
|
|
|
// Channel melody: slot 1 = melody 1 (variant bit clear), 2 = melody 2 (set).
|
2026-05-17 00:12:40 +02:00
|
|
|
uint8_t chNotifMelody(uint8_t ch_idx) const {
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
2026-06-29 22:48:10 +02:00
|
|
|
return p ? maskPairGet(p->ch_notif_melody_set, p->ch_notif_melody_2, ch_idx, 2, 1) : 0;
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
void setChNotifMelody(uint8_t ch_idx, uint8_t slot) {
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
2026-06-29 22:48:10 +02:00
|
|
|
if (p) maskPairSet(p->ch_notif_melody_set, p->ch_notif_melody_2, ch_idx, slot, 2);
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint8_t dmMelodySlot(const uint8_t* pub_key) const {
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
2026-06-29 22:48:10 +02:00
|
|
|
return p ? prefTableGet(p->dm_melody, NodePrefs::DM_MELODY_TABLE_MAX, pub_key,
|
|
|
|
|
&NodePrefs::DmMelodyEntry::slot) : 0;
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
void setDmMelody(const uint8_t* pub_key, uint8_t slot) {
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
2026-06-29 22:48:10 +02:00
|
|
|
if (p) prefTableSet(p->dm_melody, NodePrefs::DM_MELODY_TABLE_MAX, pub_key,
|
|
|
|
|
&NodePrefs::DmMelodyEntry::slot, slot);
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
// On-device channel Add/Edit form (Channels tab) — owned by this screen and
|
|
|
|
|
// delegated to while active(), the same relationship WaypointsView has with
|
|
|
|
|
// TrailScreen.
|
|
|
|
|
ChannelsView _ch_view;
|
|
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
public:
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
MessagesScreen(UITask* task, KeyboardWidget* kb)
|
2026-06-08 20:42:18 +02:00
|
|
|
: _task(task), _kb(kb), _phase(MODE_SELECT), _mode_sel(0),
|
2026-06-26 10:55:24 +02:00
|
|
|
_contact_sel(0), _contact_scroll(0), _num_contacts(0), _room_mode(false), _login_mode(false),
|
2026-05-17 00:12:40 +02:00
|
|
|
_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),
|
|
|
|
|
_dm_hist_sel(-1), _dm_hist_scroll(0),
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
_ctx_dirty(false), _pin_picker_active(false), _dm_direct_entry(false), _reply_mode(false),
|
|
|
|
|
_ch_view(task) {
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
// The history rings + per-channel unread counters init in MessageHistory.
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
// First free channel slot (existing config or blank name), or -1 if full.
|
|
|
|
|
int findFreeChannelSlot() const {
|
|
|
|
|
for (int i = 0; i < MAX_GROUP_CHANNELS; i++) {
|
|
|
|
|
ChannelDetails ch;
|
|
|
|
|
if (!the_mesh.getChannel(i, ch) || ch.name[0] == '\0') return i;
|
|
|
|
|
}
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CHANNEL_PICK row count including the synthetic "+ Add channel" row
|
|
|
|
|
// (suppressed while picking a channel for the bot).
|
|
|
|
|
int channelPickTotal() const { return _num_channels + (_pick_bot_channel ? 0 : 1); }
|
|
|
|
|
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
// Public entry points (routed from MyMesh / the bot via UITask) — thin
|
|
|
|
|
// forwarders to the history store. addChannelMsg computes the "viewing" flag
|
|
|
|
|
// (a phase-machine fact the store can't see) and returns the ring position so
|
|
|
|
|
// the outgoing path can attach a relay seq to that exact entry.
|
2026-06-28 11:00:45 +02:00
|
|
|
int addChannelMsg(uint8_t ch_idx, const char* text, uint32_t timestamp = 0) {
|
2026-05-30 23:57:12 +02:00
|
|
|
bool viewing = (_phase == CHANNEL_HIST && _sel_channel_idx == (int)ch_idx);
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
return _history.addChannelMsg(ch_idx, text, viewing, timestamp);
|
2026-06-14 23:33:16 +02:00
|
|
|
}
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
void markChannelRelayed(uint32_t seq) { _history.markChannelRelayed(seq); }
|
2026-06-14 23:33:16 +02:00
|
|
|
void addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text,
|
|
|
|
|
uint32_t sender_timestamp = 0) {
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
_history.addDMMsg(pub_key, outgoing, text, sender_timestamp);
|
2026-06-14 23:33:16 +02:00
|
|
|
}
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
void markDmDelivered(uint32_t ack_crc) { _history.markDmDelivered(ack_crc); }
|
2026-06-14 23:33:16 +02:00
|
|
|
|
2026-06-26 10:55:24 +02:00
|
|
|
// Rooms successfully logged in to this power-on session. RAM-only — the
|
|
|
|
|
// server's ACL (see ClientACL) is the real permission store and survives
|
|
|
|
|
// reboot on its own, but the device has no way to query it, so this is just
|
|
|
|
|
// a local memo to skip re-prompting for a password already entered this
|
|
|
|
|
// session when the same room is picked again.
|
|
|
|
|
static const int ROOM_LOGIN_TABLE_SIZE = 8;
|
|
|
|
|
uint8_t _room_login_prefix[ROOM_LOGIN_TABLE_SIZE][4];
|
|
|
|
|
int _room_login_head = 0, _room_login_count = 0;
|
|
|
|
|
|
|
|
|
|
bool isRoomLoggedIn(const uint8_t* pub_key) const {
|
|
|
|
|
for (int i = 0; i < _room_login_count; i++)
|
|
|
|
|
if (memcmp(_room_login_prefix[i], pub_key, 4) == 0) return true;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void markRoomLoggedIn(const uint8_t* pub_key) {
|
|
|
|
|
if (isRoomLoggedIn(pub_key)) return;
|
|
|
|
|
int pos;
|
|
|
|
|
if (_room_login_count < ROOM_LOGIN_TABLE_SIZE) {
|
|
|
|
|
pos = (_room_login_head + _room_login_count) % ROOM_LOGIN_TABLE_SIZE;
|
|
|
|
|
_room_login_count++;
|
|
|
|
|
} else {
|
|
|
|
|
pos = _room_login_head;
|
|
|
|
|
_room_login_head = (_room_login_head + 1) % ROOM_LOGIN_TABLE_SIZE;
|
|
|
|
|
}
|
|
|
|
|
memcpy(_room_login_prefix[pos], pub_key, 4);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-10 16:01:14 +02:00
|
|
|
// Reverses markRoomLoggedIn() on explicit Logout -- shifts the ring buffer
|
|
|
|
|
// closed over the removed slot so isRoomLoggedIn() goes back to false and
|
|
|
|
|
// the next room open prompts for a password instead of skipping it.
|
|
|
|
|
void forgetRoomLoggedIn(const uint8_t* pub_key) {
|
|
|
|
|
for (int i = 0; i < _room_login_count; i++) {
|
|
|
|
|
int pos = (_room_login_head + i) % ROOM_LOGIN_TABLE_SIZE;
|
|
|
|
|
if (memcmp(_room_login_prefix[pos], pub_key, 4) == 0) {
|
|
|
|
|
for (int j = i; j < _room_login_count - 1; j++) {
|
|
|
|
|
int from = (_room_login_head + j + 1) % ROOM_LOGIN_TABLE_SIZE;
|
|
|
|
|
int to = (_room_login_head + j) % ROOM_LOGIN_TABLE_SIZE;
|
|
|
|
|
memcpy(_room_login_prefix[to], _room_login_prefix[from], 4);
|
|
|
|
|
}
|
|
|
|
|
_room_login_count--;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-26 10:55:24 +02:00
|
|
|
// Password of the room-login attempt currently in flight -- set right
|
|
|
|
|
// before sendRoomLogin(), read back in onRoomLoginResult() so a successful
|
|
|
|
|
// attempt can be persisted (see MyMesh::saveRoomPassword()).
|
|
|
|
|
char _login_pw[16];
|
|
|
|
|
|
|
|
|
|
void startRoomLogin(const char* password) {
|
|
|
|
|
strncpy(_login_pw, password, sizeof(_login_pw) - 1);
|
|
|
|
|
_login_pw[sizeof(_login_pw) - 1] = 0;
|
|
|
|
|
bool sent = the_mesh.sendRoomLogin(_sel_contact, password);
|
|
|
|
|
_task->showAlert(sent ? "Logging in..." : "Login failed", sent ? 1000 : 1500);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Result of an on-device sendRoomLogin() (MyMesh::onContactResponse(), routed
|
|
|
|
|
// via AbstractUITask::onRoomLoginResult()). Surfaces as a transient alert.
|
|
|
|
|
void onRoomLoginResult(const uint8_t* pub_key, bool success, uint8_t permissions) {
|
|
|
|
|
(void)permissions;
|
|
|
|
|
if (success) {
|
|
|
|
|
markRoomLoggedIn(pub_key);
|
|
|
|
|
the_mesh.saveRoomPassword(pub_key, _login_pw);
|
2026-07-06 20:42:05 +02:00
|
|
|
// Auto-enter the room's chat right after a successful login so the user
|
|
|
|
|
// doesn't have to press Enter a second time. The login result is async,
|
|
|
|
|
// so only do it if they're still sitting on this same room in the picker
|
|
|
|
|
// (didn't navigate away, open a menu, or enter a share/pick sub-flow).
|
|
|
|
|
if (_phase == CONTACT_PICK && _room_mode && !_ctx_menu.active
|
feat(bot): room-server target, tab-carousel UI, per-target independence
Auto-Reply Bot gains a third target (room servers, alongside DM and
channel), new {name}/{hops} reply placeholders, and a DM all/favourites
allow-list. BotScreen is redesigned as a circular tab carousel (Direct /
Channel / Room / Other, same interaction as Nearby Nodes' filter tabs)
instead of one long scrolling list, which also exposed and fixed a
leftover coupling where Channel/Room trigger-replies and their !command
handling secretly depended on the DM tab's Enable/Commands toggles —
each target's Enable and Commands are now fully independent.
- NodePrefs: bot_room_enabled/prefix/trigger/reply, bot_dm_scope,
bot_commands_ch/bot_commands_room (bot_commands_enabled repurposed as
DM-only). SCHEMA_SENTINEL 0xC0DE001D -> 0xC0DE001F; sizeof unchanged
at 2712 (new bytes absorbed existing padding, verified via a
standalone host compile + offsetof check).
- DataStore: persists all new fields; seeds bot_commands_ch/room from
the old shared bot_commands_enabled on upgrade so existing
channel/room command behaviour isn't silently lost.
- MyMeshBot: tryBotReplyRoom/tryBotRoomCommand mirror the channel bot's
shape but post via sendMessage (room relays to members itself);
requires an existing login session with that room, same as a manual
post would. botDmSenderAllowed() gates DM trigger-reply/commands on
the favourites bit when bot_dm_scope=Fav.
- MsgExpand: {name}/{hops} as optional trailing params (default
nullptr/-1, no existing caller affected) — deliberately not exposed
on the general compose keyboard, only on bot Reply fields.
- QuickMsgScreen/UITask: room-target picker (mirrors the channel
picker), routing through the existing room-login prompt when there's
no saved password yet.
- BotScreen: tab carousel (LEFT/RIGHT switches tabs, UP/DOWN moves
rows, Enter is now the only way to change a value); Enable split out
of Channel/Room's combo row; Quiet Hours gained a stepper sub-mode;
Commands moved from a shared toggle into each tab.
- docs/tools_screen.md updated for the new tab layout and behaviour.
Not build-verified — no PlatformIO toolchain in this environment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 20:16:59 +02:00
|
|
|
&& !_share_mode && !_pick_target && !_pick_bot_room
|
2026-07-06 20:42:05 +02:00
|
|
|
&& memcmp(_sel_contact.id.pub_key, pub_key, 4) == 0) {
|
|
|
|
|
openDmHistory();
|
|
|
|
|
}
|
2026-06-26 10:55:24 +02:00
|
|
|
} else {
|
|
|
|
|
// Saved password (if any) no longer works -- forget it so the next
|
|
|
|
|
// ENTER on this room falls back to a manual prompt instead of
|
|
|
|
|
// silently retrying the same bad password forever.
|
|
|
|
|
the_mesh.forgetRoomPassword(pub_key);
|
|
|
|
|
}
|
|
|
|
|
_task->showAlert(success ? "Login OK" : "Login failed", 1200);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 20:42:05 +02:00
|
|
|
// Open the message history for the currently selected contact/room and reset
|
|
|
|
|
// the scroll/selection view state. Shared by the Enter-on-contact path and the
|
|
|
|
|
// post-login auto-enter.
|
|
|
|
|
void openDmHistory() {
|
|
|
|
|
_task->clearDMUnread(_sel_contact.id.pub_key);
|
|
|
|
|
_dm_hist_sel = -1;
|
|
|
|
|
_dm_hist_scroll = 0;
|
|
|
|
|
_dm_fs.active = false;
|
|
|
|
|
_phase = DM_HIST;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-14 23:33:16 +02:00
|
|
|
// Background tick (called every UI loop, regardless of the active screen) that
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
// drives auto-resend of on-device DMs — forwarded to the history store.
|
|
|
|
|
void tickDmResends() { _history.tickDmResends(); }
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
int getDMUnreadTotal() const {
|
|
|
|
|
return _task->getDMUnreadTotal();
|
|
|
|
|
}
|
|
|
|
|
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
int getTotalChannelUnread() const { return _history.getTotalChannelUnread(); }
|
2026-05-17 00:12:40 +02:00
|
|
|
|
2026-06-09 07:45:40 +02:00
|
|
|
void markReadAlert(int n) {
|
|
|
|
|
char msg[32];
|
|
|
|
|
snprintf(msg, sizeof(msg), "%d marked read", n);
|
|
|
|
|
_task->showAlert(msg, 800);
|
|
|
|
|
}
|
|
|
|
|
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
void clearAllChannelUnread() { _history.clearAllChannelUnread(); }
|
2026-05-17 00:12:40 +02:00
|
|
|
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
// Update the viewing-session unread bookkeeping (UI state) and push the
|
|
|
|
|
// resulting count down into the store. The ring lives in _history, but this
|
|
|
|
|
// "what has the user seen on screen" logic is pure phase-machine state.
|
2026-05-17 00:12:40 +02:00
|
|
|
void updateChannelUnread() {
|
feat(repeater): politeness controls, dedicated radio profile, and diagnostics
Settings > Radio gains a repeater toggle, 16 community-suggested radio
presets plus manual Freq/SF/BW/CR tuning (digit-by-digit Freq editor),
4 persisted user preset slots, and the packet pool bumped 16->32 so
queued retransmits stop starving incoming-packet allocation while
relaying.
Four opt-in politeness knobs (skip-advert, max-hops, yield, min-SNR),
all flood-only and off by default, plus overhear suppression that
cancels a queued retransmit if a peer relays the same packet first.
Adaptive Power Control is suppressed while relaying (pins TX power to
the ceiling) and duty-cycle RX is forced off, since a repeater needs
to hear and relay at consistent power.
A dedicated Tools > Repeater screen consolidates the toggle, the
politeness knobs, and live forwarding stats, plus an optional "Custom"
radio profile — a dedicated frequency/SF/BW/CR for relaying, separate
from the companion's own network, band-matched to the companion
frequency by default and used everywhere a radio change can happen
(boot, on-device toggle, app-driven CMD_SET_RADIO_PARAMS, Settings
radio edits) so the device never silently falls back to the wrong
params mid-relay.
Diagnostics gains the actually-forwarded packet count, real heap-free
via mallinfo(), a reset-counters popup, and hardened loop-detect
bounds. Also: a frequency-floor fix and a float-equality preset-match
fix in the repeater profile logic, deduped BW-table/valCol/profile-
seeding helpers shared between Settings and the Repeater screen, and
a build.sh fix tolerating control characters in `pio project config`'s
JSON dump.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 09:07:00 +02:00
|
|
|
if (_sel_channel_idx < 0 || _sel_channel_idx >= MAX_GROUP_CHANNELS) return;
|
2026-05-17 00:12:40 +02:00
|
|
|
// histEntryForChannel is newest-first: index 0 = newest (unread), higher = older.
|
feat(repeater): politeness controls, dedicated radio profile, and diagnostics
Settings > Radio gains a repeater toggle, 16 community-suggested radio
presets plus manual Freq/SF/BW/CR tuning (digit-by-digit Freq editor),
4 persisted user preset slots, and the packet pool bumped 16->32 so
queued retransmits stop starving incoming-packet allocation while
relaying.
Four opt-in politeness knobs (skip-advert, max-hops, yield, min-SNR),
all flood-only and off by default, plus overhear suppression that
cancels a queued retransmit if a peer relays the same packet first.
Adaptive Power Control is suppressed while relaying (pins TX power to
the ceiling) and duty-cycle RX is forced off, since a repeater needs
to hear and relay at consistent power.
A dedicated Tools > Repeater screen consolidates the toggle, the
politeness knobs, and live forwarding stats, plus an optional "Custom"
radio profile — a dedicated frequency/SF/BW/CR for relaying, separate
from the companion's own network, band-matched to the companion
frequency by default and used everywhere a radio change can happen
(boot, on-device toggle, app-driven CMD_SET_RADIO_PARAMS, Settings
radio edits) so the device never silently falls back to the wrong
params mid-relay.
Diagnostics gains the actually-forwarded packet count, real heap-free
via mallinfo(), a reset-counters popup, and hardened loop-detect
bounds. Also: a frequency-floor fix and a float-equality preset-match
fix in the repeater profile logic, deduped BW-table/valCol/profile-
seeding helpers shared between Settings and the Repeater screen, and
a build.sh fix tolerating control characters in `pio project config`'s
JSON dump.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 09:07:00 +02:00
|
|
|
// Count everything actually rendered on screen as seen — not just the
|
|
|
|
|
// highlighted row — so a taller screen that fits more boxes at once marks
|
|
|
|
|
// more read up front, instead of requiring a press per row.
|
|
|
|
|
int seen_to = _hist_scroll + _hist_visible - 1;
|
|
|
|
|
if (_hist_sel > seen_to) seen_to = _hist_sel;
|
|
|
|
|
if (seen_to > _viewing_max_seen) _viewing_max_seen = seen_to;
|
2026-05-17 00:12:40 +02:00
|
|
|
// Each step down from 0 sees one more message; seen count = max_seen + 1.
|
|
|
|
|
int remaining = _unread_at_entry - (_viewing_max_seen + 1);
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
_history.setChUnread(_sel_channel_idx, (uint8_t)(remaining > 0 ? remaining : 0));
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
2026-06-26 10:55:24 +02:00
|
|
|
_login_mode = false;
|
2026-05-17 00:12:40 +02:00
|
|
|
buildContactList();
|
|
|
|
|
buildChannelList();
|
|
|
|
|
|
|
|
|
|
_ctx_menu.active = false;
|
|
|
|
|
_ctx_dirty = false;
|
feat(nav): navigate to / save a location shared in a message
Locations already travel as plain text ({loc} expands to "lat,lon"), so any
message carrying coordinates can now be acted on. In the fullscreen message
view, the context-menu (Hold Enter) gains Navigate and Save waypoint
entries whenever the message contains a coordinate — alongside Reply.
- geo::parseLatLon: shared scanner for an embedded "lat,lon" (the {loc}
format), with an optional [WAY] tag that supplies a label. Range-checked,
requires a decimal point to avoid matching plain integer pairs. Will also
back the waypoint-share feature later.
- QuickMsgScreen: Navigate opens NavView inline (To/Hdg/distance, honouring
the global Units setting) over the message, back returns to it; Save
waypoint stores the point (label from [WAY], else auto-named). Works in
both DM and channel histories, and on incoming or outgoing messages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:28:17 +02:00
|
|
|
_nav_active = false;
|
2026-06-04 11:38:12 +02:00
|
|
|
_share_mode = false;
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
_pick_target = false;
|
feat(bot): room-server target, tab-carousel UI, per-target independence
Auto-Reply Bot gains a third target (room servers, alongside DM and
channel), new {name}/{hops} reply placeholders, and a DM all/favourites
allow-list. BotScreen is redesigned as a circular tab carousel (Direct /
Channel / Room / Other, same interaction as Nearby Nodes' filter tabs)
instead of one long scrolling list, which also exposed and fixed a
leftover coupling where Channel/Room trigger-replies and their !command
handling secretly depended on the DM tab's Enable/Commands toggles —
each target's Enable and Commands are now fully independent.
- NodePrefs: bot_room_enabled/prefix/trigger/reply, bot_dm_scope,
bot_commands_ch/bot_commands_room (bot_commands_enabled repurposed as
DM-only). SCHEMA_SENTINEL 0xC0DE001D -> 0xC0DE001F; sizeof unchanged
at 2712 (new bytes absorbed existing padding, verified via a
standalone host compile + offsetof check).
- DataStore: persists all new fields; seeds bot_commands_ch/room from
the old shared bot_commands_enabled on upgrade so existing
channel/room command behaviour isn't silently lost.
- MyMeshBot: tryBotReplyRoom/tryBotRoomCommand mirror the channel bot's
shape but post via sendMessage (room relays to members itself);
requires an existing login session with that room, same as a manual
post would. botDmSenderAllowed() gates DM trigger-reply/commands on
the favourites bit when bot_dm_scope=Fav.
- MsgExpand: {name}/{hops} as optional trailing params (default
nullptr/-1, no existing caller affected) — deliberately not exposed
on the general compose keyboard, only on bot Reply fields.
- QuickMsgScreen/UITask: room-target picker (mirrors the channel
picker), routing through the existing room-login prompt when there's
no saved password yet.
- BotScreen: tab carousel (LEFT/RIGHT switches tabs, UP/DOWN moves
rows, Enter is now the only way to change a value); Enable split out
of Channel/Room's combo row; Quiet Hours gained a stepper sub-mode;
Commands moved from a shared toggle into each tab.
- docs/tools_screen.md updated for the new tab layout and behaviour.
Not build-verified — no PlatformIO toolchain in this environment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 20:16:59 +02:00
|
|
|
_pick_bot_channel = false;
|
|
|
|
|
_pick_bot_room = 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;
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
_ch_view.reset();
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
// Recent DM contacts, newest first, deduped (forwarded to the history store).
|
2026-05-25 08:08:12 +02:00
|
|
|
int getRecentDMContacts(uint8_t out[][NodePrefs::FAVOURITE_PREFIX_LEN], int max) const {
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
return _history.getRecentDMContacts(out, max);
|
2026-05-25 08:08:12 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-25 07:52:56 +02:00
|
|
|
// Jump straight into a contact's DM history (used by the Favourites dial).
|
2026-05-25 07:58:45 +02:00
|
|
|
// Caller must have already reset() the screen. Marks the entry so KEY_CANCEL
|
|
|
|
|
// from DM_HIST returns to the home screen instead of falling back through
|
|
|
|
|
// CONTACT_PICK → MODE_SELECT.
|
2026-06-04 11:38:12 +02:00
|
|
|
// Enter the screen pre-loaded to share `text` (e.g. a "[WAY]lat,lon label"
|
|
|
|
|
// waypoint). The user picks Direct/Channel then a recipient; selecting one
|
|
|
|
|
// jumps straight to the keyboard prefilled with the text (see beginShareCompose).
|
|
|
|
|
void startShare(const char* text) {
|
|
|
|
|
reset();
|
|
|
|
|
_share_mode = true;
|
|
|
|
|
strncpy(_share_text, text, sizeof(_share_text) - 1);
|
|
|
|
|
_share_text[sizeof(_share_text) - 1] = '\0';
|
|
|
|
|
_phase = MODE_SELECT;
|
|
|
|
|
}
|
|
|
|
|
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
// Open the recipient chooser to set the live-share target (channel/DM). On
|
|
|
|
|
// selection the target is stored in NodePrefs and the Map screen is restored.
|
|
|
|
|
void startPickTarget() {
|
|
|
|
|
reset();
|
|
|
|
|
_pick_target = true;
|
|
|
|
|
_phase = MODE_SELECT;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void commitPickTargetChannel(int ch_idx) {
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
if (p) {
|
|
|
|
|
p->loc_share_target_type = 0;
|
|
|
|
|
p->loc_share_channel_idx = (uint8_t)ch_idx;
|
|
|
|
|
the_mesh.savePrefs();
|
|
|
|
|
}
|
|
|
|
|
_pick_target = false;
|
|
|
|
|
_task->showAlert("Share target set", 1200);
|
|
|
|
|
_task->gotoLiveShareScreen();
|
|
|
|
|
}
|
|
|
|
|
|
feat(bot): room-server target, tab-carousel UI, per-target independence
Auto-Reply Bot gains a third target (room servers, alongside DM and
channel), new {name}/{hops} reply placeholders, and a DM all/favourites
allow-list. BotScreen is redesigned as a circular tab carousel (Direct /
Channel / Room / Other, same interaction as Nearby Nodes' filter tabs)
instead of one long scrolling list, which also exposed and fixed a
leftover coupling where Channel/Room trigger-replies and their !command
handling secretly depended on the DM tab's Enable/Commands toggles —
each target's Enable and Commands are now fully independent.
- NodePrefs: bot_room_enabled/prefix/trigger/reply, bot_dm_scope,
bot_commands_ch/bot_commands_room (bot_commands_enabled repurposed as
DM-only). SCHEMA_SENTINEL 0xC0DE001D -> 0xC0DE001F; sizeof unchanged
at 2712 (new bytes absorbed existing padding, verified via a
standalone host compile + offsetof check).
- DataStore: persists all new fields; seeds bot_commands_ch/room from
the old shared bot_commands_enabled on upgrade so existing
channel/room command behaviour isn't silently lost.
- MyMeshBot: tryBotReplyRoom/tryBotRoomCommand mirror the channel bot's
shape but post via sendMessage (room relays to members itself);
requires an existing login session with that room, same as a manual
post would. botDmSenderAllowed() gates DM trigger-reply/commands on
the favourites bit when bot_dm_scope=Fav.
- MsgExpand: {name}/{hops} as optional trailing params (default
nullptr/-1, no existing caller affected) — deliberately not exposed
on the general compose keyboard, only on bot Reply fields.
- QuickMsgScreen/UITask: room-target picker (mirrors the channel
picker), routing through the existing room-login prompt when there's
no saved password yet.
- BotScreen: tab carousel (LEFT/RIGHT switches tabs, UP/DOWN moves
rows, Enter is now the only way to change a value); Enable split out
of Channel/Room's combo row; Quiet Hours gained a stepper sub-mode;
Commands moved from a shared toggle into each tab.
- docs/tools_screen.md updated for the new tab layout and behaviour.
Not build-verified — no PlatformIO toolchain in this environment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 20:16:59 +02:00
|
|
|
// Open the channel chooser to set the auto-reply bot's channel. Skips
|
|
|
|
|
// MODE_SELECT (the bot only ever targets a channel) and lands straight on
|
|
|
|
|
// CHANNEL_PICK, browsing real channel names instead of a bare index cycle.
|
|
|
|
|
void startPickBotChannel() {
|
|
|
|
|
reset();
|
|
|
|
|
_pick_bot_channel = true;
|
|
|
|
|
_phase = CHANNEL_PICK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void commitPickBotChannel(int ch_idx) {
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
if (p) {
|
|
|
|
|
p->bot_channel_enabled = 1;
|
|
|
|
|
p->bot_channel_idx = (uint8_t)ch_idx;
|
|
|
|
|
the_mesh.savePrefs();
|
|
|
|
|
}
|
|
|
|
|
_pick_bot_channel = false;
|
|
|
|
|
_task->showAlert("Bot channel set", 1200);
|
|
|
|
|
_task->gotoBotScreen();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Open the room chooser to set the auto-reply bot's room. Enter routes
|
|
|
|
|
// through the same login handling the normal room-open flow uses (see the
|
|
|
|
|
// CONTACT_PICK/room_mode Enter handler below) — the bot can't ever post to
|
|
|
|
|
// a room it has no password for, so picking one is a good moment to prompt.
|
|
|
|
|
void startPickBotRoom() {
|
|
|
|
|
reset();
|
|
|
|
|
_pick_bot_room = true;
|
|
|
|
|
_room_mode = true;
|
|
|
|
|
buildContactList(); // rebuild now that _room_mode is on (reset() built the DM list)
|
|
|
|
|
_contact_sel = _contact_scroll = 0;
|
|
|
|
|
_phase = CONTACT_PICK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void commitPickBotRoom(const ContactInfo& ci) {
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
if (p) {
|
|
|
|
|
p->bot_room_enabled = 1;
|
|
|
|
|
memcpy(p->bot_room_prefix, ci.id.pub_key, NodePrefs::FAVOURITE_PREFIX_LEN);
|
|
|
|
|
the_mesh.savePrefs();
|
|
|
|
|
}
|
|
|
|
|
_pick_bot_room = false;
|
|
|
|
|
_room_mode = false;
|
|
|
|
|
_task->showAlert("Bot room set", 1200);
|
|
|
|
|
_task->gotoBotScreen();
|
|
|
|
|
}
|
|
|
|
|
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
void commitPickTargetDM(const ContactInfo& ci) {
|
|
|
|
|
// A room server can't be a live-share target — a [LOC] DM to it is never
|
|
|
|
|
// reposted to the room's members. Reject the pick and keep the chooser open.
|
|
|
|
|
if (ci.type == ADV_TYPE_ROOM) {
|
|
|
|
|
_task->showAlert("Rooms not supported", 1400);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
if (p) {
|
|
|
|
|
p->loc_share_target_type = 1;
|
|
|
|
|
memcpy(p->loc_share_dm_prefix, ci.id.pub_key, NodePrefs::FAVOURITE_PREFIX_LEN);
|
|
|
|
|
the_mesh.savePrefs();
|
|
|
|
|
}
|
|
|
|
|
_pick_target = false;
|
|
|
|
|
_task->showAlert("Share target set", 1200);
|
|
|
|
|
_task->gotoLiveShareScreen();
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 07:52:56 +02:00
|
|
|
void enterDM(const ContactInfo& ci) {
|
|
|
|
|
_sel_contact = ci;
|
|
|
|
|
_task->clearDMUnread(ci.id.pub_key);
|
|
|
|
|
_dm_hist_sel = -1;
|
|
|
|
|
_dm_hist_scroll = 0;
|
|
|
|
|
_dm_fs.active = false;
|
|
|
|
|
_room_mode = false;
|
|
|
|
|
_phase = DM_HIST;
|
2026-05-25 07:58:45 +02:00
|
|
|
_dm_direct_entry = true;
|
2026-05-25 07:52:56 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
int render(DisplayDriver& display) override {
|
|
|
|
|
display.setTextSize(1);
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
// Channel Add/Edit form owns the screen while active.
|
|
|
|
|
if (_ch_view.active()) return _ch_view.render(display);
|
|
|
|
|
|
feat(nav): navigate to / save a location shared in a message
Locations already travel as plain text ({loc} expands to "lat,lon"), so any
message carrying coordinates can now be acted on. In the fullscreen message
view, the context-menu (Hold Enter) gains Navigate and Save waypoint
entries whenever the message contains a coordinate — alongside Reply.
- geo::parseLatLon: shared scanner for an embedded "lat,lon" (the {loc}
format), with an optional [WAY] tag that supplies a label. Range-checked,
requires a decimal point to avoid matching plain integer pairs. Will also
back the waypoint-share feature later.
- QuickMsgScreen: Navigate opens NavView inline (To/Hdg/distance, honouring
the global Units setting) over the message, back returns to it; Save
waypoint stores the point (label from [WAY], else auto-named). Works in
both DM and channel histories, and on incoming or outgoing messages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:28:17 +02:00
|
|
|
// Navigate-to-location view sits over everything else while active.
|
|
|
|
|
if (_nav_active) { renderNav(display); return 1000; }
|
|
|
|
|
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
int lh = display.getLineHeight();
|
|
|
|
|
int item_h = display.lineStep();
|
|
|
|
|
int start_y = display.listStart();
|
|
|
|
|
int cw = display.getCharWidth();
|
|
|
|
|
|
2026-05-17 00:12:40 +02:00
|
|
|
if (_phase == MODE_SELECT) {
|
feat(ui): discoverable context-menu hint — header glyph, highlight, corner dropdown
Hold-Enter context menus were invisible. Add a menu affordance to every screen
that has one:
- A small ≡ glyph in the header's top-right, via new DisplayDriver::
drawContextMenuHint() and an optional menu_hint arg on drawCenteredHeader() /
drawInvertedHeader() (reserves the corner so the title never runs under it).
- The glyph highlights (corner cell filled, bars knocked out) while the menu is
open, tying the hint to the popup it spawned — driven by a menu_open flag the
screens pass from their live popup state.
- The context menu now drops out of that corner: PopupMenu gains an opt-in
top-right anchor (begin(..., anchor_top_right)); it right-aligns under the
header instead of centring, so the menu reads as emerging from the ≡. Default
stays centred, so non-context popups (Tools/Settings/etc.) are untouched.
Enabled on Nodes (list/scan/detail) and Messages (mode select, contact/room and
channel pickers, DM and channel history).
Both solo envs build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 10:31:01 +02:00
|
|
|
display.drawCenteredHeader("MESSAGE", true, _ctx_menu.active);
|
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);
|
feat(ui): proportional scrollbar with mini-icon caps; reclaim row space
Replace the text ^/v scroll arrows with a font-scaling indicator drawn in
icons.h: a proportional thumb between fixed up/down triangle mini-icon caps.
drawScrollIndicator() lives in a ~5px right-edge column and scales via
miniIconScale (1x OLED, 2x landscape e-ink).
- Mini-icons: ICON_SCROLL_UP/DOWN; miniIconDrawTop (exact placement) and
miniIconDrawHalo (shape-hugging 1px DARK halo, clip_top to spare the header
separator) so caps stay visible on the LIGHT selection bar without a box.
- Thumb is 3*s wide so it centres on the triangle column; caps are static end
markers (always drawn) so neither vanishes at the extremes.
- scrollIndicatorReserve(): right-edge gutter (0 when the list fits). Content,
the selection bar and the message-history bubbles are all narrowed by it so
nothing renders under the scrollbar. Portrait e-ink wrap width subtracts the
reserve in both the box-height pass and render so wrapped text can't spill.
- Drop the redundant ">" selection marker (the highlight bar already shows
selection) and shift rows to x=2, reclaiming left-edge space.
- Wire the indicator into every scrollable list (Bot, Settings, Nearby, Tools,
Trail, QuickMsg lists + DM/channel history) and add one to WaypointsView,
which scrolled with no indicator before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:41:46 +02:00
|
|
|
display.setCursor(2, y);
|
2026-05-17 00:12:40 +02:00
|
|
|
display.print(opts[i]);
|
feat(ui): pill unread badges, unified DM/CH headers, trimmed home carousel
UI-polish trio from CODE_REVIEW (biggest "feels finished" gain per line):
- Pill unread badges: new DisplayDriver::drawUnreadBadge()/unreadBadgeWidth()
draw a filled capsule with the count knocked out (corners knocked back for a
rounded look; inverts on a selected row). Replaces the bare right-aligned
digits in MODE_SELECT, the contact/channel pickers and favourites tiles.
No <stdio.h> in the header — fmtBadgeCount formats manually, clamps to 99+.
- Unified DM/CH history headers: DM_HIST and CHANNEL_HIST drew their titles by
hand (drawTextCentered + fillRect at lh+1), a different height/separator than
every other screen. Both now route through drawCenteredHeader().
- Trimmed default home carousel: new NodePrefs::HP_DEFAULT (Clock, Tools,
Shutdown, Favourites, Map; Messages + Settings always visible = 7 pages).
applyDefaults() seeds it instead of HP_ALL. Recent/Radio/BT/Advert/GPS/Sensors
are opt-in via Settings > Home Pages. Existing users keep their saved mask —
factory default only; no migration, no schema bump.
Both solo envs build green (OLED RAM 69.9%/Flash 62.8%; e-ink SUCCESS).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 01:12:51 +02:00
|
|
|
if (badges[i] > 0)
|
|
|
|
|
display.drawUnreadBadge(display.width() - 1, y, badges[i], sel);
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
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) {
|
feat(ui): discoverable context-menu hint — header glyph, highlight, corner dropdown
Hold-Enter context menus were invisible. Add a menu affordance to every screen
that has one:
- A small ≡ glyph in the header's top-right, via new DisplayDriver::
drawContextMenuHint() and an optional menu_hint arg on drawCenteredHeader() /
drawInvertedHeader() (reserves the corner so the title never runs under it).
- The glyph highlights (corner cell filled, bars knocked out) while the menu is
open, tying the hint to the popup it spawned — driven by a menu_open flag the
screens pass from their live popup state.
- The context menu now drops out of that corner: PopupMenu gains an opt-in
top-right anchor (begin(..., anchor_top_right)); it right-aligns under the
header instead of centring, so the menu reads as emerging from the ≡. Default
stays centred, so non-context popups (Tools/Settings/etc.) are untouched.
Enabled on Nodes (list/scan/detail) and Messages (mode select, contact/room and
channel pickers, DM and channel history).
Both solo envs build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 10:31:01 +02:00
|
|
|
display.drawCenteredHeader(_room_mode ? "SELECT ROOM" : "SELECT CONTACT", true, _ctx_menu.active);
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 08:54:11 +02:00
|
|
|
drawList(display, _num_contacts, _contact_sel, _contact_scroll, [&](int list_idx, int y, bool sel, int reserve) {
|
2026-05-17 00:12:40 +02:00
|
|
|
int mesh_idx = _sorted[list_idx];
|
|
|
|
|
|
2026-06-29 18:53:18 +02:00
|
|
|
drawRowSelection(display, y, sel, reserve);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
ContactInfo c;
|
|
|
|
|
if (the_mesh.getContactByIdx(mesh_idx, c)) {
|
|
|
|
|
char filtered[sizeof(c.name)];
|
|
|
|
|
display.translateUTF8ToBlocks(filtered, c.name, sizeof(filtered));
|
|
|
|
|
uint8_t dm_unread = _task->getDMUnread(c.id.pub_key);
|
feat(ui): pill unread badges, unified DM/CH headers, trimmed home carousel
UI-polish trio from CODE_REVIEW (biggest "feels finished" gain per line):
- Pill unread badges: new DisplayDriver::drawUnreadBadge()/unreadBadgeWidth()
draw a filled capsule with the count knocked out (corners knocked back for a
rounded look; inverts on a selected row). Replaces the bare right-aligned
digits in MODE_SELECT, the contact/channel pickers and favourites tiles.
No <stdio.h> in the header — fmtBadgeCount formats manually, clamps to 99+.
- Unified DM/CH history headers: DM_HIST and CHANNEL_HIST drew their titles by
hand (drawTextCentered + fillRect at lh+1), a different height/separator than
every other screen. Both now route through drawCenteredHeader().
- Trimmed default home carousel: new NodePrefs::HP_DEFAULT (Clock, Tools,
Shutdown, Favourites, Map; Messages + Settings always visible = 7 pages).
applyDefaults() seeds it instead of HP_ALL. Recent/Radio/BT/Advert/GPS/Sensors
are opt-in via Settings > Home Pages. Existing users keep their saved mask —
factory default only; no migration, no schema bump.
Both solo envs build green (OLED RAM 69.9%/Flash 62.8%; e-ink SUCCESS).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 01:12:51 +02:00
|
|
|
int bw = dm_unread > 0 ? display.unreadBadgeWidth(dm_unread) + 2 : 0;
|
|
|
|
|
display.drawTextEllipsized(2, y, display.width() - 2 - bw - reserve, filtered);
|
|
|
|
|
if (dm_unread > 0)
|
|
|
|
|
display.drawUnreadBadge(display.width() - reserve, y, dm_unread, sel);
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
refactor(ui): shared drawList + header + key-decode helpers
- drawList(): scrollable list skeleton (visible window, keep-sel-in-view,
scroll-column reserve, per-row callback, indicator). Migrate Tools, Bot,
Nearby, Waypoints list and the three QuickMsg lists onto it; centralises the
scroll-clamp/reserve/indicator that each had open-coded (and inconsistently).
- drawCenteredHeader(): centred title + separator, replacing the duplicated
two-liner across ~10 screens (incl. QuickMsg, which used width()/2 spacing and
was missed before); unifies the separator at headerH()-sepH().
- keyIsPrev()/keyIsNext() in UIScreen.h for the LEFT||PREV / RIGHT||NEXT idiom;
applied to 6 screens, behaviour-preserving. Cancel/context-menu stay
per-screen (they mean different things on different screens).
- rotLabel() dedups the 0/90/180/270 array in Settings.
- Unify selection-row height to lineStep()-1 (Tools/Bot matched the rest).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:23:00 +02:00
|
|
|
});
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
// Context menu overlay
|
|
|
|
|
if (_ctx_menu.active) _ctx_menu.render(display);
|
|
|
|
|
|
|
|
|
|
} else if (_phase == CHANNEL_PICK) {
|
feat(ui): discoverable context-menu hint — header glyph, highlight, corner dropdown
Hold-Enter context menus were invisible. Add a menu affordance to every screen
that has one:
- A small ≡ glyph in the header's top-right, via new DisplayDriver::
drawContextMenuHint() and an optional menu_hint arg on drawCenteredHeader() /
drawInvertedHeader() (reserves the corner so the title never runs under it).
- The glyph highlights (corner cell filled, bars knocked out) while the menu is
open, tying the hint to the popup it spawned — driven by a menu_open flag the
screens pass from their live popup state.
- The context menu now drops out of that corner: PopupMenu gains an opt-in
top-right anchor (begin(..., anchor_top_right)); it right-aligns under the
header instead of centring, so the menu reads as emerging from the ≡. Default
stays centred, so non-context popups (Tools/Settings/etc.) are untouched.
Enabled on Nodes (list/scan/detail) and Messages (mode select, contact/room and
channel pickers, DM and channel history).
Both solo envs build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 10:31:01 +02:00
|
|
|
display.drawCenteredHeader("SELECT CHANNEL", true, _ctx_menu.active);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
// "+ Add channel" is a synthetic trailing row — suppressed while picking
|
|
|
|
|
// a channel for the bot, so that picker's list stays unchanged.
|
|
|
|
|
bool show_add = !_pick_bot_channel;
|
|
|
|
|
int total = _num_channels + (show_add ? 1 : 0);
|
|
|
|
|
|
|
|
|
|
if (total == 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;
|
|
|
|
|
}
|
|
|
|
|
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
drawList(display, total, _channel_sel, _channel_scroll, [&](int list_idx, int y, bool sel, int reserve) {
|
2026-06-29 18:53:18 +02:00
|
|
|
drawRowSelection(display, y, sel, reserve);
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
if (list_idx == _num_channels) { // the synthetic "Add" row
|
|
|
|
|
display.setCursor(2, y); display.print("+ Add channel");
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
ChannelDetails ch;
|
|
|
|
|
if (the_mesh.getChannel(_channel_indices[list_idx], ch)) {
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
uint8_t unread = _history.chUnread(_channel_indices[list_idx]);
|
feat(ui): pill unread badges, unified DM/CH headers, trimmed home carousel
UI-polish trio from CODE_REVIEW (biggest "feels finished" gain per line):
- Pill unread badges: new DisplayDriver::drawUnreadBadge()/unreadBadgeWidth()
draw a filled capsule with the count knocked out (corners knocked back for a
rounded look; inverts on a selected row). Replaces the bare right-aligned
digits in MODE_SELECT, the contact/channel pickers and favourites tiles.
No <stdio.h> in the header — fmtBadgeCount formats manually, clamps to 99+.
- Unified DM/CH history headers: DM_HIST and CHANNEL_HIST drew their titles by
hand (drawTextCentered + fillRect at lh+1), a different height/separator than
every other screen. Both now route through drawCenteredHeader().
- Trimmed default home carousel: new NodePrefs::HP_DEFAULT (Clock, Tools,
Shutdown, Favourites, Map; Messages + Settings always visible = 7 pages).
applyDefaults() seeds it instead of HP_ALL. Recent/Radio/BT/Advert/GPS/Sensors
are opt-in via Settings > Home Pages. Existing users keep their saved mask —
factory default only; no migration, no schema bump.
Both solo envs build green (OLED RAM 69.9%/Flash 62.8%; e-ink SUCCESS).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 01:12:51 +02:00
|
|
|
int bw = unread > 0 ? display.unreadBadgeWidth(unread) + 2 : 0;
|
feat(ui): proportional scrollbar with mini-icon caps; reclaim row space
Replace the text ^/v scroll arrows with a font-scaling indicator drawn in
icons.h: a proportional thumb between fixed up/down triangle mini-icon caps.
drawScrollIndicator() lives in a ~5px right-edge column and scales via
miniIconScale (1x OLED, 2x landscape e-ink).
- Mini-icons: ICON_SCROLL_UP/DOWN; miniIconDrawTop (exact placement) and
miniIconDrawHalo (shape-hugging 1px DARK halo, clip_top to spare the header
separator) so caps stay visible on the LIGHT selection bar without a box.
- Thumb is 3*s wide so it centres on the triangle column; caps are static end
markers (always drawn) so neither vanishes at the extremes.
- scrollIndicatorReserve(): right-edge gutter (0 when the list fits). Content,
the selection bar and the message-history bubbles are all narrowed by it so
nothing renders under the scrollbar. Portrait e-ink wrap width subtracts the
reserve in both the box-height pass and render so wrapped text can't spill.
- Drop the redundant ">" selection marker (the highlight bar already shows
selection) and shift rows to x=2, reclaiming left-edge space.
- Wire the indicator into every scrollable list (Bot, Settings, Nearby, Tools,
Trail, QuickMsg lists + DM/channel history) and add one to WaypointsView,
which scrolled with no indicator before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:41:46 +02:00
|
|
|
display.drawTextEllipsized(2, y, display.width() - 4 - bw - reserve, ch.name);
|
feat(ui): pill unread badges, unified DM/CH headers, trimmed home carousel
UI-polish trio from CODE_REVIEW (biggest "feels finished" gain per line):
- Pill unread badges: new DisplayDriver::drawUnreadBadge()/unreadBadgeWidth()
draw a filled capsule with the count knocked out (corners knocked back for a
rounded look; inverts on a selected row). Replaces the bare right-aligned
digits in MODE_SELECT, the contact/channel pickers and favourites tiles.
No <stdio.h> in the header — fmtBadgeCount formats manually, clamps to 99+.
- Unified DM/CH history headers: DM_HIST and CHANNEL_HIST drew their titles by
hand (drawTextCentered + fillRect at lh+1), a different height/separator than
every other screen. Both now route through drawCenteredHeader().
- Trimmed default home carousel: new NodePrefs::HP_DEFAULT (Clock, Tools,
Shutdown, Favourites, Map; Messages + Settings always visible = 7 pages).
applyDefaults() seeds it instead of HP_ALL. Recent/Radio/BT/Advert/GPS/Sensors
are opt-in via Settings > Home Pages. Existing users keep their saved mask —
factory default only; no migration, no schema bump.
Both solo envs build green (OLED RAM 69.9%/Flash 62.8%; e-ink SUCCESS).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 01:12:51 +02:00
|
|
|
if (unread > 0)
|
|
|
|
|
display.drawUnreadBadge(display.width() - reserve, y, unread, sel);
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
refactor(ui): shared drawList + header + key-decode helpers
- drawList(): scrollable list skeleton (visible window, keep-sel-in-view,
scroll-column reserve, per-row callback, indicator). Migrate Tools, Bot,
Nearby, Waypoints list and the three QuickMsg lists onto it; centralises the
scroll-clamp/reserve/indicator that each had open-coded (and inconsistently).
- drawCenteredHeader(): centred title + separator, replacing the duplicated
two-liner across ~10 screens (incl. QuickMsg, which used width()/2 spacing and
was missed before); unifies the separator at headerH()-sepH().
- keyIsPrev()/keyIsNext() in UIScreen.h for the LEFT||PREV / RIGHT||NEXT idiom;
applied to 6 screens, behaviour-preserving. Cancel/context-menu stay
per-screen (they mean different things on different screens).
- rotLabel() dedups the 0/90/180/270 array in Settings.
- Unify selection-row height to lineStep()-1 (Tools/Bot matched the rest).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:23:00 +02:00
|
|
|
});
|
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) {
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
int ring_pos = _history.dmHistEntryForContact(_sel_contact.id.pub_key, _dm_hist_sel);
|
2026-05-17 00:12:40 +02:00
|
|
|
if (ring_pos >= 0) {
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
const DmHistEntry& e = _history.dmAtPos(ring_pos);
|
2026-06-22 18:31:47 +02:00
|
|
|
char sender_buf[33];
|
|
|
|
|
const char* body = dmDisplayParts(e, _sel_contact.type == ADV_TYPE_ROOM,
|
|
|
|
|
filtered_name, sender_buf, sizeof(sender_buf));
|
|
|
|
|
const char* sender = sender_buf;
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
int dm_count = _history.dmHistCountForContact(_sel_contact.id.pub_key);
|
2026-06-22 18:31:47 +02:00
|
|
|
int ret = _dm_fs.render(display, sender, body,
|
2026-05-18 16:11:25 +02:00
|
|
|
_dm_hist_sel < dm_count - 1,
|
|
|
|
|
_dm_hist_sel > 0);
|
2026-06-14 23:33:16 +02:00
|
|
|
if (e.outgoing) { // delivery marker in the (inverted) header bar
|
|
|
|
|
display.setColor(DisplayDriver::DARK);
|
|
|
|
|
drawAckGlyph(display, 2 + display.getTextWidth(sender) + 3, 1,
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
_history.dmEffectiveStatus(e), e.attempt + 1);
|
2026-06-14 23:33:16 +02:00
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
}
|
2026-05-18 16:11:25 +02:00
|
|
|
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];
|
|
|
|
|
snprintf(title, sizeof(title), "%.23s", filtered_name);
|
feat(ui): discoverable context-menu hint — header glyph, highlight, corner dropdown
Hold-Enter context menus were invisible. Add a menu affordance to every screen
that has one:
- A small ≡ glyph in the header's top-right, via new DisplayDriver::
drawContextMenuHint() and an optional menu_hint arg on drawCenteredHeader() /
drawInvertedHeader() (reserves the corner so the title never runs under it).
- The glyph highlights (corner cell filled, bars knocked out) while the menu is
open, tying the hint to the popup it spawned — driven by a menu_open flag the
screens pass from their live popup state.
- The context menu now drops out of that corner: PopupMenu gains an opt-in
top-right anchor (begin(..., anchor_top_right)); it right-aligns under the
header instead of centring, so the menu reads as emerging from the ≡. Default
stays centred, so non-context popups (Tools/Settings/etc.) are untouched.
Enabled on Nodes (list/scan/detail) and Messages (mode select, contact/room and
channel pickers, DM and channel history).
Both solo envs build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 10:31:01 +02:00
|
|
|
display.drawCenteredHeader(title, true, _ctx_menu.active);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
int dm_count = _history.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-06-22 18:31:47 +02:00
|
|
|
bool is_room = (_sel_contact.type == ADV_TYPE_ROOM);
|
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;
|
2026-06-16 09:32:09 +02:00
|
|
|
// Fixed-track scrollbar metrics + a stable gutter reserve (decided by a
|
|
|
|
|
// whole-list fit test, not last frame's visible count) so message boxes
|
|
|
|
|
// don't reflow their width as messages arrive.
|
|
|
|
|
HistScroll hs = computeHistScroll(display, portrait_expand, dm_count, _dm_hist_scroll,
|
|
|
|
|
hist_start_y, cby, lh,
|
|
|
|
|
[&](int idx) -> const char* {
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
int rp = _history.dmHistEntryForContact(_sel_contact.id.pub_key, idx);
|
|
|
|
|
return rp >= 0 ? _history.dmAtPos(rp).text : nullptr;
|
2026-06-16 09:32:09 +02:00
|
|
|
});
|
|
|
|
|
int reserve = hs.reserve;
|
2026-05-27 23:47:30 +02:00
|
|
|
{
|
|
|
|
|
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) {
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
int rp = _history.dmHistEntryForContact(_sel_contact.id.pub_key, _dm_hist_scroll + ii);
|
2026-05-27 23:47:30 +02:00
|
|
|
if (rp >= 0) {
|
2026-06-22 18:31:47 +02:00
|
|
|
char hsb[33];
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
const char* hbody = dmDisplayParts(_history.dmAtPos(rp), is_room, filtered_name, hsb, sizeof(hsb));
|
2026-06-22 18:31:47 +02:00
|
|
|
display.translateUTF8ToBlocks(s_wrap_trans, hbody, sizeof(s_wrap_trans));
|
2026-06-16 08:54:11 +02:00
|
|
|
int nl = FullscreenMsgView::wrapLines(display, s_wrap_trans, display.width() - 6 - reserve, s_wrap_lines, 8);
|
2026-05-27 23:47:30 +02:00
|
|
|
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
|
|
|
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
int ring_pos = _history.dmHistEntryForContact(_sel_contact.id.pub_key, item);
|
2026-05-17 00:12:40 +02:00
|
|
|
if (ring_pos < 0) continue;
|
|
|
|
|
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
const DmHistEntry& e = _history.dmAtPos(ring_pos);
|
2026-06-22 18:31:47 +02:00
|
|
|
char sender_buf[33];
|
|
|
|
|
const char* body = dmDisplayParts(e, is_room, filtered_name, sender_buf, sizeof(sender_buf));
|
|
|
|
|
const char* sender = sender_buf;
|
2026-05-17 00:12:40 +02:00
|
|
|
|
2026-06-29 17:43:17 +02:00
|
|
|
char age[6]; geo::fmtAgeShort(age, sizeof(age), now_ts, e.timestamp);
|
2026-05-19 20:21:32 +02:00
|
|
|
int age_w = age[0] ? display.getTextWidth(age) + 3 : 0;
|
|
|
|
|
|
2026-06-29 23:05:11 +02:00
|
|
|
drawHistRowFrame(display, y, bh, reserve, lh, sel);
|
feat(ui): proportional scrollbar with mini-icon caps; reclaim row space
Replace the text ^/v scroll arrows with a font-scaling indicator drawn in
icons.h: a proportional thumb between fixed up/down triangle mini-icon caps.
drawScrollIndicator() lives in a ~5px right-edge column and scales via
miniIconScale (1x OLED, 2x landscape e-ink).
- Mini-icons: ICON_SCROLL_UP/DOWN; miniIconDrawTop (exact placement) and
miniIconDrawHalo (shape-hugging 1px DARK halo, clip_top to spare the header
separator) so caps stay visible on the LIGHT selection bar without a box.
- Thumb is 3*s wide so it centres on the triangle column; caps are static end
markers (always drawn) so neither vanishes at the extremes.
- scrollIndicatorReserve(): right-edge gutter (0 when the list fits). Content,
the selection bar and the message-history bubbles are all narrowed by it so
nothing renders under the scrollbar. Portrait e-ink wrap width subtracts the
reserve in both the box-height pass and render so wrapped text can't spill.
- Drop the redundant ">" selection marker (the highlight bar already shows
selection) and shift rows to x=2, reclaiming left-edge space.
- Wire the indicator into every scrollable list (Bot, Settings, Nearby, Tools,
Trail, QuickMsg lists + DM/channel history) and add one to WaypointsView,
which scrolled with no indicator before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:41:46 +02:00
|
|
|
display.drawTextEllipsized(3, y + 1, display.width() - cw - 2 - age_w - reserve, sender);
|
2026-06-14 23:33:16 +02:00
|
|
|
if (e.outgoing) { // delivery marker after "Me"
|
|
|
|
|
int gx = 3 + display.getTextWidth(sender) + 3;
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
drawAckGlyph(display, gx, y + 1, _history.dmEffectiveStatus(e), e.attempt + 1);
|
2026-06-14 23:33:16 +02:00
|
|
|
}
|
feat(ui): proportional scrollbar with mini-icon caps; reclaim row space
Replace the text ^/v scroll arrows with a font-scaling indicator drawn in
icons.h: a proportional thumb between fixed up/down triangle mini-icon caps.
drawScrollIndicator() lives in a ~5px right-edge column and scales via
miniIconScale (1x OLED, 2x landscape e-ink).
- Mini-icons: ICON_SCROLL_UP/DOWN; miniIconDrawTop (exact placement) and
miniIconDrawHalo (shape-hugging 1px DARK halo, clip_top to spare the header
separator) so caps stay visible on the LIGHT selection bar without a box.
- Thumb is 3*s wide so it centres on the triangle column; caps are static end
markers (always drawn) so neither vanishes at the extremes.
- scrollIndicatorReserve(): right-edge gutter (0 when the list fits). Content,
the selection bar and the message-history bubbles are all narrowed by it so
nothing renders under the scrollbar. Portrait e-ink wrap width subtracts the
reserve in both the box-height pass and render so wrapped text can't spill.
- Drop the redundant ">" selection marker (the highlight bar already shows
selection) and shift rows to x=2, reclaiming left-edge space.
- Wire the indicator into every scrollable list (Bot, Settings, Nearby, Tools,
Trail, QuickMsg lists + DM/channel history) and add one to WaypointsView,
which scrolled with no indicator before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:41:46 +02:00
|
|
|
if (age[0]) { display.setCursor(display.width() - age_w - reserve, y + 1); display.print(age); }
|
2026-05-27 23:47:30 +02:00
|
|
|
if (!sel) display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
if (portrait_expand) {
|
2026-06-22 18:31:47 +02:00
|
|
|
display.translateUTF8ToBlocks(s_wrap_trans, body, sizeof(s_wrap_trans));
|
2026-06-16 08:54:11 +02:00
|
|
|
int nl = FullscreenMsgView::wrapLines(display, s_wrap_trans, display.width() - 6 - reserve, s_wrap_lines, 8);
|
|
|
|
|
for (int li = 0; li < nl; li++) { display.setCursor(3, y + (li + 1) * lh + 1); display.print(s_wrap_lines[li]); }
|
2026-05-27 23:47:30 +02:00
|
|
|
} else {
|
2026-06-22 18:31:47 +02:00
|
|
|
display.drawTextEllipsized(3, y + lh + 1, display.width() - cw - 2, body);
|
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
|
|
|
}
|
|
|
|
|
|
2026-06-16 09:32:09 +02:00
|
|
|
// Scrollbar: track pinned to the full list area; thumb sized/positioned
|
|
|
|
|
// from hs's pixel metrics (stable while scrolling the same list).
|
|
|
|
|
if (hs.need)
|
|
|
|
|
drawScrollIndicatorPx(display, hist_start_y + 1, hs.view_px,
|
|
|
|
|
hs.total_px, hs.view_px, hs.scroll_px);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
2026-06-29 23:05:11 +02:00
|
|
|
drawComposeButton(display, cby, lh, _dm_hist_sel == -1);
|
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) {
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
int fs_hist_count = _history.histCountForChannel(_sel_channel_idx);
|
|
|
|
|
int ring_pos = _history.histEntryForChannel(_sel_channel_idx, _hist_sel);
|
2026-05-17 00:12:40 +02:00
|
|
|
if (ring_pos >= 0) {
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
const char* ftext = _history.chAtPos(ring_pos).text;
|
2026-05-17 00:12:40 +02:00
|
|
|
const char* fsep = strstr(ftext, ": ");
|
2026-06-04 12:10:26 +02:00
|
|
|
char fsender[33], fmsg[MSG_TEXT_BUF];
|
2026-05-17 00:12:40 +02:00
|
|
|
if (fsep) {
|
2026-06-04 12:10:26 +02:00
|
|
|
int nl = fsep - ftext; if (nl > (int)sizeof(fsender) - 1) nl = sizeof(fsender) - 1;
|
2026-05-17 00:12:40 +02:00
|
|
|
strncpy(fsender, ftext, nl); fsender[nl] = '\0';
|
|
|
|
|
strncpy(fmsg, fsep + 2, sizeof(fmsg) - 1); fmsg[sizeof(fmsg)-1] = '\0';
|
|
|
|
|
} else {
|
2026-05-31 00:11:43 +02:00
|
|
|
strcpy(fsender, "?");
|
2026-05-17 00:12:40 +02:00
|
|
|
strncpy(fmsg, ftext, sizeof(fmsg) - 1); fmsg[sizeof(fmsg)-1] = '\0';
|
|
|
|
|
}
|
2026-05-18 16:11:25 +02:00
|
|
|
int ret = _fs.render(display, fsender, fmsg,
|
|
|
|
|
_hist_sel < fs_hist_count - 1,
|
|
|
|
|
_hist_sel > 0);
|
2026-06-14 23:33:16 +02:00
|
|
|
// Channels: ✓ only once a repeater echo confirms relay (see list view).
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
if (strcmp(fsender, "Me") == 0 && _history.chAtPos(ring_pos).relay_status == ACK_OK) {
|
2026-06-14 23:33:16 +02:00
|
|
|
display.setColor(DisplayDriver::DARK);
|
|
|
|
|
drawAckGlyph(display, 2 + display.getTextWidth(fsender) + 3, 1, ACK_OK);
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
}
|
2026-05-18 16:11:25 +02:00
|
|
|
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);
|
feat(ui): discoverable context-menu hint — header glyph, highlight, corner dropdown
Hold-Enter context menus were invisible. Add a menu affordance to every screen
that has one:
- A small ≡ glyph in the header's top-right, via new DisplayDriver::
drawContextMenuHint() and an optional menu_hint arg on drawCenteredHeader() /
drawInvertedHeader() (reserves the corner so the title never runs under it).
- The glyph highlights (corner cell filled, bars knocked out) while the menu is
open, tying the hint to the popup it spawned — driven by a menu_open flag the
screens pass from their live popup state.
- The context menu now drops out of that corner: PopupMenu gains an opt-in
top-right anchor (begin(..., anchor_top_right)); it right-aligns under the
header instead of centring, so the menu reads as emerging from the ≡. Default
stays centred, so non-context popups (Tools/Settings/etc.) are untouched.
Enabled on Nodes (list/scan/detail) and Messages (mode select, contact/room and
channel pickers, DM and channel history).
Both solo envs build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 10:31:01 +02:00
|
|
|
display.drawCenteredHeader(title, true, _ctx_menu.active);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
int ch_hist_count = _history.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;
|
2026-06-16 09:32:09 +02:00
|
|
|
// Fixed-track scrollbar metrics + stable gutter reserve (see DM history above).
|
|
|
|
|
HistScroll hs = computeHistScroll(display, portrait_expand, ch_hist_count, _hist_scroll,
|
|
|
|
|
hist_start_y, cby, lh,
|
|
|
|
|
[&](int idx) -> const char* {
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
int rp = _history.histEntryForChannel(_sel_channel_idx, idx);
|
2026-06-16 09:32:09 +02:00
|
|
|
if (rp < 0) return nullptr;
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
const char* t = _history.chAtPos(rp).text;
|
2026-06-16 09:32:09 +02:00
|
|
|
const char* s = strstr(t, ": ");
|
|
|
|
|
return s ? s + 2 : t;
|
|
|
|
|
});
|
|
|
|
|
int reserve = hs.reserve;
|
2026-05-27 23:47:30 +02:00
|
|
|
{
|
|
|
|
|
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) {
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
int rp = _history.histEntryForChannel(_sel_channel_idx, _hist_scroll + ii);
|
2026-05-27 23:47:30 +02:00
|
|
|
if (rp >= 0) {
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
const char* rtext = _history.chAtPos(rp).text;
|
2026-05-27 23:47:30 +02:00
|
|
|
const char* rsep = strstr(rtext, ": ");
|
|
|
|
|
const char* rbody = rsep ? rsep + 2 : rtext;
|
2026-06-16 08:54:11 +02:00
|
|
|
display.translateUTF8ToBlocks(s_wrap_trans, skipReplyPrefix(rbody), sizeof(s_wrap_trans));
|
|
|
|
|
int nl = FullscreenMsgView::wrapLines(display, s_wrap_trans, display.width() - 6 - reserve, s_wrap_lines, 8);
|
2026-05-27 23:47:30 +02:00
|
|
|
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;
|
feat(repeater): politeness controls, dedicated radio profile, and diagnostics
Settings > Radio gains a repeater toggle, 16 community-suggested radio
presets plus manual Freq/SF/BW/CR tuning (digit-by-digit Freq editor),
4 persisted user preset slots, and the packet pool bumped 16->32 so
queued retransmits stop starving incoming-packet allocation while
relaying.
Four opt-in politeness knobs (skip-advert, max-hops, yield, min-SNR),
all flood-only and off by default, plus overhear suppression that
cancels a queued retransmit if a peer relays the same packet first.
Adaptive Power Control is suppressed while relaying (pins TX power to
the ceiling) and duty-cycle RX is forced off, since a repeater needs
to hear and relay at consistent power.
A dedicated Tools > Repeater screen consolidates the toggle, the
politeness knobs, and live forwarding stats, plus an optional "Custom"
radio profile — a dedicated frequency/SF/BW/CR for relaying, separate
from the companion's own network, band-matched to the companion
frequency by default and used everywhere a radio change can happen
(boot, on-device toggle, app-driven CMD_SET_RADIO_PARAMS, Settings
radio edits) so the device never silently falls back to the wrong
params mid-relay.
Diagnostics gains the actually-forwarded packet count, real heap-free
via mallinfo(), a reset-counters popup, and hardened loop-detect
bounds. Also: a frequency-floor fix and a float-equality preset-match
fix in the repeater profile logic, deduped BW-table/valCol/profile-
seeding helpers shared between Settings and the Repeater screen, and
a build.sh fix tolerating control characters in `pio project config`'s
JSON dump.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 09:07:00 +02:00
|
|
|
updateChannelUnread(); // mark everything in the just-computed visible window as seen
|
2026-05-27 23:47:30 +02:00
|
|
|
|
|
|
|
|
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
|
|
|
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
int ring_pos = _history.histEntryForChannel(_sel_channel_idx, item);
|
2026-05-17 00:12:40 +02:00
|
|
|
if (ring_pos < 0) continue;
|
|
|
|
|
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
const char* text = _history.chAtPos(ring_pos).text;
|
2026-05-17 00:12:40 +02:00
|
|
|
const char* sep = strstr(text, ": ");
|
2026-06-04 12:10:26 +02:00
|
|
|
char sender[33], msg_part[MSG_TEXT_BUF];
|
2026-05-17 00:12:40 +02:00
|
|
|
if (sep) {
|
2026-06-04 12:10:26 +02:00
|
|
|
int nl = sep - text; if (nl > (int)sizeof(sender) - 1) nl = sizeof(sender) - 1;
|
2026-05-17 00:12:40 +02:00
|
|
|
strncpy(sender, text, nl); sender[nl] = '\0';
|
|
|
|
|
strncpy(msg_part, sep + 2, sizeof(msg_part) - 1);
|
|
|
|
|
msg_part[sizeof(msg_part) - 1] = '\0';
|
|
|
|
|
} else {
|
2026-05-31 00:11:43 +02:00
|
|
|
strcpy(sender, "?");
|
2026-05-17 00:12:40 +02:00
|
|
|
strncpy(msg_part, text, sizeof(msg_part) - 1);
|
|
|
|
|
msg_part[sizeof(msg_part) - 1] = '\0';
|
|
|
|
|
}
|
|
|
|
|
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
char age[6]; geo::fmtAgeShort(age, sizeof(age), now_ts, _history.chAtPos(ring_pos).timestamp);
|
2026-05-19 20:21:32 +02:00
|
|
|
int age_w = age[0] ? display.getTextWidth(age) + 3 : 0;
|
|
|
|
|
|
2026-06-29 23:05:11 +02:00
|
|
|
drawHistRowFrame(display, y, bh, reserve, lh, sel);
|
feat(ui): proportional scrollbar with mini-icon caps; reclaim row space
Replace the text ^/v scroll arrows with a font-scaling indicator drawn in
icons.h: a proportional thumb between fixed up/down triangle mini-icon caps.
drawScrollIndicator() lives in a ~5px right-edge column and scales via
miniIconScale (1x OLED, 2x landscape e-ink).
- Mini-icons: ICON_SCROLL_UP/DOWN; miniIconDrawTop (exact placement) and
miniIconDrawHalo (shape-hugging 1px DARK halo, clip_top to spare the header
separator) so caps stay visible on the LIGHT selection bar without a box.
- Thumb is 3*s wide so it centres on the triangle column; caps are static end
markers (always drawn) so neither vanishes at the extremes.
- scrollIndicatorReserve(): right-edge gutter (0 when the list fits). Content,
the selection bar and the message-history bubbles are all narrowed by it so
nothing renders under the scrollbar. Portrait e-ink wrap width subtracts the
reserve in both the box-height pass and render so wrapped text can't spill.
- Drop the redundant ">" selection marker (the highlight bar already shows
selection) and shift rows to x=2, reclaiming left-edge space.
- Wire the indicator into every scrollable list (Bot, Settings, Nearby, Tools,
Trail, QuickMsg lists + DM/channel history) and add one to WaypointsView,
which scrolled with no indicator before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:41:46 +02:00
|
|
|
display.drawTextEllipsized(3, y + 1, display.width() - cw - 2 - age_w - reserve, sender);
|
2026-06-14 23:33:16 +02:00
|
|
|
// Channels have no recipient ACK — only show ✓ once a repeater echo
|
|
|
|
|
// confirms the send was relayed into the mesh; otherwise no marker
|
|
|
|
|
// (absence is normal, not a failure).
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
if (strcmp(sender, "Me") == 0 && _history.chAtPos(ring_pos).relay_status == ACK_OK) {
|
2026-06-14 23:33:16 +02:00
|
|
|
int gx = 3 + display.getTextWidth(sender) + 3;
|
|
|
|
|
drawAckGlyph(display, gx, y + 1, ACK_OK);
|
|
|
|
|
}
|
feat(ui): proportional scrollbar with mini-icon caps; reclaim row space
Replace the text ^/v scroll arrows with a font-scaling indicator drawn in
icons.h: a proportional thumb between fixed up/down triangle mini-icon caps.
drawScrollIndicator() lives in a ~5px right-edge column and scales via
miniIconScale (1x OLED, 2x landscape e-ink).
- Mini-icons: ICON_SCROLL_UP/DOWN; miniIconDrawTop (exact placement) and
miniIconDrawHalo (shape-hugging 1px DARK halo, clip_top to spare the header
separator) so caps stay visible on the LIGHT selection bar without a box.
- Thumb is 3*s wide so it centres on the triangle column; caps are static end
markers (always drawn) so neither vanishes at the extremes.
- scrollIndicatorReserve(): right-edge gutter (0 when the list fits). Content,
the selection bar and the message-history bubbles are all narrowed by it so
nothing renders under the scrollbar. Portrait e-ink wrap width subtracts the
reserve in both the box-height pass and render so wrapped text can't spill.
- Drop the redundant ">" selection marker (the highlight bar already shows
selection) and shift rows to x=2, reclaiming left-edge space.
- Wire the indicator into every scrollable list (Bot, Settings, Nearby, Tools,
Trail, QuickMsg lists + DM/channel history) and add one to WaypointsView,
which scrolled with no indicator before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:41:46 +02:00
|
|
|
if (age[0]) { display.setCursor(display.width() - age_w - reserve, y + 1); display.print(age); }
|
2026-05-27 23:47:30 +02:00
|
|
|
if (!sel) display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
if (portrait_expand) {
|
2026-06-16 08:54:11 +02:00
|
|
|
display.translateUTF8ToBlocks(s_wrap_trans, skipReplyPrefix(msg_part), sizeof(s_wrap_trans));
|
|
|
|
|
int nl = FullscreenMsgView::wrapLines(display, s_wrap_trans, display.width() - 6 - reserve, s_wrap_lines, 8);
|
|
|
|
|
for (int li = 0; li < nl; li++) { display.setCursor(3, y + (li + 1) * lh + 1); display.print(s_wrap_lines[li]); }
|
2026-05-27 23:47:30 +02:00
|
|
|
} 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
|
|
|
}
|
|
|
|
|
|
2026-06-16 09:32:09 +02:00
|
|
|
// Scrollbar: track pinned to the full list area; thumb from hs metrics.
|
|
|
|
|
if (hs.need)
|
|
|
|
|
drawScrollIndicatorPx(display, hist_start_y + 1, hs.view_px,
|
|
|
|
|
hs.total_px, hs.view_px, hs.scroll_px);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
2026-06-29 23:05:11 +02:00
|
|
|
drawComposeButton(display, cby, lh, _hist_sel == -1);
|
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) {
|
2026-06-08 20:42:18 +02:00
|
|
|
return _kb->render(display);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
} 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);
|
|
|
|
|
}
|
refactor(ui): shared drawList + header + key-decode helpers
- drawList(): scrollable list skeleton (visible window, keep-sel-in-view,
scroll-column reserve, per-row callback, indicator). Migrate Tools, Bot,
Nearby, Waypoints list and the three QuickMsg lists onto it; centralises the
scroll-clamp/reserve/indicator that each had open-coded (and inconsistently).
- drawCenteredHeader(): centred title + separator, replacing the duplicated
two-liner across ~10 screens (incl. QuickMsg, which used width()/2 spacing and
was missed before); unifies the separator at headerH()-sepH().
- keyIsPrev()/keyIsNext() in UIScreen.h for the LEFT||PREV / RIGHT||NEXT idiom;
applied to 6 screens, behaviour-preserving. Cancel/context-menu stay
per-screen (they mean different things on different screens).
- rotLabel() dedups the 0/90/180/270 array in Settings.
- Unify selection-row height to lineStep()-1 (Tools/Bot matched the rest).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:23:00 +02:00
|
|
|
display.drawCenteredHeader(title);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
int total_msg_items = 1 + _active_msg_count;
|
2026-06-16 08:54:11 +02:00
|
|
|
drawList(display, total_msg_items, _msg_sel, _msg_scroll, [&](int idx, int y, bool sel, int reserve) {
|
2026-06-29 18:53:18 +02:00
|
|
|
drawRowSelection(display, y, sel, reserve);
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
if (idx == 0) {
|
feat(ui): proportional scrollbar with mini-icon caps; reclaim row space
Replace the text ^/v scroll arrows with a font-scaling indicator drawn in
icons.h: a proportional thumb between fixed up/down triangle mini-icon caps.
drawScrollIndicator() lives in a ~5px right-edge column and scales via
miniIconScale (1x OLED, 2x landscape e-ink).
- Mini-icons: ICON_SCROLL_UP/DOWN; miniIconDrawTop (exact placement) and
miniIconDrawHalo (shape-hugging 1px DARK halo, clip_top to spare the header
separator) so caps stay visible on the LIGHT selection bar without a box.
- Thumb is 3*s wide so it centres on the triangle column; caps are static end
markers (always drawn) so neither vanishes at the extremes.
- scrollIndicatorReserve(): right-edge gutter (0 when the list fits). Content,
the selection bar and the message-history bubbles are all narrowed by it so
nothing renders under the scrollbar. Portrait e-ink wrap width subtracts the
reserve in both the box-height pass and render so wrapped text can't spill.
- Drop the redundant ">" selection marker (the highlight bar already shows
selection) and shift rows to x=2, reclaiming left-edge space.
- Wire the indicator into every scrollable list (Bot, Settings, Nearby, Tools,
Trail, QuickMsg lists + DM/channel history) and add one to WaypointsView,
which scrolled with no indicator before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:41:46 +02:00
|
|
|
display.setCursor(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] : "";
|
feat(ui): proportional scrollbar with mini-icon caps; reclaim row space
Replace the text ^/v scroll arrows with a font-scaling indicator drawn in
icons.h: a proportional thumb between fixed up/down triangle mini-icon caps.
drawScrollIndicator() lives in a ~5px right-edge column and scales via
miniIconScale (1x OLED, 2x landscape e-ink).
- Mini-icons: ICON_SCROLL_UP/DOWN; miniIconDrawTop (exact placement) and
miniIconDrawHalo (shape-hugging 1px DARK halo, clip_top to spare the header
separator) so caps stay visible on the LIGHT selection bar without a box.
- Thumb is 3*s wide so it centres on the triangle column; caps are static end
markers (always drawn) so neither vanishes at the extremes.
- scrollIndicatorReserve(): right-edge gutter (0 when the list fits). Content,
the selection bar and the message-history bubbles are all narrowed by it so
nothing renders under the scrollbar. Portrait e-ink wrap width subtracts the
reserve in both the box-height pass and render so wrapped text can't spill.
- Drop the redundant ">" selection marker (the highlight bar already shows
selection) and shift rows to x=2, reclaiming left-edge space.
- Wire the indicator into every scrollable list (Bot, Settings, Nearby, Tools,
Trail, QuickMsg lists + DM/channel history) and add one to WaypointsView,
which scrolled with no indicator before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:41:46 +02:00
|
|
|
display.drawTextEllipsized(2, y, display.width() - 4 - reserve, tmpl);
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
refactor(ui): shared drawList + header + key-decode helpers
- drawList(): scrollable list skeleton (visible window, keep-sel-in-view,
scroll-column reserve, per-row callback, indicator). Migrate Tools, Bot,
Nearby, Waypoints list and the three QuickMsg lists onto it; centralises the
scroll-clamp/reserve/indicator that each had open-coded (and inconsistently).
- drawCenteredHeader(): centred title + separator, replacing the duplicated
two-liner across ~10 screens (incl. QuickMsg, which used width()/2 spacing and
was missed before); unifies the separator at headerH()-sepH().
- keyIsPrev()/keyIsNext() in UIScreen.h for the LEFT||PREV / RIGHT||NEXT idiom;
applied to 6 screens, behaviour-preserving. Cancel/context-menu stay
per-screen (they mean different things on different screens).
- rotLabel() dedups the 0/90/180/270 array in Settings.
- Unify selection-row height to lineStep()-1 (Tools/Bot matched the rest).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:23:00 +02:00
|
|
|
});
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
return 2000;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool handleInput(char c) override {
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
// Channel Add/Edit form consumes all input while active.
|
|
|
|
|
if (_ch_view.active()) return _ch_view.handleInput(c);
|
|
|
|
|
|
feat(nav): navigate to / save a location shared in a message
Locations already travel as plain text ({loc} expands to "lat,lon"), so any
message carrying coordinates can now be acted on. In the fullscreen message
view, the context-menu (Hold Enter) gains Navigate and Save waypoint
entries whenever the message contains a coordinate — alongside Reply.
- geo::parseLatLon: shared scanner for an embedded "lat,lon" (the {loc}
format), with an optional [WAY] tag that supplies a label. Range-checked,
requires a decimal point to avoid matching plain integer pairs. Will also
back the waypoint-share feature later.
- QuickMsgScreen: Navigate opens NavView inline (To/Hdg/distance, honouring
the global Units setting) over the message, back returns to it; Save
waypoint stores the point (label from [WAY], else auto-named). Works in
both DM and channel histories, and on incoming or outgoing messages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:28:17 +02:00
|
|
|
// Navigate view: any back key returns to the message it was opened from.
|
|
|
|
|
if (_nav_active) {
|
|
|
|
|
if (c == KEY_CANCEL || c == KEY_ENTER || c == KEY_CONTEXT_MENU) _nav_active = false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
if (_phase == MODE_SELECT) {
|
2026-05-24 22:43:31 +02:00
|
|
|
// Context menu (Mark-all-read) takes precedence while active.
|
|
|
|
|
if (_ctx_menu.active) {
|
|
|
|
|
auto res = _ctx_menu.handleInput(c);
|
|
|
|
|
if (res == PopupMenu::SELECTED) {
|
2026-05-24 22:46:54 +02:00
|
|
|
int cleared = 0;
|
2026-05-24 22:43:31 +02:00
|
|
|
if (_mode_sel == 0) {
|
2026-05-24 22:46:54 +02:00
|
|
|
cleared = getDMUnreadTotal();
|
2026-05-24 22:43:31 +02:00
|
|
|
_task->clearAllDMUnread();
|
|
|
|
|
} else if (_mode_sel == 1) {
|
2026-05-24 22:46:54 +02:00
|
|
|
cleared = _task->getChannelUnreadCount();
|
2026-05-24 22:43:31 +02:00
|
|
|
clearAllChannelUnread();
|
|
|
|
|
} else {
|
2026-05-24 22:46:54 +02:00
|
|
|
cleared = _task->getRoomUnreadCount();
|
2026-05-24 22:43:31 +02:00
|
|
|
_task->clearRoomUnread();
|
|
|
|
|
}
|
2026-06-09 07:45:40 +02:00
|
|
|
markReadAlert(cleared);
|
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; }
|
2026-06-18 09:16:57 +02:00
|
|
|
if (c == KEY_UP) { _mode_sel = (_mode_sel > 0) ? _mode_sel - 1 : 2; return true; }
|
|
|
|
|
if (c == KEY_DOWN) { _mode_sel = (_mode_sel < 2) ? _mode_sel + 1 : 0; 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" };
|
2026-07-10 16:01:14 +02:00
|
|
|
_ctx_menu.begin(MODE_TITLES[_mode_sel < 3 ? _mode_sel : 0], 1);
|
2026-05-24 22:43:31 +02:00
|
|
|
_ctx_menu.addItem("Mark all read");
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
if (c == KEY_ENTER) {
|
|
|
|
|
if (_mode_sel == 1) {
|
|
|
|
|
_phase = CHANNEL_PICK;
|
|
|
|
|
} else {
|
|
|
|
|
_room_mode = (_mode_sel == 2);
|
|
|
|
|
if (_room_mode) _task->clearRoomUnread();
|
|
|
|
|
buildContactList();
|
|
|
|
|
_contact_sel = _contact_scroll = 0;
|
|
|
|
|
_phase = CONTACT_PICK;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else if (_phase == CONTACT_PICK) {
|
|
|
|
|
// Context menu consumes all input while open
|
|
|
|
|
if (_ctx_menu.active) {
|
2026-06-26 10:55:24 +02:00
|
|
|
if (_room_mode) {
|
|
|
|
|
auto res = _ctx_menu.handleInput(c);
|
|
|
|
|
if (res == PopupMenu::SELECTED && _num_contacts > 0) {
|
|
|
|
|
if (the_mesh.getContactByIdx(_sorted[_contact_sel], _sel_contact)) {
|
2026-07-10 16:01:14 +02:00
|
|
|
int sel = _ctx_menu.selectedIndex();
|
|
|
|
|
if (sel == 0) {
|
|
|
|
|
_login_mode = true;
|
|
|
|
|
_kb->begin("", 15); // room/repeater password: max 15 chars
|
|
|
|
|
_kb->clearPlaceholders(); // {loc}/{time} are for messages, not a password
|
|
|
|
|
_phase = KEYBOARD;
|
|
|
|
|
} else {
|
|
|
|
|
// Logout: only reachable when isRoomLoggedIn() added this item.
|
|
|
|
|
the_mesh.logoutRoom(_sel_contact.id.pub_key);
|
|
|
|
|
forgetRoomLoggedIn(_sel_contact.id.pub_key);
|
|
|
|
|
_task->showAlert("Logged out", 1000);
|
|
|
|
|
}
|
2026-06-26 10:55:24 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-05-29 00:25:02 +02:00
|
|
|
// LEFT/RIGHT cycle Notif/Melody in-place (menu stays open).
|
|
|
|
|
if (!_pin_picker_active && _num_contacts > 0) {
|
2026-06-29 18:01:29 +02:00
|
|
|
bool left = keyIsPrev(c);
|
|
|
|
|
bool right = keyIsNext(c);
|
2026-05-29 00:25:02 +02:00
|
|
|
if (left || right) {
|
|
|
|
|
static const char* NOTIF_LABELS[] = { "default", "OFF", "ON" };
|
|
|
|
|
static const char* ML[] = { "global", "M1", "M2" };
|
|
|
|
|
ContactInfo ci;
|
|
|
|
|
if (the_mesh.getContactByIdx(_sorted[_contact_sel], ci)) {
|
|
|
|
|
int sel = _ctx_menu.selectedIndex();
|
|
|
|
|
if (sel == 1) {
|
|
|
|
|
uint8_t v = dmNotifState(ci.id.pub_key);
|
|
|
|
|
v = right ? (v + 1) % 3 : (v + 2) % 3;
|
|
|
|
|
setDmNotifState(ci.id.pub_key, v);
|
|
|
|
|
snprintf(_ctx_notif_item, sizeof(_ctx_notif_item), "Notif: %s", NOTIF_LABELS[v]);
|
|
|
|
|
_ctx_dirty = true;
|
|
|
|
|
} else if (sel == 2) {
|
|
|
|
|
uint8_t v = dmMelodySlot(ci.id.pub_key);
|
|
|
|
|
v = right ? (v + 1) % 3 : (v + 2) % 3;
|
|
|
|
|
setDmMelody(ci.id.pub_key, v);
|
|
|
|
|
snprintf(_ctx_melody_item, sizeof(_ctx_melody_item), "Melody: %s", ML[v]);
|
|
|
|
|
_ctx_dirty = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
auto res = _ctx_menu.handleInput(c);
|
2026-05-24 23:30:16 +02:00
|
|
|
if (_pin_picker_active) {
|
|
|
|
|
// Slot picker sub-menu: index 0..FAVOURITES_COUNT-1 maps directly to slot.
|
|
|
|
|
if (res == PopupMenu::SELECTED && _num_contacts > 0) {
|
|
|
|
|
ContactInfo ci;
|
|
|
|
|
if (the_mesh.getContactByIdx(_sorted[_contact_sel], ci)) {
|
|
|
|
|
int slot = _ctx_menu.selectedIndex();
|
|
|
|
|
_task->setFavouriteSlot(slot, ci.id.pub_key);
|
|
|
|
|
the_mesh.savePrefs();
|
|
|
|
|
char alert[24];
|
|
|
|
|
snprintf(alert, sizeof(alert), "Pinned to slot %d", slot + 1);
|
|
|
|
|
_task->showAlert(alert, 800);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (res != PopupMenu::NONE) _pin_picker_active = false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
if (res == PopupMenu::SELECTED && _num_contacts > 0) {
|
|
|
|
|
ContactInfo ci;
|
|
|
|
|
if (the_mesh.getContactByIdx(_sorted[_contact_sel], ci)) {
|
2026-05-29 00:25:02 +02:00
|
|
|
int sel = _ctx_menu.selectedIndex();
|
|
|
|
|
if (sel == 0) {
|
2026-06-09 07:43:51 +02:00
|
|
|
int cleared = (int)_task->getDMUnread(ci.id.pub_key);
|
2026-05-17 00:12:40 +02:00
|
|
|
_task->clearDMUnread(ci.id.pub_key);
|
2026-06-09 07:45:40 +02:00
|
|
|
markReadAlert(cleared);
|
2026-05-29 00:25:02 +02:00
|
|
|
} else if (sel == 3) {
|
2026-05-24 23:30:16 +02:00
|
|
|
// Pin / Unpin
|
|
|
|
|
int pinned_slot = _task->findFavouriteSlot(ci.id.pub_key);
|
|
|
|
|
if (pinned_slot >= 0) {
|
|
|
|
|
_task->clearFavouriteSlot(pinned_slot);
|
|
|
|
|
the_mesh.savePrefs();
|
|
|
|
|
char alert[24];
|
|
|
|
|
snprintf(alert, sizeof(alert), "Unpinned (slot %d)", pinned_slot + 1);
|
|
|
|
|
_task->showAlert(alert, 800);
|
|
|
|
|
} else {
|
|
|
|
|
for (int s = 0; s < NodePrefs::FAVOURITES_COUNT; s++) {
|
|
|
|
|
if (_task->isFavouriteSlotEmpty(s)) {
|
|
|
|
|
snprintf(_pin_slot_labels[s], sizeof(_pin_slot_labels[s]), "Slot %d: empty", s + 1);
|
|
|
|
|
} else {
|
|
|
|
|
char nm[16] = "?";
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
if (p) {
|
|
|
|
|
const uint8_t* pfx = p->favourite_contacts[s];
|
|
|
|
|
for (int idx = 0; ; idx++) {
|
|
|
|
|
ContactInfo c2;
|
|
|
|
|
if (!the_mesh.getContactByIdx(idx, c2)) break;
|
|
|
|
|
if (memcmp(c2.id.pub_key, pfx, NodePrefs::FAVOURITE_PREFIX_LEN) == 0) {
|
|
|
|
|
DisplayDriver::translateUTF8Static(nm, c2.name, sizeof(nm));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
snprintf(_pin_slot_labels[s], sizeof(_pin_slot_labels[s]), "Slot %d: %s", s + 1, nm);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-10 16:01:14 +02:00
|
|
|
_ctx_menu.begin("Pick slot", 3);
|
2026-05-24 23:30:16 +02:00
|
|
|
for (int s = 0; s < NodePrefs::FAVOURITES_COUNT; s++) _ctx_menu.addItem(_pin_slot_labels[s]);
|
|
|
|
|
_pin_picker_active = true;
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
2026-05-29 00:25:02 +02:00
|
|
|
// sel == 1 (Notif) and sel == 2 (Melody): already cycled via LEFT/RIGHT; ENTER just closes.
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-06-29 18:47:51 +02:00
|
|
|
if (res != PopupMenu::NONE) _task->savePrefsIfDirty(_ctx_dirty);
|
2026-05-17 00:12:40 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
feat(bot): room-server target, tab-carousel UI, per-target independence
Auto-Reply Bot gains a third target (room servers, alongside DM and
channel), new {name}/{hops} reply placeholders, and a DM all/favourites
allow-list. BotScreen is redesigned as a circular tab carousel (Direct /
Channel / Room / Other, same interaction as Nearby Nodes' filter tabs)
instead of one long scrolling list, which also exposed and fixed a
leftover coupling where Channel/Room trigger-replies and their !command
handling secretly depended on the DM tab's Enable/Commands toggles —
each target's Enable and Commands are now fully independent.
- NodePrefs: bot_room_enabled/prefix/trigger/reply, bot_dm_scope,
bot_commands_ch/bot_commands_room (bot_commands_enabled repurposed as
DM-only). SCHEMA_SENTINEL 0xC0DE001D -> 0xC0DE001F; sizeof unchanged
at 2712 (new bytes absorbed existing padding, verified via a
standalone host compile + offsetof check).
- DataStore: persists all new fields; seeds bot_commands_ch/room from
the old shared bot_commands_enabled on upgrade so existing
channel/room command behaviour isn't silently lost.
- MyMeshBot: tryBotReplyRoom/tryBotRoomCommand mirror the channel bot's
shape but post via sendMessage (room relays to members itself);
requires an existing login session with that room, same as a manual
post would. botDmSenderAllowed() gates DM trigger-reply/commands on
the favourites bit when bot_dm_scope=Fav.
- MsgExpand: {name}/{hops} as optional trailing params (default
nullptr/-1, no existing caller affected) — deliberately not exposed
on the general compose keyboard, only on bot Reply fields.
- QuickMsgScreen/UITask: room-target picker (mirrors the channel
picker), routing through the existing room-login prompt when there's
no saved password yet.
- BotScreen: tab carousel (LEFT/RIGHT switches tabs, UP/DOWN moves
rows, Enter is now the only way to change a value); Enable split out
of Channel/Room's combo row; Quiet Hours gained a stepper sub-mode;
Commands moved from a shared toggle into each tab.
- docs/tools_screen.md updated for the new tab layout and behaviour.
Not build-verified — no PlatformIO toolchain in this environment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 20:16:59 +02:00
|
|
|
if (c == KEY_CANCEL) {
|
|
|
|
|
if (_pick_bot_room) { _pick_bot_room = false; _room_mode = false; _task->gotoBotScreen(); return true; }
|
|
|
|
|
_room_mode = false;
|
|
|
|
|
_phase = MODE_SELECT;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-06-16 08:54:11 +02:00
|
|
|
// drawList() reclamps _contact_scroll from _contact_sel every render.
|
2026-06-18 09:16:57 +02:00
|
|
|
if (c == KEY_UP && _num_contacts > 0) { _contact_sel = (_contact_sel > 0) ? _contact_sel - 1 : _num_contacts - 1; return true; }
|
|
|
|
|
if (c == KEY_DOWN && _num_contacts > 0) { _contact_sel = (_contact_sel < _num_contacts - 1) ? _contact_sel + 1 : 0; return true; }
|
2026-05-17 00:12:40 +02:00
|
|
|
if (c == KEY_ENTER && _num_contacts > 0) {
|
|
|
|
|
if (the_mesh.getContactByIdx(_sorted[_contact_sel], _sel_contact)) {
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
if (_pick_target) { commitPickTargetDM(_sel_contact); return true; }
|
feat(bot): room-server target, tab-carousel UI, per-target independence
Auto-Reply Bot gains a third target (room servers, alongside DM and
channel), new {name}/{hops} reply placeholders, and a DM all/favourites
allow-list. BotScreen is redesigned as a circular tab carousel (Direct /
Channel / Room / Other, same interaction as Nearby Nodes' filter tabs)
instead of one long scrolling list, which also exposed and fixed a
leftover coupling where Channel/Room trigger-replies and their !command
handling secretly depended on the DM tab's Enable/Commands toggles —
each target's Enable and Commands are now fully independent.
- NodePrefs: bot_room_enabled/prefix/trigger/reply, bot_dm_scope,
bot_commands_ch/bot_commands_room (bot_commands_enabled repurposed as
DM-only). SCHEMA_SENTINEL 0xC0DE001D -> 0xC0DE001F; sizeof unchanged
at 2712 (new bytes absorbed existing padding, verified via a
standalone host compile + offsetof check).
- DataStore: persists all new fields; seeds bot_commands_ch/room from
the old shared bot_commands_enabled on upgrade so existing
channel/room command behaviour isn't silently lost.
- MyMeshBot: tryBotReplyRoom/tryBotRoomCommand mirror the channel bot's
shape but post via sendMessage (room relays to members itself);
requires an existing login session with that room, same as a manual
post would. botDmSenderAllowed() gates DM trigger-reply/commands on
the favourites bit when bot_dm_scope=Fav.
- MsgExpand: {name}/{hops} as optional trailing params (default
nullptr/-1, no existing caller affected) — deliberately not exposed
on the general compose keyboard, only on bot Reply fields.
- QuickMsgScreen/UITask: room-target picker (mirrors the channel
picker), routing through the existing room-login prompt when there's
no saved password yet.
- BotScreen: tab carousel (LEFT/RIGHT switches tabs, UP/DOWN moves
rows, Enter is now the only way to change a value); Enable split out
of Channel/Room's combo row; Quiet Hours gained a stepper sub-mode;
Commands moved from a shared toggle into each tab.
- docs/tools_screen.md updated for the new tab layout and behaviour.
Not build-verified — no PlatformIO toolchain in this environment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 20:16:59 +02:00
|
|
|
if (_pick_bot_room) {
|
|
|
|
|
if (!isRoomLoggedIn(_sel_contact.id.pub_key)) {
|
|
|
|
|
char saved_pw[sizeof(_login_pw)];
|
|
|
|
|
if (the_mesh.getRoomPassword(_sel_contact.id.pub_key, saved_pw, sizeof(saved_pw))) {
|
|
|
|
|
// Known password, just not re-established this boot — retry
|
|
|
|
|
// silently in the background; the bot target is set below
|
|
|
|
|
// regardless of this attempt's outcome (self-heals like any
|
|
|
|
|
// other saved room password would on the next real open).
|
|
|
|
|
startRoomLogin(saved_pw);
|
|
|
|
|
} else {
|
|
|
|
|
// Never logged in — the bot could never post here without a
|
|
|
|
|
// password, so prompt for one now instead of picking an
|
|
|
|
|
// unusable target. Commits once submitted (see the KEYBOARD/
|
|
|
|
|
// _login_mode handler), whether or not login itself succeeds.
|
|
|
|
|
_login_mode = true;
|
|
|
|
|
_kb->begin("", 15); // room/repeater password: max 15 chars
|
|
|
|
|
_kb->clearPlaceholders();
|
|
|
|
|
_phase = KEYBOARD;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
commitPickBotRoom(_sel_contact);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-06-26 10:55:24 +02:00
|
|
|
if (_room_mode && !isRoomLoggedIn(_sel_contact.id.pub_key)) {
|
|
|
|
|
// Posting to a room requires a login handshake first (even with a
|
|
|
|
|
// blank password) — go straight to the password prompt instead of
|
|
|
|
|
// a history view that would silently fail to send.
|
|
|
|
|
char saved_pw[sizeof(_login_pw)];
|
|
|
|
|
if (the_mesh.getRoomPassword(_sel_contact.id.pub_key, saved_pw, sizeof(saved_pw))) {
|
|
|
|
|
// Logged in to this room before, on an earlier boot -- retry
|
|
|
|
|
// with the remembered password instead of prompting again.
|
|
|
|
|
startRoomLogin(saved_pw);
|
|
|
|
|
} else {
|
|
|
|
|
_login_mode = true;
|
|
|
|
|
_kb->begin("", 15); // room/repeater password: max 15 chars
|
2026-07-06 20:39:11 +02:00
|
|
|
_kb->clearPlaceholders(); // {loc}/{time} are for messages, not a password
|
2026-06-26 10:55:24 +02:00
|
|
|
_phase = KEYBOARD;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-07-06 20:42:05 +02:00
|
|
|
openDmHistory();
|
2026-06-04 11:38:12 +02:00
|
|
|
if (_share_mode) beginShareCompose(false);
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-06-26 10:55:24 +02:00
|
|
|
if (c == KEY_CONTEXT_MENU && _num_contacts > 0 && _room_mode) {
|
2026-07-10 16:01:14 +02:00
|
|
|
ContactInfo ci;
|
|
|
|
|
bool logged_in = the_mesh.getContactByIdx(_sorted[_contact_sel], ci) && isRoomLoggedIn(ci.id.pub_key);
|
|
|
|
|
_ctx_menu.begin("Room options", logged_in ? 2 : 1);
|
2026-06-26 10:55:24 +02:00
|
|
|
_ctx_menu.addItem("Login...");
|
2026-07-10 16:01:14 +02:00
|
|
|
if (logged_in) _ctx_menu.addItem("Logout");
|
2026-06-26 10:55:24 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
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-07-10 16:01:14 +02:00
|
|
|
_ctx_menu.begin("Contact options", 3);
|
2026-05-17 00:12:40 +02:00
|
|
|
_ctx_menu.addItem("Mark as read");
|
|
|
|
|
_ctx_menu.addItem(_ctx_notif_item);
|
|
|
|
|
_ctx_menu.addItem(_ctx_melody_item);
|
2026-05-24 23:30:16 +02:00
|
|
|
_ctx_menu.addItem(_ctx_pin_item);
|
2026-05-17 00:12:40 +02:00
|
|
|
_ctx_dirty = false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else if (_phase == CHANNEL_PICK) {
|
|
|
|
|
// Context menu consumes all input while open
|
|
|
|
|
if (_ctx_menu.active) {
|
2026-05-29 00:25:02 +02:00
|
|
|
// LEFT/RIGHT cycle Notif/Melody/Fav in-place (menu stays open).
|
|
|
|
|
if (_num_channels > 0) {
|
2026-06-29 18:01:29 +02:00
|
|
|
bool left = keyIsPrev(c);
|
|
|
|
|
bool right = keyIsNext(c);
|
2026-05-29 00:25:02 +02:00
|
|
|
if (left || right) {
|
|
|
|
|
static const char* NOTIF_LABELS[] = { "default", "OFF", "ON" };
|
|
|
|
|
static const char* ML[] = { "global", "M1", "M2" };
|
2026-07-05 16:08:44 +02:00
|
|
|
uint8_t ch_idx = _ctx_ch_idx; // frozen at menu open — see declaration
|
2026-05-29 00:25:02 +02:00
|
|
|
int sel = _ctx_menu.selectedIndex();
|
|
|
|
|
if (sel == 1) {
|
|
|
|
|
uint8_t v = chNotifState(ch_idx);
|
|
|
|
|
v = right ? (v + 1) % 3 : (v + 2) % 3;
|
|
|
|
|
setChNotifState(ch_idx, v);
|
|
|
|
|
snprintf(_ctx_notif_item, sizeof(_ctx_notif_item), "Notif: %s", NOTIF_LABELS[v]);
|
|
|
|
|
_ctx_dirty = true;
|
|
|
|
|
} else if (sel == 2) {
|
|
|
|
|
uint8_t v = chNotifMelody(ch_idx);
|
|
|
|
|
v = right ? (v + 1) % 3 : (v + 2) % 3;
|
|
|
|
|
setChNotifMelody(ch_idx, v);
|
|
|
|
|
snprintf(_ctx_melody_item, sizeof(_ctx_melody_item), "Melody: %s", ML[v]);
|
|
|
|
|
_ctx_dirty = true;
|
|
|
|
|
} else if (sel == 3) {
|
|
|
|
|
NodePrefs* p2 = _task->getNodePrefs();
|
|
|
|
|
if (p2) {
|
|
|
|
|
p2->ch_fav_bitmask ^= (1ULL << ch_idx);
|
|
|
|
|
bool is_fav = (p2->ch_fav_bitmask & (1ULL << ch_idx));
|
|
|
|
|
snprintf(_ctx_ch_fav_item, sizeof(_ctx_ch_fav_item), is_fav ? "Fav: yes" : "Fav: no");
|
|
|
|
|
_ctx_dirty = true;
|
2026-07-05 16:08:44 +02:00
|
|
|
// List rebuild is deferred to menu close: with the fav-only
|
|
|
|
|
// filter on, un-favouriting this channel removes it from the
|
|
|
|
|
// list, and rebuilding under the open menu would shift
|
|
|
|
|
// _channel_sel onto a different channel mid-interaction.
|
2026-05-29 00:25:02 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
auto res = _ctx_menu.handleInput(c);
|
|
|
|
|
if (res == PopupMenu::SELECTED && _num_channels > 0) {
|
2026-07-05 16:08:44 +02:00
|
|
|
uint8_t ch_idx = _ctx_ch_idx; // frozen at menu open — see declaration
|
2026-05-29 00:25:02 +02:00
|
|
|
int sel = _ctx_menu.selectedIndex();
|
|
|
|
|
if (sel == 0) {
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
int cleared = (int)_history.chUnread(ch_idx);
|
|
|
|
|
_history.setChUnread(ch_idx, 0);
|
2026-06-09 07:45:40 +02:00
|
|
|
markReadAlert(cleared);
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
} else if (sel == 4) { // Edit
|
|
|
|
|
ChannelDetails ch;
|
|
|
|
|
if (the_mesh.getChannel(ch_idx, ch)) _ch_view.openEdit(ch_idx, ch.name);
|
|
|
|
|
} else if (sel == 5) { // Delete
|
|
|
|
|
ChannelDetails ch;
|
|
|
|
|
memset(&ch, 0, sizeof(ch));
|
|
|
|
|
the_mesh.setChannelLocal(ch_idx, ch);
|
|
|
|
|
_task->showAlert("Channel deleted", 1000);
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
2026-05-29 00:25:02 +02:00
|
|
|
// sel 1/2/3 already handled by LEFT/RIGHT; ENTER just closes.
|
2026-05-17 00:12:40 +02:00
|
|
|
}
|
2026-07-05 16:08:44 +02:00
|
|
|
if (res != PopupMenu::NONE) {
|
|
|
|
|
_task->savePrefsIfDirty(_ctx_dirty);
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
// Apply any Fav/Edit/Delete change to the visible list now that the menu is done.
|
2026-07-05 16:08:44 +02:00
|
|
|
buildChannelList();
|
|
|
|
|
if (_channel_sel >= _num_channels) _channel_sel = _num_channels > 0 ? _num_channels - 1 : 0;
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
feat(bot): room-server target, tab-carousel UI, per-target independence
Auto-Reply Bot gains a third target (room servers, alongside DM and
channel), new {name}/{hops} reply placeholders, and a DM all/favourites
allow-list. BotScreen is redesigned as a circular tab carousel (Direct /
Channel / Room / Other, same interaction as Nearby Nodes' filter tabs)
instead of one long scrolling list, which also exposed and fixed a
leftover coupling where Channel/Room trigger-replies and their !command
handling secretly depended on the DM tab's Enable/Commands toggles —
each target's Enable and Commands are now fully independent.
- NodePrefs: bot_room_enabled/prefix/trigger/reply, bot_dm_scope,
bot_commands_ch/bot_commands_room (bot_commands_enabled repurposed as
DM-only). SCHEMA_SENTINEL 0xC0DE001D -> 0xC0DE001F; sizeof unchanged
at 2712 (new bytes absorbed existing padding, verified via a
standalone host compile + offsetof check).
- DataStore: persists all new fields; seeds bot_commands_ch/room from
the old shared bot_commands_enabled on upgrade so existing
channel/room command behaviour isn't silently lost.
- MyMeshBot: tryBotReplyRoom/tryBotRoomCommand mirror the channel bot's
shape but post via sendMessage (room relays to members itself);
requires an existing login session with that room, same as a manual
post would. botDmSenderAllowed() gates DM trigger-reply/commands on
the favourites bit when bot_dm_scope=Fav.
- MsgExpand: {name}/{hops} as optional trailing params (default
nullptr/-1, no existing caller affected) — deliberately not exposed
on the general compose keyboard, only on bot Reply fields.
- QuickMsgScreen/UITask: room-target picker (mirrors the channel
picker), routing through the existing room-login prompt when there's
no saved password yet.
- BotScreen: tab carousel (LEFT/RIGHT switches tabs, UP/DOWN moves
rows, Enter is now the only way to change a value); Enable split out
of Channel/Room's combo row; Quiet Hours gained a stepper sub-mode;
Commands moved from a shared toggle into each tab.
- docs/tools_screen.md updated for the new tab layout and behaviour.
Not build-verified — no PlatformIO toolchain in this environment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 20:16:59 +02:00
|
|
|
if (c == KEY_CANCEL) {
|
|
|
|
|
if (_pick_bot_channel) { _pick_bot_channel = false; _task->gotoBotScreen(); return true; }
|
|
|
|
|
_phase = MODE_SELECT;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-06-16 08:54:11 +02:00
|
|
|
// drawList() reclamps _channel_scroll from _channel_sel every render.
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
{ int total = channelPickTotal();
|
|
|
|
|
if (c == KEY_UP && total > 0) { _channel_sel = (_channel_sel > 0) ? _channel_sel - 1 : total - 1; return true; }
|
|
|
|
|
if (c == KEY_DOWN && total > 0) { _channel_sel = (_channel_sel < total - 1) ? _channel_sel + 1 : 0; return true; }
|
|
|
|
|
}
|
|
|
|
|
if (c == KEY_ENTER && _channel_sel == _num_channels && !_pick_bot_channel) {
|
|
|
|
|
int idx = findFreeChannelSlot();
|
|
|
|
|
if (idx < 0) _task->showAlert("Channels full", 1200);
|
|
|
|
|
else _ch_view.openAdd(idx);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (c == KEY_ENTER && _num_channels > 0 && _channel_sel < _num_channels) {
|
2026-05-17 00:12:40 +02:00
|
|
|
_sel_channel_idx = _channel_indices[_channel_sel];
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
if (_pick_target) { commitPickTargetChannel(_sel_channel_idx); return true; }
|
feat(bot): room-server target, tab-carousel UI, per-target independence
Auto-Reply Bot gains a third target (room servers, alongside DM and
channel), new {name}/{hops} reply placeholders, and a DM all/favourites
allow-list. BotScreen is redesigned as a circular tab carousel (Direct /
Channel / Room / Other, same interaction as Nearby Nodes' filter tabs)
instead of one long scrolling list, which also exposed and fixed a
leftover coupling where Channel/Room trigger-replies and their !command
handling secretly depended on the DM tab's Enable/Commands toggles —
each target's Enable and Commands are now fully independent.
- NodePrefs: bot_room_enabled/prefix/trigger/reply, bot_dm_scope,
bot_commands_ch/bot_commands_room (bot_commands_enabled repurposed as
DM-only). SCHEMA_SENTINEL 0xC0DE001D -> 0xC0DE001F; sizeof unchanged
at 2712 (new bytes absorbed existing padding, verified via a
standalone host compile + offsetof check).
- DataStore: persists all new fields; seeds bot_commands_ch/room from
the old shared bot_commands_enabled on upgrade so existing
channel/room command behaviour isn't silently lost.
- MyMeshBot: tryBotReplyRoom/tryBotRoomCommand mirror the channel bot's
shape but post via sendMessage (room relays to members itself);
requires an existing login session with that room, same as a manual
post would. botDmSenderAllowed() gates DM trigger-reply/commands on
the favourites bit when bot_dm_scope=Fav.
- MsgExpand: {name}/{hops} as optional trailing params (default
nullptr/-1, no existing caller affected) — deliberately not exposed
on the general compose keyboard, only on bot Reply fields.
- QuickMsgScreen/UITask: room-target picker (mirrors the channel
picker), routing through the existing room-login prompt when there's
no saved password yet.
- BotScreen: tab carousel (LEFT/RIGHT switches tabs, UP/DOWN moves
rows, Enter is now the only way to change a value); Enable split out
of Channel/Room's combo row; Quiet Hours gained a stepper sub-mode;
Commands moved from a shared toggle into each tab.
- docs/tools_screen.md updated for the new tab layout and behaviour.
Not build-verified — no PlatformIO toolchain in this environment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 20:16:59 +02:00
|
|
|
if (_pick_bot_channel) { commitPickBotChannel(_sel_channel_idx); return true; }
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
int hc = _history.histCountForChannel(_sel_channel_idx);
|
|
|
|
|
_unread_at_entry = (int)_history.chUnread(_sel_channel_idx);
|
2026-05-17 00:12:40 +02:00
|
|
|
_hist_scroll = 0;
|
|
|
|
|
_hist_sel = hc > 0 ? 0 : -1;
|
|
|
|
|
_viewing_max_seen = _hist_sel >= 0 ? _hist_sel : 0;
|
|
|
|
|
_phase = CHANNEL_HIST;
|
feat(repeater): politeness controls, dedicated radio profile, and diagnostics
Settings > Radio gains a repeater toggle, 16 community-suggested radio
presets plus manual Freq/SF/BW/CR tuning (digit-by-digit Freq editor),
4 persisted user preset slots, and the packet pool bumped 16->32 so
queued retransmits stop starving incoming-packet allocation while
relaying.
Four opt-in politeness knobs (skip-advert, max-hops, yield, min-SNR),
all flood-only and off by default, plus overhear suppression that
cancels a queued retransmit if a peer relays the same packet first.
Adaptive Power Control is suppressed while relaying (pins TX power to
the ceiling) and duty-cycle RX is forced off, since a repeater needs
to hear and relay at consistent power.
A dedicated Tools > Repeater screen consolidates the toggle, the
politeness knobs, and live forwarding stats, plus an optional "Custom"
radio profile — a dedicated frequency/SF/BW/CR for relaying, separate
from the companion's own network, band-matched to the companion
frequency by default and used everywhere a radio change can happen
(boot, on-device toggle, app-driven CMD_SET_RADIO_PARAMS, Settings
radio edits) so the device never silently falls back to the wrong
params mid-relay.
Diagnostics gains the actually-forwarded packet count, real heap-free
via mallinfo(), a reset-counters popup, and hardened loop-detect
bounds. Also: a frequency-floor fix and a float-equality preset-match
fix in the repeater profile logic, deduped BW-table/valCol/profile-
seeding helpers shared between Settings and the Repeater screen, and
a build.sh fix tolerating control characters in `pio project config`'s
JSON dump.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 09:07:00 +02:00
|
|
|
// Not updateChannelUnread() here: _hist_visible is still whatever this
|
|
|
|
|
// channel's first render() hasn't computed yet (stale, shared with
|
|
|
|
|
// DM_HIST) — calling it now could ratchet _viewing_max_seen past what's
|
|
|
|
|
// actually about to be shown. render() calls it once that's fresh.
|
2026-06-04 11:38:12 +02:00
|
|
|
if (_share_mode) beginShareCompose(true);
|
2026-05-17 00:12:40 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
if (c == KEY_CONTEXT_MENU && _num_channels > 0 && _channel_sel < _num_channels) {
|
2026-05-17 00:12:40 +02:00
|
|
|
uint8_t ch_idx = _channel_indices[_channel_sel];
|
2026-07-05 16:08:44 +02:00
|
|
|
_ctx_ch_idx = ch_idx; // freeze the menu's target channel
|
2026-05-17 00:12:40 +02:00
|
|
|
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"); }
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
_ctx_menu.begin("Channel options", 6);
|
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);
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
_ctx_menu.addItem("Edit");
|
|
|
|
|
_ctx_menu.addItem("Delete");
|
2026-05-17 00:12:40 +02:00
|
|
|
_ctx_dirty = false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else if (_phase == DM_HIST) {
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
int dm_count = _history.dmHistCountForContact(_sel_contact.id.pub_key);
|
2026-05-17 00:12:40 +02:00
|
|
|
if (_dm_fs.active) {
|
2026-05-18 15:12:11 +02:00
|
|
|
if (_ctx_menu.active) {
|
|
|
|
|
auto res = _ctx_menu.handleInput(c);
|
|
|
|
|
if (res == PopupMenu::SELECTED) {
|
feat(nav): navigate to / save a location shared in a message
Locations already travel as plain text ({loc} expands to "lat,lon"), so any
message carrying coordinates can now be acted on. In the fullscreen message
view, the context-menu (Hold Enter) gains Navigate and Save waypoint
entries whenever the message contains a coordinate — alongside Reply.
- geo::parseLatLon: shared scanner for an embedded "lat,lon" (the {loc}
format), with an optional [WAY] tag that supplies a label. Range-checked,
requires a decimal point to avoid matching plain integer pairs. Will also
back the waypoint-share feature later.
- QuickMsgScreen: Navigate opens NavView inline (To/Hdg/distance, honouring
the global Units setting) over the message, back returns to it; Save
waypoint stores the point (label from [WAY], else auto-named). Works in
both DM and channel histories, and on incoming or outgoing messages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:28:17 +02:00
|
|
|
dispatchFsAction(false);
|
2026-05-18 15:12:11 +02:00
|
|
|
} else if (res != PopupMenu::NONE) {
|
|
|
|
|
_ctx_menu.active = false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
auto res = _dm_fs.handleInput(c);
|
|
|
|
|
if (res == FullscreenMsgView::PREV) {
|
|
|
|
|
if (_dm_hist_sel < dm_count - 1) { _dm_hist_sel++; _dm_fs.scroll = 0; }
|
|
|
|
|
} else if (res == FullscreenMsgView::NEXT) {
|
|
|
|
|
if (_dm_hist_sel > 0) { _dm_hist_sel--; _dm_fs.scroll = 0; }
|
|
|
|
|
} else if (res == FullscreenMsgView::CLOSE) {
|
|
|
|
|
_dm_fs.active = false;
|
2026-05-18 15:12:11 +02:00
|
|
|
} else if (res == FullscreenMsgView::REPLY) {
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
int ring_pos = _history.dmHistEntryForContact(_sel_contact.id.pub_key, _dm_hist_sel);
|
feat(nav): navigate to / save a location shared in a message
Locations already travel as plain text ({loc} expands to "lat,lon"), so any
message carrying coordinates can now be acted on. In the fullscreen message
view, the context-menu (Hold Enter) gains Navigate and Save waypoint
entries whenever the message contains a coordinate — alongside Reply.
- geo::parseLatLon: shared scanner for an embedded "lat,lon" (the {loc}
format), with an optional [WAY] tag that supplies a label. Range-checked,
requires a decimal point to avoid matching plain integer pairs. Will also
back the waypoint-share feature later.
- QuickMsgScreen: Navigate opens NavView inline (To/Hdg/distance, honouring
the global Units setting) over the message, back returns to it; Save
waypoint stores the point (label from [WAY], else auto-named). Works in
both DM and channel histories, and on incoming or outgoing messages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:28:17 +02:00
|
|
|
if (ring_pos >= 0) {
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
bool reply_ok = !_history.dmAtPos(ring_pos).outgoing;
|
|
|
|
|
if (reply_ok) buildDmReplyPrefix(_history.dmAtPos(ring_pos));
|
|
|
|
|
buildFsMenu(_history.dmAtPos(ring_pos).text, reply_ok);
|
2026-05-18 15:12:11 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (_ctx_menu.active) {
|
|
|
|
|
auto res = _ctx_menu.handleInput(c);
|
|
|
|
|
if (res == PopupMenu::SELECTED) {
|
2026-06-04 11:57:29 +02:00
|
|
|
dispatchFsAction(false);
|
2026-05-18 15:12:11 +02:00
|
|
|
} 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) {
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
int ring_pos = _history.dmHistEntryForContact(_sel_contact.id.pub_key, _dm_hist_sel);
|
2026-06-04 11:57:29 +02:00
|
|
|
if (ring_pos >= 0) {
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
bool reply_ok = !_history.dmAtPos(ring_pos).outgoing;
|
|
|
|
|
if (reply_ok) buildDmReplyPrefix(_history.dmAtPos(ring_pos));
|
|
|
|
|
buildFsMenu(_history.dmAtPos(ring_pos).text, reply_ok);
|
2026-05-18 15:12:11 +02:00
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
} else if (_phase == CHANNEL_HIST) {
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
int ch_hist_count = _history.histCountForChannel(_sel_channel_idx);
|
2026-05-17 00:12:40 +02:00
|
|
|
if (_fs.active) {
|
2026-05-18 15:12:11 +02:00
|
|
|
if (_ctx_menu.active) {
|
|
|
|
|
auto res = _ctx_menu.handleInput(c);
|
|
|
|
|
if (res == PopupMenu::SELECTED) {
|
feat(nav): navigate to / save a location shared in a message
Locations already travel as plain text ({loc} expands to "lat,lon"), so any
message carrying coordinates can now be acted on. In the fullscreen message
view, the context-menu (Hold Enter) gains Navigate and Save waypoint
entries whenever the message contains a coordinate — alongside Reply.
- geo::parseLatLon: shared scanner for an embedded "lat,lon" (the {loc}
format), with an optional [WAY] tag that supplies a label. Range-checked,
requires a decimal point to avoid matching plain integer pairs. Will also
back the waypoint-share feature later.
- QuickMsgScreen: Navigate opens NavView inline (To/Hdg/distance, honouring
the global Units setting) over the message, back returns to it; Save
waypoint stores the point (label from [WAY], else auto-named). Works in
both DM and channel histories, and on incoming or outgoing messages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:28:17 +02:00
|
|
|
dispatchFsAction(true);
|
2026-05-18 15:12:11 +02:00
|
|
|
} else if (res != PopupMenu::NONE) {
|
|
|
|
|
_ctx_menu.active = false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
auto res = _fs.handleInput(c);
|
|
|
|
|
if (res == FullscreenMsgView::PREV) {
|
|
|
|
|
if (_hist_sel < ch_hist_count - 1) { _hist_sel++; _fs.scroll = 0; updateChannelUnread(); }
|
|
|
|
|
} else if (res == FullscreenMsgView::NEXT) {
|
|
|
|
|
if (_hist_sel > 0) { _hist_sel--; _fs.scroll = 0; updateChannelUnread(); }
|
|
|
|
|
} else if (res == FullscreenMsgView::CLOSE) {
|
|
|
|
|
_fs.active = false;
|
2026-05-18 15:12:11 +02:00
|
|
|
} else if (res == FullscreenMsgView::REPLY) {
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
int ring_pos = _history.histEntryForChannel(_sel_channel_idx, _hist_sel);
|
feat(nav): navigate to / save a location shared in a message
Locations already travel as plain text ({loc} expands to "lat,lon"), so any
message carrying coordinates can now be acted on. In the fullscreen message
view, the context-menu (Hold Enter) gains Navigate and Save waypoint
entries whenever the message contains a coordinate — alongside Reply.
- geo::parseLatLon: shared scanner for an embedded "lat,lon" (the {loc}
format), with an optional [WAY] tag that supplies a label. Range-checked,
requires a decimal point to avoid matching plain integer pairs. Will also
back the waypoint-share feature later.
- QuickMsgScreen: Navigate opens NavView inline (To/Hdg/distance, honouring
the global Units setting) over the message, back returns to it; Save
waypoint stores the point (label from [WAY], else auto-named). Works in
both DM and channel histories, and on incoming or outgoing messages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:28:17 +02:00
|
|
|
if (ring_pos >= 0)
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
buildFsMenu(_history.chAtPos(ring_pos).text, buildChannelReplyPrefix(_history.chAtPos(ring_pos).text));
|
2026-05-18 15:12:11 +02:00
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (_ctx_menu.active) {
|
|
|
|
|
auto res = _ctx_menu.handleInput(c);
|
|
|
|
|
if (res == PopupMenu::SELECTED) {
|
2026-06-04 11:57:29 +02:00
|
|
|
dispatchFsAction(true);
|
2026-05-18 15:12:11 +02:00
|
|
|
} 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) {
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
int ring_pos = _history.histEntryForChannel(_sel_channel_idx, _hist_sel);
|
2026-06-04 11:57:29 +02:00
|
|
|
if (ring_pos >= 0)
|
refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.
Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
file-scope in MessageHistory.h, so the phase machine still refers to them
unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
flag (a phase fact the store can't see) and returns the ring pos;
afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
forwarders, so no caller changes.
QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
|
|
|
buildFsMenu(_history.chAtPos(ring_pos).text, buildChannelReplyPrefix(_history.chAtPos(ring_pos).text));
|
2026-05-18 15:12:11 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
|
|
|
|
|
} else if (_phase == KEYBOARD) {
|
2026-06-08 20:42:18 +02:00
|
|
|
auto res = _kb->handleInput(c);
|
2026-06-26 10:55:24 +02:00
|
|
|
if (_login_mode) {
|
|
|
|
|
if (res == KeyboardWidget::CANCELLED) {
|
|
|
|
|
_login_mode = false;
|
|
|
|
|
_phase = CONTACT_PICK;
|
|
|
|
|
} else if (res == KeyboardWidget::DONE) {
|
|
|
|
|
// Blank password is valid (guest/no-password rooms) — unlike normal
|
|
|
|
|
// message text, an empty submit here is a deliberate "log in with no
|
|
|
|
|
// password" attempt, so it isn't suppressed like an empty message is.
|
|
|
|
|
_login_mode = false;
|
|
|
|
|
startRoomLogin(_kb->buf);
|
feat(bot): room-server target, tab-carousel UI, per-target independence
Auto-Reply Bot gains a third target (room servers, alongside DM and
channel), new {name}/{hops} reply placeholders, and a DM all/favourites
allow-list. BotScreen is redesigned as a circular tab carousel (Direct /
Channel / Room / Other, same interaction as Nearby Nodes' filter tabs)
instead of one long scrolling list, which also exposed and fixed a
leftover coupling where Channel/Room trigger-replies and their !command
handling secretly depended on the DM tab's Enable/Commands toggles —
each target's Enable and Commands are now fully independent.
- NodePrefs: bot_room_enabled/prefix/trigger/reply, bot_dm_scope,
bot_commands_ch/bot_commands_room (bot_commands_enabled repurposed as
DM-only). SCHEMA_SENTINEL 0xC0DE001D -> 0xC0DE001F; sizeof unchanged
at 2712 (new bytes absorbed existing padding, verified via a
standalone host compile + offsetof check).
- DataStore: persists all new fields; seeds bot_commands_ch/room from
the old shared bot_commands_enabled on upgrade so existing
channel/room command behaviour isn't silently lost.
- MyMeshBot: tryBotReplyRoom/tryBotRoomCommand mirror the channel bot's
shape but post via sendMessage (room relays to members itself);
requires an existing login session with that room, same as a manual
post would. botDmSenderAllowed() gates DM trigger-reply/commands on
the favourites bit when bot_dm_scope=Fav.
- MsgExpand: {name}/{hops} as optional trailing params (default
nullptr/-1, no existing caller affected) — deliberately not exposed
on the general compose keyboard, only on bot Reply fields.
- QuickMsgScreen/UITask: room-target picker (mirrors the channel
picker), routing through the existing room-login prompt when there's
no saved password yet.
- BotScreen: tab carousel (LEFT/RIGHT switches tabs, UP/DOWN moves
rows, Enter is now the only way to change a value); Enable split out
of Channel/Room's combo row; Quiet Hours gained a stepper sub-mode;
Commands moved from a shared toggle into each tab.
- docs/tools_screen.md updated for the new tab layout and behaviour.
Not build-verified — no PlatformIO toolchain in this environment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 20:16:59 +02:00
|
|
|
if (_pick_bot_room) { commitPickBotRoom(_sel_contact); return true; }
|
2026-06-26 10:55:24 +02:00
|
|
|
_phase = CONTACT_PICK;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-05-17 00:12:40 +02:00
|
|
|
if (res == KeyboardWidget::CANCELLED) {
|
2026-06-04 11:38:12 +02:00
|
|
|
if (_share_mode) { _share_mode = false; _task->gotoHomeScreen(); }
|
|
|
|
|
else { _phase = MSG_PICK; }
|
2026-05-17 00:12:40 +02:00
|
|
|
} else if (res == KeyboardWidget::DONE) {
|
2026-05-24 19:53:05 +02:00
|
|
|
int prefix_len = _reply_mode ? (int)strlen(_reply_prefix) : 0;
|
2026-06-08 20:42:18 +02:00
|
|
|
if (_kb->len > prefix_len) {
|
2026-05-24 19:53:05 +02:00
|
|
|
// 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) {
|
2026-06-08 20:42:18 +02:00
|
|
|
memcpy(expanded, _kb->buf, prefix_len);
|
|
|
|
|
expandMsg(_kb->buf + prefix_len, expanded + prefix_len, sizeof(expanded) - prefix_len);
|
2026-05-24 19:53:05 +02:00
|
|
|
} else {
|
2026-06-08 20:42:18 +02:00
|
|
|
expandMsg(_kb->buf, expanded, sizeof(expanded));
|
2026-05-24 19:53:05 +02:00
|
|
|
}
|
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;
|
|
|
|
|
}
|
2026-06-16 08:54:11 +02:00
|
|
|
// drawList() reclamps _msg_scroll from _msg_sel every render.
|
2026-06-18 09:16:57 +02:00
|
|
|
if (c == KEY_UP) { _msg_sel = (_msg_sel > 0) ? _msg_sel - 1 : total_msg_items - 1; return true; }
|
|
|
|
|
if (c == KEY_DOWN) { _msg_sel = (_msg_sel < total_msg_items - 1) ? _msg_sel + 1 : 0; return true; }
|
2026-05-17 00:12:40 +02:00
|
|
|
if (c == KEY_ENTER) {
|
|
|
|
|
if (_msg_sel == 0) {
|
2026-06-08 20:42:18 +02:00
|
|
|
_kb->begin(_reply_mode ? _reply_prefix : "");
|
2026-06-09 15:09:57 +02:00
|
|
|
kbAddSensorPlaceholders(*_kb, &sensors);
|
2026-05-17 00:12:40 +02:00
|
|
|
_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-06-04 12:15:30 +02:00
|
|
|
char msg[MSG_TEXT_BUF];
|
2026-05-18 15:12:11 +02:00
|
|
|
if (_reply_mode) {
|
2026-06-04 12:15:30 +02:00
|
|
|
char body[MSG_TEXT_BUF];
|
2026-05-18 15:12:11 +02:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
};
|