/** * UserProfilePopover -- Click avatar/name to view a lightweight profile card. * Positioned below the trigger by default, flips above if insufficient space. */ import { useState, useRef, useEffect, useCallback, type ReactNode } from 'react' import { useUserLookup } from 'deepspace' import { usePresence } from 'deepspace' interface UserProfilePopoverProps { userId: string children: ReactNode } function formatLastSeen(lastSeenAt: string | undefined): string { if (!lastSeenAt) return 'Never seen' const diff = Date.now() - new Date(lastSeenAt).getTime() const minutes = Math.floor(diff / 60000) if (minutes < 1) return 'Just now' if (minutes < 60) return `${minutes}m ago` const hours = Math.floor(minutes / 60) if (hours < 24) return `${hours}h ago` const days = Math.floor(hours / 24) return `${days}d ago` } const POPOVER_HEIGHT_ESTIMATE = 180 export function UserProfilePopover({ userId, children }: UserProfilePopoverProps) { const { getUser } = useUserLookup() const { isOnline, users } = usePresence() const [open, setOpen] = useState(false) const [placement, setPlacement] = useState<'below' | 'above'>('below') const triggerRef = useRef(null) const popoverRef = useRef(null) const user = getUser(userId) const roomUser = users.find((u) => u.id === userId) const online = isOnline(userId) const close = useCallback(() => setOpen(false), []) const handleToggle = useCallback(() => { setOpen((prev) => { if (!prev && triggerRef.current) { const rect = triggerRef.current.getBoundingClientRect() const spaceBelow = window.innerHeight - rect.bottom const inBottomHalf = rect.top > window.innerHeight / 2 setPlacement(spaceBelow < POPOVER_HEIGHT_ESTIMATE || inBottomHalf ? 'above' : 'below') } return !prev }) }, []) useEffect(() => { if (!open) return function handleClickOutside(e: MouseEvent) { if ( popoverRef.current && !popoverRef.current.contains(e.target as Node) && triggerRef.current && !triggerRef.current.contains(e.target as Node) ) { close() } } function handleEscape(e: KeyboardEvent) { if (e.key === 'Escape') close() } const timer = setTimeout(() => { document.addEventListener('mousedown', handleClickOutside) document.addEventListener('keydown', handleEscape) }, 0) return () => { clearTimeout(timer) document.removeEventListener('mousedown', handleClickOutside) document.removeEventListener('keydown', handleEscape) } }, [open, close]) const positionClass = placement === 'above' ? 'bottom-full mb-2 left-0' : 'top-full mt-2 left-0' return ( {open && user && (
{user.imageUrl ? ( ) : (
{(user.name ?? '?')[0]?.toUpperCase()}
)}

{user.name ?? 'Unknown'}

{roomUser?.role && ( {roomUser.role} )}
{roomUser?.email && (

{roomUser.email}

)}

{online ? 'Online' : `Last seen ${formatLastSeen(roomUser?.lastSeenAt)}`}

)}
) }