feat: add support for offline edit

This commit is contained in:
fantasticit 2022-03-24 17:23:47 +08:00
parent ca7f18e640
commit 2ad6518a51
7 changed files with 93 additions and 4 deletions

View File

@ -78,6 +78,7 @@
"scroll-into-view-if-needed": "^2.2.29", "scroll-into-view-if-needed": "^2.2.29",
"swr": "^1.2.0", "swr": "^1.2.0",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"y-indexeddb": "^9.0.7",
"y-prosemirror": "^1.0.14", "y-prosemirror": "^1.0.14",
"yjs": "^13.5.24" "yjs": "^13.5.24"
}, },

View File

@ -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<IProps> = ({ type, description, duration }) => {
const timer = useRef<ReturnType<typeof setTimeout>>();
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 <SemiBanner type="success" description="以为您恢复上一次离线时编辑数据。 " />;
};

View File

@ -1,20 +1,24 @@
import React, { useMemo, useEffect } from 'react'; import React, { useMemo, useEffect, useState } from 'react';
import cls from 'classnames'; import cls from 'classnames';
import { useEditor, EditorContent } from '@tiptap/react'; import { useEditor, EditorContent } from '@tiptap/react';
import { BackTop } from '@douyinfe/semi-ui'; import { BackTop } from '@douyinfe/semi-ui';
import { ILoginUser, IAuthority } from '@think/domains'; import { ILoginUser, IAuthority } from '@think/domains';
import { useToggle } from 'hooks/useToggle'; import { useToggle } from 'hooks/useToggle';
import { import {
MenuBar,
DEFAULT_EXTENSION, DEFAULT_EXTENSION,
DocumentWithTitle, DocumentWithTitle,
getCollaborationExtension, getCollaborationExtension,
getCollaborationCursorExtension, getCollaborationCursorExtension,
getProvider, getProvider,
destoryProvider, destoryProvider,
MenuBar, ProviderStatus,
getIndexdbProvider,
destoryIndexdbProvider,
} from 'components/tiptap'; } from 'components/tiptap';
import { DataRender } from 'components/data-render'; import { DataRender } from 'components/data-render';
import { joinUser } from 'components/document/collaboration'; import { joinUser } from 'components/document/collaboration';
import { Banner } from 'components/banner';
import { debounce } from 'helpers/debounce'; import { debounce } from 'helpers/debounce';
import { changeTitle } from './index'; import { changeTitle } from './index';
import styles from './index.module.scss'; import styles from './index.module.scss';
@ -29,7 +33,7 @@ interface IProps {
export const Editor: React.FC<IProps> = ({ user, documentId, authority, className, style }) => { export const Editor: React.FC<IProps> = ({ user, documentId, authority, className, style }) => {
if (!user) return null; if (!user) return null;
const [status, setStatus] = useState<ProviderStatus>('connecting');
const provider = useMemo(() => { const provider = useMemo(() => {
return getProvider({ return getProvider({
targetId: documentId, targetId: documentId,
@ -63,16 +67,23 @@ export const Editor: React.FC<IProps> = ({ user, documentId, authority, classNam
const [loading, toggleLoading] = useToggle(true); const [loading, toggleLoading] = useToggle(true);
useEffect(() => { useEffect(() => {
const indexdbProvider = getIndexdbProvider(documentId, provider.document);
indexdbProvider.on('synced', () => {
setStatus('loadCacheSuccess');
});
provider.on('synced', () => { provider.on('synced', () => {
toggleLoading(false); toggleLoading(false);
}); });
provider.on('status', async ({ status }) => { provider.on('status', async ({ status }) => {
console.log('status', status); setStatus(status);
}); });
return () => { return () => {
destoryProvider(provider, 'EDITOR'); destoryProvider(provider, 'EDITOR');
destoryIndexdbProvider(documentId);
}; };
}, []); }, []);
@ -83,6 +94,13 @@ export const Editor: React.FC<IProps> = ({ user, documentId, authority, classNam
normalContent={() => { normalContent={() => {
return ( return (
<div className={styles.editorWrap}> <div className={styles.editorWrap}>
{status === 'disconnected' && (
<Banner
type="warning"
description="
"
/>
)}
<header className={className}> <header className={className}>
<div> <div>
<MenuBar editor={editor} /> <MenuBar editor={editor} />

View File

@ -9,6 +9,7 @@ import { BaseKit } from './basekit';
export { getSchema } from '@tiptap/core'; export { getSchema } from '@tiptap/core';
export * from './menubar'; export * from './menubar';
export * from './provider'; export * from './provider';
export * from './indexdb';
export * from './skeleton'; export * from './skeleton';
export const DocumentWithTitle = Document.extend({ export const DocumentWithTitle = Document.extend({

View File

@ -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);
};

View File

@ -4,6 +4,8 @@ import { IUser } from '@think/domains';
const PROVIDER_POOL_READER = new Map(); const PROVIDER_POOL_READER = new Map();
const PROVIDER_POOL_EDITOR = new Map(); const PROVIDER_POOL_EDITOR = new Map();
export type ProviderStatus = 'connecting' | 'connected' | 'disconnected' | 'loadCacheSuccess';
export const getProvider = ({ export const getProvider = ({
targetId, targetId,
token, token,

View File

@ -116,6 +116,7 @@ importers:
tippy.js: ^6.3.7 tippy.js: ^6.3.7
tsconfig-paths-webpack-plugin: ^3.5.2 tsconfig-paths-webpack-plugin: ^3.5.2
typescript: 4.5.5 typescript: 4.5.5
y-indexeddb: ^9.0.7
y-prosemirror: ^1.0.14 y-prosemirror: ^1.0.14
yjs: ^13.5.24 yjs: ^13.5.24
dependencies: dependencies:
@ -188,6 +189,7 @@ importers:
scroll-into-view-if-needed: 2.2.29 scroll-into-view-if-needed: 2.2.29
swr: 1.2.0_react@17.0.2 swr: 1.2.0_react@17.0.2
tippy.js: 6.3.7 tippy.js: 6.3.7
y-indexeddb: 9.0.7_yjs@13.5.24
y-prosemirror: 1.0.14_0fedec857d2fb730ad5b02a71124bf2a y-prosemirror: 1.0.14_0fedec857d2fb730ad5b02a71124bf2a
yjs: 13.5.24 yjs: 13.5.24
devDependencies: devDependencies:
@ -8500,6 +8502,15 @@ packages:
engines: {node: '>=0.4'} engines: {node: '>=0.4'}
dev: false 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: /y-prosemirror/1.0.14_0fedec857d2fb730ad5b02a71124bf2a:
resolution: {integrity: sha512-fJQn/XT+z/gks9sd64eB+Mf1UQP0d5SHQ8RwbrzIwYFW+FgU/nx8/hJm+nrBAoO9azHOY5aDeoffeLmOFXLD9w==} resolution: {integrity: sha512-fJQn/XT+z/gks9sd64eB+Mf1UQP0d5SHQ8RwbrzIwYFW+FgU/nx8/hJm+nrBAoO9azHOY5aDeoffeLmOFXLD9w==}
peerDependencies: peerDependencies: