ReviewPanelController.js 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346
  1. /* eslint-disable
  2. camelcase,
  3. max-len,
  4. no-return-assign,
  5. no-unused-vars,
  6. */
  7. // TODO: This file was created by bulk-decaffeinate.
  8. // Fix any style issues and re-enable lint.
  9. /*
  10. * decaffeinate suggestions:
  11. * DS101: Remove unnecessary use of Array.from
  12. * DS102: Remove unnecessary code created because of implicit returns
  13. * DS205: Consider reworking code to avoid use of IIFEs
  14. * DS207: Consider shorter variations of null checks
  15. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  16. */
  17. import RangesTracker from '@overleaf/ranges-tracker'
  18. import App from '../../../base'
  19. import EventEmitter from '../../../utils/EventEmitter'
  20. import ColorManager from '../../colors/ColorManager'
  21. import getMeta from '../../../utils/meta'
  22. export default App.controller('ReviewPanelController', [
  23. '$scope',
  24. '$element',
  25. 'ide',
  26. '$timeout',
  27. '$http',
  28. '$modal',
  29. 'eventTracking',
  30. 'localStorage',
  31. function (
  32. $scope,
  33. $element,
  34. ide,
  35. $timeout,
  36. $http,
  37. $modal,
  38. eventTracking,
  39. localStorage
  40. ) {
  41. let UserTCSyncState
  42. const $reviewPanelEl = $element.find('#review-panel')
  43. const UserTypes = {
  44. MEMBER: 'member', // Invited, listed in project.members
  45. GUEST: 'guest', // Not invited, but logged in so has a user_id
  46. ANONYMOUS: 'anonymous', // No user_id
  47. }
  48. const currentUserType = function () {
  49. if ((ide.$scope.user != null ? ide.$scope.user.id : undefined) == null) {
  50. return UserTypes.ANONYMOUS
  51. } else {
  52. const user_id = ide.$scope.user.id
  53. const { project } = ide.$scope
  54. if (
  55. (project.owner != null ? project.owner.id : undefined) === user_id
  56. ) {
  57. return UserTypes.MEMBER
  58. }
  59. for (const member of Array.from(project.members)) {
  60. if (member._id === user_id) {
  61. return UserTypes.MEMBER
  62. }
  63. }
  64. return UserTypes.GUEST
  65. }
  66. }
  67. $scope.SubViews = {
  68. CUR_FILE: 'cur_file',
  69. OVERVIEW: 'overview',
  70. }
  71. $scope.UserTCSyncState = UserTCSyncState = {
  72. SYNCED: 'synced',
  73. PENDING: 'pending',
  74. }
  75. ide.$scope.reviewPanel = {
  76. trackChangesState: {},
  77. trackChangesOnForEveryone: false,
  78. trackChangesOnForGuests: false,
  79. trackChangesForGuestsAvailable: false,
  80. entries: {},
  81. resolvedComments: {},
  82. hasEntries: false,
  83. subView: $scope.SubViews.CUR_FILE,
  84. openSubView: $scope.SubViews.CUR_FILE,
  85. overview: {
  86. loading: false,
  87. docsCollapsedState:
  88. JSON.parse(
  89. localStorage(`docs_collapsed_state:${$scope.project_id}`)
  90. ) || {},
  91. },
  92. dropdown: {
  93. loading: false,
  94. },
  95. commentThreads: {},
  96. resolvedThreadIds: {},
  97. layoutToLeft: false,
  98. rendererData: {},
  99. formattedProjectMembers: {},
  100. fullTCStateCollapsed: true,
  101. // All selected changes. If a aggregated change (insertion + deletion) is selection, the two ids
  102. // will be present. The length of this array will differ from the count below (see explanation).
  103. selectedEntryIds: [],
  104. // A count of user-facing selected changes. An aggregated change (insertion + deletion) will count
  105. // as only one.
  106. nVisibleSelectedChanges: 0,
  107. entryHover: false,
  108. }
  109. ide.$scope.loadingThreads = true
  110. window.addEventListener('beforeunload', function () {
  111. const collapsedStates = {}
  112. for (const doc in ide.$scope.reviewPanel.overview.docsCollapsedState) {
  113. const state = ide.$scope.reviewPanel.overview.docsCollapsedState[doc]
  114. if (state) {
  115. collapsedStates[doc] = state
  116. }
  117. }
  118. const valToStore =
  119. Object.keys(collapsedStates).length > 0
  120. ? JSON.stringify(collapsedStates)
  121. : null
  122. return localStorage(
  123. `docs_collapsed_state:${$scope.project_id}`,
  124. valToStore
  125. )
  126. })
  127. $scope.$on('layout:pdf:linked', (event, state) =>
  128. ide.$scope.$broadcast('review-panel:layout')
  129. )
  130. $scope.$on('layout:pdf:resize', (event, state) => {
  131. ide.$scope.reviewPanel.layoutToLeft =
  132. state.east?.size < 220 || state.east?.initClosed
  133. ide.$scope.$broadcast('review-panel:layout', false)
  134. })
  135. $scope.$on('review-panel:sizes', (e, sizes) => {
  136. $scope.$broadcast('editor:set-scroll-size', sizes)
  137. dispatchReviewPanelEvent('sizes', sizes)
  138. })
  139. $scope.$watch('project.features.trackChangesVisible', function (visible) {
  140. if (visible == null) {
  141. return
  142. }
  143. if (!visible) {
  144. return ($scope.ui.reviewPanelOpen = false)
  145. }
  146. })
  147. $scope.$watch('project.members', function (members) {
  148. ide.$scope.reviewPanel.formattedProjectMembers = {}
  149. if (($scope.project != null ? $scope.project.owner : undefined) != null) {
  150. ide.$scope.reviewPanel.formattedProjectMembers[
  151. $scope.project.owner._id
  152. ] = formatUser($scope.project.owner)
  153. }
  154. if (
  155. ($scope.project != null ? $scope.project.members : undefined) != null
  156. ) {
  157. return (() => {
  158. const result = []
  159. for (const member of Array.from(members)) {
  160. if (member.privileges === 'readAndWrite') {
  161. if (
  162. ide.$scope.reviewPanel.trackChangesState[member._id] == null
  163. ) {
  164. // An added member will have track changes enabled if track changes is on for everyone
  165. _setUserTCState(
  166. member._id,
  167. ide.$scope.reviewPanel.trackChangesOnForEveryone,
  168. true
  169. )
  170. }
  171. result.push(
  172. (ide.$scope.reviewPanel.formattedProjectMembers[member._id] =
  173. formatUser(member))
  174. )
  175. } else {
  176. result.push(undefined)
  177. }
  178. }
  179. return result
  180. })()
  181. }
  182. })
  183. $scope.commentState = {
  184. adding: false,
  185. content: '',
  186. }
  187. ide.$scope.users = $scope.users = {}
  188. ide.$scope.reviewPanelEventsBridge = new EventEmitter()
  189. ide.socket.on('new-comment', function (thread_id, comment) {
  190. const thread = getThread(thread_id)
  191. delete thread.submitting
  192. thread.messages.push(formatComment(comment))
  193. $scope.$apply()
  194. return $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
  195. })
  196. ide.socket.on('accept-changes', function (doc_id, change_ids) {
  197. if (doc_id !== $scope.editor.open_doc_id) {
  198. getChangeTracker(doc_id).removeChangeIds(change_ids)
  199. } else {
  200. $scope.$broadcast('changes:accept', change_ids)
  201. dispatchReviewPanelEvent('changes:accept', change_ids)
  202. }
  203. updateEntries(doc_id)
  204. return $scope.$apply(function () {})
  205. })
  206. ide.socket.on('resolve-thread', (thread_id, user) =>
  207. _onCommentResolved(thread_id, user)
  208. )
  209. ide.socket.on('reopen-thread', thread_id => _onCommentReopened(thread_id))
  210. ide.socket.on('delete-thread', function (thread_id) {
  211. _onThreadDeleted(thread_id)
  212. return $scope.$apply(function () {})
  213. })
  214. ide.socket.on('edit-message', function (thread_id, message_id, content) {
  215. _onCommentEdited(thread_id, message_id, content)
  216. return $scope.$apply(function () {})
  217. })
  218. ide.socket.on('delete-message', function (thread_id, message_id) {
  219. _onCommentDeleted(thread_id, message_id)
  220. return $scope.$apply(function () {})
  221. })
  222. const rangesTrackers = {}
  223. const getDocEntries = function (doc_id) {
  224. if (ide.$scope.reviewPanel.entries[doc_id] == null) {
  225. ide.$scope.reviewPanel.entries[doc_id] = {}
  226. }
  227. return ide.$scope.reviewPanel.entries[doc_id]
  228. }
  229. const getDocResolvedComments = function (doc_id) {
  230. if (ide.$scope.reviewPanel.resolvedComments[doc_id] == null) {
  231. ide.$scope.reviewPanel.resolvedComments[doc_id] = {}
  232. }
  233. return ide.$scope.reviewPanel.resolvedComments[doc_id]
  234. }
  235. function getThread(thread_id) {
  236. if (ide.$scope.reviewPanel.commentThreads[thread_id] == null) {
  237. ide.$scope.reviewPanel.commentThreads[thread_id] = { messages: [] }
  238. }
  239. return ide.$scope.reviewPanel.commentThreads[thread_id]
  240. }
  241. function getChangeTracker(doc_id) {
  242. if (rangesTrackers[doc_id] == null) {
  243. rangesTrackers[doc_id] = new RangesTracker()
  244. rangesTrackers[doc_id].resolvedThreadIds =
  245. ide.$scope.reviewPanel.resolvedThreadIds
  246. }
  247. return rangesTrackers[doc_id]
  248. }
  249. $scope.$watch(
  250. '!ui.reviewPanelOpen && reviewPanel.hasEntries',
  251. function (open, prevVal) {
  252. if (open == null) {
  253. return
  254. }
  255. $scope.ui.miniReviewPanelVisible = open
  256. if (open !== prevVal) {
  257. return $timeout(() => $scope.$broadcast('review-panel:toggle'))
  258. }
  259. }
  260. )
  261. $scope.$watch('ui.reviewPanelOpen', function (open) {
  262. if (open == null) {
  263. return
  264. }
  265. if (!open) {
  266. // Always show current file when not open, but save current state
  267. ide.$scope.reviewPanel.openSubView = ide.$scope.reviewPanel.subView
  268. ide.$scope.reviewPanel.subView = $scope.SubViews.CUR_FILE
  269. } else {
  270. // Reset back to what we had when previously open
  271. ide.$scope.reviewPanel.subView = ide.$scope.reviewPanel.openSubView
  272. }
  273. return $timeout(function () {
  274. $scope.$broadcast('review-panel:toggle')
  275. return ide.$scope.$broadcast('review-panel:layout', false)
  276. })
  277. })
  278. $scope.$watch('reviewPanel.subView', function (view, oldView) {
  279. if (view == null) {
  280. return
  281. }
  282. if (view === $scope.SubViews.OVERVIEW) {
  283. return refreshOverviewPanel()
  284. } else if (oldView === $scope.SubViews.OVERVIEW) {
  285. dispatchReviewPanelEvent('overview-closed', view)
  286. }
  287. })
  288. $scope.$watch('editor.sharejs_doc', function (doc, old_doc) {
  289. if (doc == null) {
  290. return
  291. }
  292. // The open doc range tracker is kept up to date in real-time so
  293. // replace any outdated info with this
  294. rangesTrackers[doc.doc_id] = doc.ranges
  295. rangesTrackers[doc.doc_id].resolvedThreadIds =
  296. ide.$scope.reviewPanel.resolvedThreadIds
  297. ide.$scope.reviewPanel.rangesTracker = rangesTrackers[doc.doc_id]
  298. if (old_doc != null) {
  299. old_doc.off('flipped_pending_to_inflight')
  300. }
  301. doc.on('flipped_pending_to_inflight', () => regenerateTrackChangesId(doc))
  302. return regenerateTrackChangesId(doc)
  303. })
  304. $scope.$watch(
  305. function () {
  306. const entries =
  307. ide.$scope.reviewPanel.entries[$scope.editor.open_doc_id] || {}
  308. const permEntries = {}
  309. for (const entry in entries) {
  310. const entryData = entries[entry]
  311. if (!['add-comment', 'bulk-actions'].includes(entry)) {
  312. permEntries[entry] = entryData
  313. }
  314. }
  315. return Object.keys(permEntries).length
  316. },
  317. nEntries =>
  318. (ide.$scope.reviewPanel.hasEntries =
  319. nEntries > 0 && $scope.project.features.trackChangesVisible)
  320. )
  321. function regenerateTrackChangesId(doc) {
  322. const old_id = getChangeTracker(doc.doc_id).getIdSeed()
  323. const new_id = RangesTracker.generateIdSeed()
  324. getChangeTracker(doc.doc_id).setIdSeed(new_id)
  325. return doc.setTrackChangesIdSeeds({ pending: new_id, inflight: old_id })
  326. }
  327. const refreshRanges = () =>
  328. $http
  329. .get(`/project/${$scope.project_id}/ranges`)
  330. .then(function (response) {
  331. const docs = response.data
  332. return (() => {
  333. const result = []
  334. for (const doc of Array.from(docs)) {
  335. if (
  336. ide.$scope.reviewPanel.overview.docsCollapsedState[doc.id] ==
  337. null
  338. ) {
  339. ide.$scope.reviewPanel.overview.docsCollapsedState[doc.id] =
  340. false
  341. }
  342. if (doc.id !== $scope.editor.open_doc_id) {
  343. // this is kept up to date in real-time, don't overwrite
  344. const rangesTracker = getChangeTracker(doc.id)
  345. rangesTracker.comments =
  346. (doc.ranges != null ? doc.ranges.comments : undefined) || []
  347. rangesTracker.changes =
  348. (doc.ranges != null ? doc.ranges.changes : undefined) || []
  349. }
  350. result.push(updateEntries(doc.id))
  351. }
  352. return result
  353. })()
  354. })
  355. function refreshOverviewPanel() {
  356. ide.$scope.reviewPanel.overview.loading = true
  357. return refreshRanges()
  358. .then(() => (ide.$scope.reviewPanel.overview.loading = false))
  359. .catch(() => (ide.$scope.reviewPanel.overview.loading = false))
  360. }
  361. ide.$scope.refreshResolvedCommentsDropdown = function () {
  362. ide.$scope.reviewPanel.dropdown.loading = true
  363. const q = refreshRanges()
  364. q.then(() => (ide.$scope.reviewPanel.dropdown.loading = false))
  365. q.catch(() => (ide.$scope.reviewPanel.dropdown.loading = false))
  366. return q
  367. }
  368. async function updateEntries(doc_id) {
  369. const rangesTracker = getChangeTracker(doc_id)
  370. const entries = getDocEntries(doc_id)
  371. const resolvedComments = getDocResolvedComments(doc_id)
  372. let changed = false
  373. // Assume we'll delete everything until we see it, then we'll remove it from this object
  374. const delete_changes = {}
  375. for (const id in entries) {
  376. const change = entries[id]
  377. if (!['add-comment', 'bulk-actions'].includes(id)) {
  378. for (const entry_id of Array.from(change.entry_ids)) {
  379. delete_changes[entry_id] = true
  380. }
  381. }
  382. }
  383. for (const id in resolvedComments) {
  384. const change = resolvedComments[id]
  385. for (const entry_id of Array.from(change.entry_ids)) {
  386. delete_changes[entry_id] = true
  387. }
  388. }
  389. let potential_aggregate = false
  390. let prev_insertion = null
  391. for (const change of Array.from(rangesTracker.changes)) {
  392. changed = true
  393. if (
  394. potential_aggregate &&
  395. change.op.d &&
  396. change.op.p === prev_insertion.op.p + prev_insertion.op.i.length &&
  397. change.metadata.user_id === prev_insertion.metadata.user_id
  398. ) {
  399. // An actual aggregate op.
  400. entries[prev_insertion.id].type = 'aggregate-change'
  401. entries[prev_insertion.id].metadata.replaced_content = change.op.d
  402. entries[prev_insertion.id].entry_ids.push(change.id)
  403. } else {
  404. if (entries[change.id] == null) {
  405. entries[change.id] = {}
  406. }
  407. delete delete_changes[change.id]
  408. const new_entry = {
  409. type: change.op.i ? 'insert' : 'delete',
  410. entry_ids: [change.id],
  411. content: change.op.i || change.op.d,
  412. offset: change.op.p,
  413. metadata: change.metadata,
  414. }
  415. for (const key in new_entry) {
  416. entries[change.id][key] = new_entry[key]
  417. }
  418. }
  419. if (change.op.i) {
  420. potential_aggregate = true
  421. prev_insertion = change
  422. } else {
  423. potential_aggregate = false
  424. prev_insertion = null
  425. }
  426. if ($scope.users[change.metadata.user_id] == null) {
  427. if (!window.isRestrictedTokenMember) {
  428. refreshChangeUsers(change.metadata.user_id)
  429. }
  430. }
  431. }
  432. if (!window.isRestrictedTokenMember) {
  433. if (rangesTracker.comments.length > 0) {
  434. await ensureThreadsAreLoaded()
  435. } else if (ide.$scope.loadingThreads === true) {
  436. // ensure that tracked changes are highlighted even if no comments are loaded
  437. ide.$scope.loadingThreads = false
  438. dispatchReviewPanelEvent('loaded_threads')
  439. }
  440. }
  441. if (!_loadingThreadsInProgress) {
  442. for (const comment of Array.from(rangesTracker.comments)) {
  443. let new_comment
  444. changed = true
  445. delete delete_changes[comment.id]
  446. if (ide.$scope.reviewPanel.resolvedThreadIds[comment.op.t]) {
  447. new_comment =
  448. resolvedComments[comment.id] != null
  449. ? resolvedComments[comment.id]
  450. : (resolvedComments[comment.id] = {})
  451. delete entries[comment.id]
  452. } else {
  453. new_comment =
  454. entries[comment.id] != null
  455. ? entries[comment.id]
  456. : (entries[comment.id] = {})
  457. delete resolvedComments[comment.id]
  458. }
  459. const new_entry = {
  460. type: 'comment',
  461. thread_id: comment.op.t,
  462. entry_ids: [comment.id],
  463. content: comment.op.c,
  464. offset: comment.op.p,
  465. }
  466. for (const key in new_entry) {
  467. new_comment[key] = new_entry[key]
  468. }
  469. }
  470. }
  471. for (const change_id in delete_changes) {
  472. const _ = delete_changes[change_id]
  473. changed = true
  474. delete entries[change_id]
  475. delete resolvedComments[change_id]
  476. }
  477. if (changed) {
  478. // TODO: unused?
  479. $scope.$broadcast('entries:changed')
  480. }
  481. return entries
  482. }
  483. $scope.$on('editor:track-changes:changed', async function () {
  484. const doc_id = $scope.editor.open_doc_id
  485. const entries = await updateEntries(doc_id)
  486. $scope.$broadcast('review-panel:recalculate-screen-positions')
  487. dispatchReviewPanelEvent('recalculate-screen-positions', {
  488. entries,
  489. updateType: 'trackedChangesChange',
  490. })
  491. // Ensure that watchers, such as the React-based review panel component,
  492. // are informed of the changes to entries
  493. ide.$scope.$apply()
  494. return ide.$scope.$broadcast('review-panel:layout')
  495. })
  496. $scope.$on('editor:track-changes:visibility_changed', () =>
  497. $timeout(() => ide.$scope.$broadcast('review-panel:layout', false))
  498. )
  499. $scope.$on(
  500. 'editor:focus:changed',
  501. function (
  502. e,
  503. selection_offset_start,
  504. selection_offset_end,
  505. selection,
  506. updateType = null
  507. ) {
  508. const doc_id = $scope.editor.open_doc_id
  509. const entries = getDocEntries(doc_id)
  510. // All selected changes will be added to this array.
  511. ide.$scope.reviewPanel.selectedEntryIds = []
  512. // Count of user-visible changes, i.e. an aggregated change will count as one.
  513. ide.$scope.reviewPanel.nVisibleSelectedChanges = 0
  514. const offset = selection_offset_start
  515. const length = selection_offset_end - selection_offset_start
  516. // Recreate the add comment and bulk actions entries only when
  517. // necessary. This is to avoid the UI thinking that these entries have
  518. // changed and getting into an infinite loop.
  519. if (selection) {
  520. const existingAddComment = entries['add-comment']
  521. if (
  522. !existingAddComment ||
  523. existingAddComment.offset !== offset ||
  524. existingAddComment.length !== length
  525. ) {
  526. entries['add-comment'] = {
  527. type: 'add-comment',
  528. offset,
  529. length,
  530. }
  531. }
  532. const existingBulkActions = entries['bulk-actions']
  533. if (
  534. !existingBulkActions ||
  535. existingBulkActions.offset !== offset ||
  536. existingBulkActions.length !== length
  537. ) {
  538. entries['bulk-actions'] = {
  539. type: 'bulk-actions',
  540. offset,
  541. length,
  542. }
  543. }
  544. } else {
  545. delete entries['add-comment']
  546. delete entries['bulk-actions']
  547. }
  548. for (const id in entries) {
  549. const entry = entries[id]
  550. let isChangeEntryAndWithinSelection = false
  551. if (
  552. entry.type === 'comment' &&
  553. !ide.$scope.reviewPanel.resolvedThreadIds[entry.thread_id]
  554. ) {
  555. entry.focused =
  556. entry.offset <= selection_offset_start &&
  557. selection_offset_start <= entry.offset + entry.content.length
  558. } else if (entry.type === 'insert') {
  559. isChangeEntryAndWithinSelection =
  560. entry.offset >= selection_offset_start &&
  561. entry.offset + entry.content.length <= selection_offset_end
  562. entry.focused =
  563. entry.offset <= selection_offset_start &&
  564. selection_offset_start <= entry.offset + entry.content.length
  565. } else if (entry.type === 'delete') {
  566. isChangeEntryAndWithinSelection =
  567. selection_offset_start <= entry.offset &&
  568. entry.offset <= selection_offset_end
  569. entry.focused = entry.offset === selection_offset_start
  570. } else if (entry.type === 'aggregate-change') {
  571. isChangeEntryAndWithinSelection =
  572. entry.offset >= selection_offset_start &&
  573. entry.offset + entry.content.length <= selection_offset_end
  574. entry.focused =
  575. entry.offset <= selection_offset_start &&
  576. selection_offset_start <= entry.offset + entry.content.length
  577. } else if (
  578. ['add-comment', 'bulk-actions'].includes(entry.type) &&
  579. selection
  580. ) {
  581. entry.focused = true
  582. }
  583. if (isChangeEntryAndWithinSelection) {
  584. for (const entry_id of Array.from(entry.entry_ids)) {
  585. ide.$scope.reviewPanel.selectedEntryIds.push(entry_id)
  586. }
  587. ide.$scope.reviewPanel.nVisibleSelectedChanges++
  588. }
  589. }
  590. $scope.$broadcast('review-panel:recalculate-screen-positions')
  591. dispatchReviewPanelEvent('recalculate-screen-positions', {
  592. entries,
  593. updateType,
  594. })
  595. // Ensure that watchers, such as the React-based review panel component,
  596. // are informed of the changes to entries
  597. ide.$scope.$apply()
  598. return ide.$scope.$broadcast('review-panel:layout')
  599. }
  600. )
  601. ide.$scope.acceptChanges = function (change_ids) {
  602. _doAcceptChanges(change_ids)
  603. eventTracking.sendMB('rp-changes-accepted', {
  604. view: $scope.ui.reviewPanelOpen
  605. ? ide.$scope.reviewPanel.subView
  606. : 'mini',
  607. })
  608. }
  609. ide.$scope.rejectChanges = function (change_ids) {
  610. _doRejectChanges(change_ids)
  611. eventTracking.sendMB('rp-changes-rejected', {
  612. view: $scope.ui.reviewPanelOpen
  613. ? ide.$scope.reviewPanel.subView
  614. : 'mini',
  615. })
  616. }
  617. // The next two functions control a class on the review panel that affects
  618. // the overflow-y rule on the panel. This is necessary so that an entry in
  619. // the review panel is visible when hovering over its indicator when the
  620. // review panel is minimized. See issue #8057.
  621. $scope.mouseEnterIndicator = function () {
  622. ide.$scope.reviewPanel.entryHover = true
  623. }
  624. $scope.mouseLeaveIndicator = function () {
  625. ide.$scope.reviewPanel.entryHover = false
  626. }
  627. function _doAcceptChanges(change_ids) {
  628. $http.post(
  629. `/project/${$scope.project_id}/doc/${$scope.editor.open_doc_id}/changes/accept`,
  630. { change_ids, _csrf: window.csrfToken }
  631. )
  632. $scope.$broadcast('changes:accept', change_ids)
  633. dispatchReviewPanelEvent('changes:accept', change_ids)
  634. }
  635. const _doRejectChanges = change_ids => {
  636. $scope.$broadcast('changes:reject', change_ids)
  637. dispatchReviewPanelEvent('changes:reject', change_ids)
  638. }
  639. ide.$scope.bulkAcceptActions = function () {
  640. _doAcceptChanges(ide.$scope.reviewPanel.selectedEntryIds.slice())
  641. eventTracking.sendMB('rp-bulk-accept', {
  642. view: $scope.ui.reviewPanelOpen
  643. ? ide.$scope.reviewPanel.subView
  644. : 'mini',
  645. nEntries: ide.$scope.reviewPanel.nVisibleSelectedChanges,
  646. })
  647. }
  648. ide.$scope.bulkRejectActions = function () {
  649. _doRejectChanges(ide.$scope.reviewPanel.selectedEntryIds.slice())
  650. eventTracking.sendMB('rp-bulk-reject', {
  651. view: $scope.ui.reviewPanelOpen
  652. ? ide.$scope.reviewPanel.subView
  653. : 'mini',
  654. nEntries: ide.$scope.reviewPanel.nVisibleSelectedChanges,
  655. })
  656. }
  657. ide.$scope.addNewComment = function (e) {
  658. e.preventDefault()
  659. ide.$scope.$broadcast('comment:start_adding')
  660. return $scope.toggleReviewPanel()
  661. }
  662. $scope.addNewCommentFromKbdShortcut = function () {
  663. if (!$scope.project.features.trackChangesVisible) {
  664. return
  665. }
  666. $scope.$broadcast('comment:select_line')
  667. dispatchReviewPanelEvent('comment:select_line')
  668. if (!$scope.ui.reviewPanelOpen) {
  669. $scope.toggleReviewPanel()
  670. }
  671. return $timeout(function () {
  672. ide.$scope.$broadcast('review-panel:layout')
  673. ide.$scope.$broadcast('comment:start_adding')
  674. })
  675. }
  676. $scope.startNewComment = function () {
  677. $scope.$broadcast('comment:select_line')
  678. dispatchReviewPanelEvent('comment:select_line')
  679. return $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
  680. }
  681. ide.$scope.submitNewComment = function (content) {
  682. if (content == null || content === '') {
  683. return
  684. }
  685. const doc_id = $scope.editor.open_doc_id
  686. const entries = getDocEntries(doc_id)
  687. if (entries['add-comment'] == null) {
  688. return
  689. }
  690. const { offset, length } = entries['add-comment']
  691. const thread_id = RangesTracker.generateId()
  692. const thread = getThread(thread_id)
  693. thread.submitting = true
  694. const emitCommentAdd = () => {
  695. $scope.$broadcast('comment:add', thread_id, offset, length)
  696. dispatchReviewPanelEvent('comment:add', {
  697. threadId: thread_id,
  698. offset,
  699. length,
  700. })
  701. // TODO: unused?
  702. $scope.$broadcast('editor:clearSelection')
  703. $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
  704. eventTracking.sendMB('rp-new-comment', { size: content.length })
  705. }
  706. return $http
  707. .post(`/project/${$scope.project_id}/thread/${thread_id}/messages`, {
  708. content,
  709. _csrf: window.csrfToken,
  710. })
  711. .then(() => {
  712. emitCommentAdd()
  713. })
  714. .catch(() => {
  715. ide.showGenericMessageModal(
  716. 'Error submitting comment',
  717. 'Sorry, there was a problem submitting your comment'
  718. )
  719. throw Error('Error submitting comment')
  720. })
  721. }
  722. $scope.cancelNewComment = entry =>
  723. $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
  724. $scope.startReply = function (entry) {
  725. entry.replying = true
  726. return $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
  727. }
  728. ide.$scope.submitReply = function (entry, entry_id) {
  729. const { thread_id } = entry
  730. const content = entry.replyContent
  731. $http
  732. .post(`/project/${$scope.project_id}/thread/${thread_id}/messages`, {
  733. content,
  734. _csrf: window.csrfToken,
  735. })
  736. .catch(() =>
  737. ide.showGenericMessageModal(
  738. 'Error submitting comment',
  739. 'Sorry, there was a problem submitting your comment'
  740. )
  741. )
  742. const trackingMetadata = {
  743. view: $scope.ui.reviewPanelOpen
  744. ? ide.$scope.reviewPanel.subView
  745. : 'mini',
  746. size: entry.replyContent.length,
  747. thread: thread_id,
  748. }
  749. const thread = getThread(thread_id)
  750. thread.submitting = true
  751. entry.replyContent = ''
  752. entry.replying = false
  753. $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
  754. eventTracking.sendMB('rp-comment-reply', trackingMetadata)
  755. }
  756. $scope.cancelReply = function (entry) {
  757. entry.replying = false
  758. entry.replyContent = ''
  759. return ide.$scope.$broadcast('review-panel:layout')
  760. }
  761. ide.$scope.resolveComment = function (doc_id, entry_id) {
  762. const entry = getDocEntries(doc_id)[entry_id]
  763. entry.focused = false
  764. $http.post(
  765. `/project/${$scope.project_id}/thread/${entry.thread_id}/resolve`,
  766. { _csrf: window.csrfToken }
  767. )
  768. _onCommentResolved(entry.thread_id, ide.$scope.user)
  769. eventTracking.sendMB('rp-comment-resolve', {
  770. view: $scope.ui.reviewPanelOpen
  771. ? ide.$scope.reviewPanel.subView
  772. : 'mini',
  773. })
  774. }
  775. ide.$scope.unresolveComment = function (thread_id) {
  776. _onCommentReopened(thread_id)
  777. $http.post(`/project/${$scope.project_id}/thread/${thread_id}/reopen`, {
  778. _csrf: window.csrfToken,
  779. })
  780. eventTracking.sendMB('rp-comment-reopen')
  781. }
  782. function _onCommentResolved(thread_id, user) {
  783. const thread = getThread(thread_id)
  784. if (thread == null) {
  785. return
  786. }
  787. thread.resolved = true
  788. thread.resolved_by_user = formatUser(user)
  789. thread.resolved_at = new Date().toISOString()
  790. ide.$scope.reviewPanel.resolvedThreadIds[thread_id] = true
  791. $scope.$broadcast('comment:resolve_threads', [thread_id])
  792. dispatchReviewPanelEvent('comment:resolve_threads', [thread_id])
  793. }
  794. function _onCommentReopened(thread_id) {
  795. const thread = getThread(thread_id)
  796. if (thread == null) {
  797. return
  798. }
  799. delete thread.resolved
  800. delete thread.resolved_by_user
  801. delete thread.resolved_at
  802. delete ide.$scope.reviewPanel.resolvedThreadIds[thread_id]
  803. $scope.$broadcast('comment:unresolve_thread', thread_id)
  804. dispatchReviewPanelEvent('comment:unresolve_thread', thread_id)
  805. }
  806. function _onThreadDeleted(thread_id) {
  807. delete ide.$scope.reviewPanel.resolvedThreadIds[thread_id]
  808. delete ide.$scope.reviewPanel.commentThreads[thread_id]
  809. $scope.$broadcast('comment:remove', thread_id)
  810. dispatchReviewPanelEvent('comment:remove', thread_id)
  811. }
  812. function _onCommentEdited(thread_id, comment_id, content) {
  813. const thread = getThread(thread_id)
  814. if (thread == null) {
  815. return
  816. }
  817. for (const message of Array.from(thread.messages)) {
  818. if (message.id === comment_id) {
  819. message.content = content
  820. }
  821. }
  822. return updateEntries()
  823. }
  824. function _onCommentDeleted(thread_id, comment_id) {
  825. const thread = getThread(thread_id)
  826. if (thread == null) {
  827. return
  828. }
  829. thread.messages = thread.messages.filter(m => m.id !== comment_id)
  830. return updateEntries()
  831. }
  832. ide.$scope.deleteThread = function (entry_id, doc_id, thread_id) {
  833. _onThreadDeleted(thread_id)
  834. $http({
  835. method: 'DELETE',
  836. url: `/project/${$scope.project_id}/doc/${doc_id}/thread/${thread_id}`,
  837. headers: {
  838. 'X-CSRF-Token': window.csrfToken,
  839. },
  840. })
  841. eventTracking.sendMB('rp-comment-delete')
  842. }
  843. ide.$scope.saveEdit = function (thread_id, comment_id, content) {
  844. $http.post(
  845. `/project/${$scope.project_id}/thread/${thread_id}/messages/${comment_id}/edit`,
  846. {
  847. content,
  848. _csrf: window.csrfToken,
  849. }
  850. )
  851. return $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
  852. }
  853. ide.$scope.deleteComment = function (thread_id, comment_id) {
  854. _onCommentDeleted(thread_id, comment_id)
  855. $http({
  856. method: 'DELETE',
  857. url: `/project/${$scope.project_id}/thread/${thread_id}/messages/${comment_id}`,
  858. headers: {
  859. 'X-CSRF-Token': window.csrfToken,
  860. },
  861. })
  862. return $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
  863. }
  864. $scope.setSubView = function (subView) {
  865. ide.$scope.reviewPanel.subView = subView
  866. eventTracking.sendMB('rp-subview-change', { subView })
  867. }
  868. ide.$scope.gotoEntry = (doc_id, entry_offset) =>
  869. ide.editorManager.openDocId(doc_id, { gotoOffset: entry_offset })
  870. const _setUserTCState = function (userId, newValue, isLocal) {
  871. if (isLocal == null) {
  872. isLocal = false
  873. }
  874. if (ide.$scope.reviewPanel.trackChangesState[userId] == null) {
  875. ide.$scope.reviewPanel.trackChangesState[userId] = {}
  876. }
  877. const state = ide.$scope.reviewPanel.trackChangesState[userId]
  878. if (
  879. state.syncState == null ||
  880. state.syncState === UserTCSyncState.SYNCED
  881. ) {
  882. state.value = newValue
  883. state.syncState = UserTCSyncState.SYNCED
  884. } else if (
  885. state.syncState === UserTCSyncState.PENDING &&
  886. state.value === newValue
  887. ) {
  888. state.syncState = UserTCSyncState.SYNCED
  889. } else if (isLocal) {
  890. state.value = newValue
  891. state.syncState = UserTCSyncState.PENDING
  892. }
  893. if (userId === ide.$scope.user.id) {
  894. return ($scope.editor.wantTrackChanges = newValue)
  895. }
  896. }
  897. const _setEveryoneTCState = function (newValue, isLocal) {
  898. if (isLocal == null) {
  899. isLocal = false
  900. }
  901. ide.$scope.reviewPanel.trackChangesOnForEveryone = newValue
  902. const { project } = $scope
  903. for (const member of Array.from(project.members)) {
  904. _setUserTCState(member._id, newValue, isLocal)
  905. }
  906. _setGuestsTCState(newValue, isLocal)
  907. return _setUserTCState(project.owner._id, newValue, isLocal)
  908. }
  909. function _setGuestsTCState(newValue, isLocal) {
  910. if (isLocal == null) {
  911. isLocal = false
  912. }
  913. ide.$scope.reviewPanel.trackChangesOnForGuests = newValue
  914. if (
  915. currentUserType() === UserTypes.GUEST ||
  916. currentUserType() === UserTypes.ANONYMOUS
  917. ) {
  918. return ($scope.editor.wantTrackChanges = newValue)
  919. }
  920. }
  921. const applyClientTrackChangesStateToServer = function () {
  922. ide.$scope.reviewPanel.trackChangesState = {
  923. ...ide.$scope.reviewPanel.trackChangesState,
  924. }
  925. const data = {}
  926. if (ide.$scope.reviewPanel.trackChangesOnForEveryone) {
  927. data.on = true
  928. } else {
  929. data.on_for = {}
  930. for (const userId in ide.$scope.reviewPanel.trackChangesState) {
  931. const userState = ide.$scope.reviewPanel.trackChangesState[userId]
  932. data.on_for[userId] = userState.value
  933. }
  934. if (ide.$scope.reviewPanel.trackChangesOnForGuests) {
  935. data.on_for_guests = true
  936. }
  937. }
  938. data._csrf = window.csrfToken
  939. return $http.post(`/project/${$scope.project_id}/track_changes`, data)
  940. }
  941. const applyTrackChangesStateToClient = function (state) {
  942. if (typeof state === 'boolean') {
  943. _setEveryoneTCState(state)
  944. return _setGuestsTCState(state)
  945. } else {
  946. const { project } = $scope
  947. ide.$scope.reviewPanel.trackChangesOnForEveryone = false
  948. _setGuestsTCState(state.__guests__ === true)
  949. for (const member of Array.from(project.members)) {
  950. _setUserTCState(
  951. member._id,
  952. state[member._id] != null ? state[member._id] : false
  953. )
  954. }
  955. return _setUserTCState(
  956. $scope.project.owner._id,
  957. state[$scope.project.owner._id] != null
  958. ? state[$scope.project.owner._id]
  959. : false
  960. )
  961. }
  962. }
  963. ide.$scope.toggleTrackChangesForEveryone = function (onForEveryone) {
  964. _setEveryoneTCState(onForEveryone, true)
  965. _setGuestsTCState(onForEveryone, true)
  966. return applyClientTrackChangesStateToServer()
  967. }
  968. ide.$scope.toggleTrackChangesForGuests = function (onForGuests) {
  969. _setGuestsTCState(onForGuests, true)
  970. return applyClientTrackChangesStateToServer()
  971. }
  972. ide.$scope.toggleTrackChangesForUser = function (onForUser, userId) {
  973. _setUserTCState(userId, onForUser, true)
  974. return applyClientTrackChangesStateToServer()
  975. }
  976. ide.socket.on('toggle-track-changes', state =>
  977. $scope.$apply(() => applyTrackChangesStateToClient(state))
  978. )
  979. $scope.toggleTrackChangesFromKbdShortcut = function () {
  980. if (
  981. !(
  982. $scope.project.features.trackChangesVisible &&
  983. $scope.project.features.trackChanges
  984. )
  985. ) {
  986. return
  987. }
  988. return $scope.toggleTrackChangesForUser(
  989. !ide.$scope.reviewPanel.trackChangesState[ide.$scope.user.id].value,
  990. ide.$scope.user.id
  991. )
  992. }
  993. const setGuestFeatureBasedOnProjectAccessLevel = projectPublicAccessLevel =>
  994. (ide.$scope.reviewPanel.trackChangesForGuestsAvailable =
  995. projectPublicAccessLevel === 'tokenBased')
  996. const onToggleTrackChangesForGuestsAvailability = function (available) {
  997. // If the feature is no longer available we need to turn off the guest flag
  998. if (available) {
  999. return
  1000. }
  1001. if (!ide.$scope.reviewPanel.trackChangesOnForGuests) {
  1002. return
  1003. } // Already turned off
  1004. if (ide.$scope.reviewPanel.trackChangesOnForEveryone) {
  1005. return
  1006. } // Overrides guest setting
  1007. return $scope.toggleTrackChangesForGuests(false)
  1008. }
  1009. $scope.$watch(
  1010. 'project.publicAccesLevel',
  1011. setGuestFeatureBasedOnProjectAccessLevel
  1012. )
  1013. $scope.$watch(
  1014. 'reviewPanel.trackChangesForGuestsAvailable',
  1015. function (available) {
  1016. if (available != null) {
  1017. return onToggleTrackChangesForGuestsAvailability(available)
  1018. }
  1019. }
  1020. )
  1021. let _inited = false
  1022. ide.$scope.$on('project:joined', function () {
  1023. if (_inited) {
  1024. return
  1025. }
  1026. const { project } = ide.$scope
  1027. if (project.features.trackChanges) {
  1028. applyTrackChangesStateToClient(project.trackChangesState)
  1029. } else {
  1030. applyTrackChangesStateToClient(false)
  1031. }
  1032. setGuestFeatureBasedOnProjectAccessLevel(project.publicAccesLevel)
  1033. return (_inited = true)
  1034. })
  1035. let _refreshingRangeUsers = false
  1036. const _refreshedForUserIds = {}
  1037. function refreshChangeUsers(refresh_for_user_id) {
  1038. if (refresh_for_user_id != null) {
  1039. if (_refreshedForUserIds[refresh_for_user_id] != null) {
  1040. // We've already tried to refresh to get this user id, so stop it looping
  1041. return
  1042. }
  1043. _refreshedForUserIds[refresh_for_user_id] = true
  1044. }
  1045. // Only do one refresh at once
  1046. if (_refreshingRangeUsers) {
  1047. return
  1048. }
  1049. _refreshingRangeUsers = true
  1050. return $http
  1051. .get(`/project/${$scope.project_id}/changes/users`)
  1052. .then(function (response) {
  1053. const users = response.data
  1054. _refreshingRangeUsers = false
  1055. ide.$scope.users = $scope.users = {}
  1056. // Always include ourself, since if we submit an op, we might need to display info
  1057. // about it locally before it has been flushed through the server
  1058. if (
  1059. (ide.$scope.user != null ? ide.$scope.user.id : undefined) != null
  1060. ) {
  1061. $scope.users[ide.$scope.user.id] = formatUser(ide.$scope.user)
  1062. }
  1063. return (() => {
  1064. const result = []
  1065. for (const user of Array.from(users)) {
  1066. if (user.id != null) {
  1067. result.push(($scope.users[user.id] = formatUser(user)))
  1068. } else {
  1069. result.push(undefined)
  1070. }
  1071. }
  1072. return result
  1073. })()
  1074. })
  1075. .catch(() => (_refreshingRangeUsers = false))
  1076. }
  1077. let _threadsLoadedOnce = false
  1078. let _loadingThreadsInProgress = false
  1079. async function ensureThreadsAreLoaded() {
  1080. if (_threadsLoadedOnce) {
  1081. // We get any updates in real time so only need to load them once.
  1082. return
  1083. }
  1084. _threadsLoadedOnce = true
  1085. _loadingThreadsInProgress = true
  1086. ide.$scope.loadingThreads = true
  1087. return $http
  1088. .get(`/project/${$scope.project_id}/threads`)
  1089. .then(async function (response) {
  1090. const threads = response.data
  1091. ide.$scope.loadingThreads = false
  1092. _loadingThreadsInProgress = false
  1093. for (const thread_id in ide.$scope.reviewPanel.resolvedThreadIds) {
  1094. delete ide.$scope.reviewPanel.resolvedThreadIds[thread_id]
  1095. }
  1096. for (const thread_id in threads) {
  1097. const thread = threads[thread_id]
  1098. for (const comment of Array.from(thread.messages)) {
  1099. formatComment(comment)
  1100. }
  1101. if (thread.resolved_by_user != null) {
  1102. thread.resolved_by_user = formatUser(thread.resolved_by_user)
  1103. ide.$scope.reviewPanel.resolvedThreadIds[thread_id] = true
  1104. $scope.$broadcast('comment:resolve_threads', [thread_id])
  1105. }
  1106. }
  1107. ide.$scope.reviewPanel.commentThreads = threads
  1108. // Update entries so that the view has up-to-date information about
  1109. // the entries when handling the loaded_threads events, which avoids
  1110. // thrashing
  1111. await updateEntries($scope.editor.open_doc_id)
  1112. dispatchReviewPanelEvent('loaded_threads')
  1113. return $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
  1114. })
  1115. }
  1116. function formatComment(comment) {
  1117. comment.user = formatUser(comment.user)
  1118. comment.timestamp = new Date(comment.timestamp)
  1119. return comment
  1120. }
  1121. function formatUser(user) {
  1122. let isSelf, name
  1123. const id =
  1124. (user != null ? user._id : undefined) ||
  1125. (user != null ? user.id : undefined)
  1126. if (id == null) {
  1127. return {
  1128. email: null,
  1129. name: 'Anonymous',
  1130. isSelf: false,
  1131. hue: ColorManager.ANONYMOUS_HUE,
  1132. avatar_text: 'A',
  1133. }
  1134. }
  1135. if (id === window.user_id) {
  1136. name = 'You'
  1137. isSelf = true
  1138. } else {
  1139. name = [user.first_name, user.last_name]
  1140. .filter(n => n != null && n !== '')
  1141. .join(' ')
  1142. if (name === '') {
  1143. name =
  1144. (user.email != null ? user.email.split('@')[0] : undefined) ||
  1145. 'Unknown'
  1146. }
  1147. isSelf = false
  1148. }
  1149. return {
  1150. id,
  1151. email: user.email,
  1152. name,
  1153. isSelf,
  1154. hue: ColorManager.getHueForUserId(id),
  1155. avatar_text: [user.first_name, user.last_name]
  1156. .filter(n => n != null)
  1157. .map(n => n[0])
  1158. .join(''),
  1159. }
  1160. }
  1161. // listen for events from the CodeMirror 6 track changes extension
  1162. window.addEventListener('editor:event', event => {
  1163. const { type, payload } = event.detail
  1164. switch (type) {
  1165. case 'line-height': {
  1166. ide.$scope.reviewPanel.rendererData.lineHeight = payload
  1167. ide.$scope.$broadcast('review-panel:layout')
  1168. break
  1169. }
  1170. case 'track-changes:changed': {
  1171. $scope.$broadcast('editor:track-changes:changed')
  1172. break
  1173. }
  1174. case 'track-changes:visibility_changed': {
  1175. $scope.$broadcast('editor:track-changes:visibility_changed')
  1176. break
  1177. }
  1178. case 'focus:changed': {
  1179. const { from, to, empty, updateType } = payload
  1180. $scope.$broadcast(
  1181. 'editor:focus:changed',
  1182. from,
  1183. to,
  1184. !empty,
  1185. updateType
  1186. )
  1187. break
  1188. }
  1189. case 'add-new-comment': {
  1190. $scope.addNewCommentFromKbdShortcut()
  1191. break
  1192. }
  1193. case 'toggle-track-changes': {
  1194. $scope.toggleTrackChangesFromKbdShortcut()
  1195. break
  1196. }
  1197. case 'toggle-review-panel': {
  1198. $scope.toggleReviewPanel()
  1199. break
  1200. }
  1201. }
  1202. })
  1203. // Add methods somewhere that React can see them
  1204. $scope.reviewPanel.saveEdit = $scope.saveEdit
  1205. },
  1206. ])
  1207. // send events to the CodeMirror 6 track changes extension
  1208. const dispatchReviewPanelEvent = (type, payload) => {
  1209. window.dispatchEvent(
  1210. new CustomEvent('review-panel:event', {
  1211. detail: { type, payload },
  1212. })
  1213. )
  1214. }