Step 3 — member visibility (best-of-both, honest):
- MemberVisibility ('public' | 'crew') stored on the member's OWN kind-1985
attestation via a `visibility` tag, so a public crew can contain a private
member. Confirmed crew-mates + founder see them; outsiders see anonymised
"hidden member" slots. Framed honestly in-UI (app-level discovery privacy).
- VisibilityPicker in ConfirmMembershipCard (choose on join) +
MemberVisibilityControl for confirmed members to change it anytime.
- useCollectiveMembers parses visibility; CollectiveMembers hides crew-only
members from non-members.
Step 4 — anti-fraud vouch layer:
- useVouches/useVouchFor: NIP-32 vouch labels, trust-WEIGHTED via web-of-trust
so fake-account vouch rings carry no weight. VouchCard with new/vouched/
scene-verified tiers, wired into every Lab profile detail page.
Step 4 — CrewDetail UX redesign:
- Activity consolidated into clean Feed/Events/Chat tabs (no more giant scroll)
- Sidebar reordered: Members → Support → Band → Connections → Founder
- Member visibility control surfaced; tighter hero
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
162 lines
5.2 KiB
TypeScript
162 lines
5.2 KiB
TypeScript
import { useState } from 'react';
|
|
import { Users, Check, Loader2, Globe, EyeOff } from 'lucide-react';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
|
import { useCollectiveMembers } from '@/hooks/useCollectiveMembers';
|
|
import { useConfirmMembership } from '@/hooks/useMembershipAttestation';
|
|
import { useToast } from '@/hooks/useToast';
|
|
import { cn } from '@/lib/utils';
|
|
import {
|
|
MEMBER_VISIBILITY_DESCRIPTIONS,
|
|
type MemberVisibility,
|
|
} from '@/lib/types';
|
|
|
|
interface ConfirmMembershipCardProps {
|
|
kind: number;
|
|
founderPubkey: string;
|
|
identifier: string;
|
|
collectiveName: string;
|
|
claimedMembers: string[];
|
|
accentBg?: string;
|
|
}
|
|
|
|
/** Two-option visibility picker used both when confirming and when editing. */
|
|
export function VisibilityPicker({
|
|
value,
|
|
onChange,
|
|
accentBg = 'bg-indigo-600',
|
|
}: {
|
|
value: MemberVisibility;
|
|
onChange: (v: MemberVisibility) => void;
|
|
accentBg?: string;
|
|
}) {
|
|
const options: { key: MemberVisibility; label: string; icon: React.ComponentType<{ className?: string }> }[] = [
|
|
{ key: 'public', label: 'Public', icon: Globe },
|
|
{ key: 'crew', label: 'Crew only', icon: EyeOff },
|
|
];
|
|
return (
|
|
<div className="space-y-2">
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{options.map(({ key, label, icon: Icon }) => {
|
|
const active = value === key;
|
|
return (
|
|
<button
|
|
key={key}
|
|
type="button"
|
|
onClick={() => onChange(key)}
|
|
className={cn(
|
|
'flex items-center justify-center gap-1.5 rounded-lg border px-3 py-2 text-sm font-bold transition-all active:scale-95',
|
|
active
|
|
? cn('border-transparent text-white', accentBg)
|
|
: 'border-white/10 bg-white/5 text-gray-400 hover:bg-white/10 hover:text-white'
|
|
)}
|
|
>
|
|
<Icon className="h-4 w-4" />
|
|
{label}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
<p className="text-xs leading-relaxed text-gray-500">
|
|
{MEMBER_VISIBILITY_DESCRIPTIONS[value]}
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Shown to a logged-in user who has been LISTED as a member of a collective but
|
|
* has not yet confirmed. Lets them pick their visibility and publishes a NIP-32
|
|
* attestation on click. Hidden for everyone else (non-members, already-confirmed
|
|
* members, the founder).
|
|
*/
|
|
export function ConfirmMembershipCard({
|
|
kind,
|
|
founderPubkey,
|
|
identifier,
|
|
collectiveName,
|
|
claimedMembers,
|
|
accentBg = 'bg-indigo-600 hover:bg-indigo-700',
|
|
}: ConfirmMembershipCardProps) {
|
|
const { user } = useCurrentUser();
|
|
const { toast } = useToast();
|
|
const { mutateAsync: confirm, isPending } = useConfirmMembership();
|
|
const [visibility, setVisibility] = useState<MemberVisibility>('public');
|
|
const { data: members } = useCollectiveMembers({
|
|
kind,
|
|
founderPubkey,
|
|
identifier,
|
|
claimedMembers,
|
|
enabled: claimedMembers.length > 0,
|
|
});
|
|
|
|
if (!user) return null;
|
|
|
|
const me = user.pubkey.toLowerCase();
|
|
// Only relevant if I'm actually listed as a member and I'm not the founder.
|
|
if (me === founderPubkey.toLowerCase()) return null;
|
|
if (!claimedMembers.includes(me)) return null;
|
|
|
|
const myStatus = members?.find((m) => m.pubkey === me)?.status;
|
|
if (myStatus === 'confirmed') return null;
|
|
|
|
const handleConfirm = async () => {
|
|
try {
|
|
await confirm({ kind, founderPubkey, identifier, collectiveName, visibility });
|
|
toast({
|
|
title: 'Membership confirmed!',
|
|
description:
|
|
visibility === 'crew'
|
|
? `You're verified in ${collectiveName} — visible to crew-mates only.`
|
|
: `You now show as a verified member of ${collectiveName}.`,
|
|
});
|
|
} catch (error) {
|
|
toast({
|
|
title: 'Failed to confirm',
|
|
description: error instanceof Error ? error.message : 'Please try again.',
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Card className="border-2 border-amber-500/50 bg-amber-950/20">
|
|
<CardContent className="space-y-4 p-4">
|
|
<div className="flex items-start gap-3">
|
|
<Users className="mt-0.5 h-6 w-6 shrink-0 text-amber-400" />
|
|
<div>
|
|
<h3 className="font-bold text-amber-300">You've been added to {collectiveName}</h3>
|
|
<p className="text-sm text-gray-300">
|
|
Confirm your membership so it shows as verified on your profile and theirs.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<p className="mb-2 text-xs font-black uppercase tracking-widest text-gray-400">
|
|
Your visibility
|
|
</p>
|
|
<VisibilityPicker value={visibility} onChange={setVisibility} accentBg={accentBg.split(' ')[0]} />
|
|
</div>
|
|
|
|
<Button
|
|
type="button"
|
|
onClick={handleConfirm}
|
|
disabled={isPending}
|
|
className={cn('w-full gap-2 font-bold', accentBg)}
|
|
>
|
|
{isPending ? (
|
|
<>
|
|
<Loader2 className="h-4 w-4 animate-spin" /> Confirming…
|
|
</>
|
|
) : (
|
|
<>
|
|
<Check className="h-4 w-4" /> Confirm Membership
|
|
</>
|
|
)}
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|