SkateSpots/src/hooks/useListingReports.ts
SkateSpots 21a0390ed5 refactor(relays): route every relay consumer through useRelays()
So Local relays are used and Blocked relays are excluded EVERYWHERE, not just in
the NPool routers:
- useListingReports, useSellerInfo, useSkateRelays (read+write), useZaps
  (NIP-57 relays tag), usePurchasePro, CollectionDetailPage, CrewForm,
  useDirectMessages write fallback
- AppProvider's empty-relays defaults check is the only intentional direct reader
- AGENTS.md updated

Gate: 101 tests, 0 errors/warnings, boot smoke green.
2026-09-26 22:36:14 +07:00

181 lines
5.2 KiB
TypeScript

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<string, NostrEvent>();
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<string, number>();
// 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<string, NostrEvent>();
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,
});
}