Browse Source

refactor: document data and update

qinluhe 2 years ago
parent
commit
df66521f13
36 changed files with 996 additions and 802 deletions
  1. 6 2
      frontend/appflowy_tauri/package.json
  2. 77 8
      frontend/appflowy_tauri/pnpm-lock.yaml
  3. 0 0
      frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/FormatButton.tsx
  4. 0 0
      frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/FormatIcon.tsx
  5. 0 0
      frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/index.hooks.ts
  6. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/index.tsx
  7. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx
  8. 9 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/BlockPortal/index.tsx
  9. 7 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/DocumentTitle.hooks.ts
  10. 14 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/index.tsx
  11. 32 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatButton.tsx
  12. 20 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatIcon.tsx
  13. 34 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts
  14. 30 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.tsx
  15. 11 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/Node/Node.hooks.ts
  16. 37 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx
  17. 16 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Root.hooks.tsx
  18. 63 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx
  19. 32 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx
  20. 62 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/BindYjs.hooks.ts
  21. 41 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/Leaf.tsx
  22. 75 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/TextBlock.hooks.ts
  23. 40 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx
  24. 21 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizerList.hooks.tsx
  25. 52 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx
  26. 12 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/ErrorBoundaryFallbackComponent.tsx
  27. 16 0
      frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts
  28. 5 0
      frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.hooks.ts
  29. 31 0
      frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts
  30. 120 0
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts
  31. 63 0
      frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts
  32. 2 0
      frontend/appflowy_tauri/src/appflowy_app/stores/store.ts
  33. 9 0
      frontend/appflowy_tauri/src/appflowy_app/utils/block.ts
  34. 37 0
      frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts
  35. 12 778
      frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts
  36. 8 12
      frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx

+ 6 - 2
frontend/appflowy_tauri/package.json

@@ -20,6 +20,7 @@
     "@mui/icons-material": "^5.11.11",
     "@mui/material": "^5.11.12",
     "@reduxjs/toolkit": "^1.9.2",
+    "@slate-yjs/core": "^0.3.1",
     "@tanstack/react-virtual": "3.0.0-beta.54",
     "@tauri-apps/api": "^1.2.0",
     "events": "^3.3.0",
@@ -42,8 +43,9 @@
     "slate": "^0.91.4",
     "slate-react": "^0.91.9",
     "ts-results": "^3.3.0",
-    "ulid": "^2.3.0",
-    "utf8": "^3.0.0"
+    "utf8": "^3.0.0",
+    "yjs": "^13.5.51",
+		"y-indexeddb": "^9.0.9"
   },
   "devDependencies": {
     "@tauri-apps/cli": "^1.2.2",
@@ -53,6 +55,7 @@
     "@types/react": "^18.0.15",
     "@types/react-dom": "^18.0.6",
     "@types/utf8": "^3.0.1",
+    "@types/uuid": "^9.0.1",
     "@typescript-eslint/eslint-plugin": "^5.51.0",
     "@typescript-eslint/parser": "^5.51.0",
     "@vitejs/plugin-react": "^3.0.0",
@@ -64,6 +67,7 @@
     "prettier-plugin-tailwindcss": "^0.2.2",
     "tailwindcss": "^3.2.7",
     "typescript": "^4.6.4",
+    "uuid": "^9.0.0",
     "vite": "^4.0.0"
   }
 }

+ 77 - 8
frontend/appflowy_tauri/pnpm-lock.yaml

@@ -6,6 +6,7 @@ specifiers:
   '@mui/icons-material': ^5.11.11
   '@mui/material': ^5.11.12
   '@reduxjs/toolkit': ^1.9.2
+  '@slate-yjs/core': ^0.3.1
   '@tanstack/react-virtual': 3.0.0-beta.54
   '@tauri-apps/api': ^1.2.0
   '@tauri-apps/cli': ^1.2.2
@@ -15,6 +16,7 @@ specifiers:
   '@types/react': ^18.0.15
   '@types/react-dom': ^18.0.6
   '@types/utf8': ^3.0.1
+  '@types/uuid': ^9.0.1
   '@typescript-eslint/eslint-plugin': ^5.51.0
   '@typescript-eslint/parser': ^5.51.0
   '@vitejs/plugin-react': ^3.0.0
@@ -31,6 +33,7 @@ specifiers:
   postcss: ^8.4.21
   prettier: 2.8.4
   prettier-plugin-tailwindcss: ^0.2.2
+  protoc-gen-ts: ^0.8.5
   react: ^18.2.0
   react-dom: ^18.2.0
   react-error-boundary: ^3.1.4
@@ -45,9 +48,11 @@ specifiers:
   tailwindcss: ^3.2.7
   ts-results: ^3.3.0
   typescript: ^4.6.4
-  ulid: ^2.3.0
   utf8: ^3.0.0
+  uuid: ^9.0.0
   vite: ^4.0.0
+  y-indexeddb: ^9.0.9
+  yjs: ^13.5.51
 
 dependencies:
   '@emotion/react': 11.10.6_pmekkgnqduwlme35zpnqhenc34
@@ -55,6 +60,7 @@ dependencies:
   '@mui/icons-material': 5.11.11_ao76n7r2cajsoyr3cbwrn7geoi
   '@mui/material': 5.11.12_xqeqsl5kvjjtyxwyi3jhw3yuli
   '@reduxjs/toolkit': 1.9.3_k4ae6lp43ej6mezo3ztvx6pykq
+  '@slate-yjs/core': [email protected][email protected]
   '@tanstack/react-virtual': [email protected]
   '@tauri-apps/api': 1.2.0
   events: 3.3.0
@@ -64,6 +70,7 @@ dependencies:
   is-hotkey: 0.2.0
   jest: 29.5.0_@[email protected]
   nanoid: 4.0.1
+  protoc-gen-ts: 0.8.6_ss7alqtodw6rv4lluxhr36xjoa
   react: 18.2.0
   react-dom: [email protected]
   react-error-boundary: [email protected]
@@ -76,8 +83,9 @@ dependencies:
   slate: 0.91.4
   slate-react: 0.91.9_6tgy34rvmll7duwkm4ydcekf3u
   ts-results: 3.3.0
-  ulid: 2.3.0
   utf8: 3.0.0
+  y-indexeddb: [email protected]
+  yjs: 13.5.51
 
 devDependencies:
   '@tauri-apps/cli': 1.2.3
@@ -87,6 +95,7 @@ devDependencies:
   '@types/react': 18.0.28
   '@types/react-dom': 18.0.11
   '@types/utf8': 3.0.1
+  '@types/uuid': 9.0.1
   '@typescript-eslint/eslint-plugin': 5.54.0_6mj2wypvdnknez7kws2nfdgupi
   '@typescript-eslint/parser': 5.54.0_ycpbpc6yetojsgtrx3mwntkhsu
   '@vitejs/plugin-react': [email protected]
@@ -98,6 +107,7 @@ devDependencies:
   prettier-plugin-tailwindcss: [email protected]
   tailwindcss: [email protected]
   typescript: 4.9.5
+  uuid: 9.0.0
   vite: 4.1.4_@[email protected]
 
 packages:
@@ -1308,6 +1318,17 @@ packages:
       '@sinonjs/commons': 2.0.0
     dev: false
 
