import React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import ReactMarkdown from 'react-markdown'; import type { User } from '@backspace/shared'; import { Avatar } from '../ui/Avatar'; import { Username } from '../ui/Username'; import { api } from '../../api/client'; import { useSpaceStore } from '../../stores/spaceStore'; import { useUIStore } from '../../stores/uiStore'; import { getAvatarGradient, adjustColor } from '../../utils/gradients'; import { parseFederatedUsername } from '../../utils/identity'; interface UserProfilePopoutProps { user: User; onClose: () => void; position?: { top: number; left: number }; } export function UserProfilePopout({ user, onClose, position }: UserProfilePopoutProps) { const navigate = useNavigate(); const addDmChannel = useSpaceStore((s) => s.addDmChannel); const openModal = useUIStore((s) => s.openModal); const { baseName, domain } = parseFederatedUsername(user.username); const displayName = user.displayName ?? baseName; const [mutualCounts, setMutualCounts] = useState<{ friends: number; spaces: number } | null>(null); useEffect(() => { api.users.getMutuals(user.id) .then((data) => setMutualCounts({ friends: data.mutualFriends.length, spaces: data.mutualSpaces.length })) .catch(() => {}); }, [user.id]); const top = position ? Math.min(Math.max(8, position.top), window.innerHeight - 460) : undefined; const left = position ? Math.min(Math.max(8, position.left), window.innerWidth - 356) : undefined; const handleSendMessage = async () => { try { const existing = useSpaceStore.getState().findExistingDmForUser(user); if (existing) { useUIStore.getState().setShowDms(true); onClose(); navigate(`/channels/@me/${existing.dm.id}`); return; } const channel = await api.dm.create({ userId: user.id }); addDmChannel(channel); useUIStore.getState().setShowDms(true); onClose(); navigate(`/channels/@me/${channel.id}`); } catch (err) { console.error('Failed to create DM channel:', err); } }; const handleViewFullProfile = () => { onClose(); openModal('userProfile', { userId: user.id }); }; // Banner display const bannerSrc = user.banner ? (user.banner.startsWith('http') ? user.banner : api.uploads.url(user.banner)) : null; const bannerFallback = user.accentColor ? `linear-gradient(135deg, ${user.accentColor}, ${adjustColor(user.accentColor, -40)})` : getAvatarGradient(user.homeUserId ?? user.id, displayName).gradient; return (
{/* Banner */}
{/* Body */}
{/* Avatar */}
{/* Name & info */}
{domain ? ( ) : ( @{baseName} )}
{user.customStatus && (
{user.customStatus}
)}
{/* Bio */} {user.bio && ( <>
About Me
{user.bio}
)}
{/* Member since + Mutuals */}
Member Since {new Date(user.createdAt).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}
{mutualCounts && (mutualCounts.friends > 0 || mutualCounts.spaces > 0) && (
{mutualCounts.friends > 0 && ( {mutualCounts.friends} mutual friend{mutualCounts.friends !== 1 ? 's' : ''} )} {mutualCounts.friends > 0 && mutualCounts.spaces > 0 && ( · )} {mutualCounts.spaces > 0 && ( {mutualCounts.spaces} mutual space{mutualCounts.spaces !== 1 ? 's' : ''} )}
)}
{/* Actions */}
); }