Voice notes drafts
This commit is contained in:
parent
356fb301e1
commit
99015d7b96
48 changed files with 2113 additions and 909 deletions
156
ts/components/CompositionRecording.tsx
Normal file
156
ts/components/CompositionRecording.tsx
Normal file
|
@ -0,0 +1,156 @@
|
|||
// 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>
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue