diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 43cacc99..b8dd623d 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -67,5 +67,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index b4e35edb..92ebc463 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -55,5 +55,10 @@
This app uses Bluetooth to communicate with MeshCore devices.
NSCameraUsageDescription
This app uses the camera to scan QR codes for joining communities.
+ LSApplicationQueriesSchemes
+
+ http
+ https
+
diff --git a/lib/helpers/link_handler.dart b/lib/helpers/link_handler.dart
new file mode 100644
index 00000000..fa8e5ffd
--- /dev/null
+++ b/lib/helpers/link_handler.dart
@@ -0,0 +1,76 @@
+import 'package:flutter/material.dart';
+import 'package:url_launcher/url_launcher.dart';
+import '../l10n/l10n.dart';
+
+class LinkHandler {
+ static Future handleLinkTap(BuildContext context, String url) async {
+ // Show confirmation dialog
+ final shouldOpen = await showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: Text(context.l10n.chat_openLink),
+ content: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ context.l10n.chat_openLinkConfirmation,
+ style: const TextStyle(fontSize: 14),
+ ),
+ const SizedBox(height: 16),
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: SelectableText(
+ url,
+ style: const TextStyle(
+ fontSize: 12,
+ fontFamily: 'monospace',
+ ),
+ ),
+ ),
+ ],
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context, false),
+ child: Text(context.l10n.common_cancel),
+ ),
+ FilledButton(
+ onPressed: () => Navigator.pop(context, true),
+ child: Text(context.l10n.chat_open),
+ ),
+ ],
+ ),
+ );
+
+ if (shouldOpen != true) return;
+
+ // Launch URL
+ try {
+ final uri = Uri.parse(url);
+ if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) {
+ if (context.mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(context.l10n.chat_couldNotOpenLink(url)),
+ backgroundColor: Colors.red,
+ ),
+ );
+ }
+ }
+ } catch (e) {
+ if (context.mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(context.l10n.chat_invalidLink),
+ backgroundColor: Colors.red,
+ ),
+ );
+ }
+ }
+ }
+}
diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb
index 1b5e5de3..e5f40f38 100644
--- a/lib/l10n/app_bg.arb
+++ b/lib/l10n/app_bg.arb
@@ -604,6 +604,18 @@
}
}
},
+ "chat_openLink": "Отваряне на връзката?",
+ "chat_openLinkConfirmation": "Искате ли да отворите тази връзка в браузъра си?",
+ "chat_open": "Отвори",
+ "chat_couldNotOpenLink": "Не можа да се отвори връзката: {url}",
+ "@chat_couldNotOpenLink": {
+ "placeholders": {
+ "url": {
+ "type": "String"
+ }
+ }
+ },
+ "chat_invalidLink": "Невалиден формат на връзката",
"map_title": "Карта на възлите",
"map_noNodesWithLocation": "Няма възли с данни за местоположение.",
"map_nodesNeedGps": "Възлагат се възлозите да споделят техните GPS координати,\nза да се появят на картата.",
@@ -1473,7 +1485,9 @@
"community_deleteChannelsWarning": "Това ще изтрие също {count} канал(а) и техните съобщения.",
"@community_deleteChannelsWarning": {
"placeholders": {
- "count": {"type": "int"}
+ "count": {
+ "type": "int"
+ }
}
},
"community_deleted": "Остави общността \"{name}\"",
@@ -1484,5 +1498,40 @@
"community_regularHashtagDesc": "Общ хаштаг (всеки може да се присъедини)",
"community_communityHashtag": "Общностен хаштаг",
"community_communityHashtagDesc": "Само за членове на общността",
- "community_forCommunity": "За {name}"
+ "community_forCommunity": "За {name}",
+ "@community_regenerateSecretConfirm": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_secretRegenerated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_secretUpdated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_scanToUpdateSecret": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_regenerateSecretConfirm": "Регенерация на секретния ключ за \"{name}\"? Всички членове ще трябва да сканират новия QR код, за да продължат комуникацията.",
+ "community_secretRegenerated": "Секретно презареждане за \"{name}\"",
+ "community_regenerateSecret": "Регенерейрай секрет",
+ "community_regenerate": "Регенерация",
+ "community_updateSecret": "Актуализирай тайна",
+ "community_scanToUpdateSecret": "Сканьорвайте новия QR код, за да актуализирате секрета за \"{name}\"",
+ "community_secretUpdated": "Секретно обновено за \"{name}\""
}
diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb
index 07f395a6..0bb17c84 100644
--- a/lib/l10n/app_de.arb
+++ b/lib/l10n/app_de.arb
@@ -604,6 +604,18 @@
}
}
},
+ "chat_openLink": "Link öffnen?",
+ "chat_openLinkConfirmation": "Möchten Sie diesen Link in Ihrem Browser öffnen?",
+ "chat_open": "Öffnen",
+ "chat_couldNotOpenLink": "Link konnte nicht geöffnet werden: {url}",
+ "@chat_couldNotOpenLink": {
+ "placeholders": {
+ "url": {
+ "type": "String"
+ }
+ }
+ },
+ "chat_invalidLink": "Ungültiges Link-Format",
"map_title": "Karte",
"map_noNodesWithLocation": "Keine Knoten mit Standortdaten",
"map_nodesNeedGps": "Knoten müssen ihre GPS-Koordinaten teilen,\num auf der Karte zu erscheinen.",
@@ -1473,7 +1485,9 @@
"community_deleteChannelsWarning": "Dies löscht auch {count} Kanal/Kanäle und deren Nachrichten.",
"@community_deleteChannelsWarning": {
"placeholders": {
- "count": {"type": "int"}
+ "count": {
+ "type": "int"
+ }
}
},
"community_deleted": "Community \"{name}\" verlassen",
@@ -1484,5 +1498,40 @@
"community_regularHashtagDesc": "Öffentliches Hashtag (jeder kann teilnehmen)",
"community_communityHashtagDesc": "Nur für Mitglieder der Community",
"community_forCommunity": "Für {name}",
- "community_communityHashtag": "Community Hashtag"
+ "community_communityHashtag": "Community Hashtag",
+ "@community_regenerateSecretConfirm": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_secretRegenerated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_secretUpdated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_scanToUpdateSecret": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_regenerate": "Neu generieren",
+ "community_secretRegenerated": "Geheime Wiederherstellung für \"{name}\" erfolgreich",
+ "community_regenerateSecretConfirm": "Nehmen Sie den geheimen Schlüssel für \"{name}\" neu auf? Alle Mitglieder müssen den neuen QR-Code scannen, um die Kommunikation fortzusetzen.",
+ "community_regenerateSecret": "Neu generieren Sie das Geheimnis",
+ "community_secretUpdated": "Geheime für \"{name}\" aktualisiert",
+ "community_scanToUpdateSecret": "Scannen Sie den neuen QR-Code, um das Geheimnis für \"{name}\" zu aktualisieren.",
+ "community_updateSecret": "Aktualisieren Sie das Geheimnis"
}
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index 1c1ee514..56cb1cc1 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -550,6 +550,16 @@
"count": {"type": "int"}
}
},
+ "chat_openLink": "Open Link?",
+ "chat_openLinkConfirmation": "Do you want to open this link in your browser?",
+ "chat_open": "Open",
+ "chat_couldNotOpenLink": "Could not open link: {url}",
+ "@chat_couldNotOpenLink": {
+ "placeholders": {
+ "url": {"type": "String"}
+ }
+ },
+ "chat_invalidLink": "Invalid link format",
"map_title": "Node Map",
"map_noNodesWithLocation": "No nodes with location data",
diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb
index b406e942..4b6b5262 100644
--- a/lib/l10n/app_es.arb
+++ b/lib/l10n/app_es.arb
@@ -604,6 +604,18 @@
}
}
},
+ "chat_openLink": "¿Abrir enlace?",
+ "chat_openLinkConfirmation": "¿Quiere abrir este enlace en su navegador?",
+ "chat_open": "Abrir",
+ "chat_couldNotOpenLink": "No se pudo abrir el enlace: {url}",
+ "@chat_couldNotOpenLink": {
+ "placeholders": {
+ "url": {
+ "type": "String"
+ }
+ }
+ },
+ "chat_invalidLink": "Formato de enlace no válido",
"map_title": "Mapa de Nodos",
"map_noNodesWithLocation": "No hay nodos con datos de ubicación",
"map_nodesNeedGps": "Los nodos necesitan compartir sus coordenadas GPS\npara aparecer en el mapa",
@@ -1473,7 +1485,9 @@
"community_deleteChannelsWarning": "Esto también eliminará {count} canal(es) y sus mensajes.",
"@community_deleteChannelsWarning": {
"placeholders": {
- "count": {"type": "int"}
+ "count": {
+ "type": "int"
+ }
}
},
"community_deleted": "Has salido de la comunidad \"{name}\"",
@@ -1484,5 +1498,40 @@
"community_regularHashtagDesc": "Hashtag público (cualquiera puede unirse)",
"community_communityHashtag": "Hashtag de la Comunidad",
"community_communityHashtagDesc": "Exclusivo para miembros de la comunidad",
- "community_forCommunity": "Para {name}"
+ "community_forCommunity": "Para {name}",
+ "@community_regenerateSecretConfirm": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_secretRegenerated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_secretUpdated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_scanToUpdateSecret": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_regenerateSecret": "Regenerar Contraseña Secreta",
+ "community_regenerateSecretConfirm": "Regenerar la clave secreta para \"{name}\"? Todos los miembros deberán escanear el nuevo código QR para seguir comunicándose.",
+ "community_secretRegenerated": "Código secreto regenerado para \"{name}\"",
+ "community_regenerate": "Regenerar",
+ "community_secretUpdated": "Confidencialidad actualizada para \"{name}\"",
+ "community_scanToUpdateSecret": "Escanear el nuevo código QR para actualizar el secreto de \"{name}\"",
+ "community_updateSecret": "Actualizar Contraseña"
}
diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb
index 785ee377..1b8d35d1 100644
--- a/lib/l10n/app_fr.arb
+++ b/lib/l10n/app_fr.arb
@@ -604,6 +604,18 @@
}
}
},
+ "chat_openLink": "Ouvrir le lien ?",
+ "chat_openLinkConfirmation": "Voulez-vous ouvrir ce lien dans votre navigateur ?",
+ "chat_open": "Ouvrir",
+ "chat_couldNotOpenLink": "Impossible d'ouvrir le lien : {url}",
+ "@chat_couldNotOpenLink": {
+ "placeholders": {
+ "url": {
+ "type": "String"
+ }
+ }
+ },
+ "chat_invalidLink": "Format de lien invalide",
"map_title": "Carte des nœuds",
"map_noNodesWithLocation": "Aucun nœud avec des données de localisation",
"map_nodesNeedGps": "Les nœuds doivent partager leurs coordonnées GPS\npour apparaître sur la carte.",
@@ -1473,7 +1485,9 @@
"community_deleteChannelsWarning": "Cela supprimera également {count} canal/canaux et leurs messages.",
"@community_deleteChannelsWarning": {
"placeholders": {
- "count": {"type": "int"}
+ "count": {
+ "type": "int"
+ }
}
},
"community_deleted": "Communauté \"{name}\" quittée",
@@ -1484,5 +1498,40 @@
"community_regularHashtagDesc": "Hashtag public (tout le monde peut rejoindre)",
"community_communityHashtag": "Hashtag de la communauté",
"community_communityHashtagDesc": "Exclusif aux membres de la communauté",
- "community_forCommunity": "Pour {name}"
+ "community_forCommunity": "Pour {name}",
+ "@community_regenerateSecretConfirm": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_secretRegenerated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_secretUpdated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_scanToUpdateSecret": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_regenerateSecret": "Régénérer le secret",
+ "community_regenerateSecretConfirm": "Régénérer la clé secrète pour \"{name}\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.",
+ "community_regenerate": "Régénérer",
+ "community_secretRegenerated": "Mot de passe secret régénéré pour \"{name}\"",
+ "community_scanToUpdateSecret": "Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"{name}\"",
+ "community_updateSecret": "Mettre à jour le secret",
+ "community_secretUpdated": "Modification secrète mise à jour pour \"{name}\""
}
diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb
index b0c13a00..cd031fbf 100644
--- a/lib/l10n/app_it.arb
+++ b/lib/l10n/app_it.arb
@@ -604,6 +604,18 @@
}
}
},
+ "chat_openLink": "Aprire il link?",
+ "chat_openLinkConfirmation": "Vuoi aprire questo link nel tuo browser?",
+ "chat_open": "Apri",
+ "chat_couldNotOpenLink": "Impossibile aprire il link: {url}",
+ "@chat_couldNotOpenLink": {
+ "placeholders": {
+ "url": {
+ "type": "String"
+ }
+ }
+ },
+ "chat_invalidLink": "Formato di link non valido",
"map_title": "Mappa Nodi",
"map_noNodesWithLocation": "Nessun nodo con dati di posizione",
"map_nodesNeedGps": "I nodi devono condividere le loro coordinate GPS\nper apparire sulla mappa",
@@ -1473,7 +1485,9 @@
"community_deleteChannelsWarning": "Questo eliminerà anche {count} canale/i e i loro messaggi.",
"@community_deleteChannelsWarning": {
"placeholders": {
- "count": {"type": "int"}
+ "count": {
+ "type": "int"
+ }
}
},
"community_deleted": "Hai lasciato la comunità \"{name}\"",
@@ -1484,5 +1498,40 @@
"community_regularHashtagDesc": "Hashtag pubblico (chiunque può unirsi)",
"community_communityHashtag": "Hashtag della Comunità",
"community_communityHashtagDesc": "Visibile solo ai membri della comunità",
- "community_forCommunity": "Per {name}"
+ "community_forCommunity": "Per {name}",
+ "@community_regenerateSecretConfirm": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_secretRegenerated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_secretUpdated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_scanToUpdateSecret": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_regenerateSecretConfirm": "Regenera la chiave segreta per \"{name}\"? Tutti i membri dovranno scansionare il nuovo codice QR per continuare a comunicare.",
+ "community_regenerateSecret": "Ri genera la chiave segreta",
+ "community_regenerate": "Rigenera",
+ "community_secretRegenerated": "Codice segreto rigenerato per \"{name}\"",
+ "community_updateSecret": "Aggiorna Segreto",
+ "community_secretUpdated": "Segreto aggiornato per \"{name}\"",
+ "community_scanToUpdateSecret": "Scansiona il nuovo codice QR per aggiornare il segreto di \"{name}\""
}
diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart
index fe4fc016..d52830ca 100644
--- a/lib/l10n/app_localizations.dart
+++ b/lib/l10n/app_localizations.dart
@@ -2226,6 +2226,36 @@ abstract class AppLocalizations {
/// **'Unread: {count}'**
String chat_unread(int count);
+ /// No description provided for @chat_openLink.
+ ///
+ /// In en, this message translates to:
+ /// **'Open Link?'**
+ String get chat_openLink;
+
+ /// No description provided for @chat_openLinkConfirmation.
+ ///
+ /// In en, this message translates to:
+ /// **'Do you want to open this link in your browser?'**
+ String get chat_openLinkConfirmation;
+
+ /// No description provided for @chat_open.
+ ///
+ /// In en, this message translates to:
+ /// **'Open'**
+ String get chat_open;
+
+ /// No description provided for @chat_couldNotOpenLink.
+ ///
+ /// In en, this message translates to:
+ /// **'Could not open link: {url}'**
+ String chat_couldNotOpenLink(String url);
+
+ /// No description provided for @chat_invalidLink.
+ ///
+ /// In en, this message translates to:
+ /// **'Invalid link format'**
+ String get chat_invalidLink;
+
/// No description provided for @map_title.
///
/// In en, this message translates to:
diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart
index 314e702f..9b70d9c4 100644
--- a/lib/l10n/app_localizations_bg.dart
+++ b/lib/l10n/app_localizations_bg.dart
@@ -1207,6 +1207,24 @@ class AppLocalizationsBg extends AppLocalizations {
return 'Непрочетени: $count';
}
+ @override
+ String get chat_openLink => 'Отваряне на връзката?';
+
+ @override
+ String get chat_openLinkConfirmation =>
+ 'Искате ли да отворите тази връзка в браузъра си?';
+
+ @override
+ String get chat_open => 'Отвори';
+
+ @override
+ String chat_couldNotOpenLink(String url) {
+ return 'Не можа да се отвори връзката: $url';
+ }
+
+ @override
+ String get chat_invalidLink => 'Невалиден формат на връзката';
+
@override
String get map_title => 'Карта на възлите';
@@ -2567,32 +2585,32 @@ class AppLocalizationsBg extends AppLocalizations {
}
@override
- String get community_regenerateSecret => 'Regenerate Secret';
+ String get community_regenerateSecret => 'Регенерейрай секрет';
@override
String community_regenerateSecretConfirm(String name) {
- return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
+ return 'Регенерация на секретния ключ за \"$name\"? Всички членове ще трябва да сканират новия QR код, за да продължат комуникацията.';
}
@override
- String get community_regenerate => 'Regenerate';
+ String get community_regenerate => 'Регенерация';
@override
String community_secretRegenerated(String name) {
- return 'Secret regenerated for \"$name\"';
+ return 'Секретно презареждане за \"$name\"';
}
@override
- String get community_updateSecret => 'Update Secret';
+ String get community_updateSecret => 'Актуализирай тайна';
@override
String community_secretUpdated(String name) {
- return 'Secret updated for \"$name\"';
+ return 'Секретно обновено за \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
- return 'Scan the new QR code to update the secret for \"$name\"';
+ return 'Сканьорвайте новия QR код, за да актуализирате секрета за \"$name\"';
}
@override
diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart
index b884f3c1..9bab237e 100644
--- a/lib/l10n/app_localizations_de.dart
+++ b/lib/l10n/app_localizations_de.dart
@@ -1206,6 +1206,24 @@ class AppLocalizationsDe extends AppLocalizations {
return 'Ungelesen: $count';
}
+ @override
+ String get chat_openLink => 'Link öffnen?';
+
+ @override
+ String get chat_openLinkConfirmation =>
+ 'Möchten Sie diesen Link in Ihrem Browser öffnen?';
+
+ @override
+ String get chat_open => 'Öffnen';
+
+ @override
+ String chat_couldNotOpenLink(String url) {
+ return 'Link konnte nicht geöffnet werden: $url';
+ }
+
+ @override
+ String get chat_invalidLink => 'Ungültiges Link-Format';
+
@override
String get map_title => 'Karte';
@@ -2570,32 +2588,32 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
- String get community_regenerateSecret => 'Regenerate Secret';
+ String get community_regenerateSecret => 'Neu generieren Sie das Geheimnis';
@override
String community_regenerateSecretConfirm(String name) {
- return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
+ return 'Nehmen Sie den geheimen Schlüssel für \"$name\" neu auf? Alle Mitglieder müssen den neuen QR-Code scannen, um die Kommunikation fortzusetzen.';
}
@override
- String get community_regenerate => 'Regenerate';
+ String get community_regenerate => 'Neu generieren';
@override
String community_secretRegenerated(String name) {
- return 'Secret regenerated for \"$name\"';
+ return 'Geheime Wiederherstellung für \"$name\" erfolgreich';
}
@override
- String get community_updateSecret => 'Update Secret';
+ String get community_updateSecret => 'Aktualisieren Sie das Geheimnis';
@override
String community_secretUpdated(String name) {
- return 'Secret updated for \"$name\"';
+ return 'Geheime für \"$name\" aktualisiert';
}
@override
String community_scanToUpdateSecret(String name) {
- return 'Scan the new QR code to update the secret for \"$name\"';
+ return 'Scannen Sie den neuen QR-Code, um das Geheimnis für \"$name\" zu aktualisieren.';
}
@override
diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart
index 96ba1b97..86f18ba2 100644
--- a/lib/l10n/app_localizations_en.dart
+++ b/lib/l10n/app_localizations_en.dart
@@ -1186,6 +1186,24 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Unread: $count';
}
+ @override
+ String get chat_openLink => 'Open Link?';
+
+ @override
+ String get chat_openLinkConfirmation =>
+ 'Do you want to open this link in your browser?';
+
+ @override
+ String get chat_open => 'Open';
+
+ @override
+ String chat_couldNotOpenLink(String url) {
+ return 'Could not open link: $url';
+ }
+
+ @override
+ String get chat_invalidLink => 'Invalid link format';
+
@override
String get map_title => 'Node Map';
diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart
index 029ed11e..908c88c8 100644
--- a/lib/l10n/app_localizations_es.dart
+++ b/lib/l10n/app_localizations_es.dart
@@ -1204,6 +1204,24 @@ class AppLocalizationsEs extends AppLocalizations {
return 'Sin leer: $count';
}
+ @override
+ String get chat_openLink => '¿Abrir enlace?';
+
+ @override
+ String get chat_openLinkConfirmation =>
+ '¿Quiere abrir este enlace en su navegador?';
+
+ @override
+ String get chat_open => 'Abrir';
+
+ @override
+ String chat_couldNotOpenLink(String url) {
+ return 'No se pudo abrir el enlace: $url';
+ }
+
+ @override
+ String get chat_invalidLink => 'Formato de enlace no válido';
+
@override
String get map_title => 'Mapa de Nodos';
@@ -2565,32 +2583,32 @@ class AppLocalizationsEs extends AppLocalizations {
}
@override
- String get community_regenerateSecret => 'Regenerate Secret';
+ String get community_regenerateSecret => 'Regenerar Contraseña Secreta';
@override
String community_regenerateSecretConfirm(String name) {
- return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
+ return 'Regenerar la clave secreta para \"$name\"? Todos los miembros deberán escanear el nuevo código QR para seguir comunicándose.';
}
@override
- String get community_regenerate => 'Regenerate';
+ String get community_regenerate => 'Regenerar';
@override
String community_secretRegenerated(String name) {
- return 'Secret regenerated for \"$name\"';
+ return 'Código secreto regenerado para \"$name\"';
}
@override
- String get community_updateSecret => 'Update Secret';
+ String get community_updateSecret => 'Actualizar Contraseña';
@override
String community_secretUpdated(String name) {
- return 'Secret updated for \"$name\"';
+ return 'Confidencialidad actualizada para \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
- return 'Scan the new QR code to update the secret for \"$name\"';
+ return 'Escanear el nuevo código QR para actualizar el secreto de \"$name\"';
}
@override
diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart
index 1dce57f6..48a6ac48 100644
--- a/lib/l10n/app_localizations_fr.dart
+++ b/lib/l10n/app_localizations_fr.dart
@@ -1209,6 +1209,24 @@ class AppLocalizationsFr extends AppLocalizations {
return 'Non lu : $count';
}
+ @override
+ String get chat_openLink => 'Ouvrir le lien ?';
+
+ @override
+ String get chat_openLinkConfirmation =>
+ 'Voulez-vous ouvrir ce lien dans votre navigateur ?';
+
+ @override
+ String get chat_open => 'Ouvrir';
+
+ @override
+ String chat_couldNotOpenLink(String url) {
+ return 'Impossible d\'ouvrir le lien : $url';
+ }
+
+ @override
+ String get chat_invalidLink => 'Format de lien invalide';
+
@override
String get map_title => 'Carte des nœuds';
@@ -2581,32 +2599,32 @@ class AppLocalizationsFr extends AppLocalizations {
}
@override
- String get community_regenerateSecret => 'Regenerate Secret';
+ String get community_regenerateSecret => 'Régénérer le secret';
@override
String community_regenerateSecretConfirm(String name) {
- return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
+ return 'Régénérer la clé secrète pour \"$name\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.';
}
@override
- String get community_regenerate => 'Regenerate';
+ String get community_regenerate => 'Régénérer';
@override
String community_secretRegenerated(String name) {
- return 'Secret regenerated for \"$name\"';
+ return 'Mot de passe secret régénéré pour \"$name\"';
}
@override
- String get community_updateSecret => 'Update Secret';
+ String get community_updateSecret => 'Mettre à jour le secret';
@override
String community_secretUpdated(String name) {
- return 'Secret updated for \"$name\"';
+ return 'Modification secrète mise à jour pour \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
- return 'Scan the new QR code to update the secret for \"$name\"';
+ return 'Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"$name\"';
}
@override
diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart
index 20df619a..83010d8d 100644
--- a/lib/l10n/app_localizations_it.dart
+++ b/lib/l10n/app_localizations_it.dart
@@ -1203,6 +1203,24 @@ class AppLocalizationsIt extends AppLocalizations {
return 'Non letti: $count';
}
+ @override
+ String get chat_openLink => 'Aprire il link?';
+
+ @override
+ String get chat_openLinkConfirmation =>
+ 'Vuoi aprire questo link nel tuo browser?';
+
+ @override
+ String get chat_open => 'Apri';
+
+ @override
+ String chat_couldNotOpenLink(String url) {
+ return 'Impossibile aprire il link: $url';
+ }
+
+ @override
+ String get chat_invalidLink => 'Formato di link non valido';
+
@override
String get map_title => 'Mappa Nodi';
@@ -2565,32 +2583,32 @@ class AppLocalizationsIt extends AppLocalizations {
}
@override
- String get community_regenerateSecret => 'Regenerate Secret';
+ String get community_regenerateSecret => 'Ri genera la chiave segreta';
@override
String community_regenerateSecretConfirm(String name) {
- return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
+ return 'Regenera la chiave segreta per \"$name\"? Tutti i membri dovranno scansionare il nuovo codice QR per continuare a comunicare.';
}
@override
- String get community_regenerate => 'Regenerate';
+ String get community_regenerate => 'Rigenera';
@override
String community_secretRegenerated(String name) {
- return 'Secret regenerated for \"$name\"';
+ return 'Codice segreto rigenerato per \"$name\"';
}
@override
- String get community_updateSecret => 'Update Secret';
+ String get community_updateSecret => 'Aggiorna Segreto';
@override
String community_secretUpdated(String name) {
- return 'Secret updated for \"$name\"';
+ return 'Segreto aggiornato per \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
- return 'Scan the new QR code to update the secret for \"$name\"';
+ return 'Scansiona il nuovo codice QR per aggiornare il segreto di \"$name\"';
}
@override
diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart
index 50f5744b..ce60a8f8 100644
--- a/lib/l10n/app_localizations_nl.dart
+++ b/lib/l10n/app_localizations_nl.dart
@@ -1199,6 +1199,24 @@ class AppLocalizationsNl extends AppLocalizations {
return 'Nieuw: $count';
}
+ @override
+ String get chat_openLink => 'Link openen?';
+
+ @override
+ String get chat_openLinkConfirmation =>
+ 'Wilt u deze link in uw browser openen?';
+
+ @override
+ String get chat_open => 'Openen';
+
+ @override
+ String chat_couldNotOpenLink(String url) {
+ return 'Kan link niet openen: $url';
+ }
+
+ @override
+ String get chat_invalidLink => 'Ongeldig linkformaat';
+
@override
String get map_title => 'Node Map';
@@ -2556,32 +2574,32 @@ class AppLocalizationsNl extends AppLocalizations {
}
@override
- String get community_regenerateSecret => 'Regenerate Secret';
+ String get community_regenerateSecret => 'Regeneer Geheimwoord';
@override
String community_regenerateSecretConfirm(String name) {
- return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
+ return 'Regeneere de geheime sleutel voor \"$name\"? Alle leden moeten de nieuwe QR-code scannen om verder te communiceren.';
}
@override
- String get community_regenerate => 'Regenerate';
+ String get community_regenerate => 'Regeneer';
@override
String community_secretRegenerated(String name) {
- return 'Secret regenerated for \"$name\"';
+ return 'Geheim hersteld voor \"$name\"';
}
@override
- String get community_updateSecret => 'Update Secret';
+ String get community_updateSecret => 'Bijwerken Geheime';
@override
String community_secretUpdated(String name) {
- return 'Secret updated for \"$name\"';
+ return 'Geheim gewijzigd voor \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
- return 'Scan the new QR code to update the secret for \"$name\"';
+ return 'Scan de nieuwe QR-code om het geheim voor \"$name\" bij te werken';
}
@override
diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart
index 378858a0..13fbeb0a 100644
--- a/lib/l10n/app_localizations_pl.dart
+++ b/lib/l10n/app_localizations_pl.dart
@@ -1205,6 +1205,24 @@ class AppLocalizationsPl extends AppLocalizations {
return 'Niezgłoszone: $count';
}
+ @override
+ String get chat_openLink => 'Otworzyć link?';
+
+ @override
+ String get chat_openLinkConfirmation =>
+ 'Czy chcesz otworzyć ten link w przeglądarce?';
+
+ @override
+ String get chat_open => 'Otwórz';
+
+ @override
+ String chat_couldNotOpenLink(String url) {
+ return 'Nie można otworzyć linku: $url';
+ }
+
+ @override
+ String get chat_invalidLink => 'Nieprawidłowy format linku';
+
@override
String get map_title => 'Mapa węzłów';
@@ -2564,32 +2582,32 @@ class AppLocalizationsPl extends AppLocalizations {
}
@override
- String get community_regenerateSecret => 'Regenerate Secret';
+ String get community_regenerateSecret => 'Zregeneruj sekret';
@override
String community_regenerateSecretConfirm(String name) {
- return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
+ return 'Regeneruj tajny klucz dla \"$name\"? Wszyscy członkowie będą musieli zeskanować nowy kod QR, aby kontynuować komunikację.';
}
@override
- String get community_regenerate => 'Regenerate';
+ String get community_regenerate => 'Zregeneruj';
@override
String community_secretRegenerated(String name) {
- return 'Secret regenerated for \"$name\"';
+ return 'Hasło ponownie wygenerowane dla \"$name\"';
}
@override
- String get community_updateSecret => 'Update Secret';
+ String get community_updateSecret => 'Zaktualizuj tajny klucz';
@override
String community_secretUpdated(String name) {
- return 'Secret updated for \"$name\"';
+ return 'Hasło zaktualizowane dla \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
- return 'Scan the new QR code to update the secret for \"$name\"';
+ return 'Skanuj nowy kod QR, aby zaktualizować sekret dla \"$name\"';
}
@override
diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart
index ae02aff3..3f54001f 100644
--- a/lib/l10n/app_localizations_pt.dart
+++ b/lib/l10n/app_localizations_pt.dart
@@ -1204,6 +1204,24 @@ class AppLocalizationsPt extends AppLocalizations {
return 'Não lido: $count';
}
+ @override
+ String get chat_openLink => 'Abrir link?';
+
+ @override
+ String get chat_openLinkConfirmation =>
+ 'Deseja abrir este link no seu navegador?';
+
+ @override
+ String get chat_open => 'Abrir';
+
+ @override
+ String chat_couldNotOpenLink(String url) {
+ return 'Não foi possível abrir o link: $url';
+ }
+
+ @override
+ String get chat_invalidLink => 'Formato de link inválido';
+
@override
String get map_title => 'Mapa de Nós';
@@ -2567,32 +2585,32 @@ class AppLocalizationsPt extends AppLocalizations {
}
@override
- String get community_regenerateSecret => 'Regenerate Secret';
+ String get community_regenerateSecret => 'Regenerar Senha Segura';
@override
String community_regenerateSecretConfirm(String name) {
- return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
+ return 'Regenerar a chave secreta para \"$name\"? Todos os membros precisarão escanear o novo código QR para continuar a comunicação.';
}
@override
- String get community_regenerate => 'Regenerate';
+ String get community_regenerate => 'Regenerar';
@override
String community_secretRegenerated(String name) {
- return 'Secret regenerated for \"$name\"';
+ return 'Senha secreta regenerada para \"$name\"';
}
@override
- String get community_updateSecret => 'Update Secret';
+ String get community_updateSecret => 'Atualizar Segredo';
@override
String community_secretUpdated(String name) {
- return 'Secret updated for \"$name\"';
+ return 'Segredo atualizado para \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
- return 'Scan the new QR code to update the secret for \"$name\"';
+ return 'Scanar o novo código QR para atualizar o segredo para \"$name\"\n\n\n+++++';
}
@override
diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart
index 81bf16aa..75d46549 100644
--- a/lib/l10n/app_localizations_sk.dart
+++ b/lib/l10n/app_localizations_sk.dart
@@ -1200,6 +1200,24 @@ class AppLocalizationsSk extends AppLocalizations {
return 'Nezriadené: $count';
}
+ @override
+ String get chat_openLink => 'Otvoriť odkaz?';
+
+ @override
+ String get chat_openLinkConfirmation =>
+ 'Chcete otvoriť tento odkaz v prehliadači?';
+
+ @override
+ String get chat_open => 'Otvoriť';
+
+ @override
+ String chat_couldNotOpenLink(String url) {
+ return 'Nepodarilo sa otvoriť odkaz: $url';
+ }
+
+ @override
+ String get chat_invalidLink => 'Neplatný formát odkazu';
+
@override
String get map_title => 'Mapa uzlov';
@@ -2553,32 +2571,32 @@ class AppLocalizationsSk extends AppLocalizations {
}
@override
- String get community_regenerateSecret => 'Regenerate Secret';
+ String get community_regenerateSecret => 'Zobraziť nový tajný kód';
@override
String community_regenerateSecretConfirm(String name) {
- return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
+ return 'Znovu vygenerovať tajný kľúč pre \"$name\"? Všetci členovia budú musieť skanovať nový QR kód, aby mohli nadviazať komunikáciu.';
}
@override
- String get community_regenerate => 'Regenerate';
+ String get community_regenerate => 'Znovu vygenerovať';
@override
String community_secretRegenerated(String name) {
- return 'Secret regenerated for \"$name\"';
+ return 'Záznam pre \"$name\" bol regenerovaný tajne';
}
@override
- String get community_updateSecret => 'Update Secret';
+ String get community_updateSecret => 'Aktualizovať tajné heslo';
@override
String community_secretUpdated(String name) {
- return 'Secret updated for \"$name\"';
+ return 'Zmena tajnej slova pre \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
- return 'Scan the new QR code to update the secret for \"$name\"';
+ return 'Skáňte nový QR kód na aktualizáciu tajného hesla pre \"$name\"';
}
@override
diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart
index cdcd23c7..be9556f3 100644
--- a/lib/l10n/app_localizations_sl.dart
+++ b/lib/l10n/app_localizations_sl.dart
@@ -1197,6 +1197,24 @@ class AppLocalizationsSl extends AppLocalizations {
return 'Nerešeno: $count';
}
+ @override
+ String get chat_openLink => 'Odpreti povezavo?';
+
+ @override
+ String get chat_openLinkConfirmation =>
+ 'Ali želite odpreti to povezavo v brskalniku?';
+
+ @override
+ String get chat_open => 'Odpri';
+
+ @override
+ String chat_couldNotOpenLink(String url) {
+ return 'Povezave ni bilo mogoče odpreti: $url';
+ }
+
+ @override
+ String get chat_invalidLink => 'Neveljavna oblika povezave';
+
@override
String get map_title => 'Mapa omrežja';
@@ -2557,32 +2575,32 @@ class AppLocalizationsSl extends AppLocalizations {
}
@override
- String get community_regenerateSecret => 'Regenerate Secret';
+ String get community_regenerateSecret => 'Preberi nov tajni kôd';
@override
String community_regenerateSecretConfirm(String name) {
- return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
+ return 'Preberite novo tajno geslo za \"$name\"? Vsi članici morajo prebrati novo QR kodo, da lahko nadaljujejo s komunikacijo.';
}
@override
- String get community_regenerate => 'Regenerate';
+ String get community_regenerate => 'Preberi znova';
@override
String community_secretRegenerated(String name) {
- return 'Secret regenerated for \"$name\"';
+ return 'Tajna za \"$name\" ponovno ustvarjena';
}
@override
- String get community_updateSecret => 'Update Secret';
+ String get community_updateSecret => 'Ažurniraj tajno';
@override
String community_secretUpdated(String name) {
- return 'Secret updated for \"$name\"';
+ return 'Skrivnostno spremembo za \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
- return 'Scan the new QR code to update the secret for \"$name\"';
+ return 'Skeniraj nov kôd QR za posodabljanje tajne za $name';
}
@override
diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart
index 8b36b976..34b54b46 100644
--- a/lib/l10n/app_localizations_sv.dart
+++ b/lib/l10n/app_localizations_sv.dart
@@ -1192,6 +1192,24 @@ class AppLocalizationsSv extends AppLocalizations {
return 'Olästa: $count';
}
+ @override
+ String get chat_openLink => 'Öppna länk?';
+
+ @override
+ String get chat_openLinkConfirmation =>
+ 'Vill du öppna den här länken i din webbläsare?';
+
+ @override
+ String get chat_open => 'Öppna';
+
+ @override
+ String chat_couldNotOpenLink(String url) {
+ return 'Kunde inte öppna länken: $url';
+ }
+
+ @override
+ String get chat_invalidLink => 'Ogiltigt länkformat';
+
@override
String get map_title => 'Nodkarta';
@@ -2541,32 +2559,32 @@ class AppLocalizationsSv extends AppLocalizations {
}
@override
- String get community_regenerateSecret => 'Regenerate Secret';
+ String get community_regenerateSecret => 'Regenerera hemlig kod';
@override
String community_regenerateSecretConfirm(String name) {
- return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
+ return 'Regenerera den hemliga nyckeln för \"$name\"? Alla medlemmar måste scanna den nya QR-koden för att fortsätta kommunicera.';
}
@override
- String get community_regenerate => 'Regenerate';
+ String get community_regenerate => 'Regenerera';
@override
String community_secretRegenerated(String name) {
- return 'Secret regenerated for \"$name\"';
+ return 'Lösenord återskapad för \"$name\"';
}
@override
- String get community_updateSecret => 'Update Secret';
+ String get community_updateSecret => 'Uppdatera hemlighet';
@override
String community_secretUpdated(String name) {
- return 'Secret updated for \"$name\"';
+ return 'Hemlighet uppdaterad för \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
- return 'Scan the new QR code to update the secret for \"$name\"';
+ return 'Skanna den nya QR-koden för att uppdatera hemligheten för \"$name\"';
}
@override
diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart
index 3d7bd06e..cd9c3be8 100644
--- a/lib/l10n/app_localizations_zh.dart
+++ b/lib/l10n/app_localizations_zh.dart
@@ -1148,6 +1148,23 @@ class AppLocalizationsZh extends AppLocalizations {
return '未读:$count';
}
+ @override
+ String get chat_openLink => '打开链接?';
+
+ @override
+ String get chat_openLinkConfirmation => '您想在浏览器中打开此链接吗?';
+
+ @override
+ String get chat_open => '打开';
+
+ @override
+ String chat_couldNotOpenLink(String url) {
+ return '无法打开链接:$url';
+ }
+
+ @override
+ String get chat_invalidLink => '链接格式无效';
+
@override
String get map_title => '节点地图';
@@ -2425,32 +2442,32 @@ class AppLocalizationsZh extends AppLocalizations {
}
@override
- String get community_regenerateSecret => 'Regenerate Secret';
+ String get community_regenerateSecret => '重新生成密钥';
@override
String community_regenerateSecretConfirm(String name) {
- return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
+ return '重新生成“$name”的秘密密钥?所有成员将需要扫描新的二维码才能继续沟通。';
}
@override
- String get community_regenerate => 'Regenerate';
+ String get community_regenerate => '重新生成';
@override
String community_secretRegenerated(String name) {
- return 'Secret regenerated for \"$name\"';
+ return '密码已重置为“$name”';
}
@override
- String get community_updateSecret => 'Update Secret';
+ String get community_updateSecret => '更新密钥';
@override
String community_secretUpdated(String name) {
- return 'Secret updated for \"$name\"';
+ return '密码已更新为“$name”';
}
@override
String community_scanToUpdateSecret(String name) {
- return 'Scan the new QR code to update the secret for \"$name\"';
+ return '扫描新的二维码更新\"$name\"的密码';
}
@override
diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb
index 75a6cc09..48ef3ddd 100644
--- a/lib/l10n/app_nl.arb
+++ b/lib/l10n/app_nl.arb
@@ -604,6 +604,18 @@
}
}
},
+ "chat_openLink": "Link openen?",
+ "chat_openLinkConfirmation": "Wilt u deze link in uw browser openen?",
+ "chat_open": "Openen",
+ "chat_couldNotOpenLink": "Kan link niet openen: {url}",
+ "@chat_couldNotOpenLink": {
+ "placeholders": {
+ "url": {
+ "type": "String"
+ }
+ }
+ },
+ "chat_invalidLink": "Ongeldig linkformaat",
"map_title": "Node Map",
"map_noNodesWithLocation": "Geen nodes met locatiegegevens",
"map_nodesNeedGps": "Nodes moeten hun GPS-coördinaten delen\nom op de kaart te verschijnen",
@@ -1473,7 +1485,9 @@
"community_deleteChannelsWarning": "Dit verwijdert ook {count} kanaal/kanalen en hun berichten.",
"@community_deleteChannelsWarning": {
"placeholders": {
- "count": {"type": "int"}
+ "count": {
+ "type": "int"
+ }
}
},
"community_deleted": "Community \"{name}\" verlaten",
@@ -1484,5 +1498,40 @@
"community_regularHashtagDesc": "Open hashtag (iedereen kan deelnemen)",
"community_communityHashtag": "Gemeenschappelijk Hashtag",
"community_communityHashtagDesc": "Alleen zichtbaar voor leden van de community",
- "community_forCommunity": "Voor {name}"
+ "community_forCommunity": "Voor {name}",
+ "@community_regenerateSecretConfirm": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_secretRegenerated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_secretUpdated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_scanToUpdateSecret": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_secretRegenerated": "Geheim hersteld voor \"{name}\"",
+ "community_regenerateSecret": "Regeneer Geheimwoord",
+ "community_regenerateSecretConfirm": "Regeneere de geheime sleutel voor \"{name}\"? Alle leden moeten de nieuwe QR-code scannen om verder te communiceren.",
+ "community_regenerate": "Regeneer",
+ "community_updateSecret": "Bijwerken Geheime",
+ "community_secretUpdated": "Geheim gewijzigd voor \"{name}\"",
+ "community_scanToUpdateSecret": "Scan de nieuwe QR-code om het geheim voor \"{name}\" bij te werken"
}
diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb
index 50732d10..823bba1e 100644
--- a/lib/l10n/app_pl.arb
+++ b/lib/l10n/app_pl.arb
@@ -604,6 +604,18 @@
}
}
},
+ "chat_openLink": "Otworzyć link?",
+ "chat_openLinkConfirmation": "Czy chcesz otworzyć ten link w przeglądarce?",
+ "chat_open": "Otwórz",
+ "chat_couldNotOpenLink": "Nie można otworzyć linku: {url}",
+ "@chat_couldNotOpenLink": {
+ "placeholders": {
+ "url": {
+ "type": "String"
+ }
+ }
+ },
+ "chat_invalidLink": "Nieprawidłowy format linku",
"map_title": "Mapa węzłów",
"map_noNodesWithLocation": "Brak węzłów z danymi lokalizacyjnymi",
"map_nodesNeedGps": "Węzły muszą udostępniać swoje współrzędne GPS,\naby pojawić się na mapie.",
@@ -1473,7 +1485,9 @@
"community_deleteChannelsWarning": "Spowoduje to również usunięcie {count} kanału/kanałów i ich wiadomości.",
"@community_deleteChannelsWarning": {
"placeholders": {
- "count": {"type": "int"}
+ "count": {
+ "type": "int"
+ }
}
},
"community_deleted": "Opuszczono społeczność \"{name}\"",
@@ -1484,5 +1498,40 @@
"community_regularHashtagDesc": "Publiczny hashtag (każdy może dołączyć)",
"community_communityHashtag": "Hashtag Społeczności",
"community_communityHashtagDesc": "Dostępne tylko dla członków społeczności",
- "community_forCommunity": "Dla {name}"
+ "community_forCommunity": "Dla {name}",
+ "@community_regenerateSecretConfirm": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_secretRegenerated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_secretUpdated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_scanToUpdateSecret": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_regenerate": "Zregeneruj",
+ "community_secretRegenerated": "Hasło ponownie wygenerowane dla \"{name}\"",
+ "community_regenerateSecret": "Zregeneruj sekret",
+ "community_regenerateSecretConfirm": "Regeneruj tajny klucz dla \"{name}\"? Wszyscy członkowie będą musieli zeskanować nowy kod QR, aby kontynuować komunikację.",
+ "community_scanToUpdateSecret": "Skanuj nowy kod QR, aby zaktualizować sekret dla \"{name}\"",
+ "community_secretUpdated": "Hasło zaktualizowane dla \"{name}\"",
+ "community_updateSecret": "Zaktualizuj tajny klucz"
}
diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb
index 34797bea..b48db37b 100644
--- a/lib/l10n/app_pt.arb
+++ b/lib/l10n/app_pt.arb
@@ -604,6 +604,18 @@
}
}
},
+ "chat_openLink": "Abrir link?",
+ "chat_openLinkConfirmation": "Deseja abrir este link no seu navegador?",
+ "chat_open": "Abrir",
+ "chat_couldNotOpenLink": "Não foi possível abrir o link: {url}",
+ "@chat_couldNotOpenLink": {
+ "placeholders": {
+ "url": {
+ "type": "String"
+ }
+ }
+ },
+ "chat_invalidLink": "Formato de link inválido",
"map_title": "Mapa de Nós",
"map_noNodesWithLocation": "Não existem nós com dados de localização.",
"map_nodesNeedGps": "Os nós precisam partilhar as suas coordenadas GPS\npara aparecerem no mapa",
@@ -1473,7 +1485,9 @@
"community_deleteChannelsWarning": "Isso também excluirá {count} canal/canais e suas mensagens.",
"@community_deleteChannelsWarning": {
"placeholders": {
- "count": {"type": "int"}
+ "count": {
+ "type": "int"
+ }
}
},
"community_deleted": "Saiu da comunidade \"{name}\"",
@@ -1484,5 +1498,40 @@
"community_regularHashtagDesc": "Hashtag público (qualquer pessoa pode participar)",
"community_communityHashtag": "Hashtag da Comunidade",
"community_communityHashtagDesc": "Apenas para membros da comunidade",
- "community_forCommunity": "Para {name}"
+ "community_forCommunity": "Para {name}",
+ "@community_regenerateSecretConfirm": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_secretRegenerated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_secretUpdated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_scanToUpdateSecret": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_regenerateSecretConfirm": "Regenerar a chave secreta para \"{name}\"? Todos os membros precisarão escanear o novo código QR para continuar a comunicação.",
+ "community_regenerateSecret": "Regenerar Senha Segura",
+ "community_secretRegenerated": "Senha secreta regenerada para \"{name}\"",
+ "community_regenerate": "Regenerar",
+ "community_secretUpdated": "Segredo atualizado para \"{name}\"",
+ "community_scanToUpdateSecret": "Scanar o novo código QR para atualizar o segredo para \"{name}\"\n\n\n+++++",
+ "community_updateSecret": "Atualizar Segredo"
}
diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb
index d6ea7d83..71871d16 100644
--- a/lib/l10n/app_sk.arb
+++ b/lib/l10n/app_sk.arb
@@ -604,6 +604,18 @@
}
}
},
+ "chat_openLink": "Otvoriť odkaz?",
+ "chat_openLinkConfirmation": "Chcete otvoriť tento odkaz v prehliadači?",
+ "chat_open": "Otvoriť",
+ "chat_couldNotOpenLink": "Nepodarilo sa otvoriť odkaz: {url}",
+ "@chat_couldNotOpenLink": {
+ "placeholders": {
+ "url": {
+ "type": "String"
+ }
+ }
+ },
+ "chat_invalidLink": "Neplatný formát odkazu",
"map_title": "Mapa uzlov",
"map_noNodesWithLocation": "Žiadne uzly s údajmi o polohe",
"map_nodesNeedGps": "Uholníky musia zdieľať svoje GPS súradnice, aby sa zobrazili na mape.",
@@ -1473,7 +1485,9 @@
"community_deleteChannelsWarning": "Tým sa tiež vymaže {count} kanál/kanálov a ich správy.",
"@community_deleteChannelsWarning": {
"placeholders": {
- "count": {"type": "int"}
+ "count": {
+ "type": "int"
+ }
}
},
"community_deleted": "Opustená komunita \"{name}\"",
@@ -1484,5 +1498,40 @@
"community_regularHashtagDesc": "Veľký hashtag (ktočokoľvek sa môže pridať)",
"community_communityHashtag": "Komunitný Hashtag",
"community_communityHashtagDesc": "Špecifické pre členov komunity",
- "community_forCommunity": "Pre {name}"
+ "community_forCommunity": "Pre {name}",
+ "@community_regenerateSecretConfirm": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_secretRegenerated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_secretUpdated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_scanToUpdateSecret": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_secretRegenerated": "Záznam pre \"{name}\" bol regenerovaný tajne",
+ "community_regenerateSecretConfirm": "Znovu vygenerovať tajný kľúč pre \"{name}\"? Všetci členovia budú musieť skanovať nový QR kód, aby mohli nadviazať komunikáciu.",
+ "community_regenerate": "Znovu vygenerovať",
+ "community_regenerateSecret": "Zobraziť nový tajný kód",
+ "community_scanToUpdateSecret": "Skáňte nový QR kód na aktualizáciu tajného hesla pre \"{name}\"",
+ "community_updateSecret": "Aktualizovať tajné heslo",
+ "community_secretUpdated": "Zmena tajnej slova pre \"{name}\""
}
diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb
index 09ee0bcf..977f29cd 100644
--- a/lib/l10n/app_sl.arb
+++ b/lib/l10n/app_sl.arb
@@ -604,6 +604,18 @@
}
}
},
+ "chat_openLink": "Odpreti povezavo?",
+ "chat_openLinkConfirmation": "Ali želite odpreti to povezavo v brskalniku?",
+ "chat_open": "Odpri",
+ "chat_couldNotOpenLink": "Povezave ni bilo mogoče odpreti: {url}",
+ "@chat_couldNotOpenLink": {
+ "placeholders": {
+ "url": {
+ "type": "String"
+ }
+ }
+ },
+ "chat_invalidLink": "Neveljavna oblika povezave",
"map_title": "Mapa omrežja",
"map_noNodesWithLocation": "Nihče od notranjih elementov nima podatkov o lokaciji.",
"map_nodesNeedGps": "Omrežje morajo deliti svoje GPS koordinate,\nda se prikazao na zemljeobrazniku.",
@@ -1473,7 +1485,9 @@
"community_deleteChannelsWarning": "To bo izbrisalo tudi {count} kanal/kanalov in njihova sporočila.",
"@community_deleteChannelsWarning": {
"placeholders": {
- "count": {"type": "int"}
+ "count": {
+ "type": "int"
+ }
}
},
"community_deleted": "Zapustil skupnost \"{name}\"",
@@ -1484,5 +1498,40 @@
"community_regularHashtagDesc": "javna oznaka (kateri koli lahko sodelujejo)",
"community_communityHashtag": "Skupnostni hashtag",
"community_communityHashtagDesc": "Izključeno za uporabnike skupnosti",
- "community_forCommunity": "Za {name}"
+ "community_forCommunity": "Za {name}",
+ "@community_regenerateSecretConfirm": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_secretRegenerated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_secretUpdated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_scanToUpdateSecret": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_secretRegenerated": "Tajna za \"{name}\" ponovno ustvarjena",
+ "community_regenerateSecret": "Preberi nov tajni kôd",
+ "community_regenerateSecretConfirm": "Preberite novo tajno geslo za \"{name}\"? Vsi članici morajo prebrati novo QR kodo, da lahko nadaljujejo s komunikacijo.",
+ "community_regenerate": "Preberi znova",
+ "community_scanToUpdateSecret": "Skeniraj nov kôd QR za posodabljanje tajne za {name}",
+ "community_updateSecret": "Ažurniraj tajno",
+ "community_secretUpdated": "Skrivnostno spremembo za \"{name}\""
}
diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb
index 4d302a56..f1da7c8b 100644
--- a/lib/l10n/app_sv.arb
+++ b/lib/l10n/app_sv.arb
@@ -604,6 +604,18 @@
}
}
},
+ "chat_openLink": "Öppna länk?",
+ "chat_openLinkConfirmation": "Vill du öppna den här länken i din webbläsare?",
+ "chat_open": "Öppna",
+ "chat_couldNotOpenLink": "Kunde inte öppna länken: {url}",
+ "@chat_couldNotOpenLink": {
+ "placeholders": {
+ "url": {
+ "type": "String"
+ }
+ }
+ },
+ "chat_invalidLink": "Ogiltigt länkformat",
"map_title": "Nodkarta",
"map_noNodesWithLocation": "Inga noder med platsinformation",
"map_nodesNeedGps": "Noder måste dela sina GPS-koordinater\nför att visas på kartan",
@@ -1473,7 +1485,9 @@
"community_deleteChannelsWarning": "Detta kommer också att radera {count} kanal/kanaler och deras meddelanden.",
"@community_deleteChannelsWarning": {
"placeholders": {
- "count": {"type": "int"}
+ "count": {
+ "type": "int"
+ }
}
},
"community_deleted": "Lämnade community \"{name}\"",
@@ -1484,5 +1498,40 @@
"community_regularHashtagDesc": "Offentlig hashtag (alla kan gå med)",
"community_communityHashtagDesc": "Endast för medlemmar",
"community_forCommunity": "För {name}",
- "community_communityHashtag": "Community Hashtag"
+ "community_communityHashtag": "Community Hashtag",
+ "@community_regenerateSecretConfirm": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_secretRegenerated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_secretUpdated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_scanToUpdateSecret": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_regenerate": "Regenerera",
+ "community_regenerateSecretConfirm": "Regenerera den hemliga nyckeln för \"{name}\"? Alla medlemmar måste scanna den nya QR-koden för att fortsätta kommunicera.",
+ "community_secretRegenerated": "Lösenord återskapad för \"{name}\"",
+ "community_regenerateSecret": "Regenerera hemlig kod",
+ "community_scanToUpdateSecret": "Skanna den nya QR-koden för att uppdatera hemligheten för \"{name}\"",
+ "community_secretUpdated": "Hemlighet uppdaterad för \"{name}\"",
+ "community_updateSecret": "Uppdatera hemlighet"
}
diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb
index c0704140..ae10f604 100644
--- a/lib/l10n/app_zh.arb
+++ b/lib/l10n/app_zh.arb
@@ -604,6 +604,18 @@
}
}
},
+ "chat_openLink": "打开链接?",
+ "chat_openLinkConfirmation": "您想在浏览器中打开此链接吗?",
+ "chat_open": "打开",
+ "chat_couldNotOpenLink": "无法打开链接:{url}",
+ "@chat_couldNotOpenLink": {
+ "placeholders": {
+ "url": {
+ "type": "String"
+ }
+ }
+ },
+ "chat_invalidLink": "链接格式无效",
"map_title": "节点地图",
"map_noNodesWithLocation": "没有具有位置数据的节点",
"map_nodesNeedGps": "节点需要共享它们的 GPS 坐标\n才能在地图上显示",
@@ -1473,7 +1485,9 @@
"community_deleteChannelsWarning": "这也将删除 {count} 个频道及其消息。",
"@community_deleteChannelsWarning": {
"placeholders": {
- "count": {"type": "int"}
+ "count": {
+ "type": "int"
+ }
}
},
"community_deleted": "已退出社区 \"{name}\"",
@@ -1484,5 +1498,40 @@
"community_regularHashtagDesc": "公共话题(任何人都可以加入)",
"community_communityHashtag": "社区标签",
"community_communityHashtagDesc": "仅限社区成员使用",
- "community_forCommunity": "对于 {name}"
+ "community_forCommunity": "对于 {name}",
+ "@community_regenerateSecretConfirm": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_secretRegenerated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_secretUpdated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "@community_scanToUpdateSecret": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_regenerateSecret": "重新生成密钥",
+ "community_secretRegenerated": "密码已重置为“{name}”",
+ "community_regenerate": "重新生成",
+ "community_regenerateSecretConfirm": "重新生成“{name}”的秘密密钥?所有成员将需要扫描新的二维码才能继续沟通。",
+ "community_scanToUpdateSecret": "扫描新的二维码更新\"{name}\"的密码",
+ "community_updateSecret": "更新密钥",
+ "community_secretUpdated": "密码已更新为“{name}”"
}
diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart
index 46648433..84aeb0a6 100644
--- a/lib/screens/channel_chat_screen.dart
+++ b/lib/screens/channel_chat_screen.dart
@@ -3,11 +3,13 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
+import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
+import '../helpers/link_handler.dart';
import '../helpers/utf8_length_limiter.dart';
import '../l10n/l10n.dart';
import '../models/channel.dart';
@@ -280,9 +282,20 @@ class _ChannelChatScreenState extends State {
: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
)
else
- Text(
- message.text,
+ Linkify(
+ text: message.text,
style: const TextStyle(fontSize: 14),
+ linkStyle: const TextStyle(
+ fontSize: 14,
+ color: Colors.green,
+ decoration: TextDecoration.underline,
+ ),
+ options: const LinkifyOptions(
+ humanize: false,
+ defaultToHttps: false,
+ ),
+ linkifiers: const [UrlLinkifier()],
+ onOpen: (link) => LinkHandler.handleLinkTap(context, link.url),
),
if (displayPath.isNotEmpty) ...[
const SizedBox(height: 4),
diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart
index 1cb66ab5..a120a0d8 100644
--- a/lib/screens/channels_screen.dart
+++ b/lib/screens/channels_screen.dart
@@ -18,7 +18,6 @@ import '../widgets/battery_indicator.dart';
import '../widgets/list_filter_widget.dart';
import '../widgets/empty_state.dart';
import '../widgets/qr_code_display.dart';
-import '../widgets/qr_scanner_widget.dart';
import '../widgets/quick_switch_bar.dart';
import '../widgets/unread_badge.dart';
import 'channel_chat_screen.dart';
@@ -27,20 +26,12 @@ import 'contacts_screen.dart';
import 'map_screen.dart';
import 'settings_screen.dart';
-enum ChannelSortOption {
- manual,
- name,
- latestMessages,
- unread,
-}
+enum ChannelSortOption { manual, name, latestMessages, unread }
class ChannelsScreen extends StatefulWidget {
final bool hideBackButton;
- const ChannelsScreen({
- super.key,
- this.hideBackButton = false,
- });
+ const ChannelsScreen({super.key, this.hideBackButton = false});
@override
State createState() => _ChannelsScreenState();
@@ -54,7 +45,7 @@ class _ChannelsScreenState extends State
Timer? _searchDebounce;
ChannelSortOption _sortOption = ChannelSortOption.manual;
List _communities = [];
-
+
// Cache of PSK hex -> Community for quick lookup
final Map _pskToCommunity = {};
@@ -66,7 +57,7 @@ class _ChannelsScreenState extends State
_loadCommunities();
});
}
-
+
Future _loadCommunities() async {
final communities = await _communityStore.loadCommunities();
if (mounted) {
@@ -76,14 +67,14 @@ class _ChannelsScreenState extends State
});
}
}
-
+
void _buildPskCommunityMap() {
_pskToCommunity.clear();
for (final community in _communities) {
// Map the community public channel PSK
final publicPsk = community.deriveCommunityPublicPsk();
_pskToCommunity[Channel.formatPskHex(publicPsk)] = community;
-
+
// Map all known hashtag channel PSKs
for (final hashtag in community.hashtagChannels) {
final hashtagPsk = community.deriveCommunityHashtagPsk(hashtag);
@@ -91,12 +82,12 @@ class _ChannelsScreenState extends State
}
}
}
-
+
/// Returns the community this channel belongs to, or null if not a community channel
Community? _getCommunityForChannel(Channel channel) {
return _pskToCommunity[channel.pskHex];
}
-
+
/// Returns true if this is the community's public channel
bool _isCommunityPublicChannel(Channel channel, Community community) {
final publicPsk = community.deriveCommunityPublicPsk();
@@ -181,7 +172,10 @@ class _ChannelsScreenState extends State
);
}
- final filteredChannels = _filterAndSortChannels(channels, connector);
+ final filteredChannels = _filterAndSortChannels(
+ channels,
+ connector,
+ );
return Column(
children: [
@@ -211,17 +205,22 @@ class _ChannelsScreenState extends State
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
- contentPadding:
- const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 12,
+ ),
),
onChanged: (value) {
_searchDebounce?.cancel();
- _searchDebounce = Timer(const Duration(milliseconds: 300), () {
- if (!mounted) return;
- setState(() {
- _searchQuery = value.toLowerCase();
- });
- });
+ _searchDebounce = Timer(
+ const Duration(milliseconds: 300),
+ () {
+ if (!mounted) return;
+ setState(() {
+ _searchQuery = value.toLowerCase();
+ });
+ },
+ );
},
),
),
@@ -235,11 +234,18 @@ class _ChannelsScreenState extends State
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
- Icon(Icons.search_off, size: 64, color: Colors.grey[400]),
+ Icon(
+ Icons.search_off,
+ size: 64,
+ color: Colors.grey[400],
+ ),
const SizedBox(height: 16),
Text(
context.l10n.channels_noChannelsFound,
- style: TextStyle(fontSize: 16, color: Colors.grey[600]),
+ style: TextStyle(
+ fontSize: 16,
+ color: Colors.grey[600],
+ ),
),
],
),
@@ -247,51 +253,58 @@ class _ChannelsScreenState extends State
),
],
)
- : (_sortOption == ChannelSortOption.manual && _searchQuery.isEmpty)
- ? ReorderableListView.builder(
- padding: const EdgeInsets.only(
- left: 16,
- right: 16,
- top: 8,
- bottom: 88,
+ : (_sortOption == ChannelSortOption.manual &&
+ _searchQuery.isEmpty)
+ ? ReorderableListView.builder(
+ padding: const EdgeInsets.only(
+ left: 16,
+ right: 16,
+ top: 8,
+ bottom: 88,
+ ),
+ buildDefaultDragHandles: false,
+ itemCount: filteredChannels.length,
+ onReorder: (oldIndex, newIndex) {
+ if (newIndex > oldIndex) newIndex -= 1;
+ final reordered = List.from(
+ filteredChannels,
+ );
+ final item = reordered.removeAt(oldIndex);
+ reordered.insert(newIndex, item);
+ unawaited(
+ connector.setChannelOrder(
+ reordered.map((c) => c.index).toList(),
),
- buildDefaultDragHandles: false,
- itemCount: filteredChannels.length,
- onReorder: (oldIndex, newIndex) {
- if (newIndex > oldIndex) newIndex -= 1;
- final reordered = List.from(filteredChannels);
- final item = reordered.removeAt(oldIndex);
- reordered.insert(newIndex, item);
- unawaited(
- connector.setChannelOrder(
- reordered.map((c) => c.index).toList(),
- ),
- );
- },
- itemBuilder: (context, index) {
- final channel = filteredChannels[index];
- return _buildChannelTile(
- context,
- connector,
- channel,
- showDragHandle: true,
- dragIndex: index,
- );
- },
- )
- : ListView.builder(
- padding: const EdgeInsets.only(
- left: 16,
- right: 16,
- top: 8,
- bottom: 88,
- ),
- itemCount: filteredChannels.length,
- itemBuilder: (context, index) {
- final channel = filteredChannels[index];
- return _buildChannelTile(context, connector, channel);
- },
- ),
+ );
+ },
+ itemBuilder: (context, index) {
+ final channel = filteredChannels[index];
+ return _buildChannelTile(
+ context,
+ connector,
+ channel,
+ showDragHandle: true,
+ dragIndex: index,
+ );
+ },
+ )
+ : ListView.builder(
+ padding: const EdgeInsets.only(
+ left: 16,
+ right: 16,
+ top: 8,
+ bottom: 88,
+ ),
+ itemCount: filteredChannels.length,
+ itemBuilder: (context, index) {
+ final channel = filteredChannels[index];
+ return _buildChannelTile(
+ context,
+ connector,
+ channel,
+ );
+ },
+ ),
),
],
);
@@ -305,7 +318,8 @@ class _ChannelsScreenState extends State
top: false,
child: QuickSwitchBar(
selectedIndex: 1,
- onDestinationSelected: (index) => _handleQuickSwitch(index, context),
+ onDestinationSelected: (index) =>
+ _handleQuickSwitch(index, context),
),
),
),
@@ -315,33 +329,34 @@ class _ChannelsScreenState extends State
Widget _buildChannelTile(
BuildContext context,
MeshCoreConnector connector,
- Channel channel,
- {
+ Channel channel, {
bool showDragHandle = false,
int? dragIndex,
- }
- ) {
+ }) {
final unreadCount = connector.getUnreadCountForChannel(channel);
final community = _getCommunityForChannel(channel);
final isCommunityChannel = community != null;
- final isCommunityPublic = isCommunityChannel && _isCommunityPublicChannel(channel, community);
-
+ final isCommunityPublic =
+ isCommunityChannel && _isCommunityPublicChannel(channel, community);
+
// Determine icon and colors based on channel type
IconData icon;
Color iconColor;
Color bgColor;
String subtitle;
-
+
if (isCommunityChannel) {
// Community channel styling
iconColor = Colors.purple;
bgColor = Colors.purple.withValues(alpha: 0.2);
if (isCommunityPublic) {
icon = Icons.groups;
- subtitle = '${context.l10n.community_publicChannel} • ${community.name}';
+ subtitle =
+ '${context.l10n.community_publicChannel} • ${community.name}';
} else {
icon = Icons.tag;
- subtitle = '${context.l10n.community_hashtagChannel} • ${community.name}';
+ subtitle =
+ '${context.l10n.community_hashtagChannel} • ${community.name}';
}
} else if (channel.isPublicChannel) {
icon = Icons.public;
@@ -359,7 +374,7 @@ class _ChannelsScreenState extends State
bgColor = Colors.blue.withValues(alpha: 0.2);
subtitle = context.l10n.channels_privateChannel;
}
-
+
return Card(
key: ValueKey('channel_${channel.index}'),
margin: const EdgeInsets.only(bottom: 12),
@@ -389,24 +404,18 @@ class _ChannelsScreenState extends State
width: 2,
),
),
- child: const Icon(
- Icons.people,
- size: 8,
- color: Colors.white,
- ),
+ child: const Icon(Icons.people, size: 8, color: Colors.white),
),
),
],
),
title: Text(
- channel.name.isEmpty ? context.l10n.channels_channelIndex(channel.index) : channel.name,
+ channel.name.isEmpty
+ ? context.l10n.channels_channelIndex(channel.index)
+ : channel.name,
style: const TextStyle(fontWeight: FontWeight.w500),
),
- subtitle: Text(
- subtitle,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- ),
+ subtitle: Text(subtitle, maxLines: 1, overflow: TextOverflow.ellipsis),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
@@ -465,7 +474,10 @@ class _ChannelsScreenState extends State
),
ListTile(
leading: const Icon(Icons.delete_outline, color: Colors.red),
- title: Text(context.l10n.channels_deleteChannel, style: const TextStyle(color: Colors.red)),
+ title: Text(
+ context.l10n.channels_deleteChannel,
+ style: const TextStyle(color: Colors.red),
+ ),
onTap: () async {
Navigator.pop(context);
await Future.delayed(const Duration(milliseconds: 100));
@@ -486,17 +498,13 @@ class _ChannelsScreenState extends State
case 0:
Navigator.pushReplacement(
context,
- buildQuickSwitchRoute(
- const ContactsScreen(hideBackButton: true),
- ),
+ buildQuickSwitchRoute(const ContactsScreen(hideBackButton: true)),
);
break;
case 2:
Navigator.pushReplacement(
context,
- buildQuickSwitchRoute(
- const MapScreen(hideBackButton: true),
- ),
+ buildQuickSwitchRoute(const MapScreen(hideBackButton: true)),
);
break;
}
@@ -587,8 +595,12 @@ class _ChannelsScreenState extends State
filtered.sort((a, b) {
final aMessages = connector.getChannelMessages(a);
final bMessages = connector.getChannelMessages(b);
- final aLast = aMessages.isEmpty ? DateTime(1970) : aMessages.last.timestamp;
- final bLast = bMessages.isEmpty ? DateTime(1970) : bMessages.last.timestamp;
+ final aLast = aMessages.isEmpty
+ ? DateTime(1970)
+ : aMessages.last.timestamp;
+ final bLast = bMessages.isEmpty
+ ? DateTime(1970)
+ : bMessages.last.timestamp;
final timeCompare = bLast.compareTo(aLast);
if (timeCompare != 0) return timeCompare;
return compareByName(a, b);
@@ -612,7 +624,9 @@ class _ChannelsScreenState extends State
}
String _normalizeChannelName(Channel channel) {
- if (channel.name.isEmpty) return 'Channel ${channel.index}'; // Fallback for sorting
+ if (channel.name.isEmpty) {
+ return 'Channel ${channel.index}'; // Fallback for sorting
+ }
final trimmed = channel.name.trim();
if (trimmed.startsWith('#') && trimmed.length > 1) {
return trimmed.substring(1);
@@ -622,7 +636,10 @@ class _ChannelsScreenState extends State
void _showAddChannelDialog(BuildContext context) {
final connector = context.read();
- final nextIndex = _findNextAvailableIndex(connector.channels, connector.maxChannels);
+ final nextIndex = _findNextAvailableIndex(
+ connector.channels,
+ connector.maxChannels,
+ );
final hasPublicChannel = connector.channels.any((c) => c.isPublicChannel);
int? selectedOption;
final nameController = TextEditingController();
@@ -647,12 +664,16 @@ class _ChannelsScreenState extends State
return ListTile(
leading: CircleAvatar(
backgroundColor: enabled
- ? (isSelected ? Theme.of(dialogContext).colorScheme.primaryContainer : null)
+ ? (isSelected
+ ? Theme.of(dialogContext).colorScheme.primaryContainer
+ : null)
: Colors.grey.withValues(alpha: 0.2),
child: Icon(
icon,
color: enabled
- ? (isSelected ? Theme.of(dialogContext).colorScheme.primary : null)
+ ? (isSelected
+ ? Theme.of(dialogContext).colorScheme.primary
+ : null)
: Colors.grey,
),
),
@@ -685,7 +706,10 @@ class _ChannelsScreenState extends State
return Column(
children: [
Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 8,
+ ),
child: TextField(
controller: nameController,
decoration: InputDecoration(
@@ -704,8 +728,16 @@ class _ChannelsScreenState extends State
onPressed: () {
final name = nameController.text.trim();
if (name.isEmpty) {
- ScaffoldMessenger.of(dialogContext).showSnackBar(
- SnackBar(content: Text(dialogContext.l10n.channels_enterChannelName)),
+ ScaffoldMessenger.of(
+ dialogContext,
+ ).showSnackBar(
+ SnackBar(
+ content: Text(
+ dialogContext
+ .l10n
+ .channels_enterChannelName,
+ ),
+ ),
);
return;
}
@@ -718,7 +750,13 @@ class _ChannelsScreenState extends State
connector.setChannel(nextIndex, name, psk);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(context.l10n.channels_channelAdded(name))),
+ SnackBar(
+ content: Text(
+ context.l10n.channels_channelAdded(
+ name,
+ ),
+ ),
+ ),
);
}
},
@@ -735,7 +773,10 @@ class _ChannelsScreenState extends State
return Column(
children: [
Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 8,
+ ),
child: TextField(
controller: nameController,
decoration: InputDecoration(
@@ -746,7 +787,10 @@ class _ChannelsScreenState extends State
),
),
Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 8,
+ ),
child: TextField(
controller: pskController,
decoration: InputDecoration(
@@ -765,8 +809,16 @@ class _ChannelsScreenState extends State
final name = nameController.text.trim();
final pskHex = pskController.text.trim();
if (name.isEmpty) {
- ScaffoldMessenger.of(dialogContext).showSnackBar(
- SnackBar(content: Text(dialogContext.l10n.channels_enterChannelName)),
+ ScaffoldMessenger.of(
+ dialogContext,
+ ).showSnackBar(
+ SnackBar(
+ content: Text(
+ dialogContext
+ .l10n
+ .channels_enterChannelName,
+ ),
+ ),
);
return;
}
@@ -774,8 +826,16 @@ class _ChannelsScreenState extends State
try {
psk = Channel.parsePskHex(pskHex);
} on FormatException {
- ScaffoldMessenger.of(dialogContext).showSnackBar(
- SnackBar(content: Text(dialogContext.l10n.channels_pskMustBe32Hex)),
+ ScaffoldMessenger.of(
+ dialogContext,
+ ).showSnackBar(
+ SnackBar(
+ content: Text(
+ dialogContext
+ .l10n
+ .channels_pskMustBe32Hex,
+ ),
+ ),
);
return;
}
@@ -783,7 +843,13 @@ class _ChannelsScreenState extends State
connector.setChannel(nextIndex, name, psk);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(context.l10n.channels_channelAdded(name))),
+ SnackBar(
+ content: Text(
+ context.l10n.channels_channelAdded(
+ name,
+ ),
+ ),
+ ),
);
}
},
@@ -798,18 +864,27 @@ class _ChannelsScreenState extends State
case 2: // Join Public Channel
return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 8,
+ ),
child: Row(
children: [
Expanded(
child: FilledButton(
onPressed: () {
- final psk = Channel.parsePskHex(Channel.publicChannelPsk);
+ final psk = Channel.parsePskHex(
+ Channel.publicChannelPsk,
+ );
Navigator.pop(dialogContext);
connector.setChannel(nextIndex, 'Public', psk);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(context.l10n.channels_publicChannelAdded)),
+ SnackBar(
+ content: Text(
+ context.l10n.channels_publicChannelAdded,
+ ),
+ ),
);
}
},
@@ -825,55 +900,76 @@ class _ChannelsScreenState extends State
children: [
// Only show type selection if user has communities
if (_communities.isNotEmpty) ...[
- RadioGroup(
+ RadioListTile(
+ value: true,
groupValue: isRegularHashtag,
onChanged: (v) => setDialogState(() {
- if (v != null) {
- isRegularHashtag = v;
- if (isRegularHashtag) {
- selectedCommunity = null;
- } else if (selectedCommunity == null && _communities.isNotEmpty) {
- selectedCommunity = _communities.first;
- }
+ isRegularHashtag = v!;
+ if (isRegularHashtag) {
+ selectedCommunity = null;
}
}),
- child: Column(
- children: [
- RadioListTile(
- value: true,
- title: Text(dialogContext.l10n.community_regularHashtag),
- subtitle: Text(dialogContext.l10n.community_regularHashtagDesc),
- dense: true,
- ),
- RadioListTile(
- value: false,
- title: Text(dialogContext.l10n.community_communityHashtag),
- subtitle: Text(dialogContext.l10n.community_communityHashtagDesc),
- dense: true,
- ),
- ],
+ title: Text(
+ dialogContext.l10n.community_regularHashtag,
),
+ subtitle: Text(
+ dialogContext.l10n.community_regularHashtagDesc,
+ ),
+ dense: true,
+ ),
+ RadioListTile(
+ value: false,
+ groupValue: isRegularHashtag,
+ onChanged: (v) => setDialogState(() {
+ isRegularHashtag = v!;
+ if (!isRegularHashtag &&
+ selectedCommunity == null &&
+ _communities.isNotEmpty) {
+ selectedCommunity = _communities.first;
+ }
+ }),
+ title: Text(
+ dialogContext.l10n.community_communityHashtag,
+ ),
+ subtitle: Text(
+ dialogContext.l10n.community_communityHashtagDesc,
+ ),
+ dense: true,
),
],
// Community dropdown (only if community hashtag selected)
if (!isRegularHashtag && _communities.isNotEmpty)
Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
- child: DropdownMenu(
- initialSelection: selectedCommunity,
- dropdownMenuEntries: _communities.map((c) => DropdownMenuEntry(
- value: c,
- label: c.name,
- )).toList(),
- onSelected: (c) => setDialogState(() => selectedCommunity = c),
- label: Text(dialogContext.l10n.community_selectCommunity),
- leadingIcon: const Icon(Icons.groups),
- expandedInsets: EdgeInsets.zero,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 8,
+ ),
+ child: DropdownButtonFormField(
+ initialValue: selectedCommunity,
+ items: _communities
+ .map(
+ (c) => DropdownMenuItem(
+ value: c,
+ child: Text(c.name),
+ ),
+ )
+ .toList(),
+ onChanged: (c) =>
+ setDialogState(() => selectedCommunity = c),
+ decoration: InputDecoration(
+ labelText:
+ dialogContext.l10n.community_selectCommunity,
+ border: const OutlineInputBorder(),
+ prefixIcon: const Icon(Icons.groups),
+ ),
),
),
// Hashtag name input
Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 8,
+ ),
child: TextField(
controller: hashtagController,
decoration: InputDecoration(
@@ -899,7 +995,10 @@ class _ChannelsScreenState extends State
),
),
Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 8,
+ ),
child: Row(
children: [
Expanded(
@@ -907,18 +1006,26 @@ class _ChannelsScreenState extends State
onPressed: () async {
var hashtag = hashtagController.text.trim();
if (hashtag.isEmpty) {
- ScaffoldMessenger.of(dialogContext).showSnackBar(
- SnackBar(content: Text(dialogContext.l10n.channels_enterChannelName)),
+ ScaffoldMessenger.of(
+ dialogContext,
+ ).showSnackBar(
+ SnackBar(
+ content: Text(
+ dialogContext
+ .l10n
+ .channels_enterChannelName,
+ ),
+ ),
);
return;
}
-
+
// Normalize hashtag name (remove leading # if present)
if (hashtag.startsWith('#')) {
hashtag = hashtag.substring(1);
}
final channelName = '#$hashtag';
-
+
final Uint8List psk;
if (isRegularHashtag) {
// Regular hashtag - public derivation using SHA256
@@ -926,24 +1033,46 @@ class _ChannelsScreenState extends State
} else {
// Community hashtag - HMAC derivation from community secret
if (selectedCommunity == null) {
- ScaffoldMessenger.of(dialogContext).showSnackBar(
- SnackBar(content: Text(dialogContext.l10n.community_selectCommunity)),
+ ScaffoldMessenger.of(
+ dialogContext,
+ ).showSnackBar(
+ SnackBar(
+ content: Text(
+ dialogContext
+ .l10n
+ .community_selectCommunity,
+ ),
+ ),
);
return;
}
- psk = selectedCommunity!.deriveCommunityHashtagPsk(hashtag);
+ psk = selectedCommunity!
+ .deriveCommunityHashtagPsk(hashtag);
// Track in community's hashtag list
- await _communityStore.addHashtagChannel(selectedCommunity!.id, hashtag);
+ await _communityStore.addHashtagChannel(
+ selectedCommunity!.id,
+ hashtag,
+ );
_loadCommunities();
}
-
+
if (dialogContext.mounted) {
Navigator.pop(dialogContext);
}
- connector.setChannel(nextIndex, channelName, psk);
+ connector.setChannel(
+ nextIndex,
+ channelName,
+ psk,
+ );
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(context.l10n.channels_channelAdded(channelName))),
+ SnackBar(
+ content: Text(
+ context.l10n.channels_channelAdded(
+ channelName,
+ ),
+ ),
+ ),
);
}
},
@@ -958,7 +1087,10 @@ class _ChannelsScreenState extends State
case 4: // Scan Community QR
return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 8,
+ ),
child: Row(
children: [
Expanded(
@@ -966,15 +1098,16 @@ class _ChannelsScreenState extends State
onPressed: () async {
Navigator.pop(dialogContext);
if (context.mounted) {
- await Navigator.push(
+ final result = await Navigator.push(
context,
MaterialPageRoute(
- builder: (context) => const CommunityQrScannerScreen(),
+ builder: (context) =>
+ const CommunityQrScannerScreen(),
),
);
- // Refresh communities list when returning from scanner
- if (context.mounted) {
- _loadCommunities();
+ // Result handled by scanner screen
+ if (result != null && context.mounted) {
+ // Community was joined, refresh might be needed
}
}
},
@@ -990,7 +1123,10 @@ class _ChannelsScreenState extends State
return Column(
children: [
Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 8,
+ ),
child: TextField(
controller: nameController,
decoration: InputDecoration(
@@ -1009,10 +1145,16 @@ class _ChannelsScreenState extends State
addPublicChannel = value ?? true;
});
},
- title: Text(dialogContext.l10n.community_addPublicChannel),
- subtitle: Text(dialogContext.l10n.community_addPublicChannelHint),
+ title: Text(
+ dialogContext.l10n.community_addPublicChannel,
+ ),
+ subtitle: Text(
+ dialogContext.l10n.community_addPublicChannelHint,
+ ),
controlAffinity: ListTileControlAffinity.leading,
- contentPadding: const EdgeInsets.symmetric(horizontal: 16),
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ ),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
@@ -1023,47 +1165,68 @@ class _ChannelsScreenState extends State
onPressed: () async {
final name = nameController.text.trim();
if (name.isEmpty) {
- ScaffoldMessenger.of(dialogContext).showSnackBar(
- SnackBar(content: Text(dialogContext.l10n.community_enterName)),
+ ScaffoldMessenger.of(
+ dialogContext,
+ ).showSnackBar(
+ SnackBar(
+ content: Text(
+ dialogContext.l10n.community_enterName,
+ ),
+ ),
);
return;
}
-
+
// Create community with random secret
final community = Community.create(
id: const Uuid().v4(),
name: name,
);
-
+
// Save to store
await _communityStore.addCommunity(community);
-
+
// Optionally add the community public channel to the device
if (addPublicChannel) {
- final psk = community.deriveCommunityPublicPsk();
- final channelName = '${community.name} Public';
- connector.setChannel(nextIndex, channelName, psk);
+ final psk = community
+ .deriveCommunityPublicPsk();
+ final channelName =
+ '${community.name} Public';
+ connector.setChannel(
+ nextIndex,
+ channelName,
+ psk,
+ );
}
-
+
if (dialogContext.mounted) {
Navigator.pop(dialogContext);
}
-
+
// Refresh communities list
_loadCommunities();
-
+
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(context.l10n.community_created(name))),
+ SnackBar(
+ content: Text(
+ context.l10n.community_created(name),
+ ),
+ ),
);
-
+
// Show QR code dialog
await QrCodeShareDialog.show(
context: context,
data: community.toQrJson(),
title: context.l10n.community_qrTitle,
- instructions: context.l10n.community_qrInstructions(name),
- embeddedImage: Image.asset('assets/images/mesh-icon.png', width: 40, height: 40),
+ instructions: context.l10n
+ .community_qrInstructions(name),
+ embeddedImage: Image.asset(
+ 'assets/images/mesh-icon.png',
+ width: 40,
+ height: 40,
+ ),
);
}
},
@@ -1094,7 +1257,8 @@ class _ChannelsScreenState extends State
optionIndex: 0,
icon: Icons.add,
title: dialogContext.l10n.channels_createPrivateChannel,
- subtitle: dialogContext.l10n.channels_createPrivateChannelDesc,
+ subtitle:
+ dialogContext.l10n.channels_createPrivateChannelDesc,
),
if (selectedOption == 0) buildExpandedContent()!,
const Divider(height: 1),
@@ -1102,7 +1266,8 @@ class _ChannelsScreenState extends State
optionIndex: 1,
icon: Icons.lock,
title: dialogContext.l10n.channels_joinPrivateChannel,
- subtitle: dialogContext.l10n.channels_joinPrivateChannelDesc,
+ subtitle:
+ dialogContext.l10n.channels_joinPrivateChannelDesc,
),
if (selectedOption == 1) buildExpandedContent()!,
if (!hasPublicChannel) ...[
@@ -1111,7 +1276,8 @@ class _ChannelsScreenState extends State
optionIndex: 2,
icon: Icons.public,
title: dialogContext.l10n.channels_joinPublicChannel,
- subtitle: dialogContext.l10n.channels_joinPublicChannelDesc,
+ subtitle:
+ dialogContext.l10n.channels_joinPublicChannelDesc,
),
if (selectedOption == 2) buildExpandedContent()!,
],
@@ -1120,7 +1286,8 @@ class _ChannelsScreenState extends State
optionIndex: 3,
icon: Icons.tag,
title: dialogContext.l10n.channels_joinHashtagChannel,
- subtitle: dialogContext.l10n.channels_joinHashtagChannelDesc,
+ subtitle:
+ dialogContext.l10n.channels_joinHashtagChannelDesc,
),
if (selectedOption == 3) buildExpandedContent()!,
const Divider(height: 1),
@@ -1168,7 +1335,9 @@ class _ChannelsScreenState extends State
context: context,
builder: (dialogContext) => StatefulBuilder(
builder: (dialogContext, setState) => AlertDialog(
- title: Text(dialogContext.l10n.channels_editChannelTitle(channel.index)),
+ title: Text(
+ dialogContext.l10n.channels_editChannelTitle(channel.index),
+ ),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -1226,7 +1395,9 @@ class _ChannelsScreenState extends State
psk = Channel.parsePskHex(pskHex);
} on FormatException {
ScaffoldMessenger.of(dialogContext).showSnackBar(
- SnackBar(content: Text(dialogContext.l10n.channels_pskMustBe32Hex)),
+ SnackBar(
+ content: Text(dialogContext.l10n.channels_pskMustBe32Hex),
+ ),
);
return;
}
@@ -1235,7 +1406,9 @@ class _ChannelsScreenState extends State
connector.setChannel(channel.index, name, psk);
connector.setChannelSmazEnabled(channel.index, smazEnabled);
ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(context.l10n.channels_channelUpdated(name))),
+ SnackBar(
+ content: Text(context.l10n.channels_channelUpdated(name)),
+ ),
);
},
child: Text(dialogContext.l10n.common_save),
@@ -1255,7 +1428,9 @@ class _ChannelsScreenState extends State
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(dialogContext.l10n.channels_deleteChannel),
- content: Text(dialogContext.l10n.channels_deleteChannelConfirm(channel.name)),
+ content: Text(
+ dialogContext.l10n.channels_deleteChannelConfirm(channel.name),
+ ),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
@@ -1266,10 +1441,17 @@ class _ChannelsScreenState extends State
Navigator.pop(dialogContext);
connector.deleteChannel(channel.index);
ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(context.l10n.channels_channelDeleted(channel.name))),
+ SnackBar(
+ content: Text(
+ context.l10n.channels_channelDeleted(channel.name),
+ ),
+ ),
);
},
- child: Text(dialogContext.l10n.common_delete, style: const TextStyle(color: Colors.red)),
+ child: Text(
+ dialogContext.l10n.common_delete,
+ style: const TextStyle(color: Colors.red),
+ ),
),
],
),
@@ -1323,16 +1505,26 @@ class _ChannelsScreenState extends State
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
- Icon(Icons.groups_outlined, size: 64, color: Colors.grey[400]),
+ Icon(
+ Icons.groups_outlined,
+ size: 64,
+ color: Colors.grey[400],
+ ),
const SizedBox(height: 16),
Text(
context.l10n.community_noCommunities,
- style: TextStyle(fontSize: 16, color: Colors.grey[600]),
+ style: TextStyle(
+ fontSize: 16,
+ color: Colors.grey[600],
+ ),
),
const SizedBox(height: 8),
Text(
context.l10n.community_scanOrCreate,
- style: TextStyle(fontSize: 14, color: Colors.grey[500]),
+ style: TextStyle(
+ fontSize: 14,
+ color: Colors.grey[500],
+ ),
textAlign: TextAlign.center,
),
],
@@ -1345,8 +1537,13 @@ class _ChannelsScreenState extends State
final community = _communities[index];
return ListTile(
leading: CircleAvatar(
- backgroundColor: Colors.purple.withValues(alpha: 0.2),
- child: const Icon(Icons.groups, color: Colors.purple),
+ backgroundColor: Colors.purple.withValues(
+ alpha: 0.2,
+ ),
+ child: const Icon(
+ Icons.groups,
+ color: Colors.purple,
+ ),
),
title: Text(community.name),
subtitle: Text(
@@ -1361,10 +1558,6 @@ class _ChannelsScreenState extends State
Navigator.pop(sheetContext);
if (value == 'share') {
_showCommunityQrDialog(context, community);
- } else if (value == 'regenerate') {
- _regenerateCommunitySecret(context, community);
- } else if (value == 'update') {
- _updateCommunitySecret(context, community);
} else if (value == 'leave') {
_confirmLeaveCommunity(context, community);
}
@@ -1380,32 +1573,14 @@ class _ChannelsScreenState extends State
],
),
),
- PopupMenuItem(
- value: 'regenerate',
- child: Row(
- children: [
- const Icon(Icons.refresh),
- const SizedBox(width: 12),
- Text(context.l10n.community_regenerateSecret),
- ],
- ),
- ),
- PopupMenuItem(
- value: 'update',
- child: Row(
- children: [
- const Icon(Icons.qr_code_scanner),
- const SizedBox(width: 12),
- Text(context.l10n.community_updateSecret),
- ],
- ),
- ),
- const PopupMenuDivider(),
PopupMenuItem(
value: 'leave',
child: Row(
children: [
- const Icon(Icons.exit_to_app, color: Colors.red),
+ const Icon(
+ Icons.exit_to_app,
+ color: Colors.red,
+ ),
const SizedBox(width: 12),
Text(
context.l10n.community_delete,
@@ -1436,128 +1611,23 @@ class _ChannelsScreenState extends State
data: community.toQrJson(),
title: context.l10n.community_qrTitle,
instructions: context.l10n.community_qrInstructions(community.name),
- embeddedImage: Image.asset('assets/images/mesh-icon.png', width: 40, height: 40),
- );
- }
-
- /// Regenerate the community secret and update all associated channels
- void _regenerateCommunitySecret(BuildContext context, Community community) {
- showDialog(
- context: context,
- builder: (dialogContext) => AlertDialog(
- title: Text(dialogContext.l10n.community_regenerateSecret),
- content: Text(dialogContext.l10n.community_regenerateSecretConfirm(community.name)),
- actions: [
- TextButton(
- onPressed: () => Navigator.pop(dialogContext),
- child: Text(dialogContext.l10n.common_cancel),
- ),
- FilledButton(
- onPressed: () async {
- Navigator.pop(dialogContext);
-
- final connector = context.read();
- final newCommunity = community.withRegeneratedSecret();
-
- // Update channel PSKs
- await _updateCommunityChannelPsks(connector, community, newCommunity);
-
- // Save updated community
- await _communityStore.updateCommunity(newCommunity);
- _loadCommunities();
-
- if (context.mounted) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(context.l10n.community_secretRegenerated(community.name))),
- );
-
- // Show the new QR code
- _showCommunityQrDialog(context, newCommunity);
- }
- },
- child: Text(dialogContext.l10n.community_regenerate),
- ),
- ],
+ embeddedImage: Image.asset(
+ 'assets/images/mesh-icon.png',
+ width: 40,
+ height: 40,
),
);
}
- /// Update community secret from a scanned QR code
- void _updateCommunitySecret(BuildContext context, Community community) async {
- final result = await Navigator.push(
- context,
- MaterialPageRoute(
- builder: (context) => _CommunitySecretScannerScreen(
- communityName: community.name,
- ),
- ),
- );
-
- if (result == null || !context.mounted) return;
-
- final newSecret = Community.extractSecretFromQrData(result);
- if (newSecret == null) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(context.l10n.community_invalidQrCode)),
- );
- return;
- }
-
- final connector = context.read();
- final newCommunity = community.withNewSecret(newSecret);
-
- // Update channel PSKs
- await _updateCommunityChannelPsks(connector, community, newCommunity);
-
- // Save updated community
- await _communityStore.updateCommunity(newCommunity);
- _loadCommunities();
-
- if (context.mounted) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(context.l10n.community_secretUpdated(community.name))),
- );
- }
- }
-
- /// Update PSKs for all channels belonging to a community
- Future _updateCommunityChannelPsks(
- MeshCoreConnector connector,
- Community oldCommunity,
- Community newCommunity,
- ) async {
- // Find and update the public channel
- final oldPublicPskHex = Channel.formatPskHex(oldCommunity.deriveCommunityPublicPsk());
- final newPublicPsk = newCommunity.deriveCommunityPublicPsk();
-
- for (final channel in connector.channels) {
- if (channel.pskHex == oldPublicPskHex) {
- await connector.setChannel(channel.index, channel.name, newPublicPsk);
- break;
- }
- }
-
- // Find and update hashtag channels
- for (final hashtag in oldCommunity.hashtagChannels) {
- final oldHashtagPskHex = Channel.formatPskHex(oldCommunity.deriveCommunityHashtagPsk(hashtag));
- final newHashtagPsk = newCommunity.deriveCommunityHashtagPsk(hashtag);
-
- for (final channel in connector.channels) {
- if (channel.pskHex == oldHashtagPskHex) {
- await connector.setChannel(channel.index, channel.name, newHashtagPsk);
- break;
- }
- }
- }
- }
-
void _confirmLeaveCommunity(BuildContext context, Community community) {
final connector = context.read();
-
+
// Find all channels that belong to this community
List communityChannels = [];
- final publicPskHex = Channel.formatPskHex(community.deriveCommunityPublicPsk());
-
+ final publicPskHex = Channel.formatPskHex(
+ community.deriveCommunityPublicPsk(),
+ );
+
for (final channel in connector.channels) {
// Check if it's the public channel
if (channel.pskHex == publicPskHex) {
@@ -1566,16 +1636,18 @@ class _ChannelsScreenState extends State
}
// Check if it's a hashtag channel
for (final hashtag in community.hashtagChannels) {
- final hashtagPskHex = Channel.formatPskHex(community.deriveCommunityHashtagPsk(hashtag));
+ final hashtagPskHex = Channel.formatPskHex(
+ community.deriveCommunityHashtagPsk(hashtag),
+ );
if (channel.pskHex == hashtagPskHex) {
communityChannels.add(channel);
break;
}
}
}
-
+
final channelCount = communityChannels.length;
-
+
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
@@ -1593,19 +1665,23 @@ class _ChannelsScreenState extends State
TextButton(
onPressed: () async {
Navigator.pop(dialogContext);
-
+
// Delete all community channels from the device
for (final channel in communityChannels) {
await connector.deleteChannel(channel.index);
}
-
+
// Remove community from store
await _communityStore.removeCommunity(community.id);
_loadCommunities();
-
+
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(context.l10n.community_deleted(community.name))),
+ SnackBar(
+ content: Text(
+ context.l10n.community_deleted(community.name),
+ ),
+ ),
);
}
},
@@ -1619,32 +1695,3 @@ class _ChannelsScreenState extends State
);
}
}
-
-/// Simple scanner screen for updating community secret
-class _CommunitySecretScannerScreen extends StatelessWidget {
- final String communityName;
-
- const _CommunitySecretScannerScreen({required this.communityName});
-
- @override
- Widget build(BuildContext context) {
- return Scaffold(
- appBar: AppBar(
- title: Text(context.l10n.community_updateSecret),
- ),
- body: QrScannerWidget(
- onScanned: (data) {
- Navigator.pop(context, data);
- },
- validator: (data) => Community.isValidQrData(data),
- onValidationFailed: (data) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(context.l10n.community_invalidQrCode)),
- );
- },
- instructions: context.l10n.community_scanToUpdateSecret(communityName),
- overlay: const ScannerCornerOverlay(),
- ),
- );
- }
-}
diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart
index 5764bc88..a1707b95 100644
--- a/lib/screens/chat_screen.dart
+++ b/lib/screens/chat_screen.dart
@@ -5,11 +5,13 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
+import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:provider/provider.dart';
import 'package:latlong2/latlong.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
+import '../helpers/link_handler.dart';
import '../helpers/utf8_length_limiter.dart';
import '../models/channel_message.dart';
import '../models/contact.dart';
@@ -988,11 +990,21 @@ class _MessageBubble extends StatelessWidget {
fallbackTextColor: textColor.withValues(alpha: 0.7),
)
else
- Text(
- messageText,
+ Linkify(
+ text: messageText,
style: TextStyle(
color: textColor,
),
+ linkStyle: const TextStyle(
+ color: Colors.green,
+ decoration: TextDecoration.underline,
+ ),
+ options: const LinkifyOptions(
+ humanize: false,
+ defaultToHttps: false,
+ ),
+ linkifiers: const [UrlLinkifier()],
+ onOpen: (link) => LinkHandler.handleLinkTap(context, link.url),
),
if (isOutgoing && message.retryCount > 0) ...[
const SizedBox(height: 4),
diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc
index e71a16d2..f6f23bfe 100644
--- a/linux/flutter/generated_plugin_registrant.cc
+++ b/linux/flutter/generated_plugin_registrant.cc
@@ -6,6 +6,10 @@
#include "generated_plugin_registrant.h"
+#include
void fl_register_plugins(FlPluginRegistry* registry) {
+ g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
+ fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
+ url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
}
diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake
index 2e1de87a..f16b4c34 100644
--- a/linux/flutter/generated_plugins.cmake
+++ b/linux/flutter/generated_plugins.cmake
@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
+ url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift
index fdb93ad1..31428df1 100644
--- a/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -12,6 +12,7 @@ import package_info_plus
import path_provider_foundation
import shared_preferences_foundation
import sqflite_darwin
+import url_launcher_macos
import wakelock_plus
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
@@ -22,5 +23,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
+ UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
}
diff --git a/pubspec.lock b/pubspec.lock
index de12f546..2e2b7466 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -262,6 +262,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.13.1"
+ flutter_linkify:
+ dependency: "direct main"
+ description:
+ name: flutter_linkify
+ sha256: "74669e06a8f358fee4512b4320c0b80e51cffc496607931de68d28f099254073"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.0.0"
flutter_lints:
dependency: "direct dev"
description:
@@ -397,6 +405,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.2"
+ linkify:
+ dependency: transitive
+ description:
+ name: linkify
+ sha256: "4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.0.0"
lints:
dependency: transitive
description:
@@ -818,6 +834,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.3.1"
+ url_launcher:
+ dependency: "direct main"
+ description:
+ name: url_launcher
+ sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.3.2"
+ url_launcher_android:
+ dependency: transitive
+ description:
+ name: url_launcher_android
+ sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.3.28"
+ url_launcher_ios:
+ dependency: transitive
+ description:
+ name: url_launcher_ios
+ sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.3.6"
+ url_launcher_linux:
+ dependency: transitive
+ description:
+ name: url_launcher_linux
+ sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.2.2"
+ url_launcher_macos:
+ dependency: transitive
+ description:
+ name: url_launcher_macos
+ sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.2.5"
+ url_launcher_platform_interface:
+ dependency: transitive
+ description:
+ name: url_launcher_platform_interface
+ sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.2"
+ url_launcher_web:
+ dependency: transitive
+ description:
+ name: url_launcher_web
+ sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.2"
+ url_launcher_windows:
+ dependency: transitive
+ description:
+ name: url_launcher_windows
+ sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.5"
uuid:
dependency: "direct main"
description:
@@ -907,5 +987,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
- dart: ">=3.9.2 <4.0.0"
- flutter: ">=3.35.0"
+ dart: ">=3.10.0 <4.0.0"
+ flutter: ">=3.38.0"
diff --git a/pubspec.yaml b/pubspec.yaml
index 490c83d6..c9e9120b 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -55,6 +55,8 @@ dependencies:
package_info_plus: ^8.0.0
mobile_scanner: ^6.0.0 # QR/barcode scanning
qr_flutter: ^4.1.0 # QR code generation
+ url_launcher: ^6.3.0 # Launch URLs in system browser
+ flutter_linkify: ^6.0.0 # Auto-detect and linkify URLs in text
dev_dependencies:
flutter_test:
diff --git a/untranslated.json b/untranslated.json
index 2138a62f..9e26dfee 100644
--- a/untranslated.json
+++ b/untranslated.json
@@ -1,121 +1 @@
-{
- "bg": [
- "community_regenerateSecret",
- "community_regenerateSecretConfirm",
- "community_regenerate",
- "community_secretRegenerated",
- "community_updateSecret",
- "community_secretUpdated",
- "community_scanToUpdateSecret"
- ],
-
- "de": [
- "community_regenerateSecret",
- "community_regenerateSecretConfirm",
- "community_regenerate",
- "community_secretRegenerated",
- "community_updateSecret",
- "community_secretUpdated",
- "community_scanToUpdateSecret"
- ],
-
- "es": [
- "community_regenerateSecret",
- "community_regenerateSecretConfirm",
- "community_regenerate",
- "community_secretRegenerated",
- "community_updateSecret",
- "community_secretUpdated",
- "community_scanToUpdateSecret"
- ],
-
- "fr": [
- "community_regenerateSecret",
- "community_regenerateSecretConfirm",
- "community_regenerate",
- "community_secretRegenerated",
- "community_updateSecret",
- "community_secretUpdated",
- "community_scanToUpdateSecret"
- ],
-
- "it": [
- "community_regenerateSecret",
- "community_regenerateSecretConfirm",
- "community_regenerate",
- "community_secretRegenerated",
- "community_updateSecret",
- "community_secretUpdated",
- "community_scanToUpdateSecret"
- ],
-
- "nl": [
- "community_regenerateSecret",
- "community_regenerateSecretConfirm",
- "community_regenerate",
- "community_secretRegenerated",
- "community_updateSecret",
- "community_secretUpdated",
- "community_scanToUpdateSecret"
- ],
-
- "pl": [
- "community_regenerateSecret",
- "community_regenerateSecretConfirm",
- "community_regenerate",
- "community_secretRegenerated",
- "community_updateSecret",
- "community_secretUpdated",
- "community_scanToUpdateSecret"
- ],
-
- "pt": [
- "community_regenerateSecret",
- "community_regenerateSecretConfirm",
- "community_regenerate",
- "community_secretRegenerated",
- "community_updateSecret",
- "community_secretUpdated",
- "community_scanToUpdateSecret"
- ],
-
- "sk": [
- "community_regenerateSecret",
- "community_regenerateSecretConfirm",
- "community_regenerate",
- "community_secretRegenerated",
- "community_updateSecret",
- "community_secretUpdated",
- "community_scanToUpdateSecret"
- ],
-
- "sl": [
- "community_regenerateSecret",
- "community_regenerateSecretConfirm",
- "community_regenerate",
- "community_secretRegenerated",
- "community_updateSecret",
- "community_secretUpdated",
- "community_scanToUpdateSecret"
- ],
-
- "sv": [
- "community_regenerateSecret",
- "community_regenerateSecretConfirm",
- "community_regenerate",
- "community_secretRegenerated",
- "community_updateSecret",
- "community_secretUpdated",
- "community_scanToUpdateSecret"
- ],
-
- "zh": [
- "community_regenerateSecret",
- "community_regenerateSecretConfirm",
- "community_regenerate",
- "community_secretRegenerated",
- "community_updateSecret",
- "community_secretUpdated",
- "community_scanToUpdateSecret"
- ]
-}
+{}
\ No newline at end of file
diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc
index c158b14b..eeb548fa 100644
--- a/windows/flutter/generated_plugin_registrant.cc
+++ b/windows/flutter/generated_plugin_registrant.cc
@@ -7,8 +7,11 @@
#include "generated_plugin_registrant.h"
#include
+#include
void RegisterPlugins(flutter::PluginRegistry* registry) {
FlutterBluePlusPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterBluePlusPlugin"));
+ UrlLauncherWindowsRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}
diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake
index 905321af..68825d8b 100644
--- a/windows/flutter/generated_plugins.cmake
+++ b/windows/flutter/generated_plugins.cmake
@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
flutter_blue_plus_winrt
+ url_launcher_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST