powr pack bug fixes

This commit is contained in:
DocNR 2025-03-16 21:31:38 -04:00
parent 80bdb87fdc
commit f1411e8568
10 changed files with 837 additions and 340 deletions

View File

@ -169,13 +169,13 @@ export default function ImportPOWRPackScreen() {
// Get pack title from event // Get pack title from event
const getPackTitle = (): string => { const getPackTitle = (): string => {
if (!packData?.packEvent) return 'Unknown Pack'; 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 // Get pack description from event
const getPackDescription = (): string => { const getPackDescription = (): string => {
if (!packData?.packEvent) return ''; if (!packData?.packEvent) return '';
return findTagValue(packData.packEvent.tags, 'description') || packData.packEvent.content || ''; return findTagValue(packData.packEvent.tags, 'about') || packData.packEvent.content || '';
}; };
return ( return (

View File

@ -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', id: 'relays',
icon: Globe, icon: Globe,
@ -191,12 +173,6 @@ export default function SettingsDrawer() {
router.push("/(packs)/manage"); router.push("/(packs)/manage");
}, },
}, },
{
id: 'device',
icon: Smartphone,
label: 'Device Settings',
onPress: () => closeDrawer(),
},
{ {
id: 'nostr', id: 'nostr',
icon: Zap, icon: Zap,

View File

@ -1,37 +1,59 @@
// components/social/POWRPackSection.tsx // 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 { View, ScrollView, StyleSheet, TouchableOpacity, Image } from 'react-native';
import { router } from 'expo-router'; import { router } from 'expo-router';
import { useNDK } from '@/lib/hooks/useNDK'; import { useNDK } from '@/lib/hooks/useNDK';
import { useSubscribe } from '@/lib/hooks/useSubscribe';
import { findTagValue } from '@/utils/nostr-utils'; import { findTagValue } from '@/utils/nostr-utils';
import { Text } from '@/components/ui/text'; import { Text } from '@/components/ui/text';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton'; 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 { NDKEvent } from '@nostr-dev-kit/ndk-mobile';
import { usePOWRPackService } from '@/components/DatabaseProvider';
import { Clipboard } from 'react-native'; import { Clipboard } from 'react-native';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
export default function POWRPackSection() { export default function POWRPackSection() {
const { ndk } = useNDK(); const { ndk } = useNDK();
const powrPackService = usePOWRPackService(); const [isLoading, setIsLoading] = useState(false);
const [featuredPacks, setFeaturedPacks] = useState<NDKEvent[]>([]); const [featuredPacks, setFeaturedPacks] = useState<NDKEvent[]>([]);
const [error, setError] = useState<Error | null>(null);
// Subscribe to POWR packs (kind 30004 with powrpack hashtag) // Manual fetch function
const { events, isLoading } = useSubscribe( const handleFetchPacks = async () => {
ndk ? [{ kinds: [30004], '#t': ['powrpack', 'fitness', 'workout'], limit: 10 }] : false, if (!ndk) return;
{ enabled: !!ndk }
); try {
setIsLoading(true);
// Update featured packs when events change setError(null);
useEffect(() => {
if (events.length > 0) { console.log('Manually fetching POWR packs');
setFeaturedPacks(events); 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 // Handle pack click
const handlePackClick = (packEvent: NDKEvent) => { const handlePackClick = (packEvent: NDKEvent) => {
@ -42,12 +64,23 @@ export default function POWRPackSection() {
throw new Error('Pack is missing identifier (d tag)'); 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 // Create shareable naddr
const naddr = nip19.naddrEncode({ const naddr = nip19.naddrEncode({
kind: 30004, kind: 30004,
pubkey: packEvent.pubkey, pubkey: packEvent.pubkey,
identifier: dTag, identifier: dTag,
relays: ['wss://relay.damus.io', 'wss://nos.lol', 'wss://relay.nostr.band'] relays
}); });
// Copy to clipboard // Copy to clipboard
@ -69,21 +102,30 @@ export default function POWRPackSection() {
router.push('/(packs)/manage'); router.push('/(packs)/manage');
}; };
// Only show section if we have packs or are loading // Fetch packs when mounted
const showSection = featuredPacks.length > 0 || isLoading; React.useEffect(() => {
if (ndk) {
if (!showSection) { handleFetchPacks();
return null; }
} }, [ndk]);
return ( return (
<View style={styles.container}> <View style={styles.container}>
<View style={styles.header}> <View style={styles.header}>
<Text style={styles.title}>POWR Packs</Text> <Text style={styles.title}>POWR Packs</Text>
<TouchableOpacity onPress={handleViewAll} style={styles.viewAll}> <View style={styles.headerButtons}>
<Text style={styles.viewAllText}>View All</Text> <TouchableOpacity
<ArrowRight size={16} color="#6b7280" /> onPress={handleFetchPacks}
</TouchableOpacity> style={styles.refreshButton}
disabled={isLoading}
>
<RefreshCw size={16} color="#6b7280" />
</TouchableOpacity>
<TouchableOpacity onPress={handleViewAll} style={styles.viewAll}>
<Text style={styles.viewAllText}>View All</Text>
<ArrowRight size={16} color="#6b7280" />
</TouchableOpacity>
</View>
</View> </View>
<ScrollView <ScrollView
@ -141,6 +183,19 @@ export default function POWRPackSection() {
</TouchableOpacity> </TouchableOpacity>
); );
}) })
) : error ? (
// Error state
<View style={styles.emptyState}>
<Text style={styles.errorText}>Error loading packs</Text>
<Button
onPress={handleFetchPacks}
size="sm"
variant="outline"
style={styles.emptyButton}
>
<Text>Try Again</Text>
</Button>
</View>
) : ( ) : (
// No packs found // No packs found
<View style={styles.emptyState}> <View style={styles.emptyState}>
@ -176,6 +231,14 @@ const styles = StyleSheet.create({
fontSize: 18, fontSize: 18,
fontWeight: '600', fontWeight: '600',
}, },
headerButtons: {
flexDirection: 'row',
alignItems: 'center',
},
refreshButton: {
padding: 8,
marginRight: 8,
},
viewAll: { viewAll: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
@ -233,7 +296,7 @@ const styles = StyleSheet.create({
borderRadius: 4, borderRadius: 4,
}, },
emptyState: { emptyState: {
width: '100%', width: 280,
padding: 24, padding: 24,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
@ -243,6 +306,11 @@ const styles = StyleSheet.create({
marginBottom: 16, marginBottom: 16,
color: '#6b7280', color: '#6b7280',
}, },
errorText: {
marginTop: 8,
marginBottom: 16,
color: '#ef4444',
},
emptyButton: { emptyButton: {
marginTop: 8, marginTop: 8,
} }

View File

@ -2,7 +2,7 @@
import { SQLiteDatabase } from 'expo-sqlite'; import { SQLiteDatabase } from 'expo-sqlite';
import { Platform } from 'react-native'; import { Platform } from 'react-native';
export const SCHEMA_VERSION = 9; export const SCHEMA_VERSION = 10;
class Schema { class Schema {
private async getCurrentVersion(db: SQLiteDatabase): Promise<number> { private async getCurrentVersion(db: SQLiteDatabase): Promise<number> {
@ -29,6 +29,37 @@ class Schema {
} }
} }
// Version 8 migration - add template archive and author pubkey
async migrate_v8(db: SQLiteDatabase): Promise<void> {
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<void> { async migrate_v9(db: SQLiteDatabase): Promise<void> {
try { try {
console.log('[Schema] Running migration v9 - Enhanced Nostr metadata'); console.log('[Schema] Running migration v9 - Enhanced Nostr metadata');
@ -72,6 +103,31 @@ class Schema {
} }
} }
async migrate_v10(db: SQLiteDatabase): Promise<void> {
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<void> { async createTables(db: SQLiteDatabase): Promise<void> {
try { try {
console.log(`[Schema] Initializing database on ${Platform.OS}`); console.log(`[Schema] Initializing database on ${Platform.OS}`);
@ -118,6 +174,16 @@ class Schema {
await this.migrate_v8(db); 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 // Update schema version at the end of the transaction
await this.updateSchemaVersion(db); await this.updateSchemaVersion(db);
}); });
@ -135,12 +201,22 @@ class Schema {
// Create all tables in their latest form // Create all tables in their latest form
await this.createAllTables(db); await this.createAllTables(db);
// Run migrations if needed // Run migrations if needed (same as in transaction)
if (currentVersion < 8) { if (currentVersion < 8) {
console.log(`[Schema] Running migration from version ${currentVersion} to 8`); console.log(`[Schema] Running migration from version ${currentVersion} to 8`);
await this.migrate_v8(db); 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 // Update schema version
await this.updateSchemaVersion(db); await this.updateSchemaVersion(db);
@ -151,38 +227,161 @@ class Schema {
throw error; throw error;
} }
} }
// Version 8 migration - add template archive and author pubkey
async migrate_v8(db: SQLiteDatabase): Promise<void> { private async createAllTables(db: SQLiteDatabase): Promise<void> {
try { 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 // Create exercises table
const columnsResult = await db.getAllAsync<{ name: string }>( console.log('[Schema] Creating exercises table...');
"PRAGMA table_info(templates)" 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 // Create templates table with new columns
if (!columnNames.includes('is_archived')) { console.log('[Schema] Creating templates table...');
console.log('[Schema] Adding is_archived column to templates table'); await db.execAsync(`
await db.execAsync('ALTER TABLE templates ADD COLUMN is_archived BOOLEAN NOT NULL DEFAULT 0'); 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 // Create favorites table - moved inside the try block
if (!columnNames.includes('author_pubkey')) { console.log('[Schema] Creating favorites table...');
console.log('[Schema] Adding author_pubkey column to templates table'); await db.execAsync(`
await db.execAsync('ALTER TABLE templates ADD COLUMN author_pubkey TEXT'); 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) { } catch (error) {
console.error('[Schema] Error in migration v8:', error); console.error('[Schema] Error in createAllTables:', error);
throw error; throw error;
} }
} }
// Add this method to check for and create critical tables
async ensureCriticalTablesExist(db: SQLiteDatabase): Promise<void> { async ensureCriticalTablesExist(db: SQLiteDatabase): Promise<void> {
try { try {
console.log('[Schema] Checking for missing critical tables...'); 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); CREATE INDEX IF NOT EXISTS idx_workout_sets_exercise_id ON workout_sets(workout_exercise_id);
`); `);
} }
// Check if templates table exists // Check if templates table exists
const templatesTableExists = await db.getFirstAsync<{ count: number }>( const templatesTableExists = await db.getFirstAsync<{ count: number }>(
`SELECT count(*) as count FROM sqlite_master `SELECT count(*) as count FROM sqlite_master
@ -301,6 +501,30 @@ class Schema {
// If templates table exists, ensure new columns are added // If templates table exists, ensure new columns are added
await this.migrate_v8(db); 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'); console.log('[Schema] Critical tables check complete');
} catch (error) { } catch (error) {
@ -308,7 +532,7 @@ class Schema {
throw error; throw error;
} }
} }
private async dropAllTables(db: SQLiteDatabase): Promise<void> { private async dropAllTables(db: SQLiteDatabase): Promise<void> {
try { try {
console.log('[Schema] Getting list of tables to drop...'); console.log('[Schema] Getting list of tables to drop...');
@ -335,145 +559,6 @@ class Schema {
throw error; throw error;
} }
} }
private async createAllTables(db: SQLiteDatabase): Promise<void> {
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<void> { private async updateSchemaVersion(db: SQLiteDatabase): Promise<void> {
try { try {
@ -530,4 +615,5 @@ class Schema {
} }
} }
export const schema = new Schema(); export const schema = new Schema();

View File

@ -68,9 +68,33 @@ export class FavoritesService {
throw error; throw error;
} }
} }
private async ensureTableExists(): Promise<boolean> {
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<boolean> { async isFavorite(contentType: ContentType, contentId: string): Promise<boolean> {
try { try {
if (!(await this.ensureTableExists())) {
return false;
}
const result = await this.db.getFirstAsync<{ count: number }>( const result = await this.db.getFirstAsync<{ count: number }>(
`SELECT COUNT(*) as count FROM favorites WHERE content_type = ? AND content_id = ?`, `SELECT COUNT(*) as count FROM favorites WHERE content_type = ? AND content_id = ?`,
[contentType, contentId] [contentType, contentId]
@ -83,8 +107,22 @@ export class FavoritesService {
} }
} }
// Modify the getFavoriteIds method in FavoritesService.ts:
async getFavoriteIds(contentType: ContentType): Promise<string[]> { async getFavoriteIds(contentType: ContentType): Promise<string[]> {
try { 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 }>( const result = await this.db.getAllAsync<{ content_id: string }>(
`SELECT content_id FROM favorites WHERE content_type = ?`, `SELECT content_id FROM favorites WHERE content_type = ?`,
[contentType] [contentType]
@ -99,6 +137,10 @@ export class FavoritesService {
async getFavorites<T>(contentType: ContentType): Promise<Array<{id: string, content: T, addedAt: number}>> { async getFavorites<T>(contentType: ContentType): Promise<Array<{id: string, content: T, addedAt: number}>> {
try { try {
if (!(await this.ensureTableExists())) {
return [];
}
const result = await this.db.getAllAsync<{ const result = await this.db.getAllAsync<{
id: string, id: string,
content_id: string, content_id: string,

View File

@ -267,38 +267,138 @@ export class NostrIntegration {
return 'Custom'; return 'Custom';
} }
/** // Add this updated method to the NostrIntegration class
* Get exercise references from a template event
*/
getTemplateExerciseRefs(templateEvent: NDKEvent): string[] { getTemplateExerciseRefs(templateEvent: NDKEvent): string[] {
const exerciseTags = templateEvent.getMatchingTags('exercise'); const exerciseTags = templateEvent.getMatchingTags('exercise');
const exerciseRefs: string[] = []; const exerciseRefs: string[] = [];
for (const tag of exerciseTags) { for (const tag of exerciseTags) {
if (tag.length > 1) { if (tag.length < 2) continue;
// Get the reference exactly as it appears in the tag
const ref = tag[1]; 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 // Find all non-relay parameters
if (tag.length > 2) { const params: string[] = [];
// Add parameters with "::" separator for (let i = paramStart; i < tag.length; i++) {
const params = tag.slice(2).join(':'); if (!tag[i].startsWith('wss://')) {
exerciseRefs.push(`${ref}::${params}`); params.push(tag[i]);
} else { }
exerciseRefs.push(ref);
} }
// Log the exact reference for debugging if (params.length > 0) {
console.log(`Extracted reference from template: ${exerciseRefs[exerciseRefs.length-1]}`); // 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; return exerciseRefs;
} }
// Add this updated method to the NostrIntegration class
async findExercisesByNostrReference(refs: string[]): Promise<Map<string, string>> {
try {
const result = new Map<string, string>();
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();
}
}
/** // Add this method to the NostrIntegration class
* Save an imported exercise to the database
*/
async saveImportedExercise(exercise: BaseExercise, originalEvent?: NDKEvent): Promise<string> { async saveImportedExercise(exercise: BaseExercise, originalEvent?: NDKEvent): Promise<string> {
try { try {
// Convert format objects to JSON strings // Convert format objects to JSON strings
@ -313,11 +413,22 @@ export class NostrIntegration {
const dTag = originalEvent?.tagValue('d') || const dTag = originalEvent?.tagValue('d') ||
(exercise.availability?.lastSynced?.nostr?.metadata?.dTag || null); (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({ const nostrMetadata = JSON.stringify({
pubkey: originalEvent?.pubkey || exercise.availability?.lastSynced?.nostr?.metadata?.pubkey, pubkey: originalEvent?.pubkey || exercise.availability?.lastSynced?.nostr?.metadata?.pubkey,
dTag: dTag, dTag: dTag,
eventId: nostrEventId eventId: nostrEventId,
relays: relayHints
}); });
// Check if nostr_metadata column exists // Check if nostr_metadata column exists
@ -330,9 +441,9 @@ export class NostrIntegration {
await this.db.runAsync( await this.db.runAsync(
`INSERT INTO exercises `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' : ''}) created_at, updated_at, source, nostr_event_id${hasNostrMetadata ? ', nostr_metadata' : ''})
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?${hasNostrMetadata ? ', ?' : ''})`, VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?${hasNostrMetadata ? ', ?' : ''})`,
[ [
exercise.id, exercise.id,
exercise.title, exercise.title,
@ -436,6 +547,14 @@ export class NostrIntegration {
await this.db.execAsync(`ALTER TABLE template_exercises ADD COLUMN nostr_reference TEXT`); 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 // Create template exercise records
for (let i = 0; i < exerciseIds.length; i++) { for (let i = 0; i < exerciseIds.length; i++) {
const exerciseId = exerciseIds[i]; const exerciseId = exerciseIds[i];
@ -446,6 +565,11 @@ export class NostrIntegration {
const exerciseRef = exerciseRefs[i] || ''; const exerciseRef = exerciseRefs[i] || '';
console.log(`Processing reference: ${exerciseRef}`); 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 // Parse the reference format: kind:pubkey:d-tag::sets:reps:weight
let targetSets = null; let targetSets = null;
let targetReps = null; let targetReps = null;
@ -453,8 +577,8 @@ export class NostrIntegration {
let setType = null; let setType = null;
// Check if reference contains parameters // Check if reference contains parameters
if (exerciseRef.includes('::')) { if (baseRefWithParams.includes('::')) {
const [_, paramString] = exerciseRef.split('::'); const [_, paramString] = baseRefWithParams.split('::');
const params = paramString.split(':'); const params = paramString.split(':');
if (params.length > 0) targetSets = params[0] ? parseInt(params[0]) : null; 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}`); 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( await this.db.runAsync(
`INSERT INTO template_exercises `INSERT INTO template_exercises
(id, template_id, exercise_id, display_order, target_sets, target_reps, target_weight, created_at, updated_at${hasNostrReference ? ', nostr_reference' : ''}) (id, template_id, exercise_id, display_order, target_sets, target_reps, target_weight, created_at, updated_at
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?${hasNostrReference ? ', ?' : ''})`, ${hasNostrReference ? ', nostr_reference' : ''}${hasRelayHints ? ', relay_hints' : ''})
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?${hasNostrReference ? ', ?' : ''}${hasRelayHints ? ', ?' : ''})`,
[ [
templateExerciseId, templateExerciseId,
templateId, templateId,
@ -479,11 +607,12 @@ export class NostrIntegration {
targetWeight, targetWeight,
now, now,
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}`); 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<Map<string, string>> {
try {
const result = new Map<string, string>();
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 * Check if a column exists in a table
*/ */

View File

@ -14,6 +14,8 @@ import {
WorkoutTemplate, WorkoutTemplate,
TemplateType TemplateType
} from '@/types/templates'; } 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) * Service for managing POWR Packs (importable collections of templates and exercises)
@ -21,13 +23,14 @@ import {
export default class POWRPackService { export default class POWRPackService {
private db: SQLiteDatabase; private db: SQLiteDatabase;
private nostrIntegration: NostrIntegration; private nostrIntegration: NostrIntegration;
private exerciseWithRelays: Map<string, {event: NDKEvent, relays: string[]}> = new Map();
constructor(db: SQLiteDatabase) { constructor(db: SQLiteDatabase) {
this.db = db; this.db = db;
this.nostrIntegration = new NostrIntegration(db); this.nostrIntegration = new NostrIntegration(db);
} }
/** /**
* Fetch a POWR Pack from a Nostr address (naddr) * Fetch a POWR Pack from a Nostr address (naddr)
*/ */
async fetchPackFromNaddr(naddr: string, ndk: NDK): Promise<POWRPackImport> { async fetchPackFromNaddr(naddr: string, ndk: NDK): Promise<POWRPackImport> {
@ -45,8 +48,26 @@ export default class POWRPackService {
throw new Error('Invalid naddr format'); 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(`Decoded naddr: pubkey=${pubkey}, kind=${kind}, identifier=${identifier}`);
console.log(`Relay hints: ${relays ? relays.join(', ') : 'none'}`);
// Track temporarily added relays
const addedRelays = new Set<string>();
// 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 // Create filter to fetch the pack event
const packFilter: NDKFilter = { const packFilter: NDKFilter = {
@ -59,6 +80,19 @@ export default class POWRPackService {
// Fetch the pack event // Fetch the pack event
const events = await ndk.fetchEvents(packFilter); 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) { if (events.size === 0) {
throw new Error('Pack not found'); throw new Error('Pack not found');
} }
@ -83,9 +117,29 @@ export default class POWRPackService {
const addressPointer = tag[1]; const addressPointer = tag[1];
if (addressPointer.startsWith('33402:')) { if (addressPointer.startsWith('33402:')) {
console.log(`Found template reference: ${addressPointer}`); 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); templateRefs.push(addressPointer);
} else if (addressPointer.startsWith('33401:')) { } else if (addressPointer.startsWith('33401:')) {
console.log(`Found exercise reference: ${addressPointer}`); 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); exerciseRefs.push(addressPointer);
} }
} }
@ -118,15 +172,34 @@ export default class POWRPackService {
console.log(`Fetching references: ${JSON.stringify(refs)}`); console.log(`Fetching references: ${JSON.stringify(refs)}`);
const events: NDKEvent[] = []; const events: NDKEvent[] = [];
const addedRelays: Set<string> = new Set(); // Track temporarily added relays
for (const ref of refs) { for (const ref of refs) {
try { try {
// Parse the reference format (kind:pubkey:d-tag) // Parse the reference format (kind:pubkey:d-tag,relay1,relay2)
const [kindStr, pubkey, dTag] = ref.split(':'); 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); const kind = parseInt(kindStr);
console.log(`Fetching ${kind} event with d-tag ${dTag} from author ${pubkey}`); 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 // Create a filter to find this specific event
const filter: NDKFilter = { const filter: NDKFilter = {
kinds: [kind], kinds: [kind],
@ -138,7 +211,23 @@ export default class POWRPackService {
const fetchedEvents = await ndk.fetchEvents(filter); const fetchedEvents = await ndk.fetchEvents(filter);
if (fetchedEvents.size > 0) { 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; continue;
} }
@ -148,6 +237,21 @@ export default class POWRPackService {
const event = await ndk.fetchEvent(dTag); const event = await ndk.fetchEvent(dTag);
if (event) { if (event) {
console.log(`Successfully fetched event by ID: ${dTag}`); 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); events.push(event);
} }
} catch (idError) { } 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}`); console.log(`Total fetched referenced events: ${events.length}`);
return events; return events;
} }
@ -168,14 +284,27 @@ export default class POWRPackService {
analyzeDependencies(templates: NDKEvent[], exercises: NDKEvent[]): Record<string, string[]> { analyzeDependencies(templates: NDKEvent[], exercises: NDKEvent[]): Record<string, string[]> {
const dependencies: Record<string, string[]> = {}; const dependencies: Record<string, string[]> = {};
const exerciseMap = new Map<string, NDKEvent>(); const exerciseMap = new Map<string, NDKEvent>();
const exerciseWithRelays = new Map<string, {event: NDKEvent, relays: string[]}>();
// 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) { for (const exercise of exercises) {
const dTag = exercise.tagValue('d'); const dTag = exercise.tagValue('d');
if (dTag) { if (dTag) {
const reference = `33401:${exercise.pubkey}:${dTag}`; const reference = `33401:${exercise.pubkey}:${dTag}`;
exerciseMap.set(reference, exercise); 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) { for (const tag of exerciseTags) {
if (tag.length < 2) continue; if (tag.length < 2) continue;
const exerciseRef = tag[1]; // Parse the full reference with potential relay hints
console.log(`Template ${templateName} references ${exerciseRef}`); const fullRef = tag[1];
// Find the exercise in our mapped exercises // Split the reference to handle parameters first
const exercise = exerciseMap.get(exerciseRef); 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) { if (exercise) {
dependencies[templateId].push(exercise.id); 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}`); console.log(`Template ${templateName} depends on exercise ${exercise.id}`);
} else { } 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`); 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; 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 * Import a POWR Pack into the local database
@ -284,7 +484,9 @@ export default class POWRPackService {
for (const ref of exerciseRefs) { for (const ref of exerciseRefs) {
// Extract the base reference (before any parameters) // Extract the base reference (before any parameters)
const refParts = ref.split('::'); 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}`); console.log(`Looking for matching exercise for reference: ${baseRef}`);

View File

@ -42,17 +42,57 @@ export function useSubscribe(
setIsLoading(true); 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(() => { useEffect(() => {
if (!ndk || !filters || !enabled) { if (!ndk || !filters || !enabled) {
setIsLoading(false); setIsLoading(false);
return; return;
} }
// Clean up any existing subscription
if (subscriptionRef.current) {
subscriptionRef.current.stop();
subscriptionRef.current = null;
}
setIsLoading(true); setIsLoading(true);
setEose(false); setEose(false);
try { try {
// Create subscription with NDK Mobile console.log('[useSubscribe] Creating new subscription');
// Create subscription with NDK
const subscription = ndk.subscribe(filters, { const subscription = ndk.subscribe(filters, {
closeOnEose, closeOnEose,
...subscriptionOptions ...subscriptionOptions
@ -60,32 +100,39 @@ export function useSubscribe(
subscriptionRef.current = subscription; subscriptionRef.current = subscription;
subscription.on('event', (event: NDKEvent) => { // Event handler - use a function reference to avoid recreating
const handleEvent = (event: NDKEvent) => {
setEvents(prev => { setEvents(prev => {
if (deduplicate && prev.some(e => e.id === event.id)) { if (deduplicate && prev.some(e => e.id === event.id)) {
return prev; return prev;
} }
return [...prev, event]; return [...prev, event];
}); });
}); };
subscription.on('eose', () => { // EOSE handler
const handleEose = () => {
setIsLoading(false); setIsLoading(false);
setEose(true); 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) { } catch (error) {
console.error('[useSubscribe] Error:', error); console.error('[useSubscribe] Subscription error:', error);
setIsLoading(false); setIsLoading(false);
} }
}, [ndk, enabled, filtersKey, optionsKey, closeOnEose, deduplicate]);
// Cleanup function
return () => {
if (subscriptionRef.current) {
subscriptionRef.current.stop();
subscriptionRef.current = null;
}
};
}, [ndk, enabled, closeOnEose, JSON.stringify(filters), JSON.stringify(subscriptionOptions)]);
return { return {
events, events,
@ -93,6 +140,6 @@ export function useSubscribe(
eose, eose,
clearEvents, clearEvents,
resubscribe, resubscribe,
subscription: subscriptionRef.current fetchEvents: manualFetch // Function to trigger manual fetch
}; };
} }

View File

@ -8,15 +8,15 @@
// Define a universal NDK interface that works with both packages // Define a universal NDK interface that works with both packages
export interface NDKCommon { export interface NDKCommon {
pool: { pool: {
relays: Map<string, any>; relays: Map<string, any>;
getRelay: (url: string) => any; getRelay: (url: string) => any;
}; };
connect: () => Promise<void>; connect: () => Promise<void>;
disconnect: () => void; disconnect?: () => void; // Make disconnect optional
fetchEvents: (filter: any) => Promise<Set<any>>; fetchEvents: (filter: any) => Promise<Set<any>>;
signer?: any; signer?: any;
} }
// Define a universal NDKRelay interface // Define a universal NDKRelay interface
export interface NDKRelayCommon { export interface NDKRelayCommon {

View File

@ -10,13 +10,11 @@ declare module '@nostr-dev-kit/ndk-mobile' {
} }
interface NDK { interface NDK {
// Add missing methods removeRelay(url: string): void;
removeRelay?(url: string): void; addRelay(url: string, opts?: { read?: boolean; write?: boolean }, authPolicy?: any): NDKRelay | undefined;
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 { export function extendNDK(ndk: any): any {
// Only add methods if they don't already exist // Only add methods if they don't already exist
if (!ndk.hasOwnProperty('removeRelay')) { if (!ndk.hasOwnProperty('removeRelay')) {