diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index f27e66e..500be12 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -489,7 +489,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 126; DEVELOPMENT_TEAM = JND55328G8; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -511,7 +511,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 126; DEVELOPMENT_TEAM = JND55328G8; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; @@ -530,7 +530,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 126; DEVELOPMENT_TEAM = JND55328G8; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; @@ -547,7 +547,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 126; DEVELOPMENT_TEAM = JND55328G8; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; @@ -679,7 +679,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 126; DEVELOPMENT_TEAM = JND55328G8; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -702,7 +702,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 126; DEVELOPMENT_TEAM = JND55328G8; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 503a8ff..e7aadf3 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -43,7 +43,7 @@ CFBundleSignature ???? CFBundleVersion - 125 + 126 LSRequiresIPhoneOS ITSAppUsesNonExemptEncryption diff --git a/ios/fastlane/report.xml b/ios/fastlane/report.xml index e0139aa..3e48371 100644 --- a/ios/fastlane/report.xml +++ b/ios/fastlane/report.xml @@ -5,22 +5,22 @@ - + - + - + - + diff --git a/lib/providers/messages_provider.dart b/lib/providers/messages_provider.dart index 632c693..868c297 100644 --- a/lib/providers/messages_provider.dart +++ b/lib/providers/messages_provider.dart @@ -664,6 +664,11 @@ class MessagesProvider with ChangeNotifier { return; // Skip duplicate } + final matchingSentReplayIndex = _findMatchingSentReplayIndex(finalMessage); + if (matchingSentReplayIndex != -1) { + _clearChannelSendWarning(_messages[matchingSentReplayIndex].id); + } + _messages.add(finalMessage); if (contactLocationSnapshot != null) { _messageContactLocations[finalMessage.id] = contactLocationSnapshot; @@ -712,7 +717,8 @@ class MessagesProvider with ChangeNotifier { for (int index = 0; index < _messages.length; index++) { final existing = _messages[index]; - if (!_matchesDuplicateScope(existing, message) || + if (existing.isSentMessage || + !_matchesDuplicateScope(existing, message) || existing.text != message.text) { continue; } @@ -790,6 +796,27 @@ class MessagesProvider with ChangeNotifier { existingSenderName == incomingSenderName; } + int _findMatchingSentReplayIndex(Message message) { + if (!message.isChannelMessage || message.isSentMessage) { + return -1; + } + + for (int index = 0; index < _messages.length; index++) { + final existing = _messages[index]; + if (!existing.isSentMessage || + !existing.isChannelMessage || + existing.text != message.text) { + continue; + } + + if (_matchesDuplicateScope(existing, message)) { + return index; + } + } + + return -1; + } + /// Add multiple messages void addMessages(List messages) { int addedCount = 0; diff --git a/pubspec.yaml b/pubspec.yaml index 2f1cef1..85d02b5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 2026.0322.2+39 +version: 2026.0323.1+40 environment: sdk: ^3.9.2 diff --git a/test/providers/messages_provider_retransmission_test.dart b/test/providers/messages_provider_retransmission_test.dart index bb962eb..02aaa4d 100644 --- a/test/providers/messages_provider_retransmission_test.dart +++ b/test/providers/messages_provider_retransmission_test.dart @@ -214,7 +214,7 @@ void main() { }); }); - test('channel warning clears when replay is deduped into sent bubble', () { + test('channel warning clears when replay arrives after send', () { fakeAsync((async) { final provider = MessagesProvider(); provider.resolveContactNameCallback = (_) => 'dz0ny (SI)'; @@ -239,11 +239,11 @@ void main() { ); expect(provider.hasChannelSendWarning('c-warn-replay'), isFalse); - expect(provider.messages, hasLength(1)); + expect(provider.messages, hasLength(2)); }); }); - test('channel replay is deduped for self sender within repeat window', () { + test('channel replay is kept separate for self sender within repeat window', () { final provider = MessagesProvider(); provider.resolveContactNameCallback = (_) => 'dz0ny (SI)'; provider.addSentMessage( @@ -259,9 +259,10 @@ void main() { ), ); - expect(provider.messages, hasLength(1)); - expect(provider.messages.single.id, equals('c-echo')); - expect(provider.messages.single.senderName, equals('dz0ny (SI)')); + expect(provider.messages, hasLength(2)); + expect(provider.messages.first.id, equals('c-echo')); + expect(provider.messages.last.id, equals('c-echo-incoming')); + expect(provider.messages.last.senderName, equals('dz0ny (SI)')); }); test( @@ -308,7 +309,7 @@ void main() { expect(provider.messages, hasLength(2)); }); - test('channel replay can dedupe using lazily resolved self name', () { + test('channel replay stays separate using lazily resolved self name', () { final provider = MessagesProvider(); provider.addSentMessage( _buildSentChannelMessage(id: 'c-lazy', senderTimestamp: 1700000400), @@ -324,11 +325,12 @@ void main() { ), ); - expect(provider.messages, hasLength(1)); - expect(provider.messages.single.id, equals('c-lazy')); + expect(provider.messages, hasLength(2)); + expect(provider.messages.first.id, equals('c-lazy')); + expect(provider.messages.last.id, equals('c-lazy-incoming')); }); - test('channel replay dedupes meshcore-prefixed self sender name', () { + test('channel replay stays separate for meshcore-prefixed self sender name', () { final provider = MessagesProvider(); provider.resolveContactNameCallback = (_) => 'MeshCore-dz0ny (SI)'; provider.addSentMessage( @@ -344,8 +346,9 @@ void main() { ), ); - expect(provider.messages, hasLength(1)); - expect(provider.messages.single.id, equals('c-prefix')); + expect(provider.messages, hasLength(2)); + expect(provider.messages.first.id, equals('c-prefix')); + expect(provider.messages.last.id, equals('c-prefix-incoming')); }); test('duplicate incoming message increments received copy count', () {