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>
192 lines
5.4 KiB
TypeScript
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,
|
|
});
|
|
}
|