offline mode#440
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces an “offline mode” experience by keeping the app usable when the companion connection is unavailable, and by restoring cached data scoped to the last connected companion.
Changes:
- Remove auto-navigation-to-scanner-on-disconnect and instead allow browsing offline data in Contacts/Channels/Map.
- Add a “Connect” action in overflow menus (when disconnected) and hide/guard message composers when offline.
- Persist/restore last companion scope and preload cached companion-scoped data on startup; add new localization for the offline/companion-required dialog.
Reviewed changes
Copilot reviewed 46 out of 46 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/utils/disconnect_navigation_mixin.dart | Removed mixin that forced navigation away on disconnect to enable offline browsing. |
| lib/screens/map_screen.dart | Adds Connect/Disconnect menu behavior based on connection state. |
| lib/screens/contacts_screen.dart | Removes forced disconnect navigation; adds Connect menu entry; blocks repeater/room logins while offline with a dialog. |
| lib/screens/chat_screen.dart | Hides input bar and prevents sending when disconnected. |
| lib/screens/channels_screen.dart | Removes forced disconnect navigation; adds Connect menu entry. |
| lib/screens/channel_chat_screen.dart | Hides composer and prevents sending when disconnected. |
| lib/models/path_history.dart | Adds byteCount field to PathRecord. |
| lib/main.dart | Initializes PrefsManager; restores last companion scope and loads cached data at startup; changes home screen to Contacts. |
| lib/connector/meshcore_connector.dart | Adds last-companion scoping persistence/restore and cached-data loading helpers; tweaks cached channel loading and lazy DM loading. |
| lib/l10n/app_*.arb | Adds contact_connectCompanion translation strings across locales. |
| lib/l10n/app_localizations*.dart | Adds generated localization getter for contact_connectCompanion. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8ec82f87fe
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- 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.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
- Introduced new dialog messages for connecting to a companion and handling disconnection across multiple languages. - Updated localization files for French, Hungarian, Italian, Japanese, Korean, Bulgarian, German, English, Spanish, Dutch, Polish, Portuguese, Russian, Slovak, Slovenian, Swedish, Ukrainian, and Chinese. - Modified the contacts and map screens to utilize the new dialog messages. - Enhanced the disconnect confirmation dialog to show a message upon successful disconnection. - Updated app bar to conditionally display radio stats based on companion connection status.
| PopupMenuItem( | ||
| child: Row( | ||
| children: [ | ||
| const Icon(Icons.connect_without_contact), | ||
| const SizedBox(width: 8), | ||
| Text(context.l10n.contacts_zeroHopAdvert), | ||
| ], | ||
| ), | ||
| onTap: () => { | ||
| connector.sendSelfAdvert(flood: false), | ||
| showDismissibleSnackBar( | ||
| context, | ||
| content: Text(context.l10n.settings_advertisementSent), | ||
| ), | ||
| }, | ||
| ), | ||
| onTap: () => { | ||
| connector.sendSelfAdvert(flood: false), | ||
| 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.cell_tower), | ||
| const SizedBox(width: 8), | ||
| Text(context.l10n.contacts_floodAdvert), | ||
| ], | ||
| onTap: () => { | ||
| connector.sendSelfAdvert(flood: true), | ||
| showDismissibleSnackBar( | ||
| context, | ||
| content: Text(context.l10n.settings_advertisementSent), | ||
| ), | ||
| }, |
| "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.", |
| "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." |
| 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, | ||
| ); |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
| // Only add non-empty channels | ||
| if (!channel.isEmpty) { | ||
| if (!channel.isEmpty && | ||
| _channels.any((c) => c.pskHex != channel.pskHex)) { | ||
| _channels.add(channel); |
| if (lastCompanionPublicKeyHex == null || | ||
| lastCompanionPublicKeyHex.trim().isEmpty) { | ||
| return; | ||
| } | ||
| _selfPublicKey = hexToPubKey(lastCompanionPublicKeyHex); | ||
| _setScopedStorePublicKey(lastCompanionPublicKeyHex); |
| "repeater_chanUtil": "频道利用率", | ||
| "dialog_connectCompanion": "连接伴机以访问中继器和房间服务器功能。", | ||
| "dialog_disconnectedTitle": "已断开连接", | ||
| "dialog_disconnectedMessage": "你已与你的伙伴断开连接。", | ||
| "contact_connectCompanion": "连接至伴侣设备以访问中继器和房间服务器功能。" |
|
|
||
| @override | ||
| String get dialog_disconnectedMessage => | ||
| 'Je bent verbonden met je companion.'; |
| "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." |
| "dialog_connectCompanion": "Csatlakozzon egy kísérőhöz a ismétlő és szobaszerver funkciók eléréséhez.", | ||
| "dialog_disconnectedTitle": "Lejárat", | ||
| "dialog_disconnectedMessage": "Lehentetőtől megszakadtál.", | ||
| "contact_connectCompanion": "Csatlakozzon egy társhoz az ismpa- és szobaszerver funkciók eléréséhez." |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
| // Only add non-empty channels | ||
| if (!channel.isEmpty) { | ||
| if (!channel.isEmpty && | ||
| _channels.any((c) => c.pskHex != channel.pskHex)) { |
| final lastCompanionPublicKeyHex = prefs.getString( | ||
| _lastCompanionPublicKeyPref, | ||
| ); | ||
| if (lastCompanionPublicKeyHex == null || | ||
| lastCompanionPublicKeyHex.trim().isEmpty) { | ||
| return; | ||
| } | ||
| _selfPublicKey = hexToPubKey(lastCompanionPublicKeyHex); | ||
| _setScopedStorePublicKey(lastCompanionPublicKeyHex); | ||
| } | ||
|
|
| "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.", |
|
|
||
| @override | ||
| String get dialog_disconnectedMessage => | ||
| 'Je bent verbonden met je companion.'; |
| "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." |
|
|
||
| @override | ||
| String get dialog_disconnectedMessage => | ||
| 'Prekinjena povezava s vašim spre伴ovalcem.'; |
This lets you see the map, view contact messages, and channel messages from the last connected companion.