Selaa lähdekoodia

Merge pull request #2258 from qinluhe/feat/refactor-tauri-document

Feat/refactor tauri document
qinluhe 2 vuotta sitten
vanhempi
commit
0e5a03a282
33 muutettua tiedostoa jossa 764 lisäystä ja 788 poistoa
  1. 1 1
      frontend/appflowy_tauri/package.json
  2. 211 265
      frontend/appflowy_tauri/pnpm-lock.yaml
  3. 219 164
      frontend/appflowy_tauri/src-tauri/Cargo.lock
  4. 4 4
      frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockSelection.hooks.tsx
  5. 5 13
      frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/BlockSideTools.hooks.tsx
  6. 3 4
      frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/DocumentTitle.hooks.ts
  7. 9 2
      frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/index.tsx
  8. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/document/HeadingBlock/index.tsx
  9. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/index.tsx
  10. 17 17
      frontend/appflowy_tauri/src/appflowy_app/components/document/Node/Node.hooks.ts
  11. 17 6
      frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx
  12. 3 4
      frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx
  13. 2 2
      frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx
  14. 0 61
      frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts
  15. 16 71
      frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts
  16. 4 6
      frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx
  17. 3 3
      frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizedList/VirtualizedList.hooks.tsx
  18. 6 7
      frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizedList/index.tsx
  19. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/FormatButton.tsx
  20. 0 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/FormatIcon.tsx
  21. 4 6
      frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.hooks.ts
  22. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.tsx
  23. 21 15
      frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts
  24. 114 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextInput.hooks.ts
  25. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/tests/TestDocument.tsx
  26. 15 3
      frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts
  27. 2 17
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_bd_svc.ts
  28. 31 49
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts
  29. 2 2
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_observer.ts
  30. 46 51
      frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts
  31. 1 1
      frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts
  32. 2 2
      frontend/rust-lib/flowy-document2/src/document.rs
  33. 1 7
      frontend/rust-lib/flowy-document2/src/entities.rs

+ 1 - 1
frontend/appflowy_tauri/package.json

@@ -74,4 +74,4 @@
     "uuid": "^9.0.0",
     "vite": "^4.0.0"
   }
-}
+}

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 211 - 265
frontend/appflowy_tauri/pnpm-lock.yaml


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 219 - 164
frontend/appflowy_tauri/src-tauri/Cargo.lock


+ 4 - 4
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);
     },

+ 5 - 13
frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/BlockSideTools.hooks.tsx

