online-users-widget.tsx 4.0 KB

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