mirror of
https://github.com/DocNR/POWR.git
synced 2025-06-06 18:31:03 +00:00
improved relay system, fixed bugs
This commit is contained in:
parent
4cd62cf775
commit
704fa27950
@ -1,7 +1,7 @@
|
|||||||
// app/(tabs)/index.tsx
|
// app/(tabs)/index.tsx
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { ScrollView, View, TouchableOpacity } from 'react-native'
|
import { ScrollView, View, TouchableOpacity, Platform } from 'react-native'
|
||||||
import { useFocusEffect } from '@react-navigation/native';
|
import { useFocusEffect, useTheme } from '@react-navigation/native';
|
||||||
import { router } from 'expo-router'
|
import { router } from 'expo-router'
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
@ -15,8 +15,6 @@ import {
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { TabScreen } from '@/components/layout/TabScreen'
|
import { TabScreen } from '@/components/layout/TabScreen'
|
||||||
import Header from '@/components/Header'
|
import Header from '@/components/Header'
|
||||||
import HomeWorkout from '@/components/workout/HomeWorkout'
|
|
||||||
import FavoriteTemplate from '@/components/workout/FavoriteTemplate'
|
|
||||||
import { useWorkoutStore } from '@/stores/workoutStore'
|
import { useWorkoutStore } from '@/stores/workoutStore'
|
||||||
import { Text } from '@/components/ui/text'
|
import { Text } from '@/components/ui/text'
|
||||||
import { getRandomWorkoutTitle } from '@/utils/workoutTitles'
|
import { getRandomWorkoutTitle } from '@/utils/workoutTitles'
|
||||||
@ -63,6 +61,8 @@ export default function WorkoutScreen() {
|
|||||||
endWorkout
|
endWorkout
|
||||||
} = useWorkoutStore();
|
} = useWorkoutStore();
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
loadFavorites();
|
loadFavorites();
|
||||||
@ -241,7 +241,10 @@ export default function WorkoutScreen() {
|
|||||||
onPress={() => console.log('Open notifications')}
|
onPress={() => console.log('Open notifications')}
|
||||||
>
|
>
|
||||||
<View className="relative">
|
<View className="relative">
|
||||||
<Bell className="text-foreground" />
|
<Bell size={24} color={Platform.select({
|
||||||
|
ios: undefined,
|
||||||
|
android: '#8B5CF6'
|
||||||
|
})} />
|
||||||
<View className="absolute -top-1 -right-1 w-2 h-2 bg-primary rounded-full" />
|
<View className="absolute -top-1 -right-1 w-2 h-2 bg-primary rounded-full" />
|
||||||
</View>
|
</View>
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -161,74 +161,28 @@ export default function ProgramsScreen() {
|
|||||||
console.warn('[Database Reset] Error clearing keys:', keyError);
|
console.warn('[Database Reset] Error clearing keys:', keyError);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define explicit type for tables
|
// Use the new complete reset method
|
||||||
let tables: { name: string }[] = [];
|
await schema.resetDatabaseCompletely(db);
|
||||||
|
|
||||||
// Try to get existing tables
|
|
||||||
try {
|
|
||||||
tables = await db.getAllAsync<{ name: string }>(
|
|
||||||
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
|
|
||||||
);
|
|
||||||
console.log(`[Database Reset] Found ${tables.length} tables to drop`);
|
|
||||||
} catch (tableListError) {
|
|
||||||
console.warn('[Database Reset] Error listing tables:', tableListError);
|
|
||||||
// Initialize with empty array if query fails
|
|
||||||
tables = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drop tables one by one
|
|
||||||
for (const table of tables) {
|
|
||||||
try {
|
|
||||||
await db.execAsync(`DROP TABLE IF EXISTS "${table.name}";`);
|
|
||||||
console.log(`[Database Reset] Dropped table: ${table.name}`);
|
|
||||||
} catch (dropError) {
|
|
||||||
console.error(`[Database Reset] Error dropping table ${table.name}:`, dropError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use a delay to allow any pending operations to complete
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
// Create a completely new database instance instead of using the existing one
|
|
||||||
// This will bypass the "Access to closed resource" issue
|
|
||||||
Alert.alert(
|
|
||||||
'Database Tables Dropped',
|
|
||||||
'All database tables have been dropped. The app needs to be restarted to complete the reset process.',
|
|
||||||
[
|
|
||||||
{
|
|
||||||
text: 'Restart Now',
|
|
||||||
style: 'destructive',
|
|
||||||
onPress: () => {
|
|
||||||
// In a production app, you would use something like RN's DevSettings.reload()
|
|
||||||
// For Expo, we'll suggest manual restart
|
|
||||||
Alert.alert(
|
|
||||||
'Manual Restart Required',
|
|
||||||
'Please completely close the app and reopen it to finish the database reset.',
|
|
||||||
[{ text: 'OK', style: 'default' }]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// Show success message
|
||||||
setTestResults({
|
setTestResults({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Database tables dropped. Please restart the app to complete the reset.'
|
message: 'Database completely reset. The app will need to be restarted to see the changes.'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Recommend a restart
|
||||||
|
Alert.alert(
|
||||||
|
'Database Reset Complete',
|
||||||
|
'The database has been completely reset. Please restart the app for the changes to take effect fully.',
|
||||||
|
[{ text: 'OK', style: 'default' }]
|
||||||
|
);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Database Reset] Error resetting database:', error);
|
console.error('[Database Reset] Error resetting database:', error);
|
||||||
setTestResults({
|
setTestResults({
|
||||||
success: false,
|
success: false,
|
||||||
message: error instanceof Error ? error.message : 'Unknown error during database reset'
|
message: error instanceof Error ? error.message : 'Unknown error during database reset'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Still recommend a restart since the database might be in an inconsistent state
|
|
||||||
Alert.alert(
|
|
||||||
'Database Reset Error',
|
|
||||||
'There was an error during database reset. Please restart the app and try again.',
|
|
||||||
[{ text: 'OK', style: 'default' }]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -36,6 +36,32 @@ interface DatabaseProviderProps {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a DelayedInitializer component to ensure database is fully ready
|
||||||
|
const DelayedInitializer: React.FC<{children: React.ReactNode}> = ({children}) => {
|
||||||
|
const [ready, setReady] = React.useState(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// Small delay to ensure database is fully ready
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
console.log('[Database] Delayed initialization complete');
|
||||||
|
setReady(true);
|
||||||
|
}, 300); // 300ms delay should be sufficient
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!ready) {
|
||||||
|
return (
|
||||||
|
<View className="flex-1 items-center justify-center bg-background">
|
||||||
|
<ActivityIndicator size="small" className="mb-2" />
|
||||||
|
<Text className="text-foreground text-sm">Finishing initialization...</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
export function DatabaseProvider({ children }: DatabaseProviderProps) {
|
export function DatabaseProvider({ children }: DatabaseProviderProps) {
|
||||||
const [isReady, setIsReady] = React.useState(false);
|
const [isReady, setIsReady] = React.useState(false);
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
@ -63,12 +89,20 @@ export function DatabaseProvider({ children }: DatabaseProviderProps) {
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
async function initDatabase() {
|
async function initDatabase() {
|
||||||
try {
|
try {
|
||||||
|
console.log('[DB] Starting database initialization...');
|
||||||
|
|
||||||
|
// Add a small delay to ensure system is ready (especially on Android)
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
|
|
||||||
console.log('[DB] Opening database...');
|
console.log('[DB] Opening database...');
|
||||||
const db = openDatabaseSync('powr.db');
|
const db = openDatabaseSync('powr.db');
|
||||||
|
|
||||||
console.log('[DB] Creating schema...');
|
console.log('[DB] Creating schema...');
|
||||||
await schema.createTables(db);
|
await schema.createTables(db);
|
||||||
|
|
||||||
|
// Explicitly check for critical tables after schema creation
|
||||||
|
await schema.ensureCriticalTablesExist(db);
|
||||||
|
|
||||||
// Initialize services
|
// Initialize services
|
||||||
console.log('[DB] Initializing services...');
|
console.log('[DB] Initializing services...');
|
||||||
const exerciseService = new ExerciseService(db);
|
const exerciseService = new ExerciseService(db);
|
||||||
@ -101,7 +135,12 @@ export function DatabaseProvider({ children }: DatabaseProviderProps) {
|
|||||||
// Seed development database
|
// Seed development database
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
console.log('[DB] Seeding development database...');
|
console.log('[DB] Seeding development database...');
|
||||||
|
try {
|
||||||
await devSeeder.seedDatabase();
|
await devSeeder.seedDatabase();
|
||||||
|
} catch (seedError) {
|
||||||
|
console.error('[DB] Error seeding database:', seedError);
|
||||||
|
// Continue even if seeding fails
|
||||||
|
}
|
||||||
await logDatabaseInfo();
|
await logDatabaseInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,7 +176,9 @@ export function DatabaseProvider({ children }: DatabaseProviderProps) {
|
|||||||
return (
|
return (
|
||||||
<SQLiteProvider databaseName="powr.db">
|
<SQLiteProvider databaseName="powr.db">
|
||||||
<DatabaseServicesContext.Provider value={services}>
|
<DatabaseServicesContext.Provider value={services}>
|
||||||
|
<DelayedInitializer>
|
||||||
{children}
|
{children}
|
||||||
|
</DelayedInitializer>
|
||||||
</DatabaseServicesContext.Provider>
|
</DatabaseServicesContext.Provider>
|
||||||
</SQLiteProvider>
|
</SQLiteProvider>
|
||||||
);
|
);
|
||||||
|
@ -77,7 +77,7 @@ export default function Header({
|
|||||||
onPress={() => {}}
|
onPress={() => {}}
|
||||||
>
|
>
|
||||||
<View className="relative">
|
<View className="relative">
|
||||||
<Bell size={24} className="text-foreground" />
|
<Bell size={24} color={theme.colors.text} />
|
||||||
{/* Notification indicator - you can conditionally render this */}
|
{/* Notification indicator - you can conditionally render this */}
|
||||||
<View className="absolute -top-1 -right-1 w-2 h-2 bg-primary rounded-full" />
|
<View className="absolute -top-1 -right-1 w-2 h-2 bg-primary rounded-full" />
|
||||||
</View>
|
</View>
|
||||||
|
@ -1,23 +1,31 @@
|
|||||||
|
// components/RelayManagement.tsx
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { View, Text, FlatList, TouchableOpacity, Modal, ActivityIndicator } from 'react-native';
|
import { View, Text, FlatList, TouchableOpacity, Modal, ActivityIndicator, TextInput, SafeAreaView, KeyboardAvoidingView, Platform } from 'react-native';
|
||||||
import { useRelayStore } from '@/lib/stores/relayStore';
|
import { useRelayStore } from '@/lib/stores/relayStore';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { X } from 'lucide-react-native';
|
import { X, RefreshCw, PlusCircle, AlertTriangle } from 'lucide-react-native';
|
||||||
import { RelayWithStatus } from '@/lib/db/services/RelayService';
|
import { RelayWithStatus } from '@/lib/db/services/RelayService';
|
||||||
|
|
||||||
// Define proper interface for component props
|
|
||||||
interface RelayManagementProps {
|
interface RelayManagementProps {
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple RelayManagement component
|
|
||||||
export default function RelayManagement({ isVisible, onClose }: RelayManagementProps) {
|
export default function RelayManagement({ isVisible, onClose }: RelayManagementProps) {
|
||||||
const relays = useRelayStore(state => state.relays);
|
const relays = useRelayStore(state => state.relays);
|
||||||
const isLoading = useRelayStore(state => state.isLoading);
|
const isLoading = useRelayStore(state => state.isLoading);
|
||||||
|
const isSaving = useRelayStore(state => state.isSaving);
|
||||||
const loadRelays = useRelayStore(state => state.loadRelays);
|
const loadRelays = useRelayStore(state => state.loadRelays);
|
||||||
const updateRelay = useRelayStore(state => state.updateRelay);
|
const updateRelay = useRelayStore(state => state.updateRelay);
|
||||||
|
const applyChanges = useRelayStore(state => state.applyChanges);
|
||||||
|
const resetToDefaults = useRelayStore(state => state.resetToDefaults);
|
||||||
|
const addRelay = useRelayStore(state => state.addRelay);
|
||||||
|
|
||||||
|
const [newRelayUrl, setNewRelayUrl] = useState('');
|
||||||
|
const [showAddRelay, setShowAddRelay] = useState(false);
|
||||||
|
const [confirmReset, setConfirmReset] = useState(false);
|
||||||
|
const [hasUnappliedChanges, setHasUnappliedChanges] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
@ -25,7 +33,52 @@ export default function RelayManagement({ isVisible, onClose }: RelayManagementP
|
|||||||
}
|
}
|
||||||
}, [isVisible]);
|
}, [isVisible]);
|
||||||
|
|
||||||
// Status indicator color with proper typing
|
// Track if there are unapplied changes
|
||||||
|
const handleRelayUpdate = (url: string, changes: Partial<RelayWithStatus>) => {
|
||||||
|
updateRelay(url, changes);
|
||||||
|
setHasUnappliedChanges(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle applying changes
|
||||||
|
const handleApplyChanges = async () => {
|
||||||
|
const success = await applyChanges();
|
||||||
|
if (success) {
|
||||||
|
setHasUnappliedChanges(false);
|
||||||
|
// Success notification could be added here
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add new relay
|
||||||
|
const handleAddRelay = async () => {
|
||||||
|
if (!newRelayUrl || !newRelayUrl.startsWith('wss://')) {
|
||||||
|
alert('Please enter a valid relay URL starting with wss://');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await addRelay(newRelayUrl);
|
||||||
|
setNewRelayUrl('');
|
||||||
|
setShowAddRelay(false);
|
||||||
|
setHasUnappliedChanges(true);
|
||||||
|
} catch (error) {
|
||||||
|
alert(`Failed to add relay: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset to defaults with confirmation
|
||||||
|
const handleResetToDefaults = async () => {
|
||||||
|
if (confirmReset) {
|
||||||
|
await resetToDefaults();
|
||||||
|
setConfirmReset(false);
|
||||||
|
setHasUnappliedChanges(true);
|
||||||
|
} else {
|
||||||
|
setConfirmReset(true);
|
||||||
|
// Auto-reset the confirmation after 3 seconds
|
||||||
|
setTimeout(() => setConfirmReset(false), 3000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Status indicator color
|
||||||
const getStatusColor = (status: string): string => {
|
const getStatusColor = (status: string): string => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'connected': return '#10b981'; // Green
|
case 'connected': return '#10b981'; // Green
|
||||||
@ -36,58 +89,49 @@ export default function RelayManagement({ isVisible, onClose }: RelayManagementP
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Render a relay item with proper typing
|
// Render a relay item
|
||||||
const renderRelayItem = ({ item }: { item: RelayWithStatus }) => (
|
const renderRelayItem = ({ item }: { item: RelayWithStatus }) => (
|
||||||
<View style={{
|
<View className="p-4 bg-card mb-2 rounded-lg border border-border">
|
||||||
padding: 12,
|
<View className="flex-row items-center justify-between mb-2">
|
||||||
borderBottomWidth: 1,
|
<View className="flex-row items-center flex-1">
|
||||||
borderBottomColor: '#e5e7eb',
|
<View
|
||||||
backgroundColor: 'white',
|
style={{
|
||||||
}}>
|
width: 10,
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
height: 10,
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', flex: 1 }}>
|
borderRadius: 5,
|
||||||
<View style={{
|
|
||||||
width: 8,
|
|
||||||
height: 8,
|
|
||||||
borderRadius: 4,
|
|
||||||
backgroundColor: getStatusColor(item.status),
|
backgroundColor: getStatusColor(item.status),
|
||||||
marginRight: 8
|
marginRight: 8
|
||||||
}} />
|
}}
|
||||||
<Text style={{ flex: 1 }}>{item.url}</Text>
|
/>
|
||||||
|
<Text className="text-foreground flex-1" numberOfLines={1}>{item.url}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text style={{ fontSize: 12, color: '#6b7280' }}>
|
<Text className="text-xs text-muted-foreground capitalize">
|
||||||
{item.status}
|
{item.status}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={{
|
<View className="flex-row justify-between items-center mt-3 pt-3 border-t border-border/30">
|
||||||
flexDirection: 'row',
|
<View className="flex-row items-center">
|
||||||
justifyContent: 'space-around',
|
<Text className="text-foreground mr-2">Read</Text>
|
||||||
marginTop: 8,
|
|
||||||
paddingTop: 8,
|
|
||||||
borderTopWidth: 1,
|
|
||||||
borderTopColor: '#f3f4f6'
|
|
||||||
}}>
|
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
||||||
<Text style={{ marginRight: 8 }}>Read</Text>
|
|
||||||
<Switch
|
<Switch
|
||||||
checked={item.read}
|
checked={item.read}
|
||||||
onCheckedChange={() => updateRelay(item.url, { read: !item.read })}
|
onCheckedChange={() => handleRelayUpdate(item.url, { read: !item.read })}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
<View className="flex-row items-center">
|
||||||
<Text style={{ marginRight: 8 }}>Write</Text>
|
<Text className="text-foreground mr-2">Write</Text>
|
||||||
<Switch
|
<Switch
|
||||||
checked={item.write}
|
checked={item.write}
|
||||||
onCheckedChange={() => updateRelay(item.url, { write: !item.write })}
|
onCheckedChange={() => handleRelayUpdate(item.url, { write: !item.write })}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Main Render
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible={isVisible}
|
visible={isVisible}
|
||||||
@ -95,62 +139,132 @@ export default function RelayManagement({ isVisible, onClose }: RelayManagementP
|
|||||||
transparent={true}
|
transparent={true}
|
||||||
onRequestClose={onClose}
|
onRequestClose={onClose}
|
||||||
>
|
>
|
||||||
<View style={{
|
<KeyboardAvoidingView
|
||||||
flex: 1,
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
style={{ flex: 1 }}
|
||||||
justifyContent: 'flex-end',
|
>
|
||||||
}}>
|
<SafeAreaView style={{ flex: 1 }} className="bg-background/95">
|
||||||
<View style={{
|
{/* Header */}
|
||||||
backgroundColor: 'white',
|
<View className="p-4 flex-row items-center justify-between bg-card border-b border-border">
|
||||||
borderTopLeftRadius: 20,
|
<Text className="text-xl font-semibold text-foreground">Relay Management</Text>
|
||||||
borderTopRightRadius: 20,
|
|
||||||
paddingBottom: 20,
|
|
||||||
maxHeight: '80%',
|
|
||||||
}}>
|
|
||||||
<View style={{
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: 16,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: '#e5e7eb',
|
|
||||||
}}>
|
|
||||||
<Text style={{ fontSize: 18, fontWeight: 'bold' }}>Manage Relays</Text>
|
|
||||||
<TouchableOpacity onPress={onClose}>
|
<TouchableOpacity onPress={onClose}>
|
||||||
<X size={24} />
|
<X size={24} className="text-foreground" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<View className="flex-1 px-4 pt-2">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<View style={{ padding: 20, alignItems: 'center' }}>
|
<View className="flex-1 items-center justify-center">
|
||||||
<ActivityIndicator size="large" />
|
<ActivityIndicator size="large" />
|
||||||
<Text style={{ marginTop: 10 }}>Loading relays...</Text>
|
<Text className="mt-2 text-foreground">Loading relays...</Text>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
{/* Summary */}
|
||||||
|
<View className="flex-row justify-between mb-2 items-center">
|
||||||
|
<Text className="text-foreground">
|
||||||
|
{relays.length} Relays ({relays.filter(r => r.status === 'connected').length} Connected)
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity onPress={loadRelays} className="p-2">
|
||||||
|
<RefreshCw size={18} className="text-primary" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Relay List */}
|
||||||
|
{relays.length === 0 ? (
|
||||||
|
<View className="items-center justify-center py-8">
|
||||||
|
<Text className="text-muted-foreground mb-4">No relays configured</Text>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onPress={handleResetToDefaults}
|
||||||
|
>
|
||||||
|
<Text>Reset to Defaults</Text>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={relays}
|
data={relays}
|
||||||
keyExtractor={(item) => item.url}
|
|
||||||
renderItem={renderRelayItem}
|
renderItem={renderRelayItem}
|
||||||
style={{ maxHeight: '70%' }}
|
keyExtractor={(item) => item.url}
|
||||||
|
ListEmptyComponent={
|
||||||
|
<View className="items-center justify-center py-8">
|
||||||
|
<Text className="text-muted-foreground">No relays found</Text>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
contentContainerStyle={{ paddingBottom: showAddRelay ? 180 : 100 }}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<View style={{ padding: 16 }}>
|
{/* Add Relay Form */}
|
||||||
<Button onPress={loadRelays} style={{ marginBottom: 8 }}>
|
{showAddRelay && (
|
||||||
<Text style={{ color: 'white' }}>Refresh Relays</Text>
|
<View className="bg-card p-4 rounded-lg mb-4 mt-2 border border-border">
|
||||||
|
<Text className="text-foreground mb-2">Add New Relay</Text>
|
||||||
|
<TextInput
|
||||||
|
className="bg-background text-foreground p-2 rounded-md border border-border mb-2"
|
||||||
|
placeholder="wss://relay.example.com"
|
||||||
|
placeholderTextColor="#6b7280"
|
||||||
|
value={newRelayUrl}
|
||||||
|
onChangeText={setNewRelayUrl}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
/>
|
||||||
|
<View className="flex-row justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onPress={() => setShowAddRelay(false)}
|
||||||
|
>
|
||||||
|
<Text>Cancel</Text>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onPress={handleAddRelay}
|
||||||
|
disabled={!newRelayUrl.startsWith('wss://')}
|
||||||
|
>
|
||||||
|
<Text className="text-primary-foreground">Add Relay</Text>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<View className="p-4 bg-card border-t border-border">
|
||||||
|
<View className="flex-row gap-2 mb-2">
|
||||||
|
<Button
|
||||||
|
className="flex-1"
|
||||||
|
onPress={handleApplyChanges}
|
||||||
|
disabled={!hasUnappliedChanges || isSaving}
|
||||||
|
>
|
||||||
|
{isSaving ? (
|
||||||
|
<ActivityIndicator size="small" color="#fff" />
|
||||||
|
) : (
|
||||||
|
<Text className="text-primary-foreground">Apply Changes</Text>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onPress={onClose}
|
onPress={() => setShowAddRelay(true)}
|
||||||
|
disabled={showAddRelay}
|
||||||
>
|
>
|
||||||
<Text>Close</Text>
|
<PlusCircle size={18} className="mr-1" />
|
||||||
|
<Text>Add Relay</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</>
|
|
||||||
)}
|
<Button
|
||||||
</View>
|
variant={confirmReset ? "destructive" : "outline"}
|
||||||
|
onPress={handleResetToDefaults}
|
||||||
|
>
|
||||||
|
<Text className={confirmReset ? "text-destructive-foreground" : ""}>
|
||||||
|
{confirmReset ? "Confirm Reset" : "Reset to Defaults"}
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
178
lib/db/schema.ts
178
lib/db/schema.ts
@ -2,7 +2,7 @@
|
|||||||
import { SQLiteDatabase } from 'expo-sqlite';
|
import { SQLiteDatabase } from 'expo-sqlite';
|
||||||
import { Platform } from 'react-native';
|
import { Platform } from 'react-native';
|
||||||
|
|
||||||
export const SCHEMA_VERSION = 3; // Incrementing to add the relays table
|
export const SCHEMA_VERSION = 6; // Incremented from 5 to 6 for relay table removal
|
||||||
|
|
||||||
class Schema {
|
class Schema {
|
||||||
private async getCurrentVersion(db: SQLiteDatabase): Promise<number> {
|
private async getCurrentVersion(db: SQLiteDatabase): Promise<number> {
|
||||||
@ -44,9 +44,17 @@ class Schema {
|
|||||||
const currentVersion = await this.getCurrentVersion(db);
|
const currentVersion = await this.getCurrentVersion(db);
|
||||||
console.log(`[Schema] Current version: ${currentVersion}, Target version: ${SCHEMA_VERSION}`);
|
console.log(`[Schema] Current version: ${currentVersion}, Target version: ${SCHEMA_VERSION}`);
|
||||||
|
|
||||||
// If we already have the current version, no need to recreate tables
|
// If we already have the current version, check for missing tables
|
||||||
if (currentVersion === SCHEMA_VERSION) {
|
if (currentVersion === SCHEMA_VERSION) {
|
||||||
console.log(`[Schema] Database already at version ${SCHEMA_VERSION}`);
|
console.log(`[Schema] Database already at version ${SCHEMA_VERSION}, checking for missing tables`);
|
||||||
|
await this.ensureCriticalTablesExist(db);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle higher version numbers - especially important for Android
|
||||||
|
if (currentVersion > SCHEMA_VERSION) {
|
||||||
|
console.log(`[Schema] Database version ${currentVersion} is newer than target ${SCHEMA_VERSION}, checking for missing tables`);
|
||||||
|
await this.ensureCriticalTablesExist(db);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,6 +97,129 @@ class Schema {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add this method to check for and create critical tables
|
||||||
|
async ensureCriticalTablesExist(db: SQLiteDatabase): Promise<void> {
|
||||||
|
try {
|
||||||
|
console.log('[Schema] Checking for missing critical tables...');
|
||||||
|
|
||||||
|
// Check if workouts table exists
|
||||||
|
const workoutsTableExists = await db.getFirstAsync<{ count: number }>(
|
||||||
|
`SELECT count(*) as count FROM sqlite_master
|
||||||
|
WHERE type='table' AND name='workouts'`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!workoutsTableExists || workoutsTableExists.count === 0) {
|
||||||
|
console.log('[Schema] Creating missing workouts tables...');
|
||||||
|
|
||||||
|
// Create workouts table
|
||||||
|
await db.execAsync(`
|
||||||
|
CREATE TABLE IF NOT EXISTS workouts (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
start_time INTEGER NOT NULL,
|
||||||
|
end_time INTEGER,
|
||||||
|
is_completed BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL,
|
||||||
|
template_id TEXT,
|
||||||
|
nostr_event_id TEXT,
|
||||||
|
share_status TEXT NOT NULL DEFAULT 'local',
|
||||||
|
source TEXT NOT NULL DEFAULT 'local',
|
||||||
|
total_volume REAL,
|
||||||
|
total_reps INTEGER,
|
||||||
|
notes TEXT
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_workouts_start_time ON workouts(start_time);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_workouts_template_id ON workouts(template_id);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create workout_exercises table
|
||||||
|
await db.execAsync(`
|
||||||
|
CREATE TABLE IF NOT EXISTS workout_exercises (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
workout_id TEXT NOT NULL,
|
||||||
|
exercise_id TEXT NOT NULL,
|
||||||
|
display_order INTEGER NOT NULL,
|
||||||
|
notes TEXT,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY(workout_id) REFERENCES workouts(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_workout_exercises_workout_id ON workout_exercises(workout_id);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create workout_sets table
|
||||||
|
await db.execAsync(`
|
||||||
|
CREATE TABLE IF NOT EXISTS workout_sets (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
workout_exercise_id TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL DEFAULT 'normal',
|
||||||
|
weight REAL,
|
||||||
|
reps INTEGER,
|
||||||
|
rpe REAL,
|
||||||
|
duration INTEGER,
|
||||||
|
is_completed BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
completed_at INTEGER,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY(workout_exercise_id) REFERENCES workout_exercises(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_workout_sets_exercise_id ON workout_sets(workout_exercise_id);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if templates table exists
|
||||||
|
const templatesTableExists = await db.getFirstAsync<{ count: number }>(
|
||||||
|
`SELECT count(*) as count FROM sqlite_master
|
||||||
|
WHERE type='table' AND name='templates'`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!templatesTableExists || templatesTableExists.count === 0) {
|
||||||
|
console.log('[Schema] Creating missing templates tables...');
|
||||||
|
|
||||||
|
// Create templates table
|
||||||
|
await db.execAsync(`
|
||||||
|
CREATE TABLE IF NOT EXISTS templates (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL,
|
||||||
|
nostr_event_id TEXT,
|
||||||
|
source TEXT NOT NULL DEFAULT 'local',
|
||||||
|
parent_id TEXT
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_templates_updated_at ON templates(updated_at);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create template_exercises table
|
||||||
|
await db.execAsync(`
|
||||||
|
CREATE TABLE IF NOT EXISTS template_exercises (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
template_id TEXT NOT NULL,
|
||||||
|
exercise_id TEXT NOT NULL,
|
||||||
|
display_order INTEGER NOT NULL,
|
||||||
|
target_sets INTEGER,
|
||||||
|
target_reps INTEGER,
|
||||||
|
target_weight REAL,
|
||||||
|
notes TEXT,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY(template_id) REFERENCES templates(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_template_exercises_template_id ON template_exercises(template_id);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Schema] Critical tables check complete');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Schema] Error ensuring critical tables exist:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async dropAllTables(db: SQLiteDatabase): Promise<void> {
|
private async dropAllTables(db: SQLiteDatabase): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('[Schema] Getting list of tables to drop...');
|
console.log('[Schema] Getting list of tables to drop...');
|
||||||
@ -288,20 +419,6 @@ class Schema {
|
|||||||
CREATE INDEX idx_favorites_content_id ON favorites(content_id);
|
CREATE 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 === //
|
// === NEW TABLES === //
|
||||||
|
|
||||||
// Create workouts table
|
// Create workouts table
|
||||||
@ -427,25 +544,36 @@ class Schema {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async resetDatabase(db: SQLiteDatabase): Promise<void> {
|
async resetDatabaseCompletely(db: SQLiteDatabase): Promise<void> {
|
||||||
if (!__DEV__) {
|
if (!__DEV__) {
|
||||||
console.log('[Schema] Database reset is only available in development mode');
|
console.log('[Schema] Database reset is only available in development mode');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('[Schema] Resetting database...');
|
console.log('[Schema] Completely resetting database...');
|
||||||
|
|
||||||
// Clear schema_version to force recreation of all tables
|
// Get all tables
|
||||||
await db.execAsync('DROP TABLE IF EXISTS schema_version');
|
const tables = await db.getAllAsync<{ name: string }>(
|
||||||
console.log('[Schema] Dropped schema_version table');
|
"SELECT name FROM sqlite_master WHERE type='table'"
|
||||||
|
);
|
||||||
|
|
||||||
// Now create tables from scratch
|
// Drop all tables including schema_version
|
||||||
|
for (const { name } of tables) {
|
||||||
|
try {
|
||||||
|
await db.execAsync(`DROP TABLE IF EXISTS ${name}`);
|
||||||
|
console.log(`[Schema] Dropped table: ${name}`);
|
||||||
|
} catch (dropError) {
|
||||||
|
console.error(`[Schema] Error dropping table ${name}:`, dropError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create tables from scratch
|
||||||
await this.createTables(db);
|
await this.createTables(db);
|
||||||
|
|
||||||
console.log('[Schema] Database reset complete');
|
console.log('[Schema] Database completely reset');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Schema] Error resetting database:', error);
|
console.error('[Schema] Error completely resetting database:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import { logDatabaseInfo } from '../debug';
|
|||||||
import { mockExerciseEvents, convertNostrToExercise } from '../../mocks/exercises';
|
import { mockExerciseEvents, convertNostrToExercise } from '../../mocks/exercises';
|
||||||
import { DbService } from '../db-service';
|
import { DbService } from '../db-service';
|
||||||
import NDK, { NDKEvent } from '@nostr-dev-kit/ndk-mobile';
|
import NDK, { NDKEvent } from '@nostr-dev-kit/ndk-mobile';
|
||||||
|
import { NostrEvent } from '@/types/nostr'; // Assuming you have this type defined
|
||||||
|
|
||||||
export class DevSeederService {
|
export class DevSeederService {
|
||||||
private db: SQLiteDatabase;
|
private db: SQLiteDatabase;
|
||||||
@ -181,6 +182,71 @@ export class DevSeederService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed the database with real events from Nostr relays instead of mock data
|
||||||
|
* @param filter The filter to use when fetching events from relays
|
||||||
|
* @param limit Maximum number of events to seed (optional)
|
||||||
|
*/
|
||||||
|
async seedFromNostr(filter: any, limit?: number) {
|
||||||
|
if (!this.ndk) {
|
||||||
|
console.log('NDK not available for seeding from Nostr');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`Seeding from Nostr with filter:`, filter);
|
||||||
|
|
||||||
|
// Fetch events from relays
|
||||||
|
const events = await this.ndk.fetchEvents(filter);
|
||||||
|
|
||||||
|
console.log(`Found ${events.size} events on Nostr`);
|
||||||
|
|
||||||
|
// Convert to array and limit if needed
|
||||||
|
const eventsArray = Array.from(events);
|
||||||
|
const eventsToProcess = limit ? eventsArray.slice(0, limit) : eventsArray;
|
||||||
|
|
||||||
|
// Process each event individually
|
||||||
|
let successCount = 0;
|
||||||
|
|
||||||
|
for (const ndkEvent of eventsToProcess) {
|
||||||
|
try {
|
||||||
|
// Convert NDKEvent to your NostrEvent format
|
||||||
|
const nostrEvent: NostrEvent = {
|
||||||
|
id: ndkEvent.id || '',
|
||||||
|
pubkey: ndkEvent.pubkey || '',
|
||||||
|
created_at: ndkEvent.created_at || 0, // Set a default value of 0 if undefined
|
||||||
|
kind: ndkEvent.kind || 0,
|
||||||
|
tags: ndkEvent.tags || [],
|
||||||
|
content: ndkEvent.content || '',
|
||||||
|
sig: ndkEvent.sig || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cache the event
|
||||||
|
if (this.eventCache) {
|
||||||
|
await this.eventCache.setEvent(nostrEvent, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process based on kind
|
||||||
|
if (ndkEvent.kind === 33401) { // Exercise
|
||||||
|
const exercise = convertNostrToExercise(nostrEvent);
|
||||||
|
await this.exerciseService.createExercise(exercise, true);
|
||||||
|
successCount++;
|
||||||
|
}
|
||||||
|
// Add more event type processing here as needed
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error processing Nostr event:`, error);
|
||||||
|
// Continue with next event
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Successfully seeded ${successCount} items from Nostr`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error seeding from Nostr:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async clearDatabase() {
|
async clearDatabase() {
|
||||||
if (!__DEV__) return;
|
if (!__DEV__) return;
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -2,58 +2,39 @@
|
|||||||
import 'react-native-get-random-values'; // This must be the first import
|
import 'react-native-get-random-values'; // This must be the first import
|
||||||
import NDK, { NDKCacheAdapterSqlite } from '@nostr-dev-kit/ndk-mobile';
|
import NDK, { NDKCacheAdapterSqlite } from '@nostr-dev-kit/ndk-mobile';
|
||||||
import * as SecureStore from 'expo-secure-store';
|
import * as SecureStore from 'expo-secure-store';
|
||||||
import { openDatabaseSync } from 'expo-sqlite';
|
|
||||||
import { RelayService, DEFAULT_RELAYS } from '@/lib/db/services/RelayService';
|
import { RelayService, DEFAULT_RELAYS } from '@/lib/db/services/RelayService';
|
||||||
import { NDKCommon } from '@/types/ndk-common';
|
|
||||||
import { extendNDK } from '@/types/ndk-extensions';
|
import { extendNDK } from '@/types/ndk-extensions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize NDK with relays from database or defaults
|
* Initialize NDK with relays
|
||||||
*/
|
*/
|
||||||
export async function initializeNDK() {
|
export async function initializeNDK() {
|
||||||
console.log('[NDK] Initializing NDK with mobile adapter...');
|
console.log('[NDK] Initializing NDK with mobile adapter...');
|
||||||
|
|
||||||
// Create a mobile-specific cache adapter
|
// Create a mobile-specific cache adapter
|
||||||
const cacheAdapter = new NDKCacheAdapterSqlite('powr', 1000);
|
const cacheAdapter = new NDKCacheAdapterSqlite('powr', 1000);
|
||||||
|
await cacheAdapter.initialize();
|
||||||
|
|
||||||
// Initialize database and relay service
|
// Initialize relay service
|
||||||
const db = openDatabaseSync('powr.db');
|
const relayService = new RelayService();
|
||||||
const relayService = new RelayService(db);
|
|
||||||
|
|
||||||
relayService.enableDebug();
|
relayService.enableDebug();
|
||||||
|
|
||||||
// Load relays from database or use defaults
|
|
||||||
console.log('[NDK] Loading relay configuration...');
|
|
||||||
let relays: string[];
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Try to initialize relays from database (will add defaults if none exist)
|
|
||||||
relays = await relayService.initializeRelays();
|
|
||||||
console.log(`[NDK] Loaded ${relays.length} relays from database:`, relays);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[NDK] Error loading relays from database:', error);
|
|
||||||
console.log('[NDK] Falling back to default relays');
|
|
||||||
relays = DEFAULT_RELAYS;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create settings store
|
// Create settings store
|
||||||
const settingsStore = {
|
const settingsStore = {
|
||||||
get: SecureStore.getItemAsync,
|
get: SecureStore.getItemAsync,
|
||||||
set: SecureStore.setItemAsync,
|
set: SecureStore.setItemAsync,
|
||||||
delete: SecureStore.deleteItemAsync,
|
delete: SecureStore.deleteItemAsync,
|
||||||
getSync: (key: string) => {
|
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');
|
console.log('[Settings] Warning: getSync called but returning null, not supported in this implementation');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize NDK with options
|
// Initialize NDK with default relays first
|
||||||
console.log(`[NDK] Creating NDK instance with ${relays.length} relays`);
|
console.log(`[NDK] Creating NDK instance with default relays`);
|
||||||
let ndk = new NDK({
|
let ndk = new NDK({
|
||||||
cacheAdapter,
|
cacheAdapter,
|
||||||
explicitRelayUrls: relays,
|
explicitRelayUrls: DEFAULT_RELAYS,
|
||||||
enableOutboxModel: true,
|
enableOutboxModel: true,
|
||||||
autoConnectUserRelays: true,
|
autoConnectUserRelays: true,
|
||||||
clientName: 'powr',
|
clientName: 'powr',
|
||||||
@ -62,41 +43,8 @@ export async function initializeNDK() {
|
|||||||
// Extend NDK with helper methods for better compatibility
|
// Extend NDK with helper methods for better compatibility
|
||||||
ndk = extendNDK(ndk);
|
ndk = extendNDK(ndk);
|
||||||
|
|
||||||
// Initialize cache adapter
|
// Set the NDK instance in the RelayService
|
||||||
await cacheAdapter.initialize();
|
relayService.setNDK(ndk);
|
||||||
|
|
||||||
// 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'> = {};
|
|
||||||
relays.forEach(url => {
|
|
||||||
relayStatus[url] = 'connecting';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set up listeners before connecting
|
|
||||||
relays.forEach(url => {
|
|
||||||
const relay = ndk.pool.getRelay(url);
|
|
||||||
if (relay) {
|
|
||||||
// Connection success
|
|
||||||
relay.on('connect', () => {
|
|
||||||
console.log(`[NDK] Relay connected: ${url}`);
|
|
||||||
relayStatus[url] = 'connected';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Connection closed
|
|
||||||
relay.on('disconnect', () => {
|
|
||||||
console.log(`[NDK] Relay disconnected: ${url}`);
|
|
||||||
relayStatus[url] = 'disconnected';
|
|
||||||
});
|
|
||||||
|
|
||||||
// For errors, use the notice event which is used for errors in NDK
|
|
||||||
relay.on('notice', (notice: string) => {
|
|
||||||
console.error(`[NDK] Relay notice/error for ${url}:`, notice);
|
|
||||||
relayStatus[url] = 'error';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('[NDK] Connecting to relays...');
|
console.log('[NDK] Connecting to relays...');
|
||||||
@ -105,27 +53,24 @@ export async function initializeNDK() {
|
|||||||
// Wait a moment for connections to establish
|
// Wait a moment for connections to establish
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
// Get updated relay statuses
|
||||||
|
const relaysWithStatus = await relayService.getAllRelaysWithStatus();
|
||||||
|
|
||||||
// Count connected relays
|
// Count connected relays
|
||||||
const connectedRelays = Object.entries(relayStatus)
|
const connectedRelays = relaysWithStatus
|
||||||
.filter(([_, status]) => status === 'connected')
|
.filter(relay => relay.status === 'connected')
|
||||||
.map(([url]) => url);
|
.map(relay => relay.url);
|
||||||
|
|
||||||
console.log(`[NDK] Connected to ${connectedRelays.length}/${relays.length} relays`);
|
console.log(`[NDK] Connected to ${connectedRelays.length}/${relaysWithStatus.length} relays`);
|
||||||
|
|
||||||
// Add more detailed relay status logging
|
// Log detailed relay status
|
||||||
console.log('[NDK] Detailed relay status:');
|
console.log('[NDK] Detailed relay status:');
|
||||||
relays.forEach(url => {
|
relaysWithStatus.forEach(relay => {
|
||||||
const relay = ndk.pool.getRelay(url);
|
console.log(` - ${relay.url}: ${relay.status}`);
|
||||||
console.log(` - ${url}: ${relay ?
|
|
||||||
(relay.status === 1 ? 'connected' :
|
|
||||||
relay.status === 0 ? 'connecting' :
|
|
||||||
relay.status === 3 ? 'disconnected' :
|
|
||||||
`status=${relay.status}`) : 'not found'}`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ndk,
|
ndk,
|
||||||
relayStatus,
|
|
||||||
relayService,
|
relayService,
|
||||||
connectedRelayCount: connectedRelays.length,
|
connectedRelayCount: connectedRelays.length,
|
||||||
connectedRelays
|
connectedRelays
|
||||||
@ -135,7 +80,6 @@ export async function initializeNDK() {
|
|||||||
// Still return the NDK instance so the app can work offline
|
// Still return the NDK instance so the app can work offline
|
||||||
return {
|
return {
|
||||||
ndk,
|
ndk,
|
||||||
relayStatus,
|
|
||||||
relayService,
|
relayService,
|
||||||
connectedRelayCount: 0,
|
connectedRelayCount: 0,
|
||||||
connectedRelays: []
|
connectedRelays: []
|
||||||
|
@ -6,11 +6,10 @@ import NDK, {
|
|||||||
NDKUser,
|
NDKUser,
|
||||||
NDKRelay,
|
NDKRelay,
|
||||||
NDKPrivateKeySigner
|
NDKPrivateKeySigner
|
||||||
} from '@nostr-dev-kit/ndk';
|
} from '@nostr-dev-kit/ndk-mobile';
|
||||||
import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools';
|
import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools';
|
||||||
import * as SecureStore from 'expo-secure-store';
|
import * as SecureStore from 'expo-secure-store';
|
||||||
import { RelayService } from '@/lib/db/services/RelayService';
|
import { RelayService } from '@/lib/db/services/RelayService';
|
||||||
import { openDatabaseSync } from 'expo-sqlite';
|
|
||||||
|
|
||||||
// Constants for SecureStore
|
// Constants for SecureStore
|
||||||
const PRIVATE_KEY_STORAGE_KEY = 'nostr_privkey';
|
const PRIVATE_KEY_STORAGE_KEY = 'nostr_privkey';
|
||||||
@ -133,85 +132,129 @@ export const useNDKStore = create<NDKStoreState & NDKStoreActions>((set, get) =>
|
|||||||
|
|
||||||
login: async (privateKeyInput?: string) => {
|
login: async (privateKeyInput?: string) => {
|
||||||
set({ isLoading: true, error: null });
|
set({ isLoading: true, error: null });
|
||||||
|
console.log('[NDK] Login attempt starting');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { ndk } = get();
|
const { ndk } = get();
|
||||||
if (!ndk) {
|
if (!ndk) {
|
||||||
|
console.log('[NDK] Error: NDK not initialized');
|
||||||
throw new Error('NDK not initialized');
|
throw new Error('NDK not initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[NDK] Processing private key input');
|
||||||
|
|
||||||
// If no private key is provided, generate one
|
// If no private key is provided, generate one
|
||||||
let privateKeyHex = privateKeyInput;
|
let privateKeyHex = privateKeyInput;
|
||||||
if (!privateKeyHex) {
|
if (!privateKeyHex) {
|
||||||
|
console.log('[NDK] No key provided, generating new key');
|
||||||
const { privateKey } = get().generateKeys();
|
const { privateKey } = get().generateKeys();
|
||||||
privateKeyHex = privateKey;
|
privateKeyHex = privateKey;
|
||||||
|
} else {
|
||||||
|
console.log('[NDK] Using provided key, format:',
|
||||||
|
privateKeyHex.startsWith('nsec') ? 'nsec' : 'hex',
|
||||||
|
'length:', privateKeyHex.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle nsec format
|
// Handle nsec format
|
||||||
if (privateKeyHex.startsWith('nsec')) {
|
if (privateKeyHex.startsWith('nsec')) {
|
||||||
try {
|
try {
|
||||||
|
console.log('[NDK] Decoding nsec format key');
|
||||||
const decoded = nip19.decode(privateKeyHex);
|
const decoded = nip19.decode(privateKeyHex);
|
||||||
|
console.log('[NDK] Decoded type:', decoded.type);
|
||||||
if (decoded.type === 'nsec') {
|
if (decoded.type === 'nsec') {
|
||||||
// Get the data as hex
|
// Get the data as hex
|
||||||
privateKeyHex = bytesToHex(decoded.data as any);
|
privateKeyHex = bytesToHex(decoded.data as any);
|
||||||
|
console.log('[NDK] Converted to hex, new length:', privateKeyHex.length);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error decoding nsec:', error);
|
console.error('[NDK] Error decoding nsec:', error);
|
||||||
throw new Error('Invalid nsec format');
|
throw new Error('Invalid nsec format');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[NDK] Creating signer with key length:', privateKeyHex.length);
|
||||||
|
|
||||||
// Create signer with private key
|
// Create signer with private key
|
||||||
const signer = new NDKPrivateKeySigner(privateKeyHex);
|
const signer = new NDKPrivateKeySigner(privateKeyHex);
|
||||||
|
console.log('[NDK] Signer created, setting on NDK');
|
||||||
ndk.signer = signer;
|
ndk.signer = signer;
|
||||||
|
|
||||||
// Get user
|
// Get user
|
||||||
|
console.log('[NDK] Getting user from signer');
|
||||||
const user = await ndk.signer.user();
|
const user = await ndk.signer.user();
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
console.log('[NDK] Error: Could not get user from signer');
|
||||||
throw new Error('Could not get user from signer');
|
throw new Error('Could not get user from signer');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[NDK] User retrieved, pubkey:', user.pubkey ? user.pubkey.substring(0, 8) + '...' : 'undefined');
|
||||||
|
|
||||||
// Fetch user profile
|
// Fetch user profile
|
||||||
console.log('[NDK] Fetching user profile');
|
console.log('[NDK] Fetching user profile');
|
||||||
|
try {
|
||||||
await user.fetchProfile();
|
await user.fetchProfile();
|
||||||
|
console.log('[NDK] Profile fetched successfully');
|
||||||
|
} catch (profileError) {
|
||||||
|
console.warn('[NDK] Warning: Could not fetch user profile:', profileError);
|
||||||
|
// Continue even if profile fetch fails
|
||||||
|
}
|
||||||
|
|
||||||
// Process profile data to ensure image property is set
|
// Process profile data to ensure image property is set
|
||||||
if (user.profile) {
|
if (user.profile) {
|
||||||
|
console.log('[NDK] Profile data available');
|
||||||
if (!user.profile.image && (user.profile as any).picture) {
|
if (!user.profile.image && (user.profile as any).picture) {
|
||||||
user.profile.image = (user.profile as any).picture;
|
user.profile.image = (user.profile as any).picture;
|
||||||
|
console.log('[NDK] Set image from picture property');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[NDK] User profile loaded:', user.profile);
|
console.log('[NDK] User profile loaded:',
|
||||||
|
user.profile.name || user.profile.displayName || 'No name available');
|
||||||
|
} else {
|
||||||
|
console.log('[NDK] No profile data available');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the private key hex string securely
|
// Save the private key hex string securely
|
||||||
|
console.log('[NDK] Saving private key to secure storage');
|
||||||
await SecureStore.setItemAsync(PRIVATE_KEY_STORAGE_KEY, privateKeyHex);
|
await SecureStore.setItemAsync(PRIVATE_KEY_STORAGE_KEY, privateKeyHex);
|
||||||
|
|
||||||
// After successful login, import user relay preferences
|
// After successful login, import user relay preferences
|
||||||
try {
|
try {
|
||||||
console.log('[NDK] Login successful, importing user relay preferences');
|
console.log('[NDK] Creating RelayService to import user preferences');
|
||||||
const db = openDatabaseSync('powr.db');
|
const relayService = new RelayService();
|
||||||
const relayService = new RelayService(db);
|
|
||||||
|
|
||||||
// Set NDK on the relay service
|
// Set NDK on the relay service
|
||||||
relayService.setNDK(ndk as any);
|
console.log('[NDK] Setting NDK on RelayService');
|
||||||
|
relayService.setNDK(ndk as any); // Using type assertion
|
||||||
|
|
||||||
// Import and apply user relay preferences
|
// Import user relay preferences from metadata (kind:3 events)
|
||||||
await relayService.importUserRelaysOnLogin(user, ndk);
|
if (user.pubkey) {
|
||||||
|
console.log('[NDK] Importing relay metadata for user:', user.pubkey.substring(0, 8) + '...');
|
||||||
|
try {
|
||||||
|
await relayService.importFromUserMetadata(user.pubkey, ndk);
|
||||||
|
console.log('[NDK] Successfully imported user relay preferences');
|
||||||
|
} catch (importError) {
|
||||||
|
console.warn('[NDK] Could not import user relay preferences:', importError);
|
||||||
|
// Continue even if import fails
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('[NDK] Cannot import relay metadata: No pubkey available');
|
||||||
|
}
|
||||||
} catch (relayError) {
|
} catch (relayError) {
|
||||||
console.error('[NDK] Error importing user relay preferences:', relayError);
|
console.error('[NDK] Error with RelayService:', relayError);
|
||||||
// Continue with login even if relay import fails
|
// Continue with login even if relay import fails
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[NDK] Login successful, updating state');
|
||||||
set({
|
set({
|
||||||
currentUser: user,
|
currentUser: user,
|
||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
isLoading: false
|
isLoading: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('[NDK] Login complete');
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[NDK] Login error:', error);
|
console.error('[NDK] Login error detailed:', error);
|
||||||
set({
|
set({
|
||||||
error: error instanceof Error ? error : new Error('Failed to login'),
|
error: error instanceof Error ? error : new Error('Failed to login'),
|
||||||
isLoading: false
|
isLoading: false
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
// lib/stores/relayStore.ts
|
// lib/stores/relayStore.ts
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { openDatabaseSync } from 'expo-sqlite';
|
|
||||||
import type { RelayWithStatus } from '@/lib/db/services/RelayService';
|
import type { RelayWithStatus } from '@/lib/db/services/RelayService';
|
||||||
import { RelayService } from '@/lib/db/services/RelayService';
|
import { RelayService } from '@/lib/db/services/RelayService';
|
||||||
import { useNDKStore } from './ndk';
|
import { useNDKStore } from './ndk';
|
||||||
@ -11,8 +10,7 @@ let relayServiceInstance: RelayService | null = null;
|
|||||||
|
|
||||||
const getRelayService = (): RelayService => {
|
const getRelayService = (): RelayService => {
|
||||||
if (!relayServiceInstance) {
|
if (!relayServiceInstance) {
|
||||||
const db = openDatabaseSync('powr.db');
|
relayServiceInstance = new RelayService();
|
||||||
relayServiceInstance = new RelayService(db);
|
|
||||||
console.log('[RelayStore] Created RelayService instance');
|
console.log('[RelayStore] Created RelayService instance');
|
||||||
}
|
}
|
||||||
return relayServiceInstance;
|
return relayServiceInstance;
|
||||||
@ -60,7 +58,8 @@ export const useRelayStore = create<RelayStoreState & RelayStoreActions>((set, g
|
|||||||
const ndk = ndkState.ndk as unknown as NDKCommon;
|
const ndk = ndkState.ndk as unknown as NDKCommon;
|
||||||
|
|
||||||
if (ndk) {
|
if (ndk) {
|
||||||
relayService.setNDK(ndk);
|
// Use type assertion to ensure compatibility
|
||||||
|
relayService.setNDK(ndk as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relays = await relayService.getAllRelaysWithStatus();
|
const relays = await relayService.getAllRelaysWithStatus();
|
||||||
@ -159,7 +158,8 @@ export const useRelayStore = create<RelayStoreState & RelayStoreActions>((set, g
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply relay config changes to NDK
|
// Apply relay config changes to NDK
|
||||||
const success = await relayService.applyRelayConfig(ndk);
|
const relays = get().relays;
|
||||||
|
const success = await relayService.applyRelayConfig(relays);
|
||||||
|
|
||||||
// Wait a moment before reloading to give connections time to establish
|
// Wait a moment before reloading to give connections time to establish
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user