diff --git a/app/assets/javascripts/app/controllers/editor.js b/app/assets/javascripts/app/controllers/editor.js index 6f4e309af..5fcb72b79 100644 --- a/app/assets/javascripts/app/controllers/editor.js +++ b/app/assets/javascripts/app/controllers/editor.js @@ -40,6 +40,33 @@ angular.module('app') this.loadTagsString(); }.bind(this)); + modelManager.addItemSyncObserver("component-manager", "Note", (allItems, validItems, deletedItems, source) => { + if(!this.note) { return; } + + // Before checking if isMappingSourceRetrieved, we check if this item was deleted via a local source, + // such as alternating uuids during sign in. Otherwise, we only want to make interface updates if it's a + // remote retrieved source. + if(this.note.deleted) { + $rootScope.notifyDelete(); + return; + } + + if(!ModelManager.isMappingSourceRetrieved(source)) { + return; + } + + var matchingNote = allItems.find((item) => { + return item.uuid == this.note.uuid; + }); + + if(!matchingNote) { + return; + } + + // Update tags + this.loadTagsString(); + }); + this.noteDidChange = function(note, oldNote) { this.setNote(note, oldNote); this.reloadComponentContext(); diff --git a/app/assets/javascripts/app/controllers/home.js b/app/assets/javascripts/app/controllers/home.js index d07eb1dfc..a1b153809 100644 --- a/app/assets/javascripts/app/controllers/home.js +++ b/app/assets/javascripts/app/controllers/home.js @@ -145,11 +145,11 @@ angular.module('app') } $scope.tagsSelectionMade = function(tag) { - $scope.selectedTag = tag; - if($scope.selectedNote && $scope.selectedNote.dummy) { modelManager.removeItemLocally($scope.selectedNote); } + + $scope.selectedTag = tag; } $scope.tagsAddNew = function(tag) { @@ -227,7 +227,7 @@ angular.module('app') this.$apply(fn); }; - $scope.notifyDelete = function() { + $rootScope.notifyDelete = function() { $timeout(function() { $rootScope.$broadcast("noteDeleted"); }.bind(this), 0); @@ -243,7 +243,7 @@ angular.module('app') if(note.dummy) { modelManager.removeItemLocally(note); - $scope.notifyDelete(); + $rootScope.notifyDelete(); return; } @@ -251,11 +251,11 @@ angular.module('app') if(authManager.offline()) { // when deleting items while ofline, we need to explictly tell angular to refresh UI setTimeout(function () { - $scope.notifyDelete(); + $rootScope.notifyDelete(); $scope.safeApply(); }, 50); } else { - $scope.notifyDelete(); + $rootScope.notifyDelete(); } }, null, "deleteNote"); } diff --git a/app/assets/javascripts/app/controllers/notes.js b/app/assets/javascripts/app/controllers/notes.js index 138efb42a..c9e5478c2 100644 --- a/app/assets/javascripts/app/controllers/notes.js +++ b/app/assets/javascripts/app/controllers/notes.js @@ -89,7 +89,7 @@ angular.module('app') this.onNoteRemoval = function() { let visibleNotes = this.visibleNotes(); if(this.selectedIndex < visibleNotes.length) { - this.selectNote(visibleNotes[this.selectedIndex]); + this.selectNote(visibleNotes[Math.max(this.selectedIndex, 0)]); } else { this.selectNote(visibleNotes[visibleNotes.length - 1]); } @@ -190,11 +190,14 @@ angular.module('app') } this.selectNote = function(note, viaClick = false) { - if(!note) { return; } + if(!note) { + this.createNewNote(); + return; + } this.selectedNote = note; note.conflict_of = null; // clear conflict this.selectionMade()(note); - this.selectedIndex = this.visibleNotes().indexOf(note); + this.selectedIndex = Math.max(this.visibleNotes().indexOf(note), 0); if(viaClick && this.isFiltering()) { desktopManager.searchText(this.noteFilter.text); diff --git a/app/assets/javascripts/app/controllers/tags.js b/app/assets/javascripts/app/controllers/tags.js index 1b3e5eedb..21a430b45 100644 --- a/app/assets/javascripts/app/controllers/tags.js +++ b/app/assets/javascripts/app/controllers/tags.js @@ -129,10 +129,12 @@ angular.module('app') return; } - this.save()(tag, function(savedTag){ - this.selectTag(tag); - this.newTag = null; - }.bind(this)); + this.save()(tag, (savedTag) => { + $timeout(() => { + this.selectTag(tag); + this.newTag = null; + }) + }); } function inputElementForTag(tag) { diff --git a/app/assets/javascripts/app/directives/views/accountMenu.js b/app/assets/javascripts/app/directives/views/accountMenu.js index da31ab353..1344712e6 100644 --- a/app/assets/javascripts/app/directives/views/accountMenu.js +++ b/app/assets/javascripts/app/directives/views/accountMenu.js @@ -105,11 +105,18 @@ class AccountMenu { } $scope.login = function(extraParams) { + // Prevent a timed sync from occuring while signing in. There may be a race condition where when + // calling `markAllItemsDirtyAndSaveOffline` during sign in, if an authenticated sync happens to occur + // right before that's called, items retreived from that sync will be marked as dirty, then resynced, causing mass duplication. + // Unlock sync after all sign in processes are complete. + syncManager.lockSyncing(); + $scope.formData.status = "Generating Login Keys..."; $timeout(function(){ authManager.login($scope.formData.url, $scope.formData.email, $scope.formData.user_password, $scope.formData.ephemeral, extraParams, (response) => { if(!response || response.error) { + syncManager.unlockSyncing(); $scope.formData.status = null; var error = response ? response.error : {message: "An unknown error occured."} @@ -133,7 +140,10 @@ class AccountMenu { // Success else { - $scope.onAuthSuccess(); + $scope.onAuthSuccess(() => { + syncManager.unlockSyncing(); + syncManager.sync("onLogin"); + }); } }); }) @@ -156,7 +166,9 @@ class AccountMenu { var error = response ? response.error : {message: "An unknown error occured."} alert(error.message); } else { - $scope.onAuthSuccess(); + $scope.onAuthSuccess(() => { + syncManager.sync("onRegister"); + }); } }); }) @@ -170,12 +182,12 @@ class AccountMenu { } } - $scope.onAuthSuccess = function() { + $scope.onAuthSuccess = function(callback) { var block = function() { $timeout(function(){ $scope.onSuccessfulAuth()(); syncManager.refreshErroredItems(); - syncManager.sync("onAuthSuccess"); + callback && callback(); }) } @@ -196,7 +208,7 @@ class AccountMenu { // clearAllModels will remove data from backing store, but not from working memory // See: https://github.com/standardnotes/desktop/issues/131 $scope.clearDatabaseAndRewriteAllItems = function(alternateUuids, callback) { - storageManager.clearAllModels(function(){ + storageManager.clearAllModels(() => { syncManager.markAllItemsDirtyAndSaveOffline(function(){ callback && callback(); }, alternateUuids) diff --git a/app/assets/javascripts/app/models/api/item.js b/app/assets/javascripts/app/models/api/item.js index caf061735..fe85f5504 100644 --- a/app/assets/javascripts/app/models/api/item.js +++ b/app/assets/javascripts/app/models/api/item.js @@ -55,6 +55,13 @@ class Item { } } + refreshContentObject() { + // Before an item can be duplicated or cloned, we must update this.content (if it is an object) with the object's + // current physical properties, because updateFromJSON, which is what all new items must go through, + // will call this.mapContentToLocalProperties(this.contentObject), which may have stale values if not explicitly updated. + + this.content = this.structureParams(); + } /* Allows the item to handle the case where the item is deleted and the content is null */ handleDeletedContent() { diff --git a/app/assets/javascripts/app/services/modelManager.js b/app/assets/javascripts/app/services/modelManager.js index 68d2a926c..3ba84cf01 100644 --- a/app/assets/javascripts/app/services/modelManager.js +++ b/app/assets/javascripts/app/services/modelManager.js @@ -10,6 +10,14 @@ class ModelManager { ModelManager.MappingSourceRemoteActionRetrieved = "MappingSourceRemoteActionRetrieved"; /* aciton-based Extensions like note history */ ModelManager.MappingSourceFileImport = "MappingSourceFileImport"; + ModelManager.isMappingSourceRetrieved = (source) => { + return [ + ModelManager.MappingSourceRemoteRetrieved, + ModelManager.MappingSourceComponentRetrieved, + ModelManager.MappingSourceRemoteActionRetrieved + ].includes(source); + } + this.storageManager = storageManager; this.notes = []; this.tags = []; @@ -44,7 +52,11 @@ class ModelManager { } 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) + // We need to clone this item and give it a new uuid, then delete item with old uuid from db (you can't modify uuid's in our indexeddb setup) + + // Collapse in memory properties to item's content object, as the new item will be created based on the content object, and not the physical properties. (like note.text or note.title) + item.refreshContentObject(); + var newItem = this.createItem(item); newItem.uuid = SFJS.crypto.generateUUID(); @@ -54,6 +66,7 @@ class ModelManager { this.informModelsOfUUIDChangeForItem(newItem, item.uuid, newItem.uuid); + console.log(item.uuid, "-->", newItem.uuid); var block = () => { @@ -261,7 +274,14 @@ class ModelManager { return item; } - createDuplicateItem(itemResponse, sourceItem) { + /* + Be sure itemResponse is a generic Javascript object, and not an Item. + An Item needs to collapse its properties into its content object before it can be duplicated. + Note: the reason we need this function is specificallty for the call to resolveReferencesForItem. + This method creates but does not add the item to the global inventory. It's used by syncManager + to check if this prospective duplicate item is identical to another item, including the references. + */ + createDuplicateItem(itemResponse) { var dup = this.createItem(itemResponse, true); this.resolveReferencesForItem(dup); return dup; diff --git a/app/assets/javascripts/app/services/syncManager.js b/app/assets/javascripts/app/services/syncManager.js index 565dea713..610d81da7 100644 --- a/app/assets/javascripts/app/services/syncManager.js +++ b/app/assets/javascripts/app/services/syncManager.js @@ -193,8 +193,21 @@ class SyncManager { this.$interval.cancel(this.syncStatus.checker); } + lockSyncing() { + this.syncLocked = true; + } + + unlockSyncing() { + this.syncLocked = false; + } + sync(callback, options = {}, source) { + if(this.syncLocked) { + console.log("Sync Locked, Returning;"); + return; + } + if(!options) options = {}; if(typeof callback == 'string') { @@ -475,7 +488,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.createDuplicateItem(itemResponse, item); + var dup = this.modelManager.createDuplicateItem(itemResponse); if(!itemResponse.deleted && !item.isItemContentEqualWith(dup)) { this.modelManager.addItem(dup); dup.conflict_of = item.uuid; diff --git a/app/assets/templates/tags.html.haml b/app/assets/templates/tags.html.haml index 8bb32cced..e40505d20 100644 --- a/app/assets/templates/tags.html.haml +++ b/app/assets/templates/tags.html.haml @@ -18,7 +18,7 @@ .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)", "sn-autofocus" => "true", "should-focus" => "ctrl.newTag || ctrl.editingTag == tag", + "ng-keyup" => "$event.keyCode == 13 && $event.target.blur()", "sn-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)}}