Răsfoiți Sursa

Merge pull request #13378 from overleaf/ii-review-panel-migration-create-context-api

[web] Create context api for review panel

GitOrigin-RevId: 3d550e2f3c14ecd0586d088f4ecb75acd44aa782
ilkin-overleaf 3 ani în urmă
părinte
comite
1918ef3767

+ 19 - 0
services/web/frontend/js/features/source-editor/components/review-panel/container.tsx

@@ -0,0 +1,19 @@
+import classnames from 'classnames'
+
+const reviewPanelClasses = ['ol-cm-review-panel']
+
+type ContainerProps = {
+  children?: React.ReactNode
+  classNames?: Record<string, boolean>
+  style?: React.CSSProperties
+}
+
+function Container({ children, classNames, ...rest }: ContainerProps) {
+  return (
+    <div className={classnames(...reviewPanelClasses, classNames)} {...rest}>
+      {children}
+    </div>
+  )
+}
+
+export default Container

+ 12 - 0
services/web/frontend/js/features/source-editor/components/review-panel/current-file-container.tsx

@@ -0,0 +1,12 @@
+import Container from './container'
+
+function CurrentFileContainer() {
+  return (
+    <Container>
+      <em>ReviewPanelCurrentFileContainer</em>
+      <div className="review-panel-tools">Tools</div>
+    </Container>
+  )
+}
+
+export default CurrentFileContainer

+ 1 - 0
services/web/frontend/js/features/source-editor/components/review-panel/nav.tsx

@@ -0,0 +1 @@
+export type SubView = 'cur_file' | 'overview'

+ 11 - 0
services/web/frontend/js/features/source-editor/components/review-panel/overview-container.tsx

@@ -0,0 +1,11 @@
+import Container from './container'
+
+function OverviewContainer() {
+  return (
+    <Container>
+      <em>ReviewPanelOverviewContainer</em>
+    </Container>
+  )
+}
+
+export default OverviewContainer

+ 35 - 1
services/web/frontend/js/features/source-editor/components/review-panel/review-panel.tsx

@@ -1,5 +1,39 @@
+import ReactDOM from 'react-dom'
+import { useCodeMirrorViewContext } from '../codemirror-editor'
+import {
+  ReviewPanelStateProvider,
+  useReviewPanelStateValueContext,
+} from '../../context/review-panel-state-context'
+import CurrentFileContainer from './current-file-container'
+import OverviewContainer from './overview-container'
+
+type ReviewPanelViewProps = {
+  parentDomNode: Element
+}
+
+function ReviewPanelView({ parentDomNode }: ReviewPanelViewProps) {
+  const { subView } = useReviewPanelStateValueContext()
+
+  return ReactDOM.createPortal(
+    <>
+      {subView === 'cur_file' ? (
+        <CurrentFileContainer />
+      ) : (
+        <OverviewContainer />
+      )}
+    </>,
+    parentDomNode
+  )
+}
+
 function ReviewPanel() {
-  return <div>Content to be added.</div>
+  const view = useCodeMirrorViewContext()
+
+  return (
+    <ReviewPanelStateProvider>
+      <ReviewPanelView parentDomNode={view.scrollDOM} />
+    </ReviewPanelStateProvider>
+  )
 }
 
 export default ReviewPanel

+ 109 - 0
services/web/frontend/js/features/source-editor/context/review-panel-state-context.tsx

@@ -0,0 +1,109 @@
+import { createContext, useContext, useMemo } from 'react'
+import useScopeValue from '../../../shared/hooks/use-scope-value'
+import { SubView } from '../components/review-panel/nav'
+
+interface ReviewPanelState {
+  values: {
+    subView: SubView
+    collapsed: Record<string, boolean>
+  }
+  updaterFns: {
+    setSubView: React.Dispatch<
+      React.SetStateAction<ReviewPanelState['values']['subView']>
+    >
+    setCollapsed: React.Dispatch<
+      React.SetStateAction<ReviewPanelState['values']['collapsed']>
+    >
+  }
+}
+
+function useAngularReviewPanelState(): ReviewPanelState {
+  const [subView, setSubView] = useScopeValue<SubView>('reviewPanel.subView')
+  const [collapsed, setCollapsed] = useScopeValue<Record<string, boolean>>(
+    'reviewPanel.overview.docsCollapsedState'
+  )
+
+  const values = useMemo<ReviewPanelState['values']>(
+    () => ({
+      subView,
+      collapsed,
+    }),
+    [subView, collapsed]
+  )
+
+  const updaterFns = useMemo<ReviewPanelState['updaterFns']>(
+    () => ({
+      setSubView,
+      setCollapsed,
+    }),
+    [setSubView, setCollapsed]
+  )
+
+  return { values, updaterFns }
+}
+
+const ReviewPanelStateValueContext = createContext<
+  ReviewPanelState['values'] | undefined
+>(undefined)
+
+type ReviewPanelStateValueProps = {
+  children?: React.ReactNode
+  value: ReviewPanelState['values']
+}
+
+function ReviewPanelStateValueProvider(props: ReviewPanelStateValueProps) {
+  return <ReviewPanelStateValueContext.Provider {...props} />
+}
+
+const ReviewPanelStateUpdaterFnsContext = createContext<
+  ReviewPanelState['updaterFns'] | undefined
+>(undefined)
+
+type ReviewPanelStateUpdaterFnsProviderProps = {
+  children?: React.ReactNode
+  value: ReviewPanelState['updaterFns']
+}
+
+function ReviewPanelStateUpdaterFnsProvider(
+  props: ReviewPanelStateUpdaterFnsProviderProps
+) {
+  return <ReviewPanelStateUpdaterFnsContext.Provider {...props} />
+}
+
+type ReviewPanelStateProviderProps = {
+  children?: React.ReactNode
+}
+
+export function ReviewPanelStateProvider({
+  children,
+}: ReviewPanelStateProviderProps) {
+  const { values, updaterFns } = useAngularReviewPanelState()
+
+  return (
+    <ReviewPanelStateValueProvider value={values}>
+      <ReviewPanelStateUpdaterFnsProvider value={updaterFns}>
+        {children}
+      </ReviewPanelStateUpdaterFnsProvider>
+    </ReviewPanelStateValueProvider>
+  )
+}
+
+export function useReviewPanelStateValueContext() {
+  const context = useContext(ReviewPanelStateValueContext)
+  if (!context) {
+    throw new Error(
+      'ReviewPanelStateValueContext is only available inside ReviewPanelStateProvider'
+    )
+  }
+  return context
+}
+
+export function useReviewPanelStateUpdaterFnsContext() {
+  const context = useContext(ReviewPanelStateUpdaterFnsContext)
+  if (!context) {
+    throw new Error(
+      'ReviewPanelStateUpdaterFnsContext is only available inside ReviewPanelStateProvider'
+    )
+  }
+  return context
+}

+ 130 - 111
services/web/frontend/js/ide/review-panel/controllers/ReviewPanelController.js

@@ -71,7 +71,7 @@ export default App.controller(
       PENDING: 'pending',
     }
 
