ReviewPanelController.coffee 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801
  1. define [
  2. "base",
  3. "utils/EventEmitter"
  4. "ide/colors/ColorManager"
  5. "ide/review-panel/RangesTracker"
  6. ], (App, EventEmitter, ColorManager, RangesTracker) ->
  7. App.controller "ReviewPanelController", ($scope, $element, ide, $timeout, $http, $modal, event_tracking, localStorage) ->
  8. $reviewPanelEl = $element.find "#review-panel"
  9. $scope.SubViews =
  10. CUR_FILE : "cur_file"
  11. OVERVIEW : "overview"
  12. $scope.UserTCSyncState = UserTCSyncState =
  13. SYNCED : "synced"
  14. PENDING : "pending"
  15. $scope.reviewPanel =
  16. trackChangesState: {}
  17. trackChangesOnForEveryone: false
  18. trackChangesOnForGuests: false
  19. trackChangesOnForThisGuestClient: false
  20. trackChangesForGuestsAvailable: false
  21. entries: {}
  22. resolvedComments: {}
  23. hasEntries: false
  24. subView: $scope.SubViews.CUR_FILE
  25. openSubView: $scope.SubViews.CUR_FILE
  26. overview:
  27. loading: false
  28. docsCollapsedState: JSON.parse(localStorage("docs_collapsed_state:#{$scope.project_id}")) or {}
  29. dropdown:
  30. loading: false
  31. commentThreads: {}
  32. resolvedThreadIds: {}
  33. rendererData: {}
  34. formattedProjectMembers: {}
  35. fullTCStateCollapsed: true
  36. loadingThreads: false
  37. # All selected changes. If a aggregated change (insertion + deletion) is selection, the two ids
  38. # will be present. The length of this array will differ from the count below (see explanation).
  39. selectedEntryIds: []
  40. # A count of user-facing selected changes. An aggregated change (insertion + deletion) will count
  41. # as only one.
  42. nVisibleSelectedChanges: 0
  43. window.addEventListener "beforeunload", () ->
  44. collapsedStates = {}
  45. for doc, state of $scope.reviewPanel.overview.docsCollapsedState
  46. if state
  47. collapsedStates[doc] = state
  48. valToStore = if Object.keys(collapsedStates).length > 0 then JSON.stringify(collapsedStates) else null
  49. localStorage("docs_collapsed_state:#{$scope.project_id}", valToStore)
  50. $scope.$on "layout:pdf:linked", (event, state) ->
  51. $scope.$broadcast "review-panel:layout"
  52. $scope.$on "layout:pdf:resize", (event, state) ->
  53. $scope.$broadcast "review-panel:layout", false
  54. $scope.$on "expandable-text-area:resize", (event) ->
  55. $timeout () ->
  56. $scope.$broadcast "review-panel:layout"
  57. $scope.$on "review-panel:sizes", (e, sizes) ->
  58. $scope.$broadcast "editor:set-scroll-size", sizes
  59. $scope.$watch "project.features.trackChangesVisible", (visible) ->
  60. return if !visible?
  61. if !visible
  62. $scope.ui.reviewPanelOpen = false
  63. $scope.$watch "project.members", (members) ->
  64. $scope.reviewPanel.formattedProjectMembers = {}
  65. if $scope.project?.owner?
  66. $scope.reviewPanel.formattedProjectMembers[$scope.project.owner._id] = formatUser($scope.project.owner)
  67. if $scope.project?.members?
  68. for member in members
  69. if member.privileges == "readAndWrite"
  70. $scope.reviewPanel.formattedProjectMembers[member._id] = formatUser(member)
  71. $scope.$watch 'project.publicAccesLevel', (level) ->
  72. if level?
  73. $scope.reviewPanel.trackChangesForGuestsAvailable = level == 'tokenBased'
  74. if !$scope.reviewPanel.trackChangesForGuestsAvailable
  75. $scope.trackChangesOnForThisGuestClient = false
  76. $scope.toggleTrackChangesForGuests false
  77. $scope.commentState =
  78. adding: false
  79. content: ""
  80. $scope.users = {}
  81. $scope.reviewPanelEventsBridge = new EventEmitter()
  82. ide.socket.on "new-comment", (thread_id, comment) ->
  83. thread = getThread(thread_id)
  84. delete thread.submitting
  85. thread.messages.push(formatComment(comment))
  86. $scope.$apply()
  87. $timeout () ->
  88. $scope.$broadcast "review-panel:layout"
  89. ide.socket.on "accept-changes", (doc_id, change_ids) ->
  90. if doc_id != $scope.editor.open_doc_id
  91. getChangeTracker(doc_id).removeChangeIds(change_ids)
  92. else
  93. $scope.$broadcast "changes:accept", change_ids
  94. updateEntries(doc_id)
  95. $scope.$apply () ->
  96. ide.socket.on "resolve-thread", (thread_id, user) ->
  97. _onCommentResolved(thread_id, user)
  98. ide.socket.on "reopen-thread", (thread_id) ->
  99. _onCommentReopened(thread_id)
  100. ide.socket.on "delete-thread", (thread_id) ->
  101. _onThreadDeleted(thread_id)
  102. $scope.$apply () ->
  103. ide.socket.on "edit-message", (thread_id, message_id, content) ->
  104. _onCommentEdited(thread_id, message_id, content)
  105. $scope.$apply () ->
  106. ide.socket.on "delete-message", (thread_id, message_id) ->
  107. _onCommentDeleted(thread_id, message_id)
  108. $scope.$apply () ->
  109. rangesTrackers = {}
  110. getDocEntries = (doc_id) ->
  111. $scope.reviewPanel.entries[doc_id] ?= {}
  112. return $scope.reviewPanel.entries[doc_id]
  113. getDocResolvedComments = (doc_id) ->
  114. $scope.reviewPanel.resolvedComments[doc_id] ?= {}
  115. return $scope.reviewPanel.resolvedComments[doc_id]
  116. getThread = (thread_id) ->
  117. $scope.reviewPanel.commentThreads[thread_id] ?= { messages: [] }
  118. return $scope.reviewPanel.commentThreads[thread_id]
  119. getChangeTracker = (doc_id) ->
  120. if !rangesTrackers[doc_id]?
  121. rangesTrackers[doc_id] = new RangesTracker()
  122. rangesTrackers[doc_id].resolvedThreadIds = $scope.reviewPanel.resolvedThreadIds
  123. return rangesTrackers[doc_id]
  124. scrollbar = {}
  125. $scope.reviewPanelEventsBridge.on "aceScrollbarVisibilityChanged", (isVisible, scrollbarWidth) ->
  126. scrollbar = {isVisible, scrollbarWidth}
  127. updateScrollbar()
  128. updateScrollbar = () ->
  129. if scrollbar.isVisible and $scope.reviewPanel.subView == $scope.SubViews.CUR_FILE
  130. $reviewPanelEl.css "right", "#{ scrollbar.scrollbarWidth }px"
  131. else
  132. $reviewPanelEl.css "right", "0"
  133. $scope.$watch "!ui.reviewPanelOpen && reviewPanel.hasEntries", (open, prevVal) ->
  134. return if !open?
  135. $scope.ui.miniReviewPanelVisible = open
  136. if open != prevVal
  137. $timeout () -> $scope.$broadcast "review-panel:toggle"
  138. $scope.$watch "ui.reviewPanelOpen", (open) ->
  139. return if !open?
  140. if !open
  141. # Always show current file when not open, but save current state
  142. $scope.reviewPanel.openSubView = $scope.reviewPanel.subView
  143. $scope.reviewPanel.subView = $scope.SubViews.CUR_FILE
  144. else
  145. # Reset back to what we had when previously open
  146. $scope.reviewPanel.subView = $scope.reviewPanel.openSubView
  147. $timeout () ->
  148. $scope.$broadcast "review-panel:toggle"
  149. $scope.$broadcast "review-panel:layout", false
  150. $scope.$watch "reviewPanel.subView", (view) ->
  151. return if !view?
  152. updateScrollbar()
  153. if view == $scope.SubViews.OVERVIEW
  154. refreshOverviewPanel()
  155. $scope.$watch "editor.sharejs_doc", (doc, old_doc) ->
  156. return if !doc?
  157. # The open doc range tracker is kept up to date in real-time so
  158. # replace any outdated info with this
  159. rangesTrackers[doc.doc_id] = doc.ranges
  160. rangesTrackers[doc.doc_id].resolvedThreadIds = $scope.reviewPanel.resolvedThreadIds
  161. $scope.reviewPanel.rangesTracker = rangesTrackers[doc.doc_id]
  162. if old_doc?
  163. old_doc.off "flipped_pending_to_inflight"
  164. doc.on "flipped_pending_to_inflight", () ->
  165. regenerateTrackChangesId(doc)
  166. regenerateTrackChangesId(doc)
  167. $scope.$watch (() ->
  168. entries = $scope.reviewPanel.entries[$scope.editor.open_doc_id] or {}
  169. permEntries = {}
  170. for entry, entryData of entries
  171. if entry not in [ "add-comment", "bulk-actions" ]
  172. permEntries[entry] = entryData
  173. Object.keys(permEntries).length
  174. ), (nEntries) ->
  175. $scope.reviewPanel.hasEntries = nEntries > 0 and $scope.project.features.trackChangesVisible
  176. regenerateTrackChangesId = (doc) ->
  177. old_id = getChangeTracker(doc.doc_id).getIdSeed()
  178. new_id = RangesTracker.generateIdSeed()
  179. getChangeTracker(doc.doc_id).setIdSeed(new_id)
  180. doc.setTrackChangesIdSeeds({pending: new_id, inflight: old_id})
  181. refreshRanges = () ->
  182. $http.get "/project/#{$scope.project_id}/ranges"
  183. .then (response) ->
  184. docs = response.data
  185. for doc in docs
  186. if !$scope.reviewPanel.overview.docsCollapsedState[doc.id]?
  187. $scope.reviewPanel.overview.docsCollapsedState[doc.id] = false
  188. if doc.id != $scope.editor.open_doc_id # this is kept up to date in real-time, don't overwrite
  189. rangesTracker = getChangeTracker(doc.id)
  190. rangesTracker.comments = doc.ranges?.comments or []
  191. rangesTracker.changes = doc.ranges?.changes or []
  192. updateEntries(doc.id)
  193. refreshOverviewPanel = () ->
  194. $scope.reviewPanel.overview.loading = true
  195. refreshRanges()
  196. .then () ->
  197. $scope.reviewPanel.overview.loading = false
  198. .catch () ->
  199. $scope.reviewPanel.overview.loading = false
  200. $scope.refreshResolvedCommentsDropdown = () ->
  201. $scope.reviewPanel.dropdown.loading = true
  202. q = refreshRanges()
  203. q.then () ->
  204. $scope.reviewPanel.dropdown.loading = false
  205. q.catch () ->
  206. $scope.reviewPanel.dropdown.loading = false
  207. return q
  208. updateEntries = (doc_id) ->
  209. rangesTracker = getChangeTracker(doc_id)
  210. entries = getDocEntries(doc_id)
  211. resolvedComments = getDocResolvedComments(doc_id)
  212. changed = false
  213. # Assume we'll delete everything until we see it, then we'll remove it from this object
  214. delete_changes = {}
  215. for id, change of entries
  216. if id not in [ "add-comment", "bulk-actions" ]
  217. for entry_id in change.entry_ids
  218. delete_changes[entry_id] = true
  219. for id, change of resolvedComments
  220. for entry_id in change.entry_ids
  221. delete_changes[entry_id] = true
  222. potential_aggregate = false
  223. prev_insertion = null
  224. for change in rangesTracker.changes
  225. changed = true
  226. if (
  227. potential_aggregate and
  228. change.op.d and
  229. change.op.p == prev_insertion.op.p + prev_insertion.op.i.length and
  230. change.metadata.user_id == prev_insertion.metadata.user_id
  231. )
  232. # An actual aggregate op.
  233. entries[prev_insertion.id].type = "aggregate-change"
  234. entries[prev_insertion.id].metadata.replaced_content = change.op.d
  235. entries[prev_insertion.id].entry_ids.push change.id
  236. else
  237. entries[change.id] ?= {}
  238. delete delete_changes[change.id]
  239. new_entry = {
  240. type: if change.op.i then "insert" else "delete"
  241. entry_ids: [ change.id ]
  242. content: change.op.i or change.op.d
  243. offset: change.op.p
  244. metadata: change.metadata
  245. }
  246. for key, value of new_entry
  247. entries[change.id][key] = value
  248. if change.op.i
  249. potential_aggregate = true
  250. prev_insertion = change
  251. else
  252. potential_aggregate = false
  253. prev_insertion = null
  254. if !$scope.users[change.metadata.user_id]?
  255. refreshChangeUsers(change.metadata.user_id)
  256. if rangesTracker.comments.length > 0
  257. ensureThreadsAreLoaded()
  258. for comment in rangesTracker.comments
  259. changed = true
  260. delete delete_changes[comment.id]
  261. if $scope.reviewPanel.resolvedThreadIds[comment.op.t]
  262. new_comment = resolvedComments[comment.id] ?= {}
  263. delete entries[comment.id]
  264. else
  265. new_comment = entries[comment.id] ?= {}
  266. delete resolvedComments[comment.id]
  267. new_entry = {
  268. type: "comment"
  269. thread_id: comment.op.t
  270. entry_ids: [ comment.id ]
  271. content: comment.op.c
  272. offset: comment.op.p
  273. }
  274. for key, value of new_entry
  275. new_comment[key] = value
  276. for change_id, _ of delete_changes
  277. changed = true
  278. delete entries[change_id]
  279. delete resolvedComments[change_id]
  280. if changed
  281. $scope.$broadcast "entries:changed"
  282. $scope.$on "editor:track-changes:changed", () ->
  283. doc_id = $scope.editor.open_doc_id
  284. updateEntries(doc_id)
  285. $scope.$broadcast "review-panel:recalculate-screen-positions"
  286. $scope.$broadcast "review-panel:layout"
  287. $scope.$on "editor:track-changes:visibility_changed", () ->
  288. $timeout () ->
  289. $scope.$broadcast "review-panel:layout", false
  290. $scope.$on "editor:focus:changed", (e, selection_offset_start, selection_offset_end, selection) ->
  291. doc_id = $scope.editor.open_doc_id
  292. entries = getDocEntries(doc_id)
  293. # All selected changes will be added to this array.
  294. $scope.reviewPanel.selectedEntryIds = []
  295. # Count of user-visible changes, i.e. an aggregated change will count as one.
  296. $scope.reviewPanel.nVisibleSelectedChanges = 0
  297. delete entries["add-comment"]
  298. delete entries["bulk-actions"]
  299. if selection
  300. entries["add-comment"] = {
  301. type: "add-comment"
  302. offset: selection_offset_start
  303. length: selection_offset_end - selection_offset_start
  304. }
  305. entries["bulk-actions"] = {
  306. type: "bulk-actions"
  307. offset: selection_offset_start
  308. length: selection_offset_end - selection_offset_start
  309. }
  310. for id, entry of entries
  311. isChangeEntryAndWithinSelection = false
  312. if entry.type == "comment" and not $scope.reviewPanel.resolvedThreadIds[entry.thread_id]
  313. entry.focused = (entry.offset <= selection_offset_start <= entry.offset + entry.content.length)
  314. else if entry.type == "insert"
  315. isChangeEntryAndWithinSelection = entry.offset >= selection_offset_start and entry.offset + entry.content.length <= selection_offset_end
  316. entry.focused = (entry.offset <= selection_offset_start <= entry.offset + entry.content.length)
  317. else if entry.type == "delete"
  318. isChangeEntryAndWithinSelection = selection_offset_start <= entry.offset <= selection_offset_end
  319. entry.focused = (entry.offset == selection_offset_start)
  320. else if entry.type == "aggregate-change"
  321. isChangeEntryAndWithinSelection = entry.offset >= selection_offset_start and entry.offset + entry.content.length <= selection_offset_end
  322. entry.focused = (entry.offset <= selection_offset_start <= entry.offset + entry.content.length)
  323. else if entry.type in [ "add-comment", "bulk-actions" ] and selection
  324. entry.focused = true
  325. if isChangeEntryAndWithinSelection
  326. for entry_id in entry.entry_ids
  327. $scope.reviewPanel.selectedEntryIds.push entry_id
  328. $scope.reviewPanel.nVisibleSelectedChanges++
  329. $scope.$broadcast "review-panel:recalculate-screen-positions"
  330. $scope.$broadcast "review-panel:layout"
  331. $scope.acceptChanges = (change_ids) ->
  332. _doAcceptChanges change_ids
  333. event_tracking.sendMB "rp-changes-accepted", { view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini' }
  334. $scope.rejectChanges = (change_ids) ->
  335. _doRejectChanges change_ids
  336. event_tracking.sendMB "rp-changes-rejected", { view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini' }
  337. _doAcceptChanges = (change_ids) ->
  338. $http.post "/project/#{$scope.project_id}/doc/#{$scope.editor.open_doc_id}/changes/accept", { change_ids, _csrf: window.csrfToken}
  339. $scope.$broadcast "changes:accept", change_ids
  340. _doRejectChanges = (change_ids) ->
  341. $scope.$broadcast "changes:reject", change_ids
  342. bulkAccept = () ->
  343. _doAcceptChanges $scope.reviewPanel.selectedEntryIds.slice()
  344. event_tracking.sendMB "rp-bulk-accept", {
  345. view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini',
  346. nEntries: $scope.reviewPanel.nVisibleSelectedChanges
  347. }
  348. bulkReject = () ->
  349. _doRejectChanges $scope.reviewPanel.selectedEntryIds.slice()
  350. event_tracking.sendMB "rp-bulk-reject", {
  351. view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini',
  352. nEntries: $scope.reviewPanel.nVisibleSelectedChanges
  353. }
  354. $scope.showBulkAcceptDialog = () ->
  355. showBulkActionsDialog true
  356. $scope.showBulkRejectDialog = () ->
  357. showBulkActionsDialog false
  358. showBulkActionsDialog = (isAccept) ->
  359. $modal.open({
  360. templateUrl: "bulkActionsModalTemplate"
  361. controller: "BulkActionsModalController"
  362. resolve:
  363. isAccept: () -> isAccept
  364. nChanges: () -> $scope.reviewPanel.nVisibleSelectedChanges
  365. scope: $scope.$new()
  366. }).result.then (isAccept) ->
  367. if isAccept
  368. bulkAccept()
  369. else
  370. bulkReject()
  371. $scope.handleTogglerClick = (e) ->
  372. e.target.blur()
  373. $scope.toggleReviewPanel()
  374. $scope.addNewComment = () ->
  375. $scope.$broadcast "comment:start_adding"
  376. $scope.toggleReviewPanel()
  377. $scope.addNewCommentFromKbdShortcut = () ->
  378. if !$scope.project.features.trackChangesVisible
  379. return
  380. $scope.$broadcast "comment:select_line"
  381. if !$scope.ui.reviewPanelOpen
  382. $scope.toggleReviewPanel()
  383. $timeout () ->
  384. $scope.$broadcast "review-panel:layout"
  385. $scope.$broadcast "comment:start_adding"
  386. $scope.startNewComment = () ->
  387. $scope.$broadcast "comment:select_line"
  388. $timeout () ->
  389. $scope.$broadcast "review-panel:layout"
  390. $scope.submitNewComment = (content) ->
  391. return if !content? or content == ""
  392. doc_id = $scope.editor.open_doc_id
  393. entries = getDocEntries(doc_id)
  394. return if !entries["add-comment"]?
  395. {offset, length} = entries["add-comment"]
  396. thread_id = RangesTracker.generateId()
  397. thread = getThread(thread_id)
  398. thread.submitting = true
  399. $scope.$broadcast "comment:add", thread_id, offset, length
  400. $http.post("/project/#{$scope.project_id}/thread/#{thread_id}/messages", {content, _csrf: window.csrfToken})
  401. .catch () ->
  402. ide.showGenericMessageModal("Error submitting comment", "Sorry, there was a problem submitting your comment")
  403. $scope.$broadcast "editor:clearSelection"
  404. $timeout () ->
  405. $scope.$broadcast "review-panel:layout"
  406. event_tracking.sendMB "rp-new-comment", { size: content.length }
  407. $scope.cancelNewComment = (entry) ->
  408. $timeout () ->
  409. $scope.$broadcast "review-panel:layout"
  410. $scope.startReply = (entry) ->
  411. entry.replying = true
  412. $timeout () ->
  413. $scope.$broadcast "review-panel:layout"
  414. $scope.submitReply = (entry, entry_id) ->
  415. thread_id = entry.thread_id
  416. content = entry.replyContent
  417. $http.post("/project/#{$scope.project_id}/thread/#{thread_id}/messages", {content, _csrf: window.csrfToken})
  418. .catch () ->
  419. ide.showGenericMessageModal("Error submitting comment", "Sorry, there was a problem submitting your comment")
  420. trackingMetadata =
  421. view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini'
  422. size: entry.replyContent.length
  423. thread: thread_id
  424. thread = getThread(thread_id)
  425. thread.submitting = true
  426. entry.replyContent = ""
  427. entry.replying = false
  428. $timeout () ->
  429. $scope.$broadcast "review-panel:layout"
  430. event_tracking.sendMB "rp-comment-reply", trackingMetadata
  431. $scope.cancelReply = (entry) ->
  432. entry.replying = false
  433. entry.replyContent = ""
  434. $scope.$broadcast "review-panel:layout"
  435. $scope.resolveComment = (entry, entry_id) ->
  436. entry.focused = false
  437. $http.post "/project/#{$scope.project_id}/thread/#{entry.thread_id}/resolve", {_csrf: window.csrfToken}
  438. _onCommentResolved(entry.thread_id, ide.$scope.user)
  439. event_tracking.sendMB "rp-comment-resolve", { view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini' }
  440. $scope.unresolveComment = (thread_id) ->
  441. _onCommentReopened(thread_id)
  442. $http.post "/project/#{$scope.project_id}/thread/#{thread_id}/reopen", {_csrf: window.csrfToken}
  443. event_tracking.sendMB "rp-comment-reopen"
  444. _onCommentResolved = (thread_id, user) ->
  445. thread = getThread(thread_id)
  446. return if !thread?
  447. thread.resolved = true
  448. thread.resolved_by_user = formatUser(user)
  449. thread.resolved_at = new Date().toISOString()
  450. $scope.reviewPanel.resolvedThreadIds[thread_id] = true
  451. $scope.$broadcast "comment:resolve_threads", [thread_id]
  452. _onCommentReopened = (thread_id) ->
  453. thread = getThread(thread_id)
  454. return if !thread?
  455. delete thread.resolved
  456. delete thread.resolved_by_user
  457. delete thread.resolved_at
  458. delete $scope.reviewPanel.resolvedThreadIds[thread_id]
  459. $scope.$broadcast "comment:unresolve_thread", thread_id
  460. _onThreadDeleted = (thread_id) ->
  461. delete $scope.reviewPanel.resolvedThreadIds[thread_id]
  462. delete $scope.reviewPanel.commentThreads[thread_id]
  463. $scope.$broadcast "comment:remove", thread_id
  464. _onCommentEdited = (thread_id, comment_id, content) ->
  465. thread = getThread(thread_id)
  466. return if !thread?
  467. for message in thread.messages
  468. if message.id == comment_id
  469. message.content = content
  470. updateEntries()
  471. _onCommentDeleted = (thread_id, comment_id) ->
  472. thread = getThread(thread_id)
  473. return if !thread?
  474. thread.messages = thread.messages.filter (m) -> m.id != comment_id
  475. updateEntries()
  476. $scope.deleteThread = (entry_id, doc_id, thread_id) ->
  477. _onThreadDeleted(thread_id)
  478. $http({
  479. method: "DELETE"
  480. url: "/project/#{$scope.project_id}/doc/#{doc_id}/thread/#{thread_id}",
  481. headers: {
  482. 'X-CSRF-Token': window.csrfToken
  483. }
  484. })
  485. event_tracking.sendMB "rp-comment-delete"
  486. $scope.saveEdit = (thread_id, comment) ->
  487. $http.post("/project/#{$scope.project_id}/thread/#{thread_id}/messages/#{comment.id}/edit", {
  488. content: comment.content
  489. _csrf: window.csrfToken
  490. })
  491. $timeout () ->
  492. $scope.$broadcast "review-panel:layout"
  493. $scope.deleteComment = (thread_id, comment) ->
  494. _onCommentDeleted(thread_id, comment.id)
  495. $http({
  496. method: "DELETE"
  497. url: "/project/#{$scope.project_id}/thread/#{thread_id}/messages/#{comment.id}",
  498. headers: {
  499. 'X-CSRF-Token': window.csrfToken
  500. }
  501. })
  502. $timeout () ->
  503. $scope.$broadcast "review-panel:layout"
  504. $scope.setSubView = (subView) ->
  505. $scope.reviewPanel.subView = subView
  506. event_tracking.sendMB "rp-subview-change", { subView }
  507. $scope.gotoEntry = (doc_id, entry) ->
  508. ide.editorManager.openDocId(doc_id, { gotoOffset: entry.offset })
  509. $scope.toggleFullTCStateCollapse = () ->
  510. if $scope.project.features.trackChanges
  511. $scope.reviewPanel.fullTCStateCollapsed = !$scope.reviewPanel.fullTCStateCollapsed
  512. else
  513. $scope.openTrackChangesUpgradeModal()
  514. _setUserTCState = (userId, newValue, isLocal = false) ->
  515. $scope.reviewPanel.trackChangesState[userId] ?= {}
  516. state = $scope.reviewPanel.trackChangesState[userId]
  517. if !state.syncState? or state.syncState == UserTCSyncState.SYNCED
  518. state.value = newValue
  519. state.syncState = UserTCSyncState.SYNCED
  520. else if state.syncState == UserTCSyncState.PENDING and state.value == newValue
  521. state.syncState = UserTCSyncState.SYNCED
  522. else if isLocal
  523. state.value = newValue
  524. state.syncState = UserTCSyncState.PENDING
  525. if userId == ide.$scope.user.id
  526. $scope.editor.wantTrackChanges = newValue
  527. _setEveryoneTCState = (newValue, isLocal = false) ->
  528. $scope.reviewPanel.trackChangesOnForEveryone = newValue
  529. project = $scope.project
  530. for member in project.members
  531. _setUserTCState(member._id, newValue, isLocal)
  532. _setGuestsTCState(newValue, isLocal)
  533. _setUserTCState(project.owner._id, newValue, isLocal)
  534. _setGuestsTCState = (newValue, isLocal = false) ->
  535. if $scope.reviewPanel.trackChangesForGuestsAvailable
  536. $scope.reviewPanel.trackChangesOnForGuests = newValue
  537. if (
  538. ide.$scope.project.publicAccesLevel == 'tokenBased' &&
  539. ide.$scope.isTokenMember &&
  540. ide.$scope?.user?.id?
  541. )
  542. $scope.trackChangesOnForThisGuestClient = newValue
  543. _setUserTCState(ide.$scope.user.id, newValue, isLocal)
  544. else
  545. $scope.reviewPanel.trackChangesOnForGuests = false
  546. if (
  547. $scope.isTokenMember &&
  548. $scope.user?.id? &&
  549. !_.any($scope.project.members, (m) -> m._id == $scope.user.id)
  550. )
  551. $scope.trackChangesOnForThisGuestClient = false
  552. _setUserTCState(ide.$scope.user.id, false, isLocal)
  553. applyClientTrackChangesStateToServer = () ->
  554. data = {}
  555. if $scope.reviewPanel.trackChangesOnForGuests
  556. data.on_for_guests = true
  557. if $scope.reviewPanel.trackChangesOnForEveryone
  558. data.on = true
  559. else
  560. data.on_for = {}
  561. for userId, userState of $scope.reviewPanel.trackChangesState
  562. if !(
  563. $scope.reviewPanel.trackChangesOnForGuests &&
  564. $scope.reviewPanel.trackChangesOnForThisGuestClient
  565. )
  566. data.on_for[userId] = userState.value
  567. data._csrf = window.csrfToken
  568. $http.post "/project/#{$scope.project_id}/track_changes", data
  569. applyTrackChangesStateToClient = (state) ->
  570. if typeof state is "boolean"
  571. _setEveryoneTCState state
  572. _setGuestsTCState state
  573. else
  574. project = $scope.project
  575. $scope.reviewPanel.trackChangesOnForEveryone = false
  576. _setGuestsTCState(state.__guests__ == true)
  577. for member in project.members
  578. _setUserTCState(member._id, state[member._id] ? false)
  579. _setUserTCState($scope.project.owner._id, state[$scope.project.owner._id] ? false)
  580. $scope.toggleTrackChangesForEveryone = (onForEveryone) ->
  581. _setEveryoneTCState onForEveryone, true
  582. _setGuestsTCState onForEveryone, true
  583. applyClientTrackChangesStateToServer()
  584. $scope.toggleTrackChangesForGuests = (onForGuests) ->
  585. _setGuestsTCState onForGuests, true
  586. applyClientTrackChangesStateToServer()
  587. $scope.toggleTrackChangesForUser = (onForUser, userId) ->
  588. _setUserTCState userId, onForUser, true
  589. applyClientTrackChangesStateToServer()
  590. ide.socket.on "toggle-track-changes", (state) ->
  591. $scope.$apply () ->
  592. applyTrackChangesStateToClient(state)
  593. $scope.toggleTrackChangesFromKbdShortcut = () ->
  594. if !($scope.project.features.trackChangesVisible && $scope.project.features.trackChanges)
  595. return
  596. $scope.toggleTrackChangesForUser !$scope.reviewPanel.trackChangesState[ide.$scope.user.id].value, ide.$scope.user.id
  597. _inited = false
  598. ide.$scope.$on "project:joined", () ->
  599. return if _inited
  600. project = ide.$scope.project
  601. if project.features.trackChanges
  602. window.trackChangesState ?= false
  603. applyTrackChangesStateToClient(window.trackChangesState)
  604. else
  605. applyTrackChangesStateToClient(false)
  606. _inited = true
  607. _refreshingRangeUsers = false
  608. _refreshedForUserIds = {}
  609. refreshChangeUsers = (refresh_for_user_id) ->
  610. if refresh_for_user_id?
  611. if _refreshedForUserIds[refresh_for_user_id]?
  612. # We've already tried to refresh to get this user id, so stop it looping
  613. return
  614. _refreshedForUserIds[refresh_for_user_id] = true
  615. # Only do one refresh at once
  616. if _refreshingRangeUsers
  617. return
  618. _refreshingRangeUsers = true
  619. $http.get "/project/#{$scope.project_id}/changes/users"
  620. .then (response) ->
  621. users = response.data
  622. _refreshingRangeUsers = false
  623. $scope.users = {}
  624. # Always include ourself, since if we submit an op, we might need to display info
  625. # about it locally before it has been flushed through the server
  626. if ide.$scope.user?.id?
  627. $scope.users[ide.$scope.user.id] = formatUser(ide.$scope.user)
  628. for user in users
  629. if user.id?
  630. $scope.users[user.id] = formatUser(user)
  631. .catch () ->
  632. _refreshingRangeUsers = false
  633. _threadsLoaded = false
  634. ensureThreadsAreLoaded = () ->
  635. if _threadsLoaded
  636. # We get any updates in real time so only need to load them once.
  637. return
  638. _threadsLoaded = true
  639. $scope.reviewPanel.loadingThreads = true
  640. $http.get "/project/#{$scope.project_id}/threads"
  641. .then (response) ->
  642. threads = response.data
  643. $scope.reviewPanel.loadingThreads = false
  644. for thread_id, _ of $scope.reviewPanel.resolvedThreadIds
  645. delete $scope.reviewPanel.resolvedThreadIds[thread_id]
  646. for thread_id, thread of threads
  647. for comment in thread.messages
  648. formatComment(comment)
  649. if thread.resolved_by_user?
  650. thread.resolved_by_user = formatUser(thread.resolved_by_user)
  651. $scope.reviewPanel.resolvedThreadIds[thread_id] = true
  652. $scope.$broadcast "comment:resolve_threads", [thread_id]
  653. $scope.reviewPanel.commentThreads = threads
  654. $timeout () ->
  655. $scope.$broadcast "review-panel:layout"
  656. formatComment = (comment) ->
  657. comment.user = formatUser(comment.user)
  658. comment.timestamp = new Date(comment.timestamp)
  659. return comment
  660. formatUser = (user) ->
  661. id = user?._id or user?.id
  662. if !id?
  663. return {
  664. email: null
  665. name: "Anonymous"
  666. isSelf: false
  667. hue: ColorManager.ANONYMOUS_HUE
  668. avatar_text: "A"
  669. }
  670. if id == window.user_id
  671. name = "You"
  672. isSelf = true
  673. else
  674. name = [user.first_name, user.last_name].filter((n) -> n? and n != "").join(" ")
  675. if name == ""
  676. name = user.email?.split("@")[0] or "Unknown"
  677. isSelf = false
  678. return {
  679. id: id
  680. email: user.email
  681. name: name
  682. isSelf: isSelf
  683. hue: ColorManager.getHueForUserId(id)
  684. avatar_text: [user.first_name, user.last_name].filter((n) -> n?).map((n) -> n[0]).join ""
  685. }
  686. $scope.openTrackChangesUpgradeModal = () ->
  687. $modal.open {
  688. templateUrl: "trackChangesUpgradeModalTemplate"
  689. controller: "TrackChangesUpgradeModalController"
  690. scope: $scope.$new()
  691. }