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 (