157 lines
4.6 KiB
TypeScript
157 lines
4.6 KiB
TypeScript
|
// Copyright 2023 Signal Messenger, LLC
|
||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||
|
|
||
|
import { noop } from 'lodash';
|
||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||
|
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
||
|
import { usePrevious } from '../hooks/usePrevious';
|
||
|
import type { InMemoryAttachmentDraftType } from '../types/Attachment';
|
||
|
import { ErrorDialogAudioRecorderType } from '../types/AudioRecorder';
|
||
|
import type { LocalizerType } from '../types/Util';
|
||
|
import { DurationInSeconds, SECOND } from '../util/durations';
|
||
|
import { durationToPlaybackText } from '../util/durationToPlaybackText';
|
||
|
import { ConfirmationDialog } from './ConfirmationDialog';
|
||
|
import { RecordingComposer } from './RecordingComposer';
|
||
|
import { ToastVoiceNoteLimit } from './ToastVoiceNoteLimit';
|
||
|
|
||
|
export type Props = {
|
||
|
i18n: LocalizerType;
|
||
|
conversationId: string;
|
||
|
onCancel: () => void;
|
||
|
onSend: () => void;
|
||
|
errorRecording: (e: ErrorDialogAudioRecorderType) => unknown;
|
||
|
errorDialogAudioRecorderType?: ErrorDialogAudioRecorderType;
|
||
|
addAttachment: (
|
||
|
conversationId: string,
|
||
|
attachment: InMemoryAttachmentDraftType
|
||
|
) => unknown;
|
||
|
completeRecording: (
|
||
|
conversationId: string,
|
||
|
onRecordingComplete: (rec: InMemoryAttachmentDraftType) => unknown
|
||
|
) => unknown;
|
||
|
};
|
||
|
|
||
|
export function CompositionRecording({
|
||
|
i18n,
|
||
|
conversationId,
|
||
|
onCancel,
|
||
|
onSend,
|
||
|
errorRecording,
|
||
|
errorDialogAudioRecorderType,
|
||
|
addAttachment,
|
||
|
completeRecording,
|
||
|
}: Props): JSX.Element {
|
||
|
useEscapeHandling(onCancel);
|
||
|
|
||
|
const [showVoiceNoteLimitToast, setShowVoiceNoteLimitToast] = useState(true);
|
||
|
|
||
|
// when interrupted (blur, switching convos)
|
||
|
// stop recording and save draft
|
||
|
const handleRecordingInterruption = useCallback(() => {
|
||
|
completeRecording(conversationId, attachment => {
|
||
|
addAttachment(conversationId, attachment);
|
||
|
});
|
||
|
}, [conversationId, completeRecording, addAttachment]);
|
||
|
|
||
|
// switched to another app
|
||
|
useEffect(() => {
|
||
|
window.addEventListener('blur', handleRecordingInterruption);
|
||
|
return () => {
|
||
|
window.removeEventListener('blur', handleRecordingInterruption);
|
||
|
};
|
||
|
}, [handleRecordingInterruption]);
|
||
|
|
||
|
// switched conversations
|
||
|
const previousConversationId = usePrevious(conversationId, conversationId);
|
||
|
useEffect(() => {
|
||
|
if (previousConversationId !== conversationId) {
|
||
|
handleRecordingInterruption();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const handleCloseToast = useCallback(() => {
|
||
|
setShowVoiceNoteLimitToast(false);
|
||
|
}, []);
|
||
|
|
||
|
useEffect(() => {
|
||
|
return () => {
|
||
|
handleCloseToast();
|
||
|
};
|
||
|
}, [handleCloseToast]);
|
||
|
|
||
|
const startTime = useRef(Date.now());
|
||
|
const [duration, setDuration] = useState(0);
|
||
|
const drift = useRef(0);
|
||
|
|
||
|
// update recording duration
|
||
|
useEffect(() => {
|
||
|
const timeoutId = setTimeout(() => {
|
||
|
const now = Date.now();
|
||
|
const newDurationMs = now - startTime.current;
|
||
|
drift.current = newDurationMs % SECOND;
|
||
|
setDuration(newDurationMs / SECOND);
|
||
|
|
||
|
if (
|
||
|
DurationInSeconds.fromMillis(newDurationMs) >= DurationInSeconds.HOUR
|
||
|
) {
|
||
|
errorRecording(ErrorDialogAudioRecorderType.Timeout);
|
||
|
}
|
||
|
}, SECOND - drift.current);
|
||
|
|
||
|
return () => {
|
||
|
clearTimeout(timeoutId);
|
||
|
};
|
||
|
}, [duration, errorRecording]);
|
||
|
|
||
|
let confirmationDialog: JSX.Element | undefined;
|
||
|
if (errorDialogAudioRecorderType === ErrorDialogAudioRecorderType.Timeout) {
|
||
|
confirmationDialog = (
|
||
|
<ConfirmationDialog
|
||
|
dialogName="AudioCapture.sendAnyway"
|
||
|
i18n={i18n}
|
||
|
onCancel={onCancel}
|
||
|
onClose={noop}
|
||
|
cancelText={i18n('discard')}
|
||
|
actions={[
|
||
|
{
|
||
|
text: i18n('sendAnyway'),
|
||
|
style: 'affirmative',
|
||
|
action: onSend,
|
||
|
},
|
||
|
]}
|
||
|
>
|
||
|
{i18n('voiceRecordingInterruptedMax')}
|
||
|
</ConfirmationDialog>
|
||
|
);
|
||
|
} else if (
|
||
|
errorDialogAudioRecorderType === ErrorDialogAudioRecorderType.ErrorRecording
|
||
|
) {
|
||
|
confirmationDialog = (
|
||
|
<ConfirmationDialog
|
||
|
dialogName="AudioCapture.error"
|
||
|
i18n={i18n}
|
||
|
onCancel={onCancel}
|
||
|
onClose={noop}
|
||
|
cancelText={i18n('ok')}
|
||
|
actions={[]}
|
||
|
>
|
||
|
{i18n('voiceNoteError')}
|
||
|
</ConfirmationDialog>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
return (
|
||
|
<RecordingComposer i18n={i18n} onCancel={onCancel} onSend={onSend}>
|
||
|
<div className="CompositionRecording__microphone" />
|
||
|
<div className="CompositionRecording__timer">
|
||
|
{durationToPlaybackText(duration)}
|
||
|
</div>
|
||
|
|
||
|
{confirmationDialog}
|
||
|
{showVoiceNoteLimitToast && (
|
||
|
<ToastVoiceNoteLimit i18n={i18n} onClose={handleCloseToast} />
|
||
|
)}
|
||
|
</RecordingComposer>
|
||
|
);
|
||
|
}
|