From 2ad6518a516a7cf5d1b437cfd45cb4439268ef97 Mon Sep 17 00:00:00 2001 From: fantasticit Date: Thu, 24 Mar 2022 17:23:47 +0800 Subject: [PATCH] feat: add support for offline edit --- packages/client/package.json | 1 + .../client/src/components/banner/index.tsx | 30 +++++++++++++++++++ .../src/components/document/editor/editor.tsx | 26 +++++++++++++--- .../client/src/components/tiptap/index.ts | 1 + .../client/src/components/tiptap/indexdb.ts | 26 ++++++++++++++++ .../client/src/components/tiptap/provider.ts | 2 ++ pnpm-lock.yaml | 11 +++++++ 7 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 packages/client/src/components/banner/index.tsx create mode 100644 packages/client/src/components/tiptap/indexdb.ts diff --git a/packages/client/package.json b/packages/client/package.json index 9a76045f..83e9cf6c 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -78,6 +78,7 @@ "scroll-into-view-if-needed": "^2.2.29", "swr": "^1.2.0", "tippy.js": "^6.3.7", + "y-indexeddb": "^9.0.7", "y-prosemirror": "^1.0.14", "yjs": "^13.5.24" }, diff --git a/packages/client/src/components/banner/index.tsx b/packages/client/src/components/banner/index.tsx new file mode 100644 index 00000000..88dc0637 --- /dev/null +++ b/packages/client/src/components/banner/index.tsx @@ -0,0 +1,30 @@ +import React, { useEffect, useRef } from 'react'; +import { Banner as SemiBanner } from '@douyinfe/semi-ui'; +import { BannerProps } from '@douyinfe/semi-ui/banner'; +import { useToggle } from 'hooks/useToggle'; + +interface IProps extends BannerProps { + duration?: number; +} + +export const Banner: React.FC = ({ type, description, duration }) => { + const timer = useRef>(); + const [visible, toggleVisible] = useToggle(true); + + useEffect(() => { + clearTimeout(timer.current); + if (duration <= 0) return; + + timer.current = setTimeout(() => { + toggleVisible(false); + }, duration); + + return () => { + clearTimeout(timer.current); + }; + }, [duration]); + + if (!visible) return null; + + return ; +}; diff --git a/packages/client/src/components/document/editor/editor.tsx b/packages/client/src/components/document/editor/editor.tsx index a89f9dd1..3afe8020 100644 --- a/packages/client/src/components/document/editor/editor.tsx +++ b/packages/client/src/components/document/editor/editor.tsx @@ -1,20 +1,24 @@ -import React, { useMemo, useEffect } from 'react'; +import React, { useMemo, useEffect, useState } from 'react'; import cls from 'classnames'; import { useEditor, EditorContent } from '@tiptap/react'; import { BackTop } from '@douyinfe/semi-ui'; import { ILoginUser, IAuthority } from '@think/domains'; import { useToggle } from 'hooks/useToggle'; import { + MenuBar, DEFAULT_EXTENSION, DocumentWithTitle, getCollaborationExtension, getCollaborationCursorExtension, getProvider, destoryProvider, - MenuBar, + ProviderStatus, + getIndexdbProvider, + destoryIndexdbProvider, } from 'components/tiptap'; import { DataRender } from 'components/data-render'; import { joinUser } from 'components/document/collaboration'; +import { Banner } from 'components/banner'; import { debounce } from 'helpers/debounce'; import { changeTitle } from './index'; import styles from './index.module.scss'; @@ -29,7 +33,7 @@ interface IProps { export const Editor: React.FC = ({ user, documentId, authority, className, style }) => { if (!user) return null; - + const [status, setStatus] = useState('connecting'); const provider = useMemo(() => { return getProvider({ targetId: documentId, @@ -63,16 +67,23 @@ export const Editor: React.FC = ({ user, documentId, authority, classNam const [loading, toggleLoading] = useToggle(true); useEffect(() => { + const indexdbProvider = getIndexdbProvider(documentId, provider.document); + + indexdbProvider.on('synced', () => { + setStatus('loadCacheSuccess'); + }); + provider.on('synced', () => { toggleLoading(false); }); provider.on('status', async ({ status }) => { - console.log('status', status); + setStatus(status); }); return () => { destoryProvider(provider, 'EDITOR'); + destoryIndexdbProvider(documentId); }; }, []); @@ -83,6 +94,13 @@ export const Editor: React.FC = ({ user, documentId, authority, classNam normalContent={() => { return (
+ {status === 'disconnected' && ( + + )}
diff --git a/packages/client/src/components/tiptap/index.ts b/packages/client/src/components/tiptap/index.ts index 0201ff20..eb031e24 100644 --- a/packages/client/src/components/tiptap/index.ts +++ b/packages/client/src/components/tiptap/index.ts @@ -9,6 +9,7 @@ import { BaseKit } from './basekit'; export { getSchema } from '@tiptap/core'; export * from './menubar'; export * from './provider'; +export * from './indexdb'; export * from './skeleton'; export const DocumentWithTitle = Document.extend({ diff --git a/packages/client/src/components/tiptap/indexdb.ts b/packages/client/src/components/tiptap/indexdb.ts new file mode 100644 index 00000000..29b9e02f --- /dev/null +++ b/packages/client/src/components/tiptap/indexdb.ts @@ -0,0 +1,26 @@ +// const provider = new IndexeddbPersistence(docName, ydoc); + +// provider.on('synced', () => { +// console.log('content from the database is loaded'); +// }); + +import { IndexeddbPersistence } from 'y-indexeddb'; + +const POOL = new Map(); + +export const getIndexdbProvider = (name, doc) => { + if (!POOL.has(name)) { + POOL.set(name, new IndexeddbPersistence(name, doc)); + } + + return POOL.get(name); +}; + +export const destoryIndexdbProvider = (name) => { + const provider = POOL.get(name); + + if (!provider) return; + + provider.destroy(); + POOL.delete(name); +}; diff --git a/packages/client/src/components/tiptap/provider.ts b/packages/client/src/components/tiptap/provider.ts index 1af01843..32e160bf 100644 --- a/packages/client/src/components/tiptap/provider.ts +++ b/packages/client/src/components/tiptap/provider.ts @@ -4,6 +4,8 @@ import { IUser } from '@think/domains'; const PROVIDER_POOL_READER = new Map(); const PROVIDER_POOL_EDITOR = new Map(); +export type ProviderStatus = 'connecting' | 'connected' | 'disconnected' | 'loadCacheSuccess'; + export const getProvider = ({ targetId, token, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 54e7dd04..b5f8558d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -116,6 +116,7 @@ importers: tippy.js: ^6.3.7 tsconfig-paths-webpack-plugin: ^3.5.2 typescript: 4.5.5 + y-indexeddb: ^9.0.7 y-prosemirror: ^1.0.14 yjs: ^13.5.24 dependencies: @@ -188,6 +189,7 @@ importers: scroll-into-view-if-needed: 2.2.29 swr: 1.2.0_react@17.0.2 tippy.js: 6.3.7 + y-indexeddb: 9.0.7_yjs@13.5.24 y-prosemirror: 1.0.14_0fedec857d2fb730ad5b02a71124bf2a yjs: 13.5.24 devDependencies: @@ -8500,6 +8502,15 @@ packages: engines: {node: '>=0.4'} dev: false + /y-indexeddb/9.0.7_yjs@13.5.24: + resolution: {integrity: sha512-58rDlwtRgXucgR9Kxc49AepAR6uGNGfcvPPAMrmMfSvRBQ/tPnx6NoHNyrRkhbALEAjV9tPEAIP0a/KkVqAIyA==} + peerDependencies: + yjs: ^13.0.0 + dependencies: + lib0: 0.2.47 + yjs: 13.5.24 + dev: false + /y-prosemirror/1.0.14_0fedec857d2fb730ad5b02a71124bf2a: resolution: {integrity: sha512-fJQn/XT+z/gks9sd64eB+Mf1UQP0d5SHQ8RwbrzIwYFW+FgU/nx8/hJm+nrBAoO9azHOY5aDeoffeLmOFXLD9w==} peerDependencies: