// app/(tabs)/library/programs.tsx import React, { useState, useEffect, useRef } from 'react'; import { View, ScrollView, TextInput, ActivityIndicator, Platform, 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 { AlertCircle, CheckCircle2, Database, RefreshCcw, Trash2, Code, Search, ListFilter, Wifi, Zap, FileJson } from 'lucide-react-native'; import { useSQLiteContext } from 'expo-sqlite'; import { ExerciseType, ExerciseCategory, Equipment } from '@/types/exercise'; import { SQLTransaction, SQLResultSet, SQLError } from '@/lib/db/types'; import { schema } from '@/lib/db/schema'; import { FilterSheet, type FilterOptions, type SourceType } from '@/components/library/FilterSheet'; import { Switch } from '@/components/ui/switch'; import { Label } from '@/components/ui/label'; import { Separator } from '@/components/ui/separator'; import { getPublicKey } from 'nostr-tools'; // Constants for Nostr const EVENT_KIND_EXERCISE = 33401; const EVENT_KIND_WORKOUT_TEMPLATE = 33402; const EVENT_KIND_WORKOUT_RECORD = 1301; // Simplified mock implementations for testing const generatePrivateKey = (): string => { // Generate a random hex string (32 bytes) return Array.from({ length: 64 }, () => Math.floor(Math.random() * 16).toString(16) ).join(''); }; const getEventHash = (event: any): string => { // For testing, just create a mock hash const eventData = JSON.stringify([ 0, event.pubkey, event.created_at, event.kind, event.tags, event.content ]); // Simple hash function for demonstration return Array.from(eventData) .reduce((hash, char) => { return ((hash << 5) - hash) + char.charCodeAt(0); }, 0) .toString(16) .padStart(64, '0'); }; const signEvent = (event: any, privateKey: string): string => { // In real implementation, this would sign the event hash with the private key // For testing, we'll just return a mock signature return Array.from({ length: 128 }, () => Math.floor(Math.random() * 16).toString(16) ).join(''); }; interface NostrEvent { id?: string; pubkey: string; created_at: number; kind: number; tags: string[][]; content: string; sig?: string; } interface TableInfo { name: string; } interface TableSchema { name: string; sql: string; } interface SchemaVersion { version: number; } interface ExerciseRow { id: string; title: string; type: string; category: string; equipment: string | null; description: string | null; created_at: number; updated_at: number; format_json: string; format_units_json: string; } // Default available filters for programs - can be adjusted later 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([]); 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 [relayUrl, setRelayUrl] = useState('ws://localhost:7777'); const [connected, setConnected] = useState(false); const [connecting, setConnecting] = useState(false); const [statusMessage, setStatusMessage] = useState(''); const [events, setEvents] = useState([]); const [loading, setLoading] = useState(false); const [privateKey, setPrivateKey] = useState(''); const [publicKey, setPublicKey] = useState(''); const [useGeneratedKeys, setUseGeneratedKeys] = useState(true); const [eventKind, setEventKind] = useState(EVENT_KIND_EXERCISE); const [eventContent, setEventContent] = useState(''); // WebSocket reference const socketRef = useRef(null); // Tab state const [activeTab, setActiveTab] = useState('database'); useEffect(() => { checkDatabase(); inspectDatabase(); generateKeys(); }, []); // DATABASE FUNCTIONS const inspectDatabase = async () => { try { const result = await db.getAllAsync( "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( 'SELECT version FROM schema_version ORDER BY version DESC LIMIT 1' ); // Get all tables const tables = await db.getAllAsync( "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 { await db.withTransactionAsync(async () => { // Drop all tables const tables = await db.getAllAsync<{ name: string }>( "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'" ); for (const { name } of tables) { await db.execAsync(`DROP TABLE IF EXISTS ${name}`); } // Recreate schema await schema.createTables(db); }); setTestResults({ success: true, message: 'Database reset successfully' }); // Refresh database status checkDatabase(); inspectDatabase(); } catch (error) { console.error('Error resetting database:', error); setTestResults({ success: false, message: error instanceof Error ? error.message : 'Unknown error during reset' }); } }; const runTestInsert = async () => { try { // Test exercise const testExercise = { title: "Test Squat", type: "strength" as ExerciseType, category: "Legs" as ExerciseCategory, equipment: "barbell" as Equipment, description: "Test exercise", tags: ["test", "legs"], format: { weight: true, reps: true }, format_units: { weight: "kg" as const, reps: "count" as const } }; 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 // Generate new keypair const generateKeys = () => { try { const privKey = generatePrivateKey(); // For getPublicKey, we can use a mock function that returns a valid-looking pubkey const pubKey = privKey.slice(0, 64); // Just use part of the private key for demo setPrivateKey(privKey); setPublicKey(pubKey); setStatusMessage('Keys generated successfully'); } catch (error) { setStatusMessage(`Error generating keys: ${error}`); } }; // Connect to relay const connectToRelay = () => { if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) { socketRef.current.close(); } setConnecting(true); setStatusMessage('Connecting to relay...'); try { const socket = new WebSocket(relayUrl); socket.onopen = () => { setConnected(true); setConnecting(false); setStatusMessage('Connected to relay!'); socketRef.current = socket; // Subscribe to exercise-related events const subscriptionId = 'test-sub-' + Math.random().toString(36).substring(2, 15); const subscription = JSON.stringify([ 'REQ', subscriptionId, { kinds: [EVENT_KIND_EXERCISE, EVENT_KIND_WORKOUT_TEMPLATE, EVENT_KIND_WORKOUT_RECORD], limit: 10 } ]); socket.send(subscription); }; socket.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data[0] === 'EVENT' && data[1] && data[2]) { const nostrEvent = data[2]; setEvents(prev => [nostrEvent, ...prev].slice(0, 50)); // Keep most recent 50 events } else if (data[0] === 'NOTICE') { setStatusMessage(`Relay message: ${data[1]}`); } } catch (error) { console.error('Error parsing message:', error, event.data); } }; socket.onclose = () => { setConnected(false); setConnecting(false); setStatusMessage('Disconnected from relay'); }; socket.onerror = (error) => { setConnected(false); setConnecting(false); setStatusMessage(`Connection error: ${error}`); console.error('WebSocket error:', error); }; } catch (error) { setConnecting(false); setStatusMessage(`Failed to connect: ${error}`); console.error('Connection setup error:', error); } }; // Disconnect from relay const disconnectFromRelay = () => { if (socketRef.current) { socketRef.current.close(); } }; // Create and publish a new event const publishEvent = () => { if (!socketRef.current || socketRef.current.readyState !== WebSocket.OPEN) { setStatusMessage('Not connected to a relay'); return; } if (!privateKey || !publicKey) { setStatusMessage('Need private and public keys to publish'); return; } try { setLoading(true); // Create event with required pubkey (no longer optional) const event: NostrEvent = { pubkey: publicKey, created_at: Math.floor(Date.now() / 1000), kind: eventKind, tags: [], content: eventContent, }; // A basic implementation for each event kind if (eventKind === EVENT_KIND_EXERCISE) { event.tags.push(['d', `exercise-${Date.now()}`]); event.tags.push(['title', 'Test Exercise']); event.tags.push(['format', 'weight', 'reps']); event.tags.push(['format_units', 'kg', 'count']); event.tags.push(['equipment', 'barbell']); } else if (eventKind === EVENT_KIND_WORKOUT_TEMPLATE) { event.tags.push(['d', `template-${Date.now()}`]); event.tags.push(['title', 'Test Workout Template']); event.tags.push(['type', 'strength']); } else if (eventKind === EVENT_KIND_WORKOUT_RECORD) { event.tags.push(['d', `workout-${Date.now()}`]); event.tags.push(['title', 'Test Workout Record']); event.tags.push(['start', `${Math.floor(Date.now() / 1000) - 3600}`]); event.tags.push(['end', `${Math.floor(Date.now() / 1000)}`]); } // Hash and sign event.id = getEventHash(event); event.sig = signEvent(event, privateKey); // Publish to relay const message = JSON.stringify(['EVENT', event]); socketRef.current.send(message); setStatusMessage('Event published successfully!'); setEventContent(''); setLoading(false); } catch (error) { setStatusMessage(`Error publishing event: ${error}`); setLoading(false); } }; // Query events from relay const queryEvents = () => { if (!socketRef.current || socketRef.current.readyState !== WebSocket.OPEN) { setStatusMessage('Not connected to a relay'); return; } try { setEvents([]); setLoading(true); // Create a new subscription for the selected event kind const subscriptionId = 'query-' + Math.random().toString(36).substring(2, 15); const subscription = JSON.stringify([ 'REQ', subscriptionId, { kinds: [eventKind], limit: 20 } ]); socketRef.current.send(subscription); setStatusMessage(`Querying events of kind ${eventKind}...`); // Close this subscription after 5 seconds setTimeout(() => { if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) { socketRef.current.send(JSON.stringify(['CLOSE', subscriptionId])); setLoading(false); setStatusMessage(`Completed query for kind ${eventKind}`); } }, 5000); } catch (error) { setLoading(false); setStatusMessage(`Error querying events: ${error}`); } }; 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 controls */} Relay Connection Status: {connected ? 'Connected' : 'Disconnected'} {statusMessage ? ( {statusMessage} ) : null} {/* Keys */} Nostr Keys Public Key: Private Key: Note: Never share your private key in a production app {/* 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} {/* Display tags */} {event.tags && event.tags.length > 0 && ( Tags: {event.tags.map((tag, tagIndex) => ( {tag.join(', ')} ))} )} Content: {event.content} {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 )} {/* How To Use Guide */} Testing Guide How to test Nostr integration: 1. Start local strfry relay using: ./strfry relay 2. Connect to the relay (ws://localhost:7777) 3. Generate or enter Nostr keys 4. Create and publish test events 5. Query for existing events For details, see the Nostr Integration Testing Guide )} ); }