+  /@slate-yjs/core/[email protected][email protected]:
+    resolution: {integrity: sha512-8nvS9m5FhMNONgydAfzwDCUhuoWbgzx5Bvw1/foSe+JO331UOT1xAKbUX5FzGCOunUcbRjMPXSdNyiPc0dodJg==}
+    peerDependencies:
+      slate: '>=0.70.0'
+      yjs: ^13.5.29
+    dependencies:
+      slate: 0.91.4
+      y-protocols: 1.0.5
+      yjs: 13.5.51
+    dev: false
+
   /@tanstack/react-virtual/[email protected]:
     resolution: {integrity: sha512-D1mDMf4UPbrtHRZZriCly5bXTBMhylslm4dhcHqTtDJ6brQcgGmk8YD9JdWBGWfGSWPKoh2x1H3e7eh+hgPXtQ==}
     peerDependencies:
@@ -1553,6 +1574,10 @@ packages:
     resolution: {integrity: sha512-1EkWuw7rT3BMz2HpmcEOr/HL61mWNA6Ulr/KdbXR9AI0A55wD4Qfv8hizd8Q1DnknSIzzDvQmvvY/guvX7jjZA==}
     dev: true
 
+  /@types/uuid/9.0.1:
+    resolution: {integrity: sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==}
+    dev: true
+
   /@types/yargs-parser/21.0.0:
     resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
     dev: false
@@ -3050,6 +3075,10 @@ packages:
   /isexe/2.0.0:
     resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
 
+  /isomorphic.js/0.2.5:
+    resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==}
+    dev: false
+
   /istanbul-lib-coverage/3.2.0:
     resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==}
     engines: {node: '>=8'}
@@ -3575,6 +3604,14 @@ packages:
       type-check: 0.4.0
     dev: true
 
+  /lib0/0.2.73:
+    resolution: {integrity: sha512-aJJIElCLWnHMcYZPtsM07QoSfHwpxCy4VUzBYGXFYEmh/h2QS5uZNbCCfL0CqnkOE30b7Tp9DVfjXag+3qzZjQ==}
+    engines: {node: '>=14'}
+    hasBin: true
+    dependencies:
+      isomorphic.js: 0.2.5
+    dev: false
+
   /lilconfig/2.1.0:
     resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
     engines: {node: '>=10'}
@@ -4055,6 +4092,17 @@ packages:
       object-assign: 4.1.1
       react-is: 16.13.1
 
+  /protoc-gen-ts/0.8.6_ss7alqtodw6rv4lluxhr36xjoa:
+    resolution: {integrity: sha512-66oeorGy4QBvYjQGd/gaeOYyFqKyRmRgTpofmnw8buMG0P7A0jQjoKSvKJz5h5tNUaVkIzvGBUTRVGakrhhwpA==}
+    hasBin: true
+    peerDependencies:
+      google-protobuf: ^3.13.0
+      typescript: 4.x.x
+    dependencies:
+      google-protobuf: 3.21.2
+      typescript: 4.9.5
+    dev: false
+
   /punycode/2.3.0:
     resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
     engines: {node: '>=6'}
@@ -4678,12 +4726,6 @@ packages:
     resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==}
     engines: {node: '>=4.2.0'}
     hasBin: true
-    dev: true
-
-  /ulid/2.3.0:
-    resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==}
-    hasBin: true
-    dev: false
 
   /unbox-primitive/1.0.2:
     resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
@@ -4726,6 +4768,11 @@ packages:
     resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
     dev: true
 
+  /uuid/9.0.0:
+    resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==}
+    hasBin: true
+    dev: true
+
   /v8-to-istanbul/9.1.0:
     resolution: {integrity: sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==}
     engines: {node: '>=10.12.0'}
@@ -4839,6 +4886,21 @@ packages:
     engines: {node: '>=0.4'}
     dev: true
 
+  /y-indexeddb/[email protected]:
+    resolution: {integrity: sha512-GcJbiJa2eD5hankj46Hea9z4hbDnDjvh1fT62E5SpZRsv8GcEemw34l1hwI2eknGcv5Ih9JfusT37JLx9q3LFg==}
+    peerDependencies:
+      yjs: ^13.0.0
+    dependencies:
+      lib0: 0.2.73
+      yjs: 13.5.51
+    dev: false
+
+  /y-protocols/1.0.5:
+    resolution: {integrity: sha512-Wil92b7cGk712lRHDqS4T90IczF6RkcvCwAD0A2OPg+adKmOe+nOiT/N2hvpQIWS3zfjmtL4CPaH5sIW1Hkm/A==}
+    dependencies:
+      lib0: 0.2.73
+    dev: false
+
   /y18n/5.0.8:
     resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
     engines: {node: '>=10'}
@@ -4872,6 +4934,13 @@ packages:
       yargs-parser: 21.1.1
     dev: false
 
+  /yjs/13.5.51:
+    resolution: {integrity: sha512-F1Nb3z3TdandD80IAeQqgqy/2n9AhDLcXoBhZvCUX1dNVe0ef7fIwi6MjSYaGAYF2Ev8VcLcsGnmuGGOl7AWbw==}
+    engines: {node: '>=16.0.0', npm: '>=8.0.0'}
+    dependencies:
+      lib0: 0.2.73
+    dev: false
+
   /yocto-queue/0.1.0:
     resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
     engines: {node: '>=10'}

+ 0 - 0
frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatButton.tsx → frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/FormatButton.tsx


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


+ 0 - 0
frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.hooks.ts → frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/index.hooks.ts


+ 1 - 1
frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx → frontend/appflowy_tauri/src/appflowy_app/components/block/HoveringToolbar/index.tsx

@@ -1,5 +1,5 @@
 import FormatButton from './FormatButton';
-import Portal from '../block/BlockPortal';
+import Portal from '../BlockPortal';
 import { TreeNode } from '$app/block_editor/view/tree_node';
 import { useHoveringToolbar } from './index.hooks';
 

+ 1 - 1
frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx

@@ -1,7 +1,7 @@
 import BlockComponent from '../BlockComponent';
 import { Slate, Editable } from 'slate-react';
 import Leaf from './Leaf';
-import HoveringToolbar from '$app/components/HoveringToolbar';
+import HoveringToolbar from '@/appflowy_app/components/block/HoveringToolbar';
 import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node';
 import { useTextBlock } from './index.hooks';
 import { BlockCommonProps, TextBlockToolbarProps } from '@/appflowy_app/interfaces';

+ 9 - 0
frontend/appflowy_tauri/src/appflowy_app/components/document/BlockPortal/index.tsx

@@ -0,0 +1,9 @@
+import ReactDOM from 'react-dom';
+
+const BlockPortal = ({ blockId, children }: { blockId: string; children: JSX.Element }) => {
+  const root = document.querySelectorAll(`[data-block-id=${blockId}] > .block-overlay`)[0];
+
+  return typeof document === 'object' && root ? ReactDOM.createPortal(children, root) : null;
+};
+
+export default BlockPortal;

+ 7 - 0
frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/DocumentTitle.hooks.ts

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

+ 14 - 0
frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentTitle/index.tsx

@@ -0,0 +1,14 @@
+import React from 'react';
+import { useDocumentTitle } from './DocumentTitle.hooks';
+import TextBlock from '../TextBlock';
+
+export default function DocumentTitle({ id }: { id: string }) {
+  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={[]} node={node} />
+    </div>
+  );
+}

+ 32 - 0
frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/FormatButton.tsx

