Session history

This commit is contained in:
Mo Bitar
2018-07-19 22:25:14 -05:00
parent 8a127f97a1
commit 1250f3fd5e
13 changed files with 456 additions and 843 deletions

View File

@@ -22,7 +22,9 @@ angular.module('app')
}
}
})
.controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, actionsManager, syncManager, modelManager, themeManager, componentManager, storageManager) {
.controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, actionsManager, syncManager, modelManager, themeManager, componentManager, storageManager, sessionHistory) {
this.showSessionHistory = true;
this.spellcheck = true;
this.componentManager = componentManager;

View File

@@ -7,7 +7,7 @@ class MenuRow {
this.scope = {
circle: "=",
label: "=",
subtite: "=",
subtitle: "=",
hasButton: "=",
buttonText: "=",
buttonClass: "=",

View File

@@ -0,0 +1,49 @@
class RevisionPreviewModal {
constructor() {
this.restrict = "E";
this.templateUrl = "directives/revision-preview-modal.html";
this.scope = {
revision: "=",
show: "=",
callback: "="
};
}
link($scope, el, attrs) {
$scope.dismiss = function() {
el.remove();
}
}
controller($scope, modelManager, syncManager) {
'ngInject';
$scope.restore = function(asCopy) {
if(!asCopy && !confirm("Are you sure you want to replace the current note's contents with what you see in this preview?")) {
return;
}
var item;
if(asCopy) {
var contentCopy = Object.assign({}, $scope.revision.content);
if(contentCopy.title) { contentCopy.title += " (copy)"; }
item = modelManager.createItem({content_type: "Note", content: contentCopy});
modelManager.addItem(item);
} else {
item = modelManager.findItem($scope.revision.itemUuid);
item.content = Object.assign({}, $scope.revision.content);
item.mapContentToLocalProperties(item.content);
}
item.setDirty(true);
syncManager.sync();
$scope.dismiss();
}
}
}
angular.module('app').directive('revisionPreviewModal', () => new RevisionPreviewModal);

View File

@@ -0,0 +1,72 @@
class SessionHistoryMenu {
constructor() {
this.restrict = "E";
this.templateUrl = "directives/session-history-menu.html";
this.scope = {
item: "="
};
}
controller($scope, modelManager, sessionHistory, actionsManager, $timeout) {
'ngInject';
$scope.diskEnabled = sessionHistory.diskEnabled;
$scope.reloadHistory = function() {
$scope.history = sessionHistory.historyForItem($scope.item);
}
$scope.reloadHistory();
$scope.openRevision = function(revision) {
actionsManager.presentRevisionPreviewModal(revision);
}
$scope.classForRevision = function(revision) {
var vector = revision.operationVector();
if(vector == 0) {
return "default";
} else if(vector == 1) {
return "success";
} else if(vector == -1) {
return "danger";
}
}
$scope.clearItemHistory = function() {
if(!confirm("Are you sure you want to delete the local session history for this note?")) {
return;
}
sessionHistory.clearItemHistory($scope.item).then(() => {
$timeout(() => {
$scope.reloadHistory();
})
});
}
$scope.clearAllHistory = function() {
if(!confirm("Are you sure you want to delete the local session history for all notes?")) {
return;
}
sessionHistory.clearAllHistory().then(() => {
$timeout(() => {
$scope.reloadHistory();
})
});
}
$scope.toggleDiskSaving = function() {
sessionHistory.toggleDiskSaving().then(() => {
$timeout(() => {
$scope.diskEnabled = sessionHistory.diskEnabled;
})
});
}
}
}
angular.module('app').directive('sessionHistoryMenu', () => new SessionHistoryMenu);

View File

@@ -198,8 +198,14 @@ class ActionsManager {
})
}
presentPasswordModal(callback) {
presentRevisionPreviewModal(revision) {
var scope = this.$rootScope.$new(true);
scope.revision = revision;
var el = this.$compile( "<revision-preview-modal revision='revision' callback='callback' class='modal'></revision-preview-modal>" )(scope);
angular.element(document.body).append(el);
}
presentPasswordModal(callback) {
var scope = this.$rootScope.$new(true);
scope.type = "password";
scope.title = "Decryption Assistance";

View File

@@ -0,0 +1,213 @@
class SessionHistory {
constructor(modelManager, storageManager, authManager, passcodeManager, $timeout) {
this.modelManager = modelManager;
this.storageManager = storageManager;
this.authManager = authManager;
this.passcodeManager = passcodeManager;
this.$timeout = $timeout;
this.loadFromDisk().then(() => {
this.modelManager.addItemSyncObserver("session-history", "Note", (allItems, validItems, deletedItems, source, sourceKey) => {
for(let item of allItems) {
this.addRevision(item);
}
});
})
}
async encryptionParams() {
let offline = this.authManager.offline();
let auth_params = offline ? this.passcodeManager.passcodeAuthParams() : await this.authManager.getAuthParams();
let keys = offline ? this.passcodeManager.keys() : await this.authManager.keys();
return {keys, auth_params};
}
addRevision(item) {
var added = this.historyContainer.addRevision(item);
if(added) {
if(this.diskTimeout) {this.$timeout.cancel(this.diskTimeout)};
this.diskTimeout = this.$timeout(() => {
this.saveToDisk();
}, 1000)
}
}
historyForItem(item) {
return this.historyContainer.historyForItem(item);
}
async clearItemHistory(item) {
delete this.historyContainer.clearItemHistory(item);
return this.saveToDisk();
}
async clearAllHistory() {
this.historyContainer.clearAllHistory();
return this.storageManager.removeItem("sessionHistory");
}
async toggleDiskSaving() {
this.diskEnabled = !this.diskEnabled;
if(this.diskEnabled) {
this.storageManager.setItem("persistSessionHistory", JSON.stringify(true));
this.saveToDisk();
} else {
this.storageManager.removeItem("persistSessionHistory");
}
}
async saveToDisk() {
if(!this.diskEnabled) {
return;
}
let encryptionParams = await this.encryptionParams();
var itemParams = new SFItemParams(this.historyContainer, encryptionParams.keys, encryptionParams.auth_params);
itemParams.paramsForSync().then((syncParams) => {
console.log("Saving to disk", syncParams);
this.storageManager.setItem("sessionHistory", JSON.stringify(syncParams));
})
}
async loadFromDisk() {
var diskValue = await this.storageManager.getItem("persistSessionHistory");
if(diskValue) {
this.diskEnabled = JSON.parse(diskValue);
}
var historyValue = await this.storageManager.getItem("sessionHistory");
if(historyValue) {
historyValue = JSON.parse(historyValue);
let encryptionParams = await this.encryptionParams();
await SFJS.itemTransformer.decryptItem(historyValue, encryptionParams.keys);
var historyContainer = new HistoryContainer(historyValue);
this.historyContainer = historyContainer;
} else {
this.historyContainer = new HistoryContainer();
}
}
}
class HistoryContainer extends SFItem {
constructor(json_obj) {
super(json_obj);
if(!this.content.itemsDictionary) {
this.content.itemsDictionary = {};
}
var objectKeys = Object.keys(this.content.itemsDictionary);
objectKeys.forEach((key) => {
var value = this.content.itemsDictionary[key];
this.content.itemsDictionary[key] = new ItemHistory(value);
});
}
addRevision(item) {
if(!this.content.itemsDictionary[item.uuid]) {
this.content.itemsDictionary[item.uuid] = new ItemHistory();
}
var itemHistory = this.content.itemsDictionary[item.uuid];
return itemHistory.addRevision(item);
}
historyForItem(item) {
return this.content.itemsDictionary[item.uuid];
}
clearItemHistory(item) {
delete this.content.itemsDictionary[item.uuid];
}
clearAllHistory() {
this.content.itemsDictionary = {};
}
}
class ItemHistory {
constructor(json_obj = {}) {
if(!this.revisions) {
this.revisions = [];
}
if(json_obj.revisions) {
for(var revision of json_obj.revisions) {
this.revisions.push(new NoteRevision(revision, this.revisions[this.revisions.length - 1], revision.date));
}
}
}
addRevision(item) {
var previousRevision = this.revisions[this.revisions.length - 1];
var prospectiveRevision = new NoteRevision(item, previousRevision, item.updated_at);
if(prospectiveRevision.isSameAsRevision(previousRevision)) {
return;
}
this.revisions.push(prospectiveRevision);
return prospectiveRevision;
}
}
class ItemRevision {
constructor(item, previousRevision, date) {
if(typeof(date) == "string") {
this.date = new Date(date);
} else {
this.date = date;
}
this.itemUuid = item.uuid;
this.hasPreviousRevision = previousRevision != null;
this.content = Object.assign({}, item.content);
}
isSameAsRevision(revision) {
if(!revision) {
return false;
}
return JSON.stringify(this.content) === JSON.stringify(revision.content);
}
}
class NoteRevision extends ItemRevision {
constructor(item, previousRevision, date) {
super(item, previousRevision, date);
if(previousRevision) {
this.textCharDiffLength = this.content.text.length - previousRevision.content.text.length;
} else {
this.textCharDiffLength = this.content.text.length;
}
}
previewTitle() {
return this.date.toLocaleString();
}
operationVector() {
if(!this.hasPreviousRevision || this.textCharDiffLength == 0) {
return 0;
} else if(this.textCharDiffLength < 0) {
return -1;
} else {
return 1;
}
}
previewSubTitle() {
if(!this.hasPreviousRevision) {
return `${this.textCharDiffLength} characters loaded`
} else if(this.textCharDiffLength < 0) {
return `${this.textCharDiffLength * -1} characters removed`
} else if(this.textCharDiffLength > 0) {
return `${this.textCharDiffLength} characters added`
} else {
return "Title changed"
}
}
}
angular.module('app').service('sessionHistory', SessionHistory);

View File

@@ -16,6 +16,13 @@
font-size: 16px;
}
#item-preview-modal {
> .content {
width: 800px;
height: 500px;
}
}
.panel {
background-color: white;
}

View File

@@ -53,3 +53,9 @@
color: $blue-color;
}
}
#session-history-menu {
.menu-panel .row .sublabel.opaque {
opacity: 1.0
}
}

