import { useNostr } from '@nostrify/react'; import { useQuery } from '@tanstack/react-query'; import type { NostrEvent } from '@nostrify/nostrify'; import { useRelays } from '@/lib/relays'; const REPORT_KIND = 1984; // NIP-56 Report kind export interface ListingReport { id: string; pubkey: string; reportedEventId: string; reportedPubkey: string; reason: string; content: string; createdAt: number; } /** * Hook to fetch reports for a specific listing event. * Returns the count and details of reports. */ export function useListingReports(eventId: string, pubkey: string) { const { nostr } = useNostr(); const { read } = useRelays(); const relayUrls = read.slice().sort().join(','); return useQuery({ queryKey: ['listing-reports', eventId, relayUrls], queryFn: async (c) => { const signal = AbortSignal.any([c.signal, AbortSignal.timeout(8000)]); try { const readRelays = read; const allEvents: NostrEvent[] = []; for (const relayUrl of readRelays) { try { const relay = nostr.relay(relayUrl); const relayEvents = await relay.query( [ { kinds: [REPORT_KIND], '#e': [eventId], limit: 50, }, ], { signal } ); allEvents.push(...relayEvents); } catch (error) { console.warn(`[useListingReports] Failed to query relay ${relayUrl}:`, error); } } // Deduplicate by event ID const eventMap = new Map(); for (const event of allEvents) { if (!eventMap.has(event.id)) { eventMap.set(event.id, event); } } const reports: ListingReport[] = []; for (const event of Array.from(eventMap.values())) { // Extract reason from e tag (3rd element) or p tag const eTag = event.tags.find(([t, id]: [string, string]) => t === 'e' && id === eventId); const pTag = event.tags.find(([t]: [string]) => t === 'p'); const reason = eTag?.[2] || pTag?.[2] || 'other'; reports.push({ id: event.id, pubkey: event.pubkey, reportedEventId: eventId, reportedPubkey: pubkey, reason, content: event.content, createdAt: event.created_at, }); } return { reports, count: reports.length, isReported: reports.length > 0, shouldHide: reports.length >= 3, // Auto-hide threshold }; } catch (error) { console.warn('[useListingReports] Error fetching reports:', error); return { reports: [], count: 0, isReported: false, shouldHide: false, }; } }, enabled: !!eventId, staleTime: 60000, // Cache for 1 minute gcTime: 300000, // Keep in cache for 5 minutes }); } /** * Hook to batch fetch report counts for multiple listings. * More efficient than calling useListingReports multiple times. */ export function useBatchListingReports(eventIds: string[]) { const { nostr } = useNostr(); const { read } = useRelays(); const relayUrls = read.slice().sort().join(','); return useQuery({ queryKey: ['batch-listing-reports', eventIds.sort().join(','), relayUrls], queryFn: async (c) => { const signal = AbortSignal.any([c.signal, AbortSignal.timeout(10000)]); const reportCounts = new Map(); // Initialize all to 0 eventIds.forEach(id => reportCounts.set(id, 0)); if (eventIds.length === 0) { return reportCounts; } try { const readRelays = read; const allEvents: NostrEvent[] = []; for (const relayUrl of readRelays) { try { const relay = nostr.relay(relayUrl); const relayEvents = await relay.query( [ { kinds: [REPORT_KIND], '#e': eventIds, limit: 200, }, ], { signal } ); allEvents.push(...relayEvents); } catch (error) { console.warn(`[useBatchListingReports] Failed to query relay ${relayUrl}:`, error); } } // Deduplicate by event ID const eventMap = new Map(); for (const event of allEvents) { if (!eventMap.has(event.id)) { eventMap.set(event.id, event); } } // Count reports per listing for (const event of Array.from(eventMap.values())) { const eTags = event.tags.filter(([t]: [string]) => t === 'e'); for (const [, eventId] of eTags) { if (eventIds.includes(eventId)) { reportCounts.set(eventId, (reportCounts.get(eventId) || 0) + 1); } } } return reportCounts; } catch (error) { console.warn('[useBatchListingReports] Error fetching batch reports:', error); return reportCounts; } }, enabled: eventIds.length > 0, staleTime: 60000, gcTime: 300000, }); }