mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-03 04:31:46 +02:00
feat: implement note locking (#92)
This commit is contained in:
@@ -10,9 +10,12 @@ const CheckBox = props => {
|
||||
return (
|
||||
<Flex
|
||||
onClick={() => {
|
||||
setChecked(!checked);
|
||||
if (props.onChecked) {
|
||||
props.onChecked(!checked);
|
||||
setChecked(!checked);
|
||||
}
|
||||
if (props.onClick) {
|
||||
props.onClick();
|
||||
}
|
||||
}}
|
||||
width="full"
|
||||
|
||||
96
apps/web/src/components/dialogs/password-dialog.js
Normal file
96
apps/web/src/components/dialogs/password-dialog.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import React, { useState, useRef, useCallback } from "react";
|
||||
import { Box, Text, Flex } from "rebass";
|
||||
import { Input } from "@rebass/forms";
|
||||
import Dialog, { showDialog } from "./dialog";
|
||||
import * as Icon from "react-feather";
|
||||
|
||||
function PasswordDialog(props) {
|
||||
const [isWrong, setIsWrong] = useState(false);
|
||||
const passwordRef = useRef();
|
||||
const submit = useCallback(async () => {
|
||||
const password = passwordRef.current.value;
|
||||
if (await props.validate(password)) {
|
||||
props.onDone();
|
||||
} else {
|
||||
setIsWrong(true);
|
||||
passwordRef.current.focus();
|
||||
}
|
||||
}, [setIsWrong, props]);
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title={props.title}
|
||||
icon={props.icon}
|
||||
content={
|
||||
<Box my={1}>
|
||||
<Input
|
||||
ref={passwordRef}
|
||||
autoFocus
|
||||
variant={isWrong ? "error" : "default"}
|
||||
type="password"
|
||||
placeholder="Enter vault password"
|
||||
onKeyUp={async e => {
|
||||
if (e.key === "Enter") {
|
||||
await submit();
|
||||
} else {
|
||||
setIsWrong(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{isWrong && (
|
||||
<Flex alignItems="center" color="red" mt={2}>
|
||||
<Icon.AlertTriangle size={16} />
|
||||
<Text ml={1} fontSize={"subBody"}>
|
||||
Wrong password
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
positiveButton={{
|
||||
text: props.positiveButtonText,
|
||||
onClick: submit
|
||||
}}
|
||||
negativeButton={{ text: "Cancel", onClick: props.onCancel }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getDialogData(type) {
|
||||
switch (type) {
|
||||
case "create_vault":
|
||||
return {
|
||||
title: "Set Up Your Vault",
|
||||
icon: Icon.Shield,
|
||||
positiveButtonText: "Done"
|
||||
};
|
||||
case "unlock_vault":
|
||||
return {
|
||||
title: "Unlock Vault",
|
||||
icon: Icon.Unlock,
|
||||
positiveButtonText: "Unlock"
|
||||
};
|
||||
case "unlock_note":
|
||||
return {
|
||||
title: "Unlock Note",
|
||||
icon: Icon.Unlock,
|
||||
positiveButtonText: "Unlock"
|
||||
};
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export const showPasswordDialog = (type, validate) => {
|
||||
const { title, icon, positiveButtonText } = getDialogData(type);
|
||||
return showDialog(perform => (
|
||||
<PasswordDialog
|
||||
title={title}
|
||||
icon={icon}
|
||||
positiveButtonText={positiveButtonText}
|
||||
onCancel={() => perform(false)}
|
||||
validate={validate}
|
||||
onDone={() => perform(true)}
|
||||
/>
|
||||
));
|
||||
};
|
||||
@@ -8,6 +8,8 @@ import { confirm } from "../dialogs/confirm";
|
||||
import { showMoveNoteDialog } from "../dialogs/movenotedialog";
|
||||
import { store, useStore } from "../../stores/note-store";
|
||||
import { store as editorStore } from "../../stores/editor-store";
|
||||
import { showPasswordDialog } from "../dialogs/passworddialog";
|
||||
import { db } from "../../common";
|
||||
|
||||
const dropdownRefs = [];
|
||||
const menuItems = (note, index) => [
|
||||
@@ -25,14 +27,33 @@ const menuItems = (note, index) => [
|
||||
},
|
||||
{
|
||||
title: note.favorite ? "Unfavorite" : "Favorite",
|
||||
onClick: () => store.getState().favorite(note, index)
|
||||
onClick: () => store.getState().favorite(note)
|
||||
},
|
||||
{ title: "Edit", onClick: () => editorStore.getState().openSession(note) },
|
||||
{ title: note.locked ? "Remove lock" : "Lock" }, //TODO
|
||||
{
|
||||
title: note.locked ? "Unlock" : "Lock",
|
||||
onClick: async () => {
|
||||
const { unlock, lock } = store.getState();
|
||||
if (!note.locked) {
|
||||
lock(note.id);
|
||||
} else {
|
||||
unlock(note.id);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Move to Trash",
|
||||
color: "red",
|
||||
onClick: () => {
|
||||
onClick: async () => {
|
||||
if (note.locked) {
|
||||
const res = await showPasswordDialog("unlock_note", password => {
|
||||
return db.vault
|
||||
.unlock(password)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
});
|
||||
if (!res) return;
|
||||
}
|
||||
confirm(
|
||||
Icon.Trash2,
|
||||
"Delete",
|
||||
@@ -89,7 +110,7 @@ function Note(props) {
|
||||
)}
|
||||
</Flex>
|
||||
}
|
||||
pinned={note.pinned}
|
||||
pinned={props.pinnable && note.pinned}
|
||||
menuData={note}
|
||||
menuItems={menuItems(note, index)}
|
||||
dropdownRefs={dropdownRefs}
|
||||
@@ -104,6 +125,7 @@ export default React.memo(Note, function(prevProps, nextProps) {
|
||||
prevItem.pinned === nextItem.pinned &&
|
||||
prevItem.favorite === nextItem.favorite &&
|
||||
prevItem.headline === nextItem.headline &&
|
||||
prevItem.title === nextItem.title
|
||||
prevItem.title === nextItem.title &&
|
||||
prevItem.locked === nextItem.locked
|
||||
);
|
||||
});
|
||||
|
||||
@@ -12,12 +12,14 @@ import { motion } from "framer-motion";
|
||||
const Properties = props => {
|
||||
const pinned = useStore(store => store.session.pinned);
|
||||
const favorite = useStore(store => store.session.favorite);
|
||||
const locked = useStore(store => store.session.locked);
|
||||
const colors = useStore(store => store.session.colors);
|
||||
const tags = useStore(store => store.session.tags);
|
||||
|
||||
const setSession = useStore(store => store.setSession);
|
||||
const setColor = useStore(store => store.setColor);
|
||||
const setTag = useStore(store => store.setTag);
|
||||
const toggleLocked = useStore(store => store.toggleLocked);
|
||||
const hideProperties = useAppStore(store => store.hideProperties);
|
||||
const showProperties = useAppStore(store => store.showProperties);
|
||||
const arePropertiesVisible = useAppStore(store => store.arePropertiesVisible);
|
||||
@@ -118,7 +120,8 @@ const Properties = props => {
|
||||
<CheckBox
|
||||
icon={Icon.Lock}
|
||||
label="Lock"
|
||||
onChecked={props.onLocked}
|
||||
checked={locked}
|
||||
onClick={toggleLocked}
|
||||
/>
|
||||
<Flex fontSize="body" sx={{ marginBottom: 3 }} alignItems="center">
|
||||
<Icon.Book size={18} />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import createStore from "../common/store";
|
||||
import { db } from "../common";
|
||||
import { showPasswordDialog } from "../components/dialogs/passworddialog";
|
||||
|
||||
function appStore(set, get) {
|
||||
return {
|
||||
@@ -67,6 +68,19 @@ function appStore(set, get) {
|
||||
if (get().selectedItems.length <= 0) {
|
||||
get().exitSelectionMode();
|
||||
}
|
||||
},
|
||||
createVault: function() {
|
||||
return showPasswordDialog("create_vault", password =>
|
||||
db.vault.create(password)
|
||||
);
|
||||
},
|
||||
unlockVault: function() {
|
||||
return showPasswordDialog("unlock_vault", password => {
|
||||
return db.vault
|
||||
.unlock(password)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import createStore from "../common/store";
|
||||
import { store as noteStore, LIST_TYPES } from "./note-store";
|
||||
import { store as appStore } from "./app-store";
|
||||
import { db } from "../common";
|
||||
import { showPasswordDialog } from "../components/dialogs/passworddialog";
|
||||
|
||||
const SESSION_STATES = {
|
||||
stale: "stale",
|
||||
@@ -16,6 +17,7 @@ const DEFAULT_SESSION = {
|
||||
id: "",
|
||||
pinned: false,
|
||||
favorite: false,
|
||||
locked: false,
|
||||
tags: [],
|
||||
colors: [],
|
||||
dateEdited: 0,
|
||||
@@ -42,10 +44,27 @@ function editorStore(set, get) {
|
||||
},
|
||||
openSession: async function(note) {
|
||||
clearTimeout(get().session.timeout);
|
||||
const content = {
|
||||
text: note.content.text,
|
||||
delta: await db.notes.note(note).delta()
|
||||
};
|
||||
let content = {};
|
||||
if (!note.locked) {
|
||||
content = {
|
||||
text: note.content.text,
|
||||
delta: await db.notes.note(note).delta()
|
||||
};
|
||||
} else {
|
||||
const result = await showPasswordDialog("unlock_note", password => {
|
||||
return db.vault
|
||||
.open(note.id, password)
|
||||
.then(note => {
|
||||
content = note.content;
|
||||
return true;
|
||||
})
|
||||
.catch(e => {
|
||||
if (e.message === "ERR_WRNG_PwD") return false;
|
||||
else console.error(e);
|
||||
});
|
||||
});
|
||||
if (!result) return;
|
||||
}
|
||||
noteStore.getState().setSelectedNote(note.id);
|
||||
set(state => {
|
||||
state.session = {
|
||||
@@ -55,20 +74,31 @@ function editorStore(set, get) {
|
||||
pinned: note.pinned,
|
||||
favorite: note.favorite,
|
||||
colors: note.colors,
|
||||
locked: note.locked,
|
||||
tags: note.tags,
|
||||
dateEdited: note.dateEdited,
|
||||
content,
|
||||
state: SESSION_STATES.new
|
||||
};
|
||||
});
|
||||
saveLastOpenedNote(note.id);
|
||||
saveLastOpenedNote(!note.locked ? note.id : undefined);
|
||||
},
|
||||
saveSession: function(oldSession) {
|
||||
set(state => {
|
||||
state.session.isSaving = true;
|
||||
});
|
||||
const { session } = get();
|
||||
const { title, id, content, pinned, favorite, tags, colors } = session;
|
||||
const {
|
||||
title,
|
||||
id,
|
||||
content,
|
||||
pinned,
|
||||
favorite,
|
||||
locked,
|
||||
tags,
|
||||
colors
|
||||
} = session;
|
||||
|
||||
let note = {
|
||||
content,
|
||||
title,
|
||||
@@ -78,7 +108,12 @@ function editorStore(set, get) {
|
||||
tags,
|
||||
colors
|
||||
};
|
||||
db.notes.add(note).then(id => {
|
||||
|
||||
const func = locked
|
||||
? db.vault.save.bind(db.vault)
|
||||
: db.notes.add.bind(db.notes);
|
||||
|
||||
func(note).then(id => {
|
||||
if (tags.length > 0) updateContext("tags", tags);
|
||||
if (colors.length > 0) {
|
||||
updateContext("colors", colors);
|
||||
@@ -95,7 +130,7 @@ function editorStore(set, get) {
|
||||
});
|
||||
|
||||
notesState.refresh();
|
||||
saveLastOpenedNote(id);
|
||||
saveLastOpenedNote(locked ? undefined : id);
|
||||
|
||||
// we update favorites only if favorite has changed
|
||||
if (!oldSession || oldSession.favorite !== session.favorite) {
|
||||
@@ -126,6 +161,14 @@ function editorStore(set, get) {
|
||||
saveLastOpenedNote();
|
||||
noteStore.getState().setSelectedNote(0);
|
||||
},
|
||||
toggleLocked: function() {
|
||||
const { session } = get();
|
||||
if (session.locked) {
|
||||
noteStore.getState().unlock(session.id);
|
||||
} else {
|
||||
noteStore.getState().lock(session.id);
|
||||
}
|
||||
},
|
||||
setColor: function(color) {
|
||||
setTagOrColor(get().session, "colors", color, "color", get().setSession);
|
||||
},
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { db } from "../common/index";
|
||||
import createStore from "../common/store";
|
||||
import { store as editorStore } from "./editor-store";
|
||||
import { store as appStore } from "./app-store";
|
||||
import { showPasswordDialog } from "../components/dialogs/passworddialog";
|
||||
|
||||
const LIST_TYPES = {
|
||||
fav: "favorites"
|
||||
@@ -34,6 +36,7 @@ function noteStore(set, get) {
|
||||
});
|
||||
},
|
||||
setSelectedContext: function(context) {
|
||||
console.log("setting context");
|
||||
let notes = [];
|
||||
switch (context.type) {
|
||||
case "tag":
|
||||
@@ -75,33 +78,75 @@ function noteStore(set, get) {
|
||||
await db.notes.note(note).pin();
|
||||
set(state => {
|
||||
state.notes = db.notes.group(undefined, true);
|
||||
syncEditor(note, "pinned");
|
||||
});
|
||||
syncEditor(note.id, "pinned");
|
||||
},
|
||||
favorite: async function(note) {
|
||||
await db.notes.note(note).favorite();
|
||||
setValue(set, note.id, "favorite", !note.favorite);
|
||||
get().refreshList(LIST_TYPES.fav);
|
||||
},
|
||||
unlock: function(noteId) {
|
||||
showPasswordDialog("unlock_note", password => {
|
||||
return db.vault
|
||||
.remove(noteId, password)
|
||||
.then(() => true)
|
||||
.catch(e => {
|
||||
if (e.message === "ERR_WRNG_PWD") return false;
|
||||
else console.error(e);
|
||||
});
|
||||
}).then(res => {
|
||||
if (res) {
|
||||
setValue(set, noteId, "locked", false);
|
||||
}
|
||||
});
|
||||
},
|
||||
favorite: async function(note, index) {
|
||||
await db.notes.note(note).favorite();
|
||||
set(state => {
|
||||
if (index < 0 || !index) {
|
||||
index = state.notes.items.findIndex(n => n.id === note.id);
|
||||
if (index < 0) return;
|
||||
}
|
||||
state.notes.items[index].favorite = !note.favorite;
|
||||
syncEditor(note, "favorite");
|
||||
});
|
||||
get().refreshList(LIST_TYPES.fav);
|
||||
lock: function lock(noteId) {
|
||||
db.vault
|
||||
.add(noteId)
|
||||
.then(() => {
|
||||
setValue(set, noteId, "locked", true);
|
||||
})
|
||||
.catch(async ({ message }) => {
|
||||
switch (message) {
|
||||
case "ERR_NO_VAULT":
|
||||
return appStore.getState().createVault();
|
||||
case "ERR_VAULT_LOCKED":
|
||||
return appStore.getState().unlockVault();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.then(result => {
|
||||
if (result === true) {
|
||||
lock(noteId);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function syncEditor(note, action) {
|
||||
function syncEditor(noteId, action) {
|
||||
const editorState = editorStore.getState();
|
||||
if (editorState.session.id === note.id) {
|
||||
if (editorState.session.id === noteId) {
|
||||
editorState.setSession(
|
||||
state => (state.session[action] = !state.session[action])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function setValue(set, noteId, prop, value) {
|
||||
set(state => {
|
||||
const arr = !state.selectedNotes.length
|
||||
? state.notes.items
|
||||
: state.selectedNotes;
|
||||
let index = arr.findIndex(n => n.id === noteId);
|
||||
if (index < 0) return;
|
||||
arr[index][prop] = value;
|
||||
});
|
||||
syncEditor(noteId, prop);
|
||||
}
|
||||
|
||||
const [useStore, store] = createStore(noteStore);
|
||||
|
||||
export { useStore, store, LIST_TYPES };
|
||||
|
||||
@@ -97,6 +97,17 @@ const theme = (colors, shadows) => ({
|
||||
outline: "none",
|
||||
boxShadow: 4
|
||||
}
|
||||
},
|
||||
error: {
|
||||
variant: "forms.default",
|
||||
borderColor: "red",
|
||||
":focus": {
|
||||
outline: "none",
|
||||
borderColor: "red"
|
||||
},
|
||||
":hover": {
|
||||
borderColor: "red"
|
||||
}
|
||||
}
|
||||
},
|
||||
text: {
|
||||
|
||||
@@ -64,6 +64,7 @@ function Home() {
|
||||
notes.groupCounts[groupIndex] && (
|
||||
<Note
|
||||
index={index}
|
||||
pinnable={true}
|
||||
groupIndex={groupIndex}
|
||||
item={notes.items[index]}
|
||||
/>
|
||||
|
||||
@@ -16,9 +16,12 @@ const Notes = props => {
|
||||
clearSelectedContext();
|
||||
};
|
||||
}, [clearSelectedContext]);
|
||||
console.log("refreshing notesssss", selectedNotes);
|
||||
return (
|
||||
<ListContainer
|
||||
item={index => <Note index={index} item={selectedNotes[index]} />}
|
||||
item={index => (
|
||||
<Note index={index} pinnable={false} item={selectedNotes[index]} />
|
||||
)}
|
||||
itemsLength={selectedNotes.length}
|
||||
button={{
|
||||
content: "Make a new note",
|
||||
|
||||
@@ -6877,7 +6877,7 @@ normalize-url@^3.0.0, normalize-url@^3.0.1:
|
||||
|
||||
"notes-core@https://github.com/thecodrr/notes-core.git":
|
||||
version "1.2.0"
|
||||
resolved "https://github.com/thecodrr/notes-core.git#5ee5dfbd5df2abb748cc1a6b8eae6bdad234dba3"
|
||||
resolved "https://github.com/thecodrr/notes-core.git#ad90f7747165db301cff457b0f702a147977ca47"
|
||||
dependencies:
|
||||
fast-sort "^2.0.1"
|
||||
fuzzysearch "^1.0.3"
|
||||
|
||||
Reference in New Issue
Block a user