fix(web): preserve composer attachments across session switches (#1110)

* fix(web): preserve composer attachments across session switches

Persist composer files per session and restore completed uploads without
uploading them again. Clear attachment drafts alongside text after send and
cover restoration, isolation, and adapter reuse with regression tests.

Fixes #465

* fix(web): keep cleared attachment drafts tombstoned

Retain an empty in-memory cache entry until the queued IndexedDB delete completes so a fast remount cannot restore stale files. Add regression coverage for the clear/remount race.

* fix(web): defer attachment restore for inactive sessions

Only restore or clear attachment drafts while the session attachment adapter is available. Preserve saved files when an inactive session mounts without attachment support and add regression coverage.
This commit is contained in:
Ananovo
2026-07-24 10:55:42 +08:00
committed by GitHub
parent 3021c9cba0
commit b2ec09bd0c
9 changed files with 473 additions and 25 deletions
+42
View File
@@ -0,0 +1,42 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
describe('attachmentAdapter restored uploads', () => {
beforeEach(() => {
vi.stubGlobal('indexedDB', undefined)
vi.resetModules()
})
afterEach(() => {
vi.unstubAllGlobals()
})
it('restores an uploaded draft without uploading it again', async () => {
const drafts = await import('./composer-attachment-drafts')
const { createAttachmentAdapter } = await import('./attachmentAdapter')
const file = new File(['image'], 'ready.png', { type: 'image/png' })
drafts.saveDraftAttachments('session-1', [{
id: 'attachment-ready',
file,
path: '/uploads/ready.png',
previewUrl: 'data:image/png;base64,aW1hZ2U=',
}])
const [restored] = await drafts.getDraftAttachments('session-1')
expect(restored).toBeDefined()
const uploadFile = vi.fn()
const adapter = createAttachmentAdapter({ uploadFile } as never, 'session-1')
const emitted = []
const additions = adapter.add({ file: restored! }) as AsyncIterable<unknown>
for await (const attachment of additions) {
emitted.push(attachment)
}
expect(uploadFile).not.toHaveBeenCalled()
expect(emitted).toEqual([expect.objectContaining({
id: 'attachment-ready',
path: '/uploads/ready.png',
previewUrl: 'data:image/png;base64,aW1hZ2U=',
status: { type: 'requires-action', reason: 'composer-send' },
})])
})
})