ReviewPanelController.coffee 23 KB

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