diff --git a/app/assets/javascripts/app/controllers/editor.js b/app/assets/javascripts/app/controllers/editor.js index a4d35eeab..ed7975894 100644 --- a/app/assets/javascripts/app/controllers/editor.js +++ b/app/assets/javascripts/app/controllers/editor.js @@ -56,7 +56,7 @@ angular.module('app') // 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) { + if(this.note.deleted || this.note.content.trashed) { $rootScope.notifyDelete(); return; } @@ -381,7 +381,7 @@ angular.module('app') } } - this.deleteNote = async function() { + this.deleteNote = async function(permanently) { let run = () => { $timeout(() => { if(this.note.locked) { @@ -390,8 +390,15 @@ angular.module('app') } 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); + let message = permanently ? `Are you sure you want to permanently delete ${title}?` + : `Are you sure you want to move ${title} to the trash?` + if(confirm(message)) { + if(permanently) { + this.remove()(this.note); + } else { + this.note.content.trashed = true; + this.changesMade({dontUpdateClientModified: true, dontUpdatePreviews: true}); + } this.showMenu = false; } }); @@ -406,6 +413,27 @@ angular.module('app') } } + this.restoreTrashedNote = function() { + this.note.content.trashed = false; + this.changesMade({dontUpdateClientModified: true, dontUpdatePreviews: true}); + } + + this.deleteNotePermanantely = function() { + this.deleteNote(true); + } + + this.getTrashCount = function() { + return modelManager.trashedItems().length; + } + + this.emptyTrash = function() { + let count = this.getTrashCount(); + if(confirm(`Are you sure you want to permanently delete ${count} note(s)?`)) { + modelManager.emptyTrash(); + syncManager.sync(); + } + } + this.togglePin = function() { this.note.setAppDataItem("pinned", !this.note.pinned); this.changesMade({dontUpdatePreviews: true}); diff --git a/app/assets/javascripts/app/controllers/footer.js b/app/assets/javascripts/app/controllers/footer.js index 15fcd07f2..0bb3add86 100644 --- a/app/assets/javascripts/app/controllers/footer.js +++ b/app/assets/javascripts/app/controllers/footer.js @@ -34,6 +34,26 @@ angular.module('app') this.securityUpdateAvailable = authManager.securityUpdateAvailable; }) + $rootScope.$on("did-begin-local-backup", () => { + $timeout(() => { + this.arbitraryStatusMessage = "Saving local backup..."; + }) + }); + + $rootScope.$on("did-finish-local-backup", (event, data) => { + $timeout(() => { + if(data.success) { + this.arbitraryStatusMessage = "Successfully saved backup."; + } else { + this.arbitraryStatusMessage = "Unable to save local backup."; + } + + $timeout(() => { + this.arbitraryStatusMessage = null; + }, 2000) + }) + }); + this.openSecurityUpdate = function() { authManager.presentPasswordWizard("upgrade-security"); } diff --git a/app/assets/javascripts/app/controllers/home.js b/app/assets/javascripts/app/controllers/home.js index 0c155635f..4905bab32 100644 --- a/app/assets/javascripts/app/controllers/home.js +++ b/app/assets/javascripts/app/controllers/home.js @@ -40,10 +40,6 @@ angular.module('app') openDatabase(); // Retrieve local data and begin sycing timer initiateSync(); - // Configure "All" psuedo-tag - loadAllTag(); - // Configure "Archived" psuedo-tag - loadArchivedTag(); } if(passcodeManager.isLocked()) { @@ -94,7 +90,6 @@ angular.module('app') syncManager.loadLocalItems().then(() => { $timeout(() => { - $scope.allTag.didLoad = true; $rootScope.$broadcast("initial-data-loaded"); // This needs to be processed first before sync is called so that singletonManager observers function properly. syncManager.sync(); // refresh every 30s @@ -113,25 +108,6 @@ angular.module('app') }) } - function loadAllTag() { - var allTag = new SNTag({content: {title: "All"}}); - allTag.all = true; - allTag.needsLoad = true; - $scope.allTag = allTag; - $scope.tags = modelManager.tags; - $scope.allTag.notes = modelManager.notes; - } - - function loadArchivedTag() { - var archiveTag = new SNSmartTag({content: {title: "Archived", predicate: ["archived", "=", true]}}); - Object.defineProperty(archiveTag, "notes", { - get: () => { - return modelManager.notesMatchingPredicate(archiveTag.content.predicate); - } - }); - $scope.archiveTag = archiveTag; - } - /* Editor Callbacks */ @@ -220,7 +196,7 @@ angular.module('app') $scope.notesAddNew = function(note) { modelManager.addItem(note); - if(!$scope.selectedTag.all && !$scope.selectedTag.isSmartTag()) { + if(!$scope.selectedTag.isSmartTag()) { $scope.selectedTag.addItemAsRelationship(note); $scope.selectedTag.setDirty(true); } diff --git a/app/assets/javascripts/app/controllers/notes.js b/app/assets/javascripts/app/controllers/notes.js index 374560039..6f58ceca9 100644 --- a/app/assets/javascripts/app/controllers/notes.js +++ b/app/assets/javascripts/app/controllers/notes.js @@ -41,6 +41,13 @@ angular.module('app') this.loadPreferences(); }); + modelManager.addItemSyncObserver("note-list", "Note", (allItems, validItems, deletedItems, source, sourceKey) => { + // Note has changed values, reset its flags + for(var note of allItems) { + note.flags = null; + } + }); + this.loadPreferences = function() { let prevSortValue = this.sortBy; @@ -129,7 +136,7 @@ angular.module('app') if(this.isFiltering()) { return `${this.tag.notes.filter((i) => {return i.visible;}).length} search results`; } else if(this.tag) { - return `${this.tag.title} notes`; + return `${this.tag.title}`; } } @@ -143,19 +150,63 @@ angular.module('app') base += " Title"; } - if(!this.tag || !this.tag.isSmartTag()) { - // These rules don't apply for smart tags - if(this.showArchived) { - base += " | + Archived" - } - if(this.hidePinned) { - base += " | – Pinned" - } + if(this.showArchived) { + base += " | + Archived" + } + if(this.hidePinned) { + base += " | – Pinned" } return base; } + this.getNoteFlags = (note) => { + if(note.flags) { + return note.flags; + } + + let flags = []; + + if(note.pinned) { + flags.push({ + text: "Pinned", + class: "info" + }) + } + + if(note.archived) { + flags.push({ + text: "Archived", + class: "warning" + }) + } + + if(note.content.protected) { + flags.push({ + text: "Protected", + class: "success" + }) + } + + if(note.locked) { + flags.push({ + text: "Locked", + class: "neutral" + }) + } + + if(note.content.trashed) { + flags.push({ + text: "Deleted", + class: "danger" + }) + } + + note.flags = flags; + + return flags; + } + this.toggleKey = function(key) { this[key] = !this[key]; authManager.setUserPrefValue(key, this[key]); @@ -254,13 +305,17 @@ angular.module('app') this.noteFilter = {text : ''}; this.filterNotes = function(note) { - var canShowArchived = false, canShowPinned = true; + let canShowArchived = this.showArchived, canShowPinned = !this.hidePinned; + let isTrash = this.tag.content.isTrashTag; + + if(!isTrash && note.content.trashed) { + note.visible = false; + return note.visible; + } + var isSmartTag = this.tag.isSmartTag(); if(isSmartTag) { - canShowArchived = this.tag.isReferencingArchivedNotes(); - } else { - canShowArchived = this.showArchived; - canShowPinned = !this.hidePinned; + canShowArchived = canShowArchived || this.tag.content.isArchiveTag || isTrash; } if((note.archived && !canShowArchived) || (note.pinned && !canShowPinned)) { @@ -339,11 +394,15 @@ angular.module('app') } this.shouldShowTags = function(note) { - if(this.hideTags) { + if(this.hideTags || note.content.protected) { return false; } - if(this.tag.all) { + if(this.tag.content.isAllTag) { + return note.tags && note.tags.length > 0; + } + + if(this.tag.isSmartTag()) { return true; } diff --git a/app/assets/javascripts/app/controllers/tags.js b/app/assets/javascripts/app/controllers/tags.js index 4d6daa3a7..c3d08ae94 100644 --- a/app/assets/javascripts/app/controllers/tags.js +++ b/app/assets/javascripts/app/controllers/tags.js @@ -7,8 +7,6 @@ angular.module('app') selectionMade: "&", save: "&", tags: "=", - allTag: "=", - archiveTag: "=", updateNoteTag: "&", removeTag: "&" }, @@ -17,25 +15,23 @@ angular.module('app') controller: 'TagsCtrl', controllerAs: 'ctrl', bindToController: true, - - link:function(scope, elem, attrs, ctrl) { - scope.$watch('ctrl.tags', function(newTags){ - if(newTags) { - ctrl.setTags(newTags); - } - }); - - scope.$watch('ctrl.allTag', function(allTag){ - if(allTag) { - ctrl.setAllTag(allTag); - } - }); - } } }) - .controller('TagsCtrl', function ($rootScope, modelManager, $timeout, componentManager, authManager) { + .controller('TagsCtrl', function ($rootScope, modelManager, syncManager, $timeout, componentManager, authManager) { + // Wrap in timeout so that selectTag is defined + $timeout(() => { + this.smartTags = modelManager.getSmartTags(); + this.selectTag(this.smartTags[0]); + }) - var initialLoad = true; + syncManager.addEventHandler((syncEvent, data) => { + if(syncEvent == "local-data-loaded" + || syncEvent == "sync:completed" + || syncEvent == "local-data-incremental-load") { + this.tags = modelManager.tags; + this.smartTags = modelManager.getSmartTags(); + } + }); this.panelController = {}; @@ -69,40 +65,27 @@ angular.module('app') }.bind(this), actionHandler: function(component, action, data){ if(action === "select-item") { if(data.item.content_type == "Tag") { - var tag = modelManager.findItem(data.item.uuid); + let tag = modelManager.findItem(data.item.uuid); if(tag) { this.selectTag(tag); } } else if(data.item.content_type == "SN|SmartTag") { - var tag = new SNSmartTag(data.item); - Object.defineProperty(tag, "notes", { - get: () => { - return modelManager.notesMatchingPredicate(tag.content.predicate); - } - }); - this.selectTag(tag); + let smartTag = new SNSmartTag(data.item); + this.selectTag(smartTag); } } else if(action === "clear-selection") { - this.selectTag(this.allTag); + this.selectTag(this.smartTags[0]); } }.bind(this)}); - this.setAllTag = function(allTag) { - this.selectTag(this.allTag); - } - - this.setTags = function(tags) { - if(initialLoad) { - initialLoad = false; - this.selectTag(this.allTag); - } else { - if(tags && tags.length > 0) { - this.selectTag(tags[0]); - } - } - } - this.selectTag = function(tag) { + if(tag.isSmartTag()) { + Object.defineProperty(tag, "notes", { + get: () => { + return modelManager.notesMatchingSmartTag(tag); + } + }); + } this.selectedTag = tag; tag.conflict_of = null; // clear conflict this.selectionMade()(tag); @@ -161,12 +144,12 @@ angular.module('app') this.selectedDeleteTag = function(tag) { this.removeTag()(tag); - this.selectTag(this.allTag); + this.selectTag(this.smartTags[0]); } this.noteCount = function(tag) { var validNotes = SNNote.filterDummyNotes(tag.notes).filter(function(note){ - return !note.archived; + return !note.archived && !note.content.trashed; }); return validNotes.length; } diff --git a/app/assets/javascripts/app/services/desktopManager.js b/app/assets/javascripts/app/services/desktopManager.js index 58ce492d9..a4c02fb18 100644 --- a/app/assets/javascripts/app/services/desktopManager.js +++ b/app/assets/javascripts/app/services/desktopManager.js @@ -182,6 +182,13 @@ class DesktopManager { this.majorDataChangeHandler = handler; } + desktop_didBeginBackup() { + this.$rootScope.$broadcast("did-begin-local-backup"); + } + + desktop_didFinishBackup(success) { + this.$rootScope.$broadcast("did-finish-local-backup", {success: success}); + } } angular.module('app').service('desktopManager', DesktopManager); diff --git a/app/assets/javascripts/app/services/migrationManager.js b/app/assets/javascripts/app/services/migrationManager.js index d6d522203..66f52bca8 100644 --- a/app/assets/javascripts/app/services/migrationManager.js +++ b/app/assets/javascripts/app/services/migrationManager.js @@ -21,7 +21,7 @@ class MigrationManager extends SFMigrationManager { return { name: "editor-to-component", content_type: "SN|Editor", - handler: (editors) => { + handler: async (editors) => { // Convert editors to components for(var editor of editors) { // If there's already a component for this url, then skip this editor @@ -60,7 +60,8 @@ class MigrationManager extends SFMigrationManager { return { name: "component-url-to-hosted-url", content_type: "SN|Component", - handler: (components) => { + handler: async (components) => { + let hasChanges = false; var notes = this.modelManager.validItemsForContentType("Note"); for(var note of notes) { for(var component of components) { @@ -69,10 +70,14 @@ class MigrationManager extends SFMigrationManager { note.setDomainDataItem(component.uuid, clientData, ComponentManager.ClientDataDomain); note.setDomainDataItem(component.hosted_url, null, ComponentManager.ClientDataDomain); note.setDirty(true, true); // dont update client date + hasChanges = true; } } } - this.syncManager.sync(); + + if(hasChanges) { + this.syncManager.sync(); + } } } } diff --git a/app/assets/javascripts/app/services/modelManager.js b/app/assets/javascripts/app/services/modelManager.js index 9af2df562..2ca1e0571 100644 --- a/app/assets/javascripts/app/services/modelManager.js +++ b/app/assets/javascripts/app/services/modelManager.js @@ -22,6 +22,8 @@ class ModelManager extends SFModelManager { this.components = []; this.storageManager = storageManager; + + this.buildSystemSmartTags(); } handleSignout() { @@ -114,9 +116,44 @@ class ModelManager extends SFModelManager { } } - notesMatchingPredicate(predicate) { + notesMatchingSmartTag(tag) { let contentTypePredicate = new SFPredicate("content_type", "=", "Note"); - return this.itemsMatchingPredicates([contentTypePredicate, predicate]); + let predicates = [contentTypePredicate, tag.content.predicate]; + if(!tag.content.isTrashTag) { + let notTrashedPredicate = new SFPredicate("content.trashed", "=", false); + predicates.push(notTrashedPredicate); + } + return this.itemsMatchingPredicates(predicates); + } + + trashSmartTag() { + return this.systemSmartTags.find((tag) => tag.content.isTrashTag); + } + + trashedItems() { + return this.notesMatchingSmartTag(this.trashSmartTag()); + } + + emptyTrash() { + let notes = this.trashedItems(); + for(let note of notes) { + this.setItemToBeDeleted(note); + } + } + + buildSystemSmartTags() { + this.systemSmartTags = SNSmartTag.systemSmartTags(); + } + + getSmartTagWithId(id) { + return this.getSmartTags().find((candidate) => candidate.uuid == id); + } + + getSmartTags() { + let userTags = this.validItemsForContentType("SN|SmartTag").sort((a, b) => { + return a.content.title < b.content.title ? -1 : 1; + }); + return this.systemSmartTags.concat(userTags); } /* @@ -133,7 +170,10 @@ class ModelManager extends SFModelManager { "SN|Editor" : "editor", "SN|Theme" : "theme", "SF|Extension" : "server extension", - "SF|MFA" : "two-factor authentication setting" + "SF|MFA" : "two-factor authentication setting", + "SN|FileSafe|Credentials": "FileSafe credential", + "SN|FileSafe|FileMetadata": "FileSafe file", + "SN|FileSafe|Integration": "FileSafe integration" }[contentType]; } diff --git a/app/assets/javascripts/app/services/privilegesManager.js b/app/assets/javascripts/app/services/privilegesManager.js index a1c83f077..3d30fd280 100644 --- a/app/assets/javascripts/app/services/privilegesManager.js +++ b/app/assets/javascripts/app/services/privilegesManager.js @@ -1,63 +1,31 @@ -class PrivilegesManager { +class PrivilegesManager extends SFPrivilegesManager { + + constructor(passcodeManager, authManager, syncManager, singletonManager, modelManager, storageManager, $rootScope, $compile) { + super(modelManager, syncManager, singletonManager); - constructor(passcodeManager, authManager, singletonManager, modelManager, storageManager, $rootScope, $compile) { - this.passcodeManager = passcodeManager; - this.authManager = authManager; - this.singletonManager = singletonManager; - this.modelManager = modelManager; - this.storageManager = storageManager; this.$rootScope = $rootScope; this.$compile = $compile; - this.loadPrivileges(); - - PrivilegesManager.CredentialAccountPassword = "CredentialAccountPassword"; - PrivilegesManager.CredentialLocalPasscode = "CredentialLocalPasscode"; - - PrivilegesManager.ActionManageExtensions = "ActionManageExtensions"; - PrivilegesManager.ActionManageBackups = "ActionManageBackups"; - PrivilegesManager.ActionViewProtectedNotes = "ActionViewProtectedNotes"; - PrivilegesManager.ActionManagePrivileges = "ActionManagePrivileges"; - PrivilegesManager.ActionManagePasscode = "ActionManagePasscode"; - PrivilegesManager.ActionDeleteNote = "ActionDeleteNote"; - - PrivilegesManager.SessionExpiresAtKey = "SessionExpiresAtKey"; - PrivilegesManager.SessionLengthKey = "SessionLengthKey"; - - PrivilegesManager.SessionLengthNone = 0; - PrivilegesManager.SessionLengthFiveMinutes = 300; - PrivilegesManager.SessionLengthOneHour = 3600; - PrivilegesManager.SessionLengthOneWeek = 604800; - - this.availableActions = [ - PrivilegesManager.ActionViewProtectedNotes, - PrivilegesManager.ActionDeleteNote, - PrivilegesManager.ActionManagePasscode, - PrivilegesManager.ActionManageBackups, - PrivilegesManager.ActionManageExtensions, - PrivilegesManager.ActionManagePrivileges, - ] - - this.availableCredentials = [ - PrivilegesManager.CredentialAccountPassword, - PrivilegesManager.CredentialLocalPasscode - ]; - - this.sessionLengths = [ - PrivilegesManager.SessionLengthNone, - PrivilegesManager.SessionLengthFiveMinutes, - PrivilegesManager.SessionLengthOneHour, - PrivilegesManager.SessionLengthOneWeek, - PrivilegesManager.SessionLengthIndefinite - ] - } - - getAvailableActions() { - return this.availableActions; - } - - getAvailableCredentials() { - return this.availableCredentials; + this.setDelegate({ + isOffline: async () => { + return authManager.offline(); + }, + hasLocalPasscode: async () => { + return passcodeManager.hasPasscode(); + }, + saveToStorage: async (key, value) => { + return storageManager.setItem(key, value, storageManager.bestStorageMode()); + }, + getFromStorage: async (key) => { + return storageManager.getItem(key, storageManager.bestStorageMode()); + }, + verifyAccountPassword: async (password) => { + return authManager.verifyAccountPassword(password); + }, + verifyLocalPasscode: async (passcode) => { + return passcodeManager.verifyPasscode(passcode); + }, + }); } presentPrivilegesModal(action, onSuccess, onCancel) { @@ -86,25 +54,6 @@ class PrivilegesManager { this.currentAuthenticationElement = el; } - async netCredentialsForAction(action) { - let credentials = (await this.getPrivileges()).getCredentialsForAction(action); - let netCredentials = []; - - for(var cred of credentials) { - if(cred == PrivilegesManager.CredentialAccountPassword) { - if(!this.authManager.offline()) { - netCredentials.push(cred); - } - } else if(cred == PrivilegesManager.CredentialLocalPasscode) { - if(this.passcodeManager.hasPasscode()) { - netCredentials.push(cred); - } - } - } - - return netCredentials; - } - presentPrivilegesManagementModal() { var scope = this.$rootScope.$new(true); var el = this.$compile( "")(scope); @@ -115,195 +64,6 @@ class PrivilegesManager { return this.currentAuthenticationElement != null; } - async loadPrivileges() { - return new Promise((resolve, reject) => { - let prefsContentType = "SN|Privileges"; - let contentTypePredicate = new SFPredicate("content_type", "=", prefsContentType); - this.singletonManager.registerSingleton([contentTypePredicate], (resolvedSingleton) => { - this.privileges = resolvedSingleton; - if(!this.privileges.content.desktopPrivileges) { - this.privileges.content.desktopPrivileges = {}; - } - resolve(resolvedSingleton); - }, (valueCallback) => { - // Safe to create. Create and return object. - var privs = new SNPrivileges({content_type: prefsContentType}); - this.modelManager.addItem(privs); - privs.setDirty(true); - this.$rootScope.sync(); - valueCallback(privs); - resolve(privs); - }); - }); - } - - async getPrivileges() { - if(this.privileges) { - return this.privileges; - } else { - return this.loadPrivileges(); - } - } - - displayInfoForCredential(credential) { - let metadata = {} - - metadata[PrivilegesManager.CredentialAccountPassword] = { - label: "Account Password", - prompt: "Please enter your account password." - } - - metadata[PrivilegesManager.CredentialLocalPasscode] = { - label: "Local Passcode", - prompt: "Please enter your local passcode." - } - - return metadata[credential]; - } - - displayInfoForAction(action) { - let metadata = {}; - - metadata[PrivilegesManager.ActionManageExtensions] = { - label: "Manage Extensions" - }; - - metadata[PrivilegesManager.ActionManageBackups] = { - label: "Download/Import Backups" - }; - - metadata[PrivilegesManager.ActionViewProtectedNotes] = { - label: "View Protected Notes" - }; - - metadata[PrivilegesManager.ActionManagePrivileges] = { - label: "Manage Privileges" - }; - - metadata[PrivilegesManager.ActionManagePasscode] = { - label: "Manage Passcode" - } - - metadata[PrivilegesManager.ActionDeleteNote] = { - label: "Delete Notes" - } - - return metadata[action]; - } - - getSessionLengthOptions() { - return [ - { - value: PrivilegesManager.SessionLengthNone, - label: "Don't Remember" - }, - { - value: PrivilegesManager.SessionLengthFiveMinutes, - label: "5 Minutes" - }, - { - value: PrivilegesManager.SessionLengthOneHour, - label: "1 Hour" - }, - { - value: PrivilegesManager.SessionLengthOneWeek, - label: "1 Week" - } - ] - } - - async setSessionLength(length) { - let addToNow = (seconds) => { - let date = new Date(); - date.setSeconds(date.getSeconds() + seconds); - return date; - } - - let expiresAt = addToNow(length); - - return Promise.all([ - this.storageManager.setItem(PrivilegesManager.SessionExpiresAtKey, JSON.stringify(expiresAt), this.storageManager.bestStorageMode()), - this.storageManager.setItem(PrivilegesManager.SessionLengthKey, JSON.stringify(length), this.storageManager.bestStorageMode()), - ]) - } - - async clearSession() { - return this.setSessionLength(PrivilegesManager.SessionLengthNone); - } - - async getSelectedSessionLength() { - let length = await this.storageManager.getItem(PrivilegesManager.SessionLengthKey, this.storageManager.bestStorageMode()); - if(length) { - return JSON.parse(length); - } else { - return PrivilegesManager.SessionLengthNone; - } - } - - async getSessionExpirey() { - let expiresAt = await this.storageManager.getItem(PrivilegesManager.SessionExpiresAtKey, this.storageManager.bestStorageMode()); - if(expiresAt) { - return new Date(JSON.parse(expiresAt)); - } else { - return new Date(); - } - } - - async actionHasPrivilegesConfigured(action) { - return (await this.netCredentialsForAction(action)).length > 0; - } - - async actionRequiresPrivilege(action) { - let expiresAt = await this.getSessionExpirey(); - if(expiresAt > new Date()) { - return false; - } - return (await this.netCredentialsForAction(action)).length > 0; - } - - async savePrivileges() { - let privs = await this.getPrivileges(); - privs.setDirty(true); - this.$rootScope.sync(); - } - - async authenticateAction(action, credentialAuthMapping) { - var requiredCredentials = (await this.netCredentialsForAction(action)); - var successfulCredentials = [], failedCredentials = []; - - for(let requiredCredential of requiredCredentials) { - var passesAuth = await this._verifyAuthenticationParameters(requiredCredential, credentialAuthMapping[requiredCredential]); - if(passesAuth) { - successfulCredentials.push(requiredCredential); - } else { - failedCredentials.push(requiredCredential); - } - } - - return { - success: failedCredentials.length == 0, - successfulCredentials: successfulCredentials, - failedCredentials: failedCredentials - } - } - - async _verifyAuthenticationParameters(credential, value) { - - let verifyAccountPassword = async (password) => { - return this.authManager.verifyAccountPassword(password); - } - - let verifyLocalPasscode = async (passcode) => { - return this.passcodeManager.verifyPasscode(passcode); - } - - if(credential == PrivilegesManager.CredentialAccountPassword) { - return verifyAccountPassword(value); - } else if(credential == PrivilegesManager.CredentialLocalPasscode) { - return verifyLocalPasscode(value); - } - } - } angular.module('app').service('privilegesManager', PrivilegesManager); diff --git a/app/assets/stylesheets/app/_notes.scss b/app/assets/stylesheets/app/_notes.scss index 5e6a549cc..fc0ab7779 100644 --- a/app/assets/stylesheets/app/_notes.scss +++ b/app/assets/stylesheets/app/_notes.scss @@ -115,17 +115,8 @@ } .tags-string { - margin-bottom: 4px; - font-size: 13px; - } - - .pinned { - .icon { - display: inline-block; - vertical-align: top; - margin-top: 2px; - margin-right: 2px; - } + margin-top: 4px; + font-size: 12px; } .note-preview { @@ -163,9 +154,45 @@ display: flex; flex-direction: row; align-items: center; + margin-bottom: 8px; - .note-flag { - margin-right: 10px; + .flag { + padding: 4px; + padding-left: 6px; + padding-right: 6px; + border-radius: 2px; + margin-right: 4px; + + &.info { + background-color: var(--sn-stylekit-info-color); + color: var(--sn-stylekit-info-contrast-color); + } + + &.success { + background-color: var(--sn-stylekit-success-color); + color: var(--sn-stylekit-success-contrast-color); + } + + &.warning { + background-color: var(--sn-stylekit-warning-color); + color: var(--sn-stylekit-warning-contrast-color); + } + + &.neutral { + background-color: var(--sn-stylekit-neutral-color); + color: var(--sn-stylekit-neutral-contrast-color); + } + + &.danger { + background-color: var(--sn-stylekit-danger-color); + color: var(--sn-stylekit-danger-contrast-color); + } + + .label { + font-size: 10px; + font-weight: bold; + text-align: center; + } } } @@ -186,8 +213,9 @@ background-color: var(--sn-stylekit-info-color); color: var(--sn-stylekit-info-contrast-color); - .note-flag { - color: var(--sn-stylekit-info-contrast-color); + .note-flags .flag { + background-color: var(--sn-stylekit-info-contrast-color); + color: var(--sn-stylekit-info-color); } progress { diff --git a/app/assets/stylesheets/app/_tags.scss b/app/assets/stylesheets/app/_tags.scss index 8049e4b88..42d839fc3 100644 --- a/app/assets/stylesheets/app/_tags.scss +++ b/app/assets/stylesheets/app/_tags.scss @@ -11,7 +11,7 @@ flex-direction: column; } - #tags-title-bar { + .tags-title-section { color: var(--sn-stylekit-secondary-foreground-color); padding-top: 15px; padding-bottom: 8px; @@ -37,6 +37,13 @@ } } + .no-tags-placeholder { + padding: 0px 12px; + font-size: 12px; + opacity: 0.4; + margin-top: -5px; + } + .tag { min-height: 30px; padding: 5px 12px; diff --git a/app/assets/templates/directives/component-view.html.haml b/app/assets/templates/directives/component-view.html.haml index e68436225..87f036520 100644 --- a/app/assets/templates/directives/component-view.html.haml +++ b/app/assets/templates/directives/component-view.html.haml @@ -19,7 +19,7 @@ .sk-label Disable Active Theme .sn-component{"ng-if" => "expired"} - .sk-app-bar + .sk-app-bar.no-edges.no-top-edge .left .sk-app-bar-item .sk-app-bar-item-column diff --git a/app/assets/templates/directives/privileges-management-modal.html.haml b/app/assets/templates/directives/privileges-management-modal.html.haml index fb9654a05..ba57cb537 100644 --- a/app/assets/templates/directives/privileges-management-modal.html.haml +++ b/app/assets/templates/directives/privileges-management-modal.html.haml @@ -36,6 +36,6 @@ Note that when your application is unlocked, your data exists in temporary memory in an unencrypted state. Privileges are meant to protect against unwanted access in the event of an unlocked application, but do not affect data encryption state. %p.sk-p - Privileges sync across your other devices (not including mobile); however, note that if you require + Privileges sync across your other devices; however, note that if you require a "Local Passcode" privilege, and another device does not have a local passcode set up, the local passcode requirement will be ignored on that device. diff --git a/app/assets/templates/editor.html.haml b/app/assets/templates/editor.html.haml index 25386c77a..f0e6cd73c 100644 --- a/app/assets/templates/editor.html.haml +++ b/app/assets/templates/editor.html.haml @@ -38,7 +38,14 @@ %menu-row{"label" => "ctrl.note.locked ? 'Unlock' : 'Lock'", "action" => "ctrl.selectedMenuItem(true); ctrl.toggleLockNote()", "desc" => "'Locking notes prevents unintentional editing'"} %menu-row{"label" => "ctrl.note.content.protected ? 'Unprotect' : 'Protect'", "action" => "ctrl.selectedMenuItem(true); ctrl.toggleProtectNote()", "desc" => "'Protecting a note will require credentials to view it (Manage Privileges via Account menu)'"} %menu-row{"label" => "'Preview'", "circle" => "ctrl.note.content.hidePreview ? 'danger' : 'success'", "circle-align" => "'right'", "action" => "ctrl.selectedMenuItem(true); ctrl.toggleNotePreview()", "desc" => "'Hide or unhide the note preview from the list of notes'"} - %menu-row{"label" => "'Delete'", "action" => "ctrl.selectedMenuItem(); ctrl.deleteNote()", "desc" => "'Delete this note permanently from all your devices'"} + %menu-row{"ng-show" => "!ctrl.note.content.trashed", "label" => "'Move to Trash'", "action" => "ctrl.selectedMenuItem(); ctrl.deleteNote()", "desc" => "'Send this note to the trash'"} + + .sk-menu-panel-section{"ng-if" => "ctrl.note.content.trashed"} + .sk-menu-panel-header + .sk-menu-panel-header-title Trash + %menu-row{"label" => "'Restore Note'", "action" => "ctrl.selectedMenuItem(true); ctrl.restoreTrashedNote()", "desc" => "'Undelete this note and restore it back into your notes'"} + %menu-row{"label" => "'Delete Forever'", "action" => "ctrl.selectedMenuItem(true); ctrl.deleteNotePermanantely()", "desc" => "'Delete this note permanently from all your devices'"} + %menu-row{"label" => "'Empty Trash'", "subtitle" => "ctrl.getTrashCount() + ' notes in trash'", "action" => "ctrl.selectedMenuItem(true); ctrl.emptyTrash()", "desc" => "'Permanently delete all notes in the trash'"} .sk-menu-panel-section .sk-menu-panel-header diff --git a/app/assets/templates/footer.html.haml b/app/assets/templates/footer.html.haml index 31192f7eb..f8d3ce768 100644 --- a/app/assets/templates/footer.html.haml +++ b/app/assets/templates/footer.html.haml @@ -21,12 +21,17 @@ %component-modal{"ng-if" => "room.showRoom", "component" => "room", "on-dismiss" => "ctrl.onRoomDismiss"} + .center + .sk-app-bar-item{"ng-show" => "ctrl.arbitraryStatusMessage"} + .sk-app-bar-item-column + %span.neutral.sk-label {{ctrl.arbitraryStatusMessage}} + .right - .sk-app-bar-item{"ng-if" => "ctrl.securityUpdateAvailable", "ng-click" => "ctrl.openSecurityUpdate()"} + .sk-app-bar-item{"ng-show" => "ctrl.securityUpdateAvailable", "ng-click" => "ctrl.openSecurityUpdate()"} %span.success.sk-label Security update available. - .sk-app-bar-item{"ng-if" => "ctrl.newUpdateAvailable == true", "ng-click" => "ctrl.clickedNewUpdateAnnouncement()"} + .sk-app-bar-item{"ng-show" => "ctrl.newUpdateAvailable == true", "ng-click" => "ctrl.clickedNewUpdateAnnouncement()"} %span.info.sk-label New update available. .sk-app-bar-item.no-pointer{"ng-if" => "ctrl.lastSyncDate && !ctrl.isRefreshing"} diff --git a/app/assets/templates/home.html.haml b/app/assets/templates/home.html.haml index 4ca4c5695..7b408e005 100644 --- a/app/assets/templates/home.html.haml +++ b/app/assets/templates/home.html.haml @@ -4,7 +4,7 @@ .app#app{"ng-if" => "!needsUnlock", "ng-class" => "appClass"} %tags-section{"save" => "tagsSave", "add-new" => "tagsAddNew", "selection-made" => "tagsSelectionMade", - "all-tag" => "allTag", "archive-tag" => "archiveTag", "tags" => "tags", "remove-tag" => "removeTag"} + "tags" => "tags", "remove-tag" => "removeTag"} %notes-section{"add-new" => "notesAddNew", "selection-made" => "notesSelectionMade", "tag" => "selectedTag"} %editor-section{"note" => "selectedNote", "remove" => "deleteNote", "update-tags" => "updateTagsForNote"} diff --git a/app/assets/templates/notes.html.haml b/app/assets/templates/notes.html.haml index 475d35711..c0217cc92 100644 --- a/app/assets/templates/notes.html.haml +++ b/app/assets/templates/notes.html.haml @@ -32,12 +32,13 @@ %menu-row{"label" => "'Date Modified'", "circle" => "ctrl.sortBy == 'client_updated_at' && 'success'", "action" => "ctrl.selectedMenuItem(); ctrl.selectedSortByUpdated()", "desc" => "'Sort notes with the most recently updated first'"} %menu-row{"label" => "'Title'", "circle" => "ctrl.sortBy == 'title' && 'success'", "action" => "ctrl.selectedMenuItem(); ctrl.selectedSortByTitle()", "desc" => "'Sort notes alphabetically by their title'"} - .sk-menu-panel-section{"ng-if" => "!ctrl.tag.isSmartTag()"} + .sk-menu-panel-section .sk-menu-panel-header .sk-menu-panel-header-title Display %menu-row{"label" => "'Archived Notes'", "circle" => "ctrl.showArchived ? 'success' : 'danger'", "faded" => "!ctrl.showArchived", "action" => "ctrl.selectedMenuItem(); ctrl.toggleKey('showArchived')", "desc" => "'Archived notes are usually hidden. You can explicitly show them with this option.'"} %menu-row{"label" => "'Pinned Notes'", "circle" => "ctrl.hidePinned ? 'danger' : 'success'", "faded" => "ctrl.hidePinned", "action" => "ctrl.selectedMenuItem(); ctrl.toggleKey('hidePinned')", "desc" => "'Pinned notes always appear on top. You can hide them temporarily with this option so you can focus on other notes in the list.'"} + %menu-row{"label" => "'Note Preview'", "circle" => "ctrl.hideNotePreview ? 'danger' : 'success'", "faded" => "ctrl.hideNotePreview", "action" => "ctrl.selectedMenuItem(); ctrl.toggleKey('hideNotePreview')", "desc" => "'Hide the note preview for a more condensed list of notes'"} %menu-row{"label" => "'Date'", "circle" => "ctrl.hideDate ? 'danger' : 'success'","faded" => "ctrl.hideDate", "action" => "ctrl.selectedMenuItem(); ctrl.toggleKey('hideDate')", "desc" => "'Hide the date displayed in each row'"} %menu-row{"label" => "'Tags'", "circle" => "ctrl.hideTags ? 'danger' : 'success'","faded" => "ctrl.hideTags", "action" => "ctrl.selectedMenuItem(); ctrl.toggleKey('hideTags')", "desc" => "'Hide the list of tags associated with each note'"} @@ -49,23 +50,11 @@ %strong.red.medium-text{"ng-if" => "note.conflict_of"} Conflicted copy %strong.red.medium-text{"ng-if" => "note.errorDecrypting"} Unable to Decrypt - .note-flags - .pinned.note-flag{"ng-if" => "note.pinned"} - %i.icon.ion-bookmark - %strong.medium-text Pinned + .note-flags{"ng-show" => "ctrl.getNoteFlags(note).length > 0"} + .flag{"ng-repeat" => "flag in ctrl.getNoteFlags(note)", "ng-class" => "flag.class"} + .label {{flag.text}} - .archived.note-flag{"ng-if" => "note.archived && !ctrl.tag.isSmartTag()"} - %i.icon.ion-ios-box - %strong.medium-text Archived - - .tags-string{"ng-if" => "ctrl.shouldShowTags(note)"} - .faded {{note.savedTagsString || note.tagsString()}} - - .name{"ng-if" => "note.title"} - %span.note-flag{"ng-show" => "note.locked"} - %i.icon.ion-locked.medium-text - %span.note-flag{"ng-show" => "note.content.protected"} - %i.icon.ion-eye-disabled + .name{"ng-show" => "note.title"} {{note.title}} .note-preview{"ng-if" => "!ctrl.hideNotePreview && !note.content.hidePreview && !note.content.protected"} @@ -73,8 +62,11 @@ .plain-preview{"ng-if" => "!note.content.preview_html && note.content.preview_plain"} {{note.content.preview_plain}} .default-preview{"ng-if" => "!note.content.preview_html && !note.content.preview_plain"} {{note.text}} - .date.faded{"ng-if" => "!ctrl.hideDate"} - %span{"ng-if" => "ctrl.sortBy == 'client_updated_at'"} Modified {{note.updatedAtString() || 'Now'}} - %span{"ng-if" => "ctrl.sortBy != 'client_updated_at'"} {{note.createdAtString() || 'Now'}} + .date.faded{"ng-show" => "!ctrl.hideDate"} + %span{"ng-show" => "ctrl.sortBy == 'client_updated_at'"} Modified {{note.updatedAtString() || 'Now'}} + %span{"ng-show" => "ctrl.sortBy != 'client_updated_at'"} {{note.createdAtString() || 'Now'}} + + .tags-string{"ng-show" => "ctrl.shouldShowTags(note)"} + .faded {{note.savedTagsString || note.tagsString()}} %panel-resizer{"panel-id" => "'notes-column'", "default-width" => 300, "on-resize-finish" => "ctrl.onPanelResize", "control" => "ctrl.panelController", "hoverable" => "true", "collapsable" => "true"} diff --git a/app/assets/templates/tags.html.haml b/app/assets/templates/tags.html.haml index 0af5c93b6..fd2e90c08 100644 --- a/app/assets/templates/tags.html.haml +++ b/app/assets/templates/tags.html.haml @@ -4,19 +4,25 @@ %component-view.component-view{"component" => "ctrl.component"} #tags-content.content{"ng-if" => "!(ctrl.component && ctrl.component.active)"} - #tags-title-bar.section-title-bar + .tags-title-section.section-title-bar .section-title-bar-header .sk-h3.title - %span.sk-bold Tags + %span.sk-bold Views .sk-button.sk-secondary-contrast.wide{"ng-click" => "ctrl.clickedAddNewTag()", "title" => "Create a new tag"} .sk-label + .scrollable .infinite-scroll - .tag{"ng-if" => "ctrl.allTag", "ng-click" => "ctrl.selectTag(ctrl.allTag)", "ng-class" => "{'selected' : ctrl.selectedTag == ctrl.allTag}"} + .tag{"ng-repeat" => "tag in ctrl.smartTags", "ng-click" => "ctrl.selectTag(tag)", + "ng-class" => "{'selected' : ctrl.selectedTag == tag, 'faded' : !tag.content.isAllTag}"} .info - %input.title{"ng-disabled" => "true", "ng-model" => "ctrl.allTag.title"} - .count {{ctrl.noteCount(ctrl.allTag)}} + %input.title{"ng-disabled" => "true", "ng-model" => "tag.title"} + .count{"ng-show" => "tag.content.isAllTag"} {{ctrl.noteCount(tag)}} + + .tags-title-section.section-title-bar + .section-title-bar-header + .sk-h3.title + %span.sk-bold Tags .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", @@ -31,8 +37,8 @@ %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 %a.item{"ng-click" => "ctrl.selectedDeleteTag(tag)"} Delete - .tag.faded{"ng-if" => "ctrl.archiveTag", "ng-click" => "ctrl.selectTag(ctrl.archiveTag)", "ng-class" => "{'selected' : ctrl.selectedTag == ctrl.archiveTag}"} - .info - %input.title{"ng-disabled" => "true", "ng-model" => "ctrl.archiveTag.title"} + + .no-tags-placeholder{"ng-show" => "ctrl.tags.length == 0"} + No tags. Create one using the add button above. %panel-resizer{"panel-id" => "'tags-column'", "default-width" => 150, "on-resize-finish" => "ctrl.onPanelResize", "control" => "ctrl.panelController", "hoverable" => "true", "collapsable" => "true"} diff --git a/package-lock.json b/package-lock.json index e16072f11..393f5d766 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "standard-notes-web", - "version": "3.0.2", + "version": "3.0.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -7813,9 +7813,9 @@ } }, "sn-models": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/sn-models/-/sn-models-0.1.9.tgz", - "integrity": "sha512-pkoNIHPjbUaFj8y6KwwkQIMf0Nm5LgvWSFVzYmWUL6FyKpCqf9Onlf2hhHX8HpD8BjJpeHQoBSMBxpbHtwkgYQ==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/sn-models/-/sn-models-0.1.12.tgz", + "integrity": "sha512-fAiejL62ndUcEQ3ac4PURrZG0p3ZS+3jZsc35HvkF5vYKgHKhYR29VRXxUCPvTvNgn2F3U/55Aa5y9kn8XqoQg==", "dev": true }, "sn-stylekit": { @@ -8097,9 +8097,9 @@ "dev": true }, "standard-file-js": { - "version": "0.3.22", - "resolved": "https://registry.npmjs.org/standard-file-js/-/standard-file-js-0.3.22.tgz", - "integrity": "sha512-nezAUHLX0K0DVtdofrWv2wuznXD8xPJLC/F/usGqtxvtqWbpH+MG99WtHidOd3D38qlGY7PER1nRbFT0WbA92w==", + "version": "0.3.37", + "resolved": "https://registry.npmjs.org/standard-file-js/-/standard-file-js-0.3.37.tgz", + "integrity": "sha512-Je/vBxfWWHBrlDeLLW9Q4QLC+R9SxlYLhB8IpJVp6GZMHD+ET/LBPN+qlGJtUUI0Jr8HHJ1AUfEam9SNZYo1YQ==", "dev": true }, "static-extend": { diff --git a/package.json b/package.json index 4348a6afd..d6bae9b0a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "standard-notes-web", - "version": "3.0.2", + "version": "3.0.3", "license": "AGPL-3.0-or-later", "repository": { "type": "git", @@ -43,9 +43,9 @@ "karma-phantomjs-launcher": "^1.0.2", "mocha": "^5.2.0", "serve-static": "^1.13.2", - "sn-models": "0.1.9", + "sn-models": "0.1.12", "sn-stylekit": "2.0.13", - "standard-file-js": "0.3.22", + "standard-file-js": "0.3.37", "grunt-shell": "^2.1.0" } }