From a785cc908c2f16d968ecfb8d645ea9cd08fab73d Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Sat, 31 Aug 2024 13:51:28 +0200 Subject: [PATCH 01/13] fix: variety of bugs in async audio feature --- .../src/optionalDependencies/Audio.ts | 16 +++- .../components/MessageInput/MessageInput.tsx | 85 ++++++++++--------- .../MessageInput/hooks/useAudioController.tsx | 4 + 3 files changed, 63 insertions(+), 42 deletions(-) diff --git a/package/native-package/src/optionalDependencies/Audio.ts b/package/native-package/src/optionalDependencies/Audio.ts index 7de216b604..21319952cc 100644 --- a/package/native-package/src/optionalDependencies/Audio.ts +++ b/package/native-package/src/optionalDependencies/Audio.ts @@ -219,9 +219,17 @@ export const Audio = AudioRecorderPackage audioRecorderPlayer.addRecordBackListener((status) => { onRecordingStatusUpdate(status); }); + console.log('ISE: PERMISSIONS: ', recording); return { accessGranted: true, recording }; } catch (error) { console.error('Failed to start recording', error); + // There is currently a bug in react-native-audio-recorder-player and we + // need to do this until it gets fixed. More information can be found here: + // https://github.com/hyochan/react-native-audio-recorder-player/pull/625 + // eslint-disable-next-line no-underscore-dangle + audioRecorderPlayer._isRecording = false; + // eslint-disable-next-line no-underscore-dangle + audioRecorderPlayer._hasPausedRecord = false; return { accessGranted: false, recording: null }; } }, @@ -234,8 +242,12 @@ export const Audio = AudioRecorderPackage } }, stopRecording: async () => { - await audioRecorderPlayer.stopRecorder(); - audioRecorderPlayer.removeRecordBackListener(); + try { + await audioRecorderPlayer.stopRecorder(); + audioRecorderPlayer.removeRecordBackListener(); + } catch (error) { + console.log(error); + } }, } : null; diff --git a/package/src/components/MessageInput/MessageInput.tsx b/package/src/components/MessageInput/MessageInput.tsx index b14cd74d2e..7bb6fb7b61 100644 --- a/package/src/components/MessageInput/MessageInput.tsx +++ b/package/src/components/MessageInput/MessageInput.tsx @@ -2,15 +2,14 @@ import React, { useEffect, useMemo, useState } from 'react'; import { NativeSyntheticEvent, StyleSheet, TextInputFocusEventData, View } from 'react-native'; import { - GestureEvent, - PanGestureHandler, + Gesture, + GestureDetector, PanGestureHandlerEventPayload, } from 'react-native-gesture-handler'; import Animated, { Extrapolation, interpolate, runOnJS, - useAnimatedGestureHandler, useAnimatedStyle, useSharedValue, withSpring, @@ -104,6 +103,7 @@ type MessageInputPropsWithContext< | 'asyncMessagesLockDistance' | 'asyncMessagesMinimumPressDuration' | 'asyncMessagesSlideToCancelDistance' + | 'asyncMessagesMultiSendEnabled' | 'asyncUploads' | 'AudioRecorder' | 'AudioRecordingInProgress' @@ -168,6 +168,7 @@ const MessageInputWithContext = < asyncIds, asyncMessagesLockDistance, asyncMessagesMinimumPressDuration, + asyncMessagesMultiSendEnabled, asyncMessagesSlideToCancelDistance, asyncUploads, AttachmentPickerSelectionBar, @@ -624,19 +625,16 @@ const MessageInputWithContext = < const resetAudioRecording = async () => { await deleteVoiceRecording(); - micPositionX.value = 0; }; const micLockHandler = () => { setMicLocked(true); - micPositionY.value = 0; triggerHaptic('impactMedium'); }; - const handleMicGestureEvent = useAnimatedGestureHandler< - GestureEvent - >({ - onActive: (event) => { + const panGestureMic = Gesture.Pan() + .activateAfterLongPress(asyncMessagesMinimumPressDuration + 100) + .onChange((event: PanGestureHandlerEventPayload) => { const newPositionX = event.translationX; const newPositionY = event.translationY; @@ -646,27 +644,36 @@ const MessageInputWithContext = < if (newPositionY <= 0 && newPositionY >= Y_AXIS_POSITION) { micPositionY.value = newPositionY; } - }, - onFinish: () => { - if (micPositionY.value > Y_AXIS_POSITION / 2) { + }) + .onFinalize(() => { + const belowThresholdY = micPositionY.value > Y_AXIS_POSITION / 2; + const belowThresholdX = micPositionX.value > X_AXIS_POSITION / 2; + + if (belowThresholdY && belowThresholdX) { micPositionY.value = withSpring(0); - } else { + micPositionX.value = withSpring(0); + runOnJS(uploadVoiceRecording)(asyncMessagesMultiSendEnabled); + return; + } + + if (!belowThresholdY) { micPositionY.value = withSpring(Y_AXIS_POSITION); runOnJS(micLockHandler)(); } - if (micPositionX.value > X_AXIS_POSITION / 2) { - micPositionX.value = withSpring(0); - } else { + + if (!belowThresholdX) { micPositionX.value = withSpring(X_AXIS_POSITION); runOnJS(resetAudioRecording)(); } - }, - onStart: () => { + + micPositionX.value = 0; + micPositionY.value = 0; + }) + .onStart(() => { micPositionX.value = 0; micPositionY.value = 0; runOnJS(setMicLocked)(false); - }, - }); + }); const animatedStyles = { lockIndicator: useAnimatedStyle(() => ({ @@ -720,21 +727,20 @@ const MessageInputWithContext = < micLocked={micLocked} style={animatedStyles.lockIndicator} /> - {micLocked && - (recordingStatus === 'stopped' ? ( - - ) : ( - - ))} + {recordingStatus === 'stopped' ? ( + + ) : ( + + )} )} @@ -818,10 +824,7 @@ const MessageInputWithContext = < ))} {audioRecordingEnabled && !micLocked && ( - + - + )} )} @@ -1042,6 +1045,7 @@ export const MessageInput = < asyncIds, asyncMessagesLockDistance, asyncMessagesMinimumPressDuration, + asyncMessagesMultiSendEnabled, asyncMessagesSlideToCancelDistance, asyncUploads, AudioRecorder, @@ -1118,6 +1122,7 @@ export const MessageInput = < asyncIds, asyncMessagesLockDistance, asyncMessagesMinimumPressDuration, + asyncMessagesMultiSendEnabled, asyncMessagesSlideToCancelDistance, asyncUploads, AttachmentPickerSelectionBar, diff --git a/package/src/components/MessageInput/hooks/useAudioController.tsx b/package/src/components/MessageInput/hooks/useAudioController.tsx index 4580e32184..4250f75914 100644 --- a/package/src/components/MessageInput/hooks/useAudioController.tsx +++ b/package/src/components/MessageInput/hooks/useAudioController.tsx @@ -42,6 +42,7 @@ export const useAudioController = () => { useEffect( () => () => { stopVoicePlayer(); + deleteVoiceRecording(); }, [], ); @@ -162,6 +163,7 @@ export const useAudioController = () => { * Function to start voice recording. */ const startVoiceRecording = async () => { + console.log('ISE: START RECORDING') if (!Audio) return; setRecordingStatus('recording'); const recordingInfo = await Audio.startRecording( @@ -171,6 +173,7 @@ export const useAudioController = () => { onRecordingStatusUpdate, ); const accessGranted = recordingInfo.accessGranted; + console.log('ACCESS GRANTED: ', accessGranted) if (accessGranted) { setPermissionsGranted(true); const recording = recordingInfo.recording; @@ -240,6 +243,7 @@ export const useAudioController = () => { if (!paused) { await stopVoicePlayer(); } + console.log('ISE: R: ', recordingStatus) if (recordingStatus === 'recording') { await stopVoiceRecording(); } From 07110f45fe95dc74c79b9072f500b7986c3da43d Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Sun, 1 Sep 2024 05:37:36 +0200 Subject: [PATCH 02/13] fix: upload of empty messages and console.log cleanup --- package/native-package/src/optionalDependencies/Audio.ts | 1 - package/src/components/MessageInput/MessageInput.tsx | 6 ++++-- .../components/MessageInput/hooks/useAudioController.tsx | 5 +---- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/package/native-package/src/optionalDependencies/Audio.ts b/package/native-package/src/optionalDependencies/Audio.ts index 21319952cc..e96d8ee9b9 100644 --- a/package/native-package/src/optionalDependencies/Audio.ts +++ b/package/native-package/src/optionalDependencies/Audio.ts @@ -219,7 +219,6 @@ export const Audio = AudioRecorderPackage audioRecorderPlayer.addRecordBackListener((status) => { onRecordingStatusUpdate(status); }); - console.log('ISE: PERMISSIONS: ', recording); return { accessGranted: true, recording }; } catch (error) { console.error('Failed to start recording', error); diff --git a/package/src/components/MessageInput/MessageInput.tsx b/package/src/components/MessageInput/MessageInput.tsx index 7bb6fb7b61..4ce17630af 100644 --- a/package/src/components/MessageInput/MessageInput.tsx +++ b/package/src/components/MessageInput/MessageInput.tsx @@ -645,14 +645,16 @@ const MessageInputWithContext = < micPositionY.value = newPositionY; } }) - .onFinalize(() => { + .onEnd(() => { const belowThresholdY = micPositionY.value > Y_AXIS_POSITION / 2; const belowThresholdX = micPositionX.value > X_AXIS_POSITION / 2; if (belowThresholdY && belowThresholdX) { micPositionY.value = withSpring(0); micPositionX.value = withSpring(0); - runOnJS(uploadVoiceRecording)(asyncMessagesMultiSendEnabled); + if (recordingStatus === 'recording') { + runOnJS(uploadVoiceRecording)(asyncMessagesMultiSendEnabled); + } return; } diff --git a/package/src/components/MessageInput/hooks/useAudioController.tsx b/package/src/components/MessageInput/hooks/useAudioController.tsx index 4250f75914..dda0e888ae 100644 --- a/package/src/components/MessageInput/hooks/useAudioController.tsx +++ b/package/src/components/MessageInput/hooks/useAudioController.tsx @@ -163,9 +163,7 @@ export const useAudioController = () => { * Function to start voice recording. */ const startVoiceRecording = async () => { - console.log('ISE: START RECORDING') if (!Audio) return; - setRecordingStatus('recording'); const recordingInfo = await Audio.startRecording( { isMeteringEnabled: true, @@ -173,7 +171,6 @@ export const useAudioController = () => { onRecordingStatusUpdate, ); const accessGranted = recordingInfo.accessGranted; - console.log('ACCESS GRANTED: ', accessGranted) if (accessGranted) { setPermissionsGranted(true); const recording = recordingInfo.recording; @@ -181,6 +178,7 @@ export const useAudioController = () => { recording.setProgressUpdateInterval(Platform.OS === 'android' ? 100 : 60); } setRecording(recording); + setRecordingStatus('recording'); await stopVoicePlayer(); } else { setPermissionsGranted(false); @@ -243,7 +241,6 @@ export const useAudioController = () => { if (!paused) { await stopVoicePlayer(); } - console.log('ISE: R: ', recordingStatus) if (recordingStatus === 'recording') { await stopVoiceRecording(); } From 98667df183e9c17db6e9eb227657b8a5609c2a0b Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Sun, 1 Sep 2024 15:56:10 +0200 Subject: [PATCH 03/13] fix: expo permission race conditions --- package/expo-package/src/handlers/Audio.ts | 36 ++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/package/expo-package/src/handlers/Audio.ts b/package/expo-package/src/handlers/Audio.ts index 5283131e8f..f39ed8ab05 100644 --- a/package/expo-package/src/handlers/Audio.ts +++ b/package/expo-package/src/handlers/Audio.ts @@ -212,13 +212,34 @@ export type RecordingOptions = { keepAudioActiveHint?: boolean; }; +const sleep = (ms: number) => + new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, ms); + }); + export const Audio = AudioComponent ? { startRecording: async (recordingOptions: RecordingOptions, onRecordingStatusUpdate) => { try { - const permissionsGranted = await AudioComponent.getPermissionsAsync().granted; + const permissions = await AudioComponent.getPermissionsAsync(); + const permissionsStatus = permissions.status; + let permissionsGranted = permissions.granted; + + // If permissions have not been determined yet, ask the user for permissions. + if (permissionsStatus === 'undetermined') { + const newPermissions = await AudioComponent.requestPermissionsAsync(); + permissionsGranted = newPermissions.granted; + } + + // If they are explicitly denied after this, exit early by throwing an error + // that will be caught in the catch block below (as a single source of not + // starting the player). The player would error itself anyway if we did not do + // this, but there's no reason to run the asynchronous calls when we know + // immediately that the player will not be run. if (!permissionsGranted) { - await AudioComponent.requestPermissionsAsync(); + throw new Error('Missing audio recording permission.'); } await AudioComponent.setAudioModeAsync({ allowsRecordingIOS: true, @@ -244,6 +265,17 @@ export const Audio = AudioComponent web: {}, }; + // This is a band-aid fix for this (still unresolved) issue on Expo's side: + // https://github.com/expo/expo/issues/21782. It only occurs whenever we get + // the permissions dialog and actually select "Allow", causing the player to + // throw an error and send the wrong data downstream. So, if the original + // permissions.status is 'undetermined', meaning we got to here by allowing + // permissions - we sleep for 500ms before proceeding. Any subsequent calls + // to startRecording() will not invoke the sleep. + if (permissionsStatus === 'undetermined') { + await sleep(500); + } + const { recording } = await AudioComponent.Recording.createAsync( options, onRecordingStatusUpdate, From 96d523e42650b80924319634813c2997b26c339e Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Mon, 2 Sep 2024 16:02:07 +0200 Subject: [PATCH 04/13] fix: create pure version of stopRecording() for usage on unmount --- package/expo-package/src/handlers/Audio.ts | 20 +++++--- .../src/optionalDependencies/Video.ts | 4 +- .../src/optionalDependencies/Audio.ts | 27 +++++------ .../MessageInput/hooks/useAudioController.tsx | 46 +++++++++++-------- 4 files changed, 57 insertions(+), 40 deletions(-) diff --git a/package/expo-package/src/handlers/Audio.ts b/package/expo-package/src/handlers/Audio.ts index f39ed8ab05..9846ae1640 100644 --- a/package/expo-package/src/handlers/Audio.ts +++ b/package/expo-package/src/handlers/Audio.ts @@ -1,4 +1,4 @@ -import { AudioComponent } from '../optionalDependencies/Video'; +import { AudioComponent, RecordingObject } from '../optionalDependencies/Video'; export enum AndroidOutputFormat { DEFAULT = 0, @@ -220,8 +220,13 @@ const sleep = (ms: number) => }); export const Audio = AudioComponent - ? { - startRecording: async (recordingOptions: RecordingOptions, onRecordingStatusUpdate) => { + ? class { + recording: typeof RecordingObject | null; + + constructor() { + this.recording = null; + } + async startRecording(recordingOptions: RecordingOptions, onRecordingStatusUpdate) { try { const permissions = await AudioComponent.getPermissionsAsync(); const permissionsStatus = permissions.status; @@ -280,20 +285,23 @@ export const Audio = AudioComponent options, onRecordingStatusUpdate, ); + this.recording = recording; return { accessGranted: true, recording }; } catch (error) { console.error('Failed to start recording', error); return { accessGranted: false, recording: null }; } - }, - stopRecording: async () => { + } + async stopRecording() { try { + await this.recording.stopAndUnloadAsync(); await AudioComponent.setAudioModeAsync({ allowsRecordingIOS: false, }); + this.recording = null; } catch (error) { console.log('Error stopping recoding', error); } - }, + } } : null; diff --git a/package/expo-package/src/optionalDependencies/Video.ts b/package/expo-package/src/optionalDependencies/Video.ts index 75daa8c807..4505c97b69 100644 --- a/package/expo-package/src/optionalDependencies/Video.ts +++ b/package/expo-package/src/optionalDependencies/Video.ts @@ -1,9 +1,11 @@ let VideoComponent; let AudioComponent; +let RecordingObject; try { const audioVideoPackage = require('expo-av'); VideoComponent = audioVideoPackage.Video; AudioComponent = audioVideoPackage.Audio; + RecordingObject = audioVideoPackage.RecordingObject; } catch (e) { // do nothing } @@ -14,4 +16,4 @@ if (!VideoComponent || !AudioComponent) { ); } -export { AudioComponent, VideoComponent }; +export { AudioComponent, VideoComponent, RecordingObject }; diff --git a/package/native-package/src/optionalDependencies/Audio.ts b/package/native-package/src/optionalDependencies/Audio.ts index e96d8ee9b9..94e94d7cfe 100644 --- a/package/native-package/src/optionalDependencies/Audio.ts +++ b/package/native-package/src/optionalDependencies/Audio.ts @@ -169,14 +169,15 @@ const verifyAndroidPermissions = async () => { }; export const Audio = AudioRecorderPackage - ? { - pausePlayer: async () => { + ? class { + constructor() {} + async pausePlayer() { await audioRecorderPlayer.pausePlayer(); - }, - resumePlayer: async () => { + } + async resumePlayer() { await audioRecorderPlayer.resumePlayer(); - }, - startPlayer: async (uri, _, onPlaybackStatusUpdate) => { + } + async startPlayer(uri, _, onPlaybackStatusUpdate) { try { const playback = await audioRecorderPlayer.startPlayer(uri); console.log({ playback }); @@ -186,8 +187,8 @@ export const Audio = AudioRecorderPackage } catch (error) { console.log('Error starting player', error); } - }, - startRecording: async (options: RecordingOptions, onRecordingStatusUpdate) => { + } + async startRecording(options: RecordingOptions, onRecordingStatusUpdate) { if (Platform.OS === 'android') { try { await verifyAndroidPermissions(); @@ -231,22 +232,22 @@ export const Audio = AudioRecorderPackage audioRecorderPlayer._hasPausedRecord = false; return { accessGranted: false, recording: null }; } - }, - stopPlayer: async () => { + } + async stopPlayer() { try { await audioRecorderPlayer.stopPlayer(); audioRecorderPlayer.removePlayBackListener(); } catch (error) { console.log(error); } - }, - stopRecording: async () => { + } + async stopRecording() { try { await audioRecorderPlayer.stopRecorder(); audioRecorderPlayer.removeRecordBackListener(); } catch (error) { console.log(error); } - }, + } } : null; diff --git a/package/src/components/MessageInput/hooks/useAudioController.tsx b/package/src/components/MessageInput/hooks/useAudioController.tsx index dda0e888ae..cc4ea97e7b 100644 --- a/package/src/components/MessageInput/hooks/useAudioController.tsx +++ b/package/src/components/MessageInput/hooks/useAudioController.tsx @@ -4,8 +4,9 @@ import { Alert, Platform } from 'react-native'; import { useMessageInputContext } from '../../../contexts/messageInputContext/MessageInputContext'; import { - Audio, + Audio as AudioClass, AudioRecordingReturnType, + AudioType, PlaybackStatus, RecordingStatus, Sound, @@ -18,6 +19,8 @@ import { normalizeAudioLevel } from '../utils/normalizeAudioLevel'; export type RecordingStatusStates = 'idle' | 'recording' | 'stopped'; +let Audio: AudioType; + /** * The hook that controls all the async audio core features including start/stop or recording, player, upload/delete of the recorded audio. */ @@ -38,14 +41,18 @@ export const useAudioController = () => { // For playback support in Expo CLI apps const soundRef = useRef(null); - // Effect to stop the player when the component unmounts - useEffect( - () => () => { + // This effect controls the creation of an AudioClass instance during mounting + // and cleanup during unmounting (stopping both the player and a potential recording). + useEffect(() => { + if (AudioClass) { + // @ts-ignore + Audio = new AudioClass(); + } + return () => { stopVoicePlayer(); - deleteVoiceRecording(); - }, - [], - ); + stopSDKVoiceRecording(); + }; + }, []); useEffect(() => { if (isScheduledForSubmit) { @@ -187,22 +194,21 @@ export const useAudioController = () => { } }; + /** + * A function that takes care of stopping the voice recording from the library's + * side only. Meant to be used as a pure function (during unmounting for instance) + * hence this approach. + */ + const stopSDKVoiceRecording = async () => { + if (!Audio) return; + await Audio.stopRecording(); + }; + /** * Function to stop voice recording. */ const stopVoiceRecording = async () => { - if (!Audio) return; - if (recording) { - // For Expo CLI - if (typeof recording !== 'string') { - await recording.stopAndUnloadAsync(); - await Audio.stopRecording(); - } - // For RN CLI - else { - await Audio.stopRecording(); - } - } + await stopSDKVoiceRecording(); setRecordingStatus('stopped'); }; From f2474e8d9eed0708e0eb6173704ae0a179e7ba46 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Mon, 2 Sep 2024 16:25:02 +0200 Subject: [PATCH 05/13] fix: remove redundant export --- .../src/components/MessageInput/hooks/useAudioController.tsx | 1 - package/src/native.ts | 2 -- 2 files changed, 3 deletions(-) diff --git a/package/src/components/MessageInput/hooks/useAudioController.tsx b/package/src/components/MessageInput/hooks/useAudioController.tsx index cc4ea97e7b..446051f84c 100644 --- a/package/src/components/MessageInput/hooks/useAudioController.tsx +++ b/package/src/components/MessageInput/hooks/useAudioController.tsx @@ -45,7 +45,6 @@ export const useAudioController = () => { // and cleanup during unmounting (stopping both the player and a potential recording). useEffect(() => { if (AudioClass) { - // @ts-ignore Audio = new AudioClass(); } return () => { diff --git a/package/src/native.ts b/package/src/native.ts index 7120c8c583..d48f32904b 100644 --- a/package/src/native.ts +++ b/package/src/native.ts @@ -240,8 +240,6 @@ export type AudioType = { stopPlayer?: () => Promise; }; -export let Audio: AudioType; - export let Sound: SoundType; export type VideoProgressData = { From d954970267547f77f568505e29aaa7fcc4527472 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Mon, 2 Sep 2024 18:04:09 +0200 Subject: [PATCH 06/13] fix: actually amend failing test suites --- package/jest-setup.js | 7 ++++--- package/src/native.ts | 6 +++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/package/jest-setup.js b/package/jest-setup.js index 2bf6b4c61e..200dc07a61 100644 --- a/package/jest-setup.js +++ b/package/jest-setup.js @@ -15,9 +15,10 @@ export const setNetInfoFetchMock = (fn) => { netInfoFetch = fn; }; registerNativeHandlers({ - Audio: { - startPlayer: jest.fn(), - stopPlayer: jest.fn(), + Audio: class { + startPlayer = jest.fn() + stopPlayer = jest.fn() + stopRecording = jest.fn() }, compressImage: () => null, deleteFile: () => null, diff --git a/package/src/native.ts b/package/src/native.ts index d48f32904b..7b19e0a37a 100644 --- a/package/src/native.ts +++ b/package/src/native.ts @@ -240,6 +240,10 @@ export type AudioType = { stopPlayer?: () => Promise; }; +type AudioConstructSignature = new () => AudioType; + +export let Audio: AudioConstructSignature; + export let Sound: SoundType; export type VideoProgressData = { @@ -286,7 +290,7 @@ export type VideoType = { export let Video: React.ComponentType; type Handlers = { - Audio?: AudioType; + Audio?: AudioConstructSignature; compressImage?: CompressImage; deleteFile?: DeleteFile; FlatList?: typeof DefaultFlatList; From 07183c660ca816303598b17b8deeece28aa690e0 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Mon, 2 Sep 2024 18:14:57 +0200 Subject: [PATCH 07/13] chore: remove constructors as they are not needed --- package/expo-package/src/handlers/Audio.ts | 5 +---- package/native-package/src/optionalDependencies/Audio.ts | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/package/expo-package/src/handlers/Audio.ts b/package/expo-package/src/handlers/Audio.ts index 9846ae1640..8ae4a1477c 100644 --- a/package/expo-package/src/handlers/Audio.ts +++ b/package/expo-package/src/handlers/Audio.ts @@ -221,11 +221,8 @@ const sleep = (ms: number) => export const Audio = AudioComponent ? class { - recording: typeof RecordingObject | null; + recording: typeof RecordingObject | null = null; - constructor() { - this.recording = null; - } async startRecording(recordingOptions: RecordingOptions, onRecordingStatusUpdate) { try { const permissions = await AudioComponent.getPermissionsAsync(); diff --git a/package/native-package/src/optionalDependencies/Audio.ts b/package/native-package/src/optionalDependencies/Audio.ts index 94e94d7cfe..5238d92105 100644 --- a/package/native-package/src/optionalDependencies/Audio.ts +++ b/package/native-package/src/optionalDependencies/Audio.ts @@ -170,7 +170,6 @@ const verifyAndroidPermissions = async () => { export const Audio = AudioRecorderPackage ? class { - constructor() {} async pausePlayer() { await audioRecorderPlayer.pausePlayer(); } From 94fd0368440f702fbaea8e3f936f601757398389 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Mon, 2 Sep 2024 18:25:46 +0200 Subject: [PATCH 08/13] fix: pr remarks --- package/expo-package/src/handlers/Audio.ts | 8 +++---- .../src/optionalDependencies/Audio.ts | 24 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/package/expo-package/src/handlers/Audio.ts b/package/expo-package/src/handlers/Audio.ts index 8ae4a1477c..a98dbc148c 100644 --- a/package/expo-package/src/handlers/Audio.ts +++ b/package/expo-package/src/handlers/Audio.ts @@ -223,7 +223,7 @@ export const Audio = AudioComponent ? class { recording: typeof RecordingObject | null = null; - async startRecording(recordingOptions: RecordingOptions, onRecordingStatusUpdate) { + startRecording = async (recordingOptions: RecordingOptions, onRecordingStatusUpdate) => { try { const permissions = await AudioComponent.getPermissionsAsync(); const permissionsStatus = permissions.status; @@ -288,8 +288,8 @@ export const Audio = AudioComponent console.error('Failed to start recording', error); return { accessGranted: false, recording: null }; } - } - async stopRecording() { + }; + stopRecording = async () => { try { await this.recording.stopAndUnloadAsync(); await AudioComponent.setAudioModeAsync({ @@ -299,6 +299,6 @@ export const Audio = AudioComponent } catch (error) { console.log('Error stopping recoding', error); } - } + }; } : null; diff --git a/package/native-package/src/optionalDependencies/Audio.ts b/package/native-package/src/optionalDependencies/Audio.ts index 5238d92105..591ff0ba84 100644 --- a/package/native-package/src/optionalDependencies/Audio.ts +++ b/package/native-package/src/optionalDependencies/Audio.ts @@ -170,13 +170,13 @@ const verifyAndroidPermissions = async () => { export const Audio = AudioRecorderPackage ? class { - async pausePlayer() { + pausePlayer = async () => { await audioRecorderPlayer.pausePlayer(); - } - async resumePlayer() { + }; + resumePlayer = async () => { await audioRecorderPlayer.resumePlayer(); - } - async startPlayer(uri, _, onPlaybackStatusUpdate) { + }; + startPlayer = async (uri, _, onPlaybackStatusUpdate) => { try { const playback = await audioRecorderPlayer.startPlayer(uri); console.log({ playback }); @@ -186,8 +186,8 @@ export const Audio = AudioRecorderPackage } catch (error) { console.log('Error starting player', error); } - } - async startRecording(options: RecordingOptions, onRecordingStatusUpdate) { + }; + startRecording = async (options: RecordingOptions, onRecordingStatusUpdate) => { if (Platform.OS === 'android') { try { await verifyAndroidPermissions(); @@ -231,22 +231,22 @@ export const Audio = AudioRecorderPackage audioRecorderPlayer._hasPausedRecord = false; return { accessGranted: false, recording: null }; } - } - async stopPlayer() { + }; + stopPlayer = async () => { try { await audioRecorderPlayer.stopPlayer(); audioRecorderPlayer.removePlayBackListener(); } catch (error) { console.log(error); } - } - async stopRecording() { + }; + stopRecording = async () => { try { await audioRecorderPlayer.stopRecorder(); audioRecorderPlayer.removeRecordBackListener(); } catch (error) { console.log(error); } - } + }; } : null; From 05ac1b0cb776dec6d72cee4ed0556d47d1b34fb9 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Mon, 2 Sep 2024 23:34:27 +0200 Subject: [PATCH 09/13] chore: write some simplistic functionality tests --- package/jest-setup.js | 1 + .../__tests__/MessageInput.test.js | 42 ++++++++++++++++++- .../AudioRecorder/AudioRecorder.tsx | 2 +- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/package/jest-setup.js b/package/jest-setup.js index 200dc07a61..debbc040ab 100644 --- a/package/jest-setup.js +++ b/package/jest-setup.js @@ -19,6 +19,7 @@ registerNativeHandlers({ startPlayer = jest.fn() stopPlayer = jest.fn() stopRecording = jest.fn() + startRecording = jest.fn(() => ({ accessGranted: true, recording: 'some-recording-path' })) }, compressImage: () => null, deleteFile: () => null, diff --git a/package/src/components/MessageInput/__tests__/MessageInput.test.js b/package/src/components/MessageInput/__tests__/MessageInput.test.js index 070b924947..5928917e86 100644 --- a/package/src/components/MessageInput/__tests__/MessageInput.test.js +++ b/package/src/components/MessageInput/__tests__/MessageInput.test.js @@ -2,7 +2,7 @@ import React from 'react'; import { Alert } from 'react-native'; -import { cleanup, fireEvent, render, waitFor } from '@testing-library/react-native'; +import { cleanup, fireEvent, render, userEvent, waitFor } from '@testing-library/react-native'; import * as AttachmentPickerUtils from '../../../contexts/attachmentPickerContext/AttachmentPickerContext'; import { OverlayProvider } from '../../../contexts/overlayContext/OverlayProvider'; @@ -127,4 +127,44 @@ describe('MessageInput', () => { // Both for files and for images triggered in one test itself. expect(Alert.alert).toHaveBeenCalledTimes(4); }); + + it('should start the audio recorder on long press', async () => { + await initializeChannel(generateChannelResponse()); + const userBot = userEvent.setup(); + + const { queryByTestId } = render( + + + + + + ); + + await userBot.longPress(queryByTestId('audio-button'), { duration: 1000 }); + + await waitFor(() => { + expect(queryByTestId('recording-active-container')).toBeTruthy(); + expect(Alert.alert).not.toHaveBeenCalledWith('Hold to start recording.'); + }); + }); + + it('should trigger an alert if a normal press happened on audio recording', async () => { + await initializeChannel(generateChannelResponse()); + const userBot = userEvent.setup(); + + const { queryByTestId } = render( + + + + + + ); + + await userBot.press(queryByTestId('audio-button')); + + await waitFor(() => { + expect(queryByTestId('recording-active-container')).not.toBeTruthy(); + expect(Alert.alert).toHaveBeenCalledWith('Hold to start recording.'); + }); + }); }); diff --git a/package/src/components/MessageInput/components/AudioRecorder/AudioRecorder.tsx b/package/src/components/MessageInput/components/AudioRecorder/AudioRecorder.tsx index b092960c5a..9b86c8b97a 100644 --- a/package/src/components/MessageInput/components/AudioRecorder/AudioRecorder.tsx +++ b/package/src/components/MessageInput/components/AudioRecorder/AudioRecorder.tsx @@ -180,7 +180,7 @@ const AudioRecorderWithContext = < } else { return ( <> - + {recordingDuration ? dayjs.duration(recordingDuration).format('mm:ss') : null} From b192aa718f79078fc71ae2ed9784259b53cfb7cd Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Tue, 3 Sep 2024 00:09:59 +0200 Subject: [PATCH 10/13] chore: return class instance rather than class itself for better usability --- package/expo-package/src/handlers/Audio.ts | 154 ++++++++--------- package/jest-setup.js | 10 +- .../src/optionalDependencies/Audio.ts | 158 +++++++++--------- .../MessageInput/hooks/useAudioController.tsx | 21 +-- package/src/native.ts | 6 +- 5 files changed, 171 insertions(+), 178 deletions(-) diff --git a/package/expo-package/src/handlers/Audio.ts b/package/expo-package/src/handlers/Audio.ts index a98dbc148c..32b2844750 100644 --- a/package/expo-package/src/handlers/Audio.ts +++ b/package/expo-package/src/handlers/Audio.ts @@ -219,86 +219,86 @@ const sleep = (ms: number) => }, ms); }); -export const Audio = AudioComponent - ? class { - recording: typeof RecordingObject | null = null; +class _Audio { + recording: typeof RecordingObject | null = null; - startRecording = async (recordingOptions: RecordingOptions, onRecordingStatusUpdate) => { - try { - const permissions = await AudioComponent.getPermissionsAsync(); - const permissionsStatus = permissions.status; - let permissionsGranted = permissions.granted; + startRecording = async (recordingOptions: RecordingOptions, onRecordingStatusUpdate) => { + try { + const permissions = await AudioComponent.getPermissionsAsync(); + const permissionsStatus = permissions.status; + let permissionsGranted = permissions.granted; - // If permissions have not been determined yet, ask the user for permissions. - if (permissionsStatus === 'undetermined') { - const newPermissions = await AudioComponent.requestPermissionsAsync(); - permissionsGranted = newPermissions.granted; - } + // If permissions have not been determined yet, ask the user for permissions. + if (permissionsStatus === 'undetermined') { + const newPermissions = await AudioComponent.requestPermissionsAsync(); + permissionsGranted = newPermissions.granted; + } - // If they are explicitly denied after this, exit early by throwing an error - // that will be caught in the catch block below (as a single source of not - // starting the player). The player would error itself anyway if we did not do - // this, but there's no reason to run the asynchronous calls when we know - // immediately that the player will not be run. - if (!permissionsGranted) { - throw new Error('Missing audio recording permission.'); - } - await AudioComponent.setAudioModeAsync({ - allowsRecordingIOS: true, - playsInSilentModeIOS: true, - }); - const androidOptions = { - audioEncoder: AndroidAudioEncoder.AAC, - extension: '.aac', - outputFormat: AndroidOutputFormat.AAC_ADTS, - }; - const iosOptions = { - audioQuality: IOSAudioQuality.HIGH, - bitRate: 128000, - extension: '.aac', - numberOfChannels: 2, - outputFormat: IOSOutputFormat.MPEG4AAC, - sampleRate: 44100, - }; - const options = { - ...recordingOptions, - android: androidOptions, - ios: iosOptions, - web: {}, - }; - - // This is a band-aid fix for this (still unresolved) issue on Expo's side: - // https://github.com/expo/expo/issues/21782. It only occurs whenever we get - // the permissions dialog and actually select "Allow", causing the player to - // throw an error and send the wrong data downstream. So, if the original - // permissions.status is 'undetermined', meaning we got to here by allowing - // permissions - we sleep for 500ms before proceeding. Any subsequent calls - // to startRecording() will not invoke the sleep. - if (permissionsStatus === 'undetermined') { - await sleep(500); - } - - const { recording } = await AudioComponent.Recording.createAsync( - options, - onRecordingStatusUpdate, - ); - this.recording = recording; - return { accessGranted: true, recording }; - } catch (error) { - console.error('Failed to start recording', error); - return { accessGranted: false, recording: null }; - } + // If they are explicitly denied after this, exit early by throwing an error + // that will be caught in the catch block below (as a single source of not + // starting the player). The player would error itself anyway if we did not do + // this, but there's no reason to run the asynchronous calls when we know + // immediately that the player will not be run. + if (!permissionsGranted) { + throw new Error('Missing audio recording permission.'); + } + await AudioComponent.setAudioModeAsync({ + allowsRecordingIOS: true, + playsInSilentModeIOS: true, + }); + const androidOptions = { + audioEncoder: AndroidAudioEncoder.AAC, + extension: '.aac', + outputFormat: AndroidOutputFormat.AAC_ADTS, }; - stopRecording = async () => { - try { - await this.recording.stopAndUnloadAsync(); - await AudioComponent.setAudioModeAsync({ - allowsRecordingIOS: false, - }); - this.recording = null; - } catch (error) { - console.log('Error stopping recoding', error); - } + const iosOptions = { + audioQuality: IOSAudioQuality.HIGH, + bitRate: 128000, + extension: '.aac', + numberOfChannels: 2, + outputFormat: IOSOutputFormat.MPEG4AAC, + sampleRate: 44100, }; + const options = { + ...recordingOptions, + android: androidOptions, + ios: iosOptions, + web: {}, + }; + + // This is a band-aid fix for this (still unresolved) issue on Expo's side: + // https://github.com/expo/expo/issues/21782. It only occurs whenever we get + // the permissions dialog and actually select "Allow", causing the player to + // throw an error and send the wrong data downstream. So, if the original + // permissions.status is 'undetermined', meaning we got to here by allowing + // permissions - we sleep for 500ms before proceeding. Any subsequent calls + // to startRecording() will not invoke the sleep. + if (permissionsStatus === 'undetermined') { + await sleep(500); + } + + const { recording } = await AudioComponent.Recording.createAsync( + options, + onRecordingStatusUpdate, + ); + this.recording = recording; + return { accessGranted: true, recording }; + } catch (error) { + console.error('Failed to start recording', error); + return { accessGranted: false, recording: null }; + } + }; + stopRecording = async () => { + try { + await this.recording.stopAndUnloadAsync(); + await AudioComponent.setAudioModeAsync({ + allowsRecordingIOS: false, + }); + this.recording = null; + } catch (error) { + console.log('Error stopping recoding', error); } - : null; + }; +} + +export const Audio = AudioComponent ? new _Audio() : null; diff --git a/package/jest-setup.js b/package/jest-setup.js index debbc040ab..26d266bd93 100644 --- a/package/jest-setup.js +++ b/package/jest-setup.js @@ -15,11 +15,11 @@ export const setNetInfoFetchMock = (fn) => { netInfoFetch = fn; }; registerNativeHandlers({ - Audio: class { - startPlayer = jest.fn() - stopPlayer = jest.fn() - stopRecording = jest.fn() - startRecording = jest.fn(() => ({ accessGranted: true, recording: 'some-recording-path' })) + Audio: { + startPlayer: jest.fn(), + startRecording: jest.fn(() => ({ accessGranted: true, recording: 'some-recording-path' })), + stopPlayer: jest.fn(), + stopRecording: jest.fn(), }, compressImage: () => null, deleteFile: () => null, diff --git a/package/native-package/src/optionalDependencies/Audio.ts b/package/native-package/src/optionalDependencies/Audio.ts index 591ff0ba84..6f1775998b 100644 --- a/package/native-package/src/optionalDependencies/Audio.ts +++ b/package/native-package/src/optionalDependencies/Audio.ts @@ -168,85 +168,85 @@ const verifyAndroidPermissions = async () => { return true; }; -export const Audio = AudioRecorderPackage - ? class { - pausePlayer = async () => { - await audioRecorderPlayer.pausePlayer(); - }; - resumePlayer = async () => { - await audioRecorderPlayer.resumePlayer(); - }; - startPlayer = async (uri, _, onPlaybackStatusUpdate) => { - try { - const playback = await audioRecorderPlayer.startPlayer(uri); - console.log({ playback }); - audioRecorderPlayer.addPlayBackListener((status) => { - onPlaybackStatusUpdate(status); - }); - } catch (error) { - console.log('Error starting player', error); - } +class _Audio { + pausePlayer = async () => { + await audioRecorderPlayer.pausePlayer(); + }; + resumePlayer = async () => { + await audioRecorderPlayer.resumePlayer(); + }; + startPlayer = async (uri, _, onPlaybackStatusUpdate) => { + try { + const playback = await audioRecorderPlayer.startPlayer(uri); + console.log({ playback }); + audioRecorderPlayer.addPlayBackListener((status) => { + onPlaybackStatusUpdate(status); + }); + } catch (error) { + console.log('Error starting player', error); + } + }; + startRecording = async (options: RecordingOptions, onRecordingStatusUpdate) => { + if (Platform.OS === 'android') { + try { + await verifyAndroidPermissions(); + } catch (err) { + console.warn('Audio Recording Permissions error', err); + return; + } + } + try { + const path = Platform.select({ + android: `${RNFS.CachesDirectoryPath}/sound.aac`, + ios: 'sound.aac', + }); + const audioSet = { + AudioEncoderAndroid: AudioEncoderAndroidType.AAC, + AudioSourceAndroid: AudioSourceAndroidType.MIC, + AVEncoderAudioQualityKeyIOS: AVEncoderAudioQualityIOSType.high, + AVFormatIDKeyIOS: AVEncodingOption.aac, + AVModeIOS: AVModeIOSOption.measurement, + AVNumberOfChannelsKeyIOS: 2, + OutputFormatAndroid: OutputFormatAndroidType.AAC_ADTS, }; - startRecording = async (options: RecordingOptions, onRecordingStatusUpdate) => { - if (Platform.OS === 'android') { - try { - await verifyAndroidPermissions(); - } catch (err) { - console.warn('Audio Recording Permissions error', err); - return; - } - } - try { - const path = Platform.select({ - android: `${RNFS.CachesDirectoryPath}/sound.aac`, - ios: 'sound.aac', - }); - const audioSet = { - AudioEncoderAndroid: AudioEncoderAndroidType.AAC, - AudioSourceAndroid: AudioSourceAndroidType.MIC, - AVEncoderAudioQualityKeyIOS: AVEncoderAudioQualityIOSType.high, - AVFormatIDKeyIOS: AVEncodingOption.aac, - AVModeIOS: AVModeIOSOption.measurement, - AVNumberOfChannelsKeyIOS: 2, - OutputFormatAndroid: OutputFormatAndroidType.AAC_ADTS, - }; - const recording = await audioRecorderPlayer.startRecorder( - path, - audioSet, - options?.isMeteringEnabled, - ); + const recording = await audioRecorderPlayer.startRecorder( + path, + audioSet, + options?.isMeteringEnabled, + ); - audioRecorderPlayer.addRecordBackListener((status) => { - onRecordingStatusUpdate(status); - }); - return { accessGranted: true, recording }; - } catch (error) { - console.error('Failed to start recording', error); - // There is currently a bug in react-native-audio-recorder-player and we - // need to do this until it gets fixed. More information can be found here: - // https://github.com/hyochan/react-native-audio-recorder-player/pull/625 - // eslint-disable-next-line no-underscore-dangle - audioRecorderPlayer._isRecording = false; - // eslint-disable-next-line no-underscore-dangle - audioRecorderPlayer._hasPausedRecord = false; - return { accessGranted: false, recording: null }; - } - }; - stopPlayer = async () => { - try { - await audioRecorderPlayer.stopPlayer(); - audioRecorderPlayer.removePlayBackListener(); - } catch (error) { - console.log(error); - } - }; - stopRecording = async () => { - try { - await audioRecorderPlayer.stopRecorder(); - audioRecorderPlayer.removeRecordBackListener(); - } catch (error) { - console.log(error); - } - }; + audioRecorderPlayer.addRecordBackListener((status) => { + onRecordingStatusUpdate(status); + }); + return { accessGranted: true, recording }; + } catch (error) { + console.error('Failed to start recording', error); + // There is currently a bug in react-native-audio-recorder-player and we + // need to do this until it gets fixed. More information can be found here: + // https://github.com/hyochan/react-native-audio-recorder-player/pull/625 + // eslint-disable-next-line no-underscore-dangle + audioRecorderPlayer._isRecording = false; + // eslint-disable-next-line no-underscore-dangle + audioRecorderPlayer._hasPausedRecord = false; + return { accessGranted: false, recording: null }; } - : null; + }; + stopPlayer = async () => { + try { + await audioRecorderPlayer.stopPlayer(); + audioRecorderPlayer.removePlayBackListener(); + } catch (error) { + console.log(error); + } + }; + stopRecording = async () => { + try { + await audioRecorderPlayer.stopRecorder(); + audioRecorderPlayer.removeRecordBackListener(); + } catch (error) { + console.log(error); + } + }; +} + +export const Audio = AudioRecorderPackage ? new _Audio() : null; diff --git a/package/src/components/MessageInput/hooks/useAudioController.tsx b/package/src/components/MessageInput/hooks/useAudioController.tsx index 446051f84c..bc8c438ff2 100644 --- a/package/src/components/MessageInput/hooks/useAudioController.tsx +++ b/package/src/components/MessageInput/hooks/useAudioController.tsx @@ -4,9 +4,8 @@ import { Alert, Platform } from 'react-native'; import { useMessageInputContext } from '../../../contexts/messageInputContext/MessageInputContext'; import { - Audio as AudioClass, + Audio, AudioRecordingReturnType, - AudioType, PlaybackStatus, RecordingStatus, Sound, @@ -19,8 +18,6 @@ import { normalizeAudioLevel } from '../utils/normalizeAudioLevel'; export type RecordingStatusStates = 'idle' | 'recording' | 'stopped'; -let Audio: AudioType; - /** * The hook that controls all the async audio core features including start/stop or recording, player, upload/delete of the recorded audio. */ @@ -41,17 +38,15 @@ export const useAudioController = () => { // For playback support in Expo CLI apps const soundRef = useRef(null); - // This effect controls the creation of an AudioClass instance during mounting - // and cleanup during unmounting (stopping both the player and a potential recording). - useEffect(() => { - if (AudioClass) { - Audio = new AudioClass(); - } - return () => { + // This effect stop the player from playing and stops audio recording on + // the audio SDK side on unmount. + useEffect( + () => () => { stopVoicePlayer(); stopSDKVoiceRecording(); - }; - }, []); + }, + [], + ); useEffect(() => { if (isScheduledForSubmit) { diff --git a/package/src/native.ts b/package/src/native.ts index 7b19e0a37a..7120c8c583 100644 --- a/package/src/native.ts +++ b/package/src/native.ts @@ -240,9 +240,7 @@ export type AudioType = { stopPlayer?: () => Promise; }; -type AudioConstructSignature = new () => AudioType; - -export let Audio: AudioConstructSignature; +export let Audio: AudioType; export let Sound: SoundType; @@ -290,7 +288,7 @@ export type VideoType = { export let Video: React.ComponentType; type Handlers = { - Audio?: AudioConstructSignature; + Audio?: AudioType; compressImage?: CompressImage; deleteFile?: DeleteFile; FlatList?: typeof DefaultFlatList; From c1c4e88d1087bfb8cdeb182dc6aa77e245438819 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Tue, 3 Sep 2024 00:27:58 +0200 Subject: [PATCH 11/13] chore: add more robust tests --- .../__tests__/MessageInput.test.js | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/package/src/components/MessageInput/__tests__/MessageInput.test.js b/package/src/components/MessageInput/__tests__/MessageInput.test.js index 5928917e86..b9dcbbbd92 100644 --- a/package/src/components/MessageInput/__tests__/MessageInput.test.js +++ b/package/src/components/MessageInput/__tests__/MessageInput.test.js @@ -16,6 +16,7 @@ import { import { generateChannelResponse } from '../../../mock-builders/generator/channel'; import { generateUser } from '../../../mock-builders/generator/user'; import { getTestClientWithUser } from '../../../mock-builders/mock'; +import { Audio } from '../../../native'; import { AttachmentPickerSelectionBar } from '../../AttachmentPicker/components/AttachmentPickerSelectionBar'; import { CameraSelectorIcon } from '../../AttachmentPicker/components/CameraSelectorIcon'; import { FileSelectorIcon } from '../../AttachmentPicker/components/FileSelectorIcon'; @@ -128,11 +129,13 @@ describe('MessageInput', () => { expect(Alert.alert).toHaveBeenCalledTimes(4); }); - it('should start the audio recorder on long press', async () => { + it('should start the audio recorder on long press and cleanup on unmount', async () => { + jest.clearAllMocks(); + await initializeChannel(generateChannelResponse()); const userBot = userEvent.setup(); - const { queryByTestId } = render( + const { queryByTestId, unmount } = render( @@ -143,12 +146,24 @@ describe('MessageInput', () => { await userBot.longPress(queryByTestId('audio-button'), { duration: 1000 }); await waitFor(() => { + expect(Audio.startRecording).toHaveBeenCalledTimes(1); + expect(Audio.stopRecording).not.toHaveBeenCalled(); expect(queryByTestId('recording-active-container')).toBeTruthy(); expect(Alert.alert).not.toHaveBeenCalledWith('Hold to start recording.'); }); + + unmount(); + + await waitFor(() => { + expect(Audio.stopRecording).toHaveBeenCalledTimes(1); + // once when starting the recording, once on unmount + expect(Audio.stopPlayer).toHaveBeenCalledTimes(2); + }); }); it('should trigger an alert if a normal press happened on audio recording', async () => { + jest.clearAllMocks(); + await initializeChannel(generateChannelResponse()); const userBot = userEvent.setup(); @@ -163,7 +178,11 @@ describe('MessageInput', () => { await userBot.press(queryByTestId('audio-button')); await waitFor(() => { + expect(Audio.startRecording).not.toHaveBeenCalled(); + expect(Audio.stopRecording).not.toHaveBeenCalled(); expect(queryByTestId('recording-active-container')).not.toBeTruthy(); + // This is sort of a brittle test, but there doesn't seem to be another way + // to target alerts. The reason why it's here is because we had a bug with it. expect(Alert.alert).toHaveBeenCalledWith('Hold to start recording.'); }); }); From 4dd91b14ae933c35024a1903ca8b920dd12b0973 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Tue, 3 Sep 2024 00:34:09 +0200 Subject: [PATCH 12/13] fix: linter errors --- .../components/MessageInput/__tests__/MessageInput.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package/src/components/MessageInput/__tests__/MessageInput.test.js b/package/src/components/MessageInput/__tests__/MessageInput.test.js index b9dcbbbd92..a62207c531 100644 --- a/package/src/components/MessageInput/__tests__/MessageInput.test.js +++ b/package/src/components/MessageInput/__tests__/MessageInput.test.js @@ -140,7 +140,7 @@ describe('MessageInput', () => { - + , ); await userBot.longPress(queryByTestId('audio-button'), { duration: 1000 }); @@ -172,7 +172,7 @@ describe('MessageInput', () => { - + , ); await userBot.press(queryByTestId('audio-button')); From 917a57f660678df691dd7c7ffbd1b98299d434ef Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Tue, 3 Sep 2024 09:40:20 +0200 Subject: [PATCH 13/13] fix: only display waveform whenever the mic is locked --- package/src/components/MessageInput/MessageInput.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package/src/components/MessageInput/MessageInput.tsx b/package/src/components/MessageInput/MessageInput.tsx index 4ce17630af..20a9cf52fb 100644 --- a/package/src/components/MessageInput/MessageInput.tsx +++ b/package/src/components/MessageInput/MessageInput.tsx @@ -737,12 +737,12 @@ const MessageInputWithContext = < progress={progress} waveformData={waveformData} /> - ) : ( + ) : micLocked ? ( - )} + ) : null} )}