From c27bef0f736c112602bfbf25e39845a3b5b7ff8d Mon Sep 17 00:00:00 2001 From: Strycher Date: Wed, 9 Sep 2026 00:36:38 -0400 Subject: [PATCH] feat(#611): share your contact into a channel from a + picker 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 --- lib/l10n/app_en.arb | 5 ++ lib/l10n/app_localizations.dart | 30 +++++++++ lib/l10n/app_localizations_bg.dart | 17 +++++ lib/l10n/app_localizations_de.dart | 17 +++++ lib/l10n/app_localizations_en.dart | 17 +++++ lib/l10n/app_localizations_es.dart | 17 +++++ lib/l10n/app_localizations_fr.dart | 17 +++++ lib/l10n/app_localizations_hu.dart | 17 +++++ lib/l10n/app_localizations_it.dart | 17 +++++ lib/l10n/app_localizations_ja.dart | 17 +++++ lib/l10n/app_localizations_ko.dart | 17 +++++ lib/l10n/app_localizations_nl.dart | 17 +++++ lib/l10n/app_localizations_pl.dart | 17 +++++ lib/l10n/app_localizations_pt.dart | 17 +++++ lib/l10n/app_localizations_ru.dart | 17 +++++ lib/l10n/app_localizations_sk.dart | 17 +++++ lib/l10n/app_localizations_sl.dart | 17 +++++ lib/l10n/app_localizations_sv.dart | 17 +++++ lib/l10n/app_localizations_uk.dart | 17 +++++ lib/l10n/app_localizations_zh.dart | 17 +++++ lib/models/contact.dart | 29 ++++++++ lib/screens/channel_chat_screen.dart | 76 ++++++++++++++++++++- test/models/contact_share_uri_test.dart | 88 +++++++++++++++++++++++++ untranslated.json | 85 ++++++++++++++++++++++++ 24 files changed, 616 insertions(+), 3 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 4ef40f6..248b112 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -2521,6 +2521,11 @@ "contacts_verifiedByMessage": "Key confirmed. A message with this contact went through, which only works with the matching key.", "contacts_verifiedKeyOnly": "Added from a key. Nothing has confirmed it on air yet.", "contacts_lastSeenNever": "Not heard yet", + "chat_attachTooltip": "Add to message", + "chat_attachGif": "GIF", + "chat_attachMyContact": "My contact card", + "chat_attachMyContactSubtitle": "Puts your public key in the message so anyone here can add you", + "chat_contactCardNeedsConnection": "Connect to a radio first, so the app knows your public key.", "contacts_ShareContact": "Copy contact to Clipboard", "contacts_ShareContactZeroHop": "Share contact by advert", "contacts_zeroHopContactAdvertSent": "Sent contact by advert.", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 8de8748..c886449 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -7576,6 +7576,36 @@ abstract class AppLocalizations { /// **'Not heard yet'** String get contacts_lastSeenNever; + /// No description provided for @chat_attachTooltip. + /// + /// In en, this message translates to: + /// **'Add to message'** + String get chat_attachTooltip; + + /// No description provided for @chat_attachGif. + /// + /// In en, this message translates to: + /// **'GIF'** + String get chat_attachGif; + + /// No description provided for @chat_attachMyContact. + /// + /// In en, this message translates to: + /// **'My contact card'** + String get chat_attachMyContact; + + /// No description provided for @chat_attachMyContactSubtitle. + /// + /// In en, this message translates to: + /// **'Puts your public key in the message so anyone here can add you'** + String get chat_attachMyContactSubtitle; + + /// No description provided for @chat_contactCardNeedsConnection. + /// + /// In en, this message translates to: + /// **'Connect to a radio first, so the app knows your public key.'** + String get chat_contactCardNeedsConnection; + /// No description provided for @contacts_ShareContact. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index afc947b..b0cf93b 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -4425,6 +4425,23 @@ class AppLocalizationsBg extends AppLocalizations { @override String get contacts_lastSeenNever => 'Not heard yet'; + @override + String get chat_attachTooltip => 'Add to message'; + + @override + String get chat_attachGif => 'GIF'; + + @override + String get chat_attachMyContact => 'My contact card'; + + @override + String get chat_attachMyContactSubtitle => + 'Puts your public key in the message so anyone here can add you'; + + @override + String get chat_contactCardNeedsConnection => + 'Connect to a radio first, so the app knows your public key.'; + @override String get contacts_ShareContact => 'Копирай контакт в клипборда'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 24b318c..a55a6ac 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -4437,6 +4437,23 @@ class AppLocalizationsDe extends AppLocalizations { @override String get contacts_lastSeenNever => 'Not heard yet'; + @override + String get chat_attachTooltip => 'Add to message'; + + @override + String get chat_attachGif => 'GIF'; + + @override + String get chat_attachMyContact => 'My contact card'; + + @override + String get chat_attachMyContactSubtitle => + 'Puts your public key in the message so anyone here can add you'; + + @override + String get chat_contactCardNeedsConnection => + 'Connect to a radio first, so the app knows your public key.'; + @override String get contacts_ShareContact => 'Kontakt in die Zwischenablage kopieren'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 80fa2cf..0415d17 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -4359,6 +4359,23 @@ class AppLocalizationsEn extends AppLocalizations { @override String get contacts_lastSeenNever => 'Not heard yet'; + @override + String get chat_attachTooltip => 'Add to message'; + + @override + String get chat_attachGif => 'GIF'; + + @override + String get chat_attachMyContact => 'My contact card'; + + @override + String get chat_attachMyContactSubtitle => + 'Puts your public key in the message so anyone here can add you'; + + @override + String get chat_contactCardNeedsConnection => + 'Connect to a radio first, so the app knows your public key.'; + @override String get contacts_ShareContact => 'Copy contact to Clipboard'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index c44095e..7bd6e11 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -4426,6 +4426,23 @@ class AppLocalizationsEs extends AppLocalizations { @override String get contacts_lastSeenNever => 'Not heard yet'; + @override + String get chat_attachTooltip => 'Add to message'; + + @override + String get chat_attachGif => 'GIF'; + + @override + String get chat_attachMyContact => 'My contact card'; + + @override + String get chat_attachMyContactSubtitle => + 'Puts your public key in the message so anyone here can add you'; + + @override + String get chat_contactCardNeedsConnection => + 'Connect to a radio first, so the app knows your public key.'; + @override String get contacts_ShareContact => 'Copiar contacto al Portapapeles'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 33929a1..4d514b2 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -4448,6 +4448,23 @@ class AppLocalizationsFr extends AppLocalizations { @override String get contacts_lastSeenNever => 'Not heard yet'; + @override + String get chat_attachTooltip => 'Add to message'; + + @override + String get chat_attachGif => 'GIF'; + + @override + String get chat_attachMyContact => 'My contact card'; + + @override + String get chat_attachMyContactSubtitle => + 'Puts your public key in the message so anyone here can add you'; + + @override + String get chat_contactCardNeedsConnection => + 'Connect to a radio first, so the app knows your public key.'; + @override String get contacts_ShareContact => 'Copier le contact dans le presse-papiers'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 92bd608..676dafb 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -4443,6 +4443,23 @@ class AppLocalizationsHu extends AppLocalizations { @override String get contacts_lastSeenNever => 'Not heard yet'; + @override + String get chat_attachTooltip => 'Add to message'; + + @override + String get chat_attachGif => 'GIF'; + + @override + String get chat_attachMyContact => 'My contact card'; + + @override + String get chat_attachMyContactSubtitle => + 'Puts your public key in the message so anyone here can add you'; + + @override + String get chat_contactCardNeedsConnection => + 'Connect to a radio first, so the app knows your public key.'; + @override String get contacts_ShareContact => 'Másolja a kapcsolatot a kiválasztóba'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 3f6b2d7..5e9bb80 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -4431,6 +4431,23 @@ class AppLocalizationsIt extends AppLocalizations { @override String get contacts_lastSeenNever => 'Not heard yet'; + @override + String get chat_attachTooltip => 'Add to message'; + + @override + String get chat_attachGif => 'GIF'; + + @override + String get chat_attachMyContact => 'My contact card'; + + @override + String get chat_attachMyContactSubtitle => + 'Puts your public key in the message so anyone here can add you'; + + @override + String get chat_contactCardNeedsConnection => + 'Connect to a radio first, so the app knows your public key.'; + @override String get contacts_ShareContact => 'Copia contatto negli Appunti'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 13d85ec..566fa32 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -4228,6 +4228,23 @@ class AppLocalizationsJa extends AppLocalizations { @override String get contacts_lastSeenNever => 'Not heard yet'; + @override + String get chat_attachTooltip => 'Add to message'; + + @override + String get chat_attachGif => 'GIF'; + + @override + String get chat_attachMyContact => 'My contact card'; + + @override + String get chat_attachMyContactSubtitle => + 'Puts your public key in the message so anyone here can add you'; + + @override + String get chat_contactCardNeedsConnection => + 'Connect to a radio first, so the app knows your public key.'; + @override String get contacts_ShareContact => '連絡先をクリップボードにコピー'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index ab428ae..0321b79 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -4230,6 +4230,23 @@ class AppLocalizationsKo extends AppLocalizations { @override String get contacts_lastSeenNever => 'Not heard yet'; + @override + String get chat_attachTooltip => 'Add to message'; + + @override + String get chat_attachGif => 'GIF'; + + @override + String get chat_attachMyContact => 'My contact card'; + + @override + String get chat_attachMyContactSubtitle => + 'Puts your public key in the message so anyone here can add you'; + + @override + String get chat_contactCardNeedsConnection => + 'Connect to a radio first, so the app knows your public key.'; + @override String get contacts_ShareContact => '연락처를 복사'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 942c1b4..f6dd647 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -4411,6 +4411,23 @@ class AppLocalizationsNl extends AppLocalizations { @override String get contacts_lastSeenNever => 'Not heard yet'; + @override + String get chat_attachTooltip => 'Add to message'; + + @override + String get chat_attachGif => 'GIF'; + + @override + String get chat_attachMyContact => 'My contact card'; + + @override + String get chat_attachMyContactSubtitle => + 'Puts your public key in the message so anyone here can add you'; + + @override + String get chat_contactCardNeedsConnection => + 'Connect to a radio first, so the app knows your public key.'; + @override String get contacts_ShareContact => 'Contact naar Klembord kopiëren'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 486a9ca..08c0a80 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -4441,6 +4441,23 @@ class AppLocalizationsPl extends AppLocalizations { @override String get contacts_lastSeenNever => 'Not heard yet'; + @override + String get chat_attachTooltip => 'Add to message'; + + @override + String get chat_attachGif => 'GIF'; + + @override + String get chat_attachMyContact => 'My contact card'; + + @override + String get chat_attachMyContactSubtitle => + 'Puts your public key in the message so anyone here can add you'; + + @override + String get chat_contactCardNeedsConnection => + 'Connect to a radio first, so the app knows your public key.'; + @override String get contacts_ShareContact => 'Kopiuj kontakt do schowka'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 41f6820..e091edf 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -4423,6 +4423,23 @@ class AppLocalizationsPt extends AppLocalizations { @override String get contacts_lastSeenNever => 'Not heard yet'; + @override + String get chat_attachTooltip => 'Add to message'; + + @override + String get chat_attachGif => 'GIF'; + + @override + String get chat_attachMyContact => 'My contact card'; + + @override + String get chat_attachMyContactSubtitle => + 'Puts your public key in the message so anyone here can add you'; + + @override + String get chat_contactCardNeedsConnection => + 'Connect to a radio first, so the app knows your public key.'; + @override String get contacts_ShareContact => 'Copiar contato para Área de Transferência'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 42b8fad..3a543c3 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -4433,6 +4433,23 @@ class AppLocalizationsRu extends AppLocalizations { @override String get contacts_lastSeenNever => 'Not heard yet'; + @override + String get chat_attachTooltip => 'Add to message'; + + @override + String get chat_attachGif => 'GIF'; + + @override + String get chat_attachMyContact => 'My contact card'; + + @override + String get chat_attachMyContactSubtitle => + 'Puts your public key in the message so anyone here can add you'; + + @override + String get chat_contactCardNeedsConnection => + 'Connect to a radio first, so the app knows your public key.'; + @override String get contacts_ShareContact => 'Копировать контакт в буфер обмена'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index bcebfb7..0cba92f 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -4407,6 +4407,23 @@ class AppLocalizationsSk extends AppLocalizations { @override String get contacts_lastSeenNever => 'Not heard yet'; + @override + String get chat_attachTooltip => 'Add to message'; + + @override + String get chat_attachGif => 'GIF'; + + @override + String get chat_attachMyContact => 'My contact card'; + + @override + String get chat_attachMyContactSubtitle => + 'Puts your public key in the message so anyone here can add you'; + + @override + String get chat_contactCardNeedsConnection => + 'Connect to a radio first, so the app knows your public key.'; + @override String get contacts_ShareContact => 'Kopírovať kontakt do schránky'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index ac0cef7..b7bbd5a 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -4401,6 +4401,23 @@ class AppLocalizationsSl extends AppLocalizations { @override String get contacts_lastSeenNever => 'Not heard yet'; + @override + String get chat_attachTooltip => 'Add to message'; + + @override + String get chat_attachGif => 'GIF'; + + @override + String get chat_attachMyContact => 'My contact card'; + + @override + String get chat_attachMyContactSubtitle => + 'Puts your public key in the message so anyone here can add you'; + + @override + String get chat_contactCardNeedsConnection => + 'Connect to a radio first, so the app knows your public key.'; + @override String get contacts_ShareContact => 'Kopiraj stik v Odložišče'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 8965079..b8e4588 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -4380,6 +4380,23 @@ class AppLocalizationsSv extends AppLocalizations { @override String get contacts_lastSeenNever => 'Not heard yet'; + @override + String get chat_attachTooltip => 'Add to message'; + + @override + String get chat_attachGif => 'GIF'; + + @override + String get chat_attachMyContact => 'My contact card'; + + @override + String get chat_attachMyContactSubtitle => + 'Puts your public key in the message so anyone here can add you'; + + @override + String get chat_contactCardNeedsConnection => + 'Connect to a radio first, so the app knows your public key.'; + @override String get contacts_ShareContact => 'Kopiera kontakt till Urklipp'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index d78ea3a..4c6569b 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -4432,6 +4432,23 @@ class AppLocalizationsUk extends AppLocalizations { @override String get contacts_lastSeenNever => 'Not heard yet'; + @override + String get chat_attachTooltip => 'Add to message'; + + @override + String get chat_attachGif => 'GIF'; + + @override + String get chat_attachMyContact => 'My contact card'; + + @override + String get chat_attachMyContactSubtitle => + 'Puts your public key in the message so anyone here can add you'; + + @override + String get chat_contactCardNeedsConnection => + 'Connect to a radio first, so the app knows your public key.'; + @override String get contacts_ShareContact => 'Копіювати контакт у буфер обміну'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 8dcc0e2..39fff5d 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -4131,6 +4131,23 @@ class AppLocalizationsZh extends AppLocalizations { @override String get contacts_lastSeenNever => 'Not heard yet'; + @override + String get chat_attachTooltip => 'Add to message'; + + @override + String get chat_attachGif => 'GIF'; + + @override + String get chat_attachMyContact => 'My contact card'; + + @override + String get chat_attachMyContactSubtitle => + 'Puts your public key in the message so anyone here can add you'; + + @override + String get chat_contactCardNeedsConnection => + 'Connect to a radio first, so the app knows your public key.'; + @override String get contacts_ShareContact => '复制联系人信息到剪贴板'; diff --git a/lib/models/contact.dart b/lib/models/contact.dart index 07613e2..bbe3621 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -296,6 +296,35 @@ class Contact { '&public_key=$publicKeyHex' '&type=$type'; + /// Compact contact share for a CHANNEL message, ``. (#611) + /// + /// This is a second, different format from [toShareUri], and deliberately so. + /// It is what real clients put on the air, observed live in `#test` and + /// `#hamradio`, and it is far cheaper: about 75 bytes against 117 for the + /// equivalent URI. Channel text shares a 160-byte payload with the + /// `Sender: ` prefix, so that difference is airtime, not neatness. + /// + /// Use the URI form for a QR, a DM, or an out-of-band paste. Use this for a + /// channel. + String toChannelShare() => + buildChannelShare(publicKeyHex: publicKeyHex, name: name, type: type); + + /// Builds the compact channel share from raw parts, so this device can share + /// its OWN identity without constructing a [Contact]. (#611) + /// + /// Angle brackets are the delimiters, so any in [name] are dropped: a name + /// carrying one would truncate the payload for every parser reading it. A + /// colon is left alone, because the name is the final field and a correct + /// parser splits on the first two colons only. + static String buildChannelShare({ + required String publicKeyHex, + required String name, + int type = advTypeChat, + }) { + final safeName = name.replaceAll('<', '').replaceAll('>', ''); + return '<$publicKeyHex:$type:$safeName>'; + } + /// Parses a contact from the reference-app share URI, or null if malformed. /// /// `meshcore://contact/add?name=&public_key=<64 hex>&type=<1-4>` diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 33ac603..09132f3 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -1243,6 +1243,76 @@ class _ChannelChatScreenState extends State { ); } + /// The composer `+` picker. (#611) + /// + /// Owner decision: one `+` beside the text entry, offering GIF and Contact + /// for now, rather than a dedicated button per thing you can attach. The + /// sendable set stays small because a channel message shares a 160-byte + /// payload with the `Sender: ` prefix. + void _showAttachPicker(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (sheetContext) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.gif_box), + title: Text(sheetContext.l10n.chat_attachGif), + onTap: () { + Navigator.pop(sheetContext); + _showGifPicker(context); + }, + ), + ListTile( + leading: const Icon(Icons.person_outline), + title: Text(sheetContext.l10n.chat_attachMyContact), + subtitle: Text(sheetContext.l10n.chat_attachMyContactSubtitle), + onTap: () { + Navigator.pop(sheetContext); + _insertMyContactCard(context); + }, + ), + ], + ), + ), + ); + } + + /// Puts this device's own contact card into the composer. (#611) + /// + /// Inserts rather than sends, matching the GIF picker, so the text can be + /// reviewed and captioned before it goes out. + /// + /// Emits the compact `` form observed on the live mesh, NOT + /// the `meshcore://` URI. The URI is ~117 bytes against ~75 here, and the + /// channel payload budget is 160 including the `Sender: ` prefix. + void _insertMyContactCard(BuildContext context) { + final connector = Provider.of(context, listen: false); + final keyHex = connector.selfPublicKeyHex; + if (keyHex.length != pubKeySize * 2) { + showDismissibleSnackBar( + context, + content: Text(context.l10n.chat_contactCardNeedsConnection), + ); + return; + } + + final card = Contact.buildChannelShare( + publicKeyHex: keyHex, + name: connector.selfName ?? '', + // This device is a companion. + type: advTypeChat, + ); + + // Append rather than replace, so a caption already typed is not lost. + final existing = _textController.text.trimRight(); + _textController.text = existing.isEmpty ? card : '$existing $card'; + _textController.selection = TextSelection.collapsed( + offset: _textController.text.length, + ); + } + Widget _buildAvatar(String senderName) { final initial = _getFirstCharacterOrEmoji(senderName); final color = _getColorForName(senderName); @@ -1382,9 +1452,9 @@ class _ChannelChatScreenState extends State { child: Row( children: [ IconButton( - icon: const Icon(Icons.gif_box), - onPressed: () => _showGifPicker(context), - tooltip: context.l10n.chat_sendGif, + icon: const Icon(Icons.add), + onPressed: () => _showAttachPicker(context), + tooltip: context.l10n.chat_attachTooltip, ), if (settings.translationEnabled) MessageTranslationButton( diff --git a/test/models/contact_share_uri_test.dart b/test/models/contact_share_uri_test.dart index 670a9f9..49fc2eb 100644 --- a/test/models/contact_share_uri_test.dart +++ b/test/models/contact_share_uri_test.dart @@ -228,4 +228,92 @@ void main() { ); }); }); + + group('Compact channel contact share (#611)', () { + Contact stub(String name, int type) => Contact( + publicKey: hex2Uint8List(_key), + name: name, + type: type, + pathLength: -1, + path: Uint8List(0), + lastSeen: DateTime.fromMillisecondsSinceEpoch(0), + ); + + test('matches the shape observed on the live mesh', () { + // Real traffic in #test and #hamradio carries , with the + // angle brackets as literal delimiters. Key is synthetic here; the repo + // is public. + expect( + Contact.buildChannelShare(publicKeyHex: _key, name: 'KE8AFF', type: 1), + '<$_key:1:KE8AFF>', + ); + }); + + test('a name with spaces is carried verbatim', () { + // One of the two observed samples was "Roger KY4RS". + expect( + Contact.buildChannelShare( + publicKeyHex: _key, + name: 'Roger KY4RS', + type: advTypeChat, + ), + '<$_key:1:Roger KY4RS>', + ); + }); + + test('angle brackets are stripped from the name', () { + // A bracket in the name would truncate the payload for every parser + // reading it, so the emitter must not produce one. + final out = Contact.buildChannelShare( + publicKeyHex: _key, + name: 'wename', + type: advTypeChat, + ); + expect(out, '<$_key:1:weirdname>'); + expect('>'.allMatches(out).length, 1); + expect('<'.allMatches(out).length, 1); + }); + + test('a colon in the name survives, since the name is the final field', () { + // A correct parser splits on the first two colons and takes the rest as + // the name, so this needs no escaping. + expect( + Contact.buildChannelShare( + publicKeyHex: _key, + name: 'a:b', + type: advTypeChat, + ), + '<$_key:1:a:b>', + ); + }); + + test('every documented type is emitted numerically', () { + for (final t in [ + advTypeChat, + advTypeRepeater, + advTypeRoom, + advTypeSensor, + ]) { + expect( + Contact.buildChannelShare(publicKeyHex: _key, name: 'N', type: t), + '<$_key:$t:N>', + ); + } + }); + + test('toChannelShare uses the contact own key, type and name', () { + expect(stub('Bob', advTypeRepeater).toChannelShare(), '<$_key:2:Bob>'); + }); + + test('the compact form is materially cheaper than the URI', () { + // This is the whole reason both formats exist. Channel text shares a + // 160-byte payload with the "Sender: " prefix, so the difference is + // airtime, not tidiness. + final c = stub('KE8AFF', advTypeChat); + final compact = c.toChannelShare().length; + final uri = c.toShareUri().length; + expect(compact, lessThan(uri)); + expect(uri - compact, greaterThan(30)); + }); + }); } diff --git a/untranslated.json b/untranslated.json index a75aef0..2f8061a 100644 --- a/untranslated.json +++ b/untranslated.json @@ -115,6 +115,11 @@ "contacts_verifiedByMessage", "contacts_verifiedKeyOnly", "contacts_lastSeenNever", + "chat_attachTooltip", + "chat_attachGif", + "chat_attachMyContact", + "chat_attachMyContactSubtitle", + "chat_contactCardNeedsConnection", "snrIndicator_noNeighbors", "block_block", "block_unblock", @@ -256,6 +261,11 @@ "contacts_verifiedByMessage", "contacts_verifiedKeyOnly", "contacts_lastSeenNever", + "chat_attachTooltip", + "chat_attachGif", + "chat_attachMyContact", + "chat_attachMyContactSubtitle", + "chat_contactCardNeedsConnection", "snrIndicator_noNeighbors", "block_block", "block_unblock", @@ -397,6 +407,11 @@ "contacts_verifiedByMessage", "contacts_verifiedKeyOnly", "contacts_lastSeenNever", + "chat_attachTooltip", + "chat_attachGif", + "chat_attachMyContact", + "chat_attachMyContactSubtitle", + "chat_contactCardNeedsConnection", "snrIndicator_noNeighbors", "block_block", "block_unblock", @@ -538,6 +553,11 @@ "contacts_verifiedByMessage", "contacts_verifiedKeyOnly", "contacts_lastSeenNever", + "chat_attachTooltip", + "chat_attachGif", + "chat_attachMyContact", + "chat_attachMyContactSubtitle", + "chat_contactCardNeedsConnection", "snrIndicator_noNeighbors", "block_block", "block_unblock", @@ -679,6 +699,11 @@ "contacts_verifiedByMessage", "contacts_verifiedKeyOnly", "contacts_lastSeenNever", + "chat_attachTooltip", + "chat_attachGif", + "chat_attachMyContact", + "chat_attachMyContactSubtitle", + "chat_contactCardNeedsConnection", "snrIndicator_noNeighbors", "block_block", "block_unblock", @@ -820,6 +845,11 @@ "contacts_verifiedByMessage", "contacts_verifiedKeyOnly", "contacts_lastSeenNever", + "chat_attachTooltip", + "chat_attachGif", + "chat_attachMyContact", + "chat_attachMyContactSubtitle", + "chat_contactCardNeedsConnection", "snrIndicator_noNeighbors", "block_block", "block_unblock", @@ -961,6 +991,11 @@ "contacts_verifiedByMessage", "contacts_verifiedKeyOnly", "contacts_lastSeenNever", + "chat_attachTooltip", + "chat_attachGif", + "chat_attachMyContact", + "chat_attachMyContactSubtitle", + "chat_contactCardNeedsConnection", "snrIndicator_noNeighbors", "block_block", "block_unblock", @@ -1102,6 +1137,11 @@ "contacts_verifiedByMessage", "contacts_verifiedKeyOnly", "contacts_lastSeenNever", + "chat_attachTooltip", + "chat_attachGif", + "chat_attachMyContact", + "chat_attachMyContactSubtitle", + "chat_contactCardNeedsConnection", "snrIndicator_noNeighbors", "block_block", "block_unblock", @@ -1243,6 +1283,11 @@ "contacts_verifiedByMessage", "contacts_verifiedKeyOnly", "contacts_lastSeenNever", + "chat_attachTooltip", + "chat_attachGif", + "chat_attachMyContact", + "chat_attachMyContactSubtitle", + "chat_contactCardNeedsConnection", "snrIndicator_noNeighbors", "block_block", "block_unblock", @@ -1384,6 +1429,11 @@ "contacts_verifiedByMessage", "contacts_verifiedKeyOnly", "contacts_lastSeenNever", + "chat_attachTooltip", + "chat_attachGif", + "chat_attachMyContact", + "chat_attachMyContactSubtitle", + "chat_contactCardNeedsConnection", "snrIndicator_noNeighbors", "block_block", "block_unblock", @@ -1525,6 +1575,11 @@ "contacts_verifiedByMessage", "contacts_verifiedKeyOnly", "contacts_lastSeenNever", + "chat_attachTooltip", + "chat_attachGif", + "chat_attachMyContact", + "chat_attachMyContactSubtitle", + "chat_contactCardNeedsConnection", "snrIndicator_noNeighbors", "block_block", "block_unblock", @@ -1666,6 +1721,11 @@ "contacts_verifiedByMessage", "contacts_verifiedKeyOnly", "contacts_lastSeenNever", + "chat_attachTooltip", + "chat_attachGif", + "chat_attachMyContact", + "chat_attachMyContactSubtitle", + "chat_contactCardNeedsConnection", "snrIndicator_noNeighbors", "block_block", "block_unblock", @@ -1807,6 +1867,11 @@ "contacts_verifiedByMessage", "contacts_verifiedKeyOnly", "contacts_lastSeenNever", + "chat_attachTooltip", + "chat_attachGif", + "chat_attachMyContact", + "chat_attachMyContactSubtitle", + "chat_contactCardNeedsConnection", "snrIndicator_noNeighbors", "block_block", "block_unblock", @@ -1948,6 +2013,11 @@ "contacts_verifiedByMessage", "contacts_verifiedKeyOnly", "contacts_lastSeenNever", + "chat_attachTooltip", + "chat_attachGif", + "chat_attachMyContact", + "chat_attachMyContactSubtitle", + "chat_contactCardNeedsConnection", "snrIndicator_noNeighbors", "block_block", "block_unblock", @@ -2089,6 +2159,11 @@ "contacts_verifiedByMessage", "contacts_verifiedKeyOnly", "contacts_lastSeenNever", + "chat_attachTooltip", + "chat_attachGif", + "chat_attachMyContact", + "chat_attachMyContactSubtitle", + "chat_contactCardNeedsConnection", "snrIndicator_noNeighbors", "block_block", "block_unblock", @@ -2230,6 +2305,11 @@ "contacts_verifiedByMessage", "contacts_verifiedKeyOnly", "contacts_lastSeenNever", + "chat_attachTooltip", + "chat_attachGif", + "chat_attachMyContact", + "chat_attachMyContactSubtitle", + "chat_contactCardNeedsConnection", "snrIndicator_noNeighbors", "block_block", "block_unblock", @@ -2371,6 +2451,11 @@ "contacts_verifiedByMessage", "contacts_verifiedKeyOnly", "contacts_lastSeenNever", + "chat_attachTooltip", + "chat_attachGif", + "chat_attachMyContact", + "chat_attachMyContactSubtitle", + "chat_contactCardNeedsConnection", "snrIndicator_noNeighbors", "block_block", "block_unblock",