Explorar o código

Merge remote-tracking branch 'origin' into afc-email-tokens

Alberto Fernández Capel %!s(int64=8) %!d(string=hai) anos
pai
achega
6fb6119ca8
Modificáronse 51 ficheiros con 2388 adicións e 446 borrados
  1. 3 0
      services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
  2. 133 0
      services/web/app/coffee/Features/Subscription/planFeatures.coffee
  3. 1 0
      services/web/app/coffee/router.coffee
  4. 4 1
      services/web/app/views/project/editor.pug
  5. 0 17
      services/web/app/views/project/editor/history-file-tree.pug
  6. 3 82
      services/web/app/views/project/editor/history.pug
  7. 1 1
      services/web/app/views/project/editor/history/diffPanelV1.pug
  8. 82 0
      services/web/app/views/project/editor/history/entriesListV1.pug
  9. 174 0
      services/web/app/views/project/editor/history/entriesListV2.pug
  10. 70 0
      services/web/app/views/project/editor/history/fileTreeV2.pug
  11. 34 3
      services/web/app/views/project/editor/history/previewPanelV2.pug
  12. 13 0
      services/web/app/views/project/editor/history/toolbarV2.pug
  13. 118 0
      services/web/app/views/subscriptions/_plans_page_details_less.pug
  14. 160 0
      services/web/app/views/subscriptions/_plans_page_details_more.pug
  15. 162 0
      services/web/app/views/subscriptions/_plans_page_mixins.pug
  16. 107 0
      services/web/app/views/subscriptions/_plans_page_tables.pug
  17. 4 1
      services/web/app/views/subscriptions/new.pug
  18. 8 243
      services/web/app/views/subscriptions/plans.pug
  19. 39 5
      services/web/public/coffee/directives/eventTracking.coffee
  20. 5 0
      services/web/public/coffee/ide/file-tree/FileTreeManager.coffee
  21. 3 13
      services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee
  22. 13 0
      services/web/public/coffee/ide/file-tree/util/iconTypeFromName.coffee
  23. 0 1
      services/web/public/coffee/ide/history/HistoryManager.coffee
  24. 86 10
      services/web/public/coffee/ide/history/HistoryV2Manager.coffee
  25. 19 0
      services/web/public/coffee/ide/history/components/historyEntriesList.coffee
  26. 27 0
      services/web/public/coffee/ide/history/components/historyEntry.coffee
  27. 34 0
      services/web/public/coffee/ide/history/components/historyFileEntity.coffee
  28. 18 0
      services/web/public/coffee/ide/history/components/historyFileTree.coffee
  29. 1 1
      services/web/public/coffee/ide/history/controllers/HistoryListController.coffee
  30. 54 0
      services/web/public/coffee/ide/history/controllers/HistoryV2FileTreeController.coffee
  31. 76 0
      services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee
  32. 4 0
      services/web/public/coffee/ide/history/util/HistoryViewModes.coffee
  33. 7 0
      services/web/public/coffee/main/event.coffee
  34. 4 0
      services/web/public/coffee/main/new-subscription.coffee
  35. 40 29
      services/web/public/coffee/main/plans.coffee
  36. 1 1
      services/web/public/coffee/main/project-list/project-list.coffee
  37. BIN=BIN
      services/web/public/img/advocates/erdogmus.jpg
  38. BIN=BIN
      services/web/public/img/advocates/henderson.jpg
  39. 1 0
      services/web/public/stylesheets/_style_includes.less
  40. 5 1
      services/web/public/stylesheets/app/editor.less
  41. 498 0
      services/web/public/stylesheets/app/editor/history-v2.less
  42. 13 8
      services/web/public/stylesheets/app/editor/history.less
  43. 264 1
      services/web/public/stylesheets/app/plans.less
  44. 13 1
      services/web/public/stylesheets/core/_common-variables.less
  45. 11 0
      services/web/public/stylesheets/core/ol-variables.less
  46. 3 0
      services/web/public/stylesheets/core/scaffolding.less
  47. 13 1
      services/web/public/stylesheets/core/type.less
  48. 1 1
      services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee
  49. 1 1
      services/web/test/unit/coffee/History/RestoreManagerTests.coffee
  50. 5 6
      services/web/test/unit/coffee/Subscription/RecurlyWrapperTests.coffee
  51. 52 18
      services/web/test/unit/coffee/Subscription/SubscriptionControllerTests.coffee

+ 3 - 0
services/web/app/coffee/Features/Subscription/SubscriptionController.coffee

@@ -10,6 +10,7 @@ GeoIpLookup = require("../../infrastructure/GeoIpLookup")
 SubscriptionDomainHandler = require("./SubscriptionDomainHandler")
 UserGetter = require "../User/UserGetter"
 FeaturesUpdater = require './FeaturesUpdater'
+planFeatures = require './planFeatures'
 
 module.exports = SubscriptionController =
 
@@ -20,6 +21,7 @@ module.exports = SubscriptionController =
 			viewName = "#{viewName}_#{req.query.v}"
 		logger.log viewName:viewName, "showing plans page"
 		currentUser = null
+
 		GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency)->
 			return next(err) if err?
 			render = () ->
@@ -29,6 +31,7 @@ module.exports = SubscriptionController =
 					gaExperiments: Settings.gaExperiments.plansPage
 					recomendedCurrency:recomendedCurrency
 					shouldABTestPlans: currentUser == null or (currentUser?.signUpDate? and currentUser.signUpDate >= (new Date('2016-10-27')))
+					planFeatures: planFeatures
 			user_id = AuthenticationController.getLoggedInUserId(req)
 			if user_id?
 				UserGetter.getUser user_id, {signUpDate: 1}, (err, user) ->

+ 133 - 0
services/web/app/coffee/Features/Subscription/planFeatures.coffee

@@ -0,0 +1,133 @@
+module.exports =
+	[
+		{
+			feature: 'number_collab'
+			value: 'str'
+			plans: {
+				free: '1'
+				coll: '10'
+				prof: 'unlimited'
+			}
+			student: '6'
+		}
+		{
+			feature: 'unlimited_private'
+			value: 'bool'
+			info: 'unlimited_private_info'
+			plans: {
+				free: true
+				coll: true
+				prof: true
+			},
+			student: true
+		}
+		{
+			feature: 'realtime_collab'
+			value: 'bool'
+			info: 'realtime_collab_info'
+			plans: {
+				free: true
+				coll: true
+				prof: true
+			}
+			student: true
+		}
+		{
+			feature: 'hundreds_templates'
+			value: 'bool'
+			info: 'hundreds_templates_info'
+			plans: {
+				free: true
+				coll: true
+				prof: true
+			}
+			student: true
+		}
+		{
+			feature: 'powerful_latex_editor'
+			value: 'bool'
+			info: 'latex_editor_info'
+			plans: {
+				free: true
+				coll: true
+				prof: true
+			}
+			student: true
+		}
+		{
+			feature: 'realtime_track_changes'
+			value: 'bool'
+			info: 'realtime_track_changes_info'
+			plans: {
+				free: false
+				coll: true
+				prof: true
+			},
+			student: true
+		}
+		{
+			feature: 'reference_search'
+			value: 'bool'
+			info: 'reference_search_info'
+			plans: {
+				free: false
+				coll: true
+				prof: true
+			},
+			student: true
+		},
+		{
+			feature: 'reference_sync'
+			info: 'reference_sync_info'
+			value: 'bool'
+			plans: {
+				free: false
+				coll: true
+				prof: true
+			},
+			student: true
+		}
+		{
+			feature: 'full_doc_history'
+			value: 'bool'
+			info: 'full_doc_history_info'
+			plans: {
+				free: false,
+				coll: true,
+				prof: true
+			},
+			student: true
+		}
+		{
+			feature: 'dropbox_integration_lowercase'
+			value: 'bool'
+			info: 'dropbox_integration_info'
+			plans: {
+				free: false,
+				coll: true,
+				prof: true
+			},
+			student: true
+		},
+		{
+			feature: 'github_integration_lowercase'
+			value: 'bool'
+			info: 'github_integration_info'
+			plans: {
+				free: false,
+				coll: true,
+				prof: true
+			},
+			student: true
+		},
+		{
+			feature: 'priority_support',
+			value: 'bool',
+			plans: {
+				free: false,
+				coll: true,
+				prof: true
+			},
+			student: true
+		},
+	]

+ 1 - 0
services/web/app/coffee/router.coffee

@@ -207,6 +207,7 @@ module.exports = class Router
 		webRouter.get  "/project/:Project_id/updates", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApiAndInjectUserDetails
 		webRouter.get  "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
 		webRouter.get  "/project/:Project_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApiAndInjectUserDetails
+		webRouter.get  "/project/:Project_id/filetree/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
 		webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
 		webRouter.post '/project/:project_id/doc/:doc_id/restore', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreDocFromDeletedDoc
 		webRouter.post "/project/:project_id/restore_file", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreFileFromV2

+ 4 - 1
services/web/app/views/project/editor.pug

@@ -57,9 +57,12 @@ block content
 				include ./editor/share
 				!= moduleIncludes("publish:body", locals)
 
+				include ./editor/history/toolbarV2.pug
+
 				main#ide-body(
 					ng-cloak,
 					role="main",
+					ng-class="{ 'ide-history-open' : (ui.view == 'history' && history.isV2 && history.viewMode === HistoryViewModes.POINT_IN_TIME) }",
 					layout="main",
 					ng-hide="state.loading",
 					resize-on="layout:chat:resize",
@@ -70,7 +73,7 @@ block content
 				)
 					.ui-layout-west
 						include ./editor/file-tree
-						include ./editor/history-file-tree
+						include ./editor/history/fileTreeV2
 
 					.ui-layout-center
 						include ./editor/editor

+ 0 - 17
services/web/app/views/project/editor/history-file-tree.pug

@@ -1,17 +0,0 @@
-aside.file-tree.file-tree-history(ng-controller="FileTreeController", ng-class="{ 'multi-selected': multiSelectedCount > 0 }", ng-show="ui.view == 'history' && history.isV2").full-size
-	.toolbar.toolbar-filetree
-		span Modified files
-
-	.file-tree-inner
-		ul.list-unstyled.file-tree-list
-			li(
-				ng-repeat="(pathname, doc) in history.selection.docs"
-				ng-class="{ 'selected': history.selection.pathname == pathname }"
-			)
-				.entity
-					.entity-name.entity-name-history(
-						ng-click="history.selection.pathname = pathname",
-						ng-class="{ 'deleted': !!doc.deletedAtV }"
-					)
-						i.fa.fa-fw.fa-pencil
-						span  {{ pathname }}

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

@@ -40,90 +40,11 @@ div#history(ng-show="ui.view == 'history'")
 				p
 					a.small(href, ng-click="toggleHistory()") #{translate("cancel")}
 
-	aside.change-list(
-		ng-controller="HistoryListController"
-		infinite-scroll="loadMore()"
-		infinite-scroll-disabled="history.loading || history.atEnd"
-		infinite-scroll-initialize="ui.view == 'history'"
-	)
-		.infinite-scroll-inner
-			ul.list-unstyled(
-				ng-class="{\
-					'hover-state': history.hoveringOverListSelectors\
-				}"
-			)
-				li.change(
-					ng-repeat="update in history.updates"
-					ng-class="{\
-						'first-in-day': update.meta.first_in_day,\
-						'selected': update.inSelection,\
-						'selected-to': update.selectedTo,\
-						'selected-from': update.selectedFrom,\
-						'hover-selected': update.inHoverSelection,\
-						'hover-selected-to': update.hoverSelectedTo,\
-						'hover-selected-from': update.hoverSelectedFrom,\
-					}"
-					ng-controller="HistoryListItemController"
-				)
-
-					div.day(ng-show="update.meta.first_in_day") {{ update.meta.end_ts | relativeDate }}
-
-					div.selectors
-						div.range
-						form
-							input.selector-from(
-								type="radio"
-								name="fromVersion"
-								ng-model="update.selectedFrom"
-								ng-value="true"
-								ng-mouseover="mouseOverSelectedFrom()"
-								ng-mouseout="mouseOutSelectedFrom()"
-								ng-show="update.afterSelection || update.inSelection"
-							)
-						form
-							input.selector-to(
-								type="radio"
-								name="toVersion"
-								ng-model="update.selectedTo"
-								ng-value="true"
-								ng-mouseover="mouseOverSelectedTo()"
-								ng-mouseout="mouseOutSelectedTo()"
-								ng-show="update.beforeSelection || update.inSelection"
-							)
-
-					div.description(ng-click="select()")
-						div.time {{ update.meta.end_ts | formatDate:'h:mm a' }}
-						div.action.action-edited(ng-if="history.isV2 && update.pathnames.length > 0")
-							| Edited
-						div.docs(ng-repeat="pathname in update.pathnames")
-							.doc {{ pathname }}
-						div.docs(ng-repeat="project_op in update.project_ops")
-							div(ng-if="project_op.rename")
-								.action Renamed
-								.doc {{ project_op.rename.pathname }} → {{ project_op.rename.newPathname }}
-							div(ng-if="project_op.add")
-								.action Created
-								.doc {{ project_op.add.pathname }}
-							div(ng-if="project_op.remove")
-								.action Deleted
-								.doc {{ project_op.remove.pathname }}
-						div.users
-							div.user(ng-repeat="update_user in update.meta.users")
-								.color-square(ng-if="update_user != null", ng-style="{'background-color': 'hsl({{ update_user.hue }}, 70%, 50%)'}")
-								.color-square(ng-if="update_user == null", ng-style="{'background-color': 'hsl(100, 70%, 50%)'}")
-								.name(ng-if="update_user && update_user.id != user.id" ng-bind="displayName(update_user)") 
-								.name(ng-if="update_user && update_user.id == user.id") You
-								.name(ng-if="update_user == null") #{translate("anonymous")}
-							div.user(ng-if="update.meta.users.length == 0")
-								.color-square(style="background-color: hsl(100, 100%, 50%)")
-								span #{translate("anonymous")}
-
-			.loading(ng-show="history.loading")
-				i.fa.fa-spin.fa-refresh
-				|    #{translate("loading")}...
+	include ./history/entriesListV1
+	include ./history/entriesListV2
 
 	include ./history/diffPanelV1
-	include ./history/diffPanelV2
+	include ./history/previewPanelV2
 
 script(type="text/ng-template", id="historyRestoreDiffModalTemplate")
 	.modal-header

+ 1 - 1
services/web/app/views/project/editor/history/diffPanelV1.pug

@@ -14,7 +14,7 @@
 				)
 				|  in <strong>{{history.diff.pathname}}</strong>
 			.toolbar-right(ng-if="permissions.write")
