diff --git a/app/assets/javascripts/app/frontend/controllers/editor.js b/app/assets/javascripts/app/frontend/controllers/editor.js index 789f58a9c..8634475d1 100644 --- a/app/assets/javascripts/app/frontend/controllers/editor.js +++ b/app/assets/javascripts/app/frontend/controllers/editor.js @@ -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; diff --git a/app/assets/javascripts/app/frontend/controllers/notes.js b/app/assets/javascripts/app/frontend/controllers/notes.js index 7d72813da..05a1e4cd7 100644 --- a/app/assets/javascripts/app/frontend/controllers/notes.js +++ b/app/assets/javascripts/app/frontend/controllers/notes.js @@ -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 = ""; diff --git a/app/assets/javascripts/app/frontend/models/api/item.js b/app/assets/javascripts/app/frontend/models/api/item.js index b153ac92b..af65d5aa3 100644 --- a/app/assets/javascripts/app/frontend/models/api/item.js +++ b/app/assets/javascripts/app/frontend/models/api/item.js @@ -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); } diff --git a/app/assets/javascripts/app/frontend/models/api/syncAdapter.js b/app/assets/javascripts/app/frontend/models/api/syncAdapter.js index 6e74b1441..12f87d20f 100644 --- a/app/assets/javascripts/app/frontend/models/api/syncAdapter.js +++ b/app/assets/javascripts/app/frontend/models/api/syncAdapter.js @@ -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() { diff --git a/app/assets/javascripts/app/frontend/models/app/component.js b/app/assets/javascripts/app/frontend/models/app/component.js index 66d6b5c6e..c0108469c 100644 --- a/app/assets/javascripts/app/frontend/models/app/component.js +++ b/app/assets/javascripts/app/frontend/models/app/component.js @@ -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() { diff --git a/app/assets/javascripts/app/frontend/models/app/editor.js b/app/assets/javascripts/app/frontend/models/app/editor.js index 39a152147..3a38f0734 100644 --- a/app/assets/javascripts/app/frontend/models/app/editor.js +++ b/app/assets/javascripts/app/frontend/models/app/editor.js @@ -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 = []; } diff --git a/app/assets/javascripts/app/frontend/models/app/extension.js b/app/assets/javascripts/app/frontend/models/app/extension.js index 705a9eb83..445c4dbb1 100644 --- a/app/assets/javascripts/app/frontend/models/app/extension.js +++ b/app/assets/javascripts/app/frontend/models/app/extension.js @@ -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()); diff --git a/app/assets/javascripts/app/frontend/models/app/note.js b/app/assets/javascripts/app/frontend/models/app/note.js index 7ea5d10ea..e428b6717 100644 --- a/app/assets/javascripts/app/frontend/models/app/note.js +++ b/app/assets/javascripts/app/frontend/models/app/note.js @@ -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); diff --git a/app/assets/javascripts/app/frontend/models/app/tag.js b/app/assets/javascripts/app/frontend/models/app/tag.js index 1976f6f3b..f94ee4151 100644 --- a/app/assets/javascripts/app/frontend/models/app/tag.js +++ b/app/assets/javascripts/app/frontend/models/app/tag.js @@ -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); diff --git a/app/assets/javascripts/app/frontend/models/app/theme.js b/app/assets/javascripts/app/frontend/models/app/theme.js index 91b3e4915..ec7486012 100644 --- a/app/assets/javascripts/app/frontend/models/app/theme.js +++ b/app/assets/javascripts/app/frontend/models/app/theme.js @@ -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() { diff --git a/app/assets/javascripts/app/frontend/models/local/encryptedStorage.js b/app/assets/javascripts/app/frontend/models/local/encryptedStorage.js index 412c669ca..764d0ae24 100644 --- a/app/assets/javascripts/app/frontend/models/local/encryptedStorage.js +++ b/app/assets/javascripts/app/frontend/models/local/encryptedStorage.js @@ -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() { diff --git a/app/assets/javascripts/app/services/authManager.js b/app/assets/javascripts/app/services/authManager.js index 6e07e67b2..9763e716e 100644 --- a/app/assets/javascripts/app/services/authManager.js +++ b/app/assets/javascripts/app/services/authManager.js @@ -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); diff --git a/app/assets/javascripts/app/services/componentManager.js b/app/assets/javascripts/app/services/componentManager.js index 64476007f..026aeb79a 100644 --- a/app/assets/javascripts/app/services/componentManager.js +++ b/app/assets/javascripts/app/services/componentManager.js @@ -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); } diff --git a/app/assets/javascripts/app/services/directives/views/contextualExtensionsMenu.js b/app/assets/javascripts/app/services/directives/views/contextualExtensionsMenu.js index a41644af5..3825aed6e 100644 --- a/app/assets/javascripts/app/services/directives/views/contextualExtensionsMenu.js +++ b/app/assets/javascripts/app/services/directives/views/contextualExtensionsMenu.js @@ -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"; } } diff --git a/app/assets/javascripts/app/services/directives/views/globalExtensionsMenu.js b/app/assets/javascripts/app/services/directives/views/globalExtensionsMenu.js index e93c069eb..fa29c8d4d 100644 --- a/app/assets/javascripts/app/services/directives/views/globalExtensionsMenu.js +++ b/app/assets/javascripts/app/services/directives/views/globalExtensionsMenu.js @@ -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) { diff --git a/app/assets/javascripts/app/services/extensionManager.js b/app/assets/javascripts/app/services/extensionManager.js index e53e8733a..8938eaab2 100644 --- a/app/assets/javascripts/app/services/extensionManager.js +++ b/app/assets/javascripts/app/services/extensionManager.js @@ -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){ diff --git a/app/assets/javascripts/app/services/modelManager.js b/app/assets/javascripts/app/services/modelManager.js index 4586275ef..f1c8d587a 100644 --- a/app/assets/javascripts/app/services/modelManager.js +++ b/app/assets/javascripts/app/services/modelManager.js @@ -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); diff --git a/app/assets/javascripts/app/services/storageManager.js b/app/assets/javascripts/app/services/storageManager.js index 5c34e4e15..cc8a3fa9b 100644 --- a/app/assets/javascripts/app/services/storageManager.js +++ b/app/assets/javascripts/app/services/storageManager.js @@ -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() { diff --git a/app/assets/javascripts/app/services/syncManager.js b/app/assets/javascripts/app/services/syncManager.js index 7a998758e..7fe980953 100644 --- a/app/assets/javascripts/app/services/syncManager.js +++ b/app/assets/javascripts/app/services/syncManager.js @@ -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; diff --git a/app/assets/stylesheets/app/_extensions.scss b/app/assets/stylesheets/app/_extensions.scss index 65984cf1b..71edefcdb 100644 --- a/app/assets/stylesheets/app/_extensions.scss +++ b/app/assets/stylesheets/app/_extensions.scss @@ -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; diff --git a/app/assets/stylesheets/app/_footer.scss b/app/assets/stylesheets/app/_footer.scss index fb07054da..f42b9c457 100644 --- a/app/assets/stylesheets/app/_footer.scss +++ b/app/assets/stylesheets/app/_footer.scss @@ -49,11 +49,7 @@ h2 { font-weight: bold; margin-bottom: 4px; } - - strong { - display: block; - } - + h1 { font-size: 16px; } diff --git a/app/assets/templates/frontend/directives/global-extensions-menu.html.haml b/app/assets/templates/frontend/directives/global-extensions-menu.html.haml index 5642da8cc..3a55fab07 100644 --- a/app/assets/templates/frontend/directives/global-extensions-menu.html.haml +++ b/app/assets/templates/frontend/directives/global-extensions-menu.html.haml @@ -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"} diff --git a/app/assets/templates/frontend/editor.html.haml b/app/assets/templates/frontend/editor.html.haml index 56065ced9..3da918fdd 100644 --- a/app/assets/templates/frontend/editor.html.haml +++ b/app/assets/templates/frontend/editor.html.haml @@ -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()}} diff --git a/app/assets/templates/frontend/footer.html.haml b/app/assets/templates/frontend/footer.html.haml index 8814681e5..1f7506f45 100644 --- a/app/assets/templates/frontend/footer.html.haml +++ b/app/assets/templates/frontend/footer.html.haml @@ -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()"} diff --git a/app/assets/templates/frontend/notes.html.haml b/app/assets/templates/frontend/notes.html.haml index f4d83b032..020cfa552 100644 --- a/app/assets/templates/frontend/notes.html.haml +++ b/app/assets/templates/frontend/notes.html.haml @@ -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 diff --git a/app/assets/templates/frontend/tags.html.haml b/app/assets/templates/frontend/tags.html.haml index 121705900..4b689860c 100644 --- a/app/assets/templates/frontend/tags.html.haml +++ b/app/assets/templates/frontend/tags.html.haml @@ -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