Merge branch 'master' into node-6.9

This commit is contained in:
Shane Kilkelly 2017-04-05 10:15:51 +01:00
commit bb65da88fe
339 changed files with 61714 additions and 157737 deletions

1
services/web/.nvmrc Normal file
View file

@ -0,0 +1 @@
6.9.5

View file

@ -1,7 +1,15 @@
AnalyticsManager = require "./AnalyticsManager" AnalyticsManager = require "./AnalyticsManager"
Errors = require "../Errors/Errors"
AuthenticationController = require("../Authentication/AuthenticationController")
module.exports = AnalyticsController = module.exports = AnalyticsController =
recordEvent: (req, res, next) -> recordEvent: (req, res, next) ->
AnalyticsManager.recordEvent req.session?.user?._id, req.params.event, req.body, (error) -> user_id = AuthenticationController.getLoggedInUserId(req) or req.sessionID
return next(error) if error? AnalyticsManager.recordEvent user_id, req.params.event, req.body, (error) ->
res.send 204 if error instanceof Errors.ServiceNotConfiguredError
# ignore, no-op
return res.send(204)
else if error?
return next(error)
else
return res.send 204

View file

@ -2,6 +2,7 @@ settings = require "settings-sharelatex"
logger = require "logger-sharelatex" logger = require "logger-sharelatex"
_ = require "underscore" _ = require "underscore"
request = require "request" request = require "request"
Errors = require '../Errors/Errors'
makeRequest = (opts, callback)-> makeRequest = (opts, callback)->
@ -10,12 +11,20 @@ makeRequest = (opts, callback)->
opts.url = "#{settings.apis.analytics.url}#{urlPath}" opts.url = "#{settings.apis.analytics.url}#{urlPath}"
request opts, callback request opts, callback
else else
callback() callback(new Errors.ServiceNotConfiguredError('Analytics service not configured'))
module.exports = 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) ->) -> recordEvent: (user_id, event, segmentation = {}, callback = (error) ->) ->
if user_id+"" == settings.smokeTest?.userId+"" if user_id+"" == settings.smokeTest?.userId+""

View file

@ -2,7 +2,7 @@ AuthenticationManager = require ("./AuthenticationManager")
LoginRateLimiter = require("../Security/LoginRateLimiter") LoginRateLimiter = require("../Security/LoginRateLimiter")
UserGetter = require "../User/UserGetter" UserGetter = require "../User/UserGetter"
UserUpdater = require "../User/UserUpdater" UserUpdater = require "../User/UserUpdater"
Metrics = require('../../infrastructure/Metrics') Metrics = require('metrics-sharelatex')
logger = require("logger-sharelatex") logger = require("logger-sharelatex")
querystring = require('querystring') querystring = require('querystring')
Url = require("url") Url = require("url")
@ -87,6 +87,7 @@ module.exports = AuthenticationController =
LoginRateLimiter.recordSuccessfulLogin(email) LoginRateLimiter.recordSuccessfulLogin(email)
AuthenticationController._recordSuccessfulLogin(user._id) AuthenticationController._recordSuccessfulLogin(user._id)
Analytics.recordEvent(user._id, "user-logged-in", {ip:req.ip}) 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" logger.log email: email, user_id: user._id.toString(), "successful log in"
req.session.justLoggedIn = true req.session.justLoggedIn = true
# capture the request ip for use when creating the session # capture the request ip for use when creating the session

View file

@ -4,6 +4,8 @@ User = require("../../models/User").User
PrivilegeLevels = require("./PrivilegeLevels") PrivilegeLevels = require("./PrivilegeLevels")
PublicAccessLevels = require("./PublicAccessLevels") PublicAccessLevels = require("./PublicAccessLevels")
Errors = require("../Errors/Errors") Errors = require("../Errors/Errors")
ObjectId = require("mongojs").ObjectId
module.exports = AuthorizationManager = module.exports = AuthorizationManager =
# Get the privilege level that the user has for the project # 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. # * becausePublic: true if the access level is only because the project is public.
getPrivilegeLevelForProject: (user_id, project_id, callback = (error, privilegeLevel, becausePublic) ->) -> getPrivilegeLevelForProject: (user_id, project_id, callback = (error, privilegeLevel, becausePublic) ->) ->
getPublicAccessLevel = () -> getPublicAccessLevel = () ->
if !ObjectId.isValid(project_id)
return callback(new Error("invalid project id"))
Project.findOne { _id: project_id }, { publicAccesLevel: 1 }, (error, project) -> Project.findOne { _id: project_id }, { publicAccesLevel: 1 }, (error, project) ->
return callback(error) if error? return callback(error) if error?
if !project? if !project?

View file

@ -1,6 +1,6 @@
User = require("../../models/User").User User = require("../../models/User").User
logger = require 'logger-sharelatex' logger = require 'logger-sharelatex'
metrics = require("../../infrastructure/Metrics") metrics = require("metrics-sharelatex")
module.exports = BetaProgramHandler = module.exports = BetaProgramHandler =

View file

@ -10,7 +10,7 @@ module.exports = BlogController =
url = req.url?.toLowerCase() url = req.url?.toLowerCase()
blogUrl = "#{settings.apis.blog.url}#{url}" 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)-> shouldProxy = _.find extensionsToProxy, (extension)->
url.indexOf(extension) != -1 url.indexOf(extension) != -1

View file

@ -10,7 +10,7 @@ module.exports = BlogHandler =
opts = opts =
url:blogUrl url:blogUrl
json:true json:true
timeout:500 timeout:1000
request.get opts, (err, res, announcements)-> request.get opts, (err, res, announcements)->
if err? if err?
return callback err return callback err
@ -18,7 +18,6 @@ module.exports = BlogHandler =
return callback("blog announcement returned non 200") return callback("blog announcement returned non 200")
logger.log announcementsLength: announcements?.length, "announcements returned" logger.log announcementsLength: announcements?.length, "announcements returned"
announcements = _.map announcements, (announcement)-> announcements = _.map announcements, (announcement)->
announcement.url = "/blog#{announcement.url}"
announcement.date = new Date(announcement.date) announcement.date = new Date(announcement.date)
return announcement return announcement
callback(err, announcements) callback(err, announcements)

View file

@ -4,9 +4,9 @@ logger = require("logger-sharelatex")
AuthenticationController = require('../Authentication/AuthenticationController') AuthenticationController = require('../Authentication/AuthenticationController')
UserInfoManager = require('../User/UserInfoManager') UserInfoManager = require('../User/UserInfoManager')
UserInfoController = require('../User/UserInfoController') UserInfoController = require('../User/UserInfoController')
CommentsController = require('../Comments/CommentsController') async = require "async"
module.exports = module.exports = ChatController =
sendMessage: (req, res, next)-> sendMessage: (req, res, next)->
project_id = req.params.project_id project_id = req.params.project_id
content = req.body.content content = req.body.content
@ -28,7 +28,38 @@ module.exports =
logger.log project_id:project_id, query:query, "getting messages" logger.log project_id:project_id, query:query, "getting messages"
ChatApiHandler.getGlobalMessages project_id, query.limit, query.before, (err, messages) -> ChatApiHandler.getGlobalMessages project_id, query.limit, query.before, (err, messages) ->
return next(err) if err? return next(err) if err?
CommentsController._injectUserInfoIntoThreads {global: { messages: messages }}, (err) -> ChatController._injectUserInfoIntoThreads {global: { messages: messages }}, (err) ->
return next(err) if err? return next(err) if err?
logger.log length: messages?.length, "sending messages to client" logger.log length: messages?.length, "sending messages to client"
res.json messages 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

View file

@ -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

View file

@ -1,4 +1,4 @@
Metrics = require "../../infrastructure/Metrics" Metrics = require "metrics-sharelatex"
Project = require("../../models/Project").Project Project = require("../../models/Project").Project
CompileManager = require("./CompileManager") CompileManager = require("./CompileManager")
ClsiManager = require("./ClsiManager") ClsiManager = require("./ClsiManager")

View file

@ -6,7 +6,7 @@ Project = require("../../models/Project").Project
ProjectRootDocManager = require "../Project/ProjectRootDocManager" ProjectRootDocManager = require "../Project/ProjectRootDocManager"
UserGetter = require "../User/UserGetter" UserGetter = require "../User/UserGetter"
ClsiManager = require "./ClsiManager" ClsiManager = require "./ClsiManager"
Metrics = require('../../infrastructure/Metrics') Metrics = require('metrics-sharelatex')
logger = require("logger-sharelatex") logger = require("logger-sharelatex")
rateLimiter = require("../../infrastructure/RateLimiter") rateLimiter = require("../../infrastructure/RateLimiter")

View file

@ -4,7 +4,7 @@ settings = require 'settings-sharelatex'
_ = require 'underscore' _ = require 'underscore'
async = require 'async' async = require 'async'
logger = require('logger-sharelatex') logger = require('logger-sharelatex')
metrics = require('../../infrastructure/Metrics') metrics = require('metrics-sharelatex')
redis = require("redis-sharelatex") redis = require("redis-sharelatex")
rclient = redis.createClient(settings.redis.web) rclient = redis.createClient(settings.redis.web)
Project = require("../../models/Project").Project Project = require("../../models/Project").Project

View file

@ -1,5 +1,5 @@
logger = require "logger-sharelatex" logger = require "logger-sharelatex"
Metrics = require "../../infrastructure/Metrics" Metrics = require "metrics-sharelatex"
Project = require("../../models/Project").Project Project = require("../../models/Project").Project
ProjectZipStreamManager = require "./ProjectZipStreamManager" ProjectZipStreamManager = require "./ProjectZipStreamManager"
DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler" DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"

View file

@ -1,5 +1,5 @@
logger = require('logger-sharelatex') logger = require('logger-sharelatex')
Metrics = require('../../infrastructure/Metrics') Metrics = require('metrics-sharelatex')
sanitize = require('sanitizer') sanitize = require('sanitizer')
ProjectEntityHandler = require('../Project/ProjectEntityHandler') ProjectEntityHandler = require('../Project/ProjectEntityHandler')
ProjectOptionsHandler = require('../Project/ProjectOptionsHandler') ProjectOptionsHandler = require('../Project/ProjectOptionsHandler')

