Handle duplicate requests to start recording a voice note

This commit is contained in:
Scott Nonnenberg 2021-11-11 15:33:35 -08:00 committed by GitHub
parent 03631481e1
commit c5b5f2fe42
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 97 additions and 43 deletions

View file

@ -5,7 +5,7 @@ import * as React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { boolean } from '@storybook/addon-knobs';
import { boolean, select } from '@storybook/addon-knobs';
import { IMAGE_JPEG } from '../types/MIME';
import type { Props } from './CompositionArea';
@ -16,6 +16,7 @@ import enMessages from '../../_locales/en/messages.json';
import { fakeAttachment } from '../test-both/helpers/fakeAttachment';
import { landscapeGreenUrl } from '../storybook/Fixtures';
import { ThemeType } from '../types/Util';
import { RecordingState } from '../state/ducks/audioRecorder';
const i18n = setupI18n('en', enMessages);
@ -41,7 +42,11 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
cancelRecording: action('cancelRecording'),
completeRecording: action('completeRecording'),
errorRecording: action('errorRecording'),
isRecording: Boolean(overrideProps.isRecording),
recordingState: select(
'recordingState',
RecordingState,
overrideProps.recordingState || RecordingState.Idle
),
startRecording: action('startRecording'),
// StagedLinkPreview
linkPreviewLoading: Boolean(overrideProps.linkPreviewLoading),

View file

