diff --git a/js/src/features/particles/folder-view.tsx b/js/src/features/particles/folder-view.tsx index 34bcaed..b71ca82 100644 --- a/js/src/features/particles/folder-view.tsx +++ b/js/src/features/particles/folder-view.tsx @@ -9,7 +9,7 @@ interface FolderViewProps { } export function FolderView({ path, folderParticle }: FolderViewProps) { - const { children, error, isLoading } = useLiveParticleChildren(path); + const { children, error, isLoading } = useLiveParticleChildren({ path }); const { networkId } = parseParticlePath(path); return ( diff --git a/js/src/hooks/use-particle.ts b/js/src/hooks/use-particle.ts index 2f99b90..1397c66 100644 --- a/js/src/hooks/use-particle.ts +++ b/js/src/hooks/use-particle.ts @@ -1,4 +1,4 @@ -import { useState, useEffect, useMemo } from "react"; +import { useState, useEffect } from "react"; import { subscribeToParticle, subscribeToParticleChildren, @@ -26,13 +26,12 @@ export function useLiveParticle(path: ParticlePath): UseLiveParticleResult { const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - const docPath = useMemo(() => toFirestoreDocPath(path), [path]); - useEffect(() => { setIsLoading(true); setError(null); setParticle(null); + const docPath = toFirestoreDocPath(path); const unsubscribe = subscribeToParticle( docPath, (data) => { @@ -46,7 +45,7 @@ export function useLiveParticle(path: ParticlePath): UseLiveParticleResult { ); return unsubscribe; - }, [docPath]); + }, [path]); return { particle, isLoading, error }; } @@ -57,47 +56,59 @@ interface UseLiveParticleChildrenResult { 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: string = "created_at", - orderDirection: "asc" | "desc" = "desc", - visibilityScopes?: string[], - onAdded?: (child: Particle) => void, - onRemoved?: (child: Particle, updatedChildren: Particle[]) => void, - whereFilter?: QueryFieldFilterConstraint + { + 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); - const collectionPath = useMemo(() => toFirestoreChildrenPath(path), [path]); - useEffect(() => { setIsLoading(true); setError(null); setChildren([]); + const collectionPath = toFirestoreChildrenPath(path); + const unsubscribe = subscribeToParticleChildren( collectionPath, - (data) => { - setChildren(data); - setIsLoading(false); - }, - (err) => { - setError(err); - setIsLoading(false); - }, - visibilityScopes, - orderByField, - orderDirection, - onAdded, - onRemoved, - whereFilter, + { + onData: (data) => { + setChildren(data); + setIsLoading(false); + }, + onError: (err) => { + setError(err); + setIsLoading(false); + }, + visibilityScopes, + orderByField, + orderDirection, + onAdded, + onRemoved, + whereFilter, + } ); return unsubscribe; - // FIX: do we need to listen to more deps? Would that cause side effects that break behavior - }, [collectionPath]); + }, [path]); return { children, isLoading, error }; } diff --git a/js/src/hooks/use-stream-particles.ts b/js/src/hooks/use-stream-particles.ts index dac1544..3ad1254 100644 --- a/js/src/hooks/use-stream-particles.ts +++ b/js/src/hooks/use-stream-particles.ts @@ -1,10 +1,8 @@ -import { useEffect, useMemo, useState } from "react"; +import { useMemo } from "react"; import { useLiveParticleChildren } from "@/hooks/use-particle"; import { useAuthStore } from "@/stores/auth-store"; import { parseParticlePath, type ParticlePath } from "@/lib/particle-path"; import type { Particle, StreamProperties } from "@/api/types"; -import { where, Timestamp } from "firebase/firestore"; -import { useNetwork } from "@/hooks/use-networks"; type StreamParticle = Particle & { type: "stream"; properties: StreamProperties }; @@ -27,37 +25,14 @@ export function useStreamParticles(path: ParticlePath): UseStreamParticlesResult const { networkId } = parseParticlePath(path); const user = useAuthStore((s) => s.user); const visibilityScopes = useVisibilityScopes(user?.id, networkId); - const network = useNetwork(networkId); - const retentionHours = network?.message_retention_hours ?? 24; - - const [recencyCutoff, setRecencyCutoff] = useState(() => { - const d = new Date(); - d.setHours(d.getHours() - retentionHours); - return Timestamp.fromDate(d); - }); - - useEffect(() => { - // Recalculate immediately when retention changes - const d = new Date(); - d.setHours(d.getHours() - retentionHours); - setRecencyCutoff(Timestamp.fromDate(d)); - - const interval = setInterval(() => { - const d = new Date(); - d.setHours(d.getHours() - retentionHours); - setRecencyCutoff(Timestamp.fromDate(d)); - }, 60 * 60 * 1000); - return () => clearInterval(interval); - }, [retentionHours]); const { children, isLoading } = useLiveParticleChildren( path, - "last_child_created_at", - "desc", - visibilityScopes, - undefined, - undefined, - where("last_child_created_at", ">=", recencyCutoff), + { + orderByField: "last_child_created_at", + orderDirection: "desc", + visibilityScopes, + } ); const streams = useMemo( diff --git a/js/src/lib/firestore-particles.ts b/js/src/lib/firestore-particles.ts index 10ebcbb..fa2327f 100644 --- a/js/src/lib/firestore-particles.ts +++ b/js/src/lib/firestore-particles.ts @@ -128,10 +128,17 @@ export async function getParticle(docPath: string): Promise { return doc.data(); } +export interface GetParticleChildrenOptions { + orderByField: string; + orderDirection: "asc" | "desc"; +} + export async function getParticleChildren( collectionPath: string, - orderByField: string = "created_at", - orderDirection: "asc" | "desc" = "asc", + { + orderByField = "created_at", + orderDirection = "asc", + }: GetParticleChildrenOptions = { orderByField: "created_at", orderDirection: "asc" }, ): Promise { const q = query( typedCollection(collectionPath), @@ -141,16 +148,29 @@ export async function getParticleChildren( return snap.docs.map((d) => d.data()); } +export interface SubscribeToParticleChildrenOptions { + onData: (children: Particle[]) => void; + onError: (error: Error) => void; + visibilityScopes?: string[]; + orderByField?: string; + orderDirection?: "asc" | "desc"; + onAdded?: (child: Particle) => void; + onRemoved?: (child: Particle, updatedChildren: Particle[]) => void; + whereFilter?: QueryFieldFilterConstraint; +} + export function subscribeToParticleChildren( collectionPath: string, - onData: (children: Particle[]) => void, - onError: (error: Error) => void, - visibilityScopes: string[] = [], - orderByField: string = "created_at", - orderDirection: "asc" | "desc" = "desc", - onAdded?: (child: Particle) => void, - onRemoved?: (child: Particle, updatedChildren: Particle[]) => void, - whereFilter?: QueryFieldFilterConstraint, + { + onData, + onError, + visibilityScopes = [], + orderByField = "created_at", + orderDirection = "desc", + onAdded, + onRemoved, + whereFilter, + }: SubscribeToParticleChildrenOptions ): Unsubscribe { let q = query(typedCollection(collectionPath), orderBy(orderByField, orderDirection)); if (visibilityScopes.length > 0) {