web: add support for tabs

This commit is contained in:
Abdullah Atta
2024-01-05 18:50:35 +05:00
parent e166a634f2
commit 4a2fcf950e
42 changed files with 2579 additions and 1834 deletions

View File

@@ -49,6 +49,7 @@
"cronosjs": "^1.7.1",
"date-fns": "^2.30.0",
"dayjs": "1.11.9",
"diffblazer": "^1.0.1",
"electron-trpc": "0.5.2",
"event-source-polyfill": "^1.0.25",
"fflate": "^0.8.0",
@@ -73,6 +74,7 @@
"react-dom": "18.2.0",
"react-dropzone": "^14.2.3",
"react-error-boundary": "^4.0.12",
"react-freeze": "^1.0.3",
"react-hot-toast": "^2.4.1",
"react-loading-skeleton": "^3.3.1",
"react-modal": "3.16.1",
@@ -101,6 +103,7 @@
"@types/react-avatar-editor": "^13.0.2",
"@types/react-dom": "^18.2.17",
"@types/react-modal": "3.16.3",
"@types/react-scroll-sync": "^0.9.0",
"@types/tinycolor2": "^1.4.3",
"@types/wicg-file-system-access": "^2020.9.6",
"@vitejs/plugin-react-swc": "3.3.2",
@@ -31955,7 +31958,7 @@
},
"../desktop": {
"name": "@notesnook/desktop",
"version": "3.0.4-beta",
"version": "3.0.6-beta",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
@@ -35022,8 +35025,7 @@
},
"node_modules/@babel/helper-module-imports": {
"version": "7.24.3",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz",
"integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.24.0"
},
@@ -35061,8 +35063,7 @@
},
"node_modules/@babel/helper-plugin-utils": {
"version": "7.24.0",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz",
"integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
@@ -35132,8 +35133,7 @@
},
"node_modules/@babel/helper-string-parser": {
"version": "7.24.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz",
"integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
@@ -36125,8 +36125,7 @@
},
"node_modules/@babel/plugin-transform-runtime": {
"version": "7.24.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.3.tgz",
"integrity": "sha512-J0BuRPNlNqlMTRJ72eVptpt9VcInbxO6iP3jaxr+1NPhC0UkKL+6oeX6VXMEYdADnuqmMmsBspt4d5w8Y/TCbQ==",
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.24.3",
"@babel/helper-plugin-utils": "^7.24.0",
@@ -36144,8 +36143,7 @@
},
"node_modules/@babel/plugin-transform-runtime/node_modules/@babel/helper-define-polyfill-provider": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.1.tgz",
"integrity": "sha512-o7SDgTJuvx5vLKD6SFvkydkSMBvahDKGiNJzG22IZYXhiqoe9efY7zocICBgzHV4IRg5wdgl2nEL/tulKIEIbA==",
"license": "MIT",
"dependencies": {
"@babel/helper-compilation-targets": "^7.22.6",
"@babel/helper-plugin-utils": "^7.22.5",
@@ -36159,8 +36157,7 @@
},
"node_modules/@babel/plugin-transform-runtime/node_modules/babel-plugin-polyfill-corejs3": {
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz",
"integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==",
"license": "MIT",
"dependencies": {
"@babel/helper-define-polyfill-provider": "^0.6.1",
"core-js-compat": "^3.36.1"
@@ -36171,8 +36168,7 @@
},
"node_modules/@babel/plugin-transform-runtime/node_modules/babel-plugin-polyfill-regenerator": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.1.tgz",
"integrity": "sha512-JfTApdE++cgcTWjsiCQlLyFBMbTUft9ja17saCc93lgV33h4tuCVj7tlvu//qpLwaG+3yEz7/KhahGrUMkVq9g==",
"license": "MIT",
"dependencies": {
"@babel/helper-define-polyfill-provider": "^0.6.1"
},
@@ -36464,8 +36460,7 @@
},
"node_modules/@babel/types": {
"version": "7.24.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz",
"integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.20",
@@ -38092,9 +38087,8 @@
},
"node_modules/@types/react-avatar-editor": {
"version": "13.0.2",
"resolved": "https://registry.npmjs.org/@types/react-avatar-editor/-/react-avatar-editor-13.0.2.tgz",
"integrity": "sha512-vnGU4sx5TDB9JMCuw+kB3+jfDNMhVlpBbgMRZG9NzVnaBb5xUjVV4VmHdD2O8gj/pb5GN+QXKk7jan09aMjG2A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/react": "*"
}
@@ -38115,6 +38109,15 @@
"@types/react": "*"
}
},
"node_modules/@types/react-scroll-sync": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@types/react-scroll-sync/-/react-scroll-sync-0.9.0.tgz",
"integrity": "sha512-DFJzqtF0lMUNxsKZ9aoFS+LGieGLXWQVWvCMTZWKm/qcGz+GCNTqwdHsnyWW3yy/aNSUxlXCAWdHaemGaES6lQ==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/resolve": {
"version": "1.17.1",
"dev": true,
@@ -38709,8 +38712,7 @@
},
"node_modules/babel-plugin-polyfill-corejs2": {
"version": "0.4.10",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.10.tgz",
"integrity": "sha512-rpIuu//y5OX6jVU+a5BCn1R5RSZYWAl2Nar76iwaOdycqb6JPxediskWFMMl7stfwNJR4b7eiQvh5fB5TEQJTQ==",
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.22.6",
"@babel/helper-define-polyfill-provider": "^0.6.1",
@@ -38722,8 +38724,7 @@
},
"node_modules/babel-plugin-polyfill-corejs2/node_modules/@babel/helper-define-polyfill-provider": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.1.tgz",
"integrity": "sha512-o7SDgTJuvx5vLKD6SFvkydkSMBvahDKGiNJzG22IZYXhiqoe9efY7zocICBgzHV4IRg5wdgl2nEL/tulKIEIbA==",
"license": "MIT",
"dependencies": {
"@babel/helper-compilation-targets": "^7.22.6",
"@babel/helper-plugin-utils": "^7.22.5",
@@ -38874,8 +38875,6 @@
},
"node_modules/browserslist": {
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
"integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
"funding": [
{
"type": "opencollective",
@@ -38890,6 +38889,7 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"caniuse-lite": "^1.0.30001587",
"electron-to-chromium": "^1.4.668",
@@ -39020,8 +39020,6 @@
},
"node_modules/caniuse-lite": {
"version": "1.0.30001599",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001599.tgz",
"integrity": "sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA==",
"funding": [
{
"type": "opencollective",
@@ -39035,7 +39033,8 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
]
],
"license": "CC-BY-4.0"
},
"node_modules/canvas": {
"version": "2.11.2",
@@ -39253,8 +39252,7 @@
},
"node_modules/core-js-compat": {
"version": "3.36.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.1.tgz",
"integrity": "sha512-Dk997v9ZCt3X/npqzyGdTlq6t7lDBhZwGvV94PKzDArjp7BTRm7WlDAXYd/OWdeFHO8OChQYRJNJvUCqCbrtKA==",
"license": "MIT",
"dependencies": {
"browserslist": "^4.23.0"
},
@@ -39533,6 +39531,32 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/diffblazer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/diffblazer/-/diffblazer-1.0.1.tgz",
"integrity": "sha512-aBQUrBdpDYqqVf/8tlLh0JVRw/IBFlLQR7Wgn3/8kO54cq+HRcPuT/JUbbm/136hutuRFXzWERDIS/lX3HViYg==",
"dependencies": {
"htmlparser2": "^9.0.0"
}
},
"node_modules/diffblazer/node_modules/htmlparser2": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz",
"integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==",
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.1.0",
"entities": "^4.5.0"
}
},
"node_modules/dom-serializer": {
"version": "2.0.0",
"license": "MIT",
@@ -39637,8 +39661,7 @@
},
"node_modules/electron-to-chromium": {
"version": "1.4.713",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.713.tgz",
"integrity": "sha512-vDarADhwntXiULEdmWd77g2dV6FrNGa8ecAC29MZ4TwPut2fvosD0/5sJd1qWNNe8HcJFAC+F5Lf9jW1NPtWmw=="
"license": "ISC"
},
"node_modules/electron-trpc": {
"version": "0.5.2",
@@ -42830,8 +42853,7 @@
},
"node_modules/node-releases": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw=="
"license": "MIT"
},
"node_modules/nopt": {
"version": "5.0.0",
@@ -43390,8 +43412,7 @@
},
"node_modules/react-avatar-editor": {
"version": "13.0.2",
"resolved": "https://registry.npmjs.org/react-avatar-editor/-/react-avatar-editor-13.0.2.tgz",
"integrity": "sha512-a4ajbi7lwDh98kgEtSEeKMu0vs0CHTczkq4Xcxr1EiwMFH1GlgHCEtwGU8q/H5W8SeLnH4KPK8LUjEEaZXklxQ==",
"license": "MIT",
"dependencies": {
"@babel/plugin-transform-runtime": "^7.12.1",
"@babel/runtime": "^7.12.5",
@@ -43457,6 +43478,17 @@
"react": ">=16.13.1"
}
},
"node_modules/react-freeze": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.4.tgz",
"integrity": "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==",
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=17.0.0"
}
},
"node_modules/react-hot-toast": {
"version": "2.4.1",
"license": "MIT",

View File

@@ -47,6 +47,7 @@
"cronosjs": "^1.7.1",
"date-fns": "^2.30.0",
"dayjs": "1.11.9",
"diffblazer": "^1.0.1",
"electron-trpc": "0.5.2",
"event-source-polyfill": "^1.0.25",
"fflate": "^0.8.0",
@@ -71,6 +72,7 @@
"react-dom": "18.2.0",
"react-dropzone": "^14.2.3",
"react-error-boundary": "^4.0.12",
"react-freeze": "^1.0.3",
"react-hot-toast": "^2.4.1",
"react-loading-skeleton": "^3.3.1",
"react-modal": "3.16.1",
@@ -99,6 +101,7 @@
"@types/react-avatar-editor": "^13.0.2",
"@types/react-dom": "^18.2.17",
"@types/react-modal": "3.16.3",
"@types/react-scroll-sync": "^0.9.0",
"@types/tinycolor2": "^1.4.3",
"@types/wicg-file-system-access": "^2020.9.6",
"@vitejs/plugin-react-swc": "3.3.2",

View File

@@ -22,7 +22,7 @@ import { useStore } from "./stores/app-store";
import { useStore as useUserStore } from "./stores/user-store";
import { useStore as useThemeStore } from "./stores/theme-store";
import { useStore as useAttachmentStore } from "./stores/attachment-store";
import { useStore as useEditorStore } from "./stores/editor-store";
import { useEditorStore } from "./stores/editor-store";
import { useStore as useAnnouncementStore } from "./stores/announcement-store";
import { resetNotices, scheduleBackups } from "./common/notices";
import { introduceFeatures, showUpgradeReminderDialogs } from "./common";
@@ -244,7 +244,7 @@ export default function AppEffects({ setShow }: AppEffectsProps) {
onData(itemType) {
switch (itemType) {
case "note":
hashNavigate("/notes/create", { addNonce: true, replace: true });
useEditorStore.getState().newSession();
break;
case "notebook":
hashNavigate("/notebooks/create", { replace: true });

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Dialogs } from "../dialogs";
import { store as tagStore } from "../stores/tag-store";
import { store as appStore } from "../stores/app-store";
import { store as editorStore } from "../stores/editor-store";
import { useEditorStore } from "../stores/editor-store";
import { store as noteStore } from "../stores/note-store";
import { db } from "./db";
import { showToast } from "../utils/toast";
@@ -378,7 +378,7 @@ export function showEditTagDialog(tag: Tag) {
await db.tags.add({ id: tag.id, title });
showToast("success", "Tag edited!");
tagStore.refresh();
editorStore.refreshTags();
useEditorStore.getState().refreshTags();
noteStore.refresh();
appStore.refreshNavItems();
perform(true);

View File

@@ -43,12 +43,12 @@ import { FeatureKeys } from "../dialogs/feature-dialog";
import { ZipEntry, createUnzipIterator } from "../utils/streams/unzip-stream";
import { User } from "@notesnook/core/dist/api/user-manager";
import { LegacyBackupFile } from "@notesnook/core";
import { useEditorStore } from "../stores/editor-store";
export const CREATE_BUTTON_MAP = {
notes: {
title: "Add a note",
onClick: () =>
hashNavigate("/notes/create", { addNonce: true, replace: true })
onClick: () => useEditorStore.getState().newSession()
},
notebooks: {
title: "Create a notebook",

View File

@@ -18,9 +18,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { immerable, setAutoFreeze } from "immer";
import { create } from "zustand";
import { subscribeWithSelector } from "zustand/middleware";
import {
subscribeWithSelector,
persist,
PersistOptions
} from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
import { GetState, SetState } from "../stores";
import { GetState, IStore, SetState } from "../stores";
setAutoFreeze(false);
export function createStore<T>(
@@ -38,9 +42,33 @@ export function createStore<T>(
})
)
);
// return Object.defineProperty(store as CustomStore<T>, "get", {
// get: () => store.getState()
// });
return [store, store.getState()] as const;
}
export function createPersistedStore<T extends object>(
Store: IStore<T>,
options: PersistOptions<T, Partial<T>>
) {
const store = create<
T,
[
["zustand/persist", Partial<T>],
["zustand/subscribeWithSelector", never],
["zustand/immer", never]
]
>(
persist(
subscribeWithSelector(
immer((set, get) => {
const store = new Store(set, get);
(store as any)[immerable] = true;
return store;
})
),
options
)
);
return [store, store.getState()] as const;
}

View File

@@ -37,7 +37,6 @@ import {
Reupload,
Uploading
} from "../icons";
import { hashNavigate } from "../../navigation";
import {
closeOpenedDialog,
showPromptDialog
@@ -58,6 +57,7 @@ import { AppEventManager, AppEvents } from "../../common/app-events";
import { getFormattedDate } from "@notesnook/common";
import { MenuItem } from "@notesnook/ui";
import { Attachment as AttachmentType } from "@notesnook/core";
import { useEditorStore } from "../../stores/editor-store";
const FILE_ICONS: Record<string, Icon> = {
"image/": FileImage,
@@ -247,7 +247,7 @@ const AttachmentMenuItems: (
key: note.id,
title: note.title,
onClick: () => {
hashNavigate(`/notes/${note.id}/edit`);
useEditorStore.getState().openSession(note);
closeOpenedDialog();
}
});

View File

@@ -17,49 +17,62 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Flex, Button, Text } from "@theme-ui/components";
import { Flex, Button, Text, FlexProps } from "@theme-ui/components";
import { getFormattedDate } from "@notesnook/common";
function ContentToggle(props) {
type ContentToggle = {
isSelected: boolean;
isOtherSelected: boolean;
onToggle: () => void;
label: string;
dateEdited: number;
resolveConflict: (options: { saveCopy: boolean }) => void;
readonly: boolean;
sx: FlexProps["sx"];
};
function ContentToggle(props: ContentToggle) {
const {
isSelected,
isOtherSelected,
onToggle,
sx,
label,
dateEdited,
resolveConflict
resolveConflict,
readonly,
sx
} = props;
return (
<Flex sx={{ ...sx, flexDirection: "column" }}>
<Flex>
{isOtherSelected && (
{!readonly && (
<Flex>
{isOtherSelected && (
<Button
variant="accent"
mr={2}
onClick={() => resolveConflict({ saveCopy: true })}
p={1}
px={2}
>
Save copy
</Button>
)}
<Button
variant="accent"
mr={2}
onClick={() => resolveConflict({ saveCopy: true })}
variant={isOtherSelected ? "error" : "accent"}
onClick={() => {
if (isOtherSelected) {
resolveConflict({ saveCopy: false });
} else {
onToggle();
}
}}
p={1}
px={2}
>
Save copy
{isSelected ? "Undo" : isOtherSelected ? "Discard" : "Keep"}
</Button>
)}
<Button
variant={isOtherSelected ? "error" : "accent"}
onClick={() => {
if (isOtherSelected) {
resolveConflict({ saveCopy: false });
} else {
onToggle();
}
}}
p={1}
px={2}
>
{isSelected ? "Undo" : isOtherSelected ? "Discard" : "Keep"}
</Button>
</Flex>
</Flex>
)}
<Text variant="subBody" mt={1}>
{label} | {getFormattedDate(dateEdited)}
</Text>

View File

@@ -1,296 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useState, useEffect, useCallback } from "react";
import { Flex, Text, Button } from "@theme-ui/components";
import { Loading, ImageDownload } from "../icons";
import ContentToggle from "./content-toggle";
import { store as notesStore } from "../../stores/note-store";
import { db } from "../../common/db";
import { useStore as useAppStore } from "../../stores/app-store";
import { useStore as useEditorStore } from "../../stores/editor-store";
import { hashNavigate } from "../../navigation";
import { showToast } from "../../utils/toast";
import { ScrollSync, ScrollSyncPane } from "react-scroll-sync";
import { Editor } from "../editor";
function DiffViewer(props) {
const { noteId } = props;
const setIsEditorOpen = useAppStore((store) => store.setIsEditorOpen);
const sync = useAppStore((store) => store.sync);
const openSession = useEditorStore((store) => store.openSession);
const [conflictedNote, setConflictedNote] = useState();
const [remoteContent, setRemoteContent] = useState();
const [localContent, setLocalContent] = useState();
const [isDownloadingImages, setIsDownloadingImages] = useState(false);
const [htmlDiff, setHtmlDiff] = useState({});
const [selectedContent, setSelectedContent] = useState(-1);
const resolveConflict = useCallback(
async ({ toKeep, toCopy, toKeepDateEdited, dateResolved }) => {
if (!conflictedNote) return;
await db.notes.add({
id: conflictedNote.id,
dateEdited: toKeepDateEdited,
conflicted: false
});
await db.content.add({
id: conflictedNote.contentId,
data: toKeep,
type: "tiptap",
dateResolved,
conflicted: false,
sessionId: Date.now()
});
if (toCopy) {
const toCopyContent = {
data: toCopy,
type: "tiptap"
};
await db.notes.add({
content: toCopyContent,
title: conflictedNote.title + " (COPY)"
});
}
await notesStore.refresh();
hashNavigate(`/notes/${conflictedNote.id}/edit`, { replace: true });
const conflictsCount = await db.notes.conflicted.count();
if (conflictsCount) {
showToast(
"success",
`Conflict resolved. ${conflictsCount} conflicts left.`
);
} else {
showToast("success", "All conflicts resolved. Starting sync.");
await sync();
}
},
[conflictedNote, sync]
);
useEffect(() => {
(async function () {
let note = await db.notes.note(noteId);
if (!note) {
hashNavigate(`/notes/create`, { replace: true });
return;
}
await openSession(noteId);
setIsEditorOpen(true);
setConflictedNote(note);
const content = await db.content.get(note.contentId);
if (!content.conflicted)
return resolveConflict({
toKeep: content.data,
toKeepDateEdited: content.dateEdited
});
setLocalContent({ ...content, conflicted: false });
setRemoteContent(content.conflicted);
setHtmlDiff({ before: content.data, after: content.conflicted.data });
})();
}, [noteId]);
if (!conflictedNote || !localContent || !remoteContent) return null;
return (
<Flex
className="diffviewer"
sx={{
flex: "1 1 auto",
flexDirection: "column",
width: "100%",
overflow: "hidden"
}}
>
<Text
mt={2}
variant="heading"
sx={{
flexShrink: 0,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
textAlign: "center"
}}
>
{conflictedNote.title}
</Text>
<Flex mt={1} sx={{ alignSelf: "center", justifySelf: "center" }}>
<Button
variant="secondary"
onClick={async () => {
setIsDownloadingImages(true);
try {
await Promise.all([
db.content.downloadMedia(noteId, {
data: htmlDiff.before,
type: localContent.type
}),
db.content.downloadMedia(noteId, {
data: htmlDiff.after,
type: remoteContent.type
})
]);
} finally {
setIsDownloadingImages(false);
}
}}
disabled={isDownloadingImages}
mr={2}
sx={{
alignItems: "center",
justifyContent: "center",
display: "flex"
}}
>
{isDownloadingImages ? (
<Loading size={18} />
) : (
<ImageDownload size={18} />
)}
<Text
ml={1}
sx={{ fontSize: "body", display: ["none", "block", "block"] }}
>
{isDownloadingImages ? "Downloading..." : "Load images"}
</Text>
</Button>
</Flex>
<ScrollSync>
<Flex
sx={{
flex: "1 1 auto",
flexDirection: ["column", "column", "row"],
overflow: "hidden"
}}
>
<Flex
className="firstEditor"
sx={{
flex: "1 1 auto",
flexDirection: "column",
width: ["100%", "100%", "50%"],
height: ["50%", "50%", "100%"]
}}
>
<ContentToggle
label="Current note"
dateEdited={localContent.dateEdited}
isSelected={selectedContent === 0}
isOtherSelected={selectedContent === 1}
onToggle={() => setSelectedContent((s) => (s === 0 ? -1 : 0))}
resolveConflict={({ saveCopy }) => {
resolveConflict({
toKeep: remoteContent.data,
toCopy: saveCopy ? localContent.data : null,
toKeepDateEdited: localContent.dateEdited,
dateResolved: remoteContent.dateModified
});
}}
sx={{
borderStyle: "solid",
borderWidth: 0,
borderBottomWidth: 1,
borderColor: "border",
px: 2,
pb: 1
}}
/>
<ScrollSyncPane>
<Flex
sx={{
px: 2,
overflowY: "auto",
flex: 1,
borderStyle: "solid",
borderWidth: 0,
borderRightWidth: [0, 0, 1],
borderBottomWidth: [1, 1, 0],
borderColor: "border"
}}
>
<Editor
content={() => htmlDiff.before}
nonce={0}
options={{ readonly: true, headless: true }}
/>
</Flex>
</ScrollSyncPane>
</Flex>
<Flex
className="secondEditor"
sx={{
flex: "1 1 auto",
flexDirection: "column",
width: ["100%", "100%", "50%"],
height: ["50%", "50%", "100%"]
}}
>
<ContentToggle
resolveConflict={({ saveCopy }) => {
resolveConflict({
toKeep: localContent.data,
toCopy: saveCopy ? remoteContent.data : null,
toKeepDateEdited: remoteContent.dateEdited,
dateResolved: remoteContent.dateModified
});
}}
label="Incoming note"
isSelected={selectedContent === 1}
isOtherSelected={selectedContent === 0}
dateEdited={remoteContent.dateEdited}
onToggle={() => setSelectedContent((s) => (s === 1 ? -1 : 1))}
sx={{
alignItems: "flex-end",
borderStyle: "solid",
borderWidth: 0,
borderBottomWidth: 1,
borderColor: "border",
px: 2,
pb: 1,
pt: [1, 1, 0]
}}
/>
<ScrollSyncPane>
<Flex sx={{ px: 2, overflow: "auto" }}>
<Editor
content={() => htmlDiff.after}
nonce={0}
options={{ readonly: true, headless: true }}
/>
</Flex>
</ScrollSyncPane>
</Flex>
</Flex>
</ScrollSync>
</Flex>
);
}
export default DiffViewer;

View File

@@ -0,0 +1,394 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useState } from "react";
import { Flex, Text, Button } from "@theme-ui/components";
import { Loading, ImageDownload, Copy, Restore } from "../icons";
import ContentToggle from "./content-toggle";
import { store as notesStore } from "../../stores/note-store";
import { db } from "../../common/db";
import {
ConflictedEditorSession,
useEditorStore
} from "../../stores/editor-store";
import { ScrollSync, ScrollSyncPane } from "react-scroll-sync";
import { Editor } from "../editor";
import { ContentItem, Note } from "@notesnook/core";
import { UnlockView } from "../unlock";
import { getFormattedDate } from "@notesnook/common";
type DiffViewerProps = { session: ConflictedEditorSession };
function DiffViewer(props: DiffViewerProps) {
const { session } = props;
const [isDownloadingImages, setIsDownloadingImages] = useState(false);
const [selectedContent, setSelectedContent] = useState(-1);
const [content, setContent] = useState(session.content);
const [conflictedContent, setConflictedContent] = useState(
content.conflicted
);
if (!conflictedContent) return null;
return (
<Flex
className="diffviewer"
sx={{
flex: "1 1 auto",
flexDirection: "column",
width: "100%",
overflow: "hidden"
}}
>
<Text
variant="heading"
sx={{
flexShrink: 0,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
textAlign: "center"
}}
>
{session.note.title}
</Text>
<Flex mt={1} sx={{ alignSelf: "center", justifySelf: "center" }}>
{!content.locked && !conflictedContent.locked && (
<Button
variant="secondary"
onClick={async () => {
setIsDownloadingImages(true);
try {
await Promise.all([
db.content.downloadMedia(session.id, {
data: content.data,
type: content.type
}),
db.content.downloadMedia(session.id, {
data: conflictedContent.data,
type: conflictedContent.type
})
]);
} finally {
setIsDownloadingImages(false);
}
}}
disabled={isDownloadingImages}
mr={2}
sx={{
alignItems: "center",
justifyContent: "center",
display: "flex"
}}
>
{isDownloadingImages ? (
<Loading size={18} />
) : (
<ImageDownload size={18} />
)}
<Text
ml={1}
sx={{ fontSize: "body", display: ["none", "block", "block"] }}
>
{isDownloadingImages ? "Downloading..." : "Load images"}
</Text>
</Button>
)}
{session.type === "diff" ? (
<>
<Button
variant="secondary"
onClick={async () => {
const { closeSessions, openSession } =
useEditorStore.getState();
await db.noteHistory.restore(session.id);
closeSessions(session.id, session.note.id);
await notesStore.refresh();
await openSession(session.note.id, true);
}}
mr={2}
sx={{
alignItems: "center",
justifyContent: "center",
display: "flex"
}}
>
<Restore size={18} />
<Text ml={1}>Restore this version</Text>
</Button>
<Button
variant="secondary"
onClick={async () => {
const { closeSessions, openSession } =
useEditorStore.getState();
const noteId = await createCopy(session.note, content);
closeSessions(session.id);
await notesStore.refresh();
await openSession(noteId);
}}
mr={2}
sx={{
alignItems: "center",
justifyContent: "center",
display: "flex"
}}
>
<Copy size={18} />
<Text ml={1}>Save a copy</Text>
</Button>
</>
) : null}
</Flex>
<ScrollSync>
<Flex
sx={{
flex: "1 1 auto",
flexDirection: ["column", "column", "row"],
overflow: "hidden"
}}
>
<Flex
className="firstEditor"
sx={{
flex: "1 1 auto",
flexDirection: "column",
width: ["100%", "100%", "50%"],
height: ["50%", "50%", "100%"]
}}
>
{content.locked ? (
<UnlockView
title={getFormattedDate(content.dateEdited)}
subtitle="Please enter the password to view this version"
buttonTitle="Unlock"
unlock={async (password) => {
const decryptedContent = await db.vault.decryptContent(
content,
session.note.id,
password
);
setContent({
...content,
...decryptedContent,
locked: false
});
}}
/>
) : (
<>
<ContentToggle
label={
session.type === "diff" ? "Older version" : "Current note"
}
readonly={session.type === "diff"}
dateEdited={content.dateEdited}
isSelected={selectedContent === 0}
isOtherSelected={selectedContent === 1}
onToggle={() => setSelectedContent((s) => (s === 0 ? -1 : 0))}
resolveConflict={({ saveCopy }) => {
resolveConflict({
note: session.note,
toKeep: content.data,
toCopy: saveCopy ? conflictedContent : undefined,
toKeepDateEdited: content.dateEdited,
dateResolved: conflictedContent.dateModified
});
}}
sx={{
borderStyle: "solid",
borderWidth: 0,
borderBottomWidth: 1,
borderColor: "border",
px: 2,
pb: 1
}}
/>
<ScrollSyncPane>
<Flex
sx={{
px: 2,
overflowY: "auto",
flex: 1,
borderStyle: "solid",
borderWidth: 0,
borderRightWidth: [0, 0, 1],
borderBottomWidth: [1, 1, 0],
borderColor: "border"
}}
>
<Editor
id={content.id}
content={() => content.data}
nonce={0}
options={{ readonly: true, headless: true }}
/>
</Flex>
</ScrollSyncPane>
</>
)}
</Flex>
<Flex
className="secondEditor"
sx={{
flex: "1 1 auto",
flexDirection: "column",
width: ["100%", "100%", "50%"],
height: ["50%", "50%", "100%"],
borderLeft: conflictedContent.locked
? "1px solid var(--border)"
: "none"
}}
>
{conflictedContent.locked ? (
<UnlockView
title={getFormattedDate(conflictedContent.dateEdited)}
subtitle="Please enter the password to view this version"
buttonTitle="Unlock"
unlock={async (password) => {
const decryptedContent = await db.vault.decryptContent(
conflictedContent,
session.note.id,
password
);
setConflictedContent({
...conflictedContent,
...decryptedContent,
locked: false
});
}}
/>
) : (
<>
<ContentToggle
readonly={session.type === "diff"}
resolveConflict={({ saveCopy }) => {
resolveConflict({
note: session.note,
toKeep: conflictedContent.data,
toCopy: saveCopy ? content : undefined,
toKeepDateEdited: conflictedContent.dateEdited,
dateResolved: conflictedContent.dateModified
});
}}
label={
session.type === "diff"
? "Current version"
: "Incoming note"
}
isSelected={selectedContent === 1}
isOtherSelected={selectedContent === 0}
dateEdited={conflictedContent.dateEdited}
onToggle={() => setSelectedContent((s) => (s === 1 ? -1 : 1))}
sx={{
alignItems: "flex-end",
borderStyle: "solid",
borderWidth: 0,
borderBottomWidth: 1,
borderColor: "border",
px: 2,
pb: 1,
pt: [1, 1, 0]
}}
/>
<ScrollSyncPane>
<Flex sx={{ px: 2, overflow: "auto" }}>
<Editor
id={`${conflictedContent.id}-conflicted`}
content={() => conflictedContent.data}
nonce={0}
options={{ readonly: true, headless: true }}
/>
</Flex>
</ScrollSyncPane>
</>
)}
</Flex>
</Flex>
</ScrollSync>
</Flex>
);
}
export default DiffViewer;
async function resolveConflict({
note,
toKeep,
toCopy,
toKeepDateEdited,
dateResolved
}: {
note: Note;
toKeep: string;
toCopy?: ContentItem;
toKeepDateEdited: number;
dateResolved?: number;
}) {
await db.notes.add({
id: note.id,
dateEdited: toKeepDateEdited,
conflicted: false
});
await db.content.add({
id: note.contentId,
data: toKeep,
type: "tiptap",
dateResolved,
sessionId: `${Date.now()}`
});
if (toCopy) {
await createCopy(note, toCopy);
}
await notesStore.refresh();
useEditorStore.getState().openSession(note.id, true);
}
async function createCopy(note: Note, content: ContentItem) {
if (content.locked) {
const contentId = await db.content.add({
locked: true,
data: content.data,
type: content.type,
noteId: note.id
});
return await db.notes.add({
contentId,
title: note.title + " (COPY)"
});
} else {
return await db.notes.add({
content: {
type: "tiptap",
data: content.data
},
title: note.title + " (COPY)"
});
}
}

View File

@@ -0,0 +1,671 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Button, Flex, Text } from "@theme-ui/components";
import { useState } from "react";
import {
Cross,
ExitFullscreen,
FocusMode,
Fullscreen,
Lock,
NormalMode,
Note,
Pin,
Properties,
Search,
Unlock
} from "../icons";
import { ScrollContainer } from "@notesnook/ui";
import {
SessionType,
isLockedSession,
useEditorStore
} from "../../stores/editor-store";
import { Menu } from "../../hooks/use-menu";
import { useStore as useAppStore } from "../../stores/app-store";
import { useEditorManager, useSearch } from "./manager";
export function EditorActionBar() {
// const editorMargins = useEditorStore((store) => store.editorMargins);
const isFocusMode = useAppStore((store) => store.isFocusMode);
const [isFullscreen, setIsFullscreen] = useState(false);
const activeSession = useEditorStore((store) =>
store.activeSessionId ? store.getSession(store.activeSessionId) : undefined
);
const { toggleSearch } = useSearch();
const tools = [
// {
// title: editorMargins ? "Disable editor margins" : "Enable editor margins",
// icon: editorMargins ? EditorNormalWidth : EditorFullWidth,
// enabled: true,
// onClick: () => useEditorStore.getState().toggleEditorMargins()
// },
{
title: isFocusMode ? "Normal mode" : "Focus mode",
icon: isFocusMode ? FocusMode : NormalMode,
enabled: true,
hideOnMobile: true,
onClick: () => {
useAppStore.getState().toggleFocusMode();
if (document.fullscreenElement) exitFullscreen();
const id = useEditorStore.getState().activeSessionId;
const editor = id && useEditorManager.getState().getEditor(id);
if (editor) editor.editor?.focus();
}
},
{
title: isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
icon: isFullscreen ? ExitFullscreen : Fullscreen,
enabled: true,
hidden: !isFocusMode,
hideOnMobile: true,
onClick: () => {
if (isFullscreen) {
exitFullscreen();
} else {
enterFullscreen(document.documentElement);
}
setIsFullscreen((s) => !s);
}
},
{
title: "Search",
icon: Search,
enabled:
activeSession &&
activeSession.type !== "new" &&
activeSession.type !== "locked" &&
activeSession.type !== "readonly",
onClick: toggleSearch
},
{
title: "Properties",
icon: Properties,
enabled:
activeSession &&
activeSession.type !== "new" &&
activeSession.type !== "readonly" &&
activeSession.type !== "locked" &&
!isFocusMode,
onClick: () => useEditorStore.getState().toggleProperties()
}
];
return (
<Flex sx={{ mb: 2, gap: 2 }}>
<TabStrip />
<Flex
bg="background"
sx={{
borderRadius: "default",
overflow: "hidden",
alignItems: "center",
justifyContent: "flex-end",
mr: 2
}}
>
{tools.map((tool) => (
<Button
data-test-id={tool.title}
disabled={!tool.enabled}
variant="secondary"
title={tool.title}
key={tool.title}
sx={{
display: [
tool.hideOnMobile ? "none" : "flex",
tool.hidden ? "none" : "flex"
],
borderRadius: 0,
flexShrink: 0
}}
onClick={tool.onClick}
>
<tool.icon size={18} />
</Button>
))}
</Flex>
</Flex>
);
}
function TabStrip() {
const sessions = useEditorStore((store) => store.sessions);
const activeSessionId = useEditorStore((store) => store.activeSessionId);
return (
<ScrollContainer
className="tabsScroll"
suppressScrollY
style={{ flex: 1 }}
trackStyle={() => ({
backgroundColor: "transparent",
pointerEvents: "none"
})}
thumbStyle={() => ({ height: 3 })}
onWheel={(e) => {
const scrollcontainer = document.querySelector(".tabsScroll");
if (!scrollcontainer) return;
if (e.deltaY > 0) scrollcontainer.scrollLeft += 100;
else if (e.deltaY < 0) scrollcontainer.scrollLeft -= 100;
}}
>
<Flex
sx={{
flex: 1,
my: 1,
ml: 1,
gap: 1,
height: 32
}}
onDoubleClick={async (e) => {
e.stopPropagation();
useEditorStore.getState().newSession();
}}
>
{sessions.map((session, i) => (
<Tab
key={session.id}
title={
session.title ||
("note" in session ? session.note.title : "Untitled")
}
isTemporary={!!session.preview}
isActive={session.id === activeSessionId}
isPinned={!!session.pinned}
isLocked={isLockedSession(session)}
type={session.type}
index={i}
onKeepOpen={() =>
useEditorStore
.getState()
.updateSession(
session.id,
[session.type],
(s) => (s.preview = true)
)
}
onFocus={() => {
if (session.id !== activeSessionId) {
useEditorStore.getState().openSession(session.id);
}
}}
onMove={(from, to) => {
if (from === to) return;
useEditorStore.setState((state) => {
const direction =
to === 0 ? "start" : from > to ? "left" : "right";
const [fromTab] = state.sessions.splice(from, 1);
const newIndex =
direction === "start" || direction === "right" ? to : to - 1;
// unpin the tab if it is moved.
if (fromTab.pinned) fromTab.pinned = false;
// if the tab where this tab is being dropped is pinned,
// let's pin our tab too.
if (state.sessions[to].pinned) fromTab.pinned = true;
state.sessions.splice(newIndex, 0, fromTab);
});
}}
onClose={() => useEditorStore.getState().closeSessions(session.id)}
onCloseAll={() =>
useEditorStore
.getState()
.closeSessions(
...sessions.filter((s) => !s.pinned).map((s) => s.id)
)
}
onCloseOthers={() =>
useEditorStore
.getState()
.closeSessions(
...sessions
.filter((s) => s.id !== session.id && !s.pinned)
.map((s) => s.id)
)
}
onCloseToTheRight={() =>
useEditorStore
.getState()
.closeSessions(
...sessions
.filter((s, index) => index > i && !s.pinned)
.map((s) => s.id)
)
}
onCloseToTheLeft={() =>
useEditorStore
.getState()
.closeSessions(
...sessions
.filter((s, index) => index < i && !s.pinned)
.map((s) => s.id)
)
}
onPin={() => {
useEditorStore.setState((state) => {
let to = state.sessions.findLastIndex((a) => a.pinned);
if (to === -1) to = 0;
const [fromTab] = state.sessions.splice(i, 1);
fromTab.pinned = !fromTab.pinned;
// preview tabs can never be pinned.
if (fromTab.pinned) fromTab.preview = false;
state.sessions.splice(to + 1, 0, fromTab);
});
}}
/>
))}
</Flex>
</ScrollContainer>
);
}
const dragState: { element?: HTMLElement | null; index: number } = {
element: undefined,
index: -1
};
type TabProps = {
title: string;
index: number;
isActive: boolean;
isTemporary: boolean;
isPinned: boolean;
isLocked: boolean;
type: SessionType;
onKeepOpen: () => void;
onFocus: () => void;
onClose: () => void;
onCloseOthers: () => void;
onCloseToTheRight: () => void;
onCloseToTheLeft: () => void;
onCloseAll: () => void;
onPin: () => void;
onMove: (from: number, to: number) => void;
};
function Tab(props: TabProps) {
const {
title,
index,
isActive,
isTemporary,
isPinned,
isLocked,
type,
onKeepOpen,
onFocus,
onClose,
onCloseAll,
onCloseOthers,
onCloseToTheRight,
onCloseToTheLeft,
onMove,
onPin
} = props;
const [isDragOver, setIsDragOver] = useState(false);
const Icon = isLocked ? (type === "locked" ? Lock : Unlock) : Note;
return (
<Flex
className="tab"
sx={{
borderRadius: "default",
cursor: "pointer",
px: 2,
py: "7px",
bg: isDragOver
? "shade"
: isActive
? "background"
: "background-secondary",
// borderTopLeftRadius: "default",
// borderTopRightRadius: "default",
// borderBottom: isActive ? "none" : "1px solid var(--border)",
border: "1px solid",
borderColor: isActive ? "border" : "transparent",
justifyContent: "space-between",
alignItems: "center",
flexShrink: 0,
":hover": {
"& .closeTabButton": {
visibility: "visible"
},
bg: isActive ? "background" : "hover"
}
}}
onContextMenu={(e) => {
e.preventDefault();
Menu.openMenu([
{ type: "button", title: "Close", key: "close", onClick: onClose },
{
type: "button",
title: "Close others",
key: "close-others",
onClick: onCloseOthers
},
{
type: "button",
title: "Close to the right",
key: "close-to-the-right",
onClick: onCloseToTheRight
},
{
type: "button",
title: "Close to the left",
key: "close-to-the-left",
onClick: onCloseToTheLeft
},
{
type: "button",
title: "Close all",
key: "close-all",
onClick: onCloseAll
},
{ type: "separator", key: "sep" },
{
type: "button",
key: "keep-open",
title: "Keep open",
onClick: onKeepOpen,
isDisabled: !isTemporary
},
{
type: "button",
key: "pin",
title: "Pin",
onClick: onPin,
isChecked: isPinned,
icon: Pin.path
}
]);
}}
onDoubleClick={(e) => {
e.stopPropagation();
if (isTemporary) onKeepOpen();
}}
onClick={(e) => {
e.stopPropagation();
onFocus();
}}
draggable
onDragStart={(e) => {
if (!(e.target instanceof HTMLElement)) return;
onFocus();
e.target.style.cursor = "grabbing";
dragState.element = e.target;
dragState.index = index;
}}
onDragEnd={(e) => {
if (!(e.target instanceof HTMLElement)) return;
e.target.style.cursor = "pointer";
dragState.element = null;
dragState.index = -1;
}}
onDragOver={(e) => {
if (e.target === dragState.element) return;
e.preventDefault();
setIsDragOver(true);
}}
onDragEnter={(e) => {
if (e.target === dragState.element) return;
e.preventDefault();
setIsDragOver(true);
}}
onDragLeave={(e) => {
if (e.target === dragState.element) return;
setIsDragOver(false);
}}
onDrop={(e) => {
setIsDragOver(false);
onMove(dragState.index, index);
}}
>
<Flex mr={1}>
<Icon size={16} color={isActive ? "accent" : "icon"} />
<Text
variant="body"
sx={{
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflowX: "hidden",
pointerEvents: "none",
fontStyle: isTemporary ? "italic" : "normal",
maxWidth: 120
}}
ml={1}
>
{title}
</Text>
</Flex>
{isPinned ? (
<Pin
sx={{
":hover": { bg: "border" },
borderRadius: "default",
flexShrink: 0
}}
size={14}
onClick={(e) => {
e.stopPropagation();
onPin();
}}
/>
) : (
<Cross
sx={{
visibility: isActive ? "visible" : "hidden",
":hover": { bg: "border" },
borderRadius: "default",
flexShrink: 0
}}
onClick={(e) => {
e.stopPropagation();
onClose();
}}
className="closeTabButton"
size={16}
/>
)}
</Flex>
);
}
// import { useEffect, useMemo, useState } from "react";
// import { Button, Flex, Text } from "@theme-ui/components";
// import {
// Published,
// Publish,
// EditorNormalWidth,
// EditorFullWidth,
// ThemeIcon,
// FocusMode,
// NormalMode,
// ExitFullscreen,
// Fullscreen,
// Search,
// Undo,
// Redo,
// Properties,
// ArrowLeft
// } from "../icons";
// import { useStore as useThemeStore } from "../../stores/theme-store";
// import { useStore as useMonographStore } from "../../stores/monograph-store";
// import { useStore, store } from "../../stores/editor-store";
// import { showToast } from "../../utils/toast";
// import { AnimatedInput } from "../animated";
// import { showPublishView } from "../publish-view";
// import { db } from "../../common/db";
// import { useEditorInstance, useHistory, useSearch } from "./manager";
// import { AppEventManager, AppEvents } from "../../common/app-events";
// // TODO: this needs to be cleaned up!
// function Toolbar() {
// const sessionId = useStore((store) => store.session.id);
// const isDeleted = useStore((store) => store.session.isDeleted);
// const isLocked = useStore((store) => store.session.locked);
// const [isFullscreen, setIsFullscreen] = useState(false);
// const isFocusMode = useAppStore((store) => store.isFocusMode);
// const toggleFocusMode = useAppStore((store) => store.toggleFocusMode);
// const toggleProperties = useStore((store) => store.toggleProperties);
// const toggleEditorMargins = useStore((store) => store.toggleEditorMargins);
// const clearSession = useStore((store) => store.clearSession);
// const title = useStore((store) => store.session.title);
// const theme = useThemeStore((store) => store.colorScheme);
// const toggleNightMode = useThemeStore((store) => store.toggleColorScheme);
// const [isTitleVisible, setIsTitleVisible] = useState(false);
// const monographs = useMonographStore((store) => store.monographs);
// const { canRedo, canUndo, redo, undo } = useHistory();
// const { toggleSearch } = useSearch();
// const editor = useEditorInstance();
// const isNotePublished = useMemo(
// () => sessionId && db.monographs.isPublished(sessionId),
// // eslint-disable-next-line react-hooks/exhaustive-deps
// [sessionId, monographs]
// );
// useEffect(() => {
// const editorScroll = document.querySelector(".editorScroll");
// if (!editorScroll) return;
// function onScroll(e) {
// const hideOffset = document.querySelector(".editorTitle").scrollHeight;
// if (e.target.scrollTop > hideOffset && !isTitleVisible)
// setIsTitleVisible(e.target.scrollTop > hideOffset && !isTitleVisible);
// else if (e.target.scrollTop <= hideOffset && isTitleVisible)
// setIsTitleVisible(false);
// }
// editorScroll.addEventListener("scroll", onScroll);
// return () => {
// editorScroll.removeEventListener("scroll", onScroll);
// };
// }, [isTitleVisible]);
// const tools = useMemo(
// () => [
// {
// title: isNotePublished ? "Published" : "Publish",
// icon: isNotePublished ? Published : Publish,
// hidden: !sessionId || isDeleted,
// enabled: !isLocked,
// onClick: () => showPublishView(store.get().session.id, "top")
// }
// ],
// [sessionId, isLocked, isNotePublished, isDeleted]
// );
// return (
// <Flex mx={2} my={1} sx={{ justifyContent: "space-between" }}>
// <Flex sx={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
// <ArrowLeft
// sx={{
// display: ["block", "none", "none"],
// flexShrink: 0
// }}
// size={24}
// onClick={() => {
// if (store.get().session.id) showToast("success", "Note saved!");
// if (isFocusMode) toggleFocusMode();
// clearSession();
// }}
// />
// <AnimatedInput
// variant="clean"
// ml={[2, 2, 0]}
// initial={{
// opacity: isTitleVisible ? 1 : 0,
// zIndex: isTitleVisible ? 1 : -1
// }}
// animate={{
// opacity: isTitleVisible ? 1 : 0,
// zIndex: isTitleVisible ? 1 : -1
// }}
// transition={{ duration: 0.5 }}
// defaultValue={title}
// onChange={(e) => {
// AppEventManager.publish(AppEvents.changeNoteTitle, {
// title: e.target.value,
// preventSave: false
// });
// }}
// sx={{
// flex: 1,
// fontWeight: "heading",
// fontSize: "heading",
// color: "paragraph",
// p: 0,
// pl: 4,
// borderWidth: 0,
// borderRadius: "default",
// textOverflow: "ellipsis",
// whiteSpace: "nowrap",
// overflow: "hidden"
// }}
// />
// </Flex>
// <Flex sx={{ gap: 1 }}>
// {tools.map((tool) => (
// <Button
// key={tool.title}
// variant="secondary"
// data-test-id={tool.title}
// disabled={!tool.enabled}
// title={tool.title}
// sx={{
// display: [
// tool.hideOnMobile ? "none" : "flex",
// tool.hidden ? "none" : "flex"
// ],
// color: "paragraph",
// flexDirection: "row",
// flexShrink: 0,
// alignItems: "center"
// }}
// onClick={tool.onClick}
// >
// <tool.icon size={18} />
// <Text
// variant="body"
// ml={1}
// sx={{ display: ["none", "none", "block"] }}
// >
// {tool.title}
// </Text>
// </Button>
// ))}
// </Flex>
// </Flex>
// );
// }
// export default Toolbar;
function enterFullscreen(elem: HTMLElement) {
elem.requestFullscreen();
}
function exitFullscreen() {
if (!document.fullscreenElement) return;
document.exitFullscreen();
}

View File

@@ -1,160 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useCallback, useEffect, useRef } from "react";
import { IEditor, NoteStatistics } from "./types";
import createStore from "../../common/store";
import BaseStore from "../../stores";
import { shallow } from "zustand/shallow";
import type { ToolbarDefinition } from "@notesnook/editor";
import Config from "../../utils/config";
type EditorConfig = { fontFamily: string; fontSize: number };
type EditorSubState = {
editor?: IEditor;
canUndo?: boolean;
canRedo?: boolean;
searching?: boolean;
toolbarConfig?: ToolbarDefinition;
editorConfig: EditorConfig;
statistics?: NoteStatistics;
};
class EditorContext extends BaseStore<EditorContext> {
subState: EditorSubState = {
editorConfig: Config.get("editorConfig", {
fontFamily: "sans-serif",
fontSize: 16
})
};
configure = (
partial:
| Partial<EditorSubState>
| ((oldState: EditorSubState) => Partial<EditorSubState>)
) => {
this.set((state) => {
const newPartialState =
typeof partial === "function" ? partial(state.subState) : partial;
state.subState = { ...state.subState, ...newPartialState };
});
};
}
const [useEditorContext] = createStore<EditorContext>(
(set, get) => new EditorContext(set, get)
);
export function useEditorInstance() {
const editor = useEditorContext((store) => store.subState.editor);
const editorRef = useRef(editor);
useEffect(() => {
editorRef.current = editor;
}, [editor]);
return editorRef;
}
export function useConfigureEditor() {
return useEditorContext((store) => store.configure);
}
export const configureEditor = (
partial:
| Partial<EditorSubState>
| ((oldState: EditorSubState) => Partial<EditorSubState>)
) => useEditorContext.getState().configure(partial);
export function useHistory() {
return useEditorContext(
(store) =>
({
canUndo: store.subState.canUndo,
canRedo: store.subState.canRedo,
undo: store.subState.editor?.undo,
redo: store.subState.editor?.redo
} as const),
shallow
);
}
export function useSearch() {
const isSearching = useEditorContext((store) => store.subState.searching);
const configure = useEditorContext((store) => store.configure);
const toggleSearch = useCallback(
() => configure({ searching: !isSearching }),
[isSearching, configure]
);
return { isSearching, toggleSearch };
}
export function useToolbarConfig() {
const toolbarConfig = useEditorContext(
(store) => store.subState.toolbarConfig
);
const configure = useEditorContext((store) => store.configure);
const setToolbarConfig = useCallback(
(config: ToolbarDefinition) => configure({ toolbarConfig: config }),
[configure]
);
return { toolbarConfig, setToolbarConfig };
}
export function useNoteStatistics(): NoteStatistics {
return useEditorContext(
(store) =>
store.subState.statistics || {
words: { total: 0 }
}
);
}
export function useEditorConfig() {
const editorConfig = useEditorContext((store) => store.subState.editorConfig);
return { editorConfig, setEditorConfig };
}
export const editorConfig = () =>
useEditorContext.getState().subState.editorConfig;
export const setEditorConfig = (config: Partial<EditorConfig>) => {
const oldConfig = editorConfig();
if (oldConfig)
Config.set("editorConfig", {
...oldConfig,
...config
});
useEditorContext.getState().configure({
editorConfig: {
...oldConfig,
...config
}
});
};
export const onEditorConfigChange = (
selector: (editorConfig: EditorConfig) => any,
listener: (
selectedState: EditorConfig,
previousSelectedState: EditorConfig
) => void
) =>
useEditorContext.subscribe(
(s) => selector(s.subState.editorConfig),
listener
);

View File

@@ -18,11 +18,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Flex, Text } from "@theme-ui/components";
import { useMemo } from "react";
import { useStore } from "../../stores/editor-store";
import { SaveState, useEditorStore } from "../../stores/editor-store";
import { Loading, Saved, NotSaved } from "../icons";
import { useNoteStatistics } from "./context";
import { getFormattedDate } from "@notesnook/common";
import { useNoteStatistics } from "./manager";
const SAVE_STATE_ICON_MAP = {
"-1": NotSaved,
@@ -31,18 +29,14 @@ const SAVE_STATE_ICON_MAP = {
};
function EditorFooter() {
const { words } = useNoteStatistics();
const dateEdited = useStore((store) => store.session.dateEdited);
const id = useStore((store) => store.session.id);
const saveState = useStore(
(store) => store.session.saveState
) as keyof typeof SAVE_STATE_ICON_MAP;
const SaveStateIcon = useMemo(
() => SAVE_STATE_ICON_MAP[saveState],
[saveState]
const activeSessionId = useEditorStore((store) => store.activeSessionId);
const { words } = useNoteStatistics(activeSessionId || "unknown");
const saveState = useEditorStore(
(store) => store.getActiveSession(["default", "unlocked"])?.saveState
);
const SaveStateIcon = saveState ? SAVE_STATE_ICON_MAP[saveState] : null;
if (!id) return null;
if (!activeSessionId) return null;
return (
<Flex sx={{ alignItems: "center" }}>
<Text
@@ -55,23 +49,13 @@ function EditorFooter() {
{words.total + " words"}
{words.selected ? ` (${words.selected} selected)` : ""}
</Text>
<Text
className="selectable"
variant="subBody"
mr={2}
sx={{ color: "paragraph" }}
data-test-id="editor-date-edited"
title={dateEdited?.toString()}
>
{getFormattedDate(dateEdited || Date.now())}
</Text>
{SaveStateIcon && (
<SaveStateIcon
size={13}
color={
saveState === 1
saveState === SaveState.Saved
? "accent"
: saveState === "-1"
: saveState === SaveState.NotSaved
? "red"
: "paragraph"
}

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useCallback, useEffect, useRef } from "react";
import { useStore } from "../../stores/editor-store";
import { useEditorStore } from "../../stores/editor-store";
import { useStore as useTagStore } from "../../stores/tag-store";
import { useStore as useNoteStore } from "../../stores/note-store";
import { Input } from "@theme-ui/components";
@@ -30,26 +30,17 @@ import { useMenuTrigger } from "../../hooks/use-menu";
import { MenuItem } from "@notesnook/ui";
import { navigate } from "../../navigation";
import { Tag } from "@notesnook/core";
import { usePromise } from "@notesnook/common";
type HeaderProps = { readonly: boolean };
type HeaderProps = { readonly: boolean; id: string };
function Header(props: HeaderProps) {
const { readonly } = props;
const id = useStore((store) => store.session.id);
const tags = useStore((store) => store.tags);
const refreshTags = useStore((store) => store.refreshTags);
const { readonly, id } = props;
const tags = useEditorStore((store) => store.getSession(id)?.tags || []);
const refreshTags = useEditorStore((store) => store.refreshTags);
useEffect(() => {
if (!id) return;
refreshTags();
}, [id, refreshTags]);
const defaultTags = usePromise(() =>
db.tags.all
.limit(10)
.items(undefined, { sortBy: "dateCreated", sortDirection: "desc" })
);
const setTag = useCallback(
async function (noteId: string, tags: Tag[], value: string) {
const oldTag = tags.find((t) => t.title === value);
@@ -62,76 +53,76 @@ function Header(props: HeaderProps) {
{ type: "note", id: noteId }
);
await useTagStore.getState().refresh();
if (defaultTags.status === "fulfilled") defaultTags.refresh();
}
await refreshTags();
await useNoteStore.getState().refresh();
},
[refreshTags, defaultTags]
[refreshTags]
);
return (
<>
{id && (
<Flex
sx={{ lineHeight: 2.5, alignItems: "center", flexWrap: "wrap" }}
data-test-id="tags"
>
{tags?.map((tag) => (
<IconTag
testId={`tag`}
key={tag.id}
text={tag.title}
icon={TagIcon}
onClick={() => navigate(`/tags/${tag.id}`)}
onDismiss={
readonly ? undefined : () => setTag(id, tags, tag.title)
<Flex
sx={{ lineHeight: 2.5, alignItems: "center", flexWrap: "wrap" }}
data-test-id="tags"
>
{tags?.map((tag) => (
<IconTag
testId={`tag`}
key={tag.id}
text={tag.title}
icon={TagIcon}
onClick={() => navigate(`/tags/${tag.id}`)}
onDismiss={readonly ? undefined : () => setTag(id, tags, tag.title)}
styles={{ container: { mr: 1 }, text: { fontSize: "body" } }}
/>
))}
{!readonly && tags ? (
<Autosuggest
sessionId={id}
filter={(query) => db.lookup.tags(query).items(10)}
toMenuItems={(filtered, reset, query) => {
const items: MenuItem[] = [];
const isExactMatch =
!!query && filtered.some((item) => item.title === query);
if (query && !isExactMatch) {
items.push({
type: "button",
key: "new",
title: `Create "${query}" tag`,
icon: Plus.path,
onClick: () => setTag(id, tags, query).finally(reset)
});
}
styles={{ container: { mr: 1 }, text: { fontSize: "body" } }}
/>
))}
{!readonly && tags && defaultTags.status === "fulfilled" ? (
<Autosuggest
sessionId={id}
filter={(query) => db.lookup.tags(query).items(10)}
toMenuItems={(filtered, reset, query) => {
const items: MenuItem[] = [];
const isExactMatch =
!!query && filtered.some((item) => item.title === query);
if (query && !isExactMatch) {
items.push({
type: "button",
key: "new",
title: `Create "${query}" tag`,
icon: Plus.path,
onClick: () => setTag(id, tags, query).finally(reset)
});
}
if (filtered.length > 0) {
items.push(
...filtered.map((item) => ({
type: "button" as const,
key: item.id,
title: item.title,
icon: TagIcon.path,
onClick: () => setTag(id, tags, item.title).finally(reset)
}))
);
}
if (filtered.length > 0) {
items.push(
...filtered.map((item) => ({
type: "button" as const,
key: item.id,
title: item.title,
icon: TagIcon.path,
onClick: () => setTag(id, tags, item.title).finally(reset)
}))
);
}
return items;
}}
onAdd={(value) => setTag(id, tags, value)}
onRemove={() => {
if (tags.length <= 0) return;
setTag(id, tags, tags[tags.length - 1].title);
}}
defaultItems={defaultTags.value}
/>
) : null}
</Flex>
)}
return items;
}}
onAdd={(value) => setTag(id, tags, value)}
onRemove={() => {
if (tags.length <= 0) return;
setTag(id, tags, tags[tags.length - 1].title);
}}
defaultItems={() =>
db.tags.all.limit(10).items(undefined, {
sortBy: "dateCreated",
sortDirection: "desc"
})
}
/>
) : null}
</Flex>
</>
);
}
@@ -143,7 +134,7 @@ type AutosuggestProps<T> = {
onRemove: () => void;
onAdd: (text: string) => void;
toMenuItems: (filtered: T[], reset: () => void, query?: string) => MenuItem[];
defaultItems: T[];
defaultItems: () => Promise<T[]>;
};
export function Autosuggest<T>(props: AutosuggestProps<T>) {
const { sessionId, filter, onRemove, onAdd, defaultItems, toMenuItems } =
@@ -207,14 +198,14 @@ export function Autosuggest<T>(props: AutosuggestProps<T>) {
}}
placeholder="Add a tag..."
data-test-id="editor-tag-input"
onFocus={() => {
onFocus={async () => {
const text = getInputValue();
if (!text) onOpenMenu(defaultItems);
if (!text) onOpenMenu(await defaultItems());
else closeMenu();
}}
onClick={() => {
onClick={async () => {
const text = getInputValue();
if (!text) onOpenMenu(defaultItems);
if (!text) onOpenMenu(await defaultItems());
else closeMenu();
}}
onChange={async (e) => {
@@ -225,7 +216,7 @@ export function Autosuggest<T>(props: AutosuggestProps<T>) {
}
onOpenMenu(await filter(value));
}}
onKeyDown={(e) => {
onKeyDown={async (e) => {
const text = getInputValue();
if (e.key === "Enter" && !!text && isOpen && !arrowDown.current) {
onAdd(text);
@@ -239,7 +230,7 @@ export function Autosuggest<T>(props: AutosuggestProps<T>) {
e.stopPropagation();
} else if (e.key === "ArrowUp" || e.key === "ArrowDown") {
arrowDown.current = true;
if (e.key === "ArrowDown" && !text) onOpenMenu(defaultItems);
if (e.key === "ArrowDown" && !text) onOpenMenu(await defaultItems());
e.preventDefault();
} else if (e.key === "Tab") {

View File

@@ -19,28 +19,26 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, {
useEffect,
useCallback,
useState,
useRef,
PropsWithChildren,
Suspense
} from "react";
import ReactDOM from "react-dom";
import { Box, Button, Flex, Progress, Text } from "@theme-ui/components";
import { Box, Flex, Progress, Text } from "@theme-ui/components";
import Properties from "../properties";
import { useStore, store as editorstore } from "../../stores/editor-store";
import { useEditorStore, SaveState } from "../../stores/editor-store";
import {
useStore as useAppStore,
store as appstore
} from "../../stores/app-store";
import Toolbar from "./toolbar";
import { AppEventManager, AppEvents } from "../../common/app-events";
import { FlexScrollContainer } from "../scroll-container";
import Tiptap from "./tiptap";
import Tiptap, { OnChangeHandler } from "./tiptap";
import Header from "./header";
import { Attachment } from "../icons";
import { useEditorInstance } from "./context";
import { attachFiles, AttachmentProgress, insertAttachments } from "./picker";
import { useEditorManager } from "./manager";
import { saveAttachment, downloadAttachment } from "../../common/attachments";
import { EV, EVENTS } from "@notesnook/core/dist/common";
import { db } from "../../common/db";
@@ -48,20 +46,22 @@ import useMobile from "../../hooks/use-mobile";
import Titlebox from "./title-box";
import useTablet from "../../hooks/use-tablet";
import Config from "../../utils/config";
import { AnimatedFlex } from "../animated";
import { EditorLoader } from "../loaders/editor-loader";
import { ScopedThemeProvider } from "../theme-provider";
import { Lightbox } from "../lightbox";
import { Allotment } from "allotment";
import { showToast } from "../../utils/toast";
import { debounce, getFormattedDate } from "@notesnook/common";
import {
ContentType,
Item,
MaybeDeletedItem,
isDeleted
} from "@notesnook/core/dist/types";
import { debounce, debounceWithId } from "@notesnook/common";
import { PreviewSession } from "./types";
import { Freeze } from "react-freeze";
import { EditorActionBar } from "./action-bar";
import { UnlockView } from "../unlock";
import DiffViewer from "../diff-viewer";
const PDFPreview = React.lazy(() => import("../pdf-preview"));
@@ -70,45 +70,87 @@ type DocumentPreview = {
hash: string;
};
function onEditorChange(
noteId: string | undefined,
function saveContent(
noteId: string,
sessionId: string,
content: string,
ignoreEdit: boolean
ignoreEdit: boolean,
content: () => string
) {
if (!content) return;
editorstore.get().saveSessionContent(noteId, sessionId, ignoreEdit, {
console.log("SAVE to note");
useEditorStore.getState().saveSessionContent(noteId, sessionId, ignoreEdit, {
type: "tiptap",
data: content
data: content()
});
}
const deferredSave = debounceWithId(saveContent, 100);
export default function EditorManager({
noteId,
nonce
}: {
noteId?: string | number;
nonce?: string;
}) {
const isNewSession = !!nonce && !noteId;
const isOldSession = !nonce && !!noteId;
export default function TabsView() {
const sessions = useEditorStore((store) => store.sessions);
const activeSessionId = useEditorStore((store) => store.activeSessionId);
// the only state that changes. Everything else is
// stored in refs. Update this value to trigger an
// update.
const [timestamp, setTimestamp] = useState<number>(0);
useEffect(() => {
if (!activeSessionId) return;
// if the session isn't yet rendered, do nothing.
const activeSession = useEditorStore.getState().getSession(activeSessionId);
if (!activeSession?.needsHydration)
useEditorManager.getState().editors[activeSessionId]?.editor?.focus();
}, [activeSessionId]);
console.log("TabsView", sessions, activeSessionId);
return (
<>
<EditorActionBar />
{sessions.map((session) => (
<Freeze key={session.id} freeze={session.id !== activeSessionId}>
{session.needsHydration ? null : session.type === "locked" ? (
<UnlockView
buttonTitle="Open note"
subtitle="Please enter the password to unlock this note."
title={session.note.title}
unlock={async (password) => {
const note = await db.vault.open(session.id, password);
if (!note) throw new Error("note with this id does not exist.");
const lastSavedTime = useRef<number>(0);
useEditorStore.getState().addSession({
type: "default",
id: session.id,
note: session.note,
saveState: SaveState.Saved,
sessionId: `${Date.now()}`,
pinned: session.pinned,
preview: session.preview,
content: note.content
});
}}
/>
) : session.type === "conflicted" || session.type === "diff" ? (
<DiffViewer session={session} />
) : (
<MemoizedEditorView id={session.id} />
)}
</Freeze>
))}
</>
);
}
const MemoizedEditorView = React.memo(
EditorView,
(prev, next) => prev.id === next.id
);
function EditorView({ id }: { id: string }) {
const lastSavedTime = useRef<number>(Date.now());
const [docPreview, setDocPreview] = useState<DocumentPreview>();
const previewSession = useRef<PreviewSession>();
const [dropRef, overlayRef] = useDragOverlay();
const editorInstance = useEditorInstance();
const arePropertiesVisible = useStore((store) => store.arePropertiesVisible);
const toggleProperties = useStore((store) => store.toggleProperties);
const isReadonly = useStore((store) => store.session.readonly);
const arePropertiesVisible = useEditorStore(
(store) => store.arePropertiesVisible
);
const toggleProperties = useEditorStore((store) => store.toggleProperties);
const isReadonly = useEditorStore(
(store) => store.getSession(id, ["default"])?.note?.readonly
);
const isFocusMode = useAppStore((store) => store.isFocusMode);
const isPreviewSession = !!previewSession.current;
@@ -119,7 +161,13 @@ export default function EditorManager({
const event = db.eventManager.subscribe(
EVENTS.syncItemMerged,
async (item?: MaybeDeletedItem<Item>) => {
const session = useEditorStore
.getState()
.getSession(id, ["unlocked", "default"]);
const editor = useEditorManager.getState().getEditor(id)?.editor;
if (
!editor ||
!session?.note ||
!item ||
isDeleted(item) ||
(item.type !== "tiptap" && item.type !== "note") ||
@@ -129,11 +177,11 @@ export default function EditorManager({
)
return;
const { id, contentId, locked } = editorstore.get().session;
const { contentId, locked } = session.note;
const isContent = item.type === "tiptap" && item.id === contentId;
const isNote = item.type === "note" && item.id === id;
if (id && isContent && editorInstance.current) {
if (id && isContent) {
let content: string | null = null;
if (locked && item.locked) {
const result = await db.vault
@@ -142,11 +190,13 @@ export default function EditorManager({
if (result) content = result.data;
else EV.publish(EVENTS.vaultLocked);
}
editorInstance.current.updateContent(item.data as string);
editor.updateContent(item.data as string);
} else if (isNote) {
if (!locked && item.locked) return EV.publish(EVENTS.vaultLocked);
editorstore.get().updateSession(item);
useEditorStore
.getState()
.updateSession(id, ["default"], { note: item });
if (item.title)
AppEventManager.publish(AppEvents.changeNoteTitle, {
title: item.title,
@@ -158,32 +208,30 @@ export default function EditorManager({
return () => {
event.unsubscribe();
};
}, [editorInstance, isPreviewSession]);
}, [id, isPreviewSession]);
const openSession = useCallback(async (noteId: string) => {
await editorstore.get().openSession(noteId);
previewSession.current = undefined;
// const openSession = useCallback(async (noteId: string) => {
// await useEditorStore.getState().openSession(noteId);
// previewSession.current = undefined;
lastSavedTime.current = Date.now();
setTimestamp(Date.now());
}, []);
// lastSavedTime.current = Date.now();
// setTimestamp(Date.now());
// }, []);
useEffect(() => {
if (!isNewSession) return;
// useEffect(() => {
// if (!isNewSession) return;
(async function () {
await editorstore.newSession(nonce);
// editorstore.newSession();
lastSavedTime.current = 0;
setTimestamp(Date.now());
})();
}, [isNewSession, nonce]);
// lastSavedTime.current = 0;
// setTimestamp(Date.now());
// }, [isNewSession, nonce]);
useEffect(() => {
if (!isOldSession || typeof noteId === "number") return;
// useEffect(() => {
// if (!isOldSession || typeof noteId === "number") return;
openSession(noteId);
}, [noteId]);
// openSession(noteId);
// }, [noteId]);
return (
<ScopedThemeProvider scope="editor" sx={{ flex: 1 }}>
@@ -206,23 +254,32 @@ export default function EditorManager({
background: "background"
}}
>
{previewSession.current && noteId && (
{/* {previewSession.current && (
<PreviewModeNotice
{...previewSession.current}
onDiscard={() =>
typeof noteId === "string" && openSession(noteId)
}
/>
)}
)} */}
<Editor
id={noteId}
nonce={timestamp}
id={id}
nonce={1}
content={() =>
previewSession.current?.content.data ||
editorstore.get().session?.content?.data
useEditorStore.getState().getSession(id, ["default"])?.content
?.data
}
onPreviewDocument={(url) => setDocPreview(url)}
onContentChange={() => (lastSavedTime.current = Date.now())}
onSave={(content, ignoreEdit) => {
const session = useEditorStore
.getState()
.getSession(id, ["default"]);
console.log("ON SAVE", session);
if (!session) return;
deferredSave(id, id, session.sessionId, ignoreEdit, content);
}}
options={{
readonly: isReadonly || isPreviewSession,
onRequestFocus: () => toggleProperties(false),
@@ -231,15 +288,8 @@ export default function EditorManager({
}}
/>
{arePropertiesVisible && (
<Properties
onOpenPreviewSession={async (session: PreviewSession) => {
previewSession.current = session;
setTimestamp(Date.now());
}}
/>
)}
<DropZone overlayRef={overlayRef} />
{arePropertiesVisible && <Properties id={id} />}
<DropZone id={id} overlayRef={overlayRef} />
</Flex>
</Allotment.Pane>
{docPreview && (
@@ -331,16 +381,24 @@ type EditorOptions = {
onRequestFocus?: () => void;
};
type EditorProps = {
id?: string | number;
id: string;
content: () => string | undefined;
nonce?: number;
options?: EditorOptions;
onContentChange?: () => void;
onSave?: OnChangeHandler;
onPreviewDocument?: (preview: DocumentPreview) => void;
};
export function Editor(props: EditorProps) {
const { id, content, nonce, options, onContentChange, onPreviewDocument } =
props;
const {
id,
content,
onSave,
nonce,
options,
onContentChange,
onPreviewDocument
} = props;
const { readonly, headless, isMobile } = options || {
headless: false,
readonly: false,
@@ -349,13 +407,12 @@ export function Editor(props: EditorProps) {
};
const [isLoading, setIsLoading] = useState(true);
const editor = useEditorInstance();
useEffect(() => {
const event = AppEventManager.subscribe(
AppEvents.UPDATE_ATTACHMENT_PROGRESS,
({ hash, loaded, total }: AttachmentProgress) => {
editor.current?.sendAttachmentProgress(
const editor = useEditorManager.getState().getEditor(id)?.editor;
editor?.sendAttachmentProgress(
hash,
Math.round((loaded / total) * 100)
);
@@ -365,24 +422,28 @@ export function Editor(props: EditorProps) {
return () => {
event.unsubscribe();
};
}, [editor]);
}, []);
return (
<EditorChrome isLoading={isLoading} {...props}>
<Tiptap
id={id}
isMobile={isMobile}
nonce={nonce}
readonly={readonly}
toolbarContainerId={headless ? undefined : "editorToolbar"}
content={content}
downloadOptions={{
corsHost: Config.get("corsProxy", "https://cors.notesnook.com")
}}
onLoad={() => {
if (nonce && nonce > 0) setIsLoading(false);
restoreSelection(id);
setIsLoading(false);
}}
onSelectionChange={({ from, to }) =>
Config.set(`${id}:selection`, { from, to })
}
onContentChange={onContentChange}
onChange={onEditorChange}
onChange={onSave}
onDownloadAttachment={(attachment) => saveAttachment(attachment.hash)}
onPreviewAttachment={async (data) => {
const { hash } = data;
@@ -418,11 +479,12 @@ export function Editor(props: EditorProps) {
}
}}
onInsertAttachment={async (type) => {
const editor = useEditorManager.getState().getEditor(id)?.editor;
const mime = type === "file" ? "*/*" : "image/*";
const attachments = await insertAttachments(mime);
if (!attachments) return;
for (const attachment of attachments) {
editor.current?.attachFile(attachment);
editor?.attachFile(attachment);
}
}}
onGetAttachmentData={(attachment) => {
@@ -433,13 +495,19 @@ export function Editor(props: EditorProps) {
);
}}
onAttachFiles={async (files) => {
const editor = useEditorManager.getState().getEditor(id)?.editor;
const result = await attachFiles(files);
if (!result) return;
result.forEach((attachment) =>
editor.current?.attachFile(attachment)
);
result.forEach((attachment) => editor?.attachFile(attachment));
}}
/>
>
{headless ? null : (
<>
<Titlebox id={id} readonly={readonly || false} />
<Header id={id} readonly={readonly || false} />
</>
)}
</Tiptap>
</EditorChrome>
);
}
@@ -447,17 +515,16 @@ export function Editor(props: EditorProps) {
function EditorChrome(
props: PropsWithChildren<EditorProps & { isLoading: boolean }>
) {
const { options, children, isLoading } = props;
const { readonly, focusMode, headless, onRequestFocus, isMobile } =
options || {
headless: false,
readonly: false,
focusMode: false,
isMobile: false
};
const editorMargins = useStore((store) => store.editorMargins);
const { id, options, children } = props;
const { focusMode, headless, onRequestFocus, isMobile } = options || {
headless: false,
readonly: false,
focusMode: false,
isMobile: false
};
const editorMargins = useEditorStore((store) => store.editorMargins);
const editorContainerRef = useRef<HTMLElement>(null);
const editorScrollRef = useRef<HTMLElement>(null);
const editorScrollRef = useRef<HTMLElement>();
useEffect(() => {
if (!editorScrollRef.current) return;
@@ -493,7 +560,7 @@ function EditorChrome(
return (
<>
{isLoading ? (
{/* {isLoading ? (
<AnimatedFlex
sx={{
position: "absolute",
@@ -508,13 +575,27 @@ function EditorChrome(
>
<EditorLoader />
</AnimatedFlex>
) : null}
) : null} */}
<Toolbar />
{/* <Toolbar /> */}
<FlexScrollContainer
scrollRef={editorScrollRef}
className="editorScroll"
style={{ display: "flex", flexDirection: "column", flex: 1 }}
id={`${id}_editorScroll`}
scrollRef={(ref) => {
editorScrollRef.current = ref || undefined;
restoreScrollPosition(id);
}}
style={{
display: "flex",
flexDirection: "column",
flex: 1
// minHeight: Config.get(`${id}:scroll-position`, 0) + 100
}}
onScroll={debounce((e) => {
if (e.target instanceof HTMLElement) {
const scrollTop = e.target.scrollTop;
Config.set(`${id}:scroll-position`, scrollTop);
}
}, 500)}
>
<Flex
ref={editorContainerRef}
@@ -529,34 +610,35 @@ function EditorChrome(
pr={6}
onClick={onRequestFocus}
>
{!isMobile && (
<Box
id="editorToolbar"
sx={{
display: readonly ? "none" : "flex",
bg: "background",
position: "sticky",
top: 0,
mb: 1,
zIndex: 2
}}
/>
)}
<Titlebox readonly={readonly || false} />
<Header readonly={readonly || false} />
<AnimatedFlex
{/* {!isMobile && (
)} */}
{/* <Box
id={`${id}_toolbar`}
sx={{
minHeight: 34,
display: readonly ? "none" : "flex",
bg: "background",
position: "sticky",
top: 0,
mb: 1,
zIndex: 2
}}
/> */}
{/* <AnimatedFlex
initial={{ opacity: 0 }}
animate={{ opacity: isLoading ? 0 : 1 }}
transition={{ duration: 0.3, ease: "easeInOut" }}
sx={{ flex: 1 }}
>
{children}
</AnimatedFlex>
> */}
{children}
{/* </AnimatedFlex> */}
</Flex>
</FlexScrollContainer>
{isMobile && (
<Box
id="editorToolbar"
id={`${id}_toolbar`}
sx={{
display: "flex",
bg: "background",
@@ -572,68 +654,68 @@ function EditorChrome(
);
}
type PreviewModeNoticeProps = PreviewSession & {
onDiscard: () => void;
};
function PreviewModeNotice(props: PreviewModeNoticeProps) {
const { dateCreated, dateEdited, content, onDiscard } = props;
const disablePreviewMode = useCallback(
async (cancelled: boolean) => {
const { id, sessionId } = editorstore.get().session;
if (!cancelled) {
await editorstore.saveSessionContent(id, sessionId, false, content);
}
onDiscard();
},
[onDiscard, content]
);
// type PreviewModeNoticeProps = PreviewSession & {
// onDiscard: () => void;
// };
// function PreviewModeNotice(props: PreviewModeNoticeProps) {
// const { dateCreated, dateEdited, content, onDiscard } = props;
// const disablePreviewMode = useCallback(
// async (cancelled: boolean) => {
// const { id, sessionId } = useEditorStore.getState().session;
// if (!cancelled) {
// await editorstore.saveSessionContent(id, sessionId, content);
// }
// onDiscard();
// },
// [onDiscard, content]
// );
return (
<Flex
bg="var(--background-secondary)"
p={2}
sx={{ alignItems: "center", justifyContent: "space-between" }}
data-test-id="preview-notice"
>
<Flex mr={4} sx={{ flexDirection: "column" }}>
<Text variant={"subtitle"}>Preview</Text>
<Text variant={"body"}>
You are previewing note version edited from{" "}
{getFormattedDate(dateCreated, "date-time")} to{" "}
{getFormattedDate(dateEdited, "date-time")}.
</Text>
</Flex>
<Flex>
<Button
data-test-id="preview-notice-cancel"
variant={"secondary"}
mr={1}
px={4}
onClick={() => disablePreviewMode(true)}
>
Cancel
</Button>
<Button
variant="accent"
data-test-id="preview-notice-restore"
px={4}
onClick={async () => {
await disablePreviewMode(false);
}}
>
Restore
</Button>
</Flex>
</Flex>
);
}
// return (
// <Flex
// bg="var(--background-secondary)"
// p={2}
// sx={{ alignItems: "center", justifyContent: "space-between" }}
// data-test-id="preview-notice"
// >
// <Flex mr={4} sx={{ flexDirection: "column" }}>
// <Text variant={"subtitle"}>Preview</Text>
// <Text variant={"body"}>
// You are previewing note version edited from{" "}
// {getFormattedDate(dateCreated, "date-time")} to{" "}
// {getFormattedDate(dateEdited, "date-time")}.
// </Text>
// </Flex>
// <Flex>
// <Button
// data-test-id="preview-notice-cancel"
// variant={"secondary"}
// mr={1}
// px={4}
// onClick={() => disablePreviewMode(true)}
// >
// Cancel
// </Button>
// <Button
// variant="accent"
// data-test-id="preview-notice-restore"
// px={4}
// onClick={async () => {
// await disablePreviewMode(false);
// }}
// >
// Restore
// </Button>
// </Flex>
// </Flex>
// );
// }
type DropZoneProps = {
id: string;
overlayRef: React.MutableRefObject<HTMLElement | undefined>;
};
function DropZone(props: DropZoneProps) {
const { overlayRef } = props;
const editor = useEditorInstance();
const { overlayRef, id } = props;
return (
<Box
@@ -650,12 +732,14 @@ function DropZone(props: DropZoneProps) {
display: "none"
}}
onDrop={async (e) => {
const editor = useEditorManager.getState().getEditor(id)?.editor;
if (!editor || !e.dataTransfer.files?.length) return;
e.preventDefault();
(await attachFiles(Array.from(e.dataTransfer.files)))?.forEach(
(attachment) => editor.current?.attachFile(attachment)
);
const attachments = await attachFiles(Array.from(e.dataTransfer.files));
for (const attachment of attachments || []) {
editor.attachFile(attachment);
}
}}
>
<Flex
@@ -728,3 +812,22 @@ function isFile(e: DragEvent) {
e.dataTransfer.types?.some((a) => a === "Files"))
);
}
function restoreScrollPosition(id: string) {
const scrollContainer = document.getElementById(`${id}_editorScroll`);
const scrollPosition = Config.get(`${id}:scroll-position`, 0);
if (scrollContainer) {
requestAnimationFrame(() => {
if (scrollContainer.scrollHeight < scrollPosition)
scrollContainer.style.minHeight = `${scrollPosition + 100}px`;
scrollContainer.scrollTop = scrollPosition;
});
}
}
function restoreSelection(id: string) {
const editor = useEditorManager.getState().getEditor(id)?.editor;
editor?.focus({
position: Config.get(`${id}:selection`, { from: 0, to: 0 })
});
}

View File

@@ -0,0 +1,159 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useCallback } from "react";
import { IEditor, NoteStatistics } from "./types";
import createStore from "../../common/store";
import BaseStore from "../../stores";
import type { ToolbarDefinition } from "@notesnook/editor";
import Config from "../../utils/config";
import { getCurrentPreset } from "../../common/toolbar-config";
type EditorConfig = { fontFamily: string; fontSize: number };
type EditorState = {
selection?: { from: number; to: number };
scrollPosition?: number;
};
type EditorContext = {
editor?: IEditor;
canUndo?: boolean;
canRedo?: boolean;
statistics?: NoteStatistics;
};
class EditorManager extends BaseStore<EditorManager> {
toolbarConfig?: ToolbarDefinition;
editorConfig: EditorConfig = Config.get("editorConfig", {
fontFamily: "sans-serif",
fontSize: 16
});
searching?: boolean;
editors: Record<string, EditorContext> = {};
getEditor = (id: string): EditorContext | undefined => {
return this.get().editors[id];
};
setEditor = (id: string, editor?: EditorContext) => {
this.set((state) => {
if (!editor) delete state.editors[id];
else state.editors[id] = editor;
});
};
updateEditor = (
id: string,
partial:
| Partial<EditorContext>
| ((oldState: EditorContext) => Partial<EditorContext>)
) => {
this.set((state) => {
const newPartialState =
typeof partial === "function" ? partial(state.editors[id]) : partial;
state.editors[id] = { ...state.editors[id], ...newPartialState };
});
};
setEditorConfig = (config: Partial<EditorConfig>) => {
const oldConfig = this.get().editorConfig;
this.set({
editorConfig: Config.set("editorConfig", {
...oldConfig,
...config
})
});
};
}
const [useEditorManager] = createStore<EditorManager>(
(set, get) => new EditorManager(set, get)
);
export { useEditorManager };
// export function useEditorInstance(id: string) {
// const editor = useEditorContext((store) => store.subState.editors[id]);
// const editorRef = useRef(editor);
// useEffect(() => {
// editorRef.current = editor;
// }, [editor]);
// return editorRef;
// }
// export const editorInstance = (id: string) =>
// useEditorManager.getState().editors[id];
// export function useConfigureEditor() {
// return useEditorContext((store) => store.configure);
// }
// export const configureEditor = (
// partial:
// | Partial<EditorSubState>
// | ((oldState: EditorSubState) => Partial<EditorSubState>)
// ) => useEditorContext.getState().configure(partial);
export function useEditor(id: string) {
return useEditorManager((store) => store.editors[id]);
}
export function useSearch() {
const isSearching = useEditorManager((store) => store.searching);
const toggleSearch = useCallback(
() => useEditorManager.setState({ searching: !isSearching }),
[isSearching]
);
return { isSearching, toggleSearch };
}
export function useToolbarConfig() {
const toolbarConfig =
useEditorManager((store) => store.toolbarConfig) ||
getCurrentPreset().tools;
const setToolbarConfig = useCallback(
(config: ToolbarDefinition) =>
useEditorManager.setState({ toolbarConfig: config }),
[]
);
return { toolbarConfig, setToolbarConfig };
}
export function useNoteStatistics(id: string): NoteStatistics {
return useEditorManager(
(store) =>
store.editors[id]?.statistics || {
words: { total: 0 }
}
);
}
export function useEditorConfig() {
const editorConfig = useEditorManager((store) => store.editorConfig);
const setEditorConfig = useEditorManager((store) => store.setEditorConfig);
return { editorConfig, setEditorConfig };
}
export const editorConfig = () => useEditorManager.getState().editorConfig;
export const onEditorConfigChange = (
selector: (editorConfig: EditorConfig) => any,
listener: (
selectedState: EditorConfig,
previousSelectedState: EditorConfig
) => void
) => useEditorManager.subscribe((s) => selector(s.editorConfig), listener);

View File

@@ -38,37 +38,41 @@ import {
Attachment
} from "@notesnook/editor";
import { Box, Flex } from "@theme-ui/components";
import { PropsWithChildren, useEffect, useMemo, useRef, useState } from "react";
import { useState } from "react";
import {
PropsWithChildren,
useEffect,
useLayoutEffect,
useMemo,
useRef
} from "react";
import { IEditor } from "./types";
import {
useConfigureEditor,
useSearch,
useEditorConfig,
useToolbarConfig,
configureEditor,
useEditorConfig
} from "./context";
useEditorManager
} from "./manager";
import { createPortal } from "react-dom";
import { getCurrentPreset } from "../../common/toolbar-config";
import { useIsUserPremium } from "../../hooks/use-is-user-premium";
import { showBuyDialog } from "../../common/dialog-controller";
import { useStore as useSettingsStore } from "../../stores/setting-store";
import { debounce, debounceWithId } from "@notesnook/common";
import { store as editorstore } from "../../stores/editor-store";
import { debounce } from "@notesnook/common";
import { ScopedThemeProvider } from "../theme-provider";
import { writeText } from "clipboard-polyfill";
import { useStore as useThemeStore } from "../../stores/theme-store";
type OnChangeHandler = (
id: string | undefined,
sessionId: string,
content: string,
export type OnChangeHandler = (
content: () => string,
ignoreEdit: boolean
) => void;
type TipTapProps = {
editorContainer: HTMLElement;
id: string;
editorContainer: () => HTMLElement | undefined;
onLoad?: () => void;
onChange?: OnChangeHandler;
onContentChange?: () => void;
onSelectionChange?: (range: { from: number; to: number }) => void;
onInsertAttachment?: (type: AttachmentType) => void;
onDownloadAttachment?: (attachment: Attachment) => void;
onPreviewAttachment?: (attachment: Attachment) => void;
@@ -76,7 +80,6 @@ type TipTapProps = {
onAttachFiles?: (files: File[]) => void;
onFocus?: () => void;
content?: () => string | undefined;
toolbarContainerId?: string;
readonly?: boolean;
nonce?: number;
isMobile?: boolean;
@@ -85,35 +88,24 @@ type TipTapProps = {
fontFamily: string;
};
const SAVE_INTERVAL = IS_TESTING ? 100 : 300;
function save(
sessionId: string,
noteId: string | undefined,
editor: Editor,
content: Fragment,
preventSave: boolean,
ignoreEdit: boolean,
onChange?: OnChangeHandler
) {
configureEditor({
function updateWordCount(id: string, content: () => Fragment) {
const fragment = content();
useEditorManager.getState().updateEditor(id, {
statistics: {
words: {
total: countWords(content.textBetween(0, content.size, "\n", " ")),
total: countWords(fragment.textBetween(0, fragment.size, "\n", " ")),
selected: 0
}
}
});
if (preventSave) return;
const html = getHTMLFromFragment(content, editor.schema);
onChange?.(noteId, sessionId, html, ignoreEdit);
}
const deferredSave = debounceWithId(save, SAVE_INTERVAL);
const deferredUpdateWordCount = debounce(updateWordCount, 1000);
function TipTap(props: TipTapProps) {
const {
id,
onSelectionChange,
onLoad,
onChange,
onInsertAttachment,
@@ -124,7 +116,6 @@ function TipTap(props: TipTapProps) {
onContentChange,
onFocus = () => {},
content,
toolbarContainerId,
editorContainer,
readonly,
nonce,
@@ -135,7 +126,7 @@ function TipTap(props: TipTapProps) {
} = props;
const isUserPremium = useIsUserPremium();
const configure = useConfigureEditor();
// const configure = useConfigureEditor();
const doubleSpacedLines = useSettingsStore(
(store) => store.doubleSpacedParagraphs
);
@@ -187,7 +178,7 @@ function TipTap(props: TipTapProps) {
dateFormat,
timeFormat,
isMobile: isMobile || false,
element: editorContainer,
element: editorContainer(),
editable: !readonly,
content: content?.(),
autofocus: "start",
@@ -198,11 +189,11 @@ function TipTap(props: TipTapProps) {
editor.commands.focus("start", { scrollIntoView: true });
oldNonce.current = nonce;
configure({
console.log("on create new editor");
useEditorManager.getState().setEditor(id, {
editor: toIEditor(editor as Editor),
canRedo: editor.can().redo(),
canUndo: editor.can().undo(),
toolbarConfig: (await getCurrentPreset()).tools,
statistics: {
words: {
total: getTotalWords(editor as Editor),
@@ -215,32 +206,23 @@ function TipTap(props: TipTapProps) {
onUpdate: ({ editor, transaction }) => {
onContentChange?.();
const preventSave = transaction.getMeta("preventSave") as boolean;
deferredUpdateWordCount(id, () => editor.state.doc.content);
const preventSave = transaction?.getMeta("preventSave") as boolean;
const ignoreEdit = transaction.getMeta("ignoreEdit") as boolean;
const { id, sessionId } = editorstore.get().session;
const content = editor.state.doc.content;
deferredSave(
sessionId,
sessionId,
id,
editor as Editor,
content,
preventSave || !editor.isEditable,
ignoreEdit,
onChange
if (preventSave || !editor.isEditable || !onChange) return;
console.log("CHANGING", onChange);
onChange(
() => getHTMLFromFragment(editor.state.doc.content, editor.schema),
ignoreEdit
);
},
onDestroy: () => {
configure({
editor: undefined,
canRedo: false,
canUndo: false,
searching: false,
statistics: undefined
});
useEditorManager.getState().setEditor(id);
},
onTransaction: ({ editor }) => {
configure({
useEditorManager.getState().updateEditor(id, {
canRedo: editor.can().redo(),
canUndo: editor.can().undo()
});
@@ -250,7 +232,8 @@ function TipTap(props: TipTapProps) {
},
onSelectionUpdate: debounce(({ editor, transaction }) => {
const isEmptySelection = transaction.selection.empty;
configure((old) => {
if (onSelectionChange) onSelectionChange(transaction.selection);
useEditorManager.getState().updateEditor(id, (old) => {
const oldSelected = old.statistics?.words?.selected;
const oldWords = old.statistics?.words.total || 0;
if (isEmptySelection)
@@ -326,91 +309,104 @@ function TipTap(props: TipTapProps) {
[toggleSearch, editor?.storage.searchreplace?.isSearching]
);
useEffect(() => {
if (!editorContainer) return;
const currentEditor = editor;
function onClick(e: MouseEvent) {
if (e.target !== editorContainer || !currentEditor?.state.selection.empty)
return;
// useEffect(() => {
// if (!editorContainer) return;
// const currentEditor = editor;
// function onClick(e: MouseEvent) {
// if (e.target !== editorContainer || !currentEditor?.state.selection.empty)
// return;
const lastNode = currentEditor?.state.doc.lastChild;
const isLastNodeParagraph = lastNode?.type.name === "paragraph";
const isEmpty = lastNode?.nodeSize === 2;
if (isLastNodeParagraph && isEmpty) currentEditor?.commands.focus("end");
else {
currentEditor
?.chain()
.insertContentAt(currentEditor?.state.doc.nodeSize - 2, "<p></p>")
.focus("end")
.run();
}
}
editorContainer.addEventListener("click", onClick);
return () => {
editorContainer.removeEventListener("click", onClick);
};
}, [editor, editorContainer]);
// const lastNode = currentEditor?.state.doc.lastChild;
// const isLastNodeParagraph = lastNode?.type.name === "paragraph";
// const isEmpty = lastNode?.nodeSize === 2;
// if (isLastNodeParagraph && isEmpty) currentEditor?.commands.focus("end");
// else {
// currentEditor
// ?.chain()
// .insertContentAt(currentEditor?.state.doc.nodeSize - 2, "<p></p>")
// .focus("end")
// .run();
// }
// }
// editorContainer.addEventListener("click", onClick);
// return () => {
// editorContainer.removeEventListener("click", onClick);
// };
// }, [editor, editorContainer]);
if (!toolbarContainerId) return null;
console.log("RENDERING TIPTAP");
if (readonly) return null;
return (
<>
<Portal containerId={toolbarContainerId}>
<ScopedThemeProvider scope="editorToolbar" sx={{ width: "100%" }}>
<Toolbar
editor={editor}
location={isMobile ? "bottom" : "top"}
tools={toolbarConfig}
defaultFontFamily={fontFamily}
defaultFontSize={fontSize}
/>
</ScopedThemeProvider>
</Portal>
<ScopedThemeProvider scope="editorToolbar" sx={{ width: "100%" }}>
<Toolbar
editor={editor}
location={isMobile ? "bottom" : "top"}
tools={toolbarConfig}
defaultFontFamily={fontFamily}
defaultFontSize={fontSize}
/>
</ScopedThemeProvider>
</>
);
}
function TiptapWrapper(
props: Omit<
TipTapProps,
"editorContainer" | "theme" | "fontSize" | "fontFamily"
props: PropsWithChildren<
Omit<TipTapProps, "editorContainer" | "theme" | "fontSize" | "fontFamily">
>
) {
const colorScheme = useThemeStore((store) => store.colorScheme);
const theme = useThemeStore((store) =>
colorScheme === "dark" ? store.darkTheme : store.lightTheme
store.colorScheme === "dark" ? store.darkTheme : store.lightTheme
);
const [isReady, setIsReady] = useState(false);
const editorContainerRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const editorContainerRef = useRef<HTMLDivElement>();
const { editorConfig } = useEditorConfig();
useEffect(() => {
setIsReady(true);
useLayoutEffect(() => {
if (
!containerRef.current ||
!editorContainerRef.current ||
editorContainerRef.current.parentElement === containerRef.current
)
return;
containerRef.current.appendChild(editorContainerRef.current);
}, []);
useEffect(() => {
if (!editorContainerRef.current) return;
editorContainerRef.current.style.color =
theme.scopes.editor?.primary?.paragraph ||
theme.scopes.base.primary.paragraph;
}, [theme]);
return (
<PortalProvider>
<Flex sx={{ flex: 1, flexDirection: "column" }}>
{isReady && editorContainerRef.current ? (
<TipTap
{...props}
editorContainer={editorContainerRef.current}
fontFamily={editorConfig.fontFamily}
fontSize={editorConfig.fontSize}
/>
) : null}
<Box
ref={editorContainerRef}
className="selectable"
style={{
flex: 1,
cursor: "text",
color:
<Flex ref={containerRef} sx={{ flex: 1, flexDirection: "column" }}>
<TipTap
{...props}
editorContainer={() => {
if (editorContainerRef.current) return editorContainerRef.current;
const editorContainer = document.createElement("div");
editorContainer.id = "editor-container";
editorContainer.classList.add("selectable");
editorContainer.style.flex = "1";
editorContainer.style.cursor = "text";
editorContainer.style.color =
theme.scopes.editor?.primary?.paragraph ||
theme.scopes.base.primary.paragraph, // TODO!
paddingBottom: 150,
fontSize: editorConfig.fontSize,
fontFamily: getFontById(editorConfig.fontFamily)?.font
theme.scopes.base.primary.paragraph;
editorContainer.style.paddingBottom = `150px`;
editorContainer.style.fontSize = `${editorConfig.fontSize}px`;
editorContainer.style.fontFamily =
getFontById(editorConfig.fontFamily)?.font || "sans-serif";
editorContainerRef.current = editorContainer;
return editorContainer;
}}
fontFamily={editorConfig.fontFamily}
fontSize={editorConfig.fontSize}
/>
{props.children}
</Flex>
</PortalProvider>
);
@@ -429,10 +425,14 @@ function Portal(props: PropsWithChildren<{ containerId?: string }>) {
function toIEditor(editor: Editor): IEditor {
return {
focus: ({ position, scrollIntoView } = {}) =>
editor.current?.commands.focus(position, {
scrollIntoView
}),
focus: ({ position, scrollIntoView } = {}) => {
if (typeof position === "object")
editor.current?.chain().focus().setTextSelection(position).run();
else
editor.current?.commands.focus(position, {
scrollIntoView
});
},
undo: () => editor.current?.commands.undo(),
redo: () => editor.current?.commands.redo(),
updateContent: (content) => {

View File

@@ -19,24 +19,24 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useCallback, useEffect, useMemo, useRef } from "react";
import { Input } from "@theme-ui/components";
import { useStore, store } from "../../stores/editor-store";
import { useEditorStore } from "../../stores/editor-store";
import { debounceWithId } from "@notesnook/common";
import useMobile from "../../hooks/use-mobile";
import useTablet from "../../hooks/use-tablet";
import { useEditorConfig } from "./context";
import { useEditorConfig } from "./manager";
import { getFontById } from "@notesnook/editor";
import { AppEventManager, AppEvents } from "../../common/app-events";
import { replaceDateTime } from "@notesnook/editor/dist/extensions/date-time";
import { useStore as useSettingsStore } from "../../stores/setting-store";
type TitleBoxProps = {
id: string;
readonly: boolean;
};
function TitleBox(props: TitleBoxProps) {
const { readonly } = props;
const { readonly, id } = props;
const inputRef = useRef<HTMLInputElement>(null);
const id = useStore((store) => store.session.id);
// const id = useStore((store) => store.session.id);
const isMobile = useMobile();
const isTablet = useTablet();
const { editorConfig } = useEditorConfig();
@@ -61,7 +61,10 @@ function TitleBox(props: TitleBoxProps) {
);
useEffect(() => {
const { title } = useStore.getState().session;
const session = useEditorStore.getState().getSession(id);
if (!session || !session.note) return;
const { title } = session.note;
if (!inputRef.current) return;
inputRef.current.value = title || "";
updateFontSize(title?.length || 0);
@@ -72,27 +75,27 @@ function TitleBox(props: TitleBoxProps) {
updateFontSize(inputRef.current.value.length);
}, [isTablet, isMobile, updateFontSize]);
useEffect(() => {
const { unsubscribe } = AppEventManager.subscribe(
AppEvents.changeNoteTitle,
({ preventSave, title }: { title: string; preventSave: boolean }) => {
if (!inputRef.current) return;
withSelectionPersist(
inputRef.current,
(input) => (input.value = title)
);
updateFontSize(title.length);
if (!preventSave) {
const { sessionId, id } = store.get().session;
debouncedOnTitleChange(sessionId, id, title);
}
}
);
// useEffect(() => {
// const { unsubscribe } = AppEventManager.subscribe(
// AppEvents.changeNoteTitle,
// ({ preventSave, title }: { title: string; preventSave: boolean }) => {
// if (!inputRef.current) return;
// withSelectionPersist(
// inputRef.current,
// (input) => (input.value = title)
// );
// updateFontSize(title.length);
// if (!preventSave) {
// const { sessionId, id } = store.get().session;
// debouncedOnTitleChange(sessionId, id, title);
// }
// }
// );
return () => {
unsubscribe();
};
}, []);
// return () => {
// unsubscribe();
// };
// }, []);
return (
<Input
@@ -115,13 +118,12 @@ function TitleBox(props: TitleBoxProps) {
}
}}
onChange={(e) => {
const { sessionId, id } = store.get().session;
e.target.value = replaceDateTime(
e.target.value,
dateFormat,
timeFormat
);
debouncedOnTitleChange(sessionId, id, e.target.value);
debouncedOnTitleChange(id, id, e.target.value);
updateFontSize(e.target.value.length);
}}
/>
@@ -132,8 +134,8 @@ export default React.memo(TitleBox, (prevProps, nextProps) => {
return prevProps.readonly === nextProps.readonly;
});
function onTitleChange(noteId: string | undefined, title: string) {
store.get().setTitle(noteId, title);
function onTitleChange(noteId: string, title: string) {
useEditorStore.getState().setTitle(noteId, title);
}
const debouncedOnTitleChange = debounceWithId(onTitleChange, 100);

View File

@@ -1,359 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useEffect, useMemo, useRef, useState } from "react";
import { Button, Flex, Text } from "@theme-ui/components";
import {
Published,
Publish,
EditorNormalWidth,
EditorFullWidth,
ThemeIcon,
FocusMode,
NormalMode,
ExitFullscreen,
Fullscreen,
Search,
Undo,
Redo,
Properties,
ArrowLeft
} from "../icons";
import { useStore as useAppStore } from "../../stores/app-store";
import { useStore as useThemeStore } from "../../stores/theme-store";
import { useStore as useMonographStore } from "../../stores/monograph-store";
import { useStore, store } from "../../stores/editor-store";
import { showToast } from "../../utils/toast";
import { AnimatedInput } from "../animated";
import { showPublishView } from "../publish-view";
import { db } from "../../common/db";
import { useEditorInstance, useHistory, useSearch } from "./context";
import { AppEventManager, AppEvents } from "../../common/app-events";
// TODO: this needs to be cleaned up!
function Toolbar() {
const sessionId = useStore((store) => store.session.id);
const isDeleted = useStore((store) => store.session.isDeleted);
const isLocked = useStore((store) => store.session.locked);
const readonly = useStore((store) => store.session.readonly);
const [isFullscreen, setIsFullscreen] = useState(false);
const isFocusMode = useAppStore((store) => store.isFocusMode);
const toggleFocusMode = useAppStore((store) => store.toggleFocusMode);
const toggleProperties = useStore((store) => store.toggleProperties);
const toggleEditorMargins = useStore((store) => store.toggleEditorMargins);
const editorMargins = useStore((store) => store.editorMargins);
const clearSession = useStore((store) => store.clearSession);
const title = useStore((store) => store.session.title);
const theme = useThemeStore((store) => store.colorScheme);
const toggleNightMode = useThemeStore((store) => store.toggleColorScheme);
const [isTitleVisible, setIsTitleVisible] = useState(false);
const onScrollTitleRef = useRef(null);
const monographs = useMonographStore((store) => store.monographs);
const { canRedo, canUndo, redo, undo } = useHistory();
const { toggleSearch } = useSearch();
const editor = useEditorInstance();
const isNotePublished = useMemo(
() => sessionId && db.monographs.isPublished(sessionId),
// eslint-disable-next-line react-hooks/exhaustive-deps
[sessionId, monographs]
);
useEffect(() => {
const editorScroll = document.querySelector(".editorScroll");
if (!editorScroll) return;
function onScroll(e) {
const hideOffset = document.querySelector(".editorTitle").scrollHeight;
if (e.target.scrollTop > hideOffset && !isTitleVisible)
setIsTitleVisible(e.target.scrollTop > hideOffset && !isTitleVisible);
else if (e.target.scrollTop <= hideOffset && isTitleVisible)
setIsTitleVisible(false);
}
editorScroll.addEventListener("scroll", onScroll);
return () => {
editorScroll.removeEventListener("scroll", onScroll);
};
}, [isTitleVisible]);
useEffect(() => {
if (onScrollTitleRef.current) onScrollTitleRef.current.value = title;
}, [title]);
const tools = useMemo(
() => [
{
title: isNotePublished ? "Published" : "Publish",
icon: isNotePublished ? Published : Publish,
hidden: !sessionId || isDeleted,
enabled: !isLocked,
onClick: () => showPublishView(store.get().session, "top")
}
],
[sessionId, isLocked, isNotePublished, isDeleted]
);
const inlineTools = useMemo(
() => [
{
title: editorMargins
? "Disable editor margins"
: "Enable editor margins",
icon: editorMargins ? EditorNormalWidth : EditorFullWidth,
enabled: true,
onClick: () => toggleEditorMargins()
},
{
title: theme === "dark" ? "Light mode" : "Dark mode",
icon: ThemeIcon,
hidden: !isFocusMode,
enabled: true,
onClick: () => toggleNightMode()
},
{
title: isFocusMode ? "Normal mode" : "Focus mode",
icon: isFocusMode ? FocusMode : NormalMode,
enabled: true,
hideOnMobile: true,
onClick: () => {
toggleFocusMode();
if (isFullscreen) {
exitFullscreen(document);
setIsFullscreen(false);
}
if (editor) editor.current.focus();
}
},
{
title: isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
icon: isFullscreen ? ExitFullscreen : Fullscreen,
enabled: true,
hidden: !isFocusMode,
hideOnMobile: true,
onClick: () => {
if (isFullscreen) {
exitFullscreen(document);
} else {
enterFullscreen(document.documentElement);
}
setIsFullscreen((s) => !s);
}
},
{
title: "Search",
icon: Search,
enabled: true,
hidden: !sessionId || isDeleted,
onClick: () => toggleSearch()
},
{
title: "Undo",
icon: Undo,
enabled: canUndo,
hidden: !sessionId || isDeleted,
onClick: () => undo()
},
{
title: "Redo",
icon: Redo,
enabled: canRedo,
hidden: !sessionId || isDeleted,
onClick: () => redo()
},
{
title: "Properties",
icon: Properties,
enabled: true,
hidden: !sessionId || isFocusMode || isDeleted,
onClick: toggleProperties
}
],
[
editorMargins,
toggleEditorMargins,
editor,
undo,
redo,
isFullscreen,
canRedo,
canUndo,
toggleFocusMode,
toggleProperties,
isFocusMode,
theme,
toggleNightMode,
sessionId,
toggleSearch,
isDeleted
]
);
return (
<Flex mx={2} my={1} sx={{ justifyContent: "space-between" }}>
<Flex sx={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
<ArrowLeft
sx={{
display: ["block", "none", "none"],
flexShrink: 0
}}
size={24}
onClick={() => {
if (store.get().session.id) showToast("success", "Note saved!");
if (isFocusMode) toggleFocusMode();
clearSession();
}}
/>
<AnimatedInput
ref={onScrollTitleRef}
variant="clean"
ml={[2, 2, 0]}
initial={{
opacity: isTitleVisible ? 1 : 0,
zIndex: isTitleVisible ? 1 : -1
}}
animate={{
opacity: isTitleVisible ? 1 : 0,
zIndex: isTitleVisible ? 1 : -1
}}
transition={{ duration: 0.5 }}
defaultValue={title}
readOnly={readonly}
onChange={(e) => {
AppEventManager.publish(AppEvents.changeNoteTitle, {
title: e.target.value,
preventSave: false
});
}}
sx={{
flex: 1,
fontWeight: "heading",
fontSize: "heading",
color: "paragraph",
p: 0,
pl: 4,
borderWidth: 0,
borderRadius: "default",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
overflow: "hidden"
}}
/>
</Flex>
<Flex sx={{ gap: 1 }}>
{tools.map((tool) => (
<Button
key={tool.title}
variant="secondary"
data-test-id={tool.title}
disabled={!tool.enabled}
title={tool.title}
sx={{
display: [
tool.hideOnMobile ? "none" : "flex",
tool.hidden ? "none" : "flex"
],
color: "paragraph",
flexDirection: "row",
flexShrink: 0,
alignItems: "center"
}}
onClick={tool.onClick}
>
<tool.icon size={18} />
<Text
variant="body"
ml={1}
sx={{ display: ["none", "none", "block"] }}
>
{tool.title}
</Text>
</Button>
))}
<Flex
bg="background"
sx={{
borderRadius: "default",
overflow: "hidden",
alignItems: "center",
justifyContent: "flex-end"
}}
>
{inlineTools.map((tool) => (
<Button
data-test-id={tool.title}
disabled={!tool.enabled}
variant="secondary"
title={tool.title}
key={tool.title}
sx={{
display: [
tool.hideOnMobile ? "none" : "flex",
tool.hidden ? "none" : "flex"
],
borderRadius: 0,
flexShrink: 0
}}
onClick={tool.onClick}
>
<tool.icon size={18} />
</Button>
))}
</Flex>
</Flex>
</Flex>
);
}
export default Toolbar;
/* View in fullscreen */
function enterFullscreen(elem) {
// go full-screen
if (elem.requestFullscreen) {
elem.requestFullscreen();
} else if (elem.webkitRequestFullscreen) {
elem.webkitRequestFullscreen();
} else if (elem.mozRequestFullScreen) {
elem.mozRequestFullScreen();
} else if (elem.msRequestFullscreen) {
elem.msRequestFullscreen();
}
}
/* Close fullscreen */
function exitFullscreen(elem) {
if (
!document.fullscreenElement &&
!document.webkitFullscreenElement &&
!document.mozFullScreenElement
)
return;
// exit full-screen
if (elem.exitFullscreen) {
elem.exitFullscreen();
} else if (elem.webkitExitFullscreen) {
elem.webkitExitFullscreen();
} else if (elem.mozCancelFullScreen) {
elem.mozCancelFullScreen();
} else if (elem.msExitFullscreen) {
elem.msExitFullscreen();
}
}

View File

@@ -29,7 +29,7 @@ export type NoteStatistics = {
export interface IEditor {
focus: (options?: {
position?: "start" | "end";
position?: "start" | "end" | { from: number; to: number };
scrollIntoView?: boolean;
}) => void;
undo: () => void;

View File

@@ -20,9 +20,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import useHashRoutes from "../../hooks/use-hash-routes";
import hashroutes from "../../navigation/hash-routes";
import TabsView from "../editor";
function HashRouter() {
const routeResult = useHashRoutes(hashroutes);
return React.isValidElement(routeResult) ? routeResult : null;
useHashRoutes(hashroutes);
return <TabsView />; // React.isValidElement(routeResult) ? routeResult : null;
}
export default React.memo(HashRouter, () => true);

View File

@@ -62,12 +62,12 @@ import {
} from "../../common/dialog-controller";
import { store, useStore } from "../../stores/note-store";
import { store as userstore } from "../../stores/user-store";
import { store as editorStore } from "../../stores/editor-store";
import { useEditorStore } from "../../stores/editor-store";
import { store as tagStore } from "../../stores/tag-store";
import { useStore as useMonographStore } from "../../stores/monograph-store";
import { db } from "../../common/db";
import { showToast } from "../../utils/toast";
import { hashNavigate, navigate } from "../../navigation";
import { navigate } from "../../navigation";
import { showPublishView } from "../publish-view";
import IconTag from "../icon-tag";
import { exportNote, exportNotes, exportToPDF } from "../../common/export";
@@ -112,7 +112,7 @@ function Note(props: NoteProps) {
} = props;
const note = item;
const isOpened = useStore((store) => store.selectedNote === note.id);
const isOpened = useEditorStore((store) => store.activeSessionId === item.id);
const primary: SchemeColors = color ? color.colorCode : "accent-selected";
return (
@@ -138,15 +138,7 @@ function Note(props: NoteProps) {
}}
context={{ color, locked }}
menuItems={menuItems}
onClick={async () => {
if (note.conflicted) {
hashNavigate(`/notes/${note.id}/conflict`, { replace: true });
} else if (locked) {
hashNavigate(`/notes/${note.id}/unlock`, { replace: true });
} else {
hashNavigate(`/notes/${note.id}/edit`, { replace: true });
}
}}
onClick={() => useEditorStore.getState().openSession(note)}
header={
<Flex
sx={{ alignItems: "center", flexWrap: "wrap", gap: 1, mt: "small" }}
@@ -716,7 +708,7 @@ function tagsMenuItems(ids: string[]): MenuItem[] {
await db.relations.to({ id, type: "note" }, "tag").unlink();
}
tagStore.get().refresh();
await editorStore.get().refreshTags();
await useEditorStore.getState().refreshTags();
await store.get().refresh();
}
},
@@ -737,7 +729,7 @@ function tagsMenuItems(ids: string[]): MenuItem[] {
await db.relations.add(tag, { id, type: "note" });
}
await tagStore.get().refresh();
await editorStore.get().refreshTags();
await useEditorStore.getState().refreshTags();
await store.get().refresh();
}
});
@@ -758,7 +750,7 @@ function tagsMenuItems(ids: string[]): MenuItem[] {
await db.relations.unlink(tag, { id, type: "note" });
}
await tagStore.get().refresh();
await editorStore.get().refreshTags();
await useEditorStore.getState().refreshTags();
await store.get().refresh();
}
});

View File

@@ -29,7 +29,11 @@ import {
Checkmark
} from "../icons";
import { Flex, Text } from "@theme-ui/components";
import { useStore, store, EditorSession } from "../../stores/editor-store";
import {
useEditorStore,
ReadonlyEditorSession,
DefaultEditorSession
} from "../../stores/editor-store";
import { db } from "../../common/db";
import { useStore as useAppStore } from "../../stores/app-store";
import { store as noteStore } from "../../stores/note-store";
@@ -38,7 +42,6 @@ import Toggle from "./toggle";
import ScrollContainer from "../scroll-container";
import { ResolvedItem, getFormattedDate, usePromise } from "@notesnook/common";
import { ScopedThemeProvider } from "../theme-provider";
import { PreviewSession } from "../editor/types";
import { ListItemWrapper } from "../list-container/list-profiles";
import { VirtualizedList } from "../virtualized-list";
import { SessionItem } from "../session-item";
@@ -67,10 +70,10 @@ const tools = [
}
] as const;
type MetadataItem<T extends keyof EditorSession = keyof EditorSession> = {
type MetadataItem<T extends "dateCreated" | "dateEdited"> = {
key: T;
label: string;
value: (value: EditorSession[T]) => string;
value: (value: number) => string;
};
const metadataItems = [
@@ -87,19 +90,18 @@ const metadataItems = [
];
type EditorPropertiesProps = {
onOpenPreviewSession: (session: PreviewSession) => void;
id: string;
};
function EditorProperties(props: EditorPropertiesProps) {
const { onOpenPreviewSession } = props;
const { id } = props;
const toggleProperties = useStore((store) => store.toggleProperties);
const toggleProperties = useEditorStore((store) => store.toggleProperties);
const isFocusMode = useAppStore((store) => store.isFocusMode);
const session = useStore((store) => store.session);
const session = useEditorStore((store) =>
store.getSession(id, ["default", "unlocked", "readonly"])
);
const { id: sessionId, sessionType, dateCreated } = session;
const isPreviewMode = sessionType === "preview";
if (isFocusMode || !sessionId) return null;
if (isFocusMode || !session) return null;
return (
<AnimatedFlex
animate={{
@@ -116,7 +118,7 @@ function EditorProperties(props: EditorPropertiesProps) {
display: "flex",
position: "absolute",
right: 0,
zIndex: 3,
zIndex: 1,
height: "100%",
width: "300px",
borderLeft: "1px solid",
@@ -146,19 +148,18 @@ function EditorProperties(props: EditorPropertiesProps) {
/>
}
>
{!isPreviewMode && (
<>
{tools.map((tool) => (
<Toggle
{...tool}
key={tool.key}
toggleKey={tool.property}
onToggle={() => changeToggleState(tool.key)}
testId={`properties-${tool.key}`}
/>
))}
</>
)}
<>
{tools.map((tool) => (
<Toggle
{...tool}
key={tool.key}
isOn={!!session.note[tool.property]}
onToggle={() => changeToggleState(tool.key, session)}
testId={`properties-${tool.key}`}
/>
))}
</>
{metadataItems.map((item) => (
<Flex
key={item.key}
@@ -178,21 +179,16 @@ function EditorProperties(props: EditorPropertiesProps) {
variant="subBody"
sx={{ fontSize: "body" }}
>
{item.value(session[item.key])}
{item.value(session.note[item.key])}
</Text>
</Flex>
))}
{!isPreviewMode && <Colors noteId={sessionId} />}
<Colors noteId={id} color={session.color} />
</Section>
<Notebooks noteId={sessionId} />
<Reminders noteId={sessionId} />
<Attachments noteId={sessionId} />
<SessionHistory
noteId={sessionId}
dateCreated={dateCreated || 0}
isPreviewMode={isPreviewMode}
onOpenPreviewSession={onOpenPreviewSession}
/>
<Notebooks noteId={id} />
<Reminders noteId={id} />
<Attachments noteId={id} />
<SessionHistory noteId={id} />
</ScrollContainer>
</ScopedThemeProvider>
</AnimatedFlex>
@@ -200,8 +196,7 @@ function EditorProperties(props: EditorPropertiesProps) {
}
export default React.memo(EditorProperties);
function Colors({ noteId }: { noteId: string }) {
const color = useStore((store) => store.color);
function Colors({ noteId, color }: { noteId: string; color?: string }) {
const result = usePromise(() => db.colors.all.items(), [color]);
return (
<Flex
@@ -314,6 +309,7 @@ function Attachments({ noteId }: { noteId: string }) {
getItemKey={(index) => result.value.key(index)}
items={result.value.placeholders}
header={<></>}
headerSize={0}
renderRow={({ index }) => (
<ResolvedItem index={index} type="attachment" items={result.value}>
{({ item }) => <ListItemWrapper item={item} compact />}
@@ -323,17 +319,7 @@ function Attachments({ noteId }: { noteId: string }) {
</Section>
);
}
function SessionHistory({
noteId,
dateCreated,
isPreviewMode,
onOpenPreviewSession
}: {
noteId: string;
dateCreated: number;
isPreviewMode: boolean;
onOpenPreviewSession: (session: PreviewSession) => void;
}) {
function SessionHistory({ noteId }: { noteId: string }) {
const result = usePromise(() =>
db.noteHistory
.get(noteId)
@@ -353,15 +339,7 @@ function SessionHistory({
items={result.value.placeholders}
renderItem={({ index }) => (
<ResolvedItem type="session" index={index} items={result.value}>
{({ item }) => (
<SessionItem
noteId={noteId}
session={item}
dateCreated={dateCreated}
isPreviewMode={isPreviewMode}
onOpenPreviewSession={onOpenPreviewSession}
/>
)}
{({ item }) => <SessionItem noteId={noteId} session={item} />}
</ResolvedItem>
)}
/>
@@ -398,7 +376,8 @@ function Section({
}
function changeToggleState(
prop: "lock" | "readonly" | "local-only" | "pin" | "favorite"
prop: "lock" | "readonly" | "local-only" | "pin" | "favorite",
session: ReadonlyEditorSession | DefaultEditorSession
) {
const {
id: sessionId,
@@ -407,7 +386,7 @@ function changeToggleState(
localOnly,
pinned,
favorite
} = store.get().session;
} = session.note;
if (!sessionId) return;
switch (prop) {
case "lock":

View File

@@ -18,11 +18,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Flex, Switch, Text } from "@theme-ui/components";
import { useStore } from "../../stores/editor-store";
import { Icon } from "../icons";
type ToggleProps = {
icon: Icon;
label: string;
onToggle: (toggleState: boolean) => void;
isOn: boolean;
testId?: string;
};
function Toggle(props: ToggleProps) {
const { icon: ToggleIcon, label, onToggle, isOn } = props;
function Toggle(props) {
const { icon: ToggleIcon, label, onToggle, toggleKey } = props;
const isOn = useStore((store) => store.session[toggleKey]);
return (
<Flex
py={2}

View File

@@ -17,33 +17,20 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { getFormattedDate } from "@notesnook/common";
import { getFormattedHistorySessionDate } from "@notesnook/common";
import { HistorySession } from "@notesnook/core";
import { Flex, Text } from "@theme-ui/components";
import TimeAgo from "../time-ago";
import { Lock } from "../icons";
import Vault from "../../common/vault";
import { db } from "../../common/db";
import { PreviewSession } from "../editor/types";
import { useEditorStore } from "../../stores/editor-store";
type SessionItemProps = {
session: HistorySession;
isPreviewMode: boolean;
noteId: string;
dateCreated: number;
onOpenPreviewSession: (session: PreviewSession) => void;
};
export function SessionItem(props: SessionItemProps) {
const { session, isPreviewMode, dateCreated, noteId, onOpenPreviewSession } =
props;
const fromDate = getFormattedDate(session.dateCreated, "date");
const toDate = getFormattedDate(session.dateModified, "date");
const fromTime = getFormattedDate(session.dateCreated, "time");
const toTime = getFormattedDate(session.dateModified, "time");
const label = `${fromDate}, ${fromTime}${
fromDate !== toDate ? `${toDate}, ` : ""
}${toTime}`;
const isSelected = isPreviewMode && session.dateCreated === dateCreated;
const { session, noteId } = props;
const label = getFormattedHistorySessionDate(session);
return (
<Flex
@@ -53,45 +40,17 @@ export function SessionItem(props: SessionItemProps) {
px={2}
sx={{
cursor: "pointer",
bg: isSelected ? "background-selected" : "transparent",
bg: "transparent",
":hover": {
bg: isSelected ? "hover-selected" : "hover"
bg: "hover"
},
alignItems: "center",
justifyContent: "space-between"
}}
title="Click to preview"
onClick={async () => {
const content = await db.noteHistory.content(session.id);
if (!content) return;
if (session.locked) {
await Vault.askPassword(async (password) => {
try {
const decryptedContent = await db.vault.decryptContent(
content,
password
);
onOpenPreviewSession({
content: decryptedContent,
dateCreated: session.dateCreated,
dateEdited: session.dateModified
});
return true;
} catch (e) {
return false;
}
});
} else {
onOpenPreviewSession({
content: {
data: content.data as string,
type: content.type
},
dateCreated: session.dateCreated,
dateEdited: session.dateModified
});
}
}}
onClick={() =>
useEditorStore.getState().openDiffSession(noteId, session.id)
}
>
<Text variant={"body"} data-test-id="title">
{label}

View File

@@ -23,7 +23,7 @@ import { Text } from "@theme-ui/components";
import { store as appStore } from "../../stores/app-store";
import { store as tagStore } from "../../stores/tag-store";
import { store as noteStore } from "../../stores/note-store";
import { store as editorStore } from "../../stores/editor-store";
import { useEditorStore } from "../../stores/editor-store";
import { db } from "../../common/db";
import { Edit, Shortcut, DeleteForver } from "../icons";
import { showToast } from "../../utils/toast";
@@ -99,7 +99,7 @@ const menuItems: (tag: Tag, ids?: string[]) => MenuItem[] = (tag, ids = []) => {
await db.tags.remove(...ids);
showToast("success", `${pluralize(ids.length, "tag")} deleted`);
await appStore.refreshNavItems();
await editorStore.refreshTags();
await useEditorStore.getState().refreshTags();
await tagStore.refresh();
await noteStore.refresh();
},

View File

@@ -25,16 +25,15 @@ import { Flex, Text } from "@theme-ui/components";
import TimeAgo from "../time-ago";
import { pluralize, toTitleCase } from "@notesnook/common";
import { showToast } from "../../utils/toast";
import { hashNavigate } from "../../navigation";
import { useStore } from "../../stores/note-store";
import { MenuItem } from "@notesnook/ui";
import { TrashItem } from "@notesnook/core/dist/types";
import { db } from "../../common/db";
import { useEditorStore } from "../../stores/editor-store";
type TrashItemProps = { item: TrashItem; date: number };
function TrashItem(props: TrashItemProps) {
const { item, date } = props;
const isOpened = useStore((store) => store.selectedNote === item.id);
const isOpened = useEditorStore((store) => store.activeSessionId === item.id);
return (
<ListItem
@@ -59,9 +58,7 @@ function TrashItem(props: TrashItemProps) {
menuItems={menuItems}
onClick={async () => {
if (item.itemType === "note")
(await db.vaults.itemExists({ id: item.id, type: "note" }))
? showToast("error", "Locked notes cannot be previewed in trash.")
: hashNavigate(`/notes/${item.id}/edit`, { replace: true });
useEditorStore.getState().openSession(item);
}}
/>
);

View File

@@ -17,43 +17,32 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useRef, useState, useCallback, useEffect } from "react";
import { useRef, useState, useCallback } from "react";
import { Flex, Text, Button } from "@theme-ui/components";
import { Lock } from "../icons";
import { db } from "../../common/db";
import { useStore as useEditorStore } from "../../stores/editor-store";
import { useStore as useAppStore } from "../../stores/app-store";
import Field from "../field";
import { showToast } from "../../utils/toast";
import { ErrorText } from "../error-text";
import { usePromise } from "@notesnook/common";
type UnlockProps = {
noteId: string;
type UnlockViewProps = {
title: string;
subtitle: string;
buttonTitle: string;
unlock: (password: string) => Promise<void>;
};
function Unlock(props: UnlockProps) {
const { noteId } = props;
export function UnlockView(props: UnlockViewProps) {
const { title, subtitle, buttonTitle, unlock } = props;
const [isWrong, setIsWrong] = useState(false);
const [isUnlocking, setIsUnlocking] = useState(false);
const passwordRef = useRef<HTMLInputElement>(null);
const note = usePromise(() => db.notes.note(noteId), [noteId]);
const openLockedSession = useEditorStore((store) => store.openLockedSession);
const openSession = useEditorStore((store) => store.openSession);
const setIsEditorOpen = useAppStore((store) => store.setIsEditorOpen);
const submit = useCallback(async () => {
console.log("HELO", passwordRef.current);
if (!passwordRef.current) return;
if (!passwordRef.current?.value) return;
setIsUnlocking(true);
const password = passwordRef.current.value;
try {
if (!password) return;
const note = await db.vault.open(noteId, password);
if (!note) return;
openLockedSession(note);
await unlock(password);
} catch (e) {
if (
e instanceof Error &&
@@ -61,20 +50,13 @@ function Unlock(props: UnlockProps) {
) {
setIsWrong(true);
} else {
showToast("error", "Cannot unlock note: " + e);
showToast("error", "Could not unlock: " + e);
console.error(e);
}
} finally {
setIsUnlocking(false);
}
}, [setIsWrong, noteId, openLockedSession]);
useEffect(() => {
(async () => {
await openSession(noteId);
setIsEditorOpen(true);
})();
}, [openSession, setIsEditorOpen, noteId]);
}, [setIsWrong, unlock]);
return (
<Flex
@@ -101,9 +83,7 @@ function Unlock(props: UnlockProps) {
mt={25}
sx={{ fontSize: 36, textAlign: "center" }}
>
{note.status === "fulfilled" && note.value
? note.value.title
: "Open note"}
{title}
</Text>
</Flex>
<Text
@@ -116,14 +96,14 @@ function Unlock(props: UnlockProps) {
color: "var(--paragraph-secondary)"
}}
>
Please enter the password to unlock this note.
{subtitle}
</Text>
<Field
id="vaultPassword"
data-test-id="unlock-note-password"
inputRef={passwordRef}
autoFocus
sx={{ width: ["95%", "95%", "30%"] }}
sx={{ width: ["95%", "95%", "max(30%, 400px)"] }}
placeholder="Enter password"
type="password"
onKeyUp={async (e) => {
@@ -145,9 +125,8 @@ function Unlock(props: UnlockProps) {
await submit();
}}
>
{isUnlocking ? "Unlocking..." : "Open note"}
{isUnlocking ? "Unlocking..." : buttonTitle}
</Button>
</Flex>
);
}
export default Unlock;

View File

@@ -28,7 +28,7 @@ import { db } from "../common/db";
import Dialog from "../components/dialog";
import { useStore, store } from "../stores/tag-store";
import { store as notestore } from "../stores/note-store";
import { store as editorStore } from "../stores/editor-store";
import { useEditorStore } from "../stores/editor-store";
import { Perform } from "../common/dialog-controller";
import { FilteredList } from "../components/filtered-list";
import { ItemReference, Tag } from "@notesnook/core/dist/types";
@@ -101,7 +101,7 @@ function AddTagsDialog(props: AddTagsDialogProps) {
else await db.relations.unlink(tagRef, noteRef);
}
}
await editorStore.get().refreshTags();
await useEditorStore.getState().refreshTags();
await store.get().refresh();
await notestore.get().refresh();
onClose(true);

View File

@@ -49,7 +49,7 @@ import { createPortal } from "react-dom";
import { getId } from "@notesnook/core/dist/utils/id";
import { Label } from "@theme-ui/components";
import { db } from "../../../common/db";
import { useToolbarConfig } from "../../../components/editor/context";
import { useToolbarConfig } from "../../../components/editor/manager";
import {
getAllPresets,
getCurrentPreset,

View File

@@ -21,8 +21,8 @@ import { SettingsGroup } from "./types";
import {
editorConfig,
onEditorConfigChange,
setEditorConfig
} from "../../components/editor/context";
useEditorManager
} from "../../components/editor/manager";
import { useStore as useSettingStore } from "../../stores/setting-store";
import { getFonts } from "@notesnook/editor";
import { useSpellChecker } from "../../hooks/use-spell-checker";
@@ -74,7 +74,9 @@ symbols (e.g. 202305261253)`,
})),
selectedOption: () => editorConfig().fontFamily,
onSelectionChanged: (value) => {
setEditorConfig({ fontFamily: value });
useEditorManager
.getState()
.setEditorConfig({ fontFamily: value });
}
}
]
@@ -93,7 +95,8 @@ symbols (e.g. 202305261253)`,
max: 120,
min: 8,
defaultValue: () => editorConfig().fontSize,
onChange: (value) => setEditorConfig({ fontSize: value })
onChange: (value) =>
useEditorManager.getState().setEditorConfig({ fontSize: value })
}
]
},

View File

@@ -30,6 +30,7 @@ export function useIsUserPremium() {
}
export function isUserPremium(user?: User) {
return true;
if (IS_TESTING) return true;
if (!user) user = userstore.get().user;
if (!user) return false;

View File

@@ -31,19 +31,12 @@ import {
showAddNotebookDialog,
showEditNotebookDialog
} from "../common/dialog-controller";
import { closeOpenedDialog } from "../common/dialog-controller";
import RouteContainer from "../components/route-container";
import DiffViewer from "../components/diff-viewer";
import Unlock from "../components/unlock";
import { store as editorStore } from "../stores/editor-store";
import { isMobile } from "../utils/dimensions";
import { hashNavigate } from ".";
import Editor from "../components/editor";
import { defineRoutes } from "./types";
const hashroutes = defineRoutes({
"/": () => {
return !editorStore.get().session.state && <Editor nonce={"-1"} />;
// return <Editor nonce={"-1"} />;
},
"/email/verify": () => {
showEmailVerificationDialog().then(afterAction);
@@ -63,45 +56,47 @@ const hashroutes = defineRoutes({
"/tags/create": () => {
showCreateTagDialog().then(afterAction);
},
"/notes/create": () => {
closeOpenedDialog();
hashNavigate("/notes/create", { addNonce: true, replace: true });
},
"/notes/create/:nonce": ({ nonce }) => {
closeOpenedDialog();
return <Editor nonce={nonce} />;
},
"/notes/:noteId/edit": ({ noteId }) => {
closeOpenedDialog();
return <Editor noteId={noteId} />;
},
"/notes/:noteId/unlock": ({ noteId }) => {
closeOpenedDialog();
return (
<RouteContainer
type="unlock"
buttons={{
back: isMobile()
? {
title: "Go back",
onClick: () =>
hashNavigate("/notes/create", {
addNonce: true,
replace: true
})
}
: undefined
}}
>
<Unlock noteId={noteId} />
</RouteContainer>
);
},
"/notes/:noteId/conflict": ({ noteId }) => {
closeOpenedDialog();
return <DiffViewer noteId={noteId} />;
},
// "/notes/create": () => {
// closeOpenedDialog();
// editorStore.get().newSession();
// // hashNavigate("/notes/create", { addNonce: true, replace: true });
// },
// "/notes/create/:nonce": ({ nonce }) => {
// closeOpenedDialog();
// // return <Editor nonce={nonce} />;
// },
// "/notes/:noteId/edit": ({ noteId }) => {
// closeOpenedDialog();
// editorStore.get().openSession(noteId);
// // return <Editor noteId={noteId} />;
// },
// "/notes/:noteId/unlock": ({ noteId }) => {
// closeOpenedDialog();
// editorStore.get().openSession(noteId);
// // return (
// // <RouteContainer
// // type="unlock"
// // buttons={{
// // back: isMobile()
// // ? {
// // title: "Go back",
// // onClick: () =>
// // hashNavigate("/notes/create", {
// // addNonce: true,
// // replace: true
// // })
// // }
// // : undefined
// // }}
// // >
// // <Unlock noteId={noteId} />
// // </RouteContainer>
// // );
// },
// "/notes/:noteId/conflict": ({ noteId }) => {
// closeOpenedDialog();
// return <DiffViewer noteId={noteId} />;
// },
"/buy": () => {
showBuyDialog().then(afterAction);
},

View File

@@ -23,7 +23,7 @@ import { store as noteStore } from "./note-store";
import { store as notebookStore } from "./notebook-store";
import { store as trashStore } from "./trash-store";
import { store as tagStore } from "./tag-store";
import { store as editorstore } from "./editor-store";
import { useEditorStore } from "./editor-store";
import { store as attachmentStore } from "./attachment-store";
import { store as monographStore } from "./monograph-store";
import { store as reminderStore } from "./reminder-store";
@@ -157,7 +157,7 @@ class AppStore extends BaseStore<AppStore> {
await attachmentStore.refresh();
await monographStore.refresh();
await settingStore.refresh();
await editorstore.refresh();
await useEditorStore.getState().refresh();
await this.refreshNavItems();

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import createStore from "../common/store";
import { db } from "../common/db";
import BaseStore from "./index";
import { store as editorStore } from "./editor-store";
import { useEditorStore } from "./editor-store";
import { checkAttachment } from "../common/attachments";
import { showToast } from "../utils/toast";
import { AttachmentStream } from "../utils/streams/attachment-stream";
@@ -121,14 +121,14 @@ class AttachmentStore extends BaseStore<AttachmentStore> {
if (await db.attachments.remove(attachment.hash, false)) {
await this.get().refresh();
const sessionId = editorStore.get().session.id;
const sessionId = useEditorStore.getState().session.id;
if (
sessionId &&
(await db.relations
.to({ id: attachment.id, type: "attachment" }, "note")
.has(sessionId))
) {
await editorStore.clearSession();
await useEditorStore.getState().clearSession();
}
}
} catch (e) {

View File

@@ -17,224 +17,470 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import createStore from "../common/store";
import { store as noteStore } from "./note-store";
import { store as attachmentStore } from "./attachment-store";
import { createPersistedStore } from "../common/store";
import { useStore as useNoteStore } from "./note-store";
import { store as appStore } from "./app-store";
import { store as settingStore } from "./setting-store";
import { db } from "../common/db";
import BaseStore from ".";
import { EV, EVENTS } from "@notesnook/core/dist/common";
import { hashNavigate } from "../navigation";
import { logger } from "../utils/logger";
import Config from "../utils/config";
import { setDocumentTitle } from "../utils/dom";
import { Note, Tag } from "@notesnook/core";
import { BaseTrashItem, ContentItem, Note, Tag } from "@notesnook/core";
import { NoteContent } from "@notesnook/core/dist/collections/session-content";
import { Context } from "../components/list-container/types";
import { showToast } from "../utils/toast";
import { getId } from "@notesnook/core/dist/utils/id";
import { createJSONStorage } from "zustand/middleware";
import { getFormattedHistorySessionDate } from "@notesnook/common";
import { isCipher } from "@notesnook/core/dist/database/crypto";
enum SaveState {
export enum SaveState {
NotSaved = -1,
Saving = 0,
Saved = 1
}
enum SESSION_STATES {
stale = "stale",
new = "new",
locked = "locked",
unlocked = "unlocked",
opening = "opening"
new,
old,
locked,
unlocked,
conflicted
}
export type EditorSession = {
sessionType: "default" | "locked" | "preview";
content?: NoteContent<false>;
isDeleted: boolean;
attachmentsLength: number;
saveState: SaveState;
sessionId: string;
state: SESSION_STATES;
context?: Context;
nonce?: string;
} & Partial<Note>;
// type ConflictedContentItem = Omit<ContentItem, "conflicted"> & {
// conflicted: ContentItem;
// };
export const getDefaultSession = (sessionId?: string): EditorSession => {
return {
sessionType: "default",
state: SESSION_STATES.new,
saveState: SaveState.Saved, // -1 = not saved, 0 = saving, 1 = saved
sessionId: sessionId || Date.now().toString(),
attachmentsLength: 0,
isDeleted: false
};
export type BaseEditorSession = {
id: string;
needsHydration?: boolean;
pinned?: boolean;
preview?: boolean;
title?: string;
};
class EditorStore extends BaseStore<EditorStore> {
session = getDefaultSession();
export type LockedEditorSession = BaseEditorSession & {
type: "locked";
note: Note;
};
export type ReadonlyEditorSession = BaseEditorSession & {
type: "readonly";
note: Note;
content?: NoteContent<false>;
color?: string;
tags: Tag[] = [];
tags?: Tag[];
};
export type DeletedEditorSession = BaseEditorSession & {
type: "deleted";
note: BaseTrashItem<Note>;
content?: NoteContent<false>;
};
export type DefaultEditorSession = BaseEditorSession & {
type: "default";
note: Note;
content?: NoteContent<false>;
sessionId: string;
color?: string;
tags?: Tag[];
attachmentsLength?: number;
saveState: SaveState;
};
export type NewEditorSession = BaseEditorSession & {
type: "new";
context?: Context;
saveState: SaveState;
};
export type ConflictedEditorSession = BaseEditorSession & {
type: "conflicted" | "diff";
note: Note;
content: ContentItem;
};
export type EditorSession =
| DefaultEditorSession
| LockedEditorSession
| NewEditorSession
| ConflictedEditorSession
| ReadonlyEditorSession
| DeletedEditorSession;
export type SessionType = keyof SessionTypeMap;
type SessionTypeMap = {
unlocked: DefaultEditorSession;
default: DefaultEditorSession;
locked: LockedEditorSession;
new: NewEditorSession;
conflicted: ConflictedEditorSession;
diff: ConflictedEditorSession;
readonly: ReadonlyEditorSession;
deleted: DeletedEditorSession;
};
export function isLockedSession(session: EditorSession): session is
| LockedEditorSession
// TODO: | DefaultEditorSession
| DeletedEditorSession
| ConflictedEditorSession {
return (
session.type === "locked" ||
("content" in session &&
!!session.content &&
"locked" in session.content &&
session.content.locked)
);
}
class EditorStore extends BaseStore<EditorStore> {
sessions: EditorSession[] = [];
activeSessionId?: string;
arePropertiesVisible = false;
editorMargins = Config.get("editor:margins", true);
history: string[] = [];
getSession = <T extends SessionType[]>(id: string, types?: T) => {
return this.get().sessions.find(
(s): s is SessionTypeMap[T[number]] =>
s.id === id && (!types || types.includes(s.type))
);
};
getActiveSession = <T extends SessionType[]>(types?: T) => {
const { activeSessionId, sessions } = this.get();
return sessions.find(
(s): s is SessionTypeMap[T[number]] =>
s.id === activeSessionId && (!types || types.includes(s.type))
);
};
init = () => {
EV.subscribe(EVENTS.userLoggedOut, () => {
hashNavigate("/notes/create", { replace: true, addNonce: true });
const { closeSessions, sessions } = this.get();
closeSessions(...sessions.map((s) => s.id));
});
EV.subscribe(EVENTS.vaultLocked, async () => {
const { id, locked } = this.get().session;
if (id && locked) hashNavigate(`/notes/${id}/unlock`, { replace: true });
EV.subscribe(EVENTS.vaultLocked, () => {
this.set((state) => {
state.sessions = state.sessions.map((session) => {
console.log("REFRESHIGN", session);
if (isLockedSession(session)) {
if (session.type === "diff" || session.type === "deleted")
return session;
return {
type: "locked",
id: session.id,
note: session.note,
pinned: session.pinned,
preview: session.preview
};
}
return session;
});
});
});
const {
openSession,
openDiffSession,
activateSession,
activeSessionId,
getSession
} = this.get();
if (activeSessionId) {
const session = getSession(activeSessionId);
if (!session) return;
console.log("OPENING", session);
if (session.type === "diff") openDiffSession(session.note.id, session.id);
else if (session.type === "new") activateSession(session.id);
else openSession(activeSessionId);
}
};
refreshTags = async () => {
const { session } = this.get();
if (!session.id) return;
this.set({
tags: await db.relations
.to({ id: session.id, type: "note" }, "tag")
.selector.items(undefined, {
sortBy: "dateCreated",
sortDirection: "asc"
})
});
// const { session } = this.get();
// if (!session.id) return;
// this.set({
// tags: await db.relations
// .to({ id: session.id, type: "note" }, "tag")
// .selector.items(undefined, {
// sortBy: "dateCreated",
// sortDirection: "asc"
// })
// });
};
async refresh() {
const sessionId = this.get().session.id;
if (sessionId && !(await db.notes.exists(sessionId)))
await this.clearSession();
// const sessionId = this.get().session.id;
// if (sessionId && !(await db.notes.exists(sessionId)))
// await this.clearSession();
}
updateSession = async (item: Note) => {
updateSession = <T extends SessionType[]>(
id: string,
types: T,
partial:
| Partial<SessionTypeMap[T[number]]>
| ((session: SessionTypeMap[T[number]]) => void)
) => {
this.set((state) => {
state.session.title = item.title;
state.session.pinned = item.pinned;
state.session.favorite = item.favorite;
state.session.readonly = item.readonly;
state.session.dateEdited = item.dateEdited;
state.session.dateCreated = item.dateCreated;
state.session.locked = item.locked;
});
};
openLockedSession = async (note: Note) => {
this.set((state) => {
state.session = {
...getDefaultSession(note.dateEdited.toString()),
...note,
locked: true,
sessionType: "locked",
id: undefined, // NOTE: we give a session id only after the note is opened.
state: SESSION_STATES.unlocked
};
});
appStore.setIsEditorOpen(true);
hashNavigate(`/notes/${note.id}/edit`, { replace: true });
};
openSession = async (noteId: string, force = false) => {
const session = this.get().session;
if (session.id) await db.fs().cancel(session.id);
if (session.id === noteId && !force) {
noteStore.get().setSelectedNote(noteId);
setDocumentTitle(
settingStore.get().hideNoteTitle ? undefined : session.title
const index = state.sessions.findIndex(
(s) => s.id === id && types.includes(s.type)
);
return;
if (index === -1) return;
const session = state.sessions[index] as SessionTypeMap[T[number]];
if (typeof partial === "function") partial(session);
else state.sessions[index] = { ...session, ...partial };
});
};
activateSession = (id?: string) => {
const session = this.get().sessions.find((s) => s.id === id);
if (!session) id = undefined;
if (
id &&
!settingStore.get().hideNoteTitle &&
session &&
"note" in session
) {
setDocumentTitle(session.note.title);
} else setDocumentTitle();
this.set({ activeSessionId: id });
appStore.setIsEditorOpen(!!id);
this.toggleProperties(false);
if (id) {
const { history } = this.get();
if (history.includes(id)) history.splice(history.indexOf(id), 1);
history.push(id);
}
};
if (session.state === SESSION_STATES.unlocked) {
this.set((state) => {
state.session.id = noteId;
state.session.state = SESSION_STATES.new;
});
return;
}
openDiffSession = async (noteId: string, sessionId: string) => {
const session = await db.noteHistory.session(sessionId);
const note = await db.notes.note(noteId);
if (!session || !note || !note.contentId) return;
const note =
(await db.notes.note(noteId)) || (await db.notes.trashed(noteId));
if (!note) return;
const currentContent = await db.content.get(note.contentId);
const oldContent = await db.noteHistory.content(session.id);
noteStore.get().setSelectedNote(note.id);
setDocumentTitle(settingStore.get().hideNoteTitle ? undefined : note.title);
if (!oldContent || !currentContent) return;
if (await db.vaults.itemExists(note))
return hashNavigate(`/notes/${noteId}/unlock`, { replace: true });
if (note.conflicted)
return hashNavigate(`/notes/${noteId}/conflict`, { replace: true });
const content = note.contentId
? await db.content.get(note.contentId)
: undefined;
if (content && content.locked)
return hashNavigate(`/notes/${noteId}/unlock`, { replace: true });
this.set((state) => {
const defaultSession = getDefaultSession(note.dateEdited.toString());
state.session = {
...defaultSession,
...note,
content,
state: SESSION_STATES.new,
attachmentsLength: 0 // TODO: db.attachments.ofNote(note.id, "all")?.length || 0
};
const isDeleted = note.type === "trash";
if (isDeleted) {
state.session.isDeleted = true;
state.session.readonly = true;
const label = getFormattedHistorySessionDate(session);
this.get().addSession({
type: "diff",
id: session.id,
note,
title: label,
content: {
type: oldContent.type,
dateCreated: session.dateCreated,
dateEdited: session.dateModified,
dateModified: session.dateModified,
id: session.id,
localOnly: false,
noteId,
conflicted: currentContent,
...(isCipher(oldContent.data)
? { locked: true, data: oldContent.data }
: { locked: false, data: oldContent.data })
}
});
appStore.setIsEditorOpen(true);
this.toggleProperties(false);
};
openSession = async (
noteOrId: string | Note | BaseTrashItem<Note>,
force = false
): Promise<void> => {
const { getSession } = this.get();
const noteId = typeof noteOrId === "string" ? noteOrId : noteOrId.id;
const session = getSession(noteId);
if (session && !force && !session.needsHydration) {
return this.activateSession(noteId);
}
if (session && session.id) await db.fs().cancel(session.id);
const note =
typeof noteOrId === "object"
? noteOrId
: (await db.notes.note(noteId)) || (await db.notes.trashed(noteId));
if (!note) return;
const isPreview = session ? session.preview : true;
if (note.locked && note.type !== "trash") {
this.addSession({
type: "locked",
id: note.id,
note,
preview: isPreview
});
} else if (note.conflicted) {
const content = note.contentId
? await db.content.get(note.contentId)
: undefined;
if (
!content ||
content.locked ||
!content.conflicted ||
note.type === "trash"
) {
note.conflicted = false;
await db.notes.add({ id: note.id, conflicted: false });
if (content?.locked) {
await db.content.add({
id: note.contentId,
dateResolved: Date.now()
});
}
return this.openSession(note, true);
}
this.addSession({
type: "conflicted",
content: content,
id: note.id,
note,
preview: isPreview
});
} else {
const content = note.contentId
? await db.content.get(note.contentId)
: undefined;
if (content?.locked) {
note.locked = true;
await db.notes.add({ id: note.id, locked: true });
return this.openSession(note, true);
}
if (note.type === "trash") {
this.addSession({
type: "deleted",
note,
id: note.id,
content
});
} else if (note.readonly) {
this.addSession({
type: "readonly",
note,
id: note.id,
content
});
} else {
const attachmentsLength = await db.attachments
.ofNote(note.id, "all")
.count();
this.addSession({
type: "default",
id: note.id,
note,
saveState: SaveState.Saved,
sessionId: `${Date.now()}`,
attachmentsLength,
content,
preview: isPreview
});
}
}
};
addSession = (session: EditorSession, activate = true) => {
let oldSessionId: string | null = null;
this.set((state) => {
const { activeSessionIndex, duplicateSessionIndex, previewSessionIndex } =
findSessionIndices(state.sessions, session, state.activeSessionId);
if (duplicateSessionIndex > -1) {
oldSessionId = state.sessions[duplicateSessionIndex].id;
state.sessions[duplicateSessionIndex] = session;
} else if (previewSessionIndex > -1) {
oldSessionId = state.sessions[previewSessionIndex].id;
state.sessions[previewSessionIndex] = session;
} else if (activeSessionIndex > -1)
state.sessions.splice(activeSessionIndex + 1, 0, session);
else state.sessions.push(session);
});
const { history } = this.get();
if (
oldSessionId &&
oldSessionId !== session.id &&
history.includes(oldSessionId)
)
history.splice(history.indexOf(oldSessionId), 1);
if (activate) this.activateSession(session.id);
};
saveSession = async (
sessionId: string | undefined,
session: Partial<EditorSession>
) => {
if (!session) {
logger.warn("Session cannot be undefined", { sessionId, session });
return;
id: string,
partial: Partial<Omit<DefaultEditorSession, "note">> & {
note?: Partial<Note>;
ignoreEdit?: boolean;
}
) => {
const currentSession = this.getSession(id, ["new", "default"]);
if (!currentSession) return;
const currentSession = this.get().session;
if (currentSession.readonly && session.readonly !== false) return; // do not allow saving of readonly session
if (currentSession.saveState === 0 || currentSession.id !== sessionId)
// do not allow saving of readonly session
if (partial.note?.readonly) return;
if (
currentSession.saveState === SaveState.Saving ||
currentSession.id !== id
)
return;
this.setSaveState(0);
this.setSaveState(id, 0);
try {
if (session.content) this.get().session.content = session.content;
const id =
currentSession.locked && sessionId && session.content
? await db.vault.save({
content: session.content,
sessionId: session.sessionId,
id: sessionId
})
: await db.notes.add({ ...session, id: sessionId });
if (currentSession && currentSession.id !== sessionId) {
noteStore.refresh();
throw new Error("Aborting save operation: old session.");
// if (partial.content) currentSession.content = partial.content;
if (isLockedSession(currentSession)) {
await db.vault.save({
content: partial.content,
sessionId: partial.sessionId,
id
});
} else {
await db.notes.add({
...partial.note,
dateEdited:
partial.ignoreEdit && currentSession.type === "default"
? currentSession.note.dateEdited
: undefined,
content: partial.content,
sessionId: partial.sessionId,
id
});
}
if (!id) throw new Error("Note not saved.");
// if (currentSession && currentSession.id !== id) {
// noteStore.refresh();
// throw new Error("Aborting save operation: old session.");
// }
// if (!id) throw new Error("Note not saved.");
// let note = await db.notes.note(id);
// if (!note) throw new Error("Note not saved.");
if (!sessionId) {
noteStore.setSelectedNote(id);
hashNavigate(`/notes/${id}/edit`, { replace: true, notify: false });
}
// hashNavigate(`/notes/${id}/edit`, { replace: true, notify: false });
const defaultNotebook = db.settings.getDefaultNotebook();
if (currentSession.context) {
if (currentSession.type === "new" && currentSession.context) {
const { type } = currentSession.context;
if (type === "notebook")
await db.notes.addToNotebook(currentSession.context.id, id);
@@ -243,130 +489,118 @@ class EditorStore extends BaseStore<EditorStore> {
{ type, id: currentSession.context.id },
{ id, type: "note" }
);
} else if (!sessionId && defaultNotebook) {
} else if (!id && defaultNotebook) {
await db.notes.addToNotebook(defaultNotebook, id);
}
const note = await db.notes.note(id);
if (!note) throw new Error("Note not saved.");
const shouldRefreshNotes =
currentSession.context ||
!sessionId ||
note.title !== currentSession.title ||
note.headline !== currentSession.headline;
if (shouldRefreshNotes) noteStore.refresh();
const attachmentsLength = await db.attachments.ofNote(id, "all").count();
if (attachmentsLength !== currentSession.attachmentsLength) {
attachmentStore.refresh();
const shouldRefreshNotes =
currentSession.type === "new" ||
!id ||
note.title !== currentSession.note?.title ||
note.headline !== currentSession.note?.headline ||
attachmentsLength !== currentSession.attachmentsLength;
if (shouldRefreshNotes) useNoteStore.getState().refresh();
if (currentSession.type === "new") {
this.addSession({
type: "default",
id,
note,
saveState: SaveState.Saved,
sessionId: partial.sessionId || `${Date.now()}`,
attachmentsLength,
pinned: currentSession.pinned,
content: partial.content
});
} else {
this.updateSession(id, ["unlocked", "default"], {
preview: false,
attachmentsLength: attachmentsLength,
note
});
}
this.set((state) => {
if (!!state.session.id && state.session.id !== note.id) return;
for (const key in session) {
if (key === "content") continue;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
state.session[key] = session[key as keyof EditorSession];
}
state.session.context = undefined;
state.session.id = note.id;
state.session.title = note.title;
state.session.dateEdited = note.dateEdited;
state.session.attachmentsLength = attachmentsLength;
console.log("NOTE", note.dateEdited);
});
setDocumentTitle(
settingStore.get().hideNoteTitle ? undefined : note.title
);
this.setSaveState(1);
} catch (err) {
showToast(
"error",
err instanceof Error ? err.stack || err.message : JSON.stringify(err)
);
this.setSaveState(-1);
this.setSaveState(id, -1);
console.error(err);
if (err instanceof Error) logger.error(err);
if (currentSession.locked && currentSession.id) {
await this.get().openSession(currentSession.id, true);
if (isLockedSession(currentSession)) {
// TODO:
// hashNavigate(`/notes/${id}/unlock`, { replace: true });
}
}
};
newSession = async (nonce?: string) => {
const context = noteStore.get().context;
const session = this.get().session;
if (session.id) await db.fs().cancel(session.id);
this.set((state) => {
state.session = {
...getDefaultSession(),
context,
nonce,
state: SESSION_STATES.new
};
newSession = () => {
this.addSession({
type: "new",
id: getId(),
context: useNoteStore.getState().context,
saveState: SaveState.NotSaved
});
noteStore.setSelectedNote();
appStore.setIsEditorOpen(true);
setDocumentTitle();
};
clearSession = async (shouldNavigate = true) => {
const session = this.get().session;
if (session.id) await db.fs().cancel(session.id);
closeSessions = (...ids: string[]) => {
this.set((state) => {
state.session = {
...getDefaultSession(),
state: SESSION_STATES.new
};
const sessions: EditorSession[] = [];
for (let i = 0; i < state.sessions.length; ++i) {
const session = state.sessions[i];
if (!ids.includes(session.id)) {
sessions.push(session);
continue;
}
db.fs().cancel(session.id).catch(console.error);
if (state.history.includes(session.id))
state.history.splice(state.history.indexOf(session.id), 1);
}
state.sessions = sessions;
});
noteStore.setSelectedNote();
this.toggleProperties(false);
setTimeout(() => {
if (shouldNavigate)
hashNavigate(`/notes/create`, { replace: true, addNonce: true });
appStore.setIsEditorOpen(false);
setDocumentTitle();
}, 100);
const { history } = this.get();
this.activateSession(history.pop());
};
setTitle = (noteId: string | undefined, title: string) => {
return this.saveSession(noteId, { title });
setTitle = (id: string, title: string) => {
return this.saveSession(id, { note: { title } });
};
toggle = (
noteId: string,
id: string,
name: "favorite" | "pinned" | "readonly" | "localOnly" | "color",
value: boolean | string
) => {
if (name === "color" && typeof value === "string")
return this.set({ color: value });
return this.saveSession(noteId, { [name]: value });
return this.updateSession(id, ["readonly", "default"], { color: value });
return this.saveSession(id, { note: { [name]: value } });
};
saveSessionContent = (
noteId: string | undefined,
id: string,
sessionId: string,
ignoreEdit: boolean,
content: NoteContent<false>
) => {
const dateEdited = ignoreEdit ? this.get().session.dateEdited : undefined;
return this.saveSession(noteId, { sessionId, content, dateEdited });
// const dateEdited = ignoreEdit ? this.get().session.dateEdited : undefined;
return this.saveSession(id, { sessionId, content, ignoreEdit });
};
setSaveState = (saveState: SaveState) => {
this.set((state) => {
state.session.saveState = saveState;
});
setSaveState = (id: string, saveState: SaveState) => {
this.updateSession(id, ["default", "new"], { saveState: saveState });
};
toggleProperties = (toggleState: boolean) => {
toggleProperties = (toggleState?: boolean) => {
this.set(
(state) =>
(state.arePropertiesVisible =
@@ -374,22 +608,62 @@ class EditorStore extends BaseStore<EditorStore> {
);
};
toggleEditorMargins = (toggleState: boolean) => {
toggleEditorMargins = (toggleState?: boolean) => {
this.set((state) => {
state.editorMargins =
toggleState !== undefined ? toggleState : !state.editorMargins;
Config.set("editor:margins", state.editorMargins);
});
};
// _getSaveFn = () => {
// return this.get().session.locked
// ? db.vault.save.bind(db.vault)
// : db.notes.add.bind(db.notes);
// };
}
const [useStore, store] = createStore<EditorStore>(
(set, get) => new EditorStore(set, get)
);
export { useStore, store, SESSION_STATES };
const [useEditorStore] = createPersistedStore(EditorStore, {
name: "editor-sessions",
partialize: (state) => ({
history: state.history,
activeSessionId: state.activeSessionId,
arePropertiesVisible: state.arePropertiesVisible,
editorMargins: state.editorMargins,
sessions: state.sessions.reduce((sessions, session) => {
sessions.push({
id: session.id,
type: isLockedSession(session) ? "locked" : session.type,
needsHydration: true,
preview: session.preview,
pinned: session.pinned,
note:
"note" in session
? {
title: session.note.title
}
: undefined
} as EditorSession);
return sessions;
}, [] as EditorSession[])
}),
storage: createJSONStorage(() => localStorage)
});
export { useEditorStore, SESSION_STATES };
function findSessionIndices(
sessions: EditorSession[],
session: EditorSession,
activeSessionId?: string
) {
let activeSessionIndex = -1;
let previewSessionIndex = -1;
let duplicateSessionIndex = -1;
for (let i = 0; i < sessions.length; ++i) {
const { id, preview } = sessions[i];
if (id === session.id) duplicateSessionIndex = i;
else if (preview && session.preview) previewSessionIndex = i;
else if (id === activeSessionId) activeSessionIndex = i;
}
return {
activeSessionIndex,
previewSessionIndex,
duplicateSessionIndex
};
}

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { db } from "../common/db";
import createStore from "../common/store";
import { store as editorStore } from "./editor-store";
import { useEditorStore } from "./editor-store";
import { store as appStore } from "./app-store";
import { store as selectionStore } from "./selection-store";
import Vault from "../common/vault";
@@ -33,7 +33,6 @@ class NoteStore extends BaseStore<NoteStore> {
notes: VirtualizedGrouping<Note> | undefined = undefined;
contextNotes: VirtualizedGrouping<Note> | undefined = undefined;
context: Context | undefined = undefined;
selectedNote?: string;
// nonce = 0;
viewMode: ViewMode = Config.get("notes:viewMode", "detailed");
@@ -42,11 +41,6 @@ class NoteStore extends BaseStore<NoteStore> {
Config.set("notes:viewMode", viewMode);
};
setSelectedNote = (id?: string) => {
if (!id) selectionStore.get().toggleSelectionMode(false);
this.set({ selectedNote: id });
};
refresh = async () => {
const grouping = await db.notes.all.grouped(
db.settings.getGroupOptions("home")
@@ -77,7 +71,7 @@ class NoteStore extends BaseStore<NoteStore> {
};
delete = async (...ids: string[]) => {
const { session, clearSession } = editorStore.get();
const { session, clearSession } = useEditorStore.getState();
if (session.id && ids.indexOf(session.id) > -1) await clearSession();
await db.notes.moveToTrash(...ids);
await this.refresh();
@@ -97,8 +91,8 @@ class NoteStore extends BaseStore<NoteStore> {
unlock = async (id: string) => {
return await Vault.unlockNote(id).then(async (res) => {
if (editorStore.get().session.id === id)
await editorStore.clearSession(true);
if (useEditorStore.getState().session.id === id)
await useEditorStore.getState().openSession(id);
await this.refresh();
return res;
});
@@ -107,9 +101,8 @@ class NoteStore extends BaseStore<NoteStore> {
lock = async (id: string) => {
if (!(await Vault.lockNote(id))) return false;
await this.refresh();
if (editorStore.get().session.id === id)
await editorStore.openSession(id, true);
return true;
if (useEditorStore.getState().session.id === id)
await useEditorStore.getState().openSession(id, true);
};
readonly = async (state: boolean, ...ids: string[]) => {
@@ -149,7 +142,7 @@ class NoteStore extends BaseStore<NoteStore> {
action: "favorite" | "pinned" | "readonly" | "localOnly" | "color",
value: boolean | string
) => {
const { session, toggle } = editorStore.get();
const { session, toggle } = useEditorStore.getState();
if (!session.id || !noteIds.includes(session.id)) return false;
toggle(session.id, action, value);
return true;

View File

@@ -23,7 +23,7 @@ import { desktop } from "../common/desktop-bridge";
import createStore from "../common/store";
import Config from "../utils/config";
import BaseStore from "./index";
import { store as editorStore } from "./editor-store";
import { useEditorStore } from "./editor-store";
import { isTelemetryEnabled, setTelemetry } from "../utils/telemetry";
import { setDocumentTitle } from "../utils/dom";
import { TimeFormat } from "@notesnook/core/dist/utils/date";
@@ -181,7 +181,7 @@ class SettingStore extends BaseStore<SettingStore> {
this.set({ hideNoteTitle: !hideNoteTitle });
Config.set("hideNoteTitle", !hideNoteTitle);
setDocumentTitle(
!hideNoteTitle ? undefined : editorStore.get().session.title
!hideNoteTitle ? undefined : useEditorStore.getState().session.title
);
};

View File

@@ -21,6 +21,7 @@ import { tryParse } from "./parse";
function set<T>(key: string, value: T) {
window.localStorage.setItem(key, JSON.stringify(value));
return value;
}
function get<T>(key: string, def?: T): T {

View File

@@ -25,6 +25,7 @@ import useNavigate from "../hooks/use-navigate";
import Placeholder from "../components/placeholders";
import { useSearch } from "../hooks/use-search";
import { db } from "../common/db";
import { useEditorStore } from "../stores/editor-store";
function Home() {
const notes = useStore((store) => store.notes);
@@ -68,8 +69,7 @@ function Home() {
items={filteredItems || notes}
placeholder={<Placeholder context="notes" />}
button={{
onClick: () =>
hashNavigate("/notes/create", { replace: true, addNonce: true })
onClick: () => useEditorStore.getState().newSession()
}}
/>
);

View File

@@ -51,7 +51,6 @@ import { FlexScrollContainer } from "../components/scroll-container";
import { Menu } from "../hooks/use-menu";
import Config from "../utils/config";
import Notes from "./notes";
// import { showSortMenu } from "../components/group-header";
type NotebookProps = {
rootId: string;

View File

@@ -28,6 +28,7 @@ import Placeholder from "../components/placeholders";
import { useSearch } from "../hooks/use-search";
import { db } from "../common/db";
import { handleDrop } from "../common/drop-handler";
import { useEditorStore } from "../stores/editor-store";
type NotesProps = { header?: JSX.Element };
function Notes(props: NotesProps) {
@@ -74,8 +75,7 @@ function Notes(props: NotesProps) {
/>
}
button={{
onClick: () =>
hashNavigate("/notes/create", { addNonce: true, replace: true })
onClick: () => useEditorStore.getState().newSession()
}}
header={header}
/>