feat: stream list view and tasks (#279)

* first attempt at stream sidebar, tasks, and events

* fix folder from root

* cleanup folders and events, and condense changes

* cleanup and add toggle for sidebar

* cleanup

* fix nits
This commit was merged in pull request #279.
This commit is contained in:
Arjun Patel
2026-06-12 12:26:49 -07:00
committed by GitHub
parent a358774106
commit 095b9876f9
37 changed files with 2384 additions and 791 deletions
+5 -116
View File
@@ -1,126 +1,15 @@
import { useCallback, useState } from 'react';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { CircleDot, CircleCheckBig } from 'lucide-react';
import { useParams } from 'react-router-dom';
import { particlePath } from '@/lib/particle-path';
import { ParticleListView } from '@/features/particles/particle-list-view';
import { VideoAudioToggle } from '@/components/video-audio-toggle';
import { KeyHint } from '@/components/key-hint';
import { ComposeOverlay } from './compose/compose-overlay';
import { useComposeIntentStore } from '@/stores/compose-intent-store';
import { ComposeQuotaIndicator } from './compose/compose-quota-indicator';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useStreamParticles } from '@/hooks/use-stream-particles';
import { useStreamKeyboardNav } from '@/hooks/use-stream-keyboard-nav';
import { ContainerView } from '@/features/particles/container-view';
/**
* Route-level component for /:networkId (index).
* Shows root-level particles for the selected network.
* Route-level component for /:networkId (index). The network root behaves
* like a folder: a container of streams, folders, and loose particles.
*/
export default function NetworkRoot() {
const { networkId } = useParams();
if (!networkId)
throw new Error('NetworkRoot requires a :networkId route param');
const navigate = useNavigate();
const path = particlePath(networkId, []);
const [composeActive, setComposeActive] = useState(false);
const [searchParams, setSearchParams] = useSearchParams();
const statusTab: 'open' | 'closed' =
searchParams.get('status') === 'closed' ? 'closed' : 'open';
const setStatusTab = (next: 'open' | 'closed') => {
setSearchParams(
(prev) => {
const params = new URLSearchParams(prev);
params.set('status', next);
return params;
},
{ replace: true },
);
};
const { streams, isLoading, canLoadMore, loadMore } = useStreamParticles(
path,
{
status: statusTab,
},
);
const { selectedIndex } = useStreamKeyboardNav({
streams,
enabled: !composeActive,
onNavigate: useCallback(
(streamId: string) => navigate(`/${networkId}/${streamId}`),
[navigate, networkId],
),
});
return (
<div className="relative flex min-h-0 flex-1 flex-col">
<div className="flex shrink-0 items-center p-1 border-b">
<Tabs
value={statusTab}
onValueChange={(v) =>
setStatusTab(v === 'closed' ? 'closed' : 'open')
}
>
<TabsList>
<TabsTrigger value="open">
<CircleDot className="size-3 text-green-500" /> Open
</TabsTrigger>
<TabsTrigger value="closed">
<CircleCheckBig className="size-3" /> Closed
</TabsTrigger>
</TabsList>
</Tabs>
</div>
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain pb-14 py-2">
<ParticleListView
streams={streams}
networkId={networkId}
isLoading={isLoading}
selectedIndex={selectedIndex}
canLoadMore={canLoadMore}
onLoadMore={loadMore}
/>
</div>
<ComposeOverlay networkId={networkId} onActiveChange={setComposeActive} />
{!composeActive && (
<div className="pointer-events-none absolute inset-x-0 bottom-16 z-20 flex justify-center px-3">
<ComposeQuotaIndicator networkId={networkId} />
</div>
)}
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-20 flex justify-center p-3">
<div className="pointer-events-auto">
<NetworkRootControls />
</div>
</div>
</div>
);
}
function NetworkRootControls() {
const requestIntent = useComposeIntentStore((s) => s.request);
return (
<div className="flex items-center gap-4 text-sm text-white/50">
<KeyHint keys={['↑↓', 'Enter']}>navigate</KeyHint>
<KeyHint keys="19">jump</KeyHint>
<VideoAudioToggle />
<KeyHint
keys="Hold `"
onClick={() => requestIntent('record')}
title="Start recording (or hold `)"
>
to start
</KeyHint>
<KeyHint
keys="T"
onClick={() => requestIntent('text')}
title="Compose text (or press T)"
>
text
</KeyHint>
</div>
);
return <ContainerView path={particlePath(networkId, [])} />;
}