Skip to content

Conversation

@erskingardner
Copy link
Member

@erskingardner erskingardner commented Sep 13, 2025

Summary by CodeRabbit

  • New Features

    • Added Key Package Management with publish, inspect, and delete-all actions.
    • Fallback avatar now shows initials when no image is available.
    • Relay UI reorganized into streamlined relay sections with improved pull-to-refresh.
  • Bug Fixes

    • More reliable follow lookups and improved group member/admin loading with safer comparisons.
  • Refactor

    • Improved image loading (better caching, SVG and local-file handling) and refreshed settings layouts for smoother rendering and responsiveness.
    • Relay list item visuals and touch targets refined.
  • Tests

    • Updated UI tests to match new widgets and interactions.
  • Chores

    • Updated native dependency revision.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 13, 2025

Walkthrough

This PR normalizes pubkey comparisons to hex, makes group member/admin flows cache-first and domain_user-based, turns WnImage stateful and adds FallbackAvatar, restructures multiple settings and network UI components (relay sections, pull-to-refresh), updates tests, and bumps a Rust crate revision.

Changes

Cohort / File(s) Summary
Follows hex-normalization
lib/config/providers/follows_provider.dart
Compare pubkeys via hex-normalized values using PubkeyFormatter; add collection import; use firstWhereOrNull with null-safe hex handling.
Groups: domain_user + cache-first
lib/config/providers/group_provider.dart, lib/config/states/group_state.dart
Switch members/admins to domain_user.User; resolve members from follows cache (findFollowByPubkey) before API; convert metadata via domain_user.User.fromMetadata; add fallbacks and update method return types/state maps.
Core image/avatar refactor
lib/ui/core/ui/wn_image.dart, lib/ui/core/ui/wn_avatar.dart
WnImageStatefulWidget with computed props, async local-file detection, DPR-aware caching; unified SVG/raster paths; WnAvatar layout adjusted and new public FallbackAvatar widget added.
Network relays UI overhaul
lib/ui/settings/network/network_screen.dart, lib/ui/settings/network/widgets/relay_section.dart, lib/ui/settings/network/widgets/relay_tile.dart, deleted: lib/ui/settings/network/widgets/relay_expansion_tile.dart
Replace RelayExpansionTile with RelaySection; introduce _OptimizedScrollView and _RefreshIndicator; refine RelayTile layout and repaint boundaries; delete old expansion tile file.
General & Developer settings UI
lib/ui/settings/general_settings_screen.dart, lib/ui/settings/developer/developer_settings_screen.dart
Add TTL account cache and guarded loaders; invalidate on account change; reorganize UI into _SettingsSection, add Key Package Management UI, new _KeyPackageItem, and add RepaintBoundary/perf tweaks.
Tests
test/ui/contact_list/widgets/user_profile_test.dart, test/ui/settings/profile/share_profile_qr_scan_screen_test.dart
Replace SvgPicture usage with WnImage in a test; replace several pumpAndSettle() with pump() calls.
Models: displayName fallback
lib/domain/models/contact_model.dart, lib/domain/models/user_model.dart
Add sanitized name fallback and compute finalDisplayName with ordered fallbacks (displayNamename → default).
Utils comment
lib/utils/message_converter.dart
Minor comment tweak; no behavioral change.
Rust dependency bump
rust/Cargo.toml
Update whitenoise crate revision SHA.
Rust API user lookup
rust/src/api/users.rs
Replace find_user_by_pubkey calls with find_or_create_user_by_pubkey in several endpoints.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor UI
  participant GroupsNotifier
  participant FollowsProvider
  participant API as Metadata API
  note over UI,GroupsNotifier: Load group members/admins
  UI->>GroupsNotifier: loadGroupMembers(groupId)
  loop for each member pubkey
    GroupsNotifier->>FollowsProvider: findFollowByPubkey(pubkey)
    alt Found in cache
      FollowsProvider-->>GroupsNotifier: Follow metadata
      GroupsNotifier->>GroupsNotifier: domain_user.User.fromMetadata(...)
    else Not found
      GroupsNotifier->>API: fetchMetadata(pubkey)
      alt Success
        API-->>GroupsNotifier: metadata
        GroupsNotifier->>GroupsNotifier: domain_user.User.fromMetadata(...)
      else Failure
        GroupsNotifier->>GroupsNotifier: create minimal domain_user.User(pubkey)
      end
    end
  end
  GroupsNotifier-->>UI: List<domain_user.User>
Loading
sequenceDiagram
  autonumber
  actor WidgetTree
  participant WnImage
  participant State as _WnImageState
  note over WnImage,State: Stateful rendering decision flow
  WidgetTree->>WnImage: build(src, size, ...)
  WnImage->>State: createState/initState
  State->>State: _computeImageProperties(src)
  alt src changed later
    WidgetTree->>WnImage: didUpdateWidget(new src)
    State->>State: recompute + async local-file check
  end
  alt Network SVG
    State-->>WidgetTree: SvgPicture.network(...)
  else Network raster
    State-->>WidgetTree: CachedNetworkImage(... DPR-based mem cache ...)
  else Local file / Asset
    State-->>WidgetTree: SvgPicture.file/Image.file or SvgPicture.asset/Image.asset
  end
Loading
sequenceDiagram
  autonumber
  actor User
  participant NetworkScreen
  participant OptimizedScroll as _OptimizedScrollView
  participant RelaySection
  participant Notifier as RelaysNotifier
  note over User,NetworkScreen: Pull-to-refresh relays
  User->>OptimizedScroll: drag down
  OptimizedScroll->>NetworkScreen: throttled onPull(progress)
  alt threshold reached
    NetworkScreen->>NetworkScreen: _refreshData()
    NetworkScreen->>Notifier: refreshStatuses() then fetchAll()
    Notifier-->>NetworkScreen: updated RelayState
    NetworkScreen-->>RelaySection: relayState updated
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • untreu2
  • josefinalliende
  • Quwaysim

Poem

Hop, hop — I chased a hex and cached a friend,
Avatars now bloom when images mend.
Relays line up tidy, refresh with a breeze,
Keys sorted in lists, no scramble, just ease.
Carrots and commits — a rabbit's small cheer! 🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Perf fixes" aligns with the dominant theme of the changeset, which contains many performance-focused edits across the UI and internals (RepaintBoundary additions, caching and TTLs for accounts, WnImage lifecycle and caching, optimized scroll/pull-to-refresh, etc.), so it accurately signals the PR intent while remaining concise. It is somewhat generic but not misleading given the actual changes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch perf-fixes

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@erskingardner
Copy link
Member Author

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 13, 2025

✅ Actions performed

