|
@@ -11,21 +11,73 @@ export function useMultiplayerState(roomId: string) {
|
11
|
11
|
const [app, setApp] = React.useState<TldrawApp>()
|
12
|
12
|
const [error, setError] = React.useState<Error>()
|
13
|
13
|
const [loading, setLoading] = React.useState(true)
|
14
|
|
- const rExpectingUpdate = React.useRef(false)
|
15
|
14
|
|
16
|
15
|
const room = useRoom()
|
17
|
16
|
const onUndo = useUndo()
|
18
|
17
|
const onRedo = useRedo()
|
19
|
18
|
const updateMyPresence = useUpdateMyPresence()
|
20
|
19
|
|
21
|
|
- // Document Changes --------
|
22
|
|
-
|
23
|
20
|
const rLiveShapes = React.useRef<LiveMap<string, TDShape>>()
|
24
|
21
|
const rLiveBindings = React.useRef<LiveMap<string, TDBinding>>()
|
25
|
22
|
|
|
23
|
+ // Callbacks --------------
|
|
24
|
+
|
|
25
|
+ // Put the state into the window, for debugging.
|
|
26
|
+ const onMount = React.useCallback(
|
|
27
|
+ (app: TldrawApp) => {
|
|
28
|
+ app.loadRoom(roomId)
|
|
29
|
+ app.pause() // Turn off the app's own undo / redo stack
|
|
30
|
+ window.app = app
|
|
31
|
+ setApp(app)
|
|
32
|
+ },
|
|
33
|
+ [roomId]
|
|
34
|
+ )
|
|
35
|
+
|
|
36
|
+ // Update the live shapes when the app's shapes change.
|
|
37
|
+ const onChangePage = React.useCallback(
|
|
38
|
+ (
|
|
39
|
+ app: TldrawApp,
|
|
40
|
+ shapes: Record<string, TDShape | undefined>,
|
|
41
|
+ bindings: Record<string, TDBinding | undefined>
|
|
42
|
+ ) => {
|
|
43
|
+ room.batch(() => {
|
|
44
|
+ const lShapes = rLiveShapes.current
|
|
45
|
+ const lBindings = rLiveBindings.current
|
|
46
|
+
|
|
47
|
+ if (!(lShapes && lBindings)) return
|
|
48
|
+
|
|
49
|
+ Object.entries(shapes).forEach(([id, shape]) => {
|
|
50
|
+ if (!shape) {
|
|
51
|
+ lShapes.delete(id)
|
|
52
|
+ } else {
|
|
53
|
+ lShapes.set(shape.id, shape)
|
|
54
|
+ }
|
|
55
|
+ })
|
|
56
|
+
|
|
57
|
+ Object.entries(bindings).forEach(([id, binding]) => {
|
|
58
|
+ if (!binding) {
|
|
59
|
+ lBindings.delete(id)
|
|
60
|
+ } else {
|
|
61
|
+ lBindings.set(binding.id, binding)
|
|
62
|
+ }
|
|
63
|
+ })
|
|
64
|
+ })
|
|
65
|
+ },
|
|
66
|
+ [room]
|
|
67
|
+ )
|
|
68
|
+
|
|
69
|
+ // Handle presence updates when the user's pointer / selection changes
|
|
70
|
+ const onChangePresence = React.useCallback(
|
|
71
|
+ (app: TldrawApp, user: TDUser) => {
|
|
72
|
+ updateMyPresence({ id: app.room?.userId, user })
|
|
73
|
+ },
|
|
74
|
+ [updateMyPresence]
|
|
75
|
+ )
|
|
76
|
+
|
|
77
|
+ // Document Changes --------
|
|
78
|
+
|
26
|
79
|
React.useEffect(() => {
|
27
|
80
|
const unsubs: (() => void)[] = []
|
28
|
|
-
|
29
|
81
|
if (!(app && room)) return
|
30
|
82
|
// Handle errors
|
31
|
83
|
unsubs.push(room.subscribe('error', (error) => setError(error)))
|
|
@@ -67,6 +119,8 @@ export function useMultiplayerState(roomId: string) {
|
67
|
119
|
window.addEventListener('beforeunload', handleExit)
|
68
|
120
|
unsubs.push(() => window.removeEventListener('beforeunload', handleExit))
|
69
|
121
|
|
|
122
|
+ let stillAlive = true
|
|
123
|
+
|
70
|
124
|
// Setup the document's storage and subscriptions
|
71
|
125
|
async function setupDocument() {
|
72
|
126
|
const storage = await room.getStorage<any>()
|
|
@@ -87,25 +141,6 @@ export function useMultiplayerState(roomId: string) {
|
87
|
141
|
}
|
88
|
142
|
rLiveBindings.current = lBindings
|
89
|
143
|
|
90
|
|
- // Subscribe to changes
|
91
|
|
- function handleChanges() {
|
92
|
|
- if (rExpectingUpdate.current) {
|
93
|
|
- rExpectingUpdate.current = false
|
94
|
|
- return
|
95
|
|
- }
|
96
|
|
-
|
97
|
|
- app?.replacePageContent(
|
98
|
|
- Object.fromEntries(lShapes.entries()),
|
99
|
|
- Object.fromEntries(lBindings.entries())
|
100
|
|
- )
|
101
|
|
- }
|
102
|
|
-
|
103
|
|
- unsubs.push(room.subscribe(lShapes, handleChanges))
|
104
|
|
- unsubs.push(room.subscribe(lBindings, handleChanges))
|
105
|
|
-
|
106
|
|
- // Update the document with initial content
|
107
|
|
- handleChanges()
|
108
|
|
-
|
109
|
144
|
// Migrate previous versions
|
110
|
145
|
const version = storage.root.get('version')
|
111
|
146
|
|
|
@@ -121,7 +156,7 @@ export function useMultiplayerState(roomId: string) {
|
121
|
156
|
migrated?: boolean
|
122
|
157
|
}>
|
123
|
158
|
|
124
|
|
- // No doc? No problem. This was likely
|
|
159
|
+ // No doc? No problem. This was likely a newer document
|
125
|
160
|
if (doc) {
|
126
|
161
|
const {
|
127
|
162
|
document: {
|
|
@@ -129,91 +164,41 @@ export function useMultiplayerState(roomId: string) {
|
129
|
164
|
page: { shapes, bindings },
|
130
|
165
|
},
|
131
|
166
|
},
|
132
|
|
- } = doc.toObject() as { document: TDDocument }
|
133
|
|
-
|
134
|
|
- for (const key in shapes) {
|
135
|
|
- const shape = shapes[key]
|
136
|
|
- lShapes.set(shape.id, shape)
|
137
|
|
- }
|
|
167
|
+ } = doc.toObject()
|
138
|
168
|
|
139
|
|
- for (const key in bindings) {
|
140
|
|
- const binding = bindings[key]
|
141
|
|
- lBindings.set(binding.id, binding)
|
142
|
|
- }
|
|
169
|
+ Object.values(shapes).forEach((shape) => lShapes.set(shape.id, shape))
|
|
170
|
+ Object.values(bindings).forEach((binding) => lBindings.set(binding.id, binding))
|
143
|
171
|
}
|
144
|
172
|
}
|
145
|
173
|
|
146
|
174
|
// Save the version number for future migrations
|
147
|
175
|
storage.root.set('version', 2)
|
148
|
176
|
|
149
|
|
- setLoading(false)
|
150
|
|
- }
|
|
177
|
+ // Subscribe to changes
|
|
178
|
+ const handleChanges = () => {
|
|
179
|
+ app?.replacePageContent(
|
|
180
|
+ Object.fromEntries(lShapes.entries()),
|
|
181
|
+ Object.fromEntries(lBindings.entries())
|
|
182
|
+ )
|
|
183
|
+ }
|
|
184
|
+
|
|
185
|
+ if (stillAlive) {
|
|
186
|
+ unsubs.push(room.subscribe(lShapes, handleChanges))
|
|
187
|
+
|
|
188
|
+ // Update the document with initial content
|
|
189
|
+ handleChanges()
|
151
|
190
|
|
|
191
|
+ setLoading(false)
|
|
192
|
+ }
|
|
193
|
+ }
|
152
|
194
|
setupDocument()
|
153
|
195
|
|
154
|
196
|
return () => {
|
|
197
|
+ stillAlive = false
|
155
|
198
|
unsubs.forEach((unsub) => unsub())
|
156
|
199
|
}
|
157
|
200
|
}, [room, app])
|
158
|
201
|
|
159
|
|
- // Callbacks --------------
|
160
|
|
-
|
161
|
|
- // Put the state into the window, for debugging.
|
162
|
|
- const onMount = React.useCallback(
|
163
|
|
- (app: TldrawApp) => {
|
164
|
|
- app.loadRoom(roomId)
|
165
|
|
- app.pause() // Turn off the app's own undo / redo stack
|
166
|
|
- window.app = app
|
167
|
|
- setApp(app)
|
168
|
|
- },
|
169
|
|
- [roomId]
|
170
|
|
- )
|
171
|
|
-
|
172
|
|
- // Update the live shapes when the app's shapes change.
|
173
|
|
- const onChangePage = React.useCallback(
|
174
|
|
- (
|
175
|
|
- app: TldrawApp,
|
176
|
|
- shapes: Record<string, TDShape | undefined>,
|
177
|
|
- bindings: Record<string, TDBinding | undefined>
|
178
|
|
- ) => {
|
179
|
|
- room.batch(() => {
|
180
|
|
- const lShapes = rLiveShapes.current
|
181
|
|
- const lBindings = rLiveBindings.current
|
182
|
|
-
|
183
|
|
- if (!(lShapes && lBindings)) return
|
184
|
|
-
|
185
|
|
- for (const id in shapes) {
|
186
|
|
- const shape = shapes[id]
|
187
|
|
- if (!shape) {
|
188
|
|
- lShapes.delete(id)
|
189
|
|
- } else {
|
190
|
|
- lShapes.set(shape.id, shape)
|
191
|
|
- }
|
192
|
|
- }
|
193
|
|
-
|
194
|
|
- for (const id in bindings) {
|
195
|
|
- const binding = bindings[id]
|
196
|
|
- if (!binding) {
|
197
|
|
- lBindings.delete(id)
|
198
|
|
- } else {
|
199
|
|
- lBindings.set(binding.id, binding)
|
200
|
|
- }
|
201
|
|
- }
|
202
|
|
-
|
203
|
|
- rExpectingUpdate.current = true
|
204
|
|
- })
|
205
|
|
- },
|
206
|
|
- [room]
|
207
|
|
- )
|
208
|
|
-
|
209
|
|
- // Handle presence updates when the user's pointer / selection changes
|
210
|
|
- const onChangePresence = React.useCallback(
|
211
|
|
- (app: TldrawApp, user: TDUser) => {
|
212
|
|
- updateMyPresence({ id: app.room?.userId, user })
|
213
|
|
- },
|
214
|
|
- [updateMyPresence]
|
215
|
|
- )
|
216
|
|
-
|
217
|
202
|
return {
|
218
|
203
|
onUndo,
|
219
|
204
|
onRedo,
|