@@ -1,4 +1,4 @@
-import { BlockType } from '@/appflowy_app/interfaces/document';
+import { BlockType, HeadingBlockData } 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';
@@ -43,9 +43,10 @@ export function useBlockSideTools({ container }: { container: HTMLDivElement })
       el.style.zIndex = '1';
       el.style.top = '1px';
       if (node?.type === BlockType.HeadingBlock) {
-        if (node.data.style?.level === 1) {
+        const nodeData = node.data as HeadingBlockData;
+        if (nodeData.level === 1) {
           el.style.top = '8px';
-        } else if (node.data.style?.level === 2) {
+        } else if (nodeData.level === 2) {
           el.style.top = '6px';
         } else {
           el.style.top = '5px';
@@ -80,16 +81,7 @@ function useController() {
     const parentId = node.parent;
     if (!parentId || !controller) return;
 
-    controller.transact([
-      () => {
-        const newNode = {
-          id: v4(),
-          delta: [],
-          type: BlockType.TextBlock,
-        };
-        controller.insert(newNode, parentId, node.id);
-      },
-    ]);
+    //
   }, []);
 
   return {

+ 3 - 4
frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/DocumentTitle.hooks.ts

@@ -1,8 +1,7 @@
 import { useSubscribeNode } from '../_shared/SubscribeNode.hooks';
 export function useDocumentTitle(id: string) {
-  const { node, delta } = useSubscribeNode(id);
+  const { node } = useSubscribeNode(id);
   return {
     node,
-    delta
-  }
-}
+  };
+}

+ 9 - 2
frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/index.tsx

@@ -3,11 +3,18 @@ import { useDocumentTitle } from './DocumentTitle.hooks';
 import TextBlock from '../TextBlock';
 
 export default function DocumentTitle({ id }: { id: string }) {
-  const { node, delta } = useDocumentTitle(id);
+  const { node } = useDocumentTitle(id);
   if (!node) return null;
   return (
     <div data-block-id={node.id} className='doc-title relative pt-[50px] text-4xl font-bold'>
-      <TextBlock placeholder='Untitled' childIds={[]} delta={delta || []} node={node} />
+      <TextBlock placeholder='Untitled' childIds={[]} node={{
+        ...node,
+        data: {
+          ...node.data,
+          delta: node.data.delta || [],
+        }
+      }} />
+
     </div>
   );
 }

+ 1 - 1
frontend/appflowy_tauri/src/appflowy_app/components/document/HeadingBlock/index.tsx

@@ -11,7 +11,7 @@ const fontSize: Record<string, string> = {
 export default function HeadingBlock({ node, delta }: { node: Node; delta: TextDelta[] }) {
   return (
     <div className={`${fontSize[node.data.style?.level]} font-semibold	`}>
-      <TextBlock node={node} childIds={[]} delta={delta} />
+      {/*<TextBlock node={node} childIds={[]} delta={delta} />*/}
     </div>
   );
 }

+ 1 - 1
frontend/appflowy_tauri/src/appflowy_app/components/document/ListBlock/index.tsx

@@ -11,7 +11,7 @@ export default function ListBlock({ node, delta }: { node: Node; delta: TextDelt
     if (node.data.style?.type === 'column') return <></>;
     return (
       <div className='flex-1'>
-        <TextBlock delta={delta} node={node} childIds={[]} />
+        {/*<TextBlock delta={delta} node={node} childIds={[]} />*/}
       </div>
     );
   }, [node, delta]);

+ 17 - 17
frontend/appflowy_tauri/src/appflowy_app/components/document/Node/Node.hooks.ts

@@ -1,11 +1,10 @@
-
 import { useEffect, useRef } from 'react';
 import { useSubscribeNode } from '../_shared/SubscribeNode.hooks';
 import { useAppDispatch } from '$app/stores/store';
 import { documentActions } from '$app/stores/reducers/document/slice';
 
 export function useNode(id: string) {
-  const { node, childIds, delta, isSelected } = useSubscribeNode(id);
+  const { node, childIds, isSelected } = useSubscribeNode(id);
   const ref = useRef<HTMLDivElement>(null);
 
   const dispatch = useAppDispatch();
@@ -15,22 +14,23 @@ export function useNode(id: string) {
     const rect = ref.current.getBoundingClientRect();
 
     const scrollContainer = document.querySelector('.doc-scroller-container') as HTMLDivElement;
-    dispatch(documentActions.updateNodePosition({
-      id,
-      rect: {
-        x: rect.x,
-        y: rect.y + scrollContainer.scrollTop,
-        height: rect.height,
-        width: rect.width
-      }
-    }))
-  }, [])
-  
+    dispatch(
+      documentActions.updateNodePosition({
+        id,
+        rect: {
+          x: rect.x,
+          y: rect.y + scrollContainer.scrollTop,
+          height: rect.height,
+          width: rect.width,
+        },
+      })
+    );
+  }, []);
+
   return {
     ref,
     node,
     childIds,
-    delta,
-    isSelected
-  }
-}
+    isSelected,
+  };
+}

+ 17 - 6
frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx

