powr pack bug fixes

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

View File

@ -169,13 +169,13 @@ export default function ImportPOWRPackScreen() {
// Get pack title from event
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 (

View File

@ -158,24 +158,6 @@ export default function SettingsDrawer() {
/>
),
},
{
id: 'notifications',
icon: Bell,
label: 'Notifications',
onPress: () => closeDrawer(),
},
{
id: 'data-sync',
icon: RefreshCw,
label: 'Data Sync',
onPress: () => closeDrawer(),
},
{
id: 'backup-restore',
icon: Database,
label: 'Backup & Restore',
onPress: () => closeDrawer(),
},
{
id: 'relays',
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,

View File

@ -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,
}

View File

@ -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();

View File

@ -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,

View File

@ -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
*/

View File

@ -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}`);

View File

@ -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
};
}

View File

@ -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 {

View File

@ -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')) {