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 { cn } from '@/lib/utils';
interface CollectiveMembersProps {
kind: number;
founderPubkey: string;
identifier: string;
claimedMembers: string[];
/** Delegated admin pubkeys (subset of members). */
admins?: string[];
accentText?: string;
accentBorder?: string;
}
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 }: { member: CollectiveMember; isAdmin: boolean }) {
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}
{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 = [],
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) => (
))}
>
)}