DRY up wall and reply rendering with ThreadedReplyList component
Extract shared ThreadedReplyList component and use it in PostDetailPage, ExternalContentPage, and ProfilePage wall tab. Wall comments now also fetch sub-replies via useComments so they render with the same threaded/threadedLast connector-line pairing as replies.
This commit is contained in:
@@ -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 }) => (
|
||||
<div key={reply.id}>
|
||||
<NoteCard event={reply} threaded={!!firstSubReply} />
|
||||
{firstSubReply && (
|
||||
<NoteCard event={firstSubReply} threadedLast />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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() {
|
||||
))}
|
||||
</div>
|
||||
) : orderedReplies.length > 0 ? (
|
||||
orderedReplies.map(({ reply, firstSubReply }) => (
|
||||
<div key={reply.id}>
|
||||
<NoteCard event={reply} threaded={!!firstSubReply} />
|
||||
{firstSubReply && (
|
||||
<NoteCard event={firstSubReply} threadedLast />
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
<ThreadedReplyList replies={orderedReplies} />
|
||||
) : (
|
||||
<div className="py-12 text-center text-muted-foreground text-sm">
|
||||
<MessageSquare className="size-12 mx-auto mb-4 opacity-30" />
|
||||
|
||||
@@ -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 }) {
|
||||
))}
|
||||
</div>
|
||||
) : orderedReplies.length > 0 ? (
|
||||
orderedReplies.map(({ reply, firstSubReply }) => (
|
||||
<div key={reply.id}>
|
||||
<NoteCard event={reply} threaded={!!firstSubReply} />
|
||||
{firstSubReply && (
|
||||
<NoteCard event={firstSubReply} threadedLast />
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
<ThreadedReplyList replies={orderedReplies} />
|
||||
) : !parentEventId ? (
|
||||
<div className="py-12 text-center text-muted-foreground text-sm">
|
||||
No replies yet. Be the first to reply!
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : wallComments.length > 0 ? (
|
||||
) : orderedWallReplies.length > 0 ? (
|
||||
<div>
|
||||
{wallComments.map((comment) => (
|
||||
<NoteCard key={comment.id} event={comment} />
|
||||
))}
|
||||
<ThreadedReplyList replies={orderedWallReplies} />
|
||||
|
||||
{/* Infinite scroll sentinel */}
|
||||
{hasNextWallPage && (
|
||||
|
||||
Reference in New Issue
Block a user