@@ -7,14 +7,26 @@ import TextBlock from '../TextBlock';
 import { TextDelta } from '@/appflowy_app/interfaces/document';
 
 function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<HTMLDivElement>) {
-  const { node, childIds, delta, isSelected, ref } = useNode(id);
+  const { node, childIds, isSelected, ref } = useNode(id);
 
   console.log('=====', id);
-  const renderBlock = useCallback((_props: { node: Node; childIds?: string[]; delta?: TextDelta[] }) => {
+  const renderBlock = useCallback((_props: { node: Node; childIds?: string[] }) => {
     switch (_props.node.type) {
-      case 'text':
-        if (!_props.delta) return null;
-        return <TextBlock {..._props} delta={_props.delta} />;
+      case 'text': {
+        const delta = _props.node.data.delta;
+        if (!delta) return null;
+        return (
+          <TextBlock
+            node={{
+              ..._props.node,
+              data: {
+                delta,
+              },
+            }}
+            childIds={childIds}
+          />
+        );
+      }
       default:
         break;
     }
@@ -27,7 +39,6 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<H
       {renderBlock({
         node,
         childIds,
-        delta,
       })}
       <div className='block-overlay' />
       {isSelected ? <div className='pointer-events-none absolute inset-0 z-[-1] rounded-[4px] bg-[#E0F8FF]' /> : null}

+ 3 - 4
frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx

@@ -5,14 +5,13 @@ 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,
+        children: meta.childrenMap,
       })
     );
 

+ 2 - 2
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 '../VirtualizedList';
 import { Skeleton } from '@mui/material';
 
 function Root({ documentData }: { documentData: DocumentData }) {
@@ -20,7 +20,7 @@ function Root({ documentData }: { documentData: DocumentData }) {
 
   return (
     <div id='appflowy-block-doc' className='h-[100%] overflow-hidden'>
-      <VirtualizerList node={node} childIds={childIds} renderNode={renderNode} />
+      <VirtualizedList node={node} childIds={childIds} renderNode={renderNode} />
     </div>
   );
 }

+ 0 - 61
frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts

@@ -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: TextDelta[]) => void) {
-  const yTextRef = useRef<Y.XmlText>();
-  // 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 TextDelta[]);
-    }
-    yText.applyDelta(delta);
-    yText.observe(textEventHandler);
-  
-    return () => {
-      yText.unobserve(textEventHandler);
-    }
-  }, [delta])
-  
-
-  return { editor }
-}

+ 16 - 71
frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts

