feat(#628): add a contact by typing or pasting a public key

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>
feat/619-contact-identity-uri
Strycher 3 weeks ago
parent 808bc2b7b3
commit 85239b0606

@ -2493,6 +2493,23 @@
"contacts_floodAdvert": "Flood Advert",
"contacts_copyAdvertToClipboard": "Copy Advert to Clipboard",
"contacts_addContactFromClipboard": "Add Contact from Clipboard",
"contacts_addByKey": "Add by public key",
"contacts_addByKeyDescription": "Add someone using the public key they gave you. Works even if you have never heard their advert.",
"contacts_publicKeyLabel": "Public key",
"contacts_publicKeyHelper": "64 hex characters, or paste a meshcore:// contact link",
"contacts_publicKeyInvalid": "Enter 64 hex characters, or a meshcore:// contact link",
"contacts_nameLabel": "Name",
"contacts_typeLabel": "Type",
"contacts_addByKeyUnverifiedNote": "Not confirmed on air yet. The name is whatever you type here until this node sends an advert.",
"contacts_addByKeyAdded": "Added {name}",
"@contacts_addByKeyAdded": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"contacts_addByKeyFailed": "Could not add the contact. Check the radio connection.",
"contacts_ShareContact": "Copy contact to Clipboard",
"contacts_ShareContactZeroHop": "Share contact by advert",
"contacts_zeroHopContactAdvertSent": "Sent contact by advert.",

@ -7450,6 +7450,66 @@ abstract class AppLocalizations {
/// **'Add Contact from Clipboard'**
String get contacts_addContactFromClipboard;
/// No description provided for @contacts_addByKey.
///
/// In en, this message translates to:
/// **'Add by public key'**
String get contacts_addByKey;
/// No description provided for @contacts_addByKeyDescription.
///
/// In en, this message translates to:
/// **'Add someone using the public key they gave you. Works even if you have never heard their advert.'**
String get contacts_addByKeyDescription;
/// No description provided for @contacts_publicKeyLabel.
///
/// In en, this message translates to:
/// **'Public key'**
String get contacts_publicKeyLabel;
/// No description provided for @contacts_publicKeyHelper.
///
/// In en, this message translates to:
/// **'64 hex characters, or paste a meshcore:// contact link'**
String get contacts_publicKeyHelper;
/// No description provided for @contacts_publicKeyInvalid.
///
/// In en, this message translates to:
/// **'Enter 64 hex characters, or a meshcore:// contact link'**
String get contacts_publicKeyInvalid;
/// No description provided for @contacts_nameLabel.
///
/// In en, this message translates to:
/// **'Name'**
String get contacts_nameLabel;
/// No description provided for @contacts_typeLabel.
///
/// In en, this message translates to:
/// **'Type'**
String get contacts_typeLabel;
/// No description provided for @contacts_addByKeyUnverifiedNote.
///
/// In en, this message translates to:
/// **'Not confirmed on air yet. The name is whatever you type here until this node sends an advert.'**
String get contacts_addByKeyUnverifiedNote;
/// No description provided for @contacts_addByKeyAdded.
///
/// In en, this message translates to:
/// **'Added {name}'**
String contacts_addByKeyAdded(String name);
/// No description provided for @contacts_addByKeyFailed.
///
/// In en, this message translates to:
/// **'Could not add the contact. Check the radio connection.'**
String get contacts_addByKeyFailed;
/// No description provided for @contacts_ShareContact.
///
/// In en, this message translates to:

@ -4349,6 +4349,43 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get contacts_addContactFromClipboard => 'Добави контакт от клипборда';
@override
String get contacts_addByKey => 'Add by public key';
@override
String get contacts_addByKeyDescription =>
'Add someone using the public key they gave you. Works even if you have never heard their advert.';
@override
String get contacts_publicKeyLabel => 'Public key';
@override
String get contacts_publicKeyHelper =>
'64 hex characters, or paste a meshcore:// contact link';
@override
String get contacts_publicKeyInvalid =>
'Enter 64 hex characters, or a meshcore:// contact link';
@override
String get contacts_nameLabel => 'Name';
@override
String get contacts_typeLabel => 'Type';
@override
String get contacts_addByKeyUnverifiedNote =>
'Not confirmed on air yet. The name is whatever you type here until this node sends an advert.';
@override
String contacts_addByKeyAdded(String name) {
return 'Added $name';
}
@override
String get contacts_addByKeyFailed =>
'Could not add the contact. Check the radio connection.';
@override
String get contacts_ShareContact => 'Копирай контакт в клипборда';

@ -4361,6 +4361,43 @@ class AppLocalizationsDe extends AppLocalizations {
String get contacts_addContactFromClipboard =>
'Kontakt aus Zwischenablage hinzufügen';
@override
String get contacts_addByKey => 'Add by public key';
@override
String get contacts_addByKeyDescription =>
'Add someone using the public key they gave you. Works even if you have never heard their advert.';
@override
String get contacts_publicKeyLabel => 'Public key';
@override
String get contacts_publicKeyHelper =>
'64 hex characters, or paste a meshcore:// contact link';
@override
String get contacts_publicKeyInvalid =>
'Enter 64 hex characters, or a meshcore:// contact link';
@override
String get contacts_nameLabel => 'Name';
@override
String get contacts_typeLabel => 'Type';
@override
String get contacts_addByKeyUnverifiedNote =>
'Not confirmed on air yet. The name is whatever you type here until this node sends an advert.';
@override
String contacts_addByKeyAdded(String name) {
return 'Added $name';
}
@override
String get contacts_addByKeyFailed =>
'Could not add the contact. Check the radio connection.';
@override
String get contacts_ShareContact => 'Kontakt in die Zwischenablage kopieren';

@ -4283,6 +4283,43 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get contacts_addContactFromClipboard => 'Add Contact from Clipboard';
@override
String get contacts_addByKey => 'Add by public key';
@override
String get contacts_addByKeyDescription =>
'Add someone using the public key they gave you. Works even if you have never heard their advert.';
@override
String get contacts_publicKeyLabel => 'Public key';
@override
String get contacts_publicKeyHelper =>
'64 hex characters, or paste a meshcore:// contact link';
@override
String get contacts_publicKeyInvalid =>
'Enter 64 hex characters, or a meshcore:// contact link';
@override
String get contacts_nameLabel => 'Name';
@override
String get contacts_typeLabel => 'Type';
@override
String get contacts_addByKeyUnverifiedNote =>
'Not confirmed on air yet. The name is whatever you type here until this node sends an advert.';
@override
String contacts_addByKeyAdded(String name) {
return 'Added $name';
}
@override
String get contacts_addByKeyFailed =>
'Could not add the contact. Check the radio connection.';
@override
String get contacts_ShareContact => 'Copy contact to Clipboard';

@ -4350,6 +4350,43 @@ class AppLocalizationsEs extends AppLocalizations {
String get contacts_addContactFromClipboard =>
'Agregar contacto desde el portapapeles';
@override
String get contacts_addByKey => 'Add by public key';
@override
String get contacts_addByKeyDescription =>
'Add someone using the public key they gave you. Works even if you have never heard their advert.';
@override
String get contacts_publicKeyLabel => 'Public key';
@override
String get contacts_publicKeyHelper =>
'64 hex characters, or paste a meshcore:// contact link';
@override
String get contacts_publicKeyInvalid =>
'Enter 64 hex characters, or a meshcore:// contact link';
@override
String get contacts_nameLabel => 'Name';
@override
String get contacts_typeLabel => 'Type';
@override
String get contacts_addByKeyUnverifiedNote =>
'Not confirmed on air yet. The name is whatever you type here until this node sends an advert.';
@override
String contacts_addByKeyAdded(String name) {
return 'Added $name';
}
@override
String get contacts_addByKeyFailed =>
'Could not add the contact. Check the radio connection.';
@override
String get contacts_ShareContact => 'Copiar contacto al Portapapeles';

@ -4372,6 +4372,43 @@ class AppLocalizationsFr extends AppLocalizations {
String get contacts_addContactFromClipboard =>
'Ajouter un contact depuis le presse-papiers';
@override
String get contacts_addByKey => 'Add by public key';
@override
String get contacts_addByKeyDescription =>
'Add someone using the public key they gave you. Works even if you have never heard their advert.';
@override
String get contacts_publicKeyLabel => 'Public key';
@override
String get contacts_publicKeyHelper =>
'64 hex characters, or paste a meshcore:// contact link';
@override
String get contacts_publicKeyInvalid =>
'Enter 64 hex characters, or a meshcore:// contact link';
@override
String get contacts_nameLabel => 'Name';
@override
String get contacts_typeLabel => 'Type';
@override
String get contacts_addByKeyUnverifiedNote =>
'Not confirmed on air yet. The name is whatever you type here until this node sends an advert.';
@override
String contacts_addByKeyAdded(String name) {
return 'Added $name';
}
@override
String get contacts_addByKeyFailed =>
'Could not add the contact. Check the radio connection.';
@override
String get contacts_ShareContact =>
'Copier le contact dans le presse-papiers';

@ -4367,6 +4367,43 @@ class AppLocalizationsHu extends AppLocalizations {
String get contacts_addContactFromClipboard =>
'Adjon hozzá egy kapcsolatot a kiválasztott listából';
@override
String get contacts_addByKey => 'Add by public key';
@override
String get contacts_addByKeyDescription =>
'Add someone using the public key they gave you. Works even if you have never heard their advert.';
@override
String get contacts_publicKeyLabel => 'Public key';
@override
String get contacts_publicKeyHelper =>
'64 hex characters, or paste a meshcore:// contact link';
@override
String get contacts_publicKeyInvalid =>
'Enter 64 hex characters, or a meshcore:// contact link';
@override
String get contacts_nameLabel => 'Name';
@override
String get contacts_typeLabel => 'Type';
@override
String get contacts_addByKeyUnverifiedNote =>
'Not confirmed on air yet. The name is whatever you type here until this node sends an advert.';
@override
String contacts_addByKeyAdded(String name) {
return 'Added $name';
}
@override
String get contacts_addByKeyFailed =>
'Could not add the contact. Check the radio connection.';
@override
String get contacts_ShareContact => 'Másolja a kapcsolatot a kiválasztóba';

@ -4355,6 +4355,43 @@ class AppLocalizationsIt extends AppLocalizations {
String get contacts_addContactFromClipboard =>
'Aggiungere contatto dalla clipboard';
@override
String get contacts_addByKey => 'Add by public key';
@override
String get contacts_addByKeyDescription =>
'Add someone using the public key they gave you. Works even if you have never heard their advert.';
@override
String get contacts_publicKeyLabel => 'Public key';
@override
String get contacts_publicKeyHelper =>
'64 hex characters, or paste a meshcore:// contact link';
@override
String get contacts_publicKeyInvalid =>
'Enter 64 hex characters, or a meshcore:// contact link';
@override
String get contacts_nameLabel => 'Name';
@override
String get contacts_typeLabel => 'Type';
@override
String get contacts_addByKeyUnverifiedNote =>
'Not confirmed on air yet. The name is whatever you type here until this node sends an advert.';
@override
String contacts_addByKeyAdded(String name) {
return 'Added $name';
}
@override
String get contacts_addByKeyFailed =>
'Could not add the contact. Check the radio connection.';
@override
String get contacts_ShareContact => 'Copia contatto negli Appunti';

@ -4152,6 +4152,43 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get contacts_addContactFromClipboard => 'クリップボードから連絡先を追加する';
@override
String get contacts_addByKey => 'Add by public key';
@override
String get contacts_addByKeyDescription =>
'Add someone using the public key they gave you. Works even if you have never heard their advert.';
@override
String get contacts_publicKeyLabel => 'Public key';
@override
String get contacts_publicKeyHelper =>
'64 hex characters, or paste a meshcore:// contact link';
@override
String get contacts_publicKeyInvalid =>
'Enter 64 hex characters, or a meshcore:// contact link';
@override
String get contacts_nameLabel => 'Name';
@override
String get contacts_typeLabel => 'Type';
@override
String get contacts_addByKeyUnverifiedNote =>
'Not confirmed on air yet. The name is whatever you type here until this node sends an advert.';
@override
String contacts_addByKeyAdded(String name) {
return 'Added $name';
}
@override
String get contacts_addByKeyFailed =>
'Could not add the contact. Check the radio connection.';
@override
String get contacts_ShareContact => '連絡先をクリップボードにコピー';

@ -4154,6 +4154,43 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get contacts_addContactFromClipboard => '복사본에서 연락처 추가';
@override
String get contacts_addByKey => 'Add by public key';
@override
String get contacts_addByKeyDescription =>
'Add someone using the public key they gave you. Works even if you have never heard their advert.';
@override
String get contacts_publicKeyLabel => 'Public key';
@override
String get contacts_publicKeyHelper =>
'64 hex characters, or paste a meshcore:// contact link';
@override
String get contacts_publicKeyInvalid =>
'Enter 64 hex characters, or a meshcore:// contact link';
@override
String get contacts_nameLabel => 'Name';
@override
String get contacts_typeLabel => 'Type';
@override
String get contacts_addByKeyUnverifiedNote =>
'Not confirmed on air yet. The name is whatever you type here until this node sends an advert.';
@override
String contacts_addByKeyAdded(String name) {
return 'Added $name';
}
@override
String get contacts_addByKeyFailed =>
'Could not add the contact. Check the radio connection.';
@override
String get contacts_ShareContact => '연락처를 복사';

@ -4335,6 +4335,43 @@ class AppLocalizationsNl extends AppLocalizations {
String get contacts_addContactFromClipboard =>
'Contact uit klembord toevoegen';
@override
String get contacts_addByKey => 'Add by public key';
@override
String get contacts_addByKeyDescription =>
'Add someone using the public key they gave you. Works even if you have never heard their advert.';
@override
String get contacts_publicKeyLabel => 'Public key';
@override
String get contacts_publicKeyHelper =>
'64 hex characters, or paste a meshcore:// contact link';
@override
String get contacts_publicKeyInvalid =>
'Enter 64 hex characters, or a meshcore:// contact link';
@override
String get contacts_nameLabel => 'Name';
@override
String get contacts_typeLabel => 'Type';
@override
String get contacts_addByKeyUnverifiedNote =>
'Not confirmed on air yet. The name is whatever you type here until this node sends an advert.';
@override
String contacts_addByKeyAdded(String name) {
return 'Added $name';
}
@override
String get contacts_addByKeyFailed =>
'Could not add the contact. Check the radio connection.';
@override
String get contacts_ShareContact => 'Contact naar Klembord kopiëren';

@ -4365,6 +4365,43 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get contacts_addContactFromClipboard => 'Dodaj kontakt z schowka';
@override
String get contacts_addByKey => 'Add by public key';
@override
String get contacts_addByKeyDescription =>
'Add someone using the public key they gave you. Works even if you have never heard their advert.';
@override
String get contacts_publicKeyLabel => 'Public key';
@override
String get contacts_publicKeyHelper =>
'64 hex characters, or paste a meshcore:// contact link';
@override
String get contacts_publicKeyInvalid =>
'Enter 64 hex characters, or a meshcore:// contact link';
@override
String get contacts_nameLabel => 'Name';
@override
String get contacts_typeLabel => 'Type';
@override
String get contacts_addByKeyUnverifiedNote =>
'Not confirmed on air yet. The name is whatever you type here until this node sends an advert.';
@override
String contacts_addByKeyAdded(String name) {
return 'Added $name';
}
@override
String get contacts_addByKeyFailed =>
'Could not add the contact. Check the radio connection.';
@override
String get contacts_ShareContact => 'Kopiuj kontakt do schowka';

@ -4347,6 +4347,43 @@ class AppLocalizationsPt extends AppLocalizations {
String get contacts_addContactFromClipboard =>
'Adicionar Contato da Área de Transferência';
@override
String get contacts_addByKey => 'Add by public key';
@override
String get contacts_addByKeyDescription =>
'Add someone using the public key they gave you. Works even if you have never heard their advert.';
@override
String get contacts_publicKeyLabel => 'Public key';
@override
String get contacts_publicKeyHelper =>
'64 hex characters, or paste a meshcore:// contact link';
@override
String get contacts_publicKeyInvalid =>
'Enter 64 hex characters, or a meshcore:// contact link';
@override
String get contacts_nameLabel => 'Name';
@override
String get contacts_typeLabel => 'Type';
@override
String get contacts_addByKeyUnverifiedNote =>
'Not confirmed on air yet. The name is whatever you type here until this node sends an advert.';
@override
String contacts_addByKeyAdded(String name) {
return 'Added $name';
}
@override
String get contacts_addByKeyFailed =>
'Could not add the contact. Check the radio connection.';
@override
String get contacts_ShareContact =>
'Copiar contato para Área de Transferência';

@ -4357,6 +4357,43 @@ class AppLocalizationsRu extends AppLocalizations {
String get contacts_addContactFromClipboard =>
'Добавить контакт из буфера обмена';
@override
String get contacts_addByKey => 'Add by public key';
@override
String get contacts_addByKeyDescription =>
'Add someone using the public key they gave you. Works even if you have never heard their advert.';
@override
String get contacts_publicKeyLabel => 'Public key';
@override
String get contacts_publicKeyHelper =>
'64 hex characters, or paste a meshcore:// contact link';
@override
String get contacts_publicKeyInvalid =>
'Enter 64 hex characters, or a meshcore:// contact link';
@override
String get contacts_nameLabel => 'Name';
@override
String get contacts_typeLabel => 'Type';
@override
String get contacts_addByKeyUnverifiedNote =>
'Not confirmed on air yet. The name is whatever you type here until this node sends an advert.';
@override
String contacts_addByKeyAdded(String name) {
return 'Added $name';
}
@override
String get contacts_addByKeyFailed =>
'Could not add the contact. Check the radio connection.';
@override
String get contacts_ShareContact => 'Копировать контакт в буфер обмена';

@ -4331,6 +4331,43 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get contacts_addContactFromClipboard => 'Pridať kontakt z schránky';
@override
String get contacts_addByKey => 'Add by public key';
@override
String get contacts_addByKeyDescription =>
'Add someone using the public key they gave you. Works even if you have never heard their advert.';
@override
String get contacts_publicKeyLabel => 'Public key';
@override
String get contacts_publicKeyHelper =>
'64 hex characters, or paste a meshcore:// contact link';
@override
String get contacts_publicKeyInvalid =>
'Enter 64 hex characters, or a meshcore:// contact link';
@override
String get contacts_nameLabel => 'Name';
@override
String get contacts_typeLabel => 'Type';
@override
String get contacts_addByKeyUnverifiedNote =>
'Not confirmed on air yet. The name is whatever you type here until this node sends an advert.';
@override
String contacts_addByKeyAdded(String name) {
return 'Added $name';
}
@override
String get contacts_addByKeyFailed =>
'Could not add the contact. Check the radio connection.';
@override
String get contacts_ShareContact => 'Kopírovať kontakt do schránky';

@ -4325,6 +4325,43 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get contacts_addContactFromClipboard => 'Dodaj stik iz odložišča';
@override
String get contacts_addByKey => 'Add by public key';
@override
String get contacts_addByKeyDescription =>
'Add someone using the public key they gave you. Works even if you have never heard their advert.';
@override
String get contacts_publicKeyLabel => 'Public key';
@override
String get contacts_publicKeyHelper =>
'64 hex characters, or paste a meshcore:// contact link';
@override
String get contacts_publicKeyInvalid =>
'Enter 64 hex characters, or a meshcore:// contact link';
@override
String get contacts_nameLabel => 'Name';
@override
String get contacts_typeLabel => 'Type';
@override
String get contacts_addByKeyUnverifiedNote =>
'Not confirmed on air yet. The name is whatever you type here until this node sends an advert.';
@override
String contacts_addByKeyAdded(String name) {
return 'Added $name';
}
@override
String get contacts_addByKeyFailed =>
'Could not add the contact. Check the radio connection.';
@override
String get contacts_ShareContact => 'Kopiraj stik v Odložišče';

@ -4304,6 +4304,43 @@ class AppLocalizationsSv extends AppLocalizations {
String get contacts_addContactFromClipboard =>
'Lägg till kontakt från urklipp';
@override
String get contacts_addByKey => 'Add by public key';
@override
String get contacts_addByKeyDescription =>
'Add someone using the public key they gave you. Works even if you have never heard their advert.';
@override
String get contacts_publicKeyLabel => 'Public key';
@override
String get contacts_publicKeyHelper =>
'64 hex characters, or paste a meshcore:// contact link';
@override
String get contacts_publicKeyInvalid =>
'Enter 64 hex characters, or a meshcore:// contact link';
@override
String get contacts_nameLabel => 'Name';
@override
String get contacts_typeLabel => 'Type';
@override
String get contacts_addByKeyUnverifiedNote =>
'Not confirmed on air yet. The name is whatever you type here until this node sends an advert.';
@override
String contacts_addByKeyAdded(String name) {
return 'Added $name';
}
@override
String get contacts_addByKeyFailed =>
'Could not add the contact. Check the radio connection.';
@override
String get contacts_ShareContact => 'Kopiera kontakt till Urklipp';

@ -4356,6 +4356,43 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get contacts_addContactFromClipboard => 'Додати контакт з буфера';
@override
String get contacts_addByKey => 'Add by public key';
@override
String get contacts_addByKeyDescription =>
'Add someone using the public key they gave you. Works even if you have never heard their advert.';
@override
String get contacts_publicKeyLabel => 'Public key';
@override
String get contacts_publicKeyHelper =>
'64 hex characters, or paste a meshcore:// contact link';
@override
String get contacts_publicKeyInvalid =>
'Enter 64 hex characters, or a meshcore:// contact link';
@override
String get contacts_nameLabel => 'Name';
@override
String get contacts_typeLabel => 'Type';
@override
String get contacts_addByKeyUnverifiedNote =>
'Not confirmed on air yet. The name is whatever you type here until this node sends an advert.';
@override
String contacts_addByKeyAdded(String name) {
return 'Added $name';
}
@override
String get contacts_addByKeyFailed =>
'Could not add the contact. Check the radio connection.';
@override
String get contacts_ShareContact => 'Копіювати контакт у буфер обміну';

@ -4055,6 +4055,43 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get contacts_addContactFromClipboard => '从剪贴板添加联系人';
@override
String get contacts_addByKey => 'Add by public key';
@override
String get contacts_addByKeyDescription =>
'Add someone using the public key they gave you. Works even if you have never heard their advert.';
@override
String get contacts_publicKeyLabel => 'Public key';
@override
String get contacts_publicKeyHelper =>
'64 hex characters, or paste a meshcore:// contact link';
@override
String get contacts_publicKeyInvalid =>
'Enter 64 hex characters, or a meshcore:// contact link';
@override
String get contacts_nameLabel => 'Name';
@override
String get contacts_typeLabel => 'Type';
@override
String get contacts_addByKeyUnverifiedNote =>
'Not confirmed on air yet. The name is whatever you type here until this node sends an advert.';
@override
String contacts_addByKeyAdded(String name) {
return 'Added $name';
}
@override
String get contacts_addByKeyFailed =>
'Could not add the contact. Check the radio connection.';
@override
String get contacts_ShareContact => '复制联系人信息到剪贴板';

@ -34,6 +34,7 @@ import '../widgets/path_selection_dialog.dart';
import '../widgets/repeater_login_dialog.dart';
import '../widgets/room_login_dialog.dart';
import '../widgets/sync_progress_overlay.dart';
import '../widgets/add_contact_by_key_dialog.dart';
import '../widgets/unread_badge.dart';
import '../helpers/snack_bar_builder.dart';
import 'channels_screen.dart';
@ -411,6 +412,19 @@ class _ContactsScreenState extends State<ContactsScreen>
),
),
),
// Provisional home so key entry is reachable. The proper
// add-contact surface, split away from the advert affordance,
// is #632 under epic #623.
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.key_outlined),
const SizedBox(width: 8),
Text(context.l10n.contacts_addByKey),
],
),
onTap: () => showAddContactByKeyDialog(context),
),
],
icon: const Icon(Icons.more_vert),
),

@ -0,0 +1,217 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../l10n/l10n.dart';
import '../helpers/snack_bar_builder.dart';
import '../models/contact.dart';
/// Adds a contact from an identity alone: a public key, a name and a type.
///
/// This is the manual half of the identity exchange (#628). It exists because
/// a public key is never present in channel traffic, so for anyone who has not
/// recently adverted there is otherwise no way to reach them at all (#620).
///
/// The field also accepts a whole `meshcore://contact/add` link, since that is
/// what someone is most likely to paste, and a QR is only that same link
/// rendered visually.
Future<void> showAddContactByKeyDialog(BuildContext context) {
return showDialog<void>(
context: context,
builder: (_) => const _AddContactByKeyDialog(),
);
}
class _AddContactByKeyDialog extends StatefulWidget {
const _AddContactByKeyDialog();
@override
State<_AddContactByKeyDialog> createState() => _AddContactByKeyDialogState();
}
class _AddContactByKeyDialogState extends State<_AddContactByKeyDialog> {
final _keyController = TextEditingController();
final _nameController = TextEditingController();
int _type = advTypeChat;
String? _keyError;
bool _submitting = false;
@override
void dispose() {
_keyController.dispose();
_nameController.dispose();
super.dispose();
}
/// Strips whitespace so a key copied across a line break still works.
String get _cleanedKey =>
_keyController.text.replaceAll(RegExp(r'\s+'), '').toLowerCase();
bool get _keyLooksValid => Contact.isValidShareUri(
Contact.buildShareUri(publicKeyHex: _cleanedKey, name: 'x', type: _type),
);
/// If a full contact link was pasted, absorb every field from it rather than
/// making the user retype a name they already have.
void _onKeyChanged(String raw) {
final pasted = Contact.fromShareUri(raw);
if (pasted != null) {
setState(() {
_keyController.value = TextEditingValue(
text: pasted.publicKeyHex,
selection: TextSelection.collapsed(
offset: pasted.publicKeyHex.length,
),
);
if (pasted.name != 'Unknown') _nameController.text = pasted.name;
_type = pasted.type;
_keyError = null;
});
return;
}
if (_keyError != null) setState(() => _keyError = null);
}
Future<void> _submit() async {
final l10n = context.l10n;
if (!_keyLooksValid) {
setState(() => _keyError = l10n.contacts_publicKeyInvalid);
return;
}
final name = _nameController.text.trim();
// Round-trip through the share URI so a manually typed contact and a
// scanned QR produce byte-identical state. One code path, no drift.
final stub = Contact.fromShareUri(
Contact.buildShareUri(publicKeyHex: _cleanedKey, name: name, type: _type),
);
if (stub == null) {
setState(() => _keyError = l10n.contacts_publicKeyInvalid);
return;
}
setState(() => _submitting = true);
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final added = await connector.addContactByKey(stub);
if (!mounted) return;
Navigator.of(context).pop();
showDismissibleSnackBar(
context,
content: Text(
added
? l10n.contacts_addByKeyAdded(stub.name)
: l10n.contacts_addByKeyFailed,
),
);
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final theme = Theme.of(context);
return AlertDialog(
title: Text(l10n.contacts_addByKey),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.contacts_addByKeyDescription,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 16),
TextField(
controller: _keyController,
autofocus: true,
maxLines: 2,
minLines: 1,
decoration: InputDecoration(
labelText: l10n.contacts_publicKeyLabel,
helperText: l10n.contacts_publicKeyHelper,
helperMaxLines: 2,
errorText: _keyError,
errorMaxLines: 2,
border: const OutlineInputBorder(),
),
onChanged: _onKeyChanged,
),
const SizedBox(height: 16),
TextField(
controller: _nameController,
decoration: InputDecoration(
labelText: l10n.contacts_nameLabel,
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 16),
DropdownButtonFormField<int>(
initialValue: _type,
decoration: InputDecoration(
labelText: l10n.contacts_typeLabel,
border: const OutlineInputBorder(),
),
items: [
DropdownMenuItem(
value: advTypeChat,
child: Text(l10n.contact_typeChat),
),
DropdownMenuItem(
value: advTypeRepeater,
child: Text(l10n.contact_typeRepeater),
),
DropdownMenuItem(
value: advTypeRoom,
child: Text(l10n.contact_typeRoom),
),
DropdownMenuItem(
value: advTypeSensor,
child: Text(l10n.contact_typeSensor),
),
],
onChanged: (v) => setState(() => _type = v ?? advTypeChat),
),
const SizedBox(height: 16),
// Honest about what a key-only add actually gives you. Deliberately
// informational, not a warning: nothing is wrong with this contact
// (#630).
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.info_outline,
size: 16,
color: theme.colorScheme.onSurfaceVariant,
),
const SizedBox(width: 8),
Expanded(
child: Text(
l10n.contacts_addByKeyUnverifiedNote,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
),
],
),
],
),
),
actions: [
TextButton(
onPressed: _submitting ? null : () => Navigator.of(context).pop(),
child: Text(l10n.common_cancel),
),
FilledButton(
onPressed: _submitting ? null : _submit,
child: Text(l10n.common_add),
),
],
);
}
}