-				a.btn.btn-danger.btn-sm(
+				a.btn.btn-danger.btn-xs(
 					href,
 					ng-click="openRestoreDiffModal()"
 				) #{translate("restore_to_before_these_changes")}

+ 82 - 0
services/web/app/views/project/editor/history/entriesListV1.pug

@@ -0,0 +1,82 @@
+aside.change-list(
+		ng-if="!history.isV2"
+		ng-controller="HistoryListController"
+		infinite-scroll="loadMore()"
+		infinite-scroll-disabled="history.loading || history.atEnd"
+		infinite-scroll-initialize="ui.view == 'history'"
+	)
+		.infinite-scroll-inner
+			ul.list-unstyled(
+				ng-class="{\
+					'hover-state': history.hoveringOverListSelectors\
+				}"
+			)
+				li.change(
+					ng-repeat="update in history.updates"
+					ng-class="{\
+						'first-in-day': update.meta.first_in_day,\
+						'selected': update.inSelection,\
+						'selected-to': update.selectedTo,\
+						'selected-from': update.selectedFrom,\
+						'hover-selected': update.inHoverSelection,\
+						'hover-selected-to': update.hoverSelectedTo,\
+						'hover-selected-from': update.hoverSelectedFrom,\
+					}"
+					ng-controller="HistoryListItemController"
+				)
+
+					div.day(ng-show="update.meta.first_in_day") {{ update.meta.end_ts | relativeDate }}
+
+					div.selectors
+						div.range
+						form
+							input.selector-from(
+								type="radio"
+								name="fromVersion"
+								ng-model="update.selectedFrom"
+								ng-value="true"
+								ng-mouseover="mouseOverSelectedFrom()"
+								ng-mouseout="mouseOutSelectedFrom()"
+								ng-show="update.afterSelection || update.inSelection"
+							)
+						form
+							input.selector-to(
+								type="radio"
+								name="toVersion"
+								ng-model="update.selectedTo"
+								ng-value="true"
+								ng-mouseover="mouseOverSelectedTo()"
+								ng-mouseout="mouseOutSelectedTo()"
+								ng-show="update.beforeSelection || update.inSelection"
+							)
+
+					div.description(ng-click="select()")
+						div.time {{ update.meta.end_ts | formatDate:'h:mm a' }}
+						div.action.action-edited(ng-if="history.isV2 && update.pathnames.length > 0")
+							| #{translate("file_action_edited")}
+						div.docs(ng-repeat="pathname in update.pathnames")
+							.doc {{ pathname }}
+						div.docs(ng-repeat="project_op in update.project_ops")
+							div(ng-if="project_op.rename")
+								.action #{translate("file_action_renamed")}
+								.doc {{ project_op.rename.pathname }} &rarr; {{ project_op.rename.newPathname }}
+							div(ng-if="project_op.add")
+								.action #{translate("file_action_created")} 
+								.doc {{ project_op.add.pathname }}
+							div(ng-if="project_op.remove")
+								.action #{translate("file_action_deleted")}
+								.doc {{ project_op.remove.pathname }}
+						div.users
+							div.user(ng-repeat="update_user in update.meta.users")
+								.color-square(ng-if="update_user != null", ng-style="{'background-color': 'hsl({{ update_user.hue }}, 70%, 50%)'}")
+								.color-square(ng-if="update_user == null", ng-style="{'background-color': 'hsl(100, 70%, 50%)'}")
+								.name(ng-if="update_user && update_user.id != user.id" ng-bind="displayName(update_user)") 
+								.name(ng-if="update_user && update_user.id == user.id") You
+								.name(ng-if="update_user == null") #{translate("anonymous")}
+							div.user(ng-if="update.meta.users.length == 0")
+								.color-square(style="background-color: hsl(100, 100%, 50%)")
+								span #{translate("anonymous")}
+
+			.loading(ng-show="history.loading")
+				i.fa.fa-spin.fa-refresh
+				| &nbsp;&nbsp; #{translate("loading")}...

+ 174 - 0
services/web/app/views/project/editor/history/entriesListV2.pug

@@ -0,0 +1,174 @@
+aside.change-list(
+	ng-if="history.isV2 && history.viewMode === HistoryViewModes.POINT_IN_TIME"
+	ng-controller="HistoryV2ListController"
+)
+	history-entries-list(
+		entries="history.updates"
+		current-user="user"
+		load-entries="loadMore()"
+		load-disabled="history.loading || history.atEnd"
+		load-initialize="ui.view == 'history'"
+		is-loading="history.loading"
+		on-entry-select="handleEntrySelect(selectedEntry)"
+	)
+
+aside.change-list(
+	ng-if="history.isV2 && history.viewMode === HistoryViewModes.COMPARE"
+	ng-controller="HistoryListController"
+	infinite-scroll="loadMore()"
+	infinite-scroll-disabled="history.loading || history.atEnd"
+	infinite-scroll-initialize="ui.view == 'history'"
+)
+	.infinite-scroll-inner
+		ul.list-unstyled(
+			ng-class="{\
+				'hover-state': history.hoveringOverListSelectors\
+			}"
+		)
+			li.change(
+				ng-repeat="update in history.updates"
+				ng-class="{\
+					'first-in-day': update.meta.first_in_day,\
+					'selected': update.inSelection,\
+					'selected-to': update.selectedTo,\
+					'selected-from': update.selectedFrom,\
+					'hover-selected': update.inHoverSelection,\
+					'hover-selected-to': update.hoverSelectedTo,\
+					'hover-selected-from': update.hoverSelectedFrom,\
+				}"
+				ng-controller="HistoryListItemController"
+			)
+
+				div.day(ng-show="update.meta.first_in_day") {{ update.meta.end_ts | relativeDate }}
+
+				div.selectors
+					div.range
+					form
+						input.selector-from(
+							type="radio"
+							name="fromVersion"
+							ng-model="update.selectedFrom"
+							ng-value="true"
+							ng-mouseover="mouseOverSelectedFrom()"
+							ng-mouseout="mouseOutSelectedFrom()"
+							ng-show="update.afterSelection || update.inSelection"
+						)
+					form
+						input.selector-to(
+							type="radio"
+							name="toVersion"
+							ng-model="update.selectedTo"
+							ng-value="true"
+							ng-mouseover="mouseOverSelectedTo()"
+							ng-mouseout="mouseOutSelectedTo()"
+							ng-show="update.beforeSelection || update.inSelection"
+						)
+
+				div.description(ng-click="select()")
+					div.time {{ update.meta.end_ts | formatDate:'h:mm a' }}
+					div.action.action-edited(ng-if="history.isV2 && update.pathnames.length > 0")
+						| #{translate("file_action_edited")}
+					div.docs(ng-repeat="pathname in update.pathnames")
+						.doc {{ pathname }}
+					div.docs(ng-repeat="project_op in update.project_ops")
+						div(ng-if="project_op.rename")
+							.action #{translate("file_action_renamed")} 
+							.doc {{ project_op.rename.pathname }} &rarr; {{ project_op.rename.newPathname }}
+						div(ng-if="project_op.add")
+							.action #{translate("file_action_created")}
+							.doc {{ project_op.add.pathname }}
+						div(ng-if="project_op.remove")
+							.action #{translate("file_action_deleted")}
+							.doc {{ project_op.remove.pathname }}
+					div.users
+						div.user(ng-repeat="update_user in update.meta.users")
+							.color-square(ng-if="update_user != null", ng-style="{'background-color': 'hsl({{ update_user.hue }}, 70%, 50%)'}")
+							.color-square(ng-if="update_user == null", ng-style="{'background-color': 'hsl(100, 70%, 50%)'}")
+							.name(ng-if="update_user && update_user.id != user.id" ng-bind="displayName(update_user)") 
+							.name(ng-if="update_user && update_user.id == user.id") You
+							.name(ng-if="update_user == null") #{translate("anonymous")}
+						div.user(ng-if="update.meta.users.length == 0")
+							.color-square(style="background-color: hsl(100, 100%, 50%)")
+							span #{translate("anonymous")}
+
+		.loading(ng-show="history.loading")
+			i.fa.fa-spin.fa-refresh
+			| &nbsp;&nbsp; #{translate("loading")}...
+			
+script(type="text/ng-template", id="historyEntriesListTpl")
+	.history-entries(
+		infinite-scroll="$ctrl.loadEntries()"
+		infinite-scroll-disabled="$ctrl.loadDisabled"
+		infinite-scroll-initialize="$ctrl.loadInitialize"
+	)
+		.infinite-scroll-inner
+			history-entry(
+				ng-repeat="entry in $ctrl.entries"
+				entry="entry"
+				current-user="$ctrl.currentUser"
+				on-select="$ctrl.onEntrySelect({ selectedEntry: selectedEntry })"
+				ng-show="!$ctrl.isLoading"
+			)
+			.loading(ng-show="$ctrl.isLoading")
+				i.fa.fa-spin.fa-refresh
+				| &nbsp;&nbsp; #{translate("loading")}...
+
+script(type="text/ng-template", id="historyEntryTpl")
+	.history-entry(
+		ng-class="{\
+			'history-entry-first-in-day': $ctrl.entry.meta.first_in_day,\
+			'history-entry-selected': $ctrl.entry.inSelection,\
+			'history-entry-selected-to': $ctrl.entry.selectedTo,\
+			'history-entry-selected-from': $ctrl.entry.selectedFrom,\
+			'history-entry-hover-selected': $ctrl.entry.inHoverSelection,\
+			'history-entry-hover-selected-to': $ctrl.entry.hoverSelectedTo,\
+			'history-entry-hover-selected-from': $ctrl.entry.hoverSelectedFrom,\
+		}"
+	)
+
+		time.history-entry-day(ng-if="::$ctrl.entry.meta.first_in_day") {{ ::$ctrl.entry.meta.end_ts | relativeDate }}
+
+		.history-entry-details(ng-click="$ctrl.onSelect({ selectedEntry: $ctrl.entry })")
+			ol.history-entry-changes
+				li.history-entry-change(
+					ng-repeat="pathname in ::$ctrl.entry.pathnames"
+				)
+					span.history-entry-change-action #{translate("file_action_edited")} 
+					span.history-entry-change-doc {{ ::pathname }}
+				li.history-entry-change(
+					ng-repeat="project_op in ::$ctrl.entry.project_ops"
+				)
+					span.history-entry-change-action(
+						ng-if="::project_op.rename"
+					) #{translate("file_action_renamed")}
+					span.history-entry-change-action(
+						ng-if="::project_op.add"
+					) #{translate("file_action_created")}
+					span.history-entry-change-action(
+						ng-if="::project_op.remove"
+					) #{translate("file_action_deleted")}
+					span.history-entry-change-doc {{ ::$ctrl.getProjectOpDoc(project_op) }}
+			.history-entry-metadata
+				time.history-entry-metadata-time {{ ::$ctrl.entry.meta.end_ts | formatDate:'h:mm a' }}
+				span
+					|
+					| &bull;
+					|
+				ol.history-entry-metadata-users
+					li.history-entry-metadata-user(ng-repeat="update_user in ::$ctrl.entry.meta.users")
+						span.name(
+							ng-if="::update_user && update_user.id != $ctrl.currentUser.id"
+							ng-style="$ctrl.getUserCSSStyle(update_user);"
+						) {{ ::$ctrl.displayName(update_user) }} 
+						span.name(
+							ng-if="::update_user && update_user.id == $ctrl.currentUser.id"
+							ng-style="$ctrl.getUserCSSStyle(update_user);"
+						) You
+						span.name(
+							ng-if="::update_user == null"
+							ng-style="$ctrl.getUserCSSStyle(update_user);"
+						) #{translate("anonymous")}
+					li.history-entry-metadata-user(ng-if="::$ctrl.entry.meta.users.length == 0")
+						span.name(
+							ng-style="$ctrl.getUserCSSStyle();"
+						) #{translate("anonymous")}

+ 70 - 0
services/web/app/views/project/editor/history/fileTreeV2.pug

@@ -0,0 +1,70 @@
+aside.file-tree.full-size(
+	ng-controller="HistoryV2FileTreeController"
+	ng-if="ui.view == 'history' && history.isV2 && history.viewMode === HistoryViewModes.POINT_IN_TIME"
+)
+	.history-file-tree-inner
+		history-file-tree(
+			file-tree="currentFileTree"
+			selected-pathname="history.selection.pathname"
+			on-selected-file-change="handleFileSelection(file)"
+			is-loading="history.loadingFileTree"
+		)
+
+aside.file-tree.file-tree-history.full-size(
+	ng-controller="FileTreeController"
+	ng-class="{ 'multi-selected': multiSelectedCount > 0 }"
+	ng-show="ui.view == 'history' && history.isV2 && history.viewMode === HistoryViewModes.COMPARE")
+	.toolbar.toolbar-filetree
+		span Modified files
+
+	.file-tree-inner
+		ul.list-unstyled.file-tree-list
+			li(
+				ng-repeat="(pathname, doc) in history.selection.docs"
+				ng-class="{ 'selected': history.selection.pathname == pathname }"
+			)
+				.entity
+					.entity-name.entity-name-history(
+						ng-click="history.selection.pathname = pathname",
+						ng-class="{ 'deleted': !!doc.deletedAtV }"
+					)
+						i.fa.fa-fw.fa-pencil
+						span  {{ pathname }}
+
+
+
+
+script(type="text/ng-template", id="historyFileTreeTpl")
+	.history-file-tree
+		history-file-entity(
+			ng-repeat="fileEntity in $ctrl.fileTree"
+			file-entity="fileEntity"
+			ng-show="!$ctrl.isLoading"
+		)
+
+
+script(type="text/ng-template", id="historyFileEntityTpl")
+	.history-file-entity-wrapper
+		a.history-file-entity-link(
+			href
+			ng-click="$ctrl.handleClick()"
+			ng-class="{ 'history-file-entity-link-selected': $ctrl.isSelected }"
+		)
+			span.history-file-entity-name
+				i.history-file-entity-icon.history-file-entity-icon-folder-state.fa.fa-fw(
+					ng-class="{\
+						'fa-chevron-down': ($ctrl.fileEntity.type === 'folder' && $ctrl.isOpen),\
+						'fa-chevron-right': ($ctrl.fileEntity.type === 'folder' && !$ctrl.isOpen)\
+					}"
+				)
+				i.history-file-entity-icon.fa(
+					ng-class="$ctrl.iconClass"
+				)
+				| {{ ::$ctrl.fileEntity.name }}
+		div(
+			ng-show="$ctrl.isOpen"
+		)
+			history-file-entity(
+				ng-repeat="childEntity in $ctrl.fileEntity.children"
+				file-entity="childEntity"
+			)

+ 34 - 3
services/web/app/views/project/editor/history/diffPanelV2.pug → services/web/app/views/project/editor/history/previewPanelV2.pug

@@ -1,4 +1,7 @@
-.diff-panel.full-size(ng-if="history.isV2", ng-controller="HistoryV2DiffController")
+.diff-panel.full-size(
+	ng-if="history.isV2 && history.viewMode === HistoryViewModes.COMPARE"
+	ng-controller="HistoryV2DiffController"
+)
 	.diff(
 		ng-if="!!history.diff && !history.diff.loading && !history.diff.error",
 		ng-class="{ 'diff-binary': history.diff.binary }"
@@ -16,8 +19,13 @@
 					}"
 				)
 				|  in <strong>{{history.diff.pathname}}</strong>
+			.history-toolbar-btn(
+				ng-click="toggleHistoryViewMode();"
+			) 
+				i.fa
+				| #{translate("view_single_version")}
 			.toolbar-right(ng-if="history.selection.docs[history.selection.pathname].deletedAtV")
