mirror of
https://github.com/DocNR/POWR.git
synced 2025-06-05 08:42:05 +00:00
updated with NDK-mobile library and removed custom nostr functions
This commit is contained in:
parent
2564450333
commit
b61381b865
149
CHANGELOG.md
149
CHANGELOG.md
@ -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/),
|
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).
|
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]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### 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
|
- Successful Nostr protocol integration
|
||||||
- Implemented NDK-mobile for React Native compatibility
|
- Implemented NDK-mobile for React Native compatibility
|
||||||
- Added secure key management with Expo SecureStore
|
- 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
|
- Added workout persistence and recovery
|
||||||
- Built automatic timer management with background support
|
- Built automatic timer management with background support
|
||||||
- Developed minimization and maximization functionality
|
- 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
|
- Workout tracking implementation with real-time tracking
|
||||||
- Added workout timer with proper background handling
|
- Added workout timer with proper background handling
|
||||||
- Implemented rest timer functionality
|
- 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
|
- Improved workout history visualization
|
||||||
|
|
||||||
### Changed
|
### 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
|
- Improved workout screen navigation consistency
|
||||||
- Standardized screen transitions and gestures
|
- Standardized screen transitions and gestures
|
||||||
- Added back buttons for clearer navigation
|
- 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
|
- Enhanced visual separation between template metadata and content
|
||||||
|
|
||||||
### Fixed
|
### 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 navigation gesture handling issues
|
||||||
- Workout timer inconsistency during app background state
|
- Workout timer inconsistency during app background state
|
||||||
- Exercise deletion functionality
|
- 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
|
- Content rendering issues in bottom sheet components
|
||||||
|
|
||||||
### Technical Details
|
### Technical Details
|
||||||
1. Exercise Management:
|
1. Nostr Integration:
|
||||||
- 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:
|
|
||||||
- Implemented @nostr-dev-kit/ndk-mobile package for React Native compatibility
|
- Implemented @nostr-dev-kit/ndk-mobile package for React Native compatibility
|
||||||
- Created dedicated NDK store using Zustand for state management
|
- Created dedicated NDK store using Zustand for state management
|
||||||
- Built secure key storage and retrieval using Expo SecureStore
|
- 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
|
- Added relay connection management with status tracking
|
||||||
- Developed proper error handling for network operations
|
- Developed proper error handling for network operations
|
||||||
|
|
||||||
4. Cryptographic Implementation:
|
2. Cryptographic Implementation:
|
||||||
- Integrated react-native-get-random-values for crypto API polyfill
|
- Integrated react-native-get-random-values for crypto API polyfill
|
||||||
- Implemented NDKMobilePrivateKeySigner for key operations
|
- Implemented NDKMobilePrivateKeySigner for key operations
|
||||||
- Added proper key format handling (hex, nsec)
|
- Added proper key format handling (hex, nsec)
|
||||||
- Created secure key generation functionality
|
- Created secure key generation functionality
|
||||||
- Built robust error handling for cryptographic operations
|
- Built robust error handling for cryptographic operations
|
||||||
|
|
||||||
5. Programs Testing Component:
|
3. Programs Testing Component:
|
||||||
- Developed dual-purpose interface for Database and Nostr testing
|
- Developed dual-purpose interface for Database and Nostr testing
|
||||||
- Implemented login system with key generation and secure storage
|
- Implemented login system with key generation and secure storage
|
||||||
- Built event creation interface with multiple event kinds
|
- Built event creation interface with multiple event kinds
|
||||||
- Added event querying and display functionality
|
- Added event querying and display functionality
|
||||||
- Created detailed event inspection with tag visualization
|
- Created detailed event inspection with tag visualization
|
||||||
- Added relay status monitoring
|
- Added relay status monitoring
|
||||||
|
4. Database Schema Enforcement:
|
||||||
6. Database Schema Enforcement:
|
|
||||||
- Added CHECK constraints for equipment types
|
- Added CHECK constraints for equipment types
|
||||||
- Added CHECK constraints for exercise types
|
- Added CHECK constraints for exercise types
|
||||||
- Added CHECK constraints for categories
|
- Added CHECK constraints for categories
|
||||||
- Proper handling of foreign key constraints
|
- Proper handling of foreign key constraints
|
||||||
|
5. Input Validation:
|
||||||
7. Input Validation:
|
|
||||||
- Equipment options: bodyweight, barbell, dumbbell, kettlebell, machine, cable, other
|
- Equipment options: bodyweight, barbell, dumbbell, kettlebell, machine, cable, other
|
||||||
- Exercise types: strength, cardio, bodyweight
|
- Exercise types: strength, cardio, bodyweight
|
||||||
- Categories: Push, Pull, Legs, Core
|
- Categories: Push, Pull, Legs, Core
|
||||||
- Difficulty levels: beginner, intermediate, advanced
|
- Difficulty levels: beginner, intermediate, advanced
|
||||||
- Movement patterns: push, pull, squat, hinge, carry, rotation
|
- Movement patterns: push, pull, squat, hinge, carry, rotation
|
||||||
|
6. Error Handling:
|
||||||
8. Error Handling:
|
|
||||||
- Added SQLite error type definitions
|
- Added SQLite error type definitions
|
||||||
- Improved error propagation in LibraryService
|
- Improved error propagation in LibraryService
|
||||||
- Added transaction rollback on constraint violations
|
- Added transaction rollback on constraint violations
|
||||||
|
7. Database Services:
|
||||||
9. Database Services:
|
|
||||||
- Added EventCache service for Nostr events
|
- Added EventCache service for Nostr events
|
||||||
- Improved ExerciseService with transaction awareness
|
- Improved ExerciseService with transaction awareness
|
||||||
- Added DevSeederService for development data
|
- Added DevSeederService for development data
|
||||||
- Enhanced error handling and logging
|
- Enhanced error handling and logging
|
||||||
|
8. Workout State Management with Zustand:
|
||||||
10. Workout State Management with Zustand:
|
- Implemented selector pattern for performance optimization
|
||||||
- Implemented selector pattern for performance optimization
|
- Added module-level timer references for background operation
|
||||||
- Added module-level timer references for background operation
|
- Created workout persistence with auto-save functionality
|
||||||
- Created workout persistence with auto-save functionality
|
- Developed state recovery for crash protection
|
||||||
- Developed state recovery for crash protection
|
- Added support for future Nostr integration
|
||||||
- Added support for future Nostr integration
|
- Implemented workout minimization for multi-tasking
|
||||||
- Implemented workout minimization for multi-tasking
|
9. Template Details UI Architecture:
|
||||||
|
- Implemented MaterialTopTabNavigator for content organization
|
||||||
11. Template Details UI Architecture:
|
- Created screen-specific components for each tab
|
||||||
- Implemented MaterialTopTabNavigator for content organization
|
- Developed conditional rendering based on template source
|
||||||
- Created screen-specific components for each tab
|
- Implemented context-aware action buttons
|
||||||
- Developed conditional rendering based on template source
|
- Added proper navigation state handling
|
||||||
- Implemented context-aware action buttons
|
|
||||||
- Added proper navigation state handling
|
|
||||||
|
|
||||||
### Migration Notes
|
### 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
|
- Exercise creation now enforces schema constraints
|
||||||
- Input validation prevents invalid data entry
|
- Input validation prevents invalid data entry
|
||||||
- Enhanced error messages provide better debugging information
|
- Enhanced error messages provide better debugging information
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// app/(tabs)/library/programs.tsx
|
// app/(tabs)/library/programs.tsx
|
||||||
import React, { useState, useEffect } from 'react';
|
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 { Text } from '@/components/ui/text';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
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 NostrLoginSheet from '@/components/sheets/NostrLoginSheet';
|
||||||
import {
|
import {
|
||||||
AlertCircle, CheckCircle2, Database, RefreshCcw, Trash2,
|
AlertCircle, CheckCircle2, Database, RefreshCcw, Trash2,
|
||||||
Code, Search, ListFilter, Wifi, Zap, FileJson, X, Info
|
Code, Search, ListFilter, Wifi, Zap, FileJson
|
||||||
} from 'lucide-react-native';
|
} from 'lucide-react-native';
|
||||||
import { useSQLiteContext } from 'expo-sqlite';
|
import { useSQLiteContext } from 'expo-sqlite';
|
||||||
import { useNDK, useNDKAuth, useNDKCurrentUser } from '@/lib/hooks/useNDK';
|
import { useNDK, useNDKAuth, useNDKCurrentUser } from '@/lib/hooks/useNDK';
|
||||||
@ -50,6 +50,7 @@ const initialFilters: FilterOptions = {
|
|||||||
tags: [],
|
tags: [],
|
||||||
source: []
|
source: []
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ProgramsScreen() {
|
export default function ProgramsScreen() {
|
||||||
const db = useSQLiteContext();
|
const db = useSQLiteContext();
|
||||||
|
|
||||||
@ -77,7 +78,7 @@ export default function ProgramsScreen() {
|
|||||||
const [statusMessage, setStatusMessage] = useState('');
|
const [statusMessage, setStatusMessage] = useState('');
|
||||||
const [events, setEvents] = useState<DisplayEvent[]>([]);
|
const [events, setEvents] = useState<DisplayEvent[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [eventKind, setEventKind] = useState(NostrEventKind.EXERCISE);
|
const [eventKind, setEventKind] = useState(NostrEventKind.TEXT);
|
||||||
const [eventContent, setEventContent] = useState('');
|
const [eventContent, setEventContent] = useState('');
|
||||||
const [isLoginSheetOpen, setIsLoginSheetOpen] = useState(false);
|
const [isLoginSheetOpen, setIsLoginSheetOpen] = useState(false);
|
||||||
|
|
||||||
@ -87,7 +88,8 @@ export default function ProgramsScreen() {
|
|||||||
const { login, logout, generateKeys } = useNDKAuth();
|
const { login, logout, generateKeys } = useNDKAuth();
|
||||||
|
|
||||||
// Tab state
|
// Tab state
|
||||||
const [activeTab, setActiveTab] = useState('database');
|
const [activeTab, setActiveTab] = useState('nostr'); // Default to nostr tab for testing
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check database status
|
// Check database status
|
||||||
checkDatabase();
|
checkDatabase();
|
||||||
@ -261,6 +263,7 @@ export default function ProgramsScreen() {
|
|||||||
setActiveFilters(totalFilters);
|
setActiveFilters(totalFilters);
|
||||||
// Implement filtering logic for programs when available
|
// Implement filtering logic for programs when available
|
||||||
};
|
};
|
||||||
|
|
||||||
// NOSTR FUNCTIONS
|
// NOSTR FUNCTIONS
|
||||||
|
|
||||||
// Handle login dialog
|
// Handle login dialog
|
||||||
@ -317,15 +320,6 @@ export default function ProgramsScreen() {
|
|||||||
setEventContent('Hello from POWR App - Test Note');
|
setEventContent('Hello from POWR App - Test Note');
|
||||||
}
|
}
|
||||||
} else if (eventKind === NostrEventKind.EXERCISE) {
|
} 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}`;
|
const uniqueId = `exercise-${timestamp}`;
|
||||||
tags.push(
|
tags.push(
|
||||||
['d', uniqueId],
|
['d', uniqueId],
|
||||||
@ -366,7 +360,8 @@ export default function ProgramsScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use the NDK store's publishEvent function
|
// 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) {
|
if (event) {
|
||||||
// Add the published event to our display list
|
// Add the published event to our display list
|
||||||
@ -410,7 +405,7 @@ export default function ProgramsScreen() {
|
|||||||
// Create a filter for the specific kind
|
// Create a filter for the specific kind
|
||||||
const filter = { kinds: [eventKind as number], limit: 20 };
|
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 fetchedEvents = await useNDKStore.getState().fetchEventsByFilter(filter);
|
||||||
|
|
||||||
const displayEvents: DisplayEvent[] = [];
|
const displayEvents: DisplayEvent[] = [];
|
||||||
@ -437,6 +432,7 @@ export default function ProgramsScreen() {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="flex-1 bg-background">
|
<View className="flex-1 bg-background">
|
||||||
{/* Search bar with filter button */}
|
{/* Search bar with filter button */}
|
||||||
@ -497,6 +493,7 @@ export default function ProgramsScreen() {
|
|||||||
<Text className={activeTab === 'nostr' ? 'text-white' : 'text-foreground'}>Nostr</Text>
|
<Text className={activeTab === 'nostr' ? 'text-white' : 'text-foreground'}>Nostr</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Tab Content */}
|
{/* Tab Content */}
|
||||||
{activeTab === 'database' && (
|
{activeTab === 'database' && (
|
||||||
<ScrollView className="flex-1 p-4">
|
<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="font-medium mt-3">Active Relay:</Text>
|
||||||
<Text className="text-sm text-muted-foreground">wss://powr.duckdns.org</Text>
|
<Text className="text-sm text-muted-foreground">wss://powr.duckdns.org</Text>
|
||||||
<Text className="text-xs text-muted-foreground mt-1">
|
<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>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Login Modal */}
|
{/* NostrLoginSheet component */}
|
||||||
<NostrLoginSheet
|
<NostrLoginSheet
|
||||||
open={isLoginSheetOpen}
|
open={isLoginSheetOpen}
|
||||||
onClose={handleCloseLogin}
|
onClose={handleCloseLogin}
|
||||||
@ -839,6 +836,7 @@ export default function ProgramsScreen() {
|
|||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Event JSON Viewer */}
|
{/* Event JSON Viewer */}
|
||||||
<Card className="mb-4">
|
<Card className="mb-4">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@ -878,12 +876,12 @@ export default function ProgramsScreen() {
|
|||||||
<Text className="font-medium mb-2">How to test Nostr integration:</Text>
|
<Text className="font-medium mb-2">How to test Nostr integration:</Text>
|
||||||
<View className="space-y-2">
|
<View className="space-y-2">
|
||||||
<Text>1. Click "Login with Nostr" to authenticate</Text>
|
<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>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>5. Enter optional content and click "Publish"</Text>
|
||||||
<Text>6. Use "Query Events" to fetch existing events of the selected kind</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>
|
</View>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
// app/_layout.tsx
|
// app/_layout.tsx
|
||||||
import 'expo-dev-client';
|
import 'expo-dev-client';
|
||||||
import '../lib/crypto-polyfill'; // Import crypto polyfill first
|
|
||||||
import '@/global.css';
|
import '@/global.css';
|
||||||
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
||||||
import { Stack } from 'expo-router';
|
import { Stack } from 'expo-router';
|
||||||
|
@ -3,22 +3,27 @@ import { View, ActivityIndicator, Text } from 'react-native';
|
|||||||
import { SQLiteProvider, openDatabaseSync, SQLiteDatabase } from 'expo-sqlite';
|
import { SQLiteProvider, openDatabaseSync, SQLiteDatabase } from 'expo-sqlite';
|
||||||
import { schema } from '@/lib/db/schema';
|
import { schema } from '@/lib/db/schema';
|
||||||
import { ExerciseService } from '@/lib/db/services/ExerciseService';
|
import { ExerciseService } from '@/lib/db/services/ExerciseService';
|
||||||
import { EventCache } from '@/lib/db/services/EventCache';
|
|
||||||
import { DevSeederService } from '@/lib/db/services/DevSeederService';
|
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 { logDatabaseInfo } from '@/lib/db/debug';
|
||||||
|
import { useNDKStore } from '@/lib/stores/ndk';
|
||||||
|
|
||||||
// Create context for services
|
// Create context for services
|
||||||
interface DatabaseServicesContextValue {
|
interface DatabaseServicesContextValue {
|
||||||
exerciseService: ExerciseService | null;
|
exerciseService: ExerciseService | null;
|
||||||
eventCache: EventCache | null;
|
|
||||||
devSeeder: DevSeederService | 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>({
|
const DatabaseServicesContext = React.createContext<DatabaseServicesContextValue>({
|
||||||
exerciseService: null,
|
exerciseService: null,
|
||||||
eventCache: null,
|
|
||||||
devSeeder: null,
|
devSeeder: null,
|
||||||
|
publicationQueue: null,
|
||||||
|
favoritesService: null,
|
||||||
|
db: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
interface DatabaseProviderProps {
|
interface DatabaseProviderProps {
|
||||||
@ -30,9 +35,22 @@ export function DatabaseProvider({ children }: DatabaseProviderProps) {
|
|||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
const [services, setServices] = React.useState<DatabaseServicesContextValue>({
|
const [services, setServices] = React.useState<DatabaseServicesContextValue>({
|
||||||
exerciseService: null,
|
exerciseService: null,
|
||||||
eventCache: null,
|
|
||||||
devSeeder: 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(() => {
|
React.useEffect(() => {
|
||||||
async function initDatabase() {
|
async function initDatabase() {
|
||||||
@ -45,15 +63,27 @@ export function DatabaseProvider({ children }: DatabaseProviderProps) {
|
|||||||
|
|
||||||
// Initialize services
|
// Initialize services
|
||||||
console.log('[DB] Initializing services...');
|
console.log('[DB] Initializing services...');
|
||||||
const eventCache = new EventCache(db);
|
|
||||||
const exerciseService = new ExerciseService(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
|
// Set services
|
||||||
setServices({
|
setServices({
|
||||||
exerciseService,
|
exerciseService,
|
||||||
eventCache,
|
|
||||||
devSeeder,
|
devSeeder,
|
||||||
|
publicationQueue,
|
||||||
|
favoritesService,
|
||||||
|
db,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Seed development database
|
// Seed development database
|
||||||
@ -110,18 +140,34 @@ export function useExerciseService() {
|
|||||||
return context.exerciseService;
|
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() {
|
export function useDevSeeder() {
|
||||||
const context = React.useContext(DatabaseServicesContext);
|
const context = React.useContext(DatabaseServicesContext);
|
||||||
if (!context.devSeeder) {
|
if (!context.devSeeder) {
|
||||||
throw new Error('Dev seeder not initialized');
|
throw new Error('Dev seeder not initialized');
|
||||||
}
|
}
|
||||||
return context.devSeeder;
|
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
153
components/NDKTest.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -18,8 +18,9 @@ import {
|
|||||||
ExerciseDisplay
|
ExerciseDisplay
|
||||||
} from '@/types/exercise';
|
} from '@/types/exercise';
|
||||||
import { StorageSource } from '@/types/shared';
|
import { StorageSource } from '@/types/shared';
|
||||||
|
import { NDKEvent } from '@nostr-dev-kit/ndk-mobile';
|
||||||
import { useNDKStore } from '@/lib/stores/ndk';
|
import { useNDKStore } from '@/lib/stores/ndk';
|
||||||
import { useEventCache } from '@/components/DatabaseProvider';
|
import { useExerciseService, usePublicationQueue } from '@/components/DatabaseProvider';
|
||||||
|
|
||||||
interface ExerciseSheetProps {
|
interface ExerciseSheetProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -67,7 +68,7 @@ export function ExerciseSheet({ isOpen, onClose, onSubmit, exerciseToEdit, mode:
|
|||||||
const { isDarkColorScheme } = useColorScheme();
|
const { isDarkColorScheme } = useColorScheme();
|
||||||
const [formData, setFormData] = useState(DEFAULT_FORM_DATA);
|
const [formData, setFormData] = useState(DEFAULT_FORM_DATA);
|
||||||
const ndkStore = useNDKStore();
|
const ndkStore = useNDKStore();
|
||||||
const eventCache = useEventCache();
|
const publicationQueue = usePublicationQueue();
|
||||||
|
|
||||||
// Determine if we're in edit, create, or fork mode
|
// Determine if we're in edit, create, or fork mode
|
||||||
const hasExercise = !!exerciseToEdit;
|
const hasExercise = !!exerciseToEdit;
|
||||||
@ -170,11 +171,15 @@ export function ExerciseSheet({ isOpen, onClose, onSubmit, exerciseToEdit, mode:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create and attempt to publish the event
|
// 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) {
|
if (event) {
|
||||||
// Queue for publication (this will publish immediately if online)
|
// 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 this is a new exercise, add nostr to sources
|
||||||
if (!exerciseToEdit) {
|
if (!exerciseToEdit) {
|
||||||
|
@ -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();
|
|
428
lib/db/schema.ts
428
lib/db/schema.ts
@ -2,7 +2,7 @@
|
|||||||
import { SQLiteDatabase } from 'expo-sqlite';
|
import { SQLiteDatabase } from 'expo-sqlite';
|
||||||
import { Platform } from 'react-native';
|
import { Platform } from 'react-native';
|
||||||
|
|
||||||
export const SCHEMA_VERSION = 5; // Updated to version 5 for publication queue table
|
export const SCHEMA_VERSION = 1;
|
||||||
|
|
||||||
class Schema {
|
class Schema {
|
||||||
private async getCurrentVersion(db: SQLiteDatabase): Promise<number> {
|
private async getCurrentVersion(db: SQLiteDatabase): Promise<number> {
|
||||||
@ -34,6 +34,14 @@ class Schema {
|
|||||||
console.log(`[Schema] Initializing database on ${Platform.OS}`);
|
console.log(`[Schema] Initializing database on ${Platform.OS}`);
|
||||||
const currentVersion = await this.getCurrentVersion(db);
|
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
|
// Schema version tracking
|
||||||
await db.execAsync(`
|
await db.execAsync(`
|
||||||
CREATE TABLE IF NOT EXISTS schema_version (
|
CREATE TABLE IF NOT EXISTS schema_version (
|
||||||
@ -42,220 +50,220 @@ class Schema {
|
|||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
if (currentVersion < 1) {
|
// Drop all existing tables (except schema_version)
|
||||||
console.log('[Schema] Performing fresh install');
|
await this.dropAllTables(db);
|
||||||
|
|
||||||
// Drop existing tables if they exist
|
// Create all tables in their latest form
|
||||||
await db.execAsync(`DROP TABLE IF EXISTS exercise_tags`);
|
await this.createAllTables(db);
|
||||||
await db.execAsync(`DROP TABLE IF EXISTS exercises`);
|
|
||||||
await db.execAsync(`DROP TABLE IF EXISTS event_tags`);
|
// Update schema version
|
||||||
await db.execAsync(`DROP TABLE IF EXISTS nostr_events`);
|
await this.updateSchemaVersion(db);
|
||||||
|
|
||||||
// Create base tables
|
console.log(`[Schema] Database initialized at version ${SCHEMA_VERSION}`);
|
||||||
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)}`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Schema] Error creating tables:', error);
|
console.error('[Schema] Error creating tables:', error);
|
||||||
throw 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();
|
export const schema = new Schema();
|
@ -1,23 +1,25 @@
|
|||||||
// lib/db/services/DevSeederService.ts
|
// lib/db/services/DevSeederService.ts
|
||||||
import { SQLiteDatabase } from 'expo-sqlite';
|
import { SQLiteDatabase } from 'expo-sqlite';
|
||||||
import { ExerciseService } from './ExerciseService';
|
import { ExerciseService } from './ExerciseService';
|
||||||
import { EventCache } from './EventCache';
|
|
||||||
import { logDatabaseInfo } from '../debug';
|
import { logDatabaseInfo } from '../debug';
|
||||||
import { mockExerciseEvents, convertNostrToExercise } from '../../mocks/exercises';
|
import { mockExerciseEvents, convertNostrToExercise } from '../../mocks/exercises';
|
||||||
|
import NDK, { NDKEvent } from '@nostr-dev-kit/ndk-mobile';
|
||||||
|
|
||||||
export class DevSeederService {
|
export class DevSeederService {
|
||||||
private db: SQLiteDatabase;
|
private db: SQLiteDatabase;
|
||||||
private exerciseService: ExerciseService;
|
private exerciseService: ExerciseService;
|
||||||
private eventCache: EventCache;
|
private ndk: NDK | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
db: SQLiteDatabase,
|
db: SQLiteDatabase,
|
||||||
exerciseService: ExerciseService,
|
exerciseService: ExerciseService
|
||||||
eventCache: EventCache
|
|
||||||
) {
|
) {
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.exerciseService = exerciseService;
|
this.exerciseService = exerciseService;
|
||||||
this.eventCache = eventCache;
|
}
|
||||||
|
|
||||||
|
setNDK(ndk: NDK) {
|
||||||
|
this.ndk = ndk;
|
||||||
}
|
}
|
||||||
|
|
||||||
async seedDatabase() {
|
async seedDatabase() {
|
||||||
@ -42,10 +44,36 @@ export class DevSeederService {
|
|||||||
console.log('Seeding mock exercises...');
|
console.log('Seeding mock exercises...');
|
||||||
|
|
||||||
// Process all events within the same transaction
|
// Process all events within the same transaction
|
||||||
for (const event of mockExerciseEvents) {
|
for (const eventData of mockExerciseEvents) {
|
||||||
// Pass true to indicate we're in a transaction
|
if (this.ndk) {
|
||||||
await this.eventCache.setEvent(event, true);
|
// If NDK is available, use it to cache the event
|
||||||
const exercise = convertNostrToExercise(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);
|
await this.exerciseService.createExercise(exercise, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,7 +101,8 @@ export class DevSeederService {
|
|||||||
'exercise_tags',
|
'exercise_tags',
|
||||||
'nostr_events',
|
'nostr_events',
|
||||||
'event_tags',
|
'event_tags',
|
||||||
'cache_metadata'
|
'cache_metadata',
|
||||||
|
'ndk_cache' // Add the NDK Mobile cache table
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const table of tables) {
|
for (const table of tables) {
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
142
lib/db/services/FavoritesService.ts
Normal file
142
lib/db/services/FavoritesService.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,18 @@
|
|||||||
// lib/db/services/PublicationQueueService.ts
|
// lib/db/services/PublicationQueueService.ts
|
||||||
import { SQLiteDatabase } from 'expo-sqlite';
|
import { SQLiteDatabase } from 'expo-sqlite';
|
||||||
|
import NDK, { NDKEvent } from '@nostr-dev-kit/ndk-mobile';
|
||||||
import { NostrEvent } from '@/types/nostr';
|
import { NostrEvent } from '@/types/nostr';
|
||||||
import { EventCache } from './EventCache';
|
|
||||||
|
|
||||||
export class PublicationQueueService {
|
export class PublicationQueueService {
|
||||||
private db: SQLiteDatabase;
|
private db: SQLiteDatabase;
|
||||||
private eventCache: EventCache;
|
private ndk: NDK | null = null;
|
||||||
|
|
||||||
constructor(db: SQLiteDatabase, eventCache: EventCache) {
|
constructor(db: SQLiteDatabase) {
|
||||||
this.db = db;
|
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
|
* @param event The Nostr event to queue
|
||||||
* @returns Promise that resolves when the event is queued
|
* @returns Promise that resolves when the event is queued
|
||||||
*/
|
*/
|
||||||
async queueEvent(event: NostrEvent): Promise<void> {
|
async queueEvent(event: NostrEvent | NDKEvent): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// First, ensure the event is cached
|
// Convert to the right format for storage
|
||||||
await this.eventCache.setEvent(event);
|
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
|
// Cache the event if NDK is available
|
||||||
const payload = JSON.stringify(event);
|
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(
|
await this.db.runAsync(
|
||||||
`INSERT OR REPLACE INTO publication_queue
|
`INSERT OR REPLACE INTO publication_queue
|
||||||
(event_id, attempts, created_at, payload)
|
(event_id, attempts, created_at, payload)
|
||||||
VALUES (?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?)`,
|
||||||
[
|
[
|
||||||
event.id || '', // Add default empty string if undefined
|
eventId,
|
||||||
0,
|
0,
|
||||||
Date.now(),
|
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) {
|
} catch (error) {
|
||||||
console.error('[Queue] Error queueing event:', error);
|
console.error('[Queue] Error queueing event:', error);
|
||||||
throw 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
|
* Update the attempt count for an event
|
||||||
* @param eventId ID of the event
|
* @param eventId ID of the event
|
||||||
|
@ -1,18 +1,15 @@
|
|||||||
// lib/hooks/useNDK.ts
|
// lib/hooks/useNDK.ts
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useNDKStore } from '@/lib/stores/ndk';
|
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';
|
||||||
|
|
||||||
/**
|
// Core hook for NDK access
|
||||||
* Hook to access NDK instance and initialization status
|
|
||||||
*/
|
|
||||||
export function useNDK() {
|
export function useNDK() {
|
||||||
const { ndk, isLoading, error, init, relayStatus } = useNDKStore(state => ({
|
const { ndk, isLoading, error, init } = useNDKStore(state => ({
|
||||||
ndk: state.ndk,
|
ndk: state.ndk,
|
||||||
isLoading: state.isLoading,
|
isLoading: state.isLoading,
|
||||||
error: state.error,
|
error: state.error,
|
||||||
init: state.init,
|
init: state.init
|
||||||
relayStatus: state.relayStatus
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -21,22 +18,11 @@ export function useNDK() {
|
|||||||
}
|
}
|
||||||
}, [ndk, isLoading, init]);
|
}, [ndk, isLoading, init]);
|
||||||
|
|
||||||
return {
|
return { ndk, isLoading, error };
|
||||||
ndk,
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
relayStatus
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Hook for current user info
|
||||||
* Hook to access current NDK user information
|
export function useNDKCurrentUser() {
|
||||||
*/
|
|
||||||
export function useNDKCurrentUser(): {
|
|
||||||
currentUser: NDKUser | null;
|
|
||||||
isAuthenticated: boolean;
|
|
||||||
isLoading: boolean;
|
|
||||||
} {
|
|
||||||
const { currentUser, isAuthenticated, isLoading } = useNDKStore(state => ({
|
const { currentUser, isAuthenticated, isLoading } = useNDKStore(state => ({
|
||||||
currentUser: state.currentUser,
|
currentUser: state.currentUser,
|
||||||
isAuthenticated: state.isAuthenticated,
|
isAuthenticated: state.isAuthenticated,
|
||||||
@ -50,16 +36,14 @@ export function useNDKCurrentUser(): {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Hook for authentication actions
|
||||||
* Hook to access NDK authentication methods
|
|
||||||
*/
|
|
||||||
export function useNDKAuth() {
|
export function useNDKAuth() {
|
||||||
const { login, logout, isAuthenticated, isLoading, generateKeys } = useNDKStore(state => ({
|
const { login, logout, generateKeys, isAuthenticated, isLoading } = useNDKStore(state => ({
|
||||||
login: state.login,
|
login: state.login,
|
||||||
logout: state.logout,
|
logout: state.logout,
|
||||||
|
generateKeys: state.generateKeys,
|
||||||
isAuthenticated: state.isAuthenticated,
|
isAuthenticated: state.isAuthenticated,
|
||||||
isLoading: state.isLoading,
|
isLoading: state.isLoading
|
||||||
generateKeys: state.generateKeys
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -71,9 +55,7 @@ export function useNDKAuth() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// New hook for event operations
|
||||||
* Hook for direct access to Nostr event actions
|
|
||||||
*/
|
|
||||||
export function useNDKEvents() {
|
export function useNDKEvents() {
|
||||||
const { publishEvent, fetchEventsByFilter } = useNDKStore(state => ({
|
const { publishEvent, fetchEventsByFilter } = useNDKStore(state => ({
|
||||||
publishEvent: state.publishEvent,
|
publishEvent: state.publishEvent,
|
||||||
|
77
lib/hooks/useProfile.tsx
Normal file
77
lib/hooks/useProfile.tsx
Normal 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
|
||||||
|
};
|
||||||
|
}
|
@ -1,24 +1,15 @@
|
|||||||
// lib/hooks/useSubscribe.ts
|
// lib/hooks/useSubscribe.ts
|
||||||
import { useEffect, useState, useRef } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { NDKFilter, NDKSubscription } from '@nostr-dev-kit/ndk';
|
import { NDKEvent, NDKFilter, NDKSubscription, NDKSubscriptionOptions } from '@nostr-dev-kit/ndk-mobile';
|
||||||
import { NDKEvent, NDKUser } from '@nostr-dev-kit/ndk-mobile';
|
|
||||||
import { useNDK } from './useNDK';
|
import { useNDK } from './useNDK';
|
||||||
|
|
||||||
interface UseSubscribeOptions {
|
interface UseSubscribeOptions extends Partial<NDKSubscriptionOptions> {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
closeOnEose?: boolean;
|
|
||||||
deduplicate?: 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(
|
export function useSubscribe(
|
||||||
filters: NDKFilter | NDKFilter[] | false,
|
filters: NDKFilter[] | false,
|
||||||
options: UseSubscribeOptions = {}
|
options: UseSubscribeOptions = {}
|
||||||
) {
|
) {
|
||||||
const { ndk } = useNDK();
|
const { ndk } = useNDK();
|
||||||
@ -31,40 +22,46 @@ export function useSubscribe(
|
|||||||
const {
|
const {
|
||||||
enabled = true,
|
enabled = true,
|
||||||
closeOnEose = false,
|
closeOnEose = false,
|
||||||
deduplicate = true
|
deduplicate = true,
|
||||||
|
...subscriptionOptions
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
useEffect(() => {
|
// Function to clear all events
|
||||||
// Clean up previous subscription if exists
|
const clearEvents = useCallback(() => {
|
||||||
|
setEvents([]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Function to manually resubscribe
|
||||||
|
const resubscribe = useCallback(() => {
|
||||||
if (subscriptionRef.current) {
|
if (subscriptionRef.current) {
|
||||||
subscriptionRef.current.stop();
|
subscriptionRef.current.stop();
|
||||||
subscriptionRef.current = null;
|
subscriptionRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset state when filters change
|
|
||||||
setEvents([]);
|
setEvents([]);
|
||||||
setEose(false);
|
setEose(false);
|
||||||
|
setIsLoading(true);
|
||||||
// Check prerequisites
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
if (!ndk || !filters || !enabled) {
|
if (!ndk || !filters || !enabled) {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
setEose(false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Convert single filter to array if needed
|
// Create subscription with NDK Mobile
|
||||||
const filterArray = Array.isArray(filters) ? filters : [filters];
|
const subscription = ndk.subscribe(filters, {
|
||||||
|
closeOnEose,
|
||||||
|
...subscriptionOptions
|
||||||
|
});
|
||||||
|
|
||||||
// Create subscription
|
|
||||||
const subscription = ndk.subscribe(filterArray);
|
|
||||||
subscriptionRef.current = subscription;
|
subscriptionRef.current = subscription;
|
||||||
|
|
||||||
// Handle incoming events
|
|
||||||
subscription.on('event', (event: NDKEvent) => {
|
subscription.on('event', (event: NDKEvent) => {
|
||||||
setEvents(prev => {
|
setEvents(prev => {
|
||||||
// Deduplicate events if enabled
|
|
||||||
if (deduplicate && prev.some(e => e.id === event.id)) {
|
if (deduplicate && prev.some(e => e.id === event.id)) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
@ -72,15 +69,9 @@ export function useSubscribe(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle end of stored events
|
|
||||||
subscription.on('eose', () => {
|
subscription.on('eose', () => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setEose(true);
|
setEose(true);
|
||||||
|
|
||||||
if (closeOnEose && subscriptionRef.current) {
|
|
||||||
subscriptionRef.current.stop();
|
|
||||||
subscriptionRef.current = null;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[useSubscribe] Error:', error);
|
console.error('[useSubscribe] Error:', error);
|
||||||
@ -94,20 +85,14 @@ export function useSubscribe(
|
|||||||
subscriptionRef.current = null;
|
subscriptionRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [ndk, enabled, closeOnEose, deduplicate, JSON.stringify(filters)]);
|
}, [ndk, enabled, closeOnEose, JSON.stringify(filters), JSON.stringify(subscriptionOptions)]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
events,
|
events,
|
||||||
isLoading,
|
isLoading,
|
||||||
eose,
|
eose,
|
||||||
resubscribe: () => {
|
clearEvents,
|
||||||
if (subscriptionRef.current) {
|
resubscribe,
|
||||||
subscriptionRef.current.stop();
|
subscription: subscriptionRef.current
|
||||||
subscriptionRef.current = null;
|
|
||||||
}
|
|
||||||
setEvents([]);
|
|
||||||
setEose(false);
|
|
||||||
setIsLoading(true);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
@ -2,25 +2,21 @@
|
|||||||
import 'react-native-get-random-values'; // This must be the first import
|
import 'react-native-get-random-values'; // This must be the first import
|
||||||
import NDK, { NDKCacheAdapterSqlite } from '@nostr-dev-kit/ndk-mobile';
|
import NDK, { NDKCacheAdapterSqlite } from '@nostr-dev-kit/ndk-mobile';
|
||||||
import * as SecureStore from 'expo-secure-store';
|
import * as SecureStore from 'expo-secure-store';
|
||||||
import { NDKMobilePrivateKeySigner } from './mobile-signer';
|
|
||||||
|
|
||||||
// Constants for SecureStore
|
// Use the same default relays you have in your current implementation
|
||||||
const PRIVATE_KEY_STORAGE_KEY = 'nostr_privkey';
|
|
||||||
|
|
||||||
// Default relays
|
|
||||||
const DEFAULT_RELAYS = [
|
const DEFAULT_RELAYS = [
|
||||||
'wss://powr.duckdns.org',
|
'wss://powr.duckdns.org',
|
||||||
'wss://relay.damus.io',
|
'wss://relay.damus.io',
|
||||||
'wss://relay.nostr.band',
|
'wss://relay.nostr.band',
|
||||||
|
'wss://purplepag.es',
|
||||||
'wss://nos.lol'
|
'wss://nos.lol'
|
||||||
];
|
];
|
||||||
|
|
||||||
export async function initializeNDK() {
|
export async function initializeNDK() {
|
||||||
console.log('Initializing NDK with mobile adapter...');
|
console.log('Initializing NDK with mobile adapter...');
|
||||||
|
|
||||||
// Create a mobile-specific cache adapter with a valid maxSize
|
// Create a mobile-specific cache adapter
|
||||||
// The error shows maxSize must be greater than 0
|
const cacheAdapter = new NDKCacheAdapterSqlite('powr', 1000);
|
||||||
const cacheAdapter = new NDKCacheAdapterSqlite('powr', 1000); // Use 1000 as maxSize
|
|
||||||
|
|
||||||
// Initialize NDK with mobile-specific options
|
// Initialize NDK with mobile-specific options
|
||||||
const ndk = new NDK({
|
const ndk = new NDK({
|
||||||
@ -36,43 +32,5 @@ export async function initializeNDK() {
|
|||||||
// Connect to relays
|
// Connect to relays
|
||||||
await ndk.connect();
|
await ndk.connect();
|
||||||
|
|
||||||
// Set up relay status tracking
|
return { ndk };
|
||||||
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 };
|
|
||||||
}
|
}
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,25 +1,24 @@
|
|||||||
// lib/stores/ndk.ts
|
// 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 'react-native-get-random-values';
|
||||||
import { Platform } from 'react-native';
|
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
// Using standard NDK types but importing NDKEvent from ndk-mobile for compatibility
|
import NDK, {
|
||||||
import NDK, { NDKFilter } from '@nostr-dev-kit/ndk';
|
NDKEvent,
|
||||||
import { NDKEvent, NDKUser } from '@nostr-dev-kit/ndk-mobile';
|
NDKUser,
|
||||||
|
NDKRelay,
|
||||||
|
NDKPrivateKeySigner
|
||||||
|
} from '@nostr-dev-kit/ndk';
|
||||||
|
import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools';
|
||||||
import * as SecureStore from 'expo-secure-store';
|
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
|
// Constants for SecureStore
|
||||||
const PRIVATE_KEY_STORAGE_KEY = 'nostr_privkey';
|
const PRIVATE_KEY_STORAGE_KEY = 'nostr_privkey';
|
||||||
|
|
||||||
// Default relays
|
// Default relays
|
||||||
const DEFAULT_RELAYS = [
|
const DEFAULT_RELAYS = [
|
||||||
'wss://powr.duckdns.org', // Your primary relay
|
'wss://powr.duckdns.org',
|
||||||
'wss://relay.damus.io',
|
'wss://relay.damus.io',
|
||||||
'wss://relay.nostr.band',
|
'wss://relay.nostr.band',
|
||||||
|
'wss://purplepag.es',
|
||||||
'wss://nos.lol'
|
'wss://nos.lol'
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -29,7 +28,7 @@ type NDKStoreState = {
|
|||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
error: Error | null;
|
error: Error | null;
|
||||||
relayStatus: Record<string, 'connected' | 'connecting' | 'disconnected' | 'error'>;
|
relayStatus: Record<string, 'connected' | 'connecting' | 'disconnected'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type NDKStoreActions = {
|
type NDKStoreActions = {
|
||||||
@ -38,14 +37,27 @@ type NDKStoreActions = {
|
|||||||
logout: () => Promise<void>;
|
logout: () => Promise<void>;
|
||||||
generateKeys: () => { privateKey: string; publicKey: string; nsec: string; npub: string };
|
generateKeys: () => { privateKey: string; publicKey: string; nsec: string; npub: string };
|
||||||
publishEvent: (kind: number, content: string, tags: string[][]) => Promise<NDKEvent | null>;
|
publishEvent: (kind: number, content: string, tags: string[][]) => Promise<NDKEvent | null>;
|
||||||
createEvent: (kind: number, content: string, tags: string[][]) => Promise<NDKEvent | null>;
|
fetchUserProfile: (pubkey: string) => Promise<NDKUser | null>;
|
||||||
queueEventForPublishing: (event: NDKEvent) => Promise<boolean>;
|
fetchEventsByFilter: (filter: any) => Promise<NDKEvent[]>;
|
||||||
processPublicationQueue: () => Promise<void>;
|
|
||||||
fetchEventsByFilter: (filter: NDKFilter) => 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) => ({
|
export const useNDKStore = create<NDKStoreState & NDKStoreActions>((set, get) => ({
|
||||||
// State properties
|
|
||||||
ndk: null,
|
ndk: null,
|
||||||
currentUser: null,
|
currentUser: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@ -53,102 +65,53 @@ export const useNDKStore = create<NDKStoreState & NDKStoreActions>((set, get) =>
|
|||||||
error: null,
|
error: null,
|
||||||
relayStatus: {},
|
relayStatus: {},
|
||||||
|
|
||||||
// Initialize NDK
|
|
||||||
init: async () => {
|
init: async () => {
|
||||||
try {
|
try {
|
||||||
console.log('[NDK] Initializing...');
|
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 });
|
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
|
// Initialize NDK with relays
|
||||||
const ndk = new NDK({
|
const ndk = new NDK({
|
||||||
explicitRelayUrls: DEFAULT_RELAYS
|
explicitRelayUrls: DEFAULT_RELAYS
|
||||||
});
|
});
|
||||||
|
|
||||||
// Connect to relays
|
// Setup relay status tracking
|
||||||
await ndk.connect();
|
const relayStatus: Record<string, 'connected' | 'connecting' | 'disconnected'> = {};
|
||||||
|
|
||||||
// Setup relay status updates
|
|
||||||
DEFAULT_RELAYS.forEach(url => {
|
DEFAULT_RELAYS.forEach(url => {
|
||||||
const relay = ndk.pool.getRelay(url);
|
relayStatus[url] = 'connecting';
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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
|
// Check for saved private key
|
||||||
const privateKey = await SecureStore.getItemAsync(PRIVATE_KEY_STORAGE_KEY);
|
const privateKeyHex = await SecureStore.getItemAsync(PRIVATE_KEY_STORAGE_KEY);
|
||||||
if (privateKey) {
|
if (privateKeyHex) {
|
||||||
console.log('[NDK] Found saved private key, initializing signer');
|
console.log('[NDK] Found saved private key, initializing signer');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create mobile-specific signer with private key
|
await get().login(privateKeyHex);
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[NDK] Error initializing with saved key:', error);
|
console.error('[NDK] Error initializing with saved key:', error);
|
||||||
// Remove invalid key
|
// 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 });
|
set({ isLoading: false });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[NDK] Initialization error:', 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 });
|
set({ isLoading: true, error: null });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -196,14 +139,28 @@ export const useNDKStore = create<NDKStoreState & NDKStoreActions>((set, get) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If no private key is provided, generate one
|
// If no private key is provided, generate one
|
||||||
let userPrivateKey = privateKey;
|
let privateKeyHex = privateKeyInput;
|
||||||
if (!userPrivateKey) {
|
if (!privateKeyHex) {
|
||||||
const { privateKey: generatedKey } = get().generateKeys();
|
const { privateKey } = get().generateKeys();
|
||||||
userPrivateKey = generatedKey;
|
privateKeyHex = privateKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create mobile-specific signer with private key
|
// Handle nsec format
|
||||||
const signer = new NDKMobilePrivateKeySigner(userPrivateKey);
|
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;
|
ndk.signer = signer;
|
||||||
|
|
||||||
// Get user
|
// Get user
|
||||||
@ -225,8 +182,8 @@ export const useNDKStore = create<NDKStoreState & NDKStoreActions>((set, get) =>
|
|||||||
console.log('[NDK] User profile loaded:', user.profile);
|
console.log('[NDK] User profile loaded:', user.profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the private key securely
|
// Save the private key hex string securely
|
||||||
await SecureStore.setItemAsync(PRIVATE_KEY_STORAGE_KEY, userPrivateKey);
|
await SecureStore.setItemAsync(PRIVATE_KEY_STORAGE_KEY, privateKeyHex);
|
||||||
|
|
||||||
set({
|
set({
|
||||||
currentUser: user,
|
currentUser: user,
|
||||||
@ -270,7 +227,25 @@ export const useNDKStore = create<NDKStoreState & NDKStoreActions>((set, get) =>
|
|||||||
|
|
||||||
generateKeys: () => {
|
generateKeys: () => {
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
console.error('[NDK] Error generating keys:', error);
|
console.error('[NDK] Error generating keys:', error);
|
||||||
set({ error: error instanceof Error ? error : new Error('Failed to generate keys') });
|
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[][]) => {
|
publishEvent: async (kind: number, content: string, tags: string[][]) => {
|
||||||
try {
|
try {
|
||||||
const { ndk, isAuthenticated, currentUser } = get();
|
const { ndk, isAuthenticated, currentUser } = get();
|
||||||
@ -300,276 +266,54 @@ export const useNDKStore = create<NDKStoreState & NDKStoreActions>((set, get) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create event
|
// Create event
|
||||||
console.log('Creating event...');
|
|
||||||
const event = new NDKEvent(ndk);
|
const event = new NDKEvent(ndk);
|
||||||
event.kind = kind;
|
event.kind = kind;
|
||||||
event.content = content;
|
event.content = content;
|
||||||
event.tags = tags;
|
event.tags = tags;
|
||||||
|
|
||||||
// MONKEY PATCHING APPROACH:
|
// Sign and publish
|
||||||
// This is needed because the standard NDK doesn't properly work with
|
await event.sign();
|
||||||
// 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...');
|
|
||||||
await event.publish();
|
await event.publish();
|
||||||
|
|
||||||
console.log('Event published successfully:', event.id);
|
console.log('Event published successfully:', event.id);
|
||||||
return event;
|
return event;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error publishing event:', 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') });
|
set({ error: error instanceof Error ? error : new Error('Failed to publish event') });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Create and sign a Nostr event without publishing it
|
// Fetch profile for any user
|
||||||
createEvent: async (kind: number, content: string, tags: string[][]): Promise<NDKEvent | null> => {
|
fetchUserProfile: async (pubkey: string) => {
|
||||||
try {
|
try {
|
||||||
const { ndk, isAuthenticated, currentUser } = get();
|
const { ndk } = get();
|
||||||
|
|
||||||
if (!ndk) {
|
if (!ndk) {
|
||||||
throw new Error('NDK not initialized');
|
throw new Error('NDK not initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAuthenticated || !currentUser) {
|
const user = ndk.getUser({ pubkey });
|
||||||
throw new Error('Not authenticated');
|
await user.fetchProfile();
|
||||||
}
|
|
||||||
|
|
||||||
// Create event
|
return user;
|
||||||
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;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating event:', error);
|
console.error('Error fetching user profile:', error);
|
||||||
set({ error: error instanceof Error ? error : new Error('Failed to create event') });
|
set({ error: error instanceof Error ? error : new Error('Failed to fetch user profile') });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Queue an event for publishing when online
|
// Fetch events by filter
|
||||||
queueEventForPublishing: async (event: NDKEvent): Promise<boolean> => {
|
fetchEventsByFilter: async (filter: any) => {
|
||||||
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> => {
|
|
||||||
try {
|
try {
|
||||||
const { ndk } = get();
|
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) {
|
if (!ndk) {
|
||||||
throw new Error('NDK not initialized');
|
throw new Error('NDK not initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch events
|
// Fetch events using NDK
|
||||||
const events = await ndk.fetchEvents(filter);
|
const events = await ndk.fetchEvents(filter);
|
||||||
|
|
||||||
// Convert Set to Array
|
|
||||||
return Array.from(events);
|
return Array.from(events);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching events:', error);
|
console.error('Error fetching events:', error);
|
||||||
@ -577,4 +321,39 @@ export const useNDKStore = create<NDKStoreState & NDKStoreActions>((set, get) =>
|
|||||||
return [];
|
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
|
||||||
|
}));
|
||||||
|
}
|
@ -19,6 +19,8 @@ import type {
|
|||||||
} from '@/types/templates';
|
} from '@/types/templates';
|
||||||
import type { BaseExercise } from '@/types/exercise';
|
import type { BaseExercise } from '@/types/exercise';
|
||||||
import { openDatabaseSync } from 'expo-sqlite';
|
import { openDatabaseSync } from 'expo-sqlite';
|
||||||
|
import { FavoritesService } from '@/lib/db/services/FavoritesService';
|
||||||
|
|
||||||
|
|
||||||
const AUTO_SAVE_INTERVAL = 30000; // 30 seconds
|
const AUTO_SAVE_INTERVAL = 30000; // 30 seconds
|
||||||
|
|
||||||
@ -500,27 +502,12 @@ const useWorkoutStoreBase = create<ExtendedWorkoutState & ExtendedWorkoutActions
|
|||||||
// Favorite Management with SQLite persistence - IMPROVED VERSION
|
// Favorite Management with SQLite persistence - IMPROVED VERSION
|
||||||
loadFavorites: async () => {
|
loadFavorites: async () => {
|
||||||
try {
|
try {
|
||||||
|
// Get the favorites service through a local import trick since we can't use hooks here
|
||||||
const db = openDatabaseSync('powr.db');
|
const db = openDatabaseSync('powr.db');
|
||||||
|
const favoritesService = new FavoritesService(db);
|
||||||
|
|
||||||
// Ensure favorites table exists
|
// Load just the IDs
|
||||||
await db.execAsync(`
|
const favoriteIds = await favoritesService.getFavoriteIds('template');
|
||||||
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);
|
|
||||||
|
|
||||||
set({
|
set({
|
||||||
favoriteIds,
|
favoriteIds,
|
||||||
@ -528,7 +515,6 @@ const useWorkoutStoreBase = create<ExtendedWorkoutState & ExtendedWorkoutActions
|
|||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Loaded ${favoriteIds.length} favorite IDs from database`);
|
console.log(`Loaded ${favoriteIds.length} favorite IDs from database`);
|
||||||
// Don't return anything (void)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading favorites:', error);
|
console.error('Error loading favorites:', error);
|
||||||
set({ favoritesLoaded: true }); // Mark as loaded even on error
|
set({ favoritesLoaded: true }); // Mark as loaded even on error
|
||||||
@ -548,52 +534,24 @@ const useWorkoutStoreBase = create<ExtendedWorkoutState & ExtendedWorkoutActions
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query the full content from SQLite
|
|
||||||
try {
|
try {
|
||||||
const db = openDatabaseSync('powr.db');
|
const db = openDatabaseSync('powr.db');
|
||||||
|
const favoritesService = new FavoritesService(db);
|
||||||
|
|
||||||
// Generate placeholders for the SQL query
|
return await favoritesService.getFavorites('template');
|
||||||
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
|
|
||||||
}));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching favorites content:', error);
|
console.error('Error fetching favorites content:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
addFavorite: async (template: WorkoutTemplate) => {
|
addFavorite: async (template: WorkoutTemplate) => {
|
||||||
try {
|
try {
|
||||||
const db = openDatabaseSync('powr.db');
|
const db = openDatabaseSync('powr.db');
|
||||||
const now = Date.now();
|
const favoritesService = new FavoritesService(db);
|
||||||
|
|
||||||
// Add to SQLite database
|
// Add to favorites database
|
||||||
await db.runAsync(
|
await favoritesService.addFavorite('template', template.id, template);
|
||||||
`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
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update just the ID in memory state
|
// Update just the ID in memory state
|
||||||
set(state => {
|
set(state => {
|
||||||
@ -610,16 +568,14 @@ const useWorkoutStoreBase = create<ExtendedWorkoutState & ExtendedWorkoutActions
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
removeFavorite: async (templateId: string) => {
|
removeFavorite: async (templateId: string) => {
|
||||||
try {
|
try {
|
||||||
const db = openDatabaseSync('powr.db');
|
const db = openDatabaseSync('powr.db');
|
||||||
|
const favoritesService = new FavoritesService(db);
|
||||||
|
|
||||||
// Remove from SQLite database
|
// Remove from favorites database
|
||||||
await db.runAsync(
|
await favoritesService.removeFavorite('template', templateId);
|
||||||
`DELETE FROM favorites WHERE content_type = 'template' AND content_id = ?`,
|
|
||||||
[templateId]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update IDs in memory state
|
// Update IDs in memory state
|
||||||
set(state => ({
|
set(state => ({
|
||||||
|
Loading…
x
Reference in New Issue
Block a user