@sendbird/uikit-react-native 3.10.2 → 3.11.0

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 (50) hide show
  1. package/README.md +2 -2
  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/createPlayerService.native.js +191 -114
  11. package/lib/commonjs/platform/createPlayerService.native.js.map +1 -1
  12. package/lib/commonjs/platform/createRecorderService.expo.js +3 -0
  13. package/lib/commonjs/platform/createRecorderService.expo.js.map +1 -1
  14. package/lib/commonjs/platform/createRecorderService.native.js +212 -129
  15. package/lib/commonjs/platform/createRecorderService.native.js.map +1 -1
  16. package/lib/commonjs/platform/types.js.map +1 -1
  17. package/lib/commonjs/version.js +1 -1
  18. package/lib/commonjs/version.js.map +1 -1
  19. package/lib/module/components/ChannelInput/VoiceMessageInput.js +1 -1
  20. package/lib/module/components/ChannelInput/VoiceMessageInput.js.map +1 -1
  21. package/lib/module/components/ChannelInput/index.js +3 -26
  22. package/lib/module/components/ChannelInput/index.js.map +1 -1
  23. package/lib/module/hooks/useVoiceMessageInput.js +10 -2
  24. package/lib/module/hooks/useVoiceMessageInput.js.map +1 -1
  25. package/lib/module/platform/createFileService.native.js +1 -1
  26. package/lib/module/platform/createFileService.native.js.map +1 -1
  27. package/lib/module/platform/createPlayerService.native.js +191 -114
  28. package/lib/module/platform/createPlayerService.native.js.map +1 -1
  29. package/lib/module/platform/createRecorderService.expo.js +3 -0
  30. package/lib/module/platform/createRecorderService.expo.js.map +1 -1
  31. package/lib/module/platform/createRecorderService.native.js +212 -129
  32. package/lib/module/platform/createRecorderService.native.js.map +1 -1
  33. package/lib/module/platform/types.js.map +1 -1
  34. package/lib/module/version.js +1 -1
  35. package/lib/module/version.js.map +1 -1
  36. package/lib/typescript/src/containers/SendbirdUIKitContainer.d.ts +1 -1
  37. package/lib/typescript/src/platform/createPlayerService.native.d.ts +5 -3
  38. package/lib/typescript/src/platform/createRecorderService.native.d.ts +5 -3
  39. package/lib/typescript/src/platform/types.d.ts +4 -0
  40. package/lib/typescript/src/version.d.ts +1 -1
  41. package/package.json +18 -5
  42. package/src/components/ChannelInput/VoiceMessageInput.tsx +1 -1
  43. package/src/components/ChannelInput/index.tsx +6 -36
  44. package/src/hooks/useVoiceMessageInput.ts +7 -2
  45. package/src/platform/createFileService.native.ts +1 -1
  46. package/src/platform/createPlayerService.native.tsx +237 -113
  47. package/src/platform/createRecorderService.expo.tsx +4 -0
  48. package/src/platform/createRecorderService.native.tsx +240 -118
  49. package/src/platform/types.ts +5 -0
  50. package/src/version.ts +1 -1
@@ -1,5 +1,6 @@
1
1
  import { Platform } from 'react-native';
2
- import * as RNAudioRecorder from 'react-native-audio-recorder-player';
2
+ import type * as LegacyModule from 'react-native-audio-recorder-player';
3
+ import type * as NitroSoundOrLegacyV4Module from 'react-native-nitro-sound';
3
4
  import * as Permissions from 'react-native-permissions';
4
5
  import { Permission } from 'react-native-permissions/src/types';
5
6
 
@@ -9,168 +10,289 @@ import VoiceMessageConfig from '../libs/VoiceMessageConfig';
9
10
  import nativePermissionGranted from '../utils/nativePermissionGranted';
10
11
  import type { RecorderServiceInterface, Unsubscribe } from './types';
11
12
 
