Skip to content

Conversation

@xsahil03x
Copy link
Member

@xsahil03x xsahil03x commented Oct 21, 2025

Submit a pull request

Fixes: FLU-266

Description of the pull request

Adds support for the user.messages.deleted WebSocket event.

This event is now handled at the client level and propagated to all active channels. Each channel will then proceed to either soft or hard delete all messages from the specified user, depending on the event payload.

Refactors message update and removal logic to support bulk operations, improving performance and maintainability.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for the user.messages.deleted event to handle global message deletion scenarios.
    • Implemented user message deletion capabilities with both hard and soft delete options.
    • Added message limiting per channel for optimized query performance.
  • Improvements

    • Enhanced message state management and synchronization across channels and threads.
    • Improved handling of deleted messages in local state and persistence layer.

Adds support for the `user.messages.deleted` WebSocket event.

This event is now handled at the client level and propagated to all active channels. Each channel will then proceed to either soft or hard delete all messages from the specified user, depending on the event payload.

Refactors message update and removal logic to support bulk operations, improving performance and maintainability.
This commit refactors the message handling logic within the `Channel` class to improve robustness and correctness, especially for delete and update operations.

- **Unified Message Handling:** Introduces internal methods `_updateMessages` and `_removeMessages` to serve as central points for modifying channel state. These methods now consistently update channel messages, thread messages, pinned messages, and active live locations.

- **Improved Deletion Logic:** The `deleteMessagesFromUser` method now correctly identifies all messages from a user, including those in threads, and marks them for deletion. The logic for hard and soft deletes is now handled more robustly.

- **State Update Refactoring:** Message list modifications (updates and removals) have been refactored into dedicated helper methods (`_mergeMessagesIntoExisting`, `_removeMessagesFromExisting`, etc.). This change centralizes logic and ensures that related state, such as quoted messages and pinned messages, is updated consistently.

- **Pinned Message and Live Location Consistency:** Pinned messages and active live locations are now correctly removed or updated when their underlying messages are deleted or modified. A deleted message is no longer considered a valid pin.

- **Generalized `merge` Extension:** A new `mergeFrom` extension method is introduced, allowing an iterable to be merged with an iterable of a different type. This provides more flexibility than the original `merge` method.

- **Event Listener Organization:** Event listener registrations in both `Client` and `Channel` have been reordered to group related listeners, improving code readability.
Refactors the tests for the `user.messages.deleted` WebSocket event to improve reliability and correctness.

- Connects the client and waits for a connected status in `setUp`, ensuring a realistic test environment.
- Simplifies event dispatching by sending a single global event instead of one per channel.
- Removes an unnecessary test for channel-specific `user.messages.deleted` events, as these are handled by the channel itself.
- Adds tests to verify that messages are correctly deleted from the persistence client when `hardDelete` is true.
Introduces a new method `deleteMessagesFromUser` to the `ChatPersistenceClient` to allow for the bulk deletion (hard or soft) of all messages from a specific user. This operation can be scoped to a particular channel or applied globally.

The implementation is added to `StreamChatPersistenceClient`, which now delegates this action to both `messageDao` and `pinnedMessageDao` to ensure all user messages, including pinned ones, are removed from the local database.

The `Channel` class now calls this new persistence method when handling a `user.messages.deleted` event to ensure that all of a user's messages are cleared from local storage, not just those currently loaded in the channel's state.

Comprehensive unit tests have been added for the new DAO methods, the persistence client implementation, and the updated channel event handling logic.
This change introduces an optional `messageLimit` parameter to the `getChannelStates` method in `StreamChatPersistenceClient`.

This allows developers to control the number of messages fetched for each channel when querying offline channel states, defaulting to 25 if not specified. The new parameter is passed down through `StreamChatClient` and `ChatPersistenceClient` to the database query layer.
Increments the database schema version from 26 to 27.
Refactors the tests for the `user.messages.deleted` WebSocket event to improve reliability and correctness.

- Connects the client and waits for a connected status in `setUp`, ensuring a realistic test environment.
- Simplifies event dispatching by sending a single global event instead of one per channel.
- Removes an unnecessary test for channel-specific `user.messages.deleted` events, as these are handled by the channel itself.
- Adds tests to verify that messages are correctly deleted from the persistence client when `hardDelete` is true.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 21, 2025

Walkthrough

