Skip to content

Commit d4d8f75

Browse files
authored
Merge pull request #3303 from GetStream/develop
Next Release
2 parents f1f208a + 383b48f commit d4d8f75

File tree

9 files changed

+138
-62
lines changed

9 files changed

+138
-62
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
@@ -102,7 +102,6 @@ import {
102102
isImagePickerAvailable,
103103
NativeHandlers,
104104
} from '../../native';
105-
import * as dbApi from '../../store/apis';
106105
import { ChannelUnreadState, FileTypes } from '../../types/types';
107106
import { addReactionToLocalState } from '../../utils/addReactionToLocalState';
108107
import { compressedImageURI } from '../../utils/compressImage';
@@ -433,6 +432,20 @@ export type ChannelPropsWithContext = Pick<ChannelContextValue, 'channel'> &
433432
messageData: StreamMessage,
434433
options?: SendMessageOptions,
435434
) => Promise<SendMessageAPIResponse>;
435+
436+
/**
437+
* A method invoked just after the first optimistic update of a new message,
438+
* but before any other HTTP requests happen. Can be used to do extra work
439+
* (such as creating a channel, or editing a message) before the local message
440+
* is sent.
441+
* @param channelId
442+
* @param messageData Message object
443+
*/
444+
preSendMessageRequest?: (options: {
445+
localMessage: LocalMessage;
446+
message: StreamMessage;
447+
options?: SendMessageOptions;
448+
}) => Promise<SendMessageAPIResponse>;
436449
/**
437450
* Overrides the Stream default update message request (Advanced usage only)
438451
* @param channelId
@@ -492,10 +505,24 @@ export type ChannelPropsWithContext = Pick<ChannelContextValue, 'channel'> &
492505
* Tells if channel is rendering a thread list
493506
*/
494507
threadList?: boolean;
508+
/**
509+
* A boolean signifying whether the Channel component should run channel.watch()
510+
* whenever it mounts up a new channel. If set to `false`, it is the integrator's
511+
* responsibility to run channel.watch() if they wish to receive WebSocket events
512+
* for that channel.
513+
*
514+
* Can be particularly useful whenever we are viewing channels in a read-only mode
515+
* or perhaps want them in an ephemeral state (i.e not created until the first message
516+
* is sent).
517+
*/
518+
initializeOnMount?: boolean;
495519
} & Partial<
496520
Pick<
497521
InputMessageInputContextValue,
498-
'openPollCreationDialog' | 'CreatePollContent' | 'StopMessageStreamingButton'
522+
| 'openPollCreationDialog'
523+
| 'CreatePollContent'
524+
| 'StopMessageStreamingButton'
525+
| 'allowSendBeforeAttachmentsUpload'
499526
>
500527
>;
501528

@@ -567,10 +594,12 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
567594
doFileUploadRequest,
568595
doMarkReadRequest,
569596
doSendMessageRequest,
597+
preSendMessageRequest,
570598
doUpdateMessageRequest,
571599
EmptyStateIndicator = EmptyStateIndicatorDefault,
572600
enableMessageGroupingByUser = true,
573601
enableOfflineSupport,
602+
allowSendBeforeAttachmentsUpload = enableOfflineSupport,
574603
enableSwipeToReply = true,
575604
enforceUniqueReaction = false,
576605
FileAttachment = FileAttachmentDefault,
@@ -715,6 +744,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
715744
VideoThumbnail = VideoThumbnailDefault,
716745
isOnline,
717746
maximumMessageLimit,
747+
initializeOnMount = true,
718748
} = props;
719749

720750
const { thread: threadProps, threadInstance } = threadFromProps;
@@ -881,7 +911,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
881911
}
882912

883913
// only update channel state if the events are not the previously subscribed useEffect's subscription events
884-
if (channel && channel.initialized) {
914+
if (channel) {
885915
// we skip the new message events if we've already done an optimistic update for the new message
886916
if (event.type === 'message.new' || event.type === 'notification.message_new') {
887917
const messageId = event.message?.id ?? '';
@@ -915,13 +945,14 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
915945
}
916946
let errored = false;
917947

918-
if (!channel.initialized || !channel.state.isUpToDate) {
948+
if ((!channel.initialized || !channel.state.isUpToDate) && initializeOnMount) {
919949
try {
920950
await channel?.watch();
921951
} catch (err) {
922952
console.warn('Channel watch request failed with error:', err);
923953
setError(true);
924954
errored = true;
955+
channel.offlineMode = true;
925956
}
926957
}
927958

@@ -1078,7 +1109,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
10781109
});
10791110

10801111
const resyncChannel = useStableCallback(async () => {
1081-
if (!channel || syncingChannelRef.current) {
1112+
if (!channel || syncingChannelRef.current || (!channel.initialized && !channel.offlineMode)) {
10821113
return;
10831114
}
10841115
syncingChannelRef.current = true;
@@ -1099,6 +1130,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
10991130
limit: channelMessagesState.messages.length + 30,
11001131
},
11011132
});
1133+
channel.offlineMode = false;
11021134
}
11031135

