diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b20051..06fed1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,33 @@ All notable changes to the POWR project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# Changelog - March 9, 2025 + +## Added +- Relay management system + - Added relays table to SQLite schema (version 3) + - Created RelayService for database operations + - Implemented RelayStore using Zustand for state management + - Added compatibility layer for NDK and NDK-mobile + - Added relay management UI in settings drawer + - Implemented relay connection status tracking + - Added support for read/write permissions + - Created relay initialization system with defaults + +## Improved +- Enhanced NDK initialization + - Added proper relay configuration loading + - Improved connection status tracking + - Enhanced error handling for relay operations +- Settings drawer enhancements + - Added relay management option + - Improved navigation structure + - Enhanced user interface +- NDK compatibility + - Created universal interfaces for NDK implementations + - Added type safety for complex operations + - Improved error handling throughout relay management + # Changelog - March 8, 2025 ## Added diff --git a/app/_layout.tsx b/app/_layout.tsx index 10a2000..5db6cc4 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -15,6 +15,7 @@ import { DatabaseProvider } from '@/components/DatabaseProvider'; import { ErrorBoundary } from '@/components/ErrorBoundary'; import { SettingsDrawerProvider } from '@/lib/contexts/SettingsDrawerContext'; import SettingsDrawer from '@/components/SettingsDrawer'; +import RelayInitializer from '@/components/RelayInitializer'; import { useNDKStore } from '@/lib/stores/ndk'; import { useWorkoutStore } from '@/stores/workoutStore'; @@ -72,6 +73,9 @@ export default function RootLayout() { + {/* Add RelayInitializer here - it loads relay data once NDK is available */} + + { + if (ndk) { + console.log('[RelayInitializer] NDK available, loading relays...'); + loadRelays().catch(error => + console.error('[RelayInitializer] Error loading relays:', error) + ); + } + }, [ndk]); + + // This component doesn't render anything + return null; +} \ No newline at end of file diff --git a/components/RelayManagement.tsx b/components/RelayManagement.tsx new file mode 100644 index 0000000..57ad48e --- /dev/null +++ b/components/RelayManagement.tsx @@ -0,0 +1,369 @@ +// 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 { useRelayStore } from '@/lib/stores/relayStore'; +import { useNDKStore } from '@/lib/stores/ndk'; +import { RelayWithStatus } from '@/lib/db/services/RelayService'; + +interface Props { + isVisible: boolean; + onClose: () => void; +} + +export default function RelayManagement({ isVisible, onClose }: Props) { + // Get relay state and actions from the store + 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]); + + // 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) => { + switch (status) { + case 'connected': return '#10b981'; // Green + case 'connecting': return '#f59e0b'; // Amber + case 'error': 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 + + + + + + Read + handleTogglePermission(item.url, 'read')} + /> + + + + Write + handleTogglePermission(item.url, '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. + + } + /> + )} + + {/* 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 + + + + + + ); +} \ No newline at end of file diff --git a/components/SettingsDrawer.tsx b/components/SettingsDrawer.tsx index ac514dd..f7c208f 100644 --- a/components/SettingsDrawer.tsx +++ b/components/SettingsDrawer.tsx @@ -7,7 +7,7 @@ import { useRouter } from 'expo-router'; import { useSettingsDrawer } from '@/lib/contexts/SettingsDrawerContext'; import { Moon, Sun, LogOut, User, ChevronRight, X, Bell, HelpCircle, - Smartphone, Database, Zap, RefreshCw, AlertTriangle + Smartphone, Database, Zap, RefreshCw, AlertTriangle, Globe } from 'lucide-react-native'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Button } from '@/components/ui/button'; @@ -16,6 +16,7 @@ import { Separator } from '@/components/ui/separator'; import { Text } from '@/components/ui/text'; import { useColorScheme } from '@/lib/useColorScheme'; import NostrLoginSheet from '@/components/sheets/NostrLoginSheet'; +import RelayManagement from '@/components/RelayManagement'; import { useNDKCurrentUser, useNDKAuth } from '@/lib/hooks/useNDK'; import { AlertDialog, @@ -48,6 +49,7 @@ export default function SettingsDrawer() { const theme = useTheme(); const [isLoginSheetOpen, setIsLoginSheetOpen] = useState(false); const [showSignOutAlert, setShowSignOutAlert] = useState(false); + const [showRelayManager, setShowRelayManager] = useState(false); const slideAnim = useRef(new Animated.Value(-DRAWER_WIDTH)).current; const fadeAnim = useRef(new Animated.Value(0)).current; @@ -121,6 +123,11 @@ export default function SettingsDrawer() { closeDrawer(); }; + // Open relay management + const handleRelayManagement = () => { + setShowRelayManager(true); + }; + // Nostr integration handler const handleNostrIntegration = () => { if (!isAuthenticated) { @@ -169,6 +176,12 @@ export default function SettingsDrawer() { label: 'Backup & Restore', onPress: () => closeDrawer(), }, + { + id: 'relays', + icon: Globe, + label: 'Manage Relays', + onPress: handleRelayManagement, + }, { id: 'device', icon: Smartphone, @@ -325,6 +338,12 @@ export default function SettingsDrawer() { /> )} + {/* Relay Management Sheet */} + setShowRelayManager(false)} + /> + {/* Sign Out Alert Dialog */} diff --git a/lib/db/schema.ts b/lib/db/schema.ts index 9f434cb..ff02c9b 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 = 2; // Increment since we're adding new tables +export const SCHEMA_VERSION = 3; // Incrementing to add the relays table class Schema { private async getCurrentVersion(db: SQLiteDatabase): Promise { @@ -288,6 +288,20 @@ 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 diff --git a/lib/db/services/RelayService.ts b/lib/db/services/RelayService.ts new file mode 100644 index 0000000..5a73810 --- /dev/null +++ b/lib/db/services/RelayService.ts @@ -0,0 +1,698 @@ +// 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 +}; + +// Default relays to use when none are configured +export const DEFAULT_RELAYS = [ + 'wss://relay.damus.io', + 'wss://relay.nostr.band', + 'wss://nos.lol', + 'wss://relay.snort.social', + 'wss://relay.current.fyi' +]; + +export interface RelayConfig { + url: string; + read: boolean; + write: boolean; + priority?: number; + created_at: number; + updated_at: number; +} + +export interface RelayWithStatus extends RelayConfig { + status: 'connected' | 'connecting' | 'disconnected' | 'error'; +} + +/** + * Service for managing Nostr relays + */ +export class RelayService { + private db: SQLiteDatabase; + private ndk: NDKCommon | null = null; + + constructor(db: SQLiteDatabase) { + this.db = db; + } + + /** + * Set NDK instance for relay operations + */ + setNDK(ndk: NDKCommon) { + 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 with disconnected status if NDK not initialized + return relays.map(relay => ({ + ...relay, + status: 'disconnected' + })); + } + + 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); + } + + return { + ...relay, + status + }; + }); + } catch (error) { + console.error('[RelayService] Error getting relays with status:', error); + return []; + } + } + + /** + * 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(); + + // Validate URL format + if (!url.startsWith('wss://')) { + throw new Error('Relay URL must start with wss://'); + } + + const now = Date.now(); + + // Check if relay already exists + const existingRelay = await this.db.getFirstAsync<{ url: string }>( + 'SELECT url FROM relays WHERE url = ?', + [url] + ); + + if (existingRelay) { + console.log(`[RelayService] Relay ${url} already exists, updating instead`); + return this.updateRelay(url, { read, write, priority }); + } + + // 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) { + console.error('[RelayService] Error adding relay:', error); + throw error; + } + } + + /** + * Update an existing relay + */ + 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); + } + + // Prepare update fields + const updates: string[] = []; + const params: any[] = []; + + if (changes.read !== undefined) { + updates.push('read = ?'); + params.push(changes.read ? 1 : 0); + } + + if (changes.write !== undefined) { + updates.push('write = ?'); + params.push(changes.write ? 1 : 0); + } + + 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}`); + return true; + } catch (error) { + console.error('[RelayService] Error updating relay:', error); + throw error; + } + } + + /** + * Remove a relay from the database + */ + async removeRelay(url: string): Promise { + try { + 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; + } 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 { + try { + // Use provided NDK or the stored one + const ndkInstance = ndk || this.ndk; + + if (!ndkInstance) { + 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 + } + + console.log(`[RelayService] Applying configuration for ${relayConfigs.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); + } + + // 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)) { + console.log(`[RelayService] Removing relay ${url} from NDK pool`); + safeRemoveRelay(ndkInstance, 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 + } + } + } + + console.log('[RelayService] Successfully applied relay configuration'); + return true; + } catch (error) { + console.error('[RelayService] Error applying relay configuration:', error); + throw error; + } + } + + /** + * Import relays from user metadata (kind:3 events) + */ + async importFromUserMetadata(pubkey: string, ndk: any): Promise { + try { + if (!ndk) { + throw new Error('NDK not initialized'); + } + + console.log(`[RelayService] Importing relays from metadata for user ${pubkey.slice(0, 8)}...`); + + // Fetch kind:3 event for user's relay list + const filter = { kinds: [3], authors: [pubkey] }; + const events = await ndk.fetchEvents(filter); + + if (!events || events.size === 0) { + console.log('[RelayService] No relay list found in user metadata'); + return false; + } + + // Find the most recent event + let latestEvent: any = null; + let latestCreatedAt = 0; + + for (const event of events) { + if (event.created_at && event.created_at > latestCreatedAt) { + latestEvent = event; + latestCreatedAt = event.created_at; + } + } + + if (!latestEvent) { + console.log('[RelayService] No valid relay list found in user metadata'); + return false; + } + + console.log(`[RelayService] Found relay list in event created at ${new Date(latestCreatedAt * 1000).toISOString()}`); + + // 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; + + // 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 + } + + 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++; + } 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++; + } + } catch (innerError) { + console.error(`[RelayService] Error importing relay ${url}:`, innerError); + // Continue with other relays + } + } + } + + console.log(`[RelayService] Imported ${importCount} new relays, updated ${updatedCount} existing relays`); + return importCount > 0 || updatedCount > 0; + } 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(); + + 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] + ); + } + + 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; + } + } + + /** + * Create a kind:3 event with the user's relay preferences + */ + async publishRelayList(ndk?: any): Promise { + try { + // Use provided NDK or the stored one + const ndkInstance = ndk || this.ndk; + + if (!ndkInstance || !ndkInstance.signer) { + throw new Error('NDK not initialized or not signed in'); + } + + console.log('[RelayService] Publishing relay list to Nostr'); + + // Get all relays + const relays = await this.getAllRelays(); + + 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; + + 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]); + } else if (relay.read) { + // Read-only + event.tags.push(['r', relay.url, 'read']); + } else if (relay.write) { + // Write-only + event.tags.push(['r', relay.url, 'write']); + } + } + + console.log(`[RelayService] Publishing kind:3 event with ${event.tags.length} relay tags`); + + // Sign and publish + await event.sign(); + await event.publish(); + + console.log('[RelayService] Successfully published relay list'); + return true; + } catch (error) { + console.error('[RelayService] Error publishing relay list:', error); + throw error; + } + } + + /** + * 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 { + if (relay.status === NDK_RELAY_STATUS.CONNECTED) { + return 'connected'; + } else if ( + relay.status === NDK_RELAY_STATUS.CONNECTING || + relay.status === NDK_RELAY_STATUS.RECONNECTING + ) { + return 'connecting'; + } else { + 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); + } + } catch (error) { + console.error('[RelayService] Error checking relays:', 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?.count > DEFAULT_RELAYS.length) { + console.log(`[RelayService] Using existing relay configuration (${existingCount?.count} relays)`); + return; + } + + 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, using defaults'); + } + } catch (error) { + console.error('[RelayService] Error importing user relays:', error); + } + } +} \ No newline at end of file diff --git a/lib/initNDK.ts b/lib/initNDK.ts index c1e86bb..31a6e8b 100644 --- a/lib/initNDK.ts +++ b/lib/initNDK.ts @@ -1,42 +1,79 @@ // lib/initNDK.ts import 'react-native-get-random-values'; // This must be the first import -import NDK, { NDKCacheAdapterSqlite, NDKEvent, NDKRelay } from '@nostr-dev-kit/ndk-mobile'; +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'; -// Use the same default relays you have in your current implementation -const DEFAULT_RELAYS = [ - 'wss://powr.duckdns.org', - 'wss://relay.damus.io', - 'wss://relay.nostr.band', - 'wss://purplepag.es', - 'wss://nos.lol' -]; - +/** + * Initialize NDK with relays from database or defaults + */ export async function initializeNDK() { - console.log('Initializing NDK with mobile adapter...'); + console.log('[NDK] Initializing NDK with mobile adapter...'); // Create a mobile-specific cache adapter const cacheAdapter = new NDKCacheAdapterSqlite('powr', 1000); - // Initialize NDK with mobile-specific options - const ndk = new NDK({ + // Initialize database and relay service + const db = openDatabaseSync('powr.db'); + const relayService = new RelayService(db); + + // 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`); + let ndk = new NDK({ cacheAdapter, - explicitRelayUrls: DEFAULT_RELAYS, + explicitRelayUrls: relays, enableOutboxModel: true, + autoConnectUserRelays: true, clientName: 'powr', }); + // 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 = {}; - DEFAULT_RELAYS.forEach(url => { + relays.forEach(url => { relayStatus[url] = 'connecting'; }); // Set up listeners before connecting - DEFAULT_RELAYS.forEach(url => { + relays.forEach(url => { const relay = ndk.pool.getRelay(url); if (relay) { // Connection success @@ -59,61 +96,26 @@ export async function initializeNDK() { } }); - // Function to check relay connection status - const checkRelayConnections = () => { - const connected = Object.entries(relayStatus) + try { + // Connect to relays + console.log('[NDK] Connecting to relays...'); + await ndk.connect(); + + // Wait a moment for connections to establish + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Count connected relays + const connectedRelays = Object.entries(relayStatus) .filter(([_, status]) => status === 'connected') .map(([url]) => url); - console.log(`[NDK] Connected relays: ${connected.length}/${DEFAULT_RELAYS.length}`); - return { - connectedCount: connected.length, - connectedRelays: connected - }; - }; - - try { - // Connect to relays with a timeout - console.log('[NDK] Connecting to relays...'); - - // Create a promise that resolves when connected to at least one relay - const connectionPromise = new Promise((resolve, reject) => { - // Function to check if we have at least one connection - const checkConnection = () => { - const { connectedCount } = checkRelayConnections(); - if (connectedCount > 0) { - console.log('[NDK] Successfully connected to at least one relay'); - resolve(); - } - }; - - // Check immediately after connecting - ndk.pool.on('relay:connect', checkConnection); - - // Set a timeout for connection - setTimeout(() => { - const { connectedCount } = checkRelayConnections(); - if (connectedCount === 0) { - console.warn('[NDK] Connection timeout - no relays connected'); - // Don't reject, as we can still work offline - resolve(); - } - }, 5000); - }); - - // Initiate the connection - await ndk.connect(); - - // Wait for either connection or timeout - await connectionPromise; - - // Final connection check - const { connectedCount, connectedRelays } = checkRelayConnections(); + console.log(`[NDK] Connected to ${connectedRelays.length}/${relays.length} relays`); return { ndk, relayStatus, - connectedRelayCount: connectedCount, + relayService, + connectedRelayCount: connectedRelays.length, connectedRelays }; } catch (error) { @@ -122,36 +124,9 @@ export async function initializeNDK() { return { ndk, relayStatus, + relayService, connectedRelayCount: 0, connectedRelays: [] }; } -} - -// Helper function to test publishing to relays -export async function testRelayPublishing(ndk: NDK): Promise { - try { - console.log('[NDK] Testing relay publishing...'); - - // Create a simple test event - use NDKEvent constructor instead of getEvent() - const testEvent = new NDKEvent(ndk); - testEvent.kind = 1; - testEvent.content = 'Test message from POWR app'; - testEvent.tags = [['t', 'test']]; - - // Try to sign and publish with timeout - const publishPromise = Promise.race([ - testEvent.publish(), - new Promise((_, reject) => - setTimeout(() => reject(new Error('Publish timeout')), 5000) - ) - ]); - - await publishPromise; - console.log('[NDK] Test publish successful'); - return true; - } catch (error) { - console.error('[NDK] Test publish failed:', error); - return false; - } } \ No newline at end of file diff --git a/lib/stores/ndk.ts b/lib/stores/ndk.ts index 4e45e47..6bc1f4e 100644 --- a/lib/stores/ndk.ts +++ b/lib/stores/ndk.ts @@ -9,6 +9,7 @@ import NDK, { } from '@nostr-dev-kit/ndk'; import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools'; import * as SecureStore from 'expo-secure-store'; +import { RelayService } from '@/lib/db/services/RelayService'; // Constants for SecureStore const PRIVATE_KEY_STORAGE_KEY = 'nostr_privkey'; @@ -185,6 +186,22 @@ export const useNDKStore = create((set, get) => // Save the private key hex string securely 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); + + // Set NDK on the relay service + relayService.setNDK(ndk as any); + + // Import and apply user relay preferences + await relayService.importUserRelaysOnLogin(user, ndk); + } catch (relayError) { + console.error('[NDK] Error importing user relay preferences:', relayError); + // Continue with login even if relay import fails + } + set({ currentUser: user, isAuthenticated: true, diff --git a/lib/stores/relayStore.ts b/lib/stores/relayStore.ts new file mode 100644 index 0000000..df0f556 --- /dev/null +++ b/lib/stores/relayStore.ts @@ -0,0 +1,259 @@ +// 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'; +import { NDKCommon } from '@/types/ndk-common'; + +// Create a singleton instance of RelayService +let relayServiceInstance: RelayService | null = null; + +const getRelayService = (): RelayService => { + if (!relayServiceInstance) { + const db = openDatabaseSync('powr.db'); + relayServiceInstance = new RelayService(db); + console.log('[RelayStore] Created RelayService instance'); + } + return relayServiceInstance; +}; + +// Define state interface +interface RelayStoreState { + relays: RelayWithStatus[]; + isLoading: boolean; + isRefreshing: boolean; + isSaving: boolean; + error: Error | null; +} + +// Define actions interface +interface RelayStoreActions { + loadRelays: () => Promise; + addRelay: (url: string, read?: boolean, write?: boolean) => Promise; + removeRelay: (url: string) => Promise; + updateRelay: (url: string, changes: Partial) => Promise; + applyChanges: () => Promise; + resetToDefaults: () => Promise; + importFromMetadata: (pubkey: string) => Promise; + publishRelayList: () => Promise; +} + +// Create the relay store +export const useRelayStore = create((set, get) => { + return { + // Initial state + relays: [], + isLoading: true, + isRefreshing: false, + isSaving: false, + error: null, + + // Action implementations + loadRelays: async () => { + try { + console.log('[RelayStore] Loading relays...'); + set({ isRefreshing: true, error: null }); + + const relayService = getRelayService(); + const ndkState = useNDKStore.getState(); + const ndk = ndkState.ndk as unknown as NDKCommon; + + if (ndk) { + relayService.setNDK(ndk); + } + + const relays = await relayService.getAllRelaysWithStatus(); + console.log(`[RelayStore] Loaded ${relays.length} relays with status`); + + set({ + relays, + isLoading: false, + isRefreshing: false + }); + } catch (error) { + console.error('[RelayStore] Error loading relays:', error); + set({ + error: error instanceof Error ? error : new Error('Failed to load relays'), + isLoading: false, + isRefreshing: false + }); + } + }, + + addRelay: async (url, read = true, write = true) => { + try { + console.log(`[RelayStore] Adding relay: ${url}`); + const relayService = getRelayService(); + await relayService.addRelay(url, read, write); + + // Reload relays to get the updated list with status + await get().loadRelays(); + console.log(`[RelayStore] Successfully added relay: ${url}`); + } catch (error) { + console.error('[RelayStore] Error adding relay:', error); + set({ error: error instanceof Error ? error : new Error('Failed to add relay') }); + throw error; + } + }, + + removeRelay: async (url) => { + try { + console.log(`[RelayStore] Removing relay: ${url}`); + const relayService = getRelayService(); + await relayService.removeRelay(url); + + // Update local state without reload to avoid flicker + set(state => ({ + relays: state.relays.filter(relay => relay.url !== url) + })); + console.log(`[RelayStore] Successfully removed relay: ${url}`); + } catch (error) { + console.error('[RelayStore] Error removing relay:', error); + set({ error: error instanceof Error ? error : new Error('Failed to remove relay') }); + throw error; + } + }, + + updateRelay: async (url, changes) => { + try { + console.log(`[RelayStore] Updating relay: ${url}`, changes); + const relayService = getRelayService(); + + // Extract only valid properties for the service + const validChanges: Partial = {}; + if (changes.read !== undefined) validChanges.read = changes.read; + if (changes.write !== undefined) validChanges.write = changes.write; + if (changes.priority !== undefined) validChanges.priority = changes.priority; + + await relayService.updateRelay(url, validChanges); + + // Update local state to reflect the changes + set(state => ({ + relays: state.relays.map(relay => + relay.url === url ? { ...relay, ...validChanges } : relay + ) + })); + console.log(`[RelayStore] Successfully updated relay: ${url}`); + } catch (error) { + console.error('[RelayStore] Error updating relay:', error); + set({ error: error instanceof Error ? error : new Error('Failed to update relay') }); + throw error; + } + }, + + applyChanges: async () => { + // Prevent multiple simultaneous calls + if (get().isSaving) return false; + + try { + console.log('[RelayStore] Applying relay changes...'); + set({ isSaving: true, error: null }); + + const relayService = getRelayService(); + const ndkState = useNDKStore.getState(); + const ndk = ndkState.ndk as unknown as NDKCommon; + + if (!ndk) { + throw new Error('NDK not initialized'); + } + + // Apply relay config changes to NDK + const success = await relayService.applyRelayConfig(ndk); + + // Wait a moment before reloading to give connections time to establish + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Reload relays to reflect updated connection status + await get().loadRelays(); + + set({ isSaving: false }); + console.log('[RelayStore] Successfully applied relay changes'); + return success; + } catch (error) { + console.error('[RelayStore] Error applying changes:', error); + set({ + error: error instanceof Error ? error : new Error('Failed to apply changes'), + isSaving: false + }); + return false; + } + }, + + resetToDefaults: async () => { + try { + console.log('[RelayStore] Resetting relays to defaults...'); + const relayService = getRelayService(); + await relayService.resetToDefaults(); + + // Reload relays to get the updated list + await get().loadRelays(); + console.log('[RelayStore] Successfully reset relays to defaults'); + } catch (error) { + console.error('[RelayStore] Error resetting relays:', error); + set({ error: error instanceof Error ? error : new Error('Failed to reset relays') }); + throw error; + } + }, + + importFromMetadata: async (pubkey) => { + try { + console.log('[RelayStore] Importing relays from user metadata...'); + set({ isRefreshing: true, error: null }); + + const relayService = getRelayService(); + const ndkState = useNDKStore.getState(); + const ndk = ndkState.ndk; + + if (!ndk) { + throw new Error('NDK not initialized'); + } + + // Import relays from the user's metadata + await relayService.importFromUserMetadata(pubkey, ndk); + + // Reload relays to get the updated list + await get().loadRelays(); + console.log('[RelayStore] Successfully imported relays from metadata'); + } catch (error) { + console.error('[RelayStore] Error importing from metadata:', error); + set({ + error: error instanceof Error ? error : new Error('Failed to import from metadata'), + isRefreshing: false + }); + throw error; + } + }, + + publishRelayList: async () => { + try { + console.log('[RelayStore] Publishing relay list...'); + const relayService = getRelayService(); + const ndkState = useNDKStore.getState(); + const ndk = ndkState.ndk; + + if (!ndk) { + throw new Error('NDK not initialized'); + } + + // Publish relay list to the network + const result = await relayService.publishRelayList(ndk); + console.log('[RelayStore] Successfully published relay list'); + return result; + } catch (error) { + console.error('[RelayStore] Error publishing relay list:', error); + set({ error: error instanceof Error ? error : new Error('Failed to publish relay list') }); + throw error; + } + } + }; +}); + +// Export individual hooks for specific use cases +export function useLoadRelays() { + return { + loadRelays: useRelayStore(state => state.loadRelays), + isLoading: useRelayStore(state => state.isLoading), + isRefreshing: useRelayStore(state => state.isRefreshing) + }; +} \ No newline at end of file diff --git a/types/ndk-common.ts b/types/ndk-common.ts new file mode 100644 index 0000000..a99116a --- /dev/null +++ b/types/ndk-common.ts @@ -0,0 +1,125 @@ +// types/ndk-common.ts + +/** + * This file provides common interfaces that work with both + * @nostr-dev-kit/ndk and @nostr-dev-kit/ndk-mobile + * to solve TypeScript conflicts between the two packages + */ + +// Define a universal NDK interface that works with both packages +export interface NDKCommon { + pool: { + relays: Map; + getRelay: (url: string) => any; + }; + connect: () => Promise; + disconnect: () => void; + fetchEvents: (filter: any) => Promise>; + signer?: any; + } + + // Define a universal NDKRelay interface + export interface NDKRelayCommon { + url: string; + status: number; + connect: () => Promise; + disconnect: () => void; + on: (event: string, listener: (...args: any[]) => void) => void; + } + + // Safe utility function to add a relay to NDK + export function safeAddRelay(ndk: NDKCommon, url: string, opts?: { read?: boolean; write?: boolean }): any { + try { + // Try using native addRelay if it exists + if ((ndk as any).addRelay) { + return (ndk as any).addRelay(url, opts, undefined); // Add undefined for authPolicy + } + + // Fallback implementation + let relay = ndk.pool.getRelay(url); + + if (!relay) { + // Safe relay creation that works with both NDK implementations + const NDKRelay = getRelayClass(); + relay = new NDKRelay(url); + ndk.pool.relays.set(url, relay); + } + + // Set read/write permissions if provided + if (opts) { + if (opts.read !== undefined) { + (relay as any).read = opts.read; + } + if (opts.write !== undefined) { + (relay as any).write = opts.write; + } + } + + return relay; + } catch (error) { + console.error('[NDK-Common] Error adding relay:', error); + return null; + } + } + + // Safe utility function to remove a relay from NDK + export function safeRemoveRelay(ndk: NDKCommon, url: string): void { + try { + // Try using native removeRelay if it exists + if ((ndk as any).removeRelay) { + (ndk as any).removeRelay(url); + return; + } + + // Fallback implementation + ndk.pool.relays.delete(url); + } catch (error) { + console.error('[NDK-Common] Error removing relay:', error); + } + } + + // Helper to get the NDKRelay class from either package + function getRelayClass(): any { + try { + // Try to get the NDKRelay class from ndk-mobile first + const ndkMobile = require('@nostr-dev-kit/ndk-mobile'); + if (ndkMobile.NDKRelay) { + return ndkMobile.NDKRelay; + } + + // Fallback to ndk + const ndk = require('@nostr-dev-kit/ndk'); + if (ndk.NDKRelay) { + return ndk.NDKRelay; + } + + throw new Error('NDKRelay class not found'); + } catch (error) { + console.error('[NDK-Common] Error getting NDKRelay class:', error); + + // Return a minimal NDKRelay implementation as last resort + return class MinimalNDKRelay { + url: string; + status: number = 0; + read: boolean = true; + write: boolean = true; + + constructor(url: string) { + this.url = url; + } + + connect() { + console.warn(`[NDK-Common] Minimal relay implementation can't connect to ${this.url}`); + return Promise.resolve(); + } + + disconnect() { + // No-op + } + + on(event: string, listener: (...args: any[]) => void) { + // No-op + } + }; + } + } \ No newline at end of file diff --git a/types/ndk-extensions.ts b/types/ndk-extensions.ts new file mode 100644 index 0000000..98ea807 --- /dev/null +++ b/types/ndk-extensions.ts @@ -0,0 +1,81 @@ +// types/ndk-extensions.ts + +import { NDKCommon } from '@/types/ndk-common'; + +// Extend NDKRelay with missing properties +declare module '@nostr-dev-kit/ndk-mobile' { + interface NDKRelay { + read?: boolean; + write?: boolean; + } + + interface NDK { + // Add missing methods + removeRelay?(url: string): void; + addRelay?(url: string, opts?: { read?: boolean; write?: boolean }, authPolicy?: any): NDKRelay | undefined; + } +} + +// Add methods to NDK prototype for backward compatibility +export function extendNDK(ndk: any): any { + // Only add methods if they don't already exist + if (!ndk.hasOwnProperty('removeRelay')) { + ndk.removeRelay = function(url: string) { + console.log(`[NDK Extension] Removing relay: ${url}`); + if (this.pool && this.pool.relays) { + this.pool.relays.delete(url); + } + }; + } + + if (!ndk.hasOwnProperty('addRelay')) { + ndk.addRelay = function(url: string, opts?: { read?: boolean; write?: boolean }, authPolicy?: any) { + console.log(`[NDK Extension] Adding relay: ${url}`); + + // Check if pool exists + if (!this.pool) { + console.error('[NDK Extension] NDK pool does not exist'); + return undefined; + } + + // Check if relay already exists + let relay = this.pool.getRelay ? this.pool.getRelay(url) : undefined; + + if (!relay) { + try { + // Try to create a relay with the constructor from this NDK instance + const NDKRelay = this.constructor.NDKRelay; + if (NDKRelay) { + relay = new NDKRelay(url); + } else { + // Fallback to importing from ndk-mobile + const { NDKRelay: ImportedNDKRelay } = require('@nostr-dev-kit/ndk-mobile'); + relay = new ImportedNDKRelay(url); + } + + // Add to pool + if (this.pool.relays && relay) { + this.pool.relays.set(url, relay); + } + } catch (error) { + console.error('[NDK Extension] Error creating relay:', error); + return undefined; + } + } + + // Set read/write permissions if provided + if (relay && opts) { + if (opts.read !== undefined) { + relay.read = opts.read; + } + if (opts.write !== undefined) { + relay.write = opts.write; + } + } + + return relay; + }; + } + + return ndk; +} \ No newline at end of file