Merge branch 'master' into node-6.9
1
services/web/.nvmrc
Normal file
|
@ -0,0 +1 @@
|
|||
6.9.5
|
|
@ -1,7 +1,15 @@
|
|||
AnalyticsManager = require "./AnalyticsManager"
|
||||
Errors = require "../Errors/Errors"
|
||||
AuthenticationController = require("../Authentication/AuthenticationController")
|
||||
|
||||
module.exports = AnalyticsController =
|
||||
recordEvent: (req, res, next) ->
|
||||
AnalyticsManager.recordEvent req.session?.user?._id, req.params.event, req.body, (error) ->
|
||||
return next(error) if error?
|
||||
res.send 204
|
||||
user_id = AuthenticationController.getLoggedInUserId(req) or req.sessionID
|
||||
AnalyticsManager.recordEvent user_id, req.params.event, req.body, (error) ->
|
||||
if error instanceof Errors.ServiceNotConfiguredError
|
||||
# ignore, no-op
|
||||
return res.send(204)
|
||||
else if error?
|
||||
return next(error)
|
||||
else
|
||||
return res.send 204
|
||||
|
|
|
@ -2,6 +2,7 @@ settings = require "settings-sharelatex"
|
|||
logger = require "logger-sharelatex"
|
||||
_ = require "underscore"
|
||||
request = require "request"
|
||||
Errors = require '../Errors/Errors'
|
||||
|
||||
|
||||
makeRequest = (opts, callback)->
|
||||
|
@ -10,12 +11,20 @@ makeRequest = (opts, callback)->
|
|||
opts.url = "#{settings.apis.analytics.url}#{urlPath}"
|
||||
request opts, callback
|
||||
else
|
||||
callback()
|
||||
|
||||
callback(new Errors.ServiceNotConfiguredError('Analytics service not configured'))
|
||||
|
||||
|
||||
module.exports =
|
||||
|
||||
identifyUser: (user_id, old_user_id, callback = (error)->)->
|
||||
opts =
|
||||
body:
|
||||
old_user_id:old_user_id
|
||||
json:true
|
||||
method:"POST"
|
||||
timeout:1000
|
||||
url: "/user/#{user_id}/identify"
|
||||
makeRequest opts, callback
|
||||
|
||||
recordEvent: (user_id, event, segmentation = {}, callback = (error) ->) ->
|
||||
if user_id+"" == settings.smokeTest?.userId+""
|
||||
|
|
|
@ -2,7 +2,7 @@ AuthenticationManager = require ("./AuthenticationManager")
|
|||
LoginRateLimiter = require("../Security/LoginRateLimiter")
|
||||
UserGetter = require "../User/UserGetter"
|
||||
UserUpdater = require "../User/UserUpdater"
|
||||
Metrics = require('../../infrastructure/Metrics')
|
||||
Metrics = require('metrics-sharelatex')
|
||||
logger = require("logger-sharelatex")
|
||||
querystring = require('querystring')
|
||||
Url = require("url")
|
||||
|
@ -87,6 +87,7 @@ module.exports = AuthenticationController =
|
|||
LoginRateLimiter.recordSuccessfulLogin(email)
|
||||
AuthenticationController._recordSuccessfulLogin(user._id)
|
||||
Analytics.recordEvent(user._id, "user-logged-in", {ip:req.ip})
|
||||
Analytics.identifyUser(user._id, req.sessionID)
|
||||
logger.log email: email, user_id: user._id.toString(), "successful log in"
|
||||
req.session.justLoggedIn = true
|
||||
# capture the request ip for use when creating the session
|
||||
|
|
|
@ -4,6 +4,8 @@ User = require("../../models/User").User
|
|||
PrivilegeLevels = require("./PrivilegeLevels")
|
||||
PublicAccessLevels = require("./PublicAccessLevels")
|
||||
Errors = require("../Errors/Errors")
|
||||
ObjectId = require("mongojs").ObjectId
|
||||
|
||||
|
||||
module.exports = AuthorizationManager =
|
||||
# Get the privilege level that the user has for the project
|
||||
|
@ -13,6 +15,8 @@ module.exports = AuthorizationManager =
|
|||
# * becausePublic: true if the access level is only because the project is public.
|
||||
getPrivilegeLevelForProject: (user_id, project_id, callback = (error, privilegeLevel, becausePublic) ->) ->
|
||||
getPublicAccessLevel = () ->
|
||||
if !ObjectId.isValid(project_id)
|
||||
return callback(new Error("invalid project id"))
|
||||
Project.findOne { _id: project_id }, { publicAccesLevel: 1 }, (error, project) ->
|
||||
return callback(error) if error?
|
||||
if !project?
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
User = require("../../models/User").User
|
||||
logger = require 'logger-sharelatex'
|
||||
metrics = require("../../infrastructure/Metrics")
|
||||
metrics = require("metrics-sharelatex")
|
||||
|
||||
module.exports = BetaProgramHandler =
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ module.exports = BlogController =
|
|||
url = req.url?.toLowerCase()
|
||||
blogUrl = "#{settings.apis.blog.url}#{url}"
|
||||
|
||||
extensionsToProxy = [".png", ".xml", ".jpeg", ".json", ".zip", ".eps", ".gif"]
|
||||
extensionsToProxy = [".png", ".xml", ".jpeg", ".jpg", ".json", ".zip", ".eps", ".gif"]
|
||||
|
||||
shouldProxy = _.find extensionsToProxy, (extension)->
|
||||
url.indexOf(extension) != -1
|
||||
|
@ -42,4 +42,4 @@ module.exports = BlogController =
|
|||
upstream = request.get(originUrl)
|
||||
upstream.on "error", (error) ->
|
||||
logger.error err: error, "blog proxy error"
|
||||
upstream.pipe res
|
||||
upstream.pipe res
|
||||
|
|
|
@ -10,7 +10,7 @@ module.exports = BlogHandler =
|
|||
opts =
|
||||
url:blogUrl
|
||||
json:true
|
||||
timeout:500
|
||||
timeout:1000
|
||||
request.get opts, (err, res, announcements)->
|
||||
if err?
|
||||
return callback err
|
||||
|
@ -18,7 +18,6 @@ module.exports = BlogHandler =
|
|||
return callback("blog announcement returned non 200")
|
||||
logger.log announcementsLength: announcements?.length, "announcements returned"
|
||||
announcements = _.map announcements, (announcement)->
|
||||
announcement.url = "/blog#{announcement.url}"
|
||||
announcement.date = new Date(announcement.date)
|
||||
return announcement
|
||||
callback(err, announcements)
|
||||
|
|
|
@ -4,9 +4,9 @@ logger = require("logger-sharelatex")
|
|||
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
UserInfoManager = require('../User/UserInfoManager')
|
||||
UserInfoController = require('../User/UserInfoController')
|
||||
CommentsController = require('../Comments/CommentsController')
|
||||
async = require "async"
|
||||
|
||||
module.exports =
|
||||
module.exports = ChatController =
|
||||
sendMessage: (req, res, next)->
|
||||
project_id = req.params.project_id
|
||||
content = req.body.content
|
||||
|
@ -28,7 +28,38 @@ module.exports =
|
|||
logger.log project_id:project_id, query:query, "getting messages"
|
||||
ChatApiHandler.getGlobalMessages project_id, query.limit, query.before, (err, messages) ->
|
||||
return next(err) if err?
|
||||
CommentsController._injectUserInfoIntoThreads {global: { messages: messages }}, (err) ->
|
||||
ChatController._injectUserInfoIntoThreads {global: { messages: messages }}, (err) ->
|
||||
return next(err) if err?
|
||||
logger.log length: messages?.length, "sending messages to client"
|
||||
res.json messages
|
||||
|
||||
_injectUserInfoIntoThreads: (threads, callback = (error, threads) ->) ->
|
||||
userCache = {}
|
||||
getUserDetails = (user_id, callback = (error, user) ->) ->
|
||||
return callback(null, userCache[user_id]) if userCache[user_id]?
|
||||
UserInfoManager.getPersonalInfo user_id, (err, user) ->
|
||||
return callback(error) if error?
|
||||
user = UserInfoController.formatPersonalInfo user
|
||||
userCache[user_id] = user
|
||||
callback null, user
|
||||
|
||||
jobs = []
|
||||
for thread_id, thread of threads
|
||||
do (thread) ->
|
||||
if thread.resolved
|
||||
jobs.push (cb) ->
|
||||
getUserDetails thread.resolved_by_user_id, (error, user) ->
|
||||
cb(error) if error?
|
||||
thread.resolved_by_user = user
|
||||
cb()
|
||||
for message in thread.messages
|
||||
do (message) ->
|
||||
jobs.push (cb) ->
|
||||
getUserDetails message.user_id, (error, user) ->
|
||||
cb(error) if error?
|
||||
message.user = user
|
||||
cb()
|
||||
|
||||
async.series jobs, (error) ->
|
||||
return callback(error) if error?
|
||||
return callback null, threads
|
|
@ -1,111 +0,0 @@
|
|||
ChatApiHandler = require("../Chat/ChatApiHandler")
|
||||
EditorRealTimeController = require("../Editor/EditorRealTimeController")
|
||||
logger = require("logger-sharelatex")
|
||||
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
UserInfoManager = require('../User/UserInfoManager')
|
||||
UserInfoController = require('../User/UserInfoController')
|
||||
DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
|
||||
async = require "async"
|
||||
|
||||
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?
|
||||
UserInfoManager.getPersonalInfo comment.user_id, (err, user) ->
|
||||
return next(err) if err?
|
||||
comment.user = UserInfoController.formatPersonalInfo(user)
|
||||
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?
|
||||
CommentsController._injectUserInfoIntoThreads threads, (error, threads) ->
|
||||
return next(err) if err?
|
||||
res.json threads
|
||||
|
||||
resolveThread: (req, res, next) ->
|
||||
{project_id, thread_id} = req.params
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
logger.log {project_id, thread_id, user_id}, "resolving comment thread"
|
||||
ChatApiHandler.resolveThread project_id, thread_id, user_id, (err) ->
|
||||
return next(err) if err?
|
||||
UserInfoManager.getPersonalInfo user_id, (err, user) ->
|
||||
return next(err) if err?
|
||||
EditorRealTimeController.emitToRoom project_id, "resolve-thread", thread_id, UserInfoController.formatPersonalInfo(user), (err)->
|
||||
res.send 204
|
||||
|
||||
reopenThread: (req, res, next) ->
|
||||
{project_id, thread_id} = req.params
|
||||
logger.log {project_id, thread_id}, "reopening comment thread"
|
||||
ChatApiHandler.reopenThread project_id, thread_id, (err, threads) ->
|
||||
return next(err) if err?
|
||||
EditorRealTimeController.emitToRoom project_id, "reopen-thread", thread_id, (err)->
|
||||
res.send 204
|
||||
|
||||
deleteThread: (req, res, next) ->
|
||||
{project_id, doc_id, thread_id} = req.params
|
||||
logger.log {project_id, doc_id, thread_id}, "deleting comment thread"
|
||||
DocumentUpdaterHandler.deleteThread project_id, doc_id, thread_id, (err) ->
|
||||
return next(err) if err?
|
||||
ChatApiHandler.deleteThread project_id, thread_id, (err, threads) ->
|
||||
return next(err) if err?
|
||||
EditorRealTimeController.emitToRoom project_id, "delete-thread", thread_id, (err)->
|
||||
res.send 204
|
||||
|
||||
editMessage: (req, res, next) ->
|
||||
{project_id, thread_id, message_id} = req.params
|
||||
{content} = req.body
|
||||
logger.log {project_id, thread_id, message_id}, "editing message thread"
|
||||
ChatApiHandler.editMessage project_id, thread_id, message_id, content, (err) ->
|
||||
return next(err) if err?
|
||||
EditorRealTimeController.emitToRoom project_id, "edit-message", thread_id, message_id, content, (err)->
|
||||
res.send 204
|
||||
|
||||
deleteMessage: (req, res, next) ->
|
||||
{project_id, thread_id, message_id} = req.params
|
||||
logger.log {project_id, thread_id, message_id}, "deleting message"
|
||||
ChatApiHandler.deleteMessage project_id, thread_id, message_id, (err, threads) ->
|
||||
return next(err) if err?
|
||||
EditorRealTimeController.emitToRoom project_id, "delete-message", thread_id, message_id, (err)->
|
||||
res.send 204
|
||||
|
||||
_injectUserInfoIntoThreads: (threads, callback = (error, threads) ->) ->
|
||||
userCache = {}
|
||||
getUserDetails = (user_id, callback = (error, user) ->) ->
|
||||
return callback(null, userCache[user_id]) if userCache[user_id]?
|
||||
UserInfoManager.getPersonalInfo user_id, (err, user) ->
|
||||
return callback(error) if error?
|
||||
user = UserInfoController.formatPersonalInfo user
|
||||
userCache[user_id] = user
|
||||
callback null, user
|
||||
|
||||
jobs = []
|
||||
for thread_id, thread of threads
|
||||
do (thread) ->
|
||||
if thread.resolved
|
||||
jobs.push (cb) ->
|
||||
getUserDetails thread.resolved_by_user_id, (error, user) ->
|
||||
cb(error) if error?
|
||||
thread.resolved_by_user = user
|
||||
cb()
|
||||
for message in thread.messages
|
||||
do (message) ->
|
||||
jobs.push (cb) ->
|
||||
getUserDetails message.user_id, (error, user) ->
|
||||
cb(error) if error?
|
||||
message.user = user
|
||||
cb()
|
||||
|
||||
async.series jobs, (error) ->
|
||||
return callback(error) if error?
|
||||
return callback null, threads
|
|
@ -1,4 +1,4 @@
|
|||
Metrics = require "../../infrastructure/Metrics"
|
||||
Metrics = require "metrics-sharelatex"
|
||||
Project = require("../../models/Project").Project
|
||||
CompileManager = require("./CompileManager")
|
||||
ClsiManager = require("./ClsiManager")
|
||||
|
|
|
@ -6,7 +6,7 @@ Project = require("../../models/Project").Project
|
|||
ProjectRootDocManager = require "../Project/ProjectRootDocManager"
|
||||
UserGetter = require "../User/UserGetter"
|
||||
ClsiManager = require "./ClsiManager"
|
||||
Metrics = require('../../infrastructure/Metrics')
|
||||
Metrics = require('metrics-sharelatex')
|
||||
logger = require("logger-sharelatex")
|
||||
rateLimiter = require("../../infrastructure/RateLimiter")
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ settings = require 'settings-sharelatex'
|
|||
_ = require 'underscore'
|
||||
async = require 'async'
|
||||
logger = require('logger-sharelatex')
|
||||
metrics = require('../../infrastructure/Metrics')
|
||||
metrics = require('metrics-sharelatex')
|
||||
redis = require("redis-sharelatex")
|
||||
rclient = redis.createClient(settings.redis.web)
|
||||
Project = require("../../models/Project").Project
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
logger = require "logger-sharelatex"
|
||||
Metrics = require "../../infrastructure/Metrics"
|
||||
Metrics = require "metrics-sharelatex"
|
||||
Project = require("../../models/Project").Project
|
||||
ProjectZipStreamManager = require "./ProjectZipStreamManager"
|
||||
DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
logger = require('logger-sharelatex')
|
||||
Metrics = require('../../infrastructure/Metrics')
|
||||
Metrics = require('metrics-sharelatex')
|
||||
sanitize = require('sanitizer')
|
||||
ProjectEntityHandler = require('../Project/ProjectEntityHandler')
|
||||
ProjectOptionsHandler = require('../Project/ProjectOptionsHandler')
|
||||
|
|
|
@ -7,7 +7,7 @@ ProjectGetter = require('../Project/ProjectGetter')
|
|||
UserGetter = require('../User/UserGetter')
|
||||
AuthorizationManager = require("../Authorization/AuthorizationManager")
|
||||
ProjectEditorHandler = require('../Project/ProjectEditorHandler')
|
||||
Metrics = require('../../infrastructure/Metrics')
|
||||
Metrics = require('metrics-sharelatex')
|
||||
CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler")
|
||||
CollaboratorsInviteHandler = require("../Collaborators/CollaboratorsInviteHandler")
|
||||
PrivilegeLevels = require "../Authorization/PrivilegeLevels"
|
||||
|
@ -96,8 +96,10 @@ module.exports = EditorHttpController =
|
|||
EditorController.addFolder project_id, parent_folder_id, name, "editor", (error, doc) ->
|
||||
if error == "project_has_to_many_files"
|
||||
res.status(400).json(req.i18n.translate("project_has_to_many_files"))
|
||||
else if error?.message == 'invalid element name'
|
||||
res.status(400).json(req.i18n.translate('invalid_file_name'))
|
||||
else if error?
|
||||
next(error)
|
||||
res.status(500).json(req.i18n.translate('generic_something_went_wrong'))
|
||||
else
|
||||
res.json doc
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
logger = require('logger-sharelatex')
|
||||
metrics = require('../../infrastructure/Metrics')
|
||||
metrics = require('metrics-sharelatex')
|
||||
Settings = require('settings-sharelatex')
|
||||
nodemailer = require("nodemailer")
|
||||
sesTransport = require('nodemailer-ses-transport')
|
||||
|
@ -72,6 +72,7 @@ module.exports =
|
|||
client.sendMail options, (err, res)->
|
||||
if err?
|
||||
logger.err err:err, "error sending message"
|
||||
err = new Error('Cannot send email')
|
||||
else
|
||||
logger.log "Message sent to #{options.to}"
|
||||
callback(err)
|
||||
|
|
|
@ -5,5 +5,14 @@ NotFoundError = (message) ->
|
|||
return error
|
||||
NotFoundError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
ServiceNotConfiguredError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = "ServiceNotConfiguredError"
|
||||
error.__proto__ = ServiceNotConfiguredError.prototype
|
||||
return error
|
||||
|
||||
|
||||
module.exports = Errors =
|
||||
NotFoundError: NotFoundError
|
||||
NotFoundError: NotFoundError
|
||||
ServiceNotConfiguredError: ServiceNotConfiguredError
|
||||
|
|
|
@ -13,6 +13,9 @@ module.exports = FileStoreHandler =
|
|||
if err?
|
||||
logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "error stating file"
|
||||
callback(err)
|
||||
if !stat?
|
||||
logger.err project_id:project_id, file_id:file_id, fsPath:fsPath, "stat is not available, can not check file from disk"
|
||||
return callback(new Error("error getting stat, not available"))
|
||||
if !stat.isFile()
|
||||
logger.log project_id:project_id, file_id:file_id, fsPath:fsPath, "tried to upload symlink, not contining"
|
||||
return callback(new Error("can not upload symlink"))
|
||||
|
@ -25,10 +28,19 @@ module.exports = FileStoreHandler =
|
|||
timeout:fiveMinsInMs
|
||||
writeStream = request(opts)
|
||||
readStream.pipe writeStream
|
||||
writeStream.on "end", callback
|
||||
|
||||
writeStream.on 'response', (response) ->
|
||||
if response.statusCode not in [200, 201]
|
||||
err = new Error("non-ok response from filestore for upload: #{response.statusCode}")
|
||||
logger.err {err, statusCode: response.statusCode}, "error uploading to filestore"
|
||||
callback(err)
|
||||
else
|
||||
callback(null)
|
||||
|
||||
readStream.on "error", (err)->
|
||||
logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "something went wrong on the read stream of uploadFileFromDisk"
|
||||
callback err
|
||||
|
||||
writeStream.on "error", (err)->
|
||||
logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "something went wrong on the write stream of uploadFileFromDisk"
|
||||
callback err
|
||||
|
@ -79,4 +91,4 @@ module.exports = FileStoreHandler =
|
|||
callback(err)
|
||||
|
||||
_buildUrl: (project_id, file_id)->
|
||||
return "#{settings.apis.filestore.url}/project/#{project_id}/file/#{file_id}"
|
||||
return "#{settings.apis.filestore.url}/project/#{project_id}/file/#{file_id}"
|
||||
|
|
|
@ -4,11 +4,9 @@ logger = require("logger-sharelatex")
|
|||
|
||||
module.exports =
|
||||
|
||||
getProjectDetails : (req, res)->
|
||||
getProjectDetails : (req, res, next)->
|
||||
{project_id} = req.params
|
||||
ProjectDetailsHandler.getDetails project_id, (err, projDetails)->
|
||||
if err?
|
||||
logger.log err:err, project_id:project_id, "something went wrong getting project details"
|
||||
return res.sendStatus 500
|
||||
return next(err) if err?
|
||||
res.json(projDetails)
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ projectDeleter = require("./ProjectDeleter")
|
|||
projectDuplicator = require("./ProjectDuplicator")
|
||||
projectCreationHandler = require("./ProjectCreationHandler")
|
||||
editorController = require("../Editor/EditorController")
|
||||
metrics = require('../../infrastructure/Metrics')
|
||||
metrics = require('metrics-sharelatex')
|
||||
User = require('../../models/User').User
|
||||
TagsHandler = require("../Tags/TagsHandler")
|
||||
SubscriptionLocator = require("../Subscription/SubscriptionLocator")
|
||||
|
@ -224,6 +224,11 @@ module.exports = ProjectController =
|
|||
cb = underscore.once(cb)
|
||||
if !user_id?
|
||||
return cb()
|
||||
timestamp = user_id.toString().substring(0,8)
|
||||
userSignupDate = new Date( parseInt( timestamp, 16 ) * 1000 )
|
||||
if userSignupDate > new Date("2017-03-09") # 8th March
|
||||
# Don't show for users who registered after it was released
|
||||
return cb(null, false)
|
||||
timeout = setTimeout cb, 500
|
||||
AnalyticsManager.getLastOccurance user_id, "shown-track-changes-onboarding-2", (error, event) ->
|
||||
clearTimeout timeout
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
logger = require('logger-sharelatex')
|
||||
async = require("async")
|
||||
metrics = require('../../infrastructure/Metrics')
|
||||
metrics = require('metrics-sharelatex')
|
||||
Settings = require('settings-sharelatex')
|
||||
ObjectId = require('mongoose').Types.ObjectId
|
||||
Project = require('../../models/Project').Project
|
||||
|
@ -11,7 +11,8 @@ fs = require('fs')
|
|||
Path = require "path"
|
||||
_ = require "underscore"
|
||||
|
||||
module.exports =
|
||||
module.exports = ProjectCreationHandler =
|
||||
|
||||
createBlankProject : (owner_id, projectName, callback = (error, project) ->)->
|
||||
metrics.inc("project-creation")
|
||||
logger.log owner_id:owner_id, projectName:projectName, "creating blank project"
|
||||
|
@ -79,5 +80,10 @@ module.exports =
|
|||
output = _.template(template.toString(), data)
|
||||
callback null, output.split("\n")
|
||||
|
||||
metrics.timeAsyncMethod(
|
||||
ProjectCreationHandler, 'createBlankProject',
|
||||
'mongo.ProjectCreationHandler',
|
||||
logger
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ logger = require("logger-sharelatex")
|
|||
tpdsUpdateSender = require '../ThirdPartyDataStore/TpdsUpdateSender'
|
||||
_ = require("underscore")
|
||||
PublicAccessLevels = require("../Authorization/PublicAccessLevels")
|
||||
Errors = require("../Errors/Errors")
|
||||
|
||||
module.exports =
|
||||
|
||||
|
@ -13,6 +14,7 @@ module.exports =
|
|||
if err?
|
||||
logger.err err:err, project_id:project_id, "error getting project"
|
||||
return callback(err)
|
||||
return callback(new Errors.NotFoundError("project not found")) if !project?
|
||||
UserGetter.getUser project.owner_ref, (err, user) ->
|
||||
return callback(err) if err?
|
||||
details =
|
||||
|
|
|
@ -15,9 +15,11 @@ module.exports = ProjectDuplicator =
|
|||
_copyDocs: (newProject, originalRootDoc, originalFolder, desFolder, docContents, callback)->
|
||||
setRootDoc = _.once (doc_id)->
|
||||
projectEntityHandler.setRootDoc newProject._id, doc_id
|
||||
|
||||
jobs = originalFolder.docs.map (doc)->
|
||||
docs = originalFolder.docs or []
|
||||
jobs = docs.map (doc)->
|
||||
return (cb)->
|
||||
if !doc?._id?
|
||||
return callback()
|
||||
content = docContents[doc._id.toString()]
|
||||
projectEntityHandler.addDocWithProject newProject, desFolder._id, doc.name, content.lines, (err, newDoc)->
|
||||
if err?
|
||||
|
@ -30,7 +32,8 @@ module.exports = ProjectDuplicator =
|
|||
async.series jobs, callback
|
||||
|
||||
_copyFiles: (newProject, originalProject_id, originalFolder, desFolder, callback)->
|
||||
jobs = originalFolder.fileRefs.map (file)->
|
||||
fileRefs = originalFolder.fileRefs or []
|
||||
jobs = fileRefs.map (file)->
|
||||
return (cb)->
|
||||
projectEntityHandler.copyFileFromExistingProjectWithProject newProject, desFolder._id, originalProject_id, file, cb
|
||||
async.parallelLimit jobs, 5, callback
|
||||
|
@ -40,10 +43,14 @@ module.exports = ProjectDuplicator =
|
|||
ProjectGetter.getProject newProject_id, {rootFolder:true, name:true}, (err, newProject)->
|
||||
if err?
|
||||
logger.err project_id:newProject_id, "could not get project"
|
||||
return cb(err)
|
||||
return callback(err)
|
||||
|
||||
jobs = originalFolder.folders.map (childFolder)->
|
||||
folders = originalFolder.folders or []
|
||||
|
||||
jobs = folders.map (childFolder)->
|
||||
return (cb)->
|
||||
if !childFolder?._id?
|
||||
return cb()
|
||||
projectEntityHandler.addFolderWithProject newProject, desFolder?._id, childFolder.name, (err, newFolder)->
|
||||
return cb(err) if err?
|
||||
ProjectDuplicator._copyFolderRecursivly newProject_id, originalProject_id, originalRootDoc, childFolder, newFolder, docContents, cb
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
_ = require("underscore")
|
||||
|
||||
module.exports = ProjectEditorHandler =
|
||||
trackChangesAvailable: false
|
||||
|
||||
buildProjectModelView: (project, members, invites) ->
|
||||
result =
|
||||
_id : project._id
|
||||
|
@ -20,11 +22,6 @@ module.exports = ProjectEditorHandler =
|
|||
if !result.invites?
|
||||
result.invites = []
|
||||
|
||||
trackChangesVisible = false
|
||||
for member in members
|
||||
if member.privilegeLevel == "owner" and (member.user?.featureSwitches?.track_changes or member.user?.betaProgram)
|
||||
trackChangesVisible = true
|
||||
|
||||
{owner, ownerFeatures, members} = @buildOwnerAndMembersViews(members)
|
||||
result.owner = owner
|
||||
result.members = members
|
||||
|
@ -38,7 +35,7 @@ module.exports = ProjectEditorHandler =
|
|||
templates: false
|
||||
references: false
|
||||
trackChanges: false
|
||||
trackChangesVisible: trackChangesVisible
|
||||
trackChangesVisible: ProjectEditorHandler.trackChangesAvailable
|
||||
})
|
||||
|
||||
return result
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
mongojs = require("../../infrastructure/mongojs")
|
||||
metrics = require("metrics-sharelatex")
|
||||
db = mongojs.db
|
||||
ObjectId = mongojs.ObjectId
|
||||
async = require "async"
|
||||
|
@ -57,3 +58,10 @@ module.exports = ProjectGetter =
|
|||
CollaboratorsHandler.getProjectsUserIsCollaboratorOf user_id, fields, (error, readAndWriteProjects, readOnlyProjects) ->
|
||||
return callback(error) if error?
|
||||
callback null, projects, readAndWriteProjects, readOnlyProjects
|
||||
|
||||
|
||||
[
|
||||
'getProject',
|
||||
'getProjectWithoutDocLines'
|
||||
].map (method) ->
|
||||
metrics.timeAsyncMethod(ProjectGetter, method, 'mongo.ProjectGetter', logger)
|
||||
|
|
|
@ -26,6 +26,8 @@ module.exports = ProjectLocator =
|
|||
element = _.find searchFolder[elementType], (el)-> el?._id+'' == element_id+'' #need to ToString both id's for robustness
|
||||
if !element? && searchFolder.folders? && searchFolder.folders.length != 0
|
||||
_.each searchFolder.folders, (folder, index)->
|
||||
if !folder?
|
||||
return
|
||||
newPath = {}
|
||||
newPath[key] = value for own key,value of path #make a value copy of the string
|
||||
newPath.fileSystem += "/#{folder.name}"
|
||||
|
|
|
@ -9,13 +9,16 @@ module.exports =
|
|||
|
||||
webRouter.get '/tos', HomeController.externalPage("tos", "Terms of Service")
|
||||
webRouter.get '/about', HomeController.externalPage("about", "About Us")
|
||||
|
||||
webRouter.get '/security', HomeController.externalPage("security", "Security")
|
||||
webRouter.get '/privacy_policy', HomeController.externalPage("privacy", "Privacy Policy")
|
||||
webRouter.get '/planned_maintenance', HomeController.externalPage("planned_maintenance", "Planned Maintenance")
|
||||
webRouter.get '/style', HomeController.externalPage("style_guide", "Style Guide")
|
||||
webRouter.get '/jobs', HomeController.externalPage("jobs", "Jobs")
|
||||
|
||||
webRouter.get '/track-changes-and-comments-in-latex', HomeController.externalPage("review-features-page", "Review features")
|
||||
|
||||
webRouter.get '/dropbox', HomeController.externalPage("dropbox", "Dropbox and ShareLaTeX")
|
||||
|
||||
webRouter.get '/university', UniversityController.getIndexPage
|
||||
webRouter.get '/university/*', UniversityController.getPage
|
||||
webRouter.get '/university/*', UniversityController.getPage
|
||||
|
|
|
@ -469,33 +469,39 @@ module.exports = RecurlyWrapper =
|
|||
logger.err err:error, subscriptionId:subscriptionId, daysUntilExpire:daysUntilExpire, "error exending trial"
|
||||
callback(error)
|
||||
)
|
||||
|
||||
listAccountActiveSubscriptions: (account_id, callback = (error, subscriptions) ->) ->
|
||||
RecurlyWrapper.apiRequest {
|
||||
url: "accounts/#{account_id}/subscriptions"
|
||||
qs:
|
||||
state: "active"
|
||||
expect404: true
|
||||
}, (error, response, body) ->
|
||||
return callback(error) if error?
|
||||
if response.statusCode == 404
|
||||
return callback null, []
|
||||
else
|
||||
RecurlyWrapper._parseSubscriptionsXml body, callback
|
||||
|
||||
_parseSubscriptionsXml: (xml, callback) ->
|
||||
RecurlyWrapper._parseXmlAndGetAttribute xml, "subscriptions", callback
|
||||
|
||||
_parseSubscriptionXml: (xml, callback) ->
|
||||
RecurlyWrapper._parseXml xml, (error, data) ->
|
||||
return callback(error) if error?
|
||||
if data? and data.subscription?
|
||||
recurlySubscription = data.subscription
|
||||
else
|
||||
return callback "I don't understand the response from Recurly"
|
||||
callback null, recurlySubscription
|
||||
RecurlyWrapper._parseXmlAndGetAttribute xml, "subscription", callback
|
||||
|
||||
_parseAccountXml: (xml, callback) ->
|
||||
RecurlyWrapper._parseXml xml, (error, data) ->
|
||||
return callback(error) if error?
|
||||
if data? and data.account?
|
||||
account = data.account
|
||||
else
|
||||
return callback "I don't understand the response from Recurly"
|
||||
callback null, account
|
||||
RecurlyWrapper._parseXmlAndGetAttribute xml, "account", callback
|
||||
|
||||
_parseBillingInfoXml: (xml, callback) ->
|
||||
RecurlyWrapper._parseXmlAndGetAttribute xml, "billing_info", callback
|
||||
|
||||
_parseXmlAndGetAttribute: (xml, attribute, callback) ->
|
||||
RecurlyWrapper._parseXml xml, (error, data) ->
|
||||
return callback(error) if error?
|
||||
if data? and data.billing_info?
|
||||
billingInfo = data.billing_info
|
||||
if data? and data[attribute]?
|
||||
return callback null, data[attribute]
|
||||
else
|
||||
return callback "I don't understand the response from Recurly"
|
||||
callback null, billingInfo
|
||||
return callback(new Error("I don't understand the response from Recurly"))
|
||||
|
||||
_parseXml: (xml, callback) ->
|
||||
convertDataTypes = (data) ->
|
||||
|
|
|
@ -46,31 +46,39 @@ module.exports = SubscriptionController =
|
|||
if hasSubscription or !plan?
|
||||
res.redirect "/user/subscription"
|
||||
else
|
||||
currency = req.query.currency?.toUpperCase()
|
||||
GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency, countryCode)->
|
||||
return next(err) if err?
|
||||
if recomendedCurrency? and !currency?
|
||||
currency = recomendedCurrency
|
||||
RecurlyWrapper.sign {
|
||||
subscription:
|
||||
plan_code : req.query.planCode
|
||||
currency: currency
|
||||
account_code: user._id
|
||||
}, (error, signature) ->
|
||||
return next(error) if error?
|
||||
res.render "subscriptions/new",
|
||||
title : "subscribe"
|
||||
plan_code: req.query.planCode
|
||||
currency: currency
|
||||
countryCode:countryCode
|
||||
plan:plan
|
||||
showStudentPlan: req.query.ssp
|
||||
recurlyConfig: JSON.stringify
|
||||
currency: currency
|
||||
subdomain: Settings.apis.recurly.subdomain
|
||||
showCouponField: req.query.scf
|
||||
showVatField: req.query.svf
|
||||
couponCode: req.query.cc or ""
|
||||
# LimitationsManager.userHasSubscription only checks Mongo. Double check with
|
||||
# Recurly as well at this point (we don't do this most places for speed).
|
||||
SubscriptionHandler.validateNoSubscriptionInRecurly user._id, (error, valid) ->
|
||||
return next(error) if error?
|
||||
if !valid
|
||||
res.redirect "/user/subscription"
|
||||
return
|
||||
else
|
||||
currency = req.query.currency?.toUpperCase()
|
||||
GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency, countryCode)->
|
||||
return next(err) if err?
|
||||
if recomendedCurrency? and !currency?
|
||||
currency = recomendedCurrency
|
||||
RecurlyWrapper.sign {
|
||||
subscription:
|
||||
plan_code : req.query.planCode
|
||||
currency: currency
|
||||
account_code: user._id
|
||||
}, (error, signature) ->
|
||||
return next(error) if error?
|
||||
res.render "subscriptions/new",
|
||||
title : "subscribe"
|
||||
plan_code: req.query.planCode
|
||||
currency: currency
|
||||
countryCode:countryCode
|
||||
plan:plan
|
||||
showStudentPlan: req.query.ssp
|
||||
recurlyConfig: JSON.stringify
|
||||
currency: currency
|
||||
subdomain: Settings.apis.recurly.subdomain
|
||||
showCouponField: req.query.scf
|
||||
showVatField: req.query.svf
|
||||
couponCode: req.query.cc or ""
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -11,15 +11,28 @@ Analytics = require("../Analytics/AnalyticsManager")
|
|||
|
||||
|
||||
module.exports =
|
||||
validateNoSubscriptionInRecurly: (user_id, callback = (error, valid) ->) ->
|
||||
RecurlyWrapper.listAccountActiveSubscriptions user_id, (error, subscriptions = []) ->
|
||||
return callback(error) if error?
|
||||
if subscriptions.length > 0
|
||||
SubscriptionUpdater.syncSubscription subscriptions[0], user_id, (error) ->
|
||||
return callback(error) if error?
|
||||
return callback(null, false)
|
||||
else
|
||||
return callback(null, true)
|
||||
|
||||
createSubscription: (user, subscriptionDetails, recurly_token_id, callback)->
|
||||
self = @
|
||||
clientTokenId = ""
|
||||
RecurlyWrapper.createSubscription user, subscriptionDetails, recurly_token_id, (error, recurlySubscription)->
|
||||
@validateNoSubscriptionInRecurly user._id, (error, valid) ->
|
||||
return callback(error) if error?
|
||||
SubscriptionUpdater.syncSubscription recurlySubscription, user._id, (error) ->
|
||||
if !valid
|
||||
return callback(new Error("user already has subscription in recurly"))
|
||||
RecurlyWrapper.createSubscription user, subscriptionDetails, recurly_token_id, (error, recurlySubscription)->
|
||||
return callback(error) if error?
|
||||
callback()
|
||||
SubscriptionUpdater.syncSubscription recurlySubscription, user._id, (error) ->
|
||||
return callback(error) if error?
|
||||
callback()
|
||||
|
||||
updateSubscription: (user, plan_code, coupon_code, callback)->
|
||||
logger.log user:user, plan_code:plan_code, coupon_code:coupon_code, "updating subscription"
|
||||
|
|
|
@ -23,7 +23,7 @@ module.exports = SystemMessageManager =
|
|||
clearCache: () ->
|
||||
delete @_cachedMessages
|
||||
|
||||
CACHE_TIMEOUT = 5 * 60 * 1000 # 5 minutes
|
||||
CACHE_TIMEOUT = 20 * 1000 # 20 seconds
|
||||
setInterval () ->
|
||||
SystemMessageManager.clearCache()
|
||||
, CACHE_TIMEOUT
|
||||
, CACHE_TIMEOUT
|
||||
|
|
|
@ -2,7 +2,7 @@ tpdsUpdateHandler = require('./TpdsUpdateHandler')
|
|||
UpdateMerger = require "./UpdateMerger"
|
||||
logger = require('logger-sharelatex')
|
||||
Path = require('path')
|
||||
metrics = require("../../infrastructure/Metrics")
|
||||
metrics = require("metrics-sharelatex")
|
||||
|
||||
module.exports =
|
||||
# mergeUpdate and deleteUpdate are used by Dropbox, where the project is only passed as the name, as the
|
||||
|
|
|
@ -3,7 +3,7 @@ logger = require('logger-sharelatex')
|
|||
path = require('path')
|
||||
Project = require('../../models/Project').Project
|
||||
keys = require('../../infrastructure/Keys')
|
||||
metrics = require("../../infrastructure/Metrics")
|
||||
metrics = require("metrics-sharelatex")
|
||||
request = require("request")
|
||||
CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler')
|
||||
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
|
||||
DocstoreManager = require "../Docstore/DocstoreManager"
|
||||
UserInfoManager = require "../User/UserInfoManager"
|
||||
async = require "async"
|
||||
|
||||
module.exports = RangesManager =
|
||||
getAllRanges: (project_id, callback = (error, docs) ->) ->
|
||||
DocumentUpdaterHandler.flushProjectToMongo project_id, (error) ->
|
||||
return callback(error) if error?
|
||||
DocstoreManager.getAllRanges project_id, callback
|
||||
|
||||
getAllChangesUsers: (project_id, callback = (error, users) ->) ->
|
||||
user_ids = {}
|
||||
RangesManager.getAllRanges project_id, (error, docs) ->
|
||||
return callback(error) if error?
|
||||
jobs = []
|
||||
for doc in docs
|
||||
for change in doc.ranges?.changes or []
|
||||
user_ids[change.metadata.user_id] = true
|
||||
|
||||
async.mapSeries Object.keys(user_ids), (user_id, cb) ->
|
||||
UserInfoManager.getPersonalInfo user_id, cb
|
||||
, callback
|
|
@ -1,42 +0,0 @@
|
|||
RangesManager = require "./RangesManager"
|
||||
logger = require "logger-sharelatex"
|
||||
UserInfoController = require "../User/UserInfoController"
|
||||
DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
|
||||
EditorRealTimeController = require("../Editor/EditorRealTimeController")
|
||||
TrackChangesManager = require "./TrackChangesManager"
|
||||
|
||||
module.exports = TrackChangesController =
|
||||
getAllRanges: (req, res, next) ->
|
||||
project_id = req.params.project_id
|
||||
logger.log {project_id}, "request for project ranges"
|
||||
RangesManager.getAllRanges project_id, (error, docs = []) ->
|
||||
return next(error) if error?
|
||||
docs = ({id: d._id, ranges: d.ranges} for d in docs)
|
||||
res.json docs
|
||||
|
||||
getAllChangesUsers: (req, res, next) ->
|
||||
project_id = req.params.project_id
|
||||
logger.log {project_id}, "request for project range users"
|
||||
RangesManager.getAllChangesUsers project_id, (error, users) ->
|
||||
return next(error) if error?
|
||||
users = (UserInfoController.formatPersonalInfo(user) for user in users)
|
||||
# Get rid of any anonymous/deleted user objects
|
||||
users = users.filter (u) -> u?.id?
|
||||
res.json users
|
||||
|
||||
acceptChange: (req, res, next) ->
|
||||
{project_id, doc_id, change_id} = req.params
|
||||
logger.log {project_id, doc_id, change_id}, "request to accept change"
|
||||
DocumentUpdaterHandler.acceptChange project_id, doc_id, change_id, (error) ->
|
||||
return next(error) if error?
|
||||
EditorRealTimeController.emitToRoom project_id, "accept-change", doc_id, change_id, (err)->
|
||||
res.send 204
|
||||
|
||||
toggleTrackChanges: (req, res, next) ->
|
||||
{project_id} = req.params
|
||||
track_changes_on = !!req.body.on
|
||||
logger.log {project_id, track_changes_on}, "request to toggle track changes"
|
||||
TrackChangesManager.toggleTrackChanges project_id, track_changes_on, (error) ->
|
||||
return next(error) if error?
|
||||
EditorRealTimeController.emitToRoom project_id, "toggle-track-changes", track_changes_on, (err)->
|
||||
res.send 204
|
|
@ -1,5 +0,0 @@
|
|||
Project = require("../../models/Project").Project
|
||||
|
||||
module.exports = TrackChangesManager =
|
||||
toggleTrackChanges: (project_id, track_changes_on, callback = (error) ->) ->
|
||||
Project.update {_id: project_id}, {track_changes: track_changes_on}, callback
|
|
@ -1,6 +1,6 @@
|
|||
child = require "child_process"
|
||||
logger = require "logger-sharelatex"
|
||||
metrics = require "../../infrastructure/Metrics"
|
||||
metrics = require "metrics-sharelatex"
|
||||
fs = require "fs"
|
||||
Path = require "path"
|
||||
_ = require("underscore")
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
logger = require "logger-sharelatex"
|
||||
metrics = require "../../infrastructure/Metrics"
|
||||
metrics = require "metrics-sharelatex"
|
||||
fs = require "fs"
|
||||
Path = require "path"
|
||||
FileSystemImportManager = require "./FileSystemImportManager"
|
||||
|
|
|
@ -5,7 +5,7 @@ User = require("../../models/User").User
|
|||
newsLetterManager = require('../Newsletter/NewsletterManager')
|
||||
UserRegistrationHandler = require("./UserRegistrationHandler")
|
||||
logger = require("logger-sharelatex")
|
||||
metrics = require("../../infrastructure/Metrics")
|
||||
metrics = require("metrics-sharelatex")
|
||||
Url = require("url")
|
||||
AuthenticationManager = require("../Authentication/AuthenticationManager")
|
||||
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
User = require("../../models/User").User
|
||||
UserLocator = require("./UserLocator")
|
||||
logger = require("logger-sharelatex")
|
||||
metrics = require('metrics-sharelatex')
|
||||
|
||||
module.exports =
|
||||
|
||||
module.exports = UserCreator =
|
||||
|
||||
getUserOrCreateHoldingAccount: (email, callback = (err, user)->)->
|
||||
self = @
|
||||
|
@ -36,3 +38,9 @@ module.exports =
|
|||
|
||||
user.save (err)->
|
||||
callback(err, user)
|
||||
|
||||
metrics.timeAsyncMethod(
|
||||
UserCreator, 'createNewUser',
|
||||
'mongo.UserCreator',
|
||||
logger
|
||||
)
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
mongojs = require("../../infrastructure/mongojs")
|
||||
metrics = require('metrics-sharelatex')
|
||||
logger = require('logger-sharelatex')
|
||||
db = mongojs.db
|
||||
ObjectId = mongojs.ObjectId
|
||||
|
||||
|
@ -23,4 +25,11 @@ module.exports = UserGetter =
|
|||
catch error
|
||||
return callback error
|
||||
|
||||
db.users.find { _id: { $in: user_ids} }, projection, callback
|
||||
db.users.find { _id: { $in: user_ids} }, projection, callback
|
||||
|
||||
|
||||
[
|
||||
'getUser',
|
||||
'getUsers'
|
||||
].map (method) ->
|
||||
metrics.timeAsyncMethod UserGetter, method, 'mongo.UserGetter', logger
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
mongojs = require("../../infrastructure/mongojs")
|
||||
metrics = require("metrics-sharelatex")
|
||||
db = mongojs.db
|
||||
ObjectId = mongojs.ObjectId
|
||||
logger = require('logger-sharelatex')
|
||||
|
||||
module.exports =
|
||||
module.exports = UserLocator =
|
||||
|
||||
findByEmail: (email, callback)->
|
||||
email = email.trim()
|
||||
|
@ -10,4 +12,10 @@ module.exports =
|
|||
callback(err, user)
|
||||
|
||||
findById: (_id, callback)->
|
||||
db.users.findOne _id:ObjectId(_id+""), callback
|
||||
db.users.findOne _id:ObjectId(_id+""), callback
|
||||
|
||||
[
|
||||
'findById',
|
||||
'findByEmail'
|
||||
].map (method) ->
|
||||
metrics.timeAsyncMethod UserLocator, method, 'mongo.UserLocator', logger
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
logger = require("logger-sharelatex")
|
||||
mongojs = require("../../infrastructure/mongojs")
|
||||
metrics = require("metrics-sharelatex")
|
||||
db = mongojs.db
|
||||
ObjectId = mongojs.ObjectId
|
||||
UserLocator = require("./UserLocator")
|
||||
|
@ -28,3 +29,5 @@ module.exports = UserUpdater =
|
|||
return callback(err)
|
||||
callback()
|
||||
|
||||
|
||||
metrics.timeAsyncMethod UserUpdater, 'updateUser', 'mongo.UserUpdater', logger
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
metrics = require('./Metrics')
|
||||
metrics = require('metrics-sharelatex')
|
||||
module.exports =
|
||||
log: (req)->
|
||||
if req.headers["user-agent"]?
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
metrics = require('./Metrics')
|
||||
metrics = require('metrics-sharelatex')
|
||||
Settings = require('settings-sharelatex')
|
||||
redis = require("redis-sharelatex")
|
||||
rclient = redis.createClient(Settings.redis.web)
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
module.exports = require("metrics-sharelatex")
|
|
@ -1,5 +1,5 @@
|
|||
version = {
|
||||
"pdfjs": "1.6.210p2"
|
||||
"pdfjs": "1.7.225"
|
||||
"moment": "2.9.0"
|
||||
"ace": "1.2.5"
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
_ = require('underscore')
|
||||
metrics = require('./Metrics')
|
||||
metrics = require('metrics-sharelatex')
|
||||
|
||||
do trackOpenSockets = ->
|
||||
metrics.gauge("http.open-sockets", _.size(require('http').globalAgent.sockets.length), 0.5)
|
||||
|
|
|
@ -2,7 +2,7 @@ Path = require "path"
|
|||
express = require('express')
|
||||
Settings = require('settings-sharelatex')
|
||||
logger = require 'logger-sharelatex'
|
||||
metrics = require('./Metrics')
|
||||
metrics = require('metrics-sharelatex')
|
||||
crawlerLogger = require('./CrawlerLogger')
|
||||
expressLocals = require('./ExpressLocals')
|
||||
Router = require('../router')
|
||||
|
@ -39,8 +39,6 @@ ErrorController = require "../Features/Errors/ErrorController"
|
|||
UserSessionsManager = require "../Features/User/UserSessionsManager"
|
||||
AuthenticationController = require "../Features/Authentication/AuthenticationController"
|
||||
|
||||
metrics.mongodb.monitor(Path.resolve(__dirname + "/../../../node_modules/mongojs/node_modules/mongodb"), logger)
|
||||
metrics.mongodb.monitor(Path.resolve(__dirname + "/../../../node_modules/mongoose/node_modules/mongodb"), logger)
|
||||
|
||||
metrics.event_loop?.monitor(logger)
|
||||
|
||||
|
|
|
@ -39,9 +39,6 @@ UserSchema = new Schema
|
|||
references: { type:Boolean, default: Settings.defaultFeatures.references }
|
||||
trackChanges: { type:Boolean, default: Settings.defaultFeatures.trackChanges }
|
||||
}
|
||||
featureSwitches : {
|
||||
track_changes: { type: Boolean }
|
||||
}
|
||||
referal_id : {type:String, default:() -> uuid.v4().split("-")[0]}
|
||||
refered_users: [ type:ObjectId, ref:'User' ]
|
||||
refered_user_count: { type:Number, default: 0 }
|
||||
|
|
|
@ -9,7 +9,7 @@ Settings = require('settings-sharelatex')
|
|||
TpdsController = require('./Features/ThirdPartyDataStore/TpdsController')
|
||||
SubscriptionRouter = require './Features/Subscription/SubscriptionRouter'
|
||||
UploadsRouter = require './Features/Uploads/UploadsRouter'
|
||||
metrics = require('./infrastructure/Metrics')
|
||||
metrics = require('metrics-sharelatex')
|
||||
ReferalController = require('./Features/Referal/ReferalController')
|
||||
AuthenticationController = require('./Features/Authentication/AuthenticationController')
|
||||
TagsController = require("./Features/Tags/TagsController")
|
||||
|
@ -40,8 +40,6 @@ AuthorizationMiddlewear = require('./Features/Authorization/AuthorizationMiddlew
|
|||
BetaProgramController = require('./Features/BetaProgram/BetaProgramController')
|
||||
AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter')
|
||||
AnnouncementsController = require("./Features/Announcements/AnnouncementsController")
|
||||
TrackChangesController = require("./Features/TrackChanges/TrackChangesController")
|
||||
CommentsController = require "./Features/Comments/CommentsController"
|
||||
|
||||
logger = require("logger-sharelatex")
|
||||
_ = require("underscore")
|
||||
|
@ -177,11 +175,6 @@ module.exports = class Router
|
|||
webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.proxyToHistoryApi
|
||||
webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.proxyToHistoryApi
|
||||
|
||||
webRouter.get "/project/:project_id/ranges", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.getAllRanges
|
||||
webRouter.get "/project/:project_id/changes/users", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.getAllChangesUsers
|
||||
webRouter.post "/project/:project_id/doc/:doc_id/changes/:change_id/accept", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, TrackChangesController.acceptChange
|
||||
webRouter.post "/project/:project_id/track_changes", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, TrackChangesController.toggleTrackChanges
|
||||
|
||||
webRouter.get '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject
|
||||
webRouter.get '/project/download/zip', AuthorizationMiddlewear.ensureUserCanReadMultipleProjects, ProjectDownloadsController.downloadMultipleProjects
|
||||
|
||||
|
@ -232,15 +225,6 @@ module.exports = class Router
|
|||
|
||||
webRouter.get "/project/:project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.getMessages
|
||||
webRouter.post "/project/:project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.sendMessage
|
||||
|
||||
# Note: Read only users can still comment
|
||||
webRouter.post "/project/:project_id/thread/:thread_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, CommentsController.sendComment
|
||||
webRouter.get "/project/:project_id/threads", AuthorizationMiddlewear.ensureUserCanReadProject, CommentsController.getThreads
|
||||
webRouter.post "/project/:project_id/thread/:thread_id/resolve", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.resolveThread
|
||||
webRouter.post "/project/:project_id/thread/:thread_id/reopen", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.reopenThread
|
||||
webRouter.delete "/project/:project_id/doc/:doc_id/thread/:thread_id", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.deleteThread
|
||||
webRouter.post "/project/:project_id/thread/:thread_id/messages/:message_id/edit", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.editMessage
|
||||
webRouter.delete "/project/:project_id/thread/:thread_id/messages/:message_id", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.deleteMessage
|
||||
|
||||
webRouter.post "/project/:Project_id/references/index", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.index
|
||||
webRouter.post "/project/:Project_id/references/indexAll", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.indexAll
|
||||
|
|
|
@ -18,9 +18,7 @@ block content
|
|||
| #{translate("beta_program_badge_description")}
|
||||
span.beta-feature-badge
|
||||
p.text-centered
|
||||
strong We're currently testing track changes and commenting:
|
||||
p.text-centered
|
||||
img(src="/img/teasers/track-changes/track-changes-beta.png", style="max-width: 100%; border-bottom: 1px solid #ddd")
|
||||
strong We're not currently testing anything in beta, but keep checking back!
|
||||
.row.text-centered
|
||||
.col-md-12
|
||||
if user.betaProgram
|
||||
|
|
|
@ -3,11 +3,13 @@ extends ../layout
|
|||
block content
|
||||
.content
|
||||
.container
|
||||
.row
|
||||
.col-md-8.col-md-offset-2.text-center
|
||||
.page-header
|
||||
h2 #{translate("cant_find_page")}
|
||||
p
|
||||
a(href="/")
|
||||
i.fa.fa-arrow-circle-o-left
|
||||
| #{translate("take_me_home")}
|
||||
.error-container
|
||||
.error-figure
|
||||
img.error-img(
|
||||
src="/img/brand/404-visual.svg"
|
||||
alt="Not found"
|
||||
)
|
||||
.error-details
|
||||
p.error-status Not found
|
||||
p.error-description #{translate("cant_find_page")}
|
||||
a.error-btn(href="/") Home
|
|
@ -1,24 +1,36 @@
|
|||
doctype html
|
||||
html(itemscope, itemtype='http://schema.org/Product')
|
||||
html.full-height(itemscope, itemtype='http://schema.org/Product')
|
||||
head
|
||||
title Something went wrong
|
||||
link(rel="icon", href="/favicon.ico")
|
||||
if buildCssPath
|
||||
link(rel='stylesheet', href=buildCssPath('/style.css'))
|
||||
link(href="//netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css",rel="stylesheet")
|
||||
body
|
||||
.content
|
||||
.container
|
||||
.row
|
||||
.col-md-8.col-md-offset-2.text-center
|
||||
.page-header
|
||||
h2 Oh dear, something went wrong.
|
||||
if buildImgPath
|
||||
p
|
||||
img(src=buildImgPath("lion-sad-128.png"), alt="Sad Lion")
|
||||
p
|
||||
| Something went wrong with your request, sorry. Our staff are probably looking into this, but if it continues, please contact us at #{settings.adminEmail}
|
||||
p
|
||||
a(href="/")
|
||||
i.fa.fa-arrow-circle-o-left
|
||||
| Take me home
|
||||
body.full-height
|
||||
.content.full-height
|
||||
.container.full-height
|
||||
.error-container.full-height
|
||||
.error-figure.error-figure-500
|
||||
img.error-img(
|
||||
src="/img/brand/500-visual-socket.svg"
|
||||
alt="Error"
|
||||
)
|
||||
.error-details
|
||||
p.error-status Something went wrong, sorry.
|
||||
p.error-description Our staff are probably looking into this, but if it continues, please contact us at #{settings.adminEmail}
|
||||
a.error-btn(href="/") Home
|
||||
//- .content
|
||||
//- .container
|
||||
//- .row
|
||||
//- .col-md-8.col-md-offset-2.text-center
|
||||
//- .page-header
|
||||
//- h2 Oh dear, something went wrong.
|
||||
//- if buildImgPath
|
||||
//- p
|
||||
//- img(src=buildImgPath("lion-sad-128.png"), alt="Sad Lion")
|
||||
//- p
|
||||
//- | Something went wrong with your request, sorry. Our staff are probably looking into this, but if it continues, please contact us at #{settings.adminEmail}
|
||||
//- p
|
||||
//- a(href="/")
|
||||
//- i.fa.fa-arrow-circle-o-left
|
||||
//- | Take me home
|
||||
|
|
|
@ -18,7 +18,11 @@ html(itemscope, itemtype='http://schema.org/Product')
|
|||
-else
|
||||
title= translate(title) + ' - ShareLaTeX, ' + translate("online_latex_editor")
|
||||
|
||||
link(rel="icon", href="/favicon.ico")
|
||||
link(rel="icon", href="favicon.ico")
|
||||
link(rel="icon", sizes="192x192", href="touch-icon-192x192.png")
|
||||
link(rel="apple-touch-icon-precomposed", href="favicon-152.png")
|
||||
link(rel="mask-icon", href="mask-favicon.svg", color="#a93529")
|
||||
|
||||
link(rel='stylesheet', href=buildCssPath('/style.css'))
|
||||
|
||||
block _headLinks
|
||||
|
|
|
@ -7,13 +7,18 @@ block vars
|
|||
|
||||
block content
|
||||
.editor(ng-controller="IdeController").full-size
|
||||
.loading-screen(ng-show="state.loading")
|
||||
.container
|
||||
h3 #{translate("loading")}...
|
||||
.progress
|
||||
.progress-bar(style="width: 20%", ng-style="{'width': state.load_progress + '%'}")
|
||||
p.text-center.text-danger(ng-if="state.error").ng-cloak
|
||||
span(ng-bind-html="state.error")
|
||||
.loading-screen(ng-if="state.loading")
|
||||
.loading-screen-lion-container
|
||||
.loading-screen-lion(
|
||||
style="height: 20%;"
|
||||
ng-style="{ 'height': state.load_progress + '%' }"
|
||||
)
|
||||
h3.loading-screen-label(ng-if="!state.error") #{translate("loading")}
|
||||
span.loading-screen-ellip .
|
||||
span.loading-screen-ellip .
|
||||
span.loading-screen-ellip .
|
||||
p.loading-screen-error(ng-if="state.error").ng-cloak
|
||||
span(ng-bind-html="state.error")
|
||||
|
||||
include ./editor/feature-onboarding
|
||||
|
||||
|
|
|
@ -19,7 +19,8 @@ div.full-size(
|
|||
'rp-size-mini': (!ui.reviewPanelOpen && reviewPanel.hasEntries),\
|
||||
'rp-size-expanded': ui.reviewPanelOpen,\
|
||||
'rp-layout-left': reviewPanel.layoutToLeft,\
|
||||
'rp-loading-threads': reviewPanel.loadingThreads\
|
||||
'rp-loading-threads': reviewPanel.loadingThreads,\
|
||||
'rp-new-comment-ui': reviewPanel.newAddCommentUI\
|
||||
}"
|
||||
)
|
||||
.loading-panel(ng-show="!editor.sharejs_doc || editor.opening")
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
autoplay
|
||||
loop
|
||||
)
|
||||
source(src="/img/onboarding/review-panel/open-review.mp4", type="video/mp4")
|
||||
source(ng-src="{{ '/img/onboarding/review-panel/open-review.mp4' }}", type="video/mp4")
|
||||
img(src="/img/onboarding/review-panel/open-review.gif")
|
||||
div(ng-show="onboarding.innerStep === 2;")
|
||||
video.feat-onboard-video(
|
||||
|
@ -40,7 +40,7 @@
|
|||
autoplay
|
||||
loop
|
||||
)
|
||||
source(src="/img/onboarding/review-panel/commenting.mp4", type="video/mp4")
|
||||
source(ng-src="{{ '/img/onboarding/review-panel/commenting.mp4' }}", type="video/mp4")
|
||||
img(src="/img/onboarding/review-panel/commenting.gif")
|
||||
div(ng-show="onboarding.innerStep === 3;")
|
||||
video.feat-onboard-video(
|
||||
|
@ -48,7 +48,7 @@
|
|||
autoplay
|
||||
loop
|
||||
)
|
||||
source(src="/img/onboarding/review-panel/add-changes.mp4", type="video/mp4")
|
||||
source(ng-src="{{ '/img/onboarding/review-panel/add-changes.mp4' }}", type="video/mp4")
|
||||
img(src="/img/onboarding/review-panel/add-changes.gif")
|
||||
div(ng-show="onboarding.innerStep === 4;")
|
||||
video.feat-onboard-video(
|
||||
|
@ -56,7 +56,7 @@
|
|||
autoplay
|
||||
loop
|
||||
)
|
||||
source(src="/img/onboarding/review-panel/accept-changes.mp4", type="video/mp4")
|
||||
source(ng-src="{{ '/img/onboarding/review-panel/accept-changes.mp4' }}", type="video/mp4")
|
||||
img(src="/img/onboarding/review-panel/accept-changes.gif")
|
||||
button.btn.btn-primary.feat-onboard-nav-btn(
|
||||
ng-click="gotoNextStep();"
|
||||
|
|
|
@ -30,6 +30,11 @@ header.toolbar.toolbar-header.toolbar-with-labels(
|
|||
span.name(
|
||||
ng-dblclick="!permissions.admin || startRenaming()",
|
||||
ng-show="!state.renaming"
|
||||
tooltip="{{ project.name }}",
|
||||
tooltip-class="project-name-tooltip"
|
||||
tooltip-placement="bottom",
|
||||
tooltip-append-to-body="true",
|
||||
tooltip-enable="state.overflowed"
|
||||
) {{ project.name }}
|
||||
|
||||
input.form-control(
|
||||
|
@ -94,7 +99,6 @@ header.toolbar.toolbar-header.toolbar-with-labels(
|
|||
i.review-icon
|
||||
p.toolbar-label
|
||||
| #{translate("review")}
|
||||
span(style="vertical-align: 20%; margin-left: 4px; padding: 2px 4px;").beta-feature-badge
|
||||
a.btn.btn-full-height(
|
||||
href,
|
||||
ng-if="permissions.admin",
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
#review-panel
|
||||
a.rp-track-changes-indicator(
|
||||
href
|
||||
ng-if="editor.wantTrackChanges"
|
||||
ng-click="toggleReviewPanel();"
|
||||
ng-class="{ 'rp-track-changes-indicator-on-dark' : darkTheme }"
|
||||
) !{translate("track_changes_is_on")}
|
||||
.rp-in-editor-widgets
|
||||
a.rp-track-changes-indicator(
|
||||
href
|
||||
ng-if="editor.wantTrackChanges"
|
||||
ng-click="toggleReviewPanel();"
|
||||
ng-class="{ 'rp-track-changes-indicator-on-dark' : darkTheme }"
|
||||
) !{translate("track_changes_is_on")}
|
||||
a.rp-add-comment-btn(
|
||||
href
|
||||
ng-if="reviewPanel.newAddCommentUI && reviewPanel.entries[editor.open_doc_id]['add-comment'] != null"
|
||||
ng-click="addNewComment();"
|
||||
)
|
||||
i.fa.fa-comment
|
||||
| #{translate("add_comment")}
|
||||
.review-panel-toolbar
|
||||
resolved-comments-dropdown(
|
||||
class="rp-flex-block"
|
||||
|
@ -314,7 +322,7 @@ script(type='text/ng-template', id='resolvedCommentEntryTemplate')
|
|||
script(type='text/ng-template', id='addCommentEntryTemplate')
|
||||
div
|
||||
.rp-entry-callout.rp-entry-callout-add-comment
|
||||
.rp-entry-indicator(
|
||||
.rp-entry-indicator.rp-entry-indicator-add-comment(
|
||||
ng-if="!commentState.adding"
|
||||
ng-click="startNewComment(); onIndicatorClick();"
|
||||
tooltip=translate("add_comment")
|
||||
|
@ -340,10 +348,10 @@ script(type='text/ng-template', id='addCommentEntryTemplate')
|
|||
ng-keypress="handleCommentKeyPress($event);"
|
||||
placeholder=translate("add_your_comment_here")
|
||||
focus-on="comment:new:open"
|
||||
ng-blur="submitNewComment()"
|
||||
ng-blur="submitNewComment($event)"
|
||||
)
|
||||
.rp-entry-actions
|
||||
button.rp-entry-button(
|
||||
button.rp-entry-button.rp-entry-button-cancel(
|
||||
ng-click="cancelNewComment();"
|
||||
)
|
||||
i.fa.fa-times
|
||||
|
|
|
@ -4,11 +4,11 @@ block scripts
|
|||
script(src="https://js.recurly.com/v3/recurly.js")
|
||||
|
||||
script(type='text/javascript').
|
||||
window.recomendedCurrency = '#{currency}'
|
||||
window.countryCode = '#{countryCode}'
|
||||
window.plan_code = '#{plan_code}'
|
||||
window.recurlyApiKey = "!{settings.apis.recurly.publicKey}"
|
||||
window.couponCode = "#{couponCode}"
|
||||
window.couponCode = !{JSON.stringify(couponCode)}
|
||||
window.recomendedCurrency = !{JSON.stringify(currency.slice(0,3))}
|
||||
|
||||
block content
|
||||
.content.content-alt
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
"lynx": "0.1.1",
|
||||
"marked": "^0.3.5",
|
||||
"method-override": "^2.3.3",
|
||||
"metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.6.0",
|
||||
"metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.7.1",
|
||||
"mimelib": "0.2.14",
|
||||
"mocha": "1.17.1",
|
||||
"mongojs": "2.4.0",
|
||||
|
|
BIN
services/web/public/apple-touch-icon-precomposed.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
|
@ -353,11 +353,17 @@ define [
|
|||
@ranges.applyOp op, { user_id: track_changes_as }
|
||||
if old_id_seed?
|
||||
@ranges.setIdSeed(old_id_seed)
|
||||
if remote_op
|
||||
# With remote ops, Ace hasn't been updated when we receive this op,
|
||||
# so defer updating track changes until it has
|
||||
setTimeout () => @emit "ranges:dirty"
|
||||
else
|
||||
@emit "ranges:dirty"
|
||||
|
||||
_catchUpRanges: (changes = [], comments = []) ->
|
||||
# We've just been given the current server's ranges, but need to apply any local ops we have.
|
||||
# Reset to the server state then apply our local ops again.
|
||||
@ranges.emit "clear"
|
||||
@emit "ranges:clear"
|
||||
@ranges.changes = changes
|
||||
@ranges.comments = comments
|
||||
@ranges.track_changes = @doc.track_changes
|
||||
|
@ -367,4 +373,4 @@ define [
|
|||
for op in @doc.getPendingOp() or []
|
||||
@ranges.setIdSeed(@doc.track_changes_id_seeds.pending)
|
||||
@ranges.applyOp(op, { user_id: @track_changes_as })
|
||||
@ranges.emit "redraw"
|
||||
@emit "ranges:redraw"
|
||||
|
|
|
@ -121,11 +121,22 @@ define [
|
|||
|
||||
_bindToDocumentEvents: (doc, sharejs_doc) ->
|
||||
sharejs_doc.on "error", (error, meta) =>
|
||||
if error?.message?.match "maxDocLength"
|
||||
if error?.message?
|
||||
message = error.message
|
||||
else if typeof error == "string"
|
||||
message = error
|
||||
else
|
||||
message = ""
|
||||
if message.match "maxDocLength"
|
||||
@ide.showGenericMessageModal(
|
||||
"Document Too Long"
|
||||
"Sorry, this file is too long to be edited manually. Please upload it directly."
|
||||
)
|
||||
else if message.match "too many comments or tracked changes"
|
||||
@ide.showGenericMessageModal(
|
||||
"Too many comments or tracked changes"
|
||||
"Sorry, this file has too many comments or tracked changes. Please try accepting or rejecting some existing changes, or resolving and deleting some comments."
|
||||
)
|
||||
else
|
||||
@ide.socket.disconnect()
|
||||
@ide.reportError(error, meta)
|
||||
|
|
|
@ -322,10 +322,6 @@ define [
|
|||
doc = session.getDocument()
|
||||
doc.on "change", onChange
|
||||
|
||||
sharejs_doc.on "remoteop.recordRemote", (op, oldSnapshot, msg) ->
|
||||
undoManager.nextUpdateIsRemote = true
|
||||
trackChangesManager.nextUpdateMetaData = msg?.meta
|
||||
|
||||
editor.initing = true
|
||||
sharejs_doc.attachToAce(editor)
|
||||
editor.initing = false
|
||||
|
|
|
@ -14,11 +14,11 @@ define [
|
|||
return if !track_changes?
|
||||
@setTrackChanges(track_changes)
|
||||
|
||||
@$scope.$watch "sharejsDoc", (doc) =>
|
||||
@$scope.$watch "sharejsDoc", (doc, oldDoc) =>
|
||||
return if !doc?
|
||||
@disconnectFromRangesTracker()
|
||||
@rangesTracker = doc.ranges
|
||||
@connectToRangesTracker()
|
||||
if oldDoc?
|
||||
@disconnectFromDoc(oldDoc)
|
||||
@connectToDoc(doc)
|
||||
|
||||
@$scope.$on "comment:add", (e, thread_id, offset, length) =>
|
||||
@addCommentToSelection(thread_id, offset, length)
|
||||
|
@ -36,10 +36,10 @@ define [
|
|||
@removeCommentId(comment_id)
|
||||
|
||||
@$scope.$on "comment:resolve_threads", (e, thread_ids) =>
|
||||
@resolveCommentByThreadIds(thread_ids)
|
||||
@hideCommentsByThreadIds(thread_ids)
|
||||
|
||||
@$scope.$on "comment:unresolve_thread", (e, thread_id) =>
|
||||
@unresolveCommentByThreadId(thread_id)
|
||||
@showCommentByThreadId(thread_id)
|
||||
|
||||
@$scope.$on "review-panel:recalculate-screen-positions", () =>
|
||||
@recalculateReviewEntriesScreenPositions()
|
||||
|
@ -73,16 +73,24 @@ define [
|
|||
_scrollTimeout = null
|
||||
, 200
|
||||
|
||||
@_resetCutState()
|
||||
onCut = () => @onCut()
|
||||
onPaste = () => @onPaste()
|
||||
|
||||
bindToAce = () =>
|
||||
@editor.on "changeSelection", onChangeSelection
|
||||
@editor.on "change", onChangeSelection # Selection also moves with updates elsewhere in the document
|
||||
@editor.on "changeSession", onChangeSession
|
||||
@editor.on "cut", onCut
|
||||
@editor.on "paste", onPaste
|
||||
@editor.renderer.on "resize", onResize
|
||||
|
||||
unbindFromAce = () =>
|
||||
@editor.off "changeSelection", onChangeSelection
|
||||
@editor.off "change", onChangeSelection
|
||||
@editor.off "changeSession", onChangeSession
|
||||
@editor.off "cut", onCut
|
||||
@editor.off "paste", onPaste
|
||||
@editor.renderer.off "resize", onResize
|
||||
|
||||
@$scope.$watch "trackChangesEnabled", (enabled) =>
|
||||
|
@ -92,18 +100,11 @@ define [
|
|||
else
|
||||
unbindFromAce()
|
||||
|
||||
disconnectFromRangesTracker: () ->
|
||||
disconnectFromDoc: (doc) ->
|
||||
@changeIdToMarkerIdMap = {}
|
||||
|
||||
if @rangesTracker?
|
||||
@rangesTracker.off "insert:added"
|
||||
@rangesTracker.off "insert:removed"
|
||||
@rangesTracker.off "delete:added"
|
||||
@rangesTracker.off "delete:removed"
|
||||
@rangesTracker.off "changes:moved"
|
||||
@rangesTracker.off "comment:added"
|
||||
@rangesTracker.off "comment:moved"
|
||||
@rangesTracker.off "comment:removed"
|
||||
doc.off "ranges:clear"
|
||||
doc.off "ranges:redraw"
|
||||
doc.off "ranges:dirty"
|
||||
|
||||
setTrackChanges: (value) ->
|
||||
if value
|
||||
|
@ -111,56 +112,15 @@ define [
|
|||
else
|
||||
@$scope.sharejsDoc?.track_changes_as = null
|
||||
|
||||
connectToRangesTracker: () ->
|
||||
connectToDoc: (doc) ->
|
||||
@rangesTracker = doc.ranges
|
||||
@setTrackChanges(@$scope.trackChanges)
|
||||
|
||||
# Add a timeout because on remote ops, we get these notifications before
|
||||
# ace has updated
|
||||
@rangesTracker.on "insert:added", (change) =>
|
||||
sl_console.log "[insert:added]", change
|
||||
setTimeout () =>
|
||||
@_onInsertAdded(change)
|
||||
@broadcastChange()
|
||||
@rangesTracker.on "insert:removed", (change) =>
|
||||
sl_console.log "[insert:removed]", change
|
||||
setTimeout () =>
|
||||
@_onInsertRemoved(change)
|
||||
@broadcastChange()
|
||||
@rangesTracker.on "delete:added", (change) =>
|
||||
sl_console.log "[delete:added]", change
|
||||
setTimeout () =>
|
||||
@_onDeleteAdded(change)
|
||||
@broadcastChange()
|
||||
@rangesTracker.on "delete:removed", (change) =>
|
||||
sl_console.log "[delete:removed]", change
|
||||
setTimeout () =>
|
||||
@_onDeleteRemoved(change)
|
||||
@broadcastChange()
|
||||
@rangesTracker.on "changes:moved", (changes) =>
|
||||
sl_console.log "[changes:moved]", changes
|
||||
setTimeout () =>
|
||||
@_onChangesMoved(changes)
|
||||
@broadcastChange()
|
||||
|
||||
@rangesTracker.on "comment:added", (comment) =>
|
||||
sl_console.log "[comment:added]", comment
|
||||
setTimeout () =>
|
||||
@_onCommentAdded(comment)
|
||||
@broadcastChange()
|
||||
@rangesTracker.on "comment:moved", (comment) =>
|
||||
sl_console.log "[comment:moved]", comment
|
||||
setTimeout () =>
|
||||
@_onCommentMoved(comment)
|
||||
@broadcastChange()
|
||||
@rangesTracker.on "comment:removed", (comment) =>
|
||||
sl_console.log "[comment:removed]", comment
|
||||
setTimeout () =>
|
||||
@_onCommentRemoved(comment)
|
||||
@broadcastChange()
|
||||
|
||||
@rangesTracker.on "clear", () =>
|
||||
doc.on "ranges:dirty", () =>
|
||||
@updateAnnotations()
|
||||
doc.on "ranges:clear", () =>
|
||||
@clearAnnotations()
|
||||
@rangesTracker.on "redraw", () =>
|
||||
doc.on "ranges:redraw", () =>
|
||||
@redrawAnnotations()
|
||||
|
||||
clearAnnotations: () ->
|
||||
|
@ -181,6 +141,55 @@ define [
|
|||
@_onCommentAdded(comment)
|
||||
|
||||
@broadcastChange()
|
||||
|
||||
_doneUpdateThisLoop: false
|
||||
_pendingUpdates: false
|
||||
updateAnnotations: () ->
|
||||
# Doc updates with multiple ops, like search/replace or block comments
|
||||
# will call this with every individual op in a single event loop. So only
|
||||
# do the first this loop, then schedule an update for the next loop for the rest.
|
||||
if !@_doneUpdateThisLoop
|
||||
@_doUpdateAnnotations()
|
||||
@_doneUpdateThisLoop = true
|
||||
setTimeout () =>
|
||||
if @_pendingUpdates
|
||||
@_doUpdateAnnotations()
|
||||
@_doneUpdateThisLoop = false
|
||||
@_pendingUpdates = false
|
||||
else
|
||||
@_pendingUpdates = true
|
||||
|
||||
_doUpdateAnnotations: () ->
|
||||
dirty = @rangesTracker.getDirtyState()
|
||||
|
||||
updateMarkers = false
|
||||
|
||||
for id, change of dirty.change.added
|
||||
if change.op.i?
|
||||
@_onInsertAdded(change)
|
||||
else if change.op.d?
|
||||
@_onDeleteAdded(change)
|
||||
for id, change of dirty.change.removed
|
||||
if change.op.i?
|
||||
@_onInsertRemoved(change)
|
||||
else if change.op.d?
|
||||
@_onDeleteRemoved(change)
|
||||
for id, change of dirty.change.moved
|
||||
updateMarkers = true
|
||||
@_onChangeMoved(change)
|
||||
|
||||
for id, comment of dirty.comment.added
|
||||
@_onCommentAdded(comment)
|
||||
for id, comment of dirty.comment.removed
|
||||
@_onCommentRemoved(comment)
|
||||
for id, comment of dirty.comment.moved
|
||||
updateMarkers = true
|
||||
@_onCommentMoved(comment)
|
||||
|
||||
@rangesTracker.resetDirtyState()
|
||||
if updateMarkers
|
||||
@editor.renderer.updateBackMarkers()
|
||||
@broadcastChange()
|
||||
|
||||
addComment: (offset, content, thread_id) ->
|
||||
op = { c: content, p: offset, t: thread_id }
|
||||
|
@ -200,6 +209,7 @@ define [
|
|||
|
||||
acceptChangeId: (change_id) ->
|
||||
@rangesTracker.removeChangeId(change_id)
|
||||
@updateAnnotations()
|
||||
|
||||
rejectChangeId: (change_id) ->
|
||||
change = @rangesTracker.getChange(change_id)
|
||||
|
@ -208,21 +218,26 @@ define [
|
|||
if change.op.d?
|
||||
content = change.op.d
|
||||
position = @_shareJsOffsetToAcePosition(change.op.p)
|
||||
session.$fromReject = true # Tell track changes to cancel out delete
|
||||
session.insert(position, content)
|
||||
session.$fromReject = false
|
||||
else if change.op.i?
|
||||
start = @_shareJsOffsetToAcePosition(change.op.p)
|
||||
end = @_shareJsOffsetToAcePosition(change.op.p + change.op.i.length)
|
||||
editor_text = session.getDocument().getTextRange({start, end})
|
||||
if editor_text != change.op.i
|
||||
throw new Error("Op to be removed (#{JSON.stringify(change.op)}), does not match editor text, '#{editor_text}'")
|
||||
session.$fromReject = true
|
||||
session.remove({start, end})
|
||||
session.$fromReject = false
|
||||
else
|
||||
throw new Error("unknown change: #{JSON.stringify(change)}")
|
||||
|
||||
removeCommentId: (comment_id) ->
|
||||
@rangesTracker.removeCommentId(comment_id)
|
||||
@updateAnnotations()
|
||||
|
||||
resolveCommentByThreadIds: (thread_ids) ->
|
||||
hideCommentsByThreadIds: (thread_ids) ->
|
||||
resolve_ids = {}
|
||||
for id in thread_ids
|
||||
resolve_ids[id] = true
|
||||
|
@ -231,12 +246,55 @@ define [
|
|||
@_onCommentRemoved(comment)
|
||||
@broadcastChange()
|
||||
|
||||
unresolveCommentByThreadId: (thread_id) ->
|
||||
showCommentByThreadId: (thread_id) ->
|
||||
for comment in @rangesTracker?.comments or []
|
||||
if comment.op.t == thread_id
|
||||
@_onCommentAdded(comment)
|
||||
@broadcastChange()
|
||||
|
||||
_resetCutState: () ->
|
||||
@_cutState = {
|
||||
text: null
|
||||
comments: []
|
||||
docId: null
|
||||
}
|
||||
|
||||
onCut: () ->
|
||||
@_resetCutState()
|
||||
selection = @editor.getSelectionRange()
|
||||
selection_start = @_aceRangeToShareJs(selection.start)
|
||||
selection_end = @_aceRangeToShareJs(selection.end)
|
||||
@_cutState.text = @editor.getSelectedText()
|
||||
@_cutState.docId = @$scope.docId
|
||||
for comment in @rangesTracker.comments
|
||||
comment_start = comment.op.p
|
||||
comment_end = comment_start + comment.op.c.length
|
||||
if selection_start <= comment_start and comment_end <= selection_end
|
||||
@_cutState.comments.push {
|
||||
offset: comment.op.p - selection_start
|
||||
text: comment.op.c
|
||||
comment: comment
|
||||
}
|
||||
|
||||
onPaste: () =>
|
||||
@editor.once "change", (change) =>
|
||||
return if change.action != "insert"
|
||||
pasted_text = change.lines.join("\n")
|
||||
paste_offset = @_aceRangeToShareJs(change.start)
|
||||
# We have to wait until the change has been processed by the range tracker,
|
||||
# since if we move the ops into place beforehand, they will be moved again
|
||||
# when the changes are processed by the range tracker. This ranges:dirty
|
||||
# event is fired after the doc has applied the changes to the range tracker.
|
||||
@$scope.sharejsDoc.on "ranges:dirty.paste", () =>
|
||||
@$scope.sharejsDoc.off "ranges:dirty.paste" # Doc event emitter uses namespaced events
|
||||
if pasted_text == @_cutState.text and @$scope.docId == @_cutState.docId
|
||||
for {comment, offset, text} in @_cutState.comments
|
||||
op = { c: text, p: paste_offset + offset, t: comment.id }
|
||||
@$scope.sharejsDoc.submitOp op # Resubmitting an existing comment op (by thread id) will move it
|
||||
@_resetCutState()
|
||||
# Check that comments still match text. Will throw error if not.
|
||||
@rangesTracker.validate(@editor.getValue())
|
||||
|
||||
checkMapping: () ->
|
||||
# TODO: reintroduce this check
|
||||
session = @editor.getSession()
|
||||
|
@ -421,23 +479,18 @@ define [
|
|||
lines = @editor.getSession().getDocument().getAllLines()
|
||||
return AceShareJsCodec.shareJsOffsetToAcePosition(offset, lines)
|
||||
|
||||
_onChangesMoved: (changes) ->
|
||||
# TODO: PERFORMANCE: Only run through the Ace lines once, and calculate all
|
||||
# change positions as we go.
|
||||
for change in changes
|
||||
start = @_shareJsOffsetToAcePosition(change.op.p)
|
||||
if change.op.i?
|
||||
end = @_shareJsOffsetToAcePosition(change.op.p + change.op.i.length)
|
||||
else
|
||||
end = start
|
||||
@_updateMarker(change.id, start, end)
|
||||
@editor.renderer.updateBackMarkers()
|
||||
_onChangeMoved: (change) ->
|
||||
start = @_shareJsOffsetToAcePosition(change.op.p)
|
||||
if change.op.i?
|
||||
end = @_shareJsOffsetToAcePosition(change.op.p + change.op.i.length)
|
||||
else
|
||||
end = start
|
||||
@_updateMarker(change.id, start, end)
|
||||
|
||||
_onCommentMoved: (comment) ->
|
||||
start = @_shareJsOffsetToAcePosition(comment.op.p)
|
||||
end = @_shareJsOffsetToAcePosition(comment.op.p + comment.op.c.length)
|
||||
@_updateMarker(comment.id, start, end)
|
||||
@editor.renderer.updateBackMarkers()
|
||||
|
||||
_updateMarker: (change_id, start, end) ->
|
||||
return if !@changeIdToMarkerIdMap[change_id]?
|
||||
|
|
|
@ -11,10 +11,10 @@ define [
|
|||
show_remote_warning: false
|
||||
|
||||
@reset()
|
||||
@nextUpdateIsRemote = false
|
||||
|
||||
@editor.on "changeSession", (e) =>
|
||||
@reset()
|
||||
@session = e.session
|
||||
e.session.setUndoManager(@)
|
||||
|
||||
showUndoConflictWarning: () ->
|
||||
|
@ -38,20 +38,44 @@ define [
|
|||
@firstUpdate = false
|
||||
return
|
||||
aceDeltaSets = options.args[0]
|
||||
@session = options.args[1]
|
||||
return if !aceDeltaSets?
|
||||
@session = options.args[1]
|
||||
|
||||
lines = @session.getDocument().getAllLines()
|
||||
linesBeforeChange = @_revertAceDeltaSetsOnDocLines(aceDeltaSets, lines)
|
||||
simpleDeltaSets = @_aceDeltaSetsToSimpleDeltaSets(aceDeltaSets, linesBeforeChange)
|
||||
@undoStack.push(
|
||||
deltaSets: simpleDeltaSets
|
||||
remote: @nextUpdateIsRemote
|
||||
)
|
||||
# We need to split the delta sets into local or remote groups before pushing onto
|
||||
# the undo stack, since these are treated differently.
|
||||
splitDeltaSets = []
|
||||
currentDeltaSet = null # Make global to this function
|
||||
do newDeltaSet = () ->
|
||||
currentDeltaSet = {group: "doc", deltas: []}
|
||||
splitDeltaSets.push currentDeltaSet
|
||||
currentRemoteState = null
|
||||
|
||||
for deltaSet in aceDeltaSets or []
|
||||
if deltaSet.group == "doc" # ignore code folding etc.
|
||||
for delta in deltaSet.deltas
|
||||
if currentDeltaSet.remote? and currentDeltaSet.remote != !!delta.remote
|
||||
newDeltaSet()
|
||||
currentDeltaSet.deltas.push delta
|
||||
currentDeltaSet.remote = !!delta.remote
|
||||
|
||||
# The lines are currently as they are after applying all these deltas, but to turn into simple deltas,
|
||||
# we need the lines before each delta group.
|
||||
docLines = @session.getDocument().getAllLines()
|
||||
docLines = @_revertAceDeltaSetsOnDocLines(aceDeltaSets, docLines)
|
||||
for deltaSet in splitDeltaSets
|
||||
{simpleDeltaSet, docLines} = @_aceDeltaSetToSimpleDeltaSet(deltaSet, docLines)
|
||||
frame = {
|
||||
deltaSets: [simpleDeltaSet]
|
||||
remote: deltaSet.remote
|
||||
}
|
||||
@undoStack.push frame
|
||||
@redoStack = []
|
||||
@nextUpdateIsRemote = false
|
||||
|
||||
undo: (dontSelect) ->
|
||||
# We rely on the doclines being in sync with the undo stack, so make sure
|
||||
# any pending undo deltas are processed.
|
||||
@session.$syncInformUndoManager()
|
||||
|
||||
localUpdatesMade = @_shiftLocalChangeToTopOfUndoStack()
|
||||
return if !localUpdatesMade
|
||||
|
||||
|
@ -206,19 +230,16 @@ define [
|
|||
throw "Unknown delta type"
|
||||
return doc.split("\n")
|
||||
|
||||
_aceDeltaSetsToSimpleDeltaSets: (aceDeltaSets, docLines) ->
|
||||
simpleDeltaSets = []
|
||||
for deltaSet in aceDeltaSets
|
||||
if deltaSet.group == "doc" # ignore fold changes
|
||||
simpleDeltas = []
|
||||
for delta in deltaSet.deltas
|
||||
simpleDeltas.push @_aceDeltaToSimpleDelta(delta, docLines)
|
||||
docLines = @_applyAceDeltasToDocLines([delta], docLines)
|
||||
simpleDeltaSets.push {
|
||||
deltas: simpleDeltas
|
||||
group: deltaSet.group
|
||||
}
|
||||
return simpleDeltaSets
|
||||
_aceDeltaSetToSimpleDeltaSet: (deltaSet, docLines) ->
|
||||
simpleDeltas = []
|
||||
for delta in deltaSet.deltas
|
||||
simpleDeltas.push @_aceDeltaToSimpleDelta(delta, docLines)
|
||||
docLines = @_applyAceDeltasToDocLines([delta], docLines)
|
||||
simpleDeltaSet = {
|
||||
deltas: simpleDeltas
|
||||
group: deltaSet.group
|
||||
}
|
||||
return {simpleDeltaSet, docLines}
|
||||
|
||||
_simpleDeltaSetsToAceDeltaSets: (simpleDeltaSets, docLines) ->
|
||||
for deltaSet in simpleDeltaSets
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
Range = ace.require("ace/range").Range
|
||||
|
||||
# Convert an ace delta into an op understood by share.js
|
||||
applyToShareJS = (editorDoc, delta, doc) ->
|
||||
applyToShareJS = (editorDoc, delta, doc, fromUndo) ->
|
||||
# Get the start position of the range, in no. of characters
|
||||
getStartOffsetPosition = (start) ->
|
||||
# This is quite inefficient - getLines makes a copy of the entire
|
||||
|
@ -27,11 +27,11 @@ applyToShareJS = (editorDoc, delta, doc) ->
|
|||
switch delta.action
|
||||
when 'insert'
|
||||
text = delta.lines.join('\n')
|
||||
doc.insert pos, text
|
||||
doc.insert pos, text, fromUndo
|
||||
|
||||
when 'remove'
|
||||
text = delta.lines.join('\n')
|
||||
doc.del pos, text.length
|
||||
doc.del pos, text.length, fromUndo
|
||||
|
||||
else throw new Error "unknown action: #{delta.action}"
|
||||
|
||||
|
@ -78,8 +78,10 @@ window.sharejs.extendDoc 'attach_ace', (editor, keepEditorContents, maxDocLength
|
|||
if maxDocLength? and editorDoc.getValue().length > maxDocLength
|
||||
doc.emit "error", new Error("document length is greater than maxDocLength")
|
||||
return
|
||||
|
||||
fromUndo = !!(editor.getSession().$fromUndo or editor.getSession().$fromReject)
|
||||
|
||||
applyToShareJS editorDoc, change, doc
|
||||
applyToShareJS editorDoc, change, doc, fromUndo
|
||||
|
||||
check()
|
||||
|
||||
|
@ -108,16 +110,46 @@ window.sharejs.extendDoc 'attach_ace', (editor, keepEditorContents, maxDocLength
|
|||
|
||||
row:row, column:offset
|
||||
|
||||
# We want to insert a remote:true into the delta if the op comes from the
|
||||
# underlying sharejs doc (which means it is from a remote op), so we have to do
|
||||
# the work of editorDoc.insert and editorDoc.remove manually. These methods are
|
||||
# copied from ace.js doc#insert and #remove, and then inject the remote:true
|
||||
# flag into the delta.
|
||||
doc.on 'insert', (pos, text) ->
|
||||
if (editorDoc.getLength() <= 1)
|
||||
editorDoc.$detectNewLine(text)
|
||||
|
||||
lines = editorDoc.$split(text)
|
||||
position = offsetToPos(pos)
|
||||
start = editorDoc.clippedPos(position.row, position.column)
|
||||
end = {
|
||||
row: start.row + lines.length - 1,
|
||||
column: (if lines.length == 1 then start.column else 0) + lines[lines.length - 1].length
|
||||
}
|
||||
|
||||
suppress = true
|
||||
editorDoc.insert offsetToPos(pos), text
|
||||
editorDoc.applyDelta({
|
||||
start: start,
|
||||
end: end,
|
||||
action: "insert",
|
||||
lines: lines,
|
||||
remote: true
|
||||
});
|
||||
suppress = false
|
||||
check()
|
||||
|
||||
doc.on 'delete', (pos, text) ->
|
||||
suppress = true
|
||||
range = Range.fromPoints offsetToPos(pos), offsetToPos(pos + text.length)
|
||||
editorDoc.remove range
|
||||
start = editorDoc.clippedPos(range.start.row, range.start.column)
|
||||
end = editorDoc.clippedPos(range.end.row, range.end.column)
|
||||
suppress = true
|
||||
editorDoc.applyDelta({
|
||||
start: start,
|
||||
end: end,
|
||||
action: "remove",
|
||||
lines: editorDoc.getLinesForRange({start: start, end: end})
|
||||
remote: true
|
||||
});
|
||||
suppress = false
|
||||
check()
|
||||
|
||||
|
|
|
@ -11,14 +11,20 @@ text.api =
|
|||
# Get the text contents of a document
|
||||
getText: -> @snapshot
|
||||
|
||||
insert: (pos, text, callback) ->
|
||||
op = [{p:pos, i:text}]
|
||||
insert: (pos, text, fromUndo, callback) ->
|
||||
op = {p:pos, i:text}
|
||||
if fromUndo
|
||||
op.u = true
|
||||
op = [op]
|
||||
|
||||
@submitOp op, callback
|
||||
op
|
||||
|
||||
del: (pos, length, callback) ->
|
||||
op = [{p:pos, d:@snapshot[pos...(pos + length)]}]
|
||||
del: (pos, length, fromUndo, callback) ->
|
||||
op = {p:pos, d:@snapshot[pos...(pos + length)]}
|
||||
if fromUndo
|
||||
op.u = true
|
||||
op = [op]
|
||||
|
||||
@submitOp op, callback
|
||||
op
|
||||
|
|
|
@ -56,6 +56,13 @@ text.apply = (snapshot, op) ->
|
|||
throw new Error "Unknown op type"
|
||||
snapshot
|
||||
|
||||
cloneAndModify = (op, modifications) ->
|
||||
newOp = {}
|
||||
for k,v of op
|
||||
newOp[k] = v
|
||||
for k,v of modifications
|
||||
newOp[k] = v
|
||||
return newOp
|
||||
|
||||
# Exported for use by the random op generator.
|
||||
#
|
||||
|
@ -69,10 +76,10 @@ text._append = append = (newOp, c) ->
|
|||
last = newOp[newOp.length - 1]
|
||||
|
||||
# Compose the insert into the previous insert if possible
|
||||
if last.i? && c.i? and last.p <= c.p <= (last.p + last.i.length)
|
||||
newOp[newOp.length - 1] = {i:strInject(last.i, c.p - last.p, c.i), p:last.p}
|
||||
else if last.d? && c.d? and c.p <= last.p <= (c.p + c.d.length)
|
||||
newOp[newOp.length - 1] = {d:strInject(c.d, last.p - c.p, last.d), p:c.p}
|
||||
if last.i? && c.i? and last.p <= c.p <= (last.p + last.i.length) and last.u == c.u
|
||||
newOp[newOp.length - 1] = cloneAndModify(last, {i:strInject(last.i, c.p - last.p, c.i)})
|
||||
else if last.d? && c.d? and c.p <= last.p <= (c.p + c.d.length) and last.u == c.u
|
||||
newOp[newOp.length - 1] = cloneAndModify(last, {d:strInject(c.d, last.p - c.p, last.d), p: c.p})
|
||||
else
|
||||
newOp.push c
|
||||
|
||||
|
@ -150,25 +157,25 @@ text._tc = transformComponent = (dest, c, otherC, side) ->
|
|||
checkValidOp [otherC]
|
||||
|
||||
if c.i?
|
||||
append dest, {i:c.i, p:transformPosition(c.p, otherC, side == 'right')}
|
||||
append dest, cloneAndModify(c, {p:transformPosition(c.p, otherC, side == 'right')})
|
||||
|
||||
else if c.d? # Delete
|
||||
if otherC.i? # delete vs insert
|
||||
s = c.d
|
||||
if c.p < otherC.p
|
||||
append dest, {d:s[...otherC.p - c.p], p:c.p}
|
||||
append dest, cloneAndModify(c, {d:s[...otherC.p - c.p]})
|
||||
s = s[(otherC.p - c.p)..]
|
||||
if s != ''
|
||||
append dest, {d:s, p:c.p + otherC.i.length}
|
||||
append dest, cloneAndModify(c, {d:s, p:c.p + otherC.i.length})
|
||||
|
||||
else if otherC.d? # Delete vs delete
|
||||
if c.p >= otherC.p + otherC.d.length
|
||||
append dest, {d:c.d, p:c.p - otherC.d.length}
|
||||
append dest, cloneAndModify(c, {p:c.p - otherC.d.length})
|
||||
else if c.p + c.d.length <= otherC.p
|
||||
append dest, c
|
||||
else
|
||||
# They overlap somewhere.
|
||||
newC = {d:'', p:c.p}
|
||||
newC = cloneAndModify(c, {d:''})
|
||||
if c.p < otherC.p
|
||||
newC.d = c.d[...(otherC.p - c.p)]
|
||||
if c.p + c.d.length > otherC.p + otherC.d.length
|
||||
|
@ -198,18 +205,18 @@ text._tc = transformComponent = (dest, c, otherC, side) ->
|
|||
if c.p < otherC.p < c.p + c.c.length
|
||||
offset = otherC.p - c.p
|
||||
new_c = (c.c[0..(offset-1)] + otherC.i + c.c[offset...])
|
||||
append dest, {c:new_c, p:c.p, t: c.t}
|
||||
append dest, cloneAndModify(c, {c:new_c})
|
||||
else
|
||||
append dest, {c:c.c, p:transformPosition(c.p, otherC, true), t: c.t}
|
||||
append dest, cloneAndModify(c, {p:transformPosition(c.p, otherC, true)})
|
||||
|
||||
else if otherC.d?
|
||||
if c.p >= otherC.p + otherC.d.length
|
||||
append dest, {c:c.c, p:c.p - otherC.d.length, t: c.t}
|
||||
append dest, cloneAndModify(c, {p:c.p - otherC.d.length})
|
||||
else if c.p + c.c.length <= otherC.p
|
||||
append dest, c
|
||||
else # Delete overlaps comment
|
||||
# They overlap somewhere.
|
||||
newC = {c:'', p:c.p, t: c.t}
|
||||
newC = cloneAndModify(c, {c:''})
|
||||
if c.p < otherC.p
|
||||
newC.c = c.c[...(otherC.p - c.p)]
|
||||
if c.p + c.c.length > otherC.p + otherC.d.length
|
||||
|
|
|
@ -173,6 +173,8 @@ define [
|
|||
@_findEntityByPathInFolder @$scope.rootFolder, path
|
||||
|
||||
_findEntityByPathInFolder: (folder, path) ->
|
||||
if !path? or !folder?
|
||||
return null
|
||||
parts = path.split("/")
|
||||
name = parts.shift()
|
||||
rest = parts.join("/")
|
||||
|
|
|
@ -93,11 +93,11 @@ define [
|
|||
if !name? or name.length == 0
|
||||
return
|
||||
$scope.state.inflight = true
|
||||
$scope.state.inflight = true
|
||||
ide.fileTreeManager
|
||||
.createFolder(name, parent_folder)
|
||||
.error (e)->
|
||||
$scope.error = e
|
||||
$scope.state.inflight = false
|
||||
.success () ->
|
||||
$scope.state.inflight = false
|
||||
$modalInstance.close()
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
load = (EventEmitter) ->
|
||||
class RangesTracker extends EventEmitter
|
||||
# This file is shared between document-updater and web, so that the server and client share
|
||||
# an identical track changes implementation. Do not edit it directly in web or document-updater,
|
||||
# instead edit it at https://github.com/sharelatex/ranges-tracker, where it has a suite of tests
|
||||
load = () ->
|
||||
class RangesTracker
|
||||
# The purpose of this class is to track a set of inserts and deletes to a document, like
|
||||
# track changes in Word. We store these as a set of ShareJs style ranges:
|
||||
# {i: "foo", p: 42} # Insert 'foo' at offset 42
|
||||
|
@ -36,6 +39,7 @@ load = (EventEmitter) ->
|
|||
# middle of a previous insert by the first user, the original insert will be split into two.
|
||||
constructor: (@changes = [], @comments = []) ->
|
||||
@setIdSeed(RangesTracker.generateIdSeed())
|
||||
@resetDirtyState()
|
||||
|
||||
getIdSeed: () ->
|
||||
return @id_seed
|
||||
|
@ -75,8 +79,15 @@ load = (EventEmitter) ->
|
|||
comment = @getComment(comment_id)
|
||||
return if !comment?
|
||||
@comments = @comments.filter (c) -> c.id != comment_id
|
||||
@emit "comment:removed", comment
|
||||
@_markAsDirty comment, "comment", "removed"
|
||||
|
||||
moveCommentId: (comment_id, position, text) ->
|
||||
for comment in @comments
|
||||
if comment.id == comment_id
|
||||
comment.op.p = position
|
||||
comment.op.c = text
|
||||
@_markAsDirty comment, "comment", "moved"
|
||||
|
||||
getChange: (change_id) ->
|
||||
change = null
|
||||
for c in @changes
|
||||
|
@ -89,6 +100,18 @@ load = (EventEmitter) ->
|
|||
change = @getChange(change_id)
|
||||
return if !change?
|
||||
@_removeChange(change)
|
||||
|
||||
validate: (text) ->
|
||||
for change in @changes
|
||||
if change.op.i?
|
||||
content = text.slice(change.op.p, change.op.p + change.op.i.length)
|
||||
if content != change.op.i
|
||||
throw new Error("Change (#{JSON.stringify(change)}) doesn't match text (#{JSON.stringify(content)})")
|
||||
for comment in @comments
|
||||
content = text.slice(comment.op.p, comment.op.p + comment.op.c.length)
|
||||
if content != comment.op.c
|
||||
throw new Error("Comment (#{JSON.stringify(comment)}) doesn't match text (#{JSON.stringify(content)})")
|
||||
return true
|
||||
|
||||
applyOp: (op, metadata = {}) ->
|
||||
metadata.ts ?= new Date()
|
||||
|
@ -103,29 +126,37 @@ load = (EventEmitter) ->
|
|||
@addComment(op, metadata)
|
||||
else
|
||||
throw new Error("unknown op type")
|
||||
|
||||
|
||||
applyOps: (ops, metadata = {}) ->
|
||||
for op in ops
|
||||
@applyOp(op, metadata)
|
||||
|
||||
addComment: (op, metadata) ->
|
||||
# TODO: Don't allow overlapping comments?
|
||||
@comments.push comment = {
|
||||
id: op.t or @newId()
|
||||
op: # Copy because we'll modify in place
|
||||
c: op.c
|
||||
p: op.p
|
||||
t: op.t
|
||||
metadata
|
||||
}
|
||||
@emit "comment:added", comment
|
||||
return comment
|
||||
existing = @getComment(op.t)
|
||||
if existing?
|
||||
@moveCommentId(op.t, op.p, op.c)
|
||||
return existing
|
||||
else
|
||||
@comments.push comment = {
|
||||
id: op.t or @newId()
|
||||
op: # Copy because we'll modify in place
|
||||
c: op.c
|
||||
p: op.p
|
||||
t: op.t
|
||||
metadata
|
||||
}
|
||||
@_markAsDirty comment, "comment", "added"
|
||||
return comment
|
||||
|
||||
applyInsertToComments: (op) ->
|
||||
for comment in @comments
|
||||
if op.p <= comment.op.p
|
||||
comment.op.p += op.i.length
|
||||
@emit "comment:moved", comment
|
||||
@_markAsDirty comment, "comment", "moved"
|
||||
else if op.p < comment.op.p + comment.op.c.length
|
||||
offset = op.p - comment.op.p
|
||||
comment.op.c = comment.op.c[0..(offset-1)] + op.i + comment.op.c[offset...]
|
||||
@emit "comment:moved", comment
|
||||
@_markAsDirty comment, "comment", "moved"
|
||||
|
||||
applyDeleteToComments: (op) ->
|
||||
op_start = op.p
|
||||
|
@ -138,7 +169,7 @@ load = (EventEmitter) ->
|
|||
if op_end <= comment_start
|
||||
# delete is fully before comment
|
||||
comment.op.p -= op_length
|
||||
@emit "comment:moved", comment
|
||||
@_markAsDirty comment, "comment", "moved"
|
||||
else if op_start >= comment_end
|
||||
# delete is fully after comment, nothing to do
|
||||
else
|
||||
|
@ -161,12 +192,13 @@ load = (EventEmitter) ->
|
|||
|
||||
comment.op.p = Math.min(comment_start, op_start)
|
||||
comment.op.c = remaining_before + remaining_after
|
||||
@emit "comment:moved", comment
|
||||
@_markAsDirty comment, "comment", "moved"
|
||||
|
||||
applyInsertToChanges: (op, metadata) ->
|
||||
op_start = op.p
|
||||
op_length = op.i.length
|
||||
op_end = op.p + op_length
|
||||
undoing = !!op.u
|
||||
|
||||
|
||||
already_merged = false
|
||||
|
@ -184,8 +216,9 @@ load = (EventEmitter) ->
|
|||
change.op.p += op_length
|
||||
moved_changes.push change
|
||||
else if op_start == change_start
|
||||
# If the insert matches the start of the delete, just remove it from the delete instead
|
||||
if change.op.d.length >= op.i.length and change.op.d.slice(0, op.i.length) == op.i
|
||||
# If we are undoing, then we want to cancel any existing delete ranges if we can.
|
||||
# Check if the insert matches the start of the delete, and just remove it from the delete instead if so.
|
||||
if undoing and change.op.d.length >= op.i.length and change.op.d.slice(0, op.i.length) == op.i
|
||||
change.op.d = change.op.d.slice(op.i.length)
|
||||
change.op.p += op.i.length
|
||||
if change.op.d == ""
|
||||
|
@ -203,15 +236,15 @@ load = (EventEmitter) ->
|
|||
# Only merge inserts if they are from the same user
|
||||
is_same_user = metadata.user_id == change.metadata.user_id
|
||||
|
||||
# If this is an insert op at the end of an existing insert with a delete following, and it cancels out the following
|
||||
# delete then we shouldn't append it to this insert, but instead only cancel the following delete.
|
||||
# If we are undoing, then our changes will be removed from any delete ops just after. In that case, if there is also
|
||||
# an insert op just before, then we shouldn't append it to this insert, but instead only cancel the following delete.
|
||||
# E.g.
|
||||
# foo|<--- about to insert 'b' here
|
||||
# inserted 'foo' --^ ^-- deleted 'bar'
|
||||
# should become just 'foo' not 'foob' (with the delete marker becoming just 'ar'), .
|
||||
next_change = @changes[i+1]
|
||||
is_op_adjacent_to_next_delete = next_change? and next_change.op.d? and op.p == change_end and next_change.op.p == op.p
|
||||
will_op_cancel_next_delete = is_op_adjacent_to_next_delete and next_change.op.d.slice(0, op.i.length) == op.i
|
||||
will_op_cancel_next_delete = undoing and is_op_adjacent_to_next_delete and next_change.op.d.slice(0, op.i.length) == op.i
|
||||
|
||||
# If there is a delete at the start of the insert, and we're inserting
|
||||
# at the start, we SHOULDN'T merge since the delete acts as a partition.
|
||||
|
@ -281,8 +314,8 @@ load = (EventEmitter) ->
|
|||
for change in remove_changes
|
||||
@_removeChange change
|
||||
|
||||
if moved_changes.length > 0
|
||||
@emit "changes:moved", moved_changes
|
||||
for change in moved_changes
|
||||
@_markAsDirty change, "change", "moved"
|
||||
|
||||
applyDeleteToChanges: (op, metadata) ->
|
||||
op_start = op.p
|
||||
|
@ -406,8 +439,8 @@ load = (EventEmitter) ->
|
|||
@_removeChange change
|
||||
moved_changes = moved_changes.filter (c) -> c != change
|
||||
|
||||
if moved_changes.length > 0
|
||||
@emit "changes:moved", moved_changes
|
||||
for change in moved_changes
|
||||
@_markAsDirty change, "change", "moved"
|
||||
|
||||
_addOp: (op, metadata) ->
|
||||
change = {
|
||||
|
@ -427,17 +460,11 @@ load = (EventEmitter) ->
|
|||
else
|
||||
return -1
|
||||
|
||||
if op.d?
|
||||
@emit "delete:added", change
|
||||
else if op.i?
|
||||
@emit "insert:added", change
|
||||
@_markAsDirty(change, "change", "added")
|
||||
|
||||
_removeChange: (change) ->
|
||||
@changes = @changes.filter (c) -> c.id != change.id
|
||||
if change.op.d?
|
||||
@emit "delete:removed", change
|
||||
else if change.op.i?
|
||||
@emit "insert:removed", change
|
||||
@_markAsDirty change, "change", "removed"
|
||||
|
||||
_applyOpModifications: (content, op_modifications) ->
|
||||
# Put in descending position order, with deleting first if at the same offset
|
||||
|
@ -486,13 +513,32 @@ load = (EventEmitter) ->
|
|||
previous_change = change
|
||||
return { moved_changes, remove_changes }
|
||||
|
||||
resetDirtyState: () ->
|
||||
@_dirtyState = {
|
||||
comment: {
|
||||
moved: {}
|
||||
removed: {}
|
||||
added: {}
|
||||
}
|
||||
change: {
|
||||
moved: {}
|
||||
removed: {}
|
||||
added: {}
|
||||
}
|
||||
}
|
||||
|
||||
getDirtyState: () ->
|
||||
return @_dirtyState
|
||||
|
||||
_markAsDirty: (object, type, action) ->
|
||||
@_dirtyState[type][action][object.id] = object
|
||||
|
||||
_clone: (object) ->
|
||||
clone = {}
|
||||
(clone[k] = v for k,v of object)
|
||||
return clone
|
||||
|
||||
if define?
|
||||
define ["utils/EventEmitter"], load
|
||||
define [], load
|
||||
else
|
||||
EventEmitter = require("events").EventEmitter
|
||||
module.exports = load(EventEmitter)
|
||||
module.exports = load()
|
||||
|
|
|
@ -4,7 +4,7 @@ define [
|
|||
"ide/colors/ColorManager"
|
||||
"ide/review-panel/RangesTracker"
|
||||
], (App, EventEmitter, ColorManager, RangesTracker) ->
|
||||
App.controller "ReviewPanelController", ($scope, $element, ide, $timeout, $http, $modal, event_tracking, localStorage) ->
|
||||
App.controller "ReviewPanelController", ($scope, $element, ide, $timeout, $http, $modal, event_tracking, sixpack, localStorage) ->
|
||||
$reviewPanelEl = $element.find "#review-panel"
|
||||
|
||||
$scope.SubViews =
|
||||
|
@ -27,6 +27,14 @@ define [
|
|||
layoutToLeft: false
|
||||
rendererData: {}
|
||||
loadingThreads: false
|
||||
newAddCommentUI: false # Test new UI for adding comments; remove afterwards.
|
||||
|
||||
$scope.shouldABAddCommentBtn = false
|
||||
if $scope.user.signUpDate >= '2017-03-27'
|
||||
sixpack.participate "add-comment-btn", [ "default", "editor-corner" ], (variation) ->
|
||||
$scope.shouldABAddCommentBtn = true
|
||||
$scope.variationABAddCommentBtn = variation
|
||||
$scope.reviewPanel.newAddCommentUI = (variation == "editor-corner")
|
||||
|
||||
window.addEventListener "beforeunload", () ->
|
||||
collapsedStates = {}
|
||||
|
@ -163,7 +171,11 @@ define [
|
|||
|
||||
$scope.$watch (() ->
|
||||
entries = $scope.reviewPanel.entries[$scope.editor.open_doc_id] or {}
|
||||
Object.keys(entries).length
|
||||
permEntries = {}
|
||||
for entry, entryData of entries
|
||||
if entry != "add-comment" or !$scope.reviewPanel.newAddCommentUI
|
||||
permEntries[entry] = entryData
|
||||
Object.keys(permEntries).length
|
||||
), (nEntries) ->
|
||||
$scope.reviewPanel.hasEntries = nEntries > 0 and $scope.project.features.trackChangesVisible
|
||||
|
||||
|
@ -288,18 +300,11 @@ define [
|
|||
|
||||
delete entries["add-comment"]
|
||||
if selection
|
||||
# Only show add comment if we're not already overlapping one
|
||||
overlapping_comment = false
|
||||
for id, entry of entries
|
||||
if entry.type == "comment" and not $scope.reviewPanel.resolvedThreadIds[entry.thread_id]
|
||||
unless entry.offset >= selection_offset_end or entry.offset + entry.content.length <= selection_offset_start
|
||||
overlapping_comment = true
|
||||
if !overlapping_comment
|
||||
entries["add-comment"] = {
|
||||
type: "add-comment"
|
||||
offset: selection_offset_start
|
||||
length: selection_offset_end - selection_offset_start
|
||||
}
|
||||
entries["add-comment"] = {
|
||||
type: "add-comment"
|
||||
offset: selection_offset_start
|
||||
length: selection_offset_end - selection_offset_start
|
||||
}
|
||||
|
||||
for id, entry of entries
|
||||
if entry.type == "comment" and not $scope.reviewPanel.resolvedThreadIds[entry.thread_id]
|
||||
|
@ -323,11 +328,17 @@ define [
|
|||
$scope.$broadcast "change:reject", entry_id
|
||||
event_tracking.sendMB "rp-change-rejected", { view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini' }
|
||||
|
||||
$scope.addNewComment = () ->
|
||||
$scope.$broadcast "comment:start_adding"
|
||||
$scope.toggleReviewPanel()
|
||||
|
||||
$scope.startNewComment = () ->
|
||||
$scope.$broadcast "comment:select_line"
|
||||
$timeout () ->
|
||||
$scope.$broadcast "review-panel:layout"
|
||||
|
||||
if $scope.shouldABAddCommentBtn and !$scope.ui.reviewPanelOpen
|
||||
sixpack.convert "add-comment-btn"
|
||||
|
||||
$scope.submitNewComment = (content) ->
|
||||
return if !content? or content == ""
|
||||
doc_id = $scope.editor.open_doc_id
|
||||
|
@ -396,7 +407,7 @@ define [
|
|||
return if !thread?
|
||||
thread.resolved = true
|
||||
thread.resolved_by_user = formatUser(user)
|
||||
thread.resolved_at = new Date()
|
||||
thread.resolved_at = new Date().toISOString()
|
||||
$scope.reviewPanel.resolvedThreadIds[thread_id] = true
|
||||
$scope.$broadcast "comment:resolve_threads", [thread_id]
|
||||
|
||||
|
|
|
@ -8,13 +8,16 @@ define [
|
|||
onStartNew: "&"
|
||||
onSubmit: "&"
|
||||
onCancel: "&"
|
||||
onIndicatorClick: "&"
|
||||
onIndicatorClick: "&"
|
||||
layoutToLeft: "="
|
||||
link: (scope, element, attrs) ->
|
||||
scope.state =
|
||||
isAdding: false
|
||||
content: ""
|
||||
|
||||
scope.$on "comment:start_adding", () ->
|
||||
scope.startNewComment()
|
||||
|
||||
scope.startNewComment = () ->
|
||||
scope.state.isAdding = true
|
||||
scope.onStartNew()
|
||||
|
@ -31,7 +34,10 @@ define [
|
|||
if scope.state.content.length > 0
|
||||
scope.submitNewComment()
|
||||
|
||||
scope.submitNewComment = () ->
|
||||
scope.submitNewComment = (event) ->
|
||||
# If this is from a blur event from clicking on cancel, ignore it.
|
||||
if event? and event.type == "blur" and $(event.relatedTarget).hasClass("rp-entry-button-cancel")
|
||||
return true
|
||||
scope.onSubmit { content: scope.state.content }
|
||||
scope.state.isAdding = false
|
||||
scope.state.content = ""
|
||||
|
|
|
@ -2,9 +2,13 @@ define [
|
|||
"base"
|
||||
], (App) ->
|
||||
MAX_PROJECT_NAME_LENGTH = 150
|
||||
App.controller "ProjectNameController", ["$scope", "settings", "ide", ($scope, settings, ide) ->
|
||||
App.controller "ProjectNameController", ["$scope", "$element", "settings", "ide", ($scope, $element, settings, ide) ->
|
||||
projectNameReadOnlyEl = $element.find(".name")[0]
|
||||
|
||||
$scope.state =
|
||||
renaming: false
|
||||
overflowed: false
|
||||
|
||||
$scope.inputs = {}
|
||||
|
||||
$scope.startRenaming = () ->
|
||||
|
@ -29,4 +33,7 @@ define [
|
|||
$scope.$watch "project.name", (name) ->
|
||||
if name?
|
||||
window.document.title = name + " - Online LaTeX Editor ShareLaTeX"
|
||||
$scope.$applyAsync () ->
|
||||
# This ensures that the element is measured *after* the binding is done (i.e. project name is rendered).
|
||||
$scope.state.overflowed = (projectNameReadOnlyEl.scrollWidth > projectNameReadOnlyEl.clientWidth)
|
||||
]
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 5.3 KiB |
29
services/web/public/img/brand/404-visual.svg
Normal file
|
@ -0,0 +1,29 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="777.5" height="820.52" viewBox="0 0 777.5 820.52">
|
||||
<g>
|
||||
<g>
|
||||
<circle cx="388.75" cy="388.75" r="388.75" fill="#798ed2" opacity="0.05"/>
|
||||
<rect x="182.22" y="230.37" width="413.04" height="295.03" fill="#a93529"/>
|
||||
<path d="M389.5,229.95s9.34-15.08,42.47-13.25,133.77,14.17,133.77,14.17V492.25s-91.3-16.45-141.84-11.88c-22.82,2.06-35.25,11.88-35.25,11.88S390.13,230.63,389.5,229.95Z" fill="#fff"/>
|
||||
<path d="M389.18,230.52s-10.59-15.5-43.72-13.67S211.7,231,211.7,231V492.41S303,476,353.53,480.52c22.82,2.06,35.25,11.88,35.25,11.88S388.54,231.2,389.18,230.52Z" fill="#fff"/>
|
||||
<path d="M388.78,487.15v5.26s0-1.88,0-5.26Z" fill="#e9e6d9"/>
|
||||
<g>
|
||||
<circle cx="195.09" cy="656.25" r="12.92" fill="#a93529"/>
|
||||
<path d="M194.85,696.16c2.64,5.28,23.77,6.3,46.21,6.16,22.66-.14,43.81-.66,46.53-5.94,6.85-13.27-34.66-41.88-46.42-41.88C227.16,654.5,187.73,681.91,194.85,696.16Z" fill="#a93529"/>
|
||||
<circle cx="220.92" cy="630.4" r="12.92" fill="#a93529"/>
|
||||
<circle cx="261.54" cy="630.4" r="12.92" fill="#a93529"/>
|
||||
<circle cx="287.38" cy="656.25" r="12.92" fill="#a93529"/>
|
||||
</g>
|
||||
<g>
|
||||
<circle cx="490.12" cy="774.44" r="12.92" fill="#a93529"/>
|
||||
<path d="M489.89,814.35c2.64,5.28,23.77,6.3,46.21,6.16,22.66-.14,43.81-.66,46.53-5.94,6.85-13.27-34.66-41.88-46.42-41.88C522.19,772.69,482.76,800.11,489.89,814.35Z" fill="#a93529"/>
|
||||
<circle cx="515.96" cy="748.6" r="12.92" fill="#a93529"/>
|
||||
<circle cx="556.58" cy="748.6" r="12.92" fill="#a93529"/>
|
||||
<circle cx="582.41" cy="774.44" r="12.92" fill="#a93529"/>
|
||||
</g>
|
||||
<rect x="211.7" y="406.39" width="354.03" height="88.87" fill="#fff"/>
|
||||
<path d="M502.94,451.45c-58.5-14.55-114.29-5-114.29,40.8L386.37,228s.41-11.71,16.15-24.17l-3.15,27.79L442.13,218l-25.65,30.49L427.12,257l-14.5,20.45,31.82,54.39L422.55,370.5l21.88,42.19-20.3,17.14Z" fill="#cfcfcf"/>
|
||||
<path d="M353.53,480.52c22.82,2.06,35.25,11.88,35.25,11.88v2.86H211.7v-2.86S303,476,353.53,480.52Z" fill="#e7e6e6"/>
|
||||
<path d="M423.89,480.38c-22.82,2.08-35.25,12-35.25,12v2.88H565.73v-2.88S474.43,475.76,423.89,480.38Z" fill="#e7e6e6"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
10
services/web/public/img/brand/500-visual-plug.svg
Normal file
|
@ -0,0 +1,10 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="650.95" height="260.49" viewBox="0 0 650.95 260.49">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M464.53,260.49c-27.17,0-75.15-13.71-105.35-46.51-25.23-27.4-26.69-55.26-28.09-82.2-1-18.92-1.92-36.79-11-53.72C290.25,22.75,182.7-.54.34,8.84L0,2.09C187.78-7.58,294.42,16.23,326,74.86c9.84,18.27,10.86,37.74,11.84,56.57,1.35,25.75,2.74,52.37,26.32,78,30.92,33.59,81.25,46.18,105.54,44.12,8.27-.7,9.89-2.86,10-3.1,2.59-4-15.91-19.6-29.41-31-30.14-25.42-67.66-57.07-58.22-86.53,5.29-16.54,24-28.7,57.23-37.18l1.67,6.55C420.66,110,403,121,398.54,135c-8.05,25.15,29.05,56.45,56.14,79.3,22,18.57,36.56,30.84,30.74,39.83-2.22,3.42-7.32,5.5-15.15,6.17C468.48,260.42,466.56,260.49,464.53,260.49Z" fill="#505050"/>
|
||||
<path d="M526.92,129.37h0a9.79,9.79,0,0,1,10-9.55H641a9.79,9.79,0,0,1,10,9.55h0a9.79,9.79,0,0,1-10,9.55H536.87a9.79,9.79,0,0,1-10-9.55Zm10-46.66a9.79,9.79,0,0,1-10-9.55h0a9.79,9.79,0,0,1,10-9.55H641a9.79,9.79,0,0,1,10,9.55h0a9.79,9.79,0,0,1-10,9.55H536.87" fill="#919296" fill-rule="evenodd"/>
|
||||
<rect x="444.49" y="57.18" width="68.71" height="88.17" fill="#919296"/>
|
||||
<rect x="473.95" y="41.51" width="118.01" height="117.98" fill="#c1c2c5"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
12
services/web/public/img/brand/500-visual-socket.svg
Normal file
|
@ -0,0 +1,12 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="777.28" height="777.28" viewBox="0 0 777.28 777.28">
|
||||
<g>
|
||||
<g>
|
||||
<circle cx="388.64" cy="388.64" r="388.64" fill="#798ed2" opacity="0.05"/>
|
||||
<g>
|
||||
<rect x="241.27" y="232.25" width="294.75" height="294.75" fill="#c1c2c5"/>
|
||||
<path d="M388.64,468.11a88.49,88.49,0,1,0-88.49-88.49A88.66,88.66,0,0,0,388.64,468.11Z" fill="#919296" fill-rule="evenodd"/>
|
||||
<path d="M352.86,366.1a13.27,13.27,0,1,1-13.27,13.27,13.27,13.27,0,0,1,13.27-13.27Zm71.9,0a13.27,13.27,0,1,1-13.27,13.27A13.27,13.27,0,0,1,424.75,366.1Z" fill="#505050" fill-rule="evenodd"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 655 B |
13
services/web/public/img/brand/500-visual-tail.svg
Normal file
|
@ -0,0 +1,13 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="229.57" height="704.86" viewBox="0 0 229.57 704.86">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M229.57,135.43c-5.17-2.88-8.32-4.27-12.67-6.7L228.38,126c-9.06-13.8-12.33-25.5-26.85-33.1C198.67,81,175.75,74,170.33,62.23c-1.24,3.38-.91,2.15-1.7,4.65-5.23-3-9.29-8.54-12.69-15.31C144.71,26.7,140.93,1.6,139.6,0c-5.55,8.51-7.84,9.87-12.83,16.28-5.46,6.46-10.92,13.35-14.21,21.85-3.76,9-7.42,21.72-4,26.76-5.78.45-4.86-.61-5.88,1.81-4.28,10.11-11.87,44.3-10.38,55.4h0v0c-3-.71-5.23-1.07-5.23-1.07s2.85,24.23,8.25,38.47c5.8,1.19,9.22,1.57,14.1,2.58l-10.12,6.07c12.78,10.44,21.33,21.79,36.91,27.8,4.46,12.07,17.17,18.79,25.87,28.38a30.45,30.45,0,0,1,5.55-4.88,11.8,11.8,0,0,0,2.91.24,9.68,9.68,0,0,0,6.93,1.51l-2-8.51c.37.63.75,1.3,1.12,1.9,5.86,9.32,17.48,5.37,20,5.4-2-6.09-1.68-5.13-2.37-10.59,6.79,3.22,5.35,1.14,10.58,4.88,3.77-6.16,7.05-11.66,10.58-17.63,4.37-7.4,6.61-18.62,4.79-25.29C224.1,169.37,222.25,148.16,229.57,135.43Z" fill="#a73529"/>
|
||||
<g>
|
||||
<path d="M57.51,507.3c41.3-64.88,93.86-117.43,122-189.41,16.58-42.45,7.19-94.59-16.38-142.51-2.87-.33-6,2.06-4.69,5.77,21.29,58.92,22.26,100.16-6.54,155.55-26.3,50.65-64.92,92.78-96.22,139.58C27.22,518.81,2,566,.68,620.27c-.74,29.92,8.2,58.38,22.32,84.08H40C3.59,642.76,19,567.77,57.51,507.3Z" fill="#a93529"/>
|
||||
<path d="M57.24,507.13l6.36-9.79c2.09-3.28,4.35-6.45,6.52-9.68l3.29-4.82,3.37-4.77c2.25-3.18,4.47-6.37,6.77-9.51q6.82-9.47,13.77-18.85l13.86-18.77q3.47-4.69,6.88-9.42c2.27-3.15,4.59-6.28,6.81-9.47s4.5-6.34,6.71-9.54l6.56-9.64c4.26-6.5,8.53-13,12.5-19.66l3-5,2.87-5.07c1.89-3.39,3.9-6.72,5.62-10.19s3.61-6.87,5.32-10.35l4.93-10.54.62-1.32.56-1.34,1.11-2.69,2.22-5.37,1.11-2.69c.17-.43.4-.93.53-1.31L179,316l1.9-5.52a27.94,27.94,0,0,0,.81-2.77l.73-2.78c.48-1.86,1-3.69,1.33-5.6l1.08-5.68c.33-1.9.49-3.82.75-5.73.58-3.82.63-7.69.9-11.53.08-1.93,0-3.86,0-5.79s0-3.86-.11-5.79l-.32-5.78c-.09-1.93-.38-3.84-.56-5.77l-.31-2.88-.16-1.44-.22-1.43-.91-5.72c-.26-1.92-.7-3.8-1.08-5.69l-1.19-5.68-1.43-5.62c-.47-1.88-.94-3.75-1.53-5.6l-1.66-5.56c-.52-1.86-1.23-3.67-1.83-5.51s-1.24-3.67-1.91-5.48l-2.1-5.41c-1.33-3.63-3-7.15-4.47-10.72s-3.28-7-4.9-10.53l1,.74a3.4,3.4,0,0,0-2.91,1.1,2.9,2.9,0,0,0-.32,3l3,8.63.74,2.16.68,2.19,1.36,4.39c.45,1.46.92,2.92,1.34,4.39L168,207a202.89,202.89,0,0,1,6.65,36.09,147.37,147.37,0,0,1-1.41,36.71c-.6,3-1.12,6-1.78,9l-2.3,8.91-2.81,8.75c-1,2.89-2.17,5.73-3.24,8.59A286.82,286.82,0,0,1,147,348c-6,10.68-12.59,20.94-19.42,31C113.91,399.26,99,418.53,84.38,438c-7.3,9.73-14.56,19.5-21.52,29.46s-13.65,20.13-20,30.5c-12.64,20.73-23.77,42.49-31.35,65.57-.89,2.9-1.82,5.79-2.67,8.7L6.57,581l-1.89,8.9c-.55,3-1,6-1.48,9-.8,6-1.44,12-1.67,18.11-.22,3-.2,6-.21,9.1l.15,4.53c.05,1.51.2,3,.3,4.53a155.1,155.1,0,0,0,2.53,18A169.89,169.89,0,0,0,9,670.75a194.73,194.73,0,0,0,14.49,33.36l-.44-.26,17,0-.41.72-3.19-5.7-2.94-5.83-2.68-6L28.43,681l-2.14-6.17-1.86-6.26-1.58-6.33-1.3-6.4c-.34-2.15-.71-4.29-1-6.45s-.55-4.32-.76-6.48-.38-4.33-.49-6.51-.22-4.35-.23-6.52-.06-4.35,0-6.52.1-4.35.27-6.52q.4-6.51,1.22-13a203.51,203.51,0,0,1,5-25.58,230.07,230.07,0,0,1,8.06-24.79q2.36-6.08,5-12c.92-2,1.79-4,2.75-5.91s1.9-3.91,2.89-5.84,2-3.86,3-5.77l3.17-5.69,3.3-5.62Zm.53.34L54.34,513,51,518.6l-3.16,5.68c-1,1.91-2,3.84-3,5.76s-1.92,3.89-2.88,5.83-1.82,3.93-2.74,5.9q-2.65,5.94-5,12a229.49,229.49,0,0,0-8,24.72,202.83,202.83,0,0,0-5,25.5q-.83,6.45-1.2,12.94c-.16,2.16-.16,4.33-.24,6.49s0,4.33,0,6.5.17,4.33.25,6.49.34,4.32.51,6.48.52,4.3.77,6.45.7,4.27,1,6.41l1.31,6.36,1.59,6.29,1.87,6.22,2.14,6.13,2.41,6,2.68,5.91,2.94,5.79,3.19,5.65.41.72H40l-17,0h-.3l-.14-.26A195.77,195.77,0,0,1,7.87,671.11a170.91,170.91,0,0,1-4.75-17.69A156.45,156.45,0,0,1,.49,635.28c-.1-1.53-.26-3-.32-4.58L0,626.11c0-3,0-6.1.17-9.15.2-6.11.83-12.19,1.61-18.25.49-3,.92-6,1.46-9l1.87-9,2.29-8.87c.84-2.94,1.77-5.84,2.65-8.77,7.54-23.26,18.66-45.19,31.27-66,6.32-10.44,13-20.63,19.93-30.67s14.17-19.83,21.45-29.6c14.58-19.51,29.45-38.79,43-58.94,6.78-10.07,13.35-20.33,19.25-30.9,1.52-2.62,2.91-5.34,4.36-8s2.76-5.37,4.12-8.1l2-4.08c.68-1.36,1.23-2.76,1.86-4.14l1.82-4.16.91-2.08c.29-.7.54-1.41.82-2.12,1.06-2.83,2.19-5.64,3.19-8.5l2.75-8.64,2.25-8.78c.64-2.95,1.15-5.93,1.73-8.89a144.88,144.88,0,0,0,1.31-36.09,200.3,200.3,0,0,0-6.64-35.62l-1.19-4.38c-.42-1.46-.89-2.89-1.34-4.34l-1.35-4.33-.68-2.17-.75-2.16-3-8.63a6.55,6.55,0,0,1-.39-2.81A5.15,5.15,0,0,1,158,176.1a6,6,0,0,1,5.27-2.05l.74.08.31.66,2.5,5.3c.82,1.78,1.7,3.52,2.43,5.33,1.51,3.61,3.14,7.16,4.48,10.83l2.1,5.47c.67,1.84,1.27,3.7,1.91,5.54s1.31,3.68,1.84,5.57l1.67,5.62c.59,1.87,1.06,3.76,1.53,5.66l1.43,5.69,1.19,5.75c.39,1.92.82,3.83,1.08,5.77l.9,5.8.22,1.45.15,1.46L188,253c.18,1.95.47,3.89.55,5.85l.31,5.87c.15,2,.09,3.92.09,5.88s0,3.92-.07,5.88c-.3,3.91-.38,7.84-1,11.72a125.53,125.53,0,0,1-5,23l-1.94,5.5-.48,1.38c-.19.53-.38.92-.57,1.39l-1.14,2.7-2.28,5.39-1.14,2.7-.57,1.35-.63,1.32-5,10.57c-1.74,3.49-3.62,6.91-5.42,10.37S160,360.64,158,364l-2.91,5.07-3.07,5c-4,6.67-8.35,13.15-12.66,19.64l-6.63,9.62c-2.23,3.19-4.52,6.34-6.78,9.51s-4.58,6.29-6.88,9.44-4.61,6.27-6.94,9.38l-14,18.69q-7,9.34-13.89,18.75c-2.32,3.12-4.56,6.3-6.83,9.46l-3.39,4.74-3.32,4.79c-2.19,3.21-4.46,6.36-6.58,9.63Z" fill="#a93529"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 5.1 KiB |
8
services/web/public/img/brand/lion-grey.svg
Normal file
|
@ -0,0 +1,8 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 431" width="500" height="431">
|
||||
<g>
|
||||
<circle cx="291.77" cy="172.48" r="11.72" fill="#acadb2"/>
|
||||
<path d="M307.12,310.59a4.71,4.71,0,0,0-5.89-3.08c-12.93,4.05-29.57,6.22-46.52,6.56v-57c0-.14-0.07-0.25-0.08-0.39,14.13-4.8,43.14-26.44,37.62-37.14-2.48-4.81-21.73-5.28-42.35-5.41-20.43-.13-39.65.8-42.06,5.61-5.81,11.62,22.39,32.84,37.49,37.13,0,0.07,0,.13,0,0.2v57c-17-.35-33.6-2.52-46.52-6.58a4.7,4.7,0,0,0-2.82,9c15.16,4.76,34.61,7.13,54.07,7.13s38.86-2.37,54-7.11A4.71,4.71,0,0,0,307.12,310.59Z" fill="#acadb2"/>
|
||||
<circle cx="208.27" cy="172.48" r="11.72" fill="#acadb2"/>
|
||||
<path d="M249.3,405c2.88,9.34,5.7,19.73,7.43,25.28,37.9-.39,72.72-0.26,109.1,0-0.09-14.67-.93-23.15-1-35.49l19.91,21.41c18.71-36.35,41.83-62.9,48.36-103.75,26.95-17,36.71-51.27,55.46-77.28-9.25-4.64-18.86-5.69-27.55-10,11.42-3.81,27-7,39-11-16.71-26.89-30-52.75-45.65-78.11-7.54-14.6-22.8-18.82-37.65-7.17,0.35-10.22,15.18-29.16,10.76-37.39-12.87-24-26.86-50.76-41-74.1L357,41.24,368.26,0.75C340.85,1.44,312,3.18,287,2.26c-19.65-.73-37.79,6.58-41.27,29.92C243.87,21.81,240.54,12.77,226.88,8,196.14-2.66,138.19.13,123.25,1.39c5.28,13,6.07,27.18,11.07,39.46L107.79,28.42C96.56,49,84,68.43,71.62,87c-8.47,16.32-12,31.15,13.72,45-14.26-.49-25-4.33-34.37-4.65C34.13,157.58,15.5,184.53,0,214.81c16.88-.43,33.29,7.54,43.2,7.29h0c-8,5.5-10.43,16.17-22.73,24.61,22.64,19.05,17.76,60.78,48.78,67.77-0.73,5.22-4.71,14.07-2.2,19.05,14.17,28.1,32.86,51.37,48.65,79.92,9.39-13.15,19.17-17.67,26.48-27.21l-4.61,42.83c17.87,0.61,35,1.42,52.21,1.74C211.09,431.22,242.32,431.25,249.3,405ZM348.58,86.26c4.24-6.39,25.13,3.51,28.49,9.37,2.82,4.91.82,29.09-6.37,29.42-2.86.13-8.14-8.58-13.63-18S347,88.63,348.58,86.26ZM122.66,95.63c3.36-5.85,24.25-15.75,28.49-9.37,1.57,2.37-3.06,11.43-8.5,20.77S131.88,125.17,129,125C121.84,124.72,119.84,100.54,122.66,95.63Zm20,290h0Zm20.9-86.71,6.21-10.75-43.25-74.92,61.74-106.94H311.74l61.75,106.94-43.26,74.92,6.21,10.76-43.22,74.86H206.78Z" fill="#acadb2"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2 KiB |
8
services/web/public/img/brand/lion.svg
Normal file
|
@ -0,0 +1,8 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 431" width="500" height="431">
|
||||
<g>
|
||||
<circle cx="291.77" cy="172.48" r="11.72" fill="#a93529"/>
|
||||
<path d="M307.12,310.59a4.71,4.71,0,0,0-5.89-3.08c-12.93,4.05-29.57,6.22-46.52,6.56v-57c0-.14-0.07-0.25-0.08-0.39,14.13-4.8,43.14-26.44,37.62-37.14-2.48-4.81-21.73-5.28-42.35-5.41-20.43-.13-39.65.8-42.06,5.61-5.81,11.62,22.39,32.84,37.49,37.13,0,0.07,0,.13,0,0.2v57c-17-.35-33.6-2.52-46.52-6.58a4.7,4.7,0,0,0-2.82,9c15.16,4.76,34.61,7.13,54.07,7.13s38.86-2.37,54-7.11A4.71,4.71,0,0,0,307.12,310.59Z" fill="#a93529"/>
|
||||
<circle cx="208.27" cy="172.48" r="11.72" fill="#a93529"/>
|
||||
<path d="M249.3,405c2.88,9.34,5.7,19.73,7.43,25.28,37.9-.39,72.72-0.26,109.1,0-0.09-14.67-.93-23.15-1-35.49l19.91,21.41c18.71-36.35,41.83-62.9,48.36-103.75,26.95-17,36.71-51.27,55.46-77.28-9.25-4.64-18.86-5.69-27.55-10,11.42-3.81,27-7,39-11-16.71-26.89-30-52.75-45.65-78.11-7.54-14.6-22.8-18.82-37.65-7.17,0.35-10.22,15.18-29.16,10.76-37.39-12.87-24-26.86-50.76-41-74.1L357,41.24,368.26,0.75C340.85,1.44,312,3.18,287,2.26c-19.65-.73-37.79,6.58-41.27,29.92C243.87,21.81,240.54,12.77,226.88,8,196.14-2.66,138.19.13,123.25,1.39c5.28,13,6.07,27.18,11.07,39.46L107.79,28.42C96.56,49,84,68.43,71.62,87c-8.47,16.32-12,31.15,13.72,45-14.26-.49-25-4.33-34.37-4.65C34.13,157.58,15.5,184.53,0,214.81c16.88-.43,33.29,7.54,43.2,7.29h0c-8,5.5-10.43,16.17-22.73,24.61,22.64,19.05,17.76,60.78,48.78,67.77-0.73,5.22-4.71,14.07-2.2,19.05,14.17,28.1,32.86,51.37,48.65,79.92,9.39-13.15,19.17-17.67,26.48-27.21l-4.61,42.83c17.87,0.61,35,1.42,52.21,1.74C211.09,431.22,242.32,431.25,249.3,405ZM348.58,86.26c4.24-6.39,25.13,3.51,28.49,9.37,2.82,4.91.82,29.09-6.37,29.42-2.86.13-8.14-8.58-13.63-18S347,88.63,348.58,86.26ZM122.66,95.63c3.36-5.85,24.25-15.75,28.49-9.37,1.57,2.37-3.06,11.43-8.5,20.77S131.88,125.17,129,125C121.84,124.72,119.84,100.54,122.66,95.63Zm20,290h0Zm20.9-86.71,6.21-10.75-43.25-74.92,61.74-106.94H311.74l61.75,106.94-43.26,74.92,6.21,10.76-43.22,74.86H206.78Z" fill="#a93529"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2 KiB |
1
services/web/public/img/brand/logo-horizontal.svg
Normal file
After Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 593 B |
Before Width: | Height: | Size: 1.4 KiB |
BIN
services/web/public/img/feature-page/courtney-gibbons.jpg
Normal file
After Width: | Height: | Size: 57 KiB |
BIN
services/web/public/img/feature-page/feat-accept-poster.jpg
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
services/web/public/img/feature-page/feat-accept.mp4
Normal file
BIN
services/web/public/img/feature-page/feat-changes-poster.jpg
Normal file
After Width: | Height: | Size: 81 KiB |
BIN
services/web/public/img/feature-page/feat-changes.mp4
Normal file
BIN
services/web/public/img/feature-page/feat-discuss-poster.jpg
Normal file
After Width: | Height: | Size: 105 KiB |
BIN
services/web/public/img/feature-page/feat-discuss.mp4
Normal file
BIN
services/web/public/img/feature-page/feat-todos-poster.jpg
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
services/web/public/img/feature-page/feat-todos.mp4
Normal file
BIN
services/web/public/img/feature-page/intro-poster.jpg
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
services/web/public/img/feature-page/intro.mp4
Normal file
BIN
services/web/public/img/feature-page/joanna-ellis-monaghan.jpg
Normal file
After Width: | Height: | Size: 26 KiB |