import { Link } from 'react-router-dom'; import { nip19 } from 'nostr-tools'; import { Users, Check, Clock, ShieldCheck, EyeOff } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Skeleton } from '@/components/ui/skeleton'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip'; import { useAuthor } from '@/hooks/useAuthor'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { genUserName } from '@/lib/genUserName'; import { useCollectiveMembers, type CollectiveMember } from '@/hooks/useCollectiveMembers'; import { CREW_ROLE_META, type CrewRole } from '@/lib/types'; import { cn } from '@/lib/utils'; interface CollectiveMembersProps { kind: number; founderPubkey: string; identifier: string; claimedMembers: string[]; /** Delegated admin pubkeys (subset of members). */ admins?: string[]; /** Optional skate roles per member (pubkey → roles). Renders flex chips. */ rolesByPubkey?: Map; accentText?: string; accentBorder?: string; } /** Skate-role flex chip (🎥 Filmer, 📷 Photographer, …). */ function RoleChip({ role }: { role: CrewRole }) { const meta = CREW_ROLE_META[role]; return ( {meta.emoji} {meta.label} ); } function StatusChip({ status }: { status: CollectiveMember['status'] }) { if (status === 'confirmed') { return ( Confirmed ); } return ( Pending Listed by the collective — awaiting this member’s confirmation. ); } function AdminChip() { return ( Admin ); } function CrewOnlyChip() { return ( Crew only This member chose to be visible only to crew-mates. You can see them because you're in the crew. ); } function MemberRow({ member, isAdmin, roles, }: { member: CollectiveMember; isAdmin: boolean; roles?: CrewRole[]; }) { const author = useAuthor(member.pubkey); const metadata = author.data?.metadata; const name = metadata?.name ?? genUserName(member.pubkey); const npub = nip19.npubEncode(member.pubkey); return ( {name.slice(0, 2).toUpperCase()} {name} {roles?.map((r) => )} {member.visibility === 'crew' && } {isAdmin && } ); } /** Anonymous placeholder shown to NON-members for a `crew`-only member. */ function HiddenMemberRow() { return (
Hidden member Crew only
); } /** * Renders a collective's members with confirmed / pending trust chips. * Returns null for individuals or collectives with no listed members. */ export function CollectiveMembers({ kind, founderPubkey, identifier, claimedMembers, admins = [], rolesByPubkey, accentText = 'text-indigo-400', accentBorder = 'border-gray-700', }: CollectiveMembersProps) { const { user } = useCurrentUser(); const { data: members, isLoading } = useCollectiveMembers({ kind, founderPubkey, identifier, claimedMembers, enabled: claimedMembers.length > 0, }); if (claimedMembers.length === 0) return null; const adminSet = new Set(admins); const confirmedCount = members?.filter((m) => m.status === 'confirmed').length ?? 0; // The viewer counts as a crew-mate (can see crew-only members) if they are // the founder or a claimed member of this crew. const viewerIsCrewmate = !!user && (user.pubkey === founderPubkey || claimedMembers.includes(user.pubkey)); // Admins first, then the hook's existing confirmed-first ordering. const orderedMembers = members ? [...members].sort((a, b) => { const aa = adminSet.has(a.pubkey) ? 1 : 0; const ba = adminSet.has(b.pubkey) ? 1 : 0; return ba - aa; }) : members; // Split into what the viewer may see vs. anonymised "hidden member" slots. // A crew-only member is always visible to themselves. const isHiddenFromViewer = (m: CollectiveMember) => m.visibility === 'crew' && !viewerIsCrewmate && m.pubkey !== user?.pubkey; const visibleMembers = orderedMembers?.filter((m) => !isHiddenFromViewer(m)) ?? []; const hiddenCount = orderedMembers?.filter(isHiddenFromViewer).length ?? 0; return ( Members · {claimedMembers.length}

{confirmedCount > 0 ? `${confirmedCount} of ${claimedMembers.length} verified — each confirmed member signed with their own Nostr key.` : 'Membership is verified per member. Each member confirms with their own Nostr key.'} {admins.length > 0 && ` ${admins.length} admin${admins.length !== 1 ? 's' : ''} help run the crew.`}

{isLoading ? (
{claimedMembers.slice(0, 3).map((_, i) => (
))}
) : (
{visibleMembers.map((m) => ( ))} {/* Collapse all crew-only members into anonymous slots for outsiders */} {hiddenCount > 0 && ( <> {Array.from({ length: hiddenCount }).map((_, i) => ( ))} )}
)}
); }