diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 94aa780..66f8d48 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -1,7 +1,7 @@ // app/(tabs)/index.tsx import React, { useState, useEffect, useCallback } from 'react'; -import { ScrollView, View, TouchableOpacity } from 'react-native' -import { useFocusEffect } from '@react-navigation/native'; +import { ScrollView, View, TouchableOpacity, Platform } from 'react-native' +import { useFocusEffect, useTheme } from '@react-navigation/native'; import { router } from 'expo-router' import { AlertDialog, @@ -15,8 +15,6 @@ import { import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { TabScreen } from '@/components/layout/TabScreen' import Header from '@/components/Header' -import HomeWorkout from '@/components/workout/HomeWorkout' -import FavoriteTemplate from '@/components/workout/FavoriteTemplate' import { useWorkoutStore } from '@/stores/workoutStore' import { Text } from '@/components/ui/text' import { getRandomWorkoutTitle } from '@/utils/workoutTitles' @@ -63,6 +61,8 @@ export default function WorkoutScreen() { endWorkout } = useWorkoutStore(); + const theme = useTheme(); + useFocusEffect( useCallback(() => { loadFavorites(); @@ -241,7 +241,10 @@ export default function WorkoutScreen() { onPress={() => console.log('Open notifications')} > - + diff --git a/app/(tabs)/library/programs.tsx b/app/(tabs)/library/programs.tsx index 7d0e413..76517cc 100644 --- a/app/(tabs)/library/programs.tsx +++ b/app/(tabs)/library/programs.tsx @@ -161,74 +161,28 @@ export default function ProgramsScreen() { console.warn('[Database Reset] Error clearing keys:', keyError); } - // Define explicit type for tables - let tables: { name: string }[] = []; - - // Try to get existing tables - try { - tables = await db.getAllAsync<{ name: string }>( - "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'" - ); - console.log(`[Database Reset] Found ${tables.length} tables to drop`); - } catch (tableListError) { - console.warn('[Database Reset] Error listing tables:', tableListError); - // Initialize with empty array if query fails - tables = []; - } - - // Drop tables one by one - for (const table of tables) { - try { - await db.execAsync(`DROP TABLE IF EXISTS "${table.name}";`); - console.log(`[Database Reset] Dropped table: ${table.name}`); - } catch (dropError) { - console.error(`[Database Reset] Error dropping table ${table.name}:`, dropError); - } - } - - // Use a delay to allow any pending operations to complete - await new Promise(resolve => setTimeout(resolve, 1000)); - - // Create a completely new database instance instead of using the existing one - // This will bypass the "Access to closed resource" issue - Alert.alert( - 'Database Tables Dropped', - 'All database tables have been dropped. The app needs to be restarted to complete the reset process.', - [ - { - text: 'Restart Now', - style: 'destructive', - onPress: () => { - // In a production app, you would use something like RN's DevSettings.reload() - // For Expo, we'll suggest manual restart - Alert.alert( - 'Manual Restart Required', - 'Please completely close the app and reopen it to finish the database reset.', - [{ text: 'OK', style: 'default' }] - ); - } - } - ] - ); + // Use the new complete reset method + await schema.resetDatabaseCompletely(db); + // Show success message setTestResults({ success: true, - message: 'Database tables dropped. Please restart the app to complete the reset.' + message: 'Database completely reset. The app will need to be restarted to see the changes.' }); + // Recommend a restart + Alert.alert( + 'Database Reset Complete', + 'The database has been completely reset. Please restart the app for the changes to take effect fully.', + [{ text: 'OK', style: 'default' }] + ); + } catch (error) { console.error('[Database Reset] Error resetting database:', error); setTestResults({ success: false, message: error instanceof Error ? error.message : 'Unknown error during database reset' }); - - // Still recommend a restart since the database might be in an inconsistent state - Alert.alert( - 'Database Reset Error', - 'There was an error during database reset. Please restart the app and try again.', - [{ text: 'OK', style: 'default' }] - ); } }; diff --git a/components/DatabaseProvider.tsx b/components/DatabaseProvider.tsx index 8bb624e..80f50ea 100644 --- a/components/DatabaseProvider.tsx +++ b/components/DatabaseProvider.tsx @@ -36,6 +36,32 @@ interface DatabaseProviderProps { children: React.ReactNode; } +// Add a DelayedInitializer component to ensure database is fully ready +const DelayedInitializer: React.FC<{children: React.ReactNode}> = ({children}) => { + const [ready, setReady] = React.useState(false); + + React.useEffect(() => { + // Small delay to ensure database is fully ready + const timer = setTimeout(() => { + console.log('[Database] Delayed initialization complete'); + setReady(true); + }, 300); // 300ms delay should be sufficient + + return () => clearTimeout(timer); + }, []); + + if (!ready) { + return ( + + + Finishing initialization... + + ); + } + + return <>{children}; +}; + export function DatabaseProvider({ children }: DatabaseProviderProps) { const [isReady, setIsReady] = React.useState(false); const [error, setError] = React.useState(null); @@ -63,11 +89,19 @@ export function DatabaseProvider({ children }: DatabaseProviderProps) { React.useEffect(() => { async function initDatabase() { try { + console.log('[DB] Starting database initialization...'); + + // Add a small delay to ensure system is ready (especially on Android) + await new Promise(resolve => setTimeout(resolve, 200)); + console.log('[DB] Opening database...'); const db = openDatabaseSync('powr.db'); console.log('[DB] Creating schema...'); await schema.createTables(db); + + // Explicitly check for critical tables after schema creation + await schema.ensureCriticalTablesExist(db); // Initialize services console.log('[DB] Initializing services...'); @@ -101,7 +135,12 @@ export function DatabaseProvider({ children }: DatabaseProviderProps) { // Seed development database if (__DEV__) { console.log('[DB] Seeding development database...'); - await devSeeder.seedDatabase(); + try { + await devSeeder.seedDatabase(); + } catch (seedError) { + console.error('[DB] Error seeding database:', seedError); + // Continue even if seeding fails + } await logDatabaseInfo(); } @@ -137,7 +176,9 @@ export function DatabaseProvider({ children }: DatabaseProviderProps) { return ( - {children} + + {children} + ); diff --git a/components/Header.tsx b/components/Header.tsx index 9ee0b6d..d8160ff 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -77,7 +77,7 @@ export default function Header({ onPress={() => {}} > - + {/* Notification indicator - you can conditionally render this */} diff --git a/components/RelayManagement.tsx b/components/RelayManagement.tsx index 0f96772..95d37bb 100644 --- a/components/RelayManagement.tsx +++ b/components/RelayManagement.tsx @@ -1,23 +1,31 @@ +// components/RelayManagement.tsx import React, { useEffect, useState } from 'react'; -import { View, Text, FlatList, TouchableOpacity, Modal, ActivityIndicator } from 'react-native'; +import { View, Text, FlatList, TouchableOpacity, Modal, ActivityIndicator, TextInput, SafeAreaView, KeyboardAvoidingView, Platform } from 'react-native'; import { useRelayStore } from '@/lib/stores/relayStore'; import { Switch } from '@/components/ui/switch'; import { Button } from '@/components/ui/button'; -import { X } from 'lucide-react-native'; +import { X, RefreshCw, PlusCircle, AlertTriangle } from 'lucide-react-native'; import { RelayWithStatus } from '@/lib/db/services/RelayService'; -// Define proper interface for component props interface RelayManagementProps { isVisible: boolean; onClose: () => void; } -// Simple RelayManagement component export default function RelayManagement({ isVisible, onClose }: RelayManagementProps) { const relays = useRelayStore(state => state.relays); const isLoading = useRelayStore(state => state.isLoading); + const isSaving = useRelayStore(state => state.isSaving); const loadRelays = useRelayStore(state => state.loadRelays); const updateRelay = useRelayStore(state => state.updateRelay); + const applyChanges = useRelayStore(state => state.applyChanges); + const resetToDefaults = useRelayStore(state => state.resetToDefaults); + const addRelay = useRelayStore(state => state.addRelay); + + const [newRelayUrl, setNewRelayUrl] = useState(''); + const [showAddRelay, setShowAddRelay] = useState(false); + const [confirmReset, setConfirmReset] = useState(false); + const [hasUnappliedChanges, setHasUnappliedChanges] = useState(false); useEffect(() => { if (isVisible) { @@ -25,7 +33,52 @@ export default function RelayManagement({ isVisible, onClose }: RelayManagementP } }, [isVisible]); - // Status indicator color with proper typing + // Track if there are unapplied changes + const handleRelayUpdate = (url: string, changes: Partial) => { + updateRelay(url, changes); + setHasUnappliedChanges(true); + }; + + // Handle applying changes + const handleApplyChanges = async () => { + const success = await applyChanges(); + if (success) { + setHasUnappliedChanges(false); + // Success notification could be added here + } + }; + + // Add new relay + const handleAddRelay = async () => { + if (!newRelayUrl || !newRelayUrl.startsWith('wss://')) { + alert('Please enter a valid relay URL starting with wss://'); + return; + } + + try { + await addRelay(newRelayUrl); + setNewRelayUrl(''); + setShowAddRelay(false); + setHasUnappliedChanges(true); + } catch (error) { + alert(`Failed to add relay: ${error instanceof Error ? error.message : String(error)}`); + } + }; + + // Reset to defaults with confirmation + const handleResetToDefaults = async () => { + if (confirmReset) { + await resetToDefaults(); + setConfirmReset(false); + setHasUnappliedChanges(true); + } else { + setConfirmReset(true); + // Auto-reset the confirmation after 3 seconds + setTimeout(() => setConfirmReset(false), 3000); + } + }; + + // Status indicator color const getStatusColor = (status: string): string => { switch (status) { case 'connected': return '#10b981'; // Green @@ -36,58 +89,49 @@ export default function RelayManagement({ isVisible, onClose }: RelayManagementP } }; - // Render a relay item with proper typing + // Render a relay item const renderRelayItem = ({ item }: { item: RelayWithStatus }) => ( - - - - - {item.url} + + + + + {item.url} - + {item.status} - - - Read + + + Read updateRelay(item.url, { read: !item.read })} + onCheckedChange={() => handleRelayUpdate(item.url, { read: !item.read })} /> - - Write + + Write updateRelay(item.url, { write: !item.write })} + onCheckedChange={() => handleRelayUpdate(item.url, { write: !item.write })} /> ); + // Main Render return ( - - - - Manage Relays + + + {/* Header */} + + Relay Management - + - {isLoading ? ( - - - Loading relays... - - ) : ( - <> - item.url} - renderItem={renderRelayItem} - style={{ maxHeight: '70%' }} - /> - - - - - + {/* Content */} + + {isLoading ? ( + + + Loading relays... - - )} - - + ) : ( + <> + {/* Summary */} + + + {relays.length} Relays ({relays.filter(r => r.status === 'connected').length} Connected) + + + + + + + {/* Relay List */} + {relays.length === 0 ? ( + + No relays configured + + + ) : ( + item.url} + ListEmptyComponent={ + + No relays found + + } + contentContainerStyle={{ paddingBottom: showAddRelay ? 180 : 100 }} + /> + )} + + {/* Add Relay Form */} + {showAddRelay && ( + + Add New Relay + + + + + + + )} + + )} + + + {/* Footer */} + + + + + + + + + + + ); } \ No newline at end of file diff --git a/lib/db/schema.ts b/lib/db/schema.ts index ff02c9b..0b8faeb 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -2,7 +2,7 @@ import { SQLiteDatabase } from 'expo-sqlite'; import { Platform } from 'react-native'; -export const SCHEMA_VERSION = 3; // Incrementing to add the relays table +export const SCHEMA_VERSION = 6; // Incremented from 5 to 6 for relay table removal class Schema { private async getCurrentVersion(db: SQLiteDatabase): Promise { @@ -44,9 +44,17 @@ class Schema { const currentVersion = await this.getCurrentVersion(db); console.log(`[Schema] Current version: ${currentVersion}, Target version: ${SCHEMA_VERSION}`); - // If we already have the current version, no need to recreate tables + // If we already have the current version, check for missing tables if (currentVersion === SCHEMA_VERSION) { - console.log(`[Schema] Database already at version ${SCHEMA_VERSION}`); + console.log(`[Schema] Database already at version ${SCHEMA_VERSION}, checking for missing tables`); + await this.ensureCriticalTablesExist(db); + return; + } + + // Handle higher version numbers - especially important for Android + if (currentVersion > SCHEMA_VERSION) { + console.log(`[Schema] Database version ${currentVersion} is newer than target ${SCHEMA_VERSION}, checking for missing tables`); + await this.ensureCriticalTablesExist(db); return; } @@ -89,6 +97,129 @@ class Schema { } } + // Add this method to check for and create critical tables + async ensureCriticalTablesExist(db: SQLiteDatabase): Promise { + try { + console.log('[Schema] Checking for missing critical tables...'); + + // Check if workouts table exists + const workoutsTableExists = await db.getFirstAsync<{ count: number }>( + `SELECT count(*) as count FROM sqlite_master + WHERE type='table' AND name='workouts'` + ); + + if (!workoutsTableExists || workoutsTableExists.count === 0) { + console.log('[Schema] Creating missing workouts tables...'); + + // Create workouts table + await db.execAsync(` + CREATE TABLE IF NOT EXISTS workouts ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + type TEXT NOT NULL, + start_time INTEGER NOT NULL, + end_time INTEGER, + is_completed BOOLEAN NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + template_id TEXT, + nostr_event_id TEXT, + share_status TEXT NOT NULL DEFAULT 'local', + source TEXT NOT NULL DEFAULT 'local', + total_volume REAL, + total_reps INTEGER, + notes TEXT + ); + CREATE INDEX IF NOT EXISTS idx_workouts_start_time ON workouts(start_time); + CREATE INDEX IF NOT EXISTS idx_workouts_template_id ON workouts(template_id); + `); + + // Create workout_exercises table + await db.execAsync(` + CREATE TABLE IF NOT EXISTS workout_exercises ( + id TEXT PRIMARY KEY, + workout_id TEXT NOT NULL, + exercise_id TEXT NOT NULL, + display_order INTEGER NOT NULL, + notes TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + FOREIGN KEY(workout_id) REFERENCES workouts(id) ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS idx_workout_exercises_workout_id ON workout_exercises(workout_id); + `); + + // Create workout_sets table + await db.execAsync(` + CREATE TABLE IF NOT EXISTS workout_sets ( + id TEXT PRIMARY KEY, + workout_exercise_id TEXT NOT NULL, + type TEXT NOT NULL DEFAULT 'normal', + weight REAL, + reps INTEGER, + rpe REAL, + duration INTEGER, + is_completed BOOLEAN NOT NULL DEFAULT 0, + completed_at INTEGER, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + FOREIGN KEY(workout_exercise_id) REFERENCES workout_exercises(id) ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS idx_workout_sets_exercise_id ON workout_sets(workout_exercise_id); + `); + } + + // Check if templates table exists + const templatesTableExists = await db.getFirstAsync<{ count: number }>( + `SELECT count(*) as count FROM sqlite_master + WHERE type='table' AND name='templates'` + ); + + if (!templatesTableExists || templatesTableExists.count === 0) { + console.log('[Schema] Creating missing templates tables...'); + + // Create templates table + await db.execAsync(` + CREATE TABLE IF NOT EXISTS templates ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + type TEXT NOT NULL, + description TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + nostr_event_id TEXT, + source TEXT NOT NULL DEFAULT 'local', + parent_id TEXT + ); + CREATE INDEX IF NOT EXISTS idx_templates_updated_at ON templates(updated_at); + `); + + // Create template_exercises table + await db.execAsync(` + CREATE TABLE IF NOT EXISTS template_exercises ( + id TEXT PRIMARY KEY, + template_id TEXT NOT NULL, + exercise_id TEXT NOT NULL, + display_order INTEGER NOT NULL, + target_sets INTEGER, + target_reps INTEGER, + target_weight REAL, + notes TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + FOREIGN KEY(template_id) REFERENCES templates(id) ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS idx_template_exercises_template_id ON template_exercises(template_id); + `); + } + + console.log('[Schema] Critical tables check complete'); + } catch (error) { + console.error('[Schema] Error ensuring critical tables exist:', error); + throw error; + } + } + private async dropAllTables(db: SQLiteDatabase): Promise { try { console.log('[Schema] Getting list of tables to drop...'); @@ -288,20 +419,6 @@ class Schema { CREATE INDEX idx_favorites_content_id ON favorites(content_id); `); - // Create relays table - console.log('[Schema] Creating relays table...'); - await db.execAsync(` - CREATE TABLE relays ( - url TEXT PRIMARY KEY, - read INTEGER NOT NULL DEFAULT 1, - write INTEGER NOT NULL DEFAULT 1, - priority INTEGER, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ); - CREATE INDEX idx_relays_priority ON relays(priority DESC); - `); - // === NEW TABLES === // // Create workouts table @@ -427,25 +544,36 @@ class Schema { } } - async resetDatabase(db: SQLiteDatabase): Promise { + async resetDatabaseCompletely(db: SQLiteDatabase): Promise { if (!__DEV__) { console.log('[Schema] Database reset is only available in development mode'); return; } try { - console.log('[Schema] Resetting database...'); + console.log('[Schema] Completely resetting database...'); - // Clear schema_version to force recreation of all tables - await db.execAsync('DROP TABLE IF EXISTS schema_version'); - console.log('[Schema] Dropped schema_version table'); + // Get all tables + const tables = await db.getAllAsync<{ name: string }>( + "SELECT name FROM sqlite_master WHERE type='table'" + ); - // Now create tables from scratch + // Drop all tables including schema_version + for (const { name } of tables) { + try { + await db.execAsync(`DROP TABLE IF EXISTS ${name}`); + console.log(`[Schema] Dropped table: ${name}`); + } catch (dropError) { + console.error(`[Schema] Error dropping table ${name}:`, dropError); + } + } + + // Create tables from scratch await this.createTables(db); - console.log('[Schema] Database reset complete'); + console.log('[Schema] Database completely reset'); } catch (error) { - console.error('[Schema] Error resetting database:', error); + console.error('[Schema] Error completely resetting database:', error); throw error; } } diff --git a/lib/db/services/DevSeederService.ts b/lib/db/services/DevSeederService.ts index 6dd5dce..8889221 100644 --- a/lib/db/services/DevSeederService.ts +++ b/lib/db/services/DevSeederService.ts @@ -8,6 +8,7 @@ import { logDatabaseInfo } from '../debug'; import { mockExerciseEvents, convertNostrToExercise } from '../../mocks/exercises'; import { DbService } from '../db-service'; import NDK, { NDKEvent } from '@nostr-dev-kit/ndk-mobile'; +import { NostrEvent } from '@/types/nostr'; // Assuming you have this type defined export class DevSeederService { private db: SQLiteDatabase; @@ -181,6 +182,71 @@ export class DevSeederService { } } + /** + * Seed the database with real events from Nostr relays instead of mock data + * @param filter The filter to use when fetching events from relays + * @param limit Maximum number of events to seed (optional) + */ + async seedFromNostr(filter: any, limit?: number) { + if (!this.ndk) { + console.log('NDK not available for seeding from Nostr'); + return; + } + + try { + console.log(`Seeding from Nostr with filter:`, filter); + + // Fetch events from relays + const events = await this.ndk.fetchEvents(filter); + + console.log(`Found ${events.size} events on Nostr`); + + // Convert to array and limit if needed + const eventsArray = Array.from(events); + const eventsToProcess = limit ? eventsArray.slice(0, limit) : eventsArray; + + // Process each event individually + let successCount = 0; + + for (const ndkEvent of eventsToProcess) { + try { + // Convert NDKEvent to your NostrEvent format + const nostrEvent: NostrEvent = { + id: ndkEvent.id || '', + pubkey: ndkEvent.pubkey || '', + created_at: ndkEvent.created_at || 0, // Set a default value of 0 if undefined + kind: ndkEvent.kind || 0, + tags: ndkEvent.tags || [], + content: ndkEvent.content || '', + sig: ndkEvent.sig || '' + }; + + // Cache the event + if (this.eventCache) { + await this.eventCache.setEvent(nostrEvent, true); + } + + // Process based on kind + if (ndkEvent.kind === 33401) { // Exercise + const exercise = convertNostrToExercise(nostrEvent); + await this.exerciseService.createExercise(exercise, true); + successCount++; + } + // Add more event type processing here as needed + + } catch (error) { + console.error(`Error processing Nostr event:`, error); + // Continue with next event + } + } + + console.log(`Successfully seeded ${successCount} items from Nostr`); + + } catch (error) { + console.error('Error seeding from Nostr:', error); + } + } + async clearDatabase() { if (!__DEV__) return; diff --git a/lib/db/services/RelayService.ts b/lib/db/services/RelayService.ts index 52d11a8..1f5712e 100644 --- a/lib/db/services/RelayService.ts +++ b/lib/db/services/RelayService.ts @@ -1,16 +1,5 @@ // lib/db/services/RelayService.ts -import { SQLiteDatabase } from 'expo-sqlite'; -import { NDKCommon, NDKRelayCommon, safeAddRelay, safeRemoveRelay } from '@/types/ndk-common'; - -// Status constants to match NDK implementations -const NDK_RELAY_STATUS = { - CONNECTING: 0, - CONNECTED: 1, - DISCONNECTING: 2, - DISCONNECTED: 3, - RECONNECTING: 4, - AUTH_REQUIRED: 5 -}; +import NDK, { NDKRelay } from '@nostr-dev-kit/ndk-mobile'; // Default relays to use when none are configured export const DEFAULT_RELAYS = [ @@ -26,8 +15,8 @@ export interface RelayConfig { read: boolean; write: boolean; priority?: number; - created_at: number; - updated_at: number; + created_at?: number; + updated_at?: number; } export interface RelayWithStatus extends RelayConfig { @@ -36,141 +25,106 @@ export interface RelayWithStatus extends RelayConfig { /** * Service for managing Nostr relays + * This implementation uses NDK's built-in relay management capabilities */ export class RelayService { - private db: SQLiteDatabase; - private ndk: NDKCommon | null = null; + private ndk: NDK | null = null; private debug: boolean = false; - constructor(db: SQLiteDatabase) { - this.db = db; + constructor() { + // No database required anymore } enableDebug() { - this.debug = true; - console.log('[RelayService] Debug mode enabled'); - } + this.debug = true; + console.log('[RelayService] Debug mode enabled'); + } - private logDebug(message: string, ...args: any[]) { - if (this.debug) { - console.log(`[RelayService Debug] ${message}`, ...args); - } + private logDebug(message: string, ...args: any[]) { + if (this.debug) { + console.log(`[RelayService Debug] ${message}`, ...args); } + } /** * Set NDK instance for relay operations */ - setNDK(ndk: NDKCommon) { + setNDK(ndk: NDK) { this.ndk = ndk; console.log('[RelayService] NDK instance set'); } - /** - * Get all relays from database - */ - async getAllRelays(): Promise { - try { - const relays = await this.db.getAllAsync( - 'SELECT url, read, write, priority, created_at, updated_at FROM relays ORDER BY priority DESC, created_at DESC' - ); - - console.log(`[RelayService] Found ${relays.length} relays in database`); - - return relays.map(relay => ({ - ...relay, - read: Boolean(relay.read), - write: Boolean(relay.write) - })); - } catch (error) { - console.error('[RelayService] Error getting relays:', error); - return []; - } - } - /** * Get all relays with their current connection status */ async getAllRelaysWithStatus(): Promise { try { - const relays = await this.getAllRelays(); - if (!this.ndk) { - console.warn('[RelayService] NDK not initialized, returning relays with disconnected status'); - return relays.map(relay => ({ - ...relay, - status: 'disconnected' - })); + console.warn('[RelayService] NDK not initialized, returning empty relay list'); + return []; } - // Log the relays in the NDK pool for debugging - console.log('[RelayService] Checking status for relays. Current NDK pool:'); - this.ndk.pool.relays.forEach((ndkRelay, url) => { - console.log(` - ${url}: status=${ndkRelay.status}`); + const relays: RelayWithStatus[] = []; + + // Get relays directly from NDK pool + this.ndk.pool.relays.forEach((relay, url) => { + const status = this.getRelayStatus(relay); + + relays.push({ + url, + read: relay.read ?? true, + write: relay.write ?? true, + status, + priority: 0, // Default priority + created_at: Date.now(), + updated_at: Date.now() + }); }); - return relays.map(relay => { - const status = this.getRelayStatus(relay); - console.log(`[RelayService] Status for relay ${relay.url}: ${status}`); - - return { - ...relay, - status - }; - }); + console.log(`[RelayService] Found ${relays.length} relays in NDK pool`); + return relays; } catch (error) { console.error('[RelayService] Error getting relays with status:', error); return []; } } - private normalizeRelayUrl(url: string): string { - // Remove trailing slash if present - return url.replace(/\/$/, ''); - } - /** - * Add a new relay to the database + * Add a new relay to NDK */ - async addRelay(url: string, read = true, write = true, priority?: number): Promise { + async addRelay(url: string, read = true, write = true): Promise { try { - // Normalize the URL - url = this.normalizeRelayUrl(url.trim()); + if (!this.ndk) { + throw new Error('NDK not initialized'); + } + + // Normalize URL by removing trailing slash + url = url.replace(/\/$/, ''); // Validate URL format if (!url.startsWith('wss://')) { throw new Error('Relay URL must start with wss://'); } - const now = Date.now(); + console.log(`[RelayService] Adding relay ${url} with read=${read}, write=${write}`); - // Check if relay already exists - const existingRelay = await this.db.getFirstAsync<{ url: string }>( - 'SELECT url FROM relays WHERE url = ?', - [url] - ); + // Get or create the relay using the NDK pool + const relay = this.ndk.pool.getRelay(url, true); - if (existingRelay) { - console.log(`[RelayService] Relay ${url} already exists, updating instead`); - return this.updateRelay(url, { read, write, priority }); + // Set read/write permissions + relay.read = read; + relay.write = write; + + // Connect to the relay if not already connected + if (!relay.connected) { + try { + await relay.connect(); + } catch (connectError) { + console.warn(`[RelayService] Warning: Error connecting to relay ${url}:`, connectError); + // Continue even if connection fails - it will auto-reconnect + } } - // If no priority specified, make it higher than the current highest - if (priority === undefined) { - const highestPriority = await this.db.getFirstAsync<{ priority: number }>( - 'SELECT MAX(priority) as priority FROM relays' - ); - - priority = ((highestPriority?.priority || 0) + 1); - } - - console.log(`[RelayService] Adding relay ${url} with read=${read}, write=${write}, priority=${priority}`); - - // Add the relay - await this.db.runAsync( - 'INSERT INTO relays (url, read, write, priority, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)', - [url, read ? 1 : 0, write ? 1 : 0, priority, now, now] - ); - console.log(`[RelayService] Successfully added relay ${url}`); return true; } catch (error) { @@ -184,63 +138,30 @@ export class RelayService { */ async updateRelay(url: string, changes: Partial): Promise { try { - const now = Date.now(); - - // Check if relay exists - const existingRelay = await this.db.getFirstAsync<{ url: string }>( - 'SELECT url FROM relays WHERE url = ?', - [url] - ); - - if (!existingRelay) { - console.log(`[RelayService] Relay ${url} does not exist, adding instead`); - const read = changes.read !== undefined ? changes.read : true; - const write = changes.write !== undefined ? changes.write : true; - return this.addRelay(url, read, write, changes.priority); + if (!this.ndk) { + throw new Error('NDK not initialized'); } - // Prepare update fields - const updates: string[] = []; - const params: any[] = []; + // Get the relay from NDK pool + const relay = this.ndk.pool.getRelay(url, false); + if (!relay) { + console.log(`[RelayService] Relay ${url} not found, adding instead`); + const read = changes.read !== undefined ? changes.read : true; + const write = changes.write !== undefined ? changes.write : true; + return this.addRelay(url, read, write); + } + + // Update relay properties if (changes.read !== undefined) { - updates.push('read = ?'); - params.push(changes.read ? 1 : 0); + relay.read = changes.read; } if (changes.write !== undefined) { - updates.push('write = ?'); - params.push(changes.write ? 1 : 0); + relay.write = changes.write; } - if (changes.priority !== undefined) { - updates.push('priority = ?'); - params.push(changes.priority); - } - - // Always update the updated_at timestamp - updates.push('updated_at = ?'); - params.push(now); - - // Add the URL to the parameters - params.push(url); - - console.log(`[RelayService] Updating relay ${url} with changes:`, - Object.entries(changes) - .filter(([key]) => ['read', 'write', 'priority'].includes(key)) - .map(([key, value]) => `${key}=${value}`) - .join(', ') - ); - - // Execute update - if (updates.length > 0) { - await this.db.runAsync( - `UPDATE relays SET ${updates.join(', ')} WHERE url = ?`, - params - ); - } - - console.log(`[RelayService] Successfully updated relay ${url}`); + console.log(`[RelayService] Updated relay ${url} with new settings`); return true; } catch (error) { console.error('[RelayService] Error updating relay:', error); @@ -249,130 +170,73 @@ export class RelayService { } /** - * Remove a relay from the database + * Remove a relay from NDK */ async removeRelay(url: string): Promise { try { + if (!this.ndk) { + throw new Error('NDK not initialized'); + } + console.log(`[RelayService] Removing relay ${url}`); - await this.db.runAsync('DELETE FROM relays WHERE url = ?', [url]); - console.log(`[RelayService] Successfully removed relay ${url}`); - return true; + + // Use NDK pool's removeRelay method + const success = this.ndk.pool.removeRelay(url); + + if (success) { + console.log(`[RelayService] Successfully removed relay ${url}`); + } else { + console.log(`[RelayService] Relay ${url} was not found in the pool`); + } + + return success; } catch (error) { console.error('[RelayService] Error removing relay:', error); throw error; } } - /** - * Get relays that are enabled for reading, writing, or both - */ - async getEnabledRelays(): Promise { - try { - const relays = await this.db.getAllAsync<{ url: string }>( - 'SELECT url FROM relays WHERE read = 1 OR write = 1 ORDER BY priority DESC, created_at DESC' - ); - - console.log(`[RelayService] Found ${relays.length} enabled relays`); - return relays.map(relay => relay.url); - } catch (error) { - console.error('[RelayService] Error getting enabled relays:', error); - return []; - } - } - /** * Apply relay configuration to NDK - * This implementation uses the safeAddRelay and safeRemoveRelay utilities */ - async applyRelayConfig(ndk?: NDKCommon): Promise { + async applyRelayConfig(relays: RelayConfig[]): Promise { try { - // Use provided NDK or the stored one - const ndkInstance = ndk || this.ndk; - - if (!ndkInstance) { + if (!this.ndk) { throw new Error('NDK not initialized'); } - // Get all relay configurations - const relayConfigs = await this.getAllRelays(); - - if (relayConfigs.length === 0) { - console.warn('[RelayService] No relays found, using defaults'); - await this.resetToDefaults(); - return this.applyRelayConfig(ndkInstance); // Recursive call after reset + if (relays.length === 0) { + console.warn('[RelayService] No relays provided, using defaults'); + return this.resetToDefaults(); } - console.log(`[RelayService] Applying configuration for ${relayConfigs.length} relays`); + console.log(`[RelayService] Applying configuration for ${relays.length} relays`); - // Get the current relay URLs - const currentRelayUrls: string[] = []; - try { - ndkInstance.pool.relays.forEach((_, url) => currentRelayUrls.push(url)); - console.log(`[RelayService] NDK currently has ${currentRelayUrls.length} relays`); - } catch (error) { - console.error('[RelayService] Error getting current relay URLs:', error); - } + // Get current relays from NDK + const currentRelays = Array.from(this.ndk.pool.relays.keys()); - // Disconnect from relays that are not in the config or have changed permissions - for (const url of currentRelayUrls) { - // Get config for this URL if it exists - const config = relayConfigs.find(r => r.url === url); - - // If the relay doesn't exist in our config or the read/write status changed, - // we should remove it and possibly add it back with new settings - if (!config || (!config.read && !config.write)) { + // Remove relays that aren't in the new configuration + for (const url of currentRelays) { + if (!relays.find(r => r.url === url)) { console.log(`[RelayService] Removing relay ${url} from NDK pool`); - safeRemoveRelay(ndkInstance, url); + this.ndk.pool.removeRelay(url); } } - // Add or reconfigure relays - for (const relay of relayConfigs) { - if (relay.read || relay.write) { - try { - let ndkRelay = ndkInstance.pool.getRelay(relay.url); - - if (ndkRelay) { - // Update relay's read/write config if needed - try { - const needsUpdate = (ndkRelay.read !== relay.read) || - (ndkRelay.write !== relay.write); - - if (needsUpdate) { - console.log(`[RelayService] Updating relay ${relay.url} settings: read=${relay.read}, write=${relay.write}`); - // Set properties directly - ndkRelay.read = relay.read; - ndkRelay.write = relay.write; - } - } catch (error) { - // If we can't set properties directly, remove and re-add the relay - console.log(`[RelayService] Recreating relay ${relay.url} due to error:`, error); - safeRemoveRelay(ndkInstance, relay.url); - ndkRelay = safeAddRelay(ndkInstance, relay.url, { - read: relay.read, - write: relay.write - }); - } - } else { - // Add new relay - console.log(`[RelayService] Adding new relay ${relay.url} to NDK pool`); - ndkRelay = safeAddRelay(ndkInstance, relay.url, { - read: relay.read, - write: relay.write - }); - } - - // Connect the relay if it was added successfully - if (ndkRelay && typeof ndkRelay.connect === 'function') { - console.log(`[RelayService] Connecting to relay ${relay.url}`); - ndkRelay.connect().catch((error: any) => { - console.error(`[RelayService] Error connecting to relay ${relay.url}:`, error); - }); - } - } catch (innerError) { - console.error(`[RelayService] Error adding/updating relay ${relay.url}:`, innerError); - // Continue with other relays even if one fails - } + // Add or update relays from the configuration + for (const relay of relays) { + const ndkRelay = this.ndk.pool.getRelay(relay.url, false); + + if (ndkRelay) { + // Update existing relay + ndkRelay.read = relay.read; + ndkRelay.write = relay.write; + } else { + // Add new relay + console.log(`[RelayService] Adding relay ${relay.url} to NDK pool`); + const newRelay = this.ndk.pool.getRelay(relay.url, true); + newRelay.read = relay.read; + newRelay.write = relay.write; } } @@ -384,10 +248,75 @@ export class RelayService { } } + /** + * Initialize relays - used during app startup + */ + async initializeRelays(): Promise { + try { + if (!this.ndk) { + throw new Error('NDK not initialized'); + } + + console.log('[RelayService] Initializing relays'); + + // Check if we already have relays in the pool + if (this.ndk.pool.size() === 0) { + // No relays, add the defaults + console.log('[RelayService] No relays found, adding defaults'); + for (const url of DEFAULT_RELAYS) { + const relay = this.ndk.pool.getRelay(url, true); + await relay.connect(); + } + } + + return true; + } catch (error) { + console.error('[RelayService] Error initializing relays:', error); + // Reset to defaults on error + return this.resetToDefaults(); + } + } + + /** + * Reset relays to default set + */ + async resetToDefaults(): Promise { + try { + if (!this.ndk) { + throw new Error('NDK not initialized'); + } + + console.log('[RelayService] Resetting relays to defaults'); + + // Clear existing relays from NDK + const currentRelays = Array.from(this.ndk.pool.relays.keys()); + for (const url of currentRelays) { + this.ndk.pool.removeRelay(url); + } + + // Add default relays + for (const url of DEFAULT_RELAYS) { + const relay = this.ndk.pool.getRelay(url, true); + try { + await relay.connect(); + } catch (error) { + console.error(`[RelayService] Error connecting to relay ${url}:`, error); + // Continue even if connection fails + } + } + + console.log(`[RelayService] Successfully reset to ${DEFAULT_RELAYS.length} default relays`); + return true; + } catch (error) { + console.error('[RelayService] Error resetting relays to defaults:', error); + throw error; + } + } + /** * Import relays from user metadata (kind:3 events) */ - async importFromUserMetadata(pubkey: string, ndk: any): Promise { + async importFromUserMetadata(pubkey: string, ndk: NDK): Promise { try { if (!ndk) { throw new Error('NDK not initialized'); @@ -422,371 +351,50 @@ export class RelayService { console.log(`[RelayService] Found relay list in event created at ${new Date(latestCreatedAt * 1000).toISOString()}`); - // Safely log event details without circular references - try { - console.log('[RelayService] Event ID:', latestEvent.id); - console.log('[RelayService] Event Kind:', latestEvent.kind); - console.log('[RelayService] Event Created At:', latestEvent.created_at); - console.log('[RelayService] Event Tags Count:', latestEvent.tags ? latestEvent.tags.length : 0); - - // Safely log the tags - if (latestEvent.tags && Array.isArray(latestEvent.tags)) { - console.log('[RelayService] Tags:'); - latestEvent.tags.forEach((tag: any[], index: number) => { - console.log(` Tag ${index}:`, JSON.stringify(tag)); - }); - } - } catch (error) { - console.log('[RelayService] Error logging event details:', error); - } + let relaysFound = false; + const relayConfigs: RelayConfig[] = []; - // Get highest current priority - const highestPriority = await this.db.getFirstAsync<{ priority: number }>( - 'SELECT MAX(priority) as priority FROM relays' - ); - - let maxPriority = (highestPriority?.priority || 0); - let importCount = 0; - let updatedCount = 0; - - // Check if any relay tags exist - let relayTagsFound = false; - - // Process each relay in the event + // Process relay tags from event if (latestEvent.tags && Array.isArray(latestEvent.tags)) { for (const tag of latestEvent.tags) { - try { - console.log(`[RelayService] Processing tag: ${JSON.stringify(tag)}`); + if (tag[0] === 'r' && tag.length > 1) { + relaysFound = true; + const url = tag[1]; - // More flexible tag detection - handle 'r', 'R', or 'relay' tag types - if ((tag[0] === 'r' || tag[0] === 'R' || tag[0] === 'relay') && tag.length > 1 && tag[1]) { - relayTagsFound = true; - console.log(`[RelayService] Found relay tag: ${tag[1]}`); - - const url = tag[1]; - - // Ensure URL is properly formatted - if (!url.startsWith('wss://') && !url.startsWith('ws://')) { - console.log(`[RelayService] Skipping invalid relay URL: ${url}`); - continue; - } - - // Check for read/write specification in the tag - let read = true; - let write = true; - - if (tag.length > 2) { - // Handle various common formatting patterns - const readWriteSpec = tag[2]?.toLowerCase(); - if (readWriteSpec === 'write') { - read = false; - write = true; - console.log(`[RelayService] Relay ${url} configured as write-only`); - } else if (readWriteSpec === 'read') { - read = true; - write = false; - console.log(`[RelayService] Relay ${url} configured as read-only`); - } else { - console.log(`[RelayService] Unrecognized read/write spec: ${readWriteSpec}, using default (read+write)`); - } - } - - try { - // Check if the relay already exists - const existingRelay = await this.db.getFirstAsync<{ url: string }>( - 'SELECT url FROM relays WHERE url = ?', - [url] - ); - - const now = Date.now(); - - if (existingRelay) { - // Update existing relay - await this.db.runAsync( - 'UPDATE relays SET read = ?, write = ?, updated_at = ? WHERE url = ?', - [read ? 1 : 0, write ? 1 : 0, now, url] - ); - updatedCount++; - console.log(`[RelayService] Updated existing relay: ${url} (read=${read}, write=${write})`); - } else { - // Add new relay with incremented priority - maxPriority++; - await this.db.runAsync( - 'INSERT INTO relays (url, read, write, priority, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)', - [url, read ? 1 : 0, write ? 1 : 0, maxPriority, now, now] - ); - importCount++; - console.log(`[RelayService] Added new relay: ${url} (read=${read}, write=${write}, priority=${maxPriority})`); - } - } catch (innerError) { - console.error(`[RelayService] Error importing relay ${url}:`, innerError); - // Continue with other relays - } + // Skip invalid URLs + if (!url.startsWith('wss://')) continue; + + // Parse read/write settings + let read = true; + let write = true; + + if (tag.length > 2) { + read = tag[2] !== 'write'; // If "write", then not read + write = tag[2] !== 'read'; // If "read", then not write } - } catch (tagError) { - console.log('[RelayService] Error processing tag:', tagError); + + relayConfigs.push({ url, read, write }); } } } - // Check for relays in content (some clients store them there) - if (!relayTagsFound) { - console.log('[RelayService] No relay tags found in event tags, checking content...'); - - try { - // Only try to parse the content if it's a string - if (typeof latestEvent.content === 'string') { - const contentObj = JSON.parse(latestEvent.content); - - // Only log specific properties to avoid circular references - console.log('[RelayService] Content has relays property:', contentObj.hasOwnProperty('relays')); - - // Some clients store relays in content as an object - if (contentObj.relays && typeof contentObj.relays === 'object') { - console.log('[RelayService] Found relay URLs in content:', Object.keys(contentObj.relays)); - - // Process relays from content object - for (const [url, permissions] of Object.entries(contentObj.relays)) { - try { - if (typeof url === 'string' && (url.startsWith('wss://') || url.startsWith('ws://'))) { - relayTagsFound = true; - - let read = true; - let write = true; - - // Handle different formats of permissions - if (typeof permissions === 'object' && permissions !== null) { - // Format: { "wss://relay.example.com": { "read": true, "write": false } } - if ('read' in permissions) read = Boolean((permissions as any).read); - if ('write' in permissions) write = Boolean((permissions as any).write); - } else if (typeof permissions === 'string') { - // Format: { "wss://relay.example.com": "read" } - read = (permissions as string).includes('read'); - write = (permissions as string).includes('write'); - } - - console.log(`[RelayService] Found relay in content: ${url} (read=${read}, write=${write})`); - - // Then add or update the relay just like above... - try { - const existingRelay = await this.db.getFirstAsync<{ url: string }>( - 'SELECT url FROM relays WHERE url = ?', - [url] - ); - - const now = Date.now(); - - if (existingRelay) { - await this.db.runAsync( - 'UPDATE relays SET read = ?, write = ?, updated_at = ? WHERE url = ?', - [read ? 1 : 0, write ? 1 : 0, now, url] - ); - updatedCount++; - console.log(`[RelayService] Updated existing relay from content: ${url} (read=${read}, write=${write})`); - } else { - maxPriority++; - await this.db.runAsync( - 'INSERT INTO relays (url, read, write, priority, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)', - [url, read ? 1 : 0, write ? 1 : 0, maxPriority, now, now] - ); - importCount++; - console.log(`[RelayService] Added new relay from content: ${url} (read=${read}, write=${write}, priority=${maxPriority})`); - } - } catch (innerError) { - console.error(`[RelayService] Error importing relay ${url} from content:`, innerError); - } - } - } catch (relayError) { - console.log('[RelayService] Error processing relay from content:', relayError); - } - } - } - } else { - console.log('[RelayService] Content is not a string:', typeof latestEvent.content); - } - } catch (e) { - // Convert the unknown error to a string safely - const errorMessage = e instanceof Error ? e.message : String(e); - console.log('[RelayService] Content is not JSON or does not contain relay information:', errorMessage); - } + if (!relaysFound || relayConfigs.length === 0) { + console.log('[RelayService] No relay tags found in event'); + return false; } - // Check the raw event string that might be available - if (!relayTagsFound && latestEvent.rawEvent && typeof latestEvent.rawEvent === 'string') { - console.log('[RelayService] Checking raw event string for relay information'); - try { - const rawEventObj = JSON.parse(latestEvent.rawEvent); - if (rawEventObj.tags && Array.isArray(rawEventObj.tags)) { - console.log(`[RelayService] Raw event has ${rawEventObj.tags.length} tags`); - - for (const tag of rawEventObj.tags) { - try { - if ((tag[0] === 'r' || tag[0] === 'R') && tag.length > 1 && tag[1]) { - relayTagsFound = true; - const url = tag[1]; - - console.log(`[RelayService] Found relay in raw event: ${url}`); - - // Process like above... - if (url.startsWith('wss://') || url.startsWith('ws://')) { - let read = true; - let write = true; - - if (tag.length > 2) { - const readWriteSpec = tag[2]?.toLowerCase(); - if (readWriteSpec === 'write') { - read = false; - write = true; - } else if (readWriteSpec === 'read') { - read = true; - write = false; - } - } - - try { - const existingRelay = await this.db.getFirstAsync<{ url: string }>( - 'SELECT url FROM relays WHERE url = ?', - [url] - ); - - const now = Date.now(); - - if (existingRelay) { - await this.db.runAsync( - 'UPDATE relays SET read = ?, write = ?, updated_at = ? WHERE url = ?', - [read ? 1 : 0, write ? 1 : 0, now, url] - ); - updatedCount++; - } else { - maxPriority++; - await this.db.runAsync( - 'INSERT INTO relays (url, read, write, priority, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)', - [url, read ? 1 : 0, write ? 1 : 0, maxPriority, now, now] - ); - importCount++; - } - } catch (innerError) { - console.error(`[RelayService] Error importing relay ${url} from raw event:`, innerError); - } - } - } - } catch (tagError) { - console.log('[RelayService] Error processing tag from raw event:', tagError); - } - } - } - } catch (rawError) { - // Convert the unknown error to a string safely - const errorMessage = rawError instanceof Error ? rawError.message : String(rawError); - console.log('[RelayService] Error parsing raw event:', errorMessage); - } - } - - // Try to access user cached relays - if (!relayTagsFound && ndk && ndk.pool && ndk.pool.relays) { - console.log('[RelayService] Checking for relays in the user NDK pool'); - - try { - // Try to access the user's connected relays - const userRelays = Array.from(ndk.pool.relays.keys()); - if (userRelays.length > 0) { - console.log(`[RelayService] Found ${userRelays.length} relays in user's NDK pool:`, userRelays); - - // Import these relays - for (const url of userRelays) { - if (typeof url === 'string' && (url.startsWith('wss://') || url.startsWith('ws://'))) { - try { - const existingRelay = await this.db.getFirstAsync<{ url: string }>( - 'SELECT url FROM relays WHERE url = ?', - [url] - ); - - const now = Date.now(); - - if (existingRelay) { - // We'll only update the timestamp, not the permissions - await this.db.runAsync( - 'UPDATE relays SET updated_at = ? WHERE url = ?', - [now, url] - ); - updatedCount++; - console.log(`[RelayService] Updated existing relay from NDK pool: ${url}`); - } else { - maxPriority++; - await this.db.runAsync( - 'INSERT INTO relays (url, read, write, priority, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)', - [url, 1, 1, maxPriority, now, now] - ); - importCount++; - console.log(`[RelayService] Added new relay from NDK pool: ${url}`); - } - } catch (innerError) { - console.error(`[RelayService] Error importing relay ${url} from NDK pool:`, innerError); - } - } - } - - // Set flag to true because we found relays - relayTagsFound = userRelays.length > 0; - } - } catch (poolError) { - console.log('[RelayService] Error accessing NDK pool relays:', poolError); - } - } - - if (!relayTagsFound) { - console.log('[RelayService] No relay information found in any format'); - } - - console.log(`[RelayService] Imported ${importCount} new relays, updated ${updatedCount} existing relays`); - return importCount > 0 || updatedCount > 0; + // Apply the found relay configuration + return this.applyRelayConfig(relayConfigs); } catch (error) { console.error('[RelayService] Error importing relays from metadata:', error); throw error; } } - /** - * Reset relays to default set - */ - async resetToDefaults(): Promise { - try { - console.log('[RelayService] Resetting relays to defaults'); - - // Clear existing relays - await this.db.runAsync('DELETE FROM relays'); - - // Add default relays - const now = Date.now(); - let addedCount = 0; - - for (let i = 0; i < DEFAULT_RELAYS.length; i++) { - const url = DEFAULT_RELAYS[i]; - const priority = DEFAULT_RELAYS.length - i; // Higher priority for first relays - - try { - await this.db.runAsync( - 'INSERT INTO relays (url, read, write, priority, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)', - [url, 1, 1, priority, now, now] - ); - addedCount++; - } catch (innerError) { - console.error(`[RelayService] Error adding default relay ${url}:`, innerError); - } - } - - console.log(`[RelayService] Successfully reset to ${addedCount} default relays`); - return addedCount > 0; - } catch (error) { - console.error('[RelayService] Error resetting relays to defaults:', error); - throw error; - } - } - /** * Create a kind:3 event with the user's relay preferences */ - async publishRelayList(ndk?: any): Promise { + async publishRelayList(ndk?: NDK): Promise { try { // Use provided NDK or the stored one const ndkInstance = ndk || this.ndk; @@ -797,27 +405,28 @@ export class RelayService { console.log('[RelayService] Publishing relay list to Nostr'); - // Get all relays - const relays = await this.getAllRelays(); + // Get all relays directly from NDK pool + const relays: RelayConfig[] = []; + ndkInstance.pool.relays.forEach((relay, url) => { + relays.push({ + url, + read: relay.read ?? true, + write: relay.write ?? true + }); + }); if (relays.length === 0) { console.warn('[RelayService] No relays to publish'); return false; } - // Create event using any NDK version - const NDKEvent = ndkInstance.constructor.name === 'NDK' ? - ndkInstance.constructor.NDKEvent : - require('@nostr-dev-kit/ndk-mobile').NDKEvent; - + // Create kind:3 event + const { NDKEvent } = require('@nostr-dev-kit/ndk-mobile'); const event = new NDKEvent(ndkInstance); event.kind = 3; // Add relay tags for (const relay of relays) { - // Skip disabled relays - if (!relay.read && !relay.write) continue; - if (relay.read && relay.write) { // Full access event.tags.push(['r', relay.url]); @@ -844,166 +453,32 @@ export class RelayService { } } - /** - * Initialize relays from database or defaults - * If no relays in database, add defaults - */ - async initializeRelays(): Promise { - try { - console.log('[RelayService] Initializing relays'); - - // First verify the relays table exists and has the correct structure - await this.checkAndDebugRelays(); - - // Check if there are any relays in the database - const count = await this.db.getFirstAsync<{ count: number }>( - 'SELECT COUNT(*) as count FROM relays' - ); - - // If no relays, add defaults - if (!count || count.count === 0) { - console.log('[RelayService] No relays found in database, adding defaults'); - await this.resetToDefaults(); - } else { - console.log(`[RelayService] Found ${count.count} relays in database`); - } - - // Return enabled relays - const enabledRelays = await this.getEnabledRelays(); - console.log(`[RelayService] Returning ${enabledRelays.length} enabled relays`); - return enabledRelays; - } catch (error) { - console.error('[RelayService] Error initializing relays:', error); - console.log('[RelayService] Falling back to default relays'); - // Return defaults on error - return DEFAULT_RELAYS; - } - } - /** * Helper to convert NDK relay status to our status format */ - private getRelayStatus(relay: any): 'connected' | 'connecting' | 'disconnected' | 'error' { - try { - // Check if the relay has a trailing slash in the URL - const urlWithoutSlash = relay.url ? relay.url.replace(/\/$/, '') : ''; - const urlWithSlash = urlWithoutSlash + '/'; - - // Try to get the relay from NDK pool - check both with and without trailing slash - const ndkRelay = this.ndk?.pool.getRelay(urlWithoutSlash) || - this.ndk?.pool.getRelay(urlWithSlash); - - if (ndkRelay) { - console.log(`[RelayService] Detailed relay status for ${relay.url}: status=${ndkRelay.status}, connected=${!!ndkRelay.connected}`); - - // The most reliable way to check connection status is to check the 'connected' property - if (ndkRelay.connected) { + private getRelayStatus(relay: NDKRelay): 'connected' | 'connecting' | 'disconnected' | 'error' { + try { + // Check if the relay is connected + if (relay.connected) { return 'connected'; } - // NDK relay status: 0=connecting, 1=connected, 2=disconnecting, 3=disconnected, 4=reconnecting, 5=auth_required - if (ndkRelay.status === 1) { - return 'connected'; - } else if (ndkRelay.status === 0 || ndkRelay.status === 4) { // CONNECTING or RECONNECTING - return 'connecting'; - } else if (ndkRelay.status === 5) { // AUTH_REQUIRED - This is actually a connected state! - return 'connected'; // This is the key fix - } else { - return 'disconnected'; - } - } - - // If we can't find the relay in the NDK pool - return 'disconnected'; - } catch (error) { - console.error(`[RelayService] Error getting relay status:`, error); - return 'disconnected'; - } -} - - /** - * Check and debug relays table and content - */ - private async checkAndDebugRelays(): Promise { - try { - console.log('[RelayService] Checking database for relays...'); - - // Check if table exists - const tableExists = await this.db.getFirstAsync<{ count: number }>( - `SELECT count(*) as count FROM sqlite_master - WHERE type='table' AND name='relays'` - ); - - if (!tableExists || tableExists.count === 0) { - console.error('[RelayService] Relays table does not exist!'); - return; - } - - console.log('[RelayService] Relays table exists'); - - // Check relay count - const count = await this.db.getFirstAsync<{ count: number }>( - 'SELECT COUNT(*) as count FROM relays' - ); - - console.log(`[RelayService] Found ${count?.count || 0} relays in database`); - - if (count && count.count > 0) { - // Get sample relays - const sampleRelays = await this.db.getAllAsync( - 'SELECT url, read, write, priority FROM relays LIMIT 5' - ); - - console.log('[RelayService] Sample relays:', sampleRelays); + // Map NDK status to our status format + switch (relay.status) { + case 0: // CONNECTING + case 4: // RECONNECTING + return 'connecting'; + case 1: // CONNECTED + case 5: // AUTH_REQUIRED (is actually a connected state) + return 'connected'; + case 3: // DISCONNECTED + return 'disconnected'; + default: + return 'disconnected'; } } catch (error) { - console.error('[RelayService] Error checking relays:', error); + console.error('[RelayService] Error getting relay status:', error); + return 'error'; } } - - /** - * Import user's relay preferences on login - */ - async importUserRelaysOnLogin(user: any, ndk: any): Promise { - console.log('[RelayService] Checking for user relay preferences...'); - if (!user || !user.pubkey) return; - - try { - // First check if we already have relays in the database - const existingCount = await this.db.getFirstAsync<{ count: number }>( - 'SELECT COUNT(*) as count FROM relays' - ); - - // If we have relays and they're not just the defaults, skip import - if (existingCount && existingCount.count !== undefined && existingCount.count > 0) { - console.log(`[RelayService] Found ${existingCount.count} existing relays, checking if we need to import more`); - } else { - console.log('[RelayService] No existing relays found, will attempt to import'); - } - - console.log('[RelayService] Attempting to import user relay preferences'); - - // Try to import from metadata - const success = await this.importFromUserMetadata(user.pubkey, ndk); - - if (success) { - console.log('[RelayService] Successfully imported user relay preferences'); - // Apply the imported configuration immediately - await this.applyRelayConfig(ndk); - } else { - console.log('[RelayService] No relay preferences found, resetting to defaults'); - await this.resetToDefaults(); - await this.applyRelayConfig(ndk); - } - } catch (error) { - console.error('[RelayService] Error importing user relays:', error); - // On error, reset to defaults - try { - console.log('[RelayService] Error occurred, resetting to defaults'); - await this.resetToDefaults(); - await this.applyRelayConfig(ndk); - } catch (resetError) { - console.error('[RelayService] Error resetting to defaults:', resetError); - } - } - }} \ No newline at end of file +} \ No newline at end of file diff --git a/lib/initNDK.ts b/lib/initNDK.ts index 43e8bf8..8e6e136 100644 --- a/lib/initNDK.ts +++ b/lib/initNDK.ts @@ -2,58 +2,39 @@ import 'react-native-get-random-values'; // This must be the first import import NDK, { NDKCacheAdapterSqlite } from '@nostr-dev-kit/ndk-mobile'; import * as SecureStore from 'expo-secure-store'; -import { openDatabaseSync } from 'expo-sqlite'; import { RelayService, DEFAULT_RELAYS } from '@/lib/db/services/RelayService'; -import { NDKCommon } from '@/types/ndk-common'; import { extendNDK } from '@/types/ndk-extensions'; /** - * Initialize NDK with relays from database or defaults + * Initialize NDK with relays */ export async function initializeNDK() { console.log('[NDK] Initializing NDK with mobile adapter...'); // Create a mobile-specific cache adapter const cacheAdapter = new NDKCacheAdapterSqlite('powr', 1000); + await cacheAdapter.initialize(); - // Initialize database and relay service - const db = openDatabaseSync('powr.db'); - const relayService = new RelayService(db); - + // Initialize relay service + const relayService = new RelayService(); relayService.enableDebug(); - // Load relays from database or use defaults - console.log('[NDK] Loading relay configuration...'); - let relays: string[]; - - try { - // Try to initialize relays from database (will add defaults if none exist) - relays = await relayService.initializeRelays(); - console.log(`[NDK] Loaded ${relays.length} relays from database:`, relays); - } catch (error) { - console.error('[NDK] Error loading relays from database:', error); - console.log('[NDK] Falling back to default relays'); - relays = DEFAULT_RELAYS; - } - // Create settings store const settingsStore = { get: SecureStore.getItemAsync, set: SecureStore.setItemAsync, delete: SecureStore.deleteItemAsync, getSync: (key: string) => { - // This is a synchronous wrapper - for mobile we need to handle this differently - // since SecureStore is async-only console.log('[Settings] Warning: getSync called but returning null, not supported in this implementation'); return null; } }; - // Initialize NDK with options - console.log(`[NDK] Creating NDK instance with ${relays.length} relays`); + // Initialize NDK with default relays first + console.log(`[NDK] Creating NDK instance with default relays`); let ndk = new NDK({ cacheAdapter, - explicitRelayUrls: relays, + explicitRelayUrls: DEFAULT_RELAYS, enableOutboxModel: true, autoConnectUserRelays: true, clientName: 'powr', @@ -62,41 +43,8 @@ export async function initializeNDK() { // Extend NDK with helper methods for better compatibility ndk = extendNDK(ndk); - // Initialize cache adapter - await cacheAdapter.initialize(); - - // Set up the RelayService with the NDK instance for future use - relayService.setNDK(ndk as unknown as NDKCommon); - - // Setup relay status tracking - const relayStatus: Record = {}; - relays.forEach(url => { - relayStatus[url] = 'connecting'; - }); - - // Set up listeners before connecting - relays.forEach(url => { - const relay = ndk.pool.getRelay(url); - if (relay) { - // Connection success - relay.on('connect', () => { - console.log(`[NDK] Relay connected: ${url}`); - relayStatus[url] = 'connected'; - }); - - // Connection closed - relay.on('disconnect', () => { - console.log(`[NDK] Relay disconnected: ${url}`); - relayStatus[url] = 'disconnected'; - }); - - // For errors, use the notice event which is used for errors in NDK - relay.on('notice', (notice: string) => { - console.error(`[NDK] Relay notice/error for ${url}:`, notice); - relayStatus[url] = 'error'; - }); - } - }); + // Set the NDK instance in the RelayService + relayService.setNDK(ndk); try { console.log('[NDK] Connecting to relays...'); @@ -105,27 +53,24 @@ export async function initializeNDK() { // Wait a moment for connections to establish await new Promise(resolve => setTimeout(resolve, 2000)); + // Get updated relay statuses + const relaysWithStatus = await relayService.getAllRelaysWithStatus(); + // Count connected relays - const connectedRelays = Object.entries(relayStatus) - .filter(([_, status]) => status === 'connected') - .map(([url]) => url); + const connectedRelays = relaysWithStatus + .filter(relay => relay.status === 'connected') + .map(relay => relay.url); - console.log(`[NDK] Connected to ${connectedRelays.length}/${relays.length} relays`); + console.log(`[NDK] Connected to ${connectedRelays.length}/${relaysWithStatus.length} relays`); - // Add more detailed relay status logging + // Log detailed relay status console.log('[NDK] Detailed relay status:'); - relays.forEach(url => { - const relay = ndk.pool.getRelay(url); - console.log(` - ${url}: ${relay ? - (relay.status === 1 ? 'connected' : - relay.status === 0 ? 'connecting' : - relay.status === 3 ? 'disconnected' : - `status=${relay.status}`) : 'not found'}`); + relaysWithStatus.forEach(relay => { + console.log(` - ${relay.url}: ${relay.status}`); }); return { ndk, - relayStatus, relayService, connectedRelayCount: connectedRelays.length, connectedRelays @@ -135,7 +80,6 @@ export async function initializeNDK() { // Still return the NDK instance so the app can work offline return { ndk, - relayStatus, relayService, connectedRelayCount: 0, connectedRelays: [] diff --git a/lib/stores/ndk.ts b/lib/stores/ndk.ts index 0f97bd5..012ab32 100644 --- a/lib/stores/ndk.ts +++ b/lib/stores/ndk.ts @@ -6,11 +6,10 @@ import NDK, { NDKUser, NDKRelay, NDKPrivateKeySigner -} from '@nostr-dev-kit/ndk'; +} from '@nostr-dev-kit/ndk-mobile'; import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools'; import * as SecureStore from 'expo-secure-store'; import { RelayService } from '@/lib/db/services/RelayService'; -import { openDatabaseSync } from 'expo-sqlite'; // Constants for SecureStore const PRIVATE_KEY_STORAGE_KEY = 'nostr_privkey'; @@ -133,85 +132,129 @@ export const useNDKStore = create((set, get) => login: async (privateKeyInput?: string) => { set({ isLoading: true, error: null }); + console.log('[NDK] Login attempt starting'); try { const { ndk } = get(); if (!ndk) { + console.log('[NDK] Error: NDK not initialized'); throw new Error('NDK not initialized'); } + console.log('[NDK] Processing private key input'); + // If no private key is provided, generate one let privateKeyHex = privateKeyInput; if (!privateKeyHex) { + console.log('[NDK] No key provided, generating new key'); const { privateKey } = get().generateKeys(); privateKeyHex = privateKey; + } else { + console.log('[NDK] Using provided key, format:', + privateKeyHex.startsWith('nsec') ? 'nsec' : 'hex', + 'length:', privateKeyHex.length); } // Handle nsec format if (privateKeyHex.startsWith('nsec')) { try { + console.log('[NDK] Decoding nsec format key'); const decoded = nip19.decode(privateKeyHex); + console.log('[NDK] Decoded type:', decoded.type); if (decoded.type === 'nsec') { // Get the data as hex privateKeyHex = bytesToHex(decoded.data as any); + console.log('[NDK] Converted to hex, new length:', privateKeyHex.length); } } catch (error) { - console.error('Error decoding nsec:', error); + console.error('[NDK] Error decoding nsec:', error); throw new Error('Invalid nsec format'); } } + console.log('[NDK] Creating signer with key length:', privateKeyHex.length); + // Create signer with private key const signer = new NDKPrivateKeySigner(privateKeyHex); + console.log('[NDK] Signer created, setting on NDK'); ndk.signer = signer; // Get user + console.log('[NDK] Getting user from signer'); const user = await ndk.signer.user(); if (!user) { + console.log('[NDK] Error: Could not get user from signer'); throw new Error('Could not get user from signer'); } + console.log('[NDK] User retrieved, pubkey:', user.pubkey ? user.pubkey.substring(0, 8) + '...' : 'undefined'); + // Fetch user profile console.log('[NDK] Fetching user profile'); - await user.fetchProfile(); + try { + await user.fetchProfile(); + console.log('[NDK] Profile fetched successfully'); + } catch (profileError) { + console.warn('[NDK] Warning: Could not fetch user profile:', profileError); + // Continue even if profile fetch fails + } // Process profile data to ensure image property is set if (user.profile) { + console.log('[NDK] Profile data available'); if (!user.profile.image && (user.profile as any).picture) { user.profile.image = (user.profile as any).picture; + console.log('[NDK] Set image from picture property'); } - console.log('[NDK] User profile loaded:', user.profile); + console.log('[NDK] User profile loaded:', + user.profile.name || user.profile.displayName || 'No name available'); + } else { + console.log('[NDK] No profile data available'); } // Save the private key hex string securely + console.log('[NDK] Saving private key to secure storage'); await SecureStore.setItemAsync(PRIVATE_KEY_STORAGE_KEY, privateKeyHex); // After successful login, import user relay preferences try { - console.log('[NDK] Login successful, importing user relay preferences'); - const db = openDatabaseSync('powr.db'); - const relayService = new RelayService(db); + console.log('[NDK] Creating RelayService to import user preferences'); + const relayService = new RelayService(); // Set NDK on the relay service - relayService.setNDK(ndk as any); + console.log('[NDK] Setting NDK on RelayService'); + relayService.setNDK(ndk as any); // Using type assertion - // Import and apply user relay preferences - await relayService.importUserRelaysOnLogin(user, ndk); + // Import user relay preferences from metadata (kind:3 events) + if (user.pubkey) { + console.log('[NDK] Importing relay metadata for user:', user.pubkey.substring(0, 8) + '...'); + try { + await relayService.importFromUserMetadata(user.pubkey, ndk); + console.log('[NDK] Successfully imported user relay preferences'); + } catch (importError) { + console.warn('[NDK] Could not import user relay preferences:', importError); + // Continue even if import fails + } + } else { + console.log('[NDK] Cannot import relay metadata: No pubkey available'); + } } catch (relayError) { - console.error('[NDK] Error importing user relay preferences:', relayError); + console.error('[NDK] Error with RelayService:', relayError); // Continue with login even if relay import fails } + console.log('[NDK] Login successful, updating state'); set({ currentUser: user, isAuthenticated: true, isLoading: false }); + console.log('[NDK] Login complete'); return true; } catch (error) { - console.error('[NDK] Login error:', error); + console.error('[NDK] Login error detailed:', error); set({ error: error instanceof Error ? error : new Error('Failed to login'), isLoading: false diff --git a/lib/stores/relayStore.ts b/lib/stores/relayStore.ts index df0f556..41585e3 100644 --- a/lib/stores/relayStore.ts +++ b/lib/stores/relayStore.ts @@ -1,6 +1,5 @@ // lib/stores/relayStore.ts import { create } from 'zustand'; -import { openDatabaseSync } from 'expo-sqlite'; import type { RelayWithStatus } from '@/lib/db/services/RelayService'; import { RelayService } from '@/lib/db/services/RelayService'; import { useNDKStore } from './ndk'; @@ -11,8 +10,7 @@ let relayServiceInstance: RelayService | null = null; const getRelayService = (): RelayService => { if (!relayServiceInstance) { - const db = openDatabaseSync('powr.db'); - relayServiceInstance = new RelayService(db); + relayServiceInstance = new RelayService(); console.log('[RelayStore] Created RelayService instance'); } return relayServiceInstance; @@ -60,7 +58,8 @@ export const useRelayStore = create((set, g const ndk = ndkState.ndk as unknown as NDKCommon; if (ndk) { - relayService.setNDK(ndk); + // Use type assertion to ensure compatibility + relayService.setNDK(ndk as any); } const relays = await relayService.getAllRelaysWithStatus(); @@ -159,7 +158,8 @@ export const useRelayStore = create((set, g } // Apply relay config changes to NDK - const success = await relayService.applyRelayConfig(ndk); + const relays = get().relays; + const success = await relayService.applyRelayConfig(relays); // Wait a moment before reloading to give connections time to establish await new Promise(resolve => setTimeout(resolve, 2000));