mirror of
https://github.com/DocNR/POWR.git
synced 2025-04-19 10:51:19 +00:00
new relay managment system
This commit is contained in:
parent
29c4dd1675
commit
07fada6d07
27
CHANGELOG.md
27
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
|
||||
|
@ -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
|
||||
|
27
components/RelayInitializer.tsx
Normal file
27
components/RelayInitializer.tsx
Normal 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;
|
||||
}
|
369
components/RelayManagement.tsx
Normal file
369
components/RelayManagement.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
|
@ -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
|
||||
|
698
lib/db/services/RelayService.ts
Normal file
698
lib/db/services/RelayService.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
159
lib/initNDK.ts
159
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<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;
|
||||
}
|
||||
}
|
@ -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
259
lib/stores/relayStore.ts
Normal 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
125
types/ndk-common.ts
Normal 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
81
types/ndk-extensions.ts
Normal 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;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user