Skip to content

Conversation

@erskingardner
Copy link
Member

@erskingardner erskingardner commented Aug 21, 2025

This is part one of moving things to the new bridge API methods. I'm sure I'm missing some things still

Summary by CodeRabbit

  • New Features

    • Follow/unfollow users across the app; follows appear in contact lists and selection UIs.
    • Create & edit profile flows with image picking/upload and metadata updates.
    • App settings surfaced (theme control) and app info (version) shown.
  • Refactor

    • Chat/groups/messages unified on consistent domain models for improved UI behavior.
    • Account handling moved to a pubkey-driven async flow for quicker switching and state consistency.
    • Improved, centralized error messages shown to users.
  • Chores

    • Added MIME and package_info_plus dependencies; .env files ignored; updated cross-platform build recipes.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 21, 2025

Walkthrough

Migrates from account-centered to pubkey-centered identity, replaces Contacts with Follows, introduces ApiError and Flutter-friendly Rust bridge types (Account/Group/Message/Relay/User/Welcome/FlutterMetadata), rewrites many providers/UI to new types, adds two-step Whitenoise config/init, and regenerates FRB bindings.

Changes

Cohort / File(s) Summary
High-level docs removed
.context/00_high_level.md, .context/01_rust_api.md
Deleted long-form design and Rust API docs.
Git & build tooling
.gitignore, justfile, android/app/build.gradle.kts, pubspec.yaml, macos/Flutter/GeneratedPluginRegistrant.swift
Ignore env files, replace APK just target with cross-platform release recipes, remove some Gradle TODOs, add dependencies (mime, package_info_plus), register package_info_plus for macOS.
Rust: API surface & error model
rust/src/api/*.rs, rust/Cargo.toml, rust/src/api/mod.rs
Rework FRB Rust API to domain-first types (Account, Group, ChatMessage/MessageWithTokens, Relay, User, Welcome, FlutterMetadata, WhitenoiseConfig), switch public IDs to strings, add centralized ApiError, convert many endpoints to string inputs/DateTime, add crates (chrono, thiserror, etc.), remove old contacts module.
Dart FRB bindings (regenerated)
lib/src/rust/frb_generated.io.dart, lib/src/rust/api/*.dart
Regenerated Dart bridge files matching Rust changes: error, metadata, users, accounts, groups, messages, relays, welcomes, app settings/theme, etc.; removed many legacy opaque bindings.
Providers — identity & storage
lib/config/providers/*
Move to pubkey-centric flow: add active_pubkey_provider.dart (secure storage service), rewrite active_account_provider.dart to fetch via server APIs, remove account_provider.dart; many providers updated to use activePubkey/ActiveAccountState.
Providers — contacts → follows
lib/config/providers/contacts_provider.dart (removed), lib/config/providers/follows_provider.dart
Delete contacts provider; add followsProvider with FollowsState/notifier and follow management API (load/add/remove/lookup).
Profile/create/edit flows
lib/config/providers/create_profile_screen_provider.dart, lib/config/providers/edit_profile_screen_provider.dart, lib/config/providers/profile_provider.dart (removed)
Add create/edit profile providers (image pick/upload, metadata update), remove legacy profile_provider.dart.
Chat, groups, messages, welcomes
lib/config/providers/chat_provider.dart, group_provider.dart, metadata_cache_provider.dart, relay_provider.dart, relay_status_provider.dart, welcomes_provider.dart, lib/config/states/*
Migrate Data* types → domain types (Group/Welcome/ChatMessage/MessageWithTokens), adopt ApiError handling, async group-type lookups and groupTypes cache, string-based relays/relayType, update many provider signatures and return types to async/domain variants.
UI — widespread rewires
lib/ui/**
Update widgets to new domain types and providers: GroupData/WelcomeData/MessageWithTokensData → Group/Welcome/MessageWithTokens, replace contacts UI with follows, update navigation and callback signatures to accept Group, hook screens to activePubkey/activeAccount flows.
Message conversion & chat state
lib/utils/message_converter.dart, lib/ui/chat/states/chat_state.dart, lib/ui/chat/utils/message_extensions.dart
Convert converters and chat state to use ChatMessage/MessageWithTokens/SerializableToken/ReactionSummary with DateTime; adjust cache/reply lookup and user resolution to use follows/users.
Users & metadata
rust/src/api/metadata.rs, lib/src/rust/api/metadata.dart, rust/src/api/users.rs, lib/src/rust/api/users.dart, lib/utils/user_utils.dart
Add FlutterMetadata bridge with round-trip conversions; add users API (getUser, userMetadata, userRelays, userHasKeyPackage); add UserUtils for sorting/filtering.
Services — image & picker
lib/utils/image_utils.dart, lib/domain/services/image_picker_service.dart
Add ImageUtils.getMimeTypeFromPath and ImagePickerService.pickProfileImage (uses image_picker), plus mime dependency.
Services removed/replaced
lib/domain/services/key_package_service.dart (removed), lib/domain/services/dm_chat_service.dart
Remove KeyPackageService; add WnUsersApi abstraction (DefaultWnUsersApi) and use userHasKeyPackage for key-package checks; minor DM service provider updates.
FRB-generated ApiError
lib/src/rust/api/error.dart, lib/src/rust/api/error.freezed.dart
Add generated ApiError union with variants and helper methods (errorType, messageText); update Dart error handling utilities to consume ApiError.
Misc utils & string conversions
lib/utils/string_extensions.dart, lib/utils/user_utils.dart
Switch npub/hex helpers to new bridge utils (npubFromHexPubkey), add UserUtils helpers.
Deleted design files
.context/*.md
Remove long-form high-level design and Rust API markdown.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Flutter as Flutter App
  participant FRB as FRB Bridge
  participant Rust as Rust API
  User->>Flutter: Launch app
  Flutter->>FRB: createWhitenoiseConfig(dataDir, logsDir)
  FRB->>Rust: create_whitenoise_config
  Rust-->>FRB: WhitenoiseConfig
  Flutter->>FRB: initializeWhitenoise(config)
  FRB->>Rust: initialize_whitenoise(config)
  Rust-->>FRB: Ok
  FRB-->>Flutter: Init complete
  note over Flutter: Providers start: activePubkey -> activeAccount (server fetch)
Loading
sequenceDiagram
  autonumber
  actor User
  participant UI as App UI
  participant ActivePubkey as activePubkeyProvider
  participant ActiveAccount as activeAccountProvider
  participant AccountsAPI as WnAccountsApi (bridge)
  User->>UI: Log in / switch account
  UI->>ActivePubkey: setActivePubkey(pubkey)
  ActivePubkey-->>UI: pubkey state
  UI->>ActiveAccount: build()/reload
  ActiveAccount->>AccountsAPI: getAccount(pubkey) & getAccountMetadata(pubkey)
  AccountsAPI-->>ActiveAccount: Account + FlutterMetadata
  ActiveAccount-->>UI: ActiveAccountState
  UI->>ActiveAccount: uploadProfilePicture(file) / updateMetadata(...)
  ActiveAccount->>AccountsAPI: uploadAccountProfilePicture / updateAccountMetadata
  AccountsAPI-->>ActiveAccount: pictureUrl / Ok
  ActiveAccount-->>UI: invalidate -> reload state
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~150 minutes

Possibly related PRs

Suggested reviewers

  • erskingardner
  • Quwaysim
  • untreu2

Poem

I nibble keys and hop through threads,
Pubkeys glint where old accounts once led.
Follows sprout and welcomes sing,
Rust and Flutter stitched like spring.
A rabbit cheers the new code spread. 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch big-plans-part-deux

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 @coderabbit in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbit 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:
    • @coderabbit gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbit 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 @coderabbit help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbit ignore or @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbit summary or @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbit or @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.

@erskingardner erskingardner marked this pull request as draft August 21, 2025 09:30
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

Caution

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

⚠️ Outside diff range comments (1)
lib/ui/settings/app_settings/app_settings_screen.dart (1)

35-36: Fix user-facing copy (typo and grammar) and localize.

  • "Delete app app data" → "Delete app data"
  • "every profile, key, and local files" → "all profiles, keys, and local files"
    Also externalize strings via AppLocalizations per guidelines.
-            title: 'Delete app app data',
-            content: 'This will erase every profile, key, and local files. This can\'t be undone.',
+            title: 'Delete app data',
+            content: 'This will erase all profiles, keys, and local files. This can\'t be undone.',
🧹 Nitpick comments (6)
lib/ui/settings/app_settings/app_settings_screen.dart (6)

17-17: Use camelCase alias for the API import and update call sites.

Follow the code style guide for identifiers.

-import 'package:whitenoise/src/rust/api.dart' as wn_api;
+import 'package:whitenoise/src/rust/api.dart' as wnApi;

Also adjust usage below (see line 90):

-      await wn_api.deleteAllData().timeout(
+      await wnApi.deleteAllData().timeout(

90-95: Avoid magic number: extract delete timeout to a constant.

Improves readability and consistency with guidelines.

-      await wn_api.deleteAllData().timeout(
-        const Duration(seconds: 30),
+      await wnApi.deleteAllData().timeout(
+        _deleteTimeout,
         onTimeout: () {
           throw Exception('Delete operation timed out after 30 seconds');
         },
       );

Add near the top of the file (outside any class):

const Duration _deleteTimeout = Duration(seconds: 30);

124-128: Validate need for legacy activeAccountProvider invalidation.

If the pubkey model supersedes accounts, drop this block (and the legacy import) to prevent redundant work and confusion.

-      try {
-        ref.invalidate(activeAccountProvider);
-        _logger.info('✅ Active account provider invalidated');
-      } catch (e) {
-        _logger.warning('⚠️ Error invalidating active account provider: $e');
-      }

137-141: Consider invalidation order with identity first.

Invalidate activePubkeyProvider before dependents (e.g., followsProvider) to ensure recompute order is identity → dependents.


27-27: Add explicit types per guidelines.

Tighten types for logger, dialog result, and theme mode.

-  static final _logger = Logger('AppSettingsScreen');
+  static final Logger _logger = Logger('AppSettingsScreen');
-    final confirmed = await showDialog<bool>(
+    final bool? confirmed = await showDialog<bool>(
-    final themeMode = ref.watch(themeProvider).themeMode;
+    final ThemeMode themeMode = ref.watch(themeProvider).themeMode;

Also applies to: 30-30, 175-175


8-9: Keep legacy provider imports until full migration is complete
ActiveAccountProvider is still referenced throughout the codebase (UI screens, services, tests). Add a // TODO: above these imports to clarify the transitional duality and only remove the legacy import and its usages once all references have been migrated to the new provider.

📜 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 8b865d4 and 044800e.

📒 Files selected for processing (2)
  • lib/ui/contact_list/widgets/chat_list_item_tile.dart (2 hunks)
  • lib/ui/settings/app_settings/app_settings_screen.dart (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/ui/contact_list/widgets/chat_list_item_tile.dart
🧰 Additional context used
📓 Path-based instructions (1)
**/*.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/settings/app_settings/app_settings_screen.dart
🧠 Learnings (1)
📚 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/ui/settings/app_settings/app_settings_screen.dart
🔇 Additional comments (2)
lib/ui/settings/app_settings/app_settings_screen.dart (2)

