new relay managment system

This commit is contained in:
DocNR 2025-03-09 11:15:28 -04:00
parent 29c4dd1675
commit 07fada6d07
12 changed files with 1709 additions and 94 deletions

View File

@ -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

View File

@ -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() {
<DatabaseProvider>
<ThemeProvider value={isDarkColorScheme ? DARK_THEME : LIGHT_THEME}>
<SettingsDrawerProvider>
{/* Add RelayInitializer here - it loads relay data once NDK is available */}
<RelayInitializer />
<StatusBar style={isDarkColorScheme ? 'light' : 'dark'} />
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen

View File

@ -0,0 +1,27 @@
// components/RelayInitializer.tsx
import React, { useEffect } from 'react';
import { View } from 'react-native';
import { useRelayStore } from '@/lib/stores/relayStore';
import { useNDKStore } from '@/lib/stores/ndk';
/**
* A component to initialize and load relay data when the app starts
* This should be placed high in the component tree, ideally in _layout.tsx
*/
export default function RelayInitializer() {
const { loadRelays } = useRelayStore();
const { ndk } = useNDKStore();
// Load relays when NDK is initialized
useEffect(() => {
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;
}

View File

@ -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 (
<View style={{ padding: 16, borderBottomWidth: 1, borderBottomColor: '#e5e7eb' }}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<View style={{ flexDirection: 'row', alignItems: 'center', flex: 1 }}>
<View
style={{
width: 12,
height: 12,
borderRadius: 6,
backgroundColor: getStatusColor(item.status),
marginRight: 8
}}
/>
<Text numberOfLines={1} style={{ flex: 1 }}>{item.url}</Text>
</View>
<TouchableOpacity
onPress={() => handleRemoveRelay(item.url)}
style={{ padding: 8 }}
>
<Text style={{ color: '#ef4444' }}>Remove</Text>
</TouchableOpacity>
</View>
<View style={{ flexDirection: 'row', marginTop: 12, justifyContent: 'space-around' }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ marginRight: 8 }}>Read</Text>
<Switch
value={item.read}
onValueChange={() => handleTogglePermission(item.url, 'read')}
/>
</View>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ marginRight: 8 }}>Write</Text>
<Switch
value={item.write}
onValueChange={() => handleTogglePermission(item.url, 'write')}
/>
</View>
</View>
</View>
);
};
// 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 (
<Modal
visible={isVisible}
animationType="slide"
transparent={true}
onRequestClose={onClose}
>
<View style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'center', alignItems: 'center' }}>
<View style={{ width: '90%', maxHeight: '80%', backgroundColor: '#fff', borderRadius: 12 }}>
{/* Header */}
<View style={{ padding: 16, borderBottomWidth: 1, borderBottomColor: '#e5e7eb', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<Text style={{ fontSize: 18, fontWeight: 'bold' }}>Manage Relays</Text>
<TouchableOpacity onPress={onClose}>
<Text>Close</Text>
</TouchableOpacity>
</View>
{/* Content */}
<View style={{ flex: 1 }}>
{isLoading ? (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 }}>
<ActivityIndicator size="large" />
<Text style={{ marginTop: 16 }}>Loading relays...</Text>
</View>
) : (
<>
{/* Relay list */}
{relays.length === 0 ? (
<View style={{ padding: 20, alignItems: 'center' }}>
<Text style={{ marginBottom: 16 }}>No relays configured</Text>
<TouchableOpacity
onPress={handleResetToDefaults}
style={{ padding: 10, backgroundColor: '#e5e7eb', borderRadius: 8 }}
>
<Text>Reset to Defaults</Text>
</TouchableOpacity>
</View>
) : (
<FlatList
data={relays}
keyExtractor={(item) => item.url}
renderItem={renderRelayItem}
ListEmptyComponent={
<View style={{ padding: 20, alignItems: 'center' }}>
<Text>No relays found. Try resetting to defaults.</Text>
</View>
}
/>
)}
{/* Add relay section */}
{isAddingRelay ? (
<View style={{ padding: 16, borderTopWidth: 1, borderTopColor: '#e5e7eb' }}>
<Text style={{ marginBottom: 8 }}>Add New Relay</Text>
<TextInput
style={{
borderWidth: 1,
borderColor: '#e5e7eb',
borderRadius: 8,
padding: 10,
marginBottom: 12
}}
placeholder="wss://relay.example.com"
value={newRelayUrl}
onChangeText={setNewRelayUrl}
autoCapitalize="none"
/>
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
<TouchableOpacity
onPress={() => setIsAddingRelay(false)}
style={{
padding: 10,
backgroundColor: '#e5e7eb',
borderRadius: 8,
flex: 1,
marginRight: 8,
alignItems: 'center'
}}
>
<Text>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={handleAddRelay}
style={{
padding: 10,
backgroundColor: '#3b82f6',
borderRadius: 8,
flex: 1,
alignItems: 'center'
}}
disabled={!newRelayUrl.startsWith('wss://')}
>
<Text style={{ color: '#fff' }}>Add Relay</Text>
</TouchableOpacity>
</View>
</View>
) : (
<View style={{ padding: 16, borderTopWidth: 1, borderTopColor: '#e5e7eb' }}>
<TouchableOpacity
onPress={() => setIsAddingRelay(true)}
style={{
padding: 10,
backgroundColor: '#e5e7eb',
borderRadius: 8,
alignItems: 'center'
}}
>
<Text>Add New Relay</Text>
</TouchableOpacity>
</View>
)}
</>
)}
</View>
{/* Footer */}
<View style={{ padding: 16, borderTopWidth: 1, borderTopColor: '#e5e7eb' }}>
<TouchableOpacity
onPress={handleApplyChanges}
style={{
padding: 14,
backgroundColor: '#3b82f6',
borderRadius: 8,
alignItems: 'center'
}}
disabled={isSaving}
>
{isSaving ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<Text style={{ color: '#fff', fontWeight: '500' }}>Apply Changes</Text>
)}
</TouchableOpacity>
<TouchableOpacity
onPress={handleResetToDefaults}
style={{
padding: 14,
backgroundColor: 'transparent',
borderRadius: 8,
alignItems: 'center',
marginTop: 8
}}
>
<Text>Reset to Defaults</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
);
}

