diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index a731acd6a4..702b5e7a78 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -102,7 +102,11 @@ import { isImagePickerAvailable, NativeHandlers, } from '../../native'; -import { ChannelUnreadState, FileTypes } from '../../types/types'; +import { + ChannelUnreadStateStore, + ChannelUnreadStateStoreType, +} from '../../state-store/channel-unread-state'; +import { FileTypes } from '../../types/types'; import { addReactionToLocalState } from '../../utils/addReactionToLocalState'; import { compressedImageURI } from '../../utils/compressImage'; import { patchMessageTextCommand } from '../../utils/patchMessageTextCommand'; @@ -420,7 +424,7 @@ export type ChannelPropsWithContext = Pick & */ doMarkReadRequest?: ( channel: ChannelType, - setChannelUnreadUiState?: (state: ChannelUnreadState) => void, + setChannelUnreadUiState?: (data: ChannelUnreadStateStoreType['channelUnreadState']) => void, ) => void; /** * Overrides the Stream default send message request (Advanced usage only) @@ -765,10 +769,13 @@ const ChannelWithContext = (props: PropsWithChildren) = const [thread, setThread] = useState(threadProps || null); const [threadHasMore, setThreadHasMore] = useState(true); const [threadLoadingMore, setThreadLoadingMore] = useState(false); - const [channelUnreadState, setChannelUnreadState] = useState( - undefined, + const channelUnreadStateStore = useMemo(() => new ChannelUnreadStateStore(), []); + const setChannelUnreadState = useCallback( + (data: ChannelUnreadStateStoreType['channelUnreadState']) => { + channelUnreadStateStore.channelUnreadState = data; + }, + [channelUnreadStateStore], ); - const { bottomSheetRef, closePicker, openPicker } = useAttachmentPickerBottomSheet(); const syncingChannelRef = useRef(false); @@ -893,16 +900,14 @@ const ChannelWithContext = (props: PropsWithChildren) = } if (event.type === 'notification.mark_unread') { - setChannelUnreadState((prev) => { - if (!(event.last_read_at && event.user)) { - return prev; - } - return { - first_unread_message_id: event.first_unread_message_id, - last_read: new Date(event.last_read_at), - last_read_message_id: event.last_read_message_id, - unread_messages: event.unread_messages ?? 0, - }; + if (!(event.last_read_at && event.user)) { + return; + } + setChannelUnreadState({ + first_unread_message_id: event.first_unread_message_id, + last_read: new Date(event.last_read_at), + last_read_message_id: event.last_read_message_id, + unread_messages: event.unread_messages ?? 0, }); } @@ -1758,7 +1763,8 @@ const ChannelWithContext = (props: PropsWithChildren) = const channelContext = useCreateChannelContext({ channel, - channelUnreadState, + channelUnreadState: channelUnreadStateStore.channelUnreadState, + channelUnreadStateStore, disabled: !!channel?.data?.frozen, EmptyStateIndicator, enableMessageGroupingByUser, diff --git a/package/src/components/Channel/hooks/useCreateChannelContext.ts b/package/src/components/Channel/hooks/useCreateChannelContext.ts index b58b0dad60..d47c70fc5d 100644 --- a/package/src/components/Channel/hooks/useCreateChannelContext.ts +++ b/package/src/components/Channel/hooks/useCreateChannelContext.ts @@ -5,6 +5,7 @@ import type { ChannelContextValue } from '../../../contexts/channelContext/Chann export const useCreateChannelContext = ({ channel, channelUnreadState, + channelUnreadStateStore, disabled, EmptyStateIndicator, enableMessageGroupingByUser, @@ -46,12 +47,12 @@ export const useCreateChannelContext = ({ const readUsersLastReads = readUsers .map(({ last_read }) => last_read?.toISOString() ?? '') .join(); - const stringifiedChannelUnreadState = JSON.stringify(channelUnreadState); const channelContext: ChannelContextValue = useMemo( () => ({ channel, channelUnreadState, + channelUnreadStateStore, disabled, EmptyStateIndicator, enableMessageGroupingByUser, @@ -96,7 +97,6 @@ export const useCreateChannelContext = ({ membersLength, readUsersLength, readUsersLastReads, - stringifiedChannelUnreadState, targetedMessage, threadList, watcherCount, diff --git a/package/src/components/Message/Message.tsx b/package/src/components/Message/Message.tsx index d1460cb61d..7817139c20 100644 --- a/package/src/components/Message/Message.tsx +++ b/package/src/components/Message/Message.tsx @@ -252,7 +252,6 @@ const MessageWithContext = (props: MessagePropsWithContext) => { handleRetry, handleThreadReply, isTargetedMessage, - lastReceivedId, members, message, messageActions: messageActionsProp = defaultMessageActions, @@ -650,7 +649,6 @@ const MessageWithContext = (props: MessagePropsWithContext) => { isMessageAIGenerated, isMyMessage, lastGroupMessage: groupStyles?.[0] === 'single' || groupStyles?.[0] === 'bottom', - lastReceivedId, members, message, messageContentOrder, @@ -783,7 +781,6 @@ const areEqual = (prevProps: MessagePropsWithContext, nextProps: MessagePropsWit groupStyles: prevGroupStyles, isAttachmentEqual, isTargetedMessage: prevIsTargetedMessage, - lastReceivedId: prevLastReceivedId, members: prevMembers, message: prevMessage, messagesContext: prevMessagesContext, @@ -797,7 +794,6 @@ const areEqual = (prevProps: MessagePropsWithContext, nextProps: MessagePropsWit goToMessage: nextGoToMessage, groupStyles: nextGroupStyles, isTargetedMessage: nextIsTargetedMessage, - lastReceivedId: nextLastReceivedId, members: nextMembers, message: nextMessage, messagesContext: nextMessagesContext, @@ -826,17 +822,6 @@ const areEqual = (prevProps: MessagePropsWithContext, nextProps: MessagePropsWit return false; } - const lastReceivedIdChangedAndMatters = - prevLastReceivedId !== nextLastReceivedId && - (prevLastReceivedId === prevMessage.id || - prevLastReceivedId === nextMessage.id || - nextLastReceivedId === prevMessage.id || - nextLastReceivedId === nextMessage.id); - - if (lastReceivedIdChangedAndMatters) { - return false; - } - const goToMessageChangedAndMatters = nextMessage.quoted_message_id && prevGoToMessage !== nextGoToMessage; diff --git a/package/src/components/Message/MessageSimple/MessageContent.tsx b/package/src/components/Message/MessageSimple/MessageContent.tsx index 6c7ca849bd..aa7efb1af0 100644 --- a/package/src/components/Message/MessageSimple/MessageContent.tsx +++ b/package/src/components/Message/MessageSimple/MessageContent.tsx @@ -535,7 +535,6 @@ export const MessageContent = (props: MessageContentProps) => { isEditedMessageOpen, isMessageAIGenerated, isMyMessage, - lastReceivedId, message, messageContentOrder, onLongPress, @@ -575,7 +574,6 @@ export const MessageContent = (props: MessageContentProps) => { isEditedMessageOpen, isMessageAIGenerated, isMyMessage, - lastReceivedId, message, messageContentOrder, MessageError, diff --git a/package/src/components/Message/MessageSimple/MessageWrapper.tsx b/package/src/components/Message/MessageSimple/MessageWrapper.tsx new file mode 100644 index 0000000000..1fa3a83e49 --- /dev/null +++ b/package/src/components/Message/MessageSimple/MessageWrapper.tsx @@ -0,0 +1,123 @@ +import React from 'react'; + +import { View } from 'react-native'; + +import { LocalMessage } from 'stream-chat'; + +import { MessageListProps } from '../../../components/MessageList/MessageList'; +import { useChannelContext } from '../../../contexts/channelContext/ChannelContext'; +import { useChatContext } from '../../../contexts/chatContext/ChatContext'; +import { MessageContextValue } from '../../../contexts/messageContext/MessageContext'; +import { useMessagesContext } from '../../../contexts/messagesContext/MessagesContext'; +import { ThemeProvider, useTheme } from '../../../contexts/themeContext/ThemeContext'; + +import { Theme } from '../../../contexts/themeContext/utils/theme'; +import { useStateStore } from '../../../hooks/useStateStore'; +import { ChannelUnreadStateStoreType } from '../../../state-store/channel-unread-state'; + +const channelUnreadStateSelector = (state: ChannelUnreadStateStoreType) => ({ + first_unread_message_id: state.channelUnreadState?.first_unread_message_id, + last_read: state.channelUnreadState?.last_read, + last_read_message_id: state.channelUnreadState?.last_read_message_id, + unread_messages: state.channelUnreadState?.unread_messages, +}); + +export type MessageWrapperProps = Pick & + Pick & { + isNewestMessage?: boolean; + message: LocalMessage; + modifiedTheme?: Theme; + dateSeparatorDate?: Date; + messageGroupStyles?: string[]; + }; + +export const MessageWrapper = (props: MessageWrapperProps) => { + const { + dateSeparatorDate, + isNewestMessage, + message, + messageGroupStyles, + goToMessage, + onThreadSelect, + modifiedTheme, + } = props; + const { client } = useChatContext(); + const { channelUnreadStateStore, channel, highlightedMessageId, threadList } = + useChannelContext(); + const { + InlineDateSeparator, + InlineUnreadIndicator, + Message, + MessageSystem, + myMessageTheme, + shouldShowUnreadUnderlay, + } = useMessagesContext(); + + const { first_unread_message_id, last_read, last_read_message_id, unread_messages } = + useStateStore(channelUnreadStateStore.state, channelUnreadStateSelector); + const { + theme: { + messageList: { messageContainer }, + screenPadding, + }, + } = useTheme(); + if (!channel || channel.disconnected) { + return null; + } + + const createdAtTimestamp = message.created_at && new Date(message.created_at).getTime(); + const lastReadTimestamp = last_read?.getTime(); + const isLastReadMessage = + last_read_message_id === message.id || + (!unread_messages && createdAtTimestamp === lastReadTimestamp); + + const showUnreadSeparator = + isLastReadMessage && + !isNewestMessage && + // The `channelUnreadState?.first_unread_message_id` is here for sent messages unread label + (!!first_unread_message_id || !!unread_messages); + + const showUnreadUnderlay = !!shouldShowUnreadUnderlay && showUnreadSeparator; + + const wrapMessageInTheme = client.userID === message.user?.id && !!myMessageTheme; + const renderDateSeperator = dateSeparatorDate ? ( + + ) : null; + + const renderMessage = ( + + ); + + return ( + + {message.type === 'system' ? ( + + ) : wrapMessageInTheme ? ( + + + {renderDateSeperator} + {renderMessage} + + + ) : ( + + {renderDateSeperator} + {renderMessage} + + )} + {showUnreadUnderlay && } + + ); +}; diff --git a/package/src/components/Message/MessageSimple/__tests__/MessageStatus.test.js b/package/src/components/Message/MessageSimple/__tests__/MessageStatus.test.js index 36958bfe38..e741c75844 100644 --- a/package/src/components/Message/MessageSimple/__tests__/MessageStatus.test.js +++ b/package/src/components/Message/MessageSimple/__tests__/MessageStatus.test.js @@ -81,11 +81,7 @@ describe('MessageStatus', () => { - + , diff --git a/package/src/components/Message/hooks/useCreateMessageContext.ts b/package/src/components/Message/hooks/useCreateMessageContext.ts index d2bb4d994f..b44fa768e5 100644 --- a/package/src/components/Message/hooks/useCreateMessageContext.ts +++ b/package/src/components/Message/hooks/useCreateMessageContext.ts @@ -22,7 +22,6 @@ export const useCreateMessageContext = ({ isMessageAIGenerated, isMyMessage, lastGroupMessage, - lastReceivedId, members, message, messageContentOrder, @@ -74,7 +73,6 @@ export const useCreateMessageContext = ({ isMessageAIGenerated, isMyMessage, lastGroupMessage, - lastReceivedId, members, message, messageContentOrder, @@ -105,7 +103,6 @@ export const useCreateMessageContext = ({ hasReactions, isEditedMessageOpen, lastGroupMessage, - lastReceivedId, membersValue, myMessageThemeString, reactionsValue, diff --git a/package/src/components/MessageList/MessageFlashList.tsx b/package/src/components/MessageList/MessageFlashList.tsx index efd775cd99..e408d611f7 100644 --- a/package/src/components/MessageList/MessageFlashList.tsx +++ b/package/src/components/MessageList/MessageFlashList.tsx @@ -17,8 +17,6 @@ import { InlineLoadingMoreIndicator } from './InlineLoadingMoreIndicator'; import { InlineLoadingMoreRecentIndicator } from './InlineLoadingMoreRecentIndicator'; import { InlineLoadingMoreRecentThreadIndicator } from './InlineLoadingMoreRecentThreadIndicator'; -import { getLastReceivedMessageFlashList } from './utils/getLastReceivedMessageFlashList'; - import { AttachmentPickerContextValue, useAttachmentPickerContext, @@ -44,11 +42,13 @@ import { PaginatedMessageListContextValue, usePaginatedMessageListContext, } from '../../contexts/paginatedMessageListContext/PaginatedMessageListContext'; -import { mergeThemes, ThemeProvider, useTheme } from '../../contexts/themeContext/ThemeContext'; +import { mergeThemes, useTheme } from '../../contexts/themeContext/ThemeContext'; import { ThreadContextValue, useThreadContext } from '../../contexts/threadContext/ThreadContext'; -import { useStableCallback } from '../../hooks'; +import { useStableCallback, useStateStore } from '../../hooks'; +import { ChannelUnreadStateStoreType } from '../../state-store/channel-unread-state'; import { FileTypes } from '../../types/types'; +import { MessageWrapper } from '../Message/MessageSimple/MessageWrapper'; let FlashList; @@ -102,6 +102,7 @@ type MessageFlashListPropsWithContext = Pick< ChannelContextValue, | 'channel' | 'channelUnreadState' + | 'channelUnreadStateStore' | 'disabled' | 'EmptyStateIndicator' | 'hideStickyDateHeader' @@ -250,6 +251,13 @@ const getItemTypeInternal = (message: LocalMessage) => { return 'generic-message'; }; +const channelUnreadStateSelector = (state: ChannelUnreadStateStoreType) => ({ + first_unread_message_id: state.channelUnreadState?.first_unread_message_id, + last_read: state.channelUnreadState?.last_read, + last_read_message_id: state.channelUnreadState?.last_read_message_id, + unread_messages: state.channelUnreadState?.unread_messages, +}); + const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => { const LoadingMoreRecentIndicator = props.threadList ? InlineLoadingMoreRecentThreadIndicator @@ -257,7 +265,7 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => const { additionalFlashListProps, channel, - channelUnreadState, + channelUnreadStateStore, client, closePicker, DateHeader, @@ -268,9 +276,6 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => FooterComponent = LoadingMoreRecentIndicator, HeaderComponent = InlineLoadingMoreIndicator, hideStickyDateHeader, - highlightedMessageId, - InlineDateSeparator, - InlineUnreadIndicator, isListActive = false, isLiveStreaming = false, legacyImageViewerSwipeBehaviour, @@ -283,8 +288,6 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => loadMoreThread, markRead, maximumMessageLimit, - Message, - MessageSystem, myMessageTheme, readEvents, NetworkDownIndicator, @@ -299,7 +302,6 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => setMessages, setSelectedPicker, setTargetedMessage, - shouldShowUnreadUnderlay, StickyHeader, targetedMessage, thread, @@ -310,6 +312,10 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => UnreadMessagesNotification, } = props; const flashListRef = useRef | null>(null); + const channelUnreadState = useStateStore( + channelUnreadStateStore.state, + channelUnreadStateSelector, + ); const [hasMoved, setHasMoved] = useState(false); const [scrollToBottomButtonVisible, setScrollToBottomButtonVisible] = useState(false); @@ -378,11 +384,6 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => client.userID, ); - const lastReceivedId = useMemo( - () => getLastReceivedMessageFlashList(processedMessageList)?.id, - [processedMessageList], - ); - const [autoscrollToRecent, setAutoscrollToRecent] = useState(true); useEffect(() => { @@ -574,18 +575,16 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => (scrollToBottomButtonVisible || channelUnreadState?.first_unread_message_id) && !isMyOwnMessage ) { - setChannelUnreadState((prev) => { - const previousUnreadCount = prev?.unread_messages ?? 0; - const previousLastMessage = getPreviousLastMessage(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, - }; + const previousUnreadCount = channelUnreadState?.unread_messages ?? 0; + const previousLastMessage = getPreviousLastMessage(channel.state.messages, event.message); + setChannelUnreadState({ + ...channelUnreadState, + last_read: + channelUnreadState.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()) { await markRead(); @@ -599,7 +598,7 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => }; }, [ channel, - channelUnreadState?.first_unread_message_id, + channelUnreadState, client.user?.id, markRead, scrollToBottomButtonVisible, @@ -657,7 +656,7 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => const lastItemMessage = lastItem.item; const lastItemCreatedAt = lastItemMessage.created_at; - const unreadIndicatorDate = channelUnreadState?.last_read.getTime(); + const unreadIndicatorDate = channelUnreadState?.last_read?.getTime(); const lastItemDate = lastItemCreatedAt.getTime(); if ( @@ -718,88 +717,23 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => const renderItem = useCallback( ({ index, item: message }: { index: number; item: LocalMessage }) => { - if (!channel || channel.disconnected) { - return null; - } - - const createdAtTimestamp = message.created_at && new Date(message.created_at).getTime(); - const lastReadTimestamp = channelUnreadState?.last_read.getTime(); + const dateSeparatorDate = dateSeparatorsRef.current[message.id]; + const messageGroupStyles = messageGroupStylesRef.current[message.id] ?? []; const isNewestMessage = index === 0; - const isLastReadMessage = - channelUnreadState?.last_read_message_id === message.id || - (!channelUnreadState?.unread_messages && createdAtTimestamp === lastReadTimestamp); - - const showUnreadSeparator = - isLastReadMessage && - !isNewestMessage && - // The `channelUnreadState?.first_unread_message_id` is here for sent messages unread label - (!!channelUnreadState?.first_unread_message_id || !!channelUnreadState?.unread_messages); - - const showUnreadUnderlay = !!shouldShowUnreadUnderlay && showUnreadSeparator; - - const wrapMessageInTheme = client.userID === message.user?.id && !!myMessageTheme; - const renderDateSeperator = dateSeparatorsRef.current[message.id] && ( - - ); - const renderMessage = ( - ); - - return ( - - {message.type === 'system' ? ( - - ) : wrapMessageInTheme ? ( - - - {renderDateSeperator} - {renderMessage} - - - ) : ( - - {renderDateSeperator} - {renderMessage} - - )} - {showUnreadUnderlay && } - - ); }, - [ - InlineDateSeparator, - InlineUnreadIndicator, - Message, - MessageSystem, - channel, - channelUnreadState?.first_unread_message_id, - channelUnreadState?.last_read, - channelUnreadState?.last_read_message_id, - channelUnreadState?.unread_messages, - client.userID, - dateSeparatorsRef, - goToMessage, - highlightedMessageId, - lastReceivedId, - messageGroupStylesRef, - modifiedTheme, - myMessageTheme, - onThreadSelect, - shouldShowUnreadUnderlay, - threadList, - ], + [dateSeparatorsRef, goToMessage, messageGroupStylesRef, modifiedTheme, onThreadSelect], ); const messagesWithImages = @@ -1194,6 +1128,7 @@ export const MessageFlashList = (props: MessageFlashListProps) => { const { channel, channelUnreadState, + channelUnreadStateStore, disabled, EmptyStateIndicator, enableMessageGroupingByUser, @@ -1242,6 +1177,7 @@ export const MessageFlashList = (props: MessageFlashListProps) => { {...{ channel, channelUnreadState, + channelUnreadStateStore, client, closePicker, DateHeader, diff --git a/package/src/components/MessageList/MessageList.tsx b/package/src/components/MessageList/MessageList.tsx index d6ed5a9285..b52342ce24 100644 --- a/package/src/components/MessageList/MessageList.tsx +++ b/package/src/components/MessageList/MessageList.tsx @@ -17,7 +17,6 @@ import { useShouldScrollToRecentOnNewOwnMessage } from './hooks/useShouldScrollT import { InlineLoadingMoreIndicator } from './InlineLoadingMoreIndicator'; import { InlineLoadingMoreRecentIndicator } from './InlineLoadingMoreRecentIndicator'; import { InlineLoadingMoreRecentThreadIndicator } from './InlineLoadingMoreRecentThreadIndicator'; -import { getLastReceivedMessage } from './utils/getLastReceivedMessage'; import { AttachmentPickerContextValue, @@ -45,11 +44,13 @@ import { PaginatedMessageListContextValue, usePaginatedMessageListContext, } from '../../contexts/paginatedMessageListContext/PaginatedMessageListContext'; -import { mergeThemes, ThemeProvider, useTheme } from '../../contexts/themeContext/ThemeContext'; +import { mergeThemes, useTheme } from '../../contexts/themeContext/ThemeContext'; import { ThreadContextValue, useThreadContext } from '../../contexts/threadContext/ThreadContext'; -import { useStableCallback } from '../../hooks'; +import { useStableCallback, useStateStore } from '../../hooks'; +import { ChannelUnreadStateStoreType } from '../../state-store/channel-unread-state'; import { FileTypes } from '../../types/types'; +import { MessageWrapper } from '../Message/MessageSimple/MessageWrapper'; // This is just to make sure that the scrolling happens in a different task queue. // TODO: Think if we really need this and strive to remove it if we can. @@ -115,6 +116,13 @@ const getPreviousLastMessage = (messages: LocalMessage[], newMessage?: MessageRe return previousLastMessage; }; +const channelUnreadStateSelector = (state: ChannelUnreadStateStoreType) => ({ + first_unread_message_id: state.channelUnreadState?.first_unread_message_id, + last_read: state.channelUnreadState?.last_read, + last_read_message_id: state.channelUnreadState?.last_read_message_id, + unread_messages: state.channelUnreadState?.unread_messages, +}); + type MessageListPropsWithContext = Pick< AttachmentPickerContextValue, 'closePicker' | 'selectedPicker' | 'setSelectedPicker' @@ -124,10 +132,10 @@ type MessageListPropsWithContext = Pick< ChannelContextValue, | 'channel' | 'channelUnreadState' + | 'channelUnreadStateStore' | 'disabled' | 'EmptyStateIndicator' | 'hideStickyDateHeader' - | 'highlightedMessageId' | 'loadChannelAroundMessage' | 'loading' | 'LoadingIndicator' @@ -150,14 +158,9 @@ type MessageListPropsWithContext = Pick< | 'DateHeader' | 'disableTypingIndicator' | 'FlatList' - | 'InlineDateSeparator' - | 'InlineUnreadIndicator' | 'legacyImageViewerSwipeBehaviour' - | 'Message' | 'ScrollToBottomButton' - | 'MessageSystem' | 'myMessageTheme' - | 'shouldShowUnreadUnderlay' | 'TypingIndicator' | 'TypingIndicatorContainer' | 'UnreadMessagesNotification' @@ -250,7 +253,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { const { additionalFlatListProps, channel, - channelUnreadState, + channelUnreadStateStore, client, closePicker, DateHeader, @@ -261,9 +264,6 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { FooterComponent = InlineLoadingMoreIndicator, HeaderComponent = LoadingMoreRecentIndicator, hideStickyDateHeader, - highlightedMessageId, - InlineDateSeparator, - InlineUnreadIndicator, inverted = true, isListActive = false, isLiveStreaming = false, @@ -277,8 +277,6 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { loadMoreThread, markRead, maximumMessageLimit, - Message, - MessageSystem, myMessageTheme, NetworkDownIndicator, noGroupByUser, @@ -293,7 +291,6 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { setMessages, setSelectedPicker, setTargetedMessage, - shouldShowUnreadUnderlay, StickyHeader, targetedMessage, thread, @@ -305,11 +302,14 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { } = props; const [isUnreadNotificationOpen, setIsUnreadNotificationOpen] = useState(false); const { theme } = useTheme(); + const channelUnreadState = useStateStore( + channelUnreadStateStore.state, + channelUnreadStateSelector, + ); const { colors: { white_snow }, - messageList: { container, contentContainer, listContainer, messageContainer }, - screenPadding, + messageList: { container, contentContainer, listContainer }, } = theme; const myMessageThemeString = useMemo(() => JSON.stringify(myMessageTheme), [myMessageTheme]); @@ -395,10 +395,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { */ const messageIdLastScrolledToRef = useRef(undefined); const [hasMoved, setHasMoved] = useState(false); - const lastReceivedId = useMemo( - () => getLastReceivedMessage(processedMessageList)?.id, - [processedMessageList], - ); + const [scrollToBottomButtonVisible, setScrollToBottomButtonVisible] = useState(false); const [stickyHeaderDate, setStickyHeaderDate] = useState(); @@ -437,8 +434,6 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { } }); - const messagesLength = useRef(processedMessageList.length); - /** * This function should show or hide the unread indicator depending on the */ @@ -446,17 +441,15 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { // we need this check to make sure that regular list change do not trigger // the unread notification to appear (for example if the old last read messages // go out of the viewport). - if (processedMessageList.length !== messagesLength.current) { - return; - } - messagesLength.current = processedMessageList.length; + const lastReadMessageId = channelUnreadState?.last_read_message_id; + const lastReadMessageVisible = viewableItems.some((item) => item.item.id === lastReadMessageId); - if (!viewableItems.length || !readEvents) { - setIsUnreadNotificationOpen(false); - return; - } - - if (selectedPicker === 'images') { + if ( + !viewableItems.length || + !readEvents || + lastReadMessageVisible || + selectedPicker === 'images' + ) { setIsUnreadNotificationOpen(false); return; } @@ -467,7 +460,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { const lastItemMessage = lastItem.item; const lastItemCreatedAt = lastItemMessage.created_at; - const unreadIndicatorDate = channelUnreadState?.last_read.getTime(); + const unreadIndicatorDate = channelUnreadState?.last_read?.getTime(); const lastItemDate = lastItemCreatedAt.getTime(); if ( @@ -563,18 +556,16 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { (scrollToBottomButtonVisible || channelUnreadState?.first_unread_message_id) && !isMyOwnMessage ) { - setChannelUnreadState((prev) => { - const previousUnreadCount = prev?.unread_messages ?? 0; - const previousLastMessage = getPreviousLastMessage(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, - }; + const previousUnreadCount = channelUnreadState.unread_messages ?? 0; + const previousLastMessage = getPreviousLastMessage(channel.state.messages, event.message); + setChannelUnreadState({ + ...channelUnreadState, + last_read: + channelUnreadState.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()) { await markRead(); @@ -588,7 +579,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { }; }, [ channel, - channelUnreadState?.first_unread_message_id, + channelUnreadState, client.user?.id, markRead, scrollToBottomButtonVisible, @@ -781,94 +772,23 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { const renderItem = useCallback( ({ index, item: message }: { index: number; item: LocalMessage }) => { - if (!channel || channel.disconnected) { - return null; - } - - const createdAtTimestamp = message.created_at && new Date(message.created_at).getTime(); - const lastReadTimestamp = channelUnreadState?.last_read.getTime(); + const dateSeparatorDate = dateSeparatorsRef.current[message.id]; + const messageGroupStyles = messageGroupStylesRef.current[message.id] ?? []; const isNewestMessage = index === 0; - const isLastReadMessage = - channelUnreadState?.last_read_message_id === message.id || - (!channelUnreadState?.unread_messages && createdAtTimestamp === lastReadTimestamp); - - const showUnreadSeparator = - isLastReadMessage && - !isNewestMessage && - // The `channelUnreadState?.first_unread_message_id` is here for sent messages unread label - (!!channelUnreadState?.first_unread_message_id || !!channelUnreadState?.unread_messages); - const showUnreadUnderlay = !!shouldShowUnreadUnderlay && showUnreadSeparator; - - const wrapMessageInTheme = client.userID === message.user?.id && !!myMessageTheme; - const renderDateSeperator = dateSeparatorsRef.current[message.id] && ( - - ); - - const renderMessage = ( - ); - - return ( - - {message.type === 'system' ? ( - - ) : wrapMessageInTheme ? ( - - - {renderDateSeperator} - {renderMessage} - - - ) : ( - - {renderDateSeperator} - {renderMessage} - - )} - {showUnreadUnderlay && } - - ); }, - [ - InlineDateSeparator, - InlineUnreadIndicator, - Message, - MessageSystem, - channel, - channelUnreadState?.first_unread_message_id, - channelUnreadState?.last_read, - channelUnreadState?.last_read_message_id, - channelUnreadState?.unread_messages, - client.userID, - dateSeparatorsRef, - goToMessage, - highlightedMessageId, - lastReceivedId, - messageContainer, - messageGroupStylesRef, - modifiedTheme, - myMessageTheme, - onThreadSelect, - screenPadding, - shouldShowUnreadUnderlay, - threadList, - ], + [dateSeparatorsRef, goToMessage, messageGroupStylesRef, modifiedTheme, onThreadSelect], ); /** @@ -1299,6 +1219,7 @@ export const MessageList = (props: MessageListProps) => { const { channel, channelUnreadState, + channelUnreadStateStore, disabled, EmptyStateIndicator, enableMessageGroupingByUser, @@ -1347,6 +1268,7 @@ export const MessageList = (props: MessageListProps) => { {...{ channel, channelUnreadState, + channelUnreadStateStore, client, closePicker, DateHeader, diff --git a/package/src/components/MessageList/UnreadMessagesNotification.tsx b/package/src/components/MessageList/UnreadMessagesNotification.tsx index bca539e3be..47192ee192 100644 --- a/package/src/components/MessageList/UnreadMessagesNotification.tsx +++ b/package/src/components/MessageList/UnreadMessagesNotification.tsx @@ -21,7 +21,7 @@ export const UnreadMessagesNotification = (props: UnreadMessagesNotificationProp const { onCloseHandler, onPressHandler } = props; const { t } = useTranslationContext(); const { - channelUnreadState, + channelUnreadStateStore, loadChannelAtFirstUnreadMessage, markRead, setChannelUnreadState, @@ -33,7 +33,7 @@ export const UnreadMessagesNotification = (props: UnreadMessagesNotificationProp await onPressHandler(); } else { await loadChannelAtFirstUnreadMessage({ - channelUnreadState, + channelUnreadState: channelUnreadStateStore.channelUnreadState, setChannelUnreadState, setTargetedMessage, }); diff --git a/package/src/components/MessageList/__tests__/MessageList.test.js b/package/src/components/MessageList/__tests__/MessageList.test.js index 116c36a57d..066d148c74 100644 --- a/package/src/components/MessageList/__tests__/MessageList.test.js +++ b/package/src/components/MessageList/__tests__/MessageList.test.js @@ -382,12 +382,19 @@ describe('MessageList', () => { }); }); - it("should render the UnreadMessagesIndicator when there's unread messages", async () => { + it("should render the InlineUnreadIndicator when there's unread messages", async () => { const user1 = generateUser(); const user2 = generateUser(); const messages = Array.from({ length: 10 }, (_, i) => generateMessage({ id: `${i}`, text: `message-${i}` }), ); + const read_data = { + [user1.id]: { + last_read: new Date(), + last_read_message_id: '5', + unread_messages: 5, + }, + }; const mockedChannel = generateChannelResponse({ members: [generateMember({ user: user1 }), generateMember({ user: user2 })], }); @@ -397,23 +404,18 @@ describe('MessageList', () => { const channel = chatClient.channel('messaging', mockedChannel.id); await channel.watch(); - const channelUnreadState = { - last_read: new Date(), - last_read_message_id: '5', - unread_messages: 5, - }; - channel.state = { ...channelInitialState, latestMessages: [], messages, + read: read_data, }; const { queryByLabelText } = render( - + , diff --git a/package/src/components/MessageList/hooks/useMessageList.ts b/package/src/components/MessageList/hooks/useMessageList.ts index 01969ce221..f01f4d8eda 100644 --- a/package/src/components/MessageList/hooks/useMessageList.ts +++ b/package/src/components/MessageList/hooks/useMessageList.ts @@ -23,6 +23,9 @@ export type UseMessageListParams = { isFlashList?: boolean; }; +/** + * FIXME: To change it to a more specific type. + */ export type GroupType = string; export type MessageGroupStyles = { diff --git a/package/src/components/MessageList/utils/getLastReceivedMessage.ts b/package/src/components/MessageList/utils/getLastReceivedMessage.ts index 2752950258..d53e15c7d8 100644 --- a/package/src/components/MessageList/utils/getLastReceivedMessage.ts +++ b/package/src/components/MessageList/utils/getLastReceivedMessage.ts @@ -7,10 +7,7 @@ export const getLastReceivedMessage = (messages: LocalMessage[]) => { * There are no status on dates so they will be skipped */ for (const message of messages) { - if ( - message?.status === MessageStatusTypes.RECEIVED || - message?.status === MessageStatusTypes.SENDING - ) { + if (message?.status !== MessageStatusTypes.FAILED) { return message; } } diff --git a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap index af67127f37..c39ef91687 100644 --- a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap +++ b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap @@ -232,10 +232,10 @@ exports[`Thread should match thread snapshot 1`] = ` } > >; + setChannelUnreadState?: (data: ChannelUnreadStateStoreType['channelUnreadState']) => void; setTargetedMessage?: (messageId: string) => void; }) => Promise; @@ -123,7 +127,7 @@ export type ChannelContextValue = { read: ChannelState['read']; reloadChannel: () => Promise; scrollToFirstUnreadThreshold: number; - setChannelUnreadState: React.Dispatch>; + setChannelUnreadState: (data: ChannelUnreadStateStoreType['channelUnreadState']) => void; setLastRead: React.Dispatch>; setTargetedMessage: (messageId?: string) => void; /** @@ -131,7 +135,12 @@ export type ChannelContextValue = { * Its a map of filename and AbortController */ uploadAbortControllerRef: React.MutableRefObject>; + /** + * Channel unread data + * @deprecated Use channelUnreadStateStore instead + */ channelUnreadState?: ChannelUnreadState; + channelUnreadStateStore: ChannelUnreadStateStore; disabled?: boolean; enableMessageGroupingByUser?: boolean; /** diff --git a/package/src/contexts/messageContext/MessageContext.tsx b/package/src/contexts/messageContext/MessageContext.tsx index 70c44865b3..45e7f0e51c 100644 --- a/package/src/contexts/messageContext/MessageContext.tsx +++ b/package/src/contexts/messageContext/MessageContext.tsx @@ -112,7 +112,10 @@ export type MessageContextValue = { * @returns */ handleReaction?: (reactionType: string) => Promise; - /** Latest message id on current channel */ + /** + * Latest message id on current channel + * @deprecated and will be removed in the future. This is pretty much accessible through the message-list itself. + */ lastReceivedId?: string; /** * Theme provided only to messages that are the current users diff --git a/package/src/state-store/channel-unread-state.ts b/package/src/state-store/channel-unread-state.ts new file mode 100644 index 0000000000..2e938dec5b --- /dev/null +++ b/package/src/state-store/channel-unread-state.ts @@ -0,0 +1,28 @@ +import { StateStore } from 'stream-chat'; + +import type { ChannelUnreadState as ChannelUnreadStateType } from '../types/types'; + +export type ChannelUnreadStateStoreType = { + channelUnreadState?: ChannelUnreadStateType; +}; + +const INITIAL_STATE: ChannelUnreadStateStoreType = { + channelUnreadState: undefined, +}; + +export class ChannelUnreadStateStore { + public state: StateStore; + + constructor() { + this.state = new StateStore(INITIAL_STATE); + } + + set channelUnreadState(data: ChannelUnreadStateStoreType['channelUnreadState']) { + this.state.next({ channelUnreadState: data }); + } + + get channelUnreadState() { + const { channelUnreadState } = this.state.getLatestValue(); + return channelUnreadState; + } +}