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 (
-
- );
+/** 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.name}
-
- {activity.details && (
-
{activity.details}
- )}
- {activity.state && (
-
{activity.state}
- )}
- {activity.timestamps?.start && (
-
- {formatElapsed(activity.timestamps.start)} elapsed
-
- )}
-
- {activity.assets?.smallImage && (
-

- )}
-
-
- );
-}
-
-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
+
+ )}
+ >
+ );
}