// app/(tabs)/profile/overview.tsx import React, { useState, useCallback, useEffect } from 'react'; 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'; import EmptyFeed from '@/components/social/EmptyFeed'; import { useSocialFeed } from '@/lib/hooks/useSocialFeed'; import { useProfileStats } from '@/lib/hooks/useProfileStats'; import { AnyFeedEntry, WorkoutFeedEntry, ExerciseFeedEntry, TemplateFeedEntry, SocialFeedEntry, ArticleFeedEntry } from '@/types/feed'; import UserAvatar from '@/components/UserAvatar'; import { useRouter } from 'expo-router'; import { QrCode, Mail, Copy } from 'lucide-react-native'; import { useTheme } from '@react-navigation/native'; import type { CustomTheme } from '@/lib/theme'; import { Alert } from 'react-native'; import { nip19 } from 'nostr-tools'; // Define the conversion function for feed items function convertToLegacyFeedItem(entry: AnyFeedEntry) { return { id: entry.eventId, type: entry.type, originalEvent: entry.event!, parsedContent: entry.content!, createdAt: (entry.timestamp || Date.now()) / 1000 }; } export default function OverviewScreen() { const insets = useSafeAreaInsets(); const router = useRouter(); const theme = useTheme() as CustomTheme; const { currentUser, isAuthenticated } = useNDKCurrentUser(); // Initialize all state hooks at the top to maintain consistent ordering const [isLoginSheetOpen, setIsLoginSheetOpen] = useState(false); const [feedItems, setFeedItems] = useState([]); const [feedLoading, setFeedLoading] = useState(false); const [isOffline, setIsOffline] = useState(false); const [entries, setEntries] = useState([]); const [isRefreshing, setIsRefreshing] = useState(false); const [renderError, setRenderError] = useState(null); const [loadAttempts, setLoadAttempts] = useState(0); // 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: isAuthenticated && currentUser?.pubkey ? [currentUser.pubkey] : [], limit: 30 }); // 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()); // Safety timeout for Android - force refresh the view if stuck loading for too long useEffect(() => { let timeoutId: NodeJS.Timeout | null = null; if (Platform.OS === 'android' && isAuthenticated && loading && loadAttempts < 3) { // Set a safety timeout - if loading takes more than 8 seconds, force a refresh timeoutId = setTimeout(() => { console.log('[Android] Profile view safety timeout triggered, forcing refresh'); setLoadAttempts(prev => prev + 1); setFeedLoading(false); if (refresh) { try { refresh(); } catch (e) { console.error('[Android] Force refresh error:', e); } } }, 8000); } return () => { if (timeoutId) clearTimeout(timeoutId); }; }, [isAuthenticated, loading, refresh, loadAttempts]); // Update feedItems when socialFeed.feedItems changes useEffect(() => { if (isAuthenticated && socialFeed) { setFeedItems(socialFeed.feedItems); setIsOffline(socialFeed.isOffline); } else { // Clear feed items when logged out setFeedItems([]); } }, [isAuthenticated, socialFeed?.feedItems, socialFeed?.isOffline]); // Process feedItems into entries when feedItems changes // This needs to be a separate effect to avoid breaking hook order during logout useEffect(() => { if (!feedItems || !Array.isArray(feedItems)) { setEntries([]); return; } // Map items and filter out any nulls const mappedItems = feedItems.map(item => { if (!item) return null; // Create a properly typed AnyFeedEntry based on the item type // with null safety for all item properties const baseEntry = { id: item.id || `temp-${Date.now()}-${Math.random()}`, eventId: item.id || `temp-${Date.now()}-${Math.random()}`, event: item.originalEvent || {}, timestamp: ((item.createdAt || Math.floor(Date.now() / 1000)) * 1000), }; // Add type-specific properties switch (item.type) { case 'workout': return { ...baseEntry, type: 'workout', content: item.parsedContent || {} } as WorkoutFeedEntry; case 'exercise': return { ...baseEntry, type: 'exercise', content: item.parsedContent || {} } as ExerciseFeedEntry; case 'template': return { ...baseEntry, type: 'template', content: item.parsedContent || {} } as TemplateFeedEntry; case 'social': return { ...baseEntry, type: 'social', content: item.parsedContent || {} } as SocialFeedEntry; case 'article': return { ...baseEntry, type: 'article', content: item.parsedContent || {} } as ArticleFeedEntry; default: // Fallback to social type if unknown return { ...baseEntry, type: 'social', content: item.parsedContent || {} } as SocialFeedEntry; } }); // Filter out nulls to satisfy TypeScript const filteredEntries = mappedItems.filter((item): item is AnyFeedEntry => item !== null); setEntries(filteredEntries); }, [feedItems]); const resetFeed = refresh; const hasContent = entries.length > 0; // Profile data const profileImageUrl = currentUser?.profile?.image || currentUser?.profile?.picture || (currentUser?.profile as any)?.avatar; // 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') : 'Guest User'; const username = isAuthenticated ? (currentUser?.profile?.nip05 || '@user') : '@guest'; const aboutText = currentUser?.profile?.about || (currentUser?.profile as any)?.description; const pubkey = currentUser?.pubkey; // 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, refresh: refreshStats, lastRefreshed: statsLastRefreshed } = useProfileStats({ pubkey: pubkey || '', 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 // 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 ( {followingDisplay} following {isManuallyRefreshing && ( )} {followersDisplay} followers {isManuallyRefreshing && ( )} ); }; // Generate npub format for display const npubFormat = React.useMemo(() => { if (!pubkey) return ''; try { const npub = nip19.npubEncode(pubkey); return npub; } catch (error) { console.error('Error encoding npub:', error); return ''; } }, [pubkey]); // Get shortened npub display version const shortenedNpub = React.useMemo(() => { if (!npubFormat) return ''; return `${npubFormat.substring(0, 8)}...${npubFormat.substring(npubFormat.length - 5)}`; }, [npubFormat]); // Handle refresh - now also refreshes banner image and forces state updates const handleRefresh = useCallback(async () => { setIsRefreshing(true); try { 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(`[${Platform.OS}] Error during profile refresh:`, error); } finally { setIsRefreshing(false); } }, [resetFeed, refreshStats, refetchBannerImage, followersCount, followingCount, setLastStatsFetch]); // Handle post selection const handlePostPress = useCallback((entry: AnyFeedEntry) => { // Just log the entry info for now console.log(`Selected ${entry.type}:`, entry); }, []); // Copy npub to clipboard const copyPubkey = useCallback(() => { if (pubkey) { try { const npub = nip19.npubEncode(pubkey); Clipboard.setString(npub); Alert.alert('Copied', 'Public key copied to clipboard in npub format'); console.log('npub copied to clipboard:', npub); } catch (error) { console.error('Error copying npub:', error); Alert.alert('Error', 'Failed to copy public key'); } } }, [pubkey]); // Show QR code alert const showQRCode = useCallback(() => { Alert.alert('QR Code', 'QR Code functionality will be implemented soon', [ { text: 'OK' } ]); }, []); // Memoize render item function const renderItem = useCallback(({ item }: { item: AnyFeedEntry }) => ( handlePostPress(item)} /> ), [handlePostPress]); // IMPORTANT: All callback hooks must be defined before any conditional returns // to ensure consistent hook ordering across renders // Define all the callbacks at the same level, regardless of authentication state const handleEditProfilePress = useCallback(() => { if (router && isAuthenticated) { router.push('/profile/settings'); } }, [router, isAuthenticated]); const handleCopyButtonPress = useCallback(() => { if (pubkey) { copyPubkey(); } }, [pubkey, copyPubkey]); const handleQrButtonPress = useCallback(() => { showQRCode(); }, [showQRCode]); // Profile header component - making sure we have the same hooks // regardless of authentication state to avoid hook ordering issues const ProfileHeader = useCallback(() => { // 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 */} {bannerImageUrl ? ( { 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'} )} {/* Left side - Avatar */} {/* Edit Profile button - positioned to the right */} Edit Profile {/* Profile info */} {displayName} {username} {/* Display npub below username with sharing options */} {npubFormat && ( {shortenedNpub} )} {/* Follower stats - render the separated component to avoid hook ordering issues */} {/* About text */} {aboutText && ( {aboutText} )} {/* Divider */} ); }, [ 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(() => { return ( ); }, []); const renderLoadingScreen = useCallback(() => { return ( ); }, []); const renderMainContent = useCallback(() => { // Catch and recover from any rendering errors try { return ( item.id} renderItem={renderItem} refreshControl={ } ListHeaderComponent={} ListEmptyComponent={ } contentContainerStyle={{ paddingBottom: insets.bottom + 20, flexGrow: entries.length === 0 ? 1 : undefined }} /> ); } catch (error) { setRenderError(error instanceof Error ? error : new Error(String(error))); // Fallback UI when rendering fails return ( Something went wrong We had trouble loading your profile. Please try again. ); } }, [entries, renderItem, isRefreshing, handleRefresh, ProfileHeader, insets.bottom]); // Final conditional return after all hooks have been called // This ensures consistent hook ordering across renders if (!isAuthenticated) { return renderLoginScreen(); } if (renderError) { // Render error recovery UI return ( Something went wrong {renderError.message || "We had trouble loading your profile. Please try again."} ); } // Show loading screen, but with a maximum timeout on Android if (loading && entries.length === 0) { if (Platform.OS === 'android' && loadAttempts >= 3) { // After 3 attempts, show content anyway with a refresh button return ( Some content may still be loading. ); } return renderLoadingScreen(); } return renderMainContent(); }