Skip to content

Conversation

@josefinalliende
Copy link
Contributor

@josefinalliende josefinalliende commented Sep 2, 2025

Description

As part of the effort to remove the metadata cache provider (#541), this PR removes more references and introduces fixes along the way. One fix is that the edit profile screen now updates metadata correctly (#558), but the main fix is that metadata of active account is now updated. Fixes also #572

With these changes, the metadata of the active account is updated via the polling provider (which invalidates the active account provider). This ensures that metadata updates from other clients are reflected in Whitenoise.

Commit by commit:

  • Removed remaining unnecessary metadata cache provider code.
  • Added a simple userProfileDataProvider that fetches account metadata directly from the DB.
  • Replaced metadata cache provider with userProfileDataProvider in multiple places.
  • Updated polling provider to invalidate the active account provider.
  • Simplified several usages of the active account provider by switching to the active pubkey provider where possible.
  • Fixed a new “blinking” issue in the chat list screen by moving the active account provider watch into a dedicated widget (avoids rebuilding the entire screen).
  • Added changes to the changelog.
  • Fixes active account provider to use get default blossom url method and changes local default to port 3000 to match the port run by default in the whitenoise rust repo.
  • Changes pubkey conversion methods to sync, adding support for ContactModel.fromMetadata to receive pubkey in hex ur npub format. This way it fixes a problem commented in When someone is added to contacts, the button doesn't switch to 'Remove Contact' #562 with npubs showing as hex

Short video of how now when updating metadata in primal it updates in WN

now-update-metadata.mov

And the other way, changing profile picture in WN and seeing updates in primal

updating.metadata.works.mov

Type of Change

  • ✨ New feature (non-breaking change which adds functionality)
  • 🛠️ Bug fix (non-breaking change which fixes an issue)
  • ❌ Breaking change (fix or feature that would cause existing functionality to change)
  • 🧹 Code refactor
  • ✅ Build configuration change
  • 📝 Documentation
  • 🗑️ Chore
  • 🧪 Tests

Checklist

  • Run just precommit to ensure that formatting and linting are correct
  • Run just check-flutter-coverage to ensure that flutter coverage rules are passing
  • Updated the CHANGELOG.md file with your changes (if they affect the user experience)

Summary by CodeRabbit

  • New Features

    • New user profile data provider and header avatar component for account display.
  • Bug Fixes

    • Fixes profile editing when metadata is outdated; more reliable profile loading across chats, groups, welcomes and follows.
  • Refactor

    • Migration to pubkey-based flows; metadata cache and related debug utilities removed.
  • UI

    • Invite/welcome and new-chat screens now use unified profile data; avatar/header updates.
  • Developer

    • Removed Clear Cache action; debug upload server URL adjusted.
  • Tests

    • Added unit tests for the user profile data provider.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 2, 2025

Walkthrough

Replaces account-object flows with a pubkey-first approach across providers and UI, introduces a userProfileDataProvider for fetching user metadata and tests, removes metadata-cache docs/examples/utilities, converts several Rust bridge key-conversion calls to synchronous forms and removes PublicKey opaque bridging, and updates generated FRB bindings and changelog.

Changes

Cohort / File(s) Summary of changes
Providers: activeAccount → activePubkey refactor
lib/config/providers/.../chat_provider.dart, .../follows_provider.dart, .../group_provider.dart, .../nostr_keys_provider.dart, .../relay_status_provider.dart, .../welcomes_provider.dart, .../relay_provider.dart, lib/config/providers/active_account_provider.dart, lib/config/providers/active_pubkey_provider.dart
Switched from reading an Account object to reading activePubkeyProvider (string); replaced future/account reads with synchronous string reads, added empty-string guards, simplified null checks, updated logs/errors and API call args; trimmed pubkey normalization added in active_pubkey_provider.
Metadata cache and examples removed
docs/METADATA_CACHING_SOLUTION.md, lib/examples/metadata_cache_examples.dart, lib/utils/metadata_cache_utils.dart
Deleted long-form metadata-cache design doc, example/demo file, and cache debug/utility helpers; related public utilities removed.
New user profile provider & tests
lib/config/providers/user_profile_data_provider.dart, test/config/providers/user_profile_data_provider_test.dart
Added UserProfileDataNotifier and userProfileDataProvider (injectable wn_users_api.getUser), plus unit tests with MockWnUsersApi and helper mapping to ContactModel.
UI: switch metadata source to userProfileDataProvider / avatar refactor
lib/ui/.../invite/chat_invite_screen.dart, lib/ui/contact_list/new_chat_bottom_sheet.dart, lib/ui/contact_list/widgets/welcome_tile.dart, lib/ui/contact_list/group_welcome_invitation_sheet.dart, lib/ui/contact_list/search_chat_bottom_sheet.dart (deleted), lib/ui/contact_list/chat_list_screen.dart, lib/ui/contact_list/widgets/chat_list_active_account_avatar.dart, lib/ui/contact_list/new_group_chat_sheet.dart, lib/ui/contact_list/widgets/contact_list_tile.dart, lib/ui/chat/.../*, lib/ui/settings/*
Replaced metadata-cache calls with userProfileDataProvider.notifier.getUserProfileData(...); removed/rewrote FutureBuilders; added ChatListActiveAccountAvatar and integrated it; removed SearchChatBottomSheet; updated ContactModel construction to fromMetadata(pubkey, metadata).
Domain models & utilities: pubkey handling
lib/domain/models/contact_model.dart, lib/utils/public_key_validation_extension.dart, lib/utils/string_extensions.dart, lib/utils/message_converter.dart
ContactModel.fromMetadata signature changed to accept pubkey and derive NPUB via toNpub(); removed fromUser factory; added String extensions toNpub/toHex; removed older async toNpub; updated message/contact conversions to use fromMetadata.
Services & UI sync changes for pubkey conversions
lib/domain/services/dm_chat_service.dart, lib/ui/.../chat_info/*, lib/ui/chat/widgets/chat_header_widget.dart, lib/ui/chat/chat_info/widgets/group_member_bottom_sheet.dart, lib/ui/contact_list/widgets/contact_list_tile.dart, lib/ui/settings/profile/*, lib/ui/settings/developer/developer_settings_screen.dart, lib/ui/contact_list/new_chat_bottom_sheet.dart
Replaced metadata_cache usage with wn_users_api or userProfileDataProvider where applicable; switched active-account sourcing to activePubkeyProvider; removed awaits for npub/hex conversions (now synchronous), adjusted control flow and error guards accordingly.
Polling & account invalidation
lib/config/providers/polling_provider.dart
After initial and incremental polling cycles, invalidates activeAccountProvider to refresh account state.
Rust bridge & generated FRB changes
rust/src/api/utils.rs, lib/src/rust/api/utils.dart, rust/src/frb_generated.rs, lib/src/rust/frb_generated.dart, lib/src/rust/frb_generated.io.dart, rust/src/frb_generated.rs
Removed PublicKey-based bridged APIs and PublicKey opaque export; converted npub/hex conversion functions to synchronous (#[frb(sync)]) string-based calls; removed PublicKey wire/FFI scaffolding in generated bindings; adjusted debug blossom URL for debug builds; re-generated FRB artifacts updated.
Changelog
CHANGELOG.md
Added "Fixes profile edition with outdated metadata" under Unreleased > Fixed.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor UI as UI (Widget)
  participant UP as userProfileDataProvider
  participant API as WnUsersApi
  participant RUST as Rust bridge (utils/users)

  UI->>UP: getUserProfileData(pubkey)
  UP->>API: getUser(pubkey)
  API->>RUST: getUser(pubkey)
  RUST-->>API: User(metadata)
  API-->>UP: User
  UP-->>UI: ContactModel.fromMetadata(pubkey, metadata)
  note over UI,UP: Errors thrown by API propagate to caller
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • erskingardner
  • Quwaysim
  • untreu2

Poem

I nibble code beneath the moon,
swapped accounts for pubkeys — quick as a tune.
Caches cleared, bridges now sing sync,
Profiles fetched in a single blink.
Rabbit hops—tests pass—hop, hop, zoom! 🐇

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/metatada-issues

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@josefinalliende josefinalliende linked an issue Sep 2, 2025 that may be closed by this pull request
@josefinalliende josefinalliende marked this pull request as ready for review September 2, 2025 21:55
@josefinalliende josefinalliende linked an issue Sep 2, 2025 that may be closed by this pull request
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (15)
lib/config/providers/metadata_cache_provider.dart (1)

316-327: Standardize cache key to npub in updateCachedMetadata
updateCachedMetadata currently writes under normalized (hex or npub) strings, but reads always use standardized npub via _getStandardizedNpub, causing cache misses and duplicates. Change signature to async and await _getStandardizedNpub as below:

- void updateCachedMetadata(String publicKey, ContactModel contactModel) {
-   final normalizedKey = _normalizePublicKey(publicKey);
+ Future<void> updateCachedMetadata(String publicKey, ContactModel contactModel) async {
+   final String standardized = await _getStandardizedNpub(publicKey);
    final newCache = Map<String, CachedMetadata>.from(state.cache);
-   newCache[normalizedKey] = CachedMetadata(
+   newCache[standardized] = CachedMetadata(
      contactModel: contactModel,
      cachedAt: DateTime.now(),
    );
    state = state.copyWith(cache: newCache);
-   _logger.info('Updated cached metadata for $normalizedKey');
+   _logger.info('Updated cached metadata for $standardized');
 }
lib/config/providers/nostr_keys_provider.dart (1)

33-41: Ensure active pubkey is hex before FRB calls; add explicit type.

activePubkey may be npub; both npubFromHexPubkey and exportAccountNsec expect hex. Normalize first.

-      final activePubkey = ref.read(activePubkeyProvider);
+      final String? activePubkey = ref.read(activePubkeyProvider);
       if (activePubkey == null || activePubkey.isEmpty) {
         _logger.severe('NostrKeysNotifier: No active account found');
         state = state.copyWith(
           isLoading: false,
           error: 'No active account found',
         );
         return;
       }
 
-      _logger.info('NostrKeysNotifier: Loading keys for account: $activePubkey');
+      // Normalize to hex for downstream APIs
+      String hexPubkey = activePubkey;
+      if (hexPubkey.startsWith('npub1')) {
+        hexPubkey = await hexPubkeyFromNpub(npub: hexPubkey);
+      }
+      _logger.info('NostrKeysNotifier: Loading keys for account: $hexPubkey');
lib/config/providers/relay_status_provider.dart (1)

182-188: Make this FutureProvider reactive to active pubkey switches.

Watch activePubkeyProvider so a pubkey change recomputes connectivity.

 final allRelayTypesConnectionProvider = FutureProvider<bool>((ref) async {
   // Watch the relay status provider to trigger rebuilds when statuses change
   ref.watch(relayStatusProvider);
+  // Also watch active pubkey to recompute on account switch
+  ref.watch(activePubkeyProvider);
 
-  final notifier = ref.read(relayStatusProvider.notifier);
+  final RelayStatusNotifier notifier = ref.read(relayStatusProvider.notifier);
   return await notifier.areAllRelayTypesConnected();
 });
lib/ui/settings/general_settings_screen.dart (2)

79-83: Guard setState with mounted after awaits

Prevents “setState called after dispose” race.

-      setState(() {
+      if (!mounted) return;
+      setState(() {
         _accounts = accounts;
         _currentAccount = activeAccount;
         _accountContactModels = contactModels;
-      });
+      });

231-233: Fix off-by-one: treat 2 accounts as “multiple”

> 2 fails when exactly two accounts exist, breaking the post-logout flow.

-    final hasMultipleAccounts = accounts.length > 2;
+    final bool hasMultipleAccounts = accounts.length > 1;
lib/config/providers/follows_provider.dart (1)

81-94: Guard against stale updates when pubkey changes mid-flight

Capture and re-check the active pubkey after awaits to avoid applying results for an old account; also add explicit types per guidelines.

-      final activePubkey = ref.read(activePubkeyProvider);
+      final String? activePubkey = ref.read(activePubkeyProvider);
       if (activePubkey == null || activePubkey.isEmpty) {
         state = state.copyWith(error: 'No active account found', isLoading: false);
         return;
       }
-
-      final follows = await accounts_api.accountFollows(pubkey: activePubkey);
+      final String capturedPubkey = activePubkey;
+      final follows = await accounts_api.accountFollows(pubkey: capturedPubkey);
 
       _logger.info('FollowsProvider: Loaded ${follows.length} follows');
 
       final sortedFollows = UserUtils.sortUsersByName(follows);
 
+      // Drop stale responses if account switched meanwhile
+      if (capturedPubkey != ref.read(activePubkeyProvider)) {
+        _logger.fine('FollowsProvider: Dropping stale follows for $capturedPubkey');
+        state = state.copyWith(isLoading: false);
+        return;
+      }
       state = state.copyWith(follows: sortedFollows, isLoading: false);
lib/config/providers/welcomes_provider.dart (1)

61-84: Prevent stale state updates during async loads; add explicit types

Capture pubkey and verify before mutating state.

-      final activePubkey = ref.read(activePubkeyProvider);
+      final String? activePubkey = ref.read(activePubkeyProvider);
       if (activePubkey == null || activePubkey.isEmpty) {
         state = state.copyWith(error: 'No active account found', isLoading: false);
         return;
       }
-
-      final welcomes = await pendingWelcomes(pubkey: activePubkey);
+      final String capturedPubkey = activePubkey;
+      final welcomes = await pendingWelcomes(pubkey: capturedPubkey);
 
       final welcomeByData = <String, Welcome>{};
       for (final welcome in welcomes) {
         welcomeByData[welcome.id] = welcome;
       }
 
+      if (capturedPubkey != ref.read(activePubkeyProvider)) {
+        _logger.fine('WelcomesProvider: Dropping stale welcomes for $capturedPubkey');
+        state = state.copyWith(isLoading: false);
+        return;
+      }
       // Get current pending welcomes to compare
       final previousPendingIds = getPendingWelcomes().map((w) => w.id).toSet();
lib/config/providers/chat_provider.dart (3)

66-106: Add pubkey-change stale guard to message loads; type locals

Prevent cross-account leakage when account switches mid-call.

-      final activePubkey = ref.read(activePubkeyProvider);
+      final String? activePubkey = ref.read(activePubkeyProvider);
       if (activePubkey == null || activePubkey.isEmpty) {
         _setGroupError(groupId, 'No active account found');
         return;
       }
+      final String capturedPubkey = activePubkey;
 
       _logger.info('ChatProvider: Loading messages for group $groupId');
 
       // Use fetchAggregatedMessagesForGroup which includes all message data including replies
       final aggregatedMessages = await fetchAggregatedMessagesForGroup(
-        pubkey: activePubkey,
+        pubkey: capturedPubkey,
         groupId: groupId,
       );
 
       _logger.info(
         'ChatProvider: Fetched ${aggregatedMessages.length} aggregated messages',
       );
 
+      // Drop stale response if account switched
+      if (capturedPubkey != ref.read(activePubkeyProvider)) {
+        _logger.fine('ChatProvider: Dropping stale load for $capturedPubkey');
+        _setGroupError(groupId, 'Cancelled due to account switch');
+        return;
+      }
       // Sort messages by creation time (oldest first)
       aggregatedMessages.sort((a, b) => a.createdAt.compareTo(b.createdAt));
 
       final messages = await MessageConverter.fromChatMessageList(
         aggregatedMessages,
-        currentUserPublicKey: activePubkey,
+        currentUserPublicKey: capturedPubkey,
         groupId: groupId,
         ref: ref,
       );

286-305: Add stale guard to polling path; type locals

Same pattern as load.

-      final activePubkey = ref.read(activePubkeyProvider);
+      final String? activePubkey = ref.read(activePubkeyProvider);
       if (activePubkey == null || activePubkey.isEmpty) {
         return;
       }
+      final String capturedPubkey = activePubkey;
 
       // Use fetchAggregatedMessagesForGroup for polling as well
       final aggregatedMessages = await fetchAggregatedMessagesForGroup(
-        pubkey: activePubkey,
+        pubkey: capturedPubkey,
         groupId: groupId,
       );
 
+      if (capturedPubkey != ref.read(activePubkeyProvider)) {
+        _logger.fine('ChatProvider: Dropping stale poll for $capturedPubkey');
+        return;
+      }
       aggregatedMessages.sort((a, b) => a.createdAt.compareTo(b.createdAt));
       final newMessages = await MessageConverter.fromChatMessageList(
         aggregatedMessages,
-        currentUserPublicKey: activePubkey,
+        currentUserPublicKey: capturedPubkey,
         groupId: groupId,
         ref: ref,
       );

1-832: Enforce stale‐pubkey guard on all async state mutations
Capture the active pubkey once at the start of each async method (e.g. final currentPubkey = ref.read(activePubkeyProvider)) and, before any await and before mutating state, verify ref.read(activePubkeyProvider) == currentPubkey. Apply in:

  • loadMessagesForGroup(...)
  • sendMessage(...)
  • checkForNewMessages(...)
  • updateMessageReaction(...)
  • sendReplyMessage(...)
  • deleteMessage(...)
lib/ui/chat/invite/chat_invite_screen.dart (1)

101-105: Replace fixed 1s delay with a state-based await.
A hardcoded delay is brittle. Prefer awaiting a provider signal that the accepted group exists/loaded, then navigate.

Proposed change:

- await Future.delayed(const Duration(milliseconds: 1000));
- if (context.mounted) {
-   context.pushReplacement('/chats/${widget.groupId}');
- }
+ // Wait until the group is available (implement in groups notifier if missing)
+ await ref.read(groupsProvider.notifier).waitUntilGroupExists(widget.groupId, timeout: const Duration(seconds: 5));
+ if (context.mounted) {
+   context.pushReplacement('/chats/${widget.groupId}');
+ }
lib/config/providers/group_provider.dart (4)

194-201: Normalize creator key once, type explicitly, and use the trimmed value everywhere.

Reduces duplication and whitespace bugs when composing admin/creator params and logs.

-      final activePubkey = ref.read(activePubkeyProvider);
+      final String? activePubkey = ref.read(activePubkeyProvider);
@@
-      if (activePubkey == null || activePubkey.isEmpty) {
+      if (activePubkey == null || activePubkey.isEmpty) {
         state = state.copyWith(error: 'No active account found', isLoading: false);
         return null;
       }
@@
-      final creatorPubkeyHex = activePubkey.trim();
+      final String creatorPubkeyHex = activePubkey.trim();
@@
-      final resolvedAdminPublicKeys =
-          adminPublicKeyHexs.toSet().map((hexKey) => hexKey.trim()).toList();
-      final combinedAdminKeys = {activePubkey, ...resolvedAdminPublicKeys}.toList();
+      final List<String> resolvedAdminPublicKeys =
+          adminPublicKeyHexs.toSet().map((hexKey) => hexKey.trim()).toList();
+      final List<String> combinedAdminKeys = {creatorPubkeyHex, ...resolvedAdminPublicKeys}.toList();
@@
-      _logger.info('  - Creator pubkey: $activePubkey');
+      _logger.info('  - Creator pubkey: $creatorPubkeyHex');
@@
-        creatorPubkey: activePubkey,
+        creatorPubkey: creatorPubkeyHex,

Also applies to: 215-229, 235-247


260-266: Fix: missing await when unpacking ApiError details.

Currently logs a Future instead of the message.

-      if (e is ApiError) {
-        final errorDetails = e.messageText();
+      if (e is ApiError) {
+        final errorDetails = await e.messageText();
         logMessage += '$errorDetails (Type: ${e.runtimeType})';
       } else {

420-424: Fix: missing await when unpacking ApiError (admins loader).

-      if (e is ApiError) {
-        final errorDetails = e.messageText();
+      if (e is ApiError) {
+        final errorDetails = await e.messageText();
         logMessage += '$errorDetails (Type: ${e.runtimeType})';
       } else {

815-823: Fix: missing await when unpacking ApiError (polling).

-      if (e is ApiError) {
-        final errorDetails = e.messageText();
+      if (e is ApiError) {
+        final errorDetails = await e.messageText();
         logMessage += '$errorDetails (Type: ${e.runtimeType})';
       } else {
🧹 Nitpick comments (63)
lib/config/providers/active_account_provider.dart (1)

140-141: Prefer explicit local types; remove blank lines inside functions.

Per repo guidelines, declare types for locals (e.g., final String? activePubkey) and avoid blank lines within functions for consistency.

-    final activePubkey = ref.watch(activePubkeyProvider);
+    final String? activePubkey = ref.watch(activePubkeyProvider);
...
-    final activePubkey = ref.read(activePubkeyProvider);
+    final String? activePubkey = ref.read(activePubkeyProvider);
...
-      final imageUtils = ref.read(wnImageUtilsProvider);
+      final WnImageUtils imageUtils = ref.read(wnImageUtilsProvider);
-      final imageType = await imageUtils.getMimeTypeFromPath(filePath);
+      final String? imageType = await imageUtils.getMimeTypeFromPath(filePath);
...
-      final utils = ref.read(wnUtilsProvider);
+      final WnUtils utils = ref.read(wnUtilsProvider);
-      final serverUrl = await utils.getDefaultBlossomServerUrl();
+      final String serverUrl = await utils.getDefaultBlossomServerUrl();
...
-      final accountsApi = ref.read(wnAccountsApiProvider);
+      final WnAccountsApi accountsApi = ref.read(wnAccountsApiProvider);

Also applies to: 174-175, 197-198, 207-208, 212-214, 215-221

CHANGELOG.md (1)

25-26: Fix tense/wording for consistency.

Use past tense under “Fixed” and clarify phrasing.

-- Fixes profile edition with outdated metadata
+- Fixed profile editing when local metadata was outdated
rust/src/api/utils.rs (1)

49-57: Make dev URL configurable to avoid code edits.

Read from an env var with a sane default to reduce churn across branches.

 #[frb]
 pub fn get_default_blossom_server_url() -> String {
-    #[cfg(debug_assertions)]
-    {
-        "http://localhost:3000".to_string()
-    }
+    #[cfg(debug_assertions)]
+    {
+        std::env::var("BLOSSOM_UPLOADS_URL")
+            .unwrap_or_else(|_| "http://localhost:3000".to_string())
+    }
     #[cfg(not(debug_assertions))]
     {
         "https://uploads.primal.net".to_string()
     }
 }
lib/config/providers/metadata_cache_provider.dart (2)

113-116: Unnecessary hard dependency on active pubkey for read-only fetch.

check: _fetchMetadataForKey throws when no active account, but the fetch doesn’t use activePubkey. This prevents metadata lookups (e.g., search, pre-login) unnecessarily.

-      final activePubkey = ref.read(activePubkeyProvider);
-      if (activePubkey == null || activePubkey.isEmpty) {
-        throw StateError('No active account found');
-      }
+      final String? activePubkey = ref.read(activePubkeyProvider);
+      if (activePubkey == null || activePubkey.isEmpty) {
+        _logger.fine('Proceeding without active account; metadata fetch is standalone.');
+      }

Would any Rust API call here actually require an active account context?


151-211: Minor: pending fetch cleanup is good; consider eviction on error paths too.

You already remove the pending on error. Optional: add a finally block to guarantee removal if futureContactModel throws before await completes.

lib/config/providers/nostr_keys_provider.dart (1)

21-31: Minor: explicit local types; remove intra-function blank lines.

Follow repo style for local declarations and function body spacing.

Also applies to: 49-64

lib/config/providers/relay_status_provider.dart (3)

66-71: Refresh on pubkey change; type annotate and fix log/error copy.

Without reacting to activePubkey changes, this provider can serve stale relay statuses after account switches. Also, add explicit types and align log/error text with the pubkey-centric flow.

[svg: suggest_essential_refactor]

Apply within this hunk:

-      final activePubkey = ref.read(activePubkeyProvider);
-      _logger.info('RelayStatusNotifier: Active account data: $activePubkey');
-      if (activePubkey == null || activePubkey.isEmpty) {
-        _logger.warning('RelayStatusNotifier: No active account found');
-        state = state.copyWith(isLoading: false, error: 'No active account found');
+      final String? activePubkey = ref.read(activePubkeyProvider);
+      _logger.info('RelayStatusNotifier: Active pubkey: $activePubkey');
+      if (activePubkey == null || activePubkey.isEmpty) {
+        _logger.warning('RelayStatusNotifier: No active pubkey found');
+        state = state.copyWith(isLoading: false, error: 'No active pubkey found');
         return;
       }

Additionally, inside build(), listen for pubkey changes:

@override
RelayStatusState build() {
  ref.listen<String?>(activePubkeyProvider, (prev, next) {
    if (prev != next) {
      if (next != null && next.isNotEmpty) {
        loadRelayStatuses();
      } else {
        state = const RelayStatusState(relayStatuses: {}, isLoading: false);
      }
    }
  });
  Future.microtask(() => loadRelayStatuses());
  return const RelayStatusState(isLoading: true);
}

75-79: Annotate fetched tuple list for clarity.

Explicitly type the list of records to satisfy the “always declare types” guideline.

-      final relayStatuses = await fetchRelayStatus(pubkey: activePubkey);
+      final List<(String, String)> relayStatuses =
+          await fetchRelayStatus(pubkey: activePubkey);

124-131: Parallelize bridge calls and strengthen pubkey check.

Cut latency by fetching types and relays in parallel; also reject empty pubkey.

-      final accountPubKey = ref.read(activePubkeyProvider);
-      if (accountPubKey == null) return false;
-
-      // Fetch relay URLs for each type using new bridge methods
-      final nip65Type = await relayTypeNip65();
-      final inboxType = await relayTypeInbox();
-      final keyPackageType = await relayTypeKeyPackage();
+      final String? accountPubKey = ref.read(activePubkeyProvider);
+      if (accountPubKey == null || accountPubKey.isEmpty) return false;
+
+      // Fetch types in parallel
+      final typeResults = await Future.wait([
+        relayTypeNip65(),
+        relayTypeInbox(),
+        relayTypeKeyPackage(),
+      ]);
+      final nip65Type = typeResults[0];
+      final inboxType = typeResults[1];
+      final keyPackageType = typeResults[2];
 
-      final nip65Urls =
-          (await accountRelays(
-            pubkey: accountPubKey,
-            relayType: nip65Type,
-          )).map((r) => r.url).toList();
-      final inboxUrls =
-          (await accountRelays(
-            pubkey: accountPubKey,
-            relayType: inboxType,
-          )).map((r) => r.url).toList();
-      final keyPackageUrls =
-          (await accountRelays(
-            pubkey: accountPubKey,
-            relayType: keyPackageType,
-          )).map((r) => r.url).toList();
+      // Fetch relay URLs for each type in parallel
+      final relayLists = await Future.wait([
+        accountRelays(pubkey: accountPubKey, relayType: nip65Type),
+        accountRelays(pubkey: accountPubKey, relayType: inboxType),
+        accountRelays(pubkey: accountPubKey, relayType: keyPackageType),
+      ]);
+      final List<String> nip65Urls = relayLists[0].map((r) => r.url).toList();
+      final List<String> inboxUrls = relayLists[1].map((r) => r.url).toList();
+      final List<String> keyPackageUrls = relayLists[2].map((r) => r.url).toList();

Also applies to: 133-147, 149-155

lib/domain/services/dm_chat_service.dart (4)

12-17: Annotate types for clarity and consistency.

Explicit types satisfy project guidelines and aid readability.

-      final activePubkey = ref.read(activePubkeyProvider);
+      final String? activePubkey = ref.read(activePubkeyProvider);
       if (activePubkey == null || activePubkey.isEmpty) return null;
 
-      final currentUserNpub = await npubFromHexPubkey(
+      final String currentUserNpub = await npubFromHexPubkey(
         hexPubkey: activePubkey,
       );

28-41: Tighten naming and inline local to avoid typo (“npup”).

Inline the publicKey assignment and use explicit types on locals.

-        final contactModel = ContactModel.fromMetadata(
+        final ContactModel contactModel = ContactModel.fromMetadata(
           publicKey: otherMember.publicKey,
           metadata: user.metadata,
         );
-        final displayName = contactModel.displayName;
-        final displayImage = contactModel.imagePath ?? (otherMember.imagePath ?? '');
-        final nip05 = contactModel.nip05 ?? '';
-        final npup = contactModel.publicKey;
+        final String displayName = contactModel.displayName;
+        final String displayImage = contactModel.imagePath ?? (otherMember.imagePath ?? '');
+        final String nip05 = contactModel.nip05 ?? '';
         return DMChatData(
           displayName: displayName,
           displayImage: displayImage,
           nip05: nip05,
-          publicKey: npup,
+          publicKey: contactModel.publicKey,
         );

44-47: Add minimal logging on failure path.

Helps diagnose why DM chat data is null.

-      return null;
+      // TODO: route through a central logger if available
+      // ignore: avoid_print
+      print('DMChatService.getDMChatData error: $e');
+      return null;

10-48: Avoid blank lines within functions per guidelines.

There are several blank lines inside getDMChatData. Consider compacting.

lib/ui/contact_list/widgets/chat_list_active_account_avatar.dart (4)

5-5: Align with pubkey-based flow and new userProfileDataProvider.

This widget still depends on activeAccountProvider. To stay consistent with the PR (and reduce coupling), derive displayName/picture from activePubkeyProvider + userProfileDataProvider(pubkey).

-import 'package:whitenoise/config/providers/active_account_provider.dart';
+import 'package:whitenoise/config/providers/active_pubkey_provider.dart';
+import 'package:whitenoise/config/providers/user_profile_data_provider.dart';

Sketch for build() (adapt types to actual provider signatures):

@override
Widget build(BuildContext context, WidgetRef ref) {
  final String? activePubkey = ref.watch(activePubkeyProvider);
  if (activePubkey == null || activePubkey.isEmpty) {
    return WnAvatar(imageUrl: '', displayName: '', size: 36.r);
  }
  final profileAsync = ref.watch(userProfileDataProvider(activePubkey));
  final metadata = profileAsync.valueOrNull?.metadata;
  final String displayName = metadata?.displayName ?? '';
  final String profileImagePath = metadata?.picture ?? '';
  return InkWell(
    borderRadius: BorderRadius.circular(16.r),
    onTap: onTap ?? () => context.push(Routes.settings),
    child: WnAvatar(imageUrl: profileImagePath, displayName: displayName, size: 36.r),
  );
}

If activeAccountProvider is intentionally kept, please disregard and confirm.


16-20: Type annotate locals.

Adhere to the “always declare types” rule.

-    final activeAccountState = ref.watch(activeAccountProvider);
-    final metadata = activeAccountState.value?.metadata;
-    final currentDisplayName = metadata?.displayName ?? '';
-    final profileImagePath = metadata?.picture ?? '';
+    final AsyncValue<dynamic> activeAccountState = ref.watch(activeAccountProvider);
+    final dynamic metadata = activeAccountState.value?.metadata;
+    final String currentDisplayName = metadata?.displayName ?? '';
+    final String profileImagePath = metadata?.picture ?? '';

Note: replace dynamic with the concrete types used in your providers/models.


21-33: Extract magic numbers to constants.

36.r and 16.r are duplicated UI tokens; move to a constants file to ensure consistency.

-    return InkWell(
-      borderRadius: BorderRadius.circular(16.r),
+    const double kAvatarRadius = 16; // or from theme constants
+    const double kAvatarSize = 36;
+    return InkWell(
+      borderRadius: BorderRadius.circular(kAvatarRadius.r),
       ...
-      child: WnAvatar(
+      child: WnAvatar(
         imageUrl: profileImagePath,
         displayName: currentDisplayName,
-        size: 36.r,
+        size: kAvatarSize.r,
       ),

15-34: Remove blank lines inside build() per local guidelines.

Compact the method to avoid internal blank lines.

lib/config/providers/user_profile_data_provider.dart (1)

38-40: Expose a cacheable FutureProvider.family for UI consumption

This avoids ad‑hoc FutureBuilder calls and enables Riverpod caching/refresh/invalidation.

Add alongside the existing NotifierProvider:

final userProfileDataFutureProvider =
    FutureProvider.family<ContactModel, String>((ref, pubkey) {
  return ref.read(userProfileDataProvider.notifier).getUserProfileData(pubkey);
});
lib/ui/contact_list/widgets/welcome_tile.dart (3)

29-29: Declare the variable type explicitly

Matches repo guideline to always declare types.

-    final userProfileDataNotifier = ref.read(userProfileDataProvider.notifier);
+    final UserProfileDataNotifier userProfileDataNotifier =
+        ref.read(userProfileDataProvider.notifier);

31-33: Prefer Riverpod watch over FutureBuilder to avoid re-fetch on rebuilds

Using a FutureProvider.family (see provider comment) prevents repeated network calls and integrates with invalidation.

Example:

final profile = ref.watch(userProfileDataFutureProvider(welcome.welcomer));
return profile.when(
  data: (welcomerContact) { /* ... */ },
  loading: () => const SizedBox.shrink(),
  error: (_, __) => const SizedBox.shrink(),
);

