Procházet zdrojové kódy

Remove 'review-panel-redesign' split test and old code (#24235)

GitOrigin-RevId: 1f3d4a9a51429591a82391a9bee3cfdf226bc9c8
Alf Eaton před 1 rokem
rodič
revize
b8d74c6ae0
82 změnil soubory, kde provedl 140 přidání a 7196 odebrání
  1. 0 1
      services/web/app/src/Features/Project/ProjectController.js
  2. 2 2
      services/web/cypress/support/component.ts
  3. 0 19
      services/web/frontend/extracted-translations.json
  4. 0 3
      services/web/frontend/js/features/ide-react/context/ide-react-context.tsx
  5. 0 1654
      services/web/frontend/js/features/ide-react/context/review-panel/hooks/use-review-panel-state.ts
  6. 0 32
      services/web/frontend/js/features/ide-react/context/review-panel/hooks/useLayoutToLeft.tsx
  7. 0 8
      services/web/frontend/js/features/ide-react/scope-adapters/review-panel-context-adapter.ts
  8. 0 1
      services/web/frontend/js/features/review-panel-new/components/review-panel-header.tsx
  9. 12 0
      services/web/frontend/js/features/review-panel-new/components/review-panel-new.tsx
  10. 1 1
      services/web/frontend/js/features/review-panel-new/components/review-panel-track-changes-menu-button.tsx
  11. 1 1
      services/web/frontend/js/features/review-panel-new/components/review-panel-track-changes-menu.tsx
  12. 0 0
      services/web/frontend/js/features/review-panel-new/components/track-changes-toggle.tsx
  13. 4 4
      services/web/frontend/js/features/review-panel-new/components/upgrade-track-changes-modal.tsx
  14. 8 3
      services/web/frontend/js/features/review-panel-new/context/changes-users-context.tsx
  15. 0 5
      services/web/frontend/js/features/review-panel-new/context/review-panel-providers.tsx
  16. 15 7
      services/web/frontend/js/features/review-panel-new/context/threads-context.tsx
  17. 0 0
      services/web/frontend/js/features/review-panel-new/images/teaser-track-changes.gif
  18. 0 0
      services/web/frontend/js/features/review-panel-new/images/teaser-track-changes.mp4
  19. 3 5
      services/web/frontend/js/features/source-editor/components/codemirror-editor.tsx
  20. 0 5
      services/web/frontend/js/features/source-editor/components/review-panel/add-comment-button.tsx
  21. 0 20
      services/web/frontend/js/features/source-editor/components/review-panel/container.tsx
  22. 0 53
      services/web/frontend/js/features/source-editor/components/review-panel/current-file-container.tsx
  23. 0 109
      services/web/frontend/js/features/source-editor/components/review-panel/editor-widgets/editor-widgets.tsx
  24. 0 32
      services/web/frontend/js/features/source-editor/components/review-panel/editor-widgets/toggle-widget.tsx
  25. 0 165
      services/web/frontend/js/features/source-editor/components/review-panel/entries/add-comment-entry.tsx
  26. 0 159
      services/web/frontend/js/features/source-editor/components/review-panel/entries/aggregate-change-entry.tsx
  27. 0 55
      services/web/frontend/js/features/source-editor/components/review-panel/entries/bulk-actions-entry/bulk-actions-entry.tsx
  28. 0 24
      services/web/frontend/js/features/source-editor/components/review-panel/entries/bulk-actions-entry/bulk-actions.tsx
  29. 0 91
      services/web/frontend/js/features/source-editor/components/review-panel/entries/bulk-actions-entry/modal.tsx
  30. 0 161
      services/web/frontend/js/features/source-editor/components/review-panel/entries/change-entry.tsx
  31. 0 185
      services/web/frontend/js/features/source-editor/components/review-panel/entries/comment-entry.tsx
  32. 0 121
      services/web/frontend/js/features/source-editor/components/review-panel/entries/comment.tsx
  33. 0 19
      services/web/frontend/js/features/source-editor/components/review-panel/entries/entry-actions.tsx
  34. 0 7
      services/web/frontend/js/features/source-editor/components/review-panel/entries/entry-callout.tsx
  35. 0 42
      services/web/frontend/js/features/source-editor/components/review-panel/entries/entry-container.tsx
  36. 0 30
      services/web/frontend/js/features/source-editor/components/review-panel/entries/entry-indicator.tsx
  37. 0 96
      services/web/frontend/js/features/source-editor/components/review-panel/entries/overview-file-entries.tsx
  38. 0 127
      services/web/frontend/js/features/source-editor/components/review-panel/entries/resolved-comment-entry.tsx
  39. 0 109
      services/web/frontend/js/features/source-editor/components/review-panel/entry.tsx
  40. 0 33
      services/web/frontend/js/features/source-editor/components/review-panel/hooks/use-entry-click.ts
  41. 0 69
      services/web/frontend/js/features/source-editor/components/review-panel/hooks/use-indicator-hover.ts
  42. 0 65
      services/web/frontend/js/features/source-editor/components/review-panel/nav.tsx
  43. 0 43
      services/web/frontend/js/features/source-editor/components/review-panel/overview-container.tsx
  44. 0 68
      services/web/frontend/js/features/source-editor/components/review-panel/overview-file.tsx
  45. 0 502
      services/web/frontend/js/features/source-editor/components/review-panel/positioned-entries.tsx
  46. 0 37
      services/web/frontend/js/features/source-editor/components/review-panel/review-panel-content.tsx
  47. 0 19
      services/web/frontend/js/features/source-editor/components/review-panel/review-panel-migration.tsx
  48. 0 18
      services/web/frontend/js/features/source-editor/components/review-panel/review-panel.tsx
  49. 0 29
      services/web/frontend/js/features/source-editor/components/review-panel/toggler.tsx
  50. 0 133
      services/web/frontend/js/features/source-editor/components/review-panel/toolbar/resolved-comments-dropdown.tsx
  51. 0 40
      services/web/frontend/js/features/source-editor/components/review-panel/toolbar/resolved-comments-scroller.tsx
  52. 0 82
      services/web/frontend/js/features/source-editor/components/review-panel/toolbar/toggle-menu.tsx
  53. 0 30
      services/web/frontend/js/features/source-editor/components/review-panel/toolbar/toolbar.tsx
  54. 0 130
      services/web/frontend/js/features/source-editor/components/review-panel/toolbar/track-changes-menu.tsx
  55. 0 18
      services/web/frontend/js/features/source-editor/components/review-panel/types/base-change-entry-props.ts
  56. 0 31
      services/web/frontend/js/features/source-editor/components/review-panel/utils/compare-props-with-shallow-array-compare.ts
  57. 7 10
      services/web/frontend/js/features/source-editor/components/toolbar/toolbar-items.tsx
  58. 0 45
      services/web/frontend/js/features/source-editor/context/review-panel/review-panel-context.tsx
  59. 0 105
      services/web/frontend/js/features/source-editor/context/review-panel/types/review-panel-state.ts
  60. 0 616
      services/web/frontend/js/features/source-editor/extensions/changes/change-manager.ts
  61. 1 3
      services/web/frontend/js/features/source-editor/extensions/draw-selection.ts
  62. 1 5
      services/web/frontend/js/features/source-editor/extensions/index.ts
  63. 0 5
      services/web/frontend/js/features/source-editor/extensions/review-tooltip.ts
  64. 5 20
      services/web/frontend/js/features/source-editor/extensions/shortcuts.ts
  65. 0 284
      services/web/frontend/js/features/source-editor/extensions/track-changes.ts
  66. 0 7
      services/web/frontend/js/features/source-editor/extensions/track-detached-comments.ts
  67. 0 28
      services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts
  68. 0 11
      services/web/frontend/stories/source-editor/source-editor.stories.tsx
  69. 0 1
      services/web/frontend/stylesheets/bootstrap-5/pages/all.scss
  70. 42 3
      services/web/frontend/stylesheets/bootstrap-5/pages/editor/review-panel-new.scss
  71. 0 1109
      services/web/frontend/stylesheets/bootstrap-5/pages/editor/review-panel.scss
  72. 0 16
      services/web/locales/en.json
  73. 3 1
      services/web/test/frontend/features/review-panel/review-panel.spec.tsx
  74. 0 81
      services/web/test/frontend/features/review-panel/unit/compare-props-with-shallow-array-compare.tests.ts
  75. 11 13
      services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-command-tooltip.spec.tsx
  76. 15 0
      services/web/test/frontend/features/source-editor/helpers/mock-doc.ts
  77. 7 9
      services/web/test/frontend/features/source-editor/helpers/mock-scope.ts
  78. 2 2
      services/web/test/frontend/infrastructure/i18n.spec.tsx
  79. 0 14
      services/web/types/review-panel/api.ts
  80. 0 78
      services/web/types/review-panel/entry.ts
  81. 0 25
      services/web/types/review-panel/review-panel.ts
  82. 0 2
      services/web/types/utils.ts

+ 0 - 1
services/web/app/src/Features/Project/ProjectController.js

@@ -345,7 +345,6 @@ const _ProjectController = {
       'pdf-caching-prefetching',
       'pdf-caching-prefetching',
       'revert-file',
       'revert-file',
       'revert-project',
       'revert-project',
-      'review-panel-redesign',
       !anonymous && 'ro-mirror-on-client',
       !anonymous && 'ro-mirror-on-client',
       'track-pdf-download',
       'track-pdf-download',
       !anonymous && 'writefull-oauth-promotion',
       !anonymous && 'writefull-oauth-promotion',

+ 2 - 2
services/web/cypress/support/component.ts

@@ -1,6 +1,6 @@
 import 'cypress-plugin-tab'
 import 'cypress-plugin-tab'
 import { resetMeta } from './ct/window' // needs to be before i18n
 import { resetMeta } from './ct/window' // needs to be before i18n
-import '@/i18n'
+import localesPromise from '@/i18n'
 import './shared/commands'
 import './shared/commands'
 import './shared/exceptions'
 import './shared/exceptions'
 import './ct/commands'
 import './ct/commands'
@@ -8,5 +8,5 @@ import './ct/codemirror'
 import '../../test/frontend/helpers/bootstrap-5'
 import '../../test/frontend/helpers/bootstrap-5'
 
 
 beforeEach(function () {
 beforeEach(function () {
-  resetMeta()
+  cy.wrap(localesPromise).then(resetMeta)
 })
 })

+ 0 - 19
services/web/frontend/extracted-translations.json

@@ -32,8 +32,6 @@
   "about_to_leave_projects": "",
   "about_to_leave_projects": "",
   "about_to_trash_projects": "",
   "about_to_trash_projects": "",
   "about_writefull": "",
   "about_writefull": "",
-  "accept": "",
-  "accept_all": "",
   "accept_and_continue": "",
   "accept_and_continue": "",
   "accept_change": "",
   "accept_change": "",
   "accept_change_error_description": "",
   "accept_change_error_description": "",
@@ -147,7 +145,6 @@
   "are_you_affiliated_with_an_institution": "",
   "are_you_affiliated_with_an_institution": "",
   "are_you_getting_an_undefined_control_sequence_error": "",
   "are_you_getting_an_undefined_control_sequence_error": "",
   "are_you_still_at": "",
   "are_you_still_at": "",
-  "are_you_sure": "",
   "are_you_sure_you_want_to_cancel_add_on": "",
   "are_you_sure_you_want_to_cancel_add_on": "",
   "as_email": "",
   "as_email": "",
   "ask_proj_owner_to_unlink_from_current_github": "",
   "ask_proj_owner_to_unlink_from_current_github": "",
@@ -185,8 +182,6 @@
   "blocked_filename": "",
   "blocked_filename": "",
   "blog": "",
   "blog": "",
   "browser": "",
   "browser": "",
-  "bulk_accept_confirm": "",
-  "bulk_reject_confirm": "",
   "by_subscribing_you_agree_to_our_terms_of_service": "",
   "by_subscribing_you_agree_to_our_terms_of_service": "",
   "can_add_tracked_changes_and_comments": "",
   "can_add_tracked_changes_and_comments": "",
   "can_edit": "",
   "can_edit": "",
@@ -273,7 +268,6 @@
   "column_width_is_custom_click_to_resize": "",
   "column_width_is_custom_click_to_resize": "",
   "column_width_is_x_click_to_resize": "",
   "column_width_is_x_click_to_resize": "",
   "comment": "",
   "comment": "",
-  "comment_submit_error": "",
   "commit": "",
   "commit": "",
   "common": "",
   "common": "",
   "common_causes_of_compile_timeouts_include": "",
   "common_causes_of_compile_timeouts_include": "",
@@ -522,7 +516,6 @@
   "error_opening_document_detail": "",
   "error_opening_document_detail": "",
   "error_performing_request": "",
   "error_performing_request": "",
   "error_processing_file": "",
   "error_processing_file": "",
-  "error_submitting_comment": "",
   "example_project": "",
   "example_project": "",
   "existing_plan_active_until_term_end": "",
   "existing_plan_active_until_term_end": "",
   "expand": "",
   "expand": "",
@@ -724,7 +717,6 @@
   "history_view_a11y_description": "",
   "history_view_a11y_description": "",
   "history_view_all": "",
   "history_view_all": "",
   "history_view_labels": "",
   "history_view_labels": "",
-  "hit_enter_to_reply": "",
   "home": "",
   "home": "",
   "hotkey_add_a_comment": "",
   "hotkey_add_a_comment": "",
   "hotkey_autocomplete_menu": "",
   "hotkey_autocomplete_menu": "",
@@ -980,7 +972,6 @@
   "managed_users_terms": "",
   "managed_users_terms": "",
   "managers_management": "",
   "managers_management": "",
   "managing_your_subscription": "",
   "managing_your_subscription": "",
-  "mark_as_resolved": "",
   "marked_as_resolved": "",
   "marked_as_resolved": "",
   "math_display": "",
   "math_display": "",
   "math_inline": "",
   "math_inline": "",
@@ -1051,7 +1042,6 @@
   "no_actions": "",
   "no_actions": "",
   "no_borders": "",
   "no_borders": "",
   "no_caption": "",
   "no_caption": "",
-  "no_comments": "",
   "no_comments_or_suggestions": "",
   "no_comments_or_suggestions": "",
   "no_existing_password": "",
   "no_existing_password": "",
   "no_folder": "",
   "no_folder": "",
@@ -1071,7 +1061,6 @@
   "no_preview_available": "",
   "no_preview_available": "",
   "no_projects": "",
   "no_projects": "",
   "no_resolved_comments": "",
   "no_resolved_comments": "",
-  "no_resolved_threads": "",
   "no_search_results": "",
   "no_search_results": "",
   "no_selection_select_file": "",
   "no_selection_select_file": "",
   "no_symbols_found": "",
   "no_symbols_found": "",
@@ -1284,7 +1273,6 @@
   "push_sharelatex_changes_to_github": "",
   "push_sharelatex_changes_to_github": "",
   "push_to_github_pull_to_overleaf": "",
   "push_to_github_pull_to_overleaf": "",
   "quoted_text": "",
   "quoted_text": "",
-  "quoted_text_in": "",
   "raw_logs": "",
   "raw_logs": "",
   "raw_logs_description": "",
   "raw_logs_description": "",
   "react_history_tutorial_content": "",
   "react_history_tutorial_content": "",
@@ -1331,8 +1319,6 @@
   "refresh_page_after_starting_free_trial": "",
   "refresh_page_after_starting_free_trial": "",
   "refreshing": "",
   "refreshing": "",
   "regards": "",
   "regards": "",
-  "reject": "",
-  "reject_all": "",
   "reject_change": "",
   "reject_change": "",
   "reject_selected_changes": "",
   "reject_selected_changes": "",
   "relink_your_account": "",
   "relink_your_account": "",
@@ -1377,7 +1363,6 @@
   "resending_confirmation_code": "",
   "resending_confirmation_code": "",
   "resending_confirmation_email": "",
   "resending_confirmation_email": "",
   "resize": "",
   "resize": "",
-  "resolve": "",
   "resolve_comment": "",
   "resolve_comment": "",
   "resolve_comment_error_message": "",
   "resolve_comment_error_message": "",
   "resolve_comment_error_title": "",
   "resolve_comment_error_title": "",
@@ -1514,7 +1499,6 @@
   "shared_with_you": "",
   "shared_with_you": "",
   "sharelatex_beta_program": "",
   "sharelatex_beta_program": "",
   "shortcut_to_open_advanced_reference_search": "",
   "shortcut_to_open_advanced_reference_search": "",
-  "show_all": "",
   "show_all_projects": "",
   "show_all_projects": "",
   "show_document_preamble": "",
   "show_document_preamble": "",
   "show_file_tree": "",
   "show_file_tree": "",
@@ -1672,9 +1656,6 @@
   "take_survey": "",
   "take_survey": "",
   "tc_everyone": "",
   "tc_everyone": "",
   "tc_guests": "",
   "tc_guests": "",
-  "tc_switch_everyone_tip": "",
-  "tc_switch_guests_tip": "",
-  "tc_switch_user_tip": "",
   "tell_the_project_owner_and_ask_them_to_upgrade": "",
   "tell_the_project_owner_and_ask_them_to_upgrade": "",
   "template": "",
   "template": "",
   "template_description": "",
   "template_description": "",

+ 0 - 3
services/web/frontend/js/features/ide-react/context/ide-react-context.tsx

@@ -9,7 +9,6 @@ import React, {
 } from 'react'
 } from 'react'
 import { ReactScopeValueStore } from '@/features/ide-react/scope-value-store/react-scope-value-store'
 import { ReactScopeValueStore } from '@/features/ide-react/scope-value-store/react-scope-value-store'
 import populateLayoutScope from '@/features/ide-react/scope-adapters/layout-context-adapter'
 import populateLayoutScope from '@/features/ide-react/scope-adapters/layout-context-adapter'
-import populateReviewPanelScope from '@/features/ide-react/scope-adapters/review-panel-context-adapter'
 import { IdeProvider } from '@/shared/context/ide-context'
 import { IdeProvider } from '@/shared/context/ide-context'
 import {
 import {
   createIdeEventEmitter,
   createIdeEventEmitter,
@@ -73,10 +72,8 @@ export function createReactScopeValueStore(projectId: string) {
   populateLayoutScope(scopeStore)
   populateLayoutScope(scopeStore)
   populateProjectScope(scopeStore)
   populateProjectScope(scopeStore)
   populatePdfScope(scopeStore)
   populatePdfScope(scopeStore)
-  populateReviewPanelScope(scopeStore)
 
 
   scopeStore.allowNonExistentPath('hasLintingError')
   scopeStore.allowNonExistentPath('hasLintingError')
-  scopeStore.allowNonExistentPath('loadingThreads')
 
 
   return scopeStore
   return scopeStore
 }
 }

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

@@ -1,1654 +0,0 @@
-import { useState, useEffect, useMemo, useCallback, useRef } from 'react'
-import { useTranslation } from 'react-i18next'
-import { isEqual, cloneDeep } from 'lodash'
-import usePersistedState from '@/shared/hooks/use-persisted-state'
-import useScopeValue from '../../../../../shared/hooks/use-scope-value'
-import useSocketListener from '@/features/ide-react/hooks/use-socket-listener'
-import useAbortController from '@/shared/hooks/use-abort-controller'
-import useScopeEventEmitter from '@/shared/hooks/use-scope-event-emitter'
-import useLayoutToLeft from '@/features/ide-react/context/review-panel/hooks/useLayoutToLeft'
-import { sendMB } from '@/infrastructure/event-tracking'
-import {
-  dispatchReviewPanelLayout as handleLayoutChange,
-  UpdateType,
-} from '@/features/source-editor/extensions/changes/change-manager'
-import { useProjectContext } from '@/shared/context/project-context'
-import { useLayoutContext } from '@/shared/context/layout-context'
-import { useUserContext } from '@/shared/context/user-context'
-import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
-import { useConnectionContext } from '@/features/ide-react/context/connection-context'
-import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
-import { useModalsContext } from '@/features/ide-react/context/modals-context'
-import {
-  EditorManager,
-  useEditorManagerContext,
-} from '@/features/ide-react/context/editor-manager-context'
-import { debugConsole } from '@/utils/debugging'
-import { deleteJSON, getJSON, postJSON } from '@/infrastructure/fetch-json'
-import RangesTracker from '@overleaf/ranges-tracker'
-import type * as ReviewPanel from '@/features/source-editor/context/review-panel/types/review-panel-state'
-import {
-  CommentId,
-  ReviewPanelCommentThreadMessage,
-  ReviewPanelCommentThreads,
-  ReviewPanelDocEntries,
-  SubView,
-  ThreadId,
-} from '../../../../../../../types/review-panel/review-panel'
-import { UserId } from '../../../../../../../types/user'
-import { PublicAccessLevel } from '../../../../../../../types/public-access-level'
-import {
-  DeepReadonly,
-  Entries,
-  MergeAndOverride,
-} from '../../../../../../../types/utils'
-import { ReviewPanelCommentThread } from '../../../../../../../types/review-panel/comment-thread'
-import { DocId } from '../../../../../../../types/project-settings'
-import {
-  ReviewPanelAddCommentEntry,
-  ReviewPanelAggregateChangeEntry,
-  ReviewPanelBulkActionsEntry,
-  ReviewPanelChangeEntry,
-  ReviewPanelCommentEntry,
-  ReviewPanelEntry,
-} from '../../../../../../../types/review-panel/entry'
-import {
-  ReviewPanelCommentThreadMessageApi,
-  ReviewPanelCommentThreadsApi,
-} from '../../../../../../../types/review-panel/api'
-import { DateString } from '../../../../../../../types/helpers/date'
-import {
-  Change,
-  CommentOperation,
-  EditOperation,
-} from '../../../../../../../types/change'
-import { RangesTrackerWithResolvedThreadIds } from '@/features/ide-react/editor/document-container'
-import getMeta from '@/utils/meta'
-import { useEditorContext } from '@/shared/context/editor-context'
-import { getHueForUserId } from '@/shared/utils/colors'
-
-const dispatchReviewPanelEvent = (type: string, payload?: any) => {
-  window.dispatchEvent(
-    new CustomEvent('review-panel:event', {
-      detail: { type, payload },
-    })
-  )
-}
-
-const formatUser = (user: any): any => {
-  let isSelf, name
-  const id =
-    (user != null ? user._id : undefined) ||
-    (user != null ? user.id : undefined)
-
-  if (id == null) {
-    return {
-      email: null,
-      name: 'Anonymous',
-      isSelf: false,
-      hue: getHueForUserId(),
-      avatar_text: 'A',
-    }
-  }
-  if (id === getMeta('ol-user_id')) {
-    name = 'You'
-    isSelf = true
-  } else {
-    name = [user.first_name, user.last_name]
-      .filter(n => n != null && n !== '')
-      .join(' ')
-    if (name === '') {
-      name =
-        (user.email != null ? user.email.split('@')[0] : undefined) || 'Unknown'
-    }
-    isSelf = false
-  }
-  return {
-    id,
-    email: user.email,
-    name,
-    isSelf,
-    hue: getHueForUserId(id),
-    avatar_text: [user.first_name, user.last_name]
-      .filter(n => n != null)
-      .map(n => n[0])
-      .join(''),
-  }
-}
-
-const formatComment = (
-  comment: ReviewPanelCommentThreadMessageApi
-): ReviewPanelCommentThreadMessage => {
-  const commentTyped = comment as unknown as ReviewPanelCommentThreadMessage
-  commentTyped.user = formatUser(comment.user)
-  commentTyped.timestamp = new Date(comment.timestamp)
-  return commentTyped
-}
-
-function useReviewPanelState(): ReviewPanel.ReviewPanelState {
-  const { t } = useTranslation()
-  const { reviewPanelOpen, setReviewPanelOpen, setMiniReviewPanelVisible } =
-    useLayoutContext()
-  const { projectId } = useIdeReactContext()
-  const project = useProjectContext()
-  const user = useUserContext()
-  const { socket } = useConnectionContext()
-  const {
-    features: { trackChangesVisible, trackChanges },
-  } = project
-  const { isRestrictedTokenMember } = useEditorContext()
-  const {
-    openDocWithId,
-    currentDocument,
-    currentDocumentId,
-    wantTrackChanges,
-    setWantTrackChanges,
-  } = useEditorManagerContext() as MergeAndOverride<
-    EditorManager,
-    { currentDocumentId: DocId }
-  >
-  // TODO permissions to be removed from the review panel context. It currently acts just as a proxy.
-  const permissions = usePermissionsContext()
-  const { showGenericMessageModal } = useModalsContext()
-  const addCommentEmitter = useScopeEventEmitter('comment:start_adding')
-
-  const layoutToLeft = useLayoutToLeft('.ide-react-editor-panel')
-  const [subView, setSubView] =
-    useState<ReviewPanel.Value<'subView'>>('cur_file')
-  const [isOverviewLoading, setIsOverviewLoading] =
-    useState<ReviewPanel.Value<'isOverviewLoading'>>(false)
-  // All selected changes. If an aggregated change (insertion + deletion) is selected, the two ids
-  // will be present. The length of this array will differ from the count below (see explanation).
-  const selectedEntryIds = useRef<ThreadId[]>([])
-  // A count of user-facing selected changes. An aggregated change (insertion + deletion) will count
-  // as only one.
-  const [nVisibleSelectedChanges, setNVisibleSelectedChanges] =
-    useState<ReviewPanel.Value<'nVisibleSelectedChanges'>>(0)
-  const [collapsed, setCollapsed] = usePersistedState<
-    ReviewPanel.Value<'collapsed'>
-  >(`docs_collapsed_state:${projectId}`, {}, false, true)
-  const [commentThreads, setCommentThreads] = useState<
-    ReviewPanel.Value<'commentThreads'>
-  >({})
-  const [entries, setEntries] = useState<ReviewPanel.Value<'entries'>>({})
-  const [users, setUsers] = useScopeValue<ReviewPanel.Value<'users'>>('users')
-  const [resolvedComments, setResolvedComments] = useState<
-    ReviewPanel.Value<'resolvedComments'>
-  >({})
-
-  const [shouldCollapse, setShouldCollapse] =
-    useState<ReviewPanel.Value<'shouldCollapse'>>(true)
-  const [lineHeight, setLineHeight] =
-    useState<ReviewPanel.Value<'lineHeight'>>(0)
-
-  const [formattedProjectMembers, setFormattedProjectMembers] = useState<
-    ReviewPanel.Value<'formattedProjectMembers'>
-  >({})
-  const [trackChangesState, setTrackChangesState] = useState<
-    ReviewPanel.Value<'trackChangesState'>
-  >({})
-  const [trackChangesOnForEveryone, setTrackChangesOnForEveryone] =
-    useState<ReviewPanel.Value<'trackChangesOnForEveryone'>>(false)
-  const [trackChangesOnForGuests, setTrackChangesOnForGuests] =
-    useState<ReviewPanel.Value<'trackChangesOnForGuests'>>(false)
-  const [trackChangesForGuestsAvailable, setTrackChangesForGuestsAvailable] =
-    useState<ReviewPanel.Value<'trackChangesForGuestsAvailable'>>(false)
-
-  const [resolvedThreadIds, setResolvedThreadIds] = useState<
-    Record<ThreadId, boolean>
-  >({})
-
-  const [loadingThreads, setLoadingThreads] =
-    useScopeValue<boolean>('loadingThreads')
-
-  const loadThreadsController = useAbortController()
-  const threadsLoadedOnceRef = useRef(false)
-  const loadingThreadsInProgressRef = useRef(false)
-  const ensureThreadsAreLoaded = useCallback(() => {
-    if (threadsLoadedOnceRef.current) {
-      // We get any updates in real time so only need to load them once.
-      return
-    }
-    threadsLoadedOnceRef.current = true
-    loadingThreadsInProgressRef.current = true
-
-    return getJSON(`/project/${projectId}/threads`, {
-      signal: loadThreadsController.signal,
-    })
-      .then(threads => {
-        setLoadingThreads(false)
-        const tempResolvedThreadIds: typeof resolvedThreadIds = {}
-        const threadsEntries = Object.entries(threads) as [
-          [
-            ThreadId,
-            MergeAndOverride<
-              ReviewPanelCommentThread,
-              ReviewPanelCommentThreadsApi[ThreadId]
-            >,
-          ],
-        ]
-        for (const [threadId, thread] of threadsEntries) {
-          for (const comment of thread.messages) {
-            formatComment(comment)
-          }
-          if (thread.resolved_by_user) {
-            thread.resolved_by_user = formatUser(thread.resolved_by_user)
-            tempResolvedThreadIds[threadId] = true
-          }
-        }
-        setResolvedThreadIds(tempResolvedThreadIds)
-        setCommentThreads(threads as unknown as ReviewPanelCommentThreads)
-
-        dispatchReviewPanelEvent('loaded_threads')
-        handleLayoutChange({ async: true })
-
-        return {
-          resolvedThreadIds: tempResolvedThreadIds,
-          commentThreads: threads,
-        }
-      })
-      .catch(debugConsole.error)
-      .finally(() => {
-        loadingThreadsInProgressRef.current = false
-      })
-  }, [loadThreadsController.signal, projectId, setLoadingThreads])
-
-  const rangesTrackers = useRef<
-    Record<DocId, RangesTrackerWithResolvedThreadIds>
-  >({})
-  const refreshingRangeUsers = useRef(false)
-  const refreshedForUserIds = useRef(new Set<UserId>())
-  const refreshChangeUsers = useCallback(
-    (userId: UserId | null) => {
-      if (userId != null) {
-        if (refreshedForUserIds.current.has(userId)) {
-          // We've already tried to refresh to get this user id, so stop it looping
-          return
-        }
-        refreshedForUserIds.current.add(userId)
-      }
-
-      // Only do one refresh at once
-      if (refreshingRangeUsers.current) {
-        return
-      }
-      refreshingRangeUsers.current = true
-
-      getJSON(`/project/${projectId}/changes/users`)
-        .then(usersResponse => {
-          refreshingRangeUsers.current = false
-          const tempUsers = {} as ReviewPanel.Value<'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 (user?.id) {
-            tempUsers[user.id] = formatUser(user)
-          }
-
-          for (const user of usersResponse) {
-            if (user.id) {
-              tempUsers[user.id] = formatUser(user)
-            }
-          }
-
-          setUsers(tempUsers)
-        })
-        .catch(error => {
-          refreshingRangeUsers.current = false
-          debugConsole.error(error)
-        })
-    },
-    [projectId, setUsers, user]
-  )
-
-  const getChangeTracker = useCallback(
-    (docId: DocId) => {
-      if (!rangesTrackers.current[docId]) {
-        const rangesTracker = new RangesTracker([], [])
-        ;(
-          rangesTracker as RangesTrackerWithResolvedThreadIds
-        ).resolvedThreadIds = { ...resolvedThreadIds }
-        rangesTrackers.current[docId] =
-          rangesTracker as RangesTrackerWithResolvedThreadIds
-      }
-      return rangesTrackers.current[docId]!
-    },
-    [resolvedThreadIds]
-  )
-
-  const getDocEntries = useCallback(
-    (docId: DocId) => {
-      return entries[docId] ?? ({} as ReviewPanelDocEntries)
-    },
-    [entries]
-  )
-
-  const getDocResolvedComments = useCallback(
-    (docId: DocId) => {
-      return resolvedComments[docId] ?? ({} as ReviewPanelDocEntries)
-    },
-    [resolvedComments]
-  )
-
-  const getThread = useCallback(
-    (threadId: ThreadId) => {
-      return (
-        commentThreads[threadId] ??
-        ({ messages: [] } as ReviewPanelCommentThread)
-      )
-    },
-    [commentThreads]
-  )
-
-  const updateEntries = useCallback(
-    async (docId: DocId) => {
-      const rangesTracker = getChangeTracker(docId)
-      const docEntries = cloneDeep(getDocEntries(docId))
-      const docResolvedComments = cloneDeep(getDocResolvedComments(docId))
-      // Assume we'll delete everything until we see it, then we'll remove it from this object
-      const deleteChanges = new Set<keyof ReviewPanelDocEntries>()
-
-      for (const [id, change] of Object.entries(docEntries) as Entries<
-        typeof docEntries
-      >) {
-        if (
-          'entry_ids' in change &&
-          id !== 'add-comment' &&
-          id !== 'bulk-actions'
-        ) {
-          for (const entryId of change.entry_ids) {
-            deleteChanges.add(entryId)
-          }
-        }
-      }
-      for (const [, change] of Object.entries(docResolvedComments) as Entries<
-        typeof docResolvedComments
-      >) {
-        if ('entry_ids' in change) {
-          for (const entryId of change.entry_ids) {
-            deleteChanges.add(entryId)
-          }
-        }
-      }
-
-      let potentialAggregate = false
-      let prevInsertion = null
-
-      for (const change of rangesTracker.changes as any[]) {
-        if (
-          potentialAggregate &&
-          change.op.d &&
-          change.op.p === prevInsertion.op.p + prevInsertion.op.i.length &&
-          change.metadata.user_id === prevInsertion.metadata.user_id
-        ) {
-          // An actual aggregate op.
-          const aggregateChangeEntries = docEntries as Record<
-            string,
-            ReviewPanelAggregateChangeEntry
-          >
-          aggregateChangeEntries[prevInsertion.id].type = 'aggregate-change'
-          aggregateChangeEntries[prevInsertion.id].metadata.replaced_content =
-            change.op.d
-          aggregateChangeEntries[prevInsertion.id].entry_ids.push(change.id)
-        } else {
-          if (docEntries[change.id] == null) {
-            docEntries[change.id] = {} as ReviewPanelEntry
-          }
-          deleteChanges.delete(change.id)
-          const newEntry: Partial<ReviewPanelChangeEntry> = {
-            type: change.op.i ? 'insert' : 'delete',
-            entry_ids: [change.id],
-            content: change.op.i || change.op.d,
-            offset: change.op.p,
-            metadata: change.metadata,
-          }
-          for (const [key, value] of Object.entries(newEntry) as Entries<
-            typeof newEntry
-          >) {
-            const entriesTyped = docEntries[change.id] as Record<any, any>
-            entriesTyped[key] = value
-          }
-        }
-
-        if (change.op.i) {
-          potentialAggregate = true
-          prevInsertion = change
-        } else {
-          potentialAggregate = false
-          prevInsertion = null
-        }
-
-        if (!users[change.metadata.user_id]) {
-          if (!isRestrictedTokenMember) {
-            refreshChangeUsers(change.metadata.user_id)
-          }
-        }
-      }
-
-      let localResolvedThreadIds = resolvedThreadIds
-
-      if (!isRestrictedTokenMember && rangesTracker.comments.length > 0) {
-        const threadsLoadResult = await ensureThreadsAreLoaded()
-        if (threadsLoadResult?.resolvedThreadIds) {
-          localResolvedThreadIds = threadsLoadResult.resolvedThreadIds
-        }
-      } else if (loadingThreads) {
-        // ensure that tracked changes are highlighted even if no comments are loaded
-        setLoadingThreads(false)
-        dispatchReviewPanelEvent('loaded_threads')
-      }
-
-      if (!loadingThreadsInProgressRef.current) {
-        for (const comment of rangesTracker.comments) {
-          const commentId = comment.id as ThreadId
-          deleteChanges.delete(commentId)
-
-          let newComment: any
-          if (localResolvedThreadIds[comment.op.t]) {
-            docResolvedComments[commentId] ??= {} as ReviewPanelCommentEntry
-            newComment = docResolvedComments[commentId]
-            delete docEntries[commentId]
-          } else {
-            docEntries[commentId] ??= {} as ReviewPanelEntry
-            newComment = docEntries[commentId]
-            delete docResolvedComments[commentId]
-          }
-
-          newComment.type = 'comment'
-          newComment.thread_id = comment.op.t
-          newComment.entry_ids = [comment.id]
-          newComment.content = comment.op.c
-          newComment.offset = comment.op.p
-        }
-      }
-
-      deleteChanges.forEach(changeId => {
-        delete docEntries[changeId]
-        delete docResolvedComments[changeId]
-      })
-
-      setEntries(prev => {
-        return isEqual(prev[docId], docEntries)
-          ? prev
-          : { ...prev, [docId]: docEntries }
-      })
-      setResolvedComments(prev => {
-        return isEqual(prev[docId], docResolvedComments)
-          ? prev
-          : { ...prev, [docId]: docResolvedComments }
-      })
-
-      return docEntries
-    },
-    [
-      getChangeTracker,
-      getDocEntries,
-      getDocResolvedComments,
-      refreshChangeUsers,
-      resolvedThreadIds,
-      users,
-      ensureThreadsAreLoaded,
-      loadingThreads,
-      setLoadingThreads,
-      isRestrictedTokenMember,
-    ]
-  )
-
-  const regenerateTrackChangesId = useCallback(
-    (doc: typeof currentDocument) => {
-      if (doc) {
-        const currentChangeTracker = getChangeTracker(doc.doc_id as DocId)
-        const oldId = currentChangeTracker.getIdSeed()
-        const newId = RangesTracker.generateIdSeed()
-        currentChangeTracker.setIdSeed(newId)
-        doc.setTrackChangesIdSeeds({ pending: newId, inflight: oldId })
-      }
-    },
-    [getChangeTracker]
-  )
-
-  useEffect(() => {
-    if (!currentDocument) {
-      return
-    }
-    // The open doc range tracker is kept up to date in real-time so
-    // replace any outdated info with this
-    const rangesTracker = currentDocument.ranges!
-    ;(rangesTracker as RangesTrackerWithResolvedThreadIds).resolvedThreadIds = {
-      ...resolvedThreadIds,
-    }
-    rangesTrackers.current[currentDocument.doc_id as DocId] =
-      rangesTracker as RangesTrackerWithResolvedThreadIds
-    currentDocument.on('flipped_pending_to_inflight', () =>
-      regenerateTrackChangesId(currentDocument)
-    )
-    regenerateTrackChangesId(currentDocument)
-
-    return () => {
-      currentDocument.off('flipped_pending_to_inflight')
-    }
-  }, [currentDocument, regenerateTrackChangesId, resolvedThreadIds])
-
-  const currentUserType = useCallback((): 'member' | 'guest' | 'anonymous' => {
-    if (!user) {
-      return 'anonymous'
-    }
-    if (project.owner._id === user.id) {
-      return 'member'
-    }
-    for (const member of project.members as any[]) {
-      if (member._id === user.id) {
-        return 'member'
-      }
-    }
-    return 'guest'
-  }, [project.members, project.owner, user])
-
-  const applyClientTrackChangesStateToServer = useCallback(
-    (
-      trackChangesOnForEveryone: boolean,
-      trackChangesOnForGuests: boolean,
-      trackChangesState: ReviewPanel.Value<'trackChangesState'>
-    ) => {
-      const data: {
-        on?: boolean
-        on_for?: Record<UserId, boolean>
-        on_for_guests?: boolean
-      } = {}
-      if (trackChangesOnForEveryone) {
-        data.on = true
-      } else {
-        data.on_for = {}
-        const entries = Object.entries(trackChangesState) as Array<
-          [
-            UserId,
-            NonNullable<
-              (typeof trackChangesState)[keyof typeof trackChangesState]
-            >,
-          ]
-        >
-        for (const [userId, { value }] of entries) {
-          data.on_for[userId] = value
-        }
-        if (trackChangesOnForGuests) {
-          data.on_for_guests = true
-        }
-      }
-      postJSON(`/project/${projectId}/track_changes`, {
-        body: data,
-      }).catch(debugConsole.error)
-    },
-    [projectId]
-  )
-
-  const setGuestsTCState = useCallback(
-    (newValue: boolean) => {
-      setTrackChangesOnForGuests(newValue)
-      if (currentUserType() === 'guest' || currentUserType() === 'anonymous') {
-        setWantTrackChanges(newValue)
-      }
-    },
-    [currentUserType, setWantTrackChanges]
-  )
-
-  const setUserTCState = useCallback(
-    (
-      trackChangesState: DeepReadonly<ReviewPanel.Value<'trackChangesState'>>,
-      userId: UserId,
-      newValue: boolean,
-      isLocal = false
-    ) => {
-      const newTrackChangesState: ReviewPanel.Value<'trackChangesState'> = {
-        ...trackChangesState,
-      }
-      const state =
-        newTrackChangesState[userId] ??
-        ({} as NonNullable<(typeof newTrackChangesState)[UserId]>)
-      newTrackChangesState[userId] = state
-
-      if (state.syncState == null || state.syncState === 'synced') {
-        state.value = newValue
-        state.syncState = 'synced'
-      } else if (state.syncState === 'pending' && state.value === newValue) {
-        state.syncState = 'synced'
-      } else if (isLocal) {
-        state.value = newValue
-        state.syncState = 'pending'
-      }
-
-      setTrackChangesState(newTrackChangesState)
-
-      if (userId === user.id) {
-        setWantTrackChanges(newValue)
-      }
-
-      return newTrackChangesState
-    },
-    [setWantTrackChanges, user.id]
-  )
-
-  const setEveryoneTCState = useCallback(
-    (newValue: boolean, isLocal = false) => {
-      setTrackChangesOnForEveryone(newValue)
-      let newTrackChangesState: ReviewPanel.Value<'trackChangesState'> = {
-        ...trackChangesState,
-      }
-      for (const member of project.members as any[]) {
-        newTrackChangesState = setUserTCState(
-          newTrackChangesState,
-          member._id,
-          newValue,
-          isLocal
-        )
-      }
-      setGuestsTCState(newValue)
-
-      newTrackChangesState = setUserTCState(
-        newTrackChangesState,
-        project.owner._id,
-        newValue,
-        isLocal
-      )
-
-      return { trackChangesState: newTrackChangesState }
-    },
-    [
-      project.members,
-      project.owner._id,
-      setGuestsTCState,
-      setUserTCState,
-      trackChangesState,
-    ]
-  )
-
-  const toggleTrackChangesForEveryone = useCallback<
-    ReviewPanel.UpdaterFn<'toggleTrackChangesForEveryone'>
-  >(
-    (onForEveryone: boolean) => {
-      const { trackChangesState } = setEveryoneTCState(onForEveryone, true)
-      setGuestsTCState(onForEveryone)
-      applyClientTrackChangesStateToServer(
-        onForEveryone,
-        onForEveryone,
-        trackChangesState
-      )
-    },
-    [applyClientTrackChangesStateToServer, setEveryoneTCState, setGuestsTCState]
-  )
-
-  const toggleTrackChangesForGuests = useCallback<
-    ReviewPanel.UpdaterFn<'toggleTrackChangesForGuests'>
-  >(
-    (onForGuests: boolean) => {
-      setGuestsTCState(onForGuests)
-      applyClientTrackChangesStateToServer(
-        trackChangesOnForEveryone,
-        onForGuests,
-        trackChangesState
-      )
-    },
-    [
-      applyClientTrackChangesStateToServer,
-      setGuestsTCState,
-      trackChangesOnForEveryone,
-      trackChangesState,
-    ]
-  )
-
-  const toggleTrackChangesForUser = useCallback<
-    ReviewPanel.UpdaterFn<'toggleTrackChangesForUser'>
-  >(
-    (onForUser: boolean, userId: UserId) => {
-      const newTrackChangesState = setUserTCState(
-        trackChangesState,
-        userId,
-        onForUser,
-        true
-      )
-      applyClientTrackChangesStateToServer(
-        trackChangesOnForEveryone,
-        trackChangesOnForGuests,
-        newTrackChangesState
-      )
-    },
-    [
-      applyClientTrackChangesStateToServer,
-      setUserTCState,
-      trackChangesOnForEveryone,
-      trackChangesOnForGuests,
-      trackChangesState,
-    ]
-  )
-
-  const applyTrackChangesStateToClient = useCallback(
-    (state: boolean | Record<UserId | '__guests__', boolean>) => {
-      if (typeof state === 'boolean') {
-        setEveryoneTCState(state)
-        setGuestsTCState(state)
-      } else {
-        setTrackChangesOnForEveryone(false)
-        // TODO
-        // @ts-ignore
-        setGuestsTCState(state.__guests__ === true)
-
-        let newTrackChangesState: ReviewPanel.Value<'trackChangesState'> = {
-          ...trackChangesState,
-        }
-        for (const member of project.members as any[]) {
-          newTrackChangesState = setUserTCState(
-            newTrackChangesState,
-            member._id,
-            !!state[member._id]
-          )
-        }
-        newTrackChangesState = setUserTCState(
-          newTrackChangesState,
-          project.owner._id,
-          !!state[project.owner._id]
-        )
-        return newTrackChangesState
-      }
-    },
-    [
-      project.members,
-      project.owner._id,
-      setEveryoneTCState,
-      setGuestsTCState,
-      setUserTCState,
-      trackChangesState,
-    ]
-  )
-
-  const setGuestFeatureBasedOnProjectAccessLevel = (
-    projectPublicAccessLevel?: PublicAccessLevel
-  ) => {
-    setTrackChangesForGuestsAvailable(projectPublicAccessLevel === 'tokenBased')
-  }
-
-  useEffect(() => {
-    setGuestFeatureBasedOnProjectAccessLevel(project.publicAccessLevel)
-  }, [project.publicAccessLevel])
-
-  useEffect(() => {
-    if (
-      trackChangesForGuestsAvailable ||
-      !trackChangesOnForGuests ||
-      trackChangesOnForEveryone
-    ) {
-      return
-    }
-
-    // Overrides guest setting
-    toggleTrackChangesForGuests(false)
-  }, [
-    toggleTrackChangesForGuests,
-    trackChangesForGuestsAvailable,
-    trackChangesOnForEveryone,
-    trackChangesOnForGuests,
-  ])
-
-  const projectJoinedEffectExecuted = useRef(false)
-  useEffect(() => {
-    if (!projectJoinedEffectExecuted.current) {
-      projectJoinedEffectExecuted.current = true
-      requestAnimationFrame(() => {
-        if (trackChanges) {
-          applyTrackChangesStateToClient(project.trackChangesState)
-        } else {
-          applyTrackChangesStateToClient(false)
-        }
-        setGuestFeatureBasedOnProjectAccessLevel(project.publicAccessLevel)
-      })
-    }
-  }, [
-    applyTrackChangesStateToClient,
-    trackChanges,
-    project.publicAccessLevel,
-    project.trackChangesState,
-  ])
-
-  useEffect(() => {
-    setFormattedProjectMembers(prevState => {
-      const tempFormattedProjectMembers: typeof prevState = {}
-      if (project.owner) {
-        tempFormattedProjectMembers[project.owner._id] = formatUser(
-          project.owner
-        )
-      }
-      const members = project.members ?? []
-      for (const member of members) {
-        if (member.privileges === 'readAndWrite') {
-          if (!trackChangesState[member._id]) {
-            // An added member will have track changes enabled if track changes is on for everyone
-            setUserTCState(
-              trackChangesState,
-              member._id,
-              trackChangesOnForEveryone,
-              true
-            )
-          }
-          tempFormattedProjectMembers[member._id] = formatUser(member)
-        }
-      }
-      return tempFormattedProjectMembers
-    })
-  }, [
-    project.members,
-    project.owner,
-    setUserTCState,
-    trackChangesOnForEveryone,
-    trackChangesState,
-  ])
-
-  useSocketListener(
-    socket,
-    'toggle-track-changes',
-    applyTrackChangesStateToClient
-  )
-
-  const gotoEntry = useCallback(
-    (docId: DocId, entryOffset: number) => {
-      openDocWithId(docId, { gotoOffset: entryOffset })
-    },
-    [openDocWithId]
-  )
-
-  const view = reviewPanelOpen ? subView : 'mini'
-
-  const toggleReviewPanel = useCallback(() => {
-    if (!trackChangesVisible) {
-      return
-    }
-    setReviewPanelOpen(!reviewPanelOpen)
-    sendMB('rp-toggle-panel', {
-      value: !reviewPanelOpen,
-    })
-  }, [reviewPanelOpen, setReviewPanelOpen, trackChangesVisible])
-
-  const onCommentResolved = useCallback(
-    (threadId: ThreadId, user: any) => {
-      setCommentThreads(prevState => {
-        const thread = { ...getThread(threadId) }
-        thread.resolved = true
-        thread.resolved_by_user = formatUser(user)
-        thread.resolved_at = new Date().toISOString() as DateString
-        return { ...prevState, [threadId]: thread }
-      })
-      setResolvedThreadIds(prevState => ({ ...prevState, [threadId]: true }))
-      setTimeout(() => {
-        dispatchReviewPanelEvent('comment:resolve_threads', [threadId])
-      })
-    },
-    [getThread]
-  )
-
-  const resolveComment = useCallback(
-    (docId: DocId, entryId: ThreadId) => {
-      const docEntries = getDocEntries(docId)
-      const entry = docEntries[entryId] as ReviewPanelCommentEntry
-
-      setEntries(prevState => ({
-        ...prevState,
-        [docId]: {
-          ...prevState[docId],
-          [entryId]: {
-            ...prevState[docId][entryId],
-            focused: false,
-          },
-        },
-      }))
-
-      postJSON(
-        `/project/${projectId}/doc/${docId}/thread/${entry.thread_id}/resolve`
-      )
-      onCommentResolved(entry.thread_id, user)
-      sendMB('rp-comment-resolve', { view })
-    },
-    [getDocEntries, onCommentResolved, projectId, user, view]
-  )
-
-  const onCommentReopened = useCallback(
-    (threadId: ThreadId) => {
-      setCommentThreads(prevState => {
-        const {
-          resolved: _1,
-          resolved_by_user: _2,
-          resolved_at: _3,
-          ...thread
-        } = getThread(threadId)
-        return { ...prevState, [threadId]: thread }
-      })
-      setResolvedThreadIds(({ [threadId]: _, ...resolvedThreadIds }) => {
-        return resolvedThreadIds
-      })
-      setTimeout(() => {
-        dispatchReviewPanelEvent('comment:unresolve_thread', threadId)
-      })
-    },
-    [getThread]
-  )
-
-  const unresolveComment = useCallback(
-    (docId: DocId, threadId: ThreadId) => {
-      onCommentReopened(threadId)
-      const url = `/project/${projectId}/doc/${docId}/thread/${threadId}/reopen`
-      postJSON(url).catch(debugConsole.error)
-      sendMB('rp-comment-reopen')
-    },
-    [onCommentReopened, projectId]
-  )
-
-  const onThreadDeleted = useCallback((threadId: ThreadId) => {
-    setResolvedThreadIds(({ [threadId]: _, ...resolvedThreadIds }) => {
-      return resolvedThreadIds
-    })
-    setCommentThreads(({ [threadId]: _, ...commentThreads }) => {
-      return commentThreads
-    })
-    dispatchReviewPanelEvent('comment:remove', threadId)
-  }, [])
-
-  const deleteThread = useCallback(
-    (docId: DocId, threadId: ThreadId) => {
-      onThreadDeleted(threadId)
-      deleteJSON(`/project/${projectId}/doc/${docId}/thread/${threadId}`).catch(
-        debugConsole.error
-      )
-      sendMB('rp-comment-delete')
-    },
-    [onThreadDeleted, projectId]
-  )
-
-  const onCommentEdited = useCallback(
-    (threadId: ThreadId, commentId: CommentId, content: string) => {
-      setCommentThreads(prevState => {
-        const thread = { ...getThread(threadId) }
-        thread.messages = thread.messages.map(message => {
-          return message.id === commentId ? { ...message, content } : message
-        })
-        return { ...prevState, [threadId]: thread }
-      })
-    },
-    [getThread]
-  )
-
-  const saveEdit = useCallback(
-    (threadId: ThreadId, commentId: CommentId, content: string) => {
-      const url = `/project/${projectId}/thread/${threadId}/messages/${commentId}/edit`
-      postJSON(url, { body: { content } }).catch(debugConsole.error)
-      handleLayoutChange({ async: true })
-    },
-    [projectId]
-  )
-
-  const onCommentDeleted = useCallback(
-    (threadId: ThreadId, commentId: CommentId) => {
-      setCommentThreads(prevState => {
-        const thread = { ...getThread(threadId) }
-        thread.messages = thread.messages.filter(m => m.id !== commentId)
-        return { ...prevState, [threadId]: thread }
-      })
-    },
-    [getThread]
-  )
-
-  const deleteComment = useCallback(
-    (threadId: ThreadId, commentId: CommentId) => {
-      onCommentDeleted(threadId, commentId)
-      deleteJSON(
-        `/project/${projectId}/thread/${threadId}/messages/${commentId}`
-      ).catch(debugConsole.error)
-      handleLayoutChange({ async: true })
-    },
-    [onCommentDeleted, projectId]
-  )
-
-  const doAcceptChanges = useCallback(
-    (entryIds: ThreadId[]) => {
-      const url = `/project/${projectId}/doc/${currentDocumentId}/changes/accept`
-      postJSON(url, { body: { change_ids: entryIds } }).catch(
-        debugConsole.error
-      )
-      dispatchReviewPanelEvent('changes:accept', entryIds)
-    },
-    [currentDocumentId, projectId]
-  )
-
-  const acceptChanges = useCallback(
-    (entryIds: ThreadId[]) => {
-      doAcceptChanges(entryIds)
-      sendMB('rp-changes-accepted', { view })
-    },
-    [doAcceptChanges, view]
-  )
-
-  const doRejectChanges = useCallback((entryIds: ThreadId[]) => {
-    dispatchReviewPanelEvent('changes:reject', entryIds)
-  }, [])
-
-  const rejectChanges = useCallback(
-    (entryIds: ThreadId[]) => {
-      doRejectChanges(entryIds)
-      sendMB('rp-changes-rejected', { view })
-    },
-    [doRejectChanges, view]
-  )
-
-  const bulkAcceptActions = useCallback(() => {
-    doAcceptChanges(selectedEntryIds.current)
-    sendMB('rp-bulk-accept', { view, nEntries: nVisibleSelectedChanges })
-  }, [doAcceptChanges, nVisibleSelectedChanges, view])
-
-  const bulkRejectActions = useCallback(() => {
-    doRejectChanges(selectedEntryIds.current)
-    sendMB('rp-bulk-reject', { view, nEntries: nVisibleSelectedChanges })
-  }, [doRejectChanges, nVisibleSelectedChanges, view])
-
-  const refreshRanges = useCallback(() => {
-    type Doc = {
-      id: DocId
-      ranges: {
-        comments?: Change<CommentOperation>[]
-        changes?: Change<EditOperation>[]
-      }
-    }
-
-    return getJSON<Doc[]>(`/project/${projectId}/ranges`)
-      .then(docs => {
-        setCollapsed(prevState => {
-          const collapsed = { ...prevState }
-          docs.forEach(doc => {
-            if (collapsed[doc.id] == null) {
-              collapsed[doc.id] = false
-            }
-          })
-          return collapsed
-        })
-
-        docs.forEach(async doc => {
-          if (doc.id !== currentDocumentId) {
-            // this is kept up to date in real-time, don't overwrite
-            const rangesTracker = getChangeTracker(doc.id)
-            rangesTracker.comments = doc.ranges?.comments ?? []
-            rangesTracker.changes = doc.ranges?.changes ?? []
-          }
-        })
-
-        return Promise.all(docs.map(doc => updateEntries(doc.id)))
-      })
-      .catch(debugConsole.error)
-  }, [
-    currentDocumentId,
-    getChangeTracker,
-    projectId,
-    setCollapsed,
-    updateEntries,
-  ])
-
-  const handleSetSubview = useCallback((subView: SubView) => {
-    setSubView(subView)
-    sendMB('rp-subview-change', { subView })
-  }, [])
-
-  const submitReply = useCallback(
-    (threadId: ThreadId, replyContent: string) => {
-      const url = `/project/${projectId}/thread/${threadId}/messages`
-      postJSON(url, { body: { content: replyContent } }).catch(() => {
-        showGenericMessageModal(
-          t('error_submitting_comment'),
-          t('comment_submit_error')
-        )
-      })
-
-      const trackingMetadata = {
-        view,
-        size: replyContent.length,
-        thread: threadId,
-      }
-
-      setCommentThreads(prevState => ({
-        ...prevState,
-        [threadId]: { ...getThread(threadId), submitting: true },
-      }))
-      handleLayoutChange({ async: true })
-      sendMB('rp-comment-reply', trackingMetadata)
-    },
-    [getThread, projectId, showGenericMessageModal, t, view]
-  )
-
-  // TODO `submitNewComment` is partially localized in the `add-comment-entry` component.
-  const submitNewComment = useCallback(
-    (content: string) => {
-      if (!content) {
-        return
-      }
-
-      const entries = getDocEntries(currentDocumentId)
-      const addCommentEntry = entries['add-comment'] as
-        | ReviewPanelAddCommentEntry
-        | undefined
-
-      if (!addCommentEntry) {
-        return
-      }
-
-      const { offset, length } = addCommentEntry
-      const threadId = RangesTracker.generateId() as ThreadId
-      setCommentThreads(prevState => ({
-        ...prevState,
-        [threadId]: { ...getThread(threadId), submitting: true },
-      }))
-
-      const url = `/project/${projectId}/thread/${threadId}/messages`
-      postJSON(url, { body: { content } })
-        .then(() => {
-          dispatchReviewPanelEvent('comment:add', { threadId, offset, length })
-          handleLayoutChange({ async: true })
-          sendMB('rp-new-comment', { size: content.length })
-        })
-        .catch(() => {
-          showGenericMessageModal(
-            t('error_submitting_comment'),
-            t('comment_submit_error')
-          )
-        })
-    },
-    [
-      currentDocumentId,
-      getDocEntries,
-      getThread,
-      projectId,
-      showGenericMessageModal,
-      t,
-    ]
-  )
-
-  const [isAddingComment, setIsAddingComment] = useState(false)
-  const [navHeight, setNavHeight] = useState(0)
-  const [toolbarHeight, setToolbarHeight] = useState(0)
-  const [layoutSuspended, setLayoutSuspended] = useState(false)
-  const [unsavedComment, setUnsavedComment] = useState('')
-
-  useEffect(() => {
-    if (!trackChangesVisible) {
-      setReviewPanelOpen(false)
-    }
-  }, [trackChangesVisible, setReviewPanelOpen])
-
-  const hasEntries = useMemo(() => {
-    const docEntries = getDocEntries(currentDocumentId)
-    const permEntriesCount = Object.keys(docEntries).filter(key => {
-      return !['add-comment', 'bulk-actions'].includes(key)
-    }).length
-    return permEntriesCount > 0 && trackChangesVisible
-  }, [currentDocumentId, getDocEntries, trackChangesVisible])
-
-  useEffect(() => {
-    setMiniReviewPanelVisible(!reviewPanelOpen && !!hasEntries)
-  }, [reviewPanelOpen, hasEntries, setMiniReviewPanelVisible])
-
-  // listen for events from the CodeMirror 6 track changes extension
-  useEffect(() => {
-    const toggleTrackChangesFromKbdShortcut = () => {
-      const userId = user.id
-      if (trackChangesVisible && trackChanges && userId) {
-        const state = trackChangesState[userId]
-        if (state) {
-          toggleTrackChangesForUser(!state.value, userId)
-        }
-      }
-    }
-
-    const editorLineHeightChanged = (payload: typeof lineHeight) => {
-      setLineHeight(payload)
-      handleLayoutChange()
-    }
-
-    const editorTrackChangesChanged = async () => {
-      const tempEntries = cloneDeep(await updateEntries(currentDocumentId))
-
-      // `tempEntries` would be mutated
-      dispatchReviewPanelEvent('recalculate-screen-positions', {
-        entries: tempEntries,
-        updateType: 'trackedChangesChange',
-      })
-
-      // The state should be updated after dispatching the 'recalculate-screen-positions'
-      // event as `tempEntries` will be mutated
-      setEntries(prev => ({ ...prev, [currentDocumentId]: tempEntries }))
-      handleLayoutChange()
-    }
-
-    const editorTrackChangesVisibilityChanged = () => {
-      handleLayoutChange({ async: true, animate: false })
-    }
-
-    const editorFocusChanged = (
-      selectionOffsetStart: number,
-      selectionOffsetEnd: number,
-      selection: boolean,
-      updateType: UpdateType
-    ) => {
-      let tempEntries = cloneDeep(getDocEntries(currentDocumentId))
-      // All selected changes will be added to this array.
-      selectedEntryIds.current = []
-      // Count of user-visible changes, i.e. an aggregated change will count as one.
-      let tempNVisibleSelectedChanges = 0
-
-      const offset = selectionOffsetStart
-      const length = selectionOffsetEnd - selectionOffsetStart
-
-      // Recreate the add comment and bulk actions entries only when
-      // necessary. This is to avoid the UI thinking that these entries have
-      // changed and getting into an infinite loop.
-      if (selection) {
-        const existingAddComment = tempEntries[
-          'add-comment'
-        ] as ReviewPanelAddCommentEntry
-        if (
-          !existingAddComment ||
-          existingAddComment.offset !== offset ||
-          existingAddComment.length !== length
-        ) {
-          tempEntries['add-comment'] = {
-            type: 'add-comment',
-            offset,
-            length,
-          } as ReviewPanelAddCommentEntry
-        }
-        const existingBulkActions = tempEntries[
-          'bulk-actions'
-        ] as ReviewPanelBulkActionsEntry
-        if (
-          !existingBulkActions ||
-          existingBulkActions.offset !== offset ||
-          existingBulkActions.length !== length
-        ) {
-          tempEntries['bulk-actions'] = {
-            type: 'bulk-actions',
-            offset,
-            length,
-          } as ReviewPanelBulkActionsEntry
-        }
-      } else {
-        delete (tempEntries as Partial<typeof tempEntries>)['add-comment']
-        delete (tempEntries as Partial<typeof tempEntries>)['bulk-actions']
-      }
-
-      for (const [key, entry] of Object.entries(tempEntries) as Entries<
-        typeof tempEntries
-      >) {
-        let isChangeEntryAndWithinSelection = false
-        if (entry.type === 'comment' && !resolvedThreadIds[entry.thread_id]) {
-          tempEntries = {
-            ...tempEntries,
-            [key]: {
-              ...tempEntries[key],
-              focused:
-                entry.offset <= selectionOffsetStart &&
-                selectionOffsetStart <= entry.offset + entry.content.length,
-            },
-          }
-        } else if (
-          entry.type === 'insert' ||
-          entry.type === 'aggregate-change'
-        ) {
-          isChangeEntryAndWithinSelection =
-            entry.offset >= selectionOffsetStart &&
-            entry.offset + entry.content.length <= selectionOffsetEnd
-          tempEntries = {
-            ...tempEntries,
-            [key]: {
-              ...tempEntries[key],
-              focused:
-                entry.offset <= selectionOffsetStart &&
-                selectionOffsetStart <= entry.offset + entry.content.length,
-            },
-          }
-        } else if (entry.type === 'delete') {
-          isChangeEntryAndWithinSelection =
-            selectionOffsetStart <= entry.offset &&
-            entry.offset <= selectionOffsetEnd
-          tempEntries = {
-            ...tempEntries,
-            [key]: {
-              ...tempEntries[key],
-              focused: entry.offset === selectionOffsetStart,
-            },
-          }
-        } else if (
-          ['add-comment', 'bulk-actions'].includes(entry.type) &&
-          selection
-        ) {
-          tempEntries = {
-            ...tempEntries,
-            [key]: { ...tempEntries[key], focused: true },
-          }
-        }
-        if (isChangeEntryAndWithinSelection) {
-          const entryIds = 'entry_ids' in entry ? entry.entry_ids : []
-          for (const entryId of entryIds) {
-            selectedEntryIds.current.push(entryId)
-          }
-          tempNVisibleSelectedChanges++
-        }
-      }
-
-      // `tempEntries` would be mutated
-      dispatchReviewPanelEvent('recalculate-screen-positions', {
-        entries: tempEntries,
-        updateType,
-      })
-
-      // The state should be updated after dispatching the 'recalculate-screen-positions'
-      // event as `tempEntries` will be mutated
-      setEntries(prev => ({ ...prev, [currentDocumentId]: tempEntries }))
-      setNVisibleSelectedChanges(tempNVisibleSelectedChanges)
-
-      handleLayoutChange()
-    }
-
-    const addNewCommentFromKbdShortcut = () => {
-      if (!trackChangesVisible) {
-        return
-      }
-      dispatchReviewPanelEvent('comment:select_line')
-
-      if (!reviewPanelOpen) {
-        toggleReviewPanel()
-      }
-      handleLayoutChange({ async: true })
-      addCommentEmitter()
-    }
-
-    const handleEditorEvents = (e: Event) => {
-      const event = e as CustomEvent
-      const { type, payload } = event.detail
-
-      switch (type) {
-        case 'line-height': {
-          editorLineHeightChanged(payload)
-          break
-        }
-
-        case 'track-changes:changed': {
-          editorTrackChangesChanged()
-          break
-        }
-
-        case 'track-changes:visibility_changed': {
-          editorTrackChangesVisibilityChanged()
-          break
-        }
-
-        case 'focus:changed': {
-          const { from, to, empty, updateType } = payload
-          editorFocusChanged(from, to, !empty, updateType)
-          break
-        }
-
-        case 'add-new-comment': {
-          addNewCommentFromKbdShortcut()
-          break
-        }
-
-        case 'toggle-track-changes': {
-          toggleTrackChangesFromKbdShortcut()
-          break
-        }
-
-        case 'toggle-review-panel': {
-          toggleReviewPanel()
-          break
-        }
-      }
-    }
-
-    window.addEventListener('editor:event', handleEditorEvents)
-
-    return () => {
-      window.removeEventListener('editor:event', handleEditorEvents)
-    }
-  }, [
-    addCommentEmitter,
-    currentDocumentId,
-    getDocEntries,
-    resolvedThreadIds,
-    reviewPanelOpen,
-    toggleReviewPanel,
-    toggleTrackChangesForUser,
-    trackChanges,
-    trackChangesState,
-    trackChangesVisible,
-    updateEntries,
-    user.id,
-  ])
-
-  useSocketListener(socket, 'reopen-thread', onCommentReopened)
-  useSocketListener(socket, 'delete-thread', onThreadDeleted)
-  useSocketListener(socket, 'resolve-thread', onCommentResolved)
-  useSocketListener(socket, 'edit-message', onCommentEdited)
-  useSocketListener(socket, 'delete-message', onCommentDeleted)
-  useSocketListener(
-    socket,
-    'accept-changes',
-    useCallback(
-      (docId: DocId, entryIds: ThreadId[]) => {
-        if (docId !== currentDocumentId) {
-          getChangeTracker(docId).removeChangeIds(entryIds)
-        } else {
-          dispatchReviewPanelEvent('changes:accept', entryIds)
-        }
-        updateEntries(docId)
-      },
-      [currentDocumentId, getChangeTracker, updateEntries]
-    )
-  )
-  useSocketListener(
-    socket,
-    'new-comment',
-    useCallback(
-      (threadId: ThreadId, comment: ReviewPanelCommentThreadMessageApi) => {
-        setCommentThreads(prevState => {
-          const { submitting: _, ...thread } = getThread(threadId)
-          thread.messages = [...thread.messages]
-          thread.messages.push(formatComment(comment))
-          return { ...prevState, [threadId]: thread }
-        })
-        handleLayoutChange({ async: true })
-      },
-      [getThread]
-    )
-  )
-  useSocketListener(
-    socket,
-    'new-comment-threads',
-    useCallback(
-      (threads: ReviewPanelCommentThreadsApi) => {
-        setCommentThreads(prevState => {
-          const newThreads = { ...prevState }
-          for (const threadIdString of Object.keys(threads)) {
-            const threadId = threadIdString as ThreadId
-            const { submitting: _, ...thread } = getThread(threadId)
-            // Replace already loaded messages with the server provided ones
-            thread.messages = threads[threadId].messages.map(formatComment)
-            newThreads[threadId] = thread
-          }
-          return newThreads
-        })
-        handleLayoutChange({ async: true })
-      },
-      [getThread]
-    )
-  )
-
-  const openSubView = useRef<typeof subView>('cur_file')
-  useEffect(() => {
-    if (!reviewPanelOpen) {
-      // Always show current file when not open, but save current state
-      setSubView(prevState => {
-        openSubView.current = prevState
-        return 'cur_file'
-      })
-    } else {
-      // Reset back to what we had when previously open
-      setSubView(openSubView.current)
-    }
-    handleLayoutChange({ async: true, animate: false })
-  }, [reviewPanelOpen])
-
-  const canRefreshRanges = useRef(false)
-  const prevSubView = useRef(subView)
-  const initializedPrevSubView = useRef(false)
-  useEffect(() => {
-    // Prevent setting a computed value for `prevSubView` on mount
-    if (!initializedPrevSubView.current) {
-      initializedPrevSubView.current = true
-      return
-    }
-    prevSubView.current = subView === 'cur_file' ? 'overview' : 'cur_file'
-    // Allow refreshing ranges once for each `subView` change
-    canRefreshRanges.current = true
-  }, [subView])
-
-  useEffect(() => {
-    if (subView === 'overview' && canRefreshRanges.current) {
-      canRefreshRanges.current = false
-
-      setIsOverviewLoading(true)
-      refreshRanges().finally(() => {
-        setIsOverviewLoading(false)
-      })
-    }
-  }, [subView, refreshRanges])
-
-  useEffect(() => {
-    if (subView === 'cur_file' && prevSubView.current === 'overview') {
-      dispatchReviewPanelEvent('overview-closed', subView)
-    }
-  }, [subView])
-
-  useEffect(() => {
-    if (Object.keys(users).length) {
-      handleLayoutChange({ async: true })
-    }
-  }, [users])
-
-  const values = useMemo<ReviewPanel.ReviewPanelState['values']>(
-    () => ({
-      collapsed,
-      commentThreads,
-      entries,
-      isAddingComment,
-      loadingThreads,
-      nVisibleSelectedChanges,
-      permissions,
-      users,
-      resolvedComments,
-      shouldCollapse,
-      navHeight,
-      toolbarHeight,
-      subView,
-      wantTrackChanges,
-      isOverviewLoading,
-      openDocId: currentDocumentId,
-      lineHeight,
-      trackChangesState,
-      trackChangesOnForEveryone,
-      trackChangesOnForGuests,
-      trackChangesForGuestsAvailable,
-      formattedProjectMembers,
-      layoutSuspended,
-      unsavedComment,
-      layoutToLeft,
-    }),
-    [
-      collapsed,
-      commentThreads,
-      entries,
-      isAddingComment,
-      loadingThreads,
-      nVisibleSelectedChanges,
-      permissions,
-      users,
-      resolvedComments,
-      shouldCollapse,
-      navHeight,
-      toolbarHeight,
-      subView,
-      wantTrackChanges,
-      isOverviewLoading,
-      currentDocumentId,
-      lineHeight,
-      trackChangesState,
-      trackChangesOnForEveryone,
-      trackChangesOnForGuests,
-      trackChangesForGuestsAvailable,
-      formattedProjectMembers,
-      layoutSuspended,
-      unsavedComment,
-      layoutToLeft,
-    ]
-  )
-
-  const updaterFns = useMemo<ReviewPanel.ReviewPanelState['updaterFns']>(
-    () => ({
-      handleSetSubview,
-      handleLayoutChange,
-      gotoEntry,
-      resolveComment,
-      submitReply,
-      acceptChanges,
-      rejectChanges,
-      toggleReviewPanel,
-      bulkAcceptActions,
-      bulkRejectActions,
-      saveEdit,
-      submitNewComment,
-      deleteComment,
-      unresolveComment,
-      refreshResolvedCommentsDropdown: refreshRanges,
-      deleteThread,
-      toggleTrackChangesForEveryone,
-      toggleTrackChangesForUser,
-      toggleTrackChangesForGuests,
-      setCollapsed,
-      setShouldCollapse,
-      setIsAddingComment,
-      setNavHeight,
-      setToolbarHeight,
-      setLayoutSuspended,
-      setUnsavedComment,
-    }),
-    [
-      handleSetSubview,
-      gotoEntry,
-      resolveComment,
-      submitReply,
-      acceptChanges,
-      rejectChanges,
-      toggleReviewPanel,
-      bulkAcceptActions,
-      bulkRejectActions,
-      saveEdit,
-      submitNewComment,
-      deleteComment,
-      unresolveComment,
-      refreshRanges,
-      deleteThread,
-      toggleTrackChangesForEveryone,
-      toggleTrackChangesForUser,
-      toggleTrackChangesForGuests,
-      setCollapsed,
-      setShouldCollapse,
-      setIsAddingComment,
-      setNavHeight,
-      setToolbarHeight,
-      setLayoutSuspended,
-      setUnsavedComment,
-    ]
-  )
-
-  return { values, updaterFns }
-}
-
-export default useReviewPanelState

+ 0 - 32
services/web/frontend/js/features/ide-react/context/review-panel/hooks/useLayoutToLeft.tsx

@@ -1,32 +0,0 @@
-import { useState, useEffect } from 'react'
-
-function useLayoutToLeft(querySelector: string) {
-  const [layoutToLeft, setLayoutToLeft] = useState(false)
-
-  useEffect(() => {
-    if (!('ResizeObserver' in window)) return
-
-    const target = document.querySelector(querySelector)
-
-    if (!target) return
-
-    const handleResize = () => {
-      const docWidth = document.documentElement.clientWidth
-      const { right: rightEdge } = target.getBoundingClientRect()
-      setLayoutToLeft(docWidth - rightEdge < 225)
-    }
-
-    handleResize()
-
-    const observer = new ResizeObserver(handleResize)
-    observer.observe(target)
-
-    return () => {
-      observer.disconnect()
-    }
-  }, [querySelector])
-
-  return layoutToLeft
-}
-
-export default useLayoutToLeft

+ 0 - 8
services/web/frontend/js/features/ide-react/scope-adapters/review-panel-context-adapter.ts

@@ -1,8 +0,0 @@
-import { ReactScopeValueStore } from '@/features/ide-react/scope-value-store/react-scope-value-store'
-import { isSplitTestEnabled } from '@/utils/splitTestUtils'
-
-export default function populateReviewPanelScope(store: ReactScopeValueStore) {
-  store.set('loadingThreads', true)
-  store.set('users', {})
-  store.set('usingNewReviewPanel', isSplitTestEnabled('review-panel-redesign'))
-}

+ 0 - 1
services/web/frontend/js/features/review-panel-new/components/review-panel-header.tsx

@@ -20,7 +20,6 @@ const ReviewPanelHeader: FC = () => {
       <PanelHeading
       <PanelHeading
         title={t('review')}
         title={t('review')}
         handleClose={() => setReviewPanelOpen(false)}
         handleClose={() => setReviewPanelOpen(false)}
-        splitTestName="review-panel-redesign"
       >
       >
         {isReviewerRoleEnabled && <ReviewPanelResolvedThreadsButton />}
         {isReviewerRoleEnabled && <ReviewPanelResolvedThreadsButton />}
       </PanelHeading>
       </PanelHeading>

+ 12 - 0
services/web/frontend/js/features/review-panel-new/components/review-panel-new.tsx

@@ -0,0 +1,12 @@
+import React, { FC, lazy, Suspense } from 'react'
+import LoadingSpinner from '@/shared/components/loading-spinner'
+
+const ReviewPanelContainer = lazy(() => import('./review-panel-container'))
+
+export const ReviewPanelNew: FC = () => {
+  return (
+    <Suspense fallback={<LoadingSpinner delay={500} />}>
+      <ReviewPanelContainer />
+    </Suspense>
+  )
+}

+ 1 - 1
services/web/frontend/js/features/review-panel-new/components/review-panel-track-changes-menu-button.tsx

@@ -3,7 +3,7 @@ import { Trans } from 'react-i18next'
 import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
 import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
 import MaterialIcon from '@/shared/components/material-icon'
 import MaterialIcon from '@/shared/components/material-icon'
 import { useProjectContext } from '@/shared/context/project-context'
 import { useProjectContext } from '@/shared/context/project-context'
-import UpgradeTrackChangesModal from '@/features/source-editor/components/review-panel/upgrade-track-changes-modal'
+import UpgradeTrackChangesModal from '@/features/review-panel-new/components/upgrade-track-changes-modal'
 import { send, sendMB } from '@/infrastructure/event-tracking'
 import { send, sendMB } from '@/infrastructure/event-tracking'
 
 
 const sendAnalytics = () => {
 const sendAnalytics = () => {

+ 1 - 1
services/web/frontend/js/features/review-panel-new/components/review-panel-track-changes-menu.tsx

@@ -1,5 +1,5 @@
 import { FC } from 'react'
 import { FC } from 'react'
-import TrackChangesToggle from '@/features/source-editor/components/review-panel/toolbar/track-changes-toggle'
+import TrackChangesToggle from '@/features/review-panel-new/components/track-changes-toggle'
 import { useProjectContext } from '@/shared/context/project-context'
 import { useProjectContext } from '@/shared/context/project-context'
 import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
 import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'

+ 0 - 0
services/web/frontend/js/features/source-editor/components/review-panel/toolbar/track-changes-toggle.tsx → services/web/frontend/js/features/review-panel-new/components/track-changes-toggle.tsx


+ 4 - 4
services/web/frontend/js/features/source-editor/components/review-panel/upgrade-track-changes-modal.tsx → services/web/frontend/js/features/review-panel-new/components/upgrade-track-changes-modal.tsx

@@ -1,8 +1,8 @@
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
-import { useProjectContext } from '../../../../shared/context/project-context'
-import { useUserContext } from '../../../../shared/context/user-context'
-import teaserVideo from '../../images/teaser-track-changes.mp4'
-import teaserImage from '../../images/teaser-track-changes.gif'
+import { useProjectContext } from '@/shared/context/project-context'
+import { useUserContext } from '@/shared/context/user-context'
+import teaserVideo from '../images/teaser-track-changes.mp4'
+import teaserImage from '../images/teaser-track-changes.gif'
 import { startFreeTrial, upgradePlan } from '@/main/account-upgrade'
 import { startFreeTrial, upgradePlan } from '@/main/account-upgrade'
 import { memo } from 'react'
 import { memo } from 'react'
 import { useFeatureFlag } from '@/shared/context/split-test-context'
 import { useFeatureFlag } from '@/shared/context/split-test-context'

+ 8 - 3
services/web/frontend/js/features/review-panel-new/context/changes-users-context.tsx

@@ -10,6 +10,8 @@ import { getJSON } from '@/infrastructure/fetch-json'
 import { useProjectContext } from '@/shared/context/project-context'
 import { useProjectContext } from '@/shared/context/project-context'
 import { UserId } from '../../../../../types/user'
 import { UserId } from '../../../../../types/user'
 import { useEditorContext } from '@/shared/context/editor-context'
 import { useEditorContext } from '@/shared/context/editor-context'
+import { debugConsole } from '@/utils/debugging'
+import { captureException } from '@/infrastructure/error-reporter'
 
 
 export type ChangesUser = {
 export type ChangesUser = {
   id: UserId
   id: UserId
@@ -35,9 +37,12 @@ export const ChangesUsersProvider: FC = ({ children }) => {
       return
       return
     }
     }
 
 
-    getJSON<ChangesUser[]>(`/project/${projectId}/changes/users`).then(data =>
-      setChangesUsers(new Map(data.map(item => [item.id, item])))
-    )
+    getJSON<ChangesUser[]>(`/project/${projectId}/changes/users`)
+      .then(data => setChangesUsers(new Map(data.map(item => [item.id, item]))))
+      .catch(error => {
+        debugConsole.error(error)
+        captureException(error)
+      })
   }, [projectId, isRestrictedTokenMember])
   }, [projectId, isRestrictedTokenMember])
 
 
   // add the project owner and members to the changes users data
   // add the project owner and members to the changes users data

+ 0 - 5
services/web/frontend/js/features/review-panel-new/context/review-panel-providers.tsx

@@ -3,14 +3,9 @@ import { RangesProvider } from './ranges-context'
 import { ChangesUsersProvider } from './changes-users-context'
 import { ChangesUsersProvider } from './changes-users-context'
 import { TrackChangesStateProvider } from './track-changes-state-context'
 import { TrackChangesStateProvider } from './track-changes-state-context'
 import { ThreadsProvider } from './threads-context'
 import { ThreadsProvider } from './threads-context'
-import { isSplitTestEnabled } from '@/utils/splitTestUtils'
 import { ReviewPanelViewProvider } from './review-panel-view-context'
 import { ReviewPanelViewProvider } from './review-panel-view-context'
 
 
 export const ReviewPanelProviders: FC = ({ children }) => {
 export const ReviewPanelProviders: FC = ({ children }) => {
-  if (!isSplitTestEnabled('review-panel-redesign')) {
-    return <>{children}</>
-  }
-
   return (
   return (
     <ReviewPanelViewProvider>
     <ReviewPanelViewProvider>
       <ChangesUsersProvider>
       <ChangesUsersProvider>

+ 15 - 7
services/web/frontend/js/features/review-panel-new/context/threads-context.tsx

@@ -10,18 +10,20 @@ import {
 import { useProjectContext } from '@/shared/context/project-context'
 import { useProjectContext } from '@/shared/context/project-context'
 import {
 import {
   CommentId,
   CommentId,
+  ReviewPanelCommentThreadMessage,
   ThreadId,
   ThreadId,
 } from '../../../../../types/review-panel/review-panel'
 } from '../../../../../types/review-panel/review-panel'
 import { ReviewPanelCommentThread } from '../../../../../types/review-panel/comment-thread'
 import { ReviewPanelCommentThread } from '../../../../../types/review-panel/comment-thread'
 import { useConnectionContext } from '@/features/ide-react/context/connection-context'
 import { useConnectionContext } from '@/features/ide-react/context/connection-context'
 import useSocketListener from '@/features/ide-react/hooks/use-socket-listener'
 import useSocketListener from '@/features/ide-react/hooks/use-socket-listener'
-import { ReviewPanelCommentThreadMessageApi } from '../../../../../types/review-panel/api'
 import { UserId } from '../../../../../types/user'
 import { UserId } from '../../../../../types/user'
 import { deleteJSON, getJSON, postJSON } from '@/infrastructure/fetch-json'
 import { deleteJSON, getJSON, postJSON } from '@/infrastructure/fetch-json'
 import RangesTracker from '@overleaf/ranges-tracker'
 import RangesTracker from '@overleaf/ranges-tracker'
 import { CommentOperation } from '../../../../../types/change'
 import { CommentOperation } from '../../../../../types/change'
 import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
 import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
 import { useEditorContext } from '@/shared/context/editor-context'
 import { useEditorContext } from '@/shared/context/editor-context'
+import { debugConsole } from '@/utils/debugging'
+import { captureException } from '@/infrastructure/error-reporter'
 
 
 export type Threads = Record<ThreadId, ReviewPanelCommentThread>
 export type Threads = Record<ThreadId, ReviewPanelCommentThread>
 
 
@@ -64,12 +66,15 @@ export const ThreadsProvider: FC = ({ children }) => {
 
 
     getJSON(`/project/${projectId}/threads`, {
     getJSON(`/project/${projectId}/threads`, {
       signal: abortController.signal,
       signal: abortController.signal,
-    }).then(data => {
-      setData(data)
     })
     })
-    // .catch(error => {
-    //   setError(error)
-    // })
+      .then(data => {
+        setData(data)
+      })
+      .catch(error => {
+        debugConsole.error(error)
+        captureException(error)
+        // setError(error)
+      })
   }, [projectId, isRestrictedTokenMember])
   }, [projectId, isRestrictedTokenMember])
 
 
   const { socket } = useConnectionContext()
   const { socket } = useConnectionContext()
@@ -78,7 +83,10 @@ export const ThreadsProvider: FC = ({ children }) => {
     socket,
     socket,
     'new-comment',
     'new-comment',
     useCallback(
     useCallback(
-      (threadId: ThreadId, comment: ReviewPanelCommentThreadMessageApi) => {
+      (
+        threadId: ThreadId,
+        comment: ReviewPanelCommentThreadMessage & { timestamp: number }
+      ) => {
         setData(value => {
         setData(value => {
           if (value) {
           if (value) {
             const { submitting, ...thread } = value[threadId] ?? {
             const { submitting, ...thread } = value[threadId] ?? {

+ 0 - 0
services/web/frontend/js/features/source-editor/images/teaser-track-changes.gif → services/web/frontend/js/features/review-panel-new/images/teaser-track-changes.gif


+ 0 - 0
services/web/frontend/js/features/source-editor/images/teaser-track-changes.mp4 → services/web/frontend/js/features/review-panel-new/images/teaser-track-changes.mp4


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

@@ -10,9 +10,8 @@ import { CodeMirrorCommandTooltip } from './codemirror-command-tooltip'
 import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
 import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
 import { FigureModal } from './figure-modal/figure-modal'
 import { FigureModal } from './figure-modal/figure-modal'
 import { ReviewPanelProviders } from '@/features/review-panel-new/context/review-panel-providers'
 import { ReviewPanelProviders } from '@/features/review-panel-new/context/review-panel-providers'
-import { ReviewPanelMigration } from '@/features/source-editor/components/review-panel/review-panel-migration'
+import { ReviewPanelNew } from '@/features/review-panel-new/components/review-panel-new'
 import ReviewTooltipMenu from '@/features/review-panel-new/components/review-tooltip-menu'
 import ReviewTooltipMenu from '@/features/review-panel-new/components/review-tooltip-menu'
-import { useFeatureFlag } from '@/shared/context/split-test-context'
 import {
 import {
   CodeMirrorStateContext,
   CodeMirrorStateContext,
   CodeMirrorViewContext,
   CodeMirrorViewContext,
@@ -40,7 +39,6 @@ function CodeMirrorEditor() {
 
 
   const isMounted = useIsMounted()
   const isMounted = useIsMounted()
 
 
-  const newReviewPanel = useFeatureFlag('review-panel-redesign')
   const newEditor = useIsNewEditorEnabled()
   const newEditor = useIsNewEditorEnabled()
 
 
   // create the view using the initial state and intercept transactions
   // create the view using the initial state and intercept transactions
@@ -79,8 +77,8 @@ function CodeMirrorEditor() {
           <CodeMirrorCommandTooltip />
           <CodeMirrorCommandTooltip />
 
 
           <MathPreviewTooltip />
           <MathPreviewTooltip />
-          {newReviewPanel && <ReviewTooltipMenu />}
-          <ReviewPanelMigration />
+          <ReviewTooltipMenu />
+          <ReviewPanelNew />
 
 
           {sourceEditorComponents.map(
           {sourceEditorComponents.map(
             ({ import: { default: Component }, path }) => (
             ({ import: { default: Component }, path }) => (

+ 0 - 5
services/web/frontend/js/features/source-editor/components/review-panel/add-comment-button.tsx

@@ -1,5 +0,0 @@
-function AddCommentButton(props: React.ComponentPropsWithoutRef<'button'>) {
-  return <button className="rp-add-comment-btn" {...props} />
-}
-
-export default AddCommentButton

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

@@ -1,20 +0,0 @@
-import classnames from 'classnames'
-
-type ContainerProps = {
-  children?: React.ReactNode
-  className?: string
-}
-
-function Container({ children, className, ...rest }: ContainerProps) {
-  return (
-    <div
-      className={classnames('review-panel', className)}
-      {...rest}
-      data-testid="review-panel"
-    >
-      {children}
-    </div>
-  )
-}
-
-export default Container

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

@@ -1,53 +0,0 @@
-import { memo, useMemo } from 'react'
-import Container from './container'
-import Toolbar from './toolbar/toolbar'
-import Nav from './nav'
-import Toggler from './toggler'
-import PositionedEntries from './positioned-entries'
-import { useReviewPanelValueContext } from '../../context/review-panel/review-panel-context'
-import useCodeMirrorContentHeight from '../../hooks/use-codemirror-content-height'
-import { ReviewPanelEntry } from '../../../../../../types/review-panel/entry'
-import { ReviewPanelDocEntries } from '../../../../../../types/review-panel/review-panel'
-import Entry from './entry'
-
-function CurrentFileContainer() {
-  const { entries, openDocId } = useReviewPanelValueContext()
-  const contentHeight = useCodeMirrorContentHeight()
-
-  const currentDocEntries =
-    openDocId && openDocId in entries ? entries[openDocId] : undefined
-
-  const objectEntries = useMemo(() => {
-    return Object.entries(currentDocEntries || {}) as Array<
-      [keyof ReviewPanelDocEntries, ReviewPanelEntry]
-    >
-  }, [currentDocEntries])
-
-  return (
-    <Container className="rp-current-file-container">
-      <div className="review-panel-tools">
-        <Toolbar />
-        <Nav />
-      </div>
-      <Toggler />
-      <div
-        id="review-panel-current-file"
-        role="tabpanel"
-        tabIndex={0}
-        aria-labelledby="review-panel-tab-current-file"
-      >
-        <PositionedEntries
-          entries={objectEntries}
-          contentHeight={contentHeight}
-        >
-          {openDocId &&
-            objectEntries.map(([id, entry]) => {
-              return <Entry key={id} id={id} entry={entry} />
-            })}
-        </PositionedEntries>
-      </div>
-    </Container>
-  )
-}
-
-export default memo(CurrentFileContainer)

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

@@ -1,109 +0,0 @@
-import ReactDOM from 'react-dom'
-import { useTranslation } from 'react-i18next'
-import ToggleWidget from './toggle-widget'
-import BulkActions from '../entries/bulk-actions-entry/bulk-actions'
-import AddCommentButton from '../add-comment-button'
-import {
-  useReviewPanelUpdaterFnsContext,
-  useReviewPanelValueContext,
-} from '../../../context/review-panel/review-panel-context'
-import { useEditorContext } from '@/shared/context/editor-context'
-import { useCodeMirrorViewContext } from '../../codemirror-context'
-import Modal, { useBulkActionsModal } from '../entries/bulk-actions-entry/modal'
-import getMeta from '../../../../../utils/meta'
-import useScopeEventListener from '@/shared/hooks/use-scope-event-listener'
-import { memo, useCallback } from 'react'
-import { useLayoutContext } from '@/shared/context/layout-context'
-import classnames from 'classnames'
-import MaterialIcon from '@/shared/components/material-icon'
-
-function EditorWidgets() {
-  const { t } = useTranslation()
-  const {
-    show,
-    setShow,
-    isAccept,
-    handleShowBulkAcceptDialog,
-    handleShowBulkRejectDialog,
-    handleConfirmDialog,
-  } = useBulkActionsModal()
-  const { setIsAddingComment } = useReviewPanelUpdaterFnsContext()
-  const { toggleReviewPanel } = useReviewPanelUpdaterFnsContext()
-  const view = useCodeMirrorViewContext()
-  const { reviewPanelOpen } = useLayoutContext()
-  const { isRestrictedTokenMember } = useEditorContext()
-
-  const {
-    entries,
-    openDocId,
-    nVisibleSelectedChanges: nChanges,
-    wantTrackChanges,
-    permissions,
-  } = useReviewPanelValueContext()
-
-  const hasTrackChangesFeature = getMeta('ol-hasTrackChangesFeature')
-
-  const currentDocEntries =
-    openDocId && openDocId in entries ? entries[openDocId] : undefined
-
-  const handleAddNewCommentClick = (e: React.MouseEvent<HTMLButtonElement>) => {
-    e.preventDefault()
-    setIsAddingComment(true)
-    toggleReviewPanel()
-  }
-
-  useScopeEventListener(
-    'comment:start_adding',
-    useCallback(() => {
-      setIsAddingComment(true)
-    }, [setIsAddingComment])
-  )
-
-  return ReactDOM.createPortal(
-    <>
-      <div
-        className={classnames('rp-in-editor-widgets', {
-          hidden: reviewPanelOpen,
-        })}
-      >
-        <div className="rp-in-editor-widgets-inner">
-          {wantTrackChanges && <ToggleWidget />}
-          {nChanges > 1 && permissions.write && (
-            <>
-              <BulkActions.Button onClick={handleShowBulkAcceptDialog}>
-                <MaterialIcon type="check" />
-                &nbsp;
-                {t('accept_all')} ({nChanges})
-              </BulkActions.Button>
-              <BulkActions.Button onClick={handleShowBulkRejectDialog}>
-                <MaterialIcon type="close" />
-                &nbsp;
-                {t('reject_all')} ({nChanges})
-              </BulkActions.Button>
-            </>
-          )}
-          {hasTrackChangesFeature &&
-            permissions.comment &&
-            !isRestrictedTokenMember &&
-            currentDocEntries?.['add-comment'] && (
-              <AddCommentButton onClick={handleAddNewCommentClick}>
-                <MaterialIcon type="mode_comment" />
-                &nbsp;
-                {t('add_comment')}
-              </AddCommentButton>
-            )}
-        </div>
-      </div>
-      <Modal
-        show={show}
-        setShow={setShow}
-        isAccept={isAccept}
-        nChanges={nChanges}
-        onConfirm={handleConfirmDialog}
-      />
-    </>,
-    view.scrollDOM
-  )
-}
-
-export default memo(EditorWidgets)

+ 0 - 32
services/web/frontend/js/features/source-editor/components/review-panel/editor-widgets/toggle-widget.tsx

@@ -1,32 +0,0 @@
-import { Trans } from 'react-i18next'
-import { useReviewPanelUpdaterFnsContext } from '../../../context/review-panel/review-panel-context'
-import { useCodeMirrorStateContext } from '../../codemirror-context'
-import { EditorView } from '@codemirror/view'
-import classnames from 'classnames'
-import { memo } from 'react'
-
-function ToggleWidget() {
-  const { toggleReviewPanel } = useReviewPanelUpdaterFnsContext()
-  const state = useCodeMirrorStateContext()
-  const darkTheme = state.facet(EditorView.darkTheme)
-
-  return (
-    <button
-      className={classnames('rp-track-changes-indicator', {
-        'rp-track-changes-indicator-on-dark': darkTheme,
-      })}
-      onClick={toggleReviewPanel}
-    >
-      <TrackChangesOn />
-    </button>
-  )
-}
-
-const TrackChangesOn = memo(() => {
-  return (
-    <Trans i18nKey="track_changes_is_on" components={{ strong: <strong /> }} />
-  )
-})
-TrackChangesOn.displayName = 'TrackChangesOn'
-
-export default ToggleWidget

+ 0 - 165
services/web/frontend/js/features/source-editor/components/review-panel/entries/add-comment-entry.tsx

@@ -1,165 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import { useEffect, useRef, useState } from 'react'
-import EntryContainer from './entry-container'
-import EntryCallout from './entry-callout'
-import EntryActions from './entry-actions'
-import AutoExpandingTextArea from '../../../../../shared/components/auto-expanding-text-area'
-import AddCommentButton from '../add-comment-button'
-import {
-  useReviewPanelUpdaterFnsContext,
-  useReviewPanelValueContext,
-} from '../../../context/review-panel/review-panel-context'
-import classnames from 'classnames'
-import MaterialIcon from '@/shared/components/material-icon'
-import LoadingSpinner from '@/shared/components/loading-spinner'
-
-function AddCommentEntry() {
-  const { t } = useTranslation()
-  const { isAddingComment, unsavedComment } = useReviewPanelValueContext()
-  const {
-    setIsAddingComment,
-    submitNewComment,
-    handleLayoutChange,
-    setUnsavedComment,
-  } = useReviewPanelUpdaterFnsContext()
-
-  const [content, setContent] = useState(unsavedComment)
-  const [isSubmitting, setIsSubmitting] = useState(false)
-
-  const handleStartNewComment = () => {
-    setIsAddingComment(true)
-    handleLayoutChange({ async: true })
-  }
-
-  const handleSubmitNewComment = () => {
-    setIsSubmitting(true)
-    try {
-      submitNewComment(content)
-      setIsSubmitting(false)
-      setIsAddingComment(false)
-      setContent('')
-    } catch (err) {
-      setIsSubmitting(false)
-    }
-    handleLayoutChange({ async: true })
-  }
-
-  const handleCancelNewComment = () => {
-    setIsAddingComment(false)
-    setContent('')
-    handleLayoutChange({ async: true })
-  }
-
-  useEffect(() => {
-    return () => {
-      setIsAddingComment(false)
-    }
-  }, [setIsAddingComment])
-
-  const unsavedCommentRef = useRef(unsavedComment)
-
-  // Keep unsaved comment ref up to date for use when the component unmounts
-  useEffect(() => {
-    unsavedCommentRef.current = content
-  }, [content])
-
-  // Store the unsaved comment in the context on unmount
-  useEffect(() => {
-    return () => {
-      setUnsavedComment(unsavedCommentRef.current)
-    }
-  }, [setUnsavedComment])
-
-  const handleCommentKeyPress = (
-    e: React.KeyboardEvent<HTMLTextAreaElement>
-  ) => {
-    const target = e.target as HTMLTextAreaElement
-
-    if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
-      e.preventDefault()
-      if (content.length) {
-        handleSubmitNewComment()
-      }
-    }
-
-    if (['PageDown', 'PageUp'].includes(e.key)) {
-      if (target.closest('textarea')) {
-        e.preventDefault()
-      }
-    }
-  }
-
-  const handleCommentAutoFocus = (textarea: HTMLTextAreaElement) => {
-    // Sometimes the comment textarea is scrolled out of view once focussed,
-    // so this checks for that and scrolls it into view if necessary. It
-    // seems we sometimes need to allow time for the dust to settle after
-    // focussing the textarea before scrolling.
-    window.setTimeout(() => {
-      const observer = new IntersectionObserver(([entry]) => {
-        if (entry.intersectionRatio < 1) {
-          textarea.scrollIntoView({ block: 'center' })
-        }
-        observer.disconnect()
-      })
-      observer.observe(textarea)
-    }, 500)
-  }
-
-  return (
-    <EntryContainer id="add-comment">
-      <EntryCallout className="rp-entry-callout-add-comment" />
-      <div
-        className={classnames('rp-entry', 'rp-entry-add-comment', {
-          'rp-entry-adding-comment': isAddingComment,
-        })}
-      >
-        {isAddingComment ? (
-          <>
-            <div className="rp-new-comment">
-              {isSubmitting ? (
-                <LoadingSpinner className="d-flex justify-content-center" />
-              ) : (
-                <AutoExpandingTextArea
-                  className="rp-comment-input"
-                  onChange={e => setContent(e.target.value)}
-                  onKeyPress={handleCommentKeyPress}
-                  onResize={handleLayoutChange}
-                  onAutoFocus={handleCommentAutoFocus}
-                  placeholder={t('add_your_comment_here')}
-                  value={content}
-                  autoFocus // eslint-disable-line jsx-a11y/no-autofocus
-                />
-              )}
-            </div>
-            <EntryActions>
-              <EntryActions.Button
-                className="rp-entry-button-cancel"
-                onClick={handleCancelNewComment}
-              >
-                <MaterialIcon type="close" />
-                &nbsp;
-                {t('cancel')}
-              </EntryActions.Button>
-              <EntryActions.Button
-                onClick={handleSubmitNewComment}
-                disabled={isSubmitting || !content.length}
-              >
-                <MaterialIcon type="mode_comment" />
-                &nbsp;
-                {t('comment')}
-              </EntryActions.Button>
-            </EntryActions>
-          </>
-        ) : (
-          <AddCommentButton onClick={handleStartNewComment}>
-            <MaterialIcon type="mode_comment" />
-            &nbsp;
-            {t('add_comment')}
-          </AddCommentButton>
-        )}
-      </div>
-    </EntryContainer>
-  )
-}
-
-export default AddCommentEntry

+ 0 - 159
services/web/frontend/js/features/source-editor/components/review-panel/entries/aggregate-change-entry.tsx

@@ -1,159 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import { memo, useState } from 'react'
-import EntryContainer from './entry-container'
-import EntryCallout from './entry-callout'
-import EntryActions from './entry-actions'
-import { useReviewPanelUpdaterFnsContext } from '../../../context/review-panel/review-panel-context'
-import { formatTime } from '../../../../utils/format-date'
-import classnames from 'classnames'
-import comparePropsWithShallowArrayCompare from '../utils/compare-props-with-shallow-array-compare'
-import { BaseChangeEntryProps } from '../types/base-change-entry-props'
-import useIndicatorHover from '../hooks/use-indicator-hover'
-import EntryIndicator from './entry-indicator'
-import { useEntryClick } from '@/features/source-editor/components/review-panel/hooks/use-entry-click'
-import MaterialIcon from '@/shared/components/material-icon'
-
-interface AggregateChangeEntryProps extends BaseChangeEntryProps {
-  replacedContent: string
-}
-
-function AggregateChangeEntry({
-  docId,
-  entryId,
-  permissions,
-  user,
-  content,
-  replacedContent,
-  offset,
-  focused,
-  entryIds,
-  timestamp,
-  contentLimit = 17,
-}: AggregateChangeEntryProps) {
-  const { t } = useTranslation()
-  const { acceptChanges, rejectChanges, handleLayoutChange } =
-    useReviewPanelUpdaterFnsContext()
-  const [isDeletionCollapsed, setIsDeletionCollapsed] = useState(true)
-  const [isInsertionCollapsed, setIsInsertionCollapsed] = useState(true)
-  const {
-    hoverCoords,
-    indicatorRef,
-    endHover,
-    handleIndicatorMouseEnter,
-    handleIndicatorClick,
-  } = useIndicatorHover()
-
-  const deletionNeedsCollapsing = replacedContent.length > contentLimit
-  const insertionNeedsCollapsing = content.length > contentLimit
-
-  const deletionContent = isDeletionCollapsed
-    ? replacedContent.substring(0, contentLimit)
-    : replacedContent
-
-  const insertionContent = isInsertionCollapsed
-    ? content.substring(0, contentLimit)
-    : content
-
-  const handleEntryClick = useEntryClick(docId, offset, endHover)
-
-  const handleDeletionToggleCollapse = () => {
-    setIsDeletionCollapsed(value => !value)
-    handleLayoutChange()
-  }
-
-  const handleInsertionToggleCollapse = () => {
-    setIsInsertionCollapsed(value => !value)
-    handleLayoutChange()
-  }
-
-  return (
-    <EntryContainer
-      id={entryId}
-      hoverCoords={hoverCoords}
-      onClick={handleEntryClick}
-      onMouseLeave={endHover}
-    >
-      <EntryCallout className="rp-entry-callout-aggregate" />
-      <EntryIndicator
-        ref={indicatorRef}
-        focused={focused}
-        onMouseEnter={handleIndicatorMouseEnter}
-        onClick={handleIndicatorClick}
-      >
-        <MaterialIcon type="edit" />
-      </EntryIndicator>
-      <div
-        className={classnames('rp-entry', 'rp-entry-aggregate', {
-          'rp-entry-focused': focused,
-        })}
-      >
-        <div className="rp-entry-body">
-          <div className="rp-entry-action-icon">
-            <MaterialIcon type="edit" />
-          </div>
-          <div className="rp-entry-details">
-            <div className="rp-entry-description">
-              {t('aggregate_changed')}&nbsp;
-              <del className="rp-content-highlight">{deletionContent}</del>
-              {deletionNeedsCollapsing && (
-                <button
-                  className="rp-collapse-toggle"
-                  onClick={handleDeletionToggleCollapse}
-                >
-                  {isDeletionCollapsed
-                    ? `… (${t('show_all')})`
-                    : ` (${t('show_less')})`}
-                </button>
-              )}{' '}
-              {t('aggregate_to')}&nbsp;
-              <ins className="rp-content-highlight">{insertionContent}</ins>
-              {insertionNeedsCollapsing && (
-                <button
-                  className="rp-collapse-toggle"
-                  onClick={handleInsertionToggleCollapse}
-                >
-                  {isInsertionCollapsed
-                    ? `… (${t('show_all')})`
-                    : ` (${t('show_less')})`}
-                </button>
-              )}
-            </div>
-            <div className="rp-entry-metadata">
-              <span className="rp-entry-metadata-element">
-                {formatTime(timestamp, 'MMM D, Y h:mm A')}
-              </span>
-              {user && (
-                <span className="rp-entry-metadata-element">
-                  &nbsp;&bull;&nbsp;
-                  <span
-                    className="rp-entry-user"
-                    style={{ color: `hsl(${user.hue}, 70%, 40%)` }}
-                  >
-                    {user.name ?? t('anonymous')}
-                  </span>
-                </span>
-              )}
-            </div>
-          </div>
-        </div>
-        {permissions.write && (
-          <EntryActions>
-            <EntryActions.Button onClick={() => rejectChanges(entryIds)}>
-              <MaterialIcon type="close" />
-              &nbsp;{t('reject')}
-            </EntryActions.Button>
-            <EntryActions.Button onClick={() => acceptChanges(entryIds)}>
-              <MaterialIcon type="check" />
-              &nbsp;{t('accept')}
-            </EntryActions.Button>
-          </EntryActions>
-        )}
-      </div>
-    </EntryContainer>
-  )
-}
-
-export default memo(
-  AggregateChangeEntry,
-  comparePropsWithShallowArrayCompare('entryIds')
-)

+ 0 - 55
services/web/frontend/js/features/source-editor/components/review-panel/entries/bulk-actions-entry/bulk-actions-entry.tsx

@@ -1,55 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import EntryContainer from '../entry-container'
-import EntryCallout from '../entry-callout'
-import BulkActions from './bulk-actions'
-import Modal, { useBulkActionsModal } from './modal'
-import { ReviewPanelBulkActionsEntry } from '../../../../../../../../types/review-panel/entry'
-import MaterialIcon from '@/shared/components/material-icon'
-
-type BulkActionsEntryProps = {
-  entryId: ReviewPanelBulkActionsEntry['type']
-  nChanges: number
-}
-
-function BulkActionsEntry({ entryId, nChanges }: BulkActionsEntryProps) {
-  const { t } = useTranslation()
-  const {
-    show,
-    setShow,
-    isAccept,
-    handleShowBulkAcceptDialog,
-    handleShowBulkRejectDialog,
-    handleConfirmDialog,
-  } = useBulkActionsModal()
-
-  return (
-    <>
-      <EntryContainer id={entryId}>
-        {nChanges > 1 && (
-          <>
-            <EntryCallout className="rp-entry-callout-bulk-actions" />
-            <BulkActions className="rp-entry">
-              <BulkActions.Button onClick={handleShowBulkRejectDialog}>
-                <MaterialIcon type="close" />
-                &nbsp;{t('reject_all')} ({nChanges})
-              </BulkActions.Button>
-              <BulkActions.Button onClick={handleShowBulkAcceptDialog}>
-                <MaterialIcon type="check" />
-                &nbsp;{t('accept_all')} ({nChanges})
-              </BulkActions.Button>
-            </BulkActions>
-          </>
-        )}
-      </EntryContainer>
-      <Modal
-        show={show}
-        setShow={setShow}
-        isAccept={isAccept}
-        nChanges={nChanges}
-        onConfirm={handleConfirmDialog}
-      />
-    </>
-  )
-}
-
-export default BulkActionsEntry

+ 0 - 24
services/web/frontend/js/features/source-editor/components/review-panel/entries/bulk-actions-entry/bulk-actions.tsx

@@ -1,24 +0,0 @@
-import classnames from 'classnames'
-
-function BulkActions({
-  className,
-  ...rest
-}: React.ComponentPropsWithoutRef<'div'>) {
-  return (
-    <div className={classnames('rp-entry-bulk-actions', className)} {...rest} />
-  )
-}
-
-BulkActions.Button = function BulkActionsButton({
-  className,
-  ...rest
-}: React.ComponentPropsWithoutRef<'button'>) {
-  return (
-    <button
-      className={classnames('rp-bulk-actions-btn', className)}
-      {...rest}
-    />
-  )
-}
-
-export default BulkActions

+ 0 - 91
services/web/frontend/js/features/source-editor/components/review-panel/entries/bulk-actions-entry/modal.tsx

@@ -1,91 +0,0 @@
-import { useState, useCallback } from 'react'
-import { useTranslation } from 'react-i18next'
-import { useReviewPanelUpdaterFnsContext } from '../../../../context/review-panel/review-panel-context'
-import OLModal, {
-  OLModalBody,
-  OLModalFooter,
-  OLModalHeader,
-  OLModalTitle,
-} from '@/features/ui/components/ol/ol-modal'
-import OLButton from '@/features/ui/components/ol/ol-button'
-
-type BulkActionsModalProps = {
-  show: boolean
-  setShow: React.Dispatch<React.SetStateAction<boolean>>
-  isAccept: boolean
-  nChanges: number
-  onConfirm: () => void
-}
-
-function Modal({
-  show,
-  setShow,
-  isAccept,
-  nChanges,
-  onConfirm,
-}: BulkActionsModalProps) {
-  const { t } = useTranslation()
-
-  return (
-    <OLModal show={show} onHide={() => setShow(false)}>
-      <OLModalHeader closeButton>
-        <OLModalTitle>
-          {isAccept ? t('accept_all') : t('reject_all')}
-        </OLModalTitle>
-      </OLModalHeader>
-      <OLModalBody>
-        <p>
-          {isAccept
-            ? t('bulk_accept_confirm', { nChanges })
-            : t('bulk_reject_confirm', { nChanges })}
-        </p>
-      </OLModalBody>
-      <OLModalFooter>
-        <OLButton variant="secondary" onClick={() => setShow(false)}>
-          {t('cancel')}
-        </OLButton>
-        <OLButton variant="primary" onClick={onConfirm}>
-          {t('ok')}
-        </OLButton>
-      </OLModalFooter>
-    </OLModal>
-  )
-}
-
-export function useBulkActionsModal() {
-  const [show, setShow] = useState(false)
-  const [isAccept, setIsAccept] = useState(false)
-  const { bulkAcceptActions, bulkRejectActions } =
-    useReviewPanelUpdaterFnsContext()
-
-  const handleShowBulkAcceptDialog = useCallback(() => {
-    setIsAccept(true)
-    setShow(true)
-  }, [])
-
-  const handleShowBulkRejectDialog = useCallback(() => {
-    setIsAccept(false)
-    setShow(true)
-  }, [])
-
-  const handleConfirmDialog = useCallback(() => {
-    if (isAccept) {
-      bulkAcceptActions()
-    } else {
-      bulkRejectActions()
-    }
-
-    setShow(false)
-  }, [bulkAcceptActions, bulkRejectActions, isAccept])
-
-  return {
-    show,
-    setShow,
-    isAccept,
-    handleShowBulkAcceptDialog,
-    handleShowBulkRejectDialog,
-    handleConfirmDialog,
-  }
-}
-
-export default Modal

+ 0 - 161
services/web/frontend/js/features/source-editor/components/review-panel/entries/change-entry.tsx

@@ -1,161 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import { memo, useState } from 'react'
-import EntryContainer from './entry-container'
-import EntryCallout from './entry-callout'
-import EntryActions from './entry-actions'
-import { useReviewPanelUpdaterFnsContext } from '../../../context/review-panel/review-panel-context'
-import { formatTime } from '../../../../utils/format-date'
-import classnames from 'classnames'
-import { ReviewPanelChangeEntry } from '../../../../../../../types/review-panel/entry'
-import { BaseChangeEntryProps } from '../types/base-change-entry-props'
-import comparePropsWithShallowArrayCompare from '../utils/compare-props-with-shallow-array-compare'
-import useIndicatorHover from '../hooks/use-indicator-hover'
-import EntryIndicator from './entry-indicator'
-import { useEntryClick } from '@/features/source-editor/components/review-panel/hooks/use-entry-click'
-import MaterialIcon from '@/shared/components/material-icon'
-
-interface ChangeEntryProps extends BaseChangeEntryProps {
-  type: ReviewPanelChangeEntry['type']
-}
-
-function ChangeEntry({
-  docId,
-  entryId,
-  permissions,
-  user,
-  content,
-  offset,
-  type,
-  focused,
-  entryIds,
-  timestamp,
-  contentLimit = 40,
-}: ChangeEntryProps) {
-  const { t } = useTranslation()
-  const { handleLayoutChange, acceptChanges, rejectChanges } =
-    useReviewPanelUpdaterFnsContext()
-  const [isCollapsed, setIsCollapsed] = useState(true)
-  const {
-    hoverCoords,
-    indicatorRef,
-    endHover,
-    handleIndicatorMouseEnter,
-    handleIndicatorClick,
-  } = useIndicatorHover()
-
-  const contentToDisplay = isCollapsed
-    ? content.substring(0, contentLimit)
-    : content
-
-  const needsCollapsing = content.length > contentLimit
-  const isInsert = type === 'insert'
-
-  const handleEntryClick = useEntryClick(docId, offset, endHover)
-
-  const handleToggleCollapse = () => {
-    setIsCollapsed(value => !value)
-    handleLayoutChange()
-  }
-
-  return (
-    <EntryContainer
-      id={entryId}
-      hoverCoords={hoverCoords}
-      onClick={handleEntryClick}
-      onMouseLeave={endHover}
-    >
-      <EntryCallout className={`rp-entry-callout-${type}`} />
-      <EntryIndicator
-        ref={indicatorRef}
-        focused={focused}
-        onMouseEnter={handleIndicatorMouseEnter}
-        onClick={handleIndicatorClick}
-      >
-        {isInsert ? (
-          <MaterialIcon type="edit" />
-        ) : (
-          <i className="rp-icon-delete" />
-        )}
-      </EntryIndicator>
-      <div
-        className={classnames('rp-entry', `rp-entry-${type}`, {
-          'rp-entry-focused': focused,
-        })}
-      >
-        <div className="rp-entry-body">
-          <div className="rp-entry-action-icon">
-            {isInsert ? (
-              <MaterialIcon type="edit" />
-            ) : (
-              <i className="rp-icon-delete" />
-            )}
-          </div>
-          <div className="rp-entry-details">
-            <div className="rp-entry-description">
-              <span>
-                {isInsert ? (
-                  <>
-                    {t('tracked_change_added')}&nbsp;
-                    <ins className="rp-content-highlight">
-                      {contentToDisplay}
-                    </ins>
-                  </>
-                ) : (
-                  <>
-                    {t('tracked_change_deleted')}&nbsp;
-                    <del className="rp-content-highlight">
-                      {contentToDisplay}
-                    </del>
-                  </>
-                )}
-                {needsCollapsing && (
-                  <button
-                    className="rp-collapse-toggle"
-                    onClick={handleToggleCollapse}
-                  >
-                    {isCollapsed
-                      ? `… (${t('show_all')})`
-                      : ` (${t('show_less')})`}
-                  </button>
-                )}
-              </span>
-            </div>
-            <div className="rp-entry-metadata">
-              <span className="rp-entry-metadata-element">
-                {formatTime(timestamp, 'MMM D, Y h:mm A')}
-              </span>
-              {user && (
-                <span className="rp-entry-metadata-element">
-                  &nbsp;&bull;&nbsp;
-                  <span
-                    className="rp-entry-user"
-                    style={{ color: `hsl(${user.hue}, 70%, 40%)` }}
-                  >
-                    {user.name ?? t('anonymous')}
-                  </span>
-                </span>
-              )}
-            </div>
-          </div>
-        </div>
-        {permissions.write && (
-          <EntryActions>
-            <EntryActions.Button onClick={() => rejectChanges(entryIds)}>
-              <MaterialIcon type="close" />
-              &nbsp;{t('reject')}
-            </EntryActions.Button>
-            <EntryActions.Button onClick={() => acceptChanges(entryIds)}>
-              <MaterialIcon type="check" />
-              &nbsp;{t('accept')}
-            </EntryActions.Button>
-          </EntryActions>
-        )}
-      </div>
-    </EntryContainer>
-  )
-}
-
-export default memo(
-  ChangeEntry,
-  comparePropsWithShallowArrayCompare('entryIds')
-)

+ 0 - 185
services/web/frontend/js/features/source-editor/components/review-panel/entries/comment-entry.tsx

@@ -1,185 +0,0 @@
-import { useState, useRef, useEffect, memo } from 'react'
-import { useTranslation } from 'react-i18next'
-import EntryContainer from './entry-container'
-import EntryCallout from './entry-callout'
-import EntryActions from './entry-actions'
-import Comment from './comment'
-import AutoExpandingTextArea from '../../../../../shared/components/auto-expanding-text-area'
-import { useReviewPanelUpdaterFnsContext } from '../../../context/review-panel/review-panel-context'
-import classnames from 'classnames'
-import { ThreadId } from '../../../../../../../types/review-panel/review-panel'
-import { Permissions } from '@/features/ide-react/types/permissions'
-import { DocId } from '../../../../../../../types/project-settings'
-import { ReviewPanelCommentThread } from '../../../../../../../types/review-panel/comment-thread'
-import { ReviewPanelCommentEntry } from '../../../../../../../types/review-panel/entry'
-import useIndicatorHover from '../hooks/use-indicator-hover'
-import EntryIndicator from './entry-indicator'
-import { useEntryClick } from '@/features/source-editor/components/review-panel/hooks/use-entry-click'
-import MaterialIcon from '@/shared/components/material-icon'
-import LoadingSpinner from '@/shared/components/loading-spinner'
-
-type CommentEntryProps = {
-  docId: DocId
-  entryId: ThreadId
-  thread: ReviewPanelCommentThread | undefined
-  threadId: ReviewPanelCommentEntry['thread_id']
-  permissions: Permissions
-} & Pick<ReviewPanelCommentEntry, 'offset' | 'focused'>
-
-function CommentEntry({
-  docId,
-  entryId,
-  thread,
-  threadId,
-  offset,
-  focused,
-  permissions,
-}: CommentEntryProps) {
-  const { t } = useTranslation()
-  const { resolveComment, submitReply, handleLayoutChange } =
-    useReviewPanelUpdaterFnsContext()
-  const [replyContent, setReplyContent] = useState('')
-  const [animating, setAnimating] = useState(false)
-  const [resolved, setResolved] = useState(false)
-  const entryDivRef = useRef<HTMLDivElement | null>(null)
-  const {
-    hoverCoords,
-    indicatorRef,
-    endHover,
-    handleIndicatorMouseEnter,
-    handleIndicatorClick,
-  } = useIndicatorHover()
-
-  const handleEntryClick = useEntryClick(docId, offset)
-
-  const handleAnimateAndCallOnResolve = () => {
-    setAnimating(true)
-
-    if (entryDivRef.current) {
-      entryDivRef.current.style.top = '0'
-    }
-
-    setTimeout(() => {
-      setAnimating(false)
-      setResolved(true)
-      resolveComment(docId, entryId)
-    }, 350)
-  }
-
-  const handleCommentReplyKeyPress = (
-    e: React.KeyboardEvent<HTMLTextAreaElement>
-  ) => {
-    if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
-      e.preventDefault()
-
-      if (replyContent.length) {
-        ;(e.target as HTMLTextAreaElement).blur()
-        submitReply(threadId, replyContent)
-        setReplyContent('')
-      }
-    }
-  }
-
-  const handleOnReply = () => {
-    if (replyContent.length) {
-      submitReply(threadId, replyContent)
-      setReplyContent('')
-    }
-  }
-
-  const submitting = Boolean(thread?.submitting)
-
-  // Update the layout when loading finishes
-  useEffect(() => {
-    if (!submitting) {
-      // Ensure everything is rendered in the DOM before updating the layout.
-      handleLayoutChange({ async: true })
-    }
-  }, [submitting, handleLayoutChange])
-
-  if (!thread || resolved) {
-    return null
-  }
-
-  return (
-    <EntryContainer
-      id={entryId}
-      hoverCoords={hoverCoords}
-      onClick={handleEntryClick}
-      onMouseLeave={endHover}
-    >
-      {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
-      <div
-        className={classnames('rp-comment-wrapper', {
-          'rp-comment-wrapper-resolving': animating,
-        })}
-      >
-        <EntryCallout className="rp-entry-callout-comment" />
-        <EntryIndicator
-          ref={indicatorRef}
-          focused={focused}
-          onMouseEnter={handleIndicatorMouseEnter}
-          onClick={handleIndicatorClick}
-        >
-          <MaterialIcon type="mode_comment" />
-        </EntryIndicator>
-        <div
-          className={classnames('rp-entry', 'rp-entry-comment', {
-            'rp-entry-focused': focused,
-            'rp-entry-comment-resolving': animating,
-          })}
-          ref={entryDivRef}
-        >
-          {!submitting && (!thread || thread.messages.length === 0) && (
-            <div className="text-center p-1">{t('no_comments')}</div>
-          )}
-          <div className="rp-comment-loaded">
-            {thread.messages.map(comment => (
-              <Comment
-                key={comment.id}
-                thread={thread}
-                threadId={threadId}
-                comment={comment}
-              />
-            ))}
-          </div>
-          {submitting && (
-            <LoadingSpinner className="d-flex justify-content-center" />
-          )}
-          {permissions.comment && (
-            <div className="rp-comment-reply">
-              <AutoExpandingTextArea
-                className="rp-comment-input"
-                onChange={e => setReplyContent(e.target.value)}
-                onKeyPress={handleCommentReplyKeyPress}
-                onClick={e => e.stopPropagation()}
-                onResize={handleLayoutChange}
-                placeholder={t('hit_enter_to_reply')}
-                value={replyContent}
-              />
-            </div>
-          )}
-          <EntryActions>
-            {permissions.comment && permissions.write && (
-              <EntryActions.Button onClick={handleAnimateAndCallOnResolve}>
-                <MaterialIcon type="inbox" />
-                &nbsp;{t('resolve')}
-              </EntryActions.Button>
-            )}
-            {permissions.comment && (
-              <EntryActions.Button
-                onClick={handleOnReply}
-                disabled={!replyContent.length}
-              >
-                <MaterialIcon type="reply" />
-                &nbsp;{t('reply')}
-              </EntryActions.Button>
-            )}
-          </EntryActions>
-        </div>
-      </div>
-    </EntryContainer>
-  )
-}
-
-export default memo(CommentEntry)

+ 0 - 121
services/web/frontend/js/features/source-editor/components/review-panel/entries/comment.tsx

@@ -1,121 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import { memo, useState } from 'react'
-import AutoExpandingTextArea from '../../../../../shared/components/auto-expanding-text-area'
-import { formatTime } from '../../../../utils/format-date'
-import { useReviewPanelUpdaterFnsContext } from '../../../context/review-panel/review-panel-context'
-import { ReviewPanelCommentThread } from '../../../../../../../types/review-panel/comment-thread'
-import {
-  ReviewPanelCommentThreadMessage,
-  ThreadId,
-} from '../../../../../../../types/review-panel/review-panel'
-
-type CommentProps = {
-  thread: ReviewPanelCommentThread
-  threadId: ThreadId
-  comment: ReviewPanelCommentThreadMessage
-}
-
-function Comment({ thread, threadId, comment }: CommentProps) {
-  const { t } = useTranslation()
-  const { handleLayoutChange, deleteComment, saveEdit } =
-    useReviewPanelUpdaterFnsContext()
-  const [deleting, setDeleting] = useState(false)
-  const [editing, setEditing] = useState(false)
-
-  const handleConfirmDelete = () => {
-    setDeleting(true)
-    handleLayoutChange()
-  }
-
-  const handleDoDelete = () => {
-    setDeleting(false)
-    deleteComment(threadId, comment.id)
-    handleLayoutChange()
-  }
-
-  const handleCancelDelete = () => {
-    setDeleting(false)
-    handleLayoutChange()
-  }
-
-  const handleStartEditing = () => {
-    setEditing(true)
-    handleLayoutChange()
-  }
-
-  const handleSaveEditOnEnter = (
-    e: React.KeyboardEvent<HTMLTextAreaElement>
-  ) => {
-    if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
-      e.preventDefault()
-      handleSaveEdit(e)
-    }
-  }
-
-  const handleSaveEdit = (
-    e:
-      | React.FocusEvent<HTMLTextAreaElement>
-      | React.KeyboardEvent<HTMLTextAreaElement>
-  ) => {
-    setEditing(false)
-    saveEdit(threadId, comment.id, (e.target as HTMLTextAreaElement).value)
-  }
-
-  return (
-    <div className="rp-comment">
-      <p className="rp-comment-content">
-        {editing ? (
-          <AutoExpandingTextArea
-            className="rp-comment-input"
-            defaultValue={comment.content}
-            onKeyPress={handleSaveEditOnEnter}
-            onBlur={handleSaveEdit}
-            onClick={e => e.stopPropagation()}
-            onResize={handleLayoutChange}
-            autoFocus // eslint-disable-line jsx-a11y/no-autofocus
-          />
-        ) : (
-          <>
-            <span
-              className="rp-entry-user"
-              style={{ color: `hsl(${comment.user.hue}, 70%, 40%` }}
-            >
-              {comment.user.name}:
-            </span>
-            &nbsp;
-            {comment.content}
-          </>
-        )}
-      </p>
-      {!editing && (
-        <div className="rp-entry-metadata">
-          {!deleting && formatTime(comment.timestamp, 'MMM D, Y h:mm A')}
-          {comment.user.isSelf && !deleting && (
-            <span className="rp-comment-actions">
-              &nbsp;&bull;&nbsp;
-              <button onClick={handleStartEditing}>{t('edit')}</button>
-              {thread.messages.length > 1 && (
-                <>
-                  &nbsp;&bull;&nbsp;
-                  <button onClick={handleConfirmDelete}>{t('delete')}</button>
-                </>
-              )}
-            </span>
-          )}
-          {comment.user.isSelf && deleting && (
-            <span className="rp-confim-delete">
-              {t('are_you_sure')}&nbsp;&bull;&nbsp;
-              <button type="button" onClick={handleDoDelete}>
-                {t('delete')}
-              </button>
-              &nbsp;&bull;&nbsp;
-              <button onClick={handleCancelDelete}>{t('cancel')}</button>
-            </span>
-          )}
-        </div>
-      )}
-    </div>
-  )
-}
-
-export default memo(Comment)

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

@@ -1,19 +0,0 @@
-import classnames from 'classnames'
-
-function EntryActions({
-  className,
-  ...rest
-}: React.ComponentPropsWithoutRef<'div'>) {
-  return <div className={classnames('rp-entry-actions', className)} {...rest} />
-}
-
-EntryActions.Button = function EntryActionsButton({
-  className,
-  ...rest
-}: React.ComponentPropsWithoutRef<'button'>) {
-  return (
-    <button className={classnames('rp-entry-button', className)} {...rest} />
-  )
-}
-
-export default EntryActions

+ 0 - 7
services/web/frontend/js/features/source-editor/components/review-panel/entries/entry-callout.tsx

@@ -1,7 +0,0 @@
-import classnames from 'classnames'
-
-function EntryCallout({ className, ...rest }: React.ComponentProps<'div'>) {
-  return <div className={classnames('rp-entry-callout', className)} {...rest} />
-}
-
-export default EntryCallout

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

@@ -1,42 +0,0 @@
-import classNames from 'classnames'
-import { createPortal } from 'react-dom'
-import { useReviewPanelValueContext } from '@/features/source-editor/context/review-panel/review-panel-context'
-import { Coordinates } from '../hooks/use-indicator-hover'
-
-function EntryContainer({
-  id,
-  className,
-  hoverCoords,
-  ...rest
-}: React.ComponentProps<'div'> & {
-  hoverCoords?: Coordinates | null
-}) {
-  const { layoutToLeft } = useReviewPanelValueContext()
-
-  const container = (
-    <div
-      className={classNames('rp-entry-wrapper', className)}
-      data-entry-id={id}
-      {...rest}
-    />
-  )
-
-  if (hoverCoords) {
-    // Render in a floating positioned container
-    return createPortal(
-      <div
-        className={classNames('rp-floating-entry', {
-          'rp-floating-entry-layout-left': layoutToLeft,
-        })}
-        style={{ left: hoverCoords.x + 'px', top: hoverCoords.y + 'px' }}
-      >
-        {container}
-      </div>,
-      document.body
-    )
-  } else {
-    return container
-  }
-}
-
-export default EntryContainer

+ 0 - 30
services/web/frontend/js/features/source-editor/components/review-panel/entries/entry-indicator.tsx

@@ -1,30 +0,0 @@
-import classnames from 'classnames'
-import { forwardRef } from 'react'
-
-type EntryIndicatorProps = {
-  focused: boolean
-  onMouseEnter: () => void
-  onClick: () => void
-  children: React.ReactNode
-}
-
-const EntryIndicator = forwardRef<HTMLDivElement, EntryIndicatorProps>(
-  ({ focused, onMouseEnter, onClick, children }, ref) => {
-    return (
-      /* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
-      <div
-        ref={ref}
-        className={classnames('rp-entry-indicator', {
-          'rp-entry-indicator-focused': focused,
-        })}
-        onClick={onClick}
-        onMouseEnter={onMouseEnter}
-      >
-        {children}
-      </div>
-    )
-  }
-)
-EntryIndicator.displayName = 'EntryIndicator'
-
-export default EntryIndicator

+ 0 - 96
services/web/frontend/js/features/source-editor/components/review-panel/entries/overview-file-entries.tsx

@@ -1,96 +0,0 @@
-import { useMemo } from 'react'
-import ChangeEntry from '@/features/source-editor/components/review-panel/entries/change-entry'
-import AggregateChangeEntry from '@/features/source-editor/components/review-panel/entries/aggregate-change-entry'
-import CommentEntry from '@/features/source-editor/components/review-panel/entries/comment-entry'
-import { useReviewPanelValueContext } from '@/features/source-editor/context/review-panel/review-panel-context'
-import {
-  ReviewPanelDocEntries,
-  ThreadId,
-} from '../../../../../../../types/review-panel/review-panel'
-import { ReviewPanelEntry } from '../../../../../../../types/review-panel/entry'
-import { DocId } from '../../../../../../../types/project-settings'
-
-type OverviewFileEntriesProps = {
-  docId: DocId
-  docEntries: ReviewPanelDocEntries
-}
-
-function OverviewFileEntries({ docId, docEntries }: OverviewFileEntriesProps) {
-  const { commentThreads, permissions, users } = useReviewPanelValueContext()
-
-  const objectEntries = useMemo(() => {
-    const entries = Object.entries(docEntries) as Array<
-      [ThreadId, ReviewPanelEntry]
-    >
-
-    const orderedEntries = entries.sort(([, entryA], [, entryB]) => {
-      return entryA.offset - entryB.offset
-    })
-
-    return orderedEntries
-  }, [docEntries])
-
-  return (
-    <div className="rp-overview-file-entries">
-      {objectEntries.map(([id, entry]) => {
-        if (entry.type === 'insert' || entry.type === 'delete') {
-          return (
-            <ChangeEntry
-              key={id}
-              docId={docId}
-              entryId={id}
-              permissions={permissions}
-              user={users[entry.metadata.user_id]}
-              content={entry.content}
-              offset={entry.offset}
-              type={entry.type}
-              focused={entry.focused}
-              entryIds={entry.entry_ids}
-              timestamp={entry.metadata.ts}
-            />
-          )
-        }
-
-        if (entry.type === 'aggregate-change') {
-          return (
-            <AggregateChangeEntry
-              key={id}
-              docId={docId}
-              entryId={id}
-              permissions={permissions}
-              user={users[entry.metadata.user_id]}
-              content={entry.content}
-              replacedContent={entry.metadata.replaced_content}
-              offset={entry.offset}
-              focused={entry.focused}
-              entryIds={entry.entry_ids}
-              timestamp={entry.metadata.ts}
-            />
-          )
-        }
-
-        if (entry.type === 'comment') {
-          const thread = commentThreads[entry.thread_id]
-          if (!thread?.resolved) {
-            return (
-              <CommentEntry
-                key={id}
-                docId={docId}
-                threadId={entry.thread_id}
-                thread={thread}
-                entryId={id}
-                offset={entry.offset}
-                focused={entry.focused}
-                permissions={permissions}
-              />
-            )
-          }
-        }
-
-        return null
-      })}
-    </div>
-  )
-}
-
-export default OverviewFileEntries

+ 0 - 127
services/web/frontend/js/features/source-editor/components/review-panel/entries/resolved-comment-entry.tsx

@@ -1,127 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import { memo, useState } from 'react'
-import Linkify from 'react-linkify'
-import { formatTime } from '../../../../utils/format-date'
-import { useReviewPanelUpdaterFnsContext } from '../../../context/review-panel/review-panel-context'
-import { FilteredResolvedComments } from '../toolbar/resolved-comments-dropdown'
-import { Permissions } from '@/features/ide-react/types/permissions'
-
-function LinkDecorator(
-  decoratedHref: string,
-  decoratedText: string,
-  key: number
-) {
-  return (
-    <a target="blank" rel="noreferrer noopener" href={decoratedHref} key={key}>
-      {decoratedText}
-    </a>
-  )
-}
-
-type ResolvedCommentEntryProps = {
-  thread: FilteredResolvedComments
-  permissions: Permissions
-  contentLimit?: number
-}
-
-function ResolvedCommentEntry({
-  thread,
-  permissions,
-  contentLimit = 40,
-}: ResolvedCommentEntryProps) {
-  const { t } = useTranslation()
-  const { unresolveComment, deleteThread } = useReviewPanelUpdaterFnsContext()
-  const [isCollapsed, setIsCollapsed] = useState(false)
-  const needsCollapsing = thread.content.length > contentLimit
-  const content = isCollapsed
-    ? thread.content.substring(0, contentLimit)
-    : thread.content
-
-  const handleUnresolve = () => {
-    unresolveComment(thread.docId, thread.threadId)
-  }
-
-  const handleDelete = () => {
-    deleteThread(thread.docId, thread.threadId)
-  }
-
-  return (
-    <div className="rp-resolved-comment">
-      <div>
-        <div className="rp-resolved-comment-context">
-          {t('quoted_text_in')}
-          &nbsp;
-          <span className="rp-resolved-comment-context-file">
-            {thread.docName}
-          </span>
-          <p className="rp-resolved-comment-context-quote">
-            <span>{content}</span>
-          </p>
-          {needsCollapsing && (
-            <>
-              &nbsp;
-              <button
-                className="rp-collapse-toggle"
-                onClick={() => setIsCollapsed(value => !value)}
-              >
-                {isCollapsed ? `… (${t('show_all')})` : ` (${t('show_less')})`}
-              </button>
-            </>
-          )}
-        </div>
-        {thread.messages.map((comment, index) => {
-          const showUser =
-            index === 0 ||
-            comment.user.id !== thread.messages[index - 1].user.id
-
-          return (
-            <div className="rp-comment" key={comment.id}>
-              <p className="rp-comment-content">
-                {showUser && (
-                  <span
-                    className="rp-entry-user"
-                    style={{ color: `hsl(${comment.user.hue}, 70%, 40%)` }}
-                  >
-                    {comment.user.name}:&nbsp;
-                  </span>
-                )}
-                <Linkify componentDecorator={LinkDecorator}>
-                  {comment.content}
-                </Linkify>
-              </p>
-              <div className="rp-entry-metadata">
-                {formatTime(comment.timestamp, 'MMM D, Y h:mm A')}
-              </div>
-            </div>
-          )
-        })}
-        <div className="rp-comment rp-comment-resolver">
-          <p className="rp-comment-resolver-content">
-            <span
-              className="rp-entry-user"
-              style={{ color: `hsl(${thread.resolved_by_user.hue}, 70%, 40%)` }}
-            >
-              {thread.resolved_by_user.name}:&nbsp;
-            </span>
-            {t('mark_as_resolved')}.
-          </p>
-          <div className="rp-entry-metadata">
-            {formatTime(thread.resolved_at, 'MMM D, Y h:mm A')}
-          </div>
-        </div>
-      </div>
-      {permissions.comment && permissions.write && (
-        <div className="rp-entry-actions">
-          <button className="rp-entry-button" onClick={handleUnresolve}>
-            {t('reopen')}
-          </button>
-          <button className="rp-entry-button" onClick={handleDelete}>
-            {t('delete')}
-          </button>
-        </div>
-      )}
-    </div>
-  )
-}
-
-export default memo(ResolvedCommentEntry)

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

@@ -1,109 +0,0 @@
-import { memo } from 'react'
-import ChangeEntry from './entries/change-entry'
-import AggregateChangeEntry from './entries/aggregate-change-entry'
-import CommentEntry from './entries/comment-entry'
-import AddCommentEntry from './entries/add-comment-entry'
-import BulkActionsEntry from './entries/bulk-actions-entry/bulk-actions-entry'
-import {
-  ReviewPanelDocEntries,
-  ThreadId,
-} from '../../../../../../types/review-panel/review-panel'
-import { useReviewPanelValueContext } from '../../context/review-panel/review-panel-context'
-import { useEditorContext } from '../../../../shared/context/editor-context'
-
-type Props = {
-  entry: ReviewPanelDocEntries[keyof ReviewPanelDocEntries]
-  id: ThreadId | 'add-comment' | 'bulk-actions'
-}
-
-const isEntryAThreadId = (
-  entry: keyof ReviewPanelDocEntries
-): entry is ThreadId => entry !== 'add-comment' && entry !== 'bulk-actions'
-
-function Entry({ entry, id }: Props) {
-  const {
-    commentThreads,
-    openDocId,
-    permissions,
-    loadingThreads,
-    users,
-    nVisibleSelectedChanges: nChanges,
-  } = useReviewPanelValueContext()
-  const { isRestrictedTokenMember } = useEditorContext()
-
-  if (!entry.visible || !openDocId) {
-    return null
-  }
-
-  if (
-    isEntryAThreadId(id) &&
-    (entry.type === 'insert' || entry.type === 'delete')
-  ) {
-    return (
-      <ChangeEntry
-        key={id}
-        docId={openDocId}
-        entryId={id}
-        permissions={permissions}
-        user={users[entry.metadata.user_id]}
-        content={entry.content}
-        offset={entry.offset}
-        type={entry.type}
-        focused={entry.focused}
-        entryIds={entry.entry_ids}
-        timestamp={entry.metadata.ts}
-      />
-    )
-  }
-
-  if (isEntryAThreadId(id) && entry.type === 'aggregate-change') {
-    return (
-      <AggregateChangeEntry
-        key={id}
-        docId={openDocId}
-        entryId={id}
-        permissions={permissions}
-        user={users[entry.metadata.user_id]}
-        content={entry.content}
-        replacedContent={entry.metadata.replaced_content}
-        offset={entry.offset}
-        focused={entry.focused}
-        entryIds={entry.entry_ids}
-        timestamp={entry.metadata.ts}
-      />
-    )
-  }
-
-  if (isEntryAThreadId(id) && entry.type === 'comment' && !loadingThreads) {
-    return (
-      <CommentEntry
-        key={id}
-        docId={openDocId}
-        threadId={entry.thread_id}
-        thread={commentThreads[entry.thread_id]}
-        entryId={id}
-        offset={entry.offset}
-        focused={entry.focused}
-        permissions={permissions}
-      />
-    )
-  }
-
-  if (
-    entry.type === 'add-comment' &&
-    permissions.comment &&
-    !isRestrictedTokenMember
-  ) {
-    return <AddCommentEntry key={id} />
-  }
-
-  if (entry.type === 'bulk-actions' && permissions.write) {
-    return (
-      <BulkActionsEntry key={id} entryId={entry.type} nChanges={nChanges} />
-    )
-  }
-
-  return null
-}
-
-export default memo(Entry)

+ 0 - 33
services/web/frontend/js/features/source-editor/components/review-panel/hooks/use-entry-click.ts

@@ -1,33 +0,0 @@
-import { useReviewPanelUpdaterFnsContext } from '@/features/source-editor/context/review-panel/review-panel-context'
-import { DocId } from '../../../../../../../types/project-settings'
-
-export function useEntryClick(
-  docId: DocId,
-  offset: number,
-  cb?: (e: React.MouseEvent<HTMLDivElement>) => void
-) {
-  const { gotoEntry } = useReviewPanelUpdaterFnsContext()
-
-  return (e: React.MouseEvent<HTMLDivElement>) => {
-    const target = e.target as Element
-
-    // Ignore clicks inside interactive elements
-    if (!target.closest('textarea, button, a')) {
-      // If the user was making a selection within the entry rather than
-      // clicking it, ignore the click. Do this by checking whether there is a
-      // selection that intersects with the target, in which case we assume
-      // the user was making a selection
-      const selection = window.getSelection()
-      if (
-        !selection ||
-        selection.isCollapsed ||
-        selection.rangeCount === 0 ||
-        !selection.getRangeAt(0).intersectsNode(target)
-      ) {
-        gotoEntry(docId, offset)
-      }
-    }
-
-    cb?.(e)
-  }
-}

+ 0 - 69
services/web/frontend/js/features/source-editor/components/review-panel/hooks/use-indicator-hover.ts

@@ -1,69 +0,0 @@
-import { useCallback, useEffect, useRef, useState } from 'react'
-import { flushSync } from 'react-dom'
-import { useReviewPanelUpdaterFnsContext } from '../../../context/review-panel/review-panel-context'
-import { useLayoutContext } from '../../../../../shared/context/layout-context'
-import EntryIndicator from '../entries/entry-indicator'
-
-export type Coordinates = {
-  x: number
-  y: number
-}
-
-function useIndicatorHover() {
-  const [hoverCoords, setHoverCoords] = useState<Coordinates | null>(null)
-  const { toggleReviewPanel } = useReviewPanelUpdaterFnsContext()
-  const { reviewPanelOpen } = useLayoutContext()
-  const { setLayoutSuspended, handleLayoutChange } =
-    useReviewPanelUpdaterFnsContext()
-  const indicatorRef = useRef<React.ElementRef<typeof EntryIndicator> | null>(
-    null
-  )
-
-  const endHover = useCallback(() => {
-    if (!reviewPanelOpen) {
-      // Use flushSync to ensure that React renders immediately. This is
-      // necessary to ensure that the subsequent layout update acts on the
-      // updated DOM.
-      flushSync(() => {
-        setHoverCoords(null)
-        setLayoutSuspended(false)
-      })
-      handleLayoutChange({ force: true })
-    }
-  }, [handleLayoutChange, reviewPanelOpen, setLayoutSuspended])
-
-  const handleIndicatorMouseEnter = () => {
-    const rect = indicatorRef.current?.getBoundingClientRect()
-    setHoverCoords({
-      x: rect?.left || 0,
-      y: rect?.top || 0,
-    })
-    setLayoutSuspended(true)
-  }
-
-  const handleIndicatorClick = () => {
-    setHoverCoords(null)
-    setLayoutSuspended(false)
-    toggleReviewPanel()
-  }
-
-  useEffect(() => {
-    if (hoverCoords) {
-      window.addEventListener('editor:scroll', endHover)
-
-      return () => {
-        window.removeEventListener('editor:scroll', endHover)
-      }
-    }
-  }, [hoverCoords, endHover])
-
-  return {
-    hoverCoords,
-    indicatorRef,
-    endHover,
-    handleIndicatorMouseEnter,
-    handleIndicatorClick,
-  }
-}
-
-export default useIndicatorHover

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

@@ -1,65 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import classnames from 'classnames'
-import {
-  useReviewPanelValueContext,
-  useReviewPanelUpdaterFnsContext,
-} from '../../context/review-panel/review-panel-context'
-import { isCurrentFileView, isOverviewView } from '../../utils/sub-view'
-import { useCallback } from 'react'
-import { useResizeObserver } from '../../../../shared/hooks/use-resize-observer'
-import MaterialIcon from '@/shared/components/material-icon'
-
-function Nav() {
-  const { t } = useTranslation()
-  const { subView } = useReviewPanelValueContext()
-  const { handleSetSubview, setNavHeight } = useReviewPanelUpdaterFnsContext()
-  const handleResize = useCallback(
-    el => {
-      // Use requestAnimationFrame to prevent errors like "ResizeObserver loop
-      // completed with undelivered notifications" that occur if onResize does
-      // something complicated. The cost of this is that onResize lags one frame
-      // behind, but it's unlikely to matter.
-      const height = el.offsetHeight
-      window.requestAnimationFrame(() => setNavHeight(height))
-    },
-    [setNavHeight]
-  )
-  const { elementRef } = useResizeObserver(handleResize)
-
-  return (
-    <div ref={elementRef} className="rp-nav" role="tablist">
-      <button
-        type="button"
-        id="review-panel-tab-current-file"
-        role="tab"
-        aria-selected={isCurrentFileView(subView)}
-        aria-controls="review-panel-current-file"
-        tabIndex={isCurrentFileView(subView) ? 0 : -1}
-        className={classnames('rp-nav-item', {
-          'rp-nav-item-active': isCurrentFileView(subView),
-        })}
-        onClick={() => handleSetSubview('cur_file')}
-      >
-        <MaterialIcon type="description" className="align-middle" />
-        <span className="rp-nav-label">{t('current_file')}</span>
-      </button>
-      <button
-        type="button"
-        id="review-panel-tab-overview"
-        role="tab"
-        aria-selected={isOverviewView(subView)}
-        aria-controls="review-panel-overview"
-        tabIndex={isOverviewView(subView) ? 0 : -1}
-        className={classnames('rp-nav-item', {
-          'rp-nav-item-active': isOverviewView(subView),
-        })}
-        onClick={() => handleSetSubview('overview')}
-      >
-        <MaterialIcon type="list" className="align-middle" />
-        <span className="rp-nav-label">{t('overview')}</span>
-      </button>
-    </div>
-  )
-}
-
-export default Nav

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

@@ -1,43 +0,0 @@
-import Container from './container'
-import Toggler from './toggler'
-import Toolbar from './toolbar/toolbar'
-import Nav from './nav'
-import OverviewFile from './overview-file'
-import { useReviewPanelValueContext } from '../../context/review-panel/review-panel-context'
-import { useFileTreeData } from '@/shared/context/file-tree-data-context'
-import { memo } from 'react'
-import LoadingSpinner from '@/shared/components/loading-spinner'
-
-function OverviewContainer() {
-  const { isOverviewLoading } = useReviewPanelValueContext()
-  const { docs } = useFileTreeData()
-
-  return (
-    <Container>
-      <Toggler />
-      <Toolbar />
-      <div
-        className="rp-entry-list"
-        id="review-panel-overview"
-        role="tabpanel"
-        tabIndex={0}
-        aria-labelledby="review-panel-tab-overview"
-      >
-        {isOverviewLoading ? (
-          <LoadingSpinner className="d-flex justify-content-center my-2" />
-        ) : (
-          docs?.map(doc => (
-            <OverviewFile
-              key={doc.doc.id}
-              docId={doc.doc.id}
-              docPath={doc.path}
-            />
-          ))
-        )}
-      </div>
-      <Nav />
-    </Container>
-  )
-}
-
-export default memo(OverviewContainer)

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

@@ -1,68 +0,0 @@
-import { useMemo } from 'react'
-import {
-  useReviewPanelUpdaterFnsContext,
-  useReviewPanelValueContext,
-} from '../../context/review-panel/review-panel-context'
-import classnames from 'classnames'
-import { ReviewPanelDocEntries } from '../../../../../../types/review-panel/review-panel'
-import { MainDocument } from '../../../../../../types/project-settings'
-import OverviewFileEntries from '@/features/source-editor/components/review-panel/entries/overview-file-entries'
-import MaterialIcon from '@/shared/components/material-icon'
-
-type OverviewFileProps = {
-  docId: MainDocument['doc']['id']
-  docPath: MainDocument['path']
-}
-
-function OverviewFile({ docId, docPath }: OverviewFileProps) {
-  const { entries, collapsed } = useReviewPanelValueContext()
-  const { setCollapsed } = useReviewPanelUpdaterFnsContext()
-
-  const docCollapsed = collapsed[docId]
-  const docEntries = useMemo(() => {
-    return docId in entries ? entries[docId] : ({} as ReviewPanelDocEntries)
-  }, [docId, entries])
-  const entryCount = useMemo(() => {
-    return Object.keys(docEntries).filter(
-      key => key !== 'add-comment' && key !== 'bulk-actions'
-    ).length
-  }, [docEntries])
-
-  const handleToggleCollapsed = () => {
-    setCollapsed({ ...collapsed, [docId]: !docCollapsed })
-  }
-
-  return (
-    <div className="rp-overview-file">
-      {entryCount > 0 && (
-        // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
-        <div
-          className="rp-overview-file-header"
-          onClick={handleToggleCollapsed}
-        >
-          <span
-            className={classnames('rp-overview-file-header-collapse', {
-              'rp-overview-file-header-collapse-on': docCollapsed,
-            })}
-          >
-            <MaterialIcon type="expand_more" />
-          </span>
-          {docPath}
-          {docCollapsed && (
-            <>
-              &nbsp;
-              <span className="rp-overview-file-num-entries">
-                ({entryCount})
-              </span>
-            </>
-          )}
-        </div>
-      )}
-      {!docCollapsed && (
-        <OverviewFileEntries docId={docId} docEntries={docEntries} />
-      )}
-    </div>
-  )
-}
-
-export default OverviewFile

+ 0 - 502
services/web/frontend/js/features/source-editor/components/review-panel/positioned-entries.tsx

@@ -1,502 +0,0 @@
-import { useEffect, useLayoutEffect, useRef, useCallback } from 'react'
-import { useLayoutContext } from '../../../../shared/context/layout-context'
-import {
-  ReviewPanelEntry,
-  ReviewPanelEntryScreenPos,
-} from '../../../../../../types/review-panel/entry'
-import { debugConsole } from '../../../../utils/debugging'
-import { useReviewPanelValueContext } from '../../context/review-panel/review-panel-context'
-import { ReviewPanelDocEntries } from '../../../../../../types/review-panel/review-panel'
-import { dispatchReviewPanelLayout } from '../../extensions/changes/change-manager'
-import { isEqual } from 'lodash'
-
-type Positions = {
-  entryTop: number
-  callout: { top: number; height: number; inverted: boolean }
-  inViewport: boolean
-}
-
-type EntryView = {
-  entryId: keyof ReviewPanelDocEntries
-  wrapper: HTMLElement
-  indicator: HTMLElement | null
-  box: HTMLElement
-  callout: HTMLElement
-  layout: HTMLElement
-  height: number
-  entry: ReviewPanelEntry
-  hasScreenPos: boolean
-  positions?: Positions
-  previousPositions?: Positions
-}
-
-type EntryPositions = Pick<EntryView, 'entryId' | 'positions'>
-
-type PositionedEntriesProps = {
-  entries: Array<[keyof ReviewPanelDocEntries, ReviewPanelEntry]>
-  contentHeight: number
-  children: React.ReactNode
-}
-
-const initialLayoutInfo = {
-  focusedEntryIndex: 0,
-  overflowTop: 0,
-  height: 0,
-  positions: [] as EntryPositions[],
-}
-
-function css(el: HTMLElement, props: React.CSSProperties) {
-  Object.assign(el.style, props)
-}
-
-function calculateCalloutPosition(
-  screenPos: ReviewPanelEntryScreenPos,
-  entryTop: number,
-  lineHeight: number
-) {
-  const height = screenPos.height ?? lineHeight
-  const originalTop = screenPos.y
-  const inverted = entryTop <= originalTop
-  return {
-    top: inverted ? entryTop + height : originalTop + height - 1,
-    height: Math.abs(entryTop - originalTop),
-    inverted,
-  }
-}
-
-function positionsEqual(
-  entryPos1: EntryPositions[],
-  entryPos2: EntryPositions[]
-) {
-  return isEqual(entryPos1, entryPos2)
-}
-
-function updateEntryPositions(
-  entryView: EntryView,
-  entryTop: number,
-  lineHeight: number
-) {
-  const callout = calculateCalloutPosition(
-    entryView.entry.screenPos,
-    entryTop,
-    lineHeight
-  )
-  entryView.positions = {
-    entryTop,
-    callout,
-    inViewport: entryView.entry.inViewport,
-  }
-}
-
-function calculateEntryViewPositions(
-  entryViews: EntryView[],
-  lineHeight: number,
-  calculateTop: (originalTop: number, height: number) => number
-) {
-  for (const entryView of entryViews) {
-    if (entryView.hasScreenPos) {
-      const entryTop = calculateTop(
-        entryView.entry.screenPos.y,
-        entryView.height
-      )
-      updateEntryPositions(entryView, entryTop, lineHeight)
-    }
-  }
-}
-
-function hideOrShowEntries(entryViews: EntryView[]) {
-  for (const entryView of entryViews) {
-    // Completely hide any entry that has no screen position
-    entryView.wrapper.classList.toggle(
-      'rp-entry-hidden',
-      !entryView.hasScreenPos
-    )
-  }
-}
-
-function applyEntryTop(entryView: EntryView, top: number) {
-  entryView.box.style.top = top + 'px'
-
-  if (entryView.indicator) {
-    entryView.indicator.style.top = top + 'px'
-  }
-}
-
-function applyEntryVisibility(entryView: EntryView) {
-  // The entry element is invisible by default, to avoid flickering when
-  // positioning for the first time. Here we make sure it becomes visible after
-  // acquiring a screen position.
-  if (entryView.entry.inViewport) {
-    entryView.box.style.visibility = 'visible'
-    entryView.callout.style.visibility = 'visible'
-  }
-}
-
-// Position everything where it was before, taking into account the new top
-// overflow
-function moveEntriesToInitialPosition(
-  entryViews: EntryView[],
-  overflowTop: number
-) {
-  for (const entryView of entryViews) {
-    const { callout: calloutEl, positions } = entryView
-    if (positions) {
-      const { entryTop, callout } = positions
-
-      // Position the main wrapper in its original position, if it had
-      // one, or its new position otherwise
-      const entryTopInitial = entryView.previousPositions
-        ? entryView.previousPositions.entryTop
-        : entryTop
-
-      applyEntryVisibility(entryView)
-      applyEntryTop(entryView, entryTopInitial + overflowTop)
-
-      // Position the callout element in its original position, if it had
-      // one, or its new position otherwise
-      calloutEl.classList.toggle('rp-entry-callout-inverted', callout.inverted)
-      const calloutTopInitial = entryView.previousPositions
-        ? entryView.previousPositions.callout.top
-        : callout.top
-
-      css(calloutEl, {
-        top: calloutTopInitial + overflowTop + 'px',
-        height: callout.height + 'px',
-      })
-    }
-  }
-}
-
-function moveEntriesToFinalPositions(
-  entryViews: EntryView[],
-  overflowTop: number,
-  shouldApplyVisibility: boolean
-) {
-  for (const entryView of entryViews) {
-    const { callout: calloutEl, positions } = entryView
-    if (positions) {
-      const { entryTop, callout } = positions
-
-      if (shouldApplyVisibility) {
-        applyEntryVisibility(entryView)
-      }
-
-      // Position the main wrapper, if it's moved
-      if (entryView.previousPositions?.entryTop !== entryTop) {
-        entryView.box.style.top = entryTop + overflowTop + 'px'
-      }
-
-      if (entryView.indicator) {
-        entryView.indicator.style.top = entryTop + overflowTop + 'px'
-      }
-
-      // Position the callout element
-      if (entryView.previousPositions?.callout.top !== callout.top) {
-        calloutEl.style.top = callout.top + overflowTop + 'px'
-      }
-    }
-  }
-}
-
-function PositionedEntries({
-  entries,
-  contentHeight,
-  children,
-}: PositionedEntriesProps) {
-  const { navHeight, toolbarHeight, lineHeight, layoutSuspended } =
-    useReviewPanelValueContext()
-  const containerRef = useRef<HTMLDivElement | null>(null)
-  const { reviewPanelOpen } = useLayoutContext()
-  const previousLayoutInfoRef = useRef(initialLayoutInfo)
-
-  const resetLayout = () => {
-    previousLayoutInfoRef.current = initialLayoutInfo
-  }
-
-  const layout = useCallback(
-    (animate = true) => {
-      const container = containerRef.current
-      if (!container) {
-        return
-      }
-
-      const padding = reviewPanelOpen ? 8 : 4
-      const toolbarPaddedHeight = reviewPanelOpen ? toolbarHeight + 6 : 0
-      const navPaddedHeight = reviewPanelOpen ? navHeight + 4 : 0
-
-      // Create a list of entry views, typing together DOM elements and model.
-      // No measuring or style change is done at this point.
-      const entryViews: EntryView[] = []
-
-      // TODO: Look into tying the entry to the DOM element without going via a DOM data attribute
-      for (const wrapper of container.querySelectorAll<HTMLElement>(
-        '.rp-entry-wrapper'
-      )) {
-        const entryId = wrapper.dataset.entryId as
-          | EntryView['entryId']
-          | undefined
-        if (!entryId) {
-          throw new Error('Could not find an entry ID')
-        }
-
-        const entry = entries.find(value => value[0] === entryId)?.[1]
-        if (!entry) {
-          throw new Error(`Could not find an entry for ID ${entryId}`)
-        }
-
-        const indicator = wrapper.querySelector<HTMLElement>(
-          '.rp-entry-indicator'
-        )
-        const box = wrapper.querySelector<HTMLElement>('.rp-entry')
-        const callout = wrapper.querySelector<HTMLElement>('.rp-entry-callout')
-        const layoutElement = reviewPanelOpen ? box : indicator
-
-        if (box && callout && layoutElement) {
-          const previousPositions =
-            previousLayoutInfoRef.current?.positions.find(
-              pos => pos.entryId === entryId
-            )?.positions
-          const hasScreenPos = Boolean(entry.screenPos)
-          entryViews.push({
-            entryId,
-            wrapper,
-            indicator,
-            box,
-            callout,
-            layout: layoutElement,
-            hasScreenPos,
-            height: 0,
-            entry,
-            previousPositions,
-          })
-        } else {
-          debugConsole.log(
-            'Entry wrapper is missing indicator, box or callout, so ignoring',
-            wrapper
-          )
-        }
-      }
-
-      if (entryViews.length === 0) {
-        resetLayout()
-        return
-      }
-
-      entryViews.sort((a, b) => a.entry.offset - b.entry.offset)
-
-      // Do the DOM interaction in three phases:
-      //
-      // - Apply the `display` property to all elements whose visibility has
-      //   changed. This needs to happen first in order to measure heights.
-      // - Measure the height of each entry
-      // - Move each entry without animation to their original position
-      //   relative to the editor content
-      // - Re-enable animation and position each entry
-      //
-      // The idea is to batch DOM reads and writes to avoid layout thrashing. In
-      // this case, the best we can do is a write phase, a read phase then a
-      // final write phase.
-      // See https://web.dev/avoid-large-complex-layouts-and-layout-thrashing/
-
-      // First, update display for each entry that needs it
-      hideOrShowEntries(entryViews)
-
-      // Next, measure the height of each entry
-      for (const entryView of entryViews) {
-        if (entryView.hasScreenPos) {
-          entryView.height = entryView.layout.offsetHeight
-        }
-      }
-
-      // Calculate positions for all positioned entries, starting by calculating
-      // which entry to put in its desired position and anchor everything else
-      // around. If there is an explicitly focused entry, use that.
-      let focusedEntryIndex = entryViews.findIndex(view => view.entry.focused)
-      if (focusedEntryIndex === -1) {
-        // There is no explicitly focused entry, so use the focused entry from the
-        // previous layout. This will be the first entry in the list if there was
-        // no previous layout.
-        focusedEntryIndex = Math.min(
-          previousLayoutInfoRef.current.focusedEntryIndex,
-          entryViews.length - 1
-        )
-        // If the entry from the previous layout has no screen position, fall back
-        // to the first entry in the list that does.
-        if (!entryViews[focusedEntryIndex].hasScreenPos) {
-          focusedEntryIndex = entryViews.findIndex(view => view.hasScreenPos)
-        }
-      }
-
-      // If there is no entry with a screen position, bail out
-      if (focusedEntryIndex === -1) {
-        return
-      }
-
-      const focusedEntryView = entryViews[focusedEntryIndex]
-
-      // If the focused entry has no screenPos, we can't position other
-      // entryViews relative to it, so we position all other entryViews as
-      // though the focused entry is at the top and the rest follow it
-      const entryViewsAfter = focusedEntryView.hasScreenPos
-        ? entryViews.slice(focusedEntryIndex + 1)
-        : [...entryViews]
-      const entryViewsBefore = focusedEntryView.hasScreenPos
-        ? entryViews.slice(0, focusedEntryIndex).reverse() // Work through backwards, starting with the one just above
-        : []
-
-      debugConsole.log('focusedEntryIndex', focusedEntryIndex)
-
-      let lastEntryBottom = 0
-      let firstEntryTop = 0
-
-      // Put the focused entry as close as possible to where it wants to be
-      if (focusedEntryView.hasScreenPos) {
-        const focusedEntryScreenPos = focusedEntryView.entry.screenPos
-        const entryTop = Math.max(focusedEntryScreenPos.y, toolbarPaddedHeight)
-        updateEntryPositions(focusedEntryView, entryTop, lineHeight)
-        lastEntryBottom = entryTop + focusedEntryView.height
-        firstEntryTop = entryTop
-      }
-
-      // Calculate positions for entries that are below the focused entry
-      calculateEntryViewPositions(
-        entryViewsAfter,
-        lineHeight,
-        (originalTop: number, height: number) => {
-          const top = Math.max(originalTop, lastEntryBottom + padding)
-          lastEntryBottom = top + height
-          return top
-        }
-      )
-
-      // Calculate positions for entries that are above the focused entry
-      calculateEntryViewPositions(
-        entryViewsBefore,
-        lineHeight,
-        (originalTop: number, height: number) => {
-          const originalBottom = originalTop + height
-          const bottom = Math.min(originalBottom, firstEntryTop - padding)
-          const top = bottom - height
-          firstEntryTop = top
-          return top
-        }
-      )
-
-      // Calculate the new top overflow
-      const overflowTop = Math.max(0, toolbarPaddedHeight - firstEntryTop)
-
-      // Check whether the positions of any entry have changed since the last
-      // layout
-      const positions = entryViews.map(
-        (entryView): EntryPositions => ({
-          entryId: entryView.entryId,
-          positions: entryView.positions,
-        })
-      )
-
-      const positionsChanged = !positionsEqual(
-        previousLayoutInfoRef.current.positions,
-        positions
-      )
-
-      // Check whether the top overflow or review panel height have changed
-      const overflowTopChanged =
-        overflowTop !== previousLayoutInfoRef.current.overflowTop
-
-      const height = lastEntryBottom + navPaddedHeight
-      const heightChanged = height !== previousLayoutInfoRef.current.height
-      const isMoveRequired = positionsChanged || overflowTopChanged
-
-      // Move entries into their initial positions, if animating, avoiding
-      // animation until the final animated move
-      if (animate && isMoveRequired) {
-        container.classList.add('no-animate')
-        moveEntriesToInitialPosition(entryViews, overflowTop)
-      }
-
-      // Inform the editor of the new top overflow and/or height if either has
-      // changed
-      if (overflowTopChanged || heightChanged) {
-        window.dispatchEvent(
-          new CustomEvent('review-panel:event', {
-            detail: {
-              type: 'sizes',
-              payload: {
-                overflowTop,
-                height,
-              },
-            },
-          })
-        )
-      }
-
-      // Do the final move
-      if (isMoveRequired) {
-        if (animate) {
-          container.classList.remove('no-animate')
-          moveEntriesToFinalPositions(entryViews, overflowTop, false)
-        } else {
-          container.classList.add('no-animate')
-          moveEntriesToFinalPositions(entryViews, overflowTop, true)
-
-          // Force reflow now to ensure that entries are moved without animation
-          // eslint-disable-next-line no-void
-          void container.offsetHeight
-
-          container.classList.remove('no-animate')
-        }
-      }
-
-      previousLayoutInfoRef.current = {
-        positions,
-        focusedEntryIndex,
-        height,
-        overflowTop,
-      }
-    },
-    [entries, lineHeight, navHeight, reviewPanelOpen, toolbarHeight]
-  )
-
-  useLayoutEffect(() => {
-    const callback = (event: Event) => {
-      const e = event as CustomEvent
-
-      if (!layoutSuspended) {
-        // Clear previous positions if forcing a layout
-        if (e.detail.force) {
-          previousLayoutInfoRef.current = initialLayoutInfo
-        }
-        layout(e.detail.animate)
-      }
-    }
-
-    window.addEventListener('review-panel:layout', callback)
-
-    return () => {
-      window.removeEventListener('review-panel:layout', callback)
-    }
-  }, [layoutSuspended, layout])
-
-  // Layout on first render. This is necessary to ensure layout happens when
-  // switching from overview to current file view
-  useEffect(() => {
-    dispatchReviewPanelLayout()
-  }, [])
-
-  // Ensure a full layout is performed after opening or closing the review panel
-  useEffect(() => {
-    previousLayoutInfoRef.current = initialLayoutInfo
-  }, [reviewPanelOpen])
-
-  return (
-    <div
-      ref={containerRef}
-      className="rp-entry-list-react"
-      style={{ height: `${contentHeight}px` }}
-    >
-      {children}
-    </div>
-  )
-}
-
-export default PositionedEntries

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

@@ -1,37 +0,0 @@
-import { FC, memo } from 'react'
-import EditorWidgets from '@/features/source-editor/components/review-panel/editor-widgets/editor-widgets'
-import { isCurrentFileView } from '@/features/source-editor/utils/sub-view'
-import CurrentFileContainer from '@/features/source-editor/components/review-panel/current-file-container'
-import OverviewContainer from '@/features/source-editor/components/review-panel/overview-container'
-import { useReviewPanelValueContext } from '@/features/source-editor/context/review-panel/review-panel-context'
-import { useLayoutContext } from '@/shared/context/layout-context'
-import classnames from 'classnames'
-
-const ReviewPanelContent: FC = () => {
-  const { subView, loadingThreads, layoutToLeft } = useReviewPanelValueContext()
-  const { reviewPanelOpen, miniReviewPanelVisible } = useLayoutContext()
-
-  const className = classnames('review-panel-wrapper', {
-    'rp-state-current-file': subView === 'cur_file',
-    'rp-state-current-file-expanded': subView === 'cur_file' && reviewPanelOpen,
-    'rp-state-current-file-mini': subView === 'cur_file' && !reviewPanelOpen,
-    'rp-state-overview': subView === 'overview',
-    'rp-size-mini': miniReviewPanelVisible,
-    'rp-size-expanded': reviewPanelOpen,
-    'rp-layout-left': layoutToLeft,
-    'rp-loading-threads': loadingThreads,
-  })
-
-  return (
-    <div className={className}>
-      <EditorWidgets />
-      {isCurrentFileView(subView) ? (
-        <CurrentFileContainer />
-      ) : (
-        <OverviewContainer />
-      )}
-    </div>
-  )
-}
-
-export default memo(ReviewPanelContent)

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

@@ -1,19 +0,0 @@
-import React, { FC, lazy, Suspense } from 'react'
-import { useFeatureFlag } from '@/shared/context/split-test-context'
-import LoadingSpinner from '@/shared/components/loading-spinner'
-
-const ReviewPanel = lazy(() => import('./review-panel'))
-
-const ReviewPanelNew = lazy(
-  () => import('../../../review-panel-new/components/review-panel-container')
-)
-
-export const ReviewPanelMigration: FC = () => {
-  const newReviewPanel = useFeatureFlag('review-panel-redesign')
-
-  return (
-    <Suspense fallback={<LoadingSpinner delay={500} />}>
-      {newReviewPanel ? <ReviewPanelNew /> : <ReviewPanel />}
-    </Suspense>
-  )
-}

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

@@ -1,18 +0,0 @@
-import ReactDOM from 'react-dom'
-import { useCodeMirrorViewContext } from '../codemirror-context'
-import { ReviewPanelProvider } from '../../context/review-panel/review-panel-context'
-import { memo } from 'react'
-import ReviewPanelContent from '@/features/source-editor/components/review-panel/review-panel-content'
-
-function ReviewPanel() {
-  const view = useCodeMirrorViewContext()
-
-  return ReactDOM.createPortal(
-    <ReviewPanelProvider>
-      <ReviewPanelContent />
-    </ReviewPanelProvider>,
-    view.scrollDOM
-  )
-}
-
-export default memo(ReviewPanel)

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

@@ -1,29 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import { useReviewPanelUpdaterFnsContext } from '../../context/review-panel/review-panel-context'
-import MaterialIcon from '@/shared/components/material-icon'
-
-function Toggler() {
-  const { t } = useTranslation()
-  const { toggleReviewPanel } = useReviewPanelUpdaterFnsContext()
-
-  const handleTogglerClick = (event: React.MouseEvent<HTMLButtonElement>) => {
-    const target = event.target as HTMLButtonElement
-    target.blur()
-    toggleReviewPanel()
-  }
-
-  return (
-    <button
-      type="button"
-      className="review-panel-toggler"
-      onClick={handleTogglerClick}
-    >
-      <span className="sr-only">{t('hotkey_toggle_review_panel')}</span>
-      <span className="review-panel-toggler-icon">
-        <MaterialIcon type="chevron_left" />
-      </span>
-    </button>
-  )
-}
-
-export default Toggler

+ 0 - 133
services/web/frontend/js/features/source-editor/components/review-panel/toolbar/resolved-comments-dropdown.tsx

@@ -1,133 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import { useState, useMemo, useCallback } from 'react'
-import ResolvedCommentsScroller from './resolved-comments-scroller'
-import classnames from 'classnames'
-import {
-  useReviewPanelUpdaterFnsContext,
-  useReviewPanelValueContext,
-} from '../../../context/review-panel/review-panel-context'
-import {
-  ReviewPanelDocEntries,
-  ThreadId,
-} from '../../../../../../../types/review-panel/review-panel'
-import { ReviewPanelResolvedCommentThread } from '../../../../../../../types/review-panel/comment-thread'
-import { DocId } from '../../../../../../../types/project-settings'
-import { ReviewPanelEntry } from '../../../../../../../types/review-panel/entry'
-import { useFileTreeData } from '@/shared/context/file-tree-data-context'
-import LoadingSpinner from '@/shared/components/loading-spinner'
-import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
-import MaterialIcon from '@/shared/components/material-icon'
-
-export interface FilteredResolvedComments
-  extends ReviewPanelResolvedCommentThread {
-  content: string
-  threadId: ThreadId
-  entryId: ThreadId
-  docId: DocId
-  docName: string | null
-}
-
-function ResolvedCommentsDropdown() {
-  const { t } = useTranslation()
-  const [isOpen, setIsOpen] = useState(false)
-  const [isLoading, setIsLoading] = useState(false)
-  const { commentThreads, resolvedComments, permissions } =
-    useReviewPanelValueContext()
-  const { docs } = useFileTreeData()
-
-  const { refreshResolvedCommentsDropdown } = useReviewPanelUpdaterFnsContext()
-
-  const handleResolvedCommentsClick = () => {
-    setIsOpen(isOpen => {
-      if (!isOpen) {
-        setIsLoading(true)
-        refreshResolvedCommentsDropdown().finally(() => setIsLoading(false))
-      }
-
-      return !isOpen
-    })
-  }
-
-  const getDocNameById = useCallback(
-    (docId: DocId) => {
-      return docs?.find(doc => doc.doc.id === docId)?.doc.name || null
-    },
-    [docs]
-  )
-
-  const filteredResolvedComments = useMemo(() => {
-    const comments: FilteredResolvedComments[] = []
-
-    for (const [docId, docEntries] of Object.entries(resolvedComments) as Array<
-      [DocId, ReviewPanelDocEntries]
-    >) {
-      for (const [entryId, entry] of Object.entries(docEntries) as Array<
-        [ThreadId, ReviewPanelEntry]
-      >) {
-        if (entry.type === 'comment') {
-          const threadId = entry.thread_id
-          const thread =
-            threadId in commentThreads
-              ? commentThreads[entry.thread_id]
-              : undefined
-
-          if (thread?.resolved) {
-            comments.push({
-              ...thread,
-              content: entry.content,
-              threadId,
-              entryId,
-              docId,
-              docName: getDocNameById(docId),
-            })
-          }
-        }
-      }
-    }
-
-    return comments
-  }, [commentThreads, getDocNameById, resolvedComments])
-
-  return (
-    <div className="resolved-comments">
-      <div
-        aria-hidden="true"
-        className={classnames('resolved-comments-backdrop', {
-          'resolved-comments-backdrop-visible': isOpen,
-        })}
-        onClick={() => setIsOpen(false)}
-      />
-
-      <OLTooltip
-        id="resolved-comments-toggle"
-        description={t('resolved_comments')}
-        overlayProps={{ container: document.body, placement: 'bottom' }}
-      >
-        <button
-          className="resolved-comments-toggle"
-          onClick={handleResolvedCommentsClick}
-          aria-label={t('resolved_comments')}
-        >
-          <MaterialIcon type="inbox" />
-        </button>
-      </OLTooltip>
-
-      <div
-        className={classnames('resolved-comments-dropdown', {
-          'resolved-comments-dropdown-open': isOpen,
-        })}
-      >
-        {isLoading ? (
-          <LoadingSpinner className="d-flex justify-content-center my-2" />
-        ) : isOpen ? (
-          <ResolvedCommentsScroller
-            resolvedComments={filteredResolvedComments}
-            permissions={permissions}
-          />
-        ) : null}
-      </div>
-    </div>
-  )
-}
-
-export default ResolvedCommentsDropdown

+ 0 - 40
services/web/frontend/js/features/source-editor/components/review-panel/toolbar/resolved-comments-scroller.tsx

@@ -1,40 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import { useMemo } from 'react'
-import ResolvedCommentEntry from '../entries/resolved-comment-entry'
-import { FilteredResolvedComments } from './resolved-comments-dropdown'
-import { Permissions } from '@/features/ide-react/types/permissions'
-
-type ResolvedCommentsScrollerProps = {
-  resolvedComments: FilteredResolvedComments[]
-  permissions: Permissions
-}
-
-function ResolvedCommentsScroller({
-  resolvedComments,
-  permissions,
-}: ResolvedCommentsScrollerProps) {
-  const { t } = useTranslation()
-
-  const sortedResolvedComments = useMemo(() => {
-    return [...resolvedComments].sort((a, b) => {
-      return Date.parse(b.resolved_at) - Date.parse(a.resolved_at)
-    })
-  }, [resolvedComments])
-
-  return (
-    <div className="resolved-comments-scroller">
-      {sortedResolvedComments.map(comment => (
-        <ResolvedCommentEntry
-          key={comment.entryId}
-          thread={comment}
-          permissions={permissions}
-        />
-      ))}
-      {!resolvedComments.length && (
-        <div className="text-center p-1">{t('no_resolved_threads')}</div>
-      )}
-    </div>
-  )
-}
-
-export default ResolvedCommentsScroller

+ 0 - 82
services/web/frontend/js/features/source-editor/components/review-panel/toolbar/toggle-menu.tsx

@@ -1,82 +0,0 @@
-import { memo, useState } from 'react'
-import { Trans } from 'react-i18next'
-import TrackChangesMenu from '@/features/source-editor/components/review-panel/toolbar/track-changes-menu'
-import UpgradeTrackChangesModal from '../upgrade-track-changes-modal'
-import { useProjectContext } from '../../../../../shared/context/project-context'
-import {
-  useReviewPanelUpdaterFnsContext,
-  useReviewPanelValueContext,
-} from '../../../context/review-panel/review-panel-context'
-import { send, sendMB } from '../../../../../infrastructure/event-tracking'
-import classnames from 'classnames'
-import MaterialIcon from '@/shared/components/material-icon'
-
-const sendAnalytics = () => {
-  send('subscription-funnel', 'editor-click-feature', 'real-time-track-changes')
-  sendMB('paywall-prompt', {
-    'paywall-type': 'track-changes',
-  })
-}
-
-function ToggleMenu() {
-  const project = useProjectContext()
-  const { setShouldCollapse } = useReviewPanelUpdaterFnsContext()
-  const { wantTrackChanges, shouldCollapse } = useReviewPanelValueContext()
-
-  const [showModal, setShowModal] = useState(false)
-
-  const handleToggleFullTCStateCollapse = () => {
-    if (project.features.trackChanges) {
-      setShouldCollapse(value => !value)
-    } else {
-      sendAnalytics()
-      setShowModal(true)
-    }
-  }
-
-  return (
-    <>
-      <span className="review-panel-toolbar-label">
-        {wantTrackChanges && (
-          <span className="review-panel-toolbar-icon-on">
-            <span className="track-changes-indicator-circle" />
-          </span>
-        )}
-
-        <button
-          className="review-panel-toolbar-collapse-button"
-          onClick={handleToggleFullTCStateCollapse}
-        >
-          <span>
-            {wantTrackChanges ? <TrackChangesOn /> : <TrackChangesOff />}
-          </span>
-          <span
-            className={classnames('rp-tc-state-collapse', {
-              'rp-tc-state-collapse-on': shouldCollapse,
-            })}
-          >
-            <MaterialIcon type="expand_more" />
-          </span>
-        </button>
-      </span>
-
-      {!shouldCollapse && <TrackChangesMenu />}
-
-      <UpgradeTrackChangesModal show={showModal} setShow={setShowModal} />
-    </>
-  )
-}
-
-const TrackChangesOn = memo(() => {
-  return (
-    <Trans i18nKey="track_changes_is_on" components={{ strong: <strong /> }} />
-  )
-})
-TrackChangesOn.displayName = 'TrackChangesOn'
-
-const TrackChangesOff = memo(() => (
-  <Trans i18nKey="track_changes_is_off" components={{ strong: <strong /> }} />
-))
-TrackChangesOff.displayName = 'TrackChangesOff'
-
-export default memo(ToggleMenu)

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

@@ -1,30 +0,0 @@
-import ResolvedCommentsDropdown from './resolved-comments-dropdown'
-import ToggleMenu from './toggle-menu'
-import { useReviewPanelUpdaterFnsContext } from '../../../context/review-panel/review-panel-context'
-import { useCallback } from 'react'
-import { useResizeObserver } from '../../../../../shared/hooks/use-resize-observer'
-
-function Toolbar() {
-  const { setToolbarHeight } = useReviewPanelUpdaterFnsContext()
-  const handleResize = useCallback(
-    el => {
-      // Use requestAnimationFrame to prevent errors like "ResizeObserver loop
-      // completed with undelivered notifications" that occur if onResize does
-      // something complicated. The cost of this is that onResize lags one frame
-      // behind, but it's unlikely to matter.
-      const height = el.offsetHeight
-      window.requestAnimationFrame(() => setToolbarHeight(height))
-    },
-    [setToolbarHeight]
-  )
-  const { elementRef } = useResizeObserver(handleResize)
-
-  return (
-    <div ref={elementRef} className="review-panel-toolbar">
-      <ResolvedCommentsDropdown />
-      <ToggleMenu />
-    </div>
-  )
-}
-
-export default Toolbar

+ 0 - 130
services/web/frontend/js/features/source-editor/components/review-panel/toolbar/track-changes-menu.tsx

@@ -1,130 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import TrackChangesToggle from '@/features/source-editor/components/review-panel/toolbar/track-changes-toggle'
-import {
-  useReviewPanelUpdaterFnsContext,
-  useReviewPanelValueContext,
-} from '@/features/source-editor/context/review-panel/review-panel-context'
-import { useProjectContext } from '@/shared/context/project-context'
-import classnames from 'classnames'
-import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
-
-function TrackChangesMenu() {
-  const { t } = useTranslation()
-  const project = useProjectContext()
-  const {
-    toggleTrackChangesForEveryone,
-    toggleTrackChangesForUser,
-    toggleTrackChangesForGuests,
-  } = useReviewPanelUpdaterFnsContext()
-  const {
-    permissions,
-    trackChangesState,
-    trackChangesOnForEveryone,
-    trackChangesOnForGuests,
-    trackChangesForGuestsAvailable,
-    formattedProjectMembers,
-  } = useReviewPanelValueContext()
-
-  return (
-    <ul className="rp-tc-state" data-testid="review-panel-track-changes-menu">
-      <li className="rp-tc-state-item rp-tc-state-item-everyone">
-        <OLTooltip
-          description={t('tc_switch_everyone_tip')}
-          id="track-changes-switch-everyone"
-          overlayProps={{
-            container: document.body,
-            placement: 'left',
-            delay: 1000,
-          }}
-        >
-          <span className="rp-tc-state-item-name">{t('tc_everyone')}</span>
-        </OLTooltip>
-
-        <TrackChangesToggle
-          id="track-changes-everyone"
-          description={t('track_changes_for_everyone')}
-          handleToggle={() =>
-            toggleTrackChangesForEveryone(!trackChangesOnForEveryone)
-          }
-          value={trackChangesOnForEveryone}
-          disabled={!project.features.trackChanges || !permissions.write}
-        />
-      </li>
-      {Object.values(formattedProjectMembers).map(member => (
-        <li className="rp-tc-state-item" key={member.id}>
-          <OLTooltip
-            description={t('tc_switch_user_tip')}
-            id="track-changes-switch-user"
-            overlayProps={{
-              container: document.body,
-              placement: 'left',
-              delay: 1000,
-            }}
-          >
-            <span
-              className={classnames('rp-tc-state-item-name', {
-                'rp-tc-state-item-name-disabled': trackChangesOnForEveryone,
-              })}
-            >
-              {member.name}
-            </span>
-          </OLTooltip>
-
-          <TrackChangesToggle
-            id={`track-changes-user-toggle-${member.id}`}
-            description={t('track_changes_for_x', { name: member.name })}
-            handleToggle={() =>
-              toggleTrackChangesForUser(
-                !trackChangesState[member.id]?.value,
-                member.id
-              )
-            }
-            value={Boolean(trackChangesState[member.id]?.value)}
-            disabled={
-              trackChangesOnForEveryone ||
-              !project.features.trackChanges ||
-              !permissions.write
-            }
-          />
-        </li>
-      ))}
-      <li className="rp-tc-state-separator" />
-      <li className="rp-tc-state-item">
-        <OLTooltip
-          description={t('tc_switch_guests_tip')}
-          id="track-changes-switch-guests"
-          overlayProps={{
-            container: document.body,
-            placement: 'left',
-            delay: 1000,
-          }}
-        >
-          <span
-            className={classnames('rp-tc-state-item-name', {
-              'rp-tc-state-item-name-disabled': trackChangesOnForEveryone,
-            })}
-          >
-            {t('tc_guests')}
-          </span>
-        </OLTooltip>
-
-        <TrackChangesToggle
-          id="track-changes-guests-toggle"
-          description="Track changes for guests"
-          handleToggle={() =>
-            toggleTrackChangesForGuests(!trackChangesOnForGuests)
-          }
-          value={trackChangesOnForGuests}
-          disabled={
-            trackChangesOnForEveryone ||
-            !project.features.trackChanges ||
-            !permissions.write ||
-            !trackChangesForGuestsAvailable
-          }
-        />
-      </li>
-    </ul>
-  )
-}
-
-export default TrackChangesMenu

+ 0 - 18
services/web/frontend/js/features/source-editor/components/review-panel/types/base-change-entry-props.ts

@@ -1,18 +0,0 @@
-import { ReviewPanelChangeEntry } from '../../../../../../../types/review-panel/entry'
-import { DocId } from '../../../../../../../types/project-settings'
-import {
-  ReviewPanelUser,
-  ThreadId,
-} from '../../../../../../../types/review-panel/review-panel'
-import { Permissions } from '@/features/ide-react/types/permissions'
-
-export interface BaseChangeEntryProps
-  extends Pick<ReviewPanelChangeEntry, 'content' | 'offset' | 'focused'> {
-  docId: DocId
-  entryId: ThreadId
-  permissions: Permissions
-  user: ReviewPanelUser | undefined
-  timestamp: ReviewPanelChangeEntry['metadata']['ts']
-  contentLimit?: number
-  entryIds: ReviewPanelChangeEntry['entry_ids']
-}

+ 0 - 31
services/web/frontend/js/features/source-editor/components/review-panel/utils/compare-props-with-shallow-array-compare.ts

@@ -1,31 +0,0 @@
-const shallowEqual = (arr1: unknown[], arr2: unknown[]) =>
-  arr1.length === arr2.length && !arr1.some((val, index) => val !== arr2[index])
-
-// Compares props for a component, but comparing the specified props using
-// shallow array comparison rather than identity
-export default function comparePropsWithShallowArrayCompare<
-  T extends Record<string, unknown>,
->(...args: Array<keyof T>) {
-  return (prevProps: T, nextProps: T) => {
-    for (const k in prevProps) {
-      const prev = prevProps[k]
-      const next = nextProps[k]
-      if (Object.is(prev, next)) {
-        continue
-      }
-
-      if (!args.includes(k)) {
-        return false
-      }
-
-      if (
-        !Array.isArray(prev) ||
-        !Array.isArray(next) ||
-        !shallowEqual(prev, next)
-      ) {
-        return false
-      }
-    }
-    return true
-  }
-}

+ 7 - 10
services/web/frontend/js/features/source-editor/components/toolbar/toolbar-items.tsx

@@ -11,7 +11,6 @@ import { useTranslation } from 'react-i18next'
 import { MathDropdown } from './math-dropdown'
 import { MathDropdown } from './math-dropdown'
 import { TableInserterDropdown } from './table-inserter-dropdown'
 import { TableInserterDropdown } from './table-inserter-dropdown'
 import { withinFormattingCommand } from '@/features/source-editor/utils/tree-operations/formatting'
 import { withinFormattingCommand } from '@/features/source-editor/utils/tree-operations/formatting'
-import { isSplitTestEnabled } from '@/utils/splitTestUtils'
 import { isMac } from '@/shared/utils/os'
 import { isMac } from '@/shared/utils/os'
 
 
 export const ToolbarItems: FC<{
 export const ToolbarItems: FC<{
@@ -123,15 +122,13 @@ export const ToolbarItems: FC<{
                 command={commands.wrapInHref}
                 command={commands.wrapInHref}
                 icon="add_link"
                 icon="add_link"
               />
               />
-              {isSplitTestEnabled('review-panel-redesign') && (
-                <ToolbarButton
-                  id="toolbar-add-comment"
-                  label={t('add_comment')}
-                  disabled={state.selection.main.empty}
-                  command={commands.addComment}
-                  icon="add_comment"
-                />
-              )}
+              <ToolbarButton
+                id="toolbar-add-comment"
+                label={t('add_comment')}
+                disabled={state.selection.main.empty}
+                command={commands.addComment}
+                icon="add_comment"
+              />
               <ToolbarButton
               <ToolbarButton
                 id="toolbar-ref"
                 id="toolbar-ref"
                 label={t('toolbar_insert_cross_reference')}
                 label={t('toolbar_insert_cross_reference')}

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

@@ -1,45 +0,0 @@
-import { createContext, useContext } from 'react'
-import type { ReviewPanelState } from './types/review-panel-state'
-import useReviewPanelState from '@/features/ide-react/context/review-panel/hooks/use-review-panel-state'
-
-export const ReviewPanelValueContext = createContext<
-  ReviewPanelState['values'] | undefined
->(undefined)
-
-export const ReviewPanelUpdaterFnsContext = createContext<
-  ReviewPanelState['updaterFns'] | undefined
->(undefined)
-
-export const ReviewPanelProvider: React.FC = ({ children }) => {
-  const { values, updaterFns } = useReviewPanelState()
-
-  return (
-    <ReviewPanelValueContext.Provider value={values}>
-      <ReviewPanelUpdaterFnsContext.Provider value={updaterFns}>
-        {children}
-      </ReviewPanelUpdaterFnsContext.Provider>
-    </ReviewPanelValueContext.Provider>
-  )
-}
-
-export function useReviewPanelValueContext() {
-  const context = useContext(ReviewPanelValueContext)
-
-  if (!context) {
-    throw new Error(
-      'ReviewPanelValueContext is only available inside ReviewPanelProvider'
-    )
-  }
-  return context
-}
-
-export function useReviewPanelUpdaterFnsContext() {
-  const context = useContext(ReviewPanelUpdaterFnsContext)
-
-  if (!context) {
-    throw new Error(
-      'ReviewPanelUpdaterFnsContext is only available inside ReviewPanelProvider'
-    )
-  }
-  return context
-}

+ 0 - 105
services/web/frontend/js/features/source-editor/context/review-panel/types/review-panel-state.ts

@@ -1,105 +0,0 @@
-import {
-  CommentId,
-  ReviewPanelCommentThreads,
-  ReviewPanelDocEntries,
-  ReviewPanelEntries,
-  ReviewPanelUsers,
-  SubView,
-  ThreadId,
-} from '../../../../../../../types/review-panel/review-panel'
-import { Permissions } from '@/features/ide-react/types/permissions'
-import { DocId } from '../../../../../../../types/project-settings'
-import { dispatchReviewPanelLayout } from '../../../extensions/changes/change-manager'
-import { UserId } from '../../../../../../../types/user'
-
-export interface ReviewPanelState {
-  values: {
-    collapsed: Record<DocId, boolean>
-    commentThreads: ReviewPanelCommentThreads
-    entries: ReviewPanelEntries
-    isAddingComment: boolean
-    loadingThreads: boolean
-    nVisibleSelectedChanges: number
-    permissions: Permissions
-    users: ReviewPanelUsers
-    resolvedComments: ReviewPanelEntries
-    shouldCollapse: boolean
-    navHeight: number
-    toolbarHeight: number
-    subView: SubView
-    wantTrackChanges: boolean
-    isOverviewLoading: boolean
-    openDocId: DocId | null
-    lineHeight: number
-    trackChangesState:
-      | Record<UserId, { value: boolean; syncState: 'synced' | 'pending' }>
-      | Record<UserId, undefined>
-    trackChangesOnForEveryone: boolean
-    trackChangesOnForGuests: boolean
-    trackChangesForGuestsAvailable: boolean
-    formattedProjectMembers: Record<
-      string,
-      {
-        id: UserId
-        name: string
-      }
-    >
-    layoutSuspended: boolean
-    unsavedComment: string
-    layoutToLeft: boolean
-  }
-  updaterFns: {
-    handleSetSubview: (subView: SubView) => void
-    handleLayoutChange: (
-      ...args: Parameters<typeof dispatchReviewPanelLayout>
-    ) => void
-    gotoEntry: (docId: DocId, entryOffset: number) => void
-    resolveComment: (docId: DocId, entryId: ThreadId) => void
-    deleteComment: (threadId: ThreadId, commentId: CommentId) => void
-    submitReply: (threadId: ThreadId, replyContent: string) => void
-    acceptChanges: (entryIds: ThreadId[]) => void
-    rejectChanges: (entryIds: ThreadId[]) => void
-    toggleTrackChangesForEveryone: (onForEveryone: boolean) => void
-    toggleTrackChangesForUser: (onForUser: boolean, userId: UserId) => void
-    toggleTrackChangesForGuests: (onForGuests: boolean) => void
-    toggleReviewPanel: () => void
-    bulkAcceptActions: () => void
-    bulkRejectActions: () => void
-    saveEdit: (
-      threadId: ThreadId,
-      commentId: CommentId,
-      content: string
-    ) => void
-    unresolveComment: (docId: DocId, threadId: ThreadId) => void
-    deleteThread: (docId: DocId, threadId: ThreadId) => void
-    refreshResolvedCommentsDropdown: () => Promise<
-      void | ReviewPanelDocEntries[]
-    >
-    submitNewComment: (content: string) => void
-    setIsAddingComment: React.Dispatch<
-      React.SetStateAction<Value<'isAddingComment'>>
-    >
-    setCollapsed: React.Dispatch<React.SetStateAction<Value<'collapsed'>>>
-    setShouldCollapse: React.Dispatch<
-      React.SetStateAction<Value<'shouldCollapse'>>
-    >
-    setNavHeight: React.Dispatch<React.SetStateAction<Value<'navHeight'>>>
-    setToolbarHeight: React.Dispatch<
-      React.SetStateAction<Value<'toolbarHeight'>>
-    >
-    setLayoutSuspended: React.Dispatch<
-      React.SetStateAction<Value<'layoutSuspended'>>
-    >
-    setUnsavedComment: React.Dispatch<
-      React.SetStateAction<Value<'unsavedComment'>>
-    >
-  }
-}
-
-// Getter for values
-export type Value<T extends keyof ReviewPanelState['values']> =
-  ReviewPanelState['values'][T]
-
-// Getter for stable functions
-export type UpdaterFn<T extends keyof ReviewPanelState['updaterFns']> =
-  ReviewPanelState['updaterFns'][T]

+ 0 - 616
services/web/frontend/js/features/source-editor/extensions/changes/change-manager.ts

@@ -1,616 +0,0 @@
-import { trackChangesAnnotation } from '../realtime'
-import { clearChangeMarkers, buildChangeMarkers } from '../track-changes'
-import {
-  setVerticalOverflow,
-  editorVerticalTopPadding,
-  updateChangesTopPadding,
-  updateSetsVerticalOverflow,
-} from '../vertical-overflow'
-import {
-  EditorSelection,
-  EditorState,
-  StateEffect,
-  TransactionSpec,
-} from '@codemirror/state'
-import { EditorView, ViewUpdate } from '@codemirror/view'
-import { fullHeightCoordsAtPos } from '../../utils/layer'
-import { debounce } from 'lodash'
-import { Change, EditOperation } from '../../../../../../types/change'
-import { ThreadId } from '../../../../../../types/review-panel/review-panel'
-import { isDeleteOperation, isInsertOperation } from '@/utils/operations'
-import { DocumentContainer } from '@/features/ide-react/editor/document-container'
-import { updateHasEffect } from '@/features/source-editor/utils/effects'
-import { isSplitTestEnabled } from '@/utils/splitTestUtils'
-
-// With less than this number of entries, don't bother culling to avoid
-// little UI jumps when scrolling.
-const CULL_AFTER = Infinity // Note: was 100 but couldn't scroll to see items outside the viewport
-
-export const dispatchEditorEvent = (type: string, payload?: unknown) => {
-  window.setTimeout(() => {
-    window.dispatchEvent(
-      new CustomEvent('editor:event', {
-        detail: { type, payload },
-      })
-    )
-  }, 0)
-}
-
-const dispatchReviewPanelLayoutImmediately = ({
-  force = false,
-  animate = true,
-} = {}) => {
-  window.dispatchEvent(
-    new CustomEvent('review-panel:layout', { detail: { force, animate } })
-  )
-}
-
-const scheduleDispatchReviewPanelLayout = debounce(
-  dispatchReviewPanelLayoutImmediately,
-  10
-)
-
-/**
- * @param force If true, forces the entries to be repositioned
- * @param animate
- * @param async If true, calls are briefly delayed and debounced
- */
-export const dispatchReviewPanelLayout = ({
-  force = false,
-  animate = true,
-  async = false,
-} = {}) => {
-  if (async) {
-    scheduleDispatchReviewPanelLayout({ force, animate })
-  } else {
-    dispatchReviewPanelLayoutImmediately({ force, animate })
-  }
-}
-
-export type ChangeManager = {
-  initialize: () => void
-  handleUpdate: (update: ViewUpdate) => void
-  destroy: () => void
-}
-
-export type UpdateType =
-  | 'edit'
-  | 'selectionChange'
-  | 'geometryChange'
-  | 'viewportChange'
-  | 'acceptChanges'
-  | 'rejectChanges'
-  | 'trackedChangesChange'
-  | 'topPaddingChange'
-
-export const createChangeManager = (
-  view: EditorView,
-  currentDoc: DocumentContainer
-): ChangeManager | undefined => {
-  if (isSplitTestEnabled('review-panel-redesign')) {
-    return undefined
-  }
-
-  /**
-   * Calculate the screen coordinates of each entry (change or comment),
-   * for use in the review panel.
-   *
-   * Returns a boolean indicating whether the visibility of any entry has changed
-   */
-  const recalculateScreenPositions = ({
-    entries,
-    updateType,
-  }: {
-    entries?: Record<string, any>
-    updateType: UpdateType
-  }) => {
-    const contentRect = view.contentDOM.getBoundingClientRect()
-
-    const { doc } = view.state
-
-    const items = Object.values(entries || {})
-
-    const allVisible = items.length <= CULL_AFTER
-    let visibilityChanged = false
-
-    const docLength = doc.length
-
-    const editorPaddingTop = editorVerticalTopPadding(view)
-
-    for (const entry of items) {
-      // TODO: clamp to max row and column, account for folding?
-      const coords = fullHeightCoordsAtPos(
-        view,
-        Math.min(entry.offset, docLength) // avoid exception for comments at end of document when deleting text
-      )
-
-      if (coords) {
-        const y = Math.round(coords.top - contentRect.top - editorPaddingTop)
-        const height = Math.round(coords.bottom - coords.top)
-
-        if (!entry.screenPos) {
-          visibilityChanged = true
-        }
-
-        entry.screenPos = { y, height, editorPaddingTop }
-        entry.inViewport = true
-      } else {
-        entry.inViewport = false
-      }
-
-      if (allVisible) {
-        if (!entry.visible) {
-          visibilityChanged = true
-        }
-        entry.visible = true
-      }
-    }
-
-    if (!allVisible) {
-      const { from, to } = view.viewport
-
-      for (const entry of items) {
-        const previouslyVisible = entry.visible
-
-        entry.visible = entry.offset >= from && entry.offset <= to
-
-        if (previouslyVisible !== entry.visible) {
-          visibilityChanged = true
-        }
-      }
-    }
-
-    return { visibilityChanged, updateType }
-  }
-
-  /**
-   * Add a comment (thread) to the ShareJS doc when it's created
-   */
-  const addComment = (offset: number, length: number, threadId: ThreadId) => {
-    currentDoc.submitOp({
-      c: view.state.doc.sliceString(offset, offset + length),
-      p: offset,
-      t: threadId,
-    })
-  }
-
-  /**
-   * Remove a comment (thread) from the range tracker when it's deleted
-   */
-  const removeComment = (commentId: string) => {
-    currentDoc.ranges!.removeCommentId(commentId)
-  }
-
-  /**
-   * Remove tracked changes from the range tracker when they're accepted
-   */
-  const acceptChanges = (changeIds: string[]) => {
-    currentDoc.ranges!.removeChangeIds(changeIds)
-  }
-
-  /**
-   * Remove tracked changes from the range tracker when they're rejected,
-   * and restore the original content
-   */
-  const rejectChanges = (changeIds: string[]) => {
-    const changes = currentDoc.ranges!.getChanges(
-      changeIds
-    ) as Change<EditOperation>[]
-
-    if (changes.length === 0) {
-      return {}
-    }
-
-    // When doing bulk rejections, adjacent changes might interact with each other.
-    // Consider an insertion with an adjacent deletion (which is a common use-case, replacing words):
-    //
-    //     "foo bar baz" -> "foo quux baz"
-    //
-    // The change above will be modeled with two ops, with the insertion going first:
-    //
-    //     foo quux baz
-    //         |--| -> insertion of "quux", op 1, at position 4
-    //             | -> deletion of "bar", op 2, pushed forward by "quux" to position 8
-    //
-    // When rejecting these changes at once, if the insertion is rejected first, we get unexpected
-    // results. What happens is:
-    //
-    //     1) Rejecting the insertion deletes the added word "quux", i.e., it removes 4 chars
-    //        starting from position 4;
-    //
-    //           "foo quux baz" -> "foo  baz"
-    //                |--| -> 4 characters to be removed
-    //
-    //     2) Rejecting the deletion adds the deleted word "bar" at position 8 (i.e. it will act as if
-    //        the word "quuux" was still present).
-    //
-    //            "foo  baz" -> "foo  bazbar"
-    //                     | -> deletion of "bar" is reverted by reinserting "bar" at position 8
-    //
-    // While the intended result would be "foo bar baz", what we get is:
-    //
-    //      "foo  bazbar" (note "bar" readded at position 8)
-    //
-    // The issue happens because of step 1. To revert the insertion of "quux", 4 characters are deleted
-    // from position 4. This includes the position where the deletion exists; when that position is
-    // cleared, the RangesTracker considers that the deletion is gone and stops tracking/updating it.
-    // As we still hold a reference to it, the code tries to revert it by readding the deleted text, but
-    // does so at the outdated position (position 8, which was valid when "quux" was present).
-    //
-    // To avoid this kind of problem, we need to make sure that reverting operations doesn't affect
-    // subsequent operations that come after. Reverse sorting the operations based on position will
-    // achieve it; in the case above, it makes sure that the the deletion is reverted first:
-    //
-    //     1) Rejecting the deletion adds the deleted word "bar" at position 8
-    //
-    //            "foo quux baz" -> "foo quuxbar baz"
-    //                                       | -> deletion of "bar" is reverted by
-    //                                            reinserting "bar" at position 8
-    //
-    //     2) Rejecting the insertion deletes the added word "quux", i.e., it removes 4 chars
-    //        starting from position 4 and achieves the expected result:
-    //
-    //           "foo quuxbar baz" -> "foo bar baz"
-    //                |--| -> 4 characters to be removed
-
-    changes.sort((a, b) => b.op.p - a.op.p)
-
-    const changesToDispatch = changes.map(change => {
-      const { op } = change
-
-      if (isInsertOperation(op)) {
-        const from = op.p
-        const content = op.i
-        const to = from + content.length
-
-        const text = view.state.doc.sliceString(from, to)
-
-        if (text !== content) {
-          throw new Error(`Op to be removed does not match editor text`)
-        }
-
-        return { from, to, insert: '' }
-      } else if (isDeleteOperation(op)) {
-        return {
-          from: op.p,
-          to: op.p,
-          insert: op.d,
-        }
-      } else {
-        throw new Error(`unknown change type: ${JSON.stringify(change)}`)
-      }
-    })
-
-    return {
-      changes: changesToDispatch,
-      annotations: [trackChangesAnnotation.of('reject')],
-    }
-  }
-
-  /**
-   * If the current selection is empty, select the whole line.
-   *
-   * Used when adding a comment with no selected range, e.g. with a keyboard shortcut.
-   */
-  const selectCurrentLine = () => {
-    if (view.state.selection.main.empty) {
-      const line = view.state.doc.lineAt(view.state.selection.main.from)
-
-      view.dispatch({
-        selection: {
-          anchor: line.from,
-          head: line.to === view.state.doc.length ? line.to : line.to + 1,
-        },
-      })
-    }
-  }
-
-  /**
-   * Collapse the current selection to a single point (after inserting a comment)
-   */
-  const collapseSelection = () => {
-    view.dispatch({
-      selection: EditorSelection.cursor(view.state.selection.main.head),
-    })
-  }
-
-  /**
-   * Listen for events dispatched from the (Angular) review panel.
-   *
-   * These are combined into a single listener, avoiding the need to add and remove event listeners individually.
-   */
-  const reviewPanelEventListener = (event: Event) => {
-    const { type, payload } = (
-      event as CustomEvent<{ type: string; payload: any }>
-    ).detail
-
-    switch (type) {
-      // receive review panel scroll events
-      case 'scroll': {
-        view.scrollDOM.scrollBy(0, payload)
-        break
-      }
-
-      case 'overview-closed': {
-        window.setTimeout(() => {
-          dispatchScrollEvent()
-        }, 0)
-        break
-      }
-
-      case 'recalculate-screen-positions': {
-        const { visibilityChanged, updateType } =
-          recalculateScreenPositions(payload)
-        if (visibilityChanged) {
-          dispatchEditorEvent('track-changes:visibility_changed')
-        }
-        // Ensure the layout is updated once the review panel entries have
-        // updated in the React review panel. The use of a timeout is bad but
-        // the timings are a bit of a mess and will be improved when the review
-        // panel state is migrated away from Angular. Entries are not animated
-        // into position when scrolling, or when the editor geometry changes
-        // (e.g. because the window has been resized), or when the top padding
-        // is adjusted
-        const nonAnimatingUpdateTypes: UpdateType[] = [
-          'viewportChange',
-          'geometryChange',
-          'topPaddingChange',
-        ]
-        const animate = !nonAnimatingUpdateTypes.includes(updateType)
-        dispatchReviewPanelLayout({
-          async: true,
-          animate,
-          force: false, // updateType === 'geometryChange',
-        })
-        break
-      }
-
-      case 'changes:accept': {
-        acceptChanges(payload)
-        view.dispatch(buildChangeMarkers())
-        broadcastChange()
-        // Dispatch a focus:changed event to force the Angular controller to
-        // reassemble the list of entries without bulk actions
-        scheduleDispatchFocusChanged(view.state, 'acceptChanges')
-        break
-      }
-
-      case 'changes:reject': {
-        view.dispatch(rejectChanges(payload))
-        broadcastChange()
-        // Dispatch a focus:changed event to force the Angular controller to
-        // reassemble the list of entries without bulk actions
-        setTimeout(() => {
-          // Delay the execution to make sure it runs after `broadcastChange`
-          scheduleDispatchFocusChanged(view.state, 'rejectChanges')
-        }, 30)
-        break
-      }
-
-      case 'comment:select_line': {
-        selectCurrentLine()
-        broadcastChange()
-        break
-      }
-
-      case 'comment:add': {
-        addComment(payload.offset, payload.length, payload.threadId)
-        collapseSelection()
-        broadcastChange()
-        break
-      }
-
-      case 'comment:remove': {
-        removeComment(payload)
-        view.dispatch(buildChangeMarkers())
-        broadcastChange()
-        break
-      }
-
-      case 'comment:resolve_threads':
-      case 'comment:unresolve_thread': {
-        view.dispatch(buildChangeMarkers())
-        broadcastChange()
-        break
-      }
-
-      case 'loaded_threads': {
-        view.dispatch(buildChangeMarkers())
-        broadcastChange()
-        break
-      }
-
-      case 'sizes': {
-        const editorFullContentHeight = view.contentDOM.clientHeight
-        // the content height and top overflow of the review panel
-        const { height, overflowTop } = payload
-        // the difference between the review panel height and the editor content height
-        const heightDiff = height + overflowTop - editorFullContentHeight
-        // the height of the block added at the top of the editor to match the review panel
-        const topPadding = editorVerticalTopPadding(view)
-        const bottomPadding = view.documentPadding.bottom
-        const contentHeight =
-          editorFullContentHeight - (topPadding + bottomPadding)
-        const newBottomPadding = height - contentHeight
-
-        if (overflowTop !== topPadding || heightDiff !== 0) {
-          view.dispatch(
-            setVerticalOverflow({
-              top: overflowTop,
-              bottom: newBottomPadding,
-            })
-          )
-        }
-        break
-      }
-    }
-  }
-
-  const broadcastChange = debounce(() => {
-    dispatchEditorEvent('track-changes:changed')
-  }, 50)
-
-  /**
-   * When the editor content, focus, size, viewport or selection changes,
-   * tell the review panel to update.
-   *
-   * @param state object
-   * @param updateType UpdateType
-   */
-  const dispatchFocusChangedImmediately = (
-    state: EditorState,
-    updateType: UpdateType
-  ) => {
-    // TODO: multiple selections?
-    const { from, to, empty } = state.selection.main
-
-    dispatchEditorEvent('focus:changed', {
-      from,
-      to,
-      empty,
-      updateType,
-    })
-  }
-
-  const scheduleDispatchFocusChanged = debounce(
-    dispatchFocusChangedImmediately,
-    50
-  )
-
-  /**
-   * When the editor is scrolled, tell the review panel so it can scroll in sync.
-   */
-  const dispatchScrollEvent = () => {
-    window.dispatchEvent(
-      new CustomEvent('editor:scroll', {
-        detail: {
-          height: view.scrollDOM.scrollHeight,
-          scrollTop: view.scrollDOM.scrollTop,
-          paddingTop: editorVerticalTopPadding(view),
-        },
-      })
-    )
-  }
-
-  /**
-   * Add event listeners to the ShareJS doc so that change markers are rebuilt when the tracked changes are updated.
-   *
-   * Also add event listeners to the editor scroll DOM and window.
-   */
-  const addListeners = () => {
-    // NOTE: the namespace "cm6" is needed so the listeners can be removed individually
-    currentDoc.on('ranges:dirty.cm6', () => {
-      // TODO: use currentDoc.ranges.getDirtyState and only update those which have changed?
-      window.setTimeout(() => {
-        view.dispatch(buildChangeMarkers())
-        broadcastChange()
-      }, 0)
-    })
-
-    // called on joinDoc
-    currentDoc.on('ranges:clear.cm6', () => {
-      window.setTimeout(() => {
-        view.dispatch(clearChangeMarkers())
-        broadcastChange()
-      }, 0)
-    })
-
-    // called on joinDoc
-    currentDoc.on('ranges:redraw.cm6', () => {
-      window.setTimeout(() => {
-        view.dispatch(buildChangeMarkers())
-        broadcastChange()
-      }, 0)
-    })
-
-    // sync review panel scroll with editor scroll
-    view.scrollDOM.addEventListener('scroll', dispatchScrollEvent)
-
-    // listen for events from the review panel controller
-    window.addEventListener('review-panel:event', reviewPanelEventListener)
-  }
-
-  /**
-   * Remove event listeners
-   */
-  const removeListeners = () => {
-    currentDoc.off('ranges:clear.cm6')
-    currentDoc.off('ranges:dirty.cm6')
-    currentDoc.off('ranges:redraw.cm6')
-
-    view.scrollDOM.removeEventListener('scroll', dispatchScrollEvent)
-
-    window.removeEventListener('review-panel:event', reviewPanelEventListener)
-  }
-
-  let ignoreGeometryChangesUntil = 0
-
-  return {
-    initialize() {
-      addListeners()
-      broadcastChange()
-    },
-    handleUpdate(update: ViewUpdate) {
-      const changesTopPadding = updateChangesTopPadding(update)
-      const {
-        geometryChanged,
-        viewportChanged,
-        docChanged,
-        focusChanged,
-        selectionSet,
-      } = update
-      const setsVerticalOverflow = updateSetsVerticalOverflow(update)
-      const ignoringGeometryChanges = Date.now() < ignoreGeometryChangesUntil
-
-      if (geometryChanged && !docChanged && !ignoringGeometryChanges) {
-        broadcastChange()
-      }
-
-      if (
-        !setsVerticalOverflow &&
-        (geometryChanged || viewportChanged) &&
-        ignoringGeometryChanges
-      ) {
-        // Ignore a change to the editor geometry or viewport that occurs immediately after
-        // an update to the vertical padding because otherwise it triggers
-        // another update to the padding and so on ad infinitum. This is not an
-        // ideal way to handle this but I couldn't see another way.
-        return
-      }
-
-      if (changesTopPadding) {
-        scheduleDispatchFocusChanged(update.state, 'topPaddingChange')
-      } else if (docChanged) {
-        scheduleDispatchFocusChanged(update.state, 'edit')
-      } else if (focusChanged || selectionSet) {
-        scheduleDispatchFocusChanged(update.state, 'selectionChange')
-      } else if (viewportChanged && !geometryChanged) {
-        // It's better to respond immediately to a viewport change, which
-        // happens when scrolling, and have previously unpositioned entries
-        // appear immediately rather than risk a delay due to debouncing
-        dispatchFocusChangedImmediately(update.state, 'viewportChange')
-      } else if (geometryChanged) {
-        scheduleDispatchFocusChanged(update.state, 'geometryChange')
-      }
-
-      // Wait until after updating the review panel layout before starting the
-      // interval during which to ignore geometry update
-      if (setsVerticalOverflow) {
-        ignoreGeometryChangesUntil = Date.now() + 50
-      }
-    },
-    destroy() {
-      removeListeners()
-    },
-  }
-}
-
-const reviewPanelToggledEffect = StateEffect.define()
-
-export const updateHasReviewPanelToggledEffect = updateHasEffect(
-  reviewPanelToggledEffect
-)
-
-export const reviewPanelToggled = (): TransactionSpec => ({
-  effects: reviewPanelToggledEffect.of(null),
-})

+ 1 - 3
services/web/frontend/js/features/source-editor/extensions/draw-selection.ts

@@ -3,7 +3,6 @@ import { EditorView, layer } from '@codemirror/view'
 import { rectangleMarkerForRange } from '../utils/layer'
 import { rectangleMarkerForRange } from '../utils/layer'
 import { updateHasMouseDownEffect } from './visual/selection'
 import { updateHasMouseDownEffect } from './visual/selection'
 import browser from './browser'
 import browser from './browser'
-import { updateHasReviewPanelToggledEffect } from './changes/change-manager'
 
 
 /**
 /**
  * The built-in extension which draws the cursor and selection(s) in layers,
  * The built-in extension which draws the cursor and selection(s) in layers,
@@ -96,8 +95,7 @@ const selectionLayer = layer({
       update.docChanged ||
       update.docChanged ||
       update.selectionSet ||
       update.selectionSet ||
       update.viewportChanged ||
       update.viewportChanged ||
-      updateHasMouseDownEffect(update) ||
-      updateHasReviewPanelToggledEffect(update)
+      updateHasMouseDownEffect(update)
     )
     )
   },
   },
   class: 'cm-selectionLayer',
   class: 'cm-selectionLayer',

+ 1 - 5
services/web/frontend/js/features/source-editor/extensions/index.ts

@@ -23,7 +23,6 @@ import { autoPair } from './auto-pair'
 import { phrases } from './phrases'
 import { phrases } from './phrases'
 import { spelling } from './spelling'
 import { spelling } from './spelling'
 import { symbolPalette } from './symbol-palette'
 import { symbolPalette } from './symbol-palette'
-import { trackChanges } from './track-changes'
 import { search } from './search'
 import { search } from './search'
 import { filterCharacters } from './filter-characters'
 import { filterCharacters } from './filter-characters'
 import { keybindings } from './keybindings'
 import { keybindings } from './keybindings'
@@ -50,7 +49,6 @@ import { geometryChangeEvent } from './geometry-change-event'
 import { docName } from './doc-name'
 import { docName } from './doc-name'
 import { fileTreeItemDrop } from './file-tree-item-drop'
 import { fileTreeItemDrop } from './file-tree-item-drop'
 import { mathPreview } from './math-preview'
 import { mathPreview } from './math-preview'
-import { isSplitTestEnabled } from '@/utils/splitTestUtils'
 import { ranges } from './ranges'
 import { ranges } from './ranges'
 import { trackDetachedComments } from './track-detached-comments'
 import { trackDetachedComments } from './track-detached-comments'
 import { reviewTooltip } from './review-tooltip'
 import { reviewTooltip } from './review-tooltip'
@@ -144,9 +142,7 @@ export const createExtensions = (options: Record<string, any>): Extension[] => [
   // NOTE: `emptyLineFiller` needs to be before `trackChanges`,
   // NOTE: `emptyLineFiller` needs to be before `trackChanges`,
   // so the decorations are added in the correct order.
   // so the decorations are added in the correct order.
   emptyLineFiller(),
   emptyLineFiller(),
-  isSplitTestEnabled('review-panel-redesign')
-    ? ranges()
-    : trackChanges(options.currentDoc, options.changeManager),
+  ranges(),
   trackDetachedComments(options.currentDoc),
   trackDetachedComments(options.currentDoc),
   visual(options.visual),
   visual(options.visual),
   mathPreview(options.settings.mathPreview),
   mathPreview(options.settings.mathPreview),

+ 0 - 5
services/web/frontend/js/features/source-editor/extensions/review-tooltip.ts

@@ -15,7 +15,6 @@ import {
   EditorState,
   EditorState,
   Transaction,
   Transaction,
 } from '@codemirror/state'
 } from '@codemirror/state'
-import { isSplitTestEnabled } from '@/utils/splitTestUtils'
 import { v4 as uuid } from 'uuid'
 import { v4 as uuid } from 'uuid'
 
 
 export const addNewCommentRangeEffect = StateEffect.define<Range<Decoration>>()
 export const addNewCommentRangeEffect = StateEffect.define<Range<Decoration>>()
@@ -57,10 +56,6 @@ export const buildAddNewCommentRangeEffect = (range: SelectionRange) => {
 }
 }
 
 
 export const reviewTooltip = (): Extension => {
 export const reviewTooltip = (): Extension => {
-  if (!isSplitTestEnabled('review-panel-redesign')) {
-    return []
-  }
-
   let mouseUpListener: null | (() => void) = null
   let mouseUpListener: null | (() => void) = null
   const disableMouseUpListener = () => {
   const disableMouseUpListener = () => {
     if (mouseUpListener) {
     if (mouseUpListener) {

+ 5 - 20
services/web/frontend/js/features/source-editor/extensions/shortcuts.ts

@@ -1,4 +1,4 @@
-import { EditorView, keymap } from '@codemirror/view'
+import { keymap } from '@codemirror/view'
 import { Prec } from '@codemirror/state'
 import { Prec } from '@codemirror/state'
 import { indentMore } from '../commands/indent'
 import { indentMore } from '../commands/indent'
 import {
 import {
@@ -18,39 +18,24 @@ import {
 import { changeCase, duplicateSelection } from '../commands/ranges'
 import { changeCase, duplicateSelection } from '../commands/ranges'
 import { selectNextOccurrence, selectPrevOccurrence } from '../commands/select'
 import { selectNextOccurrence, selectPrevOccurrence } from '../commands/select'
 import { cloneSelectionVertically } from '../commands/cursor'
 import { cloneSelectionVertically } from '../commands/cursor'
-import { dispatchEditorEvent } from './changes/change-manager'
 import {
 import {
   deleteToVisualLineEnd,
   deleteToVisualLineEnd,
   deleteToVisualLineStart,
   deleteToVisualLineStart,
 } from './visual-line-selection'
 } from './visual-line-selection'
-import { isSplitTestEnabled } from '@/utils/splitTestUtils'
 import { emitShortcutEvent } from '@/features/source-editor/extensions/toolbar/utils/analytics'
 import { emitShortcutEvent } from '@/features/source-editor/extensions/toolbar/utils/analytics'
 
 
 const toggleReviewPanel = () => {
 const toggleReviewPanel = () => {
-  if (isSplitTestEnabled('review-panel-redesign')) {
-    window.dispatchEvent(new Event('ui.toggle-review-panel'))
-  } else {
-    dispatchEditorEvent('toggle-review-panel')
-  }
-
+  window.dispatchEvent(new Event('ui.toggle-review-panel'))
   return true
   return true
 }
 }
 
 
-const addNewCommentFromKbdShortcut = (view: EditorView) => {
-  if (isSplitTestEnabled('review-panel-redesign')) {
-    window.dispatchEvent(new Event('add-new-review-comment'))
-  } else {
-    dispatchEditorEvent('add-new-comment')
-  }
+const addNewCommentFromKbdShortcut = () => {
+  window.dispatchEvent(new Event('add-new-review-comment'))
   return true
   return true
 }
 }
 
 
 const toggleTrackChangesFromKbdShortcut = () => {
 const toggleTrackChangesFromKbdShortcut = () => {
-  if (isSplitTestEnabled('review-panel-redesign')) {
-    window.dispatchEvent(new Event('toggle-track-changes'))
-  } else {
-    dispatchEditorEvent('toggle-track-changes')
-  }
+  window.dispatchEvent(new Event('toggle-track-changes'))
   return true
   return true
 }
 }
 
 

+ 0 - 284
services/web/frontend/js/features/source-editor/extensions/track-changes.ts

@@ -1,284 +0,0 @@
-import { StateEffect } from '@codemirror/state'
-import {
-  Decoration,
-  type DecorationSet,
-  EditorView,
-  type PluginValue,
-  ViewPlugin,
-  WidgetType,
-} from '@codemirror/view'
-import { Change, DeleteOperation } from '../../../../../types/change'
-import { ChangeManager } from './changes/change-manager'
-import { debugConsole } from '@/utils/debugging'
-import { isCommentOperation, isDeleteOperation } from '@/utils/operations'
-import {
-  DocumentContainer,
-  RangesTrackerWithResolvedThreadIds,
-} from '@/features/ide-react/editor/document-container'
-
-const clearChangesEffect = StateEffect.define()
-const buildChangesEffect = StateEffect.define()
-
-type Options = {
-  currentDoc: DocumentContainer
-  loadingThreads: boolean
-}
-
-/**
- * A custom extension that initialises the change manager, passes any updates to it,
- * and produces decorations for tracked changes and comments.
- */
-export const trackChanges = (
-  { currentDoc, loadingThreads }: Options,
-  changeManager: ChangeManager
-) => {
-  return [
-    // initialize/destroy the change manager, and handle any updates
-    ViewPlugin.define(() => {
-      changeManager.initialize()
-
-      return {
-        update: update => {
-          changeManager.handleUpdate(update)
-        },
-        destroy: () => {
-          changeManager.destroy()
-        },
-      }
-    }),
-
-    // draw change decorations
-    ViewPlugin.define<
-      PluginValue & {
-        decorations: DecorationSet
-      }
-    >(
-      () => {
-        return {
-          decorations: loadingThreads
-            ? Decoration.none
-            : buildChangeDecorations(currentDoc),
-          update(update) {
-            for (const transaction of update.transactions) {
-              this.decorations = this.decorations.map(transaction.changes)
-
-              for (const effect of transaction.effects) {
-                if (effect.is(clearChangesEffect)) {
-                  this.decorations = Decoration.none
-                } else if (effect.is(buildChangesEffect)) {
-                  this.decorations = buildChangeDecorations(currentDoc)
-                }
-              }
-            }
-          },
-        }
-      },
-      {
-        decorations: value => value.decorations,
-      }
-    ),
-
-    // styles for change decorations
-    trackChangesTheme,
-  ]
-}
-
-export const clearChangeMarkers = () => {
-  return {
-    effects: clearChangesEffect.of(null),
-  }
-}
-
-export const buildChangeMarkers = () => {
-  return {
-    effects: buildChangesEffect.of(null),
-  }
-}
-
-const buildChangeDecorations = (currentDoc: DocumentContainer) => {
-  if (!currentDoc.ranges) {
-    return Decoration.none
-  }
-
-  const changes = [...currentDoc.ranges.changes, ...currentDoc.ranges.comments]
-
-  const decorations = []
-
-  for (const change of changes) {
-    try {
-      decorations.push(...createChangeRange(change, currentDoc))
-    } catch (error) {
-      // ignore invalid changes
-      debugConsole.debug('invalid change position', error)
-    }
-  }
-
-  return Decoration.set(decorations, true)
-}
-
-class ChangeDeletedWidget extends WidgetType {
-  constructor(public change: Change<DeleteOperation>) {
-    super()
-  }
-
-  toDOM() {
-    const widget = document.createElement('span')
-    widget.classList.add('ol-cm-change')
-    widget.classList.add('ol-cm-change-d')
-
-    return widget
-  }
-
-  eq() {
-    return true
-  }
-}
-
-class ChangeCalloutWidget extends WidgetType {
-  constructor(
-    public change: Change,
-    public opType: string
-  ) {
-    super()
-  }
-
-  toDOM() {
-    const widget = document.createElement('span')
-    widget.className = 'ol-cm-change-callout'
-    widget.classList.add(`ol-cm-change-callout-${this.opType}`)
-
-    const inner = document.createElement('span')
-    inner.classList.add('ol-cm-change-callout-inner')
-    widget.appendChild(inner)
-
-    return widget
-  }
-
-  eq(widget: ChangeCalloutWidget) {
-    return widget.opType === this.opType
-  }
-
-  updateDOM(element: HTMLElement) {
-    element.className = 'ol-cm-change-callout'
-    element.classList.add(`ol-cm-change-callout-${this.opType}`)
-    return true
-  }
-}
-
-const createChangeRange = (change: Change, currentDoc: DocumentContainer) => {
-  const { id, metadata, op } = change
-
-  const from = op.p
-  // TODO: find valid positions?
-
-  if (isDeleteOperation(op)) {
-    const opType = 'd'
-
-    const changeWidget = Decoration.widget({
-      widget: new ChangeDeletedWidget(change as Change<DeleteOperation>),
-      side: 1,
-      opType,
-      id,
-      metadata,
-    })
-
-    const calloutWidget = Decoration.widget({
-      widget: new ChangeCalloutWidget(change, opType),
-      side: 1,
-      opType,
-      id,
-      metadata,
-    })
-
-    return [calloutWidget.range(from, from), changeWidget.range(from, from)]
-  }
-
-  const _isCommentOperation = isCommentOperation(op)
-
-  if (
-    _isCommentOperation &&
-    (currentDoc.ranges as RangesTrackerWithResolvedThreadIds)
-      .resolvedThreadIds![op.t]
-  ) {
-    return []
-  }
-
-  const opType = _isCommentOperation ? 'c' : 'i'
-  const changedText = _isCommentOperation ? op.c : op.i
-  const to = from + changedText.length
-
-  // Mark decorations must not be empty
-  if (from === to) {
-    return []
-  }
-
-  const changeMark = Decoration.mark({
-    tagName: 'span',
-    class: `ol-cm-change ol-cm-change-${opType}`,
-    opType,
-    id,
-    metadata,
-  })
-
-  const calloutWidget = Decoration.widget({
-    widget: new ChangeCalloutWidget(change, opType),
-    opType,
-    id,
-    metadata,
-  })
-
-  return [calloutWidget.range(from, from), changeMark.range(from, to)]
-}
-
-const trackChangesTheme = EditorView.baseTheme({
-  '.cm-line': {
-    overflowX: 'hidden', // needed so the callout elements don't overflow (requires line wrapping to be on)
-  },
-  '&light .ol-cm-change-i': {
-    backgroundColor: '#2c8e304d',
-  },
-  '&dark .ol-cm-change-i': {
-    backgroundColor: 'rgba(37, 107, 41, 0.15)',
-  },
-  '&light .ol-cm-change-c': {
-    backgroundColor: '#f3b1114d',
-  },
-  '&dark .ol-cm-change-c': {
-    backgroundColor: 'rgba(194, 93, 11, 0.15)',
-  },
-  '.ol-cm-change': {
-    padding: 'var(--half-leading, 0) 0',
-  },
-  '.ol-cm-change-d': {
-    borderLeft: '2px dotted #c5060b',
-    marginLeft: '-1px',
-  },
-  '.ol-cm-change-callout': {
-    position: 'relative',
-    pointerEvents: 'none',
-    padding: 'var(--half-leading, 0) 0',
-  },
-  '.ol-cm-change-callout-inner': {
-    display: 'inline-block',
-    position: 'absolute',
-    left: 0,
-    bottom: 0,
-    width: '100vw',
-    borderBottom: '1px dashed black',
-  },
-  // disable callout line in Firefox
-  '@supports (-moz-appearance:none)': {
-    '.ol-cm-change-callout-inner': {
-      display: 'none',
-    },
-  },
-  '.ol-cm-change-callout-i .ol-cm-change-callout-inner': {
-    borderColor: '#2c8e30',
-  },
-  '.ol-cm-change-callout-c .ol-cm-change-callout-inner': {
-    borderColor: '#f3b111',
-  },
-  '.ol-cm-change-callout-d .ol-cm-change-callout-inner': {
-    borderColor: '#c5060b',
-  },
-})

+ 0 - 7
services/web/frontend/js/features/source-editor/extensions/track-detached-comments.ts

@@ -14,8 +14,6 @@ import {
 } from './changes/comments'
 } from './changes/comments'
 import { invertedEffects } from '@codemirror/commands'
 import { invertedEffects } from '@codemirror/commands'
 import { DocumentContainer } from '@/features/ide-react/editor/document-container'
 import { DocumentContainer } from '@/features/ide-react/editor/document-container'
-import { isSplitTestEnabled } from '@/utils/splitTestUtils'
-import { buildChangeMarkers } from './track-changes'
 
 
 const restoreDetachedCommentsEffect = StateEffect.define<RangeSet<any>>({
 const restoreDetachedCommentsEffect = StateEffect.define<RangeSet<any>>({
   map: (value, mapping) => {
   map: (value, mapping) => {
@@ -90,11 +88,6 @@ export const trackDetachedComments = ({
         if (effect.is(restoreDetachedCommentsEffect)) {
         if (effect.is(restoreDetachedCommentsEffect)) {
           // send the comments to the ShareJS doc
           // send the comments to the ShareJS doc
           restoreDetachedComments(currentDoc, transaction, effect.value)
           restoreDetachedComments(currentDoc, transaction, effect.value)
-
-          if (isSplitTestEnabled('review-panel-redesign')) {
-            // return a transaction spec to rebuild the change markers
-            return buildChangeMarkers()
-          }
         }
         }
       }
       }
       return null
       return null

+ 0 - 28
services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts

@@ -30,11 +30,6 @@ import { setAutoComplete } from '../extensions/auto-complete'
 import { usePhrases } from './use-phrases'
 import { usePhrases } from './use-phrases'
 import { setPhrases } from '../extensions/phrases'
 import { setPhrases } from '../extensions/phrases'
 import { setSpellCheckLanguage } from '../extensions/spelling'
 import { setSpellCheckLanguage } from '../extensions/spelling'
-import {
-  createChangeManager,
-  dispatchEditorEvent,
-  reviewPanelToggled,
-} from '../extensions/changes/change-manager'
 import { setKeybindings } from '../extensions/keybindings'
 import { setKeybindings } from '../extensions/keybindings'
 import { Highlight } from '../../../../../types/highlight'
 import { Highlight } from '../../../../../types/highlight'
 import { EditorView } from '@codemirror/view'
 import { EditorView } from '@codemirror/view'
@@ -46,7 +41,6 @@ import { setDocName } from '@/features/source-editor/extensions/doc-name'
 import { isValidTeXFile } from '@/main/is-valid-tex-file'
 import { isValidTeXFile } from '@/main/is-valid-tex-file'
 import { captureException } from '@/infrastructure/error-reporter'
 import { captureException } from '@/infrastructure/error-reporter'
 import grammarlyExtensionPresent from '@/shared/utils/grammarly'
 import grammarlyExtensionPresent from '@/shared/utils/grammarly'
-import { useLayoutContext } from '@/shared/context/layout-context'
 import { debugConsole } from '@/utils/debugging'
 import { debugConsole } from '@/utils/debugging'
 import { useMetadataContext } from '@/features/ide-react/context/metadata-context'
 import { useMetadataContext } from '@/features/ide-react/context/metadata-context'
 import { useUserContext } from '@/shared/context/user-context'
 import { useUserContext } from '@/shared/context/user-context'
@@ -57,7 +51,6 @@ import { updateRanges } from '@/features/source-editor/extensions/ranges'
 import { useThreadsContext } from '@/features/review-panel-new/context/threads-context'
 import { useThreadsContext } from '@/features/review-panel-new/context/threads-context'
 import { useHunspell } from '@/features/source-editor/hooks/use-hunspell'
 import { useHunspell } from '@/features/source-editor/hooks/use-hunspell'
 import { Permissions } from '@/features/ide-react/types/permissions'
 import { Permissions } from '@/features/ide-react/types/permissions'
-import { lineHeights } from '@/shared/utils/styles'
 import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
 import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
 import { useOnlineUsersContext } from '@/features/ide-react/context/online-users-context'
 import { useOnlineUsersContext } from '@/features/ide-react/context/online-users-context'
 
 
@@ -71,13 +64,10 @@ function useCodeMirrorScope(view: EditorView) {
   const { logEntryAnnotations, editedSinceCompileStarted, compiling } =
   const { logEntryAnnotations, editedSinceCompileStarted, compiling } =
     useCompileContext()
     useCompileContext()
 
 
-  const { reviewPanelOpen, miniReviewPanelVisible } = useLayoutContext()
   const { currentDocument, openDocName, trackChanges } =
   const { currentDocument, openDocName, trackChanges } =
     useEditorManagerContext()
     useEditorManagerContext()
   const metadata = useMetadataContext()
   const metadata = useMetadataContext()
 
 
-  const [loadingThreads] = useScopeValue<boolean>('loadingThreads')
-
   const { id: userId } = useUserContext()
   const { id: userId } = useUserContext()
   const { userSettings } = useUserSettingsContext()
   const { userSettings } = useUserSettingsContext()
   const {
   const {
@@ -166,7 +156,6 @@ function useCodeMirrorScope(view: EditorView) {
   const currentDocRef = useRef({
   const currentDocRef = useRef({
     currentDocument,
     currentDocument,
     trackChanges,
     trackChanges,
-    loadingThreads,
   })
   })
 
 
   useEffect(() => {
   useEffect(() => {
@@ -185,10 +174,6 @@ function useCodeMirrorScope(view: EditorView) {
 
 
   const docNameRef = useRef(openDocName)
   const docNameRef = useRef(openDocName)
 
 
-  useEffect(() => {
-    currentDocRef.current.loadingThreads = loadingThreads
-  }, [view, loadingThreads])
-
   useEffect(() => {
   useEffect(() => {
     currentDocRef.current.trackChanges = trackChanges
     currentDocRef.current.trackChanges = trackChanges
 
 
@@ -201,12 +186,6 @@ function useCodeMirrorScope(view: EditorView) {
     }
     }
   }, [userId, currentDocument, trackChanges])
   }, [userId, currentDocument, trackChanges])
 
 
-  useEffect(() => {
-    if (lineHeight && fontSize) {
-      dispatchEditorEvent('line-height', lineHeights[lineHeight] * fontSize)
-    }
-  }, [lineHeight, fontSize])
-
   const spellingRef = useRef({
   const spellingRef = useRef({
     spellCheckLanguage,
     spellCheckLanguage,
     hunspellManager,
     hunspellManager,
@@ -323,7 +302,6 @@ function useCodeMirrorScope(view: EditorView) {
           spelling: spellingRef.current,
           spelling: spellingRef.current,
           visual: visualRef.current,
           visual: visualRef.current,
           projectFeatures: projectFeaturesRef.current,
           projectFeatures: projectFeaturesRef.current,
-          changeManager: createChangeManager(view, currentDocument),
           handleError,
           handleError,
           handleException,
           handleException,
         }),
         }),
@@ -568,12 +546,6 @@ function useCodeMirrorScope(view: EditorView) {
       view.focus()
       view.focus()
     }, [view])
     }, [view])
   )
   )
-
-  useEffect(() => {
-    window.setTimeout(() => {
-      view.dispatch(reviewPanelToggled())
-    })
-  }, [reviewPanelOpen, miniReviewPanelVisible, view])
 }
 }
 
 
 export default useCodeMirrorScope
 export default useCodeMirrorScope

+ 0 - 11
services/web/frontend/stories/source-editor/source-editor.stories.tsx

@@ -55,13 +55,6 @@ const settings = {
   syntaxValidation: false,
   syntaxValidation: false,
 }
 }
 
 
-const reviewPanel = {
-  resolvedComments: {},
-  formattedProjectMembers: {},
-  overview: { docsCollapsedState: { 'story-doc': false } },
-  entries: {},
-}
-
 const permissions = {
 const permissions = {
   write: true,
   write: true,
 }
 }
@@ -101,7 +94,6 @@ export const Latex = (args: any, { globals: { theme } }: any) => {
       overallTheme: theme === 'default-' ? '' : theme,
       overallTheme: theme === 'default-' ? '' : theme,
     },
     },
     permissions,
     permissions,
-    reviewPanel,
   })
   })
 
 
   useMeta({
   useMeta({
@@ -122,7 +114,6 @@ export const Markdown = (args: any, { globals: { theme } }: any) => {
       overallTheme: theme === 'default-' ? '' : theme,
       overallTheme: theme === 'default-' ? '' : theme,
     },
     },
     permissions,
     permissions,
-    reviewPanel,
   })
   })
 
 
   return <SourceEditor />
   return <SourceEditor />
@@ -140,7 +131,6 @@ export const Visual = (args: any, { globals: { theme } }: any) => {
       overallTheme: theme === 'default-' ? '' : theme,
       overallTheme: theme === 'default-' ? '' : theme,
     },
     },
     permissions,
     permissions,
-    reviewPanel,
   })
   })
   useMeta({
   useMeta({
     'ol-showSymbolPalette': true,
     'ol-showSymbolPalette': true,
@@ -162,7 +152,6 @@ export const Bibtex = (args: any, { globals: { theme } }: any) => {
       overallTheme: theme === 'default-' ? '' : theme,
       overallTheme: theme === 'default-' ? '' : theme,
     },
     },
     permissions,
     permissions,
-    reviewPanel,
   })
   })
 
 
   return <SourceEditor />
   return <SourceEditor />

+ 0 - 1
services/web/frontend/stylesheets/bootstrap-5/pages/all.scss

@@ -21,7 +21,6 @@
 @import 'editor/file-tree';
 @import 'editor/file-tree';
 @import 'editor/file-view';
 @import 'editor/file-view';
 @import 'editor/figure-modal';
 @import 'editor/figure-modal';
-@import 'editor/review-panel';
 @import 'editor/chat';
 @import 'editor/chat';
 @import 'editor/toast';
 @import 'editor/toast';
 @import 'editor/history';
 @import 'editor/history';

+ 42 - 3
services/web/frontend/stylesheets/bootstrap-5/pages/editor/review-panel-new.scss

@@ -1,5 +1,16 @@
 @use 'sass:color';
 @use 'sass:color';
 
 
+$rp-border-grey: #d9d9d9;
+$rp-type-blue: #6b7797;
+$rp-type-darkgrey: #3f3f3f;
+
+:root {
+  --rp-base-font-size: var(--font-size-01);
+  --rp-border-grey: #{$rp-border-grey};
+  --rp-type-blue: #{$rp-type-blue};
+  --rp-type-darkgrey: #{$rp-type-darkgrey};
+}
+
 .review-panel-new {
 .review-panel-new {
   &.review-panel-container {
   &.review-panel-container {
     height: 100%;
     height: 100%;
@@ -182,11 +193,39 @@
 
 
   // TODO: Update this when we move the track changes menu to the new design
   // TODO: Update this when we move the track changes menu to the new design
   .rp-tc-state {
   .rp-tc-state {
+    position: absolute;
+    top: 100%;
+    left: 0;
+    right: 0;
+    overflow: hidden;
+    list-style: none;
+    padding: 0 var(--spacing-03);
+    margin: 0;
+    border-bottom: 1px solid var(--rp-border-grey);
+    text-align: left;
     background-color: var(--white);
     background-color: var(--white);
     max-height: calc(
     max-height: calc(
       100vh - var(--review-panel-top) - var(--review-panel-header-height)
       100vh - var(--review-panel-top) - var(--review-panel-header-height)
     );
     );
     overflow-y: auto;
     overflow-y: auto;
+
+    .rp-tc-state-item {
+      display: flex;
+      align-items: center;
+      padding: var(--spacing-02) 0;
+
+      &:last-of-type {
+        padding-bottom: var(--spacing-03);
+      }
+    }
+
+    .rp-tc-state-item-name {
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      flex-grow: 1;
+      font-weight: 600;
+    }
   }
   }
 
 
   .review-panel-tools {
   .review-panel-tools {
@@ -216,7 +255,7 @@
     &:hover,
     &:hover,
     &:focus {
     &:focus {
       text-decoration: none;
       text-decoration: none;
-      color: $rp-type-blue;
+      color: var(--rp-type-blue);
     }
     }
   }
   }
 
 
@@ -703,7 +742,7 @@
   display: block;
   display: block;
   padding: 5px 10px;
   padding: 5px 10px;
   background-color: rgb(240 240 240 / 90%);
   background-color: rgb(240 240 240 / 90%);
-  color: $rp-type-blue;
+  color: var(--rp-type-blue);
   text-align: center;
   text-align: center;
   border-bottom-left-radius: 3px;
   border-bottom-left-radius: 3px;
   white-space: nowrap;
   white-space: nowrap;
@@ -724,7 +763,7 @@
     outline: 0;
     outline: 0;
     text-decoration: none;
     text-decoration: none;
     background-color: rgb(240 240 240 / 100%);
     background-color: rgb(240 240 240 / 100%);
-    color: $rp-type-blue;
+    color: var(--rp-type-blue);
   }
   }
 }
 }
 
 

+ 0 - 1109
services/web/frontend/stylesheets/bootstrap-5/pages/editor/review-panel.scss

@@ -1,1109 +0,0 @@
-@use 'sass:color';
-
-$rp-type-blue: #6b7797;
-$rp-highlight-blue: #8a96b5;
-$rp-bg-blue: #dadfed;
-$rp-yellow: #f3b111;
-$rp-entry-arrow-width: 6px;
-
-:root {
-  --rp-base-font-size: var(--font-size-01);
-  --rp-small-font-size: 0.625rem; // 10px
-  --rp-icon-large-size: var(--font-size-04);
-  --rp-bg-dim-blue: #fafafa;
-  --rp-border-grey: #d9d9d9;
-  --rp-green: #2c8e30;
-  --rp-red: #c5060b;
-  --rp-type-darkgrey: #3f3f3f;
-  --rp-entry-ribbon-width: 4px;
-  --rp-entry-arrow-width: 6px;
-  --rp-semibold-weight: 600;
-  --review-panel-width: 230px;
-  --review-off-width: 22px;
-  --rp-entry-animation-speed: 0.3s;
-}
-
-%rp-button {
-  display: inline-flex;
-  align-items: center;
-  justify-content: center;
-  background-color: $rp-highlight-blue;
-  color: var(--white);
-  text-align: center;
-  line-height: 1.3;
-  user-select: none;
-  border: 0;
-
-  &:hover,
-  &:focus {
-    outline: 0;
-    background-color: color.adjust($rp-highlight-blue, $lightness: -5%);
-    text-decoration: none;
-    color: var(--white);
-  }
-
-  &[disabled] {
-    background-color: color.mix($rp-highlight-blue, $white, 50%);
-
-    &:hover,
-    &:focus {
-      background-color: color.mix($rp-highlight-blue, $white, 50%);
-    }
-  }
-}
-
-.review-panel-toolbar {
-  display: none;
-
-  .rp-size-expanded & {
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
-    padding: 0 var(--spacing-03);
-  }
-
-  position: relative;
-  border-bottom: 1px solid var(--rp-border-grey);
-  background-color: var(--rp-bg-dim-blue);
-  text-align: center;
-  z-index: 3;
-  flex-basis: 32px;
-  flex-shrink: 0;
-}
-
-.review-panel-toolbar-label {
-  cursor: pointer;
-  text-align: right;
-  flex-grow: 1;
-}
-
-.review-panel-toolbar-icon-on {
-  margin-right: var(--spacing-03);
-  color: var(--bg-accent-01);
-}
-
-.review-panel-toolbar-label-disabled {
-  margin-right: var(--spacing-03);
-  cursor: auto;
-}
-
-.rp-tc-state {
-  position: absolute;
-  top: 100%;
-  left: 0;
-  right: 0;
-  overflow: hidden;
-  list-style: none;
-  padding: 0 var(--spacing-03);
-  margin: 0;
-  border-bottom: 1px solid var(--rp-border-grey);
-  background-color: var(--rp-bg-dim-blue);
-  text-align: left;
-}
-
-.rp-tc-state-collapse,
-.rp-overview-file-header-collapse {
-  display: inline-flex;
-  transition: transform 0.15s ease;
-  transform: rotateZ(0deg);
-
-  &-on {
-    transform: rotateZ(-90deg);
-  }
-}
-
-.rp-tc-state-item {
-  display: flex;
-  align-items: center;
-  padding: var(--spacing-02) 0;
-
-  &:last-of-type {
-    padding-bottom: var(--spacing-03);
-  }
-}
-
-.rp-tc-state-separator {
-  border-bottom: 1px solid var(--rp-border-grey);
-}
-
-.rp-tc-state-item-everyone {
-  border-bottom: 1px solid var(--rp-border-grey);
-}
-
-.rp-tc-state-item-name {
-  @extend .text-truncate;
-
-  flex-grow: 1;
-  font-weight: 600;
-}
-
-.rp-tc-state-item-name-disabled {
-  opacity: 0.35;
-}
-
-.rp-entry-list {
-  display: none;
-  width: 100%;
-
-  .rp-size-expanded &,
-  .rp-size-mini & {
-    display: block;
-  }
-
-  .rp-state-overview & {
-    flex-grow: 2;
-    overflow-y: auto;
-  }
-}
-
-.rp-entry-indicator {
-  display: none;
-  z-index: 2; // above .review-panel-toggler
-
-  .material-symbols {
-    font-size: var(--font-size-02);
-    vertical-align: sub;
-  }
-
-  .rp-size-mini & {
-    display: block;
-  }
-
-  .rp-floating-entry & {
-    display: block;
-    position: static;
-    width: calc(var(--review-off-width) - 4px);
-  }
-
-  .rp-size-mini &-add-comment {
-    display: none;
-  }
-
-  position: absolute;
-  left: 2px;
-  right: 2px;
-  text-align: center;
-  background-color: $rp-highlight-blue;
-  border-radius: var(--border-radius-base);
-  color: var(--white);
-  cursor: pointer;
-  transition:
-    top var(--rp-entry-animation-speed),
-    left 0.1s,
-    right 0.1s;
-
-  .no-animate & {
-    transition:
-      left 0.1s,
-      right 0.1s;
-  }
-
-  &-focused {
-    left: 0;
-    right: 4px;
-    z-index: 1;
-  }
-}
-
-.rp-entry-wrapper {
-  &:hover .rp-entry-insert,
-  &:hover .rp-entry-delete,
-  &:hover .rp-entry-aggregate,
-  &:hover .rp-entry-comment {
-    display: block;
-  }
-
-  &.rp-entry-hidden {
-    display: none;
-  }
-}
-
-.rp-entry {
-  .rp-state-current-file & {
-    position: absolute;
-    width: var(--review-panel-width);
-  }
-
-  .rp-state-current-file-mini &,
-  .rp-floating-entry & {
-    left: calc(var(--review-off-width) + var(--rp-entry-arrow-width));
-    box-shadow: 0 0 10px 5px rgba(0 0 0 / 20%);
-    z-index: 1;
-
-    &::before {
-      content: '';
-      position: absolute;
-      inset: calc((var(--review-off-width) + var(--rp-entry-arrow-width)) * -1)
-        calc((var(--review-off-width) + var(--rp-entry-arrow-width)) * -1)
-        calc((var(--review-off-width) + var(--rp-entry-arrow-width)) * -1)
-        calc(-2 * var(--rp-entry-arrow-width) - 2px);
-      z-index: -1;
-    }
-
-    &::after {
-      @include triangle(
-        left,
-        $rp-entry-arrow-width,
-        $rp-entry-arrow-width * 1.5,
-        inherit
-      );
-
-      top: calc(var(--review-off-width) / 2 - var(--rp-entry-arrow-width));
-      left: calc(
-        (var(--rp-entry-ribbon-width) + var(--rp-entry-arrow-width)) * -1
-      );
-      content: '';
-    }
-  }
-
-  .rp-state-current-file-mini & {
-    display: none;
-  }
-
-  .rp-floating-entry & {
-    position: absolute;
-    width: var(--review-panel-width);
-    left: calc(var(--review-off-width) + var(--rp-entry-arrow-width));
-    top: 0;
-  }
-
-  .rp-state-current-file-mini.rp-layout-left &,
-  .rp-floating-entry-layout-left & {
-    border-left-width: 0;
-    border-right-width: var(--rp-entry-ribbon-width);
-    border-right-style: solid;
-
-    &::before {
-      left: calc((var(--review-off-width) + var(--rp-entry-arrow-width)) * -1);
-    }
-
-    &::after {
-      @include triangle(
-        right,
-        $rp-entry-arrow-width,
-        $rp-entry-arrow-width * 1.5,
-        inherit
-      );
-
-      right: calc(
-        (var(--rp-entry-ribbon-width) + var(--rp-entry-arrow-width)) * -1
-      );
-      left: auto;
-    }
-  }
-
-  .rp-state-current-file-mini.rp-layout-left & {
-    left: auto;
-    right: calc(var(--review-off-width) + var(--rp-entry-arrow-width));
-
-    &::before {
-      right: calc(-2 * var(--rp-entry-arrow-width) - 2px);
-    }
-  }
-
-  .rp-floating-entry-layout-left & {
-    left: calc(
-      (var(--review-panel-width) + var(--rp-entry-arrow-width) + 2px) * -1
-    );
-
-    &::before {
-      right: calc(-2 * var(--rp-entry-arrow-width));
-    }
-  }
-
-  .rp-state-current-file-expanded & {
-    visibility: hidden;
-    left: 5px;
-    right: 5px;
-    width: auto;
-
-    &-focused {
-      left: -2px;
-      right: 12px;
-      z-index: 1;
-    }
-
-    &-add-comment {
-      right: auto;
-
-      &.rp-entry-adding-comment {
-        right: 5px;
-      }
-    }
-
-    &-bulk-actions {
-      right: auto;
-    }
-  }
-
-  .rp-state-overview & {
-    border-radius: 0;
-    border-bottom: solid 1px var(--rp-border-grey);
-    cursor: pointer;
-  }
-
-  .resolved-comments-dropdown & {
-    position: static;
-    margin-bottom: var(--spacing-03);
-  }
-
-  border-left: solid var(--rp-entry-ribbon-width) transparent;
-  border-radius: var(--border-radius-base);
-  background-color: var(--white);
-  transition:
-    top var(--rp-entry-animation-speed),
-    left 0.1s,
-    right 0.1s;
-
-  .no-animate & {
-    transition:
-      left 0.1s,
-      right 0.1s;
-  }
-
-  &-insert,
-  &-aggregate {
-    border-color: var(--rp-green);
-  }
-
-  &-delete {
-    border-color: var(--rp-red);
-  }
-
-  &-comment {
-    border-color: $rp-yellow;
-  }
-
-  &-comment-resolving {
-    top: 4px;
-    left: 6px;
-    opacity: 0;
-    z-index: 3;
-    transform: scale(0.1);
-    transform-origin: 0 0;
-    transition:
-      top 0.35s ease-out,
-      left 0.35s ease-out,
-      transform 0.35s ease-out,
-      opacity 0.35s ease-out 0.2s;
-  }
-
-  &-comment-resolved {
-    border-color: #aaa;
-    background-color: #efefef;
-  }
-
-  &-add-comment {
-    background-color: transparent;
-    right: auto;
-    border-left-width: 0;
-
-    &.rp-entry-adding-comment {
-      background-color: var(--white);
-      right: 5px;
-      border-left-width: 3px;
-      border-left-color: $rp-yellow;
-    }
-  }
-
-  &-bulk-actions {
-    background-color: transparent;
-    right: auto;
-    border-left-width: 0;
-  }
-}
-
-.rp-entry-body {
-  display: flex;
-  align-items: center;
-  padding: var(--spacing-02) var(--spacing-03);
-}
-
-.rp-entry-action-icon {
-  font-size: var(--rp-icon-large-size);
-  padding: 0 var(--spacing-02);
-  line-height: 0;
-
-  .rp-state-overview & {
-    display: none;
-  }
-}
-
-.rp-entry-details {
-  line-height: 1.4;
-  margin-left: var(--spacing-03);
-
-  // We need to set any low-enough flex base size (0px), making it growable (1) and non-shrinkable (0).
-  // This is needed to ensure that IE makes the element fill the available space.
-  flex: 1 0 1px;
-  overflow-x: auto;
-
-  .rp-state-overview & {
-    margin-left: 0;
-  }
-}
-
-.rp-entry-metadata {
-  font-size: var(--rp-small-font-size);
-}
-
-.rp-entry-metadata-element {
-  display: inline-block;
-}
-
-.rp-entry-user {
-  font-weight: var(--rp-semibold-weight);
-  font-style: normal;
-}
-
-.rp-comment-actions {
-  a {
-    color: $rp-type-blue;
-  }
-}
-
-.rp-content-highlight {
-  color: var(--rp-type-darkgrey);
-  font-weight: var(--rp-semibold-weight);
-  text-decoration: none;
-}
-
-del.rp-content-highlight {
-  text-decoration: line-through;
-}
-
-.rp-entry-actions {
-  display: flex;
-
-  .rp-state-overview .rp-entry-list & {
-    display: none;
-  }
-}
-
-.rp-entry-button {
-  @extend %rp-button;
-
-  flex: 1 1 50%;
-  border-right: solid 1px var(--white);
-  padding: var(--spacing-01) 0;
-
-  &:last-child {
-    border-bottom-right-radius: var(--border-radius-base);
-    border-right-width: 0;
-  }
-
-  .rp-state-current-file-mini.rp-layout-left &,
-  .rp-floating-entry-layout-left & {
-    &:first-child {
-      border-bottom-left-radius: var(--border-radius-base);
-    }
-
-    &:last-child {
-      border-bottom-right-radius: 0;
-    }
-  }
-
-  .material-symbols {
-    font-size: inherit;
-    font-weight: bold;
-  }
-}
-
-.rp-comment {
-  margin: var(--spacing-01) var(--spacing-03);
-  padding-bottom: var(--spacing-02);
-  line-height: 1.4;
-  border-bottom: solid 1px var(--rp-border-grey);
-
-  &:last-child {
-    margin-bottom: var(--spacing-01);
-    border-bottom-width: 0;
-  }
-
-  .rp-state-overview .rp-entry-list & {
-    margin: var(--spacing-02) var(--spacing-03);
-
-    &:first-child {
-      margin-top: 0;
-      padding-top: var(--spacing-02);
-    }
-  }
-}
-
-.rp-comment-content {
-  margin: 0;
-  color: var(--rp-type-darkgrey);
-  overflow-x: auto; // Long words, like links can overflow without this.
-  white-space: pre-wrap;
-}
-
-.rp-comment-resolver {
-  color: $rp-type-blue;
-}
-
-.rp-comment-resolver-content {
-  font-style: italic;
-  margin: 0;
-}
-
-.rp-comment-reply {
-  padding: 0 var(--spacing-03);
-}
-
-.rp-add-comment-btn,
-.rp-bulk-actions-btn {
-  @extend %rp-button;
-
-  padding: 5px 10px;
-  border-radius: var(--border-radius-base);
-
-  .material-symbols {
-    font-size: inherit;
-    vertical-align: middle;
-    font-weight: bold;
-  }
-}
-
-.rp-bulk-actions-btn {
-  border-radius: 0;
-
-  &:first-child {
-    border-top-left-radius: var(--border-radius-base);
-    border-bottom-left-radius: var(--border-radius-base);
-  }
-
-  &:last-child {
-    border-top-right-radius: var(--border-radius-base);
-    border-bottom-right-radius: var(--border-radius-base);
-    margin-left: 1px;
-  }
-}
-
-.rp-new-comment {
-  padding: var(--spacing-03);
-}
-
-.rp-comment-input {
-  display: block;
-  width: 100%;
-  font-size: var(--rp-base-font-size);
-  padding: var(--spacing-01) var(--spacing-03);
-  border-radius: 3px;
-  border: solid 1px var(--rp-border-grey);
-  resize: vertical;
-  color: var(--rp-type-darkgrey);
-  background-color: var(--white);
-  margin: var(--spacing-02) 0;
-  overflow-x: hidden;
-  min-height: 3em;
-  max-height: 400px;
-}
-
-.rp-icon-delete {
-  display: inline-block;
-  line-height: 1;
-  font-style: normal;
-  font-size: 0.8em;
-  text-decoration: line-through;
-  font-weight: var(--rp-semibold-weight);
-
-  &::before {
-    content: 'Ab';
-  }
-}
-
-.rp-resolved-comment {
-  border-left: solid var(--rp-entry-ribbon-width) $rp-yellow;
-  border-radius: var(--border-radius-base);
-  background-color: var(--white);
-  margin-bottom: var(--spacing-03);
-}
-
-.rp-resolved-comment-context {
-  background-color: color.adjust($rp-yellow, $lightness: 35%);
-  padding: var(--spacing-02) var(--spacing-03);
-}
-
-.rp-resolved-comment-context-file {
-  font-weight: var(--rp-semibold-weight);
-}
-
-.rp-resolved-comment-context-quote {
-  color: #000;
-  font-family: $font-family-monospace;
-  margin: 0;
-}
-
-.rp-entry-callout {
-  transition:
-    top var(--rp-entry-animation-speed),
-    height var(--rp-entry-animation-speed);
-
-  .rp-state-current-file & {
-    position: absolute;
-    border-top: 1px solid grey;
-    border-right: 1px dashed grey;
-
-    &::after {
-      content: '';
-      position: absolute;
-      top: -1px;
-      left: 3px;
-      bottom: 0;
-      border-bottom: 1px solid grey;
-    }
-  }
-
-  .rp-state-current-file-expanded & {
-    width: 3px;
-
-    &::after {
-      width: 3px;
-    }
-  }
-
-  .rp-state-current-file-mini & {
-    width: 1px;
-
-    &::after {
-      width: 1px;
-    }
-  }
-
-  .rp-state-overview & {
-    display: none;
-  }
-
-  .rp-state-current-file &-inverted {
-    border-top: none;
-    border-bottom: 1px solid grey;
-
-    &::after {
-      top: 0;
-      bottom: -1px;
-      border-top: 1px solid grey;
-      border-bottom: none;
-    }
-  }
-
-  .rp-state-current-file &-insert {
-    border-color: var(--rp-green);
-
-    &::after {
-      border-color: var(--rp-green);
-    }
-  }
-
-  .rp-state-current-file &-delete {
-    border-color: var(--rp-red);
-
-    &::after {
-      border-color: var(--rp-red);
-    }
-  }
-
-  .rp-state-current-file &-comment {
-    border-color: $rp-yellow;
-
-    &::after {
-      border-color: $rp-yellow;
-    }
-  }
-
-  .rp-size-mini &-add-comment {
-    display: none;
-  }
-}
-
-.rp-overview-file-header {
-  padding: var(--spacing-01) var(--spacing-03);
-  border-top: solid 1px var(--rp-border-grey);
-  border-bottom: solid 1px var(--rp-border-grey);
-  background-color: var(--rp-bg-dim-blue);
-  margin-top: var(--spacing-05);
-  font-weight: var(--rp-semibold-weight);
-  text-align: center;
-  cursor: pointer;
-}
-
-.rp-overview-file-num-entries {
-  font-weight: normal;
-  font-size: 0.9em;
-}
-
-.rp-overview-file-header-collapse {
-  float: left;
-
-  .material-symbols {
-    font-size: var(--font-size-02);
-  }
-}
-
-.rp-overview-file-entries {
-  overflow: hidden;
-}
-
-.rp-comment-wrapper {
-  transition: 0.35s opacity ease-out 0.2s;
-
-  &-resolving {
-    opacity: 0;
-  }
-}
-
-.rp-nav {
-  display: none;
-  flex-shrink: 0;
-
-  .rp-size-expanded & {
-    display: flex;
-  }
-
-  .rp-state-current-file & {
-    position: absolute;
-    bottom: 0;
-  }
-
-  width: 100%;
-  font-size: var(--rp-icon-large-size);
-  text-align: center;
-  background-color: var(--rp-bg-dim-blue);
-  border-top: solid 1px var(--rp-border-grey);
-  z-index: 2;
-}
-
-.rp-nav-item {
-  display: block;
-  color: color.adjust($rp-type-blue, $lightness: 25%);
-  flex: 0 0 50%;
-  border-top: solid 3px transparent;
-  border-right: 0;
-  border-bottom: 0;
-  border-left: 0;
-  background: none;
-  padding-bottom: var(--spacing-01);
-
-  &:hover,
-  &:focus {
-    text-decoration: none;
-    color: $rp-type-blue;
-  }
-
-  &-active {
-    color: $rp-type-blue;
-    border-top: solid 3px $rp-highlight-blue;
-  }
-}
-
-.rp-nav-label {
-  display: block;
-  font-size: var(--rp-base-font-size);
-}
-
-.resolved-comments-toggle {
-  background-color: var(--bg-light-secondary);
-  font-size: var(--font-size-02);
-  color: color.adjust($rp-type-blue, $lightness: 25%);
-  border: solid 1px var(--rp-border-grey);
-  border-radius: var(--border-radius-base);
-  display: block;
-  padding: 0;
-  height: 22px;
-  width: 22px;
-  line-height: 1.4;
-
-  &:hover,
-  &:focus {
-    text-decoration: none;
-    color: $rp-type-blue;
-  }
-}
-
-.resolved-comments-backdrop {
-  display: none;
-  position: fixed;
-  inset: 0;
-
-  &-visible {
-    display: block;
-  }
-}
-
-.resolved-comments-dropdown {
-  display: none;
-  position: absolute;
-  width: 300px;
-  left: -150px;
-  max-height: calc(100vh - 100px);
-  margin-top: calc(var(--rp-entry-arrow-width) * 1.5);
-  margin-left: 1em;
-  background-color: $rp-bg-blue;
-  text-align: left;
-  align-items: stretch;
-  justify-content: center;
-  border-radius: var(--border-radius-base);
-  box-shadow: 0 0 20px 10px rgba(0 0 0 / 30%);
-  z-index: 1;
-
-  &::before {
-    content: '';
-
-    @include triangle(
-      top,
-      $rp-entry-arrow-width * 3,
-      $rp-entry-arrow-width * 1.5,
-      $rp-bg-blue
-    );
-
-    top: calc(var(--rp-entry-ribbon-width) * -2);
-    left: 50%;
-    margin-left: calc(var(--rp-entry-arrow-width) * -0.75);
-  }
-
-  &-open {
-    display: flex;
-  }
-}
-
-.resolved-comments-scroller {
-  flex: 0 0 auto; // Can't use 100% in the flex-basis key here, IE won't account for padding.
-  width: 100%; // We need to set the width explicitly, as flex-basis won't work.
-  padding: var(--spacing-03);
-  overflow-y: auto;
-  max-height: calc(
-    100vh - 100px
-  ); // We also need to explicitly set the max-height, IE won't compute the flex-determined height.
-}
-
-.rp-collapse-toggle {
-  padding: var(--spacing-01) 0;
-  color: $rp-type-blue;
-  font-weight: var(--rp-semibold-weight);
-  border: 0;
-  background-color: transparent;
-
-  &:hover,
-  &:focus {
-    color: color.adjust($rp-type-blue, $lightness: -5%);
-    text-decoration: none;
-  }
-}
-
-.rp-track-changes-indicator {
-  display: block;
-  padding: 5px 10px;
-  background-color: rgba(240 240 240 / 90%);
-  color: $rp-type-blue;
-  text-align: center;
-  border-bottom-left-radius: var(--border-radius-base);
-  white-space: nowrap;
-
-  &.rp-track-changes-indicator-on-dark {
-    background-color: rgba(88 88 88 / 80%);
-    color: var(--white);
-
-    &:hover,
-    &:focus {
-      background-color: rgba(88 88 88 / 100%);
-      color: var(--white);
-    }
-  }
-
-  &:hover,
-  &:focus {
-    outline: 0;
-    text-decoration: none;
-    background-color: rgba(240 240 240 / 100%);
-    color: $rp-type-blue;
-  }
-}
-
-.review-panel-toggler {
-  display: none;
-  position: absolute;
-  top: 0;
-  bottom: 0;
-  width: 10px;
-  opacity: 0.5;
-  color: $rp-highlight-blue;
-  z-index: 1;
-  background-color: transparent;
-  transition: background 0.1s;
-  flex-direction: column;
-  padding: 0;
-  border: 0;
-
-  .rp-size-mini &,
-  .rp-size-expanded & {
-    display: flex;
-  }
-
-  .rp-size-expanded & {
-    .review-panel-toggler-icon .material-symbols {
-      transform: translateX(-50%) rotate(180deg);
-    }
-  }
-
-  &:hover,
-  &:focus {
-    color: $rp-highlight-blue;
-    background-color: var(--white);
-  }
-
-  .review-panel-toggler-icon {
-    position: sticky;
-    top: 50%;
-    bottom: 50%;
-    width: 100%;
-    overflow: hidden;
-
-    .material-symbols {
-      transform: translateX(-50%);
-      margin-left: 50%;
-    }
-  }
-}
-
-.review-panel {
-  position: relative;
-  z-index: 6;
-  flex-shrink: 0;
-  background-color: $rp-bg-blue;
-  border-left: solid 0 var(--rp-border-grey);
-  font-family: $font-family-base;
-  line-height: $line-height-base;
-  font-size: var(--rp-base-font-size);
-  color: $rp-type-blue;
-  box-sizing: content-box;
-
-  .rp-size-expanded & {
-    display: flex;
-    flex-direction: column;
-    width: var(--review-panel-width);
-    overflow: visible;
-    border-left-width: 1px;
-  }
-
-  .rp-size-mini & {
-    width: var(--review-off-width);
-    z-index: 6;
-    border-left-width: 1px;
-  }
-
-  .review-panel-toolbar-collapse-button {
-    display: inline-flex;
-    align-items: center;
-    border: none;
-    background: none;
-    padding: 0;
-    color: $rp-type-blue;
-  }
-
-  &.rp-current-file-container {
-    display: none;
-
-    .rp-size-mini &,
-    .rp-size-expanded & {
-      display: block;
-    }
-
-    .review-panel-toolbar {
-      position: sticky;
-      top: 0;
-    }
-
-    .rp-nav {
-      position: sticky;
-      bottom: 0;
-    }
-  }
-
-  .rp-entry-list-react {
-    position: relative;
-    overflow: hidden;
-
-    .rp-size-mini & {
-      overflow: visible;
-    }
-  }
-
-  .rp-state-current-file & {
-    .review-panel-tools {
-      display: flex;
-      flex-direction: column;
-      justify-content: space-between;
-      position: absolute;
-      inset: 0;
-    }
-  }
-
-  .rp-state-overview & {
-    position: sticky;
-    top: 0;
-    display: flex;
-    flex-direction: column;
-    height: 100%;
-  }
-}
-
-.rp-floating-entry {
-  position: absolute;
-  font-size: var(--rp-base-font-size);
-  color: $rp-type-blue;
-}
-
-.rp-floating-entry,
-.review-panel {
-  .rp-entry-metadata {
-    button {
-      padding: 0;
-      border: 0;
-      background-color: transparent;
-      color: var(--bg-info-01);
-
-      &:hover,
-      &:focus {
-        text-decoration: underline;
-      }
-    }
-  }
-}
-
-.rp-in-editor-widgets {
-  position: sticky;
-  top: 0;
-  right: 0;
-  font-size: 0.6875rem; // 11px
-  z-index: 2;
-  font-family: $font-family-sans-serif;
-
-  .rp-in-editor-widgets-inner {
-    position: absolute;
-    top: 0;
-    right: 0;
-    display: flex;
-    flex-direction: column;
-  }
-
-  .rp-track-changes-indicator {
-    border: 0;
-  }
-
-  .rp-add-comment-btn,
-  .rp-bulk-actions-btn {
-    white-space: nowrap;
-    border-radius: 0;
-
-    &:last-child {
-      border-bottom-left-radius: var(--border-radius-base);
-    }
-  }
-}
-
-.track-changes-indicator-circle {
-  display: inline-block;
-  width: 10px;
-  height: 10px;
-  border-radius: 100%;
-  background-color: var(--bg-accent-01);
-}

+ 0 - 16
services/web/locales/en.json

@@ -37,7 +37,6 @@
   "about_writefull": "About Writefull",
   "about_writefull": "About Writefull",
   "abstract": "Abstract",
   "abstract": "Abstract",
   "accept": "Accept",
   "accept": "Accept",
-  "accept_all": "Accept all",
   "accept_and_continue": "Accept and continue",
   "accept_and_continue": "Accept and continue",
   "accept_change": "Accept change",
   "accept_change": "Accept change",
   "accept_change_error_description": "There was an error accepting a track change. Please try again in a few moments.",
   "accept_change_error_description": "There was an error accepting a track change. Please try again in a few moments.",
@@ -181,7 +180,6 @@
   "are_you_affiliated_with_an_institution": "Are you affiliated with an institution?",
   "are_you_affiliated_with_an_institution": "Are you affiliated with an institution?",
   "are_you_getting_an_undefined_control_sequence_error": "Are you getting an Undefined Control Sequence error? If you are, make sure you’ve loaded the graphicx package—<0>\\usepackage{graphicx}</0>—in the preamble (first section of code) in your document. <1>Learn more</1>",
   "are_you_getting_an_undefined_control_sequence_error": "Are you getting an Undefined Control Sequence error? If you are, make sure you’ve loaded the graphicx package—<0>\\usepackage{graphicx}</0>—in the preamble (first section of code) in your document. <1>Learn more</1>",
   "are_you_still_at": "Are you still at <0>__institutionName__</0>?",
   "are_you_still_at": "Are you still at <0>__institutionName__</0>?",
-  "are_you_sure": "Are you sure?",
   "are_you_sure_you_want_to_cancel_add_on": "Are you sure you want to cancel the <strong>__addOnName__</strong> add-on?",
   "are_you_sure_you_want_to_cancel_add_on": "Are you sure you want to cancel the <strong>__addOnName__</strong> add-on?",
   "article": "Article",
   "article": "Article",
   "articles": "Articles",
   "articles": "Articles",
@@ -241,8 +239,6 @@
   "brl_discount_offer_plans_page_banner": "__flag__ <b>Great news!</b> We’ve applied a 50% discount to premium plans on this page for our users in Brazil. Check out the new lower prices.",
   "brl_discount_offer_plans_page_banner": "__flag__ <b>Great news!</b> We’ve applied a 50% discount to premium plans on this page for our users in Brazil. Check out the new lower prices.",
   "browser": "Browser",
   "browser": "Browser",
   "built_in": "Built-In",
   "built_in": "Built-In",
-  "bulk_accept_confirm": "Are you sure you want to accept the selected __nChanges__ changes?",
-  "bulk_reject_confirm": "Are you sure you want to reject the selected __nChanges__ changes?",
   "buy_now_no_exclamation_mark": "Buy now",
   "buy_now_no_exclamation_mark": "Buy now",
   "by": "by",
   "by": "by",
   "by_joining_labs": "By joining Labs, you agree to receive occasional emails and updates from Overleaf—for example, to request your feedback. You also agree to our <0>terms of service</0> and <1>privacy notice</1>.",
   "by_joining_labs": "By joining Labs, you agree to receive occasional emails and updates from Overleaf—for example, to request your feedback. You also agree to our <0>terms of service</0> and <1>privacy notice</1>.",
@@ -353,7 +349,6 @@
   "column_width_is_custom_click_to_resize": "Column width is custom. Click to resize",
   "column_width_is_custom_click_to_resize": "Column width is custom. Click to resize",
   "column_width_is_x_click_to_resize": "Column width is __width__. Click to resize",
   "column_width_is_x_click_to_resize": "Column width is __width__. Click to resize",
   "comment": "Comment",
   "comment": "Comment",
-  "comment_submit_error": "Sorry, there was a problem submitting your comment",
   "commit": "Commit",
   "commit": "Commit",
   "common": "Common",
   "common": "Common",
   "common_causes_of_compile_timeouts_include": "Common causes of compile timeouts include",
   "common_causes_of_compile_timeouts_include": "Common causes of compile timeouts include",
@@ -673,7 +668,6 @@
   "error_opening_document_detail": "Sorry, something went wrong opening this document. Please try again.",
   "error_opening_document_detail": "Sorry, something went wrong opening this document. Please try again.",
   "error_performing_request": "An error has occurred while performing your request.",
   "error_performing_request": "An error has occurred while performing your request.",
   "error_processing_file": "Sorry, something went wrong processing this file. Please try again.",
   "error_processing_file": "Sorry, something went wrong processing this file. Please try again.",
-  "error_submitting_comment": "Error submitting comment",
   "es": "Spanish",
   "es": "Spanish",
   "estimated_number_of_overleaf_users": "Estimated number of __appName__ users",
   "estimated_number_of_overleaf_users": "Estimated number of __appName__ users",
   "every": "per",
   "every": "per",
@@ -946,7 +940,6 @@
   "history_view_a11y_description": "Show all of the project history or only labelled versions.",
   "history_view_a11y_description": "Show all of the project history or only labelled versions.",
   "history_view_all": "All history",
   "history_view_all": "All history",
   "history_view_labels": "Labels",
   "history_view_labels": "Labels",
-  "hit_enter_to_reply": "Hit Enter to reply",
   "home": "Home",
   "home": "Home",
   "hotkey_add_a_comment": "Add a comment",
   "hotkey_add_a_comment": "Add a comment",
   "hotkey_autocomplete_menu": "Autocomplete Menu",
   "hotkey_autocomplete_menu": "Autocomplete Menu",
@@ -1293,7 +1286,6 @@
   "managers_management": "Managers management",
   "managers_management": "Managers management",
   "managing_your_subscription": "Managing your subscription",
   "managing_your_subscription": "Managing your subscription",
   "march": "March",
   "march": "March",
-  "mark_as_resolved": "Mark as resolved",
   "marked_as_resolved": "Marked as resolved",
   "marked_as_resolved": "Marked as resolved",
   "math_display": "Math Display",
   "math_display": "Math Display",
   "math_inline": "Math Inline",
   "math_inline": "Math Inline",
@@ -1388,7 +1380,6 @@
   "no_articles_matching_your_tags": "There are no articles matching your tags",
   "no_articles_matching_your_tags": "There are no articles matching your tags",
   "no_borders": "No borders",
   "no_borders": "No borders",
   "no_caption": "No caption",
   "no_caption": "No caption",
-  "no_comments": "No comments",
   "no_comments_or_suggestions": "No comments or suggestions",
   "no_comments_or_suggestions": "No comments or suggestions",
   "no_existing_password": "Please use the password reset form to set your password",
   "no_existing_password": "Please use the password reset form to set your password",
   "no_featured_templates": "No featured templates",
   "no_featured_templates": "No featured templates",
@@ -1412,7 +1403,6 @@
   "no_preview_available": "Sorry, no preview is available.",
   "no_preview_available": "Sorry, no preview is available.",
   "no_projects": "No projects",
   "no_projects": "No projects",
   "no_resolved_comments": "No resolved comments",
   "no_resolved_comments": "No resolved comments",
-  "no_resolved_threads": "No resolved threads",
   "no_search_results": "No Search Results",
   "no_search_results": "No Search Results",
   "no_selection_select_file": "Currently, no file is selected. Please select a file from the file tree.",
   "no_selection_select_file": "Currently, no file is selected. Please select a file from the file tree.",
   "no_symbols_found": "No symbols found",
   "no_symbols_found": "No symbols found",
@@ -1700,7 +1690,6 @@
   "push_sharelatex_changes_to_github": "Push __appName__ changes to GitHub",
   "push_sharelatex_changes_to_github": "Push __appName__ changes to GitHub",
   "push_to_github_pull_to_overleaf": "Push to GitHub, pull to __appName__",
   "push_to_github_pull_to_overleaf": "Push to GitHub, pull to __appName__",
   "quoted_text": "Quoted text",
   "quoted_text": "Quoted text",
-  "quoted_text_in": "Quoted text in",
   "raw_logs": "Raw logs",
   "raw_logs": "Raw logs",
   "raw_logs_description": "Raw logs from the LaTeX compiler",
   "raw_logs_description": "Raw logs from the LaTeX compiler",
   "react_history_tutorial_content": "To compare a range of versions, use the  <0></0> on the versions you want at the start and end of the range. To add a label or to download a version use the options in the three-dot menu. <1>Learn more about using Overleaf History.</1>",
   "react_history_tutorial_content": "To compare a range of versions, use the  <0></0> on the versions you want at the start and end of the range. To add a label or to download a version use the options in the three-dot menu. <1>Learn more about using Overleaf History.</1>",
@@ -1765,7 +1754,6 @@
   "registering": "Registering",
   "registering": "Registering",
   "registration_error": "Registration error",
   "registration_error": "Registration error",
   "reject": "Reject",
   "reject": "Reject",
-  "reject_all": "Reject all",
   "reject_change": "Reject change",
   "reject_change": "Reject change",
   "reject_selected_changes": "Reject selected changes",
   "reject_selected_changes": "Reject selected changes",
   "related_tags": "Related Tags",
   "related_tags": "Related Tags",
@@ -1984,7 +1972,6 @@
   "shared_with_you": "Shared with you",
   "shared_with_you": "Shared with you",
   "sharelatex_beta_program": "__appName__ Beta Program",
   "sharelatex_beta_program": "__appName__ Beta Program",
   "shortcut_to_open_advanced_reference_search": "(<strong>__ctrlSpace__</strong> or <strong>__altSpace__</strong>)",
   "shortcut_to_open_advanced_reference_search": "(<strong>__ctrlSpace__</strong> or <strong>__altSpace__</strong>)",
-  "show_all": "show all",
   "show_all_projects": "Show all projects",
   "show_all_projects": "Show all projects",
   "show_document_preamble": "Show document preamble",
   "show_document_preamble": "Show document preamble",
   "show_file_tree": "Show file tree",
   "show_file_tree": "Show file tree",
@@ -2171,9 +2158,6 @@
   "take_survey": "Take survey",
   "take_survey": "Take survey",
   "tc_everyone": "Everyone",
   "tc_everyone": "Everyone",
   "tc_guests": "Guests",
   "tc_guests": "Guests",
-  "tc_switch_everyone_tip": "Toggle track-changes for everyone",
-  "tc_switch_guests_tip": "Toggle track-changes for all link-sharing guests",
-  "tc_switch_user_tip": "Toggle track-changes for this user",
   "tell_the_project_owner_and_ask_them_to_upgrade": "<0>Tell the project owner</0> and ask them to upgrade their Overleaf plan if you need more compile time.",
   "tell_the_project_owner_and_ask_them_to_upgrade": "<0>Tell the project owner</0> and ask them to upgrade their Overleaf plan if you need more compile time.",
   "template": "Template",
   "template": "Template",
   "template_approved_by_publisher": "This template has been approved by the publisher",
   "template_approved_by_publisher": "This template has been approved by the publisher",

+ 3 - 1
services/web/test/frontend/features/review-panel/review-panel.spec.tsx

@@ -3,7 +3,9 @@ import { EditorProviders } from '../../helpers/editor-providers'
 import { mockScope } from '../source-editor/helpers/mock-scope'
 import { mockScope } from '../source-editor/helpers/mock-scope'
 import { TestContainer } from '../source-editor/helpers/test-container'
 import { TestContainer } from '../source-editor/helpers/test-container'
 
 
-describe('<ReviewPanel />', function () {
+// TODO: update tests and re-enable once reviewer role is active
+// eslint-disable-next-line mocha/no-skipped-tests
+describe.skip('<ReviewPanel />', function () {
   beforeEach(function () {
   beforeEach(function () {
     window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
     window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
 
 

+ 0 - 81
services/web/test/frontend/features/review-panel/unit/compare-props-with-shallow-array-compare.tests.ts

@@ -1,81 +0,0 @@
-import { expect } from 'chai'
-import comparePropsWithShallowArrayCompare from '../../../../../frontend/js/features/source-editor/components/review-panel/utils/compare-props-with-shallow-array-compare'
-
-describe('comparePropsWithShallowArrayCompare', function () {
-  it('is true with all equal non-array props', function () {
-    type NoArrayProps = { prop1: string; prop2: number }
-
-    const props1: NoArrayProps = { prop1: 'wombat', prop2: 1 }
-    const props2: NoArrayProps = { prop1: 'wombat', prop2: 1 }
-
-    expect(comparePropsWithShallowArrayCompare()(props1, props2)).to.be.true
-  })
-
-  it('is false with non-equal non-array props', function () {
-    type NoArrayProps = { prop1: string; prop2: number }
-
-    const props1: NoArrayProps = { prop1: 'wombat', prop2: 1 }
-    const props2: NoArrayProps = { prop1: 'squirrel', prop2: 1 }
-
-    expect(comparePropsWithShallowArrayCompare()(props1, props2)).to.be.false
-  })
-
-  it('is false with similar but not specified array prop', function () {
-    type ArrayProps = { prop1: string; prop2: number[] }
-
-    const props1: ArrayProps = { prop1: 'wombat', prop2: [1] }
-    const props2: ArrayProps = { prop1: 'wombat', prop2: [1] }
-
-    expect(comparePropsWithShallowArrayCompare()(props1, props2)).to.be.false
-  })
-
-  it('is true with similar and specified array prop', function () {
-    type ArrayProps = { prop1: string; prop2: number[] }
-
-    const props1: ArrayProps = { prop1: 'wombat', prop2: [1] }
-    const props2: ArrayProps = { prop1: 'wombat', prop2: [1] }
-
-    expect(
-      comparePropsWithShallowArrayCompare<ArrayProps>('prop2')(props1, props2)
-    ).to.be.true
-  })
-
-  it('is false with non-similar and specified array prop', function () {
-    type ArrayProps = { prop1: string; prop2: number[] }
-
-    const props1: ArrayProps = { prop1: 'wombat', prop2: [1] }
-    const props2: ArrayProps = { prop1: 'wombat', prop2: [2] }
-
-    expect(
-      comparePropsWithShallowArrayCompare<ArrayProps>('prop2')(props1, props2)
-    ).to.be.false
-  })
-
-  it('is false with multiple similar array props with not all specified', function () {
-    type MultipleArrayProps = { prop1: number[]; prop2: number[] }
-
-    const props1: MultipleArrayProps = { prop1: [1], prop2: [2] }
-    const props2: MultipleArrayProps = { prop1: [1], prop2: [2] }
-
-    expect(
-      comparePropsWithShallowArrayCompare<MultipleArrayProps>('prop1')(
-        props1,
-        props2
-      )
-    ).to.be.false
-  })
-
-  it('is true with multiple similar array props with all specified', function () {
-    type MultipleArrayProps = { prop1: number[]; prop2: number[] }
-
-    const props1: MultipleArrayProps = { prop1: [1], prop2: [2] }
-    const props2: MultipleArrayProps = { prop1: [1], prop2: [2] }
-
-    expect(
-      comparePropsWithShallowArrayCompare<MultipleArrayProps>('prop1', 'prop2')(
-        props1,
-        props2
-      )
-    ).to.be.true
-  })
-})

+ 11 - 13
services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-command-tooltip.spec.tsx

@@ -128,19 +128,18 @@ describe('<CodeMirrorEditor/> command tooltip in Visual mode', function () {
     // assert the focused command is undecorated
     // assert the focused command is undecorated
     cy.get('@content-line').should('have.text', '\\include{foo}')
     cy.get('@content-line').should('have.text', '\\include{foo}')
 
 
-    cy.window().then(win => {
-      cy.stub(win, 'dispatchEvent').as('dispatch-event')
-    })
+    cy.contains('figures/foo.tex').click()
+    cy.get('@content-line').should('have.text', '\\include{figures/foo}')
+    cy.get('@content-line').type('{leftArrow}')
 
 
-    // open the target
+    // assert the unfocused command has a menu
     cy.findByRole('menu').should('have.length', 1)
     cy.findByRole('menu').should('have.length', 1)
-    cy.findByRole('button', { name: 'Edit file' }).click()
-    cy.get('@dispatch-event').should('have.been.calledOnce')
+    cy.findByText('Edit file')
 
 
     // assert the unfocused command is decorated
     // assert the unfocused command is decorated
     cy.get('@content-line').type('{downArrow}')
     cy.get('@content-line').type('{downArrow}')
     cy.findByRole('menu').should('have.length', 0)
     cy.findByRole('menu').should('have.length', 0)
-    cy.get('@content-line').should('have.text', '\\include{foo}')
+    cy.get('@content-line').should('have.text', '\\include{figures/foo}')
   })
   })
 
 
   it('shows a tooltip for \\input', function () {
   it('shows a tooltip for \\input', function () {
@@ -153,19 +152,18 @@ describe('<CodeMirrorEditor/> command tooltip in Visual mode', function () {
     // assert the focused command is undecorated
     // assert the focused command is undecorated
     cy.get('@content-line').should('have.text', '\\input{foo}')
     cy.get('@content-line').should('have.text', '\\input{foo}')
 
 
-    cy.window().then(win => {
-      cy.stub(win, 'dispatchEvent').as('dispatch-event')
-    })
+    cy.contains('figures/foo.tex').click()
+    cy.get('@content-line').should('have.text', '\\input{figures/foo}')
+    cy.get('@content-line').type('{leftArrow}')
 
 
     // open the target
     // open the target
     cy.findByRole('menu').should('have.length', 1)
     cy.findByRole('menu').should('have.length', 1)
-    cy.findByRole('button', { name: 'Edit file' }).click()
-    cy.get('@dispatch-event').should('have.been.calledOnce')
+    cy.findByRole('button', { name: 'Edit file' })
 
 
     // assert the unfocused command is decorated
     // assert the unfocused command is decorated
     cy.get('@content-line').type('{downArrow}')
     cy.get('@content-line').type('{downArrow}')
     cy.findByRole('menu').should('have.length', 0)
     cy.findByRole('menu').should('have.length', 0)
-    cy.get('@content-line').should('have.text', '\\input{foo}')
+    cy.get('@content-line').should('have.text', '\\input{figures/foo}')
   })
   })
 
 
   it('shows a tooltip for \\ref', function () {
   it('shows a tooltip for \\ref', function () {

+ 15 - 0
services/web/test/frontend/features/source-editor/helpers/mock-doc.ts

@@ -79,11 +79,26 @@ export const mockDoc = (content = defaultContent) => {
       getIdSeed: () => '123',
       getIdSeed: () => '123',
       setIdSeed: () => {},
       setIdSeed: () => {},
       getTrackedDeletesLength: () => 0,
       getTrackedDeletesLength: () => 0,
+      getDirtyState: () => ({
+        comment: {
+          moved: {},
+          removed: {},
+          added: {},
+        },
+        change: {
+          moved: {},
+          removed: {},
+          added: {},
+        },
+      }),
+      resetDirtyState: () => {},
     },
     },
     setTrackChangesIdSeeds: () => {},
     setTrackChangesIdSeeds: () => {},
     getTrackingChanges: () => true,
     getTrackingChanges: () => true,
     setTrackingChanges: () => {},
     setTrackingChanges: () => {},
     getInflightOp: () => null,
     getInflightOp: () => null,
     getPendingOp: () => null,
     getPendingOp: () => null,
+    hasBufferedOps: () => false,
+    leaveAndCleanUpPromise: () => false,
   }
   }
 }
 }

+ 7 - 9
services/web/test/frontend/features/source-editor/helpers/mock-scope.ts

@@ -35,7 +35,12 @@ export const mockScope = (content?: string) => {
             {
             {
               _id: figuresFolderId,
               _id: figuresFolderId,
               name: 'figures',
               name: 'figures',
-              docs: [],
+              docs: [
+                {
+                  _id: 'fake-nested-doc-id',
+                  name: 'foo.tex',
+                },
+              ],
               folders: [],
               folders: [],
               fileRefs: [
               fileRefs: [
                 {
                 {
@@ -65,15 +70,8 @@ export const mockScope = (content?: string) => {
       trackedWrite: true,
       trackedWrite: true,
       write: true,
       write: true,
     },
     },
-    reviewPanel: {
-      subView: 'cur_file',
-      formattedProjectMembers: {},
-      fullTCStateCollapsed: true,
-      entries: {},
-      resolvedComments: {},
-    },
     ui: {
     ui: {
-      reviewPanelOpen: true,
+      reviewPanelOpen: false,
     },
     },
     toggleReviewPanel: cy.stub(),
     toggleReviewPanel: cy.stub(),
     toggleTrackChangesForEveryone: cy.stub(),
     toggleTrackChangesForEveryone: cy.stub(),

+ 2 - 2
services/web/test/frontend/infrastructure/i18n.spec.tsx

@@ -5,10 +5,10 @@ describe('i18n', function () {
     it('translates a plain string', function () {
     it('translates a plain string', function () {
       const Test = () => {
       const Test = () => {
         const { t } = useTranslation()
         const { t } = useTranslation()
-        return <div>{t('accept')}</div>
+        return <div>{t('accept_change')}</div>
       }
       }
       cy.mount(<Test />)
       cy.mount(<Test />)
-      cy.findByText('Accept')
+      cy.findByText('Accept change')
     })
     })
 
 
     it('uses defaultValues', function () {
     it('uses defaultValues', function () {

+ 0 - 14
services/web/types/review-panel/api.ts

@@ -1,14 +0,0 @@
-import { ReviewPanelCommentThreadMessage, ThreadId } from './review-panel'
-import { MergeAndOverride } from '../utils'
-
-export type ReviewPanelCommentThreadMessageApi = MergeAndOverride<
-  ReviewPanelCommentThreadMessage,
-  { timestamp: number }
->
-
-export type ReviewPanelCommentThreadsApi = Record<
-  ThreadId,
-  {
-    messages: ReviewPanelCommentThreadMessageApi[]
-  }
->

+ 0 - 78
services/web/types/review-panel/entry.ts

@@ -1,78 +0,0 @@
-import { ThreadId } from './review-panel'
-import { UserId } from '../user'
-
-export interface ReviewPanelEntryScreenPos {
-  y: number
-  height: number
-  editorPaddingTop: number
-}
-
-interface ReviewPanelBaseEntry {
-  focused: boolean
-  offset: number
-  screenPos: ReviewPanelEntryScreenPos
-  visible: boolean
-  inViewport: boolean
-}
-
-interface ReviewPanelInsertOrDeleteEntry {
-  content: string
-  entry_ids: ThreadId[]
-  metadata: {
-    ts: Date
-    user_id: UserId
-  }
-}
-
-export interface ReviewPanelInsertEntry
-  extends ReviewPanelBaseEntry,
-    ReviewPanelInsertOrDeleteEntry {
-  type: 'insert'
-}
-
-export interface ReviewPanelDeleteEntry
-  extends ReviewPanelBaseEntry,
-    ReviewPanelInsertOrDeleteEntry {
-  type: 'delete'
-}
-
-export type ReviewPanelChangeEntry =
-  | ReviewPanelInsertEntry
-  | ReviewPanelDeleteEntry
-
-export interface ReviewPanelCommentEntry extends ReviewPanelBaseEntry {
-  type: 'comment'
-  content: string
-  entry_ids: ThreadId[]
-  thread_id: ThreadId
-  replyContent?: string // angular specific
-}
-
-export interface ReviewPanelAggregateChangeEntry extends ReviewPanelBaseEntry {
-  type: 'aggregate-change'
-  content: string
-  entry_ids: ThreadId[]
-  metadata: {
-    replaced_content: string
-    ts: Date
-    user_id: UserId
-  }
-}
-
-export interface ReviewPanelAddCommentEntry extends ReviewPanelBaseEntry {
-  type: 'add-comment'
-  length: number
-}
-
-export interface ReviewPanelBulkActionsEntry extends ReviewPanelBaseEntry {
-  type: 'bulk-actions'
-  length: number
-}
-
-export type ReviewPanelEntry =
-  | ReviewPanelCommentEntry
-  | ReviewPanelInsertEntry
-  | ReviewPanelDeleteEntry
-  | ReviewPanelAggregateChangeEntry
-  | ReviewPanelAddCommentEntry
-  | ReviewPanelBulkActionsEntry

+ 0 - 25
services/web/types/review-panel/review-panel.ts

@@ -1,27 +1,9 @@
 import { Brand } from '../helpers/brand'
 import { Brand } from '../helpers/brand'
-import { DocId } from '../project-settings'
 import { UserId } from '../user'
 import { UserId } from '../user'
-import {
-  ReviewPanelAddCommentEntry,
-  ReviewPanelBulkActionsEntry,
-  ReviewPanelEntry,
-} from './entry'
-import { ReviewPanelCommentThread } from './comment-thread'
 
 
 export type SubView = 'cur_file' | 'overview'
 export type SubView = 'cur_file' | 'overview'
 
 
 export type ThreadId = Brand<string, 'ThreadId'>
 export type ThreadId = Brand<string, 'ThreadId'>
-// Entries may contain `add-comment` and `bulk-actions` props along with DocIds
-// Ideally the `add-comment` and `bulk-actions` objects should not be within the entries object
-// as the doc data, but this is what currently angular returns.
-export type ReviewPanelDocEntries = Record<
-  | ThreadId
-  | ReviewPanelAddCommentEntry['type']
-  | ReviewPanelBulkActionsEntry['type'],
-  ReviewPanelEntry
->
-
-export type ReviewPanelEntries = Record<DocId, ReviewPanelDocEntries>
 
 
 export interface ReviewPanelUser {
 export interface ReviewPanelUser {
   avatar_text: string
   avatar_text: string
@@ -32,8 +14,6 @@ export interface ReviewPanelUser {
   name: string
   name: string
 }
 }
 
 
-export type ReviewPanelUsers = Record<UserId, ReviewPanelUser>
-
 export type CommentId = Brand<string, 'CommentId'>
 export type CommentId = Brand<string, 'CommentId'>
 
 
 export interface ReviewPanelCommentThreadMessage {
 export interface ReviewPanelCommentThreadMessage {
@@ -43,8 +23,3 @@ export interface ReviewPanelCommentThreadMessage {
   user: ReviewPanelUser
   user: ReviewPanelUser
   user_id: UserId
   user_id: UserId
 }
 }
-
-export type ReviewPanelCommentThreads = Record<
-  ThreadId,
-  ReviewPanelCommentThread
->

+ 0 - 2
services/web/types/utils.ts

@@ -19,6 +19,4 @@ export type DeepPartial<T> = Partial<{ [P in keyof T]: DeepPartial<T[P]> }>
 
 
 export type MergeAndOverride<Parent, Own> = Own & Omit<Parent, keyof Own>
 export type MergeAndOverride<Parent, Own> = Own & Omit<Parent, keyof Own>
 
 
-export type Entries<T extends object> = [keyof T, T[keyof T]][]
-
 export type Keys<T extends object> = (keyof T)[]
 export type Keys<T extends object> = (keyof T)[]