diff --git a/CHANGELOG.md b/CHANGELOG.md
index 84c1dbe..f0c2272 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,24 @@ All notable changes to the POWR project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+# Changelog - March 28, 2025
+
+## Added
+- Real-time follower and following counts in Profile screen
+ - Integrated with nostr.band API for comprehensive network statistics
+ - Created NostrBandService for efficient API interaction
+ - Implemented useProfileStats hook with auto-refresh capabilities
+ - Added proper loading states and error handling
+ - Created documentation in the new documentation structure
+
+## Improved
+- Enhanced Profile UI
+ - Reorganized profile screen layout for better information hierarchy
+ - Improved npub display with better sharing options
+ - Added inline copy and QR buttons for better usability
+ - Enhanced visual consistency across profile elements
+ - Replaced hardcoded follower counts with real-time data
+
# Changelog - March 26, 2025
## Fixed
diff --git a/app/(tabs)/profile/overview.tsx b/app/(tabs)/profile/overview.tsx
index 8ab49fa..e831e0a 100644
--- a/app/(tabs)/profile/overview.tsx
+++ b/app/(tabs)/profile/overview.tsx
@@ -1,6 +1,6 @@
// app/(tabs)/profile/overview.tsx
import React, { useState, useCallback } from 'react';
-import { View, FlatList, RefreshControl, Pressable, TouchableOpacity, ImageBackground } from 'react-native';
+import { View, FlatList, RefreshControl, Pressable, TouchableOpacity, ImageBackground, Clipboard } from 'react-native';
import { Text } from '@/components/ui/text';
import { Button } from '@/components/ui/button';
import { useNDKCurrentUser } from '@/lib/hooks/useNDK';
@@ -10,6 +10,7 @@ import NostrLoginSheet from '@/components/sheets/NostrLoginSheet';
import EnhancedSocialPost from '@/components/social/EnhancedSocialPost';
import EmptyFeed from '@/components/social/EmptyFeed';
import { useSocialFeed } from '@/lib/hooks/useSocialFeed';
+import { useProfileStats } from '@/lib/hooks/useProfileStats';
import {
AnyFeedEntry,
WorkoutFeedEntry,
@@ -24,6 +25,7 @@ import { QrCode, Mail, Copy } from 'lucide-react-native';
import { useTheme } from '@react-navigation/native';
import type { CustomTheme } from '@/lib/theme';
import { Alert } from 'react-native';
+import { nip19 } from 'nostr-tools';
// Define the conversion function for feed items
function convertToLegacyFeedItem(entry: AnyFeedEntry) {
@@ -139,6 +141,50 @@ export default function OverviewScreen() {
const pubkey = currentUser?.pubkey;
+ // Profile follower stats component
+ const ProfileFollowerStats = React.memo(({ pubkey }: { pubkey?: string }) => {
+ const { followersCount, followingCount, isLoading, error } = useProfileStats({
+ pubkey,
+ refreshInterval: 60000 * 15 // refresh every 15 minutes
+ });
+
+ return (
+
+
+
+ {isLoading ? '...' : followingCount.toLocaleString()}
+ following
+
+
+
+
+
+ {isLoading ? '...' : followersCount.toLocaleString()}
+ followers
+
+
+
+ );
+ });
+
+ // Generate npub format for display
+ const npubFormat = React.useMemo(() => {
+ if (!pubkey) return '';
+ try {
+ const npub = nip19.npubEncode(pubkey);
+ return npub;
+ } catch (error) {
+ console.error('Error encoding npub:', error);
+ return '';
+ }
+ }, [pubkey]);
+
+ // Get shortened npub display version
+ const shortenedNpub = React.useMemo(() => {
+ if (!npubFormat) return '';
+ return `${npubFormat.substring(0, 8)}...${npubFormat.substring(npubFormat.length - 5)}`;
+ }, [npubFormat]);
+
// Handle refresh
const handleRefresh = useCallback(async () => {
setIsRefreshing(true);
@@ -159,14 +205,18 @@ export default function OverviewScreen() {
console.log(`Selected ${entry.type}:`, entry);
}, []);
- // Copy pubkey to clipboard
+ // Copy npub to clipboard
const copyPubkey = useCallback(() => {
if (pubkey) {
- // Simple alert instead of clipboard functionality
- Alert.alert('Pubkey', pubkey, [
- { text: 'OK' }
- ]);
- console.log('Pubkey shown to user');
+ try {
+ const npub = nip19.npubEncode(pubkey);
+ Clipboard.setString(npub);
+ Alert.alert('Copied', 'Public key copied to clipboard in npub format');
+ console.log('npub copied to clipboard:', npub);
+ } catch (error) {
+ console.error('Error copying npub:', error);
+ Alert.alert('Error', 'Failed to copy public key');
+ }
}
}, [pubkey]);
@@ -238,22 +288,8 @@ export default function OverviewScreen() {
style={{ width: 90, height: 90 }}
/>
- {/* Action buttons - positioned to the right */}
-
-
-
-
-
-
-
-
-
+ {/* Edit Profile button - positioned to the right */}
+
router.push('/profile/settings')}
@@ -266,24 +302,35 @@ export default function OverviewScreen() {
{/* Profile info */}
{displayName}
- {username}
+ {username}
+
+ {/* Display npub below username with sharing options */}
+ {npubFormat && (
+
+
+ {shortenedNpub}
+
+
+
+
+
+
+
+
+ )}
{/* Follower stats */}
-
-
-
- 2,003
- following
-
-
-
-
-
- 4,764
- followers
-
-
-
+
{/* About text */}
{aboutText && (
@@ -295,7 +342,7 @@ export default function OverviewScreen() {
- ), [displayName, username, profileImageUrl, aboutText, pubkey, theme.colors.text, router, showQRCode, copyPubkey]);
+ ), [displayName, username, profileImageUrl, aboutText, pubkey, npubFormat, shortenedNpub, theme.colors.text, router, showQRCode, copyPubkey]);
if (loading && entries.length === 0) {
return (
diff --git a/docs/features/profile/follower_stats.md b/docs/features/profile/follower_stats.md
new file mode 100644
index 0000000..fcd6f56
--- /dev/null
+++ b/docs/features/profile/follower_stats.md
@@ -0,0 +1,170 @@
+# Profile Follower Statistics
+
+**Last Updated:** 2025-03-28
+**Status:** Active
+**Related To:** Profile Screen, Nostr Integration
+
+## Purpose
+
+This document explains the implementation and usage of real-time follower and following statistics in the POWR app's profile screen. This feature enhances the social experience by providing users with up-to-date visibility into their network connections.
+
+## Feature Overview
+
+The Profile Follower Statistics feature displays accurate counts of a user's followers and following accounts by integrating with the nostr.band API. This provides users with real-time social metrics directly in their profile view.
+
+Key capabilities:
+- Display of accurate follower and following counts
+- Automatic refresh of data at configurable intervals
+- Graceful loading states during data fetching
+- Error handling for network or API issues
+- Proper formatting of large numbers
+
+## User Experience
+
+### Profile Screen Integration
+
+Follower statistics appear in the profile header section, directly below the user's npub display. The statistics are presented in a horizontal layout with the following format:
+
+```
+[Following Count] following [Followers Count] followers
+```
+
+For example:
+```
+2,351 following 4,764 followers
+```
+
+The counts are formatted with thousands separators for better readability.
+
+### Loading States
+
+When statistics are being loaded for the first time or refreshed, the UI displays a loading indicator instead of numbers:
+
+```
+... following ... followers
+```
+
+This provides visual feedback to users that data is being fetched while maintaining layout stability.
+
+### Refresh Behavior
+
+Statistics automatically refresh at configurable intervals (default: every 15 minutes) to ensure data remains relatively current without excessive API calls. The refresh is performed in the background without disrupting the user experience.
+
+### Error Handling
+
+If an error occurs during data fetching, the UI gracefully falls back to a default state rather than showing error messages to the user. This ensures a clean user experience even when network issues or API limitations are encountered.
+
+## Technical Implementation
+
+### Components
+
+The feature is implemented using the following components:
+
+1. **ProfileFollowerStats Component**: A React component that displays follower and following counts
+2. **useProfileStats Hook**: Provides the data and loading states to the component
+3. **NostrBandService**: Service that interfaces with the nostr.band API
+
+### Implementation Details
+
+The follower stats component is embedded within the Profile screen and leverages the nostr.band API through our custom hook:
+
+```tsx
+// Profile follower stats component
+const ProfileFollowerStats = React.memo(({ pubkey }: { pubkey?: string }) => {
+ const { followersCount, followingCount, isLoading, error } = useProfileStats({
+ pubkey,
+ refreshInterval: 60000 * 15 // refresh every 15 minutes
+ });
+
+ return (
+
+
+
+ {isLoading ? '...' : followingCount.toLocaleString()}
+ following
+
+
+
+
+
+ {isLoading ? '...' : followersCount.toLocaleString()}
+ followers
+
+
+
+ );
+});
+```
+
+### Usage
+
+The component is used in the profile screen as follows:
+
+```tsx
+// Inside profile screen component
+
+```
+
+## Configuration Options
+
+The follower statistics feature can be configured with the following options:
+
+### Refresh Interval
+
+The time (in milliseconds) between automatic refreshes of the statistics:
+
+```tsx
+// Default: 15 minutes
+refreshInterval: 60000 * 15
+```
+
+This can be adjusted based on:
+- User engagement patterns
+- API rate limiting concerns
+- Network performance considerations
+
+## Design Considerations
+
+### UI Placement and Styling
+
+The follower statistics are positioned prominently in the profile header but below the user's identity information (name, username, npub) to create a clear visual hierarchy:
+
+1. User identity (name, username)
+2. User identification (npub)
+3. Network statistics (following/followers)
+4. User bio/about text
+
+This placement follows common social media patterns where follower counts are important but secondary to identity.
+
+### Interactions
+
+While currently implemented as TouchableOpacity components, the follower and following counts are prepared for future enhancements that would allow:
+- Tapping on "following" to show a list of accounts the user follows
+- Tapping on "followers" to show a list of followers
+
+These future enhancements will be implemented in subsequent updates.
+
+## Accessibility
+
+The follower statistics component is designed with accessibility in mind:
+
+- Text sizing uses relative units to respect system font size settings
+- Color contrast meets WCAG accessibility guidelines
+- Component supports screen readers with appropriate text labels
+
+## Future Enhancements
+
+Planned enhancements for the follower statistics feature include:
+
+1. Navigate to follower/following lists when tapping on counts
+2. Visual indicator for recent changes in follower counts
+3. Offline support with cached follower statistics
+4. Enhanced error states with retry options
+5. Additional statistics such as post count and engagement metrics
+
+## References
+
+- [NostrBandService](../../../lib/services/NostrBandService.ts) - The service implementation
+- [useProfileStats](../../../lib/hooks/useProfileStats.ts) - The React hook implementation
+- [Profile Overview Screen](../../../app/(tabs)/profile/overview.tsx) - The profile screen implementation
+- [nostr.band API Integration](../../technical/nostr/nostr_band_integration.md) - Technical documentation on the API integration
diff --git a/docs/technical/nostr/nostr_band_integration.md b/docs/technical/nostr/nostr_band_integration.md
new file mode 100644
index 0000000..1ee0fec
--- /dev/null
+++ b/docs/technical/nostr/nostr_band_integration.md
@@ -0,0 +1,211 @@
+# Nostr.band API Integration
+
+**Last Updated:** 2025-03-28
+**Status:** Active
+**Related To:** Profile Stats, NostrBandService, useProfileStats
+
+## Purpose
+
+This document outlines the integration of the nostr.band API in the POWR app for retrieving profile statistics and other Nostr-related data.
+
+## nostr.band API Overview
+
+[Nostr.band](https://nostr.band) is a Nostr search engine and indexer that provides a comprehensive API for accessing aggregated statistics and data from the Nostr network. The API offers various endpoints for querying profile stats, events, and other Nostr-related information.
+
+### API Base URL
+
+```
+https://api.nostr.band
+```
+
+### Authentication
+
+The nostr.band API (as of March 2025) does not require authentication for basic profile statistics. It uses simple REST endpoints with path parameters.
+
+## Available Endpoints
+
+### Profile Statistics
+
+Retrieve follower and following counts for a specific pubkey:
+
+```
+GET /v0/stats/profile/{pubkey}
+```
+
+Where `{pubkey}` is the hex-format Nostr public key.
+
+#### Response Format
+
+```json
+{
+ "stats": {
+ "[pubkey]": {
+ "pubkey": "[pubkey]",
+ "followers_pubkey_count": 123,
+ "pub_following_pubkey_count": 456,
+ // Other stats may be available
+ }
+ }
+}
+```
+
+## Implementation in POWR
+
+Our implementation consists of two main components:
+
+1. **NostrBandService**: A service class that handles API communication
+2. **useProfileStats**: A React hook that provides the stats to components
+
+### NostrBandService
+
+Located at `lib/services/NostrBandService.ts`, this service:
+
+- Provides methods to interact with the nostr.band API
+- Handles error cases and edge conditions
+- Converts between npub and hex formats as needed
+
+```typescript
+// Example usage:
+import { nostrBandService } from '@/lib/services/NostrBandService';
+
+// Fetch profile stats
+const stats = await nostrBandService.fetchProfileStats('npub1...');
+console.log(stats.followersCount); // Number of followers
+console.log(stats.followingCount); // Number of accounts being followed
+```
+
+### useProfileStats Hook
+
+Located at `lib/hooks/useProfileStats.ts`, this hook:
+
+- Wraps the NostrBandService in a React hook
+- Provides loading/error states
+- Supports automatic refresh intervals
+- Falls back to current user's pubkey if none provided
+
+```typescript
+// Example usage:
+import { useProfileStats } from '@/lib/hooks/useProfileStats';
+
+function MyComponent() {
+ const {
+ followersCount,
+ followingCount,
+ isLoading,
+ error
+ } = useProfileStats({
+ pubkey: 'npub1...', // Optional: defaults to current user
+ refreshInterval: 60000 * 15 // Optional: refreshes every 15 minutes
+ });
+
+ if (isLoading) return ;
+ if (error) return ;
+
+ return (
+
+ Followers: {followersCount}
+ Following: {followingCount}
+
+ );
+}
+```
+
+## How to Use the nostr.band API in POWR
+
+### Step 1: Import the Required Dependencies
+
+```typescript
+import { useProfileStats } from '@/lib/hooks/useProfileStats';
+// OR for direct API access:
+import { nostrBandService } from '@/lib/services/NostrBandService';
+```
+
+### Step 2: Use the Hook in Your Component
+
+```typescript
+// With the hook (recommended for React components)
+function ProfileComponent({ pubkey }) {
+ const {
+ followersCount,
+ followingCount,
+ isLoading,
+ refresh, // Function to manually refresh stats
+ lastRefreshed // Timestamp of last refresh
+ } = useProfileStats({
+ pubkey,
+ refreshInterval: 300000 // 5 minutes in milliseconds (optional)
+ });
+
+ // Now use the stats in your UI
+}
+```
+
+### Step 3: Handle Loading and Error States
+
+```typescript
+if (isLoading) {
+ return Loading stats...;
+}
+
+if (error) {
+ return Error loading stats: {error.message};
+}
+```
+
+### Step 4: Display the Data
+
+```typescript
+
+
+ {followingCount.toLocaleString()}
+ following
+
+
+
+ {followersCount.toLocaleString()}
+ followers
+
+
+```
+
+## API Rate Limiting
+
+As of March 2025, the nostr.band API has the following rate limits:
+
+- 60 requests per minute per IP address
+- 1000 requests per day per IP address
+
+To respect these limits:
+- Implement reasonable refresh intervals (e.g., 15 minutes)
+- Add caching mechanisms for frequently requested data
+- Use error handling to gracefully degrade when rate limits are reached
+
+## Error Handling
+
+The service handles common error cases:
+
+1. Invalid npub format
+2. Network errors
+3. API errors (non-200 responses)
+4. Missing data in response
+
+In all error cases, the service will:
+- Log the error to the console
+- Return a structured error object
+- Set appropriate loading/error states
+
+## Future Enhancements
+
+Potential future enhancements for the nostr.band integration:
+
+1. Add support for additional endpoints (search, events, etc.)
+2. Implement caching to reduce API calls
+3. Add offline support with cached data
+4. Expand UI to show additional statistics
+
+## References
+
+- [Nostr.band API Documentation](https://api.nostr.band/docs) (unofficial, as of 2025-03-28)
+- [Nostr.band Website](https://nostr.band)
+- [NostrBandService.ts](../../lib/services/NostrBandService.ts) - Service implementation
+- [useProfileStats.ts](../../lib/hooks/useProfileStats.ts) - Hook implementation
diff --git a/lib/hooks/useProfileStats.ts b/lib/hooks/useProfileStats.ts
new file mode 100644
index 0000000..43e21d0
--- /dev/null
+++ b/lib/hooks/useProfileStats.ts
@@ -0,0 +1,79 @@
+// lib/hooks/useProfileStats.ts
+import { useState, useEffect, useCallback } from 'react';
+import { nostrBandService, ProfileStats } from '@/lib/services/NostrBandService';
+import { useNDKCurrentUser } from '@/lib/hooks/useNDK';
+
+interface UseProfileStatsOptions {
+ pubkey?: string;
+ refreshInterval?: number; // in milliseconds
+}
+
+/**
+ * Hook to fetch profile statistics from nostr.band API
+ * Provides follower/following counts and other statistics
+ */
+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;
+
+ const [stats, setStats] = useState({
+ pubkey: pubkey || '',
+ followersCount: 0,
+ followingCount: 0,
+ isLoading: false,
+ error: null
+ });
+
+ const [lastRefreshed, setLastRefreshed] = useState(0);
+
+ // Function to fetch profile stats
+ const fetchStats = useCallback(async () => {
+ if (!pubkey) return;
+
+ setStats(prev => ({ ...prev, isLoading: true, error: null }));
+
+ try {
+ const profileStats = await nostrBandService.fetchProfileStats(pubkey);
+ setStats({
+ ...profileStats,
+ isLoading: false
+ });
+ setLastRefreshed(Date.now());
+ } catch (error) {
+ console.error('Error in useProfileStats:', error);
+ setStats(prev => ({
+ ...prev,
+ isLoading: false,
+ error: error instanceof Error ? error : new Error('Unknown error')
+ }));
+ }
+ }, [pubkey]);
+
+ // Initial fetch
+ useEffect(() => {
+ if (pubkey) {
+ fetchStats();
+ }
+ }, [pubkey, fetchStats]);
+
+ // Set up refresh interval if specified
+ useEffect(() => {
+ if (refreshInterval > 0 && pubkey) {
+ const intervalId = setInterval(fetchStats, refreshInterval);
+ return () => clearInterval(intervalId);
+ }
+ }, [refreshInterval, pubkey, fetchStats]);
+
+ // Return stats and helper functions
+ return {
+ ...stats,
+ refresh: fetchStats,
+ lastRefreshed
+ };
+}
diff --git a/lib/services/NostrBandService.ts b/lib/services/NostrBandService.ts
new file mode 100644
index 0000000..11c9962
--- /dev/null
+++ b/lib/services/NostrBandService.ts
@@ -0,0 +1,93 @@
+// lib/services/NostrBandService.ts
+import { nip19 } from 'nostr-tools';
+
+export interface ProfileStats {
+ pubkey: string;
+ followersCount: number;
+ followingCount: number;
+ isLoading: boolean;
+ error: Error | null;
+}
+
+interface NostrBandProfileStatsResponse {
+ stats: {
+ [pubkey: string]: {
+ pubkey: string;
+ followers_pubkey_count?: number;
+ pub_following_pubkey_count?: number;
+ // Add other fields as needed from the API response
+ };
+ };
+}
+
+/**
+ * Service for interacting with the NostrBand API
+ * This service provides methods to fetch statistics from nostr.band
+ */
+export class NostrBandService {
+ private readonly apiUrl = 'https://api.nostr.band';
+
+ /**
+ * Fetches profile statistics from nostr.band API
+ * @param pubkey Pubkey in hex format or npub format
+ * @returns Promise with profile stats
+ */
+ async fetchProfileStats(pubkey: string): Promise {
+ try {
+ // Check if pubkey is npub or hex and convert if needed
+ let hexPubkey = pubkey;
+ if (pubkey.startsWith('npub')) {
+ try {
+ const decoded = nip19.decode(pubkey);
+ if (decoded.type === 'npub') {
+ hexPubkey = decoded.data as string;
+ }
+ } catch (error) {
+ console.error('Error decoding npub:', error);
+ throw new Error('Invalid npub format');
+ }
+ }
+
+ // Build URL
+ const endpoint = `/v0/stats/profile/${hexPubkey}`;
+ const url = `${this.apiUrl}${endpoint}`;
+
+ // Fetch data
+ const response = await fetch(url);
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`API error: ${response.status} - ${errorText}`);
+ }
+
+ const data = await response.json() as NostrBandProfileStatsResponse;
+
+ // Extract relevant stats
+ const profileStats = data.stats[hexPubkey];
+
+ if (!profileStats) {
+ throw new Error('Profile stats not found in response');
+ }
+
+ return {
+ pubkey: hexPubkey,
+ followersCount: profileStats.followers_pubkey_count ?? 0,
+ followingCount: profileStats.pub_following_pubkey_count ?? 0,
+ isLoading: false,
+ error: null
+ };
+ } catch (error) {
+ console.error('Error fetching profile stats:', error);
+ return {
+ pubkey,
+ followersCount: 0,
+ followingCount: 0,
+ isLoading: false,
+ error: error instanceof Error ? error : new Error('Unknown error')
+ };
+ }
+ }
+}
+
+// Export a singleton instance
+export const nostrBandService = new NostrBandService();