Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions package/expo-package/src/handlers/Audio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,13 +212,34 @@ export type RecordingOptions = {
keepAudioActiveHint?: boolean;
};

const sleep = (ms: number) =>
new Promise<void>((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,
Expand All @@ -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,
Expand Down
15 changes: 13 additions & 2 deletions package/native-package/src/optionalDependencies/Audio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,13 @@ export const Audio = AudioRecorderPackage
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 };
}
},
Expand All @@ -234,8 +241,12 @@ export const Audio = AudioRecorderPackage
}
},
stopRecording: async () => {
await audioRecorderPlayer.stopRecorder();
audioRecorderPlayer.removeRecordBackListener();
try {
await audioRecorderPlayer.stopRecorder();
audioRecorderPlayer.removeRecordBackListener();
} catch (error) {
console.log(error);
}
},
}
: null;
87 changes: 47 additions & 40 deletions package/src/components/MessageInput/MessageInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -104,6 +103,7 @@ type MessageInputPropsWithContext<
| 'asyncMessagesLockDistance'
| 'asyncMessagesMinimumPressDuration'
| 'asyncMessagesSlideToCancelDistance'
| 'asyncMessagesMultiSendEnabled'
| 'asyncUploads'
| 'AudioRecorder'
| 'AudioRecordingInProgress'
Expand Down Expand Up @@ -168,6 +168,7 @@ const MessageInputWithContext = <
asyncIds,
asyncMessagesLockDistance,
asyncMessagesMinimumPressDuration,
asyncMessagesMultiSendEnabled,
asyncMessagesSlideToCancelDistance,
asyncUploads,
AttachmentPickerSelectionBar,
Expand Down Expand Up @@ -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<PanGestureHandlerEventPayload>
>({
onActive: (event) => {
const panGestureMic = Gesture.Pan()
.activateAfterLongPress(asyncMessagesMinimumPressDuration + 100)
.onChange((event: PanGestureHandlerEventPayload) => {
const newPositionX = event.translationX;
const newPositionY = event.translationY;

Expand All @@ -646,27 +644,38 @@ const MessageInputWithContext = <
if (newPositionY <= 0 && newPositionY >= Y_AXIS_POSITION) {
micPositionY.value = newPositionY;
}
},
onFinish: () => {
if (micPositionY.value > Y_AXIS_POSITION / 2) {
})
.onEnd(() => {
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);
if (recordingStatus === 'recording') {
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(() => ({
Expand Down Expand Up @@ -720,21 +729,20 @@ const MessageInputWithContext = <
micLocked={micLocked}
style={animatedStyles.lockIndicator}
/>
{micLocked &&
(recordingStatus === 'stopped' ? (
<AudioRecordingPreview
onVoicePlayerPlayPause={onVoicePlayerPlayPause}
paused={paused}
position={position}
progress={progress}
waveformData={waveformData}
/>
) : (
<AudioRecordingInProgress
recordingDuration={recordingDuration}
waveformData={waveformData}
/>
))}
{recordingStatus === 'stopped' ? (
<AudioRecordingPreview
onVoicePlayerPlayPause={onVoicePlayerPlayPause}
paused={paused}
position={position}
progress={progress}
waveformData={waveformData}
/>
) : (
<AudioRecordingInProgress
recordingDuration={recordingDuration}
waveformData={waveformData}
/>
)}
</>
)}

Expand Down Expand Up @@ -818,10 +826,7 @@ const MessageInputWithContext = <
</View>
))}
{audioRecordingEnabled && !micLocked && (
<PanGestureHandler
activateAfterLongPress={asyncMessagesMinimumPressDuration + 100}
onGestureEvent={handleMicGestureEvent}
>
<GestureDetector gesture={panGestureMic}>
<Animated.View
style={[
styles.micButtonContainer,
Expand All @@ -835,7 +840,7 @@ const MessageInputWithContext = <
startVoiceRecording={startVoiceRecording}
/>
</Animated.View>
</PanGestureHandler>
</GestureDetector>
)}
</>
)}
Expand Down Expand Up @@ -1042,6 +1047,7 @@ export const MessageInput = <
asyncIds,
asyncMessagesLockDistance,
asyncMessagesMinimumPressDuration,
asyncMessagesMultiSendEnabled,
asyncMessagesSlideToCancelDistance,
asyncUploads,
AudioRecorder,
Expand Down Expand Up @@ -1118,6 +1124,7 @@ export const MessageInput = <
asyncIds,
asyncMessagesLockDistance,
asyncMessagesMinimumPressDuration,
asyncMessagesMultiSendEnabled,
asyncMessagesSlideToCancelDistance,
asyncUploads,
AttachmentPickerSelectionBar,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const useAudioController = () => {
useEffect(
() => () => {
stopVoicePlayer();
deleteVoiceRecording();
},
[],
);
Expand Down Expand Up @@ -163,7 +164,6 @@ export const useAudioController = () => {
*/
const startVoiceRecording = async () => {
if (!Audio) return;
setRecordingStatus('recording');
const recordingInfo = await Audio.startRecording(
{
isMeteringEnabled: true,
Expand All @@ -178,6 +178,7 @@ export const useAudioController = () => {
recording.setProgressUpdateInterval(Platform.OS === 'android' ? 100 : 60);
}
setRecording(recording);
setRecordingStatus('recording');
await stopVoicePlayer();
} else {
setPermissionsGranted(false);
Expand Down