1
- /* eslint-env browser */
2
-
3
1
import * as Y from 'yjs' ;
4
2
import { HocuspocusProvider } from '@hocuspocus/provider' ;
5
3
import {
@@ -13,18 +11,16 @@ import { t } from 'ttag';
13
11
import { nl2br , linkify } from 'lib/text' ;
14
12
import { Button , Typography } from '@material-ui/core' ;
15
13
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' ;
20
16
import { exampleSetup } from 'prosemirror-example-setup' ;
21
17
import { keymap } from 'prosemirror-keymap' ;
22
- import { useState , useRef } from 'react' ;
18
+ import { useState , useRef , useEffect } from 'react' ;
23
19
import { makeStyles } from '@material-ui/core/styles' ;
24
20
import useCurrentUser from 'lib/useCurrentUser' ;
25
- import cx from 'clsx' ;
26
21
import PlaceholderPlugin from './Placeholder' ;
27
22
import getConfig from 'next/config' ;
23
+ import CollabHistory from './CollabHistory' ;
28
24
29
25
const {
30
26
publicRuntimeConfig : { PUBLIC_COLLAB_SERVER_URL } ,
@@ -85,24 +81,84 @@ const colors = [
85
81
86
82
const color = colors [ Math . floor ( Math . random ( ) * colors . length ) ] ;
87
83
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
+ */
88
133
const CollabEditor = ( { article } ) => {
89
134
const editor = useRef ( null ) ;
90
- const [ editorView , setEditorView ] = useState ( null ) ;
135
+ const [ showEditor , setShowEditor ] = useState ( null ) ;
136
+ const [ isSynced , setIsSynced ] = useState ( false ) ;
91
137
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
+
92
143
const onTranscribe = ( ) => {
93
144
if ( ! currentUser ) {
94
145
return alert ( t `Please login first.` ) ;
95
146
}
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 ;
100
147
148
+ setShowEditor ( true ) ;
149
+
150
+ if ( provider ) return ;
151
+ setIsSynced ( false ) ;
101
152
const provider = new HocuspocusProvider ( {
102
153
url : PUBLIC_COLLAB_SERVER_URL ,
103
154
name : article . id ,
104
155
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
+ } ,
106
162
// onAwarenessChange: ({ states }) => {
107
163
// console.log('provider', states);
108
164
// },
@@ -111,34 +167,15 @@ const CollabEditor = ({ article }) => {
111
167
name : currentUser . name ,
112
168
color,
113
169
} ) ;
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 ) ;
136
171
} ;
137
172
138
173
const onDone = ( ) => {
139
- if ( editorView ) {
174
+ // get EditorView: https://github.com/ponymessenger/use-prosemirror#prosemirror-
175
+ const prosemirrorEditorView = editor . current ?. view ;
176
+ if ( prosemirrorEditorView ) {
140
177
let text = '' ;
141
- editorView . state . doc . content . forEach ( node => {
178
+ prosemirrorEditorView . state . doc . content . forEach ( node => {
142
179
// console.log(node.textContent);
143
180
// console.log(node.type.name);
144
181
if ( node . textContent ) {
@@ -149,9 +186,8 @@ const CollabEditor = ({ article }) => {
149
186
150
187
// TODO: listen textChanged event?
151
188
article . text = text ;
152
- editorView . destroy ( ) ;
153
189
}
154
- setEditorView ( null ) ;
190
+ setShowEditor ( false ) ;
155
191
} ;
156
192
157
193
const classes = useStyles ( ) ;
@@ -168,7 +204,7 @@ const CollabEditor = ({ article }) => {
168
204
>
169
205
{ t `No transcripts yet` }
170
206
</ Typography >
171
- { ! editorView ? (
207
+ { ! showEditor ? (
172
208
< >
173
209
< Button
174
210
color = "primary"
@@ -192,22 +228,24 @@ const CollabEditor = ({ article }) => {
192
228
>
193
229
{ t `Transcript` }
194
230
</ 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
+ ) }
207
245
</ >
208
246
) }
209
247
</ div >
210
- { ! editorView ? (
248
+ { ! showEditor ? (
211
249
< >
212
250
{ article . text &&
213
251
nl2br (
@@ -220,11 +258,19 @@ const CollabEditor = ({ article }) => {
220
258
) }
221
259
</ >
222
260
) : 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 : (
228
274
< >
229
275
< div className = { classes . transcriptFooter } >
230
276
< Button
0 commit comments