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>
74 lines
2.4 KiB
TypeScript
74 lines
2.4 KiB
TypeScript
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { useNostr } from '@nostrify/react';
|
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
|
import {
|
|
MEMBERSHIP_LABEL_NAMESPACE,
|
|
MEMBERSHIP_LABEL_VALUE,
|
|
type MemberVisibility,
|
|
} from '@/lib/types';
|
|
|
|
interface AttestParams {
|
|
/** Kind of the collective profile being joined. */
|
|
kind: number;
|
|
/** Founder pubkey (author of the collective profile). */
|
|
founderPubkey: string;
|
|
/** d-tag identifier of the collective profile. */
|
|
identifier: string;
|
|
/** Collective display name (stored in content for readability). */
|
|
collectiveName?: string;
|
|
/** Relay hint for the referenced collective. */
|
|
relayHint?: string;
|
|
/** Opt-in visibility choice — defaults to 'public' if omitted. */
|
|
visibility?: MemberVisibility;
|
|
}
|
|
|
|
/**
|
|
* Publish a NIP-32 membership attestation (kind 1985) confirming that the
|
|
* logged-in user is a member of the given collective. Signed with the user's
|
|
* OWN key, so the confirmation cannot be forged by the collective.
|
|
*/
|
|
export function useConfirmMembership() {
|
|
const { nostr } = useNostr();
|
|
const { user } = useCurrentUser();
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: async ({
|
|
kind,
|
|
founderPubkey,
|
|
identifier,
|
|
collectiveName,
|
|
relayHint,
|
|
visibility = 'public',
|
|
}: AttestParams) => {
|
|
if (!user) throw new Error('You must be logged in to confirm membership');
|
|
|
|
const address = `${kind}:${founderPubkey}:${identifier}`;
|
|
const aTag = relayHint ? ['a', address, relayHint] : ['a', address];
|
|
|
|
const tags: string[][] = [
|
|
['L', MEMBERSHIP_LABEL_NAMESPACE],
|
|
['l', MEMBERSHIP_LABEL_VALUE, MEMBERSHIP_LABEL_NAMESPACE],
|
|
aTag,
|
|
['p', founderPubkey],
|
|
['visibility', visibility],
|
|
['alt', `Skate collective membership confirmation for ${collectiveName ?? address}`],
|
|
];
|
|
|
|
const event = await user.signer.signEvent({
|
|
kind: 1985,
|
|
content: collectiveName
|
|
? `I'm a member of ${collectiveName}`
|
|
: 'Confirmed collective membership',
|
|
tags,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
});
|
|
|
|
await nostr.event(event, { signal: AbortSignal.timeout(15000) });
|
|
return event;
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['collective-members'] });
|
|
},
|
|
});
|
|
}
|