ultisuite-client/lib/api/hooks/use-contact-queries.ts
R3D347HR4Y 07d57f13a8
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
Add Contact Avatar Features and Improve UI Components
- Introduced new ContactAvatar and ContactAvatarPicker components for enhanced avatar management in contact views.
- Updated ContactDetailView and ContactFormView to utilize the new avatar components, improving user experience when adding or editing contacts.
- Enhanced ContactHoverCard and ContactRow components to display avatars, providing a more visually appealing interface.
- Added loading and error states in ContactsListView for better user feedback during data fetching.
- Implemented a new ContactsLoadState component to handle loading and error scenarios in the contacts list.
- Updated package.json to include @formkit/auto-animate for improved UI animations.
2026-06-06 20:26:51 +02:00

141 lines
4.1 KiB
TypeScript

'use client'
import { useMemo } from 'react'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { rankApiContacts } from '@/lib/contacts/contact-match-score'
import { apiClient, OfflineError } from '../client'
import type { ApiContact, ApiContactSyncResponse } from '../types'
export const FALLBACK_CONTACT_BOOK_ID = 'contacts'
type ApiContactBook = { id: string; name: string }
type ApiContactsListResponse = {
contacts: ApiContact[]
pagination?: {
page: number
page_size: number
total?: number
}
}
export function normalizeContactBooksResponse(booksRaw: unknown): ApiContactBook[] {
if (Array.isArray(booksRaw)) return booksRaw as ApiContactBook[]
if (booksRaw && typeof booksRaw === 'object' && 'address_books' in booksRaw) {
return (booksRaw as { address_books: ApiContactBook[] }).address_books ?? []
}
return []
}
export async function fetchContactByPath(path: string): Promise<ApiContact> {
const apiPath = path.replace(/^\/+/, '')
return apiClient.get<ApiContact>(`/contacts/${apiPath}`)
}
export async function fetchContactsForBook(bookId: string): Promise<ApiContact[]> {
const pageSize = 500
let page = 1
const all: ApiContact[] = []
while (page <= 100) {
const res = await apiClient.get<ApiContact[] | ApiContactsListResponse>(
`/contacts/books/${bookId}`,
{ page: String(page), page_size: String(pageSize) },
)
const batch = Array.isArray(res) ? res : (res.contacts ?? [])
all.push(...batch)
if (batch.length < pageSize) break
const total = !Array.isArray(res) ? res.pagination?.total : undefined
if (total != null && all.length >= total) break
page += 1
}
return all
}
export function useDefaultContactBookId() {
const { data: booksRaw, isLoading, isError } = useContactBooks()
return useMemo(() => {
if (isLoading || isError) return undefined
const books = normalizeContactBooksResponse(booksRaw)
return books[0]?.id ?? FALLBACK_CONTACT_BOOK_ID
}, [booksRaw, isLoading, isError])
}
export function useContacts(bookId?: string) {
const defaultBookId = useDefaultContactBookId()
const resolvedBookId = bookId ?? defaultBookId
return useQuery({
queryKey: ['contacts', resolvedBookId],
queryFn: () => fetchContactsForBook(resolvedBookId!),
enabled: !!resolvedBookId,
staleTime: 5 * 60_000,
})
}
export function useContactBooks() {
return useQuery({
queryKey: ['contact-books'],
queryFn: async () => {
const res = await apiClient.get<ApiContactBook[] | { address_books: ApiContactBook[] }>(
'/contacts/books',
)
return normalizeContactBooksResponse(res)
},
staleTime: 10 * 60_000,
})
}
export function useSyncContacts(bookId?: string, syncToken?: string) {
return useQuery({
queryKey: ['contacts-sync', bookId, syncToken],
queryFn: () =>
apiClient.get<ApiContactSyncResponse>(`/contacts/books/${bookId}/sync`, {
sync_token: syncToken,
}),
enabled: !!bookId && !!syncToken,
})
}
export function useSearchContacts(query: string) {
const queryClient = useQueryClient()
return useQuery({
queryKey: ['contacts-search', query],
queryFn: async () => {
try {
const res = await apiClient.get<ApiContact[] | ApiContactsListResponse>(
'/contacts/search',
{ q: query },
)
const list = Array.isArray(res) ? res : (res.contacts ?? [])
return rankApiContacts(list, query)
} catch (err) {
if (err instanceof OfflineError) {
const cached = queryClient.getQueriesData<ApiContact[]>({
queryKey: ['contacts'],
})
const allContacts: ApiContact[] = []
for (const [, data] of cached) {
if (data) allContacts.push(...data)
}
return rankApiContacts(allContacts, query)
}
throw err
}
},
enabled: query.length >= 2,
staleTime: 30_000,
})
}
export function useContactInteractions(email?: string) {
return useQuery({
queryKey: ['contact-interactions', email],
queryFn: () => apiClient.get<unknown>('/contacts/interactions', { email }),
enabled: !!email,
})
}