Skip to content

Conversation

@josefinalliende
Copy link
Contributor

@josefinalliende josefinalliende commented Sep 17, 2025

Description

Finally ends fixing #541 ✅ , by removing the last use of metadata cache provider from message converter. Also fixes issue #612 ✅ related to double rendering some messages

I couldn't just replace directly in message converter the metadata cache provider with user profile data provider because this was used at message level. The user profile data provider instead of caching metadata, queries metadata via rust crate to the db. Which means that if had replaced directly the first provider with the second one, if I sent 100 messages in the same chat, it would have triggered 100 queries db (for the same user metadata).

So it was necessary to move the metadata queries one level above, not to message converter but where it was used, to chat provider. The problem is that the chat provider is already huge. So I added a new provider called group messages provider where that has the responsibility of: getting the group messages and the group members metadata and then converting the chat messages to message model objects using that metadata.

Also removes some unused methods from message converter file and moved reaction conversion logic to its own class.

Commit by commit

  • 🧹🧪 Moves the reaction convertion logic to its own little class (to then remove it from MessageConverter class). Adds tests to this class
  • 🧹 Moves to a tiny util to calculate isMe logic (just comparing pubkeys) that I realized I was repeating in message converter and the new provider while refactoring.
  • 🧹🧪 Simplifies MessageConverter by removing metadata cache provider and removing unused or duplicated logic that now it's going to be handled one level above (in a provider).
  • 🧹 🧪 Adds the new groupMessagesProvider, which is a family provider that receives a group id and then, fetches the messages, the group members and converts the messages to message objects. This way the chat provider will have less responsibility (that one was too long)
  • 🧹 🧪 🛠️ Adds service calles MessageMergerService with the responsibility of merging messages from db and the ones in the provider state. It decides which optimistic messages in sending state should be kept along with new messages (to help to fix double rendering message issue)
  • 🧹🛠️ Uses the new provider in the chat provider and changes optimistic message logic. This fixes the double rendering messages issue
  • 🗑️ Removes the now unused metadata cache provider

Type of Change

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

Checklist

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

Summary by CodeRabbit

  • New Features

    • Optimistic message sending with immediate display, live status updates, and preservation on reply/send.
    • Group-specific message fetching and merging that keeps recent optimistic sends when appropriate.
    • Reaction conversion with fallback for unknown users and improved reply handling.
  • Bug Fixes

    • Fixed duplicate/double message rendering; improved ordering and merge behavior.
    • Graceful handling when no active account is set.
  • Chores

    • Removed legacy metadata cache.
  • Tests

    • Added comprehensive tests for message flows, converters, reactions, and merging.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 17, 2025

Walkthrough

Replaces in-provider message loading with a new groupMessagesProvider and fetchMessages API, adds optimistic send/update/failure flows and MessageMergerService, refactors MessageConverter and reaction/pubkey utilities, removes metadata_cache_provider and UI message extensions, and adds comprehensive unit tests across providers, converters, and merger logic.

Changes

Cohort / File(s) Summary
Providers — chat & group message flow
lib/config/providers/chat_provider.dart, lib/config/providers/group_messages_provider.dart
chat_provider now delegates group loads to groupMessagesProvider(groupId).notifier.fetchMessages(); adds optimistic send/update/fail handling and integrates MessageMergerService. New GroupMessagesNotifier/GroupMessagesState and public fetchMessages() family provider added.
Removed provider
lib/config/providers/metadata_cache_provider.dart
Entire metadata cache provider deleted (CachedMetadata, MetadataCacheState, MetadataCacheNotifier, and export metadataCacheProvider).
Converters & utilities
lib/utils/message_converter.dart, lib/utils/reaction_converter.dart, lib/utils/pubkey_utils.dart
MessageConverter reworked to synchronous fromChatMessage/updated list flow, reply placeholders, optimistic message creation, and usersMap-based resolution (many cache/token helpers removed). Added ReactionConverter.fromReactionSummary and PubkeyUtils.isMe.
Service — merging optimistic + DB
lib/domain/services/message_merger_service.dart
New MessageMergerService.merge(...) to combine DB messages with recent optimistic “sending” messages using time/content deduplication rules.
UI removal
lib/ui/chat/utils/message_extensions.dart
Entire file removed (MessageWithTokens extension, MessageHelper, MockMessageStatus enum and helpers).
Changelog
CHANGELOG.md
Updates Unreleased: notes removal of metadata cache provider and a fix for double rendering; adjusts entries.
Tests added
test/config/providers/group_messages_provider_test.dart, test/utils/message_converter_test.dart, test/utils/reaction_converter_test.dart, test/domain/services/message_merger_service_test.dart
New unit/integration tests covering GroupMessagesProvider behavior, MessageConverter (including replies and optimistic), ReactionConverter mapping, and MessageMergerService scenarios.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant UI as Chat UI
  participant CP as ChatProvider
  participant GMP as groupMessagesProvider(groupId)
  participant GMN as GroupMessagesNotifier
  participant API as Messages API
  participant Merge as MessageMergerService

  UI->>CP: loadMessagesForGroup(groupId)
  CP->>GMP: .notifier
  GMP->>GMN: fetchMessages()
  GMN->>API: fetchAggregatedMessages(pubkey, groupId)
  API-->>GMN: List<ChatMessage>
  GMN->>GMN: Convert via MessageConverter + profiles
  GMN-->>CP: List<MessageModel> (dbMessages)
  CP->>CP: Collect state messages (incl. optimistic)
  CP->>Merge: merge(stateMessages, dbMessages)
  Merge-->>CP: mergedMessages
  CP-->>UI: mergedMessages (sorted)
Loading
sequenceDiagram
  autonumber
  participant UI as Chat UI
  participant CP as ChatProvider
  participant API as Send Message API
  participant State as Local State

  UI->>CP: sendMessage(groupId, content, replyTo?)
  CP->>State: create & append optimistic MessageModel (status=sending)
  CP->>API: send(content, replyToId?)
  alt success
    API-->>CP: serverMessage(id, createdAt, status)
    CP->>State: update optimistic entry with id/createdAt/status
  else error
    API-->>CP: error
    CP->>State: mark optimistic entry as failed
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • Quwaysim
  • untreu2
  • erskingardner

Poem

A rabbit hops through code and logs,
I stitch optimistic hops to thoggs.
Messages merge like carrots bright,
Reactions nibble, keys say "right."
Cache cleared, new paths run free — hooray! 🥕🐇

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title concisely describes a real and significant change in the pull request by noting the removal of the metadata cache provider, is clear and specific, and does not introduce unrelated topics or generic phrasing.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/last-step-to-remove-metadata-cache

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8bfc674 and 53c93da.

