Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions packages/stream_chat/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

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 @@ -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;
}

Expand All @@ -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;
}
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 @@ -417,6 +417,9 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
if (newStreamChannel != streamChannel) {
streamChannel = newStreamChannel;

debouncedMarkRead.cancel();
debouncedMarkThreadRead.cancel();

_messageNewListener?.cancel();
_userReadListener?.cancel();

Expand Down Expand Up @@ -467,8 +470,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 +957,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),
);
Comment thread
xsahil03x marked this conversation as resolved.

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();
}
}
Comment thread
xsahil03x marked this conversation as resolved.

Expand Down Expand Up @@ -1473,24 +1470,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