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, } 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, }: { /** 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; }) => { 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]); await publish({ kind: 1985, content: reason ?? '', tags }); // Optimistic, this device — newest verdict wins on merge. const verdict = label === MODERATION_LABEL_HIDDEN ? 'hidden' : label === MODERATION_LABEL_VERIFIED ? 'verified' : 'cleared'; setLocalVerdict({ coord, id }, verdict); queryClient.invalidateQueries({ queryKey: ['admin-moderation'] }); }, }); 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 }; }