From f1411e85681ed5f28302ee9f5a3d2e4781872d83 Mon Sep 17 00:00:00 2001 From: DocNR Date: Sun, 16 Mar 2025 21:31:38 -0400 Subject: [PATCH] powr pack bug fixes --- app/(packs)/import.tsx | 4 +- components/SettingsDrawer.tsx | 24 -- components/social/POWRPackSection.tsx | 124 ++++++-- lib/db/schema.ts | 416 ++++++++++++++++---------- lib/db/services/FavoritesService.ts | 42 +++ lib/db/services/NostrIntegration.ts | 236 ++++++++++----- lib/db/services/POWRPackService.ts | 228 +++++++++++++- lib/hooks/useSubscribe.ts | 79 ++++- types/ndk-common.ts | 18 +- types/ndk-extensions.ts | 6 +- 10 files changed, 837 insertions(+), 340 deletions(-) diff --git a/app/(packs)/import.tsx b/app/(packs)/import.tsx index 202ff7a..08cb1ca 100644 --- a/app/(packs)/import.tsx +++ b/app/(packs)/import.tsx @@ -169,13 +169,13 @@ export default function ImportPOWRPackScreen() { // Get pack title from event const getPackTitle = (): string => { if (!packData?.packEvent) return 'Unknown Pack'; - return findTagValue(packData.packEvent.tags, 'title') || 'Unnamed Pack'; + return findTagValue(packData.packEvent.tags, 'name') || 'Unnamed Pack'; }; // Get pack description from event const getPackDescription = (): string => { if (!packData?.packEvent) return ''; - return findTagValue(packData.packEvent.tags, 'description') || packData.packEvent.content || ''; + return findTagValue(packData.packEvent.tags, 'about') || packData.packEvent.content || ''; }; return ( diff --git a/components/SettingsDrawer.tsx b/components/SettingsDrawer.tsx index c0a8e35..dbf86da 100644 --- a/components/SettingsDrawer.tsx +++ b/components/SettingsDrawer.tsx @@ -158,24 +158,6 @@ export default function SettingsDrawer() { /> ), }, - { - id: 'notifications', - icon: Bell, - label: 'Notifications', - onPress: () => closeDrawer(), - }, - { - id: 'data-sync', - icon: RefreshCw, - label: 'Data Sync', - onPress: () => closeDrawer(), - }, - { - id: 'backup-restore', - icon: Database, - label: 'Backup & Restore', - onPress: () => closeDrawer(), - }, { id: 'relays', icon: Globe, @@ -191,12 +173,6 @@ export default function SettingsDrawer() { router.push("/(packs)/manage"); }, }, - { - id: 'device', - icon: Smartphone, - label: 'Device Settings', - onPress: () => closeDrawer(), - }, { id: 'nostr', icon: Zap, diff --git a/components/social/POWRPackSection.tsx b/components/social/POWRPackSection.tsx index 43e5ce0..7208702 100644 --- a/components/social/POWRPackSection.tsx +++ b/components/social/POWRPackSection.tsx @@ -1,37 +1,59 @@ // components/social/POWRPackSection.tsx -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { View, ScrollView, StyleSheet, TouchableOpacity, Image } from 'react-native'; import { router } from 'expo-router'; import { useNDK } from '@/lib/hooks/useNDK'; -import { useSubscribe } from '@/lib/hooks/useSubscribe'; import { findTagValue } from '@/utils/nostr-utils'; import { Text } from '@/components/ui/text'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; -import { PackageOpen, ArrowRight } from 'lucide-react-native'; +import { PackageOpen, ArrowRight, RefreshCw } from 'lucide-react-native'; import { NDKEvent } from '@nostr-dev-kit/ndk-mobile'; -import { usePOWRPackService } from '@/components/DatabaseProvider'; import { Clipboard } from 'react-native'; import { nip19 } from 'nostr-tools'; export default function POWRPackSection() { const { ndk } = useNDK(); - const powrPackService = usePOWRPackService(); + const [isLoading, setIsLoading] = useState(false); const [featuredPacks, setFeaturedPacks] = useState([]); + const [error, setError] = useState(null); - // Subscribe to POWR packs (kind 30004 with powrpack hashtag) - const { events, isLoading } = useSubscribe( - ndk ? [{ kinds: [30004], '#t': ['powrpack', 'fitness', 'workout'], limit: 10 }] : false, - { enabled: !!ndk } - ); - - // Update featured packs when events change - useEffect(() => { - if (events.length > 0) { - setFeaturedPacks(events); + // Manual fetch function + const handleFetchPacks = async () => { + if (!ndk) return; + + try { + setIsLoading(true); + setError(null); + + console.log('Manually fetching POWR packs'); + const events = await ndk.fetchEvents({ + kinds: [30004], + "#t": ["powrpack"], + limit: 20 + }); + const eventsArray = Array.from(events); + + console.log(`Fetched ${eventsArray.length} events`); + + // Filter to find POWR packs + const powrPacks = eventsArray.filter(event => { + // Check if any tag has 'powrpack', 'fitness', or 'workout' + return event.tags.some(tag => + tag[0] === 't' && ['powrpack', 'fitness', 'workout'].includes(tag[1]) + ); + }); + + console.log(`Found ${powrPacks.length} POWR packs`); + setFeaturedPacks(powrPacks); + } catch (err) { + console.error('Error fetching packs:', err); + setError(err instanceof Error ? err : new Error('Failed to fetch packs')); + } finally { + setIsLoading(false); } - }, [events]); + }; // Handle pack click const handlePackClick = (packEvent: NDKEvent) => { @@ -42,12 +64,23 @@ export default function POWRPackSection() { throw new Error('Pack is missing identifier (d tag)'); } + // Get relay hints from event tags + const relayHints = packEvent.tags + .filter(tag => tag[0] === 'r') + .map(tag => tag[1]) + .filter(relay => relay.startsWith('wss://')); + + // Default relays if none found + const relays = relayHints.length > 0 + ? relayHints + : ['wss://relay.damus.io', 'wss://nos.lol', 'wss://relay.nostr.band']; + // Create shareable naddr const naddr = nip19.naddrEncode({ kind: 30004, pubkey: packEvent.pubkey, identifier: dTag, - relays: ['wss://relay.damus.io', 'wss://nos.lol', 'wss://relay.nostr.band'] + relays }); // Copy to clipboard @@ -69,21 +102,30 @@ export default function POWRPackSection() { router.push('/(packs)/manage'); }; - // Only show section if we have packs or are loading - const showSection = featuredPacks.length > 0 || isLoading; - - if (!showSection) { - return null; - } + // Fetch packs when mounted + React.useEffect(() => { + if (ndk) { + handleFetchPacks(); + } + }, [ndk]); return ( POWR Packs - - View All - - + + + + + + View All + + + ); }) + ) : error ? ( + // Error state + + Error loading packs + + ) : ( // No packs found @@ -176,6 +231,14 @@ const styles = StyleSheet.create({ fontSize: 18, fontWeight: '600', }, + headerButtons: { + flexDirection: 'row', + alignItems: 'center', + }, + refreshButton: { + padding: 8, + marginRight: 8, + }, viewAll: { flexDirection: 'row', alignItems: 'center', @@ -233,7 +296,7 @@ const styles = StyleSheet.create({ borderRadius: 4, }, emptyState: { - width: '100%', + width: 280, padding: 24, alignItems: 'center', justifyContent: 'center', @@ -243,6 +306,11 @@ const styles = StyleSheet.create({ marginBottom: 16, color: '#6b7280', }, + errorText: { + marginTop: 8, + marginBottom: 16, + color: '#ef4444', + }, emptyButton: { marginTop: 8, } diff --git a/lib/db/schema.ts b/lib/db/schema.ts index d4ff1f8..caeb31c 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -2,7 +2,7 @@ import { SQLiteDatabase } from 'expo-sqlite'; import { Platform } from 'react-native'; -export const SCHEMA_VERSION = 9; +export const SCHEMA_VERSION = 10; class Schema { private async getCurrentVersion(db: SQLiteDatabase): Promise { @@ -29,6 +29,37 @@ class Schema { } } + // Version 8 migration - add template archive and author pubkey + async migrate_v8(db: SQLiteDatabase): Promise { + try { + console.log('[Schema] Running migration v8 - Template management'); + + // Check if is_archived column already exists in templates table + const columnsResult = await db.getAllAsync<{ name: string }>( + "PRAGMA table_info(templates)" + ); + + const columnNames = columnsResult.map(col => col.name); + + // Add is_archived if it doesn't exist + if (!columnNames.includes('is_archived')) { + console.log('[Schema] Adding is_archived column to templates table'); + await db.execAsync('ALTER TABLE templates ADD COLUMN is_archived BOOLEAN NOT NULL DEFAULT 0'); + } + + // Add author_pubkey if it doesn't exist + if (!columnNames.includes('author_pubkey')) { + console.log('[Schema] Adding author_pubkey column to templates table'); + await db.execAsync('ALTER TABLE templates ADD COLUMN author_pubkey TEXT'); + } + + console.log('[Schema] Migration v8 completed successfully'); + } catch (error) { + console.error('[Schema] Error in migration v8:', error); + throw error; + } + } + async migrate_v9(db: SQLiteDatabase): Promise { try { console.log('[Schema] Running migration v9 - Enhanced Nostr metadata'); @@ -72,6 +103,31 @@ class Schema { } } + async migrate_v10(db: SQLiteDatabase): Promise { + try { + console.log('[Schema] Running migration v10 - Adding Favorites table'); + + // Create favorites table if it doesn't exist + await db.execAsync(` + CREATE TABLE IF NOT EXISTS favorites ( + id TEXT PRIMARY KEY, + content_type TEXT NOT NULL, + content_id TEXT NOT NULL, + content TEXT NOT NULL, + pubkey TEXT, + created_at INTEGER NOT NULL, + UNIQUE(content_type, content_id) + ); + CREATE INDEX IF NOT EXISTS idx_favorites_content ON favorites(content_type, content_id); + `); + + console.log('[Schema] Migration v10 completed successfully'); + } catch (error) { + console.error('[Schema] Error in migration v10:', error); + throw error; + } + } + async createTables(db: SQLiteDatabase): Promise { try { console.log(`[Schema] Initializing database on ${Platform.OS}`); @@ -118,6 +174,16 @@ class Schema { await this.migrate_v8(db); } + if (currentVersion < 9) { + console.log(`[Schema] Running migration from version ${currentVersion} to 9`); + await this.migrate_v9(db); + } + + if (currentVersion < 10) { + console.log(`[Schema] Running migration from version ${currentVersion} to 10`); + await this.migrate_v10(db); + } + // Update schema version at the end of the transaction await this.updateSchemaVersion(db); }); @@ -135,12 +201,22 @@ class Schema { // Create all tables in their latest form await this.createAllTables(db); - // Run migrations if needed + // Run migrations if needed (same as in transaction) if (currentVersion < 8) { console.log(`[Schema] Running migration from version ${currentVersion} to 8`); await this.migrate_v8(db); } + if (currentVersion < 9) { + console.log(`[Schema] Running migration from version ${currentVersion} to 9`); + await this.migrate_v9(db); + } + + if (currentVersion < 10) { + console.log(`[Schema] Running migration from version ${currentVersion} to 10`); + await this.migrate_v10(db); + } + // Update schema version await this.updateSchemaVersion(db); @@ -151,38 +227,161 @@ class Schema { throw error; } } - // Version 8 migration - add template archive and author pubkey - async migrate_v8(db: SQLiteDatabase): Promise { + + private async createAllTables(db: SQLiteDatabase): Promise { try { - console.log('[Schema] Running migration v8 - Template management'); + console.log('[Schema] Creating all database tables...'); - // Check if is_archived column already exists in templates table - const columnsResult = await db.getAllAsync<{ name: string }>( - "PRAGMA table_info(templates)" - ); + // Create exercises table + console.log('[Schema] Creating exercises table...'); + await db.execAsync(` + CREATE TABLE exercises ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + type TEXT NOT NULL CHECK(type IN ('strength', 'cardio', 'bodyweight')), + category TEXT NOT NULL, + equipment TEXT, + description TEXT, + format_json TEXT, + format_units_json TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + source TEXT NOT NULL DEFAULT 'local', + nostr_event_id TEXT + ); + `); + + // Create exercise_tags table + console.log('[Schema] Creating exercise_tags table...'); + await db.execAsync(` + CREATE TABLE exercise_tags ( + exercise_id TEXT NOT NULL, + tag TEXT NOT NULL, + FOREIGN KEY(exercise_id) REFERENCES exercises(id) ON DELETE CASCADE, + UNIQUE(exercise_id, tag) + ); + CREATE INDEX idx_exercise_tags ON exercise_tags(tag); + `); - const columnNames = columnsResult.map(col => col.name); + // Create nostr_events table + console.log('[Schema] Creating nostr_events table...'); + await db.execAsync(` + CREATE TABLE nostr_events ( + id TEXT PRIMARY KEY, + pubkey TEXT NOT NULL, + kind INTEGER NOT NULL, + created_at INTEGER NOT NULL, + content TEXT NOT NULL, + sig TEXT, + raw_event TEXT NOT NULL, + received_at INTEGER NOT NULL + ); + `); + + // Create event_tags table + console.log('[Schema] Creating event_tags table...'); + await db.execAsync(` + CREATE TABLE event_tags ( + event_id TEXT NOT NULL, + name TEXT NOT NULL, + value TEXT NOT NULL, + index_num INTEGER NOT NULL, + FOREIGN KEY(event_id) REFERENCES nostr_events(id) ON DELETE CASCADE + ); + CREATE INDEX idx_event_tags ON event_tags(name, value); + `); - // Add is_archived if it doesn't exist - if (!columnNames.includes('is_archived')) { - console.log('[Schema] Adding is_archived column to templates table'); - await db.execAsync('ALTER TABLE templates ADD COLUMN is_archived BOOLEAN NOT NULL DEFAULT 0'); - } + // Create templates table with new columns + console.log('[Schema] Creating templates table...'); + await db.execAsync(` + CREATE TABLE 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, + is_archived BOOLEAN NOT NULL DEFAULT 0, + author_pubkey TEXT + ); + CREATE INDEX idx_templates_updated_at ON templates(updated_at); + `); + + // Create template_exercises table + console.log('[Schema] Creating template_exercises table...'); + await db.execAsync(` + CREATE TABLE 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 idx_template_exercises_template_id ON template_exercises(template_id); + `); + + // Create powr_packs table + console.log('[Schema] Creating powr_packs table...'); + await db.execAsync(` + CREATE TABLE powr_packs ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + description TEXT, + author_pubkey TEXT, + nostr_event_id TEXT, + import_date INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ); + CREATE INDEX idx_powr_packs_import_date ON powr_packs(import_date DESC); + `); + + // Create powr_pack_items table + console.log('[Schema] Creating powr_pack_items table...'); + await db.execAsync(` + CREATE TABLE powr_pack_items ( + pack_id TEXT NOT NULL, + item_id TEXT NOT NULL, + item_type TEXT NOT NULL CHECK(item_type IN ('exercise', 'template')), + item_order INTEGER, + is_imported BOOLEAN NOT NULL DEFAULT 0, + nostr_event_id TEXT, + PRIMARY KEY (pack_id, item_id), + FOREIGN KEY (pack_id) REFERENCES powr_packs(id) ON DELETE CASCADE + ); + CREATE INDEX idx_powr_pack_items_type ON powr_pack_items(item_type); + `); - // Add author_pubkey if it doesn't exist - if (!columnNames.includes('author_pubkey')) { - console.log('[Schema] Adding author_pubkey column to templates table'); - await db.execAsync('ALTER TABLE templates ADD COLUMN author_pubkey TEXT'); - } + // Create favorites table - moved inside the try block + console.log('[Schema] Creating favorites table...'); + await db.execAsync(` + CREATE TABLE IF NOT EXISTS favorites ( + id TEXT PRIMARY KEY, + content_type TEXT NOT NULL, + content_id TEXT NOT NULL, + content TEXT NOT NULL, + pubkey TEXT, + created_at INTEGER NOT NULL, + UNIQUE(content_type, content_id) + ); + CREATE INDEX IF NOT EXISTS idx_favorites_content ON favorites(content_type, content_id); + `); - console.log('[Schema] Migration v8 completed successfully'); + console.log('[Schema] All tables created successfully'); } catch (error) { - console.error('[Schema] Error in migration v8:', error); + console.error('[Schema] Error in createAllTables:', error); throw error; } } - - // Add this method to check for and create critical tables + async ensureCriticalTablesExist(db: SQLiteDatabase): Promise { try { console.log('[Schema] Checking for missing critical tables...'); @@ -253,6 +452,7 @@ class Schema { 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 @@ -301,6 +501,30 @@ class Schema { // If templates table exists, ensure new columns are added await this.migrate_v8(db); } + + // Check if favorites table exists + const favoritesTableExists = await db.getFirstAsync<{ count: number }>( + `SELECT count(*) as count FROM sqlite_master + WHERE type='table' AND name='favorites'` + ); + + if (!favoritesTableExists || favoritesTableExists.count === 0) { + console.log('[Schema] Creating missing favorites table...'); + + // Create favorites table + await db.execAsync(` + CREATE TABLE IF NOT EXISTS favorites ( + id TEXT PRIMARY KEY, + content_type TEXT NOT NULL, + content_id TEXT NOT NULL, + content TEXT NOT NULL, + pubkey TEXT, + created_at INTEGER NOT NULL, + UNIQUE(content_type, content_id) + ); + CREATE INDEX IF NOT EXISTS idx_favorites_content ON favorites(content_type, content_id); + `); + } console.log('[Schema] Critical tables check complete'); } catch (error) { @@ -308,7 +532,7 @@ class Schema { throw error; } } - + private async dropAllTables(db: SQLiteDatabase): Promise { try { console.log('[Schema] Getting list of tables to drop...'); @@ -335,145 +559,6 @@ class Schema { throw error; } } - private async createAllTables(db: SQLiteDatabase): Promise { - try { - console.log('[Schema] Creating all database tables...'); - - // Create exercises table - console.log('[Schema] Creating exercises table...'); - await db.execAsync(` - CREATE TABLE exercises ( - id TEXT PRIMARY KEY, - title TEXT NOT NULL, - type TEXT NOT NULL CHECK(type IN ('strength', 'cardio', 'bodyweight')), - category TEXT NOT NULL, - equipment TEXT, - description TEXT, - format_json TEXT, - format_units_json TEXT, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL, - source TEXT NOT NULL DEFAULT 'local', - nostr_event_id TEXT - ); - `); - - // Create exercise_tags table - console.log('[Schema] Creating exercise_tags table...'); - await db.execAsync(` - CREATE TABLE exercise_tags ( - exercise_id TEXT NOT NULL, - tag TEXT NOT NULL, - FOREIGN KEY(exercise_id) REFERENCES exercises(id) ON DELETE CASCADE, - UNIQUE(exercise_id, tag) - ); - CREATE INDEX idx_exercise_tags ON exercise_tags(tag); - `); - - // Create nostr_events table - console.log('[Schema] Creating nostr_events table...'); - await db.execAsync(` - CREATE TABLE nostr_events ( - id TEXT PRIMARY KEY, - pubkey TEXT NOT NULL, - kind INTEGER NOT NULL, - created_at INTEGER NOT NULL, - content TEXT NOT NULL, - sig TEXT, - raw_event TEXT NOT NULL, - received_at INTEGER NOT NULL - ); - `); - - // Create event_tags table - console.log('[Schema] Creating event_tags table...'); - await db.execAsync(` - CREATE TABLE event_tags ( - event_id TEXT NOT NULL, - name TEXT NOT NULL, - value TEXT NOT NULL, - index_num INTEGER NOT NULL, - FOREIGN KEY(event_id) REFERENCES nostr_events(id) ON DELETE CASCADE - ); - CREATE INDEX idx_event_tags ON event_tags(name, value); - `); - // Create templates table with new columns - console.log('[Schema] Creating templates table...'); - await db.execAsync(` - CREATE TABLE 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, - is_archived BOOLEAN NOT NULL DEFAULT 0, - author_pubkey TEXT - ); - CREATE INDEX idx_templates_updated_at ON templates(updated_at); - `); - - // Create template_exercises table - console.log('[Schema] Creating template_exercises table...'); - await db.execAsync(` - CREATE TABLE 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 idx_template_exercises_template_id ON template_exercises(template_id); - `); - - // Create powr_packs table - console.log('[Schema] Creating powr_packs table...'); - await db.execAsync(` - CREATE TABLE powr_packs ( - id TEXT PRIMARY KEY, - title TEXT NOT NULL, - description TEXT, - author_pubkey TEXT, - nostr_event_id TEXT, - import_date INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ); - CREATE INDEX idx_powr_packs_import_date ON powr_packs(import_date DESC); - `); - - // Create powr_pack_items table - console.log('[Schema] Creating powr_pack_items table...'); - await db.execAsync(` - CREATE TABLE powr_pack_items ( - pack_id TEXT NOT NULL, - item_id TEXT NOT NULL, - item_type TEXT NOT NULL CHECK(item_type IN ('exercise', 'template')), - item_order INTEGER, - is_imported BOOLEAN NOT NULL DEFAULT 0, - nostr_event_id TEXT, - PRIMARY KEY (pack_id, item_id), - FOREIGN KEY (pack_id) REFERENCES powr_packs(id) ON DELETE CASCADE - ); - CREATE INDEX idx_powr_pack_items_type ON powr_pack_items(item_type); - `); - // Create other tables... - // (Create your other tables here - I've removed them for brevity) - - console.log('[Schema] All tables created successfully'); - } catch (error) { - console.error('[Schema] Error in createAllTables:', error); - throw error; - } - } private async updateSchemaVersion(db: SQLiteDatabase): Promise { try { @@ -530,4 +615,5 @@ class Schema { } } -export const schema = new Schema(); \ No newline at end of file +export const schema = new Schema(); + diff --git a/lib/db/services/FavoritesService.ts b/lib/db/services/FavoritesService.ts index cb84757..4c563f0 100644 --- a/lib/db/services/FavoritesService.ts +++ b/lib/db/services/FavoritesService.ts @@ -68,9 +68,33 @@ export class FavoritesService { throw error; } } + + private async ensureTableExists(): Promise { + try { + const tableExists = await this.db.getFirstAsync<{ count: number }>( + `SELECT count(*) as count FROM sqlite_master + WHERE type='table' AND name='favorites'` + ); + + if (!tableExists || tableExists.count === 0) { + await this.initialize(); + return false; + } + + return true; + } catch (error) { + console.error('[FavoritesService] Error checking if table exists:', error); + await this.initialize(); + return false; + } + } async isFavorite(contentType: ContentType, contentId: string): Promise { try { + if (!(await this.ensureTableExists())) { + return false; + } + const result = await this.db.getFirstAsync<{ count: number }>( `SELECT COUNT(*) as count FROM favorites WHERE content_type = ? AND content_id = ?`, [contentType, contentId] @@ -83,8 +107,22 @@ export class FavoritesService { } } + // Modify the getFavoriteIds method in FavoritesService.ts: async getFavoriteIds(contentType: ContentType): Promise { try { + // First check if the table exists + const tableExists = await this.db.getFirstAsync<{ count: number }>( + `SELECT count(*) as count FROM sqlite_master + WHERE type='table' AND name='favorites'` + ); + + if (!tableExists || tableExists.count === 0) { + console.log('[FavoritesService] Favorites table does not exist yet, returning empty array'); + // Initialize the table for next time + await this.initialize(); + return []; + } + const result = await this.db.getAllAsync<{ content_id: string }>( `SELECT content_id FROM favorites WHERE content_type = ?`, [contentType] @@ -99,6 +137,10 @@ export class FavoritesService { async getFavorites(contentType: ContentType): Promise> { try { + if (!(await this.ensureTableExists())) { + return []; + } + const result = await this.db.getAllAsync<{ id: string, content_id: string, diff --git a/lib/db/services/NostrIntegration.ts b/lib/db/services/NostrIntegration.ts index 870c04f..4a421dd 100644 --- a/lib/db/services/NostrIntegration.ts +++ b/lib/db/services/NostrIntegration.ts @@ -267,38 +267,138 @@ export class NostrIntegration { return 'Custom'; } - /** - * Get exercise references from a template event - */ + // Add this updated method to the NostrIntegration class + getTemplateExerciseRefs(templateEvent: NDKEvent): string[] { const exerciseTags = templateEvent.getMatchingTags('exercise'); const exerciseRefs: string[] = []; for (const tag of exerciseTags) { - if (tag.length > 1) { - // Get the reference exactly as it appears in the tag - const ref = tag[1]; + if (tag.length < 2) continue; + + let ref = tag[1]; + + // Build a complete reference that includes relay hints + const relayHints: string[] = []; + + // Check for relay hints in the main reference (if it has commas) + if (ref.includes(',')) { + const [baseRef, ...hints] = ref.split(','); + ref = baseRef; + hints.filter(h => h.startsWith('wss://')).forEach(h => relayHints.push(h)); + } + + // Also check for relay hints in the tag itself (additional elements) + for (let i = 2; i < tag.length; i++) { + if (tag[i].startsWith('wss://')) { + relayHints.push(tag[i]); + } + } + + // Add parameters if available + let fullRef = ref; + + // Check if params start after tag[1] + if (tag.length > 2 && !tag[2].startsWith('wss://')) { + let paramStart = 2; - // Add parameters if available - if (tag.length > 2) { - // Add parameters with "::" separator - const params = tag.slice(2).join(':'); - exerciseRefs.push(`${ref}::${params}`); - } else { - exerciseRefs.push(ref); + // Find all non-relay parameters + const params: string[] = []; + for (let i = paramStart; i < tag.length; i++) { + if (!tag[i].startsWith('wss://')) { + params.push(tag[i]); + } } - // Log the exact reference for debugging - console.log(`Extracted reference from template: ${exerciseRefs[exerciseRefs.length-1]}`); + if (params.length > 0) { + // Add parameters with "::" separator + fullRef += `::${params.join(':')}`; + } } + + // Reconstruct the reference with relay hints + if (relayHints.length > 0) { + fullRef += `,${relayHints.join(',')}`; + } + + exerciseRefs.push(fullRef); + console.log(`Extracted reference from template: ${fullRef}`); } return exerciseRefs; } + + // Add this updated method to the NostrIntegration class + + async findExercisesByNostrReference(refs: string[]): Promise> { + try { + const result = new Map(); + + for (const ref of refs) { + // Split the reference to separate the base reference from relay hints + const [baseRefWithParams, ...relayHints] = ref.split(','); + + // Further split to get the basic reference and parameters + let baseRef = baseRefWithParams; + if (baseRefWithParams.includes('::')) { + baseRef = baseRefWithParams.split('::')[0]; + } + + const refParts = baseRef.split(':'); + if (refParts.length < 3) continue; + + const refKind = refParts[0]; + const refPubkey = refParts[1]; + const refDTag = refParts[2]; + + // Try to find by d-tag and pubkey in nostr_metadata if available + const hasNostrMetadata = await this.columnExists('exercises', 'nostr_metadata'); + + let exercise = null; + + if (hasNostrMetadata) { + exercise = await this.db.getFirstAsync<{ id: string }>( + `SELECT id FROM exercises WHERE + JSON_EXTRACT(nostr_metadata, '$.pubkey') = ? AND + JSON_EXTRACT(nostr_metadata, '$.dTag') = ?`, + [refPubkey, refDTag] + ); + } + + // If not found, try matching by Nostr event ID + if (!exercise) { + exercise = await this.db.getFirstAsync<{ id: string }>( + `SELECT id FROM exercises WHERE nostr_event_id = ?`, + [refDTag] + ); + } + + // If still not found, try a direct ID match (in case dTag is an event ID) + if (!exercise) { + exercise = await this.db.getFirstAsync<{ id: string }>( + `SELECT id FROM exercises WHERE nostr_event_id = ?`, + [refDTag] + ); + } + + if (exercise) { + result.set(ref, exercise.id); + console.log(`Matched exercise reference ${ref} to local ID ${exercise.id}`); + + // Also store the base reference for easier future lookup + result.set(baseRef, exercise.id); + } + } + + return result; + } catch (error) { + console.error('Error finding exercises by Nostr reference:', error); + return new Map(); + } + } - /** - * Save an imported exercise to the database - */ + // Add this method to the NostrIntegration class + async saveImportedExercise(exercise: BaseExercise, originalEvent?: NDKEvent): Promise { try { // Convert format objects to JSON strings @@ -313,11 +413,22 @@ export class NostrIntegration { const dTag = originalEvent?.tagValue('d') || (exercise.availability?.lastSynced?.nostr?.metadata?.dTag || null); - // Store the d-tag in a JSON metadata field for easier searching + // Get relay hints from the event if available + const relayHints: string[] = []; + if (originalEvent) { + originalEvent.getMatchingTags('r').forEach(tag => { + if (tag.length > 1 && tag[1].startsWith('wss://')) { + relayHints.push(tag[1]); + } + }); + } + + // Store the d-tag and relay hints in metadata const nostrMetadata = JSON.stringify({ pubkey: originalEvent?.pubkey || exercise.availability?.lastSynced?.nostr?.metadata?.pubkey, dTag: dTag, - eventId: nostrEventId + eventId: nostrEventId, + relays: relayHints }); // Check if nostr_metadata column exists @@ -330,9 +441,9 @@ export class NostrIntegration { await this.db.runAsync( `INSERT INTO exercises - (id, title, type, category, equipment, description, format_json, format_units_json, + (id, title, type, category, equipment, description, format_json, format_units_json, created_at, updated_at, source, nostr_event_id${hasNostrMetadata ? ', nostr_metadata' : ''}) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?${hasNostrMetadata ? ', ?' : ''})`, + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?${hasNostrMetadata ? ', ?' : ''})`, [ exercise.id, exercise.title, @@ -436,6 +547,14 @@ export class NostrIntegration { await this.db.execAsync(`ALTER TABLE template_exercises ADD COLUMN nostr_reference TEXT`); } + // Check if relay_hints column exists + const hasRelayHints = await this.columnExists('template_exercises', 'relay_hints'); + + if (!hasRelayHints) { + console.log("Adding relay_hints column to template_exercises table"); + await this.db.execAsync(`ALTER TABLE template_exercises ADD COLUMN relay_hints TEXT`); + } + // Create template exercise records for (let i = 0; i < exerciseIds.length; i++) { const exerciseId = exerciseIds[i]; @@ -446,6 +565,11 @@ export class NostrIntegration { const exerciseRef = exerciseRefs[i] || ''; console.log(`Processing reference: ${exerciseRef}`); + // Extract relay hints from the reference + const parts = exerciseRef.split(','); + const baseRefWithParams = parts[0]; // This might include ::params + const relayHints = parts.slice(1).filter(r => r.startsWith('wss://')); + // Parse the reference format: kind:pubkey:d-tag::sets:reps:weight let targetSets = null; let targetReps = null; @@ -453,8 +577,8 @@ export class NostrIntegration { let setType = null; // Check if reference contains parameters - if (exerciseRef.includes('::')) { - const [_, paramString] = exerciseRef.split('::'); + if (baseRefWithParams.includes('::')) { + const [_, paramString] = baseRefWithParams.split('::'); const params = paramString.split(':'); if (params.length > 0) targetSets = params[0] ? parseInt(params[0]) : null; @@ -465,10 +589,14 @@ export class NostrIntegration { console.log(`Parsed parameters: sets=${targetSets}, reps=${targetReps}, weight=${targetWeight}, type=${setType}`); } + // Store relay hints in JSON + const relayHintsJson = relayHints.length > 0 ? JSON.stringify(relayHints) : null; + await this.db.runAsync( `INSERT INTO template_exercises - (id, template_id, exercise_id, display_order, target_sets, target_reps, target_weight, created_at, updated_at${hasNostrReference ? ', nostr_reference' : ''}) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?${hasNostrReference ? ', ?' : ''})`, + (id, template_id, exercise_id, display_order, target_sets, target_reps, target_weight, created_at, updated_at + ${hasNostrReference ? ', nostr_reference' : ''}${hasRelayHints ? ', relay_hints' : ''}) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?${hasNostrReference ? ', ?' : ''}${hasRelayHints ? ', ?' : ''})`, [ templateExerciseId, templateId, @@ -479,11 +607,12 @@ export class NostrIntegration { targetWeight, now, now, - ...(hasNostrReference ? [exerciseRef] : []) + ...(hasNostrReference ? [exerciseRef] : []), + ...(hasRelayHints ? [relayHintsJson] : []) ] ); - console.log(`Saved template-exercise relationship: template=${templateId}, exercise=${exerciseId}`); + console.log(`Saved template-exercise relationship: template=${templateId}, exercise=${exerciseId} with ${relayHints.length} relay hints`); } console.log(`Successfully saved ${exerciseIds.length} template-exercise relationships for template ${templateId}`); @@ -493,57 +622,6 @@ export class NostrIntegration { } } - /** - * Find exercises by Nostr reference - * This method helps match references in templates to actual exercises - */ - async findExercisesByNostrReference(refs: string[]): Promise> { - try { - const result = new Map(); - - for (const ref of refs) { - const refParts = ref.split('::')[0].split(':'); - if (refParts.length < 3) continue; - - const refKind = refParts[0]; - const refPubkey = refParts[1]; - const refDTag = refParts[2]; - - // Try to find by d-tag and pubkey in nostr_metadata if available - const hasNostrMetadata = await this.columnExists('exercises', 'nostr_metadata'); - - let exercise = null; - - if (hasNostrMetadata) { - exercise = await this.db.getFirstAsync<{ id: string }>( - `SELECT id FROM exercises WHERE - JSON_EXTRACT(nostr_metadata, '$.pubkey') = ? AND - JSON_EXTRACT(nostr_metadata, '$.dTag') = ?`, - [refPubkey, refDTag] - ); - } - - // Fallback: try to match by event ID - if (!exercise) { - exercise = await this.db.getFirstAsync<{ id: string }>( - `SELECT id FROM exercises WHERE nostr_event_id = ?`, - [refDTag] - ); - } - - if (exercise) { - result.set(ref, exercise.id); - console.log(`Matched exercise reference ${ref} to local ID ${exercise.id}`); - } - } - - return result; - } catch (error) { - console.error('Error finding exercises by Nostr reference:', error); - return new Map(); - } - } - /** * Check if a column exists in a table */ diff --git a/lib/db/services/POWRPackService.ts b/lib/db/services/POWRPackService.ts index 6a7d269..fd1674d 100644 --- a/lib/db/services/POWRPackService.ts +++ b/lib/db/services/POWRPackService.ts @@ -14,6 +14,8 @@ import { WorkoutTemplate, TemplateType } from '@/types/templates'; +import '@/types/ndk-extensions'; +import { safeAddRelay, safeRemoveRelay } from '@/types/ndk-common'; /** * Service for managing POWR Packs (importable collections of templates and exercises) @@ -21,13 +23,14 @@ import { export default class POWRPackService { private db: SQLiteDatabase; private nostrIntegration: NostrIntegration; + private exerciseWithRelays: Map = new Map(); constructor(db: SQLiteDatabase) { this.db = db; this.nostrIntegration = new NostrIntegration(db); } - /** + /** * Fetch a POWR Pack from a Nostr address (naddr) */ async fetchPackFromNaddr(naddr: string, ndk: NDK): Promise { @@ -45,8 +48,26 @@ export default class POWRPackService { throw new Error('Invalid naddr format'); } - const { pubkey, kind, identifier } = decoded.data; + const { pubkey, kind, identifier, relays } = decoded.data; console.log(`Decoded naddr: pubkey=${pubkey}, kind=${kind}, identifier=${identifier}`); + console.log(`Relay hints: ${relays ? relays.join(', ') : 'none'}`); + + // Track temporarily added relays + const addedRelays = new Set(); + + // If relay hints are provided, add them to NDK's pool temporarily + if (relays && relays.length > 0) { + for (const relay of relays) { + try { + console.log(`Adding suggested relay: ${relay}`); + // Use type assertion + this.safeAddRelay(ndk, relay); + addedRelays.add(relay); + } catch (error) { + console.warn(`Failed to add relay ${relay}:`, error); + } + } + } // Create filter to fetch the pack event const packFilter: NDKFilter = { @@ -59,6 +80,19 @@ export default class POWRPackService { // Fetch the pack event const events = await ndk.fetchEvents(packFilter); + + // Clean up - remove any temporarily added relays + if (addedRelays.size > 0) { + console.log(`Removing ${addedRelays.size} temporarily added relays`); + for (const relay of addedRelays) { + try { + this.safeRemoveRelay(ndk, relay); + } catch (err) { + console.warn(`Failed to remove relay ${relay}:`, err); + } + } + } + if (events.size === 0) { throw new Error('Pack not found'); } @@ -83,9 +117,29 @@ export default class POWRPackService { const addressPointer = tag[1]; if (addressPointer.startsWith('33402:')) { console.log(`Found template reference: ${addressPointer}`); + + // Include any relay hints in the tag + if (tag.length > 2) { + const relayHints = tag.slice(2).filter(r => r.startsWith('wss://')); + if (relayHints.length > 0) { + templateRefs.push(`${addressPointer},${relayHints.join(',')}`); + continue; + } + } + templateRefs.push(addressPointer); } else if (addressPointer.startsWith('33401:')) { console.log(`Found exercise reference: ${addressPointer}`); + + // Include any relay hints in the tag + if (tag.length > 2) { + const relayHints = tag.slice(2).filter(r => r.startsWith('wss://')); + if (relayHints.length > 0) { + exerciseRefs.push(`${addressPointer},${relayHints.join(',')}`); + continue; + } + } + exerciseRefs.push(addressPointer); } } @@ -118,15 +172,34 @@ export default class POWRPackService { console.log(`Fetching references: ${JSON.stringify(refs)}`); const events: NDKEvent[] = []; + const addedRelays: Set = new Set(); // Track temporarily added relays for (const ref of refs) { try { - // Parse the reference format (kind:pubkey:d-tag) - const [kindStr, pubkey, dTag] = ref.split(':'); + // Parse the reference format (kind:pubkey:d-tag,relay1,relay2) + const parts = ref.split(','); + const baseRef = parts[0]; + const relayHints = parts.slice(1).filter(r => r.startsWith('wss://')); + + const [kindStr, pubkey, dTag] = baseRef.split(':'); const kind = parseInt(kindStr); console.log(`Fetching ${kind} event with d-tag ${dTag} from author ${pubkey}`); + // Temporarily add these relays to NDK for this specific fetch + if (relayHints.length > 0) { + console.log(`With relay hints: ${relayHints.join(', ')}`); + + for (const relay of relayHints) { + try { + this.safeAddRelay(ndk, relay); + addedRelays.add(relay); + } catch (err) { + console.warn(`Failed to add relay ${relay}:`, err); + } + } + } + // Create a filter to find this specific event const filter: NDKFilter = { kinds: [kind], @@ -138,7 +211,23 @@ export default class POWRPackService { const fetchedEvents = await ndk.fetchEvents(filter); if (fetchedEvents.size > 0) { - events.push(Array.from(fetchedEvents)[0]); + const event = Array.from(fetchedEvents)[0]; + + // Add the relay hints to the event for future reference + if (relayHints.length > 0) { + relayHints.forEach(relay => { + // Check if the relay is already in tags + const existingRelayTag = event.getMatchingTags('r').some(tag => + tag.length > 1 && tag[1] === relay + ); + + if (!existingRelayTag) { + event.tags.push(['r', relay]); + } + }); + } + + events.push(event); continue; } @@ -148,6 +237,21 @@ export default class POWRPackService { const event = await ndk.fetchEvent(dTag); if (event) { console.log(`Successfully fetched event by ID: ${dTag}`); + + // Add the relay hints to the event for future reference + if (relayHints.length > 0) { + relayHints.forEach(relay => { + // Check if the relay is already in tags + const existingRelayTag = event.getMatchingTags('r').some(tag => + tag.length > 1 && tag[1] === relay + ); + + if (!existingRelayTag) { + event.tags.push(['r', relay]); + } + }); + } + events.push(event); } } catch (idError) { @@ -158,6 +262,18 @@ export default class POWRPackService { } } + // Clean up - remove any temporarily added relays + if (addedRelays.size > 0) { + console.log(`Removing ${addedRelays.size} temporarily added relays`); + for (const relay of addedRelays) { + try { + this.safeRemoveRelay(ndk, relay); + } catch (err) { + console.warn(`Failed to remove relay ${relay}:`, err); + } + } + } + console.log(`Total fetched referenced events: ${events.length}`); return events; } @@ -168,14 +284,27 @@ export default class POWRPackService { analyzeDependencies(templates: NDKEvent[], exercises: NDKEvent[]): Record { const dependencies: Record = {}; const exerciseMap = new Map(); + const exerciseWithRelays = new Map(); - // Map exercises by "kind:pubkey:d-tag" for easier lookup + // Map exercises by "kind:pubkey:d-tag" for easier lookup, preserving relay hints for (const exercise of exercises) { const dTag = exercise.tagValue('d'); if (dTag) { const reference = `33401:${exercise.pubkey}:${dTag}`; exerciseMap.set(reference, exercise); - console.log(`Mapped exercise ${exercise.id} to reference ${reference}`); + + // Extract relay hints from event if available + const relays: string[] = []; + exercise.getMatchingTags('r').forEach(tag => { + if (tag.length > 1 && tag[1].startsWith('wss://')) { + relays.push(tag[1]); + } + }); + + // Store event with its relay hints + exerciseWithRelays.set(reference, {event: exercise, relays}); + + console.log(`Mapped exercise ${exercise.id} to reference ${reference} with ${relays.length} relay hints`); } } @@ -193,24 +322,95 @@ export default class POWRPackService { for (const tag of exerciseTags) { if (tag.length < 2) continue; - const exerciseRef = tag[1]; - console.log(`Template ${templateName} references ${exerciseRef}`); + // Parse the full reference with potential relay hints + const fullRef = tag[1]; - // Find the exercise in our mapped exercises - const exercise = exerciseMap.get(exerciseRef); + // Split the reference to handle parameters first + const refWithParams = fullRef.split(',')[0]; // Get reference part without relay hints + const baseRef = refWithParams.split('::')[0]; // Extract base reference without parameters + + // Extract relay hints from the comma-separated list + const relayHintsFromCommas = fullRef.split(',').slice(1).filter(r => r.startsWith('wss://')); + + // Also check for relay hints in additional tag elements + const relayHintsFromTag = tag.slice(2).filter(item => item.startsWith('wss://')); + + // Combine all relay hints + const relayHints = [...relayHintsFromCommas, ...relayHintsFromTag]; + + console.log(`Template ${templateName} references ${refWithParams} with ${relayHints.length} relay hints`); + + // Find the exercise in our mapped exercises using only the base reference + const exercise = exerciseMap.get(baseRef); if (exercise) { dependencies[templateId].push(exercise.id); + + // Add any relay hints to our stored exercise data + const existingData = exerciseWithRelays.get(baseRef); + if (existingData && relayHints.length > 0) { + // Merge relay hints without duplicates + const uniqueRelays = new Set([...existingData.relays, ...relayHints]); + exerciseWithRelays.set(baseRef, { + event: existingData.event, + relays: Array.from(uniqueRelays) + }); + + console.log(`Updated relay hints for ${baseRef}: ${Array.from(uniqueRelays).join(', ')}`); + } + console.log(`Template ${templateName} depends on exercise ${exercise.id}`); } else { - console.log(`Template ${templateName} references unknown exercise ${exerciseRef}`); + console.log(`Template ${templateName} references unknown exercise ${refWithParams}`); } } console.log(`Template ${templateName} has ${dependencies[templateId].length} dependencies`); } + // Store the enhanced exercise mapping with relay hints in a class property + this.exerciseWithRelays = exerciseWithRelays; + return dependencies; } + + // Inside your POWRPackService class + private safeAddRelay(ndk: NDK, url: string): void { + try { + // Direct property access to check if method exists + if (typeof (ndk as any).addRelay === 'function') { + (ndk as any).addRelay(url); + console.log(`Added relay: ${url}`); + } else { + // Fallback implementation using pool + if (ndk.pool && ndk.pool.relays) { + const relay = ndk.pool.getRelay?.(url); + if (!relay) { + console.log(`Could not add relay ${url} - no implementation available`); + } + } + } + } catch (error) { + console.warn(`Failed to add relay ${url}:`, error); + } + } + + private safeRemoveRelay(ndk: NDK, url: string): void { + try { + // Direct property access to check if method exists + if (typeof (ndk as any).removeRelay === 'function') { + (ndk as any).removeRelay(url); + console.log(`Removed relay: ${url}`); + } else { + // Fallback implementation using pool + if (ndk.pool && ndk.pool.relays) { + ndk.pool.relays.delete(url); + console.log(`Removed relay ${url} using pool.relays.delete`); + } + } + } catch (error) { + console.warn(`Failed to remove relay ${url}:`, error); + } + } /** * Import a POWR Pack into the local database @@ -284,7 +484,9 @@ export default class POWRPackService { for (const ref of exerciseRefs) { // Extract the base reference (before any parameters) const refParts = ref.split('::'); - const baseRef = refParts[0]; + const baseRefWithRelays = refParts[0]; // May include relay hints separated by commas + const parts = baseRefWithRelays.split(','); + const baseRef = parts[0]; // Just the kind:pubkey:d-tag part console.log(`Looking for matching exercise for reference: ${baseRef}`); diff --git a/lib/hooks/useSubscribe.ts b/lib/hooks/useSubscribe.ts index 4e64d0d..14a2d53 100644 --- a/lib/hooks/useSubscribe.ts +++ b/lib/hooks/useSubscribe.ts @@ -42,17 +42,57 @@ export function useSubscribe( setIsLoading(true); }, []); + // Direct fetch function for manual fetching + const manualFetch = useCallback(async () => { + if (!ndk || !filters) return; + + try { + console.log('[useSubscribe] Manual fetch triggered'); + setIsLoading(true); + + const fetchedEvents = await ndk.fetchEvents(filters); + const eventsArray = Array.from(fetchedEvents); + + setEvents(prev => { + if (deduplicate) { + const existingIds = new Set(prev.map(e => e.id)); + const newEvents = eventsArray.filter(e => !existingIds.has(e.id)); + return [...prev, ...newEvents]; + } + return [...prev, ...eventsArray]; + }); + + setIsLoading(false); + setEose(true); + } catch (err) { + console.error('[useSubscribe] Manual fetch error:', err); + setIsLoading(false); + } + }, [ndk, filters, deduplicate]); + + // Only run the subscription effect when dependencies change + const filtersKey = filters ? JSON.stringify(filters) : 'none'; + const optionsKey = JSON.stringify(subscriptionOptions); + useEffect(() => { if (!ndk || !filters || !enabled) { setIsLoading(false); return; } + // Clean up any existing subscription + if (subscriptionRef.current) { + subscriptionRef.current.stop(); + subscriptionRef.current = null; + } + setIsLoading(true); setEose(false); try { - // Create subscription with NDK Mobile + console.log('[useSubscribe] Creating new subscription'); + + // Create subscription with NDK const subscription = ndk.subscribe(filters, { closeOnEose, ...subscriptionOptions @@ -60,32 +100,39 @@ export function useSubscribe( subscriptionRef.current = subscription; - subscription.on('event', (event: NDKEvent) => { + // Event handler - use a function reference to avoid recreating + const handleEvent = (event: NDKEvent) => { setEvents(prev => { if (deduplicate && prev.some(e => e.id === event.id)) { return prev; } return [...prev, event]; }); - }); + }; - subscription.on('eose', () => { + // EOSE handler + const handleEose = () => { setIsLoading(false); setEose(true); - }); + }; + + subscription.on('event', handleEvent); + subscription.on('eose', handleEose); + + // Clean up on unmount or when dependencies change + return () => { + if (subscription) { + subscription.off('event', handleEvent); + subscription.off('eose', handleEose); + subscription.stop(); + } + subscriptionRef.current = null; + }; } catch (error) { - console.error('[useSubscribe] Error:', error); + console.error('[useSubscribe] Subscription error:', error); setIsLoading(false); } - - // Cleanup function - return () => { - if (subscriptionRef.current) { - subscriptionRef.current.stop(); - subscriptionRef.current = null; - } - }; - }, [ndk, enabled, closeOnEose, JSON.stringify(filters), JSON.stringify(subscriptionOptions)]); + }, [ndk, enabled, filtersKey, optionsKey, closeOnEose, deduplicate]); return { events, @@ -93,6 +140,6 @@ export function useSubscribe( eose, clearEvents, resubscribe, - subscription: subscriptionRef.current + fetchEvents: manualFetch // Function to trigger manual fetch }; } \ No newline at end of file diff --git a/types/ndk-common.ts b/types/ndk-common.ts index a99116a..dde57ce 100644 --- a/types/ndk-common.ts +++ b/types/ndk-common.ts @@ -8,15 +8,15 @@ // Define a universal NDK interface that works with both packages export interface NDKCommon { - pool: { - relays: Map; - getRelay: (url: string) => any; - }; - connect: () => Promise; - disconnect: () => void; - fetchEvents: (filter: any) => Promise>; - signer?: any; - } + pool: { + relays: Map; + getRelay: (url: string) => any; + }; + connect: () => Promise; + disconnect?: () => void; // Make disconnect optional + fetchEvents: (filter: any) => Promise>; + signer?: any; +} // Define a universal NDKRelay interface export interface NDKRelayCommon { diff --git a/types/ndk-extensions.ts b/types/ndk-extensions.ts index 98ea807..b6f9f98 100644 --- a/types/ndk-extensions.ts +++ b/types/ndk-extensions.ts @@ -10,13 +10,11 @@ declare module '@nostr-dev-kit/ndk-mobile' { } interface NDK { - // Add missing methods - removeRelay?(url: string): void; - addRelay?(url: string, opts?: { read?: boolean; write?: boolean }, authPolicy?: any): NDKRelay | undefined; + removeRelay(url: string): void; + addRelay(url: string, opts?: { read?: boolean; write?: boolean }, authPolicy?: any): NDKRelay | undefined; } } -// Add methods to NDK prototype for backward compatibility export function extendNDK(ndk: any): any { // Only add methods if they don't already exist if (!ndk.hasOwnProperty('removeRelay')) {