-    $scope.reviewPanel = {
+    ide.$scope.reviewPanel = {
       trackChangesState: {},
       trackChangesOnForEveryone: false,
       trackChangesOnForGuests: false,
@@ -111,8 +111,8 @@ export default App.controller(
 
     window.addEventListener('beforeunload', function () {
       const collapsedStates = {}
-      for (const doc in $scope.reviewPanel.overview.docsCollapsedState) {
-        const state = $scope.reviewPanel.overview.docsCollapsedState[doc]
+      for (const doc in ide.$scope.reviewPanel.overview.docsCollapsedState) {
+        const state = ide.$scope.reviewPanel.overview.docsCollapsedState[doc]
         if (state) {
           collapsedStates[doc] = state
         }
@@ -132,7 +132,7 @@ export default App.controller(
     )
 
     $scope.$on('layout:pdf:resize', (event, state) => {
-      $scope.reviewPanel.layoutToLeft =
+      ide.$scope.reviewPanel.layoutToLeft =
         state.east?.size < 220 || state.east?.initClosed
       $scope.$broadcast('review-panel:layout', false)
     })
@@ -156,10 +156,11 @@ export default App.controller(
     })
 
     $scope.$watch('project.members', function (members) {
-      $scope.reviewPanel.formattedProjectMembers = {}
+      ide.$scope.reviewPanel.formattedProjectMembers = {}
       if (($scope.project != null ? $scope.project.owner : undefined) != null) {
-        $scope.reviewPanel.formattedProjectMembers[$scope.project.owner._id] =
-          formatUser($scope.project.owner)
+        ide.$scope.reviewPanel.formattedProjectMembers[
+          $scope.project.owner._id
+        ] = formatUser($scope.project.owner)
       }
       if (
         ($scope.project != null ? $scope.project.members : undefined) != null
@@ -168,16 +169,18 @@ export default App.controller(
           const result = []
           for (const member of Array.from(members)) {
             if (member.privileges === 'readAndWrite') {
-              if ($scope.reviewPanel.trackChangesState[member._id] == null) {
+              if (
+                ide.$scope.reviewPanel.trackChangesState[member._id] == null
+              ) {
                 // An added member will have track changes enabled if track changes is on for everyone
                 _setUserTCState(
                   member._id,
-                  $scope.reviewPanel.trackChangesOnForEveryone,
+                  ide.$scope.reviewPanel.trackChangesOnForEveryone,
                   true
                 )
               }
               result.push(
-                ($scope.reviewPanel.formattedProjectMembers[member._id] =
+                (ide.$scope.reviewPanel.formattedProjectMembers[member._id] =
                   formatUser(member))
               )
             } else {
@@ -194,9 +197,9 @@ export default App.controller(
       content: '',
     }
 
-    $scope.users = {}
+    ide.$scope.users = $scope.users = {}
 
-    $scope.reviewPanelEventsBridge = new EventEmitter()
+    ide.$scope.reviewPanelEventsBridge = new EventEmitter()
 
     ide.socket.on('new-comment', function (thread_id, comment) {
       const thread = getThread(thread_id)
@@ -241,37 +244,37 @@ export default App.controller(
     const rangesTrackers = {}
 
     const getDocEntries = function (doc_id) {
-      if ($scope.reviewPanel.entries[doc_id] == null) {
-        $scope.reviewPanel.entries[doc_id] = {}
+      if (ide.$scope.reviewPanel.entries[doc_id] == null) {
+        ide.$scope.reviewPanel.entries[doc_id] = {}
       }
-      return $scope.reviewPanel.entries[doc_id]
+      return ide.$scope.reviewPanel.entries[doc_id]
     }
 
     const getDocResolvedComments = function (doc_id) {
-      if ($scope.reviewPanel.resolvedComments[doc_id] == null) {
-        $scope.reviewPanel.resolvedComments[doc_id] = {}
+      if (ide.$scope.reviewPanel.resolvedComments[doc_id] == null) {
+        ide.$scope.reviewPanel.resolvedComments[doc_id] = {}
       }
-      return $scope.reviewPanel.resolvedComments[doc_id]
+      return ide.$scope.reviewPanel.resolvedComments[doc_id]
     }
 
     function getThread(thread_id) {
-      if ($scope.reviewPanel.commentThreads[thread_id] == null) {
-        $scope.reviewPanel.commentThreads[thread_id] = { messages: [] }
+      if (ide.$scope.reviewPanel.commentThreads[thread_id] == null) {
+        ide.$scope.reviewPanel.commentThreads[thread_id] = { messages: [] }
       }
-      return $scope.reviewPanel.commentThreads[thread_id]
+      return ide.$scope.reviewPanel.commentThreads[thread_id]
     }
 
     function getChangeTracker(doc_id) {
       if (rangesTrackers[doc_id] == null) {
         rangesTrackers[doc_id] = new RangesTracker()
         rangesTrackers[doc_id].resolvedThreadIds =
-          $scope.reviewPanel.resolvedThreadIds
+          ide.$scope.reviewPanel.resolvedThreadIds
       }
       return rangesTrackers[doc_id]
     }
 
     let scrollbar = {}
-    $scope.reviewPanelEventsBridge.on(
+    ide.$scope.reviewPanelEventsBridge.on(
       'aceScrollbarVisibilityChanged',
       function (isVisible, scrollbarWidth) {
         scrollbar = { isVisible, scrollbarWidth }
@@ -282,7 +285,7 @@ export default App.controller(
     function updateScrollbar() {
       if (
         scrollbar.isVisible &&
-        $scope.reviewPanel.subView === $scope.SubViews.CUR_FILE &&
+        ide.$scope.reviewPanel.subView === $scope.SubViews.CUR_FILE &&
         !$scope.editor.showRichText
       ) {
         return $reviewPanelEl.css('right', `${scrollbar.scrollbarWidth}px`)
@@ -310,11 +313,11 @@ export default App.controller(
       }
       if (!open) {
         // Always show current file when not open, but save current state
-        $scope.reviewPanel.openSubView = $scope.reviewPanel.subView
-        $scope.reviewPanel.subView = $scope.SubViews.CUR_FILE
+        ide.$scope.reviewPanel.openSubView = ide.$scope.reviewPanel.subView
+        ide.$scope.reviewPanel.subView = $scope.SubViews.CUR_FILE
       } else {
         // Reset back to what we had when previously open
-        $scope.reviewPanel.subView = $scope.reviewPanel.openSubView
+        ide.$scope.reviewPanel.subView = ide.$scope.reviewPanel.openSubView
       }
       return $timeout(function () {
         $scope.$broadcast('review-panel:toggle')
@@ -342,8 +345,8 @@ export default App.controller(
       // replace any outdated info with this
       rangesTrackers[doc.doc_id] = doc.ranges
       rangesTrackers[doc.doc_id].resolvedThreadIds =
-        $scope.reviewPanel.resolvedThreadIds
-      $scope.reviewPanel.rangesTracker = rangesTrackers[doc.doc_id]
+        ide.$scope.reviewPanel.resolvedThreadIds
+      ide.$scope.reviewPanel.rangesTracker = rangesTrackers[doc.doc_id]
       if (old_doc != null) {
         old_doc.off('flipped_pending_to_inflight')
       }
@@ -354,7 +357,7 @@ export default App.controller(
     $scope.$watch(
       function () {
         const entries =
-          $scope.reviewPanel.entries[$scope.editor.open_doc_id] || {}
+          ide.$scope.reviewPanel.entries[$scope.editor.open_doc_id] || {}
         const permEntries = {}
         for (const entry in entries) {
           const entryData = entries[entry]
@@ -365,7 +368,7 @@ export default App.controller(
         return Object.keys(permEntries).length
       },
       nEntries =>
-        ($scope.reviewPanel.hasEntries =
+        (ide.$scope.reviewPanel.hasEntries =
           nEntries > 0 && $scope.project.features.trackChangesVisible)
     )
 
@@ -385,9 +388,12 @@ export default App.controller(
             const result = []
             for (const doc of Array.from(docs)) {
               if (
-                $scope.reviewPanel.overview.docsCollapsedState[doc.id] == null
+                ide.$scope.reviewPanel.overview.docsCollapsedState[doc.id] ==
+                null
               ) {
-                $scope.reviewPanel.overview.docsCollapsedState[doc.id] = false
+                ide.$scope.reviewPanel.overview.docsCollapsedState[
+                  doc.id
+                ] = false
               }
               if (doc.id !== $scope.editor.open_doc_id) {
                 // this is kept up to date in real-time, don't overwrite
@@ -404,17 +410,17 @@ export default App.controller(
         })
 
     function refreshOverviewPanel() {
-      $scope.reviewPanel.overview.loading = true
+      ide.$scope.reviewPanel.overview.loading = true
       return refreshRanges()
-        .then(() => ($scope.reviewPanel.overview.loading = false))
-        .catch(() => ($scope.reviewPanel.overview.loading = false))
+        .then(() => (ide.$scope.reviewPanel.overview.loading = false))
+        .catch(() => (ide.$scope.reviewPanel.overview.loading = false))
     }
 
-    $scope.refreshResolvedCommentsDropdown = function () {
-      $scope.reviewPanel.dropdown.loading = true
+    ide.$scope.refreshResolvedCommentsDropdown = function () {
+      ide.$scope.reviewPanel.dropdown.loading = true
       const q = refreshRanges()
-      q.then(() => ($scope.reviewPanel.dropdown.loading = false))
-      q.catch(() => ($scope.reviewPanel.dropdown.loading = false))
+      q.then(() => (ide.$scope.reviewPanel.dropdown.loading = false))
+      q.catch(() => (ide.$scope.reviewPanel.dropdown.loading = false))
       return q
     }
 
@@ -504,7 +510,7 @@ export default App.controller(
         let new_comment
         changed = true
         delete delete_changes[comment.id]
-        if ($scope.reviewPanel.resolvedThreadIds[comment.op.t]) {
+        if (ide.$scope.reviewPanel.resolvedThreadIds[comment.op.t]) {
           new_comment =
             resolvedComments[comment.id] != null
               ? resolvedComments[comment.id]
@@ -567,9 +573,9 @@ export default App.controller(
         const doc_id = $scope.editor.open_doc_id
         const entries = getDocEntries(doc_id)
         // All selected changes will be added to this array.
-        $scope.reviewPanel.selectedEntryIds = []
+        ide.$scope.reviewPanel.selectedEntryIds = []
         // Count of user-visible changes, i.e. an aggregated change will count as one.
-        $scope.reviewPanel.nVisibleSelectedChanges = 0
+        ide.$scope.reviewPanel.nVisibleSelectedChanges = 0
         delete entries['add-comment']
         delete entries['bulk-actions']
 
@@ -591,7 +597,7 @@ export default App.controller(
           let isChangeEntryAndWithinSelection = false
           if (
             entry.type === 'comment' &&
-            !$scope.reviewPanel.resolvedThreadIds[entry.thread_id]
+            !ide.$scope.reviewPanel.resolvedThreadIds[entry.thread_id]
           ) {
             entry.focused =
               entry.offset <= selection_offset_start &&
@@ -624,9 +630,9 @@ export default App.controller(
 
           if (isChangeEntryAndWithinSelection) {
             for (const entry_id of Array.from(entry.entry_ids)) {
-              $scope.reviewPanel.selectedEntryIds.push(entry_id)
+              ide.$scope.reviewPanel.selectedEntryIds.push(entry_id)
             }
-            $scope.reviewPanel.nVisibleSelectedChanges++
+            ide.$scope.reviewPanel.nVisibleSelectedChanges++
           }
         }
 
@@ -638,17 +644,21 @@ export default App.controller(
       }
     )
 
-    $scope.acceptChanges = function (change_ids) {
+    ide.$scope.acceptChanges = function (change_ids) {
       _doAcceptChanges(change_ids)
       eventTracking.sendMB('rp-changes-accepted', {
-        view: $scope.ui.reviewPanelOpen ? $scope.reviewPanel.subView : 'mini',
+        view: $scope.ui.reviewPanelOpen
+          ? ide.$scope.reviewPanel.subView
+          : 'mini',
       })
     }
 
-    $scope.rejectChanges = function (change_ids) {
+    ide.$scope.rejectChanges = function (change_ids) {
       _doRejectChanges(change_ids)
       eventTracking.sendMB('rp-changes-rejected', {
-        view: $scope.ui.reviewPanelOpen ? $scope.reviewPanel.subView : 'mini',
+        view: $scope.ui.reviewPanelOpen
+          ? ide.$scope.reviewPanel.subView
+          : 'mini',
       })
     }
 
@@ -657,11 +667,11 @@ export default App.controller(
     // the review panel is visible when hovering over its indicator when the
     // review panel is minimized. See issue #8057.
     $scope.mouseEnterIndicator = function () {
-      $scope.reviewPanel.entryHover = true
+      ide.$scope.reviewPanel.entryHover = true
     }
 
     $scope.mouseLeaveIndicator = function () {
-      $scope.reviewPanel.entryHover = false
+      ide.$scope.reviewPanel.entryHover = false
     }
 
     function _doAcceptChanges(change_ids) {
@@ -679,24 +689,28 @@ export default App.controller(
     }
 
     const bulkAccept = function () {
-      _doAcceptChanges($scope.reviewPanel.selectedEntryIds.slice())
+      _doAcceptChanges(ide.$scope.reviewPanel.selectedEntryIds.slice())
       eventTracking.sendMB('rp-bulk-accept', {
-        view: $scope.ui.reviewPanelOpen ? $scope.reviewPanel.subView : 'mini',
-        nEntries: $scope.reviewPanel.nVisibleSelectedChanges,
+        view: $scope.ui.reviewPanelOpen
+          ? ide.$scope.reviewPanel.subView
+          : 'mini',
+        nEntries: ide.$scope.reviewPanel.nVisibleSelectedChanges,
       })
     }
 
     const bulkReject = function () {
-      _doRejectChanges($scope.reviewPanel.selectedEntryIds.slice())
+      _doRejectChanges(ide.$scope.reviewPanel.selectedEntryIds.slice())
       eventTracking.sendMB('rp-bulk-reject', {
-        view: $scope.ui.reviewPanelOpen ? $scope.reviewPanel.subView : 'mini',
-        nEntries: $scope.reviewPanel.nVisibleSelectedChanges,
+        view: $scope.ui.reviewPanelOpen
+          ? ide.$scope.reviewPanel.subView
+          : 'mini',
+        nEntries: ide.$scope.reviewPanel.nVisibleSelectedChanges,
       })
     }
 
-    $scope.showBulkAcceptDialog = () => showBulkActionsDialog(true)
+    ide.$scope.showBulkAcceptDialog = () => showBulkActionsDialog(true)
 
-    $scope.showBulkRejectDialog = () => showBulkActionsDialog(false)
+    ide.$scope.showBulkRejectDialog = () => showBulkActionsDialog(false)
 
     const showBulkActionsDialog = isAccept =>
       $modal
@@ -708,7 +722,7 @@ export default App.controller(
               return isAccept
             },
             nChanges() {
-              return $scope.reviewPanel.nVisibleSelectedChanges
+              return ide.$scope.reviewPanel.nVisibleSelectedChanges
             },
           },
           scope: $scope.$new(),
@@ -726,7 +740,7 @@ export default App.controller(
       return $scope.toggleReviewPanel()
     }
 
-    $scope.addNewComment = function (e) {
+    ide.$scope.addNewComment = function (e) {
       e.preventDefault()
       $scope.$broadcast('comment:start_adding')
       return $scope.toggleReviewPanel()
@@ -754,7 +768,7 @@ export default App.controller(
       return $timeout(() => $scope.$broadcast('review-panel:layout'))
     }
 
-    $scope.submitNewComment = function (content) {
+    ide.$scope.submitNewComment = function (content) {
       if (content == null || content === '') {
         return
       }
@@ -798,7 +812,7 @@ export default App.controller(
       return $timeout(() => $scope.$broadcast('review-panel:layout'))
     }
 
-    $scope.submitReply = function (entry, entry_id) {
+    ide.$scope.submitReply = function (entry, entry_id) {
       const { thread_id } = entry
       const content = entry.replyContent
       $http
@@ -814,7 +828,9 @@ export default App.controller(
         )
 
       const trackingMetadata = {
-        view: $scope.ui.reviewPanelOpen ? $scope.reviewPanel.subView : 'mini',
+        view: $scope.ui.reviewPanelOpen
+          ? ide.$scope.reviewPanel.subView
+          : 'mini',
         size: entry.replyContent.length,
         thread: thread_id,
       }
@@ -833,7 +849,8 @@ export default App.controller(
       return $scope.$broadcast('review-panel:layout')
     }
 
-    $scope.resolveComment = function (entry, entry_id) {
+    ide.$scope.resolveComment = function (doc_id, entry_id) {
+      const entry = getDocEntries(doc_id)[entry_id]
       entry.focused = false
       $http.post(
         `/project/${$scope.project_id}/thread/${entry.thread_id}/resolve`,
@@ -841,11 +858,13 @@ export default App.controller(
       )
       _onCommentResolved(entry.thread_id, ide.$scope.user)
       eventTracking.sendMB('rp-comment-resolve', {
-        view: $scope.ui.reviewPanelOpen ? $scope.reviewPanel.subView : 'mini',
+        view: $scope.ui.reviewPanelOpen
+          ? ide.$scope.reviewPanel.subView
+          : 'mini',
       })
     }
 
-    $scope.unresolveComment = function (thread_id) {
+    ide.$scope.unresolveComment = function (thread_id) {
       _onCommentReopened(thread_id)
       $http.post(`/project/${$scope.project_id}/thread/${thread_id}/reopen`, {
         _csrf: window.csrfToken,
@@ -861,7 +880,7 @@ export default App.controller(
       thread.resolved = true
       thread.resolved_by_user = formatUser(user)
       thread.resolved_at = new Date().toISOString()
-      $scope.reviewPanel.resolvedThreadIds[thread_id] = true
+      ide.$scope.reviewPanel.resolvedThreadIds[thread_id] = true
       $scope.$broadcast('comment:resolve_threads', [thread_id])
       dispatchReviewPanelEvent('comment:resolve_threads', [thread_id])
     }
@@ -874,14 +893,14 @@ export default App.controller(
       delete thread.resolved
       delete thread.resolved_by_user
       delete thread.resolved_at
-      delete $scope.reviewPanel.resolvedThreadIds[thread_id]
+      delete ide.$scope.reviewPanel.resolvedThreadIds[thread_id]
       $scope.$broadcast('comment:unresolve_thread', thread_id)
       dispatchReviewPanelEvent('comment:unresolve_thread', thread_id)
     }
 
     function _onThreadDeleted(thread_id) {
-      delete $scope.reviewPanel.resolvedThreadIds[thread_id]
-      delete $scope.reviewPanel.commentThreads[thread_id]
+      delete ide.$scope.reviewPanel.resolvedThreadIds[thread_id]
+      delete ide.$scope.reviewPanel.commentThreads[thread_id]
       $scope.$broadcast('comment:remove', thread_id)
       dispatchReviewPanelEvent('comment:remove', thread_id)
     }
@@ -908,7 +927,7 @@ export default App.controller(
       return updateEntries()
     }
 
-    $scope.deleteThread = function (entry_id, doc_id, thread_id) {
+    ide.$scope.deleteThread = function (entry_id, doc_id, thread_id) {
       _onThreadDeleted(thread_id)
       $http({
         method: 'DELETE',
@@ -920,22 +939,22 @@ export default App.controller(
       eventTracking.sendMB('rp-comment-delete')
     }
 
-    $scope.saveEdit = function (thread_id, comment) {
+    ide.$scope.saveEdit = function (thread_id, comment_id, content) {
       $http.post(
-        `/project/${$scope.project_id}/thread/${thread_id}/messages/${comment.id}/edit`,
+        `/project/${$scope.project_id}/thread/${thread_id}/messages/${comment_id}/edit`,
         {
-          content: comment.content,
+          content,
           _csrf: window.csrfToken,
         }
       )
       return $timeout(() => $scope.$broadcast('review-panel:layout'))
     }
 
-    $scope.deleteComment = function (thread_id, comment) {
-      _onCommentDeleted(thread_id, comment.id)
+    ide.$scope.deleteComment = function (thread_id, comment_id) {
+      _onCommentDeleted(thread_id, comment_id)
       $http({
         method: 'DELETE',
-        url: `/project/${$scope.project_id}/thread/${thread_id}/messages/${comment.id}`,
+        url: `/project/${$scope.project_id}/thread/${thread_id}/messages/${comment_id}`,
         headers: {
           'X-CSRF-Token': window.csrfToken,
         },
@@ -944,17 +963,17 @@ export default App.controller(
     }
 
     $scope.setSubView = function (subView) {
-      $scope.reviewPanel.subView = subView
+      ide.$scope.reviewPanel.subView = subView
       eventTracking.sendMB('rp-subview-change', { subView })
     }
 
-    $scope.gotoEntry = (doc_id, entry) =>
-      ide.editorManager.openDocId(doc_id, { gotoOffset: entry.offset })
+    ide.$scope.gotoEntry = (doc_id, entry_offset) =>
+      ide.editorManager.openDocId(doc_id, { gotoOffset: entry_offset })
 
     $scope.toggleFullTCStateCollapse = function () {
       if ($scope.project.features.trackChanges) {
-        return ($scope.reviewPanel.fullTCStateCollapsed =
-          !$scope.reviewPanel.fullTCStateCollapsed)
+        return (ide.$scope.reviewPanel.fullTCStateCollapsed =
+          !ide.$scope.reviewPanel.fullTCStateCollapsed)
       } else {
         _sendAnalytics()
         return $scope.openTrackChangesUpgradeModal()
@@ -976,10 +995,10 @@ export default App.controller(
       if (isLocal == null) {
         isLocal = false
       }
-      if ($scope.reviewPanel.trackChangesState[userId] == null) {
-        $scope.reviewPanel.trackChangesState[userId] = {}
+      if (ide.$scope.reviewPanel.trackChangesState[userId] == null) {
+        ide.$scope.reviewPanel.trackChangesState[userId] = {}
       }
-      const state = $scope.reviewPanel.trackChangesState[userId]
+      const state = ide.$scope.reviewPanel.trackChangesState[userId]
 
       if (
         state.syncState == null ||
@@ -1006,7 +1025,7 @@ export default App.controller(
       if (isLocal == null) {
         isLocal = false
       }
-      $scope.reviewPanel.trackChangesOnForEveryone = newValue
+      ide.$scope.reviewPanel.trackChangesOnForEveryone = newValue
       const { project } = $scope
       for (const member of Array.from(project.members)) {
         _setUserTCState(member._id, newValue, isLocal)
@@ -1019,7 +1038,7 @@ export default App.controller(
       if (isLocal == null) {
         isLocal = false
       }
-      $scope.reviewPanel.trackChangesOnForGuests = newValue
+      ide.$scope.reviewPanel.trackChangesOnForGuests = newValue
       if (
         currentUserType() === UserTypes.GUEST ||
         currentUserType() === UserTypes.ANONYMOUS
@@ -1030,15 +1049,15 @@ export default App.controller(
 
     const applyClientTrackChangesStateToServer = function () {
       const data = {}
-      if ($scope.reviewPanel.trackChangesOnForEveryone) {
+      if (ide.$scope.reviewPanel.trackChangesOnForEveryone) {
         data.on = true
       } else {
         data.on_for = {}
-        for (const userId in $scope.reviewPanel.trackChangesState) {
-          const userState = $scope.reviewPanel.trackChangesState[userId]
+        for (const userId in ide.$scope.reviewPanel.trackChangesState) {
+          const userState = ide.$scope.reviewPanel.trackChangesState[userId]
           data.on_for[userId] = userState.value
         }
-        if ($scope.reviewPanel.trackChangesOnForGuests) {
+        if (ide.$scope.reviewPanel.trackChangesOnForGuests) {
           data.on_for_guests = true
         }
       }
@@ -1052,7 +1071,7 @@ export default App.controller(
         return _setGuestsTCState(state)
       } else {
         const { project } = $scope
-        $scope.reviewPanel.trackChangesOnForEveryone = false
+        ide.$scope.reviewPanel.trackChangesOnForEveryone = false
         _setGuestsTCState(state.__guests__ === true)
         for (const member of Array.from(project.members)) {
           _setUserTCState(
@@ -1069,18 +1088,18 @@ export default App.controller(
       }
     }
 
-    $scope.toggleTrackChangesForEveryone = function (onForEveryone) {
+    ide.$scope.toggleTrackChangesForEveryone = function (onForEveryone) {
       _setEveryoneTCState(onForEveryone, true)
       _setGuestsTCState(onForEveryone, true)
       return applyClientTrackChangesStateToServer()
     }
 
-    $scope.toggleTrackChangesForGuests = function (onForGuests) {
+    ide.$scope.toggleTrackChangesForGuests = function (onForGuests) {
       _setGuestsTCState(onForGuests, true)
       return applyClientTrackChangesStateToServer()
     }
 
-    $scope.toggleTrackChangesForUser = function (onForUser, userId) {
+    ide.$scope.toggleTrackChangesForUser = function (onForUser, userId) {
       _setUserTCState(userId, onForUser, true)
       return applyClientTrackChangesStateToServer()
     }
@@ -1099,13 +1118,13 @@ export default App.controller(
         return
       }
       return $scope.toggleTrackChangesForUser(
-        !$scope.reviewPanel.trackChangesState[ide.$scope.user.id].value,
+        !ide.$scope.reviewPanel.trackChangesState[ide.$scope.user.id].value,
         ide.$scope.user.id
       )
     }
 
     const setGuestFeatureBasedOnProjectAccessLevel = projectPublicAccessLevel =>
-      ($scope.reviewPanel.trackChangesForGuestsAvailable =
+      (ide.$scope.reviewPanel.trackChangesForGuestsAvailable =
         projectPublicAccessLevel === 'tokenBased')
 
     const onToggleTrackChangesForGuestsAvailability = function (available) {
@@ -1113,10 +1132,10 @@ export default App.controller(
       if (available) {
         return
       }
-      if (!$scope.reviewPanel.trackChangesOnForGuests) {
+      if (!ide.$scope.reviewPanel.trackChangesOnForGuests) {
         return
       } // Already turned off
-      if ($scope.reviewPanel.trackChangesOnForEveryone) {
+      if (ide.$scope.reviewPanel.trackChangesOnForEveryone) {
         return
       } // Overrides guest setting
       return $scope.toggleTrackChangesForGuests(false)
@@ -1153,7 +1172,6 @@ export default App.controller(
 
     let _refreshingRangeUsers = false
     const _refreshedForUserIds = {}
-
     function refreshChangeUsers(refresh_for_user_id) {
       if (refresh_for_user_id != null) {
         if (_refreshedForUserIds[refresh_for_user_id] != null) {
@@ -1174,7 +1192,7 @@ export default App.controller(
         .then(function (response) {
           const users = response.data
           _refreshingRangeUsers = false
-          $scope.users = {}
+          ide.$scope.users = $scope.users = {}
           // Always include ourself, since if we submit an op, we might need to display info
           // about it locally before it has been flushed through the server
           if (
@@ -1198,7 +1216,6 @@ export default App.controller(
     }
 
     let _threadsLoaded = false
-
     function ensureThreadsAreLoaded() {
       if (_threadsLoaded) {
         // We get any updates in real time so only need to load them once.
@@ -1211,9 +1228,8 @@ export default App.controller(
         .then(function (response) {
           const threads = response.data
           ide.$scope.loadingThreads = false
-          for (const thread_id in $scope.reviewPanel.resolvedThreadIds) {
-            const _ = $scope.reviewPanel.resolvedThreadIds[thread_id]
-            delete $scope.reviewPanel.resolvedThreadIds[thread_id]
+          for (const thread_id in ide.$scope.reviewPanel.resolvedThreadIds) {
+            delete ide.$scope.reviewPanel.resolvedThreadIds[thread_id]
           }
           for (const thread_id in threads) {
             const thread = threads[thread_id]
@@ -1222,11 +1238,11 @@ export default App.controller(
             }
             if (thread.resolved_by_user != null) {
               thread.resolved_by_user = formatUser(thread.resolved_by_user)
-              $scope.reviewPanel.resolvedThreadIds[thread_id] = true
+              ide.$scope.reviewPanel.resolvedThreadIds[thread_id] = true
               $scope.$broadcast('comment:resolve_threads', [thread_id])
             }
           }
-          $scope.reviewPanel.commentThreads = threads
+          ide.$scope.reviewPanel.commentThreads = threads
           dispatchReviewPanelEvent('loaded_threads')
           return $timeout(() => $scope.$broadcast('review-panel:layout'))
         })
@@ -1293,7 +1309,7 @@ export default App.controller(
 
       switch (type) {
         case 'line-height': {
-          $scope.reviewPanel.rendererData.lineHeight = payload
+          ide.$scope.reviewPanel.rendererData.lineHeight = payload
           $scope.$broadcast('review-panel:layout')
           break
         }
@@ -1325,11 +1341,14 @@ export default App.controller(
         }
 
         case 'toggle-review-panel': {
-          ide.toggleReviewPanel()
+          $scope.toggleReviewPanel()
           break
         }
       }
     })
+
+    // Add methods somewhere that React can see them
+    $scope.reviewPanel.saveEdit = $scope.saveEdit
   }
 )
 

+ 112 - 0
services/web/frontend/stylesheets/app/editor/review-panel.less

@@ -1109,3 +1109,115 @@ button when (@is-overleaf-light = true) {
 .rp-flex-block {
   display: block;
 }
+
+// CM6-specific review panel rules
+.ol-cm-review-panel {
+  display: block;
+  flex-shrink: 0;
+
+  .rp-size-expanded & {
+    display: flex;
+    flex-direction: column;
+    width: @review-panel-width;
+    overflow: visible;
+    border-left-width: 1px;
+  }
+
+  .rp-size-mini & {
+    width: @review-off-width;
+    z-index: 6;
+    border-left-width: 1px;
+  }
+
+  background-color: @rp-bg-blue;
+  border-left: solid 0 @rp-border-grey;
+  font-family: @font-family-base;
+  line-height: @line-height-base;
+  font-size: @rp-base-font-size;
+  color: @rp-type-blue;
+  z-index: 6;
+
+  position: relative;
+  box-sizing: content-box;
+  flex-direction: column;
+
+  .review-panel-toolbar {
+    & .review-panel-toolbar-collapse-button {
+      border: none;
+      background: none;
+      padding: 0;
+    }
+  }
+
+  .rp-state-current-file & {
+    .review-panel-tools {
+      display: flex;
+      flex-direction: column;
+      justify-content: space-between;
+      position: absolute;
+      top: 0;
+      bottom: 0;
+      left: 0;
+      right: 0;
+    }
+
+    .review-panel-toolbar {
+      position: sticky;
+      top: 0;
+    }
+
+    .rp-nav {
+      position: sticky;
+      bottom: 0;
+    }
+
+    .review-panel-toggler {
+      position: sticky;
+      top: 0;
+    }
+  }
+
+  .rp-state-overview & {
+    position: sticky;
+    top: 0;
+    display: flex;
+    flex-direction: column;
+
+    .review-panel-toggler {
+      position: absolute;
+      top: 0;
+      bottom: 0;
+      left: 0;
+    }
+  }
+
+  .rp-nav-item {
+    background: none;
+  }
+
+  .rp-entry-list {
+    display: inline-flex;
+    flex-direction: column;
+
+    .rp-overview-file {
+      .rp-overview-file-entries {
+        //height: auto;
+        transition: height ease-in-out 0.15s; //, display 0.15s 0s;
+      }
+
+      .rp-overview-file-num-entries {
+        display: none;
+      }
+
+      &.rp-overview-file-collapse {
+        .rp-overview-file-num-entries {
+          display: inline;
+        }
+
+        .rp-overview-file-entries {
+          // height: 0;
+        }
+      }
+    }
+  }
+}