Skip to content

Conversation

xsahil03x
Copy link
Member

@xsahil03x xsahil03x commented Sep 26, 2025

Submit a pull request

Linear: FLU-231

Description of the pull request

This commit introduces the concept of pending messages for channels.
Pending messages are messages that are awaiting moderation before being
visible to all users in a channel.

The following changes were made:

  • Added markMessagesPending field to ChannelConfig to enable/disable
    pending messages for a channel.
  • Added pendingMessages field to ChannelState to store the list of
    pending messages for a channel.
  • Updated MessageListView to mark messages as read only if pending messages
    are disabled for the channel.
  • Updated Channel to include pendingMessages in the channel state.

Summary by CodeRabbit

  • New Features

    • Added support for pending messages in channels to better handle messages awaiting delivery.
    • Added a channel-level setting to enable or disable pending messages.
  • Bug Fixes

    • Improved automatic "mark as read" behavior when you reach the bottom of a conversation, respecting channel settings and unread state.

This commit introduces the concept of pending messages for channels.
Pending messages are messages that are awaiting moderation before being
visible to all users in a channel.

The following changes were made:
- Added `markMessagesPending` field to `ChannelConfig` to enable/disable
pending messages for a channel.
- Added `pendingMessages` field to `ChannelState` to store the list of
pending messages for a channel.
- Updated `MessageListView` to mark messages as read only if pending messages
are disabled for the channel.
- Updated `Channel` to include `pendingMessages` in the channel state.
The `pending_messages` field in the channel state was not being parsed correctly. This change ensures that the field is parsed as a list of maps, and that the `message` field is extracted from each map.
Copy link
Contributor

coderabbitai bot commented Sep 26, 2025

Walkthrough

Propagates pending messages support: adds ChannelConfig.markMessagesPending, ChannelState.pendingMessages with JSON handling, includes pendingMessages in ChannelClientState.updateChannelState, and encapsulates read-marking checks in MessageListView via _maybeMarkMessagesAsRead.

Changes

Cohort / File(s) Summary of Changes
Channel state model: pending messages
packages/stream_chat/lib/src/core/models/channel_state.dart, packages/stream_chat/lib/src/core/models/channel_state.g.dart
Adds ChannelState.pendingMessages (List<Message>?) with @JsonKey(readValue: _pendingMessagesReadValue), constructor/copyWith updates, and JSON (de)serialization for pending_messages.
Channel config: pending toggle
packages/stream_chat/lib/src/core/models/channel_config.dart, packages/stream_chat/lib/src/core/models/channel_config.g.dart
Adds ChannelConfig.markMessagesPending (bool, default false) and wires JSON key mark_messages_pending to/from the model.
Client state propagation
packages/stream_chat/lib/src/client/channel.dart
ChannelClientState.updateChannelState now propagates pendingMessages: updatedState.pendingMessages when rebuilding ChannelState.
Flutter UI read-marking guard
packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart
Replaces inline read-marking checks with _maybeMarkMessagesAsRead() helper that guards on channel presence, unread count, readEvents allowance, markMessagesPending flag, and markReadWhenAtTheBottom before calling _markMessagesAsRead().

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant UI as MessageListView
  participant Channel as Channel
  participant Config as ChannelConfig
  participant Server as API

  User->>UI: Scrolls to bottom / message visible
  UI->>Channel: Query unread count & read events allowed
  UI->>Config: Check markMessagesPending flag
  alt Guards pass (unread>0, readEvents allowed, pending enabled, at-bottom)
    UI->>Server: markRead()
    Server-->>Channel: Sends updated ChannelState (includes pending_messages)
    Channel->>Channel: updateChannelState(updatedState) — includes pendingMessages
  else Guards fail
    UI-->>User: No read action taken
  end
Loading
sequenceDiagram
  autonumber
  participant ChannelClient as ChannelClientState
  participant Updated as UpdatedState
  participant Local as ChannelState

  ChannelClient->>Updated: Receives updated channel state from server
  ChannelClient->>Local: Rebuild ChannelState with fields:
  Note right of Local: messages, watchers, read, draft,\npinnedMessages, pushPreferences, pendingMessages
  Local-->>ChannelClient: New ChannelState applied
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • renefloor
  • Brazol

Poem

