// app/(tabs)/profile/overview.tsx import React, { useState, useCallback, useEffect } from 'react'; import { View, FlatList, RefreshControl, Pressable, TouchableOpacity, ImageBackground, Clipboard } 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 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(); const [isLoginSheetOpen, setIsLoginSheetOpen] = useState(false); // Initialize feed related state const [feedItems, setFeedItems] = useState([]); const [feedLoading, setFeedLoading] = useState(false); const [isOffline, setIsOffline] = useState(false); // Only call useSocialFeed when authenticated to prevent the error const socialFeed = isAuthenticated ? useSocialFeed({ feedType: 'profile', authors: 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(); // Update feedItems when socialFeed.feedItems changes useEffect(() => { if (isAuthenticated && socialFeed) { setFeedItems(socialFeed.feedItems); setIsOffline(socialFeed.isOffline); } }, [isAuthenticated, socialFeed?.feedItems, socialFeed?.isOffline]); // Convert to the format expected by the component const entries = React.useMemo(() => { return feedItems.map(item => { // Create a properly typed AnyFeedEntry based on the item type const baseEntry = { id: item.id, eventId: item.id, event: item.originalEvent, timestamp: item.createdAt * 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; } }); }, [feedItems]); const resetFeed = refresh; const hasContent = entries.length > 0; const [isRefreshing, setIsRefreshing] = useState(false); // Profile data const profileImageUrl = currentUser?.profile?.image || currentUser?.profile?.picture || (currentUser?.profile as any)?.avatar; const bannerImageUrl = currentUser?.profile?.banner || (currentUser?.profile as any)?.background; 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 } = useProfileStats({ pubkey: pubkey || '', refreshInterval: 60000 * 15 // refresh every 15 minutes }); // Use a separate component to avoid conditionally rendered hooks const ProfileFollowerStats = React.memo(() => { return ( {statsLoading ? '...' : followingCount.toLocaleString()} following {statsLoading ? '...' : followersCount.toLocaleString()} followers ); }); // 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 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)); } catch (error) { console.error('Error refreshing feed:', error); } finally { setIsRefreshing(false); } }, [resetFeed]); // 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 return ( {/* Banner Image */} {bannerImageUrl ? ( ) : ( )} {/* 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 - no longer passing pubkey as prop since we're calling useProfileStats in parent */} {/* About text */} {aboutText && ( {aboutText} )} {/* Divider */} ); }, [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 // Render functions for different app states const renderLoginScreen = useCallback(() => { return ( ); }, []); const renderLoadingScreen = useCallback(() => { return ( ); }, []); const renderMainContent = useCallback(() => { return ( item.id} renderItem={renderItem} refreshControl={ } ListHeaderComponent={} ListEmptyComponent={ } contentContainerStyle={{ paddingBottom: insets.bottom + 20, flexGrow: entries.length === 0 ? 1 : undefined }} /> ); }, [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 (loading && entries.length === 0) { return renderLoadingScreen(); } return renderMainContent(); }