@@ -10,7 +10,7 @@ import {
1010 ViewToken ,
1111} from 'react-native' ;
1212
13- import type { FormatMessageResponse } from 'stream-chat' ;
13+ import type { Channel , Event , FormatMessageResponse , MessageResponse } from 'stream-chat' ;
1414
1515import {
1616 isMessageWithStylesReadByAndDateSeparator ,
@@ -108,6 +108,36 @@ const flatListViewabilityConfig: ViewabilityConfig = {
108108 viewAreaCoveragePercentThreshold : 1 ,
109109} ;
110110
111+ const hasReadLastMessage = <
112+ StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics ,
113+ > (
114+ channel : Channel < StreamChatGenerics > ,
115+ userId : string ,
116+ ) => {
117+ const latestMessageIdInChannel = channel . state . latestMessages . slice ( - 1 ) [ 0 ] ?. id ;
118+ const lastReadMessageIdServer = channel . state . read [ userId ] ?. last_read_message_id ;
119+ return latestMessageIdInChannel === lastReadMessageIdServer ;
120+ } ;
121+
122+ const getPreviousLastMessage = <
123+ StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics ,
124+ > (
125+ messages : MessageType < StreamChatGenerics > [ ] ,
126+ newMessage ?: MessageResponse < StreamChatGenerics > ,
127+ ) => {
128+ if ( ! newMessage ) return ;
129+ let previousLastMessage ;
130+ for ( let i = messages . length - 1 ; i >= 0 ; i -- ) {
131+ const msg = messages [ i ] ;
132+ if ( ! msg ?. id ) break ;
133+ if ( msg . id !== newMessage . id ) {
134+ previousLastMessage = msg ;
135+ break ;
136+ }
137+ }
138+ return previousLastMessage ;
139+ } ;
140+
111141type MessageListPropsWithContext <
112142 StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics ,
113143> = Pick < AttachmentPickerContextValue , 'closePicker' | 'selectedPicker' | 'setSelectedPicker' > &
@@ -126,6 +156,7 @@ type MessageListPropsWithContext<
126156 | 'NetworkDownIndicator'
127157 | 'reloadChannel'
128158 | 'scrollToFirstUnreadThreshold'
159+ | 'setChannelUnreadState'
129160 | 'setTargetedMessage'
130161 | 'StickyHeader'
131162 | 'targetedMessage'
@@ -271,6 +302,7 @@ const MessageListWithContext = <
271302 reloadChannel,
272303 ScrollToBottomButton,
273304 selectedPicker,
305+ setChannelUnreadState,
274306 setFlatListRef,
275307 setMessages,
276308 setSelectedPicker,
@@ -418,14 +450,28 @@ const MessageListWithContext = <
418450 const lastItem = viewableItems [ viewableItems . length - 1 ] ;
419451
420452 if ( lastItem ) {
421- const lastItemCreatedAt = lastItem . item . created_at ;
453+ const lastItemMessage = lastItem . item ;
454+ const lastItemCreatedAt = lastItemMessage . created_at ;
422455
423456 const unreadIndicatorDate = channelUnreadState ?. last_read . getTime ( ) ;
424457 const lastItemDate = lastItemCreatedAt . getTime ( ) ;
425458
426459 if (
427460 ! channel . state . messagePagination . hasPrev &&
428- processedMessageList [ processedMessageList . length - 1 ] . id === lastItem . item . id
461+ processedMessageList [ processedMessageList . length - 1 ] . id === lastItemMessage . id
462+ ) {
463+ setIsUnreadNotificationOpen ( false ) ;
464+ return ;
465+ }
466+ /**
467+ * This is a special case where there is a single long message by the sender.
468+ * When a message is sent, we mark it as read before it actually has a `created_at` timestamp.
469+ * This is a workaround to prevent the unread indicator from showing when the message is sent.
470+ */
471+ if (
472+ viewableItems . length === 1 &&
473+ channel . countUnread ( ) === 0 &&
474+ lastItemMessage . user . id === client . userID
429475 ) {
430476 setIsUnreadNotificationOpen ( false ) ;
431477 return ;
@@ -484,20 +530,55 @@ const MessageListWithContext = <
484530 * Effect to mark the channel as read when the user scrolls to the bottom of the message list.
485531 */
486532 useEffect ( ( ) => {
487- const listener : ReturnType < typeof channel . on > = channel . on ( 'message.new' , async ( event ) => {
488- const newMessageToCurrentChannel = event . cid === channel . cid ;
489- const mainChannelUpdated = ! event . message ?. parent_id || event . message ?. show_in_channel ;
533+ const shouldMarkRead = ( ) => {
534+ return (
535+ ! channelUnreadState ?. first_unread_message_id &&
536+ ! threadList &&
537+ ! scrollToBottomButtonVisible &&
538+ client . user ?. id &&
539+ ! hasReadLastMessage ( channel , client . user ?. id )
540+ ) ;
541+ } ;
490542
491- if ( newMessageToCurrentChannel && mainChannelUpdated && ! scrollToBottomButtonVisible ) {
492- console . log ( 'markRead' ) ;
543+ const handleEvent = async ( event : Event < StreamChatGenerics > ) => {
544+ const mainChannelUpdated = ! event . message ?. parent_id || event . message ?. show_in_channel ;
545+ // When the scrollToBottomButtonVisible is true, we need to manually update the channelUnreadState.
546+ if ( scrollToBottomButtonVisible || channelUnreadState ?. first_unread_message_id ) {
547+ setChannelUnreadState ( ( prev ) => {
548+ const previousUnreadCount = prev ?. unread_messages ?? 0 ;
549+ const previousLastMessage = getPreviousLastMessage < StreamChatGenerics > (
550+ channel . state . messages ,
551+ event . message ,
552+ ) ;
553+ return {
554+ ...( prev || { } ) ,
555+ last_read :
556+ prev ?. last_read ??
557+ ( previousUnreadCount === 0 && previousLastMessage ?. created_at
558+ ? new Date ( previousLastMessage . created_at )
559+ : new Date ( 0 ) ) , // not having information about the last read message means the whole channel is unread,
560+ unread_messages : previousUnreadCount + 1 ,
561+ } ;
562+ } ) ;
563+ } else if ( mainChannelUpdated && shouldMarkRead ( ) ) {
493564 await markRead ( ) ;
494565 }
495- } ) ;
566+ } ;
567+
568+ const listener : ReturnType < typeof channel . on > = channel . on ( 'message.new' , handleEvent ) ;
496569
497570 return ( ) => {
498571 listener ?. unsubscribe ( ) ;
499572 } ;
500- } , [ channel , markRead , scrollToBottomButtonVisible ] ) ;
573+ } , [
574+ channel ,
575+ channelUnreadState ?. first_unread_message_id ,
576+ client . user ?. id ,
577+ markRead ,
578+ scrollToBottomButtonVisible ,
579+ setChannelUnreadState ,
580+ threadList ,
581+ ] ) ;
501582
502583 useEffect ( ( ) => {
503584 const lastReceivedMessage = getLastReceivedMessage ( processedMessageList ) ;
@@ -901,6 +982,13 @@ const MessageListWithContext = <
901982 }
902983
903984 setScrollToBottomButtonVisible ( false ) ;
985+ /**
986+ * When we are not in the bottom of the list, and we receive new messages, we need to mark the channel as read.
987+ We would still need to show the unread label, where the first unread message appeared so we don't update the channelUnreadState.
988+ */
989+ await markRead ( {
990+ updateChannelUnreadState : false ,
991+ } ) ;
904992 } ;
905993
906994 const scrollToIndexFailedRetryCountRef = useRef < number > ( 0 ) ;
@@ -1212,6 +1300,7 @@ export const MessageList = <
12121300 NetworkDownIndicator,
12131301 reloadChannel,
12141302 scrollToFirstUnreadThreshold,
1303+ setChannelUnreadState,
12151304 setTargetedMessage,
12161305 StickyHeader,
12171306 targetedMessage,
@@ -1277,6 +1366,7 @@ export const MessageList = <
12771366 ScrollToBottomButton,
12781367 scrollToFirstUnreadThreshold,
12791368 selectedPicker,
1369+ setChannelUnreadState,
12801370 setMessages,
12811371 setSelectedPicker,
12821372 setTargetedMessage,
0 commit comments