From 6660312da2ecc2cc4c397146da450ea044916f9c Mon Sep 17 00:00:00 2001 From: Alex Quach Date: Sun, 17 Jun 2018 10:32:20 -0700 Subject: [PATCH 01/12] Fix typo in "Date Modified". --- app/assets/javascripts/app/controllers/notes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/app/controllers/notes.js b/app/assets/javascripts/app/controllers/notes.js index 6f7362e3d..e6f51c142 100644 --- a/app/assets/javascripts/app/controllers/notes.js +++ b/app/assets/javascripts/app/controllers/notes.js @@ -128,7 +128,7 @@ angular.module('app') if(this.sortBy == "created_at") { base += " Date Added"; } else if(this.sortBy == "client_updated_at") { - base += " Date Modifed"; + base += " Date Modified"; } else if(this.sortBy == "title") { base += " Title"; } From 40ffa5f7b0752a0f6fc1abeafc6b89b6191bbbd1 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Tue, 19 Jun 2018 18:59:37 -0500 Subject: [PATCH 02/12] 401 error alert --- app/assets/javascripts/app/services/httpManager.js | 2 +- app/assets/javascripts/app/services/syncManager.js | 7 +++++-- app/assets/stylesheets/app/_main.scss | 5 ++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/app/services/httpManager.js b/app/assets/javascripts/app/services/httpManager.js index 71692f478..c8f0a722f 100644 --- a/app/assets/javascripts/app/services/httpManager.js +++ b/app/assets/javascripts/app/services/httpManager.js @@ -45,7 +45,7 @@ class HttpManager { } else { console.error("Request error:", response); this.$timeout(function(){ - onerror(response) + onerror(response, xmlhttp.status) }) } } diff --git a/app/assets/javascripts/app/services/syncManager.js b/app/assets/javascripts/app/services/syncManager.js index bb3870818..5615c5ac9 100644 --- a/app/assets/javascripts/app/services/syncManager.js +++ b/app/assets/javascripts/app/services/syncManager.js @@ -411,7 +411,7 @@ class SyncManager { ) { this.$rootScope.$broadcast("major-data-change"); } - + this.callQueuedCallbacksAndCurrent(callback, response); this.$rootScope.$broadcast("sync:completed", {retrievedItems: this.allRetreivedItems, savedItems: this.allSavedItems}); @@ -429,7 +429,10 @@ class SyncManager { console.log("Caught sync success exception:", e); } - }.bind(this), function(response){ + }.bind(this), function(response, statusCode){ + if(statusCode == 401) { + alert("Your session has expired. New changes will not be pulled in. Please sign out and sign back in to refresh your session."); + } console.log("Sync error: ", response); var error = response ? response.error : {message: "Could not connect to server."}; diff --git a/app/assets/stylesheets/app/_main.scss b/app/assets/stylesheets/app/_main.scss index e90b32957..3037bd793 100644 --- a/app/assets/stylesheets/app/_main.scss +++ b/app/assets/stylesheets/app/_main.scss @@ -87,13 +87,12 @@ $footer-height: 32px; top: 0; right: 0; z-index: 1; - width: 6px; + width: 8px; height: 100%; position: absolute; cursor: col-resize; - background-color: rgba(black, 0.05); + background-color: rgba(black, 0.1); opacity: 0; - border: 1px dashed rgba($blue-color, 0.15); border-top: none; border-bottom: none; From 27cdc19b197c26cd86c961db5f940f6dcdf7367d Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Wed, 20 Jun 2018 09:16:43 -0500 Subject: [PATCH 03/12] Item in context safety check --- app/assets/javascripts/app/services/componentManager.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/app/services/componentManager.js b/app/assets/javascripts/app/services/componentManager.js index 2c84a2a7c..2ad780ef1 100644 --- a/app/assets/javascripts/app/services/componentManager.js +++ b/app/assets/javascripts/app/services/componentManager.js @@ -164,7 +164,9 @@ class ComponentManager { for(let observer of observers) { if(handler.contextRequestHandler) { var itemInContext = handler.contextRequestHandler(observer.component); - this.sendContextItemInReply(observer.component, itemInContext, observer.originalMessage); + if(itemInContext) { + this.sendContextItemInReply(observer.component, itemInContext, observer.originalMessage); + } } } } From c67167af9707d22862829d0fe43770702ae0c5bf Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Wed, 20 Jun 2018 10:36:45 -0500 Subject: [PATCH 04/12] Use readonly instead of locked for locked notes --- app/assets/templates/editor.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/templates/editor.html.haml b/app/assets/templates/editor.html.haml index 1bd54c92e..512134f93 100644 --- a/app/assets/templates/editor.html.haml +++ b/app/assets/templates/editor.html.haml @@ -56,7 +56,7 @@ %component-view.component-view{"ng-if" => "ctrl.selectedEditor", "component" => "ctrl.selectedEditor", "ng-style" => "ctrl.note.locked && {'pointer-events' : 'none'}", "ng-class" => "{'locked' : ctrl.note.locked }"} - %textarea.editable#note-text-editor{"ng-if" => "!ctrl.selectedEditor", "ng-model" => "ctrl.note.text", "ng-disabled" => "ctrl.note.locked", + %textarea.editable#note-text-editor{"ng-if" => "!ctrl.selectedEditor", "ng-model" => "ctrl.note.text", "ng-readonly" => "ctrl.note.locked", "ng-change" => "ctrl.contentChanged()", "ng-trim" => "false", "ng-click" => "ctrl.clickedTextArea()", "ng-focus" => "ctrl.onContentFocus()", "dir" => "auto", "ng-attr-spellcheck" => "{{ctrl.spellcheck}}"} {{ctrl.onSystemEditorLoad()}} From 9a4e8ac964093290ef9b9e2957e35a27e1d1fe92 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Thu, 21 Jun 2018 23:33:34 -0500 Subject: [PATCH 05/12] Notify other components of change --- .../app/services/componentManager.js | 24 +++++++++++++++---- .../javascripts/app/services/modelManager.js | 12 +++++----- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/app/services/componentManager.js b/app/assets/javascripts/app/services/componentManager.js index 2ad780ef1..e186f5155 100644 --- a/app/assets/javascripts/app/services/componentManager.js +++ b/app/assets/javascripts/app/services/componentManager.js @@ -49,15 +49,19 @@ class ComponentManager { this.handleMessage(this.componentForSessionKey(event.data.sessionKey), event.data); }.bind(this), false); - this.modelManager.addItemSyncObserver("component-manager", "*", (allItems, validItems, deletedItems, source) => { + this.modelManager.addItemSyncObserver("component-manager", "*", (allItems, validItems, deletedItems, source, sourceKey) => { /* If the source of these new or updated items is from a Component itself saving items, we don't need to notify components again of the same item. Regarding notifying other components than the issuing component, other mapping sources will take care of that, like ModelManager.MappingSourceRemoteSaved + + Update: We will now check sourceKey to determine whether the incoming change should be sent to + a component. If sourceKey == component.uuid, it will be skipped. This way, if one component triggers a change, + it's sent to other components. */ - if(source == ModelManager.MappingSourceComponentRetrieved) { - return; - } + // if(source == ModelManager.MappingSourceComponentRetrieved) { + // return; + // } var syncedComponents = allItems.filter(function(item) { return item.content_type === "SN|Component" || item.content_type == "SN|Theme" @@ -81,6 +85,11 @@ class ComponentManager { } for(let observer of this.streamObservers) { + if(sourceKey && sourceKey == observer.component.uuid) { + // Don't notify source of change, as it is the originator, doesn't need duplicate event. + continue; + } + var relevantItems = allItems.filter(function(item){ return observer.contentTypes.indexOf(item.content_type) !== -1; }) @@ -108,6 +117,11 @@ class ComponentManager { ]; for(let observer of this.contextStreamObservers) { + if(sourceKey && sourceKey == observer.component.uuid) { + // Don't notify source of change, as it is the originator, doesn't need duplicate event. + continue; + } + for(let handler of this.handlers) { if(!handler.areas.includes(observer.component.area) && !handler.areas.includes("*")) { continue; @@ -454,7 +468,7 @@ class ComponentManager { We map the items here because modelManager is what updates the UI. If you were to instead get the items directly, this would update them server side via sync, but would never make its way back to the UI. */ - var localItems = this.modelManager.mapResponseItemsToLocalModels(responseItems, ModelManager.MappingSourceComponentRetrieved); + var localItems = this.modelManager.mapResponseItemsToLocalModels(responseItems, ModelManager.MappingSourceComponentRetrieved, component.uuid); for(var item of localItems) { var responseItem = _.find(responseItems, {uuid: item.uuid}); diff --git a/app/assets/javascripts/app/services/modelManager.js b/app/assets/javascripts/app/services/modelManager.js index 8bbb94399..3361e441f 100644 --- a/app/assets/javascripts/app/services/modelManager.js +++ b/app/assets/javascripts/app/services/modelManager.js @@ -125,11 +125,11 @@ class ModelManager { this.notifySyncObserversOfModels(items, ModelManager.MappingSourceLocalSaved); } - mapResponseItemsToLocalModels(items, source) { - return this.mapResponseItemsToLocalModelsOmittingFields(items, null, source); + mapResponseItemsToLocalModels(items, source, sourceKey) { + return this.mapResponseItemsToLocalModelsOmittingFields(items, null, source, sourceKey); } - mapResponseItemsToLocalModelsOmittingFields(items, omitFields, source) { + mapResponseItemsToLocalModelsOmittingFields(items, omitFields, source, sourceKey) { var models = [], processedObjects = [], modelsToNotifyObserversOf = []; // first loop should add and process items @@ -202,13 +202,13 @@ class ModelManager { } } - this.notifySyncObserversOfModels(modelsToNotifyObserversOf, source); + this.notifySyncObserversOfModels(modelsToNotifyObserversOf, source, sourceKey); return models; } /* Note that this function is public, and can also be called manually (desktopManager uses it) */ - notifySyncObserversOfModels(models, source) { + notifySyncObserversOfModels(models, source, sourceKey) { for(var observer of this.itemSyncObservers) { var allRelevantItems = observer.type == "*" ? models : models.filter(function(item){return item.content_type == observer.type}); var validItems = [], deletedItems = []; @@ -221,7 +221,7 @@ class ModelManager { } if(allRelevantItems.length > 0) { - observer.callback(allRelevantItems, validItems, deletedItems, source); + observer.callback(allRelevantItems, validItems, deletedItems, source, sourceKey); } } } From c14fe0383a63e5aea83a12e3cad2a842fb7dd20f Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Fri, 22 Jun 2018 11:19:15 -0500 Subject: [PATCH 06/12] Update SFJS 0.3.1 --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index caf342e00..94c93c089 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5721,9 +5721,9 @@ "dev": true }, "standard-file-js": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/standard-file-js/-/standard-file-js-0.3.0.tgz", - "integrity": "sha512-P2NGrzAHlik6/Hu+2lJ6dOjvevZEyXOSCuI4hvFAsTL9Fn3Ixl0AR+y1M6CjKBF2NxAGtDviVR1YctO5MGee9Q==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/standard-file-js/-/standard-file-js-0.3.1.tgz", + "integrity": "sha512-UEYlIiIMJJeqZSjmeJnLPlWH6SYa8x4HW9tuklF4mqSdGuZvUc1eJcUPiJ2n1Tv+rHEmKeX5IHAhSFOFoiSKag==", "dev": true }, "statuses": { diff --git a/package.json b/package.json index 7db08c23d..7aa34d9db 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "karma-jasmine": "^1.1.0", "karma-phantomjs-launcher": "^1.0.2", "sn-stylekit": "1.0.15", - "standard-file-js": "0.3.0" + "standard-file-js": "0.3.1" }, "license": "GPL-3.0" } From ce7860d4f965535df27eabbeffb26bb1bbfa3e92 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Sun, 24 Jun 2018 14:43:23 -0500 Subject: [PATCH 07/12] Better expiration handling --- .../javascripts/app/controllers/editor.js | 3 +- .../app/directives/views/componentView.js | 16 +++++--- app/assets/stylesheets/app/_modals.scss | 6 ++- .../directives/component-view.html.haml | 40 ++++++++++++------- 4 files changed, 43 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/app/controllers/editor.js b/app/assets/javascripts/app/controllers/editor.js index ca6bcfa30..aba2705c3 100644 --- a/app/assets/javascripts/app/controllers/editor.js +++ b/app/assets/javascripts/app/controllers/editor.js @@ -488,7 +488,8 @@ angular.module('app') this.tagsComponent = component.active ? component : null; } else if(component.area == "editor-editor") { // An editor is already active, ensure the potential replacement is explicitely enabled for this item - if(this.selectedEditor) { + // We also check if the selectedEditor is active. If it's inactive, we want to treat it as an external reference wishing to deactivate this editor (i.e componentView) + if(this.selectedEditor && this.selectedEditor.active) { if(component.isExplicitlyEnabledForItem(this.note)) { this.selectedEditor = component; } diff --git a/app/assets/javascripts/app/directives/views/componentView.js b/app/assets/javascripts/app/directives/views/componentView.js index 4ccb6388d..61b8d98a2 100644 --- a/app/assets/javascripts/app/directives/views/componentView.js +++ b/app/assets/javascripts/app/directives/views/componentView.js @@ -79,7 +79,7 @@ class ComponentView { } $scope.$on("ext-reload-complete", () => { - $scope.reloadStatus(); + $scope.reloadStatus(false); }) $scope.reloadComponent = function() { @@ -87,7 +87,7 @@ class ComponentView { componentManager.reloadComponent($scope.component); } - $scope.reloadStatus = function() { + $scope.reloadStatus = function(doManualReload = true) { let component = $scope.component; $scope.reloading = true; let previouslyValid = $scope.componentValid; @@ -116,9 +116,9 @@ class ComponentView { } } - if(expired && !$scope.triedReloading) { + if(expired && doManualReload) { // Try reloading, handled by footer, which will open Extensions window momentarily to pull in latest data - $scope.triedReloading = true; + // Upon completion, this method, reloadStatus, will be called, upon where doManualReload will be false to prevent recursion. $rootScope.$broadcast("reload-ext-data"); } @@ -133,14 +133,18 @@ class ComponentView { return url; } - $scope.$on("$destroy", function() { - // console.log("Deregistering handler", $scope.identifier, $scope.component.name); + $scope.destroy = function() { componentManager.deregisterHandler($scope.identifier); if($scope.component && !$scope.manualDealloc) { componentManager.deactivateComponent($scope.component, true); } desktopManager.deregisterUpdateObserver($scope.updateObserver); + } + + $scope.$on("$destroy", function() { + // console.log("Deregistering handler", $scope.identifier, $scope.component.name); + $scope.destroy(); }); } diff --git a/app/assets/stylesheets/app/_modals.scss b/app/assets/stylesheets/app/_modals.scss index a0925659e..e9d6c6fb0 100644 --- a/app/assets/stylesheets/app/_modals.scss +++ b/app/assets/stylesheets/app/_modals.scss @@ -99,7 +99,11 @@ .component-view { flex-grow: 1; display: flex; - // overflow: auto; // not sure why we need this. Removed because it creates unncessary scroll bars. Tested on folders extension, creates horizontal scrollbar at bottom on windows + + // not sure why we need this. Removed because it creates unncessary scroll bars. Tested on folders extension, creates horizontal scrollbar at bottom on windows + // overflow: auto; + // Update: we needed that because when we display the expired Extended view, it allows it to scroll vertically. + overflow-y: auto; .sn-component { min-width: 100%; diff --git a/app/assets/templates/directives/component-view.html.haml b/app/assets/templates/directives/component-view.html.haml index 44bafb048..69fc9f54d 100644 --- a/app/assets/templates/directives/component-view.html.haml +++ b/app/assets/templates/directives/component-view.html.haml @@ -26,22 +26,34 @@ .spinner.info.small{"ng-if" => "reloading"} .panel-row - .panel-row - .panel-column - %p Otherwise, please follow the steps below to disable any external editors, so you can edit your note using the plain text editor instead. + .panel-section + %p{"ng-if" => "component.isEditor()"} + Otherwise, please follow the steps below to disable any external editors, + so you can edit your note using the plain text editor instead. - %p - %ol - %li Click the "Editor" menu item above (under the note title). - %li Select "Plain Editor". - %li Repeat this for every note you'd like to access. You can also delete the editor completely to disable it for all notes. To do so, click "Extensions" in the lower left corner of the app, then, for every editor, click "Uninstall". + %p To temporarily disable this extension: - %p - Need help? Please email us at - %a{"href" => "mailto:hello@standardnotes.org", "target" => "_blank"} hello@standardnotes.org - or check out the - %a{"href" => "https://standardnotes.org/help", "target" => "_blank"} Help - page. + .panel-row + .button.info{"ng-click" => "destroy()"} + .label Disable Extension + .spinner.info.small{"ng-if" => "reloading"} + + .panel-row + + %div{"ng-if" => "component.isEditor()"} + %p To disassociate this note from this editor: + + %ol + %li Click the "Editor" menu item above (under the note title). + %li Select "Plain Editor". + %li Repeat this for every note you'd like to access. You can also delete the editor completely to disable it for all notes. To do so, click "Extensions" in the lower left corner of the app, then, for every editor, click "Uninstall". + + %p + Need help? Please email us at + %a{"href" => "mailto:hello@standardnotes.org", "target" => "_blank"} hello@standardnotes.org + or check out the + %a{"href" => "https://standardnotes.org/help", "target" => "_blank"} Help + page. .sn-component{"ng-if" => "error == 'offline-restricted'"} .panel.static From 2014f3c7a1a4342f7abbbaa519e1e5690328160b Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Sun, 24 Jun 2018 14:47:02 -0500 Subject: [PATCH 08/12] Unable to decrypt messaging --- app/assets/templates/notes.html.haml | 2 +- app/assets/templates/tags.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/templates/notes.html.haml b/app/assets/templates/notes.html.haml index 7e516461b..7d22da5bd 100644 --- a/app/assets/templates/notes.html.haml +++ b/app/assets/templates/notes.html.haml @@ -46,7 +46,7 @@ .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, true)", "ng-class" => "{'selected' : ctrl.selectedNote == note}"} %strong.red.medium-text{"ng-if" => "note.conflict_of"} Conflicted copy - %strong.red.medium-text{"ng-if" => "note.errorDecrypting"} Error decrypting + %strong.red.medium-text{"ng-if" => "note.errorDecrypting"} Unable to Decrypt .pinned.tinted{"ng-if" => "note.pinned", "ng-class" => "{'tinted-selected' : ctrl.selectedNote == note}"} %i.icon.ion-bookmark diff --git a/app/assets/templates/tags.html.haml b/app/assets/templates/tags.html.haml index 9f04c49e4..9fd9e3c80 100644 --- a/app/assets/templates/tags.html.haml +++ b/app/assets/templates/tags.html.haml @@ -23,7 +23,7 @@ .count {{ctrl.noteCount(tag)}} .red.small-text.bold{"ng-if" => "tag.conflict_of"} Conflicted copy - .red.small-text.bold{"ng-if" => "tag.errorDecrypting"} Error decrypting + .red.small-text.bold{"ng-if" => "tag.errorDecrypting"} Unable to Decrypt .menu{"ng-if" => "ctrl.selectedTag == tag"} %a.item{"ng-click" => "ctrl.selectedRenameTag($event, tag)", "ng-if" => "!ctrl.editingTag"} Rename From 820ab23c084dd1ab7726335776072244a9dcda82 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Sun, 24 Jun 2018 18:08:44 -0500 Subject: [PATCH 09/12] Remove stream observers when reloading component --- app/assets/javascripts/app/services/componentManager.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/assets/javascripts/app/services/componentManager.js b/app/assets/javascripts/app/services/componentManager.js index e186f5155..a7d3eaaba 100644 --- a/app/assets/javascripts/app/services/componentManager.js +++ b/app/assets/javascripts/app/services/componentManager.js @@ -815,6 +815,14 @@ class ComponentManager { } } + this.streamObservers = this.streamObservers.filter(function(o){ + return o.component !== component; + }) + + this.contextStreamObservers = this.contextStreamObservers.filter(function(o){ + return o.component !== component; + }) + if(component.area == "themes") { this.postActiveThemeToAllComponents(); } From 6cf92800e8cf58a165789427d9acf7748b22ad41 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Sun, 24 Jun 2018 18:21:58 -0500 Subject: [PATCH 10/12] Notify observers of tag association without waiting for sync --- app/assets/javascripts/app/controllers/editor.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/assets/javascripts/app/controllers/editor.js b/app/assets/javascripts/app/controllers/editor.js index aba2705c3..922c9cbd4 100644 --- a/app/assets/javascripts/app/controllers/editor.js +++ b/app/assets/javascripts/app/controllers/editor.js @@ -532,12 +532,20 @@ angular.module('app') if(data.item.content_type == "Tag") { var tag = modelManager.findItem(data.item.uuid); this.addTag(tag); + + // Currently extensions are not notified of association until a full server sync completes. + // We need a better system for this, but for now, we'll manually notify observers + modelManager.notifySyncObserversOfModels([this.note], ModelManager.MappingSourceLocalSaved); } } else if(action === "deassociate-item") { var tag = modelManager.findItem(data.item.uuid); this.removeTag(tag); + + // Currently extensions are not notified of association until a full server sync completes. + // We need a better system for this, but for now, we'll manually notify observers + modelManager.notifySyncObserversOfModels([this.note], ModelManager.MappingSourceLocalSaved); } else if(action === "save-items" || action === "save-success" || action == "save-error") { From fcbe8fe2a63b12d20434cd62a57d6af9f4c996df Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Mon, 25 Jun 2018 11:10:19 -0500 Subject: [PATCH 11/12] Handle offline saving errors --- .../javascripts/app/controllers/editor.js | 23 ++++++++++++-- .../javascripts/app/services/dbManager.js | 21 +++++++++---- .../app/services/storageManager.js | 6 ++-- .../javascripts/app/services/syncManager.js | 30 ++++++++++++++++++- 4 files changed, 69 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/app/controllers/editor.js b/app/assets/javascripts/app/controllers/editor.js index 922c9cbd4..39250180e 100644 --- a/app/assets/javascripts/app/controllers/editor.js +++ b/app/assets/javascripts/app/controllers/editor.js @@ -40,6 +40,19 @@ angular.module('app') this.loadTagsString(); }.bind(this)); + // Right now this only handles offline saving status changes. + this.syncStatusObserver = syncManager.registerSyncStatusObserver((status) => { + if(status.localError) { + $timeout(() => { + this.showErrorStatus({ + message: "Offline Saving Issue", + desc: "Changes not saved" + }); + }, 500) + } else { + } + }) + modelManager.addItemSyncObserver("component-manager", "Note", (allItems, validItems, deletedItems, source) => { if(!this.note) { return; } @@ -283,10 +296,16 @@ angular.module('app') this.noteStatus = $sce.trustAsHtml(status); } - this.showErrorStatus = function() { + this.showErrorStatus = function(error) { + if(!error) { + error = { + message: "Sync Unreachable", + desc: "All changes saved offline" + } + } this.saveError = true; this.syncTakingTooLong = false; - this.noteStatus = $sce.trustAsHtml("Sync Unreachable
All changes saved offline") + this.noteStatus = $sce.trustAsHtml(`${error.message}
${error.desc}`) } this.contentChanged = function() { diff --git a/app/assets/javascripts/app/services/dbManager.js b/app/assets/javascripts/app/services/dbManager.js index 521fe2a1e..fb9de21d3 100644 --- a/app/assets/javascripts/app/services/dbManager.js +++ b/app/assets/javascripts/app/services/dbManager.js @@ -90,11 +90,11 @@ class DBManager { this.saveModels([item]); } - saveModels(items, callback) { + saveModels(items, onsuccess, onerror) { if(items.length == 0) { - if(callback) { - callback(); + if(onsuccess) { + onsuccess(); } return; } @@ -109,6 +109,17 @@ class DBManager { console.log("Transaction error:", event.target.errorCode); }; + transaction.onabort = function(event) { + console.log("Offline saving aborted:", event); + var error = event.target.error; + if(error.name == "QuotaExceededError") { + alert("Unable to save changes locally because your device is out of space. Please free up some disk space and try again, otherwise, your data may end up in an inconsistent state."); + } else { + alert(`Unable to save changes locally due to an unknown system issue. Issue Code: ${error.code} Issue Name: ${error.name}.`); + } + onerror && onerror(error); + }; + var itemObjectStore = transaction.objectStore("items"); var i = 0; putNext(); @@ -119,8 +130,8 @@ class DBManager { itemObjectStore.put(item).onsuccess = putNext; ++i; } else { - if(callback){ - callback(); + if(onsuccess){ + onsuccess(); } } } diff --git a/app/assets/javascripts/app/services/storageManager.js b/app/assets/javascripts/app/services/storageManager.js index 6c52e8830..c82500c68 100644 --- a/app/assets/javascripts/app/services/storageManager.js +++ b/app/assets/javascripts/app/services/storageManager.js @@ -208,11 +208,11 @@ class StorageManager { this.saveModels([item]); } - saveModels(items, callback) { + saveModels(items, onsuccess, onerror) { if(this.modelStorageMode == StorageManager.Fixed) { - this.dbManager.saveModels(items, callback); + this.dbManager.saveModels(items, onsuccess, onerror); } else { - callback && callback(); + onsuccess && onsuccess(); } } diff --git a/app/assets/javascripts/app/services/syncManager.js b/app/assets/javascripts/app/services/syncManager.js index 5615c5ac9..031d4169d 100644 --- a/app/assets/javascripts/app/services/syncManager.js +++ b/app/assets/javascripts/app/services/syncManager.js @@ -11,6 +11,7 @@ class SyncManager { this.storageManager = storageManager; this.passcodeManager = passcodeManager; this.syncStatus = {}; + this.syncStatusObservers = []; } get serverURL() { @@ -21,6 +22,22 @@ class SyncManager { return this.storageManager.getItem("mk"); } + registerSyncStatusObserver(callback) { + var observer = {key: new Date(), callback: callback}; + this.syncStatusObservers.push(observer); + return observer; + } + + removeSyncStatusObserver(observer) { + _.pull(this.syncStatusObservers, observer); + } + + syncStatusDidChange() { + this.syncStatusObservers.forEach((observer) => { + observer.callback(this.syncStatus); + }) + } + writeItemsToLocalStorage(items, offlineOnly, callback) { if(items.length == 0) { callback && callback(); @@ -38,7 +55,18 @@ class SyncManager { } return itemParams; })).then((params) => { - this.storageManager.saveModels(params, callback); + this.storageManager.saveModels(params, () => { + // on success + if(this.syncStatus.localError) { + this.syncStatus.localError = null; + this.syncStatusDidChange(); + } + callback && callback(); + }, (error) => { + // on error + this.syncStatus.localError = error; + this.syncStatusDidChange(); + }); }) } From 498049adcbabd14d6c60cd071f974fd5a20893cf Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Tue, 26 Jun 2018 00:53:10 -0500 Subject: [PATCH 12/12] Ignore client_updated_at when checking for equality --- app/assets/javascripts/app/models/api/item.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/app/models/api/item.js b/app/assets/javascripts/app/models/api/item.js index 6fe6fdee7..888c477de 100644 --- a/app/assets/javascripts/app/models/api/item.js +++ b/app/assets/javascripts/app/models/api/item.js @@ -256,6 +256,11 @@ class Item { return []; } + // Same as above, but keys inside appData[AppDomain] + appDataKeysToIgnoreWhenCheckingContentEquality() { + return ["client_updated_at"]; + } + isItemContentEqualWith(otherItem) { let omit = (obj, keys) => { for(var key of keys) { @@ -263,8 +268,14 @@ class Item { } return obj; } - var left = omit(this.structureParams(), this.keysToIgnoreWhenCheckingContentEquality()); - var right = omit(otherItem.structureParams(), otherItem.keysToIgnoreWhenCheckingContentEquality()); + + var left = this.structureParams(); + left.appData[AppDomain] = omit(left.appData[AppDomain], this.appDataKeysToIgnoreWhenCheckingContentEquality()); + left = omit(left, this.keysToIgnoreWhenCheckingContentEquality()); + + var right = otherItem.structureParams(); + right.appData[AppDomain] = omit(right.appData[AppDomain], otherItem.appDataKeysToIgnoreWhenCheckingContentEquality()); + right = omit(right, otherItem.keysToIgnoreWhenCheckingContentEquality()); return JSON.stringify(left) === JSON.stringify(right) }