Jelajahi Sumber

refactor: document controller

qinluhe 2 tahun lalu
induk
melakukan
886766c887
25 mengubah file dengan 253 tambahan dan 532 penghapusan
  1. 4 4
      frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockSelection.hooks.tsx
  2. 2 2
      frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideTools/BlockSideTools.hooks.tsx
  3. 4 4
      frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx
  4. 2 2
      frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx
  5. 0 61
      frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts
  6. 16 72
      frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts
  7. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx
  8. 3 3
      frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizedList.hooks.tsx
  9. 6 7
      frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx
  10. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/FormatButton.tsx
  11. 0 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/FormatIcon.tsx
  12. 4 5
      frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.hooks.ts
  13. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/HoveringToolbar/index.tsx
  14. 15 1
      frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts
  15. 116 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TextInput.hooks.ts
  16. 0 5
      frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.hooks.ts
  17. 0 14
      frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts
  18. 6 2
      frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts
  19. 1 112
      frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts
  20. 27 173
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts
  21. 36 14
      frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts
  22. 0 36
      frontend/appflowy_tauri/src/appflowy_app/utils/block_selection.ts
  23. 0 6
      frontend/appflowy_tauri/src/appflowy_app/utils/slate/context.ts
  24. 5 3
      frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts
  25. 3 3
      frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx

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

+ 2 - 2
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;

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

+ 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 '../VirtualizerList';
 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: Delta) => 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 Delta);
-    }
-    yText.applyDelta(delta);
-    yText.observe(textEventHandler);
-  
-    return () => {
-      yText.unobserve(textEventHandler);
-    }
-  }, [delta])
-  
-
-  return { editor }
-}

+ 16 - 72
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<Descendant[]>([]);
-  
+
   const onChange = useCallback(
     (e: Descendant[]) => {
       setValue(e);
     },
-    [editor],
+    [editor]
   );
 
   const onKeyDownCapture = (event: React.KeyboardEvent<HTMLDivElement>) => {
@@ -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,
+  };
 }

+ 1 - 1
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';
 

+ 3 - 3
frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizerList.hooks.tsx → 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<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: Virtualize,
     parentRef,
   };
 }

+ 6 - 7
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({
         <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 - 5
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,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
-  }
-}
+    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 }) => {

+ 15 - 1
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<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;
+    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<boolean>(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)]);

+ 116 - 0
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<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) => {
+      update(event.changes.delta as TextDelta[]);
+    };
+    yText.applyDelta(delta);
+    yText.observe(textEventHandler);
+
+    return () => {
+      yText.unobserve(textEventHandler);
+    };
+  }, [delta]);
+
+  return { editor };
+}

+ 0 - 5
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,

+ 0 - 14
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<string, { title: string; key: string }> = {
     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,
-  ],
-};

+ 6 - 2
frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts

@@ -16,6 +16,8 @@ export interface NestedBlock {
   id: string;
   type: BlockType;
   data: Record<string, any>;
+  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<string, NestedBlock>;
-  ytexts: Record<string, TextDelta[]>;
-  yarrays: Record<string, string[]>;
+  meta: {
+    text_map: Record<string, TextDelta[]>;
+    children_map: Record<string, string[]>;
+  }
 }

+ 1 - 112
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 = BlockType> = 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<T = BlockType> {
-  id: string;
-  type: BlockType;
-  data: BlockData<T>;
-  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<T> {
-  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
-}
+export interface Document {}

+ 27 - 173
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<string, unknown>;
-};
+export const DocumentControllerContext = createContext<DocumentController | null>(null);
 
-export type DeltaRetain = { retain: number };
-export type DeltaDelete = { delete: number };
-export type DeltaInsert = {
-  insert: string | Y.XmlText;
-  attributes?: Record<string, unknown>;
-};
+export class DocumentController {
+  private readonly backendService: DocumentBackendService;
 
-export type InsertDelta = Array<DeltaInsert>;
-export type Delta = Array<
-  DeltaRetain | DeltaDelete | DeltaInsert | DeltaAttributes
->;
-
-
-export const YDocControllerContext = createContext<YDocController | null>(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);
-  }
-
-  handleUpdate = (update: Uint8Array, origin: any) => {
-    const isLocal = origin === null;
-    Y.logUpdate(update);
+  constructor(public readonly viewId: string) {
+    this.backendService = new DocumentBackendService(viewId);
   }
 
-
-  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<DocumentData | null> => {
+    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<DocumentData> => {
-    await this.provider.whenSynced;
-    const ydoc = this._ydoc;
-    
-    const blocks = ydoc.getMap('blocks');
-    const obj: DocumentData = {
-      rootId: ydoc.getArray<string>('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<string>(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<NestedBlock>('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<unknown>) => {
-    console.log(mapEvent.changes);
-  }
-
-  private handleTextEvent = (textEvent: Y.YTextEvent) => {
-    console.log(textEvent.changes);
+    //
   }
 
-  private handleArrayEvent = (arrayEvent: Y.YArrayEvent<string>) => {
-    console.log(arrayEvent.changes);
+  yTextApply = (yTextId: string, delta: TextDelta[]) => {
+    //
   }
 
+  dispose = async () => {
+    await this.backendService.close();
+  };
 }

+ 36 - 14
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<string, any>
-  };
-  parent: string | null;
-  children: string;
-}
+export type Node = NestedBlock;
 
 export interface NodeState {
   nodes: Record<string, Node>;
@@ -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<string, Node>;
       children: Record<string, string[]>;
       delta: Record<string, TextDelta[]>;
@@ -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<Node>) => {
+      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<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];
       }
+      // remove delta
       if (data && data.text) {
         delete state.delta[data.text];
       }
+      // remove node
       delete state.nodes[action.payload];
     },
   },

+ 0 - 36
frontend/appflowy_tauri/src/appflowy_app/utils/block_selection.ts

@@ -1,36 +0,0 @@
-import { BlockData, BlockType } from "../interfaces";
-
-
-export function filterSelections<TreeNode extends {
-  id: string;
-  children: TreeNode[];
-  parent: TreeNode | null;
-  type: BlockType;
-  data: BlockData;
-}>(ids: string[], nodeMap: Map<string, TreeNode>): string[] {
-  const selected = new Set(ids);
-  const newSelected = new Set<string>();
-  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);
-}

+ 0 - 6
frontend/appflowy_tauri/src/appflowy_app/utils/slate/context.ts

@@ -1,6 +0,0 @@
-import { createContext } from "react";
-import { TextBlockManager } from '../../block_editor/blocks/text_block';
-
-export const TextBlockContext = createContext<{
-  textBlockManager?: TextBlockManager
-}>({});

+ 5 - 3
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<string>();
   const [ documentData, setDocumentData ] = useState<DocumentData>();
-  const [ controller, setController ] = useState<YDocController | null>(null);
+  const [ controller, setController ] = useState<DocumentController | null>(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)

+ 3 - 3
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 (
     <ThemeProvider theme={theme}>
-      <YDocControllerContext.Provider value={controller}>
+      <DocumentControllerContext.Provider value={controller}>
         <Root documentData={documentData} />
-      </YDocControllerContext.Provider>
+      </DocumentControllerContext.Provider>
     </ThemeProvider>
   );
 };