Send and get comments via the chat api

This commit is contained in:
James Allen 2016-12-16 16:42:41 +00:00
parent 5717cafcec
commit 988005e929
15 changed files with 380 additions and 399 deletions

View file

@ -0,0 +1,48 @@
request = require("request")
settings = require("settings-sharelatex")
logger = require("logger-sharelatex")
module.exports = ChatApiHandler =
_apiRequest: (opts, callback = (error, data) ->) ->
request opts, (error, response, data) ->
return callback(error) if error?
if 200 <= response.statusCode < 300
return callback null, data
else
error = new Error("chat api returned non-success code: #{response.statusCode}")
error.statusCode = response.statusCode
logger.error {err: error, opts}, "error sending request to chat api"
return callback error
sendGlobalMessage: (project_id, user_id, content, callback)->
ChatApiHandler._apiRequest {
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/messages"
method: "POST"
json: {user_id, content}
}, callback
getGlobalMessages: (project_id, limit, before, callback)->
qs = {}
qs.limit = limit if limit?
qs.before = before if before?
ChatApiHandler._apiRequest {
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/messages"
method: "GET"
qs: qs
json: true
}, callback
sendComment: (project_id, thread_id, user_id, content, callback = (error) ->) ->
ChatApiHandler._apiRequest {
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/messages"
method: "POST"
json: {user_id, content}
}, callback
getThreads: (project_id, callback = (error) ->) ->
ChatApiHandler._apiRequest {
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/threads"
method: "GET"
json: true
}, callback

View file

@ -1,33 +1,26 @@
ChatHandler = require("./ChatHandler") ChatApiHandler = require("./ChatApiHandler")
EditorRealTimeController = require("../Editor/EditorRealTimeController") EditorRealTimeController = require("../Editor/EditorRealTimeController")
logger = require("logger-sharelatex") logger = require("logger-sharelatex")
AuthenticationController = require('../Authentication/AuthenticationController') AuthenticationController = require('../Authentication/AuthenticationController')
module.exports = module.exports =
sendMessage: (req, res, next)-> sendMessage: (req, res, next)->
project_id = req.params.Project_id project_id = req.params.project_id
messageContent = req.body.content content = req.body.content
user_id = AuthenticationController.getLoggedInUserId(req) user_id = AuthenticationController.getLoggedInUserId(req)
if !user_id? if !user_id?
err = new Error('no logged-in user') err = new Error('no logged-in user')
return next(err) return next(err)
ChatHandler.sendMessage project_id, user_id, messageContent, (err, builtMessge)-> ChatApiHandler.sendGlobalMessage project_id, user_id, content, (err, message) ->
if err? return next(err) if err?
logger.err err:err, project_id:project_id, user_id:user_id, messageContent:messageContent, "problem sending message to chat api" EditorRealTimeController.emitToRoom project_id, "new-chat-message", message, (err)->
return res.sendStatus(500) res.send(204)
EditorRealTimeController.emitToRoom project_id, "new-chat-message", builtMessge, (err)->
res.send()
getMessages: (req, res)-> getMessages: (req, res, next)->
project_id = req.params.Project_id project_id = req.params.project_id
query = req.query query = req.query
logger.log project_id:project_id, query:query, "getting messages" logger.log project_id:project_id, query:query, "getting messages"
ChatHandler.getMessages project_id, query, (err, messages)-> ChatApiHandler.getGlobalMessages project_id, query.limit, query.before, (err, messages) ->
if err? return next(err) if err?
logger.err err:err, query:query, "problem getting messages from chat api" logger.log length: messages?.length, "sending messages to client"
return res.sendStatus 500 res.json messages
logger.log length:messages?.length, "sending messages to client"
res.set 'Content-Type', 'application/json'
res.send messages

View file

@ -1,32 +0,0 @@
request = require("request")
settings = require("settings-sharelatex")
logger = require("logger-sharelatex")
module.exports =
sendMessage: (project_id, user_id, messageContent, callback)->
opts =
method:"post"
json:
content:messageContent
user_id:user_id
uri:"#{settings.apis.chat.internal_url}/room/#{project_id}/messages"
request opts, (err, response, body)->
if err?
logger.err err:err, "problem sending new message to chat"
callback(err, body)
getMessages: (project_id, query, callback)->
qs = {}
qs.limit = query.limit if query?.limit?
qs.before = query.before if query?.before?
opts =
uri:"#{settings.apis.chat.internal_url}/room/#{project_id}/messages"
method:"get"
qs: qs
request opts, (err, response, body)->
callback(err, body)

