diff --git a/src/client/components/ActivityDetail.tsx b/src/client/components/ActivityDetail.tsx index 62ecb527..c0315d31 100644 --- a/src/client/components/ActivityDetail.tsx +++ b/src/client/components/ActivityDetail.tsx @@ -1,4 +1,4 @@ -import React, { ComponentProps, useState, Fragment } from "react" +import React, { ComponentProps, useState, Fragment, useEffect } from "react" import { Accordion, For, Span, Stack, Text, Box, AbsoluteCenter, Button, Clipboard, Separator, HStack, Flex, Badge, IconButton, Container, Icon, useAccordionItemContext, Skeleton, SkeletonText, Collapsible, BadgeProps } from '@chakra-ui/react'; import { ComponentType, Second } from "../../core/Atomic"; import { PlayData } from "./PlayData"; @@ -13,14 +13,14 @@ import { baseUrl } from "../utils"; import { ShortDateDisplay } from "./DateDisplay"; import { TextMuted } from "./TextMuted"; import { VscDebugRestart } from "react-icons/vsc"; -import { EphemeralBadge, PlayStateBadge } from "./Badges"; +import { EphemeralElement, PlayStateBadge } from "./Badges"; import { MarkOptional } from "ts-essentials"; import { QueryPlaysOpts, QueryPlaysOptsJson } from "../../backend/common/database/drizzle/repositories/PlayRepository"; import { tanQueries } from "../queries"; import { PaginatedResponse } from "../../backend/common/database/drizzle/repositories/BaseRepository"; import { LuChevronRight } from "react-icons/lu"; import { useSSEContext, useSSEEvent } from "@flamefrontend/sse-runtime-react"; -import { DebugCopy, RetryButton } from "./icons/ChakraIcons"; +import { DebugCopy, InsertedIcon, RetryButton, UpdatedIcon } from "./icons/ChakraIcons"; import dayjs from "dayjs"; type UseActivityQueryOptions = { @@ -65,13 +65,13 @@ export function useActivityQuery( return undefined; } }, - structuralSharing(oldData: PlayApiCommonDetailed, newData: PlayApiCommonDetailed) { - if(oldData !== undefined) { - console.debug(`Merging new data for Activity ${activityUid} in Component ${componentId}`); - return {...newData, isUpdated: true, updatedAt: dayjs().toISOString()}; - } - return newData; - }, + // structuralSharing(oldData: PlayApiCommonDetailed, newData: PlayApiCommonDetailed) { + // if(oldData !== undefined) { + // console.debug(`Merging new data for Activity ${activityUid} in Component ${componentId}`); + // return {...newData, isUpdated: true, updatedAt: dayjs().toISOString()}; + // } + // return newData; + // }, }); const client = useSSEContext(); @@ -93,7 +93,7 @@ export interface ActivityDetailProps { } export interface ActivitySummaryProps extends SortPlaysByProps { - activity: PlayApiCommon & {isNew?: boolean | Second, isUpdated?: boolean | Second, updatedAt?: string} + activity: PlayApiCommon & {isNew?: boolean | Second} componentType: ComponentType } @@ -102,21 +102,30 @@ export const ActivitySummary = (props: ActivitySummaryProps) => { activity: { play, isNew, - isUpdated, updatedAt } = {}, activity, sortBy } = props; + const [updated, setUpdated] = useState<{lastUpdated: string, updated: boolean}>({lastUpdated: updatedAt, updated: false}); + useEffect(() => { + setUpdated((old) => { + if(old.lastUpdated === activity.updatedAt) { + return {lastUpdated: activity.updatedAt, updated: false}; + } + return {lastUpdated: activity.updatedAt, updated: true}; + }); + },[setUpdated, activity]); let ephemeralStatus: React.JSX.Element | undefined; - if(isUpdated || isNew) { - const eph = isUpdated ?? isNew; - ephemeralStatus = {isNew !== undefined ? 'New' : 'Updated'}; + if(isNew) { + ephemeralStatus = ; + } else if(updated.updated) { + ephemeralStatus = ; } return ( - {play.data.track}{ephemeralStatus} + {play.data.track}{ephemeralStatus} {/* */} {play.data.artists.map(x => x.name).join(' / ')} diff --git a/src/client/components/Badges.tsx b/src/client/components/Badges.tsx index 73428703..71399115 100644 --- a/src/client/components/Badges.tsx +++ b/src/client/components/Badges.tsx @@ -38,11 +38,13 @@ export const PlayStateBadge = (props: ComponentProps & { state: Pl const DEFAULT_EXPIRES = 10000; -export const EphemeralBadge = (props: ComponentProps & { expires?: Second | boolean, children: React.ReactNode }) => { +export const NewBadge = (props: ComponentProps) => ; + +export const EphemeralElement = (props: { expires?: Second | boolean, children: React.ReactNode }) => { const { expires = DEFAULT_EXPIRES, - ...rest + children } = props; let expiresTime: Second | undefined; if(expires === true) { @@ -63,7 +65,7 @@ export const EphemeralBadge = (props: ComponentProps & { expires?: }, []); if (shouldShow) { - return {props.children} + return children; } return null; } diff --git a/src/client/components/icons/ChakraIcons.tsx b/src/client/components/icons/ChakraIcons.tsx index dfd22e25..ca9fc721 100644 --- a/src/client/components/icons/ChakraIcons.tsx +++ b/src/client/components/icons/ChakraIcons.tsx @@ -9,6 +9,7 @@ import { LuCheck, LuExternalLink, LuArrowUp, + LuCircleArrowUp, LuArrowDown, LuEllipsis, LuEllipsisVertical, @@ -22,9 +23,10 @@ import { LuRefreshCw } from "react-icons/lu" import { VscDebugRestart } from 'react-icons/vsc'; +import { MdOutlineFiberNew } from "react-icons/md"; import { RiZzzFill } from "react-icons/ri"; import { SiGoogledocs } from "react-icons/si"; -import { IconButton, Clipboard, useClipboard, Spinner } from "@chakra-ui/react" +import { IconButton, Clipboard, useClipboard, Spinner, Icon } from "@chakra-ui/react" import { ComponentProps, PropsWithChildren } from 'react'; import { IconBaseProps, IconType } from "react-icons/lib"; @@ -42,6 +44,7 @@ export const makeIconButton = (Icon: IconType) => (props: PropsWithChildren ); } +export const makeChakraIcon = (IconComponent: IconType) => (props: ComponentProps & { iconProps?: IconBaseProps }) => export const ChevronRight = LuChevronRight; export const ChevronRightButton = (props: ComponentProps) => ( @@ -152,4 +155,8 @@ export const CalendarIcon = LuCalendar; export const CalendarButton = makeIconButton(CalendarIcon); export const RefreshIcon = LuRefreshCw; -export const RefreshButton = makeIconButton(RefreshIcon); \ No newline at end of file +export const RefreshButton = makeIconButton(RefreshIcon); + +export const InsertedIcon = makeChakraIcon(MdOutlineFiberNew); + +export const UpdatedIcon = makeChakraIcon(LuCircleArrowUp); \ No newline at end of file diff --git a/src/core/tests/utils/apiFixtures.ts b/src/core/tests/utils/apiFixtures.ts index 3abd1f7e..48173e54 100644 --- a/src/core/tests/utils/apiFixtures.ts +++ b/src/core/tests/utils/apiFixtures.ts @@ -31,7 +31,7 @@ export const generatePlayApiCommon = (commonData: Partial & {play const { playedAt = typeof play.data.playDate === 'string' ? play.data.playDate : play.data.playDate.toISOString(), seenAt = playedAt, - updatedAt = seenAt, + updatedAt = dayjs().toISOString(), compacted = false, state = randomPlayState(), componentId = faker.number.int({min: 1, max: 10}), diff --git a/src/stories/plays/ActivityList.stories.tsx b/src/stories/plays/ActivityList.stories.tsx index 2981c722..67c09cf0 100644 --- a/src/stories/plays/ActivityList.stories.tsx +++ b/src/stories/plays/ActivityList.stories.tsx @@ -244,9 +244,7 @@ export const ListLiveUpdates = meta.story({ msw: { handlers: [ http.get<{ uid: string }>('/api/components/:componentId/plays', async ({ params, request }) => { - if(livePlayData.length === 0) { - livePlayData = await generatePlayApiCommonDetailedList(); - } + livePlayData = await generatePlayApiCommonDetailedList(); const url = new URL(request.url) console.log(url.search); const query = qs.parse(url.search, qsOptions); @@ -269,22 +267,27 @@ export const ListLiveUpdates = meta.story({ const existingIndex = livePlayData.findIndex(x => x.uid === params.uid); if (existingIndex !== -1) { const existing = livePlayData[existingIndex]; - let newState: PlayState = existing.state; - while(newState === existing.state) { - newState = randomPlayState(); - } - existing.play.data.track = faker.music.songName(); - livePlayData[existingIndex].state = newState; - const updated = {...existing, state: newState}; - return HttpResponse.json(updated); + return HttpResponse.json(existing); } return HttpResponse.json(generatePlayApiCommonDetailed()); }), sse('/api/events?next=true', async ({ params, client }) => { - setInterval(() => client.send({ + setInterval(() => { + + const index = faker.number.int({min: 0, max: 7}); + let newState: PlayState = livePlayData[index].state; + while(newState === livePlayData[index].state) { + newState = randomPlayState(); + } + livePlayData[index].state = newState; + livePlayData[index].play.data.track = faker.music.songName(); + livePlayData[index].updatedAt = dayjs().toISOString(); + + client.send({ //@ts-expect-error event: 'playUpdate', - data: {componentId: 1, data: {uid: livePlayData[faker.number.int({min: 0, max: 7})].uid}}}), 2000); + data: {componentId: 1, data: {uid: livePlayData[index].uid}}}) + }, 2000); }) ], }, @@ -333,7 +336,7 @@ export const ListLiveInsert = meta.story({ http.get<{ uid: string }>('/api/components/:componentId/plays/:uid', async ({ params }) => { const existing = livePlayInsertData.findIndex(x => x.uid === params.uid); if (existing !== undefined) { - return HttpResponse.json(existing); + return HttpResponse.json(livePlayInsertData[existing]); } return HttpResponse.json(generatePlayApiCommonDetailed()); }),