From b1757c6539cd22829d27da359b8af1bd666d19b8 Mon Sep 17 00:00:00 2001 From: Strycher Date: Sat, 11 Jul 2026 21:40:09 -0400 Subject: [PATCH] feat(#172): block/unblock in contacts, DM, and discovery menus Completes the A5 entry points: contact long-press sheet, DM app-bar overflow, and discovery item sheet each get a Block/Unblock toggle (by pubkey, reflecting current state). Pairs with the channel 'Block sender' from 8a2e764. Epic A #165 / A5 #172. Design: docs/architecture/block-contract-as-built.md. Co-Authored-By: Claude Opus 4.8 --- lib/screens/chat_screen.dart | 118 +++++++++++++++++++----------- lib/screens/contacts_screen.dart | 21 ++++++ lib/screens/discovery_screen.dart | 18 +++++ 3 files changed, 116 insertions(+), 41 deletions(-) diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 9b9a477..f90933c 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -27,6 +27,7 @@ import '../models/app_settings.dart'; import '../models/path_history.dart'; import '../models/translation_support.dart'; import '../services/app_settings_service.dart'; +import '../services/block_service.dart'; import '../services/chat_text_scale_service.dart'; import '../services/path_history_service.dart'; import '../services/translation_service.dart'; @@ -353,52 +354,87 @@ class _ChatScreenState extends State { if (value == 'clearChat') { connector.clearMessagesForContact(widget.contact); } + if (value == 'block') { + context.read().block( + widget.contact.publicKeyHex, + ); + } + if (value == 'unblock') { + context.read().unblock( + widget.contact.publicKeyHex, + ); + } }, - itemBuilder: (context) => [ - PopupMenuItem( - value: 'info', - child: Row( - children: [ - const Icon(Icons.info_outline, size: 20), - const SizedBox(width: 12), - Text(context.l10n.contact_info), - ], + itemBuilder: (context) { + final isBlocked = context.read().isBlocked( + widget.contact.publicKeyHex, + ); + return [ + PopupMenuItem( + value: 'info', + child: Row( + children: [ + const Icon(Icons.info_outline, size: 20), + const SizedBox(width: 12), + Text(context.l10n.contact_info), + ], + ), ), - ), - PopupMenuItem( - value: 'telemetry', - child: Row( - children: [ - const Icon(Icons.bar_chart, size: 20), - const SizedBox(width: 12), - Text(context.l10n.contact_telemetry), - ], + PopupMenuItem( + value: 'telemetry', + child: Row( + children: [ + const Icon(Icons.bar_chart, size: 20), + const SizedBox(width: 12), + Text(context.l10n.contact_telemetry), + ], + ), ), - ), - PopupMenuItem( - value: 'settings', - child: Row( - children: [ - const Icon(Icons.settings, size: 20), - const SizedBox(width: 12), - Text(context.l10n.contact_settings), - ], + PopupMenuItem( + value: 'settings', + child: Row( + children: [ + const Icon(Icons.settings, size: 20), + const SizedBox(width: 12), + Text(context.l10n.contact_settings), + ], + ), ), - ), - PopupMenuItem( - value: 'clearChat', - child: Row( - children: [ - const Icon(Icons.delete, size: 20, color: Colors.red), - const SizedBox(width: 12), - Text( - context.l10n.contact_clearChat, - style: const TextStyle(color: Colors.red), - ), - ], + PopupMenuItem( + value: 'clearChat', + child: Row( + children: [ + const Icon(Icons.delete, size: 20, color: Colors.red), + const SizedBox(width: 12), + Text( + context.l10n.contact_clearChat, + style: const TextStyle(color: Colors.red), + ), + ], + ), ), - ), - ], + PopupMenuItem( + value: isBlocked ? 'unblock' : 'block', + child: Row( + children: [ + Icon( + isBlocked + ? Icons.check_circle_outline + : Icons.block, + size: 20, + color: isBlocked ? null : Colors.red.shade700, + ), + const SizedBox(width: 12), + Text( + isBlocked + ? context.l10n.block_unblock + : context.l10n.block_block, + ), + ], + ), + ), + ]; + }, ); }, ), diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 5deaaaa..2b39a4e 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -1259,6 +1259,8 @@ class _ContactsScreenState extends State final isRepeater = contact.type == advTypeRepeater; final isRoom = contact.type == advTypeRoom; final isFavorite = contact.isFavorite; + final blockService = context.read(); + final isBlocked = blockService.isBlocked(contact.publicKeyHex); showModalBottomSheet( context: context, @@ -1266,6 +1268,25 @@ class _ContactsScreenState extends State child: Column( mainAxisSize: MainAxisSize.min, children: [ + ListTile( + leading: Icon( + isBlocked ? Icons.check_circle_outline : Icons.block, + color: isBlocked ? null : Colors.red.shade700, + ), + title: Text( + isBlocked + ? context.l10n.block_unblock + : context.l10n.block_block, + ), + onTap: () async { + Navigator.pop(sheetContext); + if (isBlocked) { + await blockService.unblock(contact.publicKeyHex); + } else { + await blockService.block(contact.publicKeyHex); + } + }, + ), if (isRepeater) ...[ ListTile( leading: const Icon(Icons.radar, color: Colors.green), diff --git a/lib/screens/discovery_screen.dart b/lib/screens/discovery_screen.dart index 4663683..bb49969 100644 --- a/lib/screens/discovery_screen.dart +++ b/lib/screens/discovery_screen.dart @@ -218,6 +218,8 @@ class _DiscoveryScreenState extends State { Contact contact, MeshCoreConnector connector, ) async { + final blockService = context.read(); + final isBlocked = blockService.isBlocked(contact.publicKeyHex); final action = await showModalBottomSheet( context: context, showDragHandle: true, @@ -237,6 +239,16 @@ class _DiscoveryScreenState extends State { title: Text(l10n.discoveredContacts_copyContact), onTap: () => Navigator.of(sheetContext).pop('copy_contact'), ), + ListTile( + leading: Icon( + isBlocked ? Icons.check_circle_outline : Icons.block, + color: isBlocked ? null : Colors.red.shade700, + ), + title: Text(isBlocked ? l10n.block_unblock : l10n.block_block), + onTap: () => Navigator.of( + sheetContext, + ).pop(isBlocked ? 'unblock' : 'block'), + ), ListTile( leading: const Icon(Icons.delete), title: Text(l10n.discoveredContacts_deleteContact), @@ -267,6 +279,12 @@ class _DiscoveryScreenState extends State { case 'delete_contact': connector.removeDiscoveredContact(contact); break; + case 'block': + await blockService.block(contact.publicKeyHex); + break; + case 'unblock': + await blockService.unblock(contact.publicKeyHex); + break; } }