From 4d07aa050d8a666d46e97253a27b374ca12c8be8 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 23 Mar 2026 00:36:28 -0500 Subject: [PATCH] Add Feed and Members tabs to starter pack detail pages Starter pack pages now have two tabs: Feed (default) shows posts from all pack members using useStreamPosts, and Members shows the existing member list with follow/unfollow buttons. --- src/components/FollowPackDetailContent.tsx | 184 +++++++++++++++++---- 1 file changed, 154 insertions(+), 30 deletions(-) diff --git a/src/components/FollowPackDetailContent.tsx b/src/components/FollowPackDetailContent.tsx index 79ebd450..daf8104c 100644 --- a/src/components/FollowPackDetailContent.tsx +++ b/src/components/FollowPackDetailContent.tsx @@ -1,6 +1,6 @@ import { useMemo, useState, useCallback } from 'react'; import { Link, useNavigate } from 'react-router-dom'; -import { Users, UserPlus, Check, Loader2, Copy } from 'lucide-react'; +import { Users, UserPlus, Check, Loader2, Copy, Rss } from 'lucide-react'; import { nip19 } from 'nostr-tools'; import type { NostrEvent, NostrMetadata } from '@nostrify/nostrify'; @@ -9,12 +9,17 @@ import { getAvatarShape } from '@/lib/avatarShape'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; import { Badge } from '@/components/ui/badge'; +import { NoteCard } from '@/components/NoteCard'; +import { TabButton } from '@/components/TabButton'; import { useToast } from '@/hooks/useToast'; import { useAuthor } from '@/hooks/useAuthor'; import { useAuthors } from '@/hooks/useAuthors'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useFollowList, useFollowActions } from '@/hooks/useFollowActions'; import { useNostrPublish } from '@/hooks/useNostrPublish'; +import { useStreamPosts } from '@/hooks/useStreamPosts'; +import { useMuteList } from '@/hooks/useMuteList'; +import { isEventMuted } from '@/lib/muteHelpers'; import { useNostr } from '@nostrify/react'; import { genUserName } from '@/lib/genUserName'; import { VerifiedNip05Text } from '@/components/Nip05Badge'; @@ -30,6 +35,113 @@ function parsePackEvent(event: NostrEvent) { return { title, description, image, pubkeys }; } +type Tab = 'feed' | 'members'; + +// ─── Feed Tab ───────────────────────────────────────────────────────────────── + +function PackFeedTab({ pubkeys }: { pubkeys: string[] }) { + const { muteItems } = useMuteList(); + + const { posts, isLoading } = useStreamPosts('', { + includeReplies: false, + mediaType: 'all', + authorPubkeys: pubkeys, + }); + + const filteredPosts = useMemo(() => { + if (muteItems.length === 0) return posts; + return posts.filter((e) => !isEventMuted(e, muteItems)); + }, [posts, muteItems]); + + if (pubkeys.length === 0) { + return ( +
+ +

No members in this pack yet.

+
+ ); + } + + if (isLoading && filteredPosts.length === 0) { + return ( +
+ {Array.from({ length: 5 }).map((_, i) => ( +
+
+ +
+ + + +
+
+
+ ))} +
+ ); + } + + if (filteredPosts.length === 0) { + return ( +
+ No posts from pack members yet. +
+ ); + } + + return ( +
+ {filteredPosts.map((event) => ( + + ))} +
+ ); +} + +// ─── Members Tab ────────────────────────────────────────────────────────────── + +function PackMembersTab({ + pubkeys, + membersMap, + membersLoading, + followedPubkeys, + currentUserPubkey, +}: { + pubkeys: string[]; + membersMap: Map | undefined; + membersLoading: boolean; + followedPubkeys: Set; + currentUserPubkey: string | undefined; +}) { + if (membersLoading) { + return ( +
+ {Array.from({ length: Math.min(pubkeys.length, 8) }).map((_, i) => ( + + ))} +
+ ); + } + + return ( +
+ {pubkeys.map((pk) => { + const member = membersMap?.get(pk); + const isFollowed = followedPubkeys.has(pk); + return ( + + ); + })} +
+ ); +} + /** * Full detail view for a follow pack / starter pack event. * Shows the member list with individual follow buttons, Follow All, etc. @@ -59,6 +171,7 @@ export function FollowPackDetailContent({ event }: { event: NostrEvent }) { [pubkeys, followedPubkeys], ); + const [activeTab, setActiveTab] = useState('feed'); const [isFollowingAll, setIsFollowingAll] = useState(false); const [copied, setCopied] = useState(false); @@ -220,36 +333,47 @@ export function FollowPackDetailContent({ event }: { event: NostrEvent }) { - {/* Members list */} -
-
-

Members

-
- - {membersLoading ? ( -
- {Array.from({ length: Math.min(pubkeys.length, 8) }).map((_, i) => ( - - ))} -
- ) : ( -
- {pubkeys.map((pk) => { - const member = membersMap?.get(pk); - const isFollowed = followedPubkeys.has(pk); - return ( - - ); - })} -
- )} + {/* Tab bar */} +
+ setActiveTab('feed')} + className="py-2.5" + indicatorClassName="left-1/4 right-1/4 w-auto h-0.5" + > + + + Feed + + + setActiveTab('members')} + className="py-2.5" + indicatorClassName="left-1/4 right-1/4 w-auto h-0.5" + > + + + Members + ({pubkeys.length}) + +
+ + {/* Tab content */} + {activeTab === 'feed' ? ( + + ) : ( + + )}
); }