@ -0,0 +1,155 @@
// Widget tests for manual key entry (#628).
//
// The point of this dialog is that it must produce exactly the same contact
// stub a scanned QR would, so these pin what actually reaches the connector:
// the key, the typed name, the chosen type, and the epoch lastSeen that keeps
// the firmware advert replay guard from muting the contact (#620).
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/connector/meshcore_protocol.dart';
import 'package:meshcore_open/l10n/app_localizations.dart';
import 'package:meshcore_open/models/contact.dart';
import 'package:meshcore_open/widgets/add_contact_by_key_dialog.dart';
const _key = '00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff';
class _CapturingConn extends MeshCoreConnector {
final List<Contact> added = [];
@override
Future<bool> addContactByKey(Contact stub) async {
added.add(stub);
return true;
}
}
Future<void> _open(WidgetTester tester, _CapturingConn conn) async {
await tester.pumpWidget(
ChangeNotifierProvider<MeshCoreConnector>.value(
value: conn,
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(
body: Builder(
builder: (context) => ElevatedButton(
onPressed: () => showAddContactByKeyDialog(context),
child: const Text('open'),
),
),
),
),
),
);
await tester.tap(find.text('open'));
await tester.pumpAndSettle();
}
void main() {
testWidgets('a valid key produces an unverified stub with the typed name', (
tester,
) async {
final conn = _CapturingConn();
await _open(tester, conn);
await tester.enterText(find.byType(TextField).first, _key);
await tester.enterText(find.byType(TextField).last, 'Ka8sbi');
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(FilledButton, 'Add'));
await tester.pumpAndSettle();
expect(conn.added, hasLength(1));
final stub = conn.added.single;
expect(stub.publicKeyHex, _key);
expect(stub.name, 'Ka8sbi');
expect(stub.type, advTypeChat);
// The reason this whole path exists. Anything else here and the contact
// goes permanently deaf to its own adverts.
expect(stub.lastSeen, DateTime.fromMillisecondsSinceEpoch(0));
expect(stub.pathLength, -1);
});
testWidgets('pasting a full contact link fills in every field', (
tester,
) async {
final conn = _CapturingConn();
await _open(tester, conn);
await tester.enterText(
find.byType(TextField).first,
'meshcore://contact/add?name=Two+Words&public_key=$_key&type=2',
);
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(FilledButton, 'Add'));
await tester.pumpAndSettle();
expect(conn.added, hasLength(1));
final stub = conn.added.single;
expect(stub.publicKeyHex, _key);
// Name and type came from the pasted link, not retyped by hand.
expect(stub.name, 'Two Words');
expect(stub.type, advTypeRepeater);
});
testWidgets('whitespace in a pasted key is tolerated', (tester) async {
final conn = _CapturingConn();
await _open(tester, conn);
// A key copied out of a chat log often arrives wrapped.
await tester.enterText(
find.byType(TextField).first,
'${_key.substring(0, 32)}\n${_key.substring(32)}',
);
await tester.enterText(find.byType(TextField).last, 'Bob');
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(FilledButton, 'Add'));
await tester.pumpAndSettle();
expect(conn.added, hasLength(1));
expect(conn.added.single.publicKeyHex, _key);
});
testWidgets('an invalid key shows an error and sends nothing', (
tester,
) async {
final conn = _CapturingConn();
await _open(tester, conn);
await tester.enterText(find.byType(TextField).first, 'not-a-key');
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(FilledButton, 'Add'));
await tester.pumpAndSettle();
expect(conn.added, isEmpty);
// Dialog stays open so the user can correct it.
expect(find.byType(AlertDialog), findsOneWidget);
expect(
find.textContaining('64 hex characters', findRichText: true),
findsWidgets,
);
});
testWidgets('a missing name falls back rather than blocking the add', (
tester,
) async {
final conn = _CapturingConn();
await _open(tester, conn);
await tester.enterText(find.byType(TextField).first, _key);
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(FilledButton, 'Add'));
await tester.pumpAndSettle();
expect(conn.added, hasLength(1));
expect(conn.added.single.name, 'Unknown');
});
}