@@ -1,71 +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 { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
+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 { debounce } from "@/appflowy_app/utils/tool";
+import { useTextInput } from '../_shared/TextInput.hooks';
 
-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
-  }
-}
-
-export function useTextBlock(text: string, delta: TextDelta[]) {
-  const { sendDelta } = useTransact(text);
-
-  const { editor } = useBindYjs(delta, sendDelta);
+export function useTextBlock(delta: TextDelta[]) {
+  const { editor } = useTextInput(delta);
   const [value, setValue] = useState<Descendant[]>([]);
-  
+
   const onChange = useCallback(
     (e: Descendant[]) => {
       setValue(e);
     },
-    [editor],
+    [editor]
   );
 
   const onKeyDownCapture = (event: React.KeyboardEvent<HTMLDivElement>) => {
@@ -73,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;
@@ -88,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 {
@@ -105,6 +50,6 @@ export function useTextBlock(text: string, delta: TextDelta[]) {
     onKeyDownCapture,
     onDOMBeforeInput,
     editor,
-    value
-  }
+    value,
+  };
 }

+ 4 - 6
frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx

@@ -3,23 +3,21 @@ 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 { TextDelta } from '@/appflowy_app/interfaces/document';
+import HoveringToolbar from '../_shared/HoveringToolbar';
 import React from 'react';
+import { TextDelta } from '@/appflowy_app/interfaces/document';
 
 function TextBlock({
   node,
   childIds,
   placeholder,
-  delta,
   ...props
 }: {
-  node: Node;
-  delta: TextDelta[];
+  node: Node & { data: { delta: TextDelta[] } };
   childIds?: string[];
   placeholder?: string;
 } & React.HTMLAttributes<HTMLDivElement>) {
-  const { editor, value, onChange, onKeyDownCapture, onDOMBeforeInput } = useTextBlock(node.data.text!, delta);
+  const { editor, value, onChange, onKeyDownCapture, onDOMBeforeInput } = useTextBlock(node.data.delta);
 
   return (
     <div {...props} className={`py-[2px] ${props.className}`}>

+ 3 - 3
frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizerList.hooks.tsx → frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizedList/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<HTMLDivElement>(null);
 
-  const rowVirtualizer = useVirtualizer({
+  const virtualize = useVirtualizer({
     count,
     getScrollElement: () => parentRef.current,
     estimateSize: () => {
@@ -15,7 +15,7 @@ export function useVirtualizerList(count: number) {
   });
 
   return {
-    rowVirtualizer,
+    virtualize,
     parentRef,
   };
 }

+ 6 - 7
frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx → frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizedList/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({
         <div
           className='doc-body max-w-screen w-[900px] min-w-0'
           style={{
-            height: rowVirtualizer.getTotalSize(),
+            height: virtualize.getTotalSize(),
             position: 'relative',
           }}
         >
@@ -43,7 +42,7 @@ export default function VirtualizerList({
               {virtualItems.map((virtualRow) => {
                 const id = childIds[virtualRow.index];
                 return (
-                  <div className='p-[1px]' key={id} data-index={virtualRow.index} ref={rowVirtualizer.measureElement}>
+                  <div className='p-[1px]' key={id} data-index={virtualRow.index} ref={virtualize.measureElement}>
                     {virtualRow.index === 0 ? <DocumentTitle id={node.id} /> : null}
                     {renderNode(id)}
                   </div>

+ 1 - 1
frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatButton.tsx → 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';
 

+ 0 - 0
frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatIcon.tsx → frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/FormatIcon.tsx


+ 4 - 6
frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts → frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.hooks.ts

@@ -1,8 +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();
   const inFocus = useFocused();
@@ -29,6 +27,6 @@ export function useHoveringToolbar(id: string) {
   return {
     ref,
     inFocus,
-    editor
-  }
-}
+    editor,
+  };
+}

+ 1 - 1
frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.tsx → 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 }) => {

+ 21 - 15
frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts

@@ -1,32 +1,38 @@
 import { Node } from '@/appflowy_app/stores/reducers/document/slice';
 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<Node>(state => state.document.nodes[id]);
-  const childIds = useAppSelector<string[] | undefined>(state => {
+  const node = useAppSelector<Node>((state) => state.document.nodes[id]);
+
+  const childIds = useAppSelector<string[] | undefined>((state) => {
     const childrenId = state.document.nodes[id]?.children;
     if (!childrenId) return;
     return state.document.children[childrenId];
   });
-  const delta = useAppSelector<TextDelta[] | undefined>(state => {
-    const deltaId = state.document.nodes[id]?.data?.text;
-    if (!deltaId) return;
-    return state.document.delta[deltaId];
-  });
-  const isSelected = useAppSelector<boolean>(state => {
+
+  const isSelected = useAppSelector<boolean>((state) => {
     return state.document.selections?.includes(id) || false;
   });
 
-  const memoizedNode = useMemo(() => node, [node?.id, node?.data, node?.parent, node?.type, node?.children]);
+  // 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, JSON.stringify(node?.data), node?.parent, node?.type, node?.children]
+  );
   const memoizedChildIds = useMemo(() => childIds, [JSON.stringify(childIds)]);
-  const memoizedDelta = useMemo(() => delta, [JSON.stringify(delta)]);
-  
+
   return {
     node: memoizedNode,
     childIds: memoizedChildIds,
-    delta: memoizedDelta,
-    isSelected
+    isSelected,
   };
-}
+}

+ 114 - 0
frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextInput.hooks.ts

@@ -0,0 +1,114 @@
+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(delta: TextDelta[]) {
+  const { sendDelta } = useTransact();
+  const { editor } = useBindYjs(delta, sendDelta);
+
+  return {
+    editor,
+  };
+}
+
+function useController() {
+  const docController = useContext(DocumentControllerContext);
+
+  const update = useCallback(
+    (delta: TextDelta[]) => {
+      docController?.applyActions([
+        {
+          type: 'update',
+          payload: {
+            block: {
+              data: {
+                delta,
+              },
+            },
+          },
+        },
+      ]);
+    },
+    [docController]
+  );
+
+  return {
+    update,
+  };
+}
+
+function useTransact() {
+  const { update } = useController();
+
+  const sendDelta = useCallback(
+    (delta: TextDelta[]) => {
+      update(delta);
+    },
+    [update]
+  );
+  const debounceSendDelta = useMemo(() => debounce(sendDelta, 300), [sendDelta]);
+
+  return {
+    sendDelta: debounceSendDelta,
+  };
+}
+
+const initialValue = [
+  {
+    type: 'paragraph',
+    children: [{ text: '' }],
+  },
+];
+
+function useBindYjs(delta: TextDelta[], update: (_delta: TextDelta[]) => void) {
+  const yTextRef = useRef<Y.XmlText>();
+  // 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) => {
+      const textDelta = event.target.toDelta();
+      console.log('delta', textDelta);
+      update(textDelta);
+    };
+    yText.applyDelta(delta);
+    yText.observe(textEventHandler);
+
+    return () => {
+      yText.unobserve(textEventHandler);
+    };
+  }, [delta]);
+
+  return { editor };
+}

+ 1 - 1
frontend/appflowy_tauri/src/appflowy_app/components/tests/TestDocument.tsx

@@ -8,7 +8,7 @@ async function testCreateDocument() {
   const document = await svc.open().then((result) => result.unwrap());
 
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
-  const content = JSON.parse(document.content);
+  // const content = JSON.parse(document.content);
   // The initial document content:
   // {
   //   "document": {

+ 15 - 3
frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts

@@ -10,8 +10,19 @@ export enum BlockType {
   DividerBlock = 'divider',
   MediaBlock = 'media',
   TableBlock = 'table',
-  ColumnBlock = 'column'
+  ColumnBlock = 'column',
 }
+
+export interface HeadingBlockData {
+  level: number;
+}
+
+export interface TextBlockData {
+  delta: TextDelta[];
+}
+
+export interface PageBlockData extends TextBlockData {}
+
 export interface NestedBlock {
   id: string;
   type: BlockType;
@@ -26,6 +37,7 @@ export interface TextDelta {
 export interface DocumentData {
   rootId: string;
   blocks: Record<string, NestedBlock>;
-  ytexts: Record<string, TextDelta[]>;
-  yarrays: Record<string, string[]>;
+  meta: {
+    childrenMap: Record<string, string[]>;
+  };
 }

+ 2 - 17
frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_bd_svc.ts

@@ -24,22 +24,7 @@ import {
 export class DocumentBackendService {
   constructor(public readonly viewId: string) {}
 
-  open = (): Promise<Result<DocumentDataPB, FlowyError>> => {
-    const payload = OpenDocumentPayloadPB.fromObject({ document_id: this.viewId, version: DocumentVersionPB.V1 });
-    return DocumentEventGetDocument(payload);
-  };
-
-  applyEdit = (operations: string) => {
-    const payload = EditPayloadPB.fromObject({ doc_id: this.viewId, operations: operations });
-    return DocumentEventApplyEdit(payload);
-  };
-
-  close = () => {
-    const payload = ViewIdPB.fromObject({ value: this.viewId });
-    return FolderEventCloseView(payload);
-  };
-
-  openV2 = (): Promise<Result<DocumentDataPB2, FlowyError>> => {
+  open = (): Promise<Result<DocumentDataPB2, FlowyError>> => {
     const payload = OpenDocumentPayloadPBV2.fromObject({
       document_id: this.viewId,
     });
@@ -54,7 +39,7 @@ export class DocumentBackendService {
     return DocumentEvent2ApplyAction(payload);
   };
 
-  closeV2 = (): Promise<Result<void, FlowyError>> => {
+  close = (): Promise<Result<void, FlowyError>> => {
     const payload = CloseDocumentPayloadPBV2.fromObject({
       document_id: this.viewId,
     });

+ 31 - 49
frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts

@@ -1,10 +1,8 @@
-import { DocumentData, BlockType, TextDelta } from '@/appflowy_app/interfaces/document';
+import { DocumentData, BlockType } from '@/appflowy_app/interfaces/document';
 import { createContext } from 'react';
 import { DocumentBackendService } from './document_bd_svc';
-import { Err } from 'ts-results';
-import { BlockActionPB, BlockActionPayloadPB, BlockActionTypePB, BlockPB, FlowyError } from '@/services/backend';
+import { FlowyError } from '@/services/backend';
 import { DocumentObserver } from './document_observer';
-import { nanoid } from 'nanoid';
 
 export const DocumentControllerContext = createContext<DocumentController | null>(null);
 
@@ -17,7 +15,7 @@ export class DocumentController {
     this.observer = new DocumentObserver(viewId);
   }
 
-  open = async (): Promise<DocumentData | null> => {
+  open = async (): Promise<DocumentData | FlowyError> => {
     // example:
     await this.observer.subscribe({
       didReceiveUpdate: () => {
@@ -25,55 +23,39 @@ export class DocumentController {
       },
     });
 
-    const document = await this.backendService.openV2();
-    let root_id = '';
+    const document = await this.backendService.open();
     if (document.ok) {
-      root_id = document.val.page_id;
-      console.log(document.val.blocks);
-    }
-    await this.backendService.applyActions([
-      BlockActionPB.fromObject({
-        action: BlockActionTypePB.Insert,
-        payload: BlockActionPayloadPB.fromObject({
-          block: BlockPB.fromObject({
-            id: nanoid(10),
-            ty: 'text',
-            parent_id: root_id,
-          }),
-        }),
-      }),
-    ]);
-
-    const openDocumentResult = await this.backendService.open();
-    if (openDocumentResult.ok) {
+      console.log(document.val);
+      const blocks: DocumentData["blocks"] = {};
+      document.val.blocks.forEach((block) => {
+        blocks[block.id] = {
+          id: block.id,
+          type: block.ty as BlockType,
+          parent: block.parent_id,
+          children: block.children_id,
+          data: JSON.parse(block.data),
+        };
+      });
+      const childrenMap: Record<string, string[]> = {};
+      document.val.meta.children_map.forEach((child, key) => { childrenMap[key] = child.children; });
       return {
-        rootId: '',
-        blocks: {},
-        ytexts: {},
-        yarrays: {},
-      };
-    } else {
-      return null;
+        rootId: document.val.page_id,
+        blocks,
+        meta: {
+          childrenMap
+        }
+      }
     }
-  };
+    return document.val;
 
-  insert(
-    node: {
-      id: string;
-      type: BlockType;
-      delta?: TextDelta[];
-    },
-    parentId: string,
-    prevId: string
-  ) {
-    //
-  }
-
-  transact(actions: (() => void)[]) {
-    //
-  }
+  };
 
-  yTextApply = (yTextId: string, delta: TextDelta[]) => {
+  applyActions = (
+    actions: {
+      type: string;
+      payload: any;
+    }[]
+  ) => {
     //
   };
 

+ 2 - 2
frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_observer.ts

@@ -29,8 +29,8 @@ export class DocumentObserver {
   };
 
   unsubscribe = async () => {
-    this.appListNotifier.unsubscribe();
-    this.workspaceNotifier.unsubscribe();
+    // this.appListNotifier.unsubscribe();
+    // this.workspaceNotifier.unsubscribe();
     await this.listener?.stop();
   };
 }

+ 46 - 51
frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts

@@ -1,22 +1,12 @@
-import { BlockType, 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<string, any>
-  };
-  parent: string | null;
-  children: string;
-}
+import { BlockType, NestedBlock, TextDelta } from '@/appflowy_app/interfaces/document';
+import { PayloadAction, createSlice } from '@reduxjs/toolkit';
+import { RegionGrid } from './region_grid';
+
+export type Node = NestedBlock;
 
 export interface NodeState {
   nodes: Record<string, Node>;
   children: Record<string, string[]>;
-  delta: Record<string, TextDelta[]>;
   selections: string[];
 }
 
@@ -25,7 +15,6 @@ const regionGrid = new RegionGrid(50);
 const initialState: NodeState = {
   nodes: {},
   children: {},
-  delta: {},
   selections: [],
 };
 
@@ -33,46 +22,56 @@ export const documentSlice = createSlice({
   name: 'document',
   initialState: initialState,
   reducers: {
-    clear: (state, action: PayloadAction) => {
+    clear: () => {
       return initialState;
     },
 
-    createTree: (state, action: PayloadAction<{
-      nodes: Record<string, Node>;
-      children: Record<string, string[]>;
-      delta: Record<string, TextDelta[]>;
-    }>) => {
-      const { nodes, children, delta } = action.payload;
+    create: (
+      state,
+      action: PayloadAction<{
+        nodes: Record<string, Node>;
+        children: Record<string, string[]>;
+      }>
+    ) => {
+      const { nodes, children } = action.payload;
       state.nodes = nodes;
       state.children = children;
-      state.delta = delta;
     },
 
     updateSelections: (state, action: PayloadAction<string[]>) => {
       state.selections = action.payload;
     },
 
-    changeSelectionByIntersectRect: (state, action: PayloadAction<{
-      startX: number;
-      startY: number;
-      endX: number;
-      endY: number
-    }>) => {
+    setSelectionByRect: (
+      state,
+      action: PayloadAction<{
+        startX: number;
+        startY: number;
+        endX: number;
+        endY: number;
+      }>
+    ) => {
       const { startX, startY, endX, endY } = action.payload;
       const blocks = regionGrid.getIntersectBlocks(startX, startY, endX, endY);
-      state.selections = blocks.map(block => block.id);
+      state.selections = blocks.map((block) => block.id);
     },
 
-    updateNodePosition: (state, action: PayloadAction<{id: string; rect: {
-      x: number;
-      y: number;
-      width: number;
-      height: number;
-    }}>) => {
+    updateNodePosition: (
+      state,
+      action: PayloadAction<{
+        id: string;
+        rect: {
+          x: number;
+          y: number;
+          width: number;
+          height: number;
+        };
+      }>
+    ) => {
       const { id, rect } = action.payload;
       const position = {
         id,
-        ...rect
+        ...rect,
       };
       regionGrid.updateBlock(id, position);
     },
@@ -81,13 +80,13 @@ export const documentSlice = createSlice({
       state.nodes[action.payload.id] = action.payload;
     },
 
-    addChild: (state, action: PayloadAction<{ parentId: string, childId: string, prevId: string }>) => {
+    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)
+        children.push(childId);
       } else {
         children.splice(prevIndex + 1, 0, childId);
       }
@@ -98,32 +97,28 @@ export const documentSlice = createSlice({
       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 }>) => {
+    updateNode: (state, action: PayloadAction<{ id: string; data: any }>) => {
       state.nodes[action.payload.id] = {
         ...state.nodes[action.payload.id],
-        ...action.payload
-      }
+        ...action.payload,
+      };
     },
 
     removeNode: (state, action: PayloadAction<string>) => {
       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];
       }
-      if (data && data.text) {
-        delete state.delta[data.text];
-      }
+
+      // remove node
       delete state.nodes[action.payload];
     },
   },

+ 1 - 1
frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts

@@ -23,7 +23,7 @@ export const useDocument = () => {
       const res = await c.open();
       console.log(res)
       if (!res) return;
-      setDocumentData(res)
+      // setDocumentData(res)
       setDocumentId(params.id)
       
     })();

+ 2 - 2
frontend/rust-lib/flowy-document2/src/document.rs

@@ -14,7 +14,7 @@ use flowy_error::{ErrorCode, FlowyError, FlowyResult};
 use nanoid::nanoid;
 use parking_lot::Mutex;
 
-use crate::entities::{BlockMapPB, BlockPB, ChildrenPB, DocumentDataPB2, MetaPB};
+use crate::entities::{BlockPB, ChildrenPB, DocumentDataPB2, MetaPB};
 
 #[derive(Clone)]
 pub struct Document(Arc<Mutex<InnerDocument>>);
@@ -96,7 +96,7 @@ impl From<DocumentDataWrapper> for DocumentDataPB2 {
       .collect::<HashMap<String, ChildrenPB>>();
     Self {
       page_id: data.0.page_id,
-      blocks: BlockMapPB { blocks },
+      blocks,
       meta: MetaPB { children_map },
     }
   }

+ 1 - 7
frontend/rust-lib/flowy-document2/src/entities.rs

@@ -31,18 +31,12 @@ pub struct DocumentDataPB2 {
   pub page_id: String,
 
   #[pb(index = 2)]
-  pub blocks: BlockMapPB,
+  pub blocks: HashMap<String, BlockPB>,
 
   #[pb(index = 3)]
   pub meta: MetaPB,
 }
 
-#[derive(Default, ProtoBuf)]
-pub struct BlockMapPB {
-  #[pb(index = 1)]
-  pub blocks: HashMap<String, BlockPB>,
-}
-
 #[derive(Default, ProtoBuf)]
 pub struct BlockPB {
   #[pb(index = 1)]

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä