diff --git a/apps/web/src/components/checkbox/index.js b/apps/web/src/components/checkbox/index.js index 937b16a15..6ffcfc4fd 100644 --- a/apps/web/src/components/checkbox/index.js +++ b/apps/web/src/components/checkbox/index.js @@ -10,9 +10,12 @@ const CheckBox = props => { return ( { - setChecked(!checked); if (props.onChecked) { props.onChecked(!checked); + setChecked(!checked); + } + if (props.onClick) { + props.onClick(); } }} width="full" diff --git a/apps/web/src/components/dialogs/password-dialog.js b/apps/web/src/components/dialogs/password-dialog.js new file mode 100644 index 000000000..26755a4f4 --- /dev/null +++ b/apps/web/src/components/dialogs/password-dialog.js @@ -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 ( + + { + if (e.key === "Enter") { + await submit(); + } else { + setIsWrong(false); + } + }} + /> + {isWrong && ( + + + + Wrong password + + + )} + + } + 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 => ( + perform(false)} + validate={validate} + onDone={() => perform(true)} + /> + )); +}; diff --git a/apps/web/src/components/note/index.js b/apps/web/src/components/note/index.js index 87ec012f3..bed1a1d68 100644 --- a/apps/web/src/components/note/index.js +++ b/apps/web/src/components/note/index.js @@ -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) { )} } - 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 ); }); diff --git a/apps/web/src/components/properties/index.js b/apps/web/src/components/properties/index.js index c26ace710..e3da2ab72 100644 --- a/apps/web/src/components/properties/index.js +++ b/apps/web/src/components/properties/index.js @@ -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 => { diff --git a/apps/web/src/stores/app-store.js b/apps/web/src/stores/app-store.js index 33a5d7eff..366cb1b34 100644 --- a/apps/web/src/stores/app-store.js +++ b/apps/web/src/stores/app-store.js @@ -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); + }); } }; } diff --git a/apps/web/src/stores/editor-store.js b/apps/web/src/stores/editor-store.js index 3b1a99ead..e63592d46 100644 --- a/apps/web/src/stores/editor-store.js +++ b/apps/web/src/stores/editor-store.js @@ -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); }, diff --git a/apps/web/src/stores/note-store.js b/apps/web/src/stores/note-store.js index b868d9a2d..13095ccd3 100644 --- a/apps/web/src/stores/note-store.js +++ b/apps/web/src/stores/note-store.js @@ -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 }; diff --git a/apps/web/src/utils/theme.js b/apps/web/src/utils/theme.js index 9fd60d1ec..3c7908025 100644 --- a/apps/web/src/utils/theme.js +++ b/apps/web/src/utils/theme.js @@ -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: { diff --git a/apps/web/src/views/Home.js b/apps/web/src/views/Home.js index 14628b5bf..7c4aa6413 100644 --- a/apps/web/src/views/Home.js +++ b/apps/web/src/views/Home.js @@ -64,6 +64,7 @@ function Home() { notes.groupCounts[groupIndex] && ( diff --git a/apps/web/src/views/Notes.js b/apps/web/src/views/Notes.js index 0ab596fe6..25ece7634 100644 --- a/apps/web/src/views/Notes.js +++ b/apps/web/src/views/Notes.js @@ -16,9 +16,12 @@ const Notes = props => { clearSelectedContext(); }; }, [clearSelectedContext]); + console.log("refreshing notesssss", selectedNotes); return ( } + item={index => ( + + )} itemsLength={selectedNotes.length} button={{ content: "Make a new note", diff --git a/apps/web/yarn.lock b/apps/web/yarn.lock index b426f80a8..172b05c78 100644 --- a/apps/web/yarn.lock +++ b/apps/web/yarn.lock @@ -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"