35-36: Localize “Unknown User”

Use AppLocalizations instead of a hardcoded string.

-        final welcomerName = welcomerContact?.displayName ?? 'Unknown User';
+        final String welcomerName =
+            welcomerContact?.displayName ?? AppLocalizations.of(context)!.unknownUser;
lib/ui/settings/general_settings_screen.dart (1)

70-74: Batch fetch profiles in parallel and add explicit types

Parallelization improves UX on multiple accounts; explicit types align with repo guidelines.

-      final accounts = await getAccounts();
-      final contactModels = <String, ContactModel>{};
-      final userProfileDataNotifier = ref.read(userProfileDataProvider.notifier);
-      for (final account in accounts) {
-        final userProfileData = await userProfileDataNotifier.getUserProfileData(account.pubkey);
-        contactModels[account.pubkey] = userProfileData;
-      }
+      final List<Account> accounts = await getAccounts();
+      final UserProfileDataNotifier userProfileDataNotifier =
+          ref.read(userProfileDataProvider.notifier);
+
+      final List<Future<MapEntry<String, ContactModel>>> futures = [
+        for (final Account account in accounts)
+          userProfileDataNotifier
+              .getUserProfileData(account.pubkey)
+              .then((ContactModel data) => MapEntry(account.pubkey, data)),
+      ];
+      final Map<String, ContactModel> contactModels =
+          Map<String, ContactModel>.fromEntries(await Future.wait(futures));
test/config/providers/user_profile_data_provider_test.dart (2)

