diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
index e53ee477b4..f3faad6c69 100644
--- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
+++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
@@ -30,6 +30,24 @@ module.exports = AuthenticationController =
deserializeUser: (user, cb) ->
cb(null, user)
+ afterLoginSessionSetup: (req, user, callback=(err)->) ->
+ req.login user, (err) ->
+ # Regenerate the session to get a new sessionID (cookie value) to
+ # protect against session fixation attacks
+ oldSession = req.session
+ req.session.destroy()
+ req.sessionStore.generate(req)
+ for key, value of oldSession
+ req.session[key] = value
+ # copy to the old `session.user` location, for backward-comptability
+ req.session.user = req.session.passport.user
+ req.session.save (err) ->
+ if err?
+ logger.err {user_id: user._id}, "error saving regenerated session after login"
+ return callback(err)
+ UserSessionsManager.trackSession(user, req.sessionID, () ->)
+ callback(null)
+
passportLogin: (req, res, next) ->
# This function is middleware which wraps the passport.authenticate middleware,
# so we can send back our custom `{message: {text: "", type: ""}}` responses on failure,
@@ -38,22 +56,10 @@ module.exports = AuthenticationController =
if err?
return next(err)
if user # `user` is either a user object or false
- req.login user, (err) ->
- # Regenerate the session to get a new sessionID (cookie value) to
- # protect against session fixation attacks
- oldSession = req.session
- req.session.destroy()
- req.sessionStore.generate(req)
- for key, value of oldSession
- req.session[key] = value
- # copy to the old `session.user` location, for backward-comptability
- req.session.user = req.session.passport.user
- req.session.save (err) ->
- if err?
- logger.err {user_id: user._id}, "error saving regenerated session after login"
- return next(err)
- UserSessionsManager.trackSession(user, req.sessionID, () ->)
- res.json {redir: req._redir}
+ AuthenticationController.afterLoginSessionSetup req, user, (err) ->
+ if err?
+ return next(err)
+ res.json {redir: req._redir}
else
res.json message: info
)(req, res, next)
diff --git a/services/web/app/coffee/Features/User/UserCreator.coffee b/services/web/app/coffee/Features/User/UserCreator.coffee
index ee5a426e78..9218a48555 100644
--- a/services/web/app/coffee/Features/User/UserCreator.coffee
+++ b/services/web/app/coffee/Features/User/UserCreator.coffee
@@ -17,6 +17,7 @@ module.exports =
user = new User()
user.email = opts.email
user.holdingAccount = opts.holdingAccount
+ user.ace.syntaxValidation = true
username = opts.email.match(/^[^@]*/)
if opts.first_name? and opts.first_name.length != 0
diff --git a/services/web/app/coffee/infrastructure/Server.coffee b/services/web/app/coffee/infrastructure/Server.coffee
index 7d551f89b3..e19d9c5ca8 100644
--- a/services/web/app/coffee/infrastructure/Server.coffee
+++ b/services/web/app/coffee/infrastructure/Server.coffee
@@ -106,6 +106,10 @@ passport.use(new LocalStrategy(
passport.serializeUser(AuthenticationController.serializeUser)
passport.deserializeUser(AuthenticationController.deserializeUser)
+Modules.hooks.fire 'passportSetup', passport, (err) ->
+ if err?
+ logger.err {err}, "error setting up passport in modules"
+
# Measure expiry from last request, not last login
webRouter.use (req, res, next) ->
req.session.touch()
diff --git a/services/web/app/coffee/models/User.coffee b/services/web/app/coffee/models/User.coffee
index 3b29936ab2..f120422128 100644
--- a/services/web/app/coffee/models/User.coffee
+++ b/services/web/app/coffee/models/User.coffee
@@ -26,7 +26,7 @@ UserSchema = new Schema
autoComplete: {type : Boolean, default: true}
spellCheckLanguage : {type : String, default: "en"}
pdfViewer : {type : String, default: "pdfjs"}
- syntaxValidation : {type : Boolean, default: true}
+ syntaxValidation : {type : Boolean}
}
features : {
collaborators: { type:Number, default: Settings.defaultFeatures.collaborators }
diff --git a/services/web/app/views/project/editor.jade b/services/web/app/views/project/editor.jade
index 8bcbc1976d..a780b4a790 100644
--- a/services/web/app/views/project/editor.jade
+++ b/services/web/app/views/project/editor.jade
@@ -15,7 +15,8 @@ block content
p.text-center.text-danger(ng-if="state.error").ng-cloak
span(ng-bind-html="state.error")
-
+ include ./editor/feature-onboarding
+
.global-alerts(ng-cloak)
.alert.alert-danger.small(ng-if="connection.forced_disconnect")
strong #{translate("disconnected")}
@@ -88,7 +89,8 @@ block content
block requirejs
script(type="text/javascript" src='/socket.io/socket.io.js')
- //- don't use cdn for worker
+ //- don't use cdn for workers
+ - var aceWorkerPath = buildJsPath(lib('ace'), {cdn:false,fingerprint:false})
- var pdfWorkerPath = buildJsPath('/libs/' + lib('pdfjs') + '/pdf.worker', {cdn:false,fingerprint:false})
//- We need to do .replace(/\//g, '\\/') do that '' -> '<\/script>'
@@ -132,10 +134,6 @@ block requirejs
}
};
window.aceFingerprint = "#{fingerprint(jsPath + lib('ace') + '/ace.js')}"
-
- - var aceWorkerPath = user.betaProgram ? buildJsPath(lib('ace'), {cdn:false,fingerprint:false}) : "" // don't use cdn for worker
-
- script(type='text/javascript').
window.aceWorkerPath = "#{aceWorkerPath}";
script(
diff --git a/services/web/app/views/project/editor/feature-onboarding.jade b/services/web/app/views/project/editor/feature-onboarding.jade
new file mode 100644
index 0000000000..1a8d769088
--- /dev/null
+++ b/services/web/app/views/project/editor/feature-onboarding.jade
@@ -0,0 +1,54 @@
+.feat-onboard(
+ ng-controller="FeatureOnboardingController"
+ ng-class="('feat-onboard-step' + innerStep)"
+ ng-if="!state.loading && ui.showCodeCheckerOnboarding"
+ ng-cloak
+)
+ .feat-onboard-wrapper
+ h1.feat-onboard-title
+ | Introducing
+ span.feat-onboard-title-name Code check
+ div(ng-if="innerStep === 1;")
+ p.feat-onboard-description
+ span.feat-onboard-description-name Code check
+ | will highlight potential problems in your LaTeX code, allowing you to handle errors earlier and become more productive.
+ .row
+ video.feat-onboard-video(autoplay, loop)
+ source(src="/img/teasers/code-checker/code-checker.mp4", type="video/mp4")
+ img(src="/img/teasers/code-checker/code-checker.gif")
+ .row.feat-onboard-adv-wrapper
+ .col-xs-4
+ h2.feat-onboard-adv-title
+ | Missing
+ span.feat-onboard-adv-title-highlight brackets
+ p Forgot to place a closing bracket? We'll warn you.
+ .col-xs-4
+ h2.feat-onboard-adv-title
+ | Unclosed
+ span.feat-onboard-adv-title-highlight environments
+ p
+ | Know when you are missing an
+ code \end{...}
+ | command.
+ .col-xs-4
+ h2.feat-onboard-adv-title
+ | Incorrect
+ span.feat-onboard-adv-title-highlight nesting
+ p
+ | Order matters. Get notified when you use an
+ code \end{...}
+ | too soon.
+ .feat-onboard-btn-wrapper
+ button.btn.btn-primary(ng-click="turnCodeCheckOn();") Yes, turn Code check on
+ .feat-onboard-btn-wrapper
+ button.btn.btn-default(ng-click="turnCodeCheckOff();") No, disable it for now
+ div(ng-if="innerStep === 2;")
+ p.feat-onboard-description
+ | Remember: you can always turn
+ span.feat-onboard-description-name Code check
+ em on
+ | or
+ em off
+ |, in the settings menu.
+ .feat-onboard-btn-wrapper
+ button.btn.btn-primary(ng-click="dismiss();") OK, got it
\ No newline at end of file
diff --git a/services/web/app/views/project/editor/history.jade b/services/web/app/views/project/editor/history.jade
index 3799b22363..4806b0a9b8 100644
--- a/services/web/app/views/project/editor/history.jade
+++ b/services/web/app/views/project/editor/history.jade
@@ -2,84 +2,79 @@ div#history(ng-show="ui.view == 'history'")
span(ng-controller="HistoryPremiumPopup")
.upgrade-prompt(ng-if="project.features.versioning === false && ui.view === 'history'")
- div(sixpack-switch="teaser-history")
- .message(
- sixpack-default
- ng-show="project.owner._id == user.id"
- )
- p.text-center: strong #{translate("upgrade_to_get_feature", {feature:"full Project History"})}
- p.text-center.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
- ul.list-unstyled
- li
- i.fa.fa-check
- | #{translate("unlimited_projects")}
-
- li
- i.fa.fa-check
- | #{translate("collabs_per_proj", {collabcount:'Multiple'})}
-
- li
- i.fa.fa-check
- | #{translate("full_doc_history")}
-
- li
- i.fa.fa-check
- | #{translate("sync_to_dropbox")}
-
- li
- i.fa.fa-check
- | #{translate("sync_to_github")}
-
- li
- i.fa.fa-check
- |#{translate("compile_larger_projects")}
- p.text-center(ng-controller="FreeTrialModalController")
- a.btn.btn-success(
- href
- ng-class="buttonClass"
- ng-click="startFreeTrial('history')"
- sixpack-convert="teaser-history"
- ) #{translate("start_free_trial")}
-
- .message.message-wider(
- sixpack-when="focused"
- ng-show="project.owner._id == user.id"
- )
- header.message-header
- h3 History
-
- .message-body
- h4.teaser-title See who changed what. Go back to previous versions.
- img.teaser-img(
- src="/img/teasers/history/teaser-history.png"
- alt="History"
- )
+ div(ng-if="project.owner._id == user.id")
+ div(sixpack-switch="teaser-history")
+ .message(sixpack-default)
+ p.text-center: strong #{translate("upgrade_to_get_feature", {feature:"full Project History"})}
p.text-center.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
- .row
- .col-md-8.col-md-offset-2
- ul.list-unstyled
- li
- i.fa.fa-check
- | Catch up with your collaborators changes
-
- li
- i.fa.fa-check
- | See changes over any time period
-
- li
- i.fa.fa-check
- | Revert your documents to previous versions
-
- li
- i.fa.fa-check
- | Restore deleted files
+ ul.list-unstyled
+ li
+ i.fa.fa-check
+ | #{translate("unlimited_projects")}
+
+ li
+ i.fa.fa-check
+ | #{translate("collabs_per_proj", {collabcount:'Multiple'})}
+
+ li
+ i.fa.fa-check
+ | #{translate("full_doc_history")}
+
+ li
+ i.fa.fa-check
+ | #{translate("sync_to_dropbox")}
+
+ li
+ i.fa.fa-check
+ | #{translate("sync_to_github")}
+
+ li
+ i.fa.fa-check
+ |#{translate("compile_larger_projects")}
p.text-center(ng-controller="FreeTrialModalController")
a.btn.btn-success(
href
ng-class="buttonClass"
ng-click="startFreeTrial('history')"
sixpack-convert="teaser-history"
- ) Try it for free
+ ) #{translate("start_free_trial")}
+
+ .message.message-wider(sixpack-when="focused")
+ header.message-header
+ h3 History
+
+ .message-body
+ h4.teaser-title See who changed what. Go back to previous versions.
+ img.teaser-img(
+ src="/img/teasers/history/teaser-history.png"
+ alt="History"
+ )
+ p.text-center.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
+ .row
+ .col-md-8.col-md-offset-2
+ ul.list-unstyled
+ li
+ i.fa.fa-check
+ | Catch up with your collaborators changes
+
+ li
+ i.fa.fa-check
+ | See changes over any time period
+
+ li
+ i.fa.fa-check
+ | Revert your documents to previous versions
+
+ li
+ i.fa.fa-check
+ | Restore deleted files
+ p.text-center(ng-controller="FreeTrialModalController")
+ a.btn.btn-success(
+ href
+ ng-class="buttonClass"
+ ng-click="startFreeTrial('history')"
+ sixpack-convert="teaser-history"
+ ) Try it for free
.message(ng-show="project.owner._id != user.id")
p #{translate("ask_proj_owner_to_upgrade_for_history")}
diff --git a/services/web/app/views/project/editor/left-menu.jade b/services/web/app/views/project/editor/left-menu.jade
index 20b0905abf..80d5c606ab 100644
--- a/services/web/app/views/project/editor/left-menu.jade
+++ b/services/web/app/views/project/editor/left-menu.jade
@@ -105,14 +105,13 @@ aside#left-menu.full-size(
ng-options="o.v as o.n for o in [{ n: 'On', v: true }, { n: 'Off', v: false }]"
)
- if (user.betaProgram)
- .form-controls
- label(for="syntaxValidation") #{translate("syntax_validation")}
- select(
- name="syntaxValidation"
- ng-model="settings.syntaxValidation"
- ng-options="o.v as o.n for o in [{ n: 'On', v: true }, { n: 'Off', v: false }]"
- )
+ .form-controls.code-check-setting
+ label(for="syntaxValidation") #{translate("syntax_validation")}
+ select(
+ name="syntaxValidation"
+ ng-model="settings.syntaxValidation"
+ ng-options="o.v as o.n for o in [{ n: 'On', v: true }, { n: 'Off', v: false }]"
+ )
.form-controls
label(for="theme") #{translate("theme")}
diff --git a/services/web/app/views/project/list/side-bar.jade b/services/web/app/views/project/list/side-bar.jade
index 3738e9a4ae..1fb0e2bda9 100644
--- a/services/web/app/views/project/list/side-bar.jade
+++ b/services/web/app/views/project/list/side-bar.jade
@@ -109,51 +109,16 @@
) #{translate("complete")}
- .row-spaced(ng-if="hasProjects && userHasNoSubscription", ng-cloak, sixpack-switch="left-menu-upgraed-rotation").text-centered
- span(sixpack-default).text-centered
- hr
- p.small #{translate("on_free_sl")}
- p
- a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")}
- p.small.text-centered
- | #{translate("or_unlock_features_bonus")}
- a(href="/user/bonus") #{translate("sharing_sl")} .
+ .row-spaced(ng-if="hasProjects && userHasNoSubscription", ng-cloak).text-centered
+ hr
+ p.small #{translate("on_free_sl")}
+ p
+ a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")}
+ p.small.text-centered
+ | #{translate("or_unlock_features_bonus")}
+ a(href="/user/bonus") #{translate("sharing_sl")} .
- span(sixpack-when="random").text-centered
- span(ng-if="randomView == 'default'")
- hr
- p.small #{translate("on_free_sl")}
- p
- a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")}
- p.small.text-centered
- | #{translate("or_unlock_features_bonus")}
- a(href="/user/bonus") #{translate("sharing_sl")} .
-
- span(ng-if="randomView == 'dropbox'")
- hr
- .card.card-thin
- p
- span Get Dropbox Sync
- p
- img(src=buildImgPath("dropbox/simple_logo.png"))
- p
- a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")}
- p.small.text-centered
- | #{translate("or_unlock_features_bonus")}
- a(href="/user/bonus") #{translate("sharing_sl")} .
-
- span(ng-if="randomView == 'github'")
- hr
- .card.card-thin
- p
- span Get Github Sync
- p
- img(src=buildImgPath("github/octocat.jpg"))
- p
- a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")}
- p.small.text-centered
- | #{translate("or_unlock_features_bonus")}
- a(href="/user/bonus") #{translate("sharing_sl")} .
+
script.
window.userHasNoSubscription = #{!!(settings.enableSubscriptions && !hasSubscription)}
diff --git a/services/web/app/views/subscriptions/dashboard.jade b/services/web/app/views/subscriptions/dashboard.jade
index 175c8d1e4f..3448dc9072 100644
--- a/services/web/app/views/subscriptions/dashboard.jade
+++ b/services/web/app/views/subscriptions/dashboard.jade
@@ -113,39 +113,28 @@ block content
div
a(href="/subscription/group").btn.btn-primary !{translate("manage_group")}
-
-
.card(ng-if="view == 'cancelation'")
.page-header
h1 #{translate("Cancel Subscription")}
- span(ng-if="sixpackOpt == 'downgrade-options'")
+ div(ng-show="showExtendFreeTrial", style="text-align: center")
+ p !{translate("have_more_days_to_try", {days:14})}
+ button(type="submit", ng-click="exendTrial()", ng-disabled='inflight').btn.btn-success #{translate("ill_take_it")}
+ p
+ |
+ p
+ a(href, ng-click="cancelSubscription()", ng-disabled='inflight') #{translate("no_thanks_cancel_now")}
- div(ng-show="showExtendFreeTrial", style="text-align: center")
- p !{translate("have_more_days_to_try", {days:14})}
- button(type="submit", ng-click="exendTrial()", ng-disabled='inflight').btn.btn-success #{translate("ill_take_it")}
+ div(ng-show="showDowngradeToStudent", style="text-align: center")
+ span(ng-controller="ChangePlanFormController")
+ p !{translate("interested_in_cheaper_plan",{price:'{{studentPrice}}'})}
+ button(type="submit", ng-click="downgradeToStudent()", ng-disabled='inflight').btn.btn-success #{translate("yes_please")}
p
|
p
- a(href, ng-click="cancelSubscription()", ng-disabled='inflight') #{translate("no_thanks_cancel_now")}
+ a(href, ng-click="cancelSubscription()", ng-disabled='inflight') #{translate("no_thanks_cancel_now")}
-
- div(ng-show="showDowngradeToStudent", style="text-align: center")
- span(ng-controller="ChangePlanFormController")
- p !{translate("interested_in_cheaper_plan",{price:'{{studentPrice}}'})}
- button(type="submit", ng-click="downgradeToStudent()", ng-disabled='inflight').btn.btn-success #{translate("yes_please")}
- p
- |
- p
- a(href, ng-click="cancelSubscription()", ng-disabled='inflight') #{translate("no_thanks_cancel_now")}
-
- div(ng-show="showBasicCancel")
- p #{translate("sure_you_want_to_cancel")}
- a(href="/project").btn.btn-info #{translate("i_want_to_stay")}
- |
- a(ng-click="cancelSubscription()", ng-disabled='inflight').btn.btn-primary #{translate("cancel_my_account")}
-
- span(ng-if="sixpackOpt == 'basic'")
+ div(ng-show="showBasicCancel")
p #{translate("sure_you_want_to_cancel")}
a(href="/project").btn.btn-info #{translate("i_want_to_stay")}
|
diff --git a/services/web/app/views/subscriptions/plans.jade b/services/web/app/views/subscriptions/plans.jade
index d87f601889..16376c7e72 100644
--- a/services/web/app/views/subscriptions/plans.jade
+++ b/services/web/app/views/subscriptions/plans.jade
@@ -100,8 +100,7 @@ block content
li
br
a.btn.btn-info(
-
- ng-href="#{baseUrl}/user/subscription/new?planCode=collaborator{{ (ui.view == 'annual' ? '-annual' : '') + (plansVariant == 'default' ? planQueryString : '_'+plansVariant)}}¤cy={{currencyCode}}", ng-click="signUpNowClicked('collaborator')"
+ ng-href="#{baseUrl}/user/subscription/new?planCode={{ getCollaboratorPlanCode() }}¤cy={{currencyCode}}", ng-click="signUpNowClicked('collaborator')"
)
span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")}
span(ng-show="ui.view == 'annual'") #{translate("buy_now")}
diff --git a/services/web/app/views/subscriptions/successful_subscription.jade b/services/web/app/views/subscriptions/successful_subscription.jade
index 17ac4f3594..184c89c0d2 100644
--- a/services/web/app/views/subscriptions/successful_subscription.jade
+++ b/services/web/app/views/subscriptions/successful_subscription.jade
@@ -10,13 +10,8 @@ block content
h2 #{translate("thanks_for_subscribing")}
.alert.alert-success
p !{translate("next_payment_of_x_collectected_on_y", {paymentAmmount:""+subscription.price+"", collectionDate:""+subscription.nextPaymentDueAt+""})}
- span(sixpack-switch="upgrade-success-message")
- span(sixpack-default)
- p #{translate("if_you_dont_want_to_be_charged")}
- a(href="/user/subscription") #{translate("click_here_to_cancel")}.
- span(sixpack-when="manage-subscription")
- p #{translate("to_modify_your_subscription_go_to")}
- a(href="/user/subscription") #{translate("manage_subscription")}.
+ p #{translate("to_modify_your_subscription_go_to")}
+ a(href="/user/subscription") #{translate("manage_subscription")}.
p
- if (subscription.groupPlan == true)
a.btn.btn-success.btn-large(href="/subscription/group") #{translate("add_your_first_group_member_now")}
diff --git a/services/web/package.json b/services/web/package.json
index cca2733643..d302cc30d8 100644
--- a/services/web/package.json
+++ b/services/web/package.json
@@ -47,6 +47,7 @@
"nodemailer-ses-transport": "^1.3.0",
"optimist": "0.6.1",
"passport": "^0.3.2",
+ "passport-ldapauth": "^0.6.0",
"passport-local": "^1.0.0",
"pg": "^6.0.3",
"pg-hstore": "^2.3.2",
diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee
index 04fda70d33..9a87d0a06b 100644
--- a/services/web/public/coffee/ide.coffee
+++ b/services/web/public/coffee/ide.coffee
@@ -11,6 +11,7 @@ define [
"ide/references/ReferencesManager"
"ide/review-panel/ReviewPanelManager"
"ide/SafariScrollPatcher"
+ "ide/FeatureOnboardingController"
"ide/settings/index"
"ide/share/index"
"ide/chat/index"
@@ -67,6 +68,7 @@ define [
chatOpen: false
pdfLayout: 'sideBySide'
reviewPanelOpen: false
+ showCodeCheckerOnboarding: !window.userSettings.syntaxValidation?
}
$scope.user = window.user
diff --git a/services/web/public/coffee/ide/FeatureOnboardingController.coffee b/services/web/public/coffee/ide/FeatureOnboardingController.coffee
new file mode 100644
index 0000000000..a5687cb42f
--- /dev/null
+++ b/services/web/public/coffee/ide/FeatureOnboardingController.coffee
@@ -0,0 +1,35 @@
+define [
+ "base"
+], (App) ->
+ App.controller "FeatureOnboardingController", ($scope, settings) ->
+ $scope.innerStep = 1
+
+ $scope.turnCodeCheckOn = () ->
+ settings.saveSettings({ syntaxValidation: true })
+ $scope.settings.syntaxValidation = true
+ navToInnerStep2()
+
+ $scope.turnCodeCheckOff = () ->
+ settings.saveSettings({ syntaxValidation: false })
+ $scope.settings.syntaxValidation = false
+ navToInnerStep2()
+
+ $scope.dismiss = () ->
+ $scope.ui.leftMenuShown = false
+ $scope.ui.showCodeCheckerOnboarding = false
+
+ navToInnerStep2 = () ->
+ $scope.innerStep = 2
+ $scope.ui.leftMenuShown = true
+
+ handleKeypress = (e) ->
+ if e.keyCode == 13
+ if $scope.innerStep == 1
+ $scope.turnCodeCheckOn()
+ else
+ $scope.dismiss()
+
+ $(document).on "keypress", handleKeypress
+
+ $scope.$on "$destroy", () ->
+ $(document).off "keypress", handleKeypress
\ No newline at end of file
diff --git a/services/web/public/coffee/ide/connection/ConnectionManager.coffee b/services/web/public/coffee/ide/connection/ConnectionManager.coffee
index 811587e5ad..61d3d1c37d 100644
--- a/services/web/public/coffee/ide/connection/ConnectionManager.coffee
+++ b/services/web/public/coffee/ide/connection/ConnectionManager.coffee
@@ -28,32 +28,46 @@ define [], () ->
@connected = false
@userIsInactive = false
@gracefullyReconnecting = false
-
- @$scope.connection =
+
+ @$scope.connection =
reconnecting: false
# If we need to force everyone to reload the editor
forced_disconnect: false
inactive_disconnect: false
@$scope.tryReconnectNow = () =>
- @tryReconnect()
+ # user manually requested reconnection via "Try now" button
+ @tryReconnectWithRateLimit({force:true})
@$scope.$on 'cursor:editor:update', () =>
- @lastUserAction = new Date()
+ @lastUserAction = new Date() # time of last edit
if !@connected
- @tryReconnect()
+ # user is editing, try to reconnect
+ @tryReconnectWithRateLimit()
document.querySelector('body').addEventListener 'click', (e) =>
if !@connected and e.target.id != 'try-reconnect-now-button'
- @tryReconnect()
+ # user is editing, try to reconnect
+ @tryReconnectWithRateLimit()
@ide.socket = io.connect null,
reconnect: false
'connect timeout': 30 * 1000
"force new connection": true
+ # The "connect" event is the first event we get back. It only
+ # indicates that the websocket is connected, we still need to
+ # pass authentication to join a project.
+
@ide.socket.on "connect", () =>
sl_console.log "[socket.io connect] Connected"
+
+ # The next event we should get is an authentication response
+ # from the server, either "connectionAccepted" or
+ # "connectionRejected".
+
+ @ide.socket.on 'connectionAccepted', (message) =>
+ sl_console.log "[socket.io connectionAccepted] allowed to connect"
@connected = true
@gracefullyReconnecting = false
@ide.pushEvent("connected")
@@ -64,16 +78,26 @@ define [], () ->
if @$scope.state.loading
@$scope.state.load_progress = 70
+ # we have passed authentication so we can now join the project
setTimeout(() =>
@joinProject()
, 100)
+ @ide.socket.on 'connectionRejected', (err) =>
+ sl_console.log "[socket.io connectionRejected] session not valid or other connection error"
+ # we have failed authentication, usually due to an invalid session cookie
+ return @reportConnectionError(err)
+
+ # Alternatively the attempt to connect can fail completely, so
+ # we never get into the "connect" state.
+
@ide.socket.on "connect_failed", () =>
@connected = false
$scope.$apply () =>
@$scope.state.error = "Unable to connect, please view the connection problems guide to fix the issue."
-
+ # We can get a "disconnect" event at any point after the
+ # "connect" event.
@ide.socket.on 'disconnect', () =>
sl_console.log "[socket.io disconnect] Disconnected"
@@ -86,6 +110,8 @@ define [], () ->
if !$scope.connection.forced_disconnect and !@userIsInactive and !@gracefullyReconnecting
@startAutoReconnectCountdown()
+ # Site administrators can send the forceDisconnect event to all users
+
@ide.socket.on 'forceDisconnect', (message) =>
@$scope.$apply () =>
@$scope.permissions.write = false
@@ -99,25 +125,33 @@ define [], () ->
setTimeout () ->
location.reload()
, 10 * 1000
-
+
@ide.socket.on "reconnectGracefully", () =>
sl_console.log "Reconnect gracefully"
@reconnectGracefully()
+ # Error reporting, which can reload the page if appropriate
+
+ reportConnectionError: (err) ->
+ sl_console.log "[socket.io] reporting connection error"
+ if err?.message == "not authorized" or err?.message == "invalid session"
+ window.location = "/login?redir=#{encodeURI(window.location.pathname)}"
+ else
+ @ide.socket.disconnect()
+ @ide.showGenericMessageModal("Something went wrong connecting", """
+ Something went wrong connecting to your project. Please refresh is this continues to happen.
+ """)
+
joinProject: () ->
sl_console.log "[joinProject] joining..."
+ # Note: if the "joinProject" message doesn't reach the server
+ # (e.g. if we are in a disconnected state at this point) the
+ # callback will never be executed
@ide.socket.emit 'joinProject', {
project_id: @ide.project_id
}, (err, project, permissionsLevel, protocolVersion) =>
if err?
- if err.message == "not authorized"
- window.location = "/login?redir=#{encodeURI(window.location.pathname)}"
- else
- @ide.socket.disconnect()
- @ide.showGenericMessageModal("Something went wrong connecting", """
- Something went wrong connecting to your project. Please refresh is this continues to happen.
- """)
- return
+ return @reportConnectionError(err)
if @$scope.protocolVersion? and @$scope.protocolVersion != protocolVersion
location.reload(true)
@@ -135,11 +169,13 @@ define [], () ->
@tryReconnect()
disconnect: () ->
+ sl_console.log "[socket.io] disconnecting client"
@ide.socket.disconnect()
startAutoReconnectCountdown: () ->
+ sl_console.log "[ConnectionManager] starting autoreconnect countdown"
twoMinutes = 2 * 60 * 1000
- if @lastUpdated? and new Date() - @lastUpdated > twoMinutes
+ if @lastUserAction? and new Date() - @lastUserAction > twoMinutes
# between 1 minute and 3 minutes
countdown = 60 + Math.floor(Math.random() * 120)
else
@@ -158,10 +194,16 @@ define [], () ->
, 200)
cancelReconnect: () ->
- clearTimeout @timeoutId if @timeoutId?
-
+ # clear timeout and set to null so we know there is no countdown running
+ if @timeoutId?
+ sl_console.log "[ConnectionManager] cancelling existing reconnect timer"
+ clearTimeout @timeoutId
+ @timeoutId = null
+
decreaseCountdown: () ->
+ @timeoutId = null
return if !@$scope.connection.reconnection_countdown?
+ sl_console.log "[ConnectionManager] decreasing countdown", @$scope.connection.reconnection_countdown
@$scope.$apply () =>
@$scope.connection.reconnection_countdown--
@@ -172,13 +214,33 @@ define [], () ->
@timeoutId = setTimeout (=> @decreaseCountdown()), 1000
tryReconnect: () ->
+ sl_console.log "[ConnectionManager] tryReconnect"
@cancelReconnect()
delete @$scope.connection.reconnection_countdown
return if @connected
@$scope.connection.reconnecting = true
- @ide.socket.socket.reconnect()
+ # use socket.io connect() here to make a single attempt, the
+ # reconnect() method makes multiple attempts
+ @ide.socket.socket.connect()
+ # record the time of the last attempt to connect
+ @lastConnectionAttempt = new Date()
setTimeout (=> @startAutoReconnectCountdown() if !@connected), 2000
+ MIN_RETRY_INTERVAL: 1000 # ms
+ BACKGROUND_RETRY_INTERVAL : 30 * 1000 # ms
+
+ tryReconnectWithRateLimit: (options) ->
+ # bail out if the reconnect is already in progress
+ return if @$scope.connection?.reconnecting
+ # bail out if we are going to reconnect soon anyway
+ reconnectingSoon = @$scope.connection?.reconnection_countdown? and @$scope.connection.reconnection_countdown <= 5
+ clickedTryNow = options?.force # user requested reconnection
+ return if reconnectingSoon and not clickedTryNow
+ # bail out if we tried reconnecting recently
+ allowedInterval = if clickedTryNow then @MIN_RETRY_INTERVAL else @BACKGROUND_RETRY_INTERVAL
+ return if @lastConnectionAttempt? and new Date() - @lastConnectionAttempt < allowedInterval
+ @tryReconnect()
+
disconnectIfInactive: ()->
@userIsInactive = (new Date() - @lastUserAction) > @disconnectAfterMs
if @userIsInactive and @connected
@@ -200,10 +262,10 @@ define [], () ->
setTimeout () =>
@reconnectGracefully()
, @RECONNECT_GRACEFULLY_RETRY_INTERVAL
-
+
_reconnectGracefullyNow: () ->
@gracefullyReconnecting = true
@reconnectGracefullyStarted = null
# Clear cookie so we don't go to the same backend server
$.cookie("SERVERID", "", { expires: -1, path: "/" })
- @reconnectImmediately()
\ No newline at end of file
+ @reconnectImmediately()
diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/undo/UndoManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/undo/UndoManager.coffee
index 40f1ba7c82..de6c273c08 100644
--- a/services/web/public/coffee/ide/editor/directives/aceEditor/undo/UndoManager.coffee
+++ b/services/web/public/coffee/ide/editor/directives/aceEditor/undo/UndoManager.coffee
@@ -237,7 +237,7 @@ define [
seen = []
return JSON.stringify o, (k,v) ->
if (typeof v == 'object')
- if ( !seen.indexOf(v) )
+ if ( seen.indexOf(v) >= 0 )
return '__cycle__'
seen.push(v);
return v
diff --git a/services/web/public/coffee/main/plans.coffee b/services/web/public/coffee/main/plans.coffee
index 538152bf43..450331a25f 100644
--- a/services/web/public/coffee/main/plans.coffee
+++ b/services/web/public/coffee/main/plans.coffee
@@ -338,6 +338,21 @@ define [
$scope.changeCurreny = (newCurrency)->
$scope.currencyCode = newCurrency
+ # because ternary logic in angular bindings is hard
+ $scope.getCollaboratorPlanCode = () ->
+ view = $scope.ui.view
+ variant = $scope.plansVariant
+ if view == "annual"
+ if variant == "default"
+ return "collaborator-annual"
+ else
+ return "collaborator-annual_#{variant}"
+ else
+ if variant == "default"
+ return "collaborator#{$scope.planQueryString}"
+ else
+ return "collaborator_#{variant}"
+
$scope.signUpNowClicked = (plan, annual)->
event_tracking.sendMB 'plans-page-start-trial', {plan}
if $scope.ui.view == "annual"
diff --git a/services/web/public/coffee/main/subscription-dashboard.coffee b/services/web/public/coffee/main/subscription-dashboard.coffee
index 71bc198e6d..27e984dc38 100644
--- a/services/web/public/coffee/main/subscription-dashboard.coffee
+++ b/services/web/public/coffee/main/subscription-dashboard.coffee
@@ -153,9 +153,7 @@ define [
$scope.inflight = true
$http.post("/user/subscription/cancel", body)
.success ->
- sixpack.convert 'cancelation-options-view', ->
- sixpack.convert 'upgrade-success-message', ->
- location.reload()
+ location.reload()
.error ->
console.log "something went wrong changing plan"
@@ -169,9 +167,7 @@ define [
)
$scope.switchToCancelationView = ->
- sixpack.participate 'cancelation-options-view', ['basic', 'downgrade-options'], (view, rawResponse)->
- $scope.view = "cancelation"
- $scope.sixpackOpt = view
+ $scope.view = "cancelation"
diff --git a/services/web/public/img/teasers/code-checker/code-checker.gif b/services/web/public/img/teasers/code-checker/code-checker.gif
new file mode 100644
index 0000000000..9da06779ba
Binary files /dev/null and b/services/web/public/img/teasers/code-checker/code-checker.gif differ
diff --git a/services/web/public/img/teasers/code-checker/code-checker.mp4 b/services/web/public/img/teasers/code-checker/code-checker.mp4
new file mode 100644
index 0000000000..116274175f
Binary files /dev/null and b/services/web/public/img/teasers/code-checker/code-checker.mp4 differ
diff --git a/services/web/public/stylesheets/app/editor.less b/services/web/public/stylesheets/app/editor.less
index f082ae332e..c621ece709 100644
--- a/services/web/public/stylesheets/app/editor.less
+++ b/services/web/public/stylesheets/app/editor.less
@@ -12,6 +12,7 @@
@import "./editor/online-users.less";
@import "./editor/hotkeys.less";
@import "./editor/review-panel.less";
+@import "./editor/feature-onboarding.less";
.full-size {
position: absolute;
diff --git a/services/web/public/stylesheets/app/editor/feature-onboarding.less b/services/web/public/stylesheets/app/editor/feature-onboarding.less
new file mode 100644
index 0000000000..fa815f1d8d
--- /dev/null
+++ b/services/web/public/stylesheets/app/editor/feature-onboarding.less
@@ -0,0 +1,78 @@
+@feat-onboard-wrapper-width: 820px;
+@feat-onboard-max-text-width: 750px;
+
+.feat-onboard {
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background-image: linear-gradient(rgba(0, 0, 0, .85), rgba(0, 0, 0, .85));
+ background-repeat: no-repeat;
+ background-position-x: 0;
+ color: #FFF;
+ text-align: center;
+ z-index: 102;
+ transition: background-position ease-in-out @left-menu-animation-duration;
+ overflow: auto;
+}
+
+.feat-onboard-step2 {
+ background-position-x: @left-menu-width;
+
+ ~ #left-menu {
+ pointer-events: none;
+
+ .code-check-setting {
+ box-shadow: 0 0 300px 0 #000;
+ }
+ }
+}
+ .feat-onboard-wrapper {
+ width: @feat-onboard-wrapper-width;
+ }
+ .feat-onboard-title {
+ color: @brand-primary;
+ margin-bottom: 40px;
+
+ }
+ .feat-onboard-title-name {
+ color: #FFF;
+ font-weight: bold;
+ }
+
+ .feat-onboard-description {
+ max-width: @feat-onboard-max-text-width;
+ margin: 0 auto 30px;
+ padding: 0 80px;
+ }
+ .feat-onboard-description-name {
+ font-weight: bold;
+ }
+
+ .feat-onboard-video {
+ box-shadow: 0 0 70px 0 rgba(255, 255, 255, 0.3);
+ }
+
+ .feat-onboard-adv-wrapper {
+ text-align: left;
+ margin-bottom: 30px;
+ }
+ .feat-onboard-adv-title {
+ color: #FFF;
+ font-size: 23px;
+ }
+ .feat-onboard-adv-title-highlight {
+ font-weight: bold;
+ }
+
+ .feat-onboard-btn-wrapper {
+ margin-bottom: 10px;
+
+ > .btn {
+ box-shadow: 0 0 70px 0 rgba(255, 255, 255, 0.3);
+ }
+ }
\ No newline at end of file
diff --git a/services/web/public/stylesheets/app/editor/left-menu.less b/services/web/public/stylesheets/app/editor/left-menu.less
index a44b7289ac..8172c44b90 100644
--- a/services/web/public/stylesheets/app/editor/left-menu.less
+++ b/services/web/public/stylesheets/app/editor/left-menu.less
@@ -1,6 +1,6 @@
#left-menu {
position: absolute;
- width: 260px;
+ width: @left-menu-width;
padding: (@line-height-computed / 2);
top: 0;
bottom: 0;
@@ -8,8 +8,8 @@
z-index: 100;
overflow-y: auto;
overflow-x: hidden;
- -webkit-transition: left ease-in-out 0.35s;
- transition: left ease-in-out 0.35s;
+ -webkit-transition: left ease-in-out @left-menu-animation-duration;
+ transition: left ease-in-out @left-menu-animation-duration;
font-size: 14px;
left: -280px;
diff --git a/services/web/public/stylesheets/core/variables.less b/services/web/public/stylesheets/core/variables.less
index c9a3fcda62..a642ab97bc 100755
--- a/services/web/public/stylesheets/core/variables.less
+++ b/services/web/public/stylesheets/core/variables.less
@@ -820,6 +820,9 @@
// Custom
+@left-menu-width: 260px;
+@left-menu-animation-duration: 0.35s;
+
@toolbar-border-color: @gray-lighter;
@file-tree-droppable-background-color: rgb(252, 231, 199);
diff --git a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee
index 3a697441cb..104d2e99c3 100644
--- a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee
@@ -157,6 +157,56 @@ describe "AuthenticationController", ->
@res.json.callCount.should.equal 1
@res.json.calledWith({message: @info}).should.equal true
+ describe 'afterLoginSessionSetup', ->
+
+ beforeEach ->
+ @req.login = sinon.stub().callsArgWith(1, null)
+ @req.session = @session = {passport: {user: @user}}
+ @req.session =
+ passport: {user: {_id: "one"}}
+ @req.session.destroy = sinon.stub()
+ @req.session.save = sinon.stub().callsArgWith(0, null)
+ @req.sessionStore = {generate: sinon.stub()}
+ @UserSessionsManager.trackSession = sinon.stub()
+ @call = (callback) =>
+ @AuthenticationController.afterLoginSessionSetup @req, @user, callback
+
+ it 'should not produce an error', (done) ->
+ @call (err) =>
+ expect(err).to.equal null
+ done()
+
+ it 'should call req.login', (done) ->
+ @call (err) =>
+ @req.login.callCount.should.equal 1
+ done()
+
+ it 'should call req.session.save', (done) ->
+ @call (err) =>
+ @req.session.save.callCount.should.equal 1
+ done()
+
+ it 'should call UserSessionsManager.trackSession', (done) ->
+ @call (err) =>
+ @UserSessionsManager.trackSession.callCount.should.equal 1
+ done()
+
+ describe 'when req.session.save produces an error', ->
+
+ beforeEach ->
+ @req.session.save = sinon.stub().callsArgWith(0, new Error('woops'))
+
+ it 'should produce an error', (done) ->
+ @call (err) =>
+ expect(err).to.not.be.oneOf [null, undefined]
+ expect(err).to.be.instanceof Error
+ done()
+
+ it 'should not call UserSessionsManager.trackSession', (done) ->
+ @call (err) =>
+ @UserSessionsManager.trackSession.callCount.should.equal 0
+ done()
+
describe 'getSessionUser', ->
it 'should get the user object from session', ->
diff --git a/services/web/test/UnitTests/coffee/User/UserCreatorTests.coffee b/services/web/test/UnitTests/coffee/User/UserCreatorTests.coffee
index c2f4ea292f..1f04d2ef3c 100644
--- a/services/web/test/UnitTests/coffee/User/UserCreatorTests.coffee
+++ b/services/web/test/UnitTests/coffee/User/UserCreatorTests.coffee
@@ -9,7 +9,7 @@ describe "UserCreator", ->
beforeEach ->
self = @
- @user = {_id:"12390i"}
+ @user = {_id:"12390i", ace: {}}
@user.save = sinon.stub().callsArgWith(0)
@UserModel = class Project
constructor: ->