@@ -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,19 +450,32 @@ 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 ;
432478 }
433-
434479 if ( unreadIndicatorDate && lastItemDate > unreadIndicatorDate ) {
435480 setIsUnreadNotificationOpen ( true ) ;
436481 } else {
@@ -485,19 +530,56 @@ const MessageListWithContext = <
485530 * Effect to mark the channel as read when the user scrolls to the bottom of the message list.
486531 */
487532 useEffect ( ( ) => {
488- const listener : ReturnType < typeof channel . on > = channel . on ( 'message.new' , ( event ) => {
489- const newMessageToCurrentChannel = event . cid === channel . cid ;
490- const mainChannelUpdated = ! event . message ?. parent_id || event . message ?. show_in_channel ;
533+ const shouldMarkRead = ( ) => {
534+ return (
535+ ! channelUnreadState ?. first_unread_message_id &&
536+ ! scrollToBottomButtonVisible &&
537+ client . user ?. id &&
538+ ! hasReadLastMessage ( channel , client . user ?. id )
539+ ) ;
540+ } ;
491541
492- if ( newMessageToCurrentChannel && mainChannelUpdated && ! scrollToBottomButtonVisible ) {
493- markRead ( ) ;
542+ const handleEvent = async ( event : Event < StreamChatGenerics > ) => {
543+ const mainChannelUpdated = ! event . message ?. parent_id || event . message ?. show_in_channel ;
544+ console . log ( mainChannelUpdated , shouldMarkRead ( ) ) ;
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 ( ) ) {
564+ console . log ( 'marking read' ) ;
565+ await markRead ( ) ;
494566 }
495- } ) ;
567+ } ;
568+
569+ const listener : ReturnType < typeof channel . on > = channel . on ( 'message.new' , handleEvent ) ;
496570
497571 return ( ) => {
498572 listener ?. unsubscribe ( ) ;
499573 } ;
500- } , [ channel , markRead , scrollToBottomButtonVisible ] ) ;
574+ } , [
575+ channel ,
576+ channelUnreadState ?. first_unread_message_id ,
577+ client . user ?. id ,
578+ markRead ,
579+ scrollToBottomButtonVisible ,
580+ setChannelUnreadState ,
581+ threadList ,
582+ ] ) ;
501583
502584 useEffect ( ( ) => {
503585 const lastReceivedMessage = getLastReceivedMessage ( processedMessageList ) ;
@@ -537,6 +619,7 @@ const MessageListWithContext = <
537619 setTimeout ( ( ) => {
538620 channelResyncScrollSet . current = true ;
539621 if ( channel . countUnread ( ) > 0 ) {
622+ console . log ( 'marking read' ) ;
540623 markRead ( ) ;
541624 }
542625 } , 500 ) ;
@@ -901,6 +984,13 @@ const MessageListWithContext = <
901984 }
902985
903986 setScrollToBottomButtonVisible ( false ) ;
987+ /**
988+ * When we are not in the bottom of the list, and we receive new messages, we need to mark the channel as read.
989+ We would still need to show the unread label, where the first unread message appeared so we don't update the channelUnreadState.
990+ */
991+ await markRead ( {
992+ updateChannelUnreadState : false ,
993+ } ) ;
904994 } ;
905995
906996 const scrollToIndexFailedRetryCountRef = useRef < number > ( 0 ) ;
@@ -1212,6 +1302,7 @@ export const MessageList = <
12121302 NetworkDownIndicator,
12131303 reloadChannel,
12141304 scrollToFirstUnreadThreshold,
1305+ setChannelUnreadState,
12151306 setTargetedMessage,
12161307 StickyHeader,
12171308 targetedMessage,
@@ -1277,6 +1368,7 @@ export const MessageList = <
12771368 ScrollToBottomButton,
12781369 scrollToFirstUnreadThreshold,
12791370 selectedPicker,
1371+ setChannelUnreadState,
12801372 setMessages,
12811373 setSelectedPicker,
12821374 setTargetedMessage,
0 commit comments