ReviewPanelController.js 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349
  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}/doc/${doc_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 (doc_id, thread_id) {
  776. _onCommentReopened(thread_id)
  777. $http.post(
  778. `/project/${$scope.project_id}/doc/${doc_id}/thread/${thread_id}/reopen`,
  779. {
  780. _csrf: window.csrfToken,
  781. }
  782. )
  783. eventTracking.sendMB('rp-comment-reopen')
  784. }
  785. function _onCommentResolved(thread_id, user) {
  786. const thread = getThread(thread_id)
  787. if (thread == null) {
  788. return
  789. }
  790. thread.resolved = true
  791. thread.resolved_by_user = formatUser(user)
  792. thread.resolved_at = new Date().toISOString()
  793. ide.$scope.reviewPanel.resolvedThreadIds[thread_id] = true
  794. $scope.$broadcast('comment:resolve_threads', [thread_id])
  795. dispatchReviewPanelEvent('comment:resolve_threads', [thread_id])
  796. }
  797. function _onCommentReopened(thread_id) {
  798. const thread = getThread(thread_id)
  799. if (thread == null) {
  800. return
  801. }
  802. delete thread.resolved
  803. delete thread.resolved_by_user
  804. delete thread.resolved_at
  805. delete ide.$scope.reviewPanel.resolvedThreadIds[thread_id]
  806. $scope.$broadcast('comment:unresolve_thread', thread_id)
  807. dispatchReviewPanelEvent('comment:unresolve_thread', thread_id)
  808. }
  809. function _onThreadDeleted(thread_id) {
  810. delete ide.$scope.reviewPanel.resolvedThreadIds[thread_id]
  811. delete ide.$scope.reviewPanel.commentThreads[thread_id]
  812. $scope.$broadcast('comment:remove', thread_id)
  813. dispatchReviewPanelEvent('comment:remove', thread_id)
  814. }
  815. function _onCommentEdited(thread_id, comment_id, content) {
  816. const thread = getThread(thread_id)
  817. if (thread == null) {
  818. return
  819. }
  820. for (const message of Array.from(thread.messages)) {
  821. if (message.id === comment_id) {
  822. message.content = content
  823. }
  824. }
  825. return updateEntries()
  826. }
  827. function _onCommentDeleted(thread_id, comment_id) {
  828. const thread = getThread(thread_id)
  829. if (thread == null) {
  830. return
  831. }
  832. thread.messages = thread.messages.filter(m => m.id !== comment_id)
  833. return updateEntries()
  834. }
  835. ide.$scope.deleteThread = function (entry_id, doc_id, thread_id) {
  836. _onThreadDeleted(thread_id)
  837. $http({
  838. method: 'DELETE',
  839. url: `/project/${$scope.project_id}/doc/${doc_id}/thread/${thread_id}`,
  840. headers: {
  841. 'X-CSRF-Token': window.csrfToken,
  842. },
  843. })
  844. eventTracking.sendMB('rp-comment-delete')
  845. }
  846. ide.$scope.saveEdit = function (thread_id, comment_id, content) {
  847. $http.post(
  848. `/project/${$scope.project_id}/thread/${thread_id}/messages/${comment_id}/edit`,
  849. {
  850. content,
  851. _csrf: window.csrfToken,
  852. }
  853. )
  854. return $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
  855. }
  856. ide.$scope.deleteComment = function (thread_id, comment_id) {
  857. _onCommentDeleted(thread_id, comment_id)
  858. $http({
  859. method: 'DELETE',
  860. url: `/project/${$scope.project_id}/thread/${thread_id}/messages/${comment_id}`,
  861. headers: {
  862. 'X-CSRF-Token': window.csrfToken,
  863. },
  864. })
  865. return $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
  866. }
  867. $scope.setSubView = function (subView) {
  868. ide.$scope.reviewPanel.subView = subView
  869. eventTracking.sendMB('rp-subview-change', { subView })
  870. }
  871. ide.$scope.gotoEntry = (doc_id, entry_offset) =>
  872. ide.editorManager.openDocId(doc_id, { gotoOffset: entry_offset })
  873. const _setUserTCState = function (userId, newValue, isLocal) {
  874. if (isLocal == null) {
  875. isLocal = false
  876. }
  877. if (ide.$scope.reviewPanel.trackChangesState[userId] == null) {
  878. ide.$scope.reviewPanel.trackChangesState[userId] = {}
  879. }
  880. const state = ide.$scope.reviewPanel.trackChangesState[userId]
  881. if (
  882. state.syncState == null ||
  883. state.syncState === UserTCSyncState.SYNCED
  884. ) {
  885. state.value = newValue
  886. state.syncState = UserTCSyncState.SYNCED
  887. } else if (
  888. state.syncState === UserTCSyncState.PENDING &&
  889. state.value === newValue
  890. ) {
  891. state.syncState = UserTCSyncState.SYNCED
  892. } else if (isLocal) {
  893. state.value = newValue
  894. state.syncState = UserTCSyncState.PENDING
  895. }
  896. if (userId === ide.$scope.user.id) {
  897. return ($scope.editor.wantTrackChanges = newValue)
  898. }
  899. }
  900. const _setEveryoneTCState = function (newValue, isLocal) {
  901. if (isLocal == null) {
  902. isLocal = false
  903. }
  904. ide.$scope.reviewPanel.trackChangesOnForEveryone = newValue
  905. const { project } = $scope
  906. for (const member of Array.from(project.members)) {
  907. _setUserTCState(member._id, newValue, isLocal)
  908. }
  909. _setGuestsTCState(newValue, isLocal)
  910. return _setUserTCState(project.owner._id, newValue, isLocal)
  911. }
  912. function _setGuestsTCState(newValue, isLocal) {
  913. if (isLocal == null) {
  914. isLocal = false
  915. }
  916. ide.$scope.reviewPanel.trackChangesOnForGuests = newValue
  917. if (
  918. currentUserType() === UserTypes.GUEST ||
  919. currentUserType() === UserTypes.ANONYMOUS
  920. ) {
  921. return ($scope.editor.wantTrackChanges = newValue)
  922. }
  923. }
  924. const applyClientTrackChangesStateToServer = function () {
  925. ide.$scope.reviewPanel.trackChangesState = {
  926. ...ide.$scope.reviewPanel.trackChangesState,
  927. }
  928. const data = {}
  929. if (ide.$scope.reviewPanel.trackChangesOnForEveryone) {
  930. data.on = true
  931. } else {
  932. data.on_for = {}
  933. for (const userId in ide.$scope.reviewPanel.trackChangesState) {
  934. const userState = ide.$scope.reviewPanel.trackChangesState[userId]
  935. data.on_for[userId] = userState.value
  936. }
  937. if (ide.$scope.reviewPanel.trackChangesOnForGuests) {
  938. data.on_for_guests = true
  939. }
  940. }
  941. data._csrf = window.csrfToken
  942. return $http.post(`/project/${$scope.project_id}/track_changes`, data)
  943. }
  944. const applyTrackChangesStateToClient = function (state) {
  945. if (typeof state === 'boolean') {
  946. _setEveryoneTCState(state)
  947. return _setGuestsTCState(state)
  948. } else {
  949. const { project } = $scope
  950. ide.$scope.reviewPanel.trackChangesOnForEveryone = false
  951. _setGuestsTCState(state.__guests__ === true)
  952. for (const member of Array.from(project.members)) {
  953. _setUserTCState(
  954. member._id,
  955. state[member._id] != null ? state[member._id] : false
  956. )
  957. }
  958. return _setUserTCState(
  959. $scope.project.owner._id,
  960. state[$scope.project.owner._id] != null
  961. ? state[$scope.project.owner._id]
  962. : false
  963. )
  964. }
  965. }
  966. ide.$scope.toggleTrackChangesForEveryone = function (onForEveryone) {
  967. _setEveryoneTCState(onForEveryone, true)
  968. _setGuestsTCState(onForEveryone, true)
  969. return applyClientTrackChangesStateToServer()
  970. }
  971. ide.$scope.toggleTrackChangesForGuests = function (onForGuests) {
  972. _setGuestsTCState(onForGuests, true)
  973. return applyClientTrackChangesStateToServer()
  974. }
  975. ide.$scope.toggleTrackChangesForUser = function (onForUser, userId) {
  976. _setUserTCState(userId, onForUser, true)
  977. return applyClientTrackChangesStateToServer()
  978. }
  979. ide.socket.on('toggle-track-changes', state =>
  980. $scope.$apply(() => applyTrackChangesStateToClient(state))
  981. )
  982. $scope.toggleTrackChangesFromKbdShortcut = function () {
  983. if (
  984. !(
  985. $scope.project.features.trackChangesVisible &&
  986. $scope.project.features.trackChanges
  987. )
  988. ) {
  989. return
  990. }
  991. return $scope.toggleTrackChangesForUser(
  992. !ide.$scope.reviewPanel.trackChangesState[ide.$scope.user.id].value,
  993. ide.$scope.user.id
  994. )
  995. }
  996. const setGuestFeatureBasedOnProjectAccessLevel = projectPublicAccessLevel =>
  997. (ide.$scope.reviewPanel.trackChangesForGuestsAvailable =
  998. projectPublicAccessLevel === 'tokenBased')
  999. const onToggleTrackChangesForGuestsAvailability = function (available) {
  1000. // If the feature is no longer available we need to turn off the guest flag
  1001. if (available) {
  1002. return
  1003. }
  1004. if (!ide.$scope.reviewPanel.trackChangesOnForGuests) {
  1005. return
  1006. } // Already turned off
  1007. if (ide.$scope.reviewPanel.trackChangesOnForEveryone) {
  1008. return
  1009. } // Overrides guest setting
  1010. return $scope.toggleTrackChangesForGuests(false)
  1011. }
  1012. $scope.$watch(
  1013. 'project.publicAccesLevel',
  1014. setGuestFeatureBasedOnProjectAccessLevel
  1015. )
  1016. $scope.$watch(
  1017. 'reviewPanel.trackChangesForGuestsAvailable',
  1018. function (available) {
  1019. if (available != null) {
  1020. return onToggleTrackChangesForGuestsAvailability(available)
  1021. }
  1022. }
  1023. )
  1024. let _inited = false
  1025. ide.$scope.$on('project:joined', function () {
  1026. if (_inited) {
  1027. return
  1028. }
  1029. const { project } = ide.$scope
  1030. if (project.features.trackChanges) {
  1031. applyTrackChangesStateToClient(project.trackChangesState)
  1032. } else {
  1033. applyTrackChangesStateToClient(false)
  1034. }
  1035. setGuestFeatureBasedOnProjectAccessLevel(project.publicAccesLevel)
  1036. return (_inited = true)
  1037. })
  1038. let _refreshingRangeUsers = false
  1039. const _refreshedForUserIds = {}
  1040. function refreshChangeUsers(refresh_for_user_id) {
  1041. if (refresh_for_user_id != null) {
  1042. if (_refreshedForUserIds[refresh_for_user_id] != null) {
  1043. // We've already tried to refresh to get this user id, so stop it looping
  1044. return
  1045. }
  1046. _refreshedForUserIds[refresh_for_user_id] = true
  1047. }
  1048. // Only do one refresh at once
  1049. if (_refreshingRangeUsers) {
  1050. return
  1051. }
  1052. _refreshingRangeUsers = true
  1053. return $http
  1054. .get(`/project/${$scope.project_id}/changes/users`)
  1055. .then(function (response) {
  1056. const users = response.data
  1057. _refreshingRangeUsers = false
  1058. ide.$scope.users = $scope.users = {}
  1059. // Always include ourself, since if we submit an op, we might need to display info
  1060. // about it locally before it has been flushed through the server
  1061. if (
  1062. (ide.$scope.user != null ? ide.$scope.user.id : undefined) != null
  1063. ) {
  1064. $scope.users[ide.$scope.user.id] = formatUser(ide.$scope.user)
  1065. }
  1066. return (() => {
  1067. const result = []
  1068. for (const user of Array.from(users)) {
  1069. if (user.id != null) {
  1070. result.push(($scope.users[user.id] = formatUser(user)))
  1071. } else {
  1072. result.push(undefined)
  1073. }
  1074. }
  1075. return result
  1076. })()
  1077. })
  1078. .catch(() => (_refreshingRangeUsers = false))
  1079. }
  1080. let _threadsLoadedOnce = false
  1081. let _loadingThreadsInProgress = false
  1082. async function ensureThreadsAreLoaded() {
  1083. if (_threadsLoadedOnce) {
  1084. // We get any updates in real time so only need to load them once.
  1085. return
  1086. }
  1087. _threadsLoadedOnce = true
  1088. _loadingThreadsInProgress = true
  1089. ide.$scope.loadingThreads = true
  1090. return $http
  1091. .get(`/project/${$scope.project_id}/threads`)
  1092. .then(async function (response) {
  1093. const threads = response.data
  1094. ide.$scope.loadingThreads = false
  1095. _loadingThreadsInProgress = false
  1096. for (const thread_id in ide.$scope.reviewPanel.resolvedThreadIds) {
  1097. delete ide.$scope.reviewPanel.resolvedThreadIds[thread_id]
  1098. }
  1099. for (const thread_id in threads) {
  1100. const thread = threads[thread_id]
  1101. for (const comment of Array.from(thread.messages)) {
  1102. formatComment(comment)
  1103. }
  1104. if (thread.resolved_by_user != null) {
  1105. thread.resolved_by_user = formatUser(thread.resolved_by_user)
  1106. ide.$scope.reviewPanel.resolvedThreadIds[thread_id] = true
  1107. $scope.$broadcast('comment:resolve_threads', [thread_id])
  1108. }
  1109. }
  1110. ide.$scope.reviewPanel.commentThreads = threads
  1111. // Update entries so that the view has up-to-date information about
  1112. // the entries when handling the loaded_threads events, which avoids
  1113. // thrashing
  1114. await updateEntries($scope.editor.open_doc_id)
  1115. dispatchReviewPanelEvent('loaded_threads')
  1116. return $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
  1117. })
  1118. }
  1119. function formatComment(comment) {
  1120. comment.user = formatUser(comment.user)
  1121. comment.timestamp = new Date(comment.timestamp)
  1122. return comment
  1123. }
  1124. function formatUser(user) {
  1125. let isSelf, name
  1126. const id =
  1127. (user != null ? user._id : undefined) ||
  1128. (user != null ? user.id : undefined)
  1129. if (id == null) {
  1130. return {
  1131. email: null,
  1132. name: 'Anonymous',
  1133. isSelf: false,
  1134. hue: ColorManager.ANONYMOUS_HUE,
  1135. avatar_text: 'A',
  1136. }
  1137. }
  1138. if (id === window.user_id) {
  1139. name = 'You'
  1140. isSelf = true
  1141. } else {
  1142. name = [user.first_name, user.last_name]
  1143. .filter(n => n != null && n !== '')
  1144. .join(' ')
  1145. if (name === '') {
  1146. name =
  1147. (user.email != null ? user.email.split('@')[0] : undefined) ||
  1148. 'Unknown'
  1149. }
  1150. isSelf = false
  1151. }
  1152. return {
  1153. id,
  1154. email: user.email,
  1155. name,
  1156. isSelf,
  1157. hue: ColorManager.getHueForUserId(id),
  1158. avatar_text: [user.first_name, user.last_name]
  1159. .filter(n => n != null)
  1160. .map(n => n[0])
  1161. .join(''),
  1162. }
  1163. }
  1164. // listen for events from the CodeMirror 6 track changes extension
  1165. window.addEventListener('editor:event', event => {
  1166. const { type, payload } = event.detail
  1167. switch (type) {
  1168. case 'line-height': {
  1169. ide.$scope.reviewPanel.rendererData.lineHeight = payload
  1170. ide.$scope.$broadcast('review-panel:layout')
  1171. break
  1172. }
  1173. case 'track-changes:changed': {
  1174. $scope.$broadcast('editor:track-changes:changed')
  1175. break
  1176. }
  1177. case 'track-changes:visibility_changed': {
  1178. $scope.$broadcast('editor:track-changes:visibility_changed')
  1179. break
  1180. }
  1181. case 'focus:changed': {
  1182. const { from, to, empty, updateType } = payload
  1183. $scope.$broadcast(
  1184. 'editor:focus:changed',
  1185. from,
  1186. to,
  1187. !empty,
  1188. updateType
  1189. )
  1190. break
  1191. }
  1192. case 'add-new-comment': {
  1193. $scope.addNewCommentFromKbdShortcut()
  1194. break
  1195. }
  1196. case 'toggle-track-changes': {
  1197. $scope.toggleTrackChangesFromKbdShortcut()
  1198. break
  1199. }
  1200. case 'toggle-review-panel': {
  1201. $scope.toggleReviewPanel()
  1202. break
  1203. }
  1204. }
  1205. })
  1206. // Add methods somewhere that React can see them
  1207. $scope.reviewPanel.saveEdit = $scope.saveEdit
  1208. },
  1209. ])
  1210. // send events to the CodeMirror 6 track changes extension
  1211. const dispatchReviewPanelEvent = (type, payload) => {
  1212. window.dispatchEvent(
  1213. new CustomEvent('review-panel:event', {
  1214. detail: { type, payload },
  1215. })
  1216. )
  1217. }