-				button.btn.btn-danger.btn-sm(
+				button.btn.btn-danger.btn-xs(
 					ng-click="restoreDeletedFile()"
 					ng-show="!restoreState.error"
 					ng-disabled="restoreState.inflight"
@@ -47,4 +55,27 @@
 		i.fa.fa-spin.fa-refresh
 		| &nbsp;&nbsp;#{translate("loading")}...
 	.error-panel(ng-show="history.diff.error")
-		.alert.alert-danger #{translate("generic_something_went_wrong")}
+		.alert.alert-danger #{translate("generic_something_went_wrong")}
+
+.point-in-time-panel.full-size(
+	ng-if="history.isV2 && history.viewMode === HistoryViewModes.POINT_IN_TIME"
+)
+	.point-in-time-editor-container(
+		ng-if="!!history.selectedFile && !history.selectedFile.loading && !history.selectedFile.error"
+	)
+		.hide-ace-cursor(
+			ng-if="!history.selectedFile.binary"
+			ace-editor="history-pointintime",
+			theme="settings.theme",
+			font-size="settings.fontSize",
+			text="history.selectedFile.text",
+			read-only="true",
+			resize-on="layout:main:resize",
+		)
+		.alert.alert-info(ng-if="history.selectedFile.binary")
+			| We're still working on showing image and binary changes, sorry. Stay tuned!
+	.loading-panel(ng-show="history.selectedFile.loading")
+		i.fa.fa-spin.fa-refresh
+		| &nbsp;&nbsp;#{translate("loading")}...
+	.error-panel(ng-show="history.selectedFile.error")
+		.alert.alert-danger #{translate("generic_something_went_wrong")}

+ 13 - 0
services/web/app/views/project/editor/history/toolbarV2.pug

@@ -0,0 +1,13 @@
+.history-toolbar(
+	ng-if="ui.view == 'history' && history.isV2 && history.viewMode === HistoryViewModes.POINT_IN_TIME"
+)
+	span(ng-show="history.loadingFileTree")
+		i.fa.fa-spin.fa-refresh
+		| &nbsp;&nbsp; #{translate("loading")}...
+	span(ng-show="!history.loadingFileTree") #{translate("browsing_project_as_of")}&nbsp;
+		time.history-toolbar-time {{ history.selection.updates[0].meta.end_ts | formatDate:'Do MMM YYYY, h:mm a' }}
+	.history-toolbar-btn(
+		ng-click="toggleHistoryViewMode();"
+	) 
+		i.fa
+		| #{translate("compare_to_another_version")}

+ 118 - 0
services/web/app/views/subscriptions/_plans_page_details_less.pug

@@ -0,0 +1,118 @@
+.row
+	.col-md-12
+		.page-header.centered.plans-header.text-centered
+			h1 #{translate("start_x_day_trial", {len:'{{trial_len}}'})}
+.row
+	.col-md-8.col-md-offset-2
+		p.text-centered #{translate("sl_benefits_plans")}
+
+.row.top-switch
+	.col-md-6.col-md-offset-3
+		+plan_switch('card')
+	.col-md-2.text-right
+		+currency_dropdown
+
+div(ng-show="showPlans")
+	.row
+		.col-md-10.col-md-offset-1
+			.row
+				.card-group.text-centered(ng-if="ui.view == 'monthly' || ui.view == 'annual'")
+					.col-md-4
+						.card.card-first
+							.card-header
+								h2 #{translate("personal")}
+							.circle #{translate("free")}
+							+features_free
+					.col-md-4
+						.card.card-highlighted
+							.card-header
+								h2 #{translate("collaborator")}
+							.circle
+								+price_collaborator
+							+features_collaborator
+					.col-md-4
+						.card.card-last
+							.card-header
+								h2 #{translate("professional")}
+							.circle
+								+price_professional
+							+features_professional
+
+				.card-group.text-centered(ng-if="ui.view == 'student'")
+					.col-md-4
+						.card.card-first
+							.card-header
+								h2 #{translate("personal")}
+							.circle #{translate("free")}
+							+features_free
+
+					.col-md-4
+						.card.card-highlighted
+							+card_student_monthly
+
+					.col-md-4
+						.card.card-last
+							+card_student_annual
+
+.row.row-spaced
+	p.text-centered #{translate("choose_plan_works_for_you", {len:'{{trial_len}}'})}
+
+.row
+	.col-md-8.col-md-offset-2
+		.alert.alert-info.text-centered
+			| #{translate("interested_in_group_licence")}
+			br
+			a(href, ng-click="openGroupPlanModal()") #{translate("get_in_touch_for_details")}
+
+	script(type="text/ng-template", id="groupPlanModalTemplate")
+		.modal-header
+			h3 #{translate("group_plan_enquiry")}
+		.modal-body
+			form.text-left.form(ng-controller="UniverstiesContactController", ng-submit="contactUs()")
+				span(ng-show="sent == false && error == false")
+					.form-group
+						label#title9(for='Field9')
+							| Name
+						input#Field9.field.text.medium.span8.form-control(ng-model="form.name", maxlength='255', tabindex='1', onkeyup='')
+					label#title11.desc(for='Field11')
+						| Email
+					.form-group
+						input#Field11.field.text.medium.span8.form-control(ng-model="form.email", name='Field11', type='email', spellcheck='false', value='', maxlength='255', tabindex='2')
+					label#title12.desc(for='Field12')
+						| University / Company
+					.form-group
+						input#Field12.field.text.medium.span8.form-control(ng-model="form.university", name='Field12', type='text', value='', maxlength='255', tabindex='3', onkeyup='')
+					label#title13.desc(for='Field13')
+						| Position
+					.form-group
+						input#Field13.field.text.medium.span8.form-control(ng-model="form.position", name='Field13', type='text', value='', maxlength='255', tabindex='4', onkeyup='')
+					.form-group
+						input(ng-model="form.source", type="hidden", ng-init="form.source = '__ref__'; form.subject = 'General enquiry for larger ShareLaTeX use';")
+					.form-group.text-center
+						input#saveForm.btn-success.btn.btn-lg(name='saveForm', type='submit', ng-disabled="sending",  value='Request a quote')
+				span(ng-show="sent == true && error == false")
+					p Request Sent, Thank you.
+				span(ng-show="error")
+					p Error sending request.
+
+.row
+	.col-md-12
+		.page-header.plans-header.plans-subheader.text-centered
+			h2 #{translate("enjoy_these_features")}
+	.col-md-4
+		.card.features.text-centered
+			i.fa.fa-file-text-o.fa-5x
+			h4 #{translate("unlimited_projects")}
+			p #{translate("create_unlimited_projects")}
+	.col-md-4
+		.card.features.text-centered
+			i.fa.fa-clock-o.fa-5x
+			h4 #{translate("full_doc_history")}
+			p #{translate("never_loose_work")}
+	.col-md-4
+		.card.features.text-centered
+			i.fa.fa-dropbox.fa-5x
+			|  &nbsp; &nbsp;
+			i.fa.fa-github.fa-5x
+			h4 #{translate("sync_to_dropbox_and_github")}
+			p #{translate("access_projects_anywhere")}

+ 160 - 0
services/web/app/views/subscriptions/_plans_page_details_more.pug

@@ -0,0 +1,160 @@
+.row
+	.col-md-12
+		.page-header.centered.plans-header.text-centered
+			h1.text-capitalize #{translate('instant_access')}
+.row
+	.col-md-8.col-md-offset-2
+		p.text-centered #{translate("sl_benefits_plans")}
+
+.row.top-switch
+	.col-md-6.col-md-offset-3
+		+plan_switch('card')
+	.col-md-2.text-right
+		+currency_dropdown
+
+div(ng-show="showPlans")
+	.row
+		.col-md-10.col-md-offset-1
+			.row
+				.card-group.text-centered(ng-if="ui.view == 'monthly' || ui.view == 'annual'")
+					.col-md-4
+						.card.card-first
+							.card-header
+								h2 #{translate("personal")}
+							h5.tagline #{translate("tagline_personal")}
+							.circle #{translate("free")}
+							+features_free
+					.col-md-4
+						.card.card-highlighted
+							.best-value
+								strong #{translate('best_value')}
+							.card-header
+								h2 #{translate("collaborator")}
+							h5.tagline #{translate("tagline_collaborator")}
+							.circle
+								+price_collaborator
+							+features_collaborator
+					.col-md-4
+						.card.card-last
+							.card-header
+								h2 #{translate("professional")}
+							h5.tagline #{translate("tagline_professional")}
+							.circle
+								+price_professional
+							+features_professional
+
+				.card-group.text-centered(ng-if="ui.view == 'student'")
+					.col-md-4
+						.card.card-first
+							.card-header
+								h2 #{translate("personal")}
+							h5.tagline #{translate("tagline_personal")}
+							.circle #{translate("free")}
+							+features_free
+
+					.col-md-4
+						.card.card-highlighted
+							+card_student_annual
+
+					.col-md-4
+						.card.card-last
+							+card_student_monthly
+
+.row.row-spaced-large.text-centered
+	i.fa.fa-cc-mastercard.fa-2x &nbsp;
+	i.fa.fa-cc-visa.fa-2x &nbsp;
+	i.fa.fa-cc-amex.fa-2x &nbsp;
+	i.fa.fa-cc-paypal.fa-2x &nbsp;
+	div.text-centered #{translate('change_plans_any_time')}<br/> #{translate('billed_after_x_days', {len:'{{trial_len}}'})}
+
+.row.row-spaced-large
+	.col-md-8.col-md-offset-2
+		.card.text-centered
+			.card-header
+				h2 #{translate('looking_multiple_licenses')}
+			span #{translate('reduce_costs_group_licenses')}
+			br
+			br
+			a.btn.btn-info(href="/i/university/groups") #{translate('find_out_more')}
+
+div
+	.row.row-spaced-large
+		.col-sm-12
+			.page-header.plans-header.plans-subheader.text-centered
+				h2 #{translate('compare_plan_features')}
+	.row
+		.col-md-6.col-md-offset-3
+			+plan_switch('table')
+		.col-md-3.text-right
+			+currency_dropdown
+	.row(event-tracking="features-table-viewed" event-tracking-ga="subscription-funnel" event-tracking-trigger="scroll"  event-tracking-send-once="true")
+		.col-sm-12(ng-if="ui.view != 'student'")
+			+table_premium
+		.col-sm-12(ng-if="ui.view == 'student'")
+			+table_student
+
+	.row.row-spaced-large
+		.col-md-12
+			.page-header.plans-header.plans-subheader.text-centered
+				h2 #{translate('in_good_company')}
+	.row
+		.col-md-6
+			div
+				.row
+					.col-md-3
+						.circle-img
+							img(src=buildImgPath('advocates/erdogmus.jpg') alt="Professor Erdogmus")
+					.col-md-9
+						blockquote
+							p The ability to track changes and the real-time collaborative nature is what sets ShareLaTeX apart.
+							footer Professor Erdogmus, Northeastern University
+		.col-md-6
+			div
+				.row
+					.col-md-3
+						.circle-img
+							img(src=buildImgPath('advocates/henderson.jpg') alt="Rob Henderson")
+					.col-md-9
+						blockquote
+							p ShareLaTeX has proven to be a powerful and robust collaboration tool that is widely used in our School.
+							footer Rob Henderson, School Of Informatics And Computing - Indiana University
+
+	.faq
+		.row.row-spaced-large
+			.col-md-12
+				.page-header.plans-header.plans-subheader.text-centered
+					h2 FAQ
+		.row
+			.col-md-6
+				h3 #{translate("faq_how_free_trial_works_question")}
+				p #{translate('faq_how_free_trial_works_answer', { len:'{{trial_len}}' })}
+			.col-md-6
+				h3 #{translate('faq_change_plans_question')}
+				p #{translate('faq_change_plans_answer')}
+		.row
+			.col-md-6
+				h3 #{translate('faq_do_collab_need_premium_question')}
+				p #{translate('faq_do_collab_need_premium_answer')}
+			.col-md-6
+				h3 #{translate('faq_need_more_collab_question')}
+				p !{translate('faq_need_more_collab_answer', { referFriendsLink: '<a href="/user/bonus">' + translate('referring_your_friends') + '</a>'})}
+		.row
+			.col-md-6
+				h3 #{translate('faq_purchase_more_licenses_question')}
+				p !{translate('faq_purchase_more_licenses_answer', { groupLink: '<a href="/i/university/groups">' + translate('discounted_group_accounts') + '</a>' })}
+			.col-md-6
+				h3 #{translate('faq_monthly_or_annual_question')}
+				p #{translate('faq_monthly_or_annual_answer')}
+		.row
+			.col-md-6
+				h3 #{translate('faq_how_to_pay_question')}
+				p #{translate('faq_how_to_pay_answer')}
+			.col-md-6
+				h3 #{translate('faq_pay_by_invoice_question')}
+				p !{translate('faq_pay_by_invoice_answer', { groupLink: '<a href="/i/university/groups">' + translate('discounted_group_accounts') + '</a>' })}
+		.row.row-spaced-large.text-centery
+			.col-md-12
+				.plans-header.plans-subheader.text-centered
+					h2 #{translate('still_have_questions')}
+						button.btn.btn-info.btn-header.text-capitalize(ng-controller="ContactGeneralModal" ng-click="openModal()") #{translate('get_in_touch')}
+						!= moduleIncludes("contactModalGeneral", locals)

+ 162 - 0
services/web/app/views/subscriptions/_plans_page_mixins.pug

@@ -0,0 +1,162 @@
+//- Buy Buttons
+mixin btn_buy_collaborator(location)
+	a.btn.btn-info(
+		ng-href="/user/subscription/new?planCode={{ getCollaboratorPlanCode() }}&currency={{currencyCode}}",
+		ng-click="signUpNowClicked('collaborator','" + location + "')"
+	)
+		span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")}
+		span(ng-show="ui.view == 'annual'") #{translate("buy_now")}
+mixin btn_buy_free(location)
+	a.btn.btn-info(
+		href="/register"
+		style=(getLoggedInUserId() === null ? "" : "visibility: hidden")
+		ng-click="signUpNowClicked('free','" + location + "')"
+	)
+		span(ng-if="plansVariant !== 'more-details'") #{translate('sign_up_now')}
+		span.text-capitalize(ng-if="plansVariant === 'more-details'") #{translate('get_started_now')}
+mixin btn_buy_professional(location)
+	a.btn.btn-info(
+		ng-href="/user/subscription/new?planCode=professional{{ ui.view == 'annual' && '-annual' || planQueryString}}&currency={{currencyCode}}"
+		ng-click="signUpNowClicked('professional','" + location + "')"
+	)
+		span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")}
+		span(ng-show="ui.view == 'annual'") #{translate("buy_now")}
+mixin btn_buy_student(location, plan)
+	if plan == 'annual'
+		a.btn.btn-info(
+			ng-href="/user/subscription/new?planCode=student-annual&currency={{currencyCode}}",
+			ng-click="signUpNowClicked('student-annual','" + location + "')"
+		) #{translate("buy_now")}
+	else
+		//- planQueryString will contain _free_trial_7_days
+		a.btn.btn-info(
+			ng-href="/user/subscription/new?planCode=student{{planQueryString}}&currency={{currencyCode}}",
+			ng-click="signUpNowClicked('student-monthly','" + location + "')"
+		) #{translate("start_free_trial")}
+
+//- Cards
+mixin card_student_annual
+	.best-value(ng-if="plansVariant == 'more-details'")
+		strong #{translate('best_value')}
+	.card-header
+		h2 #{translate("student")} (#{translate("annual")})
+	h5.tagline(ng-if="plansVariant == 'more-details'") #{translate('tagline_student_annual')}
+	.circle
+		span
+			+price_student_annual
+	+features_student('card', 'annual')
+mixin card_student_monthly
+	.card-header
+		h2 #{translate("student")}
+	h5.tagline(ng-if="plansVariant == 'more-details'") #{translate('tagline_student_monthly')}
+	.circle
+		span
+			+price_student_monthly
+	+features_student('card', 'monthly')
+
+//- Features Lists
+mixin features_collaborator
+	ul.list-unstyled
+		li
+			strong #{translate("collabs_per_proj", {collabcount:10})}
+		+features_premium
+		li
+			br
+			+btn_buy_collaborator('card')
+mixin features_free
+	ul.list-unstyled
+		li #{translate("one_collaborator")}
+		li(class="hidden-xs hidden-sm") &nbsp;
+		li(class="hidden-xs hidden-sm") &nbsp;
+		li(class="hidden-xs hidden-sm") &nbsp;
+		li(class="hidden-xs hidden-sm" ng-if="plansVariant === 'more-details'") &nbsp;
+		li(class="hidden-xs hidden-sm" ng-if="plansVariant === 'more-details'") &nbsp;
+		li(class="hidden-xs hidden-sm" ng-if="plansVariant === 'more-details'") &nbsp;
+		li
+			br
+			+btn_buy_free('card')
+mixin features_premium
+	li(ng-if="plansVariant != 'more-details'") #{translate("full_doc_history")}
+	li(ng-if="plansVariant != 'more-details'") #{translate("sync_to_dropbox")}
+	li(ng-if="plansVariant != 'more-details'") #{translate("sync_to_github")}
+	li(ng-if="plansVariant === 'more-details'") &nbsp;
+	li(ng-if="plansVariant === 'more-details'")
+		strong #{translate('all_premium_features')}
+	li(ng-if="plansVariant === 'more-details'") #{translate('sync_dropbox_github')}
+	li(ng-if="plansVariant === 'more-details'") #{translate('full_doc_history')}
+	li(ng-if="plansVariant === 'more-details'") #{translate('track_changes')}
+	li(ng-if="plansVariant === 'more-details'") + #{translate('more').toLowerCase()}
+mixin features_professional
+	ul.list-unstyled
+		li
+			strong #{translate("unlimited_collabs")}
+			+features_premium
+		li
+			br
+			+btn_buy_professional('card')
+mixin features_student(location, plan)
+	ul.list-unstyled
+		li
+			strong #{translate("collabs_per_proj", {collabcount:6})}
+			+features_premium
+			li
+				br
+				+btn_buy_student(location, plan)
+
+//- Prices
+mixin price_collaborator
+	span(ng-if="ui.view == 'monthly'")
+		| {{plans[currencyCode]['collaborator']['monthly']}}
+		span.small /mo
+	span(ng-if="ui.view == 'annual'")
+		| {{plans[currencyCode]['collaborator']['annual']}}
+		span.small /yr
+mixin price_professional
+	span(ng-if="ui.view == 'monthly'")
+		| {{plans[currencyCode]['professional']['monthly']}}
+		span.small /mo
+	span(ng-if="ui.view == 'annual'")
+		| {{plans[currencyCode]['professional']['annual']}}
+		span.small /yr
+mixin price_student_annual
+	| {{plans[currencyCode]['student']['annual']}}
+	span.small /yr
+mixin price_student_monthly
+	| {{plans[currencyCode]['student']['monthly']}}
+	span.small /mo
+
+//- UI Control
+mixin currency_dropdown
+	.dropdown.currency-dropdown(dropdown)
+		a.btn.btn-default.dropdown-toggle(
+			href="#",
+			data-toggle="dropdown",
+			dropdown-toggle
+		)
+			| {{currencyCode}} ({{plans[currencyCode]['symbol']}})
+			span.caret
+
+		ul.dropdown-menu.dropdown-menu-right.text-right(role="menu")
+			li(ng-repeat="(currency, value) in plans")
+				a(
+					href="#",
+					ng-click="changeCurreny($event, currency)"
+				) {{currency}} ({{value['symbol']}})
+mixin plan_switch(location)
+	ul.nav.nav-pills
+		li(ng-class="{'active': ui.view == 'monthly'}")
+			a(
+				href="#"
+				ng-click="switchToMonthly($event,'" + location + "')"
+			) #{translate("monthly")}
+		li(ng-class="{'active': ui.view == 'annual'}")
+			a(
+				href="#"
+				ng-click="switchToAnnual($event,'" + location + "')"
+			) #{translate("annual")}
+		li(ng-class="{'active': ui.view == 'student'}")
+			a(
+				href="#"
+				ng-click="switchToStudent($event,'" + location + "')"
+			) #{translate("half_price_student")}
+

+ 107 - 0
services/web/app/views/subscriptions/_plans_page_tables.pug

@@ -0,0 +1,107 @@
+
+//- Features Tables
+mixin table_premium
+	table.card.plans-table
+		tr
+			th
+			th #{translate("personal")}
+			th #{translate("collaborator")}
+				.outer.outer-top
+					.outer-content
+						.best-value
+							strong #{translate('best_value')}
+			th #{translate("professional")}
+
+		tr
+			td #{translate("price")}
+			td #{translate("free")}
+			td
+				+price_collaborator
+			td
+				+price_professional
+
+		for feature in planFeatures
+			tr
+				td(event-tracking="features-table" event-tracking-trigger="hover" event-tracking-ga="subscription-funnel" event-tracking-label=`${feature.feature}-exp-{{plansVariant}}`)
+					if feature.info
+						span(tooltip=translate(feature.info)) #{translate(feature.feature)}
+					else
+						| #{translate(feature.feature)}
+				for plan in feature.plans
+					td
+						if feature.value == 'str'
+							| #{plan}
+						else if plan
+							i.fa.fa-check
+						else
+							i.fa.fa-times
+
+		tr
+			td
+			td
+				+btn_buy_free('table')
+			td
+				+btn_buy_collaborator('table')
+				.outer.outer-btm
+					.outer-content &nbsp;
+			td
+				+btn_buy_professional('table')
+
+mixin table_cell_student(feature)
+	if feature.value == 'str'
+		| #{feature.student}
+	else if feature.student
+		i.fa.fa-check
+	else
+		i.fa.fa-times
+
+mixin table_student
+	table.card.plans-table
+		tr
+			th
+			th #{translate("personal")}
+			th #{translate("student")} (#{translate("annual")})
+				.outer.outer-top
+					.outer-content
+						.best-value
+							strong Best Value
+			th #{translate("student")}
+
+		tr
+			td #{translate("price")}
+			td #{translate("free")}
+			td
+				+price_student_annual
+			td
+				+price_student_monthly
+
+		for feature in planFeatures
+			tr
+				td(event-tracking="plans-page-table" event-tracking-trigger="hover" event-tracking-ga="subscription-funnel" event-tracking-label=`${feature.feature}-exp-{{plansVariant}}`)
+					if feature.info
+						span(tooltip=translate(feature.info)) #{translate(feature.feature)}
+					else
+						| #{translate(feature.feature)}
+				td
+					if feature.value == 'str'
+						| #{feature.plans.free}
+					else if feature.plans.free
+						i.fa.fa-check
+					else
+						i.fa.fa-times
+				td
+					+table_cell_student(feature)
+				td
+					+table_cell_student(feature)
+
+		tr
+			td
+			td
+				+btn_buy_free('table')
+			td
+				+btn_buy_student('table', 'annual')
+				.outer.outer-btm
+					.outer-content &nbsp;
+			td
+				+btn_buy_student('table', 'monthly')
+

+ 4 - 1
services/web/app/views/subscriptions/new.pug

@@ -31,7 +31,10 @@ block content
 											li(ng-repeat="(currency, value) in plans")
 												a(
 													ng-click="changeCurrency(currency)",
-												) {{currency}} ({{value['symbol']}})
+												) {{currency}} ({{value['symbol']}}) 
+							.row(ng-if="plansVariant == 'more-details' && planCode == 'student-annual' || plansVariant == 'more-details' && planCode == 'student-monthly'")
+								.col-xs-12
+									p.student-disclaimer #{translate('student_disclaimer')}
 							hr.thin
 							.row
 								.col-md-12.text-center

+ 8 - 243
services/web/app/views/subscriptions/plans.pug

@@ -1,253 +1,18 @@
 extends ../layout
+
+include _plans_page_mixins
+include _plans_page_tables
+
 block scripts
 	script(type='text/javascript').
 		window.recomendedCurrency = '#{recomendedCurrency}'
 		window.abCurrencyFlag = '#{abCurrencyFlag}'
 		window.shouldABTestPlans = #{shouldABTestPlans || false}
 
-	script(type='text/javascript').
-		(function() {var s=document.createElement('script'); s.type='text/javascript';s.async=true; 
-		s.src=('https:'==document.location.protocol?'https':'http') + '://sharelatex-accounts.groovehq.com/widgets/f5ad3b09-7d99-431b-8af5-c5725e3760ce/ticket/api.js'; 
-		var q = document.getElementsByTagName('script')[0];q.parentNode.insertBefore(s, q);})(); 
-		
 block content
 	.content.content-alt
 		.content.plans(ng-controller="PlansController")
-			.container
-				.row
-					.col-md-12
-						.page-header.centered.plans-header.text-centered
-							h1(ng-cloak) #{translate("start_x_day_trial", {len:'{{trial_len}}'})}
-				.row
-					.col-md-8.col-md-offset-2
-						p.text-centered #{translate("sl_benefits_plans")}
-
-				.row(ng-cloak)
-					.col-md-6.col-md-offset-3
-						ul.nav.nav-pills
-							li(ng-class="{'active': ui.view == 'monthly'}")
-								a(
-									href,
-									ng-click="switchToMonthly()"
-								) #{translate("monthly")}
-							li(ng-class="{'active': ui.view == 'annual'}")
-								a(
-									href
-									ng-click="switchToAnnual()"
-								) #{translate("annual")}
-							li(ng-class="{'active': ui.view == 'student'}")
-								a(
-									href,
-									ng-click="switchToStudent()"
-								) #{translate("half_price_student")} 
-					.col-md-2.text-right
-						.dropdown.currency-dropdown(dropdown)
-							a.btn.btn-default.dropdown-toggle#currenyDropdown(
-								href="#",
-								data-toggle="dropdown",
-								dropdown-toggle
-							)
-								| {{currencyCode}} ({{plans[currencyCode]['symbol']}}) 
-								span.caret
-
-							ul.dropdown-menu.dropdown-menu-right.text-right(role="menu")
-								li(ng-repeat="(currency, value) in plans")
-									a(
-										href,
-										ng-click="changeCurreny(currency)"
-									) {{currency}} ({{value['symbol']}})
-
-				div(ng-show="showPlans")
-					.row(ng-cloak)
-						.col-md-10.col-md-offset-1
-							.row
-								.card-group.text-centered(ng-if="ui.view == 'monthly' || ui.view == 'annual'")
-									.col-md-4
-										.card.card-first
-											.card-header
-												h2 #{translate("personal")} 
-											.circle #{translate("free")} 
-											ul.list-unstyled
-												li #{translate("one_collaborator")}
-												li &nbsp;
-												li &nbsp;
-												li &nbsp;
-												li
-													br
-													a.btn.btn-info(
-														href="/register"
-														style=(getLoggedInUserId() === null ? "" : "visibility: hidden")
-													) #{translate("sign_up_now")}
-									.col-md-4
-										.card.card-highlighted
-											.card-header
-												h2 #{translate("collaborator")}
-											.circle
-												span(ng-if="ui.view == 'monthly'")
-													| {{plans[currencyCode]['collaborator']['monthly']}}
-													span.small /mo
-												span(ng-if="ui.view == 'annual'")
-													| {{plans[currencyCode]['collaborator']['annual']}}
-													span.small /yr
-											ul.list-unstyled
-												li
-													strong #{translate("collabs_per_proj", {collabcount:10})}
-												li #{translate("full_doc_history")}
-												li #{translate("sync_to_dropbox")}
-												li #{translate("sync_to_github")}
-												li
-													br
-													a.btn.btn-info(
-														ng-href="/user/subscription/new?planCode={{ getCollaboratorPlanCode() }}&currency={{currencyCode}}", ng-click="signUpNowClicked('collaborator')"
-													)
-														span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")}
-														span(ng-show="ui.view == 'annual'") #{translate("buy_now")}
-									.col-md-4
-										.card.card-last
-											.card-header
-												h2 #{translate("professional")}
-											.circle 
-												span(ng-if="ui.view == 'monthly'")
-													| {{plans[currencyCode]['professional']['monthly']}}
-													span.small /mo
-												span(ng-if="ui.view == 'annual'")
-													| {{plans[currencyCode]['professional']['annual']}}
-													span.small /yr
-											ul.list-unstyled
-												li
-													strong #{translate("unlimited_collabs")}
-												li #{translate("full_doc_history")}
-												li #{translate("sync_to_dropbox")}
-												li #{translate("sync_to_github")}
-												li
-													br
-													a.btn.btn-info(
-														ng-href="/user/subscription/new?planCode=professional{{ ui.view == 'annual' && '-annual' || planQueryString}}&currency={{currencyCode}}", ng-click="signUpNowClicked('professional')"
-													)
-														span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")}
-														span(ng-show="ui.view == 'annual'") #{translate("buy_now")}
-
-								.card-group.text-centered(ng-if="ui.view == 'student'")
-									.col-md-4
-										.card.card-first
-											.card-header
-												h2 #{translate("personal")}
-											.circle #{translate("free")}
-											ul.list-unstyled
-												li #{translate("one_collaborator")}
-												li &nbsp;
-												li &nbsp;
-												li &nbsp;
-												li
-													br
-													a.btn.btn-info(
-														href="/register"
-														style=(getLoggedInUserId() === null ? "" : "visibility: hidden")
-													) #{translate("sign_up_now")}
-
-									.col-md-4
-										.card.card-highlighted
-											.card-header
-												h2 #{translate("student")}
-											.circle
-												span
-													| {{plans[currencyCode]['student']['monthly']}}
-													span.small /mo
-											ul.list-unstyled
-												li
-													strong #{translate("collabs_per_proj", {collabcount:6})}
-												li #{translate("full_doc_history")}
-												li #{translate("sync_to_dropbox")}
-												li #{translate("sync_to_github")}
-												li
-													br
-													a.btn.btn-info(
-														ng-href="/user/subscription/new?planCode=student{{ plansVariant == 'default' ? planQueryString : '_'+plansVariant }}&currency={{currencyCode}}",
-														ng-click="signUpNowClicked('student-monthly')"
-													) #{translate("start_free_trial")}
-
-									.col-md-4
-										.card.card-last
-											.card-header
-												h2 #{translate("student")} (#{translate("annual")})
-											.circle
-												span
-													| {{plans[currencyCode]['student']['annual']}}
-													span.small /yr
-											ul.list-unstyled
-												li
-													strong #{translate("collabs_per_proj", {collabcount:6})}
-												li #{translate("full_doc_history")}
-												li #{translate("sync_to_dropbox")}
-												li #{translate("sync_to_github")}
-												li
-													br
-													a.btn.btn-info(
-														ng-href="/user/subscription/new?planCode=student-annual{{ plansVariant == 'default' ? '' : '_'+plansVariant }}&currency={{currencyCode}}",
-														ng-click="signUpNowClicked('student-annual')"
-													) #{translate("buy_now")}
-
-
-
-				.row.row-spaced(ng-cloak)
-					p.text-centered #{translate("choose_plan_works_for_you", {len:'{{trial_len}}'})}
-					
-				.row(ng-cloak)
-					.col-md-8.col-md-offset-2
-						.alert.alert-info.text-centered
-							| #{translate("interested_in_group_licence")}
-							br
-							a(href, ng-click="openGroupPlanModal()") #{translate("get_in_touch_for_details")}
-						
-					script(type="text/ng-template", id="groupPlanModalTemplate")
-						.modal-header
-							h3 #{translate("group_plan_enquiry")}
-						.modal-body
-							form.text-left.form(ng-controller="UniverstiesContactController", ng-submit="contactUs()", ng-cloak)
-								span(ng-show="sent == false && error == false")
-									.form-group
-										label#title9(for='Field9')
-											| Name
-										input#Field9.field.text.medium.span8.form-control(ng-model="form.name", maxlength='255', tabindex='1', onkeyup='')
-									label#title11.desc(for='Field11')
-										| Email
-									.form-group
-										input#Field11.field.text.medium.span8.form-control(ng-model="form.email", name='Field11', type='email', spellcheck='false', value='', maxlength='255', tabindex='2')
-									label#title12.desc(for='Field12')
-										| University / Company
-									.form-group
-										input#Field12.field.text.medium.span8.form-control(ng-model="form.university", name='Field12', type='text', value='', maxlength='255', tabindex='3', onkeyup='')
-									label#title13.desc(for='Field13')
-										| Position
-									.form-group
-										input#Field13.field.text.medium.span8.form-control(ng-model="form.position", name='Field13', type='text', value='', maxlength='255', tabindex='4', onkeyup='')
-									.form-group
-										input(ng-model="form.source", type="hidden", ng-init="form.source = '__ref__'; form.subject = 'General enquiry for larger ShareLaTeX use';")
-									.form-group.text-center
-										input#saveForm.btn-success.btn.btn-lg(name='saveForm', type='submit', ng-disabled="sending",  value='Request a quote')
-								span(ng-show="sent == true && error == false")
-									p Request Sent, Thank you.
-								span(ng-show="error")
-									p Error sending request.
-
-				.row
-					.col-md-12
-						.page-header.plans-header.plans-subheader.text-centered
-							h2 #{translate("enjoy_these_features")}
-					.col-md-4
-						.card.features.text-centered
-							i.fa.fa-file-text-o.fa-5x
-							h4 #{translate("unlimited_projects")}
-							p #{translate("create_unlimited_projects")}
-					.col-md-4
-						.card.features.text-centered
-							i.fa.fa-clock-o.fa-5x
-							h4 #{translate("full_doc_history")}
-							p #{translate("never_loose_work")}
-					.col-md-4
-						.card.features.text-centered
-							i.fa.fa-dropbox.fa-5x
-							|  &nbsp; &nbsp; 
-							i.fa.fa-github.fa-5x
-							h4 #{translate("sync_to_dropbox_and_github")}
-							p #{translate("access_projects_anywhere")}
+			.container(class="more-details" ng-cloak ng-if="plansVariant === 'more-details'")
+				include _plans_page_details_more
+			.container(ng-cloak ng-if="plansVariant != 'more-details'")
+				include _plans_page_details_less

