Compare commits

..

2 Commits

Author SHA1 Message Date
Kavi b92d44778a chore: add .gitignore, set tsc noEmit so build doesn't drop .js next to sources
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 02:05:37 -04:00
Kavi fc7420eb8f kuamail thread mutation transport updates 2026-04-23 13:37:31 -04:00
7 changed files with 39 additions and 11 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
node_modules/
dist/
*.tsbuildinfo
.DS_Store
.env
.env.local
.vite/

View File

@ -58,9 +58,12 @@ export type SyncRequest = {
mutations: Mutation[]; mutations: Mutation[];
}; };
export type MutationTargetKind = 'thread' | 'message' | 'draft';
export type Mutation = { export type Mutation = {
id: string; id: string;
op: 'flag' | 'move' | 'delete' | 'send' | 'draft_save' | 'draft_delete'; op: 'flag' | 'move' | 'delete' | 'send' | 'draft_save' | 'draft_delete';
target_kind: MutationTargetKind;
target_id: string; target_id: string;
payload: Record<string, unknown>; payload: Record<string, unknown>;
}; };

View File

@ -25,19 +25,19 @@ export function MessageReader({ threadId }: Props) {
function handleArchive() { function handleArchive() {
if (!accountId) return; if (!accountId) return;
queueMutation(accountId, 'move', threadId, { folder_special_use: 'archive' }); queueMutation(accountId, 'move', 'thread', threadId, { folder_special_use: 'archive' });
selectThread(null); selectThread(null);
} }
function handleDelete() { function handleDelete() {
if (!accountId) return; if (!accountId) return;
queueMutation(accountId, 'move', threadId, { folder_special_use: 'trash' }); queueMutation(accountId, 'move', 'thread', threadId, { folder_special_use: 'trash' });
selectThread(null); selectThread(null);
} }
function handleMarkUnread() { function handleMarkUnread() {
if (!accountId) return; if (!accountId) return;
queueMutation(accountId, 'flag', threadId, { seen: false }); queueMutation(accountId, 'flag', 'thread', threadId, { seen: false });
} }
return ( return (

View File

@ -44,7 +44,7 @@ export function ThreadList({ folderId }: Props) {
const handleStar = useCallback((e: React.MouseEvent, thread: Thread) => { const handleStar = useCallback((e: React.MouseEvent, thread: Thread) => {
e.stopPropagation(); e.stopPropagation();
if (!accountId) return; if (!accountId) return;
queueMutation(accountId, 'flag', thread.id, { flagged: !thread.has_flagged }); queueMutation(accountId, 'flag', 'thread', thread.id, { flagged: !thread.has_flagged });
}, [accountId]); }, [accountId]);
const handleCheck = useCallback((e: React.MouseEvent, threadId: string) => { const handleCheck = useCallback((e: React.MouseEvent, threadId: string) => {
@ -54,17 +54,17 @@ export function ThreadList({ folderId }: Props) {
const handleArchive = useCallback((_: React.MouseEvent, thread: Thread) => { const handleArchive = useCallback((_: React.MouseEvent, thread: Thread) => {
if (!accountId) return; if (!accountId) return;
queueMutation(accountId, 'move', thread.id, { folder_special_use: 'archive' }); queueMutation(accountId, 'move', 'thread', thread.id, { folder_special_use: 'archive' });
}, [accountId]); }, [accountId]);
const handleDelete = useCallback((_: React.MouseEvent, thread: Thread) => { const handleDelete = useCallback((_: React.MouseEvent, thread: Thread) => {
if (!accountId) return; if (!accountId) return;
queueMutation(accountId, 'move', thread.id, { folder_special_use: 'trash' }); queueMutation(accountId, 'move', 'thread', thread.id, { folder_special_use: 'trash' });
}, [accountId]); }, [accountId]);
const handleMarkRead = useCallback((_: React.MouseEvent, thread: Thread) => { const handleMarkRead = useCallback((_: React.MouseEvent, thread: Thread) => {
if (!accountId) return; if (!accountId) return;
queueMutation(accountId, 'flag', thread.id, { seen: thread.unread_count > 0 }); queueMutation(accountId, 'flag', 'thread', thread.id, { seen: true });
}, [accountId]); }, [accountId]);
const handleSnooze = useCallback((_: React.MouseEvent, _thread: Thread) => { const handleSnooze = useCallback((_: React.MouseEvent, _thread: Thread) => {
@ -74,9 +74,9 @@ export function ThreadList({ folderId }: Props) {
const bulkAction = (op: 'archive' | 'trash' | 'read') => { const bulkAction = (op: 'archive' | 'trash' | 'read') => {
if (!accountId) return; if (!accountId) return;
for (const id of checkedThreadIds) { for (const id of checkedThreadIds) {
if (op === 'archive') queueMutation(accountId, 'move', id, { folder_special_use: 'archive' }); if (op === 'archive') queueMutation(accountId, 'move', 'thread', id, { folder_special_use: 'archive' });
else if (op === 'trash') queueMutation(accountId, 'move', id, { folder_special_use: 'trash' }); else if (op === 'trash') queueMutation(accountId, 'move', 'thread', id, { folder_special_use: 'trash' });
else if (op === 'read') queueMutation(accountId, 'flag', id, { seen: true }); else if (op === 'read') queueMutation(accountId, 'flag', 'thread', id, { seen: true });
} }
clearChecks(); clearChecks();
}; };

View File

@ -15,6 +15,7 @@ type PendingMutation = {
id: string; id: string;
account_id: string; account_id: string;
op: string; op: string;
target_kind: 'thread' | 'message' | 'draft';
target_id: string; target_id: string;
payload: unknown; payload: unknown;
created_at: string; created_at: string;
@ -37,6 +38,19 @@ class MailDB extends Dexie {
syncState: 'account_id', syncState: 'account_id',
pendingMutations: 'id, account_id, created_at', 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';
})
);
} }
} }

View File

@ -1,5 +1,5 @@
import { db } from '@/db/dexie'; import { db } from '@/db/dexie';
import { api } from '@/api/client'; import { api, type MutationTargetKind } from '@/api/client';
import { monotonicFactory } from 'ulid'; import { monotonicFactory } from 'ulid';
import { connectSSE, onSSEEvent } from './sse'; import { connectSSE, onSSEEvent } from './sse';
@ -48,6 +48,7 @@ export async function pullChanges(accountId: string) {
mutations: pendingMutations.map((m) => ({ mutations: pendingMutations.map((m) => ({
id: m.id, id: m.id,
op: m.op as never, op: m.op as never,
target_kind: m.target_kind,
target_id: m.target_id, target_id: m.target_id,
payload: m.payload as Record<string, unknown>, payload: m.payload as Record<string, unknown>,
})), })),
@ -91,6 +92,7 @@ export async function pullChanges(accountId: string) {
export async function queueMutation( export async function queueMutation(
accountId: string, accountId: string,
op: string, op: string,
targetKind: MutationTargetKind,
targetId: string, targetId: string,
payload: Record<string, unknown> payload: Record<string, unknown>
) { ) {
@ -98,6 +100,7 @@ export async function queueMutation(
id: ulid(), id: ulid(),
account_id: accountId, account_id: accountId,
op, op,
target_kind: targetKind,
target_id: targetId, target_id: targetId,
payload, payload,
created_at: new Date().toISOString(), created_at: new Date().toISOString(),

View File

@ -11,6 +11,7 @@
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"skipLibCheck": true, "skipLibCheck": true,
"noEmit": true,
"paths": { "@/*": ["./src/*"] } "paths": { "@/*": ["./src/*"] }
}, },
"include": ["src"] "include": ["src"]