From b69693870c984c2ba919d49fc39b01e9a133d00c Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 26 Sep 2025 11:50:52 +0200 Subject: [PATCH 1/3] feat(moderation): add pending messages for channels 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. --- .../stream_chat/lib/src/client/channel.dart | 1 + .../lib/src/core/models/channel_config.dart | 4 +++ .../lib/src/core/models/channel_config.g.dart | 2 ++ .../lib/src/core/models/channel_state.dart | 26 +++++++++++++++ .../lib/src/core/models/channel_state.g.dart | 7 ++++ .../message_list_view/message_list_view.dart | 32 +++++++++++++------ 6 files changed, 62 insertions(+), 10 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 54f163c0f..5caf21415 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -3340,6 +3340,7 @@ class ChannelClientState { read: newReads, draft: updatedState.draft, pinnedMessages: updatedState.pinnedMessages, + pendingMessages: updatedState.pendingMessages, pushPreferences: updatedState.pushPreferences, ); } diff --git a/packages/stream_chat/lib/src/core/models/channel_config.dart b/packages/stream_chat/lib/src/core/models/channel_config.dart index 2bcf0a1ed..55ad2b2fb 100644 --- a/packages/stream_chat/lib/src/core/models/channel_config.dart +++ b/packages/stream_chat/lib/src/core/models/channel_config.dart @@ -26,6 +26,7 @@ class ChannelConfig { this.urlEnrichment = false, this.skipLastMsgUpdateForSystemMsgs = false, this.userMessageReminders = false, + this.markMessagesPending = false, }) : createdAt = createdAt ?? DateTime.now(), updatedAt = updatedAt ?? DateTime.now(); @@ -91,6 +92,9 @@ class ChannelConfig { /// True if the user can set reminders for messages in this channel. final bool userMessageReminders; + /// Whether pending messages are enabled for this channel. + final bool markMessagesPending; + /// Serialize to json Map toJson() => _$ChannelConfigToJson(this); } diff --git a/packages/stream_chat/lib/src/core/models/channel_config.g.dart b/packages/stream_chat/lib/src/core/models/channel_config.g.dart index 8cb758b87..ae1221b99 100644 --- a/packages/stream_chat/lib/src/core/models/channel_config.g.dart +++ b/packages/stream_chat/lib/src/core/models/channel_config.g.dart @@ -34,6 +34,7 @@ ChannelConfig _$ChannelConfigFromJson(Map json) => skipLastMsgUpdateForSystemMsgs: json['skip_last_msg_update_for_system_msgs'] as bool? ?? false, userMessageReminders: json['user_message_reminders'] as bool? ?? false, + markMessagesPending: json['mark_messages_pending'] as bool? ?? false, ); Map _$ChannelConfigToJson(ChannelConfig instance) => @@ -57,4 +58,5 @@ Map _$ChannelConfigToJson(ChannelConfig instance) => 'skip_last_msg_update_for_system_msgs': instance.skipLastMsgUpdateForSystemMsgs, 'user_message_reminders': instance.userMessageReminders, + 'mark_messages_pending': instance.markMessagesPending, }; diff --git a/packages/stream_chat/lib/src/core/models/channel_state.dart b/packages/stream_chat/lib/src/core/models/channel_state.dart index 601ef679a..338cc2ff0 100644 --- a/packages/stream_chat/lib/src/core/models/channel_state.dart +++ b/packages/stream_chat/lib/src/core/models/channel_state.dart @@ -30,6 +30,7 @@ class ChannelState implements ComparableFieldProvider { this.read, this.membership, this.draft, + this.pendingMessages, this.pushPreferences, }); @@ -60,6 +61,29 @@ class ChannelState implements ComparableFieldProvider { /// The draft message for this channel if it exists. final Draft? draft; + static Object? _pendingMessagesReadValue( + Map json, + String key, + ) { + final pendingMessageResponse = json[key] as List?; + if (pendingMessageResponse == null) return null; + + final value = pendingMessageResponse.map((it) { + final response = it as Map?; + if (response == null) return null; + + return response['message'] as Map?; + }).nonNulls.toList(); + + return value; + } + + /// List of messages pending for moderation on this channel. + /// + /// These messages are only visible to the author until they are approved. + @JsonKey(readValue: _pendingMessagesReadValue) + final List? pendingMessages; + /// The push preferences for this channel if it exists. final ChannelPushPreference? pushPreferences; @@ -81,6 +105,7 @@ class ChannelState implements ComparableFieldProvider { List? read, Member? membership, Object? draft = _nullConst, + List? pendingMessages, ChannelPushPreference? pushPreferences, }) => ChannelState( @@ -93,6 +118,7 @@ class ChannelState implements ComparableFieldProvider { read: read ?? this.read, membership: membership ?? this.membership, draft: draft == _nullConst ? this.draft : draft as Draft?, + pendingMessages: pendingMessages ?? this.pendingMessages, pushPreferences: pushPreferences ?? this.pushPreferences, ); diff --git a/packages/stream_chat/lib/src/core/models/channel_state.g.dart b/packages/stream_chat/lib/src/core/models/channel_state.g.dart index e154f1235..b7499ba8b 100644 --- a/packages/stream_chat/lib/src/core/models/channel_state.g.dart +++ b/packages/stream_chat/lib/src/core/models/channel_state.g.dart @@ -32,6 +32,11 @@ ChannelState _$ChannelStateFromJson(Map json) => ChannelState( draft: json['draft'] == null ? null : Draft.fromJson(json['draft'] as Map), + pendingMessages: + (ChannelState._pendingMessagesReadValue(json, 'pending_messages') + as List?) + ?.map((e) => Message.fromJson(e as Map)) + .toList(), pushPreferences: json['push_preferences'] == null ? null : ChannelPushPreference.fromJson( @@ -50,5 +55,7 @@ Map _$ChannelStateToJson(ChannelState instance) => 'read': instance.read?.map((e) => e.toJson()).toList(), 'membership': instance.membership?.toJson(), 'draft': instance.draft?.toJson(), + 'pending_messages': + instance.pendingMessages?.map((e) => e.toJson()).toList(), 'push_preferences': instance.pushPreferences?.toJson(), }; diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart index 70eae83e9..9c84b07e3 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart @@ -1478,19 +1478,31 @@ class _StreamMessageListViewState extends State { if (_upToDate && lastFullyVisibleMessageChanged) { _lastFullyVisibleMessage = newLastFullyVisibleMessage; - if (streamChannel?.channel case final channel?) { - final hasUnread = (channel.state?.unreadCount ?? 0) > 0; - final allowMarkRead = channel.config?.readEvents == true; - final canMarkReadAtBottom = widget.markReadWhenAtTheBottom; - - // Mark messages as read if it's allowed. - if (hasUnread && allowMarkRead && canMarkReadAtBottom) { - return _markMessagesAsRead().ignore(); - } - } + // Mark messages as read if needed. + _maybeMarkMessagesAsRead().ignore(); } } + Future _maybeMarkMessagesAsRead() async { + final channel = streamChannel?.channel; + if (channel == null) return; + + final hasUnread = (channel.state?.unreadCount ?? 0) > 0; + if (!hasUnread) return; + + final allowMarkRead = channel.config?.readEvents == true; + if (!allowMarkRead) return; + + final markPendingDisabled = channel.config?.markMessagesPending == false; + if (!markPendingDisabled) return; + + final canMarkReadAtBottom = widget.markReadWhenAtTheBottom; + if (!canMarkReadAtBottom) return; + + // Mark messages as read if it's allowed. + return _markMessagesAsRead(); + } + void _getOnThreadTap() { if (widget.onThreadTap != null) { _onThreadTap = (Message message) { From c551eb36aa26f19b917f0dad75a13c3425663ce4 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 26 Sep 2025 17:06:08 +0200 Subject: [PATCH 2/3] Fix: Ensure `pending_messages` are parsed correctly 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. --- .../lib/src/core/models/channel_state.dart | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/stream_chat/lib/src/core/models/channel_state.dart b/packages/stream_chat/lib/src/core/models/channel_state.dart index 338cc2ff0..e3dc81ad2 100644 --- a/packages/stream_chat/lib/src/core/models/channel_state.dart +++ b/packages/stream_chat/lib/src/core/models/channel_state.dart @@ -65,17 +65,16 @@ class ChannelState implements ComparableFieldProvider { Map json, String key, ) { - final pendingMessageResponse = json[key] as List?; - if (pendingMessageResponse == null) return null; + final pendingMessageResponse = json[key]; + if (pendingMessageResponse is! List) return null; final value = pendingMessageResponse.map((it) { - final response = it as Map?; - if (response == null) return null; + if (it is! Map) return null; + return it['message']; + }).nonNulls; - return response['message'] as Map?; - }).nonNulls.toList(); - - return value; + if (value.isEmpty) return null; + return value.toList(growable: false); } /// List of messages pending for moderation on this channel. From 2e327d8cf33a2a12983dc134f2e4c42620782312 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 26 Sep 2025 17:52:47 +0200 Subject: [PATCH 3/3] fix(ui): do not mark messages as read if `markMessagesPending` is enabled --- .../message_list_view/message_list_view.dart | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart index 9c84b07e3..5d0bef129 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart @@ -1483,6 +1483,17 @@ class _StreamMessageListViewState extends State { } } + // Marks messages as read if the conditions are met. + // + // The conditions are: + // 1. There are unread messages. + // 2. Marking messages as read is allowed in the channel config. + // 3. Marking messages as read when at the bottom is enabled. + // 4. Mark messages as pending is not enabled in the channel config. + // + // If any of the conditions are not met, the function returns early. + // Otherwise, it calls the _markMessagesAsRead function to mark the messages + // as read. Future _maybeMarkMessagesAsRead() async { final channel = streamChannel?.channel; if (channel == null) return; @@ -1493,8 +1504,10 @@ class _StreamMessageListViewState extends State { final allowMarkRead = channel.config?.readEvents == true; if (!allowMarkRead) return; - final markPendingDisabled = channel.config?.markMessagesPending == false; - if (!markPendingDisabled) 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;