From c80286b58712df48fa2949917bbe682028cf85b0 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Tue, 14 May 2024 15:35:49 +0500 Subject: [PATCH] mobile: tab history --- .../components/sheets/editor-tabs/index.tsx | 8 +- .../app/components/sheets/link-note/index.tsx | 12 +- apps/mobile/app/screens/editor/index.tsx | 2 +- .../app/screens/editor/readonly-editor.tsx | 14 +- .../app/screens/editor/tiptap/commands.ts | 45 ++- .../editor/tiptap/use-editor-events.tsx | 95 ++--- .../app/screens/editor/tiptap/use-editor.ts | 343 +++++++++--------- .../screens/editor/tiptap/use-tab-store.ts | 141 +++++-- .../mobile/app/screens/editor/tiptap/utils.ts | 17 +- apps/mobile/package-lock.json | 9 + apps/mobile/package.json | 2 +- packages/common/src/utils/editor-sessions.ts | 109 ++++++ packages/common/src/utils/tab-history.ts | 184 ++++++++++ packages/editor-mobile/package-lock.json | 46 ++- packages/editor-mobile/package.json | 4 +- .../editor-mobile/src/components/editor.tsx | 319 ++++++++++++---- .../editor-mobile/src/components/header.tsx | 167 ++++++++- .../src/components/readonly-editor.tsx | 7 +- .../editor-mobile/src/components/tags.tsx | 7 +- .../src/hooks/useEditorController.ts | 65 ++-- .../editor-mobile/src/hooks/useTabStore.ts | 50 ++- .../editor-mobile/src/utils}/editor-events.ts | 9 +- packages/editor-mobile/src/utils/index.ts | 56 +-- .../editor-mobile/src/utils/native-events.ts | 32 ++ .../editor-mobile/src/utils/pending-saves.ts | 11 +- 25 files changed, 1273 insertions(+), 481 deletions(-) create mode 100644 packages/common/src/utils/editor-sessions.ts create mode 100644 packages/common/src/utils/tab-history.ts rename {apps/mobile/app/screens/editor/tiptap => packages/editor-mobile/src/utils}/editor-events.ts (92%) create mode 100644 packages/editor-mobile/src/utils/native-events.ts diff --git a/apps/mobile/app/components/sheets/editor-tabs/index.tsx b/apps/mobile/app/components/sheets/editor-tabs/index.tsx index d62d421f5..a485b00b8 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 239dfbfa1..41a0d38ec 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/screens/editor/index.tsx b/apps/mobile/app/screens/editor/index.tsx index 849cf9488..aaa2e8336 100755 --- a/apps/mobile/app/screens/editor/index.tsx +++ b/apps/mobile/app/screens/editor/index.tsx @@ -221,7 +221,7 @@ const useLockedNoteHandler = () => { biometryAvailable: !!biometry, biometryEnrolled: !!fingerprint }); - syncTabs(); + syncTabs("biometry"); })(); }, [tab?.id]); diff --git a/apps/mobile/app/screens/editor/readonly-editor.tsx b/apps/mobile/app/screens/editor/readonly-editor.tsx index 4ee0dfde9..d28456822 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) { props.onLoad?.((content: { data: string; id: string }) => { setTimeout(() => { noteId.current = content.id; @@ -86,7 +86,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; downloadAttachment(attachment.hash, true, { @@ -104,7 +104,7 @@ export function ReadonlyEditor(props: { ); editorRef.current?.postMessage( JSON.stringify({ - type: EditorEvents.attachmentData, + type: NativeEvents.attachmentData, value: { resolverId: (editorMessage.value as any).resolverId, data @@ -115,7 +115,7 @@ export function ReadonlyEditor(props: { .catch(() => { 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/tiptap/commands.ts b/apps/mobile/app/screens/editor/tiptap/commands.ts index 99577e7fc..f3b0eeff4 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 "./tab-history"; type Action = { job: string; id: string }; @@ -167,9 +168,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); `); }; @@ -353,7 +355,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 4cfa85e1f..5d00fdd70 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 { tabHistory, useTabStore } from "./use-tab-store"; +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); @@ -352,25 +353,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; } @@ -380,8 +381,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, @@ -391,8 +392,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, @@ -402,10 +403,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, @@ -418,12 +419,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(), @@ -441,7 +442,7 @@ export const useEditorEvents = ( onAdd: () => ReminderSheet.present(undefined, note, true) }); break; - case EventTypes.newtag: + case EditorEvents.newtag: if (!noteId) { ToastManager.show({ heading: strings.createNoteFirst(), @@ -451,7 +452,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); @@ -467,7 +468,7 @@ export const useEditorEvents = ( }); } break; - case EventTypes.filepicker: + case EditorEvents.filepicker: editorState().isAwaitingResult = true; const { pick } = require("./picker").default; pick({ @@ -479,14 +480,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; @@ -506,14 +507,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 }); @@ -522,26 +523,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( @@ -553,7 +554,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; @@ -564,11 +565,11 @@ export const useEditorEvents = ( } break; } - case EventTypes.copyToClipboard: { + case EditorEvents.copyToClipboard: { Clipboard.setString(editorMessage.value as string); break; } - case EventTypes.tabsChanged: { + case EditorEvents.tabsChanged: { // useTabStore.setState({ // tabs: (editorMessage.value as any)?.tabs, // currentTab: (editorMessage.value as any)?.currentTab @@ -576,14 +577,14 @@ export const useEditorEvents = ( // 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: ( ; }; @@ -142,7 +145,7 @@ export const useEditor = ( }, [commands, insets, isDefaultEditor]); useEffect(() => { - postMessage(EditorEvents.theme, theme); + postMessage(NativeEvents.theme, theme); }, [theme, postMessage]); useEffect(() => { @@ -201,8 +204,9 @@ 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)); @@ -269,13 +273,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; @@ -325,7 +322,7 @@ export const useEditor = ( if (!noteData.title) { postMessage( - EditorEvents.title, + NativeEvents.title, currentNotes.current[id]?.title, tabId ); @@ -385,8 +382,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; } @@ -435,7 +432,7 @@ export const useEditor = ( ); const loadNote = useCallback( - async (event: { + (event: { item?: Note; forced?: boolean; newNote?: boolean; @@ -443,171 +440,165 @@ export const useEditor = ( blockId?: string; presistTab?: boolean; }) => { - if (!event) return; - - 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; } - - 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); - } - 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); - } - } 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 - }); - } else { - // 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 - }); - } - } - } else { - if (lastTabFocused.current !== event.tabId) { - useTabStore.getState().focusTab(event.tabId); - } - } - - const tabId = event.tabId || useTabStore.getState().currentTab; - if (lastTabFocused.current !== tabId) { - // if ((await waitForEvent(eEditorTabFocused, 1000)) !== tabId) { - // - // return; - // } - currentLoadingNoteId.current = item.id; - - return; - } - - state.current.movedAway = false; state.current.currentlyEditing = true; - if (!tabLocked) { - await loadContent(item); - } - if ( - currentNotes.current[item.id] && - loadingState.current && - currentContents.current[item.id]?.data && - loadingState.current === currentContents.current[item.id]?.data + !state.current.ready && + (await isEditorLoaded( + editorRef, + sessionIdRef.current, + useTabStore.getState().currentTab + )) ) { - // If note is already loading, return. - - return; + state.current.ready = true; } - if (!state.current.ready) { + if (event.newNote && !currentLoadingNoteId.current) { + const tabId = useTabStore.getState().currentTab; + await reset(tabId, true, true); + setTimeout(() => { + if (state.current?.ready && !state.current.movedAway) + commands.focus(tabId); + }); + } else { + if (!event.item) { + overlay(false); + return; + } + + const item = event.item; + currentLoadingNoteId.current = item.id; + + const currentTab = useTabStore + .getState() + .getTab(useTabStore.getState().currentTab); + if (currentTab?.previewTab && item.id !== currentTab.noteId) { + await commands.setLoading(true, useTabStore.getState().currentTab); + } + const isLockedNote = await db.vaults.itemExists( + event.item as ItemReference + ); + const tabLocked = + isLockedNote && !(event.item as NoteWithContent).content; + + let tabId = + event.tabId || useTabStore.getState().getTabForNote(event.item.id); + if (tabId === undefined) tabId = useTabStore.getState().currentTab; + + const isOpened = + useTabStore.getState().getTabForNote(event.item.id) === tabId; + + if (!isOpened) { + await commands.setLoading(true, tabId); + } + + useTabStore.getState().updateTab(tabId, { + readonly: event.item.readonly || readonly, + ...(isOpened + ? { + noteId: event.item.id + } + : { + locked: tabLocked, + noteLocked: isLockedNote, + noteId: event.item.id + }) + }); + + useTabStore.getState().focusTab(tabId); + + if (lastTabFocused.current !== tabId) { + useTabStore.getState().focusTab(tabId); + } + + setTimeout(() => { + if (blockIdRef.current) { + commands.scrollIntoViewById(blockIdRef.current); + blockIdRef.current = undefined; + } + }, 150); + + if (lastTabFocused.current !== tabId) { + // if ((await waitForEvent(eEditorTabFocused, 1000)) !== tabId) { + // console.log("tab id did not match after focus in 1000ms"); + // return; + // } + currentLoadingNoteId.current = item.id; + console.log("Waiting for tab to focus"); + return; + } + + state.current.movedAway = false; + state.current.currentlyEditing = true; + + if (!tabLocked) { + await loadContent(item); + } + + 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; + } + + if (!state.current.ready) { + currentNotes.current[item.id] = item; + currentLoadingNoteId.current = event.item?.id; + return; + } + + lastContentChangeTime.current[item.id] = item.dateEdited; currentNotes.current[item.id] = item; - return; + + if (!currentNotes.current[item.id]) return; + + editorSessionHistory.newSession(item.id); + + await commands.setStatus( + getFormattedDate(item.dateEdited, "date-time"), + "Saved", + tabId + ); + + await postMessage(NativeEvents.title, item.title, tabId); + overlay(false); + loadingState.current = currentContents.current[item.id]?.data; + + await postMessage( + NativeEvents.html, + currentContents.current[item.id]?.data || "", + tabId, + 10000 + ); + + setTimeout(() => { + if (blockIdRef.current) { + commands.scrollIntoViewById(blockIdRef.current); + blockIdRef.current = undefined; + } + }, 300); + + loadingState.current = undefined; + await commands.setTags(item); + commands.setSettings(); + setTimeout(() => { + if (currentLoadingNoteId.current === event.item?.id) { + currentLoadingNoteId.current = undefined; + } + }, 300); } - - lastContentChangeTime.current[item.id] = 0; - currentLoadingNoteId.current = item.id; - currentNotes.current[item.id] = item; - - if (!currentNotes.current[item.id]) return; - - editorSessionHistory.newSession(item.id); - - await commands.setStatus( - getFormattedDate(item.dateEdited, "date-time"), - strings.saved(), - tabId - ); - - await postMessage(EditorEvents.title, item.title, tabId); - overlay(false); - loadingState.current = currentContents.current[item.id]?.data; - - await postMessage( - EditorEvents.html, - currentContents.current[item.id]?.data || "", - tabId, - 10000 - ); - - setTimeout(() => { - if (blockIdRef.current) { - commands.scrollIntoViewById(blockIdRef.current); - blockIdRef.current = undefined; - } - }, 300); - - loadingState.current = undefined; - await commands.setTags(item); - commands.setSettings(); - setTimeout(() => { - if (currentLoadingNoteId.current === event.item?.id) { - currentLoadingNoteId.current = undefined; - } - }, 300); - } - postMessage(EditorEvents.theme, theme); + postMessage(NativeEvents.theme, theme); + }); }, [ commands, @@ -690,7 +681,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) { @@ -724,7 +715,7 @@ export const useEditor = ( } } else { await postMessage( - EditorEvents.updatehtml, + NativeEvents.updatehtml, decryptedContent.data, tabId ); @@ -736,7 +727,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; @@ -810,7 +801,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", @@ -905,7 +896,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 } 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 cf516dd55..d90ba527f 100644 --- a/apps/mobile/app/screens/editor/tiptap/use-tab-store.ts +++ b/apps/mobile/app/screens/editor/tiptap/use-tab-store.ts @@ -18,8 +18,12 @@ along with this program. If not, see . */ 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"; +import { TabHistory } from "@notesnook/common/dist/utils/tab-history"; class History { history: number[]; @@ -36,7 +40,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 +52,7 @@ class History { return removedItem; } useTabStore.setState({ - tabHistory: this.history.slice() + history: this.history.slice() }); return null; // Invalid index } @@ -59,7 +63,7 @@ class History { return restoredItem; } useTabStore.setState({ - tabHistory: this.history.slice() + history: this.history.slice() }); return null; // History is empty } @@ -99,9 +103,13 @@ export type TabStore = { focusEmptyTab: () => void; getCurrentNoteId: () => string | undefined; getTab: (tabId: number) => TabItem | undefined; - tabHistory: number[]; + history: number[]; biometryAvailable?: boolean; biometryEnrolled?: boolean; + tabHistory: Record; + canGoBack?: boolean; + canGoForward?: boolean; + sessionId?: string; }; function getId(id: number, tabs: TabItem[]): number { @@ -112,16 +120,73 @@ function getId(id: number, tabs: TabItem[]): number { return id; } -export function syncTabs() { +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({ - tabs: ${JSON.stringify(useTabStore.getState().tabs)}, - currentTab: ${useTabStore.getState().currentTab}, - biometryAvailable: ${useTabStore.getState().biometryAvailable}, - biometryEnrolled: ${useTabStore.getState().biometryEnrolled} - }); + globalThis.tabStore?.setState(${JSON.stringify(data)}); `); } +export const tabHistory = new TabHistory({ + get() { + return useTabStore.getState(); + }, + set(state) { + console.log(state, "saving tab history..."); + useTabStore.setState({ + ...state + }); + }, + getCurrentTab: () => useTabStore.getState().currentTab, + loadSession: async (sessionId: string) => { + const session = await editorController?.current?.commands.getSession( + sessionId + ); + console.log("LOADING SESSION FOR ID", sessionId, session); + if (session && session.noteId) { + const note = await db.notes.note(session.noteId); + if (note) { + eSendEvent(eOnLoadNote, { + item: note, + tabId: useTabStore.getState().currentTab + }); + return true; + } + return false; + } + return false; + }, + newSession: (sessionId, tabId, noteId) => { + editorController?.current?.commands?.newSession(sessionId, tabId, noteId); + }, + clearSessionsForTabId: (tabId: number) => { + editorController?.current?.commands?.deleteSessionsForTabId(tabId); + }, + getSession: async (sessionId: string) => { + return ( + (await editorController?.current?.commands.getSession(sessionId)) || + undefined + ); + }, + commit: () => syncTabs("history") +}); export const useTabStore = create( persist( @@ -131,14 +196,19 @@ export const useTabStore = create( id: 0 } ], - tabHistory: [0], - history: new History(), + tabHistory: {}, + history: [0], currentTab: 0, 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]; + + if (options.noteId) { + tabHistory.add(options.noteId); + } + tabs[index] = { ...tabs[index], ...options @@ -147,35 +217,14 @@ export const useTabStore = create( set({ tabs: tabs }); - syncTabs(); + syncTabs("tabs"); }, 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 - }); - const tabs = [...get().tabs]; - tabs[index] = { - ...tabs[index], - ...options, - previewTab: true, - noteId: noteId - }; - - set({ - tabs: tabs - }); - get().focusTab(tabs[index].id); - }, + ) => {}, 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(); @@ -186,6 +235,7 @@ export const useTabStore = create( id: 0 }); } + tabHistory.clearStackForTab(id); set({ tabs: nextTabs }); @@ -203,6 +253,11 @@ export const useTabStore = create( ...options } ]; + + if (options?.noteId) { + tabHistory.add(options.noteId); + } + set({ tabs: nextTabs }); @@ -220,7 +275,7 @@ export const useTabStore = create( set({ tabs: tabs }); - syncTabs(); + syncTabs("tabs"); }, focusTab: (id: number) => { @@ -228,6 +283,16 @@ export const useTabStore = create( set({ currentTab: id }); + set({ + canGoBack: tabHistory.canGoBack(), + canGoForward: tabHistory.canGoForward(), + sessionId: tabHistory.getCurrentSession() + }); + console.log( + tabHistory.canGoBack(), + tabHistory.canGoForward(), + tabHistory.getCurrentSession() + ); syncTabs(); }, getNoteIdForTab: (id: number) => { @@ -253,7 +318,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 4d27b18b8..c0cc4904a 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/package-lock.json b/apps/mobile/package-lock.json index c5d60cc23..ca38db2b1 100644 --- a/apps/mobile/package-lock.json +++ b/apps/mobile/package-lock.json @@ -34596,6 +34596,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, @@ -43365,6 +43373,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 6aae6c453..1cc7a66ef 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -54,4 +54,4 @@ "react": "18.2.0", "react-native": "0.74.5" } -} \ No newline at end of file +} diff --git a/packages/common/src/utils/editor-sessions.ts b/packages/common/src/utils/editor-sessions.ts new file mode 100644 index 000000000..d0f40be0c --- /dev/null +++ b/packages/common/src/utils/editor-sessions.ts @@ -0,0 +1,109 @@ +/* +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 . +*/ + +export type EditorSessionItem = { + tabId: number; + noteId: string; + scrollTop: number; + from: number; + to: number; + id: string; +}; + +export class EditorSessions extends Map { + timer: NodeJS.Timeout | null = null; + constructor( + public options: { + getGlobalNoteState: () => Record< + string, + { top: number; from: number; to: number } + >; + } + ) { + super(); + const savedSessions = localStorage.getItem("editor-sessions"); + if (savedSessions) { + const parsed = JSON.parse(savedSessions); + for (const [key, value] of Object.entries(parsed)) { + this.set(key, value as EditorSessionItem); + } + } + } + + save() { + if (this.timer) { + clearTimeout(this.timer); + } + this.timer = setTimeout(() => { + localStorage.setItem( + "editor-sessions", + JSON.stringify(Object.fromEntries(this.entries())) + ); + }, 1000); + } + + get(id: string): EditorSessionItem | undefined { + return super.get(id); + } + + set(id: string, session: EditorSessionItem): this { + super.set(id, session); + this.save(); + return this; + } + + delete(key: string): boolean { + super.delete(key); + this.save(); + return true; + } + + newSession( + sessionId: string, + tabId: number, + noteId: string + ): EditorSessionItem { + const session: EditorSessionItem = { + tabId, + noteId, + scrollTop: 0 || this.options.getGlobalNoteState()?.[noteId]?.top, + from: 0 || this.options.getGlobalNoteState()?.[noteId]?.from, + to: 0 || this.options.getGlobalNoteState()?.[noteId]?.to, + id: sessionId + }; + this.set(sessionId, session); + return session; + } + + updateSession(id: string, session: Partial): this { + const existing = this.get(id); + if (existing) { + this.set(id, { ...existing, ...session }); + } + return this; + } + + deleteForTabId(tabId: number) { + for (const [key, value] of this.entries()) { + if (value.tabId === tabId) { + this.delete(key); + } + } + } +} diff --git a/packages/common/src/utils/tab-history.ts b/packages/common/src/utils/tab-history.ts new file mode 100644 index 000000000..dcb6c5403 --- /dev/null +++ b/packages/common/src/utils/tab-history.ts @@ -0,0 +1,184 @@ +/* +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 { EditorSessionItem } from "./editor-sessions"; + +export type TabState = { + tabHistory: Record; + canGoBack?: boolean; + canGoForward?: boolean; + sessionId?: string; +}; + +export class TabHistory { + constructor( + public options: { + set: (state: TabState) => void; + get: () => TabState; + getCurrentTab: () => number; + loadSession: (sessionId: string) => Promise; + newSession: (sessionId: string, tabId: number, noteId: string) => void; + clearSessionsForTabId: (tabid: number) => void; + getSession: (sessionId: string) => Promise; + commit: () => void; + } + ) {} + + get back_stack() { + return ( + this.options + .get() + .tabHistory[this.options.getCurrentTab()]?.back_stack.slice() || [] + ); + } + + set back_stack(value: string[]) { + const currentTab = this.options.getCurrentTab(); + const tabHistory = this.options.get().tabHistory; + this.options.set({ + canGoBack: value.length > 1, + tabHistory: { + ...tabHistory, + [currentTab]: { + ...(tabHistory[currentTab] || {}), + back_stack: value + } + } + }); + this.options.commit(); + } + + get forward_stack() { + return ( + this.options + .get() + .tabHistory[this.options.getCurrentTab()]?.forward_stack.slice() || [] + ); + } + + set forward_stack(value: string[]) { + const currentTab = this.options.getCurrentTab(); + const tabHistory = this.options.get().tabHistory; + this.options.set({ + canGoForward: value.length > 1, + tabHistory: { + ...tabHistory, + [currentTab]: { + ...(tabHistory[currentTab] || {}), + forward_stack: value + } + } + }); + this.options.commit(); + } + + async add(noteId: string) { + const currentItemId = this.back_stack[this.back_stack.length - 1]; + const currentSession = currentItemId + ? await this.options.getSession(currentItemId) + : undefined; + + if (currentSession && currentSession.noteId === noteId) return; + const newSessionId = Math.random() + .toString(36) + .replace("0.", "es-" || ""); + + const back_stack = this.back_stack; + back_stack.push(newSessionId); + this.options.newSession(newSessionId, this.options.getCurrentTab(), noteId); + + this.back_stack = back_stack; + this.forward_stack = []; + } + + clearStackForTab(tabId: number) { + this.options.set({ + tabHistory: { + ...this.options.get().tabHistory, + [tabId]: { + back_stack: [], + forward_stack: [] + } + } + }); + this.options.clearSessionsForTabId(tabId); + } + + async back(): Promise { + 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]; + if (next_item) { + current_item && forward_stack.push(current_item); + + this.forward_stack = forward_stack; + this.back_stack = back_stack; + + if (await this.options.loadSession(next_item)) { + return next_item; + } else if (this.back_stack.length > 1) { + return this.back(); + } + } + return null; + } + + async forward(): Promise { + if (!this.canGoForward()) return null; + + const back_stack = this.back_stack; + const forward_stack = this.forward_stack; + + const item = forward_stack.pop(); + if (item) { + this.forward_stack = forward_stack; + if (await this.options.loadSession(item)) { + back_stack.push(item); + this.back_stack = back_stack; + return item; + } else if (this.forward_stack.length > 0) { + return this.forward(); + } + } + return null; + } + + getHistory() { + return { + back: this.back_stack, + forward: this.forward_stack + }; + } + + getCurrentSession() { + return this.back_stack[this.back_stack.length - 1]; + } + + 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 9abe0fbe2..b020a5220 100644 --- a/packages/editor-mobile/package-lock.json +++ b/packages/editor-mobile/package-lock.json @@ -14,6 +14,7 @@ "@lingui/react": "5.1.2", "@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", @@ -33,6 +34,25 @@ "react-scripts": "^5.0.1" } }, + "../common": { + "version": "1.0.0", + "license": "GPL-3.0-or-later", + "dependencies": { + "@notesnook/core": "file:../core", + "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", @@ -3766,6 +3786,10 @@ "node": ">= 8" } }, + "node_modules/@notesnook/common": { + "resolved": "../common", + "link": true + }, "node_modules/@notesnook/editor": { "resolved": "../editor", "link": true @@ -4459,7 +4483,7 @@ "version": "15.7.11", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", - "dev": true + "devOptional": true }, "node_modules/@types/q": { "version": "1.5.8", @@ -4483,7 +4507,7 @@ "version": "18.2.39", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz", "integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==", - "dev": true, + "devOptional": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -4518,7 +4542,7 @@ "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", - "dev": true + "devOptional": true }, "node_modules/@types/semver": { "version": "7.5.6", @@ -9780,7 +9804,7 @@ "version": "9.0.21", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", - "dev": true, + "devOptional": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -17874,6 +17898,20 @@ "is-typedarray": "^1.0.0" } }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", diff --git a/packages/editor-mobile/package.json b/packages/editor-mobile/package.json index 3b5e11b9e..cb90e1c06 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": "5.1.2", - "@lingui/react": "5.1.2" + "@lingui/react": "5.1.2", + "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 1e85f1096..41395d12f 100644 --- a/packages/editor-mobile/src/components/editor.tsx +++ b/packages/editor-mobile/src/components/editor.tsx @@ -36,6 +36,7 @@ import { useState } from "react"; import { useEditorController } from "../hooks/useEditorController"; +import { useSafeArea } from "../hooks/useSafeArea"; import { useSettings } from "../hooks/useSettings"; import { NoteState, @@ -44,7 +45,8 @@ import { useTabContext, useTabStore } from "../hooks/useTabStore"; -import { EventTypes, postAsyncWithTimeout, Settings } from "../utils"; +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"; @@ -82,6 +84,7 @@ const Tiptap = ({ undo, redo }); + const insets = useSafeArea(); tabRef.current = tab; valueRef.current = { undo, @@ -92,7 +95,7 @@ const Tiptap = ({ try { if (!tabRef.current.noteId) return; const noteState = - state || useTabStore.getState().noteState[tabRef.current.noteId]; + state || useTabStore.getState().getNoteState(tabRef.current.noteId); if (noteState && (noteState.to || noteState.from)) { const size = editors[tabRef.current.id]?.state.doc.content.size || 0; @@ -124,7 +127,7 @@ const Tiptap = ({ premium: settings.premium }, onPermissionDenied: () => { - post(EventTypes.pro, undefined, tabRef.current.id, tab.noteId); + post(EditorEvents.pro, undefined, tabRef.current.id, tab.noteId); } }); @@ -161,7 +164,7 @@ const Tiptap = ({ ) as Promise; }, createInternalLink(attributes) { - return postAsyncWithTimeout(EventTypes.createInternalLink, { + return postAsyncWithTimeout(EditorEvents.createInternalLink, { attributes }); }, @@ -305,12 +308,12 @@ const Tiptap = ({ if (isFocusedRef.current) return; if (state.currentTab === tabRef.current.id) { isFocusedRef.current = true; - const noteState = tabRef.current.noteId - ? state.noteState[tabRef.current.noteId] + const noteState = tabRef.current?.noteId + ? state.getNoteState(tabRef.current.noteId) : undefined; post( - EventTypes.tabFocused, + EditorEvents.tabFocused, !!globalThis.editorControllers[tabRef.current.id]?.content.current && !editorControllers[tabRef.current.id]?.loading, tabRef.current.id, @@ -438,7 +441,9 @@ const Tiptap = ({ display: isFocused ? "flex" : "none", flex: 1, flexDirection: "column", - maxWidth: "100vw" + maxWidth: "100vw", + position: "relative", + overflow: "hidden" }} ref={editorRoot} onDoubleClick={onClickEmptyArea} @@ -547,58 +552,83 @@ const Tiptap = ({ -
- {settings.noHeader || tab.locked ? null : ( - <> - - + {controller.loading || tab.locked ? ( + <div + style={{ + width: "100%", + height: "100%", + position: "absolute", + zIndex: 800, + backgroundColor: colors.primary.background, + display: "flex", + flexDirection: "column", + alignItems: tab.locked ? "center" : "flex-start", + justifyContent: tab.locked ? "center" : "flex-start", + boxSizing: "border-box", + rowGap: 10, + marginTop: `${50 + insets.top}px` + }} + > + {tab.locked ? ( + <div + style={{ + flexDirection: "column", + paddingLeft: 12, + paddingRight: 12, + width: "100%", + display: "flex", + justifyContent: "center", + alignItems: "center", + gap: 10 + }} + > + <p + style={{ + color: colors.primary.paragraph, + fontSize: 20, + fontWeight: "600", + textAlign: "center", + padding: "0px 20px", + marginBottom: 0, + userSelect: "none" + }} + > + {controller.title} + </p> + <p + style={{ + color: colors.primary.paragraph, + marginTop: 0, + marginBottom: 0, + userSelect: "none" + }} + > + This note is locked. + </p> - <StatusBar - container={containerRef} - loading={controller.loading} - /> - </> - )} - - {controller.loading || tab.locked ? ( - <div - style={{ - width: "100%", - height: "100%", - position: "absolute", - zIndex: 999, - backgroundColor: colors.primary.background, - paddingRight: 12, - paddingLeft: 12, - display: "flex", - flexDirection: "column", - alignItems: tab.locked ? "center" : "flex-start", - justifyContent: tab.locked ? "center" : "flex-start", - boxSizing: "border-box", - rowGap: 10 - }} - > - {tab.locked ? ( - <> - <p + <form + onSubmit={(e) => { + e.preventDefault(); + const data = new FormData(e.currentTarget); + const password = data.get("password"); + const biometrics = data.get("enrollBiometrics"); + post("editor-events:unlock", { + password, + biometrics: biometrics === "on" ? true : false + }); + }} + style={{ + display: "flex", + flexDirection: "column", + rowGap: 10 + }} + > + <input + placeholder="Enter password" + ref={controller.passwordInputRef} + name="password" + type="password" + required style={{ color: colors.primary.paragraph, fontSize: 20, @@ -771,10 +801,139 @@ const Tiptap = ({ width: "94%", backgroundColor: colors.secondary.background, borderRadius: 5, - marginTop: 10 + border: `1px solid ${colors.primary.border}`, + paddingLeft: 12, + paddingRight: 12, + fontSize: "1em", + backgroundColor: "transparent", + caretColor: colors.primary.accent, + color: colors.primary.paragraph }} /> + <button + style={{ + backgroundColor: colors.primary.accent, + borderRadius: 5, + boxSizing: "border-box", + border: "none", + color: colors.static.white, + width: 300, + fontSize: "0.9em", + height: 45, + display: "flex", + alignItems: "center", + justifyContent: "center" + }} + onMouseDown={(e) => { + if (globalThis.keyboardShown) { + e.preventDefault(); + } + }} + > + <p + style={{ + userSelect: "none" + }} + > + Unlock note + </p> + </button> + + {biometryAvailable && !biometryEnrolled ? ( + <div + style={{ + display: "flex", + alignItems: "center", + justifyContent: "center", + gap: 5 + }} + > + <input + type="checkbox" + name="enrollBiometrics" + style={{ + accentColor: colors.primary.accent + }} + onMouseDown={(e) => { + if (globalThis.keyboardShown) { + e.preventDefault(); + } + }} + /> + + <p + style={{ + color: colors.primary.paragraph, + marginTop: 0, + marginBottom: 0, + userSelect: "none" + }} + > + Enable biometric unlocking + </p> + </div> + ) : null} + </form> + + {biometryEnrolled && biometryAvailable ? ( + <button + style={{ + backgroundColor: "transparent", + borderRadius: 5, + boxSizing: "border-box", + border: "none", + color: colors.primary.accent, + width: 300, + fontSize: "0.9em", + height: 45, + display: "flex", + alignItems: "center", + justifyContent: "center", + columnGap: 5, + userSelect: "none" + }} + onMouseDown={(e) => { + if (globalThis.keyboardShown) { + e.preventDefault(); + } + }} + onClick={() => { + post("editor-events:unlock-biometrics"); + }} + > + <FingerprintIcon /> + <p + style={{ + userSelect: "none" + }} + > + Unlock with biometrics + </p> + </button> + ) : null} + </div> + ) : ( + <> + <Tags settings={settings} loading={controller.loading} /> + <div + style={{ + display: "flex", + flexDirection: "column", + paddingLeft: 12, + paddingRight: 12, + width: "100%", + gap: 10 + }} + > + <div + style={{ + height: 25, + width: "100%", + backgroundColor: colors.secondary.background, + borderRadius: 5 + }} + /> <div style={{ flexDirection: "row", @@ -842,10 +1001,42 @@ const Tiptap = ({ marginTop: 10 }} /> - </> - )} - </div> - ) : null} + </div> + </> + )} + </div> + ) : null} + + <div + onScroll={controller.scroll} + ref={containerRef} + style={{ + overflowY: controller.loading ? "hidden" : "scroll", + height: "100%", + display: "block", + position: "relative" + }} + > + {settings.noHeader || tab.locked ? null : ( + <> + <Tags settings={settings} loading={controller.loading} /> + <Title + titlePlaceholder={controller.titlePlaceholder} + readonly={settings.readonly} + controller={controllerRef} + title={controller.title} + fontFamily={settings.fontFamily} + dateFormat={settings.dateFormat} + timeFormat={settings.timeFormat} + loading={controller.loading} + /> + + <StatusBar + container={containerRef} + loading={controller.loading} + /> + </> + )} <div style={{ diff --git a/packages/editor-mobile/src/components/header.tsx b/packages/editor-mobile/src/components/header.tsx index 549e735db..0e1fc1ff6 100644 --- a/packages/editor-mobile/src/components/header.tsx +++ b/packages/editor-mobile/src/components/header.tsx @@ -19,6 +19,7 @@ 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"; @@ -30,7 +31,8 @@ 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 +102,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 +137,7 @@ function Header({ ) : ( <Button onPress={() => { - post(EventTypes.back, undefined, tab.id, tab.noteId); + post(EditorEvents.back, undefined, tab.id, tab.noteId); }} preventDefault={false} style={{ @@ -233,7 +239,7 @@ function Header({ {settings.deviceMode !== "mobile" && !settings.fullscreen ? ( <Button onPress={() => { - post(EventTypes.fullscreen, undefined, tab.id, tab.noteId); + post(EditorEvents.fullscreen, undefined, tab.id, tab.noteId); }} preventDefault={false} style={{ @@ -296,7 +302,67 @@ 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.noteId); }} preventDefault={false} style={{ @@ -341,7 +407,7 @@ function Header({ fwdRef={btnRef} onPress={() => { if (tab.locked) { - post(EventTypes.properties, undefined, tab.id, tab.noteId); + post(EditorEvents.properties, undefined, tab.id, tab.noteId); } else { setOpen(!isOpen); } @@ -395,7 +461,7 @@ function Header({ switch (e.value) { case "toc": post( - EventTypes.toc, + EditorEvents.toc, editorControllers[tab.id]?.getTableOfContents(), tab.id, tab.noteId @@ -406,7 +472,12 @@ function Header({ break; case "properties": logger("info", "post properties..."); - post(EventTypes.properties, undefined, tab.id, tab.noteId); + post( + EditorEvents.properties, + undefined, + tab.id, + tab.noteId + ); break; default: break; @@ -421,17 +492,85 @@ function Header({ alignItems: "center" }} > - <MagnifyIcon - size={22 * settings.fontScale} - color="var(--nn_primary_icon)" - /> - <span + <Button + onPress={() => { + post(EditorEvents.goBack, undefined, tab.id, tab.noteId); + setOpen(false); + }} style={{ color: "var(--nn_primary_paragraph)" }} > - {strings.search()} - </span> + <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.noteId); + setOpen(false); + }} + style={{ + borderWidth: 0, + borderRadius: 100, + color: "var(--nn_primary_icon)", + marginRight: 10, + 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)", + marginRight: 10, + 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> </MenuItem> <MenuItem diff --git a/packages/editor-mobile/src/components/readonly-editor.tsx b/packages/editor-mobile/src/components/readonly-editor.tsx index b1573c8ae..38ffa17a4 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 a232832de..e436c9303 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.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.noteId); }} > #{tag.alias} diff --git a/packages/editor-mobile/src/hooks/useEditorController.ts b/packages/editor-mobile/src/hooks/useEditorController.ts index dae40f622..8bd500d8b 100644 --- a/packages/editor-mobile/src/hooks/useEditorController.ts +++ b/packages/editor-mobile/src/hooks/useEditorController.ts @@ -32,7 +32,6 @@ import { useState } from "react"; import { - EventTypes, getRoot, isReactNative, post, @@ -40,6 +39,7 @@ 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"; @@ -158,7 +158,11 @@ export function useEditorController({ }); if (!tabRef.current.noteId && loading) { - setLoading(false); + setTimeout(() => { + if (!tabRef.current.noteId && loading) { + setLoading(false); + } + }, 3000); } const selectionChange = useCallback((_editor: Editor) => {}, []); @@ -167,7 +171,7 @@ export function useEditorController({ if (!isReactNative()) return; const currentSessionId = globalThis.sessionId; post( - EventTypes.contentchange, + EditorEvents.contentchange, undefined, tabRef.current.id, tabRef.current.noteId @@ -181,7 +185,7 @@ export function useEditorController({ currentSessionId ]; const pendingTitleIds = await pendingSaveRequests.getPendingTitleIds(); - postAsyncWithTimeout(EventTypes.title, ...params, 1000) + postAsyncWithTimeout(EditorEvents.title, ...params, 1000) .then(() => { if (pendingTitleIds.length) { dbLogger( @@ -230,7 +234,7 @@ export function useEditorController({ } const currentSessionId = globalThis.sessionId; post( - EventTypes.contentchange, + EditorEvents.contentchange, undefined, tabRef.current.id, tabRef.current.noteId @@ -239,21 +243,22 @@ export function useEditorController({ if (typeof timers.current.change === "number") { clearTimeout(timers.current?.change); } + + const params = [ + { + html: htmlContentRef.current, + ignoreEdit: ignoreEdit + }, + tabRef.current.id, + tabRef.current.noteId, + currentSessionId + ]; + timers.current.change = setTimeout(async () => { htmlContentRef.current = editor.getHTML(); - - const params = [ - { - html: htmlContentRef.current, - ignoreEdit: ignoreEdit - }, - tabRef.current.id, - tabRef.current.noteId, - currentSessionId - ]; const pendingContentIds = await pendingSaveRequests.getPendingContentIds(); - postAsyncWithTimeout(EventTypes.content, ...params, 5000) + postAsyncWithTimeout(EditorEvents.content, ...params, 5000) .then(() => { if (pendingContentIds.length) { dbLogger( @@ -284,12 +289,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); @@ -343,13 +343,13 @@ export function useEditorController({ switch (type) { case "native:updatehtml": { htmlContentRef.current = value; - logger("info", "UPDATING NOTE HTML"); + 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] + ? useTabStore.getState().getNoteState(tabRef.current?.noteId) : null; const top = scrollTop() || noteState?.top || 0; editor?.commands.setContent(htmlContentRef.current, false, { @@ -418,12 +418,17 @@ 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.noteId + ); }, []); const downloadAttachment = useCallback((attachment: Attachment) => { post( - EventTypes.download, + EditorEvents.download, attachment, tabRef.current.id, tabRef.current.noteId @@ -431,23 +436,23 @@ export function useEditorController({ }, []); const previewAttachment = useCallback((attachment: Attachment) => { post( - EventTypes.previewAttachment, + EditorEvents.previewAttachment, attachment, tabRef.current.id, tabRef.current.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.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 4d6834b73..aaaf05ec3 100644 --- a/packages/editor-mobile/src/hooks/useTabStore.ts +++ b/packages/editor-mobile/src/hooks/useTabStore.ts @@ -29,6 +29,9 @@ globalThis.statusBars = {}; export type TabItem = { id: number; noteId?: string; + /** + * @deprecated + */ previewTab?: boolean; readonly?: boolean; locked?: boolean; @@ -66,6 +69,10 @@ export type TabStore = { setNoteState: (noteId: string, state: Partial<NoteState>) => void; biometryAvailable?: boolean; biometryEnrolled?: boolean; + canGoBack?: boolean; + canGoForward?: boolean; + sessionId?: string; + getNoteState: (noteId: string) => NoteState | undefined; }; function getId(id: number, tabs: TabItem[]): number { @@ -80,10 +87,21 @@ export const useTabStore = create( persist<TabStore>( (set, get) => ({ noteState: {}, + getNoteState: (noteId: string) => { + const sessionId = get().sessionId; + const session = sessionId ? global.sessions.get(sessionId) : undefined; + if (session?.noteId === noteId) { + return { + top: session.scrollTop, + to: session.to, + from: session.from + }; + } + return undefined; + }, tabs: [ { - id: 0, - previewTab: true + id: 0 } ], currentTab: 0, @@ -91,6 +109,15 @@ export const useTabStore = create( setNoteState: (noteId: string, state: Partial<NoteState>) => { if (editorControllers[get().currentTab]?.loading) return; + const sessionId = get().sessionId; + if (sessionId) { + globalThis.sessions.updateSession(sessionId, { + from: state.from, + to: state.to, + scrollTop: state.top + }); + } + const noteState = { ...get().noteState }; @@ -127,21 +154,7 @@ export const useTabStore = create( 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 - }); - }, + focusPreviewTab: (noteId: string, options) => {}, focusEmptyTab: () => { const index = get().tabs.findIndex((t) => !t.noteId); if (index == -1) return get().newTab(); @@ -159,8 +172,7 @@ export const useTabStore = create( ...get().tabs, { id: id, - noteId, - previewTab: previewTab + noteId } ]; set({ diff --git a/apps/mobile/app/screens/editor/tiptap/editor-events.ts b/packages/editor-mobile/src/utils/editor-events.ts similarity index 92% rename from apps/mobile/app/screens/editor/tiptap/editor-events.ts rename to packages/editor-mobile/src/utils/editor-events.ts index 668c7d7cb..4c0845439 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,7 @@ 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" +} as const; diff --git a/packages/editor-mobile/src/utils/index.ts b/packages/editor-mobile/src/utils/index.ts index 8db81b8aa..992ec9051 100644 --- a/packages/editor-mobile/src/utils/index.ts +++ b/packages/editor-mobile/src/utils/index.ts @@ -21,6 +21,8 @@ import { Editor, ToolbarGroupDefinition } from "@notesnook/editor"; import { ThemeDefinition } from "@notesnook/theme"; import { Dispatch, MutableRefObject, RefObject, SetStateAction } from "react"; import { EditorController } from "../hooks/useEditorController"; +import { EditorSessions } from "@notesnook/common/dist/utils/editor-sessions"; +import { EditorEvents } from "./editor-events"; globalThis.sessionId = "notesnook-editor"; globalThis.pendingResolvers = {}; @@ -61,6 +63,7 @@ declare global { }; var readonlyEditor: boolean; + var sessions: EditorSessions; var statusBars: Record< number, | React.MutableRefObject<{ @@ -150,8 +153,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 +187,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 +209,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 +219,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 }); @@ -335,3 +300,10 @@ export function getTheme() { } return undefined; } + +const editorSessions = new EditorSessions({ + getGlobalNoteState: () => { + return globalThis.tabStore.getState().noteState; + } +}); +globalThis.sessions = editorSessions; 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 000000000..75bf67034 --- /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 2a415b944..aae55f6b5 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 + ); } };