+ 39 - 5
services/web/public/coffee/directives/eventTracking.coffee

@@ -4,11 +4,23 @@
 #   event not sent to MB.
 #     for MB, add event-tracking-mb='true'
 #     by default, event sent to MB via sendMB
-#       this can be changed to use sendMBOnce via event-tracking-send-once='true' attribute
 #   event not sent to GA.
 #     for GA, add event-tracking-ga attribute, where the value is the GA category
+# Either GA or MB can use the attribute event-tracking-send-once='true' to
+# send event just once
+#   MB will use the key and GA will use the action to determine if the event
+#   has been sent
 # event-tracking-trigger attribute is required to send event
 
+isInViewport = (element) ->
+	elTop = element.offset().top
+	elBtm = elTop + element.outerHeight()
+
+	viewportTop = $(window).scrollTop()
+	viewportBtm = viewportTop + $(window).height()
+
+	elBtm > viewportTop && elTop < viewportBtm
+
 define [
 	'base'
 ], (App) ->
@@ -22,20 +34,42 @@ define [
 				sendGA = attrs.eventTrackingGa || false
 				sendMB = attrs.eventTrackingMb || false
 				sendMBFunction = if attrs.eventTrackingSendOnce then 'sendMBOnce' else 'sendMB'
+				sendGAFunction = if attrs.eventTrackingSendOnce then 'sendGAOnce' else 'send'
 				segmentation = scope.eventSegmentation || {}
-
 				segmentation.page = window.location.pathname
 
-				sendEvent = () ->
+				sendEvent = (scrollEvent) ->
+					###
+						@param {boolean}		scrollEvent		Use to unbind scroll event
+					###
 					if sendMB
 						event_tracking[sendMBFunction] scope.eventTracking, segmentation
 					if sendGA
-						event_tracking.send attrs.eventTrackingGa, attrs.eventTrackingAction || scope.eventTracking, attrs.eventTrackingLabel || ''
+						event_tracking[sendGAFunction] attrs.eventTrackingGa, attrs.eventTrackingAction || scope.eventTracking, attrs.eventTrackingLabel || ''
+					if scrollEvent
+						$(window).unbind('resize scroll')
 
 				if attrs.eventTrackingTrigger == 'load'
 					sendEvent()
 				else if attrs.eventTrackingTrigger == 'click'
 					element.on 'click', (e) ->
 						sendEvent()
+				else if attrs.eventTrackingTrigger == 'hover'
+					timer = null
+					timeoutAmt = 500
+					if attrs.eventHoverAmt
+						timeoutAmt = parseInt(attrs.eventHoverAmt, 10)
+					element.on 'mouseenter', () ->
+						timer = setTimeout((-> sendEvent()), timeoutAmt)
+						return
+					.on 'mouseleave', () ->
+						clearTimeout(timer)
+				else if attrs.eventTrackingTrigger == 'scroll'
+					if !event_tracking.eventInCache(scope.eventTracking)
+						$(window).on 'resize scroll', () ->
+							_.throttle(
+								if isInViewport(element) && !event_tracking.eventInCache(scope.eventTracking)
+									sendEvent(true)
+							, 500)
 		}