13
+ export type AudioRecorderModule = typeof LegacyModule | typeof NitroSoundOrLegacyV4Module;
14
+
12
15
  type RecordingListener = Parameters<RecorderServiceInterface['addRecordingListener']>[number];
13
16
  type StateListener = Parameters<RecorderServiceInterface['addStateListener']>[number];
14
17
  type Modules = {
15
- audioRecorderModule: typeof RNAudioRecorder;
18
+ audioRecorderModule: AudioRecorderModule;
16
19
  permissionModule: typeof Permissions;
17
20
  };
18
- const createNativeRecorderService = ({ audioRecorderModule, permissionModule }: Modules): RecorderServiceInterface => {
19
- const module = new audioRecorderModule.default();
20
-
21
- class VoiceRecorder implements RecorderServiceInterface {
22
- public uri: RecorderServiceInterface['uri'] = undefined;
23
- public state: RecorderServiceInterface['state'] = 'idle';
24
- public options: RecorderServiceInterface['options'] = {
25
- minDuration: VoiceMessageConfig.DEFAULT.RECORDER.MIN_DURATION,
26
- maxDuration: VoiceMessageConfig.DEFAULT.RECORDER.MAX_DURATION,
27
- extension: VoiceMessageConfig.DEFAULT.RECORDER.EXTENSION,
28
- };
29
21
 
30
- // NOTE: In Android, even when startRecorder() is awaited, if stop() is executed immediately afterward, an error occurs
31
- private _recordStartedAt = 0;
32
- private _getRecorderStopSafeBuffer = () => {
33
- const minWaitingTime = 500;
34
- const elapsedTime = Date.now() - this._recordStartedAt;
35
- if (elapsedTime > minWaitingTime) return 0;
36
- else return minWaitingTime - elapsedTime;
37
- };
22
+ interface RecordBackData {
23
+ currentPosition: number;
24
+ duration?: number;
25
+ }
38
26
 
39
- private readonly recordingSubscribers = new Set<RecordingListener>();
40
- private readonly stateSubscribers = new Set<StateListener>();
41
- private readonly audioSettings = {
42
- sampleRate: VoiceMessageConfig.DEFAULT.RECORDER.SAMPLE_RATE,
43
- bitRate: VoiceMessageConfig.DEFAULT.RECORDER.BIT_RATE,
44
- audioChannels: VoiceMessageConfig.DEFAULT.RECORDER.CHANNELS,
45
- // encoding: mpeg4_aac
46
- };
47
- private readonly audioOptions = Platform.select({
27
+ interface RecorderModuleAdapter {
28
+ setSubscriptionDuration(duration: number): Promise<void> | void;
29
+ addRecordBackListener(callback: (data: RecordBackData) => void): void;
30
+ convertRecordPath(uri: string): string;
31
+ startRecorder(uri: string): Promise<void>;
32
+ stopRecorder(): Promise<void>;
33
+ }
34
+
35
+ class AudioRecorderPlayerAdapter implements RecorderModuleAdapter {
36
+ private module: InstanceType<typeof LegacyModule.default>;
37
+ private readonly audioOptions;
38
+
39
+ constructor(audioRecorderModule: typeof LegacyModule) {
40
+ this.module = new audioRecorderModule.default();
41
+
42
+ this.audioOptions = Platform.select({
48
43
  android: {
49
- AudioEncodingBitRateAndroid: this.audioSettings.bitRate,
50
- AudioChannelsAndroid: this.audioSettings.audioChannels,
51
- AudioSamplingRateAndroid: this.audioSettings.sampleRate,
44
+ AudioEncodingBitRateAndroid: VoiceMessageConfig.DEFAULT.RECORDER.BIT_RATE,
45
+ AudioChannelsAndroid: VoiceMessageConfig.DEFAULT.RECORDER.CHANNELS,
46
+ AudioSamplingRateAndroid: VoiceMessageConfig.DEFAULT.RECORDER.SAMPLE_RATE,
52
47
  AudioEncoderAndroid: audioRecorderModule.AudioEncoderAndroidType.AAC,
53
48
  OutputFormatAndroid: audioRecorderModule.OutputFormatAndroidType.MPEG_4,
54
49
  AudioSourceAndroid: audioRecorderModule.AudioSourceAndroidType.VOICE_RECOGNITION,
55
50
  },
56
51
  ios: {
57
- AVEncoderBitRateKeyIOS: this.audioSettings.bitRate,
58
- AVNumberOfChannelsKeyIOS: this.audioSettings.audioChannels,
59
- AVSampleRateKeyIOS: this.audioSettings.sampleRate,
52
+ AVEncoderBitRateKeyIOS: VoiceMessageConfig.DEFAULT.RECORDER.BIT_RATE,
53
+ AVNumberOfChannelsKeyIOS: VoiceMessageConfig.DEFAULT.RECORDER.CHANNELS,
54
+ AVSampleRateKeyIOS: VoiceMessageConfig.DEFAULT.RECORDER.SAMPLE_RATE,
60
55
  AVFormatIDKeyIOS: audioRecorderModule.AVEncodingOption.mp4, // same with aac
61
56
  AVEncoderAudioQualityKeyIOS: audioRecorderModule.AVEncoderAudioQualityIOSType.high,
62
57
  },
63
58
  default: {},
64
59
  });
60
+ }
65
61
 
66
- constructor() {
67
- module.setSubscriptionDuration(0.1).catch((error) => {
68
- Logger.warn('[RecorderService.Native] Failed to set subscription duration', error);
69
- });
70
- module.addRecordBackListener((data) => {
71
- const completed = data.currentPosition >= this.options.maxDuration;
62
+ async setSubscriptionDuration(duration: number): Promise<void> {
63
+ await this.module.setSubscriptionDuration(duration);
64
+ }
72
65
 
73
- if (completed) {
74
- this.stop().catch((error) => {
75
- Logger.warn('[RecorderService.Native] Failed to stop in RecordBackListener', error);
76
- });
77
- }
78
- if (this.state === 'recording') {
79
- this.recordingSubscribers.forEach((callback) => {
80
- callback({ currentTime: data.currentPosition, completed });
81
- });
82
- }
83
- });
66
+ addRecordBackListener(callback: (data: RecordBackData) => void): void {
67
+ this.module.addRecordBackListener(callback);
68
+ }
69
+
70
+ convertRecordPath(uri: string): string {
71
+ return Platform.OS === 'ios' ? uri.split('/').pop() || uri : uri;
72
+ }
73
+
74
+ async startRecorder(uri: string): Promise<void> {
75
+ await this.module.startRecorder(uri, this.audioOptions as Parameters<typeof this.module.startRecorder>[1]);
76
+ }
77
+
78
+ async stopRecorder(): Promise<void> {
79
+ await this.module.stopRecorder();
80
+ }
81
+ }
82
+
83
+ class NitroSoundOrLegacyV4Adapter implements RecorderModuleAdapter {
84
+ private module;
85
+ private readonly audioOptions;
86
+
87
+ constructor(audioRecorderModule: typeof NitroSoundOrLegacyV4Module) {
88
+ this.module = audioRecorderModule.default;
89
+ this.audioOptions = Platform.select({
90
+ android: {
91
+ AudioEncodingBitRateAndroid: VoiceMessageConfig.DEFAULT.RECORDER.BIT_RATE,
92
+ AudioChannelsAndroid: VoiceMessageConfig.DEFAULT.RECORDER.CHANNELS,
93
+ AudioSamplingRateAndroid: VoiceMessageConfig.DEFAULT.RECORDER.SAMPLE_RATE,
94
+ AudioEncoderAndroid: audioRecorderModule.AudioEncoderAndroidType.AAC,
95
+ OutputFormatAndroid: audioRecorderModule.OutputFormatAndroidType.MPEG_4,
96
+ AudioSourceAndroid: audioRecorderModule.AudioSourceAndroidType.VOICE_RECOGNITION,
97
+ },
98
+ ios: {
99
+ AVEncoderBitRateKeyIOS: VoiceMessageConfig.DEFAULT.RECORDER.BIT_RATE,
100
+ AVNumberOfChannelsKeyIOS: VoiceMessageConfig.DEFAULT.RECORDER.CHANNELS,
101
+ AVSampleRateKeyIOS: VoiceMessageConfig.DEFAULT.RECORDER.SAMPLE_RATE,
102
+ AVFormatIDKeyIOS: 'mp4', // same with aac
103
+ AVEncoderAudioQualityKeyIOS: audioRecorderModule.AVEncoderAudioQualityIOSType.high,
104
+ },
105
+ default: {},
106
+ });
107
+ }
108
+
109
+ setSubscriptionDuration(duration: number): void {
110
+ try {
111
+ this.module.setSubscriptionDuration(duration);
112
+ } catch (error) {
113
+ Logger.warn('[RecorderService.Native] Failed to set subscription duration', error);
84
114
  }
115
+ }
85
116
 
86
- private setState = (state: RecorderServiceInterface['state']) => {
87
- this.state = state;
88
- this.stateSubscribers.forEach((callback) => {
89
- callback(state);
90
- });
91
- };
117
+ addRecordBackListener(callback: (data: RecordBackData) => void): void {
118
+ this.module.addRecordBackListener(callback);
119
+ }
120
+
121
+ convertRecordPath(uri: string): string {
122
+ return uri;
123
+ }
124
+
125
+ async startRecorder(uri: string): Promise<void> {
126
+ await this.module.startRecorder(uri, this.audioOptions as Parameters<typeof this.module.startRecorder>[1]);
127
+ }
128
+
129
+ async stopRecorder(): Promise<void> {
130
+ await this.module.stopRecorder();
131
+ }
132
+ }
92
133
 
93
- public requestPermission = async (): Promise<boolean> => {
94
- const permission: Permission[] | undefined = Platform.select({
95
- android: [permissionModule.PERMISSIONS.ANDROID.RECORD_AUDIO],
96
- ios: [permissionModule.PERMISSIONS.IOS.MICROPHONE],
97
- windows: [permissionModule.PERMISSIONS.WINDOWS.MICROPHONE],
98
- default: undefined,
134
+ class VoiceRecorder implements RecorderServiceInterface {
135
+ public uri: RecorderServiceInterface['uri'] = undefined;
136
+ public state: RecorderServiceInterface['state'] = 'idle';
137
+ public options: RecorderServiceInterface['options'] = {
138
+ minDuration: VoiceMessageConfig.DEFAULT.RECORDER.MIN_DURATION,
139
+ maxDuration: VoiceMessageConfig.DEFAULT.RECORDER.MAX_DURATION,
140
+ extension: VoiceMessageConfig.DEFAULT.RECORDER.EXTENSION,
141
+ };
142
+
143
+ private _recordStartedAt = 0;
144
+ private _stopping = false;
145
+ private readonly recordingSubscribers = new Set<RecordingListener>();
146
+ private readonly stateSubscribers = new Set<StateListener>();
147
+
148
+ constructor(private readonly adapter: RecorderModuleAdapter, private readonly permissionModule: typeof Permissions) {
149
+ this.initialize();
150
+ }
151
+
152
+ private initialize(): void {
153
+ const setDurationResult = this.adapter.setSubscriptionDuration(0.1);
154
+ if (setDurationResult instanceof Promise) {
155
+ setDurationResult.catch((error) => {
156
+ Logger.warn('[RecorderService.Native] Failed to set subscription duration', error);
99
157
  });
158
+ }
159
+
160
+ this.adapter.addRecordBackListener((data) => {
161
+ const completed = data.currentPosition >= this.options.maxDuration;
100
162
 
101
- if (Platform.OS === 'android' && Platform.Version <= 28) {
102
- permission?.push(permissionModule.PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE);
163
+ if (completed) {
164
+ this.stop().catch((error) => {
165
+ Logger.warn('[RecorderService.Native] Failed to stop in RecordBackListener', error);
166
+ });
167
+ }
168
+ if (this.state === 'recording') {
169
+ this.recordingSubscribers.forEach((callback) => {
170
+ callback({ currentTime: data.currentPosition, completed });
171
+ });
103
172
  }
173
+ });
174
+ }
104
175
 
105
- if (permission) {
106
- const status = await permissionModule.checkMultiple(permission);
107
- if (nativePermissionGranted(status)) {
108
- return true;
109
- } else {
110
- const status = await permissionModule.requestMultiple(permission);
111
- return nativePermissionGranted(status);
112
- }
113
- } else {
176
+ private setState = (state: RecorderServiceInterface['state']): void => {
177
+ this.state = state;
178
+ this.stateSubscribers.forEach((callback) => {
179
+ callback(state);
180
+ });
181
+ };
182
+
183
+ private getRecorderStopSafeBuffer = (): number => {
184
+ const minWaitingTime = 500;
185
+ const elapsedTime = Date.now() - this._recordStartedAt;
186
+ if (elapsedTime > minWaitingTime) return 0;
187
+ else return minWaitingTime - elapsedTime;
188
+ };
189
+
190
+ public requestPermission = async (): Promise<boolean> => {
191
+ const permission: Permission[] | undefined = Platform.select({
192
+ android: [this.permissionModule.PERMISSIONS.ANDROID.RECORD_AUDIO],
193
+ ios: [this.permissionModule.PERMISSIONS.IOS.MICROPHONE],
194
+ windows: [this.permissionModule.PERMISSIONS.WINDOWS.MICROPHONE],
195
+ default: undefined,
196
+ });
197
+
198
+ if (Platform.OS === 'android' && Platform.Version <= 28) {
199
+ permission?.push(this.permissionModule.PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE);
200
+ }
201
+
202
+ if (permission) {
203
+ const status = await this.permissionModule.checkMultiple(permission);
204
+ if (nativePermissionGranted(status)) {
114
205
  return true;
206
+ } else {
207
+ const status = await this.permissionModule.requestMultiple(permission);
208
+ return nativePermissionGranted(status);
115
209
  }
116
- };
210
+ } else {
211
+ return true;
212
+ }
213
+ };
117
214
 
118
- public addRecordingListener = (callback: RecordingListener): Unsubscribe => {
119
- this.recordingSubscribers.add(callback);
120
- return () => {
121
- this.recordingSubscribers.delete(callback);
122
- };
215
+ public addRecordingListener = (callback: RecordingListener): Unsubscribe => {
216
+ this.recordingSubscribers.add(callback);
217
+ return () => {
218
+ this.recordingSubscribers.delete(callback);
123
219
  };
220
+ };
124
221
 
125
- public addStateListener = (callback: StateListener): Unsubscribe => {
126
- this.stateSubscribers.add(callback);
127
- return () => {
128
- this.stateSubscribers.delete(callback);
129
- };
222
+ public addStateListener = (callback: StateListener): Unsubscribe => {
223
+ this.stateSubscribers.add(callback);
224
+ return () => {
225
+ this.stateSubscribers.delete(callback);
130
226
  };
227
+ };
228
+
229
+ public record = async (uri: string): Promise<void> => {
230
+ if (matchesOneOf(this.state, ['idle', 'completed'])) {
231
+ try {
232
+ this.setState('preparing');
233
+ await this.adapter.startRecorder(uri);
131
234
 
132
- public record = async (uri: string): Promise<void> => {
133
- if (matchesOneOf(this.state, ['idle', 'completed'])) {
134
- try {
135
- this.setState('preparing');
136
- await module.startRecorder(uri, {
137
- ...this.audioOptions,
138
- });
139
-
140
- if (Platform.OS === 'android') {
141
- this._recordStartedAt = Date.now();
142
- }
143
-
144
- this.uri = uri;
145
- this.setState('recording');
146
- } catch (e) {
147
- this.setState('idle');
148
- throw e;
235
+ if (Platform.OS === 'android') {
236
+ this._recordStartedAt = Date.now();
149
237
  }
238
+
239
+ this.uri = uri;
240
+ this.setState('recording');
241
+ } catch (e) {
242
+ this.setState('idle');
243
+ throw e;
150
244
  }
151
- };
245
+ }
246
+ };
152
247
 
153
- public stop = async (): Promise<void> => {
154
- if (matchesOneOf(this.state, ['recording'])) {
248
+ public stop = async (): Promise<void> => {
249
+ if (matchesOneOf(this.state, ['recording']) && !this._stopping) {
250
+ this._stopping = true;
251
+ try {
155
252
  if (Platform.OS === 'android') {
156
- const buffer = this._getRecorderStopSafeBuffer();
253
+ const buffer = this.getRecorderStopSafeBuffer();
157
254
  if (buffer > 0) await sleep(buffer);
158
255
  }
159
256
 
160
- await module.stopRecorder();
257
+ await this.adapter.stopRecorder();
161
258
  this.setState('completed');
259
+ } catch (error) {
260
+ Logger.error('[RecorderService.Native] Failed to stop recorder', error);
261
+ throw error;
262
+ } finally {
263
+ this._stopping = false;
162
264
  }
163
- };
265
+ }
266
+ };
164
267
 
165
- public reset = async (): Promise<void> => {
166
- await this.stop();
167
- this.uri = undefined;
168
- this.recordingSubscribers.clear();
169
- this.setState('idle');
170
- };
171
- }
268
+ public reset = async (): Promise<void> => {
269
+ await this.stop();
270
+ this.uri = undefined;
271
+ this.recordingSubscribers.clear();
272
+ this.setState('idle');
273
+ };
274
+
275
+ public convertRecordPath = (uri: string): string => {
276
+ return this.adapter.convertRecordPath(uri);
277
+ };
278
+ }
279
+
280
+ const createNativeRecorderService = (modules: Modules): RecorderServiceInterface => {
281
+ const adapter = isNitroSoundOrLegacyV4Module(modules.audioRecorderModule)
282
+ ? new NitroSoundOrLegacyV4Adapter(modules.audioRecorderModule)
283
+ : new AudioRecorderPlayerAdapter(modules.audioRecorderModule as typeof LegacyModule);
172
284
 
173
- return new VoiceRecorder();
285
+ return new VoiceRecorder(adapter, modules.permissionModule);
174
286
  };
175
287
 
288
+ function isNitroSoundOrLegacyV4Module(module: AudioRecorderModule): module is typeof NitroSoundOrLegacyV4Module {
289
+ const isNitroSound = 'createSound' in module && typeof module.createSound === 'function';
290
+ const isLegacyV4 =
291
+ 'default' in module && 'getHybridObject' in module.default && typeof module.default.getHybridObject === 'function';
292
+ if (isLegacyV4) {
293
+ Logger.warn('react-native-audio-recorder-player is deprecated. Please use react-native-nitro-sound instead.');
294
+ }
295
+ return isNitroSound || isLegacyV4;
296
+ }
297
+
176
298
  export default createNativeRecorderService;
@@ -215,4 +215,9 @@ export interface RecorderServiceInterface {
215
215
  * [*] to [idle]
216
216
  * */
217
217
  reset(): Promise<void>;
218
+
219
+ /**
220
+ * Get the record path processed for the specific platform and adapter.
221
+ * */
222
+ convertRecordPath(uri: string): string;
218
223
  }
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
- const VERSION = '3.10.2';
1
+ const VERSION = '3.11.0';
2
2
  export default VERSION;