updated with NDK-mobile library and removed custom nostr functions

This commit is contained in:
DocNR 2025-03-06 16:34:50 -05:00
parent 2564450333
commit b61381b865
19 changed files with 1063 additions and 1191 deletions

View File

@ -5,25 +5,47 @@ All notable changes to the POWR project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
# Changelog - March 6, 2025
## Added
- NDK mobile integration for Nostr functionality
- Added event publishing and subscription capabilities
- Implemented proper type safety for NDK interactions
- Created testing components for NDK functionality verification
- Enhanced exercise management with Nostr support
- Implemented exercise creation, editing, and forking workflows
- Added support for custom exercise event kinds (33401)
- Built exercise publication queue for offline-first functionality
- User profile integration
- Added profile fetching and caching
- Implemented profile-based permissions for content editing
- Fixed type definitions for NDK user profiles
- Robust workout state management
- Fixed favorites persistence in SQLite
- Added template-based workout initialization
- Implemented workout tracking with real-time updates
## Fixed
- TypeScript errors across multiple components:
- Resolved NDK-related type errors in ExerciseSheet component
- Fixed FavoritesService reference errors in workoutStore
- Corrected null/undefined handling in NDKEvent initialization
- Fixed profile type compatibility in useProfile hook
- Added proper type definitions for NDK UserProfile
- Dependency errors in PublicationQueue and DevSeeder services
- Source and authorization checks for exercise editing permissions
- Component interoperability with NDK mobile
## Improved
- Refactored code for better type safety
- Enhanced error handling with proper type checking
- Improved Nostr event creation workflow with NDK
- Streamlined user authentication process
- Enhanced development environment with better type checking
## [Unreleased]
### Added
- Comprehensive exercise management features
- Added exercise editing functionality
- Implemented exercise forking for Nostr exercises
- Created local-first editing with offline support
- Added publication queue for deferred Nostr publishing
- Built robust exercise update workflow
- Implemented source-aware editing permissions
- Connectivity service for network state management
- Added real-time connectivity monitoring
- Implemented persistence for offline state
- Built automatic retry system for failed requests
- Created hook-based connectivity API for components
- Extended database schema for publication queuing
- Added publication_queue table
- Implemented attempt tracking and rate limiting
- Added app_status table for system-wide states
- Successful Nostr protocol integration
- Implemented NDK-mobile for React Native compatibility
- Added secure key management with Expo SecureStore
@ -49,6 +71,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added workout persistence and recovery
- Built automatic timer management with background support
- Developed minimization and maximization functionality
- Zustand workout store for state management
- Created comprehensive workout state store with Zustand
- Implemented selectors for efficient state access
- Added workout persistence and recovery
- Built automatic timer management with background support
- Developed minimization and maximization functionality
- Workout tracking implementation with real-time tracking
- Added workout timer with proper background handling
- Implemented rest timer functionality
@ -95,24 +123,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved workout history visualization
### Changed
- Enhanced exercise detail viewer
- Replaced bottom sheet with full-screen modal
- Added tabbed interface for information organization
- Implemented edit capability with ownership detection
- Added fork functionality for Nostr exercises from other users
- Improved progress visualization with charts
- Redesigned exercise editor
- Created multi-purpose editor for create/edit/fork workflows
- Added context-aware UI based on exercise source
- Implemented specialized buttons based on workflow type
- Added better form validation and feedback
- Improved keyboard handling across platforms
- Improved workflow architecture for model context protocol
- Implemented offline-first editing paradigm
- Added cryptographic signing before submission
- Built local caching with deferred publishing
- Created connectivity-aware operation queueing
- Added proper error recovery and retry mechanisms
- Improved workout screen navigation consistency
- Standardized screen transitions and gestures
- Added back buttons for clearer navigation
@ -156,12 +166,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Enhanced visual separation between template metadata and content
### Fixed
- Exercise update functionality using delete-recreate pattern
- Exercise data type handling in forking operation
- TypeScript errors in exercise component interfaces
- Nostr event queuing and retry mechanism
- Exercise ownership detection for edit vs fork workflows
- Connectivity monitoring edge cases
- Workout navigation gesture handling issues
- Workout timer inconsistency during app background state
- Exercise deletion functionality
@ -178,22 +182,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Content rendering issues in bottom sheet components
### Technical Details
1. Exercise Management:
- Implemented edit/fork/create workflows with unified interface
- Built local-first editing pattern with offline support
- Added publication queue for deferred Nostr submissions
- Created robust update mechanism in useExercises hook
- Implemented source-aware editing permissions
- Added ownership detection for exercise operations
2. Connectivity Management:
- Implemented singleton ConnectivityService for app-wide monitoring
- Added NetInfo integration for real-time status detection
- Built React hook for component-level connectivity awareness
- Created database persistence for connectivity state
- Implemented event-based notification system
3. Nostr Integration:
1. Nostr Integration:
- Implemented @nostr-dev-kit/ndk-mobile package for React Native compatibility
- Created dedicated NDK store using Zustand for state management
- Built secure key storage and retrieval using Expo SecureStore
@ -201,65 +190,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added relay connection management with status tracking
- Developed proper error handling for network operations
4. Cryptographic Implementation:
2. Cryptographic Implementation:
- Integrated react-native-get-random-values for crypto API polyfill
- Implemented NDKMobilePrivateKeySigner for key operations
- Added proper key format handling (hex, nsec)
- Created secure key generation functionality
- Built robust error handling for cryptographic operations
5. Programs Testing Component:
3. Programs Testing Component:
- Developed dual-purpose interface for Database and Nostr testing
- Implemented login system with key generation and secure storage
- Built event creation interface with multiple event kinds
- Added event querying and display functionality
- Created detailed event inspection with tag visualization
- Added relay status monitoring
6. Database Schema Enforcement:
4. Database Schema Enforcement:
- Added CHECK constraints for equipment types
- Added CHECK constraints for exercise types
- Added CHECK constraints for categories
- Proper handling of foreign key constraints
7. Input Validation:
5. Input Validation:
- Equipment options: bodyweight, barbell, dumbbell, kettlebell, machine, cable, other
- Exercise types: strength, cardio, bodyweight
- Categories: Push, Pull, Legs, Core
- Difficulty levels: beginner, intermediate, advanced
- Movement patterns: push, pull, squat, hinge, carry, rotation
8. Error Handling:
6. Error Handling:
- Added SQLite error type definitions
- Improved error propagation in LibraryService
- Added transaction rollback on constraint violations
9. Database Services:
7. Database Services:
- Added EventCache service for Nostr events
- Improved ExerciseService with transaction awareness
- Added DevSeederService for development data
- Enhanced error handling and logging
10. Workout State Management with Zustand:
- Implemented selector pattern for performance optimization
- Added module-level timer references for background operation
- Created workout persistence with auto-save functionality
- Developed state recovery for crash protection
- Added support for future Nostr integration
- Implemented workout minimization for multi-tasking
11. Template Details UI Architecture:
- Implemented MaterialTopTabNavigator for content organization
- Created screen-specific components for each tab
- Developed conditional rendering based on template source
- Implemented context-aware action buttons
- Added proper navigation state handling
8. Workout State Management with Zustand:
- Implemented selector pattern for performance optimization
- Added module-level timer references for background operation
- Created workout persistence with auto-save functionality
- Developed state recovery for crash protection
- Added support for future Nostr integration
- Implemented workout minimization for multi-tasking
9. Template Details UI Architecture:
- Implemented MaterialTopTabNavigator for content organization
- Created screen-specific components for each tab
- Developed conditional rendering based on template source
- Implemented context-aware action buttons
- Added proper navigation state handling
### Migration Notes
- Exercise editing now follows an offline-first approach with Nostr awareness
- ExerciseSheet component replaces separate create/edit components
- Exercise updates require proper source and metadata handling
- Publication queue provides automatic retry for Nostr events
- Exercise creation now enforces schema constraints
- Input validation prevents invalid data entry
- Enhanced error messages provide better debugging information

View File

