ultisuite-client/lib/api/hooks/use-mail-queries.ts
R3D347HR4Y 5304790ed5
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat(auth): enhance session management and identity provider settings
- Added SessionGuard component to manage session expiration and online status.
- Updated AuthProvider to streamline session fetching and handling.
- Introduced IdentityProvidersSection for managing OAuth, SAML, and LDAP identity providers.
- Implemented identity provider guides for easier configuration.
- Enhanced mail settings with infinite scroll option for improved user experience.
- Updated global styles and layout components for better consistency across the application.
2026-06-09 09:36:46 +02:00

203 lines
6.3 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 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 function messagesQueryKey(
folder: string,
accountId?: string,
page?: number,
pageSize?: number
) {
return ['messages', folder, accountId, page, pageSize] as const
}
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 function useMessages(
folder: string,
accountId?: string,
page?: number,
pageSize?: number
) {
const { ready, authenticated } = useAuthReady()
return useQuery({
queryKey: messagesQueryKey(folder, accountId, page, pageSize),
queryFn: () =>
fetchMessagesPage(folder, accountId, page ?? 1, pageSize ?? LIST_PAGE_SIZE),
placeholderData: keepPreviousData,
staleTime: 60_000,
enabled: ready && authenticated,
})
}
export function useMessage(messageId: string | null) {
const { ready, authenticated } = useAuthReady()
return useQuery({
queryKey: ['message', messageId],
queryFn: () => apiClient.get<ApiMessageFull>(`/mail/messages/${messageId}`),
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()
return useQuery({
queryKey: ['thread', 'v2', threadId],
queryFn: () =>
apiClient.get<
ApiMessageFull[] | { messages?: ApiMessageFull[]; thread_id?: string }
>(`/mail/threads/${threadId}`),
select: unwrapThreadMessages,
enabled: ready && authenticated && !!threadId,
})
}
export function useMailAccounts() {
const { ready, authenticated } = useAuthReady()
return useQuery({
queryKey: ['accounts'],
queryFn: async () => {
const res = await apiClient.get<ApiMailAccount[] | { accounts: ApiMailAccount[] }>(
'/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()
const { ready, authenticated } = useAuthReady()
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.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
}
},
enabled: ready && authenticated && isMessageSearchFilterActive(filter),
})
}