import { useEffect, useState } from 'react'; import type { Activity } from '@backspace/shared'; import { getPrimaryActivity } from '@backspace/shared/src/activities.js'; interface ProfileActivityProps { activities: Activity[]; } const VERB: Record = { playing: 'Playing', listening: 'Listening to', watching: 'Watching', streaming: 'Streaming', custom: '', }; function formatClock(ms: number): string { const total = Math.max(0, Math.floor(ms / 1000)); const minutes = Math.floor(total / 60); const seconds = total % 60; const hours = Math.floor(minutes / 60); if (hours > 0) return `${hours}:${String(minutes % 60).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; return `${minutes}:${String(seconds).padStart(2, '0')}`; } /** * The activity block on the profile card — the "Listening to Spotify" panel. * * Deliberately richer than `ActivityCard` (which renders name + elapsed for * compact list rows): here there is room for the artwork, the track and the * artist, so it reads `details`, `state` and `assets` too. Every one of those * is optional and the block degrades to just the name, which is all today's * process-based detector supplies — the extra fields are what a Spotify * producer would fill in. */ export function ProfileActivity({ activities }: ProfileActivityProps) { const primary = getPrimaryActivity(activities); const start = primary?.timestamps?.start; const end = primary?.timestamps?.end; // Re-render once a second only while there is a clock to advance. const [now, setNow] = useState(() => Date.now()); useEffect(() => { if (!start) return; const id = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(id); }, [start]); if (!primary || primary.type === 'custom') return null; const elapsed = start ? now - start : 0; const duration = start && end ? end - start : 0; const progress = duration > 0 ? Math.min(Math.max(elapsed / duration, 0), 1) : 0; // The server restricts asset images to http(s); this mirrors that so a // record stored before that check cannot inject another scheme. const art = primary.assets?.largeImage; const artSrc = art && (art.startsWith('https://') || art.startsWith('http://')) ? art : null; return (
{VERB[primary.type]} {primary.name}
{artSrc && ( {primary.assets?.largeText )}
{primary.details && (
{primary.details}
)} {primary.state && (
{primary.state}
)} {duration > 0 ? (
{formatClock(elapsed)} {formatClock(duration)}
) : start ? (
{formatClock(elapsed)} elapsed
) : null}
); }