@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.
Files changed (73) hide show
  1. package/README.md +9 -7
  2. package/lib/commonjs/components/ChannelInput/VoiceMessageInput.js +1 -1
  3. package/lib/commonjs/components/ChannelInput/VoiceMessageInput.js.map +1 -1
  4. package/lib/commonjs/components/ChannelInput/index.js +2 -25
  5. package/lib/commonjs/components/ChannelInput/index.js.map +1 -1
  6. package/lib/commonjs/hooks/useVoiceMessageInput.js +10 -2
  7. package/lib/commonjs/hooks/useVoiceMessageInput.js.map +1 -1
  8. package/lib/commonjs/platform/createFileService.native.js +1 -1
  9. package/lib/commonjs/platform/createFileService.native.js.map +1 -1
  10. package/lib/commonjs/platform/createMediaService.expo.js +83 -12
  11. package/lib/commonjs/platform/createMediaService.expo.js.map +1 -1
  12. package/lib/commonjs/platform/createPlayerService.expo.js +214 -113
  13. package/lib/commonjs/platform/createPlayerService.expo.js.map +1 -1
  14. package/lib/commonjs/platform/createPlayerService.native.js +191 -114
  15. package/lib/commonjs/platform/createPlayerService.native.js.map +1 -1
  16. package/lib/commonjs/platform/createRecorderService.expo.js +248 -127
  17. package/lib/commonjs/platform/createRecorderService.expo.js.map +1 -1
  18. package/lib/commonjs/platform/createRecorderService.native.js +212 -129
  19. package/lib/commonjs/platform/createRecorderService.native.js.map +1 -1
  20. package/lib/commonjs/platform/types.js.map +1 -1
  21. package/lib/commonjs/utils/expoBackwardUtils.js +23 -0
  22. package/lib/commonjs/utils/expoBackwardUtils.js.map +1 -1
  23. package/lib/commonjs/utils/expoPermissionGranted.js.map +1 -1
  24. package/lib/commonjs/version.js +1 -1
  25. package/lib/commonjs/version.js.map +1 -1
  26. package/lib/module/components/ChannelInput/VoiceMessageInput.js +1 -1
  27. package/lib/module/components/ChannelInput/VoiceMessageInput.js.map +1 -1
  28. package/lib/module/components/ChannelInput/index.js +3 -26
  29. package/lib/module/components/ChannelInput/index.js.map +1 -1
  30. package/lib/module/hooks/useVoiceMessageInput.js +10 -2
  31. package/lib/module/hooks/useVoiceMessageInput.js.map +1 -1
  32. package/lib/module/platform/createFileService.native.js +1 -1
  33. package/lib/module/platform/createFileService.native.js.map +1 -1
  34. package/lib/module/platform/createMediaService.expo.js +82 -13
  35. package/lib/module/platform/createMediaService.expo.js.map +1 -1
  36. package/lib/module/platform/createPlayerService.expo.js +214 -113
  37. package/lib/module/platform/createPlayerService.expo.js.map +1 -1
  38. package/lib/module/platform/createPlayerService.native.js +191 -114
  39. package/lib/module/platform/createPlayerService.native.js.map +1 -1
  40. package/lib/module/platform/createRecorderService.expo.js +249 -128
  41. package/lib/module/platform/createRecorderService.expo.js.map +1 -1
  42. package/lib/module/platform/createRecorderService.native.js +212 -129
  43. package/lib/module/platform/createRecorderService.native.js.map +1 -1
  44. package/lib/module/platform/types.js.map +1 -1
  45. package/lib/module/utils/expoBackwardUtils.js +23 -0
  46. package/lib/module/utils/expoBackwardUtils.js.map +1 -1
  47. package/lib/module/utils/expoPermissionGranted.js.map +1 -1
  48. package/lib/module/version.js +1 -1
  49. package/lib/module/version.js.map +1 -1
  50. package/lib/typescript/src/containers/SendbirdUIKitContainer.d.ts +1 -1
  51. package/lib/typescript/src/platform/createMediaService.expo.d.ts +2 -2
  52. package/lib/typescript/src/platform/createPlayerService.expo.d.ts +2 -2
  53. package/lib/typescript/src/platform/createPlayerService.native.d.ts +5 -3
  54. package/lib/typescript/src/platform/createRecorderService.expo.d.ts +2 -2
  55. package/lib/typescript/src/platform/createRecorderService.native.d.ts +5 -3
  56. package/lib/typescript/src/platform/types.d.ts +4 -0
  57. package/lib/typescript/src/utils/expoBackwardUtils.d.ts +10 -0
  58. package/lib/typescript/src/utils/expoPermissionGranted.d.ts +1 -1
  59. package/lib/typescript/src/version.d.ts +1 -1
  60. package/package.json +29 -5
  61. package/src/components/ChannelInput/VoiceMessageInput.tsx +1 -1
  62. package/src/components/ChannelInput/index.tsx +6 -36
  63. package/src/hooks/useVoiceMessageInput.ts +7 -2
  64. package/src/platform/createFileService.native.ts +1 -1
  65. package/src/platform/createMediaService.expo.tsx +87 -9
  66. package/src/platform/createPlayerService.expo.tsx +242 -109
  67. package/src/platform/createPlayerService.native.tsx +237 -113
  68. package/src/platform/createRecorderService.expo.tsx +268 -107
  69. package/src/platform/createRecorderService.native.tsx +240 -118
  70. package/src/platform/types.ts +5 -0
  71. package/src/utils/expoBackwardUtils.ts +29 -0
  72. package/src/utils/expoPermissionGranted.ts +3 -1
  73. package/src/version.ts +1 -1