View file

@ -0,0 +1,25 @@
ChatApiHandler = require("../Chat/ChatApiHandler")
EditorRealTimeController = require("../Editor/EditorRealTimeController")
logger = require("logger-sharelatex")
AuthenticationController = require('../Authentication/AuthenticationController')
module.exports = CommentsController =
sendComment: (req, res, next) ->
{project_id, thread_id} = req.params
content = req.body.content
user_id = AuthenticationController.getLoggedInUserId(req)
if !user_id?
err = new Error('no logged-in user')
return next(err)
logger.log {project_id, thread_id, user_id, content}, "sending comment"
ChatApiHandler.sendComment project_id, thread_id, user_id, content, (err, comment) ->
return next(err) if err?
EditorRealTimeController.emitToRoom project_id, "new-comment", thread_id, comment, (err)->
res.send 204
getThreads: (req, res, next) ->
{project_id} = req.params
logger.log {project_id}, "getting comment threads for project"
ChatApiHandler.getThreads project_id, (err, threads) ->
return next(err) if err?
res.json threads

View file

@ -41,6 +41,7 @@ BetaProgramController = require('./Features/BetaProgram/BetaProgramController')
AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter') AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter')
AnnouncementsController = require("./Features/Announcements/AnnouncementsController") AnnouncementsController = require("./Features/Announcements/AnnouncementsController")
RangesController = require("./Features/Ranges/RangesController") RangesController = require("./Features/Ranges/RangesController")
CommentsController = require "./Features/Comments/CommentsController"
logger = require("logger-sharelatex") logger = require("logger-sharelatex")
_ = require("underscore") _ = require("underscore")
@ -226,8 +227,11 @@ module.exports = class Router
webRouter.post "/spelling/check", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi webRouter.post "/spelling/check", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi
webRouter.post "/spelling/learn", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi webRouter.post "/spelling/learn", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi
webRouter.get "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.getMessages webRouter.get "/project/:project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.getMessages
webRouter.post "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.sendMessage webRouter.post "/project/:project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.sendMessage
webRouter.post "/project/:project_id/thread/:thread_id/messages", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.sendComment
webRouter.get "/project/:project_id/threads", AuthorizationMiddlewear.ensureUserCanReadProject, CommentsController.getThreads
webRouter.post "/project/:Project_id/references/index", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.index webRouter.post "/project/:Project_id/references/index", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.index
webRouter.post "/project/:Project_id/references/indexAll", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.indexAll webRouter.post "/project/:Project_id/references/indexAll", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.indexAll

View file