12-12: LGTM: followsProvider import aligns with the rename.


131-135: LGTM: followsProvider invalidation.

Optional: if follows depend on the active pubkey, consider invalidating activePubkeyProvider first to avoid any transient reads on a stale identity.

@erskingardner erskingardner merged commit 248bcbb into master Aug 29, 2025
1 check passed
@erskingardner erskingardner deleted the big-plans-part-deux branch August 29, 2025 13:29
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: 9

Caution

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

⚠️ Outside diff range comments (1)
lib/ui/settings/network/widgets/network_section.dart (1)

26-31: Fix equality override signature; use Object and a type check.

Overriding == with covariant RelayInfo breaks the standard contract and can cause asymmetric comparisons. Use Object and an is check. Also removes the blank line inside the function per guidelines.

Apply this diff:

-  @override
-  bool operator ==(covariant RelayInfo other) {
-    if (identical(this, other)) return true;
-
-    return other.url == url && other.connected == connected && other.status == status;
-  }
+  @override
+  bool operator ==(Object other) =>
+      identical(this, other) ||
+      (other is RelayInfo &&
+          other.url == url &&
+          other.connected == connected &&
+          other.status == status);
♻️ Duplicate comments (5)
justfile (3)

208-209: Align wording: “for all platforms” likely inaccurate

Past analysis shows the script builds Android and iOS only. Either update the echo or extend the script to truly build all platforms.

Apply one:

-    @echo "🎁 Building versioned release for all platforms..."
+    @echo "🎁 Building versioned release for Android and iOS..."

or extend scripts/build.sh to include macOS/Windows/Linux/Web, then keep the original message. To confirm actual targets, run the verification snippet in the first comment.


204-210: Release must not run auto-fixing precommit; use a check-only gate

Running fix during release makes artifacts non-reproducible. Swap to a non-mutating precommit.

Apply:

 release:
     @echo "🔨 Building versioned release..."
-    @echo "✔︎ Running a precommit check..."
-    just precommit
+    @echo "✔︎ Running pre-commit checks..."
+    just precommit-check

Add this recipe elsewhere in the justfile:

# Check-only precommit (no auto-fixes), safe for CI/release
precommit-check:
    just deps-flutter
    just deps-rust
    just check-rust-format
    just check-dart-format
    just lint
    just test-flutter
    just test-rust
    @echo "✅ All pre-commit checks passed (no auto-fixes)."

204-210: Guard the release build: clean tree + script existence checks

Fail fast if the repo is dirty or the build script is missing/not executable.

Apply:

     @echo "🎁 Building versioned release for all platforms..."
-    ./scripts/build.sh --full --versioned
+    @test -x ./scripts/build.sh || { echo "❌ ./scripts/build.sh missing or not executable"; exit 1; }
+    @test -z "$$(git status --porcelain)" || { echo "❌ Uncommitted/untracked changes detected. Commit or stash before releasing."; exit 1; }
+    ./scripts/build.sh --full --versioned
rust/src/api/messages.rs (1)

