import { useMutation } from '@tanstack/react-query'; import { useNostr } from '@nostrify/react'; import { useSkateRelays } from '@/hooks/useSkateRelays'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useToast } from '@/hooks/useToast'; import { useTranslation } from '@/contexts/language'; import { MODERATION_LABEL_NAMESPACE } from '@/lib/types'; /** * One-tap "this is not a skate spot" report. * * WHY NIP-56 AND NOT A NEW KIND: kind 1984 is the ecosystem's report event and * `other` is its catch-all reason, so the report is useful to *any* Nostr client * (and visible on relays) instead of being a SkateSpots-only artifact. The extra * `L`/`l`/`t` tags give us a stable taxonomy (`not-a-skate-spot`) that our own * admin tooling can filter on without breaking anyone else's interpretation. * * This is for OFF-TOPIC places (test entries, an airport, a mall, private land) — * not for disagreeing about whether a spot is good. */ export function useNotASpotReport() { const { t } = useTranslation(); const { user } = useCurrentUser(); const { nostr } = useNostr(); const { getWriteRelays } = useSkateRelays(); const { toast } = useToast(); const mutation = useMutation({ mutationFn: async ({ eventId, pubkey, title, }: { /** The spot's kind-30818 event id. */ eventId: string; /** The spot's author. */ pubkey: string; title?: string; }) => { if (!user) throw new Error(t('toastsUi.loginToReport')); const event = await user.signer.signEvent({ kind: 1984, // NIP-56 report content: title ? `Not a skate spot: ${title}` : 'Not a skate spot', created_at: Math.floor(Date.now() / 1000), tags: [ ['e', eventId, 'other'], ['p', pubkey], ['L', MODERATION_LABEL_NAMESPACE], ['l', 'not-a-skate-spot', MODERATION_LABEL_NAMESPACE], ['t', 'not-a-skate-spot'], ['alt', 'SkateSpots: report — not a skate spot'], ], }); // Publish to the COMMUNITY relay as well as the reporter's own relays. // Admins read reports from the community relay, so a report written only // to the reporter's personal relays would never reach them. const results = await Promise.allSettled( getWriteRelays().map((url) => nostr.relay(url).event(event, { signal: AbortSignal.timeout(8000) }), ), ); if (results.every((r) => r.status === 'rejected')) { throw new Error(t('toastsUi.relayUnreachable')); } }, onSuccess: () => { toast({ title: t('toastsUi.notASpotThanks'), description: t('toastsUi.notASpotThanksDesc'), }); }, onError: (error) => { toast({ title: t('toastsUi.notASpotFailed'), description: error instanceof Error ? error.message : t('toastsUi.pleaseTryAgain'), variant: 'destructive', }); }, }); return { reportNotASpot: mutation.mutateAsync, isPending: mutation.isPending, alreadyReported: mutation.isSuccess, }; }