feat(ui): make favourites mean one thing across every list

Three names had grown around one idea. "Favourite" was a filter in
Settings, an invisible app-only flag on a contact, a device-settable bit on
a channel, and — on the Nodes screen — a menu row that actually pinned to
the Favourites dial. Nothing marked a favourite on screen, and the dial
only took chat contacts.

A favourite is now the starred flag (ContactInfo::flags bit 0 for contacts
and rooms, ch_fav_bitmask for channels), settable on the device everywhere
via a Fav: ON/OFF row, marked with a star on its row, and sorted to the top
of the list — in Messages, Tools > Nodes and the Locator target picker.
Settings > Contacts > "Favs top" turns the sorting off; it defaults on, and
is stored inverted so an upgraded prefs file reads back as on rather than
off.

MyMesh::setContactFavourite() writes the same bit the app sets and bumps
lastmod, so the two stay in sync. The DMs/Rooms = Fav list filters no
longer depend on having starred someone in the app first.

Pinning is now separate and explicit, and the dial takes contacts, room
servers and channels. Slots carry a kind (NodePrefs::favourite_kinds,
schema 0xC0DE0029) — a channel slot holds an index, so emptiness is decided
by the kind first, since channel 0's payload is all zeroes. Choosing what
to pin reuses the Messages screen's own Direct/Channels/Rooms browse
instead of a second picker built on the dial, which drops that picker, its
key/label tables and the now-unused getRecentDMContacts(). A filled tile
gained Unpin/Replace under Hold Enter.

Fixes a room server being pinnable and then unremovable: the picker's
recent-conversations tier didn't filter by contact type and room posts
share the DM history, opening one from the dial skipped the login
handshake, and Unpin only existed in the chat contact list that rooms never
appear in.

Also: PopupMenu::_visible was written and never read (render recomputes the
cap from display height), which is why menus with more items than the
"visible" argument always worked; Settings' all/fav values and the
dm_show_all comment said things that were not true.

Built for Heltec V3, Wio Tracker L1, Wio Tracker L1 e-ink and T-Echo Card.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-08-31 15:02:01 +02:00
co-authored by Claude Opus 5
parent 355816eafd
commit 57851627cf
18 changed files with 660 additions and 338 deletions
+4
View File
@@ -4,10 +4,14 @@
- **Long names and labels scroll into view when selected instead of staying stuck behind an ellipsis.** Any selected row that would otherwise truncate — contacts, channels, messages, settings values, and more — now pauses, scrolls to reveal the full text, then swings back to the start and repeats, for as long as it stays selected; unselected rows keep the plain static ellipsis. E-ink gets its own slower cadence to suit the panel.
- **Messages sent from the phone app now show up in the device's own chat history**, not just on the phone — open a DM, room, or channel on the device and a message you just sent from the app is right there, with the same delivery-confirmation markers (pending/delivered for DMs, pending/relayed for channels) a message composed on the device itself gets.
- **The Favourites Dial takes channels and room servers, not just contacts.** Pinning an empty tile now opens the Messages screen's own Direct/Channels/Rooms browse — the same list you already use to open a conversation — instead of a picker built on the dial, and the tile opens the right thing: a DM, a channel history, or a room (running its login handshake first, which the dial used to skip). Channel tiles are marked with a leading `#` and carry their own unread badge. You can also pin straight from the Messages context menus, which every kind now has, and the dial itself gained **Unpin** / **Replace** under Hold Enter on a filled tile.
- **Favourites now behave the same on every list.** A favourite is marked with a ★ on its row and sorted above everything else — in Messages (contacts, rooms, channels) and in Tools Nodes alike. New **Settings Contacts Favs top** turns the sorting off if you'd rather keep the natural order; it's on by default. On Nodes, the old `Favourite` menu row actually pinned to the dial despite its name — it's now split into a real `Fav: ON/OFF` and a separate `Pin to dial`.
- **Contacts and rooms can be marked as favourites from the device.** `Fav: ON/OFF` joins the contact and room context menus, alongside the one channels already had. It's the same starred flag the phone app shows, so it syncs both ways — and it's what the Settings Contacts `DMs`/`Rooms` = `Fav` list filters read, which previously could only be satisfied by starring someone in the app.
- **"Path" / "Relayed by" — see which repeaters actually carried a message.** Hold Enter on a message (list or fullscreen) and, when the data is available, the Options menu gains a new row: **Path (N hops)** on a received message lists the repeaters it actually travelled through, in order; **Relayed by (N)** on your own channel post lists every distinct repeater heard echoing it back into the mesh. Each hop resolves to a contact's name where known, or a short hex tag otherwise.
### Fixes
- **A room server could end up pinned to the Favourites Dial and then never be removed.** The pin picker's "recent conversations" tier didn't filter by contact type, and room posts share the DM history, so a room could appear there; opening it from the dial then skipped the login handshake, and Unpin only existed in the chat contact list, which rooms never appear in. Rooms are now pinned deliberately (and open correctly), and any tile can be unpinned from the dial itself.
- **Bluetooth connect/disconnect/reconnect cycles (e.g. a phone drifting in and out of range) could make the device nearly unresponsive for a few seconds at a time**, including briefly after turning BLE off. A failed BLE notify was retried on a fixed short interval, repeatedly hitting a blocking wait deep in the BLE stack while the link stayed marginal. Failed sends now back off exponentially (capped at 2s) instead of hammering the same blocking call.
---