import React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useSocialStore, type TaggedFriend, type TaggedFriendRequest } from '../../stores/socialStore'; import { useSpaceStore } from '../../stores/spaceStore'; import { useInstanceStore } from '../../stores/instanceStore'; import { useUIStore } from '../../stores/uiStore'; import { Avatar } from '../ui/Avatar'; import { MemberListToggleButton } from '../layout/MemberListToggleButton'; import { LoadingSpinner } from '../ui/LoadingSpinner'; import { api } from '../../api/client'; type Tab = 'online' | 'all' | 'pending' | 'add'; export function FriendsPage() { const [activeTab, setActiveTab] = useState('online'); const [addUsername, setAddUsername] = useState(''); const [addStatus, setAddStatus] = useState<{ type: 'success' | 'error', message: string } | null>(null); const navigate = useNavigate(); const addDmChannel = useSpaceStore((s) => s.addDmChannel); const { friends, requests, isLoading, loadFriends, loadRequests, sendFriendRequest, updateFriendRequest, cancelFriendRequest, removeFriend } = useSocialStore(); useEffect(() => { loadFriends(); loadRequests(); }, [loadFriends, loadRequests]); 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); const handleAddFriend = async (e: React.FormEvent) => { e.preventDefault(); if (!addUsername.trim()) return; try { await sendFriendRequest(addUsername.trim()); setAddStatus({ type: 'success', message: `Success! Your friend request to ${addUsername} has been sent.` }); setAddUsername(''); } catch (err) { setAddStatus({ type: 'error', message: (err as Error).message }); } }; const handleOpenDm = async (friendId: string, instanceOrigin: string) => { try { // Check if a DM already exists with this user (on any instance) const existing = useSpaceStore.getState().findExistingDmForUser({ id: friendId }); if (existing) { useUIStore.getState().setShowDms(true); navigate(`/channels/@me/${existing.dm.id}`); return; } let client = api; if (instanceOrigin) { const instance = useInstanceStore.getState().instances.find(i => i.origin === instanceOrigin); if (instance?.api) client = instance.api; } const dmChannel = await client.dm.create({ userId: friendId }); addDmChannel(dmChannel, instanceOrigin); navigate(`/channels/@me/${dmChannel.id}`); } catch (err) { console.error('Failed to open DM:', err); } }; const renderTabContent = () => { if (isLoading && friends.length === 0 && requests.length === 0) { return (
); } switch (activeTab) { case 'online': return (

Online — {onlineFriends.length}

{onlineFriends.length === 0 ? (
(e.target as any).style.display='none'} />

No one's around to play with Wumpus.

) : ( onlineFriends.map(friend => ( removeFriend(friend.id)} onDm={() => handleOpenDm(friend.id, friend._instanceOrigin)} /> )) )}
); case 'all': return (

All Friends — {friends.length}

{friends.length === 0 ? (

Wumpus is waiting on friends. You can add them!

) : ( friends.map(friend => ( removeFriend(friend.id)} onDm={() => handleOpenDm(friend.id, friend._instanceOrigin)} /> )) )}
); case 'pending': return (

Pending — {pendingIncoming.length + pendingOutgoing.length}

{[...pendingIncoming, ...pendingOutgoing].length === 0 ? (

There are no pending friend requests. Here's Wumpus for now!

) : ( <> {pendingIncoming.map(req => ( updateFriendRequest(req.id, 'accepted')} onDecline={() => updateFriendRequest(req.id, 'declined')} /> ))} {pendingOutgoing.map(req => ( cancelFriendRequest(req.id)} /> ))} )}
); case 'add': return (

Add Friend

You can add friends with their Backspace username.

setAddUsername(e.target.value)} className="w-full bg-surface-base text-txt-primary px-4 py-3 rounded-lg border border-transparent focus:border-txt-link outline-none transition-all placeholder:text-txt-tertiary/50" />
{addStatus && (
{addStatus.message}
)}
); } }; return (
{/* Header */}
Friends
setActiveTab('online')}>Online setActiveTab('all')}>All setActiveTab('pending')}> Pending {(pendingIncoming.length > 0) && ( {pendingIncoming.length} )}
{renderTabContent()}
); } function TabButton({ children, active, onClick }: { children: React.ReactNode, active: boolean, onClick: () => void }) { return ( ); } function FriendItem({ friend, onRemove, onDm }: { friend: TaggedFriend, onRemove: () => void, onDm: () => void }) { const instanceLabel = friend._instanceOrigin ? (() => { try { return new URL(friend._instanceOrigin).host; } catch { return friend._instanceOrigin; } })() : ''; return (
{friend.displayName ?? friend.username} @{friend.username}
{friend.status} {instanceLabel && ( via {instanceLabel} )}
); } function RequestItem({ request, type, onAccept, onDecline, onCancel }: { request: TaggedFriendRequest; type: 'incoming' | 'outgoing'; onAccept?: () => void; onDecline?: () => void; onCancel?: () => void; }) { const user = request.user; if (!user) return null; const instanceLabel = request._instanceOrigin ? (() => { try { return new URL(request._instanceOrigin).host; } catch { return request._instanceOrigin; } })() : ''; return (
{user.displayName ?? user.username} @{user.username}
{type === 'incoming' ? 'Incoming Friend Request' : 'Outgoing Friend Request'} {instanceLabel && ( via {instanceLabel} )}
{type === 'incoming' ? ( <> ) : ( )}
); }