[READ-ONLY] Mirror of https://github.com/FoxxMD/multi-scrobbler. Scrobble plays from multiple sources to multiple clients docs.multi-scrobbler.app
deezer docker jellyfin koito lastfm listenbrainz maloja mopidy mpris music music-assistant plex scrobble self-hosted spotify subsonic tautulli youtube-music
0

Configure Feed

Select the types of activity you want to include in your feed.

update timelline component

* make activity optional with fallback loading render
* fix storybook default args with constant

FoxxMD (Jun 10, 2026, 7:06 PM UTC) dbd1c81c 2b42b40c

+38 -10
+33 -4
src/client/components/ActivityTimeline.tsx
··· 1 1 import { ComponentProps, useState, Fragment } from "react" 2 - import { Accordion, Timeline, Icon, Span, Stack, Heading, Card, Box, Tabs } from '@chakra-ui/react'; 2 + import { Accordion, Timeline, Icon, Span, Stack, Heading, Card, Box, Tabs, Skeleton, SkeletonCircle, SkeletonText } from '@chakra-ui/react'; 3 3 import { ErrorLike, JsonPlayObject, PlayActivity } from "../../core/Atomic"; 4 4 import { PlayData } from "./PlayData"; 5 5 import { ErrorAlert } from "./ErrorAlert"; ··· 23 23 24 24 25 25 export interface ActivityDetailProps { 26 - activity: PlayApiCommonDetailed 26 + activity?: PlayApiCommonDetailed 27 27 collapsibleOpen?: boolean, 28 28 componentType?: 'source' | 'client' 29 29 } 30 30 31 + const TimelineLoading = () => { 32 + return ( 33 + <Timeline.Root variant="subtle" size="lg"> 34 + <Timeline.Item> 35 + <Timeline.Connector> 36 + <Timeline.Separator /> 37 + <Timeline.Indicator> 38 + <Icon fontSize="lg"> 39 + <SkeletonCircle /> 40 + </Icon> 41 + </Timeline.Indicator> 42 + </Timeline.Connector> 43 + <Timeline.Content> 44 + <Timeline.Title> 45 + <SkeletonText noOfLines={1} /> 46 + </Timeline.Title> 47 + <SkeletonText noOfLines={2} /> 48 + </Timeline.Content> 49 + </Timeline.Item> 50 + </Timeline.Root> 51 + ); 52 + } 53 + 31 54 export const ActivityTimeline = (props: ActivityDetailProps) => { 55 + 56 + if(props.activity === undefined) { 57 + return <TimelineLoading/>; 58 + } 59 + 32 60 const { 33 61 activity:{ 34 62 play, 35 63 input, 36 64 seenAt 37 65 } = {}, 38 - collapsibleOpen 66 + collapsibleOpen, 67 + componentType 39 68 } = props; 40 69 const { 41 70 data: { ··· 88 117 <Timeline.Title> 89 118 <MSCollapsible 90 119 indicator={<Span> 91 - Discovered <Span color="fg.muted">new (Play) activity from</Span> <Span fontWeight="medium">{capitalize(source)}</Span> <Span color="fg.muted">at {shortTodayAwareFormat(dayjs(seenAt))}</Span> 120 + {componentType === 'source' ? 'Discovered' : 'Recieved'} <Span color="fg.muted">new Play from</Span> <Span fontWeight="medium">{capitalize(source)}</Span> <Span color="fg.muted">at {shortTodayAwareFormat(dayjs(seenAt))}</Span> 92 121 </Span>} 93 122 defaultOpen={collapsibleOpen} 94 123 timeline
+5 -6
src/stories/ActivityTimeline.stories.tsx
··· 10 10 import { generatePlayWithLifecycle, playWithLifecycleScrobble } from "../core/tests/utils/fixtures.js"; 11 11 import { asJsonPlayObject } from '../core/PlayMarshalUtils.js'; 12 12 13 - type PropsAndCustomArgs = React.ComponentProps<typeof ActivityTimeline> & { 14 - componentType: 'source' | 'client' 15 - }; 16 13 // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export 17 14 const meta = preview.meta({ 18 15 title: 'Examples/ActivityTimline', ··· 25 22 tags: ['autodocs'], 26 23 // More on argTypes: https://storybook.js.org/docs/api/argtypes 27 24 args: { 28 - activity: generatePlayApiCommonDetailed(), 29 - //componentType: 'source' 25 + //activity: generatePlayApiCommonDetailed(), 26 + componentType: 'source' as const 30 27 }, 31 28 argTypes: { 32 29 componentType: { ··· 43 40 // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#story-args 44 41 }); 45 42 43 + export const NotLoaded = meta.story({ 44 + }); 45 + 46 46 // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args 47 47 export const ActivityTimelineStory = meta.story({ 48 48 loaders: [ ··· 55 55 } 56 56 } 57 57 ))); 58 - 59 58 return { 60 59 activity: generatePlayApiCommonDetailed({ 61 60 playOpts: [{play: scrobbleError}],