📒 Files selected for processing (1)
  • CHANGELOG.md (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.md

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

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

Files:

  • CHANGELOG.md
🧠 Learnings (2)
📚 Learning: 2025-09-07T02:15:31.931Z
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#594
File: lib/src/rust/api/utils.dart:12-16
Timestamp: 2025-09-07T02:15:31.931Z
Learning: The lingering PublicKey references in lib/config/providers/auth_provider.dart, lib/config/providers/group_provider.dart, and rust_builder/cargokit/build_tool/lib/src/options.dart identified during the utils.dart API cleanup were fixed in PR #597 in the whitenoise_flutter codebase.

Applied to files:

  • CHANGELOG.md
📚 Learning: 2025-09-16T07:24:07.489Z
Learnt from: Quwaysim
PR: parres-hq/whitenoise_flutter#642
File: lib/ui/contact_list/new_chat_bottom_sheet.dart:324-336
Timestamp: 2025-09-16T07:24:07.489Z
Learning: In the whitenoise_flutter project, the user Quwaysim prefers to handle security improvements incrementally across different PRs rather than bundling them all together. Input trimming for search functionality was handled in PR #613, and private key security measures are planned for PR #505.

Applied to files:

  • CHANGELOG.md
⏰ 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

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

❤️ Share

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

@josefinalliende josefinalliende marked this pull request as ready for review September 18, 2025 01:07
@josefinalliende josefinalliende marked this pull request as draft September 18, 2025 01:07
@josefinalliende josefinalliende marked this pull request as ready for review September 18, 2025 13:12
@josefinalliende
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 18, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (10)
lib/utils/message_converter.dart (4)

32-33: Use cache-aware reaction conversion to populate reaction users.

Single-message path uses _convertReactions, which hardcodes “Unknown User” and references the old cache. Switch to the cache‑aware helper.

-    final reactions = _convertReactions(messageData.reactions);
+    final reactions = _convertReactionsWithCache(messageData.reactions, usersMap);

Also consider removing _convertReactions entirely if no longer needed, or clearly mark it as a fallback-only path.

Also applies to: 155-156, 304-308


256-267: Remove unnecessary Future.microtask wrapping.

Future.microtask adds scheduling overhead without benefit here; the function is already async.

-      return await Future.microtask(() async {
-        final userProfileDataNotifier = ref.read(userProfileDataProvider.notifier);
-        final userProfileData = await userProfileDataNotifier.getUserProfileData(pubkey);
-
-        return domain_user.User(
-          id: pubkey,
-          displayName: userProfileData.displayName,
-          nip05: userProfileData.nip05 ?? '',
-          publicKey: pubkey,
-          imagePath: userProfileData.imagePath,
-        );
-      });
+      final userProfileDataNotifier = ref.read(userProfileDataProvider.notifier);
+      final userProfileData = await userProfileDataNotifier.getUserProfileData(pubkey);
+      return domain_user.User(
+        id: pubkey,
+        displayName: userProfileData.displayName,
+        nip05: userProfileData.nip05 ?? '',
+        publicKey: pubkey,
+        imagePath: userProfileData.imagePath,
+      );

97-104: Avoid async when not awaiting (micro-optimization and clarity).

fromChatMessageList uses no await. Return a completed future without marking the function async.

-  static Future<List<MessageModel>> fromChatMessageList(
+  static Future<List<MessageModel>> fromChatMessageList(
     List<ChatMessage> chatMessages, {
       required String? currentUserPublicKey,
       String? groupId,
       required Ref ref,
       required Map<String, domain_user.User> usersMap,
-  }) async {
+  }) {
   ...
-    return messages;
+    return Future.value(messages);

Also applies to: 128-129


285-291: Remove obsolete comment referencing the deleted metadata cache.

This comment is stale post‑refactor.

-        displayName: 'Unknown User', // Will be resolved by metadata cache later
+        displayName: 'Unknown User',
lib/config/providers/chat_provider.dart (3)

182-199: Deduplicate chatMessagesMap construction (two identical blocks).

The MessageModel → ChatMessage conversion is duplicated in send and reply flows. Extract a small helper to avoid drift.

-      final chatMessagesMap = <String, ChatMessage>{};
-      for (final msg in currentMessages) {
-        // Convert existing MessageModel back to ChatMessage for cache
-        final chatMessage = ChatMessage(
-          id: msg.id,
-          pubkey: msg.sender.publicKey,
-          content: msg.content ?? '',
-          createdAt: msg.createdAt,
-          tags: const [],
-          isReply: msg.replyTo != null,
-          replyToId: msg.replyTo?.id,
-          isDeleted: false,
-          contentTokens: const [],
-          reactions: const ReactionSummary(byEmoji: [], userReactions: []),
-          kind: msg.kind, // Use the actual message kind
-        );
-        chatMessagesMap[msg.id] = chatMessage;
-      }
+      final chatMessagesMap = _buildChatMessagesMap(currentMessages);

Add this helper to ChatNotifier:

Map<String, ChatMessage> _buildChatMessagesMap(List<MessageModel> models) {
  return {
    for (final m in models)
      m.id: ChatMessage(
        id: m.id,
        pubkey: m.sender.publicKey,
        content: m.content ?? '',
        createdAt: m.createdAt,
        tags: const [],
        isReply: m.replyTo != null,
        replyToId: m.replyTo?.id,
        isDeleted: false,
        contentTokens: const [],
        reactions: const ReactionSummary(byEmoji: [], userReactions: []),
        kind: m.kind,
      )
  };
}

Also applies to: 629-646


125-128: Replace magic Nostr kind numbers with named constants.

Improves readability and prevents accidental mismatches.

-    int kind = 9, // Default to text message
+    int kind = NostrKind.text,
...
-        kind: 7, // Nostr kind 7 = reaction
+        kind: NostrKind.reaction,
...
-        kind: 5, // Nostr kind 5 = deletion
+        kind: NostrKind.deletion,

Add somewhere central (e.g., a constants file) and import:

class NostrKind {
  static const int text = 9;
  static const int reaction = 7;
  static const int deletion = 5;
}

Also applies to: 548-555, 713-715


429-444: Avoid using ??= on a parameter for clarity.

Slightly confusing to reassign the param. Prefer a local final.

-    final gId = groupId ??= state.selectedGroupId;
+    final gId = groupId ?? state.selectedGroupId;
lib/config/services/group_messages_service.dart (3)

21-36: Thread chatMessagesMap and activePubkey through helpers (no hidden reads).

Avoid re‑reading providers inside helpers; use the already available inputs and include reaction authors in the prefetch to populate reaction avatars.

   Future<MessageModel> toMessageModel({
     required ChatMessage chatMessage,
     required String activePubkey,
     required Map<String, ChatMessage> chatMessagesMap,
   }) async {
-    final usersMap = await _fetchMessageUsersMap(chatMessage: chatMessage);
+    final usersMap = await _fetchMessageUsersMap(
+      chatMessage: chatMessage,
+      activePubkey: activePubkey,
+      chatMessagesMap: chatMessagesMap,
+    );
-  Future<Map<String, domain_user.User>> _fetchMessageUsersMap({
-    required ChatMessage chatMessage,
-  }) async {
-    return await Future.microtask(() async {
-      final activePubkey = ref.read(activePubkeyProvider);
-      if (activePubkey == null || activePubkey.isEmpty) {
-        return {};
-      }
-      final messagePubkeys = [chatMessage.pubkey];
-      if (chatMessage.replyToId?.isNotEmpty ?? false) {
-        messagePubkeys.add(chatMessage.replyToId!);
-      }
-      final contacts = await _fetchUsersProfileData(messagePubkeys);
-      final domainUsersMap = _mapContactModelsToDomainUsers(
-        activePubkey: activePubkey,
-        contacts: contacts,
-      );
-      return domainUsersMap;
-    });
-  }
+  Future<Map<String, domain_user.User>> _fetchMessageUsersMap({
+    required ChatMessage chatMessage,
+    required String activePubkey,
+    required Map<String, ChatMessage> chatMessagesMap,
+  }) async {
+    final pubkeys = <String>{chatMessage.pubkey};
+    // reply author
+    if (chatMessage.isReply && chatMessage.replyToId != null) {
+      final original = chatMessagesMap[chatMessage.replyToId!];
+      if (original != null && original.pubkey.isNotEmpty) {
+        pubkeys.add(original.pubkey);
+      }
+    }
+    // reaction authors
+    for (final ur in chatMessage.reactions.userReactions) {
+      if (ur.user.isNotEmpty) pubkeys.add(ur.user);
+    }
+    final contacts = await _fetchUsersProfileData(pubkeys.toList());
+    return _mapContactModelsToDomainUsers(
+      activePubkey: activePubkey,
+      contacts: contacts,
+    );
+  }

Also applies to: 53-56


42-49: Avoid re-reading activePubkey inside _fetchGroupUsersMap and drop microtasks.

Pass activePubkey explicitly and remove Future.microtask.

   Future<List<MessageModel>> toMessageModels({
     required List<ChatMessage> chatMessages,
     required String activePubkey,
   }) async {
-    final usersMap = await _fetchGroupUsersMap();
+    final usersMap = await _fetchGroupUsersMap(activePubkey: activePubkey);
-  Future<Map<String, domain_user.User>> _fetchGroupUsersMap() async {
-    return await Future.microtask(() async {
-      final activePubkey = ref.read(activePubkeyProvider);
-      if (activePubkey == null || activePubkey.isEmpty) {
-        return {};
-      }
-      final groupContacts = await _fetchGroupUsersProfileData(activePubkey);
-      final domainUsersMap = _mapContactModelsToDomainUsers(
-        activePubkey: activePubkey,
-        contacts: groupContacts,
-      );
-      return domainUsersMap;
-    });
-  }
+  Future<Map<String, domain_user.User>> _fetchGroupUsersMap({
+    required String activePubkey,
+  }) async {
+    if (activePubkey.isEmpty) return {};
+    final groupContacts = await _fetchGroupUsersProfileData(activePubkey);
+    return _mapContactModelsToDomainUsers(
+      activePubkey: activePubkey,
+      contacts: groupContacts,
+    );
+  }

Also applies to: 74-87


89-100: De-duplicate pubkeys before fetching profiles.

Prevents redundant requests and wasted allocations.

-  Future<List<MapEntry<String, ContactModel>>> _fetchUsersProfileData(
-    List<String> pubkeys,
-  ) async {
+  Future<List<MapEntry<String, ContactModel>>> _fetchUsersProfileData(
+    List<String> pubkeys,
+  ) async {
+    pubkeys = pubkeys.toSet().toList();
     final userProfileDataNotifier = ref.read(userProfileDataProvider.notifier);
     final userFutures = pubkeys.map(
       (pubkey) => userProfileDataNotifier
           .getUserProfileData(pubkey)
           .then((contact) => MapEntry(pubkey, contact)),
     );
     final usersProfileData = await Future.wait(userFutures);
     return usersProfileData;
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d0e9414 and 2c43d74.

⛔ Files ignored due to path filters (2)
  • ios/Podfile.lock is excluded by !**/*.lock
  • pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • lib/config/providers/chat_provider.dart (7 hunks)
  • lib/config/providers/metadata_cache_provider.dart (0 hunks)
  • lib/config/services/group_messages_service.dart (1 hunks)
  • lib/utils/message_converter.dart (14 hunks)
💤 Files with no reviewable changes (1)
  • lib/config/providers/metadata_cache_provider.dart
🧰 Additional context used
📓 Path-based instructions (1)
**/*.dart

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

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

Files:

  • lib/config/services/group_messages_service.dart
  • lib/config/providers/chat_provider.dart
  • lib/utils/message_converter.dart
🧠 Learnings (4)
📓 Common learnings
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#594
File: lib/src/rust/api/utils.dart:12-16
Timestamp: 2025-09-07T02:15:31.931Z
Learning: The lingering PublicKey references in lib/config/providers/auth_provider.dart, lib/config/providers/group_provider.dart, and rust_builder/cargokit/build_tool/lib/src/options.dart identified during the utils.dart API cleanup were fixed in PR #597 in the whitenoise_flutter codebase.
📚 Learning: 2025-09-07T13:10:16.542Z
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#597
File: lib/config/providers/group_provider.dart:311-314
Timestamp: 2025-09-07T13:10:16.542Z
Learning: In the whitenoise_flutter codebase, the User class used in group_provider.dart (and similar contexts) is presentational only, not the actual user class from the Rust API. There are plans to remove this User class and replace it with UserProfileData, similar to the planned consolidation with ContactModel.

Applied to files:

  • lib/config/services/group_messages_service.dart
  • lib/utils/message_converter.dart
📚 Learning: 2025-09-14T21:22:00.962Z
Learnt from: Quwaysim
PR: parres-hq/whitenoise_flutter#634
File: lib/config/providers/group_provider.dart:1130-1132
Timestamp: 2025-09-14T21:22:00.962Z
Learning: In the whitenoise_flutter codebase, the _updateGroupInfo method in GroupsNotifier performs optimistic updates that directly modify Group objects in the provider state (groups list and groupsMap). For non-DM groups, the display name comes from group.name, so updating the Group object directly is sufficient to reflect name changes in the UI without needing to refresh the separate display name cache.

Applied to files:

  • lib/config/services/group_messages_service.dart
  • lib/config/providers/chat_provider.dart
📚 Learning: 2025-09-07T02:15:31.931Z
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#594
File: lib/src/rust/api/utils.dart:12-16
Timestamp: 2025-09-07T02:15:31.931Z
Learning: The lingering PublicKey references in lib/config/providers/auth_provider.dart, lib/config/providers/group_provider.dart, and rust_builder/cargokit/build_tool/lib/src/options.dart identified during the utils.dart API cleanup were fixed in PR #597 in the whitenoise_flutter codebase.

Applied to files:

  • lib/config/providers/chat_provider.dart

@josefinalliende josefinalliende marked this pull request as draft September 18, 2025 13:32
@josefinalliende josefinalliende force-pushed the refactor/last-step-to-remove-metadata-cache branch 5 times, most recently from 6b3bb51 to 2d99577 Compare September 19, 2025 01:43
@josefinalliende josefinalliende marked this pull request as ready for review September 19, 2025 01:45
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (21)
lib/utils/reaction_converter.dart (2)

5-34: Normalize pubkeys during lookup; simplify loop; extract constant.

Without key normalization, npub/hex mismatches can incorrectly fall back to "Unknown User". Also, the loop can be expressed functionally and the magic string should be a constant.

@@
-import 'package:whitenoise/domain/models/user_model.dart' as domain_user;
+import 'package:whitenoise/domain/models/user_model.dart' as domain_user;
+import 'package:whitenoise/utils/pubkey_formatter.dart';
@@
 class ReactionConverter {
   static List<Reaction> fromReactionSummary({
     required ReactionSummary reactionSummary,
     required Map<String, domain_user.User> usersMap,
   }) {
-    final List<Reaction> convertedReactions = [];
-
-    for (final userReaction in reactionSummary.userReactions) {
-      final user =
-          usersMap[userReaction.user] ??
-          domain_user.User(
-            id: userReaction.user,
-            displayName: 'Unknown User',
-            nip05: '',
-            publicKey: userReaction.user,
-          );
-
-      final reaction = Reaction(
-        emoji: userReaction.emoji,
-        user: user,
-        createdAt: userReaction.createdAt,
-      );
-
-      convertedReactions.add(reaction);
-    }
-
-    return convertedReactions;
+    const String unknownUserName = 'Unknown User';
+    if (reactionSummary.userReactions.isEmpty) return const <Reaction>[];
+    return reactionSummary.userReactions.map((userReaction) {
+      final String key = userReaction.user;
+      final String? hexKey = PubkeyFormatter(pubkey: key).toHex();
+      final domain_user.User user =
+          usersMap[key] ??
+          (hexKey != null ? usersMap[hexKey] : null) ??
+          domain_user.User(
+            id: key,
+            displayName: unknownUserName,
+            nip05: '',
+            publicKey: key,
+          );
+      return Reaction(
+        emoji: userReaction.emoji,
+        user: user,
+        createdAt: userReaction.createdAt,
+      );
+    }).toList(growable: false);
   }
 }

13-21: Add explicit type annotation for local user.

Aligns with the repo rule to declare types for variables.

-      final user =
+      final domain_user.User user =
           usersMap[userReaction.user] ??
lib/utils/pubkey_utils.dart (1)

3-15: LGTM; small style nits.

  • Remove the blank line inside the function to match the "no blank lines within a function" guideline.
  • Consider a brief doc comment.
 class PubkeyUtils {
   static bool isMe({required String myPubkey, required String otherPubkey}) {
     final myHexPubkey = PubkeyFormatter(pubkey: myPubkey).toHex();
     final otherHexPubkey = PubkeyFormatter(pubkey: otherPubkey).toHex();
     if (myHexPubkey == null ||
         myHexPubkey.isEmpty ||
         otherHexPubkey == null ||
         otherHexPubkey.isEmpty) {
       return false;
     }
-
     return myHexPubkey == otherHexPubkey;
   }
 }

Optionally:

/// Returns true when both pubkeys refer to the same identity (hex-normalized).
test/utils/message_converter_test.dart (1)

36-175: Comprehensive coverage; consider factoring builders.

Repeated ChatMessage construction can be centralized in small helpers to keep tests focused and shorter. Also add a whitespace-only content case to ensure filtering treats ' ' as empty.

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

76-76: Type annotation for messages.

Prefer explicit typing per repo rules.

-      final messages = await ref.read(groupMessagesProvider(groupId).notifier).fetchMessages();
+      final List<MessageModel> messages =
+          await ref.read(groupMessagesProvider(groupId).notifier).fetchMessages();

161-178: Duplicate cache building logic — extract a helper.

This block is repeated below for replies. Extract a private helper to reduce duplication and mistakes.

-      // Build message cache from current messages for consistency
-      final chatMessagesMap = <String, ChatMessage>{};
-      for (final msg in currentMessages) {
-        // Convert existing MessageModel back to ChatMessage for cache
-        final chatMessage = ChatMessage(
-          id: msg.id,
-          pubkey: msg.sender.publicKey,
-          content: msg.content ?? '',
-          createdAt: msg.createdAt,
-          tags: const [],
-          isReply: msg.replyTo != null,
-          replyToId: msg.replyTo?.id,
-          isDeleted: false,
-          contentTokens: const [],
-          reactions: const ReactionSummary(byEmoji: [], userReactions: []),
-          kind: msg.kind, // Use the actual message kind
-        );
-        chatMessagesMap[msg.id] = chatMessage;
-      }
+      // Build message cache from current messages for consistency
+      final Map<String, ChatMessage> chatMessagesMap =
+          _buildChatMessagesMapFromMessageModels(currentMessages);

Add this helper inside ChatNotifier:

Map<String, ChatMessage> _buildChatMessagesMapFromMessageModels(List<MessageModel> models) {
  final map = <String, ChatMessage>{};
  for (final msg in models) {
    map[msg.id] = ChatMessage(
      id: msg.id,
      pubkey: msg.sender.publicKey,
      content: msg.content ?? '',
      createdAt: msg.createdAt,
      tags: const [],
      isReply: msg.replyTo != null,
      replyToId: msg.replyTo?.id,
      isDeleted: false,
      contentTokens: const [],
      reactions: const ReactionSummary(byEmoji: [], userReactions: []),
      kind: msg.kind,
    );
  }
  return map;
}

180-185: Type annotation for sentMessageModel.

Keep local types explicit.

-      final sentMessageModel = await ref
+      final MessageModel sentMessageModel = await ref
           .read(groupMessagesProvider(groupId).notifier)
           .toMessageModel(
             chatMessage: sentChatMessage,
             chatMessagesMap: chatMessagesMap,
           );

272-272: Annotate type and use ID-based delta instead of index/length.

Index-based skip assumes strict append-only and identical ordering. Switch to ID diff to be robust against backfill/reordering.

-      final newMessages = await ref.read(groupMessagesProvider(groupId).notifier).fetchMessages();
+      final List<MessageModel> newMessages =
+          await ref.read(groupMessagesProvider(groupId).notifier).fetchMessages();
@@
-        if (newMessages.length > currentMessages.length) {
-          // Add only new messages to preserve performance
-          final newMessagesOnly = newMessages.skip(currentMessages.length).toList();
+        if (newMessages.length > currentMessages.length) {
+          // Add only messages with unseen IDs
+          final existingIds = {for (final m in currentMessages) m.id};
+          final newMessagesOnly =
+              newMessages.where((m) => !existingIds.contains(m.id)).toList(growable: false);

592-609: Reuse the extracted helper here as well.

Same cache-build duplication as above.

-      // Build message cache from current messages for reply lookup
-      final chatMessagesMap = <String, ChatMessage>{};
-      for (final msg in currentMessages) {
-        // Convert existing MessageModel back to ChatMessage for cache
-        final chatMessage = ChatMessage(
-          id: msg.id,
-          pubkey: msg.sender.publicKey,
-          content: msg.content ?? '',
-          createdAt: msg.createdAt,
-          tags: const [],
-          isReply: msg.replyTo != null,
-          replyToId: msg.replyTo?.id,
-          isDeleted: false,
-          contentTokens: const [],
-          reactions: const ReactionSummary(byEmoji: [], userReactions: []),
-          kind: msg.kind, // Use the actual message kind
-        );
-        chatMessagesMap[msg.id] = chatMessage;
-      }
+      // Build message cache from current messages for reply lookup
+      final Map<String, ChatMessage> chatMessagesMap =
+          _buildChatMessagesMapFromMessageModels(currentMessages);

611-616: Type annotation for sentMessageModel.

Same nit as sendMessage().

-      final sentMessageModel = await ref
+      final MessageModel sentMessageModel = await ref
           .read(groupMessagesProvider(groupId).notifier)
           .toMessageModel(
             chatMessage: sentChatMessage,
             chatMessagesMap: chatMessagesMap,
           );
test/config/providers/group_messages_provider_test.dart (4)

36-52: Remove unused MockPubkeyFormatter and factory.

These mocks aren’t used anywhere in this test file; drop them to reduce noise.

 class MockPubkeyFormatter implements PubkeyFormatter {
   final String _pubkey;

   MockPubkeyFormatter(this._pubkey);

   @override
   String? toHex() {
     // Simple mock: return the pubkey as-is for comparison
     return _pubkey;
   }

   @override
   String? toNpub() => _pubkey;

   @override
   dynamic noSuchMethod(Invocation invocation) => null;
 }
-
-PubkeyFormatter Function({String? pubkey}) mockPubkeyFormatter() {
-  return ({String? pubkey}) => MockPubkeyFormatter(pubkey ?? '');
-}

Also applies to: 69-71


84-104: Add explicit type annotations for test data.

Project guidelines require explicit types in Dart files. Annotate maps and lists.

-    final testContacts = {
+    final Map<String, ContactModel> testContacts = {
       'npub1testkey12345678901234567890': ContactModel(
         publicKey: 'npub1testkey12345678901234567890',
         displayName: 'Alice',
         imagePath: '/path/to/alice.jpg',
         nip05: 'alice@example.com',
       ),
       ...
     };
 
-    final mockMessages = [
+    final List<ChatMessage> mockMessages = [
       ChatMessage(
         id: 'message_1',
         pubkey: 'npub1testkey12345678901234567890',
         content: 'Hello world!',
         createdAt: DateTime.fromMillisecondsSinceEpoch(1234567890000),
         tags: [],
         isReply: false,
         isDeleted: false,
         contentTokens: [],
         reactions: const ReactionSummary(byEmoji: [], userReactions: []),
         kind: 9,
       ),
       ...
     ];

Also applies to: 105-142


221-224: Type inference → explicit types in assertions.

Make result types explicit per repo guidelines.

-          final messages = await notifier.fetchMessages();
+          final List<MessageModel> messages = await notifier.fetchMessages();

Apply similarly at Lines 227, 234.

Also applies to: 227-231, 234-238


266-266: Unify group title wording.

For consistency with other groups, rename to “with Carl as active pubkey”.

-      group('with Carl active pubkey', () {
+      group('with Carl as active pubkey', () {
lib/config/providers/group_messages_provider.dart (4)

113-132: Remove unnecessary Future.microtask wrapper in _fetchMessageUsersMap (simplify + avoid nested futures).

Microtask + async adds complexity without benefit. Inline the logic; also dedupe pubkeys.

-  Future<Map<String, domain_user.User>> _fetchMessageUsersMap({
+  Future<Map<String, domain_user.User>> _fetchMessageUsersMap({
     required ChatMessage chatMessage,
     required Map<String, ChatMessage> chatMessagesMap,
   }) async {
-    return await Future.microtask(() async {
-      final activePubkey = ref.read(activePubkeyProvider);
-      if (activePubkey == null || activePubkey.isEmpty) {
-        return {};
-      }
-      final messagePubkeys = [chatMessage.pubkey];
-      if (chatMessage.isReply && chatMessage.replyToId != null) {
-        final original = chatMessagesMap[chatMessage.replyToId!];
-        if (original != null && original.pubkey.isNotEmpty) {
-          messagePubkeys.add(original.pubkey);
-        }
-      }
-      final contacts = await _fetchUsersProfileData(messagePubkeys);
-      final domainUsersMap = _mapContactModelsToDomainUsers(
-        activePubkey: activePubkey,
-        contacts: contacts,
-      );
-      return domainUsersMap;
-    });
+    final String? activePubkey = ref.read(activePubkeyProvider);
+    if (activePubkey == null || activePubkey.isEmpty) {
+      return {};
+    }
+    final Set<String> pubkeys = <String>{chatMessage.pubkey};
+    if (chatMessage.isReply && chatMessage.replyToId != null) {
+      final ChatMessage? original = chatMessagesMap[chatMessage.replyToId!];
+      if (original != null && original.pubkey.isNotEmpty) {
+        pubkeys.add(original.pubkey);
+      }
+    }
+    final List<MapEntry<String, ContactModel>> contacts =
+        await _fetchUsersProfileData(pubkeys.toList());
+    return _mapContactModelsToDomainUsers(
+      activePubkey: activePubkey,
+      contacts: contacts,
+    );
   }

134-147: Same simplification for _fetchGroupUsersMap.

-  Future<Map<String, domain_user.User>> _fetchGroupUsersMap() async {
-    return await Future.microtask(() async {
-      final activePubkey = ref.read(activePubkeyProvider);
-      if (activePubkey == null || activePubkey.isEmpty) {
-        return {};
-      }
-      final groupContacts = await _fetchGroupUsersProfileData(activePubkey);
-      final domainUsersMap = _mapContactModelsToDomainUsers(
-        activePubkey: activePubkey,
-        contacts: groupContacts,
-      );
-      return domainUsersMap;
-    });
-  }
+  Future<Map<String, domain_user.User>> _fetchGroupUsersMap() async {
+    final String? activePubkey = ref.read(activePubkeyProvider);
+    if (activePubkey == null || activePubkey.isEmpty) {
+      return {};
+    }
+    final List<MapEntry<String, ContactModel>> groupContacts =
+        await _fetchGroupUsersProfileData(activePubkey);
+    return _mapContactModelsToDomainUsers(
+      activePubkey: activePubkey,
+      contacts: groupContacts,
+    );
+  }

149-160: Deduplicate pubkeys before fetching profiles.

Prevents redundant lookups when the same pubkey appears multiple times.

-  Future<List<MapEntry<String, ContactModel>>> _fetchUsersProfileData(
-    List<String> pubkeys,
-  ) async {
+  Future<List<MapEntry<String, ContactModel>>> _fetchUsersProfileData(
+    List<String> pubkeys,
+  ) async {
     final userProfileDataNotifier = ref.read(userProfileDataProvider.notifier);
-    final userFutures = pubkeys.map(
+    final List<String> uniquePubkeys = pubkeys.toSet().toList();
+    final Iterable<Future<MapEntry<String, ContactModel>>> userFutures = uniquePubkeys.map(
       (pubkey) => userProfileDataNotifier
           .getUserProfileData(pubkey)
           .then((contact) => MapEntry(pubkey, contact)),
     );
     final usersProfileData = await Future.wait(userFutures);
     return usersProfileData;
   }

187-196: Avoid magic strings for display names (“You”).

Extract to a constant to standardize across modules and ease i18n later.

-    if (_isMe(myPubkey: activePubkey, otherPubkey: contact.publicKey)) {
-      return 'You';
+    if (_isMe(myPubkey: activePubkey, otherPubkey: contact.publicKey)) {
+      return kYouDisplayName;

Add near the top of this file:

+const String kYouDisplayName = 'You';
lib/utils/message_converter.dart (3)

60-66: Make fromChatMessageList synchronous (it’s purely sync).

Removes unnecessary Future/async overhead and simplifies callers.

-  static Future<List<MessageModel>> fromChatMessageList(
+  static List<MessageModel> fromChatMessageList(
     List<ChatMessage> chatMessages, {
       required String currentUserPublicKey,
       required String groupId,
       required Map<String, domain_user.User> usersMap,
       bool Function({required String myPubkey, required String otherPubkey})? isMeFn,
-  }) async {
+  }) {
     // Filter valid messages first
-    final validMessages =
+    final List<ChatMessage> validMessages =
         chatMessages.where((msg) => !msg.isDeleted && msg.content.isNotEmpty).toList();

     // Build messages map for reply lookups
-    final chatMessagesMap = <String, ChatMessage>{};
+    final Map<String, ChatMessage> chatMessagesMap = <String, ChatMessage>{};
     for (final msg in validMessages) {
       chatMessagesMap[msg.id] = msg;
     }

-    final messages =
+    final List<MessageModel> messages =
         validMessages
             .map(
               (messageData) => fromChatMessage(
                 messageData,
                 currentUserPublicKey: currentUserPublicKey,
                 groupId: groupId,
                 chatMessagesMap: chatMessagesMap,
                 usersMap: usersMap,
                 isMeFn: isMeFn,
               ),
             )
             .toList();

     return messages;
   }

Update caller in lib/config/providers/group_messages_provider.dart Lines 100‑106:

-    final messages = MessageConverter.fromChatMessageList(
+    final List<MessageModel> messages = MessageConverter.fromChatMessageList(
       chatMessages,
       currentUserPublicKey: activePubkey,
       groupId: state.groupId,
       usersMap: usersMap,
     );
-    return messages;
+    return messages;

Also applies to: 77-92


127-141: Deterministic fallback timestamp for “message not found”.

Using DateTime.now() makes tests flaky. Prefer a fixed epoch.

-        createdAt: DateTime.now(),
+        createdAt: DateTime.fromMillisecondsSinceEpoch(0),

146-153: Avoid magic string: “Unknown User”.

Extract a constant for reuse and future i18n.

-  static domain_user.User _unknownUser({required String pubkey}) {
-    return domain_user.User(
-      id: pubkey,
-      displayName: 'Unknown User',
+  static const String kUnknownUserDisplayName = 'Unknown User';
+
+  static domain_user.User _unknownUser({required String pubkey}) {
+    return domain_user.User(
+      id: pubkey,
+      displayName: kUnknownUserDisplayName,
       nip05: '',
       publicKey: pubkey,
     );
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2c43d74 and 2d99577.

📒 Files selected for processing (10)
  • CHANGELOG.md (1 hunks)
  • lib/config/providers/chat_provider.dart (7 hunks)
  • lib/config/providers/group_messages_provider.dart (1 hunks)
  • lib/config/providers/metadata_cache_provider.dart (0 hunks)
  • lib/utils/message_converter.dart (2 hunks)
  • lib/utils/pubkey_utils.dart (1 hunks)
  • lib/utils/reaction_converter.dart (1 hunks)
  • test/config/providers/group_messages_provider_test.dart (1 hunks)
  • test/utils/message_converter_test.dart (1 hunks)
  • test/utils/reaction_converter_test.dart (1 hunks)
💤 Files with no reviewable changes (1)
  • lib/config/providers/metadata_cache_provider.dart
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🧰 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/utils/pubkey_utils.dart
  • test/utils/message_converter_test.dart
  • test/config/providers/group_messages_provider_test.dart
  • test/utils/reaction_converter_test.dart
  • lib/utils/reaction_converter.dart
  • lib/config/providers/group_messages_provider.dart
  • lib/config/providers/chat_provider.dart
  • lib/utils/message_converter.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/utils/message_converter_test.dart
  • test/config/providers/group_messages_provider_test.dart
  • test/utils/reaction_converter_test.dart
🧠 Learnings (10)
📓 Common learnings
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#594
File: lib/src/rust/api/utils.dart:12-16
Timestamp: 2025-09-07T02:15:31.931Z
Learning: The lingering PublicKey references in lib/config/providers/auth_provider.dart, lib/config/providers/group_provider.dart, and rust_builder/cargokit/build_tool/lib/src/options.dart identified during the utils.dart API cleanup were fixed in PR #597 in the whitenoise_flutter codebase.
📚 Learning: 2025-09-07T02:15:31.931Z
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#594
File: lib/src/rust/api/utils.dart:12-16
Timestamp: 2025-09-07T02:15:31.931Z
Learning: The lingering PublicKey references in lib/config/providers/auth_provider.dart, lib/config/providers/group_provider.dart, and rust_builder/cargokit/build_tool/lib/src/options.dart identified during the utils.dart API cleanup were fixed in PR #597 in the whitenoise_flutter codebase.

Applied to files:

  • lib/utils/pubkey_utils.dart
  • lib/utils/message_converter.dart
📚 Learning: 2025-09-04T17:54:09.162Z
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#594
File: lib/ui/chat/chat_info/dm_chat_info.dart:0-0
Timestamp: 2025-09-04T17:54:09.162Z
Learning: In the whitenoise_flutter codebase, pubkey conversion from hex to npub format is handled using a .toNpub() extension method on String, which eliminates the need for manual format checking or calling npubFromHexPubkey() function directly.

Applied to files:

  • lib/utils/pubkey_utils.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/utils/pubkey_utils.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/utils/pubkey_utils.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/utils/message_converter_test.dart
📚 Learning: 2025-09-08T17:39:13.711Z
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#599
File: test/ui/contact_list/start_chat_bottom_sheet_test.dart:173-176
Timestamp: 2025-09-08T17:39:13.711Z
Learning: In the whitenoise Flutter app, the followProvider uses followsProvider behind the scenes, so mocking followsProvider with MockFollowsNotifier effectively controls what followProvider sees in tests.

Applied to files:

  • test/config/providers/group_messages_provider_test.dart
📚 Learning: 2025-09-14T21:22:00.962Z
Learnt from: Quwaysim
PR: parres-hq/whitenoise_flutter#634
File: lib/config/providers/group_provider.dart:1130-1132
Timestamp: 2025-09-14T21:22:00.962Z
Learning: In the whitenoise_flutter codebase, the _updateGroupInfo method in GroupsNotifier performs optimistic updates that directly modify Group objects in the provider state (groups list and groupsMap). For non-DM groups, the display name comes from group.name, so updating the Group object directly is sufficient to reflect name changes in the UI without needing to refresh the separate display name cache.

Applied to files:

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

Applied to files:

  • lib/config/providers/group_messages_provider.dart
📚 Learning: 2025-09-05T00:08:34.509Z
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#594
File: lib/config/providers/active_account_provider.dart:140-146
Timestamp: 2025-09-05T00:08:34.509Z
Learning: josefinalliende prefers centralizing validation logic (like trimming whitespace) in providers rather than handling it defensively at every consumer site. This approach was applied to activePubkeyProvider in the whitenoise_flutter codebase.

Applied to files:

  • lib/utils/message_converter.dart
🔇 Additional comments (4)
test/utils/reaction_converter_test.dart (1)

1-124: Tests are solid — add a format‑mismatch case (hex ↔ npub)

Add a test where usersMap keys are hex while ReactionSummary.user values are bech32 (npub...) and the inverse to prevent known users being treated as "Unknown User" if formats differ.

Check usersMap construction at lib/config/providers/group_messages_provider.dart (_fetchGroupUsersMap, _mapContactModelsToDomainUsers) and the converter at lib/utils/reaction_converter.dart (fromReactionSummary); also review usages in lib/utils/message_converter.dart.

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

149-165: Good test harness (overrides + teardown).

ProviderContainer overrides are clean, and teardown disposes correctly.

Also applies to: 167-169

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

62-74: LGTM: early return on missing activePubkey and stable ascending sort.

The flow is clear and side‑effect free.

lib/utils/message_converter.dart (1)

9-32: Solid, side‑effect‑free conversion path.

Clean separation of concerns; reply handling and reaction mapping are straightforward.

Also applies to: 45-58

@josefinalliende josefinalliende marked this pull request as draft September 19, 2025 02:00
@josefinalliende josefinalliende force-pushed the refactor/last-step-to-remove-metadata-cache branch from 2d99577 to f1ee229 Compare September 19, 2025 14:48
@josefinalliende josefinalliende force-pushed the refactor/last-step-to-remove-metadata-cache branch from f1ee229 to 9ef3e09 Compare September 19, 2025 16:16
This was unlinked from issues Sep 25, 2025
@josefinalliende josefinalliende force-pushed the refactor/last-step-to-remove-metadata-cache branch 2 times, most recently from 1317a0d to 1504a15 Compare September 25, 2025 04:35
@josefinalliende
Copy link
Contributor Author

This PR is already too big, so fix for #644 will not be here :)

@josefinalliende josefinalliende marked this pull request as ready for review September 25, 2025 04:38
@josefinalliende josefinalliende force-pushed the refactor/last-step-to-remove-metadata-cache branch from 44bd366 to 9b8a376 Compare September 30, 2025 17:38
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

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

266-321: Fragile positional diffing; prefer ID‑based merge and preserve optimistic “sending.”

Comparing by index and using skip(currentMessages.length) assumes identical ordering and no gaps. Use ID sets to append truly new messages and detect edits/reactions, then re‑append recent “sending” messages from MessageFilterService.

Illustrative replacement:

-      if (newMessages.length != currentMessages.length) {
-        // New or deleted messages
-        hasChanges = true;
-      } 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;
-            break;
-          }
-        }
-      }
+      final currentById = {for (final m in currentMessages) m.id: m};
+      final newById = {for (final m in newMessages) m.id: m};
+      final currentIds = currentById.keys.toSet();
+      final newIds = newById.keys.toSet();
+
+      final addedIds = newIds.difference(currentIds);
+      final removedIds = currentIds.difference(newIds);
+      final commonIds = newIds.intersection(currentIds);
+
+      final bool contentChanged = commonIds.any((id) {
+        final a = newById[id]!, b = currentById[id]!;
+        return a.content != b.content ||
+            a.reactions.length != b.reactions.length ||
+            !_areReactionsEqual(a.reactions, b.reactions);
+      });
+
+      final bool hasChanges = addedIds.isNotEmpty || removedIds.isNotEmpty || contentChanged;
@@
-      if (hasChanges) {
-        if (newMessages.length > currentMessages.length) {
-          // Add only new messages to preserve performance
-          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.)
-          // But keep recent messages in sending state if they don't match the last server message
-          final messagesToKeep = MessageFilterService.getRecentSendingMessages(
-            currentMessages: currentMessages,
-            newMessages: newMessages,
-          );
-          state = state.copyWith(
-            groupMessages: {
-              ...state.groupMessages,
-              groupId: [...newMessages, ...messagesToKeep],
-            },
-          );
-          _logger.info(
-            'ChatProvider: Updated messages with content changes for group $groupId',
-          );
-        }
-        // Update group order when messages are updated
-        _updateGroupOrderForNewMessage(groupId);
-      }
+      if (hasChanges) {
+        final List<MessageModel> messagesToKeep = MessageFilterService.getRecentSendingMessages(
+          currentMessages: currentMessages,
+          newMessages: newMessages,
+        );
+        final List<MessageModel> merged = [
+          ...newMessages,
+          ...messagesToKeep,
+        ];
+        state = state.copyWith(
+          groupMessages: {
+            ...state.groupMessages,
+            groupId: merged,
+          },
+        );
+        _logger.info('ChatProvider: Merged ${addedIds.length} added, '
+            '${removedIds.length} removed, contentChanged=$contentChanged for group $groupId');
+        await _updateGroupOrderForNewMessage(groupId);
+      }

This aligns with the recent “keep recent sending messages” approach. Based on learnings.

🧹 Nitpick comments (9)
CHANGELOG.md (2)

14-14: Changelog entry now correctly under Unreleased.

Looks good. Consider cross‑linking the underlying issue for traceability: append a link to issue #541 since this PR “completes the fix.”

Example:

- - Removes metadata cache provider [#654](https://github.com/parres-hq/whitenoise_flutter/pull/654)
+ - Removes metadata cache provider [#654](https://github.com/parres-hq/whitenoise_flutter/pull/654) ([#541](https://github.com/parres-hq/whitenoise_flutter/issues/541))

21-21: Optional: reference the user‑visible bug it addresses.

Add a link to issue #612 for “double rendering” to help readers connect the fix.

- - Fixed double rendering issue for some messages [#654](https://github.com/parres-hq/whitenoise_flutter/pull/654)
+ - Fixed double rendering issue for some messages [#654](https://github.com/parres-hq/whitenoise_flutter/pull/654) ([#612](https://github.com/parres-hq/whitenoise_flutter/issues/612))
test/utils/message_converter_test.dart (1)

13-35: Declare explicit types for test constants and fixtures (per guidelines).

Our Dart guidelines require explicit types for variables. Please annotate these with String/User/Map types.

Apply pattern like:

-    final currentUserPublicKey = 'npub1zg69v7ys40x77y352eufp27daufrg4ncjz4ummcjx3t83y9tehhsqepuh0';
-    final otherUserPublicKey = 'npub140x77y352eufp27daufrg4ncjz4ummcjx3t83y9tehh3ydzk0zgqwxq8j0';
-    final groupId = 'group_123';
+    final String currentUserPublicKey = 'npub1zg69v7ys40x77y352eufp27daufrg4ncjz4ummcjx3t83y9tehhsqepuh0';
+    final String otherUserPublicKey = 'npub140x77y352eufp27daufrg4ncjz4ummcjx3t83y9tehh3ydzk0zgqwxq8j0';
+    final String groupId = 'group_123';
@@
-    final currentUser = domain_user.User(
+    final domain_user.User currentUser = domain_user.User(
@@
-    final otherUser = domain_user.User(
+    final domain_user.User otherUser = domain_user.User(
@@
-    final usersMap = <String, domain_user.User>{
+    final Map<String, domain_user.User> usersMap = <String, domain_user.User>{

Please propagate the same change to other locals (e.g., ChatMessage, List) throughout this file.
As per coding guidelines.

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

392-403: Avoid mutating parameters with ??=; use a local derived value.

final gId = groupId ??= state.selectedGroupId; mutates the parameter and may violate lints (parameter_assignments). Use a pure expression.

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

405-416: Same parameter-mutation issue in isNextSameSender.

Mirror the fix to avoid ??= on the parameter.

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

106-109: Avoid magic number for kind=9.

Extract a named constant (e.g., kNostrKindText = 9) or use an enum from the API if available, and reuse in send/reply.

-    int kind = 9, // Default to text message
+    int kind = kNostrKindText, // Default to text message

and declare:

const int kNostrKindText = 9;

327-329: Await group order update to avoid dropped errors.

checkForNewMessages doesn’t await the async update; prefer awaiting to ensure ordering and error surfacing.

-        _updateGroupOrderForNewMessage(groupId);
+        await _updateGroupOrderForNewMessage(groupId);

586-597: Include NIP-10 reply tags
Per NIP-10, reply events should use:

  • an e-tag with [eventId, relayURL, "reply", authorPubkey]
  • a p-tag for the replied-to event’s author
    Fallback to a minimal e-tag when replyToMessage is null.
// Create tags for reply per NIP-10
final List<Tag> replyTags = [
  if (replyToMessage != null) ...[
    await tagFromVec(vec: [
      'e',
      replyToMessage.id,
      '',                  // relay URL (empty if unknown)
      'reply',             // marker
      replyToMessage.sender.publicKey,
    ]),
    await tagFromVec(vec: ['p', replyToMessage.sender.publicKey]),
  ] else
    await tagFromVec(vec: ['e', replyToMessageId]),
];

k-tags aren’t defined by NIP-10/NIP-18 and should be omitted.

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

90-103: Use Future.value() or remove unnecessary Future.microtask wrapper.

The Future.microtask(() async { ... }) pattern is unusual here. Since the entire body is already async and you're returning a Future<Map<...>>, you can simplify this to just execute the async block directly.

Apply this diff:

-  Future<Map<String, domain_user.User>> _fetchGroupUsersMap() async {
-    return await Future.microtask(() async {
-      final activePubkey = ref.read(activePubkeyProvider);
-      if (activePubkey == null || activePubkey.isEmpty) {
-        return {};
-      }
-      final groupContacts = await _fetchGroupUsersProfileData(activePubkey);
-      final domainUsersMap = _mapContactModelsToDomainUsers(
-        activePubkey: activePubkey,
-        contacts: groupContacts,
-      );
-      return domainUsersMap;
-    });
+  Future<Map<String, domain_user.User>> _fetchGroupUsersMap() async {
+    final activePubkey = ref.read(activePubkeyProvider);
+    if (activePubkey == null || activePubkey.isEmpty) {
+      return {};
+    }
+    final groupContacts = await _fetchGroupUsersProfileData(activePubkey);
+    final domainUsersMap = _mapContactModelsToDomainUsers(
+      activePubkey: activePubkey,
+      contacts: groupContacts,
+    );
+    return domainUsersMap;
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5751ed7 and 9b8a376.

📒 Files selected for processing (13)
  • CHANGELOG.md (1 hunks)
  • lib/config/providers/chat_provider.dart (9 hunks)
  • lib/config/providers/group_messages_provider.dart (1 hunks)
  • lib/config/providers/metadata_cache_provider.dart (0 hunks)
  • lib/domain/services/message_filter_service.dart (1 hunks)
  • lib/ui/chat/utils/message_extensions.dart (0 hunks)
  • lib/utils/message_converter.dart (2 hunks)
  • lib/utils/pubkey_utils.dart (1 hunks)
  • lib/utils/reaction_converter.dart (1 hunks)
  • test/config/providers/group_messages_provider_test.dart (1 hunks)
  • test/domain/services/message_filter_service_test.dart (1 hunks)
  • test/utils/message_converter_test.dart (1 hunks)
  • test/utils/reaction_converter_test.dart (1 hunks)
💤 Files with no reviewable changes (2)
  • lib/config/providers/metadata_cache_provider.dart
  • lib/ui/chat/utils/message_extensions.dart
🚧 Files skipped from review as they are similar to previous changes (5)
  • test/utils/reaction_converter_test.dart
  • lib/utils/reaction_converter.dart
  • lib/domain/services/message_filter_service.dart
  • lib/utils/pubkey_utils.dart
  • test/domain/services/message_filter_service_test.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:

  • test/utils/message_converter_test.dart
  • lib/utils/message_converter.dart
  • test/config/providers/group_messages_provider_test.dart
  • lib/config/providers/chat_provider.dart
  • lib/config/providers/group_messages_provider.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/utils/message_converter_test.dart
  • test/config/providers/group_messages_provider_test.dart
**/*.md

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

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

Files:

  • CHANGELOG.md
🧠 Learnings (3)
📚 Learning: 2025-09-07T02:15:31.931Z
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#594
File: lib/src/rust/api/utils.dart:12-16
Timestamp: 2025-09-07T02:15:31.931Z
Learning: The lingering PublicKey references in lib/config/providers/auth_provider.dart, lib/config/providers/group_provider.dart, and rust_builder/cargokit/build_tool/lib/src/options.dart identified during the utils.dart API cleanup were fixed in PR #597 in the whitenoise_flutter codebase.

Applied to files:

  • lib/config/providers/chat_provider.dart
📚 Learning: 2025-09-29T20:12:50.785Z
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#654
File: lib/domain/services/message_filter_service.dart:0-0
Timestamp: 2025-09-29T20:12:50.785Z
Learning: In the MessageFilterService.getRecentSendingMessages method in lib/domain/services/message_filter_service.dart, the implemented solution checks the last 10 messages within a 1-minute timeframe when comparing optimistic "sending" messages against server confirmations to prevent double-rendering. This balanced approach avoids performance issues while handling typical server response times (usually < 2 seconds) with a generous buffer.

Applied to files:

  • lib/config/providers/chat_provider.dart
📚 Learning: 2025-09-14T21:22:00.962Z
Learnt from: Quwaysim
PR: parres-hq/whitenoise_flutter#634
File: lib/config/providers/group_provider.dart:1130-1132
Timestamp: 2025-09-14T21:22:00.962Z
Learning: In the whitenoise_flutter codebase, the _updateGroupInfo method in GroupsNotifier performs optimistic updates that directly modify Group objects in the provider state (groups list and groupsMap). For non-DM groups, the display name comes from group.name, so updating the Group object directly is sufficient to reflect name changes in the UI without needing to refresh the separate display name cache.

Applied to files:

  • lib/config/providers/group_messages_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 (31)
test/config/providers/group_messages_provider_test.dart (12)

10-17: LGTM - Mock follows constructor injection pattern.

The mock correctly inherits from ActivePubkeyNotifier and uses constructor parameters to inject test behavior.


19-33: LGTM - Mock provides appropriate test data.

The MockUserProfileDataNotifier correctly simulates the profile data provider with a fallback for unknown users.


35-51: LGTM - Mock implements required interface.

The MockPubkeyFormatter provides necessary methods for test scenarios.


53-76: LGTM - Helper factories simplify test setup.

These factory functions effectively create mock implementations for dependency injection in tests.


143-164: LGTM - Test container factory provides flexible setup.

The createContainer helper allows easy customization of test scenarios with sensible defaults.


170-178: LGTM - Verifies state initialization.

Confirms the provider correctly stores the groupId in its state.


182-189: LGTM - Tests null pubkey edge case.

Validates that the provider returns an empty list when no active user is present.


193-201: LGTM - Tests empty pubkey edge case.

Validates that the provider returns an empty list for empty pubkey strings.


220-230: LGTM - Verifies message count and ordering.

Tests confirm correct message retrieval and chronological ordering by createdAt.


232-237: LGTM - Validates "You" label for active user.

Confirms that messages from the active user display "You" while others show their actual names.


257-262: LGTM - Tests display name mapping for different active users.

Validates the "You" labeling changes correctly based on the active user context.


282-287: LGTM - Completes display name test coverage.

Tests the third user scenario to ensure consistent "You" labeling behavior.

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

13-19: LGTM - Simple state with required groupId.

The state class correctly encapsulates the groupId parameter.


21-53: LGTM - Constructor enables dependency injection for testing.

The notifier correctly implements constructor injection with sensible defaults, allowing test doubles to be provided.


55-60: LGTM - Initializes state with groupId.

The build method correctly creates the initial state.


62-74: LGTM - Fetches and converts messages correctly.

The method guards against null/empty pubkeys, fetches messages, sorts chronologically, and delegates conversion appropriately.


76-88: LGTM - Delegates to MessageConverter with enriched user data.

The conversion method correctly fetches the users map and passes it to the converter.


105-116: LGTM - Concurrent profile fetches improve performance.

Using Future.wait to fetch multiple user profiles concurrently is an efficient approach.


118-127: LGTM - Fetches group member profiles.

Correctly retrieves member pubkeys and delegates to profile fetching.


129-141: LGTM - Maps contacts to domain users.

The transformation from MapEntry<String, ContactModel> to Map<String, domain_user.User> is clean and functional.


143-152: LGTM - Implements "You" labeling for active user.

The display name logic correctly distinguishes the active user from others.


154-165: LGTM - Converts ContactModel to domain User.

The conversion correctly maps all necessary fields from contact to domain user.


168-171: LGTM - Declares family provider correctly.

The provider export follows the Riverpod family pattern appropriately.

lib/utils/message_converter.dart (8)

9-57: LGTM - Synchronous conversion with efficient user lookup.

The refactored fromChatMessage correctly:

  • Uses the provided usersMap for sender lookup (addressing past review feedback)
  • Falls back to _unknownUser for missing entries
  • Handles reactions conditionally via ReactionConverter
  • Creates reply placeholders when needed

59-99: LGTM - Two-pass approach resolves out-of-order replies.

The refactored fromChatMessageList correctly:

  • Builds chatMessagesMap for reply lookups (addressing past review feedback about out-of-order messages)
  • Creates all MessageModels first, then enriches with reply references
  • Uses separate maps to track messages and reply relationships

This approach successfully handles the Nostr scenario where reply timestamps may precede parent timestamps.


101-133: LGTM - Creates optimistic UI updates for sending state.

The createOptimisticMessage method correctly generates temporary messages with "You" as display name and sending status.


135-142: LGTM - Provides fallback for missing user data.

The helper creates a reasonable placeholder for unknown users.


144-163: LGTM - Iterates messages to add reply references.

The helper correctly maps over messages and delegates to _addReply.


165-190: LGTM - Conditionally adds reply to message.

The method correctly checks if a message is a reply and updates it with the reply reference via _findReply.


192-208: LGTM - Looks up reply from messageModelsMap.

The helper correctly retrieves the reply message from the map built during the first pass, falling back to a placeholder when not found. This addresses the out-of-order reply scenario discussed in past reviews.


210-229: LGTM - Builds placeholder for missing replies.

The helper creates a sensible placeholder message when the parent message cannot be found.

@josefinalliende josefinalliende marked this pull request as draft September 30, 2025 17:52
@josefinalliende josefinalliende force-pushed the refactor/last-step-to-remove-metadata-cache branch from 9b8a376 to a19b091 Compare September 30, 2025 22:05
@josefinalliende josefinalliende marked this pull request as ready for review September 30, 2025 22:08
@josefinalliende josefinalliende force-pushed the refactor/last-step-to-remove-metadata-cache branch from a19b091 to 7d92120 Compare September 30, 2025 22:29
@josefinalliende
Copy link
Contributor Author

Converted to draft and removed reviewers after discovering a serious issue with replies while addressing the last CodeRabbit comment. Initially thought it was just an edge case, but it isn’t. I’ll update when it’s ready for review again

This is fixed, now replies work as normal

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
lib/config/providers/group_messages_provider.dart (1)

72-73: Consider performance impact of in-place sort on large message lists.

The code sorts chatMessages in-place before conversion. For groups with thousands of messages, this could be noticeable. Consider whether the Rust API can return pre-sorted messages or if sorting should happen at the database level.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a19b091 and 7d92120.

📒 Files selected for processing (8)
  • CHANGELOG.md (1 hunks)
  • lib/config/providers/chat_provider.dart (9 hunks)
  • lib/config/providers/group_messages_provider.dart (1 hunks)
  • lib/config/providers/metadata_cache_provider.dart (0 hunks)
  • lib/domain/services/message_filter_service.dart (1 hunks)
  • lib/ui/chat/utils/message_extensions.dart (0 hunks)
  • test/config/providers/group_messages_provider_test.dart (1 hunks)
  • test/domain/services/message_filter_service_test.dart (1 hunks)
💤 Files with no reviewable changes (2)
  • lib/ui/chat/utils/message_extensions.dart
  • lib/config/providers/metadata_cache_provider.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/domain/services/message_filter_service.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:

  • test/domain/services/message_filter_service_test.dart
  • test/config/providers/group_messages_provider_test.dart
  • lib/config/providers/group_messages_provider.dart
  • lib/config/providers/chat_provider.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/domain/services/message_filter_service_test.dart
  • test/config/providers/group_messages_provider_test.dart
**/*.md

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

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

Files:

  • CHANGELOG.md
🧠 Learnings (3)
📚 Learning: 2025-09-29T20:12:50.785Z
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#654
File: lib/domain/services/message_filter_service.dart:0-0
Timestamp: 2025-09-29T20:12:50.785Z
Learning: In the MessageFilterService.getRecentSendingMessages method in lib/domain/services/message_filter_service.dart, the implemented solution checks the last 10 messages within a 1-minute timeframe when comparing optimistic "sending" messages against server confirmations to prevent double-rendering. This balanced approach avoids performance issues while handling typical server response times (usually < 2 seconds) with a generous buffer.

Applied to files:

  • test/domain/services/message_filter_service_test.dart
  • lib/config/providers/chat_provider.dart
📚 Learning: 2025-09-14T21:22:00.962Z
Learnt from: Quwaysim
PR: parres-hq/whitenoise_flutter#634
File: lib/config/providers/group_provider.dart:1130-1132
Timestamp: 2025-09-14T21:22:00.962Z
Learning: In the whitenoise_flutter codebase, the _updateGroupInfo method in GroupsNotifier performs optimistic updates that directly modify Group objects in the provider state (groups list and groupsMap). For non-DM groups, the display name comes from group.name, so updating the Group object directly is sufficient to reflect name changes in the UI without needing to refresh the separate display name cache.

Applied to files:

  • lib/config/providers/group_messages_provider.dart
📚 Learning: 2025-09-07T02:15:31.931Z
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#594
File: lib/src/rust/api/utils.dart:12-16
Timestamp: 2025-09-07T02:15:31.931Z
Learning: The lingering PublicKey references in lib/config/providers/auth_provider.dart, lib/config/providers/group_provider.dart, and rust_builder/cargokit/build_tool/lib/src/options.dart identified during the utils.dart API cleanup were fixed in PR #597 in the whitenoise_flutter codebase.

Applied to files:

  • CHANGELOG.md
⏰ 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 (26)
lib/config/providers/chat_provider.dart (13)

3-3: LGTM - Import added for collection package.

The collection import is needed for firstWhereOrNull used in the reply handling fix.


9-9: LGTM - Import added for group messages provider.

This import supports the provider-driven message fetching approach that reduces responsibilities in chat_provider.


13-13: LGTM - Import added for message filter service.

This import is needed for the getRecentSendingMessages method that prevents double-rendering of optimistic messages.


79-79: LGTM - Proper delegation to group messages provider.

The refactor correctly delegates message fetching to groupMessagesProvider, reducing duplication and centralizing message conversion logic.


122-146: LGTM - Clean optimistic message creation and state update.

The optimistic message flow is well-structured:

  1. Creates optimistic message immediately
  2. Appends to current messages
  3. Updates state with sending flag

This provides responsive UX while the server request is in flight.


160-170: LGTM - Proper optimistic message update on success.

The success path correctly updates the optimistic message with the server-assigned id, timestamp, and sent status.


197-217: LGTM - Proper error handling for optimistic messages.

The error path correctly marks the optimistic message as failed and preserves it in the list, allowing the UI to show the failure state.


310-319: LGTM - Proper integration with MessageFilterService.

The code correctly uses MessageFilterService.getRecentSendingMessages to merge recent optimistic messages with new server messages, preventing double-rendering while preserving unsent messages.

Based on learnings.


551-558: LGTM - Reply crash fix applied correctly.

The fix uses firstWhereOrNull instead of firstWhere, preventing crashes when the reply target isn't in memory. This addresses the serious issue noted in the PR objectives.


560-581: LGTM - Optimistic reply message handling.

The optimistic reply flow mirrors the regular send flow, creating an immediate UI response with the optional replyToMessage reference.


600-620: LGTM - Proper success handling for reply messages.

The success path correctly updates the optimistic reply message with server-assigned values and clears the sending state.


635-653: LGTM - Proper error handling for reply messages.

The error path correctly marks the optimistic reply as failed and preserves it in the message list.


116-120: No dangling optimistic state on early exit. The early-return guard runs before any optimistic message is created, and _setGroupError resets sendingStates to false, so there’s no leftover optimistic state to clear.

Likely an incorrect or invalid review comment.

lib/config/providers/group_messages_provider.dart (13)

1-12: LGTM - Proper imports and aliasing.

The imports are well-organized, and the aliasing of domain_user and groups_api prevents naming conflicts while maintaining clarity.


13-19: LGTM - Clean state class.

The GroupMessagesState class is minimal and focused, containing only the required groupId.


21-54: LGTM - Excellent testability through dependency injection.

The constructor accepts optional function overrides for fetchAggregatedMessagesForGroupFn, groupMembersFn, and isMeFn, enabling easy mocking in tests while defaulting to production implementations.


55-60: LGTM - Simple build initialization.

The build method correctly initializes the state with the provided groupId.


76-88: LGTM - Clean message conversion flow.

The _toMessageModels method properly fetches the users map and delegates to MessageConverter.fromChatMessageList for conversion.


90-101: LGTM - Proper user map construction.

The method fetches group member profiles and maps them to domain user objects with proper error handling for missing active pubkey.


103-114: LGTM - Efficient concurrent profile fetching.

The method correctly uses Future.wait to fetch multiple user profiles concurrently, improving performance for groups with many members.


116-125: LGTM - Clean composition of profile fetching.

The method properly composes _groupMembers and _fetchUsersProfileData to fetch profiles for all group members.


127-139: LGTM - Proper mapping from contacts to domain users.

The method correctly transforms the list of contact entries into a map of domain user objects.


141-150: LGTM - Clean display name logic.

The method properly uses _isMe (via PubkeyUtils.isMe) to determine whether to show "You" or the contact's display name.


152-163: LGTM - Clean domain user construction.

The method properly constructs a domain_user.User from a ContactModel with appropriate field mappings and null coalescing for nip05.


166-169: LGTM - Proper family provider declaration.

The provider is correctly declared as a family provider that creates a new GroupMessagesNotifier for each unique groupId.


62-74: Verify concurrency of fetchMessages
Ensure that concurrent invocations of fetchMessages (e.g. rapid or overlapping refreshes) won’t trigger duplicate _fetchAggregatedMessagesForGroup calls or lead to out-of-order results under race conditions.

@josefinalliende josefinalliende marked this pull request as draft September 30, 2025 22:36
@josefinalliende josefinalliende force-pushed the refactor/last-step-to-remove-metadata-cache branch from 7d92120 to 8bfc674 Compare September 30, 2025 23:36
@josefinalliende josefinalliende marked this pull request as ready for review September 30, 2025 23:38
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

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

79-90: Do not drop local optimistic bubbles on load
Line [81] writes the raw DB list back into state, so any in-flight or failed message you were showing disappears the moment a refresh completes (and the send success handler can no longer locate the optimistic entry by id). Please merge the freshly fetched records with the current state via MessageMergerService.merge before updating groupMessages.

-      final messages = await ref.read(groupMessagesProvider(groupId).notifier).fetchMessages();
-
-      state = state.copyWith(
+      final messages = await ref.read(groupMessagesProvider(groupId).notifier).fetchMessages();
+      final currentStateMessages = state.groupMessages[groupId] ?? <MessageModel>[];
+      final mergedMessages = MessageMergerService.merge(
+        stateMessages: currentStateMessages,
+        dbMessages: messages,
+      );
+
+      state = state.copyWith(
         groupMessages: {
           ...state.groupMessages,
-          groupId: messages,
+          groupId: mergedMessages,
         },

That keeps optimistic UI consistent across reloads.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7d92120 and 8bfc674.

📒 Files selected for processing (6)
  • CHANGELOG.md (1 hunks)
  • lib/config/providers/chat_provider.dart (9 hunks)
  • lib/config/providers/metadata_cache_provider.dart (0 hunks)
  • lib/domain/services/message_merger_service.dart (1 hunks)
  • lib/ui/chat/utils/message_extensions.dart (0 hunks)
  • test/domain/services/message_merger_service_test.dart (1 hunks)
💤 Files with no reviewable changes (2)
  • lib/config/providers/metadata_cache_provider.dart
  • lib/ui/chat/utils/message_extensions.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md
🧰 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/domain/services/message_merger_service.dart
  • lib/config/providers/chat_provider.dart
  • test/domain/services/message_merger_service_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/domain/services/message_merger_service_test.dart
🧠 Learnings (1)
📚 Learning: 2025-09-29T20:12:50.785Z
Learnt from: josefinalliende
PR: parres-hq/whitenoise_flutter#654
File: lib/domain/services/message_filter_service.dart:0-0
Timestamp: 2025-09-29T20:12:50.785Z
Learning: In the MessageFilterService.getRecentSendingMessages method in lib/domain/services/message_filter_service.dart, the implemented solution checks the last 10 messages within a 1-minute timeframe when comparing optimistic "sending" messages against server confirmations to prevent double-rendering. This balanced approach avoids performance issues while handling typical server response times (usually < 2 seconds) with a generous buffer.

Applied to files:

  • lib/domain/services/message_merger_service.dart
🔇 Additional comments (1)
test/domain/services/message_merger_service_test.dart (1)

38-206: Add a regression for failed optimistic messages
Please add a test case where stateMessages contains a MessageStatus.failed entry while dbMessages lacks it. After fixing the merger, the failed bubble should still be present in the result, ensuring we don’t regress again.

Copy link
Member

@erskingardner erskingardner left a comment

Choose a reason for hiding this comment

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

@josefinalliende I won't have time today to review this super carefully. I have had a quick look through everything though and didn't notice anything glaring. I trust you've checked it super thoroughly. I say 🚢 when you're ready.

@josefinalliende josefinalliende merged commit d378b7d into master Oct 1, 2025
2 checks passed
@josefinalliende josefinalliende deleted the refactor/last-step-to-remove-metadata-cache branch October 1, 2025 13:55
This was referenced Oct 1, 2025
@coderabbitai coderabbitai bot mentioned this pull request Oct 29, 2025
11 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Two messages show briefly when sending

4 participants