diff --git a/apps/mobile/app/components/attachments/actions.tsx b/apps/mobile/app/components/attachments/actions.tsx index 2e860ecd43..6d00516af5 100644 --- a/apps/mobile/app/components/attachments/actions.tsx +++ b/apps/mobile/app/components/attachments/actions.tsx @@ -182,18 +182,17 @@ const Actions = ({ relations .map((relation) => relation.fromId) .forEach(async (id) => { - const tab = useTabStore.getState().getTabForNote(id); - if (tab !== undefined) { - const isFocused = useTabStore.getState().currentTab === tab; + useTabStore.getState().forEachNoteTab(id, async (tab) => { + const isFocused = useTabStore.getState().currentTab === tab.id; if (isFocused) { eSendEvent(eOnLoadNote, { item: await db.notes.note(id), forced: true }); } else { - editorController.current.commands.setLoading(true, tab); + editorController.current.commands.setLoading(true, tab.id); } - } + }); }); close?.(); }, diff --git a/apps/mobile/app/components/properties/index.js b/apps/mobile/app/components/properties/index.js index dd7753c3c1..96cb1c5e43 100644 --- a/apps/mobile/app/components/properties/index.js +++ b/apps/mobile/app/components/properties/index.js @@ -141,7 +141,7 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => { close(); eSendEvent(eOnLoadNote, { item: item, - presistTab: true + newTab: true }); if (!DDS.isTab) { tabBarRef.current?.goToPage(1); diff --git a/apps/mobile/app/components/sheets/editor-tabs/index.tsx b/apps/mobile/app/components/sheets/editor-tabs/index.tsx index d62d421f54..a485b00b8d 100644 --- a/apps/mobile/app/components/sheets/editor-tabs/index.tsx +++ b/apps/mobile/app/components/sheets/editor-tabs/index.tsx @@ -97,11 +97,6 @@ const TabItemComponent = (props: { } props.close?.(); }} - onLongPress={() => { - useTabStore.getState().updateTab(props.tab.id, { - previewTab: false - }); - }} > { useTabStore.getState().updateTab(props.tab.id, { - pinned: !props.tab.pinned, - previewTab: false + pinned: !props.tab.pinned }); }} top={0} diff --git a/apps/mobile/app/components/sheets/link-note/index.tsx b/apps/mobile/app/components/sheets/link-note/index.tsx index 239dfbfa1e..41a0d38ec2 100644 --- a/apps/mobile/app/components/sheets/link-note/index.tsx +++ b/apps/mobile/app/components/sheets/link-note/index.tsx @@ -22,24 +22,22 @@ import { VirtualizedGrouping, createInternalLink } from "@notesnook/core"; +import type { LinkAttributes } from "@notesnook/editor"; +import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events"; +import { strings } from "@notesnook/intl"; import { useThemeColors } from "@notesnook/theme"; import React, { useEffect, useRef, useState } from "react"; import { TextInput, View } from "react-native"; import { FlatList } from "react-native-actions-sheet"; import { db } from "../../../common/database"; import { useDBItem } from "../../../hooks/use-db-item"; +import { editorController } from "../../../screens/editor/tiptap/utils"; import { presentSheet } from "../../../services/event-manager"; import { SIZE } from "../../../utils/size"; import { Button } from "../../ui/button"; import Input from "../../ui/input"; import { Pressable } from "../../ui/pressable"; import Paragraph from "../../ui/typography/paragraph"; -import type { LinkAttributes } from "@notesnook/editor"; -import { - EditorEvents, - editorController -} from "../../../screens/editor/tiptap/utils"; -import { strings } from "@notesnook/intl"; const ListNoteItem = ({ id, @@ -194,7 +192,7 @@ export default function LinkNote(props: { } : undefined ); - editorController.current?.postMessage(EditorEvents.resolve, { + editorController.current?.postMessage(NativeEvents.resolve, { data: { href: link, title: selectedNote.title diff --git a/apps/mobile/app/hooks/use-actions.tsx b/apps/mobile/app/hooks/use-actions.tsx index 28fc84108a..51004735e6 100644 --- a/apps/mobile/app/hooks/use-actions.tsx +++ b/apps/mobile/app/hooks/use-actions.tsx @@ -19,14 +19,14 @@ along with this program. If not, see . /* eslint-disable no-inner-declarations */ import { Color, + createInternalLink, ItemReference, Note, Notebook, Reminder, Tag, TrashItem, - VAULT_ERRORS, - createInternalLink + VAULT_ERRORS } from "@notesnook/core"; import { strings } from "@notesnook/intl"; import { DisplayedNotification } from "@notifee/react-native"; @@ -50,11 +50,11 @@ import ReminderSheet from "../components/sheets/reminder"; import { useSideBarDraggingStore } from "../components/side-menu/dragging-store"; import { useTabStore } from "../screens/editor/tiptap/use-tab-store"; import { - ToastManager, eSendEvent, eSubscribeEvent, openVault, - presentSheet + presentSheet, + ToastManager } from "../services/event-manager"; import Navigation from "../services/navigation"; import Notifications from "../services/notifications"; @@ -547,14 +547,13 @@ export const useActions = ({ const toggleReadyOnlyMode = async () => { const currentReadOnly = (item as Note).readonly; await db.notes.readonly(!currentReadOnly, item?.id); - - if (useTabStore.getState().hasTabForNote(item.id)) { - const tabId = useTabStore.getState().getTabForNote(item.id); - if (!tabId) return; - useTabStore.getState().updateTab(tabId, { - readonly: !currentReadOnly + useTabStore.getState().forEachNoteTab(item.id, (tab) => { + useTabStore.getState().updateTab(tab.id, { + session: { + readonly: !currentReadOnly + } }); - } + }); Navigation.queueRoutesForUpdate(); close(); }; diff --git a/apps/mobile/app/hooks/use-app-events.tsx b/apps/mobile/app/hooks/use-app-events.tsx index dd658ffe4e..e535622f3a 100644 --- a/apps/mobile/app/hooks/use-app-events.tsx +++ b/apps/mobile/app/hooks/use-app-events.tsx @@ -602,13 +602,15 @@ export const useAppEvents = () => { EV.subscribe(EVENTS.vaultLocked, async () => { // Lock all notes in all tabs... for (const tab of useTabStore.getState().tabs) { - const noteId = useTabStore.getState().getTab(tab.id)?.noteId; + const noteId = useTabStore.getState().getTab(tab.id)?.session?.noteId; if (!noteId) continue; const note = await db.notes.note(noteId); const locked = note && (await db.vaults.itemExists(note)); if (locked) { useTabStore.getState().updateTab(tab.id, { - locked: true + session: { + locked: true + } }); if ( tab.id === useTabStore.getState().currentTab && diff --git a/apps/mobile/app/navigation/tabs-holder.js b/apps/mobile/app/navigation/tabs-holder.js index fa7237c035..190f754e30 100644 --- a/apps/mobile/app/navigation/tabs-holder.js +++ b/apps/mobile/app/navigation/tabs-holder.js @@ -529,7 +529,9 @@ const onChangeTab = async (event) => { const locked = note && (await db.vaults.itemExists(note)); if (locked) { useTabStore.getState().updateTab(tab.id, { - locked: true + session: { + locked: true + } }); } } diff --git a/apps/mobile/app/package.json b/apps/mobile/app/package.json index ea3d8c0a75..587d67163e 100644 --- a/apps/mobile/app/package.json +++ b/apps/mobile/app/package.json @@ -43,7 +43,8 @@ "@trpc/client": "^10.45.2", "@trpc/react-query": "^10.45.2", "@trpc/server": "^10.45.2", - "@tanstack/react-query": "^4.36.1" + "@tanstack/react-query": "^4.36.1", + "async-mutex": "0.5.0" }, "sideEffects": false } diff --git a/apps/mobile/app/screens/editor/index.tsx b/apps/mobile/app/screens/editor/index.tsx index 9b36a029b4..0510669fd9 100755 --- a/apps/mobile/app/screens/editor/index.tsx +++ b/apps/mobile/app/screens/editor/index.tsx @@ -19,6 +19,8 @@ along with this program. If not, see . /* eslint-disable @typescript-eslint/no-var-requires */ +import { i18n } from "@lingui/core"; +import { strings } from "@notesnook/intl"; import React, { forwardRef, useCallback, @@ -46,6 +48,7 @@ import { eUnlockWithPassword } from "../../utils/events"; import { openLinkInBrowser } from "../../utils/functions"; +import { tabBarRef } from "../../utils/global-refs"; import EditorOverlay from "./loading"; import { EDITOR_URI } from "./source"; import { EditorProps, useEditorType } from "./tiptap/types"; @@ -58,9 +61,6 @@ import { openInternalLink, randId } from "./tiptap/utils"; -import { tabBarRef } from "../../utils/global-refs"; -import { strings } from "@notesnook/intl"; -import { i18n } from "@lingui/core"; const style: ViewStyle = { height: "100%", @@ -203,11 +203,13 @@ const useLockedNoteHandler = () => { useEffect(() => { for (const tab of useTabStore.getState().tabs) { - const noteId = useTabStore.getState().getTab(tab.id)?.noteId; + const noteId = useTabStore.getState().getTab(tab.id)?.session?.noteId; if (!noteId) continue; - if (tabRef.current && tabRef.current.noteLocked) { + if (tabRef.current && tabRef.current.session?.noteLocked) { useTabStore.getState().updateTab(tabRef.current.id, { - locked: true + session: { + locked: true + } }); } } @@ -221,23 +223,27 @@ const useLockedNoteHandler = () => { biometryAvailable: !!biometry, biometryEnrolled: !!fingerprint }); - syncTabs(); + syncTabs("biometry"); })(); }, [tab?.id]); useEffect(() => { const unlockWithBiometrics = async () => { try { - if (!tabRef.current?.noteLocked || !tabRef.current) return; + if (!tabRef.current?.session?.noteLocked || !tabRef.current) return; console.log("Trying to unlock with biometrics..."); const credentials = await BiometricService.getCredentials( "Unlock note", "Unlock note to open it in editor." ); - if (credentials && credentials?.password && tabRef.current.noteId) { + if ( + credentials && + credentials?.password && + tabRef.current.session?.noteId + ) { const note = await db.vault.open( - tabRef.current.noteId, + tabRef.current.session?.noteId, credentials?.password ); @@ -246,7 +252,9 @@ const useLockedNoteHandler = () => { }); useTabStore.getState().updateTab(tabRef.current.id, { - locked: false + session: { + locked: false + } }); } } catch (e) { @@ -261,7 +269,7 @@ const useLockedNoteHandler = () => { password: string; biometrics?: boolean; }) => { - if (!tabRef.current?.noteId || !tabRef.current) return; + if (!tabRef.current?.session?.noteId || !tabRef.current) return; if (!password || password.trim().length === 0) { ToastManager.show({ heading: strings.passwordNotEntered(), @@ -271,7 +279,10 @@ const useLockedNoteHandler = () => { } try { - const note = await db.vault.open(tabRef.current?.noteId, password); + const note = await db.vault.open( + tabRef.current?.session?.noteId, + password + ); if (enrollBiometrics && note) { try { const unlocked = await db.vault.unlock(password); @@ -302,7 +313,9 @@ const useLockedNoteHandler = () => { item: note }); useTabStore.getState().updateTab(tabRef.current.id, { - locked: false + session: { + locked: false + } }); } catch (e) { console.log(e); @@ -315,7 +328,7 @@ const useLockedNoteHandler = () => { const unlock = () => { if ( - (tabRef.current?.locked, + (tabRef.current?.session?.locked, useTabStore.getState().biometryAvailable && useTabStore.getState().biometryEnrolled && !editorState().movedAway) @@ -327,7 +340,7 @@ const useLockedNoteHandler = () => { console.log("Biometrics unavailable.", editorState().movedAway); if (!editorState().movedAway) { setTimeout(() => { - if (tabRef.current && tabRef.current?.locked) { + if (tabRef.current && tabRef.current?.session?.locked) { editorController.current?.commands.focus(tabRef.current?.id); } }, 100); @@ -342,13 +355,13 @@ const useLockedNoteHandler = () => { }), eSubscribeEvent(eUnlockWithPassword, onSubmit) ]; - if (tabRef.current?.locked && tabBarRef.current?.page() === 2) { + if (tabRef.current?.session?.locked && tabBarRef.current?.page() === 2) { unlock(); } return () => { subs.map((s) => s?.unsubscribe()); }; - }, [tab?.id, tab?.locked]); + }, [tab?.id, tab?.session?.locked]); return null; }; diff --git a/apps/mobile/app/screens/editor/readonly-editor.tsx b/apps/mobile/app/screens/editor/readonly-editor.tsx index ec9234ffb6..dc3ca1c993 100644 --- a/apps/mobile/app/screens/editor/readonly-editor.tsx +++ b/apps/mobile/app/screens/editor/readonly-editor.tsx @@ -27,10 +27,10 @@ import WebView from "react-native-webview"; import { useRef } from "react"; import { EDITOR_URI } from "./source"; import { EditorMessage } from "./tiptap/types"; -import { EventTypes } from "./tiptap/editor-events"; +import { EditorEvents } from "@notesnook/editor-mobile/src/utils/editor-events"; import { Attachment } from "@notesnook/editor"; import downloadAttachment from "../../common/filesystem/download-attachment"; -import { EditorEvents } from "./tiptap/utils"; +import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events"; import { useThemeColors } from "@notesnook/theme"; import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets"; import { db } from "../../common/database"; @@ -69,11 +69,11 @@ export function ReadonlyEditor(props: { const data = event.nativeEvent.data; const editorMessage = JSON.parse(data) as EditorMessage; - if (editorMessage.type === EventTypes.logger) { + if (editorMessage.type === EditorEvents.logger) { logger.info("[READONLY EDITOR LOG]", editorMessage.value); } - if (editorMessage.type === EventTypes.readonlyEditorLoaded) { + if (editorMessage.type === EditorEvents.readonlyEditorLoaded) { console.log("Readonly editor loaded."); props.onLoad?.((content: { data: string; id: string }) => { setTimeout(() => { @@ -87,7 +87,7 @@ export function ReadonlyEditor(props: { setLoading(false); }, 300); }); - } else if (editorMessage.type === EventTypes.getAttachmentData) { + } else if (editorMessage.type === EditorEvents.getAttachmentData) { const attachment = (editorMessage.value as any).attachment as Attachment; console.log("Getting attachment data:", attachment.hash, attachment.type); @@ -106,7 +106,7 @@ export function ReadonlyEditor(props: { ); editorRef.current?.postMessage( JSON.stringify({ - type: EditorEvents.attachmentData, + type: NativeEvents.attachmentData, value: { resolverId: (editorMessage.value as any).resolverId, data @@ -118,7 +118,7 @@ export function ReadonlyEditor(props: { console.log("Error downloading attachment data"); editorRef.current?.postMessage( JSON.stringify({ - type: EditorEvents.attachmentData, + type: NativeEvents.attachmentData, data: { resolverId: (editorMessage.value as any).resolverId, data: undefined diff --git a/apps/mobile/app/screens/editor/source.ts b/apps/mobile/app/screens/editor/source.ts index 3f343edf29..b7c31e88b5 100644 --- a/apps/mobile/app/screens/editor/source.ts +++ b/apps/mobile/app/screens/editor/source.ts @@ -28,5 +28,5 @@ const EditorMobileSourceUrl = * The url should be something like this: http://192.168.100.126:3000/index.html */ export const EDITOR_URI = __DEV__ - ? EditorMobileSourceUrl + ? "http://192.168.100.8:3000/index.html" : EditorMobileSourceUrl; diff --git a/apps/mobile/app/screens/editor/tiptap/commands.ts b/apps/mobile/app/screens/editor/tiptap/commands.ts index 99577e7fc6..b7bad77dab 100644 --- a/apps/mobile/app/screens/editor/tiptap/commands.ts +++ b/apps/mobile/app/screens/editor/tiptap/commands.ts @@ -30,6 +30,7 @@ import { sleep } from "../../../utils/time"; import { Settings } from "./types"; import { useTabStore } from "./use-tab-store"; import { getResponse, randId, textInput } from "./utils"; +import { EditorSessionItem } from "@notesnook/common"; type Action = { job: string; id: string }; @@ -77,13 +78,13 @@ class Commands { focus = async (tabId: number) => { if (!this.ref.current) return; + + const locked = useTabStore.getState().getTab(tabId)?.session?.locked; if (Platform.OS === "android") { //this.ref.current?.requestFocus(); setTimeout(async () => { if (!this.ref) return; textInput.current?.focus(); - - const locked = useTabStore.getState().getTab(tabId)?.locked; await this.doAsync( locked ? `editorControllers[${tabId}]?.focusPassInput();` @@ -95,7 +96,12 @@ class Commands { }, 1); } else { await sleep(400); - await this.doAsync(`editors[${tabId}]?.commands.focus()`, "focus"); + await this.doAsync( + locked + ? `editorControllers[${tabId}]?.focusPassInput();` + : `editors[${tabId}]?.commands.focus()`, + "focus" + ); } }; @@ -167,9 +173,10 @@ if (typeof statusBar !== "undefined") { setLoading = async (loading?: boolean, tabId?: number) => { await this.doAsync(` const editorController = editorControllers[${ - tabId || useTabStore.getState().currentTab + tabId === undefined ? useTabStore.getState().currentTab : tabId }]; editorController.setLoading(${loading}) + logger("info", editorController.setLoading); `); }; @@ -215,11 +222,11 @@ if (typeof statusBar !== "undefined") { setTags = async (note: Note | null | undefined) => { if (!note) return; - const tabId = useTabStore.getState().getTabForNote(note.id); - - const tags = await db.relations.to(note, "tag").resolve(); - await this.doAsync( - ` + useTabStore.getState().forEachNoteTab(note.id, async (tab) => { + const tabId = tab.id; + const tags = await db.relations.to(note, "tag").resolve(); + await this.doAsync( + ` const tags = editorTags[${tabId}]; if (tags && tags.current) { tags.current.setTags(${JSON.stringify( @@ -232,8 +239,9 @@ if (typeof statusBar !== "undefined") { )}); } `, - "setTags" - ); + "setTags" + ); + }); }; clearTags = async (tabId: number) => { @@ -353,7 +361,46 @@ editor && editor.commands.insertImage({ response = editorControllers[${tabId}]?.scrollIntoView("${id}") || []; `); }; - //todo add replace image function + + newSession = async (sessionId: string, tabId: number, noteId: string) => { + return this.doAsync(` + globalThis.sessions.newSession("${sessionId}", ${tabId}, "${noteId}"); + `); + }; + + getSession = async (id: string): Promise => { + return this.doAsync(` + response = globalThis.sessions.get("${id}"); + `); + }; + + deleteSession = async (id: string) => { + return this.doAsync(` + globalThis.sessions.delete("${id}"); + `); + }; + + deleteSessionsForTabId = async (tabId: number) => { + return this.doAsync(` + globalThis.sessions.deleteForTabId(${tabId}); + `); + }; + + updateSession = async ( + id: string, + session: { + tabId: number; + noteId: string; + scrollTop: number; + from: number; + to: number; + sessionId: string; + } + ) => { + return this.doAsync(` + globalThis.sessions.updateSession("${id}", ${JSON.stringify(session)}); + `); + }; } export default Commands; diff --git a/apps/mobile/app/screens/editor/tiptap/use-editor-events.tsx b/apps/mobile/app/screens/editor/tiptap/use-editor-events.tsx index fc098c3e5b..9fcca1c6ad 100644 --- a/apps/mobile/app/screens/editor/tiptap/use-editor-events.tsx +++ b/apps/mobile/app/screens/editor/tiptap/use-editor-events.tsx @@ -21,7 +21,10 @@ along with this program. If not, see . /* eslint-disable @typescript-eslint/no-var-requires */ import { ItemReference } from "@notesnook/core"; import type { Attachment } from "@notesnook/editor"; +import { EditorEvents } from "@notesnook/editor-mobile/src/utils/editor-events"; +import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events"; import { getDefaultPresets } from "@notesnook/editor/dist/cjs/toolbar/tool-definitions"; +import { strings } from "@notesnook/intl"; import Clipboard from "@react-native-clipboard/clipboard"; import React, { useCallback, useEffect, useRef } from "react"; import { @@ -72,11 +75,9 @@ import { import { openLinkInBrowser } from "../../../utils/functions"; import { tabBarRef } from "../../../utils/global-refs"; import { useDragState } from "../../settings/editor/state"; -import { EventTypes } from "./editor-events"; import { EditorMessage, EditorProps, useEditorType } from "./types"; import { useTabStore } from "./use-tab-store"; -import { EditorEvents, editorState, openInternalLink } from "./utils"; -import { strings } from "@notesnook/intl"; +import { editorState, openInternalLink } from "./utils"; const publishNote = async () => { const user = useUserStore.getState().user; @@ -177,7 +178,7 @@ export const useEditorEvents = ( useEffect(() => { const handleKeyboardDidShow: KeyboardEventListener = () => { editor.commands.keyboardShown(true); - editor.postMessage(EditorEvents.keyboardShown, undefined); + editor.postMessage(NativeEvents.keyboardShown, undefined); }; const handleKeyboardDidHide: KeyboardEventListener = () => { editor.commands.keyboardShown(false); @@ -353,25 +354,25 @@ export const useEditorEvents = ( const editorMessage = JSON.parse(data) as EditorMessage; if (editorMessage.hasTimeout && editorMessage.resolverId) { - editor.postMessage(EditorEvents.resolve, { + editor.postMessage(NativeEvents.resolve, { data: true, resolverId: editorMessage.resolverId }); } - if (editorMessage.type === EventTypes.load) { + if (editorMessage.type === EditorEvents.load) { DatabaseLogger.log("Editor is ready"); editor.onLoad(); return; } - if (editorMessage.type === EventTypes.back) { + if (editorMessage.type === EditorEvents.back) { return onBackPress(); } if ( editorMessage.sessionId !== editor.sessionId.current && - editorMessage.type !== EditorEvents.status + editorMessage.type !== NativeEvents.status ) { return; } @@ -381,8 +382,8 @@ export const useEditorEvents = ( .getNoteIdForTab(editorMessage.tabId); switch (editorMessage.type) { - case EventTypes.content: - DatabaseLogger.log("EventTypes.content"); + case EditorEvents.content: + DatabaseLogger.log("EditorEvents.content"); editor.saveContent({ type: editorMessage.type, content: editorMessage.value.html as string, @@ -392,8 +393,8 @@ export const useEditorEvents = ( pendingChanges: editorMessage.value?.pendingChanges }); break; - case EventTypes.title: - DatabaseLogger.log("EventTypes.title"); + case EditorEvents.title: + DatabaseLogger.log("EditorEvents.title"); editor.saveContent({ type: editorMessage.type, title: editorMessage.value?.title as string, @@ -403,10 +404,10 @@ export const useEditorEvents = ( pendingChanges: editorMessage.value?.pendingChanges }); break; - case EventTypes.logger: + case EditorEvents.logger: logger.info("[EDITOR LOG]", editorMessage.value); break; - case EventTypes.dbLogger: + case EditorEvents.dbLogger: if (editorMessage.value.error) { DatabaseLogger.error( editorMessage.value.error, @@ -419,12 +420,12 @@ export const useEditorEvents = ( DatabaseLogger.info("[EDITOR_LOG]" + editorMessage.value.message); } break; - case EventTypes.contentchange: + case EditorEvents.contentchange: editor.onContentChanged(editorMessage.noteId); break; - case EventTypes.selection: + case EditorEvents.selection: break; - case EventTypes.reminders: + case EditorEvents.reminders: if (!noteId) { ToastManager.show({ heading: strings.createNoteFirst(), @@ -442,7 +443,7 @@ export const useEditorEvents = ( onAdd: () => ReminderSheet.present(undefined, note, true) }); break; - case EventTypes.newtag: + case EditorEvents.newtag: if (!noteId) { ToastManager.show({ heading: strings.createNoteFirst(), @@ -452,7 +453,7 @@ export const useEditorEvents = ( } ManageTagsSheet.present([noteId]); break; - case EventTypes.tag: + case EditorEvents.tag: if (editorMessage.value) { if (!noteId) return; const note = await db.notes.note(noteId); @@ -468,7 +469,7 @@ export const useEditorEvents = ( }); } break; - case EventTypes.filepicker: + case EditorEvents.filepicker: editorState().isAwaitingResult = true; const { pick } = require("./picker").default; pick({ @@ -480,14 +481,14 @@ export const useEditorEvents = ( editorState().isAwaitingResult = false; }, 1000); break; - case EventTypes.download: { + case EditorEvents.download: { const downloadAttachment = require("../../../common/filesystem/download-attachment").default; downloadAttachment((editorMessage.value as Attachment)?.hash, true); break; } - case EventTypes.getAttachmentData: { + case EditorEvents.getAttachmentData: { const attachment = (editorMessage.value as any) ?.attachment as Attachment; @@ -507,14 +508,14 @@ export const useEditorEvents = ( !!data, editorMessage.resolverId ); - editor.postMessage(EditorEvents.resolve, { + editor.postMessage(NativeEvents.resolve, { resolverId: editorMessage.resolverId, data }); }) .catch((e) => { DatabaseLogger.error(e); - editor.postMessage(EditorEvents.resolve, { + editor.postMessage(NativeEvents.resolve, { resolverId: editorMessage.resolverId, data: undefined }); @@ -523,26 +524,26 @@ export const useEditorEvents = ( break; } - case EventTypes.pro: + case EditorEvents.pro: if (editor.state.current?.isFocused) { editor.state.current.isFocused = true; } eSendEvent(eOpenPremiumDialog); break; - case EventTypes.monograph: + case EditorEvents.monograph: publishNote(); break; - case EventTypes.properties: + case EditorEvents.properties: showActionsheet(); break; - case EventTypes.scroll: + case EditorEvents.scroll: editorState().scrollPosition = editorMessage.value; break; - case EventTypes.fullscreen: + case EditorEvents.fullscreen: editorState().isFullscreen = true; eSendEvent(eOpenFullscreenEditor); break; - case EventTypes.link: + case EditorEvents.link: if (editorMessage.value.startsWith("nn://")) { openInternalLink(editorMessage.value); console.log( @@ -554,7 +555,7 @@ export const useEditorEvents = ( } break; - case EventTypes.previewAttachment: { + case EditorEvents.previewAttachment: { const hash = (editorMessage.value as Attachment)?.hash; const attachment = await db.attachments?.attachment(hash); if (!attachment) return; @@ -565,11 +566,26 @@ export const useEditorEvents = ( } break; } - case EventTypes.copyToClipboard: { + case EditorEvents.copyToClipboard: { Clipboard.setString(editorMessage.value as string); break; } - case EventTypes.tabsChanged: { + case EditorEvents.saveScroll: { + useTabStore.getState().updateTab(editorMessage.tabId, { + session: { + ...editorMessage.value + } + }); + break; + } + case EditorEvents.newNote: { + eSendEvent(eOnLoadNote, { + tabId: editorMessage.tabId, + newNote: true + }); + break; + } + case EditorEvents.tabsChanged: { // useTabStore.setState({ // tabs: (editorMessage.value as any)?.tabs, // currentTab: (editorMessage.value as any)?.currentTab @@ -577,14 +593,14 @@ export const useEditorEvents = ( // console.log("Tabs updated"); break; } - case EventTypes.toc: + case EditorEvents.toc: TableOfContents.present(editorMessage.value); break; - case EventTypes.showTabs: { + case EditorEvents.showTabs: { EditorTabs.present(); break; } - case EventTypes.error: { + case EditorEvents.error: { presentSheet({ component: ( { Navigation.queueRoutesForUpdate(); diff --git a/apps/mobile/app/screens/editor/tiptap/use-editor.ts b/apps/mobile/app/screens/editor/tiptap/use-editor.ts index 261fb2db82..546a74a33e 100644 --- a/apps/mobile/app/screens/editor/tiptap/use-editor.ts +++ b/apps/mobile/app/screens/editor/tiptap/use-editor.ts @@ -33,7 +33,10 @@ import { isTrashItem } from "@notesnook/core"; import { strings } from "@notesnook/intl"; +import { EditorEvents } from "@notesnook/editor-mobile/src/utils/editor-events"; +import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events"; import { useThemeEngineStore } from "@notesnook/theme"; +import { Mutex } from "async-mutex"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import WebView from "react-native-webview"; import { DatabaseLogger, db } from "../../../common/database"; @@ -62,12 +65,10 @@ import { sleep } from "../../../utils/time"; import { unlockVault } from "../../../utils/unlock-vault"; import { onNoteCreated } from "../../notes/common"; import Commands from "./commands"; -import { EventTypes } from "./editor-events"; import { SessionHistory } from "./session-history"; import { EditorState, SavePayload } from "./types"; -import { syncTabs, useTabStore } from "./use-tab-store"; +import { TabSessionItem, syncTabs, useTabStore } from "./use-tab-store"; import { - EditorEvents, clearAppState, defaultState, getAppState, @@ -76,6 +77,8 @@ import { post } from "./utils"; +const loadNoteMutex = new Mutex(); + type NoteWithContent = Note & { content?: NoteContent; }; @@ -98,7 +101,6 @@ export const useEditor = ( isPreview?: boolean; }; }) - | null | undefined > >({}); @@ -118,7 +120,6 @@ export const useEditor = ( const lastContentChangeTime = useRef>({}); const lock = useRef(false); const currentLoadingNoteId = useRef(); - const loadingState = useRef(); const lastTabFocused = useRef(0); const blockIdRef = useRef(); const postMessage = useCallback( @@ -141,7 +142,7 @@ export const useEditor = ( }, [commands, insets, isDefaultEditor]); useEffect(() => { - postMessage(EditorEvents.theme, theme); + postMessage(NativeEvents.theme, theme); }, [theme, postMessage]); useEffect(() => { @@ -152,14 +153,14 @@ export const useEditor = ( useEffect(() => { const event = eSubscribeEvent(eEditorTabFocused, (tabId) => { - console.log("Editot tab focus changed", lastTabFocused.current, tabId); + console.log("Editor tab focus changed", lastTabFocused.current, tabId); if (lastTabFocused.current !== tabId) lock.current = false; lastTabFocused.current = tabId as number; }); return () => { event?.unsubscribe(); }; - }); + }, []); const overlay = useCallback( (show: boolean, data = { type: "new" }) => { @@ -193,7 +194,7 @@ export const useEditor = ( const noteId = useTabStore.getState().getNoteIdForTab(tabId); if (noteId) { currentNotes.current?.id && db.fs().cancel(noteId); - currentNotes.current[noteId] = null; + currentNotes.current[noteId] = undefined; currentContents.current[noteId] = null; editorSessionHistory.clearSession(noteId); lastContentChangeTime.current[noteId] = 0; @@ -201,18 +202,11 @@ export const useEditor = ( } saveCount.current = 0; - loadingState.current = undefined; + currentLoadingNoteId.current = undefined; lock.current = false; - resetContent && postMessage(EditorEvents.title, "", tabId); - + resetContent && postMessage(NativeEvents.title, "", tabId); resetContent && (await commands.clearContent(tabId)); resetContent && (await commands.clearTags(tabId)); - useTabStore.getState().updateTab(tabId, { - noteId: undefined, - locked: false, - noteLocked: false, - readonly: false - }); }, [commands, editorSessionHistory, postMessage] ); @@ -232,6 +226,16 @@ export const useEditor = ( try { if (id && !(await db.notes?.note(id))) { await reset(tabId); + useTabStore.getState().updateTab(tabId, { + session: { + noteId: undefined, + noteLocked: undefined, + locked: undefined, + readonly: undefined, + scrollTop: undefined, + selection: undefined + } + }); return; } let note = id ? await db.notes?.note(id) : undefined; @@ -270,13 +274,6 @@ export const useEditor = ( }; } - // If note is edited, the tab becomes a persistent tab automatically. - if (useTabStore.getState().getTab(tabId)?.previewTab) { - useTabStore.getState().updateTab(tabId, { - previewTab: false - }); - } - let saved = false; setTimeout(() => { if (saved) return; @@ -311,7 +308,9 @@ export const useEditor = ( } useTabStore.getState().updateTab(tabId, { - noteId: id + session: { + noteId: id + } }); const defaultNotebook = db.settings.getDefaultNotebook(); @@ -326,7 +325,7 @@ export const useEditor = ( if (!noteData.title) { postMessage( - EditorEvents.title, + NativeEvents.title, currentNotes.current[id]?.title, tabId ); @@ -386,8 +385,8 @@ export const useEditor = ( id === useTabStore.getState().getCurrentNoteId() && pendingChanges ) { - postMessage(EditorEvents.title, title || note?.title, tabId); - postMessage(EditorEvents.html, data, tabId); + postMessage(NativeEvents.title, title || note?.title, tabId); + postMessage(NativeEvents.html, data, tabId); currentNotes.current[id] = note; } @@ -429,183 +428,158 @@ export const useEditor = ( ); const loadNote = useCallback( - async (event: { + (event: { item?: Note; - forced?: boolean; newNote?: boolean; tabId?: number; blockId?: string; - presistTab?: boolean; + session?: TabSessionItem; }) => { - if (!event) return; - console.log(event.item?.id, event?.item?.title, "loading note..."); - - if (event.blockId) { - blockIdRef.current = event.blockId; - } - state.current.currentlyEditing = true; - - if ( - !state.current.ready && - (await isEditorLoaded( - editorRef, - sessionIdRef.current, - useTabStore.getState().currentTab - )) - ) { - state.current.ready = true; - } - - if (event.newNote) { - useTabStore.getState().focusEmptyTab(); - const tabId = useTabStore.getState().currentTab; - currentNotes.current && (await reset(tabId)); - setTimeout(() => { - if (state.current?.ready && !state.current.movedAway) - commands.focus(tabId); - }); - } else { - if (!event.item) { - overlay(false); - return; + loadNoteMutex.runExclusive(async () => { + if (!event) return; + if (event.blockId) { + blockIdRef.current = event.blockId; } - console.log("LOADING NOTE", event.item.id); - const item = event.item; - - const currentTab = useTabStore - .getState() - .getTab(useTabStore.getState().currentTab); - if (currentTab?.previewTab && item.id !== currentTab.noteId) { - await commands.setLoading(true, useTabStore.getState().currentTab); + state.current.currentlyEditing = true; + + if ( + !state.current.ready && + (await isEditorLoaded( + editorRef, + sessionIdRef.current, + useTabStore.getState().currentTab + )) + ) { + state.current.ready = true; } - const isLockedNote = await db.vaults.itemExists( - event.item as ItemReference - ); - const tabLocked = - isLockedNote && !(event.item as NoteWithContent).content; - - // If note was already opened in a tab, focus that tab. - if (typeof event.tabId !== "number") { - if (useTabStore.getState().hasTabForNote(event.item.id)) { - const tabId = useTabStore.getState().getTabForNote(event.item.id); - if (typeof tabId === "number") { - useTabStore.getState().updateTab(tabId, { - readonly: event.item.readonly || readonly, - locked: tabLocked, - noteLocked: isLockedNote - }); - useTabStore.getState().focusTab(tabId); - setTimeout(() => { - if (blockIdRef.current) { - commands.scrollIntoViewById(blockIdRef.current); - blockIdRef.current = undefined; - } - }, 150); - } - console.log("Note already loaded, focusing the tab"); + if (event.newNote && !currentLoadingNoteId.current) { + let tabId; + if (useTabStore.getState().tabs.length === 0) { + tabId = useTabStore.getState().newTab(); } else { - if (event.presistTab) { - // Open note in new tab. - useTabStore.getState().newTab({ - readonly: event.item.readonly || readonly, - locked: tabLocked, - noteLocked: isLockedNote, - noteId: event.item.id, - previewTab: false - }); - console.log("Opening note in new tab"); - } else { - console.log("Opening note in preview tab"); - // Otherwise we focus the preview tab or create one to open the note in. - useTabStore.getState().focusPreviewTab(event.item.id, { - readonly: event.item.readonly || readonly, - locked: tabLocked, - noteLocked: isLockedNote - }); + tabId = useTabStore.getState().currentTab; + await reset(tabId, true, true); + if ( + event.session?.noteId || + useTabStore.getState().getTab(tabId)?.session?.noteId + ) { + useTabStore.getState().newTabSession(tabId, {}); } } + + setTimeout(() => { + if (state.current?.ready && !state.current.movedAway) + commands.focus(tabId); + }); } else { - if (lastTabFocused.current !== event.tabId) { - useTabStore.getState().focusTab(event.tabId); + if (!event.item) { + overlay(false); + return; } - } - - const tabId = event.tabId || useTabStore.getState().currentTab; - if (lastTabFocused.current !== tabId) { - // if ((await waitForEvent(eEditorTabFocused, 1000)) !== tabId) { - // console.log("tab id did not match after focus in 1000ms"); - // return; - // } + const item = event.item; currentLoadingNoteId.current = item.id; - console.log("Waiting for tab to focus"); - return; - } - state.current.movedAway = false; - state.current.currentlyEditing = true; + const isLockedNote = await db.vaults.itemExists( + event.item as ItemReference + ); + const tabLocked = + isLockedNote && !(event.item as NoteWithContent).content; - if (!tabLocked) { - await loadContent(item); - } + let tabId = event.tabId; + if (tabId === undefined) tabId = useTabStore.getState().currentTab; - if ( - currentNotes.current[item.id] && - loadingState.current && - currentContents.current[item.id]?.data && - loadingState.current === currentContents.current[item.id]?.data - ) { - // If note is already loading, return. - console.log("Note is already loading..."); - return; - } + await commands.setLoading(true, tabId); - if (!state.current.ready) { - currentNotes.current[item.id] = item; - return; - } + const session: Partial = event.session || { + readonly: event.item.readonly, + locked: tabLocked, + noteLocked: isLockedNote, + noteId: event.item.id + }; - lastContentChangeTime.current[item.id] = 0; - currentLoadingNoteId.current = item.id; - currentNotes.current[item.id] = item; + const tab = useTabStore.getState().getTab(tabId); - if (!currentNotes.current[item.id]) return; + if (useTabStore.getState().tabs.length === 0) { + useTabStore.getState().newTab({ + session: session + }); + console.log("Creating a new tab..."); + } else { + if ( + event.item.id !== tab?.session?.noteId && + tab?.session?.noteId + ) { + useTabStore.getState().newTabSession(tabId, session); + console.log("Creating a new tab session"); + } else { + console.log("Updating tab session"); + useTabStore.getState().updateTab(tabId, { + session: session + }); + } + } - editorSessionHistory.newSession(item.id); + if (lastTabFocused.current !== tabId) { + console.log("Waiting for tab to get focus"); + return; + } - await commands.setStatus( - getFormattedDate(item.dateEdited, "date-time"), - strings.saved(), - tabId - ); + if (tabBarRef.current?.page() === 2) { + state.current.movedAway = false; + } - await postMessage(EditorEvents.title, item.title, tabId); - overlay(false); - loadingState.current = currentContents.current[item.id]?.data; + state.current.currentlyEditing = true; + if (!tabLocked) { + await loadContent(item); + } else { + commands.focus(tabId); + } - await postMessage( - EditorEvents.html, - currentContents.current[item.id]?.data || "", - tabId, - 10000 - ); + lastContentChangeTime.current[item.id] = item.dateEdited; + currentNotes.current[item.id] = item; - setTimeout(() => { - if (blockIdRef.current) { - commands.scrollIntoViewById(blockIdRef.current); - blockIdRef.current = undefined; - } - }, 300); + if (!currentNotes.current[item.id]) return; - loadingState.current = undefined; - await commands.setTags(item); - commands.setSettings(); - setTimeout(() => { - if (currentLoadingNoteId.current === event.item?.id) { - currentLoadingNoteId.current = undefined; - } - }, 300); - } - postMessage(EditorEvents.theme, theme); + editorSessionHistory.newSession(item.id); + + await commands.setStatus( + getFormattedDate(item.dateEdited, "date-time"), + "Saved", + tabId + ); + await postMessage(NativeEvents.title, item.title, tabId); + overlay(false); + + console.log("LOADING NOTE....", item.id, item.title); + + await postMessage( + NativeEvents.html, + { + data: currentContents.current[item.id]?.data || "", + scrollTop: tab?.session?.scrollTop, + selection: tab?.session?.selection + }, + tabId, + 10000 + ); + + setTimeout(() => { + if (blockIdRef.current) { + commands.scrollIntoViewById(blockIdRef.current); + blockIdRef.current = undefined; + } + }, 300); + + await commands.setTags(item); + commands.setSettings(); + setTimeout(() => { + if (currentLoadingNoteId.current === event.item?.id) { + currentLoadingNoteId.current = undefined; + } + }, 300); + } + postMessage(NativeEvents.theme, theme); + }); }, [ commands, @@ -660,9 +634,9 @@ export const useEditor = ( : false; if (note) { - if (!locked && tab?.noteLocked) { + if (!locked && tab?.session?.noteLocked) { // Note lock removed. - if (tab.locked) { + if (tab.session?.locked) { if (useTabStore.getState().currentTab === tabId) { eSendEvent(eOnLoadNote, { item: note, @@ -670,17 +644,21 @@ export const useEditor = ( }); } else { useTabStore.getState().updateTab(tabId, { - locked: false, - noteLocked: false + session: { + locked: false, + noteLocked: false + } }); commands.setLoading(true, tabId); } } - } else if (!tab?.noteLocked && locked) { + } else if (!tab?.session?.noteLocked && locked) { // Note lock added. useTabStore.getState().updateTab(tabId, { - locked: true, - noteLocked: true + session: { + locked: true, + noteLocked: true + } }); if (useTabStore.getState().currentTab !== tabId) { commands.clearContent(tabId); @@ -689,7 +667,7 @@ export const useEditor = ( } if (currentNotes.current[noteId]?.title !== note.title) { - postMessage(EditorEvents.title, note.title, tabId); + postMessage(NativeEvents.title, note.title, tabId); } commands.setTags(note); if (currentNotes.current[noteId]?.dateEdited !== note.dateEdited) { @@ -701,7 +679,9 @@ export const useEditor = ( } useTabStore.getState().updateTab(tabId, { - readonly: note.readonly + session: { + readonly: note.readonly + } }); } @@ -714,8 +694,10 @@ export const useEditor = ( const decryptedContent = await db.vault?.decryptContent(data); if (!decryptedContent) { useTabStore.getState().updateTab(tabId, { - locked: true, - noteLocked: true + session: { + locked: true, + noteLocked: true + } }); if (useTabStore.getState().currentTab !== tabId) { commands.clearContent(tabId); @@ -723,7 +705,7 @@ export const useEditor = ( } } else { await postMessage( - EditorEvents.updatehtml, + NativeEvents.updatehtml, decryptedContent.data, tabId ); @@ -735,7 +717,7 @@ export const useEditor = ( return; } lastContentChangeTime.current[note.id] = note.dateEdited; - await postMessage(EditorEvents.updatehtml, _nextContent, tabId); + await postMessage(NativeEvents.updatehtml, _nextContent, tabId); if (!isEncryptedContent(data)) { currentContents.current[note.id] = data as UnencryptedContentItem; @@ -809,7 +791,7 @@ export const useEditor = ( lastContentChangeTime.current[noteId] = Date.now(); } - if (type === EventTypes.content && noteId) { + if (type === EditorEvents.content && noteId) { currentContents.current[noteId as string] = { data: content, type: "tiptap", @@ -859,7 +841,9 @@ export const useEditor = ( if (!appState) return; state.current.isRestoringState = true; state.current.currentlyEditing = true; - state.current.movedAway = false; + if (tabBarRef.current?.page() === 2) { + state.current.movedAway = false; + } if (!state.current.editorStateRestored) { state.current.isRestoringState = true; @@ -904,7 +888,7 @@ export const useEditor = ( const onLoad = useCallback(async () => { setTimeout(() => { - postMessage(EditorEvents.theme, theme); + postMessage(NativeEvents.theme, theme); }); commands.setInsets( isDefaultEditor ? insets : { top: 0, left: 0, right: 0, bottom: 0 } @@ -916,6 +900,7 @@ export const useEditor = ( } const noteId = useTabStore.getState().getCurrentNoteId(); + if (!noteId) { loadNote({ newNote: true }); if (tabBarRef.current?.page() === 1) { diff --git a/apps/mobile/app/screens/editor/tiptap/use-tab-store.ts b/apps/mobile/app/screens/editor/tiptap/use-tab-store.ts index 6c3509ec65..d6d58fd2a6 100644 --- a/apps/mobile/app/screens/editor/tiptap/use-tab-store.ts +++ b/apps/mobile/app/screens/editor/tiptap/use-tab-store.ts @@ -16,12 +16,17 @@ 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 . */ +import { TabSessionHistory } from "@notesnook/common"; +import { MMKVLoader } from "react-native-mmkv-storage"; import create from "zustand"; import { persist, StateStorage } from "zustand/middleware"; +import { db } from "../../../common/database"; import { MMKV } from "../../../common/database/mmkv"; +import { eSendEvent } from "../../../services/event-manager"; +import { eOnLoadNote } from "../../../utils/events"; import { editorController } from "./utils"; -class History { +class TabHistory { history: number[]; constructor() { this.history = [0]; @@ -36,7 +41,7 @@ class History { this.history.unshift(item); // Add item to the beginning of the array useTabStore.setState({ - tabHistory: this.history.slice() + history: this.history.slice() }); return true; // Item added successfully } @@ -48,7 +53,7 @@ class History { return removedItem; } useTabStore.setState({ - tabHistory: this.history.slice() + history: this.history.slice() }); return null; // Invalid index } @@ -59,7 +64,7 @@ class History { return restoredItem; } useTabStore.setState({ - tabHistory: this.history.slice() + history: this.history.slice() }); return null; // History is empty } @@ -69,17 +74,99 @@ class History { } } -export type TabItem = { - id: number; +export type TabSessionItem = { + id: string; noteId?: string; - previewTab?: boolean; - readonly?: boolean; - locked?: boolean; + scrollTop?: number; + selection?: { to: number; from: number }; noteLocked?: boolean; + locked?: boolean; + readonly?: boolean; +}; + +const TabSessionStorageKV = new MMKVLoader() + .withInstanceID("tab-session-storage") + .disableIndexing() + .initialize(); + +class TabSessionStorage { + static storage: typeof TabSessionStorageKV = TabSessionStorageKV; + + static get(id: string): TabSessionItem | null { + return TabSessionStorage.storage.getMap(id); + } + + static set(id: string, session: TabSessionItem): void { + TabSessionStorage.storage.setMap(id, session); + } + + static update(id: string, session: Partial) { + const currentSession = TabSessionStorage.get(id); + const newSession = { + ...currentSession, + ...session + }; + TabSessionStorage.set(id, newSession as TabSessionItem); + return newSession; + } + + static remove(id: string) { + TabSessionStorageKV.removeItem(id); + } +} + +function getId(id: number, tabs: TabItem[]): number { + const exists = tabs.find((t) => t.id === id); + if (exists) { + return getId(id + 1, tabs); + } + return id; +} + +export function syncTabs( + type: "tabs" | "history" | "biometry" | "all" = "all" +) { + const data: Partial = {}; + + if (type === "tabs" || type === "all") { + data.tabs = useTabStore.getState().tabs; + data.currentTab = useTabStore.getState().currentTab; + } + if (type === "history" || type === "all") { + data.canGoBack = useTabStore.getState().canGoBack; + data.canGoForward = useTabStore.getState().canGoForward; + data.sessionId = useTabStore.getState().sessionId; + } + + if (type === "biometry" || type === "all") { + data.biometryAvailable = useTabStore.getState().biometryAvailable; + data.biometryEnrolled = useTabStore.getState().biometryEnrolled; + } + + editorController.current?.commands.doAsync(` + globalThis.tabStore?.setState(${JSON.stringify(data)}); +`); +} +export const tabSessionHistory = new TabSessionHistory({ + get() { + return useTabStore.getState(); + }, + set(state) { + useTabStore.setState({ + ...state + }); + }, + getCurrentTab: () => useTabStore.getState().currentTab +}); + +export type TabItem = { + id: number; pinned?: boolean; + needsRefresh?: boolean; + session?: Partial; }; -const history = new History(); +const history = new TabHistory(); export type TabStore = { tabs: TabItem[]; @@ -91,125 +178,207 @@ export type TabStore = { ) => void; removeTab: (index: number) => void; moveTab: (index: number, toIndex: number) => void; - newTab: (options?: Omit, "id">) => void; + newTab: (options?: Omit, "id">) => number; focusTab: (id: number) => void; getNoteIdForTab: (id: number) => string | undefined; getTabForNote: (noteId: string) => number | undefined; + getTabsForNote: (noteId: string) => TabItem[]; + forEachNoteTab: (noteId: string, cb: (tab: TabItem) => void) => void; hasTabForNote: (noteId: string) => boolean; focusEmptyTab: () => void; getCurrentNoteId: () => string | undefined; getTab: (tabId: number) => TabItem | undefined; - tabHistory: number[]; + newTabSession: ( + id: number, + options: Omit, "id"> + ) => void; + history: number[]; biometryAvailable?: boolean; biometryEnrolled?: boolean; + tabSessionHistory: Record< + number, + { back_stack: string[]; forward_stack: string[] } + >; + goBack(): void; + goForward(): void; + loadSession: (id: string) => Promise; + canGoBack?: boolean; + canGoForward?: boolean; + sessionId?: string; }; -function getId(id: number, tabs: TabItem[]): number { - const exists = tabs.find((t) => t.id === id); - if (exists) { - return getId(id + 1, tabs); - } - return id; -} - -export function syncTabs() { - editorController.current?.commands.doAsync(` - globalThis.tabStore?.setState({ - tabs: ${JSON.stringify(useTabStore.getState().tabs)}, - currentTab: ${useTabStore.getState().currentTab}, - biometryAvailable: ${useTabStore.getState().biometryAvailable}, - biometryEnrolled: ${useTabStore.getState().biometryEnrolled} - }); -`); -} - export const useTabStore = create( persist( (set, get) => ({ - tabs: [ - { - id: 0 - } - ], - tabHistory: [0], - history: new History(), + tabs: [], + tabSessionHistory: {}, + history: [0], currentTab: 0, - updateTab: (id: number, options: Omit, "id">) => { - if (!options) return; + newTabSession: ( + id: number, + options: Omit, "id"> + ) => { + const sessionId = tabSessionHistory.add(); + const session = { + id: sessionId, + ...options + }; + TabSessionStorage.set(sessionId, session); const index = get().tabs.findIndex((t) => t.id === id); if (index == -1) return; const tabs = [...get().tabs]; tabs[index] = { ...tabs[index], - ...options - }; + ...options, + session: session + } as TabItem; set({ tabs: tabs }); syncTabs(); }, - focusPreviewTab: ( - noteId: string, - options: Omit, "id" | "noteId"> - ) => { - const index = get().tabs.findIndex((t) => t.previewTab); - if (index === -1) - return get().newTab({ - noteId, - previewTab: true, - ...options - }); + updateTab: (id: number, options: Omit, "id">) => { + if (!options) return; + const index = get().tabs.findIndex((t) => t.id === id); + if (index == -1) return; const tabs = [...get().tabs]; + + const sessionId = + options.session?.id || (tabs[index].session?.id as string); + const updatedSession = !options.session + ? tabs[index].session + : TabSessionStorage.update(sessionId, options.session); + tabs[index] = { ...tabs[index], ...options, - previewTab: true, - noteId: noteId - }; - console.log("focus preview", noteId); + session: updatedSession + } as TabItem; + set({ tabs: tabs }); - get().focusTab(tabs[index].id); + syncTabs(); }, + goBack: async () => { + if (!tabSessionHistory.canGoBack()) return; + const id = tabSessionHistory.back() as string; + const sessionLoaded = await get().loadSession(id); + if (!sessionLoaded) { + tabSessionHistory.remove(id); + TabSessionStorage.remove(id); + if (!tabSessionHistory.canGoBack()) { + tabSessionHistory.forward(); + syncTabs(); + } else { + return get().goBack(); + } + } else { + syncTabs(); + } + }, + goForward: async () => { + if (!tabSessionHistory.canGoForward()) return; + const id = tabSessionHistory.forward() as string; + if (!(await get().loadSession(id))) { + tabSessionHistory.remove(id); + TabSessionStorage.remove(id); + if (!tabSessionHistory.canGoForward()) { + tabSessionHistory.back(); + syncTabs(); + } else { + return get().goForward(); + } + } else { + syncTabs(); + } + }, + loadSession: async (id: string) => { + const session = TabSessionStorage.get(id); + if (!session) return false; + + const note = session?.noteId + ? await db.notes.note(session?.noteId) + : undefined; + + if (note) { + const isLocked = await db.vaults.itemExists(note); + if (isLocked && !session?.noteLocked) { + session.locked = true; + session.noteLocked = true; + } + session.readonly = note.readonly; + } else if (session.noteId) { + console.log("Failed to load session..."); + return false; + } + + get().updateTab(get().currentTab, { + session: session + }); + console.log("Loading session", session); + eSendEvent(eOnLoadNote, { + item: note, + newNote: !note, + tabId: get().currentTab, + session: session + }); + + return true; + }, + focusPreviewTab: ( + noteId: string, + options: Omit, "id" | "noteId"> + ) => {}, + removeTab: (id: number) => { const index = get().tabs.findIndex((t) => t.id === id); - if (index > -1) { const isFocused = id === get().currentTab; const nextTabs = get().tabs.slice(); nextTabs.splice(index, 1); history.remove(id); + + const tabSessions = tabSessionHistory.getHistory(); + tabSessions.back.forEach((id) => TabSessionStorage.remove(id)); + tabSessions.forward.forEach((id) => TabSessionStorage.remove(id)); + tabSessionHistory.clearStackForTab(id); + if (nextTabs.length === 0) { - nextTabs.push({ - id: 0 + set({ + tabs: [{ id: 0 }] }); + get().newTabSession(0, {}); + get().focusTab(0); + } else { + set({ + tabs: nextTabs + }); + if (isFocused) { + get().focusTab(history.restoreLast() || 0); + } } - set({ - tabs: nextTabs - }); - get().focusTab( - isFocused ? history.restoreLast() || 0 : get().currentTab - ); + syncTabs(); } }, newTab: (options) => { const id = getId(get().tabs.length, get().tabs); - const nextTabs = [ - ...get().tabs, - { - id: id, - ...options - } - ]; set({ - tabs: nextTabs + tabs: [ + ...get().tabs, + { + id: id, + ...options + } + ] }); + get().newTabSession(id, options?.session || {}); get().focusTab(id); + return id; }, focusEmptyTab: () => { - const index = get().tabs.findIndex((t) => !t.noteId); + const index = get().tabs.findIndex((t) => !t.session?.noteId); if (index === -1) return get().newTab(); console.log("focus empty tab", get().tabs[index]); @@ -225,26 +394,39 @@ export const useTabStore = create( }, focusTab: (id: number) => { - console.log(history.getHistory(), id); history.add(id); set({ currentTab: id }); + set({ + canGoBack: tabSessionHistory.canGoBack(), + canGoForward: tabSessionHistory.canGoForward(), + sessionId: tabSessionHistory.currentSessionId() + }); syncTabs(); }, getNoteIdForTab: (id: number) => { - return get().tabs.find((t) => t.id === id)?.noteId; + return get().tabs.find((t) => t.id === id)?.session?.noteId; }, hasTabForNote: (noteId: string) => { return ( - typeof get().tabs.find((t) => t.noteId === noteId)?.id === "number" + typeof get().tabs.find((t) => t.session?.noteId === noteId)?.id === + "number" ); }, getTabForNote: (noteId: string) => { - return get().tabs.find((t) => t.noteId === noteId)?.id; + return get().tabs.find((t) => t.session?.noteId === noteId)?.id; + }, + getTabsForNote(noteId: string) { + return get().tabs.filter((t) => t.session?.noteId === noteId); + }, + forEachNoteTab: (noteId: string, cb: (tab: TabItem) => void) => { + const tabs = get().tabs.filter((t) => t.session?.noteId === noteId); + tabs.forEach(cb); }, getCurrentNoteId: () => { - return get().tabs.find((t) => t.id === get().currentTab)?.noteId; + return get().tabs.find((t) => t.id === get().currentTab)?.session + ?.noteId; }, getTab: (tabId) => { return get().tabs.find((t) => t.id === tabId); @@ -255,7 +437,7 @@ export const useTabStore = create( getStorage: () => MMKV as unknown as StateStorage, onRehydrateStorage: () => { return (state) => { - history.history = state?.tabHistory.slice() || []; + history.history = state?.history || []; }; } } diff --git a/apps/mobile/app/screens/editor/tiptap/utils.ts b/apps/mobile/app/screens/editor/tiptap/utils.ts index 44c70cc772..570a1559dc 100644 --- a/apps/mobile/app/screens/editor/tiptap/utils.ts +++ b/apps/mobile/app/screens/editor/tiptap/utils.ts @@ -31,6 +31,8 @@ import { eOnLoadNote } from "../../../utils/events"; import { NotesnookModule } from "../../../utils/notesnook-module"; import { AppState, EditorState, useEditorType } from "./types"; import { useTabStore } from "./use-tab-store"; +import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events"; + export const textInput = createRef(); export const editorController = createRef() as MutableRefObject; @@ -46,19 +48,6 @@ export function editorState() { return editorController.current?.state.current || defaultState; } -export const EditorEvents = { - html: "native:html", - updatehtml: "native:updatehtml", - title: "native:title", - theme: "native:theme", - titleplaceholder: "native:titleplaceholder", - logger: "native:logger", - status: "native:status", - keyboardShown: "native:keyboardShown", - attachmentData: "native:attachment-data", - resolve: "native:resolve" -}; - export function randId(prefix: string) { return Math.random() .toString(36) @@ -74,7 +63,7 @@ export async function isEditorLoaded( sessionId: string, tabId: number ) { - return await post(ref, sessionId, tabId, EditorEvents.status); + return await post(ref, sessionId, tabId, NativeEvents.status); } export async function post( diff --git a/apps/mobile/app/services/notifications.ts b/apps/mobile/app/services/notifications.ts index ac98c63352..30fcb32032 100644 --- a/apps/mobile/app/services/notifications.ts +++ b/apps/mobile/app/services/notifications.ts @@ -43,6 +43,7 @@ import { useRelationStore } from "../stores/use-relation-store"; import { useReminderStore } from "../stores/use-reminder-store"; import { useSettingStore } from "../stores/use-setting-store"; import { useUserStore } from "../stores/use-user-store"; +import { eOnLoadNote } from "../utils/events"; import { tabBarRef } from "../utils/global-refs"; import { convertNoteToText } from "../utils/note-to-text"; import { NotesnookModule } from "../utils/notesnook-module"; @@ -418,19 +419,10 @@ async function loadNote(id: string, jump: boolean) { }) ); - const isLocked = await db.vaults.itemExists({ - type: "note", - id: id - }); - const tab = useTabStore.getState().getTabForNote(id); - if (tab !== undefined) { - useTabStore.getState().focusTab(tab); - } else { - useTabStore.getState().focusPreviewTab(id, { - noteId: id, - readonly: note.readonly, - noteLocked: isLocked + if (useTabStore.getState().currentTab !== tab) { + eSendEvent(eOnLoadNote, { + note: note }); } } diff --git a/apps/mobile/package-lock.json b/apps/mobile/package-lock.json index b3b5c37762..d9f931ec0e 100644 --- a/apps/mobile/package-lock.json +++ b/apps/mobile/package-lock.json @@ -1,12 +1,12 @@ { "name": "@notesnook/mobile", - "version": "3.0.22", + "version": "3.0.23", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@notesnook/mobile", - "version": "3.0.22", + "version": "3.0.23", "hasInstallScript": true, "license": "GPL-3.0-or-later", "workspaces": [ @@ -3815,6 +3815,7 @@ "@lingui/react": "4.11.4", "@mdi/js": "^7.2.96", "@mdi/react": "^1.6.0", + "@notesnook/common": "file:../common", "@notesnook/editor": "file:../editor", "@notesnook/intl": "file:../intl", "@notesnook/theme": "file:../theme", @@ -3825,6 +3826,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-freeze": "^1.0.3", + "tinycolor2": "1.6.0", "zustand": "^4.4.7" }, "devDependencies": { @@ -28900,6 +28902,7 @@ "@trpc/react-query": "^10.45.2", "@trpc/server": "^10.45.2", "absolutify": "^0.1.0", + "async-mutex": "0.5.0", "buffer": "^6.0.3", "dayjs": "^1.10.4", "deprecated-react-native-prop-types": "^4.1.0", @@ -35264,6 +35267,14 @@ "version": "1.0.1", "license": "MIT" }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/at-least-node": { "version": "1.0.0", "dev": true, @@ -43964,6 +43975,7 @@ "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.2.0.tgz", "integrity": "sha512-JWD+aQ0lh2gvh4NM3bBM42Kx+XybOxCpgYK7F8ugAlpaTSnWsX+39Z4XkOykGZAHrjwwTZT3x3KxswVWxHPUqA==", "dev": true, + "license": "MIT", "dependencies": { "react-is": "^18.2.0", "react-shallow-renderer": "^16.15.0", diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 549bae5526..4edcda2eee 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -47,4 +47,4 @@ "react": "18.2.0", "react-native": "0.74.5" } -} \ No newline at end of file +} diff --git a/apps/monograph/package-lock.json b/apps/monograph/package-lock.json index b2b9211782..17a0257a37 100644 --- a/apps/monograph/package-lock.json +++ b/apps/monograph/package-lock.json @@ -2928,22 +2928,6 @@ "@styled-system/css": "^5.1.5" } }, - "node_modules/@theme-ui/color-modes": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@theme-ui/color-modes/-/color-modes-0.16.2.tgz", - "integrity": "sha512-jWEWx53lxNgWCT38i/kwLV2rsvJz8lVZgi5oImnVwYba9VejXD23q1ckbNFJHosQ8KKXY87ht0KPC6BQFIiHtQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@theme-ui/core": "^0.16.2", - "@theme-ui/css": "^0.16.2", - "deepmerge": "^4.2.2" - }, - "peerDependencies": { - "@emotion/react": "^11.11.1", - "react": ">=18" - } - }, "node_modules/@theme-ui/components": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@theme-ui/components/-/components-0.16.2.tgz", @@ -2989,22 +2973,6 @@ "@emotion/react": "^11.11.1" } }, - "node_modules/@theme-ui/theme-provider": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@theme-ui/theme-provider/-/theme-provider-0.16.2.tgz", - "integrity": "sha512-LRnVevODcGqO0JyLJ3wht+PV3ZoZcJ7XXLJAJWDoGeII4vZcPQKwVy4Lpz/juHsZppQxKcB3U+sQDGBnP25irQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@theme-ui/color-modes": "^0.16.2", - "@theme-ui/core": "^0.16.2", - "@theme-ui/css": "^0.16.2" - }, - "peerDependencies": { - "@emotion/react": "^11.11.1", - "react": ">=18" - } - }, "node_modules/@types/acorn": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz", @@ -3134,14 +3102,14 @@ "version": "15.7.13", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.10", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.10.tgz", "integrity": "sha512-02sAAlBnP39JgXwkAq3PeU9DVaaGpZyF3MGcC0MKgQVkZor5IiiDAipVaxQHtDJAmO4GIy/rVBy/LzVj76Cyqg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -12229,7 +12197,7 @@ "version": "3.23.8", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", - "devOptional": true, + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/apps/theme-builder/package-lock.json b/apps/theme-builder/package-lock.json index f34d169c2c..80825ac9e8 100644 --- a/apps/theme-builder/package-lock.json +++ b/apps/theme-builder/package-lock.json @@ -973,7 +973,7 @@ }, "../web": { "name": "@notesnook/web", - "version": "3.0.20", + "version": "3.0.22", "hasInstallScript": true, "license": "GPL-3.0-or-later", "dependencies": { diff --git a/apps/web/package-lock.json b/apps/web/package-lock.json index 42e2de9612..43ce547823 100644 --- a/apps/web/package-lock.json +++ b/apps/web/package-lock.json @@ -25191,6 +25191,7 @@ "@readme/data-urls": "^3.0.0", "@streetwriters/kysely": "^0.27.4", "@streetwriters/showdown": "^3.0.9-alpha", + "@types/mime-db": "^1.43.5", "async-mutex": "^0.3.2", "dayjs": "1.11.9", "dom-serializer": "^2.0.0", @@ -25203,7 +25204,7 @@ "katex": "0.16.2", "linkedom": "^0.14.17", "liqe": "^1.13.0", - "mime": "^4.0.4", + "mime-db": "^1.53.0", "prismjs": "^1.29.0", "qclone": "^1.2.0", "rfdc": "^1.3.0", @@ -35615,7 +35616,7 @@ }, "../desktop": { "name": "@notesnook/desktop", - "version": "3.0.20", + "version": "3.0.22", "hasInstallScript": true, "license": "GPL-3.0-or-later", "dependencies": { diff --git a/packages/common/package-lock.json b/packages/common/package-lock.json index 6990b86b2f..0722d5b251 100644 --- a/packages/common/package-lock.json +++ b/packages/common/package-lock.json @@ -39,6 +39,7 @@ "@readme/data-urls": "^3.0.0", "@streetwriters/kysely": "^0.27.4", "@streetwriters/showdown": "^3.0.9-alpha", + "@types/mime-db": "^1.43.5", "async-mutex": "^0.3.2", "dayjs": "1.11.9", "dom-serializer": "^2.0.0", @@ -51,7 +52,7 @@ "katex": "0.16.2", "linkedom": "^0.14.17", "liqe": "^1.13.0", - "mime": "^4.0.4", + "mime-db": "^1.53.0", "prismjs": "^1.29.0", "qclone": "^1.2.0", "rfdc": "^1.3.0", @@ -1849,6 +1850,7 @@ "@types/event-source-polyfill": "^1.0.1", "@types/html-to-text": "^9.0.0", "@types/katex": "^0.16.2", + "@types/mime-db": "^1.43.5", "@types/prismjs": "^1.26.0", "@types/spark-md5": "^3.0.2", "@types/streetwriters__showdown": "npm:@types/showdown@^2.0.6", @@ -1875,7 +1877,7 @@ "katex": "0.16.2", "linkedom": "^0.14.17", "liqe": "^1.13.0", - "mime": "^4.0.4", + "mime-db": "^1.53.0", "mockdate": "^3.0.5", "nanoid": "^5.0.1", "otplib": "^12.0.1", diff --git a/packages/common/src/utils/index.ts b/packages/common/src/utils/index.ts index 3ab8846e8b..af0513fd3f 100644 --- a/packages/common/src/utils/index.ts +++ b/packages/common/src/utils/index.ts @@ -27,3 +27,4 @@ export * from "./resolve-items.js"; export * from "./migrate-toolbar.js"; export * from "./export-notes.js"; export * from "./dataurl.js"; +export * from "./tab-session-history.js"; diff --git a/packages/common/src/utils/tab-session-history.ts b/packages/common/src/utils/tab-session-history.ts new file mode 100644 index 0000000000..84e104ae20 --- /dev/null +++ b/packages/common/src/utils/tab-session-history.ts @@ -0,0 +1,179 @@ +/* +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 . +*/ + +import { getId } from "@notesnook/core"; + +export type TabState = { + tabSessionHistory: Record< + number, + { back_stack: string[]; forward_stack: string[] } + >; + canGoBack?: boolean; + canGoForward?: boolean; + sessionId?: string; +}; + +export class TabSessionHistory { + constructor( + public options: { + set: (state: TabState) => void; + get: () => TabState; + getCurrentTab: () => number; + } + ) {} + + get back_stack() { + return ( + this.options + .get() + .tabSessionHistory[this.options.getCurrentTab()]?.back_stack.slice() || + [] + ); + } + + set back_stack(value: string[]) { + const currentTab = this.options.getCurrentTab(); + const tabHistory = this.options.get().tabSessionHistory; + console.log("back_stack", value); + this.options.set({ + canGoBack: value.length > 1, + sessionId: this.currentSessionId(), + tabSessionHistory: { + ...tabHistory, + [currentTab]: { + ...(tabHistory[currentTab] || {}), + back_stack: value + } + } + }); + } + + get forward_stack() { + return ( + this.options + .get() + .tabSessionHistory[ + this.options.getCurrentTab() + ]?.forward_stack.slice() || [] + ); + } + + set forward_stack(value: string[]) { + const currentTab = this.options.getCurrentTab(); + const tabHistory = this.options.get().tabSessionHistory; + console.log("forward_stack", value); + this.options.set({ + canGoForward: value.length > 0, + sessionId: this.currentSessionId(), + tabSessionHistory: { + ...tabHistory, + [currentTab]: { + ...(tabHistory[currentTab] || {}), + forward_stack: value + } + } + }); + } + + add() { + const sessionId = getId(); + const back_stack = this.back_stack; + back_stack.push(sessionId); + this.back_stack = back_stack; + this.forward_stack = []; + return sessionId; + } + + clearStackForTab(tabId: number) { + this.options.set({ + tabSessionHistory: { + ...this.options.get().tabSessionHistory, + [tabId]: { + back_stack: [], + forward_stack: [] + } + } + }); + } + + back(): string | null { + if (!this.canGoBack()) return null; + + const back_stack = this.back_stack; + const forward_stack = this.forward_stack; + + const current_item = back_stack.pop(); + const next_item = back_stack[back_stack.length - 1]; + + current_item && forward_stack.push(current_item); + + this.forward_stack = forward_stack; + this.back_stack = back_stack; + + console.log("back", this.forward_stack, this.back_stack); + + return next_item; + } + + remove(id: string) { + const back_stack = this.back_stack; + let index = back_stack.findIndex((item) => item === id); + if (index === -1) { + const forward_stack = this.forward_stack; + index = forward_stack.findIndex((item) => item === id); + forward_stack.splice(index, 1); + this.forward_stack = forward_stack; + } else { + back_stack.splice(index, 1); + this.back_stack = back_stack; + } + } + + forward(): string | null { + if (!this.canGoForward()) return null; + + const back_stack = this.back_stack; + const forward_stack = this.forward_stack; + + const item = forward_stack.pop() as string; + this.forward_stack = forward_stack; + back_stack.push(item); + this.back_stack = back_stack; + return item; + } + + currentSessionId() { + return this.back_stack[this.back_stack.length - 1]; + } + + getHistory() { + return { + back: this.back_stack, + forward: this.forward_stack + }; + } + + canGoBack() { + return this.back_stack.length > 1; + } + + canGoForward() { + return this.forward_stack.length > 0; + } +} diff --git a/packages/editor-mobile/package-lock.json b/packages/editor-mobile/package-lock.json index 8d84ae5d36..51d8b3b17b 100644 --- a/packages/editor-mobile/package-lock.json +++ b/packages/editor-mobile/package-lock.json @@ -14,6 +14,7 @@ "@lingui/react": "4.11.4", "@mdi/js": "^7.2.96", "@mdi/react": "^1.6.0", + "@notesnook/common": "file:../common", "@notesnook/editor": "file:../editor", "@notesnook/intl": "file:../intl", "@notesnook/theme": "file:../theme", @@ -24,6 +25,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-freeze": "^1.0.3", + "tinycolor2": "1.6.0", "zustand": "^4.4.7" }, "devDependencies": { @@ -33,6 +35,28 @@ "react-scripts": "^5.0.1" } }, + "../common": { + "name": "@notesnook/common", + "version": "2.1.3", + "license": "GPL-3.0-or-later", + "dependencies": { + "@notesnook/core": "file:../core", + "@readme/data-urls": "^3.0.0", + "dayjs": "^1.11.13", + "pathe": "^1.1.2", + "timeago.js": "4.0.2" + }, + "devDependencies": { + "@notesnook/core": "file:../core", + "@types/react": "^18.2.39", + "react": "18.2.0", + "vitest": "^1.4.0" + }, + "peerDependencies": { + "react": ">=18", + "timeago.js": "4.0.2" + } + }, "../editor": { "name": "@notesnook/editor", "version": "2.1.3", @@ -3748,6 +3772,10 @@ "node": ">= 8" } }, + "node_modules/@notesnook/common": { + "resolved": "../common", + "link": true + }, "node_modules/@notesnook/editor": { "resolved": "../editor", "link": true @@ -17593,6 +17621,11 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", diff --git a/packages/editor-mobile/package.json b/packages/editor-mobile/package.json index 0b452d2e82..18bf1e3ffd 100644 --- a/packages/editor-mobile/package.json +++ b/packages/editor-mobile/package.json @@ -18,7 +18,9 @@ "react-freeze": "^1.0.3", "zustand": "^4.4.7", "@lingui/core": "4.11.4", - "@lingui/react": "4.11.4" + "@lingui/react": "4.11.4", + "tinycolor2": "1.6.0", + "@notesnook/common": "file:../common" }, "devDependencies": { "@playwright/test": "^1.37.1", diff --git a/packages/editor-mobile/src/components/editor.tsx b/packages/editor-mobile/src/components/editor.tsx index ba6ec512f5..ef5da10469 100644 --- a/packages/editor-mobile/src/components/editor.tsx +++ b/packages/editor-mobile/src/components/editor.tsx @@ -22,9 +22,9 @@ import { getFontById, getTableOfContents, TiptapOptions, + toBlobURL, usePermissionHandler } from "@notesnook/editor"; -import { toBlobURL } from "@notesnook/editor"; import { useThemeColors } from "@notesnook/theme"; import FingerprintIcon from "mdi-react/FingerprintIcon"; import { @@ -36,15 +36,11 @@ import { useState } from "react"; import { useEditorController } from "../hooks/useEditorController"; +import { useSafeArea } from "../hooks/useSafeArea"; import { useSettings } from "../hooks/useSettings"; -import { - NoteState, - TabItem, - TabStore, - useTabContext, - useTabStore -} from "../hooks/useTabStore"; -import { EventTypes, postAsyncWithTimeout, Settings } from "../utils"; +import { TabItem, useTabContext, useTabStore } from "../hooks/useTabStore"; +import { postAsyncWithTimeout, Settings } from "../utils"; +import { EditorEvents } from "../utils/editor-events"; import { pendingSaveRequests } from "../utils/pending-saves"; import Header from "./header"; import StatusBar from "./statusbar"; @@ -78,45 +74,39 @@ const Tiptap = ({ const isFocusedRef = useRef(false); const [undo, setUndo] = useState(false); const [redo, setRedo] = useState(false); + const insets = useSafeArea(); tabRef.current = tab; - function restoreNoteSelection(state?: NoteState) { - try { - if (!tabRef.current.noteId) return; - const noteState = - state || useTabStore.getState().noteState[tabRef.current.noteId]; + logger("info", tabRef.current.id, "rendering"); - if (noteState && (noteState.to || noteState.from)) { + const restoreNoteSelection = useCallback( + (scrollTop?: number, selection?: { to: number; from: number }) => { + if (!tabRef.current.session?.noteId) return; + const sel = selection || tabRef.current.session?.selection; + if (sel && sel.to && sel.from) { const size = editors[tabRef.current.id]?.state.doc.content.size || 0; - if ( - noteState.to > 0 && - noteState.to <= size && - noteState.from > 0 && - noteState.from <= size - ) { + if (sel.to > 0 && sel.to <= size && sel.from > 0 && sel.from <= size) { editors[tabRef.current.id]?.chain().setTextSelection({ - to: noteState.to, - from: noteState.from + to: sel.to, + from: sel.from }); } } - containerRef.current?.scrollTo({ left: 0, - top: noteState?.top || 0, + top: scrollTop || tabRef.current.session?.scrollTop || 0, behavior: "auto" }); - } catch (e) { - logger("error", (e as Error).message, (e as Error).stack); - } - } + }, + [] + ); usePermissionHandler({ claims: { premium: settings.premium }, onPermissionDenied: () => { - post(EventTypes.pro, undefined, tabRef.current.id, tab.noteId); + post(EditorEvents.pro, undefined, tabRef.current.id, tab.session?.noteId); } }); @@ -147,14 +137,14 @@ const Tiptap = ({ ) as Promise; }, createInternalLink(attributes) { - return postAsyncWithTimeout(EventTypes.createInternalLink, { + return postAsyncWithTimeout(EditorEvents.createInternalLink, { attributes }); }, element: getContentDiv(), - editable: !tab.readonly, + editable: !tab.session?.readonly, editorProps: { - editable: () => !tab.readonly, + editable: () => !tab.session?.readonly, handlePaste: (view, event) => { const hasFiles = event.clipboardData?.types?.some((type) => type.startsWith("Files") @@ -197,19 +187,12 @@ const Tiptap = ({ copyToClipboard: (text) => { globalThis.editorControllers[tab.id]?.copyToClipboard(text); }, - placeholder: strings.startWritingNote(), onSelectionUpdate: () => { - if (tabRef.current.noteId) { - const noteId = tabRef.current.noteId; + if (tabRef.current.session?.noteId) { clearTimeout(noteStateUpdateTimer.current); noteStateUpdateTimer.current = setTimeout(() => { - if (tabRef.current.noteId !== noteId) return; const { to, from } = editors[tabRef.current?.id]?.state.selection || {}; - useTabStore.getState().setNoteState(noteId, { - to, - from - }); }, 500); } }, @@ -228,7 +211,7 @@ const Tiptap = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [ getContentDiv, - tab.readonly, + tab.session?.readonly, settings.doubleSpacedLines, settings.corsProxy, settings.dateFormat, @@ -238,15 +221,19 @@ const Tiptap = ({ tick ]); - const update = useCallback(() => { - setTick((tick) => tick + 1); - globalThis.editorControllers[tabRef.current.id]?.setTitlePlaceholder( - strings.noteTitle() - ); - setTimeout(() => { - editorControllers[tabRef.current.id]?.setLoading(false); - }, 300); - }, []); + const update = useCallback( + (scrollTop?: number, selection?: { to: number; from: number }) => { + setTick((tick) => tick + 1); + globalThis.editorControllers[tabRef.current.id]?.setTitlePlaceholder( + strings.noteTitle() + ); + setTimeout(() => { + editorControllers[tabRef.current.id]?.setLoading(false); + restoreNoteSelection(scrollTop, selection); + }, 300); + }, + [restoreNoteSelection] + ); const controller = useEditorController({ update, @@ -286,60 +273,39 @@ const Tiptap = ({ }); } - const updateScrollPosition = (state: TabStore) => { + const updateFocusedTab = () => { if (isFocusedRef.current) return; - if (state.currentTab === tabRef.current.id) { - isFocusedRef.current = true; - const noteState = tabRef.current.noteId - ? state.noteState[tabRef.current.noteId] - : undefined; - - post( - EventTypes.tabFocused, - !!globalThis.editorControllers[tabRef.current.id]?.content.current && - !editorControllers[tabRef.current.id]?.loading, - tabRef.current.id, - state.getCurrentNoteId() - ); - editorControllers[tabRef.current.id]?.updateTab(); - - if (noteState) { - if ( - containerRef.current && - containerRef.current?.scrollHeight < noteState.top - ) { - console.log("Container too small to scroll."); - return; - } - - restoreNoteSelection(noteState); - } else { - containerRef.current?.scrollTo({ - left: 0, - top: 0, - behavior: "auto" - }); - } - - if ( - !globalThis.editorControllers[tabRef.current.id]?.content.current && - tabRef.current.noteId - ) { - editorControllers[tabRef.current.id]?.setLoading(true); - } - } else { - isFocusedRef.current = false; + isFocusedRef.current = true; + const noteId = + useTabStore.getState().tabs[useTabStore.getState().currentTab]?.session + ?.noteId; + post( + EditorEvents.tabFocused, + undefined, + useTabStore.getState().currentTab, + noteId + ); + editorControllers[tabRef.current.id]?.updateTab(); + + restoreNoteSelection(); + + if ( + !globalThis.editorControllers[tabRef.current.id]?.content.current && + tabRef.current.session?.noteId + ) { + editorControllers[tabRef.current.id]?.setLoading(true); } }; - updateScrollPosition(useTabStore.getState()); + updateFocusedTab(); const unsub = useTabStore.subscribe((state, prevState) => { if (state.currentTab !== tabRef.current.id) { isFocusedRef.current = false; } - if (state.currentTab === prevState.currentTab) return; - updateScrollPosition(state); + if (state.currentTab === prevState.currentTab && isFocusedRef.current) + return; + updateFocusedTab(); logger("info", "updating scroll position"); }); logger("info", tabRef.current.id, "active"); @@ -348,7 +314,7 @@ const Tiptap = ({ logger("info", tabRef.current.id, "inactive"); unsub(); }; - }, [getContentDiv]); + }, [getContentDiv, restoreNoteSelection]); const onClickEmptyArea: React.MouseEventHandler = useCallback( (event) => { @@ -542,7 +508,7 @@ const Tiptap = ({ position: "relative" }} > - {settings.noHeader || tab.locked ? null : ( + {settings.noHeader || tab.session?.locked ? null : ( <> )} - {controller.loading || tab.locked ? ( + {controller.loading || tab.session?.locked ? ( <div style={{ width: "100%", @@ -575,13 +541,13 @@ const Tiptap = ({ paddingLeft: 12, display: "flex", flexDirection: "column", - alignItems: tab.locked ? "center" : "flex-start", - justifyContent: tab.locked ? "center" : "flex-start", + alignItems: tab.session?.locked ? "center" : "flex-start", + justifyContent: tab.session?.locked ? "center" : "flex-start", boxSizing: "border-box", rowGap: 10 }} > - {tab.locked ? ( + {tab.session?.locked ? ( <> <p style={{ @@ -834,7 +800,7 @@ const Tiptap = ({ <div style={{ - display: tab.locked ? "none" : "block" + display: tab.session?.locked ? "none" : "block" }} ref={contentPlaceholderRef} className="theme-scope-editor" @@ -842,11 +808,11 @@ const Tiptap = ({ <div onClick={(e) => { - if (tab.locked) return; + if (tab.session?.locked) return; onClickBottomArea(); }} onMouseDown={(e) => { - if (tab.locked) return; + if (tab.session?.locked) return; if (globalThis.keyboardShown) { e.preventDefault(); } diff --git a/packages/editor-mobile/src/components/header.tsx b/packages/editor-mobile/src/components/header.tsx index 549e735dba..1525175862 100644 --- a/packages/editor-mobile/src/components/header.tsx +++ b/packages/editor-mobile/src/components/header.tsx @@ -19,18 +19,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. import { ControlledMenu, MenuItem as MenuItemInner } from "@szhsin/react-menu"; import ArrowBackIcon from "mdi-react/ArrowBackIcon"; +import ArrowForwardIcon from "mdi-react/ArrowForwardIcon"; import ArrowULeftTopIcon from "mdi-react/ArrowULeftTopIcon"; import ArrowURightTopIcon from "mdi-react/ArrowURightTopIcon"; import DotsHorizontalIcon from "mdi-react/DotsHorizontalIcon"; import DotsVerticalIcon from "mdi-react/DotsVerticalIcon"; import FullscreenIcon from "mdi-react/FullscreenIcon"; import MagnifyIcon from "mdi-react/MagnifyIcon"; +import PlusIcon from "mdi-react/PlusIcon"; + import PencilLockIcon from "mdi-react/PencilLockIcon"; import TableOfContentsIcon from "mdi-react/TableOfContentsIcon"; import React, { useRef, useState } from "react"; import { useSafeArea } from "../hooks/useSafeArea"; import { useTabContext, useTabStore } from "../hooks/useTabStore"; -import { EventTypes, Settings } from "../utils"; +import { Settings } from "../utils"; +import { EditorEvents } from "../utils/editor-events"; import styles from "./styles.module.css"; import { strings } from "@notesnook/intl"; @@ -100,6 +104,10 @@ function Header({ const openedTabsCount = useTabStore((state) => state.tabs.length); const [isOpen, setOpen] = useState(false); const btnRef = useRef(null); + const [canGoBack, canGoForward] = useTabStore((state) => [ + state.canGoBack, + state.canGoForward + ]); return ( <div @@ -131,7 +139,7 @@ function Header({ ) : ( <Button onPress={() => { - post(EventTypes.back, undefined, tab.id, tab.noteId); + post(EditorEvents.back, undefined, tab.id, tab.session?.noteId); }} preventDefault={false} style={{ @@ -165,75 +173,15 @@ function Header({ flexDirection: "row" }} > - {tab.locked ? null : ( - <> - <Button - onPress={() => { - editor?.commands.undo(); - }} - style={{ - borderWidth: 0, - borderRadius: 100, - color: "var(--nn_primary_icon)", - marginRight: 10, - width: 39, - height: 39, - display: "flex", - justifyContent: "center", - alignItems: "center", - position: "relative" - }} - > - <ArrowULeftTopIcon - color={ - !hasUndo - ? "var(--nn_secondary_border)" - : "var(--nn_primary_icon)" - } - size={25 * settings.fontScale} - style={{ - position: "absolute" - }} - /> - </Button> - - <Button - onPress={() => { - if (tab.locked) return; - editor?.commands.redo(); - }} - style={{ - borderWidth: 0, - borderRadius: 100, - color: "var(--nn_primary_icon)", - marginRight: 10, - width: 39, - height: 39, - display: "flex", - justifyContent: "center", - alignItems: "center", - position: "relative" - }} - > - <ArrowURightTopIcon - color={ - !hasRedo - ? "var(--nn_secondary_border)" - : "var(--nn_primary_icon)" - } - size={25 * settings.fontScale} - style={{ - position: "absolute" - }} - /> - </Button> - </> - )} - {settings.deviceMode !== "mobile" && !settings.fullscreen ? ( <Button onPress={() => { - post(EventTypes.fullscreen, undefined, tab.id, tab.noteId); + post( + EditorEvents.fullscreen, + undefined, + tab.id, + tab.session?.noteId + ); }} preventDefault={false} style={{ @@ -259,14 +207,12 @@ function Header({ </Button> ) : null} - {tab.readonly ? ( + {tab.session?.readonly ? ( <Button onPress={() => { post( "editor-events:disable-readonly-mode", - useTabStore - .getState() - .getNoteIdForTab(useTabStore.getState().currentTab) + tab.session?.noteId ); }} fwdRef={btnRef} @@ -296,7 +242,72 @@ function Header({ <Button onPress={() => { - post(EventTypes.showTabs, undefined, tab.id, tab.noteId); + editor?.commands.undo(); + }} + style={{ + borderWidth: 0, + borderRadius: 100, + color: "var(--nn_primary_icon)", + marginRight: 10, + width: 39, + height: 39, + display: "flex", + justifyContent: "center", + alignItems: "center", + position: "relative" + }} + > + <ArrowULeftTopIcon + color={ + !hasUndo + ? "var(--nn_secondary_border)" + : "var(--nn_primary_icon)" + } + size={25 * settings.fontScale} + style={{ + position: "absolute" + }} + /> + </Button> + + <Button + onPress={() => { + editor?.commands.redo(); + }} + style={{ + borderWidth: 0, + borderRadius: 100, + color: "var(--nn_primary_icon)", + marginRight: 10, + width: 39, + height: 39, + display: "flex", + justifyContent: "center", + alignItems: "center", + position: "relative" + }} + > + <ArrowURightTopIcon + color={ + !hasRedo + ? "var(--nn_secondary_border)" + : "var(--nn_primary_icon)" + } + size={25 * settings.fontScale} + style={{ + position: "absolute" + }} + /> + </Button> + + <Button + onPress={() => { + post( + EditorEvents.showTabs, + undefined, + tab.id, + tab.session?.noteId + ); }} preventDefault={false} style={{ @@ -340,8 +351,13 @@ function Header({ <Button fwdRef={btnRef} onPress={() => { - if (tab.locked) { - post(EventTypes.properties, undefined, tab.id, tab.noteId); + if (tab.session?.locked) { + post( + EditorEvents.properties, + undefined, + tab.id, + tab.session?.noteId + ); } else { setOpen(!isOpen); } @@ -360,7 +376,7 @@ function Header({ position: "relative" }} > - {tab.locked ? ( + {tab.session?.locked ? ( <DotsHorizontalIcon size={25 * settings.fontScale} style={{ @@ -395,33 +411,153 @@ function Header({ switch (e.value) { case "toc": post( - EventTypes.toc, + EditorEvents.toc, editorControllers[tab.id]?.getTableOfContents(), tab.id, - tab.noteId + tab.session?.noteId ); break; case "search": editor?.commands.startSearch(); break; + case "newNote": + post( + EditorEvents.newNote, + undefined, + tab.id, + tab.session?.noteId + ); + break; case "properties": - logger("info", "post properties..."); - post(EventTypes.properties, undefined, tab.id, tab.noteId); + post( + EditorEvents.properties, + undefined, + tab.id, + tab.session?.noteId + ); break; default: break; } }} > + <div + style={{ + display: "flex", + gap: 10, + alignItems: "center", + flexDirection: "row", + justifyContent: "center", + flex: 1, + paddingTop: 5 + }} + > + <Button + onPress={() => { + post( + EditorEvents.goBack, + undefined, + tab.id, + tab.session?.noteId + ); + setOpen(false); + }} + style={{ + borderWidth: 0, + borderRadius: 100, + color: "var(--nn_primary_icon)", + width: 39, + height: 39, + display: "flex", + justifyContent: "center", + alignItems: "center", + position: "relative" + }} + > + <ArrowBackIcon + color={ + !canGoBack + ? "var(--nn_secondary_border)" + : "var(--nn_primary_icon)" + } + size={25 * settings.fontScale} + style={{ + position: "absolute" + }} + /> + </Button> + + <Button + onPress={() => { + post( + EditorEvents.goForward, + undefined, + tab.id, + tab.session?.noteId + ); + setOpen(false); + }} + style={{ + borderWidth: 0, + borderRadius: 100, + color: "var(--nn_primary_icon)", + width: 39, + height: 39, + display: "flex", + justifyContent: "center", + alignItems: "center", + position: "relative" + }} + > + <ArrowForwardIcon + color={ + !canGoForward + ? "var(--nn_secondary_border)" + : "var(--nn_primary_icon)" + } + size={25 * settings.fontScale} + style={{ + position: "absolute" + }} + /> + </Button> + + <Button + onPress={() => { + editor?.commands.startSearch(); + setOpen(false); + }} + style={{ + borderWidth: 0, + borderRadius: 100, + color: "var(--nn_primary_icon)", + width: 39, + height: 39, + display: "flex", + justifyContent: "center", + alignItems: "center", + position: "relative" + }} + > + <MagnifyIcon + size={28 * settings.fontScale} + style={{ + position: "absolute" + }} + color="var(--nn_primary_icon)" + /> + </Button> + </div> + <MenuItem - value="search" + value="newNote" style={{ display: "flex", gap: 10, alignItems: "center" }} > - <MagnifyIcon + <PlusIcon size={22 * settings.fontScale} color="var(--nn_primary_icon)" /> @@ -430,7 +566,7 @@ function Header({ color: "var(--nn_primary_paragraph)" }} > - {strings.search()} + New note </span> </MenuItem> diff --git a/packages/editor-mobile/src/components/readonly-editor.tsx b/packages/editor-mobile/src/components/readonly-editor.tsx index b1573c8ae6..38ffa17a4a 100644 --- a/packages/editor-mobile/src/components/readonly-editor.tsx +++ b/packages/editor-mobile/src/components/readonly-editor.tsx @@ -27,7 +27,8 @@ import { useState } from "react"; import { useSettings } from "../hooks/useSettings"; -import { EventTypes, Settings, isReactNative, randId } from "../utils"; +import { Settings, isReactNative, randId } from "../utils"; +import { EditorEvents } from "../utils/editor-events"; export const ReadonlyEditorProvider = (): JSX.Element => { const settings = useSettings(); @@ -95,7 +96,7 @@ const Tiptap = ({ delete pendingResolvers[resolverId]; resolve(data); }; - post(EventTypes.getAttachmentData, { + post(EditorEvents.getAttachmentData, { attachment, resolverId: resolverId }); @@ -142,7 +143,7 @@ const Tiptap = ({ if (isSafari) { root = window; } - post(EventTypes.readonlyEditorLoaded); + post(EditorEvents.readonlyEditorLoaded); const onMessage = (event: any) => { if (event?.data?.[0] !== "{") return; diff --git a/packages/editor-mobile/src/components/tags.tsx b/packages/editor-mobile/src/components/tags.tsx index a232832de5..56399757ea 100644 --- a/packages/editor-mobile/src/components/tags.tsx +++ b/packages/editor-mobile/src/components/tags.tsx @@ -18,7 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ import React, { useEffect, useRef, useState } from "react"; -import { EventTypes, Settings } from "../utils"; +import { Settings } from "../utils"; +import { EditorEvents } from "../utils/editor-events"; import styles from "./styles.module.css"; import { useTabContext } from "../hooks/useTabStore"; import { strings } from "@notesnook/intl"; @@ -45,7 +46,7 @@ function Tags(props: { settings: Settings; loading?: boolean }): JSX.Element { editor.commands.blur(); editorTitles[tab.id]?.current?.blur(); } - post(EventTypes.newtag, undefined, tab.id, tab.noteId); + post(EditorEvents.newtag, undefined, tab.id, tab.session?.noteId); }; const fontScale = props.settings?.fontScale || 1; @@ -126,7 +127,7 @@ function Tags(props: { settings: Settings; loading?: boolean }): JSX.Element { }} onClick={(e) => { e.preventDefault(); - post(EventTypes.tag, tag, tab.id, tab.noteId); + post(EditorEvents.tag, tag, tab.id, tab.session?.noteId); }} > #{tag.alias} diff --git a/packages/editor-mobile/src/components/tiptap.tsx b/packages/editor-mobile/src/components/tiptap.tsx index 28bced355e..b1f91f2104 100644 --- a/packages/editor-mobile/src/components/tiptap.tsx +++ b/packages/editor-mobile/src/components/tiptap.tsx @@ -36,7 +36,7 @@ export default function TiptapEditorWrapper(props: { return ( <> - {tab.locked ? null : ( + {tab.session?.locked ? null : ( <EmotionEditorToolbarTheme> <Toolbar className="theme-scope-editorToolbar" diff --git a/packages/editor-mobile/src/components/title.tsx b/packages/editor-mobile/src/components/title.tsx index 751122c5f9..00672a849e 100644 --- a/packages/editor-mobile/src/components/title.tsx +++ b/packages/editor-mobile/src/components/title.tsx @@ -17,8 +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 { getFontById } from "@notesnook/editor"; -import { replaceDateTime } from "@notesnook/editor"; +import { getFontById, replaceDateTime } from "@notesnook/editor"; import React, { RefObject, useCallback, useEffect, useRef } from "react"; import { EditorController } from "../hooks/useEditorController"; import { useTabContext } from "../hooks/useTabStore"; diff --git a/packages/editor-mobile/src/hooks/useEditorController.ts b/packages/editor-mobile/src/hooks/useEditorController.ts index c6b92118a0..b943e3ddf4 100644 --- a/packages/editor-mobile/src/hooks/useEditorController.ts +++ b/packages/editor-mobile/src/hooks/useEditorController.ts @@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ import { Editor, scrollIntoViewById } from "@notesnook/editor"; +import { strings } from "@notesnook/intl"; import { ThemeDefinition, useThemeColors, @@ -31,7 +32,6 @@ import { useState } from "react"; import { - EventTypes, getRoot, isReactNative, post, @@ -39,9 +39,9 @@ import { saveTheme } from "../utils"; import { injectCss, transform } from "../utils/css"; +import { EditorEvents } from "../utils/editor-events"; import { pendingSaveRequests } from "../utils/pending-saves"; import { useTabContext, useTabStore } from "./useTabStore"; -import { strings } from "@notesnook/intl"; type Attachment = { hash: string; @@ -128,7 +128,10 @@ export function useEditorController({ getTableOfContents, scrollTo }: { - update: () => void; + update: ( + scrollTop?: number, + selection?: { to: number; from: number } + ) => void; getTableOfContents: () => any[]; scrollTo: (top: number) => void; }): EditorController { @@ -137,7 +140,7 @@ export function useEditorController({ const tabRef = useRef(tab); tabRef.current = tab; - const [loading, setLoading] = useState(true); + const [loading, setLoading] = useState(false); const setTheme = useThemeEngineStore((store) => store.setTheme); const { colors } = useThemeColors("editor"); const [title, setTitle] = useState(""); @@ -151,8 +154,12 @@ export function useEditorController({ scroll: null }); - if (!tabRef.current.noteId && loading) { - setLoading(false); + if (!tabRef.current.session?.noteId && loading) { + setTimeout(() => { + if (!tabRef.current.session?.noteId && loading) { + setLoading(false); + } + }, 3000); } const selectionChange = useCallback((_editor: Editor) => {}, []); @@ -161,21 +168,21 @@ export function useEditorController({ if (!isReactNative()) return; const currentSessionId = globalThis.sessionId; post( - EventTypes.contentchange, + EditorEvents.contentchange, undefined, tabRef.current.id, - tabRef.current.noteId + tabRef.current.session?.noteId ); const params = [ { title }, tabRef.current.id, - tabRef.current.noteId, + tabRef.current.session?.noteId, currentSessionId ]; const pendingTitleIds = await pendingSaveRequests.getPendingTitleIds(); - postAsyncWithTimeout(EventTypes.title, ...params, 1000) + postAsyncWithTimeout(EditorEvents.title, ...params, 1000) .then(() => { if (pendingTitleIds.length) { dbLogger( @@ -224,30 +231,31 @@ export function useEditorController({ } const currentSessionId = globalThis.sessionId; post( - EventTypes.contentchange, + EditorEvents.contentchange, undefined, tabRef.current.id, - tabRef.current.noteId + tabRef.current.session?.noteId ); if (!editor) return; if (typeof timers.current.change === "number") { clearTimeout(timers.current?.change); } + timers.current.change = setTimeout(async () => { htmlContentRef.current = editor.getHTML(); - const params = [ { html: htmlContentRef.current, ignoreEdit: ignoreEdit }, tabRef.current.id, - tabRef.current.noteId, + tabRef.current.session?.noteId, currentSessionId ]; + const pendingContentIds = await pendingSaveRequests.getPendingContentIds(); - postAsyncWithTimeout(EventTypes.content, ...params, 5000) + postAsyncWithTimeout(EditorEvents.content, ...params, 5000) .then(() => { if (pendingContentIds.length) { dbLogger( @@ -278,12 +286,7 @@ export function useEditorController({ } }); - logger( - "info", - "Editor saving content", - tabRef.current.id, - tabRef.current.noteId - ); + logger("info", "Editor saving content", params[1], params[2]); }, 300); countWords(5000); @@ -297,14 +300,24 @@ export function useEditorController({ if (timers.current.scroll !== null) clearTimeout(timers.current.scroll); timers.current.scroll = setTimeout(() => { if ( - tabRef.current.noteId && - tabRef.current.noteId === useTabStore.getState().getCurrentNoteId() + tabRef.current.session?.noteId && + tabRef.current.session?.noteId === + useTabStore.getState().getCurrentNoteId() ) { - useTabStore.getState().setNoteState(tabRef.current.noteId, { - top: value - }); + post( + EditorEvents.saveScroll, + { + scrollTop: value, + selection: { + to: editors[tabRef.current.id]?.state.selection.to, + from: editors[tabRef.current.id]?.state.selection.from + } + }, + tabRef.current.id, + tabRef.current.session?.noteId + ); } - }, 16); + }, 300); }, [] ); @@ -315,12 +328,12 @@ export function useEditorController({ }, [update]); useEffect(() => { - if (tab.locked) { + if (tab.session?.locked) { htmlContentRef.current = ""; setLoading(true); onUpdate(); } - }, [tab.locked, onUpdate]); + }, [tab.session?.locked, onUpdate]); const onMessage = useCallback( (event: Event & { data?: string }) => { @@ -336,40 +349,36 @@ export function useEditorController({ const editor = editors[tabRef.current.id]; switch (type) { case "native:updatehtml": { - htmlContentRef.current = value; - logger("info", "UPDATING NOTE HTML"); + htmlContentRef.current = value.data; if (tabRef.current.id !== useTabStore.getState().currentTab) { updateTabOnFocus.current = true; } else { if (!editor) break; - const noteState = tabRef.current?.noteId - ? useTabStore.getState().noteState[tabRef.current?.noteId] - : null; - editor?.commands.setContent(htmlContentRef.current, false, { preserveWhitespace: true }); - if (noteState) { - editor.commands.setTextSelection({ - from: noteState.from, - to: noteState.to - }); + if (value.selection) { + editor.commands.setTextSelection(value.selection); } - scrollTo?.(noteState?.top || 0); + scrollTo?.(value.scrollTop || 0); + setLoading(false); countWords(0); } break; } case "native:html": - if (htmlContentRef.current === value) break; - htmlContentRef.current = value; + if (htmlContentRef.current === value.data) { + setLoading(false); + break; + } + htmlContentRef.current = value.data; logger("info", "LOADING NOTE HTML"); if (!editor) break; - update(); + update(value.scrollTop, value.selection); setTimeout(() => { countWords(0); }, 300); @@ -403,7 +412,7 @@ export function useEditorController({ } post(type); // Notify that message was delivered successfully. }, - [update, countWords, setTheme] + [update, setTheme, scrollTo, countWords] ); useEffect(() => { @@ -414,36 +423,46 @@ export function useEditorController({ }, [onMessage]); const openFilePicker = useCallback((type: "image" | "file" | "camera") => { - post(EventTypes.filepicker, type, tabRef.current.id, tabRef.current.noteId); + post( + EditorEvents.filepicker, + type, + tabRef.current.id, + tabRef.current.session?.noteId + ); }, []); const downloadAttachment = useCallback((attachment: Attachment) => { post( - EventTypes.download, + EditorEvents.download, attachment, tabRef.current.id, - tabRef.current.noteId + tabRef.current.session?.noteId ); }, []); const previewAttachment = useCallback((attachment: Attachment) => { post( - EventTypes.previewAttachment, + EditorEvents.previewAttachment, attachment, tabRef.current.id, - tabRef.current.noteId + tabRef.current.session?.noteId ); }, []); const openLink = useCallback((url: string) => { - post(EventTypes.link, url, tabRef.current.id, tabRef.current.noteId); + post( + EditorEvents.link, + url, + tabRef.current.id, + tabRef.current.session?.noteId + ); return true; }, []); const copyToClipboard = (text: string) => { - post(EventTypes.copyToClipboard, text); + post(EditorEvents.copyToClipboard, text); }; const getAttachmentData = (attachment: Partial<Attachment>) => { - return postAsyncWithTimeout(EventTypes.getAttachmentData, { + return postAsyncWithTimeout(EditorEvents.getAttachmentData, { attachment }); }; diff --git a/packages/editor-mobile/src/hooks/useTabStore.ts b/packages/editor-mobile/src/hooks/useTabStore.ts index 4d6834b73f..46a2cf2c3a 100644 --- a/packages/editor-mobile/src/hooks/useTabStore.ts +++ b/packages/editor-mobile/src/hooks/useTabStore.ts @@ -28,182 +28,43 @@ globalThis.statusBars = {}; export type TabItem = { id: number; - noteId?: string; - previewTab?: boolean; - readonly?: boolean; - locked?: boolean; - noteLocked?: boolean; + session?: { + noteId?: string; + readonly?: boolean; + locked?: boolean; + noteLocked?: boolean; + scrollTop?: number; + selection?: { to: number; from: number }; + }; pinned?: boolean; -}; - -export type NoteState = { - top: number; - to: number; - from: number; + needsRefresh?: boolean; }; export type TabStore = { tabs: TabItem[]; currentTab: number; scrollPosition: Record<number, number>; - noteState: Record<string, NoteState>; - updateTab: (id: number, options: Omit<Partial<TabItem>, "id">) => void; - removeTab: (index: number) => void; - moveTab: (index: number, toIndex: number) => void; - newTab: (noteId?: string, previewTab?: boolean) => void; - focusTab: (id: number) => void; - setScrollPosition: (id: number, position: number) => void; - getNoteIdForTab: (id: number) => string | undefined; - getTabForNote: (noteId: string) => number | undefined; - hasTabForNote: (noteId: string) => boolean; - focusEmptyTab: () => void; - focusPreviewTab: ( - noteId: string, - options: Omit<Partial<TabItem>, "id"> - ) => void; - getCurrentNoteId: () => string | undefined; - getTab: (tabId: number) => TabItem | undefined; - setNoteState: (noteId: string, state: Partial<NoteState>) => void; biometryAvailable?: boolean; biometryEnrolled?: boolean; + canGoBack?: boolean; + canGoForward?: boolean; + sessionId?: string; + getCurrentNoteId: () => string | undefined; }; -function getId(id: number, tabs: TabItem[]): number { - const exists = tabs.find((t) => t.id === id); - if (exists) { - return getId(id + 1, tabs); - } - return id; -} - export const useTabStore = create( persist<TabStore>( (set, get) => ({ - noteState: {}, tabs: [ { - id: 0, - previewTab: true + id: 0 } ], currentTab: 0, scrollPosition: {}, - setNoteState: (noteId: string, state: Partial<NoteState>) => { - if (editorControllers[get().currentTab]?.loading) return; - - const noteState = { - ...get().noteState - }; - noteState[noteId] = { - ...get().noteState[noteId], - ...state - }; - - set({ - noteState - }); - }, - updateTab: (id: number, options: Omit<Partial<TabItem>, "id">) => { - const index = get().tabs.findIndex((t) => t.id === id); - if (index == -1) return; - const tabs = [...get().tabs]; - tabs[index] = { - ...tabs[index], - ...options - }; - set({ - tabs: tabs - }); - }, - removeTab: (index: number) => { - const scrollPosition = { ...get().scrollPosition }; - if (scrollPosition[index]) { - delete scrollPosition[index]; - } - globalThis.editorControllers[index] = undefined; - globalThis.editors[index] = null; - - set({ - scrollPosition - }); - }, - focusPreviewTab: (noteId: string, options) => { - const index = get().tabs.findIndex((t) => t.previewTab); - if (index == -1) return get().newTab(noteId, true); - const tabs = [...get().tabs]; - tabs[index] = { - ...tabs[index], - noteId: noteId, - previewTab: true, - ...options - }; - - set({ - currentTab: tabs[index].id - }); - }, - focusEmptyTab: () => { - const index = get().tabs.findIndex((t) => !t.noteId); - if (index == -1) return get().newTab(); - const tabs = [...get().tabs]; - tabs[index] = { - ...tabs[index] - }; - set({ - currentTab: tabs[index].id - }); - }, - newTab: (noteId?: string, previewTab?: boolean) => { - const id = getId(get().tabs.length, get().tabs); - const nextTabs = [ - ...get().tabs, - { - id: id, - noteId, - previewTab: previewTab - } - ]; - set({ - tabs: nextTabs, - currentTab: id - }); - }, - moveTab: (index: number, toIndex: number) => { - const tabs = get().tabs.slice(); - tabs.splice(toIndex, 0, tabs.slice(index, 1)[0]); - set({ - tabs: tabs - }); - }, - focusTab: (id: number) => { - set({ - currentTab: id - }); - }, - setScrollPosition: (id: number, position: number) => { - set({ - scrollPosition: { - ...get().scrollPosition, - [id]: position - } - }); - }, - getNoteIdForTab: (id: number) => { - return get().tabs.find((t) => t.id === id)?.noteId; - }, - hasTabForNote: (noteId: string) => { - return ( - typeof get().tabs.find((t) => t.noteId === noteId)?.id === "number" - ); - }, - getTabForNote: (noteId: string) => { - return get().tabs.find((t) => t.noteId === noteId)?.id; - }, getCurrentNoteId: () => { - return get().tabs.find((t) => t.id === get().currentTab)?.noteId; - }, - getTab: (tabId) => { - return get().tabs.find((t) => t.id === tabId); + return get().tabs.find((t) => t.id === get().currentTab)?.session + ?.noteId; } }), { diff --git a/apps/mobile/app/screens/editor/tiptap/editor-events.ts b/packages/editor-mobile/src/utils/editor-events.ts similarity index 89% rename from apps/mobile/app/screens/editor/tiptap/editor-events.ts rename to packages/editor-mobile/src/utils/editor-events.ts index 668c7d7cb2..2d036d2b40 100644 --- a/apps/mobile/app/screens/editor/tiptap/editor-events.ts +++ b/packages/editor-mobile/src/utils/editor-events.ts @@ -16,7 +16,8 @@ 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/>. */ -export const EventTypes = { + +export const EditorEvents = { selection: "editor-event:selection", content: "editor-event:content", title: "editor-event:title", @@ -49,5 +50,9 @@ export const EventTypes = { disableReadonlyMode: "editor-events:disable-readonly-mode", readonlyEditorLoaded: "readonlyEditorLoaded", error: "editorError", - dbLogger: "editor-events:dbLogger" -}; + dbLogger: "editor-events:dbLogger", + goBack: "editor-events:go-back", + goForward: "editor-events:go-forward", + saveScroll: "editor-events:save-scroll", + newNote: "editor-events:new-note" +} as const; diff --git a/packages/editor-mobile/src/utils/index.ts b/packages/editor-mobile/src/utils/index.ts index 8db81b8aaa..4d6983fff9 100644 --- a/packages/editor-mobile/src/utils/index.ts +++ b/packages/editor-mobile/src/utils/index.ts @@ -22,6 +22,8 @@ import { ThemeDefinition } from "@notesnook/theme"; import { Dispatch, MutableRefObject, RefObject, SetStateAction } from "react"; import { EditorController } from "../hooks/useEditorController"; +import { EditorEvents } from "./editor-events"; + globalThis.sessionId = "notesnook-editor"; globalThis.pendingResolvers = {}; @@ -150,8 +152,8 @@ declare global { * @param value */ - function post<T extends keyof typeof EventTypes>( - type: (typeof EventTypes)[T], + function post<T extends keyof typeof EditorEvents>( + type: (typeof EditorEvents)[T], value?: unknown, tabId?: number, noteId?: string, @@ -184,44 +186,6 @@ export function getOnMessageListener(callback: () => void) { }; } -/* eslint-enable no-var */ - -export const EventTypes = { - selection: "editor-event:selection", - content: "editor-event:content", - title: "editor-event:title", - scroll: "editor-event:scroll", - history: "editor-event:history", - newtag: "editor-event:newtag", - tag: "editor-event:tag", - filepicker: "editor-event:picker", - download: "editor-event:download-attachment", - logger: "native:logger", - back: "editor-event:back", - pro: "editor-event:pro", - monograph: "editor-event:monograph", - properties: "editor-event:properties", - fullscreen: "editor-event:fullscreen", - link: "editor-event:link", - contentchange: "editor-event:content-change", - reminders: "editor-event:reminders", - previewAttachment: "editor-event:preview-attachment", - copyToClipboard: "editor-events:copy-to-clipboard", - getAttachmentData: "editor-events:get-attachment-data", - tabsChanged: "editor-events:tabs-changed", - showTabs: "editor-events:show-tabs", - tabFocused: "editor-events:tab-focused", - toc: "editor-events:toc", - createInternalLink: "editor-events:create-internal-link", - load: "editor-events:load", - unlock: "editor-events:unlock", - unlockWithBiometrics: "editor-events:unlock-biometrics", - disableReadonlyMode: "editor-events:disable-readonly-mode", - readonlyEditorLoaded: "readonlyEditorLoaded", - error: "editorError", - dbLogger: "editor-events:dbLogger" -} as const; - export function randId(prefix: string) { return Math.random() .toString(36) @@ -244,7 +208,7 @@ export function logger( }) .join(" "); - post(EventTypes.logger, `[${type}]: ` + logString); + post(EditorEvents.logger, `[${type}]: ` + logString); } export function dbLogger(type: "error" | "log", ...logs: unknown[]): void { @@ -254,7 +218,7 @@ export function dbLogger(type: "error" | "log", ...logs: unknown[]): void { }) .join(" "); - post(EventTypes.dbLogger, { + post(EditorEvents.dbLogger, { message: `[${type}]: ` + logString, error: logs[0] instanceof Error ? logs[0] : undefined }); diff --git a/packages/editor-mobile/src/utils/native-events.ts b/packages/editor-mobile/src/utils/native-events.ts new file mode 100644 index 0000000000..75bf670343 --- /dev/null +++ b/packages/editor-mobile/src/utils/native-events.ts @@ -0,0 +1,32 @@ +/* +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/>. +*/ + +export const NativeEvents = { + html: "native:html", + updatehtml: "native:updatehtml", + title: "native:title", + theme: "native:theme", + titleplaceholder: "native:titleplaceholder", + logger: "native:logger", + status: "native:status", + keyboardShown: "native:keyboardShown", + attachmentData: "native:attachment-data", + resolve: "native:resolve", + session: "native:session" +}; diff --git a/packages/editor-mobile/src/utils/pending-saves.ts b/packages/editor-mobile/src/utils/pending-saves.ts index 2a415b944c..aae55f6b56 100644 --- a/packages/editor-mobile/src/utils/pending-saves.ts +++ b/packages/editor-mobile/src/utils/pending-saves.ts @@ -16,7 +16,8 @@ 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 { EventTypes, postAsyncWithTimeout, randId } from "."; +import { postAsyncWithTimeout, randId } from "."; +import { EditorEvents } from "./editor-events"; class PendingSaveRequests { static TITLES = "pendingTitles"; @@ -118,7 +119,7 @@ class PendingSaveRequests { this.remove(PendingSaveRequests.TITLES); for (const pending of pendingTitles) { if (pending.params[0]) pending.params[0].pendingChanges = true; - await postAsyncWithTimeout(EventTypes.title, ...pending.params, 5000); + await postAsyncWithTimeout(EditorEvents.title, ...pending.params, 5000); } }; @@ -127,7 +128,11 @@ class PendingSaveRequests { this.remove(PendingSaveRequests.CONTENT); for (const pending of pendingContents) { if (pending.params[0]) pending.params[0].pendingChanges = true; - await postAsyncWithTimeout(EventTypes.content, ...pending.params, 5000); + await postAsyncWithTimeout( + EditorEvents.content, + ...pending.params, + 5000 + ); } };