@@ -10,7 +10,7 @@ import {
10
10
ViewToken ,
11
11
} from 'react-native' ;
12
12
13
- import type { FormatMessageResponse } from 'stream-chat' ;
13
+ import type { Channel , Event , FormatMessageResponse , MessageResponse } from 'stream-chat' ;
14
14
15
15
import {
16
16
isMessageWithStylesReadByAndDateSeparator ,
@@ -108,6 +108,36 @@ const flatListViewabilityConfig: ViewabilityConfig = {
108
108
viewAreaCoveragePercentThreshold : 1 ,
109
109
} ;
110
110
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
+
111
141
type MessageListPropsWithContext <
112
142
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics ,
113
143
> = Pick < AttachmentPickerContextValue , 'closePicker' | 'selectedPicker' | 'setSelectedPicker' > &
@@ -126,6 +156,7 @@ type MessageListPropsWithContext<
126
156
| 'NetworkDownIndicator'
127
157
| 'reloadChannel'
128
158
| 'scrollToFirstUnreadThreshold'
159
+ | 'setChannelUnreadState'
129
160
| 'setTargetedMessage'
130
161
| 'StickyHeader'
131
162
| 'targetedMessage'
@@ -271,6 +302,7 @@ const MessageListWithContext = <
271
302
reloadChannel,
272
303
ScrollToBottomButton,
273
304
selectedPicker,
305
+ setChannelUnreadState,
274
306
setFlatListRef,
275
307
setMessages,
276
308
setSelectedPicker,
@@ -418,19 +450,32 @@ const MessageListWithContext = <
418
450
const lastItem = viewableItems [ viewableItems . length - 1 ] ;
419
451
420
452
if ( lastItem ) {
421
- const lastItemCreatedAt = lastItem . item . created_at ;
453
+ const lastItemMessage = lastItem . item ;
454
+ const lastItemCreatedAt = lastItemMessage . created_at ;
422
455
423
456
const unreadIndicatorDate = channelUnreadState ?. last_read . getTime ( ) ;
424
457
const lastItemDate = lastItemCreatedAt . getTime ( ) ;
425
458
426
459
if (
427
460
! 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
429
475
) {
430
476
setIsUnreadNotificationOpen ( false ) ;
431
477
return ;
432
478
}
433
-
434
479
if ( unreadIndicatorDate && lastItemDate > unreadIndicatorDate ) {
435
480
setIsUnreadNotificationOpen ( true ) ;
436
481
} else {
@@ -485,19 +530,56 @@ const MessageListWithContext = <
485
530
* Effect to mark the channel as read when the user scrolls to the bottom of the message list.
486
531
*/
487
532
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
+ } ;
491
541
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 ( ) ;
494
566
}
495
- } ) ;
567
+ } ;
568
+
569
+ const listener : ReturnType < typeof channel . on > = channel . on ( 'message.new' , handleEvent ) ;
496
570
497
571
return ( ) => {
498
572
listener ?. unsubscribe ( ) ;
499
573
} ;
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
+ ] ) ;
501
583
502
584
useEffect ( ( ) => {
503
585
const lastReceivedMessage = getLastReceivedMessage ( processedMessageList ) ;
@@ -537,6 +619,7 @@ const MessageListWithContext = <
537
619
setTimeout ( ( ) => {
538
620
channelResyncScrollSet . current = true ;
539
621
if ( channel . countUnread ( ) > 0 ) {
622
+ console . log ( 'marking read' ) ;
540
623
markRead ( ) ;
541
624
}
542
625
} , 500 ) ;
@@ -901,6 +984,13 @@ const MessageListWithContext = <
901
984
}
902
985
903
986
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
+ } ) ;
904
994
} ;
905
995
906
996
const scrollToIndexFailedRetryCountRef = useRef < number > ( 0 ) ;
@@ -1212,6 +1302,7 @@ export const MessageList = <
1212
1302
NetworkDownIndicator,
1213
1303
reloadChannel,
1214
1304
scrollToFirstUnreadThreshold,
1305
+ setChannelUnreadState,
1215
1306
setTargetedMessage,
1216
1307
StickyHeader,
1217
1308
targetedMessage,
@@ -1277,6 +1368,7 @@ export const MessageList = <
1277
1368
ScrollToBottomButton,
1278
1369
scrollToFirstUnreadThreshold,
1279
1370
selectedPicker,
1371
+ setChannelUnreadState,
1280
1372
setMessages,
1281
1373
setSelectedPicker,
1282
1374
setTargetedMessage,
0 commit comments