@ -1,6 +1,6 @@
// app/(tabs)/library/programs.tsx
import React, { useState, useEffect } from 'react';
import { View, ScrollView, TextInput, ActivityIndicator, Platform, TouchableOpacity, Modal } from 'react-native';
import { View, ScrollView, TextInput, ActivityIndicator, Platform, TouchableOpacity } from 'react-native';
import { Text } from '@/components/ui/text';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@ -8,7 +8,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import NostrLoginSheet from '@/components/sheets/NostrLoginSheet';
import {
AlertCircle, CheckCircle2, Database, RefreshCcw, Trash2,
Code, Search, ListFilter, Wifi, Zap, FileJson, X, Info
Code, Search, ListFilter, Wifi, Zap, FileJson
} from 'lucide-react-native';
import { useSQLiteContext } from 'expo-sqlite';
import { useNDK, useNDKAuth, useNDKCurrentUser } from '@/lib/hooks/useNDK';
@ -50,6 +50,7 @@ const initialFilters: FilterOptions = {
tags: [],
source: []
};
export default function ProgramsScreen() {
const db = useSQLiteContext();
@ -77,7 +78,7 @@ export default function ProgramsScreen() {
const [statusMessage, setStatusMessage] = useState('');
const [events, setEvents] = useState<DisplayEvent[]>([]);
const [loading, setLoading] = useState(false);
const [eventKind, setEventKind] = useState(NostrEventKind.EXERCISE);
const [eventKind, setEventKind] = useState(NostrEventKind.TEXT);
const [eventContent, setEventContent] = useState('');
const [isLoginSheetOpen, setIsLoginSheetOpen] = useState(false);
@ -87,7 +88,8 @@ export default function ProgramsScreen() {
const { login, logout, generateKeys } = useNDKAuth();
// Tab state
const [activeTab, setActiveTab] = useState('database');
const [activeTab, setActiveTab] = useState('nostr'); // Default to nostr tab for testing
useEffect(() => {
// Check database status
checkDatabase();
@ -261,6 +263,7 @@ export default function ProgramsScreen() {
setActiveFilters(totalFilters);
// Implement filtering logic for programs when available
};
// NOSTR FUNCTIONS
// Handle login dialog
@ -317,15 +320,6 @@ export default function ProgramsScreen() {
setEventContent('Hello from POWR App - Test Note');
}
} else if (eventKind === NostrEventKind.EXERCISE) {
// Your existing exercise event code
const uniqueId = `exercise-${timestamp}`;
tags.push(
['d', uniqueId],
['title', eventContent || 'Test Exercise'],
// Rest of your tags...
);
}
if (eventKind === NostrEventKind.EXERCISE) {
const uniqueId = `exercise-${timestamp}`;
tags.push(
['d', uniqueId],
@ -366,7 +360,8 @@ export default function ProgramsScreen() {
}
// Use the NDK store's publishEvent function
const event = await useNDKStore.getState().publishEvent(eventKind, eventContent, tags);
const content = eventContent || `Test ${eventKind === NostrEventKind.TEXT ? 'note' : 'event'} from POWR App`;
const event = await useNDKStore.getState().publishEvent(eventKind, content, tags);
if (event) {
// Add the published event to our display list
@ -410,7 +405,7 @@ export default function ProgramsScreen() {
// Create a filter for the specific kind
const filter = { kinds: [eventKind as number], limit: 20 };
// Use the NDK store's fetchEventsByFilter function
// Get events using NDK
const fetchedEvents = await useNDKStore.getState().fetchEventsByFilter(filter);
const displayEvents: DisplayEvent[] = [];
@ -437,6 +432,7 @@ export default function ProgramsScreen() {
setLoading(false);
}
};
return (
<View className="flex-1 bg-background">
{/* Search bar with filter button */}
@ -497,6 +493,7 @@ export default function ProgramsScreen() {
<Text className={activeTab === 'nostr' ? 'text-white' : 'text-foreground'}>Nostr</Text>
</TouchableOpacity>
</View>
{/* Tab Content */}
{activeTab === 'database' && (
<ScrollView className="flex-1 p-4">
@ -690,14 +687,14 @@ export default function ProgramsScreen() {
<Text className="font-medium mt-3">Active Relay:</Text>
<Text className="text-sm text-muted-foreground">wss://powr.duckdns.org</Text>
<Text className="text-xs text-muted-foreground mt-1">
Note: To publish to additional relays, uncomment them in stores/ndk.ts
Note: To publish to additional relays, update them in stores/ndk.ts
</Text>
</View>
)}
</CardContent>
</Card>
{/* Login Modal */}
{/* NostrLoginSheet component */}
<NostrLoginSheet
open={isLoginSheetOpen}
onClose={handleCloseLogin}
@ -839,6 +836,7 @@ export default function ProgramsScreen() {
)}
</CardContent>
</Card>
{/* Event JSON Viewer */}
<Card className="mb-4">
<CardHeader>
@ -878,12 +876,12 @@ export default function ProgramsScreen() {
<Text className="font-medium mb-2">How to test Nostr integration:</Text>
<View className="space-y-2">
<Text>1. Click "Login with Nostr" to authenticate</Text>
<Text>2. On the login sheet, click "Generate New Keys" to create a new Nostr identity</Text>
<Text>2. On the login sheet, click "Generate Key" to create a new Nostr identity</Text>
<Text>3. Login with the generated keys</Text>
<Text>4. Select an event kind (Exercise, Template, or Workout)</Text>
<Text>4. Select an event kind (Text Note, Exercise, Template, or Workout)</Text>
<Text>5. Enter optional content and click "Publish"</Text>
<Text>6. Use "Query Events" to fetch existing events of the selected kind</Text>
<Text className="mt-2 text-muted-foreground">Using NDK for Nostr integration provides a more reliable experience than direct WebSocket connections.</Text>
<Text className="mt-2 text-muted-foreground">Using NDK Mobile for Nostr integration provides a more reliable experience with proper cryptographic operations.</Text>
</View>
</CardContent>
</Card>

View File

@ -1,6 +1,5 @@
// app/_layout.tsx
import 'expo-dev-client';
import '../lib/crypto-polyfill'; // Import crypto polyfill first
import '@/global.css';
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
import { Stack } from 'expo-router';

View File

@ -3,22 +3,27 @@ import { View, ActivityIndicator, Text } from 'react-native';
import { SQLiteProvider, openDatabaseSync, SQLiteDatabase } from 'expo-sqlite';
import { schema } from '@/lib/db/schema';
import { ExerciseService } from '@/lib/db/services/ExerciseService';
import { EventCache } from '@/lib/db/services/EventCache';
import { DevSeederService } from '@/lib/db/services/DevSeederService';
import { PublicationQueueService } from '@/lib/db/services/PublicationQueueService';
import { FavoritesService } from '@/lib/db/services/FavoritesService';
import { logDatabaseInfo } from '@/lib/db/debug';
import { useNDKStore } from '@/lib/stores/ndk';
// Create context for services
interface DatabaseServicesContextValue {
exerciseService: ExerciseService | null;
eventCache: EventCache | null;
devSeeder: DevSeederService | null;
// Remove NostrService since we're using the hooks-based approach now
publicationQueue: PublicationQueueService | null;
favoritesService: FavoritesService | null;
db: SQLiteDatabase | null;
}
const DatabaseServicesContext = React.createContext<DatabaseServicesContextValue>({
exerciseService: null,
eventCache: null,
devSeeder: null,
publicationQueue: null,
favoritesService: null,
db: null,
});
interface DatabaseProviderProps {
@ -30,9 +35,22 @@ export function DatabaseProvider({ children }: DatabaseProviderProps) {
const [error, setError] = React.useState<string | null>(null);
const [services, setServices] = React.useState<DatabaseServicesContextValue>({
exerciseService: null,
eventCache: null,
devSeeder: null,
publicationQueue: null,
favoritesService: null,
db: null,
});
// Get NDK from store to provide to services
const ndk = useNDKStore(state => state.ndk);
// Effect to set NDK on services when it becomes available
React.useEffect(() => {
if (ndk && services.devSeeder && services.publicationQueue) {
services.devSeeder.setNDK(ndk);
services.publicationQueue.setNDK(ndk);
}
}, [ndk, services]);
React.useEffect(() => {
async function initDatabase() {
@ -45,15 +63,27 @@ export function DatabaseProvider({ children }: DatabaseProviderProps) {
// Initialize services
console.log('[DB] Initializing services...');
const eventCache = new EventCache(db);
const exerciseService = new ExerciseService(db);
const devSeeder = new DevSeederService(db, exerciseService, eventCache);
const devSeeder = new DevSeederService(db, exerciseService);
const publicationQueue = new PublicationQueueService(db);
const favoritesService = new FavoritesService(db);
// Initialize the favorites service
await favoritesService.initialize();
// Initialize NDK on services if available
if (ndk) {
devSeeder.setNDK(ndk);
publicationQueue.setNDK(ndk);
}
// Set services
setServices({
exerciseService,
eventCache,
devSeeder,
publicationQueue,
favoritesService,
db,
});
// Seed development database
@ -110,18 +140,34 @@ export function useExerciseService() {
return context.exerciseService;
}
export function useEventCache() {
const context = React.useContext(DatabaseServicesContext);
if (!context.eventCache) {
throw new Error('Event cache not initialized');
}
return context.eventCache;
}
export function useDevSeeder() {
const context = React.useContext(DatabaseServicesContext);
if (!context.devSeeder) {
throw new Error('Dev seeder not initialized');
}
return context.devSeeder;
}
export function usePublicationQueue() {
const context = React.useContext(DatabaseServicesContext);
if (!context.publicationQueue) {
throw new Error('Publication queue not initialized');
}
return context.publicationQueue;
}
export function useFavoritesService() {
const context = React.useContext(DatabaseServicesContext);
if (!context.favoritesService) {
throw new Error('Favorites service not initialized');
}
return context.favoritesService;
}
export function useDatabase() {
const context = React.useContext(DatabaseServicesContext);
if (!context.db) {
throw new Error('Database not initialized');
}
return context.db;
}

153
components/NDKTest.tsx Normal file
View File

@ -0,0 +1,153 @@
// components/NDKTest.tsx
import React, { useState, useEffect } from 'react';
import { View, ScrollView } from 'react-native';
import { Text } from '@/components/ui/text';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Wifi, AlertCircle, Check, RefreshCw, Zap } from 'lucide-react-native';
import { useNDK, useNDKAuth, useNDKCurrentUser, useNDKEvents } from '@/lib/hooks/useNDK';
import { useSubscribe } from '@/lib/hooks/useSubscribe';
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk-mobile';
import { NostrEventKind } from '@/types/nostr';
export default function NDKTest() {
const { ndk, isLoading: ndkLoading } = useNDK();
const { isAuthenticated } = useNDKCurrentUser();
const { publishEvent } = useNDKEvents();
const [testStatus, setTestStatus] = useState<string>('Ready');
const [testEvent, setTestEvent] = useState<NDKEvent | null>(null);
// Test subscription to see our own events
const { events: receivedEvents, isLoading: subLoading, resubscribe } =
useSubscribe(
testEvent ? [{ kinds: [1], ids: [testEvent.id] }] : false,
{ closeOnEose: false }
);
useEffect(() => {
if (receivedEvents.length > 0) {
setTestStatus('Event successfully published and received!');
}
}, [receivedEvents]);
const runPublishTest = async () => {
if (!ndk || !isAuthenticated) {
setTestStatus('Error: Not authenticated');
return;
}
try {
setTestStatus('Creating test event...');
// Create event using NDK Mobile
const event = new NDKEvent(ndk);
event.kind = NDKKind.Text;
event.content = `Testing NDK Mobile from POWR app! ${new Date().toISOString()}`;
// Sign and publish
await event.publish();
setTestEvent(event);
setTestStatus(`Event published with ID: ${event.id}`);
// Resubscribe to see if we can receive our own event
resubscribe();
} catch (error) {
console.error('Test error:', error);
setTestStatus(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
const runCustomEventTest = async () => {
if (!ndk || !isAuthenticated) {
setTestStatus('Error: Not authenticated');
return;
}
try {
setTestStatus('Creating custom exercise event...');
const result = await publishEvent(
NostrEventKind.EXERCISE,
"Test exercise description",
[
['d', `exercise-${Date.now()}`],
['title', 'Test Exercise'],
['type', 'strength'],
['category', 'Legs'],
['format', 'weight', 'reps'],
['format_units', 'kg', 'count'],
['equipment', 'barbell']
]
);
if (result) {
setTestStatus(`Custom event published with ID: ${result.id}`);
} else {
setTestStatus('Failed to publish custom event');
}
} catch (error) {
console.error('Custom event test error:', error);
setTestStatus(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
return (
<ScrollView className="flex-1 p-4">
<Card className="mb-4">
<CardHeader>
<CardTitle className="flex-row items-center gap-2">
<Wifi size={20} className="text-foreground" />
<Text className="text-lg font-semibold">NDK Mobile Test</Text>
</CardTitle>
</CardHeader>
<CardContent>
<View className="space-y-4">
<Text>Status: {ndkLoading ? 'Loading NDK...' : ndk ? 'NDK initialized' : 'Not initialized'}</Text>
<Text>Authenticated: {isAuthenticated ? 'Yes' : 'No'}</Text>
<View className="bg-muted p-3 rounded-md">
<View className="flex-row items-center mb-2">
<Zap size={16} className="mr-3 text-primary" />
<Text className="font-semibold">Test Status: {testStatus}</Text>
</View>
{testEvent && (
<Text className="text-xs text-muted-foreground mt-2">
Event ID: {testEvent.id}
</Text>
)}
{receivedEvents.length > 0 && (
<View className="mt-2 border-t border-border pt-2">
<View className="flex-row items-center">
<Check size={16} className="mr-2 text-green-500" />
<Text className="text-green-500">Event successfully verified</Text>
</View>
</View>
)}
</View>
<Button
onPress={runPublishTest}
disabled={!isAuthenticated || ndkLoading}
className="w-full"
>
<RefreshCw size={18} className="mr-2 text-primary-foreground" />
<Text className="text-primary-foreground">Test Basic Event</Text>
</Button>
<Button
onPress={runCustomEventTest}
disabled={!isAuthenticated || ndkLoading}
className="w-full"
>
<Zap size={18} className="mr-2 text-primary-foreground" />
<Text className="text-primary-foreground">Test Custom Event</Text>
</Button>
</View>
</CardContent>
</Card>
</ScrollView>
);
}

View File

@ -18,8 +18,9 @@ import {
ExerciseDisplay
} from '@/types/exercise';
import { StorageSource } from '@/types/shared';
import { NDKEvent } from '@nostr-dev-kit/ndk-mobile';
import { useNDKStore } from '@/lib/stores/ndk';
import { useEventCache } from '@/components/DatabaseProvider';
import { useExerciseService, usePublicationQueue } from '@/components/DatabaseProvider';
interface ExerciseSheetProps {
isOpen: boolean;
@ -67,7 +68,7 @@ export function ExerciseSheet({ isOpen, onClose, onSubmit, exerciseToEdit, mode:
const { isDarkColorScheme } = useColorScheme();
const [formData, setFormData] = useState(DEFAULT_FORM_DATA);
const ndkStore = useNDKStore();
const eventCache = useEventCache();
const publicationQueue = usePublicationQueue();
// Determine if we're in edit, create, or fork mode
const hasExercise = !!exerciseToEdit;
@ -170,11 +171,15 @@ export function ExerciseSheet({ isOpen, onClose, onSubmit, exerciseToEdit, mode:
}
// Create and attempt to publish the event
const event = await ndkStore.createEvent(33401, exercise.description || '', nostrTags);
const event = new NDKEvent(ndkStore.ndk || undefined);
event.kind = 33401; // Or whatever kind you need
event.content = exercise.description || '';
event.tags = nostrTags;
await event.sign();
if (event) {
// Queue for publication (this will publish immediately if online)
await ndkStore.queueEventForPublishing(event);
await publicationQueue.queueEvent(event);
// If this is a new exercise, add nostr to sources
if (!exerciseToEdit) {

View File

@ -1,91 +0,0 @@
// lib/crypto-polyfill.ts
import 'react-native-get-random-values';
import * as Crypto from 'expo-crypto';
// Set up a more reliable polyfill
export function setupCryptoPolyfill() {
console.log('Setting up crypto polyfill...');
// Instead of using Object.defineProperty, let's use a different approach
try {
// First check if crypto exists and has getRandomValues
if (typeof global.crypto === 'undefined') {
(global as any).crypto = {};
}
// Only define getRandomValues if it doesn't exist or isn't working
if (!global.crypto.getRandomValues) {
console.log('Defining getRandomValues implementation');
(global.crypto as any).getRandomValues = function(array: Uint8Array) {
console.log('Custom getRandomValues called');
try {
return Crypto.getRandomBytes(array.length);
} catch (e) {
console.error('Error in getRandomValues:', e);
throw e;
}
};
}
// Test if it works
const testArray = new Uint8Array(8);
try {
const result = global.crypto.getRandomValues(testArray);
console.log('Crypto polyfill test result:', !!result);
return true;
} catch (testError) {
console.error('Crypto test failed:', testError);
return false;
}
} catch (error) {
console.error('Error setting up crypto polyfill:', error);
return false;
}
}
// Also expose a monkey-patching function for the specific libraries
export function monkeyPatchNostrLibraries() {
try {
console.log('Attempting to monkey-patch nostr libraries...');
// Direct monkey patching of the randomBytes function in nostr libraries
// This is an extreme approach but might be necessary
const customRandomBytes = function(length: number): Uint8Array {
console.log('Using custom randomBytes implementation');
return Crypto.getRandomBytes(length);
};
// Try to locate and patch the randomBytes function
try {
// Try to access the module using require
const nobleHashes = require('@noble/hashes/utils');
if (nobleHashes && nobleHashes.randomBytes) {
console.log('Patching @noble/hashes/utils randomBytes');
(nobleHashes as any).randomBytes = customRandomBytes;
}
} catch (e) {
console.log('Could not patch @noble/hashes/utils:', e);
}
// Also try to patch nostr-tools if available
try {
const nostrTools = require('nostr-tools');
if (nostrTools && nostrTools.crypto && nostrTools.crypto.randomBytes) {
console.log('Patching nostr-tools crypto.randomBytes');
(nostrTools.crypto as any).randomBytes = customRandomBytes;
}
} catch (e) {
console.log('Could not patch nostr-tools:', e);
}
return true;
} catch (error) {
console.error('Error in monkey patching:', error);
return false;
}
}
// Set up the polyfill
setupCryptoPolyfill();
// Try monkey patching as well
monkeyPatchNostrLibraries();

View File

@ -2,7 +2,7 @@
import { SQLiteDatabase } from 'expo-sqlite';
import { Platform } from 'react-native';
export const SCHEMA_VERSION = 5; // Updated to version 5 for publication queue table
export const SCHEMA_VERSION = 1;
class Schema {
private async getCurrentVersion(db: SQLiteDatabase): Promise<number> {
@ -34,6 +34,14 @@ class Schema {
console.log(`[Schema] Initializing database on ${Platform.OS}`);
const currentVersion = await this.getCurrentVersion(db);
// If we already have the current version, no need to recreate tables
if (currentVersion === SCHEMA_VERSION) {
console.log(`[Schema] Database already at version ${SCHEMA_VERSION}`);
return;
}
console.log(`[Schema] Creating tables for version ${SCHEMA_VERSION}`);
// Schema version tracking
await db.execAsync(`
CREATE TABLE IF NOT EXISTS schema_version (
@ -42,220 +50,220 @@ class Schema {
);
`);
if (currentVersion < 1) {
console.log('[Schema] Performing fresh install');
// Drop existing tables if they exist
await db.execAsync(`DROP TABLE IF EXISTS exercise_tags`);
await db.execAsync(`DROP TABLE IF EXISTS exercises`);
await db.execAsync(`DROP TABLE IF EXISTS event_tags`);
await db.execAsync(`DROP TABLE IF EXISTS nostr_events`);
// Create base tables
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'
);
`);
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);
`);
await db.runAsync(
'INSERT INTO schema_version (version, updated_at) VALUES (?, ?)',
[1, Date.now()]
);
console.log('[Schema] Base tables created successfully');
}
// Update to version 2 if needed - Nostr support
if (currentVersion < 2) {
console.log('[Schema] Upgrading to version 2');
await db.execAsync(`
CREATE TABLE IF NOT EXISTS 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
);
`);
await db.execAsync(`
CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS idx_event_tags ON event_tags(name, value);
`);
// Add Nostr reference to exercises
try {
await db.execAsync(`ALTER TABLE exercises ADD COLUMN nostr_event_id TEXT REFERENCES nostr_events(id)`);
} catch (e) {
console.log('[Schema] Note: nostr_event_id column may already exist');
}
await db.runAsync(
'INSERT INTO schema_version (version, updated_at) VALUES (?, ?)',
[2, Date.now()]
);
console.log('[Schema] Version 2 upgrade completed');
}
// Update to version 3 if needed - Event Cache
if (currentVersion < 3) {
console.log('[Schema] Upgrading to version 3');
// Create cache metadata table
await db.execAsync(`
CREATE TABLE IF NOT EXISTS cache_metadata (
content_id TEXT PRIMARY KEY,
content_type TEXT NOT NULL,
last_accessed INTEGER NOT NULL,
access_count INTEGER NOT NULL DEFAULT 0,
cache_priority INTEGER NOT NULL DEFAULT 0
);
`);
// Create media cache table
await db.execAsync(`
CREATE TABLE IF NOT EXISTS exercise_media (
exercise_id TEXT NOT NULL,
media_type TEXT NOT NULL,
content BLOB NOT NULL,
thumbnail BLOB,
created_at INTEGER NOT NULL,
FOREIGN KEY(exercise_id) REFERENCES exercises(id) ON DELETE CASCADE
);
`);
await db.runAsync(
'INSERT INTO schema_version (version, updated_at) VALUES (?, ?)',
[3, Date.now()]
);
console.log('[Schema] Version 3 upgrade completed');
}
// Update to version 4 if needed - User Profiles
if (currentVersion < 4) {
console.log('[Schema] Upgrading to version 4');
// Create user profiles table
await db.execAsync(`
CREATE TABLE IF NOT EXISTS user_profiles (
pubkey TEXT PRIMARY KEY,
name TEXT,
display_name TEXT,
about TEXT,
website TEXT,
picture TEXT,
nip05 TEXT,
lud16 TEXT,
last_updated INTEGER
);
`);
// Create index for faster lookup
await db.execAsync(`
CREATE INDEX IF NOT EXISTS idx_user_profiles_last_updated
ON user_profiles(last_updated DESC);
`);
// Create user relays table for storing preferred relays
await db.execAsync(`
CREATE TABLE IF NOT EXISTS user_relays (
pubkey TEXT NOT NULL,
relay_url TEXT NOT NULL,
read BOOLEAN NOT NULL DEFAULT 1,
write BOOLEAN NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL,
PRIMARY KEY (pubkey, relay_url),
FOREIGN KEY(pubkey) REFERENCES user_profiles(pubkey) ON DELETE CASCADE
);
`);
await db.runAsync(
'INSERT INTO schema_version (version, updated_at) VALUES (?, ?)',
[4, Date.now()]
);
console.log('[Schema] Version 4 upgrade completed');
}
// Update to version 5 if needed - Publication Queue
if (currentVersion < 5) {
console.log('[Schema] Upgrading to version 5');
// Create publication queue table
await db.execAsync(`
CREATE TABLE IF NOT EXISTS publication_queue (
event_id TEXT PRIMARY KEY,
attempts INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
last_attempt INTEGER,
payload TEXT NOT NULL,
FOREIGN KEY(event_id) REFERENCES nostr_events(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_publication_queue_created
ON publication_queue(created_at ASC);
`);
// Create app status table for tracking connectivity
await db.execAsync(`
CREATE TABLE IF NOT EXISTS app_status (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
`);
await db.runAsync(
'INSERT INTO schema_version (version, updated_at) VALUES (?, ?)',
[5, Date.now()]
);
console.log('[Schema] Version 5 upgrade completed');
}
// Verify final schema
const tables = await db.getAllAsync<{ name: string }>(
"SELECT name FROM sqlite_master WHERE type='table'"
);
console.log('[Schema] Final tables:', tables.map(t => t.name).join(', '));
console.log(`[Schema] Database initialized at version ${await this.getCurrentVersion(db)}`);
// Drop all existing tables (except schema_version)
await this.dropAllTables(db);
// Create all tables in their latest form
await this.createAllTables(db);
// Update schema version
await this.updateSchemaVersion(db);
console.log(`[Schema] Database initialized at version ${SCHEMA_VERSION}`);
} catch (error) {
console.error('[Schema] Error creating tables:', error);
throw error;
}
}
private async dropAllTables(db: SQLiteDatabase): Promise<void> {
// Get list of all tables excluding schema_version
const tables = await db.getAllAsync<{ name: string }>(
"SELECT name FROM sqlite_master WHERE type='table' AND name != 'schema_version'"
);
// Drop each table
for (const { name } of tables) {
await db.execAsync(`DROP TABLE IF EXISTS ${name}`);
console.log(`[Schema] Dropped table: ${name}`);
}
}
private async createAllTables(db: SQLiteDatabase): Promise<void> {
// Create 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
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
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
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 cache metadata table
await db.execAsync(`
CREATE TABLE cache_metadata (
content_id TEXT PRIMARY KEY,
content_type TEXT NOT NULL,
last_accessed INTEGER NOT NULL,
access_count INTEGER NOT NULL DEFAULT 0,
cache_priority INTEGER NOT NULL DEFAULT 0
);
`);
// Create media cache table
await db.execAsync(`
CREATE TABLE exercise_media (
exercise_id TEXT NOT NULL,
media_type TEXT NOT NULL,
content BLOB NOT NULL,
thumbnail BLOB,
created_at INTEGER NOT NULL,
FOREIGN KEY(exercise_id) REFERENCES exercises(id) ON DELETE CASCADE
);
`);
// Create user profiles table
await db.execAsync(`
CREATE TABLE user_profiles (
pubkey TEXT PRIMARY KEY,
name TEXT,
display_name TEXT,
about TEXT,
website TEXT,
picture TEXT,
nip05 TEXT,
lud16 TEXT,
last_updated INTEGER
);
CREATE INDEX idx_user_profiles_last_updated ON user_profiles(last_updated DESC);
`);
// Create user relays table
await db.execAsync(`
CREATE TABLE user_relays (
pubkey TEXT NOT NULL,
relay_url TEXT NOT NULL,
read BOOLEAN NOT NULL DEFAULT 1,
write BOOLEAN NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL,
PRIMARY KEY (pubkey, relay_url),
FOREIGN KEY(pubkey) REFERENCES user_profiles(pubkey) ON DELETE CASCADE
);
`);
// Create publication queue table
await db.execAsync(`
CREATE TABLE publication_queue (
event_id TEXT PRIMARY KEY,
attempts INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
last_attempt INTEGER,
payload TEXT NOT NULL,
FOREIGN KEY(event_id) REFERENCES nostr_events(id) ON DELETE CASCADE
);
CREATE INDEX idx_publication_queue_created ON publication_queue(created_at ASC);
`);
// Create app status table
await db.execAsync(`
CREATE TABLE app_status (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
`);
// Create NDK cache table
await db.execAsync(`
CREATE TABLE ndk_cache (
id TEXT PRIMARY KEY,
event TEXT NOT NULL,
created_at INTEGER NOT NULL,
kind INTEGER NOT NULL
);
CREATE INDEX idx_ndk_cache_kind ON ndk_cache(kind);
CREATE INDEX idx_ndk_cache_created ON ndk_cache(created_at);
`);
// Create favorites table
await db.execAsync(`
CREATE TABLE 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 idx_favorites_content_type ON favorites(content_type);
CREATE INDEX idx_favorites_content_id ON favorites(content_id);
`);
}
private async updateSchemaVersion(db: SQLiteDatabase): Promise<void> {
// Delete any existing schema version records
await db.runAsync('DELETE FROM schema_version');
// Insert the current version
await db.runAsync(
'INSERT INTO schema_version (version, updated_at) VALUES (?, ?)',
[SCHEMA_VERSION, Date.now()]
);
}
async resetDatabase(db: SQLiteDatabase): Promise<void> {
if (!__DEV__) return; // Only allow in development
try {
console.log('[Schema] Resetting database...');
await this.dropAllTables(db);
await this.createAllTables(db);
await this.updateSchemaVersion(db);
console.log('[Schema] Database reset complete');
} catch (error) {
console.error('[Schema] Error resetting database:', error);
throw error;
}
}
}
export const schema = new Schema();

View File

@ -1,23 +1,25 @@
// lib/db/services/DevSeederService.ts
import { SQLiteDatabase } from 'expo-sqlite';
import { ExerciseService } from './ExerciseService';
import { EventCache } from './EventCache';
import { logDatabaseInfo } from '../debug';
import { mockExerciseEvents, convertNostrToExercise } from '../../mocks/exercises';
import NDK, { NDKEvent } from '@nostr-dev-kit/ndk-mobile';
export class DevSeederService {
private db: SQLiteDatabase;
private exerciseService: ExerciseService;
private eventCache: EventCache;
private ndk: NDK | null = null;
constructor(
db: SQLiteDatabase,
exerciseService: ExerciseService,
eventCache: EventCache
exerciseService: ExerciseService
) {
this.db = db;
this.exerciseService = exerciseService;
this.eventCache = eventCache;
}
setNDK(ndk: NDK) {
this.ndk = ndk;
}
async seedDatabase() {
@ -42,10 +44,36 @@ export class DevSeederService {
console.log('Seeding mock exercises...');
// Process all events within the same transaction
for (const event of mockExerciseEvents) {
// Pass true to indicate we're in a transaction
await this.eventCache.setEvent(event, true);
const exercise = convertNostrToExercise(event);
for (const eventData of mockExerciseEvents) {
if (this.ndk) {
// If NDK is available, use it to cache the event
const event = new NDKEvent(this.ndk);
Object.assign(event, eventData);
// Cache the event in NDK
if (this.ndk) {
const ndkEvent = new NDKEvent(this.ndk);
// Copy event properties
ndkEvent.kind = eventData.kind;
ndkEvent.content = eventData.content;
ndkEvent.created_at = eventData.created_at;
ndkEvent.tags = eventData.tags;
// If we have mock signatures, use them
if (eventData.sig) {
ndkEvent.sig = eventData.sig;
ndkEvent.id = eventData.id || '';
ndkEvent.pubkey = eventData.pubkey || '';
} else if (this.ndk.signer) {
// Otherwise sign if possible
await ndkEvent.sign();
}
}
}
// Create exercise from the mock data regardless of NDK availability
const exercise = convertNostrToExercise(eventData);
await this.exerciseService.createExercise(exercise, true);
}
@ -73,7 +101,8 @@ export class DevSeederService {
'exercise_tags',
'nostr_events',
'event_tags',
'cache_metadata'
'cache_metadata',
'ndk_cache' // Add the NDK Mobile cache table
];
for (const table of tables) {

View File

@ -1,115 +0,0 @@
// lib/db/services/EventCache.ts
import { SQLiteDatabase } from 'expo-sqlite';
import { NostrEvent } from '@/types/nostr';
export class EventCache {
private db: SQLiteDatabase;
private writeBuffer: { query: string; params: any[] }[] = [];
constructor(db: SQLiteDatabase) {
this.db = db;
}
async setEvent(event: NostrEvent, inTransaction: boolean = false): Promise<void> {
try {
// Store queries to execute
const queries = [
{
query: `INSERT OR REPLACE INTO nostr_events
(id, pubkey, kind, created_at, content, sig, raw_event, received_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
params: [
event.id || '', // Convert undefined to empty string
event.pubkey || '',
event.kind,
event.created_at,
event.content,
event.sig || '',
JSON.stringify(event),
Date.now()
]
},
// Add metadata query
{
query: `INSERT OR REPLACE INTO cache_metadata
(content_id, content_type, last_accessed, access_count)
VALUES (?, ?, ?, 1)
ON CONFLICT(content_id) DO UPDATE SET
last_accessed = ?,
access_count = access_count + 1`,
params: [
event.id || '',
'event',
Date.now(),
Date.now()
]
}
];
// Add tag queries
event.tags.forEach((tag, index) => {
queries.push({
query: `INSERT OR REPLACE INTO event_tags
(event_id, name, value, index_num)
VALUES (?, ?, ?, ?)`,
params: [
event.id || '',
tag[0] || '',
tag[1] || '',
index
]
});
});
// If we're already in a transaction, just execute the queries
if (inTransaction) {
for (const { query, params } of queries) {
await this.db.runAsync(query, params);
}
} else {
// Otherwise, wrap in our own transaction
await this.db.withTransactionAsync(async () => {
for (const { query, params } of queries) {
await this.db.runAsync(query, params);
}
});
}
} catch (error) {
console.error('Error caching event:', error);
throw error;
}
}
async getEvent(id: string): Promise<NostrEvent | null> {
try {
const event = await this.db.getFirstAsync<any>(
`SELECT * FROM nostr_events WHERE id = ?`,
[id]
);
if (!event) return null;
// Get tags
const tags = await this.db.getAllAsync<{ name: string; value: string }>(
`SELECT name, value FROM event_tags WHERE event_id = ? ORDER BY index_num`,
[id]
);
// Update access metadata
await this.db.runAsync(
`UPDATE cache_metadata
SET last_accessed = ?, access_count = access_count + 1
WHERE content_id = ?`,
[Date.now(), id]
);
return {
...event,
tags: tags.map(tag => [tag.name, tag.value])
};
} catch (error) {
console.error('Error getting event from cache:', error);
return null;
}
}
}

View File

@ -0,0 +1,142 @@
// lib/db/services/FavoritesService.ts
import { SQLiteDatabase } from 'expo-sqlite';
import { generateId } from '@/utils/ids';
type ContentType = 'template' | 'exercise' | 'workout';
export class FavoritesService {
private db: SQLiteDatabase;
constructor(db: SQLiteDatabase) {
this.db = db;
}
async initialize(): Promise<void> {
try {
// Ensure the table exists with the right schema
await this.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);
`);
} catch (error) {
console.error('[FavoritesService] Error initializing favorites table:', error);
throw error;
}
}
async addFavorite<T>(contentType: ContentType, contentId: string, content: T, pubkey?: string): Promise<string> {
try {
const id = generateId('local');
const now = Date.now();
await this.db.runAsync(
`INSERT OR REPLACE INTO favorites (id, content_type, content_id, content, pubkey, created_at)
VALUES (?, ?, ?, ?, ?, ?)`,
[
id,
contentType,
contentId,
JSON.stringify(content),
pubkey || null,
now
]
);
return id;
} catch (error) {
console.error('[FavoritesService] Error adding favorite:', error);
throw error;
}
}
async removeFavorite(contentType: ContentType, contentId: string): Promise<void> {
try {
await this.db.runAsync(
`DELETE FROM favorites WHERE content_type = ? AND content_id = ?`,
[contentType, contentId]
);
} catch (error) {
console.error('[FavoritesService] Error removing favorite:', error);
throw error;
}
}
async isFavorite(contentType: ContentType, contentId: string): Promise<boolean> {
try {
const result = await this.db.getFirstAsync<{ count: number }>(
`SELECT COUNT(*) as count FROM favorites WHERE content_type = ? AND content_id = ?`,
[contentType, contentId]
);
return (result?.count || 0) > 0;
} catch (error) {
console.error('[FavoritesService] Error checking favorite status:', error);
return false;
}
}
async getFavoriteIds(contentType: ContentType): Promise<string[]> {
try {
const result = await this.db.getAllAsync<{ content_id: string }>(
`SELECT content_id FROM favorites WHERE content_type = ?`,
[contentType]
);
return result.map(item => item.content_id);
} catch (error) {
console.error('[FavoritesService] Error fetching favorite IDs:', error);
return [];
}
}
async getFavorites<T>(contentType: ContentType): Promise<Array<{id: string, content: T, addedAt: number}>> {
try {
const result = await this.db.getAllAsync<{
id: string,
content_id: string,
content: string,
created_at: number
}>(
`SELECT id, content_id, content, created_at FROM favorites WHERE content_type = ?`,
[contentType]
);
return result.map(item => ({
id: item.content_id,
content: JSON.parse(item.content) as T,
addedAt: item.created_at
}));
} catch (error) {
console.error('[FavoritesService] Error fetching favorites:', error);
return [];
}
}
async getContentById<T>(contentType: ContentType, contentId: string): Promise<T | null> {
try {
const result = await this.db.getFirstAsync<{
content: string
}>(
`SELECT content FROM favorites WHERE content_type = ? AND content_id = ?`,
[contentType, contentId]
);
if (result?.content) {
return JSON.parse(result.content) as T;
}
return null;
} catch (error) {
console.error('[FavoritesService] Error fetching content:', error);
return null;
}
}
}

View File

@ -1,15 +1,18 @@
// lib/db/services/PublicationQueueService.ts
import { SQLiteDatabase } from 'expo-sqlite';
import NDK, { NDKEvent } from '@nostr-dev-kit/ndk-mobile';
import { NostrEvent } from '@/types/nostr';
import { EventCache } from './EventCache';
export class PublicationQueueService {
private db: SQLiteDatabase;
private eventCache: EventCache;
private ndk: NDK | null = null;
constructor(db: SQLiteDatabase, eventCache: EventCache) {
constructor(db: SQLiteDatabase) {
this.db = db;
this.eventCache = eventCache;
}
setNDK(ndk: NDK) {
this.ndk = ndk;
}
/**
@ -17,27 +20,36 @@ export class PublicationQueueService {
* @param event The Nostr event to queue
* @returns Promise that resolves when the event is queued
*/
async queueEvent(event: NostrEvent): Promise<void> {
async queueEvent(event: NostrEvent | NDKEvent): Promise<void> {
try {
// First, ensure the event is cached
await this.eventCache.setEvent(event);
// Convert to the right format for storage
const eventId = event instanceof NDKEvent ? event.id : event.id || '';
const payload = event instanceof NDKEvent ?
JSON.stringify(event.rawEvent()) :
JSON.stringify(event);
// Then add to publication queue
const payload = JSON.stringify(event);
// Cache the event if NDK is available
if (this.ndk && event instanceof NDKEvent) {
// NDK handles caching internally during sign and publish
if (!event.sig) {
await event.sign();
}
}
// Add to publication queue
await this.db.runAsync(
`INSERT OR REPLACE INTO publication_queue
(event_id, attempts, created_at, payload)
VALUES (?, ?, ?, ?)`,
[
event.id || '', // Add default empty string if undefined
eventId,
0,
Date.now(),
JSON.stringify(event)
payload
]
);
console.log(`[Queue] Event ${event.id} queued for publishing`);
console.log(`[Queue] Event ${eventId} queued for publishing`);
} catch (error) {
console.error('[Queue] Error queueing event:', error);
throw error;
@ -82,6 +94,52 @@ export class PublicationQueueService {
}
}
/**
* Process pending events using NDK
* @returns Promise that resolves when processing is complete
*/
async processQueue(): Promise<void> {
if (!this.ndk) {
console.log('[Queue] NDK not available, skipping queue processing');
return;
}
try {
const pendingEvents = await this.getPendingEvents();
console.log(`[Queue] Processing ${pendingEvents.length} pending events`);
for (const item of pendingEvents) {
try {
// Update attempt count
await this.incrementAttempt(item.id);
// Create NDK event and publish
const event = new NDKEvent(this.ndk);
const rawEvent = item.payload;
// Copy properties from raw event
event.id = rawEvent.id || '';
event.pubkey = rawEvent.pubkey || '';
event.kind = rawEvent.kind || 0;
event.created_at = rawEvent.created_at || Math.floor(Date.now() / 1000);
event.content = rawEvent.content || '';
event.tags = rawEvent.tags || [];
event.sig = rawEvent.sig || '';
// Publish
await event.publish();
// Remove from queue on success
await this.removeEvent(item.id);
} catch (error) {
console.error(`[Queue] Failed to publish event ${item.id}:`, error);
}
}
} catch (error) {
console.error('[Queue] Error processing queue:', error);
}
}
/**
* Update the attempt count for an event
* @param eventId ID of the event

View File

@ -1,18 +1,15 @@
// lib/hooks/useNDK.ts
import { useEffect } from 'react';
import { useNDKStore } from '@/lib/stores/ndk';
import type { NDKUser } from '@nostr-dev-kit/ndk';
import type { NDKUser, NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk-mobile';
/**
* Hook to access NDK instance and initialization status
*/
// Core hook for NDK access
export function useNDK() {
const { ndk, isLoading, error, init, relayStatus } = useNDKStore(state => ({
const { ndk, isLoading, error, init } = useNDKStore(state => ({
ndk: state.ndk,
isLoading: state.isLoading,
error: state.error,
init: state.init,
relayStatus: state.relayStatus
init: state.init
}));
useEffect(() => {
@ -21,22 +18,11 @@ export function useNDK() {
}
}, [ndk, isLoading, init]);
return {
ndk,
isLoading,
error,
relayStatus
};
return { ndk, isLoading, error };
}
/**
* Hook to access current NDK user information
*/
export function useNDKCurrentUser(): {
currentUser: NDKUser | null;
isAuthenticated: boolean;
isLoading: boolean;
} {
// Hook for current user info
export function useNDKCurrentUser() {
const { currentUser, isAuthenticated, isLoading } = useNDKStore(state => ({
currentUser: state.currentUser,
isAuthenticated: state.isAuthenticated,
@ -50,16 +36,14 @@ export function useNDKCurrentUser(): {
};
}
/**
* Hook to access NDK authentication methods
*/
// Hook for authentication actions
export function useNDKAuth() {
const { login, logout, isAuthenticated, isLoading, generateKeys } = useNDKStore(state => ({
const { login, logout, generateKeys, isAuthenticated, isLoading } = useNDKStore(state => ({
login: state.login,
logout: state.logout,
generateKeys: state.generateKeys,
isAuthenticated: state.isAuthenticated,
isLoading: state.isLoading,
generateKeys: state.generateKeys
isLoading: state.isLoading
}));
return {
@ -71,9 +55,7 @@ export function useNDKAuth() {
};
}
/**
* Hook for direct access to Nostr event actions
*/
// New hook for event operations
export function useNDKEvents() {
const { publishEvent, fetchEventsByFilter } = useNDKStore(state => ({
publishEvent: state.publishEvent,

77
lib/hooks/useProfile.tsx Normal file
View File

@ -0,0 +1,77 @@
// lib/hooks/useProfile.ts
import { useState, useEffect } from 'react';
import { NDKUser, NDKUserProfile } from '@nostr-dev-kit/ndk-mobile';
import { useNDK } from './useNDK';
export function useProfile(pubkey: string | undefined) {
const { ndk } = useNDK();
const [profile, setProfile] = useState<NDKUserProfile | null>(null);
const [user, setUser] = useState<NDKUser | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
if (!ndk || !pubkey) {
setIsLoading(false);
return;
}
const fetchProfile = async () => {
try {
setIsLoading(true);
setError(null);
// Create NDK user
const ndkUser = ndk.getUser({ pubkey });
// Fetch profile
await ndkUser.fetchProfile();
// Normalize profile data, similar to your current implementation
if (ndkUser.profile) {
// Ensure image property exists (some clients use picture instead)
if (!ndkUser.profile.image && (ndkUser.profile as any).picture) {
ndkUser.profile.image = (ndkUser.profile as any).picture;
}
}
setUser(ndkUser);
setProfile(ndkUser.profile || null);
setIsLoading(false);
} catch (err) {
console.error('Error fetching profile:', err);
setError(err instanceof Error ? err : new Error('Failed to fetch profile'));
setIsLoading(false);
}
};
fetchProfile();
}, [ndk, pubkey]);
const refreshProfile = async () => {
if (!ndk || !pubkey) return;
try {
setIsLoading(true);
setError(null);
const ndkUser = ndk.getUser({ pubkey });
await ndkUser.fetchProfile();
setUser(ndkUser);
setProfile(ndkUser.profile || null);
} catch (err) {
setError(err instanceof Error ? err : new Error('Failed to refresh profile'));
} finally {
setIsLoading(false);
}
};
return {
profile,
user,
isLoading,
error,
refreshProfile
};
}

View File

@ -1,24 +1,15 @@
// lib/hooks/useSubscribe.ts
import { useEffect, useState, useRef } from 'react';
import { NDKFilter, NDKSubscription } from '@nostr-dev-kit/ndk';
import { NDKEvent, NDKUser } from '@nostr-dev-kit/ndk-mobile';
import { useState, useEffect, useCallback, useRef } from 'react';
import { NDKEvent, NDKFilter, NDKSubscription, NDKSubscriptionOptions } from '@nostr-dev-kit/ndk-mobile';
import { useNDK } from './useNDK';
interface UseSubscribeOptions {
interface UseSubscribeOptions extends Partial<NDKSubscriptionOptions> {
enabled?: boolean;
closeOnEose?: boolean;
deduplicate?: boolean;
}
/**
* Hook to subscribe to Nostr events
*
* @param filters The NDK filter or array of filters
* @param options Optional configuration options
* @returns Object containing events, loading state, and EOSE status
*/
export function useSubscribe(
filters: NDKFilter | NDKFilter[] | false,
filters: NDKFilter[] | false,
options: UseSubscribeOptions = {}
) {
const { ndk } = useNDK();
@ -31,40 +22,46 @@ export function useSubscribe(
const {
enabled = true,
closeOnEose = false,
deduplicate = true
deduplicate = true,
...subscriptionOptions
} = options;
useEffect(() => {
// Clean up previous subscription if exists
// Function to clear all events
const clearEvents = useCallback(() => {
setEvents([]);
}, []);
// Function to manually resubscribe
const resubscribe = useCallback(() => {
if (subscriptionRef.current) {
subscriptionRef.current.stop();
subscriptionRef.current = null;
}
// Reset state when filters change
setEvents([]);
setEose(false);
// Check prerequisites
setIsLoading(true);
}, []);
useEffect(() => {
if (!ndk || !filters || !enabled) {
setIsLoading(false);
return;
}
setIsLoading(true);
setEose(false);
try {
// Convert single filter to array if needed
const filterArray = Array.isArray(filters) ? filters : [filters];
// Create subscription with NDK Mobile
const subscription = ndk.subscribe(filters, {
closeOnEose,
...subscriptionOptions
});
// Create subscription
const subscription = ndk.subscribe(filterArray);
subscriptionRef.current = subscription;
// Handle incoming events
subscription.on('event', (event: NDKEvent) => {
setEvents(prev => {
// Deduplicate events if enabled
if (deduplicate && prev.some(e => e.id === event.id)) {
return prev;
}
@ -72,15 +69,9 @@ export function useSubscribe(
});
});
// Handle end of stored events
subscription.on('eose', () => {
setIsLoading(false);
setEose(true);
if (closeOnEose && subscriptionRef.current) {
subscriptionRef.current.stop();
subscriptionRef.current = null;
}
});
} catch (error) {
console.error('[useSubscribe] Error:', error);
@ -94,20 +85,14 @@ export function useSubscribe(
subscriptionRef.current = null;
}
};
}, [ndk, enabled, closeOnEose, deduplicate, JSON.stringify(filters)]);
}, [ndk, enabled, closeOnEose, JSON.stringify(filters), JSON.stringify(subscriptionOptions)]);
return {
events,
isLoading,
eose,
resubscribe: () => {
if (subscriptionRef.current) {
subscriptionRef.current.stop();
subscriptionRef.current = null;
}
setEvents([]);
setEose(false);
setIsLoading(true);
}
eose,
clearEvents,
resubscribe,
subscription: subscriptionRef.current
};
}

View File

@ -2,25 +2,21 @@
import 'react-native-get-random-values'; // This must be the first import
import NDK, { NDKCacheAdapterSqlite } from '@nostr-dev-kit/ndk-mobile';
import * as SecureStore from 'expo-secure-store';
import { NDKMobilePrivateKeySigner } from './mobile-signer';
// Constants for SecureStore
const PRIVATE_KEY_STORAGE_KEY = 'nostr_privkey';
// Default relays
// Use the same default relays you have in your current implementation
const DEFAULT_RELAYS = [
'wss://powr.duckdns.org',
'wss://relay.damus.io',
'wss://relay.nostr.band',
'wss://purplepag.es',
'wss://nos.lol'
];
export async function initializeNDK() {
console.log('Initializing NDK with mobile adapter...');
// Create a mobile-specific cache adapter with a valid maxSize
// The error shows maxSize must be greater than 0
const cacheAdapter = new NDKCacheAdapterSqlite('powr', 1000); // Use 1000 as maxSize
// Create a mobile-specific cache adapter
const cacheAdapter = new NDKCacheAdapterSqlite('powr', 1000);
// Initialize NDK with mobile-specific options
const ndk = new NDK({
@ -36,43 +32,5 @@ export async function initializeNDK() {
// Connect to relays
await ndk.connect();
// Set up relay status tracking
const relayStatus: Record<string, 'connected' | 'connecting' | 'disconnected' | 'error'> = {};
DEFAULT_RELAYS.forEach(url => {
relayStatus[url] = 'connecting';
const relay = ndk.pool.getRelay(url);
if (relay) {
relay.on('connect', () => {
console.log(`Connected to relay: ${url}`);
relayStatus[url] = 'connected';
});
relay.on('disconnect', () => {
console.log(`Disconnected from relay: ${url}`);
relayStatus[url] = 'disconnected';
});
}
});
// Check for saved private key
try {
const privateKey = await SecureStore.getItemAsync(PRIVATE_KEY_STORAGE_KEY);
if (privateKey) {
console.log('[NDK] Found saved private key, initializing signer');
// Create mobile-specific signer with private key
const signer = new NDKMobilePrivateKeySigner(privateKey);
ndk.signer = signer;
// Log success
console.log('[NDK] Signer initialized successfully');
}
} catch (error) {
console.error('[NDK] Error initializing with saved key:', error);
// Remove invalid key
await SecureStore.deleteItemAsync(PRIVATE_KEY_STORAGE_KEY);
}
return { ndk, relayStatus };
return { ndk };
}

View File

@ -1,76 +0,0 @@
// lib/mobile-signer.ts
import 'react-native-get-random-values'; // First import - most important!
import * as Crypto from 'expo-crypto';
import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk-mobile'; // Import from ndk-mobile
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
import * as nostrTools from 'nostr-tools';
/**
* A custom signer implementation for React Native
* Extends NDKPrivateKeySigner to handle different key formats
*/
export class NDKMobilePrivateKeySigner extends NDKPrivateKeySigner {
constructor(privateKey: string) {
// Handle different private key formats
let hexKey = privateKey;
// Convert nsec to hex if needed
if (privateKey.startsWith('nsec')) {
try {
const { type, data } = nostrTools.nip19.decode(privateKey);
if (type === 'nsec') {
// Handle the data as string (already in hex format)
if (typeof data === 'string') {
hexKey = data;
}
// Handle if it's a Uint8Array
else if (data instanceof Uint8Array) {
hexKey = bytesToHex(data);
}
} else {
throw new Error('Not an nsec key');
}
} catch (e) {
console.error('Error processing nsec key:', e);
throw new Error('Invalid private key format');
}
}
// Call the parent constructor with the hex key
super(hexKey);
}
}
/**
* Generate a new Nostr keypair
* Uses Expo's crypto functions directly instead of relying on polyfills
*/
export function generateKeyPair() {
try {
let privateKeyBytes;
// Try expo-crypto
privateKeyBytes = Crypto.getRandomBytes(32);
const privateKey = bytesToHex(privateKeyBytes);
// Get the public key from the private key using nostr-tools
const publicKey = nostrTools.getPublicKey(privateKeyBytes);
// Encode keys in bech32 format
// Fixed the parameter types for nsecEncode - it needs Uint8Array not string
const nsec = nostrTools.nip19.nsecEncode(privateKeyBytes);
const npub = nostrTools.nip19.npubEncode(publicKey);
// Make sure we return the object with all properties
return {
privateKey,
publicKey,
nsec,
npub
};
} catch (error) {
console.error('[MobileSigner] Error generating key pair:', error);
throw error; // Return the actual error for better debugging
}
}

View File

@ -1,25 +1,24 @@
// lib/stores/ndk.ts
// IMPORTANT: 'react-native-get-random-values' must be the first import to ensure
// proper crypto polyfill application before other libraries are loaded
import 'react-native-get-random-values';
import { Platform } from 'react-native';
import { create } from 'zustand';
// Using standard NDK types but importing NDKEvent from ndk-mobile for compatibility
import NDK, { NDKFilter } from '@nostr-dev-kit/ndk';
import { NDKEvent, NDKUser } from '@nostr-dev-kit/ndk-mobile';
import NDK, {
NDKEvent,
NDKUser,
NDKRelay,
NDKPrivateKeySigner
} from '@nostr-dev-kit/ndk';
import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools';
import * as SecureStore from 'expo-secure-store';
import * as Crypto from 'expo-crypto';
import { openDatabaseSync } from 'expo-sqlite';
import { NDKMobilePrivateKeySigner, generateKeyPair } from '@/lib/mobile-signer';
// Constants for SecureStore
const PRIVATE_KEY_STORAGE_KEY = 'nostr_privkey';
// Default relays
const DEFAULT_RELAYS = [
'wss://powr.duckdns.org', // Your primary relay
'wss://powr.duckdns.org',
'wss://relay.damus.io',
'wss://relay.nostr.band',
'wss://purplepag.es',
'wss://nos.lol'
];
@ -29,7 +28,7 @@ type NDKStoreState = {
isLoading: boolean;
isAuthenticated: boolean;
error: Error | null;
relayStatus: Record<string, 'connected' | 'connecting' | 'disconnected' | 'error'>;
relayStatus: Record<string, 'connected' | 'connecting' | 'disconnected'>;
};
type NDKStoreActions = {
@ -38,14 +37,27 @@ type NDKStoreActions = {
logout: () => Promise<void>;
generateKeys: () => { privateKey: string; publicKey: string; nsec: string; npub: string };
publishEvent: (kind: number, content: string, tags: string[][]) => Promise<NDKEvent | null>;
createEvent: (kind: number, content: string, tags: string[][]) => Promise<NDKEvent | null>;
queueEventForPublishing: (event: NDKEvent) => Promise<boolean>;
processPublicationQueue: () => Promise<void>;
fetchEventsByFilter: (filter: NDKFilter) => Promise<NDKEvent[]>;
fetchUserProfile: (pubkey: string) => Promise<NDKUser | null>;
fetchEventsByFilter: (filter: any) => Promise<NDKEvent[]>;
};
// Helper to convert byte array to hex string
function bytesToHex(bytes: Uint8Array): string {
return Array.from(bytes)
.map(byte => byte.toString(16).padStart(2, '0'))
.join('');
}
// Helper to convert hex string to byte array
function hexToBytes(hex: string): Uint8Array {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
}
return bytes;
}
export const useNDKStore = create<NDKStoreState & NDKStoreActions>((set, get) => ({
// State properties
ndk: null,
currentUser: null,
isLoading: false,
@ -53,102 +65,53 @@ export const useNDKStore = create<NDKStoreState & NDKStoreActions>((set, get) =>
error: null,
relayStatus: {},
// Initialize NDK
init: async () => {
try {
console.log('[NDK] Initializing...');
console.log('NDK init crypto polyfill check:', {
cryptoDefined: typeof global.crypto !== 'undefined',
getRandomValuesDefined: typeof global.crypto?.getRandomValues !== 'undefined'
});
set({ isLoading: true, error: null });
// Initialize relay status tracking
const relayStatus: Record<string, 'connected' | 'connecting' | 'disconnected' | 'error'> = {};
DEFAULT_RELAYS.forEach(r => {
relayStatus[r] = 'connecting';
});
set({ relayStatus });
// IMPORTANT: Due to the lack of an Expo config plugin for ndk-mobile,
// we're using a standard NDK initialization approach rather than trying to use
// ndk-mobile's native modules, which require a custom build.
//
// When an Expo plugin becomes available for ndk-mobile, we can remove this
// fallback approach and use the initializeNDK() function directly.
console.log('[NDK] Using standard NDK initialization');
// Initialize NDK with relays
const ndk = new NDK({
explicitRelayUrls: DEFAULT_RELAYS
});
// Connect to relays
await ndk.connect();
// Setup relay status updates
// Setup relay status tracking
const relayStatus: Record<string, 'connected' | 'connecting' | 'disconnected'> = {};
DEFAULT_RELAYS.forEach(url => {
const relay = ndk.pool.getRelay(url);
if (relay) {
relay.on('connect', () => {
set(state => ({
relayStatus: {
...state.relayStatus,
[url]: 'connected'
}
}));
});
relay.on('disconnect', () => {
set(state => ({
relayStatus: {
...state.relayStatus,
[url]: 'disconnected'
}
}));
});
// Set error status if not connected within timeout
setTimeout(() => {
set(state => {
if (state.relayStatus[url] === 'connecting') {
return {
relayStatus: {
...state.relayStatus,
[url]: 'error'
}
};
}
return state;
});
}, 10000);
}
relayStatus[url] = 'connecting';
});
set({ ndk });
// Monitor relay connections
ndk.pool.on('relay:connect', (relay: NDKRelay) => {
console.log(`[NDK] Relay connected: ${relay.url}`);
set(state => ({
relayStatus: {
...state.relayStatus,
[relay.url]: 'connected'
}
}));
});
ndk.pool.on('relay:disconnect', (relay: NDKRelay) => {
console.log(`[NDK] Relay disconnected: ${relay.url}`);
set(state => ({
relayStatus: {
...state.relayStatus,
[relay.url]: 'disconnected'
}
}));
});
await ndk.connect();
set({ ndk, relayStatus });
// Check for saved private key
const privateKey = await SecureStore.getItemAsync(PRIVATE_KEY_STORAGE_KEY);
if (privateKey) {
const privateKeyHex = await SecureStore.getItemAsync(PRIVATE_KEY_STORAGE_KEY);
if (privateKeyHex) {
console.log('[NDK] Found saved private key, initializing signer');
try {
// Create mobile-specific signer with private key
const signer = new NDKMobilePrivateKeySigner(privateKey);
ndk.signer = signer;
// Get user and profile
const user = await ndk.signer.user();
if (user) {
console.log('[NDK] User authenticated:', user.pubkey);
await user.fetchProfile();
set({
currentUser: user,
isAuthenticated: true
});
}
await get().login(privateKeyHex);
} catch (error) {
console.error('[NDK] Error initializing with saved key:', error);
// Remove invalid key
@ -156,26 +119,6 @@ export const useNDKStore = create<NDKStoreState & NDKStoreActions>((set, get) =>
}
}
// Set up connectivity monitoring to process publication queue
try {
const { ConnectivityService } = await import('@/lib/db/services/ConnectivityService');
// Process queue on initial connection
if (ConnectivityService.getInstance().getConnectionStatus()) {
get().processPublicationQueue();
}
// Add listener to process queue when coming online
ConnectivityService.getInstance().addListener((isOnline) => {
if (isOnline) {
console.log('[NDK] Connection restored, processing publication queue');
get().processPublicationQueue();
}
});
} catch (error) {
console.error('[NDK] Error setting up connectivity monitoring:', error);
}
set({ isLoading: false });
} catch (error) {
console.error('[NDK] Initialization error:', error);
@ -186,7 +129,7 @@ export const useNDKStore = create<NDKStoreState & NDKStoreActions>((set, get) =>
}
},
login: async (privateKey?: string) => {
login: async (privateKeyInput?: string) => {
set({ isLoading: true, error: null });
try {
@ -196,14 +139,28 @@ export const useNDKStore = create<NDKStoreState & NDKStoreActions>((set, get) =>
}
// If no private key is provided, generate one
let userPrivateKey = privateKey;
if (!userPrivateKey) {
const { privateKey: generatedKey } = get().generateKeys();
userPrivateKey = generatedKey;
let privateKeyHex = privateKeyInput;
if (!privateKeyHex) {
const { privateKey } = get().generateKeys();
privateKeyHex = privateKey;
}
// Create mobile-specific signer with private key
const signer = new NDKMobilePrivateKeySigner(userPrivateKey);
// Handle nsec format
if (privateKeyHex.startsWith('nsec')) {
try {
const decoded = nip19.decode(privateKeyHex);
if (decoded.type === 'nsec') {
// Get the data as hex
privateKeyHex = bytesToHex(decoded.data as any);
}
} catch (error) {
console.error('Error decoding nsec:', error);
throw new Error('Invalid nsec format');
}
}
// Create signer with private key
const signer = new NDKPrivateKeySigner(privateKeyHex);
ndk.signer = signer;
// Get user
@ -225,8 +182,8 @@ export const useNDKStore = create<NDKStoreState & NDKStoreActions>((set, get) =>
console.log('[NDK] User profile loaded:', user.profile);
}
// Save the private key securely
await SecureStore.setItemAsync(PRIVATE_KEY_STORAGE_KEY, userPrivateKey);
// Save the private key hex string securely
await SecureStore.setItemAsync(PRIVATE_KEY_STORAGE_KEY, privateKeyHex);
set({
currentUser: user,
@ -270,7 +227,25 @@ export const useNDKStore = create<NDKStoreState & NDKStoreActions>((set, get) =>
generateKeys: () => {
try {
return generateKeyPair();
// Generate a new secret key (returns Uint8Array)
const secretKeyBytes = generateSecretKey();
// Convert to hex for storage
const privateKey = bytesToHex(secretKeyBytes);
// Get public key
const publicKey = getPublicKey(secretKeyBytes);
// Generate nsec and npub
const nsec = nip19.nsecEncode(secretKeyBytes);
const npub = nip19.npubEncode(publicKey);
return {
privateKey,
publicKey,
nsec,
npub
};
} catch (error) {
console.error('[NDK] Error generating keys:', error);
set({ error: error instanceof Error ? error : new Error('Failed to generate keys') });
@ -278,15 +253,6 @@ export const useNDKStore = create<NDKStoreState & NDKStoreActions>((set, get) =>
}
},
// IMPORTANT: This method uses monkey patching to make event signing work
// in React Native environment. This is necessary because the underlying
// Nostr libraries expect Web Crypto API to be available.
//
// When ndk-mobile gets proper Expo support, this function can be simplified to:
// 1. Create the event
// 2. Call event.sign() directly
// 3. Call event.publish()
// without the monkey patching code.
publishEvent: async (kind: number, content: string, tags: string[][]) => {
try {
const { ndk, isAuthenticated, currentUser } = get();
@ -300,276 +266,54 @@ export const useNDKStore = create<NDKStoreState & NDKStoreActions>((set, get) =>
}
// Create event
console.log('Creating event...');
const event = new NDKEvent(ndk);
event.kind = kind;
event.content = content;
event.tags = tags;
// MONKEY PATCHING APPROACH:
// This is needed because the standard NDK doesn't properly work with
// React Native's crypto implementation. When ndk-mobile adds proper Expo
// support, this can be removed.
try {
// Define custom function for random bytes generation
const customRandomBytes = (length: number): Uint8Array => {
console.log('Using custom randomBytes in event signing');
return (Crypto as any).getRandomBytes(length);
};
// Try to find and override the randomBytes function
const nostrTools = require('nostr-tools');
const nobleHashes = require('@noble/hashes/utils');
// Backup original functions
const originalNobleRandomBytes = nobleHashes.randomBytes;
// Override with our implementation
(nobleHashes as any).randomBytes = customRandomBytes;
// Sign event
console.log('Signing event with patched libraries...');
await event.sign();
// Restore original functions
(nobleHashes as any).randomBytes = originalNobleRandomBytes;
console.log('Event signed successfully');
} catch (signError) {
console.error('Error signing event:', signError);
throw signError;
}
// Publish the event
console.log('Publishing event...');
// Sign and publish
await event.sign();
await event.publish();
console.log('Event published successfully:', event.id);
return event;
} catch (error) {
console.error('Error publishing event:', error);
console.error('Error details:', error instanceof Error ? error.stack : 'Unknown error');
set({ error: error instanceof Error ? error : new Error('Failed to publish event') });
return null;
}
},
// Create and sign a Nostr event without publishing it
createEvent: async (kind: number, content: string, tags: string[][]): Promise<NDKEvent | null> => {
// Fetch profile for any user
fetchUserProfile: async (pubkey: string) => {
try {
const { ndk, isAuthenticated, currentUser } = get();
const { ndk } = get();
if (!ndk) {
throw new Error('NDK not initialized');
}
if (!isAuthenticated || !currentUser) {
throw new Error('Not authenticated');
}
const user = ndk.getUser({ pubkey });
await user.fetchProfile();
// Create event
const event = new NDKEvent(ndk);
event.kind = kind;
event.content = content;
event.tags = tags;
// Define custom function for random bytes generation
const customRandomBytes = (length: number): Uint8Array => {
console.log('Using custom randomBytes in event signing');
return (Crypto as any).getRandomBytes(length);
};
// Try to find and override the randomBytes function
const nostrTools = require('nostr-tools');
const nobleHashes = require('@noble/hashes/utils');
// Backup original functions
const originalNobleRandomBytes = nobleHashes.randomBytes;
// Override with our implementation
(nobleHashes as any).randomBytes = customRandomBytes;
// Sign the event but don't publish
try {
await event.sign();
} finally {
// Restore original functions
(nobleHashes as any).randomBytes = originalNobleRandomBytes;
}
return event;
return user;
} catch (error) {
console.error('Error creating event:', error);
set({ error: error instanceof Error ? error : new Error('Failed to create event') });
console.error('Error fetching user profile:', error);
set({ error: error instanceof Error ? error : new Error('Failed to fetch user profile') });
return null;
}
},
// Queue an event for publishing when online
queueEventForPublishing: async (event: NDKEvent): Promise<boolean> => {
try {
// Only proceed if the event has an ID and signature
if (!event.id || !event.sig) {
throw new Error('Event must be signed before queueing');
}
// First cache the event itself
try {
const EventCache = (await import('@/lib/db/services/EventCache')).EventCache;
const db = openDatabaseSync('powr.db');
const cache = new EventCache(db);
// Convert NDKEvent to NostrEvent for caching
await cache.setEvent({
id: event.id,
pubkey: event.pubkey,
kind: event.kind || 0,
created_at: event.created_at || Math.floor(Date.now() / 1000),
content: event.content,
tags: event.tags.map(tag => tag.map(item => String(item))),
sig: event.sig
});
// Then add to publication queue
await db.runAsync(
`INSERT OR REPLACE INTO publication_queue
(event_id, attempts, created_at, payload)
VALUES (?, ?, ?, ?)`,
[
event.id,
0,
Date.now(),
JSON.stringify({
id: event.id,
pubkey: event.pubkey,
kind: event.kind,
created_at: event.created_at,
content: event.content,
tags: event.tags,
sig: event.sig
})
]
);
} catch (cacheError) {
console.error('Error caching event:', cacheError);
// Continue to try publishing even if caching fails
}
// Try to publish immediately if online
try {
const ConnectivityService = (await import('@/lib/db/services/ConnectivityService')).ConnectivityService;
if (ConnectivityService.getInstance().getConnectionStatus()) {
try {
await event.publish();
// Remove from queue if successful
const db = openDatabaseSync('powr.db');
await db.runAsync(
`DELETE FROM publication_queue WHERE event_id = ?`,
[event.id]
);
console.log('Event published successfully:', event.id);
return true;
} catch (publishError) {
console.log('Event queued for later publishing:', event.id);
return false;
}
} else {
console.log('Event queued for later publishing (offline):', event.id);
return false;
}
} catch (connectivityError) {
console.error('Error checking connectivity:', connectivityError);
// Assume offline if connectivity service fails
return false;
}
} catch (error) {
console.error('Error queueing event for publishing:', error);
return false;
}
},
// Process the publication queue
processPublicationQueue: async (): Promise<void> => {
// Fetch events by filter
fetchEventsByFilter: async (filter: any) => {
try {
const { ndk } = get();
if (!ndk) return;
const db = openDatabaseSync('powr.db');
// Get all queued events that haven't exceeded max attempts
const queuedEvents = await db.getAllAsync<{
event_id: string;
attempts: number;
payload: string;
}>(
`SELECT event_id, attempts, payload
FROM publication_queue
WHERE attempts < 5
ORDER BY created_at ASC`
);
console.log(`Processing publication queue: ${queuedEvents.length} events`);
for (const item of queuedEvents) {
try {
// Update attempt count and timestamp
await db.runAsync(
`UPDATE publication_queue
SET attempts = attempts + 1,
last_attempt = ?
WHERE event_id = ?`,
[Date.now(), item.event_id]
);
// Parse the event from payload
const eventData = JSON.parse(item.payload);
// Create a new NDKEvent
const event = new NDKEvent(ndk);
// Copy properties
event.id = eventData.id;
event.pubkey = eventData.pubkey;
event.kind = eventData.kind;
event.created_at = eventData.created_at;
event.content = eventData.content;
event.tags = eventData.tags;
event.sig = eventData.sig;
// Publish the event
await event.publish();
// Remove from queue on success
await db.runAsync(
`DELETE FROM publication_queue WHERE event_id = ?`,
[item.event_id]
);
console.log(`Published queued event: ${item.event_id}`);
} catch (error) {
console.error(`Error publishing queued event ${item.event_id}:`, error);
}
}
} catch (error) {
console.error('Error processing publication queue:', error);
}
},
fetchEventsByFilter: async (filter: NDKFilter) => {
try {
const { ndk } = get();
if (!ndk) {
throw new Error('NDK not initialized');
}
// Fetch events
// Fetch events using NDK
const events = await ndk.fetchEvents(filter);
// Convert Set to Array
return Array.from(events);
} catch (error) {
console.error('Error fetching events:', error);
@ -577,4 +321,39 @@ export const useNDKStore = create<NDKStoreState & NDKStoreActions>((set, get) =>
return [];
}
}
}));
}));
// Export hooks for using the store
export function useNDK() {
return useNDKStore(state => ({
ndk: state.ndk,
isLoading: state.isLoading,
error: state.error,
init: state.init
}));
}
export function useNDKCurrentUser() {
return useNDKStore(state => ({
currentUser: state.currentUser,
isAuthenticated: state.isAuthenticated,
isLoading: state.isLoading
}));
}
export function useNDKAuth() {
return useNDKStore(state => ({
login: state.login,
logout: state.logout,
generateKeys: state.generateKeys,
isAuthenticated: state.isAuthenticated,
isLoading: state.isLoading
}));
}
export function useNDKEvents() {
return useNDKStore(state => ({
publishEvent: state.publishEvent,
fetchEventsByFilter: state.fetchEventsByFilter
}));
}

View File

@ -19,6 +19,8 @@ import type {
} from '@/types/templates';
import type { BaseExercise } from '@/types/exercise';
import { openDatabaseSync } from 'expo-sqlite';
import { FavoritesService } from '@/lib/db/services/FavoritesService';
const AUTO_SAVE_INTERVAL = 30000; // 30 seconds
@ -500,27 +502,12 @@ const useWorkoutStoreBase = create<ExtendedWorkoutState & ExtendedWorkoutActions
// Favorite Management with SQLite persistence - IMPROVED VERSION
loadFavorites: async () => {
try {
// Get the favorites service through a local import trick since we can't use hooks here
const db = openDatabaseSync('powr.db');
const favoritesService = new FavoritesService(db);
// Ensure favorites table exists
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,
created_at INTEGER NOT NULL,
UNIQUE(content_type, content_id)
);
CREATE INDEX IF NOT EXISTS idx_favorites_content ON favorites(content_type, content_id);
`);
// Load just the IDs from SQLite
const result = await db.getAllAsync<{
content_id: string
}>(`SELECT content_id FROM favorites WHERE content_type = 'template'`);
const favoriteIds = result.map(item => item.content_id);
// Load just the IDs
const favoriteIds = await favoritesService.getFavoriteIds('template');
set({
favoriteIds,
@ -528,7 +515,6 @@ const useWorkoutStoreBase = create<ExtendedWorkoutState & ExtendedWorkoutActions
});
console.log(`Loaded ${favoriteIds.length} favorite IDs from database`);
// Don't return anything (void)
} catch (error) {
console.error('Error loading favorites:', error);
set({ favoritesLoaded: true }); // Mark as loaded even on error
@ -548,52 +534,24 @@ const useWorkoutStoreBase = create<ExtendedWorkoutState & ExtendedWorkoutActions
return [];
}
// Query the full content from SQLite
try {
const db = openDatabaseSync('powr.db');
const favoritesService = new FavoritesService(db);
// Generate placeholders for the SQL query
const placeholders = get().favoriteIds.map(() => '?').join(',');
// Get full content for all favorited templates
const result = await db.getAllAsync<{
id: string,
content_type: string,
content_id: string,
content: string,
created_at: number
}>(`SELECT * FROM favorites WHERE content_type = 'template' AND content_id IN (${placeholders})`,
get().favoriteIds
);
return result.map(item => ({
id: item.content_id,
content: JSON.parse(item.content),
addedAt: item.created_at
}));
return await favoritesService.getFavorites('template');
} catch (error) {
console.error('Error fetching favorites content:', error);
return [];
}
},
addFavorite: async (template: WorkoutTemplate) => {
try {
const db = openDatabaseSync('powr.db');
const now = Date.now();
const favoritesService = new FavoritesService(db);
// Add to SQLite database
await db.runAsync(
`INSERT OR REPLACE INTO favorites (id, content_type, content_id, content, created_at)
VALUES (?, ?, ?, ?, ?)`,
[
generateId('local'), // Generate a unique ID for the favorite entry
'template',
template.id,
JSON.stringify(template),
now
]
);
// Add to favorites database
await favoritesService.addFavorite('template', template.id, template);
// Update just the ID in memory state
set(state => {
@ -610,16 +568,14 @@ const useWorkoutStoreBase = create<ExtendedWorkoutState & ExtendedWorkoutActions
throw error;
}
},
removeFavorite: async (templateId: string) => {
try {
const db = openDatabaseSync('powr.db');
const favoritesService = new FavoritesService(db);
// Remove from SQLite database
await db.runAsync(
`DELETE FROM favorites WHERE content_type = 'template' AND content_id = ?`,
[templateId]
);
// Remove from favorites database
await favoritesService.removeFavorite('template', templateId);
// Update IDs in memory state
set(state => ({