From e289caf5618dad74563acccb3f7b99b73cbade2a Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Mon, 11 Mar 2024 10:56:17 +0500 Subject: [PATCH] web: define editor & session once and pass it everywhere --- apps/web/src/components/editor/index.tsx | 140 ++++++++---------- .../components/editor/table-of-contents.tsx | 20 +-- apps/web/src/components/properties/index.tsx | 51 +++---- 3 files changed, 102 insertions(+), 109 deletions(-) diff --git a/apps/web/src/components/editor/index.tsx b/apps/web/src/components/editor/index.tsx index d20dd1143..6c4a5f0c2 100644 --- a/apps/web/src/components/editor/index.tsx +++ b/apps/web/src/components/editor/index.tsx @@ -27,7 +27,15 @@ import React, { import ReactDOM from "react-dom"; import { Box, Flex, Progress, Text } from "@theme-ui/components"; import Properties from "../properties"; -import { useEditorStore, SaveState } from "../../stores/editor-store"; +import { + useEditorStore, + SaveState, + DefaultEditorSession, + DeletedEditorSession, + NewEditorSession, + ReadonlyEditorSession, + EditorSession +} from "../../stores/editor-store"; import { useStore as useAppStore, store as appstore @@ -65,6 +73,7 @@ import DiffViewer from "../diff-viewer"; import TableOfContents from "./table-of-contents"; import { showNoteLinkingDialog } from "../../common/dialog-controller"; import { scrollIntoViewById } from "@notesnook/editor"; +import { IEditor } from "./types"; const PDFPreview = React.lazy(() => import("../pdf-preview")); @@ -122,7 +131,7 @@ export default function TabsView() { ) : session.type === "conflicted" || session.type === "diff" ? ( ) : ( - + )} ))} @@ -132,10 +141,20 @@ export default function TabsView() { const MemoizedEditorView = React.memo( EditorView, - (prev, next) => prev.id === next.id + (prev, next) => + prev.session.id === next.session.id && + prev.session.type === next.session.type ); -function EditorView({ id }: { id: string }) { - const lastSavedTime = useRef(Date.now()); +function EditorView({ + session +}: { + session: + | DefaultEditorSession + | NewEditorSession + | ReadonlyEditorSession + | DeletedEditorSession; +}) { + const lastChangedTime = useRef(Date.now()); const [docPreview, setDocPreview] = useState(); const previewSession = useRef(); @@ -150,7 +169,7 @@ function EditorView({ id }: { id: string }) { (store) => store.getSession(id, ["default", "readonly"])?.note?.readonly ); const isFocusMode = useAppStore((store) => store.isFocusMode); - const isPreviewSession = !!previewSession.current; + const editor = useEditorManager((store) => store.editors[session.id]?.editor); const isMobile = useMobile(); const isTablet = useTablet(); @@ -159,25 +178,21 @@ function EditorView({ id }: { id: string }) { const event = db.eventManager.subscribe( EVENTS.syncItemMerged, async (item?: MaybeDeletedItem) => { - const session = useEditorStore - .getState() - .getSession(id, ["unlocked", "default"]); - const editor = useEditorManager.getState().getEditor(id)?.editor; if ( + session.type === "new" || !editor || - !session?.note || + !session.note || !item || isDeleted(item) || (item.type !== "tiptap" && item.type !== "note") || - lastSavedTime.current >= (item.dateEdited as number) || - isPreviewSession || + lastChangedTime.current >= (item.dateEdited as number) || !appstore.get().isRealtimeSyncEnabled ) return; const { contentId, locked } = session.note; const isContent = item.type === "tiptap" && item.id === contentId; - const isNote = item.type === "note" && item.id === id; + const isNote = item.type === "note" && item.id === session.note.id; if (id && isContent) { let content: string | null = null; @@ -194,7 +209,7 @@ function EditorView({ id }: { id: string }) { useEditorStore .getState() - .updateSession(id, ["default"], { note: item }); + .updateSession(session.id, [session.type], { note: item }); if (item.title) AppEventManager.publish(AppEvents.changeNoteTitle, { title: item.title, @@ -206,30 +221,7 @@ function EditorView({ id }: { id: string }) { return () => { event.unsubscribe(); }; - }, [id, isPreviewSession]); - - // const openSession = useCallback(async (noteId: string) => { - // await useEditorStore.getState().openSession(noteId); - // previewSession.current = undefined; - - // lastSavedTime.current = Date.now(); - // setTimestamp(Date.now()); - // }, []); - - // useEffect(() => { - // if (!isNewSession) return; - - // editorstore.newSession(); - - // lastSavedTime.current = 0; - // setTimestamp(Date.now()); - // }, [isNewSession, nonce]); - - // useEffect(() => { - // if (!isOldSession || typeof noteId === "number") return; - - // openSession(noteId); - // }, [noteId]); + }, [editor, session]); return ( @@ -261,26 +253,24 @@ function EditorView({ id }: { id: string }) { /> )} */} - previewSession.current?.content.data || - useEditorStore.getState().getSession(id, ["default"])?.content - ?.data - } + content={() => session.content?.data} + editor={editor} + session={session} onPreviewDocument={(url) => setDocPreview(url)} - onContentChange={() => (lastSavedTime.current = Date.now())} + onContentChange={() => (lastChangedTime.current = Date.now())} onSave={(content, ignoreEdit) => { deferredSave(session.id, session.id, ignoreEdit, data); }} options={{ - readonly: isReadonly || isPreviewSession, + readonly: session?.type === "readonly", onRequestFocus: () => toggleProperties(false), focusMode: isFocusMode, isMobile: isMobile || isTablet }} /> - + {editor && } {docPreview && ( @@ -319,8 +309,12 @@ function EditorView({ id }: { id: string }) { )} - {arePropertiesVisible && } - {isTOCVisible && } + {session.type !== "new" && ( + <> + {arePropertiesVisible && } + {isTOCVisible && } + + )} ); } @@ -375,7 +369,9 @@ type EditorOptions = { }; type EditorProps = { id: string; + session: EditorSession; content: () => string | undefined; + editor?: IEditor; nonce?: number; options?: EditorOptions; onContentChange?: () => void; @@ -385,6 +381,8 @@ type EditorProps = { export function Editor(props: EditorProps) { const { id, + editor, + session, content, onSave, nonce, @@ -399,13 +397,12 @@ export function Editor(props: EditorProps) { isMobile: false }; const [isLoading, setIsLoading] = useState(true); - useScrollToBlock(id); + useScrollToBlock(session); useEffect(() => { const event = AppEventManager.subscribe( AppEvents.UPDATE_ATTACHMENT_PROGRESS, ({ hash, loaded, total }: AttachmentProgress) => { - const editor = useEditorManager.getState().getEditor(id)?.editor; editor?.sendAttachmentProgress( hash, Math.round((loaded / total) * 100) @@ -416,7 +413,7 @@ export function Editor(props: EditorProps) { return () => { event.unsubscribe(); }; - }, []); + }, [editor]); return ( @@ -430,8 +427,8 @@ export function Editor(props: EditorProps) { corsHost: Config.get("corsProxy", "https://cors.notesnook.com") }} onLoad={() => { - restoreSelection(id); - restoreScrollPosition(id); + if (editor) restoreSelection(editor, id); + restoreScrollPosition(session); setIsLoading(false); }} onSelectionChange={({ from, to }) => @@ -474,7 +471,6 @@ export function Editor(props: EditorProps) { } }} onInsertAttachment={async (type) => { - const editor = useEditorManager.getState().getEditor(id)?.editor; const mime = type === "file" ? "*/*" : "image/*"; const attachments = await insertAttachments(mime); if (!attachments) return; @@ -490,7 +486,6 @@ export function Editor(props: EditorProps) { ); }} onAttachFiles={async (files) => { - const editor = useEditorManager.getState().getEditor(id)?.editor; const result = await attachFiles(files); if (!result) return; result.forEach((attachment) => editor?.attachFile(attachment)); @@ -707,11 +702,11 @@ function EditorChrome( // } type DropZoneProps = { - id: string; overlayRef: React.MutableRefObject; + editor: IEditor; }; function DropZone(props: DropZoneProps) { - const { overlayRef, id } = props; + const { overlayRef, editor } = props; return ( { - const editor = useEditorManager.getState().getEditor(id)?.editor; - if (!editor || !e.dataTransfer.files?.length) return; + if (!e.dataTransfer.files?.length) return; e.preventDefault(); const attachments = await attachFiles(Array.from(e.dataTransfer.files)); @@ -801,19 +795,17 @@ function useDragOverlay() { return [dropElementRef, overlayRef] as const; } -function useScrollToBlock(id: string) { +function useScrollToBlock(session: EditorSession) { const blockId = useEditorStore( - (store) => store.getSession(id)?.activeBlockId + (store) => store.getSession(session.id)?.activeBlockId ); useEffect(() => { if (!blockId) return; scrollIntoViewById(blockId); - useEditorStore - .getState() - .updateSession(id, ["default", "unlocked", "deleted"], { - activeBlockId: undefined - }); - }, [blockId]); + useEditorStore.getState().updateSession(session.id, [session.type], { + activeBlockId: undefined + }); + }, [session.id, session.type, blockId]); } function isFile(e: DragEvent) { @@ -824,12 +816,11 @@ function isFile(e: DragEvent) { ); } -function restoreScrollPosition(id: string) { - const session = useEditorStore.getState().getActiveSession(); +function restoreScrollPosition(session: EditorSession) { if (session?.activeBlockId) return scrollIntoViewById(session.activeBlockId); const scrollContainer = document.getElementById(`${id}_editorScroll`); - const scrollPosition = Config.get(`${id}:scroll-position`, 0); + const scrollPosition = Config.get(`${session.id}:scroll-position`, 0); if (scrollContainer) { requestAnimationFrame(() => { if (scrollContainer.scrollHeight < scrollPosition) @@ -839,9 +830,8 @@ function restoreScrollPosition(id: string) { } } -function restoreSelection(id: string) { - const editor = useEditorManager.getState().getEditor(id)?.editor; - editor?.focus({ +function restoreSelection(editor: IEditor, id: string) { + editor.focus({ position: Config.get(`${id}:selection`, { from: 0, to: 0 }) }); } diff --git a/apps/web/src/components/editor/table-of-contents.tsx b/apps/web/src/components/editor/table-of-contents.tsx index 318191838..79bfe45ed 100644 --- a/apps/web/src/components/editor/table-of-contents.tsx +++ b/apps/web/src/components/editor/table-of-contents.tsx @@ -38,7 +38,12 @@ along with this program. If not, see . import React, { useEffect, useState } from "react"; import { ArrowLeft } from "../icons"; -import { useEditorStore } from "../../stores/editor-store"; +import { + DefaultEditorSession, + DeletedEditorSession, + ReadonlyEditorSession, + useEditorStore +} from "../../stores/editor-store"; import { AnimatedFlex } from "../animated"; import ScrollContainer from "../scroll-container"; import { ScopedThemeProvider } from "../theme-provider"; @@ -48,24 +53,21 @@ import { Button, Flex } from "@theme-ui/components"; import { useEditorManager } from "./manager"; type TableOfContentsProps = { - id: string; + session: DefaultEditorSession | ReadonlyEditorSession | DeletedEditorSession; }; function TableOfContents(props: TableOfContentsProps) { - const { id } = props; + const { session } = props; const [active, setActive] = useState([]); const toggleTableOfContents = useEditorStore( (store) => store.toggleTableOfContents ); - const session = useEditorStore((store) => - store.getSession(id, ["default", "unlocked", "readonly"]) - ); const tableOfContents = useEditorManager( - (store) => store.editors[id].tableOfContents || [] + (store) => store.editors[session.id]?.tableOfContents || [] ); useEffect(() => { - const editorScroll = document.getElementById(`${id}_editorScroll`); + const editorScroll = document.getElementById(`${session.id}_editorScroll`); if (!editorScroll) return; function onScroll() { const scrollTop = editorScroll?.scrollTop || 0; @@ -85,7 +87,7 @@ function TableOfContents(props: TableOfContentsProps) { return () => { editorScroll.removeEventListener("scroll", onScroll); }; - }, [id]); + }, [session.id, tableOfContents]); if (!session) return null; return ( diff --git a/apps/web/src/components/properties/index.tsx b/apps/web/src/components/properties/index.tsx index 86bf354aa..d05c6ae3d 100644 --- a/apps/web/src/components/properties/index.tsx +++ b/apps/web/src/components/properties/index.tsx @@ -34,7 +34,8 @@ import { Box, Button, Flex, Text } from "@theme-ui/components"; import { useEditorStore, ReadonlyEditorSession, - DefaultEditorSession + DefaultEditorSession, + DeletedEditorSession } from "../../stores/editor-store"; import { db } from "../../common/db"; import { useStore as useAppStore } from "../../stores/app-store"; @@ -54,7 +55,6 @@ import { VirtualizedList } from "../virtualized-list"; import { SessionItem } from "../session-item"; import { ContentBlock, - DefaultColors, Note, VirtualizedGrouping, createInternalLink, @@ -106,18 +106,15 @@ const metadataItems = [ ]; type EditorPropertiesProps = { - id: string; + session: DefaultEditorSession | ReadonlyEditorSession | DeletedEditorSession; }; function EditorProperties(props: EditorPropertiesProps) { - const { id } = props; + const { session } = props; const toggleProperties = useEditorStore((store) => store.toggleProperties); const isFocusMode = useAppStore((store) => store.isFocusMode); - const session = useEditorStore((store) => - store.getSession(id, ["default", "unlocked", "readonly"]) - ); - if (isFocusMode || !session) return null; + if (isFocusMode) return null; return ( } > - <> - {tools.map((tool) => ( - changeToggleState(tool.key, session)} - testId={`properties-${tool.key}`} - /> - ))} - + {session.type === "deleted" ? null : ( + <> + {tools.map((tool) => ( + changeToggleState(tool.key, session)} + testId={`properties-${tool.key}`} + /> + ))} + + )} {metadataItems.map((item) => ( ))} - + {session.type === "deleted" ? null : ( + + )} - - - - - + + + + +