import { Fragment, useMemo, useRef, useEffect, memo, createElement, } from 'react'; import { Headphones, Radio, FolderIcon } from 'lucide-react'; import { cn, getInitials } from '@/lib/utils'; import { useLiveLatestChild } from '@/hooks/use-particle'; import { useAuthStore } from '@/stores/auth-store'; import { particlePath } from '@/lib/particle-path'; import { resolveHumanDisplay } from '@/lib/humans'; import { RelativeTimestamp } from '@/components/relative-timestamp'; import { HumanAvatar } from '@/components/human-avatar'; import { Separator } from '@/components/ui/separator'; import { Progress } from '@/components/ui/progress'; import { Small } from '@/components/ui/typography'; import { Button } from '@/components/ui/button'; import { isParticleDeleted, type Particle, type StreamProperties, } from '@/api/types'; import { useNetwork } from '@/hooks/use-networks'; import { useStreamAutoplay } from '@/hooks/use-stream-autoplay'; import { useDownloadUrl } from '@/hooks/use-download-url'; import { getMessagePreview, getParticleTypeIcon } from '@/lib/particle-display'; import { StreamContextMenu } from '@/features/particles/stream-context-menu'; type StreamParticle = Particle & { type: 'stream'; properties: StreamProperties; }; function VideoThumbnail({ objectId, isUnseen, }: { objectId: string; isUnseen: boolean; }) { const { data: url } = useDownloadUrl(objectId); return (
{url && (
); } const StreamRow = memo(function StreamRow({ particle, networkId, onOpen, isSelected, shortcutKey, }: { particle: StreamParticle; networkId: string; onOpen: (particleId: string) => void; isSelected?: boolean; shortcutKey?: number; }) { const streamPath = particlePath(networkId, [particle.id]); const { latestChild } = useLiveLatestChild(streamPath); const user = useAuthStore((s) => s.user); const userId = user?.id ?? ''; const network = useNetwork(networkId); useStreamAutoplay(latestChild, particle, networkId, network ?? undefined); const hasActiveHuddle = particle.huddle_active_participants && particle.huddle_active_participants.length > 0; const huddleCount = particle.huddle_active_participants?.length ?? 0; const isDM = particle.visible_to.length === 2 && particle.visible_to.every((v) => v.startsWith('human:')); const avatar = useMemo(() => { if (isDM) { const otherEntry = particle.visible_to.find( (v) => v !== `human:${userId}`, ); if (otherEntry) { const otherId = otherEntry.replace('human:', ''); const otherHuman = network?.humans?.find((h) => h.id === otherId); if (otherHuman) { return { initials: getInitials(otherHuman.email), avatarObjectId: otherHuman.avatar_object_id ?? null, }; } } } if (latestChild) { const creator = network?.humans?.find( (h) => h.id === latestChild.created_by_human_id, ); if (creator) { return { initials: getInitials(creator.email), avatarObjectId: creator.avatar_object_id ?? null, }; } } return { initials: particle.properties.name.slice(0, 2).toUpperCase(), avatarObjectId: null, }; }, [ isDM, particle.visible_to, particle.properties.name, userId, latestChild, network, ]); const isUnseen = useMemo(() => { if (!latestChild) return false; const latestChildTimestamp = latestChild.created_at.getTime(); const userPlaybackPosition = particle.playback_markers?.[userId]?.getTime() ?? 0; return latestChildTimestamp > userPlaybackPosition; }, [latestChild, particle.playback_markers, userId]); const senderPrefix = useMemo(() => { if (!latestChild) return null; const isCurrentUser = latestChild.created_by_human_id === userId; if (isDM) { return isCurrentUser ? 'You: ' : null; } // Group stream if (isCurrentUser) return 'You: '; const { displayName } = resolveHumanDisplay( latestChild.created_by_human_id, network?.humans, ); const capitalized = displayName.charAt(0).toUpperCase() + displayName.slice(1); return `${capitalized}: `; }, [latestChild, userId, isDM, network]); const subtitle = latestChild ? getMessagePreview(latestChild) : particle.properties.name; // Rendered via createElement below: a call-result used directly as a JSX tag // is flagged as a dynamically-created component. const typeIcon = latestChild ? getParticleTypeIcon(latestChild) : Radio; const videoThumbObjectId = latestChild && !isParticleDeleted(latestChild) && latestChild.type === 'media' && latestChild.properties.mime_type.startsWith('video/') ? latestChild.properties.object_id : null; return (
onOpen(particle.id)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') onOpen(particle.id); }} className={cn( 'flex w-full items-center gap-3 px-4 py-3 text-left cursor-pointer transition-colors hover:bg-accent', isSelected && 'bg-accent', hasActiveHuddle && 'bg-gradient-to-r from-red-500/10 to-transparent', )} > {shortcutKey && ( {shortcutKey} )} {videoThumbObjectId ? ( ) : ( )}

{particle.properties.name}

{hasActiveHuddle && ( {huddleCount} )} {latestChild && ( )}
{createElement(typeIcon, { className: cn( 'size-3.5 shrink-0', isUnseen ? 'text-foreground' : 'text-muted-foreground', ), })} {senderPrefix && ( {senderPrefix} )} {subtitle}
{isUnseen && }
); }); const FolderRow = memo(function FolderRow({ particle, networkId, onOpen, isSelected, shortcutKey, }: { particle: Particle & { type: 'folder' }; networkId: string; onOpen: (particleId: string) => void; isSelected?: boolean; shortcutKey?: number; }) { const network = useNetwork(networkId); const creator = resolveHumanDisplay( particle.created_by_human_id, network?.humans, ); return (
onOpen(particle.id)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') onOpen(particle.id); }} className={cn( 'flex w-full items-center gap-3 px-4 py-3 text-left cursor-pointer transition-colors hover:bg-accent', isSelected && 'bg-accent', )} > {shortcutKey && ( {shortcutKey} )}

{particle.properties.name}

Folder · {creator.displayName}
); }); const LeafRow = memo(function LeafRow({ particle, networkId, onOpen, isSelected, shortcutKey, }: { particle: Particle; networkId: string; onOpen: (particleId: string) => void; isSelected?: boolean; shortcutKey?: number; }) { const network = useNetwork(networkId); const creator = resolveHumanDisplay( particle.created_by_human_id, network?.humans, ); const typeIcon = getParticleTypeIcon(particle); const deleted = isParticleDeleted(particle); const taskDone = particle.type === 'task' && particle.properties.done; return (
onOpen(particle.id)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') onOpen(particle.id); }} className={cn( 'flex w-full items-center gap-3 px-4 py-3 text-left cursor-pointer transition-colors hover:bg-accent', isSelected && 'bg-accent', )} > {shortcutKey && ( {shortcutKey} )} {createElement(typeIcon, { className: cn( 'size-4 shrink-0 text-muted-foreground', taskDone && 'text-emerald-500', ), })}

{getMessagePreview(particle)}

{creator.displayName}
); }); interface ParticleChildrenListProps { items: Particle[]; networkId: string; isLoading: boolean; /** Open (navigate into / select) a particle by id. */ onOpen: (particleId: string) => void; selectedIndex?: number | null; /** Render 1–9 shortcut badges next to the first nine rows. */ showShortcuts?: boolean; emptyMessage?: string; /** When true, render a footer that invokes onLoadMore. */ canLoadMore?: boolean; onLoadMore?: () => void; } /** * Browsable list of a container's children, any particle type. Used by the * network root and folder views. */ export function ParticleChildrenList({ items, networkId, isLoading, onOpen, selectedIndex, showShortcuts = true, emptyMessage = 'Nothing here yet. Create something using the keyboard shortcuts below.', canLoadMore, onLoadMore, }: ParticleChildrenListProps) { const rowRefs = useRef>([]); useEffect(() => { if ( selectedIndex !== null && selectedIndex !== undefined && selectedIndex >= 0 ) { rowRefs.current[selectedIndex]?.scrollIntoView({ block: 'nearest' }); } }, [selectedIndex]); if (isLoading) { return ; } if (items.length === 0) { return (

{emptyMessage}

); } return (
{items.map((item, index) => { const rowProps = { networkId, onOpen, isSelected: index === selectedIndex, shortcutKey: showShortcuts && index < 9 ? index + 1 : undefined, }; const row = (
{ rowRefs.current[index] = el; }} > {item.type === 'stream' ? ( ) : item.type === 'folder' ? ( ) : ( )} {index < items.length - 1 && }
); return item.type === 'stream' ? ( {row} ) : ( {row} ); })} {canLoadMore && onLoadMore && (
)}
); }