Files
llink/js/mobile/src/hooks/use-particle.ts
T
Arjun PatelandGitHub e3461dd5cd mobile v0.1 with deployment for ios (#191)
* stage 1: project init

* stage 2: skeleton with navigation

* step 2.5: streams list

* step 4: stream playback experience

* step 5-6: compose experience

* fix: broken record

* transcode media particles to mp4

* build: reproducible go generate

* build: rename skaffold module for particle processor worker

* infra: increase particle processor worker resources

Was dealing with OOM errors

* tweaks to mobile

* log transcode work

* view on desktop placeholder

* tweak padding

* cap video resolution to save on memory

* infra: bump memory limits as insurance

* ux improvements

* update bundle id for mobile

* config for mobile
2026-04-29 17:39:11 -07:00

187 lines
4.7 KiB
TypeScript

import { useState, useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import type { QueryFieldFilterConstraint } from "firebase/firestore";
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 { logError } from "@/lib/errors";
interface UseLiveParticleResult {
particle: Particle | null;
isLoading: boolean;
error: Error | null;
}
export function useLiveParticle(path: ParticlePath): UseLiveParticleResult {
const [particle, setParticle] = useState<Particle | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(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;
/** Optional cap on results. Changes trigger a re-subscription. */
limit?: number;
}
export function useLiveParticleChildren(
path: ParticlePath | undefined,
{
orderByField = "created_at",
orderDirection = "desc",
visibilityScopes,
onAdded,
onRemoved,
whereFilter,
limit,
}: UseLiveParticleChildrenParams = {},
): UseLiveParticleChildrenResult {
const [children, setChildren] = useState<Particle[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
if (!path) {
setChildren([]);
setIsLoading(false);
return;
}
setIsLoading(true);
setError(null);
setChildren([]);
const collectionPath = toFirestoreChildrenPath(path);
const unsubscribe = subscribeToParticleChildren(collectionPath, {
onData: (data) => {
setChildren(data);
setIsLoading(false);
},
onError: (err) => {
logError(err, { scope: "firestore.particle-children", path });
setError(err);
setIsLoading(false);
},
visibilityScopes,
orderByField,
orderDirection,
onAdded,
onRemoved,
whereFilter,
limit,
});
return unsubscribe;
// The hook intentionally keys only on path/whereFilter/limit — desktop
// does the same. Visibility scope changes are absorbed by the active
// listener; reordering causes a re-subscription.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [path, whereFilter, limit]);
return { children, isLoading, error };
}
interface UseLiveLatestChildResult {
latestChild: Particle | null;
isLoading: boolean;
}
export function useLiveLatestChild(
path: ParticlePath,
): UseLiveLatestChildResult {
const [latestChild, setLatestChild] = useState<Particle | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setIsLoading(true);
setLatestChild(null);
const unsubscribe = subscribeToLatestChild(
toFirestoreChildrenPath(path),
(data) => {
setLatestChild(data);
setIsLoading(false);
},
(err) => {
logError(err, { scope: "firestore.latest-child", path });
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
});
}