SkateSpots/src/hooks/useNotASpotReport.ts

88 lines
3 KiB
TypeScript

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,
};
}