// app/(tabs)/library/programs.tsx import React, { useState, useEffect } from 'react'; import { View, ScrollView, TextInput, ActivityIndicator, Platform, Alert, TouchableOpacity } from 'react-native'; import { Text } from '@/components/ui/text'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import NostrLoginSheet from '@/components/sheets/NostrLoginSheet'; import { AlertCircle, CheckCircle2, Database, RefreshCcw, Trash2, Code, Search, ListFilter, Wifi, Zap, FileJson } from 'lucide-react-native'; import { useSQLiteContext } from 'expo-sqlite'; import { useNDK, useNDKAuth, useNDKCurrentUser } from '@/lib/hooks/useNDK'; import { schema } from '@/lib/db/schema'; import { FilterSheet, type FilterOptions, type SourceType } from '@/components/library/FilterSheet'; import { Separator } from '@/components/ui/separator'; import { NostrEventKind } from '@/types/nostr'; import { useNDKStore } from '@/lib/stores/ndk'; import * as SecureStore from 'expo-secure-store'; // Define relay status enum RelayStatus { DISCONNECTED = 'disconnected', CONNECTING = 'connecting', CONNECTED = 'connected', ERROR = 'error' } // Interface for event display interface DisplayEvent { id: string; pubkey: string; kind: number; created_at: number; content: string; tags: string[][]; sig?: string; } // Default available filters for programs const availableFilters = { equipment: ['Barbell', 'Dumbbell', 'Bodyweight', 'Machine', 'Cables', 'Other'], tags: ['Strength', 'Cardio', 'Mobility', 'Recovery'], source: ['local', 'powr', 'nostr'] as SourceType[] }; // Initial filter state const initialFilters: FilterOptions = { equipment: [], tags: [], source: [] }; export default function ProgramsScreen() { const db = useSQLiteContext(); // Database state const [dbStatus, setDbStatus] = useState<{ initialized: boolean; tables: string[]; error?: string; }>({ initialized: false, tables: [], }); const [schemas, setSchemas] = useState<{name: string, sql: string}[]>([]); const [testResults, setTestResults] = useState<{ success: boolean; message: string; } | null>(null); const [searchQuery, setSearchQuery] = useState(''); const [filterSheetOpen, setFilterSheetOpen] = useState(false); const [currentFilters, setCurrentFilters] = useState(initialFilters); const [activeFilters, setActiveFilters] = useState(0); // Nostr state const [relayStatus, setRelayStatus] = useState(RelayStatus.DISCONNECTED); const [statusMessage, setStatusMessage] = useState(''); const [events, setEvents] = useState([]); const [loading, setLoading] = useState(false); const [eventKind, setEventKind] = useState(NostrEventKind.TEXT); const [eventContent, setEventContent] = useState(''); const [isLoginSheetOpen, setIsLoginSheetOpen] = useState(false); // Use the NDK hooks const { ndk, isLoading: ndkLoading } = useNDK(); const { currentUser, isAuthenticated } = useNDKCurrentUser(); const { login, logout, generateKeys } = useNDKAuth(); // Tab state const [activeTab, setActiveTab] = useState('nostr'); // Default to nostr tab for testing useEffect(() => { // Check database status checkDatabase(); inspectDatabase(); // Update relay status when NDK changes if (ndk) { setRelayStatus(RelayStatus.CONNECTED); setStatusMessage(isAuthenticated ? `Connected as ${currentUser?.npub?.slice(0, 8)}...` : 'Connected to relays via NDK'); } else if (ndkLoading) { setRelayStatus(RelayStatus.CONNECTING); setStatusMessage('Connecting to relays...'); } else { setRelayStatus(RelayStatus.DISCONNECTED); setStatusMessage('Not connected'); } }, [ndk, ndkLoading, isAuthenticated, currentUser]); // DATABASE FUNCTIONS const inspectDatabase = async () => { try { const result = await db.getAllAsync<{name: string, sql: string}>( "SELECT name, sql FROM sqlite_master WHERE type='table'" ); setSchemas(result); } catch (error) { console.error('Error inspecting database:', error); } }; const checkDatabase = async () => { try { // Check schema_version table const version = await db.getFirstAsync<{version: number}>( 'SELECT version FROM schema_version ORDER BY version DESC LIMIT 1' ); // Get all tables const tables = await db.getAllAsync<{name: string}>( "SELECT name FROM sqlite_master WHERE type='table'" ); setDbStatus({ initialized: !!version, tables: tables.map(t => t.name), }); } catch (error) { console.error('Error checking database:', error); setDbStatus(prev => ({ ...prev, error: error instanceof Error ? error.message : 'Unknown error occurred', })); } }; const resetDatabase = async () => { try { setTestResults(null); // Clear stored keys first try { await SecureStore.deleteItemAsync('nostr_privkey'); console.log('[Database Reset] Cleared stored Nostr keys'); } catch (keyError) { 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' }] ); } } ] ); setTestResults({ success: true, message: 'Database tables dropped. Please restart the app to complete the reset.' }); } 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' }] ); } }; const runTestInsert = async () => { try { // Test exercise const testExercise = { title: "Test Squat", type: "strength", category: "Legs", equipment: "barbell", description: "Test exercise", tags: ["test", "legs"], format: { weight: true, reps: true }, format_units: { weight: "kg", reps: "count" } }; const timestamp = Date.now(); // Insert exercise using withTransactionAsync await db.withTransactionAsync(async () => { // Insert exercise await db.runAsync( `INSERT INTO exercises ( id, title, type, category, equipment, description, format_json, format_units_json, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ 'test-1', testExercise.title, testExercise.type, testExercise.category, testExercise.equipment || null, testExercise.description || null, JSON.stringify(testExercise.format), JSON.stringify(testExercise.format_units), timestamp, timestamp ] ); // Insert tags for (const tag of testExercise.tags) { await db.runAsync( "INSERT INTO exercise_tags (exercise_id, tag) VALUES (?, ?)", ['test-1', tag] ); } }); // Verify insert const result = await db.getFirstAsync( "SELECT * FROM exercises WHERE id = ?", ['test-1'] ); setTestResults({ success: true, message: `Successfully inserted and verified test exercise: ${JSON.stringify(result, null, 2)}` }); } catch (error) { console.error('Test insert error:', error); setTestResults({ success: false, message: error instanceof Error ? error.message : 'Unknown error occurred' }); } }; const handleApplyFilters = (filters: FilterOptions) => { setCurrentFilters(filters); const totalFilters = Object.values(filters).reduce( (acc, curr) => acc + curr.length, 0 ); setActiveFilters(totalFilters); // Implement filtering logic for programs when available }; // NOSTR FUNCTIONS // Handle login dialog const handleShowLogin = () => { setIsLoginSheetOpen(true); }; const handleCloseLogin = () => { setIsLoginSheetOpen(false); }; // Handle logout const handleLogout = async () => { try { setLoading(true); await logout(); setStatusMessage('Logged out'); setEvents([]); } catch (error) { console.error('Logout error:', error); setStatusMessage(`Logout error: ${error instanceof Error ? error.message : 'Unknown error'}`); } finally { setLoading(false); } }; // Publish an event const handlePublishEvent = async () => { if (!isAuthenticated || !ndk || !currentUser) { setStatusMessage('You must login first'); return; } setLoading(true); try { console.log('Creating event...'); // Prepare tags based on event kind const tags: string[][] = []; const timestamp = Math.floor(Date.now() / 1000); // Add appropriate tags based on event kind if (eventKind === NostrEventKind.TEXT) { // For regular text notes, we can add some simple tags tags.push( ['t', 'powr'], // Adding a hashtag ['t', 'test'], // Another hashtag ['client', 'POWR App'] // Client info ); // If no content was provided, use a default message if (!eventContent || eventContent.trim() === '') { setEventContent('Hello from POWR App - Test Note'); } } else if (eventKind === NostrEventKind.EXERCISE) { const uniqueId = `exercise-${timestamp}`; tags.push( ['d', uniqueId], ['title', eventContent || 'Test Exercise'], ['type', 'strength'], ['category', 'Legs'], ['format', 'weight', 'reps'], ['format_units', 'kg', 'count'], ['equipment', 'barbell'], ['t', 'test'], ['t', 'powr'] ); } else if (eventKind === NostrEventKind.TEMPLATE) { const uniqueId = `template-${timestamp}`; tags.push( ['d', uniqueId], ['title', eventContent || 'Test Workout Template'], ['type', 'strength'], ['t', 'strength'], ['t', 'legs'], ['t', 'powr'], // Add exercise references - these would normally reference real exercise events ['exercise', `33401:exercise-${timestamp-1}`, '3', '10', 'normal'] ); } else if (eventKind === NostrEventKind.WORKOUT) { const uniqueId = `workout-${timestamp}`; const startTime = timestamp - 3600; // 1 hour ago tags.push( ['d', uniqueId], ['title', eventContent || 'Test Workout Record'], ['start', `${startTime}`], ['end', `${timestamp}`], ['completed', 'true'], ['t', 'powr'], // Add exercise data - these would normally reference real exercise events ['exercise', `33401:exercise-${timestamp-1}`, '100', '10', '8', 'normal'] ); } // Use the NDK store's publishEvent function const content = eventContent || `Test ${eventKind === NostrEventKind.TEXT ? 'note' : 'event'} from POWR App`; const event = await useNDKStore.getState().publishEvent(eventKind, content, tags); if (event) { // Add the published event to our display list const displayEvent: DisplayEvent = { id: event.id || '', pubkey: event.pubkey, kind: event.kind || eventKind, // Add fallback to eventKind if kind is undefined created_at: event.created_at || Math.floor(Date.now() / 1000), // Add fallback timestamp content: event.content, tags: event.tags.map(tag => tag.map(item => String(item))), sig: event.sig }; setEvents(prev => [displayEvent, ...prev]); // Clear content field setEventContent(''); setStatusMessage('Event published successfully!'); } else { setStatusMessage('Failed to publish event'); } } catch (error) { console.error('Error publishing event:', error); setStatusMessage(`Error publishing: ${error instanceof Error ? error.message : 'Unknown error'}`); } finally { setLoading(false); } }; // Query events from NDK const queryEvents = async () => { if (!ndk) { setStatusMessage('NDK not initialized'); return; } setLoading(true); setEvents([]); try { // Create a filter for the specific kind const filter = { kinds: [eventKind as number], limit: 20 }; // Get events using NDK const fetchedEvents = await useNDKStore.getState().fetchEventsByFilter(filter); const displayEvents: DisplayEvent[] = []; fetchedEvents.forEach(event => { // Ensure we handle potentially undefined values displayEvents.push({ id: event.id || '', pubkey: event.pubkey, kind: event.kind || eventKind, // Use eventKind as fallback created_at: event.created_at || Math.floor(Date.now() / 1000), // Use current time as fallback content: event.content, // Convert tags to string[][] tags: event.tags.map(tag => tag.map(item => String(item))), sig: event.sig }); }); setEvents(displayEvents); setStatusMessage(`Fetched ${displayEvents.length} events`); } catch (error) { console.error('Error querying events:', error); setStatusMessage(`Error querying: ${error instanceof Error ? error.message : 'Unknown error'}`); } finally { setLoading(false); } }; return ( {/* Search bar with filter button */} {/* Filter Sheet */} setFilterSheetOpen(false)} options={currentFilters} onApplyFilters={handleApplyFilters} availableFilters={availableFilters} /> {/* Custom Tab Bar */} setActiveTab('database')} > Database setActiveTab('nostr')} > Nostr {/* Tab Content */} {activeTab === 'database' && ( Programs Coming Soon Training programs will allow you to organize your workouts into structured training plans. Database Debug Panel {/* Schema Inspector Card */} Database Schema ({Platform.OS}) {schemas.map((table) => ( {table.name} {table.sql} ))} {/* Status Card */} Database Status Initialized: {dbStatus.initialized ? '✅' : '❌'} Tables Found: {dbStatus.tables.length} {dbStatus.tables.map(table => ( • {table} ))} {dbStatus.error && ( Error {dbStatus.error} )} {/* Operations Card */} Database Operations {testResults && ( {testResults.success ? ( ) : ( )} {testResults.success ? "Success" : "Error"} {testResults.message} )} )} {activeTab === 'nostr' && ( Nostr Integration Test {/* Connection status and controls */} Nostr Connection Status: {relayStatus} {statusMessage ? ( {statusMessage} ) : null} {isAuthenticated && currentUser && ( Logged in as: {currentUser.npub} {currentUser.profile?.displayName && ( {currentUser.profile.displayName} )} {/* Display active relay */} Active Relay: wss://powr.duckdns.org Note: To publish to additional relays, update them in stores/ndk.ts )} {/* NostrLoginSheet component */} {/* Create Event */} Create Event Event Kind: Content: {/* Event List */} Recent Events {loading && events.length === 0 ? ( Loading events... ) : events.length === 0 ? ( No events yet ) : ( {events.map((event, index) => ( Kind: {event.kind} | Created: {new Date(event.created_at * 1000).toLocaleString()} ID: {event.id} Pubkey: {event.pubkey.slice(0, 8)}... {/* Display tags */} {event.tags && event.tags.length > 0 && ( Tags: {event.tags.map((tag, tagIndex) => ( {tag.join(', ')} ))} )} Content: {event.content} {/* Display signature */} {event.sig && ( Signature: {event.sig.slice(0, 16)}... )} {index < events.length - 1 && } ))} )} {/* Event JSON Viewer */} Event Details {events.length > 0 ? ( Selected Event (Latest): {JSON.stringify(events[0], null, 2)} ) : ( No events to display )} {/* Testing Guide */} Testing Guide How to test Nostr integration: 1. Click "Login with Nostr" to authenticate 2. On the login sheet, click "Generate Key" to create a new Nostr identity 3. Login with the generated keys 4. Select an event kind (Text Note, Exercise, Template, or Workout) 5. Enter optional content and click "Publish" 6. Use "Query Events" to fetch existing events of the selected kind Using NDK Mobile for Nostr integration provides a more reliable experience with proper cryptographic operations. )} ); }