diff --git a/apps/local-mail/src/up.ts b/apps/local-mail/src/up.ts index 3be2c38474..4b16ddd775 100644 --- a/apps/local-mail/src/up.ts +++ b/apps/local-mail/src/up.ts @@ -158,13 +158,11 @@ export async function runUp(): Promise { return 1; } - // Re-bind the non-null values: TS drops the post-guard narrowing of the - // destructured `runtime`/`session` bindings inside the fetch/loop/SIGINT - // closures below, so capture them here where the narrowing holds. - const rt = runtime; - const sess = session; - const { db } = sess.deps; - const readOnly = rt.config.readOnly; + // `runtime`/`session` are `const`, so their post-guard non-null narrowing + // flows into the closures below (`handleApi` is an arrow, not a hoisted + // declaration, so it inherits the narrowing rather than the widened type). + const { db } = session.deps; + const readOnly = runtime.config.readOnly; // The valid-bearer set. Dev pre-seeds the fixed proxy token; prod fills it // only through the bootstrap exchange. @@ -174,7 +172,7 @@ export async function runUp(): Promise { const devToken = process.env.LOCAL_MAIL_TOKEN; if (!devToken) { lock.release(); - sess.close(); + session.close(); console.error( 'LOCAL_MAIL_DEV=1 requires LOCAL_MAIL_TOKEN so the Vite proxy can authenticate.', ); @@ -196,7 +194,7 @@ export async function runUp(): Promise { return header.slice('Bearer '.length); } - async function handleApi(req: Request, url: URL): Promise { + const handleApi = async (req: Request, url: URL): Promise => { const { pathname } = url; // The one unauthenticated mutation: exchange the bootstrap for a bearer. @@ -226,7 +224,7 @@ export async function runUp(): Promise { } if (pathname === '/api/status' && req.method === 'GET') { - const status = await readMailStatus(rt); + const status = await readMailStatus(runtime); return json({ accountEmail: status.accountEmail, connected: status.connected, @@ -264,7 +262,7 @@ export async function runUp(): Promise { if (pathname === '/api/sync' && req.method === 'POST') { const outcome = await gate(() => - syncMailbox(sess.deps, { forceFull: false }), + syncMailbox(session.deps, { forceFull: false }), ); return json(outcome); } @@ -282,7 +280,7 @@ export async function runUp(): Promise { ); } const { data, error } = await resolveAndModifyMessageLabels({ - deps: sess.deps, + deps: session.deps, ids: body.ids, addLabels: body.addLabels ?? [], removeLabels: body.removeLabels ?? [], @@ -293,7 +291,7 @@ export async function runUp(): Promise { } return json({ error: 'Not found.' }, 404); - } + }; const server = Bun.serve({ hostname: '127.0.0.1', @@ -317,7 +315,7 @@ export async function runUp(): Promise { // The background sync loop, serialized through the same gate as POST /api/sync. (async () => { while (!controller.signal.aborted) { - await gate(() => syncMailbox(sess.deps, { forceFull: false })).catch( + await gate(() => syncMailbox(session.deps, { forceFull: false })).catch( (cause) => console.error(`[sync] loop pass failed: ${cause}`), ); if (controller.signal.aborted) break; @@ -348,7 +346,7 @@ export async function runUp(): Promise { process.on('SIGINT', () => { controller.abort(); server.stop(); - sess.close(); + session.close(); lock.release(); resolve(); }); diff --git a/apps/local-mail/ui/src/lib/components/MessageDetail.svelte b/apps/local-mail/ui/src/lib/components/MessageDetail.svelte index 283fe603d1..8496911bca 100644 --- a/apps/local-mail/ui/src/lib/components/MessageDetail.svelte +++ b/apps/local-mail/ui/src/lib/components/MessageDetail.svelte @@ -4,10 +4,8 @@ import * as DropdownMenu from '@epicenter/ui/dropdown-menu'; import * as Empty from '@epicenter/ui/empty'; import { Loading } from '@epicenter/ui/loading'; - import { Separator } from '@epicenter/ui/separator'; import ArchiveIcon from '@lucide/svelte/icons/archive'; import ArchiveRestoreIcon from '@lucide/svelte/icons/archive-restore'; - import CheckIcon from '@lucide/svelte/icons/check'; import MailOpenIcon from '@lucide/svelte/icons/mail-open'; import MailIcon from '@lucide/svelte/icons/mail'; import MousePointerClickIcon from '@lucide/svelte/icons/mouse-pointer-click'; @@ -18,7 +16,7 @@ import { toast } from 'svelte-sonner'; import { api } from '$lib/api'; import { fullDate, labelDisplayName } from '$lib/format'; - import type { MailLabel, ModifyMessageLabelsOutcome } from '$lib/types'; + import type { MailLabel } from '$lib/types'; let { id, @@ -38,13 +36,11 @@ })); let lastVerb = $state(null); - let lastOutcome = $state(null); const modify = createMutation(() => ({ mutationFn: (input: { addLabels?: string[]; removeLabels?: string[] }) => api.modify({ ids: id ? [id] : [], ...input }), onSuccess: (outcome) => { - lastOutcome = outcome; const failed = outcome.results.filter((r) => r.error).length; const ok = outcome.results.length - failed; if (outcome.aborted) { @@ -92,13 +88,6 @@ if (present) run(`Removed ${name}`, { removeLabels: [labelId] }); else run(`Added ${name}`, { addLabels: [labelId] }); } - - // Reset the inline outcome strip when a different message opens. - $effect(() => { - id; - lastOutcome = null; - lastVerb = null; - }); {#snippet actionButton( @@ -234,33 +223,6 @@ - - {#if lastOutcome} - {@const result = lastOutcome.results[0]} -
- {#if lastOutcome.aborted} - - Aborted: {lastOutcome.aborted.message} - {:else if result?.error} - - {lastVerb} failed: {result.error.message} - {:else if result && !result.folded} - - {lastVerb}. Gmail accepted it; the mirror catches up on the next sync (folded: false). - {:else} - - {lastVerb}. Mirror updated from Gmail's response. - {/if} -
- {/if} -
{#if detail.bodyText} diff --git a/apps/local-mail/ui/src/lib/components/MessageList.svelte b/apps/local-mail/ui/src/lib/components/MessageList.svelte index a27593244e..28ab64457b 100644 --- a/apps/local-mail/ui/src/lib/components/MessageList.svelte +++ b/apps/local-mail/ui/src/lib/components/MessageList.svelte @@ -22,7 +22,7 @@ selectedId, loading, error, - isFiltered, + mirrorEmpty, onSelect, }: { messages: MessageSummary[]; @@ -30,7 +30,7 @@ selectedId: string | null; loading: boolean; error: string | null; - isFiltered: boolean; + mirrorEmpty: boolean; onSelect: (id: string) => void; } = $props(); @@ -63,19 +63,19 @@ {:else if messages.length === 0} - {#if isFiltered} - - {:else} + {#if mirrorEmpty} + {:else} + {/if} - {isFiltered ? 'No messages match' : 'No messages mirrored'} + {mirrorEmpty ? 'No messages mirrored' : 'No messages match'} - {isFiltered - ? 'Try a different label or search term.' - : 'Run local-mail sync --full to populate the mirror.'} + {mirrorEmpty + ? 'Run local-mail sync --full to populate the mirror.' + : 'Try a different label or search term.'} {:else} diff --git a/apps/local-mail/ui/src/routes/+page.svelte b/apps/local-mail/ui/src/routes/+page.svelte index 8ecc6caece..df4e74aa91 100644 --- a/apps/local-mail/ui/src/routes/+page.svelte +++ b/apps/local-mail/ui/src/routes/+page.svelte @@ -49,7 +49,10 @@ const labelList = $derived(labels.data?.labels ?? []); const messageList = $derived(messages.data?.messages ?? []); - const isFiltered = $derived(search.trim().length > 0 || selectedLabel !== null); + // True when the mirror holds no messages at all (nothing synced yet), as + // opposed to this label/search view simply matching none. Drives which empty + // state the list shows: "run sync" vs "no match". + const mirrorEmpty = $derived((status.data?.rows.messages ?? 0) === 0); const syncError = $derived( sync.error?.message ?? sync.data?.failure?.message ?? null, ); @@ -90,7 +93,7 @@ {selectedId} loading={messages.isPending} error={messages.error?.message ?? null} - {isFiltered} + {mirrorEmpty} onSelect={(id) => (selectedId = id)} />