ultisuite-client/lib/scheduled-mail-context.tsx
R3D347HR4Y c87670e90f
Some checks failed
E2E / Playwright e2e (push) Has been cancelled
feat(api): offline-first mail sync w/ TanStack Query
Move mail, compose, contacts, and accounts off mocks onto REST + WS.
Add client, auth store, IDB-backed query cache, offline queue, and
sync bar; hybrid Zustand for UI-only state. Settings still local until
backend has preferences API.
2026-05-23 00:04:28 +02:00

225 lines
7.0 KiB
TypeScript

"use client"
import {
createContext,
useContext,
useMemo,
useCallback,
type ReactNode,
} from "react"
import type { Email } from "@/lib/email-data"
import type { ScheduleSendPayload } from "@/lib/api/scheduled-mail"
import {
useScheduledStore,
type OutboxEntry,
} from "@/lib/stores/scheduled-store"
import {
useScheduleSend,
useRescheduleSend,
useCancelScheduled,
useSendNow,
} from "@/lib/api/hooks/use-compose-mutations"
import { useActiveAccount } from "@/lib/stores/account-store"
export type { ScheduleSendPayload } from "@/lib/api/scheduled-mail"
type ScheduledMailContextValue = {
scheduledEmails: OutboxEntry[]
snoozedEmails: Email[]
scheduleSend: (payload: ScheduleSendPayload) => Promise<{ id: string }>
removeScheduledLocal: (id: string) => void
requestDeleteScheduled: (id: string) => Promise<void>
requestArchiveScheduled: (id: string) => Promise<void>
requestSnoozeScheduled: (id: string) => Promise<void>
requestToggleReadScheduled: (id: string, read: boolean) => Promise<void>
requestRescheduleScheduled: (id: string, sendAtIso: string) => Promise<void>
requestGetScheduledEditPayload: (id: string) => Promise<ScheduleSendPayload | null>
requestUpdateScheduledSend: (id: string, payload: ScheduleSendPayload) => Promise<void>
requestSendScheduledNow: (id: string) => Promise<void>
requestSnoozeMailboxEmail: (row: Email) => Promise<void>
requestRestoreSnoozedToInbox: (row: Email) => Promise<void>
}
const ScheduledMailContext = createContext<ScheduledMailContextValue | null>(null)
export function ScheduledMailProvider({ children }: { children: ReactNode }) {
const scheduledEmails = useScheduledStore((s) => s.scheduledEmails)
const snoozedEmails = useScheduledStore((s) => s.snoozedEmails)
const account = useActiveAccount()
const scheduleSendMutation = useScheduleSend()
const rescheduleMutation = useRescheduleSend()
const cancelMutation = useCancelScheduled()
const sendNowMutation = useSendNow()
const scheduleSend = useCallback(
async (payload: ScheduleSendPayload): Promise<{ id: string }> => {
const accountId = account?.id ?? ""
const result = await scheduleSendMutation.mutateAsync({
account_id: accountId,
to: payload.to.map((r) => ({ name: r.name, address: r.email })),
subject: payload.subject,
body_html: payload.bodyHtml,
idempotency_key: `sched-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
scheduled_at: payload.sendAtIso,
})
const id = result?.id ?? `local-${Date.now()}`
const entry: OutboxEntry = {
id,
account_id: accountId,
status: "scheduled",
subject: payload.subject,
to: payload.to.map((r) => ({ name: r.name, address: r.email })),
scheduled_at: payload.sendAtIso,
created_at: new Date().toISOString(),
}
useScheduledStore.getState().addScheduledEmail(entry)
return { id }
},
[scheduleSendMutation, account?.id]
)
const removeScheduledLocal = useCallback((id: string) => {
useScheduledStore.getState().removeScheduled(id)
}, [])
const requestDeleteScheduled = useCallback(
async (id: string) => {
await cancelMutation.mutateAsync({ id })
useScheduledStore.getState().removeScheduled(id)
},
[cancelMutation]
)
const requestArchiveScheduled = useCallback(
async (id: string) => {
await cancelMutation.mutateAsync({ id })
useScheduledStore.getState().removeScheduled(id)
},
[cancelMutation]
)
const requestSnoozeScheduled = useCallback(
async (id: string) => {
await cancelMutation.mutateAsync({ id })
useScheduledStore.getState().removeScheduled(id)
},
[cancelMutation]
)
const requestToggleReadScheduled = useCallback(
async (_id: string, _read: boolean) => {},
[]
)
const requestRescheduleScheduled = useCallback(
async (id: string, sendAtIso: string) => {
await rescheduleMutation.mutateAsync({ id, scheduled_at: sendAtIso })
const store = useScheduledStore.getState()
const existing = store.scheduledEmails.find((e) => e.id === id)
if (existing) {
store.addScheduledEmail({ ...existing, scheduled_at: sendAtIso })
}
},
[rescheduleMutation]
)
const requestGetScheduledEditPayload = useCallback(
async (id: string): Promise<ScheduleSendPayload | null> => {
const entry = useScheduledStore.getState().scheduledEmails.find((e) => e.id === id)
if (!entry) return null
return {
sendAtIso: entry.scheduled_at ?? new Date().toISOString(),
to: entry.to.map((r) => ({ name: r.name, email: r.address })),
subject: entry.subject,
previewText: "",
bodyHtml: "",
}
},
[]
)
const requestUpdateScheduledSend = useCallback(
async (id: string, payload: ScheduleSendPayload) => {
await rescheduleMutation.mutateAsync({ id, scheduled_at: payload.sendAtIso })
const entry: OutboxEntry = {
id,
account_id: account?.id ?? "",
status: "scheduled",
subject: payload.subject,
to: payload.to.map((r) => ({ name: r.name, address: r.email })),
scheduled_at: payload.sendAtIso,
created_at: new Date().toISOString(),
}
useScheduledStore.getState().addScheduledEmail(entry)
},
[rescheduleMutation, account?.id]
)
const requestSendScheduledNow = useCallback(
async (id: string) => {
await sendNowMutation.mutateAsync({ id })
useScheduledStore.getState().removeScheduled(id)
},
[sendNowMutation]
)
const requestSnoozeMailboxEmail = useCallback(async (row: Email) => {
useScheduledStore.getState().snoozeMailboxEmail(row)
}, [])
const requestRestoreSnoozedToInbox = useCallback(async (row: Email) => {
useScheduledStore.getState().restoreSnoozedToInbox(row)
}, [])
const value = useMemo<ScheduledMailContextValue>(
() => ({
scheduledEmails,
snoozedEmails,
scheduleSend,
removeScheduledLocal,
requestDeleteScheduled,
requestArchiveScheduled,
requestSnoozeScheduled,
requestToggleReadScheduled,
requestRescheduleScheduled,
requestGetScheduledEditPayload,
requestUpdateScheduledSend,
requestSendScheduledNow,
requestSnoozeMailboxEmail,
requestRestoreSnoozedToInbox,
}),
[
scheduledEmails,
snoozedEmails,
scheduleSend,
removeScheduledLocal,
requestDeleteScheduled,
requestArchiveScheduled,
requestSnoozeScheduled,
requestToggleReadScheduled,
requestRescheduleScheduled,
requestGetScheduledEditPayload,
requestUpdateScheduledSend,
requestSendScheduledNow,
requestSnoozeMailboxEmail,
requestRestoreSnoozedToInbox,
]
)
return (
<ScheduledMailContext.Provider value={value}>
{children}
</ScheduledMailContext.Provider>
)
}
export function useScheduledMail() {
const ctx = useContext(ScheduledMailContext)
if (!ctx) {
throw new Error("useScheduledMail must be used within ScheduledMailProvider")
}
return ctx
}