-	]
+	]

+ 5 - 0
services/web/public/coffee/ide/file-tree/FileTreeManager.coffee

@@ -335,6 +335,11 @@ define [
 
 			return null
 
+		projectContainsFolder: () ->
+			for entity in @$scope.rootFolder.children
+				return true if entity.type == 'folder'
+			return false
+
 		existsInThisFolder: (folder, name) ->
 			for entity in folder?.children or []
 				return true if entity.name is name

+ 3 - 13
services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee

@@ -1,6 +1,7 @@
 define [
 	"base"
-], (App) ->
+	"ide/file-tree/util/iconTypeFromName"
+], (App, iconTypeFromName) ->
 	App.controller "FileTreeEntityController", ["$scope", "ide", "$modal", ($scope, ide, $modal) ->
 		$scope.select = (e) ->
 			if e.ctrlKey or e.metaKey
@@ -70,18 +71,7 @@ define [
 		$scope.$on "delete:selected", () ->
 			$scope.openDeleteModal() if $scope.entity.selected
 
-		$scope.iconTypeFromName = (name) ->
-			ext = name.split(".").pop()?.toLowerCase()
-			if ext in ["png", "pdf", "jpg", "jpeg", "gif"]
-				return "image"
-			else if ext in ["csv", "xls", "xlsx"]
-				return "table"
-			else if ext in ["py", "r"]
-				return "file-text"
-			else if ext in ['bib']
-				return 'book'
-			else
-				return "file"
+		$scope.iconTypeFromName = iconTypeFromName
 	]
 
 	App.controller "DeleteEntityModalController", [

+ 13 - 0
services/web/public/coffee/ide/file-tree/util/iconTypeFromName.coffee

@@ -0,0 +1,13 @@
+define [], () ->
+	return iconTypeFromName = (name) ->
+		ext = name.split(".").pop()?.toLowerCase()
+		if ext in ["png", "pdf", "jpg", "jpeg", "gif"]
+			return "image"
+		else if ext in ["csv", "xls", "xlsx"]
+			return "table"
+		else if ext in ["py", "r"]
+			return "file-text"
+		else if ext in ['bib']
+			return 'book'
+		else
+			return "file"

+ 0 - 1
services/web/public/coffee/ide/history/HistoryManager.coffee

@@ -4,7 +4,6 @@ define [
 	"ide/history/util/displayNameForUser"
 	"ide/history/controllers/HistoryListController"
 	"ide/history/controllers/HistoryDiffController"
-	"ide/history/controllers/HistoryV2DiffController"
 	"ide/history/directives/infiniteScroll"
 ], (moment, ColorManager, displayNameForUser) ->
 	class HistoryManager

+ 86 - 10
services/web/public/coffee/ide/history/HistoryV2Manager.coffee

@@ -2,13 +2,20 @@ define [
 	"moment"
 	"ide/colors/ColorManager"
 	"ide/history/util/displayNameForUser"
-	"ide/history/controllers/HistoryListController"
-	"ide/history/controllers/HistoryDiffController"
+	"ide/history/util/HistoryViewModes"
+	"ide/history/controllers/HistoryV2ListController"
+	"ide/history/controllers/HistoryV2DiffController"
+	"ide/history/controllers/HistoryV2FileTreeController"
 	"ide/history/directives/infiniteScroll"
-], (moment, ColorManager, displayNameForUser) ->
+	"ide/history/components/historyEntriesList"
+	"ide/history/components/historyEntry"
+	"ide/history/components/historyFileTree"
+	"ide/history/components/historyFileEntity"
+], (moment, ColorManager, displayNameForUser, HistoryViewModes) ->
 	class HistoryManager
 		constructor: (@ide, @$scope) ->
 			@reset()
+			@$scope.HistoryViewModes = HistoryViewModes
 
 			@$scope.toggleHistory = () =>
 				if @$scope.ui.view == "history"
@@ -16,17 +23,31 @@ define [
 				else
 					@show()
 
+			@$scope.toggleHistoryViewMode = () =>
+				if @$scope.history.viewMode == HistoryViewModes.COMPARE
+					@reset()
+					@$scope.history.viewMode = HistoryViewModes.POINT_IN_TIME
+				else
+					@reset()
+					@$scope.history.viewMode = HistoryViewModes.COMPARE
+
 			@$scope.$watch "history.selection.updates", (updates) =>
-				if updates? and updates.length > 0
-					@_selectDocFromUpdates()
+				if @$scope.history.viewMode == HistoryViewModes.COMPARE
+					if updates? and updates.length > 0
+						@_selectDocFromUpdates()
+						@reloadDiff()
+			
+			@$scope.$watch "history.selection.pathname", (pathname) =>
+				if @$scope.history.viewMode == HistoryViewModes.POINT_IN_TIME
+					if pathname?
+						@loadFileAtPointInTime()
+				else 
 					@reloadDiff()
 
-			@$scope.$watch "history.selection.pathname", () =>
-				@reloadDiff()
-
 		show: () ->
 			@$scope.ui.view = "history"
 			@reset()
+			@$scope.history.viewMode = HistoryViewModes.POINT_IN_TIME
 
 		hide: () ->
 			@$scope.ui.view = "editor"
@@ -35,6 +56,7 @@ define [
 			@$scope.history = {
 				isV2: true
 				updates: []
+				viewMode: null
 				nextBeforeTimestamp: null
 				atEnd: false
 				selection: {
@@ -46,16 +68,33 @@ define [
 						toV: null
 					}
 				}
-				diff: null
+				files: []
+				diff: null # When history.viewMode == HistoryViewModes.COMPARE
+				selectedFile: null # When history.viewMode == HistoryViewModes.POINT_IN_TIME
 			}
 
 		restoreFile: (version, pathname) ->
 			url = "/project/#{@$scope.project_id}/restore_file"
+
 			@ide.$http.post(url, {
 				version, pathname,
 				_csrf: window.csrfToken
 			})
 
+		loadFileTreeForUpdate: (update) ->
+			{fromV, toV} = update
+			url = "/project/#{@$scope.project_id}/filetree/diff"
+			query = [ "from=#{toV}", "to=#{toV}" ]
+			url += "?" + query.join("&")
+			@$scope.history.loadingFileTree = true
+			@$scope.history.selectedFile = null
+			@$scope.history.selection.pathname = null
+			@ide.$http
+				.get(url)
+				.then (response) =>
+					@$scope.history.files = response.data.diff
+					@$scope.history.loadingFileTree = false
+
 		MAX_RECENT_UPDATES_TO_SELECT: 5
 		autoSelectRecentUpdates: () ->
 			return if @$scope.history.updates.length == 0
@@ -70,12 +109,28 @@ define [
 
 			@$scope.history.updates[indexOfLastUpdateNotByMe].selectedFrom = true
 
+		autoSelectLastUpdate: () ->
+			return if @$scope.history.updates.length == 0
+			@selectUpdate @$scope.history.updates[0]
+
+		selectUpdate: (update) ->
+			selectedUpdateIndex = @$scope.history.updates.indexOf update
+			if selectedUpdateIndex == -1
+				selectedUpdateIndex = 0
+			for update in @$scope.history.updates
+				update.selectedTo = false
+				update.selectedFrom = false
+			@$scope.history.updates[selectedUpdateIndex].selectedTo = true
+			@$scope.history.updates[selectedUpdateIndex].selectedFrom = true
+			@loadFileTreeForUpdate @$scope.history.updates[selectedUpdateIndex]
+
 		BATCH_SIZE: 10
 		fetchNextBatchOfUpdates: () ->
 			url = "/project/#{@ide.project_id}/updates?min_count=#{@BATCH_SIZE}"
 			if @$scope.history.nextBeforeTimestamp?
 				url += "&before=#{@$scope.history.nextBeforeTimestamp}"
 			@$scope.history.loading = true
+			@$scope.history.loadingFileTree = true
 			@ide.$http
 				.get(url)
 				.then (response) =>
@@ -86,6 +141,23 @@ define [
 						@$scope.history.atEnd = true
 					@$scope.history.loading = false
 
+		loadFileAtPointInTime: () ->
+			pathname = @$scope.history.selection.pathname
+			toV =  @$scope.history.selection.updates[0].toV
+			url = "/project/#{@$scope.project_id}/diff"
+			query = ["pathname=#{encodeURIComponent(pathname)}", "from=#{toV}", "to=#{toV}"]
+			url += "?" + query.join("&")
+			@$scope.history.selectedFile =
+				loading: true
+			@ide.$http
+				.get(url)
+				.then (response) =>
+					{text, binary} = @_parseDiff(response.data.diff)
+					@$scope.history.selectedFile.binary = binary
+					@$scope.history.selectedFile.text = text
+					@$scope.history.selectedFile.loading = false
+				.catch () ->
+
 		reloadDiff: () ->
 			diff = @$scope.history.diff
 			{updates} = @$scope.history.selection
@@ -200,7 +272,11 @@ define [
 			@$scope.history.updates =
 				@$scope.history.updates.concat(updates)
 
-			@autoSelectRecentUpdates() if firstLoad
+			if firstLoad 
+				if @$scope.history.viewMode == HistoryViewModes.COMPARE
+					@autoSelectRecentUpdates()
+				else 
+					@autoSelectLastUpdate()
 
 		_perDocSummaryOfUpdates: (updates) ->
 			# Track current_pathname -> original_pathname

+ 19 - 0
services/web/public/coffee/ide/history/components/historyEntriesList.coffee

@@ -0,0 +1,19 @@
+define [
+	"base"
+], (App) ->
+	historyEntriesListController = ($scope, $element, $attrs) ->
+		ctrl = @ 
+		return
+
+	App.component "historyEntriesList", {
+		bindings:
+			entries: "<"
+			loadEntries: "&"
+			loadDisabled: "<"
+			loadInitialize: "<"
+			isLoading: "<"
+			currentUser: "<"
+			onEntrySelect: "&"
+		controller: historyEntriesListController
+		templateUrl: "historyEntriesListTpl"
+	}

+ 27 - 0
services/web/public/coffee/ide/history/components/historyEntry.coffee

@@ -0,0 +1,27 @@
+define [
+	"base"
+	"ide/history/util/displayNameForUser"
+], (App, displayNameForUser) ->
+	historyEntryController = ($scope, $element, $attrs) ->
+		ctrl = @
+		ctrl.displayName = displayNameForUser
+		ctrl.getProjectOpDoc = (projectOp) ->
+			if projectOp.rename? then "#{ projectOp.rename.pathname} → #{ projectOp.rename.newPathname }"
+			else if projectOp.add? then "#{ projectOp.add.pathname}"
+			else if projectOp.remove? then "#{ projectOp.remove.pathname}"
+		ctrl.getUserCSSStyle = (user) ->
+			hue = user?.hue or 100
+			if ctrl.entry.inSelection 
+				color : "#FFF" 
+			else 
+				color: "hsl(#{ hue }, 70%, 50%)"
+		return
+
+	App.component "historyEntry", {
+		bindings:
+			entry: "<"
+			currentUser: "<"
+			onSelect: "&"
+		controller: historyEntryController
+		templateUrl: "historyEntryTpl"
+	}

+ 34 - 0
services/web/public/coffee/ide/history/components/historyFileEntity.coffee

@@ -0,0 +1,34 @@
+define [
+	"base"
+	"ide/file-tree/util/iconTypeFromName"
+], (App, iconTypeFromName) ->
+	# TODO Add arrows in folders
+	historyFileEntityController = ($scope, $element, $attrs) ->
+		ctrl = @
+		_handleFolderClick = () ->
+			ctrl.isOpen = !ctrl.isOpen
+			ctrl.iconClass = _getFolderIcon()
+		_handleFileClick = () ->
+			ctrl.historyFileTreeController.handleEntityClick ctrl.fileEntity
+		_getFolderIcon = () ->
+			if ctrl.isOpen then "fa-folder-open" else "fa-folder"
+		ctrl.$onInit = () ->
+			if ctrl.fileEntity.type == "folder"
+				ctrl.isOpen = true
+				ctrl.iconClass = _getFolderIcon()
+				ctrl.handleClick = _handleFolderClick
+			else
+				ctrl.iconClass = "fa-#{ iconTypeFromName(ctrl.fileEntity.name) }"
+				ctrl.handleClick = _handleFileClick
+				$scope.$watch (() -> ctrl.historyFileTreeController.selectedPathname), (newPathname) ->
+					ctrl.isSelected = ctrl.fileEntity.pathname == newPathname
+		return
+
+	App.component "historyFileEntity", {
+		require: 
+			historyFileTreeController: "^historyFileTree"
+		bindings:
+			fileEntity: "<"
+		controller: historyFileEntityController
+		templateUrl: "historyFileEntityTpl"
+	}

+ 18 - 0
services/web/public/coffee/ide/history/components/historyFileTree.coffee

@@ -0,0 +1,18 @@
+define [
+	"base"
+], (App) ->
+	historyFileTreeController = ($scope, $element, $attrs) ->
+		ctrl = @
+		ctrl.handleEntityClick = (file) ->
+			ctrl.onSelectedFileChange file: file
+		return
+
+	App.component "historyFileTree", {
+		bindings:
+			fileTree: "<"
+			selectedPathname: "<"
+			onSelectedFileChange: "&"
+			isLoading: "<"
+		controller: historyFileTreeController
+		templateUrl: "historyFileTreeTpl"
+	}

+ 1 - 1
services/web/public/coffee/ide/history/controllers/HistoryListController.coffee

@@ -5,7 +5,7 @@ define [
 
 	App.controller "HistoryListController", ["$scope", "ide", ($scope, ide) ->
 		$scope.hoveringOverListSelectors = false
-
+		
 		$scope.loadMore = () =>
 			ide.historyManager.fetchNextBatchOfUpdates()
 

+ 54 - 0
services/web/public/coffee/ide/history/controllers/HistoryV2FileTreeController.coffee

@@ -0,0 +1,54 @@
+define [
+	"base"
+], (App) ->
+
+	App.controller "HistoryV2FileTreeController", ["$scope", "ide", "_", ($scope, ide, _) ->
+		_previouslySelectedPathname = null
+		$scope.currentFileTree = []
+
+		_pathnameExistsInFiles = (pathname, files) -> 
+			_.any files, (file) -> file.pathname == pathname
+
+		_getSelectedDefaultPathname = (files) ->
+			selectedPathname = null
+			if _previouslySelectedPathname? and _pathnameExistsInFiles _previouslySelectedPathname, files
+				selectedPathname = _previouslySelectedPathname
+			else 
+				mainFile = _.find files, (file) -> /main\.tex$/.test file.pathname
+				if mainFile?
+					selectedPathname = _previouslySelectedPathname = mainFile.pathname
+				else
+					selectedPathname = _previouslySelectedPathname = files[0].pathname
+			return selectedPathname
+
+		$scope.handleFileSelection = (file) ->
+			$scope.history.selection.pathname = _previouslySelectedPathname = file.pathname
+
+		$scope.$watch 'history.files', (files) ->
+			if files? and files.length > 0
+				$scope.currentFileTree = _.reduce files, _reducePathsToTree, []
+				$scope.history.selection.pathname = _getSelectedDefaultPathname(files)
+
+		_reducePathsToTree = (currentFileTree, fileObject) ->
+			filePathParts = fileObject.pathname.split "/"
+			currentFileTreeLocation = currentFileTree
+			for pathPart, index in filePathParts
+				isFile = index == filePathParts.length - 1
+				if isFile
+					fileTreeEntity =
+						name: pathPart
+						pathname: fileObject.pathname
+						type: "file"
+						operation: fileObject.operation || "edited"
+					currentFileTreeLocation.push fileTreeEntity
+				else
+					fileTreeEntity = _.find currentFileTreeLocation, (entity) => entity.name == pathPart
+					if !fileTreeEntity?
+						fileTreeEntity = 
+							name: pathPart
+							type: "folder"
+							children: []
+						currentFileTreeLocation.push fileTreeEntity
+					currentFileTreeLocation = fileTreeEntity.children				
+			return currentFileTree
+	]

+ 76 - 0
services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee

@@ -0,0 +1,76 @@
+define [
+	"base",
+	"ide/history/util/displayNameForUser"
+], (App, displayNameForUser) ->
+
+	App.controller "HistoryV2ListController", ["$scope", "ide", ($scope, ide) ->
+		$scope.hoveringOverListSelectors = false
+		
+		$scope.loadMore = () =>
+			ide.historyManager.fetchNextBatchOfUpdates()
+
+		$scope.handleEntrySelect = (entry) ->
+			# $scope.$applyAsync () ->
+			ide.historyManager.selectUpdate(entry)
+			$scope.recalculateSelectedUpdates()
+
+		$scope.recalculateSelectedUpdates = () ->
+			beforeSelection = true
+			afterSelection = false
+			$scope.history.selection.updates = []
+			for update in $scope.history.updates
+				if update.selectedTo
+					inSelection = true
+					beforeSelection = false
+
+				update.beforeSelection = beforeSelection
+				update.inSelection = inSelection
+				update.afterSelection = afterSelection
+
+				if inSelection
+					$scope.history.selection.updates.push update
+
+				if update.selectedFrom
+					inSelection = false
+					afterSelection = true
+
+		$scope.recalculateHoveredUpdates = () ->
+			hoverSelectedFrom = false
+			hoverSelectedTo = false
+			for update in $scope.history.updates
+				# Figure out whether the to or from selector is hovered over
+				if update.hoverSelectedFrom
+					hoverSelectedFrom = true
+				if update.hoverSelectedTo
+					hoverSelectedTo = true
+
+			if hoverSelectedFrom
+				# We want to 'hover select' everything between hoverSelectedFrom and selectedTo
+				inHoverSelection = false
+				for update in $scope.history.updates
+					if update.selectedTo
+						update.hoverSelectedTo = true
+						inHoverSelection = true
+					update.inHoverSelection = inHoverSelection
+					if update.hoverSelectedFrom
+						inHoverSelection = false
+			if hoverSelectedTo
+				# We want to 'hover select' everything between hoverSelectedTo and selectedFrom
+				inHoverSelection = false
+				for update in $scope.history.updates
+					if update.hoverSelectedTo
+						inHoverSelection = true
+					update.inHoverSelection = inHoverSelection
+					if update.selectedFrom
+						update.hoverSelectedFrom = true
+						inHoverSelection = false
+
+		$scope.resetHoverState = () ->
+			for update in $scope.history.updates
+				delete update.hoverSelectedFrom
+				delete update.hoverSelectedTo
+				delete update.inHoverSelection
+
+		$scope.$watch "history.updates.length", () ->
+			$scope.recalculateSelectedUpdates()
+	]

+ 4 - 0
services/web/public/coffee/ide/history/util/HistoryViewModes.coffee

@@ -0,0 +1,4 @@
+define [], () ->
+	HistoryViewModes = 
+		POINT_IN_TIME : 'point_in_time'
+		COMPARE       : 'compare'

+ 7 - 0
services/web/public/coffee/main/event.coffee

@@ -50,6 +50,10 @@ define [
 			send: (category, action, label, value)->
 				ga('send', 'event', category, action, label, value)
 
+			sendGAOnce: (category, action, label, value) ->
+				if ! _eventInCache(action)
+					_addEventToCache(action)
+					@send category, action, label, value
 
 			editingSessionHeartbeat: () ->
 				return unless nextHeartbeat <= new Date()
@@ -86,6 +90,9 @@ define [
 				if ! _eventInCache(key)
 					_addEventToCache(key)
 					@sendMB key, segmentation
+
+			eventInCache: (key) ->
+				_eventInCache(key)
 		}
 
 

+ 4 - 0
services/web/public/coffee/main/new-subscription.coffee

@@ -9,6 +9,7 @@ define [
 
 		$scope.currencyCode = MultiCurrencyPricing.currencyCode
 		$scope.plans = MultiCurrencyPricing.plans
+		$scope.planCode = window.plan_code
 
 		$scope.switchToStudent = ()->
 			currentPlanCode = window.plan_code
@@ -234,3 +235,6 @@ define [
 			{code:'WK',name:'Wake Island'},{code:'WF',name:'Wallis and Futuna'},{code:'EH',name:'Western Sahara'},{code:'YE',name:'Yemen'},
 			{code:'ZM',name:'Zambia'},{code:'AX',name:'&angst;land Islandscode:'}
 		]
+
+		sixpack.participate 'plans', ['default', 'more-details'], (chosenVariation, rawResponse)->
+			$scope.plansVariant = chosenVariation

+ 40 - 29
services/web/public/coffee/main/plans.coffee

@@ -3,7 +3,6 @@ define [
 	"libs/recurly-4.8.5"
 ], (App, recurly) ->
 
-
 	App.factory "MultiCurrencyPricing", () ->
 
 		currencyCode = window.recomendedCurrency
@@ -146,17 +145,16 @@ define [
 		}
 
 
-	App.controller "PlansController", ($scope, $modal, event_tracking, abTestManager, MultiCurrencyPricing, $http, sixpack) ->
+	App.controller "PlansController", ($scope, $modal, event_tracking, abTestManager, MultiCurrencyPricing, $http, sixpack, $filter) ->
 
 		$scope.showPlans = false
-
-		$scope.plansVariant = 'default'
 		$scope.shouldABTestPlans = window.shouldABTestPlans
 
 		if $scope.shouldABTestPlans
-				$scope.showPlans = true
-		else
-			$scope.showPlans = true
+			sixpack.participate 'plans-details', ['default', 'more-details'], (chosenVariation, rawResponse)->
+				$scope.plansVariant = chosenVariation
+
+		$scope.showPlans = true
 
 		$scope.plans = MultiCurrencyPricing.plans
 
@@ -169,44 +167,57 @@ define [
 		$scope.ui =
 			view: "monthly"
 
-		$scope.changeCurreny = (newCurrency)->
+		$scope.changeCurreny = (e, newCurrency)->
+			e.preventDefault()
 			$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}"
+				return "collaborator-annual"
 			else
-				if variant == "default"
-					return "collaborator#{$scope.planQueryString}"
-				else
-					return "collaborator_#{variant}"
+				return "collaborator#{$scope.planQueryString}"
 
-		$scope.signUpNowClicked = (plan, annual)->
-			event_tracking.sendMB 'plans-page-start-trial', {plan}
+		$scope.signUpNowClicked = (plan, location)->
 			if $scope.ui.view == "annual"
 				plan = "#{plan}_annual"
-			event_tracking.send   'subscription-funnel', 'sign_up_now_button', plan
+			plan = eventLabel(plan, location)
+			event_tracking.sendMB 'plans-page-start-trial', {plan}
+			event_tracking.send 'subscription-funnel', 'sign_up_now_button', plan
+			if $scope.shouldABTestPlans
+				sixpack.convert 'plans-details'
 
-		$scope.switchToMonthly = ->
-			$scope.ui.view = "monthly"
-			event_tracking.send 'subscription-funnel', 'plans-page', 'monthly-prices'
+		$scope.switchToMonthly = (e, location) ->
+			uiView = 'monthly'
+			switchEvent(e, uiView + '-prices', location)
+			$scope.ui.view = uiView
 
-		$scope.switchToStudent = ->
-			$scope.ui.view = "student"
-			event_tracking.send 'subscription-funnel', 'plans-page', 'student-prices'
+		$scope.switchToStudent = (e, location) ->
+			uiView = 'student'
+			switchEvent(e, uiView + '-prices', location)
+			$scope.ui.view = uiView
 
-		$scope.switchToAnnual = ->
-			$scope.ui.view = "annual"
-			event_tracking.send 'subscription-funnel', 'plans-page', 'annual-prices'
+		$scope.switchToAnnual = (e, location) ->
+			uiView = 'annual'
+			switchEvent(e, uiView + '-prices', location)
+			$scope.ui.view = uiView
 
 		$scope.openGroupPlanModal = () ->
 			$modal.open {
 				templateUrl: "groupPlanModalTemplate"
 			}
 			event_tracking.send 'subscription-funnel', 'plans-page', 'group-inquiry-potential'
+
+		eventLabel = (label, location) ->
+			if location && $scope.plansVariant != 'default'
+				label = label + '-' + location
+			if $scope.plansVariant != 'default'
+				label += '-exp-' + $scope.plansVariant
+			label
+
+		switchEvent = (e, label, location) ->
+			e.preventDefault()
+			gaLabel = eventLabel(label, location)
+			event_tracking.send 'subscription-funnel', 'plans-page', gaLabel
+

+ 1 - 1
services/web/public/coffee/main/project-list/project-list.coffee

@@ -425,7 +425,7 @@ define [
 			for projectId in projectIds
 				queuedHttp {
 					method: "POST"
-					url: "/project/#{project_id}/restore"
+					url: "/project/#{projectId}/restore"
 					headers:
 						"X-CSRF-Token": window.csrfToken
 				}

BIN=BIN
services/web/public/img/advocates/erdogmus.jpg


BIN=BIN
services/web/public/img/advocates/henderson.jpg


+ 1 - 0
services/web/public/stylesheets/_style_includes.less

@@ -80,6 +80,7 @@
 @import "app/review-features-page.less";
 @import "app/error-pages.less";
 @import "app/v1-badge.less";
+@import "app/editor/history-v2.less";
 @import "app/metrics.less";
 
 // Vendor CSS

+ 5 - 1
services/web/public/stylesheets/app/editor.less

@@ -74,9 +74,13 @@
 
 #ide-body {
 	.full-size;
-	top: 40px;
+	top: @ide-body-top-offset;
+	&.ide-history-open {
+		top: @ide-body-top-offset + @editor-toolbar-height;
+	}
 }
 
+
 #editor, #editor-rich-text {
 	.full-size;
 }

+ 498 - 0
services/web/public/stylesheets/app/editor/history-v2.less

@@ -0,0 +1,498 @@
+.history-toolbar {
+	display: flex;
+	align-items: center;
+	position: absolute;
+	width: 100%;
+	top: @ide-body-top-offset;
+	height: @editor-toolbar-height;
+	line-height: 1;
+	font-size: @font-size-small;
+	background-color: @history-toolbar-bg-color;
+	z-index: 1;
+	color: @history-toolbar-color;
+	padding-left: (@line-height-computed / 2);
+}
+.history-toolbar when (@is-overleaf = false) {
+	border-bottom: @toolbar-border-bottom;
+}
+	.history-toolbar-time {
+		font-weight: bold;
+	}
+	.history-toolbar-btn {
+		.btn;
+		.btn-info;
+		.btn-xs;
+		padding-left: @padding-small-horizontal;
+		padding-right: @padding-small-horizontal;
+		margin-left: (@line-height-computed / 2);
+	}
+
+.history-entries {
+	font-size: @history-base-font-size;
+	color: @history-base-color;
+	height: 100%;
+	background-color: @history-base-bg;
+}
+
+.history-entry-day {
+	display: block;
+	background-color: @history-entry-day-bg;
+	color: #FFF;
+	padding: 5px 10px;
+	line-height: 1;
+}
+
+.history-entry-details {
+	background-color: #FFF;
+	margin-bottom: 2px;
+	padding: 5px 10px;
+	cursor: pointer;
+
+	.history-entry-selected & {
+		background-color: @history-entry-selected-bg;
+		color: #FFF;
+	}
+}
+	.history-entry-changes {
+		.list-unstyled;
+		margin-bottom: 3px;
+	}
+		.history-entry-change {
+
+		}
+			.history-entry-change-action {
+				margin-right: 0.5em;
+			}
+
+			.history-entry-change-doc {
+				color: @history-highlight-color;
+				font-weight: bold;
+				word-break: break-all;
+				.history-entry-selected & {
+					color: #FFF;
+				}
+			}
+	.history-entry-metadata {
+
+	}	
+		.history-entry-metadata-time {
+			white-space: nowrap;
+		}
+
+		.history-entry-metadata-users {
+			display: inline;
+			padding: 0;
+		}
+			.history-entry-metadata-user {
+				display: inline;
+				&::after {
+					content: ', ';
+				}
+				&:last-of-type::after {
+					content: none;
+				}
+			}
+
+.history-file-tree-inner {
+	.full-size;
+	overflow-y: auto;
+	background-color: @file-tree-bg;
+
+	.loading {
+		color: #FFF;
+		font-size: @history-base-font-size;
+		text-align: center;
+		font-family: @font-family-serif;
+	}
+}
+
+.history-file-tree-inner when (@is-overleaf = false) {
+	font-size: 0.8rem;
+}
+
+	.history-file-entity-wrapper {
+		color: #FFF;
+		margin-left: (@line-height-computed / 2);
+	}
+		.history-file-entity-link {
+			display: block;
+			position: relative;
+			color: @file-tree-item-color;
+			line-height: @file-tree-line-height;
+			&:hover {
+				background-color: @file-tree-item-hover-bg;
+				color: @file-tree-item-color;
+				text-decoration: none;
+			}
+			&:focus {
+				color: @file-tree-item-color;
+				outline: none;
+				text-decoration: none;
+			}
+			&:hover when (@is-overleaf = true) {
+				.fake-full-width-bg(@file-tree-item-hover-bg);
+			}
+		}
+		.history-file-entity-link-selected {
+			background-color: @file-tree-item-selected-bg;
+			font-weight: bold;
+			padding-right: 32px;
+			color: #FFF;
+			.fake-full-width-bg(@file-tree-item-selected-bg);
+			&:hover {
+				background-color: @file-tree-item-hover-bg;
+			}
+		}
+			.history-file-entity-icon {
+				color: @file-tree-item-icon-color;
+				font-size: 14px;
+				margin-right: .5em;
+				.history-file-entity-link-selected & {
+					color: #FFF;
+				}
+			}
+			.history-file-entity-name {
+				display: block;
+				max-width: 100%;
+				white-space: nowrap;
+				overflow: hidden;
+				text-overflow: ellipsis;
+			}
+
+		.history-file-entity-link-selected when (@is-overleaf = false) {
+			color: @brand-primary;
+			&:hover,
+			&:focus {
+				color: @brand-primary;
+			}
+			.history-file-entity-icon {
+				color: @brand-primary;
+			}
+		}
+// @changesListWidth:   250px;
+// @changesListPadding: @line-height-computed / 2;
+
+// @selector-padding-vertical: 10px;
+// @selector-padding-horizontal: @line-height-computed / 2;
+// @day-header-height: 24px;
+
+// @range-bar-color: @link-color;
+// @range-bar-selected-offset: 14px;
+
+// #history {
+// 	.upgrade-prompt {
+// 		position: absolute;
+// 		top: 0;
+// 		bottom: 0;
+// 		left: 0;
+// 		right: 0;
+// 		z-index: 100;
+// 		background-color: rgba(128,128,128,0.4);
+// 		.message {
+// 			margin: auto;
+// 			margin-top: 100px;
+// 			padding: (@line-height-computed / 2) @line-height-computed;
+// 			width: 400px;
+// 			background-color: white;
+// 			border-radius: 8px;
+// 		}
+// 		.message-wider {
+// 			width: 650px;
+// 			margin-top: 60px;
+// 			padding: 0;
+// 		}
+
+// 			.message-header {
+// 				.modal-header;
+// 			}
+
+// 			.message-body {
+// 				.modal-body;
+// 			}
+// 	}
+
+// 	.diff-panel {
+// 		.full-size;
+// 		margin-right: @changesListWidth;
+// 	}
+
+// 	.diff {
+// 		.full-size;
+// 		.toolbar {
+// 			padding: 3px;
+// 			.name {
+// 				float: left;
+// 				padding: 3px @line-height-computed / 4;
+// 				display: inline-block;
+// 			}
+// 		}
+// 		.diff-editor {
+// 			.full-size;
+// 			top: 40px;
+// 		}
+// 		.hide-ace-cursor {
+// 			.ace_active-line, .ace_cursor-layer, .ace_gutter-active-line {
+// 				display: none;
+// 			}
+// 		}
+// 		.diff-deleted {
+// 			padding: @line-height-computed;
+// 		}
+// 		.deleted-warning {
+// 			background-color: @brand-danger;
+// 			color: white;
+// 			padding: @line-height-computed / 2;
+// 			margin-right: @line-height-computed / 4;
+// 		}
+// 		&-binary {
+// 			.alert {
+// 				margin: @line-height-computed / 2;
+// 			}
+// 		}
+// 	}
+
+// 	aside.change-list {
+// 		border-left: 1px solid @editor-border-color;
+// 		height: 100%;
+// 		width: @changesListWidth;
+// 		position: absolute;
+// 		right: 0;
+
+// 		.loading {
+// 			text-align: center;
+// 			font-family: @font-family-serif;
+// 		}
+
+// 		ul {
+// 			li.change {
+// 				position: relative;
+// 				user-select: none;
+// 				-ms-user-select: none;
+// 				-moz-user-select: none;
+// 				-webkit-user-select: none;
+
+// 				.day {
+// 					background-color: #fafafa;
+// 					border-bottom: 1px solid @editor-border-color;
+// 					padding: 4px;
+// 					font-weight: bold;
+// 					text-align: center;
+// 					height: @day-header-height;
+// 					font-size: 14px;
+// 					line-height: 1;
+// 				}
+// 				.selectors {
+// 					input {
+// 						margin: 0;
+// 					}
+// 					position: absolute;
+// 					left: @selector-padding-horizontal;
+// 					top: 0;
+// 					bottom: 0;
+// 					width: 24px;
+// 					.selector-from {
+// 						position: absolute;
+// 						bottom: @selector-padding-vertical;
+// 						left: 0;
+// 						opacity: 0.8;
+// 					}
+// 					.selector-to {
+// 						position: absolute;
+// 						top: @selector-padding-vertical;
+// 						left: 0;
+// 						opacity: 0.8;
+// 					}
+// 					.range {
+// 						position: absolute;
+// 						left: 5px;
+// 						width: 4px;
+// 						top: 0;
+// 						bottom: 0;
+// 					}
+// 				}
+// 				.description {
+// 					padding: (@line-height-computed / 4);
+// 					padding-left: 38px;
+// 					min-height: 38px;
+// 					border-bottom: 1px solid @editor-border-color;
+// 					cursor: pointer;
+// 					&:hover {
+// 						background-color: @gray-lightest;
+// 					}
+// 				}
+// 				.users {
+// 					.user {
+// 						font-size: 0.8rem;
+// 						color: @gray;
+// 						text-transform: capitalize;
+// 						position: relative;
+// 						padding-left: 16px;
+// 						.color-square {
+// 							height: 12px;
+// 							width: 12px;
+// 							border-radius: 3px;
+// 							position: absolute;
+// 							left: 0;
+// 							bottom: 3px;
+// 						}
+// 						.name {
+// 							width: 94%;
+// 							white-space: nowrap;
+// 							overflow: hidden;
+// 							text-overflow: ellipsis;
+// 						}
+// 					}
+// 				}
+// 				.time {
+// 					float: right;
+// 					color: @gray;
+// 					display: inline-block;
+// 					padding-right: (@line-height-computed / 2);
+// 					font-size: 0.8rem;
+// 					line-height: @line-height-computed;
+// 				}
+// 				.doc {
+// 					font-size: 0.9rem;
+// 					font-weight: bold;
+// 				}
+// 				.action {
+// 					color: @gray;
+// 					text-transform: uppercase;
+// 					font-size: 0.7em;
+// 					margin-bottom: -2px;
+// 					margin-top: 2px;
+// 					&-edited {
+// 						margin-top: 0;
+// 					}
+// 				}
+// 			}
+// 			li.loading-changes, li.empty-message {
+// 				padding: 6px;
+// 				cursor: default;
+// 				&:hover {
+// 					background-color: inherit;
+// 				}
+// 			}
+// 			li.selected {
+// 				border-left: 4px solid @range-bar-color;
+// 				.day {
+// 					padding-left: 0;
+// 				}
+// 				.description {
+// 					padding-left: 34px;
+// 				}
+// 				.selectors {
+// 					left: @selector-padding-horizontal - 4px;
+// 					.range {
+// 						background-color: @range-bar-color;
+// 					}
+// 				}
+// 			}
+// 			li.selected-to {
+// 				.selectors {
+// 					.range {
+// 						top: @range-bar-selected-offset;
+// 					}
+// 					.selector-to {
+// 						opacity: 1;
+// 					}
+// 				}
+// 			}
+// 			li.selected-from {
+// 				.selectors {
+// 					.range {
+// 						bottom: @range-bar-selected-offset;
+// 					}
+// 					.selector-from {
+// 						opacity: 1;
+// 					}
+// 				}
+// 			}
+// 			li.first-in-day {
+// 				.selectors {
+// 					.selector-to {
+// 						top: @day-header-height + @selector-padding-vertical;
+// 					}
+// 				}
+// 			}
+// 			li.first-in-day.selected-to {
+// 				.selectors {
+// 					.range {
+// 						top: @day-header-height + @range-bar-selected-offset;
+// 					}
+// 				}
+// 			}
+// 		}
+// 		ul.hover-state {
+// 			li {
+// 				.selectors {
+// 					.range {
+// 						background-color: transparent;
+// 						top: 0;
+// 						bottom: 0;
+// 					}
+// 				}
+// 			}
+// 			li.hover-selected {
+// 				.selectors {
+// 					.range {
+// 						top: 0;
+// 						background-color: @gray-light;
+// 					}
+// 				}
+// 			}
+// 			li.hover-selected-to {
+// 				.selectors {
+// 					.range {
+// 						top: @range-bar-selected-offset;
+// 					}
+// 					.selector-to {
+// 						opacity: 1;
+// 					}
+// 				}
+// 			}
+// 			li.hover-selected-from {
+// 				.selectors {
+// 					.range {
+// 						bottom: @range-bar-selected-offset;
+// 					}
+// 					.selector-from {
+// 						opacity: 1;
+// 					}
+// 				}
+// 			}
+// 			li.first-in-day.hover-selected-to {
+// 				.selectors {
+// 					.range {
+// 						top: @day-header-height + @range-bar-selected-offset;
+// 					}
+// 				}
+// 			}
+// 		}
+// 	}
+// }
+
+// .diff-deleted {
+// 	padding-top: 15px;
+// }
+
+// .editor-dark {
+// 	#history {
+// 		aside.change-list {
+// 			border-color: @editor-dark-toolbar-border-color;
+			
+// 			ul li.change {
+// 				.day {
+// 					background-color: darken(@editor-dark-background-color, 10%);
+// 					border-bottom: 1px solid @editor-dark-toolbar-border-color;
+// 				}
+// 				.description {
+// 					border-bottom: 1px solid @editor-dark-toolbar-border-color;
+// 					&:hover {
+// 						background-color: black;
+// 					}
+// 				}
+// 			}
+// 		}
+// 	}
+// }

+ 13 - 8
services/web/public/stylesheets/app/editor/history.less

@@ -1,4 +1,4 @@
-@changesListWidth:   250px;
+@changesListWidth: 250px;
 @changesListPadding: @line-height-computed / 2;
 
 @selector-padding-vertical: 10px;
@@ -40,7 +40,8 @@
 			}
 	}
 
-	.diff-panel {
+	.diff-panel,
+	.point-in-time-panel {
 		.full-size;
 		margin-right: @changesListWidth;
 	}
@@ -49,6 +50,7 @@
 		.full-size;
 		.toolbar {
 			padding: 3px;
+			height: 32px;
 			.name {
 				float: left;
 				padding: 3px @line-height-computed / 4;
@@ -57,13 +59,9 @@
 		}
 		.diff-editor {
 			.full-size;
-			top: 40px;
-		}
-		.hide-ace-cursor {
-			.ace_active-line, .ace_cursor-layer, .ace_gutter-active-line {
-				display: none;
-			}
+			top: 32px;
 		}
+
 		.diff-deleted {
 			padding: @line-height-computed;
 		}
@@ -90,6 +88,7 @@
 		.loading {
 			text-align: center;
 			font-family: @font-family-serif;
+			margin-top: (@line-height-computed / 2);
 		}
 
 		ul {
@@ -305,6 +304,12 @@
 	padding-top: 15px;
 }
 
+.hide-ace-cursor {
+	.ace_active-line, .ace_cursor-layer, .ace_gutter-active-line {
+		display: none;
+	}
+}
+
 .editor-dark {
 	#history {
 		aside.change-list {

+ 264 - 1
services/web/public/stylesheets/app/plans.less

@@ -32,7 +32,7 @@
 			padding-bottom: @line-height-computed * 2;
 		}
 	}
-	
+
 	.circle {
 		font-size: 1.5rem;
 		font-weight: 700;
@@ -63,6 +63,12 @@
 	}
 
 	.card .btn { white-space:normal; }
+
+	.top-switch {
+		.currency-dropdown {
+			margin-right: -15px;
+		}
+	}
 }
 
 #changePlanSection {
@@ -127,4 +133,261 @@ input.paymentTypeOption.ng-valid {
 	text-align: right;
 }
 
+/**
+	Plans Test
+*/
+@best-val-height: 35px;
+@highlight-border: 3px;
+@highlight-color: #d3584b;
+@gray-med: #6d6d6d;
+@white-med: #fdfdfd;
+.more-details {
+	.best-value {
+		color: @red;
+		line-height: @line-height-computed;
+	}
+	blockquote {
+		footer{
+			/* accessibility fix */
+			color: @gray-med;
+		}
+	}
+	.btn-header {
+		font-family: @font-family-sans-serif;
+		margin-left: 10px;
+		margin-top: -10px;
+		text-shadow: 0 0 0;
+	}
+	.card-first, .card-last {
+		background: @white-med;
+	}
+	.card-highlighted {
+		border: @highlight-border solid @gray-lighter;
+		padding-top: 10px!important;
+		.best-value {
+			margin-bottom: 15px;
+		}
+		.card-header {
+			padding-bottom: 22px; /* align hr with other plans */
+		}
+	}
+	.card-header {
+		margin-bottom: 15px;
+	}
+	.circle {
+		/* accessibility fix */
+		span.small {
+			color: rgba(255, 255, 255, 0.85)
+		}
+	}
+	.circle-img {
+		border-radius: 50%;
+		float: right;
+		height: 100px;
+		overflow: hidden;
+		position: relative;
+		width: 100px;
+		img {
+			display: inline;
+			margin: 0 auto;
+			width: 100%;
+		}
+	}
+	.faq:last-child {
+		p {
+			margin-bottom: 0;
+		}
+	}
+	.questions-header {
+		color: @red;
+		line-height: 37px;
+		margin: 0;
+		text-align: right;
+	}
+	.tagline {
+		margin-bottom: 20px;
+	}
+	/* Media Queries */
+	@media (max-width: @screen-md-min) {
+		.card-highlighted {
+			/*override style in cards.less */
+			margin-top: @line-height-computed!important;
+		}
+		.circle-img {
+			float: left;
+			margin: 0 15px;
+		}
+	}
+	@media (min-width: @screen-md-min) {
+		blockquote {
+			margin-bottom: 0;
+		}
+		.faq {
+			.row:nth-child(2) {
+				h3 {
+					margin-top: 0;
+				}
+			}
+		}
+	}
+}
+
+.student-disclaimer {
+	font-size: 14px; /* match .paymentPageFeatures p */
+	color: @gray; /* match .paymentPageFeatures p */
+	margin: 12.5px 0 0 0;
+}
+
+/**
+	Plans Table
+*/
+.plans-table {
+	border: 1px solid @gray-lighter;
+	background-color: @white-med;
+	margin: @best-val-height 0 15px 0;
+	table-layout: fixed;
+	width: 100%;
+
+	th, td {
+		-moz-background-clip: padding;
+		-webkit-background-clip: padding;
+		background-clip: padding-box; /* needed for firefox when there is bg color */
+		border: 1px solid @gray-lighter;
+		padding: 6px;
+		text-align: center;
+		vertical-align: middle;
+	}
+
+	td {
+		font-weight: bold;
+	}
+
+	th {
+		border-top: 0;
+		font-family: @headings-font-family;
+		font-size: @font-size-h2;
+	  font-weight: @headings-font-weight;
+	  line-height: @headings-line-height;
+		padding: 18px;
+	}
+
+	th:first-child, td:first-child {
+		border-left: 0;
+	}
 
+	th:last-child, td:last-child {
+		border-right: 0;
+	}
+
+	td:first-child {
+		font-weight: bold;
+		padding-left: 18px;
+		text-align: left;
+	}
+
+	tr:first-child {
+		th {
+			position: relative;
+			/* keep here position here, otherwise messes up border on safari */
+		}
+	}
+
+	tr:last-child {
+		td {
+			border-bottom: 0;
+			padding: 18px;
+		}
+			/* highlighted column */
+		td:nth-child(3) {
+			position: relative;
+			/* keep here position here, otherwise messes up border on safari when there is a bg color */
+			&:before {
+				/* needed for safafi */
+				border-top: 1px solid @gray-lighter;
+				content: '';
+				left: 0;
+				position: absolute;
+				top: -1px;
+				width: 100%;
+			}
+		}
+		td:first-child {
+			border: 0;
+		}
+	}
+
+	.fa-check {
+		color: @green;
+	}
+
+	/* accessibility fixes */
+	.small {
+		color: @gray-med;
+	}
+
+	/* highlighted column */
+	td:nth-child(3), th:nth-child(3) {
+		background-color: white;
+		border-left: @highlight-border solid @gray-lighter;
+		border-right: @highlight-border solid @gray-lighter;
+	}
+	.outer {
+		left: -@highlight-border;
+		right: -@highlight-border;
+		position: absolute;
+
+		.outer-content {
+			background: white;
+			border: @highlight-border solid @gray-lighter;
+			border-radius: @border-radius-base;
+			font-size: @font-size-base;
+			font-family: @font-family-sans-serif;
+			font-weight: bold;
+			height: @best-val-height;
+			padding-top: 10px;
+		}
+	}
+	.outer.outer-top {
+		top: -@best-val-height;
+		.outer-content {
+			border-bottom-left-radius: 0;
+			border-bottom-right-radius: 0;
+			border-bottom: 0;
+		}
+	}
+	.outer.outer-btm {
+		bottom: -@best-val-height/2;
+		.outer-content {
+			border-top-left-radius: 0;
+			border-top-right-radius: 0;
+			border-top: 0;
+			height: @best-val-height/2;
+		}
+	}
+
+	/* highlight rows on hover */
+	tr:hover {
+		td {
+			background-color: @gray-lightest;
+		}
+	}
+	tr:first-child:hover {
+		background-color: transparent;
+	}
+	tr:last-child:hover {
+		background-color: transparent;
+		td {
+			background-color: transparent;
+		}
+	}
+
+	/* tooltip */
+	sup {
+		color: @red;
+		cursor: pointer;
+		margin-left: 5px;
+	}
+	.tooltip.in {
+		min-width: 200px
+	}
+}

+ 13 - 1
services/web/public/stylesheets/core/_common-variables.less

@@ -888,6 +888,7 @@
 @footer-padding                : 2em;
 
 // Editor header
+@ide-body-top-offset             : 40px;
 @toolbar-header-bg-color         : transparent;
 @toolbar-header-shadow           : 0 0 2px #ccc;
 @toolbar-btn-color               : @link-color;
@@ -972,4 +973,15 @@
 // System messages
 @sys-msg-background : @state-warning-bg;
 @sys-msg-color      : #333;
-@sys-msg-border     : 1px solid @common-border-color;
+@sys-msg-border     : 1px solid @common-border-color;
+
+// v2 History
+@history-base-font-size           : @font-size-small;
+@history-base-bg                  : @gray-lightest;
+@history-entry-day-bg             : @gray;
+@history-entry-selected-bg        : @red;
+@history-base-color               : @gray-light;
+@history-highlight-color          : @gray;
+@history-toolbar-bg-color         : @toolbar-alt-bg-color;
+@history-toolbar-color            : @text-color;
+

+ 11 - 0
services/web/public/stylesheets/core/ol-variables.less

@@ -265,6 +265,17 @@
 @log-line-no-color              : #FFF;
 @log-hints-color                : @ol-blue-gray-4;
 
+
+// v2 History
+@history-base-font-size          : @font-size-small;
+@history-base-bg                 : @ol-blue-gray-1;
+@history-entry-day-bg            : @ol-blue-gray-2;
+@history-entry-selected-bg       : @ol-green;
+@history-base-color              : @ol-blue-gray-2;
+@history-highlight-color         : @ol-type-color;
+@history-toolbar-bg-color        : @editor-toolbar-bg;
+@history-toolbar-color           : #FFF;
+
 // System messages
 @sys-msg-background             : @ol-blue;
 @sys-msg-color                  : #FFF;

+ 3 - 0
services/web/public/stylesheets/core/scaffolding.less

@@ -161,4 +161,7 @@ hr {
   margin-top: @line-height-computed / 2;
 }
 
+.row-spaced-large {
+  margin-top: @line-height-computed * 2;
+}
 

+ 13 - 1
services/web/public/stylesheets/core/type.less

@@ -122,6 +122,11 @@ cite    { font-style: normal; }
   text-align: center;
 }
 
+// Transformations
+.text-capitalize {
+  text-transform: capitalize;
+}
+
 // Contextual backgrounds
 // For now we'll leave these alongside the text classes until v4 when we can
 // safely shift things around (per SemVer rules).
@@ -256,7 +261,14 @@ blockquote {
       vertical-align: -0.4em;
       line-height: 0.1em;
   }
-  
+
+  &:after {
+      content: close-quote;
+      display: inherit;
+      height: 0;
+      visibility: hidden;
+  }
+
   p {
       display: inline;
   }

+ 1 - 1
services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee

@@ -12,6 +12,7 @@ ObjectId = require("mongojs").ObjectId
 
 describe "AuthenticationController", ->
 	beforeEach ->
+		tk.freeze(Date.now())
 		@AuthenticationController = SandboxedModule.require modulePath, requires:
 			"./AuthenticationManager": @AuthenticationManager = {}
 			"../User/UserGetter" : @UserGetter = {}
@@ -39,7 +40,6 @@ describe "AuthenticationController", ->
 		@req = new MockRequest()
 		@res = new MockResponse()
 		@callback = @next = sinon.stub()
-		tk.freeze(Date.now())
 
 	afterEach ->
 		tk.reset()

+ 1 - 1
services/web/test/unit/coffee/History/RestoreManagerTests.coffee

@@ -10,6 +10,7 @@ moment = require('moment')
 
 describe 'RestoreManager', ->
 	beforeEach ->
+		tk.freeze Date.now() # freeze the time for these tests
 		@RestoreManager = SandboxedModule.require modulePath, requires:
 			'../../infrastructure/FileWriter': @FileWriter = {}
 			'../Uploads/FileSystemImportManager': @FileSystemImportManager = {}
@@ -22,7 +23,6 @@ describe 'RestoreManager', ->
 		@project_id = 'mock-project-id'
 		@version = 42
 		@callback = sinon.stub()
-		tk.freeze Date.now() # freeze the time for these tests
 
 	afterEach ->
 		tk.reset()

+ 5 - 6
services/web/test/unit/coffee/Subscription/RecurlyWrapperTests.coffee

@@ -116,6 +116,7 @@ describe "RecurlyWrapper", ->
 					apiKey: 'nonsense'
 					privateKey: 'private_nonsense'
 
+		tk.freeze Date.now() # freeze the time for these tests
 		@RecurlyWrapper = RecurlyWrapper = SandboxedModule.require modulePath, requires:
 			"settings-sharelatex": @settings
 			"logger-sharelatex":
@@ -124,10 +125,11 @@ describe "RecurlyWrapper", ->
 				log:   sinon.stub()
 			"request": sinon.stub()
 
-	describe "sign", ->
+	after ->
+		tk.reset()
 
+	describe "sign", ->
 		before (done) ->
-			tk.freeze Date.now() # freeze the time for these tests
 			@RecurlyWrapper.sign({
 				subscription :
 					plan_code : "gold"
@@ -137,9 +139,6 @@ describe "RecurlyWrapper", ->
 				done()
 			)
 
-		after ->
-			tk.reset()
-
 		it "should be signed correctly", ->
 			signed = @signature.split("|")[0]
 			query = @signature.split("|")[1]
@@ -1061,4 +1060,4 @@ describe "RecurlyWrapper", ->
 				@RecurlyWrapper.listAccountActiveSubscriptions @user_id, @callback
 
 			it "should return an empty array of subscriptions", ->
-				@callback.calledWith(null, []).should.equal true
+				@callback.calledWith(null, []).should.equal true

+ 52 - 18
services/web/test/unit/coffee/Subscription/SubscriptionControllerTests.coffee

@@ -87,35 +87,69 @@ describe "SubscriptionController", ->
 		@stubbedCurrencyCode = "GBP"
 
 	describe "plansPage", ->
-		beforeEach (done) ->
+		beforeEach ->
 			@req.ip  = "1234.3123.3131.333 313.133.445.666 653.5345.5345.534"
 			@GeoIpLookup.getCurrencyCode.callsArgWith(1, null, @stubbedCurrencyCode)
-			@res.callback = done
-			@SubscriptionController.plansPage(@req, @res)
-			@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user)
 
-		it "should set the recommended currency from the geoiplookup", (done)->
-			@res.renderedVariables.recomendedCurrency.should.equal(@stubbedCurrencyCode)
-			@GeoIpLookup.getCurrencyCode.calledWith(@req.ip).should.equal true
-			done()
+		describe 'when user is logged in', (done) ->
+			beforeEach (done) ->
+				@res.callback = done
+				@SubscriptionController.plansPage(@req, @res)
+			it 'should fetch the current user', (done) ->
+				@UserGetter.getUser.callCount.should.equal 1
+				done()
 
-		it 'should fetch the current user', (done) ->
-			@UserGetter.getUser.callCount.should.equal 1
-			done()
+			it 'should decide not to AB test the plans when signed up before 2016-10-27', (done) ->
+				@res.renderedVariables.shouldABTestPlans.should.equal false
+				done()
 
-		it 'should decide not to AB test the plans', (done) ->
-			@res.renderedVariables.shouldABTestPlans.should.equal false
-			done()
+			describe 'not dependant on logged in state', (done) ->
+				# these could have been put in 'when user is not logged in' too
+				it "should set the recommended currency from the geoiplookup", (done)->
+					@res.renderedVariables.recomendedCurrency.should.equal(@stubbedCurrencyCode)
+					@GeoIpLookup.getCurrencyCode.calledWith(@req.ip).should.equal true
+					done()
+				it 'should include data for features table', (done) ->
+					# this is part of AB test. If default wins test, then remove this test
+					@res.renderedVariables.planFeatures.length.should.not.equal 0
+					done()
 
 		describe 'when user is not logged in', (done) ->
-
-			beforeEach ->
-				@AuthenticationController.getLoggedInUserId.returns(null)
+			beforeEach (done) ->
+				@UserGetter =
+					getUser: sinon.stub().callsArgWith(2, null, null)
+				@res.callback = done
+				@SubscriptionController.plansPage(@req, @res)
+				@AuthenticationController =
+					getLoggedInUser: sinon.stub().callsArgWith(1, null, null)
+					getLoggedInUserId: sinon.stub().returns(null)
+					getSessionUser: sinon.stub().returns(null)
+					isUserLoggedIn: sinon.stub().returns(false)
+
+				@SubscriptionController = SandboxedModule.require modulePath, requires:
+					'../Authentication/AuthenticationController': @AuthenticationController
+					'./SubscriptionHandler': @SubscriptionHandler
+					"./PlansLocator": @PlansLocator
+					'./SubscriptionViewModelBuilder': @SubscriptionViewModelBuilder
+					"./LimitationsManager": @LimitationsManager
+					"../../infrastructure/GeoIpLookup":@GeoIpLookup
+					"logger-sharelatex":
+						log:->
+						warn:->
+					"settings-sharelatex": @settings
+					"./SubscriptionDomainHandler":@SubscriptionDomainHandler
+					"../User/UserGetter": @UserGetter
+					"./RecurlyWrapper": @RecurlyWrapper = {}
+					"./FeaturesUpdater": @FeaturesUpdater = {}
 
 			it 'should not fetch the current user', (done) ->
 				@UserGetter.getUser.callCount.should.equal 0
 				done()
 
+			it 'should decide to AB test', (done) ->
+				@res.renderedVariables.shouldABTestPlans.should.equal true
+				done()
+
 	describe "paymentPage", ->
 		beforeEach ->
 			@req.headers = {}
@@ -460,4 +494,4 @@ describe "SubscriptionController", ->
 				@SubscriptionHandler.updateSubscription.calledWith(@user, "collaborator-annual", "COLLABORATORCODEHERE").should.equal true
 				done()
 
-			@SubscriptionController.processUpgradeToAnnualPlan @req, @res
+			@SubscriptionController.processUpgradeToAnnualPlan @req, @res