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