diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug
index 8a72cc2c27..a3a6ec932e 100644
--- a/services/web/app/views/project/editor.pug
+++ b/services/web/app/views/project/editor.pug
@@ -15,6 +15,8 @@ block content
editor-loading="editorLoading"
chat-is-open-angular="chatIsOpenAngular"
set-chat-is-open-angular="setChatIsOpenAngular"
+ open-doc="openDoc"
+ online-users-array="onlineUsersArray"
)
.loading-screen(ng-if="state.loading")
.loading-screen-brand-container
diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 945e740c71..f288d2c84d 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -21,6 +21,7 @@
"compile_mode": "",
"compile_terminated_by_user": "",
"compiling": "",
+ "connected_users": "",
"conflicting_paths_found": "",
"copy": "",
"copy_project": "",
diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/editor-navigation-toolbar-root.js b/services/web/frontend/js/features/editor-navigation-toolbar/components/editor-navigation-toolbar-root.js
index 5515bab410..a44e1f5e99 100644
--- a/services/web/frontend/js/features/editor-navigation-toolbar/components/editor-navigation-toolbar-root.js
+++ b/services/web/frontend/js/features/editor-navigation-toolbar/components/editor-navigation-toolbar-root.js
@@ -5,7 +5,13 @@ import { useEditorContext } from '../../../shared/context/editor-context'
import { useChatContext } from '../../chat/context/chat-context'
function EditorNavigationToolbarRoot({ onShowLeftMenuClick }) {
- const { cobranding, loading, ui } = useEditorContext()
+ const {
+ cobranding,
+ loading,
+ ui,
+ onlineUsersArray,
+ openDoc
+ } = useEditorContext()
const { resetUnreadMessageCount, unreadMessageCount } = useChatContext()
const toggleChatOpen = useCallback(() => {
@@ -15,6 +21,12 @@ function EditorNavigationToolbarRoot({ onShowLeftMenuClick }) {
ui.toggleChatOpen()
}, [ui, resetUnreadMessageCount])
+ function goToUser(user) {
+ if (user.doc && typeof user.row === 'number') {
+ openDoc(user.doc, { gotoLine: user.row + 1 })
+ }
+ }
+
// using {display: 'none'} as 1:1 migration from Angular's ng-hide. Using
// `loading ? null : ` causes UI glitches
return (
@@ -25,6 +37,8 @@ function EditorNavigationToolbarRoot({ onShowLeftMenuClick }) {
chatIsOpen={ui.chatIsOpen}
unreadMessageCount={unreadMessageCount}
toggleChatOpen={toggleChatOpen}
+ onlineUsers={onlineUsersArray}
+ goToUser={goToUser}
/>
)
}
diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/online-users-widget.js b/services/web/frontend/js/features/editor-navigation-toolbar/components/online-users-widget.js
new file mode 100644
index 0000000000..8dee694e08
--- /dev/null
+++ b/services/web/frontend/js/features/editor-navigation-toolbar/components/online-users-widget.js
@@ -0,0 +1,120 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { useTranslation } from 'react-i18next'
+import { Dropdown, MenuItem, OverlayTrigger, Tooltip } from 'react-bootstrap'
+import Icon from '../../../shared/components/icon'
+import ColorManager from '../../../ide/colors/ColorManager'
+
+function OnlineUsersWidget({ onlineUsers, goToUser }) {
+ const { t } = useTranslation()
+
+ const shouldDisplayDropdown = onlineUsers.length >= 4
+
+ if (shouldDisplayDropdown) {
+ return (
+
+
+
+
+ {onlineUsers.map(user => (
+
+ ))}
+
+
+ )
+ } else {
+ return (
+
+ {onlineUsers.map(user => (
+ {user.name}}
+ >
+
+ {/* OverlayTrigger won't fire unless UserIcon is wrapped in a span */}
+
+
+
+ ))}
+
+ )
+ }
+}
+
+OnlineUsersWidget.propTypes = {
+ onlineUsers: PropTypes.arrayOf(
+ PropTypes.shape({
+ user_id: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired
+ })
+ ).isRequired,
+ goToUser: PropTypes.func.isRequired
+}
+
+function UserIcon({ user, showName, onClick }) {
+ const backgroundColor = `hsl(${ColorManager.getHueForUserId(
+ user.user_id
+ )}, 70%, 50%)`
+
+ function handleOnClick() {
+ onClick(user)
+ }
+
+ return (
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
+
+
+ {user.name.slice(0, 1)}
+
+ {showName && user.name}
+
+ )
+}
+
+UserIcon.propTypes = {
+ user: PropTypes.shape({
+ user_id: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired
+ }),
+ showName: PropTypes.bool,
+ onClick: PropTypes.func.isRequired
+}
+
+const DropDownToggleButton = React.forwardRef((props, ref) => {
+ const { t } = useTranslation()
+ return (
+ {t('connected_users')}
+ }
+ >
+
+
+ )
+})
+
+DropDownToggleButton.propTypes = {
+ onlineUserCount: PropTypes.number.isRequired,
+ onClick: PropTypes.func
+}
+
+export default OnlineUsersWidget
diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/toolbar-header.js b/services/web/frontend/js/features/editor-navigation-toolbar/components/toolbar-header.js
index dc97613fdc..99ab4650c9 100644
--- a/services/web/frontend/js/features/editor-navigation-toolbar/components/toolbar-header.js
+++ b/services/web/frontend/js/features/editor-navigation-toolbar/components/toolbar-header.js
@@ -4,13 +4,16 @@ import MenuButton from './menu-button'
import CobrandingLogo from './cobranding-logo'
import BackToProjectsButton from './back-to-projects-button'
import ChatToggleButton from './chat-toggle-button'
+import OnlineUsersWidget from './online-users-widget'
function ToolbarHeader({
cobranding,
onShowLeftMenuClick,
chatIsOpen,
toggleChatOpen,
- unreadMessageCount
+ unreadMessageCount,
+ onlineUsers,
+ goToUser
}) {
return (
@@ -20,6 +23,7 @@ function ToolbarHeader({
+
{
$scope.chatIsOpenAngular = value
})
+
+ // wrapper is required to avoid scope problems with `this` inside `EditorManager`
+ $scope.openDoc = (doc, args) => ide.editorManager.openDoc(doc, args)
})
App.component(
@@ -30,6 +33,11 @@ App.component(
react2angular(rootContext.component, [
'editorLoading',
'setChatIsOpenAngular',
- 'chatIsOpenAngular'
+ 'chatIsOpenAngular',
+ 'openDoc',
+ // `$scope.onlineUsersArray` is already populated by `OnlineUsersManager`, which also creates
+ // a new array instance every time the list of online users change (which should refresh the
+ // value passed to React as a prop, triggering a re-render)
+ 'onlineUsersArray'
])
)
diff --git a/services/web/frontend/js/shared/context/editor-context.js b/services/web/frontend/js/shared/context/editor-context.js
index 15f9ac6729..988a5a7f2d 100644
--- a/services/web/frontend/js/shared/context/editor-context.js
+++ b/services/web/frontend/js/shared/context/editor-context.js
@@ -21,7 +21,9 @@ export function EditorProvider({
children,
loading,
chatIsOpenAngular,
- setChatIsOpenAngular
+ setChatIsOpenAngular,
+ openDoc,
+ onlineUsersArray
}) {
const cobranding = window.brandVariation
? {
@@ -63,6 +65,8 @@ export function EditorProvider({
loading,
projectId: window.project_id,
isProjectOwner: ownerId === window.user.id,
+ openDoc,
+ onlineUsersArray,
ui: {
chatIsOpen,
toggleChatOpen
@@ -80,7 +84,9 @@ EditorProvider.propTypes = {
children: PropTypes.any,
loading: PropTypes.bool,
chatIsOpenAngular: PropTypes.bool,
- setChatIsOpenAngular: PropTypes.func.isRequired
+ setChatIsOpenAngular: PropTypes.func.isRequired,
+ openDoc: PropTypes.func.isRequired,
+ onlineUsersArray: PropTypes.array.isRequired
}
export function useEditorContext(propTypes) {
diff --git a/services/web/frontend/js/shared/context/root-context.js b/services/web/frontend/js/shared/context/root-context.js
index ae6e378071..ae91ba783b 100644
--- a/services/web/frontend/js/shared/context/root-context.js
+++ b/services/web/frontend/js/shared/context/root-context.js
@@ -9,7 +9,9 @@ export function ContextRoot({
children,
editorLoading,
chatIsOpenAngular,
- setChatIsOpenAngular
+ setChatIsOpenAngular,
+ openDoc,
+ onlineUsersArray
}) {
return (
@@ -17,6 +19,8 @@ export function ContextRoot({
loading={editorLoading}
chatIsOpenAngular={chatIsOpenAngular}
setChatIsOpenAngular={setChatIsOpenAngular}
+ openDoc={openDoc}
+ onlineUsersArray={onlineUsersArray}
>
{children}
@@ -28,7 +32,9 @@ ContextRoot.propTypes = {
children: PropTypes.any,
editorLoading: PropTypes.bool,
chatIsOpenAngular: PropTypes.bool,
- setChatIsOpenAngular: PropTypes.func.isRequired
+ setChatIsOpenAngular: PropTypes.func.isRequired,
+ openDoc: PropTypes.func.isRequired,
+ onlineUsersArray: PropTypes.array.isRequired
}
export const rootContext = createSharedContext(ContextRoot)
diff --git a/services/web/frontend/stories/editor-navigation-toolbar.stories.js b/services/web/frontend/stories/editor-navigation-toolbar.stories.js
index c40b67b73a..cd12604b46 100644
--- a/services/web/frontend/stories/editor-navigation-toolbar.stories.js
+++ b/services/web/frontend/stories/editor-navigation-toolbar.stories.js
@@ -1,10 +1,38 @@
import React from 'react'
import ToolbarHeader from '../js/features/editor-navigation-toolbar/components/toolbar-header'
-export const Default = () => {
- return
+// required by ColorManager
+window.user = { id: 42 }
+
+export const UpToThreeConnectedUsers = args => {
+ return
+}
+UpToThreeConnectedUsers.args = {
+ onlineUsers: ['a', 'c', 'd'].map(c => ({
+ user_id: c,
+ name: `${c}_user name`
+ }))
+}
+
+export const ManyConnectedUsers = args => {
+ return
+}
+ManyConnectedUsers.args = {
+ onlineUsers: ['a', 'c', 'd', 'e', 'f'].map(c => ({
+ user_id: c,
+ name: `${c}_user name`
+ }))
}
export default {
- title: 'EditorNavigationToolbar'
+ title: 'EditorNavigationToolbar',
+ component: ToolbarHeader,
+ argTypes: {
+ goToUser: { action: 'goToUser' }
+ },
+ args: {
+ onlineUsers: [{ user_id: 'abc', name: 'overleaf' }],
+ goToUser: () => {},
+ onShowLeftMenuClick: () => {}
+ }
}
diff --git a/services/web/test/frontend/helpers/render-with-context.js b/services/web/test/frontend/helpers/render-with-context.js
index b61e93aa83..014b563f98 100644
--- a/services/web/test/frontend/helpers/render-with-context.js
+++ b/services/web/test/frontend/helpers/render-with-context.js
@@ -26,7 +26,12 @@ export function renderWithEditorContext(
}
return render(
- {}} setChatIsOpenAngular={() => {}}>
+ {}}
+ setChatIsOpenAngular={() => {}}
+ openDoc={() => {}}
+ onlineUsersArray={[]}
+ >
{children}