paginate open streams (#252)

* paginate for open and closed streams

* fix: add platform abstraction for deeplink (#251)

* fix: add platform abstraction for deeplink

This was crashing App.tsx due to the deep link listener calling methods on window which do not exist.

* fix: apply CodeRabbit auto-fixes

Fixed 1 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>

* log error

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: CodeRabbit <noreply@coderabbit.ai>

* cleanup

* fix: import

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
This commit was merged in pull request #252.
This commit is contained in:
Arjun Patel
2026-06-09 10:43:27 -07:00
committed by GitHub
parent f38835ceee
commit 92894c4af8
2 changed files with 10 additions and 25 deletions
+1
View File
@@ -11,6 +11,7 @@ import NetworkSelector from '@/features/network-selector';
import NetworkRoot from '@/features/network-root'; import NetworkRoot from '@/features/network-root';
import ParticleViewResolver from '@/features/particles/particle-view-resolver'; import ParticleViewResolver from '@/features/particles/particle-view-resolver';
import Layout from '@/features/layout'; import Layout from '@/features/layout';
import { logError } from '@/lib/errors';
import NetworkSettingsPage from '@/features/network-settings'; import NetworkSettingsPage from '@/features/network-settings';
import { Toaster } from '@/components/ui/sonner'; import { Toaster } from '@/components/ui/sonner';
import { PusherProvider } from '@/lib/pusher-provider'; import { PusherProvider } from '@/lib/pusher-provider';
+9 -25
View File
@@ -10,8 +10,8 @@ export type StreamParticle = Particle & {
properties: StreamProperties; properties: StreamProperties;
}; };
const CLOSED_INITIAL_PAGE_SIZE = 50; const INITIAL_PAGE_SIZE = 12;
const CLOSED_PAGE_INCREMENT = 50; const PAGE_INCREMENT = 12;
// Stable where-constraint references so the Firestore subscription only // Stable where-constraint references so the Firestore subscription only
// re-attaches when the tab actually changes, not on every render. // re-attaches when the tab actually changes, not on every render.
@@ -28,11 +28,7 @@ function useVisibilityScopes(userId?: string, networkId?: string) {
} }
interface UseStreamParticlesOptions { interface UseStreamParticlesOptions {
/** // Which streams to subscribe to
* 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'; status: 'open' | 'closed';
} }
@@ -40,9 +36,9 @@ interface UseStreamParticlesResult {
streams: StreamParticle[]; streams: StreamParticle[];
isLoading: boolean; isLoading: boolean;
networkId: string; networkId: string;
/** True when more closed streams may exist beyond the current window. */ /** True when more streams may exist beyond the current window. */
canLoadMore: boolean; canLoadMore: boolean;
/** Extend the pagination window. No-op on the open tab. */ /** Extend the pagination window. */
loadMore: () => void; loadMore: () => void;
} }
@@ -54,21 +50,10 @@ export function useStreamParticles(
const user = useAuthStore((s) => s.user); const user = useAuthStore((s) => s.user);
const visibilityScopes = useVisibilityScopes(user?.id, networkId); const visibilityScopes = useVisibilityScopes(user?.id, networkId);
const [closedLimit, setClosedLimit] = useState(CLOSED_INITIAL_PAGE_SIZE); const [limit, setLimit] = useState(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 = const whereFilter: QueryFieldFilterConstraint =
status === 'open' ? OPEN_STATUS_FILTER : CLOSED_STATUS_FILTER; status === 'open' ? OPEN_STATUS_FILTER : CLOSED_STATUS_FILTER;
const limit = status === 'closed' ? closedLimit : undefined;
const { children, isLoading } = useLiveParticleChildren(path, { const { children, isLoading } = useLiveParticleChildren(path, {
orderByField: 'last_child_created_at', orderByField: 'last_child_created_at',
@@ -85,12 +70,11 @@ export function useStreamParticles(
// Heuristic: if we got back as many items as we asked for, assume there // 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. // might be more. Clicking load-more when there are no more is a no-op.
const canLoadMore = status === 'closed' && streams.length >= closedLimit; const canLoadMore = streams.length >= limit;
const loadMore = useCallback(() => { const loadMore = useCallback(() => {
if (status !== 'closed') return; setLimit((prev) => prev + PAGE_INCREMENT);
setClosedLimit((prev) => prev + CLOSED_PAGE_INCREMENT); }, []);
}, [status]);
return { streams, isLoading, networkId, canLoadMore, loadMore }; return { streams, isLoading, networkId, canLoadMore, loadMore };
} }