mirror of
https://github.com/DocNR/POWR.git
synced 2025-04-23 01:01:27 +00:00

- 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.
215 lines
7.4 KiB
TypeScript
215 lines
7.4 KiB
TypeScript
import { useQuery } from '@tanstack/react-query';
|
|
import { nostrBandService, ProfileStats } from '@/lib/services/NostrBandService';
|
|
import { useNDKCurrentUser } from '@/lib/hooks/useNDK';
|
|
import { createLogger, enableModule } from '@/lib/utils/logger';
|
|
import { QUERY_KEYS } from '@/lib/queryKeys';
|
|
import { Platform } from 'react-native';
|
|
import React, { useRef, useEffect } from 'react';
|
|
|
|
// Enable logging
|
|
enableModule('useProfileStats');
|
|
const logger = createLogger('useProfileStats');
|
|
const platform = Platform.OS === 'ios' ? 'iOS' : 'Android';
|
|
|
|
interface UseProfileStatsOptions {
|
|
pubkey?: string;
|
|
refreshInterval?: number; // in milliseconds
|
|
}
|
|
|
|
/**
|
|
* Hook to fetch profile statistics from nostr.band API using React Query
|
|
* Provides follower/following counts and other statistics
|
|
* Enhanced with proper caching and refresh behavior
|
|
*/
|
|
export function useProfileStats(options: UseProfileStatsOptions = {}) {
|
|
const { currentUser } = useNDKCurrentUser();
|
|
const {
|
|
pubkey: optionsPubkey,
|
|
refreshInterval = 0 // default to no auto-refresh
|
|
} = options;
|
|
|
|
// Use provided pubkey or fall back to current user's pubkey
|
|
const pubkey = optionsPubkey || currentUser?.pubkey;
|
|
|
|
// Track if component is mounted to prevent memory leaks
|
|
const isMounted = useRef(true);
|
|
useEffect(() => {
|
|
isMounted.current = true;
|
|
return () => {
|
|
isMounted.current = false;
|
|
};
|
|
}, []);
|
|
|
|
// Platform-specific configuration
|
|
const platformConfig = Platform.select({
|
|
android: {
|
|
// More conservative settings for Android to prevent hanging
|
|
staleTime: 60 * 1000, // 1 minute - reuse cached data more aggressively
|
|
gcTime: 5 * 60 * 1000, // 5 minutes garbage collection time
|
|
retry: 2, // Fewer retries on Android
|
|
retryDelay: 2000, // Longer delay between retries
|
|
timeout: 6000, // 6 second timeout for Android
|
|
refetchInterval: refreshInterval > 0 ? refreshInterval : 30000, // 30 seconds default on Android
|
|
},
|
|
ios: {
|
|
// More aggressive settings for iOS with reduced timeout
|
|
staleTime: 0, // No stale time - always refetch when used
|
|
gcTime: 2 * 60 * 1000, // 2 minutes
|
|
retry: 2, // Reduced retries on iOS to avoid hanging
|
|
retryDelay: 1000, // 1 second between retries
|
|
timeout: 6000, // Reduced to 6 second timeout for iOS (was 10s)
|
|
refetchInterval: refreshInterval > 0 ? refreshInterval : 15000, // 15 seconds default on iOS (was 10s)
|
|
},
|
|
default: {
|
|
// Fallback settings
|
|
staleTime: 30 * 1000, // 30 seconds
|
|
gcTime: 2 * 60 * 1000, // 2 minutes
|
|
retry: 2,
|
|
retryDelay: 1500,
|
|
timeout: 8000, // 8 second timeout
|
|
refetchInterval: refreshInterval > 0 ? refreshInterval : 20000, // 20 seconds default
|
|
}
|
|
});
|
|
|
|
const query = useQuery({
|
|
queryKey: QUERY_KEYS.profile.stats(pubkey),
|
|
queryFn: async ({ signal }) => {
|
|
if (!pubkey) {
|
|
logger.warn(`[${platform}] No pubkey provided to useProfileStats`);
|
|
return {
|
|
pubkey: '',
|
|
followersCount: 0,
|
|
followingCount: 0,
|
|
isLoading: false,
|
|
error: null
|
|
} as ProfileStats;
|
|
}
|
|
|
|
logger.info(`[${platform}] Fetching profile stats for ${pubkey?.substring(0, 8)}...`);
|
|
|
|
try {
|
|
// Create our own abort controller that we can trigger manually on timeout
|
|
const timeoutController = new AbortController();
|
|
|
|
// Configure timeout
|
|
const timeoutId = setTimeout(() => {
|
|
if (!signal.aborted && isMounted.current) {
|
|
logger.warn(`[${platform}] Profile stats fetch timed out after ${platformConfig.timeout}ms`);
|
|
// Abort our manual controller to cancel the fetch
|
|
try {
|
|
timeoutController.abort();
|
|
} catch (e) {
|
|
logger.error(`[${platform}] Error aborting fetch: ${e}`);
|
|
}
|
|
}
|
|
}, platformConfig.timeout);
|
|
|
|
try {
|
|
// Force bypass cache to get latest counts when explicitly fetched
|
|
const profileStats = await nostrBandService.fetchProfileStats(pubkey, true);
|
|
|
|
if (isMounted.current) {
|
|
logger.info(`[${platform}] Retrieved profile stats: ${JSON.stringify({
|
|
followersCount: profileStats.followersCount,
|
|
followingCount: profileStats.followingCount
|
|
})}`);
|
|
|
|
// React Query will handle caching for us
|
|
return {
|
|
...profileStats,
|
|
isLoading: false,
|
|
error: null
|
|
};
|
|
} else {
|
|
// Component unmounted, return empty stats to avoid unnecessary processing
|
|
return {
|
|
pubkey,
|
|
followersCount: 0,
|
|
followingCount: 0,
|
|
isLoading: false,
|
|
error: null
|
|
};
|
|
}
|
|
} finally {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
} catch (error) {
|
|
logger.error(`[${platform}] Error fetching profile stats: ${error}`);
|
|
|
|
// On any platform, return fallback values rather than throwing
|
|
// This prevents the UI from hanging in error states
|
|
logger.warn(`[${platform}] Returning fallback stats after error`);
|
|
return {
|
|
pubkey,
|
|
followersCount: 0,
|
|
followingCount: 0,
|
|
isLoading: false,
|
|
error: error instanceof Error ? error : new Error(String(error))
|
|
};
|
|
}
|
|
},
|
|
// Configuration based on platform
|
|
staleTime: platformConfig.staleTime,
|
|
gcTime: platformConfig.gcTime,
|
|
retry: platformConfig.retry,
|
|
retryDelay: platformConfig.retryDelay,
|
|
refetchOnMount: true,
|
|
refetchOnWindowFocus: platform === 'iOS', // Only iOS refreshes on window focus
|
|
refetchOnReconnect: true,
|
|
refetchInterval: platformConfig.refetchInterval,
|
|
enabled: !!pubkey,
|
|
});
|
|
|
|
// Enable more verbose debugging
|
|
if (pubkey && (query.isLoading || query.isPending)) {
|
|
logger.info(`[${platform}] ProfileStats loading for ${pubkey.substring(0, 8)}...`);
|
|
}
|
|
|
|
if (query.error) {
|
|
logger.error(`[${platform}] ProfileStats error: ${query.error}`);
|
|
}
|
|
|
|
if (query.isSuccess && query.data) {
|
|
logger.info(`[${platform}] ProfileStats success:`, {
|
|
followersCount: query.data.followersCount,
|
|
followingCount: query.data.followingCount
|
|
});
|
|
}
|
|
|
|
// Use a properly typed default value for when query.data is undefined
|
|
const defaultStats: ProfileStats = {
|
|
pubkey: pubkey || '',
|
|
followersCount: 0,
|
|
followingCount: 0,
|
|
isLoading: false,
|
|
error: null
|
|
};
|
|
|
|
// Access the data directly from query.data with typed default
|
|
const data = query.data || defaultStats;
|
|
|
|
// Create explicit copy of values to ensure reactive updates
|
|
const result = {
|
|
pubkey: pubkey || '',
|
|
followersCount: data.followersCount,
|
|
followingCount: data.followingCount,
|
|
isLoading: query.isLoading,
|
|
error: query.error instanceof Error ? query.error : null,
|
|
refresh: async () => {
|
|
logger.info(`[${platform}] Manually refreshing stats for ${pubkey?.substring(0, 8)}...`);
|
|
return query.refetch();
|
|
},
|
|
lastRefreshed: query.dataUpdatedAt
|
|
};
|
|
|
|
// Log every time we return stats for debugging
|
|
logger.debug(`[${platform}] Returning stats:`, {
|
|
followersCount: result.followersCount,
|
|
followingCount: result.followingCount,
|
|
isLoading: result.isLoading,
|
|
lastRefreshed: new Date(result.lastRefreshed).toISOString()
|
|
});
|
|
|
|
return result;
|
|
}
|