fix: init tabs advert quick add

ref:
This commit is contained in:
Janez T
2026-02-28 11:23:11 +01:00
parent baf49d27d8
commit 6bd7517180
32 changed files with 850 additions and 11558 deletions

View File

@@ -20,7 +20,8 @@ class AppProvider with ChangeNotifier {
final DrawingProvider drawingProvider;
final ChannelsProvider channelsProvider;
final TileCacheService tileCacheService;
final LocationTrackingService locationTrackingService = LocationTrackingService();
final LocationTrackingService locationTrackingService =
LocationTrackingService();
bool _isInitialized = false;
bool get isInitialized => _isInitialized;
@@ -61,7 +62,9 @@ class AppProvider with ChangeNotifier {
// Give DrawingProvider a moment to finish loading too
await Future.delayed(const Duration(milliseconds: 100));
debugPrint('🔄 [AppProvider] Early sync: syncing drawings from messages...');
debugPrint(
'🔄 [AppProvider] Early sync: syncing drawings from messages...',
);
messagesProvider.syncDrawingsWithProvider(drawingProvider);
}
@@ -129,7 +132,9 @@ class AppProvider with ChangeNotifier {
// Setup callbacks
locationTrackingService.onPositionUpdate = (position) {
debugPrint('📍 [AppProvider] Position updated: ${position.latitude}, ${position.longitude}');
debugPrint(
'📍 [AppProvider] Position updated: ${position.latitude}, ${position.longitude}',
);
};
locationTrackingService.onBroadcastSent = (position) {
@@ -141,7 +146,9 @@ class AppProvider with ChangeNotifier {
};
locationTrackingService.onTrackingStateChanged = (isTracking) {
debugPrint('🔄 [AppProvider] Location tracking state: ${isTracking ? "started" : "stopped"}');
debugPrint(
'🔄 [AppProvider] Location tracking state: ${isTracking ? "started" : "stopped"}',
);
};
debugPrint('✅ [AppProvider] Location tracking service initialized');
@@ -187,87 +194,103 @@ class AppProvider with ChangeNotifier {
};
// When channel info is received
connectionProvider.onChannelInfoReceived = (int channelIdx, String channelName, Uint8List secret, int? flags) {
try {
debugPrint('🔔 [AppProvider] onChannelInfoReceived called: idx=$channelIdx, name="$channelName"');
// Check if this is a channel deletion (empty name)
if (channelName.isEmpty && channelIdx != 0) {
debugPrint(' 🗑️ Channel $channelIdx deleted - removing from providers');
// Remove from ChannelsProvider
channelsProvider.removeChannel(channelIdx);
debugPrint(' ✅ Removed from ChannelsProvider');
// Remove from ContactsProvider using pseudo public key
final publicKeyBytes = Uint8List(32);
publicKeyBytes[0] = 0xFF; // Special marker for channels
publicKeyBytes[1] = channelIdx; // Channel index
final publicKeyHex = publicKeyBytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join('');
contactsProvider.removeContact(publicKeyHex);
debugPrint(' ✅ Removed from ContactsProvider');
return;
}
// Add/update in ChannelsProvider
channelsProvider.addOrUpdateChannel(
index: channelIdx,
name: channelName,
secret: secret,
flags: flags,
);
debugPrint(' ✅ Added to ChannelsProvider');
// 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('#')})');
if (channelName.isNotEmpty && channelIdx != 0) {
debugPrint(' ✅ Adding channel $channelIdx to ContactsProvider as Contact');
// Create a pseudo public key for the channel based on its index
// Use channel index as a unique identifier (pad to 32 bytes)
final publicKeyBytes = Uint8List(32);
publicKeyBytes[0] = 0xFF; // Special marker for channels
publicKeyBytes[1] = channelIdx; // Channel index
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
contactsProvider.addOrUpdateContact(
Contact(
publicKey: publicKeyBytes,
type: ContactType.channel,
flags: flags ?? 0,
outPathLen: -1, // Flood mode for channels
outPath: Uint8List(0), // Empty path for channels
advName: channelName,
lastAdvert: now,
advLat: 0, // Channels don't have location
advLon: 0,
lastMod: now,
isNew: false, // Don't mark channels as new
),
);
debugPrint(' ✅ Channel contact added. Total channels in ContactsProvider: ${contactsProvider.channels.length}');
} else {
debugPrint(' ⏭️ Skipping channel $channelIdx (empty: ${channelName.isEmpty}, isPublic: ${channelIdx == 0})');
}
} catch (e, stackTrace) {
debugPrint('❌ [AppProvider] Error in onChannelInfoReceived: $e');
debugPrint(' Stack trace: $stackTrace');
}
};
connectionProvider.onChannelInfoReceived =
(int channelIdx, String channelName, Uint8List secret, int? flags) {
try {
debugPrint(
'🔔 [AppProvider] onChannelInfoReceived called: idx=$channelIdx, name="$channelName"',
);
// Check if this is a channel deletion (empty name)
if (channelName.isEmpty && channelIdx != 0) {
debugPrint(
' 🗑️ Channel $channelIdx deleted - removing from providers',
);
// Remove from ChannelsProvider
channelsProvider.removeChannel(channelIdx);
debugPrint(' ✅ Removed from ChannelsProvider');
// Remove from ContactsProvider using pseudo public key
final publicKeyBytes = Uint8List(32);
publicKeyBytes[0] = 0xFF; // Special marker for channels
publicKeyBytes[1] = channelIdx; // Channel index
final publicKeyHex = publicKeyBytes
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join('');
contactsProvider.removeContact(publicKeyHex);
debugPrint(' ✅ Removed from ContactsProvider');
return;
}
// Add/update in ChannelsProvider
channelsProvider.addOrUpdateChannel(
index: channelIdx,
name: channelName,
secret: secret,
flags: flags,
);
debugPrint(' ✅ Added to ChannelsProvider');
// 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('#')})',
);
if (channelName.isNotEmpty && channelIdx != 0) {
debugPrint(
' ✅ Adding channel $channelIdx to ContactsProvider as Contact',
);
// Create a pseudo public key for the channel based on its index
// Use channel index as a unique identifier (pad to 32 bytes)
final publicKeyBytes = Uint8List(32);
publicKeyBytes[0] = 0xFF; // Special marker for channels
publicKeyBytes[1] = channelIdx; // Channel index
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
contactsProvider.addOrUpdateContact(
Contact(
publicKey: publicKeyBytes,
type: ContactType.channel,
flags: flags ?? 0,
outPathLen: -1, // Flood mode for channels
outPath: Uint8List(0), // Empty path for channels
advName: channelName,
lastAdvert: now,
advLat: 0, // Channels don't have location
advLon: 0,
lastMod: now,
isNew: false, // Don't mark channels as new
),
);
debugPrint(
' ✅ Channel contact added. Total channels in ContactsProvider: ${contactsProvider.channels.length}',
);
} else {
debugPrint(
' ⏭️ Skipping channel $channelIdx (empty: ${channelName.isEmpty}, isPublic: ${channelIdx == 0})',
);
}
} catch (e, stackTrace) {
debugPrint('❌ [AppProvider] Error in onChannelInfoReceived: $e');
debugPrint(' Stack trace: $stackTrace');
}
};
// When a message is received
connectionProvider.onMessageReceived = (message) {
// Enrich message with sender name from contacts first
Message enrichedMessage = message;
if (message.senderPublicKeyPrefix != null && message.senderName == null) {
final contact = contactsProvider
.findContactByKey(message.senderPublicKeyPrefix!);
final contact = contactsProvider.findContactByKey(
message.senderPublicKeyPrefix!,
);
if (contact != null) {
enrichedMessage = message.copyWith(senderName: contact.advName);
}
@@ -281,10 +304,13 @@ class AppProvider with ChangeNotifier {
final drawing = DrawingMessageParser.parseDrawingMessage(
enrichedMessage.text,
senderName: senderName,
messageId: enrichedMessage.id, // Pass message ID for navigation linking
messageId:
enrichedMessage.id, // Pass message ID for navigation linking
);
if (drawing != null) {
debugPrint('🎨 [AppProvider] Drawing parsed successfully: ${drawing.type.name} from ${drawing.senderName ?? "unknown"}');
debugPrint(
'🎨 [AppProvider] Drawing parsed successfully: ${drawing.type.name} from ${drawing.senderName ?? "unknown"}',
);
debugPrint(' Drawing linked to message ID: ${enrichedMessage.id}');
drawingProvider.addReceivedDrawing(drawing);
@@ -318,7 +344,8 @@ class AppProvider with ChangeNotifier {
final contact = contactsProvider.contacts.firstWhere(
(c) => c.advName == name,
);
return contact.publicKeyHex.isNotEmpty && contact.publicKeyHex.length >= 12
return contact.publicKeyHex.isNotEmpty &&
contact.publicKeyHex.length >= 12
? contact.publicKeyHex.substring(0, 12)
: '';
} catch (e) {
@@ -335,7 +362,9 @@ class AppProvider with ChangeNotifier {
// When telemetry is received via PUSH_CODE_TELEMETRY_RESPONSE (0x8B)
// Used by older firmware versions for telemetry responses
connectionProvider.onTelemetryReceived = (publicKey, lppData) {
debugPrint('📊 [AppProvider] Telemetry response (0x8B) received - updating contact');
debugPrint(
'📊 [AppProvider] Telemetry response (0x8B) received - updating contact',
);
contactsProvider.updateTelemetry(publicKey, lppData);
};
@@ -343,7 +372,9 @@ class AppProvider with ChangeNotifier {
// Used by newer firmware versions for telemetry and other binary data
// BOTH callbacks (0x8B and 0x8C) must be handled for device compatibility
connectionProvider.onBinaryResponse = (publicKeyPrefix, tag, responseData) {
debugPrint('📊 [AppProvider] Binary response (0x8C) received - updating contact telemetry');
debugPrint(
'📊 [AppProvider] Binary response (0x8C) received - updating contact telemetry',
);
// Binary response tag 0 = telemetry data (Cayenne LPP format)
// Other tags may be used for different data types in the future
contactsProvider.updateTelemetry(publicKeyPrefix, responseData);
@@ -351,7 +382,9 @@ class AppProvider with ChangeNotifier {
// When a contact's routing path is updated in the mesh network
connectionProvider.onPathUpdated = (publicKey) {
debugPrint('🔄 [AppProvider] Path updated for contact: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...');
debugPrint(
'🔄 [AppProvider] Path updated for contact: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...',
);
// Trigger a single contact fetch to get the updated path information
// This is much more efficient than fetching all contacts
// This happens asynchronously to avoid blocking the event handler
@@ -365,11 +398,15 @@ class AppProvider with ChangeNotifier {
// When an advertisement is received (PUSH_CODE_ADVERT 0x80)
// This may be sent by the radio for existing contacts instead of PUSH_CODE_NEW_ADVERT (0x8A)
connectionProvider.onAdvertReceived = (publicKey) {
debugPrint('📡 [AppProvider] Advertisement received: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...');
debugPrint(
'📡 [AppProvider] Advertisement received: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...',
);
// Check if this is an existing contact that might have updated location
final contact = contactsProvider.findContactByKey(publicKey);
if (contact != null) {
debugPrint(' Existing contact "${contact.advName}" - fetching updated contact info (optimized)');
debugPrint(
' Existing contact "${contact.advName}" - fetching updated contact info (optimized)',
);
// Trigger a single contact fetch to get the updated contact information
// This is much more efficient than fetching all contacts
Future.delayed(const Duration(milliseconds: 100), () {
@@ -378,44 +415,88 @@ class AppProvider with ChangeNotifier {
}
});
} else {
debugPrint(' New contact - waiting for PUSH_CODE_NEW_ADVERT (0x8A) with full details');
contactsProvider.addPendingAdvert(
publicKey,
devicePublicKey: connectionProvider.deviceInfo.publicKey,
);
debugPrint(
' Unknown contact - added to pending adverts list and waiting for details',
);
}
};
// When firmware deletes a contact due to contacts table overflow (PUSH_CODE_CONTACT_DELETED 0x8F)
connectionProvider.onContactDeleted = (publicKey) {
final keyHex = publicKey
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join('');
final contact = contactsProvider.findContactByKey(publicKey);
final name = contact?.advName ?? keyHex.substring(0, 12);
debugPrint('⚠️ [AppProvider] Contact deleted by firmware: $name');
contactsProvider.removeContact(keyHex);
messagesProvider.logSystemMessage(
text: 'Contact "$name" was removed — device contacts table is full',
level: 'warning',
);
};
// When firmware reports contacts storage is full (PUSH_CODE_CONTACTS_FULL 0x90)
connectionProvider.onContactsFull = () {
debugPrint('⚠️ [AppProvider] Contacts storage full');
messagesProvider.logSystemMessage(
text:
'Device contacts storage is full. New contacts will overwrite old ones.',
level: 'warning',
);
};
// When a message is sent (RESP_CODE_SENT received)
connectionProvider.onMessageSent = (messageId, expectedAckTag, suggestedTimeoutMs) {
debugPrint('📤 [AppProvider] Message sent - Message ID: $messageId, ACK tag: $expectedAckTag');
messagesProvider.markMessageSent(messageId, expectedAckTag, suggestedTimeoutMs);
connectionProvider
.onMessageSent = (messageId, expectedAckTag, suggestedTimeoutMs) {
debugPrint(
'📤 [AppProvider] Message sent - Message ID: $messageId, ACK tag: $expectedAckTag',
);
messagesProvider.markMessageSent(
messageId,
expectedAckTag,
suggestedTimeoutMs,
);
};
// When a message is delivered (PUSH_CODE_SEND_CONFIRMED received)
connectionProvider.onMessageDelivered = (ackCode, roundTripTimeMs) {
debugPrint('✅ [AppProvider] Message delivered - ACK: $ackCode, RTT: ${roundTripTimeMs}ms');
debugPrint(
'✅ [AppProvider] Message delivered - ACK: $ackCode, RTT: ${roundTripTimeMs}ms',
);
messagesProvider.markMessageDelivered(ackCode, roundTripTimeMs);
};
// When an echo is detected for a public channel message (PUSH_CODE_LOG_RX_DATA matched)
connectionProvider.onMessageEchoDetected = (messageId, echoCount, snrRaw, rssiDbm) {
debugPrint('🔊 [AppProvider] Echo detected - Message: $messageId, Count: $echoCount');
connectionProvider
.onMessageEchoDetected = (messageId, echoCount, snrRaw, rssiDbm) {
debugPrint(
'🔊 [AppProvider] Echo detected - Message: $messageId, Count: $echoCount',
);
messagesProvider.handleMessageEcho(messageId, echoCount, snrRaw, rssiDbm);
};
// Wire up MessagesProvider's sendMessageCallback for retry logic
messagesProvider.sendMessageCallback = ({
required contactPublicKey,
required text,
required messageId,
required contact,
retryAttempt = 0,
}) async {
return await connectionProvider.sendTextMessage(
contactPublicKey: contactPublicKey,
text: text,
messageId: messageId,
contact: contact,
retryAttempt: retryAttempt,
);
};
messagesProvider.sendMessageCallback =
({
required contactPublicKey,
required text,
required messageId,
required contact,
retryAttempt = 0,
}) async {
return await connectionProvider.sendTextMessage(
contactPublicKey: contactPublicKey,
text: text,
messageId: messageId,
contact: contact,
retryAttempt: retryAttempt,
);
};
}
/// Initialize the app (load contacts, sync time, etc.)
@@ -446,19 +527,25 @@ class AppProvider with ChangeNotifier {
// In simple mode: only sync first 5 channels for faster startup
// In normal mode: sync all channels (up to device max)
final channelsToSync = _isSimpleMode ? 5 : null;
debugPrint('📻 [AppProvider] Syncing channels${_isSimpleMode ? ' (simple mode: max 5)' : ''}...');
debugPrint(
'📻 [AppProvider] Syncing channels${_isSimpleMode ? ' (simple mode: max 5)' : ''}...',
);
await connectionProvider.syncChannels(maxChannels: channelsToSync);
debugPrint('✅ [AppProvider] Channel sync complete');
// Configure the default public channel (channel 0)
// This must be done before sending any channel messages
// Note: Some firmware versions may have this pre-configured
debugPrint('📻 [AppProvider] Configuring default public channel (channel 0)...');
debugPrint(
'📻 [AppProvider] Configuring default public channel (channel 0)...',
);
try {
await connectionProvider.configureDefaultPublicChannel();
debugPrint('✅ [AppProvider] Public channel configured successfully');
} catch (e) {
debugPrint('⚠️ [AppProvider] Public channel configuration failed (may already be configured): $e');
debugPrint(
'⚠️ [AppProvider] Public channel configuration failed (may already be configured): $e',
);
// Continue anyway - channel might already be configured in firmware
}
@@ -467,19 +554,27 @@ class AppProvider with ChangeNotifier {
// FALLBACK: Sync messages once after connection to catch any missed push notifications
// This handles the case where messages arrived while the app was disconnected
debugPrint('🔄 [AppProvider] Performing initial message sync (fallback for missed pushes)');
debugPrint(
'🔄 [AppProvider] Performing initial message sync (fallback for missed pushes)',
);
final initialMessageCount = await connectionProvider.syncAllMessages();
debugPrint('📥 [AppProvider] Initial sync retrieved $initialMessageCount message(s)');
debugPrint(
'📥 [AppProvider] Initial sync retrieved $initialMessageCount message(s)',
);
// Note: Future messages are synced automatically via PUSH_CODE_MSG_WAITING events
// Start location tracking AFTER all initialization is complete
debugPrint('📍 [AppProvider] Starting location tracking after successful initialization');
debugPrint(
'📍 [AppProvider] Starting location tracking after successful initialization',
);
await _startLocationTracking();
// Sync drawing messages with DrawingProvider
// This restores any drawings that may be missing from storage
debugPrint('🎨 [AppProvider] Syncing drawing messages with DrawingProvider...');
debugPrint(
'🎨 [AppProvider] Syncing drawing messages with DrawingProvider...',
);
messagesProvider.syncDrawingsWithProvider(drawingProvider);
notifyListeners();
@@ -503,7 +598,9 @@ class AppProvider with ChangeNotifier {
return;
}
debugPrint('📂 [AppProvider] Found ${rooms.length} room(s), attempting auto-login...');
debugPrint(
'📂 [AppProvider] Found ${rooms.length} room(s), attempting auto-login...',
);
final prefs = await SharedPreferences.getInstance();
@@ -513,7 +610,9 @@ class AppProvider with ChangeNotifier {
final roomKey = 'room_password_${room.publicKeyHex}';
final savedPassword = prefs.getString(roomKey) ?? 'hello';
debugPrint('🔑 [AppProvider] Auto-logging into room: ${room.advName}');
debugPrint(
'🔑 [AppProvider] Auto-logging into room: ${room.advName}',
);
// Set up one-time callbacks for this room login
await _loginToRoomWithCallback(room, savedPassword);
@@ -521,7 +620,9 @@ class AppProvider with ChangeNotifier {
// Small delay between logins to avoid overwhelming the device
await Future.delayed(const Duration(milliseconds: 300));
} catch (e) {
debugPrint('❌ [AppProvider] Failed to auto-login to ${room.advName}: $e');
debugPrint(
'❌ [AppProvider] Failed to auto-login to ${room.advName}: $e',
);
}
}
} catch (e) {
@@ -539,13 +640,16 @@ class AppProvider with ChangeNotifier {
final originalOnFail = connectionProvider.onLoginFail;
// Set up temporary callbacks
connectionProvider.onLoginSuccess = (publicKeyPrefix, permissions, isAdmin, tag) async {
connectionProvider
.onLoginSuccess = (publicKeyPrefix, permissions, isAdmin, tag) async {
// Restore original callbacks
connectionProvider.onLoginSuccess = originalOnSuccess;
connectionProvider.onLoginFail = originalOnFail;
debugPrint('✅ [AppProvider] Auto-login successful for ${room.advName}');
debugPrint('📡 [AppProvider] Room server will push messages automatically via PUSH_CODE_MSG_WAITING');
debugPrint(
'📡 [AppProvider] Room server will push messages automatically via PUSH_CODE_MSG_WAITING',
);
completer.complete(true);
};
@@ -555,7 +659,9 @@ class AppProvider with ChangeNotifier {
connectionProvider.onLoginSuccess = originalOnSuccess;
connectionProvider.onLoginFail = originalOnFail;
debugPrint('❌ [AppProvider] Auto-login failed for ${room.advName} (incorrect password)');
debugPrint(
'❌ [AppProvider] Auto-login failed for ${room.advName} (incorrect password)',
);
completer.complete(false);
};
@@ -581,7 +687,9 @@ class AppProvider with ChangeNotifier {
// Restore callbacks on error
connectionProvider.onLoginSuccess = originalOnSuccess;
connectionProvider.onLoginFail = originalOnFail;
debugPrint('❌ [AppProvider] Error during auto-login to ${room.advName}: $e');
debugPrint(
'❌ [AppProvider] Error during auto-login to ${room.advName}: $e',
);
}
}
@@ -595,11 +703,11 @@ class AppProvider with ChangeNotifier {
try {
// Sync contacts
await connectionProvider.getContacts();
// Sync channels (respect simple mode settings)
final channelsToSync = _isSimpleMode ? 5 : null;
await connectionProvider.syncChannels(maxChannels: channelsToSync);
// Messages are automatically synced via PUSH_CODE_MSG_WAITING events
notifyListeners();
} catch (e) {
@@ -614,9 +722,13 @@ class AppProvider with ChangeNotifier {
if (!connectionProvider.deviceInfo.isConnected) return 0;
try {
debugPrint('🔄 [AppProvider] Manual message sync requested (user initiated)');
debugPrint(
'🔄 [AppProvider] Manual message sync requested (user initiated)',
);
final messageCount = await connectionProvider.syncAllMessages();
debugPrint('✅ [AppProvider] Manual sync completed: $messageCount messages');
debugPrint(
'✅ [AppProvider] Manual sync completed: $messageCount messages',
);
notifyListeners();
return messageCount;
} catch (e) {
@@ -634,7 +746,9 @@ class AppProvider with ChangeNotifier {
// Location tracking will be started AFTER initialization completes
if (!isConnected && wasTracking) {
// Connection lost - stop location tracking
debugPrint('🔴 [AppProvider] BLE disconnected - stopping location tracking');
debugPrint(
'🔴 [AppProvider] BLE disconnected - stopping location tracking',
);
_stopLocationTracking();
}
}

View File

@@ -157,6 +157,8 @@ class ConnectionProvider with ChangeNotifier {
Function(int channelIdx, String channelName, Uint8List secret, int? flags)? onChannelInfoReceived;
Function(Uint8List publicKeyPrefix, int tag, Uint8List responseData)?
onBinaryResponse;
Function(Uint8List publicKey)? onContactDeleted;
VoidCallback? onContactsFull;
Function(Uint8List publicKey)? onAdvertReceived;
Function(Uint8List publicKey)? onPathUpdated;
Function(Uint8List publicKeyPrefix, int permissions, bool isAdmin, int tag)?
@@ -319,6 +321,16 @@ class ConnectionProvider with ChangeNotifier {
onChannelInfoReceived?.call(channelIdx, channelName, secret, flags);
};
_bleService.onContactDeleted = (publicKey) {
debugPrint('⚠️ [Provider] Contact deleted by firmware (contacts full overwrite)');
onContactDeleted?.call(publicKey);
};
_bleService.onContactsFull = () {
debugPrint('⚠️ [Provider] Contacts storage is full');
onContactsFull?.call();
};
_bleService.onMessageReceived = (message) {
// Parse SAR markers
final enhancedMessage = SarMessageParser.enhanceMessage(message);

View File

@@ -4,9 +4,32 @@ import '../services/cayenne_lpp_parser.dart';
import '../services/contact_storage_service.dart';
import '../utils/key_comparison.dart';
class PendingAdvert {
final Uint8List publicKey;
final DateTime receivedAt;
const PendingAdvert({required this.publicKey, required this.receivedAt});
String get publicKeyHex =>
publicKey.map((b) => b.toRadixString(16).padLeft(2, '0')).join('');
String get shortDisplayKey {
final prefix = publicKey.length >= 6 ? publicKey.sublist(0, 6) : publicKey;
return prefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':');
}
PendingAdvert copyWith({Uint8List? publicKey, DateTime? receivedAt}) {
return PendingAdvert(
publicKey: publicKey ?? this.publicKey,
receivedAt: receivedAt ?? this.receivedAt,
);
}
}
/// Contacts Provider - manages contact list and telemetry
class ContactsProvider with ChangeNotifier {
final Map<String, Contact> _contacts = {};
final Map<String, PendingAdvert> _pendingAdverts = {};
final ContactStorageService _storageService = ContactStorageService();
bool _isInitialized = false;
@@ -156,6 +179,9 @@ class ContactsProvider with ChangeNotifier {
}
List<Contact> get contacts => _contacts.values.toList();
List<PendingAdvert> get pendingAdverts =>
_pendingAdverts.values.toList()
..sort((a, b) => b.receivedAt.compareTo(a.receivedAt));
List<Contact> get chatContacts =>
contacts.where((c) => c.isChat).toList()..sort(_sortByLastSeen);
@@ -195,11 +221,12 @@ class ContactsProvider with ChangeNotifier {
/// Add or update a contact
/// Excludes contacts that match the device's own public key
void addOrUpdateContact(Contact contact, {Uint8List? devicePublicKey}) {
debugPrint('📝 [ContactsProvider] addOrUpdateContact called: ${contact.advName} (type: ${contact.type.displayName}, key: ${contact.publicKeyHex.substring(0, 8)}...)');
debugPrint(
'📝 [ContactsProvider] addOrUpdateContact called: ${contact.advName} (type: ${contact.type.displayName}, key: ${contact.publicKeyHex.substring(0, 8)}...)',
);
// Don't add contacts that match our device's public key
if (devicePublicKey != null &&
contact.publicKey.matches(devicePublicKey)) {
if (devicePublicKey != null && contact.publicKey.matches(devicePublicKey)) {
debugPrint(
' [ContactsProvider] Ignoring contact with device\'s own public key: ${contact.advName}',
);
@@ -208,7 +235,9 @@ class ContactsProvider with ChangeNotifier {
// Check if this is a new contact
final isNewContact = !_contacts.containsKey(contact.publicKeyHex);
debugPrint(' isNew: $isNewContact, total contacts before: ${_contacts.length}');
debugPrint(
' isNew: $isNewContact, total contacts before: ${_contacts.length}',
);
Contact updatedContact;
if (isNewContact) {
@@ -246,7 +275,10 @@ class ContactsProvider with ChangeNotifier {
}
_contacts[contact.publicKeyHex] = updatedContact;
debugPrint(' ✅ Contact added/updated. Total contacts: ${_contacts.length}, channels: ${channels.length}');
_pendingAdverts.remove(contact.publicKeyHex);
debugPrint(
' ✅ Contact added/updated. Total contacts: ${_contacts.length}, channels: ${channels.length}',
);
_persistContacts();
notifyListeners();
debugPrint(' 🔔 notifyListeners() called');
@@ -267,6 +299,7 @@ class ContactsProvider with ChangeNotifier {
continue;
}
_contacts[contact.publicKeyHex] = contact;
_pendingAdverts.remove(contact.publicKeyHex);
}
if (excluded > 0) {
debugPrint(
@@ -303,8 +336,8 @@ class ContactsProvider with ChangeNotifier {
// Update contact with new telemetry AND last seen time
// lastAdvert is Unix timestamp in seconds
final currentTimestamp =
(DateTime.now().millisecondsSinceEpoch / 1000).round();
final currentTimestamp = (DateTime.now().millisecondsSinceEpoch / 1000)
.round();
debugPrint(' Old lastAdvert: ${contact.lastAdvert}');
debugPrint(' New lastAdvert: $currentTimestamp');
@@ -351,6 +384,34 @@ class ContactsProvider with ChangeNotifier {
return _contacts[keyHex];
}
/// Add or refresh a pending advert entry from PUSH_CODE_ADVERT (0x80).
/// Excludes self key and existing contacts.
void addPendingAdvert(Uint8List publicKey, {Uint8List? devicePublicKey}) {
if (devicePublicKey != null && publicKey.matches(devicePublicKey)) {
return;
}
final keyHex = publicKey
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join('');
if (_contacts.containsKey(keyHex)) {
_pendingAdverts.remove(keyHex);
return;
}
final existing = _pendingAdverts[keyHex];
final now = DateTime.now();
if (existing != null) {
_pendingAdverts[keyHex] = existing.copyWith(receivedAt: now);
} else {
_pendingAdverts[keyHex] = PendingAdvert(
publicKey: Uint8List.fromList(publicKey),
receivedAt: now,
);
}
notifyListeners();
}
/// Find contact by name
Contact? findContactByName(String name) {
return contacts.firstWhere(
@@ -404,6 +465,7 @@ class ContactsProvider with ChangeNotifier {
/// Clear all contacts
void clearContacts() {
_contacts.clear();
_pendingAdverts.clear();
_persistContacts();
notifyListeners();
}
@@ -425,6 +487,7 @@ class ContactsProvider with ChangeNotifier {
// Then remove from local storage
_contacts.remove(publicKeyHex);
_pendingAdverts.remove(publicKeyHex);
_persistContacts();
notifyListeners();
}