diff --git a/packages/web/src/components/chat/FriendsPage.tsx b/packages/web/src/components/chat/FriendsPage.tsx
index 5ce7a8a8..df9cc55f 100644
--- a/packages/web/src/components/chat/FriendsPage.tsx
+++ b/packages/web/src/components/chat/FriendsPage.tsx
@@ -11,8 +11,13 @@ import { LoadingSpinner } from '../ui/LoadingSpinner';
import { getAvatarGradient } from '../../utils/gradients';
import { api } from '../../api/client';
import { Mascot } from '../ui/Mascot';
+import { useActivityStore } from '../../stores/activityStore';
+import { ActivityCard, hasRichActivity, getActivityAccentClass } from '../ui/ActivityCard';
+import { getPrimaryActivity } from '@backspace/shared/src/activities.js';
+import { parseFederatedUsername } from '../../utils/identity';
+import { Username } from '../ui/Username';
-type Tab = 'online' | 'all' | 'pending' | 'add';
+type Tab = 'online' | 'all' | 'pending' | 'add' | 'activity';
interface FriendsPageProps {
mobile?: boolean;
@@ -42,6 +47,9 @@ export function FriendsPage({ mobile }: FriendsPageProps) {
loadRequests();
}, [loadFriends, loadRequests]);
+ const userActivities = useActivityStore((s) => s.userActivities);
+ const pushMobileScreen = useUIStore((s) => s.pushMobileScreen);
+
const onlineFriends = friends.filter(f => f.status !== 'offline');
const pendingIncoming = requests.filter(r => r.status === 'pending' && r.user?.id === r.fromId);
const pendingOutgoing = requests.filter(r => r.status === 'pending' && r.user?.id === r.toId);
@@ -172,6 +180,116 @@ export function FriendsPage({ mobile }: FriendsPageProps) {
onOpenDm={handleOpenDm}
/>
);
+ case 'activity': {
+ const activeFriends: TaggedFriend[] = [];
+ const idleFriends: TaggedFriend[] = [];
+ const offlineActivityFriends: TaggedFriend[] = [];
+ for (const f of friends) {
+ if (f.status === 'offline') {
+ offlineActivityFriends.push(f);
+ continue;
+ }
+ const acts = userActivities.get(f.homeUserId ?? f.id) ?? [];
+ const primary = getPrimaryActivity(acts);
+ if (primary && primary.type !== 'custom') {
+ activeFriends.push(f);
+ } else {
+ idleFriends.push(f);
+ }
+ }
+
+ const renderActivityFriend = (friend: TaggedFriend, 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-3 px-4 py-2.5 rounded-[10px] mb-1 cursor-pointer transition-colors glass-pill border-l-2 ${accentClass}`
+ : 'flex items-center gap-3 px-4 py-2.5 rounded-[4px] hover:bg-interactive-hover cursor-pointer transition-colors active:bg-interactive-hover';
+
+ return (
+
{
+ if (mobile) {
+ pushMobileScreen('user-profile', { userId: friend.id });
+ } else {
+ handleOpenDm(friend.id, friend._instanceOrigin ?? '', friend.homeUserId ?? undefined);
+ }
+ }}
+ className={rowClass}
+ >
+
+
+
+ {domain && !isOffline && (
+
@{domain}
+ )}
+ {!isOffline && (
+
+ )}
+
+
+ );
+ };
+
+ return (
+
+ {activeFriends.length === 0 && idleFriends.length === 0 && offlineActivityFriends.length === 0 ? (
+
+
+
+ It's quiet for now... When friends start an activity, we'll show it here!
+
+
+ ) : (
+ <>
+ {activeFriends.length > 0 && (
+
+
+ Active — {activeFriends.length}
+
+ {activeFriends.map(f => renderActivityFriend(f))}
+
+ )}
+ {idleFriends.length > 0 && (
+
+
+ Online — {idleFriends.length}
+
+ {idleFriends.map(f => renderActivityFriend(f))}
+
+ )}
+ {offlineActivityFriends.length > 0 && (
+
+
+ Offline — {offlineActivityFriends.length}
+
+ {offlineActivityFriends.map(f => renderActivityFriend(f, true))}
+
+ )}
+ >
+ )}
+
+ );
+ }
}
};
@@ -227,7 +345,7 @@ export function FriendsPage({ mobile }: FriendsPageProps) {
{/* Mobile tab bar */}
{mobile && (
- {(['online', 'all', 'pending', 'add'] as Tab[]).map((tab) => (
+ {(['online', 'all', 'pending', 'add', 'activity'] as Tab[]).map((tab) => (