Просмотр исходного кода

Merge pull request #547 from sharelatex/ja-per-user-track-changes

Per-user track changes
James Allen 9 лет назад
Родитель
Сommit
c928935865

+ 21 - 2
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

+ 1 - 1
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?

+ 3 - 2
services/web/app/views/project/editor.pug

@@ -106,7 +106,7 @@ block requirejs
 	//- We need to do .replace(/\//g, '\\/') do that '</script>' -> '<\/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" : {

+ 65 - 15
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()"
+		) &times;
+		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 &nbsp;
+						| #{translate("turn_tc_on_individuals")}
+					
+					li
+						i.fa.fa-check &nbsp;
+						| #{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(

+ 0 - 4
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 = 

+ 96 - 13
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"
+

+ 7 - 4
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: """

BIN
services/web/public/img/teasers/track-changes/per-user-track-changes.gif


BIN
services/web/public/img/teasers/track-changes/per-user-track-changes.mp4


+ 58 - 9
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 {