From 255faefbdc99b6c619c781c50301a84878cce184 Mon Sep 17 00:00:00 2001
From: Jure <44338+hoornet@users.noreply.github.com>
Date: Wed, 25 Mar 2026 10:21:18 +0100
Subject: [PATCH] Add follows view with followers/following tabs and new
follower badges
Followers tab fetches kind 3 events referencing the user, following tab
shows the contact list. Each row has avatar, NIP-05 badge, follow/unfollow
button, and "follows you" indicator. New follower notifications from the
background poller increment a sidebar badge that clears on view open.
---
src/App.tsx | 2 +
src/components/follows/FollowsView.tsx | 191 +++++++++++++++++++++++++
src/components/sidebar/Sidebar.tsx | 5 +-
src/lib/nostr/index.ts | 2 +-
src/lib/nostr/social.ts | 14 ++
src/lib/notificationPoller.ts | 1 +
src/stores/notifications.ts | 7 +
src/stores/ui.ts | 6 +-
8 files changed, 224 insertions(+), 4 deletions(-)
create mode 100644 src/components/follows/FollowsView.tsx
diff --git a/src/App.tsx b/src/App.tsx
index 543ace4..5a00e4e 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -19,6 +19,7 @@ import { NotificationsView } from "./components/notifications/NotificationsView"
import { BookmarkView } from "./components/bookmark/BookmarkView";
import { HashtagFeed } from "./components/feed/HashtagFeed";
import { PodcastsView } from "./components/podcast/PodcastsView";
+import { FollowsView } from "./components/follows/FollowsView";
import { PodcastPlayerBar } from "./components/podcast/PodcastPlayerBar";
import { ToastContainer } from "./components/shared/ToastContainer";
import { DebugPanel } from "./components/shared/DebugPanel";
@@ -123,6 +124,7 @@ function App() {
{currentView === "bookmarks" && }
{currentView === "hashtag" && }
{currentView === "podcasts" && }
+ {currentView === "follows" && }
diff --git a/src/components/follows/FollowsView.tsx b/src/components/follows/FollowsView.tsx
new file mode 100644
index 0000000..0325f2a
--- /dev/null
+++ b/src/components/follows/FollowsView.tsx
@@ -0,0 +1,191 @@
+import { useEffect, useState } from "react";
+import { useUIStore } from "../../stores/ui";
+import { useUserStore } from "../../stores/user";
+import { useNotificationsStore } from "../../stores/notifications";
+import { useProfile } from "../../hooks/useProfile";
+import { useNip05Verified } from "../../hooks/useNip05Verified";
+import { fetchFollowers, ensureConnected } from "../../lib/nostr";
+import { shortenPubkey } from "../../lib/utils";
+
+function FollowRow({
+ pubkey,
+ followsYou,
+}: {
+ pubkey: string;
+ followsYou?: boolean;
+}) {
+ const profile = useProfile(pubkey);
+ const name = profile?.displayName || profile?.name || shortenPubkey(pubkey);
+ const avatar = profile?.picture;
+ const nip05 = profile?.nip05;
+ const verified = useNip05Verified(pubkey, nip05);
+
+ const { follows, follow, unfollow, pubkey: ownPubkey } = useUserStore();
+ const { openProfile } = useUIStore();
+ const isFollowing = follows.includes(pubkey);
+ const isSelf = pubkey === ownPubkey;
+
+ return (
+
+
+
+
+
+
+ {nip05 && (
+
+ {verified === "valid" ? "✓ " : ""}{nip05}
+
+ )}
+ {followsYou && (
+ follows you
+ )}
+
+
+
+ {!isSelf && (
+
+ )}
+
+ );
+}
+
+export function FollowsView() {
+ const { followsTab, setFollowsTab } = useUIStore();
+ const { pubkey, follows } = useUserStore();
+ const { clearNewFollowers } = useNotificationsStore();
+
+ const [followers, setFollowers] = useState([]);
+ const [followersLoading, setFollowersLoading] = useState(false);
+ const [followersError, setFollowersError] = useState(null);
+ const [followersFetched, setFollowersFetched] = useState(false);
+
+ // Clear badge when view opens
+ useEffect(() => {
+ clearNewFollowers();
+ }, []);
+
+ // Fetch followers when tab is selected
+ useEffect(() => {
+ if (followsTab !== "followers" || !pubkey || followersFetched) return;
+ let cancelled = false;
+ setFollowersLoading(true);
+ setFollowersError(null);
+
+ (async () => {
+ try {
+ await ensureConnected();
+ const result = await fetchFollowers(pubkey);
+ if (!cancelled) {
+ setFollowers(result);
+ setFollowersFetched(true);
+ }
+ } catch (err) {
+ if (!cancelled) setFollowersError(`Failed to load followers: ${err}`);
+ } finally {
+ if (!cancelled) setFollowersLoading(false);
+ }
+ })();
+
+ return () => { cancelled = true; };
+ }, [followsTab, pubkey]);
+
+ // Build followers set for "follows you" badge on Following tab
+ const followersSet = new Set(followers);
+
+ const tabs: Array<{ id: "followers" | "following"; label: string; count?: number }> = [
+ { id: "followers", label: "followers", count: followersFetched ? followers.length : undefined },
+ { id: "following", label: "following", count: follows.length },
+ ];
+
+ return (
+
+ {/* Header */}
+
+
follows
+
+ {tabs.map((t) => (
+
+ ))}
+
+
+
+ {/* Content */}
+
+ {followsTab === "followers" && (
+ <>
+ {followersLoading && (
+
+
+
+ Loading followers…
+
+
+ )}
+ {followersError && (
+
{followersError}
+ )}
+ {!followersLoading && !followersError && followers.length === 0 && followersFetched && (
+
No followers found yet.
+ )}
+ {followers.map((pk) => (
+
+ ))}
+ >
+ )}
+
+ {followsTab === "following" && (
+ <>
+ {follows.length === 0 && (
+
Not following anyone yet.
+ )}
+ {follows.map((pk) => (
+
+ ))}
+ >
+ )}
+
+
+ );
+}
diff --git a/src/components/sidebar/Sidebar.tsx b/src/components/sidebar/Sidebar.tsx
index bc7216f..6feab47 100644
--- a/src/components/sidebar/Sidebar.tsx
+++ b/src/components/sidebar/Sidebar.tsx
@@ -16,6 +16,7 @@ const NAV_ITEMS = [
{ id: "bookmarks" as const, label: "bookmarks", icon: "▪" },
{ id: "dm" as const, label: "messages", icon: "✉" },
{ id: "notifications" as const, label: "notifications", icon: "🔔" },
+ { id: "follows" as const, label: "follows", icon: "♺" },
{ id: "zaps" as const, label: "zaps", icon: "⚡" },
{ id: "relays" as const, label: "relays", icon: "⟐" },
{ id: "settings" as const, label: "settings", icon: "⚙" },
@@ -25,7 +26,7 @@ const NAV_ITEMS = [
export function Sidebar() {
const { currentView, setView, sidebarCollapsed, toggleSidebar } = useUIStore();
const { loggedIn } = useUserStore();
- const { unreadCount: notifUnread, dmUnreadCount } = useNotificationsStore();
+ const { unreadCount: notifUnread, dmUnreadCount, newFollowersCount } = useNotificationsStore();
const draftCount = useDraftStore((s) => s.drafts.length);
const bookmarkUnread = useBookmarkStore((s) => s.unreadArticleCount());
@@ -93,7 +94,7 @@ export function Sidebar() {
)}
{NAV_ITEMS.map((item) => {
- const badge = item.id === "dm" ? dmUnreadCount : item.id === "notifications" ? notifUnread : item.id === "bookmarks" ? bookmarkUnread : 0;
+ const badge = item.id === "dm" ? dmUnreadCount : item.id === "notifications" ? notifUnread : item.id === "bookmarks" ? bookmarkUnread : item.id === "follows" ? newFollowersCount : 0;
return (