Handle duplicate requests to start recording a voice note
This commit is contained in:
parent
03631481e1
commit
c5b5f2fe42
6 changed files with 97 additions and 43 deletions
|
@ -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),
|
||||
|
|
|
@ -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 });
|
||||
}}
|
||||
|
|
|
@ -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,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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,30 +82,40 @@ 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();
|
||||
const started = await recorder.start();
|
||||
|
||||
if (started) {
|
||||
dispatch({
|
||||
type: NOW_RECORDING,
|
||||
payload: undefined,
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: ERROR_RECORDING,
|
||||
payload: ErrorDialogAudioRecorderType.ErrorRecording,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
dispatch({
|
||||
type: ERROR_RECORDING,
|
||||
payload: ErrorDialogAudioRecorderType.ErrorRecording,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (recordingStarted) {
|
||||
dispatch({
|
||||
type: START_RECORDING,
|
||||
payload: undefined,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue