POWR/lib/hooks/useProfilePageData.tsx
DocNR c441c5afa5 feat(profile): Improve profile loading performance with tiered display system
- Add ultra-early content display after just 500ms with ANY data available
- Implement progressive content loading with three-tier timeout system
- Reduce timeouts from 5s to 4s on Android and 4s to 3s on iOS
- Enhance render state logic to prioritize partial content display
- Improve parallel data loading for all profile elements
- Add multiple fallback timers to ensure content always displays
- Update CHANGELOG.md with detailed performance improvements

This commit dramatically improves perceived performance by showing content
as soon as it becomes available rather than waiting for complete data load.
2025-04-04 22:43:03 -04:00

278 lines
9.4 KiB
TypeScript

// lib/hooks/useProfilePageData.tsx
import { useState, useEffect, useCallback } from 'react';
import { Platform } from 'react-native';
import { useNDKCurrentUser } from '@/lib/hooks/useNDK';
import { useProfileStats } from '@/lib/hooks/useProfileStats';
import { useBannerImage } from '@/lib/hooks/useBannerImage';
import { useSocialFeed } from '@/lib/hooks/useSocialFeed';
import type { AnyFeedEntry, WorkoutFeedEntry, SocialFeedEntry, TemplateFeedEntry, ExerciseFeedEntry, ArticleFeedEntry } from '@/types/feed';
// Helper function to convert feed entries for social posts component
export function convertToLegacyFeedItem(entry: AnyFeedEntry) {
return {
id: entry.eventId,
type: entry.type,
originalEvent: entry.event!,
parsedContent: entry.content!,
createdAt: (entry.timestamp || Date.now()) / 1000
};
}
// Define possible render states
type RenderState = 'login' | 'loading' | 'content' | 'error';
/**
* Custom hook for managing profile page data and state
* Centralizes all data fetching logic in one place
*/
export function useProfilePageData() {
// Authentication state (always call this hook)
const { currentUser, isAuthenticated } = useNDKCurrentUser();
// Initialize all state hooks at the top
const [feedItems, setFeedItems] = useState<any[]>([]);
const [feedLoading, setFeedLoading] = useState(false);
const [isOffline, setIsOffline] = useState(false);
const [entries, setEntries] = useState<AnyFeedEntry[]>([]);
const [renderError, setRenderError] = useState<Error | null>(null);
const [loadAttempts, setLoadAttempts] = useState(0);
// Current pubkey (or empty string if not authenticated)
const pubkey = currentUser?.pubkey || '';
// Always call all hooks regardless of auth state
// For unauthorized state, we pass empty pubkey or arrays
// Profile stats (with empty pubkey fallback)
const stats = useProfileStats({
pubkey,
refreshInterval: 10000
});
// Banner image (with empty pubkey fallback)
const defaultBannerUrl = currentUser?.profile?.banner ||
(currentUser?.profile as any)?.background;
const { data: bannerImageUrl, refetch: refetchBanner } = useBannerImage(
pubkey,
defaultBannerUrl
);
// Social feed (with empty authors array fallback for unauthenticated state)
const socialFeed = useSocialFeed({
feedType: 'profile',
authors: isAuthenticated && pubkey ? [pubkey] : [],
limit: 30
});
// Extract values from socialFeed with fallbacks
const loading = socialFeed?.loading || feedLoading;
const refresh = socialFeed?.refresh || (() => Promise.resolve());
// Performance optimization: Start loading data immediately without waiting for full load
useEffect(() => {
let timeoutId: NodeJS.Timeout | null = null;
let progressTimerId: NodeJS.Timeout | null = null;
let ultraEarlyTimerId: NodeJS.Timeout | null = null;
if (isAuthenticated && loading) {
// Ultra-early timeout - show content after just 500ms if we have ANY data at all
ultraEarlyTimerId = setTimeout(() => {
if (entries.length > 0 || stats.followersCount > 0 || stats.followingCount > 0) {
console.log(`[${Platform.OS}] Ultra-early content display with partial data`);
setFeedLoading(false);
}
}, 500);
// Very early timeout - after 1s, force content to display if we have any data
progressTimerId = setTimeout(() => {
console.log(`[${Platform.OS}] Early timeout: Forcing content display after 1s`);
setFeedLoading(false);
}, 1000);
// Final safety timeout - much shorter than before
const timeoutDuration = Platform.OS === 'ios' ? 3000 : 4000; // 3s for iOS, 4s for Android
timeoutId = setTimeout(() => {
console.log(`[${Platform.OS}] Final safety timeout triggered after ${timeoutDuration}ms`);
setLoadAttempts(prev => prev + 1);
setFeedLoading(false);
// Try refreshing in parallel for faster results
Promise.all([
refresh().catch(e => console.error(`[${Platform.OS}] Feed refresh error:`, e)),
refetchBanner().catch(e => console.error(`[${Platform.OS}] Banner refresh error:`, e)),
stats.refresh?.().catch(e => console.error(`[${Platform.OS}] Stats refresh error:`, e))
]).catch(e => {
console.error(`[${Platform.OS}] Refresh error:`, e);
});
}, timeoutDuration);
}
return () => {
if (ultraEarlyTimerId) clearTimeout(ultraEarlyTimerId);
if (progressTimerId) clearTimeout(progressTimerId);
if (timeoutId) clearTimeout(timeoutId);
};
}, [isAuthenticated, loading, refresh, entries.length, stats, refetchBanner]);
// 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
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]);
// Determine current render state - even more aggressive about showing content early
const renderState: RenderState = !isAuthenticated
? 'login'
: renderError ? 'error'
: (entries.length > 0 || stats.followersCount > 0 || stats.followingCount > 0) ? 'content' // Show content as soon as ANY data is available
: (loading && loadAttempts < 2) ? 'loading'
: 'content'; // Fallback to content even if loading to avoid stuck loading screen
// Combined refresh function for refreshing all data
const refreshAll = useCallback(async () => {
console.log(`[${Platform.OS}] Starting full profile refresh...`);
try {
// Create an array of refresh promises to run in parallel
const refreshPromises = [];
// Refresh feed content
if (refresh) {
refreshPromises.push(
refresh()
.catch(error => console.error(`[${Platform.OS}] Error refreshing feed:`, error))
);
}
// Refresh profile stats
if (stats.refresh) {
refreshPromises.push(
stats.refresh()
.catch(error => console.error(`[${Platform.OS}] Error refreshing profile stats:`, error))
);
}
// Refresh banner image
if (refetchBanner) {
refreshPromises.push(
refetchBanner()
.catch(error => console.error(`[${Platform.OS}] Error refreshing banner image:`, error))
);
}
// Wait for all refresh operations to complete
await Promise.all(refreshPromises);
console.log(`[${Platform.OS}] Profile refresh completed successfully`);
} catch (error) {
console.error(`[${Platform.OS}] Error during profile refresh:`, error);
}
}, [refresh, stats.refresh, refetchBanner]);
// Return all the data and functions needed by the profile screen
return {
isAuthenticated,
currentUser,
stats: {
followersCount: stats.followersCount,
followingCount: stats.followingCount,
refresh: stats.refresh,
isLoading: stats.isLoading,
},
bannerImage: {
url: bannerImageUrl,
defaultUrl: defaultBannerUrl,
refetch: refetchBanner,
},
feed: {
entries,
loading,
isOffline,
refresh,
},
renderState,
renderError,
refreshAll,
loadAttempts,
setRenderError,
};
}