View file

@ -7,7 +7,7 @@ ProjectGetter = require('../Project/ProjectGetter')
UserGetter = require('../User/UserGetter') UserGetter = require('../User/UserGetter')
AuthorizationManager = require("../Authorization/AuthorizationManager") AuthorizationManager = require("../Authorization/AuthorizationManager")
ProjectEditorHandler = require('../Project/ProjectEditorHandler') ProjectEditorHandler = require('../Project/ProjectEditorHandler')
Metrics = require('../../infrastructure/Metrics') Metrics = require('metrics-sharelatex')
CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler")
CollaboratorsInviteHandler = require("../Collaborators/CollaboratorsInviteHandler") CollaboratorsInviteHandler = require("../Collaborators/CollaboratorsInviteHandler")
PrivilegeLevels = require "../Authorization/PrivilegeLevels" PrivilegeLevels = require "../Authorization/PrivilegeLevels"
@ -96,8 +96,10 @@ module.exports = EditorHttpController =
EditorController.addFolder project_id, parent_folder_id, name, "editor", (error, doc) -> EditorController.addFolder project_id, parent_folder_id, name, "editor", (error, doc) ->
if error == "project_has_to_many_files" if error == "project_has_to_many_files"
res.status(400).json(req.i18n.translate("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? else if error?
next(error) res.status(500).json(req.i18n.translate('generic_something_went_wrong'))
else else
res.json doc res.json doc

View file

@ -1,5 +1,5 @@
logger = require('logger-sharelatex') logger = require('logger-sharelatex')
metrics = require('../../infrastructure/Metrics') metrics = require('metrics-sharelatex')
Settings = require('settings-sharelatex') Settings = require('settings-sharelatex')
nodemailer = require("nodemailer") nodemailer = require("nodemailer")
sesTransport = require('nodemailer-ses-transport') sesTransport = require('nodemailer-ses-transport')
@ -72,6 +72,7 @@ module.exports =
client.sendMail options, (err, res)-> client.sendMail options, (err, res)->
if err? if err?
logger.err err:err, "error sending message" logger.err err:err, "error sending message"
err = new Error('Cannot send email')
else else
logger.log "Message sent to #{options.to}" logger.log "Message sent to #{options.to}"
callback(err) callback(err)

View file

@ -5,5 +5,14 @@ NotFoundError = (message) ->
return error return error
NotFoundError.prototype.__proto__ = Error.prototype NotFoundError.prototype.__proto__ = Error.prototype
ServiceNotConfiguredError = (message) ->
error = new Error(message)
error.name = "ServiceNotConfiguredError"
error.__proto__ = ServiceNotConfiguredError.prototype
return error
module.exports = Errors = module.exports = Errors =
NotFoundError: NotFoundError NotFoundError: NotFoundError
ServiceNotConfiguredError: ServiceNotConfiguredError

View file

@ -13,6 +13,9 @@ module.exports = FileStoreHandler =
if err? if err?
logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "error stating file" logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "error stating file"
callback(err) 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() if !stat.isFile()
logger.log project_id:project_id, file_id:file_id, fsPath:fsPath, "tried to upload symlink, not contining" 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")) return callback(new Error("can not upload symlink"))
@ -25,10 +28,19 @@ module.exports = FileStoreHandler =
timeout:fiveMinsInMs timeout:fiveMinsInMs
writeStream = request(opts) writeStream = request(opts)
readStream.pipe writeStream 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)-> 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" 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 callback err
writeStream.on "error", (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" 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 callback err

View file

@ -4,11 +4,9 @@ logger = require("logger-sharelatex")
module.exports = module.exports =
getProjectDetails : (req, res)-> getProjectDetails : (req, res, next)->
{project_id} = req.params {project_id} = req.params
ProjectDetailsHandler.getDetails project_id, (err, projDetails)-> ProjectDetailsHandler.getDetails project_id, (err, projDetails)->
if err? return next(err) if err?
logger.log err:err, project_id:project_id, "something went wrong getting project details"
return res.sendStatus 500
res.json(projDetails) res.json(projDetails)

View file

@ -4,7 +4,7 @@ projectDeleter = require("./ProjectDeleter")
projectDuplicator = require("./ProjectDuplicator") projectDuplicator = require("./ProjectDuplicator")
projectCreationHandler = require("./ProjectCreationHandler") projectCreationHandler = require("./ProjectCreationHandler")
editorController = require("../Editor/EditorController") editorController = require("../Editor/EditorController")
metrics = require('../../infrastructure/Metrics') metrics = require('metrics-sharelatex')
User = require('../../models/User').User User = require('../../models/User').User
TagsHandler = require("../Tags/TagsHandler") TagsHandler = require("../Tags/TagsHandler")
SubscriptionLocator = require("../Subscription/SubscriptionLocator") SubscriptionLocator = require("../Subscription/SubscriptionLocator")
@ -224,6 +224,11 @@ module.exports = ProjectController =
cb = underscore.once(cb) cb = underscore.once(cb)
if !user_id? if !user_id?
return cb() 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 timeout = setTimeout cb, 500
AnalyticsManager.getLastOccurance user_id, "shown-track-changes-onboarding-2", (error, event) -> AnalyticsManager.getLastOccurance user_id, "shown-track-changes-onboarding-2", (error, event) ->
clearTimeout timeout clearTimeout timeout

View file

@ -1,6 +1,6 @@
logger = require('logger-sharelatex') logger = require('logger-sharelatex')
async = require("async") async = require("async")
metrics = require('../../infrastructure/Metrics') metrics = require('metrics-sharelatex')
Settings = require('settings-sharelatex') Settings = require('settings-sharelatex')
ObjectId = require('mongoose').Types.ObjectId ObjectId = require('mongoose').Types.ObjectId
Project = require('../../models/Project').Project Project = require('../../models/Project').Project
@ -11,7 +11,8 @@ fs = require('fs')
Path = require "path" Path = require "path"
_ = require "underscore" _ = require "underscore"
module.exports = module.exports = ProjectCreationHandler =
createBlankProject : (owner_id, projectName, callback = (error, project) ->)-> createBlankProject : (owner_id, projectName, callback = (error, project) ->)->
metrics.inc("project-creation") metrics.inc("project-creation")
logger.log owner_id:owner_id, projectName:projectName, "creating blank project" logger.log owner_id:owner_id, projectName:projectName, "creating blank project"
@ -79,5 +80,10 @@ module.exports =
output = _.template(template.toString(), data) output = _.template(template.toString(), data)
callback null, output.split("\n") callback null, output.split("\n")
metrics.timeAsyncMethod(
ProjectCreationHandler, 'createBlankProject',
'mongo.ProjectCreationHandler',
logger
)

View file

@ -5,6 +5,7 @@ logger = require("logger-sharelatex")
tpdsUpdateSender = require '../ThirdPartyDataStore/TpdsUpdateSender' tpdsUpdateSender = require '../ThirdPartyDataStore/TpdsUpdateSender'
_ = require("underscore") _ = require("underscore")
PublicAccessLevels = require("../Authorization/PublicAccessLevels") PublicAccessLevels = require("../Authorization/PublicAccessLevels")
Errors = require("../Errors/Errors")
module.exports = module.exports =
@ -13,6 +14,7 @@ module.exports =
if err? if err?
logger.err err:err, project_id:project_id, "error getting project" logger.err err:err, project_id:project_id, "error getting project"
return callback(err) return callback(err)
return callback(new Errors.NotFoundError("project not found")) if !project?
UserGetter.getUser project.owner_ref, (err, user) -> UserGetter.getUser project.owner_ref, (err, user) ->
return callback(err) if err? return callback(err) if err?
details = details =

View file

@ -15,9 +15,11 @@ module.exports = ProjectDuplicator =
_copyDocs: (newProject, originalRootDoc, originalFolder, desFolder, docContents, callback)-> _copyDocs: (newProject, originalRootDoc, originalFolder, desFolder, docContents, callback)->
setRootDoc = _.once (doc_id)-> setRootDoc = _.once (doc_id)->
projectEntityHandler.setRootDoc newProject._id, doc_id projectEntityHandler.setRootDoc newProject._id, doc_id
docs = originalFolder.docs or []
jobs = originalFolder.docs.map (doc)-> jobs = docs.map (doc)->
return (cb)-> return (cb)->
if !doc?._id?
return callback()
content = docContents[doc._id.toString()] content = docContents[doc._id.toString()]
projectEntityHandler.addDocWithProject newProject, desFolder._id, doc.name, content.lines, (err, newDoc)-> projectEntityHandler.addDocWithProject newProject, desFolder._id, doc.name, content.lines, (err, newDoc)->
if err? if err?
@ -30,7 +32,8 @@ module.exports = ProjectDuplicator =
async.series jobs, callback async.series jobs, callback
_copyFiles: (newProject, originalProject_id, originalFolder, desFolder, callback)-> _copyFiles: (newProject, originalProject_id, originalFolder, desFolder, callback)->
jobs = originalFolder.fileRefs.map (file)-> fileRefs = originalFolder.fileRefs or []
jobs = fileRefs.map (file)->
return (cb)-> return (cb)->
projectEntityHandler.copyFileFromExistingProjectWithProject newProject, desFolder._id, originalProject_id, file, cb projectEntityHandler.copyFileFromExistingProjectWithProject newProject, desFolder._id, originalProject_id, file, cb
async.parallelLimit jobs, 5, callback async.parallelLimit jobs, 5, callback
@ -40,10 +43,14 @@ module.exports = ProjectDuplicator =
ProjectGetter.getProject newProject_id, {rootFolder:true, name:true}, (err, newProject)-> ProjectGetter.getProject newProject_id, {rootFolder:true, name:true}, (err, newProject)->
if err? if err?
logger.err project_id:newProject_id, "could not get project" 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)-> return (cb)->
if !childFolder?._id?
return cb()
projectEntityHandler.addFolderWithProject newProject, desFolder?._id, childFolder.name, (err, newFolder)-> projectEntityHandler.addFolderWithProject newProject, desFolder?._id, childFolder.name, (err, newFolder)->
return cb(err) if err? return cb(err) if err?
ProjectDuplicator._copyFolderRecursivly newProject_id, originalProject_id, originalRootDoc, childFolder, newFolder, docContents, cb ProjectDuplicator._copyFolderRecursivly newProject_id, originalProject_id, originalRootDoc, childFolder, newFolder, docContents, cb

View file

@ -1,6 +1,8 @@
_ = require("underscore") _ = require("underscore")
module.exports = ProjectEditorHandler = module.exports = ProjectEditorHandler =
trackChangesAvailable: false
buildProjectModelView: (project, members, invites) -> buildProjectModelView: (project, members, invites) ->
result = result =
_id : project._id _id : project._id
@ -20,11 +22,6 @@ module.exports = ProjectEditorHandler =
if !result.invites? if !result.invites?
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) {owner, ownerFeatures, members} = @buildOwnerAndMembersViews(members)
result.owner = owner result.owner = owner
result.members = members result.members = members
@ -38,7 +35,7 @@ module.exports = ProjectEditorHandler =
templates: false templates: false
references: false references: false
trackChanges: false trackChanges: false
trackChangesVisible: trackChangesVisible trackChangesVisible: ProjectEditorHandler.trackChangesAvailable
}) })
return result return result

View file

@ -1,4 +1,5 @@
mongojs = require("../../infrastructure/mongojs") mongojs = require("../../infrastructure/mongojs")
metrics = require("metrics-sharelatex")
db = mongojs.db db = mongojs.db
ObjectId = mongojs.ObjectId ObjectId = mongojs.ObjectId
async = require "async" async = require "async"
@ -57,3 +58,10 @@ module.exports = ProjectGetter =
CollaboratorsHandler.getProjectsUserIsCollaboratorOf user_id, fields, (error, readAndWriteProjects, readOnlyProjects) -> CollaboratorsHandler.getProjectsUserIsCollaboratorOf user_id, fields, (error, readAndWriteProjects, readOnlyProjects) ->
return callback(error) if error? return callback(error) if error?
callback null, projects, readAndWriteProjects, readOnlyProjects callback null, projects, readAndWriteProjects, readOnlyProjects
[
'getProject',
'getProjectWithoutDocLines'
].map (method) ->
metrics.timeAsyncMethod(ProjectGetter, method, 'mongo.ProjectGetter', logger)

View file

@ -26,6 +26,8 @@ module.exports = ProjectLocator =
element = _.find searchFolder[elementType], (el)-> el?._id+'' == element_id+'' #need to ToString both id's for robustness 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 if !element? && searchFolder.folders? && searchFolder.folders.length != 0
_.each searchFolder.folders, (folder, index)-> _.each searchFolder.folders, (folder, index)->
if !folder?
return
newPath = {} newPath = {}
newPath[key] = value for own key,value of path #make a value copy of the string newPath[key] = value for own key,value of path #make a value copy of the string
newPath.fileSystem += "/#{folder.name}" newPath.fileSystem += "/#{folder.name}"

View file

@ -9,12 +9,15 @@ module.exports =
webRouter.get '/tos', HomeController.externalPage("tos", "Terms of Service") webRouter.get '/tos', HomeController.externalPage("tos", "Terms of Service")
webRouter.get '/about', HomeController.externalPage("about", "About Us") webRouter.get '/about', HomeController.externalPage("about", "About Us")
webRouter.get '/security', HomeController.externalPage("security", "Security") webRouter.get '/security', HomeController.externalPage("security", "Security")
webRouter.get '/privacy_policy', HomeController.externalPage("privacy", "Privacy Policy") webRouter.get '/privacy_policy', HomeController.externalPage("privacy", "Privacy Policy")
webRouter.get '/planned_maintenance', HomeController.externalPage("planned_maintenance", "Planned Maintenance") webRouter.get '/planned_maintenance', HomeController.externalPage("planned_maintenance", "Planned Maintenance")
webRouter.get '/style', HomeController.externalPage("style_guide", "Style Guide") webRouter.get '/style', HomeController.externalPage("style_guide", "Style Guide")
webRouter.get '/jobs', HomeController.externalPage("jobs", "Jobs") 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 '/dropbox', HomeController.externalPage("dropbox", "Dropbox and ShareLaTeX")
webRouter.get '/university', UniversityController.getIndexPage webRouter.get '/university', UniversityController.getIndexPage

View file

@ -470,32 +470,38 @@ module.exports = RecurlyWrapper =
callback(error) callback(error)
) )
_parseSubscriptionXml: (xml, callback) -> listAccountActiveSubscriptions: (account_id, callback = (error, subscriptions) ->) ->
RecurlyWrapper._parseXml xml, (error, data) -> RecurlyWrapper.apiRequest {
url: "accounts/#{account_id}/subscriptions"
qs:
state: "active"
expect404: true
}, (error, response, body) ->
return callback(error) if error? return callback(error) if error?
if data? and data.subscription? if response.statusCode == 404
recurlySubscription = data.subscription return callback null, []
else else
return callback "I don't understand the response from Recurly" RecurlyWrapper._parseSubscriptionsXml body, callback
callback null, recurlySubscription
_parseSubscriptionsXml: (xml, callback) ->
RecurlyWrapper._parseXmlAndGetAttribute xml, "subscriptions", callback
_parseSubscriptionXml: (xml, callback) ->
RecurlyWrapper._parseXmlAndGetAttribute xml, "subscription", callback
_parseAccountXml: (xml, callback) -> _parseAccountXml: (xml, callback) ->
RecurlyWrapper._parseXml xml, (error, data) -> RecurlyWrapper._parseXmlAndGetAttribute xml, "account", callback
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
_parseBillingInfoXml: (xml, callback) -> _parseBillingInfoXml: (xml, callback) ->
RecurlyWrapper._parseXmlAndGetAttribute xml, "billing_info", callback
_parseXmlAndGetAttribute: (xml, attribute, callback) ->
RecurlyWrapper._parseXml xml, (error, data) -> RecurlyWrapper._parseXml xml, (error, data) ->
return callback(error) if error? return callback(error) if error?
if data? and data.billing_info? if data? and data[attribute]?
billingInfo = data.billing_info return callback null, data[attribute]
else else
return callback "I don't understand the response from Recurly" return callback(new Error("I don't understand the response from Recurly"))
callback null, billingInfo
_parseXml: (xml, callback) -> _parseXml: (xml, callback) ->
convertDataTypes = (data) -> convertDataTypes = (data) ->

View file

@ -45,6 +45,14 @@ module.exports = SubscriptionController =
return next(err) if err? return next(err) if err?
if hasSubscription or !plan? if hasSubscription or !plan?
res.redirect "/user/subscription" res.redirect "/user/subscription"
else
# 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 else
currency = req.query.currency?.toUpperCase() currency = req.query.currency?.toUpperCase()
GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency, countryCode)-> GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency, countryCode)->

View file

@ -11,10 +11,23 @@ Analytics = require("../Analytics/AnalyticsManager")
module.exports = 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)-> createSubscription: (user, subscriptionDetails, recurly_token_id, callback)->
self = @ self = @
clientTokenId = "" clientTokenId = ""
@validateNoSubscriptionInRecurly user._id, (error, valid) ->
return callback(error) if error?
if !valid
return callback(new Error("user already has subscription in recurly"))
RecurlyWrapper.createSubscription user, subscriptionDetails, recurly_token_id, (error, recurlySubscription)-> RecurlyWrapper.createSubscription user, subscriptionDetails, recurly_token_id, (error, recurlySubscription)->
return callback(error) if error? return callback(error) if error?
SubscriptionUpdater.syncSubscription recurlySubscription, user._id, (error) -> SubscriptionUpdater.syncSubscription recurlySubscription, user._id, (error) ->

View file

@ -23,7 +23,7 @@ module.exports = SystemMessageManager =
clearCache: () -> clearCache: () ->
delete @_cachedMessages delete @_cachedMessages
CACHE_TIMEOUT = 5 * 60 * 1000 # 5 minutes CACHE_TIMEOUT = 20 * 1000 # 20 seconds
setInterval () -> setInterval () ->
SystemMessageManager.clearCache() SystemMessageManager.clearCache()
, CACHE_TIMEOUT , CACHE_TIMEOUT

View file

@ -2,7 +2,7 @@ tpdsUpdateHandler = require('./TpdsUpdateHandler')
UpdateMerger = require "./UpdateMerger" UpdateMerger = require "./UpdateMerger"
logger = require('logger-sharelatex') logger = require('logger-sharelatex')
Path = require('path') Path = require('path')
metrics = require("../../infrastructure/Metrics") metrics = require("metrics-sharelatex")
module.exports = module.exports =
# mergeUpdate and deleteUpdate are used by Dropbox, where the project is only passed as the name, as the # mergeUpdate and deleteUpdate are used by Dropbox, where the project is only passed as the name, as the

View file

@ -3,7 +3,7 @@ logger = require('logger-sharelatex')
path = require('path') path = require('path')
Project = require('../../models/Project').Project Project = require('../../models/Project').Project
keys = require('../../infrastructure/Keys') keys = require('../../infrastructure/Keys')
metrics = require("../../infrastructure/Metrics") metrics = require("metrics-sharelatex")
request = require("request") request = require("request")
CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler')

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1,6 +1,6 @@
child = require "child_process" child = require "child_process"
logger = require "logger-sharelatex" logger = require "logger-sharelatex"
metrics = require "../../infrastructure/Metrics" metrics = require "metrics-sharelatex"
fs = require "fs" fs = require "fs"
Path = require "path" Path = require "path"
_ = require("underscore") _ = require("underscore")

View file

@ -1,5 +1,5 @@
logger = require "logger-sharelatex" logger = require "logger-sharelatex"
metrics = require "../../infrastructure/Metrics" metrics = require "metrics-sharelatex"
fs = require "fs" fs = require "fs"
Path = require "path" Path = require "path"
FileSystemImportManager = require "./FileSystemImportManager" FileSystemImportManager = require "./FileSystemImportManager"

View file

@ -5,7 +5,7 @@ User = require("../../models/User").User
newsLetterManager = require('../Newsletter/NewsletterManager') newsLetterManager = require('../Newsletter/NewsletterManager')
UserRegistrationHandler = require("./UserRegistrationHandler") UserRegistrationHandler = require("./UserRegistrationHandler")
logger = require("logger-sharelatex") logger = require("logger-sharelatex")
metrics = require("../../infrastructure/Metrics") metrics = require("metrics-sharelatex")
Url = require("url") Url = require("url")
AuthenticationManager = require("../Authentication/AuthenticationManager") AuthenticationManager = require("../Authentication/AuthenticationManager")
AuthenticationController = require('../Authentication/AuthenticationController') AuthenticationController = require('../Authentication/AuthenticationController')

View file

@ -1,8 +1,10 @@
User = require("../../models/User").User User = require("../../models/User").User
UserLocator = require("./UserLocator") UserLocator = require("./UserLocator")
logger = require("logger-sharelatex") logger = require("logger-sharelatex")
metrics = require('metrics-sharelatex')
module.exports =
module.exports = UserCreator =
getUserOrCreateHoldingAccount: (email, callback = (err, user)->)-> getUserOrCreateHoldingAccount: (email, callback = (err, user)->)->
self = @ self = @
@ -36,3 +38,9 @@ module.exports =
user.save (err)-> user.save (err)->
callback(err, user) callback(err, user)
metrics.timeAsyncMethod(
UserCreator, 'createNewUser',
'mongo.UserCreator',
logger
)

View file

@ -1,4 +1,6 @@
mongojs = require("../../infrastructure/mongojs") mongojs = require("../../infrastructure/mongojs")
metrics = require('metrics-sharelatex')
logger = require('logger-sharelatex')
db = mongojs.db db = mongojs.db
ObjectId = mongojs.ObjectId ObjectId = mongojs.ObjectId
@ -24,3 +26,10 @@ module.exports = UserGetter =
return callback 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

View file

@ -1,8 +1,10 @@
mongojs = require("../../infrastructure/mongojs") mongojs = require("../../infrastructure/mongojs")
metrics = require("metrics-sharelatex")
db = mongojs.db db = mongojs.db
ObjectId = mongojs.ObjectId ObjectId = mongojs.ObjectId
logger = require('logger-sharelatex')
module.exports = module.exports = UserLocator =
findByEmail: (email, callback)-> findByEmail: (email, callback)->
email = email.trim() email = email.trim()
@ -11,3 +13,9 @@ module.exports =
findById: (_id, callback)-> 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

View file

@ -1,5 +1,6 @@
logger = require("logger-sharelatex") logger = require("logger-sharelatex")
mongojs = require("../../infrastructure/mongojs") mongojs = require("../../infrastructure/mongojs")
metrics = require("metrics-sharelatex")
db = mongojs.db db = mongojs.db
ObjectId = mongojs.ObjectId ObjectId = mongojs.ObjectId
UserLocator = require("./UserLocator") UserLocator = require("./UserLocator")
@ -28,3 +29,5 @@ module.exports = UserUpdater =
return callback(err) return callback(err)
callback() callback()
metrics.timeAsyncMethod UserUpdater, 'updateUser', 'mongo.UserUpdater', logger

View file

@ -1,4 +1,4 @@
metrics = require('./Metrics') metrics = require('metrics-sharelatex')
module.exports = module.exports =
log: (req)-> log: (req)->
if req.headers["user-agent"]? if req.headers["user-agent"]?

View file

@ -1,4 +1,4 @@
metrics = require('./Metrics') metrics = require('metrics-sharelatex')
Settings = require('settings-sharelatex') Settings = require('settings-sharelatex')
redis = require("redis-sharelatex") redis = require("redis-sharelatex")
rclient = redis.createClient(Settings.redis.web) rclient = redis.createClient(Settings.redis.web)

View file

@ -1 +0,0 @@
module.exports = require("metrics-sharelatex")

View file

@ -1,5 +1,5 @@
version = { version = {
"pdfjs": "1.6.210p2" "pdfjs": "1.7.225"
"moment": "2.9.0" "moment": "2.9.0"
"ace": "1.2.5" "ace": "1.2.5"
} }

View file

@ -1,5 +1,5 @@
_ = require('underscore') _ = require('underscore')
metrics = require('./Metrics') metrics = require('metrics-sharelatex')
do trackOpenSockets = -> do trackOpenSockets = ->
metrics.gauge("http.open-sockets", _.size(require('http').globalAgent.sockets.length), 0.5) metrics.gauge("http.open-sockets", _.size(require('http').globalAgent.sockets.length), 0.5)

View file

@ -2,7 +2,7 @@ Path = require "path"
express = require('express') express = require('express')
Settings = require('settings-sharelatex') Settings = require('settings-sharelatex')
logger = require 'logger-sharelatex' logger = require 'logger-sharelatex'
metrics = require('./Metrics') metrics = require('metrics-sharelatex')
crawlerLogger = require('./CrawlerLogger') crawlerLogger = require('./CrawlerLogger')
expressLocals = require('./ExpressLocals') expressLocals = require('./ExpressLocals')
Router = require('../router') Router = require('../router')
@ -39,8 +39,6 @@ ErrorController = require "../Features/Errors/ErrorController"
UserSessionsManager = require "../Features/User/UserSessionsManager" UserSessionsManager = require "../Features/User/UserSessionsManager"
AuthenticationController = require "../Features/Authentication/AuthenticationController" 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) metrics.event_loop?.monitor(logger)

View file

@ -39,9 +39,6 @@ UserSchema = new Schema
references: { type:Boolean, default: Settings.defaultFeatures.references } references: { type:Boolean, default: Settings.defaultFeatures.references }
trackChanges: { type:Boolean, default: Settings.defaultFeatures.trackChanges } trackChanges: { type:Boolean, default: Settings.defaultFeatures.trackChanges }
} }
featureSwitches : {
track_changes: { type: Boolean }
}
referal_id : {type:String, default:() -> uuid.v4().split("-")[0]} referal_id : {type:String, default:() -> uuid.v4().split("-")[0]}
refered_users: [ type:ObjectId, ref:'User' ] refered_users: [ type:ObjectId, ref:'User' ]
refered_user_count: { type:Number, default: 0 } refered_user_count: { type:Number, default: 0 }

View file

@ -9,7 +9,7 @@ Settings = require('settings-sharelatex')
TpdsController = require('./Features/ThirdPartyDataStore/TpdsController') TpdsController = require('./Features/ThirdPartyDataStore/TpdsController')
SubscriptionRouter = require './Features/Subscription/SubscriptionRouter' SubscriptionRouter = require './Features/Subscription/SubscriptionRouter'
UploadsRouter = require './Features/Uploads/UploadsRouter' UploadsRouter = require './Features/Uploads/UploadsRouter'
metrics = require('./infrastructure/Metrics') metrics = require('metrics-sharelatex')
ReferalController = require('./Features/Referal/ReferalController') ReferalController = require('./Features/Referal/ReferalController')
AuthenticationController = require('./Features/Authentication/AuthenticationController') AuthenticationController = require('./Features/Authentication/AuthenticationController')
TagsController = require("./Features/Tags/TagsController") TagsController = require("./Features/Tags/TagsController")
@ -40,8 +40,6 @@ AuthorizationMiddlewear = require('./Features/Authorization/AuthorizationMiddlew
BetaProgramController = require('./Features/BetaProgram/BetaProgramController') BetaProgramController = require('./Features/BetaProgram/BetaProgramController')
AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter') AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter')
AnnouncementsController = require("./Features/Announcements/AnnouncementsController") AnnouncementsController = require("./Features/Announcements/AnnouncementsController")
TrackChangesController = require("./Features/TrackChanges/TrackChangesController")
CommentsController = require "./Features/Comments/CommentsController"
logger = require("logger-sharelatex") logger = require("logger-sharelatex")
_ = require("underscore") _ = require("underscore")
@ -177,11 +175,6 @@ module.exports = class Router
webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.proxyToHistoryApi 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.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/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject
webRouter.get '/project/download/zip', AuthorizationMiddlewear.ensureUserCanReadMultipleProjects, ProjectDownloadsController.downloadMultipleProjects webRouter.get '/project/download/zip', AuthorizationMiddlewear.ensureUserCanReadMultipleProjects, ProjectDownloadsController.downloadMultipleProjects
@ -233,15 +226,6 @@ module.exports = class Router
webRouter.get "/project/:project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.getMessages webRouter.get "/project/:project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.getMessages
webRouter.post "/project/:project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.sendMessage webRouter.post "/project/:project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.sendMessage
# 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/index", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.index
webRouter.post "/project/:Project_id/references/indexAll", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.indexAll webRouter.post "/project/:Project_id/references/indexAll", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.indexAll

View file

@ -18,9 +18,7 @@ block content
| #{translate("beta_program_badge_description")} | #{translate("beta_program_badge_description")}
span.beta-feature-badge span.beta-feature-badge
p.text-centered p.text-centered
strong We're currently testing track changes and commenting: strong We're not currently testing anything in beta, but keep checking back!
p.text-centered
img(src="/img/teasers/track-changes/track-changes-beta.png", style="max-width: 100%; border-bottom: 1px solid #ddd")
.row.text-centered .row.text-centered
.col-md-12 .col-md-12
if user.betaProgram if user.betaProgram

View file

@ -3,11 +3,13 @@ extends ../layout
block content block content
.content .content
.container .container
.row .error-container
.col-md-8.col-md-offset-2.text-center .error-figure
.page-header img.error-img(
h2 #{translate("cant_find_page")} src="/img/brand/404-visual.svg"
p alt="Not found"
a(href="/") )
i.fa.fa-arrow-circle-o-left .error-details
| #{translate("take_me_home")} p.error-status Not found
p.error-description #{translate("cant_find_page")}
a.error-btn(href="/") Home

