diff --git a/lib/providers/contacts_provider.dart b/lib/providers/contacts_provider.dart index cd50ae1..f657cb0 100644 --- a/lib/providers/contacts_provider.dart +++ b/lib/providers/contacts_provider.dart @@ -423,9 +423,16 @@ class ContactsProvider with ChangeNotifier { debugPrint(' Old lastAdvert: ${contact.lastAdvert}'); debugPrint(' New lastAdvert: $currentTimestamp'); + final persistedGps = _getValidGpsOrNull(telemetry.gpsLocation); final updatedContact = contact.copyWith( telemetry: telemetry, lastAdvert: currentTimestamp, // Update last seen time + advLat: persistedGps != null + ? _coordinateToAdvertMicrodegrees(persistedGps.latitude) + : contact.advLat, + advLon: persistedGps != null + ? _coordinateToAdvertMicrodegrees(persistedGps.longitude) + : contact.advLon, ); _contacts[contact.publicKeyHex] = updatedContact; debugPrint(' ✅ Updated contact in map (with new lastAdvert)'); @@ -476,6 +483,10 @@ class ContactsProvider with ChangeNotifier { return incomingGps == null; } + int _coordinateToAdvertMicrodegrees(double coordinate) { + return (coordinate * 1e6).round(); + } + /// Find contact by public key prefix (6 bytes) Contact? _findContactByPrefix(Uint8List prefix) { if (prefix.length < 6) return null; diff --git a/lib/providers/helpers/ping_tracker.dart b/lib/providers/helpers/ping_tracker.dart index 0bf51e5..cca4b21 100644 --- a/lib/providers/helpers/ping_tracker.dart +++ b/lib/providers/helpers/ping_tracker.dart @@ -46,8 +46,8 @@ class PingTracker { /// Mark a ping as successful (response received) /// Should be called when telemetry response arrives void markPingSuccessful(Uint8List publicKey) { - final String keyHex = _publicKeyToHex(publicKey); - final request = _pendingPings.remove(keyHex); + final requestKey = _findMatchingPendingPingKey(publicKey); + final request = requestKey != null ? _pendingPings.remove(requestKey) : null; if (request != null) { request.cancel(); @@ -81,6 +81,23 @@ class PingTracker { String _publicKeyToHex(Uint8List publicKey) { return publicKey.map((b) => b.toRadixString(16).padLeft(2, '0')).join(''); } + + String? _findMatchingPendingPingKey(Uint8List responseKey) { + final responseHex = _publicKeyToHex(responseKey); + + if (_pendingPings.containsKey(responseHex)) { + return responseHex; + } + + for (final entry in _pendingPings.entries) { + final pendingHex = entry.key; + if (pendingHex.startsWith(responseHex) || responseHex.startsWith(pendingHex)) { + return pendingHex; + } + } + + return null; + } } /// Internal class to track a single ping request diff --git a/lib/screens/contacts_tab.dart b/lib/screens/contacts_tab.dart index fd3ec98..f53ddf4 100644 --- a/lib/screens/contacts_tab.dart +++ b/lib/screens/contacts_tab.dart @@ -119,6 +119,43 @@ class _ContactsTabState extends State { return l10n.daysAgo(diff.inDays); } + List _sortContactsByDistance(List contacts) { + final sorted = List.from(contacts); + + sorted.sort((a, b) { + final distanceA = _distanceFromCurrentPosition(a); + final distanceB = _distanceFromCurrentPosition(b); + + if (distanceA != null && distanceB != null) { + final distanceCompare = distanceA.compareTo(distanceB); + if (distanceCompare != 0) return distanceCompare; + } else if (distanceA != null) { + return -1; + } else if (distanceB != null) { + return 1; + } + + return b.lastSeenTime.compareTo(a.lastSeenTime); + }); + + return sorted; + } + + double? _distanceFromCurrentPosition(Contact contact) { + final currentPosition = _currentPosition; + final contactLocation = contact.displayLocation; + if (currentPosition == null || contactLocation == null) { + return null; + } + + return _calculateDistanceInMeters( + currentPosition.latitude, + currentPosition.longitude, + contactLocation.latitude, + contactLocation.longitude, + ); + } + /// Show the add channel dialog Future _showAddChannelDialog(BuildContext context) async { final l10n = AppLocalizations.of(context)!; @@ -165,10 +202,12 @@ class _ContactsTabState extends State { return Scaffold( body: Consumer( builder: (context, contactsProvider, child) { - final chatContacts = contactsProvider.chatContacts; - final repeaters = contactsProvider.repeaters; - final rooms = contactsProvider.rooms; - final channels = contactsProvider.channels; + final chatContacts = _sortContactsByDistance( + contactsProvider.chatContacts, + ); + final repeaters = _sortContactsByDistance(contactsProvider.repeaters); + final rooms = _sortContactsByDistance(contactsProvider.rooms); + final channels = _sortContactsByDistance(contactsProvider.channels); final pendingAdverts = contactsProvider.pendingAdverts; // Check if there are any displayable contacts (excluding channels) diff --git a/lib/widgets/contacts/contact_tile.dart b/lib/widgets/contacts/contact_tile.dart index 107e775..25eefe2 100644 --- a/lib/widgets/contacts/contact_tile.dart +++ b/lib/widgets/contacts/contact_tile.dart @@ -881,12 +881,23 @@ class ContactTile extends StatelessWidget { TextButton.icon( onPressed: isPingInProgress ? null - : () { + : () async { final connectionProvider = context .read(); - connectionProvider.requestTelemetry( - contact.publicKey, - zeroHop: true, + final result = await connectionProvider.smartPing( + contactPublicKey: contact.publicKey, + hasPath: contact.routeHasPath, + ); + + if (!context.mounted || result.success) { + return; + } + + ToastLogger.error( + context, + AppLocalizations.of( + context, + )!.pingFailed(contact.displayName), ); }, icon: isPingInProgress diff --git a/test/providers/contacts_provider_test.dart b/test/providers/contacts_provider_test.dart index ef72053..48e2300 100644 --- a/test/providers/contacts_provider_test.dart +++ b/test/providers/contacts_provider_test.dart @@ -128,6 +128,8 @@ void main() { expect(updated.displayLocation, isNotNull); expect(updated.displayLocation!.latitude, closeTo(45.0001, 0.0001)); expect(updated.displayLocation!.longitude, closeTo(13.9999, 0.0001)); + expect(updated.advLat, equals((45.0001 * 1e6).round())); + expect(updated.advLon, equals((13.9999 * 1e6).round())); }); test( @@ -232,6 +234,26 @@ void main() { expect(snapshot.location.latitude, closeTo(46.0569, 0.000001)); expect(snapshot.location.longitude, closeTo(14.5058, 0.000001)); }); + + test('persists last valid telemetry gps on the contact across reloads', () async { + final telemetryData = CayenneLppParser.createGpsData( + latitude: 45.0001, + longitude: 13.9999, + ); + + provider.updateTelemetry(publicKey.sublist(0, 6), telemetryData); + await Future.delayed(Duration.zero); + + final reloadedProvider = ContactsProvider(); + await reloadedProvider.initializeEarly(); + + final reloaded = reloadedProvider.findContactByKey(publicKey)!; + expect(reloaded.advLat, equals((45.0001 * 1e6).round())); + expect(reloaded.advLon, equals((13.9999 * 1e6).round())); + expect(reloaded.advertLocation, isNotNull); + expect(reloaded.advertLocation!.latitude, closeTo(45.0001, 0.0001)); + expect(reloaded.advertLocation!.longitude, closeTo(13.9999, 0.0001)); + }); }); group('ContactsProvider route updates', () { diff --git a/test/providers/helpers/ping_tracker_test.dart b/test/providers/helpers/ping_tracker_test.dart new file mode 100644 index 0000000..3891ae2 --- /dev/null +++ b/test/providers/helpers/ping_tracker_test.dart @@ -0,0 +1,27 @@ +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_sar_app/providers/helpers/ping_tracker.dart'; + +void main() { + Uint8List createPublicKey() => Uint8List.fromList( + List.generate(32, (index) => index + 1), + ); + + group('PingTracker', () { + test('completes pending ping when response uses public key prefix', () async { + final tracker = PingTracker(); + final publicKey = createPublicKey(); + + final pingFuture = tracker.trackPing( + publicKey: publicKey, + wasDirectAttempt: true, + ); + + tracker.markPingSuccessful(publicKey.sublist(0, 6)); + + await expectLater(pingFuture, completion(isTrue)); + expect(tracker.hasPendingPing(publicKey), isFalse); + }); + }); +}