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 209fee48ca
commit d6ed8c5f13
45 changed files with 320 additions and 113 deletions
+6 -1
View File
@@ -1123,6 +1123,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(
@@ -1274,6 +1277,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;
@@ -1288,7 +1294,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 -17
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 {
@@ -40,8 +40,7 @@ class ChannelsScreen extends StatefulWidget {
State<ChannelsScreen> createState() => _ChannelsScreenState();
}
class _ChannelsScreenState extends State<ChannelsScreen>
with DisconnectNavigationMixin {
class _ChannelsScreenState extends State<ChannelsScreen> {
final TextEditingController _searchController = TextEditingController();
final CommunityStore _communityStore = CommunityStore();
final CommunityPskIndex _communityIndex = CommunityPskIndex();
@@ -89,11 +88,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(
@@ -106,16 +100,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 -17
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 }
@@ -49,8 +49,7 @@ class ContactsScreen extends StatefulWidget {
State<ContactsScreen> createState() => _ContactsScreenState();
}
class _ContactsScreenState extends State<ContactsScreen>
with DisconnectNavigationMixin {
class _ContactsScreenState extends State<ContactsScreen> {
final TextEditingController _searchController = TextEditingController();
final ContactGroupStore _groupStore = ContactGroupStore();
MeshCoreConnector? _scopeSyncConnector;
@@ -306,11 +305,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 +372,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: [
@@ -967,6 +978,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(
@@ -993,6 +1009,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(
@@ -1021,6 +1042,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: [