import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAuthStore } from '@/stores/auth-store'; import { apiClient } from '@/api/client'; import { isParticleDeleted, type Particle } from '@/api/types'; import { particlePath, toFirestoreDocPath } from '@/lib/particle-path'; import { AvatarGroup } from '@/components/ui/avatar'; import { HumanAvatar } from '@/components/human-avatar'; import { Tooltip, TooltipContent, TooltipTrigger, } from '@/components/ui/tooltip'; import { useNetwork } from '@/hooks/use-networks'; import { Button } from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Settings, CircleCheckBig, CircleDot, EllipsisVertical, Pencil, Lock, Globe, Trash2, } from 'lucide-react'; import { updateStreamStatus } from '@/lib/firestore-particles'; import { RenameStreamOverlay } from '@/features/particles/rename-stream-overlay'; import { DeleteParticleOverlay } from '@/features/particles/delete-particle-overlay'; import { StreamMembersOverlay } from '@/features/particles/stream-members-overlay'; import { parseVisibleTo } from '@/lib/stream-visibility'; import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, } from '@/components/ui/breadcrumb'; import { WindowControls } from '@/components/window-controls'; import { RelativeTimestamp } from '@/components/relative-timestamp'; import { useStreamPresence } from '@/features/particles/stream-presence-context'; import { resolveHumanDisplay } from '@/lib/humans'; import { platform } from '@/lib/platform'; import { requireDesktop } from '@/lib/platform/desktop-only'; function getParticleDisplayName(particle: Particle): string { switch (particle.type) { case 'stream': case 'folder': return particle.properties.name; case 'quest': return particle.properties.title; case 'paper': return particle.properties.title; case 'file': return particle.properties.filename; case 'text': return particle.properties.content.slice(0, 30); case 'media': return particle.type; } } interface TopBarProps { networkId: string; particle: Particle | null; streamParticle: Particle & { type: 'stream' }; } export function TopBar({ networkId, particle, streamParticle }: TopBarProps) { const navigate = useNavigate(); const network = useNetwork(networkId); const userId = useAuthStore((s) => s.user?.id); const isCreator = !!userId && userId === streamParticle.created_by_human_id; const [renameOpen, setRenameOpen] = useState(false); const [membersOpen, setMembersOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false); const canDeleteParticle = !!particle && !!userId && particle.created_by_human_id === userId && particle.type !== 'stream' && particle.type !== 'folder' && !isParticleDeleted(particle); const huddleParticipants = streamParticle.huddle_active_participants ?? []; const hasActiveHuddle = huddleParticipants.length > 0; const handleJoinHuddle = () => { if (!requireDesktop('Huddle')) return; apiClient .getLivekitToken(networkId, streamParticle.id) .then(({ token, server_url }) => { platform.huddle.open({ token, serverUrl: server_url }); }); }; return (
{streamParticle && ( <> {getParticleDisplayName(streamParticle)} )} {particle && ( <> )} {hasActiveHuddle && ( )} {streamParticle.status === 'closed' && ( Closed )} setMembersOpen(true)} /> { const docPath = toFirestoreDocPath( particlePath(networkId, [streamParticle.id]), ); await updateStreamStatus( docPath, streamParticle.status === 'open' ? 'closed' : 'open', ); }} > {streamParticle.status === 'open' ? ( <> Close stream ) : ( <> Open stream )} {isCreator && ( setRenameOpen(true)}> Rename stream )} {canDeleteParticle && ( setDeleteOpen(true)} variant="destructive" > Delete particle )} navigate('/settings')}> Settings {renameOpen && isCreator && ( setRenameOpen(false)} /> )} {deleteOpen && canDeleteParticle && particle && userId && ( setDeleteOpen(false)} /> )} {membersOpen && ( setMembersOpen(false)} /> )}
); } function MembersIndicator({ networkId, streamParticle, onClick, }: { networkId: string; streamParticle: Particle & { type: 'stream' }; onClick: () => void; }) { const network = useNetwork(networkId); const visibility = parseVisibleTo(streamParticle.visible_to, networkId); const userId = useAuthStore((s) => s.user?.id); const humans = network?.humans ?? []; const isNetworkWide = visibility.mode === 'network'; const allMemberIds = isNetworkWide ? humans.map((h) => h.id) : visibility.humanIds; const othersIds = isNetworkWide ? [] : allMemberIds.filter((id) => id != userId); const isPrivate = othersIds.length == 0; const shownMembers = othersIds .slice(0, 3) .map((id) => humans.find((h) => h.id === id)) .filter((h): h is NonNullable => !!h); const overflow = othersIds.length - shownMembers.length; return ( {isNetworkWide ? `Everyone in ${network?.name ?? 'network'}` : `${allMemberIds.length} ${allMemberIds.length === 1 ? 'member' : 'members'}`} ); } function ParticleBreadcrumbContent({ particle, networkId, }: { particle: Particle; networkId: string; }) { const network = useNetwork(networkId); const { onlineHumanIds } = useStreamPresence(); const display = resolveHumanDisplay( particle.created_by_human_id, network?.humans, ); const isOnline = particle.created_by_human_id ? onlineHumanIds.has(particle.created_by_human_id) : false; return ( {display.displayName} - ); }