mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Allow overriding contact name
This commit is contained in:
@@ -1627,6 +1627,22 @@ class ConnectionProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> factoryResetDevice() async {
|
||||
if (!_activeService.isConnected) {
|
||||
_error = 'Not connected to device';
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
_error = null;
|
||||
await _activeService.factoryReset();
|
||||
} catch (e) {
|
||||
_error = 'Failed to wipe device data: $e';
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Set advertised name
|
||||
Future<void> setAdvertName(String name) async {
|
||||
if (!_activeService.isConnected) {
|
||||
|
||||
@@ -58,6 +58,7 @@ class _RetainedRoute {
|
||||
/// Contacts Provider - manages contact list and telemetry
|
||||
class ContactsProvider with ChangeNotifier {
|
||||
static const double _firstHopFallbackOffsetMeters = 100.0;
|
||||
static const String autoGroupIdPrefix = 'auto_group_';
|
||||
final Map<String, Contact> _contacts = {};
|
||||
final List<SavedContactGroup> _savedContactGroups = <SavedContactGroup>[];
|
||||
final Map<String, PendingAdvert> _pendingAdverts = {};
|
||||
@@ -248,6 +249,8 @@ class ContactsProvider with ChangeNotifier {
|
||||
String sectionKey,
|
||||
String query, {
|
||||
String? label,
|
||||
List<String>? matchPrefixes,
|
||||
bool isAutoGroup = false,
|
||||
}) async {
|
||||
final normalizedQuery = _normalizeGroupQuery(query);
|
||||
if (normalizedQuery.isEmpty ||
|
||||
@@ -262,6 +265,8 @@ class ContactsProvider with ChangeNotifier {
|
||||
label: (label ?? query).trim(),
|
||||
query: query.trim(),
|
||||
createdAt: DateTime.now(),
|
||||
matchPrefixes: matchPrefixes,
|
||||
isAutoGroup: isAutoGroup,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -299,6 +304,18 @@ class ContactsProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> replaceAutoGroupsForSection(
|
||||
String sectionKey,
|
||||
List<SavedContactGroup> groups,
|
||||
) async {
|
||||
_savedContactGroups.removeWhere(
|
||||
(group) => group.sectionKey == sectionKey && group.isAutoGroup,
|
||||
);
|
||||
_savedContactGroups.addAll(groups);
|
||||
await _persistSavedGroups();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _persistSavedGroups() async {
|
||||
try {
|
||||
await _storageService.saveContactGroups(_savedContactGroups);
|
||||
@@ -486,6 +503,7 @@ class ContactsProvider with ChangeNotifier {
|
||||
if (existingContact == null) {
|
||||
var newContact = incomingContact.copyWith(
|
||||
isNew: true,
|
||||
nameOverride: existingContact?.nameOverride,
|
||||
telemetry: mergedTelemetry,
|
||||
outPathLen:
|
||||
retainedRoute?.signedEncodedPathLen ?? incomingContact.outPathLen,
|
||||
@@ -514,6 +532,7 @@ class ContactsProvider with ChangeNotifier {
|
||||
|
||||
var updatedContact = incomingContact.copyWith(
|
||||
isNew: false,
|
||||
nameOverride: existingContact.nameOverride,
|
||||
advertHistory: existingContact.advertHistory,
|
||||
telemetry: mergedTelemetry,
|
||||
outPathLen:
|
||||
@@ -1023,6 +1042,26 @@ class ContactsProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setContactNameOverride(String publicKeyHex, String? overrideName) {
|
||||
final contact = _contacts[publicKeyHex];
|
||||
if (contact == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final normalizedOverride = overrideName?.trim();
|
||||
final nextOverride =
|
||||
(normalizedOverride == null || normalizedOverride.isEmpty)
|
||||
? null
|
||||
: normalizedOverride;
|
||||
if (contact.nameOverride == nextOverride) {
|
||||
return;
|
||||
}
|
||||
|
||||
_contacts[publicKeyHex] = contact.copyWith(nameOverride: nextOverride);
|
||||
_persistContacts();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Add or refresh a pending advert entry from PUSH_CODE_ADVERT (0x80).
|
||||
/// Excludes self key and existing contacts.
|
||||
void addPendingAdvert(Uint8List publicKey, {Uint8List? devicePublicKey}) {
|
||||
|
||||
@@ -11,6 +11,8 @@ enum DrawingMode { none, line, rectangle, measure }
|
||||
/// Provider for managing map drawings
|
||||
class DrawingProvider with ChangeNotifier {
|
||||
static const String _storageKey = 'map_drawings';
|
||||
static const String _showReceivedDrawingsKey = 'map_show_received_drawings';
|
||||
static const String _showSarMarkersKey = 'map_show_sar_markers';
|
||||
|
||||
// Drawing state
|
||||
DrawingMode _drawingMode = DrawingMode.none;
|
||||
@@ -57,6 +59,7 @@ class DrawingProvider with ChangeNotifier {
|
||||
|
||||
/// Initialize and load saved drawings
|
||||
Future<void> initialize() async {
|
||||
await _loadPreferences();
|
||||
await _loadDrawings();
|
||||
}
|
||||
|
||||
@@ -77,15 +80,30 @@ class DrawingProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
/// Toggle visibility of received drawings
|
||||
void toggleReceivedDrawings() {
|
||||
Future<void> toggleReceivedDrawings() async {
|
||||
_showReceivedDrawings = !_showReceivedDrawings;
|
||||
notifyListeners();
|
||||
await _savePreferences();
|
||||
}
|
||||
|
||||
/// Toggle visibility of SAR markers
|
||||
void toggleSarMarkers() {
|
||||
Future<void> toggleSarMarkers() async {
|
||||
_showSarMarkers = !_showSarMarkers;
|
||||
notifyListeners();
|
||||
await _savePreferences();
|
||||
}
|
||||
|
||||
Future<void> _loadPreferences() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_showReceivedDrawings = prefs.getBool(_showReceivedDrawingsKey) ?? true;
|
||||
_showSarMarkers = prefs.getBool(_showSarMarkersKey) ?? true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _savePreferences() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_showReceivedDrawingsKey, _showReceivedDrawings);
|
||||
await prefs.setBool(_showSarMarkersKey, _showSarMarkers);
|
||||
}
|
||||
|
||||
/// Start drawing a line
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
@@ -6,6 +8,10 @@ import '../models/location_trail.dart';
|
||||
import '../models/map_drawing.dart';
|
||||
|
||||
class MapProvider with ChangeNotifier {
|
||||
MapProvider() {
|
||||
unawaited(_loadInitialState());
|
||||
}
|
||||
|
||||
LatLng? _targetLocation;
|
||||
double? _targetZoom;
|
||||
bool _shouldAnimate = false;
|
||||
@@ -33,6 +39,7 @@ class MapProvider with ChangeNotifier {
|
||||
|
||||
// Contact trail toggles
|
||||
bool _showAllContactTrails = true; // Default to showing all contact trails
|
||||
bool _hideRepeatersOnMap = false;
|
||||
|
||||
// Imported trail (from GPX)
|
||||
LocationTrail? _importedTrail;
|
||||
@@ -67,6 +74,7 @@ class MapProvider with ChangeNotifier {
|
||||
|
||||
// Contact trail getters
|
||||
bool get showAllContactTrails => _showAllContactTrails;
|
||||
bool get hideRepeatersOnMap => _hideRepeatersOnMap;
|
||||
|
||||
// Imported trail getters
|
||||
LocationTrail? get importedTrail => _importedTrail;
|
||||
@@ -350,6 +358,11 @@ class MapProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _loadInitialState() async {
|
||||
await Future.wait([loadOverlayState(), loadTrailSettings()]);
|
||||
await loadRepeaterVisibilitySettings();
|
||||
}
|
||||
|
||||
/// Save overlay state to SharedPreferences
|
||||
Future<void> _saveOverlayState() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
@@ -386,6 +399,20 @@ class MapProvider with ChangeNotifier {
|
||||
await prefs.setBool('map_show_all_contact_trails', _showAllContactTrails);
|
||||
}
|
||||
|
||||
Future<void> setHideRepeatersOnMap(bool hide) async {
|
||||
if (_hideRepeatersOnMap == hide) return;
|
||||
_hideRepeatersOnMap = hide;
|
||||
notifyListeners();
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool('map_hide_repeaters', _hideRepeatersOnMap);
|
||||
}
|
||||
|
||||
Future<void> loadRepeaterVisibilitySettings() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_hideRepeatersOnMap = prefs.getBool('map_hide_repeaters') ?? false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Set imported trail (from GPX import)
|
||||
void setImportedTrail(LocationTrail trail) {
|
||||
_importedTrail = trail;
|
||||
|
||||
@@ -22,6 +22,7 @@ import 'helpers/message_retry_manager.dart';
|
||||
class MessagesProvider with ChangeNotifier {
|
||||
final List<Message> _messages = [];
|
||||
final Map<String, SarMarker> _sarMarkers = {};
|
||||
final Set<String> _removedSarMarkerIds = <String>{};
|
||||
final MessageStorageService _storageService = MessageStorageService();
|
||||
final NotificationService _notificationService = NotificationService();
|
||||
bool _isInitialized = false;
|
||||
@@ -125,6 +126,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
_messages.where((m) => m.isSystemMessage).toList();
|
||||
|
||||
List<SarMarker> get sarMarkers => _sarMarkers.values.toList();
|
||||
Set<String> get removedSarMarkerIds => Set.unmodifiable(_removedSarMarkerIds);
|
||||
|
||||
List<SarMarker> get foundPersonMarkers =>
|
||||
sarMarkers.where((m) => m.type == SarMarkerType.foundPerson).toList();
|
||||
@@ -298,6 +300,8 @@ class MessagesProvider with ChangeNotifier {
|
||||
.loadMessageTransferDetails();
|
||||
final storedRouteMetadata = await _storageService
|
||||
.loadMessageRouteMetadata();
|
||||
final storedRemovedSarMarkerIds = await _storageService
|
||||
.loadRemovedSarMarkerIds();
|
||||
_messageContactLocations
|
||||
..clear()
|
||||
..addAll(storedContactLocations);
|
||||
@@ -310,6 +314,9 @@ class MessagesProvider with ChangeNotifier {
|
||||
_messageRouteMetadata
|
||||
..clear()
|
||||
..addAll(storedRouteMetadata);
|
||||
_removedSarMarkerIds
|
||||
..clear()
|
||||
..addAll(storedRemovedSarMarkerIds);
|
||||
|
||||
// Add stored messages with enhancement to ensure SAR detection
|
||||
for (final message in storedMessages) {
|
||||
@@ -357,7 +364,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
// Extract SAR markers
|
||||
if (enhancedMessage.isSarMarker) {
|
||||
final marker = enhancedMessage.toSarMarker();
|
||||
if (marker != null) {
|
||||
if (marker != null && !_removedSarMarkerIds.contains(marker.id)) {
|
||||
_sarMarkers[marker.id] = marker;
|
||||
}
|
||||
}
|
||||
@@ -537,29 +544,22 @@ class MessagesProvider with ChangeNotifier {
|
||||
// - Mesh network retransmissions
|
||||
// - Multiple paths in the network
|
||||
// - Syncing messages from device queue
|
||||
if (_isDuplicate(finalMessage)) {
|
||||
final duplicateIndex = _findDuplicateMessageIndex(finalMessage);
|
||||
if (duplicateIndex != -1) {
|
||||
debugPrint(
|
||||
'⚠️ [MessagesProvider] Duplicate message detected, skipping: ${finalMessage.id}',
|
||||
);
|
||||
debugPrint(
|
||||
' Text: ${finalMessage.text.substring(0, finalMessage.text.length > 50 ? 50 : finalMessage.text.length)}...',
|
||||
);
|
||||
final existingIndex = _messages.indexWhere(
|
||||
(existing) =>
|
||||
existing.messageType == finalMessage.messageType &&
|
||||
existing.senderTimestamp == finalMessage.senderTimestamp &&
|
||||
existing.text == finalMessage.text,
|
||||
);
|
||||
if (existingIndex != -1) {
|
||||
final existingId = _messages[existingIndex].id;
|
||||
if (contactLocationSnapshot != null) {
|
||||
_messageContactLocations[existingId] = contactLocationSnapshot;
|
||||
}
|
||||
if (receptionDetailsSnapshot != null) {
|
||||
_messageReceptionDetails[existingId] = receptionDetailsSnapshot;
|
||||
}
|
||||
_persistMessages();
|
||||
final existingId = _messages[duplicateIndex].id;
|
||||
if (contactLocationSnapshot != null) {
|
||||
_messageContactLocations[existingId] = contactLocationSnapshot;
|
||||
}
|
||||
if (receptionDetailsSnapshot != null) {
|
||||
_messageReceptionDetails[existingId] = receptionDetailsSnapshot;
|
||||
}
|
||||
_persistMessages();
|
||||
return; // Skip duplicate
|
||||
}
|
||||
|
||||
@@ -574,7 +574,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
// If it's a SAR marker message, extract and store the marker
|
||||
if (finalMessage.isSarMarker) {
|
||||
final marker = finalMessage.toSarMarker();
|
||||
if (marker != null) {
|
||||
if (marker != null && !_removedSarMarkerIds.contains(marker.id)) {
|
||||
_sarMarkers[marker.id] = marker;
|
||||
|
||||
// Trigger urgent notification for received SAR messages (not sent by user)
|
||||
@@ -598,50 +598,48 @@ class MessagesProvider with ChangeNotifier {
|
||||
/// Messages are considered duplicates if they have:
|
||||
/// 1. Same sender public key prefix (for contact messages)
|
||||
/// 2. Same channel index (for channel messages)
|
||||
/// 3. Same sender timestamp
|
||||
/// 4. Same text content
|
||||
/// 3. Same text content
|
||||
///
|
||||
/// Note: Sent messages (isSentMessage=true) are NEVER duplicates
|
||||
/// because they can be retried with different message IDs
|
||||
bool _isDuplicate(Message message) {
|
||||
int _findDuplicateMessageIndex(Message message) {
|
||||
// Sent messages (our own messages) should never be considered duplicates
|
||||
// They can be retried multiple times with different IDs
|
||||
if (message.isSentMessage) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (int index = 0; index < _messages.length; index++) {
|
||||
final existing = _messages[index];
|
||||
if (!_matchesDuplicateScope(existing, message) ||
|
||||
existing.text != message.text) {
|
||||
continue;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool _matchesDuplicateScope(Message existing, Message message) {
|
||||
if (existing.messageType != message.messageType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return _messages.any((existing) {
|
||||
// Check message type matches
|
||||
if (existing.messageType != message.messageType) {
|
||||
if (message.isContactMessage) {
|
||||
return existing.senderKeyShort == message.senderKeyShort;
|
||||
}
|
||||
|
||||
if (message.isChannelMessage) {
|
||||
if (existing.channelIdx != message.channelIdx) {
|
||||
return false;
|
||||
}
|
||||
final existingSender = existing.senderKeyShort ?? existing.senderName;
|
||||
final incomingSender = message.senderKeyShort ?? message.senderName;
|
||||
return existingSender == incomingSender;
|
||||
}
|
||||
|
||||
// Check sender matches
|
||||
if (message.isContactMessage) {
|
||||
// For contact messages, compare sender public key prefix
|
||||
if (existing.senderKeyShort != message.senderKeyShort) {
|
||||
return false;
|
||||
}
|
||||
} else if (message.isChannelMessage) {
|
||||
// For channel messages, compare channel index
|
||||
if (existing.channelIdx != message.channelIdx) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check timestamp matches (sender timestamp is the unique identifier from the sender)
|
||||
if (existing.senderTimestamp != message.senderTimestamp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check text content matches
|
||||
if (existing.text != message.text) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// All criteria match - this is a duplicate
|
||||
return true;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Add multiple messages
|
||||
@@ -654,7 +652,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
final enhancedMessage = SarMessageParser.enhanceMessage(message);
|
||||
|
||||
// Check for duplicates
|
||||
if (_isDuplicate(enhancedMessage)) {
|
||||
if (_findDuplicateMessageIndex(enhancedMessage) != -1) {
|
||||
duplicateCount++;
|
||||
continue; // Skip duplicate
|
||||
}
|
||||
@@ -664,7 +662,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
|
||||
if (enhancedMessage.isSarMarker) {
|
||||
final marker = enhancedMessage.toSarMarker();
|
||||
if (marker != null) {
|
||||
if (marker != null && !_removedSarMarkerIds.contains(marker.id)) {
|
||||
_sarMarkers[marker.id] = marker;
|
||||
}
|
||||
}
|
||||
@@ -886,17 +884,35 @@ class MessagesProvider with ChangeNotifier {
|
||||
return _sarMarkers[id];
|
||||
}
|
||||
|
||||
Message? getMessageById(String id) {
|
||||
final index = _messages.indexWhere((message) => message.id == id);
|
||||
if (index == -1) return null;
|
||||
return _messages[index];
|
||||
}
|
||||
|
||||
/// Get recent SAR markers (within last hour)
|
||||
List<SarMarker> getRecentSarMarkers() {
|
||||
return sarMarkers.where((m) => m.isRecent).toList();
|
||||
}
|
||||
|
||||
/// Remove a SAR marker
|
||||
void removeSarMarker(String id) {
|
||||
Future<void> removeSarMarker(String id) async {
|
||||
_sarMarkers.remove(id);
|
||||
_removedSarMarkerIds.add(id);
|
||||
await _storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> removeSarMarkerPermanently(String id) async {
|
||||
final hasBackingMessage = _messages.any((message) => message.id == id);
|
||||
if (hasBackingMessage) {
|
||||
deleteMessage(id);
|
||||
return;
|
||||
}
|
||||
|
||||
await removeSarMarker(id);
|
||||
}
|
||||
|
||||
/// Mark all messages as read
|
||||
void markAllAsRead() {
|
||||
bool hasChanges = false;
|
||||
@@ -984,6 +1000,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
final marker = message.toSarMarker();
|
||||
if (marker != null) {
|
||||
_sarMarkers.remove(marker.id);
|
||||
_removedSarMarkerIds.add(marker.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1006,6 +1023,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
debugPrint('🗑️ [MessagesProvider] Message $messageId deleted');
|
||||
|
||||
_persistMessages();
|
||||
unawaited(_storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds));
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -1030,17 +1048,21 @@ class MessagesProvider with ChangeNotifier {
|
||||
void clearMessages() {
|
||||
_messages.clear();
|
||||
_sarMarkers.clear();
|
||||
_removedSarMarkerIds.clear();
|
||||
_messageContactLocations.clear();
|
||||
_messageReceptionDetails.clear();
|
||||
_messageTransferDetails.clear();
|
||||
_messageRouteMetadata.clear();
|
||||
_persistMessages();
|
||||
unawaited(_storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Clear all SAR markers
|
||||
void clearSarMarkers() {
|
||||
Future<void> clearSarMarkers() async {
|
||||
_removedSarMarkerIds.addAll(_sarMarkers.keys);
|
||||
_sarMarkers.clear();
|
||||
await _storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -1048,11 +1070,13 @@ class MessagesProvider with ChangeNotifier {
|
||||
void clearAll() {
|
||||
_messages.clear();
|
||||
_sarMarkers.clear();
|
||||
_removedSarMarkerIds.clear();
|
||||
_messageContactLocations.clear();
|
||||
_messageReceptionDetails.clear();
|
||||
_messageTransferDetails.clear();
|
||||
_messageRouteMetadata.clear();
|
||||
_persistMessages();
|
||||
unawaited(_storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -1213,7 +1237,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
// Check for duplicates (shouldn't happen for sent messages, but be safe)
|
||||
if (_isDuplicate(enhancedMessage)) {
|
||||
if (_findDuplicateMessageIndex(enhancedMessage) != -1) {
|
||||
debugPrint(
|
||||
'⚠️ [MessagesProvider] Duplicate sent message detected, skipping: ${enhancedMessage.id}',
|
||||
);
|
||||
@@ -1238,7 +1262,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
// If it's a SAR marker message, extract and store the marker
|
||||
if (sendingMessage.isSarMarker) {
|
||||
final marker = sendingMessage.toSarMarker();
|
||||
if (marker != null) {
|
||||
if (marker != null && !_removedSarMarkerIds.contains(marker.id)) {
|
||||
debugPrint(' ✅ SAR Marker created:');
|
||||
debugPrint(' marker.id: ${marker.id}');
|
||||
debugPrint(' marker.notes: "${marker.notes}"');
|
||||
|
||||
Reference in New Issue
Block a user