From dfbffef7e3991f8628a2bbc6bb162157b57b6189 Mon Sep 17 00:00:00 2001 From: kenny lopez Date: Tue, 18 Aug 2026 16:51:18 +0100 Subject: [PATCH] Extract message send recovery helper Signed-off-by: kenny lopez --- .../messages/ui/useMentionSendFlow.helpers.ts | 105 +++++++++++++++++- .../messages/ui/useMentionSendFlow.ts | 80 +++---------- 2 files changed, 119 insertions(+), 66 deletions(-) diff --git a/desktop/src/features/messages/ui/useMentionSendFlow.helpers.ts b/desktop/src/features/messages/ui/useMentionSendFlow.helpers.ts index 4bd87c15d6..54eab57972 100644 --- a/desktop/src/features/messages/ui/useMentionSendFlow.helpers.ts +++ b/desktop/src/features/messages/ui/useMentionSendFlow.helpers.ts @@ -3,9 +3,15 @@ import { type ImetaMedia, mergeOutgoingTags, } from "@/features/messages/lib/imetaMediaMarkdown"; -import type { QueuedMediaAttachment } from "@/features/messages/lib/backgroundMediaUploadStore"; +import { + type QueuedMediaAttachment, + saveQueuedAttachmentsForDraft, +} from "@/features/messages/lib/backgroundMediaUploadStore"; import type { PreparedBackgroundLinkPreviews } from "@/features/messages/lib/linkPreviewPreparationStore"; -import type { DraftMentionRef } from "@/features/messages/lib/useDrafts"; +import type { + DraftMentionRef, + UseDraftsResult, +} from "@/features/messages/lib/useDrafts"; import { normalizePubkey } from "@/shared/lib/pubkey"; import { MENTION_REFERENCE_TAG } from "@/shared/lib/resolveMentionNames"; @@ -96,3 +102,98 @@ export function isManagedAgentRunning(agent: ManagedAgent) { export function isProviderBackedAgent(agent: ManagedAgent) { return agent.backend.type === "provider"; } + +type RestoreComposerAfterFailureOptions = { + content?: string; + withoutMedia?: boolean; +}; + +export function createMentionSendFailureRecovery({ + contentRef, + draft, + drafts, + getCurrentChannelId, + hasUnsavedMedia, + isMounted, + isSendCancelled, + restoreDraftMentionRefs, + restoreQueuedAttachments, + setContent, + setPendingImeta, + setRichTextContent, + setSpoileredAttachmentUrls, +}: { + contentRef: { current: string }; + draft: PendingNonMemberMentionSend; + drafts: Pick; + getCurrentChannelId: () => string | null; + hasUnsavedMedia: () => boolean; + isMounted: () => boolean; + isSendCancelled: () => boolean; + restoreDraftMentionRefs: (refs: DraftMentionRef[]) => void; + restoreQueuedAttachments: (attachments: QueuedMediaAttachment[]) => void; + setContent: (content: string) => void; + setPendingImeta: (pendingImeta: ImetaMedia[]) => void; + setRichTextContent: (content: string) => void; + setSpoileredAttachmentUrls?: (urls: Set) => void; +}) { + const persistCanceledDraft = ({ + content = draft.savedContent, + withoutMedia = false, + }: RestoreComposerAfterFailureOptions = {}) => { + if (isSendCancelled() || !draft.recoveryDraftKey) return; + const existing = drafts.loadDraft(draft.recoveryDraftKey); + if ( + existing && + (existing.content !== draft.savedContent || + existing.channelId !== + (draft.capturedChannelId ?? draft.recoveryDraftKey) || + JSON.stringify(existing.pendingImeta) !== + JSON.stringify(draft.savedImeta) || + JSON.stringify(existing.spoileredAttachmentUrls) !== + JSON.stringify([...draft.savedSpoileredAttachmentUrls])) + ) { + return; + } + drafts.persistDraft( + draft.recoveryDraftKey, + content, + draft.capturedChannelId ?? draft.recoveryDraftKey, + withoutMedia ? [] : draft.savedImeta, + withoutMedia ? [] : [...draft.savedSpoileredAttachmentUrls], + draft.savedMentionRefs, + ); + }; + + return ({ + content = draft.savedContent, + withoutMedia = false, + }: RestoreComposerAfterFailureOptions = {}) => { + if (isSendCancelled()) return; + persistCanceledDraft({ content, withoutMedia }); + const currentChannelId = getCurrentChannelId(); + const canRestoreCurrentComposer = + isMounted() && + (draft.capturedChannelId === currentChannelId || + currentChannelId === null) && + contentRef.current.trim().length === 0 && + !hasUnsavedMedia(); + if (!canRestoreCurrentComposer && draft.recoveryDraftKey) { + saveQueuedAttachmentsForDraft( + draft.recoveryDraftKey, + withoutMedia ? [] : draft.queuedAttachments, + ); + } + if (!canRestoreCurrentComposer) return; + + setContent(content); + contentRef.current = content; + setRichTextContent(content); + setPendingImeta(withoutMedia ? [] : draft.savedImeta); + restoreQueuedAttachments(withoutMedia ? [] : draft.queuedAttachments); + restoreDraftMentionRefs(draft.savedMentionRefs); + setSpoileredAttachmentUrls?.( + new Set(withoutMedia ? [] : draft.savedSpoileredAttachmentUrls), + ); + }; +} diff --git a/desktop/src/features/messages/ui/useMentionSendFlow.ts b/desktop/src/features/messages/ui/useMentionSendFlow.ts index 68b4ae55a3..d4850f6889 100644 --- a/desktop/src/features/messages/ui/useMentionSendFlow.ts +++ b/desktop/src/features/messages/ui/useMentionSendFlow.ts @@ -36,6 +36,7 @@ import type { AcpRuntime, ChannelType, ManagedAgent } from "@/shared/api/types"; import { normalizePubkey, truncatePubkey } from "@/shared/lib/pubkey"; import { buildCustomEmojiTags } from "@/shared/lib/customEmojiTags"; import { + createMentionSendFailureRecovery, getErrorMessage, isManagedAgentRunning, isProviderBackedAgent, @@ -495,70 +496,21 @@ export function useMentionSendFlow({ let mediaMessageSent = false; let linkMessageSent = false; let remainingTextContent = draft.savedContent; - const persistCanceledDraft = ({ - content = draft.savedContent, - withoutMedia = false, - }: { - content?: string; - withoutMedia?: boolean; - } = {}) => { - if (isSendCancelled() || !draft.recoveryDraftKey) return; - const existing = drafts.loadDraft(draft.recoveryDraftKey); - if ( - existing && - (existing.content !== draft.savedContent || - existing.channelId !== - (draft.capturedChannelId ?? draft.recoveryDraftKey) || - JSON.stringify(existing.pendingImeta) !== - JSON.stringify(draft.savedImeta) || - JSON.stringify(existing.spoileredAttachmentUrls) !== - JSON.stringify([...draft.savedSpoileredAttachmentUrls])) - ) { - return; - } - drafts.persistDraft( - draft.recoveryDraftKey, - content, - draft.capturedChannelId ?? draft.recoveryDraftKey, - withoutMedia ? [] : draft.savedImeta, - withoutMedia ? [] : [...draft.savedSpoileredAttachmentUrls], - draft.savedMentionRefs, - ); - }; - const restoreComposerAfterFailure = ({ - content = draft.savedContent, - withoutMedia = false, - }: { - content?: string; - withoutMedia?: boolean; - } = {}) => { - if (isSendCancelled()) return; - persistCanceledDraft({ content, withoutMedia }); - const canRestoreCurrentComposer = - isMountedRef.current && - (draft.capturedChannelId === channelIdRef.current || - channelIdRef.current === null) && - contentRef.current.trim().length === 0 && - !hasUnsavedMedia(); - if (!canRestoreCurrentComposer && draft.recoveryDraftKey) { - saveQueuedAttachmentsForDraft( - draft.recoveryDraftKey, - withoutMedia ? [] : draft.queuedAttachments, - ); - } - if (!canRestoreCurrentComposer) { - return; - } - setContent(content); - contentRef.current = content; - richText.setContent(content); - setPendingImeta(withoutMedia ? [] : draft.savedImeta); - restoreQueuedAttachments(withoutMedia ? [] : draft.queuedAttachments); - mentions.restoreDraftMentionRefs(draft.savedMentionRefs); - setSpoileredAttachmentUrls?.( - new Set(withoutMedia ? [] : draft.savedSpoileredAttachmentUrls), - ); - }; + const restoreComposerAfterFailure = createMentionSendFailureRecovery({ + contentRef, + draft, + drafts, + getCurrentChannelId: () => channelIdRef.current, + hasUnsavedMedia, + isMounted: () => isMountedRef.current, + isSendCancelled, + restoreDraftMentionRefs: mentions.restoreDraftMentionRefs, + restoreQueuedAttachments, + setContent, + setPendingImeta, + setRichTextContent: richText.setContent, + setSpoileredAttachmentUrls, + }); const finishSend = async ( uploaded: ImetaMedia[], signal?: AbortSignal,