import { useMemo } from 'react'; import { Pressable, ScrollView, Text, View } from 'react-native'; import { Globe, Lock, X } from 'lucide-react-native'; import { toast } from 'sonner-native'; import type { Particle } from '@/api/types'; import { resolveHumanDisplay } from '@/lib/humans'; import { useNetwork } from '@/hooks/use-networks'; import { particlePath, toFirestoreDocPath } from '@/lib/particle-path'; import { updateParticleVisibleTo } from '@/lib/firestore-particles'; import { buildCustomVisibility, buildNetworkVisibility, parseVisibleTo, } from '@/lib/stream-visibility'; import { toUserMessage } from '@/lib/errors'; import { useSuspendPlayback } from '@/hooks/use-suspend-playback'; import { BottomSheet } from '@/components/BottomSheet'; import { Avatar } from '@/components/Avatar'; import { useStreamPresence } from './stream-presence-context'; interface StreamMembersSheetProps { open: boolean; onClose: () => void; networkId: string; streamParticle: Particle & { type: 'stream' }; isCreator: boolean; } /** * Read-only-for-non-creators view of who can see the stream, plus an inline * editor for creators to flip between network-wide and per-person and to * add/remove people. Mobile counterpart of stream-members-overlay.tsx. */ export function StreamMembersSheet({ open, onClose, networkId, streamParticle, isCreator, }: StreamMembersSheetProps) { useSuspendPlayback(open, 'stream-members'); const { onlineHumanIds } = useStreamPresence(); const network = useNetwork(networkId); const humans = network?.humans ?? []; const creatorId = streamParticle.created_by_human_id; const visibility = parseVisibleTo(streamParticle.visible_to, networkId); const docPath = useMemo( () => toFirestoreDocPath(particlePath(networkId, [streamParticle.id])), [networkId, streamParticle.id], ); 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 apply = async (next: string[]) => { try { await updateParticleVisibleTo(docPath, next); } catch (err) { toast.error(toUserMessage(err)); } }; const setNetworkWide = () => apply(buildNetworkVisibility(networkId)); const setCustomOnlyCreator = () => apply(buildCustomVisibility([creatorId])); const removeMember = (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 apply(buildCustomVisibility(next)); }; const addMember = (id: string) => { if (visibility.mode !== 'custom') return; void apply(buildCustomVisibility([...visibility.humanIds, id])); }; return ( Members Visibility {isCreator ? ( } label="Network-wide" onPress={setNetworkWide} /> } label="Specific people" onPress={setCustomOnlyCreator} /> ) : ( {visibility.mode === 'network' ? ( <> Everyone in {network?.name ?? 'network'} ) : ( <> {memberIds.length} specific{' '} {memberIds.length === 1 ? 'person' : 'people'} )} )} {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} {display.exists ? ( {display.email} ) : null} {isCreatorRow ? ( Creator ) : canRemove ? ( removeMember(id)} hitSlop={10} accessibilityLabel={`Remove ${display.displayName}`} > ) : null} ); })} {isCreator && visibility.mode === 'custom' && availableToAdd.length > 0 ? ( Add people {availableToAdd.map((human) => { const display = resolveHumanDisplay(human.id, humans); return ( addMember(human.id)} className="flex-row items-center gap-3 py-2.5 active:bg-white/5 rounded-lg" > {display.displayName} {display.email} Add ); })} ) : null} ); } function ModePill({ active, icon, label, onPress, }: { active: boolean; icon: React.ReactNode; label: string; onPress: () => void; }) { return ( {icon} {label} ); }