@@ -1,47 +1,102 @@
1
- import * as ExpoAV from 'expo-av';
2
- import type { RecordingOptions } from 'expo-av/build/Audio/Recording.types';
1
+ import type * as ExpoAudio from 'expo-audio';
2
+ import type * as ExpoAV from 'expo-av';
3
3
  import { Platform } from 'react-native';
4
4
 
5
- import { matchesOneOf, sleep } from '@sendbird/uikit-utils';
5
+ import { Logger, matchesOneOf, sleep } from '@sendbird/uikit-utils';
6
6
 
7
7
  import VoiceMessageConfig from '../libs/VoiceMessageConfig';
8
+ import expoBackwardUtils from '../utils/expoBackwardUtils';
9
+ import type { ExpoAudioModule } from '../utils/expoBackwardUtils';
8
10
  import expoPermissionGranted from '../utils/expoPermissionGranted';
9
11
  import type { RecorderServiceInterface, Unsubscribe } from './types';
10
12
 
11
13
  type RecordingListener = Parameters<RecorderServiceInterface['addRecordingListener']>[number];
12
14
  type StateListener = Parameters<RecorderServiceInterface['addStateListener']>[number];
13
15
  type Modules = {
14
- avModule: typeof ExpoAV;
16
+ avModule: ExpoAudioModule;
15
17
  };
