diff --git a/CHANGELOG.md b/CHANGELOG.md index 2feb80d..4af150e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,33 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Removed +- Removed "Use Template" button from the home screen for MVP + - Simplified UI to focus on Quick Start option for MVP + - Preserved underlying functionality for future re-implementation + +### Fixed +- UI issues in workout completion flow + - Fixed weird line across input box in the 1301 event content box by adding subtle border styling + - Removed template options section that was causing bugs with workout templates + - Updated input styling to use consistent design across all multiline inputs + - Made input boxes clearly visible in light mode with thin borders + - Standardized spacing between input fields and buttons +### Fixed +- Exercise display improvements in social feeds and history + - Fixed exercise IDs showing instead of proper names in social feeds and history tabs + - Enhanced exercise name resolution with improved POWR format ID handling + - Enhanced exercise name resolution in workout history service + - Improved exercise name extraction for social feed posts + - Added comprehensive pattern matching for common exercises + - Added fallback naming strategies for unrecognized exercise IDs + - Implemented database lookup system for exercise titles + - Added name resolution for UUID-based exercise IDs in social feed + - Enhanced workout templates to display proper exercise names + - Resolved exercise titles in social feed posts via database integration + - Added specific handling for "local:" prefixed IDs in exercise naming + - Improved debug logging for exercise name resolution + - Enhanced EnhancedSocialPost components with multi-strategy name resolution ### Added - Authentication persistence debugging tools - Created dedicated AuthPersistenceTest screen for diagnosing credential issues @@ -26,6 +53,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved user experience during temporary API failures ### Improved +- Workout completion flow and publishing process + - Added workout description field for better context in workout records + - Enhanced NostrWorkoutService with client tag for better Nostr client display + - Improved social message formatting with comprehensive workout details + - Added publication state tracking (saving, publishing, error states) + - Enhanced error handling with descriptive messages + - Added visual indicators for publishing progress + - Implemented navigation guards to prevent back navigation during publishing + - Added formatted workout summary in social posts (date, time, duration, volume) + - Improved accessibility with disabled UI elements during publishing + - Authentication initialization sequence - Added proper awaiting of NDK relay connections - Implemented credential migration before authentication starts @@ -685,53 +723,4 @@ g - Created POWRPackService for fetching, importing, and managing packs - Built NostrIntegration helper for conversion between Nostr events and local models - Implemented interface to browse and import workout packs from the community - - Added pack management screen with import/delete functionality - - Created pack discovery in POWR Community tab - - Added dependency tracking for exercises required by templates - - Implemented selective import with smart dependency management - - Added clipboard support for sharing pack addresses - -## Improved -- Enhanced Social experience - - Added POWR Pack discovery to POWR Community tab - - Implemented horizontal scrolling gallery for featured packs - - Added loading states with skeleton UI - - Improved visual presentation of shared content -- Settings drawer enhancements - - Added POWR Packs management option - - Improved navigation structure -- Nostr integration - - Added support for NIP-51 lists (kind 30004) - - Enhanced compatibility between app models and Nostr events - - Improved type safety for Nostr operations - - Better error handling for network operations - - Expanded event type support for templates and exercises - -# Changelog - March 9, 2025 - -## Added -- Relay management system - - Added relays table to SQLite schema (version 3) - - Created RelayService for database operations - - Implemented RelayStore using Zustand for state management - - Added compatibility layer for NDK and NDK-mobile - - Added relay management UI in settings drawer - - Implemented relay connection status tracking - - Added support for read/write permissions - - Created relay initialization system with defaults - -## Improved -- Enhanced NDK initialization - - Added proper relay configuration loading - - Improved connection status tracking - - Enhanced error handling for relay operations -- Settings drawer enhancements - - Added relay management option - - Improved navigation structure - - Enhanced user interface -- NDK compatibility - - Created universal interfaces for NDK implementations - - Added type safety for complex operations - - Improved error handling throughout relay management - -# Changelog - March 8, + - Added pack management screen diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index bc01506..abdfbd6 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -249,7 +249,7 @@ export default function WorkoutScreen() { Begin a new workout or choose from one of your templates. - {/* Buttons from HomeWorkout but directly here */} + {/* Quick Start button */} - - @@ -393,4 +385,4 @@ export default function WorkoutScreen() { ); -} \ No newline at end of file +} diff --git a/app/(workout)/complete.tsx b/app/(workout)/complete.tsx index 557346e..fb3cd6d 100644 --- a/app/(workout)/complete.tsx +++ b/app/(workout)/complete.tsx @@ -1,6 +1,6 @@ // app/(workout)/complete.tsx import React, { useEffect } from 'react'; -import { View, Modal, TouchableOpacity } from 'react-native'; +import { View, Modal, TouchableOpacity, Alert } from 'react-native'; import { router } from 'expo-router'; import { Text } from '@/components/ui/text'; import { X } from 'lucide-react-native'; @@ -25,7 +25,7 @@ import { useColorScheme } from '@/lib/theme/useColorScheme'; * was set when the user confirmed finishing in the create screen. */ export default function CompleteWorkoutScreen() { - const { resumeWorkout, activeWorkout } = useWorkoutStore(); + const { resumeWorkout, activeWorkout, isPublishing, publishingStatus } = useWorkoutStore(); const { isDarkColorScheme } = useColorScheme(); // Check if we have a workout to complete @@ -44,8 +44,21 @@ export default function CompleteWorkoutScreen() { await completeWorkout(options); }; - // Handle cancellation (go back to workout) + // Check if we can safely close the modal + const canClose = !isPublishing; + + // Handle cancellation (go back to workout) with safety check const handleCancel = () => { + if (!canClose) { + // Show alert about publishing in progress + Alert.alert( + "Publishing in Progress", + "Please wait for publishing to complete before going back.", + [{ text: "OK" }] + ); + return; + } + // Go back to the workout screen router.back(); }; @@ -65,7 +78,12 @@ export default function CompleteWorkoutScreen() { {/* Header */} Complete Workout - + diff --git a/components/DatabaseProvider.tsx b/components/DatabaseProvider.tsx index 3ee1315..14f7ee0 100644 --- a/components/DatabaseProvider.tsx +++ b/components/DatabaseProvider.tsx @@ -13,6 +13,7 @@ import { logDatabaseInfo } from '@/lib/db/debug'; import { useNDKStore } from '@/lib/stores/ndk'; import { useLibraryStore } from '@/lib/stores/libraryStore'; import { createLogger, setQuietMode } from '@/lib/utils/logger'; +import { setDatabaseConnection } from '@/types/nostr-workout'; // Create database-specific logger const logger = createLogger('DatabaseProvider'); @@ -107,6 +108,14 @@ export function DatabaseProvider({ children }: DatabaseProviderProps) { React.useEffect(() => { if (isReady && services.db) { console.log('[DB] Database ready - triggering initial library refresh'); + // Set database for exercise title lookups + try { + const { setDatabaseConnection } = require('@/types/nostr-workout'); + setDatabaseConnection(services.db); + console.log('[DB] Database connection set for exercise title lookups'); + } catch (error) { + console.error('[DB] Failed to set database for exercise title lookups:', error); + } // Refresh all library data useLibraryStore.getState().refreshAll(); } @@ -234,6 +243,10 @@ export function DatabaseProvider({ children }: DatabaseProviderProps) { db, }); + // Set database connection for exercise title lookups + setDatabaseConnection(db); + console.log('[DB] Database connection set for exercise title lookups'); + // Display database info in development mode if (__DEV__) { await logDatabaseInfo(); diff --git a/components/social/EnhancedSocialPost.tsx b/components/social/EnhancedSocialPost.tsx index 54f2d23..91654c6 100644 --- a/components/social/EnhancedSocialPost.tsx +++ b/components/social/EnhancedSocialPost.tsx @@ -18,6 +18,7 @@ import { } from '@/types/nostr-workout'; import { formatDistance } from 'date-fns'; import Markdown from 'react-native-markdown-display'; +import { useExerciseNames, useTemplateExerciseNames } from '@/lib/hooks/useExerciseNames'; // Helper functions for all components to use // Format timestamp @@ -236,6 +237,33 @@ export default function EnhancedSocialPost({ item, onPress }: SocialPostProps) { // Component for workout records function WorkoutContent({ workout }: { workout: ParsedWorkoutRecord }) { + // Use our React Query hook for resolving exercise names + const { + data: exerciseNames = {}, + isLoading, + isError + } = useExerciseNames(workout); + + // Add enhanced logging for debugging + useEffect(() => { + console.log('[DEBUG] Original workout exercises:', workout.exercises); + console.log('[DEBUG] Resolved exercise names:', exerciseNames); + }, [workout.exercises, exerciseNames]); + + // Log status for debugging + useEffect(() => { + if (isLoading) { + console.log('[WorkoutContent] Loading exercise names...'); + } else if (isError) { + console.error('[WorkoutContent] Error loading exercise names'); + } else if (exerciseNames) { + // Log more details about each exercise ID for debugging + Object.entries(exerciseNames).forEach(([id, name]) => { + console.log(`[WorkoutContent] Exercise ${id} resolved to: ${name}`); + }); + } + }, [isLoading, isError, exerciseNames]); + return ( {workout.title} @@ -262,14 +290,53 @@ function WorkoutContent({ workout }: { workout: ParsedWorkoutRecord }) { {workout.exercises.length > 0 && ( Exercises: - {workout.exercises.slice(0, 3).map((exercise, index) => ( - - • {exercise.name} - {exercise.weight ? ` - ${exercise.weight}kg` : ''} - {exercise.reps ? ` × ${exercise.reps}` : ''} - {exercise.rpe ? ` @ RPE ${exercise.rpe}` : ''} - - ))} + {workout.exercises.slice(0, 3).map((exercise, index) => { + // Get the exercise ID + const exerciseId = exercise.id; + + // Enhanced name resolution with multiple strategies + let displayName; + + // Strategy 1: Check exerciseNames from useExerciseNames hook + if (exerciseNames[exerciseId]) { + displayName = exerciseNames[exerciseId]; + console.log(`[SocialFeed] Using resolved name for ${exerciseId}: ${displayName}`); + } + // Strategy 2: Check existing name if it's meaningful + else if (exercise.name && exercise.name !== 'Exercise' && exercise.name !== 'Unknown Exercise') { + displayName = exercise.name; + console.log(`[SocialFeed] Using original name for ${exerciseId}: ${displayName}`); + } + // Strategy 3: Format POWR-specific IDs nicely + else if (exerciseId && exerciseId.match(/^m[a-z0-9]{7}-[a-z0-9]{10}$/i)) { + displayName = `Exercise ${exerciseId.substring(1, 5).toUpperCase()}`; + console.log(`[SocialFeed] Formatting POWR ID ${exerciseId} to: ${displayName}`); + } + // Strategy 4: Handle "local:" prefix + else if (exerciseId && exerciseId.startsWith('local:')) { + const localId = exerciseId.substring(6); + if (localId.match(/^m[a-z0-9]{7}-[a-z0-9]{10}$/i)) { + displayName = `Exercise ${localId.substring(1, 5).toUpperCase()}`; + console.log(`[SocialFeed] Formatting local POWR ID ${exerciseId} to: ${displayName}`); + } else { + displayName = `Exercise ${index + 1}`; + } + } + // Strategy 5: Last resort fallback + else { + displayName = `Exercise ${index + 1}`; + console.log(`[SocialFeed] Using fallback name for ${exerciseId}: ${displayName}`); + } + + return ( + + • {displayName} + {exercise.weight ? ` - ${exercise.weight}kg` : ''} + {exercise.reps ? ` × ${exercise.reps}` : ''} + {exercise.rpe ? ` @ RPE ${exercise.rpe}` : ''} + + ); + })} {workout.exercises.length > 3 && ( +{workout.exercises.length - 3} more exercises @@ -336,6 +403,28 @@ function ExerciseContent({ exercise }: { exercise: ParsedExerciseTemplate }) { // Component for workout templates function TemplateContent({ template }: { template: ParsedWorkoutTemplate }) { + // Use our React Query hook for resolving template exercise names + const { + data: exerciseNames = {}, + isLoading, + isError + } = useTemplateExerciseNames(template.id, template.exercises); + + // Log status for debugging with more detailed information + useEffect(() => { + if (isLoading) { + console.log('[TemplateContent] Loading exercise names...'); + } else if (isError) { + console.error('[TemplateContent] Error loading exercise names'); + } else if (exerciseNames) { + // Log more details about each exercise ID for debugging + Object.entries(exerciseNames).forEach(([id, name]) => { + console.log(`[TemplateContent] Exercise ${id} resolved to: ${name}`); + }); + console.log('[TemplateContent] Exercise names loaded:', exerciseNames); + } + }, [isLoading, isError, exerciseNames]); + return ( {template.title} @@ -368,11 +457,36 @@ function TemplateContent({ template }: { template: ParsedWorkoutTemplate }) { {template.exercises.length > 0 && ( Exercises: - {template.exercises.slice(0, 3).map((exercise, index) => ( - - • {exercise.name || 'Exercise ' + (index + 1)} - - ))} + {template.exercises.slice(0, 3).map((exercise, index) => { + // Get the exercise ID for better debugging + const exerciseId = exercise.reference; + + // First try to get the name from exerciseNames + let displayName = exerciseNames[exerciseId]; + + // If no name found, try to use the existing name + if (!displayName && exercise.name && exercise.name !== 'Exercise') { + displayName = exercise.name; + } + + // Special handling for POWR-specific ID format (Mxxxxxxx-xxxxxxxxxx) + if (!displayName && exerciseId && exerciseId.match(/^m[a-z0-9]{7}-[a-z0-9]{10}$/i)) { + // Create a better-looking name from the ID: first 4 chars after the 'm' + displayName = `Exercise ${exerciseId.substring(1, 5).toUpperCase()}`; + console.log(`[TemplateContent] Formatted POWR ID ${exerciseId} to ${displayName}`); + } + + // Final fallback + if (!displayName) { + displayName = `Exercise ${index + 1}`; + } + + return ( + + • {displayName} + + ); + })} {template.exercises.length > 3 && ( +{template.exercises.length - 3} more exercises @@ -504,16 +618,58 @@ function ArticleQuote({ article }: { article: ParsedLongformContent }) { // Simplified versions of content for quoted posts function WorkoutQuote({ workout }: { workout: ParsedWorkoutRecord }) { + // Use our hook for resolving exercise names + const { data: exerciseNames = {} } = useExerciseNames(workout); + + // Count properly named exercises for better display + const namedExerciseCount = workout.exercises.slice(0, 2).map(ex => { + const exerciseId = ex.id; + + // Enhanced name resolution with multiple strategies + let displayName; + + // Strategy 1: Check exerciseNames from hook + if (exerciseNames[exerciseId]) { + displayName = exerciseNames[exerciseId]; + } + // Strategy 2: Check existing name if it's meaningful + else if (ex.name && ex.name !== 'Exercise' && ex.name !== 'Unknown Exercise') { + displayName = ex.name; + } + // Strategy 3: Format POWR-specific IDs nicely + else if (exerciseId && exerciseId.match(/^m[a-z0-9]{7}-[a-z0-9]{10}$/i)) { + displayName = `Exercise ${exerciseId.substring(1, 5).toUpperCase()}`; + } + // Strategy 4: Handle "local:" prefix + else if (exerciseId && exerciseId.startsWith('local:')) { + const localId = exerciseId.substring(6); + if (localId.match(/^m[a-z0-9]{7}-[a-z0-9]{10}$/i)) { + displayName = `Exercise ${localId.substring(1, 5).toUpperCase()}`; + } else { + displayName = `Exercise`; + } + } + // Final fallback + else { + displayName = `Exercise`; + } + + return displayName; + }); + return ( {workout.title} {workout.exercises.length} exercises • { - workout.startTime && workout.endTime ? - formatDuration(workout.endTime - workout.startTime) : - 'Duration N/A' - } + namedExerciseCount.length > 0 ? namedExerciseCount.join(', ') : '' + } {namedExerciseCount.length < workout.exercises.length && workout.exercises.length > 2 ? '...' : ''} + {workout.startTime && workout.endTime && ( + + Duration: {formatDuration(workout.endTime - workout.startTime)} + + )} ); } diff --git a/components/workout/WorkoutCard.tsx b/components/workout/WorkoutCard.tsx index 0c8902f..b0f864d 100644 --- a/components/workout/WorkoutCard.tsx +++ b/components/workout/WorkoutCard.tsx @@ -215,11 +215,16 @@ export const WorkoutCard: React.FC = ({ Exercise {/* In a real implementation, you would map through actual exercises */} {workout.exercises && workout.exercises.length > 0 ? ( - workout.exercises.slice(0, 3).map((exercise, idx) => ( - - {exercise.title} - - )) + workout.exercises.slice(0, 3).map((exercise, idx) => { + // Use the exercise title directly + const exerciseTitle = exercise.title || 'Exercise'; + + return ( + + {exerciseTitle} + + ); + }) ) : ( No exercises recorded )} diff --git a/components/workout/WorkoutCompletionFlow.tsx b/components/workout/WorkoutCompletionFlow.tsx index 01decff..9479c8b 100644 --- a/components/workout/WorkoutCompletionFlow.tsx +++ b/components/workout/WorkoutCompletionFlow.tsx @@ -1,6 +1,6 @@ // components/workout/WorkoutCompletionFlow.tsx import React, { useState, useEffect } from 'react'; -import { View, TouchableOpacity, ScrollView } from 'react-native'; +import { View, TouchableOpacity, ScrollView, ActivityIndicator } from 'react-native'; import { Text } from '@/components/ui/text'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; @@ -20,6 +20,7 @@ import { Cog } from 'lucide-react-native'; import { TemplateService } from '@/lib/db/services/TemplateService'; +import { NostrWorkoutService } from '@/lib/db/services/NostrWorkoutService'; import Confetti from '@/components/Confetti'; /** @@ -178,109 +179,41 @@ function StorageOptionsTab({ )} - {/* Template options section - only if needed */} - {hasTemplateChanges && ( - <> - - - Template Options - - Your workout includes modifications to the original template - - - {/* Keep original option */} - handleTemplateAction('keep_original')} - activeOpacity={0.7} - > - - - - Keep Original - - Don't update the template - - - - - - - {/* Update existing option */} - handleTemplateAction('update_existing')} - activeOpacity={0.7} - > - - - - Update Template - - Save these changes to the original template - - - - - - - {/* Save as new option */} - handleTemplateAction('save_as_new')} - activeOpacity={0.7} - > - - - - Save as New - - Create a new template from this workout - - - - - - - {/* Template name input if save as new is selected */} - {options.templateAction === 'save_as_new' && ( - - New template name: - setOptions({ - ...options, - newTemplateName: text - })} - /> - - )} - - )} + {/* Workout Description field and Next button */} + + Workout Notes + + Add context or details about your workout + + setOptions({ + ...options, + workoutDescription: text + })} + className="min-h-[100px] p-3 bg-background dark:bg-muted border-[0.5px] border-input dark:border-0" + style={{ marginBottom: 16 }} + /> + - {/* Next button */} + {/* Next button with direct styling */} + + {/* Template options section removed as it was causing bugs */} ); @@ -507,34 +440,28 @@ function CelebrationTab({ onComplete: (options: WorkoutCompletionOptions) => void }) { const { isAuthenticated } = useNDKCurrentUser(); - const { activeWorkout } = useWorkoutStore(); + const { activeWorkout, isPublishing, publishingStatus, publishError } = useWorkoutStore(); const [showConfetti, setShowConfetti] = useState(true); const [shareMessage, setShareMessage] = useState(''); // Purple color used throughout the app const purpleColor = 'hsl(261, 90%, 66%)'; - // Generate default share message + // Disable buttons during publishing + const isDisabled = isPublishing; + + // Generate default share message on load useEffect(() => { - // Create default message based on workout data - let message = "Just completed a workout! 💪"; - if (activeWorkout) { - const exerciseCount = activeWorkout.exercises.length; - const completedSets = activeWorkout.exercises.reduce( - (total, exercise) => total + exercise.sets.filter(set => set.isCompleted).length, 0 + // Use the enhanced formatter from NostrWorkoutService + const formattedMessage = NostrWorkoutService.createFormattedSocialMessage( + activeWorkout, + "Just completed a workout! 💪" ); - - // Add workout details - message = `Just completed a workout with ${exerciseCount} exercises and ${completedSets} sets! 💪`; - - // Add mock PR info - if (Math.random() > 0.5) { - message += " Hit some new PRs today! 🏆"; - } + setShareMessage(formattedMessage); + } else { + setShareMessage("Just completed a workout! 💪 #powr #nostr"); } - - setShareMessage(message); }, [activeWorkout]); const handleShare = () => { @@ -573,6 +500,25 @@ function CelebrationTab({ + {/* Show publishing status */} + {isPublishing && ( + + + {publishingStatus === 'saving' && 'Saving workout...'} + {publishingStatus === 'publishing-workout' && 'Publishing workout record...'} + {publishingStatus === 'publishing-social' && 'Sharing to Nostr...'} + + + + )} + + {/* Show error if any */} + {publishError && ( + + {publishError} + + )} + {/* Show sharing options for Nostr if appropriate */} {isAuthenticated && options.storageType !== 'local_only' && ( <> @@ -592,13 +538,18 @@ function CelebrationTab({ numberOfLines={4} value={shareMessage} onChangeText={setShareMessage} - className="min-h-[120px] p-3 mb-4" + className="min-h-[120px] p-3 mb-4 bg-background dark:bg-muted border-[0.5px] border-input dark:border-0" + editable={!isDisabled} />