Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/stream_chat/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## Upcoming

🐞 Fixed

- Fixed thread messages increasing the unread count in the main channel.

## 9.17.0

🐞 Fixed
Expand Down
14 changes: 13 additions & 1 deletion packages/stream_chat/lib/src/client/channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3242,7 +3242,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;
}

Expand All @@ -3264,6 +3264,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;
}
Expand Down
7 changes: 7 additions & 0 deletions packages/stream_chat_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -1594,6 +1594,10 @@ class StreamMessageInputState extends State<StreamMessageInput>

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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -467,8 +467,8 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {

@override
void dispose() {
debouncedMarkRead?.cancel();
debouncedMarkThreadRead?.cancel();
debouncedMarkRead.cancel();
debouncedMarkThreadRead.cancel();
_messageNewListener?.cancel();
_userReadListener?.cancel();
_itemPositionListener.itemPositions
Expand Down Expand Up @@ -954,29 +954,23 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
}
}

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<void> _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();
}
}

Expand Down Expand Up @@ -1473,24 +1467,43 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
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<void> _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) {
Expand Down