This PR introduces support for the user.messages.deleted event and implements user-level message deletion across the Stream Chat client. Changes include a new event type constant, refactored message update/delete operations in the Channel client, per-channel message limits for offline/online queries, new persistence methods for hard/soft user message deletion, event listening with channel-wide propagation, and comprehensive test coverage for deletion scenarios.

Changes

Cohort / File(s) Summary
Changelog entries
packages/stream_chat/CHANGELOG.md, packages/stream_chat_persistence/CHANGELOG.md
Adds "Upcoming Beta" entries documenting support for user.messages.deleted event and new deleteMessagesFromUser persistence API with messageLimit parameter for getChannelStates.
Event type definition
packages/stream_chat/lib/src/event_type.dart
Adds new public constant userMessagesDeleted = 'user.messages.deleted' to EventType class.
Extension utilities
packages/stream_chat/lib/src/core/util/extension.dart
Adds public methods merge<K> and mergeFrom<K, V> to Iterable for merging collections with key-based reconciliation and optional value mapping.
Channel state management
packages/stream_chat/lib/src/client/channel.dart
Refactors message update/delete logic: replaces in-place mutations with modular flow via _updateMessages, _removeMessages, and new _deleteMessages / _deleteMessagesFromUser methods; adds _listenUserMessagesDeleted event handler; introduces _mergeMessagesIntoExisting helper; adds pin validation for deleted messages; updates updateChannelState and updateThreadInfo to use merged logic.
Client initialization & query parameters
packages/stream_chat/lib/src/client/client.dart
Adds optional messageLimit parameter (default 25) to queryChannelsOffline and queryChannelsOnline; adds _listenUserMessagesDeleted() invocation in subscribeToEvents to broadcast user deletion events across all cached channels.
Persistence interface
packages/stream_chat/lib/src/db/chat_persistence_client.dart
Updates getChannelStates signature to accept optional messageLimit parameter; adds new public method deleteMessagesFromUser for user-scoped hard/soft deletion.
Persistence DAO layer
packages/stream_chat_persistence/lib/src/dao/message_dao.dart, packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart
Adds deleteMessagesByUser method supporting hard delete (remove rows) and soft delete (mark type='deleted', set remoteDeletedAt); handles optional channel filtering and returns affected row count.
Persistence implementation
packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart
Adds deleteMessagesFromUser method delegating to both message and pinned-message DAOs; updates getChannelStates to accept and apply messageLimit via per-channel PaginationParams.
Schema versioning
packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart
Bumps Drift schema version from 1026 to 1027.
Client tests
packages/stream_chat/test/src/client/channel_test.dart
Adds test group "User messages deleted event" with MockPersistenceClient; covers soft/hard deletion of user messages, thread message handling, persistence method invocations, and no-op edge cases.
Client integration tests
packages/stream_chat/test/src/client/client_test.dart
Adds WebSocket event test validating global userMessagesDeleted event propagation across multiple channels; asserts message state changes and preservation of non-target-user messages.
Persistence test mocks
packages/stream_chat/test/src/db/chat_persistence_client_test.dart
Extends TestPersistenceClient mock with deleteMessagesFromUser override and updates getChannelStates signature to include messageLimit parameter.
Persistence DAO tests
packages/stream_chat_persistence/test/src/dao/message_dao_test.dart, packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart
Adds comprehensive deleteMessagesByUser test groups covering hard/soft deletion per-channel and cross-channel, thread message handling, and state verification for both message and pinned-message tables.
Persistence integration tests
packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart
Adds tests for deleteMessagesFromUser method with hard/soft delete and cross-channel scenarios; updates existing tests to pass messagePagination to getMessagesByCid and mock draftMessageDao.getDraftMessageByCid.

Sequence Diagram

