Signal-Desktop/ts/components/conversation/AudioCapture.tsx

253 lines
7.1 KiB
TypeScript

// Copyright 2016-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback, useEffect, useState } from 'react';
import * as moment from 'moment';
import { noop } from 'lodash';
import type {
AttachmentDraftType,
InMemoryAttachmentDraftType,
} from '../../types/Attachment';
import { ConfirmationDialog } from '../ConfirmationDialog';
import type { LocalizerType } from '../../types/Util';
import {
ErrorDialogAudioRecorderType,
RecordingState,
} from '../../state/ducks/audioRecorder';
import { ToastVoiceNoteLimit } from '../ToastVoiceNoteLimit';
import { ToastVoiceNoteMustBeOnlyAttachment } from '../ToastVoiceNoteMustBeOnlyAttachment';
import { useEscapeHandling } from '../../hooks/useEscapeHandling';
import {
useStartRecordingShortcut,
useKeyboardShortcuts,
} from '../../hooks/useKeyboardShortcuts';
type OnSendAudioRecordingType = (rec: InMemoryAttachmentDraftType) => unknown;
export type PropsType = {
cancelRecording: () => unknown;
conversationId: string;
completeRecording: (
conversationId: string,
onSendAudioRecording?: OnSendAudioRecordingType
) => unknown;
draftAttachments: ReadonlyArray<AttachmentDraftType>;
errorDialogAudioRecorderType?: ErrorDialogAudioRecorderType;
errorRecording: (e: ErrorDialogAudioRecorderType) => unknown;
i18n: LocalizerType;
recordingState: RecordingState;
onSendAudioRecording: OnSendAudioRecordingType;
startRecording: () => unknown;
};
enum ToastType {
VoiceNoteLimit,
VoiceNoteMustBeOnlyAttachment,
}
const START_DURATION_TEXT = '0:00';
export const AudioCapture = ({
cancelRecording,
completeRecording,
conversationId,
draftAttachments,
errorDialogAudioRecorderType,
errorRecording,
i18n,
recordingState,
onSendAudioRecording,
startRecording,
}: PropsType): JSX.Element => {
const [durationText, setDurationText] = useState<string>(START_DURATION_TEXT);
const [toastType, setToastType] = useState<ToastType | undefined>();
// Cancel recording if we switch away from this conversation, unmounting
useEffect(() => {
return () => {
cancelRecording();
};
}, [cancelRecording]);
// Stop recording and show confirmation if user switches away from this app
useEffect(() => {
if (recordingState !== RecordingState.Recording) {
return;
}
const handler = () => {
errorRecording(ErrorDialogAudioRecorderType.Blur);
};
window.addEventListener('blur', handler);
return () => {
window.removeEventListener('blur', handler);
};
}, [recordingState, completeRecording, errorRecording]);
const escapeRecording = useCallback(() => {
if (recordingState !== RecordingState.Recording) {
return;
}
cancelRecording();
}, [cancelRecording, recordingState]);
useEscapeHandling(escapeRecording);
const startRecordingShortcut = useStartRecordingShortcut(startRecording);
useKeyboardShortcuts(startRecordingShortcut);
const closeToast = useCallback(() => {
setToastType(undefined);
}, []);
// Update timestamp regularly, then timeout if recording goes over five minutes
useEffect(() => {
if (recordingState !== RecordingState.Recording) {
return;
}
setDurationText(START_DURATION_TEXT);
setToastType(ToastType.VoiceNoteLimit);
const startTime = Date.now();
const interval = setInterval(() => {
const duration = moment.duration(Date.now() - startTime, 'ms');
const minutes = `${Math.trunc(duration.asMinutes())}`;
let seconds = `${duration.seconds()}`;
if (seconds.length < 2) {
seconds = `0${seconds}`;
}
setDurationText(`${minutes}:${seconds}`);
if (duration >= moment.duration(1, 'hours')) {
errorRecording(ErrorDialogAudioRecorderType.Timeout);
}
}, 1000);
return () => {
clearInterval(interval);
closeToast();
};
}, [
closeToast,
completeRecording,
errorRecording,
recordingState,
setDurationText,
]);
const clickCancel = useCallback(() => {
cancelRecording();
}, [cancelRecording]);
const clickSend = useCallback(() => {
completeRecording(conversationId, onSendAudioRecording);
}, [conversationId, completeRecording, onSendAudioRecording]);
let toastElement: JSX.Element | undefined;
if (toastType === ToastType.VoiceNoteLimit) {
toastElement = <ToastVoiceNoteLimit i18n={i18n} onClose={closeToast} />;
} else if (toastType === ToastType.VoiceNoteMustBeOnlyAttachment) {
toastElement = (
<ToastVoiceNoteMustBeOnlyAttachment i18n={i18n} onClose={closeToast} />
);
}
let confirmationDialog: JSX.Element | undefined;
if (
errorDialogAudioRecorderType === ErrorDialogAudioRecorderType.Blur ||
errorDialogAudioRecorderType === ErrorDialogAudioRecorderType.Timeout
) {
const confirmationDialogText =
errorDialogAudioRecorderType === ErrorDialogAudioRecorderType.Blur
? i18n('voiceRecordingInterruptedBlur')
: i18n('voiceRecordingInterruptedMax');
confirmationDialog = (
<ConfirmationDialog
i18n={i18n}
onCancel={clickCancel}
onClose={noop}
cancelText={i18n('discard')}
actions={[
{
text: i18n('sendAnyway'),
style: 'affirmative',
action: clickSend,
},
]}
>
{confirmationDialogText}
</ConfirmationDialog>
);
} else if (
errorDialogAudioRecorderType === ErrorDialogAudioRecorderType.ErrorRecording
) {
confirmationDialog = (
<ConfirmationDialog
i18n={i18n}
onCancel={clickCancel}
onClose={noop}
cancelText={i18n('ok')}
actions={[]}
>
{i18n('voiceNoteError')}
</ConfirmationDialog>
);
}
if (recordingState === RecordingState.Recording && !confirmationDialog) {
return (
<>
<div className="AudioCapture">
<button
className="AudioCapture__recorder-button AudioCapture__recorder-button--complete"
onClick={clickSend}
tabIndex={0}
title={i18n('voiceRecording--complete')}
type="button"
>
<span className="icon" />
</button>
<span className="AudioCapture__time">{durationText}</span>
<button
className="AudioCapture__recorder-button AudioCapture__recorder-button--cancel"
onClick={clickCancel}
tabIndex={0}
title={i18n('voiceRecording--cancel')}
type="button"
>
<span className="icon" />
</button>
</div>
{toastElement}
</>
);
}
return (
<>
<div className="AudioCapture">
<button
aria-label={i18n('voiceRecording--start')}
className="AudioCapture__microphone"
onClick={() => {
if (draftAttachments.length) {
setToastType(ToastType.VoiceNoteMustBeOnlyAttachment);
} else {
startRecording();
}
}}
title={i18n('voiceRecording--start')}
type="button"
/>
{confirmationDialog}
</div>
{toastElement}
</>
);
};