View file

@ -1,24 +1,36 @@
doctype html doctype html
html(itemscope, itemtype='http://schema.org/Product') html.full-height(itemscope, itemtype='http://schema.org/Product')
head head
title Something went wrong title Something went wrong
link(rel="icon", href="/favicon.ico") link(rel="icon", href="/favicon.ico")
if buildCssPath if buildCssPath
link(rel='stylesheet', href=buildCssPath('/style.css')) link(rel='stylesheet', href=buildCssPath('/style.css'))
link(href="//netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css",rel="stylesheet") link(href="//netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css",rel="stylesheet")
body body.full-height
.content .content.full-height
.container .container.full-height
.row .error-container.full-height
.col-md-8.col-md-offset-2.text-center .error-figure.error-figure-500
.page-header img.error-img(
h2 Oh dear, something went wrong. src="/img/brand/500-visual-socket.svg"
if buildImgPath alt="Error"
p )
img(src=buildImgPath("lion-sad-128.png"), alt="Sad Lion") .error-details
p p.error-status Something went wrong, sorry.
| 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.error-description Our staff are probably looking into this, but if it continues, please contact us at #{settings.adminEmail}
p a.error-btn(href="/") Home
a(href="/") //- .content
i.fa.fa-arrow-circle-o-left //- .container
| Take me home //- .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

View file

@ -18,7 +18,11 @@ html(itemscope, itemtype='http://schema.org/Product')
-else -else
title= translate(title) + ' - ShareLaTeX, ' + translate("online_latex_editor") 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')) link(rel='stylesheet', href=buildCssPath('/style.css'))
block _headLinks block _headLinks

View file

@ -7,12 +7,17 @@ block vars
block content block content
.editor(ng-controller="IdeController").full-size .editor(ng-controller="IdeController").full-size
.loading-screen(ng-show="state.loading") .loading-screen(ng-if="state.loading")
.container .loading-screen-lion-container
h3 #{translate("loading")}... .loading-screen-lion(
.progress style="height: 20%;"
.progress-bar(style="width: 20%", ng-style="{'width': state.load_progress + '%'}") ng-style="{ 'height': state.load_progress + '%' }"
p.text-center.text-danger(ng-if="state.error").ng-cloak )
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") span(ng-bind-html="state.error")
include ./editor/feature-onboarding include ./editor/feature-onboarding

View file

