Skip to content

Commit 53eeafb

Browse files
authored
fix: unread indicator label presence in message list (#3031)
* fix: unread indicator label presence in message list * fix: unread indicator label presence in message list * fix: threadList checjk
1 parent ccbce6e commit 53eeafb

File tree

2 files changed

+106
-12
lines changed

2 files changed

+106
-12
lines changed

package/src/components/Channel/Channel.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -694,7 +694,8 @@ const ChannelWithContext = <
694694
const [deleted, setDeleted] = useState<boolean>(false);
695695
const [editing, setEditing] = useState<MessageType<StreamChatGenerics> | undefined>(undefined);
696696
const [error, setError] = useState<Error | boolean>(false);
697-
const [lastRead, setLastRead] = useState<ChannelContextValue<StreamChatGenerics>['lastRead']>();
697+
const [lastRead, setLastRead] = useState<Date | undefined>();
698+
698699
const [quotedMessage, setQuotedMessage] = useState<MessageType<StreamChatGenerics> | undefined>(
699700
undefined,
700701
);
@@ -961,6 +962,7 @@ const ChannelWithContext = <
961962
last_read_message_id: response?.event.last_read_message_id,
962963
unread_messages: 0,
963964
});
965+
setLastRead(new Date());
964966
}
965967
} catch (err) {
966968
console.log('Error marking channel as read:', err);

package/src/components/MessageList/MessageList.tsx

+103-11
Original file line numberDiff line numberDiff line change
@@ -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

1515
import {
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+
111141
type 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

Comments
 (0)