diff --git a/ts/components/CompositionArea.stories.tsx b/ts/components/CompositionArea.stories.tsx index 2d57ff2fb4c..2b05179d6f6 100644 --- a/ts/components/CompositionArea.stories.tsx +++ b/ts/components/CompositionArea.stories.tsx @@ -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); @@ -42,7 +43,11 @@ const createProps = (overrideProps: Partial = {}): 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), diff --git a/ts/components/CompositionArea.tsx b/ts/components/CompositionArea.tsx index c4ab4bafe35..f0c59552d19 100644 --- a/ts/components/CompositionArea.tsx +++ b/ts/components/CompositionArea.tsx @@ -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; @@ -176,7 +179,7 @@ export const CompositionArea = ({ completeRecording, errorDialogAudioRecorderType, errorRecording, - isRecording, + recordingState, startRecording, // StagedLinkPreview linkPreviewLoading, @@ -371,7 +374,7 @@ export const CompositionArea = ({ errorDialogAudioRecorderType={errorDialogAudioRecorderType} errorRecording={errorRecording} i18n={i18n} - isRecording={isRecording} + recordingState={recordingState} onSendAudioRecording={(voiceNoteAttachment: AttachmentType) => { onSendMessage({ voiceNoteAttachment }); }} diff --git a/ts/components/conversation/AudioCapture.stories.tsx b/ts/components/conversation/AudioCapture.stories.tsx index fe9b3e38d3d..b44e89d5cec 100644 --- a/ts/components/conversation/AudioCapture.stories.tsx +++ b/ts/components/conversation/AudioCapture.stories.tsx @@ -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 => ({ 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 ; }); +story.add('Initializing', () => { + return ( + + ); +}); + story.add('Recording', () => { return ( ); @@ -49,7 +66,7 @@ story.add('Voice Limit', () => { ); @@ -60,7 +77,7 @@ story.add('Switched Apps', () => { ); diff --git a/ts/components/conversation/AudioCapture.tsx b/ts/components/conversation/AudioCapture.tsx index 31861f8f484..fc93b42eef6 100644 --- a/ts/components/conversation/AudioCapture.tsx +++ b/ts/components/conversation/AudioCapture.tsx @@ -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 ( <>
diff --git a/ts/state/ducks/audioRecorder.ts b/ts/state/ducks/audioRecorder.ts index c1fa00f4d20..fe91e7a247e 100644 --- a/ts/state/ducks/audioRecorder.ts +++ b/ts/state/ducks/audioRecorder.ts @@ -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, }; } diff --git a/ts/state/smart/CompositionArea.tsx b/ts/state/smart/CompositionArea.tsx index 1f63f249d34..95970206188 100644 --- a/ts/state/smart/CompositionArea.tsx +++ b/ts/state/smart/CompositionArea.tsx @@ -87,7 +87,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => { // AudioCapture errorDialogAudioRecorderType: state.audioRecorder.errorDialogAudioRecorderType, - isRecording: state.audioRecorder.isRecording, + recordingState: state.audioRecorder.recordingState, // AttachmentsList draftAttachments, // MediaQualitySelector