From 24e715d01efe1ba6e8bcc05ede9483643458ae9c Mon Sep 17 00:00:00 2001 From: Janez T Date: Fri, 13 Mar 2026 14:18:27 +0100 Subject: [PATCH] Bump iOS project version --- ios/Runner.xcodeproj/project.pbxproj | 12 +-- ios/Runner/Info.plist | 2 +- ios/fastlane/report.xml | 8 +- lib/providers/app_provider.dart | 42 ++++++-- lib/providers/connection_provider.dart | 34 ++++--- lib/providers/messages_provider.dart | 18 +++- lib/widgets/common/bidirectional_refresh.dart | 13 ++- macos/Podfile.lock | 38 ++++--- pubspec.lock | 8 +- pubspec.yaml | 7 +- .../app_provider_channel_info_test.dart | 31 ++++++ ...connection_provider_channel_slot_test.dart | 21 ++++ ...messages_provider_retransmission_test.dart | 98 ++++++++++++++++--- .../recipient_selector_sheet_test.dart | 2 +- 14 files changed, 261 insertions(+), 73 deletions(-) create mode 100644 test/providers/app_provider_channel_info_test.dart create mode 100644 test/providers/connection_provider_channel_slot_test.dart diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 6b85c9a..acc0526 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 = 112; + CURRENT_PROJECT_VERSION = 113; 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 = 112; + CURRENT_PROJECT_VERSION = 113; 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 = 112; + CURRENT_PROJECT_VERSION = 113; 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 = 112; + CURRENT_PROJECT_VERSION = 113; 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 = 112; + CURRENT_PROJECT_VERSION = 113; 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 = 112; + CURRENT_PROJECT_VERSION = 113; DEVELOPMENT_TEAM = JND55328G8; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index ac68671..3d869bf 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -43,7 +43,7 @@ CFBundleSignature ???? CFBundleVersion - 112 + 113 LSRequiresIPhoneOS ITSAppUsesNonExemptEncryption diff --git a/ios/fastlane/report.xml b/ios/fastlane/report.xml index fdaa206..5563930 100644 --- a/ios/fastlane/report.xml +++ b/ios/fastlane/report.xml @@ -5,22 +5,22 @@ - + - + - + - + diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart index 33d4bb2..7e63149 100644 --- a/lib/providers/app_provider.dart +++ b/lib/providers/app_provider.dart @@ -59,6 +59,24 @@ class _DirectMessageRouteSession { /// Main App Provider - coordinates all other providers class AppProvider with ChangeNotifier { static const int _maxDirectPayloadHops = 3; + @visibleForTesting + static bool isDeletedChannelInfo( + int channelIdx, + String channelName, + Uint8List secret, + ) { + return channelIdx != 0 && + channelName.isEmpty && + secret.every((byte) => byte == 0); + } + + @visibleForTesting + static String channelContactName(int channelIdx, String channelName) { + return channelName.isEmpty && channelIdx != 0 + ? 'Channel $channelIdx' + : channelName; + } + final ConnectionProvider connectionProvider; final ContactsProvider contactsProvider; final MessagesProvider messagesProvider; @@ -736,8 +754,20 @@ class AppProvider with ChangeNotifier { '🔔 [AppProvider] onChannelInfoReceived called: idx=$channelIdx, name="$channelName"', ); - // Check if this is a channel deletion (empty name) - if (channelName.isEmpty && channelIdx != 0) { + final isDeletedChannel = AppProvider.isDeletedChannelInfo( + channelIdx, + channelName, + secret, + ); + final contactChannelName = AppProvider.channelContactName( + channelIdx, + channelName, + ); + + // Only treat the slot as deleted when firmware returns an empty + // name and a zeroed secret. Protected channels may still have an + // empty display name but remain fully configured. + if (isDeletedChannel) { debugPrint( ' 🗑️ Channel $channelIdx deleted - removing from providers', ); @@ -772,10 +802,10 @@ class AppProvider with ChangeNotifier { // Also add as Contact to ContactsProvider (for UI display) // Skip if it's public channel (already exists) debugPrint( - '📻 [AppProvider] Channel $channelIdx: "$channelName" (isEmpty: ${channelName.isEmpty}, isHashChannel: ${channelName.startsWith('#')})', + '📻 [AppProvider] Channel $channelIdx: "$contactChannelName" (rawNameEmpty: ${channelName.isEmpty}, isHashChannel: ${channelName.startsWith('#')})', ); - if (channelName.isNotEmpty && channelIdx != 0) { + if (channelIdx != 0) { debugPrint( ' ✅ Adding channel $channelIdx to ContactsProvider as Contact', ); @@ -795,7 +825,7 @@ class AppProvider with ChangeNotifier { flags: flags ?? 0, outPathLen: -1, // Flood mode for channels outPath: Uint8List(0), // Empty path for channels - advName: channelName, + advName: contactChannelName, lastAdvert: now, advLat: 0, // Channels don't have location advLon: 0, @@ -809,7 +839,7 @@ class AppProvider with ChangeNotifier { ); } else { debugPrint( - ' ⏭️ Skipping channel $channelIdx (empty: ${channelName.isEmpty}, isPublic: ${channelIdx == 0})', + ' ⏭️ Skipping channel $channelIdx (isPublic: ${channelIdx == 0})', ); } } catch (e, stackTrace) { diff --git a/lib/providers/connection_provider.dart b/lib/providers/connection_provider.dart index 24abd64..9e7491c 100644 --- a/lib/providers/connection_provider.dart +++ b/lib/providers/connection_provider.dart @@ -825,6 +825,20 @@ class ConnectionProvider with ChangeNotifier { /// This should be set by AppProvider to query ChannelsProvider Function(int channelIdx)? getChannelInfo; + @visibleForTesting + static bool channelHasConfiguredSecret(Uint8List secret) { + return secret.any((byte) => byte != 0); + } + + bool _channelSlotIsOccupied(Object channel) { + final channelName = (channel as dynamic).name as String?; + final secret = (channel as dynamic).secret; + final hasConfiguredSecret = + secret is Uint8List && channelHasConfiguredSecret(secret); + return (channelName != null && channelName.isNotEmpty) || + hasConfiguredSecret; + } + /// Check if a specific channel slot is empty Future isChannelSlotEmpty(int channelIdx) async { if (!_activeService.isConnected) { @@ -836,8 +850,7 @@ class ConnectionProvider with ChangeNotifier { if (getChannelInfo != null) { final channel = getChannelInfo!(channelIdx); if (channel != null) { - final channelName = (channel as dynamic).name as String?; - return channelName == null || channelName.isEmpty; + return !_channelSlotIsOccupied(channel); } } @@ -849,8 +862,7 @@ class ConnectionProvider with ChangeNotifier { if (getChannelInfo != null) { final channel = getChannelInfo!(channelIdx); if (channel != null) { - final channelName = (channel as dynamic).name as String?; - return channelName == null || channelName.isEmpty; + return !_channelSlotIsOccupied(channel); } } @@ -900,8 +912,7 @@ class ConnectionProvider with ChangeNotifier { continue; } - final channelName = (channel as dynamic).name as String?; - if (channelName != null && channelName.isNotEmpty) { + if (_channelSlotIsOccupied(channel)) { usedIndices.add(i); } } @@ -923,12 +934,13 @@ class ConnectionProvider with ChangeNotifier { // First check cache if (getChannelInfo != null) { final channel = getChannelInfo!(i); - if (channel != null) { + if (channel != null && _channelSlotIsOccupied(channel)) { final channelName = (channel as dynamic).name as String?; - if (channelName != null && channelName.isNotEmpty) { - debugPrint(' ⏭️ Slot $i occupied: "$channelName"'); - continue; // Skip occupied slots - } + final label = (channelName != null && channelName.isNotEmpty) + ? channelName + : 'Channel $i'; + debugPrint(' ⏭️ Slot $i occupied: "$label"'); + continue; // Skip occupied slots } } diff --git a/lib/providers/messages_provider.dart b/lib/providers/messages_provider.dart index 04970c9..1844b4e 100644 --- a/lib/providers/messages_provider.dart +++ b/lib/providers/messages_provider.dart @@ -636,7 +636,23 @@ class MessagesProvider with ChangeNotifier { } final existingSender = existing.senderKeyShort ?? existing.senderName; final incomingSender = message.senderKeyShort ?? message.senderName; - return existingSender == incomingSender; + if (existingSender == incomingSender) { + 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 with only the public sender handle. Only fold that replay + // into the original bubble after LOG_RX_DATA has already confirmed it as + // our own transmitted packet. + if (existing.isSentMessage && + existing.echoCount > 0 && + !message.isSentMessage && + existing.senderTimestamp == message.senderTimestamp) { + return true; + } + + return false; } return true; diff --git a/lib/widgets/common/bidirectional_refresh.dart b/lib/widgets/common/bidirectional_refresh.dart index ca5601a..0db5d18 100644 --- a/lib/widgets/common/bidirectional_refresh.dart +++ b/lib/widgets/common/bidirectional_refresh.dart @@ -81,14 +81,13 @@ class _BidirectionalRefreshState extends State { try { await widget.onRefresh(); } finally { - if (!mounted) { - return; + if (mounted) { + setState(() { + _isRefreshing = false; + _topPullDistance = 0; + _bottomPullDistance = 0; + }); } - setState(() { - _isRefreshing = false; - _topPullDistance = 0; - _bottomPullDistance = 0; - }); } } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 5560fdc..ccb71c5 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -8,6 +8,10 @@ PODS: - FlutterMacOS - file_picker (0.0.1): - FlutterMacOS + - file_selector_macos (0.0.1): + - FlutterMacOS + - flutter_avif_macos (0.0.1): + - FlutterMacOS - flutter_blue_plus_darwin (0.0.2): - Flutter - FlutterMacOS @@ -19,10 +23,6 @@ PODS: - FlutterMacOS - nsd_macos (0.0.1): - FlutterMacOS - - ObjectBox (4.4.1) - - objectbox_flutter_libs (0.0.1): - - FlutterMacOS - - ObjectBox (= 4.4.1) - package_info_plus (0.0.1): - FlutterMacOS - path_provider_foundation (0.0.1): @@ -35,6 +35,9 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS + - sqflite_darwin (0.0.4): + - Flutter + - FlutterMacOS - url_launcher_macos (0.0.1): - FlutterMacOS @@ -43,23 +46,21 @@ DEPENDENCIES: - codec2_flutter (from `Flutter/ephemeral/.symlinks/plugins/codec2_flutter/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) + - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) + - flutter_avif_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_avif_macos/macos`) - flutter_blue_plus_darwin (from `Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin`) - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - geolocator_apple (from `Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin`) - nsd_macos (from `Flutter/ephemeral/.symlinks/plugins/nsd_macos/macos`) - - objectbox_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/objectbox_flutter_libs/macos`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - record_macos (from `Flutter/ephemeral/.symlinks/plugins/record_macos/macos`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) -SPEC REPOS: - trunk: - - ObjectBox - EXTERNAL SOURCES: audioplayers_darwin: :path: Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/darwin @@ -69,6 +70,10 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos file_picker: :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos + file_selector_macos: + :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos + flutter_avif_macos: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_avif_macos/macos flutter_blue_plus_darwin: :path: Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin flutter_local_notifications: @@ -79,8 +84,6 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin nsd_macos: :path: Flutter/ephemeral/.symlinks/plugins/nsd_macos/macos - objectbox_flutter_libs: - :path: Flutter/ephemeral/.symlinks/plugins/objectbox_flutter_libs/macos package_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos path_provider_foundation: @@ -91,28 +94,31 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + sqflite_darwin: + :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: - audioplayers_darwin: 4f9ca89d92d3d21cec7ec580e78ca888e5fb68bd + audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5 codec2_flutter: 50d930e6a1a187f05891b89300c6ef589651bfbd device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76 file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a + file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7 + flutter_avif_macos: 9ed61d67adfbd6964eccb59971020fb55c31fc11 flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3 - flutter_local_notifications: 4bf37a31afde695b56091b4ae3e4d9c7a7e6cda0 + flutter_local_notifications: 1fc7ffb10a83d6a2eeeeddb152d43f1944b0aad0 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e nsd_macos: a472240e770b92f6c6df1022403aa29c90d012e3 - ObjectBox: 7da4aceb5013d041bfafdbc6d744a26918b09757 - objectbox_flutter_libs: f51d18f6a4b5965c218843373b7dc5ed5e3a2008 package_info_plus: f0052d280d17aa382b932f399edf32507174e870 path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 record_macos: 7f227161b93c49e7e34fe681c5891c8622c8cc8b share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd -PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 +PODFILE CHECKSUM: e3de8f2486e300492cc80a2fe0576e9d269ba22b COCOAPODS: 1.16.2 diff --git a/pubspec.lock b/pubspec.lock index 57a6418..7e8b913 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -794,9 +794,11 @@ packages: meshcore_client: dependency: "direct main" description: - path: "../meshcore_client" - relative: true - source: path + path: "." + ref: fbe0803 + resolved-ref: fbe08032df6a76ebc8339cbcca794a95e976a514 + url: "https://github.com/dz0ny/meshcore_client.git" + source: git version: "0.1.0" meta: dependency: transitive diff --git a/pubspec.yaml b/pubspec.yaml index d86e210..4f26a3a 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.0312.1+27 +version: 2026.0313.1+28 environment: sdk: ^3.9.2 @@ -44,7 +44,7 @@ dependencies: meshcore_client: git: url: https://github.com/dz0ny/meshcore_client.git - ref: main + ref: "fbe0803" # Codec2 ultra-low-bitrate speech codec (FFI plugin) codec2_flutter: @@ -139,9 +139,6 @@ dev_dependencies: fake_async: ^1.3.3 dependency_overrides: - meshcore_client: - path: ../meshcore_client - # path_provider_foundation 2.6.0 pulls in package:objective_c as a native # asset. That framework has been ending up archived with a macOS platform # slice and fails App Store validation for iOS uploads. diff --git a/test/providers/app_provider_channel_info_test.dart b/test/providers/app_provider_channel_info_test.dart new file mode 100644 index 0000000..a2f42bb --- /dev/null +++ b/test/providers/app_provider_channel_info_test.dart @@ -0,0 +1,31 @@ +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_sar_app/providers/app_provider.dart'; + +void main() { + group('AppProvider channel info handling', () { + test('treats zeroed unnamed non-public channel as deleted', () { + expect( + AppProvider.isDeletedChannelInfo(2, '', Uint8List(16)), + isTrue, + ); + }); + + test('keeps unnamed non-public channel when secret is configured', () { + final secret = Uint8List.fromList([ + 1, + ...List.filled(15, 0), + ]); + + expect( + AppProvider.isDeletedChannelInfo(2, '', secret), + isFalse, + ); + expect( + AppProvider.channelContactName(2, ''), + 'Channel 2', + ); + }); + }); +} diff --git a/test/providers/connection_provider_channel_slot_test.dart b/test/providers/connection_provider_channel_slot_test.dart new file mode 100644 index 0000000..5bad222 --- /dev/null +++ b/test/providers/connection_provider_channel_slot_test.dart @@ -0,0 +1,21 @@ +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_sar_app/providers/connection_provider.dart'; + +void main() { + group('ConnectionProvider channel slot occupancy', () { + test('treats non-zero secret as configured', () { + expect( + ConnectionProvider.channelHasConfiguredSecret(Uint8List(16)), + isFalse, + ); + expect( + ConnectionProvider.channelHasConfiguredSecret( + Uint8List.fromList([1, ...List.filled(15, 0)]), + ), + isTrue, + ); + }); + }); +} diff --git a/test/providers/messages_provider_retransmission_test.dart b/test/providers/messages_provider_retransmission_test.dart index dcb8e28..19b4e8e 100644 --- a/test/providers/messages_provider_retransmission_test.dart +++ b/test/providers/messages_provider_retransmission_test.dart @@ -39,6 +39,46 @@ Message _buildDirectMessage(String id) { ); } +Message _buildSentChannelMessage({ + required String id, + required int senderTimestamp, + String text = 'broadcast', + int channelIdx = 0, +}) { + return Message( + id: id, + messageType: MessageType.channel, + senderPublicKeyPrefix: Uint8List.fromList([0, 1, 2, 3, 4, 5]), + channelIdx: channelIdx, + pathLen: 0, + textType: MessageTextType.plain, + senderTimestamp: senderTimestamp, + text: text, + receivedAt: DateTime.now(), + deliveryStatus: MessageDeliveryStatus.sending, + ); +} + +Message _buildReceivedChannelReplay({ + required String id, + required int senderTimestamp, + String text = 'broadcast', + int channelIdx = 0, + String senderName = 'Radio Alpha', +}) { + return Message( + id: id, + messageType: MessageType.channel, + channelIdx: channelIdx, + pathLen: 1, + textType: MessageTextType.plain, + senderTimestamp: senderTimestamp, + text: text, + senderName: senderName, + receivedAt: DateTime.now(), + ); +} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -124,18 +164,7 @@ void main() { test('channel messages are marked sent immediately', () { final provider = MessagesProvider(); provider.addSentMessage( - Message( - id: 'c1', - messageType: MessageType.channel, - senderPublicKeyPrefix: Uint8List.fromList([0, 1, 2, 3, 4, 5]), - channelIdx: 0, - pathLen: 0, - textType: MessageTextType.plain, - senderTimestamp: 1700000000, - text: 'broadcast', - receivedAt: DateTime.now(), - deliveryStatus: MessageDeliveryStatus.sending, - ), + _buildSentChannelMessage(id: 'c1', senderTimestamp: 1700000000), ); provider.markMessageSent('c1', 0, 0); @@ -146,6 +175,51 @@ void main() { ); }); + test( + 'channel replay is deduped after raw echo detection confirms our send', + () { + final provider = MessagesProvider(); + provider.addSentMessage( + _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: 1700000100, + ), + ); + + expect(provider.messages, hasLength(1)); + expect(provider.messages.single.id, equals('c-echo')); + }, + ); + + test( + 'channel replay is not deduped before raw echo detection confirms it', + () { + final provider = MessagesProvider(); + provider.addSentMessage( + _buildSentChannelMessage( + id: 'c-no-echo', + senderTimestamp: 1700000200, + ), + ); + provider.markMessageSent('c-no-echo', 0, 0); + + provider.addMessage( + _buildReceivedChannelReplay( + id: 'c-no-echo-incoming', + senderTimestamp: 1700000200, + ), + ); + + expect(provider.messages, hasLength(2)); + }, + ); + test('missing ACK schedules a delayed retransmission', () { fakeAsync((async) { final provider = MessagesProvider(); diff --git a/test/widgets/recipient_selector_sheet_test.dart b/test/widgets/recipient_selector_sheet_test.dart index 4b9abfb..8222170 100644 --- a/test/widgets/recipient_selector_sheet_test.dart +++ b/test/widgets/recipient_selector_sheet_test.dart @@ -89,7 +89,7 @@ void main() { unreadCount: 0, unreadCountsByPublicKey: const {}, showAllOption: false, - onSelect: (_, __) {}, + onSelect: (selectedRecipient, draftMessage) {}, ), ), ),