@@ -0,0 +1,32 @@
+import { toggleFormat, isFormatActive } from '@/appflowy_app/utils/slate/format';
+import IconButton from '@mui/material/IconButton';
+import Tooltip from '@mui/material/Tooltip';
+
+import { command } from '$app/constants/toolbar';
+import FormatIcon from './FormatIcon';
+import { BaseEditor } from 'slate';
+
+const FormatButton = ({ editor, format, icon }: { editor: BaseEditor; format: string; icon: string }) => {
+  return (
+    <Tooltip
+      slotProps={{ tooltip: { style: { background: '#E0F8FF', borderRadius: 8 } } }}
+      title={
+        <div className='flex flex-col'>
+          <span className='text-base font-medium text-black'>{command[format].title}</span>
+          <span className='text-sm text-slate-400'>{command[format].key}</span>
+        </div>
+      }
+      placement='top-start'
+    >
+      <IconButton
+        size='small'
+        sx={{ color: isFormatActive(editor, format) ? '#00BCF0' : 'white' }}
+        onClick={() => toggleFormat(editor, format)}
+      >
+        <FormatIcon icon={icon} />
+      </IconButton>
+    </Tooltip>
+  );
+};
+
+export default FormatButton;

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

@@ -0,0 +1,20 @@
+import React from 'react';
+import { FormatBold, FormatUnderlined, FormatItalic, CodeOutlined, StrikethroughSOutlined } from '@mui/icons-material';
+import { iconSize } from '$app/constants/toolbar';
+
+export default function FormatIcon({ icon }: { icon: string }) {
+  switch (icon) {
+    case 'bold':
+      return <FormatBold sx={iconSize} />;
+    case 'underlined':
+      return <FormatUnderlined sx={iconSize} />;
+    case 'italic':
+      return <FormatItalic sx={iconSize} />;
+    case 'code':
+      return <CodeOutlined sx={iconSize} />;
+    case 'strikethrough':
+      return <StrikethroughSOutlined sx={iconSize} />;
+    default:
+      return null;
+  }
+}

+ 34 - 0
frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.hooks.ts

@@ -0,0 +1,34 @@
+import { useEffect, useRef } from 'react';
+import { useFocused, useSlate } from 'slate-react';
+import { calcToolbarPosition } from '@/appflowy_app/utils/slate/toolbar';
+
+
+export function useHoveringToolbar(id: string) {
+  const editor = useSlate();
+  const inFocus = useFocused();
+  const ref = useRef<HTMLDivElement | null>(null);
+
+  useEffect(() => {
+    const el = ref.current;
+    if (!el) return;
+    const nodeRect = document.querySelector(`[data-block-id=${id}]`)?.getBoundingClientRect();
+
+    if (!nodeRect) return;
+    const position = calcToolbarPosition(editor, el, nodeRect);
+
+    if (!position) {
+      el.style.opacity = '0';
+      el.style.zIndex = '-1';
+    } else {
+      el.style.opacity = '1';
+      el.style.zIndex = '1';
+      el.style.top = position.top;
+      el.style.left = position.left;
+    }
+  });
+  return {
+    ref,
+    inFocus,
+    editor
+  }
+}

+ 30 - 0
frontend/appflowy_tauri/src/appflowy_app/components/document/HoveringToolbar/index.tsx

@@ -0,0 +1,30 @@
+import FormatButton from './FormatButton';
+import Portal from '../BlockPortal';
+import { useHoveringToolbar } from './index.hooks';
+
+const HoveringToolbar = ({ id }: { id: string }) => {
+  const { inFocus, ref, editor } = useHoveringToolbar(id);
+  if (!inFocus) return null;
+
+  return (
+    <Portal blockId={id}>
+      <div
+        ref={ref}
+        style={{
+          opacity: 0,
+        }}
+        className='z-1 absolute mt-[-6px] inline-flex h-[32px] items-stretch overflow-hidden rounded-[8px] bg-[#333] p-2 leading-tight shadow-lg transition-opacity duration-700'
+        onMouseDown={(e) => {
+          // prevent toolbar from taking focus away from editor
+          e.preventDefault();
+        }}
+      >
+        {['bold', 'italic', 'underlined', 'strikethrough', 'code'].map((format) => (
+          <FormatButton key={format} editor={editor} format={format} icon={format} />
+        ))}
+      </div>
+    </Portal>
+  );
+};
+
+export default HoveringToolbar;

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

@@ -0,0 +1,11 @@
+
+import { useSubscribeNode } from '../_shared/SubscribeNode.hooks';
+
+export function useNode(id: string) {
+  const { node, childIds } = useSubscribeNode(id);
+
+  return {
+    node,
+    childIds,
+  }
+}

+ 37 - 0
frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx

@@ -0,0 +1,37 @@
+import React, { useCallback } from 'react';
+import { useNode } from './Node.hooks';
+import { withErrorBoundary } from 'react-error-boundary';
+import { ErrorBoundaryFallbackComponent } from '../_shared/ErrorBoundaryFallbackComponent';
+import { Node } from '@/appflowy_app/stores/reducers/document/slice';
+import TextBlock from '../TextBlock';
+
+function NodeComponent({ id }: { id: string }) {
+  const { node, childIds } = useNode(id);
+
+  const renderBlock = useCallback((props: { node: Node; childIds?: string[] }) => {
+    switch (props.node.type) {
+      case 'text':
+        return <TextBlock {...props} />;
+      default:
+        break;
+    }
+  }, []);
+
+  if (!node) return null;
+
+  return (
+    <div data-block-id={node.id} className='relative my-[1px]'>
+      {renderBlock({
+        node,
+        childIds,
+      })}
+      <div className='block-overlay' />
+    </div>
+  );
+}
+
+const NodeWithErrorBoundary = withErrorBoundary(NodeComponent, {
+  FallbackComponent: ErrorBoundaryFallbackComponent,
+});
+
+export default React.memo(NodeWithErrorBoundary);

+ 16 - 0
frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Root.hooks.tsx

@@ -0,0 +1,16 @@
+import { DocumentData } from '$app/interfaces/document';
+import { useSubscribeNode } from '../_shared/SubscribeNode.hooks';
+import { useParseTree } from './Tree.hooks';
+
+export function useRoot({ documentData }: { documentData: DocumentData }) {
+  const { rootId } = documentData;
+
+  useParseTree(documentData);
+
+  const { node: rootNode, childIds: rootChildIds } = useSubscribeNode(rootId);
+
+  return {
+    node: rootNode,
+    childIds: rootChildIds,
+  };
+}

+ 63 - 0
frontend/appflowy_tauri/src/appflowy_app/components/document/Root/Tree.hooks.tsx

@@ -0,0 +1,63 @@
+import { useEffect } from 'react';
+import { DocumentData, NestedBlock } from '$app/interfaces/document';
+import { useAppDispatch } from '@/appflowy_app/stores/store';
+import { documentActions, Node } from '$app/stores/reducers/document/slice';
+
+export function useParseTree(documentData: DocumentData) {
+  const dispatch = useAppDispatch();
+  const { blocks, ytexts, yarrays, rootId } = documentData;
+  const flattenNestedBlocks = (
+    block: NestedBlock
+  ): (Node & {
+    children: string[];
+  })[] => {
+    const node: Node & {
+      children: string[];
+    } = {
+      id: block.id,
+      delta: ytexts[block.data.text],
+      data: block.data,
+      type: block.type,
+      parent: block.parent,
+      children: yarrays[block.children],
+    };
+
+    const nodes = [node];
+    node.children.forEach((child) => {
+      const childBlock = blocks[child];
+      nodes.push(...flattenNestedBlocks(childBlock));
+    });
+    return nodes;
+  };
+
+  const initializeNodeHierarchy = (parentId: string, children: string[]) => {
+    children.forEach((childId) => {
+      dispatch(documentActions.addChild({ parentId, childId }));
+      const child = blocks[childId];
+      initializeNodeHierarchy(childId, yarrays[child.children]);
+    });
+  };
+
+  useEffect(() => {
+    const root = documentData.blocks[rootId];
+
+    const initialNodes = flattenNestedBlocks(root);
+
+    initialNodes.forEach((node) => {
+      const _node = {
+        id: node.id,
+        parent: node.parent,
+        data: node.data,
+        type: node.type,
+        delta: node.delta,
+      };
+      dispatch(documentActions.addNode(_node));
+    });
+
+    initializeNodeHierarchy(rootId, yarrays[root.children]);
+
+    return () => {
+      dispatch(documentActions.clear());
+    };
+  }, [documentData]);
+}

