From 821c3019be53cc4109177053044e4d83586fa552 Mon Sep 17 00:00:00 2001 From: iliya Date: Sat, 7 Mar 2026 13:46:31 +0200 Subject: [PATCH] feat: refactor ActivityTimeline and TaskCommentsSection to utilize useNewItemKeys hook - Replaced manual tracking of new item keys in ActivityTimeline and TaskCommentsSection with a new custom hook, useNewItemKeys, for improved code clarity and reusability. - Simplified state management related to new items in both components, enhancing maintainability and reducing complexity. - Updated related logic to ensure consistent handling of newly visible items during pagination and resets. --- .../team/activity/ActivityTimeline.tsx | 37 ++---------- .../team/activity/useNewItemKeys.ts | 56 +++++++++++++++++++ .../team/dialogs/TaskCommentsSection.tsx | 41 +++----------- 3 files changed, 69 insertions(+), 65 deletions(-) create mode 100644 src/renderer/components/team/activity/useNewItemKeys.ts diff --git a/src/renderer/components/team/activity/ActivityTimeline.tsx b/src/renderer/components/team/activity/ActivityTimeline.tsx index 48ef0bfd..c546961c 100644 --- a/src/renderer/components/team/activity/ActivityTimeline.tsx +++ b/src/renderer/components/team/activity/ActivityTimeline.tsx @@ -7,6 +7,7 @@ import { AnimatedHeightReveal } from './AnimatedHeightReveal'; import { ActivityItem, isNoiseMessage } from './ActivityItem'; import { findNewestMessageIndex, resolveTimelineCollapseState } from './collapseState'; import { groupTimelineItems, isLeadThought, LeadThoughtsGroupRow } from './LeadThoughtsGroup'; +import { useNewItemKeys } from './useNewItemKeys'; import type { TimelineItem } from './LeadThoughtsGroup'; import type { ActivityCollapseState } from './collapseState'; @@ -145,11 +146,6 @@ export const ActivityTimeline = ({ }: ActivityTimelineProps): React.JSX.Element => { const [visibleCount, setVisibleCount] = useState(MESSAGES_PAGE_SIZE); - // --- New-message animation tracking --- - const knownKeysRef = useRef>(new Set()); - const isInitializedRef = useRef(false); - const prevVisibleCountRef = useRef(visibleCount); - const colorMap = members ? buildMemberColorMap(members) : new Map(); const memberInfo = new Map(); if (members) { @@ -243,32 +239,11 @@ export const ActivityTimeline = ({ return timelineItems.map(getItemKey); }, [timelineItems]); - const isPaginationExpansion = - isInitializedRef.current && visibleCount > prevVisibleCountRef.current; - - const newItemKeys = useMemo(() => { - if (!isInitializedRef.current || isPaginationExpansion) { - return new Set(); - } - - const newKeys = new Set(); - for (const key of timelineItemKeys) { - if (!knownKeysRef.current.has(key)) { - newKeys.add(key); - } - } - return newKeys; - }, [isPaginationExpansion, timelineItemKeys]); - - useEffect(() => { - if (!isInitializedRef.current) { - isInitializedRef.current = true; - } - for (const key of timelineItemKeys) { - knownKeysRef.current.add(key); - } - prevVisibleCountRef.current = visibleCount; - }, [timelineItemKeys, visibleCount]); + const newItemKeys = useNewItemKeys({ + itemKeys: timelineItemKeys, + paginationKey: visibleCount, + resetKey: teamName, + }); const handleShowMore = (): void => { setVisibleCount((prev) => prev + MESSAGES_PAGE_SIZE); diff --git a/src/renderer/components/team/activity/useNewItemKeys.ts b/src/renderer/components/team/activity/useNewItemKeys.ts new file mode 100644 index 00000000..51aa9a0d --- /dev/null +++ b/src/renderer/components/team/activity/useNewItemKeys.ts @@ -0,0 +1,56 @@ +import { useEffect, useMemo, useRef } from 'react'; + +interface UseNewItemKeysOptions { + itemKeys: string[]; + paginationKey?: number; + resetKey?: string; +} + +/** + * Tracks which currently visible items are newly mounted since the last committed render. + * Pagination expansions are treated as non-animated so "Show more" does not replay enter motion. + */ +export function useNewItemKeys({ + itemKeys, + paginationKey = 0, + resetKey, +}: UseNewItemKeysOptions): Set { + const knownKeysRef = useRef>(new Set()); + const isInitializedRef = useRef(false); + const prevPaginationKeyRef = useRef(paginationKey); + + useEffect(() => { + knownKeysRef.current = new Set(); + isInitializedRef.current = false; + prevPaginationKeyRef.current = paginationKey; + }, [resetKey]); + + const isPaginationExpansion = + isInitializedRef.current && paginationKey > prevPaginationKeyRef.current; + + const newItemKeys = useMemo(() => { + if (!isInitializedRef.current || isPaginationExpansion) { + return new Set(); + } + + const next = new Set(); + for (const key of itemKeys) { + if (!knownKeysRef.current.has(key)) { + next.add(key); + } + } + return next; + }, [isPaginationExpansion, itemKeys]); + + useEffect(() => { + if (!isInitializedRef.current) { + isInitializedRef.current = true; + } + for (const key of itemKeys) { + knownKeysRef.current.add(key); + } + prevPaginationKeyRef.current = paginationKey; + }, [itemKeys, paginationKey]); + + return newItemKeys; +} diff --git a/src/renderer/components/team/dialogs/TaskCommentsSection.tsx b/src/renderer/components/team/dialogs/TaskCommentsSection.tsx index 9814c692..411e6ffe 100644 --- a/src/renderer/components/team/dialogs/TaskCommentsSection.tsx +++ b/src/renderer/components/team/dialogs/TaskCommentsSection.tsx @@ -1,9 +1,10 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { CopyButton } from '@renderer/components/common/CopyButton'; import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer'; import { AnimatedHeightReveal } from '@renderer/components/team/activity/AnimatedHeightReveal'; import { ReplyQuoteBlock } from '@renderer/components/team/activity/ReplyQuoteBlock'; +import { useNewItemKeys } from '@renderer/components/team/activity/useNewItemKeys'; import { ImageLightbox } from '@renderer/components/team/attachments/ImageLightbox'; import { MemberBadge } from '@renderer/components/team/MemberBadge'; import { ExpandableContent } from '@renderer/components/ui/ExpandableContent'; @@ -90,9 +91,6 @@ export const TaskCommentsSection = ({ const [replyTo, setReplyTo] = useState<{ author: string; text: string } | null>(null); const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COMMENTS); const [previewImageUrl, setPreviewImageUrl] = useState(null); - const knownCommentIdsRef = useRef>(new Set()); - const isInitializedRef = useRef(false); - const prevVisibleCountRef = useRef(INITIAL_VISIBLE_COMMENTS); // Reset local UI state when team/task changes. useEffect(() => { @@ -100,9 +98,6 @@ export const TaskCommentsSection = ({ setVisibleCount(INITIAL_VISIBLE_COMMENTS); setReplyTo(null); setPreviewImageUrl(null); - knownCommentIdsRef.current = new Set(); - isInitializedRef.current = false; - prevVisibleCountRef.current = INITIAL_VISIBLE_COMMENTS; }, [teamName, taskId]); const draft = useDraftPersistence({ key: `taskComment:${teamName}:${taskId}` }); @@ -130,33 +125,11 @@ export const TaskCommentsSection = ({ () => visibleComments.map((comment) => comment.id), [visibleComments] ); - - const isPaginationExpansion = - isInitializedRef.current && visibleCount > prevVisibleCountRef.current; - - const newCommentIds = useMemo(() => { - if (!isInitializedRef.current || isPaginationExpansion) { - return new Set(); - } - - const next = new Set(); - for (const id of visibleCommentIds) { - if (!knownCommentIdsRef.current.has(id)) { - next.add(id); - } - } - return next; - }, [isPaginationExpansion, visibleCommentIds]); - - useEffect(() => { - if (!isInitializedRef.current) { - isInitializedRef.current = true; - } - for (const id of visibleCommentIds) { - knownCommentIdsRef.current.add(id); - } - prevVisibleCountRef.current = visibleCount; - }, [visibleCommentIds, visibleCount]); + const newCommentIds = useNewItemKeys({ + itemKeys: visibleCommentIds, + paginationKey: visibleCount, + resetKey: `${teamName}:${taskId}`, + }); const mentionSuggestions = useMemo( () =>