diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart index dfc381a..2be800b 100644 --- a/lib/providers/app_provider.dart +++ b/lib/providers/app_provider.dart @@ -293,9 +293,10 @@ class AppProvider with ChangeNotifier { if (publicKey == null || publicKey.isEmpty) return null; final ownPublicKey = connectionProvider.deviceInfo.publicKey; - final ownName = - connectionProvider.deviceInfo.deviceName ?? - connectionProvider.deviceInfo.selfName; + final ownName = _preferredSelfDisplayName( + deviceName: connectionProvider.deviceInfo.deviceName, + selfName: connectionProvider.deviceInfo.selfName, + ); if (_matchesPublicKeyPrefix(publicKey, ownPublicKey) && ownName != null && ownName.trim().isNotEmpty) { @@ -340,6 +341,35 @@ class AppProvider with ChangeNotifier { return trimmedSenderName == trimmedOwnName; } + static String? preferredSelfDisplayName({ + required String? deviceName, + required String? selfName, + }) => _preferredSelfDisplayName(deviceName: deviceName, selfName: selfName); + + static String? _preferredSelfDisplayName({ + required String? deviceName, + required String? selfName, + }) { + final trimmedSelfName = selfName?.trim(); + if (trimmedSelfName != null && trimmedSelfName.isNotEmpty) { + return trimmedSelfName; + } + + final trimmedDeviceName = deviceName?.trim(); + if (trimmedDeviceName == null || trimmedDeviceName.isEmpty) { + return null; + } + + if (trimmedDeviceName.toLowerCase().startsWith('meshcore-')) { + final stripped = trimmedDeviceName.substring('meshcore-'.length).trim(); + if (stripped.isNotEmpty) { + return stripped; + } + } + + return trimmedDeviceName; + } + static bool _matchesPublicKeyPrefix( Uint8List? candidateKey, Uint8List? ownPublicKey, @@ -820,10 +850,10 @@ class AppProvider with ChangeNotifier { final isDeletedChannel = connectionProvider .shouldTreatChannelInfoAsDeleted( - channelIdx, - channelName, - secret, - ); + channelIdx, + channelName, + secret, + ); final isEmptyChannelInfo = AppProvider.isDeletedChannelInfo( channelIdx, channelName, @@ -930,6 +960,18 @@ class AppProvider with ChangeNotifier { // When a message is received connectionProvider.onMessageReceived = (message) { + if (message.isChannelMessage) { + debugPrint( + '๐Ÿ“ฅ [AppProvider] Channel message received: ' + 'id=${message.id}, ' + 'senderName=${message.senderName ?? "-"}, ' + 'senderKey=${message.senderKeyShort ?? "-"}, ' + 'pathLen=${message.pathLen}, ' + 'ts=${message.senderTimestamp}, ' + 'text=${message.text}', + ); + } + if (AppProvider.shouldIgnoreSelfReplay( message: message, ownPublicKey: connectionProvider.deviceInfo.publicKey, diff --git a/lib/providers/messages_provider.dart b/lib/providers/messages_provider.dart index d56a6c4..c65495e 100644 --- a/lib/providers/messages_provider.dart +++ b/lib/providers/messages_provider.dart @@ -627,6 +627,13 @@ class MessagesProvider with ChangeNotifier { existing.text != message.text) { continue; } + if (message.isChannelMessage) { + debugPrint( + '๐Ÿ” [MessagesProvider] Channel duplicate match: ' + 'incoming(id=${message.id}, senderName=${message.senderName ?? "-"}, senderKey=${message.senderKeyShort ?? "-"}, pathLen=${message.pathLen}, ts=${message.senderTimestamp}) ' + 'existing(id=${existing.id}, sent=${existing.isSentMessage}, senderName=${existing.senderName ?? "-"}, senderKey=${existing.senderKeyShort ?? "-"}, pathLen=${existing.pathLen}, ts=${existing.senderTimestamp})', + ); + } return index; } @@ -647,9 +654,8 @@ class MessagesProvider with ChangeNotifier { return false; } - final timestampDeltaSeconds = - (existing.senderTimestamp - message.senderTimestamp).abs(); - final withinChannelRepeatWindow = timestampDeltaSeconds <= 5; + final withinChannelRepeatWindow = + (existing.senderTimestamp - message.senderTimestamp).abs() <= 30; final existingSenderKey = existing.senderKeyShort; final incomingSenderKey = message.senderKeyShort; @@ -660,8 +666,16 @@ class MessagesProvider with ChangeNotifier { return true; } - final existingSenderName = _normalizeSenderName(existing.senderName); - final incomingSenderName = _normalizeSenderName(message.senderName); + final existingSenderName = _normalizedResolvedSenderName(existing); + final incomingSenderName = _normalizedResolvedSenderName(message); + if (existing.isChannelMessage && message.isChannelMessage) { + debugPrint( + '๐Ÿงช [MessagesProvider] Channel dedupe check: ' + 'existing(id=${existing.id}, sent=${existing.isSentMessage}, senderName=${existing.senderName ?? "-"}, resolvedSender=${existingSenderName ?? "-"}, senderKey=${existing.senderKeyShort ?? "-"}, ts=${existing.senderTimestamp}, pathLen=${existing.pathLen}) ' + 'incoming(id=${message.id}, sent=${message.isSentMessage}, senderName=${message.senderName ?? "-"}, resolvedSender=${incomingSenderName ?? "-"}, senderKey=${message.senderKeyShort ?? "-"}, ts=${message.senderTimestamp}, pathLen=${message.pathLen}) ' + 'withinWindow=$withinChannelRepeatWindow', + ); + } if (existingSenderName != null && incomingSenderName != null && existingSenderName == incomingSenderName) { @@ -733,7 +747,13 @@ class MessagesProvider with ChangeNotifier { if (trimmed == null || trimmed.isEmpty) { return null; } - return trimmed.toLowerCase(); + + var normalized = trimmed.toLowerCase(); + if (normalized.startsWith('meshcore-')) { + normalized = normalized.substring('meshcore-'.length).trim(); + } + + return normalized.isEmpty ? null : normalized; } Message _resolveSenderNameIfNeeded(Message message) { @@ -751,6 +771,22 @@ class MessagesProvider with ChangeNotifier { return message.copyWith(senderName: resolvedSenderName.trim()); } + String? _normalizedResolvedSenderName(Message message) { + final directName = _normalizeSenderName(message.senderName); + if (directName != null) { + return directName; + } + + if (message.senderPublicKeyPrefix == null) { + return null; + } + + final resolved = resolveContactNameCallback?.call( + message.senderPublicKeyPrefix, + ); + return _normalizeSenderName(resolved); + } + /// Trigger urgent notification for SAR marker Future _triggerSarNotification( Message message, diff --git a/test/providers/app_provider_channel_info_test.dart b/test/providers/app_provider_channel_info_test.dart index cbeadd3..f9377c6 100644 --- a/test/providers/app_provider_channel_info_test.dart +++ b/test/providers/app_provider_channel_info_test.dart @@ -22,6 +22,16 @@ void main() { }); group('AppProvider self replay handling', () { + test('prefers self name over meshcore device name', () { + expect( + AppProvider.preferredSelfDisplayName( + deviceName: 'MeshCore-dz0ny (SI)', + selfName: 'dz0ny (SI)', + ), + 'dz0ny (SI)', + ); + }); + test('ignores direct self replay without hops', () { final message = Message( id: 'dm-self', diff --git a/test/providers/messages_provider_retransmission_test.dart b/test/providers/messages_provider_retransmission_test.dart index 01197e9..d7388ad 100644 --- a/test/providers/messages_provider_retransmission_test.dart +++ b/test/providers/messages_provider_retransmission_test.dart @@ -233,7 +233,7 @@ void main() { provider.addMessage( _buildReceivedChannelReplay( id: 'c-late-incoming', - senderTimestamp: 1700000306, + senderTimestamp: 1700000331, senderName: 'dz0ny (SI)', ), ); @@ -241,6 +241,46 @@ void main() { expect(provider.messages, hasLength(2)); }); + test('channel replay can dedupe using lazily resolved self name', () { + final provider = MessagesProvider(); + provider.addSentMessage( + _buildSentChannelMessage(id: 'c-lazy', senderTimestamp: 1700000400), + ); + provider.markMessageSent('c-lazy', 0, 0); + provider.resolveContactNameCallback = (_) => 'dz0ny (SI)'; + + provider.addMessage( + _buildReceivedChannelReplay( + id: 'c-lazy-incoming', + senderTimestamp: 1700000406, + senderName: 'dz0ny (SI)', + ), + ); + + expect(provider.messages, hasLength(1)); + expect(provider.messages.single.id, equals('c-lazy')); + }); + + test('channel replay dedupes meshcore-prefixed self sender name', () { + final provider = MessagesProvider(); + provider.resolveContactNameCallback = (_) => 'MeshCore-dz0ny (SI)'; + provider.addSentMessage( + _buildSentChannelMessage(id: 'c-prefix', senderTimestamp: 1700000500), + ); + provider.markMessageSent('c-prefix', 0, 0); + + provider.addMessage( + _buildReceivedChannelReplay( + id: 'c-prefix-incoming', + senderTimestamp: 1700000500, + senderName: 'dz0ny (SI)', + ), + ); + + expect(provider.messages, hasLength(1)); + expect(provider.messages.single.id, equals('c-prefix')); + }); + test('missing ACK schedules a delayed retransmission', () { fakeAsync((async) { final provider = MessagesProvider();