From b9c2dd54806386329aed64eef1e9590102143ef8 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 20 Apr 2026 00:40:07 +0500 Subject: [PATCH] perf(team): measureElement + suppress remount animation on virtualized rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fifth step of the virtualization plan. Two small, coupled changes that make the virtualized path stable without a merged-ref helper. - Attach `rowVirtualizer.measureElement` to the existing virtualizer wrapper div. Because the wrapper carries no padding or margin, its bounding box matches the inner row, so the observer ref (which stays on the inner AnimatedHeightReveal node) and the measure ref (on the outer wrapper) address the same effective height. No merged ref callback is needed. - Suppress mount-based entry animation inside the virtualized path. The virtualizer mounts and unmounts rows as the user scrolls them in and out; without this, the "new item" fade would replay every time an older row re-entered the viewport. `renderTimelineRow` now takes an optional `suppressEntryAnimation` flag and forwards `isNew=false` to both `LeadThoughtsGroupRow` and `MemoizedMessageRowWithObserver` when set. The direct render path is unchanged. Still dormant in this release — `viewport.virtualizationEnabled` stays false at every call site. PR #6 adds the threshold gate, tests, and opt-in wiring. --- .../team/activity/ActivityTimeline.tsx | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/renderer/components/team/activity/ActivityTimeline.tsx b/src/renderer/components/team/activity/ActivityTimeline.tsx index 479158f0..bc513762 100644 --- a/src/renderer/components/team/activity/ActivityTimeline.tsx +++ b/src/renderer/components/team/activity/ActivityTimeline.tsx @@ -698,7 +698,18 @@ export const ActivityTimeline = React.memo(function ActivityTimeline({ // render path; separators and dividers are their own rows rather than // being bundled into Fragments, which is the contract the virtualizer will // consume in a follow-up PR. - const renderTimelineRow = (row: TimelineRow): React.JSX.Element | null => { + // + // `suppressEntryAnimation` is set when the caller is the virtualized path: + // the virtualizer mounts and unmounts rows as they enter and leave the + // viewport, so relying on mount as a signal of "this item is new" would + // replay the entry animation every time the user scrolls back to an old + // row. In the direct render path the flag stays false and animation still + // runs on real data-set additions. + const renderTimelineRow = ( + row: TimelineRow, + options?: { suppressEntryAnimation?: boolean } + ): React.JSX.Element | null => { + const suppressEntry = options?.suppressEntryAnimation === true; switch (row.kind) { case 'session-separator': return ( @@ -735,7 +746,7 @@ export const ActivityTimeline = React.memo(function ActivityTimeline({ isTeamAlive={pinnedCanBeLive ? isTeamAlive : undefined} leadActivity={pinnedCanBeLive ? leadActivity : undefined} leadContextUpdatedAt={pinnedCanBeLive ? leadContextUpdatedAt : undefined} - isNew={newItemKeys.has(key)} + isNew={!suppressEntry && newItemKeys.has(key)} onVisible={onMessageVisible} observerRoot={observerRoot} zebraShade={zebraShadeSet.has(itemIndex)} @@ -772,7 +783,7 @@ export const ActivityTimeline = React.memo(function ActivityTimeline({ memberColor={renderProps.memberColor} recipientColor={renderProps.recipientColor} isUnread={isUnread} - isNew={newItemKeys.has(key)} + isNew={!suppressEntry && newItemKeys.has(key)} zebraShade={zebraShadeSet.has(itemIndex)} memberColorMap={colorMap} localMemberNames={localMemberNames} @@ -826,6 +837,14 @@ export const ActivityTimeline = React.memo(function ActivityTimeline({ return (
- {renderTimelineRow(row)} + {renderTimelineRow(row, { suppressEntryAnimation: true })}
); })}