From 4cd62cf77587278ab06bcd1045f1747746d4f89d Mon Sep 17 00:00:00 2001 From: DocNR Date: Sun, 9 Mar 2025 12:48:24 -0400 Subject: [PATCH] basic relay managment system working --- app/(tabs)/library/programs.tsx | 85 ++++-- components/RelayManagement.tsx | 431 ++++++++---------------------- lib/db/services/RelayService.ts | 453 +++++++++++++++++++++++++++----- lib/initNDK.ts | 14 +- lib/stores/ndk.ts | 1 + 5 files changed, 573 insertions(+), 411 deletions(-) diff --git a/app/(tabs)/library/programs.tsx b/app/(tabs)/library/programs.tsx index 9b25d18..7d0e413 100644 --- a/app/(tabs)/library/programs.tsx +++ b/app/(tabs)/library/programs.tsx @@ -1,6 +1,6 @@ // app/(tabs)/library/programs.tsx import React, { useState, useEffect } from 'react'; -import { View, ScrollView, TextInput, ActivityIndicator, Platform, TouchableOpacity } from 'react-native'; +import { View, ScrollView, TextInput, ActivityIndicator, Platform, Alert, TouchableOpacity } from 'react-native'; import { Text } from '@/components/ui/text'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -17,6 +17,7 @@ import { FilterSheet, type FilterOptions, type SourceType } from '@/components/l import { Separator } from '@/components/ui/separator'; import { NostrEventKind } from '@/types/nostr'; import { useNDKStore } from '@/lib/stores/ndk'; +import * as SecureStore from 'expo-secure-store'; // Define relay status enum RelayStatus { @@ -150,34 +151,84 @@ export default function ProgramsScreen() { const resetDatabase = async () => { try { - await db.withTransactionAsync(async () => { - // Drop all tables - const tables = await db.getAllAsync<{ name: string }>( + setTestResults(null); + + // Clear stored keys first + try { + await SecureStore.deleteItemAsync('nostr_privkey'); + console.log('[Database Reset] Cleared stored Nostr keys'); + } catch (keyError) { + 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_%'" ); - - for (const { name } of tables) { - await db.execAsync(`DROP TABLE IF EXISTS ${name}`); + 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); } - - // Recreate schema - await schema.createTables(db); - }); + } + + // 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' }] + ); + } + } + ] + ); setTestResults({ success: true, - message: 'Database reset successfully' + message: 'Database tables dropped. Please restart the app to complete the reset.' }); - // Refresh database status - checkDatabase(); - inspectDatabase(); } catch (error) { - console.error('Error resetting database:', error); + console.error('[Database Reset] Error resetting database:', error); setTestResults({ success: false, - message: error instanceof Error ? error.message : 'Unknown error during reset' + 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/RelayManagement.tsx b/components/RelayManagement.tsx index 57ad48e..0f96772 100644 --- a/components/RelayManagement.tsx +++ b/components/RelayManagement.tsx @@ -1,217 +1,92 @@ -// components/RelayManagement.tsx -import React, { useState, useEffect } from 'react'; -import { View, Text, FlatList, TextInput, ActivityIndicator, Alert, TouchableOpacity, Modal, ScrollView } from 'react-native'; -import { Switch } from 'react-native-gesture-handler'; // Or your UI library's Switch component +import React, { useEffect, useState } from 'react'; +import { View, Text, FlatList, TouchableOpacity, Modal, ActivityIndicator } from 'react-native'; import { useRelayStore } from '@/lib/stores/relayStore'; -import { useNDKStore } from '@/lib/stores/ndk'; +import { Switch } from '@/components/ui/switch'; +import { Button } from '@/components/ui/button'; +import { X } from 'lucide-react-native'; import { RelayWithStatus } from '@/lib/db/services/RelayService'; -interface Props { +// Define proper interface for component props +interface RelayManagementProps { isVisible: boolean; onClose: () => void; } -export default function RelayManagement({ isVisible, onClose }: Props) { - // Get relay state and actions from the store +// Simple RelayManagement component +export default function RelayManagement({ isVisible, onClose }: RelayManagementProps) { const relays = useRelayStore(state => state.relays); const isLoading = useRelayStore(state => state.isLoading); - const isRefreshing = useRelayStore(state => state.isRefreshing); - const isSaving = useRelayStore(state => state.isSaving); const loadRelays = useRelayStore(state => state.loadRelays); - const addRelay = useRelayStore(state => state.addRelay); - const removeRelay = useRelayStore(state => state.removeRelay); const updateRelay = useRelayStore(state => state.updateRelay); - const applyChanges = useRelayStore(state => state.applyChanges); - const resetToDefaults = useRelayStore(state => state.resetToDefaults); - // Get current user for import/export functions - const { ndk, currentUser } = useNDKStore(); - - // Local state - const [newRelayUrl, setNewRelayUrl] = useState(''); - const [isAddingRelay, setIsAddingRelay] = useState(false); - - // Load relays when component mounts or becomes visible useEffect(() => { if (isVisible) { - console.log('[RelayManagement] Component became visible, loading relays'); loadRelays(); } - }, [isVisible, loadRelays]); - - // Debug logging - useEffect(() => { - if (isVisible) { - console.log('[RelayManagement] Component state:', { - relaysCount: relays.length, - isLoading, - isRefreshing, - isAddingRelay - }); - - // Log the first relay for inspection - if (relays.length > 0) { - console.log('[RelayManagement] First relay:', relays[0]); - } else { - console.log('[RelayManagement] No relays loaded'); - } - } - }, [isVisible, relays, isLoading, isRefreshing, isAddingRelay]); + }, [isVisible]); - // Function to add a new relay - const handleAddRelay = async () => { - if (!newRelayUrl || !newRelayUrl.startsWith('wss://')) { - Alert.alert('Invalid Relay URL', 'Relay URL must start with wss://'); - return; - } - - try { - await addRelay(newRelayUrl); - setNewRelayUrl(''); - setIsAddingRelay(false); - } catch (error) { - Alert.alert('Error', error instanceof Error ? error.message : 'Failed to add relay'); - } - }; - - // Function to handle relay removal with confirmation - const handleRemoveRelay = (url: string) => { - Alert.alert( - 'Remove Relay', - `Are you sure you want to remove ${url}?`, - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Remove', - style: 'destructive', - onPress: async () => { - try { - await removeRelay(url); - } catch (error) { - Alert.alert('Error', 'Failed to remove relay'); - } - } - } - ] - ); - }; - - // Function to toggle read/write permission - const handleTogglePermission = (url: string, permission: 'read' | 'write') => { - const relay = relays.find(r => r.url === url); - if (relay) { - console.log(`[RelayManagement] Toggling ${permission} for relay ${url}`); - updateRelay(url, { [permission]: !relay[permission] }); - } - }; - - // Function to apply changes - const handleApplyChanges = async () => { - try { - console.log('[RelayManagement] Applying changes...'); - const success = await applyChanges(); - if (success) { - Alert.alert('Success', 'Relay configuration applied successfully!'); - } - } catch (error) { - Alert.alert('Error', 'Failed to apply relay configuration'); - } - }; - - // Function to reset to defaults with confirmation - const handleResetToDefaults = () => { - Alert.alert( - 'Reset to Defaults', - 'Are you sure you want to reset all relays to the default configuration?', - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Reset', - style: 'destructive', - onPress: async () => { - try { - await resetToDefaults(); - Alert.alert('Success', 'Relays reset to defaults'); - } catch (error) { - Alert.alert('Error', 'Failed to reset relays'); - } - } - } - ] - ); - }; - - // Function to get color based on relay status - const getStatusColor = (status: string) => { + // Status indicator color with proper typing + const getStatusColor = (status: string): string => { switch (status) { case 'connected': return '#10b981'; // Green case 'connecting': return '#f59e0b'; // Amber - case 'error': return '#ef4444'; // Red + case 'error': + case 'disconnected': return '#ef4444'; // Red default: return '#6b7280'; // Gray } }; - // Render a relay item - const renderRelayItem = ({ item }: { item: RelayWithStatus }) => { - console.log(`[RelayManagement] Rendering relay: ${item.url}, status: ${item.status}`); - return ( - - - - - {item.url} - - - handleRemoveRelay(item.url)} - style={{ padding: 8 }} - > - Remove - + // Render a relay item with proper typing + const renderRelayItem = ({ item }: { item: RelayWithStatus }) => ( + + + + + {item.url} - - - Read - handleTogglePermission(item.url, 'read')} - /> - - - - Write - handleTogglePermission(item.url, 'write')} - /> - + + {item.status} + + + + + + Read + updateRelay(item.url, { read: !item.read })} + /> + + + + Write + updateRelay(item.url, { write: !item.write })} + /> - ); - }; - - // Reset component state - const resetComponent = () => { - setIsAddingRelay(false); - setNewRelayUrl(''); - loadRelays(); - }; - - // Error handler - const handleError = (error: any) => { - console.error('[RelayManagement] Error:', error); - Alert.alert('Error', error instanceof Error ? error.message : 'An unknown error occurred'); - resetComponent(); - }; + + ); return ( - - - {/* Header */} - + + + Manage Relays - Close + - {/* Content */} - - {isLoading ? ( - - - Loading relays... - - ) : ( - <> - {/* Relay list */} - {relays.length === 0 ? ( - - No relays configured - - Reset to Defaults - - - ) : ( - item.url} - renderItem={renderRelayItem} - ListEmptyComponent={ - - No relays found. Try resetting to defaults. - - } - /> - )} + {isLoading ? ( + + + Loading relays... + + ) : ( + <> + item.url} + renderItem={renderRelayItem} + style={{ maxHeight: '70%' }} + /> + + + - {/* Add relay section */} - {isAddingRelay ? ( - - Add New Relay - - - - setIsAddingRelay(false)} - style={{ - padding: 10, - backgroundColor: '#e5e7eb', - borderRadius: 8, - flex: 1, - marginRight: 8, - alignItems: 'center' - }} - > - Cancel - - - - Add Relay - - - - ) : ( - - setIsAddingRelay(true)} - style={{ - padding: 10, - backgroundColor: '#e5e7eb', - borderRadius: 8, - alignItems: 'center' - }} - > - Add New Relay - - - )} - - )} - - - {/* Footer */} - - - {isSaving ? ( - - ) : ( - Apply Changes - )} - - - - Reset to Defaults - - + + + + )} diff --git a/lib/db/services/RelayService.ts b/lib/db/services/RelayService.ts index 5a73810..52d11a8 100644 --- a/lib/db/services/RelayService.ts +++ b/lib/db/services/RelayService.ts @@ -40,11 +40,23 @@ export interface RelayWithStatus extends RelayConfig { export class RelayService { private db: SQLiteDatabase; private ndk: NDKCommon | null = null; + private debug: boolean = false; constructor(db: SQLiteDatabase) { this.db = db; } + enableDebug() { + this.debug = true; + console.log('[RelayService] Debug mode enabled'); + } + + private logDebug(message: string, ...args: any[]) { + if (this.debug) { + console.log(`[RelayService Debug] ${message}`, ...args); + } + } + /** * Set NDK instance for relay operations */ @@ -84,24 +96,21 @@ export class RelayService { if (!this.ndk) { console.warn('[RelayService] NDK not initialized, returning relays with disconnected status'); - // Return relays with disconnected status if NDK not initialized return relays.map(relay => ({ ...relay, status: 'disconnected' })); } + // 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}`); + }); + return relays.map(relay => { - let status: 'connected' | 'connecting' | 'disconnected' | 'error' = 'disconnected'; - - try { - const ndkRelay = this.ndk?.pool.getRelay(relay.url); - if (ndkRelay) { - status = this.getRelayStatus(ndkRelay); - } - } catch (error) { - console.error(`[RelayService] Error getting status for relay ${relay.url}:`, error); - } + const status = this.getRelayStatus(relay); + console.log(`[RelayService] Status for relay ${relay.url}: ${status}`); return { ...relay, @@ -114,13 +123,18 @@ export class RelayService { } } + private normalizeRelayUrl(url: string): string { + // Remove trailing slash if present + return url.replace(/\/$/, ''); + } + /** * Add a new relay to the database */ async addRelay(url: string, read = true, write = true, priority?: number): Promise { try { // Normalize the URL - url = url.trim(); + url = this.normalizeRelayUrl(url.trim()); // Validate URL format if (!url.startsWith('wss://')) { @@ -408,6 +422,24 @@ 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); + } + // Get highest current priority const highestPriority = await this.db.getFirstAsync<{ priority: number }>( 'SELECT MAX(priority) as priority FROM relays' @@ -417,52 +449,295 @@ export class RelayService { let importCount = 0; let updatedCount = 0; + // Check if any relay tags exist + let relayTagsFound = false; + // Process each relay in the event - for (const tag of latestEvent.tags) { - if (tag[0] === 'r') { - const url = tag[1]; - - // Check for read/write specification in the tag - 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 - } - + if (latestEvent.tags && Array.isArray(latestEvent.tags)) { + for (const tag of latestEvent.tags) { try { - // Check if the relay already exists - const existingRelay = await this.db.getFirstAsync<{ url: string }>( - 'SELECT url FROM relays WHERE url = ?', - [url] - ); + console.log(`[RelayService] Processing tag: ${JSON.stringify(tag)}`); - 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++; - } 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++; + // 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 + } } - } catch (innerError) { - console.error(`[RelayService] Error importing relay ${url}:`, innerError); - // Continue with other relays + } catch (tagError) { + console.log('[RelayService] Error processing tag:', tagError); } } } + // 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); + } + } + + // 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; } catch (error) { @@ -483,19 +758,25 @@ export class RelayService { // 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 - await this.db.runAsync( - 'INSERT INTO relays (url, read, write, priority, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)', - [url, 1, 1, priority, now, now] - ); + 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 ${DEFAULT_RELAYS.length} default relays`); - return true; + 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; @@ -603,22 +884,42 @@ export class RelayService { * Helper to convert NDK relay status to our status format */ private getRelayStatus(relay: any): 'connected' | 'connecting' | 'disconnected' | 'error' { - try { - if (relay.status === NDK_RELAY_STATUS.CONNECTED) { + 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) { return 'connected'; - } else if ( - relay.status === NDK_RELAY_STATUS.CONNECTING || - relay.status === NDK_RELAY_STATUS.RECONNECTING - ) { + } + + // 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'; } - } catch (error) { - console.error(`[RelayService] Error getting relay status:`, error); - 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 @@ -674,9 +975,10 @@ export class RelayService { ); // If we have relays and they're not just the defaults, skip import - if (existingCount?.count > DEFAULT_RELAYS.length) { - console.log(`[RelayService] Using existing relay configuration (${existingCount?.count} relays)`); - return; + 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'); @@ -689,10 +991,19 @@ export class RelayService { // Apply the imported configuration immediately await this.applyRelayConfig(ndk); } else { - console.log('[RelayService] No relay preferences found, using defaults'); + 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 31a6e8b..43e8bf8 100644 --- a/lib/initNDK.ts +++ b/lib/initNDK.ts @@ -19,6 +19,8 @@ export async function initializeNDK() { // Initialize database and relay service const db = openDatabaseSync('powr.db'); const relayService = new RelayService(db); + + relayService.enableDebug(); // Load relays from database or use defaults console.log('[NDK] Loading relay configuration...'); @@ -97,7 +99,6 @@ export async function initializeNDK() { }); try { - // Connect to relays console.log('[NDK] Connecting to relays...'); await ndk.connect(); @@ -111,6 +112,17 @@ export async function initializeNDK() { console.log(`[NDK] Connected to ${connectedRelays.length}/${relays.length} relays`); + // Add more detailed relay status logging + 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'}`); + }); + return { ndk, relayStatus, diff --git a/lib/stores/ndk.ts b/lib/stores/ndk.ts index 6bc1f4e..0f97bd5 100644 --- a/lib/stores/ndk.ts +++ b/lib/stores/ndk.ts @@ -10,6 +10,7 @@ import NDK, { 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';