WIP
This commit is contained in:
@@ -7,10 +7,12 @@ class ActionsMenuCtrl extends PureCtrl {
|
||||
$scope,
|
||||
$timeout,
|
||||
actionsManager,
|
||||
godService
|
||||
) {
|
||||
super($timeout);
|
||||
this.$timeout = $timeout;
|
||||
this.actionsManager = actionsManager;
|
||||
this.godService = godService;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
@@ -64,7 +66,7 @@ class ActionsMenuCtrl extends PureCtrl {
|
||||
switch (action.verb) {
|
||||
case 'render': {
|
||||
const item = result.item;
|
||||
this.actionsManager.presentRevisionPreviewModal(
|
||||
this.godService.presentRevisionPreviewModal(
|
||||
item.uuid,
|
||||
item.content
|
||||
);
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { protocolManager, SNComponent, SFItem, SFModelManager } from 'snjs';
|
||||
import {
|
||||
PAYLOAD_SOURCE_REMOTE_ACTION_RETRIEVED,
|
||||
CONTENT_TYPE_NOTE,
|
||||
CONTENT_TYPE_COMPONENT
|
||||
} from 'snjs';
|
||||
import template from '%/directives/revision-preview-modal.pug';
|
||||
|
||||
class RevisionPreviewModalCtrl {
|
||||
@@ -6,44 +10,40 @@ class RevisionPreviewModalCtrl {
|
||||
constructor(
|
||||
$element,
|
||||
$scope,
|
||||
$timeout,
|
||||
syncManager,
|
||||
$timeout
|
||||
) {
|
||||
this.$element = $element;
|
||||
this.$scope = $scope;
|
||||
this.$timeout = $timeout;
|
||||
this.syncManager = syncManager;
|
||||
this.createNote();
|
||||
this.configureEditor();
|
||||
this.configure();
|
||||
$scope.$on('$destroy', () => {
|
||||
if (this.identifier) {
|
||||
this.application.componentManager.deregisterHandler(this.identifier);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createNote() {
|
||||
this.note = new SFItem({
|
||||
content: this.content,
|
||||
content_type: "Note"
|
||||
|
||||
async configure() {
|
||||
this.note = await this.application.createItem({
|
||||
contentType: CONTENT_TYPE_NOTE,
|
||||
content: this.content
|
||||
});
|
||||
}
|
||||
|
||||
configureEditor() {
|
||||
/**
|
||||
* Set UUID so editoForNote can find proper editor, but then generate new uuid
|
||||
* for note as not to save changes to original, if editor makes changes.
|
||||
*/
|
||||
this.note.uuid = this.uuid;
|
||||
const editorForNote = this.application.componentManager.editorForNote(this.note);
|
||||
this.note.uuid = protocolManager.crypto.generateUUIDSync();
|
||||
this.note.uuid = await this.application.generateUuid();
|
||||
if (editorForNote) {
|
||||
/**
|
||||
* Create temporary copy, as a lot of componentManager is uuid based, so might
|
||||
* interfere with active editor. Be sure to copy only the content, as the top level
|
||||
* editor object has non-copyable properties like .window, which cannot be transfered
|
||||
*/
|
||||
const editorCopy = new SNComponent({
|
||||
const editorCopy = await this.application.createItem({
|
||||
contentType: CONTENT_TYPE_COMPONENT,
|
||||
content: editorForNote.content
|
||||
});
|
||||
editorCopy.readonly = true;
|
||||
@@ -85,13 +85,12 @@ class RevisionPreviewModalCtrl {
|
||||
const uuid = this.uuid;
|
||||
item = this.application.findItem({uuid: uuid});
|
||||
item.content = Object.assign({}, this.content);
|
||||
this.modelManager.mapResponseItemsToLocalModels(
|
||||
[item],
|
||||
SFModelManager.MappingSourceRemoteActionRetrieved
|
||||
);
|
||||
await this.application.mergeItem({
|
||||
item: item,
|
||||
source: PAYLOAD_SOURCE_REMOTE_ACTION_RETRIEVED
|
||||
});
|
||||
}
|
||||
this.modelManager.setItemDirty(item);
|
||||
this.syncManager.sync();
|
||||
this.application.saveItem({item});
|
||||
this.dismiss();
|
||||
};
|
||||
|
||||
|
||||
@@ -4,16 +4,14 @@ class SessionHistoryMenuCtrl {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$timeout,
|
||||
actionsManager,
|
||||
alertManager,
|
||||
sessionHistory,
|
||||
godService,
|
||||
application,
|
||||
) {
|
||||
this.$timeout = $timeout;
|
||||
this.alertManager = alertManager;
|
||||
this.actionsManager = actionsManager;
|
||||
this.sessionHistory = sessionHistory;
|
||||
this.diskEnabled = this.sessionHistory.diskEnabled;
|
||||
this.autoOptimize = this.sessionHistory.autoOptimize;
|
||||
this.godService = godService;
|
||||
this.application = application;
|
||||
this.diskEnabled = this.application.historyManager.isDiskEnabled();
|
||||
this.autoOptimize = this.application.historyManager.isAutoOptimizeEnabled();
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
@@ -21,7 +19,7 @@ class SessionHistoryMenuCtrl {
|
||||
}
|
||||
|
||||
reloadHistory() {
|
||||
const history = this.sessionHistory.historyForItem(this.item);
|
||||
const history = this.application.historyManager.historyForItem(this.item);
|
||||
this.entries = history.entries.slice(0).sort((a, b) => {
|
||||
return a.item.updated_at < b.item.updated_at ? 1 : -1;
|
||||
});
|
||||
@@ -29,7 +27,7 @@ class SessionHistoryMenuCtrl {
|
||||
}
|
||||
|
||||
openRevision(revision) {
|
||||
this.actionsManager.presentRevisionPreviewModal(
|
||||
this.godService.presentRevisionPreviewModal(
|
||||
revision.item.uuid,
|
||||
revision.item.content
|
||||
);
|
||||
@@ -47,11 +45,11 @@ class SessionHistoryMenuCtrl {
|
||||
}
|
||||
|
||||
clearItemHistory() {
|
||||
this.alertManager.confirm({
|
||||
this.application.alertManager.confirm({
|
||||
text: "Are you sure you want to delete the local session history for this note?",
|
||||
destructive: true,
|
||||
onConfirm: () => {
|
||||
this.sessionHistory.clearHistoryForItem(this.item).then(() => {
|
||||
this.application.historyManager.clearHistoryForItem(this.item).then(() => {
|
||||
this.$timeout(() => {
|
||||
this.reloadHistory();
|
||||
});
|
||||
@@ -61,11 +59,11 @@ class SessionHistoryMenuCtrl {
|
||||
}
|
||||
|
||||
clearAllHistory() {
|
||||
this.alertManager.confirm({
|
||||
this.application.alertManager.confirm({
|
||||
text: "Are you sure you want to delete the local session history for all notes?",
|
||||
destructive: true,
|
||||
onConfirm: () => {
|
||||
this.sessionHistory.clearAllHistory().then(() => {
|
||||
this.application.historyManager.clearAllHistory().then(() => {
|
||||
this.$timeout(() => {
|
||||
this.reloadHistory();
|
||||
});
|
||||
@@ -76,14 +74,14 @@ class SessionHistoryMenuCtrl {
|
||||
|
||||
toggleDiskSaving() {
|
||||
const run = () => {
|
||||
this.sessionHistory.toggleDiskSaving().then(() => {
|
||||
this.application.historyManager.toggleDiskSaving().then(() => {
|
||||
this.$timeout(() => {
|
||||
this.diskEnabled = this.sessionHistory.diskEnabled;
|
||||
this.diskEnabled = this.application.historyManager.diskEnabled;
|
||||
});
|
||||
});
|
||||
};
|
||||
if (!this.sessionHistory.diskEnabled) {
|
||||
this.alertManager.confirm({
|
||||
if (!this.application.historyManager.diskEnabled) {
|
||||
this.application.alertManager.confirm({
|
||||
text: `Are you sure you want to save history to disk? This will decrease general
|
||||
performance, especially as you type. You are advised to disable this feature
|
||||
if you experience any lagging.`,
|
||||
@@ -96,9 +94,9 @@ class SessionHistoryMenuCtrl {
|
||||
}
|
||||
|
||||
toggleAutoOptimize() {
|
||||
this.sessionHistory.toggleAutoOptimize().then(() => {
|
||||
this.application.historyManager.toggleAutoOptimize().then(() => {
|
||||
this.$timeout(() => {
|
||||
this.autoOptimize = this.sessionHistory.autoOptimize;
|
||||
this.autoOptimize = this.application.historyManager.autoOptimize;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@ class SyncResolutionMenuCtrl {
|
||||
constructor(
|
||||
$timeout,
|
||||
archiveManager,
|
||||
syncManager,
|
||||
application
|
||||
) {
|
||||
this.$timeout = $timeout;
|
||||
this.archiveManager = archiveManager;
|
||||
this.syncManager = syncManager;
|
||||
this.application = application;
|
||||
this.status = {};
|
||||
}
|
||||
|
||||
@@ -24,11 +24,11 @@ class SyncResolutionMenuCtrl {
|
||||
|
||||
async performSyncResolution() {
|
||||
this.status.resolving = true;
|
||||
await this.syncManager.resolveOutOfSync();
|
||||
await this.application.resolveOutOfSync();
|
||||
this.$timeout(() => {
|
||||
this.status.resolving = false;
|
||||
this.status.attemptedResolution = true;
|
||||
if (this.syncManager.isOutOfSync()) {
|
||||
if (this.application.getSyncStatus().isOutOfSync()) {
|
||||
this.status.fail = true;
|
||||
} else {
|
||||
this.status.success = true;
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { SFItemHistoryEntry } from 'snjs';
|
||||
|
||||
export class NoteHistoryEntry extends SFItemHistoryEntry {
|
||||
|
||||
previewTitle() {
|
||||
return this.item.updated_at.toLocaleString();
|
||||
}
|
||||
|
||||
previewSubTitle() {
|
||||
if(!this.hasPreviousEntry) {
|
||||
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 or metadata changed";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,297 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import angular from 'angular';
|
||||
import { Action, SFModelManager, SFItemParams, protocolManager } from 'snjs';
|
||||
|
||||
export class ActionsManager {
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$compile,
|
||||
$rootScope,
|
||||
$timeout,
|
||||
alertManager,
|
||||
authManager,
|
||||
httpManager,
|
||||
modelManager,
|
||||
syncManager,
|
||||
) {
|
||||
this.$compile = $compile;
|
||||
this.$rootScope = $rootScope;
|
||||
this.$timeout = $timeout;
|
||||
this.alertManager = alertManager;
|
||||
this.authManager = authManager;
|
||||
this.httpManager = httpManager;
|
||||
this.modelManager = modelManager;
|
||||
this.syncManager = syncManager;
|
||||
/* Used when decrypting old items with new keys. This array is only kept in memory. */
|
||||
this.previousPasswords = [];
|
||||
}
|
||||
|
||||
get extensions() {
|
||||
return this.modelManager.validItemsForContentType('Extension');
|
||||
}
|
||||
|
||||
extensionsInContextOfItem(item) {
|
||||
return this.extensions.filter((ext) => {
|
||||
return _.includes(ext.supported_types, item.content_type) ||
|
||||
ext.actionsWithContextForItem(item).length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an extension in the context of a certain item.
|
||||
* The server then has the chance to respond with actions that are
|
||||
* relevant just to this item. The response extension is not saved,
|
||||
* just displayed as a one-time thing.
|
||||
*/
|
||||
async loadExtensionInContextOfItem(extension, item) {
|
||||
const params = {
|
||||
content_type: item.content_type,
|
||||
item_uuid: item.uuid
|
||||
};
|
||||
const emptyFunc = () => { };
|
||||
return this.httpManager.getAbsolute(
|
||||
extension.url,
|
||||
params,
|
||||
emptyFunc,
|
||||
emptyFunc
|
||||
).then((response) => {
|
||||
this.updateExtensionFromRemoteResponse(extension, response);
|
||||
return extension;
|
||||
}).catch((response) => {
|
||||
console.error("Error loading extension", response);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
updateExtensionFromRemoteResponse(extension, response) {
|
||||
if (response.description) {
|
||||
extension.description = response.description;
|
||||
}
|
||||
if (response.supported_types) {
|
||||
extension.supported_types = response.supported_types;
|
||||
}
|
||||
if (response.actions) {
|
||||
extension.actions = response.actions.map((action) => {
|
||||
return new Action(action);
|
||||
});
|
||||
} else {
|
||||
extension.actions = [];
|
||||
}
|
||||
}
|
||||
|
||||
async executeAction(action, extension, item) {
|
||||
action.running = true;
|
||||
let result;
|
||||
switch (action.verb) {
|
||||
case 'get':
|
||||
result = await this.handleGetAction(action);
|
||||
break;
|
||||
case 'render':
|
||||
result = await this.handleRenderAction(action);
|
||||
break;
|
||||
case 'show':
|
||||
result = await this.handleShowAction(action);
|
||||
break;
|
||||
case 'post':
|
||||
result = await this.handlePostAction(action, item, extension);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
action.lastExecuted = new Date();
|
||||
action.running = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
async decryptResponse(response, keys) {
|
||||
const responseItem = response.item;
|
||||
await protocolManager.decryptItem(responseItem, keys);
|
||||
if (!responseItem.errorDecrypting) {
|
||||
return {
|
||||
response: response,
|
||||
item: responseItem
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.auth_params) {
|
||||
/**
|
||||
* In some cases revisions were missing auth params.
|
||||
* Instruct the user to email us to get this remedied.
|
||||
*/
|
||||
this.alertManager.alert({
|
||||
text: `We were unable to decrypt this revision using your current keys,
|
||||
and this revision is missing metadata that would allow us to try different
|
||||
keys to decrypt it. This can likely be fixed with some manual intervention.
|
||||
Please email hello@standardnotes.org for assistance.`
|
||||
});
|
||||
return {};
|
||||
}
|
||||
|
||||
/* Try previous passwords */
|
||||
const triedPasswords = [];
|
||||
for (const passwordCandidate of this.previousPasswords) {
|
||||
if (triedPasswords.includes(passwordCandidate)) {
|
||||
continue;
|
||||
}
|
||||
triedPasswords.push(passwordCandidate);
|
||||
const keyResults = await protocolManager.computeEncryptionKeysForUser(
|
||||
passwordCandidate,
|
||||
response.auth_params
|
||||
);
|
||||
if (!keyResults) {
|
||||
continue;
|
||||
}
|
||||
const nestedResponse = await this.decryptResponse(
|
||||
response,
|
||||
keyResults
|
||||
);
|
||||
if (nestedResponse.item) {
|
||||
return nestedResponse;
|
||||
}
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
this.presentPasswordModal((password) => {
|
||||
this.previousPasswords.push(password);
|
||||
const result = this.decryptResponse(response, keys);
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async handlePostAction(action, item, extension) {
|
||||
const decrypted = action.access_type === 'decrypted';
|
||||
const itemParams = await this.outgoingParamsForItem(item, extension, decrypted);
|
||||
const params = {
|
||||
items: [itemParams]
|
||||
};
|
||||
/* Needed until SNJS detects null function */
|
||||
const emptyFunc = () => { };
|
||||
return this.httpManager.postAbsolute(
|
||||
action.url,
|
||||
params,
|
||||
emptyFunc,
|
||||
emptyFunc
|
||||
).then((response) => {
|
||||
action.error = false;
|
||||
return {response: response};
|
||||
}).catch((response) => {
|
||||
action.error = true;
|
||||
console.error("Action error response:", response);
|
||||
this.alertManager.alert({
|
||||
text: "An issue occurred while processing this action. Please try again."
|
||||
});
|
||||
return { response: response };
|
||||
});
|
||||
}
|
||||
|
||||
async handleShowAction(action) {
|
||||
const win = window.open(action.url, '_blank');
|
||||
if (win) {
|
||||
win.focus();
|
||||
}
|
||||
return { response: null };
|
||||
}
|
||||
|
||||
async handleGetAction(action) {
|
||||
/* Needed until SNJS detects null function */
|
||||
const emptyFunc = () => {};
|
||||
const onConfirm = async () => {
|
||||
return this.httpManager.getAbsolute(action.url, {}, emptyFunc, emptyFunc)
|
||||
.then(async (response) => {
|
||||
action.error = false;
|
||||
await this.decryptResponse(response, await this.authManager.keys());
|
||||
const items = await this.modelManager.mapResponseItemsToLocalModels(
|
||||
[response.item],
|
||||
SFModelManager.MappingSourceRemoteActionRetrieved
|
||||
);
|
||||
for (const mappedItem of items) {
|
||||
this.modelManager.setItemDirty(mappedItem, true);
|
||||
}
|
||||
this.syncManager.sync();
|
||||
return {
|
||||
response: response,
|
||||
item: response.item
|
||||
};
|
||||
}).catch((response) => {
|
||||
const error = (response && response.error)
|
||||
|| { message: "An issue occurred while processing this action. Please try again." };
|
||||
this.alertManager.alert({ text: error.message });
|
||||
action.error = true;
|
||||
return { error: error };
|
||||
});
|
||||
};
|
||||
return new Promise((resolve, reject) => {
|
||||
this.alertManager.confirm({
|
||||
text: "Are you sure you want to replace the current note contents with this action's results?",
|
||||
onConfirm: () => {
|
||||
onConfirm().then(resolve);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async handleRenderAction(action) {
|
||||
/* Needed until SNJS detects null function */
|
||||
const emptyFunc = () => {};
|
||||
return this.httpManager.getAbsolute(
|
||||
action.url,
|
||||
{},
|
||||
emptyFunc,
|
||||
emptyFunc
|
||||
).then(async (response) => {
|
||||
action.error = false;
|
||||
const result = await this.decryptResponse(response, await this.authManager.keys());
|
||||
const item = this.modelManager.createItem(result.item);
|
||||
return {
|
||||
response: result.response,
|
||||
item: item
|
||||
};
|
||||
}).catch((response) => {
|
||||
const error = (response && response.error)
|
||||
|| { message: "An issue occurred while processing this action. Please try again." };
|
||||
this.alertManager.alert({ text: error.message });
|
||||
action.error = true;
|
||||
return { error: error };
|
||||
});
|
||||
}
|
||||
|
||||
async outgoingParamsForItem(item, extension, decrypted = false) {
|
||||
let keys = await this.authManager.keys();
|
||||
if (decrypted) {
|
||||
keys = null;
|
||||
}
|
||||
const itemParams = new SFItemParams(
|
||||
item,
|
||||
keys,
|
||||
await this.authManager.getAuthParams()
|
||||
);
|
||||
return itemParams.paramsForExtension();
|
||||
}
|
||||
|
||||
presentRevisionPreviewModal(uuid, content) {
|
||||
const scope = this.$rootScope.$new(true);
|
||||
scope.uuid = uuid;
|
||||
scope.content = content;
|
||||
const el = this.$compile(
|
||||
`<revision-preview-modal uuid='uuid' content='content'
|
||||
class='sk-modal'></revision-preview-modal>`
|
||||
)(scope);
|
||||
angular.element(document.body).append(el);
|
||||
}
|
||||
|
||||
presentPasswordModal(callback) {
|
||||
const scope = this.$rootScope.$new(true);
|
||||
scope.type = "password";
|
||||
scope.title = "Decryption Assistance";
|
||||
scope.message = `Unable to decrypt this item with your current keys.
|
||||
Please enter your account password at the time of this revision.`;
|
||||
scope.callback = callback;
|
||||
const el = this.$compile(
|
||||
`<input-modal type='type' message='message'
|
||||
title='title' callback='callback'></input-modal>`
|
||||
)(scope);
|
||||
angular.element(document.body).append(el);
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,12 @@ export class GodService {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$rootScope,
|
||||
$compile
|
||||
$compile,
|
||||
application
|
||||
) {
|
||||
this.$rootScope = $rootScope;
|
||||
this.$compile = $compile;
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
async checkForSecurityUpdate() {
|
||||
@@ -16,7 +18,7 @@ export class GodService {
|
||||
return false;
|
||||
}
|
||||
|
||||
const latest = protocolManager.version();
|
||||
const latest = await this.application.getUserVersion();
|
||||
const updateAvailable = await this.protocolVersion() !== latest;
|
||||
if (updateAvailable !== this.securityUpdateAvailable) {
|
||||
this.securityUpdateAvailable = updateAvailable;
|
||||
@@ -70,4 +72,29 @@ export class GodService {
|
||||
authenticationInProgress() {
|
||||
return this.currentAuthenticationElement != null;
|
||||
}
|
||||
|
||||
presentPasswordModal(callback) {
|
||||
const scope = this.$rootScope.$new(true);
|
||||
scope.type = "password";
|
||||
scope.title = "Decryption Assistance";
|
||||
scope.message = `Unable to decrypt this item with your current keys.
|
||||
Please enter your account password at the time of this revision.`;
|
||||
scope.callback = callback;
|
||||
const el = this.$compile(
|
||||
`<input-modal type='type' message='message'
|
||||
title='title' callback='callback'></input-modal>`
|
||||
)(scope);
|
||||
angular.element(document.body).append(el);
|
||||
}
|
||||
|
||||
presentRevisionPreviewModal(uuid, content) {
|
||||
const scope = this.$rootScope.$new(true);
|
||||
scope.uuid = uuid;
|
||||
scope.content = content;
|
||||
const el = this.$compile(
|
||||
`<revision-preview-modal uuid='uuid' content='content'
|
||||
class='sk-modal'></revision-preview-modal>`
|
||||
)(scope);
|
||||
angular.element(document.body).append(el);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,12 @@ export class WebDeviceInterface extends DeviceInterface {
|
||||
localStorage.clear();
|
||||
}
|
||||
|
||||
openUrl(url) {
|
||||
const win = window.open(url, '_blank');
|
||||
if (win) {
|
||||
win.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/** @database */
|
||||
|
||||
|
||||
4995
dist/javascripts/app.js
vendored
4995
dist/javascripts/app.js
vendored
File diff suppressed because one or more lines are too long
2
dist/javascripts/app.js.map
vendored
2
dist/javascripts/app.js.map
vendored
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user