diff --git a/src/components/ProfileView.tsx b/src/components/ProfileView.tsx index e57bbbe..488dadf 100644 --- a/src/components/ProfileView.tsx +++ b/src/components/ProfileView.tsx @@ -24,6 +24,10 @@ interface ProfileViewProps { postsLoading?: boolean; bookmarksLoading?: boolean; highlightsLoading?: boolean; + followerCount?: number; + followingCount?: number; + followerCountLoading?: boolean; + followingCountLoading?: boolean; } export function ProfileView({ @@ -35,6 +39,10 @@ export function ProfileView({ postsLoading = false, bookmarksLoading = false, highlightsLoading = false, + followerCount = 0, + followingCount = 0, + followerCountLoading = false, + followingCountLoading = false, }: ProfileViewProps) { const { toast } = useToast(); const [copied, setCopied] = useState(false); @@ -138,6 +146,26 @@ export function ProfileView({ )} + {/* Follower/Following Stats */} +
+
+ {followingCountLoading ? ( + + ) : ( + {followingCount?.toLocaleString() ?? 0} + )} + Following +
+
+ {followerCountLoading ? ( + + ) : ( + {followerCount?.toLocaleString() ?? 0} + )} + Followers +
+
+ {about && (

{about} diff --git a/src/hooks/useFollowerCount.ts b/src/hooks/useFollowerCount.ts new file mode 100644 index 0000000..92cea33 --- /dev/null +++ b/src/hooks/useFollowerCount.ts @@ -0,0 +1,42 @@ +import { useQuery } from '@tanstack/react-query'; +import { useNostr } from '@nostrify/react'; + +/** + * Hook to get follower count for a pubkey. + * + * Ideally this would use NIP-45 COUNT verb for efficiency: + * `["COUNT", , {"kinds": [3], "#p": []}]` + * + * However, since the Nostrify library doesn't directly expose COUNT, + * we query kind-3 events that reference the pubkey and count them. + * + * Note: This queries up to 5000 events. For users with more followers, + * the count will be capped at 5000 until NIP-45 COUNT is directly supported. + */ +export function useFollowerCount(pubkey: string | undefined) { + const { nostr } = useNostr(); + + return useQuery({ + queryKey: ['follower-count', pubkey], + queryFn: async (c) => { + if (!pubkey) return 0; + + const signal = AbortSignal.any([c.signal, AbortSignal.timeout(3000)]); + + try { + // Query kind-3 events (contact lists) that tag this pubkey + // Using a high limit to approximate a count + const events = await nostr.query( + [{ kinds: [3], '#p': [pubkey], limit: 5000 }], + { signal } + ); + return events.length; + } catch (error) { + console.error('Failed to fetch follower count:', error); + return 0; + } + }, + enabled: !!pubkey, + staleTime: 60000, // Cache for 1 minute + }); +} diff --git a/src/hooks/useFollowingCount.ts b/src/hooks/useFollowingCount.ts new file mode 100644 index 0000000..c2edcb4 --- /dev/null +++ b/src/hooks/useFollowingCount.ts @@ -0,0 +1,38 @@ +import { useQuery } from '@tanstack/react-query'; +import { useNostr } from '@nostrify/react'; + +/** + * Hook to get following count for a pubkey. + * Gets the most recent kind-3 event (contact list) and counts the 'p' tags. + */ +export function useFollowingCount(pubkey: string | undefined) { + const { nostr } = useNostr(); + + return useQuery({ + queryKey: ['following-count', pubkey], + queryFn: async (c) => { + if (!pubkey) return 0; + + const signal = AbortSignal.any([c.signal, AbortSignal.timeout(3000)]); + + try { + // Query the user's most recent kind-3 event (contact list) + const events = await nostr.query( + [{ kinds: [3], authors: [pubkey], limit: 1 }], + { signal } + ); + + if (events.length === 0) return 0; + + // Count the number of 'p' tags (people being followed) + const followingCount = events[0].tags.filter(([tag]) => tag === 'p').length; + return followingCount; + } catch (error) { + console.error('Failed to fetch following count:', error); + return 0; + } + }, + enabled: !!pubkey, + staleTime: 60000, // Cache for 1 minute + }); +} diff --git a/src/hooks/useToggleFollow.ts b/src/hooks/useToggleFollow.ts index c54f09d..2da00c6 100644 --- a/src/hooks/useToggleFollow.ts +++ b/src/hooks/useToggleFollow.ts @@ -57,10 +57,16 @@ export function useToggleFollow() { return { event, isFollowing: !isFollowing }; }, - onSuccess: () => { + onSuccess: (_, { pubkey: targetPubkey }) => { // Invalidate following queries to refetch queryClient.invalidateQueries({ queryKey: ['following'] }); queryClient.invalidateQueries({ queryKey: ['following-posts'] }); + // Invalidate the current user's following count + if (user?.pubkey) { + queryClient.invalidateQueries({ queryKey: ['following-count', user.pubkey] }); + } + // Invalidate the target user's follower count + queryClient.invalidateQueries({ queryKey: ['follower-count', targetPubkey] }); }, }); } diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx index a6c3e26..478fd94 100644 --- a/src/pages/ProfilePage.tsx +++ b/src/pages/ProfilePage.tsx @@ -5,6 +5,8 @@ import { useAuthor } from '@/hooks/useAuthor'; import { useAuthorBlogPosts } from '@/hooks/useAuthorBlogPosts'; import { useUserBookmarkedArticles } from '@/hooks/useUserBookmarkedArticles'; import { useUserHighlights } from '@/hooks/useHighlights'; +import { useFollowerCount } from '@/hooks/useFollowerCount'; +import { useFollowingCount } from '@/hooks/useFollowingCount'; import { ProfileView } from '@/components/ProfileView'; import { ProfileSkeleton } from '@/components/ProfileSkeleton'; import NotFound from '@/pages/NotFound'; @@ -39,6 +41,8 @@ export default function ProfilePage() { const { data: posts, isLoading: postsLoading } = useAuthorBlogPosts(pubkey); const { data: bookmarkedArticles, isLoading: bookmarksLoading } = useUserBookmarkedArticles(pubkey); const { data: highlights, isLoading: highlightsLoading } = useUserHighlights(pubkey); + const { data: followerCount, isLoading: followerCountLoading } = useFollowerCount(pubkey); + const { data: followingCount, isLoading: followingCountLoading } = useFollowingCount(pubkey); const metadata = author.data?.metadata; const displayName = metadata?.display_name || metadata?.name || genUserName(pubkey); @@ -104,6 +108,10 @@ export default function ProfilePage() { postsLoading={postsLoading} bookmarksLoading={bookmarksLoading} highlightsLoading={highlightsLoading} + followerCount={followerCount} + followingCount={followingCount} + followerCountLoading={followerCountLoading} + followingCountLoading={followingCountLoading} /> ); }