Skip to content

Commit 37eb8cf

Browse files
Stabilize timeline item renders
Keep shift-click range data behind a stable getter and memoize the timeline row shell so sidebar panel updates do not recreate every item action.
1 parent adb4bd0 commit 37eb8cf

2 files changed

Lines changed: 81 additions & 42 deletions

File tree

apps/desktop/src/sidebar/timeline/index.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,9 @@ export const TimelineView = memo(function TimelineView({
160160
}
161161
return keys;
162162
}, [buckets]);
163+
const flatItemKeysRef = useRef(flatItemKeys);
164+
flatItemKeysRef.current = flatItemKeys;
165+
const getFlatItemKeys = useCallback(() => flatItemKeysRef.current, []);
163166
const flatSessionItemKeys = useMemo(
164167
() => flatItemKeys.filter(isSessionItemKey),
165168
[flatItemKeys],
@@ -516,7 +519,7 @@ export const TimelineView = memo(function TimelineView({
516519
suppressCurrentTimeIndicator={hasActiveVisibleSession}
517520
timezone={timezone}
518521
selectedIds={selectedIds}
519-
flatItemKeys={flatItemKeys}
522+
getFlatItemKeys={getFlatItemKeys}
520523
upcomingItemKey={upcomingMeetingStatus?.itemKey}
521524
upcomingItemLabel={upcomingMeetingStatus?.label}
522525
upcomingItemNodeRef={setUpcomingMeetingNodeRef}
@@ -534,7 +537,7 @@ export const TimelineView = memo(function TimelineView({
534537
selected={selected}
535538
timezone={timezone}
536539
multiSelected={selectedIds.includes(itemKey)}
537-
flatItemKeys={flatItemKeys}
540+
getFlatItemKeys={getFlatItemKeys}
538541
selectedNodeRef={
539542
selected ? scrollSelectedSessionIntoView : undefined
540543
}
@@ -852,7 +855,7 @@ function TodayBucket({
852855
suppressCurrentTimeIndicator,
853856
timezone,
854857
selectedIds,
855-
flatItemKeys,
858+
getFlatItemKeys,
856859
upcomingItemKey,
857860
upcomingItemLabel,
858861
upcomingItemNodeRef,
@@ -865,7 +868,7 @@ function TodayBucket({
865868
suppressCurrentTimeIndicator: boolean;
866869
timezone?: string;
867870
selectedIds: string[];
868-
flatItemKeys: string[];
871+
getFlatItemKeys: () => string[];
869872
upcomingItemKey?: string;
870873
upcomingItemLabel?: string;
871874
upcomingItemNodeRef: RefCallback<HTMLDivElement>;
@@ -939,7 +942,7 @@ function TodayBucket({
939942
selected={selected}
940943
timezone={timezone}
941944
multiSelected={selectedIds.includes(itemKey)}
942-
flatItemKeys={flatItemKeys}
945+
getFlatItemKeys={getFlatItemKeys}
943946
selectedNodeRef={selected ? selectedNodeRef : undefined}
944947
itemNodeRef={
945948
itemKey === upcomingItemKey ? upcomingItemNodeRef : undefined
@@ -1009,7 +1012,7 @@ function TodayBucket({
10091012
suppressCurrentTimeIndicator,
10101013
timezone,
10111014
selectedIds,
1012-
flatItemKeys,
1015+
getFlatItemKeys,
10131016
upcomingItemKey,
10141017
upcomingItemLabel,
10151018
upcomingItemNodeRef,

apps/desktop/src/sidebar/timeline/item.tsx

Lines changed: 72 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,33 @@ import { useTabs } from "~/store/zustand/tabs";
3737
import { useTimelineSelection } from "~/store/zustand/timeline-selection";
3838
import { useListener } from "~/stt/contexts";
3939

40+
const EMPTY_TIMELINE_ITEM_KEYS: string[] = [];
41+
42+
type ItemBaseProps = {
43+
title: string;
44+
displayTime: string;
45+
isLive?: boolean;
46+
amplitude?: number;
47+
showSpinner?: boolean;
48+
selected: boolean;
49+
ignored?: boolean;
50+
muted?: boolean;
51+
multiSelected: boolean;
52+
onClick: () => void;
53+
onDoubleClick?: () => void;
54+
onCmdClick: () => void;
55+
onShiftClick: () => void;
56+
onStop?: () => void;
57+
onDragStart?: (event: DragEvent<HTMLElement>) => void;
58+
contextMenu: MenuItemDef[];
59+
draggable?: boolean;
60+
selectedNodeRef?: RefCallback<HTMLDivElement>;
61+
itemNodeRef?: RefCallback<HTMLDivElement>;
62+
timelineSessionId?: string;
63+
isUpcoming?: boolean;
64+
upcomingLabel?: string;
65+
};
66+
4067
export const TimelineItemComponent = memo(
4168
({
4269
item,
@@ -45,6 +72,7 @@ export const TimelineItemComponent = memo(
4572
timezone,
4673
multiSelected,
4774
flatItemKeys,
75+
getFlatItemKeys,
4876
selectedNodeRef,
4977
itemNodeRef,
5078
isUpcoming,
@@ -55,12 +83,16 @@ export const TimelineItemComponent = memo(
5583
selected: boolean;
5684
timezone?: string;
5785
multiSelected: boolean;
58-
flatItemKeys: string[];
86+
flatItemKeys?: string[];
87+
getFlatItemKeys?: () => string[];
5988
selectedNodeRef?: RefCallback<HTMLDivElement>;
6089
itemNodeRef?: RefCallback<HTMLDivElement>;
6190
isUpcoming?: boolean;
6291
upcomingLabel?: string;
6392
}) => {
93+
const readFlatItemKeys =
94+
getFlatItemKeys ?? (() => flatItemKeys ?? EMPTY_TIMELINE_ITEM_KEYS);
95+
6496
if (item.type === "event") {
6597
return (
6698
<EventItem
@@ -69,7 +101,7 @@ export const TimelineItemComponent = memo(
69101
selected={selected}
70102
timezone={timezone}
71103
multiSelected={multiSelected}
72-
flatItemKeys={flatItemKeys}
104+
getFlatItemKeys={readFlatItemKeys}
73105
selectedNodeRef={selectedNodeRef}
74106
itemNodeRef={itemNodeRef}
75107
isUpcoming={isUpcoming}
@@ -84,7 +116,7 @@ export const TimelineItemComponent = memo(
84116
selected={selected}
85117
timezone={timezone}
86118
multiSelected={multiSelected}
87-
flatItemKeys={flatItemKeys}
119+
getFlatItemKeys={readFlatItemKeys}
88120
selectedNodeRef={selectedNodeRef}
89121
itemNodeRef={itemNodeRef}
90122
isUpcoming={isUpcoming}
@@ -94,7 +126,7 @@ export const TimelineItemComponent = memo(
94126
},
95127
);
96128

97-
function ItemBase({
129+
const ItemBase = memo(function ItemBase({
98130
title,
99131
displayTime,
100132
isLive,
@@ -117,30 +149,7 @@ function ItemBase({
117149
timelineSessionId,
118150
isUpcoming,
119151
upcomingLabel,
120-
}: {
121-
title: string;
122-
displayTime: string;
123-
isLive?: boolean;
124-
amplitude?: number;
125-
showSpinner?: boolean;
126-
selected: boolean;
127-
ignored?: boolean;
128-
muted?: boolean;
129-
multiSelected: boolean;
130-
onClick: () => void;
131-
onDoubleClick?: () => void;
132-
onCmdClick: () => void;
133-
onShiftClick: () => void;
134-
onStop?: () => void;
135-
onDragStart?: (event: DragEvent<HTMLElement>) => void;
136-
contextMenu: MenuItemDef[];
137-
draggable?: boolean;
138-
selectedNodeRef?: RefCallback<HTMLDivElement>;
139-
itemNodeRef?: RefCallback<HTMLDivElement>;
140-
timelineSessionId?: string;
141-
isUpcoming?: boolean;
142-
upcomingLabel?: string;
143-
}) {
152+
}: ItemBaseProps) {
144153
const { t } = useLingui();
145154
const hasSelection = useTimelineSelection((s) => s.selectedIds.length > 0);
146155
const showLiveStop = isLive && onStop;
@@ -271,6 +280,33 @@ function ItemBase({
271280
) : null}
272281
</div>
273282
);
283+
}, itemBasePropsAreEqual);
284+
285+
function itemBasePropsAreEqual(prev: ItemBaseProps, next: ItemBaseProps) {
286+
return (
287+
prev.title === next.title &&
288+
prev.displayTime === next.displayTime &&
289+
prev.isLive === next.isLive &&
290+
prev.amplitude === next.amplitude &&
291+
prev.showSpinner === next.showSpinner &&
292+
prev.selected === next.selected &&
293+
prev.ignored === next.ignored &&
294+
prev.muted === next.muted &&
295+
prev.multiSelected === next.multiSelected &&
296+
prev.onClick === next.onClick &&
297+
prev.onDoubleClick === next.onDoubleClick &&
298+
prev.onCmdClick === next.onCmdClick &&
299+
prev.onShiftClick === next.onShiftClick &&
300+
prev.onStop === next.onStop &&
301+
prev.onDragStart === next.onDragStart &&
302+
prev.contextMenu === next.contextMenu &&
303+
prev.draggable === next.draggable &&
304+
prev.selectedNodeRef === next.selectedNodeRef &&
305+
prev.itemNodeRef === next.itemNodeRef &&
306+
prev.timelineSessionId === next.timelineSessionId &&
307+
prev.isUpcoming === next.isUpcoming &&
308+
prev.upcomingLabel === next.upcomingLabel
309+
);
274310
}
275311

276312
const EventItem = memo(
@@ -280,7 +316,7 @@ const EventItem = memo(
280316
selected,
281317
timezone,
282318
multiSelected,
283-
flatItemKeys,
319+
getFlatItemKeys,
284320
selectedNodeRef,
285321
itemNodeRef,
286322
isUpcoming,
@@ -291,7 +327,7 @@ const EventItem = memo(
291327
selected: boolean;
292328
timezone?: string;
293329
multiSelected: boolean;
294-
flatItemKeys: string[];
330+
getFlatItemKeys: () => string[];
295331
selectedNodeRef?: RefCallback<HTMLDivElement>;
296332
itemNodeRef?: RefCallback<HTMLDivElement>;
297333
isUpcoming?: boolean;
@@ -343,8 +379,8 @@ const EventItem = memo(
343379
}, [itemKey]);
344380

345381
const handleShiftClick = useCallback(() => {
346-
useTimelineSelection.getState().selectRange(flatItemKeys, itemKey);
347-
}, [flatItemKeys, itemKey]);
382+
useTimelineSelection.getState().selectRange(getFlatItemKeys(), itemKey);
383+
}, [getFlatItemKeys, itemKey]);
348384

349385
const handleIgnore = useCallback(() => {
350386
if (!trackingIdEvent) return;
@@ -439,7 +475,7 @@ const SessionItem = memo(
439475
selected,
440476
timezone,
441477
multiSelected,
442-
flatItemKeys,
478+
getFlatItemKeys,
443479
selectedNodeRef,
444480
itemNodeRef,
445481
isUpcoming,
@@ -450,7 +486,7 @@ const SessionItem = memo(
450486
selected: boolean;
451487
timezone?: string;
452488
multiSelected: boolean;
453-
flatItemKeys: string[];
489+
getFlatItemKeys: () => string[];
454490
selectedNodeRef?: RefCallback<HTMLDivElement>;
455491
itemNodeRef?: RefCallback<HTMLDivElement>;
456492
isUpcoming?: boolean;
@@ -512,8 +548,8 @@ const SessionItem = memo(
512548
}, [itemKey]);
513549

514550
const handleShiftClick = useCallback(() => {
515-
useTimelineSelection.getState().selectRange(flatItemKeys, itemKey);
516-
}, [flatItemKeys, itemKey]);
551+
useTimelineSelection.getState().selectRange(getFlatItemKeys(), itemKey);
552+
}, [getFlatItemKeys, itemKey]);
517553

518554
const handleOpenStandaloneWindow = useCallback(() => {
519555
void openStandaloneNoteWindow(sessionId);

0 commit comments

Comments
 (0)