import { appApiPath } from "@agent-native/core/client/api-path"; import { callAction } from "@agent-native/core/client/hooks"; import type { ComposeAttachment } from "@shared/types"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { suppressThread, unsuppressThread, mapInfiniteEmails, flattenInfiniteEmails, type InfiniteEmails, } from "./use-emails"; export interface ScheduledJob { id: string; type: "snooze" | "send_later"; ownerEmail?: string | null; emailId: string | null; threadId?: string | null; accountEmail?: string | null; payload: string; // JSON string runAt: number; // epoch ms status: "pending" | "processing" | "done" | "cancelled"; createdAt: number; } function assertActionSuccess(result: T): T { if ( typeof result === "string" && (/^Error:/i.test(result) || /\bwas not found\b/i.test(result)) ) { throw new Error(result); } return result; } export function useScheduledJobs() { return useQuery({ queryKey: ["scheduled-jobs"], queryFn: () => callAction("list-scheduled-jobs", {}, { method: "GET" }), refetchInterval: 30_000, // Refresh every 30s }); } export function useCreateScheduledJob() { const qc = useQueryClient(); return useMutation({ mutationFn: (data: { type: "snooze" | "send_later"; emailId?: string; payload?: Record; runAt: number; }) => callAction("create-scheduled-job", data) as Promise, onSuccess: () => qc.invalidateQueries({ queryKey: ["scheduled-jobs"] }), }); } export function useUpdateScheduledJob() { const qc = useQueryClient(); return useMutation({ mutationFn: (data: { id: string; runAt: number }) => callAction("update-scheduled-job", data) as Promise, onSuccess: () => qc.invalidateQueries({ queryKey: ["scheduled-jobs"] }), }); } export function useSnoozeEmail() { const qc = useQueryClient(); return useMutation({ mutationFn: async (data: { emailId: string; runAt: number; accountEmail?: string; }) => { const res = await fetch( appApiPath(`/api/emails/${data.emailId}/snooze`), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ runAt: data.runAt, accountEmail: data.accountEmail, }), }, ); if (!res.ok) { const body = await res.json().catch(() => null); throw new Error(body?.error || "Failed to snooze email"); } return res.json() as Promise; }, onMutate: async (data) => { await qc.cancelQueries({ queryKey: ["emails"] }); const previous = qc.getQueriesData({ queryKey: ["emails"], }); const target = previous .flatMap(([, d]) => flattenInfiniteEmails(d)) .find((e) => e.id === data.emailId); const threadId = target?.threadId || data.emailId; suppressThread(threadId, "snooze"); qc.setQueriesData({ queryKey: ["emails"] }, (old) => mapInfiniteEmails(old, (emails) => emails.filter((e) => (e.threadId || e.id) !== threadId), ), ); return { previous, threadId }; }, onError: (_err, _vars, context) => { if (context?.threadId) unsuppressThread(context.threadId); context?.previous?.forEach(([key, data]) => qc.setQueryData(key, data)); }, onSettled: () => { qc.invalidateQueries({ queryKey: ["scheduled-jobs"] }); // Delay email/label refetch — Gmail eventual consistency setTimeout(() => { qc.invalidateQueries({ queryKey: ["emails"] }); qc.invalidateQueries({ queryKey: ["labels"] }); }, 3000); }, }); } export function useScheduleEmail() { const qc = useQueryClient(); return useMutation({ mutationFn: async (data: { to: string; cc?: string; bcc?: string; subject: string; body: string; runAt: number; accountEmail?: string; from?: string; replyToId?: string; threadId?: string; attachments?: ComposeAttachment[]; }) => { const res = await fetch(appApiPath("/api/emails/schedule"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (!res.ok) { const body = await res.json().catch(() => null); throw new Error(body?.error || "Failed to schedule email"); } return res.json() as Promise; }, onSuccess: () => { qc.invalidateQueries({ queryKey: ["scheduled-jobs"] }); qc.invalidateQueries({ queryKey: ["emails"] }); }, }); } export function useDeleteScheduledJob() { const qc = useQueryClient(); return useMutation({ mutationFn: (id: string) => callAction("cancel-scheduled-email", { id }).then(assertActionSuccess), onMutate: async (id: string) => { await qc.cancelQueries({ queryKey: ["emails"] }); const previous = qc.getQueriesData({ queryKey: ["emails"], }); qc.setQueriesData({ queryKey: ["emails"] }, (old) => mapInfiniteEmails(old, (emails) => emails.filter((email) => email.id !== `scheduled-${id}`), ), ); return { previous }; }, onError: (_err, _id, context) => { context?.previous.forEach(([key, data]) => qc.setQueryData(key, data)); }, onSettled: () => { qc.invalidateQueries({ queryKey: ["scheduled-jobs"] }); qc.invalidateQueries({ queryKey: ["emails"] }); }, }); } export function useSendScheduledJobNow() { const qc = useQueryClient(); return useMutation({ mutationFn: (id: string) => callAction("send-scheduled-email-now", { id }), onMutate: async (id: string) => { await qc.cancelQueries({ queryKey: ["emails"] }); const previous = qc.getQueriesData({ queryKey: ["emails"], }); qc.setQueriesData({ queryKey: ["emails"] }, (old) => mapInfiniteEmails(old, (emails) => emails.filter((email) => email.id !== `scheduled-${id}`), ), ); return { previous }; }, onError: (_err, _id, context) => { context?.previous.forEach(([key, data]) => qc.setQueryData(key, data)); }, onSettled: () => { qc.invalidateQueries({ queryKey: ["scheduled-jobs"] }); qc.invalidateQueries({ queryKey: ["emails"] }); }, }); } export function useParseDate() { return useMutation({ mutationFn: async (data: { nlInput: string; timezone: string }) => { const res = await fetch(appApiPath("/api/parse-date"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (!res.ok) throw new Error("Failed to parse date"); return res.json() as Promise<{ timestamp: number | null; formatted: string | null; }>; }, }); }