+ 32 - 0
frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx

@@ -0,0 +1,32 @@
+import { DocumentData } from '@/appflowy_app/interfaces/document';
+import React, { useCallback } from 'react';
+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 { Skeleton } from '@mui/material';
+
+function Root({ documentData }: { documentData: DocumentData }) {
+  const { node, childIds } = useRoot({ documentData });
+
+  const renderNode = useCallback((nodeId: string) => {
+    return <Node key={nodeId} id={nodeId} />;
+  }, []);
+
+  if (!node || !childIds) {
+    return <Skeleton />;
+  }
+
+  return (
+    <div id='appflowy-block-doc' className='h-[100%] overflow-hidden'>
+      <VirtualizerList node={node} childIds={childIds} renderNode={renderNode} />
+    </div>
+  );
+}
+
+const RootWithErrorBoundary = withErrorBoundary(Root, {
+  FallbackComponent: ErrorBoundaryFallbackComponent,
+});
+
+export default React.memo(RootWithErrorBoundary);

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

@@ -0,0 +1,62 @@
+
+
+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) => {
+      console.log(event.delta, event.target.toDelta());
+      update(event.delta as Delta);
+    }
+    yText.applyDelta(delta);
+    yText.observe(textEventHandler);
+  
+    return () => {
+      yText.unobserve(textEventHandler);
+    }
+  }, [delta])
+  
+
+  return { editor }
+}

+ 41 - 0
frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/Leaf.tsx

@@ -0,0 +1,41 @@
+import { BaseText } from 'slate';
+import { RenderLeafProps } from 'slate-react';
+
+const Leaf = ({
+  attributes,
+  children,
+  leaf,
+}: RenderLeafProps & {
+  leaf: BaseText & {
+    bold?: boolean;
+    code?: boolean;
+    italic?: boolean;
+    underlined?: boolean;
+    strikethrough?: boolean;
+  };
+}) => {
+  let newChildren = children;
+  if (leaf.bold) {
+    newChildren = <strong>{children}</strong>;
+  }
+
+  if (leaf.code) {
+    newChildren = <code className='rounded-sm	 bg-[#F2FCFF] p-1'>{newChildren}</code>;
+  }
+
+  if (leaf.italic) {
+    newChildren = <em>{newChildren}</em>;
+  }
+
+  if (leaf.underlined) {
+    newChildren = <u>{newChildren}</u>;
+  }
+
+  return (
+    <span {...attributes} className={leaf.strikethrough ? `line-through` : ''}>
+      {newChildren}
+    </span>
+  );
+};
+
+export default Leaf;

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

@@ -0,0 +1,75 @@
+import { triggerHotkey } from "@/appflowy_app/utils/slate/hotkey";
+import { useCallback, useContext, 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';
+
+function useController(textId: string) {
+  const docController = useContext(YDocControllerContext);
+  
+  const update  = useCallback(
+    (delta: Delta) => {
+      docController?.yTextApply(textId, delta)
+    },
+    [textId],
+  )
+  
+  return {
+    update
+  }
+}
+
+export function useTextBlock(text: string, delta: TextDelta[]) {
+  const { update } = useController(text);
+  const { editor } = useBindYjs(delta, update);
+  const [value, setValue] = useState<Descendant[]>([]);
+  
+  const onChange = useCallback(
+    (e: Descendant[]) => {
+      setValue(e);
+    },
+    [editor],
+  );
+
+  const onKeyDownCapture = (event: React.KeyboardEvent<HTMLDivElement>) => {
+    switch (event.key) {
+      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') {
+          event.stopPropagation();
+          event.preventDefault();
+          return;
+        }
+      }
+    }
+    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
+    if (e.inputType === 'insertFromComposition') {
+      e.preventDefault();
+    }
+
+  }, []);
+
+  return {
+    onChange,
+    onKeyDownCapture,
+    onDOMBeforeInput,
+    editor,
+    value
+  }
+}

+ 40 - 0
frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/index.tsx

@@ -0,0 +1,40 @@
+import { Slate, Editable } from 'slate-react';
+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';
+
+export default function TextBlock({
+  node,
+  childIds,
+  placeholder,
+  ...props
+}: {
+  node: Node;
+  childIds?: string[];
+  placeholder?: string;
+} & React.HTMLAttributes<HTMLDivElement>) {
+  const { editor, value, onChange, onKeyDownCapture, onDOMBeforeInput } = useTextBlock(node.data.text!, node.delta);
+
+  return (
+    <div {...props} className={props.className}>
+      <Slate editor={editor} onChange={onChange} value={value}>
+        <HoveringToolbar id={node.id} />
+        <Editable
+          onKeyDownCapture={onKeyDownCapture}
+          onDOMBeforeInput={onDOMBeforeInput}
+          renderLeaf={(leafProps) => <Leaf {...leafProps} />}
+          placeholder={placeholder || 'Please enter some text...'}
+        />
+      </Slate>
+      {childIds && childIds.length > 0 ? (
+        <div className='pl-[1.5em]'>
+          {childIds.map((item) => (
+            <NodeComponent key={item} id={item} />
+          ))}
+        </div>
+      ) : null}
+    </div>
+  );
+}

+ 21 - 0
frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/VirtualizerList.hooks.tsx

@@ -0,0 +1,21 @@
+import { useVirtualizer } from '@tanstack/react-virtual';
+import { useRef } from 'react';
+
+const defaultSize = 60;
+
+export function useVirtualizerList(count: number) {
+  const parentRef = useRef<HTMLDivElement>(null);
+
+  const rowVirtualizer = useVirtualizer({
+    count,
+    getScrollElement: () => parentRef.current,
+    estimateSize: () => {
+      return defaultSize;
+    },
+  });
+
+  return {
+    rowVirtualizer,
+    parentRef,
+  };
+}

+ 52 - 0
frontend/appflowy_tauri/src/appflowy_app/components/document/VirtualizerList/index.tsx

@@ -0,0 +1,52 @@
+import React from 'react';
+import { useVirtualizerList } from './VirtualizerList.hooks';
+import { Node } from '@/appflowy_app/stores/reducers/document/slice';
+import DocumentTitle from '../DocumentTitle';
+
+export default function VirtualizerList({
+  childIds,
+  node,
+  renderNode,
+}: {
+  childIds: string[];
+  node: Node;
+  renderNode: (nodeId: string) => JSX.Element;
+}) {
+  const { rowVirtualizer, parentRef } = useVirtualizerList(childIds.length);
+
+  const virtualItems = rowVirtualizer.getVirtualItems();
+
+  return (
+    <div ref={parentRef} className={`doc-scroller-container flex h-[100%] flex-wrap justify-center overflow-auto px-20`}>
+      <div
+        className='doc-body max-w-screen w-[900px] min-w-0'
+        style={{
+          height: rowVirtualizer.getTotalSize(),
+          position: 'relative',
+        }}
+      >
+        {node && childIds && virtualItems.length ? (
+          <div
+            style={{
+              position: 'absolute',
+              top: 0,
+              left: 0,
+              width: '100%',
+              transform: `translateY(${virtualItems[0].start || 0}px)`,
+            }}
+          >
+            {virtualItems.map((virtualRow) => {
+              const id = childIds[virtualRow.index];
+              return (
+                <div className='p-[1px]' key={id} data-index={virtualRow.index} ref={rowVirtualizer.measureElement}>
+                  {virtualRow.index === 0 ? <DocumentTitle id={node.id} /> : null}
+                  {renderNode(id)}
+                </div>
+              );
+            })}
+          </div>
+        ) : null}
+      </div>
+    </div>
+  );
+}

