Просмотр исходного кода

Merge pull request #18936 from overleaf/rh-viewer-no-comment

[web] Remove access to commenting/chat for collaborators with Viewer permission

GitOrigin-RevId: 03957cb8c04866318a4b94bdb72843e7d7a5a003
roo hutton 2 лет назад
Родитель
Сommit
bf90932f40

+ 8 - 3
services/web/frontend/js/features/chat/components/chat-pane.tsx

@@ -11,13 +11,14 @@ import withErrorBoundary from '../../../infrastructure/error-boundary'
 import { FetchError } from '../../../infrastructure/fetch-json'
 import { useChatContext } from '../context/chat-context'
 import LoadingSpinner from '../../../shared/components/loading-spinner'
+import useViewerPermissions from '@/shared/hooks/use-viewer-permissions'
 
 const MessageList = lazy(() => import('./message-list'))
 
 const ChatPane = React.memo(function ChatPane() {
   const { t } = useTranslation()
 
-  const { chatIsOpen } = useLayoutContext()
+  const { setChatIsOpen, chatIsOpen } = useLayoutContext()
   const user = useUserContext()
 
   const {
@@ -46,13 +47,17 @@ const ChatPane = React.memo(function ChatPane() {
     0
   )
 
+  const shouldShowChat = !useViewerPermissions()
+
   // Keep the chat pane in the DOM to avoid resetting the form input and re-rendering MathJax content.
   const [chatOpenedOnce, setChatOpenedOnce] = useState(chatIsOpen)
   useEffect(() => {
-    if (chatIsOpen) {
+    if (chatIsOpen && shouldShowChat) {
       setChatOpenedOnce(true)
+    } else if (chatIsOpen && !shouldShowChat) {
+      setChatIsOpen(false)
     }
-  }, [chatIsOpen])
+  }, [chatIsOpen, setChatIsOpen, shouldShowChat])
 
   if (error) {
     // let user try recover from fetch errors

+ 4 - 2
services/web/frontend/js/features/chat/context/chat-context.tsx

@@ -17,6 +17,7 @@ import { appendMessage, prependMessages } from '../utils/message-list-appender'
 import useBrowserWindow from '../../../shared/hooks/use-browser-window'
 import { useLayoutContext } from '../../../shared/context/layout-context'
 import { useIdeContext } from '@/shared/context/ide-context'
+import useViewerPermissions from '@/shared/hooks/use-viewer-permissions'
 
 const PAGE_SIZE = 50
 
@@ -286,9 +287,10 @@ export const ChatProvider: FC = ({ children }) => {
   }, [])
 
   // Handling receiving messages over the socket
+  const hasViewerPermissions = useViewerPermissions()
   const { socket } = useIdeContext()
   useEffect(() => {
-    if (!socket) return
+    if (!socket || hasViewerPermissions) return
 
     function receivedMessage(message: any) {
       // If the message is from the current client id, then we are receiving the sent message back from the socket.
@@ -304,7 +306,7 @@ export const ChatProvider: FC = ({ children }) => {
 
       socket.removeListener('new-chat-message', receivedMessage)
     }
-  }, [socket])
+  }, [socket, hasViewerPermissions])
 
   // Handle unread messages
   useEffect(() => {

+ 4 - 1
services/web/frontend/js/features/editor-navigation-toolbar/components/editor-navigation-toolbar-root.tsx

@@ -6,6 +6,7 @@ import { useLayoutContext } from '../../../shared/context/layout-context'
 import { useProjectContext } from '../../../shared/context/project-context'
 import * as eventTracking from '../../../infrastructure/event-tracking'
 import { Doc } from '../../../../../types/doc'
+import useViewerPermissions from '@/shared/hooks/use-viewer-permissions'
 
 function isOpentoString(open: boolean) {
   return open ? 'open' : 'close'
@@ -44,6 +45,7 @@ const EditorNavigationToolbarRoot = React.memo(
     } = useLayoutContext()
 
     const { markMessagesAsRead, unreadMessageCount } = useChatContext()
+    const canViewChatAndTrackChanges = !useViewerPermissions()
 
     const toggleChatOpen = useCallback(() => {
       if (!chatIsOpen) {
@@ -119,11 +121,12 @@ const EditorNavigationToolbarRoot = React.memo(
         hasPublishPermissions={
           permissionsLevel === 'owner' || permissionsLevel === 'readAndWrite'
         }
+        chatVisible={!(isRestrictedTokenMember || !canViewChatAndTrackChanges)}
         projectName={projectName}
         renameProject={renameProject}
         hasRenamePermissions={permissionsLevel === 'owner'}
         openShareModal={openShareModal}
-        trackChangesVisible={trackChangesVisible}
+        trackChangesVisible={canViewChatAndTrackChanges && trackChangesVisible}
       />
     )
   }

+ 3 - 1
services/web/frontend/js/features/editor-navigation-toolbar/components/toolbar-header.jsx

@@ -33,6 +33,7 @@ const ToolbarHeader = React.memo(function ToolbarHeader({
   goToUser,
   isRestrictedTokenMember,
   hasPublishPermissions,
+  chatVisible,
   projectName,
   renameProject,
   hasRenamePermissions,
@@ -89,7 +90,7 @@ const ToolbarHeader = React.memo(function ToolbarHeader({
 
             <LayoutDropdownButton />
 
-            {!isRestrictedTokenMember && (
+            {chatVisible && (
               <ChatToggleButton
                 chatIsOpen={chatIsOpen}
                 onClick={toggleChatOpen}
@@ -117,6 +118,7 @@ ToolbarHeader.propTypes = {
   goToUser: PropTypes.func.isRequired,
   isRestrictedTokenMember: PropTypes.bool,
   hasPublishPermissions: PropTypes.bool,
+  chatVisible: PropTypes.bool,
   projectName: PropTypes.string.isRequired,
   renameProject: PropTypes.func.isRequired,
   hasRenamePermissions: PropTypes.bool,

+ 17 - 4
services/web/frontend/js/features/ide-react/context/permissions-context.tsx

@@ -8,6 +8,7 @@ import {
 } from '@/features/ide-react/types/permissions'
 import useScopeValue from '@/shared/hooks/use-scope-value'
 import { DeepReadonly } from '../../../../../types/utils'
+import useViewerPermissions from '@/shared/hooks/use-viewer-permissions'
 
 export const PermissionsContext = createContext<Permissions | undefined>(
   undefined
@@ -40,6 +41,12 @@ const anonymousPermissionsMap: typeof permissionsMap = {
   owner: { ...permissionsMap.owner, comment: false },
 }
 
+const linkSharingWarningPermissionsMap: typeof permissionsMap = {
+  readOnly: { ...permissionsMap.readOnly, comment: false },
+  readAndWrite: permissionsMap.readAndWrite,
+  owner: permissionsMap.owner,
+}
+
 export const PermissionsProvider: React.FC = ({ children }) => {
   const [permissions, setPermissions] =
     useScopeValue<Readonly<Permissions>>('permissions')
@@ -47,14 +54,20 @@ export const PermissionsProvider: React.FC = ({ children }) => {
   const { permissionsLevel } = useEditorContext() as {
     permissionsLevel: PermissionsLevel
   }
+  const hasViewerPermissions = useViewerPermissions()
   const anonymous = getMeta('ol-anonymous')
 
   useEffect(() => {
-    const activePermissionsMap = anonymous
-      ? anonymousPermissionsMap
-      : permissionsMap
+    let activePermissionsMap
+    if (hasViewerPermissions) {
+      activePermissionsMap = linkSharingWarningPermissionsMap
+    } else {
+      activePermissionsMap = anonymous
+        ? anonymousPermissionsMap
+        : permissionsMap
+    }
     setPermissions(activePermissionsMap[permissionsLevel])
-  }, [anonymous, permissionsLevel, setPermissions])
+  }, [anonymous, permissionsLevel, setPermissions, hasViewerPermissions])
 
   useEffect(() => {
     if (connectionState.forceDisconnected) {

+ 10 - 4
services/web/frontend/js/features/ide-react/context/review-panel/hooks/use-review-panel-state.ts

@@ -24,7 +24,6 @@ import {
   useEditorManagerContext,
 } from '@/features/ide-react/context/editor-manager-context'
 import { debugConsole } from '@/utils/debugging'
-import { useEditorContext } from '@/shared/context/editor-context'
 import { deleteJSON, getJSON, postJSON } from '@/infrastructure/fetch-json'
 import ColorManager from '@/ide/colors/ColorManager'
 import RangesTracker from '@overleaf/ranges-tracker'
@@ -65,7 +64,9 @@ import {
   EditOperation,
 } from '../../../../../../../types/change'
 import { RangesTrackerWithResolvedThreadIds } from '@/features/ide-react/editor/document-container'
+import useViewerPermissions from '@/shared/hooks/use-viewer-permissions'
 import getMeta from '@/utils/meta'
+import { useEditorContext } from '@/shared/context/editor-context'
 
 const dispatchReviewPanelEvent = (type: string, payload?: any) => {
   window.dispatchEvent(
@@ -151,6 +152,7 @@ function useReviewPanelState(): ReviewPanel.ReviewPanelState {
   const permissions = usePermissionsContext()
   const { showGenericMessageModal } = useModalsContext()
   const addCommentEmitter = useScopeEventEmitter('comment:start_adding')
+  const hasViewerPermissions = useViewerPermissions()
 
   const layoutToLeft = useLayoutToLeft('.ide-react-editor-panel')
   const [subView, setSubView] =
@@ -418,7 +420,7 @@ function useReviewPanelState(): ReviewPanel.ReviewPanelState {
         }
 
         if (!users[change.metadata.user_id]) {
-          if (!isRestrictedTokenMember) {
+          if (!(isRestrictedTokenMember || hasViewerPermissions)) {
             refreshChangeUsers(change.metadata.user_id)
           }
         }
@@ -426,7 +428,10 @@ function useReviewPanelState(): ReviewPanel.ReviewPanelState {
 
       let localResolvedThreadIds = resolvedThreadIds
 
-      if (!isRestrictedTokenMember && rangesTracker.comments.length > 0) {
+      if (
+        !(isRestrictedTokenMember || hasViewerPermissions) &&
+        rangesTracker.comments.length > 0
+      ) {
         const threadsLoadResult = await ensureThreadsAreLoaded()
         if (threadsLoadResult?.resolvedThreadIds) {
           localResolvedThreadIds = threadsLoadResult.resolvedThreadIds
@@ -483,13 +488,14 @@ function useReviewPanelState(): ReviewPanel.ReviewPanelState {
       getChangeTracker,
       getDocEntries,
       getDocResolvedComments,
-      isRestrictedTokenMember,
       refreshChangeUsers,
       resolvedThreadIds,
       users,
       ensureThreadsAreLoaded,
       loadingThreads,
       setLoadingThreads,
+      hasViewerPermissions,
+      isRestrictedTokenMember,
     ]
   )
 

+ 5 - 1
services/web/frontend/js/features/source-editor/components/codemirror-editor.tsx

@@ -19,6 +19,7 @@ import { dispatchTimer } from '../../../infrastructure/cm6-performance'
 import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
 import { FigureModal } from './figure-modal/figure-modal'
 import ReviewPanel from './review-panel/review-panel'
+import useViewerPermissions from '@/shared/hooks/use-viewer-permissions'
 
 const sourceEditorComponents = importOverleafModules(
   'sourceEditorComponents'
@@ -36,6 +37,8 @@ function CodeMirrorEditor() {
 
   const isMounted = useIsMounted()
 
+  const shouldShowReviewPanel = !useViewerPermissions()
+
   // create the view using the initial state and intercept transactions
   const viewRef = useRef<EditorView | null>(null)
   if (viewRef.current === null) {
@@ -69,7 +72,8 @@ function CodeMirrorEditor() {
           )
         )}
         <CodeMirrorCommandTooltip />
-        <ReviewPanel />
+
+        {shouldShowReviewPanel && <ReviewPanel />}
         {sourceEditorComponents.map(
           ({ import: { default: Component }, path }) => (
             <Component key={path} />

+ 12 - 0
services/web/frontend/js/shared/hooks/use-viewer-permissions.ts

@@ -0,0 +1,12 @@
+import { useEditorContext } from '../context/editor-context'
+import getMeta from '@/utils/meta'
+
+function useViewerPermissions() {
+  const { permissionsLevel } = useEditorContext()
+
+  const hasViewerPermissions =
+    getMeta('ol-linkSharingWarning') && permissionsLevel === 'readOnly'
+  return hasViewerPermissions
+}
+
+export default useViewerPermissions

+ 3 - 2
services/web/test/frontend/features/editor-navigation-toolbar/components/toolbar-header.test.jsx

@@ -17,6 +17,7 @@ describe('<ToolbarHeader />', function () {
     renameProject: () => {},
     openShareModal: () => {},
     hasPublishPermissions: true,
+    chatVisible: true,
     trackChangesVisible: true,
     handleChangeLayout: () => {},
     pdfLayout: 'sideBySide',
@@ -83,10 +84,10 @@ describe('<ToolbarHeader />', function () {
       screen.getByText('Chat')
     })
 
-    it('is not displayed when "isRestrictedTokenMember" prop is set to true', function () {
+    it('is not displayed when "chatVisible" prop is set to false', function () {
       const props = {
         ...defaultProps,
-        isRestrictedTokenMember: true,
+        chatVisible: false,
       }
       renderWithEditorContext(<ToolbarHeader {...props} />)
       expect(screen.queryByText('Chat')).to.not.exist