Skip to content

Refactor storage classes to include companion's public key#277

Merged
wel97459 merged 8 commits into
mainfrom
dev-dbDevicePrefix
Mar 12, 2026
Merged

Refactor storage classes to include companion's public key#277
wel97459 merged 8 commits into
mainfrom
dev-dbDevicePrefix

Conversation

@wel97459

Copy link
Copy Markdown
Collaborator

This adds support to connect to multiple companions without having the data bleed over into each other.

Adds 5 bytes of the companion's public key to the prefix of the data stores.

Copilot AI review requested due to automatic review settings March 11, 2026 02:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors multiple SharedPreferences-backed storage classes to namespace persisted data by the connected companion (by prefixing keys with the first 5 bytes / 10 hex chars of the companion’s public key) to prevent data bleed when switching between multiple companions.

Changes:

  • Add publicKeyHex-based key namespacing to most lib/storage/*_store.dart classes.
  • Wire up store key initialization from MeshCoreConnector.selfPublicKeyHex once self info is available.
  • Update Channels UI usage to set the key for ChannelMessageStore before accessing persisted channel messages.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
lib/storage/unread_store.dart Namespaces unread-count persistence key by companion public key prefix.
lib/storage/message_store.dart Namespaces message persistence keys by companion public key prefix.
lib/storage/contact_store.dart Namespaces contacts persistence key by companion public key prefix.
lib/storage/contact_settings_store.dart Namespaces contact settings keys by companion public key prefix.
lib/storage/contact_group_store.dart Namespaces contact group persistence key by companion public key prefix.
lib/storage/contact_discovery_store.dart Intended to namespace discovered contacts by companion, but currently does not.
lib/storage/community_store.dart Namespaces community persistence key by companion public key prefix (requires callers to set it).
lib/storage/channel_store.dart Namespaces channels persistence key by companion public key prefix.
lib/storage/channel_settings_store.dart Namespaces channel settings keys by companion public key prefix.
lib/storage/channel_order_store.dart Namespaces channel order persistence key by companion public key prefix.
lib/storage/channel_message_store.dart Namespaces channel message keys by companion public key prefix (guards missing in mutating methods).
lib/screens/channels_screen.dart Sets ChannelMessageStore public key prefix from connector.
lib/connector/meshcore_connector.dart Adds selfPublicKeyHex and sets store key prefixes after SELF_INFO is received.
Comments suppressed due to low confidence (2)

lib/storage/channel_message_store.dart:31

  • saveChannelMessages/clearChannelMessages/clearAllChannelMessages don’t guard against an unset publicKeyHex. With an empty key, these methods will write/remove keys under the shared _keyPrefix namespace, which can cause cross-companion data bleed or mass deletion. Add the same early-return/log guard pattern used by the other stores before computing keyFor/iterating getKeys().
  /// Save messages for a specific channel
  Future<void> saveChannelMessages(
    int channelIndex,
    List<ChannelMessage> messages,
  ) async {
    final prefs = PrefsManager.instance;
    final key = '$keyFor$channelIndex';

    // Convert messages to JSON
    final jsonList = messages.map((msg) => _messageToJson(msg)).toList();
    final jsonString = jsonEncode(jsonList);

    await prefs.setString(key, jsonString);
  }

lib/connector/meshcore_connector.dart:1208

  • loadDiscoveredContactCache() is no longer invoked during initial sync (and there are no other callers), so _discoveredContacts will always start empty on app launch even though _persistDiscoveredContacts() still saves updates. Add a call to loadDiscoveredContactCache() once the discovery store has been namespaced with the companion key (e.g., after self info is available).
    _bleInitialSyncStarted = true;

    await _requestDeviceInfo();
    _startBatteryPolling();

    final gotSelfInfo = await _waitForSelfInfo(
      timeout: const Duration(seconds: 3),
    );
    if (!gotSelfInfo) {
      await refreshDeviceInfo();
      await _waitForSelfInfo(timeout: const Duration(seconds: 3));
    }

    await syncTime();
    unawaited(getChannels());
  }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

set setPublicKeyHex(String value) =>
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';

String get keyFor => _keyPrefix;

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keyFor currently ignores publicKeyHex (String get keyFor => _keyPrefix;), so discovered contacts will still be persisted under a shared key across companions. This defeats the PR’s isolation goal; update keyFor to incorporate the truncated public key (consistent with other stores).

Suggested change
String get keyFor => _keyPrefix;
String get keyFor => '$_keyPrefix_$publicKeyHex';

Copilot uses AI. Check for mistakes.
Comment on lines 38 to 42
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(keyFor, jsonEncode(jsonList));
}

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

saveContacts does not guard against an unset publicKeyHex, unlike loadContacts. Once keyFor is made companion-specific, saving before setting the key would write to an empty-suffix key (or the wrong namespace) and can reintroduce data bleed; add the same early-return/log guard used by the other stores.

Copilot uses AI. Check for mistakes.
Comment on lines +21 to 28
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));

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

saveMessages/loadMessages now use keyFor (includes publicKeyHex), but clearMessages still removes '$_keyPrefix$contactKeyHex' (old/global key). This means removing a contact won’t clear its persisted conversation for the current companion, and may delete legacy/other-companion data instead; update clearMessages to use the same namespaced key as save/load (and apply the same empty-key guard).

Copilot uses AI. Check for mistakes.

String publicKeyHex = '';
set setPublicKeyHex(String value) =>
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several stores use publicKeyHex = value.length > 10 ? value.substring(0, 10) : ''. Using > 10 drops the key when the input is exactly 10 hex chars, and using '' for shorter values causes silent no-ops plus repeated warnings. Prefer >= 10 (or min(value.length, 10)) and consider keeping a shorter prefix rather than clearing it so the store remains usable in edge cases/testing.

Suggested change
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
publicKeyHex = value.length >= 10 ? value.substring(0, 10) : value;

Copilot uses AI. Check for mistakes.
Comment on lines 63 to 69
/// 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);
}

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clearAllChannelMessages filters prefs.getKeys() with startsWith(keyFor). If publicKeyHex hasn’t been set (so keyFor is just _keyPrefix), this will match and remove all channel message keys for all companions. Ensure you bail out when publicKeyHex is empty (similar to loadChannelMessages) to avoid unintended data loss.

Copilot uses AI. Check for mistakes.
final connector = context.watch<MeshCoreConnector>();

final channelMessageStore = ChannelMessageStore();
channelMessageStore.setPublicKeyHex = connector.selfPublicKeyHex;

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This screen creates/uses a CommunityStore, but after this PR CommunityStore won’t load/save unless setPublicKeyHex is called. You’ve set the key for ChannelMessageStore here; do the same for _communityStore (and any other per-companion stores used by this screen) to avoid communities silently not persisting.

Suggested change
channelMessageStore.setPublicKeyHex = connector.selfPublicKeyHex;
channelMessageStore.setPublicKeyHex = connector.selfPublicKeyHex;
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;

Copilot uses AI. Check for mistakes.

// Now that we have self info, we can load all the persisted data for this node
_loadChannelOrder();
loadContactCache();

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After setting the stores’ publicKeyHex, you reload several persisted datasets but not discovered contacts. Since loadDiscoveredContactCache() isn’t called elsewhere, add it here so the discovery list is restored per companion. Also, because _loadChannelOrder() now depends on publicKeyHex, consider removing the earlier _loadChannelOrder() call in initialize() to avoid warning logs and double-loading.

Suggested change
loadContactCache();
loadContactCache();
loadDiscoveredContactCache();

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +26
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 [];
}

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CommunityStore now hard-requires publicKeyHex to be set (otherwise load/save no-op). Current call sites that construct CommunityStore() (e.g. ChannelsScreen / CommunityQrScannerScreen) don’t set it, so communities will stop persisting/loading after this change. Either plumb the companion key into these screens (or accept it in the constructor) and ensure it’s always set before any operation.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings March 12, 2026 02:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 21 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


String publicKeyHex = '';
set setPublicKeyHex(String value) =>
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setPublicKeyHex uses value.length > 10, which clears publicKeyHex when the input is exactly 10 chars (the intended prefix length). Accept >= 10 (or clamp via min).

Suggested change
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
publicKeyHex = value.length >= 10 ? value.substring(0, 10) : '';

Copilot uses AI. Check for mistakes.
Comment thread lib/storage/community_store.dart Outdated
Comment on lines 28 to 29
final jsonString = prefs.getString(keyFor);
if (jsonString == null || jsonString.isEmpty) {

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This store switched from the legacy key (communities_v1) to a scoped key (keyFor). Without a migration, existing users will appear to lose stored communities after upgrading. Consider migrating/copying the legacy value into keyFor the first time you load for a given publicKeyHex.

Suggested change
final jsonString = prefs.getString(keyFor);
if (jsonString == null || jsonString.isEmpty) {
String? jsonString = prefs.getString(keyFor);
if (jsonString == null || jsonString.isEmpty) {
// Attempt migration from legacy unscoped key on first load
final legacyJsonString = prefs.getString(_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) {

Copilot uses AI. Check for mistakes.
Comment thread lib/storage/channel_store.dart Outdated
static const String _keyPrefix = 'channels';
String publicKeyHex = '';
set setPublicKeyHex(String value) =>
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setPublicKeyHex uses value.length > 10, which clears publicKeyHex when the provided value is exactly 10 chars (the intended 5-byte prefix). Accept >= 10 (or clamp).

Suggested change
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
publicKeyHex = value.length >= 10 ? value.substring(0, 10) : '';

Copilot uses AI. Check for mistakes.
Comment on lines +2505 to +2512
_loadChannelOrder();
loadContactCache();
loadChannelSettings();
loadCachedChannels();

// Load persisted channel messages
loadAllChannelMessages();
loadUnreadState();

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These Future-returning load methods are called without await/unawaited. The repo already uses unawaited(...) elsewhere, so this likely triggers unawaited_futures warnings and makes sequencing less explicit. Wrap these calls in unawaited(...) (or make _handleSelfInfo async and await them).

Suggested change
_loadChannelOrder();
loadContactCache();
loadChannelSettings();
loadCachedChannels();
// Load persisted channel messages
loadAllChannelMessages();
loadUnreadState();
unawaited(_loadChannelOrder());
unawaited(loadContactCache());
unawaited(loadChannelSettings());
unawaited(loadCachedChannels());
// Load persisted channel messages
unawaited(loadAllChannelMessages());
unawaited(loadUnreadState());

Copilot uses AI. Check for mistakes.
Comment thread lib/storage/unread_store.dart Outdated
Comment on lines +12 to +13
set setPublicKeyHex(String value) =>
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setPublicKeyHex currently clears publicKeyHex unless value.length > 10. This rejects exactly-10-char prefixes (the intended 5 bytes) and can leave the store unscoped. Use a condition that accepts length 10 (and ideally derive the prefix via min(value.length, 10)).

Suggested change
set setPublicKeyHex(String value) =>
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
set setPublicKeyHex(String value) {
if (value.isEmpty) {
publicKeyHex = '';
} else if (value.length <= 10) {
publicKeyHex = value;
} else {
publicKeyHex = value.substring(0, 10);
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +16
String get keyFor => '$_keyPrefix$publicKeyHex';

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keyFor now scopes message storage by companion, but clearMessages() in this class still removes the legacy unscoped key ($_keyPrefix$contactKeyHex). This means scoped messages won’t be cleared (and could also delete another companion’s legacy data). Update clearMessages() to use the same scoped key as saveMessages/loadMessages and add the same publicKeyHex guard.

Copilot uses AI. Check for mistakes.

String publicKeyHex = '';
set setPublicKeyHex(String value) =>
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setPublicKeyHex uses value.length > 10, which will set publicKeyHex to empty when the provided value is exactly 10 chars (the intended 5-byte / 10-hex-char prefix). Accept >= 10 (or clamp via min).

Suggested change
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
publicKeyHex = value.length >= 10 ? value.substring(0, 10) : '';

Copilot uses AI. Check for mistakes.

String publicKeyHex = '';
set setPublicKeyHex(String value) =>
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setPublicKeyHex currently requires value.length > 10, which rejects exactly-10-char prefixes and leaves the store unscoped. Since the intended prefix is 10 hex chars, accept >= 10 (or clamp).

Suggested change
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
publicKeyHex = value.length >= 10 ? value.substring(0, 10) : '';

Copilot uses AI. Check for mistakes.

String publicKeyHex = '';
set setPublicKeyHex(String value) =>
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setPublicKeyHex currently requires value.length > 10, which rejects exactly-10-char prefixes and leaves this store unscoped. Since the intended prefix is 10 hex chars, accept >= 10 (or clamp).

Suggested change
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
publicKeyHex = value.length >= 10 ? value.substring(0, 10) : '';

Copilot uses AI. Check for mistakes.
Comment on lines 57 to 60
Future<void> clearChannelMessages(int channelIndex) async {
final prefs = PrefsManager.instance;
final key = '$_keyPrefix$channelIndex';
final key = '$keyFor$channelIndex';
await prefs.remove(key);

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clearChannelMessages doesn’t guard against an empty publicKeyHex. If it’s unset, this can delete unscoped (shared) channel message data. Add a guard similar to loadChannelMessages() to prevent deletions when the store isn’t scoped.

Copilot uses AI. Check for mistakes.
@wel97459

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 796f6f4ccd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}
final prefs = PrefsManager.instance;
final key = '$_keyPrefix$contactKeyHex';
final key = '$keyFor$contactKeyHex';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep message deletion key format consistent

This change namespaces saved messages with keyFor, but clearMessages in the same class still deletes $_keyPrefix$contactKeyHex (without the companion prefix). When MeshCoreConnector removes a contact and calls clearMessages, the newly saved per-companion conversation is left behind and can reappear on next load, so delete semantics are now broken for all contacts saved after this refactor.

Useful? React with 👍 / 👎.

Comment on lines +23 to +25
if (publicKeyHex.isEmpty) {
appLogger.warn('Public key hex is not set. Cannot load communities.');
return [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Initialize CommunityStore namespace before enforcing guard

CommunityStore now returns early whenever publicKeyHex is unset, but no call site sets setPublicKeyHex before using it (e.g. community load/add flows in ChannelsScreen and CommunityQrScannerScreen). That makes loadCommunities always return [] and saveCommunities no-op, so creating/joining/managing communities no longer persists after this commit.

Useful? React with 👍 / 👎.

Comment on lines +16 to +18
if (publicKeyHex.isEmpty) {
appLogger.warn('Public key hex is not set. Cannot load contact groups.');
return [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Initialize ContactGroupStore key before load/save

ContactGroupStore now hard-fails reads/writes when publicKeyHex is empty, but the ContactsScreen usage never sets setPublicKeyHex. As a result, group data always loads as empty and save operations are dropped, so contact grouping persistence is effectively disabled by this change.

Useful? React with 👍 / 👎.

Copilot AI review requested due to automatic review settings March 12, 2026 05:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 70 to 75
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);
}

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clearAllChannelMessages filters keys with startsWith(keyFor). If publicKeyHex is empty (because setPublicKeyHex hasn’t been called yet), keyFor becomes just the base prefix and this will match/remove all channel message keys across companions. Add the same publicKeyHex.isEmpty guard used in the load/save methods before iterating/removing keys.

Copilot uses AI. Check for mistakes.
Comment on lines 666 to 668
_loadChannelOrder();
_loadDiscoveredContactCache();

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initialize() calls _loadChannelOrder() before setPublicKeyHex is applied to the stores (this only happens later once self info is available). With the new store guards, this first load will return an empty order (and log warnings), then notify listeners, and later reload again once self info arrives—potentially causing unnecessary UI churn/flicker. Consider deferring _loadChannelOrder() until after the stores are scoped to selfPublicKeyHex (or make the stores tolerate an unset key without logging/overwriting state).

Suggested change
_loadChannelOrder();
_loadDiscoveredContactCache();
// Only load key-scoped stores once the self public key is available.
if (_selfPublicKey != null) {
_loadChannelOrder();
_loadDiscoveredContactCache();
}

Copilot uses AI. Check for mistakes.
bool get isLoadingChannels => _isLoadingChannels;
Stream<Uint8List> get receivedFrames => _receivedFramesController.stream;
Uint8List? get selfPublicKey => _selfPublicKey;
String get selfPublicKeyHex => pubKeyToHex(_selfPublicKey ?? Uint8List(0));

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

selfPublicKeyHex currently allocates a new Uint8List(0) every time the getter is called when _selfPublicKey is null. Since this getter may be read frequently during widget rebuilds, consider avoiding the allocation by explicitly returning '' when _selfPublicKey == null (or caching the computed hex string when the self key is set).

Suggested change
String get selfPublicKeyHex => pubKeyToHex(_selfPublicKey ?? Uint8List(0));
String get selfPublicKeyHex {
final key = _selfPublicKey;
if (key == null || key.isEmpty) {
return '';
}
return pubKeyToHex(key);
}

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings March 12, 2026 05:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 9 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/storage/contact_store.dart Outdated
Comment on lines +21 to +26
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) {

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadGroups() reads from _keyPrefix instead of the scoped keyFor, and the migration path reads/removes _keyPrefix again. After migration, future loads won't read the scoped value written under keyFor, effectively dropping persisted groups. Read from keyFor first, then fall back to legacy _keyPrefix for one-time migration.

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +37
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);

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadCommunities() reads from _keyPrefix instead of the scoped keyFor, and the migration path reads/removes _keyPrefix again. After migrating, subsequent loads won't read the scoped key, so communities appear to disappear. Read from keyFor first, then fall back to legacy _keyPrefix for one-time migration.

Suggested change
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);
// First try to read from the scoped key
String? jsonString = prefs.getString(keyFor);
if (jsonString == null || jsonString.isEmpty) {
// Attempt migration from legacy unscoped key on first load
final legacyJsonString = prefs.getString(_keyPrefix);
if (legacyJsonString != null && legacyJsonString.isNotEmpty) {
appLogger.info(
'Migrating communities from legacy key $_keyPrefix to scoped key $keyFor',
);
await prefs.setString(keyFor, legacyJsonString);
prefs.remove(_keyPrefix);

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +34
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) {

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadChannelOrder() reads from _keyPrefix ("channel_order_") rather than the scoped keyFor, and the migration also reads/removes _keyPrefix. This both (1) prevents loading the scoped value stored under keyFor, and (2) won't migrate the real legacy key (previously channel_order). Read from keyFor first, and migrate from the old constant key name.

Copilot uses AI. Check for mistakes.
Comment on lines +50 to +59
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);

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadChannelMessages() fetches from prefs.getString(_keyPrefix) instead of prefs.getString(key) (scoped per channel). The migration path also reads/removes _keyPrefix rather than the per-channel legacy key (e.g. channel_messages_<index>). This means scoped messages will never load after migration. Read key first and migrate from the correct per-channel legacy key.

Suggested change
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);
// First, try to load from the scoped key used by saveChannelMessages
String? jsonString = prefs.getString(key);
if (jsonString == null || jsonString.isEmpty) {
// Attempt migration from legacy per-channel key on first load
final String legacyKey = '$_keyPrefix$channelIndex';
final String? legacyJsonString = prefs.getString(legacyKey);
if (legacyJsonString != null && legacyJsonString.isNotEmpty) {
appLogger.info(
'Migrating channel messages from legacy key $legacyKey to scoped key $key',
);
await prefs.setString(key, legacyJsonString);
await prefs.remove(legacyKey);

Copilot uses AI. Check for mistakes.
Comment on lines 76 to 81
/// 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);
}

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clearChannelMessages() does not guard against an unset publicKeyHex. With an empty publicKeyHex, $keyFor$channelIndex collapses to the legacy prefix and can remove another companion's data. Add the same publicKeyHex.isEmpty early-return guard used by the other methods in this store.

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +44
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);

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadContactUnreadCount() reads from _keyPrefix instead of the scoped keyFor, and the migration path also reads/removes _keyPrefix again. After the legacy key is removed, subsequent loads will never read the data written under keyFor, effectively losing unread state. Read from keyFor first, then fall back to the legacy _keyPrefix key for one-time migration.

Suggested change
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);
// Read from scoped key first
String? jsonString = prefs.getString(keyFor);
if (jsonString == null || jsonString.isEmpty) {
// Attempt migration from legacy unscoped key on first load
final legacyJsonString = prefs.getString(_keyPrefix);
if (legacyJsonString != null && legacyJsonString.isNotEmpty) {
appLogger.info(
'Migrating channel messages from legacy key $_keyPrefix to scoped key $keyFor',
);
await prefs.setString(keyFor, legacyJsonString);
await prefs.remove(_keyPrefix);

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +31
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);

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadChannels() reads from _keyPrefix instead of the scoped keyFor, and the migration path reads/removes _keyPrefix again. This prevents loading channels stored under keyFor after the first migration. Read from keyFor first, then fall back to the legacy _keyPrefix key for one-time migration.

Suggested change
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);
String? jsonString = prefs.getString(keyFor);
if (jsonString == null || jsonString.isEmpty) {
// Attempt migration from legacy unscoped key on first load
final legacyJsonString = prefs.getString(_keyPrefix);
if (legacyJsonString != null && legacyJsonString.isNotEmpty) {
appLogger.info(
'Migrating channel messages from legacy key $_keyPrefix to scoped key $keyFor',
);
await prefs.setString(keyFor, legacyJsonString);
prefs.remove(_keyPrefix);

Copilot uses AI. Check for mistakes.
Comment on lines 83 to 88
/// 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);

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clearAllChannelMessages() now filters keys by keyFor. If publicKeyHex is unset, keyFor collapses to the legacy prefix and this method will delete all channel message keys across companions. Add a publicKeyHex.isEmpty guard (consistent with save/load) before enumerating/removing keys.

Copilot uses AI. Check for mistakes.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 12, 2026 06:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

prefs.remove(_keyPrefix);
if (legacyJsonString != null && legacyJsonString.isNotEmpty) {
appLogger.info(
'Migrating channel messages from legacy key $_keyPrefix to scoped key $keyFor',

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The migration log message says "Migrating channel messages" but this store migrates contact groups; the message should reflect the data type being migrated.

Suggested change
'Migrating channel messages from legacy key $_keyPrefix to scoped key $keyFor',
'Migrating contact groups from legacy key $_keyPrefix to scoped key $keyFor',

Copilot uses AI. Check for mistakes.
prefs.remove(_keyPrefix);
if (legacyJsonString != null && legacyJsonString.isNotEmpty) {
appLogger.info(
'Migrating channel messages from legacy key $_keyPrefix to scoped key $keyFor',

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The migration log message says "Migrating channel messages" but this store migrates channel metadata (channels list). Consider updating it to avoid confusing logs.

Suggested change
'Migrating channel messages from legacy key $_keyPrefix to scoped key $keyFor',
'Migrating channel metadata (channels list) from legacy key $_keyPrefix to scoped key $keyFor',

Copilot uses AI. Check for mistakes.
prefs.remove(_keyPrefix);
if (legacyJsonString != null && legacyJsonString.isNotEmpty) {
appLogger.info(
'Migrating channel messages from legacy key $_keyPrefix to scoped key $keyFor',

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The migration log message refers to "channel messages", but this store migrates unread counts. Updating the log message will make troubleshooting and log filtering accurate.

Suggested change
'Migrating channel messages from legacy key $_keyPrefix to scoped key $keyFor',
'Migrating unread counts from legacy key $_keyPrefix to scoped key $keyFor',

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +30
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);

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadGroups() reads from _keyPrefix (legacy/unscoped) but saveGroups() writes to keyFor (scoped). This means groups won’t load after the first scoped save. Read from keyFor first, and migrate from _keyPrefix only when keyFor is missing.

Suggested change
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);
String? jsonString = prefs.getString(keyFor);
if (jsonString == null || jsonString.isEmpty) {
// Attempt migration from legacy unscoped key on first load
final legacyJsonString = prefs.getString(_keyPrefix);
if (legacyJsonString != null && legacyJsonString.isNotEmpty) {
appLogger.info(
'Migrating contact groups from legacy key $_keyPrefix to scoped key $keyFor',
);
await prefs.setString(keyFor, legacyJsonString);
await prefs.remove(_keyPrefix);

Copilot uses AI. Check for mistakes.
@wel97459 wel97459 merged commit 1fba531 into main Mar 12, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants