'use client' import { useQuery, useQueryClient, keepPreviousData } from '@tanstack/react-query' import { apiClient, OfflineError } from '../client' import { useAuthReady } from '../use-auth-ready' import { normalizeListPageSize } from '@/lib/mail-list-page-size' import type { PaginatedResponse, ApiMessageSummary, ApiMessageFull, ApiMailAccount, MessageSearchFilter, } from '../types' import { isMessageSearchFilterActive } from '@/lib/mail-search/search-filter' type ApiMessagesPayload = PaginatedResponse & { messages?: ApiMessageSummary[] } /** Backend returns `{ messages, pagination }`; client normalizes to `{ data, pagination }`. */ export function unwrapMessages( res: ApiMessagesPayload ): PaginatedResponse { if (Array.isArray(res.data)) { return { data: res.data, pagination: res.pagination } } const messages = res.messages ?? [] return { data: messages, pagination: res.pagination ?? { page: 1, page_size: messages.length, total: messages.length, }, } } export function useMessages( folder: string, accountId?: string, page?: number, pageSize?: number ) { return useQuery({ queryKey: ['messages', folder, accountId, page, pageSize], queryFn: async () => { const safePageSize = normalizeListPageSize(pageSize ?? 50) const res = await apiClient.get('/mail/messages', { folder, account_id: accountId, page: String(page ?? 1), page_size: String(safePageSize), }) return unwrapMessages(res) }, placeholderData: keepPreviousData, staleTime: 60_000, }) } export function useMessage(messageId: string | null) { return useQuery({ queryKey: ['message', messageId], queryFn: () => apiClient.get(`/mail/messages/${messageId}`), enabled: !!messageId, placeholderData: keepPreviousData, staleTime: 5 * 60_000, }) } /** Backend returns `{ thread_id, messages }`; normalize to array for consumers. */ export function unwrapThreadMessages( res: | ApiMessageFull[] | { messages?: ApiMessageFull[]; thread_id?: string } | null | undefined ): ApiMessageFull[] { if (!res) return [] if (Array.isArray(res)) return res return Array.isArray(res.messages) ? res.messages : [] } export function useThread(threadId: string | null) { return useQuery({ queryKey: ['thread', 'v2', threadId], queryFn: () => apiClient.get< ApiMessageFull[] | { messages?: ApiMessageFull[]; thread_id?: string } >(`/mail/threads/${threadId}`), select: unwrapThreadMessages, enabled: !!threadId, }) } export function useMailAccounts() { const { ready, authenticated } = useAuthReady() return useQuery({ queryKey: ['accounts'], queryFn: async () => { const res = await apiClient.get( '/mail/accounts' ) return Array.isArray(res) ? res : (res.accounts ?? []) }, staleTime: 5 * 60_000, enabled: ready && authenticated, retry: 1, }) } export function useMailSearch(filter: MessageSearchFilter | null) { const queryClient = useQueryClient() return useQuery({ queryKey: ['mail-search', filter], queryFn: async () => { const params: Record = {} if (filter) { if (filter.q) params.q = filter.q if (filter.from) params.sender = 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 { const res = await apiClient.get('/mail/search', params) return unwrapMessages(res) } catch (err) { if (err instanceof OfflineError) { const cached = queryClient.getQueriesData>({ 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 } if (filter?.has_attachment !== undefined) { if (m.has_attachments !== filter.has_attachment) return false } return true }) return { data: filtered, pagination: { page: 1, page_size: filtered.length } } } throw err } }, enabled: isMessageSearchFilterActive(filter), }) }