11041136
if (!thread) {
@@ -1300,9 +1332,13 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
13001332
attachment.image_url = uploadResponse.file;
13011333
delete attachment.originalFile;
13021334

1303-
await dbApi.updateMessage({
1304-
message: { ...updatedMessage, cid: channel.cid },
1305-
});
1335+
client.offlineDb?.executeQuerySafely(
1336+
(db) =>
1337+
db.updateMessage({
1338+
message: { ...updatedMessage, cid: channel.cid },
1339+
}),
1340+
{ method: 'updateMessage' },
1341+
);
13061342
}
13071343

13081344
if (attachment.type !== FileTypes.Image && file?.uri) {
@@ -1321,9 +1357,13 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
13211357
}
13221358

13231359
delete attachment.originalFile;
1324-
await dbApi.updateMessage({
1325-
message: { ...updatedMessage, cid: channel.cid },
1326-
});
1360+
client.offlineDb?.executeQuerySafely(
1361+
(db) =>
1362+
db.updateMessage({
1363+
message: { ...updatedMessage, cid: channel.cid },
1364+
}),
1365+
{ method: 'updateMessage' },
1366+
);
13271367
}
13281368
}
13291369
}
@@ -1344,7 +1384,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
13441384
retrying?: boolean;
13451385
}) => {
13461386
let failedMessageUpdated = false;
1347-
const handleFailedMessage = async () => {
1387+
const handleFailedMessage = () => {
13481388
if (!failedMessageUpdated) {
13491389
const updatedMessage = {
13501390
...localMessage,
@@ -1355,11 +1395,13 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
13551395
threadInstance?.upsertReplyLocally?.({ message: updatedMessage });
13561396
optimisticallyUpdatedNewMessages.delete(localMessage.id);
13571397

1358-
if (enableOfflineSupport) {
1359-
await dbApi.updateMessage({
1360-
message: updatedMessage,
1361-
});
1362-
}
1398+
client.offlineDb?.executeQuerySafely(
1399+
(db) =>
1400+
db.updateMessage({
1401+
message: updatedMessage,
1402+
}),
1403+
{ method: 'updateMessage' },
1404+
);
13631405

13641406
failedMessageUpdated = true;
13651407
}
@@ -1397,11 +1439,14 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
13971439
status: MessageStatusTypes.RECEIVED,
13981440
};
13991441

1400-
if (enableOfflineSupport) {
1401-
await dbApi.updateMessage({
1402-
message: { ...newMessageResponse, cid: channel.cid },
1403-
});
1404-
}
1442+
client.offlineDb?.executeQuerySafely(
1443+
(db) =>
1444+
db.updateMessage({
1445+
message: { ...newMessageResponse, cid: channel.cid },
1446+
}),
1447+
{ method: 'updateMessage' },
1448+
);
1449+
14051450
if (retrying) {
14061451
replaceMessage(localMessage, newMessageResponse);
14071452
} else {
@@ -1425,16 +1470,22 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
14251470
threadInstance?.upsertReplyLocally?.({ message: localMessage });
14261471
optimisticallyUpdatedNewMessages.add(localMessage.id);
14271472

1428-
if (enableOfflineSupport) {
1429-
// While sending a message, we add the message to local db with failed status, so that
1430-
// if app gets closed before message gets sent and next time user opens the app
1431-
// then user can see that message in failed state and can retry.
1432-
// If succesfull, it will be updated with received status.
1433-
await dbApi.upsertMessages({
1434-
messages: [{ ...localMessage, cid: channel.cid, status: MessageStatusTypes.FAILED }],
1435-
});
1436-
}
1473+
// While sending a message, we add the message to local db with failed status, so that
1474+
// if app gets closed before message gets sent and next time user opens the app
1475+
// then user can see that message in failed state and can retry.
1476+
// If succesfull, it will be updated with received status.
1477+
client.offlineDb?.executeQuerySafely(
1478+
(db) =>
1479+
db.upsertMessages({
1480+
// @ts-ignore
1481+
messages: [{ ...localMessage, cid: channel.cid, status: MessageStatusTypes.FAILED }],
1482+
}),
1483+
{ method: 'upsertMessages' },
1484+
);
14371485

1486+
if (preSendMessageRequest) {
1487+
await preSendMessageRequest({ localMessage, message, options });
1488+
}
14381489
await sendMessageRequest({ localMessage, message, options });
14391490
},
14401491
);
@@ -1756,6 +1807,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
17561807

17571808
const inputMessageInputContext = useCreateInputMessageInputContext({
17581809
additionalTextInputProps,
1810+
allowSendBeforeAttachmentsUpload,
17591811
asyncMessagesLockDistance,
17601812
asyncMessagesMinimumPressDuration,
17611813
asyncMessagesMultiSendEnabled,

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

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

4444
const readUsers = Object.values(read);
4545
const readUsersLength = readUsers.length;
46-
const readUsersLastReads = readUsers.map(({ last_read }) => last_read.toISOString()).join();
46+
const readUsersLastReads = readUsers
47+
.map(({ last_read }) => last_read?.toISOString() ?? '')
48+
.join();
4749
const stringifiedChannelUnreadState = JSON.stringify(channelUnreadState);
4850

4951
const channelContext: ChannelContextValue = useMemo(

0 commit comments

Comments
 (0)