31-33: Replace Debug tag formatting with a stable representation (Vec<Vec>).

Debug strings are unstable across versions and languages. Emit tags as vectors of fields instead and convert deterministically.

- use crate::api::{error::ApiError, utils::group_id_from_string};
+ use crate::api::{error::ApiError, utils::{group_id_from_string, tag_to_vec}};
@@
-    pub tags: Vec<String>, // Simplified tags representation for Flutter
+    pub tags: Vec<Vec<String>>, // Stable tag fields for Flutter
@@
-        let tags = chat_message
+        let tags = chat_message
             .tags
             .iter()
-            .map(|tag| format!("{tag:?}"))
+            .map(|tag| tag_to_vec(tag))
             .collect();

Add helper (in utils.rs or here) to produce a deterministic Vec per Tag:

pub fn tag_to_vec(tag: &nostr_sdk::Tag) -> Vec<String> {
    // Implement using nostr-sdk’s stable conversion (or match variants).
    // Example fallback (adjust as needed):
    tag.as_slice().iter().map(|s| s.to_string()).collect()
}

Also applies to: 189-193, 1-1

lib/domain/models/contact_model.dart (1)

51-58: Null-safety: user.metadata can be null.
This will throw today. Guard and sanitize.

-    final metadata = user.metadata;
-    final displayName = _sanitizeString(metadata.displayName);
-    final about = _sanitizeString(metadata.about);
-    final website = _sanitizeUrl(metadata.website);
-    final nip05 = _sanitizeString(metadata.nip05);
-    final lud16 = _sanitizeString(metadata.lud16);
-    final picture = _sanitizeUrl(metadata.picture);
+    final metadata = user.metadata; // may be null
+    final String displayName = _sanitizeString(metadata?.displayName);
+    final String about = _sanitizeString(metadata?.about);
+    final String? website = _sanitizeUrl(metadata?.website);
+    final String nip05 = _sanitizeString(metadata?.nip05);
+    final String lud16 = _sanitizeString(metadata?.lud16);
+    final String? picture = _sanitizeUrl(metadata?.picture);
🧹 Nitpick comments (15)
rust/Cargo.toml (3)

19-22: Remove redundant version keys on git deps; Cargo ignores them for git sources.

Having both git/rev and version is confusing; version is not used for resolution here.

-nostr-mls = { version = "0.43.0", git="https://github.com/rust-nostr/nostr", rev = "84b1a016cffc30625567a03e2d3bcae86463f075" }
-nostr-sdk = { version = "0.43.0", git="https://github.com/rust-nostr/nostr", rev = "84b1a016cffc30625567a03e2d3bcae86463f075" }
+nostr-mls = { git = "https://github.com/rust-nostr/nostr", rev = "84b1a016cffc30625567a03e2d3bcae86463f075" }
+nostr-sdk = { git = "https://github.com/rust-nostr/nostr", rev = "84b1a016cffc30625567a03e2d3bcae86463f075" }

26-27: Same for whitenoise: drop unused version when pinning to a git rev.

Keeps dependency intent unambiguous. If a release tag exists matching the needed APIs, consider pinning to tag instead of a raw commit for easier audits.

-whitenoise = { version = "0.1.0", git = "https://github.com/parres-hq/whitenoise", rev = "b1ac58bfa3da5c907acac4ce0788b5c2ffbd0d70" }
+whitenoise = { git = "https://github.com/parres-hq/whitenoise", rev = "b1ac58bfa3da5c907acac4ce0788b5c2ffbd0d70" }

Optional supply‑chain hardening:

  • Add cargo-deny/cargo-audit to CI for license and vuln checks.
  • Commit Cargo.lock (for app/binary outputs) to lock transitive deps.

12-12: Rust and Dart FRB versions match; consider caret range (optional)
The Dart pubspec’s flutter_rust_bridge: 2.11.1 aligns with the crate’s =2.11.1. If strict pinning isn’t required for codegen, you can relax to a caret constraint (e.g. ^2.11.1) to automatically receive patch fixes.

rust/src/api/messages.rs (2)

90-96: DRY timestamp conversion into a small helper.

Same fallible u64→DateTime logic appears three times.

-            created_at: {
-                let ts =
-                    i64::try_from(message_with_tokens.message.created_at.as_u64()).unwrap_or(0);
-                Utc.timestamp_opt(ts, 0)
-                    .single()
-                    .unwrap_or_else(|| Utc.timestamp_opt(0, 0).single().unwrap())
-            },
+            created_at: utc_from_u64(message_with_tokens.message.created_at.as_u64()),
@@
-                created_at: {
-                    let ts = i64::try_from(user_reaction.created_at.as_u64()).unwrap_or(0);
-                    Utc.timestamp_opt(ts, 0)
-                        .single()
-                        .unwrap_or_else(|| Utc.timestamp_opt(0, 0).single().unwrap())
-                },
+                created_at: utc_from_u64(user_reaction.created_at.as_u64()),
@@
-            created_at: {
-                let ts = i64::try_from(chat_message.created_at.as_u64()).unwrap_or(0);
-                Utc.timestamp_opt(ts, 0)
-                    .single()
-                    .unwrap_or_else(|| Utc.timestamp_opt(0, 0).single().unwrap())
-            },
+            created_at: utc_from_u64(chat_message.created_at.as_u64()),

Add once:

fn utc_from_u64(ts_u64: u64) -> chrono::DateTime<Utc> {
    let ts = i64::try_from(ts_u64).unwrap_or(0);
    Utc.timestamp_opt(ts, 0).single().unwrap_or_else(|| Utc.timestamp_opt(0, 0).single().unwrap())
}

Also applies to: 164-169, 210-214


20-20: Consider making MessageWithTokens.content a plain String.

It’s always Some(...). Drop Option to simplify the model.

-    pub content: Option<String>,
+    pub content: String,
@@
-            content: Some(message_with_tokens.message.content.clone()),
+            content: message_with_tokens.message.content.clone(),

Also applies to: 97-99

rust/src/api/relays.rs (2)

46-51: Make output order deterministic (sort by URL).

Stable ordering helps UI diffs and caching.

Apply:

-    let converted_statuses = statuses
-        .into_iter()
-        .map(|(url, status)| (url.to_string(), status.to_string()))
-        .collect();
-    Ok(converted_statuses)
+    let mut converted_statuses: Vec<(String, String)> = statuses
+        .into_iter()
+        .map(|(url, status)| (url.to_string(), status.to_string()))
+        .collect();
+    converted_statuses.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
+    Ok(converted_statuses)

1-6: Nit: avoid glob import from nostr_sdk prelude here.

For a small file only using PublicKey, prefer explicit import to minimize surface (optional).

rust/src/api/accounts.rs (4)

28-31: Return accounts in a stable order (newest first).

Deterministic ordering reduces UI churn.

-    let accounts = whitenoise.all_accounts().await?;
-    Ok(accounts.into_iter().map(|a| a.into()).collect())
+    let accounts = whitenoise.all_accounts().await?;
+    let mut out: Vec<Account> = accounts.into_iter().map(|a| a.into()).collect();
+    out.sort_unstable_by(|a, b| b.updated_at.cmp(&a.updated_at));
+    Ok(out)

