SkateSpots/src/hooks/useListingReports.ts
shakespeare.diy e29d35f1e4 fix: restore full skate-spots source tree from misplaced src/src/ copy
The entire skate-spots codebase was accidentally copied into src/src/ and
public/public/ instead of src/ and public/ directly. This left the real
src/ directory as blank MKStack template stubs, explaining why the landing
page showed the default template instead of SkateSpots.

Changes made:
- Copied all files from src/src/ up to src/ (App.tsx, AppRouter.tsx, main.tsx,
  all pages, components, hooks, contexts, lib, i18n, test)
- Copied all files from public/public/ up to public/ (images, manifest, etc.)
- Deleted src/src/ and public/public/ nested directories
- Removed HelloWorld.tsx template stub from pages/
- Retained the previously fixed index.css (Tailwind v3 directives)

Full skate-spots feature set is now active:
- All 30+ routes (map, events, spots, coaches, marketplace, wallet, etc.)
- NWCProvider, LanguageProvider, PWAInstallPrompt, AmberCallbackHandler
- Skate community relays (goskatespots.nostr1.com, wotr.relatr.xyz, etc.)
- Outfit Variable font, Amber pre-check in main.tsx
- All hooks (useSkateSpots, useCoaches, useZaps, etc.)
- i18n support (en/es)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-15 22:18:28 +07:00

192 lines
5.4 KiB
TypeScript

import { useNostr } from '@nostrify/react';
import { useQuery } from '@tanstack/react-query';
import { useAppContext } from '@/hooks/useAppContext';
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 { config } = useAppContext();
const relayUrls = config.relayMetadata.relays
.filter(r => r.read)
.map(r => r.url)
.sort()
.join(',');
return useQuery({
queryKey: ['listing-reports', eventId, relayUrls],
queryFn: async (c) => {
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(8000)]);
try {
const readRelays = config.relayMetadata.relays
.filter(r => r.read)
.map(r => r.url);
const allEvents: any[] = [];
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, any>();
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 { config } = useAppContext();
const relayUrls = config.relayMetadata.relays
.filter(r => r.read)
.map(r => r.url)
.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 = config.relayMetadata.relays
.filter(r => r.read)
.map(r => r.url);
const allEvents: any[] = [];
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, any>();
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,
});
}