diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 193965c101..84fa89ea37 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -230,6 +230,24 @@ module.exports = ProjectController = return cb(null, false) else return cb(null, true) + showPerUserTCNotice: (cb) -> + cb = underscore.once(cb) + if !user_id? + return cb() + timestamp = user_id.toString().substring(0,8) + userSignupDate = new Date( parseInt( timestamp, 16 ) * 1000 ) + if userSignupDate > new Date("2017-08-09") + # Don't show for users who registered after it was released + return cb(null, false) + timeout = setTimeout cb, 500 + AnalyticsManager.getLastOccurance user_id, "shown-per-user-tc-notice", (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" @@ -237,7 +255,7 @@ module.exports = ProjectController = project = results.project user = results.user subscription = results.subscription - showTrackChangesOnboarding = results.showTrackChangesOnboarding + { showTrackChangesOnboarding, showPerUserTCNotice } = results daysSinceLastUpdated = (new Date() - project.lastUpdated) /86400000 logger.log project_id:project_id, daysSinceLastUpdated:daysSinceLastUpdated, "got db results for loading editor" @@ -279,8 +297,9 @@ module.exports = ProjectController = pdfViewer : user.ace.pdfViewer syntaxValidation: user.ace.syntaxValidation } - trackChangesEnabled: !!project.track_changes + trackChangesState: project.track_changes showTrackChangesOnboarding: !!showTrackChangesOnboarding + showPerUserTCNotice: !!showPerUserTCNotice privilegeLevel: privilegeLevel chatUrl: Settings.apis.chat.url anonymous: anonymous diff --git a/services/web/app/coffee/models/Project.coffee b/services/web/app/coffee/models/Project.coffee index 18387bdc0b..c63f9efef3 100644 --- a/services/web/app/coffee/models/Project.coffee +++ b/services/web/app/coffee/models/Project.coffee @@ -32,7 +32,7 @@ ProjectSchema = new Schema archived : { type: Boolean } deletedDocs : [DeletedDocSchema] imageName : { type: String } - track_changes : { type: Boolean } + track_changes : { type: Object } ProjectSchema.statics.getProject = (project_or_id, fields, callback)-> if project_or_id._id? diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index 7cf3e40e78..190a46b77e 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -106,7 +106,7 @@ block requirejs //- We need to do .replace(/\//g, '\\/') do that '' -> '<\/script>' //- and doesn't prematurely end the script tag. script#data(type="application/json"). - !{JSON.stringify({userSettings: userSettings, user: user}).replace(/\//g, '\\/')} + !{JSON.stringify({userSettings: userSettings, user: user, trackChangesState: trackChangesState}).replace(/\//g, '\\/')} script(type="text/javascript"). window.data = JSON.parse($("#data").text()); @@ -118,8 +118,9 @@ block requirejs window.csrfToken = "!{csrfToken}"; window.anonymous = #{anonymous}; window.maxDocLength = #{maxDocLength}; - window.trackChangesEnabled = #{trackChangesEnabled}; + window.trackChangesState = data.trackChangesState; window.showTrackChangesOnboarding = #{!!showTrackChangesOnboarding}; + window.showPerUserTCNotice = #{!!showPerUserTCNotice}; window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)}; window.requirejs = { "paths" : { diff --git a/services/web/app/views/project/editor/review-panel.pug b/services/web/app/views/project/editor/review-panel.pug index 07f6bd081e..6db177eba8 100644 --- a/services/web/app/views/project/editor/review-panel.pug +++ b/services/web/app/views/project/editor/review-panel.pug @@ -46,21 +46,40 @@ is-loading="reviewPanel.dropdown.loading" permissions="permissions" ) - span.review-panel-toolbar-label(ng-if="permissions.write") - span(ng-click="toggleTrackChanges(true)", ng-if="editor.wantTrackChanges === false") !{translate("track_changes_is_off")} - span(ng-click="toggleTrackChanges(false)", ng-if="editor.wantTrackChanges === true") !{translate("track_changes_is_on")} - review-panel-toggle( - ng-if="editor.wantTrackChanges == editor.trackChanges" - ng-model="editor.wantTrackChanges" - on-toggle="toggleTrackChanges" - disabled="!project.features.trackChanges" - on-disabled-click="openTrackChangesUpgradeModal" + span.review-panel-toolbar-label + span.review-panel-toolbar-icon-on( + ng-if="editor.wantTrackChanges === true" ) - span.review-panel-toolbar-label.review-panel-toolbar-label-disabled(ng-if="!permissions.write") - span(ng-if="editor.wantTrackChanges === false") !{translate("track_changes_is_off")} - span(ng-if="editor.wantTrackChanges === true") !{translate("track_changes_is_on")} - span.review-panel-toolbar-spinner(ng-if="editor.wantTrackChanges != editor.trackChanges") - i.fa.fa-spin.fa-spinner + i.fa.fa-circle + span(ng-click="toggleFullTCStateCollapse();") + span(ng-if="editor.wantTrackChanges === false") !{translate("track_changes_is_off")} + span(ng-if="editor.wantTrackChanges === true") !{translate("track_changes_is_on")} + span.rp-tc-state-collapse( + ng-class="{ 'rp-tc-state-collapse-on': reviewPanel.fullTCStateCollapsed }" + ) + i.fa.fa-angle-down + ul.rp-tc-state( + review-panel-collapse-height="reviewPanel.fullTCStateCollapsed" + ) + li.rp-tc-state-item.rp-tc-state-item-everyone + span.rp-tc-state-item-name !{translate("tc_everyone")} + review-panel-toggle( + ng-model="reviewPanel.trackChangesOnForEveryone" + on-toggle="toggleTrackChangesForEveryone(isOn);" + disabled="!project.features.trackChanges || !permissions.write" + ) + li.rp-tc-state-item( + ng-repeat="member in reviewPanel.formattedProjectMembers" + ) + span.rp-tc-state-item-name( + ng-class="{ 'rp-tc-state-item-name-disabled' : reviewPanel.trackChangesOnForEveryone}" + style="color: hsl({{ member.hue }}, 70%, 40%);" + ) {{ member.name }} + review-panel-toggle( + ng-model="reviewPanel.trackChangesState[member.id].value" + on-toggle="toggleTrackChangesForUser(isOn, member.id);" + disabled="reviewPanel.trackChangesOnForEveryone || !project.features.trackChanges || !permissions.write" + ) .rp-entry-list( review-panel-sorted @@ -494,7 +513,7 @@ script(type="text/ng-template", id="trackChangesUpgradeModalTemplate") .modal-body .teaser-video-container video.teaser-video(autoplay, loop) - source(src="/img/teasers/track-changes/teaser-track-changes.mp4", type="video/mp4") + source(ng-src="{{ '/img/teasers/track-changes/teaser-track-changes.mp4' }}", type="video/mp4") img(src="/img/teasers/track-changes/teaser-track-changes.gif") h4.teaser-title #{translate("see_changes_in_your_documents_live")} @@ -532,6 +551,37 @@ script(type="text/ng-template", id="trackChangesUpgradeModalTemplate") ) span #{translate("close")} +script(type="text/ng-template", id="perUserTCNoticeModalTemplate") + .modal-header + button.close( + type="button" + data-dismiss="modal" + ng-click="$close()" + ) × + h3 #{translate("per_user_tc_title")} + .modal-body + .teaser-video-container + video.teaser-video(autoplay, loop) + source(ng-src="{{ '/img/teasers/track-changes/per-user-track-changes.mp4' }}", type="video/mp4") + img(src="/img/teasers/track-changes/per-user-track-changes.gif") + h4.teaser-title #{translate("you_can_use_per_user_tc")} + .row + .col-md-8.col-md-offset-2 + ul.list-unstyled + li + i.fa.fa-check   + | #{translate("turn_tc_on_individuals")} + + li + i.fa.fa-check   + | #{translate("keep_tc_on_like_before")} + + .modal-footer() + button.btn.btn-default( + ng-click="$close()" + ) + span #{translate("close")} + script(type="text/ng-template", id="bulkActionsModalTemplate") .modal-header button.close( diff --git a/services/web/public/coffee/ide/editor/EditorManager.coffee b/services/web/public/coffee/ide/editor/EditorManager.coffee index a1d00d3180..bf752ccffb 100644 --- a/services/web/public/coffee/ide/editor/EditorManager.coffee +++ b/services/web/public/coffee/ide/editor/EditorManager.coffee @@ -37,10 +37,6 @@ define [ @$scope.$watch "editor.wantTrackChanges", (value) => return if !value? @_syncTrackChangesState(@$scope.editor.sharejs_doc) - - @$scope.$watch "project.features.trackChanges", (trackChangesFeature) => - return if !trackChangesFeature? - @$scope.editor.wantTrackChanges = window.trackChangesEnabled and trackChangesFeature autoOpenDoc: () -> open_doc_id = diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index dc75fe1a19..67cbedb3e8 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -11,7 +11,13 @@ define [ CUR_FILE : "cur_file" OVERVIEW : "overview" + $scope.UserTCSyncState = UserTCSyncState = + SYNCED : "synced" + PENDING : "pending" + $scope.reviewPanel = + trackChangesState: {} + trackChangesOnForEveryone: false entries: {} resolvedComments: {} hasEntries: false @@ -25,6 +31,8 @@ define [ commentThreads: {} resolvedThreadIds: {} rendererData: {} + formattedProjectMembers: {} + fullTCStateCollapsed: true loadingThreads: false # All selected changes. If a aggregated change (insertion + deletion) is selection, the two ids # will be present. The length of this array will differ from the count below (see explanation). @@ -32,6 +40,7 @@ define [ # A count of user-facing selected changes. An aggregated change (insertion + deletion) will count # as only one. nVisibleSelectedChanges: 0 + showPerUserTCNotice: window.showPerUserTCNotice window.addEventListener "beforeunload", () -> collapsedStates = {} @@ -59,6 +68,15 @@ define [ if !visible $scope.ui.reviewPanelOpen = false + $scope.$watch "project.members", (members) -> + $scope.reviewPanel.formattedProjectMembers = {} + if $scope.project?.owner? + $scope.reviewPanel.formattedProjectMembers[$scope.project.owner._id] = formatUser($scope.project.owner) + if $scope.project?.members? + for member in members + if member.privileges == "readAndWrite" + $scope.reviewPanel.formattedProjectMembers[member._id] = formatUser(member) + $scope.commentState = adding: false content: "" @@ -567,26 +585,83 @@ define [ $scope.gotoEntry = (doc_id, entry) -> ide.editorManager.openDocId(doc_id, { gotoOffset: entry.offset }) - - $scope.toggleTrackChanges = (value) -> + + $scope.toggleFullTCStateCollapse = () -> if $scope.project.features.trackChanges - $scope.editor.wantTrackChanges = value - $http.post "/project/#{$scope.project_id}/track_changes", {_csrf: window.csrfToken, on: value} - event_tracking.sendMB "rp-trackchanges-toggle", { value } + if $scope.reviewPanel.showPerUserTCNotice + $scope.openPerUserTCNoticeModal() + $scope.reviewPanel.fullTCStateCollapsed = !$scope.reviewPanel.fullTCStateCollapsed else $scope.openTrackChangesUpgradeModal() + _setUserTCState = (userId, newValue, isLocal = false) -> + $scope.reviewPanel.trackChangesState[userId] ?= {} + state = $scope.reviewPanel.trackChangesState[userId] + + if !state.syncState? or state.syncState == UserTCSyncState.SYNCED + state.value = newValue + state.syncState = UserTCSyncState.SYNCED + else if state.syncState == UserTCSyncState.PENDING and state.value == newValue + state.syncState = UserTCSyncState.SYNCED + else if isLocal + state.value = newValue + state.syncState = UserTCSyncState.PENDING + + if userId == ide.$scope.user.id + $scope.editor.wantTrackChanges = newValue + + _setEveryoneTCState = (newValue, isLocal = false) -> + $scope.reviewPanel.trackChangesOnForEveryone = newValue + for member in $scope.project.members + _setUserTCState(member._id, newValue, isLocal) + _setUserTCState($scope.project.owner._id, newValue, isLocal) + + applyClientTrackChangesStateToServer = () -> + if $scope.reviewPanel.trackChangesOnForEveryone + data = {on : true} + else + data = {on_for: {}} + for userId, userState of $scope.reviewPanel.trackChangesState + data.on_for[userId] = userState.value + data._csrf = window.csrfToken + $http.post "/project/#{$scope.project_id}/track_changes", data + + applyTrackChangesStateToClient = (state) -> + if typeof state is "boolean" + _setEveryoneTCState state + else + $scope.reviewPanel.trackChangesOnForEveryone = false + for member in $scope.project.members + _setUserTCState(member._id, state[member._id] ? false) + _setUserTCState($scope.project.owner._id, state[$scope.project.owner._id] ? false) + + $scope.toggleTrackChangesForEveryone = (onForEveryone) -> + _setEveryoneTCState onForEveryone, true + applyClientTrackChangesStateToServer() + + $scope.toggleTrackChangesForUser = (onForUser, userId) -> + _setUserTCState userId, onForUser, true + applyClientTrackChangesStateToServer() + + ide.socket.on "toggle-track-changes", (state) -> + $scope.$apply () -> + applyTrackChangesStateToClient(state) + $scope.toggleTrackChangesFromKbdShortcut = () -> if !$scope.project.features.trackChangesVisible return - if $scope.editor.wantTrackChanges - $scope.toggleTrackChanges false - else - $scope.toggleTrackChanges true - - ide.socket.on "toggle-track-changes", (value) -> - $scope.$apply () -> - $scope.editor.wantTrackChanges = value + $scope.toggleTrackChangesForUser !$scope.reviewPanel.trackChangesState[ide.$scope.user.id].value, ide.$scope.user.id + + _inited = false + ide.$scope.$on "project:joined", () -> + return if _inited + project = ide.$scope.project + if project.features.trackChanges + window.trackChangesState ?= false + applyTrackChangesStateToClient(window.trackChangesState) + else + applyTrackChangesStateToClient(false) + _inited = true _refreshingRangeUsers = false _refreshedForUserIds = {} @@ -680,3 +755,11 @@ define [ controller: "TrackChangesUpgradeModalController" scope: $scope.$new() } + + $scope.openPerUserTCNoticeModal = () -> + $scope.reviewPanel.showPerUserTCNotice = false + $modal.open({ + templateUrl: "perUserTCNoticeModalTemplate" + }).result.finally () -> + event_tracking.sendMB "shown-per-user-tc-notice" + diff --git a/services/web/public/coffee/ide/review-panel/directives/reviewPanelToggle.coffee b/services/web/public/coffee/ide/review-panel/directives/reviewPanelToggle.coffee index 2b5180dce6..6f2288634b 100644 --- a/services/web/public/coffee/ide/review-panel/directives/reviewPanelToggle.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/reviewPanelToggle.coffee @@ -4,20 +4,23 @@ define [ App.directive "reviewPanelToggle", () -> restrict: "E" scope: - onToggle: '=' + onToggle: '&' ngModel: '=' + valWhenUndefined: '=?' disabled: '=?' - onDisabledClick: '=?' + onDisabledClick: '&?' link: (scope) -> if !scope.disabled? scope.disabled = false scope.onChange = (args...) -> - scope.onToggle(scope.localModel) + scope.onToggle({ isOn: scope.localModel }) scope.handleClick = () -> - if scope.disabled + if scope.disabled and scope.onDisabledClick? scope.onDisabledClick() scope.localModel = scope.ngModel scope.$watch "ngModel", (value) -> + if scope.valWhenUndefined? and !value? + value = scope.valWhenUndefined scope.localModel = value template: """ diff --git a/services/web/public/img/teasers/track-changes/per-user-track-changes.gif b/services/web/public/img/teasers/track-changes/per-user-track-changes.gif new file mode 100644 index 0000000000..ca7c636642 Binary files /dev/null and b/services/web/public/img/teasers/track-changes/per-user-track-changes.gif differ diff --git a/services/web/public/img/teasers/track-changes/per-user-track-changes.mp4 b/services/web/public/img/teasers/track-changes/per-user-track-changes.mp4 new file mode 100644 index 0000000000..9e4d0cbe29 Binary files /dev/null and b/services/web/public/img/teasers/track-changes/per-user-track-changes.mp4 differ diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 312b933b85..13b8b7e03f 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -57,6 +57,15 @@ } } +.rp-collapse-arrow() { + display: inline-block; + transform: rotateZ(0deg); + transition: transform 0.15s ease; + &-on { + transform: rotateZ(-90deg); + } +} + .triangle(@_, @width, @height, @color) { position: absolute; border-color: transparent; @@ -131,7 +140,6 @@ } position: relative; - height: @rp-toolbar-height; border-bottom: 1px solid @rp-border-grey; background-color: @rp-bg-dim-blue; text-align: center; @@ -144,6 +152,10 @@ text-align: right; flex-grow: 1; } + .review-panel-toolbar-icon-on { + margin-right: 5px; + color: @red; + } .review-panel-toolbar-label-disabled { cursor: auto; margin-right: 5px; @@ -151,6 +163,45 @@ .review-panel-toolbar-spinner { margin-left: 5px; } + .rp-tc-state { + position: absolute; + top: 100%; + left: 0; + right: 0; + overflow: hidden; + list-style: none; + padding: 0 5px; + margin: 0; + border-bottom: 1px solid @rp-border-grey; + background-color: @rp-bg-dim-blue; + text-align: left; + } + .rp-tc-state-collapse { + .rp-collapse-arrow; + margin-left: 5px; + } + .rp-tc-state-item { + display: flex; + align-items: center; + padding: 3px 0; + &:last-of-type { + padding-bottom: 5px; + } + } + .rp-tc-state-item-everyone { + border-bottom: 1px solid @rp-border-grey; + color: @red; + } + .rp-tc-state-item-name { + flex-grow: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: @rp-semibold-weight; + } + .rp-tc-state-item-name-disabled { + opacity: .35; + } .rp-entry-list { display: none; @@ -629,16 +680,9 @@ font-size: 0.9em; } .rp-overview-file-header-collapse { - display: inline-block; + .rp-collapse-arrow; float: left; - transform: rotateZ(0deg); - transition: transform 0.15s ease } - - .rp-overview-file-header-collapse-on { - transform: rotateZ(-90deg); - } - .rp-overview-file-entries { overflow: hidden; } @@ -771,6 +815,11 @@ background-color: #FFF; } } + + &:disabled + .rp-toggle-btn { + cursor: default; + opacity: .35; + } } .ace-editor-wrapper {