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; }