From 837984cf2a81bc52387cc89e5decf0a60802c531 Mon Sep 17 00:00:00 2001 From: Strycher Date: Mon, 20 Jul 2026 22:56:57 -0400 Subject: [PATCH] feat(#351): reach contact settings from the long-press menu The per-contact settings dialog (compression + telemetry grants) was reachable only from a contact's chat via ellipsis -> Contact settings. Extract it to a shared showContactSettingsDialog() and add a "Contact settings" entry as the first item of the contact long-press/right-click menu, so it opens the same surface from both places. chat_screen's _showContactSettings now delegates to the shared dialog; its _buildInfoRow stays (still used by the contact-info dialog). Co-Authored-By: Claude Opus 4.8 --- lib/screens/chat_screen.dart | 162 +------------------- lib/screens/contacts_screen.dart | 11 ++ lib/widgets/contact_settings_dialog.dart | 180 +++++++++++++++++++++++ 3 files changed, 193 insertions(+), 160 deletions(-) create mode 100644 lib/widgets/contact_settings_dialog.dart diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index c43886b..0e73350 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -32,6 +32,7 @@ import '../services/chat_text_scale_service.dart'; import '../services/path_history_service.dart'; import '../services/translation_service.dart'; import '../widgets/chat_zoom_wrapper.dart'; +import '../widgets/contact_settings_dialog.dart'; import '../widgets/elements_ui.dart'; import '../widgets/mention_autocomplete.dart'; import '../helpers/emoji_shortcodes.dart'; @@ -1349,166 +1350,7 @@ class _ChatScreenState extends State { } void _showContactSettings(BuildContext context) { - final connector = Provider.of(context, listen: false); - final appSettingsService = Provider.of( - context, - listen: false, - ); - connector.ensureContactSmazSettingLoaded(widget.contact.publicKeyHex); - connector.ensureContactCyr2LatSettingLoaded(widget.contact.publicKeyHex); - final contact = widget.contact; - bool smazEnabled = connector.isContactSmazEnabled(contact.publicKeyHex); - bool cyr2latEnabled = connector.isContactCyr2LatEnabled( - contact.publicKeyHex, - ); - String? selectedCyr2LatProfileId = connector.getContactCyr2LatProfileId( - contact.publicKeyHex, - ); - bool teleBaseEnabled = contact.teleBaseEnabled; - bool teleLocEnabled = contact.teleLocEnabled; - bool teleEnvEnabled = contact.teleEnvEnabled; - showDialog( - context: context, - builder: (context) => StatefulBuilder( - builder: (context, setDialogState) => AlertDialog( - title: Text(context.l10n.contact_settings), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (contact.hasLocation) ...[ - _buildInfoRow( - context.l10n.chat_location, - '${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}', - ), - const Divider(height: 8), - ], - SwitchListTile( - contentPadding: EdgeInsets.zero, - title: Text(context.l10n.channels_smazCompression), - subtitle: Text(context.l10n.chat_compressOutgoingMessages), - value: smazEnabled, - onChanged: (value) { - connector.setContactSmazEnabled( - contact.publicKeyHex, - value, - ); - connector.setContactCyr2LatEnabled( - contact.publicKeyHex, - false, - ); - setDialogState(() { - smazEnabled = value; - if (smazEnabled) { - cyr2latEnabled = false; - } - }); - }, - ), - const Divider(height: 8), - SwitchListTile( - contentPadding: EdgeInsets.zero, - title: Text(context.l10n.channels_cyr2latCompression), - subtitle: Text(context.l10n.channels_cyr2latCompressionDscr), - value: cyr2latEnabled, - onChanged: (value) { - connector.setContactCyr2LatEnabled( - contact.publicKeyHex, - value, - ); - connector.setContactSmazEnabled( - contact.publicKeyHex, - false, - ); - setDialogState(() { - cyr2latEnabled = value; - if (cyr2latEnabled) { - smazEnabled = false; - } - }); - }, - ), - if (cyr2latEnabled) ...[ - Padding( - padding: const EdgeInsets.fromLTRB(0, 8, 0, 8), - child: DropdownButtonFormField( - initialValue: selectedCyr2LatProfileId, - decoration: InputDecoration( - labelText: - context.l10n.channels_cyr2latSettingsSubheading, - border: const OutlineInputBorder(), - ), - items: appSettingsService.settings.cyr2latProfiles.map(( - profile, - ) { - return DropdownMenuItem( - value: profile.id, - child: Text(profile.name), - ); - }).toList(), - onChanged: (value) { - connector.setContactCyr2LatProfileId( - contact.publicKeyHex, - value, - ); - setDialogState(() { - selectedCyr2LatProfileId = value; - }); - }, - ), - ), - ], - const Divider(height: 8), - SwitchListTile( - contentPadding: EdgeInsets.zero, - title: Text(context.l10n.contact_teleBase), - subtitle: Text(context.l10n.contact_teleBaseSubtitle), - value: teleBaseEnabled, - onChanged: (value) { - setDialogState(() => teleBaseEnabled = value); - }, - ), - const Divider(height: 8), - SwitchListTile( - contentPadding: EdgeInsets.zero, - title: Text(context.l10n.contact_teleLoc), - subtitle: Text(context.l10n.contact_teleLocSubtitle), - value: teleLocEnabled, - onChanged: (value) { - setDialogState(() => teleLocEnabled = value); - }, - ), - const Divider(height: 8), - SwitchListTile( - contentPadding: EdgeInsets.zero, - title: Text(context.l10n.contact_teleEnv), - subtitle: Text(context.l10n.contact_teleEnvSubtitle), - value: teleEnvEnabled, - onChanged: (value) { - setDialogState(() => teleEnvEnabled = value); - }, - ), - ], - ), - ), - actions: [ - TextButton( - onPressed: () { - connector.setContactFlags( - contact, - teleBase: teleBaseEnabled, - teleLoc: teleLocEnabled, - teleEnv: teleEnvEnabled, - ); - Navigator.pop(context); - }, - child: Text(context.l10n.common_close), - ), - ], - ), - ), - ); + showContactSettingsDialog(context, widget.contact); } Widget _buildInfoRow(String label, String value) { diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index ce32f4c..9ffbae6 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -29,6 +29,7 @@ import '../widgets/empty_state.dart'; import '../widgets/blocked_badge.dart'; import '../widgets/app_shell.dart'; import '../widgets/contact_filter_rail.dart'; +import '../widgets/contact_settings_dialog.dart'; import '../widgets/path_selection_dialog.dart'; import '../widgets/repeater_login_dialog.dart'; import '../widgets/room_login_dialog.dart'; @@ -1252,6 +1253,16 @@ class _ContactsScreenState extends State child: Column( mainAxisSize: MainAxisSize.min, children: [ + // Same destination as the chat ellipsis -> Contact settings (#351). + // First item, so the destructive Block tile stays separated below. + ListTile( + leading: const Icon(Icons.settings_outlined), + title: Text(context.l10n.contact_settings), + onTap: () { + Navigator.pop(sheetContext); + showContactSettingsDialog(context, contact); + }, + ), // Blocking your own node is never meaningful (#250). if (!isSelf) ListTile( diff --git a/lib/widgets/contact_settings_dialog.dart b/lib/widgets/contact_settings_dialog.dart new file mode 100644 index 0000000..d22777e --- /dev/null +++ b/lib/widgets/contact_settings_dialog.dart @@ -0,0 +1,180 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../connector/meshcore_connector.dart'; +import '../l10n/l10n.dart'; +import '../models/contact.dart'; +import '../services/app_settings_service.dart'; + +/// The per-contact settings dialog (Smaz/Cyr2Lat compression + telemetry +/// grants). Shared so it can be opened from the chat ellipsis AND the contacts +/// long-press menu without duplicating the body (#351). +void showContactSettingsDialog(BuildContext context, Contact contact) { + final connector = Provider.of(context, listen: false); + final appSettingsService = Provider.of( + context, + listen: false, + ); + connector.ensureContactSmazSettingLoaded(contact.publicKeyHex); + connector.ensureContactCyr2LatSettingLoaded(contact.publicKeyHex); + bool smazEnabled = connector.isContactSmazEnabled(contact.publicKeyHex); + bool cyr2latEnabled = connector.isContactCyr2LatEnabled(contact.publicKeyHex); + String? selectedCyr2LatProfileId = connector.getContactCyr2LatProfileId( + contact.publicKeyHex, + ); + bool teleBaseEnabled = contact.teleBaseEnabled; + bool teleLocEnabled = contact.teleLocEnabled; + bool teleEnvEnabled = contact.teleEnvEnabled; + showDialog( + context: context, + builder: (context) => StatefulBuilder( + builder: (context, setDialogState) => AlertDialog( + title: Text(context.l10n.contact_settings), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (contact.hasLocation) ...[ + _infoRow( + context.l10n.chat_location, + '${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}', + ), + const Divider(height: 8), + ], + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: Text(context.l10n.channels_smazCompression), + subtitle: Text(context.l10n.chat_compressOutgoingMessages), + value: smazEnabled, + onChanged: (value) { + connector.setContactSmazEnabled(contact.publicKeyHex, value); + connector.setContactCyr2LatEnabled( + contact.publicKeyHex, + false, + ); + setDialogState(() { + smazEnabled = value; + if (smazEnabled) { + cyr2latEnabled = false; + } + }); + }, + ), + const Divider(height: 8), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: Text(context.l10n.channels_cyr2latCompression), + subtitle: Text(context.l10n.channels_cyr2latCompressionDscr), + value: cyr2latEnabled, + onChanged: (value) { + connector.setContactCyr2LatEnabled( + contact.publicKeyHex, + value, + ); + connector.setContactSmazEnabled(contact.publicKeyHex, false); + setDialogState(() { + cyr2latEnabled = value; + if (cyr2latEnabled) { + smazEnabled = false; + } + }); + }, + ), + if (cyr2latEnabled) ...[ + Padding( + padding: const EdgeInsets.fromLTRB(0, 8, 0, 8), + child: DropdownButtonFormField( + initialValue: selectedCyr2LatProfileId, + decoration: InputDecoration( + labelText: + context.l10n.channels_cyr2latSettingsSubheading, + border: const OutlineInputBorder(), + ), + items: appSettingsService.settings.cyr2latProfiles.map(( + profile, + ) { + return DropdownMenuItem( + value: profile.id, + child: Text(profile.name), + ); + }).toList(), + onChanged: (value) { + connector.setContactCyr2LatProfileId( + contact.publicKeyHex, + value, + ); + setDialogState(() { + selectedCyr2LatProfileId = value; + }); + }, + ), + ), + ], + const Divider(height: 8), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: Text(context.l10n.contact_teleBase), + subtitle: Text(context.l10n.contact_teleBaseSubtitle), + value: teleBaseEnabled, + onChanged: (value) { + setDialogState(() => teleBaseEnabled = value); + }, + ), + const Divider(height: 8), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: Text(context.l10n.contact_teleLoc), + subtitle: Text(context.l10n.contact_teleLocSubtitle), + value: teleLocEnabled, + onChanged: (value) { + setDialogState(() => teleLocEnabled = value); + }, + ), + const Divider(height: 8), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: Text(context.l10n.contact_teleEnv), + subtitle: Text(context.l10n.contact_teleEnvSubtitle), + value: teleEnvEnabled, + onChanged: (value) { + setDialogState(() => teleEnvEnabled = value); + }, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + connector.setContactFlags( + contact, + teleBase: teleBaseEnabled, + teleLoc: teleLocEnabled, + teleEnv: teleEnvEnabled, + ); + Navigator.pop(context); + }, + child: Text(context.l10n.common_close), + ), + ], + ), + ), + ); +} + +Widget _infoRow(String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 80, + child: Text(label, style: TextStyle(color: Colors.grey[600])), + ), + Expanded(child: SelectableText(value)), + ], + ), + ); +}