The composer GIF button becomes a +, opening a short picker with GIF and
My contact card. Owner decision after seeing the build: one affordance
beside the text entry rather than a button per attachable thing, because
the sendable set stays small when a channel message shares a 160-byte
payload with the Sender: prefix.
Sends the COMPACT format, not the meshcore:// URI:
<{64-hex key}:{type}:{name}>
That shape was observed on the live mesh, twice, from different senders
in #test and #hamradio four weeks apart. It costs about 75 bytes against
117 for the equivalent URI, so on a 160-byte budget the difference is
airtime rather than tidiness. The URI form stays correct for a QR, a DM,
or an out-of-band paste; this is the channel idiom.
Angle brackets are stripped from an emitted name because they are the
delimiters, and a name carrying one would truncate the payload for every
parser reading it. A colon is left alone: the name is the final field,
so a correct parser splits on the first two colons and takes the rest.
Inserts into the composer rather than sending, matching the GIF picker,
so the card can be captioned and reviewed first. It appends, so a
caption already typed is not destroyed.
Parsing a received share is #610 and is not in this change, so an
inbound card still renders as raw text for now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds a calm three-state indicator beside each contact name, per the
owner steer: a green check for advert-verified, a neutral outline check
for key-confirmed, a muted key for key-only. No amber and no hazard
glyph, because nothing is wrong with a key-added contact. The scale
reads as how much we know, never as how risky.
The states are not cosmetic:
- advertVerified: a signed advert arrived, so the node itself asserted
its name, type and position, and the raw packet is stored, which is
also what makes the contact re-shareable.
- keyConfirmed: a message with this contact went through. Direct
messages are encrypted with an ECDH secret derived from the contact
key, and the ACK is computed over the decrypted plaintext
(BaseChatMesh.cpp:442,451), so this is proof the holder of the
matching private key is live. It does NOT prove the person is who the
name claims.
- keyOnly: someone supplied a key and nothing has confirmed it on air.
Costs almost nothing to compute. Contact.isAdvertVerified falls out of
the epoch last_advert_timestamp that #627 already writes, and it clears
itself when a real advert rewrites the field. The advert check runs
first so only an unconfirmed contact pays for a message scan, which
keeps a long contact list cheap.
Also fixes a defect the epoch sentinel introduced: _formatLastSeen ran
the epoch through the relative formatter and claimed a key-added contact
was last seen tens of thousands of days ago. It now reads Not heard yet.
Epic #619.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both halves of the visual exchange, sharing one format and one parser.
Render: showMyContactQrDialog puts this device's own identity on screen
as a QR plus the same link as selectable, copyable text. It refuses to
render when not connected, since an empty key would encode a QR nobody
can add.
Scan: ContactQrScannerScreen validates with Contact.isValidShareUri, the
same check the paste path uses, and pops the raw string. The add dialog
routes a scan through the same handler as a paste, so a QR gets no
separate code path.
The scan affordance is gated to platforms mobile_scanner supports
(Android, iOS, macOS, web). On Windows and Linux the QR half is the
render side, which is the better desktop flow anyway: put your code on
the big screen and let the other person scan it with a phone.
Fixes a real defect found by the new test, not a test artifact:
QrCodeDisplay built its QrImageView through a LayoutBuilder, which
cannot answer intrinsic dimension queries, so any intrinsic-measuring
parent threw. AlertDialog measures its content's max intrinsic height,
so the dialog crashed on open. The QR is now bounded by a tight
SizedBox, which answers the intrinsic itself. This was the widget's
first real call site, so the bug had never been exercised.
Entry point is provisional alongside Add by key; #632 reorganises.
Epic #619.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
First user-reachable piece of the identity exchange. A dialog takes a
public key, a name and a contact type, and hands the stub to
addContactByKey.
The key field also accepts a whole meshcore://contact/add link and
absorbs every field from it, because that is what someone actually
pastes, and a QR is only that link rendered visually. Whitespace is
stripped so a key copied across a line break still works.
The stub is built by round-tripping through buildShareUri and
fromShareUri rather than constructing a Contact directly, so manual
entry and a scanned QR cannot drift apart. One code path, one set of
invariants.
An informational note states plainly that the contact is not confirmed
on air and that the name is whatever the user typed until the node
adverts. Deliberately informational rather than a warning: nothing is
wrong with a key-added contact (#630).
Entry point is provisional, sitting in the contacts overflow menu so the
feature is reachable. The proper add-contact surface, split away from the
advert affordance, is #632 under epic #623.
Five widget tests pin what reaches the connector: key, typed name,
chosen type, the flood sentinel, and the epoch lastSeen that keeps the
firmware replay guard from muting the contact. Also covered: pasted-link
prefill, wrapped-key whitespace, an invalid key sending nothing, and the
missing-name fallback.
Epic #619.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds three tappable rows to the About page opening in the external browser via
url_launcher: offband.org, the Google Play listing (app.offband.meshcore), and
offband.org/donate (Ko-fi + GitHub Sponsors options, live-verified). Failure
surfaces a snackbar. Completes epic #525.
Agent: QuietSnow (session 31eaba02)
About moves out of the Debug pane into a top-level 'About' category (its own
pane, not the stock dialog). Keeps the marketing version display, adds a
description + refreshed legalese (Offband, zjs81/MeshCore MIT attribution kept),
and a View licenses row (showLicensePage). Build identity stays in Device Info
(#553). #527 adds the outbound links here next. Part of epic #525.
Agent: QuietSnow (session 31eaba02)
Hiding the section left enabled features running invisibly. The action is now
'Disable experimental features': disableExperimental() turns off every
experimental flag (CoreScope observer counts, future ones) AND re-locks the
section in one atomic write. Also fixes the subtitle to say 'Build row' (the
#553 anchor), not 'version row'. Part of the #509/#553 experimental section.
Agent: QuietSnow (session 31eaba02)
Badge stays observers-primary; tooltip/long-press now read 'N observers . M
observations' so they reconcile with CoreScope's 'Observations (N)' feed (the
two are distinct metrics: 15 distinct observers vs 34 total sightings). Refresh
feedback moved from a bottom snackbar to an auto-dismissed top MaterialBanner,
out of the way of the composer. Part of #524.
Agent: QuietSnow (session 31eaba02)
Owner testing: the inline badge needed pixel-accurate taps, the refresh gave no
feedback, and there was no refresh in the message action sheet. Now the badge is
a padded InkWell (real tap target), the long-press panel has a 'Refresh CoreScope
observers' action, both show a snackbar with the fetched count, and the auto-poll
is more patient (adds 90s tail steps, stabilises after 3 flat checks) so it climbs
closer to the total before you need to tap. Part of #524.
Agent: QuietSnow (session 31eaba02)
After a channel send, when firmware advertises 0xC6 (cap2 0x08 + ver>=22) AND
the owner enabled the feature, the connector queries the packet hash, then
CoreScope for observer_count, and stamps it on the message (background,
best-effort). Adds the coreScopeObserverCountEnabled AppSettings flag, a gated
switch in the #509 Experimental section (disabled until the radio supports the
capability), and a distinct cloud badge next to the radio-heard count. All
inert until the firmware PR (#611) lands. Completes the client side of #524.
Agent: QuietSnow (session 31eaba02)
7 taps on the About version row (rolling ~3s window) reveal a neutral
'Experimental' settings category; persisted on AppSettings, survives
restart, re-hidden from a switch inside the section. Empty of toggles
for now (Fast Sync #118, CoreScope #524 land into it later).
Countdown snackbars after tap 4; already-unlocked feedback. Version-text
tap is absorbed so it counts without opening the About dialog.
Agent: QuietSnow (session 31eaba02)
No in-app signal of which binary is running has caused repeated confusion
(a debug-signed APK silently not installing over the release app, old layout
persisting with nothing to indicate the new build never landed).
BuildInfo reads GIT_SHA/GIT_BRANCH/BUILD_TIME from --dart-define with dev
fallbacks, so every build carries its own identity independent of the pubspec
version. Surfaced as its own copyable Build row in Device Info.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stores are keyed by the first 10 hex of the connected radio's public key, so
connecting a different radio silently swaps which contacts, channels, and
history you are viewing with nothing in the UI saying so. Device Info showed
the full public key but never tied it to storage.
Adds a Data scope row with the key actually in effect, plus a one-line
explanation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A capability-gated control that doesn't appear is indistinguishable from
a broken one. Diagnosing "the FEM LNA toggle is missing" on real hardware
required a rebuild, because the caps byte was never surfaced and the app
debug log records nothing in a release build unless logging is enabled.
Adds an "Offband capabilities" row to Device Info showing the raw caps
byte, firmware version code, and which gated features it grants, e.g.
"0x02 (v16) - block". That turned an unexplained missing toggle into a
one-look answer: firmware v16 running, FEM LNA bit clear, client correct.
Also logs the same values at device-info parse for anyone who does have
logging on.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the client half against the firmware as-built (#298):
- Device-info offset 83 carries the FEM LNA state on v16+, appended
unconditionally (reads 0 on non-capable boards). parseFemLnaState is
length-guarded and returns null pre-v16. Byte presence signals firmware
version, not capability — the cap BIT gates the UI.
- 0xC3 reply handler adopts the reported value verbatim: firmware returns
post-apply hardware state, not an echo, so a refused write surfaces as
truth rather than a lie.
- setFemLna / requestFemLnaState no-op unless the capability bit is set,
so a non-capable radio never sees 0xC3 traffic.
- Radio Settings toggle rendered only when the bit is set, driven by
connector state via ListenableBuilder rather than local optimistic
state, so it always shows what the radio reports.
- errCodeUnsupportedCmd documented: a mis-gated request draws [0x01][0x01],
not [0x01][0x06]. Neither is 0xC3-prefixed, so no new error path.
Gating is on the bit alone, never model or version: firmware derives it
from a runtime FEM probe, so two Heltec V4s can legitimately disagree and
rak3401 reports false by design (its SKY66122 gates LNA and PA together).
13 tests. Not hardware-validated — firmware 0xC3 is still on a branch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The channel long-press sheet's binary Mute/Unmute tile becomes a
Notifications tile showing the current level, opening a picker with All
messages / Mentions only / Off.
The mode is read and written against the channel's PSK identity via
AppSettings.channelNotifyKey, so it survives a rename and does not follow a
reused slot (#259). Plain ListTiles with a check mark are used rather than
RadioListTile to stay clear of the Radio groupValue deprecation.
Adds channels_notifications / channels_notifyAll / channels_notifyMentionsOnly
/ channels_notifyOff to app_en.arb and regenerates l10n; the other 17 locales
fall back to English and are recorded in untranslated.json.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the DM composer with an "Unblock to message" notice bar when
the contact is blocked, so a block also stops outgoing DMs (previously
only incoming DMs/adverts were suppressed). Inline unblock restores the
composer. Adds block_composerNotice l10n string and documents the
outgoing-DM rule in block-contract-as-built.md §4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Settings showed the self public key truncated to 16 hex chars + "..." as
plain Text (settings_screen:262/:410), so it couldn't be shared. Show the
full key, make it selectable, and add a copy button beside it.
- _buildInfoRow gains an optional copyValue: renders the value as
SelectableText + a copy IconButton that copies the full key and shows a
"Public key copied" snackbar. Both self-pubkey rows pass the full key.
- New l10n string settings_publicKeyCopied (regenerated localizations +
untranslated.json).
Part of #234.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Connector parses ADD/REMOVE/CLEAR replies; ADD ok=0 (node store full at 32) sets
blockOffloadStoreFull (local stays authoritative), cleared on reconcile. BlockedView
shows a firmware-offload status row when supportsOffbandBlock — 'active', or a
'radio block list full' warning. Adds block_offload* l10n.
Epic B #166 / B5 #180.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New BlockedView pane (Settings category, after Privacy): lists blocked pubkeys
(contact name when resolvable, else short key) and name-only channel blocks, each
with Unblock; empty state when nothing blocked. App-local; firmware-offload
indicator deferred to Epic B. Adds block_* settings l10n.
Epic A #165 / A6 #173. Design: docs/architecture/block-contract-as-built.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Long-press a channel post -> Block sender: resolve the claimed name to known
pubkey(s) and block each (full block across DM+adverts+all channels, multi-device
covered); if unresolved, block the name globally across ALL channels (promotes to
pubkey later, #174). Adds block_* l10n strings. A5 channel surface;
contacts/chat/discovery + unblock still to come.
Epic A #165 / A5 #172. Design: docs/architecture/block-contract-as-built.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Offband 0xC1 GPS query: 1-byte request -> RESP 0xC1 ASCII reply
(enabled/detected/active/fix/baud/lat/lon/alt_cm/sats/time), parsed into
OffbandGpsStatus (lat/lon /1e6). New GPS-status section in the Location dialog
(live-fix vs stored banner + coords/sats/alt/time). Validated against RedCreek's
feat/gps-state-query test firmware.
#140: the per-minute GPS poll now uses the lightweight 0xC1 query instead of
CMD_APP_START (which re-walked channels + re-drained messages = the BLE re-init
flood); a live fix updates self-lat/lon. Validated on b42 (APP_START ~1/min ->
~0, 0xC1 polls clean every 60s, channel re-walk gone).
Gemini (fix-then-ship): atomic completer-claim in _handleOffbandGps to shrink
the stale-reply race; full elimination needs a protocol sequence id, but the
poll shares in-flight via the re-entrancy guard and the query button is disabled
during a request, so concurrent requests don't occur in practice. The hardcoded
snackbars Gemini flagged in _editLocation are pre-existing l10n debt, not
introduced here; all new strings are localized.
Closes#135Closes#140
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Parse the device-info reply's readable strings (build date / model / firmware
version, NUL-terminated after the 8-byte header) and show firmwareVersion +
deviceModel in Settings → Device Info. Raw strings are logged on connect.
End-anchored field mapping (deviceInfoFields) keeps the version correct on
partial frames.
Gemini (fix-then-ship): removed the unused deviceBuildDate field/getter (dead
state) — the build date stays in the connect log via the raw strings, and
deviceInfoFields still parses + tests it.
Closes#134
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Reply long-press drops the cursor into the composer (post-frame focus). (#131)
- "Reply with route" sends `@[sender] ↩ N hops · via <path>`, byte-truncated with `…`, Cyr2Lat-aware. (#106)
Gemini (fix-then-ship): the unawaited `sendChannelMessage` in `_sendRouteReply` mirrors the established `_sendMessage` convention — failure surfaces via the visible message's delivery status, not the returned Future; holistic channel-send error handling is tracked in #133 per maintainer decision. O(n^2) path truncation is negligible on the <=160-byte payload; the `w<1` guard is a defensive div-by-zero (pathHashByteWidth is 1-3).
Closes#131Closes#106
Part of #132
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Clock setting (System / 12h / 24h) in App Settings; chat times follow it + locale.
- Channel tile: always-on metadata row - time + hops on inbound, time + cell_tower
heard count + status on outbound - decoupled from the message-tracing toggle. The
via-path stays behind tracing.
- Drop the auto reply-to block and the most-recent-sender guess; @mentions now
render inline in the message text.
- Direct chat: same clock-aware time formatting.
Closes#37
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Split the monolithic AppSettingsScreen (reached via a redundant tile->push from the App Settings category) into two embeddable views rendered directly in the shell panes: AppSettingsView (appearance, translation, battery, map, Cyr2Lat, debug) and MessageSettingsView (notifications + message handling, a new top-level 'Message Settings' category after Contacts). Removes the category->tile->separate-screen layer; each category now lands directly on its settings. Deletes app_settings_screen.dart (no remaining refs). Adds settings_messageSettings(+Subtitle) l10n keys (en template; other locales fall back pending the l10n sweep). Card bodies moved verbatim -- behavior preserved.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Added translations for "Send message", "Guest information", and "Guest tools" in Bulgarian, German, Spanish, French, Hungarian, Italian, Japanese, Korean, Dutch, Polish, Portuguese, Russian, Slovak, Slovenian, Swedish, Ukrainian, and Chinese.
- Updated the "Clock synchronization after login" feature subtitle in all affected languages.
- Removed untranslated keys from the untranslated.json file as they have now been localized.
* Add notification rate limiting with privacy-safe debug logging
- Add batching system to prevent notification storms (3s rate limit, 5s batch window)
- Queue rapid notifications and show batch summaries
- Debug logs show device names for adverts, sender/channel for messages (no content leaks)
- Remove unused _maxBatchSize constant
Context: Added after getting notification-flooded while evaluating RF flood management. The irony.
* Update notification_service.dart
I made a mistake and removed this
* Add l10n support for notification strings
Addresses PR #110 review feedback to use the translations system:
- Add notification strings to app_en.arb (plurals for batch summary)
- Update NotificationService to use lookupAppLocalizations()
- Wire locale from MaterialApp to NotificationService
- Regenerate localization files
New strings added (English only, translations needed):
- notification_activityTitle: "MeshCore Activity"
- notification_messagesCount: "{count} message(s)"
- notification_channelMessagesCount: "{count} channel message(s)"
- notification_newNodesCount: "{count} new node(s)"
- notification_newTypeDiscovered: "New {type} discovered"
- notification_receivedNewMessage: "Received new message"
* Add notification string translations for all supported languages
Translated notification_activityTitle, notification_messagesCount,
notification_channelMessagesCount, notification_newNodesCount,
notification_newTypeDiscovered, and notification_receivedNewMessage
to: bg, de, es, fr, it, nl, pl, pt, ru, sk, sl, sv, uk, zh
Includes proper ICU plural forms for Slavic languages (few/many/other)
and Slovenian dual form.
* Apply dart format to notification_service.dart
---------
Co-authored-by: Winston Lowe <wel97459@gmail.com>
These languages had translation files but were missing from the
settings UI. Adds appSettings_languageRu and appSettings_languageUk
strings and corresponding RadioListTile entries.
Fixes missing languages in app settings.
- Added flutter_linkify package to auto-detect and linkify URLs in chat messages.
- Implemented LinkHandler class to manage link tap confirmations and URL launching.
- Updated chat_screen.dart to use Linkify for displaying message text with links.
- Registered url_launcher plugin for handling URL launches across platforms.
- Updated pubspec.yaml and pubspec.lock to include new dependencies.
- Cleaned up untranslated.json by removing unused translations.
- Implement Community model for managing community data, including secret handling and PSK derivation.
- Create CommunityQrScannerScreen for scanning and joining communities via QR codes.
- Develop CommunityStore for persisting community data using SharedPreferences.
- Introduce QrCodeDisplay widget for displaying QR codes with customizable options.
- Add QrScannerWidget for reusable QR code scanning functionality with validation and controls.
- Added `untranslated.json` to track untranslated messages.
- Updated localization keys in various language files to use camelCase format for consistency.
- Modified `neighbours_screen.dart` to reference updated localization keys.