16
- const createExpoRecorderService = ({ avModule }: Modules): RecorderServiceInterface => {
17
- class VoiceRecorder implements RecorderServiceInterface {
18
- public uri: RecorderServiceInterface['uri'] = undefined;
19
- public state: RecorderServiceInterface['state'] = 'idle';
20
- public options: RecorderServiceInterface['options'] = {
21
- minDuration: VoiceMessageConfig.DEFAULT.RECORDER.MIN_DURATION,
22
- maxDuration: VoiceMessageConfig.DEFAULT.RECORDER.MAX_DURATION,
23
- extension: VoiceMessageConfig.DEFAULT.RECORDER.EXTENSION,
24
- };
25
18
 
26
- // NOTE: In Android, even when startRecorder() is awaited, if stop() is executed immediately afterward, an error occurs
27
- private _recordStartedAt = 0;
28
- private _getRecorderStopSafeBuffer = () => {
29
- const minWaitingTime = 500;
30
- const elapsedTime = Date.now() - this._recordStartedAt;
31
- if (elapsedTime > minWaitingTime) return 0;
32
- else return minWaitingTime - elapsedTime;
19
+ interface AudioRecorderAdapter {
20
+ requestPermission(): Promise<boolean>;
21
+ record(): Promise<void>;
22
+ stop(): Promise<void>;
23
+ reset(): Promise<void>;
24
+ addRecordingListener(callback: RecordingListener): Unsubscribe;
25
+ addStateListener(callback: StateListener): Unsubscribe;
26
+ convertRecordPath(uri: string): string;
27
+ readonly state: RecorderServiceInterface['state'];
28
+ readonly options: RecorderServiceInterface['options'];
29
+ uri?: string;
30
+ }
31
+
32
+ abstract class BaseAudioRecorderAdapter implements AudioRecorderAdapter {
33
+ public uri: RecorderServiceInterface['uri'] = undefined;
34
+ public state: RecorderServiceInterface['state'] = 'idle';
35
+ public options: RecorderServiceInterface['options'] = {
36
+ minDuration: VoiceMessageConfig.DEFAULT.RECORDER.MIN_DURATION,
37
+ maxDuration: VoiceMessageConfig.DEFAULT.RECORDER.MAX_DURATION,
38
+ extension: VoiceMessageConfig.DEFAULT.RECORDER.EXTENSION,
39
+ };
40
+
41
+ protected readonly _audioSettings = {
42
+ sampleRate: VoiceMessageConfig.DEFAULT.RECORDER.SAMPLE_RATE,
43
+ bitRate: VoiceMessageConfig.DEFAULT.RECORDER.BIT_RATE,
44
+ numberOfChannels: VoiceMessageConfig.DEFAULT.RECORDER.CHANNELS,
45
+ // encoding: mpeg4_aac
46
+ };
47
+ protected readonly _recordingSubscribers = new Set<RecordingListener>();
48
+ protected readonly _stateSubscribers = new Set<StateListener>();
49
+
50
+ // NOTE: In Android, even when startRecorder() is awaited, if stop() is executed immediately afterward, an error occurs
51
+ protected _recordStartedAt = 0;
52
+ protected _getRecorderStopSafeBuffer = () => {
53
+ const minWaitingTime = 500;
54
+ const elapsedTime = Date.now() - this._recordStartedAt;
55
+ if (elapsedTime > minWaitingTime) return 0;
56
+ else return minWaitingTime - elapsedTime;
57
+ };
58
+ protected setState = (state: RecorderServiceInterface['state']) => {
59
+ this.state = state;
60
+ this._stateSubscribers.forEach((callback) => {
61
+ callback(state);
62
+ });
63
+ };
64
+
65
+ public addRecordingListener = (callback: RecordingListener): Unsubscribe => {
66
+ this._recordingSubscribers.add(callback);
67
+ return () => {
68
+ this._recordingSubscribers.delete(callback);
33
69
  };
70
+ };
34
71
 
35
- private _recorder = new avModule.Audio.Recording();
36
- private readonly _recordingSubscribers = new Set<RecordingListener>();
37
- private readonly _stateSubscribers = new Set<StateListener>();
38
- private readonly _audioSettings = {
39
- sampleRate: VoiceMessageConfig.DEFAULT.RECORDER.SAMPLE_RATE,
40
- bitRate: VoiceMessageConfig.DEFAULT.RECORDER.BIT_RATE,
41
- numberOfChannels: VoiceMessageConfig.DEFAULT.RECORDER.CHANNELS,
42
- // encoding: mpeg4_aac
72
+ public addStateListener = (callback: StateListener): Unsubscribe => {
73
+ this._stateSubscribers.add(callback);
74
+ return () => {
75
+ this._stateSubscribers.delete(callback);
43
76
  };
44
- private readonly _audioOptions: RecordingOptions = {
77
+ };
78
+
79
+ public convertRecordPath = (uri: string): string => {
80
+ return uri;
81
+ };
82
+
83
+ abstract requestPermission(): Promise<boolean>;
84
+ abstract record(): Promise<void>;
85
+ abstract stop(): Promise<void>;
86
+ abstract reset(): Promise<void>;
87
+ }
88
+
89
+ class LegacyExpoAVRecorderAdapter extends BaseAudioRecorderAdapter {
90
+ private readonly avModule: typeof ExpoAV;
91
+
92
+ private _recorder: ExpoAV.Audio.Recording;
93
+ private readonly _audioOptions: ExpoAV.Audio.RecordingOptions;
94
+
95
+ constructor(avModule: typeof ExpoAV) {
96
+ super();
97
+ this.avModule = avModule;
98
+ this._recorder = new avModule.Audio.Recording();
99
+ this._audioOptions = {
45
100
  android: {
46
101
  ...this._audioSettings,
47
102
  extension: `.${this.options.extension}`,
@@ -56,105 +111,211 @@ const createExpoRecorderService = ({ avModule }: Modules): RecorderServiceInterf
56
111
  },
57
112
  web: {},
58
113
  };
114
+ }
59
115
 
60
- private prepare = async () => {
61
- this.setState('preparing');
62
- if (Platform.OS === 'ios') {
63
- await avModule.Audio.setAudioModeAsync({ allowsRecordingIOS: true, playsInSilentModeIOS: true });
64
- }
116
+ private prepare = async () => {
117
+ this.setState('preparing');
118
+ if (Platform.OS === 'ios') {
119
+ await this.avModule.Audio.setAudioModeAsync({ allowsRecordingIOS: true, playsInSilentModeIOS: true });
120
+ }
65
121
 
66
- if (this._recorder._isDoneRecording) {
67
- this._recorder = new avModule.Audio.Recording();
122
+ if (this._recorder._isDoneRecording) {
123
+ this._recorder = new this.avModule.Audio.Recording();
124
+ }
125
+ this._recorder.setProgressUpdateInterval(100);
126
+ this._recorder.setOnRecordingStatusUpdate((status) => {
127
+ const completed = status.durationMillis >= this.options.maxDuration;
128
+ if (completed) this.stop();
129
+ if (status.isRecording) {
130
+ this._recordingSubscribers.forEach((callback) => {
131
+ callback({ currentTime: status.durationMillis, completed: completed });
132
+ });
68
133
  }
69
- this._recorder.setProgressUpdateInterval(100);
70
- this._recorder.setOnRecordingStatusUpdate((status) => {
71
- const completed = status.durationMillis >= this.options.maxDuration;
72
- if (completed) this.stop();
73
- if (status.isRecording) {
74
- this._recordingSubscribers.forEach((callback) => {
75
- callback({ currentTime: status.durationMillis, completed: completed });
76
- });
134
+ });
135
+ await this._recorder.prepareToRecordAsync(this._audioOptions);
136
+ };
137
+
138
+ public requestPermission = async (): Promise<boolean> => {
139
+ const status = await this.avModule.Audio.getPermissionsAsync();
140
+ if (expoPermissionGranted([status])) {
141
+ return true;
142
+ } else {
143
+ const status = await this.avModule.Audio.requestPermissionsAsync();
144
+ return expoPermissionGranted([status]);
145
+ }
146
+ };
147
+
148
+ public record = async (): Promise<void> => {
149
+ if (matchesOneOf(this.state, ['idle', 'completed'])) {
150
+ try {
151
+ await this.prepare();
152
+ await this._recorder.startAsync();
153
+
154
+ if (Platform.OS === 'android') {
155
+ this._recordStartedAt = Date.now();
77
156
  }
78
- });
79
- await this._recorder.prepareToRecordAsync(this._audioOptions);
80
- };
81
157
 
82
- private setState = (state: RecorderServiceInterface['state']) => {
83
- this.state = state;
84
- this._stateSubscribers.forEach((callback) => {
85
- callback(state);
86
- });
87
- };
158
+ const uri = this._recorder.getURI();
159
+ if (uri) this.uri = uri;
160
+ this.setState('recording');
161
+ } catch (e) {
162
+ this.setState('idle');
163
+ throw e;
164
+ }
165
+ }
166
+ };
88
167
 
89
- public requestPermission = async (): Promise<boolean> => {
90
- const status = await avModule.Audio.getPermissionsAsync();
91
- if (expoPermissionGranted([status])) {
92
- return true;
93
- } else {
94
- const status = await avModule.Audio.requestPermissionsAsync();
95
- return expoPermissionGranted([status]);
168
+ public stop = async (): Promise<void> => {
169
+ if (matchesOneOf(this.state, ['recording'])) {
170
+ if (Platform.OS === 'android') {
171
+ const buffer = this._getRecorderStopSafeBuffer();
172
+ if (buffer > 0) await sleep(buffer);
96
173
  }
97
- };
98
174
 
99
- public addRecordingListener = (callback: RecordingListener): Unsubscribe => {
100
- this._recordingSubscribers.add(callback);
101
- return () => {
102
- this._recordingSubscribers.delete(callback);
103
- };
104
- };
175
+ await this._recorder.stopAndUnloadAsync();
176
+ if (Platform.OS === 'ios') {
177
+ await this.avModule.Audio.setAudioModeAsync({ allowsRecordingIOS: false, playsInSilentModeIOS: false });
178
+ }
179
+ this.setState('completed');
180
+ }
181
+ };
105
182
 
106
- public addStateListener = (callback: StateListener): Unsubscribe => {
107
- this._stateSubscribers.add(callback);
108
- return () => {
109
- this._stateSubscribers.delete(callback);
110
- };
111
- };
183
+ public reset = async (): Promise<void> => {
184
+ await this.stop();
185
+ this.uri = undefined;
186
+ this._recordingSubscribers.clear();
187
+ this._recorder = new this.avModule.Audio.Recording();
188
+ this.setState('idle');
189
+ };
190
+ }
191
+
192
+ class ExpoAudioRecorderAdapter extends BaseAudioRecorderAdapter {
193
+ private readonly audioModule: typeof ExpoAudio;
194
+ private recorder: ExpoAudio.AudioRecorder | null = null;
195
+ private recordingUpdateInterval: NodeJS.Timeout | null = null;
196
+
197
+ constructor(audioModule: typeof ExpoAudio) {
198
+ super();
199
+ this.audioModule = audioModule;
200
+ }
201
+
202
+ private setListener = () => {
203
+ if (!this.recorder) return;
204
+
205
+ this.recordingUpdateInterval = setInterval(() => {
206
+ if (this.recorder && this.recorder.isRecording) {
207
+ const currentTime = this.recorder.currentTime * 1000;
208
+ const completed = currentTime >= this.options.maxDuration;
112
209
 
113
- public record = async (): Promise<void> => {
114
- if (matchesOneOf(this.state, ['idle', 'completed'])) {
115
- try {
116
- await this.prepare();
117
- await this._recorder.startAsync();
118
-
119
- if (Platform.OS === 'android') {
120
- this._recordStartedAt = Date.now();
121
- }
122
-
123
- const uri = this._recorder.getURI();
124
- if (uri) this.uri = uri;
125
- this.setState('recording');
126
- } catch (e) {
127
- this.setState('idle');
128
- throw e;
210
+ if (completed) {
211
+ this.stop().catch((error) => {
212
+ Logger.warn('[RecorderService.Expo] Failed to stop in update interval', error);
213
+ });
129
214
  }
215
+
216
+ this._recordingSubscribers.forEach((callback) => {
217
+ callback({ currentTime, completed });
218
+ });
130
219
  }
220
+ }, 100);
221
+ };
222
+
223
+ private removeListener = () => {
224
+ if (this.recordingUpdateInterval) {
225
+ clearInterval(this.recordingUpdateInterval);
226
+ this.recordingUpdateInterval = null;
227
+ }
228
+ };
229
+
230
+ private prepare = async () => {
231
+ this.setState('preparing');
232
+ if (Platform.OS === 'ios') {
233
+ await this.audioModule.setAudioModeAsync({
234
+ allowsRecording: true,
235
+ playsInSilentMode: true,
236
+ });
237
+ }
238
+
239
+ const recordingOptions = {
240
+ ...this._audioSettings,
241
+ extension: `.${this.options.extension}`,
131
242
  };
132
243
 
133
- public stop = async (): Promise<void> => {
134
- if (matchesOneOf(this.state, ['recording'])) {
244
+ this.recorder = new this.audioModule.AudioModule.AudioRecorder(recordingOptions);
245
+ await this.recorder.prepareToRecordAsync();
246
+ };
247
+
248
+ public requestPermission = async (): Promise<boolean> => {
249
+ const status = await this.audioModule.getRecordingPermissionsAsync();
250
+ if (expoPermissionGranted([status])) {
251
+ return true;
252
+ } else {
253
+ const status = await this.audioModule.requestRecordingPermissionsAsync();
254
+ return expoPermissionGranted([status]);
255
+ }
256
+ };
257
+
258
+ public record = async (): Promise<void> => {
259
+ if (matchesOneOf(this.state, ['idle', 'completed'])) {
260
+ try {
261
+ await this.prepare();
262
+ this.setListener();
263
+ this.recorder?.record();
264
+
135
265
  if (Platform.OS === 'android') {
136
- const buffer = this._getRecorderStopSafeBuffer();
137
- if (buffer > 0) await sleep(buffer);
266
+ this._recordStartedAt = Date.now();
138
267
  }
139
268
 
140
- await this._recorder.stopAndUnloadAsync();
141
- if (Platform.OS === 'ios') {
142
- await avModule.Audio.setAudioModeAsync({ allowsRecordingIOS: false, playsInSilentModeIOS: false });
143
- }
144
- this.setState('completed');
269
+ const uri = this.recorder?.uri;
270
+ if (uri) this.uri = uri;
271
+ this.setState('recording');
272
+ } catch (e) {
273
+ this.setState('idle');
274
+ this.removeListener();
275
+ throw e;
145
276
  }
146
- };
277
+ }
278
+ };
147
279
 
148
- public reset = async (): Promise<void> => {
149
- await this.stop();
150
- this.uri = undefined;
151
- this._recordingSubscribers.clear();
152
- this._recorder = new avModule.Audio.Recording();
153
- this.setState('idle');
154
- };
280
+ public stop = async (): Promise<void> => {
281
+ if (matchesOneOf(this.state, ['recording'])) {
282
+ if (Platform.OS === 'android') {
283
+ const buffer = this._getRecorderStopSafeBuffer();
284
+ if (buffer > 0) await sleep(buffer);
285
+ }
286
+
287
+ await this.recorder?.stop();
288
+ this.removeListener();
289
+ if (Platform.OS === 'ios') {
290
+ await this.audioModule.setAudioModeAsync({
291
+ allowsRecording: false,
292
+ playsInSilentMode: false,
293
+ });
294
+ }
295
+ this.setState('completed');
296
+ }
297
+ };
298
+
299
+ public reset = async (): Promise<void> => {
300
+ await this.stop();
301
+ this.recorder = null;
302
+ this.uri = undefined;
303
+ this._recordingSubscribers.clear();
304
+ this._stateSubscribers.clear();
305
+ this.setState('idle');
306
+ };
307
+ }
308
+
309
+ const createExpoRecorderService = ({ avModule }: Modules): RecorderServiceInterface => {
310
+ if (expoBackwardUtils.expoAV.isLegacyAVModule(avModule)) {
311
+ Logger.warn(
312
+ '[RecorderService.Expo] expo-av is deprecated and will be removed in Expo 54. Please migrate to expo-audio.',
313
+ );
155
314
  }
156
315
 
157
- return new VoiceRecorder();
316
+ return expoBackwardUtils.expoAV.isAudioModule(avModule)
317
+ ? new ExpoAudioRecorderAdapter(avModule)
318
+ : new LegacyExpoAVRecorderAdapter(avModule);
158
319
  };
159
320
 
160
321
  export default createExpoRecorderService;