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 <noreply@anthropic.com>
pull/209/head
Strycher 3 weeks ago
parent 00ae5558c7
commit 18e49ba4b5

@ -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<String> resolveContactKeysByName(String name) {
final target = name.trim().toLowerCase();
if (target.isEmpty) return const [];
final keys = <String>{};
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<void> deleteChannelMessage(ChannelMessage message) async {
final channelIndex = message.channelIndex;
if (channelIndex == null) return;

@ -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<ChannelChatScreen> {
child: Consumer<MeshCoreConnector>(
builder: (context, connector, child) {
final messages = connector.getChannelMessages(widget.channel);
if (messages.isEmpty) {
final blockService = context.watch<BlockService>();
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<ChannelChatScreen> {
}
// 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<ChannelChatScreen> {
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<AppSettingsService>();
final enableTracing = settingsService.settings.enableMessageTracing;

Loading…
Cancel
Save

Powered by TurnKey Linux.