@ -11,7 +11,10 @@ import type {
LocalizerType,
ThemeType,
} from '../types/Util';
import type { ErrorDialogAudioRecorderType } from '../state/ducks/audioRecorder';
import type {
ErrorDialogAudioRecorderType,
RecordingState,
} from '../state/ducks/audioRecorder';
import type { HandleAttachmentsProcessingArgsType } from '../util/handleAttachmentsProcessing';
import { Spinner } from './Spinner';
import type { Props as EmojiButtonProps } from './emoji/EmojiButton';
@ -90,7 +93,7 @@ export type OwnProps = Readonly<{
isFetchingUUID?: boolean;
isGroupV1AndDisabled?: boolean;
isMissingMandatoryProfileSharing?: boolean;
isRecording: boolean;
recordingState: RecordingState;
isSMSOnly?: boolean;
left?: boolean;
linkPreviewLoading: boolean;
@ -174,7 +177,7 @@ export const CompositionArea = ({
completeRecording,
errorDialogAudioRecorderType,
errorRecording,
isRecording,
recordingState,
startRecording,
// StagedLinkPreview
linkPreviewLoading,
@ -369,7 +372,7 @@ export const CompositionArea = ({
errorDialogAudioRecorderType={errorDialogAudioRecorderType}
errorRecording={errorRecording}
i18n={i18n}
isRecording={isRecording}
recordingState={recordingState}
onSendAudioRecording={(voiceNoteAttachment: AttachmentType) => {
onSendMessage({ voiceNoteAttachment });
}}

View file

@ -5,9 +5,12 @@ import * as React from 'react';
import { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';
import { boolean } from '@storybook/addon-knobs';
import { select } from '@storybook/addon-knobs';
import { ErrorDialogAudioRecorderType } from '../../state/ducks/audioRecorder';
import {
ErrorDialogAudioRecorderType,
RecordingState,
} from '../../state/ducks/audioRecorder';
import type { PropsType } from './AudioCapture';
import { AudioCapture } from './AudioCapture';
import { setupI18n } from '../../util/setupI18n';
@ -25,7 +28,11 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
errorDialogAudioRecorderType: overrideProps.errorDialogAudioRecorderType,
errorRecording: action('errorRecording'),
i18n,
isRecording: boolean('isRecording', overrideProps.isRecording || false),
recordingState: select(
'recordingState',
RecordingState,
overrideProps.recordingState || RecordingState.Idle
),
onSendAudioRecording: action('onSendAudioRecording'),
startRecording: action('startRecording'),
});
@ -34,11 +41,21 @@ story.add('Default', () => {
return <AudioCapture {...createProps()} />;
});
story.add('Initializing', () => {
return (
<AudioCapture
{...createProps({
recordingState: RecordingState.Initializing,
})}
/>
);
});
story.add('Recording', () => {
return (
<AudioCapture
{...createProps({
isRecording: true,
recordingState: RecordingState.Recording,
})}
/>
);
@ -49,7 +66,7 @@ story.add('Voice Limit', () => {
<AudioCapture
{...createProps({
errorDialogAudioRecorderType: ErrorDialogAudioRecorderType.Timeout,
isRecording: true,
recordingState: RecordingState.Recording,
})}
/>
);
@ -60,7 +77,7 @@ story.add('Switched Apps', () => {
<AudioCapture
{...createProps({
errorDialogAudioRecorderType: ErrorDialogAudioRecorderType.Blur,
isRecording: true,
recordingState: RecordingState.Recording,
})}
/>
);

View file

@ -8,7 +8,10 @@ import { noop } from 'lodash';
import type { AttachmentType } from '../../types/Attachment';
import { ConfirmationDialog } from '../ConfirmationDialog';
import type { LocalizerType } from '../../types/Util';
import { ErrorDialogAudioRecorderType } from '../../state/ducks/audioRecorder';
import {
ErrorDialogAudioRecorderType,
RecordingState,
} from '../../state/ducks/audioRecorder';
import { ToastVoiceNoteLimit } from '../ToastVoiceNoteLimit';
import { ToastVoiceNoteMustBeOnlyAttachment } from '../ToastVoiceNoteMustBeOnlyAttachment';
import { useEscapeHandling } from '../../hooks/useEscapeHandling';
@ -30,7 +33,7 @@ export type PropsType = {
errorDialogAudioRecorderType?: ErrorDialogAudioRecorderType;
errorRecording: (e: ErrorDialogAudioRecorderType) => unknown;
i18n: LocalizerType;
isRecording: boolean;
recordingState: RecordingState;
onSendAudioRecording: OnSendAudioRecordingType;
startRecording: () => unknown;
};
@ -50,7 +53,7 @@ export const AudioCapture = ({
errorDialogAudioRecorderType,
errorRecording,
i18n,
isRecording,
recordingState,
onSendAudioRecording,
startRecording,
}: PropsType): JSX.Element => {
@ -59,18 +62,14 @@ export const AudioCapture = ({
// Cancel recording if we switch away from this conversation, unmounting
useEffect(() => {
if (!isRecording) {
return;
}
return () => {
cancelRecording();
};
}, [cancelRecording, isRecording]);
}, [cancelRecording]);
// Stop recording and show confirmation if user switches away from this app
useEffect(() => {
if (!isRecording) {
if (recordingState !== RecordingState.Recording) {
return;
}
@ -82,15 +81,15 @@ export const AudioCapture = ({
return () => {
window.removeEventListener('blur', handler);
};
}, [isRecording, completeRecording, errorRecording]);
}, [recordingState, completeRecording, errorRecording]);
const escapeRecording = useCallback(() => {
if (!isRecording) {
if (recordingState !== RecordingState.Recording) {
return;
}
cancelRecording();
}, [cancelRecording, isRecording]);
}, [cancelRecording, recordingState]);
useEscapeHandling(escapeRecording);
@ -103,7 +102,7 @@ export const AudioCapture = ({
// Update timestamp regularly, then timeout if recording goes over five minutes
useEffect(() => {
if (!isRecording) {
if (recordingState !== RecordingState.Recording) {
return;
}
@ -133,7 +132,7 @@ export const AudioCapture = ({
closeToast,
completeRecording,
errorRecording,
isRecording,
recordingState,
setDurationText,
]);
@ -197,7 +196,7 @@ export const AudioCapture = ({
);
}
if (isRecording && !confirmationDialog) {
if (recordingState === RecordingState.Recording && !confirmationDialog) {
return (
<>
<div className="AudioCapture">

View file

@ -20,8 +20,14 @@ export enum ErrorDialogAudioRecorderType {
// State
export enum RecordingState {
Recording = 'recording',
Initializing = 'initializing',
Idle = 'idle',
}
export type AudioPlayerStateType = {
readonly isRecording: boolean;
readonly recordingState: RecordingState;
readonly errorDialogAudioRecorderType?: ErrorDialogAudioRecorderType;
};
@ -30,6 +36,7 @@ export type AudioPlayerStateType = {
const CANCEL_RECORDING = 'audioRecorder/CANCEL_RECORDING';
const COMPLETE_RECORDING = 'audioRecorder/COMPLETE_RECORDING';
const ERROR_RECORDING = 'audioRecorder/ERROR_RECORDING';
const NOW_RECORDING = 'audioRecorder/NOW_RECORDING';
const START_RECORDING = 'audioRecorder/START_RECORDING';
type CancelRecordingAction = {
@ -48,11 +55,16 @@ type StartRecordingAction = {
type: typeof START_RECORDING;
payload: undefined;
};
type NowRecordingAction = {
type: typeof NOW_RECORDING;
payload: undefined;
};
type AudioPlayerActionType =
| CancelRecordingAction
| CompleteRecordingAction
| ErrorRecordingAction
| NowRecordingAction
| StartRecordingAction;
// Action Creators
@ -70,29 +82,39 @@ function startRecording(): ThunkAction<
void,
RootStateType,
unknown,
StartRecordingAction | ErrorRecordingAction
StartRecordingAction | NowRecordingAction | ErrorRecordingAction
> {
return async (dispatch, getState) => {
if (getState().composer.attachments.length) {
return;
}
if (getState().audioRecorder.recordingState !== RecordingState.Idle) {
return;
}
let recordingStarted = false;
dispatch({
type: START_RECORDING,
payload: undefined,
});
try {
recordingStarted = await recorder.start();
} catch (err) {
const started = await recorder.start();
if (started) {
dispatch({
type: NOW_RECORDING,
payload: undefined,
});
} else {
dispatch({
type: ERROR_RECORDING,
payload: ErrorDialogAudioRecorderType.ErrorRecording,
});
return;
}
if (recordingStarted) {
} catch (err) {
dispatch({
type: START_RECORDING,
payload: undefined,
type: ERROR_RECORDING,
payload: ErrorDialogAudioRecorderType.ErrorRecording,
});
}
};
@ -184,7 +206,7 @@ function errorRecording(
function getEmptyState(): AudioPlayerStateType {
return {
isRecording: false,
recordingState: RecordingState.Idle,
};
}
@ -196,7 +218,15 @@ export function reducer(
return {
...state,
errorDialogAudioRecorderType: undefined,
isRecording: true,
recordingState: RecordingState.Initializing,
};
}
if (action.type === NOW_RECORDING) {
return {
...state,
errorDialogAudioRecorderType: undefined,
recordingState: RecordingState.Recording,
};
}
@ -204,7 +234,7 @@ export function reducer(
return {
...state,
errorDialogAudioRecorderType: undefined,
isRecording: false,
recordingState: RecordingState.Idle,
};
}

View file

@ -83,7 +83,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
// AudioCapture
errorDialogAudioRecorderType:
state.audioRecorder.errorDialogAudioRecorderType,
isRecording: state.audioRecorder.isRecording,
recordingState: state.audioRecorder.recordingState,
// AttachmentsList
draftAttachments,
// MediaQualitySelector