diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 3e6beaa0fb..f54faa606f 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -20,6 +20,7 @@ ProjectGetter = require("./ProjectGetter") PrivilegeLevels = require("../Authorization/PrivilegeLevels") AuthenticationController = require("../Authentication/AuthenticationController") PackageVersions = require("../../infrastructure/PackageVersions") +AnalyticsManager = require "../Analytics/AnalyticsManager" module.exports = ProjectController = @@ -219,6 +220,19 @@ module.exports = ProjectController = #don't need to wait for this to complete ProjectUpdateHandler.markAsOpened project_id, -> cb() + showTrackChangesOnboarding: (cb) -> + cb = _.once(cb) + if !user_id? + return cb() + timeout = setTimeout cb, 500 + AnalyticsManager.getLastOccurance user_id, "shown-track-changes-onboarding", (error, event) -> + clearTimeout timeout + if error? + return cb(null, false) + else if event? + return cb(null, false) + else + return cb(null, true) }, (err, results)-> if err? logger.err err:err, "error getting details for project page" @@ -226,7 +240,7 @@ module.exports = ProjectController = project = results.project user = results.user subscription = results.subscription - + showTrackChangesOnboarding = results.showTrackChangesOnboarding daysSinceLastUpdated = (new Date() - project.lastUpdated) /86400000 logger.log project_id:project_id, daysSinceLastUpdated:daysSinceLastUpdated, "got db results for loading editor" @@ -268,6 +282,7 @@ module.exports = ProjectController = syntaxValidation: user.ace.syntaxValidation } trackChangesEnabled: !!project.track_changes + showTrackChangesOnboarding: !!showTrackChangesOnboarding privilegeLevel: privilegeLevel chatUrl: Settings.apis.chat.url anonymous: anonymous diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index 54c742fd15..eb8bdc684c 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -108,6 +108,7 @@ block requirejs window.anonymous = #{anonymous}; window.maxDocLength = #{maxDocLength}; window.trackChangesEnabled = #{trackChangesEnabled}; + window.showTrackChangesOnboarding = #{!!showTrackChangesOnboarding}; window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)}; window.requirejs = { "paths" : { diff --git a/services/web/app/views/project/editor/feature-onboarding.pug b/services/web/app/views/project/editor/feature-onboarding.pug index 1a8d769088..f05ed8b45a 100644 --- a/services/web/app/views/project/editor/feature-onboarding.pug +++ b/services/web/app/views/project/editor/feature-onboarding.pug @@ -1,54 +1,113 @@ -.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;") +div(ng-controller="FeatureOnboardingController") + .feat-onboard( + ng-class="('feat-onboard-step' + onboarding.innerStep)" + ng-if="!state.loading && showCollabFeaturesOnboarding" + ng-cloak + stop-propagation="click" + ) + a.feat-onboard-dismiss( + href + ng-click="dismiss();" + ) × + .feat-onboard-wrapper + h1.feat-onboard-title + span.feat-onboard-highlight Commenting + | & + span.feat-onboard-highlight Track Changes 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;") + | Collaboration features are here! 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 + span.feat-onboard-highlight Commenting + | and + span.feat-onboard-highlight Track Changes + | will make it easier for you to work with peers in your documents. + + .feat-onboard-tutorial-wrapper + button.btn.btn-primary.feat-onboard-nav-btn( + ng-click="gotoPrevStep();" + ng-disabled="onboarding.innerStep === 1;") + i.fa.fa-arrow-left + div(ng-show="onboarding.innerStep === 1;") + video.feat-onboard-video( + video-play-state="onboarding.innerStep === 1;" + autoplay + loop + ) + source(src="/img/onboarding/review-panel/open-review.mp4", type="video/mp4") + img(src="/img/onboarding/review-panel/open-review.gif") + div(ng-show="onboarding.innerStep === 2;") + video.feat-onboard-video( + video-play-state="onboarding.innerStep === 2;" + autoplay + loop + ) + source(src="/img/onboarding/review-panel/commenting.mp4", type="video/mp4") + img(src="/img/onboarding/review-panel/commenting.gif") + div(ng-show="onboarding.innerStep === 3;") + video.feat-onboard-video( + video-play-state="onboarding.innerStep === 3;" + autoplay + loop + ) + source(src="/img/onboarding/review-panel/add-changes.mp4", type="video/mp4") + img(src="/img/onboarding/review-panel/add-changes.gif") + div(ng-show="onboarding.innerStep === 4;") + video.feat-onboard-video( + video-play-state="onboarding.innerStep === 4;" + autoplay + loop + ) + source(src="/img/onboarding/review-panel/accept-changes.mp4", type="video/mp4") + img(src="/img/onboarding/review-panel/accept-changes.gif") + button.btn.btn-primary.feat-onboard-nav-btn( + ng-click="gotoNextStep();" + ng-disabled="onboarding.innerStep === onboarding.nSteps;") + i.fa.fa-arrow-right + + div(ng-switch="onboarding.innerStep") + .row(ng-switch-when="1") + .col-xs-6 + h2.feat-onboard-adv-title Commenting + p.feat-onboard-description Want to discuss specific parts of the text? + p.feat-onboard-description Use our brand-new commenting system. + .col-xs-6 + h2.feat-onboard-adv-title Track Changes + p.feat-onboard-description See changes in your documents, live. + p.feat-onboard-description Track, accept and reject changes individually. + .row(ng-switch-when="2") + .col-xs-12 + h2.feat-onboard-adv-title Commenting + p.feat-onboard-description Just select a span of text and click on + span.feat-onboard-highlight “Add comment” + | . + p.feat-onboard-description + span.feat-onboard-highlight Comments + | can be + span.feat-onboard-highlight replied + | to, + span.feat-onboard-highlight resolved + | and permanently + span.feat-onboard-highlight deleted + | . + .row(ng-switch-when="3") + .col-xs-12 + h2.feat-onboard-adv-title Track Changes + p.feat-onboard-description + | Let your peers know what you've been up to. + p.feat-onboard-description + | Click on the + span.feat-onboard-highlight “Track Changes” + | toggle to start marking your insertions, as well as your deletions. + + .row(ng-switch-when="4") + .col-xs-12 + h2.feat-onboard-adv-title Track Changes + p.feat-onboard-description Upon reviewing, + span.feat-onboard-highlight changes + | can be accepted or undone. + p.feat-onboard-description + | Click  + span.feat-onboard-highlight “Accept” + | or  + span.feat-onboard-highlight “Reject” + | to incorporate or discard an individual change. diff --git a/services/web/app/views/project/editor/review-panel.pug b/services/web/app/views/project/editor/review-panel.pug index 7b1c96b5eb..79e8a93144 100644 --- a/services/web/app/views/project/editor/review-panel.pug +++ b/services/web/app/views/project/editor/review-panel.pug @@ -6,7 +6,7 @@ ng-class="{ 'rp-track-changes-indicator-on-dark' : darkTheme }" ) Track changes is strong on - + .review-panel-toolbar resolved-comments-dropdown( class="rp-flex-block" @@ -426,4 +426,4 @@ script(type="text/ng-template", id="trackChangesUpgradeModalTemplate") button.btn.btn-default( ng-click="cancel()" ) - span #{translate("close")} \ No newline at end of file + span #{translate("close")} diff --git a/services/web/public/coffee/directives/videoPlayState.coffee b/services/web/public/coffee/directives/videoPlayState.coffee new file mode 100644 index 0000000000..5dbf470bee --- /dev/null +++ b/services/web/public/coffee/directives/videoPlayState.coffee @@ -0,0 +1,15 @@ +define [ + "base" +], (App) -> + App.directive "videoPlayState", ($parse) -> + return { + restrict: "A", + link: (scope, element, attrs) -> + videoDOMEl = element[0] + scope.$watch (() -> $parse(attrs.videoPlayState)(scope)), (shouldPlay) -> + if shouldPlay + videoDOMEl.currentTime = 0 + videoDOMEl.play() + else + videoDOMEl.pause() + } diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index 08531f993a..9bf86d576e 100644 --- a/services/web/public/coffee/ide.coffee +++ b/services/web/public/coffee/ide.coffee @@ -29,6 +29,7 @@ define [ "directives/stopPropagation" "directives/rightClick" "directives/expandableTextArea" + "directives/videoPlayState" "services/queued-http" "filters/formatDate" "main/event" @@ -69,7 +70,6 @@ define [ chatOpen: false pdfLayout: 'sideBySide' reviewPanelOpen: localStorage("ui.reviewPanelOpen.#{window.project_id}") - 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 index a5687cb42f..30db829b56 100644 --- a/services/web/public/coffee/ide/FeatureOnboardingController.coffee +++ b/services/web/public/coffee/ide/FeatureOnboardingController.coffee @@ -1,35 +1,36 @@ 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() + App.controller "FeatureOnboardingController", ($scope, settings, event_tracking) -> + $scope.onboarding = + innerStep: 1 + nSteps: 4 + + $scope.$watch "project.features.trackChangesVisible", (visible) -> + return if !visible? + $scope.showCollabFeaturesOnboarding = window.showTrackChangesOnboarding and visible $scope.dismiss = () -> - $scope.ui.leftMenuShown = false - $scope.ui.showCodeCheckerOnboarding = false + event_tracking.sendMB "shown-track-changes-onboarding" + $scope.$applyAsync(() -> $scope.showCollabFeaturesOnboarding = false) - navToInnerStep2 = () -> - $scope.innerStep = 2 - $scope.ui.leftMenuShown = true + $scope.gotoPrevStep = () -> + if $scope.onboarding.innerStep > 1 + $scope.$applyAsync(() -> $scope.onboarding.innerStep--) - handleKeypress = (e) -> - if e.keyCode == 13 - if $scope.innerStep == 1 - $scope.turnCodeCheckOn() - else - $scope.dismiss() + $scope.gotoNextStep = () -> + if $scope.onboarding.innerStep < 4 + $scope.$applyAsync(() -> $scope.onboarding.innerStep++) - $(document).on "keypress", handleKeypress + handleKeydown = (e) -> + switch e.keyCode + when 37 then $scope.gotoPrevStep() # left directional key + when 39, 13 then $scope.gotoNextStep() # right directional key, enter + when 27 then $scope.dismiss() # escape + + $(document).on "keydown", handleKeydown + $(document).on "click", $scope.dismiss $scope.$on "$destroy", () -> - $(document).off "keypress", handleKeypress \ No newline at end of file + $(document).off "keydown", handleKeydown + $(document).off "click", $scope.dismiss \ No newline at end of file diff --git a/services/web/public/img/onboarding/review-panel/accept-changes.gif b/services/web/public/img/onboarding/review-panel/accept-changes.gif new file mode 100644 index 0000000000..eca888f908 Binary files /dev/null and b/services/web/public/img/onboarding/review-panel/accept-changes.gif differ diff --git a/services/web/public/img/onboarding/review-panel/accept-changes.mp4 b/services/web/public/img/onboarding/review-panel/accept-changes.mp4 new file mode 100644 index 0000000000..7a486c9363 Binary files /dev/null and b/services/web/public/img/onboarding/review-panel/accept-changes.mp4 differ diff --git a/services/web/public/img/onboarding/review-panel/add-changes.gif b/services/web/public/img/onboarding/review-panel/add-changes.gif new file mode 100644 index 0000000000..4a46a45b6e Binary files /dev/null and b/services/web/public/img/onboarding/review-panel/add-changes.gif differ diff --git a/services/web/public/img/onboarding/review-panel/add-changes.mp4 b/services/web/public/img/onboarding/review-panel/add-changes.mp4 new file mode 100644 index 0000000000..a73fbd1eab Binary files /dev/null and b/services/web/public/img/onboarding/review-panel/add-changes.mp4 differ diff --git a/services/web/public/img/onboarding/review-panel/commenting.gif b/services/web/public/img/onboarding/review-panel/commenting.gif new file mode 100644 index 0000000000..648c7dd4c3 Binary files /dev/null and b/services/web/public/img/onboarding/review-panel/commenting.gif differ diff --git a/services/web/public/img/onboarding/review-panel/commenting.mp4 b/services/web/public/img/onboarding/review-panel/commenting.mp4 new file mode 100644 index 0000000000..9f0f50eb0d Binary files /dev/null and b/services/web/public/img/onboarding/review-panel/commenting.mp4 differ diff --git a/services/web/public/img/onboarding/review-panel/open-review.gif b/services/web/public/img/onboarding/review-panel/open-review.gif new file mode 100644 index 0000000000..603a05933d Binary files /dev/null and b/services/web/public/img/onboarding/review-panel/open-review.gif differ diff --git a/services/web/public/img/onboarding/review-panel/open-review.mp4 b/services/web/public/img/onboarding/review-panel/open-review.mp4 new file mode 100644 index 0000000000..c8288b8d41 Binary files /dev/null and b/services/web/public/img/onboarding/review-panel/open-review.mp4 differ diff --git a/services/web/public/stylesheets/app/editor/feature-onboarding.less b/services/web/public/stylesheets/app/editor/feature-onboarding.less index fa815f1d8d..7289ed4fb7 100644 --- a/services/web/public/stylesheets/app/editor/feature-onboarding.less +++ b/services/web/public/stylesheets/app/editor/feature-onboarding.less @@ -1,78 +1,101 @@ -@feat-onboard-wrapper-width: 820px; -@feat-onboard-max-text-width: 750px; +@feat-onboard-width: 900px; .feat-onboard { position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; + top: 50px; + bottom: 50px; + left: 50%; + width: @feat-onboard-width; + margin-left: -(@feat-onboard-width / 2); display: flex; justify-content: center; - align-items: center; - background-image: linear-gradient(rgba(0, 0, 0, .85), rgba(0, 0, 0, .85)); + align-items: baseline; + background-color: rgba(0, 0, 0, .85); background-repeat: no-repeat; background-position-x: 0; color: #FFF; text-align: center; + border-radius: 1em; 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; + padding: 30px 0; } - .feat-onboard-title { - color: @brand-primary; - margin-bottom: 40px; + .feat-onboard-title { + color: #FFF; + margin-bottom: 30px; } - .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; + max-width: 35em; + margin: 0 auto 5px; } - .feat-onboard-description-name { + + .feat-onboard-highlight { + font-weight: bold; + white-space: nowrap; + } + + .feat-onboard-adv-title { font-weight: bold; + white-space: nowrap; + color: #FFF; + font-size: 23px; + margin-top: 0; } - .feat-onboard-video { - box-shadow: 0 0 70px 0 rgba(255, 255, 255, 0.3); + .feat-onboard-tutorial-wrapper { + display: flex; + align-items: center; + padding: 30px 0 15px; } - - .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 { + .feat-onboard-video { + width: 616px; + margin: 0 30px; box-shadow: 0 0 70px 0 rgba(255, 255, 255, 0.3); } - } \ No newline at end of file + + .feat-onboard-nav-btn { + border-radius: 1em; + width: 2em; + height: 2em; + text-align: center; + padding: 0; + font-size: 1.3em; + line-height: 1em; + box-shadow: 0 0 70px 0 rgba(255, 255, 255, 0.3); + + &[disabled] { + opacity: 0.2; + } + + &:focus, + &:active:focus { + outline: 0; + box-shadow: 0 0 70px 0 rgba(255, 255, 255, 0.3); + } + } + +a.feat-onboard-dismiss { + position: absolute; + top: 10px; + right: 10px; + width: 1em; + height: 1em; + line-height: 1em; + font-size: 2.5em; + color: #FFF; + background-color: rgba(0,0,0, .25); + opacity: 0.7; + border-radius: 0.5em; + transition: opacity .15s ease-in-out; + + &:hover, + &:focus { + text-decoration: none; + color: #FFF; + opacity: 1; + } +} \ No newline at end of file diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 0d4c0032e8..1d1900c90c 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -391,7 +391,7 @@ border-right-width: 0; } - .rp-layout-left & { + .rp-state-current-file-mini.rp-layout-left & { &:first-child { border-bottom-left-radius: 3px; } diff --git a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee index bcb2354b55..ba92046c28 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee @@ -58,6 +58,8 @@ describe "ProjectController", -> getLoggedInUserId: sinon.stub().returns(@user._id) getSessionUser: sinon.stub().returns(@user) isUserLoggedIn: sinon.stub().returns(true) + @AnalyticsManager = + getLastOccurance: sinon.stub() @ProjectController = SandboxedModule.require modulePath, requires: "settings-sharelatex":@settings "logger-sharelatex": @@ -82,6 +84,7 @@ describe "ProjectController", -> "../ReferencesSearch/ReferencesSearchHandler": @ReferencesSearchHandler "./ProjectGetter": @ProjectGetter '../Authentication/AuthenticationController': @AuthenticationController + "../Analytics/AnalyticsManager": @AnalyticsManager @projectName = "£12321jkj9ujkljds" @req = @@ -310,9 +313,9 @@ describe "ProjectController", -> @AuthorizationManager.getPrivilegeLevelForProject.callsArgWith 2, null, "owner" @ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub() @InactiveProjectManager.reactivateProjectIfRequired.callsArgWith(1) + @AnalyticsManager.getLastOccurance.yields(null, {"mock": "event"}) @ProjectUpdateHandler.markAsOpened.callsArgWith(1) - it "should render the project/editor page", (done)-> @res.render = (pageName, opts)=> pageName.should.equal "project/editor" @@ -357,3 +360,24 @@ describe "ProjectController", -> @ProjectUpdateHandler.markAsOpened.calledWith(@project_id).should.equal true done() @ProjectController.loadEditor @req, @res + + it "should set showTrackChangesOnboarding = false if there is an event", (done) -> + @AnalyticsManager.getLastOccurance.yields(null, {"mock": "event"}) + @res.render = (pageName, opts)=> + opts.showTrackChangesOnboarding.should.equal false + done() + @ProjectController.loadEditor @req, @res + + it "should set showTrackChangesOnboarding = true if there is no event", (done) -> + @AnalyticsManager.getLastOccurance.yields(null, null) + @res.render = (pageName, opts)=> + opts.showTrackChangesOnboarding.should.equal true + done() + @ProjectController.loadEditor @req, @res + + it "should set showTrackChangesOnboarding = false if there is an error", (done) -> + @AnalyticsManager.getLastOccurance.yields(new Error("oops"), null) + @res.render = (pageName, opts)=> + opts.showTrackChangesOnboarding.should.equal false + done() + @ProjectController.loadEditor @req, @res