mirror of
https://github.com/DocNR/POWR.git
synced 2025-04-23 01:01:27 +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
|
// 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 (
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
416
lib/db/schema.ts
416
lib/db/schema.ts
@ -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();
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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}`);
|
||||||
|
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
@ -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 {
|
||||||
|
@ -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')) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user