sequenceDiagram
    actor WS as WebSocket
    participant Client
    participant Channel
    participant Persistence
    participant Store as DB

    WS->>Client: userMessagesDeleted event
    activate Client
    Client->>Client: _listenUserMessagesDeleted()
    
    alt event.cid is null
        Client->>Client: iterate all cached channels
        loop for each channel
            Client->>Channel: forward event
        end
    else event.cid is set
        Client->>Channel: forward event to specific channel
    end
    deactivate Client
    
    activate Channel
    Channel->>Channel: _listenUserMessagesDeleted()
    
    alt hardDelete = true
        Channel->>Channel: _deleteMessagesFromUser(hard)
        Channel->>Channel: _removeMessages (from memory)
        Channel->>Persistence: deleteMessageByIds (pinned)
        Channel->>Persistence: deleteMessageByIds (messages)
    else hardDelete = false
        Channel->>Channel: _deleteMessagesFromUser(soft)
        Channel->>Channel: mark messages deleted,<br/>set deletedAt, state flag
    end
    
    Persistence->>Store: execute deletions
    Store-->>Persistence: confirm
    deactivate Channel
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Rationale: This PR involves substantial refactoring of core message handling logic in channel.dart (new multi-method pattern with dedicated helpers), introduces new event handling infrastructure in client.dart, extends the persistence layer with new deletion semantics across DAOs, adds new public API signatures with optional parameters, and spans 15+ files with mixed complexity. While many changes follow a consistent pattern (e.g., repetitive test coverage), the core logic changes in channel state management and the coordination of hard/soft delete flows across in-memory state and persistence require careful review.

Possibly related PRs

  • GetStream/stream-chat-flutter#2350: Modifies ChannelClientState.updateChannelState to add push preferences handling, intersecting with this PR's refactoring of the same method.
  • GetStream/stream-chat-flutter#2394: Extends per-user deletion flows and event handling in Channel, Client, and persistence layers, overlapping directly with this PR's deletion and event-propagation infrastructure.

Suggested reviewers

  • renefloor
  • Brazol

Poem

🐰 Hoppy news! A message deleted,
User-wide, globally reflected!
Hard or soft, the choice is clear,
Events flow, no need to fear.
Pinned, threaded, merged with care—
Stream Chat's deletion, now more fair!

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "feat(llc, persistence): handle user.messages.deleted event" clearly and specifically describes the primary objective of the changeset. It accurately summarizes the main change—implementing support for handling the user.messages.deleted WebSocket event across the low-level client and persistence layers. The title is concise, directly related to the substantial changes throughout the codebase, and provides sufficient context for team members reviewing the repository history.
Linked Issues Check ✅ Passed The PR objectives indicate the changes should add support for the user.messages.deleted WebSocket event, handle it at the client level, propagate it to channels, and support soft/hard deletion. The changeset demonstrates these requirements are met: EventType.userMessagesDeleted is added, _listenUserMessagesDeleted handlers are implemented in both client and channel layers, deleteMessagesFromUser methods are added to persistence DAOs and clients supporting both hard and soft deletes, refactored message update/removal logic supports bulk operations, and comprehensive tests validate all scenarios. While the linked issue FLU-266 lacks a detailed description, all stated PR objectives from the issue reference appear to be implemented.
Out of Scope Changes Check ✅ Passed The changes are focused on implementing user.messages.deleted event handling and supporting infrastructure. Core changes include adding the new event type, client and channel-level event listeners, deletion logic in persistence layers, and comprehensive test coverage—all directly addressing the stated objective. Additional changes such as messageLimit parameter additions to queryChannelsOffline, queryChannelsOnline, and getChannelStates appear to be supporting infrastructure for controlling message fetching during bulk operations. The significant refactoring of message update/removal logic in channel.dart is explicitly mentioned in the PR objectives as necessary to support bulk operations, and the Drift schema version bump is a routine maintenance update tied to the persistence changes.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/user.messages.deleted

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.

Replaces the `MessageState.deleted` factory with a `switch` expression to set the message state.

This change simplifies how a message's state is updated upon deletion by using `MessageState.hardDeleted` or `MessageState.softDeleted` directly, based on the `hardDelete` boolean.
@xsahil03x xsahil03x changed the title feat(llc): handle user.messages.deleted event feat(llc, persistence): handle user.messages.deleted event Oct 21, 2025
@codecov
Copy link

codecov bot commented Oct 21, 2025

Codecov Report

❌ Patch coverage is 97.37991% with 6 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (v10.0.0@e6f5e25). Learn more about missing BASE report.

Files with missing lines Patch % Lines
packages/stream_chat/lib/src/client/channel.dart 96.57% 6 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             v10.0.0    #2423   +/-   ##
==========================================
  Coverage           ?   65.40%           
==========================================
  Files              ?      421           
  Lines              ?    26266           
  Branches           ?        0           
==========================================
  Hits               ?    17180           
  Misses             ?     9086           
  Partials           ?        0           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment on lines 3787 to 3793