@ -60,9 +60,21 @@
"channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"channel_senderNameUnknown",
"channel_senderNameUnknownHint",
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
"path_suggestedRoute",
"path_searchRepeaters",
"repeater_cliCommandTimeout",
"repeater_cliLateResponse",
"repeater_cliUnmatchedResponse",
"repeater_lateResponseDiscarded",
"repeater_lateResponseReceived",
"channelPath_routeDirect",
"channelPath_routeFlood",
"channelPath_snrLabel",
@ -82,6 +94,16 @@
"channels_qrExists",
"listFilter_sensors",
"contacts_repeaterPathTraceVia",
"contacts_addByKey",
"contacts_addByKeyDescription",
"contacts_publicKeyLabel",
"contacts_publicKeyHelper",
"contacts_publicKeyInvalid",
"contacts_nameLabel",
"contacts_typeLabel",
"contacts_addByKeyUnverifiedNote",
"contacts_addByKeyAdded",
"contacts_addByKeyFailed",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -168,9 +190,21 @@
"channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"channel_senderNameUnknown",
"channel_senderNameUnknownHint",
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
"path_suggestedRoute",
"path_searchRepeaters",
"repeater_cliCommandTimeout",
"repeater_cliLateResponse",
"repeater_cliUnmatchedResponse",
"repeater_lateResponseDiscarded",
"repeater_lateResponseReceived",
"channelPath_routeDirect",
"channelPath_routeFlood",
"channelPath_snrLabel",
@ -190,6 +224,16 @@
"channels_qrExists",
"listFilter_sensors",
"contacts_repeaterPathTraceVia",
"contacts_addByKey",
"contacts_addByKeyDescription",
"contacts_publicKeyLabel",
"contacts_publicKeyHelper",
"contacts_publicKeyInvalid",
"contacts_nameLabel",
"contacts_typeLabel",
"contacts_addByKeyUnverifiedNote",
"contacts_addByKeyAdded",
"contacts_addByKeyFailed",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -276,9 +320,21 @@
"channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"channel_senderNameUnknown",
"channel_senderNameUnknownHint",
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
"path_suggestedRoute",
"path_searchRepeaters",
"repeater_cliCommandTimeout",
"repeater_cliLateResponse",
"repeater_cliUnmatchedResponse",
"repeater_lateResponseDiscarded",
"repeater_lateResponseReceived",
"channelPath_routeDirect",
"channelPath_routeFlood",
"channelPath_snrLabel",
@ -298,6 +354,16 @@
"channels_qrExists",
"listFilter_sensors",
"contacts_repeaterPathTraceVia",
"contacts_addByKey",
"contacts_addByKeyDescription",
"contacts_publicKeyLabel",
"contacts_publicKeyHelper",
"contacts_publicKeyInvalid",
"contacts_nameLabel",
"contacts_typeLabel",
"contacts_addByKeyUnverifiedNote",
"contacts_addByKeyAdded",
"contacts_addByKeyFailed",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -384,9 +450,21 @@
"channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"channel_senderNameUnknown",
"channel_senderNameUnknownHint",
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
"path_suggestedRoute",
"path_searchRepeaters",
"repeater_cliCommandTimeout",
"repeater_cliLateResponse",
"repeater_cliUnmatchedResponse",
"repeater_lateResponseDiscarded",
"repeater_lateResponseReceived",
"channelPath_routeDirect",
"channelPath_routeFlood",
"channelPath_snrLabel",
@ -406,6 +484,16 @@
"channels_qrExists",
"listFilter_sensors",
"contacts_repeaterPathTraceVia",
"contacts_addByKey",
"contacts_addByKeyDescription",
"contacts_publicKeyLabel",
"contacts_publicKeyHelper",
"contacts_publicKeyInvalid",
"contacts_nameLabel",
"contacts_typeLabel",
"contacts_addByKeyUnverifiedNote",
"contacts_addByKeyAdded",
"contacts_addByKeyFailed",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -492,9 +580,21 @@
"channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"channel_senderNameUnknown",
"channel_senderNameUnknownHint",
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
"path_suggestedRoute",
"path_searchRepeaters",
"repeater_cliCommandTimeout",
"repeater_cliLateResponse",
"repeater_cliUnmatchedResponse",
"repeater_lateResponseDiscarded",
"repeater_lateResponseReceived",
"channelPath_routeDirect",
"channelPath_routeFlood",
"channelPath_snrLabel",
@ -514,6 +614,16 @@
"channels_qrExists",
"listFilter_sensors",
"contacts_repeaterPathTraceVia",
"contacts_addByKey",
"contacts_addByKeyDescription",
"contacts_publicKeyLabel",
"contacts_publicKeyHelper",
"contacts_publicKeyInvalid",
"contacts_nameLabel",
"contacts_typeLabel",
"contacts_addByKeyUnverifiedNote",
"contacts_addByKeyAdded",
"contacts_addByKeyFailed",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -600,9 +710,21 @@
"channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"channel_senderNameUnknown",
"channel_senderNameUnknownHint",
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
"path_suggestedRoute",
"path_searchRepeaters",
"repeater_cliCommandTimeout",
"repeater_cliLateResponse",
"repeater_cliUnmatchedResponse",
"repeater_lateResponseDiscarded",
"repeater_lateResponseReceived",
"channelPath_routeDirect",
"channelPath_routeFlood",
"channelPath_snrLabel",
@ -622,6 +744,16 @@
"channels_qrExists",
"listFilter_sensors",
"contacts_repeaterPathTraceVia",
"contacts_addByKey",
"contacts_addByKeyDescription",
"contacts_publicKeyLabel",
"contacts_publicKeyHelper",
"contacts_publicKeyInvalid",
"contacts_nameLabel",
"contacts_typeLabel",
"contacts_addByKeyUnverifiedNote",
"contacts_addByKeyAdded",
"contacts_addByKeyFailed",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -708,9 +840,21 @@
"channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"channel_senderNameUnknown",
"channel_senderNameUnknownHint",
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
"path_suggestedRoute",
"path_searchRepeaters",
"repeater_cliCommandTimeout",
"repeater_cliLateResponse",
"repeater_cliUnmatchedResponse",
"repeater_lateResponseDiscarded",
"repeater_lateResponseReceived",
"channelPath_routeDirect",
"channelPath_routeFlood",
"channelPath_snrLabel",
@ -730,6 +874,16 @@
"channels_qrExists",
"listFilter_sensors",
"contacts_repeaterPathTraceVia",
"contacts_addByKey",
"contacts_addByKeyDescription",
"contacts_publicKeyLabel",
"contacts_publicKeyHelper",
"contacts_publicKeyInvalid",
"contacts_nameLabel",
"contacts_typeLabel",
"contacts_addByKeyUnverifiedNote",
"contacts_addByKeyAdded",
"contacts_addByKeyFailed",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -816,9 +970,21 @@
"channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"channel_senderNameUnknown",
"channel_senderNameUnknownHint",
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
"path_suggestedRoute",
"path_searchRepeaters",
"repeater_cliCommandTimeout",
"repeater_cliLateResponse",
"repeater_cliUnmatchedResponse",
"repeater_lateResponseDiscarded",
"repeater_lateResponseReceived",
"channelPath_routeDirect",
"channelPath_routeFlood",
"channelPath_snrLabel",
@ -838,6 +1004,16 @@
"channels_qrExists",
"listFilter_sensors",
"contacts_repeaterPathTraceVia",
"contacts_addByKey",
"contacts_addByKeyDescription",
"contacts_publicKeyLabel",
"contacts_publicKeyHelper",
"contacts_publicKeyInvalid",
"contacts_nameLabel",
"contacts_typeLabel",
"contacts_addByKeyUnverifiedNote",
"contacts_addByKeyAdded",
"contacts_addByKeyFailed",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -924,9 +1100,21 @@
"channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"channel_senderNameUnknown",
"channel_senderNameUnknownHint",
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
"path_suggestedRoute",
"path_searchRepeaters",
"repeater_cliCommandTimeout",
"repeater_cliLateResponse",
"repeater_cliUnmatchedResponse",
"repeater_lateResponseDiscarded",
"repeater_lateResponseReceived",
"channelPath_routeDirect",
"channelPath_routeFlood",
"channelPath_snrLabel",
@ -946,6 +1134,16 @@
"channels_qrExists",
"listFilter_sensors",
"contacts_repeaterPathTraceVia",
"contacts_addByKey",
"contacts_addByKeyDescription",
"contacts_publicKeyLabel",
"contacts_publicKeyHelper",
"contacts_publicKeyInvalid",
"contacts_nameLabel",
"contacts_typeLabel",
"contacts_addByKeyUnverifiedNote",
"contacts_addByKeyAdded",
"contacts_addByKeyFailed",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -1032,9 +1230,21 @@
"channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"channel_senderNameUnknown",
"channel_senderNameUnknownHint",
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
"path_suggestedRoute",
"path_searchRepeaters",
"repeater_cliCommandTimeout",
"repeater_cliLateResponse",
"repeater_cliUnmatchedResponse",
"repeater_lateResponseDiscarded",
"repeater_lateResponseReceived",
"channelPath_routeDirect",
"channelPath_routeFlood",
"channelPath_snrLabel",
@ -1054,6 +1264,16 @@
"channels_qrExists",
"listFilter_sensors",
"contacts_repeaterPathTraceVia",
"contacts_addByKey",
"contacts_addByKeyDescription",
"contacts_publicKeyLabel",
"contacts_publicKeyHelper",
"contacts_publicKeyInvalid",
"contacts_nameLabel",
"contacts_typeLabel",
"contacts_addByKeyUnverifiedNote",
"contacts_addByKeyAdded",
"contacts_addByKeyFailed",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -1140,9 +1360,21 @@
"channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"channel_senderNameUnknown",
"channel_senderNameUnknownHint",
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
"path_suggestedRoute",
"path_searchRepeaters",
"repeater_cliCommandTimeout",
"repeater_cliLateResponse",
"repeater_cliUnmatchedResponse",
"repeater_lateResponseDiscarded",
"repeater_lateResponseReceived",
"channelPath_routeDirect",
"channelPath_routeFlood",
"channelPath_snrLabel",
@ -1162,6 +1394,16 @@
"channels_qrExists",
"listFilter_sensors",
"contacts_repeaterPathTraceVia",
"contacts_addByKey",
"contacts_addByKeyDescription",
"contacts_publicKeyLabel",
"contacts_publicKeyHelper",
"contacts_publicKeyInvalid",
"contacts_nameLabel",
"contacts_typeLabel",
"contacts_addByKeyUnverifiedNote",
"contacts_addByKeyAdded",
"contacts_addByKeyFailed",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -1248,9 +1490,21 @@
"channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"channel_senderNameUnknown",
"channel_senderNameUnknownHint",
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
"path_suggestedRoute",
"path_searchRepeaters",
"repeater_cliCommandTimeout",
"repeater_cliLateResponse",
"repeater_cliUnmatchedResponse",
"repeater_lateResponseDiscarded",
"repeater_lateResponseReceived",
"channelPath_routeDirect",
"channelPath_routeFlood",
"channelPath_snrLabel",
@ -1270,6 +1524,16 @@
"channels_qrExists",
"listFilter_sensors",
"contacts_repeaterPathTraceVia",
"contacts_addByKey",
"contacts_addByKeyDescription",
"contacts_publicKeyLabel",
"contacts_publicKeyHelper",
"contacts_publicKeyInvalid",
"contacts_nameLabel",
"contacts_typeLabel",
"contacts_addByKeyUnverifiedNote",
"contacts_addByKeyAdded",
"contacts_addByKeyFailed",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -1356,9 +1620,21 @@
"channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"channel_senderNameUnknown",
"channel_senderNameUnknownHint",
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
"path_suggestedRoute",
"path_searchRepeaters",
"repeater_cliCommandTimeout",
"repeater_cliLateResponse",
"repeater_cliUnmatchedResponse",
"repeater_lateResponseDiscarded",
"repeater_lateResponseReceived",
"channelPath_routeDirect",
"channelPath_routeFlood",
"channelPath_snrLabel",
@ -1378,6 +1654,16 @@
"channels_qrExists",
"listFilter_sensors",
"contacts_repeaterPathTraceVia",
"contacts_addByKey",
"contacts_addByKeyDescription",
"contacts_publicKeyLabel",
"contacts_publicKeyHelper",
"contacts_publicKeyInvalid",
"contacts_nameLabel",
"contacts_typeLabel",
"contacts_addByKeyUnverifiedNote",
"contacts_addByKeyAdded",
"contacts_addByKeyFailed",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -1464,9 +1750,21 @@
"channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"channel_senderNameUnknown",
"channel_senderNameUnknownHint",
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
"path_suggestedRoute",
"path_searchRepeaters",
"repeater_cliCommandTimeout",
"repeater_cliLateResponse",
"repeater_cliUnmatchedResponse",
"repeater_lateResponseDiscarded",
"repeater_lateResponseReceived",
"channelPath_routeDirect",
"channelPath_routeFlood",
"channelPath_snrLabel",
@ -1486,6 +1784,16 @@
"channels_qrExists",
"listFilter_sensors",
"contacts_repeaterPathTraceVia",
"contacts_addByKey",
"contacts_addByKeyDescription",
"contacts_publicKeyLabel",
"contacts_publicKeyHelper",
"contacts_publicKeyInvalid",
"contacts_nameLabel",
"contacts_typeLabel",
"contacts_addByKeyUnverifiedNote",
"contacts_addByKeyAdded",
"contacts_addByKeyFailed",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -1572,9 +1880,21 @@
"channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"channel_senderNameUnknown",
"channel_senderNameUnknownHint",
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
"path_suggestedRoute",
"path_searchRepeaters",
"repeater_cliCommandTimeout",
"repeater_cliLateResponse",
"repeater_cliUnmatchedResponse",
"repeater_lateResponseDiscarded",
"repeater_lateResponseReceived",
"channelPath_routeDirect",
"channelPath_routeFlood",
"channelPath_snrLabel",
@ -1594,6 +1914,16 @@
"channels_qrExists",
"listFilter_sensors",
"contacts_repeaterPathTraceVia",
"contacts_addByKey",
"contacts_addByKeyDescription",
"contacts_publicKeyLabel",
"contacts_publicKeyHelper",
"contacts_publicKeyInvalid",
"contacts_nameLabel",
"contacts_typeLabel",
"contacts_addByKeyUnverifiedNote",
"contacts_addByKeyAdded",
"contacts_addByKeyFailed",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -1680,9 +2010,21 @@
"channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"channel_senderNameUnknown",
"channel_senderNameUnknownHint",
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
"path_suggestedRoute",
"path_searchRepeaters",
"repeater_cliCommandTimeout",
"repeater_cliLateResponse",
"repeater_cliUnmatchedResponse",
"repeater_lateResponseDiscarded",
"repeater_lateResponseReceived",
"channelPath_routeDirect",
"channelPath_routeFlood",
"channelPath_snrLabel",
@ -1702,6 +2044,16 @@
"channels_qrExists",
"listFilter_sensors",
"contacts_repeaterPathTraceVia",
"contacts_addByKey",
"contacts_addByKeyDescription",
"contacts_publicKeyLabel",
"contacts_publicKeyHelper",
"contacts_publicKeyInvalid",
"contacts_nameLabel",
"contacts_typeLabel",
"contacts_addByKeyUnverifiedNote",
"contacts_addByKeyAdded",
"contacts_addByKeyFailed",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -1788,9 +2140,21 @@
"channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"channel_senderNameUnknown",
"channel_senderNameUnknownHint",
"channel_senderAlreadyContact",
"channel_senderMultipleMatches",
"channel_senderAddNotConnected",
"channel_senderAddFailed",
"channel_senderCandidate",
"map_alwaysShowNames",
"path_suggestedRoute",
"path_searchRepeaters",
"repeater_cliCommandTimeout",
"repeater_cliLateResponse",
"repeater_cliUnmatchedResponse",
"repeater_lateResponseDiscarded",
"repeater_lateResponseReceived",
"channelPath_routeDirect",
"channelPath_routeFlood",
"channelPath_snrLabel",
@ -1810,6 +2174,16 @@
"channels_qrExists",
"listFilter_sensors",
"contacts_repeaterPathTraceVia",
"contacts_addByKey",
"contacts_addByKeyDescription",
"contacts_publicKeyLabel",
"contacts_publicKeyHelper",
"contacts_publicKeyInvalid",
"contacts_nameLabel",
"contacts_typeLabel",
"contacts_addByKeyUnverifiedNote",
"contacts_addByKeyAdded",
"contacts_addByKeyFailed",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",

Loading…
Cancel
Save

Powered by TurnKey Linux.