Skip to content

Commit 63c03a7

Browse files
authored
Merge pull request #545 from cofacts/feature/collab-version
Collaborative editing history
2 parents 4ccf425 + a09b2e9 commit 63c03a7

File tree

7 files changed

+835
-60
lines changed

7 files changed

+835
-60
lines changed

components/Collaborate/CollabEditor.js

+105-59
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
/* eslint-env browser */
2-
31
import * as Y from 'yjs';
42
import { HocuspocusProvider } from '@hocuspocus/provider';
53
import {
@@ -13,18 +11,16 @@ import { t } from 'ttag';
1311
import { nl2br, linkify } from 'lib/text';
1412
import { Button, Typography } from '@material-ui/core';
1513
import { TranscribePenIcon } from 'components/icons';
16-
import { EditorState } from 'prosemirror-state';
17-
import { EditorView } from 'prosemirror-view';
18-
import { schema } from 'prosemirror-schema-basic';
19-
import { DOMParser } from 'prosemirror-model';
14+
import { useProseMirror, ProseMirror } from 'use-prosemirror';
15+
import { schema } from './Schema';
2016
import { exampleSetup } from 'prosemirror-example-setup';
2117
import { keymap } from 'prosemirror-keymap';
22-
import { useState, useRef } from 'react';
18+
import { useState, useRef, useEffect } from 'react';
2319
import { makeStyles } from '@material-ui/core/styles';
2420
import useCurrentUser from 'lib/useCurrentUser';
25-
import cx from 'clsx';
2621
import PlaceholderPlugin from './Placeholder';
2722
import getConfig from 'next/config';
23+
import CollabHistory from './CollabHistory';
2824

2925
const {
3026
publicRuntimeConfig: { PUBLIC_COLLAB_SERVER_URL },
@@ -85,24 +81,84 @@ const colors = [
8581

8682
const color = colors[Math.floor(Math.random() * colors.length)];
8783

84+
const Editor = ({ provider, currentUser, className, innerRef, onUnmount }) => {
85+
useEffect(() => {
86+
// console.log('editor mount');
87+
return () => {
88+
onUnmount();
89+
};
90+
}, [onUnmount]);
91+
92+
const ydoc = provider.document;
93+
const permanentUserData = new Y.PermanentUserData(ydoc);
94+
permanentUserData.setUserMapping(
95+
ydoc,
96+
ydoc.clientID,
97+
JSON.stringify({
98+
id: currentUser.id,
99+
name: currentUser.name,
100+
})
101+
);
102+
103+
const yXmlFragment = ydoc.get('prosemirror', Y.XmlFragment);
104+
105+
const [state, setState] = useProseMirror({
106+
schema,
107+
plugins: [
108+
ySyncPlugin(yXmlFragment, { permanentUserData }),
109+
yCursorPlugin(provider.awareness),
110+
yUndoPlugin(),
111+
keymap({
112+
'Mod-z': undo,
113+
'Mod-y': redo,
114+
'Mod-Shift-z': redo,
115+
}),
116+
PlaceholderPlugin(t`Input transcript`),
117+
].concat(exampleSetup({ schema, menuBar: false })),
118+
});
119+
120+
return (
121+
<ProseMirror
122+
ref={innerRef}
123+
state={state}
124+
onChange={setState}
125+
className={className}
126+
/>
127+
);
128+
};
129+
130+
/**
131+
* @param {Article} props.article
132+
*/
88133
const CollabEditor = ({ article }) => {
89134
const editor = useRef(null);
90-
const [editorView, setEditorView] = useState(null);
135+
const [showEditor, setShowEditor] = useState(null);
136+
const [isSynced, setIsSynced] = useState(false);
91137
const currentUser = useCurrentUser();
138+
139+
// onTranscribe setup provider for both Editor and CollabHistory to use.
140+
// And, to avoid duplicated connection, provider will be destroyed(close connection) when Editor unmounted.
141+
const [provider, setProvider] = useState(null);
142+
92143
const onTranscribe = () => {
93144
if (!currentUser) {
94145
return alert(t`Please login first.`);
95146
}
96-
const ydoc = new Y.Doc();
97-
const permanentUserData = new Y.PermanentUserData(ydoc);
98-
permanentUserData.setUserMapping(ydoc, ydoc.clientID, currentUser.name);
99-
ydoc.gc = false;
100147

148+
setShowEditor(true);
149+
150+
if (provider) return;
151+
setIsSynced(false);
101152
const provider = new HocuspocusProvider({
102153
url: PUBLIC_COLLAB_SERVER_URL,
103154
name: article.id,
104155
broadcast: false,
105-
document: ydoc,
156+
document: new Y.Doc({ gc: false }), // set gc to false to keep doc (delete)history
157+
onSynced: () => {
158+
// https://github.com/ueberdosis/hocuspocus/blob/main/docs/provider/events.md
159+
// console.log('onSynced');
160+
setIsSynced(true);
161+
},
106162
// onAwarenessChange: ({ states }) => {
107163
// console.log('provider', states);
108164
// },
@@ -111,34 +167,15 @@ const CollabEditor = ({ article }) => {
111167
name: currentUser.name,
112168
color,
113169
});
114-
const yXmlFragment = ydoc.get('prosemirror', Y.XmlFragment);
115-
116-
if (editorView) editorView.destroy();
117-
setEditorView(
118-
new EditorView(editor.current, {
119-
state: EditorState.create({
120-
schema,
121-
doc: DOMParser.fromSchema(schema).parse(editor.current),
122-
plugins: [
123-
ySyncPlugin(yXmlFragment, { permanentUserData }),
124-
yCursorPlugin(provider.awareness),
125-
yUndoPlugin(),
126-
keymap({
127-
'Mod-z': undo,
128-
'Mod-y': redo,
129-
'Mod-Shift-z': redo,
130-
}),
131-
PlaceholderPlugin(t`Input transcript`),
132-
].concat(exampleSetup({ schema, menuBar: false })),
133-
}),
134-
})
135-
);
170+
setProvider(provider);
136171
};
137172

138173
const onDone = () => {
139-
if (editorView) {
174+
// get EditorView: https://github.com/ponymessenger/use-prosemirror#prosemirror-
175+
const prosemirrorEditorView = editor.current?.view;
176+
if (prosemirrorEditorView) {
140177
let text = '';
141-
editorView.state.doc.content.forEach(node => {
178+
prosemirrorEditorView.state.doc.content.forEach(node => {
142179
// console.log(node.textContent);
143180
// console.log(node.type.name);
144181
if (node.textContent) {
@@ -149,9 +186,8 @@ const CollabEditor = ({ article }) => {
149186

150187
// TODO: listen textChanged event?
151188
article.text = text;
152-
editorView.destroy();
153189
}
154-
setEditorView(null);
190+
setShowEditor(false);
155191
};
156192

157193
const classes = useStyles();
@@ -168,7 +204,7 @@ const CollabEditor = ({ article }) => {
168204
>
169205
{t`No transcripts yet`}
170206
</Typography>
171-
{!editorView ? (
207+
{!showEditor ? (
172208
<>
173209
<Button
174210
color="primary"
@@ -192,22 +228,24 @@ const CollabEditor = ({ article }) => {
192228
>
193229
{t`Transcript`}
194230
</Typography>
195-
{!editorView ? (
196-
<>
197-
<Button
198-
variant="outlined"
199-
className={classes.editButton}
200-
onClick={onTranscribe}
201-
>
202-
<TranscribePenIcon className={classes.newReplyFabIcon} />
203-
{t`Edit`}
204-
</Button>
205-
</>
206-
) : null}
231+
{!showEditor ? (
232+
<Button
233+
variant="outlined"
234+
className={classes.editButton}
235+
onClick={onTranscribe}
236+
>
237+
<TranscribePenIcon className={classes.newReplyFabIcon} />
238+
{t`Edit`}
239+
</Button>
240+
) : (
241+
isSynced && (
242+
<CollabHistory ydoc={provider.document} docName={article.id} />
243+
)
244+
)}
207245
</>
208246
)}
209247
</div>
210-
{!editorView ? (
248+
{!showEditor ? (
211249
<>
212250
{article.text &&
213251
nl2br(
@@ -220,11 +258,19 @@ const CollabEditor = ({ article }) => {
220258
)}
221259
</>
222260
) : null}
223-
<div
224-
ref={editor}
225-
className={cx(classes.prosemirrorEditor, !editorView && 'hide')}
226-
/>
227-
{!editorView ? null : (
261+
{showEditor && isSynced && (
262+
<Editor
263+
provider={provider}
264+
innerRef={editor}
265+
className={classes.prosemirrorEditor}
266+
currentUser={currentUser}
267+
onUnmount={() => {
268+
// console.log('destroy provider');
269+
provider.destroy();
270+
}}
271+
/>
272+
)}
273+
{!showEditor ? null : (
228274
<>
229275
<div className={classes.transcriptFooter}>
230276
<Button

0 commit comments

Comments
 (0)