final persistence = _channel.client.chatPersistenceClient;
persistence?.deleteMessagesFromUser(
userId: userId,
cid: _channel.cid,
hardDelete: hardDelete,
deletedAt: deletedAt,
);
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't we await here? Because otherwise we'll try to delete the same messages in _deleteMessages which might give a race condition?

Copy link
Member Author

Choose a reason for hiding this comment

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

Done in 728ac1b

@xsahil03x
Copy link
Member Author

xsahil03x commented Oct 21, 2025

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 21, 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.

1 similar comment
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 21, 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: 1

♻️ Duplicate comments (1)
packages/stream_chat/lib/src/client/channel.dart (1)

3777-3811: Persistence-first user-wide deletion is correct (await fixed).

Awaiting deleteMessagesFromUser before in-memory changes prevents races with subsequent removals. Matches earlier feedback; thanks for addressing.

🧹 Nitpick comments (9)
packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart (1)

245-278: Implementation is correct and consistent with message_dao.

The method properly mirrors the logic from message_dao.dart, handling both hard and soft deletion for pinned messages. While there is code duplication between the two DAOs, it's acceptable given the different Drift table types (PinnedMessagesCompanion vs MessagesCompanion).

If code duplication becomes a maintenance concern in the future, consider extracting the shared deletion logic into a helper function or mixin. However, this is not critical given the relatively small and stable nature of this code.

packages/stream_chat/test/src/client/channel_test.dart (4)

6663-6695: Soft-delete: assert deletedAt equals event time and verify persistence call.

Strengthen the test by checking deletedAt == event.createdAt and that persistence is instructed to soft‑delete user messages (with the same timestamp).

Apply this diff:

