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.
193 lines
6.2 KiB
TypeScript
193 lines
6.2 KiB
TypeScript
'use client'
|
|
|
|
import { useMemo } from 'react'
|
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
|
import { rankApiContacts } from '@/lib/contacts/contact-match-score'
|
|
import { useAuthReady } from '@/lib/api/use-auth-ready'
|
|
import { useIsDemoContacts } from '@/lib/demo/demo-contacts-context'
|
|
import { DEMO_CONTACTS_QUERY_ROOT } from '@/lib/demo/demo-contacts-bootstrap'
|
|
import { useDemoContactsStore } from '@/lib/demo/demo-contacts-store'
|
|
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(/^\/+/, '')
|
|
if (useDemoContactsStore.getState().active) {
|
|
const found = useDemoContactsStore
|
|
.getState()
|
|
.contacts.find(
|
|
(contact) =>
|
|
contact.path?.replace(/^\/+/, '') === apiPath ||
|
|
contact.uid === apiPath ||
|
|
contact.uid === path,
|
|
)
|
|
if (found) return found
|
|
}
|
|
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 { ready, authenticated } = useAuthReady()
|
|
const isDemoContacts = useIsDemoContacts()
|
|
const demoVersion = useDemoContactsStore((s) => s.version)
|
|
const defaultBookId = useDefaultContactBookId()
|
|
const resolvedBookId = bookId ?? defaultBookId
|
|
|
|
return useQuery({
|
|
queryKey: isDemoContacts
|
|
? [...DEMO_CONTACTS_QUERY_ROOT, 'list', resolvedBookId, demoVersion]
|
|
: ['contacts', resolvedBookId],
|
|
queryFn: () => {
|
|
if (isDemoContacts && resolvedBookId) {
|
|
return useDemoContactsStore.getState().listContacts(resolvedBookId)
|
|
}
|
|
return fetchContactsForBook(resolvedBookId!)
|
|
},
|
|
enabled: isDemoContacts ? ready && authenticated && !!resolvedBookId : !!resolvedBookId,
|
|
staleTime: 5 * 60_000,
|
|
initialData:
|
|
isDemoContacts && resolvedBookId
|
|
? () => useDemoContactsStore.getState().listContacts(resolvedBookId)
|
|
: undefined,
|
|
})
|
|
}
|
|
|
|
export function useContactBooks() {
|
|
const { ready, authenticated } = useAuthReady()
|
|
const isDemoContacts = useIsDemoContacts()
|
|
const demoVersion = useDemoContactsStore((s) => s.version)
|
|
|
|
return useQuery({
|
|
queryKey: isDemoContacts
|
|
? [...DEMO_CONTACTS_QUERY_ROOT, 'books', demoVersion]
|
|
: ['contact-books'],
|
|
queryFn: async () => {
|
|
if (isDemoContacts) {
|
|
return useDemoContactsStore.getState().listBooks()
|
|
}
|
|
const res = await apiClient.get<ApiContactBook[] | { address_books: ApiContactBook[] }>(
|
|
'/contacts/books',
|
|
)
|
|
return normalizeContactBooksResponse(res)
|
|
},
|
|
enabled: isDemoContacts ? ready && authenticated : true,
|
|
staleTime: 10 * 60_000,
|
|
initialData: isDemoContacts
|
|
? () => useDemoContactsStore.getState().listBooks()
|
|
: undefined,
|
|
})
|
|
}
|
|
|
|
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()
|
|
const isDemoContacts = useIsDemoContacts()
|
|
const demoVersion = useDemoContactsStore((s) => s.version)
|
|
|
|
return useQuery({
|
|
queryKey: isDemoContacts
|
|
? [...DEMO_CONTACTS_QUERY_ROOT, 'search', query, demoVersion]
|
|
: ['contacts-search', query],
|
|
queryFn: async () => {
|
|
if (isDemoContacts) {
|
|
return useDemoContactsStore.getState().searchContacts(query)
|
|
}
|
|
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,
|
|
initialData: isDemoContacts
|
|
? () => useDemoContactsStore.getState().searchContacts(query)
|
|
: undefined,
|
|
})
|
|
}
|
|
|
|
export function useContactInteractions(email?: string) {
|
|
return useQuery({
|
|
queryKey: ['contact-interactions', email],
|
|
queryFn: () => apiClient.get<unknown>('/contacts/interactions', { email }),
|
|
enabled: !!email,
|
|
})
|
|
}
|