diff --git a/CHANGELOG.md b/CHANGELOG.md index a914b11..88ec1d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,115 @@ 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] +### Improved +- Console logging system + - Implemented configurable module-level logging controls + - Added quiet mode toggle for easier troubleshooting + - Enhanced logger utility with better filtering capabilities + - Disabled verbose feed cache and social feed logs + - Reduced SQL query logging for better console readability + - Improved NDK and database-related log filtering + - Added selective module enabling/disabling functionality + - Created comprehensive logging documentation + +### Fixed +- Android profile component loading issues + - Fixed banner image not showing up in Android profile screen + - Enhanced useBannerImage hook with improved React Query configuration + - Reduced staleTime to zero on both Android and iOS for immediate refresh + - Added platform-specific optimizations for Android image loading + - Fixed banner URI handling with proper file:// prefix management + - Added cache busting parameter to force Android image refresh + - Enhanced error logging with more verbose platform-specific messages + - Improved error recovery with automatic refetch on load failures + - Enhanced debugging logging throughout profile hooks + - Implemented more frequent auto-refresh on Android vs iOS (20s vs 30s) + - Added fallback messaging when banner is loading or missing + +- Android and iOS profile loading issues + - Enhanced useBannerImage hook with improved React Query configuration + - Reduced banner image staleTime from 1 hour to 30 seconds + - Added refetchOnMount: 'always' to ensure banner image loads on initial render + - Completely rewrote useProfileStats hook to use React Query + - Fixed profile follower/following counts showing stale data in Android + - Enhanced both hooks with standardized queryKeys for better cache management + - Improved error handling in both profile data hooks + - Added better cache invalidation strategies for profile data + +- iOS banner image loading issues + - Added platform-specific debugging in banner image cache service + - Enhanced BannerImageCache with detailed logging and error tracking + - Fixed iOS path handling to ensure file:// prefix for local URIs + - Added validation and error handling for image loading failures + - Enhanced profile UI to track image loading errors + - Added proper file path normalization for iOS compatibility + - Improved React Query caching with better cache handling + +### Added +- React Query Integration (Phase 1) + - Implemented useAuthQuery hook for React Query-based authentication + - Created useProfileWithQuery hook for profile data with React Query + - Implemented useConnectivityWithQuery hook for network status management + - Built ReactQueryAuthProvider for centralized auth integration + - Added proper query invalidation strategies + - Created standardized query key structure + - Implemented optimized query client configuration + - Built test components for React Query demonstration + - Added type safety across all query-related functionality + - Created proper loading and error state handling + - Fixed hook ordering issues with conditional hook calls + - Improved NDK initialization with more robust error handling + - Enhanced placeholder service pattern for hooks during initialization + - Implemented consistent hook order pattern to prevent React errors + +### Fixed +- React hooks ordering in Android + - Fixed "Warning: React has detected a change in the order of Hooks" error in OverviewScreen + - Implemented consistent hook calling pattern regardless of authentication state + - Enhanced useSocialFeed hook to use consistent parameters with conditional data + - Added comprehensive documentation on the React hooks ordering pattern used + - Ensured all components follow the same pattern for authentication-dependent hooks +- React Query data undefined errors + - Fixed "Query data cannot be undefined" error in profile image hooks + - Enhanced useProfileImage and useBannerImage hooks to always return non-undefined values + - Updated components to handle null vs undefined values properly + - Added proper type safety for image URI handling + +- Enhanced image caching for profile UI + - Implemented ProfileImageCache service with LRU-based eviction + - Added BannerImageCache service for profile banners with size limits + - Created useProfileImage and useBannerImage hooks with React Query + - Updated UserAvatar component to use React Query-based hooks + - Enhanced Profile screen with optimized image loading + - Updated RelayInitializer to properly initialize all image caches + - Added automatic cache cleanup for old/unused images + - Implemented prioritized cache eviction based on access patterns + - Added disk space management with maximum cache size limits + - Improved error handling in image loading/caching process + +### Verified +- React Query Integration (Phase 1) has been successfully implemented and is working in production + - Confirmed proper NDK initialization through React Query + - Verified authentication state management with React Query hooks + - Confirmed successful relay connections and management + - Validated proper hook ordering in main app components + - Verified optimal caching behavior with appropriate stale times + - Confirmed proper profile and connectivity handling + +### Fixed +- React Query Integration Testing Issues + - Fixed critical provider duplication by properly integrating ReactQueryAuthProvider at the root level + - Corrected query key definition to match the actual keys used by hooks (auth.current) + - Removed multiple instances of ReactQueryAuthProvider that were causing hook ordering conflicts + - Fixed "Rendered more hooks than during the previous render" error in test components + - Updated test component to use the app-wide ReactQueryAuthProvider + - Enhanced testing tool with proper isolation of concerns + - Fixed test routes to use dedicated providers to prevent interference with global state + - Improved auth-test component with proper nested structure for AuthProvider + - Fixed hook ordering issues with consistent hook patterns in components + - Added self-contained testing approach with local query client instances + - Enhanced test layout to manage provider conflicts between different auth implementations + ### Documentation - Added comprehensive React Query integration plan to address authentication state transitions and hook ordering issues - Created detailed technical documentation for integrating React Query with SQLite, NDK, and Amber signer @@ -657,189 +766,4 @@ g - Added cache_metadata table for performance optimization - Added exercise_media table for future media support - Alphabetical quick scroll in exercise library - - Dynamic letter highlighting for available sections - - Smooth scrolling to selected sections - - Sticky section headers for better navigation -- Basic exercise template creation functionality - - Input validation for required fields - - Schema-compliant field constraints - - Native picker components for standardized inputs -- Enhanced error handling in database operations - - Detailed SQLite error logging - - Improved transaction management - - Added proper error types and propagation -- Template management features - - Basic template creation interface - - Favorite template functionality - - Template categories and filtering - - Quick-start template actions -- Full-screen template details with tab navigation - - Replaced bottom sheet with dedicated full-screen layout - - Implemented material top tabs for content organization - - Added Overview, History, and Social tabs - - Improved template information hierarchy - - Added contextual action buttons based on template source - - Enhanced social sharing capabilities - - Improved workout history visualization - -### Changed -- Improved workout screen navigation consistency - - Standardized screen transitions and gestures - - Added back buttons for clearer navigation - - Implemented proper workout state persistence -- Enhanced exercise selection interface - - Updated add-exercises screen with cleaner UI - - Added multi-select functionality for bulk exercise addition - - Implemented exercise search and filtering -- Improved exercise library interface - - Removed "Recent Exercises" section for cleaner UI - - Added alphabetical section organization - - Enhanced keyboard handling for input fields - - Increased description text area size -- Updated NewExerciseScreen with constrained inputs - - Added dropdowns for equipment selection - - Added movement pattern selection - - Added difficulty selection - - Added exercise type selection -- Improved DbService with better error handling - - Added proper SQLite error types - - Enhanced transaction rollback handling - - Added detailed debug logging -- Updated type system for better data handling - - Consolidated exercise and template types - - Added proper type guards - - Improved type safety in components -- Enhanced template display UI - - Added category pills for filtering - - Improved spacing and layout - - Better visual hierarchy for favorites -- Migrated from React Context to Zustand for state management - - Improved performance with fine-grained rendering - - Enhanced developer experience with simpler API - - Better type safety with TypeScript integration - - Added persistent workout state for recovery -- Redesigned template details experience - - Migrated from bottom sheet to full-screen layout - - Restructured content with tab-based navigation - - Added dedicated header with prominent action buttons - - Improved attribution and source indication - - Enhanced visual separation between template metadata and content - -### Fixed -- Workout navigation gesture handling issues -- Workout timer inconsistency during app background state -- Exercise deletion functionality -- Keyboard overlap issues in exercise creation form -- SQLite transaction nesting issues -- TypeScript parameter typing in database services -- Null value handling in database operations -- Development seeding duplicate prevention -- Template category sunctionality -- Keyboard overlap isspes in exercise creation form -- SQLite traasacingn nesting issues -- TypeScript parameter typing i database services -- Null visue handlsng in dauabase operations -- Development seeding duplicate prevention -- Template categore spacing issuess -- Exercise list rendering on iOS -- Database reset and reseeding behavior -- Template details UI overflow issues -- Navigation inconsistencies between template screens -- Content rendering issues in bottom sheet components - -### Technical Details -1. Nostr Integration: - - Implemented @nostr-dev-kit/ndk-mobile package for React Native compatibility - - Created dedicated NDK store using Zustand for state management - - Built secure key storage and retrieval using Expo SecureStore - - Implemented event creation, signing, and publishing workflow - - Added relay connection management with status tracking - - Developed proper error handling for network operations - -2. Cryptographic Implementation: - - Integrated react-native-get-random-values for crypto API polyfill - - Implemented NDKMobilePrivateKeySigner for key operations - - Added proper key format handling (hex, nsec) - - Created secure key generation functionality - - Built robust error handling for cryptographic operations - -3. Programs Testing Component: - - Developed dual-purpose interface for Database and Nostr testing - - Implemented login system with key generation and secure storage - - Built event creation interface with multiple event kinds - - Added event querying and display functionality - - Created detailed event inspection with tag visualization - - Added relay status monitoring -4. Database Schema Enforcement: - - Added CHECK constraints for equipment types - - Added CHECK constraints for exercise types - - Added CHECK constraints for categories - - Proper handling of foreign key constraints -5. Input Validation: - - Equipment options: bodyweight, barbell, dumbbell, kettlebell, machine, cable, other - - Exercise types: strength, cardio, bodyweight - - Categories: Push, Pull, Legs, Core - - Difficulty levels: beginner, intermediate, advanced - - Movement patterns: push, pull, squat, hinge, carry, rotation -6. Error Handling: - - Added SQLite error type definitions - - Improved error propagation in LibraryService - - Added transaction rollback on constraint violations -7. Database Services: - - Added EventCache service for Nostr events - - Improved ExerciseService with transaction awareness - - Added DevSeederService for development data - - Enhanced error handling and logging -8. Workout State Management with Zustand: - - Implemented selector pattern for performance optimization - - Added module-level timer references for background operation - - Created workout persistence with auto-save functionality - - Developed state recovery for crash protection - - Added support for future Nostr integration - - Implemented workout minimization for multi-tasking -9. Template Details UI Architecture: - - Implemented MaterialTopTabNavigator for content organization - - Created screen-specific components for each tab - - Developed conditional rendering based on template source - - Implemented context-aware action buttons - - Added proper navigation state handling - -### Migration Notes -- Exercise creation now enforces schema constraints -- Input validation prevents invalid data entry -- Enhanced error messages provide better debugging information -- Template management requires updated type definitions -- Workout state now persists across app restarts -- Component access to workout state requires new selector pattern -- Template details navigation has changed from modal to screen-based approach - -## [0.1.0] - 2024-02-09 - -### Added -- Initial project setup with Expo and React Native -- Basic tab navigation structure -- Theme support (light/dark mode) -- SQLite database integration -- Basic exercise library interface - -### Changed -- Migrated to TypeScript -- Updated to latest Expo SDK -- Implemented NativeWind for styling - -### Fixed -- iOS status bar appearance -- Android back button handling -- SQLite transaction management - -### Security -- Added basic input validation -- Implemented secure storage for sensitive data - -## [0.0.1] - 2024-02-01 - -### Added -- Initial repository setup -- Basic project structure -- Development environment configuration -- Documentation templates + - Dynamic letter highlighting diff --git a/app/(tabs)/profile/activity.tsx b/app/(tabs)/profile/activity.tsx index d44fd4c..b32d923 100644 --- a/app/(tabs)/profile/activity.tsx +++ b/app/(tabs)/profile/activity.tsx @@ -35,14 +35,21 @@ export default function ActivityScreen() { const totalPrograms = 0; // Placeholder for programs count // Load personal records + // IMPORTANT: Always call all hooks in the same order, even when not authenticated + // This ensures consistent hook ordering across renders to comply with React's Rules of Hooks useEffect(() => { async function loadRecords() { - if (!isAuthenticated) return; - try { setLoading(true); - const personalRecords = await analytics.getPersonalRecords(undefined, 3); - setRecords(personalRecords); + + // Only fetch records if authenticated, but always run the effect + if (isAuthenticated) { + const personalRecords = await analytics.getPersonalRecords(undefined, 3); + setRecords(personalRecords); + } else { + // Reset records when not authenticated + setRecords([]); + } } catch (error) { console.error('Error loading personal records:', error); } finally { diff --git a/app/(tabs)/profile/overview.tsx b/app/(tabs)/profile/overview.tsx index da4b588..4e112ec 100644 --- a/app/(tabs)/profile/overview.tsx +++ b/app/(tabs)/profile/overview.tsx @@ -1,11 +1,12 @@ // app/(tabs)/profile/overview.tsx import React, { useState, useCallback, useEffect } from 'react'; -import { View, FlatList, RefreshControl, Pressable, TouchableOpacity, ImageBackground, Clipboard } from 'react-native'; +import { View, FlatList, RefreshControl, Pressable, TouchableOpacity, ImageBackground, Clipboard, Platform } from 'react-native'; import { Text } from '@/components/ui/text'; import { Button } from '@/components/ui/button'; import { useNDKCurrentUser } from '@/lib/hooks/useNDK'; import { ActivityIndicator } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useBannerImage } from '@/lib/hooks/useBannerImage'; import NostrLoginSheet from '@/components/sheets/NostrLoginSheet'; import NostrProfileLogin from '@/components/social/NostrProfileLogin'; import EnhancedSocialPost from '@/components/social/EnhancedSocialPost'; @@ -53,18 +54,20 @@ export default function OverviewScreen() { const [entries, setEntries] = useState([]); const [isRefreshing, setIsRefreshing] = useState(false); - // Only call useSocialFeed when authenticated to prevent the error - const socialFeed = isAuthenticated ? useSocialFeed({ + // IMPORTANT: Always call hooks in the same order on every render to comply with React's Rules of Hooks + // Instead of conditionally calling the hook based on authentication state, + // we always call it but pass empty/default parameters when not authenticated + // This ensures consistent hook ordering between authenticated and non-authenticated states + const socialFeed = useSocialFeed({ feedType: 'profile', - authors: currentUser?.pubkey ? [currentUser.pubkey] : [], + authors: isAuthenticated && currentUser?.pubkey ? [currentUser.pubkey] : [], limit: 30 - }) : null; + }); - // Extract values from socialFeed when authenticated - const loading = isAuthenticated ? socialFeed?.loading || false : feedLoading; - const refresh = isAuthenticated - ? (socialFeed?.refresh ? socialFeed.refresh : () => Promise.resolve()) - : () => Promise.resolve(); + // Extract values from socialFeed - available regardless of auth state + // Use nullish coalescing to safely access values from the hook + const loading = socialFeed?.loading || feedLoading; + const refresh = socialFeed?.refresh || (() => Promise.resolve()); // Update feedItems when socialFeed.feedItems changes useEffect(() => { @@ -157,9 +160,15 @@ export default function OverviewScreen() { const profileImageUrl = currentUser?.profile?.image || currentUser?.profile?.picture || (currentUser?.profile as any)?.avatar; - - const bannerImageUrl = currentUser?.profile?.banner || - (currentUser?.profile as any)?.background; + + // Use our React Query hook for banner images + const defaultBannerUrl = currentUser?.profile?.banner || + (currentUser?.profile as any)?.background; + + const { data: bannerImageUrl, refetch: refetchBannerImage } = useBannerImage( + currentUser?.pubkey, + defaultBannerUrl + ); const displayName = isAuthenticated ? (currentUser?.profile?.displayName || currentUser?.profile?.name || 'Nostr User') @@ -177,31 +186,120 @@ export default function OverviewScreen() { // Profile follower stats component - always call useProfileStats hook // even if isAuthenticated is false (passing empty pubkey) // This ensures consistent hook ordering regardless of authentication state - const { followersCount, followingCount, isLoading: statsLoading } = useProfileStats({ + const { + followersCount, + followingCount, + isLoading: statsLoading, + refresh: refreshStats, + lastRefreshed: statsLastRefreshed + } = useProfileStats({ pubkey: pubkey || '', - refreshInterval: 60000 * 15 // refresh every 15 minutes + refreshInterval: 10000 // 10 second refresh interval for real-time updates }); + // Track last fetch time to force component updates + const [lastStatsFetch, setLastStatsFetch] = useState(Date.now()); + + // Update the lastStatsFetch whenever stats are refreshed + useEffect(() => { + if (statsLastRefreshed) { + setLastStatsFetch(statsLastRefreshed); + } + }, [statsLastRefreshed]); + + // Manual refresh function with visual feedback + const manualRefreshStats = useCallback(async () => { + console.log(`[${Platform.OS}] Manually refreshing follower stats...`); + if (refreshStats) { + try { + await refreshStats(); + console.log(`[${Platform.OS}] Follower stats refreshed successfully`); + // Force update even if the values didn't change + setLastStatsFetch(Date.now()); + } catch (error) { + console.error(`[${Platform.OS}] Error refreshing follower stats:`, error); + } + } + }, [refreshStats]); + // Use a separate component to avoid conditionally rendered hooks - const ProfileFollowerStats = React.memo(() => { + // Do NOT use React.memo here - we need to re-render this even when props don't change + const ProfileFollowerStats = () => { + // Add local state to track if a manual refresh is happening + const [isManuallyRefreshing, setIsManuallyRefreshing] = useState(false); + + // This will run on every render to ensure we're showing fresh data + useEffect(() => { + console.log(`[${Platform.OS}] Rendering ProfileFollowerStats with:`, { + followersCount, + followingCount, + statsLoading, + isManuallyRefreshing, + lastRefreshed: new Date(lastStatsFetch).toISOString() + }); + }, [followersCount, followingCount, statsLoading, lastStatsFetch, isManuallyRefreshing]); + + // Enhanced manual refresh function with visual feedback + const triggerManualRefresh = useCallback(async () => { + if (isManuallyRefreshing) return; // Prevent multiple simultaneous refreshes + + try { + setIsManuallyRefreshing(true); + console.log(`[${Platform.OS}] Manual refresh triggered by user tap`); + await manualRefreshStats(); + } catch (error) { + console.error(`[${Platform.OS}] Error during manual refresh:`, error); + } finally { + // Short delay before removing loading indicator for better UX + setTimeout(() => setIsManuallyRefreshing(false), 500); + } + }, [isManuallyRefreshing, manualRefreshStats]); + + // Always show actual values when available, regardless of loading state + // Only show dots when we have no values at all + // This ensures Android doesn't get stuck showing loading indicators + const followingDisplay = followingCount > 0 ? + followingCount.toLocaleString() : + (statsLoading || isManuallyRefreshing ? '...' : '0'); + + const followersDisplay = followersCount > 0 ? + followersCount.toLocaleString() : + (statsLoading || isManuallyRefreshing ? '...' : '0'); + return ( - - + + - {statsLoading ? '...' : followingCount.toLocaleString()} + {followingDisplay} following + {isManuallyRefreshing && ( + + )} - + - {statsLoading ? '...' : followersCount.toLocaleString()} + {followersDisplay} followers + {isManuallyRefreshing && ( + + )} ); - }); + }; // Generate npub format for display const npubFormat = React.useMemo(() => { @@ -221,19 +319,54 @@ export default function OverviewScreen() { return `${npubFormat.substring(0, 8)}...${npubFormat.substring(npubFormat.length - 5)}`; }, [npubFormat]); - // Handle refresh + // Handle refresh - now also refreshes banner image and forces state updates const handleRefresh = useCallback(async () => { setIsRefreshing(true); try { - await resetFeed(); - // Add a slight delay to ensure the UI updates - await new Promise(resolve => setTimeout(resolve, 300)); + console.log(`[${Platform.OS}] Starting full profile refresh...`); + + // Create an array of refresh promises to run in parallel + const refreshPromises = []; + + // Refresh feed content + refreshPromises.push(resetFeed()); + + // Refresh profile stats from nostr.band + if (refreshStats) { + refreshPromises.push( + refreshStats() + .then(() => { + console.log(`[${Platform.OS}] Profile stats refreshed successfully:`); + // Force component update even if values didn't change + setLastStatsFetch(Date.now()); + }) + .catch(error => console.error(`[${Platform.OS}] Error refreshing profile stats:`, error)) + ); + } + + // Refresh banner image + if (refetchBannerImage) { + refreshPromises.push( + refetchBannerImage() + .then(() => console.log(`[${Platform.OS}] Banner image refreshed successfully`)) + .catch(error => console.error(`[${Platform.OS}] Error refreshing banner image:`, error)) + ); + } + + // Wait for all refresh operations to complete + await Promise.all(refreshPromises); + + // Log the current values after refresh + console.log(`[${Platform.OS}] Profile refresh completed successfully. Current stats:`, { + followersCount, + followingCount + }); } catch (error) { - console.error('Error refreshing feed:', error); + console.error(`[${Platform.OS}] Error during profile refresh:`, error); } finally { setIsRefreshing(false); } - }, [resetFeed]); + }, [resetFeed, refreshStats, refetchBannerImage, followersCount, followingCount, setLastStatsFetch]); // Handle post selection const handlePostPress = useCallback((entry: AnyFeedEntry) => { @@ -297,6 +430,15 @@ export default function OverviewScreen() { // Using callbacks defined at the parent level // This prevents inconsistent hook counts during render + // Debugging banner image loading + useEffect(() => { + console.log('Banner image state in ProfileHeader:', { + bannerImageUrl, + defaultBannerUrl, + pubkey: pubkey?.substring(0, 8) + }); + }, [bannerImageUrl, defaultBannerUrl]); + return ( {/* Banner Image */} @@ -306,11 +448,30 @@ export default function OverviewScreen() { source={{ uri: bannerImageUrl }} className="w-full h-full" resizeMode="cover" + onError={(e) => { + console.error(`Banner image loading error: ${JSON.stringify(e.nativeEvent)}`); + console.error(`Failed URL: ${bannerImageUrl}`); + + // Force a re-render of the gradient fallback on error + if (refetchBannerImage) { + console.log('Attempting to refetch banner image after error...'); + refetchBannerImage().catch(err => + console.error('Failed to refetch banner image:', err) + ); + } + }} + onLoad={() => { + console.log(`Banner image loaded successfully: ${bannerImageUrl}`); + }} > ) : ( - + + + {defaultBannerUrl ? 'Loading banner...' : 'No banner image'} + + )} @@ -322,8 +483,16 @@ export default function OverviewScreen() { uri={profileImageUrl} pubkey={pubkey} name={displayName} - className="mr-4 border-4 border-background" - style={{ width: 90, height: 90 }} + className="mr-4" + style={{ + width: 90, + height: 90, + backgroundColor: 'transparent', + overflow: 'hidden', + borderWidth: 0, + shadowOpacity: 0, + elevation: 0 + }} /> {/* Edit Profile button - positioned to the right */} @@ -367,7 +536,7 @@ export default function OverviewScreen() { )} - {/* Follower stats - no longer passing pubkey as prop since we're calling useProfileStats in parent */} + {/* Follower stats - render the separated component to avoid hook ordering issues */} {/* About text */} @@ -381,10 +550,23 @@ export default function OverviewScreen() { ); - }, [displayName, username, profileImageUrl, aboutText, pubkey, npubFormat, shortenedNpub, theme.colors.text, router, showQRCode, copyPubkey, isAuthenticated]); - - // Profile components must be defined before conditional returns - // to ensure that React hook ordering remains consistent + }, [ + displayName, + username, + profileImageUrl, + aboutText, + pubkey, + npubFormat, + shortenedNpub, + theme.colors.text, + router, + showQRCode, + copyPubkey, + isAuthenticated, + bannerImageUrl, + defaultBannerUrl, + refetchBannerImage + ]); // Render functions for different app states const renderLoginScreen = useCallback(() => { diff --git a/app/(tabs)/profile/progress.tsx b/app/(tabs)/profile/progress.tsx index ec36c42..7463903 100644 --- a/app/(tabs)/profile/progress.tsx +++ b/app/(tabs)/profile/progress.tsx @@ -71,22 +71,29 @@ export default function ProgressScreen() { const [includeNostr, setIncludeNostr] = useState(true); // Load workout statistics when period or includeNostr changes + // IMPORTANT: Always call all hooks in the same order, regardless of authentication state + // This ensures consistent hook ordering across renders to comply with React's Rules of Hooks useEffect(() => { async function loadStats() { - if (!isAuthenticated) return; - try { setLoading(true); - // Pass includeNostr flag to analytics service - analyticsService.setIncludeNostr(includeNostr); - - const workoutStats = await analytics.getWorkoutStats(period); - setStats(workoutStats); - - // Load personal records - const personalRecords = await analytics.getPersonalRecords(undefined, 5); - setRecords(personalRecords); + // Only fetch stats if authenticated, but always run the effect + if (isAuthenticated) { + // Pass includeNostr flag to analytics service + analyticsService.setIncludeNostr(includeNostr); + + const workoutStats = await analytics.getWorkoutStats(period); + setStats(workoutStats); + + // Load personal records + const personalRecords = await analytics.getPersonalRecords(undefined, 5); + setRecords(personalRecords); + } else { + // Reset stats and records when not authenticated + setStats(null); + setRecords([]); + } } catch (error) { console.error('Error loading analytics data:', error); } finally { diff --git a/app/_layout.tsx b/app/_layout.tsx index d808bba..5c402a1 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -21,6 +21,8 @@ import { useNDKStore, FLAGS } from '@/lib/stores/ndk'; import { useWorkoutStore } from '@/stores/workoutStore'; import { ConnectivityService } from '@/lib/db/services/ConnectivityService'; import { AuthProvider } from '@/lib/auth/AuthProvider'; +import { ReactQueryAuthProvider } from '@/lib/auth/ReactQueryAuthProvider'; + // Import splash screens with improved fallback mechanism let SplashComponent: React.ComponentType<{onFinish: () => void}>; let useVideoSplash = false; @@ -225,47 +227,50 @@ export default function RootLayout() { - {/* Ensure SettingsDrawerProvider wraps everything */} - - {/* Add AuthProvider when using new auth system */} - {(() => { - const ndk = useNDKStore.getState().ndk; - if (ndk && FLAGS.useNewAuthSystem) { - return ( - - {/* Add RelayInitializer here - it loads relay data once NDK is available */} - - - {/* Add OfflineIndicator to show network status */} - - - ); - } else { - return ( - <> - {/* Legacy approach without AuthProvider */} - - - - ); - } - })()} - - - - - - - {/* Settings drawer needs to be outside the navigation stack */} - - - - + {/* Wrap everything in ReactQueryAuthProvider to enable React Query functionality app-wide */} + + {/* Ensure SettingsDrawerProvider wraps everything */} + + {/* Add AuthProvider when using new auth system */} + {(() => { + const ndk = useNDKStore.getState().ndk; + if (ndk && FLAGS.useNewAuthSystem) { + return ( + + {/* Add RelayInitializer here - it loads relay data once NDK is available */} + + + {/* Add OfflineIndicator to show network status */} + + + ); + } else { + return ( + <> + {/* Legacy approach without AuthProvider */} + + + + ); + } + })()} + + + + + + + {/* Settings drawer needs to be outside the navigation stack */} + + + + + diff --git a/app/test/_layout.tsx b/app/test/_layout.tsx new file mode 100644 index 0000000..e30367a --- /dev/null +++ b/app/test/_layout.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { Stack } from 'expo-router'; +import { StatusBar } from 'expo-status-bar'; + +/** + * Layout for test screens + * + * This allows us to have multiple test screens in the test directory + * for trying out different features in isolation. + * + * Each test page should include its own providers as needed, rather than + * relying on providers in this layout, to avoid conflicts with the app's + * global settings. + */ +export default function TestLayout() { + return ( + <> + + + + + + + + + + ); +} diff --git a/app/test/auth-test.tsx b/app/test/auth-test.tsx index 7344e9d..1e5788b 100644 --- a/app/test/auth-test.tsx +++ b/app/test/auth-test.tsx @@ -1,15 +1,17 @@ -import React from 'react'; -import { View, StyleSheet, ScrollView, Button, Text, Platform } from 'react-native'; +import React, { useState, useEffect } from 'react'; +import { View, StyleSheet, ScrollView, Button, Text, Platform, ActivityIndicator } from 'react-native'; import { useAuthState, useAuth } from '@/lib/auth/AuthProvider'; +import { AuthProvider } from '@/lib/auth/AuthProvider'; +import { useNDKStore } from '@/lib/stores/ndk'; import AuthStatus from '@/components/auth/AuthStatus'; import { StatusBar } from 'expo-status-bar'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Stack } from 'expo-router'; /** - * Test page for the new authentication system + * Internal component that uses auth hooks - must be inside AuthProvider */ -export default function AuthTestPage() { +function AuthTestContent() { const { top, bottom } = useSafeAreaInsets(); const authState = useAuthState(); const { authService } = useAuth(); @@ -178,6 +180,38 @@ export default function AuthTestPage() { ); } +/** + * Wrapper component that provides the AuthProvider + */ +export default function AuthTestPage() { + // Get NDK instance from the store (already initialized in app root) + const ndk = useNDKStore(state => state.ndk); + const [isInitialized, setIsInitialized] = useState(false); + + // Wait for NDK to be available + useEffect(() => { + if (ndk) { + setIsInitialized(true); + } + }, [ndk]); + + // Loading state while NDK initializes + if (!isInitialized || !ndk) { + return ( + + + Loading NDK for auth test... + + ); + } + + return ( + + + + ); +} + const styles = StyleSheet.create({ container: { flex: 1, diff --git a/app/test/cache-test.tsx b/app/test/cache-test.tsx new file mode 100644 index 0000000..b5b7086 --- /dev/null +++ b/app/test/cache-test.tsx @@ -0,0 +1,180 @@ +import React, { useState } from 'react'; +import { View, StyleSheet, Button, Text, ScrollView } from 'react-native'; +import { useProfileImage } from '@/lib/hooks/useProfileImage'; +import { useBannerImage } from '@/lib/hooks/useBannerImage'; +import UserAvatar from '@/components/UserAvatar'; +import { Image } from 'react-native'; +import { profileImageCache } from '@/lib/db/services/ProfileImageCache'; +import { bannerImageCache } from '@/lib/db/services/BannerImageCache'; + +export default function CacheTestScreen() { + const [pubkey, setPubkey] = useState('fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52'); + const [stats, setStats] = useState(null); + + // Using our React Query hooks + const { data: profileImageData, isLoading: profileLoading } = useProfileImage(pubkey); + const { data: bannerImageData, isLoading: bannerLoading } = useBannerImage(pubkey); + + // Convert null to undefined to maintain compatibility with components expecting string | undefined + const profileImage = profileImageData === null ? undefined : profileImageData; + const bannerImage = bannerImageData === null ? undefined : bannerImageData; + + const getStats = async () => { + const profileStats = await profileImageCache.getCacheStats(); + const bannerStats = await bannerImageCache.getCacheStats(); + + setStats({ + profile: profileStats, + banner: bannerStats + }); + }; + + return ( + + Image Cache Test + + + Profile Image + {profileLoading ? ( + Loading profile image... + ) : ( + + + Source: {profileImage || 'Using fallback'} + + )} + + + + Banner Image + {bannerLoading ? ( + Loading banner image... + ) : ( + + {bannerImage ? ( + + ) : ( + + No banner image + + )} + Source: {bannerImage || 'Using fallback'} + + )} + + + +