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

View File

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

View File

@ -5,9 +5,12 @@ import * as React from 'react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react'; 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 type { PropsType } from './AudioCapture';
import { AudioCapture } from './AudioCapture'; import { AudioCapture } from './AudioCapture';
import { setupI18n } from '../../util/setupI18n'; import { setupI18n } from '../../util/setupI18n';
@ -25,7 +28,11 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
errorDialogAudioRecorderType: overrideProps.errorDialogAudioRecorderType, errorDialogAudioRecorderType: overrideProps.errorDialogAudioRecorderType,
errorRecording: action('errorRecording'), errorRecording: action('errorRecording'),
i18n, i18n,
isRecording: boolean('isRecording', overrideProps.isRecording || false), recordingState: select(
'recordingState',
RecordingState,
overrideProps.recordingState || RecordingState.Idle
),
onSendAudioRecording: action('onSendAudioRecording'), onSendAudioRecording: action('onSendAudioRecording'),
startRecording: action('startRecording'), startRecording: action('startRecording'),
}); });
@ -34,11 +41,21 @@ story.add('Default', () => {
return <AudioCapture {...createProps()} />; return <AudioCapture {...createProps()} />;
}); });
story.add('Initializing', () => {
return (
<AudioCapture
{...createProps({
recordingState: RecordingState.Initializing,
})}
/>
);
});
story.add('Recording', () => { story.add('Recording', () => {
return ( return (
<AudioCapture <AudioCapture
{...createProps({ {...createProps({
isRecording: true, recordingState: RecordingState.Recording,
})} })}
/> />
); );
@ -49,7 +66,7 @@ story.add('Voice Limit', () => {
<AudioCapture <AudioCapture
{...createProps({ {...createProps({
errorDialogAudioRecorderType: ErrorDialogAudioRecorderType.Timeout, errorDialogAudioRecorderType: ErrorDialogAudioRecorderType.Timeout,
isRecording: true, recordingState: RecordingState.Recording,
})} })}
/> />
); );
@ -60,7 +77,7 @@ story.add('Switched Apps', () => {
<AudioCapture <AudioCapture
{...createProps({ {...createProps({
errorDialogAudioRecorderType: ErrorDialogAudioRecorderType.Blur, 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 type { AttachmentType } from '../../types/Attachment';
import { ConfirmationDialog } from '../ConfirmationDialog'; import { ConfirmationDialog } from '../ConfirmationDialog';
import type { LocalizerType } from '../../types/Util'; import type { LocalizerType } from '../../types/Util';
import { ErrorDialogAudioRecorderType } from '../../state/ducks/audioRecorder'; import {
ErrorDialogAudioRecorderType,
RecordingState,
} from '../../state/ducks/audioRecorder';
import { ToastVoiceNoteLimit } from '../ToastVoiceNoteLimit'; import { ToastVoiceNoteLimit } from '../ToastVoiceNoteLimit';
import { ToastVoiceNoteMustBeOnlyAttachment } from '../ToastVoiceNoteMustBeOnlyAttachment'; import { ToastVoiceNoteMustBeOnlyAttachment } from '../ToastVoiceNoteMustBeOnlyAttachment';
import { useEscapeHandling } from '../../hooks/useEscapeHandling'; import { useEscapeHandling } from '../../hooks/useEscapeHandling';
@ -30,7 +33,7 @@ export type PropsType = {
errorDialogAudioRecorderType?: ErrorDialogAudioRecorderType; errorDialogAudioRecorderType?: ErrorDialogAudioRecorderType;
errorRecording: (e: ErrorDialogAudioRecorderType) => unknown; errorRecording: (e: ErrorDialogAudioRecorderType) => unknown;
i18n: LocalizerType; i18n: LocalizerType;
isRecording: boolean; recordingState: RecordingState;
onSendAudioRecording: OnSendAudioRecordingType; onSendAudioRecording: OnSendAudioRecordingType;
startRecording: () => unknown; startRecording: () => unknown;
}; };
@ -50,7 +53,7 @@ export const AudioCapture = ({
errorDialogAudioRecorderType, errorDialogAudioRecorderType,
errorRecording, errorRecording,
i18n, i18n,
isRecording, recordingState,
onSendAudioRecording, onSendAudioRecording,
startRecording, startRecording,
}: PropsType): JSX.Element => { }: PropsType): JSX.Element => {
@ -59,18 +62,14 @@ export const AudioCapture = ({
// Cancel recording if we switch away from this conversation, unmounting // Cancel recording if we switch away from this conversation, unmounting
useEffect(() => { useEffect(() => {
if (!isRecording) {
return;
}
return () => { return () => {
cancelRecording(); cancelRecording();
}; };
}, [cancelRecording, isRecording]); }, [cancelRecording]);
// Stop recording and show confirmation if user switches away from this app // Stop recording and show confirmation if user switches away from this app
useEffect(() => { useEffect(() => {
if (!isRecording) { if (recordingState !== RecordingState.Recording) {
return; return;
} }
@ -82,15 +81,15 @@ export const AudioCapture = ({
return () => { return () => {
window.removeEventListener('blur', handler); window.removeEventListener('blur', handler);
}; };
}, [isRecording, completeRecording, errorRecording]); }, [recordingState, completeRecording, errorRecording]);
const escapeRecording = useCallback(() => { const escapeRecording = useCallback(() => {
if (!isRecording) { if (recordingState !== RecordingState.Recording) {
return; return;
} }
cancelRecording(); cancelRecording();
}, [cancelRecording, isRecording]); }, [cancelRecording, recordingState]);
useEscapeHandling(escapeRecording); useEscapeHandling(escapeRecording);
@ -103,7 +102,7 @@ export const AudioCapture = ({
// Update timestamp regularly, then timeout if recording goes over five minutes // Update timestamp regularly, then timeout if recording goes over five minutes
useEffect(() => { useEffect(() => {
if (!isRecording) { if (recordingState !== RecordingState.Recording) {
return; return;
} }
@ -133,7 +132,7 @@ export const AudioCapture = ({
closeToast, closeToast,
completeRecording, completeRecording,
errorRecording, errorRecording,
isRecording, recordingState,
setDurationText, setDurationText,
]); ]);
@ -197,7 +196,7 @@ export const AudioCapture = ({
); );
} }
if (isRecording && !confirmationDialog) { if (recordingState === RecordingState.Recording && !confirmationDialog) {
return ( return (
<> <>
<div className="AudioCapture"> <div className="AudioCapture">

View File

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

View File

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