From 5c6edefc5668a14513070cf4704895180dfcb1bc Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Thu, 2 Jul 2026 21:32:48 +0000 Subject: [PATCH] feat(ui): track virtual play key by uid rather than index Should prevent pop-in and false update-indicator triggering --- .../components/playActivity/ListParts.tsx | 25 ++++++++++++++++++- .../playActivity/VirtualListDynamic.tsx | 4 +-- src/core/DataUtils.ts | 15 ++++++++++- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/client/components/playActivity/ListParts.tsx b/src/client/components/playActivity/ListParts.tsx index 5e2159f2..823ccb4f 100644 --- a/src/client/components/playActivity/ListParts.tsx +++ b/src/client/components/playActivity/ListParts.tsx @@ -7,6 +7,7 @@ import { PlayApiCommon, PlayApiCommonDetailed, SortPlaysByProps } from '../../.. import { QueryPlaysOpts, QueryPlaysOptsJson } from '../../../backend/common/database/drizzle/repositories/PlayRepository.js'; import { VscDebugRestart } from 'react-icons/vsc'; import { sortByNewestDate } from '../../../core/PlayUtils.js'; +import { getAllIndexes } from '../../../core/DataUtils.js'; dayjs.extend(doy); @@ -106,10 +107,32 @@ export const generateGroupPlays = (data: PlayApiCommon[]): GroupData[] => { } export const generateFlatItems = (data: PlayApiCommon[]) => { + // ensure there are no duplicates + // this may happen if a play is "bumped" from one "page" to another, based on offset, + // when new plays are inserted out of order (playedAt) + const allIds: string[] = []; + const dupes: string[] = []; + for(const d of data) { + if(allIds.includes(d.uid)) { + console.warn(`Duplicate ID detected ${d.uid}`); + dupes.push(d.uid); + } else { + allIds.push(d.uid); + } + } + for(const uid of dupes) { + const indexes = getAllIndexes(data, (d) => d.uid === uid); + // keep only the first one since its likely the freshest + const oldIndexes = indexes.slice(1); + for(const old of oldIndexes) { + data.splice(old, 1); + } + } + const groups = generateGroupPlays(data); groups.sort((a, b) => sortByNewestDate(a.date, b.date)); return groups.map((x) => { x.plays.sort((a, b) => sortByNewestDate(a.playedAt, b.playedAt)); - return [{count: x.plays.length, date: x.date, uid: x.date.toISOString()}, ...x.plays]; + return [{count: x.plays.length, date: x.date, uid: `${x.date.toISOString()}-${x.plays.length}`}, ...x.plays]; }).flat(1); } \ No newline at end of file diff --git a/src/client/components/playActivity/VirtualListDynamic.tsx b/src/client/components/playActivity/VirtualListDynamic.tsx index 78d6eef3..0824cc1f 100644 --- a/src/client/components/playActivity/VirtualListDynamic.tsx +++ b/src/client/components/playActivity/VirtualListDynamic.tsx @@ -56,7 +56,7 @@ export const VirtualizedListDynamic = (props: ActivityLogProps & Pick { - return items[index].uid; + return items[index] !== undefined ? items[index].uid : index; },[items]); const parentRef = React.useRef(null) @@ -65,7 +65,7 @@ export const VirtualizedListDynamic = (props: ActivityLogProps & Pick parentRef.current, - //getItemKey: getItemKey, + getItemKey: getItemKey, estimateSize: () => 85, directDomUpdates: true, //directDomUpdatesMode: 'position', diff --git a/src/core/DataUtils.ts b/src/core/DataUtils.ts index 7be7847e..abe39cca 100644 --- a/src/core/DataUtils.ts +++ b/src/core/DataUtils.ts @@ -122,4 +122,17 @@ export const asErrorSerializedObject = (obj: T): U => { } }); return cloned as unknown as U; -}; \ No newline at end of file +}; + +/** + * Get indexes of all elements in array that make the function return true + * + * @see https://stackoverflow.com/a/20798567/1469797 + */ +export const getAllIndexes = (arr: T[], truthyFunc: (val: T) => boolean) => { + var indexes = [], i: number; + for(i = 0; i < arr.length; i++) + if (truthyFunc(arr[i])) + indexes.push(i); + return indexes; +} \ No newline at end of file