diff --git a/apps/mobile/app/components/sheets/editor-tabs/index.tsx b/apps/mobile/app/components/sheets/editor-tabs/index.tsx index bc9692d4e2..7794bdc803 100644 --- a/apps/mobile/app/components/sheets/editor-tabs/index.tsx +++ b/apps/mobile/app/components/sheets/editor-tabs/index.tsx @@ -96,11 +96,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 306b506db5..bed6bcd4e6 100644 --- a/apps/mobile/app/components/sheets/link-note/index.tsx +++ b/apps/mobile/app/components/sheets/link-note/index.tsx @@ -22,23 +22,21 @@ import { VirtualizedGrouping, createInternalLink } from "@notesnook/core"; +import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events"; +import type { LinkAttributes } from "@notesnook/editor/dist/extensions/link"; 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/dist/extensions/link"; -import { - EditorEvents, - editorController -} from "../../../screens/editor/tiptap/utils"; const ListNoteItem = ({ id, @@ -193,7 +191,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/package.json b/apps/mobile/app/package.json index 15a1191c8b..931c068c36 100644 --- a/apps/mobile/app/package.json +++ b/apps/mobile/app/package.json @@ -41,7 +41,8 @@ "@trpc/react-query": "10.38.3", "@trpc/server": "10.38.3", "kysely": "^0.26.3", - "pathe": "1.1.2" + "pathe": "1.1.2", + "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 d5dfd99fea..1b3df03031 100755 --- a/apps/mobile/app/screens/editor/index.tsx +++ b/apps/mobile/app/screens/editor/index.tsx @@ -214,7 +214,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 7b290803e0..3d49b4ed9d 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"; @@ -68,11 +68,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 }) => { noteId.current = content.id; @@ -84,7 +84,7 @@ export function ReadonlyEditor(props: { ); setLoading(false); }); - } 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); @@ -103,7 +103,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: { 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/tiptap/commands.ts b/apps/mobile/app/screens/editor/tiptap/commands.ts index cf0ca480a0..958de43fe0 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 b562e3aa47..80c15e22c9 100644 --- a/apps/mobile/app/screens/editor/tiptap/use-editor-events.tsx +++ b/apps/mobile/app/screens/editor/tiptap/use-editor-events.tsx @@ -72,10 +72,11 @@ import { import { openLinkInBrowser } from "../../../utils/functions"; import { tabBarRef } from "../../../utils/global-refs"; import { useDragState } from "../../settings/editor/state"; -import { EventTypes } from "./editor-events"; +import { EditorEvents } from "@notesnook/editor-mobile/src/utils/editor-events"; +import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events"; import { EditorMessage, EditorProps, useEditorType } from "./types"; -import { useTabStore } from "./use-tab-store"; -import { EditorEvents, editorState, openInternalLink } from "./utils"; +import { tabHistory, useTabStore } from "./use-tab-store"; +import { editorState, openInternalLink } from "./utils"; const publishNote = async () => { const user = useUserStore.getState().user; @@ -178,7 +179,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); @@ -354,25 +355,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; } @@ -382,8 +383,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, @@ -393,8 +394,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, @@ -404,10 +405,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, @@ -420,12 +421,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: "Create a note first to add a reminder", @@ -443,7 +444,7 @@ export const useEditorEvents = ( onAdd: () => ReminderSheet.present(undefined, note, true) }); break; - case EventTypes.newtag: + case EditorEvents.newtag: if (!noteId) { ToastManager.show({ heading: "Create a note first to add a tag", @@ -453,7 +454,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); @@ -469,7 +470,7 @@ export const useEditorEvents = ( }); } break; - case EventTypes.filepicker: + case EditorEvents.filepicker: editorState().isAwaitingResult = true; const { pick } = require("./picker").default; pick({ @@ -481,14 +482,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; @@ -508,14 +509,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 }); @@ -524,26 +525,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( @@ -555,7 +556,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; @@ -566,11 +567,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 @@ -578,14 +579,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: ( ; @@ -140,7 +143,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; @@ -328,7 +325,7 @@ export const useEditor = ( if (!noteData.title) { postMessage( - EditorEvents.title, + NativeEvents.title, currentNotes.current[id]?.title, tabId ); @@ -382,8 +379,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; } @@ -425,7 +422,7 @@ export const useEditor = ( ); const loadNote = useCallback( - async (event: { + (event: { item?: Note; forced?: boolean; newNote?: boolean; @@ -433,175 +430,165 @@ export const useEditor = ( blockId?: string; presistTab?: boolean; }) => { - 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"); - } 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 - }); - } - } + + 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 (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 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 (!tabLocked) { - await loadContent(item); - } + let tabId = + event.tabId || useTabStore.getState().getTabForNote(event.item.id); + 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; - } + const isOpened = + useTabStore.getState().getTabForNote(event.item.id) === tabId; - if (!state.current.ready) { - currentNotes.current[item.id] = item; - return; - } + if (!isOpened) { + await commands.setLoading(true, tabId); + } - lastContentChangeTime.current[item.id] = item.dateEdited; - currentLoadingNoteId.current = item.id; - currentNotes.current[item.id] = item; + useTabStore.getState().updateTab(tabId, { + readonly: event.item.readonly || readonly, + ...(isOpened + ? { + noteId: event.item.id + } + : { + locked: tabLocked, + noteLocked: isLockedNote, + noteId: event.item.id + }) + }); - if (!currentNotes.current[item.id]) return; + useTabStore.getState().focusTab(tabId); - editorSessionHistory.newSession(item.id); + if (lastTabFocused.current !== tabId) { + useTabStore.getState().focusTab(tabId); + } - await commands.setStatus( - getFormattedDate(item.dateEdited, "date-time"), - "Saved", - 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; + } - await postMessage(EditorEvents.title, item.title, tabId); - overlay(false); - loadingState.current = currentContents.current[item.id]?.data; + state.current.movedAway = false; + state.current.currentlyEditing = true; - await postMessage( - EditorEvents.html, - currentContents.current[item.id]?.data || "", - tabId, - 10000 - ); + if (!tabLocked) { + await loadContent(item); + } - setTimeout(() => { - if (blockIdRef.current) { - commands.scrollIntoViewById(blockIdRef.current); - blockIdRef.current = undefined; + 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; } - }, 300); - loadingState.current = undefined; - await commands.setTags(item); - commands.setSettings(); - setTimeout(() => { - if (currentLoadingNoteId.current === event.item?.id) { - currentLoadingNoteId.current = undefined; + if (!state.current.ready) { + currentNotes.current[item.id] = item; + currentLoadingNoteId.current = event.item?.id; + return; } - }, 300); - } - postMessage(EditorEvents.theme, theme); + + lastContentChangeTime.current[item.id] = item.dateEdited; + currentNotes.current[item.id] = item; + + 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); + } + postMessage(NativeEvents.theme, theme); + }); }, [ commands, @@ -685,7 +672,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) { @@ -720,7 +707,7 @@ export const useEditor = ( } } else { await postMessage( - EditorEvents.updatehtml, + NativeEvents.updatehtml, decryptedContent.data, tabId ); @@ -732,7 +719,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; @@ -806,7 +793,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", @@ -897,7 +884,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 } @@ -909,6 +896,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..afb8b693f5 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 - }; - console.log("focus preview", 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 }); @@ -221,15 +276,24 @@ export const useTabStore = create( set({ tabs: tabs }); - syncTabs(); + syncTabs("tabs"); }, focusTab: (id: number) => { - console.log(history.getHistory(), id); history.add(id); 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) => { @@ -255,7 +319,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 a6b241e63c..7008355135 100644 --- a/apps/mobile/app/screens/editor/tiptap/utils.ts +++ b/apps/mobile/app/screens/editor/tiptap/utils.ts @@ -31,6 +31,8 @@ import { useTabStore } from "./use-tab-store"; import { parseInternalLink } from "@notesnook/core"; import { eOnLoadNote } from "../../../utils/events"; import { db } from "../../../common/database"; +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 53714b92a9..53edbc96dc 100644 --- a/apps/mobile/package-lock.json +++ b/apps/mobile/package-lock.json @@ -1,12 +1,12 @@ { "name": "@notesnook/mobile", - "version": "3.0.3", + "version": "3.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@notesnook/mobile", - "version": "3.0.3", + "version": "3.0.4", "hasInstallScript": true, "license": "GPL-3.0-or-later", "workspaces": [ @@ -21,6 +21,7 @@ "@notesnook/logger": "file:../../packages/logger", "@notesnook/theme": "file:../../packages/theme", "@notesnook/themes-server": "file:../../servers/themes", + "async-mutex": "^0.5.0", "diffblazer": "^1.0.1", "react": "18.2.0", "react-native": "0.72.0" @@ -7163,7 +7164,7 @@ }, "../../packages/editor-mobile/node_modules/@types/prop-types": { "version": "15.7.11", - "dev": true, + "devOptional": true, "license": "MIT" }, "../../packages/editor-mobile/node_modules/@types/q": { @@ -7183,7 +7184,7 @@ }, "../../packages/editor-mobile/node_modules/@types/react": { "version": "18.2.39", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -7214,7 +7215,7 @@ }, "../../packages/editor-mobile/node_modules/@types/scheduler": { "version": "0.16.8", - "dev": true, + "devOptional": true, "license": "MIT" }, "../../packages/editor-mobile/node_modules/@types/semver": { @@ -12072,7 +12073,7 @@ }, "../../packages/editor-mobile/node_modules/immer": { "version": "9.0.21", - "dev": true, + "devOptional": true, "license": "MIT", "funding": { "type": "opencollective", @@ -22461,7 +22462,6 @@ }, "../../packages/editor/node_modules/js-tokens": { "version": "4.0.0", - "dev": true, "license": "MIT" }, "../../packages/editor/node_modules/json-parse-even-better-errors": { @@ -22517,7 +22517,6 @@ }, "../../packages/editor/node_modules/loose-envify": { "version": "1.4.0", - "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -23039,7 +23038,6 @@ }, "../../packages/editor/node_modules/react": { "version": "18.2.0", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -23058,7 +23056,6 @@ }, "../../packages/editor/node_modules/react-dom": { "version": "18.2.0", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", @@ -23178,7 +23175,6 @@ }, "../../packages/editor/node_modules/scheduler": { "version": "0.23.0", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -30896,7 +30892,6 @@ }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -30910,7 +30905,6 @@ }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -31057,7 +31051,6 @@ }, "node_modules/@babel/plugin-proposal-private-property-in-object": { "version": "7.21.0-placeholder-for-preset-env.2", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -31068,7 +31061,6 @@ }, "node_modules/@babel/plugin-proposal-unicode-property-regex": { "version": "7.18.6", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", @@ -31114,7 +31106,6 @@ }, "node_modules/@babel/plugin-syntax-class-static-block": { "version": "7.14.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" @@ -31151,7 +31142,6 @@ }, "node_modules/@babel/plugin-syntax-export-namespace-from": { "version": "7.8.3", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.3" @@ -31175,7 +31165,6 @@ }, "node_modules/@babel/plugin-syntax-import-assertions": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -31189,7 +31178,6 @@ }, "node_modules/@babel/plugin-syntax-import-attributes": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -31203,7 +31191,6 @@ }, "node_modules/@babel/plugin-syntax-import-meta": { "version": "7.10.4", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" @@ -31214,7 +31201,6 @@ }, "node_modules/@babel/plugin-syntax-json-strings": { "version": "7.8.3", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -31238,7 +31224,6 @@ }, "node_modules/@babel/plugin-syntax-logical-assignment-operators": { "version": "7.10.4", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" @@ -31299,7 +31284,6 @@ }, "node_modules/@babel/plugin-syntax-private-property-in-object": { "version": "7.14.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" @@ -31313,7 +31297,6 @@ }, "node_modules/@babel/plugin-syntax-top-level-await": { "version": "7.14.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" @@ -31340,7 +31323,6 @@ }, "node_modules/@babel/plugin-syntax-unicode-sets-regex": { "version": "7.18.6", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", @@ -31368,7 +31350,6 @@ }, "node_modules/@babel/plugin-transform-async-generator-functions": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-environment-visitor": "^7.22.5", @@ -31426,7 +31407,6 @@ }, "node_modules/@babel/plugin-transform-class-properties": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.22.5", @@ -31441,7 +31421,6 @@ }, "node_modules/@babel/plugin-transform-class-static-block": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.22.5", @@ -31505,7 +31484,6 @@ }, "node_modules/@babel/plugin-transform-dotall-regex": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.5", @@ -31520,7 +31498,6 @@ }, "node_modules/@babel/plugin-transform-duplicate-keys": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -31534,7 +31511,6 @@ }, "node_modules/@babel/plugin-transform-dynamic-import": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -31563,7 +31539,6 @@ }, "node_modules/@babel/plugin-transform-export-namespace-from": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -31620,7 +31595,6 @@ }, "node_modules/@babel/plugin-transform-json-strings": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -31648,7 +31622,6 @@ }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -31676,7 +31649,6 @@ }, "node_modules/@babel/plugin-transform-modules-amd": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.22.5", @@ -31706,7 +31678,6 @@ }, "node_modules/@babel/plugin-transform-modules-systemjs": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-hoist-variables": "^7.22.5", @@ -31723,7 +31694,6 @@ }, "node_modules/@babel/plugin-transform-modules-umd": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.22.5", @@ -31752,7 +31722,6 @@ }, "node_modules/@babel/plugin-transform-new-target": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -31766,7 +31735,6 @@ }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -31781,7 +31749,6 @@ }, "node_modules/@babel/plugin-transform-numeric-separator": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -31809,7 +31776,6 @@ }, "node_modules/@babel/plugin-transform-object-rest-spread": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.22.5", @@ -31841,7 +31807,6 @@ }, "node_modules/@babel/plugin-transform-optional-catch-binding": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -31856,7 +31821,6 @@ }, "node_modules/@babel/plugin-transform-optional-chaining": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -31885,7 +31849,6 @@ }, "node_modules/@babel/plugin-transform-private-methods": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.22.5", @@ -31900,7 +31863,6 @@ }, "node_modules/@babel/plugin-transform-private-property-in-object": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", @@ -32000,7 +31962,6 @@ }, "node_modules/@babel/plugin-transform-reserved-words": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -32092,7 +32053,6 @@ }, "node_modules/@babel/plugin-transform-typeof-symbol": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -32122,7 +32082,6 @@ }, "node_modules/@babel/plugin-transform-unicode-escapes": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -32136,7 +32095,6 @@ }, "node_modules/@babel/plugin-transform-unicode-property-regex": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.5", @@ -32165,7 +32123,6 @@ }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.5", @@ -32180,7 +32137,6 @@ }, "node_modules/@babel/preset-env": { "version": "7.22.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.22.5", @@ -32273,7 +32229,6 @@ }, "node_modules/@babel/preset-env/node_modules/semver": { "version": "6.3.0", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -32296,7 +32251,6 @@ }, "node_modules/@babel/preset-modules": { "version": "0.1.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", @@ -34946,7 +34900,6 @@ }, "node_modules/@types/eslint": { "version": "8.40.2", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "*", @@ -34955,7 +34908,6 @@ }, "node_modules/@types/eslint-scope": { "version": "3.7.4", - "dev": true, "license": "MIT", "dependencies": { "@types/eslint": "*", @@ -34964,7 +34916,6 @@ }, "node_modules/@types/estree": { "version": "1.0.1", - "dev": true, "license": "MIT" }, "node_modules/@types/graceful-fs": { @@ -35413,7 +35364,6 @@ }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/helper-numbers": "1.11.6", @@ -35422,22 +35372,18 @@ }, "node_modules/@webassemblyjs/floating-point-hex-parser": { "version": "1.11.6", - "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.11.6", - "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.11.6", - "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.11.6", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.11.6", @@ -35447,12 +35393,10 @@ }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { "version": "1.11.6", - "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.11.6", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.11.6", @@ -35463,7 +35407,6 @@ }, "node_modules/@webassemblyjs/ieee754": { "version": "1.11.6", - "dev": true, "license": "MIT", "dependencies": { "@xtuc/ieee754": "^1.2.0" @@ -35471,7 +35414,6 @@ }, "node_modules/@webassemblyjs/leb128": { "version": "1.11.6", - "dev": true, "license": "Apache-2.0", "dependencies": { "@xtuc/long": "4.2.2" @@ -35479,12 +35421,10 @@ }, "node_modules/@webassemblyjs/utf8": { "version": "1.11.6", - "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.11.6", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.11.6", @@ -35499,7 +35439,6 @@ }, "node_modules/@webassemblyjs/wasm-gen": { "version": "1.11.6", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.11.6", @@ -35511,7 +35450,6 @@ }, "node_modules/@webassemblyjs/wasm-opt": { "version": "1.11.6", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.11.6", @@ -35522,7 +35460,6 @@ }, "node_modules/@webassemblyjs/wasm-parser": { "version": "1.11.6", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.11.6", @@ -35535,7 +35472,6 @@ }, "node_modules/@webassemblyjs/wast-printer": { "version": "1.11.6", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.11.6", @@ -35593,12 +35529,10 @@ }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/@xtuc/long": { "version": "4.2.2", - "dev": true, "license": "Apache-2.0" }, "node_modules/@yarnpkg/lockfile": { @@ -35654,7 +35588,6 @@ }, "node_modules/acorn-import-assertions": { "version": "1.9.0", - "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^8" @@ -35991,6 +35924,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, @@ -36759,7 +36700,6 @@ }, "node_modules/chrome-trace-event": { "version": "1.0.3", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0" @@ -37860,7 +37800,6 @@ }, "node_modules/enhanced-resolve": { "version": "5.15.0", - "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -37973,7 +37912,6 @@ }, "node_modules/es-module-lexer": { "version": "1.3.0", - "dev": true, "license": "MIT" }, "node_modules/es-set-tostringtag": { @@ -38304,7 +38242,6 @@ }, "node_modules/eslint-scope": { "version": "5.1.1", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", @@ -38316,7 +38253,6 @@ }, "node_modules/eslint-scope/node_modules/estraverse": { "version": "4.3.0", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -38502,7 +38438,6 @@ }, "node_modules/esrecurse": { "version": "4.3.0", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" @@ -38513,7 +38448,6 @@ }, "node_modules/estraverse": { "version": "5.3.0", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -38521,7 +38455,6 @@ }, "node_modules/esutils": { "version": "2.0.3", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -39578,7 +39511,6 @@ }, "node_modules/glob-to-regexp": { "version": "0.4.1", - "dev": true, "license": "BSD-2-Clause" }, "node_modules/global": { @@ -41822,7 +41754,6 @@ }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", - "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { @@ -42090,7 +42021,6 @@ }, "node_modules/loader-runner": { "version": "4.3.0", - "dev": true, "license": "MIT", "engines": { "node": ">=6.11.5" @@ -44742,7 +44672,6 @@ }, "node_modules/randombytes": { "version": "2.1.0", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" @@ -45514,7 +45443,6 @@ }, "node_modules/react-test-renderer": { "version": "18.2.0", - "dev": true, "license": "MIT", "dependencies": { "react-is": "^18.2.0", @@ -45527,12 +45455,10 @@ }, "node_modules/react-test-renderer/node_modules/react-is": { "version": "18.2.0", - "dev": true, "license": "MIT" }, "node_modules/react-test-renderer/node_modules/scheduler": { "version": "0.23.0", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -46368,7 +46294,6 @@ }, "node_modules/serialize-javascript": { "version": "6.0.1", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" @@ -47424,7 +47349,6 @@ }, "node_modules/terser-webpack-plugin": { "version": "5.3.9", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.17", @@ -47457,7 +47381,6 @@ }, "node_modules/terser-webpack-plugin/node_modules/jest-worker": { "version": "27.5.1", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -47470,7 +47393,6 @@ }, "node_modules/terser-webpack-plugin/node_modules/supports-color": { "version": "8.1.1", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -48215,7 +48137,6 @@ }, "node_modules/watchpack": { "version": "2.4.0", - "dev": true, "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", @@ -48238,7 +48159,6 @@ }, "node_modules/webpack": { "version": "5.88.2", - "dev": true, "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.3", @@ -48353,7 +48273,6 @@ }, "node_modules/webpack-sources": { "version": "3.2.3", - "dev": true, "license": "MIT", "engines": { "node": ">=10.13.0" diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 486fc8a0c4..c516f86ec9 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -41,4 +41,4 @@ "react": "18.2.0", "react-native": "0.72.0" } -} \ 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 0000000000..d0f40be0ce --- /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 0000000000..dcb6c54035 --- /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 0de25ff68d..67335c4445 100644 --- a/packages/editor-mobile/package-lock.json +++ b/packages/editor-mobile/package-lock.json @@ -12,6 +12,7 @@ "@emotion/react": "11.11.1", "@mdi/js": "^7.2.96", "@mdi/react": "^1.6.0", + "@notesnook/common": "file:../common", "@notesnook/editor": "file:../editor", "@notesnook/theme": "file:../theme", "@szhsin/react-menu": "^4.1.0", @@ -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": "1.6.1", @@ -3687,6 +3707,10 @@ "node": ">= 8" } }, + "node_modules/@notesnook/common": { + "resolved": "../common", + "link": true + }, "node_modules/@notesnook/editor": { "resolved": "../editor", "link": true @@ -4376,7 +4400,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", @@ -4400,7 +4424,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": "*", @@ -4435,7 +4459,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", @@ -9736,7 +9760,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" @@ -17822,6 +17846,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 078f3946c2..69c6b8459c 100644 --- a/packages/editor-mobile/package.json +++ b/packages/editor-mobile/package.json @@ -17,7 +17,8 @@ "react-dom": "^18.2.0", "react-freeze": "^1.0.3", "tinycolor2": "1.6.0", - "zustand": "^4.4.7" + "zustand": "^4.4.7", + "@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 86422cbd0d..c83aff0e96 100644 --- a/packages/editor-mobile/src/components/editor.tsx +++ b/packages/editor-mobile/src/components/editor.tsx @@ -38,6 +38,7 @@ import { useState } from "react"; import { useEditorController } from "../hooks/useEditorController"; +import { useSafeArea } from "../hooks/useSafeArea"; import { useSettings } from "../hooks/useSettings"; import { NoteState, @@ -47,12 +48,13 @@ import { useTabStore } from "../hooks/useTabStore"; import { EmotionEditorToolbarTheme } from "../theme-factory"; -import { EventTypes, postAsyncWithTimeout, randId, 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"; import Tags from "./tags"; import Title from "./title"; -import { pendingSaveRequests } from "../utils/pending-saves"; globalThis.toBlobURL = toBlobURL as typeof globalThis.toBlobURL; @@ -77,13 +79,14 @@ const Tiptap = ({ const biometryEnrolled = useTabStore((state) => state.biometryEnrolled); const editorRoot = useRef(null); const isFocusedRef = useRef(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]; + state || useTabStore.getState().getNoteState(tabRef.current.noteId); if (noteState && (noteState.to || noteState.from)) { const size = editors[tabRef.current.id]?.state.doc.content.size || 0; @@ -115,7 +118,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); } }); @@ -146,7 +149,7 @@ const Tiptap = ({ ) as Promise; }, createInternalLink(attributes) { - return postAsyncWithTimeout(EventTypes.createInternalLink, { + return postAsyncWithTimeout(EditorEvents.createInternalLink, { attributes }); }, @@ -292,12 +295,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, @@ -421,7 +424,9 @@ const Tiptap = ({ display: isFocused ? "flex" : "none", flex: 1, flexDirection: "column", - maxWidth: "100vw" + maxWidth: "100vw", + position: "relative", + overflow: "hidden" }} ref={editorRoot} onDoubleClick={onClickEmptyArea} @@ -533,233 +538,221 @@ const Tiptap = ({ -
- {settings.noHeader || tab.locked ? null : ( - <> - - - - <StatusBar - container={containerRef} - loading={controller.loading} - /> - </> - )} + {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> - {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 - style={{ - color: colors.primary.paragraph, - fontSize: 20, - fontWeight: "600", - textAlign: "center", - padding: "0px 20px", - marginBottom: 0, - userSelect: "none" - }} - > - {controller.title} - </p> - <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, - marginTop: 0, - marginBottom: 0, - userSelect: "none" - }} - > - This note is 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 - }); + boxSizing: "border-box", + width: 300, + height: 45, + borderRadius: 5, + 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", - flexDirection: "column", - rowGap: 10 + alignItems: "center", + justifyContent: "center" + }} + onMouseDown={(e) => { + if (globalThis.keyboardShown) { + e.preventDefault(); + } }} > - <input - placeholder="Enter password" - ref={controller.passwordInputRef} - name="password" - type="password" - required + <p style={{ - boxSizing: "border-box", - width: 300, - height: 45, - borderRadius: 5, - border: `1px solid ${colors.primary.border}`, - paddingLeft: 12, - paddingRight: 12, - fontSize: "1em", - backgroundColor: "transparent", - caretColor: colors.primary.accent, - color: colors.primary.paragraph + userSelect: "none" }} - /> + > + Unlock note + </p> + </button> - <button + {biometryAvailable && !biometryEnrolled ? ( + <div 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(); - } + 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" }} > - Unlock note + Enable biometric unlocking </p> - </button> + </div> + ) : null} + </form> - {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 + {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={{ - 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} - </> - ) : ( - <> + 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: "94%", + width: "100%", backgroundColor: colors.secondary.background, - borderRadius: 5, - marginTop: 10 + borderRadius: 5 }} /> - <div style={{ flexDirection: "row", @@ -827,10 +820,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 87cdaffa74..94525669e6 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 CrownIcon from "mdi-react/CrownIcon"; @@ -31,7 +32,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"; const menuClassName = ({ state }: any) => @@ -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={{ @@ -168,7 +174,7 @@ function Header({ {!settings.premium && ( <Button onPress={() => { - post(EventTypes.pro); + post(EditorEvents.pro); }} preventDefault={false} style={{ @@ -197,7 +203,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={{ @@ -260,7 +266,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={{ @@ -345,7 +411,7 @@ function Header({ switch (e.value) { case "toc": post( - EventTypes.toc, + EditorEvents.toc, editorControllers[tab.id]?.getTableOfContents(), tab.id, tab.noteId @@ -353,7 +419,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; @@ -371,7 +442,8 @@ function Header({ > <Button onPress={() => { - editor?.commands.undo(); + post(EditorEvents.goBack, undefined, tab.id, tab.noteId); + setOpen(false); }} style={{ borderWidth: 0, @@ -386,9 +458,9 @@ function Header({ position: "relative" }} > - <ArrowULeftTopIcon + <ArrowBackIcon color={ - !hasUndo + !canGoBack ? "var(--nn_secondary_border)" : "var(--nn_primary_icon)" } @@ -401,7 +473,8 @@ function Header({ <Button onPress={() => { - editor?.commands.redo(); + post(EditorEvents.goForward, undefined, tab.id, tab.noteId); + setOpen(false); }} style={{ borderWidth: 0, @@ -416,9 +489,9 @@ function Header({ position: "relative" }} > - <ArrowURightTopIcon + <ArrowForwardIcon color={ - !hasRedo + !canGoForward ? "var(--nn_secondary_border)" : "var(--nn_primary_icon)" } @@ -432,6 +505,7 @@ function Header({ <Button onPress={() => { editor?.commands.startSearch(); + setOpen(false); }} style={{ borderWidth: 0, 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 6c3c37e47e..cd368e8143 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"; @@ -44,7 +45,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; @@ -125,7 +126,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 0d7a99f284..7f27b8eac3 100644 --- a/packages/editor-mobile/src/hooks/useEditorController.ts +++ b/packages/editor-mobile/src/hooks/useEditorController.ts @@ -30,13 +30,8 @@ import { useRef, useState } from "react"; -import { - EventTypes, - getRoot, - post, - postAsyncWithTimeout, - saveTheme -} from "../utils"; +import { getRoot, post, postAsyncWithTimeout, saveTheme } from "../utils"; +import { EditorEvents } from "../utils/editor-events"; import { injectCss, transform } from "../utils/css"; import { pendingSaveRequests } from "../utils/pending-saves"; import { useTabContext, useTabStore } from "./useTabStore"; @@ -150,7 +145,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) => {}, []); @@ -158,7 +157,7 @@ export function useEditorController({ const titleChange = useCallback(async (title: string) => { const currentSessionId = globalThis.sessionId; post( - EventTypes.contentchange, + EditorEvents.contentchange, undefined, tabRef.current.id, tabRef.current.noteId @@ -172,7 +171,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( @@ -220,7 +219,7 @@ export function useEditorController({ } const currentSessionId = globalThis.sessionId; post( - EventTypes.contentchange, + EditorEvents.contentchange, undefined, tabRef.current.id, tabRef.current.noteId @@ -229,21 +228,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( @@ -274,12 +274,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); @@ -333,14 +328,14 @@ 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; editor?.commands.setContent(htmlContentRef.current, false, { @@ -410,12 +405,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 @@ -423,23 +423,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 4d6834b73f..aaaf05ec34 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 668c7d7cb2..4c08454392 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 b0f4f58fd4..8520a7b692 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 = {}; @@ -58,6 +60,7 @@ declare global { [key: string]: (value: any) => void; }; var readonlyEditor: boolean; + var sessions: EditorSessions; var statusBars: Record< number, | React.MutableRefObject<{ @@ -145,8 +148,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, @@ -179,44 +182,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) @@ -239,7 +204,7 @@ export function logger( }) .join(" "); - post(EventTypes.logger, `[${type}]: ` + logString); + post(EditorEvents.logger, `[${type}]: ` + logString); } export function dbLogger(type: "error" | "log", ...logs: unknown[]): void { @@ -249,7 +214,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 }); @@ -330,3 +295,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 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 + ); } };