@@
-          final deletedAt = DateTime.now();
+          final deletedAt = DateTime.now();
@@
-          for (final message in deletedMessages!) {
+          for (final message in deletedMessages!) {
             expect(message.type, equals(MessageType.deleted));
             expect(message.deletedAt, isNotNull);
             expect(message.state.isDeleted, isTrue);
+            expect(message.deletedAt!.isAtSameMomentAs(deletedAt), isTrue);
           }
@@
-          expect(user2Message?.deletedAt, isNull);
+          expect(user2Message?.deletedAt, isNull);
+
+          // Persistence should receive a soft-delete for all user messages in storage
+          final capturedDeletedAt = verify(
+            () => persistenceClient.deleteMessagesFromUser(
+              cid: channel.cid,
+              userId: user1.id,
+              hardDelete: false,
+              deletedAt: captureAny(named: 'deletedAt'),
+            ),
+          ).captured.first as DateTime;
+          expect(capturedDeletedAt.isAtSameMomentAs(deletedAt), isTrue);

6791-6811: Thread soft-delete: set event.createdAt and assert thread messages’ deletedAt.

Improves correctness and parity with non-thread case.

Apply this diff:

@@
-          // Create user.messages.deleted event (soft delete)
-          final userMessagesDeletedEvent = Event(
+          // Create user.messages.deleted event (soft delete)
+          final deletedAt = DateTime.now();
+          final userMessagesDeletedEvent = Event(
             cid: channel.cid,
             type: EventType.userMessagesDeleted,
             user: user1,
             hardDelete: false,
+            createdAt: deletedAt,
           );
@@
-          for (final message in threadMessages!) {
+          for (final message in threadMessages!) {
             expect(message.type, equals(MessageType.deleted));
             expect(message.state.isDeleted, isTrue);
+            expect(message.deletedAt!.isAtSameMomentAs(deletedAt), isTrue);
           }

6963-6973: Soft-delete should still touch persistence (storage), not only in-memory.

Even when hardDelete is false, the storage layer should be instructed to soft-delete all user messages. Add an assertion for deleteMessagesFromUser with hardDelete: false.

Apply this diff:

@@
-          // Verify persistence deletion methods were NOT called
+          // Verify per-ID deletion methods were NOT called
           verifyNever(() => persistenceClient.deleteMessageByIds(any()));
           verifyNever(() => persistenceClient.deletePinnedMessageByIds(any()));
 
           // Verify message is soft deleted (still in state)
           expect(channel.state?.messages.length, equals(1));
           expect(
               channel.state?.messages.first.type, equals(MessageType.deleted));
+
+          // But storage should receive a soft-delete for all user messages
+          verify(
+            () => persistenceClient.deleteMessagesFromUser(
+              cid: channel.cid,
+              userId: user1.id,
+              hardDelete: false,
+              deletedAt: any(named: 'deletedAt'),
+            ),
+          ).called(1);

7063-7075: Also verify pinned IDs removed from persistence in the storage-wide delete test.

You already capture deleteMessageByIds. Mirror that for pinned IDs to ensure both tables are cleaned consistently.

Apply this diff:

@@
           final capturedIds = verify(
             () => persistenceClient.deleteMessageByIds(captureAny()),
           ).captured.first as List<String>;
@@
           expect(
             capturedIds,
             containsAll([
               'msg-1', // state message
               'thread-msg-1', // state thread message
             ]),
           );
+
+          // Pinned clean-up should include pinned state message
+          final capturedPinnedIds = verify(
+            () => persistenceClient.deletePinnedMessageByIds(captureAny()),
+          ).captured.first as List<String>;
+          expect(capturedPinnedIds, unorderedEquals(['msg-1']));
packages/stream_chat/lib/src/client/channel.dart (3)

3494-3498: Safe message merging into channel state.

_mergeMessagesIntoExisting with stable sort is clear. Watch for O(n log n) on very large merges; acceptable here.

If hot, consider skipping sort when toMerge is already >= last createdAt.


3800-3805: Minor: unify timestamp source.

You use DateTime.timestamp() elsewhere; here you use DateTime.now() for deletedAt/state. Consider using one consistently.

-        deletedAt: deletedAt ?? DateTime.now(),
+        deletedAt: deletedAt ?? DateTime.timestamp(),

4139-4154: Channel-scoped user.messages.deleted listener is correct.

Ignores null user, applies hard/soft via unified path. Consider logging count of affected messages for observability.

packages/stream_chat/lib/src/client/client.dart (1)

2396-2412: Global-to-channel event fan-out is sound.

Skips events already scoped to a cid, iterates cached channels, and reuses handleEvent so per-channel filters apply. Consider short-circuiting to only channels where the user is (recent) member to reduce no-op work on large caches.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e6f5e25 and 728ac1b.

📒 Files selected for processing (17)
  • packages/stream_chat/CHANGELOG.md (1 hunks)
  • packages/stream_chat/lib/src/client/channel.dart (7 hunks)
  • packages/stream_chat/lib/src/client/client.dart (5 hunks)
  • packages/stream_chat/lib/src/core/util/extension.dart (2 hunks)
  • packages/stream_chat/lib/src/db/chat_persistence_client.dart (2 hunks)
  • packages/stream_chat/lib/src/event_type.dart (1 hunks)
  • packages/stream_chat/test/src/client/channel_test.dart (1 hunks)
  • packages/stream_chat/test/src/client/client_test.dart (6 hunks)
  • packages/stream_chat/test/src/db/chat_persistence_client_test.dart (2 hunks)
  • packages/stream_chat_persistence/CHANGELOG.md (1 hunks)
  • packages/stream_chat_persistence/lib/src/dao/message_dao.dart (2 hunks)
  • packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart (2 hunks)
  • packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart (1 hunks)
  • packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart (2 hunks)
  • packages/stream_chat_persistence/test/src/dao/message_dao_test.dart (2 hunks)
  • packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart (2 hunks)
  • packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart (4 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-25T08:19:01.469Z
Learnt from: xsahil03x
PR: GetStream/stream-chat-flutter#2394
File: packages/stream_chat_flutter/lib/src/message_action/message_actions_builder.dart:82-92
Timestamp: 2025-09-25T08:19:01.469Z
Learning: In the Stream Chat Flutter library, when deleting a message with MessageSendingStatus.failed or MessageSendingStatus.failed_update status, the _deleteMessage method in channel.dart automatically handles deletion locally via _deleteLocalMessage without making API calls, preventing 404 errors and deletingFailed states.

Applied to files:

  • packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart
  • packages/stream_chat/lib/src/db/chat_persistence_client.dart
  • packages/stream_chat_persistence/lib/src/dao/message_dao.dart
  • packages/stream_chat/lib/src/client/channel.dart
📚 Learning: 2025-09-25T08:19:01.469Z
Learnt from: xsahil03x
PR: GetStream/stream-chat-flutter#2394
File: packages/stream_chat_flutter/lib/src/message_action/message_actions_builder.dart:82-92
Timestamp: 2025-09-25T08:19:01.469Z
Learning: In the Stream Chat Flutter library, when deleting a message that hasn't been sent to the server yet (message.remoteCreatedAt == null) or is bounced with error, the _deleteMessage method in channel.dart automatically handles deletion locally via _deleteLocalMessage without making API calls, preventing 404 errors and deletingFailed states.

Applied to files:

  • packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart
  • packages/stream_chat/lib/src/db/chat_persistence_client.dart
  • packages/stream_chat_persistence/lib/src/dao/message_dao.dart
  • packages/stream_chat/lib/src/client/channel.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). (4)
  • GitHub Check: build (ios)
  • GitHub Check: test
  • GitHub Check: build (android)
  • GitHub Check: format
🔇 Additional comments (26)
packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart (1)

60-60: LGTM! Schema version bumped appropriately.

The schema version increment aligns with the new persistence features introduced in this PR (user message deletion and message limit support).

packages/stream_chat/lib/src/event_type.dart (1)

193-194: LGTM! Event type constant added correctly.

The new userMessagesDeleted event type follows the established naming convention and is properly documented.

packages/stream_chat/lib/src/core/util/extension.dart (2)

75-86: LGTM! Clean delegation pattern.

The merge method provides a convenient API for same-type merging by delegating to the more general mergeFrom method with an identity mapper.


142-164: LGTM! Well-implemented merge utility with excellent documentation.

The mergeFrom method correctly handles null values, skips items when the value mapper returns null, and properly merges items by key. The comprehensive documentation with examples makes the API easy to understand.

packages/stream_chat/test/src/db/chat_persistence_client_test.dart (2)

52-58: LGTM! Test stub properly defined.

The deleteMessagesFromUser stub correctly throws UnimplementedError, which is appropriate for a test mock that's not meant to be called in this test suite.


90-95: LGTM! Signature updated to match new API.

The messageLimit parameter addition aligns with the new per-channel message limiting feature.

packages/stream_chat/CHANGELOG.md (1)

1-5: LGTM! Changelog entry is clear and well-formatted.

The entry correctly documents the new user.messages.deleted event support and follows the established changelog format.

packages/stream_chat_persistence/CHANGELOG.md (1)

1-8: LGTM! Changelog entries are comprehensive and well-documented.

Both new features are clearly described:

  1. The new deleteMessagesFromUser() method for user-level deletion
  2. The messageLimit parameter for controlling per-channel message pagination
packages/stream_chat_persistence/lib/src/dao/message_dao.dart (1)

248-281: Code implementation is correct — no changes needed.

The MessageState.softDeleted is properly serializable through the @freezed package's auto-generated toJson() method. Using jsonEncode(MessageState.softDeleted) in the soft delete operation is correct and consistent with existing codebase patterns.

packages/stream_chat/test/src/client/client_test.dart (1)

4114-4249: Excellent end-to-end coverage for user.ws deletions

Appreciate how the new tests exercise both soft and hard delete flows across multiple channels, mirroring the production ClientState._listenUserMessagesDeleted behavior. That should give us strong regression protection for the global event fan-out.

packages/stream_chat/lib/src/db/chat_persistence_client.dart (1)

133-193: API surface evolves cleanly

The additional messageLimit knob and the new deleteMessagesFromUser contract slot neatly into the abstraction—nice job tightening the persistence API without breaking cohesion.

packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart (1)

214-236: Delegation stays DRY and consistent

Great to see deleteMessagesFromUser fan out through both message and pinned-message DAOs with a shared parameter set via Future.wait; keeps the orchestration lean and consistent.

packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart (1)

431-608: Pinned-message delete paths thoroughly exercised

These specs do a solid job validating both scoped and global deletions, including soft-delete metadata—exactly what we need to guard the new DAO logic.

packages/stream_chat_persistence/test/src/dao/message_dao_test.dart (1)

440-634: Great breadth on message DAO coverage

Covering per-channel, global, and threaded cases (both hard and soft) gives high confidence the DAO implementation handles every path we care about.

packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart (1)

334-945: Integration tests nail the delegation contracts

The mock-based assertions around deleteMessagesFromUser and the updated expectations for per-channel message limits nicely validate the orchestration layer.

packages/stream_chat/lib/src/client/channel.dart (9)

2339-2339: Good addition: channel-level listener bootstrapped.

Wires up user.messages.deleted handling at channel state creation. Looks correct.


3210-3210: Update/remove paths centralized.

Delegating update/remove/delete to unified helpers reduces drift and keeps persistence/thread/pin/location state consistent. Nice.

Also applies to: 3218-3219, 3222-3223, 3226-3227


3597-3609: Thread merge path is consistent with channel path.

Maintains order and uses the same merge semantics. Good.


3813-3821: lastMessageAt behavior on deletions — confirm UX.

soft delete updates messages in place and recalculates lastMessageAt only via max(existing, createdAt). If the latest message becomes deleted, lastMessageAt won’t regress. Is this the intended UX? If not, consider recomputing from last non-deleted message.

Also applies to: 3855-3888


3823-3831: Solid fan-out: threads, channel, pins, live locations.

Update helpers correctly touch all affected collections and rely on a single merge function. Pin invalidation handled later. Good.

Also applies to: 3832-3853, 3890-3902, 3904-3916


3918-3944: Live-location cleanup on message changes.

Removes expired or deleted-attached locations. Correct keying and filters.


3946-3954: Merge semantics preserve quotedMessage and sync fields.

updated.syncWith(original) and quotedMessage preservation prevent UI flicker and dangling refs. Nice.

Also applies to: 3956-3988


3990-4003: Removals keep persistence and references in sync.

Pinned, threads, channel list, and locations are all pruned; persistence cleared via DAO calls. Looks robust.

Also applies to: 4005-4037, 4039-4063, 4065-4091, 4093-4106, 4108-4116, 4118-4137


4173-4185: Pin validity guard updated for deleted messages.

Treating deleted as invalid pin is the right call. 👍

packages/stream_chat/lib/src/client/client.dart (2)

657-657: messageLimit forwarded consistently (default 25).

Good parity between online/offline code paths; avoids surprising page sizes.

Also applies to: 738-740, 780-795


2281-2281: Subscribed to user.messages.deleted at client level.

Ensures propagation is active once websocket connects. Good placement.

Comment on lines +6888 to +6902
final message1 = Message(
id: 'msg-1',
text: 'Message from user 1',
user: user1,
);
final message2 = Message(
id: 'msg-2',
text: 'Another message from user 1',
user: user1,
);
final message3 = Message(
id: 'msg-3',
text: 'Message from user 2',
user: user2,
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Pinned deletion verification is incorrect; make a message pinned and assert only pinned IDs are sent.

The test expects deletePinnedMessageByIds to be called with all message IDs, even though none are pinned. This likely fails against an implementation that deletes pinned entries only for pinned messages. Pin one message and assert pinned IDs specifically; also capture lists to avoid order flakiness.

Apply this diff:

@@
-          final message1 = Message(
+          final message1 = Message(
             id: 'msg-1',
             text: 'Message from user 1',
             user: user1,
           );
-          final message2 = Message(
+          final message2 = Message(
             id: 'msg-2',
             text: 'Another message from user 1',
-            user: user1,
+            user: user1,
+            pinned: true, // make one message pinned
           );
@@
-          // Verify messages are removed from persistence
-          verify(
-            () => persistenceClient.deleteMessageByIds(['msg-1', 'msg-2']),
-          ).called(1);
-          verify(
-            () =>
-                persistenceClient.deletePinnedMessageByIds(['msg-1', 'msg-2']),
-          ).called(1);
+          // Verify messages are removed from persistence (order-independent)
+          final capturedMsgIds = verify(
+            () => persistenceClient.deleteMessageByIds(captureAny()),
+          ).captured.first as List<String>;
+          expect(capturedMsgIds, unorderedEquals(['msg-1', 'msg-2']));
+
+          // Only pinned IDs should be sent to pinned deletion
+          final capturedPinnedIds = verify(
+            () => persistenceClient.deletePinnedMessageByIds(captureAny()),
+          ).captured.first as List<String>;
+          expect(capturedPinnedIds, unorderedEquals(['msg-2']));

Also applies to: 6926-6933

@xsahil03x xsahil03x merged commit 0db0485 into v10.0.0 Oct 21, 2025
10 checks passed
@xsahil03x xsahil03x deleted the feat/user.messages.deleted branch October 21, 2025 14:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants