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}
/>
);
}