Merge pull request #162 from standardnotes/2.0.2

2.0.2
This commit is contained in:
Mo Bitar
2017-11-04 19:02:20 -07:00
committed by GitHub
26 changed files with 294 additions and 200 deletions

View File

@@ -352,6 +352,7 @@ angular.module('app.frontend')
this.onNameBlur = function() {
this.editingName = false;
this.updateTagsFromTagsString()
}
this.toggleFullScreen = function() {
@@ -366,7 +367,8 @@ angular.module('app.frontend')
}
this.deleteNote = function() {
if(confirm("Are you sure you want to delete this note?")) {
let title = this.note.safeTitle().length ? `'${this.note.title}'` : "this note";
if(confirm(`Are you sure you want to delete ${title}?`)) {
this.remove()(this.note);
this.showMenu = false;
}
@@ -430,9 +432,7 @@ angular.module('app.frontend')
this.loadTagsString();
}
this.updateTagsFromTagsString = function($event) {
$event.target.blur();
this.updateTagsFromTagsString = function() {
var tags = this.tagsString.split("#");
tags = _.filter(tags, function(tag){
return tag.length > 0;

View File

@@ -80,7 +80,9 @@ angular.module('app.frontend')
this.showMenu = false;
if(this.selectedNote && this.selectedNote.dummy) {
_.remove(oldTag.notes, this.selectedNote);
if(oldTag) {
_.remove(oldTag.notes, this.selectedNote);
}
}
this.noteFilter.text = "";

View File

@@ -66,6 +66,7 @@ class Item {
reference.setDirty(true);
})
}
addObserver(observer, callback) {
if(!_.find(this.observers, observer)) {
this.observers.push({observer: observer, callback: callback});
@@ -116,7 +117,7 @@ class Item {
}
removeAllRelationships() {
removeAndDirtyAllRelationships() {
// must override
this.setDirty(true);
}

View File

@@ -4,9 +4,9 @@ class SyncAdapter extends Item {
super(json_obj);
}
mapContentToLocalProperties(contentObject) {
super.mapContentToLocalProperties(contentObject)
this.url = contentObject.url;
mapContentToLocalProperties(content) {
super.mapContentToLocalProperties(content)
this.url = content.url;
}
structureParams() {

View File

@@ -12,22 +12,22 @@ class Component extends Item {
}
}
mapContentToLocalProperties(contentObject) {
super.mapContentToLocalProperties(contentObject)
this.url = contentObject.url;
this.name = contentObject.name;
mapContentToLocalProperties(content) {
super.mapContentToLocalProperties(content)
this.url = content.url;
this.name = content.name;
// the location in the view this component is located in. Valid values are currently tags-list, note-tags, and editor-stack`
this.area = contentObject.area;
this.area = content.area;
this.permissions = contentObject.permissions;
this.active = contentObject.active;
this.permissions = content.permissions;
this.active = content.active;
// custom data that a component can store in itself
this.componentData = contentObject.componentData || {};
this.componentData = content.componentData || {};
// items that have requested a component to be disabled in its context
this.disassociatedItemIds = contentObject.disassociatedItemIds || [];
this.disassociatedItemIds = content.disassociatedItemIds || [];
}
structureParams() {

View File

@@ -10,13 +10,13 @@ class Editor extends Item {
}
}
mapContentToLocalProperties(contentObject) {
super.mapContentToLocalProperties(contentObject)
this.url = contentObject.url;
this.name = contentObject.name;
this.data = contentObject.data || {};
this.default = contentObject.default;
this.systemEditor = contentObject.systemEditor;
mapContentToLocalProperties(content) {
super.mapContentToLocalProperties(content)
this.url = content.url;
this.name = content.name;
this.data = content.data || {};
this.default = content.default;
this.systemEditor = content.systemEditor;
}
structureParams() {
@@ -56,8 +56,8 @@ class Editor extends Item {
super.removeItemAsRelationship(item);
}
removeAllRelationships() {
super.removeAllRelationships();
removeAndDirtyAllRelationships() {
super.removeAndDirtyAllRelationships();
this.notes = [];
}

View File

@@ -51,16 +51,21 @@ class Action {
class Extension extends Item {
constructor(json) {
super(json);
_.merge(this, json);
this.encrypted = true;
this.content_type = "Extension";
if(this.encrypted === null || this.encrypted === undefined) {
// Default to encrypted on creation.
this.encrypted = true;
}
if(json.actions) {
this.actions = json.actions.map(function(action){
return new Action(action);
})
}
if(!this.actions) {
this.actions = [];
}
}
actionsInGlobalContext() {
@@ -75,39 +80,42 @@ class Extension extends Item {
})
}
mapContentToLocalProperties(contentObject) {
super.mapContentToLocalProperties(contentObject)
this.name = contentObject.name;
this.description = contentObject.description;
this.url = contentObject.url;
this.supported_types = contentObject.supported_types;
if(contentObject.actions) {
this.actions = contentObject.actions.map(function(action){
mapContentToLocalProperties(content) {
super.mapContentToLocalProperties(content)
this.name = content.name;
this.description = content.description;
this.url = content.url;
if(content.encrypted !== null && content.encrypted !== undefined) {
this.encrypted = content.encrypted;
} else {
this.encrypted = true;
}
this.supported_types = content.supported_types;
if(content.actions) {
this.actions = content.actions.map(function(action){
return new Action(action);
})
} else {
this.actions = [];
}
}
updateFromExternalResponseItem(externalResponseItem) {
_.merge(this, externalResponseItem);
this.actions = externalResponseItem.actions.map(function(action){
return new Action(action);
})
}
referenceParams() {
return null;
}
get content_type() {
return "Extension";
}
structureParams() {
var params = {
name: this.name,
url: this.url,
description: this.description,
actions: this.actions,
supported_types: this.supported_types
supported_types: this.supported_types,
encrypted: this.encrypted
};
_.merge(params, super.structureParams());

View File

@@ -8,10 +8,10 @@ class Note extends Item {
}
}
mapContentToLocalProperties(contentObject) {
super.mapContentToLocalProperties(contentObject)
this.title = contentObject.title;
this.text = contentObject.text;
mapContentToLocalProperties(content) {
super.mapContentToLocalProperties(content)
this.title = content.title;
this.text = content.text;
}
referenceParams() {
@@ -48,7 +48,7 @@ class Note extends Item {
super.removeItemAsRelationship(item);
}
removeAllRelationships() {
removeAndDirtyAllRelationships() {
this.tags.forEach(function(tag){
_.pull(tag.notes, this);
tag.setDirty(true);

View File

@@ -8,9 +8,9 @@ class Tag extends Item {
}
}
mapContentToLocalProperties(contentObject) {
super.mapContentToLocalProperties(contentObject)
this.title = contentObject.title;
mapContentToLocalProperties(content) {
super.mapContentToLocalProperties(content)
this.title = content.title;
}
referenceParams() {
@@ -46,7 +46,7 @@ class Tag extends Item {
super.removeItemAsRelationship(item);
}
removeAllRelationships() {
removeAndDirtyAllRelationships() {
this.notes.forEach(function(note){
_.pull(note.tags, this);
note.setDirty(true);

View File

@@ -4,10 +4,10 @@ class Theme extends Item {
super(json_obj);
}
mapContentToLocalProperties(contentObject) {
super.mapContentToLocalProperties(contentObject)
this.url = contentObject.url;
this.name = contentObject.name;
mapContentToLocalProperties(content) {
super.mapContentToLocalProperties(content)
this.url = content.url;
this.name = content.name;
}
structureParams() {

View File

@@ -4,9 +4,9 @@ class EncryptedStorage extends Item {
super(json_obj);
}
mapContentToLocalProperties(contentObject) {
super.mapContentToLocalProperties(contentObject)
this.storage = contentObject.storage;
mapContentToLocalProperties(content) {
super.mapContentToLocalProperties(content)
this.storage = content.storage;
}
structureParams() {

View File

@@ -45,6 +45,9 @@ angular.module('app.frontend')
storageManager.setModelStorageMode(StorageManager.Ephemeral);
storageManager.setItemsMode(storageManager.hasPasscode() ? StorageManager.FixedEncrypted : StorageManager.Ephemeral);
} else {
storageManager.setModelStorageMode(StorageManager.Fixed);
storageManager.setItemsMode(storageManager.hasPasscode() ? StorageManager.FixedEncrypted : StorageManager.Fixed);
storageManager.setItem("ephemeral", JSON.stringify(false), StorageManager.Fixed);
}
}
@@ -150,12 +153,8 @@ angular.module('app.frontend')
var params = {password: keys.pw, email: email};
httpManager.postAbsolute(requestUrl, params, function(response){
this.setEphemeral(ephemeral);
this.handleAuthResponse(response, email, url, authParams, keys);
storageManager.setModelStorageMode(ephemeral ? StorageManager.Ephemeral : StorageManager.Fixed);
this.checkForSecurityUpdate();
callback(response);
}.bind(this), function(response){
console.error("Error logging in", response);
@@ -199,11 +198,8 @@ angular.module('app.frontend')
httpManager.postAbsolute(requestUrl, params, function(response){
this.setEphemeral(ephemeral);
this.handleAuthResponse(response, email, url, authParams, keys);
storageManager.setModelStorageMode(ephemeral ? StorageManager.Ephemeral : StorageManager.Fixed);
callback(response);
}.bind(this), function(response){
console.error("Registration error", response);

View File

@@ -28,12 +28,12 @@ class ComponentManager {
this.handleMessage(this.componentForSessionKey(event.data.sessionKey), event.data);
}.bind(this), false);
this.modelManager.addItemSyncObserver("component-manager", "*", function(items) {
this.modelManager.addItemSyncObserver("component-manager", "*", function(allItems, validItems, deletedItems) {
var syncedComponents = items.filter(function(item){return item.content_type === "SN|Component" });
var syncedComponents = allItems.filter(function(item){return item.content_type === "SN|Component" });
for(var component of syncedComponents) {
var activeComponent = _.find(this.activeComponents, {uuid: component.uuid});
if(component.active && !activeComponent) {
if(component.active && !component.deleted && !activeComponent) {
this.activateComponent(component);
} else if(!component.active && activeComponent) {
this.deactivateComponent(component);
@@ -41,7 +41,7 @@ class ComponentManager {
}
for(let observer of this.streamObservers) {
var relevantItems = items.filter(function(item){
var relevantItems = allItems.filter(function(item){
return observer.contentTypes.indexOf(item.content_type) !== -1;
})
@@ -70,7 +70,7 @@ class ComponentManager {
}
var itemInContext = handler.contextRequestHandler(observer.component);
if(itemInContext) {
var matchingItem = _.find(items, {uuid: itemInContext.uuid});
var matchingItem = _.find(allItems, {uuid: itemInContext.uuid});
if(matchingItem) {
this.sendContextItemInReply(observer.component, matchingItem, observer.originalMessage);
}

View File

@@ -66,7 +66,7 @@ class ContextualExtensionsMenu {
$scope.isActionEnabled = function(action, extension) {
if(action.access_type) {
var extEncryptedAccess = extensionManager.extensionUsesEncryptedData(extension);
var extEncryptedAccess = extension.encrypted;
if(action.access_type == "decrypted" && extEncryptedAccess) {
return false;
} else if(action.access_type == "encrypted" && !extEncryptedAccess) {
@@ -77,7 +77,7 @@ class ContextualExtensionsMenu {
}
$scope.accessTypeForExtension = function(extension) {
return extensionManager.extensionUsesEncryptedData(extension) ? "encrypted" : "decrypted";
return extension.encrypted ? "encrypted" : "decrypted";
}
}

View File

@@ -32,7 +32,9 @@ class GlobalExtensionsMenu {
}
$scope.changeExtensionEncryptionFormat = function(encrypted, extension) {
extensionManager.changeExtensionEncryptionFormat(encrypted, extension);
extension.encrypted = encrypted;
extension.setDirty(true);
syncManager.sync();
}
$scope.deleteActionExtension = function(extension) {
@@ -55,6 +57,36 @@ class GlobalExtensionsMenu {
}
}
$scope.renameExtension = function(extension) {
extension.tempName = extension.name;
extension.rename = true;
}
$scope.submitExtensionRename = function(extension) {
extension.name = extension.tempName;
extension.tempName = null;
extension.setDirty(true);
extension.rename = false;
syncManager.sync();
}
$scope.clickedExtension = function(extension) {
if(extension.rename) {
return;
}
if($scope.currentlyExpandedExtension && $scope.currentlyExpandedExtension !== extension) {
$scope.currentlyExpandedExtension.showDetails = false;
$scope.currentlyExpandedExtension.rename = false;
}
extension.showDetails = !extension.showDetails;
if(extension.showDetails) {
$scope.currentlyExpandedExtension = extension;
}
}
// Server extensions
$scope.deleteServerExt = function(ext) {

View File

@@ -5,15 +5,11 @@ class ExtensionManager {
this.modelManager = modelManager;
this.authManager = authManager;
this.enabledRepeatActionUrls = JSON.parse(storageManager.getItem("enabledRepeatActionUrls")) || [];
this.decryptedExtensions = JSON.parse(storageManager.getItem("decryptedExtensions")) || [];
this.syncManager = syncManager;
this.storageManager = storageManager;
modelManager.addItemSyncObserver("extensionManager", "Extension", function(items){
for (var ext of items) {
ext.encrypted = this.extensionUsesEncryptedData(ext);
modelManager.addItemSyncObserver("extensionManager", "Extension", function(allItems, validItems, deletedItems){
for (var ext of validItems) {
for (var action of ext.actions) {
if(_.includes(this.enabledRepeatActionUrls, action.url)) {
this.enableRepeatAction(action, ext);
@@ -39,29 +35,12 @@ class ExtensionManager {
}
}
extensionUsesEncryptedData(extension) {
return !_.includes(this.decryptedExtensions, extension.url);
}
changeExtensionEncryptionFormat(encrypted, extension) {
if(encrypted) {
_.pull(this.decryptedExtensions, extension.url);
} else {
this.decryptedExtensions.push(extension.url);
}
this.storageManager.setItem("decryptedExtensions", JSON.stringify(this.decryptedExtensions))
extension.encrypted = this.extensionUsesEncryptedData(extension);
}
addExtension(url, callback) {
this.retrieveExtensionFromServer(url, callback);
}
deleteExtension(extension) {
for(var action of extension.actions) {
_.pull(this.decryptedExtensions, extension);
if(action.repeat_mode) {
if(this.isRepeatActionEnabled(action)) {
this.disableRepeatAction(action);
@@ -80,18 +59,10 @@ class ExtensionManager {
loadExtensionInContextOfItem(extension, item, callback) {
this.httpManager.getAbsolute(extension.url, {content_type: item.content_type, item_uuid: item.uuid}, function(response){
var scopedExtension = new Extension(response);
if(scopedExtension) {
_.merge(extension, scopedExtension);
extension.actions = scopedExtension.actions;
extension.encrypted = this.extensionUsesEncryptedData(extension);
}
if(callback) {
callback(scopedExtension);
}
this.updateExtensionFromRemoteResponse(extension, response);
callback && callback(extension);
}.bind(this), function(response){
console.log("Error loading extension", response);
extension.encrypted = this.extensionUsesEncryptedData(extension);
if(callback) {
callback(null);
}
@@ -118,9 +89,13 @@ class ExtensionManager {
}
handleExtensionLoadExternalResponseItem(url, externalResponseItem) {
// Don't allow remote response to set these flags
delete externalResponseItem.encrypted;
delete externalResponseItem.uuid;
var extension = _.find(this.extensions, {url: url});
if(extension) {
extension.updateFromExternalResponseItem(externalResponseItem);
this.updateExtensionFromRemoteResponse(extension, externalResponseItem);
} else {
extension = new Extension(externalResponseItem);
extension.url = url;
@@ -132,6 +107,23 @@ class ExtensionManager {
return extension;
}
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(function(action){
return new Action(action);
})
} else {
extension.actions = [];
}
}
refreshExtensionsFromServer() {
for (var url of this.enabledRepeatActionUrls) {
var action = this.actionWithURL(url);
@@ -149,7 +141,7 @@ class ExtensionManager {
executeAction(action, extension, item, callback) {
if(this.extensionUsesEncryptedData(extension) && this.authManager.offline()) {
if(extension.encrypted && this.userManager.offline()) {
alert("To send data encrypted, you must have an encryption key, and must therefore be signed in.");
callback(null);
return;
@@ -274,11 +266,9 @@ class ExtensionManager {
return;
}
// console.log("Successfully queued", action, this.actionQueue.length);
this.actionQueue.push(action);
setTimeout(function () {
// console.log("Performing queued action", action);
this.triggerWatchAction(action, extension, changedItems);
_.pull(this.actionQueue, action);
}.bind(this), delay * 1000);
@@ -297,8 +287,6 @@ class ExtensionManager {
action.lastExecuted = new Date();
console.log("Performing action.");
if(action.verb == "post") {
var params = {};
params.items = changedItems.map(function(item){
@@ -316,8 +304,8 @@ class ExtensionManager {
}
outgoingParamsForItem(item, extension) {
var keys = this.authManager.keys();
if(!this.extensionUsesEncryptedData(extension)) {
var keys = this.userManager.keys();
if(!extension.encrypted) {
keys = null;
}
var itemParams = new ItemParams(item, keys, this.authManager.protocolVersion());
@@ -326,8 +314,8 @@ class ExtensionManager {
performPost(action, extension, params, callback) {
if(this.extensionUsesEncryptedData(extension)) {
params.auth_params = this.authManager.getAuthParams();
if(extension.encrypted) {
params.auth_params = this.userManager.getAuthParams();
}
this.httpManager.postAbsolute(action.url, params, function(response){

View File

@@ -6,9 +6,13 @@ class ModelManager {
this.tags = [];
this.itemSyncObservers = [];
this.itemChangeObservers = [];
this.itemsPendingRemoval = [];
this.items = [];
this._extensions = [];
this.acceptableContentTypes = ["Note", "Tag", "Extension", "SN|Editor", "SN|Theme", "SN|Component", "SF|Extension"];
this.acceptableContentTypes = [
"Note", "Tag", "Extension", "SN|Editor", "SN|Theme",
"SN|Component", "SF|Extension", "SN|UserPreferences"
];
}
resetLocalMemory() {
@@ -30,7 +34,7 @@ class ModelManager {
})
}
alternateUUIDForItem(item, callback) {
alternateUUIDForItem(item, callback, removeOriginal) {
// we need to clone this item and give it a new uuid, then delete item with old uuid from db (you can't mofidy uuid's in our indexeddb setup)
var newItem = this.createItem(item);
@@ -41,12 +45,20 @@ class ModelManager {
this.informModelsOfUUIDChangeForItem(newItem, item.uuid, newItem.uuid);
this.removeItemLocally(item, function(){
var block = () => {
this.addItem(newItem);
newItem.setDirty(true);
newItem.markAllReferencesDirty();
callback();
}.bind(this));
}
if(removeOriginal) {
this.removeItemLocally(item, function(){
block();
});
} else {
block();
}
}
informModelsOfUUIDChangeForItem(newItem, oldUUID, newUUID) {
@@ -89,23 +101,33 @@ class ModelManager {
}
mapResponseItemsToLocalModelsOmittingFields(items, omitFields) {
var models = [], processedObjects = [], allModels = [];
var models = [], processedObjects = [], modelsToNotifyObserversOf = [];
// first loop should add and process items
for (var json_obj of items) {
json_obj = _.omit(json_obj, omitFields || [])
var item = this.findItem(json_obj["uuid"]);
if((!json_obj.content_type || !json_obj.content) && !json_obj.deleted) {
// An item that is not deleted should never have empty content
console.error("Server response item is corrupt:", json_obj);
continue;
}
_.omit(json_obj, omitFields);
json_obj = _.omit(json_obj, omitFields || [])
var item = this.findItem(json_obj.uuid);
if(item) {
item.updateFromJSON(json_obj);
}
if(json_obj["deleted"] == true || !_.includes(this.acceptableContentTypes, json_obj["content_type"])) {
if(item) {
allModels.push(item);
this.removeItemLocally(item)
if(this.itemsPendingRemoval.includes(json_obj.uuid)) {
_.pull(this.itemsPendingRemoval, json_obj.uuid);
continue;
}
var unknownContentType = !_.includes(this.acceptableContentTypes, json_obj["content_type"]);
if(json_obj.deleted == true || unknownContentType) {
if(item && !unknownContentType) {
modelsToNotifyObserversOf.push(item);
this.removeItemLocally(item);
}
continue;
}
@@ -116,7 +138,7 @@ class ModelManager {
this.addItem(item);
allModels.push(item);
modelsToNotifyObserversOf.push(item);
models.push(item);
processedObjects.push(json_obj);
}
@@ -129,16 +151,25 @@ class ModelManager {
}
}
this.notifySyncObserversOfModels(allModels);
this.notifySyncObserversOfModels(modelsToNotifyObserversOf);
return models;
}
notifySyncObserversOfModels(models) {
for(var observer of this.itemSyncObservers) {
var relevantItems = models.filter(function(item){return item.content_type == observer.type || observer.type == "*"});
if(relevantItems.length > 0) {
observer.callback(relevantItems);
var allRelevantItems = models.filter(function(item){return item.content_type == observer.type || observer.type == "*"});
var validItems = [], deletedItems = [];
for(var item of allRelevantItems) {
if(item.deleted) {
deletedItems.push(item);
} else {
validItems.push(item);
}
}
if(allRelevantItems.length > 0) {
observer.callback(allRelevantItems, validItems, deletedItems);
}
}
}
@@ -184,6 +215,12 @@ class ModelManager {
return item;
}
createDuplicateItem(itemResponse, sourceItem) {
var dup = this.createItem(itemResponse);
this.resolveReferencesForItem(dup);
return dup;
}
addItems(items) {
items.forEach(function(item){
if(item.content_type == "Tag") {
@@ -283,7 +320,7 @@ class ModelManager {
if(!item.dummy) {
item.setDirty(true);
}
item.removeAllRelationships();
item.removeAndDirtyAllRelationships();
}
/* Used when changing encryption key */
@@ -302,6 +339,8 @@ class ModelManager {
item.isBeingRemovedLocally();
this.itemsPendingRemoval.push(item.uuid);
if(item.content_type == "Tag") {
_.pull(this.tags, item);
} else if(item.content_type == "Note") {
@@ -324,14 +363,6 @@ class ModelManager {
itemOne.setDirty(true);
itemTwo.setDirty(true);
}
removeRelationshipBetweenItems(itemOne, itemTwo) {
itemOne.removeItemAsRelationship(itemTwo);
itemTwo.removeItemAsRelationship(itemOne);
itemOne.setDirty(true);
itemTwo.setDirty(true);
}
}
angular.module('app.frontend').service('modelManager', ModelManager);

View File

@@ -42,11 +42,14 @@ class StorageManager {
if(hasPasscode) {
// We don't want to save anything in fixed storage except for actual item data (in IndexedDB)
this.storage = this.memoryStorage;
this.itemsStorageMode = StorageManager.FixedEncrypted;
} else if(ephemeral) {
// We don't want to save anything in fixed storage as well as IndexedDB
this.storage = this.memoryStorage;
this.itemsStorageMode = StorageManager.Ephemeral;
} else {
this.storage = localStorage;
this.itemsStorageMode = StorageManager.Fixed;
}
this.modelStorageMode = ephemeral ? StorageManager.Ephemeral : StorageManager.Fixed;
@@ -69,6 +72,7 @@ class StorageManager {
newStorage.setItem(key, this.storage.getItem(key));
}
this.itemsStorageMode = mode;
this.storage.clear();
this.storage = newStorage;
@@ -83,25 +87,21 @@ class StorageManager {
getVault(vaultKey) {
if(vaultKey) {
return this.storageForVault(vaultKey);
if(vaultKey == StorageManager.Ephemeral || vaultKey == StorageManager.FixedEncrypted) {
return this.memoryStorage;
} else {
return localStorage;
}
} else {
return this.storage;
}
}
storageForVault(vault) {
if(vault == StorageManager.Ephemeral || vault == StorageManager.FixedEncrypted) {
return this.memoryStorage;
} else {
return localStorage;
}
}
setItem(key, value, vault) {
var storage = this.getVault(vault);
setItem(key, value, vaultKey) {
var storage = this.getVault(vaultKey);
storage.setItem(key, value);
if(vault === StorageManager.FixedEncrypted) {
if(vaultKey === StorageManager.FixedEncrypted || (!vaultKey && this.itemsStorageMode === StorageManager.FixedEncrypted)) {
this.writeEncryptedStorageToDisk();
}
}
@@ -147,6 +147,7 @@ class StorageManager {
var encryptedStorage = new EncryptedStorage();
// Copy over totality of current storage
encryptedStorage.storage = this.storageAsHash();
// Save new encrypted storage in Fixed storage
var params = new ItemParams(encryptedStorage, this.encryptedStorageKeys);
this.setItem("encryptedStorage", JSON.stringify(params.paramsForSync()), StorageManager.Fixed);
@@ -160,6 +161,7 @@ class StorageManager {
for(var key of Object.keys(encryptedStorage.storage)) {
this.setItem(key, encryptedStorage.storage[key]);
}
}
hasPasscode() {

View File

@@ -75,13 +75,16 @@ class SyncManager {
Alternating here forces us to to create duplicates of the items instead.
*/
markAllItemsDirtyAndSaveOffline(callback, alternateUUIDs) {
var originalItems = this.modelManager.allItems;
var block = (items) => {
for(var item of items) {
// use a copy, as alternating uuid will affect array
var originalItems = this.modelManager.allItems.slice();
var block = () => {
var allItems = this.modelManager.allItems;
for(var item of allItems) {
item.setDirty(true);
}
this.writeItemsToLocalStorage(items, false, callback);
this.writeItemsToLocalStorage(allItems, false, callback);
}
if(alternateUUIDs) {
@@ -90,18 +93,26 @@ class SyncManager {
let alternateNextItem = () => {
if(index >= originalItems.length) {
// We don't use originalItems as altnerating UUID will have deleted them.
block(this.modelManager.allItems);
block();
return;
}
var item = originalItems[index];
this.modelManager.alternateUUIDForItem(item, alternateNextItem);
++index;
index++;
// alternateUUIDForItem last param is a boolean that controls whether the original item
// should be removed locally after new item is created. We set this to true, since during sign in,
// all item ids are alternated, and we only want one final copy of the entire data set.
// Passing false can be desired sometimes, when for example the app has signed out the user,
// but for some reason retained their data (This happens in Firefox when using private mode).
// In this case, we should pass false so that both copies are kept. However, it's difficult to
// detect when the app has entered this state. We will just use true to remove original items for now.
this.modelManager.alternateUUIDForItem(item, alternateNextItem, true);
}
alternateNextItem();
} else {
block(originalItems);
block();
}
}
@@ -254,16 +265,25 @@ class SyncManager {
this.$rootScope.$broadcast("sync:updated_token", this.syncToken);
var retrieved = this.handleItemsResponse(response.retrieved_items, null);
// Map retrieved items to local data
var retrieved
= this.handleItemsResponse(response.retrieved_items, null);
// Append items to master list of retrieved items for this ongoing sync operation
this.allRetreivedItems = this.allRetreivedItems.concat(retrieved);
// merge only metadata for saved items
// Merge only metadata for saved items
// we write saved items to disk now because it clears their dirty status then saves
// if we saved items before completion, we had have to save them as dirty and save them again on success as clean
var omitFields = ["content", "auth_hash"];
var saved = this.handleItemsResponse(response.saved_items, omitFields);
// Map saved items to local data
var saved =
this.handleItemsResponse(response.saved_items, omitFields);
// Create copies of items or alternate their uuids if neccessary
this.handleUnsavedItemsResponse(response.unsaved)
this.writeItemsToLocalStorage(saved, false, null);
this.syncStatus.syncOpInProgress = false;
@@ -361,7 +381,7 @@ class SyncManager {
// UUID conflicts can occur if a user attempts to
// import an old data archive with uuids from the old account into a new account
handled = true;
this.modelManager.alternateUUIDForItem(item, handleNext);
this.modelManager.alternateUUIDForItem(item, handleNext, true);
}
else if(error.tag === "sync_conflict") {
@@ -370,7 +390,7 @@ class SyncManager {
// We want a new uuid for the new item. Note that this won't neccessarily adjust references.
itemResponse.uuid = null;
var dup = this.modelManager.createItem(itemResponse);
var dup = this.modelManager.createDuplicateItem(itemResponse, item);
if(!itemResponse.deleted && JSON.stringify(item.structureParams()) !== JSON.stringify(dup.structureParams())) {
this.modelManager.addItem(dup);
dup.conflict_of = item.uuid;

View File

@@ -67,12 +67,15 @@
border-radius: 2px;
}
.header {
padding-bottom: 12px;
}
ul {
border-top: 1px solid $light-bg-color;
border-bottom: 1px solid $light-bg-color;
margin: 0;
padding: 0;
margin-top: 12px;
li {
cursor: pointer;

View File

@@ -49,11 +49,7 @@ h2 {
font-weight: bold;
margin-bottom: 4px;
}
strong {
display: block;
}
h1 {
font-size: 16px;
}

View File

@@ -16,47 +16,55 @@
%h3 Learn More
%div{"ng-if" => "themeManager.themes.length > 0"}
.container.no-bottom.section-margin
.header.container.section-margin
%h2 Themes
%ul
%li{"ng-repeat" => "theme in themeManager.themes", "ng-click" => "theme.showDetails = !theme.showDetails"}
%li{"ng-repeat" => "theme in themeManager.themes | orderBy: 'name'", "ng-click" => "clickedExtension(theme)"}
.container
%h3 {{theme.name}}
%h3
%input.bold{"ng-if" => "theme.rename", "ng-model" => "theme.tempName", "ng-keyup" => "$event.keyCode == 13 && submitExtensionRename(theme);", "mb-autofocus" => "true", "should-focus" => "true"}
%span{"ng-if" => "!theme.rename"} {{theme.name}}
%a{"ng-if" => "!themeManager.isThemeActive(theme)", "ng-click" => "themeManager.activateTheme(theme); $event.stopPropagation();"} Activate
%a{"ng-if" => "themeManager.isThemeActive(theme)", "ng-click" => "themeManager.deactivateTheme(theme); $event.stopPropagation();"} Deactivate
.mt-3{"ng-if" => "theme.showDetails"}
.link-group
%a.red{"ng-click" => "deleteTheme(theme); $event.stopPropagation();"} Delete
%a{"ng-click" => "renameExtension(theme); $event.stopPropagation();"} Rename
%a{"ng-click" => "theme.showLink = !theme.showLink; $event.stopPropagation();"} Show Link
%a.red{"ng-click" => "deleteTheme(theme); $event.stopPropagation();"} Delete
%p.small.selectable.wrap{"ng-if" => "theme.showLink"}
{{theme.url}}
%div{"ng-if" => "editorManager.externalEditors.length > 0"}
.container.no-bottom.section-margin
.header.container.section-margin
%h2 Editors
%p{"style" => "margin-top: 3px;"} Choose "Editor" in the note menu to use an editor for a specific note.
%ul
%li{"ng-repeat" => "editor in editorManager.externalEditors", "ng-click" => "editor.showDetails = !editor.showDetails"}
%li{"ng-repeat" => "editor in editorManager.externalEditors | orderBy: 'name'", "ng-click" => "clickedExtension(editor)"}
.container
%strong.red.medium{"ng-if" => "editor.conflict_of"} Conflicted copy
%h3 {{editor.name}}
%h3
%input.bold{"ng-if" => "editor.rename", "ng-model" => "editor.tempName", "ng-keyup" => "$event.keyCode == 13 && submitExtensionRename(editor);", "mb-autofocus" => "true", "should-focus" => "true"}
%span{"ng-if" => "!editor.rename"} {{editor.name}}
%div.mt-5{"ng-if" => "editor.showDetails"}
.link-group
%a{"ng-if" => "!editor.default", "ng-click" => "setDefaultEditor(editor); $event.stopPropagation();"} Make Default
%a.tinted{"ng-if" => "editor.default", "ng-click" => "removeDefaultEditor(editor); $event.stopPropagation();"} Remove as Default
%a{"ng-click" => "renameExtension(editor); $event.stopPropagation();"} Rename
%a{"ng-click" => "editor.showUrl = !editor.showUrl; $event.stopPropagation();"} Show Link
%a.red{ "ng-click" => "deleteEditor(editor); $event.stopPropagation();"} Delete
.wrap.mt-5.selectable{"ng-if" => "editor.showUrl"} {{editor.url}}
%div{"ng-if" => "extensionManager.extensions.length"}
.container.no-bottom.section-margin
.header.container.section-margin
%h2 Actions
%p{"style" => "margin-top: 3px;"} Choose "Actions" in the note editor to use installed actions.
%ul
%li{"ng-repeat" => "extension in extensionManager.extensions | orderBy: 'name'", "ng-init" => "extension.formData = {}", "ng-click" => "extension.showDetails = !extension.showDetails"}
%li{"ng-repeat" => "extension in extensionManager.extensions | orderBy: 'name'", "ng-init" => "extension.formData = {}", "ng-click" => "clickedExtension(extension)"}
.container
%h3 {{extension.name}}
%h3
%input.bold{"ng-if" => "extension.rename", "ng-model" => "extension.tempName", "ng-keyup" => "$event.keyCode == 13 && submitExtensionRename(extension);", "mb-autofocus" => "true", "should-focus" => "true"}
%span{"ng-if" => "!extension.rename"} {{extension.name}}
%p.small{"ng-if" => "extension.description"} {{extension.description}}
%div{"ng-if" => "extension.showDetails"}
.mt-10
@@ -96,29 +104,33 @@
%label.red{"ng-if" => "action.error"}
Error performing action.
%a.block.mt-5{"ng-click" => "renameExtension(extension); $event.stopPropagation();"} Rename
%a.block.mt-5{"ng-click" => "extension.showURL = !extension.showURL; $event.stopPropagation();"} Show Link
%p.wrap.selectable.small{"ng-if" => "extension.showURL"} {{extension.url}}
%a.block.mt-5{"ng-click" => "deleteActionExtension(extension); $event.stopPropagation();"} Remove extension
%a.block.mt-5{"ng-click" => "deleteActionExtension(extension); $event.stopPropagation();"} Delete
%div{"ng-if" => "componentManager.components.length > 0"}
.container.no-bottom.section-margin
.header.container.section-margin
%h2 Components
%ul
%li{"ng-repeat" => "component in componentManager.components", "ng-click" => "component.showDetails = !component.showDetails"}
%li{"ng-repeat" => "component in componentManager.components | orderBy: 'name'", "ng-click" => "clickedExtension(component)"}
.container
%h3 {{component.name}}
%h3
%input.bold{"ng-if" => "component.rename", "ng-model" => "component.tempName", "ng-keyup" => "$event.keyCode == 13 && submitExtensionRename(component);", "mb-autofocus" => "true", "should-focus" => "true"}
%span{"ng-if" => "!component.rename"} {{component.name}}
%a{"ng-if" => "!componentManager.isComponentActive(component)", "ng-click" => "componentManager.activateComponent(component); $event.stopPropagation();"} Activate
%a{"ng-if" => "componentManager.isComponentActive(component)", "ng-click" => "componentManager.deactivateComponent(component); $event.stopPropagation();"} Deactivate
.mt-3{"ng-if" => "component.showDetails"}
.link-group
%a.red{"ng-click" => "deleteComponent(component); $event.stopPropagation();"} Delete
%a{"ng-click" => "renameExtension(component); $event.stopPropagation();"} Rename
%a{"ng-click" => "component.showLink = !component.showLink; $event.stopPropagation();"} Show Link
%a{"ng-if" => "component.permissions.length", "ng-click" => "revokePermissions(component); $event.stopPropagation();"} Revoke Permissions
%a.red{"ng-click" => "deleteComponent(component); $event.stopPropagation();"} Delete
%p.small.selectable.wrap{"ng-if" => "component.showLink"}
{{component.url}}
%div{"ng-if" => "serverExtensions.length > 0"}
.container.no-bottom.section-margin
.header.container.section-margin
%h2 Server Extensions
%ul
%li{"ng-repeat" => "ext in serverExtensions", "ng-click" => "ext.showDetails = !ext.showDetails"}

View File

@@ -49,7 +49,7 @@
%iframe#editor-iframe{"ng-if" => "ctrl.editor && !ctrl.editor.systemEditor", "ng-src" => "{{ctrl.editor.url | trusted}}", "frameBorder" => "0", "style" => "width: 100%;"}
Loading
%textarea.editable#note-text-editor{"ng-if" => "!ctrl.editor || ctrl.editor.systemEditor", "ng-class" => "{'fullscreen' : ctrl.fullscreen }", "ng-model" => "ctrl.note.text",
"ng-change" => "ctrl.contentChanged()", "ng-click" => "ctrl.clickedTextArea()", "ng-focus" => "ctrl.onContentFocus()"}
"ng-change" => "ctrl.contentChanged()", "ng-click" => "ctrl.clickedTextArea()", "ng-focus" => "ctrl.onContentFocus()", "dir" => "auto"}
{{ctrl.onSystemEditorLoad()}}

View File

@@ -18,7 +18,7 @@
%span.tinted.normal New update downloaded. Installs on app restart.
.footer-bar-link{"style" => "margin-right: 5px;"}
%div{"ng-if" => "ctrl.lastSyncDate", "style" => "float: left; font-weight: normal; margin-right: 8px;"}
%span{"ng-if" => "ctrl.lastSyncDate", "style" => "float: left; font-weight: normal; margin-right: 8px;"}
%span{"ng-if" => "!ctrl.isRefreshing"}
Last refreshed {{ctrl.lastSyncDate | appDateTime}}
%span{"ng-if" => "ctrl.isRefreshing"}
@@ -28,4 +28,4 @@
%a{"ng-if" => "!ctrl.offline", "ng-click" => "ctrl.refreshData()"} Refresh
%span{"ng-if" => "ctrl.hasPasscode()"}
%i.icon.ion-locked{"ng-click" => "ctrl.lockApp()"}
%i.icon.ion-locked{"ng-if" => "ctrl.hasPasscode()", "ng-click" => "ctrl.lockApp()"}

View File

@@ -39,7 +39,7 @@
.scrollable
.infinite-scroll{"infinite-scroll" => "ctrl.paginate()", "can-load" => "true", "threshold" => "200"}
.note{"ng-repeat" => "note in (ctrl.sortedNotes = (ctrl.tag.notes | filter: ctrl.filterNotes | sortBy: ctrl.sortBy| limitTo:ctrl.notesToDisplay))",
.note{"ng-repeat" => "note in (ctrl.sortedNotes = (ctrl.tag.notes | filter: ctrl.filterNotes | sortBy: ctrl.sortBy| limitTo:ctrl.notesToDisplay)) track by note.uuid",
"ng-click" => "ctrl.selectNote(note)", "ng-class" => "{'selected' : ctrl.selectedNote == note}"}
%strong.red.medium{"ng-if" => "note.conflict_of"} Conflicted copy
%strong.red.medium{"ng-if" => "note.errorDecrypting"} Error decrypting

View File

@@ -10,13 +10,16 @@
.info
%input.title{"ng-disabled" => "true", "ng-model" => "ctrl.allTag.title"}
.count {{ctrl.noteCount(ctrl.allTag)}}
.tag{"ng-repeat" => "tag in ctrl.tags", "ng-click" => "ctrl.selectTag(tag)", "ng-class" => "{'selected' : ctrl.selectedTag == tag}"}
.tag{"ng-repeat" => "tag in ctrl.tags track by tag.uuid", "ng-click" => "ctrl.selectTag(tag)", "ng-class" => "{'selected' : ctrl.selectedTag == tag}"}
.info
%input.title{"ng-attr-id" => "tag-{{tag.uuid}}", "ng-click" => "ctrl.selectTag(tag)", "ng-model" => "tag.title",
"ng-keyup" => "$event.keyCode == 13 && ctrl.saveTag($event, tag)", "mb-autofocus" => "true", "should-focus" => "ctrl.newTag || ctrl.editingTag == tag",
"ng-change" => "ctrl.tagTitleDidChange(tag)", "ng-blur" => "ctrl.saveTag($event, tag)", "spellcheck" => "false"}
.count {{ctrl.noteCount(tag)}}
.red.small.bold{"ng-if" => "tag.conflict_of"} Conflicted copy
.red.small.bold{"ng-if" => "tag.errorDecrypting"} Error decrypting
.menu{"ng-if" => "ctrl.selectedTag == tag"}
%a.item{"ng-click" => "ctrl.selectedRenameTag($event, tag)", "ng-if" => "!ctrl.editingTag"} Rename
%a.item{"ng-click" => "ctrl.saveTag($event, tag)", "ng-if" => "ctrl.editingTag"} Save