@ -28,7 +28,7 @@
div(ng-if="entry.type === 'comment'") div(ng-if="entry.type === 'comment'")
comment-entry( comment-entry(
entry="entry" entry="entry"
users="users" threads="reviewPanel.commentThreads"
on-resolve="resolveComment(entry, entry_id)" on-resolve="resolveComment(entry, entry_id)"
on-unresolve="unresolveComment(entry_id)" on-unresolve="unresolveComment(entry_id)"
on-show-thread="showThread(entry)" on-show-thread="showThread(entry)"
@ -150,19 +150,19 @@ script(type='text/ng-template', id='commentEntryTemplate')
) )
.rp-comment( .rp-comment(
ng-if="!entry.resolved || entry.showWhenResolved" ng-if="!entry.resolved || entry.showWhenResolved"
ng-repeat="comment in entry.thread" ng-repeat="comment in threads[entry.thread_id]"
ng-class="users[comment.user_id].isSelf ? 'rp-comment-self' : '';" ng-class="comment.user.isSelf ? 'rp-comment-self' : '';"
) )
.rp-avatar( .rp-avatar(
ng-if="!users[comment.user_id].isSelf;" ng-if="!comment.user.isSelf;"
style="background-color: hsl({{ users[comment.user_id].hue }}, 70%, 50%);" style="background-color: hsl({{ comment.user.hue }}, 70%, 50%);"
) {{ users[comment.user_id].avatar_text | limitTo : 1 }} ) {{ comment.user.avatar_text | limitTo : 1 }}
.rp-comment-body(style="color: hsl({{ users[comment.user_id].hue }}, 70%, 90%);") .rp-comment-body(style="color: hsl({{ comment.user.hue }}, 70%, 90%);")
p.rp-comment-content {{ comment.content }} p.rp-comment-content {{ comment.content }}
p.rp-comment-metadata p.rp-comment-metadata
| {{ comment.ts | date : 'MMM d, y h:mm a' }} | {{ comment.timestamp | date : 'MMM d, y h:mm a' }}
| &nbsp;&bull;&nbsp; | &nbsp;&bull;&nbsp;
span(style="color: hsl({{ users[comment.user_id].hue }}, 70%, 40%);") {{ users[comment.user_id].name }} span(style="color: hsl({{ comment.user.hue }}, 70%, 40%);") {{ comment.user.name }}
.rp-comment-reply(ng-if="!entry.resolved || entry.showWhenResolved") .rp-comment-reply(ng-if="!entry.resolved || entry.showWhenResolved")
textarea.rp-comment-input( textarea.rp-comment-input(
ng-model="entry.replyContent" ng-model="entry.replyContent"

View file

@ -339,8 +339,6 @@ define [
track_changes_as = msg.meta.user_id track_changes_as = msg.meta.user_id
else if !remote_op and @track_changes_as? else if !remote_op and @track_changes_as?
track_changes_as = @track_changes_as track_changes_as = @track_changes_as
console.log "CHANGED", oldSnapshot, ops, track_changes_as
@ranges.track_changes = track_changes_as? @ranges.track_changes = track_changes_as?
for op in ops for op in ops
console.log "APPLYING OP", op, @ranges.track_changes
@ranges.applyOp op, { user_id: track_changes_as } @ranges.applyOp op, { user_id: track_changes_as }

View file

@ -20,8 +20,8 @@ define [
@rangesTracker = doc.ranges @rangesTracker = doc.ranges
@connectToRangesTracker() @connectToRangesTracker()
@$scope.$on "comment:add", (e) => @$scope.$on "comment:add", (e, thread_id) =>
@addCommentToSelection() @addCommentToSelection(thread_id)
@$scope.$on "comment:select_line", (e) => @$scope.$on "comment:select_line", (e) =>
@selectLineIfNoSelection() @selectLineIfNoSelection()
@ -146,16 +146,16 @@ define [
for comment in @rangesTracker.comments for comment in @rangesTracker.comments
@_onCommentAdded(comment) @_onCommentAdded(comment)
addComment: (offset, content) -> addComment: (offset, content, thread_id) ->
op = { c: content, p: offset } op = { c: content, p: offset, t: thread_id }
# @rangesTracker.applyOp op # Will apply via sharejs # @rangesTracker.applyOp op # Will apply via sharejs
@$scope.sharejsDoc.submitOp op @$scope.sharejsDoc.submitOp op
addCommentToSelection: () -> addCommentToSelection: (thread_id) ->
range = @editor.getSelectionRange() range = @editor.getSelectionRange()
content = @editor.getSelectedText() content = @editor.getSelectedText()
offset = @_aceRangeToShareJs(range.start) offset = @_aceRangeToShareJs(range.start)
@addComment(offset, content) @addComment(offset, content, thread_id)
selectLineIfNoSelection: () -> selectLineIfNoSelection: () ->
if @editor.selection.isEmpty() if @editor.selection.isEmpty()

View file

@ -35,18 +35,25 @@ load = (EventEmitter) ->
# * Inserts by another user will not combine with inserts by the first user. If they are in the # * Inserts by another user will not combine with inserts by the first user. If they are in the
# middle of a previous insert by the first user, the original insert will be split into two. # middle of a previous insert by the first user, the original insert will be split into two.
constructor: (@changes = [], @comments = []) -> constructor: (@changes = [], @comments = []) ->
# Change objects have the following structure:
# { @_increment: 0
# id: ... # Uniquely generated by us @newId: () ->
# op: { # ShareJs style op tracking the offset (p) and content inserted (i) or deleted (d) # Generate a Mongo ObjectId
# i: "..." # Reference: https://github.com/dreampulse/ObjectId.js/blob/master/src/main/javascript/Objectid.js
# p: 42 @_pid ?= Math.floor(Math.random() * (32767))
# } @_machine ?= Math.floor(Math.random() * (16777216))
# } timestamp = Math.floor(new Date().valueOf() / 1000)
# @_increment++
# Ids are used to uniquely identify a change, e.g. for updating it in the database, or keeping in
# sync with Ace ranges. timestamp = timestamp.toString(16)
@id = 0 machine = @_machine.toString(16)
pid = @_pid.toString(16)
increment = @_increment.toString(16)
return '00000000'.substr(0, 8 - timestamp.length) + timestamp +
'000000'.substr(0, 6 - machine.length) + machine +
'0000'.substr(0, 4 - pid.length) + pid +
'000000'.substr(0, 6 - increment.length) + increment;
getComment: (comment_id) -> getComment: (comment_id) ->
comment = null comment = null
@ -105,7 +112,7 @@ load = (EventEmitter) ->
addComment: (op, metadata) -> addComment: (op, metadata) ->
# TODO: Don't allow overlapping comments? # TODO: Don't allow overlapping comments?
@comments.push comment = { @comments.push comment = {
id: @_newId() id: RangesTracker.newId()
op: # Copy because we'll modify in place op: # Copy because we'll modify in place
c: op.c c: op.c
p: op.p p: op.p
@ -394,28 +401,9 @@ load = (EventEmitter) ->
if moved_changes.length > 0 if moved_changes.length > 0
@emit "changes:moved", moved_changes @emit "changes:moved", moved_changes
_newId: () ->
# Generate a Mongo ObjectId
# Reference: https://github.com/dreampulse/ObjectId.js/blob/master/src/main/javascript/Objectid.js
@_pid ?= Math.floor(Math.random() * (32767))
@_machine ?= Math.floor(Math.random() * (16777216))
timestamp = Math.floor(new Date().valueOf() / 1000)
@_increment ?= 0
@_increment++
timestamp = timestamp.toString(16)
machine = @_machine.toString(16)
pid = @_pid.toString(16)
increment = @_increment.toString(16)
return '00000000'.substr(0, 8 - timestamp.length) + timestamp +
'000000'.substr(0, 6 - machine.length) + machine +
'0000'.substr(0, 4 - pid.length) + pid +
'000000'.substr(0, 6 - increment.length) + increment;
_addOp: (op, metadata) -> _addOp: (op, metadata) ->
change = { change = {
id: @_newId() id: RangesTracker.newId()
op: op op: op
metadata: metadata metadata: metadata
} }

View file

@ -18,12 +18,27 @@ define [
openSubView: $scope.SubViews.CUR_FILE openSubView: $scope.SubViews.CUR_FILE
overview: overview:
loading: false loading: false
commentThreads: {}
$scope.commentState = $scope.commentState =
adding: false adding: false
content: "" content: ""
$scope.reviewPanelEventsBridge = new EventEmitter() $scope.reviewPanelEventsBridge = new EventEmitter()
$http.get "/project/#{$scope.project_id}/threads"
.success (threads) ->
for thread_id, comments of threads
for comment in comments
formatComment(comment)
$scope.reviewPanel.commentThreads = threads
ide.socket.on "new-comment", (thread_id, comment) ->
$scope.reviewPanel.commentThreads[thread_id] ?= []
$scope.reviewPanel.commentThreads[thread_id].push(formatComment(comment))
$scope.$apply()
$timeout () ->
$scope.$broadcast "review-panel:layout"
rangesTrackers = {} rangesTrackers = {}
@ -35,95 +50,6 @@ define [
rangesTrackers[doc_id] ?= new RangesTracker() rangesTrackers[doc_id] ?= new RangesTracker()
return rangesTrackers[doc_id] return rangesTrackers[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]?
rangesTracker = getChangeTracker(entity.id)
for change in mock_changes[entity.name].changes
rangesTracker._addOp change.op, change.metadata
for comment in mock_changes[entity.name].comments
rangesTracker.addComment comment.offset, comment.length, comment.metadata
for doc_id, rangesTracker of rangesTrackers
updateEntries(doc_id)
scrollbar = {} scrollbar = {}
$scope.reviewPanelEventsBridge.on "aceScrollbarVisibilityChanged", (isVisible, scrollbarWidth) -> $scope.reviewPanelEventsBridge.on "aceScrollbarVisibilityChanged", (isVisible, scrollbarWidth) ->
scrollbar = {isVisible, scrollbarWidth} scrollbar = {isVisible, scrollbarWidth}
@ -156,7 +82,6 @@ define [
$scope.$watch "editor.sharejs_doc", (doc) -> $scope.$watch "editor.sharejs_doc", (doc) ->
return if !doc? return if !doc?
console.log "DOC changed", doc
# The open doc range tracker is kept up to date in real-time so # The open doc range tracker is kept up to date in real-time so
# replace any outdated info with this # replace any outdated info with this
rangesTrackers[doc.doc_id] = doc.ranges rangesTrackers[doc.doc_id] = doc.ranges
@ -218,7 +143,7 @@ define [
entries[comment.id] ?= {} entries[comment.id] ?= {}
new_entry = { new_entry = {
type: "comment" type: "comment"
thread: comment.metadata.thread or [] thread_id: comment.op.t
resolved: comment.metadata?.resolved resolved: comment.metadata?.resolved
resolved_data: comment.metadata?.resolved_data resolved_data: comment.metadata?.resolved_data
content: comment.op.c content: comment.op.c
@ -250,7 +175,7 @@ define [
for id, entry of entries for id, entry of entries
if entry.type == "comment" and not entry.resolved if entry.type == "comment" and not entry.resolved
entry.focused = (entry.offset <= cursor_offset <= entry.offset + entry.length) entry.focused = (entry.offset <= cursor_offset <= entry.offset + entry.content.length)
else if entry.type == "insert" else if entry.type == "insert"
entry.focused = (entry.offset <= cursor_offset <= entry.offset + entry.content.length) entry.focused = (entry.offset <= cursor_offset <= entry.offset + entry.content.length)
else if entry.type == "delete" else if entry.type == "delete"
@ -268,21 +193,20 @@ define [
$scope.$broadcast "change:reject", entry_id $scope.$broadcast "change:reject", entry_id
$scope.startNewComment = () -> $scope.startNewComment = () ->
# $scope.commentState.adding = true
$scope.$broadcast "comment:select_line" $scope.$broadcast "comment:select_line"
$timeout () -> $timeout () ->
$scope.$broadcast "review-panel:layout" $scope.$broadcast "review-panel:layout"
$scope.submitNewComment = (content) -> $scope.submitNewComment = (content) ->
# $scope.commentState.adding = false thread_id = RangesTracker.newId()
$scope.$broadcast "comment:add", content $scope.$broadcast "comment:add", thread_id
# $scope.commentState.content = "" $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 () -> $timeout () ->
$scope.$broadcast "review-panel:layout" $scope.$broadcast "review-panel:layout"
$scope.cancelNewComment = (entry) -> $scope.cancelNewComment = (entry) ->
# $scope.commentState.adding = false
# $scope.commentState.content = ""
$timeout () -> $timeout () ->
$scope.$broadcast "review-panel:layout" $scope.$broadcast "review-panel:layout"
@ -291,40 +215,19 @@ define [
$timeout () -> $timeout () ->
$scope.$broadcast "review-panel:layout" $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.submitReply = (entry, entry_id) ->
$scope.unresolveComment(entry_id) $scope.unresolveComment(entry_id)
entry.thread.push { thread_id = entry.thread_id
content: entry.replyContent content = entry.replyContent
ts: new Date() $http.post("/project/#{$scope.project_id}/thread/#{thread_id}/messages", {content, _csrf: window.csrfToken})
user_id: window.user_id .error (error) ->
} ide.showGenericMessageModal("Error submitting comment", "Sorry, there was a problem submitting your comment")
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
}
entry.replyContent = "" entry.replyContent = ""
entry.replying = false entry.replying = false
$timeout () -> $timeout () ->
$scope.$broadcast "review-panel:layout" $scope.$broadcast "review-panel:layout"
$scope.cancelReply = (entry) -> $scope.cancelReply = (entry) ->
entry.replying = false entry.replying = false
entry.replyContent = "" entry.replyContent = ""
@ -361,37 +264,39 @@ define [
# when we get an id we don't know. This'll do for client side testing # when we get an id we don't know. This'll do for client side testing
refreshUsers = () -> refreshUsers = () ->
$scope.users = {} $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"
}
for member in $scope.project.members.concat($scope.project.owner) for member in $scope.project.members.concat($scope.project.owner)
if member._id == window.user_id $scope.users[member._id] = formatUser(member)
name = "You"
isSelf = true
else
name = "#{member.first_name} #{member.last_name}"
isSelf = false
$scope.users[member._id] = { formatComment = (comment) ->
email: member.email comment.user = formatUser(user)
name: name comment.timestamp = new Date(comment.timestamp)
isSelf: isSelf return comment
hue: ColorManager.getHueForUserId(member._id)
avatar_text: [member.first_name, member.last_name].filter((n) -> n?).map((n) -> n[0]).join "" formatUser = (user) ->
if !user?
return {
email: null
name: "Anonymous"
isSelf: false
hue: ColorManager.ANONYMOUS_HUE
avatar_text: "A"
} }
id = user._id or user.id
if id == window.user_id
name = "You"
isSelf = true
else
name = "#{user.first_name} #{user.last_name}"
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 ""
}
$scope.$watch "project.members", (members) -> $scope.$watch "project.members", (members) ->
return if !members? return if !members?

View file

@ -6,7 +6,7 @@ define [
templateUrl: "commentEntryTemplate" templateUrl: "commentEntryTemplate"
scope: scope:
entry: "=" entry: "="
users: "=" threads: "="
onResolve: "&" onResolve: "&"
onReply: "&" onReply: "&"
onIndicatorClick: "&" onIndicatorClick: "&"

View file

@ -0,0 +1,92 @@
should = require('chai').should()
SandboxedModule = require('sandboxed-module')
assert = require('assert')
path = require('path')
sinon = require('sinon')
modulePath = path.join __dirname, "../../../../app/js/Features/Chat/ChatApiHandler"
expect = require("chai").expect
describe "ChatApiHandler", ->
beforeEach ->
@settings =
apis:
chat:
internal_url:"chat.sharelatex.env"
@request = sinon.stub()
@ChatApiHandler = SandboxedModule.require modulePath, requires:
"settings-sharelatex": @settings
"logger-sharelatex": { log: sinon.stub(), error: sinon.stub() }
"request": @request
@project_id = "3213213kl12j"
@user_id = "2k3jlkjs9"
@content = "my message here"
@callback = sinon.stub()
describe "sendGlobalMessage", ->
describe "successfully", ->
beforeEach ->
@message = { "mock": "message" }
@request.callsArgWith(1, null, {statusCode: 200}, @message)
@ChatApiHandler.sendGlobalMessage @project_id, @user_id, @content, @callback
it "should post the data to the chat api", ->
@request.calledWith({
url: "#{@settings.apis.chat.internal_url}/project/#{@project_id}/messages"
method: "POST"
json:
content: @content
user_id: @user_id
}).should.equal true
it "should return the message from the post", ->
@callback.calledWith(null, @message).should.equal true
describe "with a non-success status code", ->
beforeEach ->
@request.callsArgWith(1, null, {statusCode: 500})
@ChatApiHandler.sendGlobalMessage @project_id, @user_id, @content, @callback
it "should return an error", ->
error = new Error()
error.statusCode = 500
@callback.calledWith(error).should.equal true
describe "getGlobalMessages", ->
beforeEach ->
@messages = [{ "mock": "message" }]
@limit = 30
@before = "1234"
describe "successfully", ->
beforeEach ->
@request.callsArgWith(1, null, {statusCode: 200}, @messages)
@ChatApiHandler.getGlobalMessages @project_id, @limit, @before, @callback
it "should make get request for room to chat api", ->
@request.calledWith({
method: "GET"
url: "#{@settings.apis.chat.internal_url}/project/#{@project_id}/messages"
qs:
limit: @limit
before: @before
json: true
}).should.equal true
it "should return the messages from the request", ->
@callback.calledWith(null, @messages).should.equal true
describe "with failure error code", ->
beforeEach ->
@request.callsArgWith(1, null, {statusCode: 500}, null)
@ChatApiHandler.getGlobalMessages @project_id, @limit, @before, @callback
it "should return an error", ->
error = new Error()
error.statusCode = 500
@callback.calledWith(error).should.equal true

View file

@ -7,75 +7,59 @@ modulePath = path.join __dirname, "../../../../app/js/Features/Chat/ChatControll
expect = require("chai").expect expect = require("chai").expect
describe "ChatController", -> describe "ChatController", ->
beforeEach -> beforeEach ->
@user_id = 'mock-user-id'
@user_id = 'ier_'
@settings = {} @settings = {}
@ChatHandler = @ChatApiHandler = {}
sendMessage:sinon.stub()
getMessages:sinon.stub()
@EditorRealTimeController = @EditorRealTimeController =
emitToRoom:sinon.stub().callsArgWith(3) emitToRoom:sinon.stub().callsArgWith(3)
@AuthenticationController = @AuthenticationController =
getLoggedInUserId: sinon.stub().returns(@user_id) getLoggedInUserId: sinon.stub().returns(@user_id)
@ChatController = SandboxedModule.require modulePath, requires: @ChatController = SandboxedModule.require modulePath, requires:
"settings-sharelatex":@settings "settings-sharelatex": @settings
"logger-sharelatex": log:-> "logger-sharelatex": log: ->
"./ChatHandler":@ChatHandler "./ChatApiHandler": @ChatApiHandler
"../Editor/EditorRealTimeController":@EditorRealTimeController "../Editor/EditorRealTimeController": @EditorRealTimeController
'../Authentication/AuthenticationController': @AuthenticationController '../Authentication/AuthenticationController': @AuthenticationController
@query =
before:"some time"
@req = @req =
params: params:
Project_id:@project_id project_id: @project_id
session:
user:
_id:@user_id
body:
content:@messageContent
@res = @res =
set:sinon.stub() json: sinon.stub()
send: sinon.stub()
describe "sendMessage", -> describe "sendMessage", ->
beforeEach ->
it "should tell the chat handler about the message", (done)-> @req.body =
@ChatHandler.sendMessage.callsArgWith(3) content: @content = "message-content"
@res.send = => @ChatApiHandler.sendGlobalMessage = sinon.stub().yields(null, @message = {"mock": "message"})
@ChatHandler.sendMessage.calledWith(@project_id, @user_id, @messageContent).should.equal true
done()
@ChatController.sendMessage @req, @res @ChatController.sendMessage @req, @res
it "should tell the editor real time controller about the update with the data from the chat handler", (done)-> it "should tell the chat handler about the message", ->
@chatMessage = @ChatApiHandler.sendGlobalMessage
content:"hello world" .calledWith(@project_id, @user_id, @content)
@ChatHandler.sendMessage.callsArgWith(3, null, @chatMessage) .should.equal true
@res.send = =>
@EditorRealTimeController.emitToRoom.calledWith(@project_id, "new-chat-message", @chatMessage).should.equal true it "should tell the editor real time controller about the update with the data from the chat handler", ->
done() @EditorRealTimeController.emitToRoom
@ChatController.sendMessage @req, @res .calledWith(@project_id, "new-chat-message", @message)
.should.equal true
it "should return a 204 status code", ->
@res.send.calledWith(204).should.equal true
describe "getMessages", -> describe "getMessages", ->
beforeEach -> beforeEach ->
@req.query = @query @req.query =
limit: @limit = "30"
it "should ask the chat handler about the request", (done)-> before: @before = "12345"
@ChatApiHandler.getGlobalMessages = sinon.stub().yields(null, @messages = ["mock", "messages"])
@ChatHandler.getMessages.callsArgWith(2)
@res.send = =>
@ChatHandler.getMessages.calledWith(@project_id, @query).should.equal true
done()
@ChatController.getMessages @req, @res @ChatController.getMessages @req, @res
it "should return the messages", (done)-> it "should ask the chat handler about the request", ->
messages = [{content:"hello"}] @ChatApiHandler.getGlobalMessages
@ChatHandler.getMessages.callsArgWith(2, null, messages) .calledWith(@project_id, @limit, @before)
@res.send = (sentMessages)=> .should.equal true
@res.set.calledWith('Content-Type', 'application/json').should.equal true
sentMessages.should.deep.equal messages it "should return the messages", ->
done() @res.json.calledWith(@messages).should.equal true
@ChatController.getMessages @req, @res

View file

@ -1,89 +0,0 @@
should = require('chai').should()
SandboxedModule = require('sandboxed-module')
assert = require('assert')
path = require('path')
sinon = require('sinon')
modulePath = path.join __dirname, "../../../../app/js/Features/Chat/ChatHandler"
expect = require("chai").expect
describe "ChatHandler", ->
beforeEach ->
@settings =
apis:
chat:
internal_url:"chat.sharelatex.env"
@request = sinon.stub()
@ChatHandler = SandboxedModule.require modulePath, requires:
"settings-sharelatex":@settings
"logger-sharelatex": log:->
"request": @request
@project_id = "3213213kl12j"
@user_id = "2k3jlkjs9"
@messageContent = "my message here"
describe "sending message", ->
beforeEach ->
@messageResponse =
message:"Details"
@request.callsArgWith(1, null, null, @messageResponse)
it "should post the data to the chat api", (done)->
@ChatHandler.sendMessage @project_id, @user_id, @messageContent, (err)=>
@opts =
method:"post"
json:
content:@messageContent
user_id:@user_id
uri:"#{@settings.apis.chat.internal_url}/room/#{@project_id}/messages"
@request.calledWith(@opts).should.equal true
done()
it "should return the message from the post", (done)->
@ChatHandler.sendMessage @project_id, @user_id, @messageContent, (err, returnedMessage)=>
returnedMessage.should.equal @messageResponse
done()
describe "get messages", ->
beforeEach ->
@returnedMessages = [{content:"hello world"}]
@request.callsArgWith(1, null, null, @returnedMessages)
@query = {}
it "should make get request for room to chat api", (done)->
@ChatHandler.getMessages @project_id, @query, (err)=>
@opts =
method:"get"
uri:"#{@settings.apis.chat.internal_url}/room/#{@project_id}/messages"
qs:{}
@request.calledWith(@opts).should.equal true
done()
it "should make get request for room to chat api with query string", (done)->
@query = {limit:5, before:12345, ignore:"this"}
@ChatHandler.getMessages @project_id, @query, (err)=>
@opts =
method:"get"
uri:"#{@settings.apis.chat.internal_url}/room/#{@project_id}/messages"
qs:
limit:5
before:12345
@request.calledWith(@opts).should.equal true
done()
it "should return the messages from the request", (done)->
@ChatHandler.getMessages @project_id, @query, (err, returnedMessages)=>
returnedMessages.should.equal @returnedMessages
done()

View file

@ -0,0 +1,65 @@
should = require('chai').should()
SandboxedModule = require('sandboxed-module')
assert = require('assert')
path = require('path')
sinon = require('sinon')
modulePath = path.join __dirname, "../../../../app/js/Features/Comments/CommentsController"
expect = require("chai").expect
describe "CommentsController", ->
beforeEach ->
@user_id = 'mock-user-id'
@settings = {}
@ChatApiHandler = {}
@EditorRealTimeController =
emitToRoom:sinon.stub()
@AuthenticationController =
getLoggedInUserId: sinon.stub().returns(@user_id)
@CommentsController = SandboxedModule.require modulePath, requires:
"settings-sharelatex": @settings
"logger-sharelatex": log: ->
"../Chat/ChatApiHandler": @ChatApiHandler
"../Editor/EditorRealTimeController": @EditorRealTimeController
'../Authentication/AuthenticationController': @AuthenticationController
@req = {}
@res =
json: sinon.stub()
send: sinon.stub()
describe "sendComment", ->
beforeEach ->
@req.params =
project_id: @project_id
thread_id: @thread_id
@req.body =
content: @content = "message-content"
@ChatApiHandler.sendComment = sinon.stub().yields(null, @message = {"mock": "message"})
@CommentsController.sendComment @req, @res
it "should tell the chat handler about the message", ->
@ChatApiHandler.sendComment
.calledWith(@project_id, @thread_id, @user_id, @content)
.should.equal true
it "should tell the editor real time controller about the update with the data from the chat handler", ->
@EditorRealTimeController.emitToRoom
.calledWith(@project_id, "new-comment", @thread_id, @message)
.should.equal true
it "should return a 204 status code", ->
@res.send.calledWith(204).should.equal true
describe "getThreads", ->
beforeEach ->
@req.params =
project_id: @project_id
@ChatApiHandler.getThreads = sinon.stub().yields(null, @threads = {"mock", "threads"})
@CommentsController.getThreads @req, @res
it "should ask the chat handler about the request", ->
@ChatApiHandler.getThreads
.calledWith(@project_id)
.should.equal true
it "should return the messages", ->
@res.json.calledWith(@threads).should.equal true