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 <noreply@anthropic.com>
pull/209/head
Strycher 1 week ago
parent 41113cddde
commit b1757c6539

@ -27,6 +27,7 @@ import '../models/app_settings.dart';
import '../models/path_history.dart'; import '../models/path_history.dart';
import '../models/translation_support.dart'; import '../models/translation_support.dart';
import '../services/app_settings_service.dart'; import '../services/app_settings_service.dart';
import '../services/block_service.dart';
import '../services/chat_text_scale_service.dart'; import '../services/chat_text_scale_service.dart';
import '../services/path_history_service.dart'; import '../services/path_history_service.dart';
import '../services/translation_service.dart'; import '../services/translation_service.dart';
@ -353,52 +354,87 @@ class _ChatScreenState extends State<ChatScreen> {
if (value == 'clearChat') { if (value == 'clearChat') {
connector.clearMessagesForContact(widget.contact); connector.clearMessagesForContact(widget.contact);
} }
if (value == 'block') {
context.read<BlockService>().block(
widget.contact.publicKeyHex,
);
}
if (value == 'unblock') {
context.read<BlockService>().unblock(
widget.contact.publicKeyHex,
);
}
}, },
itemBuilder: (context) => [ itemBuilder: (context) {
PopupMenuItem( final isBlocked = context.read<BlockService>().isBlocked(
value: 'info', widget.contact.publicKeyHex,
child: Row( );
children: [ return [
const Icon(Icons.info_outline, size: 20), PopupMenuItem(
const SizedBox(width: 12), value: 'info',
Text(context.l10n.contact_info), child: Row(
], children: [
const Icon(Icons.info_outline, size: 20),
const SizedBox(width: 12),
Text(context.l10n.contact_info),
],
),
), ),
), PopupMenuItem(
PopupMenuItem( value: 'telemetry',
value: 'telemetry', child: Row(
child: Row( children: [
children: [ const Icon(Icons.bar_chart, size: 20),
const Icon(Icons.bar_chart, size: 20), const SizedBox(width: 12),
const SizedBox(width: 12), Text(context.l10n.contact_telemetry),
Text(context.l10n.contact_telemetry), ],
], ),
), ),
), PopupMenuItem(
PopupMenuItem( value: 'settings',
value: 'settings', child: Row(
child: Row( children: [
children: [ const Icon(Icons.settings, size: 20),
const Icon(Icons.settings, size: 20), const SizedBox(width: 12),
const SizedBox(width: 12), Text(context.l10n.contact_settings),
Text(context.l10n.contact_settings), ],
], ),
), ),
), PopupMenuItem(
PopupMenuItem( value: 'clearChat',
value: 'clearChat', child: Row(
child: Row( children: [
children: [ const Icon(Icons.delete, size: 20, color: Colors.red),
const Icon(Icons.delete, size: 20, color: Colors.red), const SizedBox(width: 12),
const SizedBox(width: 12), Text(
Text( context.l10n.contact_clearChat,
context.l10n.contact_clearChat, style: const TextStyle(color: Colors.red),
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,
),
],
),
),
];
},
); );
}, },
), ),

@ -1259,6 +1259,8 @@ class _ContactsScreenState extends State<ContactsScreen>
final isRepeater = contact.type == advTypeRepeater; final isRepeater = contact.type == advTypeRepeater;
final isRoom = contact.type == advTypeRoom; final isRoom = contact.type == advTypeRoom;
final isFavorite = contact.isFavorite; final isFavorite = contact.isFavorite;
final blockService = context.read<BlockService>();
final isBlocked = blockService.isBlocked(contact.publicKeyHex);
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
@ -1266,6 +1268,25 @@ class _ContactsScreenState extends State<ContactsScreen>
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ 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) ...[ if (isRepeater) ...[
ListTile( ListTile(
leading: const Icon(Icons.radar, color: Colors.green), leading: const Icon(Icons.radar, color: Colors.green),

@ -218,6 +218,8 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
Contact contact, Contact contact,
MeshCoreConnector connector, MeshCoreConnector connector,
) async { ) async {
final blockService = context.read<BlockService>();
final isBlocked = blockService.isBlocked(contact.publicKeyHex);
final action = await showModalBottomSheet<String>( final action = await showModalBottomSheet<String>(
context: context, context: context,
showDragHandle: true, showDragHandle: true,
@ -237,6 +239,16 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
title: Text(l10n.discoveredContacts_copyContact), title: Text(l10n.discoveredContacts_copyContact),
onTap: () => Navigator.of(sheetContext).pop('copy_contact'), 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( ListTile(
leading: const Icon(Icons.delete), leading: const Icon(Icons.delete),
title: Text(l10n.discoveredContacts_deleteContact), title: Text(l10n.discoveredContacts_deleteContact),
@ -267,6 +279,12 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
case 'delete_contact': case 'delete_contact':
connector.removeDiscoveredContact(contact); connector.removeDiscoveredContact(contact);
break; break;
case 'block':
await blockService.block(contact.publicKeyHex);
break;
case 'unblock':
await blockService.unblock(contact.publicKeyHex);
break;
} }
} }

Loading…
Cancel
Save

Powered by TurnKey Linux.