diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockSelection.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockSelection.hooks.tsx index 0404fe42b8..6015440964 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockSelection.hooks.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockSelection.hooks.tsx @@ -72,7 +72,7 @@ export function useBlockSelection({ }); }, []); - const calcIntersectBlocks = useCallback( + const updateSelctionsByPoint = useCallback( (clientX: number, clientY: number) => { if (!isDragging) return; const [startX, startY] = pointRef.current; @@ -86,7 +86,7 @@ export function useBlockSelection({ endY, }); disaptch( - documentActions.changeSelectionByIntersectRect({ + documentActions.setSelectionByRect({ startX: Math.min(startX, endX), startY: Math.min(startY, endY), endX: Math.max(startX, endX), @@ -102,7 +102,7 @@ export function useBlockSelection({ if (!isDragging) return; e.preventDefault(); e.stopPropagation(); - calcIntersectBlocks(e.clientX, e.clientY); + updateSelctionsByPoint(e.clientX, e.clientY); const { top, bottom } = container.getBoundingClientRect(); if (e.clientY >= bottom) { @@ -124,7 +124,7 @@ export function useBlockSelection({ } if (!isDragging) return; e.preventDefault(); - calcIntersectBlocks(e.clientX, e.clientY); + updateSelctionsByPoint(e.clientX, e.clientY); setDragging(false); setRect(null); }, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/BlockSideTools.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/BlockSideTools.hooks.tsx index 9773339a45..c707e4c4e1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/BlockSideTools.hooks.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/BlockSideTools.hooks.tsx @@ -2,7 +2,7 @@ import { BlockType } from '@/appflowy_app/interfaces/document'; import { useAppSelector } from '@/appflowy_app/stores/store'; import { debounce } from '@/appflowy_app/utils/tool'; import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; -import { YDocControllerContext } from '../../../stores/effects/document/document_controller'; +import { DocumentControllerContext } from '$app/stores/effects/document/document_controller'; import { Node } from '@/appflowy_app/stores/reducers/document/slice'; import { v4 } from 'uuid'; @@ -74,7 +74,7 @@ export function useBlockSideTools({ container }: { container: HTMLDivElement }) } function useController() { - const controller = useContext(YDocControllerContext); + const controller = useContext(DocumentControllerContext); const insertAfter = useCallback((node: Node) => { const parentId = node.parent; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx index 1191705f0b..62e249d354 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx @@ -5,14 +5,14 @@ import { documentActions } from '$app/stores/reducers/document/slice'; export function useParseTree(documentData: DocumentData) { const dispatch = useAppDispatch(); - const { blocks, ytexts, yarrays } = documentData; + const { blocks, meta } = documentData; useEffect(() => { dispatch( - documentActions.createTree({ + documentActions.create({ nodes: blocks, - delta: ytexts, - children: yarrays, + delta: meta.text_map, + children: meta.children_map, }) ); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx index 3e89c1b31e..556370e6d4 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx @@ -4,7 +4,7 @@ import { useRoot } from './Root.hooks'; import Node from '../Node'; import { withErrorBoundary } from 'react-error-boundary'; import { ErrorBoundaryFallbackComponent } from '../_shared/ErrorBoundaryFallbackComponent'; -import VirtualizerList from '../VirtualizerList'; +import VirtualizedList from '../VirtualizerList'; import { Skeleton } from '@mui/material'; function Root({ documentData }: { documentData: DocumentData }) { @@ -20,7 +20,7 @@ function Root({ documentData }: { documentData: DocumentData }) { return (
- +
); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts deleted file mode 100644 index f30afffad4..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts +++ /dev/null @@ -1,61 +0,0 @@ - - -import { useEffect, useMemo, useRef } from "react"; -import { createEditor } from "slate"; -import { withReact } from "slate-react"; - -import * as Y from 'yjs'; -import { withYjs, YjsEditor, slateNodesToInsertDelta } from '@slate-yjs/core'; -import { Delta } from '@slate-yjs/core/dist/model/types'; -import { TextDelta } from '@/appflowy_app/interfaces/document'; - -const initialValue = [{ - type: 'paragraph', - children: [{ text: '' }], -}]; - -export function useBindYjs(delta: TextDelta[], update: (_delta: Delta) => void) { - const yTextRef = useRef(); - // Create a yjs document and get the shared type - const sharedType = useMemo(() => { - const ydoc = new Y.Doc() - const _sharedType = ydoc.get('content', Y.XmlText) as Y.XmlText; - - const insertDelta = slateNodesToInsertDelta(initialValue); - // Load the initial value into the yjs document - _sharedType.applyDelta(insertDelta); - - const yText = insertDelta[0].insert as Y.XmlText; - yTextRef.current = yText; - - return _sharedType; - }, []); - - const editor = useMemo(() => withYjs(withReact(createEditor()), sharedType), []); - - useEffect(() => { - YjsEditor.connect(editor); - return () => { - yTextRef.current = undefined; - YjsEditor.disconnect(editor); - } - }, [editor]); - - useEffect(() => { - const yText = yTextRef.current; - if (!yText) return; - - const textEventHandler = (event: Y.YTextEvent) => { - update(event.changes.delta as Delta); - } - yText.applyDelta(delta); - yText.observe(textEventHandler); - - return () => { - yText.unobserve(textEventHandler); - } - }, [delta]) - - - return { editor } -} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts index cea09635ac..a513bb2521 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts @@ -1,72 +1,18 @@ -import { triggerHotkey } from "@/appflowy_app/utils/slate/hotkey"; -import { useCallback, useContext, useMemo, useRef, useState } from "react"; -import { Descendant, Range } from "slate"; -import { useBindYjs } from "./BindYjs.hooks"; -import { YDocControllerContext } from '../../../stores/effects/document/document_controller'; -import { Delta } from "@slate-yjs/core/dist/model/types"; -import { TextDelta } from '../../../interfaces/document'; -import { debounce } from "@/appflowy_app/utils/tool"; - -function useController(textId: string) { - const docController = useContext(YDocControllerContext); - - const update = useCallback( - (delta: Delta) => { - docController?.yTextApply(textId, delta) - }, - [textId], - ); - const transact = useCallback( - (actions: (() => void)[]) => { - docController?.transact(actions) - }, - [textId], - ) - - return { - update, - transact - } -} - -function useTransact(textId: string) { - const pendingActions = useRef<(() => void)[]>([]); - const { update, transact } = useController(textId); - - const sendTransact = useCallback( - () => { - const actions = pendingActions.current; - transact(actions); - }, - [transact], - ) - - const debounceSendTransact = useMemo(() => debounce(sendTransact, 300), [transact]); - - const sendDelta = useCallback( - (delta: Delta) => { - const action = () => update(delta); - pendingActions.current.push(action); - debounceSendTransact() - }, - [update, debounceSendTransact], - ); - return { - sendDelta - } -} +import { triggerHotkey } from '@/appflowy_app/utils/slate/hotkey'; +import { useCallback, useState } from 'react'; +import { Descendant, Range } from 'slate'; +import { TextDelta } from '$app/interfaces/document'; +import { useTextInput } from '../_shared/TextInput.hooks'; export function useTextBlock(text: string, delta: TextDelta[]) { - const { sendDelta } = useTransact(text); - - const { editor } = useBindYjs(delta, sendDelta); + const { editor } = useTextInput(text, delta); const [value, setValue] = useState([]); - + const onChange = useCallback( (e: Descendant[]) => { setValue(e); }, - [editor], + [editor] ); const onKeyDownCapture = (event: React.KeyboardEvent) => { @@ -74,14 +20,13 @@ export function useTextBlock(text: string, delta: TextDelta[]) { case 'Enter': { event.stopPropagation(); event.preventDefault(); - return; } case 'Backspace': { if (!editor.selection) return; const { anchor } = editor.selection; - const isCollapase = Range.isCollapsed(editor.selection); - if (isCollapase && anchor.offset === 0 && anchor.path.toString() === '0,0') { + const isCollapsed = Range.isCollapsed(editor.selection); + if (isCollapsed && anchor.offset === 0 && anchor.path.toString() === '0,0') { event.stopPropagation(); event.preventDefault(); return; @@ -89,16 +34,15 @@ export function useTextBlock(text: string, delta: TextDelta[]) { } } triggerHotkey(event, editor); - } + }; const onDOMBeforeInput = useCallback((e: InputEvent) => { - // COMPAT: in Apple, `compositionend` is dispatched after the - // `beforeinput` for "insertFromComposition". It will cause repeated characters when inputting Chinese. - // Here, prevent the beforeInput event and wait for the compositionend event to take effect + // COMPAT: in Apple, `compositionend` is dispatched after the `beforeinput` for "insertFromComposition". + // It will cause repeated characters when inputting Chinese. + // Here, prevent the beforeInput event and wait for the compositionend event to take effect. if (e.inputType === 'insertFromComposition') { e.preventDefault(); } - }, []); return { @@ -106,6 +50,6 @@ export function useTextBlock(text: string, delta: TextDelta[]) { onKeyDownCapture, onDOMBeforeInput, editor, - value - } + value, + }; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx index a64bd56990..2c9439af6c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx @@ -3,7 +3,7 @@ import Leaf from './Leaf'; import { useTextBlock } from './TextBlock.hooks'; import { Node } from '@/appflowy_app/stores/reducers/document/slice'; import NodeComponent from '../Node'; -import HoveringToolbar from '../HoveringToolbar'; +import HoveringToolbar from '../_shared/HoveringToolbar'; import { TextDelta } from '@/appflowy_app/interfaces/document'; import React from 'react'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizerList.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizedList.hooks.tsx similarity index 73% rename from frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizerList.hooks.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizedList.hooks.tsx index c0e543bf5f..e9a8513f2e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizerList.hooks.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizedList.hooks.tsx @@ -3,10 +3,10 @@ import { useRef } from 'react'; const defaultSize = 60; -export function useVirtualizerList(count: number) { +export function useVirtualizedList(count: number) { const parentRef = useRef(null); - const rowVirtualizer = useVirtualizer({ + const Virtualize = useVirtualizer({ count, getScrollElement: () => parentRef.current, estimateSize: () => { @@ -15,7 +15,7 @@ export function useVirtualizerList(count: number) { }); return { - rowVirtualizer, + Virtualize: Virtualize, parentRef, }; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx index 5b3253b299..83f2731213 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { useVirtualizerList } from './VirtualizerList.hooks'; +import { useVirtualizedList } from './VirtualizedList.hooks'; import { Node } from '@/appflowy_app/stores/reducers/document/slice'; import DocumentTitle from '../DocumentTitle'; import Overlay from '../Overlay'; -export default function VirtualizerList({ +export default function VirtualizedList({ childIds, node, renderNode, @@ -13,9 +13,8 @@ export default function VirtualizerList({ node: Node; renderNode: (nodeId: string) => JSX.Element; }) { - const { rowVirtualizer, parentRef } = useVirtualizerList(childIds.length); - - const virtualItems = rowVirtualizer.getVirtualItems(); + const { Virtualize, parentRef } = useVirtualizedList(childIds.length); + const virtualItems = Virtualize.getVirtualItems(); return ( <> @@ -26,7 +25,7 @@ export default function VirtualizerList({
@@ -43,7 +42,7 @@ export default function VirtualizerList({ {virtualItems.map((virtualRow) => { const id = childIds[virtualRow.index]; return ( -
+
{virtualRow.index === 0 ? : null} {renderNode(id)}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/FormatButton.tsx similarity index 92% rename from frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatButton.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/FormatButton.tsx index 1409680f24..903603480e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatButton.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/FormatButton.tsx @@ -1,4 +1,4 @@ -import { toggleFormat, isFormatActive } from '@/appflowy_app/utils/slate/format'; +import { toggleFormat, isFormatActive } from '$app/utils/slate/format'; import IconButton from '@mui/material/IconButton'; import Tooltip from '@mui/material/Tooltip'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatIcon.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/FormatIcon.tsx similarity index 100% rename from frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatIcon.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/FormatIcon.tsx diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.hooks.ts similarity index 89% rename from frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts rename to frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.hooks.ts index ac512b536f..c5099732f8 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.hooks.ts @@ -1,7 +1,6 @@ import { useEffect, useRef } from 'react'; import { useFocused, useSlate } from 'slate-react'; -import { calcToolbarPosition } from '@/appflowy_app/utils/slate/toolbar'; - +import { calcToolbarPosition } from '$app/utils/slate/toolbar'; export function useHoveringToolbar(id: string) { const editor = useSlate(); @@ -29,6 +28,6 @@ export function useHoveringToolbar(id: string) { return { ref, inFocus, - editor - } -} \ No newline at end of file + editor, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.tsx similarity index 95% rename from frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.tsx index a35588033c..d4a671ec83 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.tsx @@ -1,5 +1,5 @@ import FormatButton from './FormatButton'; -import Portal from '../BlockPortal'; +import Portal from '../../BlockPortal'; import { useHoveringToolbar } from './index.hooks'; const HoveringToolbar = ({ id }: { id: string }) => { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts index 1b3b4b71c8..2adfb073f8 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts @@ -3,22 +3,36 @@ import { useAppSelector } from '@/appflowy_app/stores/store'; import { useMemo } from 'react'; import { TextDelta } from '@/appflowy_app/interfaces/document'; +/** + * Subscribe to a node and its children + * It will be change when the node or its children is changed + * And it will not be change when other node is changed + * @param id + */ export function useSubscribeNode(id: string) { const node = useAppSelector(state => state.document.nodes[id]); + const childIds = useAppSelector(state => { const childrenId = state.document.nodes[id]?.children; if (!childrenId) return; return state.document.children[childrenId]; }); + const delta = useAppSelector(state => { - const deltaId = state.document.nodes[id]?.data?.text; + const externalType = state.document.nodes[id]?.externalType; + if (externalType !== 'text') return; + const deltaId = state.document.nodes[id]?.externalId; if (!deltaId) return; return state.document.delta[deltaId]; }); + const isSelected = useAppSelector(state => { return state.document.selections?.includes(id) || false; }); + // Memoize the node and its children + // So that the component will not be re-rendered when other node is changed + // It very important for performance const memoizedNode = useMemo(() => node, [node?.id, node?.data, node?.parent, node?.type, node?.children]); const memoizedChildIds = useMemo(() => childIds, [JSON.stringify(childIds)]); const memoizedDelta = useMemo(() => delta, [JSON.stringify(delta)]); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextInput.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextInput.hooks.ts new file mode 100644 index 0000000000..579bdccec4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextInput.hooks.ts @@ -0,0 +1,116 @@ +import { useCallback, useContext, useMemo, useRef, useEffect } from 'react'; +import { DocumentControllerContext } from '$app/stores/effects/document/document_controller'; +import { TextDelta } from '$app/interfaces/document'; +import { debounce } from '@/appflowy_app/utils/tool'; +import { createEditor } from 'slate'; +import { withReact } from 'slate-react'; + +import * as Y from 'yjs'; +import { withYjs, YjsEditor, slateNodesToInsertDelta } from '@slate-yjs/core'; + +export function useTextInput(text: string, delta: TextDelta[]) { + const { sendDelta } = useTransact(text); + const { editor } = useBindYjs(delta, sendDelta); + + return { + editor, + }; +} + +function useController(textId: string) { + const docController = useContext(DocumentControllerContext); + + const update = useCallback( + (delta: TextDelta[]) => { + docController?.yTextApply(textId, delta); + }, + [textId] + ); + const transact = useCallback( + (actions: (() => void)[]) => { + docController?.transact(actions); + }, + [textId] + ); + + return { + update, + transact, + }; +} + +function useTransact(textId: string) { + const pendingActions = useRef<(() => void)[]>([]); + const { update, transact } = useController(textId); + + const sendTransact = useCallback(() => { + const actions = pendingActions.current; + transact(actions); + }, [transact]); + + const debounceSendTransact = useMemo(() => debounce(sendTransact, 300), [transact]); + + const sendDelta = useCallback( + (delta: TextDelta[]) => { + const action = () => update(delta); + pendingActions.current.push(action); + debounceSendTransact(); + }, + [update, debounceSendTransact] + ); + return { + sendDelta, + }; +} + +const initialValue = [ + { + type: 'paragraph', + children: [{ text: '' }], + }, +]; + +export function useBindYjs(delta: TextDelta[], update: (_delta: TextDelta[]) => void) { + const yTextRef = useRef(); + // Create a yjs document and get the shared type + const sharedType = useMemo(() => { + const doc = new Y.Doc(); + const _sharedType = doc.get('content', Y.XmlText) as Y.XmlText; + + const insertDelta = slateNodesToInsertDelta(initialValue); + // Load the initial value into the yjs document + _sharedType.applyDelta(insertDelta); + + const yText = insertDelta[0].insert as Y.XmlText; + yTextRef.current = yText; + + return _sharedType; + }, []); + + const editor = useMemo(() => withYjs(withReact(createEditor()), sharedType), []); + + useEffect(() => { + YjsEditor.connect(editor); + return () => { + yTextRef.current = undefined; + YjsEditor.disconnect(editor); + }; + }, [editor]); + + useEffect(() => { + const yText = yTextRef.current; + if (!yText) return; + + const textEventHandler = (event: Y.YTextEvent) => { + update(event.changes.delta as TextDelta[]); + }; + yText.applyDelta(delta); + yText.observe(textEventHandler); + + return () => { + yText.unobserve(textEventHandler); + }; + }, [delta]); + + return { editor }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.hooks.ts index b03ecd865d..3c38ab70cd 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.hooks.ts @@ -9,7 +9,6 @@ import { useError } from '../../error/Error.hooks'; import { AppObserver } from '../../../stores/effects/folder/app/app_observer'; import { useNavigate } from 'react-router-dom'; import { INITIAL_FOLDER_HEIGHT, PAGE_ITEM_HEIGHT } from '../../_shared/constants'; -import { YDocController } from '$app/stores/effects/document/document_controller'; export const useFolderEvents = (folder: IFolder, pages: IPage[]) => { const appDispatch = useAppDispatch(); @@ -133,10 +132,6 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => { layoutType: ViewLayoutTypePB.Document, }); - // temp: let me try it by yjs - const ydocController = new YDocController(newView.id); - await ydocController.createDocument(); - appDispatch( pagesActions.addPage({ folderId: folder.id, diff --git a/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts b/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts index a0efb98d60..61c9a88e06 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts @@ -1,4 +1,3 @@ -import { TextBlockToolbarGroup } from "../interfaces"; export const iconSize = { width: 18, height: 18 }; @@ -24,16 +23,3 @@ export const command: Record = { key: '⌘ + Shift + S or ⌘ + Shift + X', }, }; - -export const toolbarDefaultProps = { - showGroups: [ - TextBlockToolbarGroup.ASK_AI, - TextBlockToolbarGroup.BLOCK_SELECT, - TextBlockToolbarGroup.ADD_LINK, - TextBlockToolbarGroup.COMMENT, - TextBlockToolbarGroup.TEXT_FORMAT, - TextBlockToolbarGroup.TEXT_COLOR, - TextBlockToolbarGroup.MENTION, - TextBlockToolbarGroup.MORE, - ], -}; \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts index 0cfefe0d75..90d91c6d94 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts @@ -16,6 +16,8 @@ export interface NestedBlock { id: string; type: BlockType; data: Record; + externalId: string; + externalType: 'text' | 'array' | 'map'; parent: string | null; children: string; } @@ -26,6 +28,8 @@ export interface TextDelta { export interface DocumentData { rootId: string; blocks: Record; - ytexts: Record; - yarrays: Record; + meta: { + text_map: Record; + children_map: Record; + } } diff --git a/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts b/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts index e6d0760f64..db6c7f48b3 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts @@ -1,112 +1 @@ -import { Descendant } from "slate"; - -// eslint-disable-next-line no-shadow -export enum BlockType { - PageBlock = 'page', - HeadingBlock = 'heading', - ListBlock = 'list', - TextBlock = 'text', - CodeBlock = 'code', - EmbedBlock = 'embed', - QuoteBlock = 'quote', - DividerBlock = 'divider', - MediaBlock = 'media', - TableBlock = 'table', - ColumnBlock = 'column' - -} - -export type BlockData = T extends BlockType.TextBlock ? TextBlockData : -T extends BlockType.PageBlock ? PageBlockData : -T extends BlockType.HeadingBlock ? HeadingBlockData : -T extends BlockType.ListBlock ? ListBlockData : -T extends BlockType.ColumnBlock ? ColumnBlockData : any; - - -export interface BlockInterface { - id: string; - type: BlockType; - data: BlockData; - next: string | null; - firstChild: string | null; -} - - -export interface TextBlockData { - content: Descendant[]; -} - -interface PageBlockData { - title: string; -} - -interface ListBlockData extends TextBlockData { - type: 'numbered' | 'bulleted' | 'column'; -} - -interface HeadingBlockData extends TextBlockData { - level: number; -} - -interface ColumnBlockData { - ratio: string; -} - -// eslint-disable-next-line no-shadow -export enum TextBlockToolbarGroup { - ASK_AI, - BLOCK_SELECT, - ADD_LINK, - COMMENT, - TEXT_FORMAT, - TEXT_COLOR, - MENTION, - MORE -} -export interface TextBlockToolbarProps { - showGroups: TextBlockToolbarGroup[] -} - - -export interface BlockCommonProps { - version: number; - node: T; -} - -export interface BackendOp { - type: 'update' | 'insert' | 'remove' | 'move' | 'move_range'; - version: number; - data: UpdateOpData | InsertOpData | moveRangeOpData | moveOpData | removeOpData; -} -export interface LocalOp { - type: 'update' | 'insert' | 'remove' | 'move' | 'move_range'; - version: number; - data: UpdateOpData | InsertOpData | moveRangeOpData | moveOpData | removeOpData; -} - -export interface UpdateOpData { - blockId: string; - value: BlockData; - path: string[]; -} -export interface InsertOpData { - block: BlockInterface; - parentId: string; - prevId?: string -} - -export interface moveRangeOpData { - range: [string, string]; - newParentId: string; - newPrevId?: string -} - -export interface moveOpData { - blockId: string; - newParentId: string; - newPrevId?: string -} - -export interface removeOpData { - blockId: string -} \ No newline at end of file +export interface Document {} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts index 6b17cfbbd9..286c2cb3b5 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts @@ -1,194 +1,48 @@ -import * as Y from 'yjs'; -import { IndexeddbPersistence } from 'y-indexeddb'; -import { v4 } from 'uuid'; -import { DocumentData, NestedBlock } from '@/appflowy_app/interfaces/document'; +import { DocumentData, BlockType, TextDelta } from '@/appflowy_app/interfaces/document'; import { createContext } from 'react'; -import { BlockType } from '@/appflowy_app/interfaces'; +import { DocumentBackendService } from './document_bd_svc'; -export type DeltaAttributes = { - retain: number; - attributes: Record; -}; +export const DocumentControllerContext = createContext(null); -export type DeltaRetain = { retain: number }; -export type DeltaDelete = { delete: number }; -export type DeltaInsert = { - insert: string | Y.XmlText; - attributes?: Record; -}; +export class DocumentController { + private readonly backendService: DocumentBackendService; -export type InsertDelta = Array; -export type Delta = Array< - DeltaRetain | DeltaDelete | DeltaInsert | DeltaAttributes ->; - - -export const YDocControllerContext = createContext(null); - -export class YDocController { - private _ydoc: Y.Doc; - private readonly provider: IndexeddbPersistence; - - constructor(private id: string) { - this._ydoc = new Y.Doc(); - this.provider = new IndexeddbPersistence(`document-${this.id}`, this._ydoc); - this._ydoc.on('update', this.handleUpdate); + constructor(public readonly viewId: string) { + this.backendService = new DocumentBackendService(viewId); } - handleUpdate = (update: Uint8Array, origin: any) => { - const isLocal = origin === null; - Y.logUpdate(update); - } - - - createDocument = async () => { - await this.provider.whenSynced; - const ydoc = this._ydoc; - const blocks = ydoc.getMap('blocks'); - const rootNode = ydoc.getArray("root"); - - // create page block for root node - const rootId = v4(); - rootNode.push([rootId]) - const rootChildrenId = v4(); - const rootChildren = ydoc.getArray(rootChildrenId); - const rootTitleId = v4(); - const yTitle = ydoc.getText(rootTitleId); - yTitle.insert(0, ""); - const root = { - id: rootId, - type: 'page', - data: { - text: rootTitleId - }, - parent: null, - children: rootChildrenId - }; - blocks.set(root.id, root); - - // create text block for first line - const textId = v4(); - const yTextId = v4(); - const ytext = ydoc.getText(yTextId); - ytext.insert(0, ""); - const textChildrenId = v4(); - ydoc.getArray(textChildrenId); - const text = { - id: textId, - type: 'text', - data: { - text: yTextId, - }, - parent: rootId, - children: textChildrenId, + open = async (): Promise => { + const openDocumentResult = await this.backendService.open(); + if (openDocumentResult.ok) { + return { + rootId: '', + blocks: {}, + ytexts: {}, + yarrays: {} + }; + } else { + return null; } - - // add text block to root children - rootChildren.push([textId]); - blocks.set(text.id, text); - } + }; - open = async (): Promise => { - await this.provider.whenSynced; - const ydoc = this._ydoc; - - const blocks = ydoc.getMap('blocks'); - const obj: DocumentData = { - rootId: ydoc.getArray('root').toArray()[0] || '', - blocks: blocks.toJSON(), - ytexts: {}, - yarrays: {} - }; - - Object.keys(obj.blocks).forEach(key => { - const value = obj.blocks[key]; - if (value.children) { - const yarray = ydoc.getArray(value.children); - Object.assign(obj.yarrays, { - [value.children]: yarray.toArray() - }); - } - if (value.data.text) { - const ytext = ydoc.getText(value.data.text); - Object.assign(obj.ytexts, { - [value.data.text]: ytext.toDelta() - }) - } - }); - - blocks.observe(this.handleBlocksEvent); - return obj; - } insert(node: { id: string, type: BlockType, - delta?: Delta + delta?: TextDelta[] }, parentId: string, prevId: string) { - const blocks = this._ydoc.getMap('blocks'); - const parent = blocks.get(parentId); - if (!parent) return; - const insertNode = { - id: node.id, - type: node.type, - data: { - text: '' - }, - children: '', - parent: '' - } - // create ytext - if (node.delta) { - const ytextId = v4(); - const ytext = this._ydoc.getText(ytextId); - ytext.applyDelta(node.delta); - insertNode.data.text = ytextId; - } - // create children - const yArrayId = v4(); - this._ydoc.getArray(yArrayId); - insertNode.children = yArrayId; - // insert in parent's children - const children = this._ydoc.getArray(parent.children); - const index = children.toArray().indexOf(prevId) + 1; - children.insert(index, [node.id]); - insertNode.parent = parentId; - // set in blocks - this._ydoc.getMap('blocks').set(node.id, insertNode); + // } transact(actions: (() => void)[]) { - const ydoc = this._ydoc; - console.log('====transact') - ydoc.transact(() => { - actions.forEach(action => { - action(); - }); - }); + // } - yTextApply = (yTextId: string, delta: Delta) => { - const ydoc = this._ydoc; - const ytext = ydoc.getText(yTextId); - ytext.applyDelta(delta); - console.log("====", yTextId, delta); - } - - close = () => { - const blocks = this._ydoc.getMap('blocks'); - blocks.unobserve(this.handleBlocksEvent); - } - - private handleBlocksEvent = (mapEvent: Y.YMapEvent) => { - console.log(mapEvent.changes); - } - - private handleTextEvent = (textEvent: Y.YTextEvent) => { - console.log(textEvent.changes); - } - - private handleArrayEvent = (arrayEvent: Y.YArrayEvent) => { - console.log(arrayEvent.changes); + yTextApply = (yTextId: string, delta: TextDelta[]) => { + // } + dispose = async () => { + await this.backendService.close(); + }; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts index 4ef40d3781..dc19a528de 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts @@ -1,17 +1,8 @@ -import { BlockType, TextDelta } from "@/appflowy_app/interfaces/document"; +import { BlockType, NestedBlock, TextDelta } from "@/appflowy_app/interfaces/document"; import { PayloadAction, createSlice } from "@reduxjs/toolkit"; import { RegionGrid } from "./region_grid"; -export interface Node { - id: string; - type: BlockType; - data: { - text?: string; - style?: Record - }; - parent: string | null; - children: string; -} +export type Node = NestedBlock; export interface NodeState { nodes: Record; @@ -33,11 +24,11 @@ export const documentSlice = createSlice({ name: 'document', initialState: initialState, reducers: { - clear: (state, action: PayloadAction) => { + clear: () => { return initialState; }, - createTree: (state, action: PayloadAction<{ + create: (state, action: PayloadAction<{ nodes: Record; children: Record; delta: Record; @@ -52,7 +43,7 @@ export const documentSlice = createSlice({ state.selections = action.payload; }, - changeSelectionByIntersectRect: (state, action: PayloadAction<{ + setSelectionByRect: (state, action: PayloadAction<{ startX: number; startY: number; endX: number; @@ -77,26 +68,57 @@ export const documentSlice = createSlice({ regionGrid.updateBlock(id, position); }, + addNode: (state, action: PayloadAction) => { + state.nodes[action.payload.id] = action.payload; + }, + + addChild: (state, action: PayloadAction<{ parentId: string, childId: string, prevId: string }>) => { + const { parentId, childId, prevId } = action.payload; + const parentChildrenId = state.nodes[parentId].children; + const children = state.children[parentChildrenId]; + const prevIndex = children.indexOf(prevId); + if (prevIndex === -1) { + children.push(childId) + } else { + children.splice(prevIndex + 1, 0, childId); + } + }, + + updateChildren: (state, action: PayloadAction<{ id: string; childIds: string[] }>) => { + const { id, childIds } = action.payload; + state.children[id] = childIds; + }, + + updateDelta: (state, action: PayloadAction<{ id: string; delta: TextDelta[] }>) => { + const { id, delta } = action.payload; + state.delta[id] = delta; + }, + updateNode: (state, action: PayloadAction<{id: string; type?: BlockType; data?: any }>) => { state.nodes[action.payload.id] = { ...state.nodes[action.payload.id], ...action.payload } }, + removeNode: (state, action: PayloadAction) => { const { children, data, parent } = state.nodes[action.payload]; + // remove from parent if (parent) { const index = state.children[state.nodes[parent].children].indexOf(action.payload); if (index > -1) { state.children[state.nodes[parent].children].splice(index, 1); } } + // remove children if (children) { delete state.children[children]; } + // remove delta if (data && data.text) { delete state.delta[data.text]; } + // remove node delete state.nodes[action.payload]; }, }, diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/block_selection.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/block_selection.ts deleted file mode 100644 index 8bc67522ce..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/block_selection.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { BlockData, BlockType } from "../interfaces"; - - -export function filterSelections(ids: string[], nodeMap: Map): string[] { - const selected = new Set(ids); - const newSelected = new Set(); - ids.forEach(selectedId => { - const node = nodeMap.get(selectedId); - if (!node) return; - if (node.type === BlockType.ListBlock && node.data.type === 'column') { - return; - } - if (node.children.length === 0) { - newSelected.add(selectedId); - return; - } - const hasChildSelected = node.children.some(i => selected.has(i.id)); - if (!hasChildSelected) { - newSelected.add(selectedId); - return; - } - const hasSiblingSelected = node.parent?.children.filter(i => i.id !== selectedId).some(i => selected.has(i.id)); - if (hasChildSelected && hasSiblingSelected) { - newSelected.add(selectedId); - return; - } - }); - - return Array.from(newSelected); -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/slate/context.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/slate/context.ts deleted file mode 100644 index 387b74ff50..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/slate/context.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createContext } from "react"; -import { TextBlockManager } from '../../block_editor/blocks/text_block'; - -export const TextBlockContext = createContext<{ - textBlockManager?: TextBlockManager -}>({}); diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts index c91ba322d4..d9016e2586 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts @@ -6,24 +6,26 @@ import { } from '../../services/backend/events/flowy-document'; import { useParams } from 'react-router-dom'; import { DocumentData } from '../interfaces/document'; -import { YDocController } from '$app/stores/effects/document/document_controller'; +import { DocumentController } from '$app/stores/effects/document/document_controller'; export const useDocument = () => { const params = useParams(); const [ documentId, setDocumentId ] = useState(); const [ documentData, setDocumentData ] = useState(); - const [ controller, setController ] = useState(null); + const [ controller, setController ] = useState(null); useEffect(() => { void (async () => { if (!params?.id) return; - const c = new YDocController(params.id); + const c = new DocumentController(params.id); setController(c); const res = await c.open(); console.log(res) + if (!res) return; setDocumentData(res) setDocumentId(params.id) + })(); return () => { console.log('==== leave ====', params?.id) diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx index 7386c106a6..301c241081 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx @@ -1,7 +1,7 @@ import { useDocument } from './DocumentPage.hooks'; import { createTheme, ThemeProvider } from '@mui/material'; import Root from '../components/document/Root'; -import { YDocControllerContext } from '../stores/effects/document/document_controller'; +import { DocumentControllerContext } from '../stores/effects/document/document_controller'; const theme = createTheme({ typography: { @@ -15,9 +15,9 @@ export const DocumentPage = () => { if (!documentId || !documentData || !controller) return null; return ( - + - + ); };