+ 12 - 0
frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/ErrorBoundaryFallbackComponent.tsx

@@ -0,0 +1,12 @@
+import { Alert } from '@mui/material';
+import { FallbackProps } from 'react-error-boundary';
+
+export function ErrorBoundaryFallbackComponent({ error, resetErrorBoundary }: FallbackProps) {
+  return (
+    <Alert severity='error' className='mb-2'>
+      <p>Something went wrong:</p>
+      <pre>{error.message}</pre>
+      <button onClick={resetErrorBoundary}>Try again</button>
+    </Alert>
+  );
+}

+ 16 - 0
frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts

@@ -0,0 +1,16 @@
+import { Node } from '@/appflowy_app/stores/reducers/document/slice';
+import { useAppSelector } from '@/appflowy_app/stores/store';
+import { useMemo } from 'react';
+
+export function useSubscribeNode(id: string) {
+  const node = useAppSelector<Node | undefined>(state => state.document.nodes[id]);
+  const childIds = useAppSelector<string[] | undefined>(state => state.document.children[id]);
+
+  const memoizedNode = useMemo(() => node, [node?.id, node?.data, node?.type]);
+  const memoizedChildIds = useMemo(() => childIds, [JSON.stringify(childIds)]);
+
+  return {
+    node: memoizedNode,
+    childIds: memoizedChildIds
+  }
+}

+ 5 - 0
frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.hooks.ts

@@ -9,6 +9,7 @@ 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();
@@ -132,6 +133,10 @@ 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,

+ 31 - 0
frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts

@@ -0,0 +1,31 @@
+// 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 interface NestedBlock {
+  id: string;
+  type: BlockType;
+  data: Record<string, any>;
+  parent: string | null;
+  children: string;
+}
+export interface TextDelta {
+  insert: string;
+  attributes?: Record<string, string | boolean>;
+}
+export interface DocumentData {
+  rootId: string;
+  blocks: Record<string, NestedBlock>;
+  ytexts: Record<string, TextDelta[]>;
+  yarrays: Record<string, string[]>;
+}

+ 120 - 0
frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_controller.ts

@@ -0,0 +1,120 @@
+import * as Y from 'yjs';
+import { IndexeddbPersistence } from 'y-indexeddb';
+import { v4 } from 'uuid';
+import { DocumentData } from '@/appflowy_app/interfaces/document';
+import { createContext } from 'react';
+
+export type DeltaAttributes = {
+  retain: number;
+  attributes: Record<string, unknown>;
+};
+
+export type DeltaRetain = { retain: number };
+export type DeltaDelete = { delete: number };
+export type DeltaInsert = {
+  insert: string | Y.XmlText;
+  attributes?: Record<string, unknown>;
+};
+
+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);
+  }
+
+
+  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,
+    }
+    
+    // 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;
+    ydoc.on('updateV2', (update) => {
+      console.log('======', update);
+    })
+    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) {
+        Object.assign(obj.yarrays, {
+          [value.children]: ydoc.getArray(value.children).toArray()
+        });
+      }
+      if (value.data.text) {
+        Object.assign(obj.ytexts, {
+          [value.data.text]: ydoc.getText(value.data.text).toDelta()
+        })
+      }
+    });
+    return obj;
+  }
+
+
+  yTextApply = (yTextId: string, delta: Delta) => {
+    console.log("====", yTextId, delta);
+    const ydoc = this._ydoc;
+    const ytext = ydoc.getText(yTextId);
+    ytext.applyDelta(delta);
+  }
+
+}

+ 63 - 0
frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/slice.ts

@@ -0,0 +1,63 @@
+import { BlockType, TextDelta } from "@/appflowy_app/interfaces/document";
+import { PayloadAction, createSlice } from "@reduxjs/toolkit";
+
+export interface Node {
+  id: string;
+  parent: string | null;
+  type: BlockType;
+  selected?: boolean;
+  delta: TextDelta[];
+  data: {
+    text?: string;
+  };
+}
+
+export type NodeState = {
+  nodes: Record<string, Node>;
+  children: Record<string, string[]>;
+};
+const initialState: NodeState = {
+  nodes: {},
+  children: {},
+};
+
+export const documentSlice = createSlice({
+  name: 'document',
+  initialState: initialState,
+  reducers: {
+    clear: (state, action: PayloadAction) => {
+      return initialState;
+    },
+    addNode: (state, action: PayloadAction<Node>) => {
+      state.nodes[action.payload.id] = action.payload;
+    },
+    addChild: (state, action: PayloadAction<{ parentId: string, childId: string }>) => {
+      const children = state.children[action.payload.parentId];
+      if (children) {
+        children.push(action.payload.childId);
+      } else {
+        state.children[action.payload.parentId] = [action.payload.childId]
+      }
+    },
+
+    updateNode: (state, action: PayloadAction<{id: string; parent?: string; type?: BlockType; data?: any }>) => {
+      state.nodes[action.payload.id] = {
+        ...state.nodes[action.payload.id],
+        ...action.payload
+      }
+    },
+
+    removeNode: (state, action: PayloadAction<string>) => {
+      const parentId = state.nodes[action.payload].parent;
+      delete state.nodes[action.payload];
+      if (parentId) {
+        const index = state.children[parentId].indexOf(action.payload);
+        if (index > -1) {
+          state.children[parentId].splice(index, 1);
+        }
+      }
+    },
+  },
+});
+
+export const documentActions = documentSlice.actions;

+ 2 - 0
frontend/appflowy_tauri/src/appflowy_app/stores/store.ts

@@ -14,6 +14,7 @@ import { currentUserSlice } from './reducers/current-user/slice';
 import { gridSlice } from './reducers/grid/slice';
 import { workspaceSlice } from './reducers/workspace/slice';
 import { databaseSlice } from './reducers/database/slice';
+import { documentSlice } from './reducers/document/slice';
 import { boardSlice } from './reducers/board/slice';
 import { errorSlice } from './reducers/error/slice';
 import { activePageIdSlice } from './reducers/activePageId/slice';
@@ -32,6 +33,7 @@ const store = configureStore({
     [gridSlice.name]: gridSlice.reducer,
     [databaseSlice.name]: databaseSlice.reducer,
     [boardSlice.name]: boardSlice.reducer,
+    [documentSlice.name]: documentSlice.reducer,
     [workspaceSlice.name]: workspaceSlice.reducer,
     [errorSlice.name]: errorSlice.reducer,
   },

+ 9 - 0
frontend/appflowy_tauri/src/appflowy_app/utils/block.ts

@@ -2,6 +2,7 @@
 import { createContext } from 'react';
 import { ulid } from "ulid";
 import { BlockEditor } from '../block_editor/index';
+import { BlockType } from '../interfaces';
 
 export const BlockContext = createContext<{
   id?: string;
@@ -23,3 +24,11 @@ export function calculateViewportBlockMaxCount() {
 }
 
 
+export interface NestedNode {
+  id: string;
+  children: string;
+  parent: string | null;
+  type: BlockType;
+  data: any;
+}
+

+ 37 - 0
frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts

@@ -49,3 +49,40 @@ export function set(obj: any, path: string[], value: any): void {
     }
   }
 }
