diff --git a/apps/web/__e2e__/models/settings-view.model.ts b/apps/web/__e2e__/models/settings-view.model.ts
index f1fa8b6d5..d87b7b660 100644
--- a/apps/web/__e2e__/models/settings-view.model.ts
+++ b/apps/web/__e2e__/models/settings-view.model.ts
@@ -193,4 +193,15 @@ export class SettingsViewModel {
.locator("input");
await titleFormatInput.fill(format);
}
+
+ async toggleKeepVaultNotesUnlocked() {
+ const item = await this.navigation.findItem("Vault");
+ await item?.click();
+
+ const keepVaultNotesUnlocked = this.page
+ .locator(getTestId("setting-keep-note-unlocked"))
+ .locator("label");
+
+ await keepVaultNotesUnlocked.click();
+ }
}
diff --git a/apps/web/__e2e__/vault.test.ts b/apps/web/__e2e__/vault.test.ts
index 779d040d1..b625d4e06 100644
--- a/apps/web/__e2e__/vault.test.ts
+++ b/apps/web/__e2e__/vault.test.ts
@@ -125,3 +125,42 @@ test("clicking on vault unlocked status should lock the readonly note", async ({
expect(await note?.isLockedNotePasswordFieldVisible()).toBe(true);
});
+
+test("when keep note unlocked setting is disabled (default), locked note should be locked again when it is closed after opening", async ({
+ page
+}) => {
+ const app = new AppModel(page);
+ await app.goto();
+ const notes = await app.goToNotes();
+ const note = await notes.createNote(NOTE);
+
+ await note?.contextMenu.lock(PASSWORD);
+ await note?.openLockedNote(PASSWORD);
+ const tabs = await notes.editor.getTabs();
+ await tabs[0].close();
+
+ await note?.click();
+
+ expect(await note?.isLockedNotePasswordFieldVisible()).toBe(true);
+});
+
+test("when keep note unlocked setting is enabled, locked note should not be locked again when it is closed after opening", async ({
+ page
+}) => {
+ const app = new AppModel(page);
+ await app.goto();
+ const notes = await app.goToNotes();
+ const note = await notes.createNote(NOTE);
+
+ await note?.contextMenu.lock(PASSWORD);
+ const settings = await app.goToSettings();
+ await settings.toggleKeepVaultNotesUnlocked();
+ await settings.close();
+ await note?.openLockedNote(PASSWORD);
+ const tabs = await notes.editor.getTabs();
+ await tabs[0].close();
+
+ await note?.click();
+
+ expect(await note?.isLockedNotePasswordFieldVisible()).toBe(false);
+});
diff --git a/apps/web/src/common/vault.ts b/apps/web/src/common/vault.ts
index 140f1cd5b..39ef84cd5 100644
--- a/apps/web/src/common/vault.ts
+++ b/apps/web/src/common/vault.ts
@@ -22,6 +22,7 @@ import { showPasswordDialog } from "../dialogs/password-dialog";
import { showToast } from "../utils/toast";
import { VAULT_ERRORS } from "@notesnook/core";
import { strings } from "@notesnook/intl";
+import { useStore as useAppStore } from "../stores/app-store";
class Vault {
static async createVault() {
@@ -34,6 +35,7 @@ class Vault {
},
validate: async ({ password }) => {
await db.vault.create(password);
+ useAppStore.getState().setIsVaultCreated(true);
showToast("success", strings.vaultCreated());
return true;
}
diff --git a/apps/web/src/components/status-bar/index.tsx b/apps/web/src/components/status-bar/index.tsx
index c65432362..7be1b2c30 100644
--- a/apps/web/src/components/status-bar/index.tsx
+++ b/apps/web/src/components/status-bar/index.tsx
@@ -204,7 +204,7 @@ function StatusBar() {
}}
data-test-id="vault-unlocked"
>
-
+
{strings.vaultUnlocked()}
diff --git a/apps/web/src/dialogs/settings/vault-settings.tsx b/apps/web/src/dialogs/settings/vault-settings.tsx
index becda4006..871612443 100644
--- a/apps/web/src/dialogs/settings/vault-settings.tsx
+++ b/apps/web/src/dialogs/settings/vault-settings.tsx
@@ -42,14 +42,29 @@ export const VaultSettings: SettingsGroup[] = [
type: "button",
title: strings.create(),
action: () => {
- Vault.createVault().then((res) => {
- useAppStore.getState().setIsVaultCreated(res);
- });
+ Vault.createVault();
},
variant: "secondary"
}
]
},
+ {
+ key: "keep-note-unlocked",
+ title: strings.keepNoteUnlocked(),
+ description: strings.keepNoteUnlockedDesc(),
+ isHidden: () => !useAppStore.getState().isVaultCreated,
+ onStateChange: (listener) =>
+ useAppStore.subscribe((s) => s.isVaultCreated, listener),
+ components: [
+ {
+ type: "toggle",
+ isToggled: () => useAppStore.getState().keepVaultNotesUnlocked,
+ toggle: () => {
+ useAppStore.getState().toggleKeepVaultNotesUnlocked();
+ }
+ }
+ ]
+ },
{
key: "change-vault-password",
title: strings.changeVaultPassword(),
diff --git a/apps/web/src/stores/app-store.ts b/apps/web/src/stores/app-store.ts
index 48ae8a2a4..af75095ff 100644
--- a/apps/web/src/stores/app-store.ts
+++ b/apps/web/src/stores/app-store.ts
@@ -68,6 +68,7 @@ class AppStore extends BaseStore {
isListPaneVisible = true;
isNavPaneCollapsed = false;
isVaultCreated = false;
+ keepVaultNotesUnlocked = Config.get("vault:keepNotesUnlocked", false);
isAutoSyncEnabled = Config.get("autoSyncEnabled", true);
isSyncEnabled = Config.get("syncEnabled", true);
isRealtimeSyncEnabled = Config.get("isRealtimeSyncEnabled", true);
@@ -373,6 +374,12 @@ class AppStore extends BaseStore {
setNavigationTab = (tab: NavigationTabItem["id"]) => {
this.set((state) => (state.navigationTab = tab));
};
+
+ toggleKeepVaultNotesUnlocked = () => {
+ const newValue = !this.get().keepVaultNotesUnlocked;
+ Config.set("vault:keepNotesUnlocked", newValue);
+ this.set({ keepVaultNotesUnlocked: newValue });
+ };
}
const [useStore, store] = createStore(
diff --git a/apps/web/src/stores/editor-store.ts b/apps/web/src/stores/editor-store.ts
index 445e7ab9c..1a2647696 100644
--- a/apps/web/src/stores/editor-store.ts
+++ b/apps/web/src/stores/editor-store.ts
@@ -777,16 +777,37 @@ class EditorStore extends BaseStore {
options
);
} else if (isLocked && note.type !== "trash") {
- this.addSession(
- {
- type: "locked",
- id: sessionId,
- note,
- activeBlockId: options.activeBlockId,
- tabId
- },
- options
- );
+ if (
+ appStore.get().keepVaultNotesUnlocked &&
+ db.vault.isNoteOpened(note.id)
+ ) {
+ const tags = await db.notes.tags(note.id);
+ const noteFromVault = await db.vault.open(note.id);
+ if (noteFromVault) {
+ this.addSession({
+ type: note.readonly ? "readonly" : "default",
+ locked: true,
+ id: sessionId,
+ note: noteFromVault,
+ saveState: SaveState.Saved,
+ sessionId: `${Date.now()}`,
+ tags: tags,
+ tabId: tabId,
+ content: noteFromVault.content
+ });
+ }
+ } else {
+ this.addSession(
+ {
+ type: "locked",
+ id: sessionId,
+ note,
+ activeBlockId: options.activeBlockId,
+ tabId
+ },
+ options
+ );
+ }
} else {
const content = note.contentId
? await db.content.get(note.contentId)
diff --git a/docs/help/contents/lock-notes-with-private-vault.md b/docs/help/contents/lock-notes-with-private-vault.md
index 4a0c22850..c6da5ba20 100644
--- a/docs/help/contents/lock-notes-with-private-vault.md
+++ b/docs/help/contents/lock-notes-with-private-vault.md
@@ -51,6 +51,16 @@ Adding notes to private vault is useful when you do not want anyone to read your
To open, edit or delete a locked note, you must provide the password for the vault to unlock it.
+## Keep note unlocked when it is opened
+
+By default, when you open a locked note, it will be locked again after you close the note or lock the vault. You can change this behavior, so that the note remains unlocked even after you close it. Now the note will only be locked when the vault is locked.
+
+# [Desktop/Web](#/tab/web)
+
+1. Go to Settings
+2. Go to `Vault` in `Security & privacy` section
+3. Toggle `Keep note unlocked`
+
## Unlock a note permanently
# [Desktop/Web](#/tab/web)
diff --git a/packages/core/__tests__/vault.test.js b/packages/core/__tests__/vault.test.js
index eaac8e847..4b3b09d37 100644
--- a/packages/core/__tests__/vault.test.js
+++ b/packages/core/__tests__/vault.test.js
@@ -83,6 +83,7 @@ test("lock a note", () =>
expect(content.data.cipher).toBeDefined();
expect(await db.relations.from(vault, "note").has(id)).toBe(true);
+ expect(db.vault.isNoteOpened(id)).toBeFalsy();
}));
test("locked note is not favorited", () =>
@@ -106,6 +107,7 @@ test("unlock a note", () =>
expect(note.content.data).toBeDefined();
expect(note.content.type).toBe(TEST_NOTE.content.type);
expect(await db.relations.from(vault, "note").has(id)).toBe(true);
+ expect(db.vault.isNoteOpened(id)).toBeTruthy();
}));
test("unlock a note permanently", () =>
@@ -124,6 +126,7 @@ test("unlock a note permanently", () =>
expect(content.data).toBeDefined();
expect(typeof content.data).toBe("string");
expect(await db.relations.from(vault, "note").has(id)).toBe(false);
+ expect(db.vault.isNoteOpened(id)).toBeFalsy();
}));
test("lock an empty note", () =>
@@ -141,6 +144,7 @@ test("lock an empty note", () =>
expect(content.data.iv).toBeDefined();
expect(content.data.cipher).toBeDefined();
expect(await db.relations.from(vault, "note").has(id)).toBe(true);
+ expect(db.vault.isNoteOpened(id)).toBeFalsy();
}));
test("save a locked note", () =>
@@ -154,6 +158,7 @@ test("save a locked note", () =>
const content = await db.content.get(note.contentId);
expect(content.data.cipher).toBeTypeOf("string");
+ expect(db.vault.isNoteOpened(id)).toBeFalsy();
}));
test("save an edited locked note", () =>
@@ -173,6 +178,7 @@ test("save an edited locked note", () =>
expect(() => JSON.parse(content.data.cipher)).toThrow();
expect(note.dateEdited).toBeLessThan((await db.notes.note(id)).dateEdited);
expect(note.dateEdited).toBeLessThan(content.dateEdited);
+ expect(db.vault.isNoteOpened(id)).toBeFalsy();
}));
test("change vault password", () =>
diff --git a/packages/core/src/api/vault.ts b/packages/core/src/api/vault.ts
index da700e794..576a04970 100644
--- a/packages/core/src/api/vault.ts
+++ b/packages/core/src/api/vault.ts
@@ -23,6 +23,7 @@ import { EV, EVENTS } from "../common.js";
import { isCipher } from "../utils/crypto.js";
import { Note, NoteContent } from "../types.js";
import { logger } from "../logger.js";
+import { addItem } from "../utils/array.js";
export const VAULT_ERRORS = {
noVault: "ERR_NO_VAULT",
@@ -35,6 +36,7 @@ export default class Vault {
private vaultPassword?: string;
private erasureTimeout = 0;
private key = "svvaads1212#2123";
+ private openedNotes: string[] = [];
private get password() {
return this.vaultPassword;
@@ -66,6 +68,10 @@ export default class Vault {
return !!this.vaultPassword;
}
+ isNoteOpened(noteId: string) {
+ return this.openedNotes.includes(noteId);
+ }
+
async create(password: string) {
const vaultKey = await this.getKey();
if (!vaultKey || !isCipher(vaultKey)) {
@@ -80,6 +86,7 @@ export default class Vault {
async lock() {
this.password = undefined;
+ this.openedNotes = [];
EV.publish(EVENTS.vaultLocked);
return true;
}
@@ -202,6 +209,8 @@ export default class Vault {
false
);
+ addItem(this.openedNotes, noteId);
+
if (password) {
this.password = password;
if (!(await this.exists())) await this.create(password);
diff --git a/packages/intl/locale/en.po b/packages/intl/locale/en.po
index 6536d15cb..8bc3d0862 100644
--- a/packages/intl/locale/en.po
+++ b/packages/intl/locale/en.po
@@ -3510,6 +3510,14 @@ msgstr "Jump to group"
msgid "Keep"
msgstr "Keep"
+#: src/strings.ts:2639
+msgid "Keep a vault note unlocked once it is opened. Note will be locked back once vault is locked."
+msgstr "Keep a vault note unlocked once it is opened. Note will be locked back once vault is locked."
+
+#: src/strings.ts:2637
+msgid "Keep note unlocked"
+msgstr "Keep note unlocked"
+
#: src/strings.ts:2021
msgid "Keep open"
msgstr "Keep open"
diff --git a/packages/intl/locale/pseudo-LOCALE.po b/packages/intl/locale/pseudo-LOCALE.po
index db8f7b51d..447a55984 100644
--- a/packages/intl/locale/pseudo-LOCALE.po
+++ b/packages/intl/locale/pseudo-LOCALE.po
@@ -3490,6 +3490,14 @@ msgstr ""
msgid "Keep"
msgstr ""
+#: src/strings.ts:2639
+msgid "Keep a vault note unlocked once it is opened. Note will be locked back once vault is locked."
+msgstr ""
+
+#: src/strings.ts:2637
+msgid "Keep note unlocked"
+msgstr ""
+
#: src/strings.ts:2021
msgid "Keep open"
msgstr ""
diff --git a/packages/intl/src/strings.ts b/packages/intl/src/strings.ts
index a31841975..23d14c484 100644
--- a/packages/intl/src/strings.ts
+++ b/packages/intl/src/strings.ts
@@ -2633,5 +2633,8 @@ Use this if changes from other devices are not appearing on this device. This wi
exportCsv: () => t`Export CSV`,
importCsv: () => t`Import CSV`,
noContent: () => t`This note is empty`,
- deleteData: () => t`Delete data`
+ deleteData: () => t`Delete data`,
+ keepNoteUnlocked: () => t`Keep note unlocked`,
+ keepNoteUnlockedDesc: () =>
+ t`Keep a vault note unlocked once it is opened. Note will be locked back once vault is locked.`
};