Commit Graph
19 Commits
Author SHA1 Message Date
Jakub d25fda5f32 refactor(ui): extract PopupMenu::beginConfirm() for destructive-action popups
Found while re-reviewing this session's own commits: five screens each
hand-built the same 2-row Action/Cancel confirm popup, defaulting the
highlight to Cancel -- NearbyScreen's contact-delete, AdminScreen's
OTA-start, and the three just added (Trail's reset, Messages' channel
delete, RadioPresetPicker's preset delete). The plan that added those
three had already flagged this exact duplication without acting on it, so
it just tripled instead of getting fixed.

One PopupMenu::beginConfirm(title, action_label, cancel_label="Cancel")
replaces all five call sites, and makes "defaults to Cancel" a property of
the popup itself rather than something each new confirm has to remember.
Also drops two small redundancies spotted along the way: NearbyScreen's
_confirm.active = true, dead since begin() already sets it, and
RadioPresetPicker's deleting = false being set twice in a row (once inside
openConfirm(), once again by its only caller).

No behavior change; verified against the actual PopupMenu/menu-level state
machines in each of the five call sites before touching them.
2026-08-31 20:09:59 +02:00
Jakub 691e2ab506 fix(ui): confirm destructive actions, retire last Hold-Enter-cancel, de-dup labels
Continuing the consistency review: sweep for the same three defect shapes
elsewhere in ui-new/ (own read pass plus two parallel research agents),
verified against source before acting.

Three destructive actions fired on a single Enter with no way back, unlike
contact-delete's existing confirm-defaulted-to-Cancel popup: Trail's "Reset
trail" (wipes the whole recorded route, no undo short of a prior manual
Save -- reuses Trail's own multi-level menu machinery, alongside its
GPS-off confirm), Messages' channel Delete, and RadioPresetPicker's saved-
preset delete (shared by Settings > Radio and Tools > Repeater, so one fix
covers both). All three now confirm the same way, defaulting to Cancel.

KeyboardWidget was the one place Hold-Enter still doubled as Cancel: Shift,
Backspace and a Latin letter's accent popup already have real, kept
meanings under a hold, but every other special-row cell (Space, OK/Done,
the {} placeholder) fell through to a bare CANCELLED, closing the keyboard
exactly like the real Cancel key. Now a no-op there too, matching the "only
Back closes it" rule already applied to popups and screens.

MessagesScreen defined the same two label arrays (Notif states, melody
slots) four times over, once per context-menu handler. Hoisted to one
pair of static class members -- constexpr wasn't enough to get the linker
to emit them on this toolchain, so they follow the same declare-in-class/
define-out-of-class shape NearbyScreen::FILTER_LABELS already uses.

Alert text: "Advert sent!"/"Advert failed.." and "Sent!" were the only
toasts anywhere with trailing punctuation; normalized to the plain style
every other confirmation uses. Unpinning from the Favourites Dial reported
the freed slot number from the Messages screens but not from Nodes or the
dial's own tile menu; now consistent everywhere pinning already was.

DiagnosticsScreen's Live/System/Font tab renderers hand-rolled the same
scroll-clamp/loop/indicator skeleton drawList() already bundles; switched
both to drawList (passing the screen's own _scroll as its `sel` too, since
neither tab has a row cursor -- makes drawList's internal clamp a no-op and
leaves clampScroll() as the only thing bounding it, unchanged). Pure
internal tidy, no behavior change.
2026-08-31 19:52:43 +02:00
Jakub f40748ba61 fix(ui): unify the four navigate views, add Set as target everywhere
Continuing the consistency pass: the four screens that show the
distance/bearing "navigate to a point" view (Nodes, Waypoints, Trail's
Track back, and navigating to a location shared in a message) had drifted
apart in three ways.

Only two of the four passed an EtaTracker to navview::draw(), so only
Nodes and Track back showed the closing-speed/ETA line -- navigating to a
waypoint or a shared location left it off for no reason. All four get one
now. They also left the view on three different key sets (Back alone,
Back+LEFT/RIGHT, Back+Enter); Back is now the only way out of any of them,
so a stray sideways nudge can't drop you out of a running track-back.
Messages' renderNav() also switched from reading node_prefs directly to
the shared useImperial() helper the other three already used.

