mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-07-05 04:45:31 +01:00
145 lines
4.5 KiB
TypeScript
145 lines
4.5 KiB
TypeScript
// Copyright 2023 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
import { useEffect, useRef, useState, type JSX } from 'react';
|
|
import { useEscapeHandling } from '../hooks/useEscapeHandling.dom.ts';
|
|
import type {
|
|
HideToastAction,
|
|
ShowToastAction,
|
|
} from '../state/ducks/toast.preload.ts';
|
|
import type { PeakType } from '../types/Audio.dom.tsx';
|
|
import { ErrorDialogAudioRecorderType } from '../types/AudioRecorder.std.ts';
|
|
import type { LocalizerType } from '../types/Util.std.ts';
|
|
import type { AnyToast } from '../types/Toast.dom.tsx';
|
|
import { ToastType } from '../types/Toast.dom.tsx';
|
|
import { DurationInSeconds, SECOND } from '../util/durations/index.std.ts';
|
|
import { tw } from '../axo/tw.dom.tsx';
|
|
import { durationToPlaybackText } from '../util/durationToPlaybackText.std.ts';
|
|
import { RecordingComposer } from './RecordingComposer.dom.tsx';
|
|
import { AxoConfirmDialog } from '../axo/AxoConfirmDialog.dom.tsx';
|
|
|
|
export type Props = Readonly<{
|
|
i18n: LocalizerType;
|
|
onCancel: () => void;
|
|
onSend: () => void;
|
|
errorRecording: (e: ErrorDialogAudioRecorderType) => unknown;
|
|
errorDialogAudioRecorderType?: ErrorDialogAudioRecorderType;
|
|
peaks: ReadonlyArray<PeakType>;
|
|
saveDraftRecordingIfNeeded: () => void;
|
|
showToast: ShowToastAction;
|
|
hideToast: HideToastAction;
|
|
}>;
|
|
|
|
export function CompositionRecording({
|
|
i18n,
|
|
onCancel,
|
|
onSend,
|
|
errorRecording,
|
|
errorDialogAudioRecorderType,
|
|
peaks,
|
|
saveDraftRecordingIfNeeded,
|
|
showToast,
|
|
hideToast,
|
|
}: Props): JSX.Element {
|
|
useEscapeHandling(onCancel);
|
|
|
|
// switched to another app
|
|
useEffect(() => {
|
|
window.addEventListener('blur', saveDraftRecordingIfNeeded);
|
|
return () => {
|
|
window.removeEventListener('blur', saveDraftRecordingIfNeeded);
|
|
};
|
|
}, [saveDraftRecordingIfNeeded]);
|
|
|
|
useEffect(() => {
|
|
const toast: AnyToast = { toastType: ToastType.VoiceNoteLimit };
|
|
showToast(toast);
|
|
|
|
return () => hideToast(toast);
|
|
}, [showToast, hideToast]);
|
|
|
|
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 = (
|
|
<AxoConfirmDialog.Root
|
|
open
|
|
onOpenChange={onCancel}
|
|
// @ts-expect-error ConfirmationDialog migration: Needs title
|
|
title={null}
|
|
description={i18n('icu:voiceRecordingInterruptedMax')}
|
|
>
|
|
<AxoConfirmDialog.Cancel>{i18n('icu:discard')}</AxoConfirmDialog.Cancel>
|
|
<AxoConfirmDialog.Action variant="primary" onClick={onSend}>
|
|
{i18n('icu:sendAnyway')}
|
|
</AxoConfirmDialog.Action>
|
|
</AxoConfirmDialog.Root>
|
|
);
|
|
} else if (
|
|
errorDialogAudioRecorderType === ErrorDialogAudioRecorderType.ErrorRecording
|
|
) {
|
|
confirmationDialog = (
|
|
<AxoConfirmDialog.Root
|
|
open
|
|
onOpenChange={onCancel}
|
|
// @ts-expect-error ConfirmationDialog migration: Needs title
|
|
title={null}
|
|
description={i18n('icu:voiceNoteError')}
|
|
>
|
|
<AxoConfirmDialog.Cancel>{i18n('icu:ok')}</AxoConfirmDialog.Cancel>
|
|
</AxoConfirmDialog.Root>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<RecordingComposer i18n={i18n} onCancel={onCancel} onSend={onSend}>
|
|
<div className="CompositionRecording__microphone" />
|
|
<div className="CompositionRecording__timer">
|
|
{durationToPlaybackText(duration)}
|
|
</div>
|
|
<div
|
|
className={tw(
|
|
'shrink-0 grow overflow-hidden',
|
|
'flex flex-row-reverse items-center gap-0.5',
|
|
'bg-elevated-background-tertiary',
|
|
'h-full basis-0 rounded-sm'
|
|
)}
|
|
>
|
|
{peaks.toReversed().map(({ value, index }) => {
|
|
return (
|
|
<b
|
|
key={index}
|
|
style={{ height: `${value * 100}%` }}
|
|
className={tw('rounded-sm bg-label-placeholder p-px')}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{confirmationDialog}
|
|
</RecordingComposer>
|
|
);
|
|
}
|