From ee573a8b33a0114f05737ee6bad5046a75441616 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Thu, 3 Jan 2019 13:10:10 -0600 Subject: [PATCH] Privileges --- package-lock.json | 6 +- package.json | 2 +- src/app.js | 3 + src/global.js | 4 +- src/lib/itemActionManager.js | 10 ++ src/lib/keysManager.js | 4 +- src/lib/sfjs/authManager.js | 7 + src/lib/sfjs/modelManager.js | 7 +- src/lib/sfjs/privilegesManager.js | 102 ++++++++++++ src/models/extend/item.js | 10 -- src/screens/Abstract.js | 16 ++ src/screens/Authentication/Authenticate.js | 42 ++++- .../AuthenticationSourceAccountPassword.js | 9 +- src/screens/Notes/NoteCell.js | 33 ++-- src/screens/Notes/Notes.js | 12 +- src/screens/Settings.js | 154 +++++++++--------- src/screens/SideMenu/NoteSideMenu.js | 28 +++- 17 files changed, 322 insertions(+), 127 deletions(-) create mode 100644 src/lib/sfjs/privilegesManager.js diff --git a/package-lock.json b/package-lock.json index 6590e91d..98a1a8bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11777,9 +11777,9 @@ "integrity": "sha1-ATl5IuX2Ls8whFUiyVxP4dJefU4=" }, "standard-file-js": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/standard-file-js/-/standard-file-js-0.3.19.tgz", - "integrity": "sha512-H23VzukJescWRQp8WoSGm/arU7KzglC7bY7sGy8AwCcStxvAz278VqysGTnwQGpObxEHsp9hKucnV34RhASLtA==" + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/standard-file-js/-/standard-file-js-0.3.24.tgz", + "integrity": "sha512-LGqhfvnskr0IaH7rM28bZEI+8xJZectXdtcXPPfSdv46kIq9QjqcoqJnPNfqBHWwHu8L8grPaUBK36Q7ALQBwQ==" }, "static-extend": { "version": "0.1.2", diff --git a/package.json b/package.json index aa7bf083..45ab7312 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "react-navigation-header-buttons": "^2.1.1", "regenerator": "^0.13.3", "sn-models": "0.1.8", - "standard-file-js": "0.3.19" + "standard-file-js": "0.3.24" }, "devDependencies": { "babel-jest": "^23.6.0", diff --git a/src/app.js b/src/app.js index b99de0e9..b575c04c 100644 --- a/src/app.js +++ b/src/app.js @@ -9,6 +9,7 @@ import Icons from '@Style/Icons'; import ApplicationState from "@Lib/ApplicationState" import Auth from './lib/sfjs/authManager' import ModelManager from './lib/sfjs/modelManager' +import PrivilegesManager from '@SFJS/privilegesManager' import Sync from './lib/sfjs/syncManager' import Storage from './lib/sfjs/storageManager' import ReviewManager from './lib/reviewManager'; @@ -123,6 +124,8 @@ export default class App extends Component { // Initialize iOS review manager. Will automatically handle requesting review logic. ReviewManager.initialize(); + PrivilegesManager.get().loadPrivileges(); + // Listen to sign out event Auth.get().addEventHandler((event) => { if(event == SFAuthManager.DidSignOutEvent) { diff --git a/src/global.js b/src/global.js index 55694ae7..7920205b 100644 --- a/src/global.js +++ b/src/global.js @@ -22,7 +22,8 @@ import { SFAlertManager, SFStorageManager, SFHttpManager, - SFAuthManager + SFAuthManager, + SFPrivilegesManager } from 'standard-file-js'; SFItem.AppDomain = "org.standardnotes.sn"; @@ -35,6 +36,7 @@ global.SFAlertManager = SFAlertManager; global.SFStorageManager = SFStorageManager; global.SFHttpManager = SFHttpManager; global.SFAuthManager = SFAuthManager; +global.SFPrivilegesManager = SFPrivilegesManager; import SF from "./lib/sfjs/sfjs" global.SFJS = SF.get(); diff --git a/src/lib/itemActionManager.js b/src/lib/itemActionManager.js index 9a60a273..97a5f93b 100644 --- a/src/lib/itemActionManager.js +++ b/src/lib/itemActionManager.js @@ -17,6 +17,9 @@ export default class ItemActionManager { static LockEvent = "LockEvent"; static UnlockEvent = "UnlockEvent"; + static ProtectEvent = "ProtectEvent"; + static UnprotectEvent = "UnprotectEvent"; + static ShareEvent = "ShareEvent"; /* The afterConfirmCallback is called after user confirms deletion pop up */ @@ -66,6 +69,13 @@ export default class ItemActionManager { callback && callback(); } + else if(event == this.ProtectEvent || event == this.UnprotectEvent) { + item.content.protected = !item.content.protected; + item.setDirty(true); + Sync.get().sync(); + callback && callback(); + } + else if(event == this.ShareEvent) { ApplicationState.get().performActionWithoutStateChangeImpact(() => { Share.share({ diff --git a/src/lib/keysManager.js b/src/lib/keysManager.js index 9b4db3ae..65e8d2b0 100644 --- a/src/lib/keysManager.js +++ b/src/lib/keysManager.js @@ -295,9 +295,9 @@ export default class KeysManager { // Local Security - async clearOfflineKeysAndData() { + async clearOfflineKeysAndData(force = false) { // make sure user is authenticated before performing this step - if(!this.offlineKeys.mk) { + if(!this.offlineKeys.mk && !force) { alert("Unable to remove passcode. Make sure you are properly authenticated and try again."); return false; } diff --git a/src/lib/sfjs/authManager.js b/src/lib/sfjs/authManager.js index 88544079..4143d001 100644 --- a/src/lib/sfjs/authManager.js +++ b/src/lib/sfjs/authManager.js @@ -66,4 +66,11 @@ export default class Auth extends SFAuthManager { return null; } } + + async verifyAccountPassword(password) { + let authParams = await this.getAuthParams(); + let keys = await SFJS.crypto.computeEncryptionKeysForUser(password, authParams); + let success = keys.mk === (await this.keys()).mk; + return success; + } } diff --git a/src/lib/sfjs/modelManager.js b/src/lib/sfjs/modelManager.js index f53ce085..77e6831d 100644 --- a/src/lib/sfjs/modelManager.js +++ b/src/lib/sfjs/modelManager.js @@ -1,13 +1,14 @@ import Storage from "./storageManager" import "../../models/extend/item.js"; -import { SFPredicate } from "standard-file-js"; +import { SFPredicate, SFPrivileges } from "standard-file-js"; SFModelManager.ContentTypeClassMapping = { "Note" : SNNote, "Tag" : SNTag, "SN|SmartTag": SNSmartTag, "SN|Theme" : SNTheme, - "SN|Component" : SNComponent + "SN|Component" : SNComponent, + "SN|Privileges" : SFPrivileges }; const SystemSmartTagIdAllNotes = "all-notes"; @@ -32,8 +33,6 @@ export default class ModelManager extends SFModelManager { this.tags = []; this.themes = []; - this.acceptableContentTypes = ["Note", "Tag", "SN|SmartTag", "SN|Theme", "SN|Component"]; - this.buildSystemSmartTags(); } diff --git a/src/lib/sfjs/privilegesManager.js b/src/lib/sfjs/privilegesManager.js new file mode 100644 index 00000000..36ab140f --- /dev/null +++ b/src/lib/sfjs/privilegesManager.js @@ -0,0 +1,102 @@ +import ModelManager from "./modelManager"; +import Sync from "./syncManager"; +import { SFPrivilegesManager, SFSingletonManager } from "standard-file-js"; +import AuthenticationSourceAccountPassword from "@Screens/Authentication/Sources/AuthenticationSourceAccountPassword"; +import AuthenticationSourceLocalPasscode from "@Screens/Authentication/Sources/AuthenticationSourceLocalPasscode"; +import AuthenticationSourceFingerprint from "@Screens/Authentication/Sources/AuthenticationSourceFingerprint"; +import KeysManager from "@Lib/keysManager" +import Storage from "@SFJS/storageManager" +import Auth from "@SFJS/authManager" + +export default class PrivilegesManager extends SFPrivilegesManager { + + static instance = null; + + static get() { + if (this.instance == null) { + let singletonManager = new SFSingletonManager(ModelManager.get(), Sync.get()); + this.instance = new PrivilegesManager(ModelManager.get(), Sync.get(), singletonManager); + } + + return this.instance; + } + + constructor(modelManager, syncManager, singletonManager) { + super(modelManager, syncManager, singletonManager); + + this.setDelegate({ + isOffline: async () => { + return Auth.get().offline(); + }, + hasLocalPasscode: async () => { + return KeysManager.get().hasOfflinePasscode(); + }, + saveToStorage: async (key, value) => { + return Storage.get().setItem(key, value); + }, + getFromStorage: async (key) => { + return Storage.get().getItem(key); + } + }); + } + + async presentPrivilegesModal(action, navigation, onSuccess, onCancel) { + if(this.authenticationInProgress()) { + onCancel && onCancel(); + return; + } + + let customSuccess = () => { + onSuccess && onSuccess(); + this.authInProgress = false; + } + + let customCancel = () => { + onCancel && onCancel(); + this.authInProgress = false; + } + + let sources = await this.sourcesForAction(action); + + navigation.navigate("Authenticate", { + authenticationSources: sources, + hasCancelOption: true, + onSuccess: () => { + customSuccess(); + }, + onCancel: () => { + customCancel(); + } + }); + + this.authInProgress = true; + } + + authenticationInProgress() { + return this.authInProgress; + } + + async sourcesForAction(action) { + const sourcesForCredential = (credential) => { + if(credential == SFPrivilegesManager.CredentialAccountPassword) { + return [new AuthenticationSourceAccountPassword()]; + } else if(credential == SFPrivilegesManager.CredentialLocalPasscode) { + var hasPasscode = KeysManager.get().hasOfflinePasscode(); + var hasFingerprint = KeysManager.get().hasFingerprint(); + let sources = []; + if(hasPasscode) {sources.push(new AuthenticationSourceLocalPasscode());} + if(hasFingerprint) {sources.push(new AuthenticationSourceFingerprint());} + return sources; + } + } + + let credentials = await this.netCredentialsForAction(action); + let sources = []; + for(var credential of credentials) { + sources = sources.concat(sourcesForCredential(credential)); + } + + return sources; + } + +} diff --git a/src/models/extend/item.js b/src/models/extend/item.js index 0c10ff65..980e572c 100644 --- a/src/models/extend/item.js +++ b/src/models/extend/item.js @@ -21,16 +21,6 @@ SFItem.prototype.dateToLocalizedString = function(date) { return moment(date).format('llll'); } -// Define these new methods - -SFItem.prototype.initUUID = async function() { - if(!this.uuid) { - return SFJS.crypto.generateUUID().then((uuid) => { - this.uuid = uuid; - }) - } -} - // Define these getters Object.defineProperty(SFItem.prototype, "key", { diff --git a/src/screens/Abstract.js b/src/screens/Abstract.js index 90cf7fa6..8601c4ec 100644 --- a/src/screens/Abstract.js +++ b/src/screens/Abstract.js @@ -6,6 +6,7 @@ import HeaderTitleView from "../components/HeaderTitleView" import HeaderButtons, { HeaderButton, Item } from 'react-navigation-header-buttons'; import Icon from 'react-native-vector-icons/Ionicons'; import ThemedComponent from "@Components/ThemedComponent"; +import PrivilegesManager from "@SFJS/privilegesManager" const IoniconsHeaderButton = passMeFurther => ( // the `passMeFurther` variable here contains props from as well as @@ -214,6 +215,21 @@ export default class Abstract extends ThemedComponent { this.props.navigation.goBack(null); } + async handlePrivilegedAction(isProtected, action, run) { + if(isProtected) { + let actionRequiresPrivs = await PrivilegesManager.get().actionRequiresPrivilege(action); + if(actionRequiresPrivs) { + PrivilegesManager.get().presentPrivilegesModal(action, this.props.navigation, () => { + run(); + }); + } else { + run(); + } + } else { + run(); + } + } + static IsShallowEqual = (newObj, prevObj, keys) => { for(var key of keys) { if(newObj[key] !== prevObj[key]) { diff --git a/src/screens/Authentication/Authenticate.js b/src/screens/Authentication/Authenticate.js index ea3f7c07..2f6dffe5 100644 --- a/src/screens/Authentication/Authenticate.js +++ b/src/screens/Authentication/Authenticate.js @@ -10,13 +10,19 @@ import SectionedAccessoryTableCell from "@Components/SectionedAccessoryTableCell import SectionedOptionsTableCell from "@Components/SectionedOptionsTableCell"; import StyleKit from "@Style/StyleKit" import Icon from 'react-native-vector-icons/Ionicons'; -import KeysManager from "@Lib/keysManager"; + +// Dev mode only. Used to destroy data +// import KeysManager from "@Lib/keysManager"; +// import Auth from "@SFJS/authManager" export default class Authenticate extends Abstract { static navigationOptions = ({ navigation, navigationOptions }) => { let templateOptions = { title: "Authenticate", + rightButton: { + title: "Submit", + } } return Abstract.getDefaultNavigationOptions({navigation, navigationOptions, templateOptions}); }; @@ -30,13 +36,25 @@ export default class Authenticate extends Abstract { } } - if(__DEV__) { + // if(__DEV__) { + // props.navigation.setParams({ + // leftButton: { + // title: "Destroy Data", + // onPress: () => { + // Auth.get().signout(); + // KeysManager.get().clearOfflineKeysAndData(true); + // } + // } + // }) + // } + + if(this.getProp("hasCancelOption")) { props.navigation.setParams({ leftButton: { - title: "Clear", + title: "Cancel", onPress: () => { - KeysManager.get().clearAccountKeysAndData(); - KeysManager.get().clearOfflineKeysAndData(); + this.getProp("onCancel")(); + this.dismiss(); } } }) @@ -113,10 +131,20 @@ export default class Authenticate extends Abstract { } onSuccess() { - this.getProp("onSuccess")(); + // Wait for componentWillBlur to call onSuccess callback. + // This way, if the callback has another route change, the dismissal + // of this one won't affect it. + this.successful = true; this.dismiss(); } + componentWillBlur() { + super.componentWillBlur(); + if(this.successful) { + this.getProp("onSuccess")(); + } + } + inputTextChanged(text, source) { source.setAuthenticationValue(text); this.forceUpdate(); @@ -164,7 +192,7 @@ export default class Authenticate extends Abstract { {source.type == "input" && diff --git a/src/screens/Authentication/Sources/AuthenticationSourceAccountPassword.js b/src/screens/Authentication/Sources/AuthenticationSourceAccountPassword.js index 21f54026..8c45d6dd 100644 --- a/src/screens/Authentication/Sources/AuthenticationSourceAccountPassword.js +++ b/src/screens/Authentication/Sources/AuthenticationSourceAccountPassword.js @@ -1,5 +1,6 @@ import SF from '@SFJS/sfjs' import Storage from '@SFJS/storageManager' +import Auth from '@SFJS/authManager' import KeysManager from '@Lib/keysManager' import AuthenticationSource from "./AuthenticationSource" @@ -41,8 +42,12 @@ export default class AuthenticationSourceAccountPassword extends AuthenticationS async authenticate() { this.didBegin(); - // TODO - return this._success(); + let success = await Auth.get().verifyAccountPassword(this.authenticationValue); + if(success) { + return this._success(); + } else { + return this._fail("Invalid account password. Please try again."); + } } _success() { diff --git a/src/screens/Notes/NoteCell.js b/src/screens/Notes/NoteCell.js index 082fa61f..62cf342c 100644 --- a/src/screens/Notes/NoteCell.js +++ b/src/screens/Notes/NoteCell.js @@ -65,17 +65,28 @@ export default class NoteCell extends ThemedPureComponent { var archiveLabel = this.props.item.archived ? "Unarchive" : "Archive"; let archiveEvent = archiveLabel == "Archive" ? ItemActionManager.ArchiveEvent : ItemActionManager.UnarchiveEvent; - let sheet = new ActionSheetWrapper({ - title: this.props.item.safeTitle(), - options: [ - ActionSheetWrapper.BuildOption({text: pinLabel, key: pinEvent, callback: callbackForOption}), - ActionSheetWrapper.BuildOption({text: archiveLabel, key: archiveEvent, callback: callbackForOption}), - ActionSheetWrapper.BuildOption({text: "Share", key: ItemActionManager.ShareEvent, callback: callbackForOption}), - ActionSheetWrapper.BuildOption({text: "Delete", key: ItemActionManager.DeleteEvent, destructive: true, callback: callbackForOption}), - ], onCancel: () => { - this.setState({actionSheet: null}); - } - }); + let sheet; + if(this.props.item.content.protected) { + sheet = new ActionSheetWrapper({ + title: "Note Protected", + options: [], + onCancel: () => { + this.setState({actionSheet: null}); + } + }); + } else { + sheet = new ActionSheetWrapper({ + title: this.props.item.safeTitle(), + options: [ + ActionSheetWrapper.BuildOption({text: pinLabel, key: pinEvent, callback: callbackForOption}), + ActionSheetWrapper.BuildOption({text: archiveLabel, key: archiveEvent, callback: callbackForOption}), + ActionSheetWrapper.BuildOption({text: "Share", key: ItemActionManager.ShareEvent, callback: callbackForOption}), + ActionSheetWrapper.BuildOption({text: "Delete", key: ItemActionManager.DeleteEvent, destructive: true, callback: callbackForOption}), + ], onCancel: () => { + this.setState({actionSheet: null}); + } + }); + } this.setState({actionSheet: sheet.actionSheetElement()}); sheet.show(); diff --git a/src/screens/Notes/Notes.js b/src/screens/Notes/Notes.js index df494672..1d536ce8 100644 --- a/src/screens/Notes/Notes.js +++ b/src/screens/Notes/Notes.js @@ -337,11 +337,13 @@ export default class Notes extends Abstract { } } - presentComposer(item) { - this.props.navigation.navigate("Compose", { - noteId: item && item.uuid, - selectedTagId: this.options.selectedTagIds.length && this.options.selectedTagIds[0], - }); + async presentComposer(note) { + this.handlePrivilegedAction(note && note.content.protected, SFPrivilegesManager.ActionViewProtectedNotes, () => { + this.props.navigation.navigate("Compose", { + noteId: note && note.uuid, + selectedTagId: this.options.selectedTagIds.length && this.options.selectedTagIds[0], + }); + }) } reloadList(force) { diff --git a/src/screens/Settings.js b/src/screens/Settings.js index d565788d..c2d799ae 100644 --- a/src/screens/Settings.js +++ b/src/screens/Settings.js @@ -269,66 +269,68 @@ export default class Settings extends Abstract { } onExportPress = async (encrypted, callback) => { - let customCallback = (success) => { - if(success) { - // UserPrefsManager.get().clearLastExportDate(); - var date = new Date(); - this.setState({lastExportDate: date}); - UserPrefsManager.get().setLastExportDate(date); + this.handlePrivilegedAction(true, SFPrivilegesManager.ActionManageBackups, async () => { + let customCallback = (success) => { + if(success) { + // UserPrefsManager.get().clearLastExportDate(); + var date = new Date(); + this.setState({lastExportDate: date}); + UserPrefsManager.get().setLastExportDate(date); + } + callback(); } - callback(); - } - var auth_params = await Auth.get().getAuthParams(); - var keys = encrypted ? KeysManager.get().activeKeys() : null; + var auth_params = await Auth.get().getAuthParams(); + var keys = encrypted ? KeysManager.get().activeKeys() : null; - var items = []; - - for(var item of ModelManager.get().allItems) { - var itemParams = new SFItemParams(item, keys, auth_params); - var params = await itemParams.paramsForExportFile(); - items.push(params); - } + var items = []; - if(items.length == 0) { - Alert.alert('No Data', "You don't have any notes yet."); - customCallback(); - return; - } + for(var item of ModelManager.get().allItems) { + var itemParams = new SFItemParams(item, keys, auth_params); + var params = await itemParams.paramsForExportFile(); + items.push(params); + } - var data = {items: items} + if(items.length == 0) { + Alert.alert('No Data', "You don't have any notes yet."); + customCallback(); + return; + } - if(keys) { - var authParams = KeysManager.get().activeAuthParams(); - // auth params are only needed when encrypted with a standard file key - data["auth_params"] = authParams; - } + var data = {items: items} - var jsonString = JSON.stringify(data, null, 2 /* pretty print */); - var stringData = ApplicationState.isIOS ? jsonString : base64.encode(unescape(encodeURIComponent(jsonString))) - var fileType = ApplicationState.isAndroid ? ".json" : "json"; // Android creates a tmp file and expects dot with extension - - var calledCallback = false; - - Mailer.mail({ - subject: 'Standard Notes Backup', - recipients: [''], - body: '', - isHTML: true, - attachment: { data: stringData, type: fileType, name: encrypted ? "SN-Encrypted-Backup" : 'SN-Decrypted-Backup' } - }, (error, event) => { - customCallback(false); - calledCallback = true; - if(error) { - Alert.alert('Error', 'Unable to send email.'); + if(keys) { + var authParams = KeysManager.get().activeAuthParams(); + // auth params are only needed when encrypted with a standard file key + data["auth_params"] = authParams; } - }); - // On Android the Mailer callback event isn't always triggered. - setTimeout(function () { - if(!calledCallback) { - customCallback(true); - } - }, 2500); + var jsonString = JSON.stringify(data, null, 2 /* pretty print */); + var stringData = ApplicationState.isIOS ? jsonString : base64.encode(unescape(encodeURIComponent(jsonString))) + var fileType = ApplicationState.isAndroid ? ".json" : "json"; // Android creates a tmp file and expects dot with extension + + var calledCallback = false; + + Mailer.mail({ + subject: 'Standard Notes Backup', + recipients: [''], + body: '', + isHTML: true, + attachment: { data: stringData, type: fileType, name: encrypted ? "SN-Encrypted-Backup" : 'SN-Decrypted-Backup' } + }, (error, event) => { + customCallback(false); + calledCallback = true; + if(error) { + Alert.alert('Error', 'Unable to send email.'); + } + }); + + // On Android the Mailer callback event isn't always triggered. + setTimeout(function () { + if(!calledCallback) { + customCallback(true); + } + }, 2500); + }); } onStorageEncryptionEnable = () => { @@ -396,28 +398,30 @@ export default class Settings extends Abstract { } onPasscodeDisable = () => { - var encryptionSource = KeysManager.get().encryptionSource(); - var message; - if(encryptionSource == "account") { - message = "Are you sure you want to disable your local passcode? This will not affect your encryption status, as your data is currently being encrypted through your sync account keys."; - } else if(encryptionSource == "offline") { - message = "Are you sure you want to disable your local passcode? This will disable encryption on your data."; - } + this.handlePrivilegedAction(true, SFPrivilegesManager.ActionManagePasscode, () => { + var encryptionSource = KeysManager.get().encryptionSource(); + var message; + if(encryptionSource == "account") { + message = "Are you sure you want to disable your local passcode? This will not affect your encryption status, as your data is currently being encrypted through your sync account keys."; + } else if(encryptionSource == "offline") { + message = "Are you sure you want to disable your local passcode? This will disable encryption on your data."; + } - AlertManager.get().confirm({ - title: "Disable Passcode", - text: message, - confirmButtonText: "Disable Passcode", - onConfirm: async () => { - var result = await KeysManager.get().clearOfflineKeysAndData(); - if(encryptionSource == "offline") { - // remove encryption from all items - this.resaveOfflineData(null, true); - } + AlertManager.get().confirm({ + title: "Disable Passcode", + text: message, + confirmButtonText: "Disable Passcode", + onConfirm: async () => { + var result = await KeysManager.get().clearOfflineKeysAndData(); + if(encryptionSource == "offline") { + // remove encryption from all items + this.resaveOfflineData(null, true); + } - this.mergeState({hasPasscode: false}); - this.forceUpdate(); - } + this.mergeState({hasPasscode: false}); + this.forceUpdate(); + } + }) }) } @@ -427,8 +431,10 @@ export default class Settings extends Abstract { } onFingerprintDisable = () => { - KeysManager.get().disableFingerprint(); - this.loadSecurityStatus(); + this.handlePrivilegedAction(true, SFPrivilegesManager.ActionManagePasscode, () => { + KeysManager.get().disableFingerprint(); + this.loadSecurityStatus(); + }); } onCompanyAction = (action) => { diff --git a/src/screens/SideMenu/NoteSideMenu.js b/src/screens/SideMenu/NoteSideMenu.js index f869ddc2..07891612 100644 --- a/src/screens/SideMenu/NoteSideMenu.js +++ b/src/screens/SideMenu/NoteSideMenu.js @@ -127,10 +127,14 @@ export default class NoteSideMenu extends AbstractSideMenu { var lockOption = this.note.locked ? "Unlock" : "Lock"; let lockEvent = lockOption == "Lock" ? ItemActionManager.LockEvent : ItemActionManager.UnlockEvent; + var protectOption = this.note.content.protected ? "Unprotect" : "Protect"; + let protectEvent = protectOption == "Protect" ? ItemActionManager.ProtectEvent : ItemActionManager.UnprotectEvent; + let rawOptions = [ { text: pinOption, key: pinEvent, icon: "bookmark" }, { text: archiveOption, key: archiveEvent, icon: "archive" }, { text: lockOption, key: lockEvent, icon: "lock" }, + { text: protectOption, key: protectEvent, icon: "finger-print" }, { text: "Share", key: ItemActionManager.ShareEvent, icon: "share" }, { text: "Delete", key: ItemActionManager.DeleteEvent, icon: "trash" }, ]; @@ -142,13 +146,23 @@ export default class NoteSideMenu extends AbstractSideMenu { key: rawOption.key, iconDesc: { type: "icon", side: "right", name: Icons.nameForIcon(rawOption.icon) }, onSelect: () => { - ItemActionManager.handleEvent(rawOption.key, this.note, () => { - if(rawOption.key == ItemActionManager.DeleteEvent) { - this.popToRoot(); - } else { - this.forceUpdate(); - } - }); + let run = () => { + ItemActionManager.handleEvent(rawOption.key, this.note, () => { + if(rawOption.key == ItemActionManager.DeleteEvent) { + this.popToRoot(); + } else { + this.forceUpdate(); + } + }); + } + if(rawOption.key == ItemActionManager.DeleteEvent && this.note.content.protected) { + this.handlePrivilegedAction(this.note.content.protected, SFPrivilegesManager.ActionDeleteNote, () => { + run(); + }) + } else { + run(); + } + }, }) options.push(option);