mirror of
https://github.com/DocNR/POWR.git
synced 2025-04-23 01:01:27 +00:00
nostr.band integration for follow/following count
This commit is contained in:
parent
08bb9884bc
commit
3f2ababe4f
18
CHANGELOG.md
18
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/),
|
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).
|
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
|
# Changelog - March 26, 2025
|
||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// app/(tabs)/profile/overview.tsx
|
// app/(tabs)/profile/overview.tsx
|
||||||
import React, { useState, useCallback } from 'react';
|
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 { Text } from '@/components/ui/text';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useNDKCurrentUser } from '@/lib/hooks/useNDK';
|
import { useNDKCurrentUser } from '@/lib/hooks/useNDK';
|
||||||
@ -10,6 +10,7 @@ import NostrLoginSheet from '@/components/sheets/NostrLoginSheet';
|
|||||||
import EnhancedSocialPost from '@/components/social/EnhancedSocialPost';
|
import EnhancedSocialPost from '@/components/social/EnhancedSocialPost';
|
||||||
import EmptyFeed from '@/components/social/EmptyFeed';
|
import EmptyFeed from '@/components/social/EmptyFeed';
|
||||||
import { useSocialFeed } from '@/lib/hooks/useSocialFeed';
|
import { useSocialFeed } from '@/lib/hooks/useSocialFeed';
|
||||||
|
import { useProfileStats } from '@/lib/hooks/useProfileStats';
|
||||||
import {
|
import {
|
||||||
AnyFeedEntry,
|
AnyFeedEntry,
|
||||||
WorkoutFeedEntry,
|
WorkoutFeedEntry,
|
||||||
@ -24,6 +25,7 @@ import { QrCode, Mail, Copy } from 'lucide-react-native';
|
|||||||
import { useTheme } from '@react-navigation/native';
|
import { useTheme } from '@react-navigation/native';
|
||||||
import type { CustomTheme } from '@/lib/theme';
|
import type { CustomTheme } from '@/lib/theme';
|
||||||
import { Alert } from 'react-native';
|
import { Alert } from 'react-native';
|
||||||
|
import { nip19 } from 'nostr-tools';
|
||||||
|
|
||||||
// Define the conversion function for feed items
|
// Define the conversion function for feed items
|
||||||
function convertToLegacyFeedItem(entry: AnyFeedEntry) {
|
function convertToLegacyFeedItem(entry: AnyFeedEntry) {
|
||||||
@ -139,6 +141,50 @@ export default function OverviewScreen() {
|
|||||||
|
|
||||||
const pubkey = currentUser?.pubkey;
|
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 (
|
||||||
|
<View className="flex-row mb-2">
|
||||||
|
<TouchableOpacity className="mr-4">
|
||||||
|
<Text>
|
||||||
|
<Text className="font-bold">{isLoading ? '...' : followingCount.toLocaleString()}</Text>
|
||||||
|
<Text className="text-muted-foreground"> following</Text>
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity>
|
||||||
|
<Text>
|
||||||
|
<Text className="font-bold">{isLoading ? '...' : followersCount.toLocaleString()}</Text>
|
||||||
|
<Text className="text-muted-foreground"> followers</Text>
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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
|
// Handle refresh
|
||||||
const handleRefresh = useCallback(async () => {
|
const handleRefresh = useCallback(async () => {
|
||||||
setIsRefreshing(true);
|
setIsRefreshing(true);
|
||||||
@ -159,14 +205,18 @@ export default function OverviewScreen() {
|
|||||||
console.log(`Selected ${entry.type}:`, entry);
|
console.log(`Selected ${entry.type}:`, entry);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Copy pubkey to clipboard
|
// Copy npub to clipboard
|
||||||
const copyPubkey = useCallback(() => {
|
const copyPubkey = useCallback(() => {
|
||||||
if (pubkey) {
|
if (pubkey) {
|
||||||
// Simple alert instead of clipboard functionality
|
try {
|
||||||
Alert.alert('Pubkey', pubkey, [
|
const npub = nip19.npubEncode(pubkey);
|
||||||
{ text: 'OK' }
|
Clipboard.setString(npub);
|
||||||
]);
|
Alert.alert('Copied', 'Public key copied to clipboard in npub format');
|
||||||
console.log('Pubkey shown to user');
|
console.log('npub copied to clipboard:', npub);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error copying npub:', error);
|
||||||
|
Alert.alert('Error', 'Failed to copy public key');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [pubkey]);
|
}, [pubkey]);
|
||||||
|
|
||||||
@ -238,22 +288,8 @@ export default function OverviewScreen() {
|
|||||||
style={{ width: 90, height: 90 }}
|
style={{ width: 90, height: 90 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Action buttons - positioned to the right */}
|
{/* Edit Profile button - positioned to the right */}
|
||||||
<View className="flex-row ml-auto mb-2">
|
<View className="ml-auto mb-2">
|
||||||
<TouchableOpacity
|
|
||||||
className="w-10 h-10 items-center justify-center rounded-md bg-muted mr-2"
|
|
||||||
onPress={showQRCode}
|
|
||||||
>
|
|
||||||
<QrCode size={18} color={theme.colors.text} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
className="w-10 h-10 items-center justify-center rounded-md bg-muted mr-2"
|
|
||||||
onPress={copyPubkey}
|
|
||||||
>
|
|
||||||
<Copy size={18} color={theme.colors.text} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
className="px-4 h-10 items-center justify-center rounded-md bg-muted"
|
className="px-4 h-10 items-center justify-center rounded-md bg-muted"
|
||||||
onPress={() => router.push('/profile/settings')}
|
onPress={() => router.push('/profile/settings')}
|
||||||
@ -266,24 +302,35 @@ export default function OverviewScreen() {
|
|||||||
{/* Profile info */}
|
{/* Profile info */}
|
||||||
<View>
|
<View>
|
||||||
<Text className="text-xl font-bold">{displayName}</Text>
|
<Text className="text-xl font-bold">{displayName}</Text>
|
||||||
<Text className="text-muted-foreground mb-2">{username}</Text>
|
<Text className="text-muted-foreground">{username}</Text>
|
||||||
|
|
||||||
|
{/* Display npub below username with sharing options */}
|
||||||
|
{npubFormat && (
|
||||||
|
<View className="flex-row items-center mt-1 mb-2">
|
||||||
|
<Text className="text-xs text-muted-foreground font-mono">
|
||||||
|
{shortenedNpub}
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
className="ml-2 p-1"
|
||||||
|
onPress={copyPubkey}
|
||||||
|
accessibilityLabel="Copy public key"
|
||||||
|
accessibilityHint="Copies your Nostr public key to clipboard"
|
||||||
|
>
|
||||||
|
<Copy size={12} color={theme.colors.text} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
className="ml-2 p-1"
|
||||||
|
onPress={showQRCode}
|
||||||
|
accessibilityLabel="Show QR Code"
|
||||||
|
accessibilityHint="Shows a QR code with your Nostr public key"
|
||||||
|
>
|
||||||
|
<QrCode size={12} color={theme.colors.text} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Follower stats */}
|
{/* Follower stats */}
|
||||||
<View className="flex-row mb-2">
|
<ProfileFollowerStats pubkey={pubkey} />
|
||||||
<TouchableOpacity className="mr-4">
|
|
||||||
<Text>
|
|
||||||
<Text className="font-bold">2,003</Text>
|
|
||||||
<Text className="text-muted-foreground"> following</Text>
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<TouchableOpacity>
|
|
||||||
<Text>
|
|
||||||
<Text className="font-bold">4,764</Text>
|
|
||||||
<Text className="text-muted-foreground"> followers</Text>
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* About text */}
|
{/* About text */}
|
||||||
{aboutText && (
|
{aboutText && (
|
||||||
@ -295,7 +342,7 @@ export default function OverviewScreen() {
|
|||||||
<View className="h-px bg-border w-full mt-2" />
|
<View className="h-px bg-border w-full mt-2" />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
), [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) {
|
if (loading && entries.length === 0) {
|
||||||
return (
|
return (
|
||||||
|
170
docs/features/profile/follower_stats.md
Normal file
170
docs/features/profile/follower_stats.md
Normal file
@ -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 (
|
||||||
|
<View className="flex-row mb-2">
|
||||||
|
<TouchableOpacity className="mr-4">
|
||||||
|
<Text>
|
||||||
|
<Text className="font-bold">{isLoading ? '...' : followingCount.toLocaleString()}</Text>
|
||||||
|
<Text className="text-muted-foreground"> following</Text>
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity>
|
||||||
|
<Text>
|
||||||
|
<Text className="font-bold">{isLoading ? '...' : followersCount.toLocaleString()}</Text>
|
||||||
|
<Text className="text-muted-foreground"> followers</Text>
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
The component is used in the profile screen as follows:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Inside profile screen component
|
||||||
|
<ProfileFollowerStats pubkey={currentUser?.pubkey} />
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
211
docs/technical/nostr/nostr_band_integration.md
Normal file
211
docs/technical/nostr/nostr_band_integration.md
Normal file
@ -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 <LoadingIndicator />;
|
||||||
|
if (error) return <ErrorMessage error={error} />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Text>Followers: {followersCount}</Text>
|
||||||
|
<Text>Following: {followingCount}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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 <Text>Loading stats...</Text>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <Text>Error loading stats: {error.message}</Text>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Display the Data
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<View className="flex-row mb-2">
|
||||||
|
<Text>
|
||||||
|
<Text className="font-bold">{followingCount.toLocaleString()}</Text>
|
||||||
|
<Text className="text-muted-foreground"> following</Text>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text className="ml-4">
|
||||||
|
<Text className="font-bold">{followersCount.toLocaleString()}</Text>
|
||||||
|
<Text className="text-muted-foreground"> followers</Text>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
79
lib/hooks/useProfileStats.ts
Normal file
79
lib/hooks/useProfileStats.ts
Normal file
@ -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<ProfileStats>({
|
||||||
|
pubkey: pubkey || '',
|
||||||
|
followersCount: 0,
|
||||||
|
followingCount: 0,
|
||||||
|
isLoading: false,
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const [lastRefreshed, setLastRefreshed] = useState<number>(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
|
||||||
|
};
|
||||||
|
}
|
93
lib/services/NostrBandService.ts
Normal file
93
lib/services/NostrBandService.ts
Normal file
@ -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<ProfileStats> {
|
||||||
|
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();
|
Loading…
x
Reference in New Issue
Block a user