SkateSpots/src/hooks/useMembershipAttestation.ts
shakespeare.diy 926470a86e Crews: opt-in member visibility + Lab vouch/web-of-trust anti-fraud + CrewDetail redesign
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>
2026-08-10 21:01:57 +07:00

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'] });
},
});
}