@@ -13,8 +13,14 @@ import * as yorkie from 'yorkie-js-sdk';
13
13
import randomColor from 'randomcolor' ;
14
14
import { uniqueNamesGenerator , names } from 'unique-names-generator' ;
15
15
import _ from 'lodash' ;
16
+ import useUndoRedo from './useUndoRedo' ;
16
17
17
- import type { Options , YorkieDocType , YorkiePresenceType } from './types' ;
18
+ import type {
19
+ Options ,
20
+ YorkieDocType ,
21
+ YorkiePresenceType ,
22
+ TlType ,
23
+ } from './types' ;
18
24
19
25
// Yorkie Client declaration
20
26
let client : yorkie . Client ;
@@ -25,6 +31,7 @@ let doc: yorkie.Document<YorkieDocType, YorkiePresenceType>;
25
31
export function useMultiplayerState ( roomId : string ) {
26
32
const [ app , setApp ] = useState < TldrawApp > ( ) ;
27
33
const [ loading , setLoading ] = useState ( true ) ;
34
+ const { push, undo, redo } = useUndoRedo ( ) ;
28
35
29
36
// Callbacks --------------
30
37
@@ -55,6 +62,43 @@ export function useMultiplayerState(roomId: string) {
55
62
[ roomId ] ,
56
63
) ;
57
64
65
+ // undo
66
+
67
+ const onUndo = useCallback (
68
+ ( app : TldrawApp ) => {
69
+ undo ( ) ;
70
+ } ,
71
+ [ roomId ] ,
72
+ ) ;
73
+
74
+ // redo
75
+
76
+ const onRedo = useCallback (
77
+ ( app : TldrawApp ) => {
78
+ redo ( ) ;
79
+ } ,
80
+ [ roomId ] ,
81
+ ) ;
82
+
83
+ // Subscribe to changes
84
+ function handleChanges ( ) {
85
+ const root = doc . getRoot ( ) ;
86
+
87
+ // Parse proxy object to record
88
+ const shapeRecord : Record < string , TDShape > = JSON . parse (
89
+ root . shapes . toJSON ! ( ) ,
90
+ ) ;
91
+ const bindingRecord : Record < string , TDBinding > = JSON . parse (
92
+ root . bindings . toJSON ! ( ) ,
93
+ ) ;
94
+ const assetRecord : Record < string , TDAsset > = JSON . parse (
95
+ root . assets . toJSON ! ( ) ,
96
+ ) ;
97
+
98
+ // Replace page content with changed(propagated) records
99
+ app ?. replacePageContent ( shapeRecord , bindingRecord , assetRecord ) ;
100
+ }
101
+
58
102
// Update Yorkie doc when the app's shapes change.
59
103
// Prevent overloading yorkie update api call by throttle
60
104
const onChangePage = useThrottleCallback (
@@ -65,6 +109,13 @@ export function useMultiplayerState(roomId: string) {
65
109
) => {
66
110
if ( ! app || client === undefined || doc === undefined ) return ;
67
111
112
+ // Object that stores the latest state value of yorkie doc before the client changes
113
+ const currentYorkieDocSnapshot : TlType = {
114
+ shapes : { } ,
115
+ bindings : { } ,
116
+ assets : { } ,
117
+ } ;
118
+
68
119
const getUpdatedPropertyList = < T extends object > (
69
120
source : T ,
70
121
target : T ,
@@ -76,18 +127,26 @@ export function useMultiplayerState(roomId: string) {
76
127
77
128
Object . entries ( shapes ) . forEach ( ( [ id , shape ] ) => {
78
129
doc . update ( ( root ) => {
130
+ const rootShapesToJS = root . shapes . toJS ! ( ) ;
79
131
if ( ! shape ) {
132
+ currentYorkieDocSnapshot . shapes [ id ] = rootShapesToJS [ id ] ;
80
133
delete root . shapes [ id ] ;
81
134
} else if ( ! root . shapes [ id ] ) {
135
+ currentYorkieDocSnapshot . shapes [ id ] = undefined ! ;
82
136
root . shapes [ id ] = shape ;
83
137
} else {
84
138
const updatedPropertyList = getUpdatedPropertyList (
85
139
shape ,
86
- root . shapes [ id ] ! . toJS ! ( ) ,
140
+ rootShapesToJS [ id ] ,
87
141
) ;
88
-
142
+ currentYorkieDocSnapshot . shapes [ id ] =
143
+ { } as yorkie . JSONObject < TDShape > ;
89
144
updatedPropertyList . forEach ( ( key ) => {
90
145
const newValue = shape [ key ] ;
146
+ const snapshotValue = rootShapesToJS [ id ] [ key ] ;
147
+ ( currentYorkieDocSnapshot . shapes [ id ] [
148
+ key
149
+ ] as typeof snapshotValue ) = snapshotValue ;
91
150
( root . shapes [ id ] [ key ] as typeof newValue ) = newValue ;
92
151
} ) ;
93
152
}
@@ -96,18 +155,26 @@ export function useMultiplayerState(roomId: string) {
96
155
97
156
Object . entries ( bindings ) . forEach ( ( [ id , binding ] ) => {
98
157
doc . update ( ( root ) => {
158
+ const rootBindingsToJS = root . bindings . toJS ! ( ) ;
99
159
if ( ! binding ) {
160
+ currentYorkieDocSnapshot . bindings [ id ] = rootBindingsToJS [ id ] ;
100
161
delete root . bindings [ id ] ;
101
162
} else if ( ! root . bindings [ id ] ) {
163
+ currentYorkieDocSnapshot . bindings [ id ] = undefined ! ;
102
164
root . bindings [ id ] = binding ;
103
165
} else {
104
166
const updatedPropertyList = getUpdatedPropertyList (
105
167
binding ,
106
- root . bindings [ id ] ! . toJS ! ( ) ,
168
+ rootBindingsToJS [ id ] ,
107
169
) ;
108
-
170
+ currentYorkieDocSnapshot . bindings [ id ] =
171
+ { } as yorkie . JSONObject < TDBinding > ;
109
172
updatedPropertyList . forEach ( ( key ) => {
110
173
const newValue = binding [ key ] ;
174
+ const snapshotValue = rootBindingsToJS [ id ] [ key ] ;
175
+ ( currentYorkieDocSnapshot . bindings [ id ] [
176
+ key
177
+ ] as typeof snapshotValue ) = snapshotValue ;
111
178
( root . bindings [ id ] [ key ] as typeof newValue ) = newValue ;
112
179
} ) ;
113
180
}
@@ -118,14 +185,16 @@ export function useMultiplayerState(roomId: string) {
118
185
// Document key for assets should be asset.id (string), not index
119
186
Object . entries ( app . assets ) . forEach ( ( [ , asset ] ) => {
120
187
doc . update ( ( root ) => {
188
+ const rootAssetsToJS = root . assets . toJS ! ( ) ;
189
+ currentYorkieDocSnapshot . assets [ asset . id ] = rootAssetsToJS [ asset . id ] ;
121
190
if ( ! asset . id ) {
122
191
delete root . assets [ asset . id ] ;
123
- } else if ( root . assets [ asset . id ] ) {
192
+ } else if ( ! root . assets [ asset . id ] ) {
124
193
root . assets [ asset . id ] = asset ;
125
194
} else {
126
195
const updatedPropertyList = getUpdatedPropertyList (
127
196
asset ,
128
- root . assets [ asset . id ] ! . toJS ! ( ) ,
197
+ rootAssetsToJS [ asset . id ] ,
129
198
) ;
130
199
131
200
updatedPropertyList . forEach ( ( key ) => {
@@ -135,8 +204,112 @@ export function useMultiplayerState(roomId: string) {
135
204
}
136
205
} ) ;
137
206
} ) ;
207
+
208
+ // Command object for action
209
+ // Undo, redo work the same way
210
+ // undo(): Save yorkie doc's state before returning
211
+ // redo(): Save yorkie doc's state before moving forward
212
+ const command = {
213
+ snapshot : currentYorkieDocSnapshot ,
214
+ undo : ( ) => {
215
+ const currentYorkieDocSnapshot : TlType = {
216
+ shapes : { } ,
217
+ bindings : { } ,
218
+ assets : { } ,
219
+ } ;
220
+ const snapshot = command . snapshot ;
221
+ Object . entries ( snapshot . shapes ) . forEach ( ( [ id , shape ] ) => {
222
+ doc . update ( ( root ) => {
223
+ const rootShapesToJS = root . shapes . toJS ! ( ) ;
224
+ if ( ! shape ) {
225
+ currentYorkieDocSnapshot . shapes [ id ] = rootShapesToJS [ id ] ;
226
+ delete root . shapes [ id ] ;
227
+ } else if ( ! root . shapes . toJS ! ( ) [ id ] ) {
228
+ currentYorkieDocSnapshot . shapes [ id ] = undefined ! ;
229
+ if ( shape . id ) root . shapes [ id ] = shape ;
230
+ } else {
231
+ currentYorkieDocSnapshot . shapes [ id ] =
232
+ { } as yorkie . JSONObject < TDShape > ;
233
+ (
234
+ Object . keys ( snapshot . shapes [ id ] ) as Array < keyof TDShape >
235
+ ) . forEach ( ( key ) => {
236
+ const snapshotValue = snapshot . shapes [ id ] [ key ] ;
237
+ const newSnapshotValue = rootShapesToJS [ id ] [ key ] ;
238
+
239
+ ( currentYorkieDocSnapshot . shapes [ id ] [
240
+ key
241
+ ] as typeof newSnapshotValue ) = newSnapshotValue ;
242
+ ( root . shapes [ id ] [ key ] as typeof snapshotValue ) =
243
+ snapshotValue ;
244
+ } ) ;
245
+ }
246
+ } ) ;
247
+ } ) ;
248
+
249
+ Object . entries ( snapshot . bindings ) . forEach ( ( [ id , binding ] ) => {
250
+ doc . update ( ( root ) => {
251
+ const rootBindingsToJs = root . bindings . toJS ! ( ) ;
252
+ if ( ! binding ) {
253
+ currentYorkieDocSnapshot . bindings [ id ] = rootBindingsToJs [ id ] ;
254
+ delete root . bindings [ id ] ;
255
+ } else if ( ! root . bindings . toJS ! ( ) [ id ] ) {
256
+ currentYorkieDocSnapshot . bindings [ id ] = undefined ! ;
257
+ if ( binding . id ) root . bindings [ id ] = binding ;
258
+ } else {
259
+ currentYorkieDocSnapshot . bindings [ id ] =
260
+ { } as yorkie . JSONObject < TDBinding > ;
261
+ (
262
+ Object . keys ( snapshot . bindings [ id ] ) as Array < keyof TDBinding >
263
+ ) . forEach ( ( key ) => {
264
+ const snapshotValue = snapshot . bindings [ id ] [ key ] ;
265
+ const newSnapshotValue = rootBindingsToJs [ id ] [ key ] ;
266
+
267
+ ( currentYorkieDocSnapshot . bindings [ id ] [
268
+ key
269
+ ] as typeof newSnapshotValue ) = newSnapshotValue ;
270
+ ( root . bindings [ id ] [ key ] as typeof snapshotValue ) =
271
+ snapshotValue ;
272
+ } ) ;
273
+ }
274
+ } ) ;
275
+ } ) ;
276
+
277
+ Object . entries ( snapshot . assets ) . forEach ( ( [ , asset ] ) => {
278
+ doc . update ( ( root ) => {
279
+ const rootAssetsToJs = root . assets . toJS ! ( ) ;
280
+ currentYorkieDocSnapshot . assets [ asset . id ] =
281
+ rootAssetsToJs [ asset . id ] ;
282
+ if ( ! asset . id ) {
283
+ delete root . assets [ asset . id ] ;
284
+ } else if ( ! root . assets . toJS ! ( ) [ asset . id ] ) {
285
+ root . assets [ asset . id ] = asset ;
286
+ } else {
287
+ const updatedPropertyList = getUpdatedPropertyList (
288
+ asset ,
289
+ rootAssetsToJs [ asset . id ] ,
290
+ ) ;
291
+
292
+ updatedPropertyList . forEach ( ( key ) => {
293
+ const newValue = asset [ key ] ;
294
+ ( root . assets [ asset . id ] [ key ] as typeof newValue ) = newValue ;
295
+ } ) ;
296
+ }
297
+ } ) ;
298
+ } ) ;
299
+ command . snapshot = currentYorkieDocSnapshot ;
300
+ // Reflect changes locally
301
+ handleChanges ( ) ;
302
+ } ,
303
+ redo : ( ) => {
304
+ command . undo ( ) ;
305
+ handleChanges ( ) ;
306
+ } ,
307
+ } ;
308
+
309
+ // Create History
310
+ push ( command ) ;
138
311
} ,
139
- 60 ,
312
+ 20 ,
140
313
false ,
141
314
) ;
142
315
@@ -168,25 +341,6 @@ export function useMultiplayerState(roomId: string) {
168
341
169
342
window . addEventListener ( 'beforeunload' , handleDisconnect ) ;
170
343
171
- // Subscribe to changes
172
- function handleChanges ( ) {
173
- const root = doc . getRoot ( ) ;
174
-
175
- // Parse proxy object to record
176
- const shapeRecord : Record < string , TDShape > = JSON . parse (
177
- root . shapes . toJSON ! ( ) ,
178
- ) ;
179
- const bindingRecord : Record < string , TDBinding > = JSON . parse (
180
- root . bindings . toJSON ! ( ) ,
181
- ) ;
182
- const assetRecord : Record < string , TDAsset > = JSON . parse (
183
- root . assets . toJSON ! ( ) ,
184
- ) ;
185
-
186
- // Replace page content with changed(propagated) records
187
- app ?. replacePageContent ( shapeRecord , bindingRecord , assetRecord ) ;
188
- }
189
-
190
344
let stillAlive = true ;
191
345
192
346
// Setup the document's storage and subscriptions
@@ -294,5 +448,7 @@ export function useMultiplayerState(roomId: string) {
294
448
onChangePage,
295
449
loading,
296
450
onChangePresence,
451
+ onUndo,
452
+ onRedo,
297
453
} ;
298
454
}
0 commit comments