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>
This commit is contained in:
Winston Lowe
2026-03-12 00:14:48 -07:00
committed by GitHub
parent a1b77bb29b
commit 1fba5312a2
13 changed files with 378 additions and 51 deletions
+24 -2
View File
@@ -291,6 +291,7 @@ class MeshCoreConnector extends ChangeNotifier {
bool get isLoadingChannels => _isLoadingChannels;
Stream<Uint8List> 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<void> loadDiscoveredContactCache() async {
Future<void> _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;
+2
View File
@@ -106,7 +106,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
@override
Widget build(BuildContext context) {
final connector = context.watch<MeshCoreConnector>();
final channelMessageStore = ChannelMessageStore();
channelMessageStore.setPublicKeyHex = connector.selfPublicKeyHex;
// Auto-navigate back to scanner if disconnected
if (!checkConnectionAndNavigate(connector)) {
+40 -6
View File
@@ -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<void> saveChannelMessages(
int channelIndex,
List<ChannelMessage> 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<List<ChannelMessage>> 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<dynamic>;
@@ -42,14 +76,14 @@ class ChannelMessageStore {
/// Clear messages for a specific channel
Future<void> 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<void> 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);
}
+35 -6
View File
@@ -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<void> saveChannelOrder(List<int> 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<List<int>> 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<int>()
+35 -3
View File
@@ -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<bool> 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<void> 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);
}
}
+33 -5
View File
@@ -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<List<Channel>> 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<dynamic>;
final jsonList = jsonDecode(jsonString) as List<dynamic>;
return jsonList
.map((entry) => _fromJson(entry as Map<String, dynamic>))
.toList();
@@ -23,9 +47,13 @@ class ChannelStore {
}
Future<void> saveChannels(List<Channel> 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<String, dynamic> _toJson(Channel channel) {
+30 -3
View File
@@ -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<List<Community>> 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<void> saveCommunities(List<Community> 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
+3 -3
View File
@@ -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<List<DiscoveryContact>> 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<void> saveContacts(List<DiscoveryContact> 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<String, dynamic> _toJson(DiscoveryContact contact) {
+34 -5
View File
@@ -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<List<ContactGroup>> 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<Map<String, dynamic>>()
@@ -25,8 +50,12 @@ class ContactGroupStore {
}
Future<void> saveGroups(List<ContactGroup> 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);
}
}
+35 -3
View File
@@ -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<bool> 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<void> 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);
}
}
+34 -5
View File
@@ -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<List<Contact>> 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<dynamic>;
final jsonList = jsonDecode(jsonString) as List<dynamic>;
return jsonList
.map((entry) => _fromJson(entry as Map<String, dynamic>))
.toList();
@@ -23,9 +48,13 @@ class ContactStore {
}
Future<void> saveContacts(List<Contact> 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<String, dynamic> _toJson(Contact contact) {
+39 -5
View File
@@ -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<void> saveMessages(
String contactKeyHex,
List<Message> 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<List<Message>> 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<dynamic>;
@@ -32,8 +62,12 @@ class MessageStore {
}
Future<void> 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);
}
+34 -5
View File
@@ -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<Map<String, int>> 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<String, dynamic>;
final json = jsonDecode(jsonString) as Map<String, dynamic>;
return json.map((key, value) => MapEntry(key, value as int));
} catch (_) {
return {};
@@ -33,6 +58,10 @@ class UnreadStore {
}
void saveContactUnreadCount(Map<String, int> 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;
}