fixed infinite loop in local/all workout toggle in history view

This commit is contained in:
DocNR 2025-03-28 07:24:09 -07:00
parent e81012841f
commit 08bb9884bc
17 changed files with 91 additions and 7688 deletions

View File

@ -139,6 +139,16 @@ export default function HistoryScreen() {
const [includeNostr, setIncludeNostr] = useState(true);
const [isLoginSheetOpen, setIsLoginSheetOpen] = useState(false);
// Create memoized filters to prevent recreation on every render
const filters = React.useMemo(() => {
if (includeNostr) {
return undefined;
} else {
// Explicitly type the array to match WorkoutFilters interface
return { source: ['local' as const] };
}
}, [includeNostr]);
// Use the unified workout history hook
const {
workouts: allWorkouts,
@ -148,29 +158,30 @@ export default function HistoryScreen() {
error
} = useWorkoutHistory({
includeNostr,
filters: includeNostr ? undefined : { source: ['local'] },
filters,
realtime: true
});
// Set workouts from the hook
// Set workouts from the hook with more careful state updates
useEffect(() => {
if (loading) {
setIsLoading(true);
return; // Exit early to avoid multiple state updates in one effect
}
setIsLoading(false);
setRefreshing(false);
// Check if we need to use mock data (empty workouts)
if (allWorkouts.length === 0 && !error) {
console.log('No workouts found, using mock data');
setWorkouts(mockWorkouts);
setUseMockData(true);
} else {
setWorkouts(allWorkouts);
setIsLoading(false);
setRefreshing(false);
// Check if we need to use mock data (empty workouts)
if (allWorkouts.length === 0 && !error) {
console.log('No workouts found, using mock data');
setWorkouts(mockWorkouts);
setUseMockData(true);
} else {
setUseMockData(false);
}
setUseMockData(false);
}
}, [allWorkouts, loading, error]);
}, [allWorkouts, loading, error, mockWorkouts]);
// Pull to refresh handler
const onRefresh = React.useCallback(() => {
@ -231,23 +242,54 @@ export default function HistoryScreen() {
)}
{isAuthenticated && (
<View className="flex-row justify-end mb-4">
<Pressable
onPress={() => setIncludeNostr(!includeNostr)}
style={{
backgroundColor: includeNostr ? primaryBgColor : mutedBgColor,
paddingHorizontal: 12,
paddingVertical: 4,
borderRadius: 9999,
}}
>
<Text style={{
color: includeNostr ? primaryTextColor : mutedTextColor,
fontSize: 14,
}}>
{includeNostr ? 'Showing All Workouts' : 'Local Workouts Only'}
<View className="mb-4">
<View className="flex-row justify-between items-center">
<Text className="text-foreground text-sm font-medium">
Workout Source
</Text>
</Pressable>
<View className="flex-row border border-border rounded-full overflow-hidden">
<Pressable
onPress={() => setIncludeNostr(true)}
style={{
backgroundColor: includeNostr ? primaryBgColor : 'transparent',
paddingHorizontal: 12,
paddingVertical: 6,
}}
>
<Text style={{
color: includeNostr ? primaryTextColor : mutedTextColor,
fontSize: 14,
fontWeight: includeNostr ? '600' : '400',
}}>
All Workouts
</Text>
</Pressable>
<Pressable
onPress={() => setIncludeNostr(false)}
style={{
backgroundColor: !includeNostr ? primaryBgColor : 'transparent',
paddingHorizontal: 12,
paddingVertical: 6,
}}
>
<Text style={{
color: !includeNostr ? primaryTextColor : mutedTextColor,
fontSize: 14,
fontWeight: !includeNostr ? '600' : '400',
}}>
Local Only
</Text>
</Pressable>
</View>
</View>
<Text className="text-muted-foreground text-xs mt-1">
{includeNostr
? 'Showing all workouts from Nostr and your local device'
: 'Only showing workouts saved on this device'}
</Text>
</View>
)}

File diff suppressed because it is too large Load Diff

View File

@ -1,261 +0,0 @@
# NDK Functions for Hex Keys and NIP-19 Encoding/Decoding
When working with Nostr addresses (like naddr) and converting between hex and other formats, NDK provides several key functions. Here's a comprehensive overview of the main functions you'll need for handling hex keys and NIP-19 encoding/decoding in your POWR Pack implementation:
## Core NIP-19 Functions
NDK implements NIP-19 functionality in the `events/nip19.ts` file. The key functions you'll need are:
### 1. Decoding NIP-19 Entities
```typescript
import { nip19 } from '@nostr-dev-kit/ndk';
// Decode any NIP-19 entity (naddr, npub, nsec, note, etc.)
function decodeNaddr(naddrString: string) {
try {
const decoded = nip19.decode(naddrString);
// For naddr specifically, you'll get:
if (decoded.type === 'naddr') {
const { pubkey, kind, identifier } = decoded.data;
// pubkey is the hex public key of the author
// kind is the event kind (30004 for lists)
// identifier is the 'd' tag value
console.log('Hex pubkey:', pubkey);
console.log('Event kind:', kind);
console.log('Identifier:', identifier);
return decoded.data;
}
return null;
} catch (error) {
console.error('Invalid NIP-19 format:', error);
return null;
}
}
```
### 2. Encoding to NIP-19 Formats
```typescript
// Create an naddr from components
function createNaddr(pubkey: string, kind: number, identifier: string) {
return nip19.naddrEncode({
pubkey, // Hex pubkey
kind, // Event kind (number)
identifier // The 'd' tag value
});
}
// Create an npub from a hex public key
function hexToNpub(hexPubkey: string) {
return nip19.npubEncode(hexPubkey);
}
// Create a note (event reference) from event ID
function eventIdToNote(eventId: string) {
return nip19.noteEncode(eventId);
}
```
### 3. Utility Functions for Hex Keys
```typescript
// Convert npub to hex pubkey
function npubToHex(npub: string) {
try {
const decoded = nip19.decode(npub);
if (decoded.type === 'npub') {
return decoded.data as string; // This is the hex pubkey
}
return null;
} catch (error) {
console.error('Invalid npub format:', error);
return null;
}
}
// Check if a string is a valid hex key (pubkey or event id)
function isValidHexKey(hexString: string) {
return /^[0-9a-f]{64}$/i.test(hexString);
}
```
## Using NIP-19 Functions with NDK Filters
Here's how you would use these functions with NDK filters to fetch a POWR Pack from an naddr:
```typescript
async function fetchPackFromNaddr(naddr: string) {
try {
// Decode the naddr to get event coordinates
const decoded = nip19.decode(naddr);
if (decoded.type !== 'naddr') {
throw new Error('Not an naddr');
}
const { pubkey, kind, identifier } = decoded.data;
// Ensure it's a list (kind 30004)
if (kind !== 30004) {
throw new Error('Not a NIP-51 list');
}
// Create a filter to fetch the specific list event
const filter = {
kinds: [kind],
authors: [pubkey], // Using the hex pubkey from the naddr
'#d': identifier ? [identifier] : undefined, // Using the d-tag if available
};
// Fetch the event
const events = await ndk.fetchEvents(filter);
if (events.size === 0) {
throw new Error('Pack not found');
}
// Get the first matching event
return Array.from(events)[0];
} catch (error) {
console.error('Error fetching pack:', error);
throw error;
}
}
```
## Implementing the Complete naddr Workflow for POWR Packs
Here's a complete example for fetching and processing a POWR Pack from an naddr:
```typescript
import NDK, { NDKEvent, NDKFilter, nip19 } from '@nostr-dev-kit/ndk';
async function fetchAndProcessPOWRPack(naddr: string) {
// 1. Initialize NDK
const ndk = new NDK({
explicitRelayUrls: [
'wss://relay.damus.io',
'wss://relay.nostr.band'
]
});
await ndk.connect();
// 2. Decode the naddr
const decoded = nip19.decode(naddr);
if (decoded.type !== 'naddr') {
throw new Error('Invalid naddr format');
}
const { pubkey, kind, identifier } = decoded.data;
// 3. Create filter to fetch the pack event
const packFilter: NDKFilter = {
kinds: [kind],
authors: [pubkey],
'#d': identifier ? [identifier] : undefined
};
// 4. Fetch the pack event
const packEvents = await ndk.fetchEvents(packFilter);
if (packEvents.size === 0) {
throw new Error('Pack not found');
}
const packEvent = Array.from(packEvents)[0];
// 5. Extract template and exercise references
const templateRefs: string[] = [];
const exerciseRefs: string[] = [];
for (const tag of packEvent.tags) {
if (tag[0] === 'a') {
const addressPointer = tag[1];
// Format is kind:pubkey:d-tag
if (addressPointer.startsWith('33402:')) { // Workout template
templateRefs.push(addressPointer);
} else if (addressPointer.startsWith('33401:')) { // Exercise
exerciseRefs.push(addressPointer);
}
}
}
// 6. Fetch templates and exercises
const templates = await fetchReferencedEvents(ndk, templateRefs);
const exercises = await fetchReferencedEvents(ndk, exerciseRefs);
// 7. Return the complete pack data
return {
pack: packEvent,
templates,
exercises
};
}
// Helper function to fetch events from address pointers
async function fetchReferencedEvents(ndk: NDK, addressPointers: string[]) {
const events: NDKEvent[] = [];
for (const pointer of addressPointers) {
// Parse the pointer (kind:pubkey:d-tag)
const [kindStr, hexPubkey, dTag] = pointer.split(':');
const kind = parseInt(kindStr);
// Create a filter to find this specific event
const filter: NDKFilter = {
kinds: [kind],
authors: [hexPubkey]
};
if (dTag) {
filter['#d'] = [dTag];
}
// Fetch the events
const fetchedEvents = await ndk.fetchEvents(filter);
events.push(...Array.from(fetchedEvents));
}
return events;
}
```
## Creating naddr for Sharing Packs
If you want to generate an naddr that can be shared to allow others to import your POWR Pack:
```typescript
function createShareableNaddr(packEvent: NDKEvent) {
// Extract the d-tag (identifier)
const dTags = packEvent.getMatchingTags('d');
const identifier = dTags[0]?.[1] || '';
// Create the naddr
const naddr = nip19.naddrEncode({
pubkey: packEvent.pubkey,
kind: packEvent.kind,
identifier
});
return naddr;
}
```
## Best Practices for Working with NIP-19 Formats
1. **Always validate decoded values**: Check that the decoded data is of the expected type and has the necessary properties.
2. **Handle encoding/decoding errors**: These functions can throw exceptions if the input is malformed.
3. **Normalize hex keys**: Convert to lowercase for consistency in filters and comparisons.
4. **Check event kinds**: Verify that the decoded event kind matches what you expect (30004 for NIP-51 lists).
5. **Use strong typing**: TypeScript's type system can help catch errors with NIP-19 data.
The main challenge when working with naddr and other NIP-19 formats is correctly translating between the human-readable encoded forms and the internal hex representations needed for Nostr protocol operations. NDK's nip19 module abstracts this complexity for you, allowing you to focus on the core business logic of your application.

View File

@ -1,204 +0,0 @@
# ARCHIVED: This document has moved
**NOTE:** This document has been migrated to [docs/project/mvp_and_rebuild.md](../project/mvp_and_rebuild.md) as part of the documentation reorganization.
# POWR App Roadmap - MVP and Social Rebuild
## MVP Definition
The Minimum Viable Product (MVP) will focus on core functionality while simplifying social features:
### Core Features (MVP Priority)
- Complete workout tracking and history
- Exercise library and template management
- POWR Pack support
- Basic Nostr integration:
- Ability to publish kind 1301 workout records
- Ability to share workouts with kind 1 notes (quoting 1301 records)
- NIP-89 compliance for app identification
### Simplified Social Implementation
- Social tab with "Coming Soon" placeholder or minimal POWR official feed
- Profile tab with limited social activity display
- Workout sharing from completion flow (with simplified UI)
- Add workout sharing from history tab
## Current Technical Challenges
### Authentication Issues
- Inconsistent auth state management causing cascading problems
- Logout process triggering uncoordinated state changes
### Subscription Management Problems
- Subscription lifecycle not properly managed
- Resources not being cleaned up consistently
### React Hook Implementation
- "Rendered fewer hooks than expected" errors
- Component lifecycle hook management issues
### Component Coupling
- Tight interdependencies between components
- Difficulty isolating fixes for individual components
## Implementation Phases
### Phase 1: MVP Stabilization (Current Focus)
- Implement fundamental architecture improvements:
- Authentication state management with clear lifecycle hooks
- Basic subscription management improvements
- Simplify or disable problematic social features
- Add workout sharing from history tab
- Ensure stable workout tracking, history, and template management
- Fix critical bugs in core functionality
### Phase 2: Social Foundation Rebuild (Post-MVP)
- Complete targeted rebuild of authentication and subscription management
- Implement proper data layer with caching
- Create clear separation between data and UI layers
- Develop and test in parallel with MVP branch
### Phase 3: Social Feature Re-implementation
- Gradually re-enable social features using new architecture
- Start with simplest screens (e.g., official POWR feed)
- Progress to more complex screens (Following, Global)
- Implement enhanced profile activity view
### Phase 4: Extended Features
- Amber integration for Android users
- Enhanced social features beyond original implementation
- Additional Nostr integrations and social capabilities
## Architecture Design
### 1. Authentication State Management
- Implementation of a proper state machine pattern
- Clear transitions: unauthenticated → authenticating → authenticated → deauthenticating
- Use of Zustand store (aligned with current workoutStore approach)
- Event listeners/callbacks for components to respond to auth changes
### 2. Subscription Management
- Centralized service for managing subscriptions
- Automatic tracking and cleanup of subscriptions
- Integration with component lifecycle
- Rate limiting and cooldown mechanisms
### 3. Data Layer Design
- Clean separation between data fetching and UI components
- Proper caching with expiration policies
- Offline support strategy
- Clear interfaces for data services
### 4. UI Component Structure
- Consistent component patterns across social features
- Proper error boundaries and loading states
- Better separation of concerns between components
- Rebuilt social feed components with cleaner architecture
## Git and Release Strategy
### Branch Strategy
- Create `mvp` branch from current state
- Implement MVP simplifications and critical fixes in this branch
- In parallel, start architecture rebuild in `social-rebuild` branch
- Once MVP is released, gradually merge rebuilt components from `social-rebuild` to `main`
### Feature Flag Implementation
- Add configuration system for feature toggling
- Create conditional rendering for social features
- Define clear interfaces between components to allow swapping implementations
- Store feature flag state in persistent storage for consistency across app launches
### Release Plan
1. iOS TestFlight (MVP)
2. Implement Amber integration and final Android preparations
3. Android Google Play / APK release
4. Gradual social feature re-enablement through app updates
## Key Files to Modify
### MVP Initial Changes
- `app/(tabs)/social/_layout.tsx` - Add "Coming Soon" placeholder or simplified view
- `components/workout/WorkoutCompletionFlow.tsx` - Ensure sharing functionality is stable
- `lib/db/services/NostrWorkoutService.ts` - Review for stability and proper NIP-89 implementation
- `app/(tabs)/history/workoutHistory.tsx` - Add sharing capability
### Core Architecture Improvements
- `lib/stores/ndk.ts` - Enhance with better auth management
- `lib/hooks/useNDK.ts` - Refactor for more predictable state management
- `components/RelayInitializer.tsx` - Review for subscription management issues
- `lib/hooks/useSubscribe.ts` - Improve subscription lifecycle management
### Future Rebuild Targets (Post-MVP)
- `lib/hooks/useSocialFeed.ts` - Replace with new service
- `lib/social/socialFeedService.ts` - Refactor with cleaner architecture
- `app/(tabs)/social/*` - Rebuild social feed screens with new architecture
- `components/social/*` - Rebuild social components with consistent patterns
## Development Timeline
### 1. Architecture Design: 2-3 days
- Create detailed service interfaces
- Design state management approach
- Document component lifecycle integration
### 2. Core Service Implementation: 3-5 days
- Build authentication manager
- Implement subscription manager
- Create data fetching services
### 3. UI Component Rebuild: 5-7 days
- Rebuild one screen at a time
- Implement with new architectural patterns
- Add comprehensive error handling
### 4. Testing and Integration: 2-3 days
- Test with various network conditions
- Verify authentication edge cases
- Confirm subscription cleanup
### 5. Cleanup and Documentation: 1-2 days
- Remove deprecated code
- Document new architecture
- Create developer onboarding guide
## Risk Mitigation
- Implement feature flags to toggle between old and new implementations
- Add enhanced logging during transition
- Create robust error boundaries to prevent cascade failures
- Maintain backward compatibility for core APIs during migration
## Original Requirements and Questions
### Simplified MVP Social Experience
- Minimal or no social feed
- Replace social tab with "Coming Soon" placeholder
- Focus on core functionality:
- Allow users to post kind 1 notes quoting 1301 workout records
- Publishing workflow:
1. User performs workout
2. User completes workout and opts to share publicly
3. User edits pre-populated kind 1 note and submits
4. App publishes kind 1301 workout record, then publishes kind 1 note quoting the record
5. Result: kind 1 note published to socials, kind 1301 record visible in workout history
- Implement NIP-89 for app identification in published records
### Key Questions Addressed
#### Impact on Workout History Functionality
The targeted rebuild approach preserves workout history functionality by focusing primarily on problematic social components. Core authentication and subscription management improvements will benefit the entire app without disrupting workflow.
#### MVP Architecture Requirements
For a stable MVP with limited social features, we recommend implementing the fundamental Authentication state management and Subscription Management components. These are foundational and will improve stability across all features that use Nostr integration.
#### Caching Services
Existing caching for user metadata can likely be preserved with clearer interfaces. For the MVP, we can simplify how these caches are used rather than fully rebuilding them.
#### Workout History Sharing
Adding the ability to share workouts from the history tab would be valuable and consistent with the completion flow sharing functionality. This will require a review of local vs. Nostr event strategies.
#### Amber Integration
Amber integration should be prioritized after the initial iOS TestFlight release but before wider Android distribution.
#### Git Strategy
Creating an `mvp` branch from the current state makes sense for the MVP implementation. The feature flag approach will allow gradual introduction of rebuilt components without disrupting the user experience.

View File

@ -1,211 +0,0 @@
# POWR Pack Implementation Document
## Overview
This document outlines the implementation plan for creating a "POWR Pack" feature in the POWR fitness app. POWR Packs are shareable collections of workout templates and exercises that users can import into their app. This feature leverages the Nostr protocol (NIP-51 lists) to enable decentralized sharing of fitness content.
## Key Concepts
1. **POWR Pack**: A collection of workout templates and exercises stored as a NIP-51 list (kind 30004 "Curation set")
2. **Pack Sharing**: Packs are shared via `naddr1` links that encode references to the collection
3. **Selective Import**: Users can select which templates/exercises to import from a pack
4. **Dependency Management**: When selecting a template, all required exercises are automatically selected
## Implementation Steps
### 1. Database Schema Extensions
Add new tables to track imported packs and their contents:
```sql
-- POWR Packs table
CREATE TABLE powr_packs (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
description TEXT,
author_pubkey TEXT,
nostr_event_id TEXT,
import_date INTEGER NOT NULL
);
-- POWR Pack items table
CREATE TABLE powr_pack_items (
pack_id TEXT NOT NULL,
item_id TEXT NOT NULL,
item_type TEXT NOT NULL,
item_order INTEGER,
PRIMARY KEY (pack_id, item_id),
FOREIGN KEY (pack_id) REFERENCES powr_packs(id) ON DELETE CASCADE
);
```
### 2. New Service: POWRPackService
Create a new service in `lib/db/services/POWRPackService.ts` with these key methods:
- `fetchPackFromNaddr(naddr: string)`: Fetch a pack and its content from Nostr
- `importPack(pack, templates, exercises, selectedIds)`: Import selected items to local database
- `getImportedPacks()`: List all imported packs with metadata
- `deletePack(packId, keepItems)`: Remove a pack while optionally keeping its content
### 3. UI Components
#### Settings Integration
Add POWR Packs to the settings drawer:
- "Import POWR Pack" item
- "Manage POWR Packs" item
#### Import Flow
Create screen at `app/(packs)/import.tsx`:
- Input field for naddr
- Pack details display
- Selectable list of templates
- Selectable list of exercises with auto-selection based on template dependencies
- Import button
#### Management Interface
Create screen at `app/(packs)/manage.tsx`:
- List of imported packs
- Pack details (templates/exercises count, import date)
- Delete functionality
#### Social Discovery
Add a section to the social tab:
- Horizontal scrolling list of available packs
- Tap to view/import a pack
### 4. Routing
Configure routing in `app/(packs)/_layout.tsx`:
- Import screen as modal
- Management screen as standard page
## Technical Implementation Details
### Data Flow
1. **Pack Creation**: Exercise → Template → Pack (we've validated this flow works via NAK tests)
2. **Pack Import**:
- Decode naddr
- Fetch pack event and referenced content
- Parse Nostr events to POWR model objects
- Save selected items to database
### Dependency Management
When users select a workout template, the system will:
1. Identify all exercises referenced by the template
2. Automatically select these exercises (shown as "required")
3. Prevent deselection of required exercises
### Integration with Existing Services
- **NostrWorkoutService**: Use existing conversion methods between Nostr events and app models
- **LibraryService**: Update to query content from imported packs
- **NDK**: Use for fetching Nostr events and managing relay connections
## Sharing UI Mockups
### Import Screen
```
┌─────────────────────────────┐
│ Import POWR Pack │
├─────────────────────────────┤
│ ┌───────────────────────┐ │
│ │ naddr1... │ │
│ └───────────────────────┘ │
│ │
│ ┌─────────────┐ │
│ │ Fetch Pack │ │
│ └─────────────┘ │
│ │
│ Pack Name │
│ Description text here... │
│ │
│ Templates │
│ ┌─────────────────────────┐ │
│ │ ☑ Beginner Full Body │ │
│ │ Strength workout │ │
│ └─────────────────────────┘ │
│ │
│ Exercises │
│ ┌─────────────────────────┐ │
│ │ ☑ Squat │ │
│ │ Required by template │ │
│ └─────────────────────────┘ │
│ │
│ ┌───────────────────────┐ │
│ │ Import 3 items │ │
│ └───────────────────────┘ │
└─────────────────────────────┘
```
### Management Screen
```
┌─────────────────────────────┐
│ Manage POWR Packs │
├─────────────────────────────┤
│ ┌─────────────────────────┐ │
│ │ POWR Test Pack [🗑]│ │
│ │ A test collection... │ │
│ │ │ │
│ │ 2 templates • 2 exercises│
│ │ Imported 2 days ago │ │
│ └─────────────────────────┘ │
│ │
│ ┌─────────────────────────┐ │
│ │ Beginner Pack [🗑]│ │
│ │ For new users... │ │
│ │ │ │
│ │ 3 templates • 5 exercises│
│ │ Imported 1 week ago │ │
│ └─────────────────────────┘ │
│ │
└─────────────────────────────┘
```
### Social Discovery
```
┌─────────────────────────────┐
│ │
│ POWR Packs │
│ Discover workout collections│
│ │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │Pack1│ │Pack2│ │Pack3│ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ └─────┘ └─────┘ └─────┘ │
│ │
└─────────────────────────────┘
```
## Testing and Validation
We've successfully tested the basic Nostr event publishing flow using NAK:
1. Created exercise events (kind 33401)
2. Created template events (kind 33402) that reference the exercises
3. Created a pack event (kind 30004) that references both templates and exercises
4. Verified that all events were published and can be fetched by ID
## Implementation Timeline
1. **Database Schema Updates**: Implement new tables
2. **POWRPackService**: Create service for fetching and importing packs
3. **Settings Integration**: Add menu items to settings drawer
4. **Import UI**: Implement import screen with selection logic
5. **Management UI**: Create pack management interface
6. **Social Discovery**: Add pack discovery section to social tab
7. **Testing**: Validate full import/management flow
## Next Steps
1. Implement the database schema changes
2. Build POWRPackService
3. Create the UI components
4. Test the full feature flow
5. Consider future enhancements (creating/publishing packs from within the app)

View File

@ -1,616 +0,0 @@
# Updated POWR Pack Integration Plan
## Current Status Assessment
Based on the current implementation of POWR Packs, we've identified several issues that need to be addressed:
1. **Missing Template-Exercise Relationships**: Templates are being imported but not properly linked to their associated exercises
2. **Parameter Extraction Issues**: The system isn't correctly parsing parameters from exercise references
3. **Lack of Future Extensibility**: The current approach doesn't adequately support future changes to the NIP-4e specification
4. **Template Management**: Tools for template archiving and deletion are incomplete
## Implementation Plan
This plan outlines both immediate fixes and longer-term improvements for a more extensible architecture.
### Phase 1: Critical Fixes (Immediate)
#### 1. Fix Template-Exercise Relationship
**Problem**: Templates are imported but show 0 exercises because the references aren't correctly matched.
**Solution**:
- Update `POWRPackService.ts` to correctly parse exercise references by d-tag
- Improve the exercise matching logic to use the correct format (`33401:pubkey:d-tag`)
- Add detailed logging for troubleshooting
```typescript
// Find the corresponding imported exercise IDs
const templateExerciseIds: string[] = [];
const matchedRefs: string[] = [];
for (const ref of exerciseRefs) {
// Extract the base reference (before any parameters)
const refParts = ref.split('::');
const baseRef = refParts[0];
console.log(`Looking for matching exercise for reference: ${baseRef}`);
// Parse the reference format: kind:pubkey:d-tag
const refSegments = baseRef.split(':');
if (refSegments.length < 3) {
console.log(`Invalid reference format: ${baseRef}`);
continue;
}
const refKind = refSegments[0];
const refPubkey = refSegments[1];
const refDTag = refSegments[2];
// Find the event that matches by d-tag
const matchingEvent = exercises.find(e => {
const dTag = findTagValue(e.tags, 'd');
if (!dTag || e.pubkey !== refPubkey) return false;
const match = dTag === refDTag;
if (match) {
console.log(`Found matching event: ${e.id} with d-tag: ${dTag}`);
}
return match;
});
if (matchingEvent && exerciseIdMap.has(matchingEvent.id)) {
const localExerciseId = exerciseIdMap.get(matchingEvent.id) || '';
templateExerciseIds.push(localExerciseId);
matchedRefs.push(ref); // Keep the full reference including parameters
console.log(`Mapped Nostr event ${matchingEvent.id} to local exercise ID ${localExerciseId}`);
} else {
console.log(`No matching exercise found for reference: ${baseRef}`);
}
}
```
#### 2. Fix Parameter Extraction in NostrIntegration.ts
**Problem**: Parameter values from exercise references aren't being properly extracted.
**Solution**:
```typescript
async saveTemplateExercisesWithParams(
templateId: string,
exerciseIds: string[],
exerciseRefs: string[]
): Promise<void> {
try {
console.log(`Saving ${exerciseIds.length} exercise relationships for template ${templateId}`);
// Create template exercise records
for (let i = 0; i < exerciseIds.length; i++) {
const exerciseId = exerciseIds[i];
const templateExerciseId = generateId();
const now = Date.now();
// Get the corresponding exercise reference with parameters
const exerciseRef = exerciseRefs[i] || '';
// Parse the reference format: kind:pubkey:d-tag::sets:reps:weight
let targetSets = null;
let targetReps = null;
let targetWeight = null;
// Check if reference contains parameters
if (exerciseRef.includes('::')) {
const parts = exerciseRef.split('::');
if (parts.length > 1) {
const params = parts[1].split(':');
if (params.length > 0 && params[0]) targetSets = parseInt(params[0]) || null;
if (params.length > 1 && params[1]) targetReps = parseInt(params[1]) || null;
if (params.length > 2 && params[2]) targetWeight = parseFloat(params[2]) || null;
}
}
console.log(`Template exercise ${i}: ${exerciseId} with sets=${targetSets}, reps=${targetReps}, weight=${targetWeight}`);
await this.db.runAsync(
`INSERT INTO template_exercises
(id, template_id, exercise_id, display_order, target_sets, target_reps, target_weight, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
templateExerciseId,
templateId,
exerciseId,
i,
targetSets,
targetReps,
targetWeight,
now,
now
]
);
}
console.log(`Successfully saved all template-exercise relationships for template ${templateId}`);
} catch (error) {
console.error('Error saving template exercises with parameters:', error);
throw error;
}
}
```
#### 3. Add Template Management Functions
**Problem**: Need better tools for template archiving and deletion.
**Solution**:
- Add an `is_archived` column to templates table
- Create archive/unarchive functions
- Implement safe template removal with dependency handling
```typescript
// Schema update
await db.execAsync(`
ALTER TABLE templates ADD COLUMN is_archived BOOLEAN NOT NULL DEFAULT 0;
ALTER TABLE templates ADD COLUMN author_pubkey TEXT;
`);
// Template management functions
async archiveTemplate(id: string, archive: boolean = true): Promise<void> {
await this.db.runAsync(
'UPDATE templates SET is_archived = ? WHERE id = ?',
[archive ? 1 : 0, id]
);
}
async removeFromLibrary(id: string): Promise<void> {
await this.db.withTransactionAsync(async () => {
// Delete template-exercise relationships
await this.db.runAsync(
'DELETE FROM template_exercises WHERE template_id = ?',
[id]
);
// Delete template
await this.db.runAsync(
'DELETE FROM templates WHERE id = ?',
[id]
);
// Update powr_pack_items to mark as not imported
await this.db.runAsync(
'UPDATE powr_pack_items SET is_imported = 0 WHERE item_id = ? AND item_type = "template"',
[id]
);
});
}
```
### Phase 2: Extensibility Improvements (Short-term)
#### 1. Schema Updates for Extensibility
**Problem**: Schema is too rigid for future extensions to exercise parameters and workout types.
**Solution**:
```typescript
// Add schema update in a migration file or update schema.ts
async function addExtensibilityColumns(db: SQLiteDatabase): Promise<void> {
// Add params_json to template_exercises for extensible parameters
await db.execAsync(`
ALTER TABLE template_exercises ADD COLUMN params_json TEXT;
`);
// Add workout_type_config to templates for type-specific configurations
await db.execAsync(`
ALTER TABLE templates ADD COLUMN workout_type_config TEXT;
`);
}
```
#### 2. Flexible Parameter Extraction
**Problem**: Current parameter extraction is hardcoded for a limited set of parameters.
**Solution**:
- Create a parameter mapper service
- Implement dynamic parameter extraction based on exercise format
```typescript
class ExerciseParameterMapper {
// Extract parameters from a Nostr reference based on exercise format
static extractParameters(exerciseRef: string, formatJson?: string): Record<string, any> {
const parameters: Record<string, any> = {};
// If no reference with parameters, return empty object
if (!exerciseRef || !exerciseRef.includes('::')) {
return parameters;
}
const [baseRef, paramString] = exerciseRef.split('::');
if (!paramString) return parameters;
const paramValues = paramString.split(':');
// If we have format information, use it to map parameters
if (formatJson) {
try {
const format = JSON.parse(formatJson);
const formatKeys = Object.keys(format).filter(key => format[key] === true);
formatKeys.forEach((key, index) => {
if (index < paramValues.length && paramValues[index]) {
// Convert value to appropriate type based on parameter name
if (key === 'weight') {
parameters[key] = parseFloat(paramValues[index]) || null;
} else if (['reps', 'sets', 'duration'].includes(key)) {
parameters[key] = parseInt(paramValues[index]) || null;
} else {
// For other parameters, keep as string
parameters[key] = paramValues[index];
}
}
});
return parameters;
} catch (error) {
console.warn('Error parsing format JSON:', error);
// Fall back to default mapping below
}
}
// Default parameter mapping if no format or error parsing
if (paramValues.length > 0) parameters.target_sets = parseInt(paramValues[0]) || null;
if (paramValues.length > 1) parameters.target_reps = parseInt(paramValues[1]) || null;
if (paramValues.length > 2) parameters.target_weight = parseFloat(paramValues[2]) || null;
if (paramValues.length > 3) parameters.set_type = paramValues[3];
return parameters;
}
// Convert parameters back to Nostr reference format
static formatParameters(parameters: Record<string, any>, formatJson?: string): string {
if (!Object.keys(parameters).length) return '';
let paramArray: (string | number | null)[] = [];
// If we have format information, use it for parameter ordering
if (formatJson) {
try {
const format = JSON.parse(formatJson);
const formatKeys = Object.keys(format).filter(key => format[key] === true);
paramArray = formatKeys.map(key => parameters[key] ?? '');
} catch (error) {
console.warn('Error parsing format JSON:', error);
// Fall back to default format below
}
}
// Default parameter format if no format JSON or error parsing
if (!paramArray.length) {
paramArray = [
parameters.target_sets ?? parameters.sets ?? '',
parameters.target_reps ?? parameters.reps ?? '',
parameters.target_weight ?? parameters.weight ?? '',
parameters.set_type ?? ''
];
}
// Trim trailing empty values
while (paramArray.length > 0 &&
(paramArray[paramArray.length - 1] === '' ||
paramArray[paramArray.length - 1] === null)) {
paramArray.pop();
}
// If no parameters left, return empty string
if (!paramArray.length) return '';
// Join parameters with colon
return paramArray.join(':');
}
}
```
#### 3. Workout Type-Specific Handling
**Problem**: Different workout types (AMRAP, EMOM, circuit, strength) have specific data needs.
**Solution**:
- Create workout type processors
- Implement template service enhancements for type-specific configurations
```typescript
// WorkoutTypesService.ts
import { WorkoutTemplate, TemplateType } from '@/types/templates';
// Factory pattern for creating workout type processors
export class WorkoutTypeFactory {
static createProcessor(type: TemplateType): WorkoutTypeProcessor {
switch (type) {
case 'strength':
return new StrengthWorkoutProcessor();
case 'circuit':
return new CircuitWorkoutProcessor();
case 'emom':
return new EMOMWorkoutProcessor();
case 'amrap':
return new AMRAPWorkoutProcessor();
default:
return new DefaultWorkoutProcessor();
}
}
}
// Interface for workout type processors
export interface WorkoutTypeProcessor {
parseTemplateConfig(tags: string[][]): Record<string, any>;
getDefaultParameters(): Record<string, any>;
formatTemplateConfig(config: Record<string, any>): string[][];
}
// Example implementation for EMOM workouts
class EMOMWorkoutProcessor implements WorkoutTypeProcessor {
parseTemplateConfig(tags: string[][]): Record<string, any> {
const config: Record<string, any> = {
type: 'emom',
rounds: 0,
interval: 60, // Default 60 seconds
rest: 0
};
// Extract rounds (total number of intervals)
const roundsTag = tags.find(t => t[0] === 'rounds');
if (roundsTag && roundsTag.length > 1) {
config.rounds = parseInt(roundsTag[1]) || 0;
}
// Extract interval duration
const intervalTag = tags.find(t => t[0] === 'interval');
if (intervalTag && intervalTag.length > 1) {
config.interval = parseInt(intervalTag[1]) || 60;
}
// Extract rest between rounds
const restTag = tags.find(t => t[0] === 'rest_between_rounds');
if (restTag && restTag.length > 1) {
config.rest = parseInt(restTag[1]) || 0;
}
return config;
}
getDefaultParameters(): Record<string, any> {
return {
rounds: 10,
interval: 60,
rest: 0
};
}
formatTemplateConfig(config: Record<string, any>): string[][] {
const tags: string[][] = [];
if (config.rounds) {
tags.push(['rounds', config.rounds.toString()]);
}
if (config.interval) {
tags.push(['interval', config.interval.toString()]);
}
if (config.rest) {
tags.push(['rest_between_rounds', config.rest.toString()]);
}
return tags;
}
}
```
### Phase 3: Long-Term Architecture (Future)
#### 1. Modular Event Processor Architecture
**Problem**: Need a more adaptable system for handling evolving Nostr event schemas.
**Solution**:
- Create a plugin-based architecture for event processors
- Implement versioning for Nostr event handling
- Design a flexible mapping system between Nostr events and local database schema
```typescript
// Interface for event processors
interface NostrEventProcessor<T> {
// Check if processor can handle this event
canProcess(event: NostrEvent): boolean;
// Process event to local model
processEvent(event: NostrEvent): T;
// Convert local model to event
createEvent(model: T): NostrEvent;
// Get processor version
getVersion(): string;
}
// Registry for event processors
class EventProcessorRegistry {
private processors: Map<number, NostrEventProcessor<any>[]> = new Map();
// Register a processor for a specific kind
registerProcessor(kind: number, processor: NostrEventProcessor<any>): void {
if (!this.processors.has(kind)) {
this.processors.set(kind, []);
}
this.processors.get(kind)?.push(processor);
}
// Get appropriate processor for an event
getProcessor<T>(event: NostrEvent): NostrEventProcessor<T> | null {
const kindProcessors = this.processors.get(event.kind);
if (!kindProcessors) return null;
// Find the first processor that can process this event
for (const processor of kindProcessors) {
if (processor.canProcess(event)) {
return processor as NostrEventProcessor<T>;
}
}
return null;
}
}
```
#### 2. Schema Migration System
**Problem**: Database schema needs to evolve with Nostr specification changes.
**Solution**:
- Create a versioned migration system
- Implement automatic schema updates
- Track schema versions
```typescript
// Migration interface
interface SchemaMigration {
version: number;
up(db: SQLiteDatabase): Promise<void>;
down(db: SQLiteDatabase): Promise<void>;
}
// Migration runner
class MigrationRunner {
private migrations: SchemaMigration[] = [];
// Register a migration
registerMigration(migration: SchemaMigration): void {
this.migrations.push(migration);
// Sort migrations by version
this.migrations.sort((a, b) => a.version - b.version);
}
// Run migrations up to a specific version
async migrate(db: SQLiteDatabase, targetVersion: number): Promise<void> {
// Get current version
const currentVersion = await this.getCurrentVersion(db);
if (currentVersion < targetVersion) {
// Run UP migrations
for (const migration of this.migrations) {
if (migration.version > currentVersion && migration.version <= targetVersion) {
await migration.up(db);
await this.updateVersion(db, migration.version);
}
}
} else if (currentVersion > targetVersion) {
// Run DOWN migrations
for (const migration of [...this.migrations].reverse()) {
if (migration.version <= currentVersion && migration.version > targetVersion) {
await migration.down(db);
await this.updateVersion(db, migration.version - 1);
}
}
}
}
// Helper methods
private async getCurrentVersion(db: SQLiteDatabase): Promise<number> {
// Implementation
return 0;
}
private async updateVersion(db: SQLiteDatabase, version: number): Promise<void> {
// Implementation
}
}
```
#### 3. Future-Proof Integration Patterns
**Problem**: Need to ensure the POWR app can adapt to future Nostr specification changes.
**Solution**:
- Implement adapter pattern for Nostr protocol
- Create abstraction layers for data synchronization
- Design entity mappers for different data versions
```typescript
// Adapter for Nostr protocol versions
interface NostrProtocolAdapter {
// Get exercise from event
getExerciseFromEvent(event: NostrEvent): BaseExercise;
// Get template from event
getTemplateFromEvent(event: NostrEvent): WorkoutTemplate;
// Get workout record from event
getWorkoutFromEvent(event: NostrEvent): Workout;
// Create events from local models
createExerciseEvent(exercise: BaseExercise): NostrEvent;
createTemplateEvent(template: WorkoutTemplate): NostrEvent;
createWorkoutEvent(workout: Workout): NostrEvent;
}
// Versioned adapter implementation
class NostrProtocolAdapterV1 implements NostrProtocolAdapter {
// Implementation for first version of NIP-4e
}
```
## Testing Strategy
### Phase 1 (Immediate)
1. Create a test POWR Pack with variety of exercise types and templates
2. Test importing the pack with the updated code
3. Verify that templates contain the correct exercise relationships
4. Validate parameter extraction works correctly
### Phase 2 (Short-term)
1. Create test cases for different workout types (strength, circuit, EMOM, AMRAP)
2. Verify parameter mapping works as expected
3. Test template management functions
### Phase 3 (Long-term)
1. Create comprehensive integration tests
2. Design migration testing framework
3. Implement automated testing for different Nostr protocol versions
## Implementation Timeline
### Phase 1: Critical Fixes
- **Day 1**: Fix template-exercise relationship in `POWRPackService.ts`
- **Day 2**: Fix parameter extraction in `NostrIntegration.ts`
- **Day 3**: Implement template management functions and schema updates
- **Day 4**: Testing and bug fixes
### Phase 2: Extensibility Improvements
- **Week 2**: Implement schema updates and flexible parameter extraction
- **Week 3**: Develop workout type-specific processing
- **Week 4**: UI enhancements and testing
### Phase 3: Long-Term Architecture
- **Future**: Implement as part of broader architectural improvements
## Conclusion
This updated plan addresses both the immediate issues with POWR Pack integration and lays out a path for future extensibility as the Nostr Exercise NIP evolves. By implementing these changes in phases, we can quickly fix the current template-exercise relationship problems while establishing a foundation for more sophisticated features in the future.
The proposed approach balances pragmatism with future-proofing, ensuring that users can immediately benefit from POWR Packs while the system remains adaptable to changes in workout types, exercise parameters, and Nostr protocol specifications.

View File

@ -1,744 +0,0 @@
# POWR Social Architecture
## Problem Statement
POWR needs to integrate social features that leverage the Nostr protocol while maintaining a local-first architecture. The system must provide a seamless way for users to share workout content, receive feedback, and engage with the fitness community without compromising the standalone functionality of the application. Additionally, the implementation must support future integration with value-exchange mechanisms through Nostr Wallet Connect.
## Requirements
### Functional Requirements
- Custom Nostr event types for exercises, workout templates, and workout records
- Social sharing of workout content via NIP-19 references
- Content management including deletion requests
- Comment system on exercises, templates, and workout records
- Reactions and likes on shared content
- App discovery through NIP-89 handlers
- Support for zaps and Lightning payments via NWC
- Ability to track template usage and popularity
- User profile and content discovery
- Local-first storage with Nostr sync capabilities
### Non-Functional Requirements
- Performance: Social content loads within 500ms when online
- Security: User private keys are never exposed to the application
- Reliability: All created content must be usable offline
- Usability: Social interactions should be intuitive and seamlessly integrated
- Privacy: Users control what content gets shared publicly
- Scalability: System handles thousands of exercise templates and workout records
## Design Decisions
### 1. Custom Event Kinds vs. Standard Kinds
**Approach**: Use custom event kinds (33401, 33402, 1301) for exercises, templates, and workout records rather than generic kind 1 events.
**Rationale**:
- Custom kinds enable clear data separation and easier filtering
- Avoids confusion between social posts and fitness data
- Enables specialized app handlers via NIP-89
- Aligns with best practices for specialized content types
- Enables distinct validation rules for each content type
**Trade-offs**:
- Requires implementing NIP-89 app handlers for client support
- Less immediate visibility in generic Nostr clients
- Needs additional social sharing mechanism for discovery
### 2. Template-Record Relationship
**Approach**: Implement standalone workout records with explicit references to templates.
**Rationale**:
- Cleaner separation between templates and completed workouts
- More flexible for workouts that modify templates
- Better support for offline-first usage
- Simplifies synchronization logic
- Easier to implement privacy controls
**Trade-offs**:
- Requires custom queries to track template usage
- Doesn't leverage built-in reply notification systems
- Additional relationship management logic needed
### 3. Comments Implementation
**Approach**: Use NIP-22 generic comments system with proper reference structure.
**Rationale**:
- Standardized approach compatible with existing Nostr clients
- Supports threaded conversations
- Clear distinction between content and comments
- Integrates with existing notification systems
- Simple to implement using established patterns
**Trade-offs**:
- Requires filtering to display relevant comments
- Additional UI components for comment display
- Need for moderation tools (client-side filtering)
### 4. Nostr Wallet Connect Integration
**Approach**: Implement NIP-47 Nostr Wallet Connect for Lightning payments and zaps.
**Rationale**:
- Secure payment capabilities without managing private keys
- Enables zaps on workout content
- Creates opportunities for creator compensation
- Integrates with existing Nostr Lightning infrastructure
- Future-proofs for monetization features
**Trade-offs**:
- Additional complexity in wallet connection management
- Dependency on external wallet implementations
- Requires careful error handling for payment flows
### 5. Content Publishing and Deletion Workflow
**Approach**: Implement a three-tier approach to content sharing with NIP-09 deletion requests.
**Rationale**:
- Gives users control over content visibility
- Maintains local-first philosophy
- Provides clear separation between private and public data
- Follows Nostr standards for content management
- Enables social sharing while maintaining specialized data format
**Trade-offs**:
- Deletion on Nostr is not guaranteed across all relays
- Additional UI complexity to explain publishing/deletion states
- Need to track content state across local storage and relays
## Technical Design
### Core Components
```typescript
// Exercise Template Event (Kind 33401)
interface ExerciseTemplate extends NostrEvent {
kind: 33401;
content: string; // Detailed instructions
tags: [
["d", string], // Unique identifier
["title", string], // Exercise name
["format", ...string[]], // Data structure parameters
["format_units", ...string[]], // Units for parameters
["equipment", string], // Equipment type
["difficulty"?, string], // Optional skill level
["imeta"?, ...string[]], // Optional media metadata
["t"?, string][], // Optional hashtags
]
}
// Workout Template Event (Kind 33402)
interface WorkoutTemplate extends NostrEvent {
kind: 33402;
content: string; // Workout notes and instructions
tags: [
["d", string], // Unique identifier
["title", string], // Workout name
["type", string], // Workout type (strength, circuit, etc.)
["exercise", ...string[]][], // Exercise references with parameters
["rounds"?, string], // Optional rounds count
["duration"?, string], // Optional total duration
["interval"?, string], // Optional interval duration
["rest_between_rounds"?, string], // Optional rest time
["t"?, string][], // Optional hashtags
]
}
// Workout Record Event (Kind 1301)
interface WorkoutRecord extends NostrEvent {
kind: 1301;
content: string; // Workout notes
tags: [
["d", string], // Unique identifier
["title", string], // Workout name
["type", string], // Workout type
["template", "33402:<pubkey>:<d-tag>", "<relay-url>"], // Explicit template reference
["exercise", ...string[]][], // Exercises with actual values
["start", string], // Start timestamp
["end", string], // End timestamp
["completed", string], // Completion status
["rounds_completed"?, string], // Optional rounds completed
["interval"?, string], // Optional interval duration
["pr"?, string][], // Optional personal records
["t"?, string][], // Optional hashtags
]
}
// Social Share (Kind 1)
interface SocialShare extends NostrEvent {
kind: 1;
content: string; // Social post text
tags: [
// Quote reference to the exercise, template or workout
["q", string, string, string], // event-id, relay-url, pubkey
// Kind tag to indicate what kind of event is being quoted
["k", string], // The kind number of the quoted event (e.g., "1301")
// Mention author's pubkey
["p", string] // pubkey of the event creator
]
}
// Deletion Request (Kind 5) - NIP-09
interface DeletionRequest extends NostrEvent {
kind: 5;
content: string; // Reason for deletion (optional)
tags: [
// Event reference(s) to delete
["e", string], // event-id(s) to delete
// Or addressable event reference
["a", string], // "<kind>:<pubkey>:<d-identifier>"
// Kind of the event being deleted
["k", string] // kind number as string
]
}
// Comment (Kind 1111 - as per NIP-22)
interface WorkoutComment extends NostrEvent {
kind: 1111;
content: string; // Comment text
tags: [
// Root reference (exercise, template, or record)
["e", string, string, string], // id, relay, marker "root"
["K", string], // Root kind (33401, 33402, or 1301)
["P", string, string], // Root pubkey, relay
// Parent comment (for replies)
["e"?, string, string, string], // id, relay, marker "reply"
["k"?, string], // Parent kind (1111)
["p"?, string, string], // Parent pubkey, relay
]
}
// Reaction (Kind 7 - as per NIP-25)
interface Reaction extends NostrEvent {
kind: 7;
content: "+" | "🔥" | "👍"; // Standard reaction symbols
tags: [
["e", string, string], // event-id, relay-url
["p", string] // pubkey of the event creator
]
}
// App Handler Registration (Kind 31990)
interface AppHandler extends NostrEvent {
kind: 31990;
content: string;
tags: [
["k", "33401", "exercise-template"],
["k", "33402", "workout-template"],
["k", "1301", "workout-record"],
["web", string], // App URL
["name", string], // App name
["description", string] // App description
]
}
// Nostr Wallet Connection Manager
class NWCManager {
async connectWallet(nwcURI: string): Promise<{
connected: boolean;
pubkey?: string;
error?: string;
}>;
async getBalance(): Promise<number>;
async zapEvent(
event: NostrEvent,
amount: number,
comment?: string
): Promise<{
success: boolean;
preimage?: string;
error?: string;
}>;
}
// Social Service
class SocialService {
// Share workout on social feeds
async shareWorkoutSocially(
workout: WorkoutTemplate | WorkoutRecord,
message: string
): Promise<NostrEvent>;
// Get comments for content
async getComments(
eventId: string,
rootKind: number
): Promise<WorkoutComment[]>;
// Post comment
async postComment(
rootEvent: NostrEvent,
content: string,
parentComment?: WorkoutComment
): Promise<WorkoutComment>;
// Track template usage
async getTemplateUsageCount(templateId: string): Promise<number>;
// React to content
async reactToEvent(
event: NostrEvent,
reaction: "+" | "🔥" | "👍"
): Promise<NostrEvent>;
// Request deletion of event
async requestDeletion(
eventId: string,
eventKind: number,
reason?: string
): Promise<NostrEvent>;
// Request deletion of addressable event
async requestAddressableDeletion(
kind: number,
pubkey: string,
dTag: string,
reason?: string
): Promise<NostrEvent>;
}
```
### Content Publishing Workflow
```mermaid
graph TD
A[Create Content] --> B{Publish to Relays?}
B -->|No| C[Local Storage Only]
B -->|Yes| D[Save to Local Storage]
D --> E[Publish to Relays]
E --> F{Share Socially?}
F -->|No| G[Done - Content on Relays]
F -->|Yes| H[Create kind:1 Social Post]
H --> I[Reference Original Event]
I --> J[Done - Content Shared]
K[Delete Content] --> L{Delete from Relays?}
L -->|No| M[Delete from Local Only]
L -->|Yes| N[Create kind:5 Deletion Request]
N --> O[Publish Deletion Request]
O --> P{Delete Locally?}
P -->|No| Q[Done - Deletion Requested]
P -->|Yes| R[Delete from Local Storage]
R --> S[Done - Content Deleted]
```
### Data Flow Diagram
```mermaid
graph TD
subgraph User
A[Create Content] --> B[Local Storage]
G[View Content] --> F[UI Components]
T[Request Deletion] --> U[Deletion Manager]
end
subgraph LocalStorage
B --> C[SQLite Database]
C --> D[Event Processor]
end
subgraph NostrNetwork
D -->|Publish| E[Relays]
E -->|Subscribe| F
U -->|Publish| E
end
subgraph SocialInteractions
H[Comments] --> I[Comment Processor]
J[Reactions] --> K[Reaction Processor]
L[Zaps] --> M[NWC Manager]
end
I -->|Publish| E
K -->|Publish| E
M -->|Request| N[Lightning Wallet]
N -->|Zap| E
E -->|Fetch Related| F
C -->|Offline Data| F
```
### Query Examples
```typescript
// Find all exercise templates
const exerciseTemplatesQuery = {
kinds: [33401],
limit: 50
};
// Find workout templates that use a specific exercise
const templatesWithExerciseQuery = {
kinds: [33402],
"#exercise": [`33401:${pubkey}:${exerciseId}`]
};
// Find workout records for a specific template
const workoutRecordsQuery = {
kinds: [1301],
"#template": [`33402:${pubkey}:${templateId}`]
};
// Find comments on a workout record
const commentsQuery = {
kinds: [1111],
"#e": [workoutEventId],
"#K": ["1301"] // Root kind filter
};
// Find all social posts specifically referencing workout records
const workoutPostsQuery = {
kinds: [1],
"#k": ["1301"]
};
// Find all social posts referencing any POWR content types
const allPowrContentQuery = {
kinds: [1],
"#k": ["1301", "33401", "33402"]
};
// Find all social posts referencing POWR content from a specific user
const userPowrContentQuery = {
kinds: [1],
"#k": ["1301", "33401", "33402"],
authors: [userPubkey]
};
// Find posts with POWR hashtag
const powrHashtagQuery = {
kinds: [1],
"#t": ["powrapp"]
};
// Get reactions to a workout record
const reactionsQuery = {
kinds: [7],
"#e": [workoutEventId]
};
// Find deletion requests for an event
const deletionRequestQuery = {
kinds: [5],
"#e": [eventId]
};
// Find deletion requests for an addressable event
const addressableDeletionRequestQuery = {
kinds: [5],
"#a": [`${kind}:${pubkey}:${dTag}`]
};
```
## Event Publishing and Deletion Implementation
### Publishing Workflow
POWR implements a three-tier approach to content publishing:
1. **Local Only**
- Content is saved only to the device's local storage
- No Nostr events are published
- Content is completely private to the user
2. **Publish to Relays**
- Content is saved locally and published to user-selected relays
- Published as appropriate Nostr events (33401, 33402, 1301)
- Content becomes discoverable by compatible apps via NIP-89
- Local copy is marked as "published to relays"
3. **Social Sharing**
- Content is published to relays as in step 2
- Additionally, a kind:1 social post is created
- The social post quotes the specialized content
- Makes content visible in standard Nostr social clients
- Links back to the specialized content via NIP-19 references
### Deletion Workflow
POWR implements NIP-09 for deletion requests:
1. **Local Deletion**
- Content is removed from local storage only
- No effect on previously published relay content
- User maintains control over local data independent of relay status
2. **Relay Deletion Request**
- Creates a kind:5 deletion request event
- References the content to be deleted
- Includes the kind of content being deleted
- Published to relays that had the original content
- Original content may remain in local storage if desired
3. **Complete Deletion**
- Combination of local deletion and relay deletion request
- Content is removed locally and requested for deletion from relays
- Any social shares remain unless specifically deleted
### Example Implementation
```typescript
// Publishing Content
async function publishExerciseTemplate(exercise) {
// Save locally first
const localId = await localDb.saveExercise(exercise);
// If user wants to publish to relays
if (exercise.publishToRelays) {
// Create Nostr event
const event = {
kind: 33401,
pubkey: userPubkey,
created_at: Math.floor(Date.now() / 1000),
tags: [
["d", localId],
["title", exercise.title],
["format", ...Object.keys(exercise.format)],
["format_units", ...formatUnitsToArray(exercise.format_units)],
["equipment", exercise.equipment],
...exercise.tags.map(tag => ["t", tag])
],
content: exercise.description || ""
};
// Sign and publish
event.id = getEventHash(event);
event.sig = signEvent(event, userPrivkey);
await publishToRelays(event);
// Update local record to reflect published status
await localDb.markAsPublished(localId, event.id);
// If user wants to share socially
if (exercise.shareAsSocialPost) {
await createSocialShare(event, exercise.socialShareText || "Check out this exercise!");
}
return { localId, eventId: event.id };
}
return { localId };
}
// Requesting Deletion
async function requestDeletion(eventId, eventKind, options = {}) {
const { deleteLocally = false, reason = "" } = options;
// Create deletion request
const deletionRequest = {
kind: 5,
pubkey: userPubkey,
created_at: Math.floor(Date.now() / 1000),
tags: [
["e", eventId],
["k", eventKind.toString()]
],
content: reason
};
// Sign and publish
deletionRequest.id = getEventHash(deletionRequest);
deletionRequest.sig = signEvent(deletionRequest, userPrivkey);
await publishToRelays(deletionRequest);
// Update local storage
await localDb.markAsDeletedFromRelays(eventId);
// Delete locally if requested
if (deleteLocally) {
await localDb.deleteContentLocally(eventId);
}
return deletionRequest;
}
// Request deletion of addressable event
async function requestAddressableDeletion(kind, pubkey, dTag, options = {}) {
const { deleteLocally = false, reason = "" } = options;
const deletionRequest = {
kind: 5,
pubkey: userPubkey,
created_at: Math.floor(Date.now() / 1000),
tags: [
["a", `${kind}:${pubkey}:${dTag}`],
["k", kind.toString()]
],
content: reason
};
// Sign and publish
deletionRequest.id = getEventHash(deletionRequest);
deletionRequest.sig = signEvent(deletionRequest, userPrivkey);
await publishToRelays(deletionRequest);
// Update local storage
await localDb.markAddressableEventAsDeletedFromRelays(kind, pubkey, dTag);
// Delete locally if requested
if (deleteLocally) {
await localDb.deleteAddressableContentLocally(kind, pubkey, dTag);
}
return deletionRequest;
}
// Check for deletion requests when viewing content
async function checkDeletionStatus(eventId) {
const deletionRequests = await ndk.fetchEvents({
kinds: [5],
"#e": [eventId]
});
for (const request of deletionRequests) {
// Verify the deletion request is from the original author
if (request.pubkey === event.pubkey) {
return { isDeleted: true, request };
}
}
return { isDeleted: false };
}
```
## User Interface Design
### Content Status Indicators
The UI should clearly indicate the status of fitness content:
1. **Local Only**
- Visual indicator showing content is only on device
- Options to publish to relays or share socially
2. **Published to Relays**
- Indicator showing content is published
- Display relay publishing status
- Option to create social share
3. **Socially Shared**
- Indicator showing content has been shared socially
- Link to view social post
- Stats on social engagement (comments, reactions)
4. **Deletion Requested**
- Indicator showing deletion has been requested
- Option to delete locally if not already done
- Explanation that deletion from all relays cannot be guaranteed
### Deletion Interface
The UI for deletion should be clear and informative:
1. **Deletion Options**
- "Delete Locally" - Removes from device only
- "Request Deletion from Relays" - Issues NIP-09 deletion request
- "Delete Completely" - Both local and relay deletion
2. **Confirmation Dialog**
- Clear explanation of deletion scope
- Warning that relay deletion is not guaranteed
- Option to provide reason for deletion (for relay requests)
3. **Deletion Status**
- Visual indicator for content with deletion requests
- Option to view deletion request details
- Ability to check status across relays
## Implementation Plan
### Phase 1: Core Nostr Event Structure
1. Implement custom event kinds (33401, 33402, 1301)
2. Create local storage schema with publishing status tracking
3. Build basic event publishing to relays
4. Implement NIP-09 deletion requests
### Phase 2: Social Interaction Foundation
1. Implement NIP-22 comments system
2. Create NIP-25 reactions support
3. Build NIP-19 social sharing functions
4. Implement NIP-89 app handler registration
5. Develop UI components for social interactions
### Phase 3: Nostr Wallet Connect
1. Implement NWC connection management
2. Create wallet interface in profile section
3. Develop zap functionality for content
4. Build UI for Lightning interactions
5. Add tipping capability for creators
### Phase 4: Advanced Social Features
1. Implement NIP-51 lists for workout collections
2. Create user follows and discoveries
3. Develop achievement sharing
4. Build coaching and feedback tools
5. Add paid content capabilities
## Testing Strategy
### Unit Tests
- Event validation and processing tests
- Deletion request handling tests
- Comment threading logic tests
- Wallet connection management tests
- Relay communication tests
- Social share URL generation tests
### Integration Tests
- End-to-end publishing flow testing
- Deletion request workflow testing
- Comment and reaction functionality testing
- Template usage tracking tests
- Social sharing workflow tests
- Zap flow testing
### User Testing
- Usability of publishing and deletion workflows
- Clarity of content status indicators
- Wallet connection experience
- Performance on different devices and connection speeds
## Security Considerations
- Never store or request user private keys
- Secure management of NWC connection secrets
- Client-side validation of all incoming events
- Content filtering for inappropriate material
- User control over content visibility
- Protection against spam and abuse
## Rollout Strategy
### Development Phase
1. Implement custom event kinds and validation
2. Create UI components for content publishing status
3. Develop local-first storage with Nostr sync
4. Build and test deletion request functionality
5. Implement wallet connection interface
6. Add documentation for Nostr integration
### Beta Testing
1. Release to limited test group
2. Monitor relay performance and sync issues
3. Gather feedback on publishing and deletion flows
4. Test cross-client compatibility
5. Evaluate Lightning payment reliability
### Production Deployment
1. Deploy app handler registration
2. Roll out features progressively
3. Monitor engagement and performance metrics
4. Provide guides for feature usage
5. Establish relay connection recommendations
6. Create nostr:// URI scheme handlers
## References
- [Nostr NIPs Repository](https://github.com/nostr-protocol/nips)
- [NIP-09 Event Deletion](https://github.com/nostr-protocol/nips/blob/master/09.md)
- [NIP-10 Text Notes and Threads](https://github.com/nostr-protocol/nips/blob/master/10.md)
- [NIP-19 bech32-encoded entities](https://github.com/nostr-protocol/nips/blob/master/19.md)
- [NIP-22 Comment](https://github.com/nostr-protocol/nips/blob/master/22.md)
- [NIP-89 Recommended Application Handlers](https://github.com/nostr-protocol/nips/blob/master/89.md)
- [NDK Documentation](https://github.com/nostr-dev-kit/ndk)
- [NIP-47 Wallet Connect](https://github.com/nostr-protocol/nips/blob/master/47.md)
- [NIP-57 Lightning Zaps](https://github.com/nostr-protocol/nips/blob/master/57.md)

File diff suppressed because it is too large Load Diff

View File

@ -1,447 +0,0 @@
# Social Feed Cache Implementation
## Overview
This document outlines the implementation of the Social Feed Cache system in the POWR app. The cache system is designed to provide offline access to social feed data, reduce network usage, and improve performance.
## Key Components
1. **SocialFeedCache**: The main service that handles caching of social feed events
2. **EventCache**: A service for caching individual Nostr events
3. **useSocialFeed**: A hook that provides access to the social feed data
4. **RelayInitializer**: A component that initializes the cache system
## Implementation Details
### Write Buffer System
The SocialFeedCache uses a write buffer system to batch database operations and reduce transaction conflicts. This approach is inspired by the Olas NDK Mobile implementation.
```typescript
private writeBuffer: { query: string; params: any[] }[] = [];
private bufferFlushTimer: NodeJS.Timeout | null = null;
private bufferFlushTimeout: number = 100; // milliseconds
private processingTransaction: boolean = false;
private bufferWrite(query: string, params: any[]) {
this.writeBuffer.push({ query, params });
if (!this.bufferFlushTimer) {
this.bufferFlushTimer = setTimeout(() => this.flushWriteBuffer(), this.bufferFlushTimeout);
}
}
private async flushWriteBuffer() {
if (this.writeBuffer.length === 0 || this.processingTransaction) return;
const bufferCopy = [...this.writeBuffer];
this.writeBuffer = [];
this.processingTransaction = true;
try {
await this.db.withTransactionAsync(async () => {
for (const { query, params } of bufferCopy) {
await this.db.runAsync(query, params);
}
});
} catch (error) {
console.error('[SocialFeedCache] Error flushing write buffer:', error);
// If there was an error, add the operations back to the buffer
for (const op of bufferCopy) {
if (!this.writeBuffer.some(item =>
item.query === op.query &&
JSON.stringify(item.params) === JSON.stringify(op.params)
)) {
this.writeBuffer.push(op);
}
}
} finally {
this.processingTransaction = false;
}
this.bufferFlushTimer = null;
// If there are more operations, start a new timer
if (this.writeBuffer.length > 0) {
this.bufferFlushTimer = setTimeout(() => this.flushWriteBuffer(), this.bufferFlushTimeout);
}
}
```
### In-Memory Tracking with LRU Cache
To prevent redundant database operations, the SocialFeedCache uses an LRU (Least Recently Used) cache to track known events:
```typescript
private knownEventIds: LRUCache<string, number>; // Event ID -> timestamp
constructor(database: SQLiteDatabase) {
this.db = new DbService(database);
this.eventCache = new EventCache(database);
// Initialize LRU cache for known events (limit to 1000 entries)
this.knownEventIds = new LRUCache<string, number>({ maxSize: 1000 });
// Ensure feed_cache table exists
this.initializeTable();
}
```
### Debounced Subscriptions
The `useSocialFeed` hook implements debouncing to prevent rapid resubscriptions:
```typescript
// Subscription cooldown to prevent rapid resubscriptions
const subscriptionCooldown = useRef<NodeJS.Timeout | null>(null);
const cooldownPeriod = 2000; // 2 seconds
const subscriptionAttempts = useRef(0);
const maxSubscriptionAttempts = 3;
// In loadFeed function:
// Prevent rapid resubscriptions
if (subscriptionCooldown.current) {
console.log('[useSocialFeed] Subscription on cooldown, skipping');
return;
}
// Track subscription attempts to prevent infinite loops
subscriptionAttempts.current += 1;
if (subscriptionAttempts.current > maxSubscriptionAttempts) {
console.error(`[useSocialFeed] Too many subscription attempts (${subscriptionAttempts.current}), giving up`);
setLoading(false);
return;
}
// Set a cooldown to prevent rapid resubscriptions
subscriptionCooldown.current = setTimeout(() => {
subscriptionCooldown.current = null;
// Reset attempt counter after cooldown period
subscriptionAttempts.current = 0;
}, cooldownPeriod);
```
### Proper Initialization
The RelayInitializer component ensures that the SocialFeedCache is properly initialized with the NDK instance:
```typescript
// Initialize ProfileImageCache and SocialFeedCache with NDK instance
useEffect(() => {
if (ndk) {
console.log('[RelayInitializer] Setting NDK instance in ProfileImageCache');
profileImageCache.setNDK(ndk);
// Initialize SocialFeedCache with NDK instance
if (db) {
try {
const socialFeedCache = getSocialFeedCache(db);
socialFeedCache.setNDK(ndk);
console.log('[RelayInitializer] SocialFeedCache initialized with NDK');
} catch (error) {
console.error('[RelayInitializer] Error initializing SocialFeedCache:', error);
}
}
}
}, [ndk, db]);
```
### Global Transaction Lock Mechanism
To prevent transaction conflicts between different services (such as SocialFeedCache and ContactCacheService), we've implemented a global transaction lock mechanism in the SocialFeedCache class:
```typescript
// Global transaction lock to prevent transaction conflicts across services
private static transactionLock: boolean = false;
private static transactionQueue: (() => Promise<void>)[] = [];
private static processingQueue: boolean = false;
/**
* Acquire the global transaction lock
* @returns True if lock was acquired, false otherwise
*/
private static acquireTransactionLock(): boolean {
if (SocialFeedCache.transactionLock) {
return false;
}
SocialFeedCache.transactionLock = true;
return true;
}
/**
* Release the global transaction lock
*/
private static releaseTransactionLock(): void {
SocialFeedCache.transactionLock = false;
// Process the next transaction in queue if any
if (SocialFeedCache.transactionQueue.length > 0 && !SocialFeedCache.processingQueue) {
SocialFeedCache.processTransactionQueue();
}
}
/**
* Add a transaction to the queue
* @param transaction Function that performs the transaction
*/
private static enqueueTransaction(transaction: () => Promise<void>): void {
SocialFeedCache.transactionQueue.push(transaction);
// Start processing the queue if not already processing
if (!SocialFeedCache.processingQueue) {
SocialFeedCache.processTransactionQueue();
}
}
/**
* Process the transaction queue
*/
private static async processTransactionQueue(): Promise<void> {
if (SocialFeedCache.processingQueue || SocialFeedCache.transactionQueue.length === 0) {
return;
}
SocialFeedCache.processingQueue = true;
try {
while (SocialFeedCache.transactionQueue.length > 0) {
// Wait until we can acquire the lock
if (!SocialFeedCache.acquireTransactionLock()) {
// If we can't acquire the lock, wait and try again
await new Promise(resolve => setTimeout(resolve, 100));
continue;
}
// Get the next transaction
const transaction = SocialFeedCache.transactionQueue.shift();
if (!transaction) {
SocialFeedCache.releaseTransactionLock();
continue;
}
try {
// Execute the transaction
await transaction();
} catch (error) {
console.error('[SocialFeedCache] Error executing queued transaction:', error);
} finally {
// Release the lock
SocialFeedCache.releaseTransactionLock();
}
}
} finally {
SocialFeedCache.processingQueue = false;
}
}
/**
* Execute a transaction with the global lock
* @param transaction Function that performs the transaction
*/
public static async executeWithLock(transaction: () => Promise<void>): Promise<void> {
// Add the transaction to the queue
SocialFeedCache.enqueueTransaction(transaction);
}
```
This mechanism ensures that only one transaction is active at any given time, preventing the "cannot start a transaction within a transaction" error that can occur when two services try to start transactions simultaneously.
The `executeWithLock` method can be used by other services to coordinate their database transactions with SocialFeedCache:
```typescript
// Example usage in ContactCacheService
async cacheContacts(ownerPubkey: string, contacts: string[]): Promise<void> {
if (!ownerPubkey || !contacts.length) return;
try {
// Use the global transaction lock to prevent conflicts with other services
await SocialFeedCache.executeWithLock(async () => {
try {
// Use a transaction for better performance
await this.db.withTransactionAsync(async () => {
// Database operations...
});
} catch (error) {
console.error('[ContactCacheService] Error in transaction:', error);
throw error; // Rethrow to ensure the transaction is marked as failed
}
});
} catch (error) {
console.error('[ContactCacheService] Error caching contacts:', error);
}
}
```
### Enhanced Write Buffer System
The write buffer system has been enhanced with exponential backoff and improved error handling:
```typescript
private async flushWriteBuffer() {
if (this.writeBuffer.length === 0 || this.processingTransaction) return;
// Check if database is available
if (!this.isDbAvailable()) {
console.log('[SocialFeedCache] Database not available, delaying flush');
this.scheduleNextFlush(true); // Schedule with backoff
return;
}
// Take only a batch of operations to process at once
const bufferCopy = [...this.writeBuffer].slice(0, this.maxBatchSize);
this.writeBuffer = this.writeBuffer.slice(bufferCopy.length);
this.processingTransaction = true;
// Use the transaction lock to prevent conflicts
try {
// Check if we've exceeded the maximum retry count
if (this.retryCount > this.maxRetryCount) {
console.warn(`[SocialFeedCache] Exceeded maximum retry count (${this.maxRetryCount}), dropping ${bufferCopy.length} operations`);
// Reset retry count but don't retry these operations
this.retryCount = 0;
this.processingTransaction = false;
this.scheduleNextFlush();
return;
}
// Increment retry count before attempting transaction
this.retryCount++;
// Execute the transaction with the global lock
await SocialFeedCache.executeWithLock(async () => {
try {
// Execute the transaction
await this.db.withTransactionAsync(async () => {
for (const { query, params } of bufferCopy) {
try {
await this.db.runAsync(query, params);
} catch (innerError) {
// Log individual query errors but continue with other queries
console.error(`[SocialFeedCache] Error executing query: ${query}`, innerError);
// Don't rethrow to allow other queries to proceed
}
}
});
// Success - reset retry count
this.retryCount = 0;
this.dbAvailable = true; // Mark database as available
} catch (error) {
console.error('[SocialFeedCache] Error in transaction:', error);
// Check for database connection errors
if (error instanceof Error &&
(error.message.includes('closed resource') ||
error.message.includes('Database not available'))) {
// Mark database as unavailable
this.dbAvailable = false;
console.warn('[SocialFeedCache] Database connection issue detected, marking as unavailable');
// Add all operations back to the buffer
this.writeBuffer = [...bufferCopy, ...this.writeBuffer];
} else {
// For other errors, add operations back to the buffer
// but only if they're not already there (avoid duplicates)
for (const op of bufferCopy) {
if (!this.writeBuffer.some(item =>
item.query === op.query &&
JSON.stringify(item.params) === JSON.stringify(op.params)
)) {
// Add back to the beginning of the buffer to retry sooner
this.writeBuffer.unshift(op);
}
}
}
// Rethrow to ensure the transaction is marked as failed
throw error;
}
});
} catch (error) {
console.error('[SocialFeedCache] Error flushing write buffer:', error);
} finally {
this.processingTransaction = false;
this.scheduleNextFlush();
}
}
/**
* Schedule the next buffer flush with optional backoff
*/
private scheduleNextFlush(withBackoff: boolean = false) {
if (this.bufferFlushTimer) {
clearTimeout(this.bufferFlushTimer);
this.bufferFlushTimer = null;
}
if (this.writeBuffer.length > 0) {
let delay = this.bufferFlushTimeout;
if (withBackoff) {
// Use exponential backoff based on retry count
delay = Math.min(
this.bufferFlushTimeout * Math.pow(2, this.retryCount),
this.maxBackoffTime
);
}
console.log(`[SocialFeedCache] Scheduling next flush in ${delay}ms (retry: ${this.retryCount})`);
this.bufferFlushTimer = setTimeout(() => this.flushWriteBuffer(), delay);
}
}
```
## Benefits
1. **Eliminated Transaction Conflicts**: The global transaction lock mechanism prevents transaction conflicts between different services.
2. **Improved Reliability**: The transaction queue ensures that all transactions are processed even if they can't be executed immediately.
3. **Enhanced Error Recovery**: The exponential backoff and retry mechanism improves recovery from temporary database errors.
4. **Better Offline Stability**: The system handles database unavailability gracefully, enabling seamless offline operation.
5. **Reduced Database Contention**: Coordinated transactions reduce contention on the database.
6. **Improved Performance**: The LRU cache reduces redundant database operations.
7. **Better Error Handling**: The system includes robust error handling to prevent cascading failures.
8. **Offline Support**: The cache system provides offline access to social feed data.
9. **Reduced Network Usage**: The system reduces network usage by caching events locally.
## Debugging
The Following screen includes debug information to help troubleshoot issues:
```typescript
// Debug controls component - memoized
const DebugControls = useCallback(() => (
<View className="bg-gray-100 p-4 rounded-lg mx-4 mb-4">
<Text className="font-bold mb-2">Debug Info:</Text>
<Text>User: {currentUser?.pubkey?.substring(0, 8)}...</Text>
<Text>Feed Items: {entries.length}</Text>
<Text>Loading: {loading ? "Yes" : "No"}</Text>
<Text>Offline: {isOffline ? "Yes" : "No"}</Text>
<Text>Contacts: {contacts.length}</Text>
<Text>Loading Contacts: {isLoadingContacts ? "Yes" : "No"}</Text>
<View className="flex-row mt-4 justify-between">
<TouchableOpacity
className="bg-blue-500 p-2 rounded flex-1 mr-2"
onPress={checkRelayConnections}
>
<Text className="text-white text-center">Check Relays</Text>
</TouchableOpacity>
<TouchableOpacity
className="bg-green-500 p-2 rounded flex-1"
onPress={handleRefresh}
>
<Text className="text-white text-center">Force Refresh</Text>
</TouchableOpacity>
</View>
</View>
), [currentUser?.pubkey, entries.length, loading, isOffline, contacts.length, isLoadingContacts, checkRelayConnections, handleRefresh]);
```
## Future Improvements
1. **Automatic Cache Cleanup**: Implement automatic cleanup of old cached events.
2. **Cache Synchronization**: Implement synchronization between the cache and the server.
3. **Cache Compression**: Implement compression of cached data to reduce storage usage.
4. **Cache Encryption**: Implement encryption of cached data to improve security.
5. **Cache Analytics**: Implement analytics to track cache usage and performance.

View File

@ -1,220 +0,0 @@
# Social Feed Filtering Rules
This document outlines the filtering rules for the different social feed tabs in the POWR app.
## Overview
The POWR app has three main social feed tabs:
1. **POWR** - Official content from the POWR team
2. **Following** - Content from users the current user follows
3. **Community** (formerly Global) - Content from the broader Nostr community
Each feed has specific filtering rules to ensure users see relevant fitness-related content.
## Content Types
The app handles several types of Nostr events:
- **Social Posts** (kind 1) - Regular text posts
- **Articles** (kind 30023) - Long-form content
- **Article Drafts** (kind 30024) - Unpublished long-form content
- **Workout Records** (kind 1301) - Completed workouts
- **Exercise Templates** (kind 33401) - Exercise definitions
- **Workout Templates** (kind 33402) - Workout plans
## Filtering Rules
### POWR Feed
- Shows content **only** from the official POWR account (`npub1p0wer69rpkraqs02l5v8rutagfh6g9wxn2dgytkv44ysz7avt8nsusvpjk`)
- Includes:
- Social posts (kind 1)
- Published articles (kind 30023)
- Workout records (kind 1301)
- Exercise templates (kind 33401)
- Workout templates (kind 33402)
- **Excludes** article drafts (kind 30024)
### Following Feed
- Shows content from users the current user follows
- For social posts (kind 1) and articles (kind 30023), only shows content with fitness-related tags:
- #workout
- #fitness
- #powr
- #31days
- #crossfit
- #wod
- #gym
- #strength
- #cardio
- #training
- #exercise
- Always shows workout-specific content (kinds 1301, 33401, 33402) from followed users
- **Excludes** article drafts (kind 30024)
### Community Feed
- Shows content from all users
- For social posts (kind 1) and articles (kind 30023), only shows content with fitness-related tags (same as Following Feed)
- Always shows workout-specific content (kinds 1301, 33401, 33402)
- **Excludes** article drafts (kind 30024)
### User Activity Feed
- Shows only the current user's own content
- For social posts (kind 1) and articles (kind 30023), only shows content with fitness-related tags (same as Following Feed)
- Always shows the user's workout-specific content (kinds 1301, 33401, 33402)
- **Excludes** article drafts (kind 30024)
## Implementation Details
The filtering is implemented in several key files:
- `lib/social/socialFeedService.ts` - Core service that handles feed subscriptions
- `lib/hooks/useFeedHooks.ts` - React hooks for the different feed types
- `components/social/EnhancedSocialPost.tsx` - Component that renders feed items
### Tag-Based Filtering
For social posts and articles, we filter based on the presence of fitness-related tags. This ensures that users only see content relevant to fitness and workouts.
### Content Type Filtering
Workout-specific content (kinds 1301, 33401, 33402) is always included in the feeds, as these are inherently fitness-related.
### Draft Exclusion
Article drafts (kind 30024) are excluded from all feeds to ensure users only see published content.
## Modifying Feed Filtering
If you need to modify the event types or tags used for filtering, you'll need to update the following files:
### 1. To modify event kinds (content types):
#### a. `lib/social/socialFeedService.ts`:
- The `subscribeFeed` method contains the core filtering logic
- Modify the `workoutFilter` object to change workout-specific content kinds (1301, 33401, 33402)
- Modify the `socialPostFilter` object to change social post kinds (1)
- Modify the `articleFilter` object to change article kinds (30023)
- The special case for draft articles (30024) has been removed, but you can add it back if needed
```typescript
// Example: To add a new workout-related kind (e.g., 1302)
const workoutFilter: NDKFilter = {
kinds: [1301, 33401, 33402, 1302] as any[],
// ...
};
```
#### b. `lib/hooks/useFeedHooks.ts`:
- Update the filter arrays in each hook function:
- `useFollowingFeed`
- `usePOWRFeed`
- `useGlobalFeed`
- `useUserActivityFeed`
```typescript
// Example: Adding a new kind to the POWR feed
const powrFilters = useMemo<NDKFilter[]>(() => {
if (!POWR_PUBKEY_HEX) return [];
return [
{
kinds: [1, 30023, 1302] as any[], // Added new kind 1302
authors: [POWR_PUBKEY_HEX],
limit: 25
},
// ...
];
}, []);
```
### 2. To modify fitness-related tags:
#### a. `lib/social/socialFeedService.ts`:
- Find the tag arrays in the `socialPostFilter` and `articleFilter` objects:
```typescript
socialPostFilter['#t'] = [
'workout', 'fitness', 'powr', '31days',
'crossfit', 'wod', 'gym', 'strength',
'cardio', 'training', 'exercise'
// Add new tags here
];
```
#### b. `lib/hooks/useFeedHooks.ts`:
- Update the tag arrays in each hook function:
- `useFollowingFeed`
- `useGlobalFeed`
- `useUserActivityFeed`
```typescript
'#t': [
'workout', 'fitness', 'powr', '31days',
'crossfit', 'wod', 'gym', 'strength',
'cardio', 'training', 'exercise',
'newTag1', 'newTag2' // Add new tags here
]
```
### 3. To modify content rendering:
#### a. `components/social/EnhancedSocialPost.tsx`:
- The `renderContent` method determines how different content types are displayed
- Modify this method if you add new event kinds or need to change how existing kinds are rendered
```typescript
// Example: Adding support for a new kind
case 'newContentType':
return <NewContentTypeComponent data={item.parsedContent as NewContentType} />;
```
### 4. To modify event parsing:
#### a. `lib/hooks/useSocialFeed.ts`:
- The `processEvent` function parses events based on their kind
- Update this function if you add new event kinds or change how existing kinds are processed
```typescript
// Example: Adding support for a new kind
case NEW_KIND:
feedItem = {
id: event.id,
type: 'newType',
originalEvent: event,
parsedContent: parseNewContent(event),
createdAt: timestamp
};
break;
```
### 5. Event type definitions:
#### a. `types/nostr-workout.ts`:
- Contains the `POWR_EVENT_KINDS` enum with all supported event kinds
- Update this enum if you add new event kinds
```typescript
// Example: Adding a new kind
export enum POWR_EVENT_KINDS {
// Existing kinds...
NEW_KIND = 1302,
}
```
## Testing Changes
After modifying the filtering rules, test the changes in all feed tabs:
1. POWR feed
2. Following feed
3. Community feed
4. User Activity feed (in the Profile tab)
Verify that:
- Only the expected content types appear in each feed
- Content with the specified tags is properly filtered
- New event kinds are correctly rendered
## Future Improvements
Potential future improvements to the filtering system:
- Add user-configurable filters for specific fitness interests
- Implement AI-based content relevance scoring
- Add support for more content types as the Nostr ecosystem evolves

View File

@ -1,282 +0,0 @@
### 1. Implement a Centralized Feed Management System
Based on both Olas and NDK's approaches:
- **Create a `FeedEntry` type** with clear support for different event kinds and content types
- **Implement state management using references** following NDK's pattern of using `Map` and `Set` references for better performance
- **Add proper event deduplication** using NDK's seen events tracking mechanism
### 2. Improve Event Subscription and Filtering
From NDK's implementation:
- **Utilize NDK's subscription options more effectively**:
```typescript
const subscription = ndk.subscribe(filters, {
closeOnEose: true,
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST,
groupable: true,
skipVerification: false
});
```
- **Implement caching strategies** using NDK's caching mechanisms:
```typescript
// Try cache first, then relays
const cacheStrategy = NDKSubscriptionCacheUsage.CACHE_FIRST;
```
- **Use subscription grouping** to reduce relay connections for similar queries:
```typescript
// Group similar subscriptions with a delay
const groupableDelay = 100; // ms
```
### 3. Enhance Contact List Fetching
NDK offers more sophisticated contact list handling:
- **Use NDK's contact list event fetching** with explicit validation:
```typescript
// Direct method to get follows for a user
async followSet(opts?: NDKSubscriptionOptions): Promise<Set<Hexpubkey>> {
const follows = await this.follows(opts);
return new Set(Array.from(follows).map((f) => f.pubkey));
}
```
- **Implement loading states and retry logic** using NDK's pattern:
```typescript
// Track loading state with cleanup on unmount
let isMounted = true;
// ...
return () => { isMounted = false; }
```
### 4. Enhanced Subscription Management
From NDK's subscription implementation:
- **Proper lifecycle management** for subscriptions:
```typescript
// Keep track of subscriptions for cleanup
const subscriptionRef = useRef<NDKSubscription | null>(null);
// Clean up subscription on component unmount
useEffect(() => {
return () => {
if (subscriptionRef.current) {
subscriptionRef.current.stop();
}
};
}, []);
```
- **Handle relay connection state** more effectively:
```typescript
// Monitor relay connections
ndk.pool.on('relay:connect', (relay: NDKRelay) => {
console.log(`Relay connected: ${relay.url}`);
});
```
### 5. Optimize Event Processing Pipeline
Based on NDK's efficient event handling:
- **Implement event processing with proper validation**:
```typescript
// Only process events that pass validation
if (!this.skipValidation) {
if (!ndkEvent.isValid) {
return;
}
}
```
- **Use NDK's event queuing and batch processing**:
```typescript
// Batch event processing for better performance
const updateEntries = (reason: string) => {
const newSlice = entriesFromIds(newEntriesRef.current);
// ... process in batch rather than individually
}
```
- **Implement EOSE (End of Stored Events) handling** more effectively:
```typescript
// Handle EOSE with improved timing
subscription.on('eose', () => {
if (isMounted) {
setLoading(false);
setEose(true);
// Process any accumulated events after EOSE
updateEntries('eose');
}
});
```
### 6. Implement Progressive Loading
From NDK's subscription approach:
- **Use cache-first loading with fallback to relays**:
```typescript
// Try cache first for immediate feedback
if (ndk.cacheAdapter) {
const cachedEvents = await ndk.cacheAdapter.query(subscription);
if (cachedEvents.length > 0) {
// Process cached events immediately
processCachedEvents(cachedEvents);
}
}
```
- **Implement pagination with proper thresholds**:
```typescript
// Only fetch more when needed
if (hasMore && !loading && !refreshing) {
loadMore(oldestTimestamp - 1);
}
```
## Key Architectural Improvements
1. **More Robust Feed State Management**:
- Use `useRef` for maintaining state references that don't trigger re-renders
- Implement batch updates for feed state changes
- Use proper cleanup and event deduplication
2. **Enhanced Subscription Management**:
- Use `NDKSubscription` with appropriate options
- Implement proper grouping and caching strategies
- Handle subscription lifecycle with proper cleanup
3. **Contact List Reliability**:
- Use multiple approaches to fetch and validate contacts
- Implement caching and retry logic for reliable contact detection
- Handle edge cases (like self-follows) appropriately
4. **Event Processing Optimization**:
- Implement more efficient event filtering and processing
- Use batch processing for events after EOSE
- Add proper validation and verification
5. **Performance Enhancements**:
- Use NDK's optimization patterns like subscription grouping
- Implement proper memory management for large event sets
- Use efficient data structures (Map/Set) for event tracking
## Implementation Strategy
1. **Start with rewriting the subscription layer** using NDK's patterns
2. **Then enhance the contact list fetching** with multiple reliable approaches
3. **Next, implement improved feed state management** with efficient data structures
4. **Finally, optimize the UI components** for better rendering performance
This updated plan combines the best practices from both Olas's specialized feed implementation and NDK's efficient subscription management, providing a solid foundation for building a fast, reliable, and focused social feed for workout-related content in the POWR app.
Let me review your current subscription and feed code, and identify which files we'll need to delete, update, or create to implement the improved architecture.
## Current Code Structure
From what I can see in the shared code, you have:
1. **useFeedEvents.ts** - Core hook for handling feed events
2. **useFeedHooks.ts** - Specialized feed hooks (Following, POWR, Global)
3. **useFeedMonitor.ts** - For monitoring feed state and auto-refresh
4. **powr.tsx** - The POWR tab screen component
5. **following.tsx** - The Following tab screen component
6. **global.tsx** - The Global tab screen component
7. **EnhancedSocialPost.tsx** - Component for rendering feed posts
8. **socialFeedService.ts** - Service for interacting with Nostr
## Plan for Implementation
### Files to Update (Modify):
1. **useFeedEvents.ts** - Update with NDK's subscription patterns and better event processing
- Improve subscription lifecycle management
- Implement better event deduplication
- Add batch processing and performance optimizations
2. **useFeedHooks.ts** - Enhance contact list fetching and filter creation
- Implement multiple approaches for contact list fetching
- Improve filter creation for better relevance
- Remove hardcoded fallbacks in favor of more reliable mechanisms
3. **useFeedMonitor.ts** - Minor updates for integration with new feed structure
- Enhance refresh mechanisms
- Improve state tracking
4. **following.tsx** - Update to use the new feed architecture
- Remove debug code once implementation is stable
- Improve UI for feed state feedback
5. **powr.tsx** - Update to fix infinite loop issues
- Improve component lifecycle management
- Enhance performance
6. **global.tsx** - Update for consistency with other feed implementations
- Ensure consistent behavior across all feed tabs
7. **socialFeedService.ts** - Enhance with better relay interaction
- Improve subscription creation
- Add better event filtering
### Files to Create:
1. **useFeedState.ts** - New hook for centralized feed state management
```typescript
// Manage feed entries state with efficient updates
export function useFeedState() {
// Implementation here
}
```
2. **useFeedSubscription.ts** - New hook for subscription management
```typescript
// Handle NDK subscriptions with proper lifecycle
export function useFeedSubscription(filters: NDKFilter[]) {
// Implementation here
}
```
3. **types/feed.ts** - Enhanced type definitions for feed entries
```typescript
// More comprehensive feed entry types
export interface FeedEntry {
// Enhanced type definition
}
```
4. **utils/feedUtils.ts** - Utility functions for feed operations
```typescript
// Feed-related utility functions
export function deduplicateEvents() {
// Implementation
}
```
5. **components/feed/FeedList.tsx** - Shared component for feed rendering
```typescript
// Reusable feed list component with virtualization
export function FeedList({ entries, onItemPress }) {
// Implementation
}
```
### Files to Delete:
None of the files need to be deleted entirely. Instead, we'll refactor and enhance the existing codebase to implement the new architecture.
## Implementation Order
1. First, create the new type definitions in **types/feed.ts**
2. Then, implement the new hooks in **useFeedSubscription.ts** and **useFeedState.ts**
3. Update **useFeedEvents.ts** and **useFeedHooks.ts** with improved implementations
4. Create utility functions in **utils/feedUtils.ts**
5. Implement the shared component in **components/feed/FeedList.tsx**
6. Finally, update the screen components to use the new architecture
This approach allows us to gradually refactor the codebase while maintaining functionality throughout the process. Each step builds on the previous one, ultimately resulting in a more robust and efficient feed implementation.

View File

@ -1,275 +0,0 @@
# Workout Completion Flow Design Document
## Problem Statement
Users need a clear, privacy-respecting process for completing workouts, with options to save locally and/or publish to Nostr, update templates based on changes made during the workout, and optionally share their accomplishments socially. The current implementation lacks a structured flow for these decisions and doesn't address privacy concerns around workout metrics.
## Requirements
### Functional Requirements
- Allow users to complete workouts and save data locally
- Provide options to publish workouts to Nostr with complete or limited data
- Enable social sharing of workout accomplishments
- Support template updates based on workout modifications
- Maintain proper attribution for templates
- Support offline completion with queued publishing
- Present clear workout summary and celebration screens
### Non-Functional Requirements
- Privacy: Control over what workout metrics are published
- Performance: Completion flow should respond within 500ms
- Reliability: Work offline with 100% data retention
- Usability: Max 3 steps to complete a workout
- Security: Secure handling of Nostr keys and signing
- Consistency: Match Nostr protocol specifications (NIP-4e)
## Design Decisions
### 1. Three-Tier Storage Approach
Implement a tiered approach to workout data storage and sharing: Local Only, Publish to Nostr (Complete/Limited), and Social Sharing.
**Rationale**:
- Provides users with clear control over their data privacy
- Aligns with the Nostr protocol's decentralized nature
- Balances social engagement with privacy concerns
- Enables participation regardless of privacy preferences
**Trade-offs**:
- Additional complexity in the UI
- More complex data handling logic
- Potential confusion around data visibility
### 2. Template Update Handling
When users modify a workout during execution, offer options to: Keep Original Template, Update Existing Template, or Save as New Template.
**Rationale**:
- Supports natural evolution of workout templates
- Maintains history and attribution
- Prevents accidental template modifications
- Enables template personalization
**Trade-offs**:
- Additional decision point for users
- Version tracking complexity
- Potential template proliferation
### 3. Conflict Resolution Strategy
Implement a "Last Write Wins with Notification" approach for template conflicts, with options to keep local changes, accept remote changes, or create a fork.
**Rationale**:
- Simple to implement and understand
- Provides user awareness of conflicts
- Maintains user control over conflict resolution
- Avoids blocking workout completion flow
**Trade-offs**:
- May occasionally result in lost updates
- Requires additional UI for conflict resolution
- Can create multiple versions of templates
## Technical Design
### Core Components
```typescript
// Workout Completion Options
interface WorkoutCompletionOptions {
storageType: 'local_only' | 'publish_complete' | 'publish_limited';
shareOnSocial: boolean;
socialMessage?: string;
templateAction: 'keep_original' | 'update_existing' | 'save_as_new';
newTemplateName?: string;
}
// Nostr Event Creation
interface NostrEventCreator {
createWorkoutRecord(
workout: Workout,
options: WorkoutCompletionOptions
): NostrEvent;
createSocialShare(
workoutRecord: NostrEvent,
message: string
): NostrEvent;
updateTemplate(
originalTemplate: WorkoutTemplate,
modifiedWorkout: Workout
): NostrEvent;
}
// Publishing Queue
interface PublishingQueue {
queueEvent(event: NostrEvent): Promise<void>;
processQueue(): Promise<void>;
getQueueStatus(): { pending: number, failed: number };
}
// Conflict Resolution
interface ConflictResolver {
detectConflicts(localTemplate: WorkoutTemplate, remoteTemplate: WorkoutTemplate): boolean;
resolveConflict(
localTemplate: WorkoutTemplate,
remoteTemplate: WorkoutTemplate,
resolution: 'use_local' | 'use_remote' | 'create_fork'
): WorkoutTemplate;
}
```
### Workout Completion Flow
```typescript
async function completeWorkout(
workout: Workout,
options: WorkoutCompletionOptions
): Promise<CompletionResult> {
// 1. Save complete workout data locally
await saveWorkoutLocally(workout);
// 2. Handle template updates if needed
if (workout.templateId && workout.hasChanges) {
await handleTemplateUpdate(workout, options.templateAction, options.newTemplateName);
}
// 3. Publish to Nostr if selected
let workoutEvent: NostrEvent | null = null;
if (options.storageType !== 'local_only') {
const isLimited = options.storageType === 'publish_limited';
workoutEvent = await publishWorkoutToNostr(workout, isLimited);
}
// 4. Create social share if selected
if (options.shareOnSocial && workoutEvent) {
await createSocialShare(workoutEvent, options.socialMessage || '');
}
// 5. Return completion status
return {
success: true,
localId: workout.id,
nostrEventId: workoutEvent?.id,
pendingSync: !navigator.onLine
};
}
```
## Implementation Plan
### Phase 1: Core Completion Flow
1. Implement workout completion confirmation dialog
2. Create completion options screen with storage choices
3. Build local storage functionality with workout summary
4. Add workout celebration screen with achievements
5. Implement template difference detection
### Phase 2: Nostr Integration
1. Implement workout record (kind 1301) publishing
2. Add support for limited metrics publishing
3. Create template update/versioning system
4. Implement social sharing via kind 1 posts
5. Add offline queue with sync status indicators
### Phase 3: Refinement and Enhancement
1. Add conflict detection and resolution
2. Implement template attribution preservation
3. Create version history browsing
4. Add advanced privacy controls
5. Implement achievement recognition system
## Testing Strategy
### Unit Tests
- Template difference detection
- Nostr event generation (complete and limited)
- Social post creation
- Conflict detection
- Privacy filtering logic
### Integration Tests
- End-to-end workout completion flow
- Offline completion and sync
- Template update scenarios
- Cross-device template conflict resolution
- Social sharing with quoted content
### User Testing
- Template modification scenarios
- Privacy control understanding
- Conflict resolution UX
- Workout completion satisfaction
## Observability
### Logging
- Workout completion events
- Publishing attempts and results
- Template update operations
- Conflict detection and resolution
- Offline queue processing
### Metrics
- Completion rates
- Publishing success rates
- Social sharing frequency
- Template update frequency
- Offline queue size and processing time
## Future Considerations
### Potential Enhancements
- Collaborative template editing
- Richer social sharing with images/graphics
- Template popularity and trending metrics
- Coach/trainee permission model
- Interactive workout summary visualizations
### Known Limitations
- Limited to Nostr protocol constraints
- No guaranteed deletion of published content
- Template conflicts require manual resolution
- No cross-device real-time sync
- Limited to supported NIP implementations
## Dependencies
### Runtime Dependencies
- Nostr NDK for event handling
- SQLite for local storage
- Expo SecureStore for key management
- Connectivity detection for offline mode
### Development Dependencies
- TypeScript for type safety
- React Native testing tools
- Mock Nostr relay for testing
- UI/UX prototyping tools
## Security Considerations
- Private keys never exposed to application code
- Local workout data encrypted at rest
- Clear indication of what data is being published
- Template attribution verification
- Rate limiting for publishing operations
## Rollout Strategy
### Development Phase
1. Implement core completion flow with local storage
2. Add Nostr publishing with complete/limited options
3. Implement template handling and conflict resolution
4. Add social sharing capabilities
5. Implement comprehensive testing suite
### Production Deployment
1. Release to limited beta testing group
2. Monitor completion flow metrics and error rates
3. Gather feedback on privacy controls and template handling
4. Implement refinements based on user feedback
5. Roll out to all users with clear documentation
## References
- [NIP-4e: Workout Events](https://github.com/nostr-protocol/nips/blob/4e-draft/4e.md)
- [POWR Social Features Design Document](https://github.com/docNR/powr/blob/main/docs/design/SocialDesignDocument.md)
- [Nostr NDK Documentation](https://github.com/nostr-dev-kit/ndk)
- [Offline-First Application Architecture](https://blog.flutter.io/offline-first-application-architecture-a2c4b2c61c8b)
- [React Native Performance Optimization](https://reactnative.dev/docs/performance)

View File

@ -1,848 +0,0 @@
# POWR Workout Data Flow Specification
## Overview
This document outlines the complete data flow for the workout feature, from initialization through completion and storage. The design prioritizes data integrity, performance, and future extensibility with Nostr integration.
## Data Flow Diagram
```mermaid
flowchart TD
subgraph Initialization
A[Template Selection] -->|Load Template| B[Template-to-Workout Transformation]
C[Quick Start] -->|Create Empty| B
B -->|Initialize| D[Workout Context]
end
subgraph Active Tracking
D -->|Current State| E[UI Components]
E -->|User Input| F[Dispatch Actions]
F -->|State Updates| D
D -->|Timer Events| G[Auto-save]
end
subgraph Persistence
G -->|Incremental Writes| H[(SQLite)]
I[Workout Completion] -->|Final Write| H
J[Manual Save] -->|Checkpoint| H
end
subgraph Analysis
H -->|Load History| K[PR Detection]
H -->|Aggregate Data| L[Statistics Calculation]
K --> M[Achievements]
L --> M
end
subgraph Sync
H -->|Format Events| N[Nostr Event Creation]
N -->|Publish| O[Relays]
O -->|Subscribe| P[Other Devices]
end
```
## Data Transformation Stages
### 1. Template to Workout Conversion
```typescript
interface WorkoutTemplateToWorkoutParams {
template: Template;
workoutSettings?: {
skipExercises?: string[];
addExercises?: WorkoutExercise[];
adjustRestTimes?: boolean;
scaleWeights?: number; // Percentage multiplier
};
}
function convertTemplateToWorkout(
params: WorkoutTemplateToWorkoutParams
): Workout {
// 1. Deep clone template structure
// 2. Apply user customizations
// 3. Initialize tracking metadata
// 4. Generate unique IDs
// 5. Add timestamps
}
```
Key operations:
- Exercise copies maintain reference to source exercise
- Sets are initialized with default values from template
- Additional metadata fields added for tracking
- Timestamps initialized
- IDs generated for all entities
### 2. Workout State Management
The central reducer handles all state transitions and ensures data consistency:
```typescript
function workoutReducer(
state: WorkoutState,
action: WorkoutAction
): WorkoutState {
switch (action.type) {
case 'START_WORKOUT':
return {
...state,
status: 'active',
activeWorkout: action.payload,
startTime: Date.now(),
elapsedTime: 0,
};
case 'UPDATE_SET':
const { exerciseIndex, setIndex, data } = action.payload;
const updatedExercises = [...state.activeWorkout.exercises];
const updatedSets = [...updatedExercises[exerciseIndex].sets];
updatedSets[setIndex] = {
...updatedSets[setIndex],
...data,
lastUpdated: Date.now(),
};
updatedExercises[exerciseIndex] = {
...updatedExercises[exerciseIndex],
sets: updatedSets,
};
return {
...state,
activeWorkout: {
...state.activeWorkout,
exercises: updatedExercises,
lastUpdated: Date.now(),
},
needsSave: true,
};
// Additional cases for all actions...
}
}
```
### 3. Persistence Layer
Data is saved incrementally with different strategies:
```typescript
class WorkoutPersistence {
// Save entire workout
async saveWorkout(workout: Workout): Promise<void> {
return this.db.withTransactionAsync(async () => {
// 1. Save workout metadata
// 2. Save all exercises
// 3. Save all sets
// 4. Update related statistics
});
}
// Save only modified data
async saveIncrementalChanges(workout: Workout): Promise<void> {
const dirtyExercises = workout.exercises.filter(e => e.isDirty);
return this.db.withTransactionAsync(async () => {
// Only update changed exercises and sets
for (const exercise of dirtyExercises) {
// Update exercise
// Update dirty sets
exercise.isDirty = false;
for (const set of exercise.sets) {
set.isDirty = false;
}
}
});
}
}
```
Save triggers:
1. **Auto-save**: Every 30 seconds during active workout
2. **Exercise change**: When navigating between exercises
3. **Pause**: When workout is paused
4. **Completion**: Final save with additional metadata
5. **Manual save**: User-triggered save
6. **App background**: When app moves to background
### 4. Workout Completion Processing
```typescript
async function processWorkoutCompletion(workout: Workout): Promise<WorkoutSummary> {
// 1. Mark workout as completed
const completedWorkout = {
...workout,
isCompleted: true,
endTime: Date.now(),
};
// 2. Calculate final statistics
const stats = calculateWorkoutStatistics(completedWorkout);
// 3. Detect personal records
const personalRecords = detectPersonalRecords(completedWorkout);
// 4. Save everything to database
await workoutPersistence.saveCompletedWorkout(
completedWorkout,
stats,
personalRecords
);
// 5. Return summary data
return {
workout: completedWorkout,
statistics: stats,
achievements: {
personalRecords,
streaks: detectStreaks(completedWorkout),
milestones: detectMilestones(completedWorkout),
},
};
}
```
### 5. Nostr Event Creation
```typescript
function createNostrWorkoutEvent(workout: CompletedWorkout): NostrEvent {
return {
kind: 33403, // Workout Record
content: workout.notes || '',
tags: [
['d', workout.id],
['title', workout.title],
['type', workout.type],
['start', workout.startTime.toString()],
['end', workout.endTime.toString()],
['completed', workout.isCompleted.toString()],
// Exercise data
...workout.exercises.flatMap(exercise => {
const exerciseRef = `33401:${exercise.author || 'local'}:${exercise.sourceId}`;
return exercise.sets.map(set => [
'exercise',
exerciseRef,
set.weight?.toString() || '',
set.reps?.toString() || '',
set.rpe?.toString() || '',
set.type,
]);
}),
// PR tags if applicable
...workout.personalRecords.map(pr => [
'pr',
`${pr.exerciseId},${pr.metric},${pr.value}`
]),
// Categorization tags
...workout.tags.map(tag => ['t', tag])
],
created_at: Math.floor(workout.endTime / 1000),
};
}
```
## Data Structures
### Workout State
```typescript
interface WorkoutState {
status: 'idle' | 'active' | 'paused' | 'completed';
activeWorkout: Workout | null;
currentExerciseIndex: number;
currentSetIndex: number;
startTime: number | null;
endTime: number | null;
elapsedTime: number;
restTimer: {
isActive: boolean;
duration: number;
remaining: number;
exerciseId?: string;
setIndex?: number;
};
needsSave: boolean;
lastSaved: number | null;
}
```
### Active Workout
```typescript
interface Workout {
id: string;
title: string;
type: WorkoutType;
startTime: number;
endTime?: number;
isCompleted: boolean;
templateId?: string;
exercises: WorkoutExercise[];
notes?: string;
tags: string[];
lastUpdated: number;
}
interface WorkoutExercise {
id: string;
sourceId: string; // Reference to exercise definition
title: string;
sets: WorkoutSet[];
notes?: string;
isDirty: boolean;
isCompleted: boolean;
order: number;
restTime?: number;
}
interface WorkoutSet {
id: string;
setNumber: number;
type: SetType;
weight?: number;
reps?: number;
rpe?: number;
isCompleted: boolean;
isDirty: boolean;
timestamp?: number;
notes?: string;
}
```
### Workout Summary
```typescript
interface WorkoutSummary {
id: string;
title: string;
type: WorkoutType;
duration: number; // In milliseconds
startTime: number;
endTime: number;
exerciseCount: number;
completedExercises: number;
totalVolume: number;
totalReps: number;
averageRpe?: number;
exerciseSummaries: ExerciseSummary[];
personalRecords: PersonalRecord[];
}
interface ExerciseSummary {
exerciseId: string;
title: string;
setCount: number;
completedSets: number;
volume: number;
peakWeight?: number;
totalReps: number;
averageRpe?: number;
}
```
## SQLite Schema Integration
Building on the existing schema, these additional tables and relationships will be needed:
```sql
-- Workout-specific schema extensions
-- Active workout tracking
CREATE TABLE IF NOT EXISTS active_workouts (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
type TEXT NOT NULL,
start_time INTEGER NOT NULL,
last_updated INTEGER NOT NULL,
template_id TEXT,
metadata TEXT, -- JSON blob of additional data
FOREIGN KEY(template_id) REFERENCES templates(id)
);
-- Completed workouts
CREATE TABLE IF NOT EXISTS completed_workouts (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
type TEXT NOT NULL,
start_time INTEGER NOT NULL,
end_time INTEGER NOT NULL,
duration INTEGER NOT NULL, -- In milliseconds
total_volume REAL,
total_reps INTEGER,
average_rpe REAL,
notes TEXT,
template_id TEXT,
nostr_event_id TEXT,
FOREIGN KEY(template_id) REFERENCES templates(id)
);
-- Individual workout exercises
CREATE TABLE IF NOT EXISTS workout_exercises (
id TEXT PRIMARY KEY,
workout_id TEXT NOT NULL,
exercise_id TEXT NOT NULL,
position INTEGER NOT NULL,
is_completed BOOLEAN DEFAULT 0,
notes TEXT,
rest_time INTEGER, -- In seconds
FOREIGN KEY(workout_id) REFERENCES active_workouts(id) ON DELETE CASCADE,
FOREIGN KEY(exercise_id) REFERENCES exercises(id)
);
-- Set data
CREATE TABLE IF NOT EXISTS workout_sets (
id TEXT PRIMARY KEY,
workout_exercise_id TEXT NOT NULL,
set_number INTEGER NOT NULL,
weight REAL,
reps INTEGER,
rpe REAL,
completed BOOLEAN DEFAULT 0,
set_type TEXT NOT NULL,
timestamp INTEGER,
notes TEXT,
FOREIGN KEY(workout_exercise_id) REFERENCES workout_exercises(id) ON DELETE CASCADE
);
-- Personal records
CREATE TABLE IF NOT EXISTS personal_records (
id TEXT PRIMARY KEY,
exercise_id TEXT NOT NULL,
metric TEXT NOT NULL, -- 'weight', 'reps', 'volume', etc.
value REAL NOT NULL,
workout_id TEXT NOT NULL,
achieved_at INTEGER NOT NULL,
FOREIGN KEY(exercise_id) REFERENCES exercises(id) ON DELETE CASCADE,
FOREIGN KEY(workout_id) REFERENCES completed_workouts(id)
);
-- Workout tags
CREATE TABLE IF NOT EXISTS workout_tags (
workout_id TEXT NOT NULL,
tag TEXT NOT NULL,
FOREIGN KEY(workout_id) REFERENCES completed_workouts(id) ON DELETE CASCADE,
PRIMARY KEY(workout_id, tag)
);
-- Workout statistics
CREATE TABLE IF NOT EXISTS workout_statistics (
workout_id TEXT PRIMARY KEY,
stats_json TEXT NOT NULL, -- Flexible JSON storage for various metrics
calculated_at INTEGER NOT NULL,
FOREIGN KEY(workout_id) REFERENCES completed_workouts(id) ON DELETE CASCADE
);
```
## Optimization Strategies
### 1. Batch Processing
For performance-critical operations, batch updates are used:
```typescript
// Instead of individual operations
async function saveSetsIndividually(sets: WorkoutSet[]) {
for (const set of sets) {
await db.runAsync(
'UPDATE workout_sets SET weight = ?, reps = ?, completed = ? WHERE id = ?',
[set.weight, set.reps, set.isCompleted, set.id]
);
}
}
// Use batch operations
async function saveSetsInBatch(sets: WorkoutSet[]) {
if (sets.length === 0) return;
const placeholders = sets.map(() => '(?, ?, ?, ?, ?, ?, ?)').join(', ');
const values = sets.flatMap(set => [
set.id,
set.workout_exercise_id,
set.set_number,
set.weight || null,
set.reps || null,
set.rpe || null,
set.isCompleted ? 1 : 0
]);
await db.runAsync(`
INSERT OR REPLACE INTO workout_sets
(id, workout_exercise_id, set_number, weight, reps, rpe, completed)
VALUES ${placeholders}
`, values);
}
```
### 2. Dirty Tracking
Optimize saves by only updating changed data:
```typescript
function markDirty(entity: { isDirty?: boolean, lastUpdated?: number }) {
entity.isDirty = true;
entity.lastUpdated = Date.now();
}
// In reducer
case 'UPDATE_SET': {
const { exerciseIndex, setIndex, data } = action.payload;
const updatedExercises = [...state.activeWorkout.exercises];
const updatedSets = [...updatedExercises[exerciseIndex].sets];
// Only mark as dirty if actually changed
const currentSet = updatedSets[setIndex];
const hasChanged = Object.entries(data).some(
([key, value]) => currentSet[key] !== value
);
if (hasChanged) {
updatedSets[setIndex] = {
...updatedSets[setIndex],
...data,
isDirty: true,
lastUpdated: Date.now(),
};
updatedExercises[exerciseIndex] = {
...updatedExercises[exerciseIndex],
sets: updatedSets,
isDirty: true,
lastUpdated: Date.now(),
};
}
return {
...state,
activeWorkout: {
...state.activeWorkout,
exercises: updatedExercises,
lastUpdated: hasChanged ? Date.now() : state.activeWorkout.lastUpdated,
},
needsSave: hasChanged,
};
}
```
### 3. Incremental Auto-save
```typescript
function useAutoSave(
workout: Workout | null,
needsSave: boolean,
saveWorkout: (workout: Workout) => Promise<void>
) {
const [lastSaveTime, setLastSaveTime] = useState<number | null>(null);
const saveIntervalRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
if (!workout) return;
// Set up interval for periodic saves
saveIntervalRef.current = setInterval(() => {
if (workout && needsSave) {
saveWorkout(workout)
.then(() => setLastSaveTime(Date.now()))
.catch(err => console.error('Auto-save failed:', err));
}
}, 30000); // 30 seconds
return () => {
if (saveIntervalRef.current) {
clearInterval(saveIntervalRef.current);
}
};
}, [workout, needsSave, saveWorkout]);
// Additional save on app state changes
useAppState(
(nextAppState) => {
if (nextAppState === 'background' && workout && needsSave) {
saveWorkout(workout)
.then(() => setLastSaveTime(Date.now()))
.catch(err => console.error('Background save failed:', err));
}
}
);
return lastSaveTime;
}
```
## Error Handling and Recovery
### 1. Save Failure Recovery
```typescript
async function saveWithRetry(
workout: Workout,
maxRetries = 3
): Promise<boolean> {
let attempts = 0;
while (attempts < maxRetries) {
try {
await workoutPersistence.saveWorkout(workout);
return true;
} catch (error) {
attempts++;
console.error(`Save failed (attempt ${attempts}):`, error);
if (attempts >= maxRetries) {
// Create emergency backup
await createEmergencyBackup(workout);
notifyUser('Workout save failed. Emergency backup created.');
return false;
}
// Exponential backoff
await new Promise(resolve =>
setTimeout(resolve, 1000 * Math.pow(2, attempts))
);
}
}
return false;
}
async function createEmergencyBackup(workout: Workout): Promise<void> {
try {
const backupJson = JSON.stringify(workout);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `workout-backup-${timestamp}.json`;
await FileSystem.writeAsStringAsync(
`${FileSystem.documentDirectory}backups/${filename}`,
backupJson
);
} catch (e) {
console.error('Emergency backup failed:', e);
}
}
```
### 2. Crash Recovery
```typescript
async function checkForUnfinishedWorkouts(): Promise<Workout | null> {
try {
const activeWorkouts = await db.getAllAsync<ActiveWorkoutRow>(
'SELECT * FROM active_workouts WHERE end_time IS NULL'
);
if (activeWorkouts.length === 0) return null;
// Find most recent active workout
const mostRecent = activeWorkouts.reduce((latest, current) =>
current.last_updated > latest.last_updated ? current : latest
);
// Reconstruct full workout object
return reconstructWorkoutFromDatabase(mostRecent.id);
} catch (error) {
console.error('Error checking for unfinished workouts:', error);
return null;
}
}
function useWorkoutRecovery() {
const [recoveryWorkout, setRecoveryWorkout] = useState<Workout | null>(null);
const [showRecoveryDialog, setShowRecoveryDialog] = useState(false);
useEffect(() => {
const checkRecovery = async () => {
const unfinishedWorkout = await checkForUnfinishedWorkouts();
if (unfinishedWorkout) {
setRecoveryWorkout(unfinishedWorkout);
setShowRecoveryDialog(true);
}
};
checkRecovery();
}, []);
const handleRecovery = (shouldRecover: boolean) => {
if (shouldRecover && recoveryWorkout) {
// Resume workout
dispatch({
type: 'RECOVER_WORKOUT',
payload: recoveryWorkout
});
} else if (recoveryWorkout) {
// Discard unfinished workout
workoutPersistence.discardWorkout(recoveryWorkout.id);
}
setShowRecoveryDialog(false);
};
return {
showRecoveryDialog,
recoveryWorkout,
handleRecovery
};
}
```
## Nostr Integration
### 1. Event Publishing
```typescript
async function publishWorkoutToNostr(workout: CompletedWorkout): Promise<string> {
try {
// Convert to Nostr event format
const event = createNostrWorkoutEvent(workout);
// Sign event
const signedEvent = await ndk.signer.sign(event);
// Publish to relays
await ndk.publish(signedEvent);
// Update local record with event ID
await db.runAsync(
'UPDATE completed_workouts SET nostr_event_id = ? WHERE id = ?',
[signedEvent.id, workout.id]
);
return signedEvent.id;
} catch (error) {
console.error('Failed to publish workout to Nostr:', error);
throw error;
}
}
```
### 2. Subscription Integration
```typescript
function subscribeToWorkoutEvents() {
// Subscribe to workout events from followed users
const filter = {
kinds: [33401, 33402, 33403],
authors: followedPubkeys,
since: lastSyncTimestamp
};
const subscription = ndk.subscribe(filter);
subscription.on('event', (event) => {
try {
processIncomingNostrEvent(event);
} catch (error) {
console.error('Error processing incoming event:', error);
}
});
return subscription;
}
async function processIncomingNostrEvent(event: NostrEvent) {
switch (event.kind) {
case 33401: // Exercise definition
await processExerciseDefinition(event);
break;
case 33402: // Workout template
await processWorkoutTemplate(event);
break;
case 33403: // Workout record
await processWorkoutRecord(event);
break;
}
}
```
## Metrics and Analytics
```typescript
interface WorkoutMetrics {
// Time metrics
totalDuration: number;
exerciseTime: number;
restTime: number;
averageSetDuration: number;
// Volume metrics
totalVolume: number;
volumeByExercise: Record<string, number>;
volumeByMuscleGroup: Record<string, number>;
// Intensity metrics
averageRpe: number;
peakRpe: number;
intensityDistribution: {
low: number; // Sets with RPE 1-4
medium: number; // Sets with RPE 5-7
high: number; // Sets with RPE 8-10
};
// Completion metrics
exerciseCompletionRate: number;
setCompletionRate: number;
plannedVsActualVolume: number;
}
function calculateWorkoutMetrics(workout: CompletedWorkout): WorkoutMetrics {
// Implementation of metric calculations
// ...
return metrics;
}
```
## Data Flow Timeline
1. **Workout Initialization** (t=0)
- Template loaded or empty workout created
- Initial state populated
- Workout ID generated
- Database record created
2. **Active Tracking** (t=0 → completion)
- User inputs captured through reducers
- State updates trigger UI refreshes
- Dirty tracking flags changes
- Auto-save runs periodically
3. **Exercise Transitions**
- Current exercise state saved
- Next exercise loaded
- Progress indicators updated
4. **Completion Processing**
- Final state saving
- Statistics calculation
- PR detection
- Achievement unlocking
5. **Post-Workout**
- History update
- Nostr publishing (if enabled)
- Cleanup of temporary data
## Integration with Existing Architecture
The workout data flow integrates with existing systems:
1. **Library System** - Templates loaded from library
2. **User Profiles** - PRs and achievements tied to user
3. **Social Features** - Workout sharing via Nostr
4. **History Tab** - Completed workouts appear in history
5. **Exercise Database** - Exercise references maintained
## Future Extensibility
This design supports future enhancements:
1. **AI Recommendations** - Data structured for ML analysis
2. **External Device Integration** - Schema allows for sensor data
3. **Advanced Periodization** - Tracking supports long-term planning
4. **Video Analysis** - Form tracking integration points
5. **Multi-user Workouts** - Shared workout capabilities

View File

@ -1,376 +0,0 @@
# POWR Workout Tab Design Document
## Problem Statement
Users need a dedicated interface for tracking workout sessions in real-time, including starting new workouts from templates or creating custom workouts on the fly. The workout experience must support various workout types (strength, circuit, EMOM, AMRAP), maintain offline functionality, and prepare for future Nostr integration.
## Requirements
### Functional Requirements
- Start new workouts from templates or create custom workouts
- Track sets, reps, weight, and other exercise metrics in real-time
- Support rest timers between sets and exercises
- Allow recording of RPE (Rate of Perceived Exertion)
- Enable workout notes and feedback
- Save completed workout history
- Track PRs (Personal Records)
- Support various workout structures (traditional, circuit, EMOM, AMRAP)
- Provide workout summary statistics
### Non-Functional Requirements
- Performant timer implementation (accurate to within 100ms)
- Smooth UI experience during workout tracking
- Reliable offline functionality
- Data persistence during app crashes
- Battery-efficient implementation
- Support for future Nostr event publishing (kinds 33401-33403)
## Design Decisions
### 1. Workout State Management
**Approach:** Context-based state management with reducers
**Rationale:**
- Workouts require complex state that needs to be accessed by multiple components
- Reducer pattern provides predictable state transitions
- Context allows state sharing without prop drilling
- Enables isolation of workout logic from UI components
### 2. Timer Implementation
**Approach:** Custom hook-based timer with requestAnimationFrame
**Rationale:**
- More accurate than setInterval for visual timing
- Better battery performance than interval-based approaches
- Handles background/foreground transitions gracefully
- Can be paused/resumed without drift
### 3. Offline Data Persistence
**Approach:** Incremental SQLite saves with optimistic UI updates
**Rationale:**
- Balances performance with data safety
- Prevents data loss during crashes
- Maintains responsive UI during saves
- Supports future sync capabilities
### 4. Template-to-Workout Transformation
**Approach:** Deep copy with runtime customization
**Rationale:**
- Preserves template integrity
- Allows workout-specific modifications
- Maintains type safety
- Supports progression tracking
## Technical Design
### Core Components
#### WorkoutProvider (Context)
```typescript
interface WorkoutContextState {
status: 'idle' | 'active' | 'paused' | 'completed';
activeWorkout: Workout | null;
currentExerciseIndex: number;
currentSetIndex: number;
elapsedTime: number;
restTimers: {
isActive: boolean;
duration: number;
remaining: number;
};
}
type WorkoutAction =
| { type: 'START_WORKOUT', payload: Workout }
| { type: 'PAUSE_WORKOUT' }
| { type: 'RESUME_WORKOUT' }
| { type: 'COMPLETE_WORKOUT' }
| { type: 'UPDATE_SET', payload: { exerciseIndex: number, setIndex: number, data: Partial<WorkoutSet> } }
| { type: 'NEXT_EXERCISE' }
| { type: 'PREVIOUS_EXERCISE' }
| { type: 'START_REST_TIMER', payload: number }
| { type: 'TICK_TIMER', payload: number };
function workoutReducer(state: WorkoutContextState, action: WorkoutAction): WorkoutContextState {
// State transitions and logic
}
```
#### Workout Screen Structure
```typescript
// Main layout components
function WorkoutScreen() {
// Handles routing between idle/active states
}
function ActiveWorkoutScreen() {
// Active workout tracking UI
}
function WorkoutSetupScreen() {
// Template selection or custom workout creation
}
function WorkoutSummaryScreen() {
// Post-workout summary and stats
}
```
#### Timer Hook
```typescript
function useWorkoutTimer({
isActive,
onTick,
}: {
isActive: boolean;
onTick: (elapsedMs: number) => void;
}) {
// Timer implementation using requestAnimationFrame
// Handles background/foreground transitions
}
```
### Database Schema Extensions
Building on the existing schema, we'll add:
```sql
-- Workout tracking
CREATE TABLE IF NOT EXISTS workouts (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
type TEXT NOT NULL,
start_time INTEGER NOT NULL,
end_time INTEGER,
completed BOOLEAN DEFAULT 0,
notes TEXT,
total_volume REAL,
template_id TEXT,
nostr_event_id TEXT,
FOREIGN KEY(template_id) REFERENCES templates(id)
);
-- Individual workout exercises
CREATE TABLE IF NOT EXISTS workout_exercises (
id TEXT PRIMARY KEY,
workout_id TEXT NOT NULL,
exercise_id TEXT NOT NULL,
position INTEGER NOT NULL,
notes TEXT,
FOREIGN KEY(workout_id) REFERENCES workouts(id) ON DELETE CASCADE,
FOREIGN KEY(exercise_id) REFERENCES exercises(id)
);
-- Set data
CREATE TABLE IF NOT EXISTS workout_sets (
id TEXT PRIMARY KEY,
workout_exercise_id TEXT NOT NULL,
set_number INTEGER NOT NULL,
weight REAL,
reps INTEGER,
rpe REAL,
completed BOOLEAN DEFAULT 0,
set_type TEXT NOT NULL,
timestamp INTEGER,
FOREIGN KEY(workout_exercise_id) REFERENCES workout_exercises(id) ON DELETE CASCADE
);
-- Personal records
CREATE TABLE IF NOT EXISTS personal_records (
id TEXT PRIMARY KEY,
exercise_id TEXT NOT NULL,
metric TEXT NOT NULL,
value REAL NOT NULL,
workout_id TEXT NOT NULL,
achieved_at INTEGER NOT NULL,
FOREIGN KEY(exercise_id) REFERENCES exercises(id) ON DELETE CASCADE,
FOREIGN KEY(workout_id) REFERENCES workouts(id)
);
```
### TypeScript Definitions
```typescript
// Workout Types
export interface Workout {
id: string;
title: string;
type: WorkoutType;
exercises: WorkoutExercise[];
startTime: number;
endTime?: number;
isCompleted: boolean;
notes?: string;
templateId?: string;
totalVolume?: number;
}
export type WorkoutType = 'strength' | 'circuit' | 'emom' | 'amrap';
// PR Tracking
export interface PersonalRecord {
id: string;
exerciseId: string;
metric: 'weight' | 'reps' | 'volume';
value: number;
workoutId: string;
achievedAt: number;
}
```
## User Interface Design
### Workout Flow
#### 1. Idle State / Setup
- Option to start from template
- Option to create custom workout
- Quick-start recent workouts
- Template browsing/filtering
#### 2. Active Workout Screens
- **Header:** Workout title, timer, complete button
- **Exercise Navigation:** Current exercise, navigation controls
- **Set Tracking:** Weight/reps input, rest timer, RPE selection
- **Notes:** Exercise-specific notes field
- **Progress:** Visual indicators of completion status
#### 3. Summary Screen
- Workout duration
- Total volume
- PR achievements
- Exercise completion rates
- Option to add notes
- Share capabilities (future Nostr publishing)
### UI Components
#### WorkoutHeader
Displays current status, elapsed time, and workout controls
#### ExerciseTracker
Primary interface for tracking sets of current exercise
#### RestTimer
Visual countdown with sound/vibration alerts
#### WorkoutSummary
Post-workout statistics and achievements
## Implementation Plan
### Phase 1: Core Workout Flow (2 weeks)
1. Implement WorkoutContext and reducer
2. Build workout setup screen
3. Create basic exercise tracking UI
4. Implement timer functionality
5. Add SQLite persistence for workouts
### Phase 2: Advanced Features (2 weeks)
1. Implement rest timers with alerts
2. Add PR tracking and detection
3. Create workout summary screen
4. Support for different workout types
5. Add notes and feedback options
### Phase 3: Polish & Integration (1 week)
1. UI refinements and animations
2. Performance optimization
3. Integration with Nostr publishing
4. Add sharing capabilities
5. Final testing and bug fixes
## Testing Strategy
### Unit Tests
- Timer accuracy and pause/resume functionality
- State transitions in workout reducer
- Template transformation logic
- PR detection algorithms
- Exercise progression calculations
### Integration Tests
- Workout flow from setup to completion
- Data persistence during app lifecycle events
- Template-to-workout conversion
- History recording accuracy
### Performance Tests
- Timer precision under load
- UI responsiveness during data saving
- Battery usage monitoring
- Memory profiling during long workouts
## Observability
### Logging
- Workout state transitions
- Timer accuracy metrics
- Data save operations
- Error conditions
### Analytics (Future)
- Workout completion rates
- Average workout duration
- Most used templates
- Feature usage statistics
## Future Considerations
### Potential Enhancements
- Voice feedback during workouts
- Video form checking integration
- Social sharing via Nostr
- Workout streaks and achievements
- AI-powered workout recommendations
- Heart rate monitor integration
- Barcode scanner for gym equipment
### Known Limitations
- Timer may drift slightly in background
- Workout types limited to predefined structures
- No direct hardware integrations in MVP
- Offline-only in initial implementation
## Dependencies
### Runtime Dependencies
- SQLite for data persistence
- Timer implementation libraries
- Chart visualization for summary
- Haptic feedback for timers
### Development Dependencies
- Testing framework for timer accuracy
- Mock data generators
- Performance monitoring tools
## Security Considerations
- Personal fitness data privacy
- Optional anonymization for shared workouts
- Secure storage of personal records
- Permission handling for notifications
## Rollout Strategy
### Development Phase
1. Implement core workout tracking
2. Add template integration
3. Build timer functionality
4. Create persistence layer
5. Add summary statistics
### Production Deployment
1. Internal testing with sample workouts
2. Beta testing with power users
3. Phased rollout to all users
4. Monitoring for performance issues
5. Iterative improvements based on feedback
## References
- Nostr NIP-33401: Exercise Templates
- Nostr NIP-33402: Workout Templates
- Nostr NIP-33403: Workout Records
- React Native Animation Performance Guide
- SQLite Transaction Best Practices

View File

@ -1,348 +0,0 @@
# Workout UI Component Specification
## Overview
This document outlines the key UI components needed for the POWR workout tracking experience. The interface prioritizes readability during exercise, quick data entry, and clear visualization of progress.
## Core UI Components
### 1. Workout Header
![Workout Header](https://via.placeholder.com/400x100)
```tsx
interface WorkoutHeaderProps {
title: string;
type: WorkoutType;
elapsedTime: number;
isActive: boolean;
onPause: () => void;
onResume: () => void;
onComplete: () => void;
}
```
#### Features
- Sticky header with minimized height
- Elapsed time with large, readable timer
- Workout title and type indicator
- Status indicators (active/paused)
- Action buttons (pause/resume/complete)
- Optional: calorie/heart rate display
#### Behavior
- Time updates every second
- Color changes based on active/paused state
- Confirm dialog appears before completing workout
---
### 2. Exercise Navigation
![Exercise Navigation](https://via.placeholder.com/400x80)
```tsx
interface ExerciseNavigationProps {
exercises: Array<{
id: string;
title: string;
isCompleted: boolean;
}>;
currentIndex: number;
onSelectExercise: (index: number) => void;
onNext: () => void;
onPrevious: () => void;
}
```
#### Features
- Horizontal scroll for exercise list
- Current exercise highlighted
- Progress indicators showing completion status
- Previous/next navigation controls
- Jump-to capability for non-linear workouts
#### Behavior
- Swipe gestures to change exercises
- Auto-scrolls to keep current exercise visible
- Vibration feedback on exercise change
- Optional confirmation when leaving incomplete exercise
---
### 3. Set Tracker
![Set Tracker](https://via.placeholder.com/400x300)
```tsx
interface SetTrackerProps {
sets: WorkoutSet[];
exercise: WorkoutExercise;
onUpdateSet: (setIndex: number, data: Partial<WorkoutSet>) => void;
onAddSet: () => void;
onRemoveSet: (setIndex: number) => void;
showRestTimer: boolean;
onStartRest: (duration: number) => void;
}
```
#### Features
- Individual set cards with weight/reps/RPE inputs
- Completion toggle for each set
- Previous set data for reference
- Support for different input types based on exercise format
- "Add Set" button for additional sets
- Rest timer trigger
#### Behavior
- Auto-focuses appropriate input field
- Supports quick incrementing/decrementing of values
- Auto-suggests rest time based on set intensity
- Remembers input patterns within workout
- Validates inputs against exercise constraints
---
### 4. Rest Timer
![Rest Timer](https://via.placeholder.com/300x300)
```tsx
interface RestTimerProps {
duration: number;
remaining: number;
isActive: boolean;
onPause: () => void;
onResume: () => void;
onSkip: () => void;
onExtend: (seconds: number) => void;
}
```
#### Features
- Large circular countdown display
- Visual progress indicator
- Time remaining in large font
- Control buttons (pause/resume/skip)
- Quick-extend buttons (+30s, +1m)
- Next exercise preview
#### Behavior
- Full-screen takeover when active
- Haptic feedback at 50% and 10% remaining
- Sound alert at completion (if enabled)
- Auto-dismisses after completion
- Background timer continues running
- Screen prevents sleep during active timer
---
### 5. Exercise Details Panel
![Exercise Details](https://via.placeholder.com/400x250)
```tsx
interface ExerciseDetailsPanelProps {
exercise: WorkoutExercise;
previousPerformance?: {
date: number;
sets: WorkoutSet[];
personalBests: Record<string, number>;
};
onAddNote: (note: string) => void;
}
```
#### Features
- Collapsible panel for exercise details
- Form instructions and tips
- Previous performance metrics
- Personal best indicators
- Notes field for technique reminders
- Optional media previews (images/video)
#### Behavior
- Expandable/collapsible with smooth animation
- Auto-collapses during timer to maximize screen space
- Persists notes between workout sessions
- Highlights personal records
---
### 6. Workout Controls
![Workout Controls](https://via.placeholder.com/400x120)
```tsx
interface WorkoutControlsProps {
canComplete: boolean;
onPause: () => void;
onResume: () => void;
onCancel: () => void;
onComplete: () => void;
isActive: boolean;
}
```
#### Features
- Fixed position at screen bottom
- Primary action button (Complete Workout)
- Secondary actions (pause/resume)
- Cancel workout option
- Status indicators
#### Behavior
- Complete button enables when minimum criteria met
- Confirmation dialog for cancel action
- Smooth transition animations between states
- Haptic feedback on major actions
---
### 7. Workout Summary
![Workout Summary](https://via.placeholder.com/400x500)
```tsx
interface WorkoutSummaryProps {
workout: CompletedWorkout;
achievements: {
personalRecords: PersonalRecord[];
streaks: Streak[];
milestones: Milestone[];
};
onSave: (notes: string) => void;
onShare: () => void;
onDiscard: () => void;
}
```
#### Features
- Comprehensive workout statistics
- Total volume, duration, and intensity metrics
- Exercise completion breakdown
- Personal records achieved
- Notes field for workout reflection
- Visual charts of performance
- Share and save controls
#### Behavior
- Scrollable container for all summary data
- Expandable sections for detailed stats
- Animated entry of achievement cards
- Pre-populates notes from during-workout entries
- Save confirmation with preview
## Layout Variations
### 1. Strength Workout Layout
Optimized for tracking weight, reps and rest periods.
- Prominent weight/rep inputs
- Set-rest-set pattern flow
- Previous lift stats readily visible
- PR tracking indicators
- Weight plate calculator
### 2. Circuit Workout Layout
Designed for quick transitions between exercises.
- Minimized input fields
- Prominent exercise timer
- Next exercise preview
- Round counter
- Overall circuit progress
### 3. EMOM/AMRAP Layout
Focused on timed intervals and rep counting.
- Large interval timer
- Quick rep counter
- Round progression
- Work/rest indicators
- Audio cues for intervals
## Interaction Patterns
### 1. Data Entry
- Single-tap to select input field
- Long-press for quick increment/decrement
- Swipe to mark set complete
- Shake to undo last action
- Double-tap to copy previous set values
### 2. Navigation
- Swipe between exercises
- Pull down to reveal workout overview
- Pull up for exercise details
- Pinch to zoom workout timeline
- Double-tap header to toggle timer visibility
### 3. Timers
- Tap timer to pause/resume
- Swipe up on timer for fullscreen mode
- Rotate device for alternative timer view
- Shake to skip timer
- Volume buttons as quick controls
## Accessibility Considerations
### 1. Visual
- High contrast mode for gym environments
- Large text option for all metrics
- Color-blind friendly progress indicators
- Screen rotation lock option
- Auto-brightness adjustment
### 2. Motor
- Large touch targets for sweaty hands
- Voice control for hands-free operation
- Simplified layout option with fewer controls
- Adjustable button sensitivity
- Support for external Bluetooth controls
### 3. Auditory
- Vibration patterns as alternative to sound
- Visual countdown alternatives
- Adjustable volume levels
- Custom sound selection
- Background noise filtering for voice features
## State Transitions
### 1. Idle → Active
- Template selection or quick start
- Exercise preview animation
- Timer initialization
- Welcome guidance (configurable)
### 2. Active → Paused
- Dim UI elements
- Prominent resume button
- Elapsed time continues but visually distinguished
- Quick access to notes and adjustments
### 3. Active → Complete
- Celebration animation
- Stats calculation overlay
- Achievement unlocks
- Social share prompts (optional)
- Return to home or next workout suggestion
## Theme Integration
All components should support both light and dark themes with special considerations:
1. **Dark gym mode** - Ultra-dark background with high contrast elements for poorly lit environments
2. **Outdoor mode** - High contrast, glare-resistant design for outdoor workouts
3. **Night mode** - Red-shifted colors to minimize blue light during evening workouts
4. **Energy saver** - Minimalist UI with reduced animations to maximize battery life
## Component Integration
These components will integrate with the existing POWR architecture:
1. **Component Library** - Extends existing UI components with workout-specific variants
2. **Theme System** - Utilizes current theme tokens with workout-specific additions
3. **Navigation** - Embeds within the tab navigation as a modal flow when active
4. **Context** - Consumes the WorkoutContext for state management
## Next Steps
1. Create low-fidelity wireframes for each component
2. Develop component prototypes using existing design system
3. Test input patterns with sample workout data
4. Validate timer accuracy across devices
5. Create component documentation in Storybook

View File

@ -1,303 +0,0 @@
# NIP-4e: Workout Events
`draft` `optional`
This specification defines workout events for fitness tracking. These workout events support both planning (templates) and recording (completed activities).
## Event Kinds
### Event Kind Selection Rationale
The event kinds in this NIP follow Nostr protocol conventions:
- **Exercise and Workout Templates** (33401, 33402) use parameterized replaceable event kinds (30000+) because:
- They represent content that may be updated or improved over time
- The author may want to replace previous versions with improved ones
- They need the `d` parameter to distinguish between different templates by the same author
- Multiple versions shouldn't accumulate in clients' storage
- **Workout Records** (1301) use a standard event kind (0-9999) because:
- They represent a chronological feed of activity that shouldn't replace previous records
- Each workout is a unique occurrence that adds to a user's history
- Users publish multiple records over time, creating a timeline
- They're conceptually similar to notes (kind 1) but with structured fitness data
### Exercise Template (kind: 33401)
Defines reusable exercise definitions. These should remain public to enable discovery and sharing. The `content` field contains detailed form instructions and notes.
#### Format
The format uses an _addressable event_ of `kind:33401`.
The `.content` of these events SHOULD be detailed instructions for proper exercise form. It is required but can be an empty string.
The list of tags are as follows:
* `d` (required) - universally unique identifier (UUID). Generated by the client creating the exercise template.
* `title` (required) - Exercise name
* `format` (required) - Defines data structure for exercise tracking (possible parameters: `weight`, `reps`, `rpe`, `set_type`)
* `format_units` (required) - Defines units for each parameter (possible formats: "kg", "count", "0-10", "warmup|normal|drop|failure")
* `equipment` (required) - Equipment type (possible values: `barbell`, `dumbbell`, `bodyweight`, `machine`, `cardio`)
* `difficulty` (optional) - Skill level (possible values: `beginner`, `intermediate`, `advanced`)
* `imeta` (optional) - Media metadata for form demonstrations following NIP-92 format
* `t` (optional, repeated) - Hashtags for categorization such as muscle group or body movement (possible values: `chest`, `legs`, `push`, `pull`)
```
{
"id": <32-bytes lowercase hex-encoded SHA-256 of the serialized event data>,
"pubkey": <32-bytes lowercase hex-encoded public key of the event creator>,
"created_at": <Unix timestamp in seconds>,
"kind": 33401,
"content": "<detailed form instructions and notes>",
"tags": [
["d", "<UUID>"],
["title", "<exercise name>"],
["format", "<parameter>", "<parameter>", "<parameter>", "<parameter>"],
["format_units", "<unit>", "<unit>", "<unit>", "<unit>"],
["equipment", "<equipment type>"],
["difficulty", "<skill level>"],
["imeta",
"url <url to demonstration media>",
"m <media type>",
"dim <dimensions>",
"alt <alt text>"
],
["t", "<hashtag>"],
["t", "<hashtag>"],
["t", "<hashtag>"]
]
}
```
### Workout Template (kind: 33402)
Defines a complete workout plan. The `content` field contains workout notes and instructions. Workout templates can prescribe specific parameters while leaving others configurable by the user performing the workout.
#### Format
The format uses an _addressable event_ of `kind:33402`.
The `.content` of these events SHOULD contain workout notes and instructions. It is required but can be an empty string.
The list of tags are as follows:
* `d` (required) - universally unique identifier (UUID). Generated by the client creating the workout template.
* `title` (required) - Workout name
* `type` (required) - Type of workout (possible values: `strength`, `circuit`, `emom`, `amrap`)
* `exercise` (required, repeated) - Exercise reference and prescription. Format: ["exercise", "<kind>:<pubkey>:<d-tag>", "<relay-url>", ...parameters matching exercise template format]
* `rounds` (optional) - Number of rounds for repeating formats
* `duration` (optional) - Total workout duration in seconds
* `interval` (optional) - Duration of each exercise portion in seconds (for timed workouts)
* `rest_between_rounds` (optional) - Rest time between rounds in seconds
* `t` (optional, repeated) - Hashtags for categorization
```
{
"id": <32-bytes lowercase hex-encoded SHA-256 of the serialized event data>,
"pubkey": <32-bytes lowercase hex-encoded public key of the event creator>,
"created_at": <Unix timestamp in seconds>,
"kind": 33402,
"content": "<workout notes and instructions>",
"tags": [
["d", "<UUID>"],
["title", "<workout name>"],
["type", "<workout type>"],
["rounds", "<number of rounds>"],
["duration", "<duration in seconds>"],
["interval", "<interval in seconds>"],
["rest_between_rounds", "<rest time in seconds>"],
["exercise", "<kind>:<pubkey>:<d-tag>", "<relay-url>", "<param1>", "<param2>", "<param3>", "<param4>"],
["exercise", "<kind>:<pubkey>:<d-tag>", "<relay-url>", "<param1>", "<param2>", "<param3>", "<param4>"],
["t", "<hashtag>"],
["t", "<hashtag>"]
]
}
```
### Workout Record (kind: 1301)
Records a completed workout session. The `content` field contains notes about the workout.
#### Format
The format uses a standard event of `kind:1301`.
The `.content` of these events SHOULD contain notes about the workout experience. It is required but can be an empty string.
The list of tags are as follows:
* `d` (required) - universally unique identifier (UUID). Generated by the client creating the workout record.
* `title` (required) - Workout name
* `type` (required) - Type of workout (possible values: `strength`, `circuit`, `emom`, `amrap`)
* `exercise` (required, repeated) - Exercise reference and completion data. Format: ["exercise", "<kind>:<pubkey>:<d-tag>", "<relay-url>", ...parameters matching exercise template format]
* `start` (required) - Unix timestamp in seconds for workout start
* `end` (required) - Unix timestamp in seconds for workout end
* `completed` (required) - Boolean indicating if workout was completed as planned
* `rounds_completed` (optional) - Number of rounds completed
* `interval` (optional) - Duration of each exercise portion in seconds (for timed workouts)
* `template` (optional) - Reference to the workout template used, if any. Format: ["template", "<kind>:<pubkey>:<d-tag>", "<relay-url>"]
* `pr` (optional, repeated) - Personal Record achieved during workout. Format: "<kind>:<pubkey>:<d-tag>,<metric>,<value>"
* `t` (optional, repeated) - Hashtags for categorization
```
{
"id": <32-bytes lowercase hex-encoded SHA-256 of the serialized event data>,
"pubkey": <32-bytes lowercase hex-encoded public key of the event creator>,
"created_at": <Unix timestamp in seconds>,
"kind": 1301,
"content": "<workout notes>",
"tags": [
["d", "<UUID>"],
["title", "<workout name>"],
["type", "<workout type>"],
["rounds_completed", "<number of rounds completed>"],
["start", "<Unix timestamp in seconds>"],
["end", "<Unix timestamp in seconds>"],
["exercise", "<kind>:<pubkey>:<d-tag>", "<relay-url>", "<weight>", "<reps>", "<rpe>", "<set_type>"],
["exercise", "<kind>:<pubkey>:<d-tag>", "<relay-url>", "<weight>", "<reps>", "<rpe>", "<set_type>"],
["template", "<kind>:<pubkey>:<d-tag>", "<relay-url>"],
["pr", "<kind>:<pubkey>:<d-tag>,<metric>,<value>"],
["completed", "<true/false>"],
["t", "<hashtag>"],
["t", "<hashtag>"]
]
}
```
## Exercise Parameters
### Standard Parameters and Units
* `weight` - Load in kilograms (kg). Empty string for bodyweight exercises, negative values for assisted exercises
* `reps` - Number of repetitions (count)
* `rpe` - Rate of Perceived Exertion (0-10):
- RPE 10: Could not do any more reps, technical failure
- RPE 9: Could maybe do 1 more rep
- RPE 8: Could definitely do 1 more rep, maybe 2
- RPE 7: Could do 2-3 more reps
* `duration` - Time in seconds
* `set_type` - Set classification (possible values: `warmup`, `normal`, `drop`, `failure`)
Additional parameters can be defined in exercise templates in the `format_units` tag as needed for specific activities (e.g., distance, heartrate, intensity).
## Workout Types and Terminology
This specification provides examples of common workout structures but is not limited to these types. The format is extensible to support various training methodologies while maintaining consistent data structure.
### Common Workout Types
#### Strength
Traditional strength training focusing on sets and reps with defined weights. Typically includes warm-up sets, working sets, and may include techniques like drop sets or failure sets.
#### Circuit
Multiple exercises performed in sequence with minimal rest between exercises and defined rest periods between rounds. Focuses on maintaining work rate through prescribed exercises.
#### EMOM (Every Minute On the Minute)
Time-based workout where specific exercises are performed at the start of each minute. Rest time is whatever remains in the minute after completing prescribed work.
#### AMRAP (As Many Rounds/Reps As Possible)
Time-capped workout where the goal is to complete as many rounds or repetitions as possible of prescribed exercises while maintaining proper form.
## Set Types
### Normal Sets
Standard working sets that count toward volume and progress tracking.
### Warm-up Sets
Preparatory sets using submaximal weights. These sets are not counted in metrics or progress tracking.
### Drop Sets
Sets performed immediately after a working set with reduced weight. These are counted in volume calculations but tracked separately for progress analysis.
### Failure Sets
Sets where technical failure was reached before completing prescribed reps. These sets are counted in metrics but marked to indicate intensity/failure was reached.
## Examples
### Exercise Template
```
{
"kind": 33401,
"content": "Stand with feet hip-width apart, barbell over midfoot. Hinge at hips, grip bar outside knees. Flatten back, brace core. Drive through floor, keeping bar close to legs.\n\nForm demonstration: https://powr.me/exercises/deadlift-demo.mp4",
"tags": [
["d", "<UUID-deadlift>"],
["title", "Barbell Deadlift"],
["format", "weight", "reps", "rpe", "set_type"],
["format_units", "kg", "count", "0-10", "warmup|normal|drop|failure"],
["equipment", "barbell"],
["difficulty", "intermediate"],
["imeta",
"url https://powr.me/exercises/deadlift-demo.mp4",
"m video/mp4",
"dim 1920x1080",
"alt Demonstration of proper barbell deadlift form"
],
["t", "compound"],
["t", "legs"],
["t", "posterior"]
]
}
```
### EMOM Workout Template
```
{
"kind": 33402,
"content": "20 minute EMOM alternating between squats and deadlifts every 30 seconds. Scale weight as needed to complete all reps within each interval.",
"tags": [
["d", "<UUID-emom-template>"],
["title", "20min Squat/Deadlift EMOM"],
["type", "emom"],
["duration", "1200"],
["rounds", "20"],
["interval", "30"],
["exercise", "33401:<pubkey>:<UUID-squat>", "<relay-url>", "", "5", "7", "normal"],
["exercise", "33401:<pubkey>:<UUID-deadlift>", "<relay-url>", "", "4", "7", "normal"],
["t", "conditioning"],
["t", "legs"]
]
}
```
### Circuit Workout Record
```
{
"kind": 1301,
"content": "Completed first round as prescribed. Second round showed form deterioration on deadlifts.",
"tags": [
["d", "<UUID-workout-record>"],
["title", "Leg Circuit"],
["type", "circuit"],
["rounds_completed", "1.5"],
["start", "1706454000"],
["end", "1706455800"],
["exercise", "33401:<pubkey>:<UUID-squat>", "<relay-url>", "80", "12", "7", "normal"],
["exercise", "33401:<pubkey>:<UUID-deadlift>", "<relay-url>", "100", "10", "7", "normal"],
["exercise", "33401:<pubkey>:<UUID-squat>", "<relay-url>", "80", "12", "8", "normal"],
["exercise", "33401:<pubkey>:<UUID-deadlift>", "<relay-url>", "100", "4", "10", "failure"],
["completed", "false"],
["t", "legs"]
]
}
```
## Implementation Guidelines
1. All workout records MUST include accurate start and end times
2. Templates MAY prescribe specific parameters while leaving others as empty strings for user input
3. Records MUST include actual values for all parameters defined in exercise format
4. Failed sets SHOULD be marked with `failure` set_type
5. Records SHOULD be marked as `false` for completed if prescribed work wasn't completed
6. PRs SHOULD only be tracked in workout records, not templates
7. Exercise references MUST use the format "kind:pubkey:d-tag" to ensure proper attribution and versioning
## References
This NIP draws inspiration from:
- [NIP-01: Basic Protocol Flow Description](https://github.com/nostr-protocol/nips/blob/master/01.md)
- [NIP-52: Calendar Events](https://github.com/nostr-protocol/nips/blob/master/52.md)
- [NIP-92: Media Attachments](https://github.com/nostr-protocol/nips/blob/master/92.md#nip-92)

View File

@ -71,25 +71,28 @@ export function useWorkoutHistory(options: UseWorkoutHistoryOptions = {}) {
// Set up real-time subscription if enabled
useEffect(() => {
if (!realtime || !isAuthenticated || !currentUser?.pubkey || !includeNostr) {
if (!realtime || !isAuthenticated || !currentUser?.pubkey) {
return;
}
// Subscribe to real-time updates
// Only create the subscription when we need it (not dependent on includeNostr to prevent re-subs)
const subId = workoutHistoryService.subscribeToNostrWorkouts(
currentUser.pubkey,
(newWorkout) => {
setWorkouts(prev => {
// Check if workout already exists
const exists = prev.some(w => w.id === newWorkout.id);
if (exists) {
// Update existing workout
return prev.map(w => w.id === newWorkout.id ? newWorkout : w);
} else {
// Add new workout
return [newWorkout, ...prev];
}
});
// Only update state if we're including Nostr workouts
if (includeNostr) {
setWorkouts(prev => {
// Check if workout already exists
const exists = prev.some(w => w.id === newWorkout.id);
if (exists) {
// Update existing workout
return prev.map(w => w.id === newWorkout.id ? newWorkout : w);
} else {
// Add new workout
return [newWorkout, ...prev];
}
});
}
}
);
@ -97,7 +100,9 @@ export function useWorkoutHistory(options: UseWorkoutHistoryOptions = {}) {
return () => {
workoutHistoryService.unsubscribeFromNostrWorkouts(subId);
};
}, [workoutHistoryService, currentUser?.pubkey, isAuthenticated, realtime, includeNostr]);
// Remove includeNostr from dependencies to prevent re-subs when it changes
// We handle the includeNostr state inside the callback instead
}, [workoutHistoryService, currentUser?.pubkey, isAuthenticated, realtime]);
// Refresh function for pull-to-refresh
const refresh = useCallback(() => {