View File

@@ -0,0 +1,13 @@
.modal.medium#item-preview-modal
.content
.sn-component
.panel
.header
%h1.title Preview
.horizontal-group
%a.close-button.info{"ng-click" => "restore(false)"} Restore
%a.close-button.info{"ng-click" => "restore(true)"} Restore as copy
%a.close-button.info{"ng-click" => "dismiss(); $event.stopPropagation()"} Close
.content.selectable
%h2 {{revision.content.title}}
%p.normal{"style" => "white-space: pre-wrap; font-size: 16px;"} {{revision.content.text}}

View File

@@ -0,0 +1,31 @@
.sn-component#session-history-menu
.menu-panel.dropdown-menu
.header
.column
%h4.title {{history.revisions.length || 'No'}} revisions
%h4{"ng-click" => "showOptions = !showOptions; $event.stopPropagation();"}
%a Options
%div{"ng-if" => "showOptions"}
%menu-row{"label" => "'Clear note local history'", "ng-click" => "clearItemHistory(); $event.stopPropagation();"}
%menu-row{"label" => "'Clear all local history'", "ng-click" => "clearAllHistory(); $event.stopPropagation();"}
%menu-row{"label" => "(diskEnabled ? 'Disable' : 'Enable') + ' saving history to disk'", "ng-click" => "toggleDiskSaving(); $event.stopPropagation();"}
.sublabel
May increase app loading speed and memory footprint.
%menu-row{"ng-repeat" => "revision in history.revisions",
"ng-click" => "openRevision(revision); $event.stopPropagation();",
"label" => "revision.previewTitle()"}
.sublabel.opaque{"ng-class" => "classForRevision(revision)"}
{{revision.previewSubTitle()}}
.modal.medium-text.medium{"ng-if" => "renderData.showRenderModal", "ng-click" => "$event.stopPropagation();"}
.content
.sn-component
.panel
.header
%h1.title Preview
%a.close-button.info{"ng-click" => "renderData.showRenderModal = false; $event.stopPropagation();"} Close
.content.selectable
%h2 {{renderData.title}}
%p.normal{"style" => "white-space: pre-wrap; font-size: 16px;"} {{renderData.text}}

