diff --git a/packages/web/src/components/layout/ActivityPanel.tsx b/packages/web/src/components/layout/ActivityPanel.tsx index 39a25a92..ccc51165 100644 --- a/packages/web/src/components/layout/ActivityPanel.tsx +++ b/packages/web/src/components/layout/ActivityPanel.tsx @@ -4,7 +4,7 @@ import { useUIStore } from '../../stores/uiStore'; import { useActivityStore } from '../../stores/activityStore'; import { Avatar } from '../ui/Avatar'; import { Username } from '../ui/Username'; -import { ActivityCard } from '../ui/ActivityCard'; +import { ActivityCard, hasRichActivity, getActivityAccentClass } from '../ui/ActivityCard'; import type { Friend } from '@backspace/shared'; import { getPrimaryActivity } from '@backspace/shared/src/activities.js'; import { parseFederatedUsername } from '../../utils/identity'; @@ -73,15 +73,23 @@ export function ActivityPanel() { ); }; - const renderFriend = (friend: Friend, isOffline = false, isCompact = true) => { + const renderFriend = (friend: Friend, isOffline = false) => { const { baseName, domain } = parseFederatedUsername(friend.username); const friendDisplayName = friend.displayName ?? baseName; const activities = userActivities.get(friend.homeUserId ?? friend.id) ?? []; + const isRichActivity = !isOffline && hasRichActivity(activities); + const primary = getPrimaryActivity(activities); + const accentClass = primary ? getActivityAccentClass(primary.type) : ''; + + const rowClass = isRichActivity + ? `flex items-center gap-2.5 px-2.5 py-2 rounded-[10px] mb-1 cursor-pointer transition-colors glass-pill border-l-2 ${accentClass}` + : 'flex items-center gap-2.5 px-2 py-1.5 rounded-[4px] hover:bg-interactive-hover cursor-pointer group transition-colors'; + return (
handleFriendClick(e, friend)} - className="flex items-center gap-2.5 px-2 py-1.5 rounded-[4px] hover:bg-interactive-hover cursor-pointer group transition-colors" + className={rowClass} > )} @@ -128,7 +135,7 @@ export function ActivityPanel() { <> {activeFriends.length > 0 && (
- {activeFriends.map(f => renderFriend(f, false, false))} + {activeFriends.map(f => renderFriend(f))}
)} {onlineFriends.length > 0 && ( diff --git a/packages/web/src/components/layout/MemberSidebar.tsx b/packages/web/src/components/layout/MemberSidebar.tsx index 4db5e4a0..1f506fd7 100644 --- a/packages/web/src/components/layout/MemberSidebar.tsx +++ b/packages/web/src/components/layout/MemberSidebar.tsx @@ -5,7 +5,8 @@ import { useUIStore } from '../../stores/uiStore'; import { useActivityStore } from '../../stores/activityStore'; import { Avatar } from '../ui/Avatar'; import { Username } from '../ui/Username'; -import { ActivityCard } from '../ui/ActivityCard'; +import { ActivityCard, hasRichActivity, getActivityAccentClass } from '../ui/ActivityCard'; +import { getPrimaryActivity } from '@backspace/shared/src/activities.js'; import { parseFederatedUsername } from '../../utils/identity'; /** @@ -102,11 +103,20 @@ export function MemberSidebar() { const { baseName, domain } = parseFederatedUsername(member.user.username); const displayName = member.user.displayName ?? baseName; const colorStyle = isOffline ? undefined : getMemberColor(member); + const activities = userActivities.get(member.userId) ?? []; + const isRichActivity = !isOffline && hasRichActivity(activities); + const primary = getPrimaryActivity(activities); + const accentClass = primary ? getActivityAccentClass(primary.type) : ''; + + const rowClass = isRichActivity + ? `flex items-center gap-2.5 px-2.5 py-2 rounded-[10px] mb-1 cursor-pointer transition-colors glass-pill border-l-2 ${accentClass}` + : 'flex items-center gap-2.5 px-2 py-1.5 rounded-[4px] hover:bg-interactive-hover cursor-pointer group transition-colors'; + return (
handleMemberClick(e, member.user)} - className="flex items-center gap-2.5 px-2 py-1.5 rounded-[4px] hover:bg-interactive-hover cursor-pointer group transition-colors" + className={rowClass} > )} diff --git a/packages/web/src/components/ui/ActivityCard.tsx b/packages/web/src/components/ui/ActivityCard.tsx index e611aec3..a5acd500 100644 --- a/packages/web/src/components/ui/ActivityCard.tsx +++ b/packages/web/src/components/ui/ActivityCard.tsx @@ -3,7 +3,6 @@ import { getPrimaryActivity } from '@backspace/shared/src/activities.js'; interface ActivityCardProps { activities: Activity[]; - compact?: boolean; fallbackCustomStatus?: string | null; } @@ -15,96 +14,28 @@ function formatElapsed(startMs: number): string { return `${minutes}m`; } -function ActivityIcon({ type }: { type: Activity['type'] }) { +/** Returns the accent border color class for an activity type */ +export function getActivityAccentClass(type: Activity['type']): string { switch (type) { - case 'playing': - return PLAYING; - case 'listening': - return LISTENING; - case 'watching': - return WATCHING; - case 'streaming': - return LIVE; - default: - return null; + case 'playing': return 'border-l-accent-mint'; + case 'listening': return 'border-l-accent-sky'; + case 'watching': return 'border-l-accent-lavender'; + case 'streaming': return 'border-l-accent-rose'; + default: return ''; } } -function CompactActivity({ activity }: { activity: Activity }) { - const label = activity.type === 'custom' - ? activity.name - : activity.type === 'playing' - ? `Playing ${activity.name}` - : activity.type === 'listening' - ? activity.details ?? activity.name - : activity.type === 'watching' - ? `Watching ${activity.name}` - : activity.name; - - return ( -
- - {label} -
- ); +/** Returns whether an activity should get the glass card row treatment */ +export function hasRichActivity(activities: Activity[]): boolean { + const primary = getPrimaryActivity(activities); + return !!primary && primary.type !== 'custom'; } -function FullActivity({ activity }: { activity: Activity }) { - if (activity.type === 'custom') { - return ( -
- {activity.name} -
- ); - } - - const typeLabel = activity.type === 'playing' ? 'Playing' - : activity.type === 'listening' ? 'Listening to' - : activity.type === 'watching' ? 'Watching' - : 'Streaming'; - - return ( -
-
- {typeLabel} -
-
- {activity.assets?.largeImage && ( - {activity.assets.largeText - )} -
-
- {activity.name} -
- {activity.details && ( -
{activity.details}
- )} - {activity.state && ( -
{activity.state}
- )} - {activity.timestamps?.start && ( -
- {formatElapsed(activity.timestamps.start)} elapsed -
- )} -
- {activity.assets?.smallImage && ( - {activity.assets.smallText - )} -
-
- ); -} - -export function ActivityCard({ activities, compact = false, fallbackCustomStatus }: ActivityCardProps) { +/** + * Renders activity details (app name + elapsed time). + * The glass card wrapper is applied by the parent row container. + */ +export function ActivityCard({ activities, fallbackCustomStatus }: ActivityCardProps) { const primary = getPrimaryActivity(activities); if (!primary) { @@ -114,9 +45,22 @@ export function ActivityCard({ activities, compact = false, fallbackCustomStatus return null; } - if (compact) { - return ; + // Custom status — plain text, no card treatment + if (primary.type === 'custom') { + return
{primary.name}
; } - return ; + // Rich activity — app name + elapsed (card wrapper is on the parent row) + return ( + <> +
+ {primary.name} +
+ {primary.timestamps?.start && ( +
+ {formatElapsed(primary.timestamps.start)} elapsed +
+ )} + + ); }