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.
This commit is contained in:
Winston Lowe
2026-05-10 16:02:58 -07:00
parent 396f70c994
commit 094d5d2706
45 changed files with 320 additions and 110 deletions
+86 -20
View File
@@ -40,6 +40,7 @@ import '../storage/contact_discovery_store.dart';
import '../storage/contact_settings_store.dart';
import '../storage/contact_store.dart';
import '../storage/message_store.dart';
import '../storage/prefs_manager.dart';
import '../storage/unread_store.dart';
import '../utils/app_logger.dart';
import '../utils/battery_utils.dart';
@@ -124,6 +125,8 @@ class MeshCoreRadioStateSnapshot {
class MeshCoreConnector extends ChangeNotifier {
// Message windowing to limit memory usage
static const int _messageWindowSize = 200;
static const String _lastCompanionPublicKeyPref =
'last_companion_public_key_hex';
MeshCoreConnectionState _state = MeshCoreConnectionState.disconnected;
BluetoothDevice? _device;
@@ -476,6 +479,9 @@ class MeshCoreConnector extends ChangeNotifier {
}
List<Message> getMessages(Contact contact) {
if (!_loadedConversationKeys.contains(contact.publicKeyHex)) {
unawaited(_loadMessagesForContact(contact.publicKeyHex));
}
return _conversations[contact.publicKeyHex] ?? [];
}
@@ -653,7 +659,14 @@ class MeshCoreConnector extends ChangeNotifier {
}
Future<void> loadCachedChannels() async {
_cachedChannels = await _channelStore.loadChannels();
final loaded = await _channelStore.loadChannels();
_cachedChannels = loaded;
if (_channels.isEmpty && loaded.isNotEmpty) {
_channels
..clear()
..addAll(loaded);
notifyListeners();
}
}
void setActiveContact(String? contactKeyHex) {
@@ -913,6 +926,69 @@ class MeshCoreConnector extends ChangeNotifier {
}
}
Future<void> loadAllCachedDataForCurrentCompanion() async {
await loadContactCache();
await _loadDiscoveredContactCache();
await loadChannelSettings();
await loadCachedChannels();
await loadAllChannelMessages();
await loadUnreadState();
}
Future<void> restoreLastCompanionScope() async {
final prefs = PrefsManager.instance;
final lastCompanionPublicKeyHex = prefs.getString(
_lastCompanionPublicKeyPref,
);
if (lastCompanionPublicKeyHex == null ||
lastCompanionPublicKeyHex.trim().isEmpty) {
return;
}
_setScopedStorePublicKey(lastCompanionPublicKeyHex);
}
Future<void> loadDiscoveredContactCache() => _loadDiscoveredContactCache();
void _setScopedStorePublicKey(String publicKeyHex) {
_channelMessageStore.setPublicKeyHex = publicKeyHex;
_messageStore.setPublicKeyHex = publicKeyHex;
_channelOrderStore.setPublicKeyHex = publicKeyHex;
_channelSettingsStore.setPublicKeyHex = publicKeyHex;
_contactSettingsStore.setPublicKeyHex = publicKeyHex;
_contactStore.setPublicKeyHex = publicKeyHex;
_channelStore.setPublicKeyHex = publicKeyHex;
_unreadStore.setPublicKeyHex = publicKeyHex;
}
void _clearCachedCompanionData() {
_contacts.clear();
_discoveredContacts.clear();
_conversations.clear();
_loadedConversationKeys.clear();
_channelMessages.clear();
_cachedChannels.clear();
_knownContactKeys.clear();
_contactUnreadCount.clear();
_unreadStateLoaded = false;
notifyListeners();
}
Future<void> _persistLastCompanionScope() async {
final keyHex = selfPublicKeyHex;
if (keyHex.isEmpty) return;
final prefs = PrefsManager.instance;
await prefs.setString(_lastCompanionPublicKeyPref, keyHex);
}
Future<void> _reloadOfflineCachesForLastCompanion() async {
if (_state != MeshCoreConnectionState.disconnected) {
return;
}
await restoreLastCompanionScope();
await loadContactCache();
await _loadDiscoveredContactCache();
}
Future<void> _loadDiscoveredContactCache() async {
final cached = await _discoveryContactStore.loadContacts();
_discoveredContacts
@@ -1450,6 +1526,7 @@ class MeshCoreConnector extends ChangeNotifier {
_cancelReconnectTimer();
_manualDisconnect = false;
_resetConnectionHandshakeState();
_clearCachedCompanionData();
_activeTransport = MeshCoreTransportType.usb;
_setState(MeshCoreConnectionState.connecting);
@@ -1531,6 +1608,7 @@ class MeshCoreConnector extends ChangeNotifier {
_cancelReconnectTimer();
_manualDisconnect = false;
_resetConnectionHandshakeState();
_clearCachedCompanionData();
_activeTransport = MeshCoreTransportType.tcp;
_setState(MeshCoreConnectionState.connecting);
@@ -1665,6 +1743,7 @@ class MeshCoreConnector extends ChangeNotifier {
_activeTransport = MeshCoreTransportType.bluetooth;
await stopScan();
_clearCachedCompanionData();
_setState(MeshCoreConnectionState.connecting);
_device = device;
_deviceId = device.remoteId.toString();
@@ -2416,6 +2495,7 @@ class MeshCoreConnector extends ChangeNotifier {
'Disconnect complete transport=$transportLabel manual=$manual',
tag: 'Connection',
);
unawaited(_reloadOfflineCachesForLastCompanion());
if (!manual && transportAtDisconnect == MeshCoreTransportType.bluetooth) {
_scheduleReconnect();
}
@@ -3796,26 +3876,12 @@ class MeshCoreConnector extends ChangeNotifier {
return;
}
//set all the stores' public key so they can load the correct data
_channelMessageStore.setPublicKeyHex = selfPublicKeyHex;
_messageStore.setPublicKeyHex = selfPublicKeyHex;
_channelOrderStore.setPublicKeyHex = selfPublicKeyHex;
_channelSettingsStore.setPublicKeyHex = selfPublicKeyHex;
_contactSettingsStore.setPublicKeyHex = selfPublicKeyHex;
_contactStore.setPublicKeyHex = selfPublicKeyHex;
_channelStore.setPublicKeyHex = selfPublicKeyHex;
_unreadStore.setPublicKeyHex = selfPublicKeyHex;
// Set scoped stores to this companion and remember it for next launch.
_setScopedStorePublicKey(selfPublicKeyHex);
unawaited(_persistLastCompanionScope());
// Now that we have self info, we can load all the persisted data for this node
_loadChannelOrder();
loadContactCache();
loadChannelSettings();
loadCachedChannels();
// Load persisted channel messages
loadAllChannelMessages();
loadUnreadState();
_loadDiscoveredContactCache();
// Now that we have self info, we can load all the persisted data for this node.
unawaited(loadAllCachedDataForCurrentCompanion());
_awaitingSelfInfo = false;
_selfInfoRetryTimer?.cancel();
+2 -1
View File
@@ -2311,5 +2311,6 @@
"settings_companionDebugLogSubtitle": "Команди, отговори и сурови данни за протоколите BLE/TCP/USB",
"chat_newMessages": "Нови съобщения",
"settings_companionDebugLog": "Лог за отстраняване на грешки (за съпътстваща програма)",
"repeater_chanUtil": "Използване на канала"
"repeater_chanUtil": "Използване на канала",
"contact_connectCompanion": "Свържете се с придружител, за да получите достъп до функциите на ретранслатора и сървъра за стаи."
}
+2 -1
View File
@@ -2339,5 +2339,6 @@
"chat_newMessages": "Neue Nachrichten",
"settings_companionDebugLog": "Debug-Protokoll für die Begleitsoftware",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB-Befehle, Antworten und Rohdaten",
"repeater_chanUtil": "Nutzung des Kanals"
"repeater_chanUtil": "Nutzung des Kanals",
"contact_connectCompanion": "Verbinden Sie sich mit einem Companion, um auf die Funktionen des Repeaters und des Raumservers zuzugreifen."
}
+2 -1
View File
@@ -2366,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."
}
+2 -1
View File
@@ -2339,5 +2339,6 @@
"chat_newMessages": "Nuevos mensajes",
"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"
"repeater_chanUtil": "Utilización del canal",
"contact_connectCompanion": "Conéctate a un compañero para acceder a las funciones de repetidor y servidor de sala."
}
+2 -1
View File
@@ -2318,5 +2318,6 @@
"chat_markAsUnread": "Signaler comme non lu",
"chat_newMessages": "Nouveaux messages",
"settings_companionDebugLogSubtitle": "Commandes, réponses et données brutes pour les protocoles BLE/TCP/USB",
"repeater_chanUtil": "Utilisation du canal"
"repeater_chanUtil": "Utilisation du canal",
"contact_connectCompanion": "Connectez-vous à un compagnon pour accéder aux fonctionnalités de répéteur et de serveur de salle."
}
+2 -1
View File
@@ -2349,5 +2349,6 @@
"chat_newMessages": "Új üzenetek",
"settings_companionDebugLog": "Párhuzamos hibakeresési napló",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB parancsok, válaszok és alapvető adatok",
"repeater_chanUtil": "Csatorna-használat"
"repeater_chanUtil": "Csatorna-használat",
"contact_connectCompanion": "Csatlakozzon egy kísérőhöz a ismétlő és szobaszerver funkciók eléréséhez."
}
+2 -1
View File
@@ -2311,5 +2311,6 @@
"settings_companionDebugLog": "Registro di debug per il supporto",
"chat_newMessages": "Nuovi messaggi",
"chat_markAsUnread": "Segna come non letto",
"repeater_chanUtil": "Utilizzo del canale"
"repeater_chanUtil": "Utilizzo del canale",
"contact_connectCompanion": "Connettiti a un dispositivo companion per accedere alle funzionalità di ripetitore e server stanza."
}
+2 -1
View File
@@ -2349,5 +2349,6 @@
"settings_companionDebugLog": "同伴デバッグログ",
"chat_newMessages": "新しいメッセージ",
"chat_markAsUnread": "未読としてマークする",
"repeater_chanUtil": "チャンネルの利用状況"
"repeater_chanUtil": "チャンネルの利用状況",
"contact_connectCompanion": "コネクトしてリピーターとルームサーバー機能にアクセス"
}
+2 -1
View File
@@ -2349,5 +2349,6 @@
"chat_newMessages": "새로운 메시지",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB 명령어, 응답 및 원시 데이터",
"chat_markAsUnread": "미리 읽지 않음으로 표시",
"repeater_chanUtil": "채널 활용도"
"repeater_chanUtil": "채널 활용도",
"contact_connectCompanion": "리피터 및 룸 서버 기능에 액세스하려면 컴패니언에 연결하세요."
}
+6
View File
@@ -7335,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
View File
@@ -4288,4 +4288,8 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion =>
'Свържете се с придружител, за да получите достъп до функциите на ретранслатора и сървъра за стаи.';
}
+4
View File
@@ -4305,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
View File
@@ -4210,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
View File
@@ -4292,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
View File
@@ -4321,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
View File
@@ -4309,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
View File
@@ -4297,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
View File
@@ -4063,4 +4063,7 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion => 'コネクトしてリピーターとルームサーバー機能にアクセス';
}
+3
View File
@@ -4064,4 +4064,7 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion => '리피터 및 룸 서버 기능에 액세스하려면 컴패니언에 연결하세요.';
}
+4
View File
@@ -4273,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
View File
@@ -4309,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
View File
@@ -4285,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
View File
@@ -4303,4 +4303,8 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get contact_typeUnknown => 'Неизвестно';
@override
String get contact_connectCompanion =>
'Подключитесь к компаньону, чтобы получить доступ к функциям ретранслятора и сервера комнат.';
}
+4
View File
@@ -4269,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
View File
@@ -4267,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
View File
@@ -4241,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
View File
@@ -4304,4 +4304,8 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get contact_typeUnknown => 'Невідомо';
@override
String get contact_connectCompanion =>
'Підключіться до супутнього пристрою, щоб отримати доступ до функцій ретранслятора та сервера кімнат.';
}
+3
View File
@@ -3938,4 +3938,7 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get contact_connectCompanion => '连接伴机以访问中继器和房间服务器功能。';
}
+2 -1
View File
@@ -2311,5 +2311,6 @@
"chat_newMessages": "Nieuwe berichten",
"chat_markAsUnread": "Markeer als ongelezen",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB commando's, antwoorden en ruwe data",
"repeater_chanUtil": "Gebruik van het kanaal"
"repeater_chanUtil": "Gebruik van het kanaal",
"contact_connectCompanion": "Maak verbinding met een companion om repeater- en kamerserverfuncties te gebruiken."
}
+2 -1
View File
@@ -2349,5 +2349,6 @@
"settings_companionDebugLogSubtitle": "Polecenia, odpowiedzi i surowe dane związane z protokołami BLE/TCP/USB",
"chat_markAsUnread": "Oznacz jako nieprzeczytane",
"settings_companionDebugLog": "Log debugowania (dla pomocy w rozwiązywaniu problemów)",
"repeater_chanUtil": "Wykorzystanie kanału"
"repeater_chanUtil": "Wykorzystanie kanału",
"contact_connectCompanion": "Połącz się z towarzyszem, aby uzyskać dostęp do funkcji powtarzacza i serwera pokoi."
}
+2 -1
View File
@@ -2311,5 +2311,6 @@
"settings_companionDebugLogSubtitle": "Comandos, respostas e dados brutos para protocolos BLE/TCP/USB",
"chat_markAsUnread": "Marcar como não lido",
"chat_newMessages": "Novas mensagens",
"repeater_chanUtil": "Utilização do canal"
"repeater_chanUtil": "Utilização do canal",
"contact_connectCompanion": "Conecte-se a um dispositivo companion para acessar as funcionalidades de repetidor e servidor de salas."
}
+2 -1
View File
@@ -1614,5 +1614,6 @@
"repeater_cliHelpStatsCore": "(Только для серийного оборудования) Отображает основные статистические данные прошивки.",
"settings_companionDebugLogSubtitle": "Команды, ответы и необработанные данные, используемые для протоколов BLE, TCP и USB.",
"repeater_chanUtil": "Использование канала",
"settings_companionDebugLog": "Журнал отладки (для сопутствующего приложения)"
"settings_companionDebugLog": "Журнал отладки (для сопутствующего приложения)",
"contact_connectCompanion": "Подключитесь к компаньону, чтобы получить доступ к функциям ретранслятора и сервера комнат."
}
+2 -1
View File
@@ -2311,5 +2311,6 @@
"settings_companionDebugLogSubtitle": "Príkazy, odpovede a surové dáta pre protokoly BLE/TCP/USB",
"settings_companionDebugLog": "Logovanie pre ladenie (sprievodný log)",
"chat_newMessages": "Nové správy",
"repeater_chanUtil": "Využitie kanálu"
"repeater_chanUtil": "Využitie kanálu",
"contact_connectCompanion": "Pripojte sa k sprievodcovi a získajte prístup k funkciám opakovača a serveru miestností."
}
+2 -1
View File
@@ -2311,5 +2311,6 @@
"chat_markAsUnread": "Označiti kot neneobdelano",
"chat_newMessages": "Nove novice",
"settings_companionDebugLogSubtitle": "Navodila, odgovori in surova podatka za BLE/TCP/USB.",
"repeater_chanUtil": "Uporaba kanala"
"repeater_chanUtil": "Uporaba kanala",
"contact_connectCompanion": "Povežite se s spremljevalnikom za dostop do funkcij ponavljalnika in strežnika sob."
}
+2 -1
View File
@@ -2311,5 +2311,6 @@
"settings_companionDebugLog": "Följande felsökningslogg",
"chat_newMessages": "Nya meddelanden",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB-kommandon, svar och rådata",
"repeater_chanUtil": "Användning av kanal"
"repeater_chanUtil": "Användning av kanal",
"contact_connectCompanion": "Anslut till en sällskapstjänst för att komma åt upprepning och rumsserverfunktioner."
}
+2 -1
View File
@@ -2291,5 +2291,6 @@
"settings_companionDebugLogSubtitle": "Команди, відповіді та необроблена інформація для протоколів BLE/TCP/USB",
"chat_newMessages": "Нові повідомлення",
"chat_markAsUnread": "Позначити як непрочитане",
"repeater_chanUtil": "Використання каналу"
"repeater_chanUtil": "Використання каналу",
"contact_connectCompanion": "Підключіться до супутнього пристрою, щоб отримати доступ до функцій ретранслятора та сервера кімнат."
}
+2 -1
View File
@@ -2316,5 +2316,6 @@
"settings_companionDebugLog": "调试日志",
"chat_newMessages": "新的消息",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB 协议、响应和原始数据",
"repeater_chanUtil": "频道利用率"
"repeater_chanUtil": "频道利用率",
"contact_connectCompanion": "连接伴机以访问中继器和房间服务器功能。"
}
+4 -9
View File
@@ -5,10 +5,10 @@ import 'l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'screens/chrome_required_screen.dart';
import 'screens/contacts_screen.dart';
import 'utils/platform_info.dart';
import 'connector/meshcore_connector.dart';
import 'screens/scanner_screen.dart';
import 'services/storage_service.dart';
import 'services/message_retry_service.dart';
import 'services/path_history_service.dart';
@@ -81,13 +81,8 @@ void main() async {
timeoutPredictionService: timeoutPredictionService,
);
await connector.loadContactCache();
await connector.loadChannelSettings();
await connector.loadCachedChannels();
// Load persisted channel messages
await connector.loadAllChannelMessages();
await connector.loadUnreadState();
await connector.restoreLastCompanionScope();
await connector.loadAllCachedDataForCurrentCompanion();
runApp(
MeshCoreApp(
@@ -218,7 +213,7 @@ class MeshCoreApp extends StatelessWidget {
},
home: (PlatformInfo.isWeb && !PlatformInfo.isChrome)
? const ChromeRequiredScreen()
: const ScannerScreen(),
: const ContactsScreen(),
);
},
),
+6 -1
View File
@@ -1057,6 +1057,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
Widget _buildMessageComposer() {
final connector = context.watch<MeshCoreConnector>();
if (!connector.isConnected) {
return const SizedBox.shrink();
}
final maxBytes = maxChannelMessageBytes(connector.selfName);
final settings = context.watch<AppSettingsService>().settings;
return Column(
@@ -1208,6 +1211,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
}
Future<void> _sendMessage() async {
final connector = context.read<MeshCoreConnector>();
if (!connector.isConnected) return;
final text = _textController.text.trim();
if (text.isEmpty) return;
@@ -1222,7 +1228,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
}
_lastChannelSendAt = now;
final connector = context.read<MeshCoreConnector>();
final settings = context.read<AppSettingsService>().settings;
final translationService = context.read<TranslationService>();
+28 -16
View File
@@ -17,7 +17,6 @@ import '../models/channel.dart';
import '../models/community.dart';
import '../storage/community_store.dart';
import '../utils/dialog_utils.dart';
import '../utils/disconnect_navigation_mixin.dart';
import '../utils/route_transitions.dart';
import '../widgets/list_filter_widget.dart';
import '../widgets/empty_state.dart';
@@ -29,6 +28,7 @@ import 'channel_chat_screen.dart';
import 'community_qr_scanner_screen.dart';
import 'contacts_screen.dart';
import 'map_screen.dart';
import 'scanner_screen.dart';
import 'settings_screen.dart';
class ChannelsScreen extends StatefulWidget {
@@ -41,7 +41,7 @@ class ChannelsScreen extends StatefulWidget {
}
class _ChannelsScreenState extends State<ChannelsScreen>
with DisconnectNavigationMixin {
{
final TextEditingController _searchController = TextEditingController();
final CommunityStore _communityStore = CommunityStore();
Timer? _searchDebounce;
@@ -117,11 +117,6 @@ class _ChannelsScreenState extends State<ChannelsScreen>
final channelMessageStore = ChannelMessageStore();
channelMessageStore.setPublicKeyHex = connector.selfPublicKeyHex;
// Auto-navigate back to scanner if disconnected
if (!checkConnectionAndNavigate(connector)) {
return const SizedBox.shrink();
}
final allowBack = !connector.isConnected;
return PopScope(
@@ -134,16 +129,33 @@ class _ChannelsScreenState extends State<ChannelsScreen>
actions: [
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.logout, color: Colors.red),
const SizedBox(width: 8),
Text(context.l10n.common_disconnect),
],
if (connector.isConnected)
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.logout, color: Colors.red),
const SizedBox(width: 8),
Text(context.l10n.common_disconnect),
],
),
onTap: () => _disconnect(context),
)
else
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.bluetooth_searching),
const SizedBox(width: 8),
Text(context.l10n.common_connect),
],
),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ScannerScreen(),
),
),
),
onTap: () => _disconnect(context),
),
if (_communities.isNotEmpty)
PopupMenuItem(
child: Row(
+4 -1
View File
@@ -414,7 +414,7 @@ class _ChatScreenState extends State<ChatScreen> {
],
),
),
_buildInputBar(connector),
if (connector.isConnected) _buildInputBar(connector),
],
);
},
@@ -693,6 +693,9 @@ class _ChatScreenState extends State<ChatScreen> {
}
Future<void> _sendMessage(MeshCoreConnector connector) async {
if (!connector.isConnected) {
return;
}
final text = _textController.text.trim();
if (text.isEmpty) return;
+54 -16
View File
@@ -19,7 +19,6 @@ import '../services/ui_view_state_service.dart';
import '../utils/contact_search.dart';
import '../storage/contact_group_store.dart';
import '../utils/dialog_utils.dart';
import '../utils/disconnect_navigation_mixin.dart';
import '../utils/emoji_utils.dart';
import '../utils/route_transitions.dart';
import '../widgets/list_filter_widget.dart';
@@ -34,6 +33,7 @@ import 'chat_screen.dart';
import 'discovery_screen.dart';
import 'map_screen.dart';
import 'repeater_hub_screen.dart';
import 'scanner_screen.dart';
import 'settings_screen.dart';
enum RoomLoginDestination { chat, management }
@@ -50,7 +50,7 @@ class ContactsScreen extends StatefulWidget {
}
class _ContactsScreenState extends State<ContactsScreen>
with DisconnectNavigationMixin {
{
final TextEditingController _searchController = TextEditingController();
final ContactGroupStore _groupStore = ContactGroupStore();
MeshCoreConnector? _scopeSyncConnector;
@@ -306,11 +306,6 @@ class _ContactsScreenState extends State<ContactsScreen>
Widget build(BuildContext context) {
final connector = context.watch<MeshCoreConnector>();
// Auto-navigate back to scanner if disconnected
if (!checkConnectionAndNavigate(connector)) {
return const SizedBox.shrink();
}
final allowBack = !connector.isConnected;
return PopScope(
canPop: allowBack,
@@ -378,16 +373,33 @@ class _ContactsScreenState extends State<ContactsScreen>
),
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.logout, color: Colors.red),
const SizedBox(width: 8),
Text(context.l10n.common_disconnect),
],
if (connector.isConnected)
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.logout, color: Colors.red),
const SizedBox(width: 8),
Text(context.l10n.common_disconnect),
],
),
onTap: () => _disconnect(context, connector),
)
else
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.bluetooth_searching),
const SizedBox(width: 8),
Text(context.l10n.common_connect),
],
),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ScannerScreen(),
),
),
),
onTap: () => _disconnect(context, connector),
),
PopupMenuItem(
child: Row(
children: [
@@ -965,6 +977,11 @@ class _ContactsScreenState extends State<ContactsScreen>
}
void _showRepeaterLogin(BuildContext context, Contact repeater) {
final connector = context.read<MeshCoreConnector>();
if (!connector.isConnected) {
_showCompanionRequiredDialog(context);
return;
}
showDialog(
context: context,
builder: (context) => RepeaterLoginDialog(
@@ -991,6 +1008,11 @@ class _ContactsScreenState extends State<ContactsScreen>
Contact room,
RoomLoginDestination destination,
) {
final connector = context.read<MeshCoreConnector>();
if (!connector.isConnected) {
_showCompanionRequiredDialog(context);
return;
}
showDialog(
context: context,
builder: (context) => RoomLoginDialog(
@@ -1019,6 +1041,22 @@ class _ContactsScreenState extends State<ContactsScreen>
);
}
void _showCompanionRequiredDialog(BuildContext context) {
showDialog<void>(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(context.l10n.scanner_notConnected),
content: Text(context.l10n.contact_connectCompanion),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(context.l10n.common_ok),
),
],
),
);
}
void _confirmDeleteGroup(BuildContext context, ContactGroup group) {
if (!_hasGroupStoreScope(context.read<MeshCoreConnector>())) {
_showGroupsUnavailableMessage(context);
+27 -9
View File
@@ -31,6 +31,7 @@ import '../widgets/repeater_login_dialog.dart';
import '../widgets/room_login_dialog.dart';
import '../helpers/snack_bar_builder.dart';
import 'repeater_hub_screen.dart';
import 'scanner_screen.dart';
import 'settings_screen.dart';
import 'line_of_sight_map_screen.dart';
@@ -466,16 +467,33 @@ class _MapScreenState extends State<MapScreen> {
),
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.logout, color: Colors.red),
const SizedBox(width: 8),
Text(context.l10n.common_disconnect),
],
if (connector.isConnected)
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.logout, color: Colors.red),
const SizedBox(width: 8),
Text(context.l10n.common_disconnect),
],
),
onTap: () => _disconnect(context, connector),
)
else
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.bluetooth_searching),
const SizedBox(width: 8),
Text(context.l10n.common_connect),
],
),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ScannerScreen(),
),
),
),
onTap: () => _disconnect(context, connector),
),
PopupMenuItem(
child: Row(
children: [
@@ -1,20 +0,0 @@
import 'package:flutter/material.dart';
import '../connector/meshcore_connector.dart';
/// Mixin that automatically navigates back to scanner when disconnected.
/// Use in State classes for screens that require active connection.
mixin DisconnectNavigationMixin<T extends StatefulWidget> on State<T> {
/// Call this in your Widget build method to enable auto-navigation.
/// Returns true if still connected, false if navigation was triggered.
bool checkConnectionAndNavigate(MeshCoreConnector connector) {
if (!connector.isConnected) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
Navigator.popUntil(context, (route) => route.isFirst);
}
});
return false;
}
return true;
}
}