Skip to content

Commit a7f9dab

Browse files
authored
fix: performance improvement for message list render item (#3306)
1 parent dd374e2 commit a7f9dab

28 files changed

+890
-537
lines changed

package/src/components/Channel/Channel.tsx

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,11 @@ import {
106106
isImagePickerAvailable,
107107
NativeHandlers,
108108
} from '../../native';
109-
import { ChannelUnreadState, FileTypes } from '../../types/types';
109+
import {
110+
ChannelUnreadStateStore,
111+
ChannelUnreadStateStoreType,
112+
} from '../../state-store/channel-unread-state';
113+
import { FileTypes } from '../../types/types';
110114
import { addReactionToLocalState } from '../../utils/addReactionToLocalState';
111115
import { compressedImageURI } from '../../utils/compressImage';
112116
import { patchMessageTextCommand } from '../../utils/patchMessageTextCommand';
@@ -326,6 +330,7 @@ export type ChannelPropsWithContext = Pick<ChannelContextValue, 'channel'> &
326330
| 'forceAlignMessages'
327331
| 'Gallery'
328332
| 'getMessagesGroupStyles'
333+
| 'getMessageGroupStyle'
329334
| 'Giphy'
330335
| 'giphyVersion'
331336
| 'handleBan'
@@ -426,7 +431,7 @@ export type ChannelPropsWithContext = Pick<ChannelContextValue, 'channel'> &
426431
*/
427432
doMarkReadRequest?: (
428433
channel: ChannelType,
429-
setChannelUnreadUiState?: (state: ChannelUnreadState) => void,
434+
setChannelUnreadUiState?: (data: ChannelUnreadStateStoreType['channelUnreadState']) => void,
430435
) => void;
431436
/**
432437
* Overrides the Stream default send message request (Advanced usage only)
@@ -622,6 +627,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
622627
forceAlignMessages,
623628
Gallery = GalleryDefault,
624629
getMessagesGroupStyles,
630+
getMessageGroupStyle,
625631
Giphy = GiphyDefault,
626632
giphyVersion = 'fixed_height',
627633
handleAttachButtonPress,
@@ -778,10 +784,13 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
778784
const [thread, setThread] = useState<LocalMessage | null>(threadProps || null);
779785
const [threadHasMore, setThreadHasMore] = useState(true);
780786
const [threadLoadingMore, setThreadLoadingMore] = useState(false);
781-
const [channelUnreadState, setChannelUnreadState] = useState<ChannelUnreadState | undefined>(
782-
undefined,
787+
const [channelUnreadStateStore] = useState(new ChannelUnreadStateStore());
788+
const setChannelUnreadState = useCallback(
789+
(data: ChannelUnreadStateStoreType['channelUnreadState']) => {
790+
channelUnreadStateStore.channelUnreadState = data;
791+
},
792+
[channelUnreadStateStore],
783793
);
784-
785794
const { bottomSheetRef, closePicker, openPicker } = useAttachmentPickerBottomSheet();
786795

787796
const syncingChannelRef = useRef(false);
@@ -906,16 +915,14 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
906915
}
907916

908917
if (event.type === 'notification.mark_unread') {
909-
setChannelUnreadState((prev) => {
910-
if (!(event.last_read_at && event.user)) {
911-
return prev;
912-
}
913-
return {
914-
first_unread_message_id: event.first_unread_message_id,
915-
last_read: new Date(event.last_read_at),
916-
last_read_message_id: event.last_read_message_id,
917-
unread_messages: event.unread_messages ?? 0,
918-
};
918+
if (!(event.last_read_at && event.user)) {
919+
return;
920+
}
921+
setChannelUnreadState({
922+
first_unread_message_id: event.first_unread_message_id,
923+
last_read: new Date(event.last_read_at),
924+
last_read_message_id: event.last_read_message_id,
925+
unread_messages: event.unread_messages ?? 0,
919926
});
920927
}
921928

@@ -1773,7 +1780,8 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
17731780

17741781
const channelContext = useCreateChannelContext({
17751782
channel,
1776-
channelUnreadState,
1783+
channelUnreadState: channelUnreadStateStore.channelUnreadState,
1784+
channelUnreadStateStore,
17771785
disabled: !!channel?.data?.frozen,
17781786
EmptyStateIndicator,
17791787
enableMessageGroupingByUser,
@@ -1921,6 +1929,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
19211929
FlatList,
19221930
forceAlignMessages,
19231931
Gallery,
1932+
getMessageGroupStyle,
19241933
getMessagesGroupStyles,
19251934
Giphy,
19261935
giphyVersion,

package/src/components/Channel/hooks/useCreateChannelContext.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { ChannelContextValue } from '../../../contexts/channelContext/Chann
55
export const useCreateChannelContext = ({
66
channel,
77
channelUnreadState,
8+
channelUnreadStateStore,
89
disabled,
910
EmptyStateIndicator,
1011
enableMessageGroupingByUser,
@@ -46,12 +47,12 @@ export const useCreateChannelContext = ({
4647
const readUsersLastReads = readUsers
4748
.map(({ last_read }) => last_read?.toISOString() ?? '')
4849
.join();
49-
const stringifiedChannelUnreadState = JSON.stringify(channelUnreadState);
5050

5151
const channelContext: ChannelContextValue = useMemo(
5252
() => ({
5353
channel,
5454
channelUnreadState,
55+
channelUnreadStateStore,
5556
disabled,
5657
EmptyStateIndicator,
5758
enableMessageGroupingByUser,
@@ -96,7 +97,6 @@ export const useCreateChannelContext = ({
9697
membersLength,
9798
readUsersLength,
9899
readUsersLastReads,
99-
stringifiedChannelUnreadState,
100100
targetedMessage,
101101
threadList,
102102
watcherCount,

package/src/components/Channel/hooks/useCreateMessagesContext.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const useCreateMessagesContext = ({
2727
FlatList,
2828
forceAlignMessages,
2929
Gallery,
30+
getMessageGroupStyle,
3031
getMessagesGroupStyles,
3132
Giphy,
3233
giphyVersion,
@@ -146,6 +147,7 @@ export const useCreateMessagesContext = ({
146147
FlatList,
147148
forceAlignMessages,
148149
Gallery,
150+
getMessageGroupStyle,
149151
getMessagesGroupStyles,
150152
Giphy,
151153
giphyVersion,

package/src/components/Message/Message.tsx

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,6 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
253253
handleRetry,
254254
handleThreadReply,
255255
isTargetedMessage,
256-
lastReceivedId,
257256
members,
258257
message,
259258
messageActions: messageActionsProp = defaultMessageActions,
@@ -652,7 +651,6 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
652651
isMessageAIGenerated,
653652
isMyMessage,
654653
lastGroupMessage: groupStyles?.[0] === 'single' || groupStyles?.[0] === 'bottom',
655-
lastReceivedId,
656654
members,
657655
message,
658656
messageContentOrder,
@@ -789,7 +787,6 @@ const areEqual = (prevProps: MessagePropsWithContext, nextProps: MessagePropsWit
789787
groupStyles: prevGroupStyles,
790788
isAttachmentEqual,
791789
isTargetedMessage: prevIsTargetedMessage,
792-
lastReceivedId: prevLastReceivedId,
793790
members: prevMembers,
794791
message: prevMessage,
795792
messagesContext: prevMessagesContext,
@@ -803,7 +800,6 @@ const areEqual = (prevProps: MessagePropsWithContext, nextProps: MessagePropsWit
803800
goToMessage: nextGoToMessage,
804801
groupStyles: nextGroupStyles,
805802
isTargetedMessage: nextIsTargetedMessage,
806-
lastReceivedId: nextLastReceivedId,
807803
members: nextMembers,
808804
message: nextMessage,
809805
messagesContext: nextMessagesContext,
@@ -832,17 +828,6 @@ const areEqual = (prevProps: MessagePropsWithContext, nextProps: MessagePropsWit
832828
return false;
833829
}
834830

835-
const lastReceivedIdChangedAndMatters =
836-
prevLastReceivedId !== nextLastReceivedId &&
837-
(prevLastReceivedId === prevMessage.id ||
838-
prevLastReceivedId === nextMessage.id ||
839-
nextLastReceivedId === prevMessage.id ||
840-
nextLastReceivedId === nextMessage.id);
841-
842-
if (lastReceivedIdChangedAndMatters) {
843-
return false;
844-
}
845-
846831
const goToMessageChangedAndMatters =
847832
nextMessage.quoted_message_id && prevGoToMessage !== nextGoToMessage;
848833

package/src/components/Message/MessageSimple/MessageContent.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,6 @@ export const MessageContent = (props: MessageContentProps) => {
535535
isEditedMessageOpen,
536536
isMessageAIGenerated,
537537
isMyMessage,
538-
lastReceivedId,
539538
message,
540539
messageContentOrder,
541540
onLongPress,
@@ -575,7 +574,6 @@ export const MessageContent = (props: MessageContentProps) => {
575574
isEditedMessageOpen,
576575
isMessageAIGenerated,
577576
isMyMessage,
578-
lastReceivedId,
579577
message,
580578
messageContentOrder,
581579
MessageError,
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import React, { useCallback } from 'react';
2+
3+
import { View } from 'react-native';
4+
5+
import { LocalMessage } from 'stream-chat';
6+
7+
import { useMessageDateSeparator } from '../../../components/MessageList/hooks/useMessageDateSeparator';
8+
import { useMessageGroupStyles } from '../../../components/MessageList/hooks/useMessageGroupStyles';
9+
import { useChannelContext } from '../../../contexts/channelContext/ChannelContext';
10+
import { useChatContext } from '../../../contexts/chatContext/ChatContext';
11+
import { useMessageListItemContext } from '../../../contexts/messageListItemContext/MessageListItemContext';
12+
import { useMessagesContext } from '../../../contexts/messagesContext/MessagesContext';
13+
import { ThemeProvider, useTheme } from '../../../contexts/themeContext/ThemeContext';
14+
15+
import { useStateStore } from '../../../hooks/useStateStore';
16+
import { ChannelUnreadStateStoreType } from '../../../state-store/channel-unread-state';
17+
import { MessagePreviousAndNextMessageStoreType } from '../../../state-store/message-list-prev-next-state';
18+
19+
const channelUnreadStateSelector = (state: ChannelUnreadStateStoreType) => ({
20+
first_unread_message_id: state.channelUnreadState?.first_unread_message_id,
21+
last_read_message_id: state.channelUnreadState?.last_read_message_id,
22+
last_read_timestamp: state.channelUnreadState?.last_read?.getTime(),
23+
unread_messages: state.channelUnreadState?.unread_messages,
24+
});
25+
26+
export type MessageWrapperProps = {
27+
message: LocalMessage;
28+
};
29+
30+
export const MessageWrapper = React.memo((props: MessageWrapperProps) => {
31+
const { message } = props;
32+
const { client } = useChatContext();
33+
const {
34+
channelUnreadStateStore,
35+
channel,
36+
hideDateSeparators,
37+
highlightedMessageId,
38+
maxTimeBetweenGroupedMessages,
39+
threadList,
40+
} = useChannelContext();
41+
const {
42+
getMessageGroupStyle,
43+
InlineDateSeparator,
44+
InlineUnreadIndicator,
45+
Message,
46+
MessageSystem,
47+
myMessageTheme,
48+
shouldShowUnreadUnderlay,
49+
} = useMessagesContext();
50+
const {
51+
goToMessage,
52+
onThreadSelect,
53+
noGroupByUser,
54+
modifiedTheme,
55+
messageListPreviousAndNextMessageStore,
56+
} = useMessageListItemContext();
57+
58+
const dateSeparatorDate = useMessageDateSeparator({
59+
hideDateSeparators,
60+
message,
61+
messageListPreviousAndNextMessageStore,
62+
});
63+
64+
const selector = useCallback(
65+
(state: MessagePreviousAndNextMessageStoreType) => ({
66+
nextMessage: state.messageList[message.id]?.nextMessage,
67+
}),
68+
[message.id],
69+
);
70+
const { nextMessage } = useStateStore(messageListPreviousAndNextMessageStore.state, selector);
71+
const isNewestMessage = nextMessage === undefined;
72+
const groupStyles = useMessageGroupStyles({
73+
dateSeparatorDate,
74+
getMessageGroupStyle,
75+
maxTimeBetweenGroupedMessages,
76+
message,
77+
messageListPreviousAndNextMessageStore,
78+
noGroupByUser,
79+
});
80+
81+
const { first_unread_message_id, last_read_timestamp, last_read_message_id, unread_messages } =
82+
useStateStore(channelUnreadStateStore.state, channelUnreadStateSelector);
83+
const {
84+
theme: {
85+
messageList: { messageContainer },
86+
screenPadding,
87+
},
88+
} = useTheme();
89+
if (!channel || channel.disconnected) {
90+
return null;
91+
}
92+
93+
const createdAtTimestamp = message.created_at && new Date(message.created_at).getTime();
94+
const isLastReadMessage =
95+
last_read_message_id === message.id ||
96+
(!unread_messages && createdAtTimestamp === last_read_timestamp);
97+
98+
const showUnreadSeparator =
99+
isLastReadMessage &&
100+
!isNewestMessage &&
101+
// The `channelUnreadState?.first_unread_message_id` is here for sent messages unread label
102+
(!!first_unread_message_id || !!unread_messages);
103+
104+
const showUnreadUnderlay = !!shouldShowUnreadUnderlay && showUnreadSeparator;
105+
106+
const wrapMessageInTheme = client.userID === message.user?.id && !!myMessageTheme;
107+
const renderDateSeperator = dateSeparatorDate ? (
108+
<InlineDateSeparator date={dateSeparatorDate} />
109+
) : null;
110+
111+
const renderMessage = (
112+
<Message
113+
goToMessage={goToMessage}
114+
groupStyles={groupStyles}
115+
isTargetedMessage={highlightedMessageId === message.id}
116+
message={message}
117+
onThreadSelect={onThreadSelect}
118+
showUnreadUnderlay={showUnreadUnderlay}
119+
style={[messageContainer]}
120+
threadList={threadList}
121+
/>
122+
);
123+
124+
return (
125+
<View testID={`message-list-item-${message.id}`}>
126+
{message.type === 'system' ? (
127+
<MessageSystem
128+
message={message}
129+
style={[{ paddingHorizontal: screenPadding }, messageContainer]}
130+
/>
131+
) : wrapMessageInTheme ? (
132+
<ThemeProvider mergedStyle={modifiedTheme}>
133+
<View testID={`message-list-item-${message.id}`}>
134+
{renderDateSeperator}
135+
{renderMessage}
136+
</View>
137+
</ThemeProvider>
138+
) : (
139+
<View testID={`message-list-item-${message.id}`}>
140+
{renderDateSeperator}
141+
{renderMessage}
142+
</View>
143+
)}
144+
{showUnreadUnderlay && <InlineUnreadIndicator />}
145+
</View>
146+
);
147+
});

package/src/components/Message/MessageSimple/__tests__/MessageStatus.test.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,7 @@ describe('MessageStatus', () => {
8181
<ChannelsStateProvider>
8282
<Chat client={chatClient} i18nInstance={i18nInstance}>
8383
<Channel channel={channel}>
84-
<MessageStatus
85-
lastReceivedId={staticMessage.id}
86-
message={staticMessage}
87-
readBy={readBy}
88-
/>
84+
<MessageStatus message={staticMessage} readBy={readBy} />
8985
</Channel>
9086
</Chat>
9187
</ChannelsStateProvider>,

package/src/components/Message/hooks/useCreateMessageContext.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ export const useCreateMessageContext = ({
2222
isMessageAIGenerated,
2323
isMyMessage,
2424
lastGroupMessage,
25-
lastReceivedId,
2625
members,
2726
message,
2827
messageContentOrder,
@@ -74,7 +73,6 @@ export const useCreateMessageContext = ({
7473
isMessageAIGenerated,
7574
isMyMessage,
7675
lastGroupMessage,
77-
lastReceivedId,
7876
members,
7977
message,
8078
messageContentOrder,
@@ -105,7 +103,6 @@ export const useCreateMessageContext = ({
105103
hasReactions,
106104
isEditedMessageOpen,
107105
lastGroupMessage,
108-
lastReceivedId,
109106
membersValue,
110107
myMessageThemeString,
111108
reactionsValue,

0 commit comments

Comments
 (0)