diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 791927e40..b3a90bed8 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -2,6 +2,7 @@ 🐞 Fixed +- Fixed thread messages increasing the unread count in the main channel. - Fixed `ChannelState.memberCount`, `ChannelState.config` and `ChannelState.extraData` getting reset on first load. diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 22e4a8c04..b2c2efee9 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -3229,7 +3229,7 @@ class ChannelClientState { if (message.isEphemeral) return false; // Don't count thread replies which are not shown in the channel as unread. - if (message.parentId != null && message.showInChannel == false) { + if (message.parentId != null && message.showInChannel != true) { return false; } @@ -3251,6 +3251,18 @@ class ChannelClientState { final isMuted = currentUser.mutes.any((it) => it.user.id == messageUser.id); if (isMuted) return false; + final lastRead = currentUserRead?.lastRead; + // Don't count messages created before the last read time as unread. + if (lastRead case final read? when message.createdAt.isBefore(read)) { + return false; + } + + final lastReadMessageId = currentUserRead?.lastReadMessageId; + // Don't count if the last read message id is the same as the message id. + if (lastReadMessageId case final id? when message.id == id) { + return false; + } + // If we've passed all checks, count the message as unread. return true; } diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index a83303ad5..341f5ecbe 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,10 @@ +## Upcoming + +🐞 Fixed + +- Fixed `StreamMessageListView` not marking thread messages as read when scrolled to the bottom of the list. +- Fixed `StreamMessageInput` not validating draft messages before creating/updating them. + ## 9.17.0 ✅ Added diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart index 1355150f1..e79a7f18c 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart @@ -463,11 +463,11 @@ class StreamMessageInput extends StatefulWidget { } static bool _defaultValidator(Message message) { - // The message is valid if it has text or attachments. - if (message.attachments.isNotEmpty) return true; - if (message.text?.trim() case final text? when text.isNotEmpty) return true; + final hasText = message.text?.trim().isNotEmpty == true; + final hasAttachments = message.attachments.isNotEmpty; + final hasPoll = message.pollId != null; - return false; + return hasText || hasAttachments || hasPoll; } static bool _defaultSendMessageKeyPredicate( @@ -1594,6 +1594,10 @@ class StreamMessageInputState extends State final draftMessage = message.toDraftMessage(); + // If the draft message is not valid, we don't need to update it. + final isDraftValid = widget.validator.call(draftMessage.toMessage()); + if (!isDraftValid) return; + // If the draft message didn't change, we don't need to update it. if (draft?.message == draftMessage) return; 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..b273a1acc 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 @@ -417,6 +417,9 @@ class _StreamMessageListViewState extends State { if (newStreamChannel != streamChannel) { streamChannel = newStreamChannel; + debouncedMarkRead.cancel(); + debouncedMarkThreadRead.cancel(); + _messageNewListener?.cancel(); _userReadListener?.cancel(); @@ -467,8 +470,8 @@ class _StreamMessageListViewState extends State { @override void dispose() { - debouncedMarkRead?.cancel(); - debouncedMarkThreadRead?.cancel(); + debouncedMarkRead.cancel(); + debouncedMarkThreadRead.cancel(); _messageNewListener?.cancel(); _userReadListener?.cancel(); _itemPositionListener.itemPositions @@ -954,29 +957,23 @@ class _StreamMessageListViewState extends State { } } - late final debouncedMarkRead = switch (streamChannel) { - final streamChannel? => debounce( - streamChannel.channel.markRead, - const Duration(seconds: 1), - ), - _ => null, - }; + late final debouncedMarkRead = debounce( + ([String? id]) => streamChannel?.channel.markRead(messageId: id), + const Duration(seconds: 1), + ); - late final debouncedMarkThreadRead = switch (streamChannel) { - final streamChannel? => debounce( - streamChannel.channel.markThreadRead, - const Duration(seconds: 1), - ), - _ => null, - }; + late final debouncedMarkThreadRead = debounce( + (String parentId) => streamChannel?.channel.markThreadRead(parentId), + const Duration(seconds: 1), + ); Future _markMessagesAsRead() async { - // Mark regular messages as read. - debouncedMarkRead?.call(); - - // Mark thread messages as read. if (widget.parentMessage case final parent?) { - debouncedMarkThreadRead?.call([parent.id]); + // If we are in a thread, mark the thread as read. + debouncedMarkThreadRead.call([parent.id]); + } else { + // Otherwise, mark the channel as read. + debouncedMarkRead.call(); } } @@ -1473,24 +1470,43 @@ class _StreamMessageListViewState extends State { null => true, // Allows setting the initial value. }; - // If the channel is upToDate and the last fully visible message has - // been changed, we need to update the value and mark the messages as read. - if (_upToDate && lastFullyVisibleMessageChanged) { + // If the last fully visible message has been changed, we need to update the + // value and maybe mark messages as read if needed. + if (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. + if (widget.markReadWhenAtTheBottom) { + _maybeMarkMessagesAsRead().ignore(); } } } + // Marks messages as read if the conditions are met. + // + // The conditions are: + // 1. The channel is up to date or we are in a thread conversation. + // 2. There are unread messages or we are in a thread conversation. + // + // 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; + + final isInThread = widget.parentMessage != null; + + final isUpToDate = channel.state?.isUpToDate ?? false; + if (!isInThread && !isUpToDate) return; + + final hasUnread = (channel.state?.unreadCount ?? 0) > 0; + if (!isInThread && !hasUnread) return; + + // Mark messages as read if it's allowed. + return _markMessagesAsRead(); + } + void _getOnThreadTap() { if (widget.onThreadTap != null) { _onThreadTap = (Message message) {