import { useNostr } from '@nostrify/react'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useAppContext } from '@/hooks/useAppContext'; import { SKATE_RELAY } from '@/lib/types'; import type { NostrEvent, NostrFilter } from '@nostrify/nostrify'; /** * Hook that provides relay-aware query functions for skate-specific content. * * Logic: * - Logged out: Always use community relay only (bootstrap) * - Logged in + useSkateRelay=true: Community relay + user relays (default) * - Logged in + useSkateRelay=false: User relays only (opted out) */ export function useSkateRelays() { const { nostr } = useNostr(); const { user } = useCurrentUser(); const { config } = useAppContext(); const isLoggedIn = !!user; const useSkateRelay = config.useSkateRelay ?? true; /** * Get the list of relay URLs to query for skate content */ const getReadRelays = (): string[] => { // Logged out: always use community relay only if (!isLoggedIn) { return [SKATE_RELAY]; } // Logged in: check setting const userRelays = config.relayMetadata.relays .filter(r => r.read) .map(r => r.url); if (useSkateRelay) { // Include community relay + user relays (deduplicated) const relaySet = new Set([SKATE_RELAY, ...userRelays]); return Array.from(relaySet); } else { // User opted out - only their relays // If they have no relays configured, fall back to community relay return userRelays.length > 0 ? userRelays : [SKATE_RELAY]; } }; /** * Get the list of relay URLs to publish skate content to */ const getWriteRelays = (): string[] => { // Logged out shouldn't be publishing, but just in case if (!isLoggedIn) { return [SKATE_RELAY]; } const userRelays = config.relayMetadata.relays .filter(r => r.write) .map(r => r.url); if (useSkateRelay) { // Include community relay + user relays (deduplicated) const relaySet = new Set([SKATE_RELAY, ...userRelays]); return Array.from(relaySet); } else { // User opted out - only their relays // If they have no relays configured, fall back to community relay return userRelays.length > 0 ? userRelays : [SKATE_RELAY]; } }; /** * Query skate content from appropriate relays based on user state/settings. * Automatically handles parallel queries and deduplication for addressable events. */ const querySkateContent = async ( filters: NostrFilter[], options: { signal: AbortSignal } ): Promise => { const relays = getReadRelays(); console.log(`[useSkateRelays] Querying ${relays.length} relay(s):`, relays); // Query all relays in parallel const queryPromises = relays.map(async (relayUrl) => { try { const relay = nostr.relay(relayUrl); const events = await relay.query(filters, options); console.log(`[useSkateRelays] ${relayUrl} returned ${events.length} events`); return events; } catch (error) { console.warn(`[useSkateRelays] Failed to query ${relayUrl}:`, error); return []; } }); const results = await Promise.all(queryPromises); const allEvents = results.flat(); // Deduplicate addressable events by pubkey+kind+d-tag const eventMap = new Map(); for (const event of allEvents) { const dTag = event.tags.find(([name]) => name === 'd')?.[1]; if (dTag) { // Addressable event - dedupe by coordinate const key = `${event.pubkey}:${event.kind}:${dTag}`; const existing = eventMap.get(key); if (!existing || event.created_at > existing.created_at) { eventMap.set(key, event); } } else { // Regular event - dedupe by id eventMap.set(event.id, event); } } const uniqueEvents = Array.from(eventMap.values()); console.log(`[useSkateRelays] Total unique events: ${uniqueEvents.length}`); return uniqueEvents; }; /** * Generate a stable query key suffix based on relay configuration. * Used to invalidate cache when relay settings change. */ const getQueryKeySuffix = (): string => { const relays = getReadRelays(); return relays.sort().join(','); }; return { getReadRelays, getWriteRelays, querySkateContent, getQueryKeySuffix, isLoggedIn, useSkateRelay, }; }