From 18e49ba4b558557222f8a8c43cd6f6eeff92aa58 Mon Sep 17 00:00:00 2001 From: Strycher Date: Thu, 2 Jul 2026 14:54:33 -0400 Subject: [PATCH] feat(#170): channel name-key resolution hides blocked senders - Connector: resolveContactKeysByName(name) maps an anonymous channel sender (name-only) to matching contact/discovered pubkeys (self-heals via adverts keeping contact names current). - channel_chat_screen: hide a post only when the name resolves and EVERY matching pubkey is blocked (ambiguous namesake with any unblocked match still shown), or the name is in blockedNames. View-layer filter only. Epic A #165 / A3 #170. Design: docs/architecture/block-contract-as-built.md. Co-Authored-By: Claude Opus 4.8 --- lib/connector/meshcore_connector.dart | 17 ++++++++++++++++ lib/screens/channel_chat_screen.dart | 29 ++++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 20baa07..e3c8938 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -695,6 +695,23 @@ class MeshCoreConnector extends ChangeNotifier { return _channelMessages[channel.index] ?? []; } + /// Public keys (hex) of known contacts / discovered nodes whose current + /// display name matches [name] (case-insensitive). Resolves an anonymous + /// channel sender (name-only, no pubkey) back to identities for block + /// matching. Multiple keys => several devices/people share the name. + List resolveContactKeysByName(String name) { + final target = name.trim().toLowerCase(); + if (target.isEmpty) return const []; + final keys = {}; + for (final c in contacts) { + if (c.name.trim().toLowerCase() == target) keys.add(c.publicKeyHex); + } + for (final c in discoveredContacts) { + if (c.name.trim().toLowerCase() == target) keys.add(c.publicKeyHex); + } + return keys.toList(); + } + Future deleteChannelMessage(ChannelMessage message) async { final channelIndex = message.channelIndex; if (channelIndex == null) return; diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 755a106..f790211 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -24,6 +24,7 @@ import '../models/channel_message.dart'; import '../models/translation_support.dart'; import '../models/app_settings.dart'; import '../services/app_settings_service.dart'; +import '../services/block_service.dart'; import '../services/chat_text_scale_service.dart'; import '../services/translation_service.dart'; import '../utils/emoji_utils.dart'; @@ -348,8 +349,15 @@ class _ChannelChatScreenState extends State { child: Consumer( builder: (context, connector, child) { final messages = connector.getChannelMessages(widget.channel); - - if (messages.isEmpty) { + final blockService = context.watch(); + final visibleMessages = messages + .where( + (m) => + !_isChannelSenderHidden(m, connector, blockService), + ) + .toList(); + + if (visibleMessages.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -383,7 +391,7 @@ class _ChannelChatScreenState extends State { } // Reverse messages so newest appear at bottom with reverse: true - final reversedMessages = messages.reversed.toList(); + final reversedMessages = visibleMessages.reversed.toList(); final itemCount = reversedMessages.length + (_isLoadingOlder ? 1 : 0); @@ -496,6 +504,21 @@ class _ChannelChatScreenState extends State { connector.setChannelUnreadCount(widget.channel.index, count); } + /// A channel post is hidden only when its claimed name resolves to at least + /// one identity and *every* matching pubkey is blocked (an ambiguous namesake + /// with any unblocked match is still shown), or the name is in blockedNames. + bool _isChannelSenderHidden( + ChannelMessage message, + MeshCoreConnector connector, + BlockService blockService, + ) { + if (message.isOutgoing) return false; + final name = message.senderName; + if (blockService.isNameBlocked(name)) return true; + final keys = connector.resolveContactKeysByName(name); + return keys.isNotEmpty && keys.every(blockService.isBlocked); + } + Widget _buildMessageBubble(ChannelMessage message, double textScale) { final settingsService = context.watch(); final enableTracing = settingsService.settings.enableMessageTracing;