diff --git a/src/components/ThreadedReplyList.tsx b/src/components/ThreadedReplyList.tsx new file mode 100644 index 00000000..487a7d88 --- /dev/null +++ b/src/components/ThreadedReplyList.tsx @@ -0,0 +1,31 @@ +import type { NostrEvent } from '@nostrify/nostrify'; +import { NoteCard } from '@/components/NoteCard'; + +export interface ThreadedReply { + reply: NostrEvent; + firstSubReply?: NostrEvent; +} + +interface ThreadedReplyListProps { + replies: ThreadedReply[]; +} + +/** + * Renders a flat list of replies where each top-level reply is optionally + * followed by its first sub-reply, using NoteCard's threaded/threadedLast + * connector-line styling for visual continuity. + */ +export function ThreadedReplyList({ replies }: ThreadedReplyListProps) { + return ( + <> + {replies.map(({ reply, firstSubReply }) => ( +
+ + {firstSubReply && ( + + )} +
+ ))} + + ); +} diff --git a/src/pages/ExternalContentPage.tsx b/src/pages/ExternalContentPage.tsx index df4e4341..4304831c 100644 --- a/src/pages/ExternalContentPage.tsx +++ b/src/pages/ExternalContentPage.tsx @@ -4,7 +4,7 @@ import { ArrowLeft, Globe, Heart, MessageSquare, Repeat2 } from 'lucide-react'; import { Link, useLocation, useParams } from 'react-router-dom'; import { Skeleton } from '@/components/ui/skeleton'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; -import { NoteCard } from '@/components/NoteCard'; +import { ThreadedReplyList } from '@/components/ThreadedReplyList'; import { ComposeBox } from '@/components/ComposeBox'; import { ReplyComposeModal } from '@/components/ReplyComposeModal'; import { QuickReactMenu } from '@/components/QuickReactMenu'; @@ -326,14 +326,7 @@ export function ExternalContentPage() { ))} ) : orderedReplies.length > 0 ? ( - orderedReplies.map(({ reply, firstSubReply }) => ( -
- - {firstSubReply && ( - - )} -
- )) + ) : (
diff --git a/src/pages/PostDetailPage.tsx b/src/pages/PostDetailPage.tsx index d2f132e6..594860fe 100644 --- a/src/pages/PostDetailPage.tsx +++ b/src/pages/PostDetailPage.tsx @@ -15,6 +15,7 @@ import { Input } from '@/components/ui/input'; import { NoteContent } from '@/components/NoteContent'; import { VideoPlayer } from '@/components/VideoPlayer'; import { NoteCard } from '@/components/NoteCard'; +import { ThreadedReplyList } from '@/components/ThreadedReplyList'; import { NoteMoreMenu } from '@/components/NoteMoreMenu'; import { ReplyComposeModal } from '@/components/ReplyComposeModal'; import { ReactionButton } from '@/components/ReactionButton'; @@ -1070,14 +1071,7 @@ function PostDetailContent({ event }: { event: NostrEvent }) { ))}
) : orderedReplies.length > 0 ? ( - orderedReplies.map(({ reply, firstSubReply }) => ( -
- - {firstSubReply && ( - - )} -
- )) + ) : !parentEventId ? (
No replies yet. Be the first to reply! diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx index af96190c..d938a9da 100644 --- a/src/pages/ProfilePage.tsx +++ b/src/pages/ProfilePage.tsx @@ -37,6 +37,8 @@ import type { ProfileTab } from '@/hooks/useProfileFeed'; import { useProfileMedia } from '@/hooks/useProfileMedia'; import { useProfileSupplementary } from '@/hooks/useProfileData'; import { useWallComments } from '@/hooks/useWallComments'; +import { useComments } from '@/hooks/useComments'; +import { ThreadedReplyList } from '@/components/ThreadedReplyList'; import { useNip05Resolve } from '@/hooks/useNip05Resolve'; import { genUserName } from '@/lib/genUserName'; @@ -745,6 +747,9 @@ export function ProfilePage() { }; }, [pubkey, metadataEvent]); + // Fetch all NIP-22 comments on the profile (for sub-reply pairing in the wall tab) + const { data: wallCommentsData } = useComments(wallReplyTarget, 500); + // Wall compose modal state (for FAB on wall tab) const [wallComposeOpen, setWallComposeOpen] = useState(false); @@ -1032,6 +1037,17 @@ export function ProfilePage() { return items; }, [wallData?.pages, muteItems]); + // Pair each wall comment with its first direct sub-reply (same pattern as PostDetailPage replies) + const orderedWallReplies = useMemo(() => { + return wallComments.map((comment) => { + const subReplies = wallCommentsData?.getDirectReplies(comment.id) ?? []; + return { + reply: comment, + firstSubReply: subReplies[0] as NostrEvent | undefined, + }; + }); + }, [wallComments, wallCommentsData]); + const streak = useMemo(() => { if (!feedData?.pages) return 0; const events: NostrEvent[] = []; @@ -1545,11 +1561,9 @@ export function ProfilePage() {
))} - ) : wallComments.length > 0 ? ( + ) : orderedWallReplies.length > 0 ? (
- {wallComments.map((comment) => ( - - ))} + {/* Infinite scroll sentinel */} {hasNextWallPage && (