SkateSpots/src/components/NostrSync.tsx
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

251 lines
9.3 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import { useNostr } from '@nostrify/react';
import { useNostrLogin } from '@nostrify/react/login';
import { useQueryClient } from '@tanstack/react-query';
import { NSchema as n } from '@nostrify/nostrify';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useAppContext } from '@/hooks/useAppContext';
import { useToast } from '@/hooks/useToast';
// Default relays for new users
const NEW_USER_RELAYS = [
{ url: 'wss://relay.ditto.pub', read: true, write: true },
{ url: 'wss://goskatespots.nostr1.com', read: true, write: true },
{ url: 'wss://wotr.relatr.xyz', read: true, write: true },
{ url: 'wss://purplepag.es', read: true, write: false }, // Profile aggregator
];
// Fallback relays for fetching profile data (aggregators that index many profiles)
const FALLBACK_RELAYS = [
'wss://purplepag.es',
'wss://relay.nostr.band',
'wss://relay.damus.io',
'wss://nos.lol',
];
/**
* NostrSync - Syncs user's Nostr data on login
*
* This component runs globally to sync various Nostr data when the user logs in.
* It fetches data and populates TanStack Query cache so other hooks don't need to refetch.
*
* Currently syncs:
* - NIP-65 relay list (kind 10002)
* - Profile metadata (kind 0) - cached for useAuthor hook
*
* New users (without a published relay list) get default relays instead of
* inheriting from previous browser sessions.
*/
export function NostrSync() {
const { nostr } = useNostr();
const { user } = useCurrentUser();
const { logins } = useNostrLogin();
const { config, updateConfig } = useAppContext();
const { toast } = useToast();
const queryClient = useQueryClient();
const syncedUsers = useRef<Set<string>>(new Set());
const [syncStatus, setSyncStatus] = useState<'idle' | 'syncing' | 'success' | 'error'>('idle');
useEffect(() => {
if (!user) return;
// Only sync once per user per session
if (syncedUsers.current.has(user.pubkey)) return;
const syncWithRetry = async <T,>(
queryFn: () => Promise<T>,
maxRetries: number = 5,
initialDelayMs: number = 1000
): Promise<T | null> => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await queryFn();
} catch (error) {
console.warn(`[NostrSync] Attempt ${attempt}/${maxRetries} failed:`, error);
if (attempt < maxRetries) {
// Exponential backoff: 1s, 2s, 4s, 8s, 16s
const delay = initialDelayMs * Math.pow(2, attempt - 1);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
return null;
};
const syncUserData = async () => {
setSyncStatus('syncing');
console.log('[NostrSync] Starting sync for user:', user.pubkey.slice(0, 8) + '...');
// FIX B: Invalidate any stale cached data for this user BEFORE fetching
// This ensures we don't use old cached empty results
queryClient.invalidateQueries({ queryKey: ['author', user.pubkey] });
// Also invalidate the logins cache so useLoggedInAccounts refetches
const loginsKey = logins.map((l) => l.id).join(';');
queryClient.invalidateQueries({ queryKey: ['nostr', 'logins', loginsKey] });
let relaysSynced = false;
let profileSynced = false;
// Sync relay list (NIP-65)
const syncRelays = async (): Promise<boolean> => {
const events = await nostr.query(
[{ kinds: [10002], authors: [user.pubkey], limit: 1 }],
{ signal: AbortSignal.timeout(10000) }
);
if (events.length > 0) {
const event = events[0];
// Only update if the event is newer than our stored data
if (event.created_at > config.relayMetadata.updatedAt) {
const fetchedRelays = event.tags
.filter(([name]) => name === 'r')
.map(([, url, marker]) => ({
url,
read: !marker || marker === 'read',
write: !marker || marker === 'write',
}));
if (fetchedRelays.length > 0) {
console.log('[NostrSync] Synced relay list from NIP-65:', fetchedRelays.length, 'relays');
updateConfig((current) => ({
...current,
relayMetadata: {
relays: fetchedRelays,
updatedAt: event.created_at,
},
}));
return true;
}
} else {
return true; // Already have latest
}
} else {
// No NIP-65 relay list found - this is a new user!
console.log('[NostrSync] New user - setting default relays');
updateConfig((current) => ({
...current,
relayMetadata: {
relays: NEW_USER_RELAYS,
updatedAt: 0,
},
}));
return true;
}
return false;
};
// Sync profile (kind 0) and populate useAuthor cache
const syncProfile = async (relayGroup?: ReturnType<typeof nostr.group>): Promise<boolean> => {
const querySource = relayGroup || nostr;
console.log('[NostrSync] Querying profile for pubkey:', user.pubkey.slice(0, 8) + '...');
const events = await querySource.query(
[{ kinds: [0], authors: [user.pubkey], limit: 1 }],
{ signal: AbortSignal.timeout(10000) }
);
console.log('[NostrSync] Profile query returned', events.length, 'events');
if (events.length > 0) {
const event = events[0];
// FIX A: Parse metadata and populate useAuthor's TanStack Query cache
try {
const metadata = n.json().pipe(n.metadata()).parse(event.content);
// Set the cache data that useAuthor expects
queryClient.setQueryData(['author', user.pubkey], {
metadata,
event
});
// FIX A+: Also update useLoggedInAccounts cache so profile shows everywhere
const loginsKey = logins.map((l) => l.id).join(';');
queryClient.setQueryData(['nostr', 'logins', loginsKey], (oldData: Array<{ id: string; pubkey: string; metadata: typeof metadata; event?: typeof event }> | undefined) => {
if (!oldData) {
// Create initial data from logins
return logins.map((login) => {
if (login.pubkey === user.pubkey) {
return { id: login.id, pubkey: login.pubkey, metadata, event };
}
return { id: login.id, pubkey: login.pubkey, metadata: {} };
});
}
// Update existing data
return oldData.map((account) => {
if (account.pubkey === user.pubkey) {
return { ...account, metadata, event };
}
return account;
});
});
console.log('[NostrSync] Profile synced and cached:', metadata.name || 'unnamed');
return true;
} catch (parseError) {
// Event exists but content couldn't be parsed - still cache the event
queryClient.setQueryData(['author', user.pubkey], { event });
console.warn('[NostrSync] Profile event found but metadata parse failed');
return true;
}
}
return false;
};
// Try main relays first with retry
const mainResult = await syncWithRetry(async () => {
const [relayResult, profileResult] = await Promise.all([
syncRelays(),
syncProfile()
]);
return { relays: relayResult, profile: profileResult };
}, 3, 1500);
if (mainResult) {
relaysSynced = mainResult.relays;
profileSynced = mainResult.profile;
}
// If profile not found on main relays, try fallback relays
if (!profileSynced) {
console.log('[NostrSync] Profile not found on main relays, trying fallbacks...');
const fallbackResult = await syncWithRetry(async () => {
const fallbackGroup = nostr.group(FALLBACK_RELAYS);
return await syncProfile(fallbackGroup);
}, 3, 2000);
if (fallbackResult) {
profileSynced = true;
}
}
// Mark sync complete for this user
syncedUsers.current.add(user.pubkey);
if (relaysSynced || profileSynced) {
setSyncStatus('success');
console.log('[NostrSync] Sync completed - relays:', relaysSynced, 'profile:', profileSynced);
} else {
setSyncStatus('error');
console.error('[NostrSync] Sync failed - could not reach relays');
toast({
title: 'Connection Issue',
description: 'Could not sync your profile. Check your internet connection.',
variant: 'destructive',
});
}
};
// FIX C: Longer initial delay to allow relay connections to establish
// This helps with external signers like Amber where PWA context may need time
// to re-establish WebSocket connections after returning from the signer app
const timeoutId = setTimeout(() => {
syncUserData();
}, 800);
return () => clearTimeout(timeoutId);
}, [user, config.relayMetadata.updatedAt, nostr, updateConfig, toast, queryClient, logins]);
return null;
}