From 1fba5312a234d19f7c617f04ec49937d3da97c82 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Thu, 12 Mar 2026 00:14:48 -0700 Subject: [PATCH] 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 89aeca09..2ea09cac 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 582fee74..00820ed7 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 1151514d..9c9f7e8c 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 b9657c4d..48a80f22 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 eee97aab..3b639cd8 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 eaa7a615..1bad7e31 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 a81cccdb..c7198e7b 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 37bfbb4d..ac47615d 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 907cc5c3..c1a77027 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 5a7949d5..94c6430e 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 504ff165..8f9e84da 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 9526ef32..82caa786 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 201d25e3..c0ceceea 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; }