diff --git a/.eslintignore b/.eslintignore index 0342583ba..7b325e57a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -15,6 +15,8 @@ apps/mobile/e2e/ # web apps/web/public/an.js +apps/web/src/common/sqlite/wa-sqlite-async.js +apps/web/src/common/sqlite/wa-sqlite.js # editor packages/editor/styles/ diff --git a/apps/web/__e2e__/models/app.model.ts b/apps/web/__e2e__/models/app.model.ts index b152e11fa..2e87849cf 100644 --- a/apps/web/__e2e__/models/app.model.ts +++ b/apps/web/__e2e__/models/app.model.ts @@ -81,7 +81,7 @@ export class AppModel { async goToTags() { await this.navigateTo("Tags"); - return new ItemsViewModel(this.page, "tags"); + return new ItemsViewModel(this.page); } async goToColor(color: string) { diff --git a/apps/web/src/common/attachments.ts b/apps/web/src/common/attachments.ts index abae11871..939a8767e 100644 --- a/apps/web/src/common/attachments.ts +++ b/apps/web/src/common/attachments.ts @@ -18,19 +18,17 @@ along with this program. If not, see . */ import { lazify } from "../utils/lazify"; -import { Attachment } from "@notesnook/core"; import { db } from "./db"; async function download(hash: string, groupId?: string) { - const attachment = db.attachments.attachment(hash); + const attachment = await db.attachments.attachment(hash); if (!attachment) return; const downloadResult = await db .fs() .downloadFile( - groupId || attachment.metadata.hash, - attachment.metadata.hash, - attachment.chunkSize, - attachment.metadata + groupId || attachment.hash, + attachment.hash, + attachment.chunkSize ); if (!downloadResult) throw new Error("Failed to download file."); @@ -46,7 +44,7 @@ export async function saveAttachment(hash: string) { const { attachment, key } = response; await lazify(import("../interfaces/fs"), ({ saveFile }) => - saveFile(attachment.metadata.hash, { + saveFile(attachment.hash, { key, iv: attachment.iv, name: attachment.filename, @@ -77,7 +75,7 @@ export async function downloadAttachment< return (await db.attachments.read(hash, type)) as TOutputType; const blob = await lazify(import("../interfaces/fs"), ({ decryptFile }) => - decryptFile(attachment.metadata.hash, { + decryptFile(attachment.hash, { key, iv: attachment.iv, name: attachment.filename, diff --git a/apps/web/src/common/dialog-controller.tsx b/apps/web/src/common/dialog-controller.tsx index 417a9b37d..f7a988498 100644 --- a/apps/web/src/common/dialog-controller.tsx +++ b/apps/web/src/common/dialog-controller.tsx @@ -18,7 +18,6 @@ along with this program. If not, see . */ import { Dialogs } from "../dialogs"; -import { store as notebookStore } from "../stores/notebook-store"; import { store as tagStore } from "../stores/tag-store"; import { store as appStore } from "../stores/app-store"; import { store as editorStore } from "../stores/editor-store"; @@ -26,7 +25,6 @@ import { store as noteStore } from "../stores/note-store"; import { db } from "./db"; import { showToast } from "../utils/toast"; import { Text } from "@theme-ui/components"; -import { Topic as TopicIcon } from "../components/icons"; import Config from "../utils/config"; import { AppVersion, getChangelog } from "../utils/version"; import { Period } from "../dialogs/buy-dialog/types"; @@ -36,7 +34,7 @@ import { ConfirmDialogProps } from "../dialogs/confirm"; import { getFormattedDate } from "@notesnook/common"; import { downloadUpdate } from "../utils/updater"; import { ThemeMetadata } from "@notesnook/themes-server"; -import { Reminder } from "@notesnook/core"; +import { Color, Reminder, Tag } from "@notesnook/core"; import { AuthenticatorType } from "@notesnook/core/dist/api/user-manager"; import { createRoot } from "react-dom/client"; @@ -405,21 +403,25 @@ export function showPasswordDialog( password?: string; oldPassword?: string; newPassword?: string; + deleteAllLockedNotes?: boolean; }) => boolean | Promise ) { const { title, subtitle, positiveButtonText, checks } = getDialogData(type); - return showDialog("PasswordDialog", (Dialog, perform) => ( - perform(false)} - onDone={() => perform(true)} - /> - )); + return showDialog<"PasswordDialog", boolean>( + "PasswordDialog", + (Dialog, perform) => ( + perform(false)} + onDone={() => perform(true)} + /> + ) + ); } export function showBackupPasswordDialog( @@ -462,9 +464,7 @@ export function showCreateTagDialog() { )); } -export function showEditTagDialog(tagId: string) { - const tag = db.tags.tag(tagId); - if (!tag) return; +export function showEditTagDialog(tag: Tag) { return showDialog("ItemDialog", (Dialog, perform) => ( perform(false)} onAction={async (title: string) => { if (!title) return; - await db.tags.add({ id: tagId, title }); + await db.tags.add({ id: tag.id, title }); showToast("success", "Tag edited!"); tagStore.refresh(); editorStore.refreshTags(); @@ -486,9 +486,7 @@ export function showEditTagDialog(tagId: string) { )); } -export function showRenameColorDialog(colorId: string) { - const color = db.colors.color(colorId); - if (!color) return; +export function showRenameColorDialog(color: Color) { return showDialog("ItemDialog", (Dialog, perform) => ( perform(false)} onAction={async (title: string) => { if (!title) return; - await db.tags.add({ id: colorId, title }); + await db.tags.add({ id: color.id, title }); showToast("success", "Color renamed!"); appStore.refreshNavItems(); perform(true); diff --git a/apps/web/src/common/index.ts b/apps/web/src/common/index.ts index 006b67101..ba1e181ae 100644 --- a/apps/web/src/common/index.ts +++ b/apps/web/src/common/index.ts @@ -41,6 +41,8 @@ import { createWritableStream } from "./desktop-bridge"; import { createZipStream } from "../utils/streams/zip-stream"; 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"; export const CREATE_BUTTON_MAP = { notes: { @@ -52,10 +54,6 @@ export const CREATE_BUTTON_MAP = { title: "Create a notebook", onClick: () => hashNavigate("/notebooks/create", { replace: true }) }, - topics: { - title: "Create a topic", - onClick: () => hashNavigate(`/topics/create`, { replace: true }) - }, tags: { title: "Create a tag", onClick: () => hashNavigate(`/tags/create`, { replace: true }) @@ -227,7 +225,7 @@ export async function restoreBackupFile(backupFile: File) { } async function restoreWithProgress( - backup: Record, + backup: LegacyBackupFile, password?: string, key?: string ) { @@ -263,8 +261,8 @@ async function restoreWithProgress( export async function verifyAccount() { if (!(await db.user?.getUser())) return true; - return showPasswordDialog("verify_account", ({ password }) => { - return db.user?.verifyPassword(password) || false; + return showPasswordDialog("verify_account", async ({ password }) => { + return !!password && (await db.user?.verifyPassword(password)); }); } @@ -297,7 +295,7 @@ export async function showUpgradeReminderDialogs() { } async function restore( - backup: Record, + backup: LegacyBackupFile, password?: string, key?: string ) { diff --git a/apps/web/src/common/multi-select.ts b/apps/web/src/common/multi-select.ts index c2eb8fcdc..d2538f98e 100644 --- a/apps/web/src/common/multi-select.ts +++ b/apps/web/src/common/multi-select.ts @@ -27,7 +27,6 @@ import { showToast } from "../utils/toast"; import Vault from "./vault"; import { TaskManager } from "./task-manager"; import { pluralize } from "@notesnook/common"; -import { Reminder } from "@notesnook/core"; async function moveNotesToTrash(ids: string[], confirm = true) { if (confirm && !(await showMultiDeleteConfirmation(ids.length))) return; @@ -103,10 +102,10 @@ async function deleteAttachments(ids: string[]) { showToast("success", `${pluralize(ids.length, "attachment")} deleted`); } -async function moveRemindersToTrash(reminders: Reminder[]) { - const isMultiselect = reminders.length > 1; +async function moveRemindersToTrash(ids: string[]) { + const isMultiselect = ids.length > 1; if (isMultiselect) { - if (!(await showMultiDeleteConfirmation(reminders.length))) return; + if (!(await showMultiDeleteConfirmation(ids.length))) return; } await TaskManager.startTask({ @@ -114,13 +113,13 @@ async function moveRemindersToTrash(reminders: Reminder[]) { id: "deleteReminders", action: async (report) => { report({ - text: `Deleting ${pluralize(reminders.length, "reminder")}...` + text: `Deleting ${pluralize(ids.length, "reminder")}...` }); - await reminderStore.delete(...reminders.map((i) => i.id)); + await reminderStore.delete(...ids); } }); - showToast("success", `${pluralize(reminders.length, "reminder")} deleted.`); + showToast("success", `${pluralize(ids.length, "reminder")} deleted.`); } export const Multiselect = { diff --git a/apps/web/src/common/sqlite/sqlite-api.js b/apps/web/src/common/sqlite/sqlite-api.js index 3940cc117..7f2b5fa6d 100644 --- a/apps/web/src/common/sqlite/sqlite-api.js +++ b/apps/web/src/common/sqlite/sqlite-api.js @@ -288,10 +288,11 @@ export function Factory(Module) { return sqlite3.column_blob(stmt, iCol); case SQLite.SQLITE_FLOAT: return sqlite3.column_double(stmt, iCol); - case SQLite.SQLITE_INTEGER: + case SQLite.SQLITE_INTEGER: { const lo32 = sqlite3.column_int(stmt, iCol); const hi32 = Module.getTempRet0(); return cvt32x2AsSafe(lo32, hi32); + } case SQLite.SQLITE_NULL: return null; case SQLite.SQLITE_TEXT: @@ -813,10 +814,11 @@ export function Factory(Module) { return sqlite3.value_blob(pValue); case SQLite.SQLITE_FLOAT: return sqlite3.value_double(pValue); - case SQLite.SQLITE_INTEGER: + case SQLite.SQLITE_INTEGER: { const lo32 = sqlite3.value_int(pValue); const hi32 = Module.getTempRet0(); return cvt32x2AsSafe(lo32, hi32); + } case SQLite.SQLITE_NULL: return null; case SQLite.SQLITE_TEXT: diff --git a/apps/web/src/common/vault.js b/apps/web/src/common/vault.ts similarity index 80% rename from apps/web/src/common/vault.js rename to apps/web/src/common/vault.ts index cf2052555..8e33c0c02 100644 --- a/apps/web/src/common/vault.js +++ b/apps/web/src/common/vault.ts @@ -26,6 +26,7 @@ class Vault { static async createVault() { if (await db.vault.exists()) return false; return await showPasswordDialog("create_vault", async ({ password }) => { + if (!password) return false; await db.vault.create(password); showToast("success", "Vault created."); return true; @@ -35,6 +36,7 @@ class Vault { static async clearVault() { if (!(await db.vault.exists())) return false; return await showPasswordDialog("clear_vault", async ({ password }) => { + if (!password) return false; try { await db.vault.clear(password); return true; @@ -49,6 +51,7 @@ class Vault { return await showPasswordDialog( "delete_vault", async ({ password, deleteAllLockedNotes }) => { + if (!password) return false; if (!(await db.user.verifyPassword(password))) return false; await db.vault.delete(!!deleteAllLockedNotes); return true; @@ -62,6 +65,7 @@ class Vault { */ static unlockVault() { return showPasswordDialog("ask_vault_password", ({ password }) => { + if (!password) return false; return db.vault .unlock(password) .then(() => true) @@ -73,6 +77,7 @@ class Vault { return showPasswordDialog( "change_password", async ({ oldPassword, newPassword }) => { + if (!oldPassword || !newPassword) return false; await db.vault.changePassword(oldPassword, newPassword); showToast("success", "Vault password changed."); return true; @@ -80,29 +85,28 @@ class Vault { ); } - static unlockNote(id, type = "unlock_note") { - return new Promise((resolve) => { - return showPasswordDialog(type, ({ password }) => { - return db.vault - .remove(id, password) - .then(() => true) - .catch((e) => { - console.error(e); - return false; - }); - }).then(resolve); + static unlockNote(id: string, type = "unlock_note") { + return showPasswordDialog(type, ({ password }) => { + if (!password) return false; + return db.vault + .remove(id, password) + .then(() => true) + .catch((e) => { + console.error(e); + return false; + }); }); } - static lockNote(id) { + static lockNote(id: string): Promise { return db.vault .add(id) .then(() => true) .catch(({ message }) => { switch (message) { - case db.vault.ERRORS.noVault: + case VAULT_ERRORS.noVault: return Vault.createVault().then(() => Vault.lockNote(id)); - case db.vault.ERRORS.vaultLocked: + case VAULT_ERRORS.vaultLocked: return Vault.unlockVault().then(() => Vault.lockNote(id)); default: showToast("error", message); @@ -112,8 +116,9 @@ class Vault { }); } - static askPassword(action) { + static askPassword(action: (password: string) => Promise) { return showPasswordDialog("ask_vault_password", ({ password }) => { + if (!password) return false; return action(password); }); } diff --git a/apps/web/src/components/dialog/index.tsx b/apps/web/src/components/dialog/index.tsx index 398005e0b..896b6f175 100644 --- a/apps/web/src/components/dialog/index.tsx +++ b/apps/web/src/components/dialog/index.tsx @@ -50,7 +50,7 @@ type DialogProps = SxProp & { description?: string; positiveButton?: DialogButtonProps | null; negativeButton?: DialogButtonProps | null; - footer?: React.Component; + footer?: React.ReactNode; noScroll?: boolean; }; diff --git a/apps/web/src/components/editor/picker.ts b/apps/web/src/components/editor/picker.ts index eb8260779..72ea9cea1 100644 --- a/apps/web/src/components/editor/picker.ts +++ b/apps/web/src/components/editor/picker.ts @@ -157,13 +157,15 @@ async function addAttachment( file: File, options: AddAttachmentOptions = {} ): Promise { - const { default: FS } = await import("../../interfaces/fs"); + const { getUploadedFileSize, hashStream, writeEncryptedFile } = await import( + "../../interfaces/fs" + ); const { expectedFileHash, showProgress = true } = options; let forceWrite = options.forceWrite; const action = async () => { const reader = file.stream().getReader(); - const { hash, type: hashType } = await FS.hashStream(reader); + const { hash, type: hashType } = await hashStream(reader); reader.releaseLock(); if (expectedFileHash && hash !== expectedFileHash) @@ -171,15 +173,15 @@ async function addAttachment( `Please select the same file for reuploading. Expected hash ${expectedFileHash} but got ${hash}.` ); - const exists = db.attachments?.attachment(hash); + const exists = await db.attachments.attachment(hash); if (!forceWrite && exists) { - forceWrite = (await FS.getUploadedFileSize(hash)) <= 0; + forceWrite = (await getUploadedFileSize(hash)) <= 0; } if (forceWrite || !exists) { const key: SerializedKey = await getEncryptionKey(); - const output = await FS.writeEncryptedFile(file, key, hash); + const output = await writeEncryptedFile(file, key, hash); if (!output) throw new Error("Could not encrypt file."); if (forceWrite && exists) await db.attachments.reset(hash); diff --git a/apps/web/src/components/editor/title-box.tsx b/apps/web/src/components/editor/title-box.tsx index 822cff971..e4f88e536 100644 --- a/apps/web/src/components/editor/title-box.tsx +++ b/apps/web/src/components/editor/title-box.tsx @@ -49,7 +49,7 @@ function TitleBox(props: TitleBoxProps) { ); const updateFontSize = useCallback( - (length) => { + (length: number) => { if (!inputRef.current) return; const fontSize = textLengthToFontSize( length, diff --git a/apps/web/src/components/field/index.tsx b/apps/web/src/components/field/index.tsx index 566be4aa9..b41710bdf 100644 --- a/apps/web/src/components/field/index.tsx +++ b/apps/web/src/components/field/index.tsx @@ -143,7 +143,11 @@ function Field(props: FieldProps) { }} disabled={action.disabled} > - {action.component ? action.component : } + {action.component ? ( + action.component + ) : action.icon ? ( + + ) : null} )} diff --git a/apps/web/src/components/filtered-list/index.tsx b/apps/web/src/components/filtered-list/index.tsx index 5d030fe6b..5839dc59e 100644 --- a/apps/web/src/components/filtered-list/index.tsx +++ b/apps/web/src/components/filtered-list/index.tsx @@ -27,7 +27,7 @@ type FilteredListProps = { placeholders: { filter: string; empty: string }; filter: (query: string) => Promise; onCreateNewItem: (title: string) => Promise; -} & VirtualizedListProps; +} & VirtualizedListProps; export function FilteredList(props: FilteredListProps) { const { items, filter, onCreateNewItem, placeholders, ...listProps } = props; @@ -38,7 +38,7 @@ export function FilteredList(props: FilteredListProps) { const inputRef = useRef(null); const _filter = useCallback( - async (query) => { + async (query = "") => { setFilteredItems(query ? await filter(query) : []); setQuery(query); }, @@ -46,7 +46,7 @@ export function FilteredList(props: FilteredListProps) { ); const _createNewItem = useCallback( - async (title) => { + async (title: string) => { await onCreateNewItem(title); setQuery(undefined); if (inputRef.current) inputRef.current.value = ""; diff --git a/apps/web/src/components/navigation-menu/index.tsx b/apps/web/src/components/navigation-menu/index.tsx index e6c0a0485..7c10d2209 100644 --- a/apps/web/src/components/navigation-menu/index.tsx +++ b/apps/web/src/components/navigation-menu/index.tsx @@ -121,7 +121,7 @@ function NavigationMenu(props: NavigationMenuProps) { ); const _navigate = useCallback( - (path) => { + (path: string) => { toggleNavigationContainer(true); const nestedRoute = findNestedRoute(path); navigate(!nestedRoute || nestedRoute === location ? path : nestedRoute); @@ -218,7 +218,7 @@ function NavigationMenu(props: NavigationMenuProps) { key: "rename", title: "Rename color", onClick: async () => { - await showRenameColorDialog(color.id); + await showRenameColorDialog(color); } } ]} diff --git a/apps/web/src/components/note/index.tsx b/apps/web/src/components/note/index.tsx index e61b4f9f4..e1ff93d51 100644 --- a/apps/web/src/components/note/index.tsx +++ b/apps/web/src/components/note/index.tsx @@ -94,7 +94,7 @@ import { NotebooksWithDateEdited, TagsWithDateEdited } from "../list-container/types"; -import { SchemeColors, StaticColors } from "@notesnook/theme"; +import { SchemeColors } from "@notesnook/theme"; import Vault from "../../common/vault"; type NoteProps = { diff --git a/apps/web/src/components/properties/index.tsx b/apps/web/src/components/properties/index.tsx index 64d499202..33e778941 100644 --- a/apps/web/src/components/properties/index.tsx +++ b/apps/web/src/components/properties/index.tsx @@ -46,6 +46,7 @@ import { ResolvedItem } from "../list-container/resolved-item"; import { SessionItem } from "../session-item"; import { COLORS } from "../../common/constants"; import { DefaultColors } from "@notesnook/core"; +import { VirtualizedTable } from "../virtualized-table"; const tools = [ { key: "pin", property: "pinned", icon: Pin, label: "Pin" }, @@ -272,10 +273,14 @@ function Notebooks({ noteId }: { noteId: string }) { result.value.getKey(index)} - items={result.value.ungrouped} - renderItem={({ item: id }) => ( - + getItemKey={(index) => result.value.key(index)} + items={result.value.ids} + renderItem={({ item: index }) => ( + + {({ item, data }) => ( + + )} + )} /> @@ -295,10 +300,14 @@ function Reminders({ noteId }: { noteId: string }) { result.value.getKey(index)} - items={result.value.ungrouped} - renderItem={({ item: id }) => ( - + getItemKey={(index) => result.value.key(index)} + items={result.value.ids} + renderItem={({ item: index }) => ( + + {({ item, data }) => ( + + )} + )} /> @@ -315,14 +324,17 @@ function Attachments({ noteId }: { noteId: string }) { return (
- {result.value.ids.map((id, index) => ( - - ))} + result.value.key(index)} + items={result.value.ids} + header={<>} + renderRow={({ item: index }) => ( + + {({ item }) => } + + )} + />
); } @@ -353,10 +365,10 @@ function SessionHistory({ result.value.getKey(index)} - items={result.value.ungrouped} - renderItem={({ item: id }) => ( - + getItemKey={(index) => result.value.key(index)} + items={result.value.ids} + renderItem={({ item: index }) => ( + {({ item }) => ( MenuItem[] = (tag, ids = []) => { title: "Rename tag", icon: Edit.path, onClick: () => { - hashNavigate(`/tags/${tag.id}/edit`); + showEditTagDialog(tag); } }, { diff --git a/apps/web/src/components/theme-preview/index.tsx b/apps/web/src/components/theme-preview/index.tsx index b60bb6a71..4021f42e3 100644 --- a/apps/web/src/components/theme-preview/index.tsx +++ b/apps/web/src/components/theme-preview/index.tsx @@ -66,6 +66,7 @@ export function ThemePreview(props: ThemePreviewProps) { theme.previewColors.background ].map((color) => ( a.id === tag) > -1) continue; - - if (await tagHasNotes(tag, noteIds)) { - selected.push({ - id: tag, - op: "add", - new: false - }); - } - } + const selectedTags = await db.relations + .to({ type: "note", ids: noteIds }, "tag") + .get(); + selectedTags.forEach((r) => { + if (selected.findIndex((a) => a.id === r.fromId) > -1) return; + selected.push({ id: r.fromId, op: "add", new: false }); + }); useSelectionStore.getState().setSelected(selected); })(); - }, [tags]); + }, [tags, noteIds]); return ( {tags && ( items[index]} + getItemKey={(index) => tags.key(index)} mode="fixed" estimatedSize={30} - items={tags.ungrouped} + items={tags.ids} sx={{ mt: 2 }} itemGap={5} placeholders={{ @@ -132,10 +126,10 @@ function AddTagsDialog(props: AddTagsDialogProps) { const { selected, setSelected } = useSelectionStore.getState(); setSelected([...selected, { id: tagId, new: true, op: "add" }]); }} - renderItem={({ item: tagId }) => { + renderItem={({ item: index }) => { return ( - - {({ item }) => } + + {({ item }) => } ); }} diff --git a/apps/web/src/dialogs/attachments-dialog.tsx b/apps/web/src/dialogs/attachments-dialog.tsx index fa44c7999..ec9374b35 100644 --- a/apps/web/src/dialogs/attachments-dialog.tsx +++ b/apps/web/src/dialogs/attachments-dialog.tsx @@ -162,7 +162,7 @@ function AttachmentsDialog({ onClose }: AttachmentsDialogProps) { download(allAttachments?.ungrouped || [])} filter={async (query) => { - setAttachments(await db.lookup.attachments(query)); + setAttachments(await db.lookup.attachments(query).sorted()); }} counts={counts} onRouteChange={async (route) => { diff --git a/apps/web/src/dialogs/mfa/steps.tsx b/apps/web/src/dialogs/mfa/steps.tsx index 377b41ded..75fa3b90c 100644 --- a/apps/web/src/dialogs/mfa/steps.tsx +++ b/apps/web/src/dialogs/mfa/steps.tsx @@ -528,7 +528,7 @@ function SetupSMS(props: SetupAuthenticatorProps) { } }} action={{ - disabled: error || isSending || !enabled, + disabled: !!error || isSending || !enabled, component: ( {isSending ? ( diff --git a/apps/web/src/dialogs/move-note-dialog.tsx b/apps/web/src/dialogs/move-note-dialog.tsx index 291e1178b..8886bdd31 100644 --- a/apps/web/src/dialogs/move-note-dialog.tsx +++ b/apps/web/src/dialogs/move-note-dialog.tsx @@ -38,7 +38,7 @@ import { Perform, showAddNotebookDialog } from "../common/dialog-controller"; import { showToast } from "../utils/toast"; import { isMac } from "../utils/platform"; import { create } from "zustand"; -import { Notebook, isGroupHeader } from "@notesnook/core/dist/types"; +import { Notebook } from "@notesnook/core/dist/types"; import { UncontrolledTreeEnvironment, Tree, @@ -47,6 +47,7 @@ import { } from "react-complex-tree"; import { FlexScrollContainer } from "../components/scroll-container"; import { pluralize } from "@notesnook/common"; +import usePromise from "../hooks/use-promise"; type MoveDialogProps = { onClose: Perform; noteIds: string[] }; type NotebookReference = { @@ -73,18 +74,14 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) { const setIsMultiselect = useSelectionStore((store) => store.setIsMultiselect); const isMultiselect = useSelectionStore((store) => store.isMultiselect); const refreshNotebooks = useStore((store) => store.refresh); - const notebooks = useStore((store) => store.notebooks); + // const notebooks = useStore((store) => store.notebooks); const reloadItem = useRef<(changedItemIds: TreeItemIndex[]) => void>(); const treeRef = useRef(null); + const rootNotebooks = usePromise(() => + db.notebooks.roots.ids(db.settings.getGroupOptions("notebooks")) + ); useEffect(() => { - if (!notebooks) { - (async function () { - await refreshNotebooks(); - })(); - return; - } - // for (const notebook of notebooks.ids) { // if (isGroupHeader(notebook)) continue; // // for (const topic of notebook.topics) { @@ -141,7 +138,7 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) { // new: false // }); // } - }, [noteIds, notebooks, refreshNotebooks, setSelected, setIsMultiselect]); + }, [noteIds, refreshNotebooks, setSelected, setIsMultiselect]); const _onClose = useCallback( (result: boolean) => { @@ -214,7 +211,8 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) { Reset selection )} - {notebooks && notebooks.ids.length > 0 ? ( + {rootNotebooks.status === "fulfilled" && + rootNotebooks.value.length > 0 ? ( !isGroupHeader(t) - ) as string[] + children: rootNotebooks.value }; } @@ -265,7 +261,6 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) { "notebook" ) .get(); - console.log(itemIds, notebooks?.ids); return itemIds.filter(Boolean).map((id) => { if (id === "root") { return { @@ -274,9 +269,7 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) { isFolder: true, canMove: false, canRename: false, - children: notebooks.ids.filter( - (t) => !isGroupHeader(t) - ) as string[] + children: rootNotebooks.value }; } diff --git a/apps/web/src/dialogs/reminder-preview-dialog.tsx b/apps/web/src/dialogs/reminder-preview-dialog.tsx index 0dbbd85df..f2d62e280 100644 --- a/apps/web/src/dialogs/reminder-preview-dialog.tsx +++ b/apps/web/src/dialogs/reminder-preview-dialog.tsx @@ -26,6 +26,7 @@ import IconTag from "../components/icon-tag"; import { Clock, Refresh } from "../components/icons"; import Note from "../components/note"; import { getFormattedReminderTime } from "@notesnook/common"; +import usePromise from "../hooks/use-promise"; export type ReminderPreviewDialogProps = { onClose: Perform; @@ -57,9 +58,11 @@ export default function ReminderPreviewDialog( props: ReminderPreviewDialogProps ) { const { reminder } = props; - const referencedNotes = db.relations - .to({ id: reminder.id, type: "reminder" }, "note") - .resolved(); + const referencedNotes = usePromise( + () => + db.relations.to({ id: reminder.id, type: "reminder" }, "note").resolve(), + [reminder.id] + ); return ( ))} - {referencedNotes && referencedNotes.length > 0 && ( - <> - References: - {referencedNotes.map((item, index) => ( - - ))} - - )} + {referencedNotes && + referencedNotes.status === "fulfilled" && + referencedNotes.value.length > 0 && ( + <> + References: + {referencedNotes.value.map((item, index) => ( + + ))} + + )} ); } diff --git a/apps/web/src/dialogs/settings/auth-settings.ts b/apps/web/src/dialogs/settings/auth-settings.ts index fc0cb2000..e5f82f340 100644 --- a/apps/web/src/dialogs/settings/auth-settings.ts +++ b/apps/web/src/dialogs/settings/auth-settings.ts @@ -49,13 +49,12 @@ export const AuthenticationSettings: SettingsGroup[] = [ await createBackup(); const result = await showPasswordDialog( "change_account_password", - async (data) => { + async ({ newPassword, oldPassword }) => { + if (!newPassword || !oldPassword) return false; await db.user.clearSessions(); return ( - db.user.changePassword( - data.oldPassword, - data.newPassword - ) || false + (await db.user.changePassword(oldPassword, newPassword)) || + false ); } ); diff --git a/apps/web/src/dialogs/settings/components/importer.tsx b/apps/web/src/dialogs/settings/components/importer.tsx index fe8eeaeb3..114ca476d 100644 --- a/apps/web/src/dialogs/settings/components/importer.tsx +++ b/apps/web/src/dialogs/settings/components/importer.tsx @@ -34,7 +34,7 @@ export function Importer() { const notesCounter = useRef(null); const importProgress = useRef(null); - const onDrop = useCallback((acceptedFiles) => { + const onDrop = useCallback((acceptedFiles: File[]) => { setFiles((files) => { const newFiles = [...acceptedFiles, ...files]; return newFiles; @@ -43,7 +43,9 @@ export function Importer() { const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, - accept: [".zip"] + accept: { + "application/zip": [".zip"] + } }); return ( diff --git a/apps/web/src/dialogs/settings/components/spell-checker-languages.tsx b/apps/web/src/dialogs/settings/components/spell-checker-languages.tsx index 0597481bf..d80b4b660 100644 --- a/apps/web/src/dialogs/settings/components/spell-checker-languages.tsx +++ b/apps/web/src/dialogs/settings/components/spell-checker-languages.tsx @@ -36,7 +36,7 @@ export function SpellCheckerLanguages() { }, [spellChecker.enabledLanguages, spellChecker.languages]); const filter = useCallback( - async (query) => { + async (query: string) => { if (!spellChecker.languages) return; setLanguages( spellChecker.languages.filter( diff --git a/apps/web/src/dialogs/settings/profile-settings.ts b/apps/web/src/dialogs/settings/profile-settings.ts index 81887dae7..fd8787a18 100644 --- a/apps/web/src/dialogs/settings/profile-settings.ts +++ b/apps/web/src/dialogs/settings/profile-settings.ts @@ -103,6 +103,7 @@ export const ProfileSettings: SettingsGroup[] = [ title: "Delete account", action: async () => showPasswordDialog("delete_account", async ({ password }) => { + if (!password) return false; await db.user.deleteUser(password); return true; }) diff --git a/apps/web/src/hooks/use-is-user-premium.ts b/apps/web/src/hooks/use-is-user-premium.ts index 5d8994e76..c73091664 100644 --- a/apps/web/src/hooks/use-is-user-premium.ts +++ b/apps/web/src/hooks/use-is-user-premium.ts @@ -30,7 +30,6 @@ 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; diff --git a/apps/web/src/hooks/use-slider.ts b/apps/web/src/hooks/use-slider.ts index 40fa05a1c..d13929be7 100644 --- a/apps/web/src/hooks/use-slider.ts +++ b/apps/web/src/hooks/use-slider.ts @@ -89,7 +89,7 @@ export default function useSlider( }, [ref, slides, onSliding, onChange]); const slideToIndex = useCallback( - (index) => { + (index: number) => { if (!slides || !ref.current || index >= slides.length) return; console.log(slides[index].offset, slides[index].width); const slider = ref.current; diff --git a/apps/web/src/interfaces/fs.ts b/apps/web/src/interfaces/fs.ts index bdbdc4f4f..379f694d8 100644 --- a/apps/web/src/interfaces/fs.ts +++ b/apps/web/src/interfaces/fs.ts @@ -33,7 +33,7 @@ import { ProgressStream } from "../utils/streams/progress-stream"; import { consumeReadableStream } from "../utils/stream"; import { Base64DecoderStream } from "../utils/streams/base64-decoder-stream"; import { toBlob } from "@notesnook-importer/core/dist/src/utils/stream"; -import { Cipher, DataFormat, SerializedKey } from "@notesnook/crypto"; +import { DataFormat, SerializedKey } from "@notesnook/crypto"; import { IDataType } from "hash-wasm/dist/lib/util"; import { IndexedDBKVStore } from "./key-value"; import FileHandle from "@notesnook/streamable-fs/dist/src/filehandle"; @@ -44,6 +44,7 @@ import { } from "./file-store"; import { isFeatureSupported } from "../utils/feature-check"; import { + FileEncryptionMetadataWithHash, FileEncryptionMetadataWithOutputType, IFileStorage, Output, @@ -117,7 +118,7 @@ export async function writeEncryptedFile( return { chunkSize: CHUNK_SIZE, iv: iv, - length: file.size, + size: file.size, salt: key.salt!, alg: "xcha-stream" }; @@ -134,15 +135,15 @@ async function writeEncryptedBase64( data: string, key: SerializedKey, mimeType: string -) { +): Promise { const bytes = new Uint8Array(Buffer.from(data, "base64")); const { hash, type: hashType } = await hashBuffer(bytes); - const attachment = db.attachments.attachment(hash); + const attachment = await db.attachments.attachment(hash); const file = new File([bytes.buffer], hash, { - type: attachment?.metadata.type || mimeType || "application/octet-stream" + type: attachment?.mimeType || mimeType || "application/octet-stream" }); const result = await writeEncryptedFile(file, key, hash); @@ -180,7 +181,7 @@ export async function hashStream( return { type: "xxh64", hash: hasher.digest("hex") }; } -async function readEncrypted( +async function readEncrypted( filename: string, key: SerializedKey, cipherData: FileEncryptionMetadataWithOutputType @@ -468,7 +469,7 @@ async function downloadFile( return true; else if (handle) await handle.delete(); - const attachment = db.attachments?.attachment(filename); + const attachment = await db.attachments.attachment(filename); reportProgress( { total: 100, loaded: 0 }, @@ -510,7 +511,7 @@ async function downloadFile( const totalChunks = Math.ceil(contentLength / chunkSize); const decryptedLength = contentLength - totalChunks * ABYTES; - if (attachment && attachment.length !== decryptedLength) { + if (attachment && attachment.size !== decryptedLength) { const error = `File length mismatch. Please upload this file again from the attachment manager. (File hash: ${filename})`; await db.attachments.markAsFailed(filename, error); throw new Error(error); @@ -519,7 +520,7 @@ async function downloadFile( const fileHandle = await streamablefs.createFile( filename, decryptedLength, - attachment?.metadata.type || "application/octet-stream" + attachment?.mimeType || "application/octet-stream" ); await response.body @@ -554,7 +555,7 @@ async function downloadFile( async function exists(filename: string) { const handle = await streamablefs.readFile(filename); return ( - handle && + !!handle && handle.file.size === (await handle.size()) - handle.file.chunks * ABYTES ); } diff --git a/apps/web/src/navigation/hash-routes.tsx b/apps/web/src/navigation/hash-routes.tsx index 4b87ec2fb..ec57512ce 100644 --- a/apps/web/src/navigation/hash-routes.tsx +++ b/apps/web/src/navigation/hash-routes.tsx @@ -22,7 +22,6 @@ import { showBuyDialog, showCreateTagDialog, showEditReminderDialog, - showEditTagDialog, showEmailVerificationDialog, showFeatureDialog, showOnboardingDialog, @@ -64,9 +63,6 @@ const hashroutes = defineRoutes({ "/tags/create": () => { showCreateTagDialog().then(afterAction); }, - "/tags/:tagId/edit": ({ tagId }) => { - showEditTagDialog(tagId)?.then(afterAction); - }, "/notes/create": () => { closeOpenedDialog(); hashNavigate("/notes/create", { addNonce: true, replace: true }); diff --git a/apps/web/src/stores/app-store.ts b/apps/web/src/stores/app-store.ts index 2d8add679..b3ebf09ef 100644 --- a/apps/web/src/stores/app-store.ts +++ b/apps/web/src/stores/app-store.ts @@ -293,8 +293,7 @@ class AppStore extends BaseStore { try { const result = await db.sync({ type: full ? "full" : "send", - force, - serverLastSynced: lastSynced + force }); if (!result) return this.updateSyncStatus("failed"); diff --git a/apps/web/src/utils/importer.ts b/apps/web/src/utils/importer.ts index f3fcf9c5d..8993c2dcf 100644 --- a/apps/web/src/utils/importer.ts +++ b/apps/web/src/utils/importer.ts @@ -124,10 +124,7 @@ async function processNote(entry: ZipEntry, attachments: Record) { for (const nb of notebooks) { const notebook = await importNotebook(nb).catch(() => ({ id: undefined })); if (!notebook.id) continue; - await db.notes.addToNotebook( - { id: notebook.id, topic: notebook.topic }, - noteId - ); + await db.notes.addToNotebook(notebook.id, noteId); } } diff --git a/apps/web/src/utils/streams/attachment-stream.ts b/apps/web/src/utils/streams/attachment-stream.ts index 43bfdfd45..23cd55252 100644 --- a/apps/web/src/utils/streams/attachment-stream.ts +++ b/apps/web/src/utils/streams/attachment-stream.ts @@ -62,7 +62,7 @@ export class AttachmentStream extends ReadableStream { const file = await lazify( import("../../interfaces/fs"), ({ decryptFile }) => - decryptFile(attachment.metadata.hash, { + decryptFile(attachment.hash, { key, iv: attachment.iv, name: attachment.filename, diff --git a/apps/web/src/utils/web-extension-server.ts b/apps/web/src/utils/web-extension-server.ts index ca69a5bb6..382645b9b 100644 --- a/apps/web/src/utils/web-extension-server.ts +++ b/apps/web/src/utils/web-extension-server.ts @@ -43,27 +43,32 @@ export class WebExtensionServer implements Server { } async getNotes(): Promise { - return db.notes.all - .filter((n) => !n.locked) - .map((note) => ({ id: note.id, title: note.title })); + const notes = await db.notes.all + .where((eb) => eb("notes.locked", "==", false)) + .fields(["notes.id", "notes.title"]) + .items(undefined, db.settings.getGroupOptions("notes")); + return notes; } - async getNotebooks(): Promise { - return db.notebooks.all.map((nb) => ({ - id: nb.id, - title: nb.title, - topics: nb.topics.map((topic: ItemReference) => ({ - id: topic.id, - title: topic.title - })) - })); + async getNotebooks( + parentId?: string + ): Promise { + if (!parentId) + return await db.notebooks.roots + .fields(["notebooks.id", "notebooks.title"]) + .items(); + + return await db.relations + .from({ type: "notebook", id: parentId as string }, "notebook") + .selector.fields(["notebooks.id", "notebooks.title"]) + .items(); } async getTags(): Promise { - return db.tags.all.map((tag) => ({ - id: tag.id, - title: tag.title - })); + const tags = await db.tags.all + .fields(["notes.id", "notes.title"]) + .items(undefined, db.settings.getGroupOptions("tags")); + return tags; } async saveClip(clip: Clip) { @@ -94,9 +99,10 @@ export class WebExtensionServer implements Server { }).outerHTML; } - const note = clip.note?.id ? db.notes.note(clip.note?.id) : null; + const note = clip.note?.id ? await db.notes.note(clip.note?.id) : null; - let content = (await note?.content()) || ""; + let content = + (!!note?.contentId && (await db.content.get(note.contentId))?.data) || ""; if (isCipher(content)) return; content += clipContent; @@ -113,12 +119,9 @@ export class WebExtensionServer implements Server { }); if (id && clip.tags) { - for (const title of clip.tags) { - const tagId = db.tags.tag(title)?.id || (await db.tags.add({ title })); - await db.relations.add( - { id: tagId, type: "tag" }, - { id, type: "note" } - ); + for (const id of clip.tags) { + if (!(await db.tags.exists(id))) continue; + await db.relations.add({ id: id, type: "tag" }, { id, type: "note" }); } } @@ -126,13 +129,7 @@ export class WebExtensionServer implements Server { for (const ref of clip.refs) { switch (ref.type) { case "notebook": - await db.notes.addToNotebook({ id: ref.id }, id); - break; - case "topic": - await db.notes.addToNotebook( - { id: ref.parentId, topic: ref.id }, - id - ); + await db.notes.addToNotebook(ref.id, id); break; } } diff --git a/apps/web/src/views/notebook.tsx b/apps/web/src/views/notebook.tsx index b7d6587e4..16e971990 100644 --- a/apps/web/src/views/notebook.tsx +++ b/apps/web/src/views/notebook.tsx @@ -36,7 +36,10 @@ import { import { pluralize } from "@notesnook/common"; import { Allotment, AllotmentHandle } from "allotment"; import { Plus } from "../components/icons"; -import { useStore as useNotesStore } from "../stores/note-store"; +import { + notesFromContext, + useStore as useNotesStore +} from "../stores/note-store"; import { useStore as useNotebookStore } from "../stores/notebook-store"; import Placeholder from "../components/placeholders"; import { db } from "../common/db"; @@ -74,8 +77,9 @@ function Notebook(props: NotebookProps) { const filteredItems = useSearch( "notes", (query) => { - if (!context || !notes || context.type !== "notebook") return; - return db.lookup.notes(query, notes.ungrouped).sorted(); + if (!context || context.type !== "notebook") return; + const notes = notesFromContext(context); + return db.lookup.notes(query, notes).sorted(); }, [context, notes] ); @@ -95,7 +99,7 @@ function Notebook(props: NotebookProps) { setContext({ type: "notebook", id: notebookId || rootId }); }, [rootId, notebookId]); - const toggleCollapse = useCallback((isCollapsed) => { + const toggleCollapse = useCallback((isCollapsed: boolean) => { if (!paneRef.current || !sizes.current) return; if (!isCollapsed) { diff --git a/extensions/web-clipper/src/common/bridge.ts b/extensions/web-clipper/src/common/bridge.ts index 5b0f0799b..0f5327e5b 100644 --- a/extensions/web-clipper/src/common/bridge.ts +++ b/extensions/web-clipper/src/common/bridge.ts @@ -33,9 +33,7 @@ export type ItemReference = { title: string; }; -export type NotebookReference = ItemReference & { - topics: ItemReference[]; -}; +export type NotebookReference = ItemReference; export type ClientMetadata = { id: string; @@ -76,7 +74,7 @@ export type Clip = { export interface Server { login(): Promise; getNotes(): Promise; - getNotebooks(): Promise; + getNotebooks(parentId?: string): Promise; getTags(): Promise; saveClip(clip: Clip): Promise; } diff --git a/extensions/web-clipper/src/components/notebook-picker/index.tsx b/extensions/web-clipper/src/components/notebook-picker/index.tsx index a78197942..b52de2003 100644 --- a/extensions/web-clipper/src/components/notebook-picker/index.tsx +++ b/extensions/web-clipper/src/components/notebook-picker/index.tsx @@ -168,7 +168,7 @@ function Notebook(props: NotebookProps) { }} /> - notebook.topics} itemName="topic" placeholder={"Search for a topic"} @@ -188,7 +188,7 @@ function Notebook(props: NotebookProps) { }} /> )} - /> + /> */} ); } diff --git a/packages/core/src/api/vault.ts b/packages/core/src/api/vault.ts index 615179964..08d428d5a 100644 --- a/packages/core/src/api/vault.ts +++ b/packages/core/src/api/vault.ts @@ -209,7 +209,7 @@ export default class Vault { async exists(vaultKey?: Cipher<"base64">) { if (!vaultKey) vaultKey = await this.getKey(); - return vaultKey && isCipher(vaultKey); + return !!vaultKey && isCipher(vaultKey); } // Private & internal methods diff --git a/packages/core/src/database/backup.ts b/packages/core/src/database/backup.ts index d22d1f5ba..d0b07b651 100644 --- a/packages/core/src/database/backup.ts +++ b/packages/core/src/database/backup.ts @@ -62,8 +62,10 @@ type EncryptedBackupFile = BaseBackupFile & { encrypted: true; }; -type BackupFile = UnencryptedBackupFile | EncryptedBackupFile; -type LegacyBackupFile = LegacyUnencryptedBackupFile | LegacyEncryptedBackupFile; +export type BackupFile = UnencryptedBackupFile | EncryptedBackupFile; +export type LegacyBackupFile = + | LegacyUnencryptedBackupFile + | LegacyEncryptedBackupFile; type BackupState = { buffer: string[]; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 7752f4c4f..ce05c7d7c 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -20,3 +20,4 @@ along with this program. If not, see . export * from "./types"; export { VirtualizedGrouping } from "./utils/virtualized-grouping"; export { DefaultColors } from "./collections/colors"; +export { type BackupFile, type LegacyBackupFile } from "./database/backup";