diff --git a/package-lock.json b/package-lock.json index 7d1efd30..8e0fc7c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -101,7 +101,7 @@ "react-icons": "^5.6.0", "react-redux": "^8.1.2", "react-router-dom": "^6.15.0", - "react-virtuoso": "^4.17.0", + "react-virtuoso": "^4.18.3", "round-robin-js": "^3.0.10", "sass": "^1.93.3", "serialize-error": "^13.0.1", @@ -16975,9 +16975,9 @@ } }, "node_modules/react-virtuoso": { - "version": "4.17.0", - "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.17.0.tgz", - "integrity": "sha512-od3pi2v13v31uzn5zPXC2u3ouISFCVhjFVFch2VvS2Cx7pWA2F1aJa3XhNTN2F07M3lhfnMnsmGeH+7wZICr7w==", + "version": "4.18.3", + "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.18.3.tgz", + "integrity": "sha512-fLz/peHAx4Eu0DLHurFEEI7Y6n5CqEoxBh04rgJM9yMuOJah2a9zWg/MUOmZLcp7zuWYorXq5+5bf3IRgkNvWg==", "license": "MIT", "peerDependencies": { "react": ">=16 || >=17 || >= 18 || >= 19", diff --git a/package.json b/package.json index e32bfff7..676b9622 100644 --- a/package.json +++ b/package.json @@ -139,7 +139,7 @@ "react-icons": "^5.6.0", "react-redux": "^8.1.2", "react-router-dom": "^6.15.0", - "react-virtuoso": "^4.17.0", + "react-virtuoso": "^4.18.3", "round-robin-js": "^3.0.10", "sass": "^1.93.3", "serialize-error": "^13.0.1", diff --git a/src/client/components/playActivity/PlayList.scss b/src/client/components/playActivity/PlayList.scss index 2621f18c..79bf745b 100644 --- a/src/client/components/playActivity/PlayList.scss +++ b/src/client/components/playActivity/PlayList.scss @@ -1,9 +1,9 @@ -[data-testid="virtuoso-item-list"] div[data-index]:first-child div.chakra-collapsible__root { - border-top-left-radius: 0.25rem; - border-top-right-radius: 0.25rem; -} +// [data-testid="virtuoso-item-list"] div[data-index]:first-child div.chakra-collapsible__root { +// border-top-left-radius: 0.25rem; +// border-top-right-radius: 0.25rem; +// } -[data-testid="virtuoso-item-list"] div[data-index]:last-child div.chakra-collapsible__root { - border-bottom-left-radius: 0.25rem; - border-bottom-right-radius: 0.25rem; -} \ No newline at end of file +// [data-testid="virtuoso-item-list"] div[data-index]:last-child div.chakra-collapsible__root { +// border-bottom-left-radius: 0.25rem; +// border-bottom-right-radius: 0.25rem; +// } \ No newline at end of file diff --git a/src/client/components/playActivity/PlayList.tsx b/src/client/components/playActivity/PlayList.tsx index 174a4d83..9b38b435 100644 --- a/src/client/components/playActivity/PlayList.tsx +++ b/src/client/components/playActivity/PlayList.tsx @@ -4,11 +4,10 @@ import { ShortDateDisplay } from '../DateDisplay.js'; import { TextMuted } from '../TextMuted.js'; import { LuChevronRight } from "react-icons/lu" import { capitalize } from '../../../core/StringUtils.js'; -import { ComponentProps, useMemo } from "react" +import { ComponentProps, useMemo, forwardRef, Fragment } from "react" import dayjs, { Dayjs } from 'dayjs'; import doy from 'dayjs/plugin/dayOfYear.js'; import { VscDebugRestart } from "react-icons/vsc"; -import { PlayData, PlayInfoContainer } from '../PlayData.js'; import { GroupedVirtuoso } from 'react-virtuoso' import { ActivityDetails } from '../ActivityDetail.js'; import { sortByNewestPlayDate } from '../../../core/PlayUtils.js'; @@ -19,7 +18,7 @@ dayjs.extend(doy); export interface ActivityLogProps { data: PlayActivity[] sortBy?: 'played' | 'seen' - virtual?: boolean + render?: 'virtCollapse' | 'virtAccordian' | 'accordian' } interface GroupInfo { @@ -27,186 +26,333 @@ interface GroupInfo { date: Dayjs } +interface GroupData { + plays: PlayActivity[] + date: Dayjs +} + +const generateGroupInfo = (data: PlayActivity[]): GroupInfo[] => { + + const groupsReduced = data.reduce((acc: { groups: GroupInfo[], active?: GroupInfo }, curr, index) => { + const date = dayjs(curr.play.data.playDate); + if (acc.active === undefined) { + return { ...acc, active: { count: 1, date } }; + } + if (acc.active.date.dayOfYear() !== date.dayOfYear()) { + return { groups: [...acc.groups, acc.active], active: { count: 1, date } } + } + + return { groups: acc.groups, active: { ...acc.active, count: acc.active.count + 1 } }; + }, { groups: [] }); + + return groupsReduced.groups.concat(groupsReduced.active); +} + +const generateGroupPlays = (data: PlayActivity[]): GroupData[] => { + + const groupsReduced = data.reduce((acc: { groups: GroupData[], active?: GroupData }, curr, index) => { + const date = dayjs(curr.play.data.playDate); + if (acc.active === undefined) { + return { ...acc, active: { plays: [curr], date } }; + } + if (acc.active.date.dayOfYear() !== date.dayOfYear()) { + return { groups: [...acc.groups, acc.active], active: { plays: [curr], date } } + } + + return { groups: acc.groups, active: { ...acc.active, plays: acc.active.plays.concat(curr) } }; + }, { groups: [] }); + + return groupsReduced.groups.concat(groupsReduced.active); +} + export const PlayList = (props: ActivityLogProps) => { const { data = [], sortBy = 'played', - virtual = false, + render = 'accordian' } = props; - if(virtual) { - - - const sorted = useMemo(() => props.data.toSorted((a, b) => sortByNewestPlayDate(a.play, b.play)), [data, sortBy]); - const groupsReduced = useMemo(() => { - return sorted.reduce((acc: { groups: GroupInfo[], active?: GroupInfo }, curr, index) => { - const date = dayjs(curr.play.data.playDate); - if (acc.active === undefined) { - return { ...acc, active: { count: 1, date } }; - } - if (acc.active.date.dayOfYear() !== date.dayOfYear()) { - return { groups: [...acc.groups, acc.active], active: { count: 1, date } } - } - - return { groups: acc.groups, active: { ...acc.active, count: acc.active.count + 1 } }; - }, { groups: [] }); - }, [sorted, sortBy]); - - const allGroups = groupsReduced.groups.concat(groupsReduced.active); + if (render === 'accordian') { + return + } + if (render === 'virtCollapse') { + return + } + if (render === 'virtAccordian') { + return ; + } +} + +const VirtualizedCollapse = (props: { data: PlayActivity[] }) => { + const { + data, + } = props; + const groups = useMemo(() => generateGroupInfo(data), [data]); return ( - x.count)} - groupContent={(index) => { - const gData = allGroups[index]; - let headerText: string; - if (gData.date.isToday()) { - headerText = 'Today'; - } else { - headerText = gData.date.format('MMM DD'); - if (gData.date.year() !== dayjs().year()) { - headerText += `, ${gData.date.year()}`; - } + { + // return
{args.children}
} + // }} + groupCounts={groups.map(x => x.count)} + groupContent={(index) => { + const gData = groups[index]; + let headerText: string; + if (gData.date.isToday()) { + headerText = 'Today'; + } else { + headerText = gData.date.format('MMM DD'); + if (gData.date.year() !== dayjs().year()) { + headerText += `, ${gData.date.year()}`; } - return ( - - + } + return ( + + - {headerText} + {headerText} + + + + + + + + ) + }} + itemContent={(index, groupIndex) => { + const activity = data[index]; + const { play } = activity; + return ( + - - - - - - ) - }} - itemContent={(index, groupIndex) => { - const activity = sorted[index]; - const { play } = activity; - return ( - - - + + + - - - - - {play.data.track} - {play.data.artists.join(' / ')} - - - {play.meta?.source} - - - - - - {activity.status === 'error' ? - - : null} + + + + {play.data.track} + {play.data.artists.join(' / ')} + + + {play.meta?.source} + + + + + {activity.status === 'error' ? + + : null} + - - - - - - ) - }} - /> + + + + + + ) + }} + /> ); +} + +const VirtualizedAccordian = (props: { data: PlayActivity[] }) => { + const { + data, + } = props; + const groups = useMemo(() => generateGroupInfo(data), [data]); + return ( + { + // @ts-ignore + if (args.children.length === 1 && args.children[0].type.name === 'Group') { + // @ts-ignore + return
{args.children}
+ } + // @ts-ignore + return {args.children} + }), + Group: (args) => { + return
{args.children}
+ } + }} + groupCounts={groups.map(x => x.count)} + groupContent={(index) => { + const gData = groups[index]; + let headerText: string; + if (gData.date.isToday()) { + headerText = 'Today'; + } else { + headerText = gData.date.format('MMM DD'); + if (gData.date.year() !== dayjs().year()) { + headerText += `, ${gData.date.year()}`; + } + } + return ( + + + + {headerText} + + + + + + + ) + }} + itemContent={(index, groupIndex) => { + const activity = data[index]; + const { play } = activity; + return ( + + + + + + {play.data.track} + {play.data.artists.join(' / ')} + + + {play.meta?.source} + + + + + + {activity.status === 'error' ? + + : null} + + + + + + + + + + ) + }} + /> + ); } +const PlainAccordian = (props: { data: PlayActivity[] }) => { + const { data = [] } = props; + const groups = generateGroupPlays(data); return ( - - - - Today - - - - - - - - - {props.data.map((activity, index) => { - const { play } = activity; - return ( - - - - - - {play.data.track} - {play.data.artists.join(' / ')} - - - {play.meta?.source} - - - - - - {activity.status === 'error' ? - - : null} - + {groups.map((g) => { + let headerText: string; + if (g.date.isToday()) { + headerText = 'Today'; + } else { + headerText = g.date.format('MMM DD'); + if (g.date.year() !== dayjs().year()) { + headerText += `, ${g.date.year()}`; + } + } + return ( + + + + {headerText} + + + + - - - - - - - ) - })} - + + + + {g.plays.map((activity, index) => { + const { play } = activity; + return ( + + + + + + {play.data.track} + {play.data.artists.join(' / ')} + + + {play.meta?.source} + + + + + + {activity.status === 'error' ? + + : null} + + + + + + + + + + ) + })} + + + ) + })} ); } diff --git a/src/core/tests/utils/fixtures.ts b/src/core/tests/utils/fixtures.ts index 679fe8a9..ca1e405c 100644 --- a/src/core/tests/utils/fixtures.ts +++ b/src/core/tests/utils/fixtures.ts @@ -296,7 +296,7 @@ export const generateLifecycleStep = (play: PlayObject, opts: GenerateLifecycleO return; } somethingModified = true; - if(ctx.key === 'brainz' && Object.keys(x).length === 0) { + if(ctx.key === 'brainz' && Object.keys(x ?? {}).length === 0) { ctx.update(generateBrainz(play, {include: ['album', 'artist', 'track']}), true); } else if (ctx.parent !== undefined && ctx.parent.key === 'brainz') { if (Array.isArray(x)) {