Bump iOS project version

This commit is contained in:
Janez T
2026-03-13 14:18:27 +01:00
parent 6e17de3866
commit 24e715d01e
14 changed files with 261 additions and 73 deletions

View File

@@ -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) {

View File

@@ -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<bool> 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
}
}

View File

@@ -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;

View File

@@ -81,14 +81,13 @@ class _BidirectionalRefreshState extends State<BidirectionalRefresh> {
try {
await widget.onRefresh();
} finally {
if (!mounted) {
return;
if (mounted) {
setState(() {
_isRefreshing = false;
_topPullDistance = 0;
_bottomPullDistance = 0;
});
}
setState(() {
_isRefreshing = false;
_topPullDistance = 0;
_bottomPullDistance = 0;
});
}
}