A hop, a skip, my carrot queued,
Pending notes in channels strewed.
Config winks, the state holds fast,
Reads mark true when shadows pass.
I twitch my nose—syncs done, at last. 🥕

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 title clearly describes the primary change by indicating the feature addition for pending messages on channels and follows conventional commit style, making it concise and informative.
Linked Issues Check ✅ Passed All requested objectives for FLU-231 are met: the SDK now supports enabling pending messages per channel via ChannelConfig, captures pendingMessages in ChannelState with JSON serialization, propagates this state in ChannelClientState, and updates message read logic in MessageListView according to pending-message semantics.
Out of Scope Changes Check ✅ Passed All changes directly implement the pending messages feature as specified—no unrelated functionality or files were modified outside the scope of FLU-231.
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 feat/pending-messages

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.

@xsahil03x xsahil03x changed the title feat(moderation): add pending messages for channels feat(llc): add pending messages for channels Sep 26, 2025
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

📜 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 008bece and c551eb3.

📒 Files selected for processing (6)
  • packages/stream_chat/lib/src/client/channel.dart (1 hunks)
  • packages/stream_chat/lib/src/core/models/channel_config.dart (2 hunks)
  • packages/stream_chat/lib/src/core/models/channel_config.g.dart (2 hunks)
  • packages/stream_chat/lib/src/core/models/channel_state.dart (4 hunks)
  • packages/stream_chat/lib/src/core/models/channel_state.g.dart (2 hunks)
  • packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart (1 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/lib/src/client/channel.dart
  • packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.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_flutter/lib/src/message_list_view/message_list_view.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). (9)
  • GitHub Check: build (android)
  • GitHub Check: build (ios)
  • GitHub Check: test
  • GitHub Check: analyze
  • GitHub Check: stream_chat_localizations
  • GitHub Check: stream_chat_flutter_core
  • GitHub Check: stream_chat_flutter
  • GitHub Check: stream_chat_persistence
  • GitHub Check: stream_chat
🔇 Additional comments (11)
packages/stream_chat/lib/src/core/models/channel_state.g.dart (2)

35-39: LGTM: Clean JSON serialization for pending messages

The implementation correctly handles the deserialization of pending messages from the pending_messages JSON field and maps it to the Message objects. The generated code properly uses the custom _pendingMessagesReadValue helper function for reading the data.


58-59: LGTM: Proper JSON serialization for pending messages

The toJson implementation correctly serializes the pendingMessages list to the pending_messages field, maintaining consistency with the Stream Chat API structure.

packages/stream_chat/lib/src/core/models/channel_config.g.dart (2)

37-37: LGTM: Correct JSON deserialization with appropriate default

The field is properly deserialized from mark_messages_pending with a sensible default value of false, which aligns with the Stream Chat API documentation where this feature needs to be explicitly enabled.


61-61: LGTM: Consistent JSON serialization

The field is correctly serialized back to the mark_messages_pending JSON key, maintaining API compatibility.

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

3343-3343: LGTM: Proper state synchronization for pending messages

The change correctly propagates pendingMessages from the updated channel state into the local channel state during updateChannelState. This ensures that pending messages are properly synchronized between server and local state.

packages/stream_chat/lib/src/core/models/channel_config.dart (2)

29-29: LGTM: Consistent constructor parameter

The new parameter is correctly added to the constructor with an appropriate default value of false, ensuring backward compatibility.


95-96: LGTM: Well-documented new field

The documentation clearly explains the purpose of the field and its relationship to pending messages functionality. The field declaration is properly typed as a final boolean.

packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart (1)

1481-1482: LGTM: Cleaner async handling

Good refactoring to extract the message read logic into a separate method and handle it asynchronously with proper error suppression using .ignore().

packages/stream_chat/lib/src/core/models/channel_state.dart (3)

64-78: LGTM: Robust JSON parsing for pending messages structure

The _pendingMessagesReadValue helper correctly handles the nested JSON structure where pending messages are wrapped in objects containing a message field. The implementation properly validates the data types and handles edge cases like null values and empty arrays.


80-84: LGTM: Well-documented field with proper JSON annotation

The field is well-documented with clear explanation of its purpose and visibility constraints. The @JsonKey annotation correctly uses the custom read function to handle the nested JSON structure from the Stream Chat API.


107-107: LGTM: Proper copyWith implementation

The pendingMessages parameter is correctly added to the copyWith method and properly propagated to the new instance.

Also applies to: 120-120

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (2)
packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart (2)

