Add support for private and hashtag channels in localization and channel management

- Updated Polish, Portuguese, Slovak, Slovenian, Swedish, and Chinese localization files to include new strings for creating and joining private channels, as well as joining hashtag channels.
- Enhanced the channel management UI to allow users to create and join private channels, join public channels, and join channels via hashtags.
- Implemented PSK derivation from hashtags using SHA256 in the Channel model.
- Improved the translation script to handle missing keys and translate all locales efficiently.
This commit is contained in:
zjs81
2026-01-16 19:06:39 -07:00
parent a14462978d
commit 14ff8250c0
30 changed files with 1250 additions and 141 deletions
+13 -1
View File
@@ -1335,5 +1335,17 @@
"listFilter_repeaters": "Повторители",
"listFilter_roomServers": "Сървъри на стая",
"listFilter_unreadOnly": "Само непрочетените",
"listFilter_newGroup": "Нова група"
"listFilter_newGroup": "Нова група",
"channels_createPrivateChannel": "Създай Частен Канал",
"channels_joinPrivateChannel": "Присъедини се към Частен Канал",
"channels_createPrivateChannelDesc": "Защитено с таен ключ.",
"channels_joinPrivateChannelDesc": "Ръчно въведете таен ключ.",
"channels_joinPublicChannel": "Присъединете се към Публичния канал",
"channels_joinPublicChannelDesc": "Всеки може да се присъедини към този канал.",
"channels_joinHashtagChannel": "Присъедини се към Хаштаг Канал",
"channels_joinHashtagChannelDesc": "Всеки може да се присъедини към хаштаговите канали.",
"channels_scanQrCode": "Сканирайте QR код",
"channels_scanQrCodeComingSoon": "Ще излезе скоро",
"channels_enterHashtag": "Въведете хаштаг",
"channels_hashtagHint": "напр. #отбор"
}
+13 -1
View File
@@ -1335,5 +1335,17 @@
"listFilter_repeaters": "Wiederholer",
"listFilter_roomServers": "Raumserver",
"listFilter_unreadOnly": "Nur nicht gelesen",
"listFilter_newGroup": "Neue Gruppe"
"listFilter_newGroup": "Neue Gruppe",
"channels_joinPrivateChannel": "Treten Sie einem privaten Kanal bei",
"channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.",
"channels_createPrivateChannel": "Erstelle einen privaten Kanal",
"channels_createPrivateChannelDesc": "Verschlüsselt mit einem geheimen Schlüssel.",
"channels_joinPublicChannel": "Tritt dem öffentlichen Kanal bei",
"channels_joinPublicChannelDesc": "Jeder kann diesem Kanal beitreten.",
"channels_joinHashtagChannel": "Treten Sie einem Hashtag-Kanal bei",
"channels_joinHashtagChannelDesc": "Jeder kann sich bei Hashtag-Kanälen beteiligen.",
"channels_scanQrCode": "Scannen Sie einen QR-Code",
"channels_scanQrCodeComingSoon": "Bald verfügbar",
"channels_enterHashtag": "Gib Hashtag ein",
"channels_hashtagHint": "z.B. #team"
}
+12
View File
@@ -361,6 +361,18 @@
"channels_sortAZ": "A-Z",
"channels_sortLatestMessages": "Latest messages",
"channels_sortUnread": "Unread",
"channels_createPrivateChannel": "Create a Private Channel",
"channels_createPrivateChannelDesc": "Secured with a secret key.",
"channels_joinPrivateChannel": "Join a Private Channel",
"channels_joinPrivateChannelDesc": "Manually enter a secret key.",
"channels_joinPublicChannel": "Join the Public Channel",
"channels_joinPublicChannelDesc": "Anyone can join this channel.",
"channels_joinHashtagChannel": "Join a Hashtag Channel",
"channels_joinHashtagChannelDesc": "Anyone can join hashtag channels.",
"channels_scanQrCode": "Scan a QR Code",
"channels_scanQrCodeComingSoon": "Coming soon",
"channels_enterHashtag": "Enter hashtag",
"channels_hashtagHint": "e.g. #team",
"chat_noMessages": "No messages yet",
"chat_sendMessageToStart": "Send a message to get started",
+13 -1
View File
@@ -1335,5 +1335,17 @@
"listFilter_repeaters": "Repetidores",
"listFilter_roomServers": "Servidores de la sala",
"listFilter_unreadOnly": "Solo sin leer",
"listFilter_newGroup": "Nuevo grupo"
"listFilter_newGroup": "Nuevo grupo",
"channels_joinPrivateChannel": "Únete a un Canal Privado",
"channels_createPrivateChannel": "Crear un Canal Privado",
"channels_createPrivateChannelDesc": "Cifrado con una clave secreta.",
"channels_joinPrivateChannelDesc": "Introducir manualmente una clave secreta.",
"channels_joinPublicChannel": "Únete al Canal Público",
"channels_joinPublicChannelDesc": "Cualquiera puede unirse a este canal.",
"channels_joinHashtagChannel": "Únete a un Canal con Hashtag",
"channels_joinHashtagChannelDesc": "Cualquiera puede unirse a los canales de hashtag.",
"channels_scanQrCode": "Escanear un Código QR",
"channels_scanQrCodeComingSoon": "Próximamente",
"channels_enterHashtag": "Introducir hashtag",
"channels_hashtagHint": "ej. #equipo"
}
+13 -1
View File
@@ -1335,5 +1335,17 @@
"listFilter_repeaters": "Répéteurs",
"listFilter_roomServers": "Serveurs de pièce",
"listFilter_unreadOnly": "Messages non lus seulement",
"listFilter_newGroup": "Nouvelle groupe"
"listFilter_newGroup": "Nouvelle groupe",
"channels_createPrivateChannelDesc": "Sécurisé avec une clé secrète.",
"channels_joinPrivateChannel": "Rejoindre un Canal Privé",
"channels_createPrivateChannel": "Créer un Canal Privé",
"channels_joinPrivateChannelDesc": "Entrer manuellement une clé secrète.",
"channels_joinPublicChannel": "Rejoindre le canal public",
"channels_joinPublicChannelDesc": "Tout le monde peut rejoindre ce canal.",
"channels_joinHashtagChannel": "Rejoindre un Canal Hashtag",
"channels_joinHashtagChannelDesc": "N'importe qui peut rejoindre les canaux #hashtag.",
"channels_scanQrCode": "Scanner un code QR",
"channels_scanQrCodeComingSoon": "Bientôt disponible",
"channels_enterHashtag": "Entrez le hashtag",
"channels_hashtagHint": "ex. #équipe"
}
+13 -1
View File
@@ -1335,5 +1335,17 @@
"listFilter_repeaters": "Ripetitori",
"listFilter_roomServers": "Server della stanza",
"listFilter_unreadOnly": "Solo non letto",
"listFilter_newGroup": "Nuovo gruppo"
"listFilter_newGroup": "Nuovo gruppo",
"channels_createPrivateChannel": "Crea un Canale Privato",
"channels_createPrivateChannelDesc": "Protetta con una chiave segreta.",
"channels_joinPrivateChannel": "Unisciti a un Canale Privato",
"channels_joinPrivateChannelDesc": "Inserire manualmente una chiave segreta.",
"channels_joinPublicChannel": "Unisciti al Canale Pubblico",
"channels_joinPublicChannelDesc": "Chiunque può unirsi a questo canale.",
"channels_joinHashtagChannel": "Unisciti a un Canale con Hashtag",
"channels_joinHashtagChannelDesc": "Chiunque può unirsi ai canali hashtag.",
"channels_scanQrCode": "Scansiona un codice QR",
"channels_scanQrCodeComingSoon": "Arriverà presto",
"channels_enterHashtag": "Inserisci hashtag",
"channels_hashtagHint": "es. #team"
}
+72
View File
@@ -1596,6 +1596,78 @@ abstract class AppLocalizations {
/// **'Unread'**
String get channels_sortUnread;
/// No description provided for @channels_createPrivateChannel.
///
/// In en, this message translates to:
/// **'Create a Private Channel'**
String get channels_createPrivateChannel;
/// No description provided for @channels_createPrivateChannelDesc.
///
/// In en, this message translates to:
/// **'Secured with a secret key.'**
String get channels_createPrivateChannelDesc;
/// No description provided for @channels_joinPrivateChannel.
///
/// In en, this message translates to:
/// **'Join a Private Channel'**
String get channels_joinPrivateChannel;
/// No description provided for @channels_joinPrivateChannelDesc.
///
/// In en, this message translates to:
/// **'Manually enter a secret key.'**
String get channels_joinPrivateChannelDesc;
/// No description provided for @channels_joinPublicChannel.
///
/// In en, this message translates to:
/// **'Join the Public Channel'**
String get channels_joinPublicChannel;
/// No description provided for @channels_joinPublicChannelDesc.
///
/// In en, this message translates to:
/// **'Anyone can join this channel.'**
String get channels_joinPublicChannelDesc;
/// No description provided for @channels_joinHashtagChannel.
///
/// In en, this message translates to:
/// **'Join a Hashtag Channel'**
String get channels_joinHashtagChannel;
/// No description provided for @channels_joinHashtagChannelDesc.
///
/// In en, this message translates to:
/// **'Anyone can join hashtag channels.'**
String get channels_joinHashtagChannelDesc;
/// No description provided for @channels_scanQrCode.
///
/// In en, this message translates to:
/// **'Scan a QR Code'**
String get channels_scanQrCode;
/// No description provided for @channels_scanQrCodeComingSoon.
///
/// In en, this message translates to:
/// **'Coming soon'**
String get channels_scanQrCodeComingSoon;
/// No description provided for @channels_enterHashtag.
///
/// In en, this message translates to:
/// **'Enter hashtag'**
String get channels_enterHashtag;
/// No description provided for @channels_hashtagHint.
///
/// In en, this message translates to:
/// **'e.g. #team'**
String get channels_hashtagHint;
/// No description provided for @chat_noMessages.
///
/// In en, this message translates to:
+39
View File
@@ -830,6 +830,45 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get channels_sortUnread => 'Непрочетено';
@override
String get channels_createPrivateChannel => 'Създай Частен Канал';
@override
String get channels_createPrivateChannelDesc => 'Защитено с таен ключ.';
@override
String get channels_joinPrivateChannel => 'Присъедини се към Частен Канал';
@override
String get channels_joinPrivateChannelDesc => 'Ръчно въведете таен ключ.';
@override
String get channels_joinPublicChannel =>
'Присъединете се към Публичния канал';
@override
String get channels_joinPublicChannelDesc =>
'Всеки може да се присъедини към този канал.';
@override
String get channels_joinHashtagChannel => 'Присъедини се към Хаштаг Канал';
@override
String get channels_joinHashtagChannelDesc =>
'Всеки може да се присъедини към хаштаговите канали.';
@override
String get channels_scanQrCode => 'Сканирайте QR код';
@override
String get channels_scanQrCodeComingSoon => 'Ще излезе скоро';
@override
String get channels_enterHashtag => 'Въведете хаштаг';
@override
String get channels_hashtagHint => 'напр. #отбор';
@override
String get chat_noMessages => 'Няма съобщения.';
+42
View File
@@ -828,6 +828,48 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get channels_sortUnread => 'Unlescht';
@override
String get channels_createPrivateChannel => 'Erstelle einen privaten Kanal';
@override
String get channels_createPrivateChannelDesc =>
'Verschlüsselt mit einem geheimen Schlüssel.';
@override
String get channels_joinPrivateChannel =>
'Treten Sie einem privaten Kanal bei';
@override
String get channels_joinPrivateChannelDesc =>
'Manuelle Eingabe eines geheimen Schlüssels.';
@override
String get channels_joinPublicChannel => 'Tritt dem öffentlichen Kanal bei';
@override
String get channels_joinPublicChannelDesc =>
'Jeder kann diesem Kanal beitreten.';
@override
String get channels_joinHashtagChannel =>
'Treten Sie einem Hashtag-Kanal bei';
@override
String get channels_joinHashtagChannelDesc =>
'Jeder kann sich bei Hashtag-Kanälen beteiligen.';
@override
String get channels_scanQrCode => 'Scannen Sie einen QR-Code';
@override
String get channels_scanQrCodeComingSoon => 'Bald verfügbar';
@override
String get channels_enterHashtag => 'Gib Hashtag ein';
@override
String get channels_hashtagHint => 'z.B. #team';
@override
String get chat_noMessages => 'Noch keine Nachrichten.';
+37
View File
@@ -818,6 +818,43 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get channels_sortUnread => 'Unread';
@override
String get channels_createPrivateChannel => 'Create a Private Channel';
@override
String get channels_createPrivateChannelDesc => 'Secured with a secret key.';
@override
String get channels_joinPrivateChannel => 'Join a Private Channel';
@override
String get channels_joinPrivateChannelDesc => 'Manually enter a secret key.';
@override
String get channels_joinPublicChannel => 'Join the Public Channel';
@override
String get channels_joinPublicChannelDesc => 'Anyone can join this channel.';
@override
String get channels_joinHashtagChannel => 'Join a Hashtag Channel';
@override
String get channels_joinHashtagChannelDesc =>
'Anyone can join hashtag channels.';
@override
String get channels_scanQrCode => 'Scan a QR Code';
@override
String get channels_scanQrCodeComingSoon => 'Coming soon';
@override
String get channels_enterHashtag => 'Enter hashtag';
@override
String get channels_hashtagHint => 'e.g. #team';
@override
String get chat_noMessages => 'No messages yet';
+40
View File
@@ -829,6 +829,46 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get channels_sortUnread => 'Sin leer';
@override
String get channels_createPrivateChannel => 'Crear un Canal Privado';
@override
String get channels_createPrivateChannelDesc =>
'Cifrado con una clave secreta.';
@override
String get channels_joinPrivateChannel => 'Únete a un Canal Privado';
@override
String get channels_joinPrivateChannelDesc =>
'Introducir manualmente una clave secreta.';
@override
String get channels_joinPublicChannel => 'Únete al Canal Público';
@override
String get channels_joinPublicChannelDesc =>
'Cualquiera puede unirse a este canal.';
@override
String get channels_joinHashtagChannel => 'Únete a un Canal con Hashtag';
@override
String get channels_joinHashtagChannelDesc =>
'Cualquiera puede unirse a los canales de hashtag.';
@override
String get channels_scanQrCode => 'Escanear un Código QR';
@override
String get channels_scanQrCodeComingSoon => 'Próximamente';
@override
String get channels_enterHashtag => 'Introducir hashtag';
@override
String get channels_hashtagHint => 'ej. #equipo';
@override
String get chat_noMessages => 'Aún no hay mensajes';
+40
View File
@@ -830,6 +830,46 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get channels_sortUnread => 'Non lu';
@override
String get channels_createPrivateChannel => 'Créer un Canal Privé';
@override
String get channels_createPrivateChannelDesc =>
'Sécurisé avec une clé secrète.';
@override
String get channels_joinPrivateChannel => 'Rejoindre un Canal Privé';
@override
String get channels_joinPrivateChannelDesc =>
'Entrer manuellement une clé secrète.';
@override
String get channels_joinPublicChannel => 'Rejoindre le canal public';
@override
String get channels_joinPublicChannelDesc =>
'Tout le monde peut rejoindre ce canal.';
@override
String get channels_joinHashtagChannel => 'Rejoindre un Canal Hashtag';
@override
String get channels_joinHashtagChannelDesc =>
'N\'importe qui peut rejoindre les canaux #hashtag.';
@override
String get channels_scanQrCode => 'Scanner un code QR';
@override
String get channels_scanQrCodeComingSoon => 'Bientôt disponible';
@override
String get channels_enterHashtag => 'Entrez le hashtag';
@override
String get channels_hashtagHint => 'ex. #équipe';
@override
String get chat_noMessages => 'Aucun message pour le moment.';
+40
View File
@@ -827,6 +827,46 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get channels_sortUnread => 'Non letto';
@override
String get channels_createPrivateChannel => 'Crea un Canale Privato';
@override
String get channels_createPrivateChannelDesc =>
'Protetta con una chiave segreta.';
@override
String get channels_joinPrivateChannel => 'Unisciti a un Canale Privato';
@override
String get channels_joinPrivateChannelDesc =>
'Inserire manualmente una chiave segreta.';
@override
String get channels_joinPublicChannel => 'Unisciti al Canale Pubblico';
@override
String get channels_joinPublicChannelDesc =>
'Chiunque può unirsi a questo canale.';
@override
String get channels_joinHashtagChannel => 'Unisciti a un Canale con Hashtag';
@override
String get channels_joinHashtagChannelDesc =>
'Chiunque può unirsi ai canali hashtag.';
@override
String get channels_scanQrCode => 'Scansiona un codice QR';
@override
String get channels_scanQrCodeComingSoon => 'Arriverà presto';
@override
String get channels_enterHashtag => 'Inserisci hashtag';
@override
String get channels_hashtagHint => 'es. #team';
@override
String get chat_noMessages => 'Nessun messaggio ancora';
+40
View File
@@ -824,6 +824,46 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get channels_sortUnread => 'Ongelezen';
@override
String get channels_createPrivateChannel => 'Maak een Privé Kanaal';
@override
String get channels_createPrivateChannelDesc =>
'Beveiligd met een geheime sleutel.';
@override
String get channels_joinPrivateChannel => 'Sluit een Privé Kanaal aan';
@override
String get channels_joinPrivateChannelDesc =>
'Handmatig een geheime sleutel invoeren.';
@override
String get channels_joinPublicChannel => 'Sluit het Open Kanaal';
@override
String get channels_joinPublicChannelDesc =>
'Iedereen kan dit kanaal aanmelden.';
@override
String get channels_joinHashtagChannel => 'Sluit een Hashtag Kanaal';
@override
String get channels_joinHashtagChannelDesc =>
'Iedereen kan lid worden van hashtag-kanalen.';
@override
String get channels_scanQrCode => 'Scan een QR-code';
@override
String get channels_scanQrCodeComingSoon => 'Komt later';
@override
String get channels_enterHashtag => 'Voer hashtag in';
@override
String get channels_hashtagHint => 'bijv. #team';
@override
String get chat_noMessages => 'Nog geen berichten.';
+40
View File
@@ -828,6 +828,46 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get channels_sortUnread => 'Niezgłoszone';
@override
String get channels_createPrivateChannel => 'Utwórz Prywatny Kanał';
@override
String get channels_createPrivateChannelDesc =>
'Zabezpieczone kluczem szyfrowym.';
@override
String get channels_joinPrivateChannel => 'Dołącz do Prywatnego Kanału';
@override
String get channels_joinPrivateChannelDesc => 'Ręcznie wprowadź klucz tajny.';
@override
String get channels_joinPublicChannel => 'Dołącz do kanału publicznego.';
@override
String get channels_joinPublicChannelDesc =>
'Każdy może dołączyć do tego kanału.';
@override
String get channels_joinHashtagChannel =>
'Dołącz do kanału oznaczanego hashtagiem';
@override
String get channels_joinHashtagChannelDesc =>
'Każdy może dołączyć do kanałów z hashtagami.';
@override
String get channels_scanQrCode => 'Skanuj kod QR';
@override
String get channels_scanQrCodeComingSoon => 'Wkrótce';
@override
String get channels_enterHashtag => 'Wprowadź hashtag';
@override
String get channels_hashtagHint => 'np. #zespół';
@override
String get chat_noMessages => 'Brak jeszcze wiadomości';
+40
View File
@@ -829,6 +829,46 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get channels_sortUnread => 'Não lido';
@override
String get channels_createPrivateChannel => 'Criar um Canal Privado';
@override
String get channels_createPrivateChannelDesc =>
'Protegido com uma chave secreta.';
@override
String get channels_joinPrivateChannel => 'Junte-se a um Canal Privado';
@override
String get channels_joinPrivateChannelDesc =>
'Inserir uma chave secreta manualmente.';
@override
String get channels_joinPublicChannel => 'Junte-se ao Canal Público';
@override
String get channels_joinPublicChannelDesc =>
'Qualquer pessoa pode entrar neste canal.';
@override
String get channels_joinHashtagChannel => 'Junte-se a um Canal com Hashtag';
@override
String get channels_joinHashtagChannelDesc =>
'Qualquer pessoa pode participar de canais com hashtag.';
@override
String get channels_scanQrCode => 'Digitalizar um Código QR';
@override
String get channels_scanQrCodeComingSoon => 'Em breve';
@override
String get channels_enterHashtag => 'Insira hashtag';
@override
String get channels_hashtagHint => 'ex. #equipe';
@override
String get chat_noMessages => 'Ainda não existem mensagens.';
+39
View File
@@ -824,6 +824,45 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get channels_sortUnread => 'Nezriadené';
@override
String get channels_createPrivateChannel => 'Vytvorte súkromný kanál';
@override
String get channels_createPrivateChannelDesc =>
'Zabezpečené pomocou tajného kľúča.';
@override
String get channels_joinPrivateChannel => 'Pripojiť sa k súkromnému kanálu';
@override
String get channels_joinPrivateChannelDesc => 'Ručne zadajte tajný kľúč.';
@override
String get channels_joinPublicChannel => 'Pripojte sa k verejnému kanálu';
@override
String get channels_joinPublicChannelDesc =>
'Któvek sátó na tutó kanalizovát.';
@override
String get channels_joinHashtagChannel => 'Pripojte sa k Hashtag Kanálu';
@override
String get channels_joinHashtagChannelDesc =>
'Ktoekolikoľvek sa môže pridať do hashtag kanálov.';
@override
String get channels_scanQrCode => 'Skenujte QR kód';
@override
String get channels_scanQrCodeComingSoon => 'Čoskoro';
@override
String get channels_enterHashtag => 'Zadajte hashtag';
@override
String get channels_hashtagHint => 'napr. #tím';
@override
String get chat_noMessages => 'Zatiaľ žiadne správy.';
+39
View File
@@ -824,6 +824,45 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get channels_sortUnread => 'Nerešeno';
@override
String get channels_createPrivateChannel => 'Ustvari zasebno kanal.';
@override
String get channels_createPrivateChannelDesc =>
'Varno zaklenjeno s skrivnim ključem.';
@override
String get channels_joinPrivateChannel => 'Pridružite se zasebni skupini';
@override
String get channels_joinPrivateChannelDesc => 'Ročno vnesite zaporni ključ.';
@override
String get channels_joinPublicChannel => 'Pridružite se javnemu kanalu';
@override
String get channels_joinPublicChannelDesc =>
'Kdor karkoli je, lahko se pridruži tej skupini.';
@override
String get channels_joinHashtagChannel => 'Pridružite se Kanalu z Hashtagom';
@override
String get channels_joinHashtagChannelDesc =>
'Kdor karkoli, lahko se pridruži hashtag kanalom.';
@override
String get channels_scanQrCode => 'Skeniraj QR kodo';
@override
String get channels_scanQrCodeComingSoon => 'Prihajajoča';
@override
String get channels_enterHashtag => 'Vnesite hashtag';
@override
String get channels_hashtagHint => 'npr. #ekipa';
@override
String get chat_noMessages => 'Še ni sporočil.';
+40
View File
@@ -817,6 +817,46 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get channels_sortUnread => 'Oläst';
@override
String get channels_createPrivateChannel => 'Skapa en privat kanal';
@override
String get channels_createPrivateChannelDesc =>
'Skyddat med en hemlig nyckel.';
@override
String get channels_joinPrivateChannel => 'Gå med i en Privat Kanal';
@override
String get channels_joinPrivateChannelDesc =>
'Ange en hemlig nyckel manuellt.';
@override
String get channels_joinPublicChannel => 'Gå med i den Offentliga Kanalen';
@override
String get channels_joinPublicChannelDesc =>
'Vem som helst kan gå med i denna kanal.';
@override
String get channels_joinHashtagChannel => 'Gå med i en Hashtagkanal';
@override
String get channels_joinHashtagChannelDesc =>
'Väldigt enkelt att gå med i hashtag-kanaler.';
@override
String get channels_scanQrCode => 'Skanna en QR-kod';
@override
String get channels_scanQrCodeComingSoon => 'Kommer snart';
@override
String get channels_enterHashtag => 'Ange hashtag';
@override
String get channels_hashtagHint => 't.ex. #team';
@override
String get chat_noMessages => 'Inga meddelanden ännu';
+36
View File
@@ -789,6 +789,42 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get channels_sortUnread => '未读';
@override
String get channels_createPrivateChannel => '创建私聊频道';
@override
String get channels_createPrivateChannelDesc => '使用密钥保护。';
@override
String get channels_joinPrivateChannel => '加入私密频道';
@override
String get channels_joinPrivateChannelDesc => '手动输入密钥。';
@override
String get channels_joinPublicChannel => '加入公共频道';
@override
String get channels_joinPublicChannelDesc => '任何人都可以加入这个频道。';
@override
String get channels_joinHashtagChannel => '加入标签频道';
@override
String get channels_joinHashtagChannelDesc => '任何人都可以加入话题频道。';
@override
String get channels_scanQrCode => '扫描二维码';
@override
String get channels_scanQrCodeComingSoon => '即将到来';
@override
String get channels_enterHashtag => '输入标签';
@override
String get channels_hashtagHint => '例如 #团队';
@override
String get chat_noMessages => '目前还没有消息';
+13 -1
View File
@@ -1335,5 +1335,17 @@
"listFilter_repeaters": "Repeaters",
"listFilter_roomServers": "Roomservers",
"listFilter_unreadOnly": "Alleen ongelezen",
"listFilter_newGroup": "Nieuwe groep"
"listFilter_newGroup": "Nieuwe groep",
"channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.",
"channels_createPrivateChannel": "Maak een Privé Kanaal",
"channels_joinPrivateChannel": "Sluit een Privé Kanaal aan",
"channels_joinPrivateChannelDesc": "Handmatig een geheime sleutel invoeren.",
"channels_joinPublicChannel": "Sluit het Open Kanaal",
"channels_joinPublicChannelDesc": "Iedereen kan dit kanaal aanmelden.",
"channels_joinHashtagChannel": "Sluit een Hashtag Kanaal",
"channels_joinHashtagChannelDesc": "Iedereen kan lid worden van hashtag-kanalen.",
"channels_scanQrCode": "Scan een QR-code",
"channels_scanQrCodeComingSoon": "Komt later",
"channels_enterHashtag": "Voer hashtag in",
"channels_hashtagHint": "bijv. #team"
}
+13 -1
View File
@@ -1335,5 +1335,17 @@
"listFilter_repeaters": "Powtarzacze",
"listFilter_roomServers": "Serwery pokoju",
"listFilter_unreadOnly": "Tylko nieprzeczytane",
"listFilter_newGroup": "Nowa grupa"
"listFilter_newGroup": "Nowa grupa",
"channels_joinPrivateChannelDesc": "Ręcznie wprowadź klucz tajny.",
"channels_createPrivateChannel": "Utwórz Prywatny Kanał",
"channels_createPrivateChannelDesc": "Zabezpieczone kluczem szyfrowym.",
"channels_joinPrivateChannel": "Dołącz do Prywatnego Kanału",
"channels_joinPublicChannel": "Dołącz do kanału publicznego.",
"channels_joinPublicChannelDesc": "Każdy może dołączyć do tego kanału.",
"channels_joinHashtagChannel": "Dołącz do kanału oznaczanego hashtagiem",
"channels_joinHashtagChannelDesc": "Każdy może dołączyć do kanałów z hashtagami.",
"channels_scanQrCode": "Skanuj kod QR",
"channels_scanQrCodeComingSoon": "Wkrótce",
"channels_enterHashtag": "Wprowadź hashtag",
"channels_hashtagHint": "np. #zespół"
}
+13 -1
View File
@@ -1335,5 +1335,17 @@
"listFilter_repeaters": "Repetidores",
"listFilter_roomServers": "Servidores de sala",
"listFilter_unreadOnly": "Apenas não lido",
"listFilter_newGroup": "Novo grupo"
"listFilter_newGroup": "Novo grupo",
"channels_createPrivateChannelDesc": "Protegido com uma chave secreta.",
"channels_joinPrivateChannelDesc": "Inserir uma chave secreta manualmente.",
"channels_createPrivateChannel": "Criar um Canal Privado",
"channels_joinPrivateChannel": "Junte-se a um Canal Privado",
"channels_joinPublicChannel": "Junte-se ao Canal Público",
"channels_joinPublicChannelDesc": "Qualquer pessoa pode entrar neste canal.",
"channels_joinHashtagChannel": "Junte-se a um Canal com Hashtag",
"channels_joinHashtagChannelDesc": "Qualquer pessoa pode participar de canais com hashtag.",
"channels_scanQrCode": "Digitalizar um Código QR",
"channels_scanQrCodeComingSoon": "Em breve",
"channels_enterHashtag": "Insira hashtag",
"channels_hashtagHint": "ex. #equipe"
}
+13 -1
View File
@@ -1335,5 +1335,17 @@
"listFilter_repeaters": "Opakovadlá",
"listFilter_roomServers": "Servéry miestnosti",
"listFilter_unreadOnly": "Nezaregistrované len",
"listFilter_newGroup": "Nová skupina"
"listFilter_newGroup": "Nová skupina",
"channels_createPrivateChannel": "Vytvorte súkromný kanál",
"channels_joinPrivateChannel": "Pripojiť sa k súkromnému kanálu",
"channels_joinPrivateChannelDesc": "Ručne zadajte tajný kľúč.",
"channels_createPrivateChannelDesc": "Zabezpečené pomocou tajného kľúča.",
"channels_joinPublicChannel": "Pripojte sa k verejnému kanálu",
"channels_joinPublicChannelDesc": "Któvek sátó na tutó kanalizovát.",
"channels_joinHashtagChannel": "Pripojte sa k Hashtag Kanálu",
"channels_joinHashtagChannelDesc": "Ktoekolikoľvek sa môže pridať do hashtag kanálov.",
"channels_scanQrCode": "Skenujte QR kód",
"channels_scanQrCodeComingSoon": "Čoskoro",
"channels_enterHashtag": "Zadajte hashtag",
"channels_hashtagHint": "napr. #tím"
}
+13 -1
View File
@@ -1335,5 +1335,17 @@
"listFilter_repeaters": "Ponovitve",
"listFilter_roomServers": "Smeti za prostore",
"listFilter_unreadOnly": "Nezbrani samo",
"listFilter_newGroup": "Nova skupina"
"listFilter_newGroup": "Nova skupina",
"channels_joinPrivateChannel": "Pridružite se zasebni skupini",
"channels_createPrivateChannelDesc": "Varno zaklenjeno s skrivnim ključem.",
"channels_joinPrivateChannelDesc": "Ročno vnesite zaporni ključ.",
"channels_createPrivateChannel": "Ustvari zasebno kanal.",
"channels_joinPublicChannel": "Pridružite se javnemu kanalu",
"channels_joinPublicChannelDesc": "Kdor karkoli je, lahko se pridruži tej skupini.",
"channels_joinHashtagChannel": "Pridružite se Kanalu z Hashtagom",
"channels_joinHashtagChannelDesc": "Kdor karkoli, lahko se pridruži hashtag kanalom.",
"channels_scanQrCode": "Skeniraj QR kodo",
"channels_scanQrCodeComingSoon": "Prihajajoča",
"channels_enterHashtag": "Vnesite hashtag",
"channels_hashtagHint": "npr. #ekipa"
}
+13 -1
View File
@@ -1335,5 +1335,17 @@
"listFilter_repeaters": "Upprepare",
"listFilter_roomServers": "Rumservrar",
"listFilter_unreadOnly": "Endast oinlästa",
"listFilter_newGroup": "Ny grupp"
"listFilter_newGroup": "Ny grupp",
"channels_createPrivateChannel": "Skapa en privat kanal",
"channels_joinPrivateChannel": "Gå med i en Privat Kanal",
"channels_joinPrivateChannelDesc": "Ange en hemlig nyckel manuellt.",
"channels_createPrivateChannelDesc": "Skyddat med en hemlig nyckel.",
"channels_joinPublicChannel": "Gå med i den Offentliga Kanalen",
"channels_joinPublicChannelDesc": "Vem som helst kan gå med i denna kanal.",
"channels_joinHashtagChannel": "Gå med i en Hashtagkanal",
"channels_joinHashtagChannelDesc": "Väldigt enkelt att gå med i hashtag-kanaler.",
"channels_scanQrCode": "Skanna en QR-kod",
"channels_scanQrCodeComingSoon": "Kommer snart",
"channels_enterHashtag": "Ange hashtag",
"channels_hashtagHint": "t.ex. #team"
}
+13 -1
View File
@@ -1335,5 +1335,17 @@
"listFilter_repeaters": "重复器",
"listFilter_roomServers": "房间服务器",
"listFilter_unreadOnly": "未读消息",
"listFilter_newGroup": "新组"
"listFilter_newGroup": "新组",
"channels_joinPrivateChannel": "加入私密频道",
"channels_createPrivateChannelDesc": "使用密钥保护。",
"channels_joinPrivateChannelDesc": "手动输入密钥。",
"channels_createPrivateChannel": "创建私聊频道",
"channels_joinPublicChannel": "加入公共频道",
"channels_joinPublicChannelDesc": "任何人都可以加入这个频道。",
"channels_joinHashtagChannel": "加入标签频道",
"channels_joinHashtagChannelDesc": "任何人都可以加入话题频道。",
"channels_scanQrCode": "扫描二维码",
"channels_scanQrCodeComingSoon": "即将到来",
"channels_enterHashtag": "输入标签",
"channels_hashtagHint": "例如 #团队"
}
+12
View File
@@ -1,5 +1,8 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
import '../connector/meshcore_protocol.dart';
class Channel {
@@ -61,6 +64,15 @@ class Channel {
return bytes;
}
/// Derive PSK from hashtag name using SHA256.
/// The hashtag is normalized to include '#' prefix.
/// Returns first 16 bytes of SHA256 hash as PSK.
static Uint8List derivePskFromHashtag(String hashtag) {
final name = hashtag.startsWith('#') ? hashtag : '#$hashtag';
final hash = crypto.sha256.convert(utf8.encode(name)).bytes;
return Uint8List.fromList(hash.sublist(0, 16));
}
static String formatPskHex(Uint8List psk) {
return _bytesToHex(psk);
}
+297 -111
View File
@@ -515,132 +515,318 @@ class _ChannelsScreenState extends State<ChannelsScreen>
void _showAddChannelDialog(BuildContext context) {
final connector = context.read<MeshCoreConnector>();
final nextIndex = _findNextAvailableIndex(connector.channels, connector.maxChannels);
final hasPublicChannel = connector.channels.any((c) => c.isPublicChannel);
int? selectedOption;
final nameController = TextEditingController();
final pskController = TextEditingController();
final maxChannels = connector.maxChannels;
int selectedIndex = _findNextAvailableIndex(connector.channels, maxChannels);
bool usePublicPsk = false;
final hashtagController = TextEditingController();
showDialog(
context: context,
builder: (dialogContext) => StatefulBuilder(
builder: (dialogContext, setDialogState) => AlertDialog(
title: Text(dialogContext.l10n.channels_addChannel),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DropdownButtonFormField<int>(
initialValue: selectedIndex,
decoration: InputDecoration(
labelText: dialogContext.l10n.channels_channelIndexLabel,
border: const OutlineInputBorder(),
),
items: List.generate(maxChannels, (i) => i)
.map((i) => DropdownMenuItem(
value: i,
child: Text(dialogContext.l10n.channels_channelIndex(i)),
))
.toList(),
onChanged: (value) {
if (value != null) {
setDialogState(() => selectedIndex = value);
}
},
builder: (dialogContext, setDialogState) {
Widget buildOptionTile({
required int optionIndex,
required IconData icon,
required String title,
required String subtitle,
bool enabled = true,
}) {
final isSelected = selectedOption == optionIndex;
return ListTile(
leading: CircleAvatar(
backgroundColor: enabled
? (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)
: Colors.grey,
),
const SizedBox(height: 16),
TextField(
controller: nameController,
decoration: InputDecoration(
labelText: dialogContext.l10n.channels_channelName,
border: const OutlineInputBorder(),
),
maxLength: 31,
),
const SizedBox(height: 8),
CheckboxListTile(
title: Text(dialogContext.l10n.channels_usePublicChannel),
subtitle: Text(dialogContext.l10n.channels_standardPublicPsk),
value: usePublicPsk,
onChanged: (value) {
setDialogState(() {
usePublicPsk = value ?? false;
if (usePublicPsk) {
nameController.text = 'Public';
pskController.text = Channel.publicChannelPsk;
} else {
),
title: Text(
title,
style: TextStyle(color: enabled ? null : Colors.grey),
),
subtitle: Text(
subtitle,
style: TextStyle(color: enabled ? null : Colors.grey),
),
trailing: enabled ? const Icon(Icons.chevron_right) : null,
selected: isSelected,
onTap: enabled
? () {
setDialogState(() {
selectedOption = optionIndex;
nameController.clear();
pskController.clear();
}
});
},
),
if (!usePublicPsk) ...[
const SizedBox(height: 8),
TextField(
controller: pskController,
decoration: InputDecoration(
labelText: dialogContext.l10n.channels_pskHex,
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: const Icon(Icons.casino),
tooltip: dialogContext.l10n.channels_generateRandomPsk,
onPressed: () {
final random = Random.secure();
final bytes = Uint8List(16);
for (int i = 0; i < 16; i++) {
bytes[i] = random.nextInt(256);
}
pskController.text = Channel.formatPskHex(bytes);
},
hashtagController.clear();
});
}
: null,
);
}
Widget? buildExpandedContent() {
switch (selectedOption) {
case 0: // Create Private Channel
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: TextField(
controller: nameController,
decoration: InputDecoration(
labelText: dialogContext.l10n.channels_channelName,
border: const OutlineInputBorder(),
),
maxLength: 31,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
child: FilledButton(
onPressed: () {
final name = nameController.text.trim();
if (name.isEmpty) {
ScaffoldMessenger.of(dialogContext).showSnackBar(
SnackBar(content: Text(dialogContext.l10n.channels_enterChannelName)),
);
return;
}
final random = Random.secure();
final psk = Uint8List(16);
for (int i = 0; i < 16; i++) {
psk[i] = random.nextInt(256);
}
Navigator.pop(dialogContext);
connector.setChannel(nextIndex, name, psk);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.channels_channelAdded(name))),
);
}
},
child: Text(dialogContext.l10n.common_create),
),
),
],
),
),
],
);
case 1: // Join Private Channel
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: TextField(
controller: nameController,
decoration: InputDecoration(
labelText: dialogContext.l10n.channels_channelName,
border: const OutlineInputBorder(),
),
maxLength: 31,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: TextField(
controller: pskController,
decoration: InputDecoration(
labelText: dialogContext.l10n.channels_pskHex,
border: const OutlineInputBorder(),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
child: FilledButton(
onPressed: () {
final name = nameController.text.trim();
final pskHex = pskController.text.trim();
if (name.isEmpty) {
ScaffoldMessenger.of(dialogContext).showSnackBar(
SnackBar(content: Text(dialogContext.l10n.channels_enterChannelName)),
);
return;
}
Uint8List psk;
try {
psk = Channel.parsePskHex(pskHex);
} on FormatException {
ScaffoldMessenger.of(dialogContext).showSnackBar(
SnackBar(content: Text(dialogContext.l10n.channels_pskMustBe32Hex)),
);
return;
}
Navigator.pop(dialogContext);
connector.setChannel(nextIndex, name, psk);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.channels_channelAdded(name))),
);
}
},
child: Text(dialogContext.l10n.common_add),
),
),
],
),
),
],
);
case 2: // Join Public Channel
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Expanded(
child: FilledButton(
onPressed: () {
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)),
);
}
},
child: Text(dialogContext.l10n.common_add),
),
),
],
),
],
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(dialogContext.l10n.common_cancel),
),
FilledButton(
onPressed: () {
final name = nameController.text.trim();
final pskHex = usePublicPsk
? Channel.publicChannelPsk
: pskController.text.trim();
);
if (name.isEmpty) {
ScaffoldMessenger.of(dialogContext).showSnackBar(
SnackBar(content: Text(dialogContext.l10n.channels_enterChannelName)),
);
return;
}
case 3: // Join Hashtag Channel
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: TextField(
controller: hashtagController,
decoration: InputDecoration(
labelText: dialogContext.l10n.channels_enterHashtag,
hintText: dialogContext.l10n.channels_hashtagHint,
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.tag),
),
maxLength: 31,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
child: FilledButton(
onPressed: () {
var hashtag = hashtagController.text.trim();
if (hashtag.isEmpty) {
ScaffoldMessenger.of(dialogContext).showSnackBar(
SnackBar(content: Text(dialogContext.l10n.channels_enterChannelName)),
);
return;
}
// Normalize hashtag name
final name = hashtag.startsWith('#') ? hashtag : '#$hashtag';
final psk = Channel.derivePskFromHashtag(hashtag);
Navigator.pop(dialogContext);
connector.setChannel(nextIndex, name, psk);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.channels_channelAdded(name))),
);
}
},
child: Text(dialogContext.l10n.common_add),
),
),
],
),
),
],
);
Uint8List psk;
try {
psk = Channel.parsePskHex(pskHex);
} on FormatException {
ScaffoldMessenger.of(dialogContext).showSnackBar(
SnackBar(content: Text(dialogContext.l10n.channels_pskMustBe32Hex)),
);
return;
}
default:
return null;
}
}
Navigator.pop(dialogContext);
connector.setChannel(selectedIndex, name, psk);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.channels_channelAdded(name))),
);
}
},
child: Text(dialogContext.l10n.common_add),
return AlertDialog(
title: Text(dialogContext.l10n.channels_addChannel),
contentPadding: const EdgeInsets.symmetric(vertical: 16),
content: SizedBox(
width: double.maxFinite,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
buildOptionTile(
optionIndex: 0,
icon: Icons.add,
title: dialogContext.l10n.channels_createPrivateChannel,
subtitle: dialogContext.l10n.channels_createPrivateChannelDesc,
),
if (selectedOption == 0) buildExpandedContent()!,
const Divider(height: 1),
buildOptionTile(
optionIndex: 1,
icon: Icons.lock,
title: dialogContext.l10n.channels_joinPrivateChannel,
subtitle: dialogContext.l10n.channels_joinPrivateChannelDesc,
),
if (selectedOption == 1) buildExpandedContent()!,
if (!hasPublicChannel) ...[
const Divider(height: 1),
buildOptionTile(
optionIndex: 2,
icon: Icons.public,
title: dialogContext.l10n.channels_joinPublicChannel,
subtitle: dialogContext.l10n.channels_joinPublicChannelDesc,
),
if (selectedOption == 2) buildExpandedContent()!,
],
const Divider(height: 1),
buildOptionTile(
optionIndex: 3,
icon: Icons.tag,
title: dialogContext.l10n.channels_joinHashtagChannel,
subtitle: dialogContext.l10n.channels_joinHashtagChannelDesc,
),
if (selectedOption == 3) buildExpandedContent()!,
const Divider(height: 1),
buildOptionTile(
optionIndex: 4,
icon: Icons.qr_code,
title: dialogContext.l10n.channels_scanQrCode,
subtitle: dialogContext.l10n.channels_scanQrCodeComingSoon,
enabled: false,
),
],
),
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(dialogContext.l10n.common_close),
),
],
);
},
),
);
}