View File

@@ -50,6 +50,10 @@
.label Actions
%actions-menu{"ng-if" => "ctrl.showExtensions", "item" => "ctrl.note"}
.item{"ng-click" => "ctrl.showSessionHistory = !ctrl.showSessionHistory; ctrl.showMenu = false; ctrl.showEditorMenu = false;", "click-outside" => "ctrl.showSessionHistory = false;", "is-open" => "ctrl.showSessionHistory"}
.label Session History
%session-history-menu{"ng-if" => "ctrl.showSessionHistory", "item" => "ctrl.note"}
.editor-content#editor-content{"ng-if" => "ctrl.noteReady && !ctrl.note.errorDecrypting"}
%panel-resizer.left{"panel-id" => "'editor-content'", "on-resize-finish" => "ctrl.onPanelResizeFinish","control" => "ctrl.resizeControl", "min-width" => 300, "property" => "'left'", "hoverable" => "true"}

888
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -38,7 +38,7 @@
"karma-phantomjs-launcher": "^1.0.2",
"sn-stylekit": "1.0.15",
"standard-file-js": "0.3.2",
"sn-models": "0.1.0",
"sn-models": "file:~/Desktop/sn-models",
"connect": "^3.6.6",
"mocha": "^5.2.0",
"serve-static": "^1.13.2",