Handle duplicate requests to start recording a voice note
This commit is contained in:
parent
03631481e1
commit
c5b5f2fe42
|
@ -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),
|
||||||
|
|
|
@ -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 });
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -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,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue