mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Add avatars display on map
This commit is contained in:
@@ -423,9 +423,16 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
debugPrint(' Old lastAdvert: ${contact.lastAdvert}');
|
debugPrint(' Old lastAdvert: ${contact.lastAdvert}');
|
||||||
debugPrint(' New lastAdvert: $currentTimestamp');
|
debugPrint(' New lastAdvert: $currentTimestamp');
|
||||||
|
|
||||||
|
final persistedGps = _getValidGpsOrNull(telemetry.gpsLocation);
|
||||||
final updatedContact = contact.copyWith(
|
final updatedContact = contact.copyWith(
|
||||||
telemetry: telemetry,
|
telemetry: telemetry,
|
||||||
lastAdvert: currentTimestamp, // Update last seen time
|
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;
|
_contacts[contact.publicKeyHex] = updatedContact;
|
||||||
debugPrint(' ✅ Updated contact in map (with new lastAdvert)');
|
debugPrint(' ✅ Updated contact in map (with new lastAdvert)');
|
||||||
@@ -476,6 +483,10 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
return incomingGps == null;
|
return incomingGps == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int _coordinateToAdvertMicrodegrees(double coordinate) {
|
||||||
|
return (coordinate * 1e6).round();
|
||||||
|
}
|
||||||
|
|
||||||
/// Find contact by public key prefix (6 bytes)
|
/// Find contact by public key prefix (6 bytes)
|
||||||
Contact? _findContactByPrefix(Uint8List prefix) {
|
Contact? _findContactByPrefix(Uint8List prefix) {
|
||||||
if (prefix.length < 6) return null;
|
if (prefix.length < 6) return null;
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ class PingTracker {
|
|||||||
/// Mark a ping as successful (response received)
|
/// Mark a ping as successful (response received)
|
||||||
/// Should be called when telemetry response arrives
|
/// Should be called when telemetry response arrives
|
||||||
void markPingSuccessful(Uint8List publicKey) {
|
void markPingSuccessful(Uint8List publicKey) {
|
||||||
final String keyHex = _publicKeyToHex(publicKey);
|
final requestKey = _findMatchingPendingPingKey(publicKey);
|
||||||
final request = _pendingPings.remove(keyHex);
|
final request = requestKey != null ? _pendingPings.remove(requestKey) : null;
|
||||||
|
|
||||||
if (request != null) {
|
if (request != null) {
|
||||||
request.cancel();
|
request.cancel();
|
||||||
@@ -81,6 +81,23 @@ class PingTracker {
|
|||||||
String _publicKeyToHex(Uint8List publicKey) {
|
String _publicKeyToHex(Uint8List publicKey) {
|
||||||
return publicKey.map((b) => b.toRadixString(16).padLeft(2, '0')).join('');
|
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
|
/// Internal class to track a single ping request
|
||||||
|
|||||||
@@ -119,6 +119,43 @@ class _ContactsTabState extends State<ContactsTab> {
|
|||||||
return l10n.daysAgo(diff.inDays);
|
return l10n.daysAgo(diff.inDays);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Contact> _sortContactsByDistance(List<Contact> contacts) {
|
||||||
|
final sorted = List<Contact>.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
|
/// Show the add channel dialog
|
||||||
Future<void> _showAddChannelDialog(BuildContext context) async {
|
Future<void> _showAddChannelDialog(BuildContext context) async {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
@@ -165,10 +202,12 @@ class _ContactsTabState extends State<ContactsTab> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Consumer<ContactsProvider>(
|
body: Consumer<ContactsProvider>(
|
||||||
builder: (context, contactsProvider, child) {
|
builder: (context, contactsProvider, child) {
|
||||||
final chatContacts = contactsProvider.chatContacts;
|
final chatContacts = _sortContactsByDistance(
|
||||||
final repeaters = contactsProvider.repeaters;
|
contactsProvider.chatContacts,
|
||||||
final rooms = contactsProvider.rooms;
|
);
|
||||||
final channels = contactsProvider.channels;
|
final repeaters = _sortContactsByDistance(contactsProvider.repeaters);
|
||||||
|
final rooms = _sortContactsByDistance(contactsProvider.rooms);
|
||||||
|
final channels = _sortContactsByDistance(contactsProvider.channels);
|
||||||
final pendingAdverts = contactsProvider.pendingAdverts;
|
final pendingAdverts = contactsProvider.pendingAdverts;
|
||||||
|
|
||||||
// Check if there are any displayable contacts (excluding channels)
|
// Check if there are any displayable contacts (excluding channels)
|
||||||
|
|||||||
@@ -881,12 +881,23 @@ class ContactTile extends StatelessWidget {
|
|||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: isPingInProgress
|
onPressed: isPingInProgress
|
||||||
? null
|
? null
|
||||||
: () {
|
: () async {
|
||||||
final connectionProvider = context
|
final connectionProvider = context
|
||||||
.read<ConnectionProvider>();
|
.read<ConnectionProvider>();
|
||||||
connectionProvider.requestTelemetry(
|
final result = await connectionProvider.smartPing(
|
||||||
contact.publicKey,
|
contactPublicKey: contact.publicKey,
|
||||||
zeroHop: true,
|
hasPath: contact.routeHasPath,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!context.mounted || result.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ToastLogger.error(
|
||||||
|
context,
|
||||||
|
AppLocalizations.of(
|
||||||
|
context,
|
||||||
|
)!.pingFailed(contact.displayName),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: isPingInProgress
|
icon: isPingInProgress
|
||||||
|
|||||||
@@ -128,6 +128,8 @@ void main() {
|
|||||||
expect(updated.displayLocation, isNotNull);
|
expect(updated.displayLocation, isNotNull);
|
||||||
expect(updated.displayLocation!.latitude, closeTo(45.0001, 0.0001));
|
expect(updated.displayLocation!.latitude, closeTo(45.0001, 0.0001));
|
||||||
expect(updated.displayLocation!.longitude, closeTo(13.9999, 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(
|
test(
|
||||||
@@ -232,6 +234,26 @@ void main() {
|
|||||||
expect(snapshot.location.latitude, closeTo(46.0569, 0.000001));
|
expect(snapshot.location.latitude, closeTo(46.0569, 0.000001));
|
||||||
expect(snapshot.location.longitude, closeTo(14.5058, 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<void>.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', () {
|
group('ContactsProvider route updates', () {
|
||||||
|
|||||||
27
test/providers/helpers/ping_tracker_test.dart
Normal file
27
test/providers/helpers/ping_tracker_test.dart
Normal file
@@ -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<int>.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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user