feat: filter to ephemeral streams and particles

This commit is contained in:
talksik
2026-03-21 18:55:03 -07:00
parent 356a166c7e
commit 97b2e7914e
5 changed files with 41 additions and 3 deletions
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import beepSound from "../../../assets/sound.wav"; import beepSound from "../../../assets/sound.wav";
import { import {
@@ -28,6 +28,8 @@ import { Progress } from "@/components/ui/progress";
import { Small } from "@/components/ui/typography"; import { Small } from "@/components/ui/typography";
import type { Particle, StreamProperties } from "@/api/types"; import type { Particle, StreamProperties } from "@/api/types";
import { useAutoplayStore } from "@/stores/autoplay-store"; import { useAutoplayStore } from "@/stores/autoplay-store";
import { where, Timestamp } from "firebase/firestore";
import { RECENCY_WINDOW_HOURS } from "@/lib/constants";
function getParticleTypeIcon(particle: Particle): LucideIcon { function getParticleTypeIcon(particle: Particle): LucideIcon {
switch (particle.type) { switch (particle.type) {
@@ -257,7 +259,23 @@ export function ParticleListView({ path }: ParticleListViewProps) {
const user = useAuthStore((s) => s.user); const user = useAuthStore((s) => s.user);
const visibilityScopes = useVisibilityScopes(user?.email, networkId); const visibilityScopes = useVisibilityScopes(user?.email, networkId);
const { children, isLoading } = useLiveParticleChildren(path, "last_child_created_at", "desc", visibilityScopes); const [recencyCutoff, setRecencyCutoff] = useState(() => {
const d = new Date();
d.setHours(d.getHours() - RECENCY_WINDOW_HOURS);
return Timestamp.fromDate(d);
});
// Refresh the cutoff every hour so streams don't vanish/appear stale
useEffect(() => {
const interval = setInterval(() => {
const d = new Date();
d.setHours(d.getHours() - RECENCY_WINDOW_HOURS);
setRecencyCutoff(Timestamp.fromDate(d));
}, 60 * 60 * 1000);
return () => clearInterval(interval);
}, []);
const { children, isLoading } = useLiveParticleChildren(path, "last_child_created_at", "desc", visibilityScopes, undefined, undefined, where("last_child_created_at", ">=", recencyCutoff));
const navigate = useNavigate(); const navigate = useNavigate();
const streams = useMemo( const streams = useMemo(
+4
View File
@@ -12,6 +12,7 @@ import {
toFirestoreChildrenPath, toFirestoreChildrenPath,
} from "@/lib/particle-path"; } from "@/lib/particle-path";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { QueryFieldFilterConstraint } from "firebase/firestore";
interface UseLiveParticleResult { interface UseLiveParticleResult {
particle: Particle | null; particle: Particle | null;
@@ -62,6 +63,7 @@ export function useLiveParticleChildren(
visibilityScopes?: string[], visibilityScopes?: string[],
onAdded?: (child: Particle) => void, onAdded?: (child: Particle) => void,
onRemoved?: (child: Particle, updatedChildren: Particle[]) => void, onRemoved?: (child: Particle, updatedChildren: Particle[]) => void,
whereFilter?: QueryFieldFilterConstraint
): UseLiveParticleChildrenResult { ): UseLiveParticleChildrenResult {
const [children, setChildren] = useState<Particle[]>([]); const [children, setChildren] = useState<Particle[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@@ -89,9 +91,11 @@ export function useLiveParticleChildren(
orderDirection, orderDirection,
onAdded, onAdded,
onRemoved, onRemoved,
whereFilter,
); );
return unsubscribe; return unsubscribe;
// FIX: do we need to listen to more deps? Would that cause side effects that break behavior
}, [collectionPath]); }, [collectionPath]);
return { children, isLoading, error }; return { children, isLoading, error };
+10 -1
View File
@@ -1,9 +1,11 @@
import { useCallback, useEffect, useEffectEvent, useMemo, useReducer, useRef } from "react"; import { useCallback, useEffect, useEffectEvent, useMemo, useReducer, useRef, useState } from "react";
import { useAuthStore } from "@/stores/auth-store"; import { useAuthStore } from "@/stores/auth-store";
import type { Particle } from "@/api/types"; import type { Particle } from "@/api/types";
import { useLiveParticleChildren } from "@/hooks/use-particle"; import { useLiveParticleChildren } from "@/hooks/use-particle";
import { toFirestoreDocPath, type ParticlePath } from "@/lib/particle-path"; import { toFirestoreDocPath, type ParticlePath } from "@/lib/particle-path";
import { updateStreamPlaybackMarker } from "@/lib/firestore-particles"; import { updateStreamPlaybackMarker } from "@/lib/firestore-particles";
import { where, Timestamp } from "firebase/firestore";
import { RECENCY_WINDOW_HOURS } from "@/lib/constants";
// --- Playback reducer (ID-based) --- // --- Playback reducer (ID-based) ---
@@ -112,8 +114,15 @@ export function useStreamPlayback(
}); });
}); });
const [recencyCutoff] = useState(() => {
const d = new Date();
d.setHours(d.getHours() - RECENCY_WINDOW_HOURS);
return Timestamp.fromDate(d);
});
const { children } = useLiveParticleChildren( const { children } = useLiveParticleChildren(
path, "created_at", "asc", undefined, onParticleAdded, onParticleRemoved, path, "created_at", "asc", undefined, onParticleAdded, onParticleRemoved,
where("created_at", ">=", recencyCutoff),
); );
// Derive current index and particle from ID // Derive current index and particle from ID
+2
View File
@@ -0,0 +1,2 @@
/** How far back to look when filtering particles by recency. */
export const RECENCY_WINDOW_HOURS = 24;
+5
View File
@@ -16,6 +16,7 @@ import {
type QueryDocumentSnapshot, type QueryDocumentSnapshot,
type SnapshotOptions, type SnapshotOptions,
type Unsubscribe, type Unsubscribe,
QueryFieldFilterConstraint,
} from "firebase/firestore"; } from "firebase/firestore";
import { firestoreDb } from "@/firebase"; import { firestoreDb } from "@/firebase";
import { isContainerType, ParticleSchema } from "@/api/types"; import { isContainerType, ParticleSchema } from "@/api/types";
@@ -134,6 +135,7 @@ export function subscribeToParticleChildren(
orderDirection: "asc" | "desc" = "desc", orderDirection: "asc" | "desc" = "desc",
onAdded?: (child: Particle) => void, onAdded?: (child: Particle) => void,
onRemoved?: (child: Particle, updatedChildren: Particle[]) => void, onRemoved?: (child: Particle, updatedChildren: Particle[]) => void,
whereFilter?: QueryFieldFilterConstraint,
): Unsubscribe { ): Unsubscribe {
let q = query(typedCollection(collectionPath), orderBy(orderByField, orderDirection)); let q = query(typedCollection(collectionPath), orderBy(orderByField, orderDirection));
if (visibilityScopes.length > 0) { if (visibilityScopes.length > 0) {
@@ -142,6 +144,9 @@ export function subscribeToParticleChildren(
where("visible_to", "array-contains-any", visibilityScopes), where("visible_to", "array-contains-any", visibilityScopes),
); );
} }
if (whereFilter) {
q = query(q, whereFilter);
}
return onSnapshot( return onSnapshot(
q, q,
(snap) => { (snap) => {