@@ -10,7 +10,7 @@ import {
1010 ViewToken ,
1111} from 'react-native' ;
1212
13- import type { LocalMessage } from 'stream-chat' ;
13+ import type { Channel , LocalMessage } from 'stream-chat' ;
1414
1515import {
1616 isMessageWithStylesReadByAndDateSeparator ,
@@ -104,6 +104,32 @@ const flatListViewabilityConfig: ViewabilityConfig = {
104104 viewAreaCoveragePercentThreshold : 1 ,
105105} ;
106106
107+ const hasReadLastMessage = (
108+ channel : Channel ,
109+ userId : string ,
110+ ) => {
111+ const latestMessageIdInChannel = channel . state . latestMessages . slice ( - 1 ) [ 0 ] ?. id ;
112+ const lastReadMessageIdServer = channel . state . read [ userId ] ?. last_read_message_id ;
113+ return latestMessageIdInChannel === lastReadMessageIdServer ;
114+ } ;
115+
116+ const getPreviousLastMessage = (
117+ messages : MessageType [ ] ,
118+ newMessage ?: MessageResponse ,
119+ ) => {
120+ if ( ! newMessage ) return ;
121+ let previousLastMessage ;
122+ for ( let i = messages . length - 1 ; i >= 0 ; i -- ) {
123+ const msg = messages [ i ] ;
124+ if ( ! msg ?. id ) break ;
125+ if ( msg . id !== newMessage . id ) {
126+ previousLastMessage = msg ;
127+ break ;
128+ }
129+ }
130+ return previousLastMessage ;
131+ } ;
132+
107133type MessageListPropsWithContext = Pick <
108134 AttachmentPickerContextValue ,
109135 'closePicker' | 'selectedPicker' | 'setSelectedPicker'
@@ -123,6 +149,7 @@ type MessageListPropsWithContext = Pick<
123149 | 'NetworkDownIndicator'
124150 | 'reloadChannel'
125151 | 'scrollToFirstUnreadThreshold'
152+ | 'setChannelUnreadState'
126153 | 'setTargetedMessage'
127154 | 'StickyHeader'
128155 | 'targetedMessage'
@@ -264,6 +291,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
264291 reloadChannel,
265292 ScrollToBottomButton,
266293 selectedPicker,
294+ setChannelUnreadState,
267295 setFlatListRef,
268296 setMessages,
269297 setSelectedPicker,
@@ -409,19 +437,32 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
409437 const lastItem = viewableItems [ viewableItems . length - 1 ] ;
410438
411439 if ( lastItem ) {
412- const lastItemCreatedAt = lastItem . item . created_at ;
440+ const lastItemMessage = lastItem . item ;
441+ const lastItemCreatedAt = lastItemMessage . created_at ;
413442
414443 const unreadIndicatorDate = channelUnreadState ?. last_read . getTime ( ) ;
415444 const lastItemDate = lastItemCreatedAt . getTime ( ) ;
416445
417446 if (
418447 ! channel . state . messagePagination . hasPrev &&
419- processedMessageList [ processedMessageList . length - 1 ] . id === lastItem . item . id
448+ processedMessageList [ processedMessageList . length - 1 ] . id === lastItemMessage . id
449+ ) {
450+ setIsUnreadNotificationOpen ( false ) ;
451+ return ;
452+ }
453+ /**
454+ * This is a special case where there is a single long message by the sender.
455+ * When a message is sent, we mark it as read before it actually has a `created_at` timestamp.
456+ * This is a workaround to prevent the unread indicator from showing when the message is sent.
457+ */
458+ if (
459+ viewableItems . length === 1 &&
460+ channel . countUnread ( ) === 0 &&
461+ lastItemMessage . user . id === client . userID
420462 ) {
421463 setIsUnreadNotificationOpen ( false ) ;
422464 return ;
423465 }
424-
425466 if ( unreadIndicatorDate && lastItemDate > unreadIndicatorDate ) {
426467 setIsUnreadNotificationOpen ( true ) ;
427468 } else {
@@ -476,19 +517,54 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
476517 * Effect to mark the channel as read when the user scrolls to the bottom of the message list.
477518 */
478519 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 ;
520+ const shouldMarkRead = ( ) => {
521+ return (
522+ ! channelUnreadState ?. first_unread_message_id &&
523+ ! scrollToBottomButtonVisible &&
524+ client . user ?. id &&
525+ ! hasReadLastMessage ( channel , client . user ?. id )
526+ ) ;
527+ } ;
482528
483- if ( newMessageToCurrentChannel && mainChannelUpdated && ! scrollToBottomButtonVisible ) {
484- markRead ( ) ;
529+ const handleEvent = async ( event : Event < StreamChatGenerics > ) => {
530+ const mainChannelUpdated = ! event . message ?. parent_id || event . message ?. show_in_channel ;
531+ // When the scrollToBottomButtonVisible is true, we need to manually update the channelUnreadState.
532+ if ( scrollToBottomButtonVisible || channelUnreadState ?. first_unread_message_id ) {
533+ setChannelUnreadState ( ( prev ) => {
534+ const previousUnreadCount = prev ?. unread_messages ?? 0 ;
535+ const previousLastMessage = getPreviousLastMessage < StreamChatGenerics > (
536+ channel . state . messages ,
537+ event . message ,
538+ ) ;
539+ return {
540+ ...( prev || { } ) ,
541+ last_read :
542+ prev ?. last_read ??
543+ ( previousUnreadCount === 0 && previousLastMessage ?. created_at
544+ ? new Date ( previousLastMessage . created_at )
545+ : new Date ( 0 ) ) , // not having information about the last read message means the whole channel is unread,
546+ unread_messages : previousUnreadCount + 1 ,
547+ } ;
548+ } ) ;
549+ } else if ( mainChannelUpdated && shouldMarkRead ( ) ) {
550+ await markRead ( ) ;
485551 }
486- } ) ;
552+ } ;
553+
554+ const listener : ReturnType < typeof channel . on > = channel . on ( 'message.new' , handleEvent ) ;
487555
488556 return ( ) => {
489557 listener ?. unsubscribe ( ) ;
490558 } ;
491- } , [ channel , markRead , scrollToBottomButtonVisible ] ) ;
559+ } , [
560+ channel ,
561+ channelUnreadState ?. first_unread_message_id ,
562+ client . user ?. id ,
563+ markRead ,
564+ scrollToBottomButtonVisible ,
565+ setChannelUnreadState ,
566+ threadList ,
567+ ] ) ;
492568
493569 useEffect ( ( ) => {
494570 const lastReceivedMessage = getLastReceivedMessage ( processedMessageList ) ;
@@ -886,6 +962,13 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
886962 }
887963
888964 setScrollToBottomButtonVisible ( false ) ;
965+ /**
966+ * When we are not in the bottom of the list, and we receive new messages, we need to mark the channel as read.
967+ We would still need to show the unread label, where the first unread message appeared so we don't update the channelUnreadState.
968+ */
969+ await markRead ( {
970+ updateChannelUnreadState : false ,
971+ } ) ;
889972 } ;
890973
891974 const scrollToIndexFailedRetryCountRef = useRef < number > ( 0 ) ;
@@ -1191,6 +1274,7 @@ export const MessageList = (props: MessageListProps) => {
11911274 NetworkDownIndicator,
11921275 reloadChannel,
11931276 scrollToFirstUnreadThreshold,
1277+ setChannelUnreadState,
11941278 setTargetedMessage,
11951279 StickyHeader,
11961280 targetedMessage,
@@ -1255,6 +1339,7 @@ export const MessageList = (props: MessageListProps) => {
12551339 ScrollToBottomButton,
12561340 scrollToFirstUnreadThreshold,
12571341 selectedPicker,
1342+ setChannelUnreadState,
12581343 setMessages,
12591344 setSelectedPicker,
12601345 setTargetedMessage,
0 commit comments