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>