Full review triggered.

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 (9)
lib/utils/message_converter.dart (1)

529-541: Normalize pubkeys before fetching to avoid cache misses and “isMe” mismatches

If upstream can emit npub or hex, compare and fetch using a normalized form. Also avoid scheduling with microtask unless you’re inside a build; here it’s safe to call directly.

Apply this diff to normalize and use the normalized key consistently:

-      // If not found in contacts, fetch directly from API with build-safe scheduling
-      return await Future.microtask(() async {
-        final userProfileNotifier = ref.read(userProfileDataProvider.notifier);
-        final contactModel = await userProfileNotifier.getUserProfileData(pubkey);
+      // If not found in contacts, fetch from profile data provider
+      final userProfileNotifier = ref.read(userProfileDataProvider.notifier);
+      final normalized = pubkey; // replace with extension when available
+      final contactModel = await userProfileNotifier.getUserProfileData(normalized);
 
-        return User(
-          id: pubkey,
-          displayName: contactModel.displayName,
-          nip05: contactModel.nip05 ?? '',
-          publicKey: pubkey,
-          imagePath: contactModel.imagePath,
-        );
-      });
+      return User(
+        id: normalized,
+        displayName: contactModel.displayName,
+        nip05: contactModel.nip05 ?? '',
+        publicKey: normalized,
+        imagePath: contactModel.imagePath,
+      );

Outside this hunk, import and use your pubkey normalization extension (see next comment).

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

209-215: Logic bug: multiple accounts threshold

hasMultipleAccounts should be > 1, not > 2. As written, two accounts won’t trigger the multiple-account flow.

-    final hasMultipleAccounts = _accounts.length > 2;
+    final hasMultipleAccounts = _accounts.length > 1;
lib/ui/settings/developer/developer_settings_screen.dart (3)

