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>
Incoming room-server posts fired the notification and reached the app via
the offline queue, but never appeared when the room was opened directly on
the device: the on-device history (addDMMsg) was gated to ADV_TYPE_CHAT only,
while rooms (ADV_TYPE_ROOM) share that same history list (keyed by the
server's pubkey). Include ADV_TYPE_ROOM so room posts are stored too.
A room carries many guests, so prefix each stored post with its author —
resolved from the signed message's sender pubkey prefix (fallback: short
hex) — and have the DM history view split "Sender: text" for room contacts
so each line is attributed to the guest who wrote it, instead of showing the
room server's name for every message.
Builds verified: WioTrackerL1 + GAT562 30S solo.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Trail map dots/diamonds/cross/start markers and the north indicator were
drawn as raw unscaled pixels, so they shrank to near-invisible specks on
landscape e-ink's larger font scale; they now route through the mini-icon
framework (icons.h) like the rest of the UI, and the grid intersection dots
scale with it too. The keyboard's preview/grid separator had the same bug
(hardcoded 1px) and now uses display.sepH().
Also makes UP/DOWN (and the keyboard's LEFT/RIGHT/UP/DOWN) wrap top<->bottom
across every list, menu and form selector, matching the behavior PopupMenu
already had.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Portrait e-ink message history had two scrollbar glitches:
- Track length tracked the last visible box's bottom, so a long message at
the bottom that left empty space shrank the whole bar. Pin the track to the
full list area (hist_start_y..cby); only the thumb sizes/moves.
- Thumb size was derived from the per-frame visible-box count, which fluctuates
with variable-height boxes. Drive it off pixel sums with a constant viewport
(view_px = track_h) so the thumb stays stable while scrolling one list.
- Gutter reserve used last frame's _hist_visible, so an incoming message briefly
toggled the gutter and reflowed box widths. Decide it from a whole-list fit
test at the widest layout instead — stable, no flicker.
Unify both fixes in computeHistScroll() (shared by DM + channel views) and
split drawScrollIndicator into a pixel core (drawScrollIndicatorPx) + an
item-count wrapper.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes from a full ui-new audit (OLED + e-ink both build green):
- remove dead DisplayDriver::drawScrollArrows (all lists use drawScrollIndicator)
- SettingsScreen selection bar -> lineStep()-1, matching the drawList screens
- HomeScreen: don't force the 1s blink refresh on the CLOCK page (status icons
are hidden there anyway)
- FullscreenMsgView: clamp KEY_DOWN scroll to a cached _max_scroll instead of
over-incrementing and leaning on the next render to clamp
- DataStore: clamp use_lemon_font (>1 -> 0) on load, like the other enum fields
- move the ~1.5KB wrap scratch (trans/lines) off the render stack into shared
file-scope statics in FullscreenMsgView (single-threaded UI; the fullscreen
view and history list never lay out in the same frame)
Cleanup:
- drop dead members RingtoneEditorScreen::DUR_VALS and HomeScreen::sensors_scroll
- delete redundant manual scroll-clamps in handleInput across the drawList
screens (drawList already reclamps each render); remove the now-unused _visible
from QuickMsgScreen/NearbyScreen/BotScreen and BotScreen::scrollToSel()
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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>
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>
Author mini-icons as ASCII-art rows packed to bytes at compile time via
constexpr packRow(), so the glyph shape is visible in source while the
binary holds only the packed bytes (strings never reach flash, identical
to the previous hand-written hex).
Bundle each icon's width/height with its data through MiniIcon and the
MINI_ICON(name, width, ...) macro, so draw sites carry no magic numbers:
miniIconDraw(d, x, y, ICON_CHECK). Also add an unused-by-design skeleton
for full-size page icons (≤32 px): packRow32 / BigIcon / BIG_ICON /
bigIconDraw, for authoring a future big glyph the same readable way.
Existing XBM bitmaps left untouched.
Flash size unchanged; verified no ASCII-art strings reach the .elf.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
End-to-end delivery indicators on the device UI, drawn next to outgoing
messages and auto-scaled to the font (legible on landscape e-ink).
DM (and room servers, which share the DM path):
- Pending / delivered / failed marker driven by the real end-to-end ACK.
Pending shows a row of dots — one per send — so auto-resend progress is
visible before it resolves to ✓ / ✗.
- Auto-resend: new pref dm_resend_count (0-5, default 2), Settings ›
Messages › Resend. A pending DM whose ACK times out is re-sent (reusing
the original timestamp) until resends run out, then ✗. Driven from
UITask::loop so it completes in the background, independent of screen.
- Incoming dedup: a retry reuses the sender timestamp + text but carries a
fresh packet hash, so addDMMsg drops copies matching prefix+ts+text.
Channels (flood, no recipient ACK):
- ✓ only once a repeater echo confirms the send was relayed into the mesh;
no echo is normal, not a failure (no pending/fail shown). A small ring
tracks a burst of sends so each matches its echo. Receive-path hashing is
gated so the hot flood path is untouched when idle.
Shared:
- Markers shown in both the history list and the fullscreen message view.
- Reusable scalable mini-icon facility in icons.h (bitmap + auto-scale);
adding a new status icon is a bitmap plus one draw call.
No changes to the upstream mesh library (src/).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Persist.h: shared writeHeader()/readHeader() for the magic+version+count
on-disk header; Waypoint and Trail snapshots now share one implementation
(byte layout unchanged; each keeps its own count clamp/reject + extra fields).
- TrailScreen: pushAction() guards _act_map / PopupMenu capacity, so adding a
new action can't silently overrun the fixed array.
- PopupMenu: add count()/setSelected() accessors; replace the remaining direct
_sel/_count field pokes in NearbyScreen, TrailScreen and QuickMsgScreen.
- Trail summary: unit-suffixed time ("1h 05m" / "5m 03s") so hours can't be
misread as minutes.
- Trail::haversineMeters delegates to geo::haversineKm — single Haversine
implementation (also trims a little flash).
Verified: GAT562 (SSD1306) and WioTrackerL1Eink (GxEPD) solo builds compile clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pull repeated UI idioms into DisplayDriver and remove duplicated logic:
- DisplayDriver: add drawScrollArrows() and drawInvertedHeader(); replaces 11
copy-pasted scroll-arrow blocks across 5 screens and 3 inverted title-bar
blocks (NearbyScreen x2, NavView). Standardises the detail-view separator.
- useImperial(): single source on UITask; NearbyScreen/TrailScreen delegate
instead of each re-reading NodePrefs.units_imperial.
- NearbyScreen: extract saveSelectedWaypoint() (two identical blocks -> one);
drop the redundant local label buffer (WaypointStore truncates).
- UITask::shutdown(): power GPS off through LocationProvider::stop() instead of
poking PIN_GPS_EN directly — centralises enable/reset-pin + active-level logic.
Net -58 lines. Verified: GAT562 (SSD1306) and WioTrackerL1Eink (GxEPD) solo
builds compile clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NearbyScreen:
- Hold Enter on a list row now shows Navigate and Save waypoint directly,
without entering the detail view first
- TIME added as the last filter slot (LEFT/RIGHT cycles Fav→ALL→…→Snsr→TIME);
time-sort shows age in right column and sorts by last-heard descending
- Save waypoint also available from detail view Options menu
UITask:
- Extract addWaypoint() helper (two overloads: with/without explicit timestamp)
consolidating the full()+add()+saveWaypoints()+alert pattern; TrailScreen
and QuickMsgScreen updated to use it
Fixes:
- SoundNotifier.h: correct NodePrefs.h include path (../NodePrefs.h)
- kbAddSensorPlaceholders: dereference _kb pointer in SettingsScreen,
QuickMsgScreen and BotScreen (signature changed to KeyboardWidget&)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Deduplicate the snprintf+showAlert pattern used in three mark-as-read
sites into a single private method.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MODE_SELECT already showed the count; CONTACT_PICK ("Mark as read") and
CHANNEL_PICK ("Mark all read") were silently clearing unread without
any feedback. Count is read before zeroing so the alert is accurate.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves KeyboardWidget from a per-screen value member to a single instance
owned by UITask, passed to SettingsScreen, QuickMsgScreen, and BotScreen
as a pointer. Only one screen is ever active at a time, so the shared
state is safe. Saves ~1.5 KB of always-resident heap.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After widening the history buffers, the compose side was still the
bottleneck: the keyboard capped input at 139 bytes and the quick-message
expand buffers were 140. Raise KB_MAX_LEN to 160 (MeshCore's MAX_TEXT_LEN)
and size the expand/compose buffers to MSG_TEXT_BUF, so a message can be
typed, expanded, sent, stored and displayed at the full protocol length on
every path.
The custom-message editor now passes an explicit max bound to its 140-byte
store, so the wider keyboard buffer can't overflow it; the bot editor
already passed per-field maxima.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Message history buffers were smaller than the over-the-air maximum, so long
messages lost their tail on display. Channel messages carry the sender
embedded as "Name: body" in the payload (up to MAX_TEXT_LEN = 160 B), but
ChHistEntry::text was only 140 B; DmHistEntry::text was just 80 B — and with
Polish text each accented char is two UTF-8 bytes, so the effective limit was
roughly half the visible characters.
Size both history buffers (and the fullscreen/preview split copies) to
MAX_TEXT_LEN + 1 so a full-length message is stored and shown intact. Sender
split buffers widened to the 32-char name limit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Previously a shared location was only actionable after opening the message
fullscreen; Hold Enter on a history row only ever offered Reply. Route the
DM and channel list context menus through the same buildFsMenu /
dispatchFsAction path as the fullscreen view, so Navigate / Save waypoint
appear directly on the list row when the selected message carries a
location (Navigate returns to the list on Back).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Waypoints list → Hold Enter → Send hands the point to the Messages screen
as "[WAY]<lat>,<lon> <label>" (same text format geo::parseLatLon already
reads). The user picks a contact or channel, the text lands prefilled in
the keyboard to confirm/edit, then sends — closing the loop with the
Navigate / Save waypoint actions on the receiving end.
- QuickMsgScreen: share-compose mode (startShare/beginShareCompose). Picking
a recipient jumps straight to the prefilled keyboard; cancel returns home;
afterSend clears the mode.
- UITask::shareToMessage hands off from TrailScreen to the Messages screen.
- TrailScreen: "Send" added to the waypoint Rename/Delete popup; builds the
[WAY] payload at the {loc} precision (5 dp).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
The channel history is a single ring shared by all channels (was 32
entries), while unread counts live in a separate per-channel array.
Once 32 messages had accumulated across channels, the oldest entry was
evicted on each new message — but the matching _ch_unread[] counter was
left untouched. The Messages badge then claimed unread messages the ring
could no longer surface, so a channel showed "N unread" but opened empty.
(The companion app stores its own copy over the protocol, which is why
the phone could still read them.)
Two changes:
- addChannelMsg() now decrements _ch_unread[evicted.ch_idx] when an entry
is pushed out of the ring, keeping the badge consistent with what is
actually displayable.
- CH_HIST_MAX raised 32 → 96 (~14 KB RAM) so eviction is far less frequent
under normal multi-channel traffic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
M2: CMD_SET_DEFAULT_FLOOD_SCOPE used strlen() on the frame's 31-byte name
slot, which is not required to be NUL-terminated. Switched to strnlen()
so the search can't run past the field into the 16-byte key.
M4: NearbyScreen::renderDiscoverDetail computed strncpy(b64, ..., max_chars - 3)
where max_chars came from display width. On very narrow displays (width < ~28
at 6 px font) this became negative. Skip the pub-key line entirely when
max_chars < 4 so we don't risk a negative count and a bogus terminator.
L1: SNR was shown as truncated integer dB. Switched the detail view and
the 2-line discover cards to %.1f so they keep the 0.25 dB resolution
(consistent with the ping popup, which already used %.1f).
L4: Two fallback "?" sender placeholders used strncpy(buf, "?", sizeof(buf))
— functional but it memsets 21 unused bytes for a one-character string.
Replaced with strcpy.
M1 marked as not-a-bug after re-check: default_scope_name is char[31],
so the n < 31 guard correctly admits the max 30-char string + NUL.
FEATURES.md audit section updated with current status.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
C1/C2: onChannelMessageRecv and onChannelDataRecv both passed
findChannelIdx() through (uint8_t), so an unknown-secret packet
turned -1 into 255 and the message flowed into the offline queue,
UI history, notification and bot reply with a bogus channel index.
Drop such packets at the recv path; also harden addChannelMsg with
an MAX_GROUP_CHANNELS bounds check so a future caller can't poison
the channel-history ring buffer either.
H4: loadPrefsInt always reset trail_units_idx on sentinel mismatch,
even for jumps where the field was saved correctly (e.g. 0xC0DE0004
→ 0xC0DE0005). Scope the reset to sentinel == 0xC0DE0003, which is
the only case where the byte at that offset is actually the stale
sentinel low byte.
FEATURES.md: mark these three items as ✅ in the audit backlog.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PopupMenu:
- _cap now uses max_by_height as a hard ceiling (never draw outside screen).
Previously max(_visible, max_by_height) let callers exceed the screen on
OLED 64px. Now: OLED→4 items, landscape e-ink→10, portrait e-ink→22.
QuickMsgScreen:
- Contact context menu: Notif and Melody cycle with LEFT/RIGHT in-place;
ENTER closes without re-cycling.
- Channel context menu: Notif, Melody and Fav same pattern.
RingtoneEditorScreen:
- Migrated from bespoke menu to PopupMenu (same pattern as everywhere else).
- Duration (1/4…1/32) and BPM (60…180) are now single rows that cycle with
LEFT/RIGHT; separate BPM+/BPM- rows removed.
- Fixed _menu_dur_label buffer too small for "Duration: 1/16" (14→16 bytes).
NearbyScreen:
- Removed redundant "Back" item from context menu (Cancel key navigates back).
TrailScreen:
- Grid toggle responds to LEFT/RIGHT in addition to ENTER.
- Export labels: "Export GPX (live/saved)" → "Export (live/saved)" to fit PM_BW.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous fix set joystick_rotation=0 before reading from file, but
the subsequent rd() call overwrote it with whatever was stored (e.g. 2
from a prior e-ink firmware). Move the hardware-enforce block to after
rd() so stale values in flash are always corrected on OLED builds
(FEAT_JOYSTICK_ROTATION_SETTING=0). Remove the now-redundant pre-read
block.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add per-channel favourite marking and a Settings filter (all/fav) for
the channel list, symmetric with the existing DM and Room filters.
NodePrefs:
- ch_fav_bitmask (uint64_t): bit i = channel i is marked favourite
- ch_fav_only (uint8_t): 0=show all channels, 1=favourites only
- SCHEMA_SENTINEL bumped to 0xC0DE0005
DataStore: save/load both new fields before the sentinel.
SettingsScreen: new CH_FILTER item ('Channels: all/fav') in the
Contacts section, positioned between DM_FILTER and ROOM_FILTER.
QuickMsgScreen:
- buildChannelList() skips non-favourite channels when ch_fav_only=1
- Channel context menu gains a 4th option 'Fav'/'Unfav' that toggles
ch_fav_bitmask for the selected channel and rebuilds the list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Enter on an empty Favourites slot opens a PopupMenu listing candidate
contacts. Sourced first from upstream-favourited chat contacts (those
flagged with c.flags & 0x01), then from recent DM contacts (resolved
from the 4-byte _dm_hist prefix to a full 6-byte pub_key via the contact
list), deduped against the favourites list. Capped at 12 entries.
Selection writes the chosen 6-byte prefix into the target slot; if the
contact is already pinned elsewhere, the previous slot is vacated first
(one contact per dial, per the agreed pin-only semantics). savePrefs
commits to flash and a brief alert confirms.
Plumbing:
- New QuickMsgScreen::getRecentDMContacts() resolves recent DM senders
to 6-byte pub_key prefixes, deduped, newest first
- UITask::getRecentDMContacts() wraps it so HomeScreen doesn't reach
into the QuickMsg screen directly
- HomeScreen gains its own PopupMenu + per-entry pub_key / label buffers
(12 × 6 B keys + 12 × 22 B labels), no shared state with QuickMsg's
Contact-options menu
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
QuickMsgScreen::enterDM (used by openContactDM from the Favourites dial)
now sets _dm_direct_entry so KEY_CANCEL in DM_HIST jumps back to the
home screen instead of falling through CONTACT_PICK → MODE_SELECT. The
flag clears on every reset() and after the cancel returns.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Enter on a populated slot of the Favourites grid now jumps straight into
that contact's DM history — resolves the pinned 6-byte pub_key prefix
against the contact list, then routes through new
UITask::openContactDM(ci) which resets QuickMsgScreen and calls
enterDM() to land directly in DM_HIST.
Empty slots still show the "pin from contact options" hint until phase 3
wires the in-grid mini-picker.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a fourth item to the QuickMsg Contact options popup that toggles
between "Pin to dial" (when the contact isn't pinned) and
"Unpin (slot N)" (when it is). Pinning opens a six-slot picker submenu
labeled "Slot N: <name>" or "Slot N: empty"; the chosen slot stores the
6-byte pub_key prefix in NodePrefs::favourite_contacts and savePrefs
commits to flash with a brief confirmation alert.
New UITask helpers backing the flow:
- findFavouriteSlot(pub_key) — returns 0..5 if pinned, -1 otherwise
- isFavouriteSlotEmpty(slot)
- setFavouriteSlot(slot, pub_key) / clearFavouriteSlot(slot)
PopupMenu reuse: a single _ctx_menu instance hosts both the Contact
options menu and the slot picker, gated by _pin_picker_active so input
routes correctly. Reset on screen reset() and on any non-NONE menu
result.
Slot labels live in a member buffer (6×22 B) because PopupMenu stores
the string pointers verbatim.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sample the unread total for the active mode (DM/Channels/Rooms) before
clearing, then alert "N marked read". Matches the original FEATURES.md
design note.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hold Enter (context menu) on the DM / Channels / Rooms picker opens a
single-item "Mark all read" menu that clears every unread counter of
the highlighted type. Per-mode title is a static-const string because
PopupMenu stores the title pointer verbatim.
New UITask::clearAllDMUnread() resets the whole _dm_unread_table;
clearAllChannelUnread() and clearRoomUnread() already existed.
Also document the GPS breadcrumb storage decision in FEATURES.md:
RAM-only ring with explicit per-slot saves, no background flash writes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
- DM/channel history rows precomputed age_w (= getTextWidth(age) + 3) but
then called getTextWidth(age) again to place the cursor. Reuse age_w.
- Contact-by-message-count sort fetched each contact twice: once when
building _sorted, once when filling counts[]. Fold counts[] into the
build loop so each contact is fetched once.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously page_order_set status was inferred from page_order[0] being
in 1..11, so a single junk byte in that range falsely triggered
custom-order mode. Add explicit page_order_set magic (0xA5) written
alongside the array; pre-magic saves are migrated once on load when
their first byte still passes the legacy range check.
Also fix(ui): keep "@[nick] " prefix out of placeholder expansion in
QuickMsgScreen reply path, so a nick containing a token like {loc}
isn't substituted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- GxEPDDisplay::print() renders '\xDB' as filled rect for Lemon font,
matching OLED Lemon block behavior
- Remove translateUTF8ToBlocks override (base class passthrough sufficient)
- transliterateCodepoint fallback changed '?' → '\xDB' for consistency
- drawTextCentered/Right/LeftAlign now call translateUTF8ToBlocks
- isLemonFont() added to DisplayDriver, SH1106Display, GxEPDDisplay
- isLandscape() guarded by EINK_DISPLAY_MODEL to distinguish OLED vs e-ink
- ind_h trims indicator box height only for Lemon font
- getTextWidth disables wrap before getTextBounds, restores after
- QuickMsgScreen: tighter hist_item padding; 2 messages visible with Lemon
- Hibernate hint fits on one line when width allows, splits otherwise
- BLE PIN page shows toggle hint on landscape e-ink
- Remove unused muted_icon bitmap
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Contacts with at least one message float to the top, ordered by
total message count descending. Contacts with no messages keep their
original relative order. Sorting is applied each time the message
screen is opened (buildContactList), so the list always reflects the
current session's activity.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Reply prefix was eating ~9 chars of display width, causing the actual
message body to be ellipsized in list cards. Fullscreen view already
showed the recipient correctly via the To: header line.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stores RTC timestamp on each history entry and renders it right-aligned
on the sender row using a compact format (Xs/Xm/Xh/>1d).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Reply prefix changed to "@[nick] " format — bracket delimiter handles
nicks with spaces unambiguously
- FullscreenMsgView: parse "@[nick] body" to show "To: nick" header;
fall back gracefully if format not matched
- Reduce FS_CHARS 21→20 to prevent scroll arrows overlapping last char
- Transliterate nick before displaying in "To:" header and "RE:" title
so non-ASCII characters render correctly on the OLED font
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_ctx_menu.render() was only called in CONTACT_PICK and CHANNEL_PICK,
so the reply popup was active but invisible in history views. Added
render calls for list view and fullscreen view in both DM_HIST and
CHANNEL_HIST phases.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Long-press Enter (KEY_CONTEXT_MENU) on a selected message shows an
'Options / Reply' popup in both list and fullscreen views. Confirming
opens MSG_PICK with 'RE: nick' title — the reply can be sent via
keyboard or quick message templates, both automatically prefixed with
'@nick '.
- Channel: reply prefix extracted from 'sender: message' format
- DM: reply available only on incoming messages, uses contact name
- FullscreenMsgView: REPLY added to Result enum, KEY_CONTEXT_MENU handled
- MSG_PICK: _reply_mode flag routes prefix through keyboard and templates
- Guard: prevent sending empty reply (prefix-only keyboard input ignored)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
custom_msgs templates are up to 140 chars; the 80-byte msg[] buffer
silently truncated any message longer than 79 characters before sending.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>