From 5dc0f05a707e367955b10f6bfbbd8db5d4c5ea51 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Tue, 3 Nov 2026 09:16:32 +0100 Subject: [PATCH] fix: count a draft with a waiting send once when signing out --- components/offline/__tests__/outbox-drafts.test.tsx | 1 - lib/store/__tests__/drafts.test.ts | 6 ++++-- lib/store/drafts/calls.ts | 8 +++----- lib/store/drafts/pending.ts | 6 ++++++ lib/store/outbox/calls.ts | 4 ++-- 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/components/offline/__tests__/outbox-drafts.test.tsx b/components/offline/__tests__/outbox-drafts.test.tsx index 6bb708538..6113196d3 100644 --- a/components/offline/__tests__/outbox-drafts.test.tsx +++ b/components/offline/__tests__/outbox-drafts.test.tsx @@ -22,7 +22,6 @@ const draft = (overrides: Partial = {}): LocalDraftRow => ({ jmap: 'j1', subject: 'Only here', status: 'waiting', - hasFiles: false, createdAt: Date.UTC(2026, 10, 3), ...overrides, }); diff --git a/lib/store/__tests__/drafts.test.ts b/lib/store/__tests__/drafts.test.ts index 31fa738a8..b24c3031c 100644 --- a/lib/store/__tests__/drafts.test.ts +++ b/lib/store/__tests__/drafts.test.ts @@ -580,10 +580,12 @@ describe('sending a draft that\'s on the device', () => { }); await flush(); expect(t.server.drafts()).toEqual([]); - expect(await list()).toHaveLength(1); + expect((await get(id)).list).toHaveLength(1); + // It's that message, in the outbox: not counted a second time as unsent. + expect(await list()).toEqual([]); await t.engine.areaCall('drafts.discard', { jmap: JMAP_ACCOUNT, id }); - expect(await list()).toEqual([]); + expect((await get(id)).notFound).toEqual([id]); }); it('goes up as a draft if the send fails, so nothing depends on the outbox', async () => { diff --git a/lib/store/drafts/calls.ts b/lib/store/drafts/calls.ts index 3378c36ed..a6a678f37 100644 --- a/lib/store/drafts/calls.ts +++ b/lib/store/drafts/calls.ts @@ -5,8 +5,8 @@ import { upsertEmail, type EmailObject } from '../mail/records'; import { savePatch, EMAIL_TYPE } from '../outbox/email-set'; import type { ConflictSummary } from '../outbox/items'; import { applyPatch, destroyConflicts, type RecordState } from '../outbox/patches'; -import { blobPartsOf, type DraftCreate } from './local-email'; -import { allPending, destroyForDraft, pendingForFlush, replaceLocalRow, replacesOf, resolveStandIns, type DraftError, type DraftRow, type DraftStatus } from './pending'; +import type { DraftCreate } from './local-email'; +import { destroyForDraft, listedDrafts, pendingForFlush, replaceLocalRow, replacesOf, resolveStandIns, type DraftError, type DraftRow, type DraftStatus } from './pending'; export interface PendingDraft { localId: string; @@ -33,7 +33,6 @@ export interface LocalDraftSummary { subject: string; status: DraftStatus; error?: DraftError; - hasFiles: boolean; createdAt: number; } @@ -67,12 +66,11 @@ function read(ctx: StoreContext, params: { before: number }): { rows: PendingDra } function list(ctx: StoreContext): LocalDraftSummary[] { - return allPending(ctx.db).map((row) => ({ + return listedDrafts(ctx.db).map((row) => ({ localId: row.local_id, jmap: row.jmap, subject: row.subject ?? '', status: row.status, - hasFiles: blobPartsOf(JSON.parse(row.create_json) as DraftCreate).length > 0, createdAt: Number(row.created_at), ...(row.error ? { error: JSON.parse(row.error) as DraftError } : {}), })); diff --git a/lib/store/drafts/pending.ts b/lib/store/drafts/pending.ts index d890ad3f0..bbdaef0b2 100644 --- a/lib/store/drafts/pending.ts +++ b/lib/store/drafts/pending.ts @@ -204,6 +204,12 @@ export function allPending(db: StorageAdapter): DraftRow[] { return db.all('SELECT * FROM draft_pending ORDER BY seq ASC'); } +/** The drafts that are only here and nothing else speaks for. One a send names is that message, in the outbox. */ +export function listedDrafts(db: StorageAdapter): DraftRow[] { + const held = draftsWithQueuedSend(db); + return allPending(db).filter((row) => !held.has(row.local_id)); +} + export function pendingForFlush(db: StorageAdapter): DraftRow[] { const held = draftsWithQueuedSend(db); return allPending(db).filter((row) => row.status !== 'refused' && !held.has(row.local_id)); diff --git a/lib/store/outbox/calls.ts b/lib/store/outbox/calls.ts index a87c9be15..2496b3e6d 100644 --- a/lib/store/outbox/calls.ts +++ b/lib/store/outbox/calls.ts @@ -156,8 +156,8 @@ function remove(ctx: StoreContext, params: { id: number }): void { // ---- Files attached offline ---- -/** The size the page cuts a file into on its way here. */ -export const HOLD_CHUNK_BYTES = 1024 * 1024; +/** The same size the page cuts a file into on its way here. */ +const HOLD_CHUNK_BYTES = 1024 * 1024; /** A file that comes in pieces: nothing can read it until the last one is in. */ function holdStart(ctx: StoreContext, params: { jmap?: string; name: string; type: string; size: number }): { blobId: string } {