From fcdacbcdf65ffa4166360451b1bc1fa89d1e372d Mon Sep 17 00:00:00 2001 From: Arjun Patel Date: Tue, 9 Jun 2026 09:44:10 -0700 Subject: [PATCH 1/5] paginate for open and closed streams --- js/desktop/src/hooks/use-stream-particles.ts | 25 ++++++-------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/js/desktop/src/hooks/use-stream-particles.ts b/js/desktop/src/hooks/use-stream-particles.ts index 41bcae1..b7db180 100644 --- a/js/desktop/src/hooks/use-stream-particles.ts +++ b/js/desktop/src/hooks/use-stream-particles.ts @@ -10,8 +10,8 @@ export type StreamParticle = Particle & { properties: StreamProperties; }; -const CLOSED_INITIAL_PAGE_SIZE = 50; -const CLOSED_PAGE_INCREMENT = 50; +const INITIAL_PAGE_SIZE = 3; +const PAGE_INCREMENT = 3; // Stable where-constraint references so the Firestore subscription only // re-attaches when the tab actually changes, not on every render. @@ -33,6 +33,7 @@ interface UseStreamParticlesOptions { * by active work — full realtime coverage is needed for autoplay/huddles). * Closed streams are paginated via `loadMore`. */ + // TODO: can't we paginate open streams, and if the top set changes then our auto-play would still trigger? status: 'open' | 'closed'; } @@ -54,21 +55,10 @@ export function useStreamParticles( 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 [limit, setLimit] = useState(INITIAL_PAGE_SIZE); const whereFilter: QueryFieldFilterConstraint = status === 'open' ? OPEN_STATUS_FILTER : CLOSED_STATUS_FILTER; - const limit = status === 'closed' ? closedLimit : undefined; const { children, isLoading } = useLiveParticleChildren(path, { orderByField: 'last_child_created_at', @@ -85,12 +75,11 @@ export function useStreamParticles( // 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 canLoadMore = streams.length >= limit; const loadMore = useCallback(() => { - if (status !== 'closed') return; - setClosedLimit((prev) => prev + CLOSED_PAGE_INCREMENT); - }, [status]); + setLimit((prev) => prev + PAGE_INCREMENT); + }, []); return { streams, isLoading, networkId, canLoadMore, loadMore }; } -- 2.54.0 From f38835ceeefe5bd1afebe4d063140b0cd4807142 Mon Sep 17 00:00:00 2001 From: Arjun Patel Date: Tue, 9 Jun 2026 10:01:22 -0700 Subject: [PATCH 2/5] 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 * log error --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: CodeRabbit --- js/desktop/src/App.tsx | 14 ++++++++++---- js/desktop/src/features/network-root.tsx | 5 +---- js/desktop/src/lib/platform/electron.ts | 5 +++++ js/desktop/src/lib/platform/types.ts | 5 +++++ js/desktop/src/lib/platform/web.ts | 7 +++++++ 5 files changed, 28 insertions(+), 8 deletions(-) diff --git a/js/desktop/src/App.tsx b/js/desktop/src/App.tsx index 5a8da62..8a84a3b 100644 --- a/js/desktop/src/App.tsx +++ b/js/desktop/src/App.tsx @@ -56,10 +56,16 @@ function DeepLinkNavigationListener() { const navigate = useNavigate(); useEffect(() => { - window.electronDeepLink.getPending().then((path) => { - if (path) navigate(path); - }); - return window.electronDeepLink.onNavigate((path) => navigate(path)); + platform.deepLink + .getPending() + .then((path) => { + if (path) navigate(path); + }) + .catch((err) => { + logError(err, { scope: 'deepLink.getPending' }); + }); + + return platform.deepLink.onNavigate((path) => navigate(path)); }, [navigate]); return null; diff --git a/js/desktop/src/features/network-root.tsx b/js/desktop/src/features/network-root.tsx index 17d0f00..5b6556a 100644 --- a/js/desktop/src/features/network-root.tsx +++ b/js/desktop/src/features/network-root.tsx @@ -30,8 +30,7 @@ export default function NetworkRoot() { setSearchParams( (prev) => { const params = new URLSearchParams(prev); - if (next === 'open') params.delete('status'); - else params.set('status', next); + params.set('status', next); return params; }, { replace: true }, @@ -56,7 +55,6 @@ export default function NetworkRoot() { return (
- {/* Top bar — stays in place */}
- {/* Scrollable content */}
window.electronApp.setDockBadge(count), getVersion: () => window.electronApp.getVersion(), }, + + deepLink: { + getPending: () => window.electronDeepLink.getPending(), + onNavigate: (cb) => window.electronDeepLink.onNavigate(cb), + }, }; diff --git a/js/desktop/src/lib/platform/types.ts b/js/desktop/src/lib/platform/types.ts index d5874ba..4e21f13 100644 --- a/js/desktop/src/lib/platform/types.ts +++ b/js/desktop/src/lib/platform/types.ts @@ -60,6 +60,11 @@ export interface Platform { setDockBadge: (count: number) => void; getVersion: () => Promise; }; + + deepLink: { + getPending: () => Promise; + onNavigate: (callback: (path: string) => void) => () => void; + }; } export const DESKTOP_DOWNLOAD_URL = 'https://flowylabs.ai/llink/download'; diff --git a/js/desktop/src/lib/platform/web.ts b/js/desktop/src/lib/platform/web.ts index b02a978..11e46b5 100644 --- a/js/desktop/src/lib/platform/web.ts +++ b/js/desktop/src/lib/platform/web.ts @@ -117,4 +117,11 @@ export const webPlatform: Platform = { setDockBadge: applyDockBadge, getVersion: async () => __APP_VERSION__, }, + + deepLink: { + getPending: () => Promise.resolve(null), + onNavigate: (_) => { + return () => {}; + }, + }, }; -- 2.54.0 From 38cecd84bdf1ec0e350ff343f1996a3dc2b7592b Mon Sep 17 00:00:00 2001 From: Arjun Patel Date: Tue, 9 Jun 2026 10:01:22 -0700 Subject: [PATCH 3/5] 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 * log error --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: CodeRabbit --- js/desktop/src/App.tsx | 14 ++++++++++---- js/desktop/src/features/network-root.tsx | 5 +---- js/desktop/src/lib/platform/electron.ts | 5 +++++ js/desktop/src/lib/platform/types.ts | 5 +++++ js/desktop/src/lib/platform/web.ts | 7 +++++++ 5 files changed, 28 insertions(+), 8 deletions(-) diff --git a/js/desktop/src/App.tsx b/js/desktop/src/App.tsx index 5a8da62..8a84a3b 100644 --- a/js/desktop/src/App.tsx +++ b/js/desktop/src/App.tsx @@ -56,10 +56,16 @@ function DeepLinkNavigationListener() { const navigate = useNavigate(); useEffect(() => { - window.electronDeepLink.getPending().then((path) => { - if (path) navigate(path); - }); - return window.electronDeepLink.onNavigate((path) => navigate(path)); + platform.deepLink + .getPending() + .then((path) => { + if (path) navigate(path); + }) + .catch((err) => { + logError(err, { scope: 'deepLink.getPending' }); + }); + + return platform.deepLink.onNavigate((path) => navigate(path)); }, [navigate]); return null; diff --git a/js/desktop/src/features/network-root.tsx b/js/desktop/src/features/network-root.tsx index 17d0f00..5b6556a 100644 --- a/js/desktop/src/features/network-root.tsx +++ b/js/desktop/src/features/network-root.tsx @@ -30,8 +30,7 @@ export default function NetworkRoot() { setSearchParams( (prev) => { const params = new URLSearchParams(prev); - if (next === 'open') params.delete('status'); - else params.set('status', next); + params.set('status', next); return params; }, { replace: true }, @@ -56,7 +55,6 @@ export default function NetworkRoot() { return (
- {/* Top bar — stays in place */}
- {/* Scrollable content */}
window.electronApp.setDockBadge(count), getVersion: () => window.electronApp.getVersion(), }, + + deepLink: { + getPending: () => window.electronDeepLink.getPending(), + onNavigate: (cb) => window.electronDeepLink.onNavigate(cb), + }, }; diff --git a/js/desktop/src/lib/platform/types.ts b/js/desktop/src/lib/platform/types.ts index d5874ba..4e21f13 100644 --- a/js/desktop/src/lib/platform/types.ts +++ b/js/desktop/src/lib/platform/types.ts @@ -60,6 +60,11 @@ export interface Platform { setDockBadge: (count: number) => void; getVersion: () => Promise; }; + + deepLink: { + getPending: () => Promise; + onNavigate: (callback: (path: string) => void) => () => void; + }; } export const DESKTOP_DOWNLOAD_URL = 'https://flowylabs.ai/llink/download'; diff --git a/js/desktop/src/lib/platform/web.ts b/js/desktop/src/lib/platform/web.ts index b02a978..11e46b5 100644 --- a/js/desktop/src/lib/platform/web.ts +++ b/js/desktop/src/lib/platform/web.ts @@ -117,4 +117,11 @@ export const webPlatform: Platform = { setDockBadge: applyDockBadge, getVersion: async () => __APP_VERSION__, }, + + deepLink: { + getPending: () => Promise.resolve(null), + onNavigate: (_) => { + return () => {}; + }, + }, }; -- 2.54.0 From 0604b260d21f5b9dc539e3400d7aceb97e8940e7 Mon Sep 17 00:00:00 2001 From: Arjun Patel Date: Tue, 9 Jun 2026 10:10:16 -0700 Subject: [PATCH 4/5] cleanup --- js/desktop/src/hooks/use-stream-particles.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/js/desktop/src/hooks/use-stream-particles.ts b/js/desktop/src/hooks/use-stream-particles.ts index b7db180..7be9ff6 100644 --- a/js/desktop/src/hooks/use-stream-particles.ts +++ b/js/desktop/src/hooks/use-stream-particles.ts @@ -10,8 +10,8 @@ export type StreamParticle = Particle & { properties: StreamProperties; }; -const INITIAL_PAGE_SIZE = 3; -const PAGE_INCREMENT = 3; +const INITIAL_PAGE_SIZE = 12; +const PAGE_INCREMENT = 12; // Stable where-constraint references so the Firestore subscription only // re-attaches when the tab actually changes, not on every render. @@ -28,12 +28,7 @@ function useVisibilityScopes(userId?: string, networkId?: string) { } 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`. - */ - // TODO: can't we paginate open streams, and if the top set changes then our auto-play would still trigger? + // Which streams to subscribe to status: 'open' | 'closed'; } @@ -41,9 +36,9 @@ interface UseStreamParticlesResult { streams: StreamParticle[]; isLoading: boolean; 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; - /** Extend the pagination window. No-op on the open tab. */ + /** Extend the pagination window. */ loadMore: () => void; } -- 2.54.0 From 89d78246d3cb7d515aefecfff3c895915d2885b9 Mon Sep 17 00:00:00 2001 From: Arjun Patel Date: Tue, 9 Jun 2026 10:41:21 -0700 Subject: [PATCH 5/5] fix: import --- js/desktop/src/App.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/js/desktop/src/App.tsx b/js/desktop/src/App.tsx index 8a84a3b..2858f50 100644 --- a/js/desktop/src/App.tsx +++ b/js/desktop/src/App.tsx @@ -11,6 +11,7 @@ import NetworkSelector from '@/features/network-selector'; import NetworkRoot from '@/features/network-root'; import ParticleViewResolver from '@/features/particles/particle-view-resolver'; import Layout from '@/features/layout'; +import { logError } from '@/lib/errors'; import NetworkSettingsPage from '@/features/network-settings'; import { Toaster } from '@/components/ui/sonner'; import { PusherProvider } from '@/lib/pusher-provider'; -- 2.54.0