@ -19,7 +19,8 @@ div.full-size(
'rp-size-mini': (!ui.reviewPanelOpen && reviewPanel.hasEntries),\ 'rp-size-mini': (!ui.reviewPanelOpen && reviewPanel.hasEntries),\
'rp-size-expanded': ui.reviewPanelOpen,\ 'rp-size-expanded': ui.reviewPanelOpen,\
'rp-layout-left': reviewPanel.layoutToLeft,\ '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") .loading-panel(ng-show="!editor.sharejs_doc || editor.opening")

View file

@ -32,7 +32,7 @@
autoplay autoplay
loop 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") img(src="/img/onboarding/review-panel/open-review.gif")
div(ng-show="onboarding.innerStep === 2;") div(ng-show="onboarding.innerStep === 2;")
video.feat-onboard-video( video.feat-onboard-video(
@ -40,7 +40,7 @@
autoplay autoplay
loop 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") img(src="/img/onboarding/review-panel/commenting.gif")
div(ng-show="onboarding.innerStep === 3;") div(ng-show="onboarding.innerStep === 3;")
video.feat-onboard-video( video.feat-onboard-video(
@ -48,7 +48,7 @@
autoplay autoplay
loop 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") img(src="/img/onboarding/review-panel/add-changes.gif")
div(ng-show="onboarding.innerStep === 4;") div(ng-show="onboarding.innerStep === 4;")
video.feat-onboard-video( video.feat-onboard-video(
@ -56,7 +56,7 @@
autoplay autoplay
loop 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") img(src="/img/onboarding/review-panel/accept-changes.gif")
button.btn.btn-primary.feat-onboard-nav-btn( button.btn.btn-primary.feat-onboard-nav-btn(
ng-click="gotoNextStep();" ng-click="gotoNextStep();"

View file

@ -30,6 +30,11 @@ header.toolbar.toolbar-header.toolbar-with-labels(
span.name( span.name(
ng-dblclick="!permissions.admin || startRenaming()", ng-dblclick="!permissions.admin || startRenaming()",
ng-show="!state.renaming" 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 }} ) {{ project.name }}
input.form-control( input.form-control(
@ -94,7 +99,6 @@ header.toolbar.toolbar-header.toolbar-with-labels(
i.review-icon i.review-icon
p.toolbar-label p.toolbar-label
| #{translate("review")} | #{translate("review")}
span(style="vertical-align: 20%; margin-left: 4px; padding: 2px 4px;").beta-feature-badge
a.btn.btn-full-height( a.btn.btn-full-height(
href, href,
ng-if="permissions.admin", ng-if="permissions.admin",

View file

@ -1,10 +1,18 @@
#review-panel #review-panel
.rp-in-editor-widgets
a.rp-track-changes-indicator( a.rp-track-changes-indicator(
href href
ng-if="editor.wantTrackChanges" ng-if="editor.wantTrackChanges"
ng-click="toggleReviewPanel();" ng-click="toggleReviewPanel();"
ng-class="{ 'rp-track-changes-indicator-on-dark' : darkTheme }" ng-class="{ 'rp-track-changes-indicator-on-dark' : darkTheme }"
) !{translate("track_changes_is_on")} ) !{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 .review-panel-toolbar
resolved-comments-dropdown( resolved-comments-dropdown(
class="rp-flex-block" class="rp-flex-block"
@ -314,7 +322,7 @@ script(type='text/ng-template', id='resolvedCommentEntryTemplate')
script(type='text/ng-template', id='addCommentEntryTemplate') script(type='text/ng-template', id='addCommentEntryTemplate')
div div
.rp-entry-callout.rp-entry-callout-add-comment .rp-entry-callout.rp-entry-callout-add-comment
.rp-entry-indicator( .rp-entry-indicator.rp-entry-indicator-add-comment(
ng-if="!commentState.adding" ng-if="!commentState.adding"
ng-click="startNewComment(); onIndicatorClick();" ng-click="startNewComment(); onIndicatorClick();"
tooltip=translate("add_comment") tooltip=translate("add_comment")
@ -340,10 +348,10 @@ script(type='text/ng-template', id='addCommentEntryTemplate')
ng-keypress="handleCommentKeyPress($event);" ng-keypress="handleCommentKeyPress($event);"
placeholder=translate("add_your_comment_here") placeholder=translate("add_your_comment_here")
focus-on="comment:new:open" focus-on="comment:new:open"
ng-blur="submitNewComment()" ng-blur="submitNewComment($event)"
) )
.rp-entry-actions .rp-entry-actions
button.rp-entry-button( button.rp-entry-button.rp-entry-button-cancel(
ng-click="cancelNewComment();" ng-click="cancelNewComment();"
) )
i.fa.fa-times i.fa.fa-times

View file

@ -4,11 +4,11 @@ block scripts
script(src="https://js.recurly.com/v3/recurly.js") script(src="https://js.recurly.com/v3/recurly.js")
script(type='text/javascript'). script(type='text/javascript').
window.recomendedCurrency = '#{currency}'
window.countryCode = '#{countryCode}' window.countryCode = '#{countryCode}'
window.plan_code = '#{plan_code}' window.plan_code = '#{plan_code}'
window.recurlyApiKey = "!{settings.apis.recurly.publicKey}" window.recurlyApiKey = "!{settings.apis.recurly.publicKey}"
window.couponCode = "#{couponCode}" window.couponCode = !{JSON.stringify(couponCode)}
window.recomendedCurrency = !{JSON.stringify(currency.slice(0,3))}
block content block content
.content.content-alt .content.content-alt

View file

@ -35,7 +35,7 @@
"lynx": "0.1.1", "lynx": "0.1.1",
"marked": "^0.3.5", "marked": "^0.3.5",
"method-override": "^2.3.3", "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", "mimelib": "0.2.14",
"mocha": "1.17.1", "mocha": "1.17.1",
"mongojs": "2.4.0", "mongojs": "2.4.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -353,11 +353,17 @@ define [
@ranges.applyOp op, { user_id: track_changes_as } @ranges.applyOp op, { user_id: track_changes_as }
if old_id_seed? if old_id_seed?
@ranges.setIdSeed(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 = []) -> _catchUpRanges: (changes = [], comments = []) ->
# We've just been given the current server's ranges, but need to apply any local ops we have. # 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. # Reset to the server state then apply our local ops again.
@ranges.emit "clear" @emit "ranges:clear"
@ranges.changes = changes @ranges.changes = changes
@ranges.comments = comments @ranges.comments = comments
@ranges.track_changes = @doc.track_changes @ranges.track_changes = @doc.track_changes
@ -367,4 +373,4 @@ define [
for op in @doc.getPendingOp() or [] for op in @doc.getPendingOp() or []
@ranges.setIdSeed(@doc.track_changes_id_seeds.pending) @ranges.setIdSeed(@doc.track_changes_id_seeds.pending)
@ranges.applyOp(op, { user_id: @track_changes_as }) @ranges.applyOp(op, { user_id: @track_changes_as })
@ranges.emit "redraw" @emit "ranges:redraw"

View file

@ -121,11 +121,22 @@ define [
_bindToDocumentEvents: (doc, sharejs_doc) -> _bindToDocumentEvents: (doc, sharejs_doc) ->
sharejs_doc.on "error", (error, meta) => 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( @ide.showGenericMessageModal(
"Document Too Long" "Document Too Long"
"Sorry, this file is too long to be edited manually. Please upload it directly." "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 else
@ide.socket.disconnect() @ide.socket.disconnect()
@ide.reportError(error, meta) @ide.reportError(error, meta)

View file

@ -322,10 +322,6 @@ define [
doc = session.getDocument() doc = session.getDocument()
doc.on "change", onChange doc.on "change", onChange
sharejs_doc.on "remoteop.recordRemote", (op, oldSnapshot, msg) ->
undoManager.nextUpdateIsRemote = true
trackChangesManager.nextUpdateMetaData = msg?.meta
editor.initing = true editor.initing = true
sharejs_doc.attachToAce(editor) sharejs_doc.attachToAce(editor)
editor.initing = false editor.initing = false

View file

@ -14,11 +14,11 @@ define [
return if !track_changes? return if !track_changes?
@setTrackChanges(track_changes) @setTrackChanges(track_changes)
@$scope.$watch "sharejsDoc", (doc) => @$scope.$watch "sharejsDoc", (doc, oldDoc) =>
return if !doc? return if !doc?
@disconnectFromRangesTracker() if oldDoc?
@rangesTracker = doc.ranges @disconnectFromDoc(oldDoc)
@connectToRangesTracker() @connectToDoc(doc)
@$scope.$on "comment:add", (e, thread_id, offset, length) => @$scope.$on "comment:add", (e, thread_id, offset, length) =>
@addCommentToSelection(thread_id, offset, length) @addCommentToSelection(thread_id, offset, length)
@ -36,10 +36,10 @@ define [
@removeCommentId(comment_id) @removeCommentId(comment_id)
@$scope.$on "comment:resolve_threads", (e, thread_ids) => @$scope.$on "comment:resolve_threads", (e, thread_ids) =>
@resolveCommentByThreadIds(thread_ids) @hideCommentsByThreadIds(thread_ids)
@$scope.$on "comment:unresolve_thread", (e, thread_id) => @$scope.$on "comment:unresolve_thread", (e, thread_id) =>
@unresolveCommentByThreadId(thread_id) @showCommentByThreadId(thread_id)
@$scope.$on "review-panel:recalculate-screen-positions", () => @$scope.$on "review-panel:recalculate-screen-positions", () =>
@recalculateReviewEntriesScreenPositions() @recalculateReviewEntriesScreenPositions()
@ -73,16 +73,24 @@ define [
_scrollTimeout = null _scrollTimeout = null
, 200 , 200
@_resetCutState()
onCut = () => @onCut()
onPaste = () => @onPaste()
bindToAce = () => bindToAce = () =>
@editor.on "changeSelection", onChangeSelection @editor.on "changeSelection", onChangeSelection
@editor.on "change", onChangeSelection # Selection also moves with updates elsewhere in the document @editor.on "change", onChangeSelection # Selection also moves with updates elsewhere in the document
@editor.on "changeSession", onChangeSession @editor.on "changeSession", onChangeSession
@editor.on "cut", onCut
@editor.on "paste", onPaste
@editor.renderer.on "resize", onResize @editor.renderer.on "resize", onResize
unbindFromAce = () => unbindFromAce = () =>
@editor.off "changeSelection", onChangeSelection @editor.off "changeSelection", onChangeSelection
@editor.off "change", onChangeSelection @editor.off "change", onChangeSelection
@editor.off "changeSession", onChangeSession @editor.off "changeSession", onChangeSession
@editor.off "cut", onCut
@editor.off "paste", onPaste
@editor.renderer.off "resize", onResize @editor.renderer.off "resize", onResize
@$scope.$watch "trackChangesEnabled", (enabled) => @$scope.$watch "trackChangesEnabled", (enabled) =>
@ -92,18 +100,11 @@ define [
else else
unbindFromAce() unbindFromAce()
disconnectFromRangesTracker: () -> disconnectFromDoc: (doc) ->
@changeIdToMarkerIdMap = {} @changeIdToMarkerIdMap = {}
doc.off "ranges:clear"
if @rangesTracker? doc.off "ranges:redraw"
@rangesTracker.off "insert:added" doc.off "ranges:dirty"
@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"
setTrackChanges: (value) -> setTrackChanges: (value) ->
if value if value
@ -111,56 +112,15 @@ define [
else else
@$scope.sharejsDoc?.track_changes_as = null @$scope.sharejsDoc?.track_changes_as = null
connectToRangesTracker: () -> connectToDoc: (doc) ->
@rangesTracker = doc.ranges
@setTrackChanges(@$scope.trackChanges) @setTrackChanges(@$scope.trackChanges)
# Add a timeout because on remote ops, we get these notifications before doc.on "ranges:dirty", () =>
# ace has updated @updateAnnotations()
@rangesTracker.on "insert:added", (change) => doc.on "ranges:clear", () =>
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", () =>
@clearAnnotations() @clearAnnotations()
@rangesTracker.on "redraw", () => doc.on "ranges:redraw", () =>
@redrawAnnotations() @redrawAnnotations()
clearAnnotations: () -> clearAnnotations: () ->
@ -182,6 +142,55 @@ define [
@broadcastChange() @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) -> addComment: (offset, content, thread_id) ->
op = { c: content, p: offset, t: thread_id } op = { c: content, p: offset, t: thread_id }
# @rangesTracker.applyOp op # Will apply via sharejs # @rangesTracker.applyOp op # Will apply via sharejs
@ -200,6 +209,7 @@ define [
acceptChangeId: (change_id) -> acceptChangeId: (change_id) ->
@rangesTracker.removeChangeId(change_id) @rangesTracker.removeChangeId(change_id)
@updateAnnotations()
rejectChangeId: (change_id) -> rejectChangeId: (change_id) ->
change = @rangesTracker.getChange(change_id) change = @rangesTracker.getChange(change_id)
@ -208,21 +218,26 @@ define [
if change.op.d? if change.op.d?
content = change.op.d content = change.op.d
position = @_shareJsOffsetToAcePosition(change.op.p) position = @_shareJsOffsetToAcePosition(change.op.p)
session.$fromReject = true # Tell track changes to cancel out delete
session.insert(position, content) session.insert(position, content)
session.$fromReject = false
else if change.op.i? else if change.op.i?
start = @_shareJsOffsetToAcePosition(change.op.p) start = @_shareJsOffsetToAcePosition(change.op.p)
end = @_shareJsOffsetToAcePosition(change.op.p + change.op.i.length) end = @_shareJsOffsetToAcePosition(change.op.p + change.op.i.length)
editor_text = session.getDocument().getTextRange({start, end}) editor_text = session.getDocument().getTextRange({start, end})
if editor_text != change.op.i if editor_text != change.op.i
throw new Error("Op to be removed (#{JSON.stringify(change.op)}), does not match editor text, '#{editor_text}'") 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.remove({start, end})
session.$fromReject = false
else else
throw new Error("unknown change: #{JSON.stringify(change)}") throw new Error("unknown change: #{JSON.stringify(change)}")
removeCommentId: (comment_id) -> removeCommentId: (comment_id) ->
@rangesTracker.removeCommentId(comment_id) @rangesTracker.removeCommentId(comment_id)
@updateAnnotations()
resolveCommentByThreadIds: (thread_ids) -> hideCommentsByThreadIds: (thread_ids) ->
resolve_ids = {} resolve_ids = {}
for id in thread_ids for id in thread_ids
resolve_ids[id] = true resolve_ids[id] = true
@ -231,12 +246,55 @@ define [
@_onCommentRemoved(comment) @_onCommentRemoved(comment)
@broadcastChange() @broadcastChange()
unresolveCommentByThreadId: (thread_id) -> showCommentByThreadId: (thread_id) ->
for comment in @rangesTracker?.comments or [] for comment in @rangesTracker?.comments or []
if comment.op.t == thread_id if comment.op.t == thread_id
@_onCommentAdded(comment) @_onCommentAdded(comment)
@broadcastChange() @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: () -> checkMapping: () ->
# TODO: reintroduce this check # TODO: reintroduce this check
session = @editor.getSession() session = @editor.getSession()
@ -421,23 +479,18 @@ define [
lines = @editor.getSession().getDocument().getAllLines() lines = @editor.getSession().getDocument().getAllLines()
return AceShareJsCodec.shareJsOffsetToAcePosition(offset, lines) return AceShareJsCodec.shareJsOffsetToAcePosition(offset, lines)
_onChangesMoved: (changes) -> _onChangeMoved: (change) ->
# 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) start = @_shareJsOffsetToAcePosition(change.op.p)
if change.op.i? if change.op.i?
end = @_shareJsOffsetToAcePosition(change.op.p + change.op.i.length) end = @_shareJsOffsetToAcePosition(change.op.p + change.op.i.length)
else else
end = start end = start
@_updateMarker(change.id, start, end) @_updateMarker(change.id, start, end)
@editor.renderer.updateBackMarkers()
_onCommentMoved: (comment) -> _onCommentMoved: (comment) ->
start = @_shareJsOffsetToAcePosition(comment.op.p) start = @_shareJsOffsetToAcePosition(comment.op.p)
end = @_shareJsOffsetToAcePosition(comment.op.p + comment.op.c.length) end = @_shareJsOffsetToAcePosition(comment.op.p + comment.op.c.length)
@_updateMarker(comment.id, start, end) @_updateMarker(comment.id, start, end)
@editor.renderer.updateBackMarkers()
_updateMarker: (change_id, start, end) -> _updateMarker: (change_id, start, end) ->
return if !@changeIdToMarkerIdMap[change_id]? return if !@changeIdToMarkerIdMap[change_id]?

View file

@ -11,10 +11,10 @@ define [
show_remote_warning: false show_remote_warning: false
@reset() @reset()
@nextUpdateIsRemote = false
@editor.on "changeSession", (e) => @editor.on "changeSession", (e) =>
@reset() @reset()
@session = e.session
e.session.setUndoManager(@) e.session.setUndoManager(@)
showUndoConflictWarning: () -> showUndoConflictWarning: () ->
@ -38,20 +38,44 @@ define [
@firstUpdate = false @firstUpdate = false
return return
aceDeltaSets = options.args[0] aceDeltaSets = options.args[0]
@session = options.args[1]
return if !aceDeltaSets? return if !aceDeltaSets?
@session = options.args[1]
lines = @session.getDocument().getAllLines() # We need to split the delta sets into local or remote groups before pushing onto
linesBeforeChange = @_revertAceDeltaSetsOnDocLines(aceDeltaSets, lines) # the undo stack, since these are treated differently.
simpleDeltaSets = @_aceDeltaSetsToSimpleDeltaSets(aceDeltaSets, linesBeforeChange) splitDeltaSets = []
@undoStack.push( currentDeltaSet = null # Make global to this function
deltaSets: simpleDeltaSets do newDeltaSet = () ->
remote: @nextUpdateIsRemote 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 = [] @redoStack = []
@nextUpdateIsRemote = false
undo: (dontSelect) -> 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() localUpdatesMade = @_shiftLocalChangeToTopOfUndoStack()
return if !localUpdatesMade return if !localUpdatesMade
@ -206,19 +230,16 @@ define [
throw "Unknown delta type" throw "Unknown delta type"
return doc.split("\n") return doc.split("\n")
_aceDeltaSetsToSimpleDeltaSets: (aceDeltaSets, docLines) -> _aceDeltaSetToSimpleDeltaSet: (deltaSet, docLines) ->
simpleDeltaSets = []
for deltaSet in aceDeltaSets
if deltaSet.group == "doc" # ignore fold changes
simpleDeltas = [] simpleDeltas = []
for delta in deltaSet.deltas for delta in deltaSet.deltas
simpleDeltas.push @_aceDeltaToSimpleDelta(delta, docLines) simpleDeltas.push @_aceDeltaToSimpleDelta(delta, docLines)
docLines = @_applyAceDeltasToDocLines([delta], docLines) docLines = @_applyAceDeltasToDocLines([delta], docLines)
simpleDeltaSets.push { simpleDeltaSet = {
deltas: simpleDeltas deltas: simpleDeltas
group: deltaSet.group group: deltaSet.group
} }
return simpleDeltaSets return {simpleDeltaSet, docLines}
_simpleDeltaSetsToAceDeltaSets: (simpleDeltaSets, docLines) -> _simpleDeltaSetsToAceDeltaSets: (simpleDeltaSets, docLines) ->
for deltaSet in simpleDeltaSets for deltaSet in simpleDeltaSets

View file

@ -3,7 +3,7 @@
Range = ace.require("ace/range").Range Range = ace.require("ace/range").Range
# Convert an ace delta into an op understood by share.js # 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 # Get the start position of the range, in no. of characters
getStartOffsetPosition = (start) -> getStartOffsetPosition = (start) ->
# This is quite inefficient - getLines makes a copy of the entire # This is quite inefficient - getLines makes a copy of the entire
@ -27,11 +27,11 @@ applyToShareJS = (editorDoc, delta, doc) ->
switch delta.action switch delta.action
when 'insert' when 'insert'
text = delta.lines.join('\n') text = delta.lines.join('\n')
doc.insert pos, text doc.insert pos, text, fromUndo
when 'remove' when 'remove'
text = delta.lines.join('\n') text = delta.lines.join('\n')
doc.del pos, text.length doc.del pos, text.length, fromUndo
else throw new Error "unknown action: #{delta.action}" else throw new Error "unknown action: #{delta.action}"
@ -79,7 +79,9 @@ window.sharejs.extendDoc 'attach_ace', (editor, keepEditorContents, maxDocLength
doc.emit "error", new Error("document length is greater than maxDocLength") doc.emit "error", new Error("document length is greater than maxDocLength")
return return
applyToShareJS editorDoc, change, doc fromUndo = !!(editor.getSession().$fromUndo or editor.getSession().$fromReject)
applyToShareJS editorDoc, change, doc, fromUndo
check() check()
@ -108,16 +110,46 @@ window.sharejs.extendDoc 'attach_ace', (editor, keepEditorContents, maxDocLength
row:row, column:offset 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) -> 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 suppress = true
editorDoc.insert offsetToPos(pos), text editorDoc.applyDelta({
start: start,
end: end,
action: "insert",
lines: lines,
remote: true
});
suppress = false suppress = false
check() check()
doc.on 'delete', (pos, text) -> doc.on 'delete', (pos, text) ->
suppress = true
range = Range.fromPoints offsetToPos(pos), offsetToPos(pos + text.length) 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 suppress = false
check() check()

View file

@ -11,14 +11,20 @@ text.api =
# Get the text contents of a document # Get the text contents of a document
getText: -> @snapshot getText: -> @snapshot
insert: (pos, text, callback) -> insert: (pos, text, fromUndo, callback) ->
op = [{p:pos, i:text}] op = {p:pos, i:text}
if fromUndo
op.u = true
op = [op]
@submitOp op, callback @submitOp op, callback
op op
del: (pos, length, callback) -> del: (pos, length, fromUndo, callback) ->
op = [{p:pos, d:@snapshot[pos...(pos + length)]}] op = {p:pos, d:@snapshot[pos...(pos + length)]}
if fromUndo
op.u = true
op = [op]
@submitOp op, callback @submitOp op, callback
op op

View file

@ -56,6 +56,13 @@ text.apply = (snapshot, op) ->
throw new Error "Unknown op type" throw new Error "Unknown op type"
snapshot 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. # Exported for use by the random op generator.
# #
@ -69,10 +76,10 @@ text._append = append = (newOp, c) ->
last = newOp[newOp.length - 1] last = newOp[newOp.length - 1]
# Compose the insert into the previous insert if possible # Compose the insert into the previous insert if possible
if last.i? && c.i? and last.p <= c.p <= (last.p + last.i.length) if last.i? && c.i? and last.p <= c.p <= (last.p + last.i.length) and last.u == c.u
newOp[newOp.length - 1] = {i:strInject(last.i, c.p - last.p, c.i), p:last.p} 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) 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] = {d:strInject(c.d, last.p - c.p, last.d), p:c.p} newOp[newOp.length - 1] = cloneAndModify(last, {d:strInject(c.d, last.p - c.p, last.d), p: c.p})
else else
newOp.push c newOp.push c
@ -150,25 +157,25 @@ text._tc = transformComponent = (dest, c, otherC, side) ->
checkValidOp [otherC] checkValidOp [otherC]
if c.i? 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 else if c.d? # Delete
if otherC.i? # delete vs insert if otherC.i? # delete vs insert
s = c.d s = c.d
if c.p < otherC.p 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)..] s = s[(otherC.p - c.p)..]
if s != '' 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 else if otherC.d? # Delete vs delete
if c.p >= otherC.p + otherC.d.length 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 else if c.p + c.d.length <= otherC.p
append dest, c append dest, c
else else
# They overlap somewhere. # They overlap somewhere.
newC = {d:'', p:c.p} newC = cloneAndModify(c, {d:''})
if c.p < otherC.p if c.p < otherC.p
newC.d = c.d[...(otherC.p - c.p)] newC.d = c.d[...(otherC.p - c.p)]
if c.p + c.d.length > otherC.p + otherC.d.length 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 if c.p < otherC.p < c.p + c.c.length
offset = otherC.p - c.p offset = otherC.p - c.p
new_c = (c.c[0..(offset-1)] + otherC.i + c.c[offset...]) 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 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? else if otherC.d?
if c.p >= otherC.p + otherC.d.length 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 else if c.p + c.c.length <= otherC.p
append dest, c append dest, c
else # Delete overlaps comment else # Delete overlaps comment
# They overlap somewhere. # They overlap somewhere.
newC = {c:'', p:c.p, t: c.t} newC = cloneAndModify(c, {c:''})
if c.p < otherC.p if c.p < otherC.p
newC.c = c.c[...(otherC.p - c.p)] newC.c = c.c[...(otherC.p - c.p)]
if c.p + c.c.length > otherC.p + otherC.d.length if c.p + c.c.length > otherC.p + otherC.d.length

View file

@ -173,6 +173,8 @@ define [
@_findEntityByPathInFolder @$scope.rootFolder, path @_findEntityByPathInFolder @$scope.rootFolder, path
_findEntityByPathInFolder: (folder, path) -> _findEntityByPathInFolder: (folder, path) ->
if !path? or !folder?
return null
parts = path.split("/") parts = path.split("/")
name = parts.shift() name = parts.shift()
rest = parts.join("/") rest = parts.join("/")

View file

@ -93,11 +93,11 @@ define [
if !name? or name.length == 0 if !name? or name.length == 0
return return
$scope.state.inflight = true $scope.state.inflight = true
$scope.state.inflight = true
ide.fileTreeManager ide.fileTreeManager
.createFolder(name, parent_folder) .createFolder(name, parent_folder)
.error (e)-> .error (e)->
$scope.error = e $scope.error = e
$scope.state.inflight = false
.success () -> .success () ->
$scope.state.inflight = false $scope.state.inflight = false
$modalInstance.close() $modalInstance.close()

View file

@ -1,5 +1,8 @@
load = (EventEmitter) -> # This file is shared between document-updater and web, so that the server and client share
class RangesTracker extends EventEmitter # 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 # 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: # track changes in Word. We store these as a set of ShareJs style ranges:
# {i: "foo", p: 42} # Insert 'foo' at offset 42 # {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. # middle of a previous insert by the first user, the original insert will be split into two.
constructor: (@changes = [], @comments = []) -> constructor: (@changes = [], @comments = []) ->
@setIdSeed(RangesTracker.generateIdSeed()) @setIdSeed(RangesTracker.generateIdSeed())
@resetDirtyState()
getIdSeed: () -> getIdSeed: () ->
return @id_seed return @id_seed
@ -75,7 +79,14 @@ load = (EventEmitter) ->
comment = @getComment(comment_id) comment = @getComment(comment_id)
return if !comment? return if !comment?
@comments = @comments.filter (c) -> c.id != comment_id @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) -> getChange: (change_id) ->
change = null change = null
@ -90,6 +101,18 @@ load = (EventEmitter) ->
return if !change? return if !change?
@_removeChange(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 = {}) -> applyOp: (op, metadata = {}) ->
metadata.ts ?= new Date() metadata.ts ?= new Date()
# Apply an op that has been applied to the document to our changes to keep them up to date # Apply an op that has been applied to the document to our changes to keep them up to date
@ -104,8 +127,16 @@ load = (EventEmitter) ->
else else
throw new Error("unknown op type") throw new Error("unknown op type")
applyOps: (ops, metadata = {}) ->
for op in ops
@applyOp(op, metadata)
addComment: (op, metadata) -> addComment: (op, metadata) ->
# TODO: Don't allow overlapping comments? existing = @getComment(op.t)
if existing?
@moveCommentId(op.t, op.p, op.c)
return existing
else
@comments.push comment = { @comments.push comment = {
id: op.t or @newId() id: op.t or @newId()
op: # Copy because we'll modify in place op: # Copy because we'll modify in place
@ -114,18 +145,18 @@ load = (EventEmitter) ->
t: op.t t: op.t
metadata metadata
} }
@emit "comment:added", comment @_markAsDirty comment, "comment", "added"
return comment return comment
applyInsertToComments: (op) -> applyInsertToComments: (op) ->
for comment in @comments for comment in @comments
if op.p <= comment.op.p if op.p <= comment.op.p
comment.op.p += op.i.length 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 else if op.p < comment.op.p + comment.op.c.length
offset = op.p - comment.op.p offset = op.p - comment.op.p
comment.op.c = comment.op.c[0..(offset-1)] + op.i + comment.op.c[offset...] 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) -> applyDeleteToComments: (op) ->
op_start = op.p op_start = op.p
@ -138,7 +169,7 @@ load = (EventEmitter) ->
if op_end <= comment_start if op_end <= comment_start
# delete is fully before comment # delete is fully before comment
comment.op.p -= op_length comment.op.p -= op_length
@emit "comment:moved", comment @_markAsDirty comment, "comment", "moved"
else if op_start >= comment_end else if op_start >= comment_end
# delete is fully after comment, nothing to do # delete is fully after comment, nothing to do
else else
@ -161,12 +192,13 @@ load = (EventEmitter) ->
comment.op.p = Math.min(comment_start, op_start) comment.op.p = Math.min(comment_start, op_start)
comment.op.c = remaining_before + remaining_after comment.op.c = remaining_before + remaining_after
@emit "comment:moved", comment @_markAsDirty comment, "comment", "moved"
applyInsertToChanges: (op, metadata) -> applyInsertToChanges: (op, metadata) ->
op_start = op.p op_start = op.p
op_length = op.i.length op_length = op.i.length
op_end = op.p + op_length op_end = op.p + op_length
undoing = !!op.u
already_merged = false already_merged = false
@ -184,8 +216,9 @@ load = (EventEmitter) ->
change.op.p += op_length change.op.p += op_length
moved_changes.push change moved_changes.push change
else if op_start == change_start else if op_start == change_start
# If the insert matches the start of the delete, just remove it from the delete instead # If we are undoing, then we want to cancel any existing delete ranges if we can.
if change.op.d.length >= op.i.length and change.op.d.slice(0, op.i.length) == op.i # 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.d = change.op.d.slice(op.i.length)
change.op.p += op.i.length change.op.p += op.i.length
if change.op.d == "" if change.op.d == ""
@ -203,15 +236,15 @@ load = (EventEmitter) ->
# Only merge inserts if they are from the same user # Only merge inserts if they are from the same user
is_same_user = metadata.user_id == change.metadata.user_id 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 # If we are undoing, then our changes will be removed from any delete ops just after. In that case, if there is also
# delete then we shouldn't append it to this insert, but instead only cancel the following delete. # an insert op just before, then we shouldn't append it to this insert, but instead only cancel the following delete.
# E.g. # E.g.
# foo|<--- about to insert 'b' here # foo|<--- about to insert 'b' here
# inserted 'foo' --^ ^-- deleted 'bar' # inserted 'foo' --^ ^-- deleted 'bar'
# should become just 'foo' not 'foob' (with the delete marker becoming just 'ar'), . # should become just 'foo' not 'foob' (with the delete marker becoming just 'ar'), .
next_change = @changes[i+1] 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 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 # 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. # 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 for change in remove_changes
@_removeChange change @_removeChange change
if moved_changes.length > 0 for change in moved_changes
@emit "changes:moved", moved_changes @_markAsDirty change, "change", "moved"
applyDeleteToChanges: (op, metadata) -> applyDeleteToChanges: (op, metadata) ->
op_start = op.p op_start = op.p
@ -406,8 +439,8 @@ load = (EventEmitter) ->
@_removeChange change @_removeChange change
moved_changes = moved_changes.filter (c) -> c != change moved_changes = moved_changes.filter (c) -> c != change
if moved_changes.length > 0 for change in moved_changes
@emit "changes:moved", moved_changes @_markAsDirty change, "change", "moved"
_addOp: (op, metadata) -> _addOp: (op, metadata) ->
change = { change = {
@ -427,17 +460,11 @@ load = (EventEmitter) ->
else else
return -1 return -1
if op.d? @_markAsDirty(change, "change", "added")
@emit "delete:added", change
else if op.i?
@emit "insert:added", change
_removeChange: (change) -> _removeChange: (change) ->
@changes = @changes.filter (c) -> c.id != change.id @changes = @changes.filter (c) -> c.id != change.id
if change.op.d? @_markAsDirty change, "change", "removed"
@emit "delete:removed", change
else if change.op.i?
@emit "insert:removed", change
_applyOpModifications: (content, op_modifications) -> _applyOpModifications: (content, op_modifications) ->
# Put in descending position order, with deleting first if at the same offset # Put in descending position order, with deleting first if at the same offset
@ -486,13 +513,32 @@ load = (EventEmitter) ->
previous_change = change previous_change = change
return { moved_changes, remove_changes } 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: (object) ->
clone = {} clone = {}
(clone[k] = v for k,v of object) (clone[k] = v for k,v of object)
return clone return clone
if define? if define?
define ["utils/EventEmitter"], load define [], load
else else
EventEmitter = require("events").EventEmitter module.exports = load()
module.exports = load(EventEmitter)

View file

@ -4,7 +4,7 @@ define [
"ide/colors/ColorManager" "ide/colors/ColorManager"
"ide/review-panel/RangesTracker" "ide/review-panel/RangesTracker"
], (App, EventEmitter, ColorManager, 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" $reviewPanelEl = $element.find "#review-panel"
$scope.SubViews = $scope.SubViews =
@ -27,6 +27,14 @@ define [
layoutToLeft: false layoutToLeft: false
rendererData: {} rendererData: {}
loadingThreads: false 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", () -> window.addEventListener "beforeunload", () ->
collapsedStates = {} collapsedStates = {}
@ -163,7 +171,11 @@ define [
$scope.$watch (() -> $scope.$watch (() ->
entries = $scope.reviewPanel.entries[$scope.editor.open_doc_id] or {} 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) -> ), (nEntries) ->
$scope.reviewPanel.hasEntries = nEntries > 0 and $scope.project.features.trackChangesVisible $scope.reviewPanel.hasEntries = nEntries > 0 and $scope.project.features.trackChangesVisible
@ -288,13 +300,6 @@ define [
delete entries["add-comment"] delete entries["add-comment"]
if selection 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"] = { entries["add-comment"] = {
type: "add-comment" type: "add-comment"
offset: selection_offset_start offset: selection_offset_start
@ -323,10 +328,16 @@ define [
$scope.$broadcast "change:reject", entry_id $scope.$broadcast "change:reject", entry_id
event_tracking.sendMB "rp-change-rejected", { view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini' } 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.startNewComment = () ->
$scope.$broadcast "comment:select_line" $scope.$broadcast "comment:select_line"
$timeout () -> $timeout () ->
$scope.$broadcast "review-panel:layout" $scope.$broadcast "review-panel:layout"
if $scope.shouldABAddCommentBtn and !$scope.ui.reviewPanelOpen
sixpack.convert "add-comment-btn"
$scope.submitNewComment = (content) -> $scope.submitNewComment = (content) ->
return if !content? or content == "" return if !content? or content == ""
@ -396,7 +407,7 @@ define [
return if !thread? return if !thread?
thread.resolved = true thread.resolved = true
thread.resolved_by_user = formatUser(user) thread.resolved_by_user = formatUser(user)
thread.resolved_at = new Date() thread.resolved_at = new Date().toISOString()
$scope.reviewPanel.resolvedThreadIds[thread_id] = true $scope.reviewPanel.resolvedThreadIds[thread_id] = true
$scope.$broadcast "comment:resolve_threads", [thread_id] $scope.$broadcast "comment:resolve_threads", [thread_id]

View file

@ -15,6 +15,9 @@ define [
isAdding: false isAdding: false
content: "" content: ""
scope.$on "comment:start_adding", () ->
scope.startNewComment()
scope.startNewComment = () -> scope.startNewComment = () ->
scope.state.isAdding = true scope.state.isAdding = true
scope.onStartNew() scope.onStartNew()
@ -31,7 +34,10 @@ define [
if scope.state.content.length > 0 if scope.state.content.length > 0
scope.submitNewComment() 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.onSubmit { content: scope.state.content }
scope.state.isAdding = false scope.state.isAdding = false
scope.state.content = "" scope.state.content = ""

View file

@ -2,9 +2,13 @@ define [
"base" "base"
], (App) -> ], (App) ->
MAX_PROJECT_NAME_LENGTH = 150 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 = $scope.state =
renaming: false renaming: false
overflowed: false
$scope.inputs = {} $scope.inputs = {}
$scope.startRenaming = () -> $scope.startRenaming = () ->
@ -29,4 +33,7 @@ define [
$scope.$watch "project.name", (name) -> $scope.$watch "project.name", (name) ->
if name? if name?
window.document.title = name + " - Online LaTeX Editor ShareLaTeX" 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)
] ]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View 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

View 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

View 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

View 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

View 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

View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 593 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Some files were not shown because too many files have changed in this diff Show more