diff --git a/packages/views/editor/readonly-content.tsx b/packages/views/editor/readonly-content.tsx index 1a28592aeb..de46a73738 100644 --- a/packages/views/editor/readonly-content.tsx +++ b/packages/views/editor/readonly-content.tsx @@ -16,7 +16,7 @@ * - Rendering mentions with the same IssueMentionCard component and .mention class */ -import { isValidElement, useEffect, useId, useMemo, useRef, useState } from "react"; +import { isValidElement, memo, useEffect, useId, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; import ReactMarkdown, { defaultUrlTransform, @@ -573,7 +573,7 @@ interface ReadonlyContentProps { className?: string; } -export function ReadonlyContent({ content, className }: ReadonlyContentProps) { +export const ReadonlyContent = memo(function ReadonlyContent({ content, className }: ReadonlyContentProps) { const processed = useMemo(() => preprocessMarkdown(content), [content]); const wrapperRef = useRef(null); const hover = useLinkHover(wrapperRef); @@ -591,4 +591,4 @@ export function ReadonlyContent({ content, className }: ReadonlyContentProps) { ); -} +}); diff --git a/packages/views/issues/components/comment-card.tsx b/packages/views/issues/components/comment-card.tsx index 8e086caa80..d8d619d6d9 100644 --- a/packages/views/issues/components/comment-card.tsx +++ b/packages/views/issues/components/comment-card.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useRef, useState } from "react"; +import { memo, useCallback, useRef, useState } from "react"; import { ChevronRight, Copy, Download, FileText, MoreHorizontal, Pencil, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { Card } from "@multica/ui/components/ui/card"; @@ -348,7 +348,7 @@ function CommentRow({ // CommentCard — One Card per thread (parent + all replies flat inside) // --------------------------------------------------------------------------- -function CommentCard({ +const CommentCard = memo(function CommentCard({ issueId, entry, allReplies, @@ -604,6 +604,6 @@ function CommentCard({ ); -} +}); export { CommentCard, type CommentCardProps }; diff --git a/packages/views/issues/components/issue-detail.tsx b/packages/views/issues/components/issue-detail.tsx index 25526aaf7e..77c23fc32e 100644 --- a/packages/views/issues/components/issue-detail.tsx +++ b/packages/views/issues/components/issue-detail.tsx @@ -193,6 +193,8 @@ export function IssueDetail({ issueId, onDelete, onDone, defaultSidebarOpen = tr const scrollContainerRef = useRef(null); const [highlightedId, setHighlightedId] = useState(null); const didHighlightRef = useRef(null); + const [showAllTimeline, setShowAllTimeline] = useState(false); + const INITIAL_VISIBLE_GROUPS = 50; // Issue data from TQ — uses detail query, seeded from list cache if available. // Only seed when description is present; list API omits it, and ContentEditor @@ -276,6 +278,12 @@ export function IssueDetail({ issueId, onDelete, onDone, defaultSidebarOpen = tr const loading = issueLoading; + // When a highlight target is specified, expand the full timeline so the + // comment is guaranteed to be rendered before we attempt to scroll to it. + useEffect(() => { + if (highlightCommentId) setShowAllTimeline(true); + }, [highlightCommentId]); + // Scroll to highlighted comment once timeline loads (fire only once per highlightCommentId) useEffect(() => { if (!highlightCommentId || timeline.length === 0) return; @@ -924,7 +932,12 @@ export function IssueDetail({ issueId, onDelete, onDone, defaultSidebarOpen = tr } } - return groups.map((group) => { + // Truncate: show only the most recent groups initially + const isTruncated = !showAllTimeline && groups.length > INITIAL_VISIBLE_GROUPS; + const hiddenCount = isTruncated ? groups.length - INITIAL_VISIBLE_GROUPS : 0; + const visibleGroups = isTruncated ? groups.slice(-INITIAL_VISIBLE_GROUPS) : groups; + + const renderGroup = (group: typeof groups[number]) => { if (group.type === "comment") { const entry = group.entries[0]!; return ( @@ -990,7 +1003,22 @@ export function IssueDetail({ issueId, onDelete, onDone, defaultSidebarOpen = tr })} ); - }); + }; + + return ( + <> + {isTruncated && ( + + )} + {visibleGroups.map(renderGroup)} + + ); })()}