import { useState, useEffect } 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(() => { setIsLoading(true); setError(null); setParticle(null); const docPath = toFirestoreDocPath(path); const unsubscribe = subscribeToParticle( docPath, (data) => { setParticle(data); setIsLoading(false); }, (err) => { setError(err); setIsLoading(false); }, ); return unsubscribe; }, [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; } export function useLiveParticleChildren( path: ParticlePath, { orderByField = "created_at", orderDirection = "desc", visibilityScopes, onAdded, onRemoved, whereFilter, }: UseLiveParticleChildrenParams ): UseLiveParticleChildrenResult { const [children, setChildren] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { setIsLoading(true); setError(null); setChildren([]); const collectionPath = toFirestoreChildrenPath(path); const unsubscribe = subscribeToParticleChildren( collectionPath, { onData: (data) => { setChildren(data); setIsLoading(false); }, onError: (err) => { setError(err); setIsLoading(false); }, visibilityScopes, orderByField, orderDirection, onAdded, onRemoved, whereFilter, } ); return unsubscribe; }, [path]); 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(() => { setIsLoading(true); setLatestChild(null); const unsubscribe = subscribeToLatestChild( toFirestoreChildrenPath(path), (data) => { setLatestChild(data); setIsLoading(false); }, () => { setIsLoading(false); }, ); return unsubscribe; }, [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 }); }