web: use new types from core

This commit is contained in:
Abdullah Atta
2023-08-21 16:24:17 +05:00
parent 22221afb7a
commit a881a6d51e
85 changed files with 21768 additions and 753 deletions

View File

@@ -17,7 +17,7 @@ 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 { Reminder } from "@notesnook/core/dist/collections/reminders";
import { Reminder } from "@notesnook/core/dist/types";
import { Locator } from "@playwright/test";
import { getTestId } from "../utils";
import { BaseItemModel } from "./base-item.model";

View File

@@ -21,7 +21,7 @@ import { Locator, Page } from "@playwright/test";
import { getTestId } from "../utils";
import { BaseViewModel } from "./base-view.model";
import { ReminderItemModel } from "./reminder-item.model";
import { Reminder } from "@notesnook/core/dist/collections/reminders";
import { Reminder } from "@notesnook/core/dist/types";
import { fillReminderDialog } from "./utils";
export class RemindersViewModel extends BaseViewModel {

View File

@@ -17,7 +17,7 @@ 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 { Reminder } from "@notesnook/core/dist/collections/reminders";
import { Reminder } from "@notesnook/core/dist/types";
import { Locator, Page } from "@playwright/test";
import { getTestId } from "../utils";
import { Item, Notebook } from "./types";

View File

@@ -17,7 +17,7 @@ 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 { Reminder } from "@notesnook/core/dist/collections/reminders";
import { Reminder } from "@notesnook/core/dist/types";
import { test, expect } from "@playwright/test";
import { AppModel } from "./models/app.model";

21103
apps/web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -78,6 +78,7 @@
"@swc/core": "1.3.61",
"@trpc/server": "10.38.3",
"@types/babel__core": "^7.20.1",
"@types/event-source-polyfill": "^1.0.1",
"@types/file-saver": "^2.0.5",
"@types/marked": "^4.0.7",
"@types/node-fetch": "^2.5.10",

View File

@@ -92,7 +92,7 @@ export default function AppEffects({ setShow }: AppEffectsProps) {
showUpgradeReminderDialogs();
}
await resetNotices();
setIsVaultCreated(await db.vault?.exists());
setIsVaultCreated(await db.vault.exists());
await showOnboardingDialog(interruptedOnboarding());
await showFeatureDialog("highlights");

View File

@@ -22,7 +22,7 @@ import "@notesnook/core/dist/types";
import { getCurrentHash, getCurrentPath, makeURL } from "./navigation";
import Config from "./utils/config";
import { initalizeLogger, logger } from "./utils/logger";
import { initializeLogger, logger } from "./utils/logger";
import { AuthProps } from "./views/auth";
import { initializeFeatureChecks } from "./utils/feature-check";
@@ -142,7 +142,7 @@ function isSessionExpired(path: Routes): RouteWithPath<AuthProps> | null {
}
export async function init() {
await initalizeLogger();
await initializeLogger();
await initializeFeatureChecks();
const { path, route } = getRoute();

View File

@@ -18,20 +18,23 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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 = db.attachments.attachment(hash);
if (!attachment) return;
const downloadResult = await db.fs?.downloadFile(
groupId || attachment.metadata.hash,
attachment.metadata.hash,
attachment.chunkSize,
attachment.metadata
);
const downloadResult = await db
.fs()
.downloadFile(
groupId || attachment.metadata.hash,
attachment.metadata.hash,
attachment.chunkSize,
attachment.metadata
);
if (!downloadResult) throw new Error("Failed to download file.");
const key = await db.attachments?.decryptKey(attachment.key);
const key = await db.attachments.decryptKey(attachment.key);
if (!key) throw new Error("Invalid key for attachment.");
return { key, attachment };
@@ -71,7 +74,7 @@ export async function downloadAttachment<
const { attachment, key } = response;
if (type === "base64" || type === "text")
return (await db.attachments?.read(hash, type)) as TOutputType;
return (await db.attachments.read(hash, type)) as TOutputType;
const blob = await lazify(import("../interfaces/fs"), ({ default: FS }) =>
FS.decryptFile(attachment.metadata.hash, {
@@ -88,7 +91,7 @@ export async function downloadAttachment<
}
export async function checkAttachment(hash: string) {
const attachment = db.attachments?.attachment(hash);
const attachment = db.attachments.attachment(hash);
if (!attachment) return { failed: "Attachment not found." };
try {
@@ -103,7 +106,7 @@ export async function checkAttachment(hash: string) {
}
const ABYTES = 17;
export function getTotalSize(attachments: any[]) {
export function getTotalSize(attachments: Attachment[]) {
let size = 0;
for (const attachment of attachments) {
size += attachment.length + ABYTES;

View File

@@ -18,13 +18,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
export const COLORS = [
"red",
"orange",
"yellow",
"green",
"blue",
"purple",
"gray"
{ key: "red", title: "Red" },
{ key: "orange", title: "Orange" },
{ key: "yellow", title: "Yellow" },
{ key: "green", title: "Green" },
{ key: "blue", title: "Blue" },
{ key: "purple", title: "Purple" },
{ key: "gray", title: "Gray" }
] as const;
export const SUBSCRIPTION_STATUS = {

View File

@@ -30,7 +30,7 @@ async function initializeDatabase(persistence: DatabasePersistence) {
logger.measure("Database initialization");
const { database } = await import("@notesnook/common");
const { default: FS } = await import("../interfaces/fs");
const { FileStorage } = await import("../interfaces/fs");
const { Compressor } = await import("../utils/compressor");
db = database;
@@ -42,12 +42,12 @@ async function initializeDatabase(persistence: DatabasePersistence) {
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co"
});
database.setup(
await NNStorage.createInstance("Notesnook", persistence),
EventSource,
FS,
new Compressor()
);
database.setup({
storage: await NNStorage.createInstance("Notesnook", persistence),
eventsource: EventSource,
fs: FileStorage,
compressor: new Compressor()
});
// if (IS_TESTING) {
// } else {

View File

@@ -19,7 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import ReactDOM from "react-dom";
import { Dialogs } from "../dialogs";
import qclone from "qclone";
import { store as notebookStore } from "../stores/notebook-store";
import { store as tagStore } from "../stores/tag-store";
import { store as appStore } from "../stores/app-store";
@@ -28,18 +27,19 @@ 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 } from "../components/icons";
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";
import { FeatureKeys } from "../dialogs/feature-dialog";
import { AuthenticatorType } from "../dialogs/mfa/types";
import { Suspense } from "react";
import { Reminder } from "@notesnook/core/dist/collections/reminders";
import { ConfirmDialogProps } from "../dialogs/confirm";
import { getFormattedDate } from "@notesnook/common";
import { downloadUpdate } from "../utils/updater";
import { ThemeMetadata } from "@notesnook/themes-server";
import { clone } from "@notesnook/core/dist/utils/clone";
import { Notebook, Reminder } from "@notesnook/core/dist/types";
import { AuthenticatorType } from "@notesnook/core/dist/api/user-manager";
type DialogTypes = typeof Dialogs;
type DialogIds = keyof DialogTypes;
@@ -87,7 +87,7 @@ export function closeOpenedDialog() {
export function showAddTagsDialog(noteIds: string[]) {
return showDialog("AddTagsDialog", (Dialog, perform) => (
<Dialog onClose={(res) => perform(res)} noteIds={noteIds} />
<Dialog onClose={(res: any) => perform(res)} noteIds={noteIds} />
));
}
@@ -97,7 +97,7 @@ export function showAddNotebookDialog() {
isOpen={true}
onDone={async (nb: Record<string, unknown>) => {
// add the notebook to db
const notebook = await db.notebooks?.add({ ...nb });
const notebook = await db.notebooks.add({ ...nb });
if (!notebook) return perform(false);
notebookStore.refresh();
@@ -113,23 +113,23 @@ export function showAddNotebookDialog() {
}
export function showEditNotebookDialog(notebookId: string) {
const notebook = db.notebooks?.notebook(notebookId)?.data;
const notebook = db.notebooks.notebook(notebookId)?.data;
if (!notebook) return;
return showDialog("AddNotebookDialog", (Dialog, perform) => (
<Dialog
isOpen={true}
notebook={notebook}
edit={true}
onDone={async (nb: Record<string, unknown>, deletedTopics: string[]) => {
onDone={async (nb: Notebook, deletedTopics: string[]) => {
// we remove the topics from notebook
// beforehand so we can add them manually, later
const topics = qclone(nb.topics);
const topics = clone(nb.topics);
nb.topics = [];
const notebookId = await db.notebooks?.add(nb);
const notebookId = await db.notebooks.add(nb);
// add or delete topics as required
const notebookTopics = db.notebooks?.notebook(notebookId).topics;
const notebookTopics = notebookId && db.notebooks.topics(notebookId);
if (notebookTopics) {
await notebookTopics.add(...topics);
await notebookTopics.delete(...deletedTopics);
@@ -200,7 +200,7 @@ export function showMultiDeleteConfirmation(length: number) {
return confirm({
title: `Delete ${length} items?`,
message: `These items will be **kept in your Trash for ${
db.settings?.getTrashCleanupInterval() || 7
db.settings.getTrashCleanupInterval() || 7
} days** after which they will be permanently deleted.`,
positiveButtonText: "Yes",
negativeButtonText: "No"
@@ -485,7 +485,7 @@ export function showCreateTopicDialog() {
if (!topic) return;
const notebook = notebookStore.get().selectedNotebook;
if (!notebook) return;
await db.notebooks?.notebook(notebook.id).topics.add(topic);
await db.notebooks.topics(notebook.id).add(topic);
notebookStore.setSelectedNotebook(notebook.id);
showToast("success", "Topic created!");
perform(true);
@@ -495,21 +495,19 @@ export function showCreateTopicDialog() {
}
export function showEditTopicDialog(notebookId: string, topicId: string) {
const topic = db.notebooks?.notebook(notebookId)?.topics?.topic(topicId)
?._topic as Record<string, unknown> | undefined;
const topic = db.notebooks.topics(notebookId).topic(topicId)?._topic;
if (!topic) return;
return showDialog("ItemDialog", (Dialog, perform) => (
<Dialog
title={"Edit topic"}
subtitle={`You are editing "${topic.title}" topic.`}
defaultValue={topic.title}
icon={Topic}
icon={TopicIcon}
item={topic}
onClose={() => perform(false)}
onAction={async (t: string) => {
await db.notebooks
?.notebook(topic.notebookId as string)
.topics.add({ ...topic, title: t });
await db.notebooks.topics(topic.notebookId).add({ ...topic, title: t });
notebookStore.setSelectedNotebook(topic.notebookId);
appStore.refreshNavItems();
showToast("success", "Topic edited!");
@@ -530,7 +528,7 @@ export function showCreateTagDialog() {
onAction={async (title: string) => {
if (!title) return showToast("error", "Tag title cannot be empty.");
try {
await db.tags?.add(title);
await db.tags.add({ title });
showToast("success", "Tag created!");
tagStore.refresh();
perform(true);
@@ -543,18 +541,18 @@ export function showCreateTagDialog() {
}
export function showEditTagDialog(tagId: string) {
const tag = db.tags?.tag(tagId);
const tag = db.tags.tag(tagId);
if (!tag) return;
return showDialog("ItemDialog", (Dialog, perform) => (
<Dialog
title={"Edit tag"}
subtitle={`You are editing #${db.tags?.alias(tag.id)}.`}
defaultValue={db.tags?.alias(tag.id)}
subtitle={`You are editing #${tag.title}.`}
defaultValue={tag.title}
item={tag}
onClose={() => perform(false)}
onAction={async (title: string) => {
if (!title) return;
await db.tags?.rename(tagId, title);
await db.tags.add({ id: tagId, title });
showToast("success", "Tag edited!");
tagStore.refresh();
editorStore.refreshTags();
@@ -567,18 +565,18 @@ export function showEditTagDialog(tagId: string) {
}
export function showRenameColorDialog(colorId: string) {
const color = db.colors?.tag(colorId);
const color = db.colors.color(colorId);
if (!color) return;
return showDialog("ItemDialog", (Dialog, perform) => (
<Dialog
title={"Rename color"}
subtitle={`You are renaming color ${db.colors?.alias(color.id)}.`}
subtitle={`You are renaming color ${color.title}.`}
item={color}
defaultValue={db.colors?.alias(color.id)}
defaultValue={color.title}
onClose={() => perform(false)}
onAction={async (title: string) => {
if (!title) return;
await db.colors?.rename(colorId, title);
await db.tags.add({ id: colorId, title });
showToast("success", "Color renamed!");
appStore.refreshNavItems();
perform(true);

View File

@@ -24,8 +24,12 @@ import { createWriteStream } from "../utils/stream-saver";
import { showToast } from "../utils/toast";
import Vault from "./vault";
import { db } from "./db";
import Note from "@notesnook/core/dist/models/note";
import { sanitizeFilename } from "@notesnook/common";
import { Note, isDeleted } from "@notesnook/core/dist/types";
import {
isEncryptedContent,
isUnencryptedContent
} from "@notesnook/core/dist/collections/content";
export async function exportToPDF(
title: string,
@@ -113,7 +117,7 @@ function createNoteStream(noteIds: string[]) {
async pull(controller) {
const noteId = noteIds[i++];
if (!noteId) controller.close();
else controller.enqueue(db.notes?.note(noteId));
else controller.enqueue(db.notes?.note(noteId)?.data);
},
async cancel(reason) {
throw new Error(reason);
@@ -134,26 +138,31 @@ export async function exportNote(
format: keyof typeof FORMAT_TO_EXT,
disableTemplate = false
) {
if (!db.vault?.unlocked && note.data.locked && !(await Vault.unlockVault())) {
if (!db.vault?.unlocked && note.locked && !(await Vault.unlockVault())) {
showToast("error", `Skipping note "${note.title}" as it is locked.`);
return false;
}
const rawContent = note.data.contentId
? await db.content?.raw(note.data.contentId)
: undefined;
const rawContent = note.contentId
? await db.content.raw(note.contentId)
: null;
const content =
rawContent &&
!rawContent.deleted &&
(typeof rawContent.data === "object"
? await db.vault?.decryptContent(rawContent)
: rawContent);
!rawContent || isDeleted(rawContent)
? undefined
: isEncryptedContent(rawContent)
? await db.vault.decryptContent(rawContent)
: isUnencryptedContent(rawContent)
? rawContent
: undefined;
const exported = await note
.export(format === "pdf" ? "html" : format, content, !disableTemplate)
const exported = await db.notes
.export(note.id, {
format: format === "pdf" ? "html" : format,
contentItem: content
})
.catch((e: Error) => {
console.error(note.data, e);
console.error(note, e);
showToast("error", `Failed to export note "${note.title}": ${e.message}`);
return false as const;
});

View File

@@ -287,8 +287,8 @@ export async function showUpgradeReminderDialogs() {
if (!user || !user.subscription || user.subscription?.expiry === 0) return;
const consumed = totalSubscriptionConsumed(user);
const isTrial = user?.subscription?.type === SUBSCRIPTION_STATUS.TRIAL;
const isBasic = user?.subscription?.type === SUBSCRIPTION_STATUS.BASIC;
const isTrial = user.subscription?.type === SUBSCRIPTION_STATUS.TRIAL;
const isBasic = user.subscription?.type === SUBSCRIPTION_STATUS.BASIC;
if (isBasic && consumed >= 100) {
await showReminderDialog("trialexpired");
} else if (isTrial && consumed >= 75) {

View File

@@ -58,7 +58,6 @@ async function moveNotesToTrash(notes: Item[], confirm = true) {
}
async function moveNotebooksToTrash(notebooks: Item[]) {
const item = notebooks[0];
const isMultiselect = notebooks.length > 1;
if (isMultiselect) {
if (!(await showMultiDeleteConfirmation(notebooks.length))) return;
@@ -89,9 +88,7 @@ async function deleteTopics(notebookId: string, topics: Item[]) {
report({
text: `Deleting ${pluralize(topics.length, "topic")}...`
});
await db.notebooks
?.notebook(notebookId)
.topics.delete(...topics.map((t) => t.id));
await db.notebooks.topics(notebookId).delete(...topics.map((t) => t.id));
notebookStore.setSelectedNotebook(notebookId);
noteStore.refresh();
}

View File

@@ -76,9 +76,9 @@ export async function shouldAddBackupNotice() {
const backupInterval = Config.get("backupReminderOffset", 0);
if (!backupInterval) return false;
const lastBackupTime = await db.backup?.lastBackupTime();
const lastBackupTime = await db.backup.lastBackupTime();
if (!lastBackupTime) {
await db.backup?.updateBackupTime();
await db.backup.updateBackupTime();
return false;
}
@@ -96,13 +96,13 @@ export async function shouldAddRecoveryKeyBackupNotice() {
}
export async function shouldAddLoginNotice() {
const user = await db.user?.getUser();
const user = await db.user.getUser();
if (!user) return true;
}
export async function shouldAddConfirmEmailNotice() {
const user = await db.user?.getUser();
return !user?.isEmailConfirmed;
const user = await db.user.getUser();
return !user || user.isEmailConfirmed;
}
type NoticeData = {
@@ -187,7 +187,7 @@ async function saveBackup() {
{
text: "Later",
onClick: async () => {
await db.backup?.updateBackupTime();
await db.backup.updateBackupTime();
openedToast?.hide();
openedToast = null;
},

View File

@@ -39,11 +39,11 @@ const presets: Record<PresetId, Preset> = {
};
export function getCurrentPreset() {
const preset = db.settings?.getToolbarConfig("desktop");
const preset = db.settings.getToolbarConfig("desktop");
if (!preset) return presets.default;
switch (preset.preset as PresetId) {
case "custom":
presets.custom.tools = preset.config;
presets.custom.tools = preset.config || [];
return presets.custom;
case "minimal":
return presets.minimal;

View File

@@ -242,7 +242,7 @@ const AttachmentMenuItems: (
icon: References.path,
menu: {
items: (attachment.noteIds as string[]).reduce((prev, curr) => {
const note = db.notes?.note(curr);
const note = db.notes.note(curr);
if (!note)
prev.push({
type: "button",
@@ -297,7 +297,7 @@ const AttachmentMenuItems: (
onClick: async () => {
const isDownloading = status?.type === "download";
if (isDownloading) {
await db.fs?.cancel(attachment.metadata.hash, "download");
await db.fs().cancel(attachment.metadata.hash, "download");
} else await saveAttachment(attachment.metadata.hash);
}
},
@@ -309,7 +309,7 @@ const AttachmentMenuItems: (
onClick: async () => {
const isDownloading = status?.type === "upload";
if (isDownloading) {
await db.fs?.cancel(attachment.metadata.hash, "upload");
await db.fs().cancel(attachment.metadata.hash, "upload");
} else
await reuploadAttachment(
attachment.metadata.type,

View File

@@ -17,7 +17,7 @@ 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, useMemo, useRef } from "react";
import { useCallback, useEffect, useRef } from "react";
import { useStore } from "../../stores/editor-store";
import { Input } from "@theme-ui/components";
import { Tag, Plus } from "../icons";
@@ -32,12 +32,9 @@ type HeaderProps = { readonly: boolean };
function Header(props: HeaderProps) {
const { readonly } = props;
const id = useStore((store) => store.session.id);
const tags = useStore((store) => store.session.tags);
const tags = useStore((store) => store.tags);
const setTag = useStore((store) => store.setTag);
const filterableTags = useMemo(() => {
return db.tags?.all.filter((t) => tags?.every((tag) => tag !== t?.title));
}, [tags]);
console.log(tags);
return (
<>
{id && (
@@ -45,22 +42,14 @@ function Header(props: HeaderProps) {
sx={{ lineHeight: 2.5, alignItems: "center", flexWrap: "wrap" }}
data-test-id="tags"
>
{tags?.map((tag) => (
{tags.map((tag) => (
<IconTag
testId={`tag`}
key={tag}
text={db.tags?.alias(tag)}
key={tag.id}
text={tag.title}
icon={Tag}
title={tag}
onClick={() => {
const tagItem = db.tags?.tag(tag);
if (!tagItem) {
setTag(tag);
return;
}
navigate(`/tags/${tagItem.id}`);
}}
onDismiss={readonly ? undefined : () => setTag(tag)}
onClick={() => navigate(`/tags/${tag.id}`)}
onDismiss={readonly ? undefined : () => setTag(tag.title)}
styles={{ container: { mr: 1 }, text: { fontSize: "body" } }}
/>
))}
@@ -68,15 +57,15 @@ function Header(props: HeaderProps) {
<Autosuggest
sessionId={id}
filter={(query) =>
db.lookup?.tags(filterableTags, query).slice(0, 10) || []
db.lookup?.tags(tags, query).slice(0, 10) || []
}
onAdd={(value) => setTag(value)}
onSelect={(item) => setTag(item.title)}
onRemove={() => {
if (tags.length <= 0) return;
setTag(tags[tags.length - 1]);
setTag(tags[tags.length - 1].title);
}}
defaultItems={filterableTags?.slice(0, 10) || []}
defaultItems={tags.slice(0, 10) || []}
/>
)}
</Flex>

View File

@@ -55,11 +55,18 @@ 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 { isEncryptedContent } from "@notesnook/core/dist/collections/content";
const PDFPreview = React.lazy(() => import("../pdf-preview"));
type PreviewSession = {
content: { data: string; type: string };
content: { data: string; type: ContentType };
dateCreated: number;
dateEdited: number;
};
@@ -117,9 +124,11 @@ export default function EditorManager({
useEffect(() => {
const event = db.eventManager.subscribe(
EVENTS.syncItemMerged,
async (item?: Record<string, string | number>) => {
async (item?: MaybeDeletedItem<Item>) => {
if (
!item ||
isDeleted(item) ||
(item.type !== "tiptap" && item.type !== "note") ||
lastSavedTime.current >= (item.dateEdited as number) ||
isPreviewSession ||
!appstore.get().isRealtimeSyncEnabled
@@ -130,10 +139,13 @@ export default function EditorManager({
const isContent = item.type === "tiptap" && item.id === contentId;
const isNote = item.type === "note" && item.id === id;
if (isContent && editorInstance.current) {
if (locked) {
const result = await db.vault?.decryptContent(item).catch(() => {});
if (result) item.data = result.data;
if (id && isContent && editorInstance.current) {
let content: string | null = null;
if (locked && isEncryptedContent(item)) {
const result = await db.vault
.decryptContent(item)
.catch(() => undefined);
if (result) content = result.data;
else EV.publish(EVENTS.vaultLocked);
}
editorInstance.current.updateContent(item.data as string);
@@ -210,7 +222,7 @@ export default function EditorManager({
id={noteId}
nonce={timestamp}
content={() =>
previewSession.current?.content?.data ||
previewSession.current?.content.data ||
editorstore.get().session?.content?.data
}
onPreviewDocument={(url) => setDocPreview(url)}
@@ -378,7 +390,7 @@ export function Editor(props: EditorProps) {
onDownloadAttachment={(attachment) => saveAttachment(attachment.hash)}
onPreviewAttachment={async (data) => {
const { hash } = data;
const attachment = db.attachments?.attachment(hash);
const attachment = db.attachments.attachment(hash);
if (attachment && attachment.metadata.type.startsWith("image/")) {
const container = document.getElementById("dialogContainer");
if (!(container instanceof HTMLElement)) return;

View File

@@ -135,7 +135,7 @@ async function pickImage(
}
async function getEncryptionKey(): Promise<SerializedKey> {
const key = await db.attachments?.generateKey();
const key = await db.attachments.generateKey();
if (!key) throw new Error("Could not generate a new encryption key.");
return key;
}
@@ -182,13 +182,15 @@ async function addAttachment(
const output = await FS.writeEncryptedFile(file, key, hash);
if (!output) throw new Error("Could not encrypt file.");
if (forceWrite && exists) await db.attachments?.reset(hash);
await db.attachments?.add({
if (forceWrite && exists) await db.attachments.reset(hash);
await db.attachments.add({
...output,
hash,
hashType,
filename: exists?.metadata.filename || file.name,
type: exists?.metadata.type || file.type,
metadata: {
hash,
hashType,
filename: exists?.metadata.filename || file.name,
type: exists?.metadata.type || file.type
},
key
});
}

View File

@@ -38,6 +38,11 @@ import { useStore as useNoteStore } from "../../stores/note-store";
import { useStore as useNotebookStore } from "../../stores/notebook-store";
import useMobile from "../../hooks/use-mobile";
import { MenuButtonItem, MenuItem } from "@notesnook/ui";
import {
GroupHeader as GroupHeaderType,
GroupOptions,
GroupingKey
} from "@notesnook/core/dist/types";
const groupByToTitleMap = {
none: "None",
@@ -162,7 +167,7 @@ const sortByMenu: (options: GroupingMenuOptions) => MenuItem = (options) => ({
});
export function showSortMenu(groupingKey: GroupingKey, refresh: () => void) {
const groupOptions = db.settings?.getGroupOptions(groupingKey);
const groupOptions = db.settings.getGroupOptions(groupingKey);
if (!groupOptions) return;
const menuOptions: Omit<GroupingMenuOptions, "parentKey"> = {
@@ -196,7 +201,7 @@ function changeGroupOptions(
if (item.key === "abc") groupOptions.sortBy = "title";
else groupOptions.sortBy = "dateEdited";
}
db.settings?.setGroupOptions(options.groupingKey, groupOptions);
db.settings.setGroupOptions(options.groupingKey, groupOptions);
options.refresh();
}
@@ -216,7 +221,7 @@ type GroupHeaderProps = {
groupingKey: GroupingKey;
index: number;
groups: { title: string }[];
groups: GroupHeaderType[];
onJump: (title: string) => void;
refresh: () => void;
onSelectGroup: () => void;
@@ -234,7 +239,7 @@ function GroupHeader(props: GroupHeaderProps) {
isFocused
} = props;
const [groupOptions, setGroupOptions] = useState(
db.settings!.getGroupOptions(groupingKey)
db.settings.getGroupOptions(groupingKey)
);
const groupHeaderRef = useRef<HTMLDivElement>(null);
const { openMenu, target } = useMenuTrigger();

View File

@@ -17,7 +17,7 @@ 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 { forwardRef, useEffect, useMemo, useRef, useState } from "react";
import { forwardRef, useEffect, useRef, useState } from "react";
import { Flex, Button } from "@theme-ui/components";
import { Plus } from "../icons";
import { ScrollerProps, Virtuoso, VirtuosoHandle } from "react-virtuoso";
@@ -26,12 +26,19 @@ import {
store as selectionStore
} from "../../stores/selection-store";
import GroupHeader from "../group-header";
import { DEFAULT_ITEM_HEIGHT, ListProfiles } from "./list-profiles";
import { DEFAULT_ITEM_HEIGHT, ListItemWrapper } from "./list-profiles";
import Announcements from "../announcements";
import { ListLoader } from "../loaders/list-loader";
import ScrollContainer from "../scroll-container";
import { useKeyboardListNavigation } from "../../hooks/use-keyboard-list-navigation";
import { Context, Item } from "./types";
import { Context } from "./types";
import {
GroupHeader as GroupHeaderType,
GroupedItems,
GroupingKey,
Item,
isGroupHeader
} from "@notesnook/core/dist/types";
export const CustomScrollbarsVirtualList = forwardRef<
HTMLDivElement,
@@ -49,9 +56,8 @@ export const CustomScrollbarsVirtualList = forwardRef<
});
type ListContainerProps = {
type: keyof typeof ListProfiles;
items: Item[];
groupingKey?: GroupingKey;
group?: GroupingKey;
items: GroupedItems<Item>;
compact?: boolean;
context?: Context;
refresh: () => void;
@@ -64,16 +70,7 @@ type ListContainerProps = {
};
function ListContainer(props: ListContainerProps) {
const {
type,
groupingKey,
items,
context,
refresh,
header,
button,
compact
} = props;
const { group, items, context, refresh, header, button, compact } = props;
const [focusedGroupIndex, setFocusedGroupIndex] = useState(-1);
@@ -88,11 +85,6 @@ function ListContainer(props: ListContainerProps) {
const listRef = useRef<VirtuosoHandle>(null);
const listContainerRef = useRef(null);
const groups = useMemo(
() => props.items.filter((v) => v.type === "header"),
[props.items]
);
useEffect(() => {
return () => {
selectionStore.toggleSelectionMode(false);
@@ -127,8 +119,6 @@ function ListContainer(props: ListContainerProps) {
}
});
const Component = ListProfiles[type];
return (
<Flex variant="columnFill">
{!props.items.length && props.placeholder ? (
@@ -147,12 +137,12 @@ function ListContainer(props: ListContainerProps) {
<Flex
ref={listContainerRef}
variant="columnFill"
data-test-id={`${type}-list`}
data-test-id={`${group}-list`}
>
<Virtuoso
ref={listRef}
data={items}
computeItemKey={(index) => items[index].id || items[index].title}
computeItemKey={(index) => items[index].id}
defaultItemHeight={DEFAULT_ITEM_HEIGHT}
totalCount={items.length}
onBlur={() => setFocusedGroupIndex(-1)}
@@ -177,10 +167,10 @@ function ListContainer(props: ListContainerProps) {
switch (item.type) {
case "header":
if (!groupingKey) return null;
if (!group) return null;
return (
<GroupHeader
groupingKey={groupingKey}
groupingKey={group}
refresh={refresh}
title={item.title}
isFocused={index === focusedGroupIndex}
@@ -201,10 +191,14 @@ function ListContainer(props: ListContainerProps) {
)
]);
}}
groups={groups}
groups={
props.items.filter((v) =>
isGroupHeader(v)
) as GroupHeaderType[]
}
onJump={(title: string) => {
const index = props.items.findIndex(
(v) => v.title === title
(v) => isGroupHeader(v) && v.title === title
);
if (index < 0) return;
listRef.current?.scrollToIndex({
@@ -218,10 +212,10 @@ function ListContainer(props: ListContainerProps) {
);
default:
return (
<Component
<ListItemWrapper
item={item}
context={context}
type={type}
group={group}
compact={compact}
/>
);
@@ -234,7 +228,7 @@ function ListContainer(props: ListContainerProps) {
{button && (
<Button
variant="accent"
data-test-id={`${props.type}-action-button`}
data-test-id={`${group}-action-button`}
onClick={button.onClick}
sx={{
position: "absolute",

View File

@@ -25,76 +25,71 @@ import TrashItem from "../trash-item";
import { db } from "../../common/db";
import { getTotalNotes } from "@notesnook/common";
import Reminder from "../reminder";
import { useMemo } from "react";
import { ReferencesWithDateEdited, Reference, Context } from "./types";
import {
ReferencesWithDateEdited,
ItemWrapper,
GroupingKey,
Item,
NotebookReference,
NotebookType,
Reference
} from "./types";
NotebookReference
} from "@notesnook/core/dist/types";
import { getSortValue } from "@notesnook/core/dist/utils/grouping";
const SINGLE_LINE_HEIGHT = 1.4;
const DEFAULT_LINE_HEIGHT =
(document.getElementById("p")?.clientHeight || 16) - 1;
export const DEFAULT_ITEM_HEIGHT = SINGLE_LINE_HEIGHT * 2 * DEFAULT_LINE_HEIGHT;
const NotesProfile: ItemWrapper = ({ item, type, context, compact }) => {
const references = useMemo(
() => getReferences(item.id, item.notebooks as Item[], context?.type),
[item, context]
);
return (
<Note
compact={compact}
item={item}
tags={getTags(item)}
references={references}
reminder={getReminder(item.id)}
date={getDate(item, type)}
context={context}
/>
);
type ListItemWrapperProps<TItem = Item> = {
group?: GroupingKey;
item: TItem;
context?: Context;
compact?: boolean;
};
export function ListItemWrapper(props: ListItemWrapperProps) {
const { item, group, compact, context } = props;
const { type } = item;
const NotebooksProfile: ItemWrapper = ({ item, type }) => (
<Notebook
item={item}
totalNotes={getTotalNotes(item)}
date={getDate(item, type)}
/>
);
const TrashProfile: ItemWrapper = ({ item, type }) => (
<TrashItem item={item} date={getDate(item, type)} />
);
export const ListProfiles = {
home: NotesProfile,
notebooks: NotebooksProfile,
notes: NotesProfile,
reminders: Reminder,
tags: Tag,
topics: Topic,
trash: TrashProfile
} as const;
function getTags(item: Item) {
let tags = item.tags as Item[];
if (tags)
tags = tags.slice(0, 3).reduce((prev, curr) => {
const tag = db.tags?.tag(curr);
if (tag) prev.push(tag);
return prev;
}, [] as Item[]);
return tags || [];
switch (type) {
case "note": {
const tags = db.relations.to(item, "tag").resolved(3) || [];
const color = db.relations.to(item, "color").resolved(1)?.[0];
const references = getReferences(item.id, item.notebooks, context?.type);
return (
<Note
compact={compact}
item={item}
tags={tags}
color={color}
references={references}
reminder={getReminder(item.id)}
date={getDate(item, group)}
context={context}
/>
);
}
case "notebook":
return (
<Notebook
item={item}
totalNotes={getTotalNotes(item)}
date={getDate(item, group)}
/>
);
case "trash":
return <TrashItem item={item} date={getDate(item, type)} />;
case "reminder":
return <Reminder item={item} />;
case "topic":
return <Topic item={item} />;
case "tag":
return <Tag item={item} />;
default:
return null;
}
}
function getReferences(
noteId: string,
notebooks: Item[],
notebooks?: NotebookReference[],
contextType?: string
): ReferencesWithDateEdited | undefined {
if (["topic", "notebook"].includes(contextType || "")) return;
@@ -104,12 +99,13 @@ function getReferences(
db.relations
?.to({ id: noteId, type: "note" }, "notebook")
?.forEach((notebook: any) => {
?.resolved()
.forEach((notebook) => {
references.push({
type: "notebook",
url: `/notebooks/${notebook.id}`,
title: notebook.title
} as Reference);
});
if (latestDateEdited < notebook.dateEdited)
latestDateEdited = notebook.dateEdited;
@@ -117,10 +113,10 @@ function getReferences(
notebooks?.forEach((curr) => {
const topicId = (curr as NotebookReference).topics[0];
const notebook = db.notebooks?.notebook(curr.id)?.data as NotebookType;
const notebook = db.notebooks.notebook(curr.id)?.data;
if (!notebook) return;
const topic = notebook.topics.find((t: Item) => t.id === topicId);
const topic = notebook.topics.find((t) => t.id === topicId);
if (!topic) return;
references.push({
@@ -136,21 +132,20 @@ function getReferences(
}
function getReminder(noteId: string) {
return db.relations?.from({ id: noteId, type: "note" }, "reminder")[0];
return db.relations
?.from({ id: noteId, type: "note" }, "reminder")
.resolved(1)[0];
}
function getDate(item: Item, groupType: keyof typeof ListProfiles): number {
const sortBy = db.settings?.getGroupOptions(groupType).sortBy;
switch (sortBy) {
case "dateEdited":
return item.dateEdited;
case "dateCreated":
return item.dateCreated;
case "dateModified":
return item.dateModified;
case "dateDeleted":
return item.dateDeleted;
default:
return item.dateCreated;
}
function getDate(item: Item, groupType?: GroupingKey): number {
return getSortValue(
groupType
? db.settings.getGroupOptions(groupType)
: {
groupBy: "default",
sortBy: "dateEdited",
sortDirection: "desc"
},
item
);
}

View File

@@ -17,34 +17,14 @@ 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 { ListProfiles } from "./list-profiles";
import { Note } from "@notesnook/core/dist/types";
export type Item = {
id: string;
export type Context = {
type: string;
title: string;
dateEdited: number;
dateModified: number;
dateDeleted: number;
dateCreated: number;
notes?: Note[];
value?: { topic?: string };
} & Record<string, unknown>;
export type NotebookReference = Item & { topics: string[] };
export type NotebookType = Item & { topics: Item[] };
export type Context = { type: string } & Record<string, unknown>;
export type ItemWrapperProps<TItem = Item> = {
item: TItem;
type: keyof typeof ListProfiles;
context?: Context;
compact?: boolean;
};
export type ItemWrapper<TItem = Item> = (
props: ItemWrapperProps<TItem>
) => JSX.Element | null;
export type Reference = {
type: "topic" | "notebook";
url: string;

View File

@@ -25,9 +25,9 @@ import {
import { useMenuTrigger } from "../../hooks/use-menu";
import React, { useRef } from "react";
import { SchemeColors } from "@notesnook/theme";
import { Item } from "../list-container/types";
import { MenuItem } from "@notesnook/ui";
import { alpha } from "@theme-ui/color";
import { Item } from "@notesnook/core/dist/types";
type ListItemProps = {
colors?: {

View File

@@ -207,7 +207,7 @@ function NavigationMenu(props: NavigationMenuProps) {
index={index}
isTablet={isTablet}
key={color.id}
title={db.colors?.alias(color.id)}
title={color.title}
icon={Circle}
selected={location === `/colors/${color.id}`}
color={color.title.toLowerCase()}
@@ -237,16 +237,14 @@ function NavigationMenu(props: NavigationMenuProps) {
index={colors.length - 1 + index}
isTablet={isTablet}
key={item.id}
title={
item.type === "tag" ? db.tags?.alias(item.id) : item.title
}
title={item.title}
menuItems={[
{
type: "button",
key: "removeshortcut",
title: "Remove shortcut",
onClick: async () => {
await db.shortcuts?.remove(item.id);
await db.shortcuts.remove(item.id);
refreshNavItems();
}
}

View File

@@ -49,8 +49,8 @@ import {
AddToNotebook,
RemoveShortcutLink,
Plus,
Tag,
Copy
Tag as TagIcon
} from "../icons";
import TimeAgo from "../time-ago";
import ListItem from "../list-item";
@@ -62,6 +62,8 @@ 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 { store as tagStore } from "../../stores/tag-store";
import { useStore as useAttachmentStore } from "../../stores/attachment-store";
import { db } from "../../common/db";
import { showUnpinnedToast } from "../../common/toasts";
@@ -74,24 +76,29 @@ import { exportNote, exportNotes, exportToPDF } from "../../common/export";
import { Multiselect } from "../../common/multi-select";
import { store as selectionStore } from "../../stores/selection-store";
import {
Reminder as ReminderType,
isReminderActive,
isReminderToday
} from "@notesnook/core/dist/collections/reminders";
import { getFormattedReminderTime } from "@notesnook/common";
import { MenuItem } from "@notesnook/ui";
import {
Context,
Item,
ReferencesWithDateEdited
} from "../list-container/types";
import { SchemeColors } from "@notesnook/theme";
import { SchemeColors, StaticColors } from "@notesnook/theme";
import FileSaver from "file-saver";
import {
Reminder as ReminderType,
Tag,
Color,
Note
} from "@notesnook/core/dist/types";
import { MenuItem } from "@notesnook/ui";
type NoteProps = {
tags: Item[];
tags: Tag[];
color?: Color;
references?: ReferencesWithDateEdited;
item: Item;
item: Note;
context?: Context;
date: number;
reminder?: ReminderType;
@@ -100,7 +107,8 @@ type NoteProps = {
};
function Note(props: NoteProps) {
const { tags, references, item, date, reminder, simplified, compact } = props;
const { tags, color, references, item, date, reminder, simplified, compact } =
props;
const note = item;
const isOpened = useStore((store) => store.selectedNote === note.id);
@@ -111,9 +119,7 @@ function Note(props: NoteProps) {
() => attachments.filter((a) => a.failed),
[attachments]
);
const primary: SchemeColors = !note.color
? "accent-selected"
: (note.color as string).toLowerCase();
const primary: SchemeColors = color ? color.colorCode : "accent-selected";
return (
<ListItem
@@ -133,7 +139,7 @@ function Note(props: NoteProps) {
}}
colors={{
accent: primary,
heading: note.color ? primary : "heading",
heading: color ? primary : "heading",
background: "background"
}}
menuItems={menuItems}
@@ -230,13 +236,13 @@ function Note(props: NoteProps) {
{note.favorite && <Star color={primary} size={15} />}
{tags?.map((tag) => {
{tags.map((tag) => {
return (
<Button
data-test-id={`tag-item`}
key={tag.id}
variant="anchor"
title={`Go to #${tag.alias}`}
title={`Go to #${tag.title}`}
onClick={(e) => {
e.stopPropagation();
if (!tag.id) return showToast("error", "Tag not found.");
@@ -249,7 +255,7 @@ function Note(props: NoteProps) {
color: "var(--paragraph-secondary)"
}}
>
#{tag.alias}
#{tag.title}
</Button>
);
})}
@@ -282,7 +288,7 @@ export default React.memo(Note, function (prevProps, nextProps) {
);
});
const pin = (note: Item) => {
const pin = (note: Note) => {
return store
.pin(note.id)
.then(async () => {
@@ -327,11 +333,11 @@ const formats = [
const notFullySyncedText =
"Cannot perform this action because note is not fully synced.";
const menuItems: (note: any, items?: any[]) => MenuItem[] = (
const menuItems: (note: Note, items?: Note[]) => MenuItem[] = (
note,
items = []
) => {
const isSynced = db.notes?.note(note.id).synced();
const isSynced = db.notes.note(note.id)?.synced();
const ids = items.map((i) => i.id);
return [
@@ -433,13 +439,13 @@ const menuItems: (note: any, items?: any[]) => MenuItem[] = (
type: "button",
key: "publish",
isDisabled:
!isSynced || (!db.monographs?.isPublished(note.id) && note.locked),
!isSynced || (!db.monographs.isPublished(note.id) && note.locked),
icon: Publish.path,
title: "Publish",
isChecked: db.monographs?.isPublished(note.id),
isChecked: db.monographs.isPublished(note.id),
onClick: async () => {
const isPublished = db.monographs?.isPublished(note.id);
if (isPublished) await db.monographs?.unpublish(note.id);
const isPublished = db.monographs.isPublished(note.id);
if (isPublished) await db.monographs.unpublish(note.id);
else await showPublishView(note.id, "bottom");
}
},
@@ -568,22 +574,22 @@ const menuItems: (note: any, items?: any[]) => MenuItem[] = (
];
};
function colorsToMenuItems(note: any): MenuItem[] {
return COLORS.map((label) => {
const lowercase = label.toLowerCase();
function colorsToMenuItems(note: Note): MenuItem[] {
const noteColor = db.relations.to(note, "color").resolved(1)[0];
return COLORS.map((color) => {
return {
type: "button",
key: lowercase,
title: db.colors?.alias(lowercase) || label,
key: color.key,
title: color.title,
icon: Circle.path,
styles: { icon: { color: lowercase } },
isChecked: note.color === lowercase,
onClick: () => store.setColor(note.id, lowercase)
};
styles: { icon: { color: StaticColors[color.key] } },
isChecked: noteColor.title === color.title,
onClick: () => store.setColor(note.id, color.title)
} satisfies MenuItem;
});
}
function notebooksMenuItems(items: any[]): MenuItem[] {
function notebooksMenuItems(items: Note[]): MenuItem[] {
const noteIds = items.map((i) => i.id);
const menuItems: MenuItem[] = [];
@@ -596,11 +602,11 @@ function notebooksMenuItems(items: any[]): MenuItem[] {
});
const notebooks = items
.map((note) => db.relations?.to(note, "notebook"))
.map((note) => db.relations.to(note, "notebook").resolved())
.flat();
const topics = items.map((note) => note.notebooks || []).flat();
if (topics?.length > 0 || notebooks?.length > 0) {
if (topics?.length > 0 || notebooks.length > 0) {
menuItems.push(
{
type: "button",
@@ -608,39 +614,40 @@ function notebooksMenuItems(items: any[]): MenuItem[] {
title: "Unlink from all",
icon: RemoveShortcutLink.path,
onClick: async () => {
await db.notes?.removeFromAllNotebooks(...noteIds);
await db.notes.removeFromAllNotebooks(...noteIds);
store.refresh();
}
},
{ key: "sep", type: "separator" }
);
notebooks?.forEach((notebook) => {
notebooks.forEach((notebook) => {
if (!notebook || menuItems.find((item) => item.key === notebook.id))
return;
menuItems.push({
type: "button",
key: notebook.id,
title: db.notebooks?.notebook(notebook.id).title,
title: notebook.title,
icon: Notebook.path,
isChecked: true,
tooltip: "Click to remove from this notebook",
onClick: async () => {
await db.notes?.removeFromNotebook({ id: notebook.id }, ...noteIds);
await db.notes.removeFromNotebook({ id: notebook.id }, ...noteIds);
store.refresh();
}
});
});
topics?.forEach((ref) => {
const notebook = db.notebooks?.notebook(ref.id);
topics.forEach((ref) => {
const notebook = db.notebooks.notebook(ref.id);
if (!notebook) return;
for (const topicId of ref.topics) {
if (!notebook.topics.topic(topicId)) continue;
if (menuItems.find((item) => item.key === topicId)) continue;
const topic = notebook.topics.topic(topicId)._topic;
const topic = notebook.topics.topic(topicId)?._topic;
if (!topic) continue;
menuItems.push({
type: "button",
key: topicId,
@@ -649,7 +656,7 @@ function notebooksMenuItems(items: any[]): MenuItem[] {
isChecked: true,
tooltip: "Click to remove from this topic",
onClick: async () => {
await db.notes?.removeFromNotebook(
await db.notes.removeFromNotebook(
{ id: ref.id, topic: topic.id },
...noteIds
);
@@ -663,7 +670,7 @@ function notebooksMenuItems(items: any[]): MenuItem[] {
return menuItems;
}
function tagsMenuItems(items: any[]): MenuItem[] {
function tagsMenuItems(items: Note[]): MenuItem[] {
const noteIds = items.map((i) => i.id);
const menuItems: MenuItem[] = [];
@@ -677,9 +684,11 @@ function tagsMenuItems(items: any[]): MenuItem[] {
}
});
const tags = items.map((note) => note.tags).flat();
const tags = items
.map((note) => db.relations.to(note, "tag").resolved())
.flat();
if (tags?.length > 0) {
if (tags.length > 0) {
menuItems.push(
{
type: "button",
@@ -689,32 +698,34 @@ function tagsMenuItems(items: any[]): MenuItem[] {
onClick: async () => {
for (const note of items) {
for (const tag of tags) {
if (!note.tags.includes(tag)) continue;
await db.notes?.note(note).untag(tag);
await db.relations.unlink(tag, note);
}
}
store.refresh();
tagStore.get().refresh();
editorStore.get().refreshTags();
store.get().refresh();
}
},
{ key: "sep", type: "separator" }
);
tags?.forEach((tag) => {
if (menuItems.find((item) => item.key === tag)) return;
tags.forEach((tag) => {
if (menuItems.find((item) => item.key === tag.id)) return;
menuItems.push({
type: "button",
key: tag,
title: db.tags?.alias(tag),
icon: Tag.path,
key: tag.id,
title: tag.title,
icon: TagIcon.path,
isChecked: true,
tooltip: "Click to remove from this tag",
onClick: async () => {
for (const note of items) {
if (!note.tags.includes(tag)) continue;
await db.notes?.note(note).untag(tag);
await db.relations.unlink(tag, note);
}
store.refresh();
tagStore.get().refresh();
editorStore.get().refreshTags();
store.get().refresh();
}
});
});

View File

@@ -25,7 +25,7 @@ import { store as appStore } from "../../stores/app-store";
import { showUnpinnedToast } from "../../common/toasts";
import { db } from "../../common/db";
import {
Topic,
Topic as TopicIcon,
PinFilled,
NotebookEdit,
Notebook as NotebookIcon,
@@ -42,10 +42,10 @@ import { pluralize } from "@notesnook/common";
import { confirm } from "../../common/dialog-controller";
import { getFormattedDate } from "@notesnook/common";
import { MenuItem } from "@notesnook/ui";
import { Item } from "../list-container/types";
import { Note, Notebook } from "@notesnook/core/dist/types";
type NotebookProps = {
item: Item;
item: Notebook;
totalNotes: number;
date: number;
simplified?: boolean;
@@ -76,11 +76,11 @@ function Notebook(props: NotebookProps) {
<>
{notebook?.topics && (
<Flex mb={1} sx={{ gap: 1 }}>
{(notebook?.topics as Item[]).slice(0, 3).map((topic) => (
{notebook.topics.slice(0, 3).map((topic) => (
<IconTag
key={topic.id}
text={topic.title}
icon={Topic}
icon={TopicIcon}
onClick={() => {
navigate(`/notebooks/${notebook.id}/${topic.id}`);
}}
@@ -128,7 +128,7 @@ export default React.memo(Notebook, (prev, next) => {
);
});
const pin = (notebook: Item) => {
const pin = (notebook: Notebook) => {
return store
.pin(notebook.id)
.then(() => {
@@ -137,11 +137,11 @@ const pin = (notebook: Item) => {
.catch((error) => showToast("error", error.message));
};
const menuItems: (notebook: any, items?: any[]) => MenuItem[] = (
const menuItems: (notebook: Notebook, items?: Notebook[]) => MenuItem[] = (
notebook,
items = []
) => {
const defaultNotebook = db.settings?.getDefaultNotebook();
const defaultNotebook = db.settings.getDefaultNotebook();
return [
{
@@ -158,10 +158,10 @@ const menuItems: (notebook: any, items?: any[]) => MenuItem[] = (
isChecked: defaultNotebook?.id === notebook.id && !defaultNotebook?.topic,
icon: NotebookIcon.path,
onClick: async () => {
const defaultNotebook = db.settings?.getDefaultNotebook();
const defaultNotebook = db.settings.getDefaultNotebook();
const isDefault =
defaultNotebook?.id === notebook.id && !defaultNotebook?.topic;
await db.settings?.setDefaultNotebook(
await db.settings.setDefaultNotebook(
isDefault ? undefined : { id: notebook.id }
);
}
@@ -177,10 +177,10 @@ const menuItems: (notebook: any, items?: any[]) => MenuItem[] = (
{
type: "button",
key: "shortcut",
icon: db.shortcuts?.exists(notebook.id)
icon: db.shortcuts.exists(notebook.id)
? RemoveShortcutLink.path
: Shortcut.path,
title: db.shortcuts?.exists(notebook.id)
title: db.shortcuts.exists(notebook.id)
? "Remove shortcut"
: "Create shortcut",
onClick: () => appStore.addToShortcuts(notebook)
@@ -208,13 +208,13 @@ const menuItems: (notebook: any, items?: any[]) => MenuItem[] = (
if (result) {
if (result.deleteContainingNotes) {
const notes = [];
const notes: Note[] = [];
for (const item of items) {
notes.push(...(db.relations?.from(item, "note") || []));
const topics = db.notebooks?.notebook(item.id).topics;
notes.push(...(db.relations.from(item, "note").resolved() || []));
const topics = db.notebooks.topics(item.id);
if (!topics) return;
for (const topic of topics.all) {
notes.push(...topics.topic(topic.id).all);
notes.push(...(topics.topic(topic.id)?.all || []));
}
}
await Multiselect.moveNotesToTrash(notes, false);

View File

@@ -105,10 +105,9 @@ function Properties(props) {
dateCreated
} = session;
const isPreviewMode = sessionType === "preview";
const reminders = db.relations.from(
{ id: session.id, type: "note" },
"reminder"
);
const reminders = db.relations
.from({ id: session.id, type: "note" }, "reminder")
.resolved();
const allNotebooks = useMemo(
() =>
[
@@ -289,7 +288,7 @@ function Properties(props) {
))}
</Card>
)}
{reminders?.length > 0 && (
{reminders.length > 0 && (
<Card title="Reminders">
{reminders.map((reminder) => {
return (
@@ -299,7 +298,7 @@ function Properties(props) {
</Card>
)}
{attachments?.length > 0 && (
{attachments.length > 0 && (
<Card
title="Attachments"
subtitle={`${attachments.length} attachments | ${formatBytes(

View File

@@ -32,10 +32,7 @@ import {
Trash
} from "../icons";
import IconTag from "../icon-tag";
import {
Reminder as ReminderType,
isReminderToday
} from "@notesnook/core/dist/collections/reminders";
import { isReminderToday } from "@notesnook/core/dist/collections/reminders";
import { hashNavigate } from "../../navigation";
import { Multiselect } from "../../common/multi-select";
import { store } from "../../stores/reminder-store";
@@ -46,8 +43,8 @@ import {
} from "../../common/dialog-controller";
import { pluralize } from "@notesnook/common";
import { getFormattedReminderTime } from "@notesnook/common";
import { Item } from "../list-container/types";
import { MenuItem } from "@notesnook/ui";
import { Reminder as ReminderType } from "@notesnook/core/dist/types";
const RECURRING_MODE_MAP = {
week: "Weekly",
@@ -62,7 +59,7 @@ const PRIORITY_ICON_MAP = {
} as const;
type ReminderProps = {
item: Item;
item: ReminderType;
simplified?: boolean;
};
@@ -132,7 +129,7 @@ const menuItems: (
title: reminder.disabled ? "Activate" : "Deactivate",
icon: reminder.disabled ? Reminders.path : ReminderOff.path,
onClick: async () => {
await db.reminders?.add({
await db.reminders.add({
id: reminder.id,
disabled: !reminder.disabled
});

View File

@@ -44,6 +44,7 @@ import useStatus, { statusToString } from "../../hooks/use-status";
import { ScopedThemeProvider } from "../theme-provider";
import { checkForUpdate, installUpdate } from "../../utils/updater";
import { toTitleCase } from "@notesnook/common";
import { User } from "@notesnook/core/dist/api/user-manager";
function StatusBar() {
const user = useUserStore((state) => state.user);
@@ -84,12 +85,7 @@ function StatusBar() {
}}
>
<Circle size={7} color={"var(--icon-error)"} />
<Text
className="selectable"
variant="subBody"
ml={1}
sx={{ color: "paragraph" }}
>
<Text variant="subBody" ml={1} sx={{ color: "paragraph" }}>
Email not confirmed
</Text>
</Button>

View File

@@ -28,13 +28,14 @@ import { db } from "../../common/db";
import { Edit, Shortcut, DeleteForver } from "../icons";
import { showToast } from "../../utils/toast";
import { pluralize } from "@notesnook/common";
import { Item } from "../list-container/types";
import { MenuItem } from "@notesnook/ui";
import { Tag } from "@notesnook/core/dist/types";
type TagProps = { item: Item };
type TagProps = { item: Tag };
function Tag(props: TagProps) {
const { item } = props;
const { id, noteIds, alias } = item;
const { id, title } = item;
const totalNotes = db.relations.to(item, "note").length;
return (
<ListItem
@@ -45,12 +46,12 @@ function Tag(props: TagProps) {
<Text as="span" sx={{ color: "accent" }}>
{"#"}
</Text>
{alias}
{title}
</Text>
}
footer={
<Text mt={1} variant="subBody">
{(noteIds as string[]).length}
{totalNotes}
</Text>
}
menuItems={menuItems}
@@ -62,7 +63,7 @@ function Tag(props: TagProps) {
}
export default Tag;
const menuItems: (tag: any, items?: any[]) => MenuItem[] = (
const menuItems: (tag: Tag, items?: Tag[]) => MenuItem[] = (
tag,
items = []
) => {
@@ -79,7 +80,7 @@ const menuItems: (tag: any, items?: any[]) => MenuItem[] = (
{
type: "button",
key: "shortcut",
title: db.shortcuts?.exists(tag.id)
title: db.shortcuts.exists(tag.id)
? "Remove shortcut"
: "Create shortcut",
icon: Shortcut.path,
@@ -94,11 +95,10 @@ const menuItems: (tag: any, items?: any[]) => MenuItem[] = (
icon: DeleteForver.path,
onClick: async () => {
for (const tag of items) {
if (tag.noteIds.includes(editorStore.get().session.id))
await editorStore.clearSession();
await db.tags?.remove(tag.id);
await db.tags.remove(tag.id);
}
showToast("success", `${pluralize(items.length, "tag")} deleted`);
editorStore.refreshTags();
tagStore.refresh();
noteStore.refresh();
},

View File

@@ -22,21 +22,21 @@ import ListItem from "../list-item";
import { db } from "../../common/db";
import { store as appStore } from "../../stores/app-store";
import { hashNavigate, navigate } from "../../navigation";
import { Flex, Text } from "@theme-ui/components";
import { Text } from "@theme-ui/components";
import { Edit, Topic as TopicIcon, Shortcut, Trash } from "../icons";
import { Multiselect } from "../../common/multi-select";
import { confirm } from "../../common/dialog-controller";
import { useStore as useNotesStore } from "../../stores/note-store";
import { pluralize } from "@notesnook/common";
import { getTotalNotes } from "@notesnook/common";
import { Item } from "../list-container/types";
import { MenuItem } from "@notesnook/ui";
import { Note, Topic } from "@notesnook/core/dist/types";
type TopicProps = { item: Item };
type TopicProps = { item: Topic };
function Topic(props: TopicProps) {
const { item: topic } = props;
const isOpened = useNotesStore(
(store) => (store.context?.value as any)?.topic === topic.id
(store) => store.context?.value?.topic === topic.id
);
return (
@@ -56,11 +56,11 @@ export default React.memo(Topic, (prev, next) => {
return prev?.item?.title === next?.item?.title;
});
const menuItems: (topic: any, items?: any[]) => MenuItem[] = (
const menuItems: (topic: Topic, items?: Topic[]) => MenuItem[] = (
topic,
items = []
) => {
const defaultNotebook = db.settings?.getDefaultNotebook();
const defaultNotebook = db.settings.getDefaultNotebook();
return [
{
type: "button",
@@ -79,12 +79,12 @@ const menuItems: (topic: any, items?: any[]) => MenuItem[] = (
defaultNotebook?.topic === topic.id,
icon: TopicIcon.path,
onClick: async () => {
const defaultNotebook = db.settings?.getDefaultNotebook();
const defaultNotebook = db.settings.getDefaultNotebook();
const isDefault =
defaultNotebook?.id === topic.notebookId &&
defaultNotebook?.topic === topic.id;
await db.settings?.setDefaultNotebook(
await db.settings.setDefaultNotebook(
isDefault ? undefined : { id: topic.notebookId, topic: topic.id }
);
}
@@ -92,7 +92,7 @@ const menuItems: (topic: any, items?: any[]) => MenuItem[] = (
{
type: "button",
key: "shortcut",
title: db.shortcuts?.exists(topic.id)
title: db.shortcuts.exists(topic.id)
? "Remove shortcut"
: "Create shortcut",
icon: Shortcut.path,
@@ -121,11 +121,9 @@ const menuItems: (topic: any, items?: any[]) => MenuItem[] = (
if (result) {
if (result.deleteContainingNotes) {
const notes = [];
const notes: Note[] = [];
for (const item of items) {
const topic = db.notebooks
?.notebook(item.notebookId)
.topics.topic(item.id);
const topic = db.notebooks.topics(item.notebookId).topic(item.id);
if (!topic) continue;
notes.push(...topic.all);
}

View File

@@ -28,10 +28,10 @@ import { showUndoableToast } from "../../common/toasts";
import { showToast } from "../../utils/toast";
import { hashNavigate } from "../../navigation";
import { useStore } from "../../stores/note-store";
import { Item } from "../list-container/types";
import { MenuItem } from "@notesnook/ui";
import { TrashItem } from "@notesnook/core/dist/types";
type TrashItemProps = { item: Item; date: number };
type TrashItemProps = { item: TrashItem; date: number };
function TrashItem(props: TrashItemProps) {
const { item, date } = props;
const isOpened = useStore((store) => store.selectedNote === item.id);
@@ -41,7 +41,7 @@ function TrashItem(props: TrashItemProps) {
isFocused={isOpened}
item={item}
title={item.title}
body={(item.headline || item.description) as string}
body={item.itemType === "note" ? item.headline : item.description}
footer={
<Flex
mt={1}
@@ -68,7 +68,7 @@ function TrashItem(props: TrashItemProps) {
}
export default TrashItem;
const menuItems: (item: any, items?: any[]) => MenuItem[] = (
const menuItems: (item: TrashItem, items?: TrashItem[]) => MenuItem[] = (
item,
items = []
) => {

View File

@@ -40,7 +40,7 @@ function Unlock(props: UnlockProps) {
const passwordRef = useRef<HTMLInputElement>();
const note = useMemo(
() => !isLoading && db.notes?.note(noteId)?.data,
() => (!isLoading ? db.notes.note(noteId)?.data : undefined),
[noteId, isLoading]
);
const openLockedSession = useEditorStore((store) => store.openLockedSession);
@@ -53,7 +53,7 @@ function Unlock(props: UnlockProps) {
const password = passwordRef.current.value;
try {
if (!password) return;
const note = await db.vault?.open(noteId, password);
const note = await db.vault.open(noteId, password);
openLockedSession(note);
} catch (e) {
if (

View File

@@ -110,10 +110,7 @@ class AddNotebookDialog extends React.Component {
const notebook = {
title: this.title,
description: this.description,
topics: this.state.topics.map((topic) => {
if (topic.id) return topic;
return topic.title;
}),
topics: this.state.topics,
id: this.id
};
this.props.onDone(notebook, this.deletedTopics);

View File

@@ -120,7 +120,7 @@ export default function AddReminderDialog(props: AddReminderDialogProps) {
useEffect(() => {
if (!reminderId) return;
const reminder = db.reminders?.reminder(reminderId);
const reminder = db.reminders.reminder(reminderId);
if (!reminder) return;
setSelectedDays(reminder.selectedDays || []);
@@ -135,7 +135,7 @@ export default function AddReminderDialog(props: AddReminderDialogProps) {
useEffect(() => {
if (!noteId) return;
const note = db.notes?.note(noteId);
const note = db.notes.note(noteId);
if (!note) return;
setTitle(note.title);
@@ -186,7 +186,7 @@ export default function AddReminderDialog(props: AddReminderDialogProps) {
return;
}
const id = await db.reminders?.add({
const id = await db.reminders.add({
id: reminderId,
recurringMode,
mode,
@@ -200,7 +200,7 @@ export default function AddReminderDialog(props: AddReminderDialogProps) {
});
if (id && noteId) {
await db.relations?.add(
await db.relations.add(
{ id: noteId, type: "note" },
{ id, type: "reminder" }
);

View File

@@ -28,8 +28,10 @@ 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 { Perform } from "../common/dialog-controller";
import { FilteredList } from "../components/filtered-list";
import { ItemReference, Tag } from "@notesnook/core/dist/types";
type SelectedReference = {
id: string;
@@ -42,7 +44,6 @@ type Item = {
type: "tag" | "header";
title: string;
};
type Tag = Item & { noteIds: string[] };
export type AddTagsDialogProps = {
onClose: Perform;
@@ -63,7 +64,7 @@ function AddTagsDialog(props: AddTagsDialogProps) {
const getAllTags = useCallback(() => {
refreshTags();
return (store.get().tags as Item[]).filter((a) => a.type !== "header");
return store.get().tags.filter((a) => a.type !== "header");
}, [refreshTags]);
useEffect(() => {
@@ -71,7 +72,7 @@ function AddTagsDialog(props: AddTagsDialogProps) {
setSelected((s) => {
const selected = s.slice();
for (const tag of tags as Tag[]) {
for (const tag of tags) {
if (tag.type === "header") continue;
if (selected.findIndex((a) => a.id === tag.id) > -1) continue;
if (tagHasNotes(tag, noteIds)) {
@@ -98,11 +99,15 @@ function AddTagsDialog(props: AddTagsDialogProps) {
onClick: async () => {
for (const id of noteIds) {
for (const item of selected) {
if (item.op === "add") await db.notes?.note(id).tag(item.id);
else await db.notes?.note(id).untag(item.id);
const tagRef: ItemReference = { type: "tag", id: item.id };
const noteRef: ItemReference = { id, type: "note" };
if (item.op === "add") await db.relations.add(tagRef, noteRef);
else await db.relations.unlink(tagRef, noteRef);
}
}
notestore.refresh();
editorstore.get().refreshTags();
store.get().refresh();
notestore.get().refresh();
onClose(true);
}
}}
@@ -122,12 +127,13 @@ function AddTagsDialog(props: AddTagsDialogProps) {
empty: "Add a new tag",
filter: "Search or add a new tag"
}}
filter={(tags, query) => db.lookup?.tags(tags, query) || []}
filter={(tags, query) => db.lookup.tags(tags, query) || []}
onCreateNewItem={async (title) => {
const tag = await db.tags?.add(title);
const tagId = await db.tags.add({ title });
if (!tagId) return;
setSelected((selected) => [
...selected,
{ id: tag.id, new: true, op: "add" }
{ id: tagId, new: true, op: "add" }
]);
}}
renderItem={(tag, _index) => {
@@ -219,5 +225,7 @@ function SelectedCheck({
}
function tagHasNotes(tag: Tag, noteIds: string[]) {
return tag.noteIds.some((id) => noteIds.indexOf(id) > -1);
return db.relations
?.from(tag, "note")
?.some((r) => noteIds.indexOf(r.to.id) > -1);
}

View File

@@ -156,7 +156,7 @@ function AttachmentsDialog({ onClose }: AttachmentsDialogProps) {
totalSize={totalSize}
filter={(query) => {
setAttachments(
db.lookup?.attachments(db.attachments?.all || [], query) || []
db.lookup?.attachments(db.attachments.all || [], query) || []
);
}}
counts={getCounts(allAttachments)}
@@ -446,7 +446,7 @@ const Sidebar = memo(
if (downloadStatus) {
await cancelDownload();
} else {
await download(db.attachments?.all);
await download(db.attachments.all);
}
}}
>

View File

@@ -46,6 +46,7 @@ import { SUBSCRIPTION_STATUS } from "../../common/constants";
import { alpha } from "@theme-ui/color";
import BaseDialog from "../../components/dialog";
import { ScopedThemeProvider } from "../../components/theme-provider";
import { User } from "@notesnook/core/dist/api/user-manager";
type BuyDialogProps = {
couponCode?: string;
@@ -178,7 +179,7 @@ function SideBar(props: SideBarProps) {
report({
text: "Activating trial"
});
return db.user?.activateTrial();
return db.user.activateTrial();
}
});
if (result) onClose();

View File

@@ -49,7 +49,7 @@ export default function EmailChangeDialog(props: EmailChangeDialogProps) {
async (emailChangeState: EmailChangeState) => {
setIsSending(true);
try {
await db.user?.sendVerificationEmail(emailChangeState.newEmail);
await db.user.sendVerificationEmail(emailChangeState.newEmail);
setEnabled(false);
} catch (e) {
@@ -98,7 +98,7 @@ export default function EmailChangeDialog(props: EmailChangeDialogProps) {
return;
}
await db.user?.changeEmail(
await db.user.changeEmail(
emailChangeState.newEmail,
emailChangeState.password,
code
@@ -109,12 +109,12 @@ export default function EmailChangeDialog(props: EmailChangeDialogProps) {
if (!newEmail.trim() || !password.trim()) return;
if (!password || !(await db.user?.verifyPassword(password))) {
if (!password || !(await db.user.verifyPassword(password))) {
setError("Password is not correct.");
return;
}
await db.user?.sendVerificationEmail(newEmail);
await db.user.sendVerificationEmail(newEmail);
setEmailChangeState({ newEmail, password });
} catch (e) {
if (e instanceof Error) setError(e.message);

View File

@@ -27,9 +27,9 @@ import { confirm, Perform } from "../common/dialog-controller";
import { isUserPremium } from "../hooks/use-is-user-premium";
import { writeText } from "clipboard-polyfill";
import { store as userstore } from "../stores/user-store";
import { db } from "../common/db";
import { ErrorText } from "../components/error-text";
import { Debug } from "@notesnook/core/dist/api/debug";
const PLACEHOLDERS = {
title: "Briefly describe what happened",
@@ -79,7 +79,7 @@ function IssueDialog(props: IssueDialogProps) {
if (!requestData.title.trim() || !requestData.body.trim()) return;
requestData.body = BODY_TEMPLATE(requestData.body);
const url = await db.debug?.report({
const url = await Debug.report({
title: requestData.title,
body: requestData.body,
userId: userstore.get().user?.id

View File

@@ -28,8 +28,9 @@ import {
Step,
steps
} from "./steps";
import { Authenticator, AuthenticatorType, OnNextFunction } from "./types";
import { Authenticator, OnNextFunction } from "./types";
import { ErrorText } from "../../components/error-text";
import { AuthenticatorType } from "@notesnook/core/dist/api/user-manager";
type MultifactorDialogProps = {
onClose: Perform;

View File

@@ -21,8 +21,8 @@ import { useState } from "react";
import { Perform } from "../../common/dialog-controller";
import Dialog from "../../components/dialog";
import { steps } from "./steps";
import { AuthenticatorType } from "./types";
import { ErrorText } from "../../components/error-text";
import { AuthenticatorType } from "@notesnook/core/dist/api/user-manager";
type RecoveryCodesDialogProps = {
onClose: Perform;

View File

@@ -50,7 +50,6 @@ import { ReactComponent as MFA } from "../../assets/mfa.svg";
import { ReactComponent as Fallback2FA } from "../../assets/fallback2fa.svg";
import {
Authenticator,
AuthenticatorType,
StepComponent,
SubmitCodeFunction,
StepComponentProps,
@@ -58,6 +57,7 @@ import {
} from "./types";
import { showMultifactorDialog } from "../../common/dialog-controller";
import { ErrorText } from "../../components/error-text";
import { AuthenticatorType } from "@notesnook/core/dist/api/user-manager";
const QRCode = React.lazy(() => import("../../re-exports/react-qrcode-logo"));
@@ -312,8 +312,8 @@ function AuthenticatorSelector(props: AuthenticatorSelectorProps) {
const onSubmitCode: SubmitCodeFunction = useCallback(
async (code) => {
try {
if (isFallback) await db.mfa?.enableFallback(authenticator, code);
else await db.mfa?.enable(authenticator, code);
if (isFallback) await db.mfa.enableFallback(authenticator, code);
else await db.mfa.enable(authenticator, code);
onNext(authenticator);
} catch (e) {
const error = e as Error;
@@ -342,7 +342,7 @@ function SetupAuthenticatorApp(props: SetupAuthenticatorProps) {
useEffect(() => {
(async function () {
setAuthenticatorDetails(await db.mfa?.setup("app"));
setAuthenticatorDetails(await db.mfa.setup("app"));
})();
}, []);
@@ -426,7 +426,7 @@ function SetupEmail(props: SetupAuthenticatorProps) {
useEffect(() => {
(async () => {
if (!db.user) return;
const { email } = await db.user.getUser();
const { email } = (await db.user.getUser()) || {};
setEmail(email);
})();
}, []);
@@ -461,7 +461,7 @@ function SetupEmail(props: SetupAuthenticatorProps) {
onClick={async () => {
setIsSending(true);
try {
await db.mfa?.setup("email");
await db.mfa.setup("email");
setEnabled(false);
} catch (e) {
const error = e as Error;
@@ -548,7 +548,7 @@ function SetupSMS(props: SetupAuthenticatorProps) {
setIsSending(true);
try {
await db.mfa?.setup("sms", phoneNumber);
await db.mfa.setup("sms", phoneNumber);
setEnabled(false);
} catch (e) {
const error = e as Error;
@@ -572,7 +572,7 @@ function BackupRecoveryCodes(props: TwoFactorEnabledProps) {
const generate = useCallback(async () => {
onError && onError("");
try {
const codes = await db.mfa?.codes();
const codes = await db.mfa.codes();
if (codes) setCodes(codes);
} catch (e) {
const error = e as Error;

View File

@@ -17,11 +17,10 @@ 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 { AuthenticatorType } from "@notesnook/core/dist/api/user-manager";
import { Perform } from "../../common/dialog-controller";
import { Icon } from "../../components/icons";
export type AuthenticatorType = "app" | "sms" | "email";
export type Authenticator = {
type: AuthenticatorType;
title: string;

View File

@@ -64,7 +64,7 @@ export default function MigrationDialog(props: MigrationDialogProps) {
);
task({ text: `Processing...` });
try {
await db.migrations?.migrate();
await db.migrations.migrate();
props.onClose(true);
} catch (e) {

View File

@@ -39,6 +39,7 @@ import { pluralize } from "@notesnook/common";
import { isMac } from "../utils/platform";
import { create } from "zustand";
import { FilteredList } from "../components/filtered-list";
import { Topic, Notebook, GroupHeader } from "@notesnook/core/dist/types";
type MoveDialogProps = { onClose: Perform; noteIds: string[] };
type NotebookReference = {
@@ -47,17 +48,7 @@ type NotebookReference = {
new: boolean;
op: "add" | "remove";
};
type Item = {
id: string;
type: "topic" | "notebook" | "header";
title: string;
};
type Topic = Item & { notebookId: string };
type Notebook = Item & {
topics: Topic[];
dateCreated: number;
dateModified: number;
};
type Item = Topic | Notebook | GroupHeader;
interface ISelectionStore {
selected: NotebookReference[];
@@ -81,9 +72,7 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
const notebooks = useStore((store) => store.notebooks);
const getAllNotebooks = useCallback(() => {
refreshNotebooks();
return (store.get().notebooks as Notebook[]).filter(
(a) => a.type !== "header"
);
return store.get().notebooks.filter((a) => a.type !== "header");
}, [refreshNotebooks]);
useEffect(() => {
@@ -92,7 +81,7 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
const selected: NotebookReference[] = useSelectionStore
.getState()
.selected.slice();
for (const notebook of notebooks as Notebook[]) {
for (const notebook of notebooks) {
if (notebook.type === "header") continue;
for (const topic of notebook.topics) {
const isSelected =
@@ -111,7 +100,7 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
}
for (const notebook of noteIds
.map((id) => db.relations?.to({ id, type: "note" }, "notebook"))
.map((id) => db.relations.to({ id, type: "note" }, "notebook"))
.flat()) {
const isSelected =
notebook && selected.findIndex((item) => item.id === notebook.id) > -1;
@@ -153,9 +142,9 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
for (const item of selected) {
try {
if (item.op === "remove") {
await db.notes?.removeFromNotebook(item, ...noteIds);
await db.notes.removeFromNotebook(item, ...noteIds);
} else if (item.op === "add") {
await db.notes?.addToNotebook(item, ...noteIds);
await db.notes.addToNotebook(item, ...noteIds);
}
} catch (e) {
if (e instanceof Error) showToast("error", e.message);
@@ -209,20 +198,20 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
}}
items={getAllNotebooks}
filter={(notebooks, query) =>
db.lookup?.notebooks(notebooks, query) || []
db.lookup.notebooks(notebooks, query) || []
}
onCreateNewItem={async (title) =>
await db.notebooks?.add({
onCreateNewItem={async (title) => {
await db.notebooks.add({
title
})
}
});
}}
renderItem={(notebook, _index, refresh, isSearching) => (
<NotebookItem
key={notebook.id}
notebook={notebook}
isSearching={isSearching}
onCreateItem={async (title) => {
await db.notebooks?.notebook(notebook.id).topics.add(title);
await db.notebooks.topics(notebook.id).add({ title });
refresh();
}}
/>
@@ -482,7 +471,7 @@ function findSelectionIndex(
}
function topicHasNotes(topic: Item, noteIds: string[]) {
const notes: string[] = db.notes?.topicReferences.get(topic.id) || [];
const notes: string[] = db.notes.topicReferences.get(topic.id) || [];
return noteIds.some((id) => notes.indexOf(id) > -1);
}
@@ -549,7 +538,7 @@ function stringifySelected(suggestion: NotebookReference[]) {
}
function resolveReference(ref: NotebookReference): string | undefined {
const notebook = db.notebooks?.notebook(ref.id);
const notebook = db.notebooks.notebook(ref.id);
if (!notebook) return undefined;
if (ref.topic) {

View File

@@ -21,7 +21,7 @@ import { Perform } from "../common/dialog-controller";
import Dialog from "../components/dialog";
import { Button, Flex, Text } from "@theme-ui/components";
import { db } from "../common/db";
import { Reminder } from "@notesnook/core/dist/collections/reminders";
import { Reminder } from "@notesnook/core/dist/types";
import IconTag from "../components/icon-tag";
import { Clock, Refresh } from "../components/icons";
import Note from "../components/note";
@@ -57,10 +57,9 @@ export default function ReminderPreviewDialog(
props: ReminderPreviewDialogProps
) {
const { reminder } = props;
const referencedNotes = db.relations?.to(
{ id: reminder.id, type: "reminder" },
"note"
);
const referencedNotes = db.relations
.to({ id: reminder.id, type: "reminder" }, "note")
.resolved();
return (
<Dialog
@@ -101,7 +100,7 @@ export default function ReminderPreviewDialog(
key={time.id}
variant="secondary"
onClick={() => {
db.reminders?.add({
db.reminders.add({
id: reminder.id,
snoozeUntil: Date.now() + time.interval
});

View File

@@ -50,11 +50,12 @@ export const AuthenticationSettings: SettingsGroup[] = [
const result = await showPasswordDialog(
"change_account_password",
async (data) => {
await db.user.clearSessions();
return (
(await db.user?.changePassword(
db.user.changePassword(
data.oldPassword,
data.newPassword
)) || false
) || false
);
}
);

View File

@@ -1,39 +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 { getAllAccents } from "@notesnook/theme";
import { Flex } from "@theme-ui/components";
import AccentItem from "../../../components/accent-item";
export function AccentColors() {
return (
<Flex
sx={{
flexWrap: "wrap",
borderRadius: "default",
justifyContent: "left",
mt: 2
}}
>
{getAllAccents().map((color) => (
<AccentItem key={color.code} code={color.code} label={color.label} />
))}
</Flex>
);
}

View File

@@ -22,35 +22,12 @@ import { Loading } from "../../../components/icons";
import { Box, Flex, Link, Text } from "@theme-ui/components";
import { getFormattedDate } from "@notesnook/common";
import { db } from "../../../common/db";
import {
Transaction,
TransactionStatus
} from "@notesnook/core/dist/api/subscriptions";
type Transaction = {
order_id: string;
checkout_id: string;
amount: string;
currency: string;
status: keyof typeof TransactionStatusToText;
created_at: string;
passthrough: null;
product_id: number;
is_subscription: boolean;
is_one_off: boolean;
subscription: Subscription;
user: User;
receipt_url: string;
};
type Subscription = {
subscription_id: number;
status: string;
};
type User = {
user_id: number;
email: string;
marketing_consent: boolean;
};
const TransactionStatusToText = {
const TransactionStatusToText: Record<TransactionStatus, string> = {
completed: "Completed",
refunded: "Refunded",
partially_refunded: "Partially refunded",
@@ -68,7 +45,7 @@ export function BillingHistory() {
setError(undefined);
setIsLoading(true);
const transactions = await db.subscriptions?.transactions();
const transactions = await db.subscriptions.transactions();
if (!transactions) return;
setTransactions(transactions);
} catch (e) {

View File

@@ -89,7 +89,7 @@ export function CustomizeToolbar() {
(async () => {
const tools = unflatten(items).slice(0, -1);
await db.settings?.setToolbarConfig("desktop", {
await db.settings.setToolbarConfig("desktop", {
preset: currentPreset.id,
config: currentPreset.id === "custom" ? tools : undefined
});

View File

@@ -39,7 +39,7 @@ export function SubscriptionStatus() {
const user = useUserStore((store) => store.user);
const [activateTrial, isActivatingTrial] = useAction(async () => {
await db.user?.activateTrial();
await db.user.activateTrial();
});
const provider = PROVIDER_MAP[user?.subscription?.provider || 0];
@@ -151,7 +151,7 @@ export function SubscriptionStatus() {
type: "modal",
title: "Cancelling your subscription",
subtitle: "Please wait...",
action: () => db.subscriptions?.cancel()
action: () => db.subscriptions.cancel()
})
.catch((e) => showToast("error", e.message))
.then(() =>
@@ -181,7 +181,7 @@ export function SubscriptionStatus() {
type: "modal",
title: "Requesting refund for your subscription",
subtitle: "Please wait...",
action: () => db.subscriptions?.refund()
action: () => db.subscriptions.refund()
})
.catch((e) => showToast("error", e.message))
.then(() =>

View File

@@ -66,7 +66,7 @@ What data is collected & when?`,
type: "toggle",
isToggled: () => !!useUserStore.getState().user?.marketingConsent,
toggle: async () => {
await db.user?.changeMarketingConsent(
await db.user.changeMarketingConsent(
!useUserStore.getState().user?.marketingConsent
);
await useUserStore.getState().refreshUser();

View File

@@ -103,7 +103,7 @@ export const ProfileSettings: SettingsGroup[] = [
title: "Delete account",
action: async () =>
showPasswordDialog("delete_account", async ({ password }) => {
await db.user?.deleteUser(password);
await db.user.deleteUser(password);
return true;
})
}
@@ -132,7 +132,7 @@ export const ProfileSettings: SettingsGroup[] = [
await showLoadingDialog({
title: "You are being logged out",
subtitle: "Please wait...",
action: () => db.user?.logout(true)
action: () => db.user.logout(true)
});
showToast("success", "You have been logged out.");
}
@@ -153,7 +153,7 @@ export const ProfileSettings: SettingsGroup[] = [
action: async () => {
if (!(await showClearSessionsConfirmation())) return;
await db.user?.clearSessions();
await db.user.clearSessions();
showToast(
"success",
"You have been logged out from all other devices."

View File

@@ -46,7 +46,7 @@ export const SubscriptionSettings: SettingsGroup[] = [
title: "Update",
action: async () => {
try {
window.open(await db.subscriptions?.updateUrl(), "_blank");
window.open(await db.subscriptions.updateUrl(), "_blank");
} catch (e) {
if (e instanceof Error) showToast("error", e.message);
}

View File

@@ -102,7 +102,7 @@ export const VaultSettings: SettingsGroup[] = [
type: "button",
title: "Delete",
action: async () => {
if ((await Vault.deleteVault()) && !(await db.vault?.exists())) {
if ((await Vault.deleteVault()) && !(await db.vault.exists())) {
useAppStore.getState().setIsVaultCreated(false);
await useAppStore.getState().refresh();
showToast("success", "Vault deleted.");

View File

@@ -17,6 +17,7 @@ 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 { User } from "@notesnook/core/dist/api/user-manager";
import { SUBSCRIPTION_STATUS } from "../common/constants";
import {
useStore as useUserStore,
@@ -31,8 +32,9 @@ export function useIsUserPremium() {
export function isUserPremium(user?: User) {
if (IS_TESTING) return true;
if (!user) user = userstore.get().user;
if (!user) return false;
const subStatus = user?.subscription?.type;
const subStatus = user.subscription.type;
return (
subStatus === SUBSCRIPTION_STATUS.BETA ||
subStatus === SUBSCRIPTION_STATUS.PREMIUM ||
@@ -43,8 +45,9 @@ export function isUserPremium(user?: User) {
export function isUserSubscribed(user?: User) {
if (!user) user = userstore.get().user;
if (!user) return false;
const subStatus = user?.subscription?.type;
const subStatus = user.subscription?.type;
return (
subStatus === SUBSCRIPTION_STATUS.PREMIUM ||
subStatus === SUBSCRIPTION_STATUS.PREMIUM_CANCELED

View File

@@ -43,6 +43,12 @@ import {
OriginPrivateFileSystem
} from "./file-store";
import { isFeatureSupported } from "../utils/feature-check";
import {
FileEncryptionMetadataWithOutputType,
IFileStorage,
Output,
RequestOptions
} from "@notesnook/core/dist/interfaces";
const ABYTES = 17;
const CHUNK_SIZE = 512 * 1024;
@@ -59,7 +65,7 @@ const streamablefs = new StreamableFS(
: new IndexedDBFileStore("streamable-fs")
);
async function writeEncryptedFile(
export async function writeEncryptedFile(
file: File,
key: SerializedKey,
hash: string
@@ -124,18 +130,16 @@ async function writeEncryptedFile(
* 3. We encrypt the Uint8Array
* 4. We save the encrypted Uint8Array
*/
async function writeEncryptedBase64(metadata: {
data: string;
key: SerializedKey;
mimeType?: string;
}) {
const { data, key, mimeType } = metadata;
async function writeEncryptedBase64(
data: string,
key: SerializedKey,
mimeType: string
) {
const bytes = new Uint8Array(Buffer.from(data, "base64"));
const { hash, type: hashType } = await hashBuffer(bytes);
const attachment = db.attachments?.attachment(hash);
const attachment = db.attachments.attachment(hash);
const file = new File([bytes.buffer], hash, {
type: attachment?.metadata.type || mimeType || "application/octet-stream"
@@ -153,14 +157,16 @@ function hashBase64(data: string) {
return hashBuffer(Buffer.from(data, "base64"));
}
async function hashBuffer(data: IDataType) {
export async function hashBuffer(data: IDataType) {
return {
hash: await xxhash64(data),
type: "xxh64"
};
}
async function hashStream(reader: ReadableStreamDefaultReader<Uint8Array>) {
export async function hashStream(
reader: ReadableStreamDefaultReader<Uint8Array>
) {
const hasher = await createXXHash64();
hasher.init();
@@ -174,47 +180,46 @@ async function hashStream(reader: ReadableStreamDefaultReader<Uint8Array>) {
return { type: "xxh64", hash: hasher.digest("hex") };
}
async function readEncrypted(
async function readEncrypted<TOutputFormat extends OutputFormat>(
filename: string,
key: SerializedKey,
cipherData: Cipher<DataFormat> & { outputType: DataFormat }
cipherData: FileEncryptionMetadataWithOutputType<TOutputFormat>
) {
const fileHandle = await streamablefs.readFile(filename);
if (!fileHandle) {
console.error(`File not found. (File hash: ${filename})`);
return null;
return;
}
const decryptionStream = await NNCrypto.createDecryptionStream(
key,
cipherData.iv
);
return cipherData.outputType === "base64" || cipherData.outputType === "text"
? (
await consumeReadableStream(
fileHandle.readable
.pipeThrough(decryptionStream)
.pipeThrough(
cipherData.outputType === "text"
? new globalThis.TextDecoderStream()
: new Base64DecoderStream()
)
)
).join("")
: new Uint8Array(
Buffer.concat(
return (
cipherData.outputType === "base64" || cipherData.outputType === "text"
? (
await consumeReadableStream(
fileHandle.readable.pipeThrough(decryptionStream)
fileHandle.readable
.pipeThrough(decryptionStream)
.pipeThrough(
cipherData.outputType === "text"
? new globalThis.TextDecoderStream()
: new Base64DecoderStream()
)
)
).join("")
: new Uint8Array(
Buffer.concat(
await consumeReadableStream(
fileHandle.readable.pipeThrough(decryptionStream)
)
)
)
);
) as Output<TOutputFormat>;
}
type RequestOptions = {
headers: Record<string, string>;
type RequestOptionsWithSignal = RequestOptions & {
signal: AbortSignal;
url: string;
chunkSize: number;
};
type UploadAdditionalData = {
@@ -224,7 +229,10 @@ type UploadAdditionalData = {
uploadedChunks?: { PartNumber: number; ETag: string }[];
};
async function uploadFile(filename: string, requestOptions: RequestOptions) {
async function uploadFile(
filename: string,
requestOptions: RequestOptionsWithSignal
) {
const fileHandle = await streamablefs.readFile(filename);
if (!fileHandle || !(await exists(filename)))
throw new Error(
@@ -275,7 +283,7 @@ async function uploadFile(filename: string, requestOptions: RequestOptions) {
async function singlePartUploadFile(
fileHandle: FileHandle,
filename: string,
requestOptions: RequestOptions
requestOptions: RequestOptionsWithSignal
) {
console.log("Streaming file upload!");
const { url, headers, signal } = requestOptions;
@@ -315,7 +323,7 @@ async function singlePartUploadFile(
async function multiPartUploadFile(
fileHandle: FileHandle,
filename: string,
requestOptions: RequestOptions
requestOptions: RequestOptionsWithSignal
) {
const { headers, signal } = requestOptions;
@@ -445,7 +453,10 @@ function reportProgress(
});
}
async function downloadFile(filename: string, requestOptions: RequestOptions) {
async function downloadFile(
filename: string,
requestOptions: RequestOptionsWithSignal
) {
try {
const { url, headers, chunkSize, signal } = requestOptions;
const handle = await streamablefs.readFile(filename);
@@ -487,13 +498,13 @@ async function downloadFile(filename: string, requestOptions: RequestOptions) {
);
if (contentLength === 0 || isNaN(contentLength)) {
const error = `File length is 0. Please upload this file again from the attachment manager. (File hash: ${filename})`;
await db.attachments?.markAsFailed(filename, error);
await db.attachments.markAsFailed(filename, error);
throw new Error(error);
}
if (!response.body) {
const error = `The download response does not contain a body. Please upload this file again from the attachment manager. (File hash: ${filename})`;
await db.attachments?.markAsFailed(filename, error);
await db.attachments.markAsFailed(filename, error);
throw new Error(error);
}
@@ -501,7 +512,7 @@ async function downloadFile(filename: string, requestOptions: RequestOptions) {
const decryptedLength = contentLength - totalChunks * ABYTES;
if (attachment && attachment.length !== decryptedLength) {
const error = `File length mismatch. Please upload this file again from the attachment manager. (File hash: ${filename})`;
await db.attachments?.markAsFailed(filename, error);
await db.attachments.markAsFailed(filename, error);
throw new Error(error);
}
@@ -570,7 +581,7 @@ export async function decryptFile(
return await toBlob(fileHandle.readable.pipeThrough(decryptionStream));
}
async function saveFile(filename: string, fileMetadata: FileMetadata) {
export async function saveFile(filename: string, fileMetadata: FileMetadata) {
const { name, type, isUploaded } = fileMetadata;
const decrypted = await decryptFile(filename, fileMetadata);
@@ -580,7 +591,10 @@ async function saveFile(filename: string, fileMetadata: FileMetadata) {
await streamablefs.deleteFile(filename);
}
async function deleteFile(filename: string, requestOptions: RequestOptions) {
async function deleteFile(
filename: string,
requestOptions: RequestOptionsWithSignal
) {
if (!requestOptions) return await streamablefs.deleteFile(filename);
if (!requestOptions && !(await streamablefs.exists(filename))) return true;
@@ -599,10 +613,10 @@ async function deleteFile(filename: string, requestOptions: RequestOptions) {
}
}
async function getUploadedFileSize(filename: string) {
export async function getUploadedFileSize(filename: string) {
try {
const url = `${hosts.API_HOST}/s3?name=${filename}`;
const token = await db.user?.tokenManager.getAccessToken();
const token = await db.tokenManager.getAccessToken();
const attachmentInfo = await axios.head(url, {
headers: { Authorization: `Bearer ${token}` }
@@ -620,24 +634,16 @@ function clearFileStorage() {
return streamablefs.clear();
}
const FS = {
export const FileStorage: IFileStorage = {
writeEncryptedBase64,
readEncrypted,
uploadFile: cancellable(uploadFile),
downloadFile: cancellable(downloadFile),
deleteFile,
saveFile,
exists,
writeEncryptedFile,
clearFileStorage,
getUploadedFileSize,
decryptFile,
hashBase64,
hashBuffer,
hashStream
hashBase64
};
export default FS;
function isAttachmentDeletable(type: string) {
return !type.startsWith("image/") && !type.startsWith("application/pdf");
@@ -647,15 +653,21 @@ function isSuccessStatusCode(statusCode: number) {
return statusCode >= 200 && statusCode <= 299;
}
function cancellable(
operation: (filename: string, requestOptions: RequestOptions) => any
function cancellable<T>(
operation: (
filename: string,
requestOptions: RequestOptionsWithSignal
) => Promise<T>
) {
return function (filename: string, requestOptions: RequestOptions) {
const abortController = new AbortController();
requestOptions.signal = abortController.signal;
return {
execute: () => operation(filename, requestOptions),
cancel: (message: string) => {
execute: () =>
operation(filename, {
...requestOptions,
signal: abortController.signal
}),
cancel: async (message: string) => {
abortController.abort(message);
}
};

View File

@@ -17,6 +17,7 @@ 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 { IStorage } from "@notesnook/core/dist/interfaces";
import {
IndexedDBKVStore,
LocalStorageKVStore,
@@ -31,7 +32,7 @@ export type DatabasePersistence = "memory" | "db";
const APP_SALT = "oVzKtazBo7d8sb7TBvY9jw";
export class NNStorage {
export class NNStorage implements IStorage {
database!: IKVStore;
static async createInstance(
@@ -53,8 +54,8 @@ export class NNStorage {
return this.database.get(key);
}
readMulti(keys: string[]) {
if (keys.length <= 0) return [];
readMulti<T>(keys: string[]): Promise<[string, T][]> {
if (keys.length <= 0) return Promise.resolve([]);
return this.database.getMany(keys.sort());
}

View File

@@ -74,7 +74,7 @@ const routes = defineRoutes({
}
}),
"/notebooks/:notebookId": ({ notebookId }) => {
const notebook = db.notebooks?.notebook(notebookId);
const notebook = db.notebooks.notebook(notebookId);
if (!notebook) return false;
nbstore.setSelectedNotebook(notebookId);
notestore.setContext({
@@ -99,7 +99,7 @@ const routes = defineRoutes({
});
},
"/notebooks/:notebookId/:topicId": ({ notebookId, topicId }) => {
const notebook = db.notebooks?.notebook(notebookId);
const notebook = db.notebooks.notebook(notebookId);
const topic = notebook?.topics?.topic(topicId)?._topic;
if (!topic) return false;
nbstore.setSelectedNotebook(notebookId);
@@ -178,11 +178,10 @@ const routes = defineRoutes({
}
}),
"/tags/:tagId": ({ tagId }) => {
const tag = db.tags?.tag(tagId);
const tag = db.tags.tag(tagId);
if (!tag) return false;
const { id } = tag;
const { id, title } = tag;
notestore.setContext({ type: "tag", value: id });
const title = db.tags?.alias(id);
return defineRoute({
key: "notes",
type: "notes",
@@ -201,13 +200,12 @@ const routes = defineRoutes({
});
},
"/colors/:colorId": ({ colorId }) => {
const color = db.colors?.tag(colorId);
const color = db.colors.color(colorId);
if (!color) {
navigate("/");
return false;
}
const { id } = color;
const title = db.colors?.alias(id);
const { id, title } = color;
notestore.setContext({ type: "color", value: id });
return defineRoute({
key: "notes",

View File

@@ -95,7 +95,7 @@ async function shouldShowAnnouncement(announcement) {
if (!show) return false;
const user = await db.user.getUser();
const subStatus = user?.subscription?.type;
const subStatus = user.subscription?.type;
show = announcement.userTypes.some((userType) => {
switch (userType) {
case "pro":

View File

@@ -63,9 +63,9 @@ export const getDefaultSession = (sessionId = Date.now()) => {
localOnly: false,
favorite: false,
locked: false,
tags: [],
// tags: [],
context: undefined,
color: undefined,
// color: undefined,
dateEdited: 0,
attachmentsLength: 0,
isDeleted: false,
@@ -82,6 +82,8 @@ export const getDefaultSession = (sessionId = Date.now()) => {
*/
class EditorStore extends BaseStore {
session = getDefaultSession();
tags = [];
color = undefined;
arePropertiesVisible = false;
editorMargins = Config.get("editor:margins", true);
@@ -98,7 +100,14 @@ class EditorStore extends BaseStore {
refreshTags = () => {
this.set((state) => {
state.session.tags = state.session.tags.slice();
if (!state.session.id) return;
console.log(
db.relations.to({ id: state.session.id, type: "note" }, "tag")
);
state.tags = db.relations.to(
{ id: state.session.id, type: "note" },
"tag"
);
});
};
@@ -110,7 +119,6 @@ class EditorStore extends BaseStore {
updateSession = async (item) => {
this.set((state) => {
state.session.title = item.title;
state.session.tags = item.tags;
state.session.pinned = item.pinned;
state.session.favorite = item.favorite;
state.session.readonly = item.readonly;
@@ -118,6 +126,7 @@ class EditorStore extends BaseStore {
state.session.dateCreated = item.dateCreated;
state.session.locked = item.locked;
});
this.refreshTags();
};
openLockedSession = async (note) => {
@@ -137,7 +146,7 @@ class EditorStore extends BaseStore {
openSession = async (noteId, force) => {
const session = this.get().session;
if (session.id) await db.fs.cancel(session.id);
if (session.id) await db.fs().cancel(session.id);
if (session.id === noteId && !force) return;
if (session.state === SESSION_STATES.unlocked) {
@@ -213,8 +222,8 @@ class EditorStore extends BaseStore {
const { type, value } = currentSession.context;
if (type === "topic" || type === "notebook")
await db.notes.addToNotebook(value, id);
else if (type === "color") await db.notes.note(id).color(value);
else if (type === "tag") await db.notes.note(id).tag(value);
else if (type === "color" || type === "tag")
await db.relations.add({ type, id: value }, { id, type: "note" });
// update the note.
note = db.notes.note(id)?.data;
} else if (!sessionId && db.settings.getDefaultNotebook()) {
@@ -265,7 +274,7 @@ class EditorStore extends BaseStore {
newSession = async (nonce) => {
let context = noteStore.get().context;
const session = this.get().session;
if (session.id) await db.fs.cancel(session.id);
if (session.id) await db.fs().cancel(session.id);
this.set((state) => {
state.session = {
@@ -282,7 +291,7 @@ class EditorStore extends BaseStore {
clearSession = async (shouldNavigate = true) => {
const session = this.get().session;
if (session.id) await db.fs.cancel(session.id);
if (session.id) await db.fs().cancel(session.id);
this.set((state) => {
state.session = {
@@ -359,26 +368,24 @@ class EditorStore extends BaseStore {
};
async _setTag(value) {
value = db.tags.sanitize(value);
if (!value) return;
const { tags, id } = this.get().session;
const {
tags,
session: { id }
} = this.get();
let note = db.notes.note(id);
if (!note) return;
let index = tags.indexOf(value);
if (index > -1) {
await note.untag(value);
let tag = tags.find((t) => t.title === value);
if (tag) {
await db.relations.unlink(tag, note._note);
appStore.refreshNavItems();
} else {
await note.tag(value);
const id = await db.tags.add({ title: value });
await db.relations.add({ id, type: "tag" }, note._note);
}
this.set((state) => {
state.session.tags = db.notes.note(id).tags.slice();
});
this.refreshTags();
tagStore.refresh();
noteStore.refresh();
}

View File

@@ -154,10 +154,17 @@ class NoteStore extends BaseStore {
try {
let note = db.notes.note(id);
if (!note) return;
if (note.data.color === color) await db.notes.note(id).uncolor();
else await db.notes.note(id).color(color);
const colorId =
db.tags.find(color)?.id || (await db.colors.add({ title: color }));
const isColored =
db.relations.from({ type: "color", id: colorId }, "note").length > 0;
if (isColored)
await db.relations.unlink({ type: "color", id: colorId }, note._note);
else await db.relations.add({ type: "color", id: colorId }, note._note);
appStore.refreshNavItems();
this._syncEditor(note.id, "color", db.notes.note(id).data.color);
this._syncEditor(note.id, "color", color);
this.refreshItem(id);
} catch (e) {
console.error(e);
@@ -191,15 +198,15 @@ function notesFromContext(context) {
let notes = [];
switch (context.type) {
case "tag":
notes = db.notes.tagged(context.value);
break;
case "color":
notes = db.notes.colored(context.value);
notes = db.relations
.from({ type: context.type, id: context.value }, "note")
.resolved();
break;
case "notebook": {
const notebook = db.notebooks.notebook(context?.value?.id);
if (!notebook) break;
notes = db.relations.from(notebook.data, "note");
notes = db.relations.from(notebook.data, "note").resolved();
break;
}
case "topic": {

View File

@@ -67,7 +67,7 @@ class NotebookStore extends BaseStore {
setSelectedNotebook = (id) => {
if (!id) return;
const notebook = db.notebooks?.notebook(id)?.data;
const notebook = db.notebooks.notebook(id)?.data;
if (!notebook) return;
this.set((state) => {

View File

@@ -21,18 +21,16 @@ import createStore from "../common/store";
import { db } from "../common/db";
import BaseStore from "./index";
import { groupArray } from "@notesnook/core/dist/utils/grouping";
import { GroupedItems, Tag } from "@notesnook/core/dist/types";
/**
* @extends {BaseStore<TagStore>}
*/
class TagStore extends BaseStore {
tags = [];
class TagStore extends BaseStore<TagStore> {
tags: GroupedItems<Tag> = [];
refresh = () => {
this.set(
(state) =>
(state.tags = groupArray(
db.tags.all,
db.tags.all || [],
db.settings.getGroupOptions("tags")
))
);

View File

@@ -40,7 +40,7 @@ class UserStore extends BaseStore {
isLoggingIn = false;
isSigningIn = false;
/**
* @type {User | undefined}
* @type {import("@notesnook/core/dist/api/user-manager").User | undefined}
*/
user = undefined;
counter = 0;

View File

@@ -22,8 +22,9 @@ import type { Compressor as CompressorWorkerType } from "./compressor.worker";
import { wrap, Remote } from "comlink";
import { desktop } from "../common/desktop-bridge";
import { ICompressor } from "@notesnook/core/dist/interfaces";
export class Compressor {
export class Compressor implements ICompressor {
private worker!: globalThis.Worker;
private compressor!: Remote<CompressorWorkerType>;
@@ -35,14 +36,15 @@ export class Compressor {
}
async compress(data: string) {
if (IS_DESKTOP_APP)
return await desktop?.compress.gzip.query({ data, level: 6 });
if (IS_DESKTOP_APP && desktop)
return await desktop.compress.gzip.query({ data, level: 6 });
return await this.compressor.gzip({ data, level: 6 });
}
async decompress(data: string) {
if (IS_DESKTOP_APP) return await desktop?.compress.gunzip.query(data);
if (IS_DESKTOP_APP && desktop)
return await desktop.compress.gunzip.query(data);
return await this.compressor.gunzip({ data });
}

View File

@@ -72,10 +72,10 @@ async function processAttachment(
const name = path.basename(entry.name);
if (!name || attachments[name] || db.attachments?.exists(name)) return;
const { default: FS } = await import("../interfaces/fs");
const { hashBuffer, writeEncryptedFile } = await import("../interfaces/fs");
const data = await entry.arrayBuffer();
const { hash } = await FS.hashBuffer(new Uint8Array(data));
const { hash } = await hashBuffer(new Uint8Array(data));
if (hash !== name) {
throw new Error(`integrity check failed: ${name} !== ${hash}`);
}
@@ -84,7 +84,7 @@ async function processAttachment(
type: "application/octet-stream"
});
const key = await db.attachments?.generateKey();
const cipherData = await FS.writeEncryptedFile(file, key, name);
const cipherData = await writeEncryptedFile(file, key, name);
attachments[name] = { ...cipherData, key };
}
@@ -114,12 +114,17 @@ async function processNote(entry: ZipEntry, attachments: Record<string, any>) {
const notebooks = note.notebooks?.slice() || [];
note.notebooks = [];
const noteId = await db.notes?.add(note);
const noteId = await db.notes.add({
...note,
content: { type: "tiptap", data: note.content?.data },
notebooks: []
});
if (!noteId) return;
for (const nb of notebooks) {
const notebook = await importNotebook(nb).catch(() => ({ id: undefined }));
if (!notebook.id) continue;
await db.notes?.addToNotebook(
await db.notes.addToNotebook(
{ id: notebook.id, topic: notebook.topic },
noteId
);
@@ -136,11 +141,10 @@ async function importNotebook(
): Promise<{ id?: string; topic?: string }> {
if (!notebook) return {};
let nb = db.notebooks?.all.find((nb) => nb.title === notebook.notebook);
let nb = db.notebooks.all.find((nb) => nb.title === notebook.notebook);
if (!nb) {
const nbId = await db.notebooks?.add({
title: notebook.notebook,
topics: notebook.topic ? [notebook.topic] : []
const nbId = await db.notebooks.add({
title: notebook.notebook
});
nb = db.notebooks?.notebook(nbId)?.data;
if (!nb) return {};

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {
initalize,
initialize,
logger as _logger,
logManager
} from "@notesnook/core/dist/logger";
@@ -29,8 +29,8 @@ import { createWriteStream } from "./stream-saver";
import { sanitizeFilename } from "@notesnook/common";
let logger: typeof _logger;
async function initalizeLogger(persistence: DatabasePersistence = "db") {
initalize(await NNStorage.createInstance("Logs", persistence), false);
async function initializeLogger(persistence: DatabasePersistence = "db") {
initialize(await NNStorage.createInstance("Logs", persistence), false);
logger = _logger.scope("notesnook-web");
}
@@ -66,4 +66,4 @@ async function clearLogs() {
await logManager.clear();
}
export { initalizeLogger, logger, downloadLogs, clearLogs };
export { initializeLogger, logger, downloadLogs, clearLogs };

View File

@@ -34,7 +34,7 @@ export class AttachmentStream extends ReadableStream<ZipFile> {
const counters: Record<string, number> = {};
if (signal)
signal.onabort = async () => {
await db.fs?.cancel(GROUP_ID, "download");
await db.fs().cancel(GROUP_ID, "download");
};
super({
@@ -49,14 +49,18 @@ export class AttachmentStream extends ReadableStream<ZipFile> {
onProgress && onProgress(index);
const attachment = attachments[index++];
await db.fs?.downloadFile(
GROUP_ID,
attachment.metadata.hash,
attachment.chunkSize,
attachment.metadata
);
await db
.fs()
.downloadFile(
GROUP_ID,
attachment.metadata.hash,
attachment.chunkSize,
attachment.metadata
);
const key = await db.attachments.decryptKey(attachment.key);
if (!key) return;
const key = await db.attachments?.decryptKey(attachment.key);
const file = await lazify(
import("../../interfaces/fs"),
({ decryptFile }) =>

View File

@@ -17,7 +17,7 @@ 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 Note from "@notesnook/core/dist/models/note";
import { Note } from "@notesnook/core/dist/types";
import { db } from "../../common/db";
import { exportNote } from "../../common/export";
import { makeUniqueFilename } from "./utils";
@@ -47,7 +47,9 @@ export class ExportStream extends TransformStream<Note, ZipFile> {
const notebooks = [
...(
db.relations?.to({ id: note.id, type: "note" }, "notebook") || []
db.relations
?.to({ id: note.id, type: "note" }, "notebook")
.resolved() || []
).map((n) => ({ title: n.title, topics: [] })),
...(note.notebooks || []).map(
(ref: { id: string; topics: string[] }) => {
@@ -83,8 +85,8 @@ export class ExportStream extends TransformStream<Note, ZipFile> {
controller.enqueue({
path: makeUniqueFilename(filePath, counters),
data: content,
mtime: new Date(note.data.dateEdited),
ctime: new Date(note.data.dateCreated)
mtime: new Date(note.dateEdited),
ctime: new Date(note.dateCreated)
});
});
} catch (e) {

View File

@@ -31,24 +31,25 @@ import { sanitizeFilename } from "@notesnook/common";
import { attachFile } from "../components/editor/picker";
import { getFormattedDate } from "@notesnook/common";
import { useStore as useThemeStore } from "../stores/theme-store";
import { isCipher } from "@notesnook/core/dist/database/crypto";
export class WebExtensionServer implements Server {
async login() {
const { colorScheme, darkTheme, lightTheme } = useThemeStore.getState();
const user = await db.user?.getUser();
const user = await db.user.getUser();
const theme = colorScheme === "dark" ? darkTheme : lightTheme;
if (!user) return { pro: false, theme };
return { email: user.email, pro: isUserPremium(user), theme };
}
async getNotes(): Promise<ItemReference[] | undefined> {
return db.notes?.all
return db.notes.all
.filter((n) => !n.locked)
.map((note) => ({ id: note.id, title: note.title }));
}
async getNotebooks(): Promise<NotebookReference[] | undefined> {
return db.notebooks?.all.map((nb) => ({
return db.notebooks.all.map((nb) => ({
id: nb.id,
title: nb.title,
topics: nb.topics.map((topic: ItemReference) => ({
@@ -59,7 +60,7 @@ export class WebExtensionServer implements Server {
}
async getTags(): Promise<ItemReference[] | undefined> {
return db.tags?.all.map((tag) => ({
return db.tags.all.map((tag) => ({
id: tag.id,
title: tag.title
}));
@@ -93,9 +94,11 @@ export class WebExtensionServer implements Server {
}).outerHTML;
}
const note = clip.note?.id ? db.notes?.note(clip.note?.id) : null;
const note = clip.note?.id ? db.notes.note(clip.note?.id) : null;
let content = (await note?.content()) || "";
if (isCipher(content)) return;
content += clipContent;
content += h("div", [
h("hr"),
@@ -103,21 +106,30 @@ export class WebExtensionServer implements Server {
h("p", [`Date clipped: ${getFormattedDate(Date.now())}`])
]).innerHTML;
const id = await db.notes?.add({
const id = await db.notes.add({
id: note?.id,
title: note ? note.title : clip.title,
content: { type: "tiptap", data: content },
tags: note ? note.tags : clip.tags
content: { type: "tiptap", data: content }
});
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" }
);
}
}
if (clip.refs && id && !clip.note) {
for (const ref of clip.refs) {
switch (ref.type) {
case "notebook":
await db.notes?.addToNotebook({ id: ref.id }, id);
await db.notes.addToNotebook({ id: ref.id }, id);
break;
case "topic":
await db.notes?.addToNotebook(
await db.notes.addToNotebook(
{ id: ref.parentId, topic: ref.id },
id
);

View File

@@ -48,8 +48,7 @@ function Home() {
return (
<ListContainer
type="home"
groupingKey="home"
group="home"
compact={isCompact}
refresh={refresh}
items={notes}

View File

@@ -37,14 +37,13 @@ import useDatabase from "../hooks/use-database";
import { Loader } from "../components/loader";
import { showToast } from "../utils/toast";
import AuthContainer from "../components/auth-container";
import { useTimer } from "../hooks/use-timer";
import { AuthenticatorType } from "../dialogs/mfa/types";
import {
showLoadingDialog,
showLogoutConfirmation
} from "../common/dialog-controller";
import { ErrorText } from "../components/error-text";
import { AuthenticatorType, User } from "@notesnook/core/dist/api/user-manager";
type EmailFormData = {
email: string;
@@ -177,7 +176,7 @@ function Auth(props: AuthProps) {
useEffect(() => {
if (!isAppLoaded) return;
db.user?.getUser().then((user) => {
db.user.getUser().then((user) => {
if (user && authorizedRoutes.includes(route) && !isSessionExpired())
return openURL("/");
setIsReady(true);
@@ -406,7 +405,7 @@ function SessionExpiry(props: BaseAuthComponentProps<"sessionExpiry">) {
useEffect(() => {
(async () => {
const user = await db.user?.getUser();
const user = await db.user.getUser();
if (user && isSessionExpired()) {
setUser(user);
} else if (!user) {
@@ -485,7 +484,7 @@ function SessionExpiry(props: BaseAuthComponentProps<"sessionExpiry">) {
if (await showLogoutConfirmation()) {
await showLoadingDialog({
title: "You are being logged out",
action: () => db.user?.logout(true),
action: () => db.user.logout(true),
subtitle: "Please wait..."
});
openURL("/login");
@@ -522,7 +521,7 @@ function AccountRecovery(props: BaseAuthComponentProps<"recover">) {
return;
}
const url = await db.user?.recoverAccount(form.email.toLowerCase());
const url = await db.user.recoverAccount(form.email.toLowerCase());
console.log(url);
if (IS_TESTING) {
window.open(url, "_self");
@@ -606,7 +605,7 @@ function MFACode(props: BaseAuthComponentProps<"mfa:code">) {
async (selectedMethod: "sms" | "email") => {
setIsSending(true);
try {
await db.mfa?.sendCode(selectedMethod);
await db.mfa.sendCode(selectedMethod);
setEnabled(false);
} catch (e) {
const error = e as Error;

View File

@@ -31,8 +31,7 @@ function Notebooks() {
return (
<>
<ListContainer
type="notebooks"
groupingKey="notebooks"
group="notebooks"
refresh={refresh}
items={notebooks}
placeholder={<Placeholder context="notebooks" />}

View File

@@ -32,7 +32,11 @@ function Notes() {
const isCompact = useNotesStore((store) => store.viewMode === "compact");
useEffect(() => {
if (context?.type === "color" && context?.notes?.length <= 0) {
if (
context?.type === "color" &&
context.notes &&
context.notes.length <= 0
) {
navigate("/", true);
}
}, [context]);
@@ -40,12 +44,15 @@ function Notes() {
if (!context) return null;
return (
<ListContainer
type="notes"
groupingKey={type}
group={type}
refresh={refreshContext}
compact={isCompact}
context={{ ...context, notes: undefined }}
items={groupArray(context.notes, db.settings?.getGroupOptions(type))}
items={
context.notes
? groupArray(context.notes, db.settings.getGroupOptions(type))
: []
}
placeholder={
<Placeholder
context={

View File

@@ -31,6 +31,7 @@ import { showRecoveryKeyDialog } from "../common/dialog-controller";
import Config from "../utils/config";
import { EVENTS } from "@notesnook/core/dist/common";
import { ErrorText } from "../components/error-text";
import { User } from "@notesnook/core/dist/api/user-manager";
type RecoveryMethodType = "key" | "backup" | "reset";
type RecoveryMethodsFormData = Record<string, unknown>;
@@ -131,14 +132,14 @@ function useAuthenticateUser({
try {
await db.init();
const accessToken = await db.user?.tokenManager.getAccessToken();
const accessToken = await db.tokenManager.getAccessToken();
if (!accessToken) {
await db.user?.tokenManager.getAccessTokenFromAuthorizationCode(
await db.tokenManager.getAccessTokenFromAuthorizationCode(
userId,
code.replace(/ /gm, "+")
);
}
const user = await db.user?.fetchUser();
const user = await db.user.fetchUser();
setUser(user);
} catch (e) {
showToast("error", "Failed to authenticate. Please try again.");
@@ -355,9 +356,9 @@ function RecoveryKeyMethod(props: BaseRecoveryComponentProps<"method:key">) {
onSubmit={async (form) => {
setProgress(0);
const user = await db.user?.getUser();
const user = await db.user.getUser();
if (!user) throw new Error("User not authenticated");
await db.storage?.write(`_uk_@${user.email}@_k`, form.recoveryKey);
await db.storage().write(`_uk_@${user.email}@_k`, form.recoveryKey);
await db.sync({ type: "fetch", force: true });
navigate("backup");
}}
@@ -499,10 +500,10 @@ function NewPassword(props: BaseRecoveryComponentProps<"new">) {
if (form.password !== form.confirmPassword)
throw new Error("Passwords do not match.");
if (formData?.userResetRequired && !(await db.user?.resetUser()))
if (formData?.userResetRequired && !(await db.user.resetUser()))
throw new Error("Failed to reset user.");
if (!(await db.user?.resetPassword(form.password)))
if (!(await db.user.resetPassword(form.password)))
throw new Error("Could not reset account password.");
if (formData?.backupFile) {
@@ -543,8 +544,8 @@ function Final(_props: BaseRecoveryComponentProps<"final">) {
async function finalize() {
await showRecoveryKeyDialog();
if (!isSessionExpired()) {
await db.user?.logout(true, "Password changed.");
await db.user?.clearSessions(true);
await db.user.logout(true, "Password changed.");
await db.user.clearSessions(true);
}
setIsReady(true);
}

View File

@@ -31,8 +31,7 @@ function Reminders() {
return (
<>
<ListContainer
type="reminders"
groupingKey="reminders"
group="reminders"
refresh={refresh}
items={reminders}
placeholder={<Placeholder context="reminders" />}

View File

@@ -133,7 +133,7 @@ function Search({ type }) {
case "monographs":
return "all monographs";
case "color": {
const color = db.colors.all.find((tag) => tag.id === context.value);
const color = db.colors.find(context.value);
return `notes in color ${color.title}`;
}
default:
@@ -197,7 +197,7 @@ function Search({ type }) {
) : (
<ListContainer
context={context}
type={type}
group={type}
items={results}
placeholder={() => (
<Placeholder

View File

@@ -28,8 +28,7 @@ function Tags() {
const refresh = useStore((store) => store.refresh);
return (
<ListContainer
type="tags"
groupingKey="tags"
group="tags"
refresh={refresh}
items={tags}
placeholder={<Placeholder context="tags" />}

View File

@@ -137,8 +137,7 @@ function Notebook() {
<Allotment.Pane>
<Flex variant="columnFill" sx={{ height: "100%" }}>
<ListContainer
type="notes"
groupingKey={"notes"}
group="notes"
refresh={refreshContext}
compact={isCompact}
context={{ ...context, notes: undefined }}
@@ -252,7 +251,7 @@ function Topics({ selectedNotebook, isCollapsed, onClick }) {
</Flex>
<ListContainer
type="topics"
group="topics"
items={topics}
context={{
notebookId: selectedNotebook.id

View File

@@ -32,8 +32,7 @@ function Trash() {
return (
<ListContainer
type="trash"
groupingKey="trash"
group="trash"
refresh={refresh}
placeholder={<Placeholder context="trash" />}
items={items}