2025-04-04 15:46:31 -04:00
|
|
|
import { useQuery } from '@tanstack/react-query';
|
2025-03-28 10:18:44 -07:00
|
|
|
import { nostrBandService, ProfileStats } from '@/lib/services/NostrBandService';
|
|
|
|
import { useNDKCurrentUser } from '@/lib/hooks/useNDK';
|
2025-04-04 15:46:31 -04:00
|
|
|
import { createLogger, enableModule } from '@/lib/utils/logger';
|
|
|
|
import { QUERY_KEYS } from '@/lib/queryKeys';
|
|
|
|
import { Platform } from 'react-native';
|
2025-04-04 18:00:20 -04:00
|
|
|
import React, { useRef, useEffect } from 'react';
|
2025-04-04 15:46:31 -04:00
|
|
|
|
|
|
|
// Enable logging
|
|
|
|
enableModule('useProfileStats');
|
|
|
|
const logger = createLogger('useProfileStats');
|
|
|
|
const platform = Platform.OS === 'ios' ? 'iOS' : 'Android';
|
2025-03-28 10:18:44 -07:00
|
|
|
|
|
|
|
interface UseProfileStatsOptions {
|
|
|
|
pubkey?: string;
|
|
|
|
refreshInterval?: number; // in milliseconds
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2025-04-04 15:46:31 -04:00
|
|
|
* Hook to fetch profile statistics from nostr.band API using React Query
|
2025-03-28 10:18:44 -07:00
|
|
|
* Provides follower/following counts and other statistics
|
2025-04-04 15:46:31 -04:00
|
|
|
* Enhanced with proper caching and refresh behavior
|
2025-03-28 10:18:44 -07:00
|
|
|
*/
|
|
|
|
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;
|
|
|
|
|
2025-04-04 18:00:20 -04:00
|
|
|
// 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: {
|
2025-04-04 22:43:03 -04:00
|
|
|
// More aggressive settings for iOS with reduced timeout
|
2025-04-04 18:00:20 -04:00
|
|
|
staleTime: 0, // No stale time - always refetch when used
|
|
|
|
gcTime: 2 * 60 * 1000, // 2 minutes
|
2025-04-04 22:43:03 -04:00
|
|
|
retry: 2, // Reduced retries on iOS to avoid hanging
|
2025-04-04 18:00:20 -04:00
|
|
|
retryDelay: 1000, // 1 second between retries
|
2025-04-04 22:43:03 -04:00
|
|
|
timeout: 6000, // Reduced to 6 second timeout for iOS (was 10s)
|
|
|
|
refetchInterval: refreshInterval > 0 ? refreshInterval : 15000, // 15 seconds default on iOS (was 10s)
|
2025-04-04 18:00:20 -04:00
|
|
|
},
|
|
|
|
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
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2025-04-04 15:46:31 -04:00
|
|
|
const query = useQuery({
|
|
|
|
queryKey: QUERY_KEYS.profile.stats(pubkey),
|
2025-04-04 18:00:20 -04:00
|
|
|
queryFn: async ({ signal }) => {
|
2025-04-04 15:46:31 -04:00
|
|
|
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 {
|
2025-04-04 22:43:03 -04:00
|
|
|
// Create our own abort controller that we can trigger manually on timeout
|
|
|
|
const timeoutController = new AbortController();
|
|
|
|
|
|
|
|
// Configure timeout
|
2025-04-04 18:00:20 -04:00
|
|
|
const timeoutId = setTimeout(() => {
|
|
|
|
if (!signal.aborted && isMounted.current) {
|
|
|
|
logger.warn(`[${platform}] Profile stats fetch timed out after ${platformConfig.timeout}ms`);
|
2025-04-04 22:43:03 -04:00
|
|
|
// Abort our manual controller to cancel the fetch
|
|
|
|
try {
|
|
|
|
timeoutController.abort();
|
|
|
|
} catch (e) {
|
|
|
|
logger.error(`[${platform}] Error aborting fetch: ${e}`);
|
|
|
|
}
|
2025-04-04 18:00:20 -04:00
|
|
|
}
|
|
|
|
}, platformConfig.timeout);
|
2025-04-04 15:46:31 -04:00
|
|
|
|
2025-04-04 18:00:20 -04:00
|
|
|
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);
|
|
|
|
}
|
2025-04-04 15:46:31 -04:00
|
|
|
} catch (error) {
|
|
|
|
logger.error(`[${platform}] Error fetching profile stats: ${error}`);
|
2025-04-04 18:00:20 -04:00
|
|
|
|
2025-04-04 22:43:03 -04:00
|
|
|
// 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))
|
|
|
|
};
|
2025-04-04 15:46:31 -04:00
|
|
|
}
|
|
|
|
},
|
2025-04-04 18:00:20 -04:00
|
|
|
// 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
|
2025-04-04 15:46:31 -04:00
|
|
|
refetchOnReconnect: true,
|
2025-04-04 18:00:20 -04:00
|
|
|
refetchInterval: platformConfig.refetchInterval,
|
2025-04-04 15:46:31 -04:00
|
|
|
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 = {
|
2025-03-28 10:18:44 -07:00
|
|
|
pubkey: pubkey || '',
|
|
|
|
followersCount: 0,
|
|
|
|
followingCount: 0,
|
|
|
|
isLoading: false,
|
|
|
|
error: null
|
2025-04-04 15:46:31 -04:00
|
|
|
};
|
2025-03-28 10:18:44 -07:00
|
|
|
|
2025-04-04 15:46:31 -04:00
|
|
|
// Access the data directly from query.data with typed default
|
|
|
|
const data = query.data || defaultStats;
|
2025-03-28 10:18:44 -07:00
|
|
|
|
2025-04-04 15:46:31 -04:00
|
|
|
// 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
|
|
|
|
};
|
2025-03-28 10:18:44 -07:00
|
|
|
|
2025-04-04 15:46:31 -04:00
|
|
|
// 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()
|
|
|
|
});
|
2025-03-28 10:18:44 -07:00
|
|
|
|
2025-04-04 15:46:31 -04:00
|
|
|
return result;
|
2025-03-28 10:18:44 -07:00
|
|
|
}
|