84-95: Prefer owned FlutterMetadata over borrowed in FRB surface.

Borrowed params can complicate FRB codegen; owned is simpler and consistent with other endpoints.

-pub async fn update_account_metadata(
-    pubkey: String,
-    metadata: &FlutterMetadata,
-) -> Result<(), ApiError> {
+pub async fn update_account_metadata(
+    pubkey: String,
+    metadata: FlutterMetadata,
+) -> Result<(), ApiError> {
     let whitenoise = Whitenoise::get_instance()?;
     let pubkey = PublicKey::parse(&pubkey)?;
     let account = whitenoise.find_account_by_pubkey(&pubkey).await?;
     account
-        .update_metadata(&metadata.into(), whitenoise)
+        .update_metadata(&metadata.into(), whitenoise)
         .await
         .map_err(ApiError::from)
 }

Please confirm FRB generates cleanly with owned FlutterMetadata.


118-124: Sort relays by URL before returning.

Stable ordering helps UI and tests.

-    let relays = account.relays(relay_type, whitenoise).await?;
-    Ok(relays.into_iter().map(|r| r.into()).collect())
+    let relays = account.relays(relay_type, whitenoise).await?;
+    let mut out: Vec<Relay> = relays.into_iter().map(|r| r.into()).collect();
+    out.sort_unstable_by(|a, b| a.url.cmp(&b.url));
+    Ok(out)

35-40: Extract pubkey parsing into a helper
Repeated calls to PublicKey::parse(&pubkey) across this module—extract into

fn parse_pubkey(input: &str) -> Result<PublicKey, ApiError>

and reuse instead of duplicating the parse logic.

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

90-93: Add explicit types per project guidelines.

-      final messages = await MessageConverter.fromChatMessageList(
+      final List<MessageModel> messages = await MessageConverter.fromChatMessageList(
@@
-      final sentChatMessage = ChatMessage(
+      final ChatMessage sentChatMessage = ChatMessage(
@@
-      final messageCache = <String, ChatMessage>{};
+      final Map<String, ChatMessage> messageCache = <String, ChatMessage>{};
@@
-      final sentMessageModel = await MessageConverter.fromChatMessage(
+      final MessageModel sentMessageModel = await MessageConverter.fromChatMessage(
@@
-      final newMessages = await MessageConverter.fromChatMessageList(
+      final List<MessageModel> newMessages = await MessageConverter.fromChatMessageList(
@@
-          final newMessagesOnly = newMessages.skip(currentMessages.length).toList();
+          final List<MessageModel> newMessagesOnly = newMessages.skip(currentMessages.length).toList();
@@
-      final sentChatMessage = ChatMessage(
+      final ChatMessage sentChatMessage = ChatMessage(
@@
-      final messageCache = <String, ChatMessage>{};
+      final Map<String, ChatMessage> messageCache = <String, ChatMessage>{};
@@
-      final sentMessageModel = await MessageConverter.fromChatMessage(
+      final MessageModel sentMessageModel = await MessageConverter.fromChatMessage(

Also applies to: 168-181, 183-201, 202-209, 303-308, 337-338, 605-617, 620-637, 639-645


121-129: Replace magic kind numbers with named constants.
Improves readability and prevents drift.

Add near the top of this file:

// Nostr kinds
const int nostrKindText = 9;
const int nostrKindReaction = 7;
const int nostrKindDeletion = 5;

Then:

-    int kind = 9, // Default to text message
+    int kind = nostrKindText,
@@
-        kind: 7, // Nostr kind 7 = reaction
+        kind: nostrKindReaction,
@@
-        kind: 9, // Kind 9 for replies
+        kind: nostrKindText,
@@
-        kind: 5, // Nostr kind 5 = deletion
+        kind: nostrKindDeletion,

Also applies to: 543-545, 597-599, 704-706


510-514: Use the validated groupId variable to avoid null-coalescing to ''.
Removes redundant null handling and avoids accidental empty key usage.

-      final groupId = message.groupId;
-      if (groupId == null || groupId.isEmpty) {
+      final String? groupId = message.groupId;
+      if (groupId == null || groupId.isEmpty) {
         _logger.warning('Cannot update reaction: message has no groupId');
         return false;
       }
@@
-        groupId: message.groupId ?? '',
+        groupId: groupId,
@@
-      await refreshMessagesForGroup(message.groupId ?? '');
+      await refreshMessagesForGroup(groupId);

Also applies to: 540-545, 548-548


231-233: Error strings: localize via AppLocalizations.

Consider wiring AppLocalizations and moving these user-facing strings to l10n.

Also applies to: 557-558, 662-663, 717-718

📜 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 044800e and 3650477.

⛔ Files ignored due to path filters (6)
  • android/app/src/main/jniLibs/arm64-v8a/librust_lib_whitenoise.so is excluded by !**/*.so
  • android/app/src/main/jniLibs/armeabi-v7a/librust_lib_whitenoise.so is excluded by !**/*.so
  • android/app/src/main/jniLibs/x86_64/librust_lib_whitenoise.so is excluded by !**/*.so
  • ios/Podfile.lock is excluded by !**/*.lock
  • pubspec.lock is excluded by !**/*.lock
  • rust/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • justfile (1 hunks)
  • lib/config/providers/chat_provider.dart (24 hunks)
  • lib/domain/models/contact_model.dart (4 hunks)
  • lib/domain/models/dm_chat_data.dart (1 hunks)
  • lib/domain/models/message_model.dart (1 hunks)
  • lib/domain/models/user_model.dart (3 hunks)
  • lib/ui/settings/general_settings_screen.dart (9 hunks)
  • lib/ui/settings/network/widgets/network_section.dart (1 hunks)
  • macos/Flutter/GeneratedPluginRegistrant.swift (2 hunks)
  • pubspec.yaml (1 hunks)
  • rust/Cargo.toml (1 hunks)
  • rust/src/api/accounts.rs (1 hunks)
  • rust/src/api/groups.rs (1 hunks)
  • rust/src/api/messages.rs (1 hunks)
  • rust/src/api/relays.rs (1 hunks)
  • rust/src/api/users.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • pubspec.yaml
  • rust/src/api/users.rs
  • rust/src/api/groups.rs
🧰 Additional context used
📓 Path-based instructions (1)
**/*.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/domain/models/message_model.dart
  • lib/domain/models/user_model.dart
  • lib/ui/settings/network/widgets/network_section.dart
  • lib/domain/models/dm_chat_data.dart
  • lib/config/providers/chat_provider.dart
  • lib/domain/models/contact_model.dart
  • lib/ui/settings/general_settings_screen.dart
🧠 Learnings (6)
📚 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:

  • rust/Cargo.toml
  • lib/config/providers/chat_provider.dart
  • rust/src/api/messages.rs
📚 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:

  • rust/Cargo.toml
📚 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/chat_provider.dart
📚 Learning: 2025-08-29T09:11:07.251Z
Learnt from: erskingardner
PR: parres-hq/whitenoise_flutter#530
File: rust/src/api/utils.rs:23-26
Timestamp: 2025-08-29T09:11:07.251Z
Learning: In the nostr-sdk Rust library, PublicKey::parse method accepts both hex and bech32 (npub) encoded strings, so there's no need to use format-specific methods like PublicKey::from_bech32 when parsing public keys from string input.

Applied to files:

  • rust/src/api/messages.rs
  • rust/src/api/accounts.rs
📚 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:

  • rust/src/api/messages.rs
  • lib/domain/models/contact_model.dart
  • rust/src/api/accounts.rs
  • 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/settings/general_settings_screen.dart
🧬 Code graph analysis (3)
rust/src/api/messages.rs (2)
rust/src/api/utils.rs (1)
  • group_id_from_string (66-69)
rust/src/api/error.rs (1)
  • from (37-41)
rust/src/api/relays.rs (3)
rust/src/api/accounts.rs (1)
  • from (17-24)
rust/src/api/groups.rs (4)
  • from (30-47)
  • from (90-96)
  • from (109-114)
  • from (126-130)
rust/src/api/users.rs (1)
  • from (18-25)
rust/src/api/accounts.rs (3)
rust/src/api/groups.rs (4)
  • from (30-47)
  • from (90-96)
  • from (109-114)
  • from (126-130)
rust/src/api/relays.rs (1)
  • from (16-22)
rust/src/api/users.rs (1)
  • from (18-25)
🔇 Additional comments (41)
lib/ui/settings/network/widgets/network_section.dart (1)

34-34: Good move to Object.hash for better distribution and readability.

This aligns with modern Dart and keeps hash fields consistent with equality.

macos/Flutter/GeneratedPluginRegistrant.swift (2)

14-14: LGTM: Correct macOS import for package_info_plus.

Importing the module as package_info_plus is correct for the macOS target and aligns with the plugin’s current packaging.


27-27: package_info_plus class prefix is correct
pubspec.lock pins package_info_plus at 2.2.0 and your macOS GeneratedPluginRegistrant.swift registers FPPPackageInfoPlusPlugin, matching the plugin’s recent prefix.

rust/Cargo.toml (1)

15-15: Current Tokio features are sufficient
No occurrences of #[tokio::main], #[tokio::test], or tokio::time were found in the codebase, so the existing ["rt", "rt-multi-thread"] feature set is adequate.

rust/src/api/messages.rs (4)

241-243: Pubkey parsing: good upgrade to parse() (hex or npub).

This aligns with the recent utils learning and hardens inputs.

Also applies to: 256-257


68-73: Token enums mapped clearly.

Straightforward, Flutter-friendly representation looks good.


146-176: ReactionSummary conversion looks correct.

Counts widened safely; keys normalized to hex.


232-239: FRB chrono support confirmed: Cargo.toml enables flutter_rust_bridge with the chrono feature and the generated Dart bindings include DateTime encode/decode methods for Chrono_Utc. No changes needed.

rust/src/api/relays.rs (3)

7-13: Relay DTO for FRB is clean and minimal — LGTM.

Fields and #[frb(non_opaque)] are appropriate for Flutter interop.


15-23: Correct conversion from WhitenoiseRelay — LGTM.

Direct field mapping is accurate.


43-45: Good: accept both hex and npub via PublicKey::parse.

Aligns with the repo’s preference to support multiple encodings.

rust/src/api/accounts.rs (4)

7-14: Account DTO matches FRB needs — LGTM.

Non-opaque + chrono fields look consistent with other API types.


16-24: From mapping is correct — LGTM.

Hex-encoding pubkey matches the rest of the API.


180-192: Ensure follow/unfollow are idempotent and resilient.

Double-follow/unfollow should be safe and return OK; network/db races should not error.

If not already guaranteed by whitenoise, consider guarding in the service layer.

Also applies to: 195-207


98-107: Verify FRB support for ImageType params and enforce HTTPS on server_url

  • Confirm flutter_rust_bridge can marshal ImageType directly in upload_account_profile_picture; if not, retain String + ImageType::try_from(...) to avoid breaking the FFI contract.
  • After let server = Url::parse(&server_url)?;, add if server.scheme() != "https" and return an appropriate ApiError variant to reject non-HTTPS URLs.
lib/domain/models/message_model.dart (1)

261-261: Switch to Object.hash is correct and consistent with ==.

lib/domain/models/dm_chat_data.dart (1)

39-39: Good move to Object.hash; null-safe and faster than manual XOR.

lib/domain/models/user_model.dart (2)

42-43: hashCode updated to Object.hash: LGTM.


19-27: Verify userMetadata return type and import alignment
Please confirm that rust_users.userMetadata returns a non-nullable Future<FlutterMetadata> (not Future<FlutterMetadata?>) and that the import ... as rust_users alias in group_provider.dart matches the binding file.

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

7-7: Active pubkey listener: good integration and safe post-frame clearing.

Also applies to: 23-34

lib/domain/models/contact_model.dart (1)

107-108: hashCode via Object.hash: good.

lib/ui/settings/general_settings_screen.dart (20)

6-6: Import added for package information retrieval.

The addition of package_info_plus import supports the new version display functionality at the bottom of the settings screen.


9-9: Import added for new pubkey-centric provider.

The addition of activePubkeyProvider import aligns with the migration from account-centered to pubkey-centered identity management.


11-11: Import added for follows management.

The addition of followsProvider import supports the new follow-based data management replacing the previous contacts system.


37-38: Type migration from AccountData to Account.

The state variables have been correctly updated to use the new Account type instead of AccountData, aligning with the bridge API changes.


40-40: Provider subscription type updated for new state.

The subscription type correctly changes from ProfileState to ActiveAccountState to match the new pubkey-centric architecture.


41-41: State variable added for package information.

The PackageInfo variable supports the version display feature at the bottom of the settings screen.


47-47: Package info loading added to initialization.

The call to _loadPackageInfo() properly initializes the version information display.


48-50: Provider subscription updated for new architecture.

The subscription correctly changes to activeAccountProvider to align with the new pubkey-centric state management.


61-61: Subscription disposal properly updated.

The disposal of the updated subscription variable maintains proper resource management.


68-68: Provider usage updated for pubkey-centric approach.

The code correctly reads from activePubkeyProvider instead of account-based providers to get the active pubkey.


87-87: Type update for currentAccount variable.

The local variable type correctly changes from AccountData to Account to match the new API.


97-97: Active pubkey management updated.

The code correctly uses activePubkeyProvider.notifier.setActivePubkey() to manage the active pubkey state, replacing the previous account-based approach.

Also applies to: 103-103


118-128: Package info loading implementation.

The implementation properly loads package information using PackageInfo.fromPlatform() with appropriate error handling. The silent error handling is appropriate since version information is non-critical.


130-130: Method signatures updated for new Account type.

Both _switchAccount and _accountToContactModel methods have been correctly updated to accept Account parameters instead of AccountData.

Also applies to: 147-147


132-134: Account switching logic updated for new architecture.

The account switching logic correctly:

  1. Updates the active pubkey using activePubkeyProvider.notifier.setActivePubkey()
  2. Loads follows using followsProvider.notifier.loadFollows()
  3. Maintains the existing groups loading logic

This properly integrates with the new pubkey-centric and follows-based architecture.


171-190: Account resolution logic with proper key format handling.

The account resolution correctly handles the key format differences:

  • selectedProfile.publicKey from metadata cache (npub format)
  • account.pubkey from getAccounts (hex format)

The implementation properly converts npub to hex using hexPubkeyFromNpub() for matching, with fallback to direct matching. This aligns with the retrieved learning about key format normalization requirements.


176-176: Variable type updated for account selection.

The selectedAccount variable type correctly changes from AccountData to Account to match the new API.


421-434: Version information display implementation.

The version display implementation is well-structured:

  • Conditionally renders only when package info is available
  • Uses appropriate styling with muted foreground color
  • Displays both version and build number
  • Positioned appropriately at the bottom with proper spacing

133-133: No issues found with loadFollows()
The followsProvider in lib/config/providers/follows_provider.dart declares and implements a Future<void> loadFollows() method (starting at line 72), so the call in your widget is valid.


68-68: activePubkeyProvider and its notifier methods are implemented in lib/config/providers/active_pubkey_provider.dart.

Comment on lines +194 to +201
# Build unversioned android release
android-build:
@echo "🔨 Building unversioned android release..."
@echo "✔︎ Running a precommit check..."
just precommit
@echo "🎁 Building unversioned android release..."
./scripts/build.sh --full --versioned
@echo "🎉 Unversioned android release built successfully!"
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

android-build is mislabeled: passes --versioned and likely builds more than Android

The recipe says "unversioned android release" but calls --versioned and (via the script) may target multiple platforms. Restrict to Android and drop versioning.

Apply:

 android-build:
     @echo "🔨 Building unversioned android release..."
     @echo "✔︎ Running a precommit check..."
-    just precommit
+    just precommit-check
     @echo "🎁 Building unversioned android release..."
-    ./scripts/build.sh --full --versioned
+    @test -x ./scripts/build.sh || { echo "❌ ./scripts/build.sh missing or not executable"; exit 1; }
+    @test -z "$$(git status --porcelain)" || { echo "❌ Uncommitted/untracked changes detected. Commit or stash before building."; exit 1; }
+    ./scripts/build.sh --full --os android

If --os android isn’t supported, switch to the equivalent flag (e.g., --platform android) or drop the selector and ensure the script only builds Android when no selector is given.

To verify the script’s flags and targets, run:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
test -f scripts/build.sh || { echo "scripts/build.sh not found"; exit 1; }
echo "== supported flags =="
rg -nP -- '--versioned|--full|--os|--platform' scripts/build.sh || true
echo -e "\n== detected flutter build targets =="
rg -nP 'flutter\s+build\s+(apk|appbundle|ios|ipa|macos|windows|linux|web)' scripts/build.sh -C2 || true

Length of output: 2566


Restrict android-build to unversioned Android only
Drop the --versioned flag and use the supported --android selector (script does not support --os/--platform):

-    ./scripts/build.sh --full --versioned
+    ./scripts/build.sh --full --android

Also verify or add a precommit-check task before renaming just precommit to just precommit-check.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Build unversioned android release
android-build:
@echo "🔨 Building unversioned android release..."
@echo "✔︎ Running a precommit check..."
just precommit
@echo "🎁 Building unversioned android release..."
./scripts/build.sh --full --versioned
@echo "🎉 Unversioned android release built successfully!"
# Build unversioned android release
android-build:
@echo "🔨 Building unversioned android release..."
@echo "✔︎ Running a precommit check..."
just precommit
@echo "🎁 Building unversioned android release..."
./scripts/build.sh --full --android
@echo "🎉 Unversioned android release built successfully!"
🤖 Prompt for AI Agents
In justfile around lines 194 to 201, the android-build recipe incorrectly passes
--versioned and should target unversioned Android only and must call the correct
precommit task: remove the --versioned flag and replace it with the supported
selector --android when invoking ./scripts/build.sh (i.e., ./scripts/build.sh
--full --android), rename the invoked precommit target to precommit-check (or
verify/add a precommit-check task in the justfile and update the call from just
precommit to just precommit-check), and keep the surrounding echo messages
consistent with “unversioned android release.”

Comment on lines +68 to 74
final activeAccountState = await ref.read(activeAccountProvider.future);
final activeAccount = activeAccountState.account;
if (activeAccount == null) {
_setGroupError(groupId, 'No active account found');
return;
}

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

Prefer activePubkeyProvider over activeAccountProvider.future, add explicit types.
Removes unnecessary async account fetches and follows the new pubkey-centric flow.

Apply (example for loadMessagesForGroup block at Lines 68-74); mirror in other blocks listed:

-      final activeAccountState = await ref.read(activeAccountProvider.future);
-      final activeAccount = activeAccountState.account;
-      if (activeAccount == null) {
+      final String? pubkey = ref.read(activePubkeyProvider);
+      if (pubkey == null || pubkey.isEmpty) {
         _setGroupError(groupId, 'No active account found');
         return;
       }
@@
-      final aggregatedMessages = await fetchAggregatedMessagesForGroup(
-        pubkey: activeAccount.pubkey,
+      final aggregatedMessages = await fetchAggregatedMessagesForGroup(
+        pubkey: pubkey,
         groupId: groupId,
       );
@@
-      final messages = await MessageConverter.fromChatMessageList(
+      final List<MessageModel> messages = await MessageConverter.fromChatMessageList(
         aggregatedMessages,
-        currentUserPublicKey: activeAccount.pubkey,
+        currentUserPublicKey: pubkey,
         groupId: groupId,
         ref: ref,
       );

Do likewise in sendMessage, checkForNewMessages, updateMessageReaction, sendReplyMessage, deleteMessage.

Also applies to: 147-152, 290-294, 504-507, 578-584, 683-689

🤖 Prompt for AI Agents
In lib/config/providers/chat_provider.dart around lines 68 to 74, replace the
awaited activeAccountProvider.future usage with a direct read of
activePubkeyProvider and add explicit local types: read final String?
activePubkey = ref.read(activePubkeyProvider); check for null and call
_setGroupError(groupId, 'No active account found') and return if null (instead
of fetching account.account), so we avoid unnecessary async account fetches;
apply the same change (use final String? activePubkey =
ref.read(activePubkeyProvider); explicit types; null check and early return) to
the other blocks listed at lines 147-152, 290-294, 504-507, 578-584, and
683-689, and update any downstream code in those blocks to use activePubkey
instead of activeAccount or activeAccount.account.

Comment on lines +784 to 788
Future<void> _updateGroupOrderForNewMessage(String groupId) async {
final now = DateTime.now();

ref.read(groupsProvider.notifier).updateGroupActivityTime(groupId, now);
await ref.read(groupsProvider.notifier).updateGroupActivityTime(groupId, now);
}
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

_updateGroupOrderForNewMessage is async: await all call sites.
Prevents unawaited future warnings and keeps ordering deterministic.

Run to find unawaited calls:

If found in async contexts (e.g., Lines 364, 655), add await.


🏁 Script executed:

#!/bin/bash
rg -n "_updateGroupOrderForNewMessage\(" -n -C2 --type dart

Length of output: 1518


Await _updateGroupOrderForNewMessage at all call sites

  • In lib/config/providers/chat_provider.dart, change line 364 and line 655 from
    _updateGroupOrderForNewMessage(groupId);
    to
    await _updateGroupOrderForNewMessage(groupId);
🤖 Prompt for AI Agents
In lib/config/providers/chat_provider.dart around lines 784-788 the method
_updateGroupOrderForNewMessage is async (returns Future<void>); update its two
call sites at lines ~364 and ~655 to await the call (change
`_updateGroupOrderForNewMessage(groupId);` to `await
_updateGroupOrderForNewMessage(groupId);`) so the caller waits for the update to
complete; ensure the enclosing functions are async (add async if missing) and
propagate or handle any errors as needed.

// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:whitenoise/src/rust/api/utils.dart';
import 'package:whitenoise/src/rust/api/metadata.dart' show FlutterMetadata;
import 'package:whitenoise/src/rust/api/users.dart' show User;
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

Avoid User type collision: alias FRB User import.
Prevents confusion with domain User.

-import 'package:whitenoise/src/rust/api/users.dart' show User;
+import 'package:whitenoise/src/rust/api/users.dart' as frb show User;
@@
-  factory ContactModel.fromUser({
-    required User user,
+  factory ContactModel.fromUser({
+    required frb.User user,

Also applies to: 48-50

🤖 Prompt for AI Agents
In lib/domain/models/contact_model.dart around lines 3 and 48-50, the imported
User from package:whitenoise/src/rust/api/users.dart conflicts with the domain
User type; rename/alias that import (e.g., import ... show User as FrbUser) and
update all usages in this file to use the alias (FrbUser) to avoid type
collision and confusion with the domain User.

Comment on lines +60 to +67
displayName: displayName.isNotEmpty ? displayName : 'Unknown User',
publicKey: user.pubkey,
imagePath: picture,
about: about,
website: website,
nip05: nip05,
lud16: lud16,
);
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

Key normalization: ensure consistent hex vs npub.
Use a single canonical format when storing/comparing publicKey.

If a helper exists (e.g., normalizePubkey), apply:

-      publicKey: user.pubkey,
+      publicKey: normalizePubkey(user.pubkey),

🏁 Script executed:

#!/bin/bash
# Find available helpers to normalize keys
rg -nP "(npub|bech32|hex).*pubkey|pubkey.*(npub|hex)|toHex|fromNpub" --type dart

Length of output: 2128


🏁 Script executed:

#!/bin/bash
# Inspect helper definitions for pubkey conversion
sed -n '1,200p' lib/config/providers/nostr_keys_provider.dart
# Inspect normalization logic in auth_provider
sed -n '280,340p' lib/config/providers/auth_provider.dart

Length of output: 5844


🏁 Script executed:

#!/bin/bash
# Inspect string extensions for pubkey conversion helpers
sed -n '1,200p' lib/utils/string_extensions.dart

Length of output: 1237


Normalize publicKey to hex before storing in ContactModel
ContactModel currently uses publicKey: user.pubkey, which may be in hex or npub form. There’s no built-in normalizePubkey helper—add one and use an async factory:

/// utils/nostr_keys.dart
Future<String> normalizeHexPubkey(String key) =>
  key.startsWith('npub')
    ? hexPubkeyFromNpub(npub: key)
    : Future.value(key.toLowerCase());

/// lib/domain/models/contact_model.dart
extension ContactModelFactory on ContactModel {
  static Future<ContactModel> fromUser({
    required User user,
    required String displayName,
    /* …other fields… */
  }) async {
    final hex = await normalizeHexPubkey(user.pubkey);
    return ContactModel(
      displayName: displayName.isNotEmpty ? displayName : 'Unknown User',
      publicKey: hex,
      /* … */
    );
  }
}

Replace all direct publicKey: user.pubkey with calls to this factory (and update callers to await).

🤖 Prompt for AI Agents
In lib/domain/models/contact_model.dart around lines 60-67, the code assigns
publicKey: user.pubkey which may be npub or hex; add a helper normalizeHexPubkey
(e.g., in utils/nostr_keys.dart) that converts npub→hex and lowercases hex
values, then add an async factory (extension ContactModelFactory.fromUser) that
calls this normalizer and returns a ContactModel with publicKey set to the
normalized hex; replace all direct uses of ContactModel(...) with calls to await
ContactModelFactory.fromUser(...) and update callers to await the factory.

@@ -1,5 +1,5 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:whitenoise/src/rust/api/utils.dart';
import 'package:whitenoise/src/rust/api/metadata.dart' show FlutterMetadata;
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

Importing FlutterMetadata: verify downstream call sites.
This file now exposes FlutterMetadata on the public surface; ensure all factories/callers updated.

Run to find callers still passing MetadataData:


🏁 Script executed:

#!/bin/bash
rg -nP "User\.fromMetadata\s*\(" -C2 --type dart

Length of output: 1274


Update downstream call sites to use FlutterMetadata
Callers in lib/config/providers/group_provider.dart at lines 324 and 405 still invoke User.fromMetadata(metadata, pubkeyString). Ensure the metadata argument is the new FlutterMetadata (not the old MetadataData) and update the import/factory signatures accordingly.

🤖 Prompt for AI Agents
In lib/domain/models/user_model.dart around line 2, the file still imports/uses
the old metadata type; update the import to bring in FlutterMetadata (import
'package:whitenoise/src/rust/api/metadata.dart' show FlutterMetadata) and change
the User.fromMetadata factory signature to accept FlutterMetadata instead of the
old MetadataData; then update downstream call sites in
lib/config/providers/group_provider.dart (around lines 324 and 405) to pass the
FlutterMetadata instance (adjust their imports if they currently import the old
type) so function signatures and call sites match the new FlutterMetadata type.

# nostr-mls = {version = "0.43.0", path = "../../rust-nostr/mls/nostr-mls"}
# nostr-sdk = {version = "0.43.0", path = "../../rust-nostr/crates/nostr-sdk"}

thiserror = "2.0.12"
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

Check MSRV compatibility for thiserror 2.x across CI/toolchains.

thiserror 2.x bumped MSRV; ensure your CI/toolchain and consumers build with a compatible Rust version.


🏁 Script executed:

#!/bin/bash
# Surface configured toolchains in-repo
fd -a 'rust-toolchain*' | xargs -I{} sh -c 'echo "--- {} ---"; cat "{}"'
fd -a '.github/workflows/*.yml' | xargs rg -nP 'toolchain|rust-version|actions-rust' -C2 || true

Length of output: 19894


Add rust-toolchain.toml and pin Rust MSRV in CI
No Rust toolchain is pinned in the repo or CI; require Rust ≥1.61 (MSRV for thiserror 2.x) by adding a rust-toolchain.toml and updating your workflows to test against that version (lib.rs)

🤖 Prompt for AI Agents
rust/Cargo.toml around line 24: the crate thiserror requires Rust ≥1.61 but the
repo and CI do not pin a toolchain; add a rust-toolchain.toml at the repo root
that pins the MSRV (e.g. [toolchain] channel = "1.61.0" or "1.61"), and update
CI workflows (e.g. .github/workflows/*) to test using that pinned version (or
add it to the actions-rs/setup-rust or matrix) so builds run with the required
Rust toolchain.

Comment on lines +144 to +158
pub async fn remove_account_relay(
pubkey: String,
url: String,
relay_type: RelayType,
) -> Result<(), ApiError> {
let whitenoise = Whitenoise::get_instance()?;
let pubkey = PublicKey::parse(&pubkey)?;
let account = whitenoise.find_account_by_pubkey(&pubkey).await?;
let relay_url = RelayUrl::parse(&url)?;
let relay = whitenoise.find_or_create_relay_by_url(&relay_url).await?;
account
.remove_relay(&relay, relay_type, whitenoise)
.await
.map_err(ApiError::from)
}
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

Avoid creating Relay records when removing.

Using find_or_create_relay_by_url during removal can create orphan relay rows if the URL didn’t exist, polluting storage.

  • Prefer a non-creating lookup (e.g., find_relay_by_url) and no-op if absent.
  • If the library lacks such method, add a removal API that accepts URL without creation.

Run to check for a non-creating lookup you could use:


🏁 Script executed:

#!/bin/bash
# Look for candidate APIs to avoid creation on removal.
rg -nP 'find_.*relay.*by_url|get_.*relay.*by_url|remove_.*relay.*by_url' -C2 --type=rust

Length of output: 888


Avoid creating Relay records when removing
In remove_account_relay (rust/src/api/accounts.rs:144–158), calling find_or_create_relay_by_url will insert a Relay when it didn’t exist, leading to orphan rows. Add and use a non-creating lookup (e.g. find_relay_by_url(&RelayUrl) -> Option<Relay>) on Whitenoise and no-op if it returns None instead of creating then removing.

🤖 Prompt for AI Agents
In rust/src/api/accounts.rs around lines 144–158, the current implementation
uses find_or_create_relay_by_url which will insert a Relay record when removing
and can create orphan rows; change this to call a non-creating lookup (e.g.
Whitenoise::find_relay_by_url(&relay_url) -> Option<Relay>), and if it returns
None simply return Ok(()) (no-op) instead of creating then removing; adjust
error handling to map existing parse/lookup failures to ApiError as before, and
only proceed to account.remove_relay when find_relay_by_url returns Some(relay).

Comment on lines 232 to +239
#[frb]
pub async fn send_message_to_group(
pubkey: &PublicKey,
group_id: whitenoise::GroupId,
pubkey: String,
group_id: String,
message: String,
kind: u16,
tags: Option<Vec<Tag>>,
) -> Result<MessageWithTokensData, WhitenoiseError> {
) -> Result<MessageWithTokens, ApiError> {
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

Avoid exposing nostr_sdk::Tag at the FRB boundary; accept Vec<Vec> and convert internally.

Passing external types through FRB is brittle. Take tag fields and build Tags inside Rust.

- use crate::api::{error::ApiError, utils::group_id_from_string};
+ use crate::api::{error::ApiError, utils::{group_id_from_string, tag_from_vec}};
@@
 pub async fn send_message_to_group(
     pubkey: String,
     group_id: String,
     message: String,
     kind: u16,
-    tags: Option<Vec<Tag>>,
+    tags: Option<Vec<Vec<String>>>,
 ) -> Result<MessageWithTokens, ApiError> {
@@
-    let message_with_tokens = whitenoise
-        .send_message_to_group(&account, &group_id, message, kind, tags)
+    let tags = tags
+        .map(|vv| vv.into_iter().map(tag_from_vec).collect::<Result<Vec<_>, ApiError>>())
+        .transpose()?;
+    let message_with_tokens = whitenoise
+        .send_message_to_group(&account, &group_id, message, kind, tags)
         .await?;

Implement in utils.rs:

pub fn tag_from_vec(fields: Vec<String>) -> Result<nostr_sdk::Tag, ApiError> {
    // Reconstruct Tag from fields (match kind, etc.). Return ApiError on invalid input.
    nostr_sdk::Tag::parse(fields).map_err(|e| ApiError::Validation { message: e.to_string() })
}

To confirm no other FRB-exposed APIs still use Tag:

Also applies to: 245-247, 1-1


🏁 Script executed:

#!/bin/bash
rg -nP '\bOption<\s*Vec\s*<\s*Tag\s*>\s*>' -C2 --type=rust

Length of output: 1534


Avoid exposing nostr_sdk::Tag at the FRB boundary

  • Change send_message_to_group to accept Option<Vec<Vec<String>>> and convert to Vec<Tag> internally via a tag_from_vec helper.
  • Implement in utils.rs:
    pub fn tag_from_vec(fields: Vec<String>) -> Result<nostr_sdk::Tag, ApiError> {
        nostr_sdk::Tag::parse(fields)
            .map_err(|e| ApiError::Validation { message: e.to_string() })
    }
  • Update your flutter_rust_bridge configuration and regenerate rust/src/frb_generated.rs so that no Option<Vec<Tag>> remains in the generated code.

Relevant diff in rust/src/api/messages.rs:

- use crate::api::{error::ApiError, utils::group_id_from_string};
+ use crate::api::{error::ApiError, utils::{group_id_from_string, tag_from_vec}};
@@
 pub async fn send_message_to_group(
     pubkey: String,
     group_id: String,
     message: String,
     kind: u16,
-    tags: Option<Vec<Tag>>,
+    tags: Option<Vec<Vec<String>>>,
 ) -> Result<MessageWithTokens, ApiError> {
@@
-    let message_with_tokens = whitenoise
-        .send_message_to_group(&account, &group_id, message, kind, tags)
+    let tags = tags
+        .map(|vv| vv.into_iter().map(tag_from_vec).collect::<Result<Vec<_>, ApiError>>())
+        .transpose()?;
+    let message_with_tokens = whitenoise
+        .send_message_to_group(&account, &group_id, message, kind, tags)
         .await?;
🤖 Prompt for AI Agents
In rust/src/api/messages.rs around lines 232 to 239, the FRB function currently
exposes nostr_sdk::Tag in its signature; change the signature to accept
Option<Vec<Vec<String>>> for tags and inside the function convert each
Vec<String> into a nostr_sdk::Tag using a helper (tag_from_vec) that maps parse
errors into ApiError::Validation, returning early on error; implement the helper
in utils.rs as described (taking Vec<String> and returning
Result<nostr_sdk::Tag, ApiError>) and use it to build a Vec<Tag> before
proceeding; finally update the flutter_rust_bridge config and regenerate
rust/src/frb_generated.rs so the generated boundary no longer contains
Option<Vec<Tag>>.

@coderabbitai coderabbitai bot mentioned this pull request Aug 29, 2025
11 tasks
@coderabbitai coderabbitai bot mentioned this pull request Oct 17, 2025
@coderabbitai coderabbitai bot mentioned this pull request Nov 3, 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.

6 participants