import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useNostrPublish } from '@/hooks/useNostrPublish'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { isAdminPubkey } from '@/lib/admins'; import { setLocalVerdict } from '@/lib/localModeration'; import { MODERATION_LABEL_NAMESPACE, MODERATION_LABEL_HIDDEN, MODERATION_LABEL_VERIFIED, MODERATION_LABEL_CLEARED, MODERATION_LABEL_CLAIM_APPROVED, } from '@/lib/types'; /** * Publish an admin moderation label (NIP-32 kind 1985) for a target. * * THE MISSING HALF: `useAdminModeration` has always READ these labels and * filtered on them, but nothing ever PUBLISHED one — the only working hide was * the local, this-device-only one in `useDeleteEntity`. This is the write side. * * Two symmetric verdicts share one code path: * • `hidden` — take it out of the app (soft, reversible); * • `verified` — vouch for it, which also clears it from the review queue; * • `cleared` — UNDO: retract a verdict, back to neutral (newest wins). * * The label is public and authored by an admin key, so every SkateSpots client * filters/boosts accordingly. Nothing is ever deleted from relays — that is the * honest behaviour on a censorship-resistant network. * * A local optimistic mirror is written too, so the effect is instant on this * device instead of waiting for the relay round-trip. */ function usePublishModerationLabel(label: string) { const { user } = useCurrentUser(); const { mutateAsync: publish, isPending: publishing } = useNostrPublish(); const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: async ({ coord, id, reason, subject, }: { /** Addressable coordinate, e.g. `30818::`. */ coord?: string; /** Event id (for regular/replaceable events). */ id?: string; /** Why — stored in the label's content for transparency. */ reason?: string; /** Optional subject pubkey (e.g. the claimant a claim-approval grants). */ subject?: string; }) => { if (!isAdminPubkey(user?.pubkey)) throw new Error('Admins only.'); if (!coord && !id) throw new Error('Need a coordinate or an event id.'); const tags: string[][] = [ ['L', MODERATION_LABEL_NAMESPACE], ['l', label, MODERATION_LABEL_NAMESPACE], ['alt', `SkateSpots: moderation label — ${label}`], ]; if (coord) tags.push(['a', coord]); if (id) tags.push(['e', id]); if (subject) tags.push(['p', subject]); await publish({ kind: 1985, content: reason ?? '', tags }); // Optimistic, this device — newest verdict wins on merge. `claim-approved` // is NOT a hide/verify state, so it writes no local verdict (it must not // clobber a real hidden/verified verdict on the same coord). if (label === MODERATION_LABEL_HIDDEN) setLocalVerdict({ coord, id }, 'hidden'); else if (label === MODERATION_LABEL_VERIFIED) setLocalVerdict({ coord, id }, 'verified'); else if (label === MODERATION_LABEL_CLEARED) setLocalVerdict({ coord, id }, 'cleared'); queryClient.invalidateQueries({ queryKey: ['admin-moderation'] }); queryClient.invalidateQueries({ queryKey: ['spot-claim-approvals'] }); }, }); return { run: mutation.mutateAsync, isPending: mutation.isPending || publishing }; } /** Hide a target across the app (soft + reversible). */ export function useAdminHide() { const { run, isPending } = usePublishModerationLabel(MODERATION_LABEL_HIDDEN); return { hideContent: run, isPending }; } /** Vouch for a target — the positive counterpart, which clears the review queue. */ export function useAdminVerify() { const { run, isPending } = usePublishModerationLabel(MODERATION_LABEL_VERIFIED); return { confirmContent: run, isPending }; } /** * UNDO: retract whatever verdict was on the target so it is merely neutral * again — not hidden, not vouched. The spot stays exactly where it was; only the * verdict goes away. */ export function useAdminClear() { const { run, isPending } = usePublishModerationLabel(MODERATION_LABEL_CLEARED); return { clearVerdict: run, isPending }; } /** * APPROVE a private-spot ownership claim. Publishes `claim-approved` tagged * with the spot coordinate and the claimant's pubkey; the claimant then finishes * the claim themselves (only they can author their own version of the spot). */ export function useAdminApproveClaim() { const { run, isPending } = usePublishModerationLabel(MODERATION_LABEL_CLAIM_APPROVED); return { approveClaim: run, isPending }; }