Compare commits

..

3 Commits

Author SHA1 Message Date
Winston Lowe d104edd65c added Groq and openAI APIs 2026-05-10 16:04:39 -07:00
Winston Lowe 094d5d2706 feat: Enhance companion connection features and UI updates
- Added functionality to load and restore the last companion's scope on app startup.
- Implemented caching mechanisms for contacts, channels, and messages related to the current companion.
- Updated UI to reflect connection status, including disabling message input when disconnected.
- Introduced new dialog prompts to inform users when they need to connect to a companion for accessing features.
- Refactored navigation logic to improve user experience when disconnected, directing users to the scanner screen.
- Added localization strings for new companion connection prompts in multiple languages.
2026-05-10 16:02:58 -07:00
Winston Lowe 396f70c994 Add byteCount field to PathRecord class with default value 2026-05-10 15:57:33 -07:00
56 changed files with 1161 additions and 1134 deletions
-1
View File
@@ -193,7 +193,6 @@ Devices are discovered by scanning for BLE advertisements with known MeshCore de
- `WisCore-`
- `HT-`
- `LowMesh_MC_`
- `NRF52`
New device prefixes can be added in `lib/connector/meshcore_uuids.dart`.
+15 -71
View File
@@ -305,8 +305,6 @@ class MeshCoreConnector extends ChangeNotifier {
final Map<String, int> _contactUnreadCount = {};
final Map<String, RepeaterBatterySnapshot> _repeaterBatterySnapshots = {};
bool _unreadStateLoaded = false;
int _cachedContactsUnreadTotal = 0;
int _cachedChannelsUnreadTotal = 0;
final Map<String, _RepeaterAckContext> _pendingRepeaterAcks = {};
String? _activeContactKey;
int? _activeChannelIndex;
@@ -614,42 +612,16 @@ class MeshCoreConnector extends ChangeNotifier {
int getTotalUnreadCount() {
if (!_unreadStateLoaded) return 0;
return getTotalContactsUnreadCount() + getTotalChannelsUnreadCount();
}
int getTotalContactsUnreadCount() {
if (!_unreadStateLoaded) return 0;
return _cachedContactsUnreadTotal;
}
int getTotalChannelsUnreadCount() {
if (!_unreadStateLoaded) return 0;
return _cachedChannelsUnreadTotal;
}
/// Recalculates both cached unread totals from scratch.
/// Called when unread state is first loaded.
void _recalculateCachedUnreadTotals() {
_recalculateCachedContactsUnreadTotal();
_recalculateCachedChannelsUnreadTotal();
}
void _recalculateCachedContactsUnreadTotal() {
int total = 0;
_contactUnreadCount.forEach((contactKeyHex, count) {
if (_shouldTrackUnreadForContactKey(contactKeyHex)) {
total += count;
}
});
_cachedContactsUnreadTotal = total;
}
void _recalculateCachedChannelsUnreadTotal() {
final allChannels = _channels.isNotEmpty ? _channels : _cachedChannels;
_cachedChannelsUnreadTotal = allChannels.fold(
0,
(total, ch) => total + ch.unreadCount,
);
var total = 0;
// Count unread contact messages
for (final contact in _contacts) {
total += getUnreadCountForContact(contact);
}
// Count unread channel messages
for (final channelIndex in _channelMessages.keys) {
total += getUnreadCountForChannelIndex(channelIndex);
}
return total;
}
bool isChannelSmazEnabled(int channelIndex) {
@@ -683,7 +655,6 @@ class MeshCoreConnector extends ChangeNotifier {
..clear()
..addAll(await _unreadStore.loadContactUnreadCount());
_unreadStateLoaded = true;
_recalculateCachedUnreadTotals();
notifyListeners();
}
@@ -722,8 +693,6 @@ class MeshCoreConnector extends ChangeNotifier {
final previousCount = _contactUnreadCount[contactKeyHex] ?? 0;
if (previousCount > 0) {
_contactUnreadCount[contactKeyHex] = 0;
_cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - previousCount)
.clamp(0, _cachedContactsUnreadTotal);
_appDebugLogService?.info(
'Contact $contactKeyHex marked as read (was $previousCount unread)',
tag: 'Unread',
@@ -765,8 +734,6 @@ class MeshCoreConnector extends ChangeNotifier {
if (channel != null && channel.unreadCount > 0) {
final previousCount = channel.unreadCount;
channel.unreadCount = 0;
_cachedChannelsUnreadTotal = (_cachedChannelsUnreadTotal - previousCount)
.clamp(0, _cachedChannelsUnreadTotal);
_appDebugLogService?.info(
'Channel ${channel.name.isNotEmpty ? channel.name : channelIndex} marked as read (was $previousCount unread)',
tag: 'Unread',
@@ -973,18 +940,11 @@ class MeshCoreConnector extends ChangeNotifier {
final lastCompanionPublicKeyHex = prefs.getString(
_lastCompanionPublicKeyPref,
);
try {
if (lastCompanionPublicKeyHex == null ||
lastCompanionPublicKeyHex.trim().isEmpty) {
return;
}
_selfPublicKey = hexToPubKey(lastCompanionPublicKeyHex);
_setScopedStorePublicKey(lastCompanionPublicKeyHex);
} catch (e) {
_appDebugLogService?.error(
'Failed to restore last companion scope with public key hex: $lastCompanionPublicKeyHex, error: $e',
);
if (lastCompanionPublicKeyHex == null ||
lastCompanionPublicKeyHex.trim().isEmpty) {
return;
}
_setScopedStorePublicKey(lastCompanionPublicKeyHex);
}
Future<void> loadDiscoveredContactCache() => _loadDiscoveredContactCache();
@@ -1006,9 +966,7 @@ class MeshCoreConnector extends ChangeNotifier {
_conversations.clear();
_loadedConversationKeys.clear();
_channelMessages.clear();
_channels.clear();
_cachedChannels.clear();
_previousChannelsCache.clear();
_knownContactKeys.clear();
_contactUnreadCount.clear();
_unreadStateLoaded = false;
@@ -1029,7 +987,6 @@ class MeshCoreConnector extends ChangeNotifier {
await restoreLastCompanionScope();
await loadContactCache();
await _loadDiscoveredContactCache();
notifyListeners();
}
Future<void> _loadDiscoveredContactCache() async {
@@ -3246,9 +3203,6 @@ class MeshCoreConnector extends ChangeNotifier {
unawaited(_persistContacts());
_conversations.remove(contact.publicKeyHex);
_loadedConversationKeys.remove(contact.publicKeyHex);
final removedCount = _contactUnreadCount[contact.publicKeyHex] ?? 0;
_cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - removedCount)
.clamp(0, _cachedContactsUnreadTotal);
_contactUnreadCount.remove(contact.publicKeyHex);
_unreadStore.saveContactUnreadCount(
Map<String, int>.from(_contactUnreadCount),
@@ -3642,7 +3596,6 @@ class MeshCoreConnector extends ChangeNotifier {
// Cache channels for offline use
_cachedChannels = List<Channel>.from(_channels);
unawaited(_channelStore.saveChannels(_channels));
_recalculateCachedChannelsUnreadTotal();
// Apply ordering and notify UI
_applyChannelOrder();
@@ -4181,9 +4134,6 @@ class MeshCoreConnector extends ChangeNotifier {
_handleDiscovery(contact, frame, noNotify: true, addActive: true);
if (contact.type == advTypeRepeater) {
final removedCount = _contactUnreadCount[contact.publicKeyHex] ?? 0;
_cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - removedCount)
.clamp(0, _cachedContactsUnreadTotal);
_contactUnreadCount.remove(contact.publicKeyHex);
_unreadStore.saveContactUnreadCount(
Map<String, int>.from(_contactUnreadCount),
@@ -4274,9 +4224,6 @@ class MeshCoreConnector extends ChangeNotifier {
}
if (contact.type == advTypeRepeater) {
final removedCount = _contactUnreadCount[contact.publicKeyHex] ?? 0;
_cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - removedCount)
.clamp(0, _cachedContactsUnreadTotal);
_contactUnreadCount.remove(contact.publicKeyHex);
_unreadStore.saveContactUnreadCount(
Map<String, int>.from(_contactUnreadCount),
@@ -5157,8 +5104,7 @@ class MeshCoreConnector extends ChangeNotifier {
_channelSyncRetries = 0; // Reset retry counter on success
// Only add non-empty channels
if (!channel.isEmpty &&
!_channels.any((c) => c.pskHex == channel.pskHex)) {
if (!channel.isEmpty) {
_channels.add(channel);
}
@@ -5290,7 +5236,6 @@ class MeshCoreConnector extends ChangeNotifier {
final channel = _findChannelByIndex(channelIndex);
if (channel != null) {
channel.unreadCount++;
_cachedChannelsUnreadTotal++;
_appDebugLogService?.info(
'Channel ${channel.name.isNotEmpty ? channel.name : channelIndex} unread count incremented to ${channel.unreadCount}',
tag: 'Unread',
@@ -5335,7 +5280,6 @@ class MeshCoreConnector extends ChangeNotifier {
final currentCount = _contactUnreadCount[contactKey] ?? 0;
_contactUnreadCount[contactKey] = currentCount + 1;
_cachedContactsUnreadTotal++;
_appDebugLogService?.info(
'Contact $contactKey unread count incremented to ${currentCount + 1}',
tag: 'Unread',
-1
View File
@@ -11,6 +11,5 @@ class MeshCoreUuids {
"Lilygo",
"HT-",
"LowMesh_MC_",
"NRF52",
];
}
+1 -4
View File
@@ -2312,8 +2312,5 @@
"chat_newMessages": "Нови съобщения",
"settings_companionDebugLog": "Лог за отстраняване на грешки (за съпътстваща програма)",
"repeater_chanUtil": "Използване на канала",
"dialog_connectCompanion": "Свържете се с придружител, за да получите достъп до функциите на ретранслатора и сървъра за стаи.",
"dialog_disconnectedTitle": "Прекъснато",
"dialog_disconnectedMessage": "Свързването ви с вашия спътник е прекъснато.",
"contact_connectCompanion": "Свържете се с спътник, за да получите достъп до функциите на repeater и room server."
"contact_connectCompanion": "Свържете се с придружител, за да получите достъп до функциите на ретранслатора и сървъра за стаи."
}
+1 -4
View File
@@ -2340,8 +2340,5 @@
"settings_companionDebugLog": "Debug-Protokoll für die Begleitsoftware",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB-Befehle, Antworten und Rohdaten",
"repeater_chanUtil": "Nutzung des Kanals",
"dialog_connectCompanion": "Verbinden Sie sich mit einem Companion, um auf die Funktionen des Repeaters und des Raumservers zuzugreifen.",
"dialog_disconnectedTitle": "Getrennt",
"dialog_disconnectedMessage": "Du wurdest von deinem Begleiter getrennt.",
"contact_connectCompanion": "Mit einem Companion verbinden, um auf Repeater- und Raumserver-Funktionen zuzugreifen."
"contact_connectCompanion": "Verbinden Sie sich mit einem Companion, um auf die Funktionen des Repeaters und des Raumservers zuzugreifen."
}
+2 -4
View File
@@ -1062,9 +1062,6 @@
"time_allTime": "All Time",
"dialog_disconnect": "Disconnect",
"dialog_disconnectConfirm": "Are you sure you want to disconnect from this device?",
"dialog_disconnectedTitle": "Disconnected",
"dialog_disconnectedMessage": "You have been disconnected from your companion.",
"dialog_connectCompanion": "Connect to a companion to access repeater and room server features.",
"login_repeaterLogin": "Repeater Login",
"login_roomLogin": "Room Server Login",
"login_password": "Password",
@@ -2369,5 +2366,6 @@
"contact_typeRepeater": "Repeater",
"contact_typeRoom": "Room",
"contact_typeSensor": "Sensor",
"contact_typeUnknown": "Unknown"
"contact_typeUnknown": "Unknown",
"contact_connectCompanion": "Connect to a companion to access repeater and room server features."
}
+1 -4
View File
@@ -2340,8 +2340,5 @@
"settings_companionDebugLogSubtitle": "Comandos, respuestas y datos brutos para protocolos BLE/TCP/USB",
"chat_markAsUnread": "Marcar como no leído",
"repeater_chanUtil": "Utilización del canal",
"dialog_connectCompanion": "Conéctate a un compañero para acceder a las funciones de repetidor y servidor de sala.",
"dialog_disconnectedTitle": "Desconectado",
"dialog_disconnectedMessage": "Te has desconectado de tu compañero.",
"contact_connectCompanion": "Conéctate a un compañero para acceder a las funciones del repetidor y del servidor de la sala."
"contact_connectCompanion": "Conéctate a un compañero para acceder a las funciones de repetidor y servidor de sala."
}
+1 -4
View File
@@ -2319,8 +2319,5 @@
"chat_newMessages": "Nouveaux messages",
"settings_companionDebugLogSubtitle": "Commandes, réponses et données brutes pour les protocoles BLE/TCP/USB",
"repeater_chanUtil": "Utilisation du canal",
"dialog_connectCompanion": "Connectez-vous à un compagnon pour accéder aux fonctionnalités de répéteur et de serveur de salle.",
"dialog_disconnectedTitle": "Déconnecté",
"dialog_disconnectedMessage": "Vous avez été déconnecté de votre compagnon.",
"contact_connectCompanion": "Connectez-vous à un compagnon pour accéder aux fonctionnalités du répéteur et du serveur de salle."
"contact_connectCompanion": "Connectez-vous à un compagnon pour accéder aux fonctionnalités de répéteur et de serveur de salle."
}
+1 -4
View File
@@ -2350,8 +2350,5 @@
"settings_companionDebugLog": "Párhuzamos hibakeresési napló",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB parancsok, válaszok és alapvető adatok",
"repeater_chanUtil": "Csatorna-használat",
"dialog_connectCompanion": "Csatlakozz egy kísérőhöz az ismétlő- és szobaszerver-funkciók eléréséhez.",
"dialog_disconnectedTitle": "Kapcsolat megszakadt",
"dialog_disconnectedMessage": "A kapcsolat megszakadt a kísérővel.",
"contact_connectCompanion": "Csatlakozz egy kísérőhöz az ismétlő- és szobaszerver-funkciók eléréséhez."
"contact_connectCompanion": "Csatlakozzon egy kísérőhöz a ismétlő és szobaszerver funkciók eléréséhez."
}
+1 -4
View File
@@ -2312,8 +2312,5 @@
"chat_newMessages": "Nuovi messaggi",
"chat_markAsUnread": "Segna come non letto",
"repeater_chanUtil": "Utilizzo del canale",
"dialog_connectCompanion": "Connettiti a un dispositivo companion per accedere alle funzionalità di ripetitore e server stanza.",
"dialog_disconnectedTitle": "Disconnesso",
"dialog_disconnectedMessage": "Sei stato disconnesso dal tuo compagno.",
"contact_connectCompanion": "Connettiti a un companion per accedere alle funzioni del repeater e del server di stanza."
"contact_connectCompanion": "Connettiti a un dispositivo companion per accedere alle funzionalità di ripetitore e server stanza."
}
+1 -4
View File
@@ -2350,8 +2350,5 @@
"chat_newMessages": "新しいメッセージ",
"chat_markAsUnread": "未読としてマークする",
"repeater_chanUtil": "チャンネルの利用状況",
"dialog_connectCompanion": "コネクトしてリピーターとルームサーバー機能にアクセス",
"dialog_disconnectedTitle": "切断済み",
"dialog_disconnectedMessage": "コンパニオンとの接続が切れました。",
"contact_connectCompanion": "リピーターおよびルームサーバー機能にアクセスするには、コンパニオンに接続してください。"
"contact_connectCompanion": "コネクトしてリピーターとルームサーバー機能にアクセス"
}
-3
View File
@@ -2350,8 +2350,5 @@
"settings_companionDebugLogSubtitle": "BLE/TCP/USB 명령어, 응답 및 원시 데이터",
"chat_markAsUnread": "미리 읽지 않음으로 표시",
"repeater_chanUtil": "채널 활용도",
"dialog_connectCompanion": "리피터 및 룸 서버 기능에 액세스하려면 컴패니언에 연결하세요.",
"dialog_disconnectedTitle": "연결 끊김",
"dialog_disconnectedMessage": "컴패니언과의 연결이 끊어졌습니다.",
"contact_connectCompanion": "리피터 및 룸 서버 기능에 액세스하려면 컴패니언에 연결하세요."
}
+6 -18
View File
@@ -3561,24 +3561,6 @@ abstract class AppLocalizations {
/// **'Are you sure you want to disconnect from this device?'**
String get dialog_disconnectConfirm;
/// No description provided for @dialog_disconnectedTitle.
///
/// In en, this message translates to:
/// **'Disconnected'**
String get dialog_disconnectedTitle;
/// No description provided for @dialog_disconnectedMessage.
///
/// In en, this message translates to:
/// **'You have been disconnected from your companion.'**
String get dialog_disconnectedMessage;
/// No description provided for @dialog_connectCompanion.
///
/// In en, this message translates to:
/// **'Connect to a companion to access repeater and room server features.'**
String get dialog_connectCompanion;
/// No description provided for @login_repeaterLogin.
///
/// In en, this message translates to:
@@ -7353,6 +7335,12 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Unknown'**
String get contact_typeUnknown;
/// No description provided for @contact_connectCompanion.
///
/// In en, this message translates to:
/// **'Connect to a companion to access repeater and room server features.'**
String get contact_connectCompanion;
}
class _AppLocalizationsDelegate
+4 -11
View File
@@ -1979,17 +1979,6 @@ class AppLocalizationsBg extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Сигурни ли сте, че искате да се откъснете от това устройство?';
@override
String get dialog_disconnectedTitle => 'Прекъснато';
@override
String get dialog_disconnectedMessage =>
'Свързването ви с вашия спътник е прекъснато.';
@override
String get dialog_connectCompanion =>
'Свържете се с придружител, за да получите достъп до функциите на ретранслатора и сървъра за стаи.';
@override
String get login_repeaterLogin => 'Повторител Вход';
@@ -4299,4 +4288,8 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion =>
'Свържете се с придружител, за да получите достъп до функциите на ретранслатора и сървъра за стаи.';
}
+4 -11
View File
@@ -1977,17 +1977,6 @@ class AppLocalizationsDe extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Sind Sie sicher, dass Sie sich von diesem Gerät trennen möchten?';
@override
String get dialog_disconnectedTitle => 'Getrennt';
@override
String get dialog_disconnectedMessage =>
'Du wurdest von deinem Begleiter getrennt.';
@override
String get dialog_connectCompanion =>
'Verbinden Sie sich mit einem Companion, um auf die Funktionen des Repeaters und des Raumservers zuzugreifen.';
@override
String get login_repeaterLogin => 'Beim Repeater anmelden';
@@ -4316,4 +4305,8 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion =>
'Verbinden Sie sich mit einem Companion, um auf die Funktionen des Repeaters und des Raumservers zuzugreifen.';
}
+4 -11
View File
@@ -1938,17 +1938,6 @@ class AppLocalizationsEn extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Are you sure you want to disconnect from this device?';
@override
String get dialog_disconnectedTitle => 'Disconnected';
@override
String get dialog_disconnectedMessage =>
'You have been disconnected from your companion.';
@override
String get dialog_connectCompanion =>
'Connect to a companion to access repeater and room server features.';
@override
String get login_repeaterLogin => 'Repeater Login';
@@ -4221,4 +4210,8 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion =>
'Connect to a companion to access repeater and room server features.';
}
+4 -11
View File
@@ -1974,17 +1974,6 @@ class AppLocalizationsEs extends AppLocalizations {
String get dialog_disconnectConfirm =>
'¿Está seguro de que desea desconectarse de este dispositivo?';
@override
String get dialog_disconnectedTitle => 'Desconectado';
@override
String get dialog_disconnectedMessage =>
'Te has desconectado de tu compañero.';
@override
String get dialog_connectCompanion =>
'Conéctate a un compañero para acceder a las funciones de repetidor y servidor de sala.';
@override
String get login_repeaterLogin => 'Iniciar sesión en el Repetidor';
@@ -4303,4 +4292,8 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion =>
'Conéctate a un compañero para acceder a las funciones de repetidor y servidor de sala.';
}
+4 -11
View File
@@ -1985,17 +1985,6 @@ class AppLocalizationsFr extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?';
@override
String get dialog_disconnectedTitle => 'Déconnecté';
@override
String get dialog_disconnectedMessage =>
'Vous avez été déconnecté de votre compagnon.';
@override
String get dialog_connectCompanion =>
'Connectez-vous à un compagnon pour accéder aux fonctionnalités de répéteur et de serveur de salle.';
@override
String get login_repeaterLogin => 'Connexion au répéteur';
@@ -4332,4 +4321,8 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion =>
'Connectez-vous à un compagnon pour accéder aux fonctionnalités de répéteur et de serveur de salle.';
}
+4 -10
View File
@@ -1986,16 +1986,6 @@ class AppLocalizationsHu extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Biztosan szeretné kiírni ezt a készüléket?';
@override
String get dialog_disconnectedTitle => 'Lejárat';
@override
String get dialog_disconnectedMessage => 'Lehentetőtől megszakadtál.';
@override
String get dialog_connectCompanion =>
'Csatlakozzon egy kísérőhöz a ismétlő és szobaszerver funkciók eléréséhez.';
@override
String get login_repeaterLogin => 'Ismételt bejelentkezés';
@@ -4319,4 +4309,8 @@ class AppLocalizationsHu extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion =>
'Csatlakozzon egy kísérőhöz a ismétlő és szobaszerver funkciók eléréséhez.';
}
+4 -11
View File
@@ -1976,17 +1976,6 @@ class AppLocalizationsIt extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Sei sicuro di voler disconnetterti da questo dispositivo?';
@override
String get dialog_disconnectedTitle => 'Disconnesso';
@override
String get dialog_disconnectedMessage =>
'Sei stato disconnesso dal tuo compagno.';
@override
String get dialog_connectCompanion =>
'Connettiti a un dispositivo companion per accedere alle funzionalità di ripetitore e server stanza.';
@override
String get login_repeaterLogin => 'Login Ripetitore';
@@ -4308,4 +4297,8 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion =>
'Connettiti a un dispositivo companion per accedere alle funzionalità di ripetitore e server stanza.';
}
+3 -9
View File
@@ -1894,15 +1894,6 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get dialog_disconnectConfirm => '本当にこのデバイスとの接続を解除したいですか?';
@override
String get dialog_disconnectedTitle => '切断済み';
@override
String get dialog_disconnectedMessage => 'コンパニオンとの接続が切れました。';
@override
String get dialog_connectCompanion => 'コネクトしてリピーターとルームサーバー機能にアクセス';
@override
String get login_repeaterLogin => '再ログイン';
@@ -4072,4 +4063,7 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion => 'コネクトしてリピーターとルームサーバー機能にアクセス';
}
+3 -9
View File
@@ -1890,15 +1890,6 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get dialog_disconnectConfirm => '이 장치와의 연결을 해제하시겠습니까?';
@override
String get dialog_disconnectedTitle => '연결 끊김';
@override
String get dialog_disconnectedMessage => '컴패니언과의 연결이 끊어졌습니다.';
@override
String get dialog_connectCompanion => '리피터 및 룸 서버 기능에 액세스하려면 컴패니언에 연결하세요.';
@override
String get login_repeaterLogin => '다시 로그인';
@@ -4073,4 +4064,7 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion => '리피터 및 룸 서버 기능에 액세스하려면 컴패니언에 연결하세요.';
}
+4 -11
View File
@@ -1963,17 +1963,6 @@ class AppLocalizationsNl extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Ben je er zeker van dat je verbinding met dit apparaat wilt verbreken?';
@override
String get dialog_disconnectedTitle => 'Verbroken';
@override
String get dialog_disconnectedMessage =>
'Je bent losgekoppeld van je companion.';
@override
String get dialog_connectCompanion =>
'Maak verbinding met een companion om repeater- en kamerserverfuncties te gebruiken.';
@override
String get login_repeaterLogin => 'Inloggen Repeater';
@@ -4284,4 +4273,8 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion =>
'Maak verbinding met een companion om repeater- en kamerserverfuncties te gebruiken.';
}
+4 -11
View File
@@ -1990,17 +1990,6 @@ class AppLocalizationsPl extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Czy na pewno chcesz się odłączyć od tego urządzenia?';
@override
String get dialog_disconnectedTitle => 'Rozłączono';
@override
String get dialog_disconnectedMessage =>
'Zostałeś rozłączony ze swoim towarzyszem.';
@override
String get dialog_connectCompanion =>
'Połącz się z towarzyszem, aby uzyskać dostęp do funkcji powtarzacza i serwera pokoi.';
@override
String get login_repeaterLogin => 'Logowanie do przekaźnika';
@@ -4320,4 +4309,8 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion =>
'Połącz się z towarzyszem, aby uzyskać dostęp do funkcji powtarzacza i serwera pokoi.';
}
+4 -11
View File
@@ -1973,17 +1973,6 @@ class AppLocalizationsPt extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Tem certeza de que deseja desconectar deste dispositivo?';
@override
String get dialog_disconnectedTitle => 'Desconectado';
@override
String get dialog_disconnectedMessage =>
'Você foi desconectado do seu companheiro.';
@override
String get dialog_connectCompanion =>
'Conecte-se a um dispositivo companion para acessar as funcionalidades de repetidor e servidor de salas.';
@override
String get login_repeaterLogin => 'Login ao Repetidor';
@@ -4296,4 +4285,8 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion =>
'Conecte-se a um dispositivo companion para acessar as funcionalidades de repetidor e servidor de salas.';
}
+4 -11
View File
@@ -1977,17 +1977,6 @@ class AppLocalizationsRu extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Вы уверены, что хотите отключиться от этого устройства?';
@override
String get dialog_disconnectedTitle => 'Отключено';
@override
String get dialog_disconnectedMessage =>
'Вы были отключены от вашего компаньона.';
@override
String get dialog_connectCompanion =>
'Подключитесь к компаньону, чтобы получить доступ к функциям ретранслятора и сервера комнат.';
@override
String get login_repeaterLogin => 'Вход в репитер';
@@ -4314,4 +4303,8 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get contact_typeUnknown => 'Неизвестно';
@override
String get contact_connectCompanion =>
'Подключитесь к компаньону, чтобы получить доступ к функциям ретранслятора и сервера комнат.';
}
+4 -11
View File
@@ -1963,17 +1963,6 @@ class AppLocalizationsSk extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Ste si istý/á, že chcete odpojiť od tohto zariadenia?';
@override
String get dialog_disconnectedTitle => 'Odpojené';
@override
String get dialog_disconnectedMessage =>
'Od vášho spoločníka ste boli odpojený.';
@override
String get dialog_connectCompanion =>
'Pripojte sa k sprievodcovi a získajte prístup k funkciám opakovača a serveru miestností.';
@override
String get login_repeaterLogin => 'Opätovné prihlásenie';
@@ -4280,4 +4269,8 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion =>
'Pripojte sa k sprievodcovi a získajte prístup k funkciám opakovača a serveru miestností.';
}
+4 -11
View File
@@ -1961,17 +1961,6 @@ class AppLocalizationsSl extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Ste prepričani, da želite se odklopiti s tega naprave?';
@override
String get dialog_disconnectedTitle => 'Prekinjeno';
@override
String get dialog_disconnectedMessage =>
'Prekinjena povezava s vašim spre伴ovalcem.';
@override
String get dialog_connectCompanion =>
'Povežite se s spremljevalnikom za dostop do funkcij ponavljalnika in strežnika sob.';
@override
String get login_repeaterLogin => 'Ponovni vnos';
@@ -4278,4 +4267,8 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion =>
'Povežite se s spremljevalnikom za dostop do funkcij ponavljalnika in strežnika sob.';
}
+4 -11
View File
@@ -1950,17 +1950,6 @@ class AppLocalizationsSv extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Är du säker på att du vill koppla från enheten?';
@override
String get dialog_disconnectedTitle => 'Ansluten ej';
@override
String get dialog_disconnectedMessage =>
'Du har kopplats från din companion.';
@override
String get dialog_connectCompanion =>
'Anslut till en sällskapstjänst för att komma åt upprepning och rumsserverfunktioner.';
@override
String get login_repeaterLogin => 'Återuppta Inloggning';
@@ -4252,4 +4241,8 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion =>
'Anslut till en sällskapstjänst för att komma åt upprepning och rumsserverfunktioner.';
}
+4 -11
View File
@@ -1972,17 +1972,6 @@ class AppLocalizationsUk extends AppLocalizations {
String get dialog_disconnectConfirm =>
'Ви впевнені, що хочете відключитись від цього пристрою?';
@override
String get dialog_disconnectedTitle => 'Від’єднано';
@override
String get dialog_disconnectedMessage =>
'Вас від’єднано від вашого супутника.';
@override
String get dialog_connectCompanion =>
'Підключіться до супутнього пристрою, щоб отримати доступ до функцій ретранслятора та сервера кімнат.';
@override
String get login_repeaterLogin => 'Вхід у ретранслятор';
@@ -4315,4 +4304,8 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get contact_typeUnknown => 'Невідомо';
@override
String get contact_connectCompanion =>
'Підключіться до супутнього пристрою, щоб отримати доступ до функцій ретранслятора та сервера кімнат.';
}
+3 -9
View File
@@ -1861,15 +1861,6 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get dialog_disconnectConfirm => '确定要断开与此设备的连接吗?';
@override
String get dialog_disconnectedTitle => '已断开连接';
@override
String get dialog_disconnectedMessage => '你已与你的伙伴断开连接。';
@override
String get dialog_connectCompanion => '连接伴机以访问中继器和房间服务器功能。';
@override
String get login_repeaterLogin => '转发节点登录';
@@ -3947,4 +3938,7 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion => '连接伴机以访问中继器和房间服务器功能。';
}
+1 -4
View File
@@ -2312,8 +2312,5 @@
"chat_markAsUnread": "Markeer als ongelezen",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB commando's, antwoorden en ruwe data",
"repeater_chanUtil": "Gebruik van het kanaal",
"dialog_connectCompanion": "Maak verbinding met een companion om repeater- en kamerserverfuncties te gebruiken.",
"dialog_disconnectedTitle": "Verbroken",
"dialog_disconnectedMessage": "Je bent losgekoppeld van je companion.",
"contact_connectCompanion": "Maak verbinding met een companion om toegang te krijgen tot repeater- en roomserverfuncties."
"contact_connectCompanion": "Maak verbinding met een companion om repeater- en kamerserverfuncties te gebruiken."
}
+1 -4
View File
@@ -2350,8 +2350,5 @@
"chat_markAsUnread": "Oznacz jako nieprzeczytane",
"settings_companionDebugLog": "Log debugowania (dla pomocy w rozwiązywaniu problemów)",
"repeater_chanUtil": "Wykorzystanie kanału",
"dialog_connectCompanion": "Połącz się z towarzyszem, aby uzyskać dostęp do funkcji powtarzacza i serwera pokoi.",
"dialog_disconnectedTitle": "Rozłączono",
"dialog_disconnectedMessage": "Zostałeś rozłączony ze swoim towarzyszem.",
"contact_connectCompanion": "Połącz się z towarzyszem, aby uzyskać dostęp do funkcji repeatera i serwera pokojowego."
"contact_connectCompanion": "Połącz się z towarzyszem, aby uzyskać dostęp do funkcji powtarzacza i serwera pokoi."
}
+1 -4
View File
@@ -2312,8 +2312,5 @@
"chat_markAsUnread": "Marcar como não lido",
"chat_newMessages": "Novas mensagens",
"repeater_chanUtil": "Utilização do canal",
"dialog_connectCompanion": "Conecte-se a um dispositivo companion para acessar as funcionalidades de repetidor e servidor de salas.",
"dialog_disconnectedTitle": "Desconectado",
"dialog_disconnectedMessage": "Você foi desconectado do seu companheiro.",
"contact_connectCompanion": "Conecte-se a um companheiro para acessar recursos de repetidor e servidor de sala."
"contact_connectCompanion": "Conecte-se a um dispositivo companion para acessar as funcionalidades de repetidor e servidor de salas."
}
+1 -4
View File
@@ -1615,8 +1615,5 @@
"settings_companionDebugLogSubtitle": "Команды, ответы и необработанные данные, используемые для протоколов BLE, TCP и USB.",
"repeater_chanUtil": "Использование канала",
"settings_companionDebugLog": "Журнал отладки (для сопутствующего приложения)",
"dialog_connectCompanion": "Подключитесь к компаньону, чтобы получить доступ к функциям ретранслятора и сервера комнат.",
"dialog_disconnectedTitle": "Отключено",
"dialog_disconnectedMessage": "Вы были отключены от вашего компаньона.",
"contact_connectCompanion": "Подключитесь к компаньону, чтобы получить доступ к функциям репитера и серверу комнаты."
"contact_connectCompanion": "Подключитесь к компаньону, чтобы получить доступ к функциям ретранслятора и сервера комнат."
}
+1 -4
View File
@@ -2312,8 +2312,5 @@
"settings_companionDebugLog": "Logovanie pre ladenie (sprievodný log)",
"chat_newMessages": "Nové správy",
"repeater_chanUtil": "Využitie kanálu",
"dialog_connectCompanion": "Pripojte sa k sprievodcovi a získajte prístup k funkciám opakovača a serveru miestností.",
"dialog_disconnectedTitle": "Odpojené",
"dialog_disconnectedMessage": "Od vášho spoločníka ste boli odpojený.",
"contact_connectCompanion": "Pripojte sa k spoločníkovi pre prístup k funkciám opakovača a miestneho servera."
"contact_connectCompanion": "Pripojte sa k sprievodcovi a získajte prístup k funkciám opakovača a serveru miestností."
}
+1 -4
View File
@@ -2312,8 +2312,5 @@
"chat_newMessages": "Nove novice",
"settings_companionDebugLogSubtitle": "Navodila, odgovori in surova podatka za BLE/TCP/USB.",
"repeater_chanUtil": "Uporaba kanala",
"dialog_connectCompanion": "Povežite se s spremljevalnikom za dostop do funkcij ponavljalnika in strežnika sob.",
"dialog_disconnectedTitle": "Prekinjeno",
"dialog_disconnectedMessage": "Prekinjena povezava s vašim spre伴ovalcem.",
"contact_connectCompanion": "Povežite se s ponсоbnikom za dostop do funkcij pon 반복nika in strežnika prostorov."
"contact_connectCompanion": "Povežite se s spremljevalnikom za dostop do funkcij ponavljalnika in strežnika sob."
}
+1 -4
View File
@@ -2312,8 +2312,5 @@
"chat_newMessages": "Nya meddelanden",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB-kommandon, svar och rådata",
"repeater_chanUtil": "Användning av kanal",
"dialog_connectCompanion": "Anslut till en sällskapstjänst för att komma åt upprepning och rumsserverfunktioner.",
"dialog_disconnectedTitle": "Ansluten ej",
"dialog_disconnectedMessage": "Du har kopplats från din companion.",
"contact_connectCompanion": "Anslut till en companion för att få tillgång till repeater- och rumsserverfunktioner."
"contact_connectCompanion": "Anslut till en sällskapstjänst för att komma åt upprepning och rumsserverfunktioner."
}
+1 -4
View File
@@ -2292,8 +2292,5 @@
"chat_newMessages": "Нові повідомлення",
"chat_markAsUnread": "Позначити як непрочитане",
"repeater_chanUtil": "Використання каналу",
"dialog_connectCompanion": "Підключіться до супутнього пристрою, щоб отримати доступ до функцій ретранслятора та сервера кімнат.",
"dialog_disconnectedTitle": "Від’єднано",
"dialog_disconnectedMessage": "Вас від’єднано від вашого супутника.",
"contact_connectCompanion": "Підключіться до супутника, щоб отримати доступ до функцій репітера та серверів кімнат."
"contact_connectCompanion": "Підключіться до супутнього пристрою, щоб отримати доступ до функцій ретранслятора та сервера кімнат."
}
+1 -4
View File
@@ -2317,8 +2317,5 @@
"chat_newMessages": "新的消息",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB 协议、响应和原始数据",
"repeater_chanUtil": "频道利用率",
"dialog_connectCompanion": "连接伴机以访问中继器和房间服务器功能。",
"dialog_disconnectedTitle": "已断开连接",
"dialog_disconnectedMessage": "你已与你的伙伴断开连接。",
"contact_connectCompanion": "连接至伴侣设备以访问中继器和房间服务器功能。"
"contact_connectCompanion": "连接伴机以访问中继器和房间服务器功能。"
}
-34
View File
@@ -4,9 +4,6 @@ import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
import '../connector/meshcore_protocol.dart';
import 'community.dart';
enum ChannelType { public, private, hashtag, communityPublic, communityHashtag }
class Channel {
final int index;
@@ -114,36 +111,5 @@ class Channel {
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
}
static bool isCommunityChannel(ChannelType channelType) {
switch (channelType) {
case ChannelType.communityPublic:
case ChannelType.communityHashtag:
return true;
case ChannelType.public:
case ChannelType.private:
case ChannelType.hashtag:
return false;
}
}
static ChannelType getChannelType(
Channel channel,
CommunityPskIndex communityIndex,
) {
Community? community = communityIndex.getCommunityForChannel(channel);
if (community != null) {
if (Community.isCommunityPublicChannel(channel, community)) {
return ChannelType.communityPublic;
}
return ChannelType.communityHashtag;
}
if (channel.isPublicChannel) {
return ChannelType.public;
} else if (channel.name.startsWith('#')) {
return ChannelType.hashtag;
}
return ChannelType.private;
}
static const String publicChannelPsk = '8b3387e9c5cdea6ac9e5edbaa115cd72';
}
-33
View File
@@ -4,8 +4,6 @@ import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
import 'channel.dart';
/// Represents a community with a shared secret for deriving channel PSKs.
///
/// A Community is a namespace with a shared secret K (32 random bytes),
@@ -164,12 +162,6 @@ class Community {
return hashtag.replaceFirst(RegExp(r'^#'), '').toLowerCase().trim();
}
/// Returns true if this is the community's public channel
static bool isCommunityPublicChannel(Channel channel, Community community) {
final publicPsk = community.deriveCommunityPublicPsk();
return channel.pskHex == Channel.formatPskHex(publicPsk);
}
/// Add a hashtag channel to this community's list
Community addHashtagChannel(String hashtag) {
final normalized = _normalizeCommunityHashtag(hashtag);
@@ -245,28 +237,3 @@ class Community {
@override
int get hashCode => id.hashCode;
}
class CommunityPskIndex {
// Cache of PSK hex -> Community for quick lookup
final Map<String, Community> _pskToCommunity = {};
void initialize(List<Community> communities) {
_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);
_pskToCommunity[Channel.formatPskHex(hashtagPsk)] = community;
}
}
}
/// Returns the community this channel belongs to, or null if not a community channel
Community? getCommunityForChannel(Channel channel) {
return _pskToCommunity[channel.pskHex];
}
}
-1
View File
@@ -50,7 +50,6 @@ class PathRecord {
successCount: json['success_count'] as int? ?? 0,
failureCount: json['failure_count'] as int? ?? 0,
routeWeight: (json['route_weight'] as num?)?.toDouble() ?? 1.0,
byteCount: json['byte_count'] as int? ?? 0,
);
}
}
+6 -79
View File
@@ -8,8 +8,6 @@ import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../models/community.dart';
import '../storage/community_store.dart';
import '../utils/platform_info.dart';
import '../helpers/chat_scroll_controller.dart';
import '../connector/meshcore_protocol.dart';
@@ -58,11 +56,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
final ChatScrollController _scrollController = ChatScrollController();
final FocusNode _textFieldFocusNode = FocusNode();
ChannelMessage? _replyingToMessage;
final CommunityStore _communityStore = CommunityStore();
final CommunityPskIndex _communityIndex = CommunityPskIndex();
final Map<String, GlobalKey> _messageKeys = {};
bool _isLoadingOlder = false;
bool _communitiesLoaded = false;
MeshCoreConnector? _connector;
DateTime? _lastChannelSendAt;
@@ -86,7 +81,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
final idx = widget.channel.index;
final unread = widget.initialUnreadCount;
final messages = connector.getChannelMessages(widget.channel);
_loadCommunities();
ChannelMessage? anchor;
if (unread > 0) {
anchor = _findOldestUnreadChannelAnchor(messages, unread);
@@ -113,19 +107,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
});
}
// TODO: Reload communities when returning from another screen
Future<void> _loadCommunities() async {
final connector = context.read<MeshCoreConnector>();
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
final communities = await _communityStore.loadCommunities();
if (mounted) {
setState(() {
_communityIndex.initialize(communities);
_communitiesLoaded = true;
});
}
}
ChannelMessage? _findOldestUnreadChannelAnchor(
List<ChannelMessage> messages,
int unreadCount,
@@ -212,63 +193,16 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
);
}
Widget _channelIcon(Channel channel) {
// Determine icon based on channel type
final ChannelType channelType = Channel.getChannelType(
channel,
_communityIndex,
);
final bool isCommunityChannel = Channel.isCommunityChannel(channelType);
IconData icon;
switch (channelType) {
case ChannelType.communityPublic:
icon = Icons.groups;
case ChannelType.communityHashtag:
icon = Icons.tag;
case ChannelType.public:
icon = Icons.public;
case ChannelType.hashtag:
icon = Icons.tag;
case ChannelType.private:
icon = Icons.lock;
}
return Stack(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: _communitiesLoaded
? Icon(icon, size: 20)
: SizedBox.square(dimension: 20),
),
if (isCommunityChannel)
Positioned(
right: 0,
bottom: 0,
child: Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: Colors.purple,
shape: BoxShape.circle,
border: Border.all(
color: Theme.of(context).cardColor,
width: 2,
),
),
child: const Icon(Icons.people, size: 8, color: Colors.white),
),
),
],
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Row(
children: [
_channelIcon(widget.channel),
Icon(
widget.channel.isPublicChannel ? Icons.public : Icons.tag,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Column(
@@ -377,7 +311,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
final reversedMessages = messages.reversed.toList();
final itemCount =
reversedMessages.length + (_isLoadingOlder ? 1 : 0);
final keyedMessageIds = <String>{};
// Auto-scroll to bottom if user is already at bottom
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -414,20 +347,14 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
}
final messageIndex = index;
final message = reversedMessages[messageIndex];
final shouldAttachMessageKey = keyedMessageIds.add(
message.messageId,
);
if (shouldAttachMessageKey &&
!_messageKeys.containsKey(message.messageId)) {
if (!_messageKeys.containsKey(message.messageId)) {
_messageKeys[message.messageId] = GlobalKey();
}
final isUnreadAnchor =
_unreadDividerMessageId != null &&
message.messageId == _unreadDividerMessageId;
return Container(
key: shouldAttachMessageKey
? _messageKeys[message.messageId]
: null,
key: _messageKeys[message.messageId]!,
child: Builder(
builder: (context) {
final textScale = context
+58 -31
View File
@@ -40,12 +40,15 @@ class ChannelsScreen extends StatefulWidget {
State<ChannelsScreen> createState() => _ChannelsScreenState();
}
class _ChannelsScreenState extends State<ChannelsScreen> {
class _ChannelsScreenState extends State<ChannelsScreen>
{
final TextEditingController _searchController = TextEditingController();
final CommunityStore _communityStore = CommunityStore();
final CommunityPskIndex _communityIndex = CommunityPskIndex();
List<Community> _communities = [];
Timer? _searchDebounce;
List<Community> _communities = [];
// Cache of PSK hex -> Community for quick lookup
final Map<String, Community> _pskToCommunity = {};
ChannelMessageStore get _channelMessageStore => ChannelMessageStore();
@@ -68,11 +71,37 @@ class _ChannelsScreenState extends State<ChannelsScreen> {
if (mounted) {
setState(() {
_communities = communities;
_communityIndex.initialize(communities);
_buildPskCommunityMap();
});
}
}
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);
_pskToCommunity[Channel.formatPskHex(hashtagPsk)] = community;
}
}
}
/// 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();
return channel.pskHex == Channel.formatPskHex(publicPsk);
}
@override
void dispose() {
_searchDebounce?.cancel();
@@ -343,8 +372,6 @@ class _ChannelsScreenState extends State<ChannelsScreen> {
selectedIndex: 1,
onDestinationSelected: (index) =>
_handleQuickSwitch(index, context),
contactsUnreadCount: connector.getTotalContactsUnreadCount(),
channelsUnreadCount: connector.getTotalChannelsUnreadCount(),
),
),
),
@@ -360,41 +387,41 @@ class _ChannelsScreenState extends State<ChannelsScreen> {
int? dragIndex,
}) {
final unreadCount = connector.getUnreadCountForChannel(channel);
final community = _getCommunityForChannel(channel);
final isCommunityChannel = community != null;
final isCommunityPublic =
isCommunityChannel && _isCommunityPublicChannel(channel, community);
// Determine icon and colors based on channel type
IconData icon;
Color iconColor;
Color bgColor;
final ChannelType channelType = Channel.getChannelType(
channel,
_communityIndex,
);
final bool isCommunityChannel = Channel.isCommunityChannel(channelType);
switch (channelType) {
case ChannelType.communityPublic:
if (isCommunityChannel) {
// Community channel styling
iconColor = Colors.purple;
bgColor = Colors.purple.withValues(alpha: 0.2);
if (isCommunityPublic) {
icon = Icons.groups;
iconColor = Colors.purple;
bgColor = Colors.purple.withValues(alpha: 0.2);
case ChannelType.communityHashtag:
} else {
icon = Icons.tag;
iconColor = Colors.purple;
bgColor = Colors.purple.withValues(alpha: 0.2);
case ChannelType.public:
icon = Icons.public;
iconColor = Colors.green;
bgColor = Colors.green.withValues(alpha: 0.2);
case ChannelType.hashtag:
icon = Icons.tag;
iconColor = Colors.blue;
bgColor = Colors.blue.withValues(alpha: 0.2);
case ChannelType.private:
icon = Icons.lock;
iconColor = Colors.blue;
bgColor = Colors.blue.withValues(alpha: 0.2);
}
} else if (channel.isPublicChannel) {
icon = Icons.public;
iconColor = Colors.green;
bgColor = Colors.green.withValues(alpha: 0.2);
} else if (channel.name.startsWith('#')) {
icon = Icons.tag;
iconColor = Colors.blue;
bgColor = Colors.blue.withValues(alpha: 0.2);
} else {
icon = Icons.lock;
iconColor = Colors.blue;
bgColor = Colors.blue.withValues(alpha: 0.2);
}
return Card(
key: ValueKey('${channel.index}_${channel.pskHex}_${channel.name}'),
key: ValueKey('channel_${channel.index}'),
margin: const EdgeInsets.only(bottom: 12),
child: GestureDetector(
onSecondaryTapUp: PlatformInfo.isDesktop
+1 -6
View File
@@ -451,7 +451,6 @@ class _ChatScreenState extends State<ChatScreen> {
) {
// Reverse messages so newest appear at bottom with reverse: true
final reversedMessages = messages.reversed.toList();
var unreadAnchorKeyAssigned = false;
final itemCount = reversedMessages.length + (_isLoadingOlder ? 1 : 0);
// Auto-scroll to bottom if user is already at bottom
@@ -526,11 +525,7 @@ class _ChatScreenState extends State<ChatScreen> {
children: [const UnreadDivider(), bubble],
)
: bubble;
final shouldAttachUnreadScrollKey =
!unreadAnchorKeyAssigned &&
identical(message, _pendingUnreadScrollTarget);
if (shouldAttachUnreadScrollKey) {
unreadAnchorKeyAssigned = true;
if (identical(message, _pendingUnreadScrollTarget)) {
return KeyedSubtree(key: _unreadScrollKey, child: child);
}
return child;
+54 -58
View File
@@ -49,7 +49,8 @@ class ContactsScreen extends StatefulWidget {
State<ContactsScreen> createState() => _ContactsScreenState();
}
class _ContactsScreenState extends State<ContactsScreen> {
class _ContactsScreenState extends State<ContactsScreen>
{
final TextEditingController _searchController = TextEditingController();
final ContactGroupStore _groupStore = ContactGroupStore();
MeshCoreConnector? _scopeSyncConnector;
@@ -313,64 +314,63 @@ class _ContactsScreenState extends State<ContactsScreen> {
title: AppBarTitle(context.l10n.contacts_title),
automaticallyImplyLeading: false,
actions: [
if (connector.isConnected)
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.connect_without_contact),
const SizedBox(width: 8),
Text(context.l10n.contacts_zeroHopAdvert),
],
),
onTap: () async {
await connector.sendSelfAdvert(flood: false);
showDismissibleSnackBar(
context,
content: Text(context.l10n.settings_advertisementSent),
);
},
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.connect_without_contact),
const SizedBox(width: 8),
Text(context.l10n.contacts_zeroHopAdvert),
],
),
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.cell_tower),
const SizedBox(width: 8),
Text(context.l10n.contacts_floodAdvert),
],
onTap: () => {
connector.sendSelfAdvert(flood: false),
showDismissibleSnackBar(
context,
content: Text(context.l10n.settings_advertisementSent),
),
onTap: () async {
await connector.sendSelfAdvert(flood: true);
showDismissibleSnackBar(
context,
content: Text(context.l10n.settings_advertisementSent),
);
},
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.cell_tower),
const SizedBox(width: 8),
Text(context.l10n.contacts_floodAdvert),
],
),
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.copy),
const SizedBox(width: 8),
Text(context.l10n.contacts_copyAdvertToClipboard),
],
onTap: () => {
connector.sendSelfAdvert(flood: true),
showDismissibleSnackBar(
context,
content: Text(context.l10n.settings_advertisementSent),
),
onTap: () => _contactExport(Uint8List.fromList([])),
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.copy),
const SizedBox(width: 8),
Text(context.l10n.contacts_copyAdvertToClipboard),
],
),
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.paste),
const SizedBox(width: 8),
Text(context.l10n.contacts_addContactFromClipboard),
],
),
onTap: () => _contactImport(),
onTap: () => _contactExport(Uint8List.fromList([])),
),
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.paste),
const SizedBox(width: 8),
Text(context.l10n.contacts_addContactFromClipboard),
],
),
],
icon: const Icon(Icons.connect_without_contact),
),
onTap: () => _contactImport(),
),
],
icon: const Icon(Icons.connect_without_contact),
),
PopupMenuButton(
itemBuilder: (context) => [
if (connector.isConnected)
@@ -442,8 +442,6 @@ class _ContactsScreenState extends State<ContactsScreen> {
selectedIndex: 0,
onDestinationSelected: (index) =>
_handleQuickSwitch(index, context),
contactsUnreadCount: connector.getTotalContactsUnreadCount(),
channelsUnreadCount: connector.getTotalChannelsUnreadCount(),
),
),
),
@@ -828,7 +826,6 @@ class _ContactsScreenState extends State<ContactsScreen> {
contact,
);
return _ContactTile(
key: ValueKey(contact.publicKeyHex),
contact: contact,
lastSeen: _resolveLastSeen(contact),
unreadCount: unreadCount,
@@ -1049,7 +1046,7 @@ class _ContactsScreenState extends State<ContactsScreen> {
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(context.l10n.scanner_notConnected),
content: Text(context.l10n.dialog_connectCompanion),
content: Text(context.l10n.contact_connectCompanion),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
@@ -1497,7 +1494,6 @@ class _ContactTile extends StatelessWidget {
final VoidCallback onLongPress;
const _ContactTile({
super.key,
required this.contact,
required this.lastSeen,
required this.unreadCount,
@@ -539,12 +539,6 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
child: QuickSwitchBar(
selectedIndex: 2,
onDestinationSelected: (index) => _handleQuickSwitch(index, context),
contactsUnreadCount: context
.watch<MeshCoreConnector>()
.getTotalContactsUnreadCount(),
channelsUnreadCount: context
.watch<MeshCoreConnector>()
.getTotalChannelsUnreadCount(),
),
),
);
+1 -29
View File
@@ -416,7 +416,7 @@ class _MapScreenState extends State<MapScreen> {
centerTitle: true,
automaticallyImplyLeading: false,
actions: [
if (!_isBuildingPathTrace && connector.isConnected)
if (!_isBuildingPathTrace)
IconButton(
icon: const Icon(Icons.radar),
onPressed: () => _startPath(
@@ -694,8 +694,6 @@ class _MapScreenState extends State<MapScreen> {
selectedIndex: 2,
onDestinationSelected: (index) =>
_handleQuickSwitch(index, context),
contactsUnreadCount: connector.getTotalContactsUnreadCount(),
channelsUnreadCount: connector.getTotalChannelsUnreadCount(),
),
),
floatingActionButton: FloatingActionButton(
@@ -1500,28 +1498,7 @@ class _MapScreenState extends State<MapScreen> {
);
}
void _showCompanionRequiredDialog(BuildContext context) {
showDialog<void>(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(context.l10n.scanner_notConnected),
content: Text(context.l10n.dialog_connectCompanion),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(context.l10n.common_ok),
),
],
),
);
}
void _showRepeaterLogin(BuildContext context, Contact repeater) {
final connector = context.read<MeshCoreConnector>();
if (!connector.isConnected) {
_showCompanionRequiredDialog(context);
return;
}
showDialog(
context: context,
builder: (context) => RepeaterLoginDialog(
@@ -1544,11 +1521,6 @@ class _MapScreenState extends State<MapScreen> {
}
void _showRoomLogin(BuildContext context, Contact room) {
final connector = context.read<MeshCoreConnector>();
if (!connector.isConnected) {
_showCompanionRequiredDialog(context);
return;
}
showDialog(
context: context,
builder: (context) => RoomLoginDialog(
+1 -10
View File
@@ -42,18 +42,9 @@ class ChannelStore {
try {
final jsonList = jsonDecode(jsonString) as List<dynamic>;
final channels = jsonList
return jsonList
.map((entry) => _fromJson(entry as Map<String, dynamic>))
.toList();
// Deduplicate: keep the last entry per channel index
final seen = <int>{};
final deduped = <Channel>[];
for (final channel in channels.reversed) {
if (seen.add(channel.index)) {
deduped.add(channel);
}
}
return deduped.reversed.toList();
} catch (_) {
return [];
}
-15
View File
@@ -31,21 +31,6 @@ Future<bool> showDisconnectDialog(
if (confirmed == true) {
appLogger.info('Disconnect confirmed from popup', tag: 'Connection');
await connector.disconnect();
if (context.mounted) {
await showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: Text(context.l10n.dialog_disconnectedTitle),
content: Text(context.l10n.dialog_disconnectedMessage),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(context.l10n.common_ok),
),
],
),
);
}
return true;
}
return false;
+1 -2
View File
@@ -69,8 +69,7 @@ class AppBarTitle extends StatelessWidget {
if (showBattery) BatteryIndicator(connector: connector),
if (showSnr) SNRIndicator(connector: connector),
if (connector.supportsCompanionRadioStats)
if (connector.isConnected)
const RadioStatsIconButton(compact: true),
const RadioStatsIconButton(compact: true),
],
),
trailing ?? const SizedBox.shrink(),
+2 -44
View File
@@ -6,15 +6,11 @@ import '../l10n/l10n.dart';
class QuickSwitchBar extends StatelessWidget {
final int selectedIndex;
final ValueChanged<int> onDestinationSelected;
final int contactsUnreadCount;
final int channelsUnreadCount;
const QuickSwitchBar({
super.key,
required this.selectedIndex,
required this.onDestinationSelected,
this.contactsUnreadCount = 0,
this.channelsUnreadCount = 0,
});
@override
@@ -66,30 +62,15 @@ class QuickSwitchBar extends StatelessWidget {
onDestinationSelected: onDestinationSelected,
destinations: [
NavigationDestination(
icon: _buildIconWithBadge(
const Icon(Icons.people_outline),
contactsUnreadCount,
),
selectedIcon: _buildIconWithBadge(
const Icon(Icons.people),
contactsUnreadCount,
),
icon: const Icon(Icons.people_outline),
label: context.l10n.nav_contacts,
),
NavigationDestination(
icon: _buildIconWithBadge(
const Icon(Icons.tag),
channelsUnreadCount,
),
selectedIcon: _buildIconWithBadge(
const Icon(Icons.tag),
channelsUnreadCount,
),
icon: const Icon(Icons.tag),
label: context.l10n.nav_channels,
),
NavigationDestination(
icon: const Icon(Icons.map_outlined),
selectedIcon: const Icon(Icons.map),
label: context.l10n.nav_map,
),
],
@@ -100,27 +81,4 @@ class QuickSwitchBar extends StatelessWidget {
),
);
}
Widget _buildIconWithBadge(Icon icon, int count) {
if (count <= 0) return icon;
return Stack(
clipBehavior: Clip.none,
children: [
icon,
Positioned(
right: -2,
top: -2,
child: Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Colors.redAccent,
shape: BoxShape.circle,
),
),
),
],
);
}
}
-6
View File
@@ -38,12 +38,6 @@ class _FakeMeshCoreConnector extends MeshCoreConnector {
lastHost = host;
lastPort = port;
}
@override
Future<void> disconnect({
bool manual = true,
bool skipBleDeviceDisconnect = false,
}) async {}
}
Widget _buildTestApp({
-6
View File
@@ -73,12 +73,6 @@ class _FakeMeshCoreConnector extends MeshCoreConnector {
void setUsbFallbackDeviceName(String label) {
fallbackDeviceName = label;
}
@override
Future<void> disconnect({
bool manual = true,
bool skipBleDeviceDisconnect = false,
}) async {}
}
Widget _buildTestApp({
+921 -413
View File
File diff suppressed because it is too large Load Diff