-
Notifications
You must be signed in to change notification settings - Fork 14
Fix/metatada issues #594
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix/metatada issues #594
Conversation
WalkthroughReplaces account-object flows with a pubkey-first approach across providers and UI, introduces a userProfileDataProvider for fetching user metadata and tests, removes metadata-cache docs/examples/utilities, converts several Rust bridge key-conversion calls to synchronous forms and removes PublicKey opaque bridging, and updates generated FRB bindings and changelog. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor UI as UI (Widget)
participant UP as userProfileDataProvider
participant API as WnUsersApi
participant RUST as Rust bridge (utils/users)
UI->>UP: getUserProfileData(pubkey)
UP->>API: getUser(pubkey)
API->>RUST: getUser(pubkey)
RUST-->>API: User(metadata)
API-->>UP: User
UP-->>UI: ContactModel.fromMetadata(pubkey, metadata)
note over UI,UP: Errors thrown by API propagate to caller
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (15)
lib/config/providers/metadata_cache_provider.dart (1)
316-327: Standardize cache key to npub in updateCachedMetadata
updateCachedMetadata currently writes under normalized (hex or npub) strings, but reads always use standardized npub via_getStandardizedNpub, causing cache misses and duplicates. Change signature to async and await_getStandardizedNpubas below:- void updateCachedMetadata(String publicKey, ContactModel contactModel) { - final normalizedKey = _normalizePublicKey(publicKey); + Future<void> updateCachedMetadata(String publicKey, ContactModel contactModel) async { + final String standardized = await _getStandardizedNpub(publicKey); final newCache = Map<String, CachedMetadata>.from(state.cache); - newCache[normalizedKey] = CachedMetadata( + newCache[standardized] = CachedMetadata( contactModel: contactModel, cachedAt: DateTime.now(), ); state = state.copyWith(cache: newCache); - _logger.info('Updated cached metadata for $normalizedKey'); + _logger.info('Updated cached metadata for $standardized'); }lib/config/providers/nostr_keys_provider.dart (1)
33-41: Ensure active pubkey is hex before FRB calls; add explicit type.activePubkey may be npub; both npubFromHexPubkey and exportAccountNsec expect hex. Normalize first.
- final activePubkey = ref.read(activePubkeyProvider); + final String? activePubkey = ref.read(activePubkeyProvider); if (activePubkey == null || activePubkey.isEmpty) { _logger.severe('NostrKeysNotifier: No active account found'); state = state.copyWith( isLoading: false, error: 'No active account found', ); return; } - _logger.info('NostrKeysNotifier: Loading keys for account: $activePubkey'); + // Normalize to hex for downstream APIs + String hexPubkey = activePubkey; + if (hexPubkey.startsWith('npub1')) { + hexPubkey = await hexPubkeyFromNpub(npub: hexPubkey); + } + _logger.info('NostrKeysNotifier: Loading keys for account: $hexPubkey');lib/config/providers/relay_status_provider.dart (1)
182-188: Make this FutureProvider reactive to active pubkey switches.Watch activePubkeyProvider so a pubkey change recomputes connectivity.
final allRelayTypesConnectionProvider = FutureProvider<bool>((ref) async { // Watch the relay status provider to trigger rebuilds when statuses change ref.watch(relayStatusProvider); + // Also watch active pubkey to recompute on account switch + ref.watch(activePubkeyProvider); - final notifier = ref.read(relayStatusProvider.notifier); + final RelayStatusNotifier notifier = ref.read(relayStatusProvider.notifier); return await notifier.areAllRelayTypesConnected(); });lib/ui/settings/general_settings_screen.dart (2)
79-83: Guard setState with mounted after awaitsPrevents “setState called after dispose” race.
- setState(() { + if (!mounted) return; + setState(() { _accounts = accounts; _currentAccount = activeAccount; _accountContactModels = contactModels; - }); + });
231-233: Fix off-by-one: treat 2 accounts as “multiple”
> 2fails when exactly two accounts exist, breaking the post-logout flow.- final hasMultipleAccounts = accounts.length > 2; + final bool hasMultipleAccounts = accounts.length > 1;lib/config/providers/follows_provider.dart (1)
81-94: Guard against stale updates when pubkey changes mid-flightCapture and re-check the active pubkey after awaits to avoid applying results for an old account; also add explicit types per guidelines.
- final activePubkey = ref.read(activePubkeyProvider); + final String? activePubkey = ref.read(activePubkeyProvider); if (activePubkey == null || activePubkey.isEmpty) { state = state.copyWith(error: 'No active account found', isLoading: false); return; } - - final follows = await accounts_api.accountFollows(pubkey: activePubkey); + final String capturedPubkey = activePubkey; + final follows = await accounts_api.accountFollows(pubkey: capturedPubkey); _logger.info('FollowsProvider: Loaded ${follows.length} follows'); final sortedFollows = UserUtils.sortUsersByName(follows); + // Drop stale responses if account switched meanwhile + if (capturedPubkey != ref.read(activePubkeyProvider)) { + _logger.fine('FollowsProvider: Dropping stale follows for $capturedPubkey'); + state = state.copyWith(isLoading: false); + return; + } state = state.copyWith(follows: sortedFollows, isLoading: false);lib/config/providers/welcomes_provider.dart (1)
61-84: Prevent stale state updates during async loads; add explicit typesCapture pubkey and verify before mutating state.
- final activePubkey = ref.read(activePubkeyProvider); + final String? activePubkey = ref.read(activePubkeyProvider); if (activePubkey == null || activePubkey.isEmpty) { state = state.copyWith(error: 'No active account found', isLoading: false); return; } - - final welcomes = await pendingWelcomes(pubkey: activePubkey); + final String capturedPubkey = activePubkey; + final welcomes = await pendingWelcomes(pubkey: capturedPubkey); final welcomeByData = <String, Welcome>{}; for (final welcome in welcomes) { welcomeByData[welcome.id] = welcome; } + if (capturedPubkey != ref.read(activePubkeyProvider)) { + _logger.fine('WelcomesProvider: Dropping stale welcomes for $capturedPubkey'); + state = state.copyWith(isLoading: false); + return; + } // Get current pending welcomes to compare final previousPendingIds = getPendingWelcomes().map((w) => w.id).toSet();lib/config/providers/chat_provider.dart (3)
66-106: Add pubkey-change stale guard to message loads; type localsPrevent cross-account leakage when account switches mid-call.
- final activePubkey = ref.read(activePubkeyProvider); + final String? activePubkey = ref.read(activePubkeyProvider); if (activePubkey == null || activePubkey.isEmpty) { _setGroupError(groupId, 'No active account found'); return; } + final String capturedPubkey = activePubkey; _logger.info('ChatProvider: Loading messages for group $groupId'); // Use fetchAggregatedMessagesForGroup which includes all message data including replies final aggregatedMessages = await fetchAggregatedMessagesForGroup( - pubkey: activePubkey, + pubkey: capturedPubkey, groupId: groupId, ); _logger.info( 'ChatProvider: Fetched ${aggregatedMessages.length} aggregated messages', ); + // Drop stale response if account switched + if (capturedPubkey != ref.read(activePubkeyProvider)) { + _logger.fine('ChatProvider: Dropping stale load for $capturedPubkey'); + _setGroupError(groupId, 'Cancelled due to account switch'); + return; + } // Sort messages by creation time (oldest first) aggregatedMessages.sort((a, b) => a.createdAt.compareTo(b.createdAt)); final messages = await MessageConverter.fromChatMessageList( aggregatedMessages, - currentUserPublicKey: activePubkey, + currentUserPublicKey: capturedPubkey, groupId: groupId, ref: ref, );
286-305: Add stale guard to polling path; type localsSame pattern as load.
- final activePubkey = ref.read(activePubkeyProvider); + final String? activePubkey = ref.read(activePubkeyProvider); if (activePubkey == null || activePubkey.isEmpty) { return; } + final String capturedPubkey = activePubkey; // Use fetchAggregatedMessagesForGroup for polling as well final aggregatedMessages = await fetchAggregatedMessagesForGroup( - pubkey: activePubkey, + pubkey: capturedPubkey, groupId: groupId, ); + if (capturedPubkey != ref.read(activePubkeyProvider)) { + _logger.fine('ChatProvider: Dropping stale poll for $capturedPubkey'); + return; + } aggregatedMessages.sort((a, b) => a.createdAt.compareTo(b.createdAt)); final newMessages = await MessageConverter.fromChatMessageList( aggregatedMessages, - currentUserPublicKey: activePubkey, + currentUserPublicKey: capturedPubkey, groupId: groupId, ref: ref, );
1-832: Enforce stale‐pubkey guard on all async state mutations
Capture the active pubkey once at the start of each async method (e.g.final currentPubkey = ref.read(activePubkeyProvider)) and, before anyawaitand before mutatingstate, verifyref.read(activePubkeyProvider) == currentPubkey. Apply in:
loadMessagesForGroup(...)sendMessage(...)checkForNewMessages(...)updateMessageReaction(...)sendReplyMessage(...)deleteMessage(...)lib/ui/chat/invite/chat_invite_screen.dart (1)
101-105: Replace fixed 1s delay with a state-based await.
A hardcoded delay is brittle. Prefer awaiting a provider signal that the accepted group exists/loaded, then navigate.Proposed change:
- await Future.delayed(const Duration(milliseconds: 1000)); - if (context.mounted) { - context.pushReplacement('/chats/${widget.groupId}'); - } + // Wait until the group is available (implement in groups notifier if missing) + await ref.read(groupsProvider.notifier).waitUntilGroupExists(widget.groupId, timeout: const Duration(seconds: 5)); + if (context.mounted) { + context.pushReplacement('/chats/${widget.groupId}'); + }lib/config/providers/group_provider.dart (4)
194-201: Normalize creator key once, type explicitly, and use the trimmed value everywhere.Reduces duplication and whitespace bugs when composing admin/creator params and logs.
- final activePubkey = ref.read(activePubkeyProvider); + final String? activePubkey = ref.read(activePubkeyProvider); @@ - if (activePubkey == null || activePubkey.isEmpty) { + if (activePubkey == null || activePubkey.isEmpty) { state = state.copyWith(error: 'No active account found', isLoading: false); return null; } @@ - final creatorPubkeyHex = activePubkey.trim(); + final String creatorPubkeyHex = activePubkey.trim(); @@ - final resolvedAdminPublicKeys = - adminPublicKeyHexs.toSet().map((hexKey) => hexKey.trim()).toList(); - final combinedAdminKeys = {activePubkey, ...resolvedAdminPublicKeys}.toList(); + final List<String> resolvedAdminPublicKeys = + adminPublicKeyHexs.toSet().map((hexKey) => hexKey.trim()).toList(); + final List<String> combinedAdminKeys = {creatorPubkeyHex, ...resolvedAdminPublicKeys}.toList(); @@ - _logger.info(' - Creator pubkey: $activePubkey'); + _logger.info(' - Creator pubkey: $creatorPubkeyHex'); @@ - creatorPubkey: activePubkey, + creatorPubkey: creatorPubkeyHex,Also applies to: 215-229, 235-247
260-266: Fix: missingawaitwhen unpacking ApiError details.Currently logs a Future instead of the message.
- if (e is ApiError) { - final errorDetails = e.messageText(); + if (e is ApiError) { + final errorDetails = await e.messageText(); logMessage += '$errorDetails (Type: ${e.runtimeType})'; } else {
420-424: Fix: missingawaitwhen unpacking ApiError (admins loader).- if (e is ApiError) { - final errorDetails = e.messageText(); + if (e is ApiError) { + final errorDetails = await e.messageText(); logMessage += '$errorDetails (Type: ${e.runtimeType})'; } else {
815-823: Fix: missingawaitwhen unpacking ApiError (polling).- if (e is ApiError) { - final errorDetails = e.messageText(); + if (e is ApiError) { + final errorDetails = await e.messageText(); logMessage += '$errorDetails (Type: ${e.runtimeType})'; } else {
🧹 Nitpick comments (63)
lib/config/providers/active_account_provider.dart (1)
140-141: Prefer explicit local types; remove blank lines inside functions.Per repo guidelines, declare types for locals (e.g., final String? activePubkey) and avoid blank lines within functions for consistency.
- final activePubkey = ref.watch(activePubkeyProvider); + final String? activePubkey = ref.watch(activePubkeyProvider); ... - final activePubkey = ref.read(activePubkeyProvider); + final String? activePubkey = ref.read(activePubkeyProvider); ... - final imageUtils = ref.read(wnImageUtilsProvider); + final WnImageUtils imageUtils = ref.read(wnImageUtilsProvider); - final imageType = await imageUtils.getMimeTypeFromPath(filePath); + final String? imageType = await imageUtils.getMimeTypeFromPath(filePath); ... - final utils = ref.read(wnUtilsProvider); + final WnUtils utils = ref.read(wnUtilsProvider); - final serverUrl = await utils.getDefaultBlossomServerUrl(); + final String serverUrl = await utils.getDefaultBlossomServerUrl(); ... - final accountsApi = ref.read(wnAccountsApiProvider); + final WnAccountsApi accountsApi = ref.read(wnAccountsApiProvider);Also applies to: 174-175, 197-198, 207-208, 212-214, 215-221
CHANGELOG.md (1)
25-26: Fix tense/wording for consistency.Use past tense under “Fixed” and clarify phrasing.
-- Fixes profile edition with outdated metadata +- Fixed profile editing when local metadata was outdatedrust/src/api/utils.rs (1)
49-57: Make dev URL configurable to avoid code edits.Read from an env var with a sane default to reduce churn across branches.
#[frb] pub fn get_default_blossom_server_url() -> String { - #[cfg(debug_assertions)] - { - "http://localhost:3000".to_string() - } + #[cfg(debug_assertions)] + { + std::env::var("BLOSSOM_UPLOADS_URL") + .unwrap_or_else(|_| "http://localhost:3000".to_string()) + } #[cfg(not(debug_assertions))] { "https://uploads.primal.net".to_string() } }lib/config/providers/metadata_cache_provider.dart (2)
113-116: Unnecessary hard dependency on active pubkey for read-only fetch.check: _fetchMetadataForKey throws when no active account, but the fetch doesn’t use activePubkey. This prevents metadata lookups (e.g., search, pre-login) unnecessarily.
- final activePubkey = ref.read(activePubkeyProvider); - if (activePubkey == null || activePubkey.isEmpty) { - throw StateError('No active account found'); - } + final String? activePubkey = ref.read(activePubkeyProvider); + if (activePubkey == null || activePubkey.isEmpty) { + _logger.fine('Proceeding without active account; metadata fetch is standalone.'); + }Would any Rust API call here actually require an active account context?
151-211: Minor: pending fetch cleanup is good; consider eviction on error paths too.You already remove the pending on error. Optional: add a finally block to guarantee removal if futureContactModel throws before await completes.
lib/config/providers/nostr_keys_provider.dart (1)
21-31: Minor: explicit local types; remove intra-function blank lines.Follow repo style for local declarations and function body spacing.
Also applies to: 49-64
lib/config/providers/relay_status_provider.dart (3)
66-71: Refresh on pubkey change; type annotate and fix log/error copy.Without reacting to activePubkey changes, this provider can serve stale relay statuses after account switches. Also, add explicit types and align log/error text with the pubkey-centric flow.
[svg: suggest_essential_refactor]
Apply within this hunk:
- final activePubkey = ref.read(activePubkeyProvider); - _logger.info('RelayStatusNotifier: Active account data: $activePubkey'); - if (activePubkey == null || activePubkey.isEmpty) { - _logger.warning('RelayStatusNotifier: No active account found'); - state = state.copyWith(isLoading: false, error: 'No active account found'); + final String? activePubkey = ref.read(activePubkeyProvider); + _logger.info('RelayStatusNotifier: Active pubkey: $activePubkey'); + if (activePubkey == null || activePubkey.isEmpty) { + _logger.warning('RelayStatusNotifier: No active pubkey found'); + state = state.copyWith(isLoading: false, error: 'No active pubkey found'); return; }Additionally, inside build(), listen for pubkey changes:
@override RelayStatusState build() { ref.listen<String?>(activePubkeyProvider, (prev, next) { if (prev != next) { if (next != null && next.isNotEmpty) { loadRelayStatuses(); } else { state = const RelayStatusState(relayStatuses: {}, isLoading: false); } } }); Future.microtask(() => loadRelayStatuses()); return const RelayStatusState(isLoading: true); }
75-79: Annotate fetched tuple list for clarity.Explicitly type the list of records to satisfy the “always declare types” guideline.
- final relayStatuses = await fetchRelayStatus(pubkey: activePubkey); + final List<(String, String)> relayStatuses = + await fetchRelayStatus(pubkey: activePubkey);
124-131: Parallelize bridge calls and strengthen pubkey check.Cut latency by fetching types and relays in parallel; also reject empty pubkey.
- final accountPubKey = ref.read(activePubkeyProvider); - if (accountPubKey == null) return false; - - // Fetch relay URLs for each type using new bridge methods - final nip65Type = await relayTypeNip65(); - final inboxType = await relayTypeInbox(); - final keyPackageType = await relayTypeKeyPackage(); + final String? accountPubKey = ref.read(activePubkeyProvider); + if (accountPubKey == null || accountPubKey.isEmpty) return false; + + // Fetch types in parallel + final typeResults = await Future.wait([ + relayTypeNip65(), + relayTypeInbox(), + relayTypeKeyPackage(), + ]); + final nip65Type = typeResults[0]; + final inboxType = typeResults[1]; + final keyPackageType = typeResults[2]; - final nip65Urls = - (await accountRelays( - pubkey: accountPubKey, - relayType: nip65Type, - )).map((r) => r.url).toList(); - final inboxUrls = - (await accountRelays( - pubkey: accountPubKey, - relayType: inboxType, - )).map((r) => r.url).toList(); - final keyPackageUrls = - (await accountRelays( - pubkey: accountPubKey, - relayType: keyPackageType, - )).map((r) => r.url).toList(); + // Fetch relay URLs for each type in parallel + final relayLists = await Future.wait([ + accountRelays(pubkey: accountPubKey, relayType: nip65Type), + accountRelays(pubkey: accountPubKey, relayType: inboxType), + accountRelays(pubkey: accountPubKey, relayType: keyPackageType), + ]); + final List<String> nip65Urls = relayLists[0].map((r) => r.url).toList(); + final List<String> inboxUrls = relayLists[1].map((r) => r.url).toList(); + final List<String> keyPackageUrls = relayLists[2].map((r) => r.url).toList();Also applies to: 133-147, 149-155
lib/domain/services/dm_chat_service.dart (4)
12-17: Annotate types for clarity and consistency.Explicit types satisfy project guidelines and aid readability.
- final activePubkey = ref.read(activePubkeyProvider); + final String? activePubkey = ref.read(activePubkeyProvider); if (activePubkey == null || activePubkey.isEmpty) return null; - final currentUserNpub = await npubFromHexPubkey( + final String currentUserNpub = await npubFromHexPubkey( hexPubkey: activePubkey, );
28-41: Tighten naming and inline local to avoid typo (“npup”).Inline the publicKey assignment and use explicit types on locals.
- final contactModel = ContactModel.fromMetadata( + final ContactModel contactModel = ContactModel.fromMetadata( publicKey: otherMember.publicKey, metadata: user.metadata, ); - final displayName = contactModel.displayName; - final displayImage = contactModel.imagePath ?? (otherMember.imagePath ?? ''); - final nip05 = contactModel.nip05 ?? ''; - final npup = contactModel.publicKey; + final String displayName = contactModel.displayName; + final String displayImage = contactModel.imagePath ?? (otherMember.imagePath ?? ''); + final String nip05 = contactModel.nip05 ?? ''; return DMChatData( displayName: displayName, displayImage: displayImage, nip05: nip05, - publicKey: npup, + publicKey: contactModel.publicKey, );
44-47: Add minimal logging on failure path.Helps diagnose why DM chat data is null.
- return null; + // TODO: route through a central logger if available + // ignore: avoid_print + print('DMChatService.getDMChatData error: $e'); + return null;
10-48: Avoid blank lines within functions per guidelines.There are several blank lines inside getDMChatData. Consider compacting.
lib/ui/contact_list/widgets/chat_list_active_account_avatar.dart (4)
5-5: Align with pubkey-based flow and new userProfileDataProvider.This widget still depends on activeAccountProvider. To stay consistent with the PR (and reduce coupling), derive displayName/picture from activePubkeyProvider + userProfileDataProvider(pubkey).
-import 'package:whitenoise/config/providers/active_account_provider.dart'; +import 'package:whitenoise/config/providers/active_pubkey_provider.dart'; +import 'package:whitenoise/config/providers/user_profile_data_provider.dart';Sketch for build() (adapt types to actual provider signatures):
@override Widget build(BuildContext context, WidgetRef ref) { final String? activePubkey = ref.watch(activePubkeyProvider); if (activePubkey == null || activePubkey.isEmpty) { return WnAvatar(imageUrl: '', displayName: '', size: 36.r); } final profileAsync = ref.watch(userProfileDataProvider(activePubkey)); final metadata = profileAsync.valueOrNull?.metadata; final String displayName = metadata?.displayName ?? ''; final String profileImagePath = metadata?.picture ?? ''; return InkWell( borderRadius: BorderRadius.circular(16.r), onTap: onTap ?? () => context.push(Routes.settings), child: WnAvatar(imageUrl: profileImagePath, displayName: displayName, size: 36.r), ); }If activeAccountProvider is intentionally kept, please disregard and confirm.
16-20: Type annotate locals.Adhere to the “always declare types” rule.
- final activeAccountState = ref.watch(activeAccountProvider); - final metadata = activeAccountState.value?.metadata; - final currentDisplayName = metadata?.displayName ?? ''; - final profileImagePath = metadata?.picture ?? ''; + final AsyncValue<dynamic> activeAccountState = ref.watch(activeAccountProvider); + final dynamic metadata = activeAccountState.value?.metadata; + final String currentDisplayName = metadata?.displayName ?? ''; + final String profileImagePath = metadata?.picture ?? '';Note: replace dynamic with the concrete types used in your providers/models.
21-33: Extract magic numbers to constants.36.r and 16.r are duplicated UI tokens; move to a constants file to ensure consistency.
- return InkWell( - borderRadius: BorderRadius.circular(16.r), + const double kAvatarRadius = 16; // or from theme constants + const double kAvatarSize = 36; + return InkWell( + borderRadius: BorderRadius.circular(kAvatarRadius.r), ... - child: WnAvatar( + child: WnAvatar( imageUrl: profileImagePath, displayName: currentDisplayName, - size: 36.r, + size: kAvatarSize.r, ),
15-34: Remove blank lines inside build() per local guidelines.Compact the method to avoid internal blank lines.
lib/config/providers/user_profile_data_provider.dart (1)
38-40: Expose a cacheable FutureProvider.family for UI consumptionThis avoids ad‑hoc FutureBuilder calls and enables Riverpod caching/refresh/invalidation.
Add alongside the existing NotifierProvider:
final userProfileDataFutureProvider = FutureProvider.family<ContactModel, String>((ref, pubkey) { return ref.read(userProfileDataProvider.notifier).getUserProfileData(pubkey); });lib/ui/contact_list/widgets/welcome_tile.dart (3)
29-29: Declare the variable type explicitlyMatches repo guideline to always declare types.
- final userProfileDataNotifier = ref.read(userProfileDataProvider.notifier); + final UserProfileDataNotifier userProfileDataNotifier = + ref.read(userProfileDataProvider.notifier);
31-33: Prefer Riverpod watch over FutureBuilder to avoid re-fetch on rebuildsUsing a
FutureProvider.family(see provider comment) prevents repeated network calls and integrates with invalidation.Example:
final profile = ref.watch(userProfileDataFutureProvider(welcome.welcomer)); return profile.when( data: (welcomerContact) { /* ... */ }, loading: () => const SizedBox.shrink(), error: (_, __) => const SizedBox.shrink(), );
35-36: Localize “Unknown User”Use AppLocalizations instead of a hardcoded string.
- final welcomerName = welcomerContact?.displayName ?? 'Unknown User'; + final String welcomerName = + welcomerContact?.displayName ?? AppLocalizations.of(context)!.unknownUser;lib/ui/settings/general_settings_screen.dart (1)
70-74: Batch fetch profiles in parallel and add explicit typesParallelization improves UX on multiple accounts; explicit types align with repo guidelines.
- final accounts = await getAccounts(); - final contactModels = <String, ContactModel>{}; - final userProfileDataNotifier = ref.read(userProfileDataProvider.notifier); - for (final account in accounts) { - final userProfileData = await userProfileDataNotifier.getUserProfileData(account.pubkey); - contactModels[account.pubkey] = userProfileData; - } + final List<Account> accounts = await getAccounts(); + final UserProfileDataNotifier userProfileDataNotifier = + ref.read(userProfileDataProvider.notifier); + + final List<Future<MapEntry<String, ContactModel>>> futures = [ + for (final Account account in accounts) + userProfileDataNotifier + .getUserProfileData(account.pubkey) + .then((ContactModel data) => MapEntry(account.pubkey, data)), + ]; + final Map<String, ContactModel> contactModels = + Map<String, ContactModel>.fromEntries(await Future.wait(futures));test/config/providers/user_profile_data_provider_test.dart (2)
42-58: Prefer explicit types for test fixtures (and make pubkey const).Aligns with our Dart guidelines to always declare types.
Apply:
- final testPubkey = 'test_pubkey_123'; - final testMetadata = const FlutterMetadata( + const String testPubkey = 'test_pubkey_123'; + final FlutterMetadata testMetadata = const FlutterMetadata( @@ - final testUser = User( + final User testUser = User(
132-143: Assert specific error types for stronger guarantees.Prefer throwsA with a matcher to avoid false positives when unrelated exceptions are thrown.
- expect( - () async => await notifier.getUserProfileData(testPubkey), - throwsException, - ); + expect( + () async => notifier.getUserProfileData(testPubkey), + throwsA(isA<Exception>()), + );lib/ui/settings/developer/developer_settings_screen.dart (3)
7-7: Good switch to activePubkeyProvider.Matches the PR’s pubkey-centered flow. Optional: if you import app_theme.dart (which re-exports ScreenUtil), you can drop the direct flutter_screenutil import for consistency with other files.
36-44: Gate UX earlier by disabling the button when no active pubkey.Current flow shows an error toast; disabling the action when unavailable reduces noise. Watch activePubkey in build and disable the button accordingly.
- WnFilledButton( - label: 'Reload Follows', - onPressed: _isLoading ? null : _reloadFollows, - loading: _isLoading, - ), + WnFilledButton( + label: 'Reload Follows', + onPressed: _isLoading || !(ref.watch(activePubkeyProvider)?.isNotEmpty ?? false) + ? null + : _reloadFollows, + loading: _isLoading, + ),
41-47: Localize user-facing strings.Use AppLocalizations for the toasts to comply with our i18n guideline.
- ref.showSuccessToast('Follows reloaded successfully'); + ref.showSuccessToast(AppLocalizations.of(context)!.followsReloaded) @@ - ref.showErrorToast('No active account found'); + ref.showErrorToast(AppLocalizations.of(context)!.noActiveAccount) @@ - ref.showErrorToast('Failed to reload follows: $e'); + ref.showErrorToast(AppLocalizations.of(context)!.failedToReloadFollows('$e'))lib/config/providers/polling_provider.dart (1)
79-80: Invalidating activeAccountProvider after loads is the right trigger.This should propagate metadata updates as intended. Consider also invalidating activePubkeyProvider if it derives from account state indirectly, to be extra-safe with UI watchers that observe only the pubkey.
+import 'package:whitenoise/config/providers/active_pubkey_provider.dart'; @@ - ref.invalidate(activeAccountProvider); + ref.invalidate(activeAccountProvider); + ref.invalidate(activePubkeyProvider); @@ - ref.invalidate(activeAccountProvider); + ref.invalidate(activeAccountProvider); + ref.invalidate(activePubkeyProvider);Also applies to: 125-126
lib/config/providers/follows_provider.dart (2)
118-127: Type annotations and basic input validationAdd explicit type for activePubkey and short-circuit on empty userPubkey to prevent useless calls.
- final activePubkey = ref.read(activePubkeyProvider); + final String? activePubkey = ref.read(activePubkeyProvider); if (activePubkey == null || activePubkey.isEmpty) { state = state.copyWith(error: 'No active account found', isLoading: false); return; } + if (userPubkey.isEmpty) { + state = state.copyWith(error: 'Invalid user pubkey', isLoading: false); + return; + } await accounts_api.followUser( accountPubkey: activePubkey, userToFollowPubkey: userPubkey, );
155-165: Mirror the same fixes in removeFollowKeep types explicit and validate input like in addFollow for consistency.
- final activePubkey = ref.read(activePubkeyProvider); + final String? activePubkey = ref.read(activePubkeyProvider); if (activePubkey == null || activePubkey.isEmpty) { state = state.copyWith(error: 'No active account found', isLoading: false); return; } + if (userPubkey.isEmpty) { + state = state.copyWith(error: 'Invalid user pubkey', isLoading: false); + return; + } await accounts_api.unfollowUser( accountPubkey: activePubkey, userToUnfollowPubkey: userPubkey, );lib/ui/contact_list/search_chat_bottom_sheet.dart (3)
78-84: Align log message with new model (pubkey vs account)Minor wording tweak for clarity.
- _logger.info('SearchChatBottomSheet: Found active account: $activePubkey'); + _logger.info('SearchChatBottomSheet: Found active pubkey: $activePubkey');
41-50: Auto-refresh follows on account switch while the sheet is openListen to activePubkey changes to reload contacts without manual reopen.
@override void initState() { super.initState(); _searchController.addListener(_onSearchChanged); _scrollController.addListener(_onScrollChanged); // Load contacts when the widget initializes WidgetsBinding.instance.addPostFrameCallback((_) { _loadContacts(); }); + ref.listen<String?>(activePubkeyProvider, (prev, next) { + if (!mounted) return; + if (prev != next && (next?.isNotEmpty ?? false)) { + ref.read(followsProvider.notifier).loadFollows(); + } + }); }Also applies to: 76-84
22-30: Externalize UI strings to AppLocalizationsMove hardcoded strings ("Search", "No active account found", placeholders) to l10n to match project guidelines.
Also applies to: 85-95, 145-297
lib/config/providers/welcomes_provider.dart (3)
113-124: Type the active pubkey local in fetchWelcomeByIdKeep locals explicitly typed per guidelines.
- final activePubkey = ref.read(activePubkeyProvider); + final String? activePubkey = ref.read(activePubkeyProvider);
148-156: Type the active pubkey local in accept flowConsistency with typing.
- final activePubkey = ref.read(activePubkeyProvider); + final String? activePubkey = ref.read(activePubkeyProvider);
180-188: Type the active pubkey local in decline flowConsistency with typing.
- final activePubkey = ref.read(activePubkeyProvider); + final String? activePubkey = ref.read(activePubkeyProvider);lib/config/providers/chat_provider.dart (4)
145-160: Type locals and carry captured pubkey through conversionsKeep types explicit and reuse the captured pubkey in message conversion.
- final activePubkey = ref.read(activePubkeyProvider); + final String? activePubkey = ref.read(activePubkeyProvider); if (activePubkey == null || activePubkey.isEmpty) { _setGroupError(groupId, 'No active account found'); return null; } + final String capturedPubkey = activePubkey; ... final sentMessage = await sendMessageToGroup( - pubkey: activePubkey, + pubkey: capturedPubkey, groupId: groupId, message: message, kind: kind, tags: tags, ); ... final sentMessageModel = await MessageConverter.fromChatMessage( sentChatMessage, - currentUserPublicKey: activePubkey, + currentUserPublicKey: capturedPubkey, groupId: groupId, ref: ref, messageCache: messageCache, );Also applies to: 199-206
499-506: Reactions: type local and use captured pubkeySmall consistency/tightening.
- final activePubkey = ref.read(activePubkeyProvider); + final String? activePubkey = ref.read(activePubkeyProvider); if (activePubkey == null || activePubkey.isEmpty) { _setGroupError(message.groupId ?? '', 'No active account found'); return false; } + final String capturedPubkey = activePubkey; ... await sendMessageToGroup( - pubkey: activePubkey, + pubkey: capturedPubkey, groupId: message.groupId ?? '', message: reactionContent, kind: 7, // Nostr kind 7 = reaction tags: reactionTags, );Also applies to: 533-541
572-593: Replies: type local and use captured pubkeyConsistency with other paths.
- final activePubkey = ref.read(activePubkeyProvider); + final String? activePubkey = ref.read(activePubkeyProvider); if (activePubkey == null || activePubkey.isEmpty) { _setGroupError(groupId, 'No active account found'); return null; } + final String capturedPubkey = activePubkey; ... final sentMessage = await sendMessageToGroup( - pubkey: activePubkey, + pubkey: capturedPubkey, groupId: groupId, message: message, kind: 9, // Kind 9 for replies tags: replyTags, ); ... final sentMessageModel = await MessageConverter.fromChatMessage( sentChatMessage, - currentUserPublicKey: activePubkey, + currentUserPublicKey: capturedPubkey, groupId: groupId, ref: ref, messageCache: messageCache, );Also applies to: 633-639
676-699: Deletes: type local and use captured pubkeySame as above.
- final activePubkey = ref.read(activePubkeyProvider); + final String? activePubkey = ref.read(activePubkeyProvider); if (activePubkey == null || activePubkey.isEmpty) { _setGroupError(groupId, 'No active account found'); return false; } + final String capturedPubkey = activePubkey; ... await sendMessageToGroup( - pubkey: activePubkey, + pubkey: capturedPubkey, groupId: groupId, message: '', // Empty content for deletion kind: 5, // Nostr kind 5 = deletion tags: deleteTags, );lib/ui/contact_list/group_welcome_invitation_sheet.dart (2)
116-121: Avoid duplicate toasts when gating on active pubkeyThese three widgets each toast on missing pubkey; centralize the check (e.g., in the sheet) to avoid multiple toasts if the sheet builds/rebuilds.
- if (activePubkey == null || activePubkey.isEmpty) { - ref.showErrorToast('No active account found'); - return null; - } + if (activePubkey == null || activePubkey.isEmpty) { + return null; // Let parent handle any user notification once + }Also applies to: 243-248, 286-291
116-121: Type the pubkey localKeep locals explicit.
- final activePubkey = ref.read(activePubkeyProvider); + final String? activePubkey = ref.read(activePubkeyProvider);lib/ui/chat/invite/chat_invite_screen.dart (3)
250-254: Avoid re-fetching on every rebuild; prefer a provider family (Future/Stream) or memoized Future.
Creating a new Future in build can cause redundant DB/API calls and misses automatic refresh when metadata changes. Consider a FutureProvider/StreamProvider.family keyed by pubkey and watch it here to get live updates and rebuilds.Would you like me to add a
userProfileDataFutureProvider.family(or stream) and refactorDMInviteHeader/DMAppBarTitletoref.watchit?
341-345: Same as above for AppBar title block.
Use a provider family to avoid per-build Future creation and to auto-update on metadata changes.
54-61: Localize hardcoded strings.
Move UI strings to AppLocalizations per guidelines.Also applies to: 171-179, 195-231, 303-321
lib/ui/contact_list/new_chat_bottom_sheet.dart (5)
94-101: Add explicit typing and guard logs.
Declare the type for readability and to follow code-style. Consider shortening logs if needed.- final activePubkey = ref.read(activePubkeyProvider); + final String? activePubkey = ref.read(activePubkeyProvider);
126-132: Type the notifier and result, and avoid repeated fetches while typing.
- Add explicit types.
- Optional: debounce metadata fetch to reduce API/DB thrash as the user types.
- final userProfileDataNotifier = ref.read(userProfileDataProvider.notifier); - final userProfileData = await userProfileDataNotifier.getUserProfileData(publicKey.trim()); + final UserProfileDataNotifier userProfileDataNotifier = + ref.read(userProfileDataProvider.notifier); + final ContactModel userProfileData = + await userProfileDataNotifier.getUserProfileData(publicKey.trim());Optional debounce (minimal diff, requires
import 'dart:async';and aTimer? _debounce;field):- if (_isValidPublicKey(_searchQuery)) { - _fetchMetadataForPublicKey(_searchQuery); - } + if (_isValidPublicKey(_searchQuery)) { + _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 300), () { + _fetchMetadataForPublicKey(_searchQuery); + }); + }
380-474: Guard the debug panel behind kDebugMode.
Prevents exposing raw data in production.- if (_searchQuery.toLowerCase() == 'debug') ...[ + if (kDebugMode && _searchQuery.toLowerCase() == 'debug') ...[Also add:
+ import 'package:flutter/foundation.dart';near the other imports.
31-40: Localize strings per guidelines.
Move 'New chat', 'Retry', 'Loading metadata…', etc., to AppLocalizations.Also applies to: 189-209, 214-256, 262-269, 271-298, 336-362, 485-503
277-284: Explicitly annotate types for clarity- final userProfileDataNotifier = ref.read(userProfileDataProvider.notifier); - final supportUserProfileData = await userProfileDataNotifier.getUserProfileData(kSupportNpub); + final UserProfileDataNotifier userProfileDataNotifier = + ref.read(userProfileDataProvider.notifier); + final ContactModel supportUserProfileData = + await userProfileDataNotifier.getUserProfileData(kSupportNpub);lib/ui/contact_list/chat_list_screen.dart (2)
382-389: Localize hardcoded strings.
'Checking for new messages…', 'No Relays Connected', 'Connect Relays', 'Search Chats' should go through AppLocalizations.Also applies to: 463-479, 403-435
248-256: Type local collections for clarity.
AnnotategroupList,welcomesList, andfilteredChatItemswith concrete types per code-style.Also applies to: 289-295
lib/config/providers/group_provider.dart (11)
153-160: TypeactivePubkeyexplicitly.Follow code style (no implicit types in Dart files).
- final activePubkey = ref.read(activePubkeyProvider); + final String? activePubkey = ref.read(activePubkeyProvider);
298-306: TypeactivePubkeyexplicitly.Applies same guideline in members loader.
- final activePubkey = ref.read(activePubkeyProvider); + final String? activePubkey = ref.read(activePubkeyProvider);
377-386: TypeactivePubkeyexplicitly.Same style compliance.
- final activePubkey = ref.read(activePubkeyProvider); + final String? activePubkey = ref.read(activePubkeyProvider);
491-496: TypeactivePubkeyexplicitly.Keeps consistency.
- final activePubkey = ref.read(activePubkeyProvider); + final String? activePubkey = ref.read(activePubkeyProvider);
592-606: Avoid repeated FFI calls: prefer cached group type when available.You already cache types; use it to avoid extra
getGroupInformationcalls.- final groupInformation = await getGroupInformation(groupId: group.mlsGroupId); - if (groupInformation.groupType == GroupType.directMessage) { + final cachedType = getCachedGroupType(group.mlsGroupId); + final GroupType groupType = + cachedType ?? (await getGroupInformation(groupId: group.mlsGroupId)).groupType; + if (groupType == GroupType.directMessage) {
718-727: TypeactivePubkeyexplicitly and simplify.- final activePubkey = ref.read(activePubkeyProvider); + final String? activePubkey = ref.read(activePubkeyProvider);
763-770: TypeactivePubkeyexplicitly and pass it through.Style compliance; rest looks good.
- final activePubkey = ref.read(activePubkeyProvider); + final String? activePubkey = ref.read(activePubkeyProvider);Also applies to: 809-869
879-896: TypeactivePubkeyexplicitly.- final activePubkey = ref.read(activePubkeyProvider); + final String? activePubkey = ref.read(activePubkeyProvider); @@ - await addMembersToGroup( - pubkey: activePubkey, + await addMembersToGroup( + pubkey: activePubkey,
934-951: TypeactivePubkeyexplicitly.- final activePubkey = ref.read(activePubkeyProvider); + final String? activePubkey = ref.read(activePubkeyProvider); @@ - await removeMembersFromGroup( - pubkey: activePubkey, + await removeMembersFromGroup( + pubkey: activePubkey,
330-337: Localize user-facing fallback strings.Replace hardcoded 'Unknown User' and 'Direct Message' with AppLocalizations entries.
Also applies to: 607-621
62-69: DRY: centralize active pubkey retrieval and error handling.Add a small helper to read/validate the active key and set the standard error.
Example helper (add inside GroupsNotifier):
String? _requireActivePubkey({bool setError = true}) { final String? key = ref.read(activePubkeyProvider); if ((key == null || key.isEmpty) && setError) { state = state.copyWith(error: 'No active account found'); } return key; }Then replace repeated reads with
final String? activePubkey = _requireActivePubkey();.Also applies to: 298-306, 378-386, 492-496, 720-727, 764-770, 879-886, 934-941
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (28)
CHANGELOG.md(1 hunks)docs/METADATA_CACHING_SOLUTION.md(0 hunks)lib/config/providers/active_account_provider.dart(2 hunks)lib/config/providers/chat_provider.dart(13 hunks)lib/config/providers/follows_provider.dart(3 hunks)lib/config/providers/group_provider.dart(16 hunks)lib/config/providers/metadata_cache_provider.dart(2 hunks)lib/config/providers/nostr_keys_provider.dart(3 hunks)lib/config/providers/polling_provider.dart(3 hunks)lib/config/providers/relay_status_provider.dart(1 hunks)lib/config/providers/user_profile_data_provider.dart(1 hunks)lib/config/providers/welcomes_provider.dart(5 hunks)lib/domain/services/dm_chat_service.dart(2 hunks)lib/examples/metadata_cache_examples.dart(0 hunks)lib/ui/chat/chat_info/chat_info_screen.dart(1 hunks)lib/ui/chat/chat_info/dm_chat_info.dart(1 hunks)lib/ui/chat/invite/chat_invite_screen.dart(3 hunks)lib/ui/contact_list/chat_list_screen.dart(2 hunks)lib/ui/contact_list/group_welcome_invitation_sheet.dart(4 hunks)lib/ui/contact_list/new_chat_bottom_sheet.dart(4 hunks)lib/ui/contact_list/search_chat_bottom_sheet.dart(2 hunks)lib/ui/contact_list/widgets/chat_list_active_account_avatar.dart(1 hunks)lib/ui/contact_list/widgets/welcome_tile.dart(2 hunks)lib/ui/settings/developer/developer_settings_screen.dart(2 hunks)lib/ui/settings/general_settings_screen.dart(2 hunks)lib/utils/metadata_cache_utils.dart(0 hunks)rust/src/api/utils.rs(1 hunks)test/config/providers/user_profile_data_provider_test.dart(1 hunks)
💤 Files with no reviewable changes (3)
- docs/METADATA_CACHING_SOLUTION.md
- lib/utils/metadata_cache_utils.dart
- lib/examples/metadata_cache_examples.dart
🧰 Additional context used
📓 Path-based instructions (3)
**/*.dart
📄 CodeRabbit inference engine (.cursor/rules/flutter.mdc)
**/*.dart: Always declare the type of each variable and function (parameters and return value). Avoid using 'any'. Create necessary types.
Don't leave blank lines within a function.
One export per file.
Use PascalCase for classes.
Use camelCase for variables, functions, and methods.
Use underscores_case for file and directory names.
Use UPPERCASE for environment variables. Avoid magic numbers and define constants.
Start each function with a verb.
Use verbs for boolean variables. Example: isLoading, hasError, canDelete, etc.
Use complete words instead of abbreviations and correct spelling, except for standard and well-known abbreviations (API, URL, i, j, err, ctx, req, res, next).
Write short functions with a single purpose. Less than 20 instructions.
Name functions with a verb and something else. If it returns a boolean, use isX or hasX, canX, etc. If it doesn't return anything, use executeX or saveX, etc.
Avoid nesting blocks by early checks and returns, or extraction to utility functions.
Use higher-order functions (map, filter, reduce, etc.) to avoid function nesting. Use arrow functions for simple functions (less than 3 instructions). Use named functions for non-simple functions.
Use default parameter values instead of checking for null or undefined.
Reduce function parameters using RO-RO: use an object to pass multiple parameters and to return results. Declare necessary types for input arguments and output.
Use a single level of abstraction in functions.
Don't abuse primitive types and encapsulate data in composite types.
Avoid data validations in functions and use classes with internal validation.
Prefer immutability for data. Use readonly for data that doesn't change. Use 'as const' for literals that don't change.
Declare interfaces to define contracts.
Write small classes with a single purpose. Less than 200 instructions, less than 10 public methods, less than 10 properties.
Use exceptions to handle errors you don't expect. If you catch an exception, it sh...
Files:
lib/ui/chat/chat_info/dm_chat_info.dartlib/config/providers/polling_provider.dartlib/config/providers/active_account_provider.dartlib/ui/contact_list/widgets/chat_list_active_account_avatar.dartlib/config/providers/metadata_cache_provider.dartlib/ui/chat/chat_info/chat_info_screen.dartlib/config/providers/follows_provider.dartlib/config/providers/chat_provider.dartlib/ui/contact_list/search_chat_bottom_sheet.dartlib/config/providers/relay_status_provider.dartlib/config/providers/user_profile_data_provider.dartlib/config/providers/nostr_keys_provider.dartlib/ui/contact_list/widgets/welcome_tile.dartlib/ui/chat/invite/chat_invite_screen.dartlib/config/providers/group_provider.dartlib/ui/contact_list/new_chat_bottom_sheet.dartlib/domain/services/dm_chat_service.dartlib/ui/contact_list/group_welcome_invitation_sheet.dartlib/ui/contact_list/chat_list_screen.dartlib/config/providers/welcomes_provider.darttest/config/providers/user_profile_data_provider_test.dartlib/ui/settings/general_settings_screen.dartlib/ui/settings/developer/developer_settings_screen.dart
**/*.md
📄 CodeRabbit inference engine (.cursor/rules/whitenoise.mdc)
**/*.md: NIPs (Nostr Implementation Possibilities) are numbered likeNIP-XXwhereXXare two capitalized hexadecimal digits, e.g.,NIP-01andNIP-C7.
To read a specific NIP, construct the NIP URL following this template:https://raw.githubusercontent.com/nostr-protocol/nips/refs/heads/master/{nip}.md(replace{nip}in the URL template with the relevant NIP name, e.g.,07for NIP-07, orC7for NIP-C7).
To read the definition of a specific kind, construct a URL following this template:https://nostrbook.dev/kinds/{kind}.md(replace{kind}in the template with the kind number, e.g.,https://nostrbook.dev/kinds/0.mdfor kind 0).
Files:
CHANGELOG.md
**/*_test.dart
📄 CodeRabbit inference engine (.cursor/rules/flutter.mdc)
**/*_test.dart: Follow the Arrange-Act-Assert convention for tests.
Name test variables clearly. Follow the convention: inputX, mockX, actualX, expectedX, etc.
Write unit tests for each public function. Use test doubles to simulate dependencies, except for third-party dependencies that are not expensive to execute.
Write acceptance tests for each module. Follow the Given-When-Then convention.
Use the standard widget testing for Flutter.
Files:
test/config/providers/user_profile_data_provider_test.dart
🧠 Learnings (8)
📚 Learning: 2025-08-12T11:21:53.640Z
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#455
File: lib/ui/settings/profile/switch_profile_bottom_sheet.dart:0-0
Timestamp: 2025-08-12T11:21:53.640Z
Learning: In the whitenoise_flutter codebase, ContactModel.publicKey can be stored in either npub format (from metadata cache) or hex format (from account storage). The activeAccountProvider may return either format depending on how the account was originally stored, so normalization to hex format is required when comparing with other hex-normalized keys in sorting logic.
Applied to files:
lib/ui/chat/chat_info/dm_chat_info.dartlib/config/providers/active_account_provider.dartlib/config/providers/metadata_cache_provider.dartlib/config/providers/chat_provider.dartlib/ui/contact_list/search_chat_bottom_sheet.dartlib/config/providers/nostr_keys_provider.dartlib/config/providers/group_provider.dartlib/ui/contact_list/new_chat_bottom_sheet.dartlib/domain/services/dm_chat_service.dartlib/ui/contact_list/group_welcome_invitation_sheet.dartlib/ui/settings/general_settings_screen.dart
📚 Learning: 2025-08-08T13:39:00.500Z
Learnt from: CR
PR: parres-hq/whitenoise_flutter#0
File: .cursor/rules/flutter.mdc:0-0
Timestamp: 2025-08-08T13:39:00.500Z
Learning: Applies to **/*.dart : Use Riverpod to manage state. Use StreamProviders to watch for state changes that come from the rust api. See keepAlive if you need to keep the state alive.
Applied to files:
lib/config/providers/polling_provider.dartlib/config/providers/active_account_provider.dartlib/config/providers/metadata_cache_provider.dartlib/config/providers/follows_provider.dartlib/config/providers/chat_provider.dartlib/config/providers/relay_status_provider.dartlib/config/providers/user_profile_data_provider.dartlib/config/providers/nostr_keys_provider.dartlib/ui/contact_list/widgets/welcome_tile.dartlib/config/providers/group_provider.dartlib/config/providers/welcomes_provider.dart
📚 Learning: 2025-08-08T13:39:00.500Z
Learnt from: CR
PR: parres-hq/whitenoise_flutter#0
File: .cursor/rules/flutter.mdc:0-0
Timestamp: 2025-08-08T13:39:00.500Z
Learning: Applies to **/*.dart : Use the whitenoise rust crate (via flutter_rust_bridge) as the source of all data and only way to trigger changes in our data model.
Applied to files:
lib/config/providers/active_account_provider.dartlib/config/providers/nostr_keys_provider.dartlib/ui/chat/invite/chat_invite_screen.dartlib/config/providers/group_provider.dartlib/ui/contact_list/new_chat_bottom_sheet.dartlib/ui/contact_list/group_welcome_invitation_sheet.dartlib/ui/settings/general_settings_screen.dart
📚 Learning: 2025-08-23T11:02:28.308Z
Learnt from: Quwaysim
PR: parres-hq/whitenoise_flutter#527
File: lib/ui/core/ui/wn_avatar.dart:1-6
Timestamp: 2025-08-23T11:02:28.308Z
Learning: In the whitenoise_flutter codebase, flutter_screenutil is re-exported through lib/ui/core/themes/src/app_theme.dart, so files that import app_theme.dart do not need to directly import flutter_screenutil to use .w, .h, .sp extensions.
Applied to files:
lib/ui/contact_list/group_welcome_invitation_sheet.dartlib/ui/settings/developer/developer_settings_screen.dart
📚 Learning: 2025-08-08T13:39:00.500Z
Learnt from: CR
PR: parres-hq/whitenoise_flutter#0
File: .cursor/rules/flutter.mdc:0-0
Timestamp: 2025-08-08T13:39:00.500Z
Learning: Applies to **/*.dart : Use flutter_rust_bridge to access core functionality of the app.
Applied to files:
lib/ui/contact_list/group_welcome_invitation_sheet.dart
📚 Learning: 2025-09-01T14:56:50.988Z
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#568
File: lib/ui/core/ui/wn_image.dart:1-2
Timestamp: 2025-09-01T14:56:50.988Z
Learning: The whitenoise_flutter project does not target web platforms and any future web version would be in a separate repository, so dart:io imports and file system operations are acceptable in this codebase.
Applied to files:
lib/ui/contact_list/group_welcome_invitation_sheet.dartlib/ui/settings/developer/developer_settings_screen.dart
📚 Learning: 2025-08-08T13:39:00.500Z
Learnt from: CR
PR: parres-hq/whitenoise_flutter#0
File: .cursor/rules/flutter.mdc:0-0
Timestamp: 2025-08-08T13:39:00.500Z
Learning: Applies to **/*_test.dart : Write unit tests for each public function. Use test doubles to simulate dependencies, except for third-party dependencies that are not expensive to execute.
Applied to files:
test/config/providers/user_profile_data_provider_test.dart
📚 Learning: 2025-08-08T13:39:00.500Z
Learnt from: CR
PR: parres-hq/whitenoise_flutter#0
File: .cursor/rules/flutter.mdc:0-0
Timestamp: 2025-08-08T13:39:00.500Z
Learning: Applies to **/*.dart : Use extensions to manage reusable code.
Applied to files:
lib/ui/settings/developer/developer_settings_screen.dart
| try { | ||
| final activeAccountState = await ref.read(activeAccountProvider.future); | ||
| final activeAccount = activeAccountState.account; | ||
| final activePubkey = ref.read(activePubkeyProvider); | ||
|
|
||
| if (activeAccount == null) { | ||
| if (activePubkey == null || activePubkey.isEmpty) { | ||
| state = state.copyWith(error: 'No active account found', isLoading: false); | ||
| return; | ||
| } | ||
|
|
||
| final groups = await activeGroups(pubkey: activeAccount.pubkey); | ||
| final groups = await activeGroups(pubkey: activePubkey); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Use explicit typing and consistently operate on the sorted list.
- Declare
activePubkeywith an explicit type to follow our Dart guidelines. - After sorting, subsequent operations should use
sortedGroups(members/types/display names/message prefetch) to avoid accidental divergence.
- final activePubkey = ref.read(activePubkeyProvider);
+ final String? activePubkey = ref.read(activePubkeyProvider);
@@
- await _loadMembersForAllGroups(groups);
+ await _loadMembersForAllGroups(sortedGroups);
@@
- await _loadGroupTypesForAllGroups(groups);
+ await _loadGroupTypesForAllGroups(sortedGroups);
@@
- await _calculateDisplayNames(groups, activePubkey);
+ await _calculateDisplayNames(sortedGroups, activePubkey);
@@
- .loadMessagesForGroups(
- groups.map((g) => g.mlsGroupId).toList(),
- );
+ .loadMessagesForGroups(
+ sortedGroups.map((g) => g.mlsGroupId).toList(),
+ );Optional: wrap the microtask body in try/catch to avoid unhandled async errors.
Also applies to: 110-118, 120-126
🤖 Prompt for AI Agents
In lib/config/providers/group_provider.dart around lines 79 to 88, declare
activePubkey with an explicit type (e.g., String? activePubkey =
ref.read(activePubkeyProvider);) to follow Dart guidelines, and after sorting
groups assign to a new variable sortedGroups and use that sortedGroups for all
subsequent operations (members/types/display names/message prefetch) to ensure
consistency and avoid divergence; also apply the same explicit-typing and
use-of-sortedGroups fixes in the other affected ranges (lines 110-118 and
120-126), and optionally wrap the microtask body in a try/catch to prevent
unhandled async errors.
| final activeAccount = activeAccountState.account; | ||
| if (activeAccount == null) { | ||
| final activePubkey = ref.read(activePubkeyProvider); | ||
| if (activePubkey == null || activePubkey.isEmpty) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can try using this extension method .nullOrEmpty
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for the suggestion! I'll replace it!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ended up applying @Quwaysim suggestion commented below of using null promotion
| return otherMembers.first; | ||
| } | ||
|
|
||
| Future<ContactModel?> getFirstOtherMember(String? groupId, String? currentUserNpub) async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this no longer being used ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, it wasn't used anywhere
| import 'package:whitenoise/src/rust/api/users.dart' as wn_users_api; | ||
| import 'package:whitenoise/src/rust/api/users.dart' show User; | ||
|
|
||
| abstract class WnUsersApi { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I dont think using an abstract is realy needed in this case. we only have one method for now. and its a one on one mapping with what the rust method provides. so maybe for now just use the rust method direclty in the provider ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I call the rust method directly inside the provider, it becomes very hard to test. The idea behind adding dependency injection is to make it easier to write tests with mocks. I’ll try a different approach to see if I can achieve the same with a const, while still keeping the provider testable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed the abstract class and instead added the dependency injection in the notifier constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I call the rust method directly inside the provider, it becomes very hard to test. The idea behind adding dependency injection is to make it easier to write tests with mocks. I’ll try a different approach to see if I can achieve the same with a const, while still keeping the provider testable.
I love that, this was one of the reasons i proposed having a datasource to handle api calls.
we can come back to this later and do it in a clean unified way.
| final activeAccountState = await ref.read(activeAccountProvider.future); | ||
| final activeAccount = activeAccountState.account; | ||
| if (activeAccount == null) { | ||
| final activePubkey = ref.read(activePubkeyProvider); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just FYI, no need to make any changes, you could do null promotion by writing:
final activePubkey = ref.read(activePubkeyProvider)?? '';
if (activePubkey.isEmpty){......}
Also, just for code asthetics 😂 I prefer activePubKey as pub and key are initially two distinct words.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll check that out. @codeswot proposed to use .nullOrEmpty and I liked that one, I think it will look prettier
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regarding the pubkey name: in the Account and User classes that come from the Rust side (not the user model, but users.dart and account.dart), the attribute is named pubkey as a single word. For consistency with those classes, I’d prefer to stick with pubkey here as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to null promotion, I ended up choosing your suggestion. The .nullOrEmpty required to add it to string extension file but for String? instead of the existing ones for String and also gave me warnings after cause the compiler god confused thinking that I was passing a String? to an argument that could only be a String. Null promotion works better in this case I think, so thanks ❤️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to null promotion, I ended up choosing your suggestion. The .nullOrEmpty required to add it to string extension file but for String? instead of the existing ones for String and also gave me warnings after cause the compiler god confused thinking that I was passing a String? to an argument that could only be a String. Null promotion works better in this case I think, so thanks ❤️
You mean when using activePubkey string in the npubFromHexPubkey() method, it complains that it could be null.
I agree Null promotion works better here as it keeps the context of the variable being checked if null
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I meant than then trying to use .nullOrEmpty , I needed to add it as a String? extension. And then after it still complained that the string could be null (although it couldn't cause this was checked before)
| // Load npub and nsec directly from hex pubkey string | ||
| final npubString = await npubFromHexPubkey(hexPubkey: activeAccount.pubkey); | ||
| final nsecString = await exportAccountNsec(pubkey: activeAccount.pubkey); | ||
| final npubString = await npubFromHexPubkey(hexPubkey: activePubkey); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We already have the npub. Is this still needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the variable name is a bit misleading and I can change it, npubFromHexPubkey is actually converting from hex format to bech32. I’ll double-check if this conversion is really needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I renamed it to activeNpub and activeNsec respectively to avoid confusions
| if (activeAccount != null) { | ||
| final activePubkey = ref.read(activePubkeyProvider); | ||
| if (activePubkey != null && activePubkey.isNotEmpty) { | ||
| final currentUserNpub = await npubFromHexPubkey( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
activePubkey is already a String npub
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but in hex format
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m considering renaming activePubkeyProvider to hexActivePubkeyProvider. I’m not a fan of making the name longer, but it would make it clearer that the value is in hex rather than bech32. Do you think that change would help? 🤔 @Quwaysim
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@josefinalliende Apologies for the late response. I think the name will be unnecessarily long. We could add a doc to the provider noting that it's returning npub in hex rather than bech32. Many of the usages I've used personally are in hex.
da194a4 to
fc563a3
Compare
6b658fc to
12c611e
Compare
|
@codeswot @Quwaysim Ithink I’ve addressed almost all of your comments, along with a few from CodeRabbit. I added support to the user profile data provider for handling both hex and bech32 pubkeys. This required changes in the ContactModel, public key extension methods, and some widgets — since the ContactModel now needs to be built with await (because it uses the Rust crate to transform hex into npub). This also fixes the issue that in some widgets npub was being shown in hex format instead of npub format. |
|
Oh no, tests are failing 😢 ... will fix them tomorrow then 😴 |
f0e57d2 to
608e91f
Compare
By making the pubkey conversion methods synchronous, I no longer need to update widgets with FutureBuilder to build contact models. The issue with npubs showing as hex is still fixed, but now in a much simpler way 😄. I also updated the change list in the PR description to make it easier to re-review |
aa3d859 to
0be4451
Compare
0be4451 to
0dc094b
Compare
0dc094b to
5f13985
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (13)
lib/ui/chat/chat_info/widgets/group_member_bottom_sheet.dart (2)
81-87: Ensure removeFromGroup receives NPUBs.Param name
membersNpubsimplies npub; passing hex could fail server-side.- membersNpubs: [widget.member.publicKey], + membersNpubs: [widget.member.publicKey.toNpub() ?? widget.member.publicKey],
186-193: Normalize formats when checking admin status.Avoid false negatives if admin list and current user differ in hex/npub.
- (admin) => admin.publicKey == currentUserNpub, + (admin) => + ((admin.publicKey.toNpub() ?? admin.publicKey) == currentUserNpub),lib/ui/contact_list/new_group_chat_sheet.dart (1)
117-122: Normalize pubkeys before excluding current user.Avoids self-inclusion when formats differ (hex vs npub).
- final contactsWithoutCreator = - contacts.where((contact) { - // Compare public keys, ensuring both are trimmed and lowercased for comparison - return currentUserPubkey == null || - contact.publicKey.trim().toLowerCase() != currentUserPubkey.trim().toLowerCase(); - }).toList(); + final contactsWithoutCreator = contacts.where((contact) { + if (currentUserPubkey == null) return true; + final contactKey = contact.publicKey.toNpub() ?? contact.publicKey; + final currentKey = currentUserPubkey.toNpub() ?? currentUserPubkey; + return contactKey.trim().toLowerCase() != currentKey.trim().toLowerCase(); + }).toList();Add import:
+import 'package:whitenoise/utils/public_key_validation_extension.dart';lib/utils/message_converter.dart (1)
3-3: Blocker: still depends on metadata_cache_provider; migrate to userProfileDataProvider.PR removes the metadata cache provider; these references will break. Switch to the new provider.
-import 'package:whitenoise/config/providers/metadata_cache_provider.dart'; +import 'package:whitenoise/config/providers/user_profile_data_provider.dart'; @@ - return await Future.microtask(() async { - final metadataCache = ref.read(metadataCacheProvider.notifier); - final contactModel = await metadataCache.getContactModel(pubkey); - return User( + return await Future.microtask(() async { + final contactModel = await ref.read(userProfileDataProvider(pubkey).future); + return User( id: pubkey, displayName: contactModel.displayName, nip05: contactModel.nip05 ?? '', publicKey: pubkey, imagePath: contactModel.imagePath, ); }); @@ - final metadataCache = ref.read(metadataCacheProvider.notifier); - final userFutures = uniquePubkeys.map( - (pubkey) => - metadataCache.getContactModel(pubkey).then((contact) => MapEntry(pubkey, contact)), - ); + final userFutures = uniquePubkeys.map( + (pubkey) => ref.read(userProfileDataProvider(pubkey).future) + .then((contact) => MapEntry(pubkey, contact)), + );Also applies to: 531-541, 560-566
lib/ui/settings/profile/switch_profile_bottom_sheet.dart (2)
96-101: Active key normalization bug: _activeAccountHex may not be hex.You store whatever
activePubkeyProviderreturns (npub or hex) into_activeAccountHex. Later comparisons are against hex, causing mismatches. Normalize to hex here.- final activeAccountPubkey = ref.read(activePubkeyProvider) ?? ''; - if (activeAccountPubkey.isNotEmpty) { - setState(() { - _activeAccountHex = activeAccountPubkey; - }); - } + final String activeAccountPubkey = ref.read(activePubkeyProvider) ?? ''; + if (activeAccountPubkey.isEmpty) return; + final String activeHex = activeAccountPubkey.startsWith('npub1') + ? hexPubkeyFromNpub(npub: activeAccountPubkey) + : activeAccountPubkey; + setState(() { + _activeAccountHex = activeHex; + });
104-119: Make _isActiveAccount synchronous and remove FutureBuilder rebuild churn.All conversions are synchronous now. Replace the async method and per-row
FutureBuilderwith a direct boolean. This reduces list rebuild “blinking” and simplifies logic.Proposed changes (outside selected lines):
- Replace the method:
bool _isActiveAccountSync(ContactModel profile) { final String aHex = _activeAccountHex ?? ''; if (aHex.isEmpty) return false; final String pHex = _pubkeyToHex[profile.publicKey] ?? profile.publicKey; return aHex == pHex; }
- Replace
FutureBuilder<bool>at Lines 179-207 with a simpleBuilder:child: Builder( builder: (context) { final bool isActiveAccount = _isActiveAccountSync(profile); return Container( decoration: isActiveAccount ? BoxDecoration(color: context.colors.primary.withValues(alpha: 0.1)) : null, // ... ); }, ),lib/config/providers/nostr_keys_provider.dart (1)
66-72: Bug: npub field should store bech32, not raw input.If pubkey is hex, storing it directly in npub will surface hex where npub is expected.
void loadPublicKeyFromAccount(String pubkey) { state = state.copyWith( - npub: pubkey, + npub: pubkey.toNpub(), error: null, ); }lib/ui/contact_list/group_welcome_invitation_sheet.dart (1)
291-291: Missing await means errors bypass try/catchWithout await, exceptions from userMetadata won’t be caught here.
- return wn_users_api.userMetadata(pubkey: widget.welcome.welcomer); + return await wn_users_api.userMetadata(pubkey: widget.welcome.welcomer);lib/config/providers/chat_provider.dart (1)
309-361: Bug: mixed “append-only” path drops content changes (reactions/edits) when new messages also arriveIf both content changes and new messages exist, current code appends tail only and misses earlier updates. Split detection into “hasContentChanges” and “isPrefixAppend” and handle accordingly.
- bool hasChanges = false; + bool hasContentChanges = false; if (newMessages.length != currentMessages.length) { - // New or deleted messages - hasChanges = true; + // Potential new/deleted messages; further checks below } else if (newMessages.isNotEmpty && currentMessages.isNotEmpty) { // Check if any message content or reactions have changed for (int i = 0; i < newMessages.length; i++) { final newMsg = newMessages[i]; final currentMsg = currentMessages[i]; - if (newMsg.content != currentMsg.content || - newMsg.reactions.length != currentMsg.reactions.length || - !_areReactionsEqual(newMsg.reactions, currentMsg.reactions)) { - hasChanges = true; + if (newMsg.content != currentMsg.content || + newMsg.reactions.length != currentMsg.reactions.length || + !_areReactionsEqual(newMsg.reactions, currentMsg.reactions)) { + hasContentChanges = true; break; } } } - - if (hasChanges) { - if (newMessages.length > currentMessages.length) { - // Add only new messages to preserve performance - final newMessagesOnly = newMessages.skip(currentMessages.length).toList(); + // Check if currentMessages is a strict prefix of newMessages (append-only case) + bool isPrefixAppend = false; + if (newMessages.length > currentMessages.length) { + isPrefixAppend = true; + for (int i = 0; i < currentMessages.length; i++) { + if (newMessages[i].id != currentMessages[i].id) { + isPrefixAppend = false; + break; + } + } + } + + if (hasContentChanges || !isPrefixAppend) { + // Replace all to reflect edits/reactions/deletions/reorders + state = state.copyWith( + groupMessages: { + ...state.groupMessages, + groupId: newMessages, + }, + ); + _logger.info('ChatProvider: Updated messages with content changes for group $groupId'); + } else if (isPrefixAppend) { + // Append tail only + final newMessagesOnly = newMessages.skip(currentMessages.length).toList(); state = state.copyWith( groupMessages: { ...state.groupMessages, groupId: [...currentMessages, ...newMessagesOnly], }, ); - _logger.info( - 'ChatProvider: Added ${newMessagesOnly.length} new messages for group $groupId', - ); - } else { - // Replace all messages when there are content changes (reactions, edits, etc.) - state = state.copyWith( - groupMessages: { - ...state.groupMessages, - groupId: newMessages, - }, - ); - - _logger.info( - 'ChatProvider: Updated messages with content changes for group $groupId', - ); - } + _logger.info('ChatProvider: Added ${newMessagesOnly.length} new messages for group $groupId'); } // Update group order when messages are updated _updateGroupOrderForNewMessage(groupId);lib/config/providers/relay_provider.dart (1)
64-107: Prevent cross-account relay leakage during async loads (add stale-guards).If the active pubkey changes mid-flight, results for the previous account can overwrite state. Capture the pubkey before awaits and drop stale results after awaits, similar to the welcomes fix.
Apply this diff:
@@ - final activePubkey = ref.read(activePubkeyProvider) ?? ''; + final String activePubkey = ref.read(activePubkeyProvider) ?? ''; + final String capturedPubkey = activePubkey; @@ - final nip65RelayType = await relayTypeNip65(); - final relayUrls = await accountRelays( + final nip65RelayType = await relayTypeNip65(); + final relayUrls = await accountRelays( pubkey: activePubkey, relayType: nip65RelayType, ).then((value) => value.map((relay) => relay.url).toList()); + + // Active changed while fetching: drop stale batch + if (capturedPubkey != (ref.read(activePubkeyProvider) ?? '')) { + _logger.fine('NormalRelaysNotifier: Dropping stale load for $capturedPubkey'); + state = state.copyWith(isLoading: false); + return; + } @@ - if (statusState.relayStatuses.isEmpty && !statusState.isLoading) { + if (statusState.relayStatuses.isEmpty && !statusState.isLoading) { _logger.info('NormalRelaysNotifier: Loading relay statuses first'); await ref.read(relayStatusProvider.notifier).loadRelayStatuses(); } + + if (capturedPubkey != (ref.read(activePubkeyProvider) ?? '')) { + _logger.fine('NormalRelaysNotifier: Dropping stale status batch for $capturedPubkey'); + state = state.copyWith(isLoading: false); + return; + } @@ - _logger.info('NormalRelaysNotifier: Successfully loaded ${relayInfos.length} relays'); + if (capturedPubkey != (ref.read(activePubkeyProvider) ?? '')) { + _logger.fine('NormalRelaysNotifier: Active changed before commit; dropping $capturedPubkey results'); + return; + } + _logger.info('NormalRelaysNotifier: Successfully loaded ${relayInfos.length} relays');@@ - final accountPubKey = ref.read(activePubkeyProvider) ?? ''; + final String accountPubKey = ref.read(activePubkeyProvider) ?? ''; @@ - final inboxRelayType = await relayTypeInbox(); - final inboxRelayUrls = await accountRelays( + final inboxRelayType = await relayTypeInbox(); + final String capturedPubkey = accountPubKey; + final inboxRelayUrls = await accountRelays( pubkey: accountPubKey, relayType: inboxRelayType, ).then((value) => value.map((relay) => relay.url).toList()); + + if (capturedPubkey != (ref.read(activePubkeyProvider) ?? '')) { + _logger.fine('InboxRelaysNotifier: Dropping stale load for $capturedPubkey'); + state = state.copyWith(isLoading: false); + return; + } @@ - if (statusState.relayStatuses.isEmpty && !statusState.isLoading) { + if (statusState.relayStatuses.isEmpty && !statusState.isLoading) { _logger.info('InboxRelaysNotifier: Loading relay statuses first'); await ref.read(relayStatusProvider.notifier).loadRelayStatuses(); } + + if (capturedPubkey != (ref.read(activePubkeyProvider) ?? '')) { + _logger.fine('InboxRelaysNotifier: Dropping stale status batch for $capturedPubkey'); + state = state.copyWith(isLoading: false); + return; + } @@ - _logger.info('InboxRelaysNotifier: Successfully loaded ${relayInfos.length} relays'); + if (capturedPubkey != (ref.read(activePubkeyProvider) ?? '')) { + _logger.fine('InboxRelaysNotifier: Active changed before commit; dropping $capturedPubkey results'); + return; + } + _logger.info('InboxRelaysNotifier: Successfully loaded ${relayInfos.length} relays');@@ - final activePubkey = ref.read(activePubkeyProvider) ?? ''; + final String activePubkey = ref.read(activePubkeyProvider) ?? ''; + final String capturedPubkey = activePubkey; @@ - final keyPackageType = await relayTypeKeyPackage(); - final relayUrls = await accountRelays( + final keyPackageType = await relayTypeKeyPackage(); + final relayUrls = await accountRelays( pubkey: activePubkey, relayType: keyPackageType, ).then((value) => value.map((relay) => relay.url).toList()); + + if (capturedPubkey != (ref.read(activePubkeyProvider) ?? '')) { + _logger.fine('KeyPackageRelaysNotifier: Dropping stale load for $capturedPubkey'); + state = state.copyWith(isLoading: false); + return; + } @@ - _logger.info('KeyPackageRelaysNotifier: Successfully loaded ${relayInfos.length} relays'); + if (capturedPubkey != (ref.read(activePubkeyProvider) ?? '')) { + _logger.fine('KeyPackageRelaysNotifier: Active changed before commit; dropping $capturedPubkey results'); + return; + } + _logger.info('KeyPackageRelaysNotifier: Successfully loaded ${relayInfos.length} relays');Also applies to: 182-207, 306-347
lib/ui/chat/invite/chat_invite_screen.dart (1)
99-105: Avoid arbitrary 1s delay; wait for group availability with a timeout.Hard-coded sleeps are flaky. Await until the group is visible (or time out) before navigating.
- final groupsNotifier = ref.read(groupsProvider.notifier); - await groupsNotifier.loadGroups(); - await Future.delayed(const Duration(milliseconds: 1000)); - if (context.mounted) { - context.pushReplacement('/chats/${widget.groupId}'); - } + final groupsNotifier = ref.read(groupsProvider.notifier); + await groupsNotifier.loadGroups(); + final bool navigated = await _navigateWhenGroupAvailable( + context, + ref, + widget.groupId, + const Duration(seconds: 5), + ); + if (!navigated && context.mounted) { + context.pushReplacement('/chats/${widget.groupId}'); + }Add this helper inside
_ChatInviteScreenState:Future<bool> _navigateWhenGroupAvailable( BuildContext context, WidgetRef ref, String groupId, Duration timeout, ) async { final Stopwatch sw = Stopwatch()..start(); while (sw.elapsed < timeout) { final groupsState = ref.read(groupsProvider); final bool exists = (groupsState.groups ?? const []) .any((g) => g.id == groupId); if (exists && context.mounted) { context.pushReplacement('/chats/$groupId'); return true; } await Future.delayed(const Duration(milliseconds: 100)); } return false; }lib/config/providers/group_provider.dart (2)
262-266: Missingawaitone.messageText()in createNewGroup catch.Without
await, you log aFutureinstead of the error text.- final errorDetails = e.messageText(); + final String errorDetails = await e.messageText();
816-821: Missingawaitone.messageText()in checkForNewGroups catch.Same issue as above, leads to logging a
Future.- final errorDetails = e.messageText(); + final String errorDetails = await e.messageText();
♻️ Duplicate comments (1)
lib/config/providers/group_provider.dart (1)
110-118: Operate onsortedGroupsconsistently and guard the microtask.Use the sorted list for follow-up operations to avoid divergence. Wrap the microtask to prevent unhandled async errors.
- await _loadMembersForAllGroups(groups); + await _loadMembersForAllGroups(sortedGroups); @@ - await _loadGroupTypesForAllGroups(groups); + await _loadGroupTypesForAllGroups(sortedGroups); @@ - await _calculateDisplayNames(groups, activePubkey); + await _calculateDisplayNames(sortedGroups, activePubkey); @@ - .loadMessagesForGroups( - groups.map((g) => g.mlsGroupId).toList(), - ); + .loadMessagesForGroups( + sortedGroups.map((g) => g.mlsGroupId).toList(), + );Optionally:
- Future.microtask(() async { - await ref + Future.microtask(() async { + try { + await ref .read(chatProvider.notifier) .loadMessagesForGroups( sortedGroups.map((g) => g.mlsGroupId).toList(), ); + } catch (e, st) { + _logger.warning('Deferred loadMessagesForGroups failed', e, st); + } });Also applies to: 120-126
🧹 Nitpick comments (53)
lib/config/providers/polling_provider.dart (2)
79-79: Invalidate only after a successful initial load to avoid redundant rebuilds.
Consider moving this invalidation to the end of the initial load block (after the microtask completes) or coalescing it with the messages load completion to reduce extra rebuilds ofactiveAccountProvider.- ref.invalidate(activeAccountProvider); + // Coalesce with message preloads; ensure one refresh at the end. + // After the microtask: + Future.microtask(() async { + try { + // ...existing group/messages code... + } finally { + ref.invalidate(activeAccountProvider); + } + });
125-125: Guard against overlapping incremental ticks.
Timer.periodicevery 2s can overlap runs if a cycle takes >2s, causing duplicate work and repeated invalidations. Add a lightweight in-flight guard.class PollingNotifier extends Notifier<bool> { static final _logger = Logger('PollingNotifier'); Timer? _pollingTimer; bool _hasInitialDataLoaded = false; bool _isDisposed = false; + bool _isTickInFlight = false; // ... Future<void> _loadIncrementalData() async { if (!state) return; + if (_isTickInFlight) { + _logger.fine('Skip tick: previous incremental cycle still running'); + return; + } + _isTickInFlight = true; try { // ...existing incremental polling... - ref.invalidate(activeAccountProvider); + ref.invalidate(activeAccountProvider); } catch (e) { _logger.warning('Error during incremental polling: $e'); + } finally { + _isTickInFlight = false; } }lib/config/providers/active_account_provider.dart (1)
194-231: Also refresh after profile picture upload.
Uploading returns the URL but UI depending onactiveAccountProviderwon’t refresh until the next poll. Invalidate here to reflect the new avatar immediately._logger.fine( 'Successfully uploaded profile picture for pubkey: $activePubkey, URL: $profilePictureUrl', ); - return profilePictureUrl; + // Refresh account/metadata state so UI updates immediately. + ref.invalidateSelf(); + return profilePictureUrl;lib/ui/chat/chat_info/widgets/group_member_bottom_sheet.dart (2)
67-73: Make _loadCurrentUserNpub synchronous and avoid redundant setState.No awaits inside; also guard against unnecessary rebuilds.
Apply:
- void _loadCurrentUserNpub() async { - final activeAccountPubkey = ref.read(activePubkeyProvider) ?? ''; - if (activeAccountPubkey.isNotEmpty) { - currentUserNpub = activeAccountPubkey.toNpub() ?? ''; - setState(() {}); - } - } + void _loadCurrentUserNpub() { + final String? activePubkey = ref.read(activePubkeyProvider); + if (activePubkey == null || activePubkey.isEmpty) return; + final String next = activePubkey.toNpub() ?? activePubkey; + if (next == currentUserNpub) return; + setState(() { + currentUserNpub = next; + }); + }
49-57: Copy normalized npub (or original) and fix misleading local name.Ensures clipboard gets npub when possible and improves readability.
- void _copyToClipboard() { - final npub = widget.member.publicKey; + void _copyToClipboard() { + final String pubkeyToCopy = widget.member.publicKey.toNpub() ?? widget.member.publicKey; ClipboardUtils.copyWithToast( ref: ref, - textToCopy: npub, + textToCopy: pubkeyToCopy, successMessage: 'Public key copied', noTextMessage: 'No public key to copy', ); }lib/ui/contact_list/widgets/contact_list_tile.dart (3)
34-42: Drop async and use the pubkey extension for format-agnostic conversion.Removes unnecessary Future + try/catch, avoids UI flicker.
- Future<String> _getNpub(String publicKeyHex) async { - try { - final npub = npubFromHexPubkey(hexPubkey: publicKeyHex); - return npub.formatPublicKey(); - } catch (e) { - // Return the full hex key as fallback - return publicKeyHex.formatPublicKey(); - } - } + String _formatPubkey(String publicKey) { + final String? npub = publicKey.toNpub(); + return (npub ?? publicKey).formatPublicKey(); + }Add/remove imports:
+import 'package:whitenoise/utils/public_key_validation_extension.dart'; -import 'package:whitenoise/src/rust/api/utils.dart';
80-112: Render synchronously; remove FutureBuilder to prevent unnecessary rebuilds.Leverages the synchronous conversion above.
- FutureBuilder<String>( - future: _getNpub(contact.publicKey), - builder: (context, snapshot) { - if (snapshot.hasData && snapshot.data!.isNotEmpty) { - return Text( - snapshot.data!, - style: TextStyle( - color: context.colors.mutedForeground, - fontSize: 12.sp, - fontFamily: 'monospace', - ), - ); - } else if (snapshot.hasError) { - return Text( - 'Error loading npub', - style: TextStyle( - color: context.colors.mutedForeground.withValues(alpha: 0.6), - fontSize: 12.sp, - fontStyle: FontStyle.italic, - ), - ); - } - // Show npub while loading immediately - return Text( - contact.publicKey.formatPublicKey(), - style: TextStyle( - color: context.colors.mutedForeground, - fontSize: 12.sp, - fontFamily: 'monospace', - ), - ); - }, - ), + Text( + _formatPubkey(contact.publicKey), + style: TextStyle( + color: context.colors.mutedForeground, + fontSize: 12.sp, + fontFamily: 'monospace', + ), + ),
151-153: Stabilize Dismissible key by normalizing.Prevents duplicated keys if formats differ.
- key: Key(contact.publicKey), + key: Key((contact.publicKey.toNpub() ?? contact.publicKey)),lib/utils/message_converter.dart (1)
18-18: Normalize pubkey equality checks for isMe and replies.Direct string equality is brittle if formats differ.
- final isMe = currentUserPublicKey != null && messageData.pubkey == currentUserPublicKey; + final isMe = currentUserPublicKey != null && + ((messageData.pubkey.toNpub() ?? messageData.pubkey) == + (currentUserPublicKey!.toNpub() ?? currentUserPublicKey));Add once at top:
+import 'package:whitenoise/utils/public_key_validation_extension.dart';Also applies to: 155-155, 246-246, 403-404
lib/utils/public_key_validation_extension.dart (3)
49-56: Tighten npub detection/validation or delegate to parser.
startsWith('npub1') && length > 10is permissive. Consider either:
- Delegating validation to the FRB conversion (attempt convert-in-try, return null on error), or
- Introducing a stricter check (e.g., bech32 charset/length) if you must short-circuit before FRB.
Current behavior is fine functionally (conversion still guards), but a stricter check would avoid unnecessary FRB calls on obviously invalid inputs.
53-55: Add explicit local types to match repo guidelines.Declare local types for readability and to follow the “Always declare the type” guideline.
- final trimmedPubkey = trim().toLowerCase(); + final String trimmedPubkey = trim().toLowerCase(); - final trimmedPubkey = trim().toLowerCase(); + final String trimmedPubkey = trim().toLowerCase();Also applies to: 74-76
18-19: Avoid re-allocating identical RegExp instances.Hoist the hex regex to a
static finalor top-levelfinaland reuse it in both places.Example (outside selected lines):
// top-level final RegExp _hex64 = RegExp(r'^[0-9a-fA-F]{64}$');Then:
return _hex64.hasMatch(trimmed);Also applies to: 33-34
lib/ui/settings/profile/switch_profile_bottom_sheet.dart (2)
64-81: _precomputeProfileHexes can be synchronous.No
awaitis used. Either dropasync/Futureor intentionally schedule it off the frame usingFuture.microtaskfor UI smoothness.Minimal change:
- Future<void> _precomputeProfileHexes() async { + void _precomputeProfileHexes() { // ... - if (mounted) setState(() {}); + if (mounted) setState(() {}); }
8-8: UI should not import FRB utils directly.Once you switch to the extension, remove
utils.dartimport here to align with our layering (UI -> extensions; extensions -> FRB).lib/config/providers/relay_status_provider.dart (1)
48-55: Clear stale errors when (re)loading.Keep UX clean by clearing
erroron new loads.- state = state.copyWith(isLoading: true); + state = state.copyWith(isLoading: true, error: null);lib/ui/chat/chat_info/group_chat_info.dart (2)
33-35: Normalize pubkey with.toNpub()- final npub = npubFromHexPubkey( - hexPubkey: groupDetails!.nostrGroupId, - ); + final npub = groupDetails!.nostrGroupId.toNpub() ?? '';The required extension import (
public_key_validation_extension.dart) is already inchat_info_screen.dart.
105-108: Normalize activePubkey with.toNpub()
Replace manualnpubFromHexPubkey(...)with thetoNpub()extension (handles both HEX and NPUB, returns null on failure), for example:- final activeAccount = ref.read(activePubkeyProvider) ?? ''; - if (activeAccount.isNotEmpty) { - final currentUserNpub = npubFromHexPubkey(hexPubkey: activeAccount); + final String activePubkey = ref.read(activePubkeyProvider) ?? ''; + final String? npub = activePubkey.toNpub(); + if (npub?.isNotEmpty ?? false) { + final currentUserNpub = npub;Also verify that any
member.publicKeyyou compare against is already in NPUB format.lib/ui/settings/profile/share_profile_screen.dart (1)
40-42: Normalize safely and avoid returningFuturewhen not awaitingAlso fall back to the original value if normalization returns null; consider listening to updates so switching profiles refreshes
npub.Apply:
- Future<void> loadProfile() async { + void loadProfile() { try { - final activeAccountPubkey = ref.read(activePubkeyProvider) ?? ''; - npub = activeAccountPubkey.toNpub() ?? ''; + final String activeAccountPubkey = ref.read(activePubkeyProvider) ?? ''; + npub = activeAccountPubkey.toNpub() ?? activeAccountPubkey; setState(() {});Optional (outside the shown lines): in
initState, add a listener to update on account switch:late final removeListener = ref.listen<String?>(activePubkeyProvider, (_, next) { final normalized = (next ?? '').toNpub() ?? (next ?? ''); if (mounted && npub != normalized) setState(() => npub = normalized); }).close; @override void dispose() { removeListener(); super.dispose(); }lib/domain/models/contact_model.dart (1)
26-28: Clarify parameter semanticsIf
pubkeyaccepts either HEX or NPUB, add a short docstring to make the accepted formats explicit and the canonical output (publicKey) format clear.lib/config/providers/nostr_keys_provider.dart (2)
33-41: Add explicit type and adjust message.
- Per codebase guidelines, declare local variable types.
- Message should reflect the actual condition (no pubkey vs no account).
- final activePubkey = ref.read(activePubkeyProvider) ?? ''; + final String activePubkey = ref.read(activePubkeyProvider) ?? ''; if (activePubkey.isEmpty) { - _logger.severe('NostrKeysNotifier: No active account found'); + _logger.severe('NostrKeysNotifier: No active pubkey found'); state = state.copyWith( isLoading: false, - error: 'No active account found', + error: 'No active pubkey found', ); return; }
18-64: Formatting nits inside functions.Project guideline says no blank lines within functions; consider tightening spacing in loadKeys for consistency.
rust/src/api/utils.rs (1)
17-21: Clarify accepted input formats in docs.PublicKey::parse accepts hex and bech32; document this to prevent misuse and surprises.
-#[frb(sync)] -pub fn hex_pubkey_from_npub(npub: &str) -> Result<String, ApiError> { +/// Converts a bech32 (npub) or hex pubkey string to hex. +/// Accepts either format; if hex is provided, it is returned normalized. +#[frb(sync)] +pub fn hex_pubkey_from_npub(npub: &str) -> Result<String, ApiError> { let pubkey = PublicKey::parse(npub).map_err(ApiError::from)?; Ok(pubkey.to_hex()) }lib/ui/contact_list/group_welcome_invitation_sheet.dart (4)
116-118: Deduplicate the “activePubkey gating” patternThis guard appears three times. Extract a tiny helper (e.g., _readActivePubkeyOrToast()) to DRY this and keep error text consistent.
- final activePubkey = ref.read(activePubkeyProvider) ?? ''; + final String activePubkey = ref.read(activePubkeyProvider) ?? '';Also applies to: 243-245, 286-288
129-131: Prefer the String.toNpub() extension; drop unnecessary async wrappersnpubFromHexPubkey is no longer needed here; use the extension to normalize either hex or npub and make these helpers synchronous.
- Future<String> _getDisplayablePublicKey() async { - try { - final npub = npubFromHexPubkey(hexPubkey: widget.welcome.nostrGroupId); - return npub; - } catch (e) { - return widget.welcome.nostrGroupId.formatPublicKey(); - } - } + String _getDisplayablePublicKey() { + try { + return widget.welcome.nostrGroupId.toNpub(); + } catch (_) { + return widget.welcome.nostrGroupId.formatPublicKey(); + } + }- Future<String> _getDisplayablePublicKey() async { - try { - final npub = npubFromHexPubkey(hexPubkey: widget.welcome.welcomer); - return npub; - } catch (e) { - return widget.welcome.welcomer.formatPublicKey(); - } - } + String _getDisplayablePublicKey() { + try { + return widget.welcome.welcomer.toNpub(); + } catch (_) { + return widget.welcome.welcomer.formatPublicKey(); + } + }Also applies to: 297-303
208-222: Remove FutureBuilder now that key conversion is syncRender directly; avoids extra frames and simplifies UI.
- FutureBuilder<String>( - future: _getDisplayablePublicKey(), - builder: (context, npubSnapshot) { - final displayKey = npubSnapshot.data ?? widget.welcome.welcomer; - return Text( - displayKey.formatPublicKey(), - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14.sp, - fontWeight: FontWeight.w500, - color: context.colors.mutedForeground, - ), - ); - }, - ), + Text( + _getDisplayablePublicKey().formatPublicKey(), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w500, + color: context.colors.mutedForeground, + ), + ),Also applies to: 365-379
182-204: Leverage userProfileDataProvider instead of direct API callsSwitching to the provider improves caching and consistency with the PR’s direction.
Would you like a patch converting these FutureBuilders to Consumer widgets that watch userProfileDataProvider(welcomerPubkey)?
Also applies to: 256-268, 308-385
lib/config/providers/chat_provider.dart (3)
67-69: Type explicitly and consider a small helper for active pubkeyFollow codebase guideline to declare types, and centralize this guard.
- final activePubkey = ref.read(activePubkeyProvider) ?? ''; + final String activePubkey = ref.read(activePubkeyProvider) ?? '';Apply similarly at the other occurrences above.
Also applies to: 145-147, 287-289, 500-502, 573-575, 677-679
424-438: Avoid mutating the parameter with ??=; prefer a local with ??Improves readability and avoids side effects on the parameter.
- final gId = groupId ??= state.selectedGroupId; + final String? gId = groupId ?? state.selectedGroupId;Apply similarly in isNextSameSender.
123-128: Define constants for Nostr kinds (5, 7, 9) instead of magic numbersExtract to a shared constants file (e.g., nostr_kinds.dart) and use named constants for readability and maintainability.
Also applies to: 490-559, 562-663, 665-717
lib/config/providers/auth_provider.dart (2)
53-58: Use explicit types for locals per repo guidelinesDeclare the variable types explicitly.
- final storedActivePubkey = ref.read(activePubkeyProvider) ?? ''; + final String storedActivePubkey = ref.read(activePubkeyProvider) ?? ''; ... - final currentActivePubkey = ref.read(activePubkeyProvider); + final String? currentActivePubkey = ref.read(activePubkeyProvider);Also applies to: 182-184
283-291: Minor: avoid logging full pubkeys if privacy-sensitiveIf logs are shared, consider abbreviating pubkeys (e.g., first/last 6 chars).
Also applies to: 324-331
lib/config/providers/relay_provider.dart (4)
64-66: Use explicit local types per project Dart guidelines.Declare local variable types explicitly (String) to match codebase standards.
- final activePubkey = ref.read(activePubkeyProvider) ?? ''; + final String activePubkey = ref.read(activePubkeyProvider) ?? ''; @@ - final accountPubKey = ref.read(activePubkeyProvider) ?? ''; + final String accountPubKey = ref.read(activePubkeyProvider) ?? ''; @@ - final accountPubKey = ref.read(activePubkeyProvider) ?? ''; + final String accountPubKey = ref.read(activePubkeyProvider) ?? ''; @@ - final accountPubKey = ref.read(activePubkeyProvider) ?? ''; + final String accountPubKey = ref.read(activePubkeyProvider) ?? ''; @@ - final accountPubKey = ref.read(activePubkeyProvider) ?? ''; + final String accountPubKey = ref.read(activePubkeyProvider) ?? ''; @@ - final accountPubkey = ref.read(activePubkeyProvider) ?? ''; + final String accountPubkey = ref.read(activePubkeyProvider) ?? ''; @@ - final activePubkey = ref.read(activePubkeyProvider) ?? ''; + final String activePubkey = ref.read(activePubkeyProvider) ?? ''; @@ - final accountPubkey = ref.read(activePubkeyProvider) ?? ''; + final String accountPubkey = ref.read(activePubkeyProvider) ?? ''; @@ - final accountPubkey = ref.read(activePubkeyProvider) ?? ''; + final String accountPubkey = ref.read(activePubkeyProvider) ?? '';Also applies to: 119-124, 137-141, 182-186, 238-243, 258-269, 306-314, 359-366, 374-382
139-141: Fix misleading log messages in delete paths and align severity.Logs say “adding relay” in delete handlers; also “no active account” should be a warning, not severe.
- _logger.severe('RelayProvider: No active account found for adding relay'); + _logger.warning('RelayProvider: No active account found for deleting relay');(Apply similarly in InboxRelaysNotifier.deleteRelay and KeyPackageRelaysNotifier.deleteRelay.)
Also applies to: 261-263, 377-379
117-133: Surface user-facing errors when no active account is present.Early-returning without updating state leaves the UI unaware. Consider setting an error on state for add/delete paths.
Also applies to: 135-150, 236-254, 256-274, 357-370, 372-386
93-105: Reduce duplication: factor RelayInfo assembly into a helper.The three notifiers duplicate the same mapping to RelayInfo. Extract into a private helper to keep logic in sync.
Also applies to: 212-224, 333-345
lib/config/providers/welcomes_provider.dart (2)
114-123: Optionally guard fetch-by-id against mid-flight account switches.Capture active pubkey before await and drop results if it changes.
- final activePubkey = ref.read(activePubkeyProvider) ?? ''; + final String activePubkey = ref.read(activePubkeyProvider) ?? ''; + final String capturedPubkey = activePubkey; @@ - final welcome = await findWelcomeByEventId( - pubkey: activePubkey, + final welcome = await findWelcomeByEventId( + pubkey: capturedPubkey, welcomeEventId: welcomeEventId, ); + if (capturedPubkey != (ref.read(activePubkeyProvider) ?? '')) { + return null; + }Also applies to: 125-131
62-64: Prefer explicit local types.Use explicit String types for readability and consistency with the project style.
- final activePubkey = ref.read(activePubkeyProvider) ?? ''; + final String activePubkey = ref.read(activePubkeyProvider) ?? ''; @@ - final activePubkey = ref.read(activePubkeyProvider) ?? ''; + final String activePubkey = ref.read(activePubkeyProvider) ?? ''; @@ - final activePubkey = ref.read(activePubkeyProvider) ?? ''; + final String activePubkey = ref.read(activePubkeyProvider) ?? ''; @@ - final activePubkey = ref.read(activePubkeyProvider) ?? ''; + final String activePubkey = ref.read(activePubkeyProvider) ?? ''; @@ - final activePubkey = ref.read(activePubkeyProvider) ?? ''; + final String activePubkey = ref.read(activePubkeyProvider) ?? ''; @@ - final String capturedPubkey = activePubkey; + final String capturedPubkey = activePubkey;Also applies to: 114-116, 149-151, 181-183, 314-316, 319-321
lib/ui/chat/invite/chat_invite_screen.dart (3)
180-187: Use the centralized pubkey normalization/formatting extension (handles hex and npub).Directly calling
npubFromHexPubkey(...)assumes hex input. Prefer the project’s pubkey normalization extension so UI renders correctly regardless of input format.If you share the exact extension/method name (e.g., from the pubkey validations extension), I can provide a precise diff here.
Also applies to: 282-289
38-40: Make local types explicit and avoid duplicate fetches.
- Declare local types (WelcomesNotifier, bool, UserProfileDataNotifier) to align with style.
- DM header and app bar each call
getUserProfileData(...); consider computing once (FutureProvider family) and sharing.- final welcomesNotifier = ref.watch(welcomesProvider.notifier); + final WelcomesNotifier welcomesNotifier = ref.watch(welcomesProvider.notifier); @@ - final isDMInvite = welcome.memberCount <= 2; + final bool isDMInvite = welcome.memberCount <= 2; @@ - final userProfileDataNotifier = ref.read(userProfileDataProvider.notifier); + final UserProfileDataNotifier userProfileDataNotifier = + ref.read(userProfileDataProvider.notifier); @@ - final userProfileDataNotifier = ref.read(userProfileDataProvider.notifier); + final UserProfileDataNotifier userProfileDataNotifier = + ref.read(userProfileDataProvider.notifier);Also applies to: 49-51, 139-143, 244-248, 329-333
41-45: Localize user-facing strings via AppLocalizations.Strings like “Invitation not found”, “Decline”, “Accept”, “Group Description”, and sentence fragments should be localized.
Also applies to: 74-76, 85-86, 191-198, 211-218, 301-303
lib/domain/services/dm_chat_service.dart (3)
13-18: Type the local var and prefer the.toNpub()extension over direct utils call.
- Declare
activePubkeyexplicitly.- Use the string extension for npub conversion to keep format handling consistent across the app.
- final activePubkey = ref.read(activePubkeyProvider) ?? ''; + final String activePubkey = ref.read(activePubkeyProvider) ?? ''; @@ - final currentUserNpub = npubFromHexPubkey( - hexPubkey: activePubkey, - ); + final String currentUserNpub = activePubkey.toNpub();
41-41: Fix typo and clarify naming.
npuplikely meantnpub. Rename for clarity.- final npup = contactModel.publicKey; + final npub = contactModel.publicKey; @@ - publicKey: npup, + publicKey: npub,
51-53: Log the failure instead of swallowing it silently.Add at least a warning log to aid debugging.
- } catch (e) { - return null; + } catch (e, st) { + // TODO: inject logger if available + // ignore: avoid_print + print('DMChatService.getDMChatData($groupId) failed: $e'); + return null;lib/config/providers/group_provider.dart (3)
80-83: Use explicit typing foractivePubkeyat all read sites.Follow our Dart guideline to declare types explicitly; also keeps intent clear (we operate on hex pubkeys here).
- final activePubkey = ref.read(activePubkeyProvider) ?? ''; + final String activePubkey = ref.read(activePubkeyProvider) ?? ''; @@ - final activePubkey = ref.read(activePubkeyProvider) ?? ''; + final String activePubkey = ref.read(activePubkeyProvider) ?? ''; @@ - final activePubkey = ref.read(activePubkeyProvider) ?? ''; + final String activePubkey = ref.read(activePubkeyProvider) ?? ''; @@ - final activePubkey = ref.read(activePubkeyProvider) ?? ''; + final String activePubkey = ref.read(activePubkeyProvider) ?? ''; @@ - final activePubkey = ref.read(activePubkeyProvider) ?? ''; + final String activePubkey = ref.read(activePubkeyProvider) ?? ''; @@ - final activePubkey = ref.read(activePubkeyProvider) ?? ''; + final String activePubkey = ref.read(activePubkeyProvider) ?? ''; @@ - final activePubkey = ref.read(activePubkeyProvider) ?? ''; + final String activePubkey = ref.read(activePubkeyProvider) ?? ''; @@ - final activePubkey = ref.read(activePubkeyProvider) ?? ''; + final String activePubkey = ref.read(activePubkeyProvider) ?? ''; @@ - final activePubkey = ref.read(activePubkeyProvider) ?? ''; + final String activePubkey = ref.read(activePubkeyProvider) ?? ''; @@ - final activePubkey = ref.read(activePubkeyProvider) ?? ''; + final String activePubkey = ref.read(activePubkeyProvider) ?? '';Also applies to: 155-160, 195-200, 299-305, 378-385, 492-496, 720-727, 764-770, 879-884, 935-939
158-160: Prefer the.toNpub()extension for conversions.Keeps format handling centralized and matches recent refactors.
- final currentUserNpub = npubFromHexPubkey(hexPubkey: activePubkey); - final otherUserNpub = npubFromHexPubkey(hexPubkey: otherUserPubkeyHex); + final String currentUserNpub = activePubkey.toNpub(); + final String otherUserNpub = otherUserPubkeyHex.toNpub(); @@ - final pubkeyString = npubFromHexPubkey(hexPubkey: memberPubkey); + final String pubkeyString = memberPubkey.toNpub(); @@ - final pubkeyString = npubFromHexPubkey(hexPubkey: adminPubkey); + final String pubkeyString = adminPubkey.toNpub(); @@ - final currentUserNpub = npubFromHexPubkey(hexPubkey: currentUserPubkey); + final String currentUserNpub = currentUserPubkey.toNpub();Also applies to: 313-314, 393-394, 598-599
214-229: Use the trimmed creator key consistently.You compute
creatorPubkeyHexbut still log/passactivePubkey. Use the trimmed value in admin set, logs, and the API call.- final combinedAdminKeys = {activePubkey, ...resolvedAdminPublicKeys}.toList(); + final combinedAdminKeys = {creatorPubkeyHex, ...resolvedAdminPublicKeys}.toList(); @@ - _logger.info(' - Creator pubkey: $activePubkey'); + _logger.info(' - Creator pubkey: $creatorPubkeyHex'); @@ - final newGroup = await createGroup( - creatorPubkey: activePubkey, + final newGroup = await createGroup( + creatorPubkey: creatorPubkeyHex,Also applies to: 235-247, 241-247
test/config/providers/user_profile_data_provider_test.dart (1)
184-195: Error-path test is adequate. Consider asserting the error message.Optional: assert on the thrown message to avoid false positives when other exceptions are thrown.
- expect( - () async => await notifier.getUserProfileData(testNpubPubkey), - throwsException, - ); + expect( + () async => notifier.getUserProfileData(testNpubPubkey), + throwsA(isA<Exception>().having((e) => e.toString(), 'message', contains('API error'))), + );lib/config/providers/metadata_cache_provider.dart (6)
68-76: Make conversion helpers synchronous; they already call sync FRB functions.These functions return immediately; keeping them
Futureforces unnecessaryawaits across the file and adds overhead.Apply this diff:
- Future<String> _safeHexToNpub(String hexPubkey) async { + String _safeHexToNpub(String hexPubkey) { try { return npubFromHexPubkey(hexPubkey: hexPubkey); } catch (e) { _logger.warning('Failed to convert hex to npub for $hexPubkey: $e'); return hexPubkey; } }
79-86: Ditto: make NPUB→HEX helper synchronous.Apply this diff:
- Future<String> _safeNpubToHex(String npub) async { + String _safeNpubToHex(String npub) { try { return hexPubkeyFromNpub(npub: npub); } catch (e) { _logger.warning('Failed to convert npub to hex for $npub: $e'); return npub; } }
89-100: Cascade sync: make_getStandardizedNpuband its call sites synchronous.With the helpers sync,
_getStandardizedNpubcan also be sync and all call sites can dropawait.Apply these diffs:
- Future<String> _getStandardizedNpub(String publicKey) async { + String _getStandardizedNpub(String publicKey) { final normalized = _normalizePublicKey(publicKey); if (normalized.isValidNpubPublicKey) { return normalized; } else if (normalized.isValidHexPublicKey) { - return await _safeHexToNpub(normalized); + return _safeHexToNpub(normalized); } else { _logger.warning('Unrecognized public key format: $normalized'); return normalized; } }- fetchKey = await _safeNpubToHex(publicKey); + fetchKey = _safeNpubToHex(publicKey);- final standardNpub = await _getStandardizedNpub(publicKey); + final standardNpub = _getStandardizedNpub(publicKey);- final standardNpub = await _getStandardizedNpub(normalizedKey); + final standardNpub = _getStandardizedNpub(normalizedKey);Also applies to: 110-111, 120-121, 153-154
150-210: Prefer immutable state maps and explicit local types.
- Use
Map.unmodifiableforcacheandpendingFetchesto reinforce immutability.- Declare explicit types for locals per code style (e.g.,
final CachedMetadata? cached = ...;).I can push a small follow-up with these readability tweaks if you want.
18-25: Avoid magic numbers for cache expiry.Extract the default expiry to a named constant for discoverability.
Add near the top of the file:
const Duration kDefaultMetadataCacheExpiry = Duration(hours: 1);Then:
- this.cacheExpiry = const Duration(hours: 1), + this.cacheExpiry = kDefaultMetadataCacheExpiry,
1-216: Style nits per repo guidelines.There are blank lines within functions and a few implicitly typed locals. The repo’s Dart rules ask to avoid blank lines inside functions and to declare variable types explicitly.
Happy to run a quick pass aligning to the style if you prefer.
| final activePubkey = ref.read(activePubkeyProvider) ?? ''; | ||
| _logger.info('RelayStatusNotifier: Active account data: $activePubkey'); | ||
| if (activePubkey.isEmpty) { | ||
| _logger.warning('RelayStatusNotifier: No active account found'); | ||
| state = state.copyWith(isLoading: false, error: 'No active account found'); | ||
| return; | ||
| } | ||
|
|
||
| _logger.info( | ||
| 'RelayStatusNotifier: Fetching relay statuses for pubkey: ${activeAccount.pubkey}', | ||
| 'RelayStatusNotifier: Fetching relay statuses for pubkey: $activePubkey', | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Provider doesn’t react to active pubkey changes. Add a listener in build().
loadRelayStatuses() reads the pubkey once. If the active pubkey changes, statuses won’t refresh. Listen to activePubkeyProvider and reload.
Add in build() (outside selected lines):
@override
RelayStatusState build() {
ref.listen<String?>(activePubkeyProvider, (prev, next) {
if (prev != next) {
loadRelayStatuses();
}
});
ref.listen<bool>(authProvider.select((s) => s.isAuthenticated), (prev, next) {
if (prev != next) {
loadRelayStatuses();
}
});
Future.microtask(() => loadRelayStatuses());
return const RelayStatusState(isLoading: true);
}🤖 Prompt for AI Agents
In lib/config/providers/relay_status_provider.dart around lines 66 to 76, the
notifier only reads activePubkey once so it won’t refresh when the active pubkey
or auth state changes; in build() add ref.listen on activePubkeyProvider and on
authProvider.select((s) => s.isAuthenticated) that call loadRelayStatuses() when
the value changes, and schedule an initial load via Future.microtask(() =>
loadRelayStatuses()); this ensures statuses reload on active account or auth
changes and still performs the initial fetch.
| final activePubkey = ref.read(activePubkeyProvider) ?? ''; | ||
| if (activePubkey.isEmpty) { | ||
| state = state.copyWith(error: 'No active account found', isLoading: false); | ||
| return; | ||
| } | ||
|
|
||
| final welcomes = await pendingWelcomes(pubkey: activeAccount.pubkey); | ||
| final welcomes = await pendingWelcomes(pubkey: activePubkey); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add the same stale-guard to full loads (loadWelcomes).
Active pubkey can change during pendingWelcomes; guard like in polling to avoid mixing accounts.
- final activePubkey = ref.read(activePubkeyProvider) ?? '';
+ final String activePubkey = ref.read(activePubkeyProvider) ?? '';
+ final String capturedPubkey = activePubkey;
@@
- final welcomes = await pendingWelcomes(pubkey: activePubkey);
+ final welcomes = await pendingWelcomes(pubkey: capturedPubkey);
+ if (capturedPubkey != (ref.read(activePubkeyProvider) ?? '')) {
+ state = state.copyWith(isLoading: false);
+ return;
+ }
@@
- state = state.copyWith(
+ state = state.copyWith(
welcomes: welcomes,
welcomeById: welcomeByData,
isLoading: false,
);Also applies to: 78-83
🤖 Prompt for AI Agents
In lib/config/providers/welcomes_provider.dart around lines 62-69 (and similarly
78-83), the code reads activePubkey then awaits pendingWelcomes/loadWelcomes
without re-checking whether the active pubkey changed, which can mix results
across accounts; capture the activePubkey into a local final variable before the
await, then after the await re-read ref.read(activePubkeyProvider) (or compare
to the captured variable) and if it differs, discard the results and return (or
set state appropriately) to avoid applying welcomes for the wrong account; apply
the same stale-guard pattern used in polling to both pendingWelcomes and
loadWelcomes blocks.
| int get rustContentHash => -1927809292; | ||
|
|
||
| static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Content hash changed; ensure all generated artifacts are in sync.
If you re-generated this file, also re-generate frb_generated.io.dart, frb_generated.web.dart, and the Rust side. Mismatched funcIds will crash at runtime.
Quick sanity check:
🏁 Script executed:
#!/bin/bash
# Look for funcId definitions and count to spot obvious drift
rg -n 'funcId:\s*\d+' lib/src/rust -g '!**/build/**' | wc -l
rg -n 'funcId:\s*\d+' rust -g '!**/target/**' | wc -l || trueLength of output: 123
Sync generated bindings: Dart vs Rust funcId count mismatch
The sanity check found 56 funcId entries in lib/src/rust/frb_generated.dart but none in the Rust directory. Re-generate the Rust bindings and update frb_generated.io.dart and frb_generated.web.dart so that all funcIds align and avoid runtime crashes.
🤖 Prompt for AI Agents
In lib/src/rust/frb_generated.dart around lines 79-81, the generated Dart
bindings contain 56 funcId entries that do not match the Rust side (no funcId
entries found), causing a Dart vs Rust funcId count mismatch; re-run the Rust
binding generator to regenerate the Rust bindings, then regenerate
frb_generated.io.dart and frb_generated.web.dart so all funcId tables are
consistent across Rust and both Dart targets, verify the rustContentHash updates
appropriately, and run the sanity check to confirm counts match before
committing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (2)
test/config/providers/active_pubkey_provider_test.dart (1)
123-149: Good addition: loadActivePubkey trims surrounding whitespaceThis covers a real-world footgun from secure storage reads. Consider adding:
- a companion test ensuring setActivePubkey persists a trimmed value (verify the write),
- and a case where storage returns only whitespace (e.g., ' ') to assert it becomes ''.
Apply these diffs to extend coverage:
@@ group('loadActivePubkey', () { + group('when storage has only whitespaces', () { + setUp(() async { + when( + mockStorage.read(key: 'active_account_pubkey'), + ).thenAnswer((_) async => ' '); + }); + + test('sets state to empty string', () async { + final notifier = container.read(activePubkeyProvider.notifier); + await notifier.loadActivePubkey(); + expect(container.read(activePubkeyProvider), ''); + }); + });@@ group('setActivePubkey', () { + group('when input has surrounding whitespaces', () { + setUp(() async { + when( + mockStorage.write(key: 'active_account_pubkey', value: 'test_pubkey_123'), + ).thenAnswer((_) async {}); + }); + + test('trims before persisting and updates state', () async { + final notifier = container.read(activePubkeyProvider.notifier); + await notifier.setActivePubkey(' test_pubkey_123 '); + expect(container.read(activePubkeyProvider), 'test_pubkey_123'); + verify(mockStorage.write( + key: 'active_account_pubkey', + value: 'test_pubkey_123', + )).called(1); + }); + + test('notifies to listeners', () async { + final notifier = container.read(activePubkeyProvider.notifier); + var wasNotified = false; + container.listen(activePubkeyProvider, (prev, next) { + wasNotified = true; + }); + await notifier.setActivePubkey(' test_pubkey_123 '); + expect(wasNotified, isTrue); + }); + });lib/config/providers/active_pubkey_provider.dart (1)
33-35: Trim on set and avoid redundant writes/notificationsMinor optimization: short-circuit when the trimmed value equals the current state to reduce secure storage I/O and unnecessary provider notifications. Also declare the local type explicitly per guidelines.
Apply:
- Future<void> setActivePubkey(String pubkey) async { - final trimmedPubkey = pubkey.trim(); - await AccountSecureStorageService.setActivePubkey(trimmedPubkey, storage: storage); - state = trimmedPubkey; - } + Future<void> setActivePubkey(String pubkey) async { + final String trimmedPubkey = pubkey.trim(); + if (trimmedPubkey == state) { + return; // no-op: avoid redundant write/notify + } + await AccountSecureStorageService.setActivePubkey(trimmedPubkey, storage: storage); + state = trimmedPubkey; + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
lib/config/providers/active_pubkey_provider.dart(1 hunks)test/config/providers/active_pubkey_provider_test.dart(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.dart
📄 CodeRabbit inference engine (.cursor/rules/flutter.mdc)
**/*.dart: Always declare the type of each variable and function (parameters and return value). Avoid using 'any'. Create necessary types.
Don't leave blank lines within a function.
One export per file.
Use PascalCase for classes.
Use camelCase for variables, functions, and methods.
Use underscores_case for file and directory names.
Use UPPERCASE for environment variables. Avoid magic numbers and define constants.
Start each function with a verb.
Use verbs for boolean variables. Example: isLoading, hasError, canDelete, etc.
Use complete words instead of abbreviations and correct spelling, except for standard and well-known abbreviations (API, URL, i, j, err, ctx, req, res, next).
Write short functions with a single purpose. Less than 20 instructions.
Name functions with a verb and something else. If it returns a boolean, use isX or hasX, canX, etc. If it doesn't return anything, use executeX or saveX, etc.
Avoid nesting blocks by early checks and returns, or extraction to utility functions.
Use higher-order functions (map, filter, reduce, etc.) to avoid function nesting. Use arrow functions for simple functions (less than 3 instructions). Use named functions for non-simple functions.
Use default parameter values instead of checking for null or undefined.
Reduce function parameters using RO-RO: use an object to pass multiple parameters and to return results. Declare necessary types for input arguments and output.
Use a single level of abstraction in functions.
Don't abuse primitive types and encapsulate data in composite types.
Avoid data validations in functions and use classes with internal validation.
Prefer immutability for data. Use readonly for data that doesn't change. Use 'as const' for literals that don't change.
Declare interfaces to define contracts.
Write small classes with a single purpose. Less than 200 instructions, less than 10 public methods, less than 10 properties.
Use exceptions to handle errors you don't expect. If you catch an exception, it sh...
Files:
lib/config/providers/active_pubkey_provider.darttest/config/providers/active_pubkey_provider_test.dart
**/*_test.dart
📄 CodeRabbit inference engine (.cursor/rules/flutter.mdc)
**/*_test.dart: Follow the Arrange-Act-Assert convention for tests.
Name test variables clearly. Follow the convention: inputX, mockX, actualX, expectedX, etc.
Write unit tests for each public function. Use test doubles to simulate dependencies, except for third-party dependencies that are not expensive to execute.
Write acceptance tests for each module. Follow the Given-When-Then convention.
Use the standard widget testing for Flutter.
Files:
test/config/providers/active_pubkey_provider_test.dart
🧠 Learnings (4)
📓 Common learnings
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#594
File: lib/config/providers/user_profile_data_provider.dart:0-0
Timestamp: 2025-09-03T20:57:53.202Z
Learning: In the whitenoise_flutter codebase, pubkey normalization (npub/hex format handling) is implemented in a pubkey validations extension rather than being imported directly into individual providers like user_profile_data_provider.dart.
📚 Learning: 2025-09-05T00:08:34.466Z
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#594
File: lib/config/providers/active_account_provider.dart:140-146
Timestamp: 2025-09-05T00:08:34.466Z
Learning: josefinalliende prefers centralizing validation logic (like trimming whitespace) in providers rather than handling it defensively at every consumer site. This approach was applied to activePubkeyProvider in the whitenoise_flutter codebase.
Applied to files:
lib/config/providers/active_pubkey_provider.dart
📚 Learning: 2025-09-03T20:57:53.202Z
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#594
File: lib/config/providers/user_profile_data_provider.dart:0-0
Timestamp: 2025-09-03T20:57:53.202Z
Learning: In the whitenoise_flutter codebase, pubkey normalization (npub/hex format handling) is implemented in a pubkey validations extension rather than being imported directly into individual providers like user_profile_data_provider.dart.
Applied to files:
lib/config/providers/active_pubkey_provider.darttest/config/providers/active_pubkey_provider_test.dart
📚 Learning: 2025-08-12T11:21:53.640Z
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#455
File: lib/ui/settings/profile/switch_profile_bottom_sheet.dart:0-0
Timestamp: 2025-08-12T11:21:53.640Z
Learning: In the whitenoise_flutter codebase, ContactModel.publicKey can be stored in either npub format (from metadata cache) or hex format (from account storage). The activeAccountProvider may return either format depending on how the account was originally stored, so normalization to hex format is required when comparing with other hex-normalized keys in sorting logic.
Applied to files:
lib/config/providers/active_pubkey_provider.dart
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Flutter CI
🔇 Additional comments (1)
lib/config/providers/active_pubkey_provider.dart (1)
27-30: Trim on load — LGTMSetting state = pubkey?.trim() prevents downstream bugs due to accidental whitespace from storage.
Quwaysim
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 🚀
Description
As part of the effort to remove the metadata cache provider (#541), this PR removes more references and introduces fixes along the way. One fix is that the edit profile screen now updates metadata correctly (#558), but the main fix is that metadata of active account is now updated. Fixes also #572
With these changes, the metadata of the active account is updated via the polling provider (which invalidates the active account provider). This ensures that metadata updates from other clients are reflected in Whitenoise.
Commit by commit:
userProfileDataProviderthat fetches account metadata directly from the DB.userProfileDataProviderin multiple places.Short video of how now when updating metadata in primal it updates in WN
now-update-metadata.mov
And the other way, changing profile picture in WN and seeing updates in primal
updating.metadata.works.mov
Type of Change
Checklist
just precommitto ensure that formatting and linting are correctjust check-flutter-coverageto ensure that flutter coverage rules are passingCHANGELOG.mdfile with your changes (if they affect the user experience)Summary by CodeRabbit
New Features
Bug Fixes
Refactor
UI
Developer
Tests