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 ( + + + + {t('connected_users')} + {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}