Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package/src/components/Channel/Channel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,8 @@ const ChannelWithContext = <
const [deleted, setDeleted] = useState<boolean>(false);
const [editing, setEditing] = useState<MessageType<StreamChatGenerics> | undefined>(undefined);
const [error, setError] = useState<Error | boolean>(false);
const [lastRead, setLastRead] = useState<ChannelContextValue<StreamChatGenerics>['lastRead']>();
const [lastRead, setLastRead] = useState<Date | undefined>();

const [quotedMessage, setQuotedMessage] = useState<MessageType<StreamChatGenerics> | undefined>(
undefined,
);
Expand Down Expand Up @@ -961,6 +962,7 @@ const ChannelWithContext = <
last_read_message_id: response?.event.last_read_message_id,
unread_messages: 0,
});
setLastRead(new Date());
Comment thread
isekovanic marked this conversation as resolved.
}
} catch (err) {
console.log('Error marking channel as read:', err);
Expand Down
114 changes: 103 additions & 11 deletions package/src/components/MessageList/MessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
ViewToken,
} from 'react-native';

import type { FormatMessageResponse } from 'stream-chat';
import type { Channel, Event, FormatMessageResponse, MessageResponse } from 'stream-chat';

import {
isMessageWithStylesReadByAndDateSeparator,
Expand Down Expand Up @@ -108,6 +108,36 @@ const flatListViewabilityConfig: ViewabilityConfig = {
viewAreaCoveragePercentThreshold: 1,
};

const hasReadLastMessage = <
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
>(
channel: Channel<StreamChatGenerics>,
userId: string,
) => {
const latestMessageIdInChannel = channel.state.latestMessages.slice(-1)[0]?.id;
Comment thread
isekovanic marked this conversation as resolved.
const lastReadMessageIdServer = channel.state.read[userId]?.last_read_message_id;
return latestMessageIdInChannel === lastReadMessageIdServer;
};

const getPreviousLastMessage = <
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
>(
messages: MessageType<StreamChatGenerics>[],
newMessage?: MessageResponse<StreamChatGenerics>,
) => {
if (!newMessage) return;
let previousLastMessage;
for (let i = messages.length - 1; i >= 0; i--) {
const msg = messages[i];
if (!msg?.id) break;
if (msg.id !== newMessage.id) {
previousLastMessage = msg;
break;
}
}
return previousLastMessage;
};

type MessageListPropsWithContext<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
> = Pick<AttachmentPickerContextValue, 'closePicker' | 'selectedPicker' | 'setSelectedPicker'> &
Expand All @@ -126,6 +156,7 @@ type MessageListPropsWithContext<
| 'NetworkDownIndicator'
| 'reloadChannel'
| 'scrollToFirstUnreadThreshold'
| 'setChannelUnreadState'
| 'setTargetedMessage'
| 'StickyHeader'
| 'targetedMessage'
Expand Down Expand Up @@ -271,6 +302,7 @@ const MessageListWithContext = <
reloadChannel,
ScrollToBottomButton,
selectedPicker,
setChannelUnreadState,
setFlatListRef,
setMessages,
setSelectedPicker,
Expand Down Expand Up @@ -418,19 +450,32 @@ const MessageListWithContext = <
const lastItem = viewableItems[viewableItems.length - 1];

if (lastItem) {
const lastItemCreatedAt = lastItem.item.created_at;
const lastItemMessage = lastItem.item;
const lastItemCreatedAt = lastItemMessage.created_at;

const unreadIndicatorDate = channelUnreadState?.last_read.getTime();
const lastItemDate = lastItemCreatedAt.getTime();

if (
!channel.state.messagePagination.hasPrev &&
processedMessageList[processedMessageList.length - 1].id === lastItem.item.id
processedMessageList[processedMessageList.length - 1].id === lastItemMessage.id
) {
setIsUnreadNotificationOpen(false);
return;
}
/**
* This is a special case where there is a single long message by the sender.
* When a message is sent, we mark it as read before it actually has a `created_at` timestamp.
* This is a workaround to prevent the unread indicator from showing when the message is sent.
*/
Comment thread
isekovanic marked this conversation as resolved.
if (
viewableItems.length === 1 &&
channel.countUnread() === 0 &&
lastItemMessage.user.id === client.userID
) {
setIsUnreadNotificationOpen(false);
return;
}

if (unreadIndicatorDate && lastItemDate > unreadIndicatorDate) {
setIsUnreadNotificationOpen(true);
} else {
Expand Down Expand Up @@ -485,19 +530,56 @@ const MessageListWithContext = <
* Effect to mark the channel as read when the user scrolls to the bottom of the message list.
*/
useEffect(() => {
const listener: ReturnType<typeof channel.on> = channel.on('message.new', (event) => {
const newMessageToCurrentChannel = event.cid === channel.cid;
const mainChannelUpdated = !event.message?.parent_id || event.message?.show_in_channel;
const shouldMarkRead = () => {
return (
!channelUnreadState?.first_unread_message_id &&
!scrollToBottomButtonVisible &&
client.user?.id &&
!hasReadLastMessage(channel, client.user?.id)
);
};

if (newMessageToCurrentChannel && mainChannelUpdated && !scrollToBottomButtonVisible) {
markRead();
const handleEvent = async (event: Event<StreamChatGenerics>) => {
const mainChannelUpdated = !event.message?.parent_id || event.message?.show_in_channel;
console.log(mainChannelUpdated, shouldMarkRead());
// When the scrollToBottomButtonVisible is true, we need to manually update the channelUnreadState.
if (scrollToBottomButtonVisible || channelUnreadState?.first_unread_message_id) {
Comment thread
isekovanic marked this conversation as resolved.
setChannelUnreadState((prev) => {
const previousUnreadCount = prev?.unread_messages ?? 0;
const previousLastMessage = getPreviousLastMessage<StreamChatGenerics>(
channel.state.messages,
event.message,
);
return {
...(prev || {}),
last_read:
prev?.last_read ??
(previousUnreadCount === 0 && previousLastMessage?.created_at
? new Date(previousLastMessage.created_at)
: new Date(0)), // not having information about the last read message means the whole channel is unread,
unread_messages: previousUnreadCount + 1,
};
});
} else if (mainChannelUpdated && shouldMarkRead()) {
console.log('marking read');
await markRead();
}
});
};

const listener: ReturnType<typeof channel.on> = channel.on('message.new', handleEvent);

return () => {
listener?.unsubscribe();
};
}, [channel, markRead, scrollToBottomButtonVisible]);
}, [
channel,
channelUnreadState?.first_unread_message_id,
client.user?.id,
markRead,
scrollToBottomButtonVisible,
setChannelUnreadState,
threadList,
]);

useEffect(() => {
const lastReceivedMessage = getLastReceivedMessage(processedMessageList);
Expand Down Expand Up @@ -537,6 +619,7 @@ const MessageListWithContext = <
setTimeout(() => {
channelResyncScrollSet.current = true;
if (channel.countUnread() > 0) {
console.log('marking read');
markRead();
}
}, 500);
Expand Down Expand Up @@ -901,6 +984,13 @@ const MessageListWithContext = <
}

setScrollToBottomButtonVisible(false);
/**
* When we are not in the bottom of the list, and we receive new messages, we need to mark the channel as read.
We would still need to show the unread label, where the first unread message appeared so we don't update the channelUnreadState.
*/
await markRead({
updateChannelUnreadState: false,
});
};

const scrollToIndexFailedRetryCountRef = useRef<number>(0);
Expand Down Expand Up @@ -1212,6 +1302,7 @@ export const MessageList = <
NetworkDownIndicator,
reloadChannel,
scrollToFirstUnreadThreshold,
setChannelUnreadState,
setTargetedMessage,
StickyHeader,
targetedMessage,
Expand Down Expand Up @@ -1277,6 +1368,7 @@ export const MessageList = <
ScrollToBottomButton,
scrollToFirstUnreadThreshold,
selectedPicker,
setChannelUnreadState,
setMessages,
setSelectedPicker,
setTargetedMessage,
Expand Down