online-users-widget.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. import { OnlineUser } from '@/features/ide-react/context/online-users-context'
  2. import {
  3. Dropdown,
  4. DropdownHeader,
  5. DropdownItem,
  6. DropdownMenu,
  7. DropdownToggle,
  8. } from '@/shared/components/dropdown/dropdown-menu'
  9. import OLTooltip from '@/shared/components/ol/ol-tooltip'
  10. import {
  11. getBackgroundColorForUserId,
  12. hslStringToLuminance,
  13. } from '@/shared/utils/colors'
  14. import classNames from 'classnames'
  15. import { useCallback, useMemo } from 'react'
  16. import { useTranslation } from 'react-i18next'
  17. import { Doc } from '@ol-types/doc'
  18. // Should be kept in sync with $max-user-circles-displayed CSS constant
  19. const MAX_USER_CIRCLES_DISPLAYED = 5
  20. // We don't want a +1 circle since we could just show the user instead
  21. const MAX_USERS_WITH_OVERFLOW_VISIBLE = MAX_USER_CIRCLES_DISPLAYED - 1
  22. export const OnlineUsersWidget = ({
  23. onlineUsers,
  24. goToUser,
  25. }: {
  26. onlineUsers: OnlineUser[]
  27. goToUser: (user: OnlineUser) => Promise<Doc | undefined>
  28. }) => {
  29. const hasOverflow = onlineUsers.length > MAX_USER_CIRCLES_DISPLAYED
  30. const usersBeforeOverflow = useMemo(
  31. () =>
  32. hasOverflow
  33. ? onlineUsers.slice(0, MAX_USERS_WITH_OVERFLOW_VISIBLE)
  34. : onlineUsers,
  35. [onlineUsers, hasOverflow]
  36. )
  37. const usersInOverflow = useMemo(
  38. () =>
  39. hasOverflow ? onlineUsers.slice(MAX_USERS_WITH_OVERFLOW_VISIBLE) : [],
  40. [onlineUsers, hasOverflow]
  41. )
  42. return (
  43. <div className="online-users-row">
  44. {usersBeforeOverflow.map((user, index) => (
  45. <OnlineUserWidget
  46. key={`${user.user_id}_${index}`}
  47. user={user}
  48. goToUser={goToUser}
  49. id={`online-user-${user.user_id}_${index}`}
  50. />
  51. ))}
  52. {hasOverflow && (
  53. <OnlineUserOverflow goToUser={goToUser} users={usersInOverflow} />
  54. )}
  55. </div>
  56. )
  57. }
  58. const OnlineUserWidget = ({
  59. user,
  60. goToUser,
  61. id,
  62. }: {
  63. user: OnlineUser
  64. goToUser: (user: OnlineUser) => void
  65. id: string
  66. }) => {
  67. const onClick = useCallback(() => {
  68. goToUser(user)
  69. }, [goToUser, user])
  70. return (
  71. <OLTooltip
  72. id={id}
  73. description={user.name}
  74. overlayProps={{
  75. placement: 'bottom',
  76. trigger: ['hover', 'focus'],
  77. delay: 0,
  78. }}
  79. >
  80. <button className="online-users-row-button" onClick={onClick}>
  81. <OnlineUserCircle user={user} />
  82. </button>
  83. </OLTooltip>
  84. )
  85. }
  86. const OnlineUserCircle = ({ user }: { user: OnlineUser }) => {
  87. const backgroundColor = getBackgroundColorForUserId(user.user_id)
  88. const luminance = hslStringToLuminance(backgroundColor)
  89. const [character] = [...user.name]
  90. return (
  91. <span
  92. className={classNames('online-user-circle', {
  93. 'online-user-circle-light-font': luminance < 0.5,
  94. 'online-user-circle-dark-font': luminance >= 0.5,
  95. })}
  96. style={{ backgroundColor }}
  97. >
  98. {character}
  99. </span>
  100. )
  101. }
  102. const OnlineUserOverflow = ({
  103. goToUser,
  104. users,
  105. }: {
  106. goToUser: (user: OnlineUser) => void
  107. users: OnlineUser[]
  108. }) => {
  109. const { t } = useTranslation()
  110. return (
  111. <Dropdown align="end">
  112. <DropdownToggle className="online-users-row-button online-user-overflow-toggle">
  113. <OLTooltip
  114. id="connected-users"
  115. description={t('n_more_collaborators', { count: users.length })}
  116. overlayProps={{ placement: 'bottom' }}
  117. >
  118. <span className="online-user-circle">+{users.length}</span>
  119. </OLTooltip>
  120. </DropdownToggle>
  121. <DropdownMenu className="online-user-overflow-dropdown">
  122. <DropdownHeader aria-hidden="true">
  123. {t('connected_users')}
  124. </DropdownHeader>
  125. {users.map((user, index) => (
  126. <li role="none" key={`${user.user_id}_${index}`}>
  127. <DropdownItem
  128. as="button"
  129. tabIndex={-1}
  130. onClick={() => goToUser(user)}
  131. >
  132. <OnlineUserCircle user={user} /> {user.name}
  133. </DropdownItem>
  134. </li>
  135. ))}
  136. </DropdownMenu>
  137. </Dropdown>
  138. )
  139. }