From 765ef24b24b96dd8258bc511a36f6b3282c9a231 Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Mon, 17 Mar 2025 19:02:00 +0530 Subject: [PATCH 1/3] fix: unread indicator label presence in message list --- package/src/components/Channel/Channel.tsx | 11 ++++++----- package/src/components/MessageList/MessageList.tsx | 6 +++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index c52f422f58..9f10748dfb 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -694,7 +694,8 @@ const ChannelWithContext = < const [deleted, setDeleted] = useState(false); const [editing, setEditing] = useState | undefined>(undefined); const [error, setError] = useState(false); - const [lastRead, setLastRead] = useState['lastRead']>(); + const lastRead = useRef(new Date()); + const [quotedMessage, setQuotedMessage] = useState | undefined>( undefined, ); @@ -824,7 +825,6 @@ const ChannelWithContext = < useEffect(() => { let listener: ReturnType; const initChannel = async () => { - setLastRead(new Date()); const unreadCount = channel.countUnread(); if (!channel || !shouldSyncChannel || channel.offlineMode) { return; @@ -950,6 +950,7 @@ const ChannelWithContext = < return; } + lastRead.current = new Date(); if (doMarkReadRequest) { doMarkReadRequest(channel, updateChannelUnreadState ? setChannelUnreadState : undefined); } else { @@ -957,7 +958,7 @@ const ChannelWithContext = < const response = await channel.markRead(); if (updateChannelUnreadState && response && lastRead) { setChannelUnreadState({ - last_read: lastRead, + last_read: lastRead.current, last_read_message_id: response?.event.last_read_message_id, unread_messages: 0, }); @@ -1725,7 +1726,7 @@ const ChannelWithContext = < hideStickyDateHeader, highlightedMessageId, isChannelActive: shouldSyncChannel, - lastRead, + lastRead: lastRead.current, loadChannelAroundMessage, loadChannelAtFirstUnreadMessage, loading: channelMessagesState.loading, @@ -1738,7 +1739,7 @@ const ChannelWithContext = < reloadChannel, scrollToFirstUnreadThreshold, setChannelUnreadState, - setLastRead, + setLastRead: () => {}, setTargetedMessage, StickyHeader, targetedMessage, diff --git a/package/src/components/MessageList/MessageList.tsx b/package/src/components/MessageList/MessageList.tsx index 43e40ed2df..93d344543b 100644 --- a/package/src/components/MessageList/MessageList.tsx +++ b/package/src/components/MessageList/MessageList.tsx @@ -430,7 +430,6 @@ const MessageListWithContext = < setIsUnreadNotificationOpen(false); return; } - if (unreadIndicatorDate && lastItemDate > unreadIndicatorDate) { setIsUnreadNotificationOpen(true); } else { @@ -485,12 +484,13 @@ const MessageListWithContext = < * Effect to mark the channel as read when the user scrolls to the bottom of the message list. */ useEffect(() => { - const listener: ReturnType = channel.on('message.new', (event) => { + const listener: ReturnType = channel.on('message.new', async (event) => { const newMessageToCurrentChannel = event.cid === channel.cid; const mainChannelUpdated = !event.message?.parent_id || event.message?.show_in_channel; if (newMessageToCurrentChannel && mainChannelUpdated && !scrollToBottomButtonVisible) { - markRead(); + console.log('markRead'); + await markRead(); } }); From f5a6b34e752f703d5835a58849ef44f8ce3bac79 Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Wed, 19 Mar 2025 11:02:51 +0530 Subject: [PATCH 2/3] fix: unread indicator label presence in message list --- package/src/components/Channel/Channel.tsx | 11 +- .../components/MessageList/MessageList.tsx | 110 ++++++++++++++++-- 2 files changed, 106 insertions(+), 15 deletions(-) diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index 9f10748dfb..78f399cca7 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -694,7 +694,7 @@ const ChannelWithContext = < const [deleted, setDeleted] = useState(false); const [editing, setEditing] = useState | undefined>(undefined); const [error, setError] = useState(false); - const lastRead = useRef(new Date()); + const [lastRead, setLastRead] = useState(); const [quotedMessage, setQuotedMessage] = useState | undefined>( undefined, @@ -825,6 +825,7 @@ const ChannelWithContext = < useEffect(() => { let listener: ReturnType; const initChannel = async () => { + setLastRead(new Date()); const unreadCount = channel.countUnread(); if (!channel || !shouldSyncChannel || channel.offlineMode) { return; @@ -950,7 +951,6 @@ const ChannelWithContext = < return; } - lastRead.current = new Date(); if (doMarkReadRequest) { doMarkReadRequest(channel, updateChannelUnreadState ? setChannelUnreadState : undefined); } else { @@ -958,10 +958,11 @@ const ChannelWithContext = < const response = await channel.markRead(); if (updateChannelUnreadState && response && lastRead) { setChannelUnreadState({ - last_read: lastRead.current, + last_read: lastRead, last_read_message_id: response?.event.last_read_message_id, unread_messages: 0, }); + setLastRead(new Date()); } } catch (err) { console.log('Error marking channel as read:', err); @@ -1726,7 +1727,7 @@ const ChannelWithContext = < hideStickyDateHeader, highlightedMessageId, isChannelActive: shouldSyncChannel, - lastRead: lastRead.current, + lastRead, loadChannelAroundMessage, loadChannelAtFirstUnreadMessage, loading: channelMessagesState.loading, @@ -1739,7 +1740,7 @@ const ChannelWithContext = < reloadChannel, scrollToFirstUnreadThreshold, setChannelUnreadState, - setLastRead: () => {}, + setLastRead, setTargetedMessage, StickyHeader, targetedMessage, diff --git a/package/src/components/MessageList/MessageList.tsx b/package/src/components/MessageList/MessageList.tsx index 93d344543b..bfd026295a 100644 --- a/package/src/components/MessageList/MessageList.tsx +++ b/package/src/components/MessageList/MessageList.tsx @@ -10,7 +10,7 @@ import { ViewToken, } from 'react-native'; -import type { FormatMessageResponse } from 'stream-chat'; +import type { Channel, Event, FormatMessageResponse, MessageResponse } from 'stream-chat'; import { isMessageWithStylesReadByAndDateSeparator, @@ -108,6 +108,36 @@ const flatListViewabilityConfig: ViewabilityConfig = { viewAreaCoveragePercentThreshold: 1, }; +const hasReadLastMessage = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +>( + channel: Channel, + userId: string, +) => { + const latestMessageIdInChannel = channel.state.latestMessages.slice(-1)[0]?.id; + const lastReadMessageIdServer = channel.state.read[userId]?.last_read_message_id; + return latestMessageIdInChannel === lastReadMessageIdServer; +}; + +const getPreviousLastMessage = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +>( + messages: MessageType[], + newMessage?: MessageResponse, +) => { + if (!newMessage) return; + let previousLastMessage; + for (let i = messages.length - 1; i >= 0; i--) { + const msg = messages[i]; + if (!msg?.id) break; + if (msg.id !== newMessage.id) { + previousLastMessage = msg; + break; + } + } + return previousLastMessage; +}; + type MessageListPropsWithContext< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = Pick & @@ -126,6 +156,7 @@ type MessageListPropsWithContext< | 'NetworkDownIndicator' | 'reloadChannel' | 'scrollToFirstUnreadThreshold' + | 'setChannelUnreadState' | 'setTargetedMessage' | 'StickyHeader' | 'targetedMessage' @@ -271,6 +302,7 @@ const MessageListWithContext = < reloadChannel, ScrollToBottomButton, selectedPicker, + setChannelUnreadState, setFlatListRef, setMessages, setSelectedPicker, @@ -418,14 +450,28 @@ const MessageListWithContext = < const lastItem = viewableItems[viewableItems.length - 1]; if (lastItem) { - const lastItemCreatedAt = lastItem.item.created_at; + const lastItemMessage = lastItem.item; + const lastItemCreatedAt = lastItemMessage.created_at; const unreadIndicatorDate = channelUnreadState?.last_read.getTime(); const lastItemDate = lastItemCreatedAt.getTime(); if ( !channel.state.messagePagination.hasPrev && - processedMessageList[processedMessageList.length - 1].id === lastItem.item.id + processedMessageList[processedMessageList.length - 1].id === lastItemMessage.id + ) { + setIsUnreadNotificationOpen(false); + return; + } + /** + * This is a special case where there is a single long message by the sender. + * When a message is sent, we mark it as read before it actually has a `created_at` timestamp. + * This is a workaround to prevent the unread indicator from showing when the message is sent. + */ + if ( + viewableItems.length === 1 && + channel.countUnread() === 0 && + lastItemMessage.user.id === client.userID ) { setIsUnreadNotificationOpen(false); return; @@ -484,20 +530,55 @@ const MessageListWithContext = < * Effect to mark the channel as read when the user scrolls to the bottom of the message list. */ useEffect(() => { - const listener: ReturnType = channel.on('message.new', async (event) => { - const newMessageToCurrentChannel = event.cid === channel.cid; - const mainChannelUpdated = !event.message?.parent_id || event.message?.show_in_channel; + const shouldMarkRead = () => { + return ( + !channelUnreadState?.first_unread_message_id && + !threadList && + !scrollToBottomButtonVisible && + client.user?.id && + !hasReadLastMessage(channel, client.user?.id) + ); + }; - if (newMessageToCurrentChannel && mainChannelUpdated && !scrollToBottomButtonVisible) { - console.log('markRead'); + const handleEvent = async (event: Event) => { + const mainChannelUpdated = !event.message?.parent_id || event.message?.show_in_channel; + // When the scrollToBottomButtonVisible is true, we need to manually update the channelUnreadState. + if (scrollToBottomButtonVisible || channelUnreadState?.first_unread_message_id) { + setChannelUnreadState((prev) => { + const previousUnreadCount = prev?.unread_messages ?? 0; + const previousLastMessage = getPreviousLastMessage( + channel.state.messages, + event.message, + ); + return { + ...(prev || {}), + last_read: + prev?.last_read ?? + (previousUnreadCount === 0 && previousLastMessage?.created_at + ? new Date(previousLastMessage.created_at) + : new Date(0)), // not having information about the last read message means the whole channel is unread, + unread_messages: previousUnreadCount + 1, + }; + }); + } else if (mainChannelUpdated && shouldMarkRead()) { await markRead(); } - }); + }; + + const listener: ReturnType = channel.on('message.new', handleEvent); return () => { listener?.unsubscribe(); }; - }, [channel, markRead, scrollToBottomButtonVisible]); + }, [ + channel, + channelUnreadState?.first_unread_message_id, + client.user?.id, + markRead, + scrollToBottomButtonVisible, + setChannelUnreadState, + threadList, + ]); useEffect(() => { const lastReceivedMessage = getLastReceivedMessage(processedMessageList); @@ -901,6 +982,13 @@ const MessageListWithContext = < } setScrollToBottomButtonVisible(false); + /** + * When we are not in the bottom of the list, and we receive new messages, we need to mark the channel as read. + We would still need to show the unread label, where the first unread message appeared so we don't update the channelUnreadState. + */ + await markRead({ + updateChannelUnreadState: false, + }); }; const scrollToIndexFailedRetryCountRef = useRef(0); @@ -1212,6 +1300,7 @@ export const MessageList = < NetworkDownIndicator, reloadChannel, scrollToFirstUnreadThreshold, + setChannelUnreadState, setTargetedMessage, StickyHeader, targetedMessage, @@ -1277,6 +1366,7 @@ export const MessageList = < ScrollToBottomButton, scrollToFirstUnreadThreshold, selectedPicker, + setChannelUnreadState, setMessages, setSelectedPicker, setTargetedMessage, From cb5640bb2d05f84fc26f5a56cf9f93c646d2b969 Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Thu, 20 Mar 2025 16:27:59 +0530 Subject: [PATCH 3/3] fix: threadList checjk --- package/src/components/MessageList/MessageList.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package/src/components/MessageList/MessageList.tsx b/package/src/components/MessageList/MessageList.tsx index bfd026295a..8edcc648eb 100644 --- a/package/src/components/MessageList/MessageList.tsx +++ b/package/src/components/MessageList/MessageList.tsx @@ -533,7 +533,6 @@ const MessageListWithContext = < const shouldMarkRead = () => { return ( !channelUnreadState?.first_unread_message_id && - !threadList && !scrollToBottomButtonVisible && client.user?.id && !hasReadLastMessage(channel, client.user?.id) @@ -542,6 +541,7 @@ const MessageListWithContext = < const handleEvent = async (event: Event) => { const mainChannelUpdated = !event.message?.parent_id || event.message?.show_in_channel; + console.log(mainChannelUpdated, shouldMarkRead()); // When the scrollToBottomButtonVisible is true, we need to manually update the channelUnreadState. if (scrollToBottomButtonVisible || channelUnreadState?.first_unread_message_id) { setChannelUnreadState((prev) => { @@ -561,6 +561,7 @@ const MessageListWithContext = < }; }); } else if (mainChannelUpdated && shouldMarkRead()) { + console.log('marking read'); await markRead(); } }; @@ -618,6 +619,7 @@ const MessageListWithContext = < setTimeout(() => { channelResyncScrollSet.current = true; if (channel.countUnread() > 0) { + console.log('marking read'); markRead(); } }, 500);