stream-chat-react-native-core 5.4.1 → 5.4.2-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/components/AttachmentPicker/AttachmentPicker.js +122 -433
- package/lib/commonjs/components/AttachmentPicker/AttachmentPicker.js.map +1 -1
- package/lib/commonjs/components/AttachmentPicker/components/AttachmentPickerItem.js +358 -0
- package/lib/commonjs/components/AttachmentPicker/components/AttachmentPickerItem.js.map +1 -0
- package/lib/commonjs/components/MessageList/MessageList.js +59 -40
- package/lib/commonjs/components/MessageList/MessageList.js.map +1 -1
- package/lib/commonjs/components/MessageList/NetworkDownIndicator.js +14 -6
- package/lib/commonjs/components/MessageList/NetworkDownIndicator.js.map +1 -1
- package/lib/commonjs/components/MessageList/hooks/useMessageList.js +4 -3
- package/lib/commonjs/components/MessageList/hooks/useMessageList.js.map +1 -1
- package/lib/commonjs/contexts/messageInputContext/MessageInputContext.js +134 -85
- package/lib/commonjs/contexts/messageInputContext/MessageInputContext.js.map +1 -1
- package/lib/commonjs/contexts/overlayContext/OverlayProvider.js +39 -17
- package/lib/commonjs/contexts/overlayContext/OverlayProvider.js.map +1 -1
- package/lib/commonjs/hooks/useAppStateListener.js +9 -15
- package/lib/commonjs/hooks/useAppStateListener.js.map +1 -1
- package/lib/commonjs/mock-builders/generator/message.js +1 -1
- package/lib/commonjs/mock-builders/generator/message.js.map +1 -1
- package/lib/commonjs/version.json +1 -1
- package/lib/module/components/AttachmentPicker/AttachmentPicker.js +122 -433
- package/lib/module/components/AttachmentPicker/AttachmentPicker.js.map +1 -1
- package/lib/module/components/AttachmentPicker/components/AttachmentPickerItem.js +358 -0
- package/lib/module/components/AttachmentPicker/components/AttachmentPickerItem.js.map +1 -0
- package/lib/module/components/MessageList/MessageList.js +59 -40
- package/lib/module/components/MessageList/MessageList.js.map +1 -1
- package/lib/module/components/MessageList/NetworkDownIndicator.js +14 -6
- package/lib/module/components/MessageList/NetworkDownIndicator.js.map +1 -1
- package/lib/module/components/MessageList/hooks/useMessageList.js +4 -3
- package/lib/module/components/MessageList/hooks/useMessageList.js.map +1 -1
- package/lib/module/contexts/messageInputContext/MessageInputContext.js +134 -85
- package/lib/module/contexts/messageInputContext/MessageInputContext.js.map +1 -1
- package/lib/module/contexts/overlayContext/OverlayProvider.js +39 -17
- package/lib/module/contexts/overlayContext/OverlayProvider.js.map +1 -1
- package/lib/module/hooks/useAppStateListener.js +9 -15
- package/lib/module/hooks/useAppStateListener.js.map +1 -1
- package/lib/module/mock-builders/generator/message.js +1 -1
- package/lib/module/mock-builders/generator/message.js.map +1 -1
- package/lib/module/version.json +1 -1
- package/lib/typescript/components/AttachmentPicker/AttachmentPicker.d.ts +0 -11
- package/lib/typescript/components/AttachmentPicker/components/AttachmentPickerItem.d.ts +16 -0
- package/lib/typescript/components/MessageList/__tests__/useMessageList.test.d.ts +1 -0
- package/lib/typescript/components/MessageList/hooks/useMessageList.d.ts +0 -1
- package/lib/typescript/contexts/messageInputContext/MessageInputContext.d.ts +1 -1
- package/lib/typescript/hooks/__tests__/useAppStateListener.test.d.ts +1 -0
- package/package.json +2 -2
- package/src/components/AttachmentPicker/AttachmentPicker.tsx +79 -322
- package/src/components/AttachmentPicker/components/AttachmentPickerItem.tsx +245 -0
- package/src/components/MessageList/MessageList.tsx +21 -20
- package/src/components/MessageList/NetworkDownIndicator.tsx +13 -10
- package/src/components/MessageList/__tests__/useMessageList.test.tsx +67 -0
- package/src/components/MessageList/hooks/useMessageList.ts +4 -7
- package/src/contexts/messageInputContext/MessageInputContext.tsx +27 -25
- package/src/contexts/overlayContext/OverlayProvider.tsx +28 -4
- package/src/hooks/__tests__/useAppStateListener.test.tsx +41 -0
- package/src/hooks/useAppStateListener.ts +9 -12
- package/src/mock-builders/generator/message.js +1 -1
- package/src/version.json +1 -1
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { Alert, ImageBackground, StyleSheet, Text, View } from 'react-native';
|
|
4
|
+
|
|
5
|
+
import { TouchableOpacity } from '@gorhom/bottom-sheet';
|
|
6
|
+
import dayjs from 'dayjs';
|
|
7
|
+
import { lookup } from 'mime-types';
|
|
8
|
+
|
|
9
|
+
import { useTheme } from '../../../contexts/themeContext/ThemeContext';
|
|
10
|
+
import { Recorder } from '../../../icons';
|
|
11
|
+
import { getLocalAssetUri } from '../../../native';
|
|
12
|
+
import type { Asset, File } from '../../../types/types';
|
|
13
|
+
import { vw } from '../../../utils/utils';
|
|
14
|
+
|
|
15
|
+
type AttachmentPickerItemType = {
|
|
16
|
+
asset: Asset;
|
|
17
|
+
ImageOverlaySelectedComponent: React.ComponentType;
|
|
18
|
+
maxNumberOfFiles: number;
|
|
19
|
+
numberOfUploads: number;
|
|
20
|
+
selected: boolean;
|
|
21
|
+
setSelectedFiles: React.Dispatch<React.SetStateAction<File[]>>;
|
|
22
|
+
setSelectedImages: React.Dispatch<React.SetStateAction<Asset[]>>;
|
|
23
|
+
numberOfAttachmentPickerImageColumns?: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type AttachmentImageProps = Omit<AttachmentPickerItemType, 'setSelectedFiles'>;
|
|
27
|
+
|
|
28
|
+
type AttachmentVideoProps = Omit<AttachmentPickerItemType, 'setSelectedImages'>;
|
|
29
|
+
|
|
30
|
+
const AttachmentVideo: React.FC<AttachmentVideoProps> = (props) => {
|
|
31
|
+
const {
|
|
32
|
+
asset,
|
|
33
|
+
ImageOverlaySelectedComponent,
|
|
34
|
+
maxNumberOfFiles,
|
|
35
|
+
numberOfAttachmentPickerImageColumns,
|
|
36
|
+
numberOfUploads,
|
|
37
|
+
selected,
|
|
38
|
+
setSelectedFiles,
|
|
39
|
+
} = props;
|
|
40
|
+
|
|
41
|
+
const {
|
|
42
|
+
theme: {
|
|
43
|
+
attachmentPicker: { durationText, image, imageOverlay },
|
|
44
|
+
colors: { overlay, white },
|
|
45
|
+
},
|
|
46
|
+
} = useTheme();
|
|
47
|
+
|
|
48
|
+
const { duration, playableDuration, uri } = asset;
|
|
49
|
+
|
|
50
|
+
const videoDuration = duration ? duration : playableDuration;
|
|
51
|
+
|
|
52
|
+
const ONE_HOUR_IN_SECONDS = 3600;
|
|
53
|
+
|
|
54
|
+
let durationLabel = '00:00';
|
|
55
|
+
|
|
56
|
+
if (videoDuration) {
|
|
57
|
+
const isDurationLongerThanHour = videoDuration / ONE_HOUR_IN_SECONDS >= 1;
|
|
58
|
+
const formattedDurationParam = isDurationLongerThanHour ? 'HH:mm:ss' : 'mm:ss';
|
|
59
|
+
const formattedVideoDuration = dayjs
|
|
60
|
+
.duration(videoDuration, 'second')
|
|
61
|
+
.format(formattedDurationParam);
|
|
62
|
+
durationLabel = formattedVideoDuration;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const size = vw(100) / (numberOfAttachmentPickerImageColumns || 3) - 2;
|
|
66
|
+
|
|
67
|
+
const onPressVideo = async () => {
|
|
68
|
+
// For the case of expo messaging app where you need to fetch the asset uri from asset id
|
|
69
|
+
const localAssetURI = asset.id && (await getLocalAssetUri(asset.id));
|
|
70
|
+
if (selected) {
|
|
71
|
+
setSelectedFiles((files) => files.filter((file) => file.uri !== asset.uri));
|
|
72
|
+
} else {
|
|
73
|
+
setSelectedFiles((files) => {
|
|
74
|
+
if (numberOfUploads >= maxNumberOfFiles) {
|
|
75
|
+
Alert.alert('Maximum number of files reached');
|
|
76
|
+
return files;
|
|
77
|
+
}
|
|
78
|
+
return [
|
|
79
|
+
...files,
|
|
80
|
+
{
|
|
81
|
+
duration: durationLabel,
|
|
82
|
+
name: asset.filename,
|
|
83
|
+
size: asset.fileSize,
|
|
84
|
+
type: 'video',
|
|
85
|
+
uri: localAssetURI || asset.uri,
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<TouchableOpacity onPress={onPressVideo}>
|
|
94
|
+
<ImageBackground
|
|
95
|
+
source={{ uri }}
|
|
96
|
+
style={[
|
|
97
|
+
{
|
|
98
|
+
height: size,
|
|
99
|
+
margin: 1,
|
|
100
|
+
width: size,
|
|
101
|
+
},
|
|
102
|
+
image,
|
|
103
|
+
]}
|
|
104
|
+
>
|
|
105
|
+
{selected && (
|
|
106
|
+
<View style={[styles.overlay, { backgroundColor: overlay }, imageOverlay]}>
|
|
107
|
+
<ImageOverlaySelectedComponent />
|
|
108
|
+
</View>
|
|
109
|
+
)}
|
|
110
|
+
<View style={styles.videoView}>
|
|
111
|
+
<Recorder height={20} pathFill={white} width={25} />
|
|
112
|
+
{videoDuration ? (
|
|
113
|
+
<Text style={[styles.durationText, durationText, { color: white }]}>
|
|
114
|
+
{durationLabel}
|
|
115
|
+
</Text>
|
|
116
|
+
) : null}
|
|
117
|
+
</View>
|
|
118
|
+
</ImageBackground>
|
|
119
|
+
</TouchableOpacity>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const AttachmentImage: React.FC<AttachmentImageProps> = (props) => {
|
|
124
|
+
const {
|
|
125
|
+
asset,
|
|
126
|
+
ImageOverlaySelectedComponent,
|
|
127
|
+
maxNumberOfFiles,
|
|
128
|
+
numberOfAttachmentPickerImageColumns,
|
|
129
|
+
numberOfUploads,
|
|
130
|
+
selected,
|
|
131
|
+
setSelectedImages,
|
|
132
|
+
} = props;
|
|
133
|
+
const {
|
|
134
|
+
theme: {
|
|
135
|
+
attachmentPicker: { image, imageOverlay },
|
|
136
|
+
colors: { overlay },
|
|
137
|
+
},
|
|
138
|
+
} = useTheme();
|
|
139
|
+
|
|
140
|
+
const size = vw(100) / (numberOfAttachmentPickerImageColumns || 3) - 2;
|
|
141
|
+
|
|
142
|
+
const { uri } = asset;
|
|
143
|
+
|
|
144
|
+
const onPressImage = () => {
|
|
145
|
+
if (selected) {
|
|
146
|
+
setSelectedImages((images) => images.filter((image) => image.uri !== asset.uri));
|
|
147
|
+
} else {
|
|
148
|
+
setSelectedImages((images) => {
|
|
149
|
+
if (numberOfUploads >= maxNumberOfFiles) {
|
|
150
|
+
Alert.alert('Maximum number of files reached');
|
|
151
|
+
return images;
|
|
152
|
+
}
|
|
153
|
+
return [...images, asset];
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<TouchableOpacity onPress={onPressImage}>
|
|
160
|
+
<ImageBackground
|
|
161
|
+
source={{ uri }}
|
|
162
|
+
style={[
|
|
163
|
+
{
|
|
164
|
+
height: size,
|
|
165
|
+
margin: 1,
|
|
166
|
+
width: size,
|
|
167
|
+
},
|
|
168
|
+
image,
|
|
169
|
+
]}
|
|
170
|
+
>
|
|
171
|
+
{selected && (
|
|
172
|
+
<View style={[styles.overlay, { backgroundColor: overlay }, imageOverlay]}>
|
|
173
|
+
<ImageOverlaySelectedComponent />
|
|
174
|
+
</View>
|
|
175
|
+
)}
|
|
176
|
+
</ImageBackground>
|
|
177
|
+
</TouchableOpacity>
|
|
178
|
+
);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const getFileType = (asset: Asset) => {
|
|
182
|
+
const { filename } = asset;
|
|
183
|
+
if (filename) {
|
|
184
|
+
const contentType = lookup(filename) || 'multipart/form-data';
|
|
185
|
+
return contentType.startsWith('image/') ? 'image' : 'video';
|
|
186
|
+
} else {
|
|
187
|
+
return asset.type === 'video' ? 'video' : 'image';
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export const renderAttachmentPickerItem = ({ item }: { item: AttachmentPickerItemType }) => {
|
|
192
|
+
const {
|
|
193
|
+
asset,
|
|
194
|
+
ImageOverlaySelectedComponent,
|
|
195
|
+
maxNumberOfFiles,
|
|
196
|
+
numberOfAttachmentPickerImageColumns,
|
|
197
|
+
numberOfUploads,
|
|
198
|
+
selected,
|
|
199
|
+
setSelectedFiles,
|
|
200
|
+
setSelectedImages,
|
|
201
|
+
} = item;
|
|
202
|
+
|
|
203
|
+
const fileType = getFileType(asset);
|
|
204
|
+
|
|
205
|
+
return fileType === 'image' ? (
|
|
206
|
+
<AttachmentImage
|
|
207
|
+
asset={asset}
|
|
208
|
+
ImageOverlaySelectedComponent={ImageOverlaySelectedComponent}
|
|
209
|
+
maxNumberOfFiles={maxNumberOfFiles}
|
|
210
|
+
numberOfAttachmentPickerImageColumns={numberOfAttachmentPickerImageColumns}
|
|
211
|
+
numberOfUploads={numberOfUploads}
|
|
212
|
+
selected={selected}
|
|
213
|
+
setSelectedImages={setSelectedImages}
|
|
214
|
+
/>
|
|
215
|
+
) : (
|
|
216
|
+
<AttachmentVideo
|
|
217
|
+
asset={asset}
|
|
218
|
+
ImageOverlaySelectedComponent={ImageOverlaySelectedComponent}
|
|
219
|
+
maxNumberOfFiles={maxNumberOfFiles}
|
|
220
|
+
numberOfAttachmentPickerImageColumns={numberOfAttachmentPickerImageColumns}
|
|
221
|
+
numberOfUploads={numberOfUploads}
|
|
222
|
+
selected={selected}
|
|
223
|
+
setSelectedFiles={setSelectedFiles}
|
|
224
|
+
/>
|
|
225
|
+
);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const styles = StyleSheet.create({
|
|
229
|
+
durationText: {
|
|
230
|
+
fontWeight: 'bold',
|
|
231
|
+
},
|
|
232
|
+
overlay: {
|
|
233
|
+
alignItems: 'flex-end',
|
|
234
|
+
flex: 1,
|
|
235
|
+
},
|
|
236
|
+
videoView: {
|
|
237
|
+
bottom: 5,
|
|
238
|
+
display: 'flex',
|
|
239
|
+
flexDirection: 'row',
|
|
240
|
+
justifyContent: 'space-between',
|
|
241
|
+
paddingHorizontal: 5,
|
|
242
|
+
position: 'absolute',
|
|
243
|
+
width: '100%',
|
|
244
|
+
},
|
|
245
|
+
});
|
|
@@ -92,13 +92,12 @@ const keyExtractor = <
|
|
|
92
92
|
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
|
|
93
93
|
>(
|
|
94
94
|
item: MessageType<StreamChatGenerics>,
|
|
95
|
-
) =>
|
|
96
|
-
item.id
|
|
97
|
-
(item.created_at
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
: Date.now().toString());
|
|
95
|
+
) => {
|
|
96
|
+
if (item.id) return item.id;
|
|
97
|
+
if (item.created_at)
|
|
98
|
+
return typeof item.created_at === 'string' ? item.created_at : item.created_at.toISOString();
|
|
99
|
+
return Date.now().toString();
|
|
100
|
+
};
|
|
102
101
|
|
|
103
102
|
const flatListViewabilityConfig = {
|
|
104
103
|
viewAreaCoveragePercentThreshold: 1,
|
|
@@ -294,7 +293,6 @@ const MessageListWithContext = <
|
|
|
294
293
|
);
|
|
295
294
|
|
|
296
295
|
const messageList = useMessageList<StreamChatGenerics>({
|
|
297
|
-
inverted,
|
|
298
296
|
noGroupByUser,
|
|
299
297
|
threadList,
|
|
300
298
|
});
|
|
@@ -911,12 +909,13 @@ const MessageListWithContext = <
|
|
|
911
909
|
stickyHeaderDate?.getFullYear() === new Date().getFullYear() ? 'MMM D' : 'MMM D, YYYY';
|
|
912
910
|
const tStickyHeaderDate =
|
|
913
911
|
stickyHeaderDate && !hideStickyDateHeader ? tDateTimeParser(stickyHeaderDate) : null;
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
912
|
+
|
|
913
|
+
const stickyHeaderDateString = useMemo(() => {
|
|
914
|
+
if (tStickyHeaderDate === null || hideStickyDateHeader) return null;
|
|
915
|
+
if (isDayOrMoment(tStickyHeaderDate)) return tStickyHeaderDate.format(stickyHeaderFormatDate);
|
|
916
|
+
|
|
917
|
+
return new Date(tStickyHeaderDate).toDateString();
|
|
918
|
+
}, [tStickyHeaderDate, stickyHeaderFormatDate, hideStickyDateHeader]);
|
|
920
919
|
|
|
921
920
|
const dismissImagePicker = () => {
|
|
922
921
|
if (!hasMoved && selectedPicker) {
|
|
@@ -964,6 +963,13 @@ const MessageListWithContext = <
|
|
|
964
963
|
);
|
|
965
964
|
}
|
|
966
965
|
|
|
966
|
+
const StickyHeaderComponent = () => {
|
|
967
|
+
if (!stickyHeaderDateString) return null;
|
|
968
|
+
if (StickyHeader) return <StickyHeader dateString={stickyHeaderDateString} />;
|
|
969
|
+
if (messageListLengthAfterUpdate) return <DateHeader dateString={stickyHeaderDateString} />;
|
|
970
|
+
return null;
|
|
971
|
+
};
|
|
972
|
+
|
|
967
973
|
return (
|
|
968
974
|
<View
|
|
969
975
|
style={[styles.container, { backgroundColor: white_snow }, container]}
|
|
@@ -1002,12 +1008,7 @@ const MessageListWithContext = <
|
|
|
1002
1008
|
{!loading && (
|
|
1003
1009
|
<>
|
|
1004
1010
|
<View style={styles.stickyHeader}>
|
|
1005
|
-
|
|
1006
|
-
(StickyHeader ? (
|
|
1007
|
-
<StickyHeader dateString={stickyHeaderDateToRender} />
|
|
1008
|
-
) : messageListLengthAfterUpdate ? (
|
|
1009
|
-
<DateHeader dateString={stickyHeaderDateToRender} />
|
|
1010
|
-
) : null)}
|
|
1011
|
+
<StickyHeaderComponent />
|
|
1011
1012
|
</View>
|
|
1012
1013
|
{!disableTypingIndicator && TypingIndicator && (
|
|
1013
1014
|
<TypingIndicatorContainer>
|
|
@@ -33,22 +33,25 @@ export const NetworkDownIndicator = () => {
|
|
|
33
33
|
} = useTheme();
|
|
34
34
|
const { t } = useTranslationContext();
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
const getText = () => {
|
|
37
|
+
if (!isOnline) {
|
|
38
|
+
return t('Reconnecting...');
|
|
39
|
+
} else if (error) {
|
|
40
|
+
return t('Error loading messages for this channel...');
|
|
41
|
+
}
|
|
42
|
+
return '';
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const indicatorText = getText();
|
|
46
|
+
|
|
47
|
+
if (!indicatorText) return null;
|
|
39
48
|
|
|
40
49
|
return (
|
|
41
50
|
<View
|
|
42
51
|
style={[styles.errorNotification, { backgroundColor: `${grey}E6` }, errorNotification]}
|
|
43
52
|
testID='error-notification'
|
|
44
53
|
>
|
|
45
|
-
<Text style={[styles.errorNotificationText, errorNotificationText]}>
|
|
46
|
-
{!isOnline
|
|
47
|
-
? t('Reconnecting...')
|
|
48
|
-
: error
|
|
49
|
-
? t('Error loading messages for this channel...')
|
|
50
|
-
: ''}
|
|
51
|
-
</Text>
|
|
54
|
+
<Text style={[styles.errorNotificationText, errorNotificationText]}>{indicatorText}</Text>
|
|
52
55
|
</View>
|
|
53
56
|
);
|
|
54
57
|
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React, { FC } from 'react';
|
|
2
|
+
|
|
3
|
+
import { renderHook } from '@testing-library/react-hooks';
|
|
4
|
+
import type { DefaultStreamChatGenerics } from 'src/types/types';
|
|
5
|
+
import type { DefaultGenerics, StreamChat } from 'stream-chat';
|
|
6
|
+
|
|
7
|
+
import { useCreatePaginatedMessageListContext } from '../../../components/Channel/hooks/useCreatePaginatedMessageListContext';
|
|
8
|
+
import {
|
|
9
|
+
ChatContext,
|
|
10
|
+
ChatContextValue,
|
|
11
|
+
PaginatedMessageListContextValue,
|
|
12
|
+
PaginatedMessageListProvider,
|
|
13
|
+
} from '../../../contexts';
|
|
14
|
+
|
|
15
|
+
import { generateMessage } from '../../../mock-builders/generator/message';
|
|
16
|
+
import { generateUser } from '../../../mock-builders/generator/user';
|
|
17
|
+
import { getTestClientWithUser } from '../../../mock-builders/mock';
|
|
18
|
+
import { useMessageList } from '../hooks/useMessageList';
|
|
19
|
+
|
|
20
|
+
jest.mock('react-native-fs', () => ({}));
|
|
21
|
+
|
|
22
|
+
const clientUser = generateUser();
|
|
23
|
+
let chatClient: StreamChat<DefaultGenerics> | StreamChat<DefaultStreamChatGenerics>;
|
|
24
|
+
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
chatClient = await getTestClientWithUser(clientUser);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const messages = new Array(10)
|
|
30
|
+
.fill(undefined)
|
|
31
|
+
.map((_: undefined, id: number) => generateMessage({ id }));
|
|
32
|
+
|
|
33
|
+
const Providers: FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
34
|
+
const messageListContext = useCreatePaginatedMessageListContext({
|
|
35
|
+
messages,
|
|
36
|
+
} as unknown as PaginatedMessageListContextValue);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<ChatContext.Provider
|
|
40
|
+
value={
|
|
41
|
+
{
|
|
42
|
+
auto_translation_enabled: true,
|
|
43
|
+
client: chatClient,
|
|
44
|
+
} as unknown as ChatContextValue
|
|
45
|
+
}
|
|
46
|
+
>
|
|
47
|
+
<PaginatedMessageListProvider value={messageListContext}>
|
|
48
|
+
{children}
|
|
49
|
+
</PaginatedMessageListProvider>
|
|
50
|
+
</ChatContext.Provider>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
describe('useMessageList', () => {
|
|
55
|
+
it('should always return a list of reversed messages', () => {
|
|
56
|
+
const { result } = renderHook(
|
|
57
|
+
() =>
|
|
58
|
+
useMessageList({
|
|
59
|
+
noGroupByUser: true,
|
|
60
|
+
threadList: false,
|
|
61
|
+
}),
|
|
62
|
+
{ wrapper: Providers },
|
|
63
|
+
);
|
|
64
|
+
const reversedMessages = messages.reverse();
|
|
65
|
+
expect(result.current.map(({ id }) => id)).toEqual(reversedMessages.map(({ id }) => id));
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -18,7 +18,6 @@ import { getReadStates } from '../utils/getReadStates';
|
|
|
18
18
|
|
|
19
19
|
export type UseMessageListParams = {
|
|
20
20
|
deletedMessagesVisibilityType?: DeletedMessagesVisibilityType;
|
|
21
|
-
inverted?: boolean;
|
|
22
21
|
noGroupByUser?: boolean;
|
|
23
22
|
threadList?: boolean;
|
|
24
23
|
};
|
|
@@ -52,7 +51,7 @@ export const useMessageList = <
|
|
|
52
51
|
>(
|
|
53
52
|
params: UseMessageListParams,
|
|
54
53
|
) => {
|
|
55
|
-
const {
|
|
54
|
+
const { noGroupByUser, threadList } = params;
|
|
56
55
|
const { client } = useChatContext<StreamChatGenerics>();
|
|
57
56
|
const { hideDateSeparators, maxTimeBetweenGroupedMessages, read } =
|
|
58
57
|
useChannelContext<StreamChatGenerics>();
|
|
@@ -104,9 +103,7 @@ export const useMessageList = <
|
|
|
104
103
|
readBy: msg.id ? readData[msg.id] || false : false,
|
|
105
104
|
}));
|
|
106
105
|
|
|
107
|
-
return
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
: messagesWithStylesReadByAndDateSeparator
|
|
111
|
-
) as MessageType<StreamChatGenerics>[];
|
|
106
|
+
return [
|
|
107
|
+
...messagesWithStylesReadByAndDateSeparator,
|
|
108
|
+
].reverse() as MessageType<StreamChatGenerics>[];
|
|
112
109
|
};
|
|
@@ -211,7 +211,7 @@ export type LocalMessageInputContext<
|
|
|
211
211
|
/** Function for attempting to upload an image */
|
|
212
212
|
uploadImage: ({ newImage }: { newImage: ImageUpload }) => Promise<void>;
|
|
213
213
|
uploadNewFile: (file: File) => Promise<void>;
|
|
214
|
-
uploadNewImage: (image: Asset) => Promise<void>;
|
|
214
|
+
uploadNewImage: (image: Partial<Asset>) => Promise<void>;
|
|
215
215
|
};
|
|
216
216
|
|
|
217
217
|
export type InputMessageInputContextValue<
|
|
@@ -549,16 +549,8 @@ export const MessageInputProvider = <
|
|
|
549
549
|
const openAttachmentPicker = () => {
|
|
550
550
|
if (hasImagePicker) {
|
|
551
551
|
Keyboard.dismiss();
|
|
552
|
-
openPicker();
|
|
553
552
|
setSelectedPicker('images');
|
|
554
|
-
|
|
555
|
-
* TODO: Remove this, this is the result of
|
|
556
|
-
* the bottom sheet now having some keyboard
|
|
557
|
-
* handling baked in, creating an issue when
|
|
558
|
-
* we call dismiss and open in short order.
|
|
559
|
-
* https://github.com/gorhom/react-native-bottom-sheet/issues/446
|
|
560
|
-
*/
|
|
561
|
-
setTimeout(openPicker, 600);
|
|
553
|
+
openPicker();
|
|
562
554
|
} else if (hasFilePicker) {
|
|
563
555
|
pickFile();
|
|
564
556
|
}
|
|
@@ -834,19 +826,27 @@ export const MessageInputProvider = <
|
|
|
834
826
|
}
|
|
835
827
|
};
|
|
836
828
|
|
|
837
|
-
const
|
|
838
|
-
|
|
839
|
-
|
|
829
|
+
const getTriggerSettings = () => {
|
|
830
|
+
let triggerSettings: TriggerSettings<StreamChatGenerics> = {};
|
|
831
|
+
if (channel) {
|
|
832
|
+
if (value.autoCompleteTriggerSettings) {
|
|
833
|
+
triggerSettings = value.autoCompleteTriggerSettings({
|
|
840
834
|
channel,
|
|
841
835
|
client,
|
|
842
836
|
onMentionSelectItem: onSelectItem,
|
|
843
|
-
})
|
|
844
|
-
|
|
837
|
+
});
|
|
838
|
+
} else {
|
|
839
|
+
triggerSettings = ACITriggerSettings<StreamChatGenerics>({
|
|
845
840
|
channel,
|
|
846
841
|
client,
|
|
847
842
|
onMentionSelectItem: onSelectItem,
|
|
848
|
-
})
|
|
849
|
-
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return triggerSettings;
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
const triggerSettings = getTriggerSettings();
|
|
850
850
|
|
|
851
851
|
const updateMessage = async () => {
|
|
852
852
|
try {
|
|
@@ -946,13 +946,15 @@ export const MessageInputProvider = <
|
|
|
946
946
|
* if the uri includes assets-library, this uses the CameraRoll.save
|
|
947
947
|
* function to also create a local uri.
|
|
948
948
|
*/
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
949
|
+
const getLocalUri = async () => {
|
|
950
|
+
if (file.id) {
|
|
951
|
+
return await getLocalAssetUri(file.id);
|
|
952
|
+
} else if (file.uri?.match(/assets-library/)) {
|
|
953
|
+
return await getLocalAssetUri(file.uri);
|
|
954
|
+
}
|
|
955
|
+
return file.uri;
|
|
956
|
+
};
|
|
957
|
+
const uri = file.name || (await getLocalUri()) || '';
|
|
956
958
|
/**
|
|
957
959
|
* We skip compression if:
|
|
958
960
|
* - the file is from the camera as that should already be compressed
|
|
@@ -1053,7 +1055,7 @@ export const MessageInputProvider = <
|
|
|
1053
1055
|
}
|
|
1054
1056
|
};
|
|
1055
1057
|
|
|
1056
|
-
const uploadNewImage = async (image: Asset) => {
|
|
1058
|
+
const uploadNewImage = async (image: Partial<Asset>) => {
|
|
1057
1059
|
const id = generateRandomId();
|
|
1058
1060
|
|
|
1059
1061
|
const isBlockedImageMimeType = blockedImageMimeTypes?.some((mimeType: string) =>
|
|
@@ -64,6 +64,7 @@ export const OverlayProvider = <
|
|
|
64
64
|
>(
|
|
65
65
|
props: PropsWithChildren<OverlayProviderProps<StreamChatGenerics>>,
|
|
66
66
|
) => {
|
|
67
|
+
const bottomSheetCloseTimeoutRef = useRef<NodeJS.Timeout>();
|
|
67
68
|
const {
|
|
68
69
|
AttachmentPickerBottomSheetHandle = DefaultAttachmentPickerBottomSheetHandle,
|
|
69
70
|
attachmentPickerBottomSheetHandleHeight,
|
|
@@ -77,8 +78,19 @@ export const OverlayProvider = <
|
|
|
77
78
|
CameraSelectorIcon = DefaultCameraSelectorIcon,
|
|
78
79
|
children,
|
|
79
80
|
closePicker = (ref) => {
|
|
80
|
-
if (ref.current) {
|
|
81
|
+
if (ref.current?.close) {
|
|
82
|
+
if (bottomSheetCloseTimeoutRef.current) {
|
|
83
|
+
clearTimeout(bottomSheetCloseTimeoutRef.current);
|
|
84
|
+
}
|
|
81
85
|
ref.current.close();
|
|
86
|
+
// Attempt to close the bottomsheet again to circumvent accidental opening on Android.
|
|
87
|
+
// Details: This to prevent a race condition where the close function is called during the point when a internal container layout happens within the bottomsheet due to keyboard affecting the layout
|
|
88
|
+
// If the container layout measures a shorter height than previous but if the close snapped to the previous height's position, the bottom sheet will show up
|
|
89
|
+
// this short delay ensures that close function is always called after a container layout due to keyboard change
|
|
90
|
+
// NOTE: this timeout has to be above 500 as the keyboardAnimationDuration is 500 in the bottomsheet library - see src/hooks/useKeyboard.ts there for more details
|
|
91
|
+
bottomSheetCloseTimeoutRef.current = setTimeout(() => {
|
|
92
|
+
ref.current?.close();
|
|
93
|
+
}, 600);
|
|
82
94
|
}
|
|
83
95
|
},
|
|
84
96
|
FileSelectorIcon = DefaultFileSelectorIcon,
|
|
@@ -96,6 +108,9 @@ export const OverlayProvider = <
|
|
|
96
108
|
numberOfAttachmentPickerImageColumns,
|
|
97
109
|
numberOfImageGalleryGridColumns,
|
|
98
110
|
openPicker = (ref) => {
|
|
111
|
+
if (bottomSheetCloseTimeoutRef.current) {
|
|
112
|
+
clearTimeout(bottomSheetCloseTimeoutRef.current);
|
|
113
|
+
}
|
|
99
114
|
if (ref.current?.snapToIndex) {
|
|
100
115
|
ref.current.snapToIndex(0);
|
|
101
116
|
} else {
|
|
@@ -154,10 +169,19 @@ export const OverlayProvider = <
|
|
|
154
169
|
return () => backHandler.remove();
|
|
155
170
|
}, [overlay]);
|
|
156
171
|
|
|
172
|
+
useEffect(
|
|
173
|
+
() =>
|
|
174
|
+
// cleanup the timeout if the component unmounts
|
|
175
|
+
() => {
|
|
176
|
+
if (bottomSheetCloseTimeoutRef.current) {
|
|
177
|
+
clearTimeout(bottomSheetCloseTimeoutRef.current);
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
[],
|
|
181
|
+
);
|
|
182
|
+
|
|
157
183
|
useEffect(() => {
|
|
158
|
-
|
|
159
|
-
bottomSheetRef.current.close?.();
|
|
160
|
-
}
|
|
184
|
+
closePicker(bottomSheetRef);
|
|
161
185
|
cancelAnimation(overlayOpacity);
|
|
162
186
|
if (overlay !== 'none') {
|
|
163
187
|
overlayOpacity.value = withTiming(1);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { AppState, AppStateStatus } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { renderHook } from '@testing-library/react-hooks/pure';
|
|
4
|
+
import { waitFor } from '@testing-library/react-native';
|
|
5
|
+
|
|
6
|
+
import { useAppStateListener } from '../useAppStateListener';
|
|
7
|
+
|
|
8
|
+
describe('useAppStateListener', () => {
|
|
9
|
+
const onForeground = jest.fn();
|
|
10
|
+
const onBackground = jest.fn();
|
|
11
|
+
const addEventListenerSpy = jest.spyOn(AppState, 'addEventListener');
|
|
12
|
+
AppState.currentState = 'active';
|
|
13
|
+
const { unmount } = renderHook(() => useAppStateListener(onForeground, onBackground));
|
|
14
|
+
const appStateOnChangeMockFunc = addEventListenerSpy.mock.calls[0][1];
|
|
15
|
+
const { remove: appStateOnChangeSubscriptionRemoveMockFunc } =
|
|
16
|
+
addEventListenerSpy.mock.results[0].value;
|
|
17
|
+
test.each<[AppStateStatus, jest.Mock, number]>([
|
|
18
|
+
['background', onBackground, 1],
|
|
19
|
+
['active', onForeground, 1],
|
|
20
|
+
['inactive', onBackground, 2],
|
|
21
|
+
['active', onForeground, 2],
|
|
22
|
+
['background', onBackground, 3],
|
|
23
|
+
])(
|
|
24
|
+
'Appropriate callback called when appstate is changed to %s)',
|
|
25
|
+
async (newAppState, expectedCallback, times) => {
|
|
26
|
+
appStateOnChangeMockFunc(newAppState);
|
|
27
|
+
await waitFor(() => {
|
|
28
|
+
expect(expectedCallback).toHaveBeenCalledTimes(times);
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
);
|
|
32
|
+
test('check unmount behavior', async () => {
|
|
33
|
+
await waitFor(() => {
|
|
34
|
+
expect(appStateOnChangeSubscriptionRemoveMockFunc).not.toHaveBeenCalled();
|
|
35
|
+
});
|
|
36
|
+
unmount();
|
|
37
|
+
await waitFor(() => {
|
|
38
|
+
expect(appStateOnChangeSubscriptionRemoveMockFunc).toHaveBeenCalled();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -1,22 +1,19 @@
|
|
|
1
|
-
import { useCallback, useEffect,
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
2
|
import { AppState, AppStateStatus } from 'react-native';
|
|
3
3
|
|
|
4
4
|
export const useAppStateListener = (onForeground?: () => void, onBackground?: () => void) => {
|
|
5
|
-
const
|
|
5
|
+
const appStateRef = useRef(AppState.currentState);
|
|
6
6
|
const handleAppStateChange = useCallback(
|
|
7
7
|
(nextAppState: AppStateStatus) => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
onBackground
|
|
14
|
-
) {
|
|
15
|
-
onBackground();
|
|
8
|
+
const prevAppState = appStateRef.current;
|
|
9
|
+
if (prevAppState.match(/inactive|background/) && nextAppState === 'active') {
|
|
10
|
+
onForeground?.();
|
|
11
|
+
} else if (prevAppState === 'active' && nextAppState.match(/inactive|background/)) {
|
|
12
|
+
onBackground?.();
|
|
16
13
|
}
|
|
17
|
-
|
|
14
|
+
appStateRef.current = nextAppState;
|
|
18
15
|
},
|
|
19
|
-
[onBackground, onForeground
|
|
16
|
+
[onBackground, onForeground],
|
|
20
17
|
);
|
|
21
18
|
|
|
22
19
|
useEffect(() => {
|