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 && ( {active && (
<CompositionRecording <CompositionRecording
i18n={i18n} i18n={i18n}
conversationId="convo-id"
onCancel={handleCancel} onCancel={handleCancel}
onSend={handleSend} onSend={handleSend}
errorRecording={_ => action('error')()} errorRecording={_ => action('error')()}
addAttachment={action('addAttachment')} addAttachment={action('addAttachment')}
completeRecording={action('completeRecording')} completeRecording={action('completeRecording')}
saveDraftRecordingIfNeeded={action('saveDraftRecordingIfNeeded')}
showToast={action('showToast')} showToast={action('showToast')}
hideToast={action('hideToast')} hideToast={action('hideToast')}
/> />

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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