From 249268dd00f90d23c791251076574ab532f599de Mon Sep 17 00:00:00 2001 From: Janez T Date: Fri, 13 Mar 2026 19:39:03 +0100 Subject: [PATCH] Prevent duplicate channel messages --- lib/providers/messages_provider.dart | 30 +++++++++++-------- ...messages_provider_retransmission_test.dart | 28 +++++++++++++---- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/lib/providers/messages_provider.dart b/lib/providers/messages_provider.dart index 2056810..cec1852 100644 --- a/lib/providers/messages_provider.dart +++ b/lib/providers/messages_provider.dart @@ -646,11 +646,18 @@ class MessagesProvider with ChangeNotifier { if (existing.channelIdx != message.channelIdx) { return false; } + + final timestampDeltaSeconds = (existing.senderTimestamp - + message.senderTimestamp) + .abs(); + final withinChannelRepeatWindow = timestampDeltaSeconds <= 5; + final existingSenderKey = existing.senderKeyShort; final incomingSenderKey = message.senderKeyShort; if (existingSenderKey != null && incomingSenderKey != null && - existingSenderKey == incomingSenderKey) { + existingSenderKey == incomingSenderKey && + withinChannelRepeatWindow) { return true; } @@ -663,23 +670,20 @@ class MessagesProvider with ChangeNotifier { return true; } - // When we send to a channel we add a local "sent" bubble immediately, - // then firmware may later sync back the same message as a received - // channel item under our public handle. Only fold that replay into the - // original bubble after LOG_RX_DATA has already confirmed it as our - // transmitted packet. + if (!withinChannelRepeatWindow) { + return false; + } + + // Mirror meshcore-open behavior for self-authored channel messages: + // zero-hop self replays are dropped earlier in AppProvider, while + // routed self replays should fold into the local sent bubble when the + // public sender name matches within a short window. if (existing.isSentMessage && - existing.echoCount > 0 && !message.isSentMessage && - (existing.senderTimestamp - message.senderTimestamp).abs() <= 1) { + existingSenderName == incomingSenderName) { return true; } - } - if (existing.isSentMessage && - existing.echoCount > 0 && - !message.isSentMessage && - existing.senderTimestamp == message.senderTimestamp) { return true; } diff --git a/test/providers/messages_provider_retransmission_test.dart b/test/providers/messages_provider_retransmission_test.dart index f73dd34..5897038 100644 --- a/test/providers/messages_provider_retransmission_test.dart +++ b/test/providers/messages_provider_retransmission_test.dart @@ -177,7 +177,7 @@ void main() { }); test( - 'channel replay is deduped after raw echo detection confirms our send', + 'channel replay is deduped for self sender within repeat window', () { final provider = MessagesProvider(); provider.resolveContactNameCallback = (_) => 'dz0ny (SI)'; @@ -185,12 +185,11 @@ void main() { _buildSentChannelMessage(id: 'c-echo', senderTimestamp: 1700000100), ); provider.markMessageSent('c-echo', 0, 0); - provider.handleMessageEcho('c-echo', 1, 12, -90); provider.addMessage( _buildReceivedChannelReplay( id: 'c-echo-incoming', - senderTimestamp: 1700000101, + senderTimestamp: 1700000104, senderName: 'dz0ny (SI)', ), ); @@ -202,7 +201,7 @@ void main() { ); test( - 'channel replay is not deduped before raw echo detection confirms it', + 'channel replay is not deduped for different sender with same text', () { final provider = MessagesProvider(); provider.resolveContactNameCallback = (_) => 'dz0ny (SI)'; @@ -218,7 +217,7 @@ void main() { _buildReceivedChannelReplay( id: 'c-no-echo-incoming', senderTimestamp: 1700000201, - senderName: 'dz0ny (SI)', + senderName: 'Radio Alpha', ), ); @@ -226,6 +225,25 @@ void main() { }, ); + test('channel replay is not deduped outside repeat window', () { + final provider = MessagesProvider(); + provider.resolveContactNameCallback = (_) => 'dz0ny (SI)'; + provider.addSentMessage( + _buildSentChannelMessage(id: 'c-late', senderTimestamp: 1700000300), + ); + provider.markMessageSent('c-late', 0, 0); + + provider.addMessage( + _buildReceivedChannelReplay( + id: 'c-late-incoming', + senderTimestamp: 1700000306, + senderName: 'dz0ny (SI)', + ), + ); + + expect(provider.messages, hasLength(2)); + }); + test('missing ACK schedules a delayed retransmission', () { fakeAsync((async) { final provider = MessagesProvider();