mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-23 23:19:40 +01:00
feat: realtime & auto sync for all users (#944)
This commit is contained in:
2
.github/workflows/core.tests.yml
vendored
2
.github/workflows/core.tests.yml
vendored
@@ -13,6 +13,8 @@ on:
|
|||||||
types:
|
types:
|
||||||
- "ready_for_review"
|
- "ready_for_review"
|
||||||
- "opened"
|
- "opened"
|
||||||
|
- "synchronize"
|
||||||
|
- "reopened"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
|
|||||||
2
.github/workflows/web.tests.yml
vendored
2
.github/workflows/web.tests.yml
vendored
@@ -13,6 +13,8 @@ on:
|
|||||||
types:
|
types:
|
||||||
- "ready_for_review"
|
- "ready_for_review"
|
||||||
- "opened"
|
- "opened"
|
||||||
|
- "synchronize"
|
||||||
|
- "reopened"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
|
|||||||
@@ -213,14 +213,10 @@ export const useAppEvents = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onSyncComplete = useCallback(async () => {
|
const onSyncComplete = useCallback(async () => {
|
||||||
|
console.log('Sync complete');
|
||||||
initAfterSync();
|
initAfterSync();
|
||||||
setLastSynced(await db.lastSynced());
|
setLastSynced(await db.lastSynced());
|
||||||
eSendEvent(eCloseProgressDialog, "sync_progress");
|
eSendEvent(eCloseProgressDialog, "sync_progress");
|
||||||
let id = useEditorStore.getState().currentEditingNote;
|
|
||||||
let note = id && db.notes.note(id).data;
|
|
||||||
if (note) {
|
|
||||||
//await updateNoteInEditor();
|
|
||||||
}
|
|
||||||
}, [setLastSynced]);
|
}, [setLastSynced]);
|
||||||
|
|
||||||
const onUrlRecieved = useCallback(
|
const onUrlRecieved = useCallback(
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export type Note = {
|
|||||||
export type Content = {
|
export type Content = {
|
||||||
data?: string;
|
data?: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
noteId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SavePayload = {
|
export type SavePayload = {
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ import { DDS } from "../../../services/device-detection";
|
|||||||
import {
|
import {
|
||||||
eSendEvent,
|
eSendEvent,
|
||||||
eSubscribeEvent,
|
eSubscribeEvent,
|
||||||
eUnSubscribeEvent
|
eUnSubscribeEvent,
|
||||||
|
openVault
|
||||||
} from "../../../services/event-manager";
|
} from "../../../services/event-manager";
|
||||||
import Navigation from "../../../services/navigation";
|
import Navigation from "../../../services/navigation";
|
||||||
import { TipManager } from "../../../services/tip-manager";
|
import { TipManager } from "../../../services/tip-manager";
|
||||||
@@ -33,7 +34,7 @@ import { useEditorStore } from "../../../stores/use-editor-store";
|
|||||||
import { useNoteStore } from "../../../stores/use-notes-store";
|
import { useNoteStore } from "../../../stores/use-notes-store";
|
||||||
import { useTagStore } from "../../../stores/use-tag-store";
|
import { useTagStore } from "../../../stores/use-tag-store";
|
||||||
import { ThemeStore, useThemeStore } from "../../../stores/use-theme-store";
|
import { ThemeStore, useThemeStore } from "../../../stores/use-theme-store";
|
||||||
import { eOnLoadNote } from "../../../utils/events";
|
import { eClearEditor, eOnLoadNote } from "../../../utils/events";
|
||||||
import { tabBarRef } from "../../../utils/global-refs";
|
import { tabBarRef } from "../../../utils/global-refs";
|
||||||
import { timeConverter } from "../../../utils/time";
|
import { timeConverter } from "../../../utils/time";
|
||||||
import { NoteType } from "../../../utils/types";
|
import { NoteType } from "../../../utils/types";
|
||||||
@@ -49,6 +50,7 @@ import {
|
|||||||
makeSessionId,
|
makeSessionId,
|
||||||
post
|
post
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
|
import { EVENTS } from "@notesnook/core/common";
|
||||||
|
|
||||||
export const useEditor = (
|
export const useEditor = (
|
||||||
editorId = "",
|
editorId = "",
|
||||||
@@ -71,6 +73,8 @@ export const useEditor = (
|
|||||||
const insets = useGlobalSafeAreaInsets();
|
const insets = useGlobalSafeAreaInsets();
|
||||||
const isDefaultEditor = editorId === "";
|
const isDefaultEditor = editorId === "";
|
||||||
const saveCount = useRef(0);
|
const saveCount = useRef(0);
|
||||||
|
const lastSuccessfulSaveTime = useRef<number>(0);
|
||||||
|
const lock = useRef(false);
|
||||||
|
|
||||||
const postMessage = useCallback(
|
const postMessage = useCallback(
|
||||||
async <T>(type: string, data: T) =>
|
async <T>(type: string, data: T) =>
|
||||||
@@ -154,6 +158,7 @@ export const useEditor = (
|
|||||||
saveCount.current = 0;
|
saveCount.current = 0;
|
||||||
useEditorStore.getState().setReadonly(false);
|
useEditorStore.getState().setReadonly(false);
|
||||||
postMessage(EditorEvents.title, "");
|
postMessage(EditorEvents.title, "");
|
||||||
|
lastSuccessfulSaveTime.current = 0;
|
||||||
await commands.clearContent();
|
await commands.clearContent();
|
||||||
await commands.clearTags();
|
await commands.clearTags();
|
||||||
if (resetState) {
|
if (resetState) {
|
||||||
@@ -245,6 +250,8 @@ export const useEditor = (
|
|||||||
note = db.notes?.note(id)?.data as Note;
|
note = db.notes?.note(id)?.data as Note;
|
||||||
await commands.setStatus(timeConverter(note.dateEdited), "Saved");
|
await commands.setStatus(timeConverter(note.dateEdited), "Saved");
|
||||||
|
|
||||||
|
lastSuccessfulSaveTime.current = note.dateEdited;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
saveCount.current < 2 ||
|
saveCount.current < 2 ||
|
||||||
currentNote.current?.title !== note.title ||
|
currentNote.current?.title !== note.title ||
|
||||||
@@ -259,8 +266,8 @@ export const useEditor = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
saveCount.current++;
|
|
||||||
|
|
||||||
|
saveCount.current++;
|
||||||
return id;
|
return id;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Error saving note: ", e);
|
console.log("Error saving note: ", e);
|
||||||
@@ -274,7 +281,8 @@ export const useEditor = (
|
|||||||
if (note.locked || note.content) {
|
if (note.locked || note.content) {
|
||||||
currentContent.current = {
|
currentContent.current = {
|
||||||
data: note.content?.data,
|
data: note.content?.data,
|
||||||
type: note.content?.type || "tiny"
|
type: note.content?.type || "tiptap",
|
||||||
|
noteId: currentNote.current?.id as string
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
currentContent.current = await db.content?.raw(note.contentId);
|
currentContent.current = await db.content?.raw(note.contentId);
|
||||||
@@ -299,6 +307,7 @@ export const useEditor = (
|
|||||||
sessionHistoryId.current = Date.now();
|
sessionHistoryId.current = Date.now();
|
||||||
await commands.setSessionId(nextSessionId);
|
await commands.setSessionId(nextSessionId);
|
||||||
await commands.focus();
|
await commands.focus();
|
||||||
|
lastSuccessfulSaveTime.current = 0;
|
||||||
useEditorStore.getState().setReadonly(false);
|
useEditorStore.getState().setReadonly(false);
|
||||||
} else {
|
} else {
|
||||||
if (!item.forced && currentNote.current?.id === item.id) return;
|
if (!item.forced && currentNote.current?.id === item.id) return;
|
||||||
@@ -306,6 +315,7 @@ export const useEditor = (
|
|||||||
overlay(true, item);
|
overlay(true, item);
|
||||||
currentNote.current && (await reset(false));
|
currentNote.current && (await reset(false));
|
||||||
await loadContent(item as NoteType);
|
await loadContent(item as NoteType);
|
||||||
|
lastSuccessfulSaveTime.current = item.dateEdited;
|
||||||
const nextSessionId = makeSessionId(item as NoteType);
|
const nextSessionId = makeSessionId(item as NoteType);
|
||||||
sessionHistoryId.current = Date.now();
|
sessionHistoryId.current = Date.now();
|
||||||
setSessionId(nextSessionId);
|
setSessionId(nextSessionId);
|
||||||
@@ -347,6 +357,77 @@ export const useEditor = (
|
|||||||
}, 300);
|
}, 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const lockNoteWithVault = useCallback((note: NoteType) => {
|
||||||
|
eSendEvent(eClearEditor);
|
||||||
|
openVault({
|
||||||
|
item: note,
|
||||||
|
novault: true,
|
||||||
|
locked: true,
|
||||||
|
goToEditor: true,
|
||||||
|
title: "Open note",
|
||||||
|
description: "Unlock note to open it in editor."
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onSyncComplete = useCallback(
|
||||||
|
async (data: NoteType | Content) => {
|
||||||
|
if (!data) return;
|
||||||
|
const noteId = data.type === "tiptap" ? data.noteId : data.id;
|
||||||
|
|
||||||
|
if (!currentNote.current || noteId !== currentNote.current.id) return;
|
||||||
|
const isContentEncrypted = typeof (data as Content)?.data === "object";
|
||||||
|
const note = db.notes?.note(currentNote.current?.id).data as NoteType;
|
||||||
|
lock.current = true;
|
||||||
|
|
||||||
|
if (data.type === "tiptap") {
|
||||||
|
if (!currentNote.current.locked && isContentEncrypted) {
|
||||||
|
lockNoteWithVault(note);
|
||||||
|
} else if (currentNote.current.locked && isContentEncrypted) {
|
||||||
|
const decryptedContent = (await db.vault?.decryptContent(
|
||||||
|
data
|
||||||
|
)) as Content;
|
||||||
|
if (!decryptedContent) {
|
||||||
|
lockNoteWithVault(note);
|
||||||
|
} else {
|
||||||
|
await postMessage(EditorEvents.updatehtml, decryptedContent.data);
|
||||||
|
currentContent.current = decryptedContent;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const _nextContent = await db.content?.raw(note.contentId);
|
||||||
|
lastSuccessfulSaveTime.current = note.dateEdited;
|
||||||
|
if (_nextContent !== currentContent.current?.data) {
|
||||||
|
await postMessage(EditorEvents.updatehtml, _nextContent.data);
|
||||||
|
currentContent.current = _nextContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const note = data as NoteType;
|
||||||
|
if (note.title !== currentNote.current.title) {
|
||||||
|
postMessage(EditorEvents.title, note.title);
|
||||||
|
}
|
||||||
|
if (note.tags !== currentNote.current.tags) {
|
||||||
|
await commands.setTags(note);
|
||||||
|
}
|
||||||
|
await commands.setStatus(timeConverter(note.dateEdited), "Saved");
|
||||||
|
}
|
||||||
|
|
||||||
|
lock.current = false;
|
||||||
|
},
|
||||||
|
[commands, postMessage, lockNoteWithVault]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const syncCompletedSubscription = db.eventManager?.subscribe(
|
||||||
|
EVENTS.syncItemMerged,
|
||||||
|
onSyncComplete
|
||||||
|
);
|
||||||
|
eSubscribeEvent(eOnLoadNote + editorId, loadNote);
|
||||||
|
return () => {
|
||||||
|
syncCompletedSubscription?.unsubscribe();
|
||||||
|
eUnSubscribeEvent(eOnLoadNote + editorId, loadNote);
|
||||||
|
};
|
||||||
|
}, [editorId, loadNote, onSyncComplete]);
|
||||||
|
|
||||||
const saveContent = useCallback(
|
const saveContent = useCallback(
|
||||||
({
|
({
|
||||||
title,
|
title,
|
||||||
@@ -357,10 +438,12 @@ export const useEditor = (
|
|||||||
content?: string;
|
content?: string;
|
||||||
type: string;
|
type: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
if (lock.current) return;
|
||||||
if (type === EditorEvents.content) {
|
if (type === EditorEvents.content) {
|
||||||
currentContent.current = {
|
currentContent.current = {
|
||||||
data: content,
|
data: content,
|
||||||
type: "tiptap"
|
type: "tiptap",
|
||||||
|
noteId: currentNote.current?.id as string
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const params = {
|
const params = {
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export function editorState() {
|
|||||||
|
|
||||||
export const EditorEvents: { [name: string]: string } = {
|
export const EditorEvents: { [name: string]: string } = {
|
||||||
html: "native:html",
|
html: "native:html",
|
||||||
|
updatehtml: "native:updatehtml",
|
||||||
title: "native:title",
|
title: "native:title",
|
||||||
theme: "native:theme",
|
theme: "native:theme",
|
||||||
titleplaceholder: "native:titleplaceholder",
|
titleplaceholder: "native:titleplaceholder",
|
||||||
|
|||||||
@@ -159,9 +159,6 @@ const onUserStatusCheck = async (type) => {
|
|||||||
desc: "With Notesnook Pro you can add notes to your vault and do so much more! Get it now."
|
desc: "With Notesnook Pro you can add notes to your vault and do so much more! Get it now."
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case CHECK_IDS.databaseSync:
|
|
||||||
message = null;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message) {
|
if (message) {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import { ToastsModel } from "./toasts.model";
|
|||||||
import { TrashViewModel } from "./trash-view.model";
|
import { TrashViewModel } from "./trash-view.model";
|
||||||
|
|
||||||
export class AppModel {
|
export class AppModel {
|
||||||
private readonly page: Page;
|
readonly page: Page;
|
||||||
readonly toasts: ToastsModel;
|
readonly toasts: ToastsModel;
|
||||||
readonly navigation: NavigationMenuModel;
|
readonly navigation: NavigationMenuModel;
|
||||||
readonly auth: AuthModel;
|
readonly auth: AuthModel;
|
||||||
@@ -103,4 +103,13 @@ export class AppModel {
|
|||||||
(await this.page.locator(getTestId("sync-status-synced")).isVisible())
|
(await this.page.locator(getTestId("sync-status-synced")).isVisible())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async waitForSync(
|
||||||
|
state: "completed" | "synced" = "completed",
|
||||||
|
text?: string
|
||||||
|
) {
|
||||||
|
await this.page
|
||||||
|
.locator(getTestId(`sync-status-${state}`), { hasText: text })
|
||||||
|
.waitFor({ state: "visible" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ export class EditorModel {
|
|||||||
async typeTitle(text: string, delay = 0) {
|
async typeTitle(text: string, delay = 0) {
|
||||||
await this.editAndWait(async () => {
|
await this.editAndWait(async () => {
|
||||||
await this.title.focus();
|
await this.title.focus();
|
||||||
|
await this.title.press("End");
|
||||||
await this.title.type(text, { delay });
|
await this.title.type(text, { delay });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -160,8 +161,12 @@ export class EditorModel {
|
|||||||
|
|
||||||
async setTags(tags: string[]) {
|
async setTags(tags: string[]) {
|
||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
|
await this.tagInput.focus();
|
||||||
await this.tagInput.fill(tag);
|
await this.tagInput.fill(tag);
|
||||||
await this.tagInput.press("Enter");
|
await this.tagInput.press("Enter");
|
||||||
|
await this.tags
|
||||||
|
.locator(":scope", { hasText: new RegExp(`^${tag}$`) })
|
||||||
|
.waitFor();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -323,7 +323,6 @@ class SessionHistoryItemModel {
|
|||||||
async preview(password?: string) {
|
async preview(password?: string) {
|
||||||
await this.properties.open();
|
await this.properties.open();
|
||||||
const isLocked = await this.locked.isVisible();
|
const isLocked = await this.locked.isVisible();
|
||||||
|
|
||||||
await this.locator.click();
|
await this.locator.click();
|
||||||
if (password && isLocked) {
|
if (password && isLocked) {
|
||||||
await fillPasswordDialog(this.page, password);
|
await fillPasswordDialog(this.page, password);
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ test("#1002 Can't add a tag that's a substring of an existing tag", async ({
|
|||||||
await notes.createNote(NOTE);
|
await notes.createNote(NOTE);
|
||||||
|
|
||||||
await notes.editor.setTags(tags);
|
await notes.editor.setTags(tags);
|
||||||
await page.waitForTimeout(200);
|
|
||||||
|
|
||||||
const noteTags = await notes.editor.getTags();
|
const noteTags = await notes.editor.getTags();
|
||||||
expect(noteTags).toHaveLength(tags.length);
|
expect(noteTags).toHaveLength(tags.length);
|
||||||
|
|||||||
95
apps/web/__e2e__/sync.test.ts
Normal file
95
apps/web/__e2e__/sync.test.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the Notesnook project (https://notesnook.com/)
|
||||||
|
|
||||||
|
Copyright (C) 2022 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 { test, Browser, expect } from "@playwright/test";
|
||||||
|
import { AppModel } from "./models/app.model";
|
||||||
|
import { USER } from "./utils";
|
||||||
|
|
||||||
|
async function createDevice(browser: Browser) {
|
||||||
|
// Create two isolated browser contexts
|
||||||
|
const context = await browser.newContext();
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
const app = new AppModel(page);
|
||||||
|
await app.auth.goto();
|
||||||
|
await app.auth.login(USER.CURRENT);
|
||||||
|
await app.waitForSync();
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function actAndSync<T>(
|
||||||
|
devices: AppModel[],
|
||||||
|
...actions: (Promise<T> | undefined)[]
|
||||||
|
) {
|
||||||
|
const results = await Promise.all([
|
||||||
|
...actions.filter((a) => !!a),
|
||||||
|
...devices.map((d) => d.waitForSync("synced", "now")),
|
||||||
|
...devices.map((d) => d.page.waitForTimeout(1000))
|
||||||
|
]);
|
||||||
|
return results.slice(0, actions.length) as T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const NOTE = {
|
||||||
|
title: "Real-time sync test note 1"
|
||||||
|
};
|
||||||
|
|
||||||
|
test("edits in a note opened on 2 devices should sync in real-time", async ({
|
||||||
|
browser
|
||||||
|
}, info) => {
|
||||||
|
info.setTimeout(30 * 1000);
|
||||||
|
const newContent = makeid(24).repeat(2);
|
||||||
|
|
||||||
|
const [deviceA, deviceB] = await Promise.all([
|
||||||
|
createDevice(browser),
|
||||||
|
createDevice(browser)
|
||||||
|
]);
|
||||||
|
const [notesA, notesB] = await Promise.all(
|
||||||
|
[deviceA, deviceB].map((d) => d.goToNotes())
|
||||||
|
);
|
||||||
|
const noteB =
|
||||||
|
(await notesB.findNote(NOTE)) ||
|
||||||
|
(await actAndSync([deviceA, deviceB], notesB.createNote(NOTE)))[0];
|
||||||
|
const noteA = await notesA.findNote(NOTE);
|
||||||
|
await Promise.all([noteA, noteB].map((note) => note?.openNote()));
|
||||||
|
|
||||||
|
const [beforeContentA, beforeContentB] = await Promise.all(
|
||||||
|
[notesA, notesB].map((notes) => notes?.editor.getContent("text"))
|
||||||
|
);
|
||||||
|
await actAndSync([deviceA, deviceB], notesB.editor.setContent(newContent));
|
||||||
|
const [afterContentA, afterContentB] = await Promise.all(
|
||||||
|
[notesA, notesB].map((notes) => notes?.editor.getContent("text"))
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(noteA).toBeDefined();
|
||||||
|
expect(noteB).toBeDefined();
|
||||||
|
expect(beforeContentA).toBe(beforeContentB);
|
||||||
|
expect(afterContentA).toBe(`${newContent}${beforeContentA}`);
|
||||||
|
expect(afterContentB).toBe(`${newContent}${beforeContentB}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
function makeid(length: number) {
|
||||||
|
let result = "";
|
||||||
|
const characters =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
const charactersLength = characters.length;
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ import { resetReminders } from "./common/reminders";
|
|||||||
import { introduceFeatures, showUpgradeReminderDialogs } from "./common";
|
import { introduceFeatures, showUpgradeReminderDialogs } from "./common";
|
||||||
import { AppEventManager, AppEvents } from "./common/app-events";
|
import { AppEventManager, AppEvents } from "./common/app-events";
|
||||||
import { db } from "./common/db";
|
import { db } from "./common/db";
|
||||||
import { CHECK_IDS, EV, EVENTS } from "@notesnook/core/common";
|
import { EV, EVENTS } from "@notesnook/core/common";
|
||||||
import { registerKeyMap } from "./common/key-map";
|
import { registerKeyMap } from "./common/key-map";
|
||||||
import { isUserPremium } from "./hooks/use-is-user-premium";
|
import { isUserPremium } from "./hooks/use-is-user-premium";
|
||||||
import useAnnouncements from "./hooks/use-announcements";
|
import useAnnouncements from "./hooks/use-announcements";
|
||||||
@@ -70,12 +70,11 @@ export default function AppEffects({ setShow }) {
|
|||||||
if (isUserPremium()) {
|
if (isUserPremium()) {
|
||||||
return { type, result: true };
|
return { type, result: true };
|
||||||
} else {
|
} else {
|
||||||
if (type !== CHECK_IDS.databaseSync)
|
showToast(
|
||||||
showToast(
|
"error",
|
||||||
"error",
|
"Please upgrade your account to Pro to use this feature.",
|
||||||
"Please upgrade your account to Pro to use this feature.",
|
[{ text: "Upgrade now", onClick: () => showBuyDialog() }]
|
||||||
[{ text: "Upgrade now", onClick: () => showBuyDialog() }]
|
);
|
||||||
);
|
|
||||||
return { type, result: false };
|
return { type, result: false };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import Toolbar from "./toolbar";
|
|||||||
import { AppEventManager, AppEvents } from "../../common/app-events";
|
import { AppEventManager, AppEvents } from "../../common/app-events";
|
||||||
import { FlexScrollContainer } from "../scroll-container";
|
import { FlexScrollContainer } from "../scroll-container";
|
||||||
import { formatDate } from "@notesnook/core/utils/date";
|
import { formatDate } from "@notesnook/core/utils/date";
|
||||||
import { debounceWithId } from "../../utils/debounce";
|
|
||||||
import Tiptap from "./tiptap";
|
import Tiptap from "./tiptap";
|
||||||
import Header from "./header";
|
import Header from "./header";
|
||||||
import { Attachment } from "../icons";
|
import { Attachment } from "../icons";
|
||||||
@@ -59,14 +58,6 @@ function onEditorChange(noteId: string, sessionId: string, content: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTitleChange(noteId: string, title: string) {
|
|
||||||
if (!title) return;
|
|
||||||
|
|
||||||
editorstore.get().setTitle(noteId, title);
|
|
||||||
}
|
|
||||||
|
|
||||||
const debouncedOnTitleChange = debounceWithId(onTitleChange, 100);
|
|
||||||
|
|
||||||
export default function EditorManager({
|
export default function EditorManager({
|
||||||
noteId,
|
noteId,
|
||||||
nonce
|
nonce
|
||||||
@@ -83,9 +74,9 @@ export default function EditorManager({
|
|||||||
const [timestamp, setTimestamp] = useState<number>(0);
|
const [timestamp, setTimestamp] = useState<number>(0);
|
||||||
|
|
||||||
const content = useRef<string>("");
|
const content = useRef<string>("");
|
||||||
const title = useRef<string>("");
|
|
||||||
const previewSession = useRef<PreviewSession>();
|
const previewSession = useRef<PreviewSession>();
|
||||||
const [dropRef, overlayRef] = useDragOverlay();
|
const [dropRef, overlayRef] = useDragOverlay();
|
||||||
|
const editorInstance = useEditorInstance();
|
||||||
|
|
||||||
const arePropertiesVisible = useStore((store) => store.arePropertiesVisible);
|
const arePropertiesVisible = useStore((store) => store.arePropertiesVisible);
|
||||||
const toggleProperties = useStore((store) => store.toggleProperties);
|
const toggleProperties = useStore((store) => store.toggleProperties);
|
||||||
@@ -93,14 +84,43 @@ export default function EditorManager({
|
|||||||
const isFocusMode = useAppStore((store) => store.isFocusMode);
|
const isFocusMode = useAppStore((store) => store.isFocusMode);
|
||||||
const isPreviewSession = !!previewSession.current;
|
const isPreviewSession = !!previewSession.current;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const event = db.eventManager.subscribe(
|
||||||
|
EVENTS.syncItemMerged,
|
||||||
|
async (item?: Record<string, string>) => {
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
const { id, contentId, locked } = editorstore.get().session;
|
||||||
|
const isContent = item.type === "tiptap" && item.id === contentId;
|
||||||
|
const isNote = item.type === "note" && item.id === id;
|
||||||
|
|
||||||
|
if (isContent) {
|
||||||
|
if (locked) {
|
||||||
|
const result = await db.vault?.decryptContent(item).catch(() => {});
|
||||||
|
if (result) item.data = result.data;
|
||||||
|
else EV.publish(EVENTS.vaultLocked);
|
||||||
|
}
|
||||||
|
|
||||||
|
editorInstance.current?.updateContent(item.data);
|
||||||
|
} else if (isNote) {
|
||||||
|
if (!locked && item.locked) return EV.publish(EVENTS.vaultLocked);
|
||||||
|
|
||||||
|
editorstore.get().updateSession(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return () => {
|
||||||
|
event.unsubscribe();
|
||||||
|
};
|
||||||
|
}, [editorInstance]);
|
||||||
|
|
||||||
const openSession = useCallback(async (noteId: string | number) => {
|
const openSession = useCallback(async (noteId: string | number) => {
|
||||||
await editorstore.get().openSession(noteId);
|
await editorstore.get().openSession(noteId);
|
||||||
|
|
||||||
const { getSessionContent, session } = editorstore.get();
|
const { getSessionContent } = editorstore.get();
|
||||||
const sessionContent = await getSessionContent();
|
const sessionContent = await getSessionContent();
|
||||||
|
|
||||||
previewSession.current = undefined;
|
previewSession.current = undefined;
|
||||||
title.current = session.title;
|
|
||||||
content.current = sessionContent?.data;
|
content.current = sessionContent?.data;
|
||||||
setTimestamp(Date.now());
|
setTimestamp(Date.now());
|
||||||
}, []);
|
}, []);
|
||||||
@@ -123,7 +143,6 @@ export default function EditorManager({
|
|||||||
(async function () {
|
(async function () {
|
||||||
await editorstore.newSession(nonce);
|
await editorstore.newSession(nonce);
|
||||||
|
|
||||||
title.current = "";
|
|
||||||
content.current = "";
|
content.current = "";
|
||||||
setTimestamp(Date.now());
|
setTimestamp(Date.now());
|
||||||
})();
|
})();
|
||||||
@@ -147,6 +166,7 @@ export default function EditorManager({
|
|||||||
flexDirection: "column"
|
flexDirection: "column"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* <UpdatesPendingNotice /> */}
|
||||||
{previewSession.current && (
|
{previewSession.current && (
|
||||||
<PreviewModeNotice
|
<PreviewModeNotice
|
||||||
{...previewSession.current}
|
{...previewSession.current}
|
||||||
@@ -155,7 +175,6 @@ export default function EditorManager({
|
|||||||
)}
|
)}
|
||||||
<Editor
|
<Editor
|
||||||
nonce={timestamp}
|
nonce={timestamp}
|
||||||
title={title.current}
|
|
||||||
content={content.current}
|
content={content.current}
|
||||||
options={{
|
options={{
|
||||||
readonly: isReadonly || isPreviewSession,
|
readonly: isReadonly || isPreviewSession,
|
||||||
@@ -188,7 +207,6 @@ type EditorOptions = {
|
|||||||
};
|
};
|
||||||
type EditorProps = {
|
type EditorProps = {
|
||||||
content: string;
|
content: string;
|
||||||
title?: string;
|
|
||||||
nonce?: number;
|
nonce?: number;
|
||||||
options?: EditorOptions;
|
options?: EditorOptions;
|
||||||
};
|
};
|
||||||
@@ -268,7 +286,7 @@ export function Editor(props: EditorProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function EditorChrome(props: PropsWithChildren<EditorProps>) {
|
function EditorChrome(props: PropsWithChildren<EditorProps>) {
|
||||||
const { title, nonce, options, children } = props;
|
const { options, children } = props;
|
||||||
const { readonly, focusMode, headless, onRequestFocus } = options || {
|
const { readonly, focusMode, headless, onRequestFocus } = options || {
|
||||||
headless: false,
|
headless: false,
|
||||||
readonly: false,
|
readonly: false,
|
||||||
@@ -313,17 +331,7 @@ function EditorChrome(props: PropsWithChildren<EditorProps>) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{title !== undefined ? (
|
<Titlebox readonly={readonly || false} />
|
||||||
<Titlebox
|
|
||||||
nonce={nonce}
|
|
||||||
readonly={readonly || false}
|
|
||||||
setTitle={(title) => {
|
|
||||||
const { sessionId, id } = editorstore.get().session;
|
|
||||||
debouncedOnTitleChange(sessionId, id, title);
|
|
||||||
}}
|
|
||||||
title={title}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<Header readonly={readonly} />
|
<Header readonly={readonly} />
|
||||||
{children}
|
{children}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -153,13 +153,14 @@ function TipTap(props: TipTapProps) {
|
|||||||
});
|
});
|
||||||
if (onLoad) onLoad();
|
if (onLoad) onLoad();
|
||||||
},
|
},
|
||||||
onUpdate: ({ editor }) => {
|
onUpdate: ({ editor, transaction }) => {
|
||||||
if (!editor.isEditable) return;
|
const preventSave = transaction?.getMeta("preventSave") as boolean;
|
||||||
const { id, sessionId } = editorstore.get().session;
|
const { id, sessionId } = editorstore.get().session;
|
||||||
const content = editor.state.doc.content;
|
const content = editor.state.doc.content;
|
||||||
const text = editor.view.dom.innerText;
|
const text = editor.view.dom.innerText;
|
||||||
|
|
||||||
deferredSave(sessionId, text, () => {
|
deferredSave(sessionId, text, () => {
|
||||||
|
if (!editor.isEditable || preventSave) return;
|
||||||
const html = getHTMLFromFragment(content, editor.schema);
|
const html = getHTMLFromFragment(content, editor.schema);
|
||||||
onChange?.(id, sessionId, html);
|
onChange?.(id, sessionId, html);
|
||||||
});
|
});
|
||||||
@@ -315,6 +316,21 @@ function toIEditor(editor: Editor): IEditor {
|
|||||||
focus: () => editor.current?.commands.focus("start"),
|
focus: () => editor.current?.commands.focus("start"),
|
||||||
undo: () => editor.current?.commands.undo(),
|
undo: () => editor.current?.commands.undo(),
|
||||||
redo: () => editor.current?.commands.redo(),
|
redo: () => editor.current?.commands.redo(),
|
||||||
|
updateContent: (content) => {
|
||||||
|
const { from, to } = editor.state.selection;
|
||||||
|
editor.current
|
||||||
|
?.chain()
|
||||||
|
.command(({ tr }) => {
|
||||||
|
tr.setMeta("preventSave", true);
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.setContent(content, true)
|
||||||
|
.setTextSelection({
|
||||||
|
from,
|
||||||
|
to
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
},
|
||||||
attachFile: (file: Attachment) => {
|
attachFile: (file: Attachment) => {
|
||||||
if (file.dataurl) {
|
if (file.dataurl) {
|
||||||
editor.current?.commands.insertImage({ ...file, src: file.dataurl });
|
editor.current?.commands.insertImage({ ...file, src: file.dataurl });
|
||||||
|
|||||||
@@ -17,31 +17,27 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { Input } from "@theme-ui/components";
|
import { Input } from "@theme-ui/components";
|
||||||
|
import { useStore, store } from "../../stores/editor-store";
|
||||||
|
import { debounceWithId } from "../../utils/debounce";
|
||||||
|
|
||||||
type TitleBoxProps = {
|
type TitleBoxProps = {
|
||||||
nonce?: number;
|
|
||||||
readonly: boolean;
|
readonly: boolean;
|
||||||
title: string;
|
|
||||||
setTitle: (title: string) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function TitleBox(props: TitleBoxProps) {
|
function TitleBox(props: TitleBoxProps) {
|
||||||
const { readonly, setTitle, title, nonce } = props;
|
const { readonly } = props;
|
||||||
const [currentTitle, setCurrentTitle] = useState<string>("");
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const title = useStore((store) => store.session.title);
|
||||||
|
|
||||||
useEffect(
|
useEffect(() => {
|
||||||
() => {
|
if (inputRef.current) inputRef.current.value = title;
|
||||||
setCurrentTitle(title);
|
}, [title]);
|
||||||
},
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
[nonce]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
value={currentTitle}
|
ref={inputRef}
|
||||||
variant="clean"
|
variant="clean"
|
||||||
data-test-id="editor-title"
|
data-test-id="editor-title"
|
||||||
className="editorTitle"
|
className="editorTitle"
|
||||||
@@ -55,16 +51,20 @@ function TitleBox(props: TitleBoxProps) {
|
|||||||
width: "100%"
|
width: "100%"
|
||||||
}}
|
}}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setCurrentTitle(e.target.value);
|
const { sessionId, id } = store.get().session;
|
||||||
setTitle(e.target.value);
|
debouncedOnTitleChange(sessionId, id, e.target.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default React.memo(TitleBox, (prevProps, nextProps) => {
|
export default React.memo(TitleBox, (prevProps, nextProps) => {
|
||||||
return (
|
return prevProps.readonly === nextProps.readonly;
|
||||||
prevProps.readonly === nextProps.readonly &&
|
|
||||||
prevProps.nonce === nextProps.nonce
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function onTitleChange(noteId: string, title: string) {
|
||||||
|
if (!title) return;
|
||||||
|
store.get().setTitle(noteId, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
const debouncedOnTitleChange = debounceWithId(onTitleChange, 100);
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export interface IEditor {
|
|||||||
focus: () => void;
|
focus: () => void;
|
||||||
undo: () => void;
|
undo: () => void;
|
||||||
redo: () => void;
|
redo: () => void;
|
||||||
|
updateContent: (content: string) => void;
|
||||||
attachFile: (file: Attachment) => void;
|
attachFile: (file: Attachment) => void;
|
||||||
loadImage: (hash: string, src: string) => void;
|
loadImage: (hash: string, src: string) => void;
|
||||||
sendAttachmentProgress: (
|
sendAttachmentProgress: (
|
||||||
|
|||||||
@@ -74,17 +74,25 @@ class EditorStore extends BaseStore {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
refresh = async () => {
|
|
||||||
const { id } = this.get().session;
|
|
||||||
await this.openSession(id, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
refreshTags = () => {
|
refreshTags = () => {
|
||||||
this.set((state) => {
|
this.set((state) => {
|
||||||
state.session.tags = state.session.tags.slice();
|
state.session.tags = state.session.tags.slice();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
state.session.dateEdited = item.dateEdited;
|
||||||
|
state.session.dateCreated = item.dateCreated;
|
||||||
|
state.session.locked = item.locked;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
openLockedSession = async (note) => {
|
openLockedSession = async (note) => {
|
||||||
this.set((state) => {
|
this.set((state) => {
|
||||||
state.session = {
|
state.session = {
|
||||||
|
|||||||
@@ -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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { checkIsUserPremium, CHECK_IDS, EVENTS } from "../../common";
|
import { EVENTS } from "../../common";
|
||||||
import { logger } from "../../logger";
|
import { logger } from "../../logger";
|
||||||
|
|
||||||
export class AutoSync {
|
export class AutoSync {
|
||||||
@@ -36,8 +36,6 @@ export class AutoSync {
|
|||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
this.logger.info(`Auto sync requested`);
|
this.logger.info(`Auto sync requested`);
|
||||||
|
|
||||||
if (!(await checkIsUserPremium(CHECK_IDS.databaseSync))) return;
|
|
||||||
if (this.isAutoSyncing) return;
|
if (this.isAutoSyncing) return;
|
||||||
|
|
||||||
this.isAutoSyncing = true;
|
this.isAutoSyncing = true;
|
||||||
@@ -64,9 +62,16 @@ export class AutoSync {
|
|||||||
if (item && (item.remote || item.localOnly || item.failed)) return;
|
if (item && (item.remote || item.localOnly || item.failed)) return;
|
||||||
|
|
||||||
clearTimeout(this.timeout);
|
clearTimeout(this.timeout);
|
||||||
|
// auto sync interval must not be 0 to avoid issues
|
||||||
|
// during data collection which works based on Date.now().
|
||||||
|
// It is required that the dateModified of an item should
|
||||||
|
// be a few milliseconds less than Date.now(). Setting sync
|
||||||
|
// interval to 0 causes a conflict where Date.now() & dateModified
|
||||||
|
// are equal causing the item to not be synced.
|
||||||
|
const interval = item && item.type === "tiptap" ? 100 : this.interval;
|
||||||
this.timeout = setTimeout(() => {
|
this.timeout = setTimeout(() => {
|
||||||
this.logger.info(`Sync requested by: ${id}`);
|
this.logger.info(`Sync requested by: ${id}`);
|
||||||
this.db.eventManager.publish(EVENTS.databaseSyncRequested, false, false);
|
this.db.eventManager.publish(EVENTS.databaseSyncRequested, false, false);
|
||||||
}, this.interval);
|
}, interval);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -381,11 +381,13 @@ class Sync {
|
|||||||
* @param {SyncTransferItem} syncStatus
|
* @param {SyncTransferItem} syncStatus
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
onSyncItem(syncStatus) {
|
async onSyncItem(syncStatus) {
|
||||||
const { item: itemJSON, itemType } = syncStatus;
|
const { item: itemJSON, itemType } = syncStatus;
|
||||||
const item = JSON.parse(itemJSON);
|
const item = JSON.parse(itemJSON);
|
||||||
|
|
||||||
return this.merger.mergeItem(itemType, item);
|
const remoteItem = await this.merger.mergeItem(itemType, item);
|
||||||
|
if (remoteItem)
|
||||||
|
this.db.eventManager.publish(EVENTS.syncItemMerged, remoteItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ class Merger {
|
|||||||
let localItem = await get(remoteItem.id);
|
let localItem = await get(remoteItem.id);
|
||||||
if (!localItem || remoteItem.dateModified > localItem.dateModified) {
|
if (!localItem || remoteItem.dateModified > localItem.dateModified) {
|
||||||
await add(remoteItem);
|
await add(remoteItem);
|
||||||
|
return remoteItem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,6 +181,7 @@ class Merger {
|
|||||||
|
|
||||||
if (!localItem) {
|
if (!localItem) {
|
||||||
await add(remoteItem);
|
await add(remoteItem);
|
||||||
|
return remoteItem;
|
||||||
} else {
|
} else {
|
||||||
const isResolved = localItem.dateResolved === remoteItem.dateModified;
|
const isResolved = localItem.dateResolved === remoteItem.dateModified;
|
||||||
const isModified = localItem.dateModified > this._lastSynced;
|
const isModified = localItem.dateModified > this._lastSynced;
|
||||||
@@ -194,6 +196,7 @@ class Merger {
|
|||||||
if (timeDiff < threshold) {
|
if (timeDiff < threshold) {
|
||||||
if (remoteItem.dateModified > localItem.dateModified) {
|
if (remoteItem.dateModified > localItem.dateModified) {
|
||||||
await add(remoteItem);
|
await add(remoteItem);
|
||||||
|
return remoteItem;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -210,6 +213,7 @@ class Merger {
|
|||||||
await markAsConflicted(localItem, remoteItem);
|
await markAsConflicted(localItem, remoteItem);
|
||||||
} else if (!isResolved) {
|
} else if (!isResolved) {
|
||||||
await add(remoteItem);
|
await add(remoteItem);
|
||||||
|
return remoteItem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -227,7 +231,7 @@ class Merger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (definition.conflict) {
|
if (definition.conflict) {
|
||||||
await this._mergeItemWithConflicts(
|
return await this._mergeItemWithConflicts(
|
||||||
item,
|
item,
|
||||||
definition.get,
|
definition.get,
|
||||||
definition.set,
|
definition.set,
|
||||||
@@ -235,7 +239,7 @@ class Merger {
|
|||||||
definition.threshold
|
definition.threshold
|
||||||
);
|
);
|
||||||
} else if (definition.get && definition.set) {
|
} else if (definition.get && definition.set) {
|
||||||
await this._mergeItem(item, definition.get, definition.set);
|
return await this._mergeItem(item, definition.get, definition.set);
|
||||||
} else if (!definition.get && definition.set) {
|
} else if (!definition.get && definition.set) {
|
||||||
await definition.set(item);
|
await definition.set(item);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -242,7 +242,12 @@ export default class Vault {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async decryptContent(encryptedContent, password) {
|
async decryptContent(encryptedContent, password = null) {
|
||||||
|
if (!password) {
|
||||||
|
await this._check();
|
||||||
|
password = this._password;
|
||||||
|
}
|
||||||
|
|
||||||
let decryptedContent = await this._storage.decrypt(
|
let decryptedContent = await this._storage.decrypt(
|
||||||
{ password },
|
{ password },
|
||||||
encryptedContent.data
|
encryptedContent.data
|
||||||
|
|||||||
@@ -54,8 +54,7 @@ export const CHECK_IDS = {
|
|||||||
noteExport: "note:export",
|
noteExport: "note:export",
|
||||||
vaultAdd: "vault:add",
|
vaultAdd: "vault:add",
|
||||||
notebookAdd: "notebook:add",
|
notebookAdd: "notebook:add",
|
||||||
backupEncrypt: "backup:encrypt",
|
backupEncrypt: "backup:encrypt"
|
||||||
databaseSync: "database:sync"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EVENTS = {
|
export const EVENTS = {
|
||||||
@@ -70,6 +69,7 @@ export const EVENTS = {
|
|||||||
databaseSyncRequested: "db:syncRequested",
|
databaseSyncRequested: "db:syncRequested",
|
||||||
syncProgress: "sync:progress",
|
syncProgress: "sync:progress",
|
||||||
syncCompleted: "sync:completed",
|
syncCompleted: "sync:completed",
|
||||||
|
syncItemMerged: "sync:itemMerged",
|
||||||
databaseUpdated: "db:updated",
|
databaseUpdated: "db:updated",
|
||||||
databaseCollectionInitiated: "db:collectionInitiated",
|
databaseCollectionInitiated: "db:collectionInitiated",
|
||||||
appRefreshRequested: "app:refreshRequested",
|
appRefreshRequested: "app:refreshRequested",
|
||||||
|
|||||||
@@ -109,6 +109,18 @@ export function useEditorController(update: () => void): EditorController {
|
|||||||
const value = message.value;
|
const value = message.value;
|
||||||
global.sessionId = message.sessionId;
|
global.sessionId = message.sessionId;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case "native:updatehtml": {
|
||||||
|
htmlContentRef.current = value;
|
||||||
|
if (!editor) break;
|
||||||
|
const { from, to } = editor.state.selection;
|
||||||
|
editor?.commands.setContent(htmlContentRef.current, false);
|
||||||
|
editor.commands.setTextSelection({
|
||||||
|
from,
|
||||||
|
to
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "native:html":
|
case "native:html":
|
||||||
htmlContentRef.current = value;
|
htmlContentRef.current = value;
|
||||||
update();
|
update();
|
||||||
|
|||||||
Reference in New Issue
Block a user