'use client' import { useQuery, useQueryClient, keepPreviousData } from '@tanstack/react-query' import { apiClient, OfflineError } from '../client' import { useAuthReady } from '../use-auth-ready' import { normalizeListPageSize, LIST_PAGE_SIZE } from '@/lib/mail-list-page-size' import { useIsDemoMail } from '@/lib/demo/demo-mail-context' import { useDemoMailStore } from '@/lib/demo/demo-mail-store' import { DEMO_MAIL_ACCOUNT } from '@/lib/demo/demo-mail-api-data' 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 const MAIL_MESSAGES_QUERY_ROOT = ['messages'] as const export const DEMO_MAIL_MESSAGES_QUERY_ROOT = ['demo', 'messages'] as const export function messagesQueryKey( folder: string, accountId?: string, page?: number, pageSize?: number ) { return ['messages', folder, accountId, page, pageSize] as const } /** Demo list keys are isolated from persisted real-mail `messages` cache (IDB). */ export function messagesListQueryKey( folder: string, accountId?: string, page?: number, pageSize?: number, isDemo?: boolean, demoVersion?: number ) { if (isDemo) { return [ 'demo', 'messages', folder, accountId, page, pageSize, demoVersion ?? 0, ] as const } return messagesQueryKey(folder, accountId, page, pageSize) } export function mailMessagesQueryRoot(isDemo: boolean) { return isDemo ? DEMO_MAIL_MESSAGES_QUERY_ROOT : MAIL_MESSAGES_QUERY_ROOT } export async function fetchMessagesPage( folder: string, accountId: string | undefined, page: number, pageSize: number ): Promise> { const safePageSize = normalizeListPageSize(pageSize) const res = await apiClient.get('/mail/messages', { folder, account_id: accountId, page: String(page), page_size: String(safePageSize), }) return unwrapMessages(res) } export async function fetchMessagesListPage( folder: string, accountId: string | undefined, page: number, pageSize: number, isDemo: boolean ): Promise> { const safePageSize = normalizeListPageSize(pageSize) if (isDemo) { return demoListMessagesPage(folder, page, safePageSize) } return fetchMessagesPage(folder, accountId, page, safePageSize) } function demoListMessagesPage( folder: string, page: number, pageSize: number ): PaginatedResponse { return useDemoMailStore.getState().listMessages( folder, page, normalizeListPageSize(pageSize) ) } export function useMessages( folder: string, accountId?: string, page?: number, pageSize?: number ) { const { ready, authenticated } = useAuthReady() const isDemoMail = useIsDemoMail() const demoVersion = useDemoMailStore((s) => s.version) const pageNum = page ?? 1 const safePageSize = pageSize ?? LIST_PAGE_SIZE return useQuery({ queryKey: messagesListQueryKey( folder, accountId, page, pageSize, isDemoMail, demoVersion ), queryFn: () => { if (isDemoMail) { return demoListMessagesPage(folder, pageNum, safePageSize) } return fetchMessagesPage(folder, accountId, pageNum, safePageSize) }, initialData: isDemoMail ? () => demoListMessagesPage(folder, pageNum, safePageSize) : undefined, placeholderData: keepPreviousData, staleTime: 60_000, enabled: ready && authenticated, }) } export function useMessage(messageId: string | null) { const { ready, authenticated } = useAuthReady() const isDemoMail = useIsDemoMail() const demoVersion = useDemoMailStore((s) => s.version) return useQuery({ queryKey: isDemoMail ? ['demo', 'message', messageId, demoVersion] : ['message', messageId], queryFn: () => { if (isDemoMail) { const message = useDemoMailStore.getState().getMessage(messageId!) if (!message) throw new Error('Message introuvable') return message } return apiClient.get(`/mail/messages/${messageId}`) }, initialData: isDemoMail && messageId ? () => { const message = useDemoMailStore.getState().getMessage(messageId) if (!message) throw new Error('Message introuvable') return message } : undefined, enabled: ready && authenticated && !!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) { const { ready, authenticated } = useAuthReady() const isDemoMail = useIsDemoMail() const demoVersion = useDemoMailStore((s) => s.version) return useQuery({ queryKey: isDemoMail ? ['demo', 'thread', 'v2', threadId, demoVersion] : ['thread', 'v2', threadId], queryFn: () => { if (isDemoMail) { return useDemoMailStore.getState().getThread(threadId!) } return apiClient.get< ApiMessageFull[] | { messages?: ApiMessageFull[]; thread_id?: string } >(`/mail/threads/${threadId}`) }, select: isDemoMail ? undefined : unwrapThreadMessages, enabled: ready && authenticated && !!threadId, }) } export function useMailAccounts() { const { ready, authenticated } = useAuthReady() const isDemoMail = useIsDemoMail() return useQuery({ queryKey: ['accounts', isDemoMail ? 'demo' : null], queryFn: async () => { if (isDemoMail) return [DEMO_MAIL_ACCOUNT] const res = await apiClient.get( '/mail/accounts' ) return Array.isArray(res) ? res : (res.accounts ?? []) }, initialData: isDemoMail ? () => [DEMO_MAIL_ACCOUNT] : undefined, staleTime: 5 * 60_000, enabled: ready && authenticated, retry: isDemoMail ? false : 1, }) } export function useMailSearch(filter: MessageSearchFilter | null) { const queryClient = useQueryClient() const { ready, authenticated } = useAuthReady() const isDemoMail = useIsDemoMail() const demoVersion = useDemoMailStore((s) => s.version) return useQuery({ queryKey: isDemoMail ? ['demo', 'mail-search', filter, demoVersion] : ['mail-search', filter], queryFn: async () => { if (isDemoMail) { return useDemoMailStore.getState().searchMessages(filter) } 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 } }, initialData: isDemoMail ? () => useDemoMailStore.getState().searchMessages(filter) : undefined, enabled: ready && authenticated && isMessageSearchFilterActive(filter), }) }