import { useCallback, useEffect, useMemo } from 'react'; import { createPortal } from 'react-dom'; import { X, UserPlus, Globe, Users, Lock } from 'lucide-react'; import { HumanAvatar } from '@/components/human-avatar'; import { ScrollArea } from '@/components/ui/scroll-area'; import { KeyHint } from '@/components/key-hint'; import { buildCustomVisibility, buildNetworkVisibility, parseVisibleTo, } from '@/lib/stream-visibility'; import { updateParticleVisibleTo } from '@/lib/firestore-particles'; import { toFirestoreDocPath, type ParticlePath } from '@/lib/particle-path'; import { useNetwork } from '@/hooks/use-networks'; import { cn, getInitials } from '@/lib/utils'; import { resolveHumanDisplay } from '@/lib/humans'; import type { Particle } from '@/api/types'; import { useSuspendPlayback } from '@/hooks/use-suspend-playback'; interface StreamMembersOverlayProps { networkId: string; streamPath: ParticlePath; streamParticle: Particle & { type: 'stream' }; isCreator: boolean; onClose: () => void; } export function StreamMembersOverlay({ networkId, streamPath, streamParticle, isCreator, onClose, }: StreamMembersOverlayProps) { useSuspendPlayback(true, 'stream-members'); const network = useNetwork(networkId); const humans = network?.humans ?? []; const creatorId = streamParticle.created_by_human_id; const visibility = useMemo( () => parseVisibleTo(streamParticle.visible_to, networkId), [streamParticle.visible_to, networkId], ); const docPath = useMemo(() => toFirestoreDocPath(streamPath), [streamPath]); const memberIds = visibility.mode === 'network' ? humans.map((h) => h.id) : visibility.humanIds; const memberSet = new Set(memberIds); const availableToAdd = humans.filter((h) => !memberSet.has(h.id)); const setNetworkWide = useCallback(() => { void updateParticleVisibleTo(docPath, buildNetworkVisibility(networkId)); }, [docPath, networkId]); const setCustomOnlyCreator = useCallback(() => { void updateParticleVisibleTo(docPath, buildCustomVisibility([creatorId])); }, [docPath, creatorId]); const removeMember = useCallback( (id: string) => { if (visibility.mode !== 'custom') return; if (id === creatorId) return; const next = visibility.humanIds.filter((x) => x !== id); if (next.length === 0) return; void updateParticleVisibleTo(docPath, buildCustomVisibility(next)); }, [docPath, creatorId, visibility], ); const addMember = useCallback( (id: string) => { if (visibility.mode !== 'custom') return; void updateParticleVisibleTo( docPath, buildCustomVisibility([...visibility.humanIds, id]), ); }, [docPath, visibility], ); useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); onClose(); } }; window.addEventListener('keydown', handler, { capture: true }); return () => window.removeEventListener('keydown', handler, { capture: true }); }, [onClose]); return createPortal(
{/* Header */}

Members

to close
{/* Visibility */}

Visibility

{isCreator ? (
} label="Network-wide" onClick={setNetworkWide} /> } label="Specific people" onClick={setCustomOnlyCreator} />
) : (
{visibility.mode === 'network' ? ( <> Everyone in {network?.name ?? 'network'} ) : ( <> {memberIds.length} specific people )}
)}
{/* Member list */}

{visibility.mode === 'network' ? 'Has access' : 'People'}{' '} {memberIds.length}

    {memberIds.map((id) => { const display = resolveHumanDisplay(id, humans); const isCreatorRow = id === creatorId; const canRemove = isCreator && visibility.mode === 'custom' && !isCreatorRow; return (
  • {display.displayName} {isCreatorRow && ( Creator )} {canRemove && ( )}
  • ); })}
{/* Add */} {isCreator && visibility.mode === 'custom' && availableToAdd.length > 0 && (

Add people

    {availableToAdd.map((human) => (
  • ))}
)} {isCreator && visibility.mode === 'custom' && availableToAdd.length === 0 && (

Everyone in the network is already a member

)}
, document.body, ); } function VisibilityPill({ active, icon, label, onClick, }: { active: boolean; icon: React.ReactNode; label: string; onClick: () => void; }) { return ( ); }