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:
parent
355fe237a6
commit
821c3019be
3 changed files with 69 additions and 65 deletions
|
|
@ -7,6 +7,7 @@ import { AnimatedHeightReveal } from './AnimatedHeightReveal';
|
||||||
import { ActivityItem, isNoiseMessage } from './ActivityItem';
|
import { ActivityItem, isNoiseMessage } from './ActivityItem';
|
||||||
import { findNewestMessageIndex, resolveTimelineCollapseState } from './collapseState';
|
import { findNewestMessageIndex, resolveTimelineCollapseState } from './collapseState';
|
||||||
import { groupTimelineItems, isLeadThought, LeadThoughtsGroupRow } from './LeadThoughtsGroup';
|
import { groupTimelineItems, isLeadThought, LeadThoughtsGroupRow } from './LeadThoughtsGroup';
|
||||||
|
import { useNewItemKeys } from './useNewItemKeys';
|
||||||
|
|
||||||
import type { TimelineItem } from './LeadThoughtsGroup';
|
import type { TimelineItem } from './LeadThoughtsGroup';
|
||||||
import type { ActivityCollapseState } from './collapseState';
|
import type { ActivityCollapseState } from './collapseState';
|
||||||
|
|
@ -145,11 +146,6 @@ export const ActivityTimeline = ({
|
||||||
}: ActivityTimelineProps): React.JSX.Element => {
|
}: ActivityTimelineProps): React.JSX.Element => {
|
||||||
const [visibleCount, setVisibleCount] = useState(MESSAGES_PAGE_SIZE);
|
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 colorMap = members ? buildMemberColorMap(members) : new Map<string, string>();
|
||||||
const memberInfo = new Map<string, { role?: string; color?: string }>();
|
const memberInfo = new Map<string, { role?: string; color?: string }>();
|
||||||
if (members) {
|
if (members) {
|
||||||
|
|
@ -243,32 +239,11 @@ export const ActivityTimeline = ({
|
||||||
return timelineItems.map(getItemKey);
|
return timelineItems.map(getItemKey);
|
||||||
}, [timelineItems]);
|
}, [timelineItems]);
|
||||||
|
|
||||||
const isPaginationExpansion =
|
const newItemKeys = useNewItemKeys({
|
||||||
isInitializedRef.current && visibleCount > prevVisibleCountRef.current;
|
itemKeys: timelineItemKeys,
|
||||||
|
paginationKey: visibleCount,
|
||||||
const newItemKeys = useMemo(() => {
|
resetKey: teamName,
|
||||||
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 handleShowMore = (): void => {
|
const handleShowMore = (): void => {
|
||||||
setVisibleCount((prev) => prev + MESSAGES_PAGE_SIZE);
|
setVisibleCount((prev) => prev + MESSAGES_PAGE_SIZE);
|
||||||
|
|
|
||||||
56
src/renderer/components/team/activity/useNewItemKeys.ts
Normal file
56
src/renderer/components/team/activity/useNewItemKeys.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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 { CopyButton } from '@renderer/components/common/CopyButton';
|
||||||
import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer';
|
import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer';
|
||||||
import { AnimatedHeightReveal } from '@renderer/components/team/activity/AnimatedHeightReveal';
|
import { AnimatedHeightReveal } from '@renderer/components/team/activity/AnimatedHeightReveal';
|
||||||
import { ReplyQuoteBlock } from '@renderer/components/team/activity/ReplyQuoteBlock';
|
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 { ImageLightbox } from '@renderer/components/team/attachments/ImageLightbox';
|
||||||
import { MemberBadge } from '@renderer/components/team/MemberBadge';
|
import { MemberBadge } from '@renderer/components/team/MemberBadge';
|
||||||
import { ExpandableContent } from '@renderer/components/ui/ExpandableContent';
|
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 [replyTo, setReplyTo] = useState<{ author: string; text: string } | null>(null);
|
||||||
const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COMMENTS);
|
const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COMMENTS);
|
||||||
const [previewImageUrl, setPreviewImageUrl] = useState<string | null>(null);
|
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.
|
// Reset local UI state when team/task changes.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -100,9 +98,6 @@ export const TaskCommentsSection = ({
|
||||||
setVisibleCount(INITIAL_VISIBLE_COMMENTS);
|
setVisibleCount(INITIAL_VISIBLE_COMMENTS);
|
||||||
setReplyTo(null);
|
setReplyTo(null);
|
||||||
setPreviewImageUrl(null);
|
setPreviewImageUrl(null);
|
||||||
knownCommentIdsRef.current = new Set();
|
|
||||||
isInitializedRef.current = false;
|
|
||||||
prevVisibleCountRef.current = INITIAL_VISIBLE_COMMENTS;
|
|
||||||
}, [teamName, taskId]);
|
}, [teamName, taskId]);
|
||||||
|
|
||||||
const draft = useDraftPersistence({ key: `taskComment:${teamName}:${taskId}` });
|
const draft = useDraftPersistence({ key: `taskComment:${teamName}:${taskId}` });
|
||||||
|
|
@ -130,33 +125,11 @@ export const TaskCommentsSection = ({
|
||||||
() => visibleComments.map((comment) => comment.id),
|
() => visibleComments.map((comment) => comment.id),
|
||||||
[visibleComments]
|
[visibleComments]
|
||||||
);
|
);
|
||||||
|
const newCommentIds = useNewItemKeys({
|
||||||
const isPaginationExpansion =
|
itemKeys: visibleCommentIds,
|
||||||
isInitializedRef.current && visibleCount > prevVisibleCountRef.current;
|
paginationKey: visibleCount,
|
||||||
|
resetKey: `${teamName}:${taskId}`,
|
||||||
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 mentionSuggestions = useMemo<MentionSuggestion[]>(
|
const mentionSuggestions = useMemo<MentionSuggestion[]>(
|
||||||
() =>
|
() =>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue