@@ -10,7 +10,7 @@ import {
1010 ViewToken ,
1111} from 'react-native' ;
1212
13- import type { LocalMessage } from 'stream-chat' ;
13+ import type { Channel , Event , LocalMessage , MessageResponse } from 'stream-chat' ;
1414
1515import {
1616 isMessageWithStylesReadByAndDateSeparator ,
@@ -104,6 +104,26 @@ const flatListViewabilityConfig: ViewabilityConfig = {
104104 viewAreaCoveragePercentThreshold : 1 ,
105105} ;
106106
107+ const hasReadLastMessage = ( channel : Channel , userId : string ) => {
108+ const latestMessageIdInChannel = channel . state . latestMessages . slice ( - 1 ) [ 0 ] ?. id ;
109+ const lastReadMessageIdServer = channel . state . read [ userId ] ?. last_read_message_id ;
110+ return latestMessageIdInChannel === lastReadMessageIdServer ;
111+ } ;
112+
113+ const getPreviousLastMessage = ( messages : MessageType [ ] , newMessage ?: MessageResponse ) => {
114+ if ( ! newMessage ) return ;
115+ let previousLastMessage ;
116+ for ( let i = messages . length - 1 ; i >= 0 ; i -- ) {
117+ const msg = messages [ i ] ;
118+ if ( ! msg ?. id ) break ;
119+ if ( msg . id !== newMessage . id ) {
120+ previousLastMessage = msg ;
121+ break ;
122+ }
123+ }
124+ return previousLastMessage ;
125+ } ;
126+
107127type MessageListPropsWithContext = Pick <
108128 AttachmentPickerContextValue ,
109129 'closePicker' | 'selectedPicker' | 'setSelectedPicker'
@@ -123,6 +143,7 @@ type MessageListPropsWithContext = Pick<
123143 | 'NetworkDownIndicator'
124144 | 'reloadChannel'
125145 | 'scrollToFirstUnreadThreshold'
146+ | 'setChannelUnreadState'
126147 | 'setTargetedMessage'
127148 | 'StickyHeader'
128149 | 'targetedMessage'
@@ -264,6 +285,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
264285 reloadChannel,
265286 ScrollToBottomButton,
266287 selectedPicker,
288+ setChannelUnreadState,
267289 setFlatListRef,
268290 setMessages,
269291 setSelectedPicker,
@@ -409,19 +431,32 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
409431 const lastItem = viewableItems [ viewableItems . length - 1 ] ;
410432
411433 if ( lastItem ) {
412- const lastItemCreatedAt = lastItem . item . created_at ;
434+ const lastItemMessage = lastItem . item ;
435+ const lastItemCreatedAt = lastItemMessage . created_at ;
413436
414437 const unreadIndicatorDate = channelUnreadState ?. last_read . getTime ( ) ;
415438 const lastItemDate = lastItemCreatedAt . getTime ( ) ;
416439
417440 if (
418441 ! channel . state . messagePagination . hasPrev &&
419- processedMessageList [ processedMessageList . length - 1 ] . id === lastItem . item . id
442+ processedMessageList [ processedMessageList . length - 1 ] . id === lastItemMessage . id
443+ ) {
444+ setIsUnreadNotificationOpen ( false ) ;
445+ return ;
446+ }
447+ /**
448+ * This is a special case where there is a single long message by the sender.
449+ * When a message is sent, we mark it as read before it actually has a `created_at` timestamp.
450+ * This is a workaround to prevent the unread indicator from showing when the message is sent.
451+ */
452+ if (
453+ viewableItems . length === 1 &&
454+ channel . countUnread ( ) === 0 &&
455+ lastItemMessage . user . id === client . userID
420456 ) {
421457 setIsUnreadNotificationOpen ( false ) ;
422458 return ;
423459 }
424-
425460 if ( unreadIndicatorDate && lastItemDate > unreadIndicatorDate ) {
426461 setIsUnreadNotificationOpen ( true ) ;
427462 } else {
@@ -476,19 +511,51 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
476511 * Effect to mark the channel as read when the user scrolls to the bottom of the message list.
477512 */
478513 useEffect ( ( ) => {
479- const listener : ReturnType < typeof channel . on > = channel . on ( 'message.new' , ( event ) => {
480- const newMessageToCurrentChannel = event . cid === channel . cid ;
481- const mainChannelUpdated = ! event . message ?. parent_id || event . message ?. show_in_channel ;
514+ const shouldMarkRead = ( ) => {
515+ return (
516+ ! channelUnreadState ?. first_unread_message_id &&
517+ ! scrollToBottomButtonVisible &&
518+ client . user ?. id &&
519+ ! hasReadLastMessage ( channel , client . user ?. id )
520+ ) ;
521+ } ;
482522
483- if ( newMessageToCurrentChannel && mainChannelUpdated && ! scrollToBottomButtonVisible ) {
484- markRead ( ) ;
523+ const handleEvent = async ( event : Event ) => {
524+ const mainChannelUpdated = ! event . message ?. parent_id || event . message ?. show_in_channel ;
525+ // When the scrollToBottomButtonVisible is true, we need to manually update the channelUnreadState.
526+ if ( scrollToBottomButtonVisible || channelUnreadState ?. first_unread_message_id ) {
527+ setChannelUnreadState ( ( prev ) => {
528+ const previousUnreadCount = prev ?. unread_messages ?? 0 ;
529+ const previousLastMessage = getPreviousLastMessage ( channel . state . messages , event . message ) ;
530+ return {
531+ ...( prev || { } ) ,
532+ last_read :
533+ prev ?. last_read ??
534+ ( previousUnreadCount === 0 && previousLastMessage ?. created_at
535+ ? new Date ( previousLastMessage . created_at )
536+ : new Date ( 0 ) ) , // not having information about the last read message means the whole channel is unread,
537+ unread_messages : previousUnreadCount + 1 ,
538+ } ;
539+ } ) ;
540+ } else if ( mainChannelUpdated && shouldMarkRead ( ) ) {
541+ await markRead ( ) ;
485542 }
486- } ) ;
543+ } ;
544+
545+ const listener : ReturnType < typeof channel . on > = channel . on ( 'message.new' , handleEvent ) ;
487546
488547 return ( ) => {
489548 listener ?. unsubscribe ( ) ;
490549 } ;
491- } , [ channel , markRead , scrollToBottomButtonVisible ] ) ;
550+ } , [
551+ channel ,
552+ channelUnreadState ?. first_unread_message_id ,
553+ client . user ?. id ,
554+ markRead ,
555+ scrollToBottomButtonVisible ,
556+ setChannelUnreadState ,
557+ threadList ,
558+ ] ) ;
492559
493560 useEffect ( ( ) => {
494561 const lastReceivedMessage = getLastReceivedMessage ( processedMessageList ) ;
@@ -886,6 +953,13 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
886953 }
887954
888955 setScrollToBottomButtonVisible ( false ) ;
956+ /**
957+ * When we are not in the bottom of the list, and we receive new messages, we need to mark the channel as read.
958+ We would still need to show the unread label, where the first unread message appeared so we don't update the channelUnreadState.
959+ */
960+ await markRead ( {
961+ updateChannelUnreadState : false ,
962+ } ) ;
889963 } ;
890964
891965 const scrollToIndexFailedRetryCountRef = useRef < number > ( 0 ) ;
@@ -1191,6 +1265,7 @@ export const MessageList = (props: MessageListProps) => {
11911265 NetworkDownIndicator,
11921266 reloadChannel,
11931267 scrollToFirstUnreadThreshold,
1268+ setChannelUnreadState,
11941269 setTargetedMessage,
11951270 StickyHeader,
11961271 targetedMessage,
@@ -1255,6 +1330,7 @@ export const MessageList = (props: MessageListProps) => {
12551330 ScrollToBottomButton,
12561331 scrollToFirstUnreadThreshold,
12571332 selectedPicker,
1333+ setChannelUnreadState,
12581334 setMessages,
12591335 setSelectedPicker,
12601336 setTargetedMessage,
0 commit comments