@@ -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,14 +450,28 @@ 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 ;
@@ -484,20 +530,55 @@ const MessageListWithContext = <
484
530
* Effect to mark the channel as read when the user scrolls to the bottom of the message list.
485
531
*/
486
532
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
+ } ;
490
542
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 ( ) ) {
493
564
await markRead ( ) ;
494
565
}
495
- } ) ;
566
+ } ;
567
+
568
+ const listener : ReturnType < typeof channel . on > = channel . on ( 'message.new' , handleEvent ) ;
496
569
497
570
return ( ) => {
498
571
listener ?. unsubscribe ( ) ;
499
572
} ;
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
+ ] ) ;
501
582
502
583
useEffect ( ( ) => {
503
584
const lastReceivedMessage = getLastReceivedMessage ( processedMessageList ) ;
@@ -901,6 +982,13 @@ const MessageListWithContext = <
901
982
}
902
983
903
984
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
+ } ) ;
904
992
} ;
905
993
906
994
const scrollToIndexFailedRetryCountRef = useRef < number > ( 0 ) ;
@@ -1212,6 +1300,7 @@ export const MessageList = <
1212
1300
NetworkDownIndicator,
1213
1301
reloadChannel,
1214
1302
scrollToFirstUnreadThreshold,
1303
+ setChannelUnreadState,
1215
1304
setTargetedMessage,
1216
1305
StickyHeader,
1217
1306
targetedMessage,
@@ -1277,6 +1366,7 @@ export const MessageList = <
1277
1366
ScrollToBottomButton,
1278
1367
scrollToFirstUnreadThreshold,
1279
1368
selectedPicker,
1369
+ setChannelUnreadState,
1280
1370
setMessages,
1281
1371
setSelectedPicker,
1282
1372
setTargetedMessage,
0 commit comments