mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-16 11:47:54 +01:00
web: use new types from core
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
21103
apps/web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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?: {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 = []
|
||||
) => {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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" }
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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(() =>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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")
|
||||
))
|
||||
);
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 }) =>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -48,8 +48,7 @@ function Home() {
|
||||
|
||||
return (
|
||||
<ListContainer
|
||||
type="home"
|
||||
groupingKey="home"
|
||||
group="home"
|
||||
compact={isCompact}
|
||||
refresh={refresh}
|
||||
items={notes}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -31,8 +31,7 @@ function Notebooks() {
|
||||
return (
|
||||
<>
|
||||
<ListContainer
|
||||
type="notebooks"
|
||||
groupingKey="notebooks"
|
||||
group="notebooks"
|
||||
refresh={refresh}
|
||||
items={notebooks}
|
||||
placeholder={<Placeholder context="notebooks" />}
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -31,8 +31,7 @@ function Reminders() {
|
||||
return (
|
||||
<>
|
||||
<ListContainer
|
||||
type="reminders"
|
||||
groupingKey="reminders"
|
||||
group="reminders"
|
||||
refresh={refresh}
|
||||
items={reminders}
|
||||
placeholder={<Placeholder context="reminders" />}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -32,8 +32,7 @@ function Trash() {
|
||||
|
||||
return (
|
||||
<ListContainer
|
||||
type="trash"
|
||||
groupingKey="trash"
|
||||
group="trash"
|
||||
refresh={refresh}
|
||||
placeholder={<Placeholder context="trash" />}
|
||||
items={items}
|
||||
|
||||
Reference in New Issue
Block a user