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) => (