873-878: Manual dismiss bypasses pending-messages guard

UnreadIndicatorButton uses _markMessagesAsRead directly, which ignores the new pending-messages guard. Route it through the guard (manual path) so pending channels aren’t marked read accidentally.

Apply:

-              onDismissTap: _markMessagesAsRead,
+              onDismissTap: () => _maybeMarkMessagesAsRead(manual: true),

957-971: Debounced callbacks capture a stale channel instance

debouncedMarkRead/debouncedMarkThreadRead are created from method tear-offs bound to the channel at creation time. If the StreamChannel changes (handled above), these closures will still call markRead/markThreadRead on the old channel. Make them reference streamChannel at call time to avoid stale bindings.

Apply:

-  late final debouncedMarkRead = switch (streamChannel) {
-    final streamChannel? => debounce(
-        streamChannel.channel.markRead,
-        const Duration(seconds: 1),
-      ),
-    _ => null,
-  };
+  late final debouncedMarkRead = debounce(
+    () {
+      final ch = streamChannel?.channel;
+      if (ch == null) return;
+      ch.markRead();
+    },
+    const Duration(seconds: 1),
+  );
 
-  late final debouncedMarkThreadRead = switch (streamChannel) {
-    final streamChannel? => debounce(
-        streamChannel.channel.markThreadRead,
-        const Duration(seconds: 1),
-      ),
-    _ => null,
-  };
+  late final debouncedMarkThreadRead = debounce(
+    (List<String> parentIds) {
+      final ch = streamChannel?.channel;
+      if (ch == null) return;
+      ch.markThreadRead(parentIds);
+    },
+    const Duration(seconds: 1),
+  );
🧹 Nitpick comments (1)
packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart (1)

1486-1517: Don’t block thread read events; add a “manual” path and tighten guards

Two gaps:

  • In thread view, gating on channel unreadCount can skip needed markThreadRead calls.
  • Reuse this guard for manual actions (e.g., unread-indicator dismiss) to consistently honor pending-messages configs.

Proposed update:

  • Add parameter {bool manual = false}.
  • Skip hasUnread and bottom checks when manual.
  • Skip hasUnread when in a thread view.

Apply:

-  Future<void> _maybeMarkMessagesAsRead() async {
+  Future<void> _maybeMarkMessagesAsRead({bool manual = false}) async {
     final channel = streamChannel?.channel;
     if (channel == null) return;

-    final hasUnread = (channel.state?.unreadCount ?? 0) > 0;
-    if (!hasUnread) return;
+    // For manual calls or thread views, don't require channel-level unread count.
+    if (!manual && !_isThreadConversation) {
+      final hasUnread = (channel.state?.unreadCount ?? 0) > 0;
+      if (!hasUnread) return;
+    }

     final allowMarkRead = channel.config?.readEvents == true;
     if (!allowMarkRead) return;

     // When markMessagesPending is enabled, messages are held for moderation
     // and should not be immediately marked as read.
     final markPendingEnabled = channel.config?.markMessagesPending == true;
     if (markPendingEnabled) return;

-    final canMarkReadAtBottom = widget.markReadWhenAtTheBottom;
-    if (!canMarkReadAtBottom) return;
+    // Only auto-mark at bottom when not invoked manually.
+    if (!manual) {
+      final canMarkReadAtBottom = widget.markReadWhenAtTheBottom;
+      if (!canMarkReadAtBottom) return;
+    }

     // Mark messages as read if it's allowed.
     return _markMessagesAsRead();
   }
📜 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 c551eb3 and 2e327d8.

📒 Files selected for processing (1)
  • packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart (1 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 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_flutter/lib/src/message_list_view/message_list_view.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 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_flutter/lib/src/message_list_view/message_list_view.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). (8)
  • GitHub Check: stream_chat_localizations
  • GitHub Check: stream_chat_persistence
  • GitHub Check: stream_chat_flutter_core
  • GitHub Check: stream_chat_flutter
  • GitHub Check: build (android)
  • GitHub Check: test
  • GitHub Check: build (ios)
  • GitHub Check: analyze
🔇 Additional comments (1)
packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart (1)

1481-1483: Good call site: centralized mark‑read trigger

Calling the new guard here keeps the scroll handler lean. Looks good.

@xsahil03x xsahil03x marked this pull request as draft September 27, 2025 22:52
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.

1 participant