View File

@ -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 */}
<RelayManagement
isVisible={showRelayManager}
onClose={() => setShowRelayManager(false)}
/>
{/* Sign Out Alert Dialog */}
<AlertDialog open={showSignOutAlert} onOpenChange={setShowSignOutAlert}>
<AlertDialogContent>

View File

@ -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<number> {
@ -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

View File

@ -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<RelayConfig[]> {
try {
const relays = await this.db.getAllAsync<RelayConfig>(
'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<RelayWithStatus[]> {
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<boolean> {
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<RelayConfig>): Promise<boolean> {
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<boolean> {
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<string[]> {
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
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<string[]> {
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<void> {
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<RelayConfig>(
'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<void> {
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);
}
}
}

View File

@ -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<string, 'connected' | 'connecting' | 'disconnected' | 'error'> = {};
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<void>((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<boolean> {
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<boolean>((_, 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;
}
}

View File

@ -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<NDKStoreState & NDKStoreActions>((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,

259
lib/stores/relayStore.ts Normal file
View File

@ -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<void>;
addRelay: (url: string, read?: boolean, write?: boolean) => Promise<void>;
removeRelay: (url: string) => Promise<void>;
updateRelay: (url: string, changes: Partial<RelayWithStatus>) => Promise<void>;
applyChanges: () => Promise<boolean>;
resetToDefaults: () => Promise<void>;
importFromMetadata: (pubkey: string) => Promise<void>;
publishRelayList: () => Promise<boolean>;
}
// Create the relay store
export const useRelayStore = create<RelayStoreState & RelayStoreActions>((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<RelayWithStatus> = {};
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)
};
}

125
types/ndk-common.ts Normal file
View File

@ -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<string, any>;
getRelay: (url: string) => any;
};
connect: () => Promise<void>;
disconnect: () => void;
fetchEvents: (filter: any) => Promise<Set<any>>;
signer?: any;
}
// Define a universal NDKRelay interface
export interface NDKRelayCommon {
url: string;
status: number;
connect: () => Promise<void>;
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
}
};
}
}

81
types/ndk-extensions.ts Normal file
View File

@ -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;
}