Set as target -- the row Nodes and Waypoints both offer for a coordinate
-- was missing from the message-location Options menu; added alongside
Navigate and Save waypoint.

Nodes' own Set as target required a full 32-byte public key, which a
name-only live-track entry (someone sharing position on a channel who
isn't a saved contact -- the group-outing case this exists for) never
has. One flag was doing two jobs: "can be pinged" (needs the full key)
and "can be identified" (needs only the 6-byte prefix a person target
actually uses). Split into has_key/has_prefix; Set as target now only
needs a position, resolving to a person target (follows them) when a
prefix is available and a place target (pinned where they were) when it
isn't -- the same distinction Locator's own picker already draws.

Locator's target picker separately still listed the people pinned to the
Favourites Dial as its privileged top tier, which stopped making sense
once pinning and favouriting became separate concepts. It now leads with
favourites instead, matching every other list in the firmware.
2026-08-31 19:25:08 +02:00
Jakub 6d1e71cd3f fix(ui): one rule for value rows, one meaning for Hold Enter
Three interaction inconsistencies found while auditing the favourites work,
all of the same shape: the same gesture meaning different things depending
on which screen you were on.

Value rows in popup menus. Rows like "Notif: ON" or "Sort: Dist" show a
value the user steps through with LEFT/RIGHT, but Enter treated them as
ordinary menu picks and closed the popup, so changing two of them meant
reopening the menu in between. Trail's settings submenu was the lone
exception, working around it by rebuilding and re-selecting after each
Enter. PopupMenu now knows the difference: addValueItem() marks a row, and
Enter on it returns the new VALUE_NEXT instead of SELECTED, leaving the
menu open. Only Back closes a menu now. Applied to the Messages
contact/room/channel menus, Nodes, the Ringtone editor and Trail, which
drops its reopenSettingsAt() workaround. The LEFT/RIGHT cycling bodies
moved into one helper per menu, since Enter and RIGHT now share them.

Nodes' Fav row was the worst case: LEFT/RIGHT did nothing there at all, so
the only way to toggle a favourite was an Enter that dismissed the menu on
every flip. Its label moved to a member buffer (as the Pin row already had)
so it can be retitled in place.

Settings rows Auto-off, Low battery, GPS pwr and Battery ignored Enter,
though their options wrap exactly like the melody/keyboard/clock rows
beside them, where Enter has always stepped forward. They accept it now.
Rows that ramp between fixed ends (Brightness, Volume, TX Pwr, Timezone,
SF/BW/CR) stay LEFT/RIGHT-only -- there is nothing to wrap to.

Hold Enter no longer doubles as Back. It quietly meant "go back" on Tools,
Locator, Live Share, Repeater, Bot, Auto-Advert, GPIO, Compass, the
Dashboard config and the Messages navigate view, while elsewhere the same
long press opens a context menu. It now only ever opens a menu, or does
nothing where there is none. Same for dismissing an open popup, which it
used to do. Checked that this strands nobody: every board that can reach
these screens has a real Back key (back_btn on joystick boards, Esc on
CardKB/TCA8418/T-Deck). Single-button boards produce no KEY_ENTER at all,
so they never leave the home pages in the first place.
2026-08-31 15:43:10 +02:00
JakubandClaude Opus 5 57851627cf 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>
2026-08-31 15:02:01 +02:00
JakubandClaude Sonnet 5 355816eafd polish(ui): text/icon consistency pass across Tools, Settings, and menus
- RepeaterScreen: drop the redundant "Rpt " prefix on the profile fields
  (Preset/Freq/SF/BW/CR), matching Settings > Radio's own terminology --
  the screen is already dedicated to the repeater's own profile, so the
  prefix disambiguated nothing.
- Channel context menu: "Fav: yes/no" -> "Fav: ON/OFF", matching every
  other toggle in the app.
- Settings (System tab): six labels left as raw concatenated identifiers
  (AutoOff/AutoLock/TimeZone/LowBat/BattDisp/BzrVol) now read like their
  space-separated neighbours (Auto pwr/Pwr save/DM sound/GPS pwr), and
  BzrVol no longer clashes with "Buzzer" one row above it for the same
  feature. Three value-label arrays also had one mismatched-case entry
  fixed to match its siblings: Auto-off's "never" -> "OFF", Batt display's
  "icon" -> "Icon", Sound's "built-in" -> "Built-in".
- ToolsScreen: re-enabled the mini-icons next to each tool, which had
  been commented out ("don't fit visually"). Root cause: the screen's own
  drawIcon() centred against lineStep() (line height + inter-row gap)
  instead of getLineHeight() alone, 2px too generous for a near-full-
  height icon -- now delegates to the already-correct miniIconDraw() used
  elsewhere (e.g. the message-list ack checkmark). Also gave Admin and
  GPIO their own icons (padlock, 3-pin header) instead of both sharing
  System's cog.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 21:37:50 +02:00
JakubandClaude Sonnet 5 b6dce2b7f0 feat(ui): show repeater echo count instead of a checkmark on channel sends
The plain ✓ on your own channel post only said "at least one repeater
heard it" -- now it shows how many distinct repeaters echoed it back,
drawn as tiny 3x5 digit icons (icons.h) rather than the normal font,
since the slot next to the sender name is icon-sized, not text-row-
sized. DM delivery ticks are untouched (no repeater-count concept
there, so they keep the plain checkmark).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 20:45:53 +02:00
JakubandClaude Sonnet 5 ba3c3cc91f fix(mesh): don't mark our own channel post as unread
An app-originated channel send (mirrored into the on-device history)
bumped that channel's unread badge whenever the device's own UI
wasn't already showing that exact channel -- unlike an on-device
compose, which sidesteps this by forcing itself into that channel's
view right before sending. Adds an explicit own_message flag through
addChannelMsg (MessageHistory -> AbstractUITask -> UITask ->
MessagesScreen) so an own post is never counted unread regardless of
what's on screen when it's sent.

Found the same bug in MyMeshBot.h's three auto-reply-into-channel call
sites (Remote Bot's own reply showing as unread on itself) and fixed
those with the same mechanism.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 20:26:57 +02:00
JakubandClaude Sonnet 5 bdd2ed379d feat(mesh): show which repeaters relayed/confirmed a message's path
Extends the existing single-boolean channel relay-echo marker into a
full count + list of distinct confirming repeaters, since each
repeater retransmit already appends its own identity hash to the
packet's path and the echo-matching hash deliberately ignores that
mutable path -- so every distinct repeater's echo of one send now
matches the same tracking slot instead of only the first.

Symmetrically captures the hop path a received DM/channel message
actually took, so a new "Path"/"Relayed by" row in the existing
Hold-Enter Options popup can show the resolved sequence of repeaters
(by contact name, or a hex fallback for an unknown one).

Also fixes a real bug caught during testing: the popup row's own
label ("Path (N hops)"/"Relayed by (N)") was built into a stack-local
buffer handed to PopupMenu, which only stores the pointer -- it
rendered as garbage once the building function returned. Moved to a
persistent member buffer.

Bumps the dev-build fallback version and adds release notes/docs
for this plus the two other 1.27 features already on this branch
(BLE retry backoff, marquee-scroll for selected long text).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 18:04:06 +02:00
JakubandClaude Sonnet 5 389f3f7a36 feat(mesh): mirror app-originated DM/channel sends into on-device history
CMD_SEND_TXT_MSG and CMD_SEND_CHANNEL_TXT_MSG (the phone app's send path)
transmitted over the mesh but never touched the device's own MessagesScreen
history, unlike a message composed on-device (MessagesScreen::afterSend) --
so a DM/channel post sent from the app was invisible if that same
conversation was later opened on the device's own screen. Both handlers now
also call into the same history-store entry points incoming messages use.

Also wires up delivery-status parity with an on-device send, not just the
raw text:
- Channels: arms the existing "relayed into mesh" repeater-echo tracker
  (trackRelaySend()/armChannelRelay()) on the new entry -- sendGroupMessage
  already runs that tracker regardless of who originated the send, this
  just attaches it to the right history entry. Required threading a ring
  position back out through AbstractUITask::addChannelMsg (now returns int)
  and a new armChannelRelay() passthrough.
- DMs: addDMMsg gained ack_tag/ack_deadline_ms/resends params (threaded
  through MessageHistory -> MessagesScreen -> AbstractUITask/UITask) so an
  app-sent DM gets the same pending -> \xe2\x9c\x93/\xe2\x9c\x97 status the on-device compose
  path shows. resends stays 0 deliberately: the app owns its own retry
  decision, so this only drives the on-screen status, never a second,
  independent auto-resend from the device itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 17:35:13 +02:00
JakubandClaude Sonnet 5 1bb28296c2 feat(ui): marquee-scroll selected long names/labels instead of static ellipsis
Selecting a row whose ellipsized text overflows now animates a "swing"
marquee: holds at the start, scrolls to reveal the full tail, holds
there, then scrolls back and repeats. Unselected/non-overflowing text
is unchanged (still a static "..."). E-ink gets slower, coarser steps
(fewer, cheaper partial refreshes) than OLED; unchanged frames are
already skipped by the display's CRC diff, so idle holds are free.

Wired into every screen with a selectable row: home favourites, DM/
channel lists and message bodies, Settings, popup menus, Bot, Admin,
Nearby, Waypoints, Locator, Live Share, and the alarm screen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 17:17:55 +02:00
MarekZegare4 55cb5d25d3 fix(gps): hold GPS awake during waypoint nav/avg/track-back and message-location navigate
These views run the same live bearing/distance readout as Compass/Nearby's
navigate mode, which already held GPS awake -- these three didn't, so
duty-cycling could leave them stuck on a stale fix until the next scheduled
wake (up to the configured sleep interval).
2026-08-14 00:40:50 +02:00
JakubandClaude Sonnet 5 8e5f083e2e fix(ui,bot): resolve remaining findings from the 2026-07-26 screen review
- DM/room unread badges could claim messages the ring no longer held
  (same class as the channel fix in 6470afaf, not covered by it):
  getDMUnread()/getDMUnreadTotal() now clamp to dmHistCountForContact(),
  and a new reconcileDMUnread() (called once per loop()) frees any
  _dm_unread_table slot whose ring occupancy has dropped to 0, so a
  17th sender isn't starved by stale entries. onContactRemoved() now
  also clears _dm_unread_table -- the one per-contact table it was
  missing.
- Shift didn't capitalise ł/ń/ź/ż (+ĺ/ľ/ň/ž): the Latin Extended-A
  case-pairing rule assumed a single parity for the whole block, but it
  flips around the unpaired codepoints ĸ/ʼn/Ÿ. Fixed with four
  sub-ranges, verified exhaustively over U+0100-U+017F.
- Triple-click could still toggle the buzzer while locked on
  PIN_USER_BTN/PIN_USER_BTN_ANA boards (joystick path already guarded
  this).
- millis() wraparound: 4 absolute comparisons in UITask.cpp (battery
  poll, auto-off, lock-wake, backlight) converted to the existing
  (int32_t)(millis()-deadline)>=0 idiom; MyMeshBot.h's DM-throttle
  eviction now picks the oldest slot by elapsed time instead of raw
  t_ms, which picked the wrong slot right after a rollover.
- Long-press bypassed checkDisplayOn() on all 5 call sites -- neither
  woke the display nor extended auto-off, and could deliver
  KEY_CONTEXT_MENU to the invisible screen. Moved the gate inside
  handleLongPress() itself instead of patching each site.
- CardKB's backspace/printable-insert branches didn't reset t9_cell,
  so typing right after a T9 cycle tap could get silently overwritten
  by a same-cell re-tap within the T9 timeout.
- buildContactList()'s counts[MAX_CONTACTS] was a 1400 B int array on
  the 4 KB loop() stack; values are bounded by DM_HIST_MAX (32), so
  now uint8_t.
- ACK table treated ack==0 as a wildcard: isAckPending(0) matched any
  free slot, and processAck() with an all-zero ACK matched the first
  free slot and returned its stale contact pointer. Both now skip/reject
  ack==0, and the matched slot's contact pointer is cleared alongside
  its ack hash.
- ensurePageOrderInit() could write one byte past page_order[13] when
  migrating a saved order with all 13 slots full and CLOCK last --
  guarded on insert_at < PAGE_ORDER_LEN.

Two findings from the same review were resolved as no-op decisions,
not code changes: !buzz over DM ignoring quiet hours is intentional
(the pull exemption is meant to cover the buzzer), and the offline
queue's full-queue drop-newest behaviour is upstream code, left alone.

Build-verified green on WioTrackerL1_companion_solo_dual (RAM 71.1%,
Flash 66.6%) and WioTrackerL1Eink_companion_solo_dual (RAM 73.0%,
Flash 67.9%).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 22:05:52 +02:00
JakubandClaude Sonnet 5 176094d32e fix(ui): Admin login could hang forever on "Logging in..."
AdminScreen's LOGIN phase had no timeout, unlike its COMMAND phase
(_cmd_deadline_ms). If a login reply never arrived -- most commonly a
saved password gone stale after the remote node's password changed,
silently dropped instead of nacked -- the screen stayed stuck with only
a manual Cancel to escape.

sendRoomLogin() now returns the same est_timeout sendAdminCommand()
already exposes; AdminScreen uses it to arm a deadline (poll(),
mirroring the COMMAND-phase pattern) that forgets the stale password
and returns to the picker on expiry, same as an explicit login
rejection already does.

Same fix as hotfix/admin-login-timeout (23f43cac), split out of this
branch's other in-progress work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 17:56:46 +02:00
JakubandClaude Sonnet 5 5bfebc6559 feat(bot): Actions commands, multi-trigger, and user GPIO pins
- Auto-Reply Bot gains Actions (!buzz/!gps/!advert) behind a new per-target
  toggle nested under Commands (bot_actions_dm/ch/room); off by default.
- Bot Trigger fields accept comma-separated multiple phrases, matching any
  one fires the reply.
- New user-assignable GPIO feature (Wio Tracker L1): !gpio1..!gpio4 bot
  commands plus a Tools > GPIO screen. Each pin cycles Off/Input/Output;
  GPIO1/GPIO2 (P0.02/P0.29, the nRF52840's AIN0/AIN5) also offer a read-only
  Analog mode via direct SAADC access. GPIO3/GPIO4 (P0.09/P0.10) are the
  chip's NFC1/NFC2 pins, repurposed as plain GPIO via a one-time UICR
  NFCPINS bit-clear in initVariant() (adapted from Adafruit's own
  nfc_to_gpio example) -- confirmed working on real hardware.
- Fix: DM/room reply-prefix ("@[nick] ") stripping happened at the wrong
  layer, hiding the "To:" header on DM replies and leaking the raw prefix
  into room messages' list view; a related mismatch had the history
  scrollbar's sizing pass wrap room messages with the sender name still
  attached, disagreeing with the actual rendered text.

Build-verified: WioTrackerL1_companion_solo_dual and
WioTrackerL1Eink_companion_solo_dual both compile and link clean
(sizeof(NodePrefs) confirmed 2720 via real build, not guessed).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 20:30:03 +02:00
JakubandClaude Sonnet 5 453dc5e570 feat(admin): typed Radio/Routing field editors; pre-release audit fixes
Tools > Admin: split the Radio tab's single "f,bw,sf,cr" comma-string
field (free-text keyboard) into 4 independent, type-appropriate rows --
Frequency (same digit-cursor DigitEditor Settings/Repeater use locally),
Bandwidth/Spreading factor/Coding rate (discrete-set LEFT/RIGHT stepping),
plus TX power and the 3 Routing numeric fields as number steppers and
Repeat as an ON/OFF toggle. Edits happen locally (zero mesh traffic per
keystroke); only Enter sends one combined `set`, Cancel sends nothing.

Full pre-release audit of all 16 commits since v1.22 (5 parallel focus
areas: display/font, keyboard/messages, Nodes/Admin, Bot/channels,
UITask+NodePrefs schema) turned up and fixed:

- Admin: fetched FK_NUMBER values weren't clamped to the field's range,
  so a value already out-of-range could get stuck unreachable; fixed
  commit-time float format noise (%.6f -> %.3f); Cancel/failed-login
  always returned to the Nodes picker even when Admin was opened
  directly from a node's Hold-Enter action -- now returns to wherever
  it was actually opened from (AdminScreen::_from_picker).
- Keyboard: one-shot Shift was consumed after the *first* T9 multi-tap,
  so cycling to the 2nd/3rd candidate always came out lowercase --
  fixed by caching the cycle's caps state (t9_caps).
- Messages: history is numbered newest-first, so a message arriving
  while scrolled up to an older one silently relabeled the view onto a
  different message -- selection now shifts with the insert.
- Channels: onChannelRemoved() didn't clear ch_notif_override/
  ch_notif_muted/ch_fav_bitmask despite its own contract comment
  requiring it (now far more reachable via the on-device Delete);
  an all-zero hex secret silently self-deleted the channel it was
  just saved into (collides with the empty-slot sentinel) -- rejected.
- Room login: isRoomLoggedIn() indexed the login-tracking ring
  directly instead of via its head offset, silently wrong once the
  ring wraps (8+ rooms/session) -- the new on-device Logout depends
  on this being right.
- Font/display: removed the now fully-inert Settings > Display > Font
  toggle and the dead LemonFont.h (retired by the earlier misc-fixed
  font unification, zero remaining includes); fixed a copy-pasted
  "5x7" comment (font is 6x9), two meaningless dead ternaries, and an
  OLED/e-ink inconsistency in the undefined-glyph fallback box offset.
- AdminField's `kind`/bounds fields no longer rely on default member
  initializers inside aggregate-init: this toolchain's actual nRF52
  build (unlike env:native) has no explicit -std= override, so it
  predates C++14's aggregate-with-default-member-initializer rule.
  Given an explicit constructor instead -- portable regardless of
  standard, all existing field-table literals unchanged.

Docs updated to match: tools_screen.md (Diagnostics as a 3-tab
carousel, was documented as one flat screen), message_screen.md
(chat bubbles, newest-at-bottom, cursor mode, secret validation),
settings_screen.md (dropped the dead Font row), solo_ui_framework.md
(header menu_hint signatures, icon priority-drop, KeyboardWidget's
T9/alphabets/cursor-mode). release-notes.md gains the v1.23 section
covering all of the above plus the other 15 commits since v1.22.

Build-verified on WioTrackerL1_companion_solo_dual.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 19:38:51 +02:00
Jakub e439dd0fbe feat(ui): chat bubbles, right-aligned compose button, keyboard cursor editing
- MessagesScreen: shrink message boxes to fit content and anchor them
  right (outgoing) / left (incoming), like typical messengers; move
  the [+ send] compose button to the right edge to match; fix its
  frame margins (no descenders in the label made the symmetric padding
  look uneven) and reclaim the freed 2px for the message list.
- KeyboardWidget: add Hold-Enter cursor-positioning mode (LEFT/RIGHT
  move, UP/DOWN jump to start/end, Enter/Cancel exit) with a visual
  "CURSOR MODE" indicator, so edits and inserts can target any point
  in the typed text instead of always the end.
2026-07-17 14:40:59 +02:00
JakubandClaude Sonnet 5 af0cd2116e feat(ui): message history newest-at-bottom; keyboard one-shot Shift + hold-clear
Message history (DM + channel) now stacks bubbles upward from the compose
row instead of the header, so the newest message sits at the bottom like
a typical messenger, with older messages progressively above. UP/DOWN and
the scrollbar direction are swapped to match; the newest bubble now keeps
the same breathing-room gap against the compose row as it does against
another bubble, instead of touching it.

Keyboard: Shift is one-shot by default (capitalises just the next
letter, then reverts), with Hold-Enter on Shift toggling a persistent
caps-lock for the old sticky behavior. Hold-Enter on Backspace clears
the whole field in one action.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 10:02:00 +02:00
JakubandClaude Sonnet 5 f399298fa6 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