114-123: Potential setState after dispose in async fetch.
setState is called after an await without a mounted check; can throw if the screen is popped mid-request.

   try {
     final keyPackages = await accounts_api.accountKeyPackages(
       accountPubkey: activePubkey,
     );
+    if (!mounted) return;
     setState(() {
       _keyPackages = keyPackages;
       _showKeyPackages = true;
     });

84-96: Same mounted guard needed after delete-all before setState.
Avoids setState on a disposed State when user leaves during the call.

   try {
     final deletedCount = await accounts_api.deleteAccountKeyPackages(
       accountPubkey: activePubkey,
     );
+    if (!mounted) return;
     ref.showSuccessToast('Deleted $deletedCount key packages successfully');
     // Clear the displayed key packages if they were being shown
     if (_showKeyPackages) {
       setState(() {
         _keyPackages = [];
         _showKeyPackages = false;
       });
     }

168-201: Destructive dialogs should not be barrier-dismissible.
Accidental outside taps will dismiss the confirmation; set barrierDismissible: false.

-    final confirmed = await showDialog<bool>(
+    final confirmed = await showDialog<bool>(
       context: context,
       barrierColor: Colors.transparent,
+      barrierDismissible: false,
       builder:
           (dialogContext) => WnDialog(

Apply the same to the “Delete All Key Packages” dialog above.

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

263-265: Missing await when extracting ApiError message.

e.messageText() returns a Future; logging will print a Future instead of the actual message.

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

919-927: Filter invalid hex conversions before adding members.

Empty strings from toHex() ?? '' will be sent to the API, likely causing failures or silent no-ops.

-      final usersPubkeyHex =
-          membersNpubs.map((userNpub) {
-            return PubkeyFormatter(pubkey: userNpub).toHex() ?? '';
-          }).toList();
+      final usersPubkeyHex = membersNpubs
+          .map((npub) => PubkeyFormatter(pubkey: npub).toHex())
+          .whereType<String>()
+          .toList();
+      if (usersPubkeyHex.isEmpty) {
+        state = state.copyWith(error: 'No valid member pubkeys to add.');
+        return;
+      }

973-977: Apply the same conversion filtering when removing members.

Mirror the add flow to avoid passing invalid pubkeys.

-      final usersPubkeyHex =
-          membersNpubs.map((userNpub) {
-            return PubkeyFormatter(pubkey: userNpub).toHex() ?? '';
-          }).toList();
+      final usersPubkeyHex = membersNpubs
+          .map((npub) => PubkeyFormatter(pubkey: npub).toHex())
+          .whereType<String>()
+          .toList();
+      if (usersPubkeyHex.isEmpty) {
+        state = state.copyWith(error: 'No valid member pubkeys to remove.');
+        return;
+      }

1063-1087: Handle null conversions in DM “other member” selection.

If toHex() returns null for the active or member keys, current logic can falsely include/exclude members.

   domain_user.User? getOtherGroupMember(String? groupId) {
     if (groupId == null) return null;
     final activePubkey = ref.read(activePubkeyProvider);
     if (activePubkey == null || activePubkey.isEmpty) return null;
     final members = getGroupMembers(groupId);
     if (members == null || members.isEmpty) return null;

-    final hexActivePubkey = PubkeyFormatter(pubkey: activePubkey).toHex();
-    final otherMembers =
-        members
-            .where(
-              (member) => PubkeyFormatter(pubkey: member.publicKey).toHex() != hexActivePubkey,
-            )
-            .toList();
+    final hexActivePubkey = PubkeyFormatter(pubkey: activePubkey).toHex();
+    if (hexActivePubkey == null) {
+      _logger.warning('GroupsProvider: Active pubkey failed to normalize for group $groupId.');
+      return null;
+    }
+    final otherMembers = members
+        .where((member) {
+          final mHex = PubkeyFormatter(pubkey: member.publicKey).toHex();
+          return mHex != null && mHex != hexActivePubkey;
+        })
+        .toList();
🧹 Nitpick comments (21)
rust/Cargo.toml (1)

26-26: Avoid mixing version with a git dependency; remove version to reduce confusion.

For git-sourced dependencies, Cargo ignores version. Keeping it can mislead readers. Prefer only git + rev (or tag if available).

-whitenoise = { version = "0.1.0", git = "https://github.com/parres-hq/whitenoise", rev = "0590dc1f1e3a7d288413781390ad789f93c54f24" }
+whitenoise = { git = "https://github.com/parres-hq/whitenoise", rev = "0590dc1f1e3a7d288413781390ad789f93c54f24" }
test/ui/settings/profile/share_profile_qr_scan_screen_test.dart (3)

144-158: Back button tap: prefer deterministic settle and assert effect

Using pump() right after tap may be flaky; also the test doesn’t assert that a pop actually occurred. Consider settling frames or pumping with a small duration and asserting navigation state.

Apply this diff to make the interaction deterministic and verify the screen was popped:

         await tester.pumpWidget(
           const MaterialApp(
-            home: ShareProfileQrScanScreen(),
+            home: Navigator(
+              onGenerateRoute: (_) => MaterialPageRoute(
+                builder: (_) => const ShareProfileQrScanScreen(),
+              ),
+            ),
           ),
         );
@@
         await tester.tap(backButton);
-        await tester.pump();
+        await tester.pumpAndSettle();
+        expect(find.byType(ShareProfileQrScanScreen), findsNothing);

189-202: Lifecycle observer test: settle frames to avoid flakiness

A single pump() may not process queued microtasks/animations. Use pumpAndSettle() (or pump with a short duration) to stabilize.

-        await tester.pump();
+        await tester.pumpAndSettle();

312-319: Accessibility tap: settle after interaction

Same as above—prefer pumpAndSettle() for deterministic behavior.

-        await tester.pump();
+        await tester.pumpAndSettle();
lib/ui/settings/network/widgets/relay_tile.dart (1)

119-131: Truncated URL: add semantics label for screen readers

Ellipsized text may be ambiguous to AT. Provide a semanticsLabel with the full sanitized URL.

-              child: RepaintBoundary(
-                child: Text(
-                  widget.relayInfo.url.sanitizedUrl,
+              child: RepaintBoundary(
+                child: Text(
+                  widget.relayInfo.url.sanitizedUrl,
                   style: TextStyle(
                     color: context.colors.primary,
                     fontWeight: FontWeight.w600,
                     fontSize: 12.sp,
                   ),
                   overflow: TextOverflow.ellipsis,
+                  semanticsLabel: widget.relayInfo.url.sanitizedUrl,
                 ),
               ),
lib/ui/core/ui/wn_image.dart (1)

158-170: Double-apply color on CachedNetworkImage

You already tint in imageBuilder via Image(... color: ...). Passing color/colorBlendMode to CachedNetworkImage may be unsupported by some versions and is redundant.

-          color: widget.color,
-          colorBlendMode: widget.color == null ? null : BlendMode.srcIn,
test/ui/contact_list/widgets/user_profile_test.dart (1)

103-110: Make the copy-button finder robust (avoid byType ambiguity).
Relying on find.byType(IconButton) can break if other IconButtons are added. Prefer a Key-based finder.

Apply this diff to the test, assuming the production IconButton gets a key:

-      final copyButton = find.byType(IconButton);
+      final copyButton = find.byKey(const Key('copyPubkeyButton'));

Outside this file (in lib/ui/contact_list/widgets/user_profile.dart), add a key to the copy button:

IconButton(
  key: const Key('copyPubkeyButton'),
  icon: WnImage(...),
  onPressed: ...,
)
lib/ui/core/ui/wn_avatar.dart (2)

61-69: Don’t require a nullable argument.
displayName is String? but marked required. Make it optional for cleaner call sites.

-  const FallbackAvatar({
+  const FallbackAvatar({
     super.key,
-    required this.displayName,
+    this.displayName,
     required this.size,
   });

86-96: Minor: prefer ColoredBox for solid backgrounds.
Tiny perf/readability win inside the padding.

-    return Padding(
-      padding: EdgeInsets.all(size * 0.25),
-      child: Center(
-        child: WnImage(
+    return Padding(
+      padding: EdgeInsets.all(size * 0.25),
+      child: Center(
+        child: WnImage(
           AssetsPaths.icUser,
           width: size * 0.4,
           height: size * 0.4,
           color: context.colors.primary,
         ),
       ),
     );
lib/ui/settings/developer/developer_settings_screen.dart (2)

268-273: Use .h for vertical padding.
bottom: 24.w should be 24.h to scale with height.

-                  padding: EdgeInsets.only(
+                  padding: EdgeInsets.only(
                     left: 16.w,
                     right: 16.w,
-                    bottom: 24.w,
+                    bottom: 24.h,
                   ),

395-497: Good separation with _KeyPackageItem; a few polish suggestions.

  • Consider a semantic label for the delete icon for accessibility.
  • Courier may not exist on all platforms; prefer a bundled mono font (e.g., RobotoMono) or theme textStyle with fontFeatures/monospace.
lib/ui/settings/network/network_screen.dart (3)

151-156: Unify pull-to-refresh thresholds (px vs .h).
Indicator opacity uses 60.h, trigger uses literal 60.0. This can feel inconsistent across devices.

Introduce configurable thresholds from the parent:

-  const _OptimizedScrollView({
-    required this.children,
-    required this.onPullToRefresh,
-    required this.onPullStateChanged,
-  });
+  const _OptimizedScrollView({
+    required this.children,
+    required this.onPullToRefresh,
+    required this.onPullStateChanged,
+    required this.refreshTrigger,
+  });
   final void Function(bool, double) onPullStateChanged;
+  final double refreshTrigger; // pass 60.h from parent

-  if (_isPulling && _pullDistance >= 60.0) {
+  if (_isPulling && _pullDistance >= widget.refreshTrigger) {

And pass refreshTrigger: 60.h from the parent.

Also applies to: 355-357


305-314: Type onPullToRefresh to Future and mark unawaited call.
Prevents analyzer “unawaited_futures” noise and documents async semantics.

-  final VoidCallback onPullToRefresh;
+  final Future<void> Function() onPullToRefresh;
-          if (_isPulling && _pullDistance >= 60.0) {
-            widget.onPullToRefresh();
+          if (_isPulling && _pullDistance >= widget.refreshTrigger) {
+            // fire-and-forget by design
+            // ignore: discarded_futures
+            widget.onPullToRefresh();
           }

Alternatively, use unawaited(widget.onPullToRefresh()); if available.

Also applies to: 356-358


338-345: Also handle OverscrollNotification for robustness.
Some physics report overscroll via OverscrollNotification (not negative pixels). Consider handling it for consistent UX.

lib/ui/settings/network/widgets/relay_section.dart (2)

41-56: Avoid runtime type checks; prefer a common interface.
Downcasting to specific notifiers is brittle. Define an abstract base with addRelay/deleteRelay and type the field to that.

Example:

abstract class RelayCommands {
  Future<void> addRelay(String url);
  Future<void> deleteRelay(String url);
}

Have your notifiers implement it and change final RelayCommands relayNotifier;.


135-142: Surface error details to aid debugging.
Rendering a generic “Error Loading Relays” hides useful info. Consider showing a terse message or a retry.

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

116-123: Index follows by normalized hex for O(1) lookups.

toHex() is called per user per lookup. Keep a Map<String, User> (hex->User) in state or Notifier and build it once on load. This will also speed up isFollowing.

I can propose a small refactor in loadFollows() to populate an index and then replace this lookup with return _followsByHex[hexPubkey];.


126-130: Unify on single source of truth for following checks.

Leverage findFollowByPubkey to avoid duplicate conversion logic.

-  bool isFollowing(String pubkey) {
-    final hexPubkey = PubkeyFormatter(pubkey: pubkey).toHex();
-    if (hexPubkey == null) return false;
-    return state.follows.any((user) => PubkeyFormatter(pubkey: user.pubkey).toHex() == hexPubkey);
-  }
+  bool isFollowing(String pubkey) {
+    return findFollowByPubkey(pubkey) != null;
+  }
lib/config/providers/group_provider.dart (3)

404-433: Parity: leverage follows cache for admins too.

Admins path doesn’t attempt cache lookup, missing a cheap win.

-      for (final adminPubkey in adminPubkeys) {
+      for (final adminPubkey in adminPubkeys) {
         try {
           final npub = PubkeyFormatter(pubkey: adminPubkey).toNpub() ?? '';
-
-          try {
+          // Try follows cache first
+          final followsNotifier = ref.read(followsProvider.notifier);
+          final existingFollow = followsNotifier.findFollowByPubkey(adminPubkey);
+          if (existingFollow != null) {
+            _logger.info('Found admin $npub in follows cache');
+            final domainUser = domain_user.User.fromMetadata(existingFollow.metadata, npub);
+            admins.add(domainUser);
+            continue;
+          }
+          try {
             final metadata = await rust_users.userMetadata(pubkey: adminPubkey);
             final user = domain_user.User.fromMetadata(metadata, npub);
             admins.add(user);

610-619: Avoid repeated getGroupInformation calls; use cached type when available.

You already cache group types in state. Prefer that for display name branching to reduce RPCs.

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

231-240: Minor log clarity: log the actual combined admin keys.

Message prints adminPublicKeyHexs instead of the combined set with the creator.

-      _logger.info('  - Admin pubkeys: $adminPublicKeyHexs');
+      _logger.info('  - Admin pubkeys (combined): $combinedAdminKeys');
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6a387b9 and 1b1ff04.

⛔ Files ignored due to path filters (4)
  • 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
  • rust/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (15)
  • lib/config/providers/follows_provider.dart (1 hunks)
  • lib/config/providers/group_provider.dart (9 hunks)
  • lib/config/providers/metadata_cache_provider.dart (0 hunks)
  • lib/ui/core/ui/wn_avatar.dart (1 hunks)
  • lib/ui/core/ui/wn_image.dart (3 hunks)
  • lib/ui/settings/developer/developer_settings_screen.dart (4 hunks)
  • lib/ui/settings/general_settings_screen.dart (8 hunks)
  • lib/ui/settings/network/network_screen.dart (6 hunks)
  • lib/ui/settings/network/widgets/relay_expansion_tile.dart (0 hunks)
  • lib/ui/settings/network/widgets/relay_section.dart (1 hunks)
  • lib/ui/settings/network/widgets/relay_tile.dart (1 hunks)
  • lib/utils/message_converter.dart (3 hunks)
  • rust/Cargo.toml (1 hunks)
  • test/ui/contact_list/widgets/user_profile_test.dart (2 hunks)
  • test/ui/settings/profile/share_profile_qr_scan_screen_test.dart (3 hunks)
💤 Files with no reviewable changes (2)
  • lib/ui/settings/network/widgets/relay_expansion_tile.dart
  • lib/config/providers/metadata_cache_provider.dart
🧰 Additional context used
🧠 Learnings (11)
📓 Common learnings
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#594
File: lib/src/rust/api/utils.dart:12-16
Timestamp: 2025-09-07T02:15:31.931Z
Learning: The lingering PublicKey references in lib/config/providers/auth_provider.dart, lib/config/providers/group_provider.dart, and rust_builder/cargokit/build_tool/lib/src/options.dart identified during the utils.dart API cleanup were fixed in PR #597 in the whitenoise_flutter codebase.
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-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/follows_provider.dart
  • lib/config/providers/group_provider.dart
📚 Learning: 2025-09-07T02:15:31.931Z
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#594
File: lib/src/rust/api/utils.dart:12-16
Timestamp: 2025-09-07T02:15:31.931Z
Learning: The lingering PublicKey references in lib/config/providers/auth_provider.dart, lib/config/providers/group_provider.dart, and rust_builder/cargokit/build_tool/lib/src/options.dart identified during the utils.dart API cleanup were fixed in PR #597 in the whitenoise_flutter codebase.

Applied to files:

  • lib/config/providers/follows_provider.dart
  • lib/utils/message_converter.dart
  • lib/config/providers/group_provider.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/follows_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 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/ui/settings/network/network_screen.dart
  • lib/utils/message_converter.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/network/network_screen.dart
  • test/ui/contact_list/widgets/user_profile_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 **/*_test.dart : Use the standard widget testing for Flutter.

Applied to files:

  • test/ui/contact_list/widgets/user_profile_test.dart
📚 Learning: 2025-09-07T13:10:16.542Z
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#597
File: lib/config/providers/group_provider.dart:311-314
Timestamp: 2025-09-07T13:10:16.542Z
Learning: In the whitenoise_flutter codebase, the User class used in group_provider.dart (and similar contexts) is presentational only, not the actual user class from the Rust API. There are plans to remove this User class and replace it with UserProfileData, similar to the planned consolidation with ContactModel.

Applied to files:

  • test/ui/contact_list/widgets/user_profile_test.dart
  • lib/utils/message_converter.dart
  • lib/config/providers/group_provider.dart
📚 Learning: 2025-09-08T17:39:13.711Z
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#599
File: test/ui/contact_list/start_chat_bottom_sheet_test.dart:173-176
Timestamp: 2025-09-08T17:39:13.711Z
Learning: In the whitenoise Flutter app, the followProvider uses followsProvider behind the scenes, so mocking followsProvider with MockFollowsNotifier effectively controls what followProvider sees in tests.

Applied to files:

  • lib/utils/message_converter.dart
  • lib/config/providers/group_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/utils/message_converter.dart
  • lib/config/providers/group_provider.dart
📚 Learning: 2025-09-05T00:08:34.509Z
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.509Z
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/group_provider.dart
🔇 Additional comments (15)
rust/Cargo.toml (1)

26-26: Verify the pinned commit and lockfile to ensure reproducible builds.

Ran the verification script: Cargo.lock not found; git ls-remote returned no match for rev 0590dc1f1e3a7d288413781390ad789f93c54f24 — verification inconclusive.

  • Confirm rev 0590dc1f1e3a7d288413781390ad789f93c54f24 exists in https://github.com/parres-hq/whitenoise and matches the intended perf fix.
  • If the repository tracks Cargo.lock, update and commit Cargo.lock so the exact git rev is recorded.
  • If a release tag points to that commit, prefer pinning by tag for readability.

File: rust/Cargo.toml (line 26)

lib/utils/message_converter.dart (1)

3-3: Provider migration LGTM

Switching to user_profile_data_provider is the right direction and aligns with recent refactors.

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

55-58: Good: invalidate cache on active account updates

This ensures fresh data after profile changes.


115-129: Package info loader: solid guard/finally reset

The idempotent guard and finally-reset pattern looks good.

lib/ui/core/ui/wn_image.dart (2)

125-137: Device pixel ratio caching: good

Caching DPR avoids repeated lookups; no concerns.


240-248: Clip only when needed: LGTM

Conditional ClipRRect based on borderRadius keeps it lightweight.

test/ui/contact_list/widgets/user_profile_test.dart (1)

5-5: LGTM: updated import aligns with WnImage migration.
No issues; keeps the test type-safe for isA().

lib/ui/core/ui/wn_avatar.dart (2)

29-39: Border/layout refactor looks good.
Outer circular border + inner ClipOval is cleaner and avoids double clips.


44-53: Confirm WnImage sizing API usage (size vs width/height).
This uses size: size here, while FallbackAvatar uses width/height. Ensure WnImage still supports size after the refactor, or standardize on width/height for consistency.

If needed, switch to width/height:

-          child: WnImage(
-            imageUrl,
-            size: size,
+          child: WnImage(
+            imageUrl,
+            width: size,
+            height: size,
             fit: BoxFit.cover,
             fallbackWidget: ...
           ),
lib/ui/settings/developer/developer_settings_screen.dart (1)

331-341: Alpha adjustment helper: confirm availability across channels.
.withValues(alpha: 0.3) may rely on custom extensions or newer Flutter APIs. If custom, fine; if not, prefer withOpacity(0.3) for compatibility.

lib/ui/settings/network/network_screen.dart (1)

144-149: Header extraction + RepaintBoundary: good for rasterization.
Clean separation; no issues spotted.

lib/ui/settings/network/widgets/relay_section.dart (1)

144-159: ListView inside parent ListView: good use of NeverScrollable + shrinkWrap.
This avoids nested scrolling issues.

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

9-12: Imports look good; domain alias improves clarity.

No functional issues here. The follows_provider import enables the cache path below.


311-326: existingFollow.metadata is FlutterMetadata — type-safe; optional: guard empty npub

existingFollow.metadata is a FlutterMetadata (rust API User.metadata) and domain_user.User.fromMetadata accepts FlutterMetadata — no runtime type error. Optional: avoid using PubkeyFormatter(...).toNpub() ?? '' — validate/skip null-or-empty npub before constructing domain_user.User to prevent users with empty id/publicKey.

Likely an incorrect or invalid review comment.


730-736: No downstream changes required — call sites & tests already use domain_user.User

Verified: tests (test/config/providers/group_provider_test.dart — expect(members, [testUser]) / expect(admins, [testUser])) and internal usages (lib/config/providers/group_provider.dart — members.map((m) => m.publicKey)) operate on domain_user.User, so no API-update work needed.

Copy link
Contributor

@josefinalliende josefinalliende left a comment

Choose a reason for hiding this comment

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

Lots of Loc, haven't tried it locally yet, but this comment I think it's important.

@erskingardner erskingardner marked this pull request as ready for review September 13, 2025 17:00
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 (2)
lib/ui/settings/network/widgets/relay_tile.dart (2)

34-35: Annotate local variable type per guidelines

Explicitly type locals in Dart per repo rules.

-    final confirmed = await showDialog<bool>(
+    final bool? confirmed = await showDialog<bool>(

45-83: Localize user-facing strings in lib/ui/settings/network/widgets/relay_tile.dart

Replace literal strings with AppLocalizations: "Remove Relay?", the confirmation body text, "Cancel", "Remove Relay", and the toast "Relay removed successfully". Locations: lines 45–83 and 93. If you share the localization keys I will propose the exact patch wiring AppLocalizations (including the toast).

♻️ Duplicate comments (3)
lib/ui/settings/network/widgets/relay_tile.dart (1)

134-152: Delete control: still-too-small hit target + missing tooltip/value; align copy with dialog

Good fix gating on showOptions. Please also enlarge the tap target to ~44x44, add a Tooltip, include the URL as Semantics.value, and align copy to “Remove relay” (dialog uses “Remove Relay?”).

-            if (widget.showOptions)
-              RepaintBoundary(
-                child: Semantics(
-                  label: 'Delete relay',
-                  button: true,
-                  child: GestureDetector(
-                    onTap: _removeRelay,
-                    behavior: HitTestBehavior.opaque,
-                    child: Padding(
-                      padding: EdgeInsets.all(4.w),
-                      child: WnImage(
-                        AssetsPaths.icDelete,
-                        color: context.colors.primary,
-                        size: 20.w,
-                      ),
-                    ),
-                  ),
-                ),
-              ),
+            if (widget.showOptions)
+              RepaintBoundary(
+                child: Semantics(
+                  label: 'Remove relay',
+                  value: widget.relayInfo.url.sanitizedUrl,
+                  button: true,
+                  child: Tooltip(
+                    message: 'Remove relay',
+                    child: GestureDetector(
+                      onTap: _removeRelay,
+                      behavior: HitTestBehavior.opaque,
+                      child: SizedBox(
+                        width: 44.w,
+                        height: 44.w,
+                        child: Center(
+                          child: WnImage(
+                            AssetsPaths.icDelete,
+                            color: context.colors.primary,
+                            size: 20.w,
+                          ),
+                        ),
+                      ),
+                    ),
+                  ),
+                ),
+              ),
lib/ui/settings/general_settings_screen.dart (1)

70-80: Cache guard now correctly checks profile data presence — LGTM

The early-return condition includes both accounts and profile data, addressing the prior concern.

lib/ui/core/ui/wn_image.dart (1)

96-111: Short-circuiting file checks for non-file URIs — LGTM

The heuristic avoids unnecessary File.exists I/O for assets/virtual URIs.

🧹 Nitpick comments (11)
lib/ui/settings/network/widgets/relay_tile.dart (3)

100-116: Overuse of RepaintBoundary; keep only the outer one

Nested RepaintBoundary widgets add compositing overhead and won’t further reduce repaints. Keep the outer boundary and remove the inner ones.

-    return RepaintBoundary(
-      child: Container(
+    return RepaintBoundary(
+      child: Container(
         padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
         decoration: BoxDecoration(
           color: context.colors.surface,
           borderRadius: BorderRadius.circular(8.r),
         ),
         child: Row(
           children: [
-            RepaintBoundary(
-              child: WnImage(
-                widget.relayInfo.status.getIconAsset(),
-                color: widget.relayInfo.status.getColor(context),
-                size: 24.w,
-              ),
-            ),
+            WnImage(
+              widget.relayInfo.status.getIconAsset(),
+              color: widget.relayInfo.status.getColor(context),
+              size: 24.w,
+            ),
             SizedBox(width: 12.w),
-            Expanded(
-              child: RepaintBoundary(
-                child: Text(
-                  widget.relayInfo.url.sanitizedUrl,
-                  style: TextStyle(
-                    color: context.colors.primary,
-                    fontWeight: FontWeight.w600,
-                    fontSize: 12.sp,
-                  ),
-                  overflow: TextOverflow.ellipsis,
-                ),
-              ),
-            ),
+            Expanded(
+              child: Text(
+                widget.relayInfo.url.sanitizedUrl,
+                style: TextStyle(
+                  color: context.colors.primary,
+                  fontWeight: FontWeight.w600,
+                  fontSize: 12.sp,
+                ),
+                overflow: TextOverflow.ellipsis,
+              ),
+            ),
-            if (widget.showOptions)
-              RepaintBoundary(
-                child: Semantics(
-                  label: 'Delete relay',
-                  button: true,
-                  child: GestureDetector(
-                    onTap: _removeRelay,
-                    behavior: HitTestBehavior.opaque,
-                    child: Padding(
-                      padding: EdgeInsets.all(4.w),
-                      child: WnImage(
-                        AssetsPaths.icDelete,
-                        color: context.colors.primary,
-                        size: 20.w,
-                      ),
-                    ),
-                  ),
-                ),
-              ),
+            // Keep only one outer RepaintBoundary for the tile; action semantics handled elsewhere.
+            if (widget.showOptions)
+              Semantics(
+                label: 'Remove relay',
+                value: widget.relayInfo.url.sanitizedUrl,
+                button: true,
+                child: Tooltip(
+                  message: 'Remove relay',
+                  child: GestureDetector(
+                    onTap: _removeRelay,
+                    behavior: HitTestBehavior.opaque,
+                    child: SizedBox(
+                      width: 44.w,
+                      height: 44.w,
+                      child: Center(
+                        child: WnImage(
+                          AssetsPaths.icDelete,
+                          color: context.colors.primary,
+                          size: 20.w,
+                        ),
+                      ),
+                    ),
+                  ),
+                ),
+              ),

Also applies to: 120-131, 135-152


53-60: Add tooltip to close button for a11y

Provide long-press tooltip and better a11y.

-                    IconButton(
+                    IconButton(
+                      tooltip: 'Close',
                       onPressed: () => Navigator.of(context).pop(false),
                       icon: WnImage(
                         AssetsPaths.icClose,
                         color: context.colors.primary,
                         size: 24.w,
                       ),
                     ),

16-21: Guard API: require onDelete when showOptions is true

Avoid rendering a delete UI that can no-op. Add an assert to keep contract tight in debug builds.

   const RelayTile({
     super.key,
     required this.relayInfo,
     this.showOptions = false,
     this.onDelete,
-  });
+  }) : assert(!showOptions || onDelete != null, 'onDelete is required when showOptions is true');
lib/ui/settings/general_settings_screen.dart (4)

171-176: Prevent accidental dismissal of sign-out dialog

Make the confirmation modal non-dismissible by tapping outside.

-    final confirmed = await showDialog<bool>(
+    final confirmed = await showDialog<bool>(
       context: context,
-      barrierColor: Colors.transparent,
+      barrierColor: Colors.transparent,
+      barrierDismissible: false,
       builder:

176-199: Localize user-facing strings and move to constants

Replace hard-coded strings with AppLocalizations keys and centralize any reusable labels in constants per guidelines.

Example:

- title: 'Sign out',
+ title: AppLocalizations.of(context)!.signOut,

Also applies to: 269-275, 290-299, 315-333, 349-357, 373-377, 386-393, 240-241


134-146: Minor: rename for clarity

hasMultipleAccounts actually checks for “≥3 accounts”. Consider renaming to hasMoreThanTwoAccounts or hasAnotherChoice.

-    final hasMultipleAccounts = _accounts.length > 2;
+    final hasMoreThanTwoAccounts = _accounts.length > 2;
...
-      if (hasMultipleAccounts) {
+      if (hasMoreThanTwoAccounts) {

46-132: Style: extra blank lines within functions

There are several blank lines inside functions; the repo guidelines ask to avoid them. Consider tightening to improve consistency.

Also applies to: 148-249, 251-399

lib/ui/core/ui/wn_image.dart (4)

113-131: Avoid stale result when src changes during async file check

Capture src before awaiting and only apply the result if it’s unchanged.

-  void _checkLocalFileAsync() async {
+  void _checkLocalFileAsync() async {
+    final currentSrc = widget.src;
     try {
-      final file = File(widget.src);
+      final file = platformFile(currentSrc) as File;
       // ignore: avoid_slow_async_io
-      final exists = await file.exists();
-      if (mounted) {
+      final exists = await platformFileExists(currentSrc);
+      if (mounted && currentSrc == widget.src) {
         setState(() {
           _isLocalFile = exists;
         });
       }
     } catch (e) {
-      if (mounted) {
+      if (mounted && currentSrc == widget.src) {
         setState(() {
           _isLocalFile = false;
         });
       }
     }
   }

88-96: Robust SVG detection

For URLs with query strings (e.g., icon.svg?v=1), endsWith('.svg') fails. Prefer checking the parsed URI path.

-    _isSvg = widget.src.toLowerCase().endsWith('.svg');
+    final pathLower = (_parsedUri?.path ?? widget.src).toLowerCase();
+    _isSvg = pathLower.endsWith('.svg');

166-191: Redundant properties when using imageBuilder

When imageBuilder is supplied, fit, alignment, color, and colorBlendMode on CachedNetworkImage are ignored. Keep them only on the built Image to avoid confusion.

-        child = CachedNetworkImage(
+        child = CachedNetworkImage(
           imageUrl: widget.src,
           width: width,
           height: height,
-          fit: widget.fit,
-          alignment: widget.alignment,
           memCacheWidth: width == null ? null : (width * _cachedDevicePixelRatio!).round(),
           memCacheHeight: height == null ? null : (height * _cachedDevicePixelRatio!).round(),
           cacheKey: widget.cacheKey,
           placeholder: (context, url) => _buildPlaceholder(context),
           errorWidget: (context, url, error) => _buildFallback(context),
-          color: widget.color,
-          colorBlendMode: widget.color == null ? null : BlendMode.srcIn,
           imageBuilder:
               (context, provider) => Image(
                 image: provider,
                 width: width,
                 height: height,
                 fit: widget.fit,
                 alignment: widget.alignment,
                 color: widget.color,
                 colorBlendMode: widget.color == null ? null : BlendMode.srcIn,
                 semanticLabel: widget.semanticLabel,
                 errorBuilder: (context, error, stackTrace) => _buildFallback(context),
               ),
         );

132-257: Style: extra blank lines within methods

Multiple blank lines inside methods; tighten per repo style guide.

Also applies to: 259-299

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1b1ff04 and 24596e6.

⛔ Files ignored due to path filters (4)
  • 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
  • rust/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • lib/config/providers/follows_provider.dart (2 hunks)
  • lib/ui/core/ui/wn_image.dart (3 hunks)
  • lib/ui/settings/general_settings_screen.dart (8 hunks)
  • lib/ui/settings/network/widgets/relay_section.dart (1 hunks)
  • lib/ui/settings/network/widgets/relay_tile.dart (1 hunks)
  • lib/utils/message_converter.dart (1 hunks)
  • rust/Cargo.toml (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • lib/utils/message_converter.dart
🚧 Files skipped from review as they are similar to previous changes (3)
  • lib/config/providers/follows_provider.dart
  • rust/Cargo.toml
  • lib/ui/settings/network/widgets/relay_section.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/core/ui/wn_image.dart
  • lib/ui/settings/network/widgets/relay_tile.dart
  • lib/ui/settings/general_settings_screen.dart
🔇 Additional comments (4)
lib/ui/settings/general_settings_screen.dart (3)

251-301: RepaintBoundary usage for hot UI paths — good perf hygiene

Wrapping heavy/interactive tiles and header elements with RepaintBoundary should reduce unnecessary repaints.

Also applies to: 310-336, 344-360, 368-381, 384-395


86-114: Account load concurrency guard and TTL update look correct

_isLoadingAccounts gating + _lastAccountsLoadTime refresh prevent stampedes and stale data.


148-167: Prevent duplicate success toast when switching profiles

SwitchProfileBottomSheet.show forwards showSuccessToast to the sheet while the onProfileSelected handler calls _switchAccount — _switchAccount also shows a success toast, which can produce duplicate feedback. Fix options:

  • Add a bool showToast = true parameter to _switchAccount and call _switchAccount(..., showToast: false) from the bottom sheet; or
  • Remove the bottom-sheet toast and centralize success feedback inside _switchAccount.

Verify implementation at lib/ui/settings/general_settings_screen.dart ( _switchAccount ≈ line 134; SwitchProfileBottomSheet.show call ≈ lines 158–166). Run to inspect the function: sed -n '120,180p' lib/ui/settings/general_settings_screen.dart

lib/ui/core/ui/wn_image.dart (1)

1-1: Web-compat: avoid unconditional dart:io in lib/ui/core/ui/wn_image.dart

  • File unconditionally imports 'dart:io' and calls File(...)/exists(); this will break Flutter Web builds.
  • Repo shows mixed signals (README: "Web — Coming soon", pubspec.lock contains web adapters; local note says repo currently doesn't target web) — confirm whether web is a supported target.
  • If web is or may become a target: replace direct dart:io usage with conditional imports + a platform adapter (e.g., platformFile/platformFileExists) and update File(...) usages accordingly.
  • Location: lib/ui/core/ui/wn_image.dart (import line, _checkLocalFileAsync, and File(...) usages).

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: 1

🧹 Nitpick comments (8)
rust/src/api/users.rs (4)

43-45: Unify error conversion (avoid mixed ? vs map_err).

get_user uses .map_err(ApiError::from)? while this call uses bare ?. If From<whitenoise::Error> for ApiError isn’t implemented, this won’t compile; if it is, we still have inconsistency. Pick one style repo‑wide.

Option A (preferred): switch get_user to ? for consistency (outside this hunk):

-    let user = whitenoise
-        .find_or_create_user_by_pubkey(&pubkey)
-        .await
-        .map_err(ApiError::from)?;
+    let user = whitenoise.find_or_create_user_by_pubkey(&pubkey).await?;

Option B: keep .map_err(...) here too:

-    let user = whitenoise.find_or_create_user_by_pubkey(&pubkey).await?;
+    let user = whitenoise
+        .find_or_create_user_by_pubkey(&pubkey)
+        .await
+        .map_err(ApiError::from)?;

43-44: Behavior change: read paths now create users — confirm this is intended and safe.

find_or_create_user_by_pubkey can write. Ensure this is idempotent, cheap, and bounded (no untrusted spam keys creating rows). Consider metrics, rate‑limits, and a feature flag if needed.


51-53: Apply the same error‑handling policy here.

Match the decision above for .await? vs .map_err(ApiError::from)? on both the find_or_create... call and relays_by_type(...).await?.

-    let user = whitenoise.find_or_create_user_by_pubkey(&pubkey).await?;
+    let user = whitenoise
+        .find_or_create_user_by_pubkey(&pubkey)
+        .await
+        .map_err(ApiError::from)?;
-    let relays = user.relays_by_type(relay_type, &whitenoise).await?;
+    let relays = user
+        .relays_by_type(relay_type, &whitenoise)
+        .await
+        .map_err(ApiError::from)?;

60-62: Nit: pass &whitenoise for consistency and align error mapping.

We pass &whitenoise elsewhere; do the same here and align error conversion.

-    let user = whitenoise.find_or_create_user_by_pubkey(&pubkey).await?;
-    Ok(user.key_package_event(whitenoise).await?.is_some())
+    let user = whitenoise
+        .find_or_create_user_by_pubkey(&pubkey)
+        .await
+        .map_err(ApiError::from)?;
+    Ok(user
+        .key_package_event(&whitenoise)
+        .await
+        .map_err(ApiError::from)?
+        .is_some())
lib/domain/models/contact_model.dart (2)

39-46: Unify fallback copy and consider localization.

'Unknown User' here vs 'Unknown' in User model diverge. Prefer a single constant (and eventually AppLocalizations in UI).


48-48: Minor naming clarity.

finalDisplayName reads fine, but resolvedDisplayName or effectiveDisplayName better conveys “selected via fallback.” Optional.

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

20-27: Trim before emptiness checks and reuse sanitizer for parity with ContactModel.

Use .trim().isNotEmpty (or share _sanitizeString) to avoid treating whitespace as a valid name. This keeps behavior consistent across models.

-    if (metadata.displayName?.isNotEmpty == true) {
-      finalDisplayName = metadata.displayName!;
-    } else if (metadata.name?.isNotEmpty == true) {
-      finalDisplayName = metadata.name!;
+    final dn = metadata.displayName?.trim();
+    final nm = metadata.name?.trim();
+    if ((dn?.isNotEmpty ?? false)) {
+      finalDisplayName = dn!;
+    } else if ((nm?.isNotEmpty ?? false)) {
+      finalDisplayName = nm!;
     }

30-30: Normalize pubkey in equality/hash or guarantee callers pass hex.

Given broader pubkey normalization changes, either:

  • Normalize publicKey and id in ==/hashCode like ContactModel, or
  • Enforce a hex‑only invariant at construction (and document it).

Happy to provide a patch once you confirm the preferred invariant.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 24596e6 and 2106948.

📒 Files selected for processing (3)
  • lib/domain/models/contact_model.dart (1 hunks)
  • lib/domain/models/user_model.dart (1 hunks)
  • rust/src/api/users.rs (2 hunks)
🧰 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/user_model.dart
  • lib/domain/models/contact_model.dart
🧠 Learnings (2)
📓 Common learnings
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#594
File: lib/src/rust/api/utils.dart:12-16
Timestamp: 2025-09-07T02:15:31.931Z
Learning: The lingering PublicKey references in lib/config/providers/auth_provider.dart, lib/config/providers/group_provider.dart, and rust_builder/cargokit/build_tool/lib/src/options.dart identified during the utils.dart API cleanup were fixed in PR #597 in the whitenoise_flutter codebase.
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-07T13:10:16.542Z
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#597
File: lib/config/providers/group_provider.dart:311-314
Timestamp: 2025-09-07T13:10:16.542Z
Learning: In the whitenoise_flutter codebase, the User class used in group_provider.dart (and similar contexts) is presentational only, not the actual user class from the Rust API. There are plans to remove this User class and replace it with UserProfileData, similar to the planned consolidation with ContactModel.

Applied to files:

  • lib/domain/models/user_model.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/domain/models/contact_model.dart (1)

31-31: LGTM on sanitizing name.

Adding _sanitizeString(metadata?.name) is a good defensive step and aligns with the existing sanitizers.

Comment on lines +39 to +46
// Use display_name first, then fall back to name, then to Unknown User
String finalDisplayName = 'Unknown User';
if (displayName.isNotEmpty) {
finalDisplayName = displayName;
} else if (name.isNotEmpty) {
finalDisplayName = name;
}

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

⚠️ Potential issue

Fix equals/hashCode contract: normalize key in hashCode too.

Equality compares normalized hex pubkeys, but hashCode (Line 98) hashes the raw publicKey. Two equal objects can then yield different hashes. Normalize in hashCode (and optionally DRY up equality).

Apply outside-range change:

   @override
   int get hashCode {
-    return Object.hash(publicKey, imagePath, displayName, about, website, nip05, lud16);
+    final normalizedHex =
+        PubkeyFormatter(pubkey: publicKey).toHex() ?? publicKey;
+    return Object.hash(
+      normalizedHex,
+      imagePath,
+      displayName,
+      about,
+      website,
+      nip05,
+      lud16,
+    );
   }

Optional: extract a private getter to reuse in operator ==.

📝 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
// Use display_name first, then fall back to name, then to Unknown User
String finalDisplayName = 'Unknown User';
if (displayName.isNotEmpty) {
finalDisplayName = displayName;
} else if (name.isNotEmpty) {
finalDisplayName = name;
}
@override
int get hashCode {
final String normalizedHex =
PubkeyFormatter(pubkey: publicKey).toHex() ?? publicKey;
return Object.hash(
normalizedHex,
imagePath,
displayName,
about,
website,
nip05,
lud16,
);
}
🤖 Prompt for AI Agents
In lib/domain/models/contact_model.dart around lines 39-46 (hashCode at ~line
98), equality currently compares normalized hex public keys but hashCode uses
the raw publicKey, breaking the equals/hashCode contract; normalize the key in
hashCode as well (same normalization used in operator==, e.g. strip 0x prefix
and lowercase) and update operator== to use that same normalized value; to DRY,
add a private getter (e.g. _normalizedPublicKey) that performs the normalization
and use it in both operator== and hashCode so both use the identical canonical
key.

Copy link
Contributor

@josefinalliende josefinalliende left a comment

Choose a reason for hiding this comment

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

Buch of fixes!! 🚀 I tried it my iphone and works good for me! Approved! ✅

import 'package:whitenoise/config/providers/follows_provider.dart';
import 'package:whitenoise/config/states/group_state.dart';
import 'package:whitenoise/domain/models/user_model.dart';
import 'package:whitenoise/domain/models/user_model.dart' as domain_user;
Copy link
Contributor

Choose a reason for hiding this comment

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

💯 nice, it was easy before to get confused between the user that came from rust crate and the user from the models file.

}

User? findFollowByPubkey(String pubkey) {
return state.follows.where((user) => user.pubkey == pubkey).firstOrNull;
Copy link
Contributor

Choose a reason for hiding this comment

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

good catch, thanks for fixing it

}

/// Optimized key package item widget with reduced rasterization cost
class _KeyPackageItem extends StatelessWidget {
Copy link
Contributor

Choose a reason for hiding this comment

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

[sug] maybe move this widget to a separate file? For me it's easier to find widgets if they are one per file. But jus suggesting

decoration: BoxDecoration(
border: Border.all(
color: context.colors.border.withValues(alpha: 0.3),
width: 0.5, // Thinner border for better performance
Copy link
Contributor

Choose a reason for hiding this comment

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

Had no idea that border widths could affect performance...

Copy link
Member Author

Choose a reason for hiding this comment

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

This is AI - I wonder if it's actually true. Claude seems to think so...

Yes, smaller border widths can improve performance in Flutter apps, though the impact is usually minimal.

Why it helps:

  • Less pixel filling work for the GPU
  • Reduced overdraw when borders overlap with other elements
  • Simpler rasterization calculations

When it matters:

  • Lists with many bordered items
  • Complex layouts with nested borders
  • Lower-end devices with limited GPU power
  • High frame rate animations involving bordered widgets

For most apps, you won't notice the difference unless you're dealing with performance-critical scenarios or rendering hundreds of bordered elements simultaneously.

@erskingardner erskingardner merged commit a486857 into master Sep 14, 2025
2 checks passed
@erskingardner erskingardner deleted the perf-fixes branch September 14, 2025 11:03
@coderabbitai coderabbitai bot mentioned this pull request Sep 30, 2025
11 tasks
@coderabbitai coderabbitai bot mentioned this pull request Oct 23, 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.

3 participants