import type { CalendarEvent, DavCollection, DavContact } from './shared'; import { useQuery } from '@tanstack/react-query'; import { useSearchParams } from 'react-router'; import { useClient } from 'hooks/useClient'; import { COLLECTION_PARAM } from './shared'; // Data layer for /calendar and /contacts. Everything goes through the /api/caldav auth proxy to the // officer-caldav sidecar, which holds the Radicale process and every DAV credential; officer forwards the // authenticated user's id and nothing else. // // There is no push channel — Radicale is a plain DAV server — so these are ordinary queries with a // generous staleTime rather than a poll. A calendar changes when someone edits it, not on a timer. const STALE_MS = 30_000; const EMPTY_COLLECTIONS: DavCollection[] = []; const EMPTY_EVENTS: CalendarEvent[] = []; const EMPTY_CONTACTS: DavContact[] = []; export function useDavCollections(kind?: DavCollection['kind']) { const { get } = useClient(); const query = useQuery({ queryKey: ['caldav', 'collections'], queryFn: () => get<{ collections: DavCollection[] }>('/caldav/_officer/collections'), staleTime: STALE_MS, }); const all = query.data?.collections ?? EMPTY_COLLECTIONS; return { collections: kind ? all.filter((c) => c.kind === kind) : all, isLoading: query.isLoading, error: query.error, }; } export function useCalendarEvents(collection: string | null) { const { get } = useClient(); const query = useQuery({ queryKey: ['caldav', 'events', collection], queryFn: () => get<{ events: CalendarEvent[] }>(`/caldav/_officer/events?collection=${encodeURIComponent(collection ?? '')}`), enabled: Boolean(collection), staleTime: STALE_MS, }); return { events: query.data?.events ?? EMPTY_EVENTS, isLoading: query.isLoading, error: query.error }; } export function useDavContacts(collection: string | null) { const { get } = useClient(); const query = useQuery({ queryKey: ['caldav', 'contacts', collection], queryFn: () => get<{ contacts: DavContact[] }>(`/caldav/_officer/contacts?collection=${encodeURIComponent(collection ?? '')}`), enabled: Boolean(collection), staleTime: STALE_MS, }); return { contacts: query.data?.contacts ?? EMPTY_CONTACTS, isLoading: query.isLoading, error: query.error }; } /** * The collection the URL selects, or the first one of that kind once the list arrives. Both panels of a * screen call this and get the same answer, because the answer is derived from the URL and the query * cache rather than passed between them. */ export function useSelectedCollection(kind: DavCollection['kind']) { const [params] = useSearchParams(); const { collections, isLoading } = useDavCollections(kind); const fromUrl = params.get(COLLECTION_PARAM); const selected = collections.find((c) => c.path === fromUrl) ?? collections[0] ?? null; return { selected, collections, isLoading }; }