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.
This commit is contained in:
iliya 2026-03-07 13:46:31 +02:00
parent 355fe237a6
commit 821c3019be
3 changed files with 69 additions and 65 deletions

View file

@ -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<Set<string>>(new Set<string>());
const isInitializedRef = useRef(false);
const prevVisibleCountRef = useRef(visibleCount);
const colorMap = members ? buildMemberColorMap(members) : new Map<string, string>();
const memberInfo = new Map<string, { role?: string; color?: string }>();
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<string>();
}
const newKeys = new Set<string>();
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);

View file

@ -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<string> {
const knownKeysRef = useRef<Set<string>>(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<string>();
}
const next = new Set<string>();
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;
}

View file

@ -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<string | null>(null);
const knownCommentIdsRef = useRef<Set<string>>(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<string>();
}
const next = new Set<string>();
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<MentionSuggestion[]>(
() =>