From fc7420eb8fef28a019dbee6b203197dc83de2531 Mon Sep 17 00:00:00 2001 From: Kavi Date: Thu, 23 Apr 2026 13:37:31 -0400 Subject: [PATCH] kuamail thread mutation transport updates --- src/api/client.ts | 3 +++ src/components/MessageReader.tsx | 6 +++--- src/components/ThreadList.tsx | 14 +++++++------- src/db/dexie.ts | 14 ++++++++++++++ src/sync/engine.ts | 5 ++++- 5 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/api/client.ts b/src/api/client.ts index bffe10d..9e4edbb 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -58,9 +58,12 @@ export type SyncRequest = { mutations: Mutation[]; }; +export type MutationTargetKind = 'thread' | 'message' | 'draft'; + export type Mutation = { id: string; op: 'flag' | 'move' | 'delete' | 'send' | 'draft_save' | 'draft_delete'; + target_kind: MutationTargetKind; target_id: string; payload: Record; }; diff --git a/src/components/MessageReader.tsx b/src/components/MessageReader.tsx index 3a33d59..f22bc24 100644 --- a/src/components/MessageReader.tsx +++ b/src/components/MessageReader.tsx @@ -25,19 +25,19 @@ export function MessageReader({ threadId }: Props) { function handleArchive() { if (!accountId) return; - queueMutation(accountId, 'move', threadId, { folder_special_use: 'archive' }); + queueMutation(accountId, 'move', 'thread', threadId, { folder_special_use: 'archive' }); selectThread(null); } function handleDelete() { if (!accountId) return; - queueMutation(accountId, 'move', threadId, { folder_special_use: 'trash' }); + queueMutation(accountId, 'move', 'thread', threadId, { folder_special_use: 'trash' }); selectThread(null); } function handleMarkUnread() { if (!accountId) return; - queueMutation(accountId, 'flag', threadId, { seen: false }); + queueMutation(accountId, 'flag', 'thread', threadId, { seen: false }); } return ( diff --git a/src/components/ThreadList.tsx b/src/components/ThreadList.tsx index f8b0969..cecd905 100644 --- a/src/components/ThreadList.tsx +++ b/src/components/ThreadList.tsx @@ -44,7 +44,7 @@ export function ThreadList({ folderId }: Props) { const handleStar = useCallback((e: React.MouseEvent, thread: Thread) => { e.stopPropagation(); if (!accountId) return; - queueMutation(accountId, 'flag', thread.id, { flagged: !thread.has_flagged }); + queueMutation(accountId, 'flag', 'thread', thread.id, { flagged: !thread.has_flagged }); }, [accountId]); const handleCheck = useCallback((e: React.MouseEvent, threadId: string) => { @@ -54,17 +54,17 @@ export function ThreadList({ folderId }: Props) { const handleArchive = useCallback((_: React.MouseEvent, thread: Thread) => { if (!accountId) return; - queueMutation(accountId, 'move', thread.id, { folder_special_use: 'archive' }); + queueMutation(accountId, 'move', 'thread', thread.id, { folder_special_use: 'archive' }); }, [accountId]); const handleDelete = useCallback((_: React.MouseEvent, thread: Thread) => { if (!accountId) return; - queueMutation(accountId, 'move', thread.id, { folder_special_use: 'trash' }); + queueMutation(accountId, 'move', 'thread', thread.id, { folder_special_use: 'trash' }); }, [accountId]); const handleMarkRead = useCallback((_: React.MouseEvent, thread: Thread) => { if (!accountId) return; - queueMutation(accountId, 'flag', thread.id, { seen: thread.unread_count > 0 }); + queueMutation(accountId, 'flag', 'thread', thread.id, { seen: true }); }, [accountId]); const handleSnooze = useCallback((_: React.MouseEvent, _thread: Thread) => { @@ -74,9 +74,9 @@ export function ThreadList({ folderId }: Props) { const bulkAction = (op: 'archive' | 'trash' | 'read') => { if (!accountId) return; for (const id of checkedThreadIds) { - if (op === 'archive') queueMutation(accountId, 'move', id, { folder_special_use: 'archive' }); - else if (op === 'trash') queueMutation(accountId, 'move', id, { folder_special_use: 'trash' }); - else if (op === 'read') queueMutation(accountId, 'flag', id, { seen: true }); + if (op === 'archive') queueMutation(accountId, 'move', 'thread', id, { folder_special_use: 'archive' }); + else if (op === 'trash') queueMutation(accountId, 'move', 'thread', id, { folder_special_use: 'trash' }); + else if (op === 'read') queueMutation(accountId, 'flag', 'thread', id, { seen: true }); } clearChecks(); }; diff --git a/src/db/dexie.ts b/src/db/dexie.ts index ffb2215..705b9ee 100644 --- a/src/db/dexie.ts +++ b/src/db/dexie.ts @@ -15,6 +15,7 @@ type PendingMutation = { id: string; account_id: string; op: string; + target_kind: 'thread' | 'message' | 'draft'; target_id: string; payload: unknown; created_at: string; @@ -37,6 +38,19 @@ class MailDB extends Dexie { syncState: 'account_id', pendingMutations: 'id, account_id, created_at', }); + this.version(2) + .stores({ + threads: 'id, folder_id, account_id, last_message_at', + messages: 'id, thread_id, account_id, date_sent, folder_id', + folders: 'id, account_id, imap_path', + syncState: 'account_id', + pendingMutations: 'id, account_id, target_kind, created_at', + }) + .upgrade((tx) => + tx.table('pendingMutations').toCollection().modify((mutation) => { + if (!mutation.target_kind) mutation.target_kind = 'thread'; + }) + ); } } diff --git a/src/sync/engine.ts b/src/sync/engine.ts index 7df6691..1988ba3 100644 --- a/src/sync/engine.ts +++ b/src/sync/engine.ts @@ -1,5 +1,5 @@ import { db } from '@/db/dexie'; -import { api } from '@/api/client'; +import { api, type MutationTargetKind } from '@/api/client'; import { monotonicFactory } from 'ulid'; import { connectSSE, onSSEEvent } from './sse'; @@ -48,6 +48,7 @@ export async function pullChanges(accountId: string) { mutations: pendingMutations.map((m) => ({ id: m.id, op: m.op as never, + target_kind: m.target_kind, target_id: m.target_id, payload: m.payload as Record, })), @@ -91,6 +92,7 @@ export async function pullChanges(accountId: string) { export async function queueMutation( accountId: string, op: string, + targetKind: MutationTargetKind, targetId: string, payload: Record ) { @@ -98,6 +100,7 @@ export async function queueMutation( id: ulid(), account_id: accountId, op, + target_kind: targetKind, target_id: targetId, payload, created_at: new Date().toISOString(),