Prevent duplicate channel messages

This commit is contained in:
Janez T
2026-03-13 19:39:03 +01:00
parent 63aa9619e6
commit 249268dd00
2 changed files with 40 additions and 18 deletions

View File

@@ -646,11 +646,18 @@ class MessagesProvider with ChangeNotifier {
if (existing.channelIdx != message.channelIdx) { if (existing.channelIdx != message.channelIdx) {
return false; return false;
} }
final timestampDeltaSeconds = (existing.senderTimestamp -
message.senderTimestamp)
.abs();
final withinChannelRepeatWindow = timestampDeltaSeconds <= 5;
final existingSenderKey = existing.senderKeyShort; final existingSenderKey = existing.senderKeyShort;
final incomingSenderKey = message.senderKeyShort; final incomingSenderKey = message.senderKeyShort;
if (existingSenderKey != null && if (existingSenderKey != null &&
incomingSenderKey != null && incomingSenderKey != null &&
existingSenderKey == incomingSenderKey) { existingSenderKey == incomingSenderKey &&
withinChannelRepeatWindow) {
return true; return true;
} }
@@ -663,23 +670,20 @@ class MessagesProvider with ChangeNotifier {
return true; return true;
} }
// When we send to a channel we add a local "sent" bubble immediately, if (!withinChannelRepeatWindow) {
// then firmware may later sync back the same message as a received return false;
// 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. // 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 && if (existing.isSentMessage &&
existing.echoCount > 0 &&
!message.isSentMessage && !message.isSentMessage &&
(existing.senderTimestamp - message.senderTimestamp).abs() <= 1) { existingSenderName == incomingSenderName) {
return true; return true;
} }
}
if (existing.isSentMessage &&
existing.echoCount > 0 &&
!message.isSentMessage &&
existing.senderTimestamp == message.senderTimestamp) {
return true; return true;
} }

View File

@@ -177,7 +177,7 @@ void main() {
}); });
test( 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(); final provider = MessagesProvider();
provider.resolveContactNameCallback = (_) => 'dz0ny (SI)'; provider.resolveContactNameCallback = (_) => 'dz0ny (SI)';
@@ -185,12 +185,11 @@ void main() {
_buildSentChannelMessage(id: 'c-echo', senderTimestamp: 1700000100), _buildSentChannelMessage(id: 'c-echo', senderTimestamp: 1700000100),
); );
provider.markMessageSent('c-echo', 0, 0); provider.markMessageSent('c-echo', 0, 0);
provider.handleMessageEcho('c-echo', 1, 12, -90);
provider.addMessage( provider.addMessage(
_buildReceivedChannelReplay( _buildReceivedChannelReplay(
id: 'c-echo-incoming', id: 'c-echo-incoming',
senderTimestamp: 1700000101, senderTimestamp: 1700000104,
senderName: 'dz0ny (SI)', senderName: 'dz0ny (SI)',
), ),
); );
@@ -202,7 +201,7 @@ void main() {
); );
test( 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(); final provider = MessagesProvider();
provider.resolveContactNameCallback = (_) => 'dz0ny (SI)'; provider.resolveContactNameCallback = (_) => 'dz0ny (SI)';
@@ -218,7 +217,7 @@ void main() {
_buildReceivedChannelReplay( _buildReceivedChannelReplay(
id: 'c-no-echo-incoming', id: 'c-no-echo-incoming',
senderTimestamp: 1700000201, 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', () { test('missing ACK schedules a delayed retransmission', () {
fakeAsync((async) { fakeAsync((async) {
final provider = MessagesProvider(); final provider = MessagesProvider();