feat: add grid view for streams
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import beepSound from "../../assets/sound.wav";
|
||||
import type { Particle, StreamProperties } from "@/api/types";
|
||||
import { useAuthStore } from "@/stores/auth-store";
|
||||
import { useAutoplayStore } from "@/stores/autoplay-store";
|
||||
|
||||
/**
|
||||
* Triggers autoplay when a stream's latest child changes to a new media particle.
|
||||
* Plays a beep sound for new text particles from other users.
|
||||
*/
|
||||
export function useStreamAutoplay(
|
||||
latestChild: Particle | null,
|
||||
streamParticle: Particle & { type: "stream"; properties: StreamProperties },
|
||||
) {
|
||||
const userId = useAuthStore((s) => s.user?.id) ?? "";
|
||||
const settledIdRef = useRef<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (!latestChild) return;
|
||||
|
||||
// First real value: record as baseline, don't autoplay
|
||||
if (settledIdRef.current === undefined) {
|
||||
settledIdRef.current = latestChild.id;
|
||||
return;
|
||||
}
|
||||
|
||||
if (latestChild.id === settledIdRef.current) return;
|
||||
settledIdRef.current = latestChild.id;
|
||||
|
||||
if (latestChild.created_by_human_id === userId) return;
|
||||
|
||||
if (latestChild.type === "text") {
|
||||
new Audio(beepSound).play().catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
if (latestChild.type !== "media") return;
|
||||
|
||||
useAutoplayStore.getState().play(latestChild, streamParticle.id);
|
||||
}, [latestChild?.id]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useEffect, useMemo, useState } 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 { RECENCY_WINDOW_HOURS } from "@/lib/constants";
|
||||
|
||||
type StreamParticle = Particle & { type: "stream"; properties: StreamProperties };
|
||||
|
||||
function useVisibilityScopes(userId?: string, networkId?: string) {
|
||||
return useMemo(() => {
|
||||
const scopes: string[] = [];
|
||||
if (userId) scopes.push(`human:${userId}`);
|
||||
if (networkId) scopes.push(`network:${networkId}`);
|
||||
return scopes;
|
||||
}, [userId, networkId]);
|
||||
}
|
||||
|
||||
interface UseStreamParticlesResult {
|
||||
streams: StreamParticle[];
|
||||
isLoading: boolean;
|
||||
networkId: string;
|
||||
}
|
||||
|
||||
export function useStreamParticles(path: ParticlePath): UseStreamParticlesResult {
|
||||
const { networkId } = parseParticlePath(path);
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const visibilityScopes = useVisibilityScopes(user?.id, networkId);
|
||||
|
||||
const [recencyCutoff, setRecencyCutoff] = useState(() => {
|
||||
const d = new Date();
|
||||
d.setHours(d.getHours() - RECENCY_WINDOW_HOURS);
|
||||
return Timestamp.fromDate(d);
|
||||
});
|
||||
|
||||
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 streams = useMemo(
|
||||
() => children.filter((c): c is StreamParticle => c.type === "stream"),
|
||||
[children],
|
||||
);
|
||||
|
||||
return { streams, isLoading, networkId };
|
||||
}
|
||||
Reference in New Issue
Block a user