1import * as Cause from "effect/Cause";
2import { AsyncResult } from "effect/reactivity";
3
4import {
5 type ComposerThreadTarget,
6 composerDraftHasUserContent,
7 resolveComposerDraftKey,
8 useComposerDraftStore,
9} from "../composerDraftStore";
10import { showThreadUndoNotice } from "../hooks/showThreadUndoNotice";
11import * as ThreadUndo from "../hooks/threadUndo";
12import { releaseDraftAttachments } from "./attachmentUploadQueue";
13
14/**
15 * Discards a draft's unsent content behind the sidebar undo notice. A new-thread
16 * draft (DraftId) loses its whole session; a thread draft only loses its
17 * composer content. Uploads are released once the undo window closes.
18 */
19export function discardComposerDraft(target: ComposerThreadTarget): void {
20 const store = useComposerDraftStore.getState();
21 const key = resolveComposerDraftKey(store, target);
22 const draft = key === null ? undefined : store.draftsByThreadKey[key];
23 if (key === null || !draft) return;
24 const session = store.draftThreadsByThreadKey[key];
25 const logicalProjectKeys = Object.entries(store.logicalProjectDraftThreadKeyByLogicalProjectKey)
26 .filter(([, draftKey]) => draftKey === key)
27 .map(([logicalProjectKey]) => logicalProjectKey);
28 const discardsSession = typeof target === "string";
29
30 const claim = ThreadUndo.begin("discard", key);
31 if (discardsSession) {
32 store.clearDraftThread(target);
33 } else {
34 store.clearComposerContent(target);
35 }
36
37 showThreadUndoNotice({
38 action: "Discarded",
39 claim,
40 failureTitle: "Failed to restore draft",
41 undo: async () => {
42 const current = useComposerDraftStore.getState().draftsByThreadKey[key];
43 if (current && composerDraftHasUserContent(current)) {
44 releaseDraftAttachments([...draft.images, ...draft.files]);
45 return AsyncResult.failure(Cause.fail(new Error("The draft has new content.")));
46 }
47 useComposerDraftStore.setState((state) => {
48 const logicalProjectDraftThreadKeyByLogicalProjectKey = {
49 ...state.logicalProjectDraftThreadKeyByLogicalProjectKey,
50 };
51 for (const logicalProjectKey of logicalProjectKeys) {
52 logicalProjectDraftThreadKeyByLogicalProjectKey[logicalProjectKey] ??= key;
53 }
54 return {
55 draftsByThreadKey: {
56 ...state.draftsByThreadKey,
57 // Removing a draft session revokes its image previews.
58 [key]: discardsSession
59 ? {
60 ...draft,
61 images: draft.images.map((image) =>
62 image.previewUrl.startsWith("blob:")
63 ? { ...image, previewUrl: URL.createObjectURL(image.file) }
64 : image,
65 ),
66 }
67 : draft,
68 },
69 draftThreadsByThreadKey: session
70 ? { ...state.draftThreadsByThreadKey, [key]: session }
71 : state.draftThreadsByThreadKey,
72 logicalProjectDraftThreadKeyByLogicalProjectKey,
73 };
74 });
75 return AsyncResult.success(undefined);
76 },
77 commit: () => releaseDraftAttachments([...draft.images, ...draft.files]),
78 });
79}