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}
);
}