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 { useTranslation } from '@/contexts/language'; 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 { CREW_ROLE_KEYS } from '@/lib/labelKeys'; 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 { t } = useTranslation(); const meta = CREW_ROLE_META[role]; return ( {meta.emoji} {CREW_ROLE_KEYS[role] ? t(CREW_ROLE_KEYS[role]) : meta.label} ); } function StatusChip({ status }: { status: CollectiveMember['status'] }) { const { t } = useTranslation(); if (status === 'confirmed') { return ( {t('socialUi.collectiveMembers.confirmed')} ); } return ( {t('socialUi.collectiveMembers.pending')} {t('socialUi.collectiveMembers.pendingTooltip')} ); } function AdminChip() { const { t } = useTranslation(); return ( {t('socialUi.inviteCard.admin')} ); } function CrewOnlyChip() { const { t } = useTranslation(); return ( {t('socialUi.collectiveMembers.crewOnly')} {t('socialUi.collectiveMembers.crewOnlyTooltip')} ); } 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() { const { t } = useTranslation(); return (
{t('socialUi.collectiveMembers.hiddenMember')} {t('socialUi.collectiveMembers.crewOnly')}
); } /** * 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 { t } = useTranslation(); 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 ( {t('socialUi.collectiveMembers.title')} · {claimedMembers.length}

{confirmedCount > 0 ? t('socialUi.collectiveMembers.verifiedOf', { confirmed: confirmedCount, total: claimedMembers.length }) : t('socialUi.collectiveMembers.verifiedPerMember')} {admins.length > 0 && (admins.length === 1 ? ` ${t('socialUi.collectiveMembers.adminsOne', { count: admins.length })}` : ` ${t('socialUi.collectiveMembers.adminsMany', { count: admins.length })}`)}

{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) => ( ))} )}
)}
); }