diff --git a/ts/components/CompositionRecording.stories.tsx b/ts/components/CompositionRecording.stories.tsx index 52b7a8f1c249..868c02979b46 100644 --- a/ts/components/CompositionRecording.stories.tsx +++ b/ts/components/CompositionRecording.stories.tsx @@ -45,12 +45,12 @@ export function Default(): JSX.Element { {active && ( action('error')()} addAttachment={action('addAttachment')} completeRecording={action('completeRecording')} + saveDraftRecordingIfNeeded={action('saveDraftRecordingIfNeeded')} showToast={action('showToast')} hideToast={action('hideToast')} /> diff --git a/ts/components/CompositionRecording.tsx b/ts/components/CompositionRecording.tsx index f4a6e97d5896..bb269196d8c1 100644 --- a/ts/components/CompositionRecording.tsx +++ b/ts/components/CompositionRecording.tsx @@ -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 }; diff --git a/ts/state/ducks/audioRecorder.ts b/ts/state/ducks/audioRecorder.ts index ef1200c2d602..ec5fb709b10f 100644 --- a/ts/state/ducks/audioRecorder.ts +++ b/ts/state/ducks/audioRecorder.ts @@ -65,6 +65,10 @@ type AudioPlayerActionType = ReadonlyDeep< | StartRecordingAction >; +export function getIsRecording(audioRecorder: AudioRecorderStateType): boolean { + return audioRecorder.recordingState === RecordingState.Recording; +} + // Action Creators export const actions = { diff --git a/ts/state/ducks/calling.ts b/ts/state/ducks/calling.ts index 162144878896..f58b9edd1546 100644 --- a/ts/state/ducks/calling.ts +++ b/ts/state/ducks/calling.ts @@ -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); diff --git a/ts/state/ducks/composer.ts b/ts/state/ducks/composer.ts index bd967abfdb91..bbe766ccf125 100644 --- a/ts/state/ducks/composer.ts +++ b/ts/state/ducks/composer.ts @@ -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 { +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; } diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 59602a95c13b..7d46407ee1dc 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -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, diff --git a/ts/state/smart/CompositionRecording.tsx b/ts/state/smart/CompositionRecording.tsx index 2c52551e3130..4a964a3043dd 100644 --- a/ts/state/smart/CompositionRecording.tsx +++ b/ts/state/smart/CompositionRecording.tsx @@ -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 (