Files
Verso/services/web/frontend/js/features/editor-navigation-toolbar/components/online-users-widget.tsx
T
Mathias JakobsenandCopybot 122ef0c130 Merge pull request #31510 from overleaf/mj-online-users-flags
[web] Handle edge cases of initials in online user circles

GitOrigin-RevId: 6ee12b22fd4e89dcae1fabda2ef0e44f08306a2a
2026-02-16 09:06:20 +00:00

148 lines
4.0 KiB
TypeScript

import { OnlineUser } from '@/features/ide-react/context/online-users-context'
import {
Dropdown,
DropdownHeader,
DropdownItem,
DropdownMenu,
DropdownToggle,
} from '@/shared/components/dropdown/dropdown-menu'
import OLTooltip from '@/shared/components/ol/ol-tooltip'
import {
getBackgroundColorForUserId,
hslStringToLuminance,
} from '@/shared/utils/colors'
import classNames from 'classnames'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { Doc } from '@ol-types/doc'
import firstCharacter from '@/shared/utils/first-character'
// Should be kept in sync with $max-user-circles-displayed CSS constant
const MAX_USER_CIRCLES_DISPLAYED = 5
// We don't want a +1 circle since we could just show the user instead
const MAX_USERS_WITH_OVERFLOW_VISIBLE = MAX_USER_CIRCLES_DISPLAYED - 1
export const OnlineUsersWidget = ({
onlineUsers,
goToUser,
}: {
onlineUsers: OnlineUser[]
goToUser: (user: OnlineUser) => Promise<Doc | undefined>
}) => {
const hasOverflow = onlineUsers.length > MAX_USER_CIRCLES_DISPLAYED
const usersBeforeOverflow = useMemo(
() =>
hasOverflow
? onlineUsers.slice(0, MAX_USERS_WITH_OVERFLOW_VISIBLE)
: onlineUsers,
[onlineUsers, hasOverflow]
)
const usersInOverflow = useMemo(
() =>
hasOverflow ? onlineUsers.slice(MAX_USERS_WITH_OVERFLOW_VISIBLE) : [],
[onlineUsers, hasOverflow]
)
return (
<div className="online-users-row">
{usersBeforeOverflow.map((user, index) => (
<OnlineUserWidget
key={`${user.user_id}_${index}`}
user={user}
goToUser={goToUser}
id={`online-user-${user.user_id}_${index}`}
/>
))}
{hasOverflow && (
<OnlineUserOverflow goToUser={goToUser} users={usersInOverflow} />
)}
</div>
)
}
const OnlineUserWidget = ({
user,
goToUser,
id,
}: {
user: OnlineUser
goToUser: (user: OnlineUser) => void
id: string
}) => {
const onClick = useCallback(() => {
goToUser(user)
}, [goToUser, user])
return (
<OLTooltip
id={id}
description={user.name}
overlayProps={{
placement: 'bottom',
trigger: ['hover', 'focus'],
delay: 0,
}}
>
<button className="online-users-row-button" onClick={onClick}>
<OnlineUserCircle user={user} />
</button>
</OLTooltip>
)
}
const OnlineUserCircle = ({ user }: { user: OnlineUser }) => {
const backgroundColor = getBackgroundColorForUserId(user.user_id)
const luminance = hslStringToLuminance(backgroundColor)
const character = firstCharacter(user.name)
return (
<span
className={classNames('online-user-circle', {
'online-user-circle-light-font': luminance < 0.5,
'online-user-circle-dark-font': luminance >= 0.5,
})}
style={{ backgroundColor }}
>
{character}
</span>
)
}
const OnlineUserOverflow = ({
goToUser,
users,
}: {
goToUser: (user: OnlineUser) => void
users: OnlineUser[]
}) => {
const { t } = useTranslation()
return (
<Dropdown align="end">
<DropdownToggle className="online-users-row-button online-user-overflow-toggle">
<OLTooltip
id="connected-users"
description={t('n_more_collaborators', { count: users.length })}
overlayProps={{ placement: 'bottom' }}
>
<span className="online-user-circle">+{users.length}</span>
</OLTooltip>
</DropdownToggle>
<DropdownMenu className="online-user-overflow-dropdown">
<DropdownHeader aria-hidden="true">
{t('connected_users')}
</DropdownHeader>
{users.map((user, index) => (
<li role="none" key={`${user.user_id}_${index}`}>
<DropdownItem
as="button"
tabIndex={-1}
onClick={() => goToUser(user)}
>
<OnlineUserCircle user={user} /> {user.name}
</DropdownItem>
</li>
))}
</DropdownMenu>
</Dropdown>
)
}