import { useCallback, useEffect, useState } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { CircleCheckBig, CircleDot, FolderIcon } from 'lucide-react'; import type { Particle } from '@/api/types'; import { parseParticlePath, type ParticlePath } from '@/lib/particle-path'; import { useAuthStore } from '@/stores/auth-store'; import { useContainerChildren } from '@/hooks/use-container-children'; import { useListKeyboardNav } from '@/hooks/use-list-keyboard-nav'; import { useCreateParticle, useCreateStreamParticle, } from '@/hooks/use-create-particle'; import { ParticleChildrenList } from '@/features/particles/particle-children-list'; import { ComposeOverlay } from '@/features/compose/compose-overlay'; import { ComposeQuotaIndicator } from '@/features/compose/compose-quota-indicator'; import { ConfigureContainerStep } from '@/features/compose/configure-container-step'; import { useComposeIntentStore } from '@/stores/compose-intent-store'; import { VideoAudioToggle } from '@/components/video-audio-toggle'; import { KeyHint } from '@/components/key-hint'; import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; type ContainerKind = 'stream' | 'folder'; interface ContainerViewProps { /** Path of the container; network root when it has no segments. */ path: ParticlePath; /** Present when the container is a folder particle (drives the header). */ folderParticle?: Particle & { type: 'folder' }; } /** * Browsable view of a container's children — used for both the network root * and folders, which share the same structure: a mixed-type child list, the * compose overlay, and one keyboard grammar. The root is just a folder * without a doc. */ export function ContainerView({ path, folderParticle }: ContainerViewProps) { const { networkId, segments } = parseParticlePath(path); const isRoot = segments.length === 0; const navigate = useNavigate(); const userId = useAuthStore((s) => s.user?.id); const [composeActive, setComposeActive] = useState(false); const [creating, setCreating] = useState(null); 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 }, ); }; // Folders are shelved for now: the root lists streams only, split by // open/closed status (server-side filter, like before folders). Folder // containers keep the mixed-type child list so the recursive container // model can be revived later. const { items, isLoading, canLoadMore, loadMore } = useContainerChildren( path, { streamStatus: isRoot ? statusTab : undefined }, ); const createParticle = useCreateParticle(); const createStream = useCreateStreamParticle(); const handleOpen = useCallback( (particleId: string) => { navigate(`/${networkId}/${[...segments, particleId].join('/')}`); }, [navigate, networkId, segments], ); const { selectedIndex } = useListKeyboardNav({ items, enabled: !composeActive && creating === null, onOpen: handleOpen, }); // N creates a stream inside a folder. Root stream creation goes through the // compose flow. useEffect(() => { if (isRoot || composeActive || creating !== null) return; const handleKeyDown = (e: KeyboardEvent) => { if (e.defaultPrevented) return; const target = e.target as HTMLElement; if ( target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable ) { return; } if (e.key === 'n' || e.key === 'N') { e.preventDefault(); setCreating('stream'); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [isRoot, composeActive, creating]); const handleCreateContainer = useCallback( async (name: string, visibleTo: string[]) => { if (!userId || !creating) return; const kind = creating; setCreating(null); const id = kind === 'folder' ? await createParticle.mutateAsync({ path, type: 'folder', properties: { name }, createdByHumanId: userId, visibleTo, }) : await createStream.mutateAsync({ networkId, parentPath: path, properties: { name }, createdByHumanId: userId, visibleTo, }); handleOpen(id); }, [ userId, creating, path, networkId, createParticle, createStream, handleOpen, ], ); return (
{isRoot && (
setStatusTab(v === 'closed' ? 'closed' : 'open') } > Open Closed
)} {folderParticle && (

{folderParticle.properties.name}

)}
{!composeActive && (
)}
setCreating('stream')} />
{creating && ( setCreating(null)} onSubmit={handleCreateContainer} /> )}
); } function ContainerControls({ isRoot, onCreateStream, }: { isRoot: boolean; onCreateStream: () => void; }) { const requestIntent = useComposeIntentStore((s) => s.request); return (
navigate jump requestIntent('record')} title="Start recording (or hold `)" > to start requestIntent('text')} title="Compose text (or press T)" > text requestIntent('task')} title="Create a task (or press D)" > task {!isRoot && ( stream )}
); }