'use client' import type { OnlineUser } from '../collaboration/types' import { ChevronDownIcon } from '@heroicons/react/20/solid' import { useEffect, useState } from 'react' import { useReactFlow } from 'reactflow' import Avatar from '@/app/components/base/avatar' import Divider from '@/app/components/base/divider' import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' import Tooltip from '@/app/components/base/tooltip' import { useAppContext } from '@/context/app-context' import { getAvatar } from '@/service/common' import { cn } from '@/utils/classnames' import { useCollaboration } from '../collaboration/hooks/use-collaboration' import { getUserColor } from '../collaboration/utils/user-color' import { useStore } from '../store' const useAvatarUrls = (users: OnlineUser[]) => { const [avatarUrls, setAvatarUrls] = useState>({}) useEffect(() => { const fetchAvatars = async () => { const newAvatarUrls: Record = {} await Promise.all( users.map(async (user) => { if (user.avatar) { try { const response = await getAvatar({ avatar: user.avatar }) newAvatarUrls[user.sid] = response.avatar_url } catch (error) { console.error('Failed to fetch avatar:', error) newAvatarUrls[user.sid] = user.avatar } } }), ) setAvatarUrls(newAvatarUrls) } if (users.length > 0) fetchAvatars() }, [users]) return avatarUrls } const OnlineUsers = () => { const appId = useStore(s => s.appId) const { onlineUsers, cursors, isEnabled: isCollaborationEnabled } = useCollaboration(appId as string) const { userProfile } = useAppContext() const reactFlow = useReactFlow() const [dropdownOpen, setDropdownOpen] = useState(false) const avatarUrls = useAvatarUrls(onlineUsers || []) const currentUserId = userProfile?.id const renderDisplayName = ( user: OnlineUser, baseClassName: string, suffixClassName: string, ) => { const baseName = user.username || 'User' const isCurrentUser = user.user_id === currentUserId return ( {baseName} {isCurrentUser && ( (You) )} ) } // Function to jump to user's cursor position const jumpToUserCursor = (userId: string) => { const cursor = cursors[userId] if (!cursor) return // Convert world coordinates to center the view on the cursor reactFlow.setCenter(cursor.x, cursor.y, { zoom: 1, duration: 800 }) } if (!isCollaborationEnabled || !onlineUsers || onlineUsers.length === 0) return null // Display logic: // 1-3 users: show all avatars // 4+ users: show 2 avatars + count + arrow const shouldShowCount = onlineUsers.length >= 4 const maxVisible = shouldShowCount ? 2 : 3 const visibleUsers = onlineUsers.slice(0, maxVisible) const remainingCount = onlineUsers.length - maxVisible const getAvatarUrl = (user: OnlineUser) => { return avatarUrls[user.sid] || user.avatar } const hasCounter = remainingCount > 0 return ( <>
{visibleUsers.map((user, index) => { const isCurrentUser = user.user_id === currentUserId const userColor = isCurrentUser ? undefined : getUserColor(user.user_id) return (
0 && '-ml-1.5', !isCurrentUser && 'cursor-pointer transition-transform hover:scale-110', )} style={{ zIndex: visibleUsers.length - index }} onClick={() => !isCurrentUser && jumpToUserCursor(user.user_id)} >
) })} {remainingCount > 0 && ( setDropdownOpen(prev => !prev)} asChild >
0 && '-ml-1', )} > + {remainingCount}
{onlineUsers.map((user) => { const isCurrentUser = user.user_id === currentUserId const userColor = isCurrentUser ? undefined : getUserColor(user.user_id) return (
{ if (!isCurrentUser) { jumpToUserCursor(user.user_id) setDropdownOpen(false) } }} >
{renderDisplayName( user, 'system-xs-medium text-text-secondary', 'text-text-tertiary', )}
) })}
)}
) } export default OnlineUsers