@sendbird/uikit-react-native 3.10.3 → 3.11.1
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/README.md +9 -7
- package/lib/commonjs/components/ChannelInput/VoiceMessageInput.js +1 -1
- package/lib/commonjs/components/ChannelInput/VoiceMessageInput.js.map +1 -1
- package/lib/commonjs/components/ChannelInput/index.js +2 -25
- package/lib/commonjs/components/ChannelInput/index.js.map +1 -1
- package/lib/commonjs/hooks/useVoiceMessageInput.js +10 -2
- package/lib/commonjs/hooks/useVoiceMessageInput.js.map +1 -1
- package/lib/commonjs/platform/createFileService.native.js +1 -1
- package/lib/commonjs/platform/createFileService.native.js.map +1 -1
- package/lib/commonjs/platform/createMediaService.expo.js +83 -12
- package/lib/commonjs/platform/createMediaService.expo.js.map +1 -1
- package/lib/commonjs/platform/createPlayerService.expo.js +214 -113
- package/lib/commonjs/platform/createPlayerService.expo.js.map +1 -1
- package/lib/commonjs/platform/createPlayerService.native.js +191 -114
- package/lib/commonjs/platform/createPlayerService.native.js.map +1 -1
- package/lib/commonjs/platform/createRecorderService.expo.js +248 -127
- package/lib/commonjs/platform/createRecorderService.expo.js.map +1 -1
- package/lib/commonjs/platform/createRecorderService.native.js +212 -129
- package/lib/commonjs/platform/createRecorderService.native.js.map +1 -1
- package/lib/commonjs/platform/types.js.map +1 -1
- package/lib/commonjs/utils/expoBackwardUtils.js +23 -0
- package/lib/commonjs/utils/expoBackwardUtils.js.map +1 -1
- package/lib/commonjs/utils/expoPermissionGranted.js.map +1 -1
- package/lib/commonjs/version.js +1 -1
- package/lib/commonjs/version.js.map +1 -1
- package/lib/module/components/ChannelInput/VoiceMessageInput.js +1 -1
- package/lib/module/components/ChannelInput/VoiceMessageInput.js.map +1 -1
- package/lib/module/components/ChannelInput/index.js +3 -26
- package/lib/module/components/ChannelInput/index.js.map +1 -1
- package/lib/module/hooks/useVoiceMessageInput.js +10 -2
- package/lib/module/hooks/useVoiceMessageInput.js.map +1 -1
- package/lib/module/platform/createFileService.native.js +1 -1
- package/lib/module/platform/createFileService.native.js.map +1 -1
- package/lib/module/platform/createMediaService.expo.js +82 -13
- package/lib/module/platform/createMediaService.expo.js.map +1 -1
- package/lib/module/platform/createPlayerService.expo.js +214 -113
- package/lib/module/platform/createPlayerService.expo.js.map +1 -1
- package/lib/module/platform/createPlayerService.native.js +191 -114
- package/lib/module/platform/createPlayerService.native.js.map +1 -1
- package/lib/module/platform/createRecorderService.expo.js +249 -128
- package/lib/module/platform/createRecorderService.expo.js.map +1 -1
- package/lib/module/platform/createRecorderService.native.js +212 -129
- package/lib/module/platform/createRecorderService.native.js.map +1 -1
- package/lib/module/platform/types.js.map +1 -1
- package/lib/module/utils/expoBackwardUtils.js +23 -0
- package/lib/module/utils/expoBackwardUtils.js.map +1 -1
- package/lib/module/utils/expoPermissionGranted.js.map +1 -1
- package/lib/module/version.js +1 -1
- package/lib/module/version.js.map +1 -1
- package/lib/typescript/src/containers/SendbirdUIKitContainer.d.ts +1 -1
- package/lib/typescript/src/platform/createMediaService.expo.d.ts +2 -2
- package/lib/typescript/src/platform/createPlayerService.expo.d.ts +2 -2
- package/lib/typescript/src/platform/createPlayerService.native.d.ts +5 -3
- package/lib/typescript/src/platform/createRecorderService.expo.d.ts +2 -2
- package/lib/typescript/src/platform/createRecorderService.native.d.ts +5 -3
- package/lib/typescript/src/platform/types.d.ts +4 -0
- package/lib/typescript/src/utils/expoBackwardUtils.d.ts +10 -0
- package/lib/typescript/src/utils/expoPermissionGranted.d.ts +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +29 -5
- package/src/components/ChannelInput/VoiceMessageInput.tsx +1 -1
- package/src/components/ChannelInput/index.tsx +6 -36
- package/src/hooks/useVoiceMessageInput.ts +7 -2
- package/src/platform/createFileService.native.ts +1 -1
- package/src/platform/createMediaService.expo.tsx +87 -9
- package/src/platform/createPlayerService.expo.tsx +242 -109
- package/src/platform/createPlayerService.native.tsx +237 -113
- package/src/platform/createRecorderService.expo.tsx +268 -107
- package/src/platform/createRecorderService.native.tsx +240 -118
- package/src/platform/types.ts +5 -0
- package/src/utils/expoBackwardUtils.ts +29 -0
- package/src/utils/expoPermissionGranted.ts +3 -1
- package/src/version.ts +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { ExpoAudioModule } from '../utils/expoBackwardUtils';
|
|
2
2
|
import type { RecorderServiceInterface } from './types';
|
|
3
3
|
type Modules = {
|
|
4
|
-
avModule:
|
|
4
|
+
avModule: ExpoAudioModule;
|
|
5
5
|
};
|
|
6
6
|
declare const createExpoRecorderService: ({ avModule }: Modules) => RecorderServiceInterface;
|
|
7
7
|
export default createExpoRecorderService;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import type * as LegacyModule from 'react-native-audio-recorder-player';
|
|
2
|
+
import type * as NitroSoundOrLegacyV4Module from 'react-native-nitro-sound';
|
|
2
3
|
import * as Permissions from 'react-native-permissions';
|
|
3
4
|
import type { RecorderServiceInterface } from './types';
|
|
5
|
+
export type AudioRecorderModule = typeof LegacyModule | typeof NitroSoundOrLegacyV4Module;
|
|
4
6
|
type Modules = {
|
|
5
|
-
audioRecorderModule:
|
|
7
|
+
audioRecorderModule: AudioRecorderModule;
|
|
6
8
|
permissionModule: typeof Permissions;
|
|
7
9
|
};
|
|
8
|
-
declare const createNativeRecorderService: (
|
|
10
|
+
declare const createNativeRecorderService: (modules: Modules) => RecorderServiceInterface;
|
|
9
11
|
export default createNativeRecorderService;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import type * as ExpoAudio from 'expo-audio';
|
|
2
|
+
import type * as ExpoAV from 'expo-av';
|
|
1
3
|
import type * as ExpoDocumentPicker from 'expo-document-picker';
|
|
2
4
|
import type * as ExpoFs from 'expo-file-system';
|
|
3
5
|
import type * as ExpoImagePicker from 'expo-image-picker';
|
|
6
|
+
import type * as ExpoVideo from 'expo-video';
|
|
4
7
|
import type { FilePickerResponse } from '../platform/types';
|
|
5
8
|
declare const expoBackwardUtils: {
|
|
6
9
|
imagePicker: {
|
|
@@ -11,6 +14,13 @@ declare const expoBackwardUtils: {
|
|
|
11
14
|
isCanceled(result: ExpoDocumentPicker.DocumentPickerResult): boolean;
|
|
12
15
|
toFilePickerResponses(result: ExpoDocumentPicker.DocumentPickerResult): Promise<FilePickerResponse[]>;
|
|
13
16
|
};
|
|
17
|
+
expoAV: {
|
|
18
|
+
isLegacyAVModule(module: ExpoAudioModule | ExpoVideoModule): module is typeof ExpoAV;
|
|
19
|
+
isAudioModule(module: ExpoAudioModule): module is typeof ExpoAudio;
|
|
20
|
+
isVideoModule(module: ExpoVideoModule): module is typeof ExpoVideo;
|
|
21
|
+
};
|
|
14
22
|
toFileSize(info: ExpoFs.FileInfo): number;
|
|
15
23
|
};
|
|
24
|
+
export type ExpoAudioModule = typeof ExpoAV | typeof ExpoAudio;
|
|
25
|
+
export type ExpoVideoModule = typeof ExpoAV | typeof ExpoVideo;
|
|
16
26
|
export default expoBackwardUtils;
|
|
@@ -7,7 +7,7 @@ export interface ExpoPermissionResponse {
|
|
|
7
7
|
export interface ExpoMediaLibraryPermissionResponse extends ExpoPermissionResponse {
|
|
8
8
|
accessPrivileges?: 'all' | 'limited' | 'none';
|
|
9
9
|
}
|
|
10
|
-
export interface ExpoPushPermissionResponse extends ExpoPermissionResponse, NotificationPermissionsStatus {
|
|
10
|
+
export interface ExpoPushPermissionResponse extends Omit<ExpoPermissionResponse, 'status'>, NotificationPermissionsStatus {
|
|
11
11
|
}
|
|
12
12
|
declare const expoPermissionGranted: (stats: Array<ExpoMediaLibraryPermissionResponse | ExpoPushPermissionResponse | ExpoPermissionResponse>, limitedCallback?: () => void) => boolean;
|
|
13
13
|
export default expoPermissionGranted;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const VERSION = "3.
|
|
1
|
+
declare const VERSION = "3.11.1";
|
|
2
2
|
export default VERSION;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendbird/uikit-react-native",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.11.1",
|
|
4
4
|
"description": "Sendbird UIKit for React Native: A feature-rich and customizable chat UI kit with messaging, channel management, and user authentication.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sendbird",
|
|
@@ -60,10 +60,10 @@
|
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@openspacelabs/react-native-zoomable-view": "^2.1.5",
|
|
63
|
-
"@sendbird/uikit-chat-hooks": "3.
|
|
64
|
-
"@sendbird/uikit-react-native-foundation": "3.
|
|
63
|
+
"@sendbird/uikit-chat-hooks": "3.11.1",
|
|
64
|
+
"@sendbird/uikit-react-native-foundation": "3.11.1",
|
|
65
65
|
"@sendbird/uikit-tools": "0.0.15",
|
|
66
|
-
"@sendbird/uikit-utils": "3.
|
|
66
|
+
"@sendbird/uikit-utils": "3.11.1"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
|
@@ -77,6 +77,8 @@
|
|
|
77
77
|
"@types/react": "*",
|
|
78
78
|
"@types/react-native": "*",
|
|
79
79
|
"date-fns": ">=2.28.0",
|
|
80
|
+
"expo": "^54.0.12",
|
|
81
|
+
"expo-audio": "^1.0.13",
|
|
80
82
|
"expo-av": "^13.2.1",
|
|
81
83
|
"expo-clipboard": "^4.1.2",
|
|
82
84
|
"expo-document-picker": "^11.5.3",
|
|
@@ -85,6 +87,7 @@
|
|
|
85
87
|
"expo-image-picker": "^14.1.1",
|
|
86
88
|
"expo-media-library": "^16.0.0",
|
|
87
89
|
"expo-notifications": "^0.18.1",
|
|
90
|
+
"expo-video": "^3.0.11",
|
|
88
91
|
"expo-video-thumbnails": "^7.2.1",
|
|
89
92
|
"glob": "^7.2.0",
|
|
90
93
|
"inquirer": "^8.2.0",
|
|
@@ -98,6 +101,8 @@
|
|
|
98
101
|
"react-native-file-access": "^3.1.0",
|
|
99
102
|
"react-native-image-picker": "^7.1.2",
|
|
100
103
|
"react-native-mmkv": "^2.12.2",
|
|
104
|
+
"react-native-nitro-modules": "^0.29.4",
|
|
105
|
+
"react-native-nitro-sound": "^0.2.0",
|
|
101
106
|
"react-native-permissions": "^3.10.1",
|
|
102
107
|
"react-native-safe-area-context": "^4.10.8",
|
|
103
108
|
"react-native-video": "^6.3.0",
|
|
@@ -115,6 +120,7 @@
|
|
|
115
120
|
"@sendbird/react-native-scrollview-enhancer": "*",
|
|
116
121
|
"@sendbird/uikit-tools": ">=0.0.10",
|
|
117
122
|
"date-fns": ">=2.28.0",
|
|
123
|
+
"expo-audio": ">=1.0.0",
|
|
118
124
|
"expo-av": ">=12.0.4",
|
|
119
125
|
"expo-clipboard": ">=2.1.1",
|
|
120
126
|
"expo-document-picker": ">=10.1.3",
|
|
@@ -123,6 +129,7 @@
|
|
|
123
129
|
"expo-image-picker": ">=12.0.2",
|
|
124
130
|
"expo-media-library": ">=16.0.0",
|
|
125
131
|
"expo-notifications": ">=0.14.1",
|
|
132
|
+
"expo-video": ">=3.0.0",
|
|
126
133
|
"expo-video-thumbnails": ">=6.4.0",
|
|
127
134
|
"react": ">=17.0.2",
|
|
128
135
|
"react-native": ">=0.65.0",
|
|
@@ -132,6 +139,8 @@
|
|
|
132
139
|
"react-native-file-access": ">=2.4.3",
|
|
133
140
|
"react-native-image-picker": ">=4.7.1",
|
|
134
141
|
"react-native-mmkv": ">=2.0.0",
|
|
142
|
+
"react-native-nitro-modules": "*",
|
|
143
|
+
"react-native-nitro-sound": ">=0.2.0",
|
|
135
144
|
"react-native-permissions": ">=3.6.0",
|
|
136
145
|
"react-native-safe-area-context": ">=3.3.2",
|
|
137
146
|
"react-native-video": ">=5.2.0"
|
|
@@ -161,6 +170,12 @@
|
|
|
161
170
|
"expo-av": {
|
|
162
171
|
"optional": true
|
|
163
172
|
},
|
|
173
|
+
"expo-audio": {
|
|
174
|
+
"optional": true
|
|
175
|
+
},
|
|
176
|
+
"expo-video": {
|
|
177
|
+
"optional": true
|
|
178
|
+
},
|
|
164
179
|
"expo-clipboard": {
|
|
165
180
|
"optional": true
|
|
166
181
|
},
|
|
@@ -202,6 +217,15 @@
|
|
|
202
217
|
},
|
|
203
218
|
"react-native-video": {
|
|
204
219
|
"optional": true
|
|
220
|
+
},
|
|
221
|
+
"react-native-audio-recorder-player": {
|
|
222
|
+
"optional": true
|
|
223
|
+
},
|
|
224
|
+
"react-native-nitro-modules": {
|
|
225
|
+
"optional": true
|
|
226
|
+
},
|
|
227
|
+
"react-native-nitro-sound": {
|
|
228
|
+
"optional": true
|
|
205
229
|
}
|
|
206
230
|
},
|
|
207
231
|
"react-native-builder-bob": {
|
|
@@ -218,5 +242,5 @@
|
|
|
218
242
|
]
|
|
219
243
|
]
|
|
220
244
|
},
|
|
221
|
-
"gitHead": "
|
|
245
|
+
"gitHead": "16f1f54e8c442492e79a9591ae32203d5db55c2b"
|
|
222
246
|
}
|
|
@@ -75,7 +75,7 @@ const VoiceMessageInput = ({ onClose, onSend }: VoiceMessageInputProps) => {
|
|
|
75
75
|
break;
|
|
76
76
|
}
|
|
77
77
|
} catch (error) {
|
|
78
|
-
Logger.warn('Failed to run voice message action.', state);
|
|
78
|
+
Logger.warn('Failed to run voice message action.', state, error);
|
|
79
79
|
}
|
|
80
80
|
};
|
|
81
81
|
const renderActionIcon = () => {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import {
|
|
3
|
-
Keyboard,
|
|
4
3
|
KeyboardAvoidingView,
|
|
5
4
|
Platform,
|
|
6
5
|
StyleProp,
|
|
@@ -95,12 +94,7 @@ export type ChannelInputProps = {
|
|
|
95
94
|
};
|
|
96
95
|
|
|
97
96
|
const AUTO_FOCUS = Platform.select({ ios: false, android: true, default: false });
|
|
98
|
-
const
|
|
99
|
-
const KEYBOARD_AVOID_VIEW_BEHAVIOR = Platform.select({
|
|
100
|
-
ios: 'padding' as const,
|
|
101
|
-
android: isAndroidApi35Plus ? ('padding' as const) : undefined,
|
|
102
|
-
default: undefined,
|
|
103
|
-
});
|
|
97
|
+
const KEYBOARD_AVOID_VIEW_BEHAVIOR = Platform.select({ ios: 'padding' as const, default: undefined });
|
|
104
98
|
|
|
105
99
|
// FIXME(iOS): Dynamic style does not work properly when typing the CJK. (https://github.com/facebook/react-native/issues/26107)
|
|
106
100
|
// To workaround temporarily, change the key for re-mount the component.
|
|
@@ -115,16 +109,6 @@ const ChannelInput = (props: ChannelInputProps) => {
|
|
|
115
109
|
|
|
116
110
|
const safeArea = useSafeAreaPadding(['top', 'left', 'right', 'bottom']);
|
|
117
111
|
|
|
118
|
-
// Android API 35+ keyboard avoidance handling
|
|
119
|
-
/**
|
|
120
|
-
* Android API 35+ introduced edge-to-edge layouts, which changed how keyboard avoidance should be handled.
|
|
121
|
-
* For API 35+, the system manages insets automatically, so we use the provided keyboardAvoidOffset directly.
|
|
122
|
-
* For older Android versions, we manually subtract the safe area bottom padding to avoid overlapping with system UI.
|
|
123
|
-
* See: https://developer.android.com/develop/ui/views/layout/edge-to-edge
|
|
124
|
-
*/
|
|
125
|
-
const keyboardVerticalOffset = isAndroidApi35Plus
|
|
126
|
-
? keyboardAvoidOffset
|
|
127
|
-
: -safeArea.paddingBottom + keyboardAvoidOffset;
|
|
128
112
|
const { colors, typography } = useUIKitTheme();
|
|
129
113
|
const { sbOptions, mentionManager } = useSendbirdChat();
|
|
130
114
|
|
|
@@ -161,30 +145,16 @@ const ChannelInput = (props: ChannelInputProps) => {
|
|
|
161
145
|
onChangeText(replace(text, searchStringRange.start, searchStringRange.end, mentionedMessageText), { user, range });
|
|
162
146
|
};
|
|
163
147
|
|
|
164
|
-
const [keyboardShown, setKeyboardShown] = useState(false);
|
|
165
|
-
|
|
166
|
-
useEffect(() => {
|
|
167
|
-
const keyboardDidShow = () => setKeyboardShown(true);
|
|
168
|
-
const keyboardDidHide = () => setKeyboardShown(false);
|
|
169
|
-
|
|
170
|
-
const showSubscription = Keyboard.addListener('keyboardDidShow', keyboardDidShow);
|
|
171
|
-
const hideSubscription = Keyboard.addListener('keyboardDidHide', keyboardDidHide);
|
|
172
|
-
|
|
173
|
-
return () => {
|
|
174
|
-
showSubscription.remove();
|
|
175
|
-
hideSubscription.remove();
|
|
176
|
-
};
|
|
177
|
-
}, []);
|
|
178
|
-
|
|
179
|
-
const shouldShowSafeAreaBottom = !isAndroidApi35Plus || (isAndroidApi35Plus && !keyboardShown);
|
|
180
|
-
|
|
181
148
|
if (!props.shouldRenderInput) {
|
|
182
149
|
return <SafeAreaBottom height={safeArea.paddingBottom} />;
|
|
183
150
|
}
|
|
184
151
|
|
|
185
152
|
return (
|
|
186
153
|
<>
|
|
187
|
-
<KeyboardAvoidingView
|
|
154
|
+
<KeyboardAvoidingView
|
|
155
|
+
keyboardVerticalOffset={-safeArea.paddingBottom + keyboardAvoidOffset}
|
|
156
|
+
behavior={KEYBOARD_AVOID_VIEW_BEHAVIOR}
|
|
157
|
+
>
|
|
188
158
|
<View
|
|
189
159
|
style={{
|
|
190
160
|
paddingStart: safeArea.paddingStart,
|
|
@@ -224,7 +194,7 @@ const ChannelInput = (props: ChannelInputProps) => {
|
|
|
224
194
|
/>
|
|
225
195
|
)}
|
|
226
196
|
</View>
|
|
227
|
-
|
|
197
|
+
<SafeAreaBottom height={safeArea.paddingBottom} />
|
|
228
198
|
</View>
|
|
229
199
|
</KeyboardAvoidingView>
|
|
230
200
|
{mentionAvailable && props.SuggestedMentionList && (
|
|
@@ -161,8 +161,10 @@ const useVoiceMessageInput = ({ onSend, onClose }: Props): VoiceMessageInputResu
|
|
|
161
161
|
setVoiceMessageRecordingPath({ recordFilePath: recorderService.uri, uri: recorderService.uri });
|
|
162
162
|
}
|
|
163
163
|
} else {
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
const recordFilePath = fileService.createRecordFilePath(recorderService.options.extension);
|
|
165
|
+
const convertedRecordPath = recorderService.convertRecordPath(recordFilePath.recordFilePath);
|
|
166
|
+
setVoiceMessageRecordingPath({ ...recordFilePath, recordFilePath: convertedRecordPath });
|
|
167
|
+
await recorderService.record(convertedRecordPath);
|
|
166
168
|
}
|
|
167
169
|
}
|
|
168
170
|
},
|
|
@@ -225,6 +227,9 @@ const useVoiceMessageInput = ({ onSend, onClose }: Props): VoiceMessageInputResu
|
|
|
225
227
|
matchesOneOf(status, ['recording', 'recording_completed', 'playing', 'playing_paused']) &&
|
|
226
228
|
recordingPath.current
|
|
227
229
|
) {
|
|
230
|
+
if (status === 'recording') {
|
|
231
|
+
await recorderService.stop();
|
|
232
|
+
}
|
|
228
233
|
const voiceFile = getVoiceMessageFileObject(recordingPath.current.uri, recorderService.options.extension);
|
|
229
234
|
onSend(voiceFile, Math.floor(recordingTime.currentTime));
|
|
230
235
|
await clear();
|
|
@@ -1,34 +1,112 @@
|
|
|
1
1
|
import type * as ExpoAV from 'expo-av';
|
|
2
2
|
import type * as ExpoFS from 'expo-file-system';
|
|
3
3
|
import type * as ExpoImageManipulator from 'expo-image-manipulator';
|
|
4
|
+
import type { EventSubscription } from 'expo-modules-core';
|
|
5
|
+
import type * as ExpoVideo from 'expo-video';
|
|
6
|
+
import type { StatusChangeEventPayload } from 'expo-video';
|
|
4
7
|
import type * as ExpoVideoThumbnail from 'expo-video-thumbnails';
|
|
5
|
-
import React from 'react';
|
|
8
|
+
import React, { useEffect } from 'react';
|
|
6
9
|
|
|
7
|
-
import { getDownscaleSize } from '@sendbird/uikit-utils';
|
|
10
|
+
import { Logger, getDownscaleSize } from '@sendbird/uikit-utils';
|
|
8
11
|
|
|
9
12
|
import SBUUtils from '../libs/SBUUtils';
|
|
10
13
|
import expoBackwardUtils from '../utils/expoBackwardUtils';
|
|
11
|
-
import type {
|
|
14
|
+
import type { ExpoVideoModule } from '../utils/expoBackwardUtils';
|
|
15
|
+
import type { MediaServiceInterface, VideoProps } from './types';
|
|
12
16
|
|
|
13
17
|
type Modules = {
|
|
14
|
-
avModule:
|
|
18
|
+
avModule: ExpoVideoModule;
|
|
15
19
|
thumbnailModule: typeof ExpoVideoThumbnail;
|
|
16
20
|
imageManipulator: typeof ExpoImageManipulator;
|
|
17
21
|
fsModule: typeof ExpoFS;
|
|
18
22
|
};
|
|
19
23
|
|
|
24
|
+
interface VideoModuleAdapter {
|
|
25
|
+
VideoComponent: React.ComponentType<VideoProps>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class LegacyExpoAVVideoAdapter implements VideoModuleAdapter {
|
|
29
|
+
private readonly avModule: typeof ExpoAV;
|
|
30
|
+
constructor(avModule: typeof ExpoAV) {
|
|
31
|
+
this.avModule = avModule;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
VideoComponent = ({ source, resizeMode, onLoad, ...props }: VideoProps) => {
|
|
35
|
+
// FIXME: type error https://github.com/expo/expo/issues/17101
|
|
36
|
+
// @ts-ignore
|
|
37
|
+
return <this.avModule.Video {...props} source={source} resizeMode={resizeMode} onLoad={onLoad} useNativeControls />;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class ExpoVideoAdapter implements VideoModuleAdapter {
|
|
42
|
+
constructor(private readonly _videoModule: typeof ExpoVideo) {}
|
|
43
|
+
|
|
44
|
+
VideoComponent = ({ source, resizeMode, onLoad, ...props }: VideoProps) => {
|
|
45
|
+
const player = this._videoModule.useVideoPlayer(source);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (onLoad && player) {
|
|
49
|
+
let subscription: EventSubscription | null = null;
|
|
50
|
+
try {
|
|
51
|
+
subscription = player.addListener('statusChange', (eventData: StatusChangeEventPayload) => {
|
|
52
|
+
const { status, error } = eventData;
|
|
53
|
+
if (status === 'readyToPlay' && !error) {
|
|
54
|
+
onLoad();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
} catch (error) {
|
|
58
|
+
const timeout = setTimeout(() => onLoad(), 300);
|
|
59
|
+
return () => clearTimeout(timeout);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return () => {
|
|
63
|
+
if (subscription) {
|
|
64
|
+
subscription.remove();
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}, [onLoad, player]);
|
|
70
|
+
|
|
71
|
+
const getContentFit = (mode: typeof resizeMode): 'cover' | 'contain' | 'fill' => {
|
|
72
|
+
switch (mode) {
|
|
73
|
+
case 'cover':
|
|
74
|
+
return 'cover';
|
|
75
|
+
case 'contain':
|
|
76
|
+
return 'contain';
|
|
77
|
+
case 'stretch':
|
|
78
|
+
return 'fill';
|
|
79
|
+
default:
|
|
80
|
+
return 'contain';
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return React.createElement(this._videoModule.VideoView, {
|
|
85
|
+
...props,
|
|
86
|
+
player,
|
|
87
|
+
contentFit: getContentFit(resizeMode),
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
20
92
|
const createExpoMediaService = ({
|
|
21
93
|
avModule,
|
|
22
94
|
thumbnailModule,
|
|
23
95
|
imageManipulator,
|
|
24
96
|
fsModule,
|
|
25
97
|
}: Modules): MediaServiceInterface => {
|
|
98
|
+
if (expoBackwardUtils.expoAV.isLegacyAVModule(avModule)) {
|
|
99
|
+
Logger.warn(
|
|
100
|
+
'[MediaService.Expo] expo-av is deprecated and will be removed in Expo 54. Please migrate to expo-video.',
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const videoAdapter = expoBackwardUtils.expoAV.isVideoModule(avModule)
|
|
105
|
+
? new ExpoVideoAdapter(avModule)
|
|
106
|
+
: new LegacyExpoAVVideoAdapter(avModule);
|
|
107
|
+
|
|
26
108
|
return {
|
|
27
|
-
VideoComponent
|
|
28
|
-
// FIXME: type error https://github.com/expo/expo/issues/17101
|
|
29
|
-
// @ts-ignore
|
|
30
|
-
return <avModule.Video {...props} source={source} resizeMode={resizeMode} onLoad={onLoad} useNativeControls />;
|
|
31
|
-
},
|
|
109
|
+
VideoComponent: videoAdapter.VideoComponent,
|
|
32
110
|
async getVideoThumbnail({ url, quality, timeMills }) {
|
|
33
111
|
try {
|
|
34
112
|
const { uri } = await thumbnailModule.getThumbnailAsync(url, { quality, time: timeMills });
|