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.
181 lines
5.2 KiB
TypeScript
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,
|
|
});
|
|
}
|