Skip to content

Commit 2300afc

Browse files
committed
fix: performance improvement for message list render item
2 parents 7ce0e69 + d4d8f75 commit 2300afc

File tree

7 files changed

+136
-60
lines changed

7 files changed

+136
-60
lines changed

examples/SampleApp/src/components/MessageInfoBottomSheet.tsx

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import React, { useMemo } from 'react';
2-
import BottomSheet, { BottomSheetFlatList } from '@gorhom/bottom-sheet';
3-
import { BottomSheetView } from '@gorhom/bottom-sheet';
42
import {
53
Avatar,
4+
BottomSheetModal,
65
useChatContext,
76
useMessageDeliveredData,
87
useMessageReadData,
98
useTheme,
109
} from 'stream-chat-react-native';
1110
import { LocalMessage, UserResponse } from 'stream-chat';
12-
import { StyleSheet, Text, View } from 'react-native';
11+
import { FlatList, StyleSheet, Text, View } from 'react-native';
1312

1413
const renderUserItem = ({ item }: { item: UserResponse }) => (
1514
<View style={styles.userItem}>
@@ -24,10 +23,12 @@ const renderEmptyText = ({ text }: { text: string }) => (
2423

2524
export const MessageInfoBottomSheet = ({
2625
message,
27-
ref,
26+
visible,
27+
onClose,
2828
}: {
2929
message?: LocalMessage;
30-
ref: React.RefObject<BottomSheet | null>;
30+
visible: boolean;
31+
onClose: () => void;
3132
}) => {
3233
const {
3334
theme: { colors },
@@ -45,26 +46,26 @@ export const MessageInfoBottomSheet = ({
4546
}, [readStatus, client?.user?.id]);
4647

4748
return (
48-
<BottomSheet enablePanDownToClose ref={ref} index={-1} snapPoints={['50%']}>
49-
<BottomSheetView style={[styles.container, { backgroundColor: colors.white_smoke }]}>
49+
<BottomSheetModal visible={visible} onClose={onClose}>
50+
<View style={[styles.container, { backgroundColor: colors.white_smoke }]}>
5051
<Text style={styles.title}>Read</Text>
51-
<BottomSheetFlatList
52+
<FlatList
5253
data={otherReadUsers}
5354
renderItem={renderUserItem}
5455
keyExtractor={(item) => item.id}
5556
style={styles.flatList}
5657
ListEmptyComponent={renderEmptyText({ text: 'No one has read this message.' })}
5758
/>
5859
<Text style={styles.title}>Delivered</Text>
59-
<BottomSheetFlatList
60+
<FlatList
6061
data={otherDeliveredToUsers}
6162
renderItem={renderUserItem}
6263
keyExtractor={(item) => item.id}
6364
style={styles.flatList}
6465
ListEmptyComponent={renderEmptyText({ text: 'The message was not delivered to anyone.' })}
6566
/>
66-
</BottomSheetView>
67-
</BottomSheet>
67+
</View>
68+
</BottomSheetModal>
6869
);
6970
};
7071

examples/SampleApp/src/screens/ChannelScreen.tsx

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback, useEffect, useRef, useState } from 'react';
1+
import React, { useCallback, useEffect, useState } from 'react';
22
import type { LocalMessage, Channel as StreamChatChannel } from 'stream-chat';
33
import { RouteProp, useFocusEffect, useNavigation } from '@react-navigation/native';
44
import {
@@ -33,7 +33,6 @@ import { channelMessageActions } from '../utils/messageActions.tsx';
3333
import { MessageLocation } from '../components/LocationSharing/MessageLocation.tsx';
3434
import { useStreamChatContext } from '../context/StreamChatContext.tsx';
3535
import { CustomAttachmentPickerSelectionBar } from '../components/AttachmentPickerSelectionBar.tsx';
36-
import BottomSheet from '@gorhom/bottom-sheet';
3736
import { MessageInfoBottomSheet } from '../components/MessageInfoBottomSheet.tsx';
3837

3938
export type ChannelScreenNavigationProp = NativeStackNavigationProp<
@@ -130,6 +129,7 @@ export const ChannelScreen: React.FC<ChannelScreenProps> = ({
130129
} = useTheme();
131130
const { t } = useTranslationContext();
132131
const { setThread } = useStreamChatContext();
132+
const [modalVisible, setModalVisible] = useState(false);
133133
const [selectedMessage, setSelectedMessage] = useState<LocalMessage | undefined>(undefined);
134134

135135
const [channel, setChannel] = useState<StreamChatChannel | undefined>(channelFromProp);
@@ -186,15 +186,14 @@ export const ChannelScreen: React.FC<ChannelScreenProps> = ({
186186
[channel, navigation, setThread],
187187
);
188188

189-
const messageInfoBottomSheetRef = useRef<BottomSheet>(null);
189+
const handleMessageInfo = useCallback((message: LocalMessage) => {
190+
setSelectedMessage(message);
191+
setModalVisible(true);
192+
}, []);
190193

191-
const handleMessageInfo = useCallback(
192-
(message: LocalMessage) => {
193-
setSelectedMessage(message);
194-
messageInfoBottomSheetRef.current?.snapToIndex(1);
195-
},
196-
[messageInfoBottomSheetRef],
197-
);
194+
const handleMessageInfoClose = useCallback(() => {
195+
setModalVisible(false);
196+
}, []);
198197

199198
const messageActions = useCallback(
200199
(params: MessageActionsParams) => {
@@ -249,7 +248,13 @@ export const ChannelScreen: React.FC<ChannelScreenProps> = ({
249248
)}
250249
<AITypingIndicatorView channel={channel} />
251250
<MessageInput />
252-
<MessageInfoBottomSheet message={selectedMessage} ref={messageInfoBottomSheetRef} />
251+
{modalVisible && (
252+
<MessageInfoBottomSheet
253+
visible={modalVisible}
254+
message={selectedMessage}
255+
onClose={handleMessageInfoClose}
256+
/>
257+
)}
253258
</Channel>
254259
</View>
255260
);

package/src/components/Channel/Channel.tsx

Lines changed: 83 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,6 @@ import {
106106
ChannelUnreadStateStore,
107107
ChannelUnreadStateStoreType,
108108
} from '../../state-store/channel-unread-state';
109-
import * as dbApi from '../../store/apis';
110109
import { FileTypes } from '../../types/types';
111110
import { addReactionToLocalState } from '../../utils/addReactionToLocalState';
112111
import { compressedImageURI } from '../../utils/compressImage';
@@ -437,6 +436,20 @@ export type ChannelPropsWithContext = Pick<ChannelContextValue, 'channel'> &
437436
messageData: StreamMessage,
438437
options?: SendMessageOptions,
439438
) => Promise<SendMessageAPIResponse>;
439+
440+
/**
441+
* A method invoked just after the first optimistic update of a new message,
442+
* but before any other HTTP requests happen. Can be used to do extra work
443+
* (such as creating a channel, or editing a message) before the local message
444+
* is sent.
445+
* @param channelId
446+
* @param messageData Message object
447+
*/
448+
preSendMessageRequest?: (options: {
449+
localMessage: LocalMessage;
450+
message: StreamMessage;
451+
options?: SendMessageOptions;
452+
}) => Promise<SendMessageAPIResponse>;
440453
/**
441454
* Overrides the Stream default update message request (Advanced usage only)
442455
* @param channelId
@@ -496,10 +509,24 @@ export type ChannelPropsWithContext = Pick<ChannelContextValue, 'channel'> &
496509
* Tells if channel is rendering a thread list
497510
*/
498511
threadList?: boolean;
512+
/**
513+
* A boolean signifying whether the Channel component should run channel.watch()
514+
* whenever it mounts up a new channel. If set to `false`, it is the integrator's
515+
* responsibility to run channel.watch() if they wish to receive WebSocket events
516+
* for that channel.
517+
*
518+
* Can be particularly useful whenever we are viewing channels in a read-only mode
519+
* or perhaps want them in an ephemeral state (i.e not created until the first message
520+
* is sent).
521+
*/
522+
initializeOnMount?: boolean;
499523
} & Partial<
500524
Pick<
501525
InputMessageInputContextValue,
502-
'openPollCreationDialog' | 'CreatePollContent' | 'StopMessageStreamingButton'
526+
| 'openPollCreationDialog'
527+
| 'CreatePollContent'
528+
| 'StopMessageStreamingButton'
529+
| 'allowSendBeforeAttachmentsUpload'
503530
>
504531
>;
505532

@@ -571,10 +598,12 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
571598
doFileUploadRequest,
572599
doMarkReadRequest,
573600
doSendMessageRequest,
601+
preSendMessageRequest,
574602
doUpdateMessageRequest,
575603
EmptyStateIndicator = EmptyStateIndicatorDefault,
576604
enableMessageGroupingByUser = true,
577605
enableOfflineSupport,
606+
allowSendBeforeAttachmentsUpload = enableOfflineSupport,
578607
enableSwipeToReply = true,
579608
enforceUniqueReaction = false,
580609
FileAttachment = FileAttachmentDefault,
@@ -719,6 +748,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
719748
VideoThumbnail = VideoThumbnailDefault,
720749
isOnline,
721750
maximumMessageLimit,
751+
initializeOnMount = true,
722752
} = props;
723753

724754
const { thread: threadProps, threadInstance } = threadFromProps;
@@ -886,7 +916,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
886916
}
887917

888918
// only update channel state if the events are not the previously subscribed useEffect's subscription events
889-
if (channel && channel.initialized) {
919+
if (channel) {
890920
// we skip the new message events if we've already done an optimistic update for the new message
891921
if (event.type === 'message.new' || event.type === 'notification.message_new') {
892922
const messageId = event.message?.id ?? '';
@@ -920,13 +950,14 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
920950
}
921951
let errored = false;
922952

923-
if (!channel.initialized || !channel.state.isUpToDate) {
953+
if ((!channel.initialized || !channel.state.isUpToDate) && initializeOnMount) {
924954
try {
925955
await channel?.watch();
926956
} catch (err) {
927957
console.warn('Channel watch request failed with error:', err);
928958
setError(true);
929959
errored = true;
960+
channel.offlineMode = true;
930961
}
931962
}
932963

@@ -1083,7 +1114,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
10831114
});
10841115

10851116
const resyncChannel = useStableCallback(async () => {
1086-
if (!channel || syncingChannelRef.current) {
1117+
if (!channel || syncingChannelRef.current || (!channel.initialized && !channel.offlineMode)) {
10871118
return;
10881119
}
10891120
syncingChannelRef.current = true;
@@ -1104,6 +1135,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
11041135
limit: channelMessagesState.messages.length + 30,
11051136
},
11061137
});
1138+
channel.offlineMode = false;
11071139
}
11081140

11091141
if (!thread) {
@@ -1305,9 +1337,13 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
13051337
attachment.image_url = uploadResponse.file;
13061338
delete attachment.originalFile;
13071339

1308-
await dbApi.updateMessage({
1309-
message: { ...updatedMessage, cid: channel.cid },
1310-
});
1340+
client.offlineDb?.executeQuerySafely(
1341+
(db) =>
1342+
db.updateMessage({
1343+
message: { ...updatedMessage, cid: channel.cid },
1344+
}),
1345+
{ method: 'updateMessage' },
1346+
);
13111347
}
13121348

13131349
if (attachment.type !== FileTypes.Image && file?.uri) {
@@ -1326,9 +1362,13 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
13261362
}
13271363

13281364
delete attachment.originalFile;
1329-
await dbApi.updateMessage({
1330-
message: { ...updatedMessage, cid: channel.cid },
1331-
});
1365+
client.offlineDb?.executeQuerySafely(
1366+
(db) =>
1367+
db.updateMessage({
1368+
message: { ...updatedMessage, cid: channel.cid },
1369+
}),
1370+
{ method: 'updateMessage' },
1371+
);
13321372
}
13331373
}
13341374
}
@@ -1349,7 +1389,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
13491389
retrying?: boolean;
13501390
}) => {
13511391
let failedMessageUpdated = false;
1352-
const handleFailedMessage = async () => {
1392+
const handleFailedMessage = () => {
13531393
if (!failedMessageUpdated) {
13541394
const updatedMessage = {
13551395
...localMessage,
@@ -1360,11 +1400,13 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
13601400
threadInstance?.upsertReplyLocally?.({ message: updatedMessage });
13611401
optimisticallyUpdatedNewMessages.delete(localMessage.id);
13621402

1363-
if (enableOfflineSupport) {
1364-
await dbApi.updateMessage({
1365-
message: updatedMessage,
1366-
});
1367-
}
1403+
client.offlineDb?.executeQuerySafely(
1404+
(db) =>
1405+
db.updateMessage({
1406+
message: updatedMessage,
1407+
}),
1408+
{ method: 'updateMessage' },
1409+
);
13681410

13691411
failedMessageUpdated = true;
13701412
}
@@ -1402,11 +1444,14 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
14021444
status: MessageStatusTypes.RECEIVED,
14031445
};
14041446

1405-
if (enableOfflineSupport) {
1406-
await dbApi.updateMessage({
1407-
message: { ...newMessageResponse, cid: channel.cid },
1408-
});
1409-
}
1447+
client.offlineDb?.executeQuerySafely(
1448+
(db) =>
1449+
db.updateMessage({
1450+
message: { ...newMessageResponse, cid: channel.cid },
1451+
}),
1452+
{ method: 'updateMessage' },
1453+
);
1454+
14101455
if (retrying) {
14111456
replaceMessage(localMessage, newMessageResponse);
14121457
} else {
@@ -1430,16 +1475,22 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
14301475
threadInstance?.upsertReplyLocally?.({ message: localMessage });
14311476
optimisticallyUpdatedNewMessages.add(localMessage.id);
14321477

1433-
if (enableOfflineSupport) {
1434-
// While sending a message, we add the message to local db with failed status, so that
1435-
// if app gets closed before message gets sent and next time user opens the app
1436-
// then user can see that message in failed state and can retry.
1437-
// If succesfull, it will be updated with received status.
1438-
await dbApi.upsertMessages({
1439-
messages: [{ ...localMessage, cid: channel.cid, status: MessageStatusTypes.FAILED }],
1440-
});
1441-
}
1478+
// While sending a message, we add the message to local db with failed status, so that
1479+
// if app gets closed before message gets sent and next time user opens the app
1480+
// then user can see that message in failed state and can retry.
1481+
// If succesfull, it will be updated with received status.
1482+
client.offlineDb?.executeQuerySafely(
1483+
(db) =>
1484+
db.upsertMessages({
1485+
// @ts-ignore
1486+
messages: [{ ...localMessage, cid: channel.cid, status: MessageStatusTypes.FAILED }],
1487+
}),
1488+
{ method: 'upsertMessages' },
1489+
);
14421490

1491+
if (preSendMessageRequest) {
1492+
await preSendMessageRequest({ localMessage, message, options });
1493+
}
14431494
await sendMessageRequest({ localMessage, message, options });
14441495
},
14451496
);
@@ -1762,6 +1813,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
17621813

17631814
const inputMessageInputContext = useCreateInputMessageInputContext({
17641815
additionalTextInputProps,
1816+
allowSendBeforeAttachmentsUpload,
17651817
asyncMessagesLockDistance,
17661818
asyncMessagesMinimumPressDuration,
17671819
asyncMessagesMultiSendEnabled,

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ export const useCreateChannelContext = ({
4444

4545
const readUsers = Object.values(read);
4646
const readUsersLength = readUsers.length;
47-
const readUsersLastReads = readUsers.map(({ last_read }) => last_read.toISOString()).join();
47+
const readUsersLastReads = readUsers
48+
.map(({ last_read }) => last_read?.toISOString() ?? '')
49+
.join();
4850

4951
const channelContext: ChannelContextValue = useMemo(
5052
() => ({

0 commit comments

Comments
 (0)