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>
110 lines
3.6 KiB
TypeScript
110 lines
3.6 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { Loader2, EyeOff, Globe } 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 { VisibilityPicker } from '@/components/ConfirmMembershipCard';
|
|
import { cn } from '@/lib/utils';
|
|
import type { MemberVisibility } from '@/lib/types';
|
|
|
|
interface Props {
|
|
kind: number;
|
|
founderPubkey: string;
|
|
identifier: string;
|
|
collectiveName: string;
|
|
claimedMembers: string[];
|
|
accentBg?: string;
|
|
}
|
|
|
|
/**
|
|
* Shown to a CONFIRMED member so they can change their visibility (public vs
|
|
* crew-only) at any time. Re-publishes their kind-1985 attestation with the new
|
|
* `visibility` tag. Hidden for the founder, non-members, and pending members
|
|
* (those use the ConfirmMembershipCard instead).
|
|
*/
|
|
export function MemberVisibilityControl({
|
|
kind,
|
|
founderPubkey,
|
|
identifier,
|
|
collectiveName,
|
|
claimedMembers,
|
|
accentBg = 'bg-teal-500 hover:bg-teal-600',
|
|
}: Props) {
|
|
const { user } = useCurrentUser();
|
|
const { toast } = useToast();
|
|
const { mutateAsync: republish, isPending } = useConfirmMembership();
|
|
const { data: members } = useCollectiveMembers({
|
|
kind,
|
|
founderPubkey,
|
|
identifier,
|
|
claimedMembers,
|
|
enabled: claimedMembers.length > 0,
|
|
});
|
|
|
|
const me = user?.pubkey.toLowerCase();
|
|
const mine = members?.find((m) => m.pubkey === me);
|
|
const [visibility, setVisibility] = useState<MemberVisibility>('public');
|
|
|
|
// Sync local state to the published value once it loads.
|
|
useEffect(() => {
|
|
if (mine?.visibility) setVisibility(mine.visibility);
|
|
}, [mine?.visibility]);
|
|
|
|
if (!user || !me) return null;
|
|
if (me === founderPubkey.toLowerCase()) return null;
|
|
// Only for confirmed members.
|
|
if (!mine || mine.status !== 'confirmed') return null;
|
|
|
|
const changed = visibility !== (mine.visibility ?? 'public');
|
|
|
|
const handleSave = async () => {
|
|
try {
|
|
await republish({ kind, founderPubkey, identifier, collectiveName, visibility });
|
|
toast({
|
|
title: 'Visibility updated',
|
|
description:
|
|
visibility === 'crew'
|
|
? 'You now appear only to crew-mates.'
|
|
: 'You now appear publicly in this crew.',
|
|
});
|
|
} catch (error) {
|
|
toast({
|
|
title: 'Could not update',
|
|
description: error instanceof Error ? error.message : 'Please try again.',
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Card className="border border-white/10 bg-black/60">
|
|
<CardContent className="space-y-3 p-4">
|
|
<div className="flex items-center gap-2">
|
|
{(mine.visibility ?? 'public') === 'crew' ? (
|
|
<EyeOff className="h-4 w-4 text-gray-400" />
|
|
) : (
|
|
<Globe className="h-4 w-4 text-gray-400" />
|
|
)}
|
|
<p className="text-xs font-black uppercase tracking-widest text-gray-400">
|
|
Your visibility in this crew
|
|
</p>
|
|
</div>
|
|
<VisibilityPicker value={visibility} onChange={setVisibility} accentBg={accentBg.split(' ')[0]} />
|
|
{changed && (
|
|
<Button
|
|
type="button"
|
|
onClick={handleSave}
|
|
disabled={isPending}
|
|
className={cn('w-full gap-2 font-bold', accentBg)}
|
|
>
|
|
{isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
|
|
Save visibility
|
|
</Button>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|