127 lines
4.1 KiB
TypeScript
127 lines
4.1 KiB
TypeScript
import { useCallback, useState } from 'react';
|
||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||
import { CircleDot, CircleCheckBig } from 'lucide-react';
|
||
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';
|
||
|
||
/**
|
||
* Route-level component for /:networkId (index).
|
||
* Shows root-level particles for the selected network.
|
||
*/
|
||
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="1–9">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>
|
||
);
|
||
}
|