From 1fba5312a234d19f7c617f04ec49937d3da97c82 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Thu, 12 Mar 2026 00:14:48 -0700 Subject: [PATCH 1/3] Refactor storage classes to include companion's public key (#277) * Refactor storage classes to include public key handling and improve data loading/saving logic * Remove redundant publicKeyHex handling from ContactDiscoveryStore and fix key reference in saveContacts method * Remove unused app_logger import from ContactDiscoveryStore * Add warning log for empty publicKeyHex in saveChannelMessages method * Add warning log for empty publicKeyHex in clearMessages method * Migrate legacy storage keys to scoped keys across multiple stores * Remove legacy unscoped keys during migration in storage classes * Update lib/storage/contact_store.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/connector/meshcore_connector.dart | 26 ++++++++++++-- lib/screens/channels_screen.dart | 2 ++ lib/storage/channel_message_store.dart | 46 ++++++++++++++++++++---- lib/storage/channel_order_store.dart | 41 +++++++++++++++++---- lib/storage/channel_settings_store.dart | 38 ++++++++++++++++++-- lib/storage/channel_store.dart | 38 +++++++++++++++++--- lib/storage/community_store.dart | 33 +++++++++++++++-- lib/storage/contact_discovery_store.dart | 6 ++-- lib/storage/contact_group_store.dart | 39 +++++++++++++++++--- lib/storage/contact_settings_store.dart | 38 ++++++++++++++++++-- lib/storage/contact_store.dart | 39 +++++++++++++++++--- lib/storage/message_store.dart | 44 ++++++++++++++++++++--- lib/storage/unread_store.dart | 39 +++++++++++++++++--- 13 files changed, 378 insertions(+), 51 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 89aeca0..2ea09ca 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -291,6 +291,7 @@ class MeshCoreConnector extends ChangeNotifier { bool get isLoadingChannels => _isLoadingChannels; Stream get receivedFrames => _receivedFramesController.stream; Uint8List? get selfPublicKey => _selfPublicKey; + String get selfPublicKeyHex => pubKeyToHex(_selfPublicKey ?? Uint8List(0)); String? get selfName => _selfName; double? get selfLatitude => _selfLatitude; double? get selfLongitude => _selfLongitude; @@ -663,6 +664,7 @@ class MeshCoreConnector extends ChangeNotifier { // Initialize notification service _notificationService.initialize(); _loadChannelOrder(); + _loadDiscoveredContactCache(); // Initialize retry service callbacks _retryService?.initialize( @@ -691,7 +693,7 @@ class MeshCoreConnector extends ChangeNotifier { } } - Future loadDiscoveredContactCache() async { + Future _loadDiscoveredContactCache() async { final cached = await _discoveryContactStore.loadContacts(); _discoveredContacts ..clear() @@ -1193,7 +1195,6 @@ class MeshCoreConnector extends ChangeNotifier { await _requestDeviceInfo(); _startBatteryPolling(); - unawaited(loadDiscoveredContactCache()); final gotSelfInfo = await _waitForSelfInfo( timeout: const Duration(seconds: 3), @@ -2489,6 +2490,27 @@ class MeshCoreConnector extends ChangeNotifier { selfName.isNotEmpty) { _usbManager.updateConnectedLabel(selfName); } + + //set all the stores' public key so they can load the correct data + _channelMessageStore.setPublicKeyHex = selfPublicKeyHex; + _messageStore.setPublicKeyHex = selfPublicKeyHex; + _channelOrderStore.setPublicKeyHex = selfPublicKeyHex; + _channelSettingsStore.setPublicKeyHex = selfPublicKeyHex; + _contactSettingsStore.setPublicKeyHex = selfPublicKeyHex; + _contactStore.setPublicKeyHex = selfPublicKeyHex; + _channelStore.setPublicKeyHex = selfPublicKeyHex; + _unreadStore.setPublicKeyHex = selfPublicKeyHex; + + // Now that we have self info, we can load all the persisted data for this node + _loadChannelOrder(); + loadContactCache(); + loadChannelSettings(); + loadCachedChannels(); + + // Load persisted channel messages + loadAllChannelMessages(); + loadUnreadState(); + _awaitingSelfInfo = false; _selfInfoRetryTimer?.cancel(); _selfInfoRetryTimer = null; diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 582fee7..00820ed 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -106,7 +106,9 @@ class _ChannelsScreenState extends State @override Widget build(BuildContext context) { final connector = context.watch(); + final channelMessageStore = ChannelMessageStore(); + channelMessageStore.setPublicKeyHex = connector.selfPublicKeyHex; // Auto-navigate back to scanner if disconnected if (!checkConnectionAndNavigate(connector)) { diff --git a/lib/storage/channel_message_store.dart b/lib/storage/channel_message_store.dart index 1151514..9c9f7e8 100644 --- a/lib/storage/channel_message_store.dart +++ b/lib/storage/channel_message_store.dart @@ -1,5 +1,7 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:meshcore_open/utils/app_logger.dart'; + import '../models/channel_message.dart'; import '../helpers/smaz.dart'; import 'prefs_manager.dart'; @@ -7,13 +9,25 @@ import 'prefs_manager.dart'; class ChannelMessageStore { static const String _keyPrefix = 'channel_messages_'; + String publicKeyHex = ''; + set setPublicKeyHex(String value) => + publicKeyHex = value.length > 10 ? value.substring(0, 10) : ''; + + String get keyFor => '$_keyPrefix$publicKeyHex'; + /// Save messages for a specific channel Future saveChannelMessages( int channelIndex, List messages, ) async { + if (publicKeyHex.isEmpty) { + appLogger.warn( + 'Public key hex is not set. Cannot save channel messages.', + ); + return; + } final prefs = PrefsManager.instance; - final key = '$_keyPrefix$channelIndex'; + final key = '$keyFor$channelIndex'; // Convert messages to JSON final jsonList = messages.map((msg) => _messageToJson(msg)).toList(); @@ -24,11 +38,31 @@ class ChannelMessageStore { /// Load messages for a specific channel Future> loadChannelMessages(int channelIndex) async { + if (publicKeyHex.isEmpty) { + appLogger.warn( + 'Public key hex is not set. Cannot load channel messages.', + ); + return []; + } final prefs = PrefsManager.instance; - final key = '$_keyPrefix$channelIndex'; + final key = '$keyFor$channelIndex'; - final jsonString = prefs.getString(key); - if (jsonString == null) return []; + String? jsonString = prefs.getString(_keyPrefix); + if (jsonString == null || jsonString.isEmpty) { + // Attempt migration from legacy unscoped key on first load + final legacyJsonString = prefs.getString(_keyPrefix); + prefs.remove(_keyPrefix); + if (legacyJsonString != null && legacyJsonString.isNotEmpty) { + appLogger.info( + 'Migrating channel messages from legacy key $_keyPrefix to scoped key $key', + ); + await prefs.setString(key, legacyJsonString); + jsonString = legacyJsonString; + } + } + if (jsonString == null || jsonString.isEmpty) { + return []; + } try { final jsonList = jsonDecode(jsonString) as List; @@ -42,14 +76,14 @@ class ChannelMessageStore { /// Clear messages for a specific channel Future clearChannelMessages(int channelIndex) async { final prefs = PrefsManager.instance; - final key = '$_keyPrefix$channelIndex'; + final key = '$keyFor$channelIndex'; await prefs.remove(key); } /// Clear all channel messages Future clearAllChannelMessages() async { final prefs = PrefsManager.instance; - final keys = prefs.getKeys().where((k) => k.startsWith(_keyPrefix)); + final keys = prefs.getKeys().where((k) => k.startsWith(keyFor)); for (var key in keys) { await prefs.remove(key); } diff --git a/lib/storage/channel_order_store.dart b/lib/storage/channel_order_store.dart index b9657c4..48a80f2 100644 --- a/lib/storage/channel_order_store.dart +++ b/lib/storage/channel_order_store.dart @@ -1,20 +1,49 @@ import 'dart:convert'; +import '../utils/app_logger.dart'; import 'prefs_manager.dart'; class ChannelOrderStore { - static const String _key = 'channel_order'; + static const String _keyPrefix = 'channel_order_'; + + String publicKeyHex = ''; + set setPublicKeyHex(String value) => + publicKeyHex = value.length > 10 ? value.substring(0, 10) : ''; + + String get keyFor => '$_keyPrefix$publicKeyHex'; Future saveChannelOrder(List order) async { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot save channel order.'); + return; + } final prefs = PrefsManager.instance; - await prefs.setString(_key, jsonEncode(order)); + await prefs.setString(keyFor, jsonEncode(order)); } Future> loadChannelOrder() async { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot load channel order.'); + return []; + } final prefs = PrefsManager.instance; - final raw = prefs.getString(_key); - if (raw == null || raw.isEmpty) return []; + String? jsonString = prefs.getString(_keyPrefix); + if (jsonString == null || jsonString.isEmpty) { + // Attempt migration from legacy unscoped key on first load + final legacyJsonString = prefs.getString(_keyPrefix); + prefs.remove(_keyPrefix); + if (legacyJsonString != null && legacyJsonString.isNotEmpty) { + appLogger.info( + 'Migrating channel order from legacy key $_keyPrefix to scoped key $keyFor', + ); + await prefs.setString(keyFor, legacyJsonString); + jsonString = legacyJsonString; + } + } + if (jsonString == null || jsonString.isEmpty) { + return []; + } try { - final decoded = jsonDecode(raw); + final decoded = jsonDecode(jsonString); if (decoded is List) { return decoded .map((value) => value is int ? value : int.tryParse('$value')) @@ -24,7 +53,7 @@ class ChannelOrderStore { } catch (_) { // fall through to legacy parse } - return raw + return jsonString .split(',') .map((value) => int.tryParse(value)) .whereType() diff --git a/lib/storage/channel_settings_store.dart b/lib/storage/channel_settings_store.dart index eee97aa..3b639cd 100644 --- a/lib/storage/channel_settings_store.dart +++ b/lib/storage/channel_settings_store.dart @@ -1,17 +1,49 @@ +import '../utils/app_logger.dart'; import 'prefs_manager.dart'; class ChannelSettingsStore { - static const String _smazKeyPrefix = 'channel_smaz_'; + static const String _keyPrefix = 'channel_smaz_'; + + String publicKeyHex = ''; + set setPublicKeyHex(String value) => + publicKeyHex = value.length > 10 ? value.substring(0, 10) : ''; + + String get keyFor => '$_keyPrefix$publicKeyHex'; Future loadSmazEnabled(int channelIndex) async { + if (publicKeyHex.isEmpty) { + appLogger.warn( + 'Public key hex is not set. Cannot load channel settings.', + ); + return false; + } final prefs = PrefsManager.instance; - final key = '$_smazKeyPrefix$channelIndex'; + final key = '$keyFor$channelIndex'; + final oldKey = '$_keyPrefix$channelIndex'; + bool? enabled = prefs.getBool(key); + if (enabled == null) { + // Attempt migration from legacy unscoped key on first load + enabled = prefs.getBool(oldKey); + prefs.remove(oldKey); + if (enabled != null) { + appLogger.info( + 'Migrating channel settings from legacy key $oldKey to scoped key $key', + ); + await prefs.setBool(key, enabled); + } + } return prefs.getBool(key) ?? false; } Future saveSmazEnabled(int channelIndex, bool enabled) async { + if (publicKeyHex.isEmpty) { + appLogger.warn( + 'Public key hex is not set. Cannot save channel settings.', + ); + return; + } final prefs = PrefsManager.instance; - final key = '$_smazKeyPrefix$channelIndex'; + final key = '$keyFor$channelIndex'; await prefs.setBool(key, enabled); } } diff --git a/lib/storage/channel_store.dart b/lib/storage/channel_store.dart index eaa7a61..1bad7e3 100644 --- a/lib/storage/channel_store.dart +++ b/lib/storage/channel_store.dart @@ -2,18 +2,42 @@ import 'dart:convert'; import 'dart:typed_data'; import '../models/channel.dart'; +import '../utils/app_logger.dart'; import 'prefs_manager.dart'; class ChannelStore { - static const String _key = 'channels'; + static const String _keyPrefix = 'channels'; + String publicKeyHex = ''; + set setPublicKeyHex(String value) => + publicKeyHex = value.length >= 10 ? value.substring(0, 10) : ''; + + String get keyFor => '$_keyPrefix$publicKeyHex'; Future> loadChannels() async { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot load channels.'); + return []; + } final prefs = PrefsManager.instance; - final jsonStr = prefs.getString(_key); - if (jsonStr == null) return []; + String? jsonString = prefs.getString(_keyPrefix); + if (jsonString == null || jsonString.isEmpty) { + // Attempt migration from legacy unscoped key on first load + final legacyJsonString = prefs.getString(_keyPrefix); + prefs.remove(_keyPrefix); + if (legacyJsonString != null && legacyJsonString.isNotEmpty) { + appLogger.info( + 'Migrating channel messages from legacy key $_keyPrefix to scoped key $keyFor', + ); + await prefs.setString(keyFor, legacyJsonString); + jsonString = legacyJsonString; + } + } + if (jsonString == null || jsonString.isEmpty) { + return []; + } try { - final jsonList = jsonDecode(jsonStr) as List; + final jsonList = jsonDecode(jsonString) as List; return jsonList .map((entry) => _fromJson(entry as Map)) .toList(); @@ -23,9 +47,13 @@ class ChannelStore { } Future saveChannels(List channels) async { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot save channels.'); + return; + } final prefs = PrefsManager.instance; final jsonList = channels.map(_toJson).toList(); - await prefs.setString(_key, jsonEncode(jsonList)); + await prefs.setString(keyFor, jsonEncode(jsonList)); } Map _toJson(Channel channel) { diff --git a/lib/storage/community_store.dart b/lib/storage/community_store.dart index a81cccd..c7198e7 100644 --- a/lib/storage/community_store.dart +++ b/lib/storage/community_store.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import '../models/community.dart'; +import '../utils/app_logger.dart'; import 'prefs_manager.dart'; /// Persists communities to local storage using SharedPreferences. @@ -9,12 +10,34 @@ import 'prefs_manager.dart'; /// Each community contains its secret K, so this data should /// be considered sensitive (though device encryption handles security). class CommunityStore { - static const String _communitiesKey = 'communities_v1'; + static const String _keyPrefix = 'communities_v1'; + + String publicKeyHex = ''; + set setPublicKeyHex(String value) => + publicKeyHex = value.length > 10 ? value.substring(0, 10) : ''; + + String get keyFor => '$_keyPrefix$publicKeyHex'; /// Load all communities from storage Future> loadCommunities() async { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot load communities.'); + return []; + } final prefs = PrefsManager.instance; - final jsonString = prefs.getString(_communitiesKey); + String? jsonString = prefs.getString(_keyPrefix); + if (jsonString == null || jsonString.isEmpty) { + // Attempt migration from legacy unscoped key on first load + final legacyJsonString = prefs.getString(_keyPrefix); + prefs.remove(_keyPrefix); + if (legacyJsonString != null && legacyJsonString.isNotEmpty) { + appLogger.info( + 'Migrating communities from legacy key $_keyPrefix to scoped key $keyFor', + ); + await prefs.setString(keyFor, legacyJsonString); + jsonString = legacyJsonString; + } + } if (jsonString == null || jsonString.isEmpty) { return []; } @@ -32,9 +55,13 @@ class CommunityStore { /// Save all communities to storage Future saveCommunities(List communities) async { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot save communities.'); + return; + } final prefs = PrefsManager.instance; final jsonList = communities.map((c) => c.toJson()).toList(); - await prefs.setString(_communitiesKey, jsonEncode(jsonList)); + await prefs.setString(keyFor, jsonEncode(jsonList)); } /// Add a new community diff --git a/lib/storage/contact_discovery_store.dart b/lib/storage/contact_discovery_store.dart index 37bfbb4..ac47615 100644 --- a/lib/storage/contact_discovery_store.dart +++ b/lib/storage/contact_discovery_store.dart @@ -5,11 +5,11 @@ import '../models/discovery_contact.dart'; import 'prefs_manager.dart'; class ContactDiscoveryStore { - static const String _key = 'discovered_contacts'; + static const String _keyPrefix = 'discovered_contacts'; Future> loadContacts() async { final prefs = PrefsManager.instance; - final jsonStr = prefs.getString(_key); + final jsonStr = prefs.getString(_keyPrefix); if (jsonStr == null) return []; try { @@ -25,7 +25,7 @@ class ContactDiscoveryStore { Future saveContacts(List contacts) async { final prefs = PrefsManager.instance; final jsonList = contacts.map(_toJson).toList(); - await prefs.setString(_key, jsonEncode(jsonList)); + await prefs.setString(_keyPrefix, jsonEncode(jsonList)); } Map _toJson(DiscoveryContact contact) { diff --git a/lib/storage/contact_group_store.dart b/lib/storage/contact_group_store.dart index 907cc5c..c1a7702 100644 --- a/lib/storage/contact_group_store.dart +++ b/lib/storage/contact_group_store.dart @@ -1,17 +1,42 @@ import 'dart:convert'; import '../models/contact_group.dart'; +import '../utils/app_logger.dart'; import 'prefs_manager.dart'; class ContactGroupStore { - static const String _key = 'contact_groups'; + static const String _keyPrefix = 'contact_groups'; + + String publicKeyHex = ''; + set setPublicKeyHex(String value) => + publicKeyHex = value.length > 10 ? value.substring(0, 10) : ''; + + String get keyFor => '$_keyPrefix$publicKeyHex'; Future> loadGroups() async { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot load contact groups.'); + return []; + } final prefs = PrefsManager.instance; - final raw = prefs.getString(_key); - if (raw == null || raw.isEmpty) return []; + String? jsonString = prefs.getString(_keyPrefix); + if (jsonString == null || jsonString.isEmpty) { + // Attempt migration from legacy unscoped key on first load + final legacyJsonString = prefs.getString(_keyPrefix); + prefs.remove(_keyPrefix); + if (legacyJsonString != null && legacyJsonString.isNotEmpty) { + appLogger.info( + 'Migrating channel messages from legacy key $_keyPrefix to scoped key $keyFor', + ); + await prefs.setString(keyFor, legacyJsonString); + jsonString = legacyJsonString; + } + } + if (jsonString == null || jsonString.isEmpty) { + return []; + } try { - final decoded = jsonDecode(raw); + final decoded = jsonDecode(jsonString); if (decoded is List) { return decoded .whereType>() @@ -25,8 +50,12 @@ class ContactGroupStore { } Future saveGroups(List groups) async { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot save contact groups.'); + return; + } final prefs = PrefsManager.instance; final encoded = jsonEncode(groups.map((group) => group.toJson()).toList()); - await prefs.setString(_key, encoded); + await prefs.setString(keyFor, encoded); } } diff --git a/lib/storage/contact_settings_store.dart b/lib/storage/contact_settings_store.dart index 5a7949d..94c6430 100644 --- a/lib/storage/contact_settings_store.dart +++ b/lib/storage/contact_settings_store.dart @@ -1,17 +1,49 @@ +import '../utils/app_logger.dart'; import 'prefs_manager.dart'; class ContactSettingsStore { - static const String _smazKeyPrefix = 'contact_smaz_'; + static const String _keyPrefix = 'contact_smaz_'; + + String publicKeyHex = ''; + set setPublicKeyHex(String value) => + publicKeyHex = value.length > 10 ? value.substring(0, 10) : ''; + + String get keyFor => '$_keyPrefix$publicKeyHex'; Future loadSmazEnabled(String contactKeyHex) async { + if (publicKeyHex.isEmpty) { + appLogger.warn( + 'Public key hex is not set. Cannot load contact settings.', + ); + return false; + } final prefs = PrefsManager.instance; - final key = '$_smazKeyPrefix$contactKeyHex'; + final key = '$keyFor$contactKeyHex'; + final oldKey = '$_keyPrefix$contactKeyHex'; + bool? enabled = prefs.getBool(key); + if (enabled == null) { + // Attempt migration from legacy unscoped key on first load + enabled = prefs.getBool(oldKey); + prefs.remove(oldKey); + if (enabled != null) { + appLogger.info( + 'Migrating contact settings from legacy key $oldKey to scoped key $key', + ); + await prefs.setBool(key, enabled); + } + } return prefs.getBool(key) ?? false; } Future saveSmazEnabled(String contactKeyHex, bool enabled) async { + if (publicKeyHex.isEmpty) { + appLogger.warn( + 'Public key hex is not set. Cannot save contact settings.', + ); + return; + } final prefs = PrefsManager.instance; - final key = '$_smazKeyPrefix$contactKeyHex'; + final key = '$keyFor$contactKeyHex'; await prefs.setBool(key, enabled); } } diff --git a/lib/storage/contact_store.dart b/lib/storage/contact_store.dart index 504ff16..8f9e84d 100644 --- a/lib/storage/contact_store.dart +++ b/lib/storage/contact_store.dart @@ -2,18 +2,43 @@ import 'dart:convert'; import 'dart:typed_data'; import '../models/contact.dart'; +import '../utils/app_logger.dart'; import 'prefs_manager.dart'; class ContactStore { - static const String _key = 'contacts'; + static const String _keyPrefix = 'contacts'; + + String publicKeyHex = ''; + set setPublicKeyHex(String value) => + publicKeyHex = value.length > 10 ? value.substring(0, 10) : ''; + + String get keyFor => '$_keyPrefix$publicKeyHex'; Future> loadContacts() async { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot load contacts.'); + return []; + } final prefs = PrefsManager.instance; - final jsonStr = prefs.getString(_key); - if (jsonStr == null) return []; + String? jsonString = prefs.getString(keyFor); + if (jsonString == null || jsonString.isEmpty) { + // Attempt migration from legacy unscoped key on first load + final legacyJsonString = prefs.getString(_keyPrefix); + prefs.remove(_keyPrefix); + if (legacyJsonString != null && legacyJsonString.isNotEmpty) { + appLogger.info( + 'Migrating contacts from legacy key $_keyPrefix to scoped key $keyFor', + ); + await prefs.setString(keyFor, legacyJsonString); + jsonString = legacyJsonString; + } + } + if (jsonString == null || jsonString.isEmpty) { + return []; + } try { - final jsonList = jsonDecode(jsonStr) as List; + final jsonList = jsonDecode(jsonString) as List; return jsonList .map((entry) => _fromJson(entry as Map)) .toList(); @@ -23,9 +48,13 @@ class ContactStore { } Future saveContacts(List contacts) async { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot save contacts.'); + return; + } final prefs = PrefsManager.instance; final jsonList = contacts.map(_toJson).toList(); - await prefs.setString(_key, jsonEncode(jsonList)); + await prefs.setString(keyFor, jsonEncode(jsonList)); } Map _toJson(Contact contact) { diff --git a/lib/storage/message_store.dart b/lib/storage/message_store.dart index 9526ef3..82caa78 100644 --- a/lib/storage/message_store.dart +++ b/lib/storage/message_store.dart @@ -2,26 +2,56 @@ import 'dart:convert'; import 'dart:typed_data'; import '../models/message.dart'; import '../helpers/smaz.dart'; +import '../utils/app_logger.dart'; import 'prefs_manager.dart'; class MessageStore { static const String _keyPrefix = 'messages_'; + String publicKeyHex = ''; + set setPublicKeyHex(String value) => + publicKeyHex = value.length > 10 ? value.substring(0, 10) : ''; + + String get keyFor => '$_keyPrefix$publicKeyHex'; + Future saveMessages( String contactKeyHex, List messages, ) async { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot save messages.'); + return; + } final prefs = PrefsManager.instance; - final key = '$_keyPrefix$contactKeyHex'; + final key = '$keyFor$contactKeyHex'; final jsonList = messages.map(_messageToJson).toList(); await prefs.setString(key, jsonEncode(jsonList)); } Future> loadMessages(String contactKeyHex) async { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot load messages.'); + return []; + } final prefs = PrefsManager.instance; - final key = '$_keyPrefix$contactKeyHex'; - final jsonString = prefs.getString(key); - if (jsonString == null) return []; + final key = '$keyFor$contactKeyHex'; + final oldKey = '$_keyPrefix$contactKeyHex'; + String? jsonString = prefs.getString(key); + if (jsonString == null || jsonString.isEmpty) { + // Attempt migration from legacy unscoped key on first load + final legacyJsonString = prefs.getString(oldKey); + prefs.remove(oldKey); + if (legacyJsonString != null && legacyJsonString.isNotEmpty) { + appLogger.info( + 'Migrating messages from legacy key $oldKey to scoped key $key', + ); + await prefs.setString(key, legacyJsonString); + jsonString = legacyJsonString; + } + } + if (jsonString == null || jsonString.isEmpty) { + return []; + } try { final jsonList = jsonDecode(jsonString) as List; @@ -32,8 +62,12 @@ class MessageStore { } Future clearMessages(String contactKeyHex) async { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot clear messages.'); + return; + } final prefs = PrefsManager.instance; - final key = '$_keyPrefix$contactKeyHex'; + final key = '$keyFor$contactKeyHex'; await prefs.remove(key); } diff --git a/lib/storage/unread_store.dart b/lib/storage/unread_store.dart index 201d25e..c0cecee 100644 --- a/lib/storage/unread_store.dart +++ b/lib/storage/unread_store.dart @@ -1,11 +1,18 @@ import 'dart:async'; import 'dart:convert'; +import '../utils/app_logger.dart'; import 'prefs_manager.dart'; /// Storage for unread message tracking with debounced writes to reduce I/O. class UnreadStore { - static const String _contactUnreadCountKey = 'contact_unread_count'; + static const String _keyPrefix = 'contact_unread_count'; + + String publicKeyHex = ''; + set setPublicKeyHex(String value) => + publicKeyHex = value.length >= 10 ? value.substring(0, 10) : ''; + + String get keyFor => '$_keyPrefix$publicKeyHex'; // Debounce timers to batch rapid writes Timer? _contactUnreadSaveTimer; @@ -20,12 +27,30 @@ class UnreadStore { } Future> loadContactUnreadCount() async { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot load unread counts.'); + return {}; + } final prefs = PrefsManager.instance; - final jsonStr = prefs.getString(_contactUnreadCountKey); - if (jsonStr == null) return {}; + String? jsonString = prefs.getString(_keyPrefix); + if (jsonString == null || jsonString.isEmpty) { + // Attempt migration from legacy unscoped key on first load + final legacyJsonString = prefs.getString(_keyPrefix); + prefs.remove(_keyPrefix); + if (legacyJsonString != null && legacyJsonString.isNotEmpty) { + appLogger.info( + 'Migrating channel messages from legacy key $_keyPrefix to scoped key $keyFor', + ); + await prefs.setString(keyFor, legacyJsonString); + jsonString = legacyJsonString; + } + } + if (jsonString == null || jsonString.isEmpty) { + return {}; + } try { - final json = jsonDecode(jsonStr) as Map; + final json = jsonDecode(jsonString) as Map; return json.map((key, value) => MapEntry(key, value as int)); } catch (_) { return {}; @@ -33,6 +58,10 @@ class UnreadStore { } void saveContactUnreadCount(Map counts) { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot save unread counts.'); + return; + } _pendingContactUnreadCount = counts; _contactUnreadSaveTimer?.cancel(); @@ -49,7 +78,7 @@ class UnreadStore { final prefs = PrefsManager.instance; final jsonStr = jsonEncode(_pendingContactUnreadCount); - await prefs.setString(_contactUnreadCountKey, jsonStr); + await prefs.setString(keyFor, jsonStr); _pendingContactUnreadCount = null; } From c81791cf1eea320af55fc4ffa3e443161b8d542d Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Thu, 12 Mar 2026 08:39:17 -0700 Subject: [PATCH 2/3] Migrate legacy storage keys to scoped keys in various store classes (#289) --- lib/storage/channel_message_store.dart | 13 ++++++++----- lib/storage/channel_settings_store.dart | 2 +- lib/storage/channel_store.dart | 4 ++++ lib/storage/community_store.dart | 3 +++ lib/storage/contact_group_store.dart | 3 +++ lib/storage/contact_store.dart | 3 +++ lib/storage/message_store.dart | 3 +++ lib/storage/unread_store.dart | 3 +++ 8 files changed, 28 insertions(+), 6 deletions(-) diff --git a/lib/storage/channel_message_store.dart b/lib/storage/channel_message_store.dart index 9c9f7e8..50d13f7 100644 --- a/lib/storage/channel_message_store.dart +++ b/lib/storage/channel_message_store.dart @@ -46,24 +46,27 @@ class ChannelMessageStore { } final prefs = PrefsManager.instance; final key = '$keyFor$channelIndex'; + final oldKey = '$_keyPrefix$channelIndex'; - String? jsonString = prefs.getString(_keyPrefix); + String? jsonString = prefs.getString(oldKey); if (jsonString == null || jsonString.isEmpty) { // Attempt migration from legacy unscoped key on first load - final legacyJsonString = prefs.getString(_keyPrefix); - prefs.remove(_keyPrefix); + final legacyJsonString = prefs.getString(oldKey); + prefs.remove(oldKey); if (legacyJsonString != null && legacyJsonString.isNotEmpty) { appLogger.info( - 'Migrating channel messages from legacy key $_keyPrefix to scoped key $key', + 'Migrating channel messages from legacy key $oldKey to scoped key $key', ); await prefs.setString(key, legacyJsonString); jsonString = legacyJsonString; } } + if (jsonString == null || jsonString.isEmpty) { + jsonString = prefs.getString(keyFor); + } if (jsonString == null || jsonString.isEmpty) { return []; } - try { final jsonList = jsonDecode(jsonString) as List; return jsonList.map((json) => _messageFromJson(json)).toList(); diff --git a/lib/storage/channel_settings_store.dart b/lib/storage/channel_settings_store.dart index 3b639cd..3fb00eb 100644 --- a/lib/storage/channel_settings_store.dart +++ b/lib/storage/channel_settings_store.dart @@ -20,7 +20,7 @@ class ChannelSettingsStore { final prefs = PrefsManager.instance; final key = '$keyFor$channelIndex'; final oldKey = '$_keyPrefix$channelIndex'; - bool? enabled = prefs.getBool(key); + bool? enabled = prefs.getBool(oldKey); if (enabled == null) { // Attempt migration from legacy unscoped key on first load enabled = prefs.getBool(oldKey); diff --git a/lib/storage/channel_store.dart b/lib/storage/channel_store.dart index 1bad7e3..775398e 100644 --- a/lib/storage/channel_store.dart +++ b/lib/storage/channel_store.dart @@ -32,6 +32,10 @@ class ChannelStore { jsonString = legacyJsonString; } } + + if (jsonString == null || jsonString.isEmpty) { + jsonString = prefs.getString(keyFor); + } if (jsonString == null || jsonString.isEmpty) { return []; } diff --git a/lib/storage/community_store.dart b/lib/storage/community_store.dart index c7198e7..6df859a 100644 --- a/lib/storage/community_store.dart +++ b/lib/storage/community_store.dart @@ -38,6 +38,9 @@ class CommunityStore { jsonString = legacyJsonString; } } + if (jsonString == null || jsonString.isEmpty) { + jsonString = prefs.getString(keyFor); + } if (jsonString == null || jsonString.isEmpty) { return []; } diff --git a/lib/storage/contact_group_store.dart b/lib/storage/contact_group_store.dart index c1a7702..986bfdd 100644 --- a/lib/storage/contact_group_store.dart +++ b/lib/storage/contact_group_store.dart @@ -31,6 +31,9 @@ class ContactGroupStore { jsonString = legacyJsonString; } } + if (jsonString == null || jsonString.isEmpty) { + jsonString = prefs.getString(keyFor); + } if (jsonString == null || jsonString.isEmpty) { return []; } diff --git a/lib/storage/contact_store.dart b/lib/storage/contact_store.dart index 8f9e84d..a4e2f0d 100644 --- a/lib/storage/contact_store.dart +++ b/lib/storage/contact_store.dart @@ -33,6 +33,9 @@ class ContactStore { jsonString = legacyJsonString; } } + if (jsonString == null || jsonString.isEmpty) { + jsonString = prefs.getString(keyFor); + } if (jsonString == null || jsonString.isEmpty) { return []; } diff --git a/lib/storage/message_store.dart b/lib/storage/message_store.dart index 82caa78..9a39e3f 100644 --- a/lib/storage/message_store.dart +++ b/lib/storage/message_store.dart @@ -49,6 +49,9 @@ class MessageStore { jsonString = legacyJsonString; } } + if (jsonString == null || jsonString.isEmpty) { + jsonString = prefs.getString(keyFor); + } if (jsonString == null || jsonString.isEmpty) { return []; } diff --git a/lib/storage/unread_store.dart b/lib/storage/unread_store.dart index c0cecee..d46fb41 100644 --- a/lib/storage/unread_store.dart +++ b/lib/storage/unread_store.dart @@ -45,6 +45,9 @@ class UnreadStore { jsonString = legacyJsonString; } } + if (jsonString == null || jsonString.isEmpty) { + jsonString = prefs.getString(keyFor); + } if (jsonString == null || jsonString.isEmpty) { return {}; } From 81758adc61d5efb84cefe5979185254038adf0e8 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Thu, 12 Mar 2026 23:08:46 -0700 Subject: [PATCH 3/3] Dev discovery (#291) * Refactor contact handling: replace DiscoveryContact with Contact, update related methods and settings * Enhance contact handling: include latitude, longitude, and last modified timestamp in contact updates; refactor path handling to accommodate discovered contacts across multiple screens * Enhance SNRIndicator: include discovered contacts in name resolution for repeaters * Refactor path handling: replace addReturnPath with buildPath to improve path construction logic and handle target contact types * Update lib/screens/map_screen.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add localization for "Show Discovery Contacts" in multiple languages and refactor location plausibility check in map screen * Enhance contact management: update discovered contacts' active status and improve contact handling with flags and raw packet data * Refactor ChannelsScreen: pass ChannelMessageStore to buildExpandedContent and ensure messages are cleared after channel creation * Update MapScreen: adjust label zoom threshold and refactor guessed marker building to include labels * Refactor ChannelsScreen: change channelMessageStore to a private getter and update its usage in buildExpandedContent calls * Enhance location plausibility check: add latitude and longitude bounds to ensure valid coordinates * Update lib/connector/meshcore_connector.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactor MeshCoreConnector and related stores: update discovered contacts handling, migrate legacy keys, and set public key in community store * Refactor MeshCoreConnector and ChannelsScreen: update discovered contacts handling and set public key in community store; enhance location plausibility check in MapScreen * Update CMD_ADD_UPDATE_CONTACT frame format to include optional latitude and longitude fields --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/connector/meshcore_connector.dart | 58 +++++-- lib/connector/meshcore_protocol.dart | 53 +++++-- lib/l10n/app_bg.arb | 3 +- lib/l10n/app_de.arb | 3 +- lib/l10n/app_en.arb | 1 + lib/l10n/app_es.arb | 3 +- lib/l10n/app_fr.arb | 3 +- lib/l10n/app_it.arb | 3 +- lib/l10n/app_localizations.dart | 6 + lib/l10n/app_localizations_bg.dart | 3 + lib/l10n/app_localizations_de.dart | 3 + lib/l10n/app_localizations_en.dart | 3 + lib/l10n/app_localizations_es.dart | 3 + lib/l10n/app_localizations_fr.dart | 3 + lib/l10n/app_localizations_it.dart | 3 + lib/l10n/app_localizations_nl.dart | 3 + lib/l10n/app_localizations_pl.dart | 3 + lib/l10n/app_localizations_pt.dart | 3 + lib/l10n/app_localizations_ru.dart | 3 + lib/l10n/app_localizations_sk.dart | 3 + lib/l10n/app_localizations_sl.dart | 3 + lib/l10n/app_localizations_sv.dart | 3 + lib/l10n/app_localizations_uk.dart | 3 + lib/l10n/app_localizations_zh.dart | 3 + lib/l10n/app_nl.arb | 3 +- lib/l10n/app_pl.arb | 3 +- lib/l10n/app_pt.arb | 3 +- lib/l10n/app_ru.arb | 3 +- lib/l10n/app_sk.arb | 3 +- lib/l10n/app_sl.arb | 3 +- lib/l10n/app_sv.arb | 3 +- lib/l10n/app_uk.arb | 3 +- lib/l10n/app_zh.arb | 3 +- lib/models/app_settings.dart | 8 + lib/models/contact.dart | 10 ++ lib/models/discovery_contact.dart | 105 ------------ lib/screens/ble_debug_log_screen.dart | 13 ++ lib/screens/channel_message_path_screen.dart | 17 +- lib/screens/channels_screen.dart | 42 +++-- lib/screens/community_qr_scanner_screen.dart | 5 + lib/screens/discovery_screen.dart | 15 +- lib/screens/map_screen.dart | 159 ++++++++++++++----- lib/screens/neighbors_screen.dart | 8 +- lib/screens/path_trace_map.dart | 55 +++++-- lib/services/app_settings_service.dart | 4 + lib/storage/channel_message_store.dart | 2 +- lib/storage/channel_order_store.dart | 2 +- lib/storage/channel_settings_store.dart | 2 +- lib/storage/channel_store.dart | 2 +- lib/storage/community_store.dart | 2 +- lib/storage/contact_discovery_store.dart | 38 ++++- lib/storage/contact_group_store.dart | 2 +- lib/storage/contact_store.dart | 8 + lib/storage/unread_store.dart | 2 +- lib/utils/contact_search.dart | 4 +- lib/widgets/snr_indicator.dart | 7 +- 56 files changed, 476 insertions(+), 241 deletions(-) delete mode 100644 lib/models/discovery_contact.dart diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 2ea09ca..9ee6e92 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:convert'; import 'package:crypto/crypto.dart' as crypto; -import 'package:meshcore_open/models/discovery_contact.dart'; import 'package:pointycastle/export.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; @@ -120,7 +119,7 @@ class MeshCoreConnector extends ChangeNotifier { final List _scanResults = []; final List _contacts = []; - final List _discoveredContacts = []; + final List _discoveredContacts = []; final List _channels = []; final Map> _conversations = {}; final Map> _channelMessages = {}; @@ -281,7 +280,7 @@ class MeshCoreConnector extends ChangeNotifier { ); } - List get discoveredContacts { + List get discoveredContacts { return List.unmodifiable(_discoveredContacts); } @@ -664,7 +663,6 @@ class MeshCoreConnector extends ChangeNotifier { // Initialize notification service _notificationService.initialize(); _loadChannelOrder(); - _loadDiscoveredContactCache(); // Initialize retry service callbacks _retryService?.initialize( @@ -1904,7 +1902,11 @@ class MeshCoreConnector extends ChangeNotifier { Future removeContact(Contact contact) async { if (!isConnected) return; - _handleDiscovery(contact, Uint8List(0), noNotify: true); + _handleDiscovery( + contact, + contact.rawPacket ?? Uint8List(0), + noNotify: true, + ); await sendFrame(buildRemoveContactFrame(contact.publicKey)); _contacts.removeWhere((c) => c.publicKeyHex == contact.publicKeyHex); @@ -1920,7 +1922,20 @@ class MeshCoreConnector extends ChangeNotifier { notifyListeners(); } - Future removeDiscoveredContact(DiscoveryContact contact) async { + Future updateKnownDiscovered() async { + if (!isConnected) return; + for (int i = 0; i < _discoveredContacts.length; i++) { + _discoveredContacts[i] = _discoveredContacts[i].copyWith( + isActive: _knownContactKeys.contains( + _discoveredContacts[i].publicKeyHex, + ), + ); + } + unawaited(_persistDiscoveredContacts()); + notifyListeners(); + } + + Future removeDiscoveredContact(Contact contact) async { if (!isConnected) return; _discoveredContacts.removeWhere( (c) => c.publicKeyHex == contact.publicKeyHex, @@ -1929,7 +1944,7 @@ class MeshCoreConnector extends ChangeNotifier { notifyListeners(); } - Future importDiscoveredContact(DiscoveryContact contact) async { + Future importDiscoveredContact(Contact contact) async { if (!isConnected) return; await sendFrame( @@ -1938,11 +1953,23 @@ class MeshCoreConnector extends ChangeNotifier { contact.path, contact.pathLength, type: contact.type, - flags: 0, + flags: contact.flags, name: contact.name, + lat: contact.latitude, + lon: contact.longitude, + lastModified: contact.lastSeen, ), ); + // Update the discovered contact to mark it as active (imported) + final discoveredIndex = _discoveredContacts.indexWhere( + (c) => c.publicKeyHex == contact.publicKeyHex, + ); + if (discoveredIndex >= 0) { + _discoveredContacts[discoveredIndex] = + _discoveredContacts[discoveredIndex].copyWith(isActive: true); + } + _handleContactAdvert( Contact( publicKey: contact.publicKey, @@ -1953,6 +1980,7 @@ class MeshCoreConnector extends ChangeNotifier { latitude: contact.latitude, longitude: contact.longitude, lastSeen: DateTime.now(), + flags: contact.flags, ), ); notifyListeners(); @@ -1969,6 +1997,8 @@ class MeshCoreConnector extends ChangeNotifier { final existing = _contacts[existingIndex]; // Use copyWith to preserve pathOverride and pathOverrideBytes _contacts[existingIndex] = existing.copyWith( + pathOverride: null, + pathOverrideBytes: null, pathLength: -1, path: Uint8List(0), ); @@ -2324,6 +2354,7 @@ class MeshCoreConnector extends ChangeNotifier { debugPrint('Got END_OF_CONTACTS'); _isLoadingContacts = false; _preserveContactsOnRefresh = false; + unawaited(updateKnownDiscovered()); notifyListeners(); unawaited(_persistContacts()); if (PlatformInfo.isWeb && @@ -2510,6 +2541,7 @@ class MeshCoreConnector extends ChangeNotifier { // Load persisted channel messages loadAllChannelMessages(); loadUnreadState(); + _loadDiscoveredContactCache(); _awaitingSelfInfo = false; _selfInfoRetryTimer?.cancel(); @@ -4406,7 +4438,7 @@ class MeshCoreConnector extends ChangeNotifier { } importDiscoveredContact( - DiscoveryContact( + Contact( rawPacket: frame, publicKey: publicKey, name: name, @@ -4477,6 +4509,7 @@ class MeshCoreConnector extends ChangeNotifier { if (isNewContact) { final newContact = Contact( + rawPacket: rawPacket, publicKey: publicKey, name: name, type: type, @@ -4622,13 +4655,15 @@ class MeshCoreConnector extends ChangeNotifier { latitude: contact.latitude, longitude: contact.longitude, lastSeen: contact.lastSeen, + flags: 0, + isActive: false, ); notifyListeners(); unawaited(_persistDiscoveredContacts()); return; } - final disContact = DiscoveryContact( + final disContact = Contact( rawPacket: rawPacket, publicKey: contact.publicKey, name: contact.name, @@ -4638,6 +4673,9 @@ class MeshCoreConnector extends ChangeNotifier { latitude: contact.latitude, longitude: contact.longitude, lastSeen: contact.lastSeen, + lastMessageAt: contact.lastMessageAt, + isActive: false, + flags: 0, ); _discoveredContacts.add(disContact); diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index 3484d47..dc9a9f5 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -148,6 +148,19 @@ class BufferWriter { void writeHex(String hex) { writeBytes(hex2Uint8List(hex)); } + + void writeBytesPadded(Uint8List bytes, int totalLength) { + // Path data (64 bytes, zero-padded) + final bytesPadded = Uint8List(totalLength); + final len = bytes.length < totalLength ? bytes.length : totalLength; + if (bytes.isNotEmpty && len > 0) { + final copyLen = bytes.length < totalLength ? bytes.length : totalLength; + for (int i = 0; i < copyLen; i++) { + bytesPadded[i] = bytes[i]; + } + } + writeBytes(bytesPadded); + } } Uint8List hex2Uint8List(String hex) { @@ -676,14 +689,17 @@ Uint8List buildResetPathFrame(Uint8List pubKey) { } // Build CMD_ADD_UPDATE_CONTACT frame to set custom path -// Format: [cmd][pub_key x32][type][flags][path_len][path x64][name x32][timestamp x4] +// Format: [cmd][pub_key x32][type][flags][path_len][path x64][name x32][Lat? x4, Lon? x4][timestamp? x4] Uint8List buildUpdateContactPathFrame( Uint8List pubKey, - Uint8List customPath, + Uint8List path, int pathLen, { int type = 1, // ADV_TYPE_CHAT int flags = 0, String name = '', + double? lat, + double? lon, + DateTime? lastModified, }) { final writer = BufferWriter(); writer.writeByte(cmdAddUpdateContact); @@ -692,17 +708,7 @@ Uint8List buildUpdateContactPathFrame( writer.writeByte(flags); writer.writeByte(pathLen); - // Path data (64 bytes, zero-padded) - final pathPadded = Uint8List(maxPathSize); - if (customPath.isNotEmpty && pathLen > 0) { - final copyLen = customPath.length < maxPathSize - ? customPath.length - : maxPathSize; - for (int i = 0; i < copyLen; i++) { - pathPadded[i] = customPath[i]; - } - } - writer.writeBytes(pathPadded); + writer.writeBytesPadded(path, maxPathSize); // Name (32 bytes, null-padded) writer.writeCString(name, maxNameSize); @@ -711,6 +717,27 @@ Uint8List buildUpdateContactPathFrame( final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000; writer.writeUInt32LE(timestamp); + if ((lat == null || lon == null) && lastModified != null) { + // If lat/lon not provided, write zeros + writer.writeInt32LE(0); + writer.writeInt32LE(0); + } else { + // Latitude and Longitude are expected in degrees, convert to int by multiplying by 1e6 + // Latitude + final latitude = lat ?? 0.0; + writer.writeInt32LE((latitude * 1e6).round()); + + // Longitude + final longitude = lon ?? 0.0; + writer.writeInt32LE((longitude * 1e6).round()); + } + + if (lastModified != null) { + // Last modified + final lastModifiedTimestamp = lastModified.millisecondsSinceEpoch ~/ 1000; + writer.writeUInt32LE(lastModifiedTimestamp); + } + return writer.toBytes(); } diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 94d8997..a2723d1 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1859,5 +1859,6 @@ "usbConnectionFailed": "Неуспешно свързване през USB: {error}", "usbStatus_notConnected": "Изберете USB устройство", "usbStatus_searching": "Търсене на USB устройства...", - "usbErrorConnectTimedOut": "Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка." + "usbErrorConnectTimedOut": "Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка.", + "map_showDiscoveryContacts": "Покажи контакти за откриване" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 9ba0f51..2f51360 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1887,5 +1887,6 @@ "usbStatus_notConnected": "Wählen Sie ein USB-Gerät aus", "usbStatus_connecting": "Verbindung zum USB-Gerät...", "usbConnectionFailed": "Fehler beim USB-Verbindungsaufbau: {error}", - "usbErrorConnectTimedOut": "Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält." + "usbErrorConnectTimedOut": "Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält.", + "map_showDiscoveryContacts": "Entdeckungs-Kontakte anzeigen" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2605628..ab33cea 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -807,6 +807,7 @@ "map_markers": "Markers", "map_showSharedMarkers": "Show shared markers", "map_showGuessedLocations": "Show guessed node locations", + "map_showDiscoveryContacts": "Show Discovery Contacts", "map_guessedLocation": "Guessed location", "map_lastSeenTime": "Last Seen Time", "map_sharedPin": "Shared pin", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 9b791d3..98d7c04 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1887,5 +1887,6 @@ "usbStatus_searching": "Buscando dispositivos USB...", "usbStatus_notConnected": "Seleccione un dispositivo USB", "usbConnectionFailed": "Error al conectar mediante USB: {error}", - "usbErrorConnectTimedOut": "La conexión ha caducado. Asegúrese de que el dispositivo tenga el firmware USB Companion." + "usbErrorConnectTimedOut": "La conexión ha caducado. Asegúrese de que el dispositivo tenga el firmware USB Companion.", + "map_showDiscoveryContacts": "Mostrar Contactos de Descubrimiento" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index a7bedc9..fe437e6 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1859,5 +1859,6 @@ "usbConnectionFailed": "Échec de la connexion USB : {error}", "usbStatus_connecting": "Connexion au périphérique USB...", "usbStatus_searching": "Recherche de périphériques USB...", - "usbErrorConnectTimedOut": "La connexion a expiré. Assurez-vous que l'appareil dispose du firmware USB Companion." + "usbErrorConnectTimedOut": "La connexion a expiré. Assurez-vous que l'appareil dispose du firmware USB Companion.", + "map_showDiscoveryContacts": "Afficher les contacts de découverte" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 423ff40..6368ab3 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1859,5 +1859,6 @@ "usbConnectionFailed": "Errore nella connessione USB: {error}", "usbStatus_notConnected": "Seleziona un dispositivo USB", "usbStatus_connecting": "Connessione al dispositivo USB...", - "usbErrorConnectTimedOut": "La connessione è scaduta. Assicurarsi che il dispositivo abbia il firmware USB Companion." + "usbErrorConnectTimedOut": "La connessione è scaduta. Assicurarsi che il dispositivo abbia il firmware USB Companion.", + "map_showDiscoveryContacts": "Mostra Contatti di Discovery" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 8d3f86b..a2a25ec 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2788,6 +2788,12 @@ abstract class AppLocalizations { /// **'Show guessed node locations'** String get map_showGuessedLocations; + /// No description provided for @map_showDiscoveryContacts. + /// + /// In en, this message translates to: + /// **'Show Discovery Contacts'** + String get map_showDiscoveryContacts; + /// No description provided for @map_guessedLocation. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 356106e..ce9c061 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -1531,6 +1531,9 @@ class AppLocalizationsBg extends AppLocalizations { String get map_showGuessedLocations => 'Покажете местоположенията на предположените възли.'; + @override + String get map_showDiscoveryContacts => 'Покажи контакти за откриване'; + @override String get map_guessedLocation => 'Предполагано местоположение'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 6353f35..14cb58c 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1531,6 +1531,9 @@ class AppLocalizationsDe extends AppLocalizations { String get map_showGuessedLocations => 'Zeige die vermuteten Knotenpositionen'; + @override + String get map_showDiscoveryContacts => 'Entdeckungs-Kontakte anzeigen'; + @override String get map_guessedLocation => 'Geschätzter Ort'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 9c20df7..9d9dab4 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1506,6 +1506,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get map_showGuessedLocations => 'Show guessed node locations'; + @override + String get map_showDiscoveryContacts => 'Show Discovery Contacts'; + @override String get map_guessedLocation => 'Guessed location'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index eecbd48..98c0f37 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1529,6 +1529,9 @@ class AppLocalizationsEs extends AppLocalizations { String get map_showGuessedLocations => 'Mostrar las ubicaciones estimadas de los nodos.'; + @override + String get map_showDiscoveryContacts => 'Mostrar Contactos de Descubrimiento'; + @override String get map_guessedLocation => 'Ubicación estimada'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 5cabc86..697db20 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1536,6 +1536,9 @@ class AppLocalizationsFr extends AppLocalizations { String get map_showGuessedLocations => 'Afficher les emplacements des nœuds estimés'; + @override + String get map_showDiscoveryContacts => 'Afficher les contacts de découverte'; + @override String get map_guessedLocation => 'Lieu deviné'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index d170540..3655cdd 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -1528,6 +1528,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get map_showGuessedLocations => 'Mostra le posizioni stimate dei nodi'; + @override + String get map_showDiscoveryContacts => 'Mostra Contatti di Discovery'; + @override String get map_guessedLocation => 'Località indovinata'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 323ba34..1acb14d 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1521,6 +1521,9 @@ class AppLocalizationsNl extends AppLocalizations { String get map_showGuessedLocations => 'Toon de voorspelde locaties van de knopen'; + @override + String get map_showDiscoveryContacts => 'Ontdek contacten weergeven'; + @override String get map_guessedLocation => 'Geroerde locatie'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 9175c3e..5d8e335 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -1530,6 +1530,9 @@ class AppLocalizationsPl extends AppLocalizations { String get map_showGuessedLocations => 'Wyświetl lokalizacje zgadanych węzłów'; + @override + String get map_showDiscoveryContacts => 'Pokaż kontakty odkrywania'; + @override String get map_guessedLocation => 'Wydana lokalizacja'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index ff09213..a533fa5 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1530,6 +1530,9 @@ class AppLocalizationsPt extends AppLocalizations { String get map_showGuessedLocations => 'Mostrar as localizações dos nós estimados'; + @override + String get map_showDiscoveryContacts => 'Mostrar Contatos de Descoberta'; + @override String get map_guessedLocation => 'Localização estimada'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 69a5891..7bf4418 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1532,6 +1532,9 @@ class AppLocalizationsRu extends AppLocalizations { String get map_showGuessedLocations => 'Отобразить предполагаемые места расположения узлов'; + @override + String get map_showDiscoveryContacts => 'Показать контакты Discovery'; + @override String get map_guessedLocation => 'Угаданное место'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index d0e75b0..516fb6c 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -1524,6 +1524,9 @@ class AppLocalizationsSk extends AppLocalizations { String get map_showGuessedLocations => 'Zobraziť umiestnenia odhadnutých uzlov'; + @override + String get map_showDiscoveryContacts => 'Zobraziť kontakty objavov'; + @override String get map_guessedLocation => 'Odhadnutá lokalita'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 21e3d9d..f6f0df8 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -1517,6 +1517,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get map_showGuessedLocations => 'Pokaži lokacije domnevnih not.'; + @override + String get map_showDiscoveryContacts => 'Prikaži odkritja kontaktov'; + @override String get map_guessedLocation => 'Predpostavljena lokacija'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 5951fae..9595dc0 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -1514,6 +1514,9 @@ class AppLocalizationsSv extends AppLocalizations { String get map_showGuessedLocations => 'Visa upp de antagna nodernas placeringar'; + @override + String get map_showDiscoveryContacts => 'Visa Discovery-kontakter'; + @override String get map_guessedLocation => 'Gissad plats'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index b8fd60a..2e2537b 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -1529,6 +1529,9 @@ class AppLocalizationsUk extends AppLocalizations { String get map_showGuessedLocations => 'Показати місцезнаходження передбачених вузлів'; + @override + String get map_showDiscoveryContacts => 'Показати контакти Відкриття'; + @override String get map_guessedLocation => 'Визначено місцезнаходження'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 27e6c21..058dce1 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1440,6 +1440,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get map_showGuessedLocations => '显示猜测的节点位置'; + @override + String get map_showDiscoveryContacts => '显示发现联系人'; + @override String get map_guessedLocation => '猜测的位置'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 94df130..0a29595 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1859,5 +1859,6 @@ "usbStatus_notConnected": "Selecteer een USB-apparaat", "usbStatus_connecting": "Verbinding maken met USB-apparaat...", "usbStatus_searching": "Zoeken naar USB-apparaten...", - "usbErrorConnectTimedOut": "Verbinding is verbroken. Zorg ervoor dat het apparaat de juiste USB-firmware heeft." + "usbErrorConnectTimedOut": "Verbinding is verbroken. Zorg ervoor dat het apparaat de juiste USB-firmware heeft.", + "map_showDiscoveryContacts": "Ontdek contacten weergeven" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index d020e0e..43ab9dd 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1859,5 +1859,6 @@ "usbStatus_connecting": "Połączenie z urządzeniem USB...", "usbStatus_notConnected": "Wybierz urządzenie USB", "usbConnectionFailed": "Błąd połączenia USB: {error}", - "usbErrorConnectTimedOut": "Połączenie nie zostało nawiązane. Upewnij się, że urządzenie posiada oprogramowanie \"USB Companion\"." + "usbErrorConnectTimedOut": "Połączenie nie zostało nawiązane. Upewnij się, że urządzenie posiada oprogramowanie \"USB Companion\".", + "map_showDiscoveryContacts": "Pokaż kontakty odkrywania" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index d52cb41..11aa84d 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1859,5 +1859,6 @@ "usbStatus_notConnected": "Selecione um dispositivo USB", "usbConnectionFailed": "Falha na conexão USB: {error}", "usbStatus_connecting": "Conectando ao dispositivo USB...", - "usbErrorConnectTimedOut": "A conexão expirou. Verifique se o dispositivo possui o firmware USB Companion." + "usbErrorConnectTimedOut": "A conexão expirou. Verifique se o dispositivo possui o firmware USB Companion.", + "map_showDiscoveryContacts": "Mostrar Contatos de Descoberta" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 92fd55e..af9c220 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1099,5 +1099,6 @@ "usbStatus_connecting": "Подключение к USB-устройству...", "usbConnectionFailed": "Не удалось установить соединение через USB: {error}", "usbStatus_notConnected": "Выберите USB-устройство", - "usbErrorConnectTimedOut": "Соединение не установлено. Убедитесь, что устройство имеет установленное программное обеспечение USB Companion." + "usbErrorConnectTimedOut": "Соединение не установлено. Убедитесь, что устройство имеет установленное программное обеспечение USB Companion.", + "map_showDiscoveryContacts": "Показать контакты Discovery" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 141147c..e844f60 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1859,5 +1859,6 @@ "usbConnectionFailed": "Neúspešné pripojenie cez USB: {error}", "usbStatus_notConnected": "Vyberte USB zariadenie", "usbStatus_connecting": "Pripojenie k USB zariadeniu...", - "usbErrorConnectTimedOut": "Pripojenie nebolo úspešné. Uistite sa, že zariadenie má nainštalovaný firmware USB Companion." + "usbErrorConnectTimedOut": "Pripojenie nebolo úspešné. Uistite sa, že zariadenie má nainštalovaný firmware USB Companion.", + "map_showDiscoveryContacts": "Zobraziť kontakty objavov" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 12529d6..939aad6 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1859,5 +1859,6 @@ "usbStatus_connecting": "Povezava z USB napravo...", "usbStatus_searching": "Iskanje USB naprav...", "usbConnectionFailed": "Napaka pri povezavi preko USB: {error}", - "usbErrorConnectTimedOut": "Vzpostavitve ni bilo mogo. Prosimo, da se prepričate, da ima naprave trenutno nameštan firmware USB Companion." + "usbErrorConnectTimedOut": "Vzpostavitve ni bilo mogo. Prosimo, da se prepričate, da ima naprave trenutno nameštan firmware USB Companion.", + "map_showDiscoveryContacts": "Prikaži odkritja kontaktov" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index f7615df..9611f18 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1859,5 +1859,6 @@ "usbStatus_notConnected": "Välj en USB-enhet", "usbConnectionFailed": "Fel vid USB-anslutning: {error}", "usbStatus_searching": "Söker efter USB-enheter...", - "usbErrorConnectTimedOut": "Anslutningen har tidsutgått. Se till att enheten har rätt USB-firmware." + "usbErrorConnectTimedOut": "Anslutningen har tidsutgått. Se till att enheten har rätt USB-firmware.", + "map_showDiscoveryContacts": "Visa Discovery-kontakter" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 7794098..389184c 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1859,5 +1859,6 @@ "usbStatus_notConnected": "Виберіть пристрій USB", "usbConnectionFailed": "Не вдалося встановити з'єднання через USB: {error}", "usbStatus_connecting": "Підключення до USB-пристрою...", - "usbErrorConnectTimedOut": "З'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion." + "usbErrorConnectTimedOut": "З'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion.", + "map_showDiscoveryContacts": "Показати контакти Відкриття" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index dfc8e64..8a52983 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1864,5 +1864,6 @@ "usbStatus_connecting": "连接USB设备...", "usbStatus_notConnected": "选择一个 USB 设备", "usbConnectionFailed": "USB 连接失败:{error}", - "usbErrorConnectTimedOut": "连接超时。请确保设备已安装 USB 伴侣固件。" + "usbErrorConnectTimedOut": "连接超时。请确保设备已安装 USB 伴侣固件。", + "map_showDiscoveryContacts": "显示发现联系人" } diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index abcc729..c89ac27 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -39,6 +39,7 @@ class AppSettings { final Map batteryChemistryByRepeaterId; final UnitSystem unitSystem; final Set mutedChannels; + final bool mapShowDiscoveryContacts; AppSettings({ this.clearPathOnMaxRetry = false, @@ -66,6 +67,7 @@ class AppSettings { Map? batteryChemistryByRepeaterId, this.unitSystem = UnitSystem.metric, Set? mutedChannels, + this.mapShowDiscoveryContacts = true, }) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {}, batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {}, mutedChannels = mutedChannels ?? {}; @@ -97,6 +99,7 @@ class AppSettings { 'battery_chemistry_by_repeater_id': batteryChemistryByRepeaterId, 'unit_system': unitSystem.value, 'muted_channels': mutedChannels.toList(), + 'map_show_discovery_contacts': mapShowDiscoveryContacts, }; } @@ -152,6 +155,8 @@ class AppSettings { ?.map((e) => e.toString()) .toSet()) ?? {}, + mapShowDiscoveryContacts: + json['map_show_discovery_contacts'] as bool? ?? true, ); } @@ -181,6 +186,7 @@ class AppSettings { Map? batteryChemistryByRepeaterId, UnitSystem? unitSystem, Set? mutedChannels, + bool? mapShowDiscoveryContacts, }) { return AppSettings( clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry, @@ -217,6 +223,8 @@ class AppSettings { batteryChemistryByRepeaterId ?? this.batteryChemistryByRepeaterId, unitSystem: unitSystem ?? this.unitSystem, mutedChannels: mutedChannels ?? this.mutedChannels, + mapShowDiscoveryContacts: + mapShowDiscoveryContacts ?? this.mapShowDiscoveryContacts, ); } } diff --git a/lib/models/contact.dart b/lib/models/contact.dart index b4acff7..cab58cb 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -17,6 +17,8 @@ class Contact { final double? longitude; final DateTime lastSeen; final DateTime lastMessageAt; + final bool isActive; + final Uint8List? rawPacket; Contact({ required this.publicKey, @@ -31,6 +33,8 @@ class Contact { this.longitude, required this.lastSeen, DateTime? lastMessageAt, + this.isActive = true, + this.rawPacket, }) : lastMessageAt = lastMessageAt ?? lastSeen; String get publicKeyHex => pubKeyToHex(publicKey); @@ -78,6 +82,8 @@ class Contact { double? longitude, DateTime? lastSeen, DateTime? lastMessageAt, + bool? isActive, + Uint8List? rawPacket, }) { return Contact( publicKey: publicKey ?? this.publicKey, @@ -96,6 +102,8 @@ class Contact { longitude: longitude ?? this.longitude, lastSeen: lastSeen ?? this.lastSeen, lastMessageAt: lastMessageAt ?? this.lastMessageAt, + isActive: isActive ?? this.isActive, + rawPacket: rawPacket ?? this.rawPacket, ); } @@ -204,6 +212,8 @@ class Contact { latitude: lat, longitude: lon, lastSeen: DateTime.fromMillisecondsSinceEpoch(lastMod * 1000), + isActive: true, + rawPacket: null, ); } catch (e) { appLogger.error('Failed to parse contact frame: $e'); diff --git a/lib/models/discovery_contact.dart b/lib/models/discovery_contact.dart deleted file mode 100644 index f6c6a52..0000000 --- a/lib/models/discovery_contact.dart +++ /dev/null @@ -1,105 +0,0 @@ -import 'dart:typed_data'; -import '../connector/meshcore_protocol.dart'; - -class DiscoveryContact { - final Uint8List rawPacket; - final Uint8List publicKey; - final String name; - final int type; - final int pathLength; // -1 = flood, 0+ = direct hops (from device) - final Uint8List path; // Path bytes from device - final double? latitude; - final double? longitude; - final DateTime lastSeen; - - DiscoveryContact({ - required this.rawPacket, - required this.publicKey, - required this.name, - required this.type, - required this.pathLength, - required this.path, - this.latitude, - this.longitude, - required this.lastSeen, - }); - - String get publicKeyHex => pubKeyToHex(publicKey); - - String get typeLabel { - switch (type) { - case advTypeChat: - return 'Chat'; - case advTypeRepeater: - return 'Repeater'; - case advTypeRoom: - return 'Room'; - case advTypeSensor: - return 'Sensor'; - default: - return 'Unknown'; - } - } - - String get pathLabel { - if (pathLength < 0) return 'Flood'; - if (pathLength == 0) return 'Direct'; - return '$pathLength hops'; - } - - bool get hasLocation => latitude != null && longitude != null; - - DiscoveryContact copyWith({ - Uint8List? rawPacket, - Uint8List? publicKey, - String? name, - int? type, - int? pathLength, - Uint8List? path, - double? latitude, - double? longitude, - DateTime? lastSeen, - }) { - return DiscoveryContact( - rawPacket: rawPacket ?? this.rawPacket, - publicKey: publicKey ?? this.publicKey, - name: name ?? this.name, - type: type ?? this.type, - pathLength: pathLength ?? this.pathLength, - path: path ?? this.path, - latitude: latitude ?? this.latitude, - longitude: longitude ?? this.longitude, - lastSeen: lastSeen ?? this.lastSeen, - ); - } - - String get pathIdList { - final pathBytes = path; - if (pathBytes.isEmpty) return ''; - final parts = []; - final groupSize = pathHashSize; - for (int i = 0; i < pathBytes.length; i += groupSize) { - final end = (i + groupSize) <= pathBytes.length - ? (i + groupSize) - : pathBytes.length; - final chunk = pathBytes.sublist(i, end); - parts.add( - chunk - .map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()) - .join(), - ); - } - return parts.join(','); - } - - String get shortPubKeyHex { - return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>"; - } - - @override - bool operator ==(Object other) => - other is DiscoveryContact && publicKeyHex == other.publicKeyHex; - - @override - int get hashCode => publicKeyHex.hashCode; -} diff --git a/lib/screens/ble_debug_log_screen.dart b/lib/screens/ble_debug_log_screen.dart index 88f734b..a90f9f0 100644 --- a/lib/screens/ble_debug_log_screen.dart +++ b/lib/screens/ble_debug_log_screen.dart @@ -118,6 +118,19 @@ class _BleDebugLogScreenState extends State { : Icons.download, size: 18, ), + onLongPress: () async { + await Clipboard.setData( + ClipboardData( + text: entry.payload + .map( + (b) => b + .toRadixString(16) + .padLeft(2, '0'), + ) + .join(''), + ), + ); + }, ); } diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 44dfe79..c2c57f0 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -40,8 +40,11 @@ class ChannelMessagePathScreen extends StatelessWidget { final primaryPath = !channelMessage && !message.isOutgoing ? Uint8List.fromList(primaryPathTmp.reversed.toList()) : primaryPathTmp; - - final hops = _buildPathHops(primaryPath, connector.contacts, l10n); + final contacts = [ + ...connector.contacts, + ...connector.discoveredContacts, + ]; + final hops = _buildPathHops(primaryPath, contacts, l10n); final hasHopDetails = primaryPath.isNotEmpty; final observedLabel = _formatObservedHops( primaryPath.length, @@ -364,11 +367,11 @@ class _ChannelMessagePathMapScreenState : selectedPathTmp; final selectedIndex = _indexForPath(selectedPath, observedPaths); - final hops = _buildPathHops( - selectedPath, - connector.contacts, - context.l10n, - ); + final contacts = [ + ...connector.contacts, + ...connector.discoveredContacts, + ]; + final hops = _buildPathHops(selectedPath, contacts, context.l10n); final points = []; diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 00820ed..b56b563 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -51,6 +51,8 @@ class _ChannelsScreenState extends State // Cache of PSK hex -> Community for quick lookup final Map _pskToCommunity = {}; + ChannelMessageStore get _channelMessageStore => ChannelMessageStore(); + @override void initState() { super.initState(); @@ -61,6 +63,8 @@ class _ChannelsScreenState extends State } Future _loadCommunities() async { + final connector = context.read(); + _communityStore.setPublicKeyHex = connector.selfPublicKeyHex; final communities = await _communityStore.loadCommunities(); if (mounted) { setState(() { @@ -714,6 +718,8 @@ class _ChannelsScreenState extends State bool isRegularHashtag = true; Community? selectedCommunity; + _communityStore.setPublicKeyHex = connector.selfPublicKeyHex; + showDialog( context: context, builder: (dialogContext) => StatefulBuilder( @@ -765,7 +771,9 @@ class _ChannelsScreenState extends State ); } - Widget? buildExpandedContent() { + Widget? buildExpandedContent( + ChannelMessageStore channelMessageStore, + ) { switch (selectedOption) { case 0: // Create Private Channel return Column( @@ -790,7 +798,7 @@ class _ChannelsScreenState extends State children: [ Expanded( child: FilledButton( - onPressed: () { + onPressed: () async { final name = nameController.text.trim(); if (name.isEmpty) { ScaffoldMessenger.of( @@ -812,7 +820,14 @@ class _ChannelsScreenState extends State psk[i] = random.nextInt(256); } Navigator.pop(dialogContext); - connector.setChannel(nextIndex, name, psk); + await connector.setChannel( + nextIndex, + name, + psk, + ); + await channelMessageStore.clearChannelMessages( + nextIndex, + ); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -1331,7 +1346,8 @@ class _ChannelsScreenState extends State subtitle: dialogContext.l10n.channels_createPrivateChannelDesc, ), - if (selectedOption == 0) buildExpandedContent()!, + if (selectedOption == 0) + buildExpandedContent(_channelMessageStore)!, const Divider(height: 1), buildOptionTile( optionIndex: 1, @@ -1340,7 +1356,8 @@ class _ChannelsScreenState extends State subtitle: dialogContext.l10n.channels_joinPrivateChannelDesc, ), - if (selectedOption == 1) buildExpandedContent()!, + if (selectedOption == 1) + buildExpandedContent(_channelMessageStore)!, if (!hasPublicChannel) ...[ const Divider(height: 1), buildOptionTile( @@ -1350,7 +1367,8 @@ class _ChannelsScreenState extends State subtitle: dialogContext.l10n.channels_joinPublicChannelDesc, ), - if (selectedOption == 2) buildExpandedContent()!, + if (selectedOption == 2) + buildExpandedContent(_channelMessageStore)!, ], const Divider(height: 1), buildOptionTile( @@ -1360,7 +1378,8 @@ class _ChannelsScreenState extends State subtitle: dialogContext.l10n.channels_joinHashtagChannelDesc, ), - if (selectedOption == 3) buildExpandedContent()!, + if (selectedOption == 3) + buildExpandedContent(_channelMessageStore)!, const Divider(height: 1), buildOptionTile( optionIndex: 4, @@ -1368,7 +1387,8 @@ class _ChannelsScreenState extends State title: dialogContext.l10n.community_scanQr, subtitle: dialogContext.l10n.community_join, ), - if (selectedOption == 4) buildExpandedContent()!, + if (selectedOption == 4) + buildExpandedContent(_channelMessageStore)!, const Divider(height: 1), buildOptionTile( optionIndex: 5, @@ -1376,7 +1396,8 @@ class _ChannelsScreenState extends State title: dialogContext.l10n.community_create, subtitle: dialogContext.l10n.community_createDesc, ), - if (selectedOption == 5) buildExpandedContent()!, + if (selectedOption == 5) + buildExpandedContent(_channelMessageStore)!, ], ), ), @@ -1526,7 +1547,7 @@ class _ChannelsScreenState extends State try { await connector.deleteChannel(channel.index); - channelMessageStore.clearChannelMessages(channel.index); + await channelMessageStore.clearChannelMessages(channel.index); if (!context.mounted) return; @@ -1751,6 +1772,7 @@ class _ChannelsScreenState extends State } final channelCount = communityChannels.length; + _communityStore.setPublicKeyHex = connector.selfPublicKeyHex; showDialog( context: context, diff --git a/lib/screens/community_qr_scanner_screen.dart b/lib/screens/community_qr_scanner_screen.dart index 9f8602d..6852dfa 100644 --- a/lib/screens/community_qr_scanner_screen.dart +++ b/lib/screens/community_qr_scanner_screen.dart @@ -51,6 +51,9 @@ class _CommunityQrScannerScreenState extends State { _isProcessing = true; }); + final connector = context.read(); + _communityStore.setPublicKeyHex = connector.selfPublicKeyHex; + try { // Parse the community data final community = Community.fromQrData(const Uuid().v4(), data); @@ -209,6 +212,8 @@ class _CommunityQrScannerScreenState extends State { bool addPublicChannel, ) async { // Save community to local storage + final connector = context.read(); + _communityStore.setPublicKeyHex = connector.selfPublicKeyHex; await _communityStore.addCommunity(community); // Optionally add the community public channel to the device diff --git a/lib/screens/discovery_screen.dart b/lib/screens/discovery_screen.dart index f122654..7f065aa 100644 --- a/lib/screens/discovery_screen.dart +++ b/lib/screens/discovery_screen.dart @@ -7,7 +7,7 @@ import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../l10n/l10n.dart'; -import '../models/discovery_contact.dart'; +import '../models/contact.dart'; import '../utils/contact_search.dart'; import '../widgets/app_bar.dart'; import '../widgets/list_filter_widget.dart'; @@ -129,7 +129,7 @@ class _DiscoveryScreenState extends State { } Future _showContactContextMenu( - DiscoveryContact contact, + Contact contact, MeshCoreConnector connector, ) async { final action = await showModalBottomSheet( @@ -169,7 +169,8 @@ class _DiscoveryScreenState extends State { connector.importDiscoveredContact(contact); break; case 'copy_contact': - final hexString = pubKeyToHex(contact.rawPacket); + if (contact.rawPacket == null) return; + final hexString = pubKeyToHex(contact.rawPacket!); Clipboard.setData(ClipboardData(text: "meshcore://$hexString")); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( @@ -207,7 +208,7 @@ class _DiscoveryScreenState extends State { } Widget _buildFilters( - List filteredAndSorted, + List filteredAndSorted, MeshCoreConnector connector, ) { String hintText = ""; @@ -309,8 +310,8 @@ class _DiscoveryScreenState extends State { ); } - List _filterAndSortContacts( - List contacts, + List _filterAndSortContacts( + List contacts, MeshCoreConnector connector, ) { var filtered = contacts.where((contact) { @@ -350,7 +351,7 @@ class _DiscoveryScreenState extends State { return filtered; } - bool _matchesTypeFilter(DiscoveryContact contact) { + bool _matchesTypeFilter(Contact contact) { switch (typeFilter) { case ContactTypeFilter.all: return true; diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 3d94701..7ffec56 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'dart:typed_data'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; @@ -50,7 +51,8 @@ class MapScreen extends StatefulWidget { } class _MapScreenState extends State { - static const double _labelZoomThreshold = 8.5; + // Zoom level at which node labels start to appear + static const double _labelZoomThreshold = 12.0; final MapController _mapController = MapController(); final MapMarkerService _markerService = MapMarkerService(); @@ -91,6 +93,15 @@ class _MapScreenState extends State { }); } + bool _checkLocationPlausibility(double lat, double lon) { + const double epsilon = 1e-6; + return (lat.abs() > epsilon || lon.abs() > epsilon) && + lat >= -90.0 && + lat <= 90.0 && + lon >= -180.0 && + lon <= 180.0; + } + double _standardDeviation(List values) { if (values.length <= 1) { return 0.0; @@ -126,7 +137,15 @@ class _MapScreenState extends State { builder: (context, connector, settingsService, pathHistory, child) { final tileCache = context.read(); final settings = settingsService.settings; - final contacts = connector.contacts; + final allContacts = [ + ...connector.contacts, + ...connector.discoveredContacts.where((c) => !c.isActive), + ]; + + final contacts = settings.mapShowDiscoveryContacts + ? allContacts + : allContacts.where((c) => c.isActive).toList(); + final highlightPosition = widget.highlightPosition; final sharedMarkers = settings.mapShowMarkers ? _collectSharedMarkers(connector) @@ -159,14 +178,21 @@ class _MapScreenState extends State { : filteredByTime; // Filter by location - final contactsWithLocation = filteredByKeyPrefix - .where((c) => c.hasLocation) - .toList(); + final contactsWithLocation = filteredByKeyPrefix.where((c) { + if (!c.hasLocation) { + return false; + } + return _checkLocationPlausibility(c.latitude!, c.longitude!); + }).toList(); // All contacts with a known location — used as anchors regardless of // time/key-prefix filters so that repeaters are always available. - final allContactsWithLocation = contacts - .where((c) => c.hasLocation) + final allContactsWithLocation = allContacts + .where( + (c) => + c.hasLocation && + _checkLocationPlausibility(c.latitude!, c.longitude!), + ) .toList(); // Compute guessed locations with caching @@ -468,7 +494,10 @@ class _MapScreenState extends State { ), ), if (!_isBuildingPathTrace) - ...guessedLocations.map(_buildGuessedMarker), + ..._buildGuessedMarker( + guessedLocations, + showLabels: _showNodeLabels, + ), ..._buildMarkers( contactsWithLocation, settings, @@ -630,6 +659,13 @@ class _MapScreenState extends State { anchors[0].latitude + offsetDeg * cos(angle), anchors[0].longitude + offsetDeg * sin(angle), ); + + if (!_checkLocationPlausibility( + position.latitude, + position.longitude, + )) { + continue; // discard implausible guesses near (0, 0) + } } else { double lat = 0, lon = 0; for (final a in anchors) { @@ -637,6 +673,12 @@ class _MapScreenState extends State { lon += a.longitude; } position = LatLng(lat / anchors.length, lon / anchors.length); + if (!_checkLocationPlausibility( + position.latitude, + position.longitude, + )) { + continue; // discard implausible guesses near (0, 0 + } } result.add( _GuessedLocation( @@ -710,40 +752,61 @@ class _MapScreenState extends State { .toList(); } - Marker _buildGuessedMarker(_GuessedLocation guess) { - final color = _getNodeColor(guess.contact.type); - return Marker( - point: guess.position, - width: 35, - height: 35, - child: GestureDetector( - onTap: () => _showNodeInfo( - context, - guess.contact, - guessedPosition: guess.position, - ), - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: color.withValues(alpha: guess.highConfidence ? 0.55 : 0.30), - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.3), - blurRadius: 4, - offset: const Offset(0, 2), + List _buildGuessedMarker( + List<_GuessedLocation> guessed, { + required bool showLabels, + }) { + final markers = []; + + for (final guess in guessed) { + final color = _getNodeColor(guess.contact.type); + final marker = Marker( + point: guess.position, + width: 35, + height: 35, + child: GestureDetector( + onTap: () => _showNodeInfo( + context, + guess.contact, + guessedPosition: guess.position, + ), + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: color.withValues( + alpha: guess.highConfidence ? 0.55 : 0.30, ), - ], - ), - child: const Icon( - Icons.not_listed_location, - color: Colors.white, - size: 20, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: const Icon( + Icons.not_listed_location, + color: Colors.white, + size: 20, + ), ), ), - ), - ); + ); + + markers.add(marker); + + if (showLabels) { + markers.add( + _buildNodeLabelMarker( + point: guess.position, + label: guess.contact.name, + ), + ); + } + } + return markers; } List _buildMarkers( @@ -1203,6 +1266,7 @@ class _MapScreenState extends State { Contact contact, { LatLng? guessedPosition, }) { + final connector = context.read(); showDialog( context: context, builder: (dialogContext) => AlertDialog( @@ -1248,6 +1312,9 @@ class _MapScreenState extends State { advTypeChat) // Only show chat button for chat nodes TextButton( onPressed: () { + if (!contact.isActive) { + connector.importDiscoveredContact(contact); + } Navigator.pop(dialogContext); Navigator.push( context, @@ -1261,6 +1328,9 @@ class _MapScreenState extends State { if (contact.type == advTypeRepeater) TextButton( onPressed: () { + if (!contact.isActive) { + connector.importDiscoveredContact(contact); + } Navigator.pop(dialogContext); _showRepeaterLogin(context, contact); }, @@ -1269,6 +1339,9 @@ class _MapScreenState extends State { if (contact.type == advTypeRoom) TextButton( onPressed: () { + if (!contact.isActive) { + connector.importDiscoveredContact(contact); + } Navigator.pop(dialogContext); _showRoomLogin(context, contact); }, @@ -1745,6 +1818,14 @@ class _MapScreenState extends State { }, contentPadding: EdgeInsets.zero, ), + CheckboxListTile( + title: Text(context.l10n.map_showDiscoveryContacts), + value: settings.mapShowDiscoveryContacts, + onChanged: (value) { + service.setMapShowDiscoveryContacts(value ?? true); + }, + contentPadding: EdgeInsets.zero, + ), const SizedBox(height: 16), Text( context.l10n.map_keyPrefix, diff --git a/lib/screens/neighbors_screen.dart b/lib/screens/neighbors_screen.dart index 3dee339..5cb8e45 100644 --- a/lib/screens/neighbors_screen.dart +++ b/lib/screens/neighbors_screen.dart @@ -124,12 +124,14 @@ class _NeighborsScreenState extends State { void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) { final buffer = BufferReader(frame); + final contacts = [ + ...connector.contacts, + ...connector.discoveredContacts, + ]; try { final neighborCount = buffer.readUInt16LE(); final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE()); - connector.contacts.where((c) => c.type == advTypeRepeater).forEach(( - repeater, - ) { + contacts.where((c) => c.type == advTypeRepeater).forEach((repeater) { for (var neighborData in parsedNeighbors) { final publicKey = neighborData['publicKey']; if (listEquals( diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index c6d800e..ceb60a6 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -114,14 +114,37 @@ class _PathTraceMapScreenState extends State { super.dispose(); } - Uint8List addReturnPath(Uint8List pathBytes) { - Uint8List? traceBytes; - final len = (pathBytes.length + pathBytes.length - 1); - traceBytes = Uint8List(len); - for (int i = 0; i < pathBytes.length; i++) { - traceBytes[i] = pathBytes[i]; - if (i < pathBytes.length - 1) { - traceBytes[len - 1 - i] = pathBytes[i]; + Uint8List buildPath(Uint8List pathBytes) { + Uint8List traceBytes; + + if (pathBytes.isEmpty) { + traceBytes = Uint8List(1); + traceBytes[0] = widget.targetContact?.publicKey[0] ?? 0; + return traceBytes; + } + + if (widget.targetContact?.type == advTypeRepeater || + widget.targetContact?.type == advTypeRoom) { + final len = (pathBytes.length + pathBytes.length + 1); + traceBytes = Uint8List(len); + traceBytes[pathBytes.length] = widget.targetContact?.publicKey[0] ?? 0; + for (int i = 0; i < pathBytes.length; i++) { + traceBytes[i] = pathBytes[i]; + if (i < pathBytes.length) { + traceBytes[len - 1 - i] = pathBytes[i]; + } + } + } else { + if (pathBytes.length < 2) { + return pathBytes[0] == 0 ? Uint8List(0) : pathBytes; + } + final len = (pathBytes.length + pathBytes.length - 1); + traceBytes = Uint8List(len); + for (int i = 0; i < pathBytes.length; i++) { + traceBytes[i] = pathBytes[i]; + if (i < pathBytes.length - 1) { + traceBytes[len - 1 - i] = pathBytes[i]; + } } } return traceBytes; @@ -142,11 +165,16 @@ class _PathTraceMapScreenState extends State { : widget.path; if (widget.flipPathRound) { - path = addReturnPath(pathTmp); + path = buildPath(pathTmp); } else { path = pathTmp; } + appLogger.info( + 'Initiating path trace with path: ${_formatPathPrefixes(path)}', + tag: 'PathTraceMapScreen', + ); + final connector = Provider.of(context, listen: false); final frame = buildTraceReq( DateTime.now().millisecondsSinceEpoch ~/ 1000, @@ -235,10 +263,11 @@ class _PathTraceMapScreenState extends State { .toList(); Map pathContacts = {}; - - connector.contacts.where((c) => c.type != advTypeChat).forEach(( - repeater, - ) { + final contacts = [ + ...connector.contacts, + ...connector.discoveredContacts, + ]; + contacts.where((c) => c.type != advTypeChat).forEach((repeater) { for (var repeaterData in pathData) { if (listEquals( repeater.publicKey.sublist(0, 1), diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index c74fa40..a52e364 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -134,6 +134,10 @@ class AppSettingsService extends ChangeNotifier { appLogger.setEnabled(value); } + Future setMapShowDiscoveryContacts(bool value) async { + await updateSettings(_settings.copyWith(mapShowDiscoveryContacts: value)); + } + Future setBatteryChemistryForDevice( String deviceId, String chemistry, diff --git a/lib/storage/channel_message_store.dart b/lib/storage/channel_message_store.dart index 50d13f7..7bf44bd 100644 --- a/lib/storage/channel_message_store.dart +++ b/lib/storage/channel_message_store.dart @@ -48,7 +48,7 @@ class ChannelMessageStore { final key = '$keyFor$channelIndex'; final oldKey = '$_keyPrefix$channelIndex'; - String? jsonString = prefs.getString(oldKey); + String? jsonString = prefs.getString(key); if (jsonString == null || jsonString.isEmpty) { // Attempt migration from legacy unscoped key on first load final legacyJsonString = prefs.getString(oldKey); diff --git a/lib/storage/channel_order_store.dart b/lib/storage/channel_order_store.dart index 48a80f2..88d3f7a 100644 --- a/lib/storage/channel_order_store.dart +++ b/lib/storage/channel_order_store.dart @@ -26,7 +26,7 @@ class ChannelOrderStore { return []; } final prefs = PrefsManager.instance; - String? jsonString = prefs.getString(_keyPrefix); + String? jsonString = prefs.getString(keyFor); if (jsonString == null || jsonString.isEmpty) { // Attempt migration from legacy unscoped key on first load final legacyJsonString = prefs.getString(_keyPrefix); diff --git a/lib/storage/channel_settings_store.dart b/lib/storage/channel_settings_store.dart index 3fb00eb..276826d 100644 --- a/lib/storage/channel_settings_store.dart +++ b/lib/storage/channel_settings_store.dart @@ -32,7 +32,7 @@ class ChannelSettingsStore { await prefs.setBool(key, enabled); } } - return prefs.getBool(key) ?? false; + return enabled ?? false; } Future saveSmazEnabled(int channelIndex, bool enabled) async { diff --git a/lib/storage/channel_store.dart b/lib/storage/channel_store.dart index 775398e..4f40482 100644 --- a/lib/storage/channel_store.dart +++ b/lib/storage/channel_store.dart @@ -19,7 +19,7 @@ class ChannelStore { return []; } final prefs = PrefsManager.instance; - String? jsonString = prefs.getString(_keyPrefix); + String? jsonString = prefs.getString(keyFor); if (jsonString == null || jsonString.isEmpty) { // Attempt migration from legacy unscoped key on first load final legacyJsonString = prefs.getString(_keyPrefix); diff --git a/lib/storage/community_store.dart b/lib/storage/community_store.dart index 6df859a..c69d0b8 100644 --- a/lib/storage/community_store.dart +++ b/lib/storage/community_store.dart @@ -25,7 +25,7 @@ class CommunityStore { return []; } final prefs = PrefsManager.instance; - String? jsonString = prefs.getString(_keyPrefix); + String? jsonString = prefs.getString(keyFor); if (jsonString == null || jsonString.isEmpty) { // Attempt migration from legacy unscoped key on first load final legacyJsonString = prefs.getString(_keyPrefix); diff --git a/lib/storage/contact_discovery_store.dart b/lib/storage/contact_discovery_store.dart index ac47615..89ca027 100644 --- a/lib/storage/contact_discovery_store.dart +++ b/lib/storage/contact_discovery_store.dart @@ -1,13 +1,13 @@ import 'dart:convert'; import 'dart:typed_data'; -import '../models/discovery_contact.dart'; +import '../models/contact.dart'; import 'prefs_manager.dart'; class ContactDiscoveryStore { static const String _keyPrefix = 'discovered_contacts'; - Future> loadContacts() async { + Future> loadContacts() async { final prefs = PrefsManager.instance; final jsonStr = prefs.getString(_keyPrefix); if (jsonStr == null) return []; @@ -22,40 +22,62 @@ class ContactDiscoveryStore { } } - Future saveContacts(List contacts) async { + Future saveContacts(List contacts) async { final prefs = PrefsManager.instance; final jsonList = contacts.map(_toJson).toList(); await prefs.setString(_keyPrefix, jsonEncode(jsonList)); } - Map _toJson(DiscoveryContact contact) { + Map _toJson(Contact contact) { return { - 'rawPacket': base64Encode(contact.rawPacket), 'publicKey': base64Encode(contact.publicKey), 'name': contact.name, 'type': contact.type, + 'flags': contact.flags, 'pathLength': contact.pathLength, 'path': base64Encode(contact.path), + 'pathOverride': contact.pathOverride, + 'pathOverrideBytes': contact.pathOverrideBytes != null + ? base64Encode(contact.pathOverrideBytes!) + : null, 'latitude': contact.latitude, 'longitude': contact.longitude, 'lastSeen': contact.lastSeen.millisecondsSinceEpoch, + 'lastMessageAt': contact.lastMessageAt.millisecondsSinceEpoch, + 'rawPacket': contact.rawPacket != null + ? base64Encode(contact.rawPacket!) + : null, }; } - DiscoveryContact _fromJson(Map json) { + Contact _fromJson(Map json) { final lastSeenMs = json['lastSeen'] as int? ?? 0; - return DiscoveryContact( - rawPacket: Uint8List.fromList(base64Decode(json['rawPacket'] as String)), + final lastMessageMs = json['lastMessageAt'] as int?; + return Contact( publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)), name: json['name'] as String? ?? 'Unknown', type: json['type'] as int? ?? 0, + flags: json['flags'] as int? ?? 0, pathLength: json['pathLength'] as int? ?? -1, path: json['path'] != null ? Uint8List.fromList(base64Decode(json['path'] as String)) : Uint8List(0), + pathOverride: json['pathOverride'] as int?, + pathOverrideBytes: json['pathOverrideBytes'] != null + ? Uint8List.fromList( + base64Decode(json['pathOverrideBytes'] as String), + ) + : null, latitude: (json['latitude'] as num?)?.toDouble(), longitude: (json['longitude'] as num?)?.toDouble(), lastSeen: DateTime.fromMillisecondsSinceEpoch(lastSeenMs), + lastMessageAt: DateTime.fromMillisecondsSinceEpoch( + lastMessageMs ?? lastSeenMs, + ), + isActive: false, + rawPacket: json['rawPacket'] != null + ? Uint8List.fromList(base64Decode(json['rawPacket'] as String)) + : null, ); } } diff --git a/lib/storage/contact_group_store.dart b/lib/storage/contact_group_store.dart index 986bfdd..ce6a0c6 100644 --- a/lib/storage/contact_group_store.dart +++ b/lib/storage/contact_group_store.dart @@ -18,7 +18,7 @@ class ContactGroupStore { return []; } final prefs = PrefsManager.instance; - String? jsonString = prefs.getString(_keyPrefix); + String? jsonString = prefs.getString(keyFor); if (jsonString == null || jsonString.isEmpty) { // Attempt migration from legacy unscoped key on first load final legacyJsonString = prefs.getString(_keyPrefix); diff --git a/lib/storage/contact_store.dart b/lib/storage/contact_store.dart index a4e2f0d..0e2e3ad 100644 --- a/lib/storage/contact_store.dart +++ b/lib/storage/contact_store.dart @@ -76,6 +76,10 @@ class ContactStore { 'longitude': contact.longitude, 'lastSeen': contact.lastSeen.millisecondsSinceEpoch, 'lastMessageAt': contact.lastMessageAt.millisecondsSinceEpoch, + 'isActive': contact.isActive, + 'rawPacket': contact.rawPacket != null + ? base64Encode(contact.rawPacket!) + : null, }; } @@ -103,6 +107,10 @@ class ContactStore { lastMessageAt: DateTime.fromMillisecondsSinceEpoch( lastMessageMs ?? lastSeenMs, ), + isActive: json['isActive'] as bool? ?? true, + rawPacket: json['rawPacket'] != null + ? Uint8List.fromList(base64Decode(json['rawPacket'] as String)) + : null, ); } } diff --git a/lib/storage/unread_store.dart b/lib/storage/unread_store.dart index d46fb41..3b615b1 100644 --- a/lib/storage/unread_store.dart +++ b/lib/storage/unread_store.dart @@ -32,7 +32,7 @@ class UnreadStore { return {}; } final prefs = PrefsManager.instance; - String? jsonString = prefs.getString(_keyPrefix); + String? jsonString = prefs.getString(keyFor); if (jsonString == null || jsonString.isEmpty) { // Attempt migration from legacy unscoped key on first load final legacyJsonString = prefs.getString(_keyPrefix); diff --git a/lib/utils/contact_search.dart b/lib/utils/contact_search.dart index beec880..1f05fdc 100644 --- a/lib/utils/contact_search.dart +++ b/lib/utils/contact_search.dart @@ -1,5 +1,3 @@ -import 'package:meshcore_open/models/discovery_contact.dart'; - import '../models/contact.dart'; bool matchesContactQuery(Contact contact, String query) { @@ -16,7 +14,7 @@ bool matchesContactQuery(Contact contact, String query) { return contact.publicKeyHex.toLowerCase().startsWith(hexPrefix); } -bool matchesDiscoveryContactQuery(DiscoveryContact contact, String query) { +bool matchesDiscoveryContactQuery(Contact contact, String query) { final normalizedQuery = query.trim().toLowerCase(); if (normalizedQuery.isEmpty) return true; diff --git a/lib/widgets/snr_indicator.dart b/lib/widgets/snr_indicator.dart index 1f592eb..f122836 100644 --- a/lib/widgets/snr_indicator.dart +++ b/lib/widgets/snr_indicator.dart @@ -157,8 +157,11 @@ class _SNRIndicatorState extends State { repeater.snr, widget.connector.currentSf, ); - - final name = widget.connector.contacts + final allContacts = [ + ...widget.connector.contacts, + ...widget.connector.discoveredContacts, + ]; + final name = allContacts .where((c) => c.publicKey.first == repeater.pubkeyFirstByte) .map((c) => c.name) .firstOrNull;