improved relay system, fixed bugs

This commit is contained in:
DocNR 2025-03-11 23:59:00 -04:00
parent 4cd62cf775
commit 704fa27950
11 changed files with 798 additions and 1030 deletions

View File

@ -1,7 +1,7 @@
// app/(tabs)/index.tsx
import React, { useState, useEffect, useCallback } from 'react';
import { ScrollView, View, TouchableOpacity } from 'react-native'
import { useFocusEffect } from '@react-navigation/native';
import { ScrollView, View, TouchableOpacity, Platform } from 'react-native'
import { useFocusEffect, useTheme } from '@react-navigation/native';
import { router } from 'expo-router'
import {
AlertDialog,
@ -15,8 +15,6 @@ import {
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { TabScreen } from '@/components/layout/TabScreen'
import Header from '@/components/Header'
import HomeWorkout from '@/components/workout/HomeWorkout'
import FavoriteTemplate from '@/components/workout/FavoriteTemplate'
import { useWorkoutStore } from '@/stores/workoutStore'
import { Text } from '@/components/ui/text'
import { getRandomWorkoutTitle } from '@/utils/workoutTitles'
@ -63,6 +61,8 @@ export default function WorkoutScreen() {
endWorkout
} = useWorkoutStore();
const theme = useTheme();
useFocusEffect(
useCallback(() => {
loadFavorites();
@ -241,7 +241,10 @@ export default function WorkoutScreen() {
onPress={() => console.log('Open notifications')}
>
<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>
</Button>

View File

@ -161,74 +161,28 @@ export default function ProgramsScreen() {
console.warn('[Database Reset] Error clearing keys:', keyError);
}
// Define explicit type for tables
let tables: { name: string }[] = [];
// Try to get existing tables
try {
tables = await db.getAllAsync<{ name: string }>(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
);
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' }]
);
}
}
]
);
// Use the new complete reset method
await schema.resetDatabaseCompletely(db);
// Show success message
setTestResults({
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) {
console.error('[Database Reset] Error resetting database:', error);
setTestResults({
success: false,
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' }]
);
}
};

View File

@ -36,6 +36,32 @@ interface DatabaseProviderProps {
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) {
const [isReady, setIsReady] = React.useState(false);
const [error, setError] = React.useState<string | null>(null);
@ -63,11 +89,19 @@ export function DatabaseProvider({ children }: DatabaseProviderProps) {
React.useEffect(() => {
async function initDatabase() {
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...');
const db = openDatabaseSync('powr.db');
console.log('[DB] Creating schema...');
await schema.createTables(db);
// Explicitly check for critical tables after schema creation
await schema.ensureCriticalTablesExist(db);
// Initialize services
console.log('[DB] Initializing services...');
@ -101,7 +135,12 @@ export function DatabaseProvider({ children }: DatabaseProviderProps) {
// Seed development database
if (__DEV__) {
console.log('[DB] Seeding development database...');
await devSeeder.seedDatabase();
try {
await devSeeder.seedDatabase();
} catch (seedError) {
console.error('[DB] Error seeding database:', seedError);
// Continue even if seeding fails
}
await logDatabaseInfo();
}
@ -137,7 +176,9 @@ export function DatabaseProvider({ children }: DatabaseProviderProps) {
return (
<SQLiteProvider databaseName="powr.db">
<DatabaseServicesContext.Provider value={services}>
{children}
<DelayedInitializer>
{children}
</DelayedInitializer>
</DatabaseServicesContext.Provider>
</SQLiteProvider>
);

View File

@ -77,7 +77,7 @@ export default function Header({
onPress={() => {}}
>
<View className="relative">
<Bell size={24} className="text-foreground" />
<Bell size={24} color={theme.colors.text} />
{/* Notification indicator - you can conditionally render this */}
<View className="absolute -top-1 -right-1 w-2 h-2 bg-primary rounded-full" />
</View>

View File

@ -1,23 +1,31 @@
// components/RelayManagement.tsx
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 { Switch } from '@/components/ui/switch';
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';
// Define proper interface for component props
interface RelayManagementProps {
isVisible: boolean;
onClose: () => void;
}
// Simple RelayManagement component
export default function RelayManagement({ isVisible, onClose }: RelayManagementProps) {
const relays = useRelayStore(state => state.relays);
const isLoading = useRelayStore(state => state.isLoading);
const isSaving = useRelayStore(state => state.isSaving);
const loadRelays = useRelayStore(state => state.loadRelays);
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(() => {
if (isVisible) {
@ -25,7 +33,52 @@ export default function RelayManagement({ isVisible, onClose }: RelayManagementP
}
}, [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 => {
switch (status) {
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 }) => (
<View style={{
padding: 12,
borderBottomWidth: 1,
borderBottomColor: '#e5e7eb',
backgroundColor: 'white',
}}>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<View style={{ flexDirection: 'row', alignItems: 'center', flex: 1 }}>
<View style={{
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: getStatusColor(item.status),
marginRight: 8
}} />
<Text style={{ flex: 1 }}>{item.url}</Text>
<View className="p-4 bg-card mb-2 rounded-lg border border-border">
<View className="flex-row items-center justify-between mb-2">
<View className="flex-row items-center flex-1">
<View
style={{
width: 10,
height: 10,
borderRadius: 5,
backgroundColor: getStatusColor(item.status),
marginRight: 8
}}
/>
<Text className="text-foreground flex-1" numberOfLines={1}>{item.url}</Text>
</View>
<Text style={{ fontSize: 12, color: '#6b7280' }}>
<Text className="text-xs text-muted-foreground capitalize">
{item.status}
</Text>
</View>
<View style={{
flexDirection: 'row',
justifyContent: 'space-around',
marginTop: 8,
paddingTop: 8,
borderTopWidth: 1,
borderTopColor: '#f3f4f6'
}}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ marginRight: 8 }}>Read</Text>
<View className="flex-row justify-between items-center mt-3 pt-3 border-t border-border/30">
<View className="flex-row items-center">
<Text className="text-foreground mr-2">Read</Text>
<Switch
checked={item.read}
onCheckedChange={() => updateRelay(item.url, { read: !item.read })}
onCheckedChange={() => handleRelayUpdate(item.url, { read: !item.read })}
/>
</View>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ marginRight: 8 }}>Write</Text>
<View className="flex-row items-center">
<Text className="text-foreground mr-2">Write</Text>
<Switch
checked={item.write}
onCheckedChange={() => updateRelay(item.url, { write: !item.write })}
onCheckedChange={() => handleRelayUpdate(item.url, { write: !item.write })}
/>
</View>
</View>
</View>
);
// Main Render
return (
<Modal
visible={isVisible}
@ -95,62 +139,132 @@ export default function RelayManagement({ isVisible, onClose }: RelayManagementP
transparent={true}
onRequestClose={onClose}
>
<View style={{
flex: 1,
backgroundColor: 'rgba(0,0,0,0.5)',
justifyContent: 'flex-end',
}}>
<View style={{
backgroundColor: 'white',
borderTopLeftRadius: 20,
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>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={{ flex: 1 }}
>
<SafeAreaView style={{ flex: 1 }} className="bg-background/95">
{/* Header */}
<View className="p-4 flex-row items-center justify-between bg-card border-b border-border">
<Text className="text-xl font-semibold text-foreground">Relay Management</Text>
<TouchableOpacity onPress={onClose}>
<X size={24} />
<X size={24} className="text-foreground" />
</TouchableOpacity>
</View>
{isLoading ? (
<View style={{ padding: 20, alignItems: 'center' }}>
<ActivityIndicator size="large" />
<Text style={{ marginTop: 10 }}>Loading relays...</Text>
</View>
) : (
<>
<FlatList
data={relays}
keyExtractor={(item) => item.url}
renderItem={renderRelayItem}
style={{ maxHeight: '70%' }}
/>
<View style={{ padding: 16 }}>
<Button onPress={loadRelays} style={{ marginBottom: 8 }}>
<Text style={{ color: 'white' }}>Refresh Relays</Text>
</Button>
<Button
variant="outline"
onPress={onClose}
>
<Text>Close</Text>
</Button>
{/* Content */}
<View className="flex-1 px-4 pt-2">
{isLoading ? (
<View className="flex-1 items-center justify-center">
<ActivityIndicator size="large" />
<Text className="mt-2 text-foreground">Loading relays...</Text>
</View>
</>
)}
</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
data={relays}
renderItem={renderRelayItem}
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 }}
/>
)}
{/* Add Relay Form */}
{showAddRelay && (
<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
variant="outline"
onPress={() => setShowAddRelay(true)}
disabled={showAddRelay}
>
<PlusCircle size={18} className="mr-1" />
<Text>Add Relay</Text>
</Button>
</View>
<Button
variant={confirmReset ? "destructive" : "outline"}
onPress={handleResetToDefaults}
>
<Text className={confirmReset ? "text-destructive-foreground" : ""}>
{confirmReset ? "Confirm Reset" : "Reset to Defaults"}
</Text>
</Button>
</View>
</SafeAreaView>
</KeyboardAvoidingView>
</Modal>
);
}

View File

@ -2,7 +2,7 @@
import { SQLiteDatabase } from 'expo-sqlite';
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 {
private async getCurrentVersion(db: SQLiteDatabase): Promise<number> {
@ -44,9 +44,17 @@ class Schema {
const currentVersion = await this.getCurrentVersion(db);
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) {
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;
}
@ -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> {
try {
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 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
@ -427,25 +544,36 @@ class Schema {
}
}
async resetDatabase(db: SQLiteDatabase): Promise<void> {
async resetDatabaseCompletely(db: SQLiteDatabase): Promise<void> {
if (!__DEV__) {
console.log('[Schema] Database reset is only available in development mode');
return;
}
try {
console.log('[Schema] Resetting database...');
console.log('[Schema] Completely resetting database...');
// Clear schema_version to force recreation of all tables
await db.execAsync('DROP TABLE IF EXISTS schema_version');
console.log('[Schema] Dropped schema_version table');
// Get all tables
const tables = await db.getAllAsync<{ name: string }>(
"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);
console.log('[Schema] Database reset complete');
console.log('[Schema] Database completely reset');
} catch (error) {
console.error('[Schema] Error resetting database:', error);
console.error('[Schema] Error completely resetting database:', error);
throw error;
}
}

View File

@ -8,6 +8,7 @@ import { logDatabaseInfo } from '../debug';
import { mockExerciseEvents, convertNostrToExercise } from '../../mocks/exercises';
import { DbService } from '../db-service';
import NDK, { NDKEvent } from '@nostr-dev-kit/ndk-mobile';
import { NostrEvent } from '@/types/nostr'; // Assuming you have this type defined
export class DevSeederService {
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() {
if (!__DEV__) return;

File diff suppressed because it is too large Load Diff

View File

@ -2,58 +2,39 @@
import 'react-native-get-random-values'; // This must be the first import
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';
/**
* Initialize NDK with relays from database or defaults
* Initialize NDK with relays
*/
export async function initializeNDK() {
console.log('[NDK] Initializing NDK with mobile adapter...');
// Create a mobile-specific cache adapter
const cacheAdapter = new NDKCacheAdapterSqlite('powr', 1000);
await cacheAdapter.initialize();
// Initialize database and relay service
const db = openDatabaseSync('powr.db');
const relayService = new RelayService(db);
// Initialize relay service
const relayService = new RelayService();
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
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`);
// Initialize NDK with default relays first
console.log(`[NDK] Creating NDK instance with default relays`);
let ndk = new NDK({
cacheAdapter,
explicitRelayUrls: relays,
explicitRelayUrls: DEFAULT_RELAYS,
enableOutboxModel: true,
autoConnectUserRelays: true,
clientName: 'powr',
@ -62,41 +43,8 @@ export async function initializeNDK() {
// 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'> = {};
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';
});
}
});
// Set the NDK instance in the RelayService
relayService.setNDK(ndk);
try {
console.log('[NDK] Connecting to relays...');
@ -105,27 +53,24 @@ export async function initializeNDK() {
// Wait a moment for connections to establish
await new Promise(resolve => setTimeout(resolve, 2000));
// Get updated relay statuses
const relaysWithStatus = await relayService.getAllRelaysWithStatus();
// Count connected relays
const connectedRelays = Object.entries(relayStatus)
.filter(([_, status]) => status === 'connected')
.map(([url]) => url);
const connectedRelays = relaysWithStatus
.filter(relay => relay.status === 'connected')
.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:');
relays.forEach(url => {
const relay = ndk.pool.getRelay(url);
console.log(` - ${url}: ${relay ?
(relay.status === 1 ? 'connected' :
relay.status === 0 ? 'connecting' :
relay.status === 3 ? 'disconnected' :
`status=${relay.status}`) : 'not found'}`);
relaysWithStatus.forEach(relay => {
console.log(` - ${relay.url}: ${relay.status}`);
});
return {
ndk,
relayStatus,
relayService,
connectedRelayCount: connectedRelays.length,
connectedRelays
@ -135,7 +80,6 @@ export async function initializeNDK() {
// Still return the NDK instance so the app can work offline
return {
ndk,
relayStatus,
relayService,
connectedRelayCount: 0,
connectedRelays: []

View File

@ -6,11 +6,10 @@ import NDK, {
NDKUser,
NDKRelay,
NDKPrivateKeySigner
} from '@nostr-dev-kit/ndk';
} from '@nostr-dev-kit/ndk-mobile';
import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools';
import * as SecureStore from 'expo-secure-store';
import { RelayService } from '@/lib/db/services/RelayService';
import { openDatabaseSync } from 'expo-sqlite';
// Constants for SecureStore
const PRIVATE_KEY_STORAGE_KEY = 'nostr_privkey';
@ -133,85 +132,129 @@ export const useNDKStore = create<NDKStoreState & NDKStoreActions>((set, get) =>
login: async (privateKeyInput?: string) => {
set({ isLoading: true, error: null });
console.log('[NDK] Login attempt starting');
try {
const { ndk } = get();
if (!ndk) {
console.log('[NDK] 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
let privateKeyHex = privateKeyInput;
if (!privateKeyHex) {
console.log('[NDK] No key provided, generating new key');
const { privateKey } = get().generateKeys();
privateKeyHex = privateKey;
} else {
console.log('[NDK] Using provided key, format:',
privateKeyHex.startsWith('nsec') ? 'nsec' : 'hex',
'length:', privateKeyHex.length);
}
// Handle nsec format
if (privateKeyHex.startsWith('nsec')) {
try {
console.log('[NDK] Decoding nsec format key');
const decoded = nip19.decode(privateKeyHex);
console.log('[NDK] Decoded type:', decoded.type);
if (decoded.type === 'nsec') {
// Get the data as hex
privateKeyHex = bytesToHex(decoded.data as any);
console.log('[NDK] Converted to hex, new length:', privateKeyHex.length);
}
} catch (error) {
console.error('Error decoding nsec:', error);
console.error('[NDK] Error decoding nsec:', error);
throw new Error('Invalid nsec format');
}
}
console.log('[NDK] Creating signer with key length:', privateKeyHex.length);
// Create signer with private key
const signer = new NDKPrivateKeySigner(privateKeyHex);
console.log('[NDK] Signer created, setting on NDK');
ndk.signer = signer;
// Get user
console.log('[NDK] Getting user from signer');
const user = await ndk.signer.user();
if (!user) {
console.log('[NDK] 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
console.log('[NDK] Fetching user profile');
await user.fetchProfile();
try {
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
if (user.profile) {
console.log('[NDK] Profile data available');
if (!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
console.log('[NDK] Saving private key to secure storage');
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);
console.log('[NDK] Creating RelayService to import user preferences');
const relayService = new RelayService();
// 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
await relayService.importUserRelaysOnLogin(user, ndk);
// Import user relay preferences from metadata (kind:3 events)
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) {
console.error('[NDK] Error importing user relay preferences:', relayError);
console.error('[NDK] Error with RelayService:', relayError);
// Continue with login even if relay import fails
}
console.log('[NDK] Login successful, updating state');
set({
currentUser: user,
isAuthenticated: true,
isLoading: false
});
console.log('[NDK] Login complete');
return true;
} catch (error) {
console.error('[NDK] Login error:', error);
console.error('[NDK] Login error detailed:', error);
set({
error: error instanceof Error ? error : new Error('Failed to login'),
isLoading: false

View File

@ -1,6 +1,5 @@
// 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';
@ -11,8 +10,7 @@ let relayServiceInstance: RelayService | null = null;
const getRelayService = (): RelayService => {
if (!relayServiceInstance) {
const db = openDatabaseSync('powr.db');
relayServiceInstance = new RelayService(db);
relayServiceInstance = new RelayService();
console.log('[RelayStore] Created RelayService instance');
}
return relayServiceInstance;
@ -60,7 +58,8 @@ export const useRelayStore = create<RelayStoreState & RelayStoreActions>((set, g
const ndk = ndkState.ndk as unknown as NDKCommon;
if (ndk) {
relayService.setNDK(ndk);
// Use type assertion to ensure compatibility
relayService.setNDK(ndk as any);
}
const relays = await relayService.getAllRelaysWithStatus();
@ -159,7 +158,8 @@ export const useRelayStore = create<RelayStoreState & RelayStoreActions>((set, g
}
// 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
await new Promise(resolve => setTimeout(resolve, 2000));