Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Introduced turbopack alias for canvas in next.config.mjs. - Updated package.json scripts for development and branding tasks. - Added new dependencies for Tiptap extensions. - Implemented new demo layouts for agenda, contacts, drive, and mail applications. - Enhanced globals.css for improved theming and splash screen animations. - Added OAuth callback handling for drive mounts. - Updated layout components to integrate new demo shells and improve structure.
321 lines
9.7 KiB
TypeScript
321 lines
9.7 KiB
TypeScript
'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<ApiMessageSummary> & {
|
|
messages?: ApiMessageSummary[]
|
|
}
|
|
|
|
/** Backend returns `{ messages, pagination }`; client normalizes to `{ data, pagination }`. */
|
|
export function unwrapMessages(
|
|
res: ApiMessagesPayload
|
|
): PaginatedResponse<ApiMessageSummary> {
|
|
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<PaginatedResponse<ApiMessageSummary>> {
|
|
const safePageSize = normalizeListPageSize(pageSize)
|
|
const res = await apiClient.get<ApiMessagesPayload>('/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<PaginatedResponse<ApiMessageSummary>> {
|
|
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<ApiMessageSummary> {
|
|
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<ApiMessageFull>(`/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<ApiMailAccount[] | { accounts: ApiMailAccount[] }>(
|
|
'/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<string, string | undefined> = {}
|
|
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<ApiMessagesPayload>('/mail/search', params)
|
|
return unwrapMessages(res)
|
|
} 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
|
|
}
|
|
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),
|
|
})
|
|
}
|