|
|
@@ -2,9 +2,9 @@ define [
|
|
|
"base",
|
|
|
"utils/EventEmitter"
|
|
|
"ide/colors/ColorManager"
|
|
|
- "ide/review-panel/ChangesTracker"
|
|
|
-], (App, EventEmitter, ColorManager, ChangesTracker) ->
|
|
|
- App.controller "ReviewPanelController", ($scope, $element, ide, $timeout) ->
|
|
|
+ "ide/review-panel/RangesTracker"
|
|
|
+], (App, EventEmitter, ColorManager, RangesTracker) ->
|
|
|
+ App.controller "ReviewPanelController", ($scope, $element, ide, $timeout, $http, event_tracking) ->
|
|
|
$reviewPanelEl = $element.find "#review-panel"
|
|
|
|
|
|
$scope.SubViews =
|
|
|
@@ -13,131 +13,90 @@ define [
|
|
|
|
|
|
$scope.reviewPanel =
|
|
|
entries: {}
|
|
|
- trackNewChanges: false
|
|
|
+ resolvedComments: {}
|
|
|
hasEntries: false
|
|
|
subView: $scope.SubViews.CUR_FILE
|
|
|
openSubView: $scope.SubViews.CUR_FILE
|
|
|
+ overview:
|
|
|
+ loading: false
|
|
|
+ dropdown:
|
|
|
+ loading: false
|
|
|
+ commentThreads: {}
|
|
|
+ resolvedThreadIds: {}
|
|
|
+ layoutToLeft: false
|
|
|
+ rendererData: {}
|
|
|
+ loadingThreads: false
|
|
|
+
|
|
|
+ $scope.$on "layout:pdf:linked", (event, state) ->
|
|
|
+ $scope.reviewPanel.layoutToLeft = (state.east?.size < 220 || state.east?.initClosed)
|
|
|
+
|
|
|
+ $scope.$on "layout:pdf:resize", (event, state) ->
|
|
|
+ $scope.reviewPanel.layoutToLeft = (state.east?.size < 220 || state.east?.initClosed)
|
|
|
+
|
|
|
+ $scope.$watch "ui.pdfLayout", (layout) ->
|
|
|
+ $scope.reviewPanel.layoutToLeft = (layout == "flat")
|
|
|
|
|
|
$scope.commentState =
|
|
|
adding: false
|
|
|
content: ""
|
|
|
|
|
|
+ $scope.users = {}
|
|
|
+
|
|
|
$scope.reviewPanelEventsBridge = new EventEmitter()
|
|
|
+
|
|
|
+ ide.socket.on "new-comment", (thread_id, comment) ->
|
|
|
+ thread = getThread(thread_id)
|
|
|
+ delete thread.submitting
|
|
|
+ thread.messages.push(formatComment(comment))
|
|
|
+ $scope.$apply()
|
|
|
+ $timeout () ->
|
|
|
+ $scope.$broadcast "review-panel:layout"
|
|
|
+
|
|
|
+ ide.socket.on "accept-change", (doc_id, change_id) ->
|
|
|
+ if doc_id != $scope.editor.open_doc_id
|
|
|
+ getChangeTracker(doc_id).removeChangeId(change_id)
|
|
|
+ else
|
|
|
+ $scope.$broadcast "change:accept", change_id
|
|
|
+ updateEntries(doc_id)
|
|
|
+ $scope.$apply () ->
|
|
|
+
|
|
|
+ ide.socket.on "resolve-thread", (thread_id, user) ->
|
|
|
+ _onCommentResolved(thread_id, user)
|
|
|
+
|
|
|
+ ide.socket.on "reopen-thread", (thread_id) ->
|
|
|
+ _onCommentReopened(thread_id)
|
|
|
|
|
|
- changesTrackers = {}
|
|
|
+ rangesTrackers = {}
|
|
|
|
|
|
getDocEntries = (doc_id) ->
|
|
|
$scope.reviewPanel.entries[doc_id] ?= {}
|
|
|
return $scope.reviewPanel.entries[doc_id]
|
|
|
|
|
|
+ getDocResolvedComments = (doc_id) ->
|
|
|
+ $scope.reviewPanel.resolvedComments[doc_id] ?= {}
|
|
|
+ return $scope.reviewPanel.resolvedComments[doc_id]
|
|
|
+
|
|
|
+ getThread = (thread_id) ->
|
|
|
+ $scope.reviewPanel.commentThreads[thread_id] ?= { messages: [] }
|
|
|
+ return $scope.reviewPanel.commentThreads[thread_id]
|
|
|
+
|
|
|
getChangeTracker = (doc_id) ->
|
|
|
- changesTrackers[doc_id] ?= new ChangesTracker()
|
|
|
- return changesTrackers[doc_id]
|
|
|
-
|
|
|
- # TODO Just for prototyping purposes; remove afterwards.
|
|
|
- mockedUserId = 'mock_user_id_1'
|
|
|
- mockedUserId2 = 'mock_user_id_2'
|
|
|
-
|
|
|
- if window.location.search.match /mocktc=true/
|
|
|
- mock_changes = {
|
|
|
- "main.tex":
|
|
|
- changes: [{
|
|
|
- op: { i: "Habitat loss and conflicts with humans are the greatest causes of concern.", p: 925 - 38 }
|
|
|
- metadata: { user_id: mockedUserId, ts: new Date(Date.now() - 30 * 60 * 1000) }
|
|
|
- }, {
|
|
|
- op: { d: "The lion is now a vulnerable species. ", p: 778 }
|
|
|
- metadata: { user_id: mockedUserId, ts: new Date(Date.now() - 31 * 60 * 1000) }
|
|
|
- }]
|
|
|
- comments: [{
|
|
|
- offset: 1375 - 38
|
|
|
- length: 79
|
|
|
- metadata:
|
|
|
- thread: [{
|
|
|
- content: "Do we have a source for this?"
|
|
|
- user_id: mockedUserId
|
|
|
- ts: new Date(Date.now() - 45 * 60 * 1000)
|
|
|
- }]
|
|
|
- }]
|
|
|
- "chapter_1.tex":
|
|
|
- changes: [{
|
|
|
- "op":{"p":740,"d":", to take down large animals"},
|
|
|
- "metadata":{"user_id":mockedUserId, ts: new Date(Date.now() - 15 * 60 * 1000)}
|
|
|
- }, {
|
|
|
- "op":{"i":", to keep hold of the prey","p":920},
|
|
|
- "metadata":{"user_id":mockedUserId, ts: new Date(Date.now() - 130 * 60 * 1000)}
|
|
|
- }, {
|
|
|
- "op":{"i":" being","p":1057},
|
|
|
- "metadata":{"user_id":mockedUserId2, ts: new Date(Date.now() - 72 * 60 * 1000)}
|
|
|
- }]
|
|
|
- comments:[{
|
|
|
- "offset":111,"length":5,
|
|
|
- "metadata":{
|
|
|
- "thread": [
|
|
|
- {"content":"Have we used 'pride' too much here?","user_id":mockedUserId, ts: new Date(Date.now() - 12 * 60 * 1000)},
|
|
|
- {"content":"No, I think this is OK","user_id":mockedUserId2, ts: new Date(Date.now() - 9 * 60 * 1000)}
|
|
|
- ]
|
|
|
- }
|
|
|
- },{
|
|
|
- "offset":452,"length":21,
|
|
|
- "metadata":{
|
|
|
- "thread":[
|
|
|
- {"content":"TODO: Don't use as many parentheses!","user_id":mockedUserId2, ts: new Date(Date.now() - 99 * 60 * 1000)}
|
|
|
- ]
|
|
|
- }
|
|
|
- }]
|
|
|
- "chapter_2.tex":
|
|
|
- changes: [{
|
|
|
- "op":{"p":458,"d":"other"},
|
|
|
- "metadata":{"user_id":mockedUserId, ts: new Date(Date.now() - 133 * 60 * 1000)}
|
|
|
- },{
|
|
|
- "op":{"i":"usually 2-3, ","p":928},
|
|
|
- "metadata":{"user_id":mockedUserId, ts: new Date(Date.now() - 27 * 60 * 1000)}
|
|
|
- },{
|
|
|
- "op":{"i":"If the parents are a male lion and a female tiger, it is called a liger. A tigon comes from a male tiger and a female lion.","p":1126},
|
|
|
- "metadata":{"user_id":mockedUserId, ts: new Date(Date.now() - 152 * 60 * 1000)}
|
|
|
- }]
|
|
|
- comments: [{
|
|
|
- "offset":299,"length":10,
|
|
|
- "metadata":{
|
|
|
- "thread":[{
|
|
|
- "content":"Should we use a different word here if 'den' needs clarifying?","user_id":mockedUserId,"ts": new Date(Date.now() - 430 * 60 * 1000)
|
|
|
- }]
|
|
|
- }
|
|
|
- },{
|
|
|
- "offset":843,"length":66,
|
|
|
- "metadata":{
|
|
|
- "thread":[{
|
|
|
- "content":"This sentence is a little ambiguous","user_id":mockedUserId,"ts": new Date(Date.now() - 430 * 60 * 1000)
|
|
|
- }]
|
|
|
- }
|
|
|
- }]
|
|
|
- }
|
|
|
- ide.$scope.$on "file-tree:initialized", () ->
|
|
|
- ide.fileTreeManager.forEachEntity (entity) ->
|
|
|
- if mock_changes[entity.name]?
|
|
|
- changesTracker = getChangeTracker(entity.id)
|
|
|
- for change in mock_changes[entity.name].changes
|
|
|
- changesTracker._addOp change.op, change.metadata
|
|
|
- for comment in mock_changes[entity.name].comments
|
|
|
- changesTracker.addComment comment.offset, comment.length, comment.metadata
|
|
|
- for doc_id, changesTracker of changesTrackers
|
|
|
- updateEntries(doc_id)
|
|
|
+ if !rangesTrackers[doc_id]?
|
|
|
+ rangesTrackers[doc_id] = new RangesTracker()
|
|
|
+ rangesTrackers[doc_id].resolvedThreadIds = $scope.reviewPanel.resolvedThreadIds
|
|
|
+ return rangesTrackers[doc_id]
|
|
|
|
|
|
scrollbar = {}
|
|
|
$scope.reviewPanelEventsBridge.on "aceScrollbarVisibilityChanged", (isVisible, scrollbarWidth) ->
|
|
|
scrollbar = {isVisible, scrollbarWidth}
|
|
|
updateScrollbar()
|
|
|
-
|
|
|
+
|
|
|
updateScrollbar = () ->
|
|
|
if scrollbar.isVisible and $scope.reviewPanel.subView == $scope.SubViews.CUR_FILE
|
|
|
$reviewPanelEl.css "right", "#{ scrollbar.scrollbarWidth }px"
|
|
|
else
|
|
|
$reviewPanelEl.css "right", "0"
|
|
|
-
|
|
|
- $scope.$watch "reviewPanel.subView", (subView) ->
|
|
|
- return if !subView?
|
|
|
- updateScrollbar()
|
|
|
-
|
|
|
+
|
|
|
$scope.$watch "ui.reviewPanelOpen", (open) ->
|
|
|
return if !open?
|
|
|
if !open
|
|
|
@@ -147,33 +106,88 @@ define [
|
|
|
else
|
|
|
# Reset back to what we had when previously open
|
|
|
$scope.reviewPanel.subView = $scope.reviewPanel.openSubView
|
|
|
+
|
|
|
+ $scope.$watch "reviewPanel.subView", (view) ->
|
|
|
+ return if !view?
|
|
|
+ updateScrollbar()
|
|
|
+ if view == $scope.SubViews.OVERVIEW
|
|
|
+ refreshOverviewPanel()
|
|
|
|
|
|
- $scope.$watch "editor.open_doc_id", (open_doc_id) ->
|
|
|
- return if !open_doc_id?
|
|
|
- changesTrackers[open_doc_id] ?= new ChangesTracker()
|
|
|
- $scope.reviewPanel.changesTracker = changesTrackers[open_doc_id]
|
|
|
+ $scope.$watch "editor.sharejs_doc", (doc, old_doc) ->
|
|
|
+ return if !doc?
|
|
|
+ # The open doc range tracker is kept up to date in real-time so
|
|
|
+ # replace any outdated info with this
|
|
|
+ rangesTrackers[doc.doc_id] = doc.ranges
|
|
|
+ rangesTrackers[doc.doc_id].resolvedThreadIds = $scope.reviewPanel.resolvedThreadIds
|
|
|
+ $scope.reviewPanel.rangesTracker = rangesTrackers[doc.doc_id]
|
|
|
+ if old_doc?
|
|
|
+ old_doc.off "flipped_pending_to_inflight"
|
|
|
+ doc.on "flipped_pending_to_inflight", () ->
|
|
|
+ regenerateTrackChangesId(doc)
|
|
|
+ regenerateTrackChangesId(doc)
|
|
|
|
|
|
$scope.$watch (() ->
|
|
|
entries = $scope.reviewPanel.entries[$scope.editor.open_doc_id] or {}
|
|
|
Object.keys(entries).length
|
|
|
), (nEntries) ->
|
|
|
- $scope.reviewPanel.hasEntries = nEntries > 0 and $scope.trackChangesFeatureFlag
|
|
|
+ $scope.reviewPanel.hasEntries = nEntries > 0 and $scope.project.features.trackChanges
|
|
|
|
|
|
$scope.$watch "ui.reviewPanelOpen", (reviewPanelOpen) ->
|
|
|
return if !reviewPanelOpen?
|
|
|
$timeout () ->
|
|
|
$scope.$broadcast "review-panel:toggle"
|
|
|
$scope.$broadcast "review-panel:layout"
|
|
|
+
|
|
|
+ regenerateTrackChangesId = (doc) ->
|
|
|
+ old_id = getChangeTracker(doc.doc_id).getIdSeed()
|
|
|
+ new_id = RangesTracker.generateIdSeed()
|
|
|
+ getChangeTracker(doc.doc_id).setIdSeed(new_id)
|
|
|
+ doc.setTrackChangesIdSeeds({pending: new_id, inflight: old_id})
|
|
|
|
|
|
+ refreshRanges = () ->
|
|
|
+ $http.get "/project/#{$scope.project_id}/ranges"
|
|
|
+ .success (docs) ->
|
|
|
+ for doc in docs
|
|
|
+ if doc.id != $scope.editor.open_doc_id # this is kept up to date in real-time, don't overwrite
|
|
|
+ rangesTracker = getChangeTracker(doc.id)
|
|
|
+ rangesTracker.comments = doc.ranges?.comments or []
|
|
|
+ rangesTracker.changes = doc.ranges?.changes or []
|
|
|
+ updateEntries(doc.id)
|
|
|
+
|
|
|
+ refreshOverviewPanel = () ->
|
|
|
+ $scope.reviewPanel.overview.loading = true
|
|
|
+ refreshRanges()
|
|
|
+ .then () ->
|
|
|
+ $scope.reviewPanel.overview.loading = false
|
|
|
+ .catch () ->
|
|
|
+ $scope.reviewPanel.overview.loading = false
|
|
|
+
|
|
|
+ $scope.refreshResolvedCommentsDropdown = () ->
|
|
|
+ $scope.reviewPanel.dropdown.loading = true
|
|
|
+ q = refreshRanges()
|
|
|
+ q.then () ->
|
|
|
+ $scope.reviewPanel.dropdown.loading = false
|
|
|
+ q.catch () ->
|
|
|
+ $scope.reviewPanel.dropdown.loading = false
|
|
|
+ return q
|
|
|
+
|
|
|
updateEntries = (doc_id) ->
|
|
|
- changesTracker = getChangeTracker(doc_id)
|
|
|
+ rangesTracker = getChangeTracker(doc_id)
|
|
|
entries = getDocEntries(doc_id)
|
|
|
+ resolvedComments = getDocResolvedComments(doc_id)
|
|
|
|
|
|
+ changed = false
|
|
|
+
|
|
|
# Assume we'll delete everything until we see it, then we'll remove it from this object
|
|
|
delete_changes = {}
|
|
|
- delete_changes[change_id] = true for change_id, change of entries
|
|
|
+ for change_id, change of entries
|
|
|
+ if change_id != "add-comment"
|
|
|
+ delete_changes[change_id] = true
|
|
|
+ for change_id, change of resolvedComments
|
|
|
+ delete_changes[change_id] = true
|
|
|
|
|
|
- for change in changesTracker.changes
|
|
|
+ for change in rangesTracker.changes
|
|
|
+ changed = true
|
|
|
delete delete_changes[change.id]
|
|
|
entries[change.id] ?= {}
|
|
|
|
|
|
@@ -189,22 +203,35 @@ define [
|
|
|
for key, value of new_entry
|
|
|
entries[change.id][key] = value
|
|
|
|
|
|
- for comment in changesTracker.comments
|
|
|
+ if !$scope.users[change.metadata.user_id]?
|
|
|
+ refreshChangeUsers(change.metadata.user_id)
|
|
|
+
|
|
|
+ if rangesTracker.comments.length > 0
|
|
|
+ ensureThreadsAreLoaded()
|
|
|
+
|
|
|
+ for comment in rangesTracker.comments
|
|
|
+ changed = true
|
|
|
delete delete_changes[comment.id]
|
|
|
- entries[comment.id] ?= {}
|
|
|
+ if $scope.reviewPanel.resolvedThreadIds[comment.op.t]
|
|
|
+ new_comment = resolvedComments[comment.id] ?= {}
|
|
|
+ else
|
|
|
+ new_comment = entries[comment.id] ?= {}
|
|
|
new_entry = {
|
|
|
type: "comment"
|
|
|
- thread: comment.metadata.thread
|
|
|
- resolved: comment.metadata.resolved
|
|
|
- resolved_data: comment.metadata.resolved_data
|
|
|
- offset: comment.offset
|
|
|
- length: comment.length
|
|
|
+ thread_id: comment.op.t
|
|
|
+ content: comment.op.c
|
|
|
+ offset: comment.op.p
|
|
|
}
|
|
|
for key, value of new_entry
|
|
|
- entries[comment.id][key] = value
|
|
|
+ new_comment[key] = value
|
|
|
|
|
|
for change_id, _ of delete_changes
|
|
|
+ changed = true
|
|
|
delete entries[change_id]
|
|
|
+ delete resolvedComments[change_id]
|
|
|
+
|
|
|
+ if changed
|
|
|
+ $scope.$broadcast "entries:changed"
|
|
|
|
|
|
$scope.$on "editor:track-changes:changed", () ->
|
|
|
doc_id = $scope.editor.open_doc_id
|
|
|
@@ -212,53 +239,64 @@ define [
|
|
|
$scope.$broadcast "review-panel:recalculate-screen-positions"
|
|
|
$scope.$broadcast "review-panel:layout"
|
|
|
|
|
|
- $scope.$on "editor:focus:changed", (e, cursor_offset, selection) ->
|
|
|
+ $scope.$on "editor:focus:changed", (e, selection_offset_start, selection_offset_end, selection) ->
|
|
|
doc_id = $scope.editor.open_doc_id
|
|
|
entries = getDocEntries(doc_id)
|
|
|
|
|
|
- if !selection
|
|
|
- delete entries["add-comment"]
|
|
|
- else
|
|
|
- entries["add-comment"] = {
|
|
|
- type: "add-comment"
|
|
|
- offset: cursor_offset
|
|
|
- }
|
|
|
+ delete entries["add-comment"]
|
|
|
+ if selection
|
|
|
+ # Only show add comment if we're not already overlapping one
|
|
|
+ overlapping_comment = false
|
|
|
+ for id, entry of entries
|
|
|
+ if entry.type == "comment" and not $scope.reviewPanel.resolvedThreadIds[entry.thread_id]
|
|
|
+ unless entry.offset >= selection_offset_end or entry.offset + entry.content.length <= selection_offset_start
|
|
|
+ overlapping_comment = true
|
|
|
+ if !overlapping_comment
|
|
|
+ entries["add-comment"] = {
|
|
|
+ type: "add-comment"
|
|
|
+ offset: selection_offset_start
|
|
|
+ }
|
|
|
|
|
|
for id, entry of entries
|
|
|
- if entry.type == "comment" and not entry.resolved
|
|
|
- entry.focused = (entry.offset <= cursor_offset <= entry.offset + entry.length)
|
|
|
+ if entry.type == "comment" and not $scope.reviewPanel.resolvedThreadIds[entry.thread_id]
|
|
|
+ entry.focused = (entry.offset <= selection_offset_start <= entry.offset + entry.content.length)
|
|
|
else if entry.type == "insert"
|
|
|
- entry.focused = (entry.offset <= cursor_offset <= entry.offset + entry.content.length)
|
|
|
+ entry.focused = (entry.offset <= selection_offset_start <= entry.offset + entry.content.length)
|
|
|
else if entry.type == "delete"
|
|
|
- entry.focused = (entry.offset == cursor_offset)
|
|
|
+ entry.focused = (entry.offset == selection_offset_start)
|
|
|
else if entry.type == "add-comment" and selection
|
|
|
entry.focused = true
|
|
|
|
|
|
$scope.$broadcast "review-panel:recalculate-screen-positions"
|
|
|
$scope.$broadcast "review-panel:layout"
|
|
|
-
|
|
|
+
|
|
|
$scope.acceptChange = (entry_id) ->
|
|
|
+ $http.post "/project/#{$scope.project_id}/doc/#{$scope.editor.open_doc_id}/changes/#{entry_id}/accept", {_csrf: window.csrfToken}
|
|
|
$scope.$broadcast "change:accept", entry_id
|
|
|
+ event_tracking.sendMB "rp-change-accepted", { view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini' }
|
|
|
|
|
|
$scope.rejectChange = (entry_id) ->
|
|
|
$scope.$broadcast "change:reject", entry_id
|
|
|
+ event_tracking.sendMB "rp-change-rejected", { view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini' }
|
|
|
|
|
|
$scope.startNewComment = () ->
|
|
|
- # $scope.commentState.adding = true
|
|
|
$scope.$broadcast "comment:select_line"
|
|
|
$timeout () ->
|
|
|
$scope.$broadcast "review-panel:layout"
|
|
|
|
|
|
$scope.submitNewComment = (content) ->
|
|
|
- # $scope.commentState.adding = false
|
|
|
- $scope.$broadcast "comment:add", content
|
|
|
- # $scope.commentState.content = ""
|
|
|
+ thread_id = RangesTracker.generateId()
|
|
|
+ thread = getThread(thread_id)
|
|
|
+ thread.submitting = true
|
|
|
+ $scope.$broadcast "comment:add", thread_id
|
|
|
+ $http.post("/project/#{$scope.project_id}/thread/#{thread_id}/messages", {content, _csrf: window.csrfToken})
|
|
|
+ .error (error) ->
|
|
|
+ ide.showGenericMessageModal("Error submitting comment", "Sorry, there was a problem submitting your comment")
|
|
|
$timeout () ->
|
|
|
$scope.$broadcast "review-panel:layout"
|
|
|
-
|
|
|
+ event_tracking.sendMB "rp-new-comment", { size: content.length }
|
|
|
+
|
|
|
$scope.cancelNewComment = (entry) ->
|
|
|
- # $scope.commentState.adding = false
|
|
|
- # $scope.commentState.content = ""
|
|
|
$timeout () ->
|
|
|
$scope.$broadcast "review-panel:layout"
|
|
|
|
|
|
@@ -267,117 +305,165 @@ define [
|
|
|
$timeout () ->
|
|
|
$scope.$broadcast "review-panel:layout"
|
|
|
|
|
|
- # $scope.handleCommentReplyKeyPress = (ev, entry) ->
|
|
|
- # if ev.keyCode == 13 and !ev.shiftKey and !ev.ctrlKey and !ev.metaKey
|
|
|
- # ev.preventDefault()
|
|
|
- # ev.target.blur()
|
|
|
- # $scope.submitReply(entry)
|
|
|
-
|
|
|
- $scope.submitReply = (entry, entry_id) ->
|
|
|
- $scope.unresolveComment(entry_id)
|
|
|
- entry.thread.push {
|
|
|
- content: entry.replyContent
|
|
|
- ts: new Date()
|
|
|
- user_id: window.user_id
|
|
|
- }
|
|
|
- entry.replyContent = ""
|
|
|
- entry.replying = false
|
|
|
- $timeout () ->
|
|
|
- $scope.$broadcast "review-panel:layout"
|
|
|
- # TODO Just for prototyping purposes; remove afterwards
|
|
|
- window.setTimeout((() ->
|
|
|
- $scope.$applyAsync(() -> submitMockedReply(entry))
|
|
|
- ), 1000 * 2)
|
|
|
-
|
|
|
- # TODO Just for prototyping purposes; remove afterwards.
|
|
|
- submitMockedReply = (entry) ->
|
|
|
- entry.thread.push {
|
|
|
- content: 'Sounds good!'
|
|
|
- ts: new Date()
|
|
|
- user_id: mockedUserId
|
|
|
- }
|
|
|
+ $scope.submitReply = (entry, entry_id) ->
|
|
|
+ thread_id = entry.thread_id
|
|
|
+ content = entry.replyContent
|
|
|
+ $http.post("/project/#{$scope.project_id}/thread/#{thread_id}/messages", {content, _csrf: window.csrfToken})
|
|
|
+ .error (error) ->
|
|
|
+ ide.showGenericMessageModal("Error submitting comment", "Sorry, there was a problem submitting your comment")
|
|
|
+
|
|
|
+ trackingMetadata =
|
|
|
+ view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini'
|
|
|
+ size: entry.replyContent.length
|
|
|
+ thread: thread_id
|
|
|
+
|
|
|
+ thread = getThread(thread_id)
|
|
|
+ thread.submitting = true
|
|
|
entry.replyContent = ""
|
|
|
entry.replying = false
|
|
|
$timeout () ->
|
|
|
$scope.$broadcast "review-panel:layout"
|
|
|
-
|
|
|
+ event_tracking.sendMB "rp-comment-reply", trackingMetadata
|
|
|
+
|
|
|
$scope.cancelReply = (entry) ->
|
|
|
entry.replying = false
|
|
|
entry.replyContent = ""
|
|
|
$scope.$broadcast "review-panel:layout"
|
|
|
|
|
|
$scope.resolveComment = (entry, entry_id) ->
|
|
|
- entry.showWhenResolved = false
|
|
|
entry.focused = false
|
|
|
- $scope.$broadcast "comment:resolve", entry_id, window.user_id
|
|
|
+ $http.post "/project/#{$scope.project_id}/thread/#{entry.thread_id}/resolve", {_csrf: window.csrfToken}
|
|
|
+ _onCommentResolved(entry.thread_id, ide.$scope.user)
|
|
|
+ event_tracking.sendMB "rp-comment-resolve", { view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini' }
|
|
|
+
|
|
|
+ $scope.unresolveComment = (thread_id) ->
|
|
|
+ _onCommentReopened(thread_id)
|
|
|
+ $http.post "/project/#{$scope.project_id}/thread/#{thread_id}/reopen", {_csrf: window.csrfToken}
|
|
|
+ event_tracking.sendMB "rp-comment-reopen"
|
|
|
|
|
|
- $scope.unresolveComment = (entry_id) ->
|
|
|
- $scope.$broadcast "comment:unresolve", entry_id
|
|
|
+ _onCommentResolved = (thread_id, user) ->
|
|
|
+ thread = $scope.reviewPanel.commentThreads[thread_id]
|
|
|
+ thread.resolved = true
|
|
|
+ thread.resolved_by_user = formatUser(user)
|
|
|
+ thread.resolved_at = new Date()
|
|
|
+ $scope.reviewPanel.resolvedThreadIds[thread_id] = true
|
|
|
+ $scope.$broadcast "comment:resolve_thread", thread_id
|
|
|
|
|
|
- $scope.deleteComment = (entry_id) ->
|
|
|
- $scope.$broadcast "comment:remove", entry_id
|
|
|
-
|
|
|
- $scope.showThread = (entry) ->
|
|
|
- entry.showWhenResolved = true
|
|
|
- $timeout () ->
|
|
|
- $scope.$broadcast "review-panel:layout"
|
|
|
+ _onCommentReopened = (thread_id) ->
|
|
|
+ thread = $scope.reviewPanel.commentThreads[thread_id]
|
|
|
+ delete thread.resolved
|
|
|
+ delete thread.resolved_by_user
|
|
|
+ delete thread.resolved_at
|
|
|
+ delete $scope.reviewPanel.resolvedThreadIds[thread_id]
|
|
|
+ $scope.$broadcast "comment:unresolve_thread", thread_id
|
|
|
|
|
|
- $scope.hideThread = (entry) ->
|
|
|
- entry.showWhenResolved = false
|
|
|
- $timeout () ->
|
|
|
- $scope.$broadcast "review-panel:layout"
|
|
|
+ _onCommentDeleted = (thread_id) ->
|
|
|
+ if $scope.reviewPanel.resolvedThreadIds[thread_id]?
|
|
|
+ delete $scope.reviewPanel.resolvedThreadIds[thread_id]
|
|
|
+
|
|
|
+ delete $scope.reviewPanel.commentThreads[thread_id]
|
|
|
+
|
|
|
+ $scope.deleteComment = (entry_id, thread_id) ->
|
|
|
+ _onCommentDeleted(thread_id)
|
|
|
+ $scope.$broadcast "comment:remove", entry_id
|
|
|
+ event_tracking.sendMB "rp-comment-delete"
|
|
|
|
|
|
$scope.setSubView = (subView) ->
|
|
|
$scope.reviewPanel.subView = subView
|
|
|
+ event_tracking.sendMB "rp-subview-change", { subView }
|
|
|
|
|
|
$scope.gotoEntry = (doc_id, entry) ->
|
|
|
ide.editorManager.openDocId(doc_id, { gotoOffset: entry.offset })
|
|
|
+
|
|
|
+ $scope.toggleTrackChanges = (value) ->
|
|
|
+ $scope.editor.wantTrackChanges = value
|
|
|
+ $http.post "/project/#{$scope.project_id}/track_changes", {_csrf: window.csrfToken, on: value}
|
|
|
+ event_tracking.sendMB "rp-trackchanges-toggle", { value }
|
|
|
+
|
|
|
+ ide.socket.on "toggle-track-changes", (value) ->
|
|
|
+ $scope.$apply () ->
|
|
|
+ $scope.editor.wantTrackChanges = value
|
|
|
|
|
|
- DOC_ID_NAMES = {}
|
|
|
- $scope.getFileName = (doc_id) ->
|
|
|
- # This is called a lot and is relatively expensive, so cache the result
|
|
|
- if !DOC_ID_NAMES[doc_id]?
|
|
|
- entity = ide.fileTreeManager.findEntityById(doc_id)
|
|
|
- return if !entity?
|
|
|
- DOC_ID_NAMES[doc_id] = ide.fileTreeManager.getEntityPath(entity)
|
|
|
- return DOC_ID_NAMES[doc_id]
|
|
|
-
|
|
|
- # TODO: Eventually we need to get this from the server, and update it
|
|
|
- # when we get an id we don't know. This'll do for client side testing
|
|
|
- refreshUsers = () ->
|
|
|
- $scope.users = {}
|
|
|
- # TODO Just for prototyping purposes; remove afterwards.
|
|
|
- $scope.users[mockedUserId] = {
|
|
|
- email: "paulo@sharelatex.com"
|
|
|
- name: "Paulo Reis"
|
|
|
- isSelf: false
|
|
|
- hue: 70
|
|
|
- avatar_text: "PR"
|
|
|
- }
|
|
|
- $scope.users[mockedUserId2] = {
|
|
|
- email: "james@sharelatex.com"
|
|
|
- name: "James Allen"
|
|
|
- isSelf: false
|
|
|
- hue: 320
|
|
|
- avatar_text: "JA"
|
|
|
- }
|
|
|
+ _refreshingRangeUsers = false
|
|
|
+ _refreshedForUserIds = {}
|
|
|
+ refreshChangeUsers = (refresh_for_user_id) ->
|
|
|
+ if refresh_for_user_id?
|
|
|
+ if _refreshedForUserIds[refresh_for_user_id]?
|
|
|
+ # We've already tried to refresh to get this user id, so stop it looping
|
|
|
+ return
|
|
|
+ _refreshedForUserIds[refresh_for_user_id] = true
|
|
|
|
|
|
- for member in $scope.project.members.concat($scope.project.owner)
|
|
|
- if member._id == window.user_id
|
|
|
- name = "You"
|
|
|
- isSelf = true
|
|
|
- else
|
|
|
- name = "#{member.first_name} #{member.last_name}"
|
|
|
- isSelf = false
|
|
|
-
|
|
|
- $scope.users[member._id] = {
|
|
|
- email: member.email
|
|
|
- name: name
|
|
|
- isSelf: isSelf
|
|
|
- hue: ColorManager.getHueForUserId(member._id)
|
|
|
- avatar_text: [member.first_name, member.last_name].filter((n) -> n?).map((n) -> n[0]).join ""
|
|
|
+ # Only do one refresh at once
|
|
|
+ if _refreshingRangeUsers
|
|
|
+ return
|
|
|
+ _refreshingRangeUsers = true
|
|
|
+
|
|
|
+ $http.get "/project/#{$scope.project_id}/changes/users"
|
|
|
+ .success (users) ->
|
|
|
+ _refreshingRangeUsers = false
|
|
|
+ $scope.users = {}
|
|
|
+ # Always include ourself, since if we submit an op, we might need to display info
|
|
|
+ # about it locally before it has been flushed through the server
|
|
|
+ if ide.$scope.user?.id?
|
|
|
+ $scope.users[ide.$scope.user.id] = formatUser(ide.$scope.user)
|
|
|
+ for user in users
|
|
|
+ if user.id?
|
|
|
+ $scope.users[user.id] = formatUser(user)
|
|
|
+ .error () ->
|
|
|
+ _refreshingRangeUsers = false
|
|
|
+
|
|
|
+ _threadsLoaded = false
|
|
|
+ ensureThreadsAreLoaded = () ->
|
|
|
+ if _threadsLoaded
|
|
|
+ # We get any updates in real time so only need to load them once.
|
|
|
+ return
|
|
|
+ _threadsLoaded = true
|
|
|
+ $scope.reviewPanel.loadingThreads = true
|
|
|
+ $http.get "/project/#{$scope.project_id}/threads"
|
|
|
+ .success (threads) ->
|
|
|
+ $scope.reviewPanel.loadingThreads = false
|
|
|
+ for thread_id, _ of $scope.reviewPanel.resolvedThreadIds
|
|
|
+ delete $scope.reviewPanel.resolvedThreadIds[thread_id]
|
|
|
+ for thread_id, thread of threads
|
|
|
+ for comment in thread.messages
|
|
|
+ formatComment(comment)
|
|
|
+ if thread.resolved_by_user?
|
|
|
+ $scope.$broadcast "comment:resolve_thread", thread_id
|
|
|
+ thread.resolved_by_user = formatUser(thread.resolved_by_user)
|
|
|
+ $scope.reviewPanel.resolvedThreadIds[thread_id] = true
|
|
|
+ $scope.reviewPanel.commentThreads = threads
|
|
|
+ $timeout () ->
|
|
|
+ $scope.$broadcast "review-panel:layout"
|
|
|
+
|
|
|
+ formatComment = (comment) ->
|
|
|
+ comment.user = formatUser(comment.user)
|
|
|
+ comment.timestamp = new Date(comment.timestamp)
|
|
|
+ return comment
|
|
|
+
|
|
|
+ formatUser = (user) ->
|
|
|
+ id = user?._id or user?.id
|
|
|
+
|
|
|
+ if !id?
|
|
|
+ return {
|
|
|
+ email: null
|
|
|
+ name: "Anonymous"
|
|
|
+ isSelf: false
|
|
|
+ hue: ColorManager.ANONYMOUS_HUE
|
|
|
+ avatar_text: "A"
|
|
|
}
|
|
|
-
|
|
|
- $scope.$watch "project.members", (members) ->
|
|
|
- return if !members?
|
|
|
- refreshUsers()
|
|
|
+ if id == window.user_id
|
|
|
+ name = "You"
|
|
|
+ isSelf = true
|
|
|
+ else
|
|
|
+ name = [user.first_name, user.last_name].filter((n) -> n? and n != "").join(" ")
|
|
|
+ if name == ""
|
|
|
+ name = user.email?.split("@")[0] or "Unknown"
|
|
|
+ isSelf = false
|
|
|
+ return {
|
|
|
+ id: id
|
|
|
+ email: user.email
|
|
|
+ name: name
|
|
|
+ isSelf: isSelf
|
|
|
+ hue: ColorManager.getHueForUserId(id)
|
|
|
+ avatar_text: [user.first_name, user.last_name].filter((n) -> n?).map((n) -> n[0]).join ""
|
|
|
+ }
|