mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #4 from sharelatex/ja-track-changes
Ja track changes
This commit is contained in:
commit
4805136d59
41 changed files with 391 additions and 40279 deletions
|
@ -12,14 +12,6 @@ module.exports = (grunt) ->
|
|||
src: "app.js"
|
||||
|
||||
coffee:
|
||||
client:
|
||||
expand: true,
|
||||
flatten: false,
|
||||
cwd: 'public/coffee',
|
||||
src: ['**/*.coffee'],
|
||||
dest: 'public/build/',
|
||||
ext: '.js'
|
||||
|
||||
server:
|
||||
expand: true,
|
||||
flatten: false,
|
||||
|
@ -56,47 +48,7 @@ module.exports = (grunt) ->
|
|||
files: ['app/**/*.coffee', 'test/unit/**/*.coffee']
|
||||
tasks: ['compile:server', 'compile:unit_tests', 'mochaTest']
|
||||
|
||||
client_coffee:
|
||||
files: ['public/**/*.coffee']
|
||||
tasks: ['compile']
|
||||
|
||||
less:
|
||||
files: ['public/less/*.less']
|
||||
tasks: ['compile']
|
||||
|
||||
jade:
|
||||
files: ['public/jade/*.jade']
|
||||
tasks: ['compile']
|
||||
|
||||
|
||||
less:
|
||||
production:
|
||||
files:
|
||||
"public/build/css/chat.css": "public/less/chat.less"
|
||||
|
||||
jade:
|
||||
compile:
|
||||
files:
|
||||
"public/build/html/templates.html": ["public/jade/templates.jade"]
|
||||
|
||||
requirejs:
|
||||
compile:
|
||||
options:
|
||||
mainConfigFile: 'public/app.build.js',
|
||||
|
||||
uglify:
|
||||
my_target:
|
||||
files:
|
||||
'public/build/chat.js': ['public/build/chat.js']
|
||||
|
||||
copy:
|
||||
main:
|
||||
expand: true
|
||||
cwd: 'public/js'
|
||||
src: '**'
|
||||
dest: 'public/build/'
|
||||
|
||||
clean: ["public/build", "app/js", "test/unit/js"]
|
||||
clean: ["app/js", "test/unit/js"]
|
||||
|
||||
nodemon:
|
||||
dev:
|
||||
|
@ -127,11 +79,6 @@ module.exports = (grunt) ->
|
|||
|
||||
grunt.loadNpmTasks 'grunt-contrib-coffee'
|
||||
grunt.loadNpmTasks 'grunt-contrib-watch'
|
||||
grunt.loadNpmTasks 'grunt-contrib-copy'
|
||||
grunt.loadNpmTasks 'grunt-contrib-less'
|
||||
grunt.loadNpmTasks 'grunt-contrib-jade'
|
||||
grunt.loadNpmTasks 'grunt-contrib-requirejs'
|
||||
grunt.loadNpmTasks 'grunt-contrib-uglify'
|
||||
grunt.loadNpmTasks 'grunt-nodemon'
|
||||
grunt.loadNpmTasks 'grunt-contrib-clean'
|
||||
grunt.loadNpmTasks 'grunt-concurrent'
|
||||
|
@ -142,10 +89,9 @@ module.exports = (grunt) ->
|
|||
grunt.loadNpmTasks 'grunt-forever'
|
||||
|
||||
|
||||
grunt.registerTask 'compile', ['clean', 'copy', 'coffee', 'less', 'jade', 'requirejs']
|
||||
grunt.registerTask 'compile', ['clean', 'coffee']
|
||||
grunt.registerTask 'install', ['compile']
|
||||
grunt.registerTask 'default', ['compile', 'bunyan', 'execute']
|
||||
grunt.registerTask 'compileAndCompress', ['compile', 'uglify']
|
||||
grunt.registerTask 'test:unit', ['compile', 'mochaTest:unit']
|
||||
grunt.registerTask 'test:acceptance', ['compile:acceptance_tests', 'mochaTest:acceptance']
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
UserFormatter = require "../Users/UserFormatter"
|
||||
|
||||
module.exports = MessageFormatter =
|
||||
formatMessageForClientSide: (message) ->
|
||||
if message._id?
|
||||
|
@ -9,8 +7,38 @@ module.exports = MessageFormatter =
|
|||
id: message.id
|
||||
content: message.content
|
||||
timestamp: message.timestamp
|
||||
user: UserFormatter.formatUserForClientSide(message.user)
|
||||
user_id: message.user_id
|
||||
return formattedMessage
|
||||
|
||||
formatMessagesForClientSide: (messages) ->
|
||||
(@formatMessageForClientSide(message) for message in messages)
|
||||
|
||||
groupMessagesByThreads: (rooms, messages) ->
|
||||
rooms_by_id = {}
|
||||
for room in rooms
|
||||
rooms_by_id[room._id.toString()] = room
|
||||
|
||||
threads = {}
|
||||
getThread = (room) ->
|
||||
thread_id = room.thread_id.toString()
|
||||
if threads[thread_id]?
|
||||
return threads[thread_id]
|
||||
else
|
||||
thread = { messages: [] }
|
||||
if room.resolved?
|
||||
thread.resolved = true
|
||||
thread.resolved_at = room.resolved.ts
|
||||
thread.resolved_by_user_id = room.resolved.user_id
|
||||
threads[thread_id] = thread
|
||||
return thread
|
||||
|
||||
for message in messages
|
||||
room = rooms_by_id[message.room_id.toString()]
|
||||
if room?
|
||||
thread = getThread(room)
|
||||
thread.messages.push MessageFormatter.formatMessageForClientSide(message)
|
||||
|
||||
for thread_id, thread of threads
|
||||
thread.messages.sort (a,b) -> a.timestamp - b.timestamp
|
||||
|
||||
return threads
|
|
@ -2,61 +2,78 @@ logger = require "logger-sharelatex"
|
|||
metrics = require "metrics-sharelatex"
|
||||
MessageManager = require "./MessageManager"
|
||||
MessageFormatter = require "./MessageFormatter"
|
||||
RoomManager = require "../Rooms/RoomManager"
|
||||
ThreadManager = require "../Threads/ThreadManager"
|
||||
{ObjectId} = require "../../mongojs"
|
||||
|
||||
module.exports = MessageHttpController =
|
||||
DEFAULT_MESSAGE_LIMIT: 50
|
||||
|
||||
getGlobalMessages: (req, res, next) ->
|
||||
MessageHttpController._getMessages(ThreadManager.GLOBAL_THREAD, req, res, next)
|
||||
|
||||
sendMessage: (req, res, next) ->
|
||||
sendGlobalMessage: (req, res, next) ->
|
||||
MessageHttpController._sendMessage(ThreadManager.GLOBAL_THREAD, req, res, next)
|
||||
|
||||
sendThreadMessage: (req, res, next) ->
|
||||
MessageHttpController._sendMessage(req.params.thread_id, req, res, next)
|
||||
|
||||
getAllThreads: (req, res, next) ->
|
||||
{project_id} = req.params
|
||||
logger.log {project_id}, "getting all threads"
|
||||
ThreadManager.findAllThreadRooms project_id, (error, rooms) ->
|
||||
return next(error) if error?
|
||||
room_ids = rooms.map (r) -> r._id
|
||||
MessageManager.findAllMessagesInRooms room_ids, (error, messages) ->
|
||||
return next(error) if error?
|
||||
threads = MessageFormatter.groupMessagesByThreads rooms, messages
|
||||
res.json threads
|
||||
|
||||
resolveThread: (req, res, next) ->
|
||||
{project_id, thread_id} = req.params
|
||||
{user_id} = req.body
|
||||
logger.log {user_id, project_id, thread_id}, "marking thread as resolved"
|
||||
ThreadManager.resolveThread project_id, thread_id, user_id, (error) ->
|
||||
return next(error) if error?
|
||||
res.send 204 # No content
|
||||
|
||||
reopenThread: (req, res, next) ->
|
||||
{project_id, thread_id} = req.params
|
||||
logger.log {project_id, thread_id}, "reopening thread"
|
||||
ThreadManager.reopenThread project_id, thread_id, (error) ->
|
||||
return next(error) if error?
|
||||
res.send 204 # No content
|
||||
|
||||
_sendMessage: (client_thread_id, req, res, next) ->
|
||||
{user_id, content} = req?.body
|
||||
{project_id} = req.params
|
||||
|
||||
logger.log user_id: user_id, content: content, "new message recived"
|
||||
RoomManager.findOrCreateRoom project_id: project_id, (error, room) ->
|
||||
if !ObjectId.isValid(user_id)
|
||||
return res.send(400, "Invalid user_id")
|
||||
logger.log {client_thread_id, project_id, user_id, content}, "new message received"
|
||||
ThreadManager.findOrCreateThread project_id, client_thread_id, (error, thread) ->
|
||||
return next(error) if error?
|
||||
newMessageOpts =
|
||||
content: content
|
||||
room_id: room._id
|
||||
user_id: user_id
|
||||
timestamp: Date.now()
|
||||
MessageManager.createMessage newMessageOpts, (error, message) ->
|
||||
if err?
|
||||
logger.err err:error, user_id:user_id, "something went wrong with create message"
|
||||
return next(err)
|
||||
MessageManager.populateMessagesWithUsers [message], (error, messages) ->
|
||||
if error?
|
||||
logger.err err:error, user_id:user_id, "something went wrong populateMessagesWithUsers"
|
||||
return next("something went wrong")
|
||||
message = MessageFormatter.formatMessageForClientSide(messages[0])
|
||||
message.room =
|
||||
id: project_id
|
||||
res.send(201, message)
|
||||
MessageManager.createMessage thread._id, user_id, content, Date.now(), (error, message) ->
|
||||
return next(error) if error?
|
||||
message = MessageFormatter.formatMessageForClientSide(message)
|
||||
message.room_id = project_id
|
||||
res.send(201, message)
|
||||
|
||||
getMessages: (req, res, next) ->
|
||||
_getMessages: (client_thread_id, req, res, next) ->
|
||||
{project_id} = req.params
|
||||
query = {}
|
||||
if req.query?.before?
|
||||
query.timestamp = $lt: parseInt(req.query.before, 10)
|
||||
before = parseInt(req.query.before, 10)
|
||||
else
|
||||
before = null
|
||||
if req.query?.limit?
|
||||
limit = parseInt(req.query.limit, 10)
|
||||
else
|
||||
limit = MessageHttpController.DEFAULT_MESSAGE_LIMIT
|
||||
options =
|
||||
order_by: "timestamp"
|
||||
sort_order: -1
|
||||
limit: limit
|
||||
logger.log options:options, "get message request recived"
|
||||
RoomManager.findOrCreateRoom project_id: project_id, (error, room) ->
|
||||
logger.log {limit, before, project_id, client_thread_id}, "get message request received"
|
||||
ThreadManager.findOrCreateThread project_id, client_thread_id, (error, thread) ->
|
||||
return next(error) if error?
|
||||
query.room_id = room._id
|
||||
MessageManager.getMessages query, options, (error, messages) ->
|
||||
if error?
|
||||
logger.err err:error, "something went getMessages"
|
||||
return next("something went wrong")
|
||||
MessageManager.populateMessagesWithUsers messages, (error, messages) ->
|
||||
if error?
|
||||
logger.err err:error, "something went populateMessagesWithUsers"
|
||||
return next("something went wrong")
|
||||
messages = MessageFormatter.formatMessagesForClientSide messages
|
||||
logger.log project_id: project_id, "got messages"
|
||||
res.send 200, messages
|
||||
thread_object_id = thread._id
|
||||
logger.log {limit, before, project_id, client_thread_id, thread_object_id}, "found or created thread"
|
||||
MessageManager.getMessages thread_object_id, limit, before, (error, messages) ->
|
||||
return next(error) if error?
|
||||
messages = MessageFormatter.formatMessagesForClientSide messages
|
||||
logger.log {project_id, messages}, "got messages"
|
||||
res.send 200, messages
|
||||
|
|
|
@ -1,50 +1,32 @@
|
|||
mongojs = require "../../mongojs"
|
||||
db = mongojs.db
|
||||
ObjectId = mongojs.ObjectId
|
||||
WebApiManager = require "../WebApi/WebApiManager"
|
||||
async = require "async"
|
||||
|
||||
module.exports = MessageManager =
|
||||
createMessage: (message, callback = (error, message) ->) ->
|
||||
message = @_ensureIdsAreObjectIds(message)
|
||||
db.messages.save message, callback
|
||||
createMessage: (room_id, user_id, content, timestamp, callback = (error, message) ->) ->
|
||||
newMessageOpts =
|
||||
content: content
|
||||
room_id: room_id
|
||||
user_id: user_id
|
||||
timestamp: timestamp
|
||||
newMessageOpts = @_ensureIdsAreObjectIds(newMessageOpts)
|
||||
db.messages.save newMessageOpts, callback
|
||||
|
||||
getMessages: (query, options, callback = (error, messages) ->) ->
|
||||
getMessages: (room_id, limit, before, callback = (error, messages) ->) ->
|
||||
query =
|
||||
room_id: room_id
|
||||
if before?
|
||||
query.timestamp = { $lt: before }
|
||||
query = @_ensureIdsAreObjectIds(query)
|
||||
cursor = db.messages.find(query)
|
||||
if options.order_by?
|
||||
options.sort_order ||= 1
|
||||
sortQuery = {}
|
||||
sortQuery[options.order_by] = options.sort_order
|
||||
cursor = cursor.sort(sortQuery)
|
||||
if options.limit?
|
||||
cursor = cursor.limit(options.limit)
|
||||
cursor = db.messages.find(query).sort({ timestamp: -1 }).limit(limit)
|
||||
cursor.toArray callback
|
||||
|
||||
populateMessagesWithUsers: (messages, callback = (error, messages) ->) ->
|
||||
jobs = new Array()
|
||||
|
||||
userCache = {}
|
||||
getUserDetails = (user_id, callback = (error, user) ->) ->
|
||||
return callback(null, userCache[user_id]) if userCache[user_id]?
|
||||
WebApiManager.getUserDetails user_id, (error, user) ->
|
||||
return callback(error) if error?
|
||||
userCache[user_id] = user
|
||||
callback null, user
|
||||
|
||||
for message in messages
|
||||
do (message) ->
|
||||
if !message?
|
||||
return
|
||||
jobs.push (callback) ->
|
||||
getUserDetails message.user_id.toString(), (error, user) ->
|
||||
return callback(error) if error?
|
||||
delete message.user_id
|
||||
message.user = user
|
||||
callback(null, message)
|
||||
|
||||
async.series jobs, callback
|
||||
|
||||
findAllMessagesInRooms: (room_ids, callback = (error, messages) ->) ->
|
||||
db.messages.find {
|
||||
room_id: { $in: room_ids }
|
||||
}, callback
|
||||
|
||||
_ensureIdsAreObjectIds: (query) ->
|
||||
if query.user_id? and query.user_id not instanceof ObjectId
|
||||
query.user_id = ObjectId(query.user_id)
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
mongojs = require("../../mongojs")
|
||||
db = mongojs.db
|
||||
ObjectId = mongojs.ObjectId
|
||||
|
||||
module.exports = RoomManager =
|
||||
findOrCreateRoom: (query, callback = (error, room) ->) ->
|
||||
if query.project_id? and query.project_id not instanceof ObjectId
|
||||
query.project_id = ObjectId(query.project_id)
|
||||
|
||||
db.rooms.findOne query, (error, room) ->
|
||||
return callback(error) if error?
|
||||
if room?
|
||||
callback null, room
|
||||
else
|
||||
db.rooms.save query, (error, room) ->
|
||||
return callback(error) if error?
|
||||
callback null, room
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
mongojs = require("../../mongojs")
|
||||
db = mongojs.db
|
||||
ObjectId = mongojs.ObjectId
|
||||
|
||||
module.exports = ThreadManager =
|
||||
GLOBAL_THREAD: "GLOBAL"
|
||||
|
||||
findOrCreateThread: (project_id, thread_id, callback = (error, thread) ->) ->
|
||||
query =
|
||||
project_id: ObjectId(project_id.toString())
|
||||
|
||||
if thread_id? and thread_id != ThreadManager.GLOBAL_THREAD
|
||||
query.thread_id = ObjectId(thread_id.toString())
|
||||
|
||||
# Threads used to be called rooms, and still are in the DB
|
||||
db.rooms.findOne query, (error, thread) ->
|
||||
return callback(error) if error?
|
||||
if thread?
|
||||
callback null, thread
|
||||
else
|
||||
db.rooms.save query, (error, thread) ->
|
||||
return callback(error) if error?
|
||||
callback null, thread
|
||||
|
||||
findAllThreadRooms: (project_id, callback = (error, rooms) ->) ->
|
||||
db.rooms.find {
|
||||
project_id: ObjectId(project_id.toString())
|
||||
thread_id: { $exists: true }
|
||||
}, {
|
||||
thread_id: 1,
|
||||
resolved: 1
|
||||
}, callback
|
||||
|
||||
resolveThread: (project_id, thread_id, user_id, callback = (error) ->) ->
|
||||
db.rooms.update {
|
||||
project_id: ObjectId(project_id.toString())
|
||||
thread_id: ObjectId(thread_id.toString())
|
||||
}, {
|
||||
$set: {
|
||||
resolved: {
|
||||
user_id: user_id
|
||||
ts: new Date()
|
||||
}
|
||||
}
|
||||
}, callback
|
||||
|
||||
reopenThread: (project_id, thread_id, callback = (error) ->) ->
|
||||
db.rooms.update {
|
||||
project_id: ObjectId(project_id.toString())
|
||||
thread_id: ObjectId(thread_id.toString())
|
||||
}, {
|
||||
$unset: {
|
||||
resolved: true
|
||||
}
|
||||
}, callback
|
|
@ -1,19 +0,0 @@
|
|||
crypto = require "crypto"
|
||||
|
||||
module.exports = UserFormatter =
|
||||
formatUserForClientSide: (user) ->
|
||||
return null if !user?
|
||||
if user._id?
|
||||
user.id = user._id.toString()
|
||||
delete user._id
|
||||
return {
|
||||
id: user.id
|
||||
first_name: user.first_name
|
||||
last_name: user.last_name
|
||||
email: user.email
|
||||
gravatar_url: @_getGravatarUrlForEmail(user.email)
|
||||
}
|
||||
|
||||
_getGravatarUrlForEmail: (email) ->
|
||||
hash = crypto.createHash("md5").update(email.toLowerCase()).digest("hex")
|
||||
return "//www.gravatar.com/avatar/#{hash}"
|
|
@ -1,43 +0,0 @@
|
|||
request = require('request').defaults(jar: false)
|
||||
Settings = require("settings-sharelatex")
|
||||
|
||||
# DEPRECATED! This method of getting user details via chat is deprecated
|
||||
# in the way we lay out our services.
|
||||
# Instead, web should be responsible for collecting the raw data (user_ids) and
|
||||
# filling it out with calls to other services. All API calls should create a
|
||||
# tree-like structure as much as possible, with web as the root.
|
||||
module.exports = WebApiManager =
|
||||
apiRequest: (url, method, options = {}, callback = (error, result) ->) ->
|
||||
if typeof options == "function"
|
||||
callback = options
|
||||
options = {}
|
||||
url = "#{Settings.apis.web.url}#{url}"
|
||||
options.url = url
|
||||
options.method = method
|
||||
request options, (error, response, body) ->
|
||||
return callback(error) if error?
|
||||
if 200 <= response.statusCode < 300
|
||||
try
|
||||
result = JSON.parse(body)
|
||||
catch e
|
||||
return callback(e)
|
||||
return callback null, result
|
||||
else
|
||||
error = new Error("web api returned non-success code: #{response.statusCode}")
|
||||
error.statusCode = response.statusCode
|
||||
return callback error
|
||||
|
||||
getUserDetails: (user_id, callback = (error, details) ->) ->
|
||||
@apiRequest "/user/#{user_id}/personal_info", "get", {
|
||||
auth:
|
||||
user: Settings.apis.web.user
|
||||
pass: Settings.apis.web.pass
|
||||
sendImmediately: true
|
||||
}, (error, data) ->
|
||||
if error?
|
||||
if error.statusCode == 404
|
||||
return callback null, null
|
||||
else
|
||||
return callback error
|
||||
else
|
||||
return callback null, data
|
|
@ -1,6 +1,6 @@
|
|||
Settings = require("settings-sharelatex")
|
||||
mongojs = require "mongojs"
|
||||
db = mongojs.connect(Settings.mongo.url, ["rooms", "messages"])
|
||||
db = mongojs(Settings.mongo.url, ["rooms", "messages"])
|
||||
module.exports =
|
||||
db: db
|
||||
ObjectId: mongojs.ObjectId
|
||||
|
|
|
@ -1,10 +1,37 @@
|
|||
MessageHttpController = require('./Features/Messages/MessageHttpController')
|
||||
{ObjectId} = require "./mongojs"
|
||||
|
||||
module.exports = Router =
|
||||
route: (app) ->
|
||||
app.get "/room/:project_id/messages", MessageHttpController.getMessages
|
||||
app.post "/room/:project_id/messages", MessageHttpController.sendMessage
|
||||
|
||||
app.param 'project_id', (req, res, next, project_id) ->
|
||||
if ObjectId.isValid(project_id)
|
||||
next()
|
||||
else
|
||||
res.send 400, "Invalid project_id"
|
||||
|
||||
app.param 'thread_id', (req, res, next, thread_id) ->
|
||||
if ObjectId.isValid(thread_id)
|
||||
next()
|
||||
else
|
||||
res.send 400, "Invalid thread_id"
|
||||
|
||||
# These are for backwards compatibility
|
||||
app.get "/room/:project_id/messages", MessageHttpController.getGlobalMessages
|
||||
app.post "/room/:project_id/messages", MessageHttpController.sendGlobalMessage
|
||||
|
||||
app.get "/project/:project_id/messages", MessageHttpController.getGlobalMessages
|
||||
app.post "/project/:project_id/messages", MessageHttpController.sendGlobalMessage
|
||||
|
||||
app.post "/project/:project_id/thread/:thread_id/messages", MessageHttpController.sendThreadMessage
|
||||
app.get "/project/:project_id/threads", MessageHttpController.getAllThreads
|
||||
|
||||
# app.post "/project/:project_id/thread/:thread_id/messages/:message_id/edit", MessageHttpController.editMessage
|
||||
# app.del "/project/:project_id/thread/:thread_id/messages/:message_id", MessageHttpController.deleteMessage
|
||||
|
||||
app.post "/project/:project_id/thread/:thread_id/resolve", MessageHttpController.resolveThread
|
||||
app.post "/project/:project_id/thread/:thread_id/reopen", MessageHttpController.reopenThread
|
||||
# app.del "/project/:project_id/thread/:thread_id", MessageHttpController.deleteThread
|
||||
|
||||
app.get "/status", (req, res, next) ->
|
||||
res.send("chat is alive")
|
||||
|
||||
|
|
|
@ -11,8 +11,8 @@
|
|||
"coffee-script": "~1.7.1",
|
||||
"express": "3.3.1",
|
||||
"logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#master",
|
||||
"metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.0.0",
|
||||
"mongojs": "0.18.2",
|
||||
"metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.6.0",
|
||||
"mongojs": "^2.4.0",
|
||||
"redis": "~0.10.1",
|
||||
"request": "^2.79.0",
|
||||
"settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0"
|
||||
|
@ -25,11 +25,6 @@
|
|||
"grunt-concurrent": "~0.4.2",
|
||||
"grunt-contrib-clean": "~0.5.0",
|
||||
"grunt-contrib-coffee": "~0.7.0",
|
||||
"grunt-contrib-copy": "~0.4.1",
|
||||
"grunt-contrib-jade": "~0.8.0",
|
||||
"grunt-contrib-less": "~0.8.2",
|
||||
"grunt-contrib-requirejs": "~0.4.1",
|
||||
"grunt-contrib-uglify": "~0.2.7",
|
||||
"grunt-contrib-watch": "~0.5.3",
|
||||
"grunt-execute": "^0.2.2",
|
||||
"grunt-forever": "^0.4.7",
|
||||
|
@ -37,7 +32,6 @@
|
|||
"grunt-nodemon": "~0.1.2",
|
||||
"grunt-notify": "~0.2.16",
|
||||
"grunt-plato": "~0.2.1",
|
||||
"grunt-requirejs": "~0.4.0",
|
||||
"sandboxed-module": "",
|
||||
"sinon": "",
|
||||
"timekeeper": ""
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
requirejs.config({
|
||||
baseUrl: "./build",
|
||||
out: "./build/chat.js",
|
||||
inlineText:true,
|
||||
preserveLicenseComments:false,
|
||||
shim: {
|
||||
"libs/underscore": {
|
||||
init: function() {
|
||||
return _.noConflict();
|
||||
}
|
||||
},
|
||||
"libs/backbone": {
|
||||
deps: ["libs/underscore"],
|
||||
init: function() {
|
||||
return Backbone.noConflict();
|
||||
}
|
||||
}
|
||||
},
|
||||
paths: {
|
||||
"moment": "libs/moment",
|
||||
},
|
||||
name:"chat",
|
||||
optimize: 'none',
|
||||
skipDirOptimize: true
|
||||
})
|
|
@ -1,109 +0,0 @@
|
|||
define [
|
||||
"utils/staticLoader"
|
||||
"libs/underscore"
|
||||
"libs/backbone"
|
||||
"libs/jquery.storage"
|
||||
"models/room"
|
||||
"models/user"
|
||||
"views/chatWindowView"
|
||||
|
||||
], (staticLoader, _, Backbone, jqueryStorage, Room, User, ChatWindowView) ->
|
||||
|
||||
staticLoader.appendAssets()
|
||||
_.templateSettings = escape : /\{\{(.+?)\}\}/g
|
||||
|
||||
class GlobalNotificationManager
|
||||
constructor: (@chat) ->
|
||||
@focussed = true
|
||||
$(window).on "focus", () =>
|
||||
@clearNewMessageNotification()
|
||||
@focussed = true
|
||||
$(window).on "blur", () => @focussed = false
|
||||
|
||||
@chat.on "joinedRoom", (room) =>
|
||||
notifyIfAppropriate = (message) =>
|
||||
if message.get("user") != @chat.user and !message.get("preloaded")
|
||||
@notifyAboutNewMessage()
|
||||
|
||||
room.get("messages").on "add", notifyIfAppropriate
|
||||
room.on "disconnect", () ->
|
||||
room.get("messages").off "add", notifyIfAppropriate
|
||||
|
||||
notifyAboutNewMessage: () ->
|
||||
if !@focussed and !@newMessageNotificationTimeout?
|
||||
@originalTitle ||= window.document.title
|
||||
do changeTitle = () =>
|
||||
if window.document.title == @originalTitle
|
||||
window.document.title = "New Message"
|
||||
else
|
||||
window.document.title = @originalTitle
|
||||
@newMessageNotificationTimeout = setTimeout changeTitle, 800
|
||||
|
||||
clearNewMessageNotification: () ->
|
||||
clearTimeout @newMessageNotificationTimeout
|
||||
delete @newMessageNotificationTimeout
|
||||
if @originalTitle?
|
||||
window.document.title = @originalTitle
|
||||
|
||||
class Chat
|
||||
constructor: (options) ->
|
||||
_.extend(@, Backbone.Events)
|
||||
window.chat = @
|
||||
@rooms = {}
|
||||
project_id = window.location.pathname.split( '/' )[2]
|
||||
@socket = socket = io.connect options.url, {
|
||||
resource: "chat/socket.io",
|
||||
"force new connection": true
|
||||
query:"project_id=#{project_id}"
|
||||
}
|
||||
|
||||
@socket.on "connect", () =>
|
||||
@connected = true
|
||||
@getAuthToken (error, auth_token) =>
|
||||
return @handleError(error) if error?
|
||||
@socket.emit "auth", {auth_token: auth_token}, (error, user_info) =>
|
||||
return @handleError(error) if error?
|
||||
@user = User.findOrCreate(user_info)
|
||||
@joinProjectRoom(options.room.project_id)
|
||||
@trigger "authed"
|
||||
|
||||
@socket.on "disconnect", () =>
|
||||
@connected = false
|
||||
@trigger "disconnected"
|
||||
|
||||
@socket.on "messageReceived", (data) =>
|
||||
@getRoom(data.message.room.id)?.onMessageReceived(data)
|
||||
|
||||
@socket.on "userJoined", (data) =>
|
||||
@getRoom(data.room.id).addConnectedUser(data.user)
|
||||
|
||||
@socket.on "userLeft", (data) =>
|
||||
@getRoom(data.room.id)?.removeConnectedUser(data.user)
|
||||
|
||||
@globalNotificationManager = new GlobalNotificationManager(@)
|
||||
|
||||
getRoom: (room_id) ->
|
||||
@rooms[room_id]
|
||||
|
||||
joinProjectRoom: (project_id) ->
|
||||
if !@room?
|
||||
@room = new Room(
|
||||
project_id: project_id
|
||||
chat: @
|
||||
)
|
||||
@window = new ChatWindowView({
|
||||
room: @room
|
||||
chat: @
|
||||
})
|
||||
@room.on "joined", => @trigger("joinedRoom", @room)
|
||||
|
||||
getAuthToken: (callback = (error, auth_token) ->) ->
|
||||
$.ajax "/user/auth_token", {
|
||||
success: (data, status, xhr) ->
|
||||
callback null, data
|
||||
error: (xhr, status, error) ->
|
||||
callback error
|
||||
}
|
||||
|
||||
handleError: (error) ->
|
||||
console.error error
|
|
@ -1,9 +0,0 @@
|
|||
define [
|
||||
"libs/backbone"
|
||||
"models/user"
|
||||
], (Backbone, User) ->
|
||||
ConnectedUsers = Backbone.Collection.extend
|
||||
model: User
|
||||
|
||||
initialize: (models, options) ->
|
||||
{@chat, @room} = options
|
|
@ -1,45 +0,0 @@
|
|||
define [
|
||||
"libs/backbone"
|
||||
"models/message"
|
||||
"models/user"
|
||||
|
||||
], (Backbone, Message, User) ->
|
||||
|
||||
Messages = Backbone.Collection.extend
|
||||
model: Message
|
||||
|
||||
initialize: (models, options) ->
|
||||
{@chat, @room} = options
|
||||
|
||||
fetchMoreMessages: (options = { preloading: false }, callback = (error) ->) ->
|
||||
limit = Messages.DEFAULT_MESSAGE_LIMIT
|
||||
|
||||
@room.fetchMessages @_buildMessagesQuery(limit), (error, messages) =>
|
||||
if error?
|
||||
callback(error)
|
||||
return @chat.handleError(error)
|
||||
if messages.length < limit
|
||||
@trigger "noMoreMessages"
|
||||
@_parseAndAddMessages(messages, options)
|
||||
callback()
|
||||
|
||||
_parseAndAddMessages: (messages, options) ->
|
||||
for message in messages
|
||||
user = User.findOrCreate message.user
|
||||
@add new Message(
|
||||
content : message.content
|
||||
timestamp : message.timestamp
|
||||
user : user
|
||||
preloaded : !!options.preloading
|
||||
), at: 0
|
||||
|
||||
_buildMessagesQuery: (limit) ->
|
||||
query =
|
||||
limit: limit
|
||||
firstMessage = @at(0)
|
||||
if firstMessage?
|
||||
query.before = firstMessage.get("timestamp")
|
||||
return query
|
||||
Messages.DEFAULT_MESSAGE_LIMIT = 50
|
||||
|
||||
return Messages
|
|
@ -1,5 +0,0 @@
|
|||
define [
|
||||
"libs/backbone"
|
||||
], (Backbone) ->
|
||||
|
||||
Message = Backbone.Model.extend {}
|
|
@ -1,85 +0,0 @@
|
|||
define [
|
||||
"libs/underscore"
|
||||
"libs/backbone"
|
||||
"collections/messages"
|
||||
"collections/connectedUsers"
|
||||
"models/user"
|
||||
"models/message"
|
||||
], (_, Backbone, Messages, ConnectedUsers, User, Message) ->
|
||||
|
||||
|
||||
Room = Backbone.Model.extend
|
||||
initialize: () ->
|
||||
@chat = @get("chat")
|
||||
@set "messages", new Messages([], chat: @chat, room: @)
|
||||
@set "connectedUsers", new ConnectedUsers([], chat: @chat, room: @)
|
||||
|
||||
@get("connectedUsers").on "change", () ->
|
||||
@get("connectedUsers").on "add", () ->
|
||||
@get("connectedUsers").on "remove", () ->
|
||||
|
||||
@connected = false
|
||||
|
||||
@chat.on "authed", () => @join()
|
||||
@chat.on "disconnected", () => @_onDisconnect()
|
||||
|
||||
join: () ->
|
||||
@chat.socket.emit "joinRoom", room: project_id: @get("project_id"), (error, data) =>
|
||||
return @chat.handleError(error) if error?
|
||||
room = data.room
|
||||
@set("id", room.id)
|
||||
@chat.rooms[room.id] = @
|
||||
@addConnectedUsers(room.connectedUsers)
|
||||
@_onJoin()
|
||||
|
||||
_onJoin: () ->
|
||||
@trigger "joined"
|
||||
@connected = true
|
||||
|
||||
if @get("messages").models.length == 0
|
||||
@get("messages").fetchMoreMessages preloading: true, () =>
|
||||
@trigger("afterMessagesPreloaded")
|
||||
|
||||
_onDisconnect: () ->
|
||||
@trigger "disconnected"
|
||||
@connected = false
|
||||
|
||||
addConnectedUsers: (users) ->
|
||||
for user in users
|
||||
@addConnectedUser(user)
|
||||
|
||||
addConnectedUser: (user) ->
|
||||
if user not instanceof User
|
||||
user = User.findOrCreate(user)
|
||||
@get("connectedUsers").add user
|
||||
|
||||
removeConnectedUser: (user) ->
|
||||
if user not instanceof User
|
||||
user = User.findOrCreate(user)
|
||||
@get("connectedUsers").remove user
|
||||
|
||||
sendMessage: (content, callback = (error) ->) ->
|
||||
if !@connected
|
||||
return callback(new Error("Not connected"))
|
||||
@chat.socket.emit "sendMessage", {
|
||||
message:
|
||||
content: content
|
||||
room:
|
||||
id: @get("id")
|
||||
}
|
||||
|
||||
fetchMessages: (query, callback = (error, messages) ->) ->
|
||||
if !@connected
|
||||
return callback(new Error("Not connected"))
|
||||
query.room = id: @get("id")
|
||||
@chat.socket.emit "getMessages", query, callback
|
||||
|
||||
onMessageReceived: (data) ->
|
||||
message = data.message
|
||||
user = User.findOrCreate message.user
|
||||
message = new Message(
|
||||
content : data.message.content
|
||||
timestamp : data.message.timestamp
|
||||
user : user
|
||||
)
|
||||
@get("messages").add message
|
|
@ -1,13 +0,0 @@
|
|||
define [
|
||||
"libs/backbone"
|
||||
], (Backbone, room) ->
|
||||
|
||||
User = Backbone.Model.extend {},
|
||||
findOrCreate: (attributes) ->
|
||||
User.cache ||= {}
|
||||
if User.cache[attributes.id]?
|
||||
return User.cache[attributes.id]
|
||||
else
|
||||
user = new User(attributes)
|
||||
User.cache[attributes.id] = user
|
||||
return user
|
|
@ -1,10 +0,0 @@
|
|||
define [
|
||||
"text!html/templates.html"
|
||||
"text!css/chat.css"
|
||||
], (templates, css)->
|
||||
|
||||
appendAssets : ->
|
||||
$(document.body).append($(templates))
|
||||
style = $("<style/>")
|
||||
style.html(css)
|
||||
$(document.body).append(style)
|
|
@ -1,219 +0,0 @@
|
|||
define [
|
||||
"libs/underscore"
|
||||
"libs/backbone"
|
||||
"views/userMessageBlockView"
|
||||
"views/timeMessageBlockView"
|
||||
], (_, Backbone, UserMessageBlockView, TimeMessageBlockView) ->
|
||||
FIVE_MINS = 5 * 60 * 1000
|
||||
ONE_HOUR = 60 * 60 * 1000
|
||||
TWELVE_HOURS = ONE_HOUR * 12
|
||||
ONE_DAY = ONE_HOUR * 24
|
||||
|
||||
ChatWindowView = Backbone.View.extend
|
||||
events:
|
||||
"keydown textarea" : "_onTextAreaKeyDown"
|
||||
"click .js-load-older-messages" : "_loadOlderMessages"
|
||||
"click .js-minimize-toggle" : "_toggleMinimizeState"
|
||||
"click h3" : "_toggleMinimizeState"
|
||||
"click .js-new-message-alert" : "_toggleMinimizeState"
|
||||
"click" : "_removeNotification"
|
||||
|
||||
initialize: () ->
|
||||
@template = $("#chatWindowTemplate").html()
|
||||
@chat = @options.chat
|
||||
@room = @options.room
|
||||
@listenTo @room.get("messages"), "add", (model, collection) -> @_onNewMessage(model, collection)
|
||||
@listenTo @room.get("messages"), "noMoreMessages", () -> @$(".js-loading").hide()
|
||||
@listenTo @room.get("connectedUsers"), "add", (user, collection) -> @_renderConnectedUsers()
|
||||
@listenTo @room.get("connectedUsers"), "remove", (user, collection) -> @_renderConnectedUsers()
|
||||
@listenTo @room.get("connectedUsers"), "change", (user, collection) -> @_renderConnectedUsers()
|
||||
@listenTo @room, "joined", -> @_onJoin()
|
||||
@listenTo @room, "disconnected", -> @_onDisconnect()
|
||||
@listenTo @room, "afterMessagesPreloaded", -> @_scrollToBottomOfMessages()
|
||||
|
||||
render: () ->
|
||||
@setElement($(@template))
|
||||
$(document.body).append(@$el)
|
||||
@_renderConnectedUsers()
|
||||
@_initializeMinimizedState()
|
||||
|
||||
_onJoin: () ->
|
||||
if !@rendered?
|
||||
@render()
|
||||
@rendered = true
|
||||
@$el.removeClass("disconnected")
|
||||
@$("textarea").removeAttr("disabled")
|
||||
|
||||
_onDisconnect: () ->
|
||||
@$el.addClass("disconnected")
|
||||
@$("textarea").attr("disabled", "disabled")
|
||||
|
||||
_onNewMessage: (message, collection) ->
|
||||
@_renderMessage(message, collection)
|
||||
@_notifyAboutNewMessage(message)
|
||||
|
||||
_renderMessage: (message, collection) ->
|
||||
|
||||
@messageBlocks ||= []
|
||||
scrollEl = @$(".sent-message-area")
|
||||
|
||||
isOldestMessage = (message, collection)->
|
||||
collection.indexOf(message) == 0
|
||||
|
||||
ismessageFromNewUser = (messageView, message)->
|
||||
!messageView? or messageView.user != message.get("user")
|
||||
|
||||
isTimeForNewBlockBackwards = (message, previousUserMessageBlockView)->
|
||||
if !message? or !previousUserMessageBlockView?
|
||||
return true
|
||||
|
||||
timeSinceMessageWasSent = new Date().getTime() - message.get("timestamp")
|
||||
|
||||
if timeSinceMessageWasSent < ONE_HOUR
|
||||
timeBlockSize = FIVE_MINS
|
||||
else if timeSinceMessageWasSent > ONE_HOUR and timeSinceMessageWasSent < (ONE_DAY + TWELVE_HOURS)
|
||||
timeBlockSize = ONE_HOUR
|
||||
else
|
||||
timeBlockSize = ONE_DAY
|
||||
|
||||
timeSinceLastPrinted = previousUserMessageBlockView.getTime() - message.get("timestamp")
|
||||
|
||||
if timeSinceLastPrinted > timeBlockSize
|
||||
return true
|
||||
else
|
||||
return false
|
||||
|
||||
|
||||
isTimeForNewBlock = (message, previousUserMessageBlockView)->
|
||||
(message.get("timestamp") - previousUserMessageBlockView.getTime()) > FIVE_MINS
|
||||
|
||||
|
||||
if isOldestMessage(message, collection)
|
||||
oldScrollTopFromBottom = scrollEl[0].scrollHeight - scrollEl.scrollTop()
|
||||
|
||||
userMessageBlockView = @messageBlocks[0]
|
||||
if ismessageFromNewUser(userMessageBlockView, message) or isTimeForNewBlockBackwards(message, userMessageBlockView)
|
||||
userMessageBlockView = new UserMessageBlockView(user: message.get("user"))
|
||||
@$(".sent-messages").prepend(userMessageBlockView.$el)
|
||||
@messageBlocks.unshift userMessageBlockView
|
||||
|
||||
userMessageBlockView.prependMessage(message)
|
||||
|
||||
scrollEl.scrollTop(scrollEl[0].scrollHeight - oldScrollTopFromBottom)
|
||||
else
|
||||
oldScrollBottom = @_getScrollBottom()
|
||||
userMessageBlockView = @messageBlocks[@messageBlocks.length - 1]
|
||||
|
||||
if ismessageFromNewUser(userMessageBlockView, message) or isTimeForNewBlock(message, userMessageBlockView)
|
||||
userMessageBlockView = new UserMessageBlockView(user: message.get("user"))
|
||||
@$(".sent-messages").append(userMessageBlockView.$el)
|
||||
@messageBlocks.push userMessageBlockView
|
||||
|
||||
userMessageBlockView.appendMessage(message)
|
||||
|
||||
if oldScrollBottom <= 0
|
||||
@_scrollToBottomOfMessages()
|
||||
|
||||
|
||||
_renderConnectedUsers: () ->
|
||||
users = @room.get("connectedUsers")
|
||||
names = users
|
||||
.reject((user) => user == @chat.user)
|
||||
.map((user) -> "#{user.get("first_name")} #{user.get("last_name")}")
|
||||
if names.length == 0
|
||||
text = "No one else is online :("
|
||||
else if names.length == 1
|
||||
text = "#{names[0]} is also online"
|
||||
else
|
||||
text = "#{names.slice(0, -1).join(", ")} and #{names[names.length - 1]} are also online"
|
||||
@$(".js-connected-users").text(text)
|
||||
@_resizeSentMessageArea()
|
||||
|
||||
_resizeSentMessageArea: () ->
|
||||
marginTop = @$(".js-header").outerHeight() + @$(".js-connected-users").outerHeight()
|
||||
@$(".js-sent-message-area").css({
|
||||
top: marginTop + "px"
|
||||
})
|
||||
|
||||
_getScrollBottom: () ->
|
||||
scrollEl = @$(".sent-message-area")
|
||||
return scrollEl[0].scrollHeight - scrollEl.scrollTop() - scrollEl.innerHeight()
|
||||
|
||||
_scrollToBottomOfMessages: () ->
|
||||
scrollEl = @$(".sent-message-area")
|
||||
doScroll = ->
|
||||
return scrollEl.scrollTop(scrollEl[0].scrollHeight - scrollEl.innerHeight())
|
||||
MathJax.Hub.Queue(["Typeset", doScroll])
|
||||
|
||||
_notifyAboutNewMessage: (message) ->
|
||||
isMessageNewToUser = message.get("user") != @chat.user and !message.get("preloaded")
|
||||
isTextAreaFocused = @$("textarea").is(":focus")
|
||||
if !isTextAreaFocused and isMessageNewToUser
|
||||
@unseenMessages ||= 0
|
||||
@unseenMessages += 1
|
||||
@$el.addClass("new-messages")
|
||||
@$(".new-message-alert").text(@unseenMessages)
|
||||
|
||||
_removeNotification: () ->
|
||||
@unseenMessages = 0
|
||||
@$el.removeClass("new-messages")
|
||||
@$(".new-message-alert").text("")
|
||||
|
||||
_onTextAreaKeyDown: (e) ->
|
||||
if e.keyCode == 13 # Enter
|
||||
e.preventDefault()
|
||||
message = @$("textarea").val()
|
||||
@$("textarea").val("")
|
||||
@_sendMessage(message)
|
||||
|
||||
_loadOlderMessages: (e) ->
|
||||
e.preventDefault()
|
||||
@room.get("messages").fetchMoreMessages()
|
||||
|
||||
_sendMessage: (content) ->
|
||||
@room.sendMessage(content)
|
||||
|
||||
isMinimized: () ->
|
||||
minimized = $.localStorage "chat.rooms.project-chat.minimized"
|
||||
if !minimized?
|
||||
minimized = false
|
||||
return minimized
|
||||
|
||||
_setMinimizedState: (state) ->
|
||||
$.localStorage "chat.rooms.project-chat.minimized", state
|
||||
|
||||
_initializeMinimizedState: () ->
|
||||
minimized = @isMinimized()
|
||||
if minimized
|
||||
@_minimize(false)
|
||||
|
||||
_toggleMinimizeState: (e) ->
|
||||
e.preventDefault()
|
||||
minimized = @isMinimized()
|
||||
if !minimized
|
||||
@_setMinimizedState(true)
|
||||
@_minimize()
|
||||
else
|
||||
@_setMinimizedState(false)
|
||||
@_unminimize()
|
||||
|
||||
_minimize: (animate = true) ->
|
||||
@$(".new-message-area").hide()
|
||||
@$(".js-connected-users").hide()
|
||||
@$el.addClass("minimized")
|
||||
if animate
|
||||
@$el.animate height: 20, width: 80
|
||||
else
|
||||
@$el.css height: 20, width: 80
|
||||
|
||||
_unminimize: () ->
|
||||
@$(".new-message-area").show()
|
||||
@$(".js-connected-users").show()
|
||||
@$el.removeClass("minimized")
|
||||
@$el.animate height: 260, width: 220, () =>
|
||||
@_resizeSentMessageArea()
|
||||
@_scrollToBottomOfMessages()
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
define [
|
||||
"libs/underscore"
|
||||
"libs/backbone"
|
||||
"moment"
|
||||
], (_, Backbone, moment) ->
|
||||
ONE_WEEK = 7 * 24 * 60 * 60 * 1000
|
||||
|
||||
TimeMessageBlockView = Backbone.View.extend
|
||||
|
||||
className : "timeSinceMessage"
|
||||
|
||||
initialize: () ->
|
||||
@autoRefresh()
|
||||
|
||||
setTimeOnce: (timestamp)->
|
||||
if !@timestamp?
|
||||
@timestamp = timestamp
|
||||
@render()
|
||||
return @
|
||||
|
||||
setTime: (@timestamp)->
|
||||
@render()
|
||||
return @
|
||||
|
||||
autoRefresh: ->
|
||||
if @timestamp?
|
||||
@render()
|
||||
self = @
|
||||
doIt = =>
|
||||
self.autoRefresh()
|
||||
setTimeout doIt, 60 * 1000
|
||||
|
||||
render: () ->
|
||||
milisecondsSince = new Date().getTime() - @timestamp
|
||||
if milisecondsSince > ONE_WEEK
|
||||
time = moment(@timestamp).format("D/MMM/YY, h:mm:ss a")
|
||||
else
|
||||
time = moment(@timestamp).fromNow()
|
||||
this.$el.html(time)
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
define [
|
||||
"libs/underscore"
|
||||
"libs/backbone"
|
||||
"views/timeMessageBlockView"
|
||||
"moment"
|
||||
"https://c328740.ssl.cf1.rackcdn.com/mathjax/latest/MathJax.js?config=TeX-AMS_HTML"
|
||||
], (_, Backbone, TimeMessageBlockView, moment) ->
|
||||
|
||||
mathjaxConfig =
|
||||
"HTML-CSS": { availableFonts: ["TeX"] },
|
||||
TeX:
|
||||
equationNumbers: { autoNumber: "AMS" },
|
||||
useLabelIDs: false
|
||||
tex2jax:
|
||||
inlineMath: [ ['$','$'], ["\\(","\\)"] ],
|
||||
displayMath: [ ['$$','$$'], ["\\[","\\]"] ],
|
||||
processEscapes: true
|
||||
|
||||
|
||||
MathJax.Hub.Config(mathjaxConfig);
|
||||
|
||||
|
||||
|
||||
UserMessageBlockView = Backbone.View.extend
|
||||
|
||||
initialize: () ->
|
||||
@template = _.template($("#messageBlockTemplate").html())
|
||||
@user = @options.user
|
||||
@timeMessageBlock = new TimeMessageBlockView()
|
||||
@render()
|
||||
|
||||
render: () ->
|
||||
@setElement $(@template(
|
||||
first_name: @user.get("first_name")
|
||||
last_name: @user.get("last_name")
|
||||
gravatar_url: @user.get("gravatar_url")
|
||||
))
|
||||
@$(".timeArea").html(@timeMessageBlock.$el)
|
||||
|
||||
getTime: ->
|
||||
return @timeMessageBlock.timestamp
|
||||
|
||||
appendMessage: (message) ->
|
||||
el = @buildHtml(message)
|
||||
@$(".messages").append(el)
|
||||
@_renderMathJax(el)
|
||||
@timeMessageBlock.setTimeOnce message.get("timestamp")
|
||||
|
||||
prependMessage: (message) ->
|
||||
el = @buildHtml(message)
|
||||
@$(".messages").prepend(el)
|
||||
@_renderMathJax(el)
|
||||
@timeMessageBlock.setTime message.get("timestamp")
|
||||
|
||||
buildHtml : (message)->
|
||||
time = moment(message.get("timestamp")).format("dddd, MMMM Do YYYY, h:mm:ss a")
|
||||
el = $("<div class='message' title='#{time}'>")
|
||||
el.text(message.get("content"))
|
||||
return el
|
||||
|
||||
|
||||
_renderMathJax: (element)->
|
||||
if element?
|
||||
MathJax.Hub.Queue(["Typeset", MathJax.Hub, element.get(0)])
|
|
@ -1,24 +0,0 @@
|
|||
script(type="text/templates")#chatWindowTemplate
|
||||
.chat-window
|
||||
.header.js-header
|
||||
h3 Chat
|
||||
.new-message-alert.js-new-message-alert
|
||||
.window-controls
|
||||
.js-minimize-toggle.minimize-toggle
|
||||
.connected-users.js-connected-users
|
||||
.sent-message-area.js-sent-message-area
|
||||
.loading.js-loading
|
||||
a(href="#").load-older-messages.js-load-older-messages Load older messages
|
||||
.sent-messages
|
||||
.new-message-area
|
||||
textarea(placeholder="Your message")
|
||||
|
||||
script(type="text/templates")#messageBlockTemplate
|
||||
.chat-block
|
||||
.message-block
|
||||
.timeArea
|
||||
.avatar
|
||||
img(src="{{ gravatar_url }}?d=mm&s=40", alt="{{first_name}} {{last_name}}")
|
||||
span.name {{first_name}} {{last_name}}
|
||||
.messages
|
||||
|
File diff suppressed because it is too large
Load diff
9478
services/chat/public/js/libs/jquery.js
vendored
9478
services/chat/public/js/libs/jquery.js
vendored
File diff suppressed because it is too large
Load diff
|
@ -1,85 +0,0 @@
|
|||
/*!
|
||||
* jquery.storage.js 0.0.3 - https://github.com/yckart/jquery.storage.js
|
||||
* The client-side storage for every browser, on any device.
|
||||
*
|
||||
* Copyright (c) 2012 Yannick Albert (http://yckart.com)
|
||||
* Licensed under the MIT license (http://www.opensource.org/licenses/mit-license.php).
|
||||
* 2013/02/10
|
||||
**/
|
||||
define([], function() {
|
||||
;(function($, window, document) {
|
||||
'use strict';
|
||||
|
||||
$.map(['localStorage', 'sessionStorage'], function( method ) {
|
||||
var defaults = {
|
||||
cookiePrefix : 'fallback:' + method + ':',
|
||||
cookieOptions : {
|
||||
path : '/',
|
||||
domain : document.domain,
|
||||
expires : ('localStorage' === method) ? { expires: 365 } : undefined
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
$.support[method] = method in window && window[method] !== null;
|
||||
} catch (e) {
|
||||
$.support[method] = false;
|
||||
}
|
||||
|
||||
$[method] = function(key, value) {
|
||||
var options = $.extend({}, defaults, $[method].options);
|
||||
|
||||
this.getItem = function( key ) {
|
||||
var returns = function(key){
|
||||
return JSON.parse($.support[method] ? window[method].getItem(key) : $.cookie(options.cookiePrefix + key));
|
||||
};
|
||||
if(typeof key === 'string') return returns(key);
|
||||
|
||||
var arr = [],
|
||||
i = key.length;
|
||||
while(i--) arr[i] = returns(key[i]);
|
||||
return arr;
|
||||
};
|
||||
|
||||
this.setItem = function( key, value ) {
|
||||
value = JSON.stringify(value);
|
||||
return $.support[method] ? window[method].setItem(key, value) : $.cookie(options.cookiePrefix + key, value, options.cookieOptions);
|
||||
};
|
||||
|
||||
this.removeItem = function( key ) {
|
||||
return $.support[method] ? window[method].removeItem(key) : $.cookie(options.cookiePrefix + key, null, $.extend(options.cookieOptions, {
|
||||
expires: -1
|
||||
}));
|
||||
};
|
||||
|
||||
this.clear = function() {
|
||||
if($.support[method]) {
|
||||
return window[method].clear();
|
||||
} else {
|
||||
var reg = new RegExp('^' + options.cookiePrefix, ''),
|
||||
opts = $.extend(options.cookieOptions, {
|
||||
expires: -1
|
||||
});
|
||||
|
||||
if(document.cookie && document.cookie !== ''){
|
||||
$.map(document.cookie.split(';'), function( cookie ){
|
||||
if(reg.test(cookie = $.trim(cookie))) {
|
||||
$.cookie( cookie.substr(0,cookie.indexOf('=')), null, opts);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof key !== "undefined") {
|
||||
return typeof value !== "undefined" ? ( value === null ? this.removeItem(key) : this.setItem(key, value) ) : this.getItem(key);
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
$[method].options = defaults;
|
||||
});
|
||||
}(jQuery, window, document));
|
||||
|
||||
});
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
|
@ -1,373 +0,0 @@
|
|||
/**
|
||||
* @license RequireJS text 2.0.7 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved.
|
||||
* Available via the MIT or new BSD license.
|
||||
* see: http://github.com/requirejs/text for details
|
||||
*/
|
||||
/*jslint regexp: true */
|
||||
/*global require, XMLHttpRequest, ActiveXObject,
|
||||
define, window, process, Packages,
|
||||
java, location, Components, FileUtils */
|
||||
|
||||
define(['module'], function (module) {
|
||||
'use strict';
|
||||
|
||||
var text, fs, Cc, Ci,
|
||||
progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'],
|
||||
xmlRegExp = /^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im,
|
||||
bodyRegExp = /<body[^>]*>\s*([\s\S]+)\s*<\/body>/im,
|
||||
hasLocation = typeof location !== 'undefined' && location.href,
|
||||
defaultProtocol = hasLocation && location.protocol && location.protocol.replace(/\:/, ''),
|
||||
defaultHostName = hasLocation && location.hostname,
|
||||
defaultPort = hasLocation && (location.port || undefined),
|
||||
buildMap = {},
|
||||
masterConfig = (module.config && module.config()) || {};
|
||||
|
||||
text = {
|
||||
version: '2.0.7',
|
||||
|
||||
strip: function (content) {
|
||||
//Strips <?xml ...?> declarations so that external SVG and XML
|
||||
//documents can be added to a document without worry. Also, if the string
|
||||
//is an HTML document, only the part inside the body tag is returned.
|
||||
if (content) {
|
||||
content = content.replace(xmlRegExp, "");
|
||||
var matches = content.match(bodyRegExp);
|
||||
if (matches) {
|
||||
content = matches[1];
|
||||
}
|
||||
} else {
|
||||
content = "";
|
||||
}
|
||||
return content;
|
||||
},
|
||||
|
||||
jsEscape: function (content) {
|
||||
return content.replace(/(['\\])/g, '\\$1')
|
||||
.replace(/[\f]/g, "\\f")
|
||||
.replace(/[\b]/g, "\\b")
|
||||
.replace(/[\n]/g, "\\n")
|
||||
.replace(/[\t]/g, "\\t")
|
||||
.replace(/[\r]/g, "\\r")
|
||||
.replace(/[\u2028]/g, "\\u2028")
|
||||
.replace(/[\u2029]/g, "\\u2029");
|
||||
},
|
||||
|
||||
createXhr: masterConfig.createXhr || function () {
|
||||
//Would love to dump the ActiveX crap in here. Need IE 6 to die first.
|
||||
var xhr, i, progId;
|
||||
if (typeof XMLHttpRequest !== "undefined") {
|
||||
return new XMLHttpRequest();
|
||||
} else if (typeof ActiveXObject !== "undefined") {
|
||||
for (i = 0; i < 3; i += 1) {
|
||||
progId = progIds[i];
|
||||
try {
|
||||
xhr = new ActiveXObject(progId);
|
||||
} catch (e) {}
|
||||
|
||||
if (xhr) {
|
||||
progIds = [progId]; // so faster next time
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return xhr;
|
||||
},
|
||||
|
||||
/**
|
||||
* Parses a resource name into its component parts. Resource names
|
||||
* look like: module/name.ext!strip, where the !strip part is
|
||||
* optional.
|
||||
* @param {String} name the resource name
|
||||
* @returns {Object} with properties "moduleName", "ext" and "strip"
|
||||
* where strip is a boolean.
|
||||
*/
|
||||
parseName: function (name) {
|
||||
var modName, ext, temp,
|
||||
strip = false,
|
||||
index = name.indexOf("."),
|
||||
isRelative = name.indexOf('./') === 0 ||
|
||||
name.indexOf('../') === 0;
|
||||
|
||||
if (index !== -1 && (!isRelative || index > 1)) {
|
||||
modName = name.substring(0, index);
|
||||
ext = name.substring(index + 1, name.length);
|
||||
} else {
|
||||
modName = name;
|
||||
}
|
||||
|
||||
temp = ext || modName;
|
||||
index = temp.indexOf("!");
|
||||
if (index !== -1) {
|
||||
//Pull off the strip arg.
|
||||
strip = temp.substring(index + 1) === "strip";
|
||||
temp = temp.substring(0, index);
|
||||
if (ext) {
|
||||
ext = temp;
|
||||
} else {
|
||||
modName = temp;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
moduleName: modName,
|
||||
ext: ext,
|
||||
strip: strip
|
||||
};
|
||||
},
|
||||
|
||||
xdRegExp: /^((\w+)\:)?\/\/([^\/\\]+)/,
|
||||
|
||||
/**
|
||||
* Is an URL on another domain. Only works for browser use, returns
|
||||
* false in non-browser environments. Only used to know if an
|
||||
* optimized .js version of a text resource should be loaded
|
||||
* instead.
|
||||
* @param {String} url
|
||||
* @returns Boolean
|
||||
*/
|
||||
useXhr: function (url, protocol, hostname, port) {
|
||||
var uProtocol, uHostName, uPort,
|
||||
match = text.xdRegExp.exec(url);
|
||||
if (!match) {
|
||||
return true;
|
||||
}
|
||||
uProtocol = match[2];
|
||||
uHostName = match[3];
|
||||
|
||||
uHostName = uHostName.split(':');
|
||||
uPort = uHostName[1];
|
||||
uHostName = uHostName[0];
|
||||
|
||||
return (!uProtocol || uProtocol === protocol) &&
|
||||
(!uHostName || uHostName.toLowerCase() === hostname.toLowerCase()) &&
|
||||
((!uPort && !uHostName) || uPort === port);
|
||||
},
|
||||
|
||||
finishLoad: function (name, strip, content, onLoad) {
|
||||
content = strip ? text.strip(content) : content;
|
||||
if (masterConfig.isBuild) {
|
||||
buildMap[name] = content;
|
||||
}
|
||||
onLoad(content);
|
||||
},
|
||||
|
||||
load: function (name, req, onLoad, config) {
|
||||
//Name has format: some.module.filext!strip
|
||||
//The strip part is optional.
|
||||
//if strip is present, then that means only get the string contents
|
||||
//inside a body tag in an HTML string. For XML/SVG content it means
|
||||
//removing the <?xml ...?> declarations so the content can be inserted
|
||||
//into the current doc without problems.
|
||||
|
||||
// Do not bother with the work if a build and text will
|
||||
// not be inlined.
|
||||
if (config.isBuild && !config.inlineText) {
|
||||
onLoad();
|
||||
return;
|
||||
}
|
||||
|
||||
masterConfig.isBuild = config.isBuild;
|
||||
|
||||
var parsed = text.parseName(name),
|
||||
nonStripName = parsed.moduleName +
|
||||
(parsed.ext ? '.' + parsed.ext : ''),
|
||||
url = req.toUrl(nonStripName),
|
||||
useXhr = (masterConfig.useXhr) ||
|
||||
text.useXhr;
|
||||
|
||||
//Load the text. Use XHR if possible and in a browser.
|
||||
if (!hasLocation || useXhr(url, defaultProtocol, defaultHostName, defaultPort)) {
|
||||
text.get(url, function (content) {
|
||||
text.finishLoad(name, parsed.strip, content, onLoad);
|
||||
}, function (err) {
|
||||
if (onLoad.error) {
|
||||
onLoad.error(err);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
//Need to fetch the resource across domains. Assume
|
||||
//the resource has been optimized into a JS module. Fetch
|
||||
//by the module name + extension, but do not include the
|
||||
//!strip part to avoid file system issues.
|
||||
req([nonStripName], function (content) {
|
||||
text.finishLoad(parsed.moduleName + '.' + parsed.ext,
|
||||
parsed.strip, content, onLoad);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
write: function (pluginName, moduleName, write, config) {
|
||||
if (buildMap.hasOwnProperty(moduleName)) {
|
||||
var content = text.jsEscape(buildMap[moduleName]);
|
||||
write.asModule(pluginName + "!" + moduleName,
|
||||
"define(function () { return '" +
|
||||
content +
|
||||
"';});\n");
|
||||
}
|
||||
},
|
||||
|
||||
writeFile: function (pluginName, moduleName, req, write, config) {
|
||||
var parsed = text.parseName(moduleName),
|
||||
extPart = parsed.ext ? '.' + parsed.ext : '',
|
||||
nonStripName = parsed.moduleName + extPart,
|
||||
//Use a '.js' file name so that it indicates it is a
|
||||
//script that can be loaded across domains.
|
||||
fileName = req.toUrl(parsed.moduleName + extPart) + '.js';
|
||||
|
||||
//Leverage own load() method to load plugin value, but only
|
||||
//write out values that do not have the strip argument,
|
||||
//to avoid any potential issues with ! in file names.
|
||||
text.load(nonStripName, req, function (value) {
|
||||
//Use own write() method to construct full module value.
|
||||
//But need to create shell that translates writeFile's
|
||||
//write() to the right interface.
|
||||
var textWrite = function (contents) {
|
||||
return write(fileName, contents);
|
||||
};
|
||||
textWrite.asModule = function (moduleName, contents) {
|
||||
return write.asModule(moduleName, fileName, contents);
|
||||
};
|
||||
|
||||
text.write(pluginName, nonStripName, textWrite, config);
|
||||
}, config);
|
||||
}
|
||||
};
|
||||
|
||||
if (masterConfig.env === 'node' || (!masterConfig.env &&
|
||||
typeof process !== "undefined" &&
|
||||
process.versions &&
|
||||
!!process.versions.node)) {
|
||||
//Using special require.nodeRequire, something added by r.js.
|
||||
fs = require.nodeRequire('fs');
|
||||
|
||||
text.get = function (url, callback, errback) {
|
||||
try {
|
||||
var file = fs.readFileSync(url, 'utf8');
|
||||
//Remove BOM (Byte Mark Order) from utf8 files if it is there.
|
||||
if (file.indexOf('\uFEFF') === 0) {
|
||||
file = file.substring(1);
|
||||
}
|
||||
callback(file);
|
||||
} catch (e) {
|
||||
errback(e);
|
||||
}
|
||||
};
|
||||
} else if (masterConfig.env === 'xhr' || (!masterConfig.env &&
|
||||
text.createXhr())) {
|
||||
text.get = function (url, callback, errback, headers) {
|
||||
var xhr = text.createXhr(), header;
|
||||
xhr.open('GET', url, true);
|
||||
|
||||
//Allow plugins direct access to xhr headers
|
||||
if (headers) {
|
||||
for (header in headers) {
|
||||
if (headers.hasOwnProperty(header)) {
|
||||
xhr.setRequestHeader(header.toLowerCase(), headers[header]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Allow overrides specified in config
|
||||
if (masterConfig.onXhr) {
|
||||
masterConfig.onXhr(xhr, url);
|
||||
}
|
||||
|
||||
xhr.onreadystatechange = function (evt) {
|
||||
var status, err;
|
||||
//Do not explicitly handle errors, those should be
|
||||
//visible via console output in the browser.
|
||||
if (xhr.readyState === 4) {
|
||||
status = xhr.status;
|
||||
if (status > 399 && status < 600) {
|
||||
//An http 4xx or 5xx error. Signal an error.
|
||||
err = new Error(url + ' HTTP status: ' + status);
|
||||
err.xhr = xhr;
|
||||
errback(err);
|
||||
} else {
|
||||
callback(xhr.responseText);
|
||||
}
|
||||
|
||||
if (masterConfig.onXhrComplete) {
|
||||
masterConfig.onXhrComplete(xhr, url);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.send(null);
|
||||
};
|
||||
} else if (masterConfig.env === 'rhino' || (!masterConfig.env &&
|
||||
typeof Packages !== 'undefined' && typeof java !== 'undefined')) {
|
||||
//Why Java, why is this so awkward?
|
||||
text.get = function (url, callback) {
|
||||
var stringBuffer, line,
|
||||
encoding = "utf-8",
|
||||
file = new java.io.File(url),
|
||||
lineSeparator = java.lang.System.getProperty("line.separator"),
|
||||
input = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(file), encoding)),
|
||||
content = '';
|
||||
try {
|
||||
stringBuffer = new java.lang.StringBuffer();
|
||||
line = input.readLine();
|
||||
|
||||
// Byte Order Mark (BOM) - The Unicode Standard, version 3.0, page 324
|
||||
// http://www.unicode.org/faq/utf_bom.html
|
||||
|
||||
// Note that when we use utf-8, the BOM should appear as "EF BB BF", but it doesn't due to this bug in the JDK:
|
||||
// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4508058
|
||||
if (line && line.length() && line.charAt(0) === 0xfeff) {
|
||||
// Eat the BOM, since we've already found the encoding on this file,
|
||||
// and we plan to concatenating this buffer with others; the BOM should
|
||||
// only appear at the top of a file.
|
||||
line = line.substring(1);
|
||||
}
|
||||
|
||||
if (line !== null) {
|
||||
stringBuffer.append(line);
|
||||
}
|
||||
|
||||
while ((line = input.readLine()) !== null) {
|
||||
stringBuffer.append(lineSeparator);
|
||||
stringBuffer.append(line);
|
||||
}
|
||||
//Make sure we return a JavaScript string and not a Java string.
|
||||
content = String(stringBuffer.toString()); //String
|
||||
} finally {
|
||||
input.close();
|
||||
}
|
||||
callback(content);
|
||||
};
|
||||
} else if (masterConfig.env === 'xpconnect' || (!masterConfig.env &&
|
||||
typeof Components !== 'undefined' && Components.classes &&
|
||||
Components.interfaces)) {
|
||||
//Avert your gaze!
|
||||
Cc = Components.classes,
|
||||
Ci = Components.interfaces;
|
||||
Components.utils['import']('resource://gre/modules/FileUtils.jsm');
|
||||
|
||||
text.get = function (url, callback) {
|
||||
var inStream, convertStream,
|
||||
readData = {},
|
||||
fileObj = new FileUtils.File(url);
|
||||
|
||||
//XPCOM, you so crazy
|
||||
try {
|
||||
inStream = Cc['@mozilla.org/network/file-input-stream;1']
|
||||
.createInstance(Ci.nsIFileInputStream);
|
||||
inStream.init(fileObj, 1, 0, false);
|
||||
|
||||
convertStream = Cc['@mozilla.org/intl/converter-input-stream;1']
|
||||
.createInstance(Ci.nsIConverterInputStream);
|
||||
convertStream.init(inStream, "utf-8", inStream.available(),
|
||||
Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
|
||||
|
||||
convertStream.readString(inStream.available(), readData);
|
||||
convertStream.close();
|
||||
inStream.close();
|
||||
callback(readData.value);
|
||||
} catch (e) {
|
||||
throw new Error((fileObj && fileObj.path || '') + ': ' + e);
|
||||
}
|
||||
};
|
||||
}
|
||||
return text;
|
||||
});
|
|
@ -1,198 +0,0 @@
|
|||
@border-color: #999;
|
||||
|
||||
.chat-window {
|
||||
border-bottom: none;
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: 10px;
|
||||
height: 260px;
|
||||
width: 220px;
|
||||
background-color: white;
|
||||
z-index: 10;
|
||||
|
||||
.header {
|
||||
h3 {
|
||||
background-color: rgb(40,40,40);
|
||||
border: 1px solid #222;
|
||||
border-bottom: none;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
padding: 4px 6px 5px;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
position: relative;
|
||||
.window-controls {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 4px;
|
||||
bottom: 0px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: black;
|
||||
}
|
||||
.minimize-toggle {
|
||||
width: 12px;
|
||||
height: 100%;
|
||||
padding: 0 4px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 4px center;
|
||||
background-image: url(data:image/png;base64,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);
|
||||
}
|
||||
}
|
||||
|
||||
.new-message-alert {
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
padding: 4px;
|
||||
top: -16px;
|
||||
left: 35px;
|
||||
background-color: red;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
padding: 1px 6px;
|
||||
-moz-border-radius: 3px;
|
||||
-webkit-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
&:after {
|
||||
border: 3px solid red;
|
||||
border-right: 3px solid transparent;
|
||||
border-bottom: 3px solid transparent;
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
bottom: -6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.connected-users {
|
||||
font-size: 10px;
|
||||
padding: 4px;
|
||||
background-color: #ddd;
|
||||
border: 1px solid @border-color;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.sent-message-area {
|
||||
border: 1px solid @border-color;
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
bottom: 28px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
font-size: 12px;
|
||||
font-family: arial, san-serif;
|
||||
overflow-y: scroll;
|
||||
|
||||
|
||||
|
||||
.chat-block {
|
||||
position: relative;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
|
||||
.message-block {
|
||||
min-height: 40px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.timeSinceMessage {
|
||||
color: @border-color;
|
||||
text-align:center;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
float: left;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
.name {
|
||||
font-weight: bold;
|
||||
color: #999;
|
||||
margin-top: 0;
|
||||
}
|
||||
.message {
|
||||
margin: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
a.load-older-messages {
|
||||
padding: 10px;
|
||||
background-color: #eee;
|
||||
text-align:center;
|
||||
display: block;
|
||||
&:hover {
|
||||
background-color: #ddd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@new-message-area-height: 27px;
|
||||
@new-message-area-padding-top: 3px;
|
||||
.new-message-area {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
height: @new-message-area-height;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
|
||||
border: 1px solid @border-color;
|
||||
border-bottom: none;
|
||||
|
||||
textarea {
|
||||
height: @new-message-area-height - 2 * @new-message-area-padding-top;
|
||||
width: 212px;
|
||||
border: none;
|
||||
padding: @new-message-area-padding-top;
|
||||
margin: 0;
|
||||
resize: none;
|
||||
font-size: 12px;
|
||||
font-family: arial, san-serif;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.chat-window.disconnected {
|
||||
.sent-message-area {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-window.new-messages {
|
||||
.header {
|
||||
h3 {
|
||||
background-color: #049cdb;
|
||||
border-color: darken(#049cdb, 10%);
|
||||
}
|
||||
|
||||
.new-message-alert {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.window-controls {
|
||||
&:hover {
|
||||
background-color: darken(#049cdb, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-window.minimized {
|
||||
.header {
|
||||
.window-controls {
|
||||
.minimize-toggle {
|
||||
background-image: url(data:image/png;base64,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@ expect = require("chai").expect
|
|||
async = require "async"
|
||||
crypto = require "crypto"
|
||||
|
||||
MockWebApi = require "./helpers/MockWebApi"
|
||||
ChatClient = require "./helpers/ChatClient"
|
||||
|
||||
describe "Getting messages", ->
|
||||
|
@ -12,62 +11,52 @@ describe "Getting messages", ->
|
|||
@user_id2 = ObjectId().toString()
|
||||
@content1 = "foo bar"
|
||||
@content2 = "hello world"
|
||||
MockWebApi.addUser @user_id1, @user1 = {
|
||||
id: @user_id1
|
||||
first_name: "Jane"
|
||||
last_name: "Smith"
|
||||
email: "jane@example.com"
|
||||
}
|
||||
MockWebApi.addUser @user_id2, @user2 = {
|
||||
id: @user_id2
|
||||
first_name: "John"
|
||||
last_name: "Doe"
|
||||
email: "john@example.com"
|
||||
}
|
||||
|
||||
describe "normally", ->
|
||||
describe "globally", ->
|
||||
before (done) ->
|
||||
@project_id = ObjectId().toString()
|
||||
async.series [
|
||||
(cb) => ChatClient.sendMessage @project_id, @user_id1, @content1, cb
|
||||
(cb) => ChatClient.sendMessage @project_id, @user_id2, @content2, cb
|
||||
(cb) => ChatClient.sendGlobalMessage @project_id, @user_id1, @content1, cb
|
||||
(cb) => ChatClient.sendGlobalMessage @project_id, @user_id2, @content2, cb
|
||||
], done
|
||||
|
||||
it "should contain the messages and populated users when getting the messages", (done) ->
|
||||
ChatClient.getMessages @project_id, (error, response, messages) =>
|
||||
ChatClient.getGlobalMessages @project_id, (error, response, messages) =>
|
||||
expect(messages.length).to.equal 2
|
||||
messages.reverse()
|
||||
expect(messages[0].content).to.equal @content1
|
||||
expect(messages[0].user).to.deep.equal {
|
||||
id: @user_id1
|
||||
first_name: "Jane"
|
||||
last_name: "Smith"
|
||||
email: "jane@example.com"
|
||||
gravatar_url: "//www.gravatar.com/avatar/#{crypto.createHash("md5").update("jane@example.com").digest("hex")}"
|
||||
}
|
||||
expect(messages[0].user_id).to.equal @user_id1
|
||||
expect(messages[1].content).to.equal @content2
|
||||
expect(messages[1].user).to.deep.equal {
|
||||
id: @user_id2
|
||||
first_name: "John"
|
||||
last_name: "Doe"
|
||||
email: "john@example.com"
|
||||
gravatar_url: "//www.gravatar.com/avatar/#{crypto.createHash("md5").update("john@example.com").digest("hex")}"
|
||||
}
|
||||
expect(messages[1].user_id).to.equal @user_id2
|
||||
done()
|
||||
|
||||
describe "when a user doesn't exit", ->
|
||||
describe "from all the threads", ->
|
||||
before (done) ->
|
||||
@project_id = ObjectId().toString()
|
||||
@user_id3 = ObjectId().toString()
|
||||
@thread_id1 = ObjectId().toString()
|
||||
@thread_id2 = ObjectId().toString()
|
||||
async.series [
|
||||
(cb) => ChatClient.sendMessage @project_id, @user_id3, @content1, cb
|
||||
(cb) => ChatClient.sendMessage @project_id, @user_id2, @content2, cb
|
||||
(cb) => ChatClient.sendMessage @project_id, @thread_id1, @user_id1, "one", cb
|
||||
(cb) => ChatClient.sendMessage @project_id, @thread_id2, @user_id2, "two", cb
|
||||
(cb) => ChatClient.sendMessage @project_id, @thread_id1, @user_id1, "three", cb
|
||||
(cb) => ChatClient.sendMessage @project_id, @thread_id2, @user_id2, "four", cb
|
||||
], done
|
||||
|
||||
it "should just return null for the user", (done) ->
|
||||
ChatClient.getMessages @project_id, (error, response, messages) =>
|
||||
expect(messages.length).to.equal 2
|
||||
messages.reverse()
|
||||
expect(messages[0].content).to.equal @content1
|
||||
expect(messages[0].user).to.equal null
|
||||
done()
|
||||
it "should contain a dictionary of threads with messages with populated users", (done) ->
|
||||
ChatClient.getThreads @project_id, (error, response, threads) =>
|
||||
expect(Object.keys(threads).length).to.equal 2
|
||||
thread1 = threads[@thread_id1]
|
||||
expect(thread1.messages.length).to.equal 2
|
||||
thread2 = threads[@thread_id2]
|
||||
expect(thread2.messages.length).to.equal 2
|
||||
|
||||
expect(thread1.messages[0].content).to.equal "one"
|
||||
expect(thread1.messages[0].user_id).to.equal @user_id1
|
||||
expect(thread1.messages[1].content).to.equal "three"
|
||||
expect(thread1.messages[1].user_id).to.equal @user_id1
|
||||
|
||||
expect(thread2.messages[0].content).to.equal "two"
|
||||
expect(thread2.messages[0].user_id).to.equal @user_id2
|
||||
expect(thread2.messages[1].content).to.equal "four"
|
||||
expect(thread2.messages[1].user_id).to.equal @user_id2
|
||||
done()
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
{ObjectId} = require "../../../app/js/mongojs"
|
||||
expect = require("chai").expect
|
||||
crypto = require "crypto"
|
||||
|
||||
ChatClient = require "./helpers/ChatClient"
|
||||
|
||||
describe "Resolving a thread", ->
|
||||
before ->
|
||||
@project_id = ObjectId().toString()
|
||||
@user_id = ObjectId().toString()
|
||||
describe "with a resolved thread", ->
|
||||
before (done) ->
|
||||
@thread_id = ObjectId().toString()
|
||||
@content = "resolved message"
|
||||
ChatClient.sendMessage @project_id, @thread_id, @user_id, @content, (error, response, body) =>
|
||||
expect(error).to.be.null
|
||||
expect(response.statusCode).to.equal 201
|
||||
ChatClient.resolveThread @project_id, @thread_id, @user_id, (error, response, body) =>
|
||||
expect(error).to.be.null
|
||||
expect(response.statusCode).to.equal 204
|
||||
done()
|
||||
|
||||
it "should then list the thread as resolved", (done) ->
|
||||
ChatClient.getThreads @project_id, (error, response, threads) =>
|
||||
expect(error).to.be.null
|
||||
expect(response.statusCode).to.equal 200
|
||||
expect(threads[@thread_id].resolved).to.equal true
|
||||
expect(threads[@thread_id].resolved_by_user_id).to.equal @user_id
|
||||
resolved_at = new Date(threads[@thread_id].resolved_at)
|
||||
expect(new Date() - resolved_at).to.be.below 1000
|
||||
done()
|
||||
|
||||
describe "when a thread is not resolved", ->
|
||||
before (done) ->
|
||||
@thread_id = ObjectId().toString()
|
||||
@content = "open message"
|
||||
ChatClient.sendMessage @project_id, @thread_id, @user_id, @content, (error, response, body) =>
|
||||
expect(error).to.be.null
|
||||
expect(response.statusCode).to.equal 201
|
||||
done()
|
||||
|
||||
it "should not list the thread as resolved", (done) ->
|
||||
ChatClient.getThreads @project_id, (error, response, threads) =>
|
||||
expect(error).to.be.null
|
||||
expect(response.statusCode).to.equal 200
|
||||
expect(threads[@thread_id].resolved).to.be.undefined
|
||||
done()
|
||||
|
||||
describe "when a thread is resolved then reopened", ->
|
||||
before (done) ->
|
||||
@thread_id = ObjectId().toString()
|
||||
@content = "resolved message"
|
||||
ChatClient.sendMessage @project_id, @thread_id, @user_id, @content, (error, response, body) =>
|
||||
expect(error).to.be.null
|
||||
expect(response.statusCode).to.equal 201
|
||||
ChatClient.resolveThread @project_id, @thread_id, @user_id, (error, response, body) =>
|
||||
expect(error).to.be.null
|
||||
expect(response.statusCode).to.equal 204
|
||||
ChatClient.reopenThread @project_id, @thread_id, (error, response, body) =>
|
||||
expect(error).to.be.null
|
||||
expect(response.statusCode).to.equal 204
|
||||
done()
|
||||
|
||||
it "should not list the thread as resolved", (done) ->
|
||||
ChatClient.getThreads @project_id, (error, response, threads) =>
|
||||
expect(error).to.be.null
|
||||
expect(response.statusCode).to.equal 200
|
||||
expect(threads[@thread_id].resolved).to.be.undefined
|
||||
done()
|
|
@ -1,23 +1,69 @@
|
|||
{ObjectId} = require "../../../app/js/mongojs"
|
||||
expect = require("chai").expect
|
||||
|
||||
MockWebApi = require "./helpers/MockWebApi"
|
||||
ChatClient = require "./helpers/ChatClient"
|
||||
|
||||
describe "Sending a message", ->
|
||||
before (done) ->
|
||||
before ->
|
||||
@project_id = ObjectId().toString()
|
||||
@user_id = ObjectId().toString()
|
||||
@content = "foo bar"
|
||||
ChatClient.sendMessage @project_id, @user_id, @content, (error, response, body) ->
|
||||
expect(error).to.be.null
|
||||
expect(response.statusCode).to.equal 201
|
||||
done()
|
||||
@thread_id = ObjectId().toString()
|
||||
|
||||
describe "globally", ->
|
||||
before (done) ->
|
||||
@content = "global message"
|
||||
ChatClient.sendGlobalMessage @project_id, @user_id, @content, (error, response, body) =>
|
||||
expect(error).to.be.null
|
||||
expect(response.statusCode).to.equal 201
|
||||
expect(body.content).to.equal @content
|
||||
expect(body.user_id).to.equal @user_id
|
||||
expect(body.room_id).to.equal @project_id
|
||||
done()
|
||||
|
||||
it "should then list the message in the project messages", (done) ->
|
||||
ChatClient.getGlobalMessages @project_id, (error, response, messages) =>
|
||||
expect(error).to.be.null
|
||||
expect(response.statusCode).to.equal 200
|
||||
expect(messages.length).to.equal 1
|
||||
expect(messages[0].content).to.equal @content
|
||||
done()
|
||||
|
||||
describe "to a thread", ->
|
||||
before (done) ->
|
||||
@content = "thread message"
|
||||
ChatClient.sendMessage @project_id, @thread_id, @user_id, @content, (error, response, body) =>
|
||||
expect(error).to.be.null
|
||||
expect(response.statusCode).to.equal 201
|
||||
expect(body.content).to.equal @content
|
||||
expect(body.user_id).to.equal @user_id
|
||||
expect(body.room_id).to.equal @project_id
|
||||
done()
|
||||
|
||||
it "should then list the message in the threads", (done) ->
|
||||
ChatClient.getThreads @project_id, (error, response, threads) =>
|
||||
expect(error).to.be.null
|
||||
expect(response.statusCode).to.equal 200
|
||||
expect(threads[@thread_id].messages.length).to.equal 1
|
||||
expect(threads[@thread_id].messages[0].content).to.equal @content
|
||||
done()
|
||||
|
||||
it "should then list the message in project messages", (done) ->
|
||||
ChatClient.getMessages @project_id, (error, response, messages) =>
|
||||
expect(error).to.be.null
|
||||
expect(response.statusCode).to.equal 200
|
||||
expect(messages.length).to.equal 1
|
||||
expect(messages[0].content).to.equal @content
|
||||
done()
|
||||
describe "with a malformed user_id", ->
|
||||
it "should return a graceful error", (done) ->
|
||||
ChatClient.sendMessage @project_id, @thread_id, "malformed-user", "content", (error, response, body) =>
|
||||
expect(response.statusCode).to.equal 400
|
||||
expect(body).to.equal "Invalid user_id"
|
||||
done()
|
||||
|
||||
describe "with a malformed project_id", ->
|
||||
it "should return a graceful error", (done) ->
|
||||
ChatClient.sendMessage "malformed-project", @thread_id, @user_id, "content", (error, response, body) =>
|
||||
expect(response.statusCode).to.equal 400
|
||||
expect(body).to.equal "Invalid project_id"
|
||||
done()
|
||||
|
||||
describe "with a malformed thread_id", ->
|
||||
it "should return a graceful error", (done) ->
|
||||
ChatClient.sendMessage @project_id, "malformed-thread-id", @user_id, "content", (error, response, body) =>
|
||||
expect(response.statusCode).to.equal 400
|
||||
expect(body).to.equal "Invalid thread_id"
|
||||
done()
|
|
@ -1,16 +1,43 @@
|
|||
request = require("request").defaults({baseUrl: "http://localhost:3010"})
|
||||
|
||||
module.exports =
|
||||
sendMessage: (project_id, user_id, content, callback) ->
|
||||
sendGlobalMessage: (project_id, user_id, content, callback) ->
|
||||
request.post {
|
||||
url: "/room/#{project_id}/messages"
|
||||
url: "/project/#{project_id}/messages"
|
||||
json:
|
||||
user_id: user_id
|
||||
content: content
|
||||
}, callback
|
||||
|
||||
getMessages: (project_id, callback) ->
|
||||
getGlobalMessages: (project_id, callback) ->
|
||||
request.get {
|
||||
url: "/room/#{project_id}/messages",
|
||||
url: "/project/#{project_id}/messages",
|
||||
json: true
|
||||
}, callback
|
||||
|
||||
sendMessage: (project_id, thread_id, user_id, content, callback) ->
|
||||
request.post {
|
||||
url: "/project/#{project_id}/thread/#{thread_id}/messages"
|
||||
json:
|
||||
user_id: user_id
|
||||
content: content
|
||||
}, callback
|
||||
|
||||
getThreads: (project_id, callback) ->
|
||||
request.get {
|
||||
url: "/project/#{project_id}/threads",
|
||||
json: true
|
||||
}, callback
|
||||
|
||||
resolveThread: (project_id, thread_id, user_id, callback) ->
|
||||
request.post {
|
||||
url: "/project/#{project_id}/thread/#{thread_id}/resolve",
|
||||
json: {
|
||||
user_id: user_id
|
||||
}
|
||||
}, callback
|
||||
|
||||
reopenThread: (project_id, thread_id, callback) ->
|
||||
request.post {
|
||||
url: "/project/#{project_id}/thread/#{thread_id}/reopen",
|
||||
}, callback
|
|
@ -1,27 +0,0 @@
|
|||
express = require("express")
|
||||
app = express()
|
||||
|
||||
module.exports = MockWebApi =
|
||||
users: {}
|
||||
|
||||
addUser: (user_id, user) ->
|
||||
@users[user_id] = user
|
||||
|
||||
getUser: (user_id, callback = (error, user) ->) ->
|
||||
return callback null, @users[user_id]
|
||||
|
||||
run: () ->
|
||||
app.get "/user/:user_id/personal_info", (req, res, next) =>
|
||||
@getUser req.params.user_id, (error, user) ->
|
||||
if error?
|
||||
res.send 500
|
||||
else if user?
|
||||
res.send JSON.stringify user
|
||||
else
|
||||
res.send 404
|
||||
|
||||
app.listen 3000, (error) ->
|
||||
throw error if error?
|
||||
|
||||
MockWebApi.run()
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
sinon = require('sinon')
|
||||
chai = require('chai')
|
||||
should = chai.should()
|
||||
expect = chai.expect
|
||||
modulePath = "../../../../app/js/Features/Messages/MessageFormatter.js"
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
events = require "events"
|
||||
ObjectId = require("mongojs").ObjectId
|
||||
|
||||
describe "MessageFormatter", ->
|
||||
beforeEach ->
|
||||
@MessageFormatter = SandboxedModule.require modulePath, requires: {}
|
||||
|
|
@ -1,173 +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/Messages/MessageHttpController"
|
||||
expect = require("chai").expect
|
||||
tk = require("timekeeper")
|
||||
|
||||
describe "MessagesHttpController", ->
|
||||
|
||||
beforeEach ->
|
||||
|
||||
@settings = {}
|
||||
@date = Date.now()
|
||||
tk.freeze(@date)
|
||||
@MessagesHttpController = SandboxedModule.require modulePath, requires:
|
||||
"settings-sharelatex":@settings
|
||||
"logger-sharelatex": log:->
|
||||
"./MessageManager": @MessageManager = {}
|
||||
"./MessageFormatter": @MessageFormatter = {}
|
||||
"../Rooms/RoomManager": @RoomManager = {}
|
||||
|
||||
@req =
|
||||
body:{}
|
||||
@res = {}
|
||||
@project_id = "12321321"
|
||||
@room_id = "Asdfadf adfafd"
|
||||
@user_id = "09832910838239081203981"
|
||||
@content = "my message here"
|
||||
|
||||
|
||||
afterEach ->
|
||||
tk.reset()
|
||||
|
||||
describe "sendMessage", ->
|
||||
|
||||
beforeEach ->
|
||||
@initialMessage = {content:@content}
|
||||
@MessageManager.createMessage = sinon.stub().callsArgWith(1, null, @initialMessage)
|
||||
@req.params =
|
||||
project_id:@project_id
|
||||
@req.body =
|
||||
user_id:@user_id
|
||||
content:@content
|
||||
@singlePopulatedMessage = {data:"here"}
|
||||
@MessageManager.populateMessagesWithUsers = sinon.stub().callsArgWith(1, null, [@singlePopulatedMessage])
|
||||
@RoomManager.findOrCreateRoom = sinon.stub().callsArgWith(1, null, @room = { _id : @room_id })
|
||||
@formattedMessage = {formatted:true}
|
||||
@MessageFormatter.formatMessageForClientSide = sinon.stub().returns(@formattedMessage)
|
||||
|
||||
it "should look up the room for the project", ->
|
||||
@res.send = =>
|
||||
@RoomManager.findOrCreateRoom
|
||||
.calledWith({
|
||||
project_id: @project_id
|
||||
})
|
||||
.should.equal true
|
||||
done()
|
||||
|
||||
it "should create the message with the message manager", (done)->
|
||||
@res.send = =>
|
||||
@MessageManager.createMessage
|
||||
.calledWith({
|
||||
content: @content
|
||||
user_id: @user_id
|
||||
room_id: @room_id
|
||||
timestamp: @date
|
||||
})
|
||||
.should.equal true
|
||||
done()
|
||||
@MessagesHttpController.sendMessage @req, @res
|
||||
|
||||
|
||||
it "should return the formetted message", (done)->
|
||||
|
||||
@res.send = (code, data)=>
|
||||
assert.deepEqual @MessageManager.populateMessagesWithUsers.args[0][0], [@initialMessage]
|
||||
code.should.equal 201
|
||||
data.should.equal @formattedMessage
|
||||
done()
|
||||
|
||||
@MessagesHttpController.sendMessage @req, @res
|
||||
|
||||
|
||||
describe "getMessages", ->
|
||||
|
||||
beforeEach ->
|
||||
@project_id = "room-id-123"
|
||||
@timestamp = Date.now()
|
||||
@limit = 42
|
||||
|
||||
@messages = "messages without users stub"
|
||||
@messagesWithUsers = "messages with users stub"
|
||||
@formattedMessages = "formatted messages stub"
|
||||
@RoomManager.findOrCreateRoom = sinon.stub().callsArgWith(1, null, @room = { _id : @room_id })
|
||||
@MessageManager.getMessages = sinon.stub().callsArgWith(2, null, @messages)
|
||||
@MessageManager.populateMessagesWithUsers = sinon.stub().callsArgWith(1, null, @messagesWithUsers)
|
||||
@MessageFormatter.formatMessagesForClientSide = sinon.stub().returns @formattedMessages
|
||||
|
||||
|
||||
describe "with a timestamp and limit", ->
|
||||
beforeEach ->
|
||||
@req.params =
|
||||
project_id:@project_id
|
||||
@req.query =
|
||||
before: @timestamp,
|
||||
limit: "#{@limit}"
|
||||
|
||||
|
||||
it "should look up the room for the project", ->
|
||||
@res.send = =>
|
||||
@RoomManager.findOrCreateRoom
|
||||
.calledWith({
|
||||
project_id: @project_id
|
||||
})
|
||||
.should.equal true
|
||||
done()
|
||||
|
||||
it "should get the requested messages", ->
|
||||
@res.send = =>
|
||||
@MessageManager.getMessages
|
||||
.calledWith({
|
||||
timestamp: $lt: @timestamp
|
||||
room_id: @room_id
|
||||
}, {
|
||||
limit: @limit
|
||||
order_by: "timestamp"
|
||||
sort_order: -1
|
||||
})
|
||||
.should.equal true
|
||||
|
||||
@MessagesHttpController.getMessages(@req, @res)
|
||||
|
||||
it "should populate the messages with the users", (done)->
|
||||
@res.send = =>
|
||||
@MessageManager.populateMessagesWithUsers.calledWith(@messages).should.equal true
|
||||
done()
|
||||
|
||||
@MessagesHttpController.getMessages(@req, @res)
|
||||
|
||||
it "should return the formatted messages", (done)->
|
||||
@res.send = ()=>
|
||||
@MessageFormatter.formatMessagesForClientSide.calledWith(@messagesWithUsers).should.equal true
|
||||
done()
|
||||
@MessagesHttpController.getMessages(@req, @res)
|
||||
|
||||
it "should send the formated messages back with a 200", (done)->
|
||||
@res.send = (code, data)=>
|
||||
code.should.equal 200
|
||||
data.should.equal @formattedMessages
|
||||
done()
|
||||
@MessagesHttpController.getMessages(@req, @res)
|
||||
|
||||
describe "without a timestamp or limit", ->
|
||||
beforeEach ->
|
||||
@req.params =
|
||||
project_id:@project_id
|
||||
|
||||
|
||||
it "should get a default number of messages from the beginning", ->
|
||||
@res.send = =>
|
||||
@MessageManager.getMessages
|
||||
.calledWith({
|
||||
room_id: @room_id
|
||||
}, {
|
||||
limit: @MessagesHttpController.DEFAULT_MESSAGE_LIMIT
|
||||
order_by: "timestamp"
|
||||
sort_order: -1
|
||||
})
|
||||
.should.equal true
|
||||
|
||||
@MessagesHttpController.getMessages(@req, @res)
|
|
@ -1,61 +0,0 @@
|
|||
sinon = require('sinon')
|
||||
chai = require('chai')
|
||||
should = chai.should()
|
||||
expect = chai.expect
|
||||
modulePath = "../../../../app/js/Features/Messages/MessageManager.js"
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
events = require "events"
|
||||
ObjectId = require("mongojs").ObjectId
|
||||
|
||||
describe "MessageManager", ->
|
||||
beforeEach ->
|
||||
@MessageManager = SandboxedModule.require modulePath, requires:
|
||||
"../WebApi/WebApiManager": @WebApiManager = {}
|
||||
"../../mongojs": {}
|
||||
@callback = sinon.stub()
|
||||
|
||||
describe "populateMessagesWithUsers", ->
|
||||
beforeEach ->
|
||||
@user0 =
|
||||
id: ObjectId().toString()
|
||||
first_name: "Adam"
|
||||
@user1 =
|
||||
id: ObjectId().toString()
|
||||
first_name: "Eve"
|
||||
@users = {}
|
||||
@users[@user0.id] = @user0
|
||||
@users[@user1.id] = @user1
|
||||
@messages = [{
|
||||
content: "First message content"
|
||||
user_id: ObjectId(@user0.id)
|
||||
}, {
|
||||
content: "Second message content"
|
||||
user_id: ObjectId(@user0.id)
|
||||
}, {
|
||||
content: "Third message content"
|
||||
user_id: ObjectId(@user1.id)
|
||||
}]
|
||||
@WebApiManager.getUserDetails = (user_id, callback = (error, user) ->) =>
|
||||
callback null, @users[user_id]
|
||||
sinon.spy @WebApiManager, "getUserDetails"
|
||||
@MessageManager.populateMessagesWithUsers @messages, @callback
|
||||
|
||||
it "should insert user objects in the place of user_ids", ->
|
||||
messages = @callback.args[0][1]
|
||||
expect(messages).to.deep.equal [{
|
||||
content: "First message content"
|
||||
user: @user0
|
||||
}, {
|
||||
content: "Second message content"
|
||||
user: @user0
|
||||
}, {
|
||||
content: "Third message content"
|
||||
user: @user1
|
||||
}]
|
||||
|
||||
it "should call getUserDetails once and only once for each user", ->
|
||||
@WebApiManager.getUserDetails.calledWith(@user0.id).should.equal true
|
||||
@WebApiManager.getUserDetails.calledWith(@user1.id).should.equal true
|
||||
@WebApiManager.getUserDetails.calledTwice.should.equal true
|
||||
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
sinon = require('sinon')
|
||||
chai = require('chai')
|
||||
should = chai.should()
|
||||
expect = chai.expect
|
||||
modulePath = "../../../../app/js/Features/Rooms/RoomManager.js"
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
events = require "events"
|
||||
mongojs = require "mongojs"
|
||||
ObjectId = mongojs.ObjectId
|
||||
|
||||
describe "RoomManager", ->
|
||||
beforeEach ->
|
||||
@RoomManager = SandboxedModule.require modulePath, requires:
|
||||
"../../mongojs":
|
||||
db: @db = { rooms: {} }
|
||||
ObjectId: ObjectId
|
||||
@callback = sinon.stub()
|
||||
|
||||
describe "findOrCreateRoom", ->
|
||||
describe "when the room exists", ->
|
||||
beforeEach ->
|
||||
@project_id = ObjectId().toString()
|
||||
@room =
|
||||
_id: ObjectId()
|
||||
project_id: ObjectId(@project_id)
|
||||
@db.rooms.findOne = sinon.stub().callsArgWith(1, null, @room)
|
||||
@RoomManager.findOrCreateRoom(project_id: @project_id, @callback)
|
||||
|
||||
it "should look up the room based on the query", ->
|
||||
@db.rooms.findOne
|
||||
.calledWith(project_id: ObjectId(@project_id))
|
||||
.should.equal true
|
||||
|
||||
it "should return the room in the callback", ->
|
||||
@callback
|
||||
.calledWith(null, @room)
|
||||
.should.equal true
|
||||
|
||||
describe "when the room does not exist", ->
|
||||
beforeEach ->
|
||||
@project_id = ObjectId().toString()
|
||||
@room =
|
||||
_id: ObjectId()
|
||||
project_id: ObjectId(@project_id)
|
||||
@db.rooms.findOne = sinon.stub().callsArgWith(1, null, null)
|
||||
@db.rooms.save = sinon.stub().callsArgWith(1, null, @room)
|
||||
@RoomManager.findOrCreateRoom(project_id: @project_id, @callback)
|
||||
|
||||
it "should create the room", ->
|
||||
@db.rooms.save
|
||||
.calledWith(project_id: ObjectId(@project_id))
|
||||
.should.equal true
|
||||
|
||||
it "should return the room in the callback", ->
|
||||
@callback
|
||||
.calledWith(null, @room)
|
||||
.should.equal true
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
sinon = require('sinon')
|
||||
chai = require('chai')
|
||||
should = chai.should()
|
||||
expect = chai.expect
|
||||
modulePath = "../../../../app/js/Features/Users/UserFormatter.js"
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
events = require "events"
|
||||
|
||||
describe "UserFormatter", ->
|
||||
beforeEach ->
|
||||
@UserFormatter = SandboxedModule.require modulePath, requires: {}
|
||||
|
Loading…
Reference in a new issue