mirror of
https://github.com/DocNR/POWR.git
synced 2025-04-19 10:51:19 +00:00
powr pack bug fixes
This commit is contained in:
parent
80bdb87fdc
commit
f1411e8568
@ -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 (
|
||||
|
@ -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,
|
||||
|
@ -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<NDKEvent[]>([]);
|
||||
const [error, setError] = useState<Error | null>(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 (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>POWR Packs</Text>
|
||||
<TouchableOpacity onPress={handleViewAll} style={styles.viewAll}>
|
||||
<Text style={styles.viewAllText}>View All</Text>
|
||||
<ArrowRight size={16} color="#6b7280" />
|
||||
</TouchableOpacity>
|
||||
<View style={styles.headerButtons}>
|
||||
<TouchableOpacity
|
||||
onPress={handleFetchPacks}
|
||||
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>
|
||||
|
||||
<ScrollView
|
||||
@ -141,6 +183,19 @@ export default function POWRPackSection() {
|
||||
</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
|
||||
<View style={styles.emptyState}>
|
||||
@ -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,
|
||||
}
|
||||
|
416
lib/db/schema.ts
416
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<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> {
|
||||
try {
|
||||
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> {
|
||||
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<void> {
|
||||
|
||||
private async createAllTables(db: SQLiteDatabase): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
try {
|
||||
console.log('[Schema] Getting list of tables to drop...');
|
||||
@ -335,145 +559,6 @@ class Schema {
|
||||
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> {
|
||||
try {
|
||||
@ -530,4 +615,5 @@ class Schema {
|
||||
}
|
||||
}
|
||||
|
||||
export const schema = new Schema();
|
||||
export const schema = new Schema();
|
||||
|
||||
|
@ -68,9 +68,33 @@ export class FavoritesService {
|
||||
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> {
|
||||
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<string[]> {
|
||||
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<T>(contentType: ContentType): Promise<Array<{id: string, content: T, addedAt: number}>> {
|
||||
try {
|
||||
if (!(await this.ensureTableExists())) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = await this.db.getAllAsync<{
|
||||
id: string,
|
||||
content_id: string,
|
||||
|
@ -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<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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an imported exercise to the database
|
||||
*/
|
||||
// Add this method to the NostrIntegration class
|
||||
|
||||
async saveImportedExercise(exercise: BaseExercise, originalEvent?: NDKEvent): Promise<string> {
|
||||
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<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
|
||||
*/
|
||||
|
@ -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<string, {event: NDKEvent, relays: string[]}> = 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<POWRPackImport> {
|
||||
@ -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<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
|
||||
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<string> = 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<string, string[]> {
|
||||
const dependencies: Record<string, string[]> = {};
|
||||
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) {
|
||||
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}`);
|
||||
|
||||
|
@ -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
|
||||
};
|
||||
}
|
@ -8,15 +8,15 @@
|
||||
|
||||
// Define a universal NDK interface that works with both packages
|
||||
export interface NDKCommon {
|
||||
pool: {
|
||||
relays: Map<string, any>;
|
||||
getRelay: (url: string) => any;
|
||||
};
|
||||
connect: () => Promise<void>;
|
||||
disconnect: () => void;
|
||||
fetchEvents: (filter: any) => Promise<Set<any>>;
|
||||
signer?: any;
|
||||
}
|
||||
pool: {
|
||||
relays: Map<string, any>;
|
||||
getRelay: (url: string) => any;
|
||||
};
|
||||
connect: () => Promise<void>;
|
||||
disconnect?: () => void; // Make disconnect optional
|
||||
fetchEvents: (filter: any) => Promise<Set<any>>;
|
||||
signer?: any;
|
||||
}
|
||||
|
||||
// Define a universal NDKRelay interface
|
||||
export interface NDKRelayCommon {
|
||||
|
@ -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')) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user