feat: enhance TaskCommentsSection with improved comment rendering and visibility management

- Added state management for visible comments, allowing users to see a limited number of comments for better performance.
- Implemented logic to cap the number of rendered comments, preventing UI freezes with large comment lists.
- Introduced sorting for comments based on creation date to display the most recent comments first.
- Updated the UI to inform users when only a subset of comments is being displayed, enhancing user experience.
This commit is contained in:
iliya 2026-02-28 22:52:18 +02:00
parent c528b07fec
commit ede47903c1

View file

@ -1,4 +1,4 @@
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer'; import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer';
import { ReplyQuoteBlock } from '@renderer/components/team/activity/ReplyQuoteBlock'; import { ReplyQuoteBlock } from '@renderer/components/team/activity/ReplyQuoteBlock';
@ -20,6 +20,9 @@ import type { MentionSuggestion } from '@renderer/types/mention';
import type { ResolvedTeamMember, TaskComment } from '@shared/types'; import type { ResolvedTeamMember, TaskComment } from '@shared/types';
const MAX_COMMENT_LENGTH = 2000; const MAX_COMMENT_LENGTH = 2000;
const INITIAL_VISIBLE_COMMENTS = 30;
const VISIBLE_COMMENTS_STEP = 50;
const MAX_COMMENTS_TO_RENDER = 2000;
interface TaskCommentsSectionProps { interface TaskCommentsSectionProps {
teamName: string; teamName: string;
@ -49,6 +52,13 @@ export const TaskCommentsSection = ({
const [replyTo, setReplyTo] = useState<{ author: string; text: string } | null>(null); const [replyTo, setReplyTo] = useState<{ author: string; text: string } | null>(null);
const [expandedCommentIds, setExpandedCommentIds] = useState<Set<string>>(new Set()); const [expandedCommentIds, setExpandedCommentIds] = useState<Set<string>>(new Set());
const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COMMENTS);
useEffect(() => {
setVisibleCount(INITIAL_VISIBLE_COMMENTS);
setExpandedCommentIds(new Set());
setReplyTo(null);
}, [teamIdKey(teamName, taskId)]);
const toggleCommentExpanded = useCallback((commentId: string) => { const toggleCommentExpanded = useCallback((commentId: string) => {
setExpandedCommentIds((prev) => { setExpandedCommentIds((prev) => {
@ -62,6 +72,24 @@ export const TaskCommentsSection = ({
const draft = useDraftPersistence({ key: `taskComment:${teamName}:${taskId}` }); const draft = useDraftPersistence({ key: `taskComment:${teamName}:${taskId}` });
const colorMap = useMemo(() => buildMemberColorMap(members), [members]); const colorMap = useMemo(() => buildMemberColorMap(members), [members]);
const cappedComments = useMemo(() => {
if (comments.length <= MAX_COMMENTS_TO_RENDER) return comments;
// In extreme cases, rendering thousands of markdown blocks can freeze the renderer.
// Keep the UI responsive by showing only the most recent subset.
return comments.slice(-MAX_COMMENTS_TO_RENDER);
}, [comments]);
const sortedComments = useMemo(() => {
const list = [...cappedComments];
list.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
return list;
}, [cappedComments]);
const visibleComments = useMemo(
() => sortedComments.slice(0, Math.min(visibleCount, sortedComments.length)),
[sortedComments, visibleCount]
);
const mentionSuggestions = useMemo<MentionSuggestion[]>( const mentionSuggestions = useMemo<MentionSuggestion[]>(
() => () =>
members.map((m) => ({ members.map((m) => ({
@ -105,9 +133,14 @@ export const TaskCommentsSection = ({
{comments.length > 0 ? ( {comments.length > 0 ? (
<div className="mb-3 space-y-2"> <div className="mb-3 space-y-2">
{[...comments] {comments.length > MAX_COMMENTS_TO_RENDER ? (
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) <div className="rounded-md border border-[var(--color-border)] bg-[var(--color-surface-raised)] px-3 py-2 text-[11px] text-[var(--color-text-muted)]">
.map((comment) => ( Showing the most recent {MAX_COMMENTS_TO_RENDER.toLocaleString()} comments to keep the
UI responsive.
</div>
) : null}
{visibleComments.map((comment) => (
<div key={comment.id} className="group p-2.5"> <div key={comment.id} className="group p-2.5">
<div className="mb-1 flex items-center gap-2 text-[10px] text-[var(--color-text-muted)]"> <div className="mb-1 flex items-center gap-2 text-[10px] text-[var(--color-text-muted)]">
<span <span
@ -185,6 +218,7 @@ export const TaskCommentsSection = ({
maxHeight={ maxHeight={
needsExpandCollapse && !expanded ? collapsedHeight : 'max-h-none' needsExpandCollapse && !expanded ? collapsedHeight : 'max-h-none'
} }
bare
/> />
)} )}
{showCollapsed && ( {showCollapsed && (
@ -229,6 +263,20 @@ export const TaskCommentsSection = ({
})()} })()}
</div> </div>
))} ))}
{sortedComments.length > visibleComments.length ? (
<div className="flex items-center justify-center pt-2">
<button
type="button"
className="rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-1.5 text-[11px] text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-raised)] hover:text-[var(--color-text)]"
onClick={() =>
setVisibleCount((v) => Math.min(sortedComments.length, v + VISIBLE_COMMENTS_STEP))
}
>
Show more comments ({visibleComments.length}/{sortedComments.length})
</button>
</div>
) : null}
</div> </div>
) : null} ) : null}
@ -313,3 +361,7 @@ export const TaskCommentsSection = ({
</div> </div>
); );
}; };
function teamIdKey(teamName: string, taskId: string): string {
return `${teamName}::${taskId}`;
}