diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index d8f23bd45f..6e2205aed4 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -1042,6 +1042,8 @@
"my_library": "",
"n_items": "",
"n_items_plural": "",
+ "n_more_collaborators": "",
+ "n_more_collaborators_plural": "",
"n_more_updates_above": "",
"n_more_updates_above_plural": "",
"n_more_updates_below": "",
diff --git a/services/web/frontend/js/features/ide-redesign/components/online-users/online-users-widget.tsx b/services/web/frontend/js/features/ide-redesign/components/online-users/online-users-widget.tsx
new file mode 100644
index 0000000000..bac1787e10
--- /dev/null
+++ b/services/web/frontend/js/features/ide-redesign/components/online-users/online-users-widget.tsx
@@ -0,0 +1,133 @@
+import { OnlineUser } from '@/features/ide-react/context/online-users-context'
+import {
+ Dropdown,
+ DropdownHeader,
+ DropdownItem,
+ DropdownMenu,
+ DropdownToggle,
+} from '@/features/ui/components/bootstrap-5/dropdown-menu'
+import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
+import { getBackgroundColorForUserId } from '@/shared/utils/colors'
+import { useCallback, useMemo } from 'react'
+import { useTranslation } from 'react-i18next'
+
+// 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) => void
+}) => {
+ 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 (
+
+ {usersBeforeOverflow.map((user, index) => (
+
+ ))}
+ {hasOverflow && (
+
+ )}
+
+ )
+}
+
+const OnlineUserWidget = ({
+ user,
+ goToUser,
+ id,
+}: {
+ user: OnlineUser
+ goToUser: (user: OnlineUser) => void
+ id: string
+}) => {
+ const onClick = useCallback(() => {
+ goToUser(user)
+ }, [goToUser, user])
+ return (
+
+
+
+ )
+}
+
+const OnlineUserCircle = ({ user }: { user: OnlineUser }) => {
+ const backgroundColor = getBackgroundColorForUserId(user.user_id)
+ return (
+
+ {user.name.charAt(0)}
+
+ )
+}
+
+const OnlineUserOverflow = ({
+ goToUser,
+ users,
+}: {
+ goToUser: (user: OnlineUser) => void
+ users: OnlineUser[]
+}) => {
+ const { t } = useTranslation()
+ return (
+
+
+
+ +{users.length}
+
+
+
+
+ {t('connected_users')}
+
+ {users.map((user, index) => (
+
+ goToUser(user)}
+ >
+ {user.name}
+
+
+ ))}
+
+
+ )
+}
diff --git a/services/web/frontend/js/features/ide-redesign/components/toolbar/online-users.tsx b/services/web/frontend/js/features/ide-redesign/components/toolbar/online-users.tsx
index d8fb314907..4d937fa89c 100644
--- a/services/web/frontend/js/features/ide-redesign/components/toolbar/online-users.tsx
+++ b/services/web/frontend/js/features/ide-redesign/components/toolbar/online-users.tsx
@@ -1,10 +1,10 @@
-import OnlineUsersWidget from '@/features/editor-navigation-toolbar/components/online-users-widget'
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
import {
OnlineUser,
useOnlineUsersContext,
} from '@/features/ide-react/context/online-users-context'
import { useCallback } from 'react'
+import { OnlineUsersWidget } from '../online-users/online-users-widget'
export const OnlineUsers = () => {
const { openDoc } = useEditorManagerContext()
diff --git a/services/web/frontend/stories/online-users.stories.tsx b/services/web/frontend/stories/online-users.stories.tsx
new file mode 100644
index 0000000000..7d2796cb2e
--- /dev/null
+++ b/services/web/frontend/stories/online-users.stories.tsx
@@ -0,0 +1,69 @@
+import { Meta } from '@storybook/react'
+import { OnlineUser } from '@/features/ide-react/context/online-users-context'
+import OnlineUsersWidgetOld from '@/features/editor-navigation-toolbar/components/online-users-widget'
+import { OnlineUsersWidget } from '@/features/ide-redesign/components/online-users/online-users-widget'
+
+const NAMES = [
+ 'Alice',
+ 'Bob',
+ 'Charlie',
+ 'Dave',
+ 'Erin',
+ 'Frank',
+ 'Grace',
+ 'Heidi',
+ 'Ivan',
+ 'Judy',
+ 'Mallory',
+ 'Niaj',
+ 'Olivia',
+ 'Peggy',
+ 'Rupert',
+]
+
+const generateUser = (_: any, index: number): OnlineUser => {
+ const name = NAMES[index % NAMES.length]
+ return {
+ user_id: `user_${'b'.repeat(index)}`,
+ name,
+ id: `user-${index}`,
+ email: `${name.toLowerCase()}@example.com`,
+ }
+}
+
+export const OnlineUsersRedesign = ({ users }: { users: number }) => {
+ const generatedUsers = Array.from({ length: users }, generateUser)
+ return (
+
+ {}} />
+
+ )
+}
+
+export const OnlineUsersOld = ({ users }: { users: number }) => {
+ const generatedUsers = Array.from({ length: users }, generateUser)
+ return (
+
+ {}} />
+
+ )
+}
+
+const meta: Meta = {
+ title: 'Editor / Online Users Widget',
+ args: {
+ users: 6,
+ },
+}
+
+export default meta
diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/online-users.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/online-users.scss
index 52265c6e97..2a03c22c19 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/online-users.scss
+++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/online-users.scss
@@ -1,9 +1,15 @@
:root {
--toolbar-btn-color: var(--white);
+ --online-users-border-color: var(--bg-dark-primary);
+ --online-users-text-color: var(--content-primary);
+ --online-users-overflow-button-background-color: var(--bg-light-secondary);
}
@include theme('light') {
--toolbar-btn-color: var(--neutral-70);
+ --online-users-border-color: var(--bg-light-primary);
+ --online-users-text-color: var(--content-primary);
+ --online-users-overflow-button-background-color: var(--bg-light-secondary);
}
.online-users {
@@ -35,3 +41,87 @@
align-items: center;
}
}
+
+.online-users-row {
+ // Keep in sync with the MAX_USER_CIRCLES_DISPLAYED js constant
+ $max-user-circles-displayed: 5;
+
+ --online-users-circle-size: 24px;
+ --online-users-border-size: 1px;
+ --online-users-overlap: 8px;
+ --online-users-circle-padding: var(--spacing-01);
+
+ .online-user-overflow-dropdown {
+ --online-users-border-color: transparent;
+ }
+
+ display: flex;
+ align-items: center;
+ color: var(--online-users-text-color);
+
+ .online-users-row-button {
+ padding: 0;
+ margin: 0;
+ background: none;
+ border: none;
+ color: var(--online-users-text-color);
+ min-width: var(--online-users-circle-size);
+ height: var(--online-users-circle-size);
+ display: block;
+ font-weight: var(--font-weight-regular);
+
+ &.dropdown-toggle::after {
+ display: none;
+ }
+
+ &:not(:last-child, .online-user-overflow-toggle) {
+ margin-right: calc(
+ var(--online-users-overlap) * -1 + var(--online-users-border-size)
+ );
+ }
+
+ @for $i from 1 through $max-user-circles-displayed {
+ &:nth-child(#{$i}):not(:hover):not(.online-user-overflow-toggle) {
+ z-index: $i;
+ }
+ }
+
+ &:hover {
+ z-index: $max-user-circles-displayed + 2;
+ }
+ }
+
+ .online-user-overflow-toggle {
+ &:not(:hover) {
+ z-index: $max-user-circles-displayed + 1;
+ }
+
+ &:hover,
+ &:active,
+ & {
+ background: none;
+ }
+
+ .online-user-circle {
+ background-color: var(--online-users-overflow-button-background-color);
+ color: var(--online-users-text-color);
+ }
+ }
+
+ .online-user-circle {
+ padding: var(--online-users-circle-padding);
+ border-radius: 50%;
+ min-width: var(--online-users-circle-size);
+ height: var(--online-users-circle-size);
+ line-height: calc(
+ var(--online-users-circle-size) - 2 * var(--online-users-border-size) - 2 *
+ var(--online-users-circle-padding)
+ );
+ font-size: var(--font-size-01);
+ text-align: center;
+ border: var(--online-users-border-size) solid
+ var(--online-users-border-color);
+ box-sizing: border-box;
+ display: inline-block;
+ }
+}
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index 08f22483a7..99f4f27473 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -1365,6 +1365,8 @@
"my_library": "My Library",
"n_items": "__count__ item",
"n_items_plural": "__count__ items",
+ "n_more_collaborators": "__count__ more collaborator",
+ "n_more_collaborators_plural": "__count__ more collaborators",
"n_more_updates_above": "__count__ more update above",
"n_more_updates_above_plural": "__count__ more updates above",
"n_more_updates_below": "__count__ more update below",