import { useState, useEffect, useRef } from 'react'; import { subscribeToParticle, subscribeToParticleChildren, subscribeToLatestChild, getParticle, getParticleChildren, } from '@/lib/firestore-particles'; import type { Particle } from '@/api/types'; import { type ParticlePath, toFirestoreDocPath, toFirestoreChildrenPath, } from '@/lib/particle-path'; import { useQuery } from '@tanstack/react-query'; import { QueryFieldFilterConstraint } from 'firebase/firestore'; interface UseLiveParticleResult { particle: Particle | null; isLoading: boolean; error: Error | null; } export function useLiveParticle(path: ParticlePath): UseLiveParticleResult { const [particle, setParticle] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const docPath = toFirestoreDocPath(path); const unsubscribe = subscribeToParticle( docPath, (data) => { setParticle(data); setIsLoading(false); }, (err) => { setError(err); setIsLoading(false); }, ); return () => { unsubscribe(); setIsLoading(true); setError(null); setParticle(null); }; }, [path]); return { particle, isLoading, error }; } interface UseLiveParticleChildrenResult { children: Particle[]; isLoading: boolean; error: Error | null; } interface UseLiveParticleChildrenParams { orderByField?: string; orderDirection?: 'asc' | 'desc'; visibilityScopes?: string[]; onAdded?: (child: Particle) => void; onRemoved?: (child: Particle, updatedChildren: Particle[]) => void; whereFilter?: QueryFieldFilterConstraint; /** Optional cap on results. Changes trigger a re-subscription. */ limit?: number; } export function useLiveParticleChildren( path: ParticlePath | undefined, { orderByField = 'created_at', orderDirection = 'desc', visibilityScopes, onAdded, onRemoved, whereFilter, limit, }: UseLiveParticleChildrenParams = {}, ): UseLiveParticleChildrenResult { const [children, setChildren] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); // Keep the latest add/remove callbacks in refs so changing them doesn't force // the subscription to re-attach — they're notifications, not query params. const onAddedRef = useRef(onAdded); const onRemovedRef = useRef(onRemoved); useEffect(() => { onAddedRef.current = onAdded; onRemovedRef.current = onRemoved; }, [onAdded, onRemoved]); useEffect(() => { if (!path) return; const collectionPath = toFirestoreChildrenPath(path); const unsubscribe = subscribeToParticleChildren(collectionPath, { onData: (data) => { setChildren(data); setIsLoading(false); }, onError: (err) => { console.warn(err); setError(err); setIsLoading(false); }, visibilityScopes, orderByField, orderDirection, onAdded: (child: Particle) => onAddedRef.current?.(child), onRemoved: (child: Particle, updatedChildren: Particle[]) => onRemovedRef.current?.(child, updatedChildren), whereFilter, limit, }); return () => { unsubscribe(); setChildren([]); setError(null); setIsLoading(true); }; }, [ path, whereFilter, limit, orderByField, orderDirection, visibilityScopes, ]); // No path: nothing to load, so report an empty non-loading state. if (!path) { return { children: [], isLoading: false, error: null }; } return { children, isLoading, error }; } interface UseLiveLatestChildResult { latestChild: Particle | null; isLoading: boolean; } export function useLiveLatestChild( path: ParticlePath, ): UseLiveLatestChildResult { const [latestChild, setLatestChild] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { const unsubscribe = subscribeToLatestChild( toFirestoreChildrenPath(path), (data) => { setLatestChild(data); setIsLoading(false); }, () => { setIsLoading(false); }, ); return () => { unsubscribe(); setIsLoading(true); setLatestChild(null); }; }, [path]); return { latestChild, isLoading }; } export function useParticle(path?: ParticlePath) { return useQuery({ queryKey: ['particle', path], queryFn: async () => { if (!path) return null; const docPath = toFirestoreDocPath(path); const particle = await getParticle(docPath); return particle; }, enabled: !!path, }); } export function useParticleChildren(path?: ParticlePath) { return useQuery({ queryKey: ['particle-children', path], queryFn: async () => { if (!path) return []; const collectionPath = toFirestoreChildrenPath(path); return getParticleChildren(collectionPath); }, enabled: !!path, staleTime: 1000 * 60 * 5, // 5 min — attachments don't change }); }