42-58: Prefer explicit types for test fixtures (and make pubkey const).

Aligns with our Dart guidelines to always declare types.

Apply:

-    final testPubkey = 'test_pubkey_123';
-    final testMetadata = const FlutterMetadata(
+    const String testPubkey = 'test_pubkey_123';
+    final FlutterMetadata testMetadata = const FlutterMetadata(
@@
-    final testUser = User(
+    final User testUser = User(

132-143: Assert specific error types for stronger guarantees.

Prefer throwsA with a matcher to avoid false positives when unrelated exceptions are thrown.

-          expect(
-            () async => await notifier.getUserProfileData(testPubkey),
-            throwsException,
-          );
+          expect(
+            () async => notifier.getUserProfileData(testPubkey),
+            throwsA(isA<Exception>()),
+          );
lib/ui/settings/developer/developer_settings_screen.dart (3)

7-7: Good switch to activePubkeyProvider.

Matches the PR’s pubkey-centered flow. Optional: if you import app_theme.dart (which re-exports ScreenUtil), you can drop the direct flutter_screenutil import for consistency with other files.


36-44: Gate UX earlier by disabling the button when no active pubkey.

Current flow shows an error toast; disabling the action when unavailable reduces noise. Watch activePubkey in build and disable the button accordingly.

-                        WnFilledButton(
-                          label: 'Reload Follows',
-                          onPressed: _isLoading ? null : _reloadFollows,
-                          loading: _isLoading,
-                        ),
+                        WnFilledButton(
+                          label: 'Reload Follows',
+                          onPressed: _isLoading || !(ref.watch(activePubkeyProvider)?.isNotEmpty ?? false)
+                              ? null
+                              : _reloadFollows,
+                          loading: _isLoading,
+                        ),

41-47: Localize user-facing strings.

Use AppLocalizations for the toasts to comply with our i18n guideline.

-        ref.showSuccessToast('Follows reloaded successfully');
+        ref.showSuccessToast(AppLocalizations.of(context)!.followsReloaded)
@@
-        ref.showErrorToast('No active account found');
+        ref.showErrorToast(AppLocalizations.of(context)!.noActiveAccount)
@@
-      ref.showErrorToast('Failed to reload follows: $e');
+      ref.showErrorToast(AppLocalizations.of(context)!.failedToReloadFollows('$e'))
lib/config/providers/polling_provider.dart (1)

79-80: Invalidating activeAccountProvider after loads is the right trigger.

This should propagate metadata updates as intended. Consider also invalidating activePubkeyProvider if it derives from account state indirectly, to be extra-safe with UI watchers that observe only the pubkey.

+import 'package:whitenoise/config/providers/active_pubkey_provider.dart';
@@
-      ref.invalidate(activeAccountProvider);
+      ref.invalidate(activeAccountProvider);
+      ref.invalidate(activePubkeyProvider);
@@
-      ref.invalidate(activeAccountProvider);
+      ref.invalidate(activeAccountProvider);
+      ref.invalidate(activePubkeyProvider);

Also applies to: 125-126

lib/config/providers/follows_provider.dart (2)

118-127: Type annotations and basic input validation

Add explicit type for activePubkey and short-circuit on empty userPubkey to prevent useless calls.

-      final activePubkey = ref.read(activePubkeyProvider);
+      final String? activePubkey = ref.read(activePubkeyProvider);
       if (activePubkey == null || activePubkey.isEmpty) {
         state = state.copyWith(error: 'No active account found', isLoading: false);
         return;
       }
+      if (userPubkey.isEmpty) {
+        state = state.copyWith(error: 'Invalid user pubkey', isLoading: false);
+        return;
+      }
 
       await accounts_api.followUser(
         accountPubkey: activePubkey,
         userToFollowPubkey: userPubkey,
       );

155-165: Mirror the same fixes in removeFollow

Keep types explicit and validate input like in addFollow for consistency.

-      final activePubkey = ref.read(activePubkeyProvider);
+      final String? activePubkey = ref.read(activePubkeyProvider);
       if (activePubkey == null || activePubkey.isEmpty) {
         state = state.copyWith(error: 'No active account found', isLoading: false);
         return;
       }
+      if (userPubkey.isEmpty) {
+        state = state.copyWith(error: 'Invalid user pubkey', isLoading: false);
+        return;
+      }
 
       await accounts_api.unfollowUser(
         accountPubkey: activePubkey,
         userToUnfollowPubkey: userPubkey,
       );
lib/ui/contact_list/search_chat_bottom_sheet.dart (3)

78-84: Align log message with new model (pubkey vs account)

Minor wording tweak for clarity.

-        _logger.info('SearchChatBottomSheet: Found active account: $activePubkey');
+        _logger.info('SearchChatBottomSheet: Found active pubkey: $activePubkey');

41-50: Auto-refresh follows on account switch while the sheet is open

Listen to activePubkey changes to reload contacts without manual reopen.

   @override
   void initState() {
     super.initState();
     _searchController.addListener(_onSearchChanged);
     _scrollController.addListener(_onScrollChanged);
     // Load contacts when the widget initializes
     WidgetsBinding.instance.addPostFrameCallback((_) {
       _loadContacts();
     });
+    ref.listen<String?>(activePubkeyProvider, (prev, next) {
+      if (!mounted) return;
+      if (prev != next && (next?.isNotEmpty ?? false)) {
+        ref.read(followsProvider.notifier).loadFollows();
+      }
+    });
   }

Also applies to: 76-84


22-30: Externalize UI strings to AppLocalizations

Move hardcoded strings ("Search", "No active account found", placeholders) to l10n to match project guidelines.

Also applies to: 85-95, 145-297

lib/config/providers/welcomes_provider.dart (3)

113-124: Type the active pubkey local in fetchWelcomeById

Keep locals explicitly typed per guidelines.

-      final activePubkey = ref.read(activePubkeyProvider);
+      final String? activePubkey = ref.read(activePubkeyProvider);

148-156: Type the active pubkey local in accept flow

Consistency with typing.

-      final activePubkey = ref.read(activePubkeyProvider);
+      final String? activePubkey = ref.read(activePubkeyProvider);

180-188: Type the active pubkey local in decline flow

Consistency with typing.

-      final activePubkey = ref.read(activePubkeyProvider);
+      final String? activePubkey = ref.read(activePubkeyProvider);
lib/config/providers/chat_provider.dart (4)

145-160: Type locals and carry captured pubkey through conversions

Keep types explicit and reuse the captured pubkey in message conversion.

-      final activePubkey = ref.read(activePubkeyProvider);
+      final String? activePubkey = ref.read(activePubkeyProvider);
       if (activePubkey == null || activePubkey.isEmpty) {
         _setGroupError(groupId, 'No active account found');
         return null;
       }
+      final String capturedPubkey = activePubkey;
 ...
       final sentMessage = await sendMessageToGroup(
-        pubkey: activePubkey,
+        pubkey: capturedPubkey,
         groupId: groupId,
         message: message,
         kind: kind,
         tags: tags,
       );
 ...
       final sentMessageModel = await MessageConverter.fromChatMessage(
         sentChatMessage,
-        currentUserPublicKey: activePubkey,
+        currentUserPublicKey: capturedPubkey,
         groupId: groupId,
         ref: ref,
         messageCache: messageCache,
       );

Also applies to: 199-206


499-506: Reactions: type local and use captured pubkey

Small consistency/tightening.

-      final activePubkey = ref.read(activePubkeyProvider);
+      final String? activePubkey = ref.read(activePubkeyProvider);
       if (activePubkey == null || activePubkey.isEmpty) {
         _setGroupError(message.groupId ?? '', 'No active account found');
         return false;
       }
+      final String capturedPubkey = activePubkey;
 ...
       await sendMessageToGroup(
-        pubkey: activePubkey,
+        pubkey: capturedPubkey,
         groupId: message.groupId ?? '',
         message: reactionContent,
         kind: 7, // Nostr kind 7 = reaction
         tags: reactionTags,
       );

Also applies to: 533-541


572-593: Replies: type local and use captured pubkey

Consistency with other paths.

-      final activePubkey = ref.read(activePubkeyProvider);
+      final String? activePubkey = ref.read(activePubkeyProvider);
       if (activePubkey == null || activePubkey.isEmpty) {
         _setGroupError(groupId, 'No active account found');
         return null;
       }
+      final String capturedPubkey = activePubkey;
 ...
       final sentMessage = await sendMessageToGroup(
-        pubkey: activePubkey,
+        pubkey: capturedPubkey,
         groupId: groupId,
         message: message,
         kind: 9, // Kind 9 for replies
         tags: replyTags,
       );
 ...
       final sentMessageModel = await MessageConverter.fromChatMessage(
         sentChatMessage,
-        currentUserPublicKey: activePubkey,
+        currentUserPublicKey: capturedPubkey,
         groupId: groupId,
         ref: ref,
         messageCache: messageCache,
       );

Also applies to: 633-639


676-699: Deletes: type local and use captured pubkey

Same as above.

-      final activePubkey = ref.read(activePubkeyProvider);
+      final String? activePubkey = ref.read(activePubkeyProvider);
       if (activePubkey == null || activePubkey.isEmpty) {
         _setGroupError(groupId, 'No active account found');
         return false;
       }
+      final String capturedPubkey = activePubkey;
 ...
       await sendMessageToGroup(
-        pubkey: activePubkey,
+        pubkey: capturedPubkey,
         groupId: groupId,
         message: '', // Empty content for deletion
         kind: 5, // Nostr kind 5 = deletion
         tags: deleteTags,
       );
lib/ui/contact_list/group_welcome_invitation_sheet.dart (2)

116-121: Avoid duplicate toasts when gating on active pubkey

These three widgets each toast on missing pubkey; centralize the check (e.g., in the sheet) to avoid multiple toasts if the sheet builds/rebuilds.

-      if (activePubkey == null || activePubkey.isEmpty) {
-        ref.showErrorToast('No active account found');
-        return null;
-      }
+      if (activePubkey == null || activePubkey.isEmpty) {
+        return null; // Let parent handle any user notification once
+      }

Also applies to: 243-248, 286-291


116-121: Type the pubkey local

Keep locals explicit.

-      final activePubkey = ref.read(activePubkeyProvider);
+      final String? activePubkey = ref.read(activePubkeyProvider);
lib/ui/chat/invite/chat_invite_screen.dart (3)

250-254: Avoid re-fetching on every rebuild; prefer a provider family (Future/Stream) or memoized Future.
Creating a new Future in build can cause redundant DB/API calls and misses automatic refresh when metadata changes. Consider a FutureProvider/StreamProvider.family keyed by pubkey and watch it here to get live updates and rebuilds.

Would you like me to add a userProfileDataFutureProvider.family (or stream) and refactor DMInviteHeader/DMAppBarTitle to ref.watch it?


341-345: Same as above for AppBar title block.
Use a provider family to avoid per-build Future creation and to auto-update on metadata changes.


54-61: Localize hardcoded strings.
Move UI strings to AppLocalizations per guidelines.

Also applies to: 171-179, 195-231, 303-321

lib/ui/contact_list/new_chat_bottom_sheet.dart (5)

94-101: Add explicit typing and guard logs.
Declare the type for readability and to follow code-style. Consider shortening logs if needed.

- final activePubkey = ref.read(activePubkeyProvider);
+ final String? activePubkey = ref.read(activePubkeyProvider);

126-132: Type the notifier and result, and avoid repeated fetches while typing.

  • Add explicit types.
  • Optional: debounce metadata fetch to reduce API/DB thrash as the user types.
- final userProfileDataNotifier = ref.read(userProfileDataProvider.notifier);
- final userProfileData = await userProfileDataNotifier.getUserProfileData(publicKey.trim());
+ final UserProfileDataNotifier userProfileDataNotifier =
+     ref.read(userProfileDataProvider.notifier);
+ final ContactModel userProfileData =
+     await userProfileDataNotifier.getUserProfileData(publicKey.trim());

Optional debounce (minimal diff, requires import 'dart:async'; and a Timer? _debounce; field):

- if (_isValidPublicKey(_searchQuery)) {
-   _fetchMetadataForPublicKey(_searchQuery);
- }
+ if (_isValidPublicKey(_searchQuery)) {
+   _debounce?.cancel();
+   _debounce = Timer(const Duration(milliseconds: 300), () {
+     _fetchMetadataForPublicKey(_searchQuery);
+   });
+ }

380-474: Guard the debug panel behind kDebugMode.
Prevents exposing raw data in production.

- if (_searchQuery.toLowerCase() == 'debug') ...[
+ if (kDebugMode && _searchQuery.toLowerCase() == 'debug') ...[

Also add:

+ import 'package:flutter/foundation.dart';

near the other imports.


31-40: Localize strings per guidelines.
Move 'New chat', 'Retry', 'Loading metadata…', etc., to AppLocalizations.

Also applies to: 189-209, 214-256, 262-269, 271-298, 336-362, 485-503


277-284: Explicitly annotate types for clarity

- final userProfileDataNotifier = ref.read(userProfileDataProvider.notifier);
- final supportUserProfileData = await userProfileDataNotifier.getUserProfileData(kSupportNpub);
+ final UserProfileDataNotifier userProfileDataNotifier =
+     ref.read(userProfileDataProvider.notifier);
+ final ContactModel supportUserProfileData =
+     await userProfileDataNotifier.getUserProfileData(kSupportNpub);
lib/ui/contact_list/chat_list_screen.dart (2)

382-389: Localize hardcoded strings.
'Checking for new messages…', 'No Relays Connected', 'Connect Relays', 'Search Chats' should go through AppLocalizations.

Also applies to: 463-479, 403-435


248-256: Type local collections for clarity.
Annotate groupList, welcomesList, and filteredChatItems with concrete types per code-style.

Also applies to: 289-295

lib/config/providers/group_provider.dart (11)

153-160: Type activePubkey explicitly.

Follow code style (no implicit types in Dart files).

-      final activePubkey = ref.read(activePubkeyProvider);
+      final String? activePubkey = ref.read(activePubkeyProvider);

298-306: Type activePubkey explicitly.

Applies same guideline in members loader.

-      final activePubkey = ref.read(activePubkeyProvider);
+      final String? activePubkey = ref.read(activePubkeyProvider);

377-386: Type activePubkey explicitly.

Same style compliance.

-      final activePubkey = ref.read(activePubkeyProvider);
+      final String? activePubkey = ref.read(activePubkeyProvider);

491-496: Type activePubkey explicitly.

Keeps consistency.

-      final activePubkey = ref.read(activePubkeyProvider);
+      final String? activePubkey = ref.read(activePubkeyProvider);

592-606: Avoid repeated FFI calls: prefer cached group type when available.

You already cache types; use it to avoid extra getGroupInformation calls.

-    final groupInformation = await getGroupInformation(groupId: group.mlsGroupId);
-    if (groupInformation.groupType == GroupType.directMessage) {
+    final cachedType = getCachedGroupType(group.mlsGroupId);
+    final GroupType groupType =
+        cachedType ?? (await getGroupInformation(groupId: group.mlsGroupId)).groupType;
+    if (groupType == GroupType.directMessage) {

718-727: Type activePubkey explicitly and simplify.

-      final activePubkey = ref.read(activePubkeyProvider);
+      final String? activePubkey = ref.read(activePubkeyProvider);

763-770: Type activePubkey explicitly and pass it through.

Style compliance; rest looks good.

-      final activePubkey = ref.read(activePubkeyProvider);
+      final String? activePubkey = ref.read(activePubkeyProvider);

Also applies to: 809-869


879-896: Type activePubkey explicitly.

-      final activePubkey = ref.read(activePubkeyProvider);
+      final String? activePubkey = ref.read(activePubkeyProvider);
@@
-      await addMembersToGroup(
-        pubkey: activePubkey,
+      await addMembersToGroup(
+        pubkey: activePubkey,

934-951: Type activePubkey explicitly.

-      final activePubkey = ref.read(activePubkeyProvider);
+      final String? activePubkey = ref.read(activePubkeyProvider);
@@
-      await removeMembersFromGroup(
-        pubkey: activePubkey,
+      await removeMembersFromGroup(
+        pubkey: activePubkey,

330-337: Localize user-facing fallback strings.

Replace hardcoded 'Unknown User' and 'Direct Message' with AppLocalizations entries.

Also applies to: 607-621


62-69: DRY: centralize active pubkey retrieval and error handling.

Add a small helper to read/validate the active key and set the standard error.

Example helper (add inside GroupsNotifier):

String? _requireActivePubkey({bool setError = true}) {
  final String? key = ref.read(activePubkeyProvider);
  if ((key == null || key.isEmpty) && setError) {
    state = state.copyWith(error: 'No active account found');
  }
  return key;
}

Then replace repeated reads with final String? activePubkey = _requireActivePubkey();.

Also applies to: 298-306, 378-386, 492-496, 720-727, 764-770, 879-886, 934-941

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5252001 and 90b9962.

📒 Files selected for processing (28)
  • CHANGELOG.md (1 hunks)
  • docs/METADATA_CACHING_SOLUTION.md (0 hunks)
  • lib/config/providers/active_account_provider.dart (2 hunks)
  • lib/config/providers/chat_provider.dart (13 hunks)
  • lib/config/providers/follows_provider.dart (3 hunks)
  • lib/config/providers/group_provider.dart (16 hunks)
  • lib/config/providers/metadata_cache_provider.dart (2 hunks)
  • lib/config/providers/nostr_keys_provider.dart (3 hunks)
  • lib/config/providers/polling_provider.dart (3 hunks)
  • lib/config/providers/relay_status_provider.dart (1 hunks)
  • lib/config/providers/user_profile_data_provider.dart (1 hunks)
  • lib/config/providers/welcomes_provider.dart (5 hunks)
  • lib/domain/services/dm_chat_service.dart (2 hunks)
  • lib/examples/metadata_cache_examples.dart (0 hunks)
  • lib/ui/chat/chat_info/chat_info_screen.dart (1 hunks)
  • lib/ui/chat/chat_info/dm_chat_info.dart (1 hunks)
  • lib/ui/chat/invite/chat_invite_screen.dart (3 hunks)
  • lib/ui/contact_list/chat_list_screen.dart (2 hunks)
  • lib/ui/contact_list/group_welcome_invitation_sheet.dart (4 hunks)
  • lib/ui/contact_list/new_chat_bottom_sheet.dart (4 hunks)
  • lib/ui/contact_list/search_chat_bottom_sheet.dart (2 hunks)
  • lib/ui/contact_list/widgets/chat_list_active_account_avatar.dart (1 hunks)
  • lib/ui/contact_list/widgets/welcome_tile.dart (2 hunks)
  • lib/ui/settings/developer/developer_settings_screen.dart (2 hunks)
  • lib/ui/settings/general_settings_screen.dart (2 hunks)
  • lib/utils/metadata_cache_utils.dart (0 hunks)
  • rust/src/api/utils.rs (1 hunks)
  • test/config/providers/user_profile_data_provider_test.dart (1 hunks)
💤 Files with no reviewable changes (3)
  • docs/METADATA_CACHING_SOLUTION.md
  • lib/utils/metadata_cache_utils.dart
  • lib/examples/metadata_cache_examples.dart
🧰 Additional context used
📓 Path-based instructions (3)
**/*.dart

📄 CodeRabbit inference engine (.cursor/rules/flutter.mdc)

**/*.dart: Always declare the type of each variable and function (parameters and return value). Avoid using 'any'. Create necessary types.
Don't leave blank lines within a function.
One export per file.
Use PascalCase for classes.
Use camelCase for variables, functions, and methods.
Use underscores_case for file and directory names.
Use UPPERCASE for environment variables. Avoid magic numbers and define constants.
Start each function with a verb.
Use verbs for boolean variables. Example: isLoading, hasError, canDelete, etc.
Use complete words instead of abbreviations and correct spelling, except for standard and well-known abbreviations (API, URL, i, j, err, ctx, req, res, next).
Write short functions with a single purpose. Less than 20 instructions.
Name functions with a verb and something else. If it returns a boolean, use isX or hasX, canX, etc. If it doesn't return anything, use executeX or saveX, etc.
Avoid nesting blocks by early checks and returns, or extraction to utility functions.
Use higher-order functions (map, filter, reduce, etc.) to avoid function nesting. Use arrow functions for simple functions (less than 3 instructions). Use named functions for non-simple functions.
Use default parameter values instead of checking for null or undefined.
Reduce function parameters using RO-RO: use an object to pass multiple parameters and to return results. Declare necessary types for input arguments and output.
Use a single level of abstraction in functions.
Don't abuse primitive types and encapsulate data in composite types.
Avoid data validations in functions and use classes with internal validation.
Prefer immutability for data. Use readonly for data that doesn't change. Use 'as const' for literals that don't change.
Declare interfaces to define contracts.
Write small classes with a single purpose. Less than 200 instructions, less than 10 public methods, less than 10 properties.
Use exceptions to handle errors you don't expect. If you catch an exception, it sh...

Files:

  • lib/ui/chat/chat_info/dm_chat_info.dart
  • lib/config/providers/polling_provider.dart
  • lib/config/providers/active_account_provider.dart
  • lib/ui/contact_list/widgets/chat_list_active_account_avatar.dart
  • lib/config/providers/metadata_cache_provider.dart
  • lib/ui/chat/chat_info/chat_info_screen.dart
  • lib/config/providers/follows_provider.dart
  • lib/config/providers/chat_provider.dart
  • lib/ui/contact_list/search_chat_bottom_sheet.dart
  • lib/config/providers/relay_status_provider.dart
  • lib/config/providers/user_profile_data_provider.dart
  • lib/config/providers/nostr_keys_provider.dart
  • lib/ui/contact_list/widgets/welcome_tile.dart
  • lib/ui/chat/invite/chat_invite_screen.dart
  • lib/config/providers/group_provider.dart
  • lib/ui/contact_list/new_chat_bottom_sheet.dart
  • lib/domain/services/dm_chat_service.dart
  • lib/ui/contact_list/group_welcome_invitation_sheet.dart
  • lib/ui/contact_list/chat_list_screen.dart
  • lib/config/providers/welcomes_provider.dart
  • test/config/providers/user_profile_data_provider_test.dart
  • lib/ui/settings/general_settings_screen.dart
  • lib/ui/settings/developer/developer_settings_screen.dart
**/*.md

📄 CodeRabbit inference engine (.cursor/rules/whitenoise.mdc)

**/*.md: NIPs (Nostr Implementation Possibilities) are numbered like NIP-XX where XX are two capitalized hexadecimal digits, e.g., NIP-01 and NIP-C7.
To read a specific NIP, construct the NIP URL following this template: https://raw.githubusercontent.com/nostr-protocol/nips/refs/heads/master/{nip}.md (replace {nip} in the URL template with the relevant NIP name, e.g., 07 for NIP-07, or C7 for NIP-C7).
To read the definition of a specific kind, construct a URL following this template: https://nostrbook.dev/kinds/{kind}.md (replace {kind} in the template with the kind number, e.g., https://nostrbook.dev/kinds/0.md for kind 0).

Files:

  • CHANGELOG.md
**/*_test.dart

📄 CodeRabbit inference engine (.cursor/rules/flutter.mdc)

**/*_test.dart: Follow the Arrange-Act-Assert convention for tests.
Name test variables clearly. Follow the convention: inputX, mockX, actualX, expectedX, etc.
Write unit tests for each public function. Use test doubles to simulate dependencies, except for third-party dependencies that are not expensive to execute.
Write acceptance tests for each module. Follow the Given-When-Then convention.
Use the standard widget testing for Flutter.

Files:

  • test/config/providers/user_profile_data_provider_test.dart
🧠 Learnings (8)
📚 Learning: 2025-08-12T11:21:53.640Z
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#455
File: lib/ui/settings/profile/switch_profile_bottom_sheet.dart:0-0
Timestamp: 2025-08-12T11:21:53.640Z
Learning: In the whitenoise_flutter codebase, ContactModel.publicKey can be stored in either npub format (from metadata cache) or hex format (from account storage). The activeAccountProvider may return either format depending on how the account was originally stored, so normalization to hex format is required when comparing with other hex-normalized keys in sorting logic.

Applied to files:

  • lib/ui/chat/chat_info/dm_chat_info.dart
  • lib/config/providers/active_account_provider.dart
  • lib/config/providers/metadata_cache_provider.dart
  • lib/config/providers/chat_provider.dart
  • lib/ui/contact_list/search_chat_bottom_sheet.dart
  • lib/config/providers/nostr_keys_provider.dart
  • lib/config/providers/group_provider.dart
  • lib/ui/contact_list/new_chat_bottom_sheet.dart
  • lib/domain/services/dm_chat_service.dart
  • lib/ui/contact_list/group_welcome_invitation_sheet.dart
  • lib/ui/settings/general_settings_screen.dart
📚 Learning: 2025-08-08T13:39:00.500Z
Learnt from: CR
PR: parres-hq/whitenoise_flutter#0
File: .cursor/rules/flutter.mdc:0-0
Timestamp: 2025-08-08T13:39:00.500Z
Learning: Applies to **/*.dart : Use Riverpod to manage state. Use StreamProviders to watch for state changes that come from the rust api. See keepAlive if you need to keep the state alive.

Applied to files:

  • lib/config/providers/polling_provider.dart
  • lib/config/providers/active_account_provider.dart
  • lib/config/providers/metadata_cache_provider.dart
  • lib/config/providers/follows_provider.dart
  • lib/config/providers/chat_provider.dart
  • lib/config/providers/relay_status_provider.dart
  • lib/config/providers/user_profile_data_provider.dart
  • lib/config/providers/nostr_keys_provider.dart
  • lib/ui/contact_list/widgets/welcome_tile.dart
  • lib/config/providers/group_provider.dart
  • lib/config/providers/welcomes_provider.dart
📚 Learning: 2025-08-08T13:39:00.500Z
Learnt from: CR
PR: parres-hq/whitenoise_flutter#0
File: .cursor/rules/flutter.mdc:0-0
Timestamp: 2025-08-08T13:39:00.500Z
Learning: Applies to **/*.dart : Use the whitenoise rust crate (via flutter_rust_bridge) as the source of all data and only way to trigger changes in our data model.

Applied to files:

  • lib/config/providers/active_account_provider.dart
  • lib/config/providers/nostr_keys_provider.dart
  • lib/ui/chat/invite/chat_invite_screen.dart
  • lib/config/providers/group_provider.dart
  • lib/ui/contact_list/new_chat_bottom_sheet.dart
  • lib/ui/contact_list/group_welcome_invitation_sheet.dart
  • lib/ui/settings/general_settings_screen.dart
📚 Learning: 2025-08-23T11:02:28.308Z
Learnt from: Quwaysim
PR: parres-hq/whitenoise_flutter#527
File: lib/ui/core/ui/wn_avatar.dart:1-6
Timestamp: 2025-08-23T11:02:28.308Z
Learning: In the whitenoise_flutter codebase, flutter_screenutil is re-exported through lib/ui/core/themes/src/app_theme.dart, so files that import app_theme.dart do not need to directly import flutter_screenutil to use .w, .h, .sp extensions.

Applied to files:

  • lib/ui/contact_list/group_welcome_invitation_sheet.dart
  • lib/ui/settings/developer/developer_settings_screen.dart
📚 Learning: 2025-08-08T13:39:00.500Z
Learnt from: CR
PR: parres-hq/whitenoise_flutter#0
File: .cursor/rules/flutter.mdc:0-0
Timestamp: 2025-08-08T13:39:00.500Z
Learning: Applies to **/*.dart : Use flutter_rust_bridge to access core functionality of the app.

Applied to files:

  • lib/ui/contact_list/group_welcome_invitation_sheet.dart
📚 Learning: 2025-09-01T14:56:50.988Z
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#568
File: lib/ui/core/ui/wn_image.dart:1-2
Timestamp: 2025-09-01T14:56:50.988Z
Learning: The whitenoise_flutter project does not target web platforms and any future web version would be in a separate repository, so dart:io imports and file system operations are acceptable in this codebase.

Applied to files:

  • lib/ui/contact_list/group_welcome_invitation_sheet.dart
  • lib/ui/settings/developer/developer_settings_screen.dart
📚 Learning: 2025-08-08T13:39:00.500Z
Learnt from: CR
PR: parres-hq/whitenoise_flutter#0
File: .cursor/rules/flutter.mdc:0-0
Timestamp: 2025-08-08T13:39:00.500Z
Learning: Applies to **/*_test.dart : Write unit tests for each public function. Use test doubles to simulate dependencies, except for third-party dependencies that are not expensive to execute.

Applied to files:

  • test/config/providers/user_profile_data_provider_test.dart
📚 Learning: 2025-08-08T13:39:00.500Z
Learnt from: CR
PR: parres-hq/whitenoise_flutter#0
File: .cursor/rules/flutter.mdc:0-0
Timestamp: 2025-08-08T13:39:00.500Z
Learning: Applies to **/*.dart : Use extensions to manage reusable code.

Applied to files:

  • lib/ui/settings/developer/developer_settings_screen.dart

Comment on lines 79 to 88
try {
final activeAccountState = await ref.read(activeAccountProvider.future);
final activeAccount = activeAccountState.account;
final activePubkey = ref.read(activePubkeyProvider);

if (activeAccount == null) {
if (activePubkey == null || activePubkey.isEmpty) {
state = state.copyWith(error: 'No active account found', isLoading: false);
return;
}

final groups = await activeGroups(pubkey: activeAccount.pubkey);
final groups = await activeGroups(pubkey: activePubkey);

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use explicit typing and consistently operate on the sorted list.

  • Declare activePubkey with an explicit type to follow our Dart guidelines.
  • After sorting, subsequent operations should use sortedGroups (members/types/display names/message prefetch) to avoid accidental divergence.
-      final activePubkey = ref.read(activePubkeyProvider);
+      final String? activePubkey = ref.read(activePubkeyProvider);
@@
-      await _loadMembersForAllGroups(groups);
+      await _loadMembersForAllGroups(sortedGroups);
@@
-      await _loadGroupTypesForAllGroups(groups);
+      await _loadGroupTypesForAllGroups(sortedGroups);
@@
-      await _calculateDisplayNames(groups, activePubkey);
+      await _calculateDisplayNames(sortedGroups, activePubkey);
@@
-            .loadMessagesForGroups(
-              groups.map((g) => g.mlsGroupId).toList(),
-            );
+            .loadMessagesForGroups(
+              sortedGroups.map((g) => g.mlsGroupId).toList(),
+            );

Optional: wrap the microtask body in try/catch to avoid unhandled async errors.

Also applies to: 110-118, 120-126

🤖 Prompt for AI Agents
In lib/config/providers/group_provider.dart around lines 79 to 88, declare
activePubkey with an explicit type (e.g., String? activePubkey =
ref.read(activePubkeyProvider);) to follow Dart guidelines, and after sorting
groups assign to a new variable sortedGroups and use that sortedGroups for all
subsequent operations (members/types/display names/message prefetch) to ensure
consistency and avoid divergence; also apply the same explicit-typing and
use-of-sortedGroups fixes in the other affected ranges (lines 110-118 and
120-126), and optionally wrap the microtask body in a try/catch to prevent
unhandled async errors.

final activeAccount = activeAccountState.account;
if (activeAccount == null) {
final activePubkey = ref.read(activePubkeyProvider);
if (activePubkey == null || activePubkey.isEmpty) {
Copy link
Contributor

Choose a reason for hiding this comment

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

you can try using this extension method .nullOrEmpty

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thanks for the suggestion! I'll replace it!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I ended up applying @Quwaysim suggestion commented below of using null promotion

return otherMembers.first;
}

Future<ContactModel?> getFirstOtherMember(String? groupId, String? currentUserNpub) async {
Copy link
Contributor

Choose a reason for hiding this comment

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

is this no longer being used ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, it wasn't used anywhere

import 'package:whitenoise/src/rust/api/users.dart' as wn_users_api;
import 'package:whitenoise/src/rust/api/users.dart' show User;

abstract class WnUsersApi {
Copy link
Contributor

Choose a reason for hiding this comment

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

I dont think using an abstract is realy needed in this case. we only have one method for now. and its a one on one mapping with what the rust method provides. so maybe for now just use the rust method direclty in the provider ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If I call the rust method directly inside the provider, it becomes very hard to test. The idea behind adding dependency injection is to make it easier to write tests with mocks. I’ll try a different approach to see if I can achieve the same with a const, while still keeping the provider testable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I removed the abstract class and instead added the dependency injection in the notifier constructor.

Copy link
Contributor

Choose a reason for hiding this comment

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

If I call the rust method directly inside the provider, it becomes very hard to test. The idea behind adding dependency injection is to make it easier to write tests with mocks. I’ll try a different approach to see if I can achieve the same with a const, while still keeping the provider testable.

I love that, this was one of the reasons i proposed having a datasource to handle api calls.
we can come back to this later and do it in a clean unified way.

final activeAccountState = await ref.read(activeAccountProvider.future);
final activeAccount = activeAccountState.account;
if (activeAccount == null) {
final activePubkey = ref.read(activePubkeyProvider);
Copy link
Contributor

Choose a reason for hiding this comment

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

Just FYI, no need to make any changes, you could do null promotion by writing:

final activePubkey = ref.read(activePubkeyProvider)?? '';

if (activePubkey.isEmpty){......}

Also, just for code asthetics 😂 I prefer activePubKey as pub and key are initially two distinct words.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll check that out. @codeswot proposed to use .nullOrEmpty and I liked that one, I think it will look prettier

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Regarding the pubkey name: in the Account and User classes that come from the Rust side (not the user model, but users.dart and account.dart), the attribute is named pubkey as a single word. For consistency with those classes, I’d prefer to stick with pubkey here as well.

Copy link
Contributor Author

@josefinalliende josefinalliende Sep 3, 2025

Choose a reason for hiding this comment

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

Related to null promotion, I ended up choosing your suggestion. The .nullOrEmpty required to add it to string extension file but for String? instead of the existing ones for String and also gave me warnings after cause the compiler god confused thinking that I was passing a String? to an argument that could only be a String. Null promotion works better in this case I think, so thanks ❤️

Copy link
Contributor

Choose a reason for hiding this comment

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

Related to null promotion, I ended up choosing your suggestion. The .nullOrEmpty required to add it to string extension file but for String? instead of the existing ones for String and also gave me warnings after cause the compiler god confused thinking that I was passing a String? to an argument that could only be a String. Null promotion works better in this case I think, so thanks ❤️

You mean when using activePubkey string in the npubFromHexPubkey() method, it complains that it could be null.
I agree Null promotion works better here as it keeps the context of the variable being checked if null

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I meant than then trying to use .nullOrEmpty , I needed to add it as a String? extension. And then after it still complained that the string could be null (although it couldn't cause this was checked before)

// Load npub and nsec directly from hex pubkey string
final npubString = await npubFromHexPubkey(hexPubkey: activeAccount.pubkey);
final nsecString = await exportAccountNsec(pubkey: activeAccount.pubkey);
final npubString = await npubFromHexPubkey(hexPubkey: activePubkey);
Copy link
Contributor

Choose a reason for hiding this comment

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

We already have the npub. Is this still needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think the variable name is a bit misleading and I can change it, npubFromHexPubkey is actually converting from hex format to bech32. I’ll double-check if this conversion is really needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I renamed it to activeNpub and activeNsec respectively to avoid confusions

if (activeAccount != null) {
final activePubkey = ref.read(activePubkeyProvider);
if (activePubkey != null && activePubkey.isNotEmpty) {
final currentUserNpub = await npubFromHexPubkey(
Copy link
Contributor

Choose a reason for hiding this comment

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

activePubkey is already a String npub

Copy link
Contributor Author

Choose a reason for hiding this comment

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

but in hex format

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I’m considering renaming activePubkeyProvider to hexActivePubkeyProvider. I’m not a fan of making the name longer, but it would make it clearer that the value is in hex rather than bech32. Do you think that change would help? 🤔 @Quwaysim

Copy link
Contributor

Choose a reason for hiding this comment

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

@josefinalliende Apologies for the late response. I think the name will be unnecessarily long. We could add a doc to the provider noting that it's returning npub in hex rather than bech32. Many of the usages I've used personally are in hex.

@josefinalliende
Copy link
Contributor Author

@codeswot @Quwaysim Ithink I’ve addressed almost all of your comments, along with a few from CodeRabbit. I added support to the user profile data provider for handling both hex and bech32 pubkeys. This required changes in the ContactModel, public key extension methods, and some widgets — since the ContactModel now needs to be built with await (because it uses the Rust crate to transform hex into npub). This also fixes the issue that in some widgets npub was being shown in hex format instead of npub format.

@josefinalliende
Copy link
Contributor Author

Oh no, tests are failing 😢 ... will fix them tomorrow then 😴

@josefinalliende josefinalliende force-pushed the fix/metatada-issues branch 3 times, most recently from f0e57d2 to 608e91f Compare September 4, 2025 17:26
@josefinalliende
Copy link
Contributor Author

@codeswot @Quwaysim Ithink I’ve addressed almost all of your comments, along with a few from CodeRabbit. I added support to the user profile data provider for handling both hex and bech32 pubkeys. This required changes in the ContactModel, public key extension methods, and some widgets — since the ContactModel now needs to be built with await (because it uses the Rust crate to transform hex into npub). This also fixes the issue that in some widgets npub was being shown in hex format instead of npub format.

By making the pubkey conversion methods synchronous, I no longer need to update widgets with FutureBuilder to build contact models. The issue with npubs showing as hex is still fixed, but now in a much simpler way 😄. I also updated the change list in the PR description to make it easier to re-review

@josefinalliende josefinalliende force-pushed the fix/metatada-issues branch 2 times, most recently from aa3d859 to 0be4451 Compare September 4, 2025 17:47
@josefinalliende josefinalliende marked this pull request as ready for review September 4, 2025 18:03
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (13)
lib/ui/chat/chat_info/widgets/group_member_bottom_sheet.dart (2)

81-87: Ensure removeFromGroup receives NPUBs.

Param name membersNpubs implies npub; passing hex could fail server-side.

-            membersNpubs: [widget.member.publicKey],
+            membersNpubs: [widget.member.publicKey.toNpub() ?? widget.member.publicKey],

186-193: Normalize formats when checking admin status.

Avoid false negatives if admin list and current user differ in hex/npub.

-              (admin) => admin.publicKey == currentUserNpub,
+              (admin) =>
+                  ((admin.publicKey.toNpub() ?? admin.publicKey) == currentUserNpub),
lib/ui/contact_list/new_group_chat_sheet.dart (1)

117-122: Normalize pubkeys before excluding current user.

Avoids self-inclusion when formats differ (hex vs npub).

-  final contactsWithoutCreator =
-      contacts.where((contact) {
-        // Compare public keys, ensuring both are trimmed and lowercased for comparison
-        return currentUserPubkey == null ||
-            contact.publicKey.trim().toLowerCase() != currentUserPubkey.trim().toLowerCase();
-      }).toList();
+  final contactsWithoutCreator = contacts.where((contact) {
+    if (currentUserPubkey == null) return true;
+    final contactKey = contact.publicKey.toNpub() ?? contact.publicKey;
+    final currentKey = currentUserPubkey.toNpub() ?? currentUserPubkey;
+    return contactKey.trim().toLowerCase() != currentKey.trim().toLowerCase();
+  }).toList();

Add import:

+import 'package:whitenoise/utils/public_key_validation_extension.dart';
lib/utils/message_converter.dart (1)

3-3: Blocker: still depends on metadata_cache_provider; migrate to userProfileDataProvider.

PR removes the metadata cache provider; these references will break. Switch to the new provider.

-import 'package:whitenoise/config/providers/metadata_cache_provider.dart';
+import 'package:whitenoise/config/providers/user_profile_data_provider.dart';
@@
-      return await Future.microtask(() async {
-        final metadataCache = ref.read(metadataCacheProvider.notifier);
-        final contactModel = await metadataCache.getContactModel(pubkey);
-        return User(
+      return await Future.microtask(() async {
+        final contactModel = await ref.read(userProfileDataProvider(pubkey).future);
+        return User(
           id: pubkey,
           displayName: contactModel.displayName,
           nip05: contactModel.nip05 ?? '',
           publicKey: pubkey,
           imagePath: contactModel.imagePath,
         );
       });
@@
-      final metadataCache = ref.read(metadataCacheProvider.notifier);
-      final userFutures = uniquePubkeys.map(
-        (pubkey) =>
-            metadataCache.getContactModel(pubkey).then((contact) => MapEntry(pubkey, contact)),
-      );
+      final userFutures = uniquePubkeys.map(
+        (pubkey) => ref.read(userProfileDataProvider(pubkey).future)
+            .then((contact) => MapEntry(pubkey, contact)),
+      );

Also applies to: 531-541, 560-566

lib/ui/settings/profile/switch_profile_bottom_sheet.dart (2)

96-101: Active key normalization bug: _activeAccountHex may not be hex.

You store whatever activePubkeyProvider returns (npub or hex) into _activeAccountHex. Later comparisons are against hex, causing mismatches. Normalize to hex here.

-    final activeAccountPubkey = ref.read(activePubkeyProvider) ?? '';
-    if (activeAccountPubkey.isNotEmpty) {
-      setState(() {
-        _activeAccountHex = activeAccountPubkey;
-      });
-    }
+    final String activeAccountPubkey = ref.read(activePubkeyProvider) ?? '';
+    if (activeAccountPubkey.isEmpty) return;
+    final String activeHex = activeAccountPubkey.startsWith('npub1')
+        ? hexPubkeyFromNpub(npub: activeAccountPubkey)
+        : activeAccountPubkey;
+    setState(() {
+      _activeAccountHex = activeHex;
+    });

104-119: Make _isActiveAccount synchronous and remove FutureBuilder rebuild churn.

All conversions are synchronous now. Replace the async method and per-row FutureBuilder with a direct boolean. This reduces list rebuild “blinking” and simplifies logic.

Proposed changes (outside selected lines):

  • Replace the method:
bool _isActiveAccountSync(ContactModel profile) {
  final String aHex = _activeAccountHex ?? '';
  if (aHex.isEmpty) return false;
  final String pHex = _pubkeyToHex[profile.publicKey] ?? profile.publicKey;
  return aHex == pHex;
}
  • Replace FutureBuilder<bool> at Lines 179-207 with a simple Builder:
child: Builder(
  builder: (context) {
    final bool isActiveAccount = _isActiveAccountSync(profile);
    return Container(
      decoration: isActiveAccount
          ? BoxDecoration(color: context.colors.primary.withValues(alpha: 0.1))
          : null,
      // ...
    );
  },
),
lib/config/providers/nostr_keys_provider.dart (1)

66-72: Bug: npub field should store bech32, not raw input.

If pubkey is hex, storing it directly in npub will surface hex where npub is expected.

   void loadPublicKeyFromAccount(String pubkey) {
     state = state.copyWith(
-      npub: pubkey,
+      npub: pubkey.toNpub(),
       error: null,
     );
   }
lib/ui/contact_list/group_welcome_invitation_sheet.dart (1)

291-291: Missing await means errors bypass try/catch

Without await, exceptions from userMetadata won’t be caught here.

-      return wn_users_api.userMetadata(pubkey: widget.welcome.welcomer);
+      return await wn_users_api.userMetadata(pubkey: widget.welcome.welcomer);
lib/config/providers/chat_provider.dart (1)

309-361: Bug: mixed “append-only” path drops content changes (reactions/edits) when new messages also arrive

If both content changes and new messages exist, current code appends tail only and misses earlier updates. Split detection into “hasContentChanges” and “isPrefixAppend” and handle accordingly.

-      bool hasChanges = false;
+      bool hasContentChanges = false;
       if (newMessages.length != currentMessages.length) {
-        // New or deleted messages
-        hasChanges = true;
+        // Potential new/deleted messages; further checks below
       } else if (newMessages.isNotEmpty && currentMessages.isNotEmpty) {
         // Check if any message content or reactions have changed
         for (int i = 0; i < newMessages.length; i++) {
           final newMsg = newMessages[i];
           final currentMsg = currentMessages[i];
-          if (newMsg.content != currentMsg.content ||
-              newMsg.reactions.length != currentMsg.reactions.length ||
-              !_areReactionsEqual(newMsg.reactions, currentMsg.reactions)) {
-            hasChanges = true;
+          if (newMsg.content != currentMsg.content ||
+              newMsg.reactions.length != currentMsg.reactions.length ||
+              !_areReactionsEqual(newMsg.reactions, currentMsg.reactions)) {
+            hasContentChanges = true;
             break;
           }
         }
       }
-
-      if (hasChanges) {
-        if (newMessages.length > currentMessages.length) {
-          // Add only new messages to preserve performance
-          final newMessagesOnly = newMessages.skip(currentMessages.length).toList();
+      // Check if currentMessages is a strict prefix of newMessages (append-only case)
+      bool isPrefixAppend = false;
+      if (newMessages.length > currentMessages.length) {
+        isPrefixAppend = true;
+        for (int i = 0; i < currentMessages.length; i++) {
+          if (newMessages[i].id != currentMessages[i].id) {
+            isPrefixAppend = false;
+            break;
+          }
+        }
+      }
+
+      if (hasContentChanges || !isPrefixAppend) {
+        // Replace all to reflect edits/reactions/deletions/reorders
+        state = state.copyWith(
+          groupMessages: {
+            ...state.groupMessages,
+            groupId: newMessages,
+          },
+        );
+        _logger.info('ChatProvider: Updated messages with content changes for group $groupId');
+      } else if (isPrefixAppend) {
+        // Append tail only
+        final newMessagesOnly = newMessages.skip(currentMessages.length).toList();
           state = state.copyWith(
             groupMessages: {
               ...state.groupMessages,
               groupId: [...currentMessages, ...newMessagesOnly],
             },
           );
-          _logger.info(
-            'ChatProvider: Added ${newMessagesOnly.length} new messages for group $groupId',
-          );
-        } else {
-          // Replace all messages when there are content changes (reactions, edits, etc.)
-          state = state.copyWith(
-            groupMessages: {
-              ...state.groupMessages,
-              groupId: newMessages,
-            },
-          );
-
-          _logger.info(
-            'ChatProvider: Updated messages with content changes for group $groupId',
-          );
-        }
+        _logger.info('ChatProvider: Added ${newMessagesOnly.length} new messages for group $groupId');
       }
 
       // Update group order when messages are updated
       _updateGroupOrderForNewMessage(groupId);
lib/config/providers/relay_provider.dart (1)

64-107: Prevent cross-account relay leakage during async loads (add stale-guards).

If the active pubkey changes mid-flight, results for the previous account can overwrite state. Capture the pubkey before awaits and drop stale results after awaits, similar to the welcomes fix.

Apply this diff:

@@
-      final activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String capturedPubkey = activePubkey;
@@
-      final nip65RelayType = await relayTypeNip65();
-      final relayUrls = await accountRelays(
+      final nip65RelayType = await relayTypeNip65();
+      final relayUrls = await accountRelays(
         pubkey: activePubkey,
         relayType: nip65RelayType,
       ).then((value) => value.map((relay) => relay.url).toList());
+
+      // Active changed while fetching: drop stale batch
+      if (capturedPubkey != (ref.read(activePubkeyProvider) ?? '')) {
+        _logger.fine('NormalRelaysNotifier: Dropping stale load for $capturedPubkey');
+        state = state.copyWith(isLoading: false);
+        return;
+      }
@@
-      if (statusState.relayStatuses.isEmpty && !statusState.isLoading) {
+      if (statusState.relayStatuses.isEmpty && !statusState.isLoading) {
         _logger.info('NormalRelaysNotifier: Loading relay statuses first');
         await ref.read(relayStatusProvider.notifier).loadRelayStatuses();
       }
+
+      if (capturedPubkey != (ref.read(activePubkeyProvider) ?? '')) {
+        _logger.fine('NormalRelaysNotifier: Dropping stale status batch for $capturedPubkey');
+        state = state.copyWith(isLoading: false);
+        return;
+      }
@@
-      _logger.info('NormalRelaysNotifier: Successfully loaded ${relayInfos.length} relays');
+      if (capturedPubkey != (ref.read(activePubkeyProvider) ?? '')) {
+        _logger.fine('NormalRelaysNotifier: Active changed before commit; dropping $capturedPubkey results');
+        return;
+      }
+      _logger.info('NormalRelaysNotifier: Successfully loaded ${relayInfos.length} relays');
@@
-      final accountPubKey = ref.read(activePubkeyProvider) ?? '';
+      final String accountPubKey = ref.read(activePubkeyProvider) ?? '';
@@
-      final inboxRelayType = await relayTypeInbox();
-      final inboxRelayUrls = await accountRelays(
+      final inboxRelayType = await relayTypeInbox();
+      final String capturedPubkey = accountPubKey;
+      final inboxRelayUrls = await accountRelays(
         pubkey: accountPubKey,
         relayType: inboxRelayType,
       ).then((value) => value.map((relay) => relay.url).toList());
+
+      if (capturedPubkey != (ref.read(activePubkeyProvider) ?? '')) {
+        _logger.fine('InboxRelaysNotifier: Dropping stale load for $capturedPubkey');
+        state = state.copyWith(isLoading: false);
+        return;
+      }
@@
-      if (statusState.relayStatuses.isEmpty && !statusState.isLoading) {
+      if (statusState.relayStatuses.isEmpty && !statusState.isLoading) {
         _logger.info('InboxRelaysNotifier: Loading relay statuses first');
         await ref.read(relayStatusProvider.notifier).loadRelayStatuses();
       }
+
+      if (capturedPubkey != (ref.read(activePubkeyProvider) ?? '')) {
+        _logger.fine('InboxRelaysNotifier: Dropping stale status batch for $capturedPubkey');
+        state = state.copyWith(isLoading: false);
+        return;
+      }
@@
-      _logger.info('InboxRelaysNotifier: Successfully loaded ${relayInfos.length} relays');
+      if (capturedPubkey != (ref.read(activePubkeyProvider) ?? '')) {
+        _logger.fine('InboxRelaysNotifier: Active changed before commit; dropping $capturedPubkey results');
+        return;
+      }
+      _logger.info('InboxRelaysNotifier: Successfully loaded ${relayInfos.length} relays');
@@
-      final activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String capturedPubkey = activePubkey;
@@
-      final keyPackageType = await relayTypeKeyPackage();
-      final relayUrls = await accountRelays(
+      final keyPackageType = await relayTypeKeyPackage();
+      final relayUrls = await accountRelays(
         pubkey: activePubkey,
         relayType: keyPackageType,
       ).then((value) => value.map((relay) => relay.url).toList());
+
+      if (capturedPubkey != (ref.read(activePubkeyProvider) ?? '')) {
+        _logger.fine('KeyPackageRelaysNotifier: Dropping stale load for $capturedPubkey');
+        state = state.copyWith(isLoading: false);
+        return;
+      }
@@
-      _logger.info('KeyPackageRelaysNotifier: Successfully loaded ${relayInfos.length} relays');
+      if (capturedPubkey != (ref.read(activePubkeyProvider) ?? '')) {
+        _logger.fine('KeyPackageRelaysNotifier: Active changed before commit; dropping $capturedPubkey results');
+        return;
+      }
+      _logger.info('KeyPackageRelaysNotifier: Successfully loaded ${relayInfos.length} relays');

Also applies to: 182-207, 306-347

lib/ui/chat/invite/chat_invite_screen.dart (1)

99-105: Avoid arbitrary 1s delay; wait for group availability with a timeout.

Hard-coded sleeps are flaky. Await until the group is visible (or time out) before navigating.

-                                  final groupsNotifier = ref.read(groupsProvider.notifier);
-                                  await groupsNotifier.loadGroups();
-                                  await Future.delayed(const Duration(milliseconds: 1000));
-                                  if (context.mounted) {
-                                    context.pushReplacement('/chats/${widget.groupId}');
-                                  }
+                                  final groupsNotifier = ref.read(groupsProvider.notifier);
+                                  await groupsNotifier.loadGroups();
+                                  final bool navigated = await _navigateWhenGroupAvailable(
+                                    context,
+                                    ref,
+                                    widget.groupId,
+                                    const Duration(seconds: 5),
+                                  );
+                                  if (!navigated && context.mounted) {
+                                    context.pushReplacement('/chats/${widget.groupId}');
+                                  }

Add this helper inside _ChatInviteScreenState:

Future<bool> _navigateWhenGroupAvailable(
  BuildContext context,
  WidgetRef ref,
  String groupId,
  Duration timeout,
) async {
  final Stopwatch sw = Stopwatch()..start();
  while (sw.elapsed < timeout) {
    final groupsState = ref.read(groupsProvider);
    final bool exists = (groupsState.groups ?? const [])
        .any((g) => g.id == groupId);
    if (exists && context.mounted) {
      context.pushReplacement('/chats/$groupId');
      return true;
    }
    await Future.delayed(const Duration(milliseconds: 100));
  }
  return false;
}
lib/config/providers/group_provider.dart (2)

262-266: Missing await on e.messageText() in createNewGroup catch.

Without await, you log a Future instead of the error text.

-        final errorDetails = e.messageText();
+        final String errorDetails = await e.messageText();

816-821: Missing await on e.messageText() in checkForNewGroups catch.

Same issue as above, leads to logging a Future.

-        final errorDetails = e.messageText();
+        final String errorDetails = await e.messageText();
♻️ Duplicate comments (1)
lib/config/providers/group_provider.dart (1)

110-118: Operate on sortedGroups consistently and guard the microtask.

Use the sorted list for follow-up operations to avoid divergence. Wrap the microtask to prevent unhandled async errors.

-      await _loadMembersForAllGroups(groups);
+      await _loadMembersForAllGroups(sortedGroups);
@@
-      await _loadGroupTypesForAllGroups(groups);
+      await _loadGroupTypesForAllGroups(sortedGroups);
@@
-      await _calculateDisplayNames(groups, activePubkey);
+      await _calculateDisplayNames(sortedGroups, activePubkey);
@@
-            .loadMessagesForGroups(
-              groups.map((g) => g.mlsGroupId).toList(),
-            );
+            .loadMessagesForGroups(
+              sortedGroups.map((g) => g.mlsGroupId).toList(),
+            );

Optionally:

-      Future.microtask(() async {
-        await ref
+      Future.microtask(() async {
+        try {
+          await ref
             .read(chatProvider.notifier)
             .loadMessagesForGroups(
               sortedGroups.map((g) => g.mlsGroupId).toList(),
             );
+        } catch (e, st) {
+          _logger.warning('Deferred loadMessagesForGroups failed', e, st);
+        }
       });

Also applies to: 120-126

🧹 Nitpick comments (53)
lib/config/providers/polling_provider.dart (2)

79-79: Invalidate only after a successful initial load to avoid redundant rebuilds.
Consider moving this invalidation to the end of the initial load block (after the microtask completes) or coalescing it with the messages load completion to reduce extra rebuilds of activeAccountProvider.

-      ref.invalidate(activeAccountProvider);
+      // Coalesce with message preloads; ensure one refresh at the end.
+      // After the microtask:
+      Future.microtask(() async {
+        try {
+          // ...existing group/messages code...
+        } finally {
+          ref.invalidate(activeAccountProvider);
+        }
+      });

125-125: Guard against overlapping incremental ticks.
Timer.periodic every 2s can overlap runs if a cycle takes >2s, causing duplicate work and repeated invalidations. Add a lightweight in-flight guard.

 class PollingNotifier extends Notifier<bool> {
   static final _logger = Logger('PollingNotifier');
   Timer? _pollingTimer;
   bool _hasInitialDataLoaded = false;
   bool _isDisposed = false;
+  bool _isTickInFlight = false;

   // ...

   Future<void> _loadIncrementalData() async {
     if (!state) return;
+    if (_isTickInFlight) {
+      _logger.fine('Skip tick: previous incremental cycle still running');
+      return;
+    }
+    _isTickInFlight = true;
     try {
       // ...existing incremental polling...
-      ref.invalidate(activeAccountProvider);
+      ref.invalidate(activeAccountProvider);
     } catch (e) {
       _logger.warning('Error during incremental polling: $e');
+    } finally {
+      _isTickInFlight = false;
     }
   }
lib/config/providers/active_account_provider.dart (1)

194-231: Also refresh after profile picture upload.
Uploading returns the URL but UI depending on activeAccountProvider won’t refresh until the next poll. Invalidate here to reflect the new avatar immediately.

       _logger.fine(
         'Successfully uploaded profile picture for pubkey: $activePubkey, URL: $profilePictureUrl',
       );
-      return profilePictureUrl;
+      // Refresh account/metadata state so UI updates immediately.
+      ref.invalidateSelf();
+      return profilePictureUrl;
lib/ui/chat/chat_info/widgets/group_member_bottom_sheet.dart (2)

67-73: Make _loadCurrentUserNpub synchronous and avoid redundant setState.

No awaits inside; also guard against unnecessary rebuilds.

Apply:

-  void _loadCurrentUserNpub() async {
-    final activeAccountPubkey = ref.read(activePubkeyProvider) ?? '';
-    if (activeAccountPubkey.isNotEmpty) {
-      currentUserNpub = activeAccountPubkey.toNpub() ?? '';
-      setState(() {});
-    }
-  }
+  void _loadCurrentUserNpub() {
+    final String? activePubkey = ref.read(activePubkeyProvider);
+    if (activePubkey == null || activePubkey.isEmpty) return;
+    final String next = activePubkey.toNpub() ?? activePubkey;
+    if (next == currentUserNpub) return;
+    setState(() {
+      currentUserNpub = next;
+    });
+  }

49-57: Copy normalized npub (or original) and fix misleading local name.

Ensures clipboard gets npub when possible and improves readability.

-  void _copyToClipboard() {
-    final npub = widget.member.publicKey;
+  void _copyToClipboard() {
+    final String pubkeyToCopy = widget.member.publicKey.toNpub() ?? widget.member.publicKey;
     ClipboardUtils.copyWithToast(
       ref: ref,
-      textToCopy: npub,
+      textToCopy: pubkeyToCopy,
       successMessage: 'Public key copied',
       noTextMessage: 'No public key to copy',
     );
   }
lib/ui/contact_list/widgets/contact_list_tile.dart (3)

34-42: Drop async and use the pubkey extension for format-agnostic conversion.

Removes unnecessary Future + try/catch, avoids UI flicker.

-  Future<String> _getNpub(String publicKeyHex) async {
-    try {
-      final npub = npubFromHexPubkey(hexPubkey: publicKeyHex);
-      return npub.formatPublicKey();
-    } catch (e) {
-      // Return the full hex key as fallback
-      return publicKeyHex.formatPublicKey();
-    }
-  }
+  String _formatPubkey(String publicKey) {
+    final String? npub = publicKey.toNpub();
+    return (npub ?? publicKey).formatPublicKey();
+  }

Add/remove imports:

+import 'package:whitenoise/utils/public_key_validation_extension.dart';
-import 'package:whitenoise/src/rust/api/utils.dart';

80-112: Render synchronously; remove FutureBuilder to prevent unnecessary rebuilds.

Leverages the synchronous conversion above.

-                  FutureBuilder<String>(
-                    future: _getNpub(contact.publicKey),
-                    builder: (context, snapshot) {
-                      if (snapshot.hasData && snapshot.data!.isNotEmpty) {
-                        return Text(
-                          snapshot.data!,
-                          style: TextStyle(
-                            color: context.colors.mutedForeground,
-                            fontSize: 12.sp,
-                            fontFamily: 'monospace',
-                          ),
-                        );
-                      } else if (snapshot.hasError) {
-                        return Text(
-                          'Error loading npub',
-                          style: TextStyle(
-                            color: context.colors.mutedForeground.withValues(alpha: 0.6),
-                            fontSize: 12.sp,
-                            fontStyle: FontStyle.italic,
-                          ),
-                        );
-                      }
-                      // Show npub while loading immediately
-                      return Text(
-                        contact.publicKey.formatPublicKey(),
-                        style: TextStyle(
-                          color: context.colors.mutedForeground,
-                          fontSize: 12.sp,
-                          fontFamily: 'monospace',
-                        ),
-                      );
-                    },
-                  ),
+                  Text(
+                    _formatPubkey(contact.publicKey),
+                    style: TextStyle(
+                      color: context.colors.mutedForeground,
+                      fontSize: 12.sp,
+                      fontFamily: 'monospace',
+                    ),
+                  ),

151-153: Stabilize Dismissible key by normalizing.

Prevents duplicated keys if formats differ.

-        key: Key(contact.publicKey),
+        key: Key((contact.publicKey.toNpub() ?? contact.publicKey)),
lib/utils/message_converter.dart (1)

18-18: Normalize pubkey equality checks for isMe and replies.

Direct string equality is brittle if formats differ.

-    final isMe = currentUserPublicKey != null && messageData.pubkey == currentUserPublicKey;
+    final isMe = currentUserPublicKey != null &&
+        ((messageData.pubkey.toNpub() ?? messageData.pubkey) ==
+         (currentUserPublicKey!.toNpub() ?? currentUserPublicKey));

Add once at top:

+import 'package:whitenoise/utils/public_key_validation_extension.dart';

Also applies to: 155-155, 246-246, 403-404

lib/utils/public_key_validation_extension.dart (3)

49-56: Tighten npub detection/validation or delegate to parser.

startsWith('npub1') && length > 10 is permissive. Consider either:

  • Delegating validation to the FRB conversion (attempt convert-in-try, return null on error), or
  • Introducing a stricter check (e.g., bech32 charset/length) if you must short-circuit before FRB.

Current behavior is fine functionally (conversion still guards), but a stricter check would avoid unnecessary FRB calls on obviously invalid inputs.


53-55: Add explicit local types to match repo guidelines.

Declare local types for readability and to follow the “Always declare the type” guideline.

-    final trimmedPubkey = trim().toLowerCase();
+    final String trimmedPubkey = trim().toLowerCase();
-    final trimmedPubkey = trim().toLowerCase();
+    final String trimmedPubkey = trim().toLowerCase();

Also applies to: 74-76


18-19: Avoid re-allocating identical RegExp instances.

Hoist the hex regex to a static final or top-level final and reuse it in both places.

Example (outside selected lines):

// top-level
final RegExp _hex64 = RegExp(r'^[0-9a-fA-F]{64}$');

Then:

return _hex64.hasMatch(trimmed);

Also applies to: 33-34

lib/ui/settings/profile/switch_profile_bottom_sheet.dart (2)

64-81: _precomputeProfileHexes can be synchronous.

No await is used. Either drop async/Future or intentionally schedule it off the frame using Future.microtask for UI smoothness.

Minimal change:

-  Future<void> _precomputeProfileHexes() async {
+  void _precomputeProfileHexes() {
     // ...
-    if (mounted) setState(() {});
+    if (mounted) setState(() {});
   }

8-8: UI should not import FRB utils directly.

Once you switch to the extension, remove utils.dart import here to align with our layering (UI -> extensions; extensions -> FRB).

lib/config/providers/relay_status_provider.dart (1)

48-55: Clear stale errors when (re)loading.

Keep UX clean by clearing error on new loads.

-    state = state.copyWith(isLoading: true);
+    state = state.copyWith(isLoading: true, error: null);
lib/ui/chat/chat_info/group_chat_info.dart (2)

33-35: Normalize pubkey with .toNpub()

-        final npub = npubFromHexPubkey(
-          hexPubkey: groupDetails!.nostrGroupId,
-        );
+        final npub = groupDetails!.nostrGroupId.toNpub() ?? '';

The required extension import (public_key_validation_extension.dart) is already in chat_info_screen.dart.


105-108: Normalize activePubkey with .toNpub()
Replace manual npubFromHexPubkey(...) with the toNpub() extension (handles both HEX and NPUB, returns null on failure), for example:

- final activeAccount = ref.read(activePubkeyProvider) ?? '';
- if (activeAccount.isNotEmpty) {
-   final currentUserNpub = npubFromHexPubkey(hexPubkey: activeAccount);
+ final String activePubkey = ref.read(activePubkeyProvider) ?? '';
+ final String? npub = activePubkey.toNpub();
+ if (npub?.isNotEmpty ?? false) {
+   final currentUserNpub = npub;

Also verify that any member.publicKey you compare against is already in NPUB format.

lib/ui/settings/profile/share_profile_screen.dart (1)

40-42: Normalize safely and avoid returning Future when not awaiting

Also fall back to the original value if normalization returns null; consider listening to updates so switching profiles refreshes npub.

Apply:

-  Future<void> loadProfile() async {
+  void loadProfile() {
     try {
-      final activeAccountPubkey = ref.read(activePubkeyProvider) ?? '';
-      npub = activeAccountPubkey.toNpub() ?? '';
+      final String activeAccountPubkey = ref.read(activePubkeyProvider) ?? '';
+      npub = activeAccountPubkey.toNpub() ?? activeAccountPubkey;
       setState(() {});

Optional (outside the shown lines): in initState, add a listener to update on account switch:

late final removeListener = ref.listen<String?>(activePubkeyProvider, (_, next) {
  final normalized = (next ?? '').toNpub() ?? (next ?? '');
  if (mounted && npub != normalized) setState(() => npub = normalized);
}).close;
@override
void dispose() { removeListener(); super.dispose(); }
lib/domain/models/contact_model.dart (1)

26-28: Clarify parameter semantics

If pubkey accepts either HEX or NPUB, add a short docstring to make the accepted formats explicit and the canonical output (publicKey) format clear.

lib/config/providers/nostr_keys_provider.dart (2)

33-41: Add explicit type and adjust message.

  • Per codebase guidelines, declare local variable types.
  • Message should reflect the actual condition (no pubkey vs no account).
-      final activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String activePubkey = ref.read(activePubkeyProvider) ?? '';
       if (activePubkey.isEmpty) {
-        _logger.severe('NostrKeysNotifier: No active account found');
+        _logger.severe('NostrKeysNotifier: No active pubkey found');
         state = state.copyWith(
           isLoading: false,
-          error: 'No active account found',
+          error: 'No active pubkey found',
         );
         return;
       }

18-64: Formatting nits inside functions.

Project guideline says no blank lines within functions; consider tightening spacing in loadKeys for consistency.

rust/src/api/utils.rs (1)

17-21: Clarify accepted input formats in docs.

PublicKey::parse accepts hex and bech32; document this to prevent misuse and surprises.

-#[frb(sync)]
-pub fn hex_pubkey_from_npub(npub: &str) -> Result<String, ApiError> {
+/// Converts a bech32 (npub) or hex pubkey string to hex.
+/// Accepts either format; if hex is provided, it is returned normalized.
+#[frb(sync)]
+pub fn hex_pubkey_from_npub(npub: &str) -> Result<String, ApiError> {
     let pubkey = PublicKey::parse(npub).map_err(ApiError::from)?;
     Ok(pubkey.to_hex())
 }
lib/ui/contact_list/group_welcome_invitation_sheet.dart (4)

116-118: Deduplicate the “activePubkey gating” pattern

This guard appears three times. Extract a tiny helper (e.g., _readActivePubkeyOrToast()) to DRY this and keep error text consistent.

-      final activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String activePubkey = ref.read(activePubkeyProvider) ?? '';

Also applies to: 243-245, 286-288


129-131: Prefer the String.toNpub() extension; drop unnecessary async wrappers

npubFromHexPubkey is no longer needed here; use the extension to normalize either hex or npub and make these helpers synchronous.

-  Future<String> _getDisplayablePublicKey() async {
-    try {
-      final npub = npubFromHexPubkey(hexPubkey: widget.welcome.nostrGroupId);
-      return npub;
-    } catch (e) {
-      return widget.welcome.nostrGroupId.formatPublicKey();
-    }
-  }
+  String _getDisplayablePublicKey() {
+    try {
+      return widget.welcome.nostrGroupId.toNpub();
+    } catch (_) {
+      return widget.welcome.nostrGroupId.formatPublicKey();
+    }
+  }
-  Future<String> _getDisplayablePublicKey() async {
-    try {
-      final npub = npubFromHexPubkey(hexPubkey: widget.welcome.welcomer);
-      return npub;
-    } catch (e) {
-      return widget.welcome.welcomer.formatPublicKey();
-    }
-  }
+  String _getDisplayablePublicKey() {
+    try {
+      return widget.welcome.welcomer.toNpub();
+    } catch (_) {
+      return widget.welcome.welcomer.formatPublicKey();
+    }
+  }

Also applies to: 297-303


208-222: Remove FutureBuilder now that key conversion is sync

Render directly; avoids extra frames and simplifies UI.

-        FutureBuilder<String>(
-          future: _getDisplayablePublicKey(),
-          builder: (context, npubSnapshot) {
-            final displayKey = npubSnapshot.data ?? widget.welcome.welcomer;
-            return Text(
-              displayKey.formatPublicKey(),
-              textAlign: TextAlign.center,
-              style: TextStyle(
-                fontSize: 14.sp,
-                fontWeight: FontWeight.w500,
-                color: context.colors.mutedForeground,
-              ),
-            );
-          },
-        ),
+        Text(
+          _getDisplayablePublicKey().formatPublicKey(),
+          textAlign: TextAlign.center,
+          style: TextStyle(
+            fontSize: 14.sp,
+            fontWeight: FontWeight.w500,
+            color: context.colors.mutedForeground,
+          ),
+        ),

Also applies to: 365-379


182-204: Leverage userProfileDataProvider instead of direct API calls

Switching to the provider improves caching and consistency with the PR’s direction.

Would you like a patch converting these FutureBuilders to Consumer widgets that watch userProfileDataProvider(welcomerPubkey)?

Also applies to: 256-268, 308-385

lib/config/providers/chat_provider.dart (3)

67-69: Type explicitly and consider a small helper for active pubkey

Follow codebase guideline to declare types, and centralize this guard.

-      final activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String activePubkey = ref.read(activePubkeyProvider) ?? '';

Apply similarly at the other occurrences above.

Also applies to: 145-147, 287-289, 500-502, 573-575, 677-679


424-438: Avoid mutating the parameter with ??=; prefer a local with ??

Improves readability and avoids side effects on the parameter.

-    final gId = groupId ??= state.selectedGroupId;
+    final String? gId = groupId ?? state.selectedGroupId;

Apply similarly in isNextSameSender.


123-128: Define constants for Nostr kinds (5, 7, 9) instead of magic numbers

Extract to a shared constants file (e.g., nostr_kinds.dart) and use named constants for readability and maintainability.

Also applies to: 490-559, 562-663, 665-717

lib/config/providers/auth_provider.dart (2)

53-58: Use explicit types for locals per repo guidelines

Declare the variable types explicitly.

-          final storedActivePubkey = ref.read(activePubkeyProvider) ?? '';
+          final String storedActivePubkey = ref.read(activePubkeyProvider) ?? '';
...
-        final currentActivePubkey = ref.read(activePubkeyProvider);
+        final String? currentActivePubkey = ref.read(activePubkeyProvider);

Also applies to: 182-184


283-291: Minor: avoid logging full pubkeys if privacy-sensitive

If logs are shared, consider abbreviating pubkeys (e.g., first/last 6 chars).

Also applies to: 324-331

lib/config/providers/relay_provider.dart (4)

64-66: Use explicit local types per project Dart guidelines.

Declare local variable types explicitly (String) to match codebase standards.

-      final activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String activePubkey = ref.read(activePubkeyProvider) ?? '';
@@
-      final accountPubKey = ref.read(activePubkeyProvider) ?? '';
+      final String accountPubKey = ref.read(activePubkeyProvider) ?? '';
@@
-      final accountPubKey = ref.read(activePubkeyProvider) ?? '';
+      final String accountPubKey = ref.read(activePubkeyProvider) ?? '';
@@
-      final accountPubKey = ref.read(activePubkeyProvider) ?? '';
+      final String accountPubKey = ref.read(activePubkeyProvider) ?? '';
@@
-      final accountPubKey = ref.read(activePubkeyProvider) ?? '';
+      final String accountPubKey = ref.read(activePubkeyProvider) ?? '';
@@
-      final accountPubkey = ref.read(activePubkeyProvider) ?? '';
+      final String accountPubkey = ref.read(activePubkeyProvider) ?? '';
@@
-      final activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String activePubkey = ref.read(activePubkeyProvider) ?? '';
@@
-      final accountPubkey = ref.read(activePubkeyProvider) ?? '';
+      final String accountPubkey = ref.read(activePubkeyProvider) ?? '';
@@
-      final accountPubkey = ref.read(activePubkeyProvider) ?? '';
+      final String accountPubkey = ref.read(activePubkeyProvider) ?? '';

Also applies to: 119-124, 137-141, 182-186, 238-243, 258-269, 306-314, 359-366, 374-382


139-141: Fix misleading log messages in delete paths and align severity.

Logs say “adding relay” in delete handlers; also “no active account” should be a warning, not severe.

-        _logger.severe('RelayProvider: No active account found for adding relay');
+        _logger.warning('RelayProvider: No active account found for deleting relay');

(Apply similarly in InboxRelaysNotifier.deleteRelay and KeyPackageRelaysNotifier.deleteRelay.)

Also applies to: 261-263, 377-379


117-133: Surface user-facing errors when no active account is present.

Early-returning without updating state leaves the UI unaware. Consider setting an error on state for add/delete paths.

Also applies to: 135-150, 236-254, 256-274, 357-370, 372-386


93-105: Reduce duplication: factor RelayInfo assembly into a helper.

The three notifiers duplicate the same mapping to RelayInfo. Extract into a private helper to keep logic in sync.

Also applies to: 212-224, 333-345

lib/config/providers/welcomes_provider.dart (2)

114-123: Optionally guard fetch-by-id against mid-flight account switches.

Capture active pubkey before await and drop results if it changes.

-      final activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String capturedPubkey = activePubkey;
@@
-      final welcome = await findWelcomeByEventId(
-        pubkey: activePubkey,
+      final welcome = await findWelcomeByEventId(
+        pubkey: capturedPubkey,
         welcomeEventId: welcomeEventId,
       );
+      if (capturedPubkey != (ref.read(activePubkeyProvider) ?? '')) {
+        return null;
+      }

Also applies to: 125-131


62-64: Prefer explicit local types.

Use explicit String types for readability and consistency with the project style.

-      final activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String activePubkey = ref.read(activePubkeyProvider) ?? '';
@@
-      final activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String activePubkey = ref.read(activePubkeyProvider) ?? '';
@@
-      final activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String activePubkey = ref.read(activePubkeyProvider) ?? '';
@@
-      final activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String activePubkey = ref.read(activePubkeyProvider) ?? '';
@@
-      final activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String activePubkey = ref.read(activePubkeyProvider) ?? '';
@@
-      final String capturedPubkey = activePubkey;
+      final String capturedPubkey = activePubkey;

Also applies to: 114-116, 149-151, 181-183, 314-316, 319-321

lib/ui/chat/invite/chat_invite_screen.dart (3)

180-187: Use the centralized pubkey normalization/formatting extension (handles hex and npub).

Directly calling npubFromHexPubkey(...) assumes hex input. Prefer the project’s pubkey normalization extension so UI renders correctly regardless of input format.

If you share the exact extension/method name (e.g., from the pubkey validations extension), I can provide a precise diff here.

Also applies to: 282-289


38-40: Make local types explicit and avoid duplicate fetches.

  • Declare local types (WelcomesNotifier, bool, UserProfileDataNotifier) to align with style.
  • DM header and app bar each call getUserProfileData(...); consider computing once (FutureProvider family) and sharing.
-    final welcomesNotifier = ref.watch(welcomesProvider.notifier);
+    final WelcomesNotifier welcomesNotifier = ref.watch(welcomesProvider.notifier);
@@
-    final isDMInvite = welcome.memberCount <= 2;
+    final bool isDMInvite = welcome.memberCount <= 2;
@@
-    final userProfileDataNotifier = ref.read(userProfileDataProvider.notifier);
+    final UserProfileDataNotifier userProfileDataNotifier =
+        ref.read(userProfileDataProvider.notifier);
@@
-    final userProfileDataNotifier = ref.read(userProfileDataProvider.notifier);
+    final UserProfileDataNotifier userProfileDataNotifier =
+        ref.read(userProfileDataProvider.notifier);

Also applies to: 49-51, 139-143, 244-248, 329-333


41-45: Localize user-facing strings via AppLocalizations.

Strings like “Invitation not found”, “Decline”, “Accept”, “Group Description”, and sentence fragments should be localized.

Also applies to: 74-76, 85-86, 191-198, 211-218, 301-303

lib/domain/services/dm_chat_service.dart (3)

13-18: Type the local var and prefer the .toNpub() extension over direct utils call.

  • Declare activePubkey explicitly.
  • Use the string extension for npub conversion to keep format handling consistent across the app.
-      final activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String activePubkey = ref.read(activePubkeyProvider) ?? '';
@@
-      final currentUserNpub = npubFromHexPubkey(
-        hexPubkey: activePubkey,
-      );
+      final String currentUserNpub = activePubkey.toNpub();

41-41: Fix typo and clarify naming.

npup likely meant npub. Rename for clarity.

-        final npup = contactModel.publicKey;
+        final npub = contactModel.publicKey;
@@
-          publicKey: npup,
+          publicKey: npub,

51-53: Log the failure instead of swallowing it silently.

Add at least a warning log to aid debugging.

-    } catch (e) {
-      return null;
+    } catch (e, st) {
+      // TODO: inject logger if available
+      // ignore: avoid_print
+      print('DMChatService.getDMChatData($groupId) failed: $e');
+      return null;
lib/config/providers/group_provider.dart (3)

80-83: Use explicit typing for activePubkey at all read sites.

Follow our Dart guideline to declare types explicitly; also keeps intent clear (we operate on hex pubkeys here).

-      final activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String activePubkey = ref.read(activePubkeyProvider) ?? '';
@@
-      final activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String activePubkey = ref.read(activePubkeyProvider) ?? '';
@@
-      final activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String activePubkey = ref.read(activePubkeyProvider) ?? '';
@@
-      final activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String activePubkey = ref.read(activePubkeyProvider) ?? '';
@@
-      final activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String activePubkey = ref.read(activePubkeyProvider) ?? '';
@@
-      final activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String activePubkey = ref.read(activePubkeyProvider) ?? '';
@@
-      final activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String activePubkey = ref.read(activePubkeyProvider) ?? '';
@@
-      final activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String activePubkey = ref.read(activePubkeyProvider) ?? '';
@@
-      final activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String activePubkey = ref.read(activePubkeyProvider) ?? '';
@@
-      final activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String activePubkey = ref.read(activePubkeyProvider) ?? '';

Also applies to: 155-160, 195-200, 299-305, 378-385, 492-496, 720-727, 764-770, 879-884, 935-939


158-160: Prefer the .toNpub() extension for conversions.

Keeps format handling centralized and matches recent refactors.

-      final currentUserNpub = npubFromHexPubkey(hexPubkey: activePubkey);
-      final otherUserNpub = npubFromHexPubkey(hexPubkey: otherUserPubkeyHex);
+      final String currentUserNpub = activePubkey.toNpub();
+      final String otherUserNpub = otherUserPubkeyHex.toNpub();
@@
-          final pubkeyString = npubFromHexPubkey(hexPubkey: memberPubkey);
+          final String pubkeyString = memberPubkey.toNpub();
@@
-          final pubkeyString = npubFromHexPubkey(hexPubkey: adminPubkey);
+          final String pubkeyString = adminPubkey.toNpub();
@@
-        final currentUserNpub = npubFromHexPubkey(hexPubkey: currentUserPubkey);
+        final String currentUserNpub = currentUserPubkey.toNpub();

Also applies to: 313-314, 393-394, 598-599


214-229: Use the trimmed creator key consistently.

You compute creatorPubkeyHex but still log/pass activePubkey. Use the trimmed value in admin set, logs, and the API call.

-      final combinedAdminKeys = {activePubkey, ...resolvedAdminPublicKeys}.toList();
+      final combinedAdminKeys = {creatorPubkeyHex, ...resolvedAdminPublicKeys}.toList();
@@
-      _logger.info('  - Creator pubkey: $activePubkey');
+      _logger.info('  - Creator pubkey: $creatorPubkeyHex');
@@
-      final newGroup = await createGroup(
-        creatorPubkey: activePubkey,
+      final newGroup = await createGroup(
+        creatorPubkey: creatorPubkeyHex,

Also applies to: 235-247, 241-247

test/config/providers/user_profile_data_provider_test.dart (1)

184-195: Error-path test is adequate. Consider asserting the error message.

Optional: assert on the thrown message to avoid false positives when other exceptions are thrown.

-          expect(
-            () async => await notifier.getUserProfileData(testNpubPubkey),
-            throwsException,
-          );
+          expect(
+            () async => notifier.getUserProfileData(testNpubPubkey),
+            throwsA(isA<Exception>().having((e) => e.toString(), 'message', contains('API error'))),
+          );
lib/config/providers/metadata_cache_provider.dart (6)

68-76: Make conversion helpers synchronous; they already call sync FRB functions.

These functions return immediately; keeping them Future forces unnecessary awaits across the file and adds overhead.

Apply this diff:

-  Future<String> _safeHexToNpub(String hexPubkey) async {
+  String _safeHexToNpub(String hexPubkey) {
     try {
       return npubFromHexPubkey(hexPubkey: hexPubkey);
     } catch (e) {
       _logger.warning('Failed to convert hex to npub for $hexPubkey: $e');
       return hexPubkey;
     }
   }

79-86: Ditto: make NPUB→HEX helper synchronous.

Apply this diff:

-  Future<String> _safeNpubToHex(String npub) async {
+  String _safeNpubToHex(String npub) {
     try {
       return hexPubkeyFromNpub(npub: npub);
     } catch (e) {
       _logger.warning('Failed to convert npub to hex for $npub: $e');
       return npub;
     }
   }

89-100: Cascade sync: make _getStandardizedNpub and its call sites synchronous.

With the helpers sync, _getStandardizedNpub can also be sync and all call sites can drop await.

Apply these diffs:

-  Future<String> _getStandardizedNpub(String publicKey) async {
+  String _getStandardizedNpub(String publicKey) {
     final normalized = _normalizePublicKey(publicKey);

     if (normalized.isValidNpubPublicKey) {
       return normalized;
     } else if (normalized.isValidHexPublicKey) {
-      return await _safeHexToNpub(normalized);
+      return _safeHexToNpub(normalized);
     } else {
       _logger.warning('Unrecognized public key format: $normalized');
       return normalized;
     }
   }
-        fetchKey = await _safeNpubToHex(publicKey);
+        fetchKey = _safeNpubToHex(publicKey);
-      final standardNpub = await _getStandardizedNpub(publicKey);
+      final standardNpub = _getStandardizedNpub(publicKey);
-    final standardNpub = await _getStandardizedNpub(normalizedKey);
+    final standardNpub = _getStandardizedNpub(normalizedKey);

Also applies to: 110-111, 120-121, 153-154


150-210: Prefer immutable state maps and explicit local types.

  • Use Map.unmodifiable for cache and pendingFetches to reinforce immutability.
  • Declare explicit types for locals per code style (e.g., final CachedMetadata? cached = ...;).

I can push a small follow-up with these readability tweaks if you want.


18-25: Avoid magic numbers for cache expiry.

Extract the default expiry to a named constant for discoverability.

Add near the top of the file:

const Duration kDefaultMetadataCacheExpiry = Duration(hours: 1);

Then:

-    this.cacheExpiry = const Duration(hours: 1),
+    this.cacheExpiry = kDefaultMetadataCacheExpiry,

1-216: Style nits per repo guidelines.

There are blank lines within functions and a few implicitly typed locals. The repo’s Dart rules ask to avoid blank lines inside functions and to declare variable types explicitly.

Happy to run a quick pass aligning to the style if you prefer.

Comment on lines +66 to 76
final activePubkey = ref.read(activePubkeyProvider) ?? '';
_logger.info('RelayStatusNotifier: Active account data: $activePubkey');
if (activePubkey.isEmpty) {
_logger.warning('RelayStatusNotifier: No active account found');
state = state.copyWith(isLoading: false, error: 'No active account found');
return;
}

_logger.info(
'RelayStatusNotifier: Fetching relay statuses for pubkey: ${activeAccount.pubkey}',
'RelayStatusNotifier: Fetching relay statuses for pubkey: $activePubkey',
);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Provider doesn’t react to active pubkey changes. Add a listener in build().

loadRelayStatuses() reads the pubkey once. If the active pubkey changes, statuses won’t refresh. Listen to activePubkeyProvider and reload.

Add in build() (outside selected lines):

@override
RelayStatusState build() {
  ref.listen<String?>(activePubkeyProvider, (prev, next) {
    if (prev != next) {
      loadRelayStatuses();
    }
  });
  ref.listen<bool>(authProvider.select((s) => s.isAuthenticated), (prev, next) {
    if (prev != next) {
      loadRelayStatuses();
    }
  });
  Future.microtask(() => loadRelayStatuses());
  return const RelayStatusState(isLoading: true);
}
🤖 Prompt for AI Agents
In lib/config/providers/relay_status_provider.dart around lines 66 to 76, the
notifier only reads activePubkey once so it won’t refresh when the active pubkey
or auth state changes; in build() add ref.listen on activePubkeyProvider and on
authProvider.select((s) => s.isAuthenticated) that call loadRelayStatuses() when
the value changes, and schedule an initial load via Future.microtask(() =>
loadRelayStatuses()); this ensures statuses reload on active account or auth
changes and still performs the initial fetch.

Comment on lines +62 to 69
final activePubkey = ref.read(activePubkeyProvider) ?? '';
if (activePubkey.isEmpty) {
state = state.copyWith(error: 'No active account found', isLoading: false);
return;
}

final welcomes = await pendingWelcomes(pubkey: activeAccount.pubkey);
final welcomes = await pendingWelcomes(pubkey: activePubkey);

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add the same stale-guard to full loads (loadWelcomes).

Active pubkey can change during pendingWelcomes; guard like in polling to avoid mixing accounts.

-      final activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String activePubkey = ref.read(activePubkeyProvider) ?? '';
+      final String capturedPubkey = activePubkey;
@@
-      final welcomes = await pendingWelcomes(pubkey: activePubkey);
+      final welcomes = await pendingWelcomes(pubkey: capturedPubkey);
+      if (capturedPubkey != (ref.read(activePubkeyProvider) ?? '')) {
+        state = state.copyWith(isLoading: false);
+        return;
+      }
@@
-      state = state.copyWith(
+      state = state.copyWith(
         welcomes: welcomes,
         welcomeById: welcomeByData,
         isLoading: false,
       );

Also applies to: 78-83

🤖 Prompt for AI Agents
In lib/config/providers/welcomes_provider.dart around lines 62-69 (and similarly
78-83), the code reads activePubkey then awaits pendingWelcomes/loadWelcomes
without re-checking whether the active pubkey changed, which can mix results
across accounts; capture the activePubkey into a local final variable before the
await, then after the await re-read ref.read(activePubkeyProvider) (or compare
to the captured variable) and if it differs, discard the results and return (or
set state appropriately) to avoid applying welcomes for the wrong account; apply
the same stale-guard pattern used in polling to both pendingWelcomes and
loadWelcomes blocks.

Comment on lines +79 to 81
int get rustContentHash => -1927809292;

static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig(
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Content hash changed; ensure all generated artifacts are in sync.

If you re-generated this file, also re-generate frb_generated.io.dart, frb_generated.web.dart, and the Rust side. Mismatched funcIds will crash at runtime.

Quick sanity check:


🏁 Script executed:

#!/bin/bash
# Look for funcId definitions and count to spot obvious drift
rg -n 'funcId:\s*\d+' lib/src/rust -g '!**/build/**' | wc -l
rg -n 'funcId:\s*\d+' rust -g '!**/target/**' | wc -l || true

Length of output: 123


Sync generated bindings: Dart vs Rust funcId count mismatch
The sanity check found 56 funcId entries in lib/src/rust/frb_generated.dart but none in the Rust directory. Re-generate the Rust bindings and update frb_generated.io.dart and frb_generated.web.dart so that all funcIds align and avoid runtime crashes.

🤖 Prompt for AI Agents
In lib/src/rust/frb_generated.dart around lines 79-81, the generated Dart
bindings contain 56 funcId entries that do not match the Rust side (no funcId
entries found), causing a Dart vs Rust funcId count mismatch; re-run the Rust
binding generator to regenerate the Rust bindings, then regenerate
frb_generated.io.dart and frb_generated.web.dart so all funcId tables are
consistent across Rust and both Dart targets, verify the rustContentHash updates
appropriately, and run the sanity check to confirm counts match before
committing.

codeswot
codeswot previously approved these changes Sep 4, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
test/config/providers/active_pubkey_provider_test.dart (1)

123-149: Good addition: loadActivePubkey trims surrounding whitespace

This covers a real-world footgun from secure storage reads. Consider adding:

  • a companion test ensuring setActivePubkey persists a trimmed value (verify the write),
  • and a case where storage returns only whitespace (e.g., ' ') to assert it becomes ''.

Apply these diffs to extend coverage:

@@ group('loadActivePubkey', () {
+      group('when storage has only whitespaces', () {
+        setUp(() async {
+          when(
+            mockStorage.read(key: 'active_account_pubkey'),
+          ).thenAnswer((_) async => '   ');
+        });
+
+        test('sets state to empty string', () async {
+          final notifier = container.read(activePubkeyProvider.notifier);
+          await notifier.loadActivePubkey();
+          expect(container.read(activePubkeyProvider), '');
+        });
+      });
@@ group('setActivePubkey', () {
+      group('when input has surrounding whitespaces', () {
+        setUp(() async {
+          when(
+            mockStorage.write(key: 'active_account_pubkey', value: 'test_pubkey_123'),
+          ).thenAnswer((_) async {});
+        });
+
+        test('trims before persisting and updates state', () async {
+          final notifier = container.read(activePubkeyProvider.notifier);
+          await notifier.setActivePubkey('  test_pubkey_123  ');
+          expect(container.read(activePubkeyProvider), 'test_pubkey_123');
+          verify(mockStorage.write(
+            key: 'active_account_pubkey',
+            value: 'test_pubkey_123',
+          )).called(1);
+        });
+
+        test('notifies to listeners', () async {
+          final notifier = container.read(activePubkeyProvider.notifier);
+          var wasNotified = false;
+          container.listen(activePubkeyProvider, (prev, next) {
+            wasNotified = true;
+          });
+          await notifier.setActivePubkey('  test_pubkey_123  ');
+          expect(wasNotified, isTrue);
+        });
+      });
lib/config/providers/active_pubkey_provider.dart (1)

33-35: Trim on set and avoid redundant writes/notifications

Minor optimization: short-circuit when the trimmed value equals the current state to reduce secure storage I/O and unnecessary provider notifications. Also declare the local type explicitly per guidelines.

Apply:

-  Future<void> setActivePubkey(String pubkey) async {
-    final trimmedPubkey = pubkey.trim();
-    await AccountSecureStorageService.setActivePubkey(trimmedPubkey, storage: storage);
-    state = trimmedPubkey;
-  }
+  Future<void> setActivePubkey(String pubkey) async {
+    final String trimmedPubkey = pubkey.trim();
+    if (trimmedPubkey == state) {
+      return; // no-op: avoid redundant write/notify
+    }
+    await AccountSecureStorageService.setActivePubkey(trimmedPubkey, storage: storage);
+    state = trimmedPubkey;
+  }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5f13985 and b612ce8.

📒 Files selected for processing (2)
  • lib/config/providers/active_pubkey_provider.dart (1 hunks)
  • test/config/providers/active_pubkey_provider_test.dart (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.dart

📄 CodeRabbit inference engine (.cursor/rules/flutter.mdc)

**/*.dart: Always declare the type of each variable and function (parameters and return value). Avoid using 'any'. Create necessary types.
Don't leave blank lines within a function.
One export per file.
Use PascalCase for classes.
Use camelCase for variables, functions, and methods.
Use underscores_case for file and directory names.
Use UPPERCASE for environment variables. Avoid magic numbers and define constants.
Start each function with a verb.
Use verbs for boolean variables. Example: isLoading, hasError, canDelete, etc.
Use complete words instead of abbreviations and correct spelling, except for standard and well-known abbreviations (API, URL, i, j, err, ctx, req, res, next).
Write short functions with a single purpose. Less than 20 instructions.
Name functions with a verb and something else. If it returns a boolean, use isX or hasX, canX, etc. If it doesn't return anything, use executeX or saveX, etc.
Avoid nesting blocks by early checks and returns, or extraction to utility functions.
Use higher-order functions (map, filter, reduce, etc.) to avoid function nesting. Use arrow functions for simple functions (less than 3 instructions). Use named functions for non-simple functions.
Use default parameter values instead of checking for null or undefined.
Reduce function parameters using RO-RO: use an object to pass multiple parameters and to return results. Declare necessary types for input arguments and output.
Use a single level of abstraction in functions.
Don't abuse primitive types and encapsulate data in composite types.
Avoid data validations in functions and use classes with internal validation.
Prefer immutability for data. Use readonly for data that doesn't change. Use 'as const' for literals that don't change.
Declare interfaces to define contracts.
Write small classes with a single purpose. Less than 200 instructions, less than 10 public methods, less than 10 properties.
Use exceptions to handle errors you don't expect. If you catch an exception, it sh...

Files:

  • lib/config/providers/active_pubkey_provider.dart
  • test/config/providers/active_pubkey_provider_test.dart
**/*_test.dart

📄 CodeRabbit inference engine (.cursor/rules/flutter.mdc)

**/*_test.dart: Follow the Arrange-Act-Assert convention for tests.
Name test variables clearly. Follow the convention: inputX, mockX, actualX, expectedX, etc.
Write unit tests for each public function. Use test doubles to simulate dependencies, except for third-party dependencies that are not expensive to execute.
Write acceptance tests for each module. Follow the Given-When-Then convention.
Use the standard widget testing for Flutter.

Files:

  • test/config/providers/active_pubkey_provider_test.dart
🧠 Learnings (4)
📓 Common learnings
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#594
File: lib/config/providers/user_profile_data_provider.dart:0-0
Timestamp: 2025-09-03T20:57:53.202Z
Learning: In the whitenoise_flutter codebase, pubkey normalization (npub/hex format handling) is implemented in a pubkey validations extension rather than being imported directly into individual providers like user_profile_data_provider.dart.
📚 Learning: 2025-09-05T00:08:34.466Z
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#594
File: lib/config/providers/active_account_provider.dart:140-146
Timestamp: 2025-09-05T00:08:34.466Z
Learning: josefinalliende prefers centralizing validation logic (like trimming whitespace) in providers rather than handling it defensively at every consumer site. This approach was applied to activePubkeyProvider in the whitenoise_flutter codebase.

Applied to files:

  • lib/config/providers/active_pubkey_provider.dart
📚 Learning: 2025-09-03T20:57:53.202Z
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#594
File: lib/config/providers/user_profile_data_provider.dart:0-0
Timestamp: 2025-09-03T20:57:53.202Z
Learning: In the whitenoise_flutter codebase, pubkey normalization (npub/hex format handling) is implemented in a pubkey validations extension rather than being imported directly into individual providers like user_profile_data_provider.dart.

Applied to files:

  • lib/config/providers/active_pubkey_provider.dart
  • test/config/providers/active_pubkey_provider_test.dart
📚 Learning: 2025-08-12T11:21:53.640Z
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#455
File: lib/ui/settings/profile/switch_profile_bottom_sheet.dart:0-0
Timestamp: 2025-08-12T11:21:53.640Z
Learning: In the whitenoise_flutter codebase, ContactModel.publicKey can be stored in either npub format (from metadata cache) or hex format (from account storage). The activeAccountProvider may return either format depending on how the account was originally stored, so normalization to hex format is required when comparing with other hex-normalized keys in sorting logic.

Applied to files:

  • lib/config/providers/active_pubkey_provider.dart
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Flutter CI
🔇 Additional comments (1)
lib/config/providers/active_pubkey_provider.dart (1)

27-30: Trim on load — LGTM

Setting state = pubkey?.trim() prevents downstream bugs due to accidental whitespace from storage.

Copy link
Contributor

@Quwaysim Quwaysim left a comment

Choose a reason for hiding this comment

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

LGTM 🚀

@josefinalliende josefinalliende merged commit b4cffff into master Sep 5, 2025
2 checks passed
@josefinalliende josefinalliende deleted the fix/metatada-issues branch September 5, 2025 13:35
@josefinalliende josefinalliende mentioned this pull request Sep 6, 2025
11 tasks
This was referenced Sep 8, 2025
@josefinalliende josefinalliende mentioned this pull request Sep 11, 2025
11 tasks
This was referenced Sep 13, 2025
@coderabbitai coderabbitai bot mentioned this pull request Oct 22, 2025
11 tasks
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.

Can't set Profile Picture Edit profile deletes metadata set in other nostr client

4 participants