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.'; |
|
I tested the Pull-request yesterday evening and got some problems: it did not load the channels reliably. Sometimes it showed no channels, and since I also tested to switch companion (I have two) he got them mixed up. |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3f4e6f4e13
ℹ️ 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".
| if (!channel.isEmpty && | ||
| _channels.any((c) => c.pskHex != channel.pskHex)) { |
There was a problem hiding this comment.
Restore adding channels during sync
When a normal channel sync starts, getChannels() clears _channels before requesting channel 0, so this new any(...) predicate is false for the first non-empty channel and remains false for every later expected channel because nothing is ever added. In that connected sync path the app completes with an empty channel list and then caches/saves that empty list, so users lose all channels after connecting or refreshing channels.
Useful? React with 👍 / 👎.
| Future<void> loadAllCachedDataForCurrentCompanion() async { | ||
| await loadContactCache(); | ||
| await _loadDiscoveredContactCache(); | ||
| await loadChannelSettings(); | ||
| await loadCachedChannels(); | ||
| await loadAllChannelMessages(); | ||
| await loadUnreadState(); |
There was a problem hiding this comment.
Reload the scoped channel order
When restoreLastCompanionScope() on startup or _setScopedStorePublicKey() after SELF_INFO changes the stores to a companion-specific scope, this helper reloads contacts/channels/messages but not _channelOrder. The only remaining _loadChannelOrder() call runs during initialize() before the scope is set, so saved per-companion channel ordering is ignored until the user reorders channels again.
Useful? React with 👍 / 👎.
|
@wel97459 Could you resolve those issues codex found then we can go ahead and merge this? |
|
Yes, I'll work on it this evening. |
…and update disconnection message in Dutch localization
This lets you see the map, view contact messages, and channel messages from the last connected companion.