Ver Fonte

Remove Angular (#17534)

GitOrigin-RevId: 7a0d45e17d9905fa75569e2d19ca59caa4a41565
Alf Eaton há 2 anos atrás
pai
commit
c24ace801b
100 ficheiros alterados com 62 adições e 9100 exclusões
  1. 0 224
      package-lock.json
  2. 0 9
      patches/ngcomponent+4.1.0.patch
  3. 0 9
      patches/react2angular+4.0.6.patch
  4. 2 8
      services/web/app/src/Features/Project/ProjectController.js
  5. 1 13
      services/web/app/src/Features/TokenAccess/TokenAccessController.js
  6. 1 1
      services/web/app/views/layout-base.pug
  7. 0 21
      services/web/app/views/layout.pug
  8. 0 42
      services/web/app/views/layout/footer.pug
  9. 0 147
      services/web/app/views/layout/navbar.pug
  10. 0 124
      services/web/app/views/project/editor.pug
  11. 0 52
      services/web/app/views/project/editor/editor-pane.pug
  12. 0 53
      services/web/app/views/project/editor/editor.pug
  13. 0 3
      services/web/app/views/project/editor/file-tree-history-react.pug
  14. 0 27
      services/web/app/views/project/editor/file-tree-react.pug
  15. 0 9
      services/web/app/views/project/editor/file-view.pug
  16. 0 12
      services/web/app/views/project/editor/header-react.pug
  17. 0 1
      services/web/app/views/project/editor/left-menu-react.pug
  18. 0 39
      services/web/app/views/project/editor/main.pug
  19. 0 1
      services/web/app/views/project/editor/meta.pug
  20. 1 1
      services/web/app/views/project/ide-react-detached.pug
  21. 1 1
      services/web/app/views/project/ide-react.pug
  22. 0 127
      services/web/app/views/project/token/access.pug
  23. 0 3
      services/web/app/views/source-editor/source-editor.pug
  24. 4 6
      services/web/app/views/subscriptions/interstitial-payment.pug
  25. 1 1
      services/web/app/views/subscriptions/plans.pug
  26. 1 1
      services/web/app/views/subscriptions/team/invite_logged_out.pug
  27. 0 47
      services/web/app/views/view_templates/bonus_templates.pug
  28. 1 1
      services/web/babel.config.json
  29. 1 31
      services/web/config/settings.defaults.js
  30. 3 0
      services/web/cypress/support/shared/commands/index.ts
  31. 3 0
      services/web/cypress/support/shared/commands/metadata.ts
  32. 0 61
      services/web/frontend/js/base.js
  33. 0 134
      services/web/frontend/js/directives/eventTracking.js
  34. 0 6
      services/web/frontend/js/features/editor-left-menu/controllers/editor-left-menu-controller.js
  35. 1 2
      services/web/frontend/js/features/editor-left-menu/hooks/use-project-wide-settings-socket-listener.tsx
  36. 1 1
      services/web/frontend/js/features/editor-left-menu/hooks/use-project-wide-settings.tsx
  37. 1 1
      services/web/frontend/js/features/editor-left-menu/hooks/use-save-project-settings.tsx
  38. 0 30
      services/web/frontend/js/features/editor-navigation-toolbar/controllers/editor-navigation-toolbar-controller.js
  39. 0 123
      services/web/frontend/js/features/file-tree/controllers/file-tree-controller.js
  40. 0 24
      services/web/frontend/js/features/file-view/controllers/file-view-controller.js
  41. 0 6
      services/web/frontend/js/features/history/controllers/history-controller.js
  42. 3 3
      services/web/frontend/js/features/ide-react/context/connection-context.tsx
  43. 3 1
      services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx
  44. 2 2
      services/web/frontend/js/features/ide-react/context/ide-react-context.tsx
  45. 1 7
      services/web/frontend/js/features/ide-react/context/metadata-context.tsx
  46. 3 1
      services/web/frontend/js/features/ide-react/context/permissions-context.tsx
  47. 8 8
      services/web/frontend/js/features/ide-react/context/react-context-root.tsx
  48. 1 5
      services/web/frontend/js/features/ide-react/context/review-panel/hooks/use-review-panel-state.ts
  49. 0 22
      services/web/frontend/js/features/ide-react/scope-event-emitter/angular-scope-event-emitter.ts
  50. 0 28
      services/web/frontend/js/features/ide-react/scope-value-store/angular-scope-value-store.ts
  51. 0 9
      services/web/frontend/js/features/outline/controllers/outline-controller.js
  52. 3 3
      services/web/frontend/js/features/pdf-preview/components/pdf-preview-detached-root.jsx
  53. 0 19
      services/web/frontend/js/features/pdf-preview/controllers/pdf-preview-controller.js
  54. 0 68
      services/web/frontend/js/features/share-project-modal/controllers/react-share-project-modal-controller.js
  55. 4 23
      services/web/frontend/js/features/source-editor/components/review-panel/editor-widgets/editor-widgets.tsx
  56. 4 17
      services/web/frontend/js/features/source-editor/components/review-panel/review-panel.tsx
  57. 0 275
      services/web/frontend/js/features/source-editor/context/review-panel/hooks/use-angular-review-panel-state.ts
  58. 0 20
      services/web/frontend/js/features/source-editor/context/review-panel/review-panel-provider.tsx
  59. 0 9
      services/web/frontend/js/features/source-editor/controllers/grammarly-advert-controller.js
  60. 0 6
      services/web/frontend/js/features/source-editor/controllers/source-editor-controller.ts
  61. 7 13
      services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts
  62. 0 1
      services/web/frontend/js/features/source-editor/ide/index.js
  63. 0 422
      services/web/frontend/js/ide.js
  64. 0 37
      services/web/frontend/js/ide/LoadingManager.js
  65. 0 73
      services/web/frontend/js/ide/binary-files/BinaryFilesManager.js
  66. 0 6
      services/web/frontend/js/ide/chat/index.js
  67. 0 59
      services/web/frontend/js/ide/cobranding/CobrandingDataService.js
  68. 0 630
      services/web/frontend/js/ide/connection/ConnectionManager.js
  69. 0 236
      services/web/frontend/js/ide/connection/EditorWatchdogManager.js
  70. 0 99
      services/web/frontend/js/ide/directives/SafePath.js
  71. 0 323
      services/web/frontend/js/ide/directives/layout.js
  72. 0 139
      services/web/frontend/js/ide/directives/verticalResizablePanes.js
  73. 0 781
      services/web/frontend/js/ide/editor/Document.js
  74. 0 510
      services/web/frontend/js/ide/editor/EditorManager.js
  75. 0 50
      services/web/frontend/js/ide/editor/EditorShareJsCodec.js
  76. 0 471
      services/web/frontend/js/ide/editor/ShareJsDoc.js
  77. 0 9
      services/web/frontend/js/ide/editor/controllers/CompileButton.js
  78. 0 103
      services/web/frontend/js/ide/editor/controllers/SavingNotificationController.js
  79. 0 9
      services/web/frontend/js/ide/editor/controllers/SwitchToPDFButton.js
  80. 0 19
      services/web/frontend/js/ide/editor/directives/formattingButtons.js
  81. 0 38
      services/web/frontend/js/ide/editor/directives/toggleSwitch.js
  82. 0 740
      services/web/frontend/js/ide/file-tree/FileTreeManager.js
  83. 0 18
      services/web/frontend/js/ide/file-tree/controllers/FileTreeController.js
  84. 0 1
      services/web/frontend/js/ide/file-view/index.js
  85. 0 28
      services/web/frontend/js/ide/metadata/MetadataManager.js
  86. 0 129
      services/web/frontend/js/ide/metadata/services/metadata.js
  87. 0 176
      services/web/frontend/js/ide/online-users/OnlineUsersManager.js
  88. 0 49
      services/web/frontend/js/ide/permissions/PermissionsManager.js
  89. 0 110
      services/web/frontend/js/ide/references/ReferencesManager.js
  90. 0 1
      services/web/frontend/js/ide/review-panel/ReviewPanelManager.js
  91. 0 1349
      services/web/frontend/js/ide/review-panel/controllers/ReviewPanelController.js
  92. 0 181
      services/web/frontend/js/ide/services/ide.js
  93. 0 11
      services/web/frontend/js/ide/toolbar/EditorLoaderController.js
  94. 0 1
      services/web/frontend/js/ide/toolbar/index.js
  95. 0 13
      services/web/frontend/js/libraries.js
  96. 0 32
      services/web/frontend/js/main.js
  97. 0 129
      services/web/frontend/js/main/event.js
  98. 0 21
      services/web/frontend/js/main/importing.js
  99. 0 63
      services/web/frontend/js/main/system-messages.js
  100. 0 92
      services/web/frontend/js/main/token-access.js

+ 0 - 224
package-lock.json

@@ -12243,12 +12243,6 @@
         "@types/node": "*"
       }
     },
-    "node_modules/@types/angular": {
-      "version": "1.8.4",
-      "resolved": "https://registry.npmjs.org/@types/angular/-/angular-1.8.4.tgz",
-      "integrity": "sha512-wPS/ncJWhyxJsndsW1B6Ta8D4mi97x1yItSu+rkLDytU3oRIh2CFAjMuJceYwFAh9+DIohndWM0QBA9OU2Hv0g==",
-      "dev": true
-    },
     "node_modules/@types/aria-query": {
       "version": "4.2.2",
       "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz",
@@ -12691,15 +12685,6 @@
       "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==",
       "dev": true
     },
-    "node_modules/@types/lodash.frompairs": {
-      "version": "4.0.6",
-      "resolved": "https://registry.npmjs.org/@types/lodash.frompairs/-/lodash.frompairs-4.0.6.tgz",
-      "integrity": "sha512-rwCUf4NMKhXpiVjL/RXP8YOk+rd02/J4tACADEgaMXRVnzDbSSlBMKFZoX/ARmHVLg3Qc98Um4PErGv8FbxU7w==",
-      "dev": true,
-      "dependencies": {
-        "@types/lodash": "*"
-      }
-    },
     "node_modules/@types/long": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz",
@@ -14733,25 +14718,6 @@
       "integrity": "sha512-VJK1SRmXBpjwsB4YOHYSturx48rLKMzHgCqDH2ZDa6ZbMS/N5huoNqyQdK5Fj/xayu3fqbXckn5SeCS1EbMDZg==",
       "dev": true
     },
-    "node_modules/angular": {
-      "version": "1.8.3",
-      "resolved": "https://registry.npmjs.org/angular/-/angular-1.8.3.tgz",
-      "integrity": "sha512-5qjkWIQQVsHj4Sb5TcEs4WZWpFeVFHXwxEBHUhrny41D8UrBAd6T/6nPPAsLngJCReIOqi95W3mxdveveutpZw==",
-      "deprecated": "For the actively supported Angular, see https://www.npmjs.com/package/@angular/core. AngularJS support has officially ended. For extended AngularJS support options, see https://goo.gle/angularjs-path-forward.",
-      "dev": true
-    },
-    "node_modules/angular-mocks": {
-      "version": "1.8.2",
-      "resolved": "https://registry.npmjs.org/angular-mocks/-/angular-mocks-1.8.2.tgz",
-      "integrity": "sha512-I5L3P0l21HPdVsP4A4qWmENt4ePjjbkDFdAzOaM7QiibFySbt14DptPbt2IjeG4vFBr4vSLbhIz8Fk03DISl8Q==",
-      "dev": true
-    },
-    "node_modules/angular-sanitize": {
-      "version": "1.8.2",
-      "resolved": "https://registry.npmjs.org/angular-sanitize/-/angular-sanitize-1.8.2.tgz",
-      "integrity": "sha512-OB6Goa+QN3byf5asQ7XRl7DKZejm/F/ZOqa9z1skqYVOWA2hoBxoCmt9E7+i7T/TbxZP5zYzKxNZVVJNu860Hg==",
-      "dev": true
-    },
     "node_modules/ansi-color": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/ansi-color/-/ansi-color-0.2.1.tgz",
@@ -15617,17 +15583,6 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/babel-plugin-angularjs-annotate": {
-      "version": "0.10.0",
-      "resolved": "https://registry.npmjs.org/babel-plugin-angularjs-annotate/-/babel-plugin-angularjs-annotate-0.10.0.tgz",
-      "integrity": "sha512-NPE7FOAxcLPCUR/kNkrhHIjoScR3RyIlRH3yRn79j8EZWtpILVnCOdA9yKfsOmRh6BHnLHKl8ZAThc+YDd/QwQ==",
-      "dev": true,
-      "dependencies": {
-        "@babel/code-frame": "^7.0.0",
-        "@babel/types": "^7.2.0",
-        "simple-is": "~0.2.0"
-      }
-    },
     "node_modules/babel-plugin-macros": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
@@ -27997,12 +27952,6 @@
         "lodash.keys": "~2.4.1"
       }
     },
-    "node_modules/lodash.frompairs": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/lodash.frompairs/-/lodash.frompairs-4.0.1.tgz",
-      "integrity": "sha1-vE5SB/onV8E25XNhTpZkUGsrG9I=",
-      "dev": true
-    },
     "node_modules/lodash.get": {
       "version": "4.4.2",
       "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
@@ -30011,18 +29960,6 @@
       "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
       "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
     },
-    "node_modules/ngcomponent": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/ngcomponent/-/ngcomponent-4.1.0.tgz",
-      "integrity": "sha512-cGL3iVoqMWTpCfaIwgRKhdaGqiy2Z+CCG0cVfjlBvdqE8saj8xap9B4OTf+qwObxLVZmDTJPDgx3bN6Q/lZ7BQ==",
-      "dev": true,
-      "dependencies": {
-        "@types/angular": "^1.6.39",
-        "@types/lodash": "^4.14.85",
-        "angular": ">=1.5.0",
-        "lodash": "^4.17.4"
-      }
-    },
     "node_modules/nise": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz",
@@ -34807,52 +34744,6 @@
         "react-dom": ">=15.0.0"
       }
     },
-    "node_modules/react2angular": {
-      "version": "4.0.6",
-      "resolved": "https://registry.npmjs.org/react2angular/-/react2angular-4.0.6.tgz",
-      "integrity": "sha512-MDl2WRoTyu7Gyh4+FAIlmsM2mxIa/DjSz6G/d90L1tK8ZRubqVEayKF6IPyAruC5DMhGDVJ7tlAIcu/gMNDjXg==",
-      "dev": true,
-      "dependencies": {
-        "@types/lodash.frompairs": "^4.0.5",
-        "angular": ">=1.5",
-        "lodash.frompairs": "^4.0.1",
-        "ngcomponent": "^4.1.0"
-      },
-      "peerDependencies": {
-        "@types/angular": ">=1.5",
-        "@types/prop-types": ">=15",
-        "@types/react": ">=16",
-        "@types/react-dom": ">=16",
-        "prop-types": ">=15",
-        "react": ">=15",
-        "react-dom": ">=15"
-      }
-    },
-    "node_modules/react2angular-shared-context": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/react2angular-shared-context/-/react2angular-shared-context-1.1.2.tgz",
-      "integrity": "sha512-0zrxBjmBs+et5zYNknx/jvrJCzGz6KbF8BHfzXHTl9ms6iMsbmmXkZiQQksVT1Og5wnkmVq9nlLVfWYJLSXF0w==",
-      "dev": true,
-      "dependencies": {
-        "uuid": "^8.3.2"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "peerDependencies": {
-        "react": "^16.0.0 || ^17",
-        "react-dom": "^16.0.0 || ^17"
-      }
-    },
-    "node_modules/react2angular-shared-context/node_modules/uuid": {
-      "version": "8.3.2",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
-      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
-      "dev": true,
-      "bin": {
-        "uuid": "dist/bin/uuid"
-      }
-    },
     "node_modules/reactcss": {
       "version": "1.2.3",
       "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz",
@@ -36673,12 +36564,6 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/simple-is": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/simple-is/-/simple-is-0.2.0.tgz",
-      "integrity": "sha1-Krt1qt453rXMgVzhDmGRFkhQuvA=",
-      "dev": true
-    },
     "node_modules/simple-oauth2": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/simple-oauth2/-/simple-oauth2-5.0.0.tgz",
@@ -44616,12 +44501,8 @@
         "acorn": "^7.1.1",
         "acorn-walk": "^7.1.1",
         "algoliasearch": "^3.35.1",
-        "angular": "~1.8.0",
-        "angular-mocks": "~1.8.0",
-        "angular-sanitize": "~1.8.0",
         "autoprefixer": "^10.4.16",
         "babel-loader": "^9.1.2",
-        "babel-plugin-angularjs-annotate": "^0.10.0",
         "babel-plugin-macros": "^3.1.0",
         "babel-plugin-module-resolver": "^5.0.0",
         "backbone": "^1.3.3",
@@ -44698,8 +44579,6 @@
         "react-linkify": "^1.0.0-alpha",
         "react-refresh": "^0.14.0",
         "react-resizable-panels": "^1.0.3",
-        "react2angular": "^4.0.6",
-        "react2angular-shared-context": "^1.1.0",
         "requirejs": "^2.3.6",
         "resolve-url-loader": "^5.0.0",
         "samlp": "^7.0.2",
@@ -53003,14 +52882,10 @@
         "acorn": "^7.1.1",
         "acorn-walk": "^7.1.1",
         "algoliasearch": "^3.35.1",
-        "angular": "~1.8.0",
-        "angular-mocks": "~1.8.0",
-        "angular-sanitize": "~1.8.0",
         "archiver": "^5.3.0",
         "async": "3.2.2",
         "autoprefixer": "^10.4.16",
         "babel-loader": "^9.1.2",
-        "babel-plugin-angularjs-annotate": "^0.10.0",
         "babel-plugin-macros": "^3.1.0",
         "babel-plugin-module-resolver": "^5.0.0",
         "backbone": "^1.3.3",
@@ -53152,8 +53027,6 @@
         "react-linkify": "^1.0.0-alpha",
         "react-refresh": "^0.14.0",
         "react-resizable-panels": "^1.0.3",
-        "react2angular": "^4.0.6",
-        "react2angular-shared-context": "^1.1.0",
         "recurly": "^4.0.0",
         "referer-parser": "github:overleaf/nodejs-referer-parser#8b8b103762d05b7be4cfa2f810e1d408be67d7bb",
         "request": "^2.88.2",
@@ -57040,12 +56913,6 @@
         "@types/node": "*"
       }
     },
-    "@types/angular": {
-      "version": "1.8.4",
-      "resolved": "https://registry.npmjs.org/@types/angular/-/angular-1.8.4.tgz",
-      "integrity": "sha512-wPS/ncJWhyxJsndsW1B6Ta8D4mi97x1yItSu+rkLDytU3oRIh2CFAjMuJceYwFAh9+DIohndWM0QBA9OU2Hv0g==",
-      "dev": true
-    },
     "@types/aria-query": {
       "version": "4.2.2",
       "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz",
@@ -57487,15 +57354,6 @@
       "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==",
       "dev": true
     },
-    "@types/lodash.frompairs": {
-      "version": "4.0.6",
-      "resolved": "https://registry.npmjs.org/@types/lodash.frompairs/-/lodash.frompairs-4.0.6.tgz",
-      "integrity": "sha512-rwCUf4NMKhXpiVjL/RXP8YOk+rd02/J4tACADEgaMXRVnzDbSSlBMKFZoX/ARmHVLg3Qc98Um4PErGv8FbxU7w==",
-      "dev": true,
-      "requires": {
-        "@types/lodash": "*"
-      }
-    },
     "@types/long": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz",
@@ -59081,24 +58939,6 @@
         }
       }
     },
-    "angular": {
-      "version": "1.8.3",
-      "resolved": "https://registry.npmjs.org/angular/-/angular-1.8.3.tgz",
-      "integrity": "sha512-5qjkWIQQVsHj4Sb5TcEs4WZWpFeVFHXwxEBHUhrny41D8UrBAd6T/6nPPAsLngJCReIOqi95W3mxdveveutpZw==",
-      "dev": true
-    },
-    "angular-mocks": {
-      "version": "1.8.2",
-      "resolved": "https://registry.npmjs.org/angular-mocks/-/angular-mocks-1.8.2.tgz",
-      "integrity": "sha512-I5L3P0l21HPdVsP4A4qWmENt4ePjjbkDFdAzOaM7QiibFySbt14DptPbt2IjeG4vFBr4vSLbhIz8Fk03DISl8Q==",
-      "dev": true
-    },
-    "angular-sanitize": {
-      "version": "1.8.2",
-      "resolved": "https://registry.npmjs.org/angular-sanitize/-/angular-sanitize-1.8.2.tgz",
-      "integrity": "sha512-OB6Goa+QN3byf5asQ7XRl7DKZejm/F/ZOqa9z1skqYVOWA2hoBxoCmt9E7+i7T/TbxZP5zYzKxNZVVJNu860Hg==",
-      "dev": true
-    },
     "ansi-color": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/ansi-color/-/ansi-color-0.2.1.tgz",
@@ -59740,17 +59580,6 @@
         }
       }
     },
-    "babel-plugin-angularjs-annotate": {
-      "version": "0.10.0",
-      "resolved": "https://registry.npmjs.org/babel-plugin-angularjs-annotate/-/babel-plugin-angularjs-annotate-0.10.0.tgz",
-      "integrity": "sha512-NPE7FOAxcLPCUR/kNkrhHIjoScR3RyIlRH3yRn79j8EZWtpILVnCOdA9yKfsOmRh6BHnLHKl8ZAThc+YDd/QwQ==",
-      "dev": true,
-      "requires": {
-        "@babel/code-frame": "^7.0.0",
-        "@babel/types": "^7.2.0",
-        "simple-is": "~0.2.0"
-      }
-    },
     "babel-plugin-macros": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
@@ -70105,12 +69934,6 @@
         "lodash.keys": "~2.4.1"
       }
     },
-    "lodash.frompairs": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/lodash.frompairs/-/lodash.frompairs-4.0.1.tgz",
-      "integrity": "sha1-vE5SB/onV8E25XNhTpZkUGsrG9I=",
-      "dev": true
-    },
     "lodash.get": {
       "version": "4.4.2",
       "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
@@ -71596,18 +71419,6 @@
       "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
       "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
     },
-    "ngcomponent": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/ngcomponent/-/ngcomponent-4.1.0.tgz",
-      "integrity": "sha512-cGL3iVoqMWTpCfaIwgRKhdaGqiy2Z+CCG0cVfjlBvdqE8saj8xap9B4OTf+qwObxLVZmDTJPDgx3bN6Q/lZ7BQ==",
-      "dev": true,
-      "requires": {
-        "@types/angular": "^1.6.39",
-        "@types/lodash": "^4.14.85",
-        "angular": ">=1.5.0",
-        "lodash": "^4.17.4"
-      }
-    },
     "nise": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz",
@@ -75149,35 +74960,6 @@
         "react-lifecycles-compat": "^3.0.4"
       }
     },
-    "react2angular": {
-      "version": "4.0.6",
-      "resolved": "https://registry.npmjs.org/react2angular/-/react2angular-4.0.6.tgz",
-      "integrity": "sha512-MDl2WRoTyu7Gyh4+FAIlmsM2mxIa/DjSz6G/d90L1tK8ZRubqVEayKF6IPyAruC5DMhGDVJ7tlAIcu/gMNDjXg==",
-      "dev": true,
-      "requires": {
-        "@types/lodash.frompairs": "^4.0.5",
-        "angular": ">=1.5",
-        "lodash.frompairs": "^4.0.1",
-        "ngcomponent": "^4.1.0"
-      }
-    },
-    "react2angular-shared-context": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/react2angular-shared-context/-/react2angular-shared-context-1.1.2.tgz",
-      "integrity": "sha512-0zrxBjmBs+et5zYNknx/jvrJCzGz6KbF8BHfzXHTl9ms6iMsbmmXkZiQQksVT1Og5wnkmVq9nlLVfWYJLSXF0w==",
-      "dev": true,
-      "requires": {
-        "uuid": "^8.3.2"
-      },
-      "dependencies": {
-        "uuid": {
-          "version": "8.3.2",
-          "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
-          "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
-          "dev": true
-        }
-      }
-    },
     "reactcss": {
       "version": "1.2.3",
       "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz",
@@ -76578,12 +76360,6 @@
         }
       }
     },
-    "simple-is": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/simple-is/-/simple-is-0.2.0.tgz",
-      "integrity": "sha1-Krt1qt453rXMgVzhDmGRFkhQuvA=",
-      "dev": true
-    },
     "simple-oauth2": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/simple-oauth2/-/simple-oauth2-5.0.0.tgz",

+ 0 - 9
patches/ngcomponent+4.1.0.patch

@@ -1,9 +0,0 @@
-diff --git a/node_modules/ngcomponent/index.ts b/node_modules/ngcomponent/index.ts
-index 5fe33c5..8e1c6fc 100644
---- a/node_modules/ngcomponent/index.ts
-+++ b/node_modules/ngcomponent/index.ts
-@@ -1,3 +1,4 @@
-+// @ts-nocheck
- import { IChangesObject } from 'angular'
- import assign = require('lodash/assign')
- import mapValues = require('lodash/mapValues')

+ 0 - 9
patches/react2angular+4.0.6.patch

@@ -1,9 +0,0 @@
-diff --git a/node_modules/react2angular/index.tsx b/node_modules/react2angular/index.tsx
-index 5cee831..a07e040 100644
---- a/node_modules/react2angular/index.tsx
-+++ b/node_modules/react2angular/index.tsx
-@@ -1,3 +1,4 @@
-+// @ts-nocheck
- import { IAugmentedJQuery, IComponentOptions } from 'angular'
- import fromPairs = require('lodash.frompairs')
- import NgComponent from 'ngcomponent'

+ 2 - 8
services/web/app/src/Features/Project/ProjectController.js

@@ -603,15 +603,10 @@ const _ProjectController = {
         !showPersonalAccessToken &&
         splitTestAssignments['personal-access-token'].variant === 'enabled' // `?personal-access-token=enabled`
 
-      const idePageReact = splitTestAssignments['ide-page'].variant === 'react'
-
       const template =
         detachRole === 'detached'
-          ? // TODO: Create React version of detached page
-            'project/editor_detached'
-          : idePageReact
-            ? 'project/ide-react'
-            : 'project/editor'
+          ? 'project/ide-react-detached'
+          : 'project/ide-react'
 
       res.render(template, {
         title: project.name,
@@ -681,7 +676,6 @@ const _ProjectController = {
         showUpgradePrompt,
         fixedSizeDocument: true,
         useOpenTelemetry: Settings.useOpenTelemetryClient,
-        idePageReact,
         showPersonalAccessToken,
         optionalPersonalAccessToken,
         hasTrackChangesFeature: Features.hasFeature('track-changes'),

+ 1 - 13
services/web/app/src/Features/TokenAccess/TokenAccessController.js

@@ -12,7 +12,6 @@ const {
   handleAdminDomainRedirect,
 } = require('../Authorization/AuthorizationMiddleware')
 const ProjectAuditLogHandler = require('../Project/ProjectAuditLogHandler')
-const SplitTestHandler = require('../SplitTests/SplitTestHandler')
 
 const orderedPrivilegeLevels = [
   PrivilegeLevels.NONE,
@@ -98,18 +97,7 @@ async function tokenAccessPage(req, res, next) {
       }
     }
 
-    const { variant } = await SplitTestHandler.promises.getAssignment(
-      req,
-      res,
-      'token-access-page'
-    )
-
-    const view =
-      variant === 'react'
-        ? 'project/token/access-react'
-        : 'project/token/access'
-
-    res.render(view, {
+    res.render('project/token/access-react', {
       postUrl: makePostUrl(token),
     })
   } catch (err) {

+ 1 - 1
services/web/app/views/layout-base.pug

@@ -72,7 +72,7 @@ html(
 
 		block head-scripts
 
-	body(ng-csp=(cspEnabled ? "no-unsafe-eval" : false) class=(showThinFooter ? 'thin-footer' : undefined))
+	body(class=(showThinFooter ? 'thin-footer' : undefined))
 		if(settings.recaptcha && settings.recaptcha.siteKeyV3)
 			script(type="text/javascript", nonce=scriptNonce, src="https://www.recaptcha.net/recaptcha/api.js?render="+settings.recaptcha.siteKeyV3, defer=deferScripts)
 

+ 0 - 21
services/web/app/views/layout.pug

@@ -1,21 +0,0 @@
-extends ./layout-base
-
-block entrypointVar
-	- entrypoint = 'main'
-
-block body
-	if (typeof(suppressNavbar) == "undefined")
-		include layout/navbar
-
-	block content
-
-	if (typeof(suppressFooter) == "undefined")
-		if showThinFooter
-			include layout/footer
-		else
-			include layout/fat-footer
-
-	if (typeof(suppressCookieBanner) == 'undefined')
-		include _cookie_banner
-
-	!= moduleIncludes("contactModal", locals)

+ 0 - 42
services/web/app/views/layout/footer.pug

@@ -1,42 +0,0 @@
-
-
-footer.site-footer
-	- var showLanguagePicker = Object.keys(settings.i18n.subdomainLang).length > 1
-	- var hasCustomLeftNav = nav.left_footer && nav.left_footer.length > 0
-	.site-footer-content.hidden-print
-		.row
-			ul.col-md-9
-				if hasFeature('saas')
-					li © #{new Date().getFullYear()} Overleaf
-				else if !settings.nav.hide_powered_by
-					li
-						//- year of Server Pro release, static
-						| © 2024
-						|
-						a(href='https://www.overleaf.com/for/enterprises') Powered by Overleaf
-
-					if showLanguagePicker || hasCustomLeftNav
-						li
-							strong.text-muted |
-
-				if showLanguagePicker
-					include language-picker
-
-				if showLanguagePicker && hasCustomLeftNav
-					li
-						strong.text-muted |
-
-				each item in nav.left_footer
-					li
-						if item.url
-							a(href=item.url, class=item.class) !{translate(item.text)}
-						else
-							| !{item.text}
-
-			ul.col-md-3.text-right
-				each item in nav.right_footer
-					li
-						if item.url
-							a(href=item.url, class=item.class, aria-label=item.label) !{item.text}
-						else
-							| !{item.text}

+ 0 - 147
services/web/app/views/layout/navbar.pug

@@ -1,147 +0,0 @@
-nav.navbar.navbar-default.navbar-main
-	.container-fluid
-		.navbar-header
-			if (typeof(suppressNavbarRight) == "undefined")
-				button.navbar-toggle(ng-init="navCollapsed = true", ng-click="navCollapsed = !navCollapsed", ng-class="{active: !navCollapsed}", aria-label="Toggle " + translate('navigation'))
-					i.fa.fa-bars(aria-hidden="true")
-			if settings.nav.custom_logo
-				a(href='/', aria-label=settings.appName, style='background-image:url("'+settings.nav.custom_logo+'")').navbar-brand
-			else if (nav.title)
-				a(href='/', aria-label=settings.appName, ng-non-bindable).navbar-title #{nav.title}
-			else
-				a(href='/', aria-label=settings.appName).navbar-brand
-
-		- var canDisplayAdminMenu = hasAdminAccess()
-		- var canDisplayAdminRedirect = canRedirectToAdminDomain()
-		- var canDisplaySplitTestMenu = hasFeature('saas') && (canDisplayAdminMenu || (getSessionUser() && getSessionUser().staffAccess && (getSessionUser().staffAccess.splitTestMetrics || getSessionUser().staffAccess.splitTestManagement)))
-		- var canDisplaySurveyMenu = hasFeature('saas') && canDisplayAdminMenu
-
-		if (typeof(suppressNavbarRight) == "undefined")
-			.navbar-collapse.collapse(collapse="navCollapsed")
-				ul.nav.navbar-nav.navbar-right
-					if (canDisplayAdminMenu || canDisplayAdminRedirect || canDisplaySplitTestMenu)
-						li.dropdown(class="subdued", dropdown)
-							a.dropdown-toggle(href, dropdown-toggle)
-								| Admin
-								b.caret
-							ul.dropdown-menu
-								if canDisplayAdminMenu
-									li
-										a(href="/admin") Manage Site
-									li
-										a(href="/admin/user") Manage Users
-									li
-										a(href="/admin/project") Project URL Lookup
-									li
-										a(href="/admin/saml/logs") SAML logs
-								if canDisplayAdminRedirect
-									li
-										a(href=settings.adminUrl) Switch to Admin
-								if canDisplaySplitTestMenu
-									li
-										a(href="/admin/split-test") Manage Feature Flags
-								if canDisplaySurveyMenu
-									li
-										a(href="/admin/survey") Manage Surveys
-
-					// loop over header_extras
-					each item in nav.header_extras
-						-
-							if ((item.only_when_logged_in && getSessionUser())
-								|| (item.only_when_logged_out && (!getSessionUser()))
-								|| (!item.only_when_logged_out && !item.only_when_logged_in && !item.only_content_pages)
-								|| (item.only_content_pages && (typeof(suppressNavContentLinks) == "undefined" || !suppressNavContentLinks))
-							){ 
-								var showNavItem = true
-							} else {
-								var showNavItem = false
-							}
-
-						if showNavItem
-							if item.dropdown
-								li.dropdown(class=item.class, dropdown)
-									a.dropdown-toggle(href, dropdown-toggle)
-										| !{translate(item.text)}
-										b.caret
-									ul.dropdown-menu
-										each child in item.dropdown
-											if child.divider
-												li.divider
-											else if child.isContactUs
-												li
-													a(ng-controller="ContactModal" ng-click="contactUsModal()" href)
-														span(event-tracking="menu-clicked-contact" event-tracking-mb="true" event-tracking-trigger="click")
-															| #{translate("contact_us")}
-											else
-												li
-													if child.url
-														a(
-															href=child.url,
-															class=child.class,
-															event-tracking=child.event
-															event-tracking-mb="true"
-															event-tracking-trigger="click"
-															event-segmentation=child.eventSegmentation
-														) !{translate(child.text)}
-													else
-														| !{translate(child.text)}
-							else
-								li(class=item.class)
-									if item.url
-										a(
-											href=item.url,
-											class=item.class,
-											event-tracking=item.event
-											event-tracking-mb="true"
-											event-tracking-trigger="click"
-										) !{translate(item.text)}
-									else
-										| !{translate(item.text)}
-
-					// logged out
-					if !getSessionUser()
-						// register link
-						if hasFeature('registration-page')
-							li
-								a(
-									href="/register"
-									event-tracking="menu-clicked-register"
-									event-tracking-action="clicked"
-									event-tracking-trigger="click"
-									event-tracking-mb="true"
-									event-segmentation={ page: currentUrl }
-								) #{translate('register')}
-
-						// login link
-						li
-							a(
-								href="/login"
-								event-tracking="menu-clicked-login"
-								event-tracking-action="clicked"
-								event-tracking-trigger="click"
-								event-tracking-mb="true"
-								event-segmentation={ page: currentUrl }
-							).text-capitalize #{translate('log_in')}
-
-					// projects link and account menu
-					if getSessionUser()
-						li
-							a(href="/project") #{translate('Projects')}
-						li.dropdown(dropdown)
-							a.dropdown-toggle(href, dropdown-toggle)
-								| #{translate('Account')}
-								b.caret
-							ul.dropdown-menu
-								li
-									div.subdued {{ usersEmail }}
-								li.divider.hidden-xs.hidden-sm
-								li
-									a(href="/user/settings") #{translate('Account Settings')}
-								if nav.showSubscriptionLink
-									li
-										a(href="/user/subscription") #{translate('subscription')}
-								li.divider.hidden-xs.hidden-sm
-								li
-									form(method="POST" action="/logout")
-										input(name='_csrf', type='hidden', value=csrfToken)
-										button.btn-link.text-left.dropdown-menu-button #{translate('log_out')}

+ 0 - 124
services/web/app/views/project/editor.pug

@@ -1,124 +0,0 @@
-extends ../layout
-
-block vars
-	- var suppressNavbar = true
-	- var suppressFooter = true
-	- var suppressSkipToContent = true
-	- var suppressCookieBanner = true
-	- metadata.robotsNoindexNofollow = true
-
-block entrypointVar
-	- entrypoint = 'ide'
-
-block content
-	.editor(ng-controller="IdeController").full-size
-		//- required by react2angular-shared-context, must be rendered as a top level component
-		shared-context-react()
-		.loading-screen(ng-if="state.loading")
-			.loading-screen-brand-container
-				.loading-screen-brand(
-					style="height: 20%;"
-					ng-style="{ 'height': state.load_progress + '%' }"
-				)
-			h3.loading-screen-label(ng-if="!state.error") #{translate("loading")}
-				span.loading-screen-ellip .
-				span.loading-screen-ellip .
-				span.loading-screen-ellip .
-			p.loading-screen-error(ng-if="state.error").ng-cloak
-				span(ng-bind-html="state.error")
-
-		.global-alerts(ng-cloak ng-hide="editor.error_state")
-			.alert.alert-danger.small(ng-if="connection.forced_disconnect")
-				strong #{translate("disconnected")}
-
-			.alert.alert-warning.small(ng-if="connection.reconnection_countdown")
-				strong #{translate("lost_connection")}.
-				|  #{translate("reconnecting_in_x_secs", {seconds:"{{ connection.reconnection_countdown }}"})}.
-				a#try-reconnect-now-button.alert-link-as-btn.pull-right(href, ng-click="tryReconnectNow()") #{translate("try_now")}
-
-			.alert.alert-warning.small(ng-if="connection.reconnecting && connection.stillReconnecting")
-				strong #{translate("reconnecting")}…
-
-			.alert.alert-warning.small(ng-if="sync_tex_error")
-				strong #{translate("synctex_failed")}.
-				a#synctex-more-info-button.alert-link-as-btn.pull-right(
-					href="/learn/how-to/SyncTeX_Errors"
-					target="_blank"
-				) #{translate("more_info")}
-
-			.alert.alert-warning.small(ng-if="connection.inactive_disconnect")
-				strong #{translate("editor_disconected_click_to_reconnect")}
-
-			.alert.alert-warning.small(ng-if="connection.debug") {{ connection.state }}
-
-			.div(ng-controller="SavingNotificationController")
-				.alert.alert-warning.small(ng-repeat="(doc_id, state) in docSavingStatus" ng-if="state.unsavedSeconds > 8") #{translate("saving_notification_with_seconds", {docname:"{{ state.doc.name }}", seconds:"{{ state.unsavedSeconds }}"})}
-
-			.div(ng-controller="SystemMessagesController")
-				.alert.alert-warning.system-message(
-					ng-repeat="message in messages"
-					ng-controller="SystemMessageController"
-					ng-hide="hidden"
-				)
-					button(ng-hide="protected" ng-click="hide()").close.pull-right
-						span(aria-hidden="true") ×
-						span.sr-only #{translate("close")}
-					.system-message-content
-						| {{htmlContent}}
-
-			if hasFeature('saas')
-				legacy-editor-warning(delay=10000)
-
-		include ./editor/main
-
-	script(type="text/ng-template" id="genericMessageModalTemplate")
-		.modal-header
-			button.close(
-				type="button"
-				data-dismiss="modal"
-				ng-click="done()"
-				aria-label="Close"
-			)
-				span(aria-hidden="true") ×
-			h3 {{ title }}
-		.modal-body(ng-bind-html="message")
-		.modal-footer
-			button.btn.btn-info(ng-click="done()") #{translate("ok")}
-
-	script(type="text/ng-template" id="outOfSyncModalTemplate")
-		.modal-header
-			button.close(
-				type="button"
-				data-dismiss="modal"
-				ng-click="done()"
-				aria-label="Close"
-			)
-				span(aria-hidden="true") ×
-			h3 {{ title }}
-		.modal-body(ng-bind-html="message")
-
-		.modal-body
-			button.btn.btn-info(
-				ng-init="showFileContents = false"
-				ng-click="showFileContents = !showFileContents"
-			)
-				| {{showFileContents ? "Hide" : "Show"}} Local File Contents
-			.text-preview(ng-show="showFileContents")
-				textarea.scroll-container(readonly="readonly" rows="{{editorContentRows}}")
-					| {{editorContent}}
-
-		.modal-footer
-			button.btn.btn-info(ng-click="done()") #{translate("reload_editor")}
-
-	script(type="text/ng-template" id="lockEditorModalTemplate")
-		.modal-header
-			h3 {{ title }}
-		.modal-body(ng-bind-html="message")
-
-block append meta
-	include ./editor/meta
-
-block prepend foot-scripts
-	each file in (useOpenTelemetry ? entrypointScripts("tracing") : [])
-		script(type="text/javascript", nonce=scriptNonce, src=file)
-	script(type="text/javascript", nonce=scriptNonce, src=(wsUrl || '/socket.io') + '/socket.io.js')

+ 0 - 52
services/web/app/views/project/editor/editor-pane.pug

@@ -1,52 +0,0 @@
-.ui-layout-center(
-		ng-controller="ReviewPanelController",
-		ng-class="{\
-			'rp-state-current-file': (reviewPanel.subView === SubViews.CUR_FILE),\
-			'rp-state-current-file-expanded': (reviewPanel.subView === SubViews.CUR_FILE && ui.reviewPanelOpen),\
-			'rp-state-current-file-mini': (reviewPanel.subView === SubViews.CUR_FILE && !ui.reviewPanelOpen),\
-			'rp-state-overview': (reviewPanel.subView === SubViews.OVERVIEW),\
-			'rp-size-mini': ui.miniReviewPanelVisible,\
-			'rp-size-expanded': ui.reviewPanelOpen,\
-			'rp-layout-left': reviewPanel.layoutToLeft,\
-			'rp-loading-threads': loadingThreads,\
-		}"
-	)
-
-		.multi-selection-ongoing(
-			ng-show="editor.multiSelectedCount > 0"
-		)
-			.multi-selection-message
-				h4 {{ editor.multiSelectedCount }} #{translate('files_selected')}
-
-		include ./file-view
-
-		.editor-container.full-size(
-			ng-show="ui.view == 'editor' && editor.multiSelectedCount === 0"
-			vertical-resizable-panes="south-pane-resizer"
-			vertical-resizable-panes-hidden-externally-on="south-pane-toggled"
-			vertical-resizable-panes-hidden-initially="true"
-			vertical-resizable-panes-default-size="250"
-			vertical-resizable-panes-min-size="250"
-			vertical-resizable-panes-max-size="336"
-			vertical-resizable-panes-resize-on="layout:flat-screen:toggle,south-pane-toggled"
-		)
-			.div(vertical-resizable-top)
-
-				.loading-panel(
-					ng-show="(!editor.sharejs_doc || editor.opening) && !editor.error_state",
-				)
-					span(ng-show="editor.open_doc_id")
-						i.fa.fa-spin.fa-refresh
-						|   #{translate("loading")}…
-					span(ng-show="!editor.open_doc_id")
-						i.fa.fa-arrow-left
-						|   #{translate("open_a_file_on_the_left")}
-
-				div(ng-controller="EditorLoaderController")
-
-				include ../../source-editor/source-editor
-
-			if moduleIncludesAvailable('editor:symbol-palette')
-				.div(vertical-resizable-bottom)
-					if moduleIncludesAvailable('editor:symbol-palette')
-						!= moduleIncludes('editor:symbol-palette', locals)

+ 0 - 53
services/web/app/views/project/editor/editor.pug

@@ -1,53 +0,0 @@
-div.full-size(
-	ng-show="ui.view == 'editor' || ui.view === 'file'"
-	layout="pdf"
-	open-east="ui.pdfOpen"
-	mask-iframes-on-resize="true"
-	resize-on="layout:main:resize"
-	resize-proportionally="true"
-	initial-size-east="'50%'"
-	minimum-restore-size-east="300"
-	allow-overflow-on="'center'"
-	custom-toggler-pane="east"
-	custom-toggler-msg-when-open=translate("tooltip_hide_pdf")
-	custom-toggler-msg-when-closed=translate("tooltip_show_pdf")
-)
-	include ./editor-pane
-
-	.ui-layout-east
-		div(ng-if="ui.pdfLayout == 'sideBySide'")
-			pdf-preview()
-
-	.ui-layout-resizer-controls.synctex-controls(
-		ng-show="settings.pdfViewer !== 'native'"
-	)
-		pdf-synctex-controls()
-
-div.full-size(
-	ng-if="ui.pdfLayout == 'flat'"
-	ng-show="ui.view == 'pdf'"
-)
-	pdf-preview()
-
-// fallback, shown when no file/view is selected
-div.full-size.no-file-selection(
-	ng-if="!ui.view"
-)
-	.no-file-selection-message(
-		ng-if="rootFolder.children && rootFolder.children.length > 0"
-	)
-		h3
-			| #{translate('no_selection_select_file')}
-	.no-file-selection-message(
-		ng-if="rootFolder.children && rootFolder.children.length === 0"
-	)
-		h3
-			| #{translate('no_selection_create_new_file')}
-		div(
-			ng-controller="FileTreeController"
-		)
-			button.btn.btn-primary(
-				ng-click="openNewDocModal()"
-			)
-				| #{translate('new_file')}
-

+ 0 - 3
services/web/app/views/project/editor/file-tree-history-react.pug

@@ -1,3 +0,0 @@
-aside.editor-sidebar.full-size.history-file-tree#history-file-tree(
-	ng-show="ui.view == 'history'"
-)

+ 0 - 27
services/web/app/views/project/editor/file-tree-react.pug

@@ -1,27 +0,0 @@
-aside.editor-sidebar.full-size(
-	ng-show="ui.view != 'history'"
-	vertical-resizable-panes="outline-resizer"
-	vertical-resizable-panes-toggled-externally-on="outline-toggled"
-	vertical-resizable-panes-default-size="350"
-	vertical-resizable-panes-min-size="32"
-	vertical-resizable-panes-max-size="'75%'"
-	vertical-resizable-panes-resize-on="left-pane-resize-all"
-)
-
-	div(
-		ng-controller="ReactFileTreeController"
-		vertical-resizable-top
-	)
-		file-tree-root(
-			on-select="onSelect"
-			on-init="onInit"
-			is-connected="isConnected"
-			ref-providers="refProviders"
-			reindex-references="reindexReferences"
-			set-ref-provider-enabled="setRefProviderEnabled"
-			set-started-free-trial="setStartedFreeTrial"
-		)
-
-	outline-container(
-		vertical-resizable-bottom
-	)

+ 0 - 9
services/web/app/views/project/editor/file-view.pug

@@ -1,9 +0,0 @@
-div(
-	ng-controller="FileViewController"
-	ng-show="ui.view == 'file'"
-	ng-if="openFile && editor.multiSelectedCount === 0"
-)
-	file-view(
-		file='file'
-		store-references-keys='storeReferencesKeys'
-	)

+ 0 - 12
services/web/app/views/project/editor/header-react.pug

@@ -1,12 +0,0 @@
-div(ng-controller="ReactShareProjectModalController")
-	share-project-modal(
-		handle-hide="handleHide"
-		show="show"
-	)
-		
-	div(ng-controller="EditorNavigationToolbarController")
-		editor-navigation-toolbar-root(
-			open-doc="openDoc"
-			online-users-array="onlineUsersArray"
-			open-share-project-modal="openShareProjectModal"
-		)

+ 0 - 1
services/web/app/views/project/editor/left-menu-react.pug

@@ -1 +0,0 @@
-editor-left-menu()

+ 0 - 39
services/web/app/views/project/editor/main.pug

@@ -1,39 +0,0 @@
-include ./left-menu-react
-
-#chat-wrapper.full-size(
-	layout="chat",
-	spacing-open="{{ui.chatResizerSizeOpen}}",
-	spacing-closed="{{ui.chatResizerSizeClosed}}",
-	ng-hide="state.loading",
-	ng-cloak
-)
-	.ui-layout-center
-		include ./header-react
-
-		main#ide-body(
-			ng-cloak,
-			role="main",
-			layout="main",
-			ng-hide="state.loading",
-			resize-on="layout:chat:resize,history:toggle,layout:flat-screen:toggle,south-pane-toggled",
-			minimum-restore-size-west="130"
-			custom-toggler-pane="west"
-			custom-toggler-msg-when-open=translate("tooltip_hide_filetree")
-		custom-toggler-msg-when-closed=translate("tooltip_show_filetree")
-		tabindex="0"
-			initial-size-east="250"
-			init-closed-east="true"
-			open-east="ui.chatOpen"
-		)
-			.ui-layout-west
-				include ./file-tree-react
-				include ./file-tree-history-react
-
-			.ui-layout-center
-				include ./editor
-				history-root()
-
-			if !isRestrictedTokenMember
-				.ui-layout-east
-					aside.chat
-						chat()

+ 0 - 1
services/web/app/views/project/editor/meta.pug

@@ -35,7 +35,6 @@ meta(name="ol-optionalPersonalAccessToken", data-type="boolean" content=optional
 meta(name="ol-hasTrackChangesFeature", data-type="boolean" content=hasTrackChangesFeature)
 meta(name="ol-inactiveTutorials", data-type="json" content=user.inactiveTutorials)
 meta(name="ol-projectTags" data-type="json" content=projectTags)
-meta(name="ol-idePageReact", data-type="boolean" content=idePageReact)
 meta(name="ol-loadingText", data-type="string" content=translate("loading"))
 meta(name="ol-translationLoadErrorMessage", data-type="string" content=translate("could_not_load_translations"))
 

+ 1 - 1
services/web/app/views/project/editor_detached.pug → services/web/app/views/project/ide-react-detached.pug

@@ -1,4 +1,4 @@
-extends ../layout
+extends ../layout-marketing
 
 block entrypointVar
 	- entrypoint = 'ide-detached'

+ 1 - 1
services/web/app/views/project/ide-react.pug

@@ -1,4 +1,4 @@
-extends ../layout
+extends ../layout-marketing
 
 block vars
 	- var suppressNavbar = true

+ 0 - 127
services/web/app/views/project/token/access.pug

@@ -1,127 +0,0 @@
-extends ../../layout
-
-block vars
-	- var suppressFooter = true
-	- var suppressCookieBanner = true
-	- var suppressSkipToContent = true
-
-block content
-
-	script(type="template", id="overleaf-token-access-data")!= StringHelper.stringifyJsonForScript({ postUrl: postUrl, csrfToken: csrfToken})
-
-	div(
-		ng-controller="TokenAccessPageController",
-		ng-init="post()"
-	)
-		.editor.full-size
-			div
-				|  
-				a(href="/project", style="font-size: 2rem; margin-left: 1rem; color: #ddd;")
-					i.fa.fa-arrow-left
-
-			.loading-screen(
-				ng-show="mode == 'accessAttempt'"
-			)
-				.loading-screen-brand-container
-					.loading-screen-brand()
-
-				h3.loading-screen-label.text-center
-					| #{translate('join_project')}
-					span(ng-show="accessInFlight == true")
-						span.loading-screen-ellip .
-						span.loading-screen-ellip .
-						span.loading-screen-ellip .
-
-
-				.global-alerts.text-center(ng-cloak)
-					div(ng-show="accessError", ng-cloak)
-						br
-						div(ng-switch="accessError", ng-cloak)
-							div(ng-switch-when="not_found")
-								h4(aria-live="assertive")
-									| Project not found
-
-							div(ng-switch-default)
-								.alert.alert-danger(aria-live="assertive") #{translate('token_access_failure')}
-								p
-									a(href="/") #{translate('home')}
-
-			.loading-screen(
-				ng-show="mode == 'v1Import'"
-			)
-				.container
-					.row
-						.col-sm-8.col-sm-offset-2
-							h1.text-center
-								span(ng-if="v1ImportData.status != 'mustLogin'") Overleaf v1 Project
-								span(ng-if="v1ImportData.status == 'mustLogin'") Please Log In
-							img.v2-import__img(
-								src="/img/v1-import/v2-editor.png"
-								alt="The new V2 editor."
-							)
-
-							div(ng-if="v1ImportData.status == 'cannotImport'")
-								h2.text-center
-									| Cannot Access Overleaf v1 Project
-								p.text-center.row-spaced-small
-									| Please contact the project owner or
-									|
-									a(href="/contact") contact support
-									|
-									| for assistance.
-
-							div(ng-if="v1ImportData.status == 'mustLogin'")
-								p.text-center.row-spaced-small
-									| You will need to log in to access this project.
-
-								.row-spaced.text-center
-									a.btn.btn-primary(
-										href="/login?redir={{ currentPath() }}"
-									) Log In To Access Project
-
-							div(ng-if="v1ImportData.status == 'canDownloadZip'")
-								p.text-center.row-spaced.small
-									| #[strong() {{ getProjectName() }}] has not yet been moved into
-									| the new version of Overleaf. This project was created
-									| anonymously and therefore cannot be automatically imported.
-									| Please download a zip file of the project and upload that to
-									| continue editing it. If you would like to delete this project
-									| after you have made a copy, please contact support.
-
-								.row-spaced.text-center
-									a.btn.btn-primary(ng-href="{{ buildZipDownloadPath(v1ImportData.projectId) }}")
-										| Download project zip file
-
-			.loading-screen(
-				ng-show="mode == 'requireAccept'"
-			)
-				.container
-					.row
-						.col-md-8.col-md-offset-2
-							.card
-								.page-header.text-centered
-									h1 #{translate("invited_to_join")}
-										br
-										em {{ getProjectName() }}
-								.row.text-center
-									.col-md-12
-										p
-											if user
-												| #{translate("accepting_invite_as")}
-												|
-												em #{user.email}
-								.row.text-center
-									.col-md-12
-											button.btn.btn-lg.btn-primary(
-												type='submit'
-												ng-click="postConfirmedByUser()"
-											) #{translate("join_project")}
-
-
-block append foot-scripts
-	script(type="text/javascript", nonce=scriptNonce).
-		$(document).ready(function () {
-			setTimeout(function() {
-				$('.loading-screen-brand').css('height', '20%')
-			}, 500);
-		});

+ 0 - 3
services/web/app/views/source-editor/source-editor.pug

@@ -1,3 +0,0 @@
-source-editor.review-panel-react#editor(
-	ng-show="!!editor.sharejs_doc && !editor.opening && multiSelectedCount === 0 && !editor.error_state"
-)

+ 4 - 6
services/web/app/views/subscriptions/interstitial-payment.pug

@@ -1,10 +1,11 @@
-extends ../layout
+extends ../layout-marketing
 
 include ./plans/_mixins
-include ../_mixins/bootstrap_js
+	
+block entrypointVar
+	- entrypoint = 'pages/user/subscription/plans-v2/plans-v2-main'
 
 block vars
-	- entrypoint = 'pages/user/subscription/plans-v2/plans-v2-main'
 	- var suppressFooter = true
 	- var suppressNavbarRight = true
 	- var suppressCookieBanner = true
@@ -66,6 +67,3 @@ block content
 					| #{translate("continue_with_free_plan")}			
 
 	!= moduleIncludes("contactModalGeneral-marketing", locals)
-
-block prepend foot-scripts
-	+bootstrap-js(bootstrapVersion)

+ 1 - 1
services/web/app/views/subscriptions/plans.pug

@@ -1,6 +1,6 @@
 extends ../layout-marketing
 
-block vars
+block entrypointVar
 	- entrypoint = 'pages/user/subscription/plans-v2/plans-v2-main'
 
 block append meta

+ 1 - 1
services/web/app/views/subscriptions/team/invite_logged_out.pug

@@ -1,4 +1,4 @@
-extends ../../layout
+extends ../../layout-marketing
 
 block content
 	main.content.content-alt.team-invite#main-content

+ 0 - 47
services/web/app/views/view_templates/bonus_templates.pug

@@ -1,47 +0,0 @@
-script(type="text/ng-template", id="BonusLinkToUsModal")
-	.modal-header
-		button.close(
-			type="button"
-			data-dismiss="modal"
-			ng-click="cancel()"
-			aria-label="Close"
-		)
-			span(aria-hidden="true") ×
-		h3 Dropbox link
-	.modal-body.modal-body-share
-
-		div(ng-show="dbState.gotLinkStatus")
-			div(ng-hide="dbState.userIsLinkedToDropbox || !dbState.hasDropboxFeature")
-
-				span(ng-hide="dbState.startedLinkProcess") Your account is not linked to dropbox
-					|    
-					a(ng-click="linkToDropbox()").btn.btn-info Update Dropbox Settings
-
-				p.small.text-center(ng-show="dbState.startedLinkProcess")
-					| Please refresh this page after starting your free trial.
-
-
-			div(ng-show="dbState.hasDropboxFeature && dbState.userIsLinkedToDropbox")
-				progressbar.progress-striped.active(value='dbState.percentageLeftTillNextPoll', type="info")
-				span 
-					strong {{dbState.minsTillNextPoll}} minutes 
-					span until dropbox is next checked for changes.
-
-			div.text-center(ng-hide="dbState.hasDropboxFeature")
-				p You need to upgrade your account to link to dropbox.
-				p
-					a.btn(ng-click="startFreeTrial('dropbox')", ng-class="buttonClass") Start Free Trial
-				p.small(ng-show="startedFreeTrial")
-					| Please refresh this page after starting your free trial.
-
-		div(ng-hide="dbState.gotLinkStatus")
-			span.small   checking dropbox status  
-			i.fa.fa-refresh.fa-spin(aria-hidden="true")
-
-
-
-	.modal-footer()
-		button.btn.btn-default(
-			ng-click="cancel()",
-		)
-			span Dismiss

+ 1 - 1
services/web/babel.config.json

@@ -27,7 +27,7 @@
     ["@babel/react", { "runtime": "automatic" }],
     "@babel/typescript"
   ],
-  "plugins": ["angularjs-annotate", "macros"],
+  "plugins": ["macros"],
   "overrides": [
     // treat .cjs files (e.g. libraries symlinked into node_modules) as commonjs
     {

+ 1 - 31
services/web/config/settings.defaults.js

@@ -1,33 +1,6 @@
-const fs = require('fs')
 const Path = require('path')
 const { merge } = require('@overleaf/settings/merge')
 
-// Automatically detect module imports that are included in this version of the application (SaaS, Server-CE, Server Pro).
-// E.g. during a Server-CE build, we will not find imports for proprietary modules.
-//
-// Restart webpack after adding/removing modules.
-const MODULES_PATH = Path.join(__dirname, '../modules')
-const entryPointsIde = []
-const entryPointsMain = []
-fs.readdirSync(MODULES_PATH).forEach(module => {
-  const entryPathIde = Path.join(
-    MODULES_PATH,
-    module,
-    '/frontend/js/ide/index.js'
-  )
-  if (fs.existsSync(entryPathIde)) {
-    entryPointsIde.push(entryPathIde)
-  }
-  const entryPathMain = Path.join(
-    MODULES_PATH,
-    module,
-    '/frontend/js/main/index.js'
-  )
-  if (fs.existsSync(entryPathMain)) {
-    entryPointsMain.push(entryPathMain)
-  }
-})
-
 let defaultFeatures, siteUrl
 
 // Make time interval config easier.
@@ -918,9 +891,6 @@ module.exports = {
     managedGroupSubscriptionEnrollmentNotification: [],
     managedGroupEnrollmentInvite: [],
     ssoCertificateInfo: [],
-    // See comment at the definition of these variables.
-    entryPointsIde,
-    entryPointsMain,
   },
 
   moduleImportSequence: [
@@ -935,7 +905,7 @@ module.exports = {
     reportOnly: process.env.CSP_REPORT_ONLY === 'true',
     reportPercentage: parseFloat(process.env.CSP_REPORT_PERCENTAGE) || 0,
     reportUri: process.env.CSP_REPORT_URI,
-    exclude: ['app/views/project/editor'],
+    exclude: [],
   },
 
   unsupportedBrowsers: {

+ 3 - 0
services/web/cypress/support/shared/commands/index.ts

@@ -11,6 +11,7 @@ import { interceptFileUpload } from './upload'
 import { interceptProjectListing } from './project-list'
 import { interceptLinkedFile } from './linked-file'
 import { interceptMathJax } from './mathjax'
+import { interceptMetadata } from './metadata'
 
 // eslint-disable-next-line no-unused-vars,@typescript-eslint/no-namespace
 declare global {
@@ -21,6 +22,7 @@ declare global {
       interceptAsync: typeof interceptAsync
       interceptCompile: typeof interceptCompile
       interceptEvents: typeof interceptEvents
+      interceptMetadata: typeof interceptMetadata
       interceptSpelling: typeof interceptSpelling
       waitForCompile: typeof waitForCompile
       interceptDeferredCompile: typeof interceptDeferredCompile
@@ -35,6 +37,7 @@ declare global {
 Cypress.Commands.add('interceptAsync', interceptAsync)
 Cypress.Commands.add('interceptCompile', interceptCompile)
 Cypress.Commands.add('interceptEvents', interceptEvents)
+Cypress.Commands.add('interceptMetadata', interceptMetadata)
 Cypress.Commands.add('interceptSpelling', interceptSpelling)
 Cypress.Commands.add('waitForCompile', waitForCompile)
 Cypress.Commands.add('interceptDeferredCompile', interceptDeferredCompile)

+ 3 - 0
services/web/cypress/support/shared/commands/metadata.ts

@@ -0,0 +1,3 @@
+export const interceptMetadata = () => {
+  cy.intercept('POST', '/project/*/doc/*/metadata', {})
+}

+ 0 - 61
services/web/frontend/js/base.js

@@ -1,61 +0,0 @@
-/* eslint-disable
-    camelcase,
-    max-len,
-    no-useless-escape,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * DS103: Rewrite code to no longer use __guard__
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-
-import './utils/webpack-public-path'
-import './libraries'
-import './infrastructure/error-reporter'
-import './modules/recursionHelper'
-import './modules/errorCatcher'
-import './modules/localStorage'
-import './modules/sessionStorage'
-import getMeta from './utils/meta'
-
-const App = angular
-  .module('OverleafApp', [
-    'ui.bootstrap',
-    'RecursionHelper',
-    'ngSanitize',
-    'ErrorCatcher',
-    'localStorage',
-    'sessionStorage',
-    'ui.select',
-  ])
-  .config([
-    '$qProvider',
-    'uiSelectConfig',
-    function ($qProvider, uiSelectConfig) {
-      $qProvider.errorOnUnhandledRejections(false)
-      uiSelectConfig.spinnerClass = 'fa fa-refresh ui-select-spin'
-    },
-  ])
-
-App.run([
-  '$rootScope',
-  '$templateCache',
-  function ($rootScope, $templateCache) {
-    $rootScope.usersEmail = getMeta('ol-usersEmail')
-
-    // UI Select templates are hard-coded and use Glyphicon icons (which we don't import).
-    // The line below simply overrides the hard-coded template with our own, which is
-    // basically the same but using Font Awesome icons.
-    $templateCache.put(
-      'bootstrap/match.tpl.html',
-      '<div class="ui-select-match" ng-hide="$select.open && $select.searchEnabled" ng-disabled="$select.disabled" ng-class="{\'btn-default-focus\':$select.focus}"><span tabindex="-1" class="btn btn-default form-control ui-select-toggle" aria-label="{{ $select.baseTitle }} activate" ng-disabled="$select.disabled" ng-click="$select.activate()" style="outline: 0;"><span ng-show="$select.isEmpty()" class="ui-select-placeholder text-muted">{{$select.placeholder}}</span> <span ng-hide="$select.isEmpty()" class="ui-select-match-text pull-left" ng-class="{\'ui-select-allow-clear\': $select.allowClear && !$select.isEmpty()}" ng-transclude=""></span> <i class="caret pull-right" ng-click="$select.toggle($event)"></i> <a ng-show="$select.allowClear && !$select.isEmpty() && ($select.disabled !== true)" aria-label="{{ $select.baseTitle }} clear" style="margin-right: 10px" ng-click="$select.clear($event)" class="btn btn-xs btn-link pull-right"><i class="fa fa-times" aria-hidden="true"></i></a></span></div>'
-    )
-  },
-])
-
-export default App

+ 0 - 134
services/web/frontend/js/directives/eventTracking.js

@@ -1,134 +0,0 @@
-import _ from 'lodash'
-/* eslint-disable
-    camelcase,
-    max-len,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-// For sending event data to metabase and google analytics
-// ---
-// by default,
-//   event not sent to MB.
-//     for MB, add event-tracking-mb='true'
-//     by default, event sent to MB via sendMB
-//   event not sent to GA.
-//     for GA, add event-tracking-ga attribute, where the value is the GA category
-// Either GA or MB can use the attribute event-tracking-send-once='true' to
-// send event just once
-//   MB will use the key and GA will use the action to determine if the event
-//   has been sent
-// event-tracking-trigger attribute is required to send event
-
-/* eslint-disable
-    camelcase,
-    max-len,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-// For sending event data to metabase and google analytics
-// ---
-// by default,
-//   event not sent to MB.
-//     for MB, add event-tracking-mb='true'
-//     by default, event sent to MB via sendMB
-//   event not sent to GA.
-//     for GA, add event-tracking-ga attribute, where the value is the GA category
-// Either GA or MB can use the attribute event-tracking-send-once='true' to
-// send event just once
-//   MB will use the key and GA will use the action to determine if the event
-//   has been sent
-// event-tracking-trigger attribute is required to send event
-
-import App from '../base'
-
-const isInViewport = function (element) {
-  const elTop = element.offset().top
-  const elBtm = elTop + element.outerHeight()
-
-  const viewportTop = $(window).scrollTop()
-  const viewportBtm = viewportTop + $(window).height()
-
-  return elBtm > viewportTop && elTop < viewportBtm
-}
-
-export default App.directive('eventTracking', [
-  'eventTracking',
-  function (eventTracking) {
-    return {
-      scope: {
-        eventTracking: '@',
-        eventSegmentation: '=?',
-      },
-      link(scope, element, attrs) {
-        const sendGA = attrs.eventTrackingGa || false
-        const sendMB = attrs.eventTrackingMb || false
-        const sendMBFunction = attrs.eventTrackingSendOnce
-          ? 'sendMBOnce'
-          : 'sendMB'
-        const sendGAFunction = attrs.eventTrackingSendOnce
-          ? 'sendGAOnce'
-          : 'send'
-        const segmentation = scope.eventSegmentation || {}
-        segmentation.page = window.location.pathname
-
-        const sendEvent = function (scrollEvent) {
-          /*
-                      @param {boolean}		scrollEvent		Use to unbind scroll event
-                  */
-          if (sendMB) {
-            eventTracking[sendMBFunction](scope.eventTracking, segmentation)
-          }
-          if (sendGA) {
-            eventTracking[sendGAFunction](
-              attrs.eventTrackingGa,
-              attrs.eventTrackingAction || scope.eventTracking,
-              attrs.eventTrackingLabel || ''
-            )
-          }
-          if (scrollEvent) {
-            return $(window).unbind('resize scroll')
-          }
-        }
-
-        if (attrs.eventTrackingTrigger === 'load') {
-          return sendEvent()
-        } else if (attrs.eventTrackingTrigger === 'click') {
-          return element.on('click', e => sendEvent())
-        } else if (attrs.eventTrackingTrigger === 'hover') {
-          let timer = null
-          let timeoutAmt = 500
-          if (attrs.eventHoverAmt) {
-            timeoutAmt = parseInt(attrs.eventHoverAmt, 10)
-          }
-          return element
-            .on('mouseenter', function () {
-              timer = setTimeout(() => sendEvent(), timeoutAmt)
-            })
-            .on('mouseleave', () => clearTimeout(timer))
-        } else if (
-          attrs.eventTrackingTrigger === 'scroll' &&
-          !eventTracking.eventInCache(scope.eventTracking)
-        ) {
-          $(window).on(
-            'resize scroll',
-            _.throttle(() => {
-              if (isInViewport(element)) {
-                sendEvent(true)
-              }
-            }, 500)
-          )
-        }
-      },
-    }
-  },
-])

+ 0 - 6
services/web/frontend/js/features/editor-left-menu/controllers/editor-left-menu-controller.js

@@ -1,6 +0,0 @@
-import App from '../../../base'
-import { react2angular } from 'react2angular'
-import { rootContext } from '../../../shared/context/root-context'
-import EditorLeftMenu from '../components/editor-left-menu'
-
-App.component('editorLeftMenu', react2angular(rootContext.use(EditorLeftMenu)))

+ 1 - 2
services/web/frontend/js/features/editor-left-menu/hooks/use-project-wide-settings-socket-listener.tsx

@@ -7,8 +7,7 @@ export default function useProjectWideSettingsSocketListener() {
   const ide = useIdeContext()
 
   const [project, setProject] = useScopeValue<ProjectSettings | undefined>(
-    'project',
-    true
+    'project'
   )
 
   const setCompiler = useCallback(

+ 1 - 1
services/web/frontend/js/features/editor-left-menu/hooks/use-project-wide-settings.tsx

@@ -8,7 +8,7 @@ import { debugConsole } from '@/utils/debugging'
 
 export default function useProjectWideSettings() {
   // The value will be undefined on mount
-  const [project] = useScopeValue<ProjectSettings | undefined>('project', true)
+  const [project] = useScopeValue<ProjectSettings | undefined>('project')
   const saveProjectSettings = useSaveProjectSettings()
 
   const setCompiler = useCallback(

+ 1 - 1
services/web/frontend/js/features/editor-left-menu/hooks/use-save-project-settings.tsx

@@ -6,7 +6,7 @@ export default function useSaveProjectSettings() {
   // projectSettings value will be undefined on mount
   const [projectSettings, setProjectSettings] = useScopeValue<
     ProjectSettings | undefined
-  >('project', true)
+  >('project')
   const { _id: projectId } = useProjectContext()
 
   return async (

+ 0 - 30
services/web/frontend/js/features/editor-navigation-toolbar/controllers/editor-navigation-toolbar-controller.js

@@ -1,30 +0,0 @@
-import App from '../../../base'
-import { react2angular } from 'react2angular'
-import EditorNavigationToolbarRoot from '../components/editor-navigation-toolbar-root'
-import { rootContext } from '../../../shared/context/root-context'
-
-App.controller('EditorNavigationToolbarController', [
-  '$scope',
-  'ide',
-  function ($scope, ide) {
-    // wrapper is required to avoid scope problems with `this` inside `EditorManager`
-    $scope.openDoc = (doc, args) => ide.editorManager.openDoc(doc, args)
-  },
-])
-
-App.component(
-  'editorNavigationToolbarRoot',
-  react2angular(rootContext.use(EditorNavigationToolbarRoot), [
-    'openDoc',
-
-    // `$scope.onlineUsersArray` is already populated by `OnlineUsersManager`, which also creates
-    // a new array instance every time the list of online users change (which should refresh the
-    // value passed to React as a prop, triggering a re-render)
-    'onlineUsersArray',
-
-    // We're still including ShareController as part fo the React navigation toolbar. The reason is
-    // the coupling between ShareController's $scope and Angular's ShareProjectModal. Once ShareProjectModal
-    // is fully ported to React we should be able to repli
-    'openShareProjectModal',
-  ])
-)

+ 0 - 123
services/web/frontend/js/features/file-tree/controllers/file-tree-controller.js

@@ -1,123 +0,0 @@
-import App from '../../../base'
-import { react2angular } from 'react2angular'
-import { cloneDeep } from 'lodash'
-
-import FileTreeRoot from '../components/file-tree-root'
-import { rootContext } from '../../../shared/context/root-context'
-
-App.controller('ReactFileTreeController', [
-  '$scope',
-  '$timeout',
-  'ide',
-  function ($scope, $timeout, ide) {
-    $scope.isConnected = true
-
-    $scope.$on('project:joined', () => {
-      $scope.$emit('file-tree:initialized')
-    })
-
-    $scope.$watch('editor.open_doc_id', openDocId => {
-      window.dispatchEvent(
-        new CustomEvent('editor.openDoc', { detail: openDocId })
-      )
-    })
-
-    $scope.$on('file-tree.reselectDoc', (ev, docId) => {
-      window.dispatchEvent(new CustomEvent('editor.openDoc', { detail: docId }))
-    })
-
-    // Set isConnected to true if:
-    // - connection state is 'ready', OR
-    // - connection state is 'waitingCountdown' and reconnection_countdown is null
-    // The added complexity is needed  because in Firefox on page reload the
-    // connection state goes into 'waitingCountdown' before being hidden and we
-    // don't want to show a disconnect UI.
-    function updateIsConnected() {
-      if ($scope.connection) {
-        const isReady = $scope.connection.state === 'ready'
-        const willStartCountdown =
-          $scope.connection.state === 'waitingCountdown' &&
-          $scope.connection.reconnection_countdown === null
-        $scope.isConnected = isReady || willStartCountdown
-      } else {
-        $scope.isConnected = false
-      }
-    }
-
-    $scope.$watch('connection.state', updateIsConnected)
-    $scope.$watch('connection.reconnection_countdown', updateIsConnected)
-
-    $scope.onInit = () => {
-      // HACK: resize the vertical pane on init after a 0ms timeout. We do not
-      // understand why this is necessary but without this the resized handle is
-      // stuck at the bottom. The vertical resize will soon be migrated to React
-      // so we accept to live with this hack for now.
-      $timeout(() => {
-        $scope.$emit('left-pane-resize-all')
-      })
-    }
-
-    $scope.onSelect = selectedEntities => {
-      if (selectedEntities.length === 1) {
-        const selectedEntity = selectedEntities[0]
-        const type =
-          selectedEntity.type === 'fileRef' ? 'file' : selectedEntity.type
-        $scope.$emit('entity:selected', {
-          ...selectedEntity.entity,
-          id: selectedEntity.entity._id,
-          type,
-        })
-
-        // in the react implementation there is no such concept as "1
-        // multi-selected entity" so here we pass a count of 0
-        $scope.$emit('entities:multiSelected', { count: 0 })
-      } else if (selectedEntities.length > 1) {
-        $scope.$emit('entities:multiSelected', {
-          count: selectedEntities.length,
-        })
-      } else {
-        $scope.$emit('entity:no-selection')
-      }
-    }
-
-    $scope.refProviders = ide.$scope.user.refProviders || {}
-
-    ide.$scope.$watch(
-      'user.refProviders',
-      refProviders => {
-        $scope.refProviders = cloneDeep(refProviders)
-      },
-      true
-    )
-
-    $scope.setRefProviderEnabled = (provider, value = true) => {
-      ide.$scope.$applyAsync(() => {
-        ide.$scope.user.refProviders[provider] = value
-      })
-    }
-
-    $scope.setStartedFreeTrial = started => {
-      $scope.$applyAsync(() => {
-        $scope.startedFreeTrial = started
-      })
-    }
-
-    $scope.reindexReferences = () => {
-      ide.$scope.$emit('references:should-reindex', {})
-    }
-  },
-])
-
-App.component(
-  'fileTreeRoot',
-  react2angular(rootContext.use(FileTreeRoot), [
-    'onSelect',
-    'onDelete',
-    'onInit',
-    'isConnected',
-    'setRefProviderEnabled',
-    'setStartedFreeTrial',
-    'reindexReferences',
-    'refProviders',
-  ])
-)

+ 0 - 24
services/web/frontend/js/features/file-view/controllers/file-view-controller.js

@@ -1,24 +0,0 @@
-import App from '../../../base'
-import { react2angular } from 'react2angular'
-import _ from 'lodash'
-
-import { rootContext } from '../../../shared/context/root-context'
-import FileView from '../components/file-view'
-
-export default App.controller('FileViewController', [
-  '$scope',
-  '$rootScope',
-  function ($scope, $rootScope) {
-    $scope.file = $scope.openFile
-
-    $scope.storeReferencesKeys = newKeys => {
-      const oldKeys = $rootScope._references.keys
-      return ($rootScope._references.keys = _.union(oldKeys, newKeys))
-    }
-  },
-])
-
-App.component(
-  'fileView',
-  react2angular(rootContext.use(FileView), ['storeReferencesKeys', 'file'])
-)

+ 0 - 6
services/web/frontend/js/features/history/controllers/history-controller.js

@@ -1,6 +0,0 @@
-import App from '../../../base'
-import { react2angular } from 'react2angular'
-import HistoryRoot from '../components/history-root'
-import { rootContext } from '../../../shared/context/root-context'
-
-App.component('historyRoot', react2angular(rootContext.use(HistoryRoot)))

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

@@ -27,9 +27,9 @@ type ConnectionContextValue = {
   disconnect: () => void
 }
 
-const ConnectionContext = createContext<ConnectionContextValue | undefined>(
-  undefined
-)
+export const ConnectionContext = createContext<
+  ConnectionContextValue | undefined
+>(undefined)
 
 export const ConnectionProvider: FC = ({ children }) => {
   const location = useLocation()

+ 3 - 1
services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx

@@ -87,7 +87,9 @@ export type EditorScopeValue = {
   error_state: boolean
 }
 
-const EditorManagerContext = createContext<EditorManager | undefined>(undefined)
+export const EditorManagerContext = createContext<EditorManager | undefined>(
+  undefined
+)
 
 export const EditorManagerProvider: FC = ({ children }) => {
   const { t } = useTranslation()

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

@@ -40,7 +40,7 @@ type IdeReactContextValue = {
   projectJoined: boolean
 }
 
-const IdeReactContext = createContext<IdeReactContextValue | undefined>(
+export const IdeReactContext = createContext<IdeReactContextValue | undefined>(
   undefined
 )
 
@@ -63,7 +63,7 @@ function populatePdfScope(store: ReactScopeValueStore) {
   store.allowNonExistentPath('pdf', true)
 }
 
-function createReactScopeValueStore(projectId: string) {
+export function createReactScopeValueStore(projectId: string) {
   const scopeStore = new ReactScopeValueStore()
 
   // Populate the scope value store with default values that will be used by

+ 1 - 7
services/web/frontend/js/features/ide-react/context/metadata-context.tsx

@@ -15,7 +15,6 @@ import _ from 'lodash'
 import { getJSON, postJSON } from '@/infrastructure/fetch-json'
 import { useOnlineUsersContext } from '@/features/ide-react/context/online-users-context'
 import { useEditorContext } from '@/shared/context/editor-context'
-import { useIdeContext } from '@/shared/context/ide-context'
 import useSocketListener from '@/features/ide-react/hooks/use-socket-listener'
 import useEventListener from '@/shared/hooks/use-event-listener'
 import { useModalsContext } from '@/features/ide-react/context/modals-context'
@@ -42,13 +41,12 @@ type MetadataContextValue = {
 
 type DocMetadataResponse = { docId: string; meta: DocumentMetadata }
 
-const MetadataContext = createContext<MetadataContextValue | undefined>(
+export const MetadataContext = createContext<MetadataContextValue | undefined>(
   undefined
 )
 
 export const MetadataProvider: FC = ({ children }) => {
   const { t } = useTranslation()
-  const ide = useIdeContext()
   const { eventEmitter, projectId } = useIdeReactContext()
   const { socket } = useConnectionContext()
   const { onlineUsersCount } = useOnlineUsersContext()
@@ -225,10 +223,6 @@ export const MetadataProvider: FC = ({ children }) => {
     [documents, getAllLabels, getAllPackages]
   )
 
-  // Expose metadataManager via ide object because useCodeMirrorScope relies on
-  // it, for now
-  ide.metadataManager = value
-
   return (
     <MetadataContext.Provider value={value}>
       {children}

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

@@ -9,7 +9,9 @@ import {
 import useScopeValue from '@/shared/hooks/use-scope-value'
 import { DeepReadonly } from '../../../../../types/utils'
 
-const PermissionsContext = createContext<Permissions | undefined>(undefined)
+export const PermissionsContext = createContext<Permissions | undefined>(
+  undefined
+)
 
 const permissionsMap: DeepReadonly<Record<PermissionsLevel, Permissions>> = {
   readOnly: {

+ 8 - 8
services/web/frontend/js/features/ide-react/context/react-context-root.tsx

@@ -40,10 +40,10 @@ export const ReactContextRoot: FC = ({ children }) => {
                             <PermissionsProvider>
                               <ProjectSettingsProvider>
                                 <LayoutProvider>
-                                  <LocalCompileProvider>
-                                    <DetachCompileProvider>
-                                      <ChatProvider>
-                                        <EditorManagerProvider>
+                                  <EditorManagerProvider>
+                                    <LocalCompileProvider>
+                                      <DetachCompileProvider>
+                                        <ChatProvider>
                                           <FileTreeOpenProvider>
                                             <OnlineUsersProvider>
                                               <MetadataProvider>
@@ -53,10 +53,10 @@ export const ReactContextRoot: FC = ({ children }) => {
                                               </MetadataProvider>
                                             </OnlineUsersProvider>
                                           </FileTreeOpenProvider>
-                                        </EditorManagerProvider>
-                                      </ChatProvider>
-                                    </DetachCompileProvider>
-                                  </LocalCompileProvider>
+                                        </ChatProvider>
+                                      </DetachCompileProvider>
+                                    </LocalCompileProvider>
+                                  </EditorManagerProvider>
                                 </LayoutProvider>
                               </ProjectSettingsProvider>
                             </PermissionsProvider>

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

@@ -170,10 +170,7 @@ function useReviewPanelState(): ReviewPanel.ReviewPanelState {
     ReviewPanel.Value<'commentThreads'>
   >({})
   const [entries, setEntries] = useState<ReviewPanel.Value<'entries'>>({})
-  const [users, setUsers] = useScopeValue<ReviewPanel.Value<'users'>>(
-    'users',
-    true
-  )
+  const [users, setUsers] = useScopeValue<ReviewPanel.Value<'users'>>('users')
   const [resolvedComments, setResolvedComments] = useState<
     ReviewPanel.Value<'resolvedComments'>
   >({})
@@ -532,7 +529,6 @@ function useReviewPanelState(): ReviewPanel.ReviewPanelState {
     if (!user) {
       return 'anonymous'
     }
-    // FIXME: check this
     if (project.owner._id === user.id) {
       return 'member'
     }

+ 0 - 22
services/web/frontend/js/features/ide-react/scope-event-emitter/angular-scope-event-emitter.ts

@@ -1,22 +0,0 @@
-import {
-  ScopeEventEmitter,
-  ScopeEventName,
-} from '../../../../../types/ide/scope-event-emitter'
-import { Scope } from '../../../../../types/angular/scope'
-
-export class AngularScopeEventEmitter implements ScopeEventEmitter {
-  // eslint-disable-next-line no-useless-constructor
-  constructor(readonly $scope: Scope) {}
-
-  emit(eventName: ScopeEventName, broadcast: boolean, ...detail: unknown[]) {
-    if (broadcast) {
-      this.$scope.$broadcast(eventName, ...detail)
-    } else {
-      this.$scope.$emit(eventName, ...detail)
-    }
-  }
-
-  on(eventName: ScopeEventName, listener: (...args: unknown[]) => void) {
-    return this.$scope.$on(eventName, listener)
-  }
-}

+ 0 - 28
services/web/frontend/js/features/ide-react/scope-value-store/angular-scope-value-store.ts

@@ -1,28 +0,0 @@
-import { ScopeValueStore } from '../../../../../types/ide/scope-value-store'
-import { Scope } from '../../../../../types/angular/scope'
-import _ from 'lodash'
-
-export class AngularScopeValueStore implements ScopeValueStore {
-  // eslint-disable-next-line no-useless-constructor
-  constructor(readonly $scope: Scope) {}
-
-  get(path: string) {
-    return _.get(this.$scope, path)
-  }
-
-  set(path: string, value: unknown): void {
-    this.$scope.$applyAsync(() => _.set(this.$scope, path, value))
-  }
-
-  watch<T>(
-    path: string,
-    callback: (newValue: T) => void,
-    deep: boolean
-  ): () => void {
-    return this.$scope.$watch(
-      path,
-      (newValue: T) => callback(deep ? _.cloneDeep(newValue) : newValue),
-      deep
-    )
-  }
-}

+ 0 - 9
services/web/frontend/js/features/outline/controllers/outline-controller.js

@@ -1,9 +0,0 @@
-import App from '@/base'
-import { react2angular } from 'react2angular'
-import { rootContext } from '@/shared/context/root-context'
-import { OutlineContainer } from '@/features/outline/components/outline-container'
-
-App.component(
-  'outlineContainer',
-  react2angular(rootContext.use(OutlineContainer))
-)

+ 3 - 3
services/web/frontend/js/features/pdf-preview/components/pdf-preview-detached-root.jsx

@@ -1,7 +1,7 @@
 import ReactDOM from 'react-dom'
 import PdfPreview from './pdf-preview'
-import { ContextRoot } from '../../../shared/context/root-context'
 import useWaitForI18n from '../../../shared/hooks/use-wait-for-i18n'
+import { ReactContextRoot } from '@/features/ide-react/context/react-context-root'
 
 function PdfPreviewDetachedRoot() {
   const { isReady } = useWaitForI18n()
@@ -11,9 +11,9 @@ function PdfPreviewDetachedRoot() {
   }
 
   return (
-    <ContextRoot>
+    <ReactContextRoot>
       <PdfPreview />
-    </ContextRoot>
+    </ReactContextRoot>
   )
 }
 

+ 0 - 19
services/web/frontend/js/features/pdf-preview/controllers/pdf-preview-controller.js

@@ -1,19 +0,0 @@
-import App from '../../../base'
-import { react2angular } from 'react2angular'
-
-import PdfPreview from '../components/pdf-preview'
-import { rootContext } from '../../../shared/context/root-context'
-import {
-  DefaultSynctexControl,
-  DetacherSynctexControl,
-} from '../components/detach-synctex-control'
-
-App.component('pdfPreview', react2angular(rootContext.use(PdfPreview), []))
-App.component(
-  'pdfSynctexControls',
-  react2angular(rootContext.use(DefaultSynctexControl), [])
-)
-App.component(
-  'detacherSynctexControl',
-  react2angular(rootContext.use(DetacherSynctexControl), [])
-)

+ 0 - 68
services/web/frontend/js/features/share-project-modal/controllers/react-share-project-modal-controller.js

@@ -1,68 +0,0 @@
-import App from '../../../base'
-import { react2angular } from 'react2angular'
-
-import ShareProjectModal from '../components/share-project-modal'
-import { rootContext } from '../../../shared/context/root-context'
-import { listProjectInvites, listProjectMembers } from '../utils/api'
-import { debugConsole } from '@/utils/debugging'
-
-App.component(
-  'shareProjectModal',
-  react2angular(rootContext.use(ShareProjectModal), [
-    'animation',
-    'handleHide',
-    'show',
-  ])
-)
-
-export default App.controller('ReactShareProjectModalController', [
-  '$scope',
-  'eventTracking',
-  'ide',
-  function ($scope, eventTracking, ide) {
-    $scope.show = false
-
-    $scope.handleHide = () => {
-      $scope.$applyAsync(() => {
-        $scope.show = false
-      })
-    }
-
-    $scope.openShareProjectModal = () => {
-      eventTracking.sendMBOnce('ide-open-share-modal-once')
-      $scope.$applyAsync(() => {
-        $scope.show = true
-      })
-    }
-
-    ide.socket.on('project:membership:changed', data => {
-      if (data.members) {
-        listProjectMembers($scope.project._id)
-          .then(({ members }) => {
-            if (members) {
-              $scope.$applyAsync(() => {
-                $scope.project.members = members
-              })
-            }
-          })
-          .catch(err => {
-            debugConsole.error('Error fetching members for project', err)
-          })
-      }
-
-      if (data.invites) {
-        listProjectInvites($scope.project._id)
-          .then(({ invites }) => {
-            if (invites) {
-              $scope.$applyAsync(() => {
-                $scope.project.invites = invites
-              })
-            }
-          })
-          .catch(err => {
-            debugConsole.error('Error fetching invites for project', err)
-          })
-      }
-    })
-  },
-])

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

@@ -8,12 +8,10 @@ import {
   useReviewPanelUpdaterFnsContext,
   useReviewPanelValueContext,
 } from '../../../context/review-panel/review-panel-context'
-import { useIdeContext } from '@/shared/context/ide-context'
 import { useEditorContext } from '@/shared/context/editor-context'
 import { useCodeMirrorViewContext } from '../../codemirror-editor'
 import Modal, { useBulkActionsModal } from '../entries/bulk-actions-entry/modal'
 import getMeta from '../../../../../utils/meta'
-import useScopeValue from '../../../../../shared/hooks/use-scope-value'
 import useScopeEventListener from '@/shared/hooks/use-scope-event-listener'
 import { memo, useCallback } from 'react'
 import { useLayoutContext } from '@/shared/context/layout-context'
@@ -29,14 +27,8 @@ function EditorWidgets() {
     handleShowBulkRejectDialog,
     handleConfirmDialog,
   } = useBulkActionsModal()
-  const { setIsAddingComment, handleSetSubview } =
-    useReviewPanelUpdaterFnsContext()
-  const { isReactIde } = useIdeContext()
+  const { setIsAddingComment } = useReviewPanelUpdaterFnsContext()
   const { toggleReviewPanel } = useReviewPanelUpdaterFnsContext()
-  const [addNewComment] =
-    useScopeValue<(e: React.MouseEvent<HTMLButtonElement>) => void>(
-      'addNewComment'
-    )
   const view = useCodeMirrorViewContext()
   const { reviewPanelOpen } = useLayoutContext()
   const { isRestrictedTokenMember } = useEditorContext()
@@ -55,20 +47,9 @@ function EditorWidgets() {
     openDocId && openDocId in entries ? entries[openDocId] : undefined
 
   const handleAddNewCommentClick = (e: React.MouseEvent<HTMLButtonElement>) => {
-    if (isReactIde) {
-      e.preventDefault()
-      setIsAddingComment(true)
-      toggleReviewPanel()
-      return
-    }
-
-    addNewComment(e)
-    setTimeout(() => {
-      // Re-render the comment box in order to add autofocus every time
-      handleSetSubview('cur_file')
-      setIsAddingComment(false)
-      setIsAddingComment(true)
-    }, 0)
+    e.preventDefault()
+    setIsAddingComment(true)
+    toggleReviewPanel()
   }
 
   useScopeEventListener(

+ 4 - 17
services/web/frontend/js/features/source-editor/components/review-panel/review-panel.tsx

@@ -8,26 +8,14 @@ import { isCurrentFileView } from '../../utils/sub-view'
 import { useLayoutContext } from '@/shared/context/layout-context'
 import classnames from 'classnames'
 import { lazy, memo } from 'react'
-import getMeta from '@/utils/meta'
 import { SubView } from '../../../../../../types/review-panel/review-panel'
 
-const isReactIde: boolean = getMeta('ol-idePageReact')
-
 type ReviewPanelViewProps = {
   parentDomNode: Element
 }
 
 function ReviewPanelView({ parentDomNode }: ReviewPanelViewProps) {
-  const { subView } = useReviewPanelValueContext()
-
-  return ReactDOM.createPortal(
-    isReactIde ? (
-      <ReviewPanelContainer />
-    ) : (
-      <ReviewPanelContent subView={subView} />
-    ),
-    parentDomNode
-  )
+  return ReactDOM.createPortal(<ReviewPanelContainer />, parentDomNode)
 }
 
 const ReviewPanelContainer = memo(() => {
@@ -65,10 +53,9 @@ const ReviewPanelContent = memo<{ subView: SubView }>(({ subView }) => (
 ))
 ReviewPanelContent.displayName = 'ReviewPanelContent'
 
-const ReviewPanelProvider = lazy(() =>
-  isReactIde
-    ? import('@/features/ide-react/context/review-panel/review-panel-provider')
-    : import('../../context/review-panel/review-panel-provider')
+const ReviewPanelProvider = lazy(
+  () =>
+    import('@/features/ide-react/context/review-panel/review-panel-provider')
 )
 
 function ReviewPanel() {

+ 0 - 275
services/web/frontend/js/features/source-editor/context/review-panel/hooks/use-angular-review-panel-state.ts

@@ -1,275 +0,0 @@
-import { useState, useMemo, useCallback } from 'react'
-import useScopeValue from '../../../../../shared/hooks/use-scope-value'
-import useLayoutToLeft from '@/features/ide-react/context/review-panel/hooks/useLayoutToLeft'
-import { sendMB } from '../../../../../infrastructure/event-tracking'
-import type * as ReviewPanel from '../types/review-panel-state'
-import {
-  SubView,
-  ThreadId,
-} from '../../../../../../../types/review-panel/review-panel'
-import { DocId } from '../../../../../../../types/project-settings'
-import { dispatchReviewPanelLayout as handleLayoutChange } from '../../../extensions/changes/change-manager'
-
-function useAngularReviewPanelState(): ReviewPanel.ReviewPanelState {
-  const [subView, setSubView] = useScopeValue<ReviewPanel.Value<'subView'>>(
-    'reviewPanel.subView'
-  )
-  const [isOverviewLoading] = useScopeValue<
-    ReviewPanel.Value<'isOverviewLoading'>
-  >('reviewPanel.overview.loading')
-  const [nVisibleSelectedChanges] = useScopeValue<
-    ReviewPanel.Value<'nVisibleSelectedChanges'>
-  >('reviewPanel.nVisibleSelectedChanges')
-  const [collapsed, setCollapsed] = useScopeValue<
-    ReviewPanel.Value<'collapsed'>
-  >('reviewPanel.overview.docsCollapsedState')
-  const [commentThreads] = useScopeValue<ReviewPanel.Value<'commentThreads'>>(
-    'reviewPanel.commentThreads',
-    true
-  )
-  const [entries] = useScopeValue<ReviewPanel.Value<'entries'>>(
-    'reviewPanel.entries',
-    true
-  )
-  const [loadingThreads] =
-    useScopeValue<ReviewPanel.Value<'loadingThreads'>>('loadingThreads')
-
-  const [permissions] =
-    useScopeValue<ReviewPanel.Value<'permissions'>>('permissions')
-  const [users] = useScopeValue<ReviewPanel.Value<'users'>>('users', true)
-  const [resolvedComments] = useScopeValue<
-    ReviewPanel.Value<'resolvedComments'>
-  >('reviewPanel.resolvedComments', true)
-
-  const [wantTrackChanges] = useScopeValue<
-    ReviewPanel.Value<'wantTrackChanges'>
-  >('editor.wantTrackChanges')
-  const [openDocId] =
-    useScopeValue<ReviewPanel.Value<'openDocId'>>('editor.open_doc_id')
-  const [shouldCollapse, setShouldCollapse] = useScopeValue<
-    ReviewPanel.Value<'shouldCollapse'>
-  >('reviewPanel.fullTCStateCollapsed')
-  const [lineHeight] = useScopeValue<number>(
-    'reviewPanel.rendererData.lineHeight'
-  )
-
-  const [toggleTrackChangesForEveryone] = useScopeValue<
-    ReviewPanel.UpdaterFn<'toggleTrackChangesForEveryone'>
-  >('toggleTrackChangesForEveryone')
-  const [toggleTrackChangesForUser] = useScopeValue<
-    ReviewPanel.UpdaterFn<'toggleTrackChangesForUser'>
-  >('toggleTrackChangesForUser')
-  const [toggleTrackChangesForGuests] = useScopeValue<
-    ReviewPanel.UpdaterFn<'toggleTrackChangesForGuests'>
-  >('toggleTrackChangesForGuests')
-
-  const [trackChangesState] = useScopeValue<
-    ReviewPanel.Value<'trackChangesState'>
-  >('reviewPanel.trackChangesState')
-  const [trackChangesOnForEveryone] = useScopeValue<
-    ReviewPanel.Value<'trackChangesOnForEveryone'>
-  >('reviewPanel.trackChangesOnForEveryone')
-  const [trackChangesOnForGuests] = useScopeValue<
-    ReviewPanel.Value<'trackChangesOnForGuests'>
-  >('reviewPanel.trackChangesOnForGuests')
-  const [trackChangesForGuestsAvailable] = useScopeValue<
-    ReviewPanel.Value<'trackChangesForGuestsAvailable'>
-  >('reviewPanel.trackChangesForGuestsAvailable')
-  const [resolveComment] =
-    useScopeValue<ReviewPanel.UpdaterFn<'resolveComment'>>('resolveComment')
-  const [submitNewComment] =
-    useScopeValue<ReviewPanel.UpdaterFn<'submitNewComment'>>('submitNewComment')
-  const [deleteComment] =
-    useScopeValue<ReviewPanel.UpdaterFn<'deleteComment'>>('deleteComment')
-  const [gotoEntry] =
-    useScopeValue<ReviewPanel.UpdaterFn<'gotoEntry'>>('gotoEntry')
-  const [saveEdit] =
-    useScopeValue<ReviewPanel.UpdaterFn<'saveEdit'>>('saveEdit')
-  const [submitReplyAngular] =
-    useScopeValue<
-      (entry: { thread_id: ThreadId; replyContent: string }) => void
-    >('submitReply')
-
-  const [formattedProjectMembers] = useScopeValue<
-    ReviewPanel.Value<'formattedProjectMembers'>
-  >('reviewPanel.formattedProjectMembers')
-
-  const [toggleReviewPanel] =
-    useScopeValue<ReviewPanel.UpdaterFn<'toggleReviewPanel'>>(
-      'toggleReviewPanel'
-    )
-  const [unresolveComment] =
-    useScopeValue<ReviewPanel.UpdaterFn<'unresolveComment'>>('unresolveComment')
-
-  const [deleteThreadAngular] =
-    useScopeValue<
-      (
-        _: unknown,
-        ...args: [...Parameters<ReviewPanel.UpdaterFn<'deleteThread'>>]
-      ) => ReturnType<ReviewPanel.UpdaterFn<'deleteThread'>>
-    >('deleteThread')
-  const deleteThread = useCallback(
-    (docId: DocId, threadId: ThreadId) => {
-      deleteThreadAngular(undefined, docId, threadId)
-    },
-    [deleteThreadAngular]
-  )
-
-  const [refreshResolvedCommentsDropdown] = useScopeValue<
-    ReviewPanel.UpdaterFn<'refreshResolvedCommentsDropdown'>
-  >('refreshResolvedCommentsDropdown')
-  const [acceptChanges] =
-    useScopeValue<ReviewPanel.UpdaterFn<'acceptChanges'>>('acceptChanges')
-  const [rejectChanges] =
-    useScopeValue<ReviewPanel.UpdaterFn<'rejectChanges'>>('rejectChanges')
-  const [bulkAcceptActions] =
-    useScopeValue<ReviewPanel.UpdaterFn<'bulkAcceptActions'>>(
-      'bulkAcceptActions'
-    )
-  const [bulkRejectActions] =
-    useScopeValue<ReviewPanel.UpdaterFn<'bulkRejectActions'>>(
-      'bulkRejectActions'
-    )
-
-  const layoutToLeft = useLayoutToLeft('#editor')
-
-  const handleSetSubview = useCallback(
-    (subView: SubView) => {
-      setSubView(subView)
-      sendMB('rp-subview-change', { subView })
-    },
-    [setSubView]
-  )
-
-  const submitReply = useCallback(
-    (threadId: ThreadId, replyContent: string) => {
-      submitReplyAngular({ thread_id: threadId, replyContent })
-    },
-    [submitReplyAngular]
-  )
-
-  const [isAddingComment, setIsAddingComment] = useState(false)
-  const [navHeight, setNavHeight] = useState(0)
-  const [toolbarHeight, setToolbarHeight] = useState(0)
-  const [layoutSuspended, setLayoutSuspended] = useState(false)
-  const [unsavedComment, setUnsavedComment] = useState('')
-
-  const values = useMemo<ReviewPanel.ReviewPanelState['values']>(
-    () => ({
-      collapsed,
-      commentThreads,
-      entries,
-      isAddingComment,
-      loadingThreads,
-      nVisibleSelectedChanges,
-      permissions,
-      users,
-      resolvedComments,
-      shouldCollapse,
-      navHeight,
-      toolbarHeight,
-      subView,
-      wantTrackChanges,
-      isOverviewLoading,
-      openDocId,
-      lineHeight,
-      trackChangesState,
-      trackChangesOnForEveryone,
-      trackChangesOnForGuests,
-      trackChangesForGuestsAvailable,
-      formattedProjectMembers,
-      layoutSuspended,
-      unsavedComment,
-      layoutToLeft,
-    }),
-    [
-      collapsed,
-      commentThreads,
-      entries,
-      isAddingComment,
-      loadingThreads,
-      nVisibleSelectedChanges,
-      permissions,
-      users,
-      resolvedComments,
-      shouldCollapse,
-      navHeight,
-      toolbarHeight,
-      subView,
-      wantTrackChanges,
-      isOverviewLoading,
-      openDocId,
-      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,
-      deleteThread,
-      toggleTrackChangesForEveryone,
-      toggleTrackChangesForUser,
-      toggleTrackChangesForGuests,
-      setCollapsed,
-      setShouldCollapse,
-      setIsAddingComment,
-      setNavHeight,
-      setToolbarHeight,
-      setLayoutSuspended,
-      setUnsavedComment,
-    }),
-    [
-      handleSetSubview,
-      gotoEntry,
-      resolveComment,
-      submitReply,
-      acceptChanges,
-      rejectChanges,
-      toggleReviewPanel,
-      bulkAcceptActions,
-      bulkRejectActions,
-      saveEdit,
-      submitNewComment,
-      deleteComment,
-      unresolveComment,
-      refreshResolvedCommentsDropdown,
-      deleteThread,
-      toggleTrackChangesForEveryone,
-      toggleTrackChangesForUser,
-      toggleTrackChangesForGuests,
-      setCollapsed,
-      setShouldCollapse,
-      setIsAddingComment,
-      setNavHeight,
-      setToolbarHeight,
-      setLayoutSuspended,
-      setUnsavedComment,
-    ]
-  )
-
-  return { values, updaterFns }
-}
-
-export default useAngularReviewPanelState

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

@@ -1,20 +0,0 @@
-import useAngularReviewPanelState from '@/features/source-editor/context/review-panel/hooks/use-angular-review-panel-state'
-import { FC } from 'react'
-import {
-  ReviewPanelUpdaterFnsContext,
-  ReviewPanelValueContext,
-} from './review-panel-context'
-
-const ReviewPanelProvider: FC = ({ children }) => {
-  const { values, updaterFns } = useAngularReviewPanelState()
-
-  return (
-    <ReviewPanelValueContext.Provider value={values}>
-      <ReviewPanelUpdaterFnsContext.Provider value={updaterFns}>
-        {children}
-      </ReviewPanelUpdaterFnsContext.Provider>
-    </ReviewPanelValueContext.Provider>
-  )
-}
-
-export default ReviewPanelProvider

+ 0 - 9
services/web/frontend/js/features/source-editor/controllers/grammarly-advert-controller.js

@@ -1,9 +0,0 @@
-import App from '../../../base'
-import { react2angular } from 'react2angular'
-import { rootContext } from '../../../shared/context/root-context'
-import GrammarlyAdvert from '../components/grammarly-advert'
-
-App.component(
-  'grammarlyAdvert',
-  react2angular(rootContext.use(GrammarlyAdvert))
-)

+ 0 - 6
services/web/frontend/js/features/source-editor/controllers/source-editor-controller.ts

@@ -1,6 +0,0 @@
-import { react2angular } from 'react2angular'
-import SourceEditor from '../components/source-editor'
-import App from '../../../base'
-import { rootContext } from '../../../shared/context/root-context'
-
-App.component('sourceEditor', react2angular(rootContext.use(SourceEditor), []))

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

@@ -26,7 +26,6 @@ import {
   setMetadata,
   setSyntaxValidation,
 } from '../extensions/language'
-import { useIdeContext } from '../../../shared/context/ide-context'
 import { restoreScrollPosition } from '../extensions/scroll-position'
 import { setEditable } from '../extensions/editable'
 import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
@@ -59,10 +58,9 @@ import grammarlyExtensionPresent from '@/shared/utils/grammarly'
 import { DocumentContainer } from '@/features/ide-react/editor/document-container'
 import { useLayoutContext } from '@/shared/context/layout-context'
 import { debugConsole } from '@/utils/debugging'
+import { useMetadataContext } from '@/features/ide-react/context/metadata-context'
 
 function useCodeMirrorScope(view: EditorView) {
-  const ide = useIdeContext()
-
   const { fileTreeData } = useFileTreeData()
 
   const [permissions] = useScopeValue<{ write: boolean }>('permissions')
@@ -74,6 +72,8 @@ function useCodeMirrorScope(view: EditorView) {
 
   const { reviewPanelOpen, miniReviewPanelVisible } = useLayoutContext()
 
+  const { metadata } = useMetadataContext()
+
   const [loadingThreads] = useScopeValue<boolean>('loadingThreads')
 
   const [currentDoc] = useScopeValue<DocumentContainer | null>(
@@ -211,22 +211,16 @@ function useCodeMirrorScope(view: EditorView) {
   // set the project metadata, mostly for use in autocomplete
   // TODO: read this data from the scope?
   const metadataRef = useRef({
-    documents: ide.metadataManager.metadata.state.documents,
+    documents: metadata.state.documents,
     references: references.keys,
     fileTreeData,
   })
 
   // listen to project metadata (docs + packages) updates
   useEffect(() => {
-    const listener = (event: Event) => {
-      metadataRef.current.documents = (
-        event as CustomEvent<Record<string, any>>
-      ).detail
-      view.dispatch(setMetadata(metadataRef.current))
-    }
-    window.addEventListener('project:metadata', listener)
-    return () => window.removeEventListener('project:metadata', listener)
-  }, [view])
+    metadataRef.current.documents = metadata.state.documents
+    view.dispatch(setMetadata(metadataRef.current))
+  }, [view, metadata.state.documents])
 
   // listen to project reference keys updates
   useEffect(() => {

+ 0 - 1
services/web/frontend/js/features/source-editor/ide/index.js

@@ -1 +0,0 @@
-import '../controllers/source-editor-controller'

+ 0 - 422
services/web/frontend/js/ide.js

@@ -1,422 +0,0 @@
-/* eslint-disable
-    camelcase,
-    max-len,
-    no-cond-assign,
-    no-return-assign,
-    no-unused-vars,
-    no-useless-escape,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * DS103: Rewrite code to no longer use __guard__
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-import App from './base'
-import FileTreeManager from './ide/file-tree/FileTreeManager'
-import LoadingManager from './ide/LoadingManager'
-import ConnectionManager from './ide/connection/ConnectionManager'
-import EditorManager from './ide/editor/EditorManager'
-import OnlineUsersManager from './ide/online-users/OnlineUsersManager'
-import PermissionsManager from './ide/permissions/PermissionsManager'
-import BinaryFilesManager from './ide/binary-files/BinaryFilesManager'
-import ReferencesManager from './ide/references/ReferencesManager'
-import MetadataManager from './ide/metadata/MetadataManager'
-import './ide/review-panel/ReviewPanelManager'
-import './ide/cobranding/CobrandingDataService'
-import './ide/chat/index'
-import './ide/file-view/index'
-import './ide/toolbar/index'
-import './ide/directives/layout'
-import './ide/directives/verticalResizablePanes'
-import './ide/services/ide'
-import './services/queued-http' // used in FileTreeManager
-import './main/event' // used in various controllers
-import './main/system-messages' // used in project/editor
-import '../../modules/modules-ide'
-import './features/source-editor/ide'
-import './shared/context/controllers/root-context-controller'
-import './features/editor-navigation-toolbar/controllers/editor-navigation-toolbar-controller'
-import './features/pdf-preview/controllers/pdf-preview-controller'
-import './features/share-project-modal/controllers/react-share-project-modal-controller'
-import './features/source-editor/controllers/grammarly-advert-controller'
-import './features/history/controllers/history-controller'
-import './features/editor-left-menu/controllers/editor-left-menu-controller'
-import './features/outline/controllers/outline-controller'
-import { cleanupServiceWorker } from './utils/service-worker-cleanup'
-import { reportCM6Perf } from './infrastructure/cm6-performance'
-import { debugConsole } from '@/utils/debugging'
-
-App.controller('IdeController', [
-  '$scope',
-  '$timeout',
-  'ide',
-  'localStorage',
-  'eventTracking',
-  'metadata',
-  'CobrandingDataService',
-  '$window',
-  function (
-    $scope,
-    $timeout,
-    ide,
-    localStorage,
-    eventTracking,
-    metadata,
-    CobrandingDataService,
-    $window
-  ) {
-    // Don't freak out if we're already in an apply callback
-    let err, pdfLayout, userAgent
-    $scope.$originalApply = $scope.$apply
-    $scope.$apply = function (fn) {
-      if (fn == null) {
-        fn = function () {}
-      }
-      const phase = this.$root.$$phase
-      if (phase === '$apply' || phase === '$digest') {
-        return fn()
-      } else {
-        return this.$originalApply(fn)
-      }
-    }
-
-    $scope.state = {
-      loading: true,
-      load_progress: 40,
-      error: null,
-    }
-    $scope.ui = {
-      leftMenuShown: false,
-      view: 'editor',
-      chatOpen: false,
-      pdfLayout: 'sideBySide',
-      pdfHidden: false,
-      pdfWidth: 0,
-      reviewPanelOpen: localStorage(`ui.reviewPanelOpen.${window.project_id}`),
-      miniReviewPanelVisible: false,
-      chatResizerSizeOpen: 7,
-      chatResizerSizeClosed: 0,
-    }
-    $scope.user = window.user
-
-    $scope.settings = window.userSettings
-    $scope.anonymous = window.anonymous
-    $scope.isTokenMember = window.isTokenMember
-    $scope.isRestrictedTokenMember = window.isRestrictedTokenMember
-
-    $scope.cobranding = {
-      isProjectCobranded: CobrandingDataService.isProjectCobranded(),
-      logoImgUrl: CobrandingDataService.getLogoImgUrl(),
-      submitBtnHtml: CobrandingDataService.getSubmitBtnHtml(),
-      brandVariationName: CobrandingDataService.getBrandVariationName(),
-      brandVariationHomeUrl: CobrandingDataService.getBrandVariationHomeUrl(),
-    }
-
-    $scope.chat = {}
-
-    ide.toggleReviewPanel = $scope.toggleReviewPanel = function () {
-      $scope.$applyAsync(() => {
-        if (!$scope.project.features.trackChangesVisible) {
-          return
-        }
-        $scope.ui.reviewPanelOpen = !$scope.ui.reviewPanelOpen
-        eventTracking.sendMB('rp-toggle-panel', {
-          value: $scope.ui.reviewPanelOpen,
-        })
-      })
-    }
-
-    $scope.$watch('ui.reviewPanelOpen', function (value) {
-      if (value != null) {
-        return localStorage(`ui.reviewPanelOpen.${window.project_id}`, value)
-      }
-    })
-
-    $scope.$on('layout:pdf:resize', function (_, layoutState) {
-      $scope.ui.pdfHidden = layoutState.east.initClosed
-      return ($scope.ui.pdfWidth = layoutState.east.size)
-    })
-
-    $scope.$watch('ui.view', function (newView, oldView) {
-      if (newView !== oldView) {
-        $scope.$broadcast('layout:flat-screen:toggle')
-      }
-      if (newView != null && newView !== 'editor' && newView !== 'pdf') {
-        eventTracking.sendMBOnce(`ide-open-view-${newView}-once`)
-      }
-    })
-
-    $scope.$watch('ui.chatOpen', function (isOpen) {
-      if (isOpen) {
-        eventTracking.sendMBOnce('ide-open-chat-once')
-      }
-    })
-
-    $scope.$watch('ui.leftMenuShown', function (isOpen) {
-      if (isOpen) {
-        eventTracking.sendMBOnce('ide-open-left-menu-once')
-      }
-    })
-
-    $scope.trackHover = feature => {
-      eventTracking.sendMBOnce(`ide-hover-${feature}-once`)
-    }
-    // End of tracking code.
-
-    window._ide = ide
-
-    ide.validFileRegex = '^[^*/]*$' // Don't allow * and /
-
-    ide.project_id = $scope.project_id = window.project_id
-    ide.$scope = $scope
-
-    ide.referencesSearchManager = new ReferencesManager(ide, $scope)
-    ide.loadingManager = new LoadingManager($scope)
-    ide.connectionManager = new ConnectionManager(ide, $scope)
-    ide.fileTreeManager = new FileTreeManager(ide, $scope)
-    ide.editorManager = new EditorManager(
-      ide,
-      $scope,
-      localStorage,
-      eventTracking
-    )
-    ide.onlineUsersManager = new OnlineUsersManager(ide, $scope)
-    ide.permissionsManager = new PermissionsManager(ide, $scope)
-    ide.binaryFilesManager = new BinaryFilesManager(ide, $scope)
-    ide.metadataManager = new MetadataManager(ide, $scope, metadata)
-
-    let inited = false
-    $scope.$on('project:joined', function () {
-      if (inited) {
-        return
-      }
-      inited = true
-      if (
-        __guard__(
-          $scope != null ? $scope.project : undefined,
-          x => x.deletedByExternalDataSource
-        )
-      ) {
-        ide.showGenericMessageModal(
-          'Project Renamed or Deleted',
-          `\
-This project has either been renamed or deleted by an external data source such as Dropbox.
-We don't want to delete your data on Overleaf, so this project still contains your history and collaborators.
-If the project has been renamed please look in your project list for a new project under the new name.\
-`
-        )
-      }
-      return $timeout(function () {
-        if ($scope.permissions.write) {
-          let _labelsInitialLoadDone
-          ide.metadataManager.loadProjectMetaFromServer()
-          return (_labelsInitialLoadDone = true)
-        }
-      }, 200)
-    })
-
-    // Count the first 'doc:opened' as a sign that the ide is loaded
-    // and broadcast a message. This is a good event to listen for
-    // if you want to wait until the ide is fully loaded and initialized
-    let _loaded = false
-    $scope.$on('doc:opened', function () {
-      if (_loaded) {
-        return
-      }
-      $scope.$broadcast('ide:loaded')
-      return (_loaded = true)
-    })
-
-    ide.editingSessionHeartbeat = () => {
-      eventTracking.editingSessionHeartbeat(() => {
-        const editorType = ide.editorManager.getEditorType()
-
-        const segmentation = {
-          editorType,
-        }
-
-        if (editorType === 'cm6' || editorType === 'cm6-rich-text') {
-          const cm6PerfData = reportCM6Perf()
-
-          // Ignore if no typing has happened
-          if (cm6PerfData.numberOfEntries > 0) {
-            const perfProps = [
-              'Max',
-              'Mean',
-              'Median',
-              'NinetyFifthPercentile',
-              'DocLength',
-              'NumberOfEntries',
-              'MaxUserEventsBetweenDomUpdates',
-              'Grammarly',
-              'SessionLength',
-              'Memory',
-              'Lags',
-              'NonLags',
-              'LongestLag',
-              'MeanLagsPerMeasure',
-              'MeanKeypressesPerMeasure',
-              'MeanKeypressPaint',
-              'LongTasks',
-              'Release',
-            ]
-
-            for (const prop of perfProps) {
-              const perfValue =
-                cm6PerfData[prop.charAt(0).toLowerCase() + prop.slice(1)]
-              if (perfValue !== null) {
-                segmentation['cm6Perf' + prop] = perfValue
-              }
-            }
-          }
-        }
-
-        return segmentation
-      })
-    }
-
-    $scope.$on('cursor:editor:update', () => {
-      ide.editingSessionHeartbeat()
-    })
-    $scope.$on('scroll:editor:update', () => {
-      ide.editingSessionHeartbeat()
-    })
-
-    angular.element($window).on('click', ide.editingSessionHeartbeat)
-
-    $scope.$on('$destroy', () =>
-      angular.element($window).off('click', ide.editingSessionHeartbeat)
-    )
-
-    const DARK_THEMES = [
-      'ambiance',
-      'chaos',
-      'clouds_midnight',
-      'cobalt',
-      'idle_fingers',
-      'merbivore',
-      'merbivore_soft',
-      'mono_industrial',
-      'monokai',
-      'pastel_on_dark',
-      'solarized_dark',
-      'terminal',
-      'tomorrow_night',
-      'tomorrow_night_blue',
-      'tomorrow_night_bright',
-      'tomorrow_night_eighties',
-      'twilight',
-      'vibrant_ink',
-    ]
-    $scope.darkTheme = false
-    // Listen for settings change from React
-    window.addEventListener('settings:change', event => {
-      $scope.darkTheme = DARK_THEMES.includes(event.detail.editorTheme)
-    })
-
-    ide.localStorage = localStorage
-
-    $scope.switchToFlatLayout = function (view) {
-      $scope.ui.pdfLayout = 'flat'
-      $scope.ui.view = view
-      return ide.localStorage('pdf.layout', 'flat')
-    }
-
-    $scope.switchToSideBySideLayout = function (view) {
-      $scope.ui.pdfLayout = 'sideBySide'
-      $scope.ui.view = view
-      return localStorage('pdf.layout', 'split')
-    }
-
-    if ((pdfLayout = localStorage('pdf.layout'))) {
-      if (pdfLayout === 'split') {
-        $scope.switchToSideBySideLayout()
-      }
-      if (pdfLayout === 'flat') {
-        $scope.switchToFlatLayout()
-      }
-    } else {
-      $scope.switchToSideBySideLayout()
-    }
-
-    // Update ui.pdfOpen when the layout changes.
-    // The east pane should open when the layout changes from "Editor only" or "PDF only" to "Editor & PDF".
-    $scope.$watch('ui.pdfLayout', value => {
-      $scope.ui.pdfOpen = value === 'sideBySide'
-    })
-
-    // Update ui.pdfLayout when the east pane is toggled.
-    // The layout should be set to "Editor & PDF" (sideBySide) when the east pane is opened, and "Editor only" (flat) when the east pane is closed.
-    $scope.$watch('ui.pdfOpen', value => {
-      $scope.ui.pdfLayout = value ? 'sideBySide' : 'flat'
-      if (value) {
-        window.dispatchEvent(new CustomEvent('ui:pdf-open'))
-      }
-    })
-
-    $scope.handleKeyDown = () => {
-      // unused?
-    }
-
-    // User can append ?ft=somefeature to url to activate a feature toggle
-    ide.featureToggle = __guard__(
-      __guard__(
-        typeof location !== 'undefined' && location !== null
-          ? location.search
-          : undefined,
-        x1 => x1.match(/^\?ft=(\w+)$/)
-      ),
-      x => x[1]
-    )
-
-    // Listen for editor:lint event from CM6 linter
-    window.addEventListener('editor:lint', event => {
-      $scope.hasLintingError = event.detail.hasLintingError
-    })
-
-    ide.socket.on('project:access:revoked', () => {
-      ide.showGenericMessageModal(
-        'Removed From Project',
-        'You have been removed from this project, and will no longer have access to it. You will be redirected to your project dashboard momentarily.'
-      )
-    })
-
-    return ide.socket.on('project:publicAccessLevel:changed', data => {
-      if (data.newAccessLevel != null) {
-        ide.$scope.project.publicAccesLevel = data.newAccessLevel
-        return $scope.$digest()
-      }
-    })
-  },
-])
-
-cleanupServiceWorker()
-
-angular.module('OverleafApp').config([
-  '$provide',
-  function ($provide) {
-    $provide.decorator('$browser', [
-      '$delegate',
-      function ($delegate) {
-        $delegate.onUrlChange = function () {}
-        $delegate.url = function () {
-          return ''
-        }
-        return $delegate
-      },
-    ])
-  },
-])
-
-export default angular.bootstrap(document.body, ['OverleafApp'])
-
-function __guard__(value, transform) {
-  return typeof value !== 'undefined' && value !== null
-    ? transform(value)
-    : undefined
-}

+ 0 - 37
services/web/frontend/js/ide/LoadingManager.js

@@ -1,37 +0,0 @@
-import i18n from '../i18n'
-
-// Control the editor loading screen. We want to show the loading screen until
-// both the websocket connection has been established (so that the editor is in
-// the correct state) and the translations have been loaded (so we don't see a
-// flash of untranslated text).
-class LoadingManager {
-  constructor($scope) {
-    this.$scope = $scope
-
-    const socketPromise = new Promise(resolve => {
-      this.resolveSocketPromise = resolve
-    })
-
-    Promise.all([socketPromise, i18n])
-      .then(() => {
-        this.$scope.$apply(() => {
-          this.$scope.state.load_progress = 100
-          this.$scope.state.loading = false
-          this.$scope.$emit('editor:loaded')
-        })
-      })
-      // Note: this will only catch errors in from i18n setup. ConnectionManager
-      // handles errors for the socket connection
-      .catch(() => {
-        this.$scope.$apply(() => {
-          this.$scope.state.error = 'Could not load translations.'
-        })
-      })
-  }
-
-  socketLoaded() {
-    this.resolveSocketPromise()
-  }
-}
-
-export default LoadingManager

+ 0 - 73
services/web/frontend/js/ide/binary-files/BinaryFilesManager.js

@@ -1,73 +0,0 @@
-/* eslint-disable
-    max-len,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-let BinaryFilesManager
-
-export default BinaryFilesManager = class BinaryFilesManager {
-  constructor(ide, $scope) {
-    this.ide = ide
-    this.$scope = $scope
-    this.$scope.$on('entity:selected', (event, entity) => {
-      if (this.$scope.ui.view !== 'track-changes' && entity.type === 'file') {
-        return this.openFile(entity)
-      } else if (entity.type === 'doc') {
-        return this.closeFile()
-      }
-    })
-  }
-
-  openFile(file) {
-    if (this.$scope.ui.view === 'editor') {
-      // store position before switching to binary view
-      this.$scope.$broadcast('store-doc-position')
-    }
-
-    this.ide.fileTreeManager.selectEntity(file)
-    if (this.$scope.ui.view !== 'history') {
-      this.$scope.ui.view = 'file'
-    }
-    this.$scope.openFile = null
-    this.$scope.$apply()
-    return window.setTimeout(
-      () => {
-        this.$scope.openFile = file
-
-        this.$scope.$apply()
-
-        this.$scope.$broadcast('file-view:file-opened')
-        window.dispatchEvent(new Event('file-view:file-opened'))
-      },
-      0,
-      this
-    )
-  }
-
-  openFileWithId(id) {
-    const entity = this.ide.fileTreeManager.findEntityById(id)
-    if (entity?.type === 'file') {
-      this.openFile(entity)
-    }
-  }
-
-  closeFile() {
-    return window.setTimeout(
-      () => {
-        this.$scope.openFile = null
-        if (this.$scope.ui.view !== 'history') {
-          this.$scope.ui.view = 'editor'
-        }
-        this.$scope.$apply()
-      },
-      0,
-      this
-    )
-  }
-}

+ 0 - 6
services/web/frontend/js/ide/chat/index.js

@@ -1,6 +0,0 @@
-import App from '../../base'
-import { rootContext } from '../../shared/context/root-context'
-import ChatPane from '../../features/chat/components/chat-pane'
-import { react2angular } from 'react2angular'
-
-App.component('chat', react2angular(rootContext.use(ChatPane)))

+ 0 - 59
services/web/frontend/js/ide/cobranding/CobrandingDataService.js

@@ -1,59 +0,0 @@
-/* eslint-disable
-    camelcase,
-    max-len,
-    no-return-assign,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-import App from '../../base'
-const _cobrandingData = window.brandVariation
-
-export default App.factory('CobrandingDataService', function () {
-  const isProjectCobranded = () => _cobrandingData != null
-
-  const getLogoImgUrl = () =>
-    _cobrandingData != null ? _cobrandingData.logo_url : undefined
-
-  const getSubmitBtnHtml = () =>
-    _cobrandingData != null ? _cobrandingData.submit_button_html : undefined
-
-  const getBrandVariationName = () =>
-    _cobrandingData != null ? _cobrandingData.name : undefined
-
-  const getBrandVariationHomeUrl = () =>
-    _cobrandingData != null ? _cobrandingData.home_url : undefined
-
-  const getPublishGuideHtml = () =>
-    _cobrandingData != null ? _cobrandingData.publish_guide_html : undefined
-
-  const getPartner = () =>
-    _cobrandingData != null ? _cobrandingData.partner : undefined
-
-  const hasBrandedMenu = () =>
-    _cobrandingData != null ? _cobrandingData.branded_menu : undefined
-
-  const getBrandId = () =>
-    _cobrandingData != null ? _cobrandingData.brand_id : undefined
-
-  const getBrandVariationId = () =>
-    _cobrandingData != null ? _cobrandingData.id : undefined
-
-  return {
-    isProjectCobranded,
-    getLogoImgUrl,
-    getSubmitBtnHtml,
-    getBrandVariationName,
-    getBrandVariationHomeUrl,
-    getPublishGuideHtml,
-    getPartner,
-    hasBrandedMenu,
-    getBrandId,
-    getBrandVariationId,
-  }
-})

+ 0 - 630
services/web/frontend/js/ide/connection/ConnectionManager.js

@@ -1,630 +0,0 @@
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS206: Consider reworking classes to avoid initClass
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-
-import SocketIoShim from './SocketIoShim'
-import getMeta from '../../utils/meta'
-import { debugConsole, debugging } from '@/utils/debugging'
-
-let ConnectionManager
-const ONEHOUR = 1000 * 60 * 60
-
-export default ConnectionManager = (function () {
-  ConnectionManager = class ConnectionManager {
-    static initClass() {
-      this.prototype.disconnectAfterMs = ONEHOUR * 24
-
-      this.prototype.lastUserAction = new Date()
-
-      this.prototype.MIN_RETRY_INTERVAL = 1000 // ms, rate limit on reconnects for user clicking "try now"
-      this.prototype.BACKGROUND_RETRY_INTERVAL = 5 * 1000
-
-      this.prototype.RECONNECT_GRACEFULLY_RETRY_INTERVAL = 5000 // ms
-      this.prototype.MAX_RECONNECT_GRACEFULLY_INTERVAL = 45 * 1000
-    }
-
-    constructor(ide, $scope) {
-      this.ide = ide
-      this.$scope = $scope
-      if (typeof window.io !== 'object') {
-        this.switchToWsFallbackIfPossible()
-        debugConsole.error(
-          'Socket.io javascript not loaded. Please check that the real-time service is running and accessible.'
-        )
-        this.ide.socket = SocketIoShim.stub()
-        this.$scope.$apply(() => {
-          return (this.$scope.state.error =
-            'Could not connect to websocket server :(')
-        })
-        return
-      }
-
-      setInterval(() => {
-        return this.disconnectIfInactive()
-      }, ONEHOUR)
-
-      // trigger a reconnect immediately if network comes back online
-      window.addEventListener('online', () => {
-        debugConsole.log('[online] browser notified online')
-        if (!this.connected) {
-          return this.tryReconnectWithRateLimit({ force: true })
-        }
-      })
-
-      this.userIsLeavingPage = false
-      window.addEventListener('beforeunload', () => {
-        this.userIsLeavingPage = true
-      }) // Don't return true or it will show a pop up
-
-      this.connected = false
-      this.userIsInactive = false
-      this.gracefullyReconnecting = false
-      this.shuttingDown = false
-
-      this.$scope.connection = {
-        debug: debugging,
-        reconnecting: false,
-        stillReconnecting: false,
-        // If we need to force everyone to reload the editor
-        forced_disconnect: false,
-        inactive_disconnect: false,
-        jobId: 0,
-      }
-
-      this.$scope.tryReconnectNow = () => {
-        // user manually requested reconnection via "Try now" button
-        return this.tryReconnectWithRateLimit({ force: true })
-      }
-
-      this.$scope.$on('cursor:editor:update', () => {
-        this.lastUserAction = new Date() // time of last edit
-        if (!this.connected) {
-          // user is editing, try to reconnect
-          return this.tryReconnectWithRateLimit()
-        }
-      })
-
-      document.querySelector('body').addEventListener('click', e => {
-        if (
-          !this.shuttingDown &&
-          !this.connected &&
-          e.target.id !== 'try-reconnect-now-button'
-        ) {
-          // user is editing, try to reconnect
-          return this.tryReconnectWithRateLimit()
-        }
-      })
-
-      // initial connection attempt
-      this.updateConnectionManagerState('connecting')
-      const parsedURL = new URL(
-        getMeta('ol-wsUrl') || '/socket.io',
-        window.origin
-      )
-      const query = new URLSearchParams({
-        projectId: getMeta('ol-project_id'),
-      }).toString()
-      this.ide.socket = SocketIoShim.connect(parsedURL.origin, {
-        resource: parsedURL.pathname.slice(1),
-        reconnect: false,
-        'connect timeout': 30 * 1000,
-        'force new connection': true,
-        query,
-      })
-
-      // handle network-level websocket errors (e.g. failed dns lookups)
-
-      let connectionAttempt = 1
-      const connectionErrorHandler = err => {
-        if (
-          window.wsRetryHandshake &&
-          connectionAttempt++ < window.wsRetryHandshake
-        ) {
-          return setTimeout(
-            () => this.ide.socket.socket.connect(),
-            // add jitter to spread reconnects
-            connectionAttempt * (1 + Math.random()) * 1000
-          )
-        }
-        this.updateConnectionManagerState('error')
-        debugConsole.log('socket.io error', err)
-        if (!this.switchToWsFallbackIfPossible()) {
-          this.connected = false
-          return this.$scope.$apply(() => {
-            return (this.$scope.state.error =
-              "Unable to connect, please view the <u><a href='/learn/Kb/Connection_problems'>connection problems guide</a></u> to fix the issue.")
-          })
-        }
-      }
-      this.ide.socket.on('error', connectionErrorHandler)
-
-      // The "connect" event is the first event we get back. It only
-      // indicates that the websocket is connected, we still need to
-      // pass authentication to join a project.
-
-      this.ide.socket.on('connect', () => {
-        // state should be 'connecting'...
-        // remove connection error handler when connected, avoid unwanted fallbacks
-        this.ide.socket.removeListener('error', connectionErrorHandler)
-        debugConsole.log('[socket.io connect] Connected')
-        this.updateConnectionManagerState('authenticating')
-      })
-
-      // The next event we should get is an authentication response
-      // from the server, either "joinProjectResponse" or "connectionRejected".
-
-      this.ide.socket.on(
-        'joinProjectResponse',
-        ({ publicId, project, permissionsLevel, protocolVersion }) => {
-          this.ide.socket.publicId = publicId
-          debugConsole.log('[socket.io bootstrap] ready for joinDoc')
-          this.connected = true
-          this.gracefullyReconnecting = false
-          this.ide.pushEvent('connected')
-          this.ide.pushEvent('joinProjectResponse')
-          this.updateConnectionManagerState('joining')
-
-          this.$scope.$apply(() => {
-            if (this.$scope.state.loading) {
-              this.$scope.state.load_progress = 70
-            }
-          })
-
-          this.handleJoinProjectResponse({
-            project,
-            permissionsLevel,
-            protocolVersion,
-          })
-        }
-      )
-
-      this.ide.socket.on('connectionRejected', err => {
-        // state should be 'authenticating'...
-        debugConsole.log(
-          '[socket.io connectionRejected] session not valid or other connection error'
-        )
-        // real-time sends a 'retry' message if the process was shutting down
-        // real-time sends TooManyRequests if joinProject was rate-limited.
-        if (err?.message === 'retry' || err?.code === 'TooManyRequests') {
-          return this.tryReconnectWithRateLimit()
-        }
-        if (err?.code === 'ProjectNotFound') {
-          // A stale browser tab tried to join a deleted project.
-          // Reloading the page will render a 404.
-          this.ide
-            .showGenericMessageModal(
-              'Project has been deleted',
-              'This project has been deleted by the owner.'
-            )
-            .result.then(() => location.reload(true))
-          return
-        }
-        // we have failed authentication, usually due to an invalid session cookie
-        return this.reportConnectionError(err)
-      })
-
-      // Alternatively the attempt to connect can fail completely, so
-      // we never get into the "connect" state.
-
-      this.ide.socket.on('connect_failed', () => {
-        this.updateConnectionManagerState('error')
-        this.connected = false
-        return this.$scope.$apply(() => {
-          return (this.$scope.state.error =
-            "Unable to connect, please view the <u><a href='/learn/Kb/Connection_problems'>connection problems guide</a></u> to fix the issue.")
-        })
-      })
-
-      // We can get a "disconnect" event at any point after the
-      // "connect" event.
-
-      this.ide.socket.on('disconnect', () => {
-        debugConsole.log('[socket.io disconnect] Disconnected')
-        this.connected = false
-        this.ide.pushEvent('disconnected')
-
-        if (!this.$scope.connection.state.match(/^waiting/)) {
-          if (
-            !this.$scope.connection.forced_disconnect &&
-            !this.userIsInactive &&
-            !this.shuttingDown
-          ) {
-            this.startAutoReconnectCountdown()
-          } else {
-            this.updateConnectionManagerState('inactive')
-          }
-        }
-      })
-
-      // Site administrators can send the forceDisconnect event to all users
-
-      this.ide.socket.on('forceDisconnect', (message, delay = 10) => {
-        this.updateConnectionManagerState('inactive')
-        this.shuttingDown = true // prevent reconnection attempts
-        this.$scope.$apply(() => {
-          this.$scope.permissions = { ...this.$scope.permissions, write: false }
-          return (this.$scope.connection.forced_disconnect = true)
-        })
-        // flush changes before disconnecting
-        this.ide.$scope.$broadcast('flush-changes')
-        window.setTimeout(() => this.ide.socket.disconnect(), 1000 * delay)
-        this.ide.showLockEditorMessageModal(
-          'Please wait',
-          `\
-We're performing maintenance on Overleaf and you need to wait a moment.
-Sorry for any inconvenience.
-The editor will refresh automatically in ${delay} seconds.\
-`
-        )
-        return setTimeout(() => location.reload(), delay * 1000)
-      })
-
-      this.ide.socket.on('reconnectGracefully', () => {
-        debugConsole.log('Reconnect gracefully')
-        this.reconnectGracefully()
-      })
-    }
-
-    switchToWsFallbackIfPossible() {
-      const search = new URLSearchParams(window.location.search)
-      if (getMeta('ol-wsUrl') && search.get('ws') !== 'fallback') {
-        // if we tried to boot from a custom real-time backend and failed,
-        // try reloading and falling back to the siteUrl
-        search.set('ws', 'fallback')
-        window.location.search = search.toString()
-        return true
-      }
-      return false
-    }
-
-    updateConnectionManagerState(state) {
-      this.$scope.$apply(() => {
-        this.$scope.connection.jobId += 1
-        const jobId = this.$scope.connection.jobId
-        debugConsole.log(
-          `[updateConnectionManagerState ${jobId}] from ${this.$scope.connection.state} to ${state}`
-        )
-        this.$scope.connection.state = state
-
-        this.$scope.connection.reconnecting = false
-        this.$scope.connection.stillReconnecting = false
-        this.$scope.connection.inactive_disconnect = false
-        this.$scope.connection.joining = false
-        this.$scope.connection.reconnection_countdown = null
-
-        if (state === 'connecting') {
-          // initial connection
-        } else if (state === 'reconnecting') {
-          // reconnection after a connection has failed
-          this.stopReconnectCountdownTimer()
-          this.$scope.connection.reconnecting = true
-          // if reconnecting takes more than 1s (it doesn't, usually) show the
-          // 'reconnecting...' warning
-          setTimeout(() => {
-            if (
-              this.$scope.connection.reconnecting &&
-              this.$scope.connection.jobId === jobId
-            ) {
-              this.$scope.connection.stillReconnecting = true
-            }
-          }, 1000)
-        } else if (state === 'reconnectFailed') {
-          // reconnect attempt failed
-        } else if (state === 'authenticating') {
-          // socket connection has been established, trying to authenticate
-        } else if (state === 'joining') {
-          // authenticated, joining project
-          this.$scope.connection.joining = true
-        } else if (state === 'ready') {
-          // project has been joined
-        } else if (state === 'waitingCountdown') {
-          // disconnected and waiting to reconnect via the countdown timer
-          this.stopReconnectCountdownTimer()
-        } else if (state === 'waitingGracefully') {
-          // disconnected and waiting to reconnect gracefully
-          this.stopReconnectCountdownTimer()
-        } else if (state === 'inactive') {
-          // disconnected and not trying to reconnect (inactive)
-        } else if (state === 'error') {
-          // something is wrong
-        } else {
-          debugConsole.log(
-            `[WARN] [updateConnectionManagerState ${jobId}] got unrecognised state ${state}`
-          )
-        }
-      })
-    }
-
-    expectConnectionManagerState(state, jobId) {
-      if (
-        this.$scope.connection.state === state &&
-        (!jobId || jobId === this.$scope.connection.jobId)
-      ) {
-        return true
-      }
-
-      debugConsole.log(
-        `[WARN] [state mismatch] expected state ${state}${
-          jobId ? '/' + jobId : ''
-        } when in ${this.$scope.connection.state}/${
-          this.$scope.connection.jobId
-        }`
-      )
-      return false
-    }
-
-    // Error reporting, which can reload the page if appropriate
-
-    reportConnectionError(err) {
-      debugConsole.log('[socket.io] reporting connection error')
-      this.updateConnectionManagerState('error')
-      if (
-        (err != null ? err.message : undefined) === 'not authorized' ||
-        (err != null ? err.message : undefined) === 'invalid session'
-      ) {
-        return (window.location = `/login?redir=${encodeURI(
-          window.location.pathname
-        )}`)
-      } else {
-        this.ide.socket.disconnect()
-        return this.ide.showGenericMessageModal(
-          'Something went wrong connecting',
-          `\
-Something went wrong connecting to your project. Please refresh if this continues to happen.\
-`
-        )
-      }
-    }
-
-    handleJoinProjectResponse({ project, permissionsLevel, protocolVersion }) {
-      if (
-        this.$scope.protocolVersion != null &&
-        this.$scope.protocolVersion !== protocolVersion
-      ) {
-        location.reload(true)
-      }
-
-      this.$scope.$apply(() => {
-        this.updateConnectionManagerState('ready')
-        this.$scope.protocolVersion = protocolVersion
-        const defaultProjectAttributes = { rootDoc_id: null }
-        this.$scope.project = { ...defaultProjectAttributes, ...project }
-        this.$scope.permissionsLevel = permissionsLevel
-        this.ide.loadingManager.socketLoaded()
-        window.dispatchEvent(
-          new CustomEvent('project:joined', { detail: this.$scope.project })
-        )
-        this.$scope.$broadcast('project:joined')
-      })
-    }
-
-    reconnectImmediately() {
-      this.disconnect()
-      return this.tryReconnect()
-    }
-
-    disconnect(options) {
-      if (options && options.permanent) {
-        debugConsole.log('[disconnect] shutting down ConnectionManager')
-        this.updateConnectionManagerState('inactive')
-        this.shuttingDown = true // prevent reconnection attempts
-      } else if (this.ide.socket.socket && !this.ide.socket.socket.connected) {
-        debugConsole.log(
-          '[socket.io] skipping disconnect because socket.io has not connected'
-        )
-        return
-      }
-      debugConsole.log('[socket.io] disconnecting client')
-      return this.ide.socket.disconnect()
-    }
-
-    startAutoReconnectCountdown() {
-      this.updateConnectionManagerState('waitingCountdown')
-      const connectionId = this.$scope.connection.jobId
-      let countdown
-      debugConsole.log('[ConnectionManager] starting autoreconnect countdown')
-      const twoMinutes = 2 * 60 * 1000
-      if (
-        this.lastUserAction != null &&
-        new Date() - this.lastUserAction > twoMinutes
-      ) {
-        // between 1 minute and 3 minutes
-        countdown = 60 + Math.floor(Math.random() * 120)
-      } else {
-        countdown = 3 + Math.floor(Math.random() * 7)
-      }
-
-      if (this.userIsLeavingPage) {
-        // user will have pressed refresh or back etc
-        return
-      }
-
-      this.$scope.$apply(() => {
-        this.$scope.connection.reconnecting = false
-        this.$scope.connection.stillReconnecting = false
-        this.$scope.connection.joining = false
-        this.$scope.connection.reconnection_countdown = countdown
-      })
-
-      setTimeout(() => {
-        if (!this.connected && !this.countdownTimeoutId) {
-          this.countdownTimeoutId = setTimeout(
-            () => this.decreaseCountdown(connectionId),
-            1000
-          )
-        }
-      }, 200)
-    }
-
-    stopReconnectCountdownTimer() {
-      // clear timeout and set to null so we know there is no countdown running
-      if (this.countdownTimeoutId != null) {
-        debugConsole.log(
-          '[ConnectionManager] cancelling existing reconnect timer'
-        )
-        clearTimeout(this.countdownTimeoutId)
-        this.countdownTimeoutId = null
-      }
-    }
-
-    decreaseCountdown(connectionId) {
-      this.countdownTimeoutId = null
-      if (this.$scope.connection.reconnection_countdown == null) {
-        return
-      }
-      if (
-        !this.expectConnectionManagerState('waitingCountdown', connectionId)
-      ) {
-        debugConsole.log(
-          `[ConnectionManager] Aborting stale countdown ${connectionId}`
-        )
-        return
-      }
-
-      debugConsole.log(
-        '[ConnectionManager] decreasing countdown',
-        this.$scope.connection.reconnection_countdown
-      )
-      this.$scope.$apply(() => {
-        this.$scope.connection.reconnection_countdown--
-      })
-
-      if (this.$scope.connection.reconnection_countdown <= 0) {
-        this.$scope.connection.reconnecting = false
-        this.$scope.$apply(() => {
-          this.tryReconnect()
-        })
-      } else {
-        this.countdownTimeoutId = setTimeout(
-          () => this.decreaseCountdown(connectionId),
-          1000
-        )
-      }
-    }
-
-    tryReconnect() {
-      debugConsole.log('[ConnectionManager] tryReconnect')
-      if (
-        this.connected ||
-        this.shuttingDown ||
-        this.$scope.connection.reconnecting
-      ) {
-        return
-      }
-      this.updateConnectionManagerState('reconnecting')
-      debugConsole.log('[ConnectionManager] Starting new connection')
-
-      const removeHandler = () => {
-        this.ide.socket.removeListener('error', handleFailure)
-        this.ide.socket.removeListener('connect', handleSuccess)
-      }
-      const handleFailure = () => {
-        debugConsole.log('[ConnectionManager] tryReconnect: failed')
-        removeHandler()
-        this.updateConnectionManagerState('reconnectFailed')
-        this.tryReconnectWithRateLimit({ force: true })
-      }
-      const handleSuccess = () => {
-        debugConsole.log('[ConnectionManager] tryReconnect: success')
-        removeHandler()
-      }
-      this.ide.socket.on('error', handleFailure)
-      this.ide.socket.on('connect', handleSuccess)
-
-      // use socket.io connect() here to make a single attempt, the
-      // reconnect() method makes multiple attempts
-      this.ide.socket.socket.connect()
-      // record the time of the last attempt to connect
-      this.lastConnectionAttempt = new Date()
-    }
-
-    tryReconnectWithRateLimit(options) {
-      // bail out if the reconnect is already in progress
-      if (this.$scope.connection.reconnecting || this.connected) {
-        return
-      }
-      // bail out if we are going to reconnect soon anyway
-      const reconnectingSoon =
-        this.$scope.connection.reconnection_countdown != null &&
-        this.$scope.connection.reconnection_countdown <= 5
-      const clickedTryNow = options != null ? options.force : undefined // user requested reconnection
-      if (reconnectingSoon && !clickedTryNow) {
-        return
-      }
-      // bail out if we tried reconnecting recently
-      const allowedInterval = clickedTryNow
-        ? this.MIN_RETRY_INTERVAL
-        : this.BACKGROUND_RETRY_INTERVAL
-      if (
-        this.lastConnectionAttempt != null &&
-        new Date() - this.lastConnectionAttempt < allowedInterval
-      ) {
-        if (this.$scope.connection.state !== 'waitingCountdown') {
-          this.startAutoReconnectCountdown()
-        }
-        return
-      }
-      this.tryReconnect()
-    }
-
-    disconnectIfInactive() {
-      this.userIsInactive =
-        new Date() - this.lastUserAction > this.disconnectAfterMs
-      if (this.userIsInactive && this.connected) {
-        this.disconnect()
-        return this.$scope.$apply(() => {
-          return (this.$scope.connection.inactive_disconnect = true)
-        }) // 5 minutes
-      }
-    }
-
-    reconnectGracefully(force) {
-      if (this.reconnectGracefullyStarted == null) {
-        this.reconnectGracefullyStarted = new Date()
-      } else {
-        if (!force) {
-          debugConsole.log(
-            '[reconnectGracefully] reconnection is already in process, so skipping'
-          )
-          return
-        }
-      }
-      const userIsInactive =
-        new Date() - this.lastUserAction >
-        this.RECONNECT_GRACEFULLY_RETRY_INTERVAL
-      const maxIntervalReached =
-        new Date() - this.reconnectGracefullyStarted >
-        this.MAX_RECONNECT_GRACEFULLY_INTERVAL
-      if (userIsInactive || maxIntervalReached) {
-        debugConsole.log(
-          "[reconnectGracefully] User didn't do anything for last 5 seconds, reconnecting"
-        )
-        this._reconnectGracefullyNow()
-      } else {
-        debugConsole.log(
-          '[reconnectGracefully] User is working, will try again in 5 seconds'
-        )
-        this.updateConnectionManagerState('waitingGracefully')
-        setTimeout(() => {
-          this.reconnectGracefully(true)
-        }, this.RECONNECT_GRACEFULLY_RETRY_INTERVAL)
-      }
-    }
-
-    _reconnectGracefullyNow() {
-      this.gracefullyReconnecting = true
-      this.reconnectGracefullyStarted = null
-      // Clear cookie so we don't go to the same backend server
-      $.cookie('SERVERID', '', { expires: -1, path: '/' })
-      return this.reconnectImmediately()
-    }
-  }
-  ConnectionManager.initClass()
-  return ConnectionManager
-})()

+ 0 - 236
services/web/frontend/js/ide/connection/EditorWatchdogManager.js

@@ -1,236 +0,0 @@
-/*
-
-  EditorWatchdogManager is used for end-to-end checks of edits.
-
-
-  The editor UI is backed by Ace and CodeMirrors, which in turn are connected
-   to ShareJs documents in the frontend.
-  Edits propagate from the editor to ShareJs and are send through socket.io
-   and real-time to document-updater.
-  In document-updater edits are integrated into the document history and
-   a confirmation/rejection is sent back to the frontend.
-
-  Along the way things can get lost.
-  We have certain safe-guards in place, but are still getting occasional
-   reports of lost edits.
-
-  EditorWatchdogManager is implementing the basis for end-to-end checks on
-   two levels:
-
-  - local/ShareJsDoc: edits that pass-by a ShareJs document shall get
-     acknowledged eventually.
-  - global: any edits made in the editor shall get acknowledged eventually,
-     independent for which ShareJs document (potentially none) sees it.
-
-  How does this work?
-  ===================
-
-  The global check is using a global EditorWatchdogManager that is available
-   via the angular factory 'ide'.
-  Local/ShareJsDoc level checks will connect to the global instance.
-
-  Each EditorWatchdogManager keeps track of the oldest un-acknowledged edit.
-  When ever a ShareJs document receives an acknowledgement event, a local
-   EditorWatchdogManager will see it and also notify the global instance about
-   it.
-  The next edit cycle will clear the oldest un-acknowledged timestamp in case
-   a new ack has arrived, otherwise it will bark loud! via the timeout handler.
-
-  Scenarios
-  =========
-
-  - User opens the CodeMirror editor
-    - attach global check to new CM instance
-    - detach Ace from the local EditorWatchdogManager
-    - when the frontend attaches the CM instance to ShareJs, we also
-       attach it to the local EditorWatchdogManager
-      - the internal attach process writes the document content to the editor,
-         which in turn emits 'change' events. These event need to be excluded
-         from the watchdog. EditorWatchdogManager.ignoreEditsFor takes care
-         of that.
-  - User opens the Ace editor (again)
-    - (attach global check to the Ace editor, only one copy of Ace is around)
-    - detach local EditorWatchdogManager from CM
-    - likewise with CM, attach Ace to the local EditorWatchdogManager
-  - User makes an edit
-    - the editor will emit a 'change' event
-    - the global EditorWatchdogManager will process it first
-    - the local EditorWatchdogManager will process it next
-  - Document-updater confirms an edit
-    - the local EditorWatchdogManager will process it first, it passes it on to
-    - the global EditorWatchdogManager will process it next
-
-  Time
-  ====
-
-  The delay between edits and acks is measured using a monotonic clock:
-   `performance.now()`.
-  It is agnostic to system clock changes in either direction and timezone
-   changes do not affect it as well.
-  Roughly speaking, it is initialized with `0` when the `window` context is
-   created, before our JS app boots.
-  As per canIUse.com and MDN `performance.now()` is available to all supported
-   Browsers, including IE11.
-  See also: https://caniuse.com/?search=performance.now
-  See also: https://developer.mozilla.org/en-US/docs/Web/API/Performance/now
- */
-
-import { debugConsole } from '@/utils/debugging'
-
-// TIMEOUT specifies the timeout for edits into a single ShareJsDoc.
-const TIMEOUT = 60 * 1000
-// GLOBAL_TIMEOUT specifies the timeout for edits into any ShareJSDoc.
-const GLOBAL_TIMEOUT = TIMEOUT
-// REPORT_EVERY specifies how often we send events/report errors.
-const REPORT_EVERY = 60 * 1000
-
-const SCOPE_LOCAL = 'ShareJsDoc'
-const SCOPE_GLOBAL = 'global'
-
-class Reporter {
-  constructor(onTimeoutHandler) {
-    this._onTimeoutHandler = onTimeoutHandler
-    this._lastReport = undefined
-    this._queue = []
-  }
-
-  _getMetaPreferLocal() {
-    for (const meta of this._queue) {
-      if (meta.scope === SCOPE_LOCAL) {
-        return meta
-      }
-    }
-    return this._queue.pop()
-  }
-
-  onTimeout(meta) {
-    // Collect all 'meta's for this update.
-    // global arrive before local ones, but we are eager to report local ones.
-    this._queue.push(meta)
-
-    setTimeout(() => {
-      // Another handler processed the 'meta' entry already
-      if (!this._queue.length) return
-
-      const maybeLocalMeta = this._getMetaPreferLocal()
-
-      // Discard other, newly arrived 'meta's
-      this._queue.length = 0
-
-      const now = Date.now()
-      // Do not flood the server with losing-edits events
-      const reportedRecently = now - this._lastReport < REPORT_EVERY
-      if (!reportedRecently) {
-        this._lastReport = now
-        this._onTimeoutHandler(maybeLocalMeta)
-      }
-    })
-  }
-}
-
-export default class EditorWatchdogManager {
-  constructor({ parent, onTimeoutHandler }) {
-    this.scope = parent ? SCOPE_LOCAL : SCOPE_GLOBAL
-    this.timeout = parent ? TIMEOUT : GLOBAL_TIMEOUT
-    this.parent = parent
-    if (parent) {
-      this.reporter = parent.reporter
-    } else {
-      this.reporter = new Reporter(onTimeoutHandler)
-    }
-
-    this.lastAck = null
-    this.lastUnackedEdit = null
-  }
-
-  onAck() {
-    this.lastAck = performance.now()
-
-    // bubble up to globalEditorWatchdogManager
-    if (this.parent) this.parent.onAck()
-  }
-
-  onEdit() {
-    // Use timestamps to track the high-water mark of unacked edits
-    const now = performance.now()
-
-    // Discard the last unacked edit if there are now newer acks
-    if (this.lastAck > this.lastUnackedEdit) {
-      this.lastUnackedEdit = null
-    }
-    // Start tracking for this keypress if we aren't already tracking an
-    //  unacked edit
-    if (!this.lastUnackedEdit) {
-      this.lastUnackedEdit = now
-    }
-
-    // Report an error if the last tracked edit hasn't been cleared by an
-    //  ack from the server after a long time
-    const delay = now - this.lastUnackedEdit
-    if (delay > this.timeout) {
-      const timeOrigin = Date.now() - now
-      const scope = this.scope
-      const lastAck = new Date(this.lastAck ? timeOrigin + this.lastAck : 0)
-      const lastUnackedEdit = new Date(timeOrigin + this.lastUnackedEdit)
-      const meta = { scope, delay, lastAck, lastUnackedEdit }
-      this._log('timedOut', meta)
-      this.reporter.onTimeout(meta)
-    }
-  }
-
-  attachToEditor(editorName, editor) {
-    let onChange
-    if (editorName === 'CM6') {
-      // Code Mirror 6
-      this._log('attach to editor', editorName)
-      onChange = (_editor, changeDescription) => {
-        if (changeDescription.origin === 'remote') return
-        if (!(changeDescription.removed || changeDescription.inserted)) return
-        this.onEdit()
-      }
-      editor.on('change', onChange)
-      const detachFromEditor = () => {
-        this._log('detach from editor', editorName)
-        editor.off('change', onChange)
-      }
-      return detachFromEditor
-    }
-    if (editorName === 'CM') {
-      // CM is passing the CM instance as first parameter, then the change.
-      onChange = (editor, change) => {
-        // Ignore remote changes.
-        if (change.origin === 'remote') return
-
-        // sharejs only looks at DEL or INSERT change events.
-        // NOTE: Keep in sync with sharejs.
-        if (!(change.removed || change.text)) return
-
-        this.onEdit()
-      }
-    } else {
-      // ACE is passing the change object as first parameter.
-      onChange = change => {
-        // Ignore remote changes.
-        if (change.origin === 'remote') return
-
-        // sharejs only looks at DEL or INSERT change events.
-        // NOTE: Keep in sync with sharejs.
-        if (!(change.action === 'remove' || change.action === 'insert')) return
-
-        this.onEdit()
-      }
-    }
-    this._log('attach to editor', editorName)
-    editor.on('change', onChange)
-
-    const detachFromEditor = () => {
-      this._log('detach from editor', editorName)
-      editor.off('change', onChange)
-    }
-    return detachFromEditor
-  }
-
-  _log() {
-    debugConsole.log(`[EditorWatchdogManager] ${this.scope}:`, ...arguments)
-  }
-}

+ 0 - 99
services/web/frontend/js/ide/directives/SafePath.js

@@ -1,99 +0,0 @@
-/* eslint-disable
-    max-len,
-    no-return-assign,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-// This file is shared between the frontend and server code of web, so that
-// filename validation is the same in both implementations.
-// The logic in all copies must be kept in sync:
-//   app/src/Features/Project/SafePath.js
-//   frontend/js/ide/directives/SafePath.js
-//   frontend/js/features/file-tree/util/safe-path.js
-
-let SafePath
-// eslint-disable-next-line prefer-regex-literals
-const BADCHAR_RX = new RegExp(
-  `\
-[\
-\\/\
-\\\\\
-\\*\
-\\u0000-\\u001F\
-\\u007F\
-\\u0080-\\u009F\
-\\uD800-\\uDFFF\
-]\
-`,
-  'g'
-)
-
-// eslint-disable-next-line prefer-regex-literals
-const BADFILE_RX = new RegExp(
-  `\
-(^\\.$)\
-|(^\\.\\.$)\
-|(^\\s+)\
-|(\\s+$)\
-`,
-  'g'
-)
-
-// Put a block on filenames which match javascript property names, as they
-// can cause exceptions where the code puts filenames into a hash. This is a
-// temporary workaround until the code in other places is made safe against
-// property names.
-//
-// The list of property names is taken from
-//   ['prototype'].concat(Object.getOwnPropertyNames(Object.prototype))
-// eslint-disable-next-line prefer-regex-literals
-const BLOCKEDFILE_RX = new RegExp(`\
-^(\
-prototype\
-|constructor\
-|toString\
-|toLocaleString\
-|valueOf\
-|hasOwnProperty\
-|isPrototypeOf\
-|propertyIsEnumerable\
-|__defineGetter__\
-|__lookupGetter__\
-|__defineSetter__\
-|__lookupSetter__\
-|__proto__\
-)$\
-`)
-
-const MAX_PATH = 1024 // Maximum path length, in characters. This is fairly arbitrary.
-
-export default SafePath = {
-  clean(filename) {
-    filename = filename.replace(BADCHAR_RX, '_')
-    // for BADFILE_RX replace any matches with an equal number of underscores
-    filename = filename.replace(BADFILE_RX, match =>
-      new Array(match.length + 1).join('_')
-    )
-    // replace blocked filenames 'prototype' with '@prototype'
-    filename = filename.replace(BLOCKEDFILE_RX, '@$1')
-    return filename
-  },
-
-  isCleanFilename(filename) {
-    return (
-      SafePath.isAllowedLength(filename) &&
-      !filename.match(BADCHAR_RX) &&
-      !filename.match(BADFILE_RX)
-    )
-  },
-
-  isAllowedLength(pathname) {
-    return pathname.length > 0 && pathname.length <= MAX_PATH
-  },
-}

+ 0 - 323
services/web/frontend/js/ide/directives/layout.js

@@ -1,323 +0,0 @@
-/* eslint-disable
-    max-len,
-    no-return-assign,
-    no-useless-escape,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-import App from '../../base'
-import _ from 'lodash'
-import '../../vendor/libs/jquery-layout'
-import '../../vendor/libs/jquery.ui.touch-punch'
-
-export default App.directive('layout', [
-  '$parse',
-  '$compile',
-  'ide',
-  function ($parse, $compile, ide) {
-    return {
-      compile() {
-        return {
-          pre(scope, element, attrs) {
-            let customTogglerEl, spacingClosed, spacingOpen, state
-            const name = attrs.layout
-
-            const { customTogglerPane } = attrs
-            const { customTogglerMsgWhenOpen } = attrs
-            const { customTogglerMsgWhenClosed } = attrs
-            const hasCustomToggler =
-              customTogglerPane != null &&
-              customTogglerMsgWhenOpen != null &&
-              customTogglerMsgWhenClosed != null
-
-            if (attrs.spacingOpen != null) {
-              spacingOpen = parseInt(attrs.spacingOpen, 10)
-            } else {
-              spacingOpen = 7
-            }
-
-            if (attrs.spacingClosed != null) {
-              spacingClosed = parseInt(attrs.spacingClosed, 10)
-            } else {
-              spacingClosed = 7
-            }
-
-            const options = {
-              spacing_open: spacingOpen,
-              spacing_closed: spacingClosed,
-              slidable: false,
-              enableCursorHotkey: false,
-              onopen: pane => {
-                return onPaneOpen(pane)
-              },
-              onclose: pane => {
-                return onPaneClose(pane)
-              },
-              onresize: () => {
-                return onInternalResize()
-              },
-              maskIframesOnResize: scope.$eval(
-                attrs.maskIframesOnResize || 'false'
-              ),
-              east: {
-                size: scope.$eval(attrs.initialSizeEast),
-                initClosed: scope.$eval(attrs.initClosedEast),
-              },
-              west: {
-                size: scope.$eval(attrs.initialSizeWest),
-                initClosed: scope.$eval(attrs.initClosedWest),
-              },
-            }
-
-            // Restore previously recorded state
-            if ((state = ide.localStorage(`layout.${name}`)) != null) {
-              if (state.east != null) {
-                if (
-                  attrs.minimumRestoreSizeEast == null ||
-                  (state.east.size >= attrs.minimumRestoreSizeEast &&
-                    !state.east.initClosed)
-                ) {
-                  options.east = state.east
-                }
-                options.east.initClosed = state.east.initClosed
-              }
-              if (state.west != null) {
-                if (
-                  attrs.minimumRestoreSizeWest == null ||
-                  (state.west.size >= attrs.minimumRestoreSizeWest &&
-                    !state.west.initClosed)
-                ) {
-                  options.west = state.west
-                }
-                // NOTE: disabled so that the file tree re-opens on page load
-                // options.west.initClosed = state.west.initClosed
-              }
-            }
-
-            options.east.resizerCursor = 'ew-resize'
-            options.west.resizerCursor = 'ew-resize'
-
-            function repositionControls() {
-              state = layout.readState()
-              if (state.east != null) {
-                const controls = element.find('> .ui-layout-resizer-controls')
-                if (state.east.initClosed) {
-                  return controls.hide()
-                } else {
-                  controls.show()
-                  return controls.css({
-                    right: state.east.size,
-                  })
-                }
-              }
-            }
-
-            function repositionCustomToggler() {
-              if (customTogglerEl == null) {
-                return
-              }
-              state = layout.readState()
-              const positionAnchor =
-                customTogglerPane === 'east' ? 'right' : 'left'
-              const paneState = state[customTogglerPane]
-              if (paneState != null) {
-                return customTogglerEl.css(
-                  positionAnchor,
-                  paneState.initClosed ? 0 : paneState.size
-                )
-              }
-            }
-
-            function resetOpenStates() {
-              state = layout.readState()
-              if (attrs.openEast != null && state.east != null) {
-                const openEast = $parse(attrs.openEast)
-                return openEast.assign(scope, !state.east.initClosed)
-              }
-            }
-
-            // Someone moved the resizer
-            function onInternalResize() {
-              state = layout.readState()
-              scope.$broadcast(`layout:${name}:resize`, state)
-              repositionControls()
-              if (hasCustomToggler) {
-                repositionCustomToggler()
-              }
-              return resetOpenStates()
-            }
-
-            let oldWidth = element.width()
-            // Something resized our parent element
-            const onExternalResize = function () {
-              if (
-                attrs.resizeProportionally != null &&
-                scope.$eval(attrs.resizeProportionally)
-              ) {
-                const eastState = layout.readState().east
-                if (eastState != null) {
-                  const currentWidth = element.width()
-                  if (currentWidth > 0) {
-                    const newInternalWidth =
-                      (eastState.size / oldWidth) * currentWidth
-                    oldWidth = currentWidth
-                    layout.sizePane('east', newInternalWidth)
-                  }
-                  return
-                }
-              }
-
-              ide.$timeout(() => {
-                layout.resizeAll()
-              })
-            }
-
-            const layout = element.layout(options)
-            layout.resizeAll()
-
-            if (attrs.resizeOn != null) {
-              for (const event of Array.from(attrs.resizeOn.split(','))) {
-                scope.$on(event, () => onExternalResize())
-              }
-            }
-
-            if (hasCustomToggler) {
-              state = layout.readState()
-              const customTogglerScope = scope.$new()
-
-              customTogglerScope.isOpen = true
-              customTogglerScope.isVisible = true
-
-              if (
-                (state[customTogglerPane] != null
-                  ? state[customTogglerPane].initClosed
-                  : undefined) === true
-              ) {
-                customTogglerScope.isOpen = false
-              }
-
-              customTogglerScope.tooltipMsgWhenOpen = customTogglerMsgWhenOpen
-              customTogglerScope.tooltipMsgWhenClosed =
-                customTogglerMsgWhenClosed
-
-              customTogglerScope.tooltipPlacement =
-                customTogglerPane === 'east' ? 'left' : 'right'
-              customTogglerScope.handleClick = function () {
-                layout.toggle(customTogglerPane)
-                return repositionCustomToggler()
-              }
-              customTogglerEl = $compile(`\
-<a href \
-ng-show=\"isVisible\" \
-class=\"custom-toggler ${`custom-toggler-${customTogglerPane}`}\" \
-ng-class=\"isOpen ? 'custom-toggler-open' : 'custom-toggler-closed'\" \
-tooltip=\"{{ isOpen ? tooltipMsgWhenOpen : tooltipMsgWhenClosed }}\" \
-tooltip-placement=\"{{ tooltipPlacement }}\" \
-ng-click=\"handleClick()\" \
-aria-label=\"{{ isOpen ? tooltipMsgWhenOpen : tooltipMsgWhenClosed }}\">\
-`)(customTogglerScope)
-              element.append(customTogglerEl)
-            }
-
-            function onPaneOpen(pane) {
-              if (!hasCustomToggler || pane !== customTogglerPane) {
-                return
-              }
-              return customTogglerEl
-                .scope()
-                .$applyAsync(() => (customTogglerEl.scope().isOpen = true))
-            }
-
-            function onPaneClose(pane) {
-              if (!hasCustomToggler || pane !== customTogglerPane) {
-                return
-              }
-              return customTogglerEl
-                .scope()
-                .$applyAsync(() => (customTogglerEl.scope().isOpen = false))
-            }
-
-            // Ensure editor resizes after loading. This is to handle the case where
-            // the window has been resized while the editor is loading
-            scope.$on('editor:loaded', () => {
-              ide.$timeout(() => layout.resizeAll())
-            })
-
-            // Save state when exiting
-            $(window).unload(() => {
-              // Save only the state properties for the current layout, ignoring sublayouts inside it.
-              // If we save sublayouts state (`children`), the layout library will use it when
-              // initializing. This raises errors when the sublayout elements aren't available (due to
-              // being loaded at init or just not existing for the current project/user).
-              const stateToSave = _.mapValues(layout.readState(), pane =>
-                _.omit(pane, 'children')
-              )
-              ide.localStorage(`layout.${name}`, stateToSave)
-            })
-
-            if (attrs.openEast != null) {
-              scope.$watch(attrs.openEast, function (value, oldValue) {
-                if (value != null && value !== oldValue) {
-                  if (value) {
-                    layout.open('east')
-                  } else {
-                    layout.close('east')
-                  }
-                  if (hasCustomToggler && customTogglerPane === 'east') {
-                    repositionCustomToggler()
-                    customTogglerEl.scope().$applyAsync(function () {
-                      customTogglerEl.scope().isOpen = value
-                    })
-                  }
-                }
-                return setTimeout(() => scope.$digest(), 0)
-              })
-            }
-
-            if (attrs.allowOverflowOn != null) {
-              const overflowPane = scope.$eval(attrs.allowOverflowOn)
-              const overflowPaneEl = layout.panes[overflowPane]
-              // Set the panel as overflowing (gives it higher z-index and sets overflow rules)
-              layout.allowOverflow(overflowPane)
-              // Read the given z-index value and increment it, so that it's higher than synctex controls.
-              const overflowPaneZVal = overflowPaneEl.zIndex()
-              overflowPaneEl.css('z-index', overflowPaneZVal + 1)
-            }
-
-            resetOpenStates()
-            onInternalResize()
-
-            if (attrs.layoutDisabled != null) {
-              return scope.$watch(attrs.layoutDisabled, function (value) {
-                if (value) {
-                  layout.hide('east')
-                } else {
-                  layout.show('east')
-                }
-                if (hasCustomToggler) {
-                  return customTogglerEl.scope().$applyAsync(function () {
-                    customTogglerEl.scope().isOpen = !value
-                    return (customTogglerEl.scope().isVisible = !value)
-                  })
-                }
-              })
-            }
-          },
-
-          post(scope, element, attrs) {
-            const name = attrs.layout
-            const state = element.layout().readState()
-            return scope.$broadcast(`layout:${name}:linked`, state)
-          },
-        }
-      },
-    }
-  },
-])

+ 0 - 139
services/web/frontend/js/ide/directives/verticalResizablePanes.js

@@ -1,139 +0,0 @@
-import App from '../../base'
-
-export default App.directive('verticalResizablePanes', [
-  'localStorage',
-  'ide',
-  function (localStorage, ide) {
-    return {
-      restrict: 'A',
-      link(scope, element, attrs) {
-        const name = attrs.verticalResizablePanes
-        const minSize = scope.$eval(attrs.verticalResizablePanesMinSize)
-        const maxSize = scope.$eval(attrs.verticalResizablePanesMaxSize)
-        const defaultSize = scope.$eval(attrs.verticalResizablePanesDefaultSize)
-        let storedSize = null
-        let manualResizeIncoming = false
-
-        if (name) {
-          const storageKey = `vertical-resizable:${name}:south-size`
-          storedSize = localStorage(storageKey)
-          $(window).unload(() => {
-            if (storedSize) {
-              localStorage(storageKey, storedSize)
-            }
-          })
-        }
-
-        const layoutOptions = {
-          center: {
-            paneSelector: '[vertical-resizable-top]',
-            paneClass: 'vertical-resizable-top',
-            size: 'auto',
-          },
-          south: {
-            paneSelector: '[vertical-resizable-bottom]',
-            paneClass: 'vertical-resizable-bottom',
-            resizerClass: 'vertical-resizable-resizer',
-            resizerCursor: 'ns-resize',
-            size: 'auto',
-            resizable: true,
-            closable: false,
-            slidable: false,
-            spacing_open: 6,
-            spacing_closed: 6,
-            maxSize: '75%',
-          },
-        }
-
-        const toggledExternally =
-          attrs.verticalResizablePanesToggledExternallyOn
-        const hiddenExternally = attrs.verticalResizablePanesHiddenExternallyOn
-        const hiddenInitially = attrs.verticalResizablePanesHiddenInitially
-        const resizeOn = attrs.verticalResizablePanesResizeOn
-        const resizerDisabledClass = `${layoutOptions.south.resizerClass}-disabled`
-
-        function enableResizer() {
-          if (layoutHandle.resizers && layoutHandle.resizers.south) {
-            layoutHandle.resizers.south.removeClass(resizerDisabledClass)
-          }
-        }
-
-        function disableResizer() {
-          if (layoutHandle.resizers && layoutHandle.resizers.south) {
-            layoutHandle.resizers.south.addClass(resizerDisabledClass)
-          }
-        }
-
-        function handleDragEnd() {
-          manualResizeIncoming = true
-        }
-
-        function handleResize(paneName, paneEl, paneState) {
-          if (manualResizeIncoming) {
-            storedSize = paneState.size
-          }
-          manualResizeIncoming = false
-        }
-
-        if (toggledExternally) {
-          scope.$on(toggledExternally, (e, open) => {
-            if (open) {
-              enableResizer()
-              layoutHandle.sizePane(
-                'south',
-                storedSize ?? defaultSize ?? 'auto'
-              )
-            } else {
-              disableResizer()
-              layoutHandle.sizePane('south', 'auto')
-            }
-          })
-        }
-
-        if (hiddenExternally) {
-          ide.$scope.$on(hiddenExternally, (e, open) => {
-            if (open) {
-              layoutHandle.show('south')
-            } else {
-              layoutHandle.hide('south')
-            }
-          })
-        }
-
-        if (resizeOn) {
-          ide.$scope.$on(resizeOn, () => {
-            ide.$timeout(() => {
-              layoutHandle.resizeAll()
-            })
-          })
-        }
-
-        if (maxSize) {
-          layoutOptions.south.maxSize = maxSize
-        }
-
-        if (minSize) {
-          layoutOptions.south.minSize = minSize
-        }
-
-        if (defaultSize) {
-          layoutOptions.south.size = defaultSize
-        }
-
-        // The `drag` event fires only when the user manually resizes the panes; the `resize` event fires even when
-        // the layout library internally resizes itself. In order to get explicit user-initiated resizes, we need to
-        // listen to `drag` events. However, when the `drag` event fires, the panes aren't yet finished sizing so we
-        // get the pane size *before* the resize happens. We do get the correct size in the next `resize` event.
-        // The solution to work around this is to set up a flag in `drag` events which tells the next `resize` event
-        // that it was user-initiated (therefore, storing the value).
-        layoutOptions.south.ondrag_end = handleDragEnd
-        layoutOptions.south.onresize = handleResize
-
-        const layoutHandle = element.layout(layoutOptions)
-        if (hiddenInitially === 'true') {
-          layoutHandle.hide('south')
-        }
-      },
-    }
-  },
-])

+ 0 - 781
services/web/frontend/js/ide/editor/Document.js

@@ -1,781 +0,0 @@
-/* eslint-disable
-    camelcase,
-    n/handle-callback-err,
-    max-len,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS001: Remove Babel/TypeScript constructor workaround
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * DS103: Rewrite code to no longer use __guard__
- * DS205: Consider reworking code to avoid use of IIFEs
- * DS206: Consider reworking classes to avoid initClass
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-import RangesTracker from '@overleaf/ranges-tracker'
-import EventEmitter from '../../utils/EventEmitter'
-import ShareJsDoc from './ShareJsDoc'
-import { debugConsole } from '@/utils/debugging'
-let Document
-
-export default Document = (function () {
-  Document = class Document extends EventEmitter {
-    static initClass() {
-      this.prototype.MAX_PENDING_OP_SIZE = 64
-    }
-
-    static getDocument(ide, doc_id) {
-      if (!this.openDocs) {
-        this.openDocs = {}
-      }
-      // Try to clean up existing docs before reopening them. If the doc has no
-      // buffered ops then it will be deleted by _cleanup() and a new instance
-      // of the document created below. This prevents us trying to follow the
-      // joinDoc:existing code path on an existing doc that doesn't have any
-      // local changes and getting an error if its version is too old.
-      if (this.openDocs[doc_id]) {
-        debugConsole.log(
-          `[getDocument] Cleaning up existing document instance for ${doc_id}`
-        )
-        this.openDocs[doc_id]._cleanUp()
-      }
-      if (this.openDocs[doc_id] == null) {
-        debugConsole.log(
-          `[getDocument] Creating new document instance for ${doc_id}`
-        )
-        this.openDocs[doc_id] = new Document(ide, doc_id)
-      } else {
-        debugConsole.log(
-          `[getDocument] Returning existing document instance for ${doc_id}`
-        )
-      }
-      return this.openDocs[doc_id]
-    }
-
-    static hasUnsavedChanges() {
-      const object = this.openDocs || {}
-      for (const doc_id in object) {
-        const doc = object[doc_id]
-        if (doc.hasBufferedOps()) {
-          return true
-        }
-      }
-      return false
-    }
-
-    static flushAll() {
-      return (() => {
-        const result = []
-        for (const doc_id in this.openDocs) {
-          const doc = this.openDocs[doc_id]
-          result.push(doc.flush())
-        }
-        return result
-      })()
-    }
-
-    constructor(ide, doc_id) {
-      super()
-      this.ide = ide
-      this.doc_id = doc_id
-      this.connected = this.ide.socket.socket.connected
-      this.joined = false
-      this.wantToBeJoined = false
-      this._checkCM6Consistency = () => this._checkConsistency(this.cm6)
-      this._bindToEditorEvents()
-      this._bindToSocketEvents()
-    }
-
-    editorType() {
-      if (this.cm6) {
-        return 'cm6'
-      } else {
-        return null
-      }
-    }
-
-    attachToCM6(cm6) {
-      this.cm6 = cm6
-      if (this.doc != null) {
-        this.doc.attachToCM6(this.cm6)
-      }
-      if (this.cm6 != null) {
-        this.cm6.on('change', this._checkCM6Consistency)
-      }
-      return this.ide.$scope.$emit('document:opened', this.doc)
-    }
-
-    detachFromCM6() {
-      if (this.doc != null) {
-        this.doc.detachFromCM6()
-      }
-      if (this.cm6 != null) {
-        this.cm6.off('change', this._checkCM6Consistency)
-      }
-      delete this.cm6
-      this.clearChaosMonkey()
-      return this.ide.$scope.$emit('document:closed', this.doc)
-    }
-
-    submitOp(...args) {
-      return this.doc != null
-        ? this.doc.submitOp(...Array.from(args || []))
-        : undefined
-    }
-
-    _checkConsistency(editor) {
-      // We've been seeing a lot of errors when I think there shouldn't be
-      // any, which may be related to this check happening before the change is
-      // applied. If we use a timeout, hopefully we can reduce this.
-      return setTimeout(() => {
-        const editorValue = editor != null ? editor.getValue() : undefined
-        const sharejsValue =
-          this.doc != null ? this.doc.getSnapshot() : undefined
-        if (editorValue !== sharejsValue) {
-          return this._onError(
-            new Error('Editor text does not match server text'),
-            {},
-            editorValue
-          )
-        }
-      }, 0)
-    }
-
-    getSnapshot() {
-      return this.doc != null ? this.doc.getSnapshot() : undefined
-    }
-
-    getType() {
-      return this.doc != null ? this.doc.getType() : undefined
-    }
-
-    getInflightOp() {
-      return this.doc != null ? this.doc.getInflightOp() : undefined
-    }
-
-    getPendingOp() {
-      return this.doc != null ? this.doc.getPendingOp() : undefined
-    }
-
-    getRecentAck() {
-      return this.doc != null ? this.doc.getRecentAck() : undefined
-    }
-
-    getOpSize(op) {
-      return this.doc != null ? this.doc.getOpSize(op) : undefined
-    }
-
-    hasBufferedOps() {
-      return this.doc != null ? this.doc.hasBufferedOps() : undefined
-    }
-
-    setTrackingChanges(track_changes) {
-      return (this.doc.track_changes = track_changes)
-    }
-
-    getTrackingChanges() {
-      return !!this.doc.track_changes
-    }
-
-    setTrackChangesIdSeeds(id_seeds) {
-      return (this.doc.track_changes_id_seeds = id_seeds)
-    }
-
-    _bindToSocketEvents() {
-      this._onUpdateAppliedHandler = update => this._onUpdateApplied(update)
-      this.ide.socket.on('otUpdateApplied', this._onUpdateAppliedHandler)
-      this._onErrorHandler = (error, message) => {
-        // 'otUpdateError' are emitted per doc socket.io room, hence we can be
-        //  sure that message.doc_id exists.
-        if (message.doc_id !== this.doc_id) {
-          // This error is for another doc. Do not action it. We could open
-          //  a modal that has the wrong context on it.
-          return
-        }
-        this._onError(error, message)
-      }
-      this.ide.socket.on('otUpdateError', this._onErrorHandler)
-      this._onDisconnectHandler = error => this._onDisconnect(error)
-      return this.ide.socket.on('disconnect', this._onDisconnectHandler)
-    }
-
-    _bindToEditorEvents() {
-      const onReconnectHandler = update => {
-        return this._onReconnect(update)
-      }
-      return (this._unsubscribeReconnectHandler = this.ide.$scope.$on(
-        'project:joined',
-        onReconnectHandler
-      ))
-    }
-
-    _unBindFromEditorEvents() {
-      return this._unsubscribeReconnectHandler()
-    }
-
-    _unBindFromSocketEvents() {
-      this.ide.socket.removeListener(
-        'otUpdateApplied',
-        this._onUpdateAppliedHandler
-      )
-      this.ide.socket.removeListener('otUpdateError', this._onErrorHandler)
-      return this.ide.socket.removeListener(
-        'disconnect',
-        this._onDisconnectHandler
-      )
-    }
-
-    leaveAndCleanUp(cb) {
-      return this.leave(error => {
-        this._cleanUp()
-        if (cb) cb(error)
-      })
-    }
-
-    join(callback) {
-      if (callback == null) {
-        callback = function () {}
-      }
-      this.wantToBeJoined = true
-      this._cancelLeave()
-      if (this.connected) {
-        return this._joinDoc(callback)
-      } else {
-        if (!this._joinCallbacks) {
-          this._joinCallbacks = []
-        }
-        return this._joinCallbacks.push(callback)
-      }
-    }
-
-    leave(callback) {
-      if (callback == null) {
-        callback = function () {}
-      }
-      this.flush() // force an immediate flush when leaving document
-      this.wantToBeJoined = false
-      this._cancelJoin()
-      if (this.doc != null && this.doc.hasBufferedOps()) {
-        debugConsole.log(
-          '[leave] Doc has buffered ops, pushing callback for later'
-        )
-        if (!this._leaveCallbacks) {
-          this._leaveCallbacks = []
-        }
-        return this._leaveCallbacks.push(callback)
-      } else if (!this.connected) {
-        debugConsole.log('[leave] Not connected, returning now')
-        return callback()
-      } else {
-        debugConsole.log('[leave] Leaving now')
-        return this._leaveDoc(callback)
-      }
-    }
-
-    flush() {
-      return this.doc != null ? this.doc.flushPendingOps() : undefined
-    }
-
-    chaosMonkey(line, char) {
-      if (line == null) {
-        line = 0
-      }
-      if (char == null) {
-        char = 'a'
-      }
-      const orig = char
-      let copy = null
-      let pos = 0
-      const timer = () => {
-        if (copy == null || !copy.length) {
-          copy = orig.slice() + ' ' + new Date() + '\n'
-          line += Math.random() > 0.1 ? 1 : -2
-          if (line < 0) {
-            line = 0
-          }
-          pos = 0
-        }
-        char = copy[0]
-        copy = copy.slice(1)
-        if (this.cm6) {
-          this.cm6.view.dispatch({
-            changes: {
-              from: Math.min(pos, this.cm6.view.state.doc.length),
-              insert: char,
-            },
-          })
-        }
-        pos += 1
-        return (this._cm = setTimeout(
-          timer,
-          100 + (Math.random() < 0.1 ? 1000 : 0)
-        ))
-      }
-      return (this._cm = timer())
-    }
-
-    clearChaosMonkey() {
-      const timer = this._cm
-      if (timer) {
-        delete this._cm
-        return clearTimeout(timer)
-      }
-    }
-
-    pollSavedStatus() {
-      // returns false if doc has ops waiting to be acknowledged or
-      // sent that haven't changed since the last time we checked.
-      // Otherwise returns true.
-      let saved
-      const inflightOp = this.getInflightOp()
-      const pendingOp = this.getPendingOp()
-      const recentAck = this.getRecentAck()
-      const pendingOpSize = pendingOp != null && this.getOpSize(pendingOp)
-      if (inflightOp == null && pendingOp == null) {
-        // there's nothing going on, this is ok.
-        saved = true
-        debugConsole.log('[pollSavedStatus] no inflight or pending ops')
-      } else if (inflightOp != null && inflightOp === this.oldInflightOp) {
-        // The same inflight op has been sitting unacked since we
-        // last checked, this is bad.
-        saved = false
-        debugConsole.log('[pollSavedStatus] inflight op is same as before')
-      } else if (
-        pendingOp != null &&
-        recentAck &&
-        pendingOpSize < this.MAX_PENDING_OP_SIZE
-      ) {
-        // There is an op waiting to go to server but it is small and
-        // within the flushDelay, this is ok for now.
-        saved = true
-        debugConsole.log(
-          '[pollSavedStatus] pending op (small with recent ack) assume ok',
-          pendingOp,
-          pendingOpSize
-        )
-      } else {
-        // In any other situation, assume the document is unsaved.
-        saved = false
-        debugConsole.log(
-          `[pollSavedStatus] assuming not saved (inflightOp?: ${
-            inflightOp != null
-          }, pendingOp?: ${pendingOp != null})`
-        )
-      }
-
-      this.oldInflightOp = inflightOp
-      return saved
-    }
-
-    _cancelLeave() {
-      if (this._leaveCallbacks != null) {
-        return delete this._leaveCallbacks
-      }
-    }
-
-    _cancelJoin() {
-      if (this._joinCallbacks != null) {
-        return delete this._joinCallbacks
-      }
-    }
-
-    _onUpdateApplied(update) {
-      this.ide.pushEvent('received-update', {
-        doc_id: this.doc_id,
-        remote_doc_id: update != null ? update.doc : undefined,
-        wantToBeJoined: this.wantToBeJoined,
-        update,
-        hasDoc: this.doc != null,
-      })
-
-      if (
-        window.disconnectOnAck != null &&
-        Math.random() < window.disconnectOnAck
-      ) {
-        debugConsole.log('Disconnecting on ack', update)
-        window._ide.socket.socket.disconnect()
-        // Pretend we never received the ack
-        return
-      }
-
-      if (window.dropAcks != null && Math.random() < window.dropAcks) {
-        if (update.op == null) {
-          // Only drop our own acks, not collaborator updates
-          debugConsole.log('Simulating a lost ack', update)
-          return
-        }
-      }
-
-      if (
-        (update != null ? update.doc : undefined) === this.doc_id &&
-        this.doc != null
-      ) {
-        this.ide.pushEvent('received-update:processing', {
-          update,
-        })
-        // FIXME: change this back to processUpdateFromServer when redis fixed
-        this.doc.processUpdateFromServerInOrder(update)
-
-        if (!this.wantToBeJoined) {
-          return this.leave()
-        }
-      }
-    }
-
-    _onDisconnect() {
-      debugConsole.log('[onDisconnect] disconnecting')
-      this.connected = false
-      this.joined = false
-      return this.doc != null
-        ? this.doc.updateConnectionState('disconnected')
-        : undefined
-    }
-
-    _onReconnect() {
-      debugConsole.log('[onReconnect] reconnected (joined project)')
-      this.ide.pushEvent('reconnected:afterJoinProject')
-
-      this.connected = true
-      if (
-        this.wantToBeJoined ||
-        (this.doc != null ? this.doc.hasBufferedOps() : undefined)
-      ) {
-        debugConsole.log(
-          `[onReconnect] Rejoining (wantToBeJoined: ${
-            this.wantToBeJoined
-          } OR hasBufferedOps: ${
-            this.doc != null ? this.doc.hasBufferedOps() : undefined
-          })`
-        )
-        return this._joinDoc(error => {
-          if (error != null) {
-            return this._onError(error)
-          }
-          this.doc.updateConnectionState('ok')
-          this.doc.flushPendingOps()
-          return this._callJoinCallbacks()
-        })
-      }
-    }
-
-    _callJoinCallbacks() {
-      for (const callback of Array.from(this._joinCallbacks || [])) {
-        callback()
-      }
-      return delete this._joinCallbacks
-    }
-
-    _joinDoc(callback) {
-      if (callback == null) {
-        callback = function () {}
-      }
-      if (this.doc != null) {
-        this.ide.pushEvent('joinDoc:existing', {
-          doc_id: this.doc_id,
-          version: this.doc.getVersion(),
-        })
-        return this.ide.socket.emit(
-          'joinDoc',
-          this.doc_id,
-          this.doc.getVersion(),
-          { encodeRanges: true },
-          (error, docLines, version, updates, ranges) => {
-            if (error != null) {
-              return callback(error)
-            }
-            this.joined = true
-            this.doc.catchUp(updates)
-            this._decodeRanges(ranges)
-            this._catchUpRanges(
-              ranges != null ? ranges.changes : undefined,
-              ranges != null ? ranges.comments : undefined
-            )
-            return callback()
-          }
-        )
-      } else {
-        this.ide.pushEvent('joinDoc:new', {
-          doc_id: this.doc_id,
-        })
-        return this.ide.socket.emit(
-          'joinDoc',
-          this.doc_id,
-          { encodeRanges: true },
-          (error, docLines, version, updates, ranges) => {
-            if (error != null) {
-              return callback(error)
-            }
-            this.joined = true
-            this.ide.pushEvent('joinDoc:inited', {
-              doc_id: this.doc_id,
-              version,
-            })
-            this.doc = new ShareJsDoc(
-              this.doc_id,
-              docLines,
-              version,
-              this.ide.socket,
-              this.ide.globalEditorWatchdogManager
-            )
-            this._decodeRanges(ranges)
-            this.ranges = new RangesTracker(
-              ranges != null ? ranges.changes : undefined,
-              ranges != null ? ranges.comments : undefined
-            )
-            this._bindToShareJsDocEvents()
-            return callback()
-          }
-        )
-      }
-    }
-
-    _decodeRanges(ranges) {
-      const decodeFromWebsockets = text => decodeURIComponent(escape(text))
-      try {
-        for (const change of Array.from(ranges.changes || [])) {
-          if (change.op.i != null) {
-            change.op.i = decodeFromWebsockets(change.op.i)
-          }
-          if (change.op.d != null) {
-            change.op.d = decodeFromWebsockets(change.op.d)
-          }
-        }
-        return (() => {
-          const result = []
-          for (const comment of Array.from(ranges.comments || [])) {
-            if (comment.op.c != null) {
-              result.push((comment.op.c = decodeFromWebsockets(comment.op.c)))
-            } else {
-              result.push(undefined)
-            }
-          }
-          return result
-        })()
-      } catch (err) {
-        debugConsole.error(err)
-      }
-    }
-
-    _leaveDoc(callback) {
-      if (callback == null) {
-        callback = function () {}
-      }
-      this.ide.pushEvent('leaveDoc', {
-        doc_id: this.doc_id,
-      })
-      debugConsole.log('[_leaveDoc] Sending leaveDoc request')
-      return this.ide.socket.emit('leaveDoc', this.doc_id, error => {
-        if (error != null) {
-          return callback(error)
-        }
-        this.joined = false
-        for (callback of Array.from(this._leaveCallbacks || [])) {
-          debugConsole.log('[_leaveDoc] Calling buffered callback', callback)
-          callback(error)
-        }
-        delete this._leaveCallbacks
-        return callback(error)
-      })
-    }
-
-    _cleanUp() {
-      // if we arrive here from _onError the pending and inflight ops will have been cleared
-      if (this.hasBufferedOps()) {
-        debugConsole.log(
-          `[_cleanUp] Document (${this.doc_id}) has buffered ops, refusing to remove from openDocs`
-        )
-        return // return immediately, do not unbind from events
-      } else if (Document.openDocs[this.doc_id] === this) {
-        debugConsole.log(
-          `[_cleanUp] Removing self (${this.doc_id}) from in openDocs`
-        )
-        delete Document.openDocs[this.doc_id]
-      } else {
-        // It's possible that this instance has error, and the doc has been reloaded.
-        // This creates a new instance in Document.openDoc with the same id. We shouldn't
-        // clear it because it's not this instance.
-        debugConsole.log(
-          `[_cleanUp] New instance of (${this.doc_id}) created. Not removing`
-        )
-      }
-      this._unBindFromEditorEvents()
-      return this._unBindFromSocketEvents()
-    }
-
-    _bindToShareJsDocEvents() {
-      this.doc.on('error', (error, meta) => this._onError(error, meta))
-      this.doc.on('externalUpdate', update => {
-        this.ide.pushEvent('externalUpdate', { doc_id: this.doc_id })
-        return this.trigger('externalUpdate', update)
-      })
-      this.doc.on('remoteop', (...args) => {
-        this.ide.pushEvent('remoteop', { doc_id: this.doc_id })
-        return this.trigger('remoteop', ...Array.from(args))
-      })
-      this.doc.on('op:sent', op => {
-        this.ide.pushEvent('op:sent', {
-          doc_id: this.doc_id,
-          op,
-        })
-        return this.trigger('op:sent')
-      })
-      this.doc.on('op:acknowledged', op => {
-        this.ide.pushEvent('op:acknowledged', {
-          doc_id: this.doc_id,
-          op,
-        })
-        this.ide.$scope.$emit('ide:opAcknowledged', {
-          doc_id: this.doc_id,
-          op,
-        })
-        return this.trigger('op:acknowledged')
-      })
-      this.doc.on('op:timeout', op => {
-        this.ide.pushEvent('op:timeout', {
-          doc_id: this.doc_id,
-          op,
-        })
-        this.trigger('op:timeout')
-        return this._onError(new Error('op timed out'))
-      })
-      this.doc.on('flush', (inflightOp, pendingOp, version) => {
-        return this.ide.pushEvent('flush', {
-          doc_id: this.doc_id,
-          inflightOp,
-          pendingOp,
-          v: version,
-        })
-      })
-
-      let docChangedTimeout
-      this.doc.on('change', (ops, oldSnapshot, msg) => {
-        this._applyOpsToRanges(ops, oldSnapshot, msg)
-        if (docChangedTimeout) {
-          window.clearTimeout(docChangedTimeout)
-        }
-        docChangedTimeout = window.setTimeout(() => {
-          window.dispatchEvent(
-            new CustomEvent('doc:changed', { detail: { id: this.doc_id } })
-          )
-          this.ide.$scope.$emit('doc:changed', { doc_id: this.doc_id })
-        }, 50)
-      })
-
-      this.doc.on('flipped_pending_to_inflight', () => {
-        return this.trigger('flipped_pending_to_inflight')
-      })
-
-      let docSavedTimeout
-      this.doc.on('saved', () => {
-        if (docSavedTimeout) {
-          window.clearTimeout(docSavedTimeout)
-        }
-        docSavedTimeout = window.setTimeout(() => {
-          window.dispatchEvent(
-            new CustomEvent('doc:saved', { detail: { id: this.doc_id } })
-          )
-          this.ide.$scope.$emit('doc:saved', { doc_id: this.doc_id })
-        }, 50)
-      })
-    }
-
-    _onError(error, meta, editorContent) {
-      if (meta == null) {
-        meta = {}
-      }
-      meta.doc_id = this.doc_id
-      debugConsole.log('ShareJS error', error, meta)
-      if (error.message === 'no project_id found on client') {
-        debugConsole.log('ignoring error, will wait to join project')
-        return
-      }
-      if (this.doc != null) {
-        this.doc.clearInflightAndPendingOps()
-      }
-      this.trigger('error', error, meta, editorContent)
-      // The clean up should run after the error is triggered because the error triggers a
-      // disconnect. If we run the clean up first, we remove our event handlers and miss
-      // the disconnect event, which means we try to leaveDoc when the connection comes back.
-      // This could intefere with the new connection of a new instance of this document.
-      return this._cleanUp()
-    }
-
-    _applyOpsToRanges(ops, oldSnapshot, msg) {
-      let old_id_seed
-      if (ops == null) {
-        ops = []
-      }
-      let track_changes_as = null
-      const remote_op = msg != null
-      if (__guard__(msg != null ? msg.meta : undefined, x => x.tc) != null) {
-        old_id_seed = this.ranges.getIdSeed()
-        this.ranges.setIdSeed(msg.meta.tc)
-      }
-      if (remote_op && (msg.meta != null ? msg.meta.tc : undefined)) {
-        track_changes_as = msg.meta.user_id
-      } else if (!remote_op && this.track_changes_as != null) {
-        ;({ track_changes_as } = this)
-      }
-      this.ranges.track_changes = track_changes_as != null
-      for (const op of this._filterOps(ops)) {
-        this.ranges.applyOp(op, { user_id: track_changes_as })
-      }
-      if (old_id_seed != null) {
-        this.ranges.setIdSeed(old_id_seed)
-      }
-      if (remote_op) {
-        // With remote ops, the editor hasn't been updated when we receive this op,
-        // so defer updating track changes until it has
-        return setTimeout(() => this.emit('ranges:dirty'))
-      } else {
-        return this.emit('ranges:dirty')
-      }
-    }
-
-    _catchUpRanges(changes, comments) {
-      // We've just been given the current server's ranges, but need to apply any local ops we have.
-      // Reset to the server state then apply our local ops again.
-      if (changes == null) {
-        changes = []
-      }
-      if (comments == null) {
-        comments = []
-      }
-      this.emit('ranges:clear')
-      this.ranges.changes = changes
-      this.ranges.comments = comments
-      this.ranges.track_changes = this.doc.track_changes
-      for (const op of this._filterOps(this.doc.getInflightOp() || [])) {
-        this.ranges.setIdSeed(this.doc.track_changes_id_seeds.inflight)
-        this.ranges.applyOp(op, { user_id: this.track_changes_as })
-      }
-      for (const op of this._filterOps(this.doc.getPendingOp() || [])) {
-        this.ranges.setIdSeed(this.doc.track_changes_id_seeds.pending)
-        this.ranges.applyOp(op, { user_id: this.track_changes_as })
-      }
-      return this.emit('ranges:redraw')
-    }
-
-    _filterOps(ops) {
-      // Read-only token users can't see/edit comment, so we filter out comment
-      // ops to avoid highlighting comment ranges.
-      if (window.isRestrictedTokenMember) {
-        return ops.filter(op => op.c == null)
-      } else {
-        return ops
-      }
-    }
-  }
-  Document.initClass()
-  return Document
-})()
-
-function __guard__(value, transform) {
-  return typeof value !== 'undefined' && value !== null
-    ? transform(value)
-    : undefined
-}

+ 0 - 510
services/web/frontend/js/ide/editor/EditorManager.js

@@ -1,510 +0,0 @@
-import _ from 'lodash'
-/* eslint-disable
-   camelcase,
-   n/handle-callback-err,
-   max-len,
-   no-return-assign,
- */
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS206: Consider reworking classes to avoid initClass
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-import Document from './Document'
-import './directives/formattingButtons'
-import './directives/toggleSwitch'
-import './controllers/SavingNotificationController'
-import './controllers/CompileButton'
-import './controllers/SwitchToPDFButton'
-import '../metadata/services/metadata'
-import { debugConsole } from '@/utils/debugging'
-import customLocalStorage from '@/infrastructure/local-storage'
-
-let EditorManager
-
-export default EditorManager = (function () {
-  EditorManager = class EditorManager {
-    static initClass() {
-      this.prototype._syncTimeout = null
-    }
-
-    constructor(ide, $scope, localStorage, eventTracking) {
-      this.ide = ide
-      this.editorOpenDocEpoch = 0 // track pending document loads
-      this.$scope = $scope
-      this.localStorage = localStorage
-      this.$scope.editor = {
-        sharejs_doc: null,
-        open_doc_id: null,
-        open_doc_name: null,
-        opening: true,
-        trackChanges: false,
-        wantTrackChanges: false,
-        docTooLongErrorShown: false,
-        showVisual: this.showVisual(),
-        showSymbolPalette: false,
-        toggleSymbolPalette: () => {
-          const newValue = !this.$scope.editor.showSymbolPalette
-          this.$scope.editor.showSymbolPalette = newValue
-          ide.$scope.$emit('south-pane-toggled', newValue)
-          eventTracking.sendMB(
-            newValue ? 'symbol-palette-show' : 'symbol-palette-hide'
-          )
-        },
-        insertSymbol: symbol => {
-          ide.$scope.$emit('editor:replace-selection', symbol.command)
-          eventTracking.sendMB('symbol-palette-insert')
-        },
-        multiSelectedCount: 0,
-      }
-
-      window.addEventListener('editor:insert-symbol', event => {
-        this.$scope.editor.insertSymbol(event.detail)
-      })
-
-      this.$scope.$on('entity:selected', (event, entity) => {
-        if (this.$scope.ui.view !== 'history' && entity.type === 'doc') {
-          return this.openDoc(entity)
-        }
-      })
-
-      this.$scope.$on('entity:no-selection', () => {
-        this.$scope.$apply(() => {
-          this.$scope.ui.view = null
-        })
-      })
-
-      this.$scope.$on('entity:deleted', (event, entity) => {
-        if (this.$scope.editor.open_doc_id === entity.id) {
-          if (!this.$scope.project.rootDoc_id) {
-            this.$scope.ui.view = null
-            return
-          }
-          const doc = this.ide.fileTreeManager.findEntityById(
-            this.$scope.project.rootDoc_id
-          )
-          if (doc == null) {
-            this.$scope.ui.view = null
-            return
-          }
-          return this.openDoc(doc)
-        }
-      })
-
-      let initialized = false
-      this.$scope.$on('file-tree:initialized', () => {
-        if (!initialized) {
-          initialized = true
-          return this.autoOpenDoc()
-        }
-      })
-
-      this.$scope.$on('flush-changes', () => {
-        return Document.flushAll()
-      })
-
-      // event dispatched by pdf preview
-      window.addEventListener('flush-changes', () => {
-        Document.flushAll()
-      })
-
-      window.addEventListener('blur', () => {
-        // The browser may put the tab into sleep as it looses focus.
-        // Flushing the documents should help with keeping the documents in
-        //  sync: we can use any new version of the doc that the server may
-        //  present us. There should be no need to insert local changes into
-        //  the doc history as the user comes back.
-        debugConsole.log('[EditorManager] forcing flush onblur')
-        Document.flushAll()
-      })
-
-      this.$scope.$watch('editor.wantTrackChanges', value => {
-        if (value == null) {
-          return
-        }
-        return this._syncTrackChangesState(this.$scope.editor.sharejs_doc)
-      })
-
-      window.addEventListener('editor:open-doc', event => {
-        const { doc, ...options } = event.detail
-        this.openDoc(doc, options)
-      })
-
-      window.addEventListener('editor:open-file', event => {
-        const { name, ...options } = event.detail
-        for (const extension of ['', '.tex']) {
-          const path = `${name}${extension}`
-          const doc = ide.fileTreeManager.findEntityByPath(path)
-          if (doc) {
-            this.openDoc(doc, options)
-            break
-          }
-        }
-      })
-    }
-
-    getEditorType() {
-      if (!this.$scope.editor.sharejs_doc) {
-        return null
-      }
-
-      let editorType = this.$scope.editor.sharejs_doc.editorType()
-
-      if (editorType === 'cm6' && this.$scope.editor.showVisual) {
-        editorType = 'cm6-rich-text'
-      }
-
-      return editorType
-    }
-
-    showVisual() {
-      const editorModeKey = `editor.mode.${this.$scope.project_id}`
-      const editorModeVal = this.localStorage(editorModeKey)
-
-      if (editorModeVal) {
-        // clean up the old key
-        customLocalStorage.removeItem(editorModeKey)
-      }
-
-      const lastUsedMode = this.localStorage(`editor.lastUsedMode`)
-      if (lastUsedMode) {
-        return lastUsedMode === 'visual'
-      } else {
-        return editorModeVal === 'rich-text'
-      }
-    }
-
-    autoOpenDoc() {
-      const open_doc_id =
-        this.ide.localStorage(`doc.open_id.${this.$scope.project_id}`) ||
-        this.$scope.project.rootDoc_id
-      if (open_doc_id == null) {
-        return
-      }
-      const doc = this.ide.fileTreeManager.findEntityById(open_doc_id)
-      if (doc == null) {
-        return
-      }
-      return this.openDoc(doc)
-    }
-
-    openDocId(doc_id, options) {
-      if (options == null) {
-        options = {}
-      }
-      const doc = this.ide.fileTreeManager.findEntityById(doc_id)
-      if (doc == null) {
-        return
-      }
-      return this.openDoc(doc, options)
-    }
-
-    jumpToLine(options) {
-      return this.$scope.$broadcast(
-        'editor:gotoLine',
-        options.gotoLine,
-        options.gotoColumn,
-        options.syncToPdf
-      )
-    }
-
-    openDoc(doc, options) {
-      if (options == null) {
-        options = {}
-      }
-      debugConsole.log(`[openDoc] Opening ${doc.id}`)
-      if (this.$scope.ui.view === 'editor') {
-        // store position of previous doc before switching docs
-        this.$scope.$broadcast('store-doc-position')
-      }
-      this.$scope.ui.view = 'editor'
-
-      const done = isNewDoc => {
-        const eventName = 'doc:after-opened'
-        this.$scope.$broadcast(eventName, { isNewDoc })
-        window.dispatchEvent(new CustomEvent(eventName, { detail: isNewDoc }))
-        if (options.gotoLine != null) {
-          // allow Ace to display document before moving, delay until next tick
-          // added delay to make this happen later that gotoStoredPosition in
-          // CursorPositionManager
-          setTimeout(() => this.jumpToLine(options))
-          // when opening a doc in CM6, jump to the line again after a stored scroll position has been restored
-          if (isNewDoc) {
-            window.addEventListener(
-              'editor:scroll-position-restored',
-              () => this.jumpToLine(options),
-              { once: true }
-            )
-          }
-        } else if (options.gotoOffset != null) {
-          setTimeout(() => {
-            this.$scope.$broadcast('editor:gotoOffset', options.gotoOffset)
-          })
-        }
-      }
-
-      // If we already have the document open we can return at this point.
-      // Note: only use forceReopen:true to override this when the document is
-      // is out of sync and needs to be reloaded from the server.
-      if (doc.id === this.$scope.editor.open_doc_id && !options.forceReopen) {
-        // automatically update the file tree whenever the file is opened
-        this.ide.fileTreeManager.selectEntity(doc)
-        this.$scope.$broadcast('file-tree.reselectDoc', doc.id)
-        this.$scope.$apply(() => {
-          return done(false)
-        })
-        return
-      }
-
-      this.$scope.$applyAsync(() => {
-        // We're now either opening a new document or reloading a broken one.
-        this.$scope.editor.open_doc_id = doc.id
-        this.$scope.editor.open_doc_name = doc.name
-
-        this.ide.localStorage(`doc.open_id.${this.$scope.project_id}`, doc.id)
-        this.ide.fileTreeManager.selectEntity(doc)
-
-        this.$scope.editor.opening = true
-        return this._openNewDocument(doc, (error, sharejs_doc) => {
-          if (error && error.message === 'another document was loaded') {
-            debugConsole.log(
-              `[openDoc] another document was loaded while ${doc.id} was loading`
-            )
-            return
-          }
-          if (error != null) {
-            this.ide.showGenericMessageModal(
-              'Error opening document',
-              'Sorry, something went wrong opening this document. Please try again.'
-            )
-            return
-          }
-
-          this._syncTrackChangesState(sharejs_doc)
-
-          this.$scope.$broadcast('doc:opened')
-
-          return this.$scope.$applyAsync(() => {
-            this.$scope.editor.opening = false
-            this.$scope.editor.sharejs_doc = sharejs_doc
-            return done(true)
-          })
-        })
-      })
-    }
-
-    _openNewDocument(doc, callback) {
-      // Leave the current document
-      //  - when we are opening a different new one, to avoid race conditions
-      //     between leaving and joining the same document
-      //  - when the current one has pending ops that need flushing, to avoid
-      //     race conditions from cleanup
-      const current_sharejs_doc = this.$scope.editor.sharejs_doc
-      const currentDocId = current_sharejs_doc && current_sharejs_doc.doc_id
-      const hasBufferedOps =
-        current_sharejs_doc && current_sharejs_doc.hasBufferedOps()
-      const changingDoc = current_sharejs_doc && currentDocId !== doc.id
-      if (changingDoc || hasBufferedOps) {
-        debugConsole.log('[_openNewDocument] Leaving existing open doc...')
-
-        // Do not trigger any UI changes from remote operations
-        this._unbindFromDocumentEvents(current_sharejs_doc)
-        // Keep listening for out-of-sync and similar errors.
-        this._attachErrorHandlerToDocument(doc, current_sharejs_doc)
-
-        // Teardown the Document -> ShareJsDoc -> sharejs doc
-        // By the time this completes, the Document instance is no longer
-        //  registered in Document.openDocs and _doOpenNewDocument can start
-        //  from scratch -- read: no corrupted internal state.
-        const editorOpenDocEpoch = ++this.editorOpenDocEpoch
-        current_sharejs_doc.leaveAndCleanUp(error => {
-          if (error) {
-            debugConsole.log(
-              `[_openNewDocument] error leaving doc ${currentDocId}`,
-              error
-            )
-            return callback(error)
-          }
-          if (this.editorOpenDocEpoch !== editorOpenDocEpoch) {
-            debugConsole.log(
-              `[openNewDocument] editorOpenDocEpoch mismatch ${this.editorOpenDocEpoch} vs ${editorOpenDocEpoch}`
-            )
-            return callback(new Error('another document was loaded'))
-          }
-          this._doOpenNewDocument(doc, callback)
-        })
-      } else {
-        this._doOpenNewDocument(doc, callback)
-      }
-    }
-
-    _doOpenNewDocument(doc, callback) {
-      if (callback == null) {
-        callback = function () {}
-      }
-      debugConsole.log('[_doOpenNewDocument] Opening...')
-      const new_sharejs_doc = Document.getDocument(this.ide, doc.id)
-      const editorOpenDocEpoch = ++this.editorOpenDocEpoch
-      return new_sharejs_doc.join(error => {
-        if (error != null) {
-          debugConsole.log(
-            `[_doOpenNewDocument] error joining doc ${doc.id}`,
-            error
-          )
-          return callback(error)
-        }
-        if (this.editorOpenDocEpoch !== editorOpenDocEpoch) {
-          debugConsole.log(
-            `[openNewDocument] editorOpenDocEpoch mismatch ${this.editorOpenDocEpoch} vs ${editorOpenDocEpoch}`
-          )
-          new_sharejs_doc.leaveAndCleanUp()
-          return callback(new Error('another document was loaded'))
-        }
-        this._bindToDocumentEvents(doc, new_sharejs_doc)
-        return callback(null, new_sharejs_doc)
-      })
-    }
-
-    _attachErrorHandlerToDocument(doc, sharejs_doc) {
-      sharejs_doc.on('error', (error, meta, editorContent) => {
-        let message
-        if ((error != null ? error.message : undefined) != null) {
-          ;({ message } = error)
-        } else if (typeof error === 'string') {
-          message = error
-        } else {
-          message = ''
-        }
-        if (/maxDocLength/.test(message)) {
-          this.$scope.docTooLongErrorShown = true
-          this.openDoc(doc, { forceReopen: true })
-          const genericMessageModal = this.ide.showGenericMessageModal(
-            'Document Too Long',
-            'Sorry, this file is too long to be edited manually. Please upload it directly.'
-          )
-          genericMessageModal.result.finally(() => {
-            this.$scope.docTooLongErrorShown = false
-          })
-        } else if (/too many comments or tracked changes/.test(message)) {
-          this.ide.showGenericMessageModal(
-            'Too many comments or tracked changes',
-            'Sorry, this file has too many comments or tracked changes. Please try accepting or rejecting some existing changes, or resolving and deleting some comments.'
-          )
-        } else if (!this.$scope.docTooLongErrorShown) {
-          // Do not allow this doc to open another error modal.
-          sharejs_doc.off('error')
-
-          // Preserve the sharejs contents before the teardown.
-          editorContent =
-            typeof editorContent === 'string'
-              ? editorContent
-              : sharejs_doc.doc._doc.snapshot
-
-          // Tear down the ShareJsDoc.
-          if (sharejs_doc.doc) sharejs_doc.doc.clearInflightAndPendingOps()
-
-          // Do not re-join after re-connecting.
-          sharejs_doc.leaveAndCleanUp()
-          this.ide.connectionManager.disconnect({ permanent: true })
-          this.ide.reportError(error, meta)
-
-          // Tell the user about the error state.
-          this.$scope.editor.error_state = true
-          this.ide.showOutOfSyncModal(
-            'Out of sync',
-            "Sorry, this file has gone out of sync and we need to do a full refresh. <br> <a target='_blank' rel='noopener noreferrer' href='/learn/Kb/Editor_out_of_sync_problems'>Please see this help guide for more information</a>",
-            editorContent
-          )
-          // Do not forceReopen the document.
-          return
-        }
-        const removeHandler = this.$scope.$on('project:joined', () => {
-          this.openDoc(doc, { forceReopen: true })
-          removeHandler()
-        })
-      })
-    }
-
-    _bindToDocumentEvents(doc, sharejs_doc) {
-      this._attachErrorHandlerToDocument(doc, sharejs_doc)
-
-      return sharejs_doc.on('externalUpdate', update => {
-        if (this._ignoreExternalUpdates) {
-          return
-        }
-        if (
-          _.property(['meta', 'type'])(update) === 'external' &&
-          _.property(['meta', 'source'])(update) === 'git-bridge'
-        ) {
-          return
-        }
-        if (update?.meta?.source === 'file-revert') {
-          return
-        }
-        return this.ide.showGenericMessageModal(
-          'Document Updated Externally',
-          'This document was just updated externally. Any recent changes you have made may have been overwritten. To see previous versions please look in the history.'
-        )
-      })
-    }
-
-    _unbindFromDocumentEvents(document) {
-      return document.off()
-    }
-
-    getCurrentDocValue() {
-      return this.$scope.editor.sharejs_doc != null
-        ? this.$scope.editor.sharejs_doc.getSnapshot()
-        : undefined
-    }
-
-    getCurrentDocId() {
-      return this.$scope.editor.open_doc_id
-    }
-
-    startIgnoringExternalUpdates() {
-      return (this._ignoreExternalUpdates = true)
-    }
-
-    stopIgnoringExternalUpdates() {
-      return (this._ignoreExternalUpdates = false)
-    }
-
-    _syncTrackChangesState(doc) {
-      let tryToggle
-      if (doc == null) {
-        return
-      }
-
-      if (this._syncTimeout != null) {
-        clearTimeout(this._syncTimeout)
-        this._syncTimeout = null
-      }
-
-      const want = this.$scope.editor.wantTrackChanges
-      const have = doc.getTrackingChanges()
-      if (want === have) {
-        this.$scope.editor.trackChanges = want
-        return
-      }
-
-      return (tryToggle = () => {
-        const saved = doc.getInflightOp() == null && doc.getPendingOp() == null
-        if (saved) {
-          doc.setTrackingChanges(want)
-          return this.$scope.$apply(() => {
-            return (this.$scope.editor.trackChanges = want)
-          })
-        } else {
-          return (this._syncTimeout = setTimeout(tryToggle, 100))
-        }
-      })()
-    }
-  }
-  EditorManager.initClass()
-  return EditorManager
-})()

+ 0 - 50
services/web/frontend/js/ide/editor/EditorShareJsCodec.js

@@ -1,50 +0,0 @@
-/* eslint-disable
-    max-len,
-    no-return-assign,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-let EditorShareJsCodec
-
-export default EditorShareJsCodec = {
-  rangeToShareJs(range, lines) {
-    let offset = 0
-    for (let i = 0; i < lines.length; i++) {
-      const line = lines[i]
-      offset += i < range.row ? line.length : range.column
-    }
-    offset += range.row // Include newlines
-    return offset
-  },
-
-  changeToShareJs(delta, lines) {
-    const offset = EditorShareJsCodec.rangeToShareJs(delta.start, lines)
-
-    const text = delta.lines.join('\n')
-    switch (delta.action) {
-      case 'insert':
-        return { i: text, p: offset }
-      case 'remove':
-        return { d: text, p: offset }
-      default:
-        throw new Error(`unknown action: ${delta.action}`)
-    }
-  },
-
-  shareJsOffsetToRowColumn(offset, lines) {
-    let row = 0
-    for (row = 0; row < lines.length; row++) {
-      const line = lines[row]
-      if (offset <= line.length) {
-        break
-      }
-      offset -= lines[row].length + 1
-    } // + 1 for newline char
-    return { row, column: offset }
-  },
-}

+ 0 - 471
services/web/frontend/js/ide/editor/ShareJsDoc.js

@@ -1,471 +0,0 @@
-/* eslint-disable
-    camelcase,
-    max-len,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS001: Remove Babel/TypeScript constructor workaround
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * DS103: Rewrite code to no longer use __guard__
- * DS205: Consider reworking code to avoid use of IIFEs
- * DS206: Consider reworking classes to avoid initClass
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-import EventEmitter from '../../utils/EventEmitter'
-import ShareJs from '../../vendor/libs/sharejs'
-import EditorWatchdogManager from '../connection/EditorWatchdogManager'
-import { debugConsole } from '@/utils/debugging'
-import { recordDocumentFirstChangeEvent } from '@/features/event-tracking/document-first-change-event'
-
-let ShareJsDoc
-const SINGLE_USER_FLUSH_DELAY = 2000 // ms
-const MULTI_USER_FLUSH_DELAY = 500 // ms
-
-export default ShareJsDoc = (function () {
-  ShareJsDoc = class ShareJsDoc extends EventEmitter {
-    static initClass() {
-      this.prototype.INFLIGHT_OP_TIMEOUT = 5000 // Retry sending ops after 5 seconds without an ack
-      this.prototype.WAIT_FOR_CONNECTION_TIMEOUT = 500
-
-      this.prototype.FATAL_OP_TIMEOUT = 30000
-    }
-
-    constructor(
-      doc_id,
-      docLines,
-      version,
-      socket,
-      globalEditorWatchdogManager
-    ) {
-      super()
-      // Dencode any binary bits of data
-      // See http://ecmanaut.blogspot.co.uk/2006/07/encoding-decoding-utf8-in-javascript.html
-      this.doc_id = doc_id
-      this.socket = socket
-      this.type = 'text'
-      docLines = Array.from(docLines).map(line =>
-        decodeURIComponent(escape(line))
-      )
-      const snapshot = docLines.join('\n')
-      this.track_changes = false
-
-      this.connection = {
-        send: update => {
-          this._startInflightOpTimeout(update)
-          if (
-            window.disconnectOnUpdate != null &&
-            Math.random() < window.disconnectOnUpdate
-          ) {
-            debugConsole.log('Disconnecting on update', update)
-            window._ide.socket.socket.disconnect()
-          }
-          if (
-            window.dropUpdates != null &&
-            Math.random() < window.dropUpdates
-          ) {
-            debugConsole.log('Simulating a lost update', update)
-            return
-          }
-          if (this.track_changes) {
-            if (update.meta == null) {
-              update.meta = {}
-            }
-            update.meta.tc = this.track_changes_id_seeds.inflight
-          }
-          return this.socket.emit(
-            'applyOtUpdate',
-            this.doc_id,
-            update,
-            error => {
-              if (error != null) {
-                return this._handleError(error)
-              }
-            }
-          )
-        },
-        state: 'ok',
-        id: this.socket.publicId,
-      }
-
-      this._doc = new ShareJs.Doc(this.connection, this.doc_id, {
-        type: this.type,
-      })
-      this._doc.setFlushDelay(SINGLE_USER_FLUSH_DELAY)
-      this._doc.on('change', (...args) => {
-        return this.trigger('change', ...Array.from(args))
-      })
-      this.EditorWatchdogManager = new EditorWatchdogManager({
-        parent: globalEditorWatchdogManager,
-      })
-      this._doc.on('acknowledge', () => {
-        this.lastAcked = new Date() // note time of last ack from server for an op we sent
-        this.EditorWatchdogManager.onAck() // keep track of last ack globally
-        return this.trigger('acknowledge')
-      })
-      this._doc.on('remoteop', (...args) => {
-        // As soon as we're working with a collaborator, start sending
-        // ops more frequently for low latency.
-        this._doc.setFlushDelay(MULTI_USER_FLUSH_DELAY)
-        return this.trigger('remoteop', ...Array.from(args))
-      })
-      this._doc.on('flipped_pending_to_inflight', () => {
-        return this.trigger('flipped_pending_to_inflight')
-      })
-      this._doc.on('saved', () => {
-        return this.trigger('saved')
-      })
-      this._doc.on('error', e => {
-        return this._handleError(e)
-      })
-
-      this._bindToDocChanges(this._doc)
-
-      this.processUpdateFromServer({
-        open: true,
-        v: version,
-        snapshot,
-      })
-      this._removeCarriageReturnCharFromShareJsDoc()
-    }
-
-    _removeCarriageReturnCharFromShareJsDoc() {
-      const doc = this._doc
-      if (doc.snapshot.indexOf('\r') === -1) {
-        return
-      }
-      window._ide.pushEvent('remove-carriage-return-char', {
-        doc_id: this.doc_id,
-      })
-      let nextPos
-      while ((nextPos = doc.snapshot.indexOf('\r')) !== -1) {
-        debugConsole.log('[ShareJsDoc] remove-carriage-return-char', nextPos)
-        doc.del(nextPos, 1)
-      }
-    }
-
-    submitOp(...args) {
-      return this._doc.submitOp(...Array.from(args || []))
-    }
-
-    // The following code puts out of order messages into a queue
-    // so that they can be processed in order.  This is a workaround
-    // for messages being delayed by redis cluster.
-    // FIXME: REMOVE THIS WHEN REDIS PUBSUB IS SENDING MESSAGES IN ORDER
-    _isAheadOfExpectedVersion(message) {
-      return this._doc.version > 0 && message.v > this._doc.version
-    }
-
-    _pushOntoQueue(message) {
-      debugConsole.log(`[processUpdate] push onto queue ${message.v}`)
-      // set a timer so that we never leave messages in the queue indefinitely
-      if (!this.queuedMessageTimer) {
-        this.queuedMessageTimer = setTimeout(() => {
-          debugConsole.log(
-            `[processUpdate] queue timeout fired for ${message.v}`
-          )
-          // force the message to be processed after the timeout,
-          // it will cause an error if the missing update has not arrived
-          this.processUpdateFromServer(message)
-        }, this.INFLIGHT_OP_TIMEOUT)
-      }
-      this.queuedMessages.push(message)
-      // keep the queue in order, lowest version first
-      this.queuedMessages.sort(function (a, b) {
-        return a.v - b.v
-      })
-    }
-
-    _clearQueue() {
-      this.queuedMessages = []
-    }
-
-    _processQueue() {
-      if (this.queuedMessages.length > 0) {
-        const nextAvailableVersion = this.queuedMessages[0].v
-        if (nextAvailableVersion > this._doc.version) {
-          // there are updates we still can't apply yet
-        } else {
-          // there's a version we can accept on the queue, apply it
-          debugConsole.log(
-            `[processUpdate] taken from queue ${nextAvailableVersion}`
-          )
-          this.processUpdateFromServerInOrder(this.queuedMessages.shift())
-          // clear the pending timer if the queue has now been cleared
-          if (this.queuedMessages.length === 0 && this.queuedMessageTimer) {
-            debugConsole.log('[processUpdate] queue is empty, cleared timeout')
-            clearTimeout(this.queuedMessageTimer)
-            this.queuedMessageTimer = null
-          }
-        }
-      }
-    }
-
-    // FIXME: This is the new method which reorders incoming updates if needed
-    // called from Document.js
-    processUpdateFromServerInOrder(message) {
-      // Create an array to hold queued messages
-      if (!this.queuedMessages) {
-        this.queuedMessages = []
-      }
-      // Is this update ahead of the next expected update?
-      // If so, put it on a queue to be handled later.
-      if (this._isAheadOfExpectedVersion(message)) {
-        this._pushOntoQueue(message)
-        return // defer processing this update for now
-      }
-      const error = this.processUpdateFromServer(message)
-      if (
-        error instanceof Error &&
-        error.message === 'Invalid version from server'
-      ) {
-        // if there was an error, abandon the queued updates ahead of this one
-        this._clearQueue()
-        return
-      }
-      // Do we have any messages queued up?
-      // find the next message if available
-      this._processQueue()
-    }
-
-    // FIXME: This is the original method. Switch back to this when redis
-    // issues are resolved.
-    processUpdateFromServer(message) {
-      try {
-        this._doc._onMessage(message)
-      } catch (error) {
-        // Version mismatches are thrown as errors
-        debugConsole.error(error)
-        this._handleError(error)
-        return error // return the error for queue handling
-      }
-
-      if (
-        __guard__(message != null ? message.meta : undefined, x => x.type) ===
-        'external'
-      ) {
-        return this.trigger('externalUpdate', message)
-      }
-    }
-
-    catchUp(updates) {
-      return (() => {
-        const result = []
-        for (let i = 0; i < updates.length; i++) {
-          const update = updates[i]
-          update.v = this._doc.version
-          update.doc = this.doc_id
-          result.push(this.processUpdateFromServer(update))
-        }
-        return result
-      })()
-    }
-
-    getSnapshot() {
-      return this._doc.snapshot
-    }
-
-    getVersion() {
-      return this._doc.version
-    }
-
-    getType() {
-      return this.type
-    }
-
-    clearInflightAndPendingOps() {
-      this._clearFatalTimeoutTimer()
-      this._doc.inflightOp = null
-      this._doc.inflightCallbacks = []
-      this._doc.pendingOp = null
-      return (this._doc.pendingCallbacks = [])
-    }
-
-    flushPendingOps() {
-      // This will flush any ops that are pending.
-      // If there is an inflight op it will do nothing.
-      return this._doc.flush()
-    }
-
-    updateConnectionState(state) {
-      debugConsole.log(`[updateConnectionState] Setting state to ${state}`)
-      this.connection.state = state
-      this.connection.id = this.socket.publicId
-      this._doc.autoOpen = false
-      this._doc._connectionStateChanged(state)
-      return (this.lastAcked = null) // reset the last ack time when connection changes
-    }
-
-    hasBufferedOps() {
-      return this._doc.inflightOp != null || this._doc.pendingOp != null
-    }
-
-    getInflightOp() {
-      return this._doc.inflightOp
-    }
-
-    getPendingOp() {
-      return this._doc.pendingOp
-    }
-
-    getRecentAck() {
-      // check if we have received an ack recently (within a factor of two of the single user flush delay)
-      return (
-        this.lastAcked != null &&
-        new Date() - this.lastAcked < 2 * SINGLE_USER_FLUSH_DELAY
-      )
-    }
-
-    getOpSize(op) {
-      // compute size of an op from its components
-      // (total number of characters inserted and deleted)
-      let size = 0
-      for (const component of Array.from(op || [])) {
-        if ((component != null ? component.i : undefined) != null) {
-          size += component.i.length
-        }
-        if ((component != null ? component.d : undefined) != null) {
-          size += component.d.length
-        }
-      }
-      return size
-    }
-
-    _attachEditorWatchdogManager(editorName, editor) {
-      // end-to-end check for edits -> acks, for this very ShareJsdoc
-      // This will catch a broken connection and missing UX-blocker for the
-      //  user, allowing them to keep editing.
-      this._detachEditorWatchdogManager =
-        this.EditorWatchdogManager.attachToEditor(editorName, editor)
-    }
-
-    _attachToEditor(editorName, editor, attachToShareJs) {
-      this._attachEditorWatchdogManager(editorName, editor)
-
-      attachToShareJs()
-    }
-
-    _maybeDetachEditorWatchdogManager() {
-      // a failed attach attempt may lead to a missing cleanup handler
-      if (this._detachEditorWatchdogManager) {
-        this._detachEditorWatchdogManager()
-        delete this._detachEditorWatchdogManager
-      }
-    }
-
-    attachToCM6(cm6) {
-      this._attachToEditor('CM6', cm6, () => {
-        cm6.attachShareJs(this._doc, window.maxDocLength)
-      })
-    }
-
-    detachFromCM6() {
-      this._maybeDetachEditorWatchdogManager()
-      if (this._doc.detach_cm6) {
-        this._doc.detach_cm6()
-      }
-    }
-
-    _startInflightOpTimeout(update) {
-      this._startFatalTimeoutTimer(update)
-      const retryOp = () => {
-        // Only send the update again if inflightOp is still populated
-        // This can be cleared when hard reloading the document in which
-        // case we don't want to keep trying to send it.
-        debugConsole.log('[inflightOpTimeout] Trying op again')
-        if (this._doc.inflightOp != null) {
-          // When there is a socket.io disconnect, @_doc.inflightSubmittedIds
-          // is updated with the socket.io client id of the current op in flight
-          // (meta.source of the op).
-          // @connection.id is the client id of the current socket.io session.
-          // So we need both depending on whether the op was submitted before
-          // one or more disconnects, or if it was submitted during the current session.
-          update.dupIfSource = [
-            this.connection.id,
-            ...Array.from(this._doc.inflightSubmittedIds),
-          ]
-
-          // We must be joined to a project for applyOtUpdate to work on the real-time
-          // service, so don't send an op if we're not. Connection state is set to 'ok'
-          // when we've joined the project
-          if (this.connection.state !== 'ok') {
-            let timer
-            debugConsole.log(
-              '[inflightOpTimeout] Not connected, retrying in 0.5s'
-            )
-            return (timer = setTimeout(
-              retryOp,
-              this.WAIT_FOR_CONNECTION_TIMEOUT
-            ))
-          } else {
-            debugConsole.log('[inflightOpTimeout] Sending')
-            return this.connection.send(update)
-          }
-        }
-      }
-
-      const timer = setTimeout(retryOp, this.INFLIGHT_OP_TIMEOUT)
-      return this._doc.inflightCallbacks.push(() => {
-        this._clearFatalTimeoutTimer()
-        return clearTimeout(timer)
-      }) // 30 seconds
-    }
-
-    _startFatalTimeoutTimer(update) {
-      // If an op doesn't get acked within FATAL_OP_TIMEOUT, something has
-      // gone unrecoverably wrong (the op will have been retried multiple times)
-      if (this._timeoutTimer != null) {
-        return
-      }
-      return (this._timeoutTimer = setTimeout(() => {
-        this._clearFatalTimeoutTimer()
-        return this.trigger('op:timeout', update)
-      }, this.FATAL_OP_TIMEOUT))
-    }
-
-    _clearFatalTimeoutTimer() {
-      if (this._timeoutTimer == null) {
-        return
-      }
-      clearTimeout(this._timeoutTimer)
-      return (this._timeoutTimer = null)
-    }
-
-    _handleError(error, meta) {
-      if (meta == null) {
-        meta = {}
-      }
-      return this.trigger('error', error, meta)
-    }
-
-    _bindToDocChanges(doc) {
-      const { submitOp } = doc
-      doc.submitOp = (...args) => {
-        recordDocumentFirstChangeEvent()
-        this.trigger('op:sent', ...Array.from(args))
-        doc.pendingCallbacks.push(() => {
-          return this.trigger('op:acknowledged', ...Array.from(args))
-        })
-        return submitOp.apply(doc, args)
-      }
-
-      const { flush } = doc
-      return (doc.flush = (...args) => {
-        this.trigger('flush', doc.inflightOp, doc.pendingOp, doc.version)
-        return flush.apply(doc, args)
-      })
-    }
-  }
-  ShareJsDoc.initClass()
-  return ShareJsDoc
-})()
-
-function __guard__(value, transform) {
-  return typeof value !== 'undefined' && value !== null
-    ? transform(value)
-    : undefined
-}

+ 0 - 9
services/web/frontend/js/ide/editor/controllers/CompileButton.js

@@ -1,9 +0,0 @@
-import App from '../../../base'
-import { react2angular } from 'react2angular'
-import { rootContext } from '../../../shared/context/root-context'
-import DetachCompileButtonWrapper from '../../../features/pdf-preview/components/detach-compile-button-wrapper'
-
-App.component(
-  'editorCompileButton',
-  react2angular(rootContext.use(DetachCompileButtonWrapper))
-)

+ 0 - 103
services/web/frontend/js/ide/editor/controllers/SavingNotificationController.js

@@ -1,103 +0,0 @@
-/* eslint-disable
-    camelcase,
-    max-len,
-    no-return-assign,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-import App from '../../../base'
-import Document from '../Document'
-
-export default App.controller('SavingNotificationController', [
-  '$scope',
-  'ide',
-  function ($scope, ide) {
-    let warnAboutUnsavedChanges
-    setInterval(() => pollSavedStatus(), 1000)
-
-    $(window).bind('beforeunload', () => {
-      return warnAboutUnsavedChanges()
-    })
-
-    let lockEditorModal = null // modal showing "connection lost"
-    let originalPermissionsLevel
-    const MAX_UNSAVED_SECONDS = 15 // lock the editor after this time if unsaved
-
-    $scope.docSavingStatus = {}
-    function pollSavedStatus() {
-      let t
-      const oldStatus = $scope.docSavingStatus
-      const oldUnsavedCount = $scope.docSavingStatusCount
-      const newStatus = {}
-      let newUnsavedCount = 0
-      let maxUnsavedSeconds = 0
-
-      for (const doc_id in Document.openDocs) {
-        const doc = Document.openDocs[doc_id]
-        const saving = doc.pollSavedStatus()
-        if (!saving) {
-          newUnsavedCount++
-          if (oldStatus[doc_id] != null) {
-            newStatus[doc_id] = oldStatus[doc_id]
-            t = newStatus[doc_id].unsavedSeconds += 1
-            if (t > maxUnsavedSeconds) {
-              maxUnsavedSeconds = t
-            }
-          } else {
-            newStatus[doc_id] = {
-              unsavedSeconds: 0,
-              doc: ide.fileTreeManager.findEntityById(doc_id),
-            }
-          }
-        }
-      }
-
-      if (newUnsavedCount > 0 && t > MAX_UNSAVED_SECONDS && !lockEditorModal) {
-        lockEditorModal = ide.showLockEditorMessageModal(
-          'Connection lost',
-          'Sorry, the connection to the server is down.'
-        )
-
-        // put editor in readOnly mode
-        originalPermissionsLevel = ide.$scope.permissionsLevel
-        ide.$scope.permissionsLevel = 'readOnly'
-
-        lockEditorModal.result.finally(() => {
-          lockEditorModal = null // unset the modal if connection comes back
-          // restore original permissions
-          ide.$scope.permissionsLevel = originalPermissionsLevel
-        })
-      }
-
-      if (lockEditorModal && newUnsavedCount === 0) {
-        lockEditorModal.dismiss('connection back up')
-        // restore original permissions if they were changed
-        if (originalPermissionsLevel) {
-          ide.$scope.permissionsLevel = originalPermissionsLevel
-        }
-      }
-
-      // for performance, only update the display if the old or new
-      // counts of unsaved files are nonzeror.  If both old and new
-      // unsaved counts are zero then we know we are in a good state
-      // and don't need to do anything to the UI.
-      if (newUnsavedCount || oldUnsavedCount) {
-        $scope.docSavingStatus = newStatus
-        $scope.docSavingStatusCount = newUnsavedCount
-        return $scope.$apply()
-      }
-    }
-
-    return (warnAboutUnsavedChanges = function () {
-      if (Document.hasUnsavedChanges()) {
-        return 'You have unsaved changes. If you leave now they will not be saved.'
-      }
-    })
-  },
-])

+ 0 - 9
services/web/frontend/js/ide/editor/controllers/SwitchToPDFButton.js

@@ -1,9 +0,0 @@
-import App from '../../../base'
-import { react2angular } from 'react2angular'
-import { rootContext } from '../../../shared/context/root-context'
-import SwitchToPDFButton from '../../../features/source-editor/components/switch-to-pdf-button'
-
-App.component(
-  'switchToPdfButton',
-  react2angular(rootContext.use(SwitchToPDFButton))
-)

+ 0 - 19
services/web/frontend/js/ide/editor/directives/formattingButtons.js

@@ -1,19 +0,0 @@
-import App from '../../../base'
-
-export default App.directive('formattingButtons', function () {
-  return {
-    scope: {
-      buttons: '=',
-      opening: '=',
-      isFullscreenEditor: '=',
-    },
-
-    link(scope, element, attrs) {
-      scope.showMore = false
-      scope.shownButtons = scope.buttons
-      scope.overflowedButtons = []
-    },
-
-    templateUrl: 'formattingButtonsTpl',
-  }
-})

+ 0 - 38
services/web/frontend/js/ide/editor/directives/toggleSwitch.js

@@ -1,38 +0,0 @@
-import App from '../../../base'
-
-export default App.directive('toggleSwitch', function () {
-  return {
-    restrict: 'E',
-    scope: {
-      description: '@',
-      labelFalse: '@',
-      labelTrue: '@',
-      ngModel: '=',
-    },
-    template: `\
-<fieldset class="toggle-switch">
-  <legend class="sr-only">{{description}}</legend>
-
-  <input
-    type="radio"
-    name="toggle-switch-{{$id}}"
-    class="toggle-switch-input"
-    id="toggle-switch-false-{{$id}}"
-    ng-value="false"
-    ng-model="ngModel"
-  >
-  <label for="toggle-switch-false-{{$id}}" class="toggle-switch-label"><span>{{labelFalse}}</span></label>
-
-  <input
-    type="radio"
-    class="toggle-switch-input"
-    name="toggle-switch-{{$id}}"
-    id="toggle-switch-true-{{$id}}"
-    ng-value="true"
-    ng-model="ngModel"
-  >
-  <label for="toggle-switch-true-{{$id}}" class="toggle-switch-label"><span>{{labelTrue}}</span></label>
-</fieldset>\
-`,
-  }
-})

+ 0 - 740
services/web/frontend/js/ide/file-tree/FileTreeManager.js

@@ -1,740 +0,0 @@
-/* eslint-disable
-    camelcase,
-    n/handle-callback-err,
-    max-len,
-    no-dupe-class-members,
-    no-return-assign,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * DS103: Rewrite code to no longer use __guard__
- * DS202: Simplify dynamic range loops
- * DS205: Consider reworking code to avoid use of IIFEs
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-import './controllers/FileTreeController'
-import '../../features/file-tree/controllers/file-tree-controller'
-import { debugConsole } from '@/utils/debugging'
-let FileTreeManager
-
-export default FileTreeManager = class FileTreeManager {
-  constructor(ide, $scope) {
-    this.ide = ide
-    this.$scope = $scope
-    this.$scope.$on('project:joined', () => {
-      this.loadRootFolder()
-      this.loadDeletedDocs()
-      return this.$scope.$emit('file-tree:initialized')
-    })
-
-    this.$scope.$on('entities:multiSelected', (_event, data) => {
-      this.$scope.$apply(() => {
-        this.$scope.multiSelectedCount = data.count
-        this.$scope.editor.multiSelectedCount = data.count
-      })
-    })
-
-    this.$scope.$watch('rootFolder', rootFolder => {
-      if (rootFolder != null) {
-        return this.recalculateDocList()
-      }
-    })
-
-    this._bindToSocketEvents()
-
-    this.$scope.multiSelectedCount = 0
-
-    $(document).on('click', () => {
-      this.clearMultiSelectedEntities()
-      setTimeout(() => this.$scope.$digest(), 0)
-    })
-  }
-
-  _bindToSocketEvents() {
-    this.ide.socket.on('reciveNewDoc', (parent_folder_id, doc) => {
-      const parent_folder =
-        this.findEntityById(parent_folder_id) || this.$scope.rootFolder
-      return this.$scope.$apply(() => {
-        parent_folder.children.push({
-          name: doc.name,
-          id: doc._id,
-          type: 'doc',
-        })
-        return this.recalculateDocList()
-      })
-    })
-
-    this.ide.socket.on(
-      'reciveNewFile',
-      (parent_folder_id, file, source, linkedFileData) => {
-        const parent_folder =
-          this.findEntityById(parent_folder_id) || this.$scope.rootFolder
-        return this.$scope.$apply(() => {
-          parent_folder.children.push({
-            name: file.name,
-            id: file._id,
-            type: 'file',
-            linkedFileData,
-            created: file.created,
-          })
-          return this.recalculateDocList()
-        })
-      }
-    )
-
-    this.ide.socket.on('reciveNewFolder', (parent_folder_id, folder) => {
-      const parent_folder =
-        this.findEntityById(parent_folder_id) || this.$scope.rootFolder
-      return this.$scope.$apply(() => {
-        parent_folder.children.push({
-          name: folder.name,
-          id: folder._id,
-          type: 'folder',
-          children: [],
-        })
-        return this.recalculateDocList()
-      })
-    })
-
-    this.ide.socket.on('reciveEntityRename', (entity_id, name) => {
-      const entity = this.findEntityById(entity_id)
-      if (entity == null) {
-        return
-      }
-      return this.$scope.$apply(() => {
-        entity.name = name
-        return this.recalculateDocList()
-      })
-    })
-
-    this.ide.socket.on('removeEntity', entity_id => {
-      const entity = this.findEntityById(entity_id)
-      if (entity == null) {
-        return
-      }
-      this.$scope.$apply(() => {
-        this._deleteEntityFromScope(entity)
-        return this.recalculateDocList()
-      })
-      return this.$scope.$broadcast('entity:deleted', entity)
-    })
-
-    return this.ide.socket.on('reciveEntityMove', (entity_id, folder_id) => {
-      const entity = this.findEntityById(entity_id)
-      const folder = this.findEntityById(folder_id)
-      return this.$scope.$apply(() => {
-        this._moveEntityInScope(entity, folder)
-        return this.recalculateDocList()
-      })
-    })
-  }
-
-  selectEntity(entity) {
-    this.selected_entity_id = entity.id // For reselecting after a reconnect
-    this.ide.fileTreeManager.forEachEntity(entity => (entity.selected = false))
-    return (entity.selected = true)
-  }
-
-  getMultiSelectedEntities() {
-    const entities = []
-    this.forEachEntity(function (e) {
-      if (e.multiSelected) {
-        return entities.push(e)
-      }
-    })
-    return entities
-  }
-
-  getFullCount() {
-    const entities = []
-    this.forEachEntity(function (e) {
-      if (!e.deleted) entities.push(e)
-    })
-    return entities.length
-  }
-
-  getMultiSelectedEntityChildNodes() {
-    // use pathnames with a leading slash to avoid
-    // problems with reserved Object properties
-    const entities = this.getMultiSelectedEntities()
-    const paths = {}
-    for (const entity of Array.from(entities)) {
-      paths['/' + this.getEntityPath(entity)] = entity
-    }
-    const prefixes = {}
-    for (const path in paths) {
-      const entity = paths[path]
-      const parts = path.split('/')
-      if (parts.length <= 2) {
-        continue
-      } else {
-        // Record prefixes a/b/c.tex -> 'a' and 'a/b'
-        for (
-          let i = 1, end = parts.length - 1, asc = end >= 1;
-          asc ? i <= end : i >= end;
-          asc ? i++ : i--
-        ) {
-          prefixes['/' + parts.slice(0, i).join('/')] = true
-        }
-      }
-    }
-    const child_entities = []
-    for (const path in paths) {
-      // If the path is in the prefixes, then it's a parent folder and
-      // should be ignore
-      const entity = paths[path]
-      if (prefixes[path] == null) {
-        child_entities.push(entity)
-      }
-    }
-    return child_entities
-  }
-
-  clearMultiSelectedEntities() {
-    if (this.$scope.multiSelectedCount === 0) {
-      return
-    } // Be efficient, this is called a lot on 'click'
-    this.forEachEntity(entity => (entity.multiSelected = false))
-    return (this.$scope.multiSelectedCount = 0)
-  }
-
-  existsInFolder(folder_id, name) {
-    const folder = this.findEntityById(folder_id)
-    if (folder == null) {
-      return false
-    }
-    const entity = this._findEntityByPathInFolder(folder, name)
-    return entity != null
-  }
-
-  findSelectedEntity() {
-    let selected = null
-    this.forEachEntity(function (entity) {
-      if (entity.selected) {
-        return (selected = entity)
-      }
-    })
-    return selected
-  }
-
-  findEntityById(id, options) {
-    if (options == null) {
-      options = {}
-    }
-    if (this.$scope.rootFolder.id === id) {
-      return this.$scope.rootFolder
-    }
-
-    let entity = this._findEntityByIdInFolder(this.$scope.rootFolder, id)
-    if (entity != null) {
-      return entity
-    }
-
-    if (options.includeDeleted) {
-      for (entity of Array.from(this.$scope.deletedDocs)) {
-        if (entity.id === id) {
-          return entity
-        }
-      }
-    }
-
-    return null
-  }
-
-  _findEntityByIdInFolder(folder, id) {
-    for (const entity of Array.from(folder.children || [])) {
-      if (entity.id === id) {
-        return entity
-      } else if (entity.children != null) {
-        const result = this._findEntityByIdInFolder(entity, id)
-        if (result != null) {
-          return result
-        }
-      }
-    }
-
-    return null
-  }
-
-  findEntityByPath(path) {
-    return this._findEntityByPathInFolder(this.$scope.rootFolder, path)
-  }
-
-  getPreviewByPath(path) {
-    for (const suffix of [
-      '',
-      '.png',
-      '.jpg',
-      '.jpeg',
-      '.pdf',
-      '.PNG',
-      '.JPG',
-      '.JPEG',
-      '.PDF',
-    ]) {
-      const entity = this.findEntityByPath(path + suffix)
-
-      if (entity) {
-        return {
-          url: `/project/${this.$scope.project._id}/file/${entity.id}`,
-          extension: entity.name.split('.').pop(),
-        }
-      }
-    }
-    return null
-  }
-
-  _findEntityByPathInFolder(folder, path) {
-    if (path == null || folder == null) {
-      return null
-    }
-    if (path === '') {
-      return folder
-    }
-
-    const parts = path.split('/')
-    const name = parts.shift()
-    const rest = parts.join('/')
-
-    if (name === '.') {
-      return this._findEntityByPathInFolder(folder, rest)
-    }
-
-    for (const entity of Array.from(folder.children)) {
-      if (entity.name === name) {
-        if (rest === '') {
-          return entity
-        } else if (entity.type === 'folder') {
-          return this._findEntityByPathInFolder(entity, rest)
-        }
-      }
-    }
-    return null
-  }
-
-  forEachEntity(callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    this._forEachEntityInFolder(this.$scope.rootFolder, null, callback)
-
-    return (() => {
-      const result = []
-      for (const entity of Array.from(this.$scope.deletedDocs || [])) {
-        result.push(callback(entity))
-      }
-      return result
-    })()
-  }
-
-  _forEachEntityInFolder(folder, path, callback) {
-    return (() => {
-      const result = []
-      for (const entity of Array.from(folder.children || [])) {
-        let childPath
-        if (path != null) {
-          childPath = path + '/' + entity.name
-        } else {
-          childPath = entity.name
-        }
-        callback(entity, folder, childPath)
-        if (entity.children != null) {
-          result.push(this._forEachEntityInFolder(entity, childPath, callback))
-        } else {
-          result.push(undefined)
-        }
-      }
-      return result
-    })()
-  }
-
-  getEntityPath(entity) {
-    return this._getEntityPathInFolder(this.$scope.rootFolder, entity)
-  }
-
-  _getEntityPathInFolder(folder, entity) {
-    for (const child of Array.from(folder.children || [])) {
-      if (child === entity) {
-        return entity.name
-      } else if (child.type === 'folder') {
-        const path = this._getEntityPathInFolder(child, entity)
-        if (path != null) {
-          return child.name + '/' + path
-        }
-      }
-    }
-    return null
-  }
-
-  getRootDocDirname() {
-    const rootDoc = this.findEntityById(this.$scope.project.rootDoc_id)
-    if (rootDoc == null) {
-      return
-    }
-    return this._getEntityDirname(rootDoc)
-  }
-
-  _getEntityDirname(entity) {
-    const path = this.getEntityPath(entity)
-    if (path == null) {
-      return
-    }
-    return path.split('/').slice(0, -1).join('/')
-  }
-
-  _findParentFolder(entity) {
-    const dirname = this._getEntityDirname(entity)
-    if (dirname == null) {
-      return
-    }
-    return this.findEntityByPath(dirname)
-  }
-
-  loadRootFolder() {
-    return (this.$scope.rootFolder = this._parseFolder(
-      __guard__(
-        this.$scope != null ? this.$scope.project : undefined,
-        x => x.rootFolder[0]
-      )
-    ))
-  }
-
-  _parseFolder(rawFolder) {
-    const folder = {
-      name: rawFolder.name,
-      id: rawFolder._id,
-      type: 'folder',
-      children: [],
-      selected: rawFolder._id === this.selected_entity_id,
-    }
-
-    for (const doc of Array.from(rawFolder.docs || [])) {
-      folder.children.push({
-        name: doc.name,
-        id: doc._id,
-        type: 'doc',
-        selected: doc._id === this.selected_entity_id,
-      })
-    }
-
-    for (const file of Array.from(rawFolder.fileRefs || [])) {
-      folder.children.push({
-        name: file.name,
-        id: file._id,
-        type: 'file',
-        selected: file._id === this.selected_entity_id,
-        linkedFileData: file.linkedFileData,
-        created: file.created,
-      })
-    }
-
-    for (const childFolder of Array.from(rawFolder.folders || [])) {
-      folder.children.push(this._parseFolder(childFolder))
-    }
-
-    return folder
-  }
-
-  loadDeletedDocs() {
-    this.$scope.deletedDocs = []
-    return Array.from(this.$scope.project.deletedDocs || []).map(doc =>
-      this.$scope.deletedDocs.push({
-        name: doc.name,
-        id: doc._id,
-        type: 'doc',
-        deleted: true,
-      })
-    )
-  }
-
-  recalculateDocList() {
-    this.$scope.docs = []
-    this.forEachEntity((entity, parentFolder, path) => {
-      if (entity.type === 'doc' && !entity.deleted) {
-        return this.$scope.docs.push({
-          doc: entity,
-          path,
-        })
-      }
-    })
-    // Keep list ordered by folders, then name
-    return this.$scope.docs.sort(function (a, b) {
-      const aDepth = (a.path.match(/\//g) || []).length
-      const bDepth = (b.path.match(/\//g) || []).length
-      if (aDepth - bDepth !== 0) {
-        return -(aDepth - bDepth) // Deeper path == folder first
-      } else if (a.path < b.path) {
-        return -1
-      } else if (a.path > b.path) {
-        return 1
-      } else {
-        return 0
-      }
-    })
-  }
-
-  getCurrentFolder() {
-    // Return the root folder if nothing is selected
-    return (
-      this._getCurrentFolder(this.$scope.rootFolder) || this.$scope.rootFolder
-    )
-  }
-
-  _getCurrentFolder(startFolder) {
-    if (startFolder == null) {
-      startFolder = this.$scope.rootFolder
-    }
-    for (const entity of Array.from(startFolder.children || [])) {
-      // The 'current' folder is either the one selected, or
-      // the one containing the selected doc/file
-      if (entity.selected) {
-        if (entity.type === 'folder') {
-          return entity
-        } else {
-          return startFolder
-        }
-      }
-
-      if (entity.type === 'folder') {
-        const result = this._getCurrentFolder(entity)
-        if (result != null) {
-          return result
-        }
-      }
-    }
-
-    return null
-  }
-
-  projectContainsFolder() {
-    for (const entity of Array.from(this.$scope.rootFolder.children)) {
-      if (entity.type === 'folder') {
-        return true
-      }
-    }
-    return false
-  }
-
-  existsInThisFolder(folder, name) {
-    for (const entity of Array.from(
-      (folder != null ? folder.children : undefined) || []
-    )) {
-      if (entity.name === name) {
-        return true
-      }
-    }
-    return false
-  }
-
-  nameExistsError(message) {
-    if (message == null) {
-      message = 'already exists'
-    }
-    const nameExists = this.ide.$q.defer()
-    nameExists.reject({ data: message })
-    return nameExists.promise
-  }
-
-  createDoc(name, parent_folder) {
-    // check if a doc/file/folder already exists with this name
-    if (parent_folder == null) {
-      parent_folder = this.getCurrentFolder()
-    }
-    if (this.existsInThisFolder(parent_folder, name)) {
-      return this.nameExistsError()
-    }
-    // We'll wait for the socket.io notification to actually
-    // add the doc for us.
-    return this.ide.$http.post(`/project/${this.ide.project_id}/doc`, {
-      name,
-      parent_folder_id: parent_folder != null ? parent_folder.id : undefined,
-      _csrf: window.csrfToken,
-    })
-  }
-
-  createFolder(name, parent_folder) {
-    // check if a doc/file/folder already exists with this name
-    if (parent_folder == null) {
-      parent_folder = this.getCurrentFolder()
-    }
-    if (this.existsInThisFolder(parent_folder, name)) {
-      return this.nameExistsError()
-    }
-    // We'll wait for the socket.io notification to actually
-    // add the folder for us.
-    return this.ide.$http.post(`/project/${this.ide.project_id}/folder`, {
-      name,
-      parent_folder_id: parent_folder != null ? parent_folder.id : undefined,
-      _csrf: window.csrfToken,
-    })
-  }
-
-  createLinkedFile(name, parent_folder, provider, data) {
-    // check if a doc/file/folder already exists with this name
-    if (parent_folder == null) {
-      parent_folder = this.getCurrentFolder()
-    }
-    if (this.existsInThisFolder(parent_folder, name)) {
-      return this.nameExistsError()
-    }
-    // We'll wait for the socket.io notification to actually
-    // add the file for us.
-    return this.ide.$http.post(
-      `/project/${this.ide.project_id}/linked_file`,
-      {
-        name,
-        parent_folder_id: parent_folder != null ? parent_folder.id : undefined,
-        provider,
-        data,
-        _csrf: window.csrfToken,
-      },
-      {
-        disableAutoLoginRedirect: true,
-      }
-    )
-  }
-
-  refreshLinkedFile(file) {
-    const parent_folder = this._findParentFolder(file)
-    const provider =
-      file.linkedFileData != null ? file.linkedFileData.provider : undefined
-    if (provider == null) {
-      debugConsole.warn(`>> no provider for ${file.name}`, file)
-      return
-    }
-    return this.ide.$http.post(
-      `/project/${this.ide.project_id}/linked_file/${file.id}/refresh`,
-      {
-        _csrf: window.csrfToken,
-      },
-      {
-        disableAutoLoginRedirect: true,
-      }
-    )
-  }
-
-  renameEntity(entity, name, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    if (entity.name === name) {
-      return
-    }
-    if (name.length >= 150) {
-      return
-    }
-    // check if a doc/file/folder already exists with this name
-    const parent_folder = this.getCurrentFolder()
-    if (this.existsInThisFolder(parent_folder, name)) {
-      return this.nameExistsError()
-    }
-    entity.renamingToName = name
-    return this.ide.$http
-      .post(
-        `/project/${this.ide.project_id}/${entity.type}/${entity.id}/rename`,
-        {
-          name,
-          _csrf: window.csrfToken,
-        }
-      )
-      .then(() => (entity.name = name))
-      .finally(() => (entity.renamingToName = null))
-  }
-
-  deleteEntity(entity, callback) {
-    // We'll wait for the socket.io notification to
-    // delete from scope.
-    if (callback == null) {
-      callback = function () {}
-    }
-    return this.ide.queuedHttp({
-      method: 'DELETE',
-      url: `/project/${this.ide.project_id}/${entity.type}/${entity.id}`,
-      headers: {
-        'X-Csrf-Token': window.csrfToken,
-      },
-    })
-  }
-
-  moveEntity(entity, parent_folder) {
-    // Abort move if the folder being moved (entity) has the parent_folder as child
-    // since that would break the tree structure.
-    if (this._isChildFolder(entity, parent_folder)) {
-      return
-    }
-    // check if a doc/file/folder already exists with this name
-    if (this.existsInThisFolder(parent_folder, entity.name)) {
-      throw new Error('file exists in this location')
-    }
-    // Wait for the http response before doing the move
-    this.ide.queuedHttp
-      .post(
-        `/project/${this.ide.project_id}/${entity.type}/${entity.id}/move`,
-        {
-          folder_id: parent_folder.id,
-          _csrf: window.csrfToken,
-        }
-      )
-      .then(() => {
-        this._moveEntityInScope(entity, parent_folder)
-      })
-  }
-
-  _isChildFolder(parent_folder, child_folder) {
-    const parent_path = this.getEntityPath(parent_folder) || '' // null if root folder
-    const child_path = this.getEntityPath(child_folder) || '' // null if root folder
-    // is parent path the beginning of child path?
-    return child_path.slice(0, parent_path.length) === parent_path
-  }
-
-  _deleteEntityFromScope(entity, options) {
-    if (options == null) {
-      options = { moveToDeleted: true }
-    }
-    if (entity == null) {
-      return
-    }
-    let parent_folder = null
-    this.forEachEntity(function (possible_entity, folder) {
-      if (possible_entity === entity) {
-        return (parent_folder = folder)
-      }
-    })
-
-    if (parent_folder != null) {
-      const index = parent_folder.children.indexOf(entity)
-      if (index > -1) {
-        parent_folder.children.splice(index, 1)
-      }
-    }
-
-    if (entity.type !== 'folder' && entity.selected) {
-      this.$scope.ui.view = null
-    }
-
-    if (entity.type === 'doc' && options.moveToDeleted) {
-      entity.deleted = true
-      return this.$scope.deletedDocs.push(entity)
-    }
-  }
-
-  _moveEntityInScope(entity, parent_folder) {
-    if (Array.from(parent_folder.children).includes(entity)) {
-      return
-    }
-    this._deleteEntityFromScope(entity, { moveToDeleted: false })
-    return parent_folder.children.push(entity)
-  }
-}
-
-function __guard__(value, transform) {
-  return typeof value !== 'undefined' && value !== null
-    ? transform(value)
-    : undefined
-}

+ 0 - 18
services/web/frontend/js/ide/file-tree/controllers/FileTreeController.js

@@ -1,18 +0,0 @@
-import App from '../../../base'
-App.controller('FileTreeController', [
-  '$scope',
-  function ($scope) {
-    $scope.openNewDocModal = () => {
-      window.dispatchEvent(
-        new CustomEvent('file-tree.start-creating', { detail: { mode: 'doc' } })
-      )
-    }
-
-    $scope.orderByFoldersFirst = function (entity) {
-      if ((entity != null ? entity.type : undefined) === 'folder') {
-        return '0'
-      }
-      return '1'
-    }
-  },
-])

+ 0 - 1
services/web/frontend/js/ide/file-view/index.js

@@ -1 +0,0 @@
-import '../../features/file-view/controllers/file-view-controller'

+ 0 - 28
services/web/frontend/js/ide/metadata/MetadataManager.js

@@ -1,28 +0,0 @@
-/* eslint-disable
-    max-len,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-let MetadataManager
-
-export default MetadataManager = class MetadataManager {
-  constructor(ide, $scope, metadata) {
-    this.ide = ide
-    this.$scope = $scope
-    this.metadata = metadata
-    this.ide.socket.on('broadcastDocMeta', data => {
-      return this.metadata.onBroadcastDocMeta(data)
-    })
-    this.$scope.$on('entity:deleted', this.metadata.onEntityDeleted)
-  }
-
-  loadProjectMetaFromServer() {
-    return this.metadata.loadProjectMetaFromServer()
-  }
-}

+ 0 - 129
services/web/frontend/js/ide/metadata/services/metadata.js

@@ -1,129 +0,0 @@
-import _ from 'lodash'
-/* eslint-disable
-    no-return-assign,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS205: Consider reworking code to avoid use of IIFEs
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-import App from '../../../base'
-
-export default App.factory('metadata', [
-  '$http',
-  'ide',
-  function ($http, ide) {
-    const debouncer = {} // DocId => Timeout
-
-    const state = { documents: {} }
-
-    const metadata = { state }
-
-    metadata.onBroadcastDocMeta = function (data) {
-      if (data.docId != null && data.meta != null) {
-        state.documents[data.docId] = data.meta
-        window.dispatchEvent(
-          new CustomEvent('project:metadata', { detail: state.documents })
-        )
-      }
-    }
-
-    metadata.onEntityDeleted = function (e, entity) {
-      if (entity.type === 'doc') {
-        return delete state.documents[entity.id]
-      }
-    }
-
-    metadata.getAllLabels = () =>
-      _.flattenDeep(
-        (() => {
-          const result = []
-          for (const docId in state.documents) {
-            const meta = state.documents[docId]
-            result.push(meta.labels)
-          }
-          return result
-        })()
-      )
-
-    metadata.getAllPackages = function () {
-      const packageCommandMapping = {}
-      for (const _docId in state.documents) {
-        const meta = state.documents[_docId]
-        for (const packageName in meta.packages) {
-          const commandSnippets = meta.packages[packageName]
-          packageCommandMapping[packageName] = commandSnippets
-        }
-      }
-      return packageCommandMapping
-    }
-
-    metadata.loadProjectMetaFromServer = () =>
-      $http
-        .get(`/project/${window.project_id}/metadata`)
-        .then(function (response) {
-          const { data } = response
-          if (data.projectMeta) {
-            return (() => {
-              const result = []
-              for (const docId in data.projectMeta) {
-                const docMeta = data.projectMeta[docId]
-                result.push((state.documents[docId] = docMeta))
-              }
-              window.dispatchEvent(
-                new CustomEvent('project:metadata', { detail: state.documents })
-              )
-              return result
-            })()
-          }
-        })
-
-    metadata.loadDocMetaFromServer = docId =>
-      $http
-        .post(`/project/${window.project_id}/doc/${docId}/metadata`, {
-          // Don't broadcast metadata when there are no other users in the
-          // project.
-          broadcast: ide.$scope.onlineUsersCount > 0,
-          _csrf: window.csrfToken,
-        })
-        .then(function (response) {
-          const { data } = response
-          // handle the POST response like a broadcast event when there are no
-          // other users in the project.
-          metadata.onBroadcastDocMeta(data)
-        })
-
-    metadata.scheduleLoadDocMetaFromServer = function (docId) {
-      if (ide.$scope.permissionsLevel === 'readOnly') {
-        // The POST request is blocked for users without write permission.
-        // The user will not be able to consume the meta data for edits anyways.
-        return
-      }
-      // De-bounce loading labels with a timeout
-      const existingTimeout = debouncer[docId]
-
-      if (existingTimeout != null) {
-        clearTimeout(existingTimeout)
-        delete debouncer[docId]
-      }
-
-      return (debouncer[docId] = setTimeout(() => {
-        // TODO: wait for the document to be saved?
-        metadata.loadDocMetaFromServer(docId)
-        return delete debouncer[docId]
-      }, 2000))
-    }
-
-    window.addEventListener('editor:metadata-outdated', () => {
-      metadata.scheduleLoadDocMetaFromServer(
-        ide.$scope.editor.sharejs_doc.doc_id
-      )
-    })
-
-    return metadata
-  },
-])

+ 0 - 176
services/web/frontend/js/ide/online-users/OnlineUsersManager.js

@@ -1,176 +0,0 @@
-/* eslint-disable
-    camelcase,
-    n/handle-callback-err,
-    max-len,
-    no-return-assign,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * DS206: Consider reworking classes to avoid initClass
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-import ColorManager from '../colors/ColorManager'
-
-let OnlineUsersManager
-
-export default OnlineUsersManager = (function () {
-  OnlineUsersManager = class OnlineUsersManager {
-    static initClass() {
-      this.prototype.cursorUpdateInterval = 500
-    }
-
-    constructor(ide, $scope) {
-      this.ide = ide
-      this.$scope = $scope
-      this.$scope.onlineUsers = {}
-      this.$scope.onlineUserCursorHighlights = {}
-      this.$scope.onlineUsersArray = []
-      this.$scope.onlineUsersCount = 0
-
-      this.$scope.$on('cursor:editor:update', (event, position) => {
-        return this.sendCursorPositionUpdate(position)
-      })
-
-      this.$scope.$on('project:joined', () => {
-        return this.ide.socket.emit(
-          'clientTracking.getConnectedUsers',
-          (error, connectedUsers) => {
-            this.$scope.onlineUsers = {}
-            for (const user of Array.from(connectedUsers || [])) {
-              if (user.client_id === this.ide.socket.publicId) {
-                // Don't store myself
-                continue
-              }
-              // Store data in the same format returned by clientTracking.clientUpdated
-
-              this.$scope.onlineUsers[user.client_id] = {
-                id: user.client_id,
-                user_id: user.user_id,
-                email: user.email,
-                name: `${user.first_name} ${user.last_name}`,
-                doc_id:
-                  user.cursorData != null ? user.cursorData.doc_id : undefined,
-                row: user.cursorData != null ? user.cursorData.row : undefined,
-                column:
-                  user.cursorData != null ? user.cursorData.column : undefined,
-              }
-            }
-            return this.refreshOnlineUsers()
-          }
-        )
-      })
-
-      this.ide.socket.on('clientTracking.clientUpdated', client => {
-        if (client.id !== this.ide.socket.publicId) {
-          // Check it's not me!
-          return this.$scope.$apply(() => {
-            this.$scope.onlineUsers[client.id] = client
-            return this.refreshOnlineUsers()
-          })
-        }
-      })
-
-      this.ide.socket.on('clientTracking.clientDisconnected', client_id => {
-        return this.$scope.$apply(() => {
-          delete this.$scope.onlineUsers[client_id]
-          return this.refreshOnlineUsers()
-        })
-      })
-
-      this.$scope.getHueForUserId = user_id => {
-        return ColorManager.getHueForUserId(user_id)
-      }
-    }
-
-    refreshOnlineUsers() {
-      this.$scope.onlineUsersArray = []
-
-      for (const client_id in this.$scope.onlineUsers) {
-        const user = this.$scope.onlineUsers[client_id]
-        if (user.doc_id != null) {
-          user.doc = this.ide.fileTreeManager.findEntityById(user.doc_id)
-        }
-
-        // If the user's name is empty use their email as display name
-        // Otherwise they're probably an anonymous user
-        if (user.name === null || user.name.trim().length === 0) {
-          if (user.email) {
-            user.name = user.email.trim()
-          } else if (user.user_id === 'anonymous-user') {
-            user.name = 'Anonymous'
-          }
-        }
-
-        user.initial = user.name != null ? user.name[0] : undefined
-        if (!user.initial || user.initial === ' ') {
-          user.initial = '?'
-        }
-
-        this.$scope.onlineUsersArray.push(user)
-      }
-
-      // keep a count of the other online users
-      this.$scope.onlineUsersCount = this.$scope.onlineUsersArray.length
-
-      this.$scope.onlineUserCursorHighlights = {}
-      for (const client_id in this.$scope.onlineUsers) {
-        const client = this.$scope.onlineUsers[client_id]
-        const { doc_id } = client
-        if (doc_id == null || client.row == null || client.column == null) {
-          continue
-        }
-        if (!this.$scope.onlineUserCursorHighlights[doc_id]) {
-          this.$scope.onlineUserCursorHighlights[doc_id] = []
-        }
-        this.$scope.onlineUserCursorHighlights[doc_id].push({
-          label: client.name,
-          cursor: {
-            row: client.row,
-            column: client.column,
-          },
-          hue: ColorManager.getHueForUserId(client.user_id),
-        })
-      }
-
-      if (this.$scope.onlineUsersArray.length > 0) {
-        delete this.cursorUpdateTimeout
-        return (this.cursorUpdateInterval = 500)
-      } else {
-        delete this.cursorUpdateTimeout
-        return (this.cursorUpdateInterval = 60 * 1000 * 5)
-      }
-    }
-
-    sendCursorPositionUpdate(position) {
-      if (position != null) {
-        this.$scope.currentPosition = position // keep track of the latest position
-      }
-      if (this.cursorUpdateTimeout == null) {
-        return (this.cursorUpdateTimeout = setTimeout(() => {
-          const doc_id = this.$scope.editor.open_doc_id
-          // always send the latest position to other clients
-          this.ide.socket.emit('clientTracking.updatePosition', {
-            row:
-              this.$scope.currentPosition != null
-                ? this.$scope.currentPosition.row
-                : undefined,
-            column:
-              this.$scope.currentPosition != null
-                ? this.$scope.currentPosition.column
-                : undefined,
-            doc_id,
-          })
-
-          return delete this.cursorUpdateTimeout
-        }, this.cursorUpdateInterval))
-      }
-    }
-  }
-  OnlineUsersManager.initClass()
-  return OnlineUsersManager
-})()

+ 0 - 49
services/web/frontend/js/ide/permissions/PermissionsManager.js

@@ -1,49 +0,0 @@
-/* eslint-disable
-    no-return-assign,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-let PermissionsManager
-
-export default PermissionsManager = class PermissionsManager {
-  constructor(ide, $scope) {
-    this.ide = ide
-    this.$scope = $scope
-    this.$scope.permissions = {
-      read: false,
-      write: false,
-      admin: false,
-      comment: false,
-    }
-    this.$scope.$watch('permissionsLevel', permissionsLevel => {
-      if (permissionsLevel != null) {
-        if (permissionsLevel === 'readOnly') {
-          this.$scope.permissions.read = true
-          this.$scope.permissions.write = false
-          this.$scope.permissions.admin = false
-          this.$scope.permissions.comment = true
-        } else if (permissionsLevel === 'readAndWrite') {
-          this.$scope.permissions.read = true
-          this.$scope.permissions.write = true
-          this.$scope.permissions.comment = true
-        } else if (permissionsLevel === 'owner') {
-          this.$scope.permissions.read = true
-          this.$scope.permissions.write = true
-          this.$scope.permissions.admin = true
-          this.$scope.permissions.comment = true
-        }
-      }
-
-      if (this.$scope.anonymous) {
-        return (this.$scope.permissions.comment = false)
-      }
-    })
-  }
-}

+ 0 - 110
services/web/frontend/js/ide/references/ReferencesManager.js

@@ -1,110 +0,0 @@
-import _ from 'lodash'
-/* eslint-disable
-    max-len,
-    no-return-assign,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS103: Rewrite code to no longer use __guard__
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-import CryptoJSSHA1 from 'crypto-js/sha1'
-let ReferencesManager
-
-export default ReferencesManager = class ReferencesManager {
-  constructor(ide, $scope) {
-    this.ide = ide
-    this.$scope = $scope
-    this.$scope.$root._references = this.state = { keys: [] }
-    this.existingIndexHash = {}
-
-    this.$scope.$on('document:closed', (e, doc) => {
-      let entity
-      if (doc.doc_id) {
-        entity = this.ide.fileTreeManager.findEntityById(doc.doc_id)
-      }
-      if (
-        __guard__(entity != null ? entity.name : undefined, x =>
-          x.match(/.*\.bib$/)
-        )
-      ) {
-        return this.indexReferencesIfDocModified(doc, true)
-      }
-    })
-
-    this.$scope.$on('references:should-reindex', (e, data) => {
-      return this.indexAllReferences(true)
-    })
-
-    // When we join the project:
-    //   index all references files
-    //   and don't broadcast to all clients
-    this.inited = false
-    this.$scope.$on('project:joined', e => {
-      // We only need to grab the references when the editor first loads,
-      // not on every reconnect
-      if (!this.inited) {
-        this.inited = true
-        this.ide.socket.on('references:keys:updated', (keys, allDocs) =>
-          this._storeReferencesKeys(keys, allDocs)
-        )
-        this.indexAllReferences(false)
-      }
-    })
-  }
-
-  _storeReferencesKeys(newKeys, replaceExistingKeys) {
-    const oldKeys = this.$scope.$root._references.keys
-    const keys = replaceExistingKeys ? newKeys : _.union(oldKeys, newKeys)
-    window.dispatchEvent(
-      new CustomEvent('project:references', {
-        detail: keys,
-      })
-    )
-    return (this.$scope.$root._references.keys = keys)
-  }
-
-  indexReferencesIfDocModified(doc, shouldBroadcast) {
-    // avoid reindexing references if the bib file has not changed since the
-    // last time they were indexed
-    const docId = doc.doc_id
-    const snapshot = doc._doc.snapshot
-    const now = Date.now()
-    const sha1 = CryptoJSSHA1(
-      'blob ' + snapshot.length + '\x00' + snapshot
-    ).toString()
-    const CACHE_LIFETIME = 6 * 3600 * 1000 // allow reindexing every 6 hours
-    const cacheEntry = this.existingIndexHash[docId]
-    const isCached =
-      cacheEntry &&
-      cacheEntry.timestamp > now - CACHE_LIFETIME &&
-      cacheEntry.hash === sha1
-    if (!isCached) {
-      this.indexAllReferences(shouldBroadcast)
-      this.existingIndexHash[docId] = { hash: sha1, timestamp: now }
-    }
-  }
-
-  indexAllReferences(shouldBroadcast) {
-    const opts = {
-      shouldBroadcast,
-      _csrf: window.csrfToken,
-    }
-    return this.ide.$http
-      .post(`/project/${this.$scope.project_id}/references/indexAll`, opts)
-      .then(response => {
-        return this._storeReferencesKeys(response.data.keys, true)
-      })
-  }
-}
-
-function __guard__(value, transform) {
-  return typeof value !== 'undefined' && value !== null
-    ? transform(value)
-    : undefined
-}

+ 0 - 1
services/web/frontend/js/ide/review-panel/ReviewPanelManager.js

@@ -1 +0,0 @@
-import './controllers/ReviewPanelController'

+ 0 - 1349
services/web/frontend/js/ide/review-panel/controllers/ReviewPanelController.js

@@ -1,1349 +0,0 @@
-/* eslint-disable
-    camelcase,
-    max-len,
-    no-return-assign,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * DS205: Consider reworking code to avoid use of IIFEs
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-import RangesTracker from '@overleaf/ranges-tracker'
-import App from '../../../base'
-import EventEmitter from '../../../utils/EventEmitter'
-import ColorManager from '../../colors/ColorManager'
-import getMeta from '../../../utils/meta'
-
-export default App.controller('ReviewPanelController', [
-  '$scope',
-  '$element',
-  'ide',
-  '$timeout',
-  '$http',
-  '$modal',
-  'eventTracking',
-  'localStorage',
-  function (
-    $scope,
-    $element,
-    ide,
-    $timeout,
-    $http,
-    $modal,
-    eventTracking,
-    localStorage
-  ) {
-    let UserTCSyncState
-    const $reviewPanelEl = $element.find('#review-panel')
-
-    const UserTypes = {
-      MEMBER: 'member', // Invited, listed in project.members
-      GUEST: 'guest', // Not invited, but logged in so has a user_id
-      ANONYMOUS: 'anonymous', // No user_id
-    }
-
-    const currentUserType = function () {
-      if ((ide.$scope.user != null ? ide.$scope.user.id : undefined) == null) {
-        return UserTypes.ANONYMOUS
-      } else {
-        const user_id = ide.$scope.user.id
-        const { project } = ide.$scope
-        if (
-          (project.owner != null ? project.owner.id : undefined) === user_id
-        ) {
-          return UserTypes.MEMBER
-        }
-        for (const member of Array.from(project.members)) {
-          if (member._id === user_id) {
-            return UserTypes.MEMBER
-          }
-        }
-        return UserTypes.GUEST
-      }
-    }
-
-    $scope.SubViews = {
-      CUR_FILE: 'cur_file',
-      OVERVIEW: 'overview',
-    }
-
-    $scope.UserTCSyncState = UserTCSyncState = {
-      SYNCED: 'synced',
-      PENDING: 'pending',
-    }
-
-    ide.$scope.reviewPanel = {
-      trackChangesState: {},
-      trackChangesOnForEveryone: false,
-      trackChangesOnForGuests: false,
-      trackChangesForGuestsAvailable: false,
-      entries: {},
-      resolvedComments: {},
-      hasEntries: false,
-      subView: $scope.SubViews.CUR_FILE,
-      openSubView: $scope.SubViews.CUR_FILE,
-      overview: {
-        loading: false,
-        docsCollapsedState:
-          JSON.parse(
-            localStorage(`docs_collapsed_state:${$scope.project_id}`)
-          ) || {},
-      },
-      dropdown: {
-        loading: false,
-      },
-      commentThreads: {},
-      resolvedThreadIds: {},
-      layoutToLeft: false,
-      rendererData: {},
-      formattedProjectMembers: {},
-      fullTCStateCollapsed: true,
-      // All selected changes. If a aggregated change (insertion + deletion) is selection, the two ids
-      // will be present. The length of this array will differ from the count below (see explanation).
-      selectedEntryIds: [],
-      // A count of user-facing selected changes. An aggregated change (insertion + deletion) will count
-      // as only one.
-      nVisibleSelectedChanges: 0,
-      entryHover: false,
-    }
-
-    ide.$scope.loadingThreads = true
-
-    window.addEventListener('beforeunload', function () {
-      const collapsedStates = {}
-      for (const doc in ide.$scope.reviewPanel.overview.docsCollapsedState) {
-        const state = ide.$scope.reviewPanel.overview.docsCollapsedState[doc]
-        if (state) {
-          collapsedStates[doc] = state
-        }
-      }
-      const valToStore =
-        Object.keys(collapsedStates).length > 0
-          ? JSON.stringify(collapsedStates)
-          : null
-      return localStorage(
-        `docs_collapsed_state:${$scope.project_id}`,
-        valToStore
-      )
-    })
-
-    $scope.$on('layout:pdf:linked', (event, state) =>
-      ide.$scope.$broadcast('review-panel:layout')
-    )
-
-    $scope.$on('layout:pdf:resize', (event, state) => {
-      ide.$scope.reviewPanel.layoutToLeft =
-        state.east?.size < 220 || state.east?.initClosed
-      ide.$scope.$broadcast('review-panel:layout', false)
-    })
-
-    $scope.$on('review-panel:sizes', (e, sizes) => {
-      $scope.$broadcast('editor:set-scroll-size', sizes)
-      dispatchReviewPanelEvent('sizes', sizes)
-    })
-
-    $scope.$watch('project.features.trackChangesVisible', function (visible) {
-      if (visible == null) {
-        return
-      }
-      if (!visible) {
-        return ($scope.ui.reviewPanelOpen = false)
-      }
-    })
-
-    $scope.$watch('project.members', function (members) {
-      ide.$scope.reviewPanel.formattedProjectMembers = {}
-      if (($scope.project != null ? $scope.project.owner : undefined) != null) {
-        ide.$scope.reviewPanel.formattedProjectMembers[
-          $scope.project.owner._id
-        ] = formatUser($scope.project.owner)
-      }
-      if (
-        ($scope.project != null ? $scope.project.members : undefined) != null
-      ) {
-        return (() => {
-          const result = []
-          for (const member of Array.from(members)) {
-            if (member.privileges === 'readAndWrite') {
-              if (
-                ide.$scope.reviewPanel.trackChangesState[member._id] == null
-              ) {
-                // An added member will have track changes enabled if track changes is on for everyone
-                _setUserTCState(
-                  member._id,
-                  ide.$scope.reviewPanel.trackChangesOnForEveryone,
-                  true
-                )
-              }
-              result.push(
-                (ide.$scope.reviewPanel.formattedProjectMembers[member._id] =
-                  formatUser(member))
-              )
-            } else {
-              result.push(undefined)
-            }
-          }
-          return result
-        })()
-      }
-    })
-
-    $scope.commentState = {
-      adding: false,
-      content: '',
-    }
-
-    ide.$scope.users = $scope.users = {}
-
-    ide.$scope.reviewPanelEventsBridge = new EventEmitter()
-
-    ide.socket.on('new-comment', function (thread_id, comment) {
-      const thread = getThread(thread_id)
-      delete thread.submitting
-      thread.messages.push(formatComment(comment))
-      $scope.$apply()
-      return $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
-    })
-
-    ide.socket.on('accept-changes', function (doc_id, change_ids) {
-      if (doc_id !== $scope.editor.open_doc_id) {
-        getChangeTracker(doc_id).removeChangeIds(change_ids)
-      } else {
-        $scope.$broadcast('changes:accept', change_ids)
-        dispatchReviewPanelEvent('changes:accept', change_ids)
-      }
-      updateEntries(doc_id)
-      return $scope.$apply(function () {})
-    })
-
-    ide.socket.on('resolve-thread', (thread_id, user) =>
-      _onCommentResolved(thread_id, user)
-    )
-
-    ide.socket.on('reopen-thread', thread_id => _onCommentReopened(thread_id))
-
-    ide.socket.on('delete-thread', function (thread_id) {
-      _onThreadDeleted(thread_id)
-      return $scope.$apply(function () {})
-    })
-
-    ide.socket.on('edit-message', function (thread_id, message_id, content) {
-      _onCommentEdited(thread_id, message_id, content)
-      return $scope.$apply(function () {})
-    })
-
-    ide.socket.on('delete-message', function (thread_id, message_id) {
-      _onCommentDeleted(thread_id, message_id)
-      return $scope.$apply(function () {})
-    })
-
-    const rangesTrackers = {}
-
-    const getDocEntries = function (doc_id) {
-      if (ide.$scope.reviewPanel.entries[doc_id] == null) {
-        ide.$scope.reviewPanel.entries[doc_id] = {}
-      }
-      return ide.$scope.reviewPanel.entries[doc_id]
-    }
-
-    const getDocResolvedComments = function (doc_id) {
-      if (ide.$scope.reviewPanel.resolvedComments[doc_id] == null) {
-        ide.$scope.reviewPanel.resolvedComments[doc_id] = {}
-      }
-      return ide.$scope.reviewPanel.resolvedComments[doc_id]
-    }
-
-    function getThread(thread_id) {
-      if (ide.$scope.reviewPanel.commentThreads[thread_id] == null) {
-        ide.$scope.reviewPanel.commentThreads[thread_id] = { messages: [] }
-      }
-      return ide.$scope.reviewPanel.commentThreads[thread_id]
-    }
-
-    function getChangeTracker(doc_id) {
-      if (rangesTrackers[doc_id] == null) {
-        rangesTrackers[doc_id] = new RangesTracker()
-        rangesTrackers[doc_id].resolvedThreadIds =
-          ide.$scope.reviewPanel.resolvedThreadIds
-      }
-      return rangesTrackers[doc_id]
-    }
-
-    $scope.$watch(
-      '!ui.reviewPanelOpen && reviewPanel.hasEntries',
-      function (open, prevVal) {
-        if (open == null) {
-          return
-        }
-        $scope.ui.miniReviewPanelVisible = open
-        if (open !== prevVal) {
-          return $timeout(() => $scope.$broadcast('review-panel:toggle'))
-        }
-      }
-    )
-
-    $scope.$watch('ui.reviewPanelOpen', function (open) {
-      if (open == null) {
-        return
-      }
-      if (!open) {
-        // Always show current file when not open, but save current state
-        ide.$scope.reviewPanel.openSubView = ide.$scope.reviewPanel.subView
-        ide.$scope.reviewPanel.subView = $scope.SubViews.CUR_FILE
-      } else {
-        // Reset back to what we had when previously open
-        ide.$scope.reviewPanel.subView = ide.$scope.reviewPanel.openSubView
-      }
-      return $timeout(function () {
-        $scope.$broadcast('review-panel:toggle')
-        return ide.$scope.$broadcast('review-panel:layout', false)
-      })
-    })
-
-    $scope.$watch('reviewPanel.subView', function (view, oldView) {
-      if (view == null) {
-        return
-      }
-      if (view === $scope.SubViews.OVERVIEW) {
-        return refreshOverviewPanel()
-      } else if (oldView === $scope.SubViews.OVERVIEW) {
-        dispatchReviewPanelEvent('overview-closed', view)
-      }
-    })
-
-    $scope.$watch('editor.sharejs_doc', function (doc, old_doc) {
-      if (doc == null) {
-        return
-      }
-      // The open doc range tracker is kept up to date in real-time so
-      // replace any outdated info with this
-      rangesTrackers[doc.doc_id] = doc.ranges
-      rangesTrackers[doc.doc_id].resolvedThreadIds =
-        ide.$scope.reviewPanel.resolvedThreadIds
-      ide.$scope.reviewPanel.rangesTracker = rangesTrackers[doc.doc_id]
-      if (old_doc != null) {
-        old_doc.off('flipped_pending_to_inflight')
-      }
-      doc.on('flipped_pending_to_inflight', () => regenerateTrackChangesId(doc))
-      return regenerateTrackChangesId(doc)
-    })
-
-    $scope.$watch(
-      function () {
-        const entries =
-          ide.$scope.reviewPanel.entries[$scope.editor.open_doc_id] || {}
-        const permEntries = {}
-        for (const entry in entries) {
-          const entryData = entries[entry]
-          if (!['add-comment', 'bulk-actions'].includes(entry)) {
-            permEntries[entry] = entryData
-          }
-        }
-        return Object.keys(permEntries).length
-      },
-      nEntries =>
-        (ide.$scope.reviewPanel.hasEntries =
-          nEntries > 0 && $scope.project.features.trackChangesVisible)
-    )
-
-    function regenerateTrackChangesId(doc) {
-      const old_id = getChangeTracker(doc.doc_id).getIdSeed()
-      const new_id = RangesTracker.generateIdSeed()
-      getChangeTracker(doc.doc_id).setIdSeed(new_id)
-      return doc.setTrackChangesIdSeeds({ pending: new_id, inflight: old_id })
-    }
-
-    const refreshRanges = () =>
-      $http
-        .get(`/project/${$scope.project_id}/ranges`)
-        .then(function (response) {
-          const docs = response.data
-          return (() => {
-            const result = []
-            for (const doc of Array.from(docs)) {
-              if (
-                ide.$scope.reviewPanel.overview.docsCollapsedState[doc.id] ==
-                null
-              ) {
-                ide.$scope.reviewPanel.overview.docsCollapsedState[doc.id] =
-                  false
-              }
-              if (doc.id !== $scope.editor.open_doc_id) {
-                // this is kept up to date in real-time, don't overwrite
-                const rangesTracker = getChangeTracker(doc.id)
-                rangesTracker.comments =
-                  (doc.ranges != null ? doc.ranges.comments : undefined) || []
-                rangesTracker.changes =
-                  (doc.ranges != null ? doc.ranges.changes : undefined) || []
-              }
-              result.push(updateEntries(doc.id))
-            }
-            return result
-          })()
-        })
-
-    function refreshOverviewPanel() {
-      ide.$scope.reviewPanel.overview.loading = true
-      return refreshRanges()
-        .then(() => (ide.$scope.reviewPanel.overview.loading = false))
-        .catch(() => (ide.$scope.reviewPanel.overview.loading = false))
-    }
-
-    ide.$scope.refreshResolvedCommentsDropdown = function () {
-      ide.$scope.reviewPanel.dropdown.loading = true
-      const q = refreshRanges()
-      q.then(() => (ide.$scope.reviewPanel.dropdown.loading = false))
-      q.catch(() => (ide.$scope.reviewPanel.dropdown.loading = false))
-      return q
-    }
-
-    async function updateEntries(doc_id) {
-      const rangesTracker = getChangeTracker(doc_id)
-      const entries = getDocEntries(doc_id)
-      const resolvedComments = getDocResolvedComments(doc_id)
-
-      let changed = false
-
-      // Assume we'll delete everything until we see it, then we'll remove it from this object
-      const delete_changes = {}
-      for (const id in entries) {
-        const change = entries[id]
-        if (!['add-comment', 'bulk-actions'].includes(id)) {
-          for (const entry_id of Array.from(change.entry_ids)) {
-            delete_changes[entry_id] = true
-          }
-        }
-      }
-      for (const id in resolvedComments) {
-        const change = resolvedComments[id]
-        for (const entry_id of Array.from(change.entry_ids)) {
-          delete_changes[entry_id] = true
-        }
-      }
-
-      let potential_aggregate = false
-      let prev_insertion = null
-
-      for (const change of Array.from(rangesTracker.changes)) {
-        changed = true
-
-        if (
-          potential_aggregate &&
-          change.op.d &&
-          change.op.p === prev_insertion.op.p + prev_insertion.op.i.length &&
-          change.metadata.user_id === prev_insertion.metadata.user_id
-        ) {
-          // An actual aggregate op.
-          entries[prev_insertion.id].type = 'aggregate-change'
-          entries[prev_insertion.id].metadata.replaced_content = change.op.d
-          entries[prev_insertion.id].entry_ids.push(change.id)
-        } else {
-          if (entries[change.id] == null) {
-            entries[change.id] = {}
-          }
-          delete delete_changes[change.id]
-          const new_entry = {
-            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 in new_entry) {
-            entries[change.id][key] = new_entry[key]
-          }
-        }
-
-        if (change.op.i) {
-          potential_aggregate = true
-          prev_insertion = change
-        } else {
-          potential_aggregate = false
-          prev_insertion = null
-        }
-
-        if ($scope.users[change.metadata.user_id] == null) {
-          if (!window.isRestrictedTokenMember) {
-            refreshChangeUsers(change.metadata.user_id)
-          }
-        }
-      }
-
-      if (!window.isRestrictedTokenMember) {
-        if (rangesTracker.comments.length > 0) {
-          await ensureThreadsAreLoaded()
-        } else if (ide.$scope.loadingThreads === true) {
-          // ensure that tracked changes are highlighted even if no comments are loaded
-          ide.$scope.loadingThreads = false
-          dispatchReviewPanelEvent('loaded_threads')
-        }
-      }
-
-      if (!_loadingThreadsInProgress) {
-        for (const comment of Array.from(rangesTracker.comments)) {
-          let new_comment
-          changed = true
-          delete delete_changes[comment.id]
-          if (ide.$scope.reviewPanel.resolvedThreadIds[comment.op.t]) {
-            new_comment =
-              resolvedComments[comment.id] != null
-                ? resolvedComments[comment.id]
-                : (resolvedComments[comment.id] = {})
-            delete entries[comment.id]
-          } else {
-            new_comment =
-              entries[comment.id] != null
-                ? entries[comment.id]
-                : (entries[comment.id] = {})
-            delete resolvedComments[comment.id]
-          }
-          const new_entry = {
-            type: 'comment',
-            thread_id: comment.op.t,
-            entry_ids: [comment.id],
-            content: comment.op.c,
-            offset: comment.op.p,
-          }
-          for (const key in new_entry) {
-            new_comment[key] = new_entry[key]
-          }
-        }
-      }
-
-      for (const change_id in delete_changes) {
-        const _ = delete_changes[change_id]
-        changed = true
-        delete entries[change_id]
-        delete resolvedComments[change_id]
-      }
-
-      if (changed) {
-        // TODO: unused?
-        $scope.$broadcast('entries:changed')
-      }
-
-      return entries
-    }
-
-    $scope.$on('editor:track-changes:changed', async function () {
-      const doc_id = $scope.editor.open_doc_id
-      const entries = await updateEntries(doc_id)
-
-      $scope.$broadcast('review-panel:recalculate-screen-positions')
-      dispatchReviewPanelEvent('recalculate-screen-positions', {
-        entries,
-        updateType: 'trackedChangesChange',
-      })
-
-      // Ensure that watchers, such as the React-based review panel component,
-      // are informed of the changes to entries
-      ide.$scope.$apply()
-
-      return ide.$scope.$broadcast('review-panel:layout')
-    })
-
-    $scope.$on('editor:track-changes:visibility_changed', () =>
-      $timeout(() => ide.$scope.$broadcast('review-panel:layout', false))
-    )
-
-    $scope.$on(
-      'editor:focus:changed',
-      function (
-        e,
-        selection_offset_start,
-        selection_offset_end,
-        selection,
-        updateType = null
-      ) {
-        const doc_id = $scope.editor.open_doc_id
-        const entries = getDocEntries(doc_id)
-        // All selected changes will be added to this array.
-        ide.$scope.reviewPanel.selectedEntryIds = []
-        // Count of user-visible changes, i.e. an aggregated change will count as one.
-        ide.$scope.reviewPanel.nVisibleSelectedChanges = 0
-
-        const offset = selection_offset_start
-        const length = selection_offset_end - selection_offset_start
-
-        // 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 = entries['add-comment']
-          if (
-            !existingAddComment ||
-            existingAddComment.offset !== offset ||
-            existingAddComment.length !== length
-          ) {
-            entries['add-comment'] = {
-              type: 'add-comment',
-              offset,
-              length,
-            }
-          }
-          const existingBulkActions = entries['bulk-actions']
-          if (
-            !existingBulkActions ||
-            existingBulkActions.offset !== offset ||
-            existingBulkActions.length !== length
-          ) {
-            entries['bulk-actions'] = {
-              type: 'bulk-actions',
-              offset,
-              length,
-            }
-          }
-        } else {
-          delete entries['add-comment']
-          delete entries['bulk-actions']
-        }
-
-        for (const id in entries) {
-          const entry = entries[id]
-          let isChangeEntryAndWithinSelection = false
-          if (
-            entry.type === 'comment' &&
-            !ide.$scope.reviewPanel.resolvedThreadIds[entry.thread_id]
-          ) {
-            entry.focused =
-              entry.offset <= selection_offset_start &&
-              selection_offset_start <= entry.offset + entry.content.length
-          } else if (entry.type === 'insert') {
-            isChangeEntryAndWithinSelection =
-              entry.offset >= selection_offset_start &&
-              entry.offset + entry.content.length <= selection_offset_end
-            entry.focused =
-              entry.offset <= selection_offset_start &&
-              selection_offset_start <= entry.offset + entry.content.length
-          } else if (entry.type === 'delete') {
-            isChangeEntryAndWithinSelection =
-              selection_offset_start <= entry.offset &&
-              entry.offset <= selection_offset_end
-            entry.focused = entry.offset === selection_offset_start
-          } else if (entry.type === 'aggregate-change') {
-            isChangeEntryAndWithinSelection =
-              entry.offset >= selection_offset_start &&
-              entry.offset + entry.content.length <= selection_offset_end
-            entry.focused =
-              entry.offset <= selection_offset_start &&
-              selection_offset_start <= entry.offset + entry.content.length
-          } else if (
-            ['add-comment', 'bulk-actions'].includes(entry.type) &&
-            selection
-          ) {
-            entry.focused = true
-          }
-
-          if (isChangeEntryAndWithinSelection) {
-            for (const entry_id of Array.from(entry.entry_ids)) {
-              ide.$scope.reviewPanel.selectedEntryIds.push(entry_id)
-            }
-            ide.$scope.reviewPanel.nVisibleSelectedChanges++
-          }
-        }
-
-        $scope.$broadcast('review-panel:recalculate-screen-positions')
-
-        dispatchReviewPanelEvent('recalculate-screen-positions', {
-          entries,
-          updateType,
-        })
-
-        // Ensure that watchers, such as the React-based review panel component,
-        // are informed of the changes to entries
-        ide.$scope.$apply()
-
-        return ide.$scope.$broadcast('review-panel:layout')
-      }
-    )
-
-    ide.$scope.acceptChanges = function (change_ids) {
-      _doAcceptChanges(change_ids)
-      eventTracking.sendMB('rp-changes-accepted', {
-        view: $scope.ui.reviewPanelOpen
-          ? ide.$scope.reviewPanel.subView
-          : 'mini',
-      })
-    }
-
-    ide.$scope.rejectChanges = function (change_ids) {
-      _doRejectChanges(change_ids)
-      eventTracking.sendMB('rp-changes-rejected', {
-        view: $scope.ui.reviewPanelOpen
-          ? ide.$scope.reviewPanel.subView
-          : 'mini',
-      })
-    }
-
-    // The next two functions control a class on the review panel that affects
-    // the overflow-y rule on the panel. This is necessary so that an entry in
-    // the review panel is visible when hovering over its indicator when the
-    // review panel is minimized. See issue #8057.
-    $scope.mouseEnterIndicator = function () {
-      ide.$scope.reviewPanel.entryHover = true
-    }
-
-    $scope.mouseLeaveIndicator = function () {
-      ide.$scope.reviewPanel.entryHover = false
-    }
-
-    function _doAcceptChanges(change_ids) {
-      $http.post(
-        `/project/${$scope.project_id}/doc/${$scope.editor.open_doc_id}/changes/accept`,
-        { change_ids, _csrf: window.csrfToken }
-      )
-      $scope.$broadcast('changes:accept', change_ids)
-      dispatchReviewPanelEvent('changes:accept', change_ids)
-    }
-
-    const _doRejectChanges = change_ids => {
-      $scope.$broadcast('changes:reject', change_ids)
-      dispatchReviewPanelEvent('changes:reject', change_ids)
-    }
-
-    ide.$scope.bulkAcceptActions = function () {
-      _doAcceptChanges(ide.$scope.reviewPanel.selectedEntryIds.slice())
-      eventTracking.sendMB('rp-bulk-accept', {
-        view: $scope.ui.reviewPanelOpen
-          ? ide.$scope.reviewPanel.subView
-          : 'mini',
-        nEntries: ide.$scope.reviewPanel.nVisibleSelectedChanges,
-      })
-    }
-
-    ide.$scope.bulkRejectActions = function () {
-      _doRejectChanges(ide.$scope.reviewPanel.selectedEntryIds.slice())
-      eventTracking.sendMB('rp-bulk-reject', {
-        view: $scope.ui.reviewPanelOpen
-          ? ide.$scope.reviewPanel.subView
-          : 'mini',
-        nEntries: ide.$scope.reviewPanel.nVisibleSelectedChanges,
-      })
-    }
-
-    ide.$scope.addNewComment = function (e) {
-      e.preventDefault()
-      ide.$scope.$broadcast('comment:start_adding')
-      return $scope.toggleReviewPanel()
-    }
-
-    $scope.addNewCommentFromKbdShortcut = function () {
-      if (!$scope.project.features.trackChangesVisible) {
-        return
-      }
-      $scope.$broadcast('comment:select_line')
-      dispatchReviewPanelEvent('comment:select_line')
-
-      if (!$scope.ui.reviewPanelOpen) {
-        $scope.toggleReviewPanel()
-      }
-      return $timeout(function () {
-        ide.$scope.$broadcast('review-panel:layout')
-        ide.$scope.$broadcast('comment:start_adding')
-      })
-    }
-
-    $scope.startNewComment = function () {
-      $scope.$broadcast('comment:select_line')
-      dispatchReviewPanelEvent('comment:select_line')
-      return $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
-    }
-
-    ide.$scope.submitNewComment = function (content) {
-      if (content == null || content === '') {
-        return
-      }
-      const doc_id = $scope.editor.open_doc_id
-      const entries = getDocEntries(doc_id)
-      if (entries['add-comment'] == null) {
-        return
-      }
-      const { offset, length } = entries['add-comment']
-      const thread_id = RangesTracker.generateId()
-      const thread = getThread(thread_id)
-      thread.submitting = true
-
-      const emitCommentAdd = () => {
-        $scope.$broadcast('comment:add', thread_id, offset, length)
-        dispatchReviewPanelEvent('comment:add', {
-          threadId: thread_id,
-          offset,
-          length,
-        })
-
-        // TODO: unused?
-        $scope.$broadcast('editor:clearSelection')
-        $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
-        eventTracking.sendMB('rp-new-comment', { size: content.length })
-      }
-
-      return $http
-        .post(`/project/${$scope.project_id}/thread/${thread_id}/messages`, {
-          content,
-          _csrf: window.csrfToken,
-        })
-        .then(() => {
-          emitCommentAdd()
-        })
-        .catch(() => {
-          ide.showGenericMessageModal(
-            'Error submitting comment',
-            'Sorry, there was a problem submitting your comment'
-          )
-          throw Error('Error submitting comment')
-        })
-    }
-
-    $scope.cancelNewComment = entry =>
-      $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
-
-    $scope.startReply = function (entry) {
-      entry.replying = true
-      return $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
-    }
-
-    ide.$scope.submitReply = function (entry, entry_id) {
-      const { thread_id } = entry
-      const content = entry.replyContent
-      $http
-        .post(`/project/${$scope.project_id}/thread/${thread_id}/messages`, {
-          content,
-          _csrf: window.csrfToken,
-        })
-        .catch(() =>
-          ide.showGenericMessageModal(
-            'Error submitting comment',
-            'Sorry, there was a problem submitting your comment'
-          )
-        )
-
-      const trackingMetadata = {
-        view: $scope.ui.reviewPanelOpen
-          ? ide.$scope.reviewPanel.subView
-          : 'mini',
-        size: entry.replyContent.length,
-        thread: thread_id,
-      }
-
-      const thread = getThread(thread_id)
-      thread.submitting = true
-      entry.replyContent = ''
-      entry.replying = false
-      $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
-      eventTracking.sendMB('rp-comment-reply', trackingMetadata)
-    }
-
-    $scope.cancelReply = function (entry) {
-      entry.replying = false
-      entry.replyContent = ''
-      return ide.$scope.$broadcast('review-panel:layout')
-    }
-
-    ide.$scope.resolveComment = function (doc_id, entry_id) {
-      const entry = getDocEntries(doc_id)[entry_id]
-      entry.focused = false
-      $http.post(
-        `/project/${$scope.project_id}/doc/${doc_id}/thread/${entry.thread_id}/resolve`,
-        { _csrf: window.csrfToken }
-      )
-      _onCommentResolved(entry.thread_id, ide.$scope.user)
-      eventTracking.sendMB('rp-comment-resolve', {
-        view: $scope.ui.reviewPanelOpen
-          ? ide.$scope.reviewPanel.subView
-          : 'mini',
-      })
-    }
-
-    ide.$scope.unresolveComment = function (doc_id, thread_id) {
-      _onCommentReopened(thread_id)
-      $http.post(
-        `/project/${$scope.project_id}/doc/${doc_id}/thread/${thread_id}/reopen`,
-        {
-          _csrf: window.csrfToken,
-        }
-      )
-      eventTracking.sendMB('rp-comment-reopen')
-    }
-
-    function _onCommentResolved(thread_id, user) {
-      const thread = getThread(thread_id)
-      if (thread == null) {
-        return
-      }
-      thread.resolved = true
-      thread.resolved_by_user = formatUser(user)
-      thread.resolved_at = new Date().toISOString()
-      ide.$scope.reviewPanel.resolvedThreadIds[thread_id] = true
-      $scope.$broadcast('comment:resolve_threads', [thread_id])
-      dispatchReviewPanelEvent('comment:resolve_threads', [thread_id])
-    }
-
-    function _onCommentReopened(thread_id) {
-      const thread = getThread(thread_id)
-      if (thread == null) {
-        return
-      }
-      delete thread.resolved
-      delete thread.resolved_by_user
-      delete thread.resolved_at
-      delete ide.$scope.reviewPanel.resolvedThreadIds[thread_id]
-      $scope.$broadcast('comment:unresolve_thread', thread_id)
-      dispatchReviewPanelEvent('comment:unresolve_thread', thread_id)
-    }
-
-    function _onThreadDeleted(thread_id) {
-      delete ide.$scope.reviewPanel.resolvedThreadIds[thread_id]
-      delete ide.$scope.reviewPanel.commentThreads[thread_id]
-      $scope.$broadcast('comment:remove', thread_id)
-      dispatchReviewPanelEvent('comment:remove', thread_id)
-    }
-
-    function _onCommentEdited(thread_id, comment_id, content) {
-      const thread = getThread(thread_id)
-      if (thread == null) {
-        return
-      }
-      for (const message of Array.from(thread.messages)) {
-        if (message.id === comment_id) {
-          message.content = content
-        }
-      }
-      return updateEntries()
-    }
-
-    function _onCommentDeleted(thread_id, comment_id) {
-      const thread = getThread(thread_id)
-      if (thread == null) {
-        return
-      }
-      thread.messages = thread.messages.filter(m => m.id !== comment_id)
-      return updateEntries()
-    }
-
-    ide.$scope.deleteThread = function (entry_id, doc_id, thread_id) {
-      _onThreadDeleted(thread_id)
-      $http({
-        method: 'DELETE',
-        url: `/project/${$scope.project_id}/doc/${doc_id}/thread/${thread_id}`,
-        headers: {
-          'X-CSRF-Token': window.csrfToken,
-        },
-      })
-      eventTracking.sendMB('rp-comment-delete')
-    }
-
-    ide.$scope.saveEdit = function (thread_id, comment_id, content) {
-      $http.post(
-        `/project/${$scope.project_id}/thread/${thread_id}/messages/${comment_id}/edit`,
-        {
-          content,
-          _csrf: window.csrfToken,
-        }
-      )
-      return $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
-    }
-
-    ide.$scope.deleteComment = function (thread_id, comment_id) {
-      _onCommentDeleted(thread_id, comment_id)
-      $http({
-        method: 'DELETE',
-        url: `/project/${$scope.project_id}/thread/${thread_id}/messages/${comment_id}`,
-        headers: {
-          'X-CSRF-Token': window.csrfToken,
-        },
-      })
-      return $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
-    }
-
-    $scope.setSubView = function (subView) {
-      ide.$scope.reviewPanel.subView = subView
-      eventTracking.sendMB('rp-subview-change', { subView })
-    }
-
-    ide.$scope.gotoEntry = (doc_id, entry_offset) =>
-      ide.editorManager.openDocId(doc_id, { gotoOffset: entry_offset })
-
-    const _setUserTCState = function (userId, newValue, isLocal) {
-      if (isLocal == null) {
-        isLocal = false
-      }
-      if (ide.$scope.reviewPanel.trackChangesState[userId] == null) {
-        ide.$scope.reviewPanel.trackChangesState[userId] = {}
-      }
-      const state = ide.$scope.reviewPanel.trackChangesState[userId]
-
-      if (
-        state.syncState == null ||
-        state.syncState === UserTCSyncState.SYNCED
-      ) {
-        state.value = newValue
-        state.syncState = UserTCSyncState.SYNCED
-      } else if (
-        state.syncState === UserTCSyncState.PENDING &&
-        state.value === newValue
-      ) {
-        state.syncState = UserTCSyncState.SYNCED
-      } else if (isLocal) {
-        state.value = newValue
-        state.syncState = UserTCSyncState.PENDING
-      }
-      if (userId === ide.$scope.user.id) {
-        return ($scope.editor.wantTrackChanges = newValue)
-      }
-    }
-
-    const _setEveryoneTCState = function (newValue, isLocal) {
-      if (isLocal == null) {
-        isLocal = false
-      }
-      ide.$scope.reviewPanel.trackChangesOnForEveryone = newValue
-      const { project } = $scope
-      for (const member of Array.from(project.members)) {
-        _setUserTCState(member._id, newValue, isLocal)
-      }
-      _setGuestsTCState(newValue, isLocal)
-      return _setUserTCState(project.owner._id, newValue, isLocal)
-    }
-
-    function _setGuestsTCState(newValue, isLocal) {
-      if (isLocal == null) {
-        isLocal = false
-      }
-      ide.$scope.reviewPanel.trackChangesOnForGuests = newValue
-      if (
-        currentUserType() === UserTypes.GUEST ||
-        currentUserType() === UserTypes.ANONYMOUS
-      ) {
-        return ($scope.editor.wantTrackChanges = newValue)
-      }
-    }
-
-    const applyClientTrackChangesStateToServer = function () {
-      ide.$scope.reviewPanel.trackChangesState = {
-        ...ide.$scope.reviewPanel.trackChangesState,
-      }
-      const data = {}
-      if (ide.$scope.reviewPanel.trackChangesOnForEveryone) {
-        data.on = true
-      } else {
-        data.on_for = {}
-        for (const userId in ide.$scope.reviewPanel.trackChangesState) {
-          const userState = ide.$scope.reviewPanel.trackChangesState[userId]
-          data.on_for[userId] = userState.value
-        }
-        if (ide.$scope.reviewPanel.trackChangesOnForGuests) {
-          data.on_for_guests = true
-        }
-      }
-      data._csrf = window.csrfToken
-      return $http.post(`/project/${$scope.project_id}/track_changes`, data)
-    }
-
-    const applyTrackChangesStateToClient = function (state) {
-      if (typeof state === 'boolean') {
-        _setEveryoneTCState(state)
-        return _setGuestsTCState(state)
-      } else {
-        const { project } = $scope
-        ide.$scope.reviewPanel.trackChangesOnForEveryone = false
-        _setGuestsTCState(state.__guests__ === true)
-        for (const member of Array.from(project.members)) {
-          _setUserTCState(
-            member._id,
-            state[member._id] != null ? state[member._id] : false
-          )
-        }
-        return _setUserTCState(
-          $scope.project.owner._id,
-          state[$scope.project.owner._id] != null
-            ? state[$scope.project.owner._id]
-            : false
-        )
-      }
-    }
-
-    ide.$scope.toggleTrackChangesForEveryone = function (onForEveryone) {
-      _setEveryoneTCState(onForEveryone, true)
-      _setGuestsTCState(onForEveryone, true)
-      return applyClientTrackChangesStateToServer()
-    }
-
-    ide.$scope.toggleTrackChangesForGuests = function (onForGuests) {
-      _setGuestsTCState(onForGuests, true)
-      return applyClientTrackChangesStateToServer()
-    }
-
-    ide.$scope.toggleTrackChangesForUser = function (onForUser, userId) {
-      _setUserTCState(userId, onForUser, true)
-      return applyClientTrackChangesStateToServer()
-    }
-
-    ide.socket.on('toggle-track-changes', state =>
-      $scope.$apply(() => applyTrackChangesStateToClient(state))
-    )
-
-    $scope.toggleTrackChangesFromKbdShortcut = function () {
-      if (
-        !(
-          $scope.project.features.trackChangesVisible &&
-          $scope.project.features.trackChanges
-        )
-      ) {
-        return
-      }
-      return $scope.toggleTrackChangesForUser(
-        !ide.$scope.reviewPanel.trackChangesState[ide.$scope.user.id].value,
-        ide.$scope.user.id
-      )
-    }
-
-    const setGuestFeatureBasedOnProjectAccessLevel = projectPublicAccessLevel =>
-      (ide.$scope.reviewPanel.trackChangesForGuestsAvailable =
-        projectPublicAccessLevel === 'tokenBased')
-
-    const onToggleTrackChangesForGuestsAvailability = function (available) {
-      // If the feature is no longer available we need to turn off the guest flag
-      if (available) {
-        return
-      }
-      if (!ide.$scope.reviewPanel.trackChangesOnForGuests) {
-        return
-      } // Already turned off
-      if (ide.$scope.reviewPanel.trackChangesOnForEveryone) {
-        return
-      } // Overrides guest setting
-      return $scope.toggleTrackChangesForGuests(false)
-    }
-
-    $scope.$watch(
-      'project.publicAccesLevel',
-      setGuestFeatureBasedOnProjectAccessLevel
-    )
-
-    $scope.$watch(
-      'reviewPanel.trackChangesForGuestsAvailable',
-      function (available) {
-        if (available != null) {
-          return onToggleTrackChangesForGuestsAvailability(available)
-        }
-      }
-    )
-
-    let _inited = false
-    ide.$scope.$on('project:joined', function () {
-      if (_inited) {
-        return
-      }
-      const { project } = ide.$scope
-      if (project.features.trackChanges) {
-        applyTrackChangesStateToClient(project.trackChangesState)
-      } else {
-        applyTrackChangesStateToClient(false)
-      }
-      setGuestFeatureBasedOnProjectAccessLevel(project.publicAccesLevel)
-      return (_inited = true)
-    })
-
-    let _refreshingRangeUsers = false
-    const _refreshedForUserIds = {}
-    function refreshChangeUsers(refresh_for_user_id) {
-      if (refresh_for_user_id != null) {
-        if (_refreshedForUserIds[refresh_for_user_id] != null) {
-          // We've already tried to refresh to get this user id, so stop it looping
-          return
-        }
-        _refreshedForUserIds[refresh_for_user_id] = true
-      }
-
-      // Only do one refresh at once
-      if (_refreshingRangeUsers) {
-        return
-      }
-      _refreshingRangeUsers = true
-
-      return $http
-        .get(`/project/${$scope.project_id}/changes/users`)
-        .then(function (response) {
-          const users = response.data
-          _refreshingRangeUsers = false
-          ide.$scope.users = $scope.users = {}
-          // Always include ourself, since if we submit an op, we might need to display info
-          // about it locally before it has been flushed through the server
-          if (
-            (ide.$scope.user != null ? ide.$scope.user.id : undefined) != null
-          ) {
-            $scope.users[ide.$scope.user.id] = formatUser(ide.$scope.user)
-          }
-          return (() => {
-            const result = []
-            for (const user of Array.from(users)) {
-              if (user.id != null) {
-                result.push(($scope.users[user.id] = formatUser(user)))
-              } else {
-                result.push(undefined)
-              }
-            }
-            return result
-          })()
-        })
-        .catch(() => (_refreshingRangeUsers = false))
-    }
-
-    let _threadsLoadedOnce = false
-    let _loadingThreadsInProgress = false
-    async function ensureThreadsAreLoaded() {
-      if (_threadsLoadedOnce) {
-        // We get any updates in real time so only need to load them once.
-        return
-      }
-      _threadsLoadedOnce = true
-      _loadingThreadsInProgress = true
-      ide.$scope.loadingThreads = true
-      return $http
-        .get(`/project/${$scope.project_id}/threads`)
-        .then(async function (response) {
-          const threads = response.data
-          ide.$scope.loadingThreads = false
-          _loadingThreadsInProgress = false
-          for (const thread_id in ide.$scope.reviewPanel.resolvedThreadIds) {
-            delete ide.$scope.reviewPanel.resolvedThreadIds[thread_id]
-          }
-          for (const thread_id in threads) {
-            const thread = threads[thread_id]
-            for (const comment of Array.from(thread.messages)) {
-              formatComment(comment)
-            }
-            if (thread.resolved_by_user != null) {
-              thread.resolved_by_user = formatUser(thread.resolved_by_user)
-              ide.$scope.reviewPanel.resolvedThreadIds[thread_id] = true
-              $scope.$broadcast('comment:resolve_threads', [thread_id])
-            }
-          }
-          ide.$scope.reviewPanel.commentThreads = threads
-          // Update entries so that the view has up-to-date information about
-          // the entries when handling the loaded_threads events, which avoids
-          // thrashing
-          await updateEntries($scope.editor.open_doc_id)
-
-          dispatchReviewPanelEvent('loaded_threads')
-          return $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
-        })
-    }
-
-    function formatComment(comment) {
-      comment.user = formatUser(comment.user)
-      comment.timestamp = new Date(comment.timestamp)
-      return comment
-    }
-
-    function formatUser(user) {
-      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: ColorManager.ANONYMOUS_HUE,
-          avatar_text: 'A',
-        }
-      }
-      if (id === window.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: ColorManager.getHueForUserId(id),
-        avatar_text: [user.first_name, user.last_name]
-          .filter(n => n != null)
-          .map(n => n[0])
-          .join(''),
-      }
-    }
-
-    // listen for events from the CodeMirror 6 track changes extension
-    window.addEventListener('editor:event', event => {
-      const { type, payload } = event.detail
-
-      switch (type) {
-        case 'line-height': {
-          ide.$scope.reviewPanel.rendererData.lineHeight = payload
-          ide.$scope.$broadcast('review-panel:layout')
-          break
-        }
-
-        case 'track-changes:changed': {
-          $scope.$broadcast('editor:track-changes:changed')
-          break
-        }
-
-        case 'track-changes:visibility_changed': {
-          $scope.$broadcast('editor:track-changes:visibility_changed')
-          break
-        }
-
-        case 'focus:changed': {
-          const { from, to, empty, updateType } = payload
-          $scope.$broadcast(
-            'editor:focus:changed',
-            from,
-            to,
-            !empty,
-            updateType
-          )
-          break
-        }
-
-        case 'add-new-comment': {
-          $scope.addNewCommentFromKbdShortcut()
-          break
-        }
-
-        case 'toggle-track-changes': {
-          $scope.toggleTrackChangesFromKbdShortcut()
-          break
-        }
-
-        case 'toggle-review-panel': {
-          $scope.toggleReviewPanel()
-          break
-        }
-      }
-    })
-
-    // Add methods somewhere that React can see them
-    $scope.reviewPanel.saveEdit = $scope.saveEdit
-  },
-])
-
-// send events to the CodeMirror 6 track changes extension
-const dispatchReviewPanelEvent = (type, payload) => {
-  window.dispatchEvent(
-    new CustomEvent('review-panel:event', {
-      detail: { type, payload },
-    })
-  )
-}

+ 0 - 181
services/web/frontend/js/ide/services/ide.js

@@ -1,181 +0,0 @@
-/* eslint-disable
-    max-len,
-    no-return-assign,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * DS103: Rewrite code to no longer use __guard__
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-import App from '../../base'
-import EditorWatchdogManager from '../connection/EditorWatchdogManager'
-import { debugConsole } from '@/utils/debugging'
-// We create and provide this as service so that we can access the global ide
-// from within other parts of the angular app.
-App.factory('ide', [
-  '$http',
-  'queuedHttp',
-  '$modal',
-  '$q',
-  '$filter',
-  '$timeout',
-  'eventTracking',
-  function ($http, queuedHttp, $modal, $q, $filter, $timeout, eventTracking) {
-    const ide = {}
-    ide.$http = $http
-    ide.queuedHttp = queuedHttp
-    ide.$q = $q
-    ide.$filter = $filter
-    ide.$timeout = $timeout
-    ide.globalEditorWatchdogManager = new EditorWatchdogManager({
-      onTimeoutHandler: meta => {
-        eventTracking.sendMB('losing-edits', meta)
-        // clone the meta object, reportError adds additional fields into it
-        ide.reportError('losing-edits', Object.assign({}, meta))
-      },
-    })
-
-    this.recentEvents = []
-    ide.pushEvent = (type, meta) => {
-      if (meta == null) {
-        meta = {}
-      }
-      debugConsole.log('event', type, meta)
-      this.recentEvents.push({ type, meta, date: new Date() })
-      if (this.recentEvents.length > 100) {
-        return this.recentEvents.shift()
-      }
-    }
-
-    ide.reportError = (error, meta) => {
-      if (meta == null) {
-        meta = {}
-      }
-      meta.user_id = window.user_id
-      meta.project_id = window.project_id
-      meta.client_id = __guard__(
-        ide.socket != null ? ide.socket.socket : undefined,
-        x => x.sessionid
-      )
-      meta.transport = __guard__(
-        __guard__(
-          ide.socket != null ? ide.socket.socket : undefined,
-          x2 => x2.transport
-        ),
-        x1 => x1.name
-      )
-      meta.client_now = new Date()
-      const errorObj = {}
-      if (typeof error === 'object') {
-        for (const key of Array.from(Object.getOwnPropertyNames(error))) {
-          errorObj[key] = error[key]
-        }
-      } else if (typeof error === 'string') {
-        errorObj.message = error
-      }
-      return $http.post('/error/client', {
-        error: errorObj,
-        meta,
-        _csrf: window.csrfToken,
-      })
-    }
-
-    ide.showGenericMessageModal = (title, message) =>
-      $modal.open({
-        templateUrl: 'genericMessageModalTemplate',
-        controller: 'GenericMessageModalController',
-        resolve: {
-          title() {
-            return title
-          },
-          message() {
-            return message
-          },
-        },
-      })
-
-    ide.showOutOfSyncModal = (title, message, editorContent) =>
-      $modal.open({
-        templateUrl: 'outOfSyncModalTemplate',
-        controller: 'OutOfSyncModalController',
-        backdrop: false, // not dismissable by clicking background
-        keyboard: false, // prevent dismiss via keyboard
-        resolve: {
-          title() {
-            return title
-          },
-          message() {
-            return message
-          },
-          editorContent() {
-            return editorContent
-          },
-        },
-        windowClass: 'out-of-sync-modal',
-      })
-
-    ide.showLockEditorMessageModal = (title, message) =>
-      // modal to block the editor when connection is down
-      $modal.open({
-        templateUrl: 'lockEditorModalTemplate',
-        controller: 'GenericMessageModalController',
-        backdrop: false, // not dismissable by clicking background
-        keyboard: false, // prevent dismiss via keyboard
-        resolve: {
-          title() {
-            return title
-          },
-          message() {
-            return message
-          },
-        },
-        windowClass: 'lock-editor-modal',
-      })
-
-    return ide
-  },
-])
-
-App.controller('GenericMessageModalController', [
-  '$scope',
-  '$modalInstance',
-  'title',
-  'message',
-  function ($scope, $modalInstance, title, message) {
-    $scope.title = title
-    $scope.message = message
-
-    return ($scope.done = () => $modalInstance.close())
-  },
-])
-
-App.controller('OutOfSyncModalController', [
-  '$scope',
-  '$window',
-  'title',
-  'message',
-  'editorContent',
-  function ($scope, $window, title, message, editorContent) {
-    $scope.title = title
-    $scope.message = message
-    $scope.editorContent = editorContent
-    $scope.editorContentRows = editorContent.split('\n').length
-
-    $scope.done = () => {
-      // Reload the page to avoid staying in an inconsistent state.
-      // https://github.com/overleaf/issues/issues/3694
-      $window.location.reload()
-    }
-  },
-])
-
-function __guard__(value, transform) {
-  return typeof value !== 'undefined' && value !== null
-    ? transform(value)
-    : undefined
-}

+ 0 - 11
services/web/frontend/js/ide/toolbar/EditorLoaderController.js

@@ -1,11 +0,0 @@
-import App from '../../base'
-
-App.controller('EditorLoaderController', [
-  '$scope',
-  'localStorage',
-  function ($scope, localStorage) {
-    $scope.$watch('editor.showVisual', function (val) {
-      localStorage(`editor.lastUsedMode`, val === true ? 'visual' : 'code')
-    })
-  },
-])

+ 0 - 1
services/web/frontend/js/ide/toolbar/index.js

@@ -1 +0,0 @@
-import './EditorLoaderController'

+ 0 - 13
services/web/frontend/js/libraries.js

@@ -1,13 +0,0 @@
-import 'jquery'
-import 'angular'
-import 'angular-sanitize'
-import 'lodash'
-import './vendor/libs/ui-bootstrap'
-import './vendor/libs/jquery.storage'
-import './vendor/libs/select/select'
-
-// CSS
-import 'angular/angular-csp.css'
-
-// Rewrite meta elements
-import './utils/meta'

+ 0 - 32
services/web/frontend/js/main.js

@@ -1,32 +0,0 @@
-/* eslint-disable
-    max-len,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-import './main/token-access' // used in project/token/access
-import './main/event' // used in various controllers
-import './main/system-messages' // used in project/editor
-import './directives/eventTracking' // used in lots of places
-import './features/cookie-banner'
-import '../../modules/modules-main'
-import { debugConsole } from '@/utils/debugging'
-angular.module('OverleafApp').config([
-  '$locationProvider',
-  function ($locationProvider) {
-    try {
-      return $locationProvider.html5Mode({
-        enabled: true,
-        requireBase: false,
-        rewriteLinks: false,
-      })
-    } catch (e) {
-      debugConsole.error("Error while trying to fix '#' links: ", e)
-    }
-  },
-])
-export default angular.bootstrap(document.body, ['OverleafApp'])

+ 0 - 129
services/web/frontend/js/main/event.js

@@ -1,129 +0,0 @@
-/* eslint-disable
-    camelcase,
-    max-len,
-    no-return-assign,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-import moment from 'moment'
-import App from '../base'
-import '../modules/localStorage'
-import { sendMB } from '../infrastructure/event-tracking'
-import { debugConsole } from '@/utils/debugging'
-const CACHE_KEY = 'mbEvents'
-
-// keep track of how many heartbeats we've sent so we can calculate how
-// long wait until the next one
-let heartbeatsSent = 0
-let nextHeartbeat = new Date()
-
-App.factory('eventTracking', [
-  '$http',
-  'localStorage',
-  function ($http, localStorage) {
-    const _getEventCache = function () {
-      let eventCache = localStorage(CACHE_KEY)
-
-      // Initialize as an empy object if the event cache is still empty.
-      if (eventCache == null) {
-        eventCache = {}
-        localStorage(CACHE_KEY, eventCache)
-      }
-
-      return eventCache
-    }
-
-    const _eventInCache = function (key) {
-      const curCache = _getEventCache()
-      return curCache[key] || false
-    }
-
-    const _addEventToCache = function (key) {
-      const curCache = _getEventCache()
-      curCache[key] = true
-
-      return localStorage(CACHE_KEY, curCache)
-    }
-
-    const _sendEditingSessionHeartbeat = segmentation =>
-      $http({
-        url: `/editingSession/${window.project_id}`,
-        method: 'PUT',
-        data: { segmentation },
-        headers: {
-          'X-CSRF-Token': window.csrfToken,
-        },
-      })
-
-    return {
-      send(category, action, label, value) {
-        return ga('send', 'event', category, action, label, value)
-      },
-
-      sendGAOnce(category, action, label, value) {
-        if (!_eventInCache(action)) {
-          _addEventToCache(action)
-          return this.send(category, action, label, value)
-        }
-      },
-
-      editingSessionHeartbeat(segmentationCb = () => {}) {
-        debugConsole.log('[Event] heartbeat trigger')
-
-        // If the next heartbeat is in the future, stop
-        if (nextHeartbeat > new Date()) return
-
-        const segmentation = segmentationCb()
-
-        debugConsole.log('[Event] send heartbeat request', segmentation)
-        _sendEditingSessionHeartbeat(segmentation)
-
-        heartbeatsSent++
-
-        // send two first heartbeats at 0 and 30s then increase the backoff time
-        // 1min per call until we reach 5 min
-        const backoffSecs =
-          heartbeatsSent <= 2
-            ? 30
-            : heartbeatsSent <= 6
-              ? (heartbeatsSent - 2) * 60
-              : 300
-
-        nextHeartbeat = moment().add(backoffSecs, 'seconds').toDate()
-      },
-
-      sendMB,
-
-      sendMBSampled(key, segmentation, rate = 0.01) {
-        if (Math.random() < rate) {
-          this.sendMB(key, segmentation)
-        }
-      },
-
-      sendMBOnce(key, segmentation) {
-        if (!_eventInCache(key)) {
-          _addEventToCache(key)
-          this.sendMB(key, segmentation)
-        }
-      },
-
-      eventInCache(key) {
-        return _eventInCache(key)
-      },
-    }
-  },
-])
-
-export default $('.navbar a').on('click', function (e) {
-  const href = $(e.target).attr('href')
-  if (href != null) {
-    return ga('send', 'event', 'navigation', 'top menu bar', href)
-  }
-})

+ 0 - 21
services/web/frontend/js/main/importing.js

@@ -1,21 +0,0 @@
-import App from '../base'
-App.controller('ImportingController', [
-  '$interval',
-  '$scope',
-  '$timeout',
-  '$window',
-  function ($interval, $scope, $timeout, $window) {
-    $interval(function () {
-      $scope.state.load_progress += 5
-      if ($scope.state.load_progress > 100) {
-        $scope.state.load_progress = 20
-      }
-    }, 500)
-    $timeout(function () {
-      $window.location.reload()
-    }, 5000)
-    $scope.state = {
-      load_progress: 20,
-    }
-  },
-])

+ 0 - 63
services/web/frontend/js/main/system-messages.js

@@ -1,63 +0,0 @@
-/* eslint-disable
-    max-len,
-    no-return-assign,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-import App from '../base'
-const MESSAGE_POLL_INTERVAL = 15 * 60 * 1000
-// Controller for messages (array)
-App.controller('SystemMessagesController', [
-  '$http',
-  '$scope',
-  function ($http, $scope) {
-    $scope.messages = []
-    function pollSystemMessages() {
-      // Ignore polling if tab is hidden or browser is offline
-      if (document.hidden || !navigator.onLine) {
-        return
-      }
-
-      $http
-        .get('/system/messages')
-        .then(response => {
-          // Ignore if content-type is anything but JSON, prevents a bug where
-          // the user logs out in another tab, then a 302 redirect was returned,
-          // which is transparently resolved by the browser to the login (HTML)
-          // page.
-          // This then caused an Angular error where it was attempting to loop
-          // through the HTML as a string
-          if (response.headers('content-type').includes('json')) {
-            $scope.messages = response.data
-          }
-        })
-        .catch(() => {
-          // ignore errors
-        })
-    }
-    pollSystemMessages()
-    setInterval(pollSystemMessages, MESSAGE_POLL_INTERVAL)
-  },
-])
-
-export default App.controller('SystemMessageController', [
-  '$scope',
-  function ($scope) {
-    $scope.hidden = $.localStorage(`systemMessage.hide.${$scope.message._id}`)
-    $scope.protected = $scope.message._id === 'protected'
-    $scope.htmlContent = $scope.message.content
-
-    return ($scope.hide = function () {
-      if (!$scope.protected) {
-        // do not allow protected messages to be hidden
-        $scope.hidden = true
-        return $.localStorage(`systemMessage.hide.${$scope.message._id}`, true)
-      }
-    })
-  },
-])

+ 0 - 92
services/web/frontend/js/main/token-access.js

@@ -1,92 +0,0 @@
-import App from '../base'
-import { debugConsole } from '@/utils/debugging'
-App.controller('TokenAccessPageController', [
-  '$scope',
-  '$http',
-  '$location',
-  function ($scope, $http, $location) {
-    window.S = $scope
-    $scope.mode = 'accessAttempt' // 'accessAttempt' | 'v1Import' | 'requireAccept'
-
-    $scope.v1ImportData = null
-    $scope.requireAccept = null
-
-    $scope.accessInFlight = false
-    $scope.accessSuccess = false
-    $scope.accessError = false
-
-    $scope.currentPath = () => {
-      return $location.path()
-    }
-
-    $scope.buildZipDownloadPath = projectId => {
-      return `/overleaf/project/${projectId}/download/zip`
-    }
-
-    $scope.getProjectName = () => {
-      if ($scope.v1ImportData?.name) {
-        return $scope.v1ImportData.name
-      } else if ($scope.requireAccept?.projectName) {
-        return $scope.requireAccept.projectName
-      } else {
-        return 'This project'
-      }
-    }
-
-    $scope.postConfirmedByUser = () => {
-      $scope.post(true)
-    }
-
-    $scope.post = (confirmedByUser = false) => {
-      $scope.mode = 'accessAttempt'
-      const textData = $('#overleaf-token-access-data').text()
-      const parsedData = JSON.parse(textData)
-      const { postUrl, csrfToken } = parsedData
-      $scope.accessInFlight = true
-      $http({
-        method: 'POST',
-        url: postUrl,
-        data: {
-          _csrf: csrfToken,
-          confirmedByUser,
-          tokenHashPrefix: window.location.hash,
-        },
-      }).then(
-        function successCallback(response) {
-          $scope.accessInFlight = false
-          $scope.accessError = false
-          const { data } = response
-          if (data.redirect) {
-            const redirect = response.data.redirect
-            if (!redirect) {
-              debugConsole.warn(
-                'no redirect supplied in success response data',
-                response
-              )
-              $scope.accessError = true
-              return
-            }
-            window.location.replace(redirect)
-          } else if (data.v1Import) {
-            $scope.mode = 'v1Import'
-            $scope.v1ImportData = data.v1Import
-          } else if (data.requireAccept) {
-            $scope.mode = 'requireAccept'
-            $scope.requireAccept = data.requireAccept
-          } else {
-            debugConsole.warn(
-              'invalid data from server in success response',
-              response
-            )
-            $scope.accessError = true
-          }
-        },
-        function errorCallback(response) {
-          debugConsole.warn('error response from server', response)
-          $scope.accessInFlight = false
-          $scope.accessError = response.status === 404 ? 'not_found' : 'error'
-        }
-      )
-    }
-  },
-])

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff