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) {},
),
),
),