Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
56 changes: 48 additions & 8 deletions package/expo-package/src/handlers/Audio.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AudioComponent } from '../optionalDependencies/Video';
import { AudioComponent, RecordingObject } from '../optionalDependencies/Video';

export enum AndroidOutputFormat {
DEFAULT = 0,
Expand Down Expand Up @@ -212,13 +212,39 @@ 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) => {
? class {
recording: typeof RecordingObject | null;

constructor() {
this.recording = null;
}
async startRecording(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,24 +270,38 @@ 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,
);
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;
4 changes: 3 additions & 1 deletion package/expo-package/src/optionalDependencies/Video.ts
Original file line number Diff line number Diff line change
@@ -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
}
Expand All @@ -14,4 +16,4 @@ if (!VideoComponent || !AudioComponent) {
);
}

export { AudioComponent, VideoComponent };
export { AudioComponent, VideoComponent, RecordingObject };
42 changes: 27 additions & 15 deletions package/native-package/src/optionalDependencies/Audio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -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();
Expand Down Expand Up @@ -222,20 +223,31 @@ 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 };
}
},
stopPlayer: async () => {
}
async stopPlayer() {
try {
await audioRecorderPlayer.stopPlayer();
audioRecorderPlayer.removePlayBackListener();
} catch (error) {
console.log(error);
}
},
stopRecording: async () => {
await audioRecorderPlayer.stopRecorder();
audioRecorderPlayer.removeRecordBackListener();
},
}
async stopRecording() {
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
Loading