Some checks failed
E2E / Playwright e2e (push) Has been cancelled
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.
225 lines
7.0 KiB
TypeScript
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
|
|
}
|