stream-chat-react-native-core 5.4.2-beta.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/NetworkDownIndicator.js +14 -6
- package/lib/commonjs/components/MessageList/NetworkDownIndicator.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/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/NetworkDownIndicator.js +14 -6
- package/lib/module/components/MessageList/NetworkDownIndicator.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/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/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/NetworkDownIndicator.tsx +13 -10
- 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/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
|
+
});
|
|
@@ -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
|
};
|
|
@@ -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(() => {
|
package/src/version.json
CHANGED