
* Add style prop to UserAvatar component for better customization * Refactor UserAvatar to use getAvatarSeed utility for consistent avatar generation * Fix React hook ordering issues in profile/overview.tsx to prevent crashes during auth state changes * Add proper state initialization and cleanup during authentication transitions * Ensure consistent fallback avatar display for unauthenticated users These changes improve stability during login/logout operations and provide better visual continuity with Robohash avatars when profile images aren't available.
8.0 KiB
Follower Statistics
Last Updated: 2025-04-02
Status: Implemented
Related To: Profile Tab, NostrBand Integration
Introduction
The follower statistics feature provides real-time information about a user's Nostr followers and following counts. This document outlines the technical implementation of follower statistics, which is primarily displayed in the Profile tab.
Implementation Overview
Follower statistics are implemented through integration with the NostrBand API, which provides network-wide statistics for Nostr users, including follower counts. This integration is handled through the NostrBandService and exposed via the useProfileStats hook.
Key Components
- NostrBandService: Core service that interfaces with the NostrBand API
- useProfileStats Hook: React hook that provides access to follower statistics
- ProfileFollowerStats Component: UI component for displaying follower statistics
NostrBand Service Implementation
The NostrBandService (implemented in lib/services/NostrBandService.ts
) provides methods for fetching follower statistics:
// NostrBand service for fetching follower statistics
export class NostrBandService {
private baseUrl = 'https://api.nostr.band/v0';
// Fetch follower statistics for a given public key
public async getProfileStats(pubkey: string): Promise<ProfileStats> {
if (!pubkey) {
return { followersCount: 0, followingCount: 0 };
}
try {
const response = await fetch(`${this.baseUrl}/profile/${pubkey}/stats`);
if (!response.ok) {
throw new Error(`NostrBand API error: ${response.status}`);
}
const data = await response.json();
return {
followersCount: data.followers_count || 0,
followingCount: data.following_count || 0
};
} catch (error) {
console.error('Error fetching profile stats:', error);
// Return default values on error
return { followersCount: 0, followingCount: 0 };
}
}
}
// Singleton instance
export const nostrBandService = new NostrBandService();
ProfileStats Interface
The ProfileStats interface defines the structure of follower statistics:
export interface ProfileStats {
followersCount: number;
followingCount: number;
}
useProfileStats Hook
The useProfileStats hook (implemented in lib/hooks/useProfileStats.ts
) provides a React interface for accessing follower statistics:
// Hook for accessing profile statistics
export function useProfileStats({
pubkey,
refreshInterval = 0
}: {
pubkey: string;
refreshInterval?: number;
}): {
followersCount: number;
followingCount: number;
isLoading: boolean;
refresh: () => Promise<void>;
} {
const [stats, setStats] = useState<ProfileStats>({ followersCount: 0, followingCount: 0 });
const [isLoading, setIsLoading] = useState(true);
// Fetch profile stats
const fetchStats = useCallback(async () => {
if (!pubkey) {
setStats({ followersCount: 0, followingCount: 0 });
setIsLoading(false);
return;
}
try {
setIsLoading(true);
const profileStats = await nostrBandService.getProfileStats(pubkey);
setStats(profileStats);
} catch (error) {
console.error('Error in useProfileStats:', error);
} finally {
setIsLoading(false);
}
}, [pubkey]);
// Initial fetch and refresh interval
useEffect(() => {
fetchStats();
if (refreshInterval > 0) {
const interval = setInterval(fetchStats, refreshInterval);
return () => clearInterval(interval);
}
}, [fetchStats, refreshInterval]);
// Return stats and loading state
return {
...stats,
isLoading,
refresh: fetchStats
};
}
Profile Integration
The follower statistics are displayed in the Profile tab using the ProfileFollowerStats component:
// Profile follower stats component
const ProfileFollowerStats = React.memo(() => {
return (
<View className="flex-row mb-2">
<TouchableOpacity className="mr-4">
<Text>
<Text className="font-bold">{statsLoading ? '...' : followingCount.toLocaleString()}</Text>
<Text className="text-muted-foreground"> following</Text>
</Text>
</TouchableOpacity>
<TouchableOpacity>
<Text>
<Text className="font-bold">{statsLoading ? '...' : followersCount.toLocaleString()}</Text>
<Text className="text-muted-foreground"> followers</Text>
</Text>
</TouchableOpacity>
</View>
);
});
This component is called in the Profile tab header:
// In the Profile header
const { followersCount, followingCount, isLoading: statsLoading } = useProfileStats({
pubkey: pubkey || '',
refreshInterval: 60000 * 15 // refresh every 15 minutes
});
// Later in the JSX
<ProfileFollowerStats />
Hook Ordering Consistency
To maintain React hook ordering consistency regardless of authentication state, the useProfileStats hook is always called, even when the user is not authenticated:
// Always call useProfileStats hook, even if isAuthenticated is false
// This ensures consistent hook ordering regardless of authentication state
const { followersCount, followingCount, isLoading: statsLoading } = useProfileStats({
pubkey: pubkey || '',
refreshInterval: 60000 * 15
});
When not authenticated or when the pubkey is empty, the hook returns default values (zero counts).
Caching Strategy
Follower statistics are cached to reduce API calls and improve performance:
// In NostrBandService
private cache = new Map<string, { stats: ProfileStats; timestamp: number }>();
private cacheTTL = 15 * 60 * 1000; // 15 minutes
public async getProfileStats(pubkey: string): Promise<ProfileStats> {
// Check cache first
const cached = this.cache.get(pubkey);
if (cached && Date.now() - cached.timestamp < this.cacheTTL) {
return cached.stats;
}
// Fetch from API if not cached or expired
try {
const stats = await this.fetchProfileStats(pubkey);
// Update cache
this.cache.set(pubkey, {
stats,
timestamp: Date.now()
});
return stats;
} catch (error) {
// Error handling...
}
}
Offline Support
The follower statistics feature gracefully handles offline states:
public async getProfileStats(pubkey: string): Promise<ProfileStats> {
// Check connectivity
if (!this.connectivityService.isOnline()) {
// Return cached data if available
const cached = this.cache.get(pubkey);
if (cached) {
return cached.stats;
}
// Return default values if no cached data
return { followersCount: 0, followingCount: 0 };
}
// Proceed with normal API call if online
// ...
}
Performance Considerations
To ensure good performance, several optimizations are implemented:
- Memoization: The ProfileFollowerStats component is memoized to prevent unnecessary re-renders
- Refresh Interval: Stats are only refreshed at specific intervals (default: 15 minutes)
- Caching: Follower statistics are cached to reduce API calls
- Lazy Loading: Stats are loaded after the profile is rendered to prioritize UI responsiveness
Future Enhancements
Future enhancements to follower statistics include:
- Follower List: Display a list of followers/following with profile information
- Interaction Analytics: Show interaction statistics with followers
- Growth Tracking: Track follower growth over time
- Network Visualization: Visualize the user's Nostr network
- Notification Integration: Notify users of new followers
Related Documentation
- Profile Tab - UI implementation of profile display
- NostrBand Integration - Technical details of the NostrBand API integration
- Authentication Patterns - How authentication affects hook ordering