feat: filter to ephemeral streams and particles
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/** How far back to look when filtering particles by recency. */
|
||||||
|
export const RECENCY_WINDOW_HOURS = 24;
|
||||||
@@ -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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user