diff --git a/src/apps/chat/components/composer/Composer.tsx b/src/apps/chat/components/composer/Composer.tsx index 54c397784..53c707c2d 100644 --- a/src/apps/chat/components/composer/Composer.tsx +++ b/src/apps/chat/components/composer/Composer.tsx @@ -155,9 +155,9 @@ export function Composer(props: { // attachments-overlay: comes from the attachments slice of the conversation overlay const { - attachmentDrafts, - attachAppendClipboardItems, attachAppendDataTransfer, attachAppendEgoContent, attachAppendFile, - attachmentsClear, attachmentsTakeAllFragments, attachmentsTakeTextFragments, + /* items */ attachmentDrafts, + /* append */ attachAppendClipboardItems, attachAppendDataTransfer, attachAppendEgoContent, attachAppendFile, + /* take */ attachmentsRemoveAll, attachmentsTakeAllFragments, attachmentsTakeTextFragments, } = useAttachmentDrafts(conversationOverlayStore, enableLoadURLsInComposer); // attachments derived state @@ -216,9 +216,9 @@ export function Composer(props: { const handleClear = React.useCallback(() => { setComposeText(''); - attachmentsClear(); + attachmentsRemoveAll(); handleReplyToClear(); - }, [attachmentsClear, handleReplyToClear, setComposeText]); + }, [attachmentsRemoveAll, handleReplyToClear, setComposeText]); const handleSendAction = React.useCallback((_chatModeId: ChatModeId, composerText: string): boolean => { if (!isValidConversation(targetConversationId)) return false; @@ -228,8 +228,10 @@ export function Composer(props: { if (composerText) fragments.push(createTextContentFragment(composerText)); const canAttach = chatModeCanAttach(_chatModeId); - if (canAttach) - fragments.push(...attachmentsTakeAllFragments(false)); + if (canAttach) { + const attachmentFragments = await attachmentsTakeAllFragments('global', 'app-chat'); + fragments.push(...attachmentFragments); + } if (!fragments.length) { // addSnackbar({ key: 'chat-composer-empty', message: 'Nothing to send', type: 'info' }); return false; diff --git a/src/apps/chat/components/composer/llmattachments/LLMAttachmentsList.tsx b/src/apps/chat/components/composer/llmattachments/LLMAttachmentsList.tsx index cee2beb7a..d3de6ed7c 100644 --- a/src/apps/chat/components/composer/llmattachments/LLMAttachmentsList.tsx +++ b/src/apps/chat/components/composer/llmattachments/LLMAttachmentsList.tsx @@ -74,7 +74,7 @@ export function LLMAttachmentsList(props: { const handleOverallClearConfirmed = React.useCallback(() => { handleOverallMenuHide(); setConfirmClearAttachmentDrafts(false); - props.attachmentDraftsStoreApi.getState().clearAttachmentsDrafts(); + props.attachmentDraftsStoreApi.getState().removeAllAttachmentDrafts(); }, [handleOverallMenuHide, props.attachmentDraftsStoreApi]); diff --git a/src/common/attachment-drafts/attachment.dblobs.ts b/src/common/attachment-drafts/attachment.dblobs.ts index 94b6d1ceb..1a470ebb7 100644 --- a/src/common/attachment-drafts/attachment.dblobs.ts +++ b/src/common/attachment-drafts/attachment.dblobs.ts @@ -103,6 +103,15 @@ export async function removeAttachmentOwnedDBAsset(fragment: DMessageAttachmentF } } +/** + * Move the DBlob items associated with the given DMessageAttachmentFragment to a new context and scope + */ +export async function transferAttachmentOwnedDBAsset(fragment: DMessageAttachmentFragment, contextId: DBlobDBContextId, scopeId: DBlobDBScopeId) { + if (fragment.part.pt === 'image_ref' && fragment.part.dataRef.reftype === 'dblob') { + await transferDBAssetContextScope(fragment.part.dataRef.dblobAssetId, contextId, scopeId); + } +} + /** * GC Functions for Attachment DBlobs systems: remove leftover drafts */ diff --git a/src/common/attachment-drafts/store-attachment-drafts-slice.tsx b/src/common/attachment-drafts/store-attachment-drafts-slice.tsx index 93a364439..85774ceed 100644 --- a/src/common/attachment-drafts/store-attachment-drafts-slice.tsx +++ b/src/common/attachment-drafts/store-attachment-drafts-slice.tsx @@ -7,7 +7,7 @@ import type { DMessageAttachmentFragment } from '~/common/stores/chat/chat.messa import type { AttachmentDraft, AttachmentDraftId, AttachmentDraftSource } from './attachment.types'; import { attachmentCreate, attachmentDefineConverters, attachmentLoadInputAsync, attachmentPerformConversion } from './attachment.pipeline'; -import { removeDBlobItemFromAttachmentFragment } from './attachment.dblobs'; +import { removeAttachmentOwnedDBAsset, transferAttachmentOwnedDBAsset } from './attachment.dblobs'; /// Attachment Draft Slice: per-conversation attachments store /// @@ -21,15 +21,16 @@ interface AttachmentDraftsState { export interface AttachmentsDraftsStore extends AttachmentDraftsState { createAttachmentDraft: (source: AttachmentDraftSource) => Promise; - clearAttachmentsDrafts: () => void; + removeAllAttachmentDrafts: () => void; removeAttachmentDraft: (attachmentDraftId: AttachmentDraftId) => void; moveAttachmentDraft: (attachmentDraftId: AttachmentDraftId, delta: 1 | -1) => void; setAttachmentDraftConverterIdxAndConvert: (attachmentDraftId: AttachmentDraftId, converterIdx: number | null) => Promise; /** - * Extracts all fragments from the all drafts and clears the store. + * Extracts all fragments from the all drafts and transfers ownership to the caller. + * This store is cleared. */ - takeAllFragments: (removeFragments: boolean) => DMessageAttachmentFragment[]; + takeAllFragments: (newContextId: DBlobDBContextId, newScopeId: DBlobDBScopeId) => Promise; /** * Extracts text fragments from the attachment drafts and optionally removes them from the store. @@ -79,17 +80,17 @@ export const createAttachmentDraftsStoreSlice: StateCreator -1 ? firstEnabledIndex : 0); }, - clearAttachmentsDrafts: () => - _set(_state => { - // NOTE: commented because right now the attachments are not moved to a different scope - // because this function is actually used to clear the attachments when the message is sent - // TODO: do not use clearAttachments when the message is sent, figure out another way0 - // Remove the DBlob items associated with the removed fragments - // for (let draft of _state.attachmentDrafts) { - // for (let fragment of draft.outputFragments) { - // void removeDBlobItemFromAttachmentFragment(fragment); - // } - // } + removeAllAttachmentDrafts: () => + _set(state => { + + // remove the associated DBlob items, as we still ahve + for (const attachmentDraft of state.attachmentDrafts) { + // Remove the DBlob items associated with the removed fragments + for (let removedFragment of attachmentDraft.outputFragments) { + void removeAttachmentOwnedDBAsset(removedFragment); + } + } + return { attachmentDrafts: [], }; @@ -103,7 +104,7 @@ export const createAttachmentDraftsStoreSlice: StateCreator { - const allFragments: DMessageAttachmentFragment[] = []; - _get().attachmentDrafts.forEach(draft => { - allFragments.push(...draft.outputFragments); - }); + takeAllFragments: async (newContextId: DBlobDBContextId, newScopeId: DBlobDBScopeId) => { + // get all the fragments + const transferredFragments: DMessageAttachmentFragment[] = + _get().attachmentDrafts.flatMap(draft => draft.outputFragments); - if (removeFragments) - _set({ attachmentDrafts: [] }); + // transfer ownership (await for transferAttachmentOwnedDBAsset) + for (const transferredFragment of transferredFragments) + await transferAttachmentOwnedDBAsset(transferredFragment, newContextId, newScopeId); - return allFragments; + // clear state + _set({ attachmentDrafts: [] }); + + return transferredFragments; }, takeTextFragments: (attachmentDraftId: AttachmentDraftId | null, removeFragments: boolean): DMessageAttachmentFragment[] => { @@ -174,7 +178,7 @@ export const createAttachmentDraftsStoreSlice: StateCreator { // state - const { _createAttachmentDraft, attachmentDrafts, attachmentsClear, attachmentsTakeAllFragments, attachmentsTakeTextFragments } = useChatAttachmentsStore(attachmentsStoreApi, useShallow(state => ({ + const { _createAttachmentDraft, attachmentDrafts, attachmentsRemoveAll, attachmentsTakeAllFragments, attachmentsTakeTextFragments } = useChatAttachmentsStore(attachmentsStoreApi, useShallow(state => ({ _createAttachmentDraft: state.createAttachmentDraft, attachmentDrafts: state.attachmentDrafts, - attachmentsClear: state.clearAttachmentsDrafts, + attachmentsRemoveAll: state.removeAllAttachmentDrafts, attachmentsTakeAllFragments: state.takeAllFragments, attachmentsTakeTextFragments: state.takeTextFragments, }))); @@ -205,7 +205,7 @@ export const useAttachmentDrafts = (attachmentsStoreApi: AttachmentDraftsStoreAp attachAppendFile, // manage attachments - attachmentsClear, + attachmentsRemoveAll, attachmentsTakeAllFragments, attachmentsTakeTextFragments, };