+
+export function isEqual<T>(value1: T, value2: T): boolean {
+  if (typeof value1 !== 'object' || value1 === null || typeof value2 !== 'object' || value2 === null) {
+    return value1 === value2;
+  }
+
+
+  if (Array.isArray(value1)) {
+    if (!Array.isArray(value2) || value1.length !== value2.length) {
+      return false;
+    }
+
+    for (let i = 0; i < value1.length; i++) {
+      if (!isEqual(value1[i], value2[i])) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  const keys1 = Object.keys(value1);
+  const keys2 = Object.keys(value2);
+
+  if (keys1.length !== keys2.length) {
+    return false;
+  }
+
+  for (const key of keys1) {  
+    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+    // @ts-expect-error  
+    if (!isEqual(value1[key], value2[key])) {
+      return false;
+    }
+  }
+  return true;
+}

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

@@ -4,796 +4,30 @@ import {
   DocumentVersionPB,
   OpenDocumentPayloadPB,
 } from '../../services/backend/events/flowy-document';
-import { BlockInterface, BlockType } from '../interfaces';
 import { useParams } from 'react-router-dom';
-import { BlockEditor } from '../block_editor';
+import { DocumentData } from '../interfaces/document';
+import { YDocController } from '$app/stores/effects/document/document_controller';
 
-const loadBlockData = async (id: string): Promise<Record<string, BlockInterface>> => {
-  return {
-    [id]: {
-      id: id,
-      type: BlockType.PageBlock,
-      data: { content: [{ text: 'Document Title' }] },
-      next: null,
-      firstChild: "L1-1",
-    },
-    "L1-1": {
-      id: "L1-1",
-      type: BlockType.HeadingBlock,
-      data: { level: 1, content: [{ text: 'Heading 1' }] },
-      next: "L1-2",
-      firstChild: null,
-    },
-    "L1-2": {
-      id: "L1-2",
-      type: BlockType.HeadingBlock,
-      data: { level: 2, content: [{ text: 'Heading 2' }] },
-      next: "L1-3",
-      firstChild: null,
-    },
-    "L1-3": {
-      id: "L1-3",
-      type: BlockType.HeadingBlock,
-      data: { level: 3, content: [{ text: 'Heading 3' }] },
-      next: "L1-4",
-      firstChild: null,
-    },
-    "L1-4": {
-      id: "L1-4",
-      type: BlockType.TextBlock,
-      data: { content: [
-        {
-          text:
-            'This example shows how you can make a hovering menu appear above your content, which you can use to make text ',
-        },
-        { text: 'bold', bold: true },
-        { text: ', ' },
-        { text: 'italic', italic: true },
-        { text: ', or anything else you might want to do!' },
-      ] },
-      next: "L1-5",
-      firstChild: null,
-    },
-    "L1-5": {
-      id: "L1-5",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-        { text: 'select any piece of text and the menu will appear', bold: true },
-        { text: '.' },
-      ] },
-      next: "L1-6",
-      firstChild: "L1-5-1",
-    },
-    "L1-5-1": {
-      id: "L1-5-1",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-      ] },
-      next: "L1-5-2",
-      firstChild: null,
-    },
-    "L1-5-2": {
-      id: "L1-5-2",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-      ] },
-      next: null,
-      firstChild: null,
-    },
-    "L1-6": {
-      id: "L1-6",
-      type: BlockType.ListBlock,
-      data: { type: 'bulleted', content: [
-        {
-          text:
-            "Since it's rich text, you can do things like turn a selection of text ",
-        },
-        { text: 'bold', bold: true },
-        {
-          text:
-            ', or add a semantically rendered block quote in the middle of the page, like this:',
-        },
-      ] },
-      next: "L1-7",
-      firstChild: "L1-6-1",
-    },
-    "L1-6-1": {
-      id: "L1-6-1",
-      type: BlockType.ListBlock,
-      data: { type: 'numbered', content: [
-        {
-          text:
-            "Since it's rich text, you can do things like turn a selection of text ",
-        },
-        
-      ] },
-      
-      next: "L1-6-2",
-      firstChild: null,
-    },
-    "L1-6-2": {
-      id: "L1-6-2",
-      type: BlockType.ListBlock,
-      data: { type: 'numbered', content: [
-        {
-          text:
-            "Since it's rich text, you can do things like turn a selection of text ",
-        },
-        
-      ] },
-      
-      next: "L1-6-3",
-      firstChild: null,
-    },
 
