Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7f0fe8a
Merge branch 'develop' of github.com:GetStream/stream-chat-react-nati…
khushal87 Nov 27, 2025
7ce0e69
fix: performance improvement for message list render item
khushal87 Nov 28, 2025
2300afc
fix: performance improvement for message list render item
khushal87 Nov 28, 2025
70ed772
fix: date separator and group styles extraction to store
khushal87 Dec 4, 2025
fc3a839
fix: date separator and group styles extraction to store
khushal87 Dec 4, 2025
bdb763a
Merge branch 'develop' of github.com:GetStream/stream-chat-react-nati…
khushal87 Dec 4, 2025
9f7638e
test: update snapshots
khushal87 Dec 4, 2025
fc728a3
Merge branch 'develop' of github.com:GetStream/stream-chat-react-nati…
khushal87 Dec 4, 2025
bd6b659
fix: remove redundant prop from useMessageGroupStyles
khushal87 Dec 5, 2025
5248e2b
fix: isNewest prop rerender
khushal87 Dec 5, 2025
e1f3e4c
fix: optimize the message list prev and next state
khushal87 Dec 9, 2025
cc6ab46
fix: optimize the message list prev and next state
khushal87 Dec 9, 2025
6b7715e
fix: optimize the message list store usage
khushal87 Dec 9, 2025
653d4ef
Merge branch 'develop' of github.com:GetStream/stream-chat-react-nati…
khushal87 Dec 10, 2025
023d1b2
fix: optimize MessageWrapper
khushal87 Dec 10, 2025
0e05faa
fix: usage of state store in list
khushal87 Dec 11, 2025
9f9cfae
fix: rely on processedMessageList for calculations
khushal87 Dec 12, 2025
cad8417
fix: rely on processedMessageList for calculations
khushal87 Dec 12, 2025
a5b5fbc
Merge branch 'develop' into perf/message-list-render-item
isekovanic Dec 17, 2025
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
41 changes: 25 additions & 16 deletions package/src/components/Channel/Channel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,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';
Expand Down Expand Up @@ -325,6 +329,7 @@ export type ChannelPropsWithContext = Pick<ChannelContextValue, 'channel'> &
| 'forceAlignMessages'
| 'Gallery'
| 'getMessagesGroupStyles'
| 'getMessageGroupStyle'
| 'Giphy'
| 'giphyVersion'
| 'handleBan'
Expand Down Expand Up @@ -424,7 +429,7 @@ export type ChannelPropsWithContext = Pick<ChannelContextValue, 'channel'> &
*/
doMarkReadRequest?: (
channel: ChannelType,
setChannelUnreadUiState?: (state: ChannelUnreadState) => void,
setChannelUnreadUiState?: (data: ChannelUnreadStateStoreType['channelUnreadState']) => void,
) => void;
/**
* Overrides the Stream default send message request (Advanced usage only)
Expand Down Expand Up @@ -620,6 +625,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
forceAlignMessages,
Gallery = GalleryDefault,
getMessagesGroupStyles,
getMessageGroupStyle,
Giphy = GiphyDefault,
giphyVersion = 'fixed_height',
handleAttachButtonPress,
Expand Down Expand Up @@ -775,10 +781,13 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
const [thread, setThread] = useState<LocalMessage | null>(threadProps || null);
const [threadHasMore, setThreadHasMore] = useState(true);
const [threadLoadingMore, setThreadLoadingMore] = useState(false);
const [channelUnreadState, setChannelUnreadState] = useState<ChannelUnreadState | undefined>(
undefined,
const [channelUnreadStateStore] = useState(new ChannelUnreadStateStore());
const setChannelUnreadState = useCallback(
(data: ChannelUnreadStateStoreType['channelUnreadState']) => {
channelUnreadStateStore.channelUnreadState = data;
},
[channelUnreadStateStore],
);

const { bottomSheetRef, closePicker, openPicker } = useAttachmentPickerBottomSheet();

const syncingChannelRef = useRef(false);
Expand Down Expand Up @@ -903,16 +912,14 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
}

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,
});
}

Expand Down Expand Up @@ -1768,7 +1775,8 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =

const channelContext = useCreateChannelContext({
channel,
channelUnreadState,
channelUnreadState: channelUnreadStateStore.channelUnreadState,
channelUnreadStateStore,
disabled: !!channel?.data?.frozen,
EmptyStateIndicator,
enableMessageGroupingByUser,
Expand Down Expand Up @@ -1916,6 +1924,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
FlatList,
forceAlignMessages,
Gallery,
getMessageGroupStyle,
getMessagesGroupStyles,
Giphy,
giphyVersion,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { ChannelContextValue } from '../../../contexts/channelContext/Chann
export const useCreateChannelContext = ({
channel,
channelUnreadState,
channelUnreadStateStore,
disabled,
EmptyStateIndicator,
enableMessageGroupingByUser,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -96,7 +97,6 @@ export const useCreateChannelContext = ({
membersLength,
readUsersLength,
readUsersLastReads,
stringifiedChannelUnreadState,
targetedMessage,
threadList,
watcherCount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const useCreateMessagesContext = ({
FlatList,
forceAlignMessages,
Gallery,
getMessageGroupStyle,
getMessagesGroupStyles,
Giphy,
giphyVersion,
Expand Down Expand Up @@ -145,6 +146,7 @@ export const useCreateMessagesContext = ({
FlatList,
forceAlignMessages,
Gallery,
getMessageGroupStyle,
getMessagesGroupStyles,
Giphy,
giphyVersion,
Expand Down
15 changes: 0 additions & 15 deletions package/src/components/Message/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,6 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
handleRetry,
handleThreadReply,
isTargetedMessage,
lastReceivedId,
members,
message,
messageActions: messageActionsProp = defaultMessageActions,
Expand Down Expand Up @@ -650,7 +649,6 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
isMessageAIGenerated,
isMyMessage,
lastGroupMessage: groupStyles?.[0] === 'single' || groupStyles?.[0] === 'bottom',
lastReceivedId,
members,
message,
messageContentOrder,
Expand Down Expand Up @@ -783,7 +781,6 @@ const areEqual = (prevProps: MessagePropsWithContext, nextProps: MessagePropsWit
groupStyles: prevGroupStyles,
isAttachmentEqual,
isTargetedMessage: prevIsTargetedMessage,
lastReceivedId: prevLastReceivedId,
members: prevMembers,
message: prevMessage,
messagesContext: prevMessagesContext,
Expand All @@ -797,7 +794,6 @@ const areEqual = (prevProps: MessagePropsWithContext, nextProps: MessagePropsWit
goToMessage: nextGoToMessage,
groupStyles: nextGroupStyles,
isTargetedMessage: nextIsTargetedMessage,
lastReceivedId: nextLastReceivedId,
members: nextMembers,
message: nextMessage,
messagesContext: nextMessagesContext,
Expand Down Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,6 @@ export const MessageContent = (props: MessageContentProps) => {
isEditedMessageOpen,
isMessageAIGenerated,
isMyMessage,
lastReceivedId,
message,
messageContentOrder,
onLongPress,
Expand Down Expand Up @@ -575,7 +574,6 @@ export const MessageContent = (props: MessageContentProps) => {
isEditedMessageOpen,
isMessageAIGenerated,
isMyMessage,
lastReceivedId,
message,
messageContentOrder,
MessageError,
Expand Down
147 changes: 147 additions & 0 deletions package/src/components/Message/MessageSimple/MessageWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import React, { useCallback } from 'react';

import { View } from 'react-native';

import { LocalMessage } from 'stream-chat';

import { useMessageDateSeparator } from '../../../components/MessageList/hooks/useMessageDateSeparator';
import { useMessageGroupStyles } from '../../../components/MessageList/hooks/useMessageGroupStyles';
import { useChannelContext } from '../../../contexts/channelContext/ChannelContext';
import { useChatContext } from '../../../contexts/chatContext/ChatContext';
import { useMessageListItemContext } from '../../../contexts/messageListItemContext/MessageListItemContext';
import { useMessagesContext } from '../../../contexts/messagesContext/MessagesContext';
import { ThemeProvider, useTheme } from '../../../contexts/themeContext/ThemeContext';

import { useStateStore } from '../../../hooks/useStateStore';
import { ChannelUnreadStateStoreType } from '../../../state-store/channel-unread-state';
import { MessagePreviousAndNextMessageStoreType } from '../../../state-store/message-list-prev-next-state';

const channelUnreadStateSelector = (state: ChannelUnreadStateStoreType) => ({
first_unread_message_id: state.channelUnreadState?.first_unread_message_id,
last_read_message_id: state.channelUnreadState?.last_read_message_id,
last_read_timestamp: state.channelUnreadState?.last_read?.getTime(),
unread_messages: state.channelUnreadState?.unread_messages,
});

export type MessageWrapperProps = {
message: LocalMessage;
};

export const MessageWrapper = React.memo((props: MessageWrapperProps) => {
const { message } = props;
const { client } = useChatContext();
const {
channelUnreadStateStore,
channel,
hideDateSeparators,
highlightedMessageId,
maxTimeBetweenGroupedMessages,
threadList,
} = useChannelContext();
const {
getMessageGroupStyle,
InlineDateSeparator,
InlineUnreadIndicator,
Message,
MessageSystem,
myMessageTheme,
shouldShowUnreadUnderlay,
} = useMessagesContext();
const {
goToMessage,
onThreadSelect,
noGroupByUser,
modifiedTheme,
messageListPreviousAndNextMessageStore,
} = useMessageListItemContext();

const dateSeparatorDate = useMessageDateSeparator({
hideDateSeparators,
message,
messageListPreviousAndNextMessageStore,
});

const selector = useCallback(
(state: MessagePreviousAndNextMessageStoreType) => ({
nextMessage: state.messageList[message.id]?.nextMessage,
}),
[message.id],
);
const { nextMessage } = useStateStore(messageListPreviousAndNextMessageStore.state, selector);
const isNewestMessage = nextMessage === undefined;
const groupStyles = useMessageGroupStyles({
dateSeparatorDate,
getMessageGroupStyle,
maxTimeBetweenGroupedMessages,
message,
messageListPreviousAndNextMessageStore,
noGroupByUser,
});

const { first_unread_message_id, last_read_timestamp, 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 isLastReadMessage =
last_read_message_id === message.id ||
(!unread_messages && createdAtTimestamp === last_read_timestamp);

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 ? (
<InlineDateSeparator date={dateSeparatorDate} />
) : null;

const renderMessage = (
<Message
goToMessage={goToMessage}
groupStyles={groupStyles}
isTargetedMessage={highlightedMessageId === message.id}
message={message}
onThreadSelect={onThreadSelect}
showUnreadUnderlay={showUnreadUnderlay}
style={[messageContainer]}
threadList={threadList}
/>
);

return (
<View testID={`message-list-item-${message.id}`}>
{message.type === 'system' ? (
<MessageSystem
message={message}
style={[{ paddingHorizontal: screenPadding }, messageContainer]}
/>
) : wrapMessageInTheme ? (
<ThemeProvider mergedStyle={modifiedTheme}>
<View testID={`message-list-item-${message.id}`}>
{renderDateSeperator}
{renderMessage}
</View>
</ThemeProvider>
) : (
<View testID={`message-list-item-${message.id}`}>
{renderDateSeperator}
{renderMessage}
</View>
)}
{showUnreadUnderlay && <InlineUnreadIndicator />}
</View>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,7 @@ describe('MessageStatus', () => {
<ChannelsStateProvider>
<Chat client={chatClient} i18nInstance={i18nInstance}>
<Channel channel={channel}>
<MessageStatus
lastReceivedId={staticMessage.id}
message={staticMessage}
readBy={readBy}
/>
<MessageStatus message={staticMessage} readBy={readBy} />
</Channel>
</Chat>
</ChannelsStateProvider>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export const useCreateMessageContext = ({
isMessageAIGenerated,
isMyMessage,
lastGroupMessage,
lastReceivedId,
members,
message,
messageContentOrder,
Expand Down Expand Up @@ -74,7 +73,6 @@ export const useCreateMessageContext = ({
isMessageAIGenerated,
isMyMessage,
lastGroupMessage,
lastReceivedId,
members,
message,
messageContentOrder,
Expand Down Expand Up @@ -105,7 +103,6 @@ export const useCreateMessageContext = ({
hasReactions,
isEditedMessageOpen,
lastGroupMessage,
lastReceivedId,
membersValue,
myMessageThemeString,
reactionsValue,
Expand Down
Loading
Loading