import { useQuery } from '@tanstack/react-query'; import { useSkateRelays } from '@/hooks/useSkateRelays'; import type { NostrEvent } from '@nostrify/nostrify'; /** NIP-56 report kind. */ export const REPORT_KIND = 1984; export interface ReportTarget { id: string; kind: number; pubkey: string; identifier?: string; /** `kind:pubkey:d` when the target is addressable (what the labels use). */ coord?: string; title?: string; } export interface RecentReport { id: string; reporter: string; createdAt: number; /** NIP-56 reason from the `e` tag: spam | illegal | impersonation | other … */ reason: string; content: string; /** The reported event, when we could resolve it. */ target?: ReportTarget; } /** * The report inbox: recent NIP-56 reports, with their targets RESOLVED. * * WHY: reports were only ever visible for items that already happened to be in * the review queue (and only as a count). A report about a comment, a profile, a * crew or anything not currently flagged had nowhere to land. This reads recent * reports and resolves the referenced event so a human can act on any of them — * no heuristics involved. */ export function useRecentReports(limit = 200) { const { querySkateContent, getQueryKeySuffix } = useSkateRelays(); return useQuery({ queryKey: ['recent-reports', limit, getQueryKeySuffix()], staleTime: 60_000, queryFn: async (c): Promise => { const signal = AbortSignal.any([c.signal, AbortSignal.timeout(9000)]); const reports = await querySkateContent([{ kinds: [REPORT_KIND], limit }], { signal }); if (reports.length === 0) return []; // Resolve the reported events: one extra query for every referenced id. const ids = [ ...new Set( reports .flatMap((r) => r.tags.filter(([n]) => n === 'e').map(([, v]) => v)) .filter(Boolean), ), ].slice(0, 200); const targets = new Map(); if (ids.length > 0) { try { const events = await querySkateContent([{ ids, limit: ids.length }], { signal }); for (const ev of events) targets.set(ev.id, ev); } catch { // Resolution is best-effort — an unresolvable report is still shown. } } return reports .map((r): RecentReport => { const eTag = r.tags.find(([n]) => n === 'e'); const targetEvent = eTag?.[1] ? targets.get(eTag[1]) : undefined; const d = targetEvent?.tags.find(([n]) => n === 'd')?.[1]; const title = targetEvent?.tags.find(([n]) => n === 'title' || n === 'name')?.[1]; return { id: r.id, reporter: r.pubkey, createdAt: r.created_at, reason: eTag?.[2] || 'other', content: r.content, target: targetEvent ? { id: targetEvent.id, kind: targetEvent.kind, pubkey: targetEvent.pubkey, identifier: d, coord: d ? `${targetEvent.kind}:${targetEvent.pubkey}:${d}` : undefined, title, } : undefined, }; }) .sort((a, b) => b.createdAt - a.createdAt); }, }); } /** Human label + route for a reported kind. */ export function reportTargetMeta(kind: number): { label: string; path?: string } { switch (kind) { case 30818: return { label: 'spot', path: '/spot' }; case 30017: return { label: 'shop', path: '/shop' }; case 31923: case 31925: return { label: 'event', path: '/event' }; case 30823: return { label: 'tour', path: '/tour' }; case 30402: return { label: 'listing', path: '/listing' }; case 35006: return { label: 'crew', path: '/crew' }; case 35000: return { label: 'skater profile' }; default: if (kind >= 35001 && kind <= 35014) return { label: 'Lab profile' }; return { label: `kind ${kind}` }; } }