Fix voice note drafts when switching chats

This commit is contained in:
ayumi-signal 2024-06-18 12:53:14 -07:00 committed by GitHub
parent a25c2036b5
commit 11b2563a3d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 39 additions and 42 deletions

View file

@ -45,12 +45,12 @@ export function Default(): JSX.Element {
{active && (
<CompositionRecording
i18n={i18n}
conversationId="convo-id"
onCancel={handleCancel}
onSend={handleSend}
errorRecording={_ => action('error')()}
addAttachment={action('addAttachment')}
completeRecording={action('completeRecording')}
saveDraftRecordingIfNeeded={action('saveDraftRecordingIfNeeded')}
showToast={action('showToast')}
hideToast={action('hideToast')}
/>

View file

@ -2,9 +2,8 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { noop } from 'lodash';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { useEscapeHandling } from '../hooks/useEscapeHandling';
import { usePrevious } from '../hooks/usePrevious';
import type { HideToastAction, ShowToastAction } from '../state/ducks/toast';
import type { InMemoryAttachmentDraftType } from '../types/Attachment';
import { ErrorDialogAudioRecorderType } from '../types/AudioRecorder';
@ -18,7 +17,6 @@ import { RecordingComposer } from './RecordingComposer';
export type Props = {
i18n: LocalizerType;
conversationId: string;
onCancel: () => void;
onSend: () => void;
errorRecording: (e: ErrorDialogAudioRecorderType) => unknown;
@ -31,47 +29,30 @@ export type Props = {
conversationId: string,
onRecordingComplete: (rec: InMemoryAttachmentDraftType) => unknown
) => unknown;
saveDraftRecordingIfNeeded: () => void;
showToast: ShowToastAction;
hideToast: HideToastAction;
};
export function CompositionRecording({
i18n,
conversationId,
onCancel,
onSend,
errorRecording,
errorDialogAudioRecorderType,
addAttachment,
completeRecording,
saveDraftRecordingIfNeeded,
showToast,
hideToast,
}: Props): JSX.Element {
useEscapeHandling(onCancel);
// 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);
window.addEventListener('blur', saveDraftRecordingIfNeeded);
return () => {
window.removeEventListener('blur', handleRecordingInterruption);
window.removeEventListener('blur', saveDraftRecordingIfNeeded);
};
}, [handleRecordingInterruption]);
// switched conversations
const previousConversationId = usePrevious(conversationId, conversationId);
useEffect(() => {
if (previousConversationId !== conversationId) {
handleRecordingInterruption();
}
});
}, [saveDraftRecordingIfNeeded]);
useEffect(() => {
const toast: AnyToast = { toastType: ToastType.VoiceNoteLimit };

View file

@ -65,6 +65,10 @@ type AudioPlayerActionType = ReadonlyDeep<
| StartRecordingAction
>;
export function getIsRecording(audioRecorder: AudioRecorderStateType): boolean {
return audioRecorder.recordingState === RecordingState.Recording;
}
// Action Creators
export const actions = {

View file

@ -96,6 +96,7 @@ import {
} from '../../types/CallDisposition';
import type { CallHistoryAdd } from './callHistory';
import { addCallHistory } from './callHistory';
import { saveDraftRecordingIfNeeded } from './composer';
// State
@ -926,6 +927,8 @@ function acceptCall(
return;
}
saveDraftRecordingIfNeeded()(dispatch, getState, undefined);
switch (call.callMode) {
case CallMode.Direct:
await calling.acceptDirectCall(conversationId, asVideoCall);

View file

@ -33,8 +33,7 @@ import {
} from './linkPreviews';
import { LinkPreviewSourceType } from '../../types/LinkPreview';
import type { AciString } from '../../types/ServiceId';
import { completeRecording } from './audioRecorder';
import { RecordingState } from '../../types/AudioRecorder';
import { completeRecording, getIsRecording } from './audioRecorder';
import { SHOW_TOAST } from './toast';
import type { AnyToast } from '../../types/Toast';
import { ToastType } from '../../types/Toast';
@ -243,6 +242,7 @@ export const actions = {
removeAttachment,
replaceAttachments,
resetComposer,
saveDraftRecordingIfNeeded,
scrollToQuotedMessage,
sendEditedMessage,
sendMultiMediaMessage,
@ -363,23 +363,33 @@ function scrollToQuotedMessage({
};
}
export function handleLeaveConversation(
conversationId: string
): ThunkAction<void, RootStateType, unknown, never> {
export function saveDraftRecordingIfNeeded(): ThunkAction<
void,
RootStateType,
unknown,
never
> {
return (dispatch, getState) => {
const { audioRecorder } = getState();
const { conversations, audioRecorder } = getState();
const { selectedConversationId: conversationId } = conversations;
if (audioRecorder.recordingState !== RecordingState.Recording) {
if (!getIsRecording(audioRecorder) || !conversationId) {
return;
}
// save draft of voice note
dispatch(
completeRecording(conversationId, attachment => {
dispatch(
addPendingAttachment(conversationId, { ...attachment, pending: true })
);
dispatch(addAttachment(conversationId, attachment));
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error('saveDraftRecordingIfNeeded: No conversation found');
}
drop(conversation.updateLastMessage());
})
);
};
@ -1014,11 +1024,9 @@ function processAttachments({
return;
}
const state = getState();
const isRecording =
state.audioRecorder.recordingState === RecordingState.Recording;
const { audioRecorder } = getState();
if (hasLinkPreviewLoaded() || isRecording) {
if (hasLinkPreviewLoaded() || getIsRecording(audioRecorder)) {
return;
}

View file

@ -168,7 +168,7 @@ import {
setComposerFocus,
setQuoteByMessageId,
resetComposer,
handleLeaveConversation,
saveDraftRecordingIfNeeded,
} from './composer';
import { ReceiptType } from '../../types/Receipt';
import { Sound, SoundType } from '../../util/Sound';
@ -4254,7 +4254,7 @@ function showConversation({
// notify composer in case we need to stop recording a voice note
if (conversations.selectedConversationId) {
dispatch(handleLeaveConversation(conversations.selectedConversationId));
saveDraftRecordingIfNeeded()(dispatch, getState, undefined);
dispatch(
onConversationClosed(
conversations.selectedConversationId,

View file

@ -23,7 +23,8 @@ export const SmartCompositionRecording = memo(
const { errorRecording, cancelRecording, completeRecording } =
useAudioRecorderActions();
const { sendMultiMediaMessage, addAttachment } = useComposerActions();
const { sendMultiMediaMessage, addAttachment, saveDraftRecordingIfNeeded } =
useComposerActions();
const { hideToast, showToast } = useToastActions();
const handleCancel = useCallback(() => {
@ -53,12 +54,12 @@ export const SmartCompositionRecording = memo(
return (
<CompositionRecording
i18n={i18n}
conversationId={selectedConversationId}
onCancel={handleCancel}
onSend={handleSend}
errorRecording={errorRecording}
addAttachment={addAttachment}
completeRecording={completeRecording}
saveDraftRecordingIfNeeded={saveDraftRecordingIfNeeded}
showToast={showToast}
hideToast={hideToast}
/>