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.
219 lines
5.8 KiB
TypeScript
219 lines
5.8 KiB
TypeScript
'use client'
|
|
|
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
import { apiClient, OfflineError } from '../client'
|
|
import { enqueue } from '../offline-queue'
|
|
import type { Recipient, ApiOutboxMessage, PaginatedResponse } from '../types'
|
|
|
|
export interface SendMessagePayload {
|
|
account_id: string
|
|
to: Recipient[]
|
|
cc?: Recipient[]
|
|
bcc?: Recipient[]
|
|
subject: string
|
|
body_html: string
|
|
in_reply_to?: string
|
|
idempotency_key: string
|
|
scheduled_at?: string
|
|
}
|
|
|
|
export type DraftPayload = Omit<SendMessagePayload, 'idempotency_key' | 'scheduled_at'>
|
|
|
|
export function useSendMessage() {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: async (payload: SendMessagePayload) => {
|
|
try {
|
|
return await apiClient.post<ApiOutboxMessage>('/mail/send', payload)
|
|
} catch (err) {
|
|
if (err instanceof OfflineError) {
|
|
await enqueue({
|
|
id: payload.idempotency_key,
|
|
timestamp: Date.now(),
|
|
type: 'send_message',
|
|
payload,
|
|
retries: 0,
|
|
})
|
|
return null
|
|
}
|
|
throw err
|
|
}
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['messages', 'sent'] })
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useCreateDraft() {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: async (payload: DraftPayload) => {
|
|
try {
|
|
return await apiClient.post<ApiOutboxMessage>('/mail/drafts', payload)
|
|
} catch (err) {
|
|
if (err instanceof OfflineError) {
|
|
await enqueue({
|
|
id: `draft-create-${Date.now()}`,
|
|
timestamp: Date.now(),
|
|
type: 'create_draft',
|
|
payload,
|
|
retries: 0,
|
|
})
|
|
return null
|
|
}
|
|
throw err
|
|
}
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['messages', 'drafts'] })
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useUpdateDraft() {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: async ({ id, ...payload }: DraftPayload & { id: string }) => {
|
|
try {
|
|
return await apiClient.put<ApiOutboxMessage>(`/mail/drafts/${id}`, payload)
|
|
} catch (err) {
|
|
if (err instanceof OfflineError) {
|
|
await enqueue({
|
|
id: `draft-update-${id}-${Date.now()}`,
|
|
timestamp: Date.now(),
|
|
type: 'update_draft',
|
|
payload: { draft_id: id, ...payload },
|
|
retries: 0,
|
|
})
|
|
return null
|
|
}
|
|
throw err
|
|
}
|
|
},
|
|
onSuccess: (_data, variables) => {
|
|
queryClient.invalidateQueries({ queryKey: ['messages', 'drafts'] })
|
|
queryClient.invalidateQueries({ queryKey: ['message', variables.id] })
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useDeleteDraft() {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: async ({ id }: { id: string }) => {
|
|
try {
|
|
await apiClient.delete(`/mail/drafts/${id}`)
|
|
} catch (err) {
|
|
if (err instanceof OfflineError) {
|
|
await enqueue({
|
|
id: `draft-delete-${id}-${Date.now()}`,
|
|
timestamp: Date.now(),
|
|
type: 'delete_draft',
|
|
payload: { draft_id: id },
|
|
retries: 0,
|
|
})
|
|
return
|
|
}
|
|
throw err
|
|
}
|
|
},
|
|
onMutate: async ({ id }) => {
|
|
await queryClient.cancelQueries({ queryKey: ['messages', 'drafts'] })
|
|
|
|
const previous = queryClient.getQueriesData<PaginatedResponse<ApiOutboxMessage>>({
|
|
queryKey: ['messages', 'drafts'],
|
|
})
|
|
|
|
queryClient.setQueriesData<PaginatedResponse<ApiOutboxMessage>>(
|
|
{ queryKey: ['messages', 'drafts'] },
|
|
(old) => {
|
|
if (!old) return old
|
|
return { ...old, data: old.data.filter((m) => m.id !== id) }
|
|
}
|
|
)
|
|
|
|
return { previous }
|
|
},
|
|
onError: (_err, _vars, context) => {
|
|
context?.previous?.forEach(([key, data]) => queryClient.setQueryData(key, data))
|
|
},
|
|
onSettled: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['messages', 'drafts'] })
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useScheduleSend() {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: async (payload: SendMessagePayload & { scheduled_at: string }) => {
|
|
try {
|
|
return await apiClient.post<ApiOutboxMessage>('/mail/send', payload)
|
|
} catch (err) {
|
|
if (err instanceof OfflineError) {
|
|
await enqueue({
|
|
id: payload.idempotency_key,
|
|
timestamp: Date.now(),
|
|
type: 'schedule_send',
|
|
payload,
|
|
retries: 0,
|
|
})
|
|
return null
|
|
}
|
|
throw err
|
|
}
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['outbox'] })
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useRescheduleSend() {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: async ({ id, scheduled_at }: { id: string; scheduled_at: string }) => {
|
|
return await apiClient.post<ApiOutboxMessage>(`/mail/outbox/${id}/reschedule`, {
|
|
scheduled_at,
|
|
})
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['outbox'] })
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useCancelScheduled() {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: async ({ id }: { id: string }) => {
|
|
return await apiClient.post<ApiOutboxMessage>(`/mail/outbox/${id}/cancel`)
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['outbox'] })
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useSendNow() {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: async ({ id }: { id: string }) => {
|
|
return await apiClient.post<ApiOutboxMessage>(`/mail/outbox/${id}/send-now`)
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['outbox'] })
|
|
queryClient.invalidateQueries({ queryKey: ['messages', 'sent'] })
|
|
},
|
|
})
|
|
}
|