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.
113 lines
3.7 KiB
TypeScript
113 lines
3.7 KiB
TypeScript
'use client'
|
|
|
|
import { useQuery, useQueryClient, keepPreviousData } from '@tanstack/react-query'
|
|
import { apiClient, OfflineError } from '../client'
|
|
import type {
|
|
PaginatedResponse,
|
|
ApiMessageSummary,
|
|
ApiMessageFull,
|
|
ApiMailAccount,
|
|
MessageSearchFilter,
|
|
} from '../types'
|
|
|
|
export function useMessages(folder: string, accountId?: string, page?: number) {
|
|
return useQuery({
|
|
queryKey: ['messages', folder, accountId, page],
|
|
queryFn: () =>
|
|
apiClient.get<PaginatedResponse<ApiMessageSummary>>('/mail/messages', {
|
|
folder,
|
|
account_id: accountId,
|
|
page: String(page ?? 1),
|
|
page_size: '50',
|
|
}),
|
|
placeholderData: keepPreviousData,
|
|
staleTime: 60_000,
|
|
})
|
|
}
|
|
|
|
export function useMessage(messageId: string | null) {
|
|
return useQuery({
|
|
queryKey: ['message', messageId],
|
|
queryFn: () => apiClient.get<ApiMessageFull>(`/mail/messages/${messageId}`),
|
|
enabled: !!messageId,
|
|
})
|
|
}
|
|
|
|
export function useThread(threadId: string | null) {
|
|
return useQuery({
|
|
queryKey: ['thread', threadId],
|
|
queryFn: () => apiClient.get<ApiMessageFull[]>(`/mail/threads/${threadId}`),
|
|
enabled: !!threadId,
|
|
})
|
|
}
|
|
|
|
export function useMailAccounts() {
|
|
return useQuery({
|
|
queryKey: ['accounts'],
|
|
queryFn: () => apiClient.get<ApiMailAccount[]>('/mail/accounts'),
|
|
staleTime: 5 * 60_000,
|
|
})
|
|
}
|
|
|
|
export function useMailSearch(filter: MessageSearchFilter | null) {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useQuery({
|
|
queryKey: ['mail-search', filter],
|
|
queryFn: async () => {
|
|
const params: Record<string, string | undefined> = {}
|
|
if (filter) {
|
|
if (filter.q) params.q = filter.q
|
|
if (filter.from) params.from = filter.from
|
|
if (filter.label) params.label = filter.label
|
|
if (filter.account_id) params.account_id = filter.account_id
|
|
if (filter.date_from) params.date_from = filter.date_from
|
|
if (filter.date_to) params.date_to = filter.date_to
|
|
if (filter.has_attachment !== undefined) params.has_attachment = String(filter.has_attachment)
|
|
}
|
|
|
|
try {
|
|
return await apiClient.get<PaginatedResponse<ApiMessageSummary>>('/mail/search', params)
|
|
} catch (err) {
|
|
if (err instanceof OfflineError) {
|
|
const cached = queryClient.getQueriesData<PaginatedResponse<ApiMessageSummary>>({
|
|
queryKey: ['messages'],
|
|
})
|
|
const allMessages: ApiMessageSummary[] = []
|
|
for (const [, data] of cached) {
|
|
if (data?.data) allMessages.push(...data.data)
|
|
}
|
|
|
|
const q = filter?.q?.toLowerCase()
|
|
const filtered = allMessages.filter((m) => {
|
|
if (q) {
|
|
const matchSubject = m.subject.toLowerCase().includes(q)
|
|
const matchSnippet = m.snippet.toLowerCase().includes(q)
|
|
const matchFrom = m.from.some(
|
|
(r) => r.address.toLowerCase().includes(q) || r.name.toLowerCase().includes(q)
|
|
)
|
|
if (!matchSubject && !matchSnippet && !matchFrom) return false
|
|
}
|
|
if (filter?.from) {
|
|
const fromMatch = m.from.some(
|
|
(r) =>
|
|
r.address.toLowerCase().includes(filter.from!.toLowerCase()) ||
|
|
r.name.toLowerCase().includes(filter.from!.toLowerCase())
|
|
)
|
|
if (!fromMatch) return false
|
|
}
|
|
if (filter?.label) {
|
|
if (!m.labels.includes(filter.label)) return false
|
|
}
|
|
return true
|
|
})
|
|
|
|
return { data: filtered, pagination: { page: 1, page_size: filtered.length } }
|
|
}
|
|
throw err
|
|
}
|
|
},
|
|
enabled: !!(filter?.q || filter?.from || filter?.label),
|
|
})
|
|
}
|