SkateSpots/src/components/NostrProvider.tsx
SkateSpots 6def78fffe chore(lint): clear all no-unused-vars warnings (position-precise)
Removed/renamed the remaining 39 unused locals/dead code, file-by-file:
- unused destructured bindings/params (remove the binding, keep the call)
- unused single-line pure consts and dead functions/interfaces removed
- deleted the dead _FeedItem component and its now-unused imports
- amber _readChunks self-call fixed; SpotMap popup call site updated

Warnings: 182 -> 7 (only react-refresh/only-export-components remain).
no-explicit-any=0, exhaustive-deps=0, no-unused-vars=0.
Gate: 101 tests, 0 errors, boot smoke green. AGENTS.md baseline + guidance updated.
2026-09-26 22:20:21 +07:00

205 lines
No EOL
7.7 KiB
TypeScript

import React, { useEffect, useRef } from 'react';
import { NostrEvent, NostrFilter, NPool, NRelay1 } from '@nostrify/nostrify';
import { NostrContext } from '@nostrify/react';
import { useQueryClient } from '@tanstack/react-query';
import { useAppContext } from '@/hooks/useAppContext';
import { useCitrine } from '@/hooks/useCitrine';
import { SKATE_RELAY } from '@/lib/types';
import { resolveRelays } from '@/lib/relays';
interface NostrProviderProps {
children: React.ReactNode;
}
// Default fallback relays - used if no relays are configured
const DEFAULT_RELAYS = [
{ url: 'wss://auth.nostr1.com', read: true, write: true },
{ url: 'wss://wotr.relatr.xyz', read: true, write: true },
{ url: 'wss://relay.ditto.pub', read: true, write: true },
{ url: SKATE_RELAY, read: true, write: true },
{ url: 'wss://purplepag.es', read: true, write: false }, // Profile aggregator
];
const NostrProvider: React.FC<NostrProviderProps> = (props) => {
const { children } = props;
const { config } = useAppContext();
const citrine = useCitrine();
const queryClient = useQueryClient();
// Create NPool instance only once
const pool = useRef<NPool | undefined>(undefined);
// Use refs so the pool always has the latest data
const relayMetadata = useRef(config.relayMetadata);
const useSkateRelay = useRef(config.useSkateRelay ?? true);
// Local Citrine relay URL when active (Android + opted-in + reachable), else
// null. Kept in a ref so the once-initialized pool's routers always read the
// current value without recreating the pool.
const citrineUrl = useRef<string | null>(citrine.url);
// Invalidate Nostr queries when relay metadata changes
useEffect(() => {
relayMetadata.current = config.relayMetadata;
useSkateRelay.current = config.useSkateRelay ?? true;
queryClient.invalidateQueries({ queryKey: ['nostr'] });
}, [config.relayMetadata, config.useSkateRelay, queryClient]);
// Keep the Citrine ref current and refresh queries when the local relay comes
// or goes, so reads immediately start (or stop) hitting it.
useEffect(() => {
citrineUrl.current = citrine.url;
queryClient.invalidateQueries({ queryKey: ['nostr'] });
}, [citrine.url, queryClient]);
// ── A. Pre-warm relay connections at boot ──────────────────────────────
// NPool opens WebSockets lazily on the first query, which pushes the whole
// TCP+TLS+WS handshake to whenever the map first renders. We instead kick the
// handshakes off the instant the provider mounts, so sockets are already open
// (or warming) by the time real queries run and the first events land sooner.
// Fire-and-forget: a tiny throwaway query per read relay. Failures are ignored
// (a dead relay must never delay or break boot).
const warmed = useRef(false);
useEffect(() => {
if (warmed.current || !pool.current) return;
warmed.current = true;
const configuredRelays = relayMetadata.current.relays.length > 0
? relayMetadata.current.relays
: DEFAULT_RELAYS;
const readUrls = new Set<string>();
if (useSkateRelay.current) readUrls.add(SKATE_RELAY);
for (const r of configuredRelays) {
if (r.read) readUrls.add(r.url);
}
// Signal "warm" as soon as the FIRST relay responds, so the splash can
// dismiss early. Guarded so it only fires once.
let signalled = false;
const signalWarm = () => {
if (signalled) return;
signalled = true;
try {
window.dispatchEvent(new Event('skatespots:relays-warm'));
} catch {
// ignore
}
};
// Open + probe each relay independently. A short filter with limit:0 opens
// the socket and returns almost immediately without pulling real data.
for (const url of readUrls) {
try {
const relay = pool.current.relay(url);
const signal = AbortSignal.timeout(4000);
// Don't await — let all sockets warm in parallel, swallow any error.
relay
.query([{ kinds: [1], limit: 0 }], { signal })
.then(signalWarm)
.catch(() => {});
} catch {
// Ignore — never let pre-warming affect boot.
}
}
// Intentionally run once on mount; relay changes are handled by the
// invalidation effects above and lazy-open on demand.
}, []);
// Initialize NPool only once
if (!pool.current) {
pool.current = new NPool({
open(url: string) {
return new NRelay1(url);
},
reqRouter(filters: NostrFilter[]) {
const routes = new Map<string, NostrFilter[]>();
// Get relays from metadata, fallback to defaults if empty.
// `resolveRelays` applies the blocklist and adds local relays.
const resolved = resolveRelays(relayMetadata.current);
const readRelays = resolved.read.length > 0
? resolved.read
: DEFAULT_RELAYS.filter((r) => r.read).map((r) => r.url);
// --- Community "catching" relay: top hierarchy ---
// goskatespots.nostr1.com is the app's curated, authoritative relay.
// It is ALWAYS queried first (even if a user hasn't added it to their
// read list), so curated skate content takes priority. Other relays act
// as supplementary sources for redundancy and reach.
if (useSkateRelay.current) {
routes.set(SKATE_RELAY, filters);
}
// --- Local Citrine relay (Android, opt-in, reachable) ---
// Query the on-device relay alongside remotes. NPool runs routes in
// parallel and the localhost hit returns first (~1ms), so reads feel
// instant while remotes reconcile in the background.
if (citrineUrl.current) {
routes.set(citrineUrl.current, filters);
}
// Ensure we always have at least one relay
if (readRelays.length === 0 && routes.size === 0) {
console.warn('[NostrProvider] No read relays configured, using defaults');
for (const relay of DEFAULT_RELAYS) {
routes.set(relay.url, filters);
}
} else {
for (const url of readRelays) {
// Skip re-adding the catching relay (already set above with priority)
if (url === SKATE_RELAY) continue;
routes.set(url, filters);
}
}
return routes;
},
eventRouter(_event: NostrEvent) {
// `resolveRelays` applies the blocklist and adds local relays.
const resolved = resolveRelays(relayMetadata.current);
const writeRelays = resolved.write.length > 0
? resolved.write
: DEFAULT_RELAYS.filter((r) => r.write).map((r) => r.url);
const allRelays = new Set<string>();
// Catching relay first: publish curated skate content to the community
// relay as the top-priority write target for discoverability.
if (useSkateRelay.current) {
allRelays.add(SKATE_RELAY);
}
// Then the user's own write relays for redundancy and reach.
for (const url of writeRelays) {
allRelays.add(url);
}
// Local Citrine relay (Android, opt-in, reachable): write here too so
// the device keeps a durable copy (backup) and captures the event even
// when offline, ready to sync to remotes when connectivity returns.
if (citrineUrl.current) {
allRelays.add(citrineUrl.current);
}
// Ensure we always have at least one relay
if (allRelays.size === 0) {
console.warn('No write relays configured, using defaults');
DEFAULT_RELAYS.forEach(r => allRelays.add(r.url));
}
return [...allRelays];
},
});
}
return (
<NostrContext.Provider value={{ nostr: pool.current }}>
{children}
</NostrContext.Provider>
);
};
export default NostrProvider;