-    "L1-6-3": {
-      id: "L1-6-3",
-      type: BlockType.TextBlock,
-      data: { content: [{ text: 'A wise quote.' }] },
-      next: null,
-      firstChild: null,
-    },
-    
-    "L1-7": {
-      id: "L1-7",
-      type: BlockType.ListBlock,
-      data: { type: 'column' },
-      
-      next: "L1-8",
-      firstChild: "L1-7-1",
-    },
-    "L1-7-1": {
-      id: "L1-7-1",
-      type: BlockType.ColumnBlock,
-      data: { ratio: '0.33' },
-      next: "L1-7-2",
-      firstChild: "L1-7-1-1",
-    },
-    "L1-7-1-1": {
-      id: "L1-7-1-1",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-      ] },
-      next: null,
-      firstChild: null,
-    },
-    "L1-7-2": {
-      id: "L1-7-2",
-      type: BlockType.ColumnBlock,
-      data: { ratio: '0.33' },
-      next: "L1-7-3",
-      firstChild: "L1-7-2-1",
-    },
-    "L1-7-2-1": {
-      id: "L1-7-2-1",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-      ] },
-      next: "L1-7-2-2",
-      firstChild: null,
-    },
-    "L1-7-2-2": {
-      id: "L1-7-2-2",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-      ] },
-      next: null,
-      firstChild: null,
-    },
-    "L1-7-3": {
-      id: "L1-7-3",
-      type: BlockType.ColumnBlock,
-      data: { ratio: '0.33' },
-      next: null,
-      firstChild: "L1-7-3-1",
-    },
-    "L1-7-3-1": {
-      id: "L1-7-3-1",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-      ] },
-      next: null,
-      firstChild: null,
-    },
-    "L1-8": {
-      id: "L1-8",
-      type: BlockType.HeadingBlock,
-      data: { level: 1, content: [{ text: 'Heading 1' }] },
-      next: "L1-9",
-      firstChild: null,
-    },
-    "L1-9": {
-      id: "L1-9",
-      type: BlockType.HeadingBlock,
-      data: { level: 2, content: [{ text: 'Heading 2' }] },
-      next: "L1-10",
-      firstChild: null,
-    },
-    "L1-10": {
-      id: "L1-10",
-      type: BlockType.HeadingBlock,
-      data: { level: 3, content: [{ text: 'Heading 3' }] },
-      next: "L1-11",
-      firstChild: null,
-    },
-    "L1-11": {
-      id: "L1-11",
-      type: BlockType.TextBlock,
-      data: { content: [
-        {
-          text:
-            'This example shows how you can make a hovering menu appear above your content, which you can use to make text ',
-        },
-        { text: 'bold', bold: true },
-        { text: ', ' },
-        { text: 'italic', italic: true },
-        { text: ', or anything else you might want to do!' },
-      ] },
-      next: "L1-12",
-      firstChild: null,
-    },
-    "L1-12": {
-      id: "L1-12",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-        { text: 'select any piece of text and the menu will appear', bold: true },
-        { text: '.' },
-      ] },
-      next: "L2-1",
-      firstChild: "L1-12-1",
-    },
-    "L1-12-1": {
-      id: "L1-12-1",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-      ] },
-      next: "L1-12-2",
-      firstChild: null,
-    },
-    "L1-12-2": {
-      id: "L1-12-2",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-      ] },
-      next: null,
-      firstChild: null,
-    },
-    "L2-1": {
-      id: "L2-1",
-      type: BlockType.HeadingBlock,
-      data: { level: 1, content: [{ text: 'Heading 1' }] },
-      next: "L2-2",
-      firstChild: null,
-    },
-    "L2-2": {
-      id: "L2-2",
-      type: BlockType.HeadingBlock,
-      data: { level: 2, content: [{ text: 'Heading 2' }] },
-      next: "L2-3",
-      firstChild: null,
-    },
-    "L2-3": {
-      id: "L2-3",
-      type: BlockType.HeadingBlock,
-      data: { level: 3, content: [{ text: 'Heading 3' }] },
-      next: "L2-4",
-      firstChild: null,
-    },
-    "L2-4": {
-      id: "L2-4",
-      type: BlockType.TextBlock,
-      data: { content: [
-        {
-          text:
-            'This example shows how you can make a hovering menu appear above your content, which you can use to make text ',
-        },
-        { text: 'bold', bold: true },
-        { text: ', ' },
-        { text: 'italic', italic: true },
-        { text: ', or anything else you might want to do!' },
-      ] },
-      next: "L2-5",
-      firstChild: null,
-    },
-    "L2-5": {
-      id: "L2-5",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-        { text: 'select any piece of text and the menu will appear', bold: true },
-        { text: '.' },
-      ] },
-      next: "L2-6",
-      firstChild: "L2-5-1",
-    },
-    "L2-5-1": {
-      id: "L2-5-1",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-      ] },
-      next: "L2-5-2",
-      firstChild: null,
-    },
-    "L2-5-2": {
-      id: "L2-5-2",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-      ] },
-      next: null,
-      firstChild: null,
-    },
-    "L2-6": {
-      id: "L2-6",
-      type: BlockType.ListBlock,
-      data: { type: 'bulleted', content: [
-        {
-          text:
-            "Since it's rich text, you can do things like turn a selection of text ",
-        },
-        { text: 'bold', bold: true },
-        {
-          text:
-            ', or add a semantically rendered block quote in the middle of the page, like this:',
-        },
-      ] },
-      next: "L2-7",
-      firstChild: "L2-6-1",
-    },
-    "L2-6-1": {
-      id: "L2-6-1",
-      type: BlockType.ListBlock,
-      data: { type: 'numbered', content: [
-        {
-          text:
-            "Since it's rich text, you can do things like turn a selection of text ",
-        },
-        
-      ] },
-      
-      next: "L2-6-2",
-      firstChild: null,
-    },
-    "L2-6-2": {
-      id: "L2-6-2",
-      type: BlockType.ListBlock,
-      data: { type: 'numbered', content: [
-        {
-          text:
-            "Since it's rich text, you can do things like turn a selection of text ",
-        },
-        
-      ] },
-      
-      next: "L2-6-3",
-      firstChild: null,
-    },
-
-    "L2-6-3": {
-      id: "L2-6-3",
-      type: BlockType.TextBlock,
-      data: { content: [{ text: 'A wise quote.' }] },
-      next: null,
-      firstChild: null,
-    },
-    
-    "L2-7": {
-      id: "L2-7",
-      type: BlockType.ListBlock,
-      data: { type: 'column' },
-      
-      next: "L2-8",
-      firstChild: "L2-7-1",
-    },
-    "L2-7-1": {
-      id: "L2-7-1",
-      type: BlockType.ColumnBlock,
-      data: { ratio: '0.33' },
-      next: "L2-7-2",
-      firstChild: "L2-7-1-1",
-    },
-    "L2-7-1-1": {
-      id: "L2-7-1-1",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-      ] },
-      next: null,
-      firstChild: null,
-    },
-    "L2-7-2": {
-      id: "L2-7-2",
-      type: BlockType.ColumnBlock,
-      data: { ratio: '0.33' },
-      next: "L2-7-3",
-      firstChild: "L2-7-2-1",
-    },
-    "L2-7-2-1": {
-      id: "L2-7-2-1",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-      ] },
-      next: "L2-7-2-2",
-      firstChild: null,
-    },
-    "L2-7-2-2": {
-      id: "L2-7-2-2",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-      ] },
-      next: null,
-      firstChild: null,
-    },
-    "L2-7-3": {
-      id: "L2-7-3",
-      type: BlockType.ColumnBlock,
-      data: { ratio: '0.33' },
-      next: null,
-      firstChild: "L2-7-3-1",
-    },
-    "L2-7-3-1": {
-      id: "L2-7-3-1",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-      ] },
-      next: null,
-      firstChild: null,
-    },
-    "L2-8": {
-      id: "L2-8",
-      type: BlockType.HeadingBlock,
-      data: { level: 1, content: [{ text: 'Heading 1' }] },
-      next: "L2-9",
-      firstChild: null,
-    },
-    "L2-9": {
-      id: "L2-9",
-      type: BlockType.HeadingBlock,
-      data: { level: 2, content: [{ text: 'Heading 2' }] },
-      next: "L2-10",
-      firstChild: null,
-    },
-    "L2-10": {
-      id: "L2-10",
-      type: BlockType.HeadingBlock,
-      data: { level: 3, content: [{ text: 'Heading 3' }] },
-      next: "L2-11",
-      firstChild: null,
-    },
-    "L2-11": {
-      id: "L2-11",
-      type: BlockType.TextBlock,
-      data: { content: [
-        {
-          text:
-            'This example shows how you can make a hovering menu appear above your content, which you can use to make text ',
-        },
-        { text: 'bold', bold: true },
-        { text: ', ' },
-        { text: 'italic', italic: true },
-        { text: ', or anything else you might want to do!' },
-      ] },
-      next: "L2-12",
-      firstChild: null,
-    },
-    "L2-12": {
-      id: "L2-12",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-        { text: 'select any piece of text and the menu will appear', bold: true },
-        { text: '.' },
-      ] },
-      next: "L3-1",
-      firstChild: "L2-12-1",
-    },
-    "L2-12-1": {
-      id: "L2-12-1",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-      ] },
-      next: "L2-12-2",
-      firstChild: null,
-    },
-    "L2-12-2": {
-      id: "L2-12-2",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-      ] },
-      next: null,
-      firstChild: null,
-    },"L3-1": {
-      id: "L3-1",
-      type: BlockType.HeadingBlock,
-      data: { level: 1, content: [{ text: 'Heading 1' }] },
-      next: "L3-2",
-      firstChild: null,
-    },
-    "L3-2": {
-      id: "L3-2",
-      type: BlockType.HeadingBlock,
-      data: { level: 2, content: [{ text: 'Heading 2' }] },
-      next: "L3-3",
-      firstChild: null,
-    },
-    "L3-3": {
-      id: "L3-3",
-      type: BlockType.HeadingBlock,
-      data: { level: 3, content: [{ text: 'Heading 3' }] },
-      next: "L3-4",
-      firstChild: null,
-    },
-    "L3-4": {
-      id: "L3-4",
-      type: BlockType.TextBlock,
-      data: { content: [
-        {
-          text:
-            'This example shows how you can make a hovering menu appear above your content, which you can use to make text ',
-        },
-        { text: 'bold', bold: true },
-        { text: ', ' },
-        { text: 'italic', italic: true },
-        { text: ', or anything else you might want to do!' },
-      ] },
-      next: "L3-5",
-      firstChild: null,
-    },
-    "L3-5": {
-      id: "L3-5",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-        { text: 'select any piece of text and the menu will appear', bold: true },
-        { text: '.' },
-      ] },
-      next: "L3-6",
-      firstChild: "L3-5-1",
-    },
-    "L3-5-1": {
-      id: "L3-5-1",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-      ] },
-      next: "L3-5-2",
-      firstChild: null,
-    },
-    "L3-5-2": {
-      id: "L3-5-2",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-      ] },
-      next: null,
-      firstChild: null,
-    },
-    "L3-6": {
-      id: "L3-6",
-      type: BlockType.ListBlock,
-      data: { type: 'bulleted', content: [
-        {
-          text:
-            "Since it's rich text, you can do things like turn a selection of text ",
-        },
-        { text: 'bold', bold: true },
-        {
-          text:
-            ', or add a semantically rendered block quote in the middle of the page, like this:',
-        },
-      ] },
-      next: "L3-7",
-      firstChild: "L3-6-1",
-    },
-    "L3-6-1": {
-      id: "L3-6-1",
-      type: BlockType.ListBlock,
-      data: { type: 'numbered', content: [
-        {
-          text:
-            "Since it's rich text, you can do things like turn a selection of text ",
-        },
-        
-      ] },
-      
-      next: "L3-6-2",
-      firstChild: null,
-    },
-    "L3-6-2": {
-      id: "L3-6-2",
-      type: BlockType.ListBlock,
-      data: { type: 'numbered', content: [
-        {
-          text:
-            "Since it's rich text, you can do things like turn a selection of text ",
-        },
-        
-      ] },
-      
-      next: "L3-6-3",
-      firstChild: null,
-    },
-    
-    "L3-6-3": {
-      id: "L3-6-3",
-      type: BlockType.TextBlock,
-      data: { content: [{ text: 'A wise quote.' }] },
-      next: null,
-      firstChild: null,
-    },
-    
-    "L3-7": {
-      id: "L3-7",
-      type: BlockType.ListBlock,
-      data: { type: 'column' },
-      
-      next: "L3-8",
-      firstChild: "L3-7-1",
-    },
-    "L3-7-1": {
-      id: "L3-7-1",
-      type: BlockType.ColumnBlock,
-      data: { ratio: '0.33' },
-      next: "L3-7-2",
-      firstChild: "L3-7-1-1",
-    },
-    "L3-7-1-1": {
-      id: "L3-7-1-1",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-      ] },
-      next: null,
-      firstChild: null,
-    },
-    "L3-7-2": {
-      id: "L3-7-2",
-      type: BlockType.ColumnBlock,
-      data: { ratio: '0.33' },
-      next: "L3-7-3",
-      firstChild: "L3-7-2-1",
-    },
-    "L3-7-2-1": {
-      id: "L3-7-2-1",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-      ] },
-      next: "L3-7-2-2",
-      firstChild: null,
-    },
-    "L3-7-2-2": {
-      id: "L3-7-2-2",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-      ] },
-      next: null,
-      firstChild: null,
-    },
-    "L3-7-3": {
-      id: "L3-7-3",
-      type: BlockType.ColumnBlock,
-      data: { ratio: '0.33' },
-      next: null,
-      firstChild: "L3-7-3-1",
-    },
-    "L3-7-3-1": {
-      id: "L3-7-3-1",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-      ] },
-      next: null,
-      firstChild: null,
-    },
-    "L3-8": {
-      id: "L3-8",
-      type: BlockType.HeadingBlock,
-      data: { level: 1, content: [{ text: 'Heading 1' }] },
-      next: "L3-9",
-      firstChild: null,
-    },
-    "L3-9": {
-      id: "L3-9",
-      type: BlockType.HeadingBlock,
-      data: { level: 2, content: [{ text: 'Heading 2' }] },
-      next: "L3-10",
-      firstChild: null,
-    },
-    "L3-10": {
-      id: "L3-10",
-      type: BlockType.HeadingBlock,
-      data: { level: 3, content: [{ text: 'Heading 3' }] },
-      next: "L3-11",
-      firstChild: null,
-    },
-    "L3-11": {
-      id: "L3-11",
-      type: BlockType.TextBlock,
-      data: { content: [
-        {
-          text:
-            'This example shows how you can make a hovering menu appear above your content, which you can use to make text ',
-        },
-        { text: 'bold', bold: true },
-        { text: ', ' },
-        { text: 'italic', italic: true },
-        { text: ', or anything else you might want to do!' },
-      ] },
-      next: "L3-12",
-      firstChild: null,
-    },
-    "L3-12": {
-      id: "L3-12",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-        { text: 'select any piece of text and the menu will appear', bold: true },
-        { text: '.' },
-      ] },
-      next: null,
-      firstChild: "L3-12-1",
-    },
-    "L3-12-1": {
-      id: "L3-12-1",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-      ] },
-      next: "L3-12-2",
-      firstChild: null,
-    },
-    "L3-12-2": {
-      id: "L3-12-2",
-      type: BlockType.TextBlock,
-      data: { content: [
-        { text: 'Try it out yourself! Just ' },
-      ] },
-      next: null,
-      firstChild: null,
-    },
-  }
-}
 export const useDocument = () => {
   const params = useParams();
-  const [blockId, setBlockId] = useState<string>();
-  const blockEditorRef = useRef<BlockEditor | null>(null)
-
+  const [ documentId, setDocumentId ] = useState<string>();
+  const [ documentData, setDocumentData ] = useState<DocumentData>();
+  const [ controller, setController ] = useState<YDocController | null>(null);
 
   useEffect(() => {
     void (async () => {
       if (!params?.id) return;
-      const data = await loadBlockData(params.id);
-      console.log('==== enter ====', params?.id, data);
-  
-      if (!blockEditorRef.current) {
-        blockEditorRef.current = new BlockEditor(params?.id, data);
-      } else {
-        blockEditorRef.current.changeDoc(params?.id, data);
-      }
-
-      setBlockId(params.id)
+      const c = new YDocController(params.id);
+      setController(c);
+      const res = await c.open();
+      console.log(res)
+      setDocumentData(res)
+      setDocumentId(params.id)
     })();
     return () => {
       console.log('==== leave ====', params?.id)
     }
   }, [params.id]);
-  return { blockId, blockEditor: blockEditorRef.current };
+  return { documentId, documentData, controller };
 };

+ 8 - 12
frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx

@@ -1,27 +1,23 @@
 import { useDocument } from './DocumentPage.hooks';
-import BlockList from '../components/block/BlockList';
-import { BlockContext } from '../utils/block';
 import { createTheme, ThemeProvider } from '@mui/material';
+import Root from '../components/document/Root';
+import { YDocControllerContext } from '../stores/effects/document/document_controller';
 
 const theme = createTheme({
   typography: {
     fontFamily: ['Poppins'].join(','),
   },
 });
+
 export const DocumentPage = () => {
-  const { blockId, blockEditor } = useDocument();
+  const { documentId, documentData, controller } = useDocument();
 
-  if (!blockId || !blockEditor) return <div className='error-page'></div>;
+  if (!documentId || !documentData || !controller) return null;
   return (
     <ThemeProvider theme={theme}>
-      <BlockContext.Provider
-        value={{
-          id: blockId,
-          blockEditor,
-        }}
-      >
-        <BlockList blockEditor={blockEditor} blockId={blockId} />
-      </BlockContext.Provider>
+      <YDocControllerContext.Provider value={controller}>
+        <Root documentData={documentData} />
+      </YDocControllerContext.Provider>
     </ThemeProvider>
   );
 };