* wip * wip * wip * format * wip(mobile): lint and format * cleanup * nits * nits * idiomatic react * nit * format all root files
98 lines
3.3 KiB
TypeScript
98 lines
3.3 KiB
TypeScript
import { useCallback, useMemo, useState } from 'react';
|
|
import { where, type QueryFieldFilterConstraint } from 'firebase/firestore';
|
|
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';
|
|
|
|
export type StreamParticle = Particle & {
|
|
type: 'stream';
|
|
properties: StreamProperties;
|
|
};
|
|
|
|
const CLOSED_INITIAL_PAGE_SIZE = 50;
|
|
const CLOSED_PAGE_INCREMENT = 50;
|
|
|
|
// Stable where-constraint references so the Firestore subscription only
|
|
// re-attaches when the tab actually changes, not on every render.
|
|
const OPEN_STATUS_FILTER = where('status', '==', 'open');
|
|
const CLOSED_STATUS_FILTER = where('status', '==', 'closed');
|
|
|
|
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 UseStreamParticlesOptions {
|
|
/**
|
|
* Which streams to subscribe to. Open streams are loaded in full (bounded
|
|
* by active work — full realtime coverage is needed for autoplay/huddles).
|
|
* Closed streams are paginated via `loadMore`.
|
|
*/
|
|
status: 'open' | 'closed';
|
|
}
|
|
|
|
interface UseStreamParticlesResult {
|
|
streams: StreamParticle[];
|
|
isLoading: boolean;
|
|
error: Error | null;
|
|
networkId: string;
|
|
/** True when more closed streams may exist beyond the current window. */
|
|
canLoadMore: boolean;
|
|
/** Extend the pagination window. No-op on the open tab. */
|
|
loadMore: () => void;
|
|
}
|
|
|
|
export function useStreamParticles(
|
|
path: ParticlePath,
|
|
{ status }: UseStreamParticlesOptions,
|
|
): UseStreamParticlesResult {
|
|
const { networkId } = parseParticlePath(path);
|
|
const user = useAuthStore((s) => s.user);
|
|
const visibilityScopes = useVisibilityScopes(user?.id, networkId);
|
|
|
|
const [closedLimit, setClosedLimit] = useState(CLOSED_INITIAL_PAGE_SIZE);
|
|
const [prevStatus, setPrevStatus] = useState(status);
|
|
|
|
// Switching back to the closed tab starts a fresh window, avoiding an
|
|
// ever-growing subscription across a long session.
|
|
if (status !== prevStatus) {
|
|
setPrevStatus(status);
|
|
if (status === 'closed') {
|
|
setClosedLimit(CLOSED_INITIAL_PAGE_SIZE);
|
|
}
|
|
}
|
|
|
|
const whereFilter: QueryFieldFilterConstraint =
|
|
status === 'open' ? OPEN_STATUS_FILTER : CLOSED_STATUS_FILTER;
|
|
const limit = status === 'closed' ? closedLimit : undefined;
|
|
|
|
const { children, isLoading, error } = useLiveParticleChildren(path, {
|
|
orderByField: 'last_child_created_at',
|
|
orderDirection: 'desc',
|
|
visibilityScopes,
|
|
whereFilter,
|
|
limit,
|
|
});
|
|
|
|
const streams = useMemo(
|
|
() => children.filter((c): c is StreamParticle => c.type === 'stream'),
|
|
[children],
|
|
);
|
|
|
|
// Heuristic: if we got back as many items as we asked for, assume there
|
|
// might be more. Clicking load-more when there are no more is a no-op.
|
|
const canLoadMore = status === 'closed' && streams.length >= closedLimit;
|
|
|
|
const loadMore = useCallback(() => {
|
|
if (status !== 'closed') return;
|
|
setClosedLimit((prev) => prev + CLOSED_PAGE_INCREMENT);
|
|
}, [status]);
|
|
|
|
return { streams, isLoading, error, networkId, canLoadMore, loadMore };
|
|
}
|