@tangle-network/ui 11.4.0 → 11.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +27 -2
- package/dist/chat.js +3 -3
- package/dist/{chunk-4ORVXSMU.js → chunk-J2G7KCYH.js} +13 -14
- package/dist/{chunk-7O22XQ4D.js → chunk-PLWRAS5Z.js} +1 -1
- package/dist/{chunk-NZLLDHEG.js → chunk-QGTIBEPX.js} +608 -420
- package/dist/{chunk-SRAKCVPO.js → chunk-R5GVQDL3.js} +1 -1
- package/dist/{chunk-IH2VRPGZ.js → chunk-RKC3GXHE.js} +2 -2
- package/dist/{chunk-J3YZIQNI.js → chunk-XPYPZMYT.js} +1 -1
- package/dist/{chunk-HW4ROISY.js → chunk-XROQW5J7.js} +1 -1
- package/dist/{document-editor-pane-MX3FIZSE.js → document-editor-pane-A3JURPW5.js} +1 -1
- package/dist/{document-editor-pane-C3cYEDWZ.d.ts → document-editor-pane-IAEQn_Dl.d.ts} +8 -4
- package/dist/editor.d.ts +5 -4
- package/dist/editor.js +1 -1
- package/dist/files.d.ts +1 -1
- package/dist/files.js +1 -1
- package/dist/hooks.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +12 -8
- package/dist/run.d.ts +48 -1
- package/dist/run.js +9 -5
- package/dist/sdk-hooks.js +3 -3
- package/package.json +1 -1
- package/src/editor/document-editor-pane.test.tsx +58 -0
- package/src/editor/editor-lazy.test.tsx +102 -0
- package/src/editor/editor-lazy.ts +40 -0
- package/src/editor/editor-loading.tsx +25 -0
- package/src/editor/editor-peers.test.ts +250 -0
- package/src/editor/editor-peers.ts +272 -0
- package/src/editor/editor-provider.test.tsx +226 -0
- package/src/editor/editor-provider.tsx +301 -265
- package/src/editor/editor-toolbar.tsx +1 -1
- package/src/editor/markdown-document-editor.test.tsx +121 -0
- package/src/editor/markdown-document-editor.tsx +114 -82
- package/src/editor/tiptap-editor.test.tsx +228 -0
- package/src/editor/tiptap-editor.tsx +162 -130
- package/src/run/index.ts +6 -0
- /package/dist/{chunk-47XH56SV.js → chunk-LQS34IGP.js} +0 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { HocuspocusProvider } from "@hocuspocus/provider";
|
|
3
|
+
import type { HocuspocusProvider } from "@hocuspocus/provider";
|
|
4
4
|
import {
|
|
5
|
+
type ComponentType,
|
|
5
6
|
createContext,
|
|
6
7
|
type ReactNode,
|
|
8
|
+
Suspense,
|
|
7
9
|
useCallback,
|
|
8
10
|
useContext,
|
|
9
11
|
useEffect,
|
|
@@ -11,7 +13,9 @@ import {
|
|
|
11
13
|
useRef,
|
|
12
14
|
useState,
|
|
13
15
|
} from "react";
|
|
14
|
-
import * as Y from "yjs";
|
|
16
|
+
import type * as Y from "yjs";
|
|
17
|
+
import { retryableLazyEditor } from "./editor-lazy";
|
|
18
|
+
import { type EditorProviderPeers, loadEditorProviderPeers } from "./editor-peers";
|
|
15
19
|
|
|
16
20
|
/**
|
|
17
21
|
* Connection state for the Hocuspocus provider.
|
|
@@ -54,6 +58,9 @@ export interface EditorContextValue {
|
|
|
54
58
|
disconnect: () => void;
|
|
55
59
|
}
|
|
56
60
|
|
|
61
|
+
// Module scope, and never inside the factory below: `useEditorContext` and
|
|
62
|
+
// every hook in `use-editor` read THIS context, so a per-build context would
|
|
63
|
+
// leave them with no provider.
|
|
57
64
|
const EditorContext = createContext<EditorContextValue | null>(null);
|
|
58
65
|
|
|
59
66
|
/**
|
|
@@ -122,295 +129,324 @@ function generateUserColor(): string {
|
|
|
122
129
|
}
|
|
123
130
|
|
|
124
131
|
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
132
|
+
* Builds the collaboration provider against loaded yjs and Hocuspocus
|
|
133
|
+
* namespaces. The peers arrive as an argument so that this module reaches
|
|
134
|
+
* them through type-only imports, which a bundler erases.
|
|
127
135
|
*/
|
|
128
|
-
export function
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
user,
|
|
134
|
-
autoConnect = true,
|
|
135
|
-
autoReconnect = true,
|
|
136
|
-
maxReconnectAttempts = 5,
|
|
137
|
-
onConnectionChange,
|
|
138
|
-
onSync,
|
|
139
|
-
onAuthError,
|
|
140
|
-
onRefreshToken,
|
|
141
|
-
children,
|
|
142
|
-
}: EditorProviderProps) {
|
|
143
|
-
const [connectionState, setConnectionState] =
|
|
144
|
-
useState<ConnectionState>("disconnected");
|
|
145
|
-
const [collaborators, setCollaborators] = useState<Collaborator[]>([]);
|
|
146
|
-
const [isSynced, setIsSynced] = useState(false);
|
|
147
|
-
|
|
148
|
-
// Use refs to avoid recreating provider on every render
|
|
149
|
-
const docRef = useRef<Y.Doc | null>(null);
|
|
150
|
-
const providerRef = useRef<HocuspocusProvider | null>(null);
|
|
151
|
-
const reconnectAttemptsRef = useRef(0);
|
|
152
|
-
const tokenRef = useRef(token);
|
|
153
|
-
const tokenExpiryRef = useRef<number | undefined>(tokenExpiresAt);
|
|
154
|
-
const refreshPromiseRef = useRef<Promise<string | null> | null>(null);
|
|
155
|
-
const refreshTimerRef = useRef<number | null>(null);
|
|
156
|
-
|
|
157
|
-
tokenRef.current = token;
|
|
158
|
-
tokenExpiryRef.current = tokenExpiresAt;
|
|
159
|
-
|
|
160
|
-
// Initialize Y.Doc once
|
|
161
|
-
if (!docRef.current) {
|
|
162
|
-
docRef.current = new Y.Doc();
|
|
163
|
-
}
|
|
164
|
-
const doc = docRef.current;
|
|
165
|
-
|
|
166
|
-
// User color (generate once per session)
|
|
167
|
-
const userColor = useMemo(
|
|
168
|
-
() => user.color ?? generateUserColor(),
|
|
169
|
-
[user.color],
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
// Update connection state and notify callback
|
|
173
|
-
const updateConnectionState = useCallback(
|
|
174
|
-
(state: ConnectionState) => {
|
|
175
|
-
setConnectionState(state);
|
|
176
|
-
onConnectionChange?.(state);
|
|
177
|
-
},
|
|
178
|
-
[onConnectionChange],
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
// Update collaborators from awareness
|
|
182
|
-
const updateCollaborators = useCallback(
|
|
183
|
-
(awareness: HocuspocusProvider["awareness"]) => {
|
|
184
|
-
if (!awareness) return;
|
|
185
|
-
|
|
186
|
-
const states = awareness.getStates();
|
|
187
|
-
const collabs: Collaborator[] = [];
|
|
188
|
-
|
|
189
|
-
states.forEach((state: Record<string, unknown>, clientId: number) => {
|
|
190
|
-
// Skip our own client
|
|
191
|
-
if (clientId === awareness.clientID) return;
|
|
192
|
-
|
|
193
|
-
if (state.user) {
|
|
194
|
-
collabs.push({
|
|
195
|
-
clientId,
|
|
196
|
-
user: state.user as Collaborator["user"],
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
setCollaborators(collabs);
|
|
202
|
-
},
|
|
203
|
-
[],
|
|
204
|
-
);
|
|
205
|
-
|
|
206
|
-
const clearRefreshTimer = useCallback(() => {
|
|
207
|
-
if (refreshTimerRef.current != null) {
|
|
208
|
-
window.clearTimeout(refreshTimerRef.current);
|
|
209
|
-
refreshTimerRef.current = null;
|
|
210
|
-
}
|
|
211
|
-
}, []);
|
|
136
|
+
export function createEditorProvider(
|
|
137
|
+
peers: EditorProviderPeers,
|
|
138
|
+
): ComponentType<EditorProviderProps> {
|
|
139
|
+
const { Doc } = peers.yjs;
|
|
140
|
+
const { HocuspocusProvider } = peers.hocuspocus;
|
|
212
141
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
142
|
+
return function EditorProvider({
|
|
143
|
+
websocketUrl,
|
|
144
|
+
documentName,
|
|
145
|
+
token,
|
|
146
|
+
tokenExpiresAt,
|
|
147
|
+
user,
|
|
148
|
+
autoConnect = true,
|
|
149
|
+
autoReconnect = true,
|
|
150
|
+
maxReconnectAttempts = 5,
|
|
151
|
+
onConnectionChange,
|
|
152
|
+
onSync,
|
|
153
|
+
onAuthError,
|
|
154
|
+
onRefreshToken,
|
|
155
|
+
children,
|
|
156
|
+
}: EditorProviderProps) {
|
|
157
|
+
const [connectionState, setConnectionState] =
|
|
158
|
+
useState<ConnectionState>("disconnected");
|
|
159
|
+
const [collaborators, setCollaborators] = useState<Collaborator[]>([]);
|
|
160
|
+
const [isSynced, setIsSynced] = useState(false);
|
|
161
|
+
|
|
162
|
+
// Use refs to avoid recreating provider on every render
|
|
163
|
+
const docRef = useRef<Y.Doc | null>(null);
|
|
164
|
+
const providerRef = useRef<HocuspocusProvider | null>(null);
|
|
165
|
+
const reconnectAttemptsRef = useRef(0);
|
|
166
|
+
const tokenRef = useRef(token);
|
|
167
|
+
const tokenExpiryRef = useRef<number | undefined>(tokenExpiresAt);
|
|
168
|
+
const refreshPromiseRef = useRef<Promise<string | null> | null>(null);
|
|
169
|
+
const refreshTimerRef = useRef<number | null>(null);
|
|
170
|
+
|
|
171
|
+
tokenRef.current = token;
|
|
172
|
+
tokenExpiryRef.current = tokenExpiresAt;
|
|
173
|
+
|
|
174
|
+
// Initialize Y.Doc once
|
|
175
|
+
if (!docRef.current) {
|
|
176
|
+
docRef.current = new Doc();
|
|
216
177
|
}
|
|
178
|
+
const doc = docRef.current;
|
|
179
|
+
|
|
180
|
+
// User color (generate once per session)
|
|
181
|
+
const userColor = useMemo(
|
|
182
|
+
() => user.color ?? generateUserColor(),
|
|
183
|
+
[user.color],
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// Update connection state and notify callback
|
|
187
|
+
const updateConnectionState = useCallback(
|
|
188
|
+
(state: ConnectionState) => {
|
|
189
|
+
setConnectionState(state);
|
|
190
|
+
onConnectionChange?.(state);
|
|
191
|
+
},
|
|
192
|
+
[onConnectionChange],
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// Update collaborators from awareness
|
|
196
|
+
const updateCollaborators = useCallback(
|
|
197
|
+
(awareness: HocuspocusProvider["awareness"]) => {
|
|
198
|
+
if (!awareness) return;
|
|
199
|
+
|
|
200
|
+
const states = awareness.getStates();
|
|
201
|
+
const collabs: Collaborator[] = [];
|
|
202
|
+
|
|
203
|
+
states.forEach((state: Record<string, unknown>, clientId: number) => {
|
|
204
|
+
// Skip our own client
|
|
205
|
+
if (clientId === awareness.clientID) return;
|
|
206
|
+
|
|
207
|
+
if (state.user) {
|
|
208
|
+
collabs.push({
|
|
209
|
+
clientId,
|
|
210
|
+
user: state.user as Collaborator["user"],
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
setCollaborators(collabs);
|
|
216
|
+
},
|
|
217
|
+
[],
|
|
218
|
+
);
|
|
217
219
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
220
|
+
const clearRefreshTimer = useCallback(() => {
|
|
221
|
+
if (refreshTimerRef.current != null) {
|
|
222
|
+
window.clearTimeout(refreshTimerRef.current);
|
|
223
|
+
refreshTimerRef.current = null;
|
|
224
|
+
}
|
|
225
|
+
}, []);
|
|
221
226
|
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
const resolvedToken = typeof next === "string" ? next : next.token;
|
|
225
|
-
const resolvedExpiry =
|
|
226
|
-
typeof next === "string" ? undefined : next.expiresAt;
|
|
227
|
-
|
|
228
|
-
tokenRef.current = resolvedToken;
|
|
229
|
-
tokenExpiryRef.current = resolvedExpiry;
|
|
230
|
-
return resolvedToken;
|
|
231
|
-
})()
|
|
232
|
-
.catch((error) => {
|
|
233
|
-
onAuthError?.(
|
|
234
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
235
|
-
);
|
|
227
|
+
const refreshToken = useCallback(async (): Promise<string | null> => {
|
|
228
|
+
if (!onRefreshToken) {
|
|
236
229
|
return null;
|
|
237
|
-
}
|
|
238
|
-
.finally(() => {
|
|
239
|
-
refreshPromiseRef.current = null;
|
|
240
|
-
});
|
|
230
|
+
}
|
|
241
231
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
232
|
+
if (refreshPromiseRef.current) {
|
|
233
|
+
return refreshPromiseRef.current;
|
|
234
|
+
}
|
|
245
235
|
|
|
246
|
-
|
|
247
|
-
|
|
236
|
+
const refreshPromise = (async () => {
|
|
237
|
+
const next = await onRefreshToken();
|
|
238
|
+
const resolvedToken = typeof next === "string" ? next : next.token;
|
|
239
|
+
const resolvedExpiry =
|
|
240
|
+
typeof next === "string" ? undefined : next.expiresAt;
|
|
241
|
+
|
|
242
|
+
tokenRef.current = resolvedToken;
|
|
243
|
+
tokenExpiryRef.current = resolvedExpiry;
|
|
244
|
+
return resolvedToken;
|
|
245
|
+
})()
|
|
246
|
+
.catch((error) => {
|
|
247
|
+
onAuthError?.(
|
|
248
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
249
|
+
);
|
|
250
|
+
return null;
|
|
251
|
+
})
|
|
252
|
+
.finally(() => {
|
|
253
|
+
refreshPromiseRef.current = null;
|
|
254
|
+
});
|
|
248
255
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
typeof window === "undefined"
|
|
253
|
-
) {
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
+
refreshPromiseRef.current = refreshPromise;
|
|
257
|
+
return refreshPromise;
|
|
258
|
+
}, [onAuthError, onRefreshToken]);
|
|
256
259
|
|
|
257
|
-
const
|
|
258
|
-
|
|
260
|
+
const scheduleTokenRefresh = useCallback(() => {
|
|
261
|
+
clearRefreshTimer();
|
|
259
262
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
263
|
+
if (
|
|
264
|
+
!tokenExpiryRef.current ||
|
|
265
|
+
!onRefreshToken ||
|
|
266
|
+
typeof window === "undefined"
|
|
267
|
+
) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
264
270
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}, delay);
|
|
268
|
-
}, [clearRefreshTimer, onRefreshToken, refreshToken]);
|
|
271
|
+
const refreshAtMs = tokenExpiryRef.current * 1000 - 60_000;
|
|
272
|
+
const delay = refreshAtMs - Date.now();
|
|
269
273
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
274
|
+
if (delay <= 0) {
|
|
275
|
+
void refreshToken();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
276
278
|
|
|
277
|
-
|
|
279
|
+
refreshTimerRef.current = window.setTimeout(() => {
|
|
280
|
+
void refreshToken();
|
|
281
|
+
}, delay);
|
|
282
|
+
}, [clearRefreshTimer, onRefreshToken, refreshToken]);
|
|
278
283
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
+
// Connect to the collaboration server
|
|
285
|
+
const connect = useCallback(() => {
|
|
286
|
+
if (providerRef.current) {
|
|
287
|
+
providerRef.current.connect();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
284
290
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
291
|
+
updateConnectionState("connecting");
|
|
292
|
+
|
|
293
|
+
const provider = new HocuspocusProvider({
|
|
294
|
+
url: websocketUrl,
|
|
295
|
+
name: documentName,
|
|
296
|
+
document: doc,
|
|
297
|
+
token: async () => tokenRef.current,
|
|
298
|
+
|
|
299
|
+
onConnect: () => {
|
|
300
|
+
reconnectAttemptsRef.current = 0;
|
|
301
|
+
updateConnectionState("connected");
|
|
302
|
+
scheduleTokenRefresh();
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
onSynced: () => {
|
|
306
|
+
setIsSynced(true);
|
|
307
|
+
updateConnectionState("synced");
|
|
308
|
+
onSync?.();
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
onDisconnect: () => {
|
|
312
|
+
updateConnectionState("disconnected");
|
|
313
|
+
setIsSynced(false);
|
|
314
|
+
clearRefreshTimer();
|
|
315
|
+
|
|
316
|
+
// Auto-reconnect logic
|
|
317
|
+
if (
|
|
318
|
+
autoReconnect &&
|
|
319
|
+
reconnectAttemptsRef.current < maxReconnectAttempts
|
|
320
|
+
) {
|
|
321
|
+
reconnectAttemptsRef.current += 1;
|
|
322
|
+
const delay = Math.min(
|
|
323
|
+
1000 * 2 ** reconnectAttemptsRef.current,
|
|
324
|
+
30000,
|
|
325
|
+
);
|
|
326
|
+
setTimeout(() => {
|
|
327
|
+
if (providerRef.current && !(providerRef.current as any).isConnected) {
|
|
328
|
+
providerRef.current.connect();
|
|
329
|
+
}
|
|
330
|
+
}, delay);
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
onAuthenticationFailed: ({ reason }: { reason?: string }) => {
|
|
335
|
+
const error = new Error(reason ?? "Authentication failed");
|
|
336
|
+
updateConnectionState("disconnected");
|
|
337
|
+
clearRefreshTimer();
|
|
338
|
+
|
|
339
|
+
if (onRefreshToken) {
|
|
340
|
+
void refreshToken().then((nextToken) => {
|
|
341
|
+
if (nextToken && providerRef.current) {
|
|
342
|
+
providerRef.current.connect();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
onAuthError?.(error);
|
|
347
|
+
});
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
onAuthError?.(error);
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
onAwarenessUpdate: () => {
|
|
355
|
+
updateCollaborators(provider.awareness);
|
|
356
|
+
},
|
|
357
|
+
});
|
|
290
358
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
359
|
+
// Set local user awareness
|
|
360
|
+
provider.awareness?.setLocalStateField("user", {
|
|
361
|
+
name: user.name,
|
|
362
|
+
color: userColor,
|
|
363
|
+
userId: user.userId,
|
|
364
|
+
});
|
|
296
365
|
|
|
297
|
-
|
|
366
|
+
providerRef.current = provider;
|
|
367
|
+
}, [
|
|
368
|
+
websocketUrl,
|
|
369
|
+
documentName,
|
|
370
|
+
doc,
|
|
371
|
+
user.name,
|
|
372
|
+
user.userId,
|
|
373
|
+
userColor,
|
|
374
|
+
autoReconnect,
|
|
375
|
+
maxReconnectAttempts,
|
|
376
|
+
clearRefreshTimer,
|
|
377
|
+
updateConnectionState,
|
|
378
|
+
updateCollaborators,
|
|
379
|
+
onSync,
|
|
380
|
+
onAuthError,
|
|
381
|
+
onRefreshToken,
|
|
382
|
+
refreshToken,
|
|
383
|
+
scheduleTokenRefresh,
|
|
384
|
+
]);
|
|
385
|
+
|
|
386
|
+
// Disconnect from the collaboration server
|
|
387
|
+
const disconnect = useCallback(() => {
|
|
388
|
+
if (providerRef.current) {
|
|
389
|
+
providerRef.current.disconnect();
|
|
298
390
|
updateConnectionState("disconnected");
|
|
299
|
-
|
|
300
|
-
|
|
391
|
+
}
|
|
392
|
+
}, [updateConnectionState]);
|
|
301
393
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
reconnectAttemptsRef.current += 1;
|
|
308
|
-
const delay = Math.min(
|
|
309
|
-
1000 * 2 ** reconnectAttemptsRef.current,
|
|
310
|
-
30000,
|
|
311
|
-
);
|
|
312
|
-
setTimeout(() => {
|
|
313
|
-
if (providerRef.current && !(providerRef.current as any).isConnected) {
|
|
314
|
-
providerRef.current.connect();
|
|
315
|
-
}
|
|
316
|
-
}, delay);
|
|
317
|
-
}
|
|
318
|
-
},
|
|
394
|
+
// Auto-connect on mount
|
|
395
|
+
useEffect(() => {
|
|
396
|
+
if (autoConnect) {
|
|
397
|
+
connect();
|
|
398
|
+
}
|
|
319
399
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
updateConnectionState("disconnected");
|
|
400
|
+
return () => {
|
|
401
|
+
// Cleanup on unmount
|
|
323
402
|
clearRefreshTimer();
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if (nextToken && providerRef.current) {
|
|
328
|
-
providerRef.current.connect();
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
onAuthError?.(error);
|
|
333
|
-
});
|
|
334
|
-
return;
|
|
403
|
+
if (providerRef.current) {
|
|
404
|
+
providerRef.current.destroy();
|
|
405
|
+
providerRef.current = null;
|
|
335
406
|
}
|
|
407
|
+
};
|
|
408
|
+
}, [autoConnect, clearRefreshTimer, connect]);
|
|
409
|
+
|
|
410
|
+
// Context value
|
|
411
|
+
const contextValue = useMemo<EditorContextValue>(
|
|
412
|
+
() => ({
|
|
413
|
+
doc,
|
|
414
|
+
provider: providerRef.current,
|
|
415
|
+
connectionState,
|
|
416
|
+
collaborators,
|
|
417
|
+
isSynced,
|
|
418
|
+
connect,
|
|
419
|
+
disconnect,
|
|
420
|
+
}),
|
|
421
|
+
[doc, connectionState, collaborators, isSynced, connect, disconnect],
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
return (
|
|
425
|
+
<EditorContext.Provider value={contextValue}>
|
|
426
|
+
{children}
|
|
427
|
+
</EditorContext.Provider>
|
|
428
|
+
);
|
|
429
|
+
};
|
|
430
|
+
}
|
|
336
431
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
onAwarenessUpdate: () => {
|
|
341
|
-
updateCollaborators(provider.awareness);
|
|
342
|
-
},
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
// Set local user awareness
|
|
346
|
-
provider.awareness?.setLocalStateField("user", {
|
|
347
|
-
name: user.name,
|
|
348
|
-
color: userColor,
|
|
349
|
-
userId: user.userId,
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
providerRef.current = provider;
|
|
353
|
-
}, [
|
|
354
|
-
websocketUrl,
|
|
355
|
-
documentName,
|
|
356
|
-
doc,
|
|
357
|
-
user.name,
|
|
358
|
-
user.userId,
|
|
359
|
-
userColor,
|
|
360
|
-
autoReconnect,
|
|
361
|
-
maxReconnectAttempts,
|
|
362
|
-
clearRefreshTimer,
|
|
363
|
-
updateConnectionState,
|
|
364
|
-
updateCollaborators,
|
|
365
|
-
onSync,
|
|
366
|
-
onAuthError,
|
|
367
|
-
onRefreshToken,
|
|
368
|
-
refreshToken,
|
|
369
|
-
scheduleTokenRefresh,
|
|
370
|
-
]);
|
|
371
|
-
|
|
372
|
-
// Disconnect from the collaboration server
|
|
373
|
-
const disconnect = useCallback(() => {
|
|
374
|
-
if (providerRef.current) {
|
|
375
|
-
providerRef.current.disconnect();
|
|
376
|
-
updateConnectionState("disconnected");
|
|
377
|
-
}
|
|
378
|
-
}, [updateConnectionState]);
|
|
379
|
-
|
|
380
|
-
// Auto-connect on mount
|
|
381
|
-
useEffect(() => {
|
|
382
|
-
if (autoConnect) {
|
|
383
|
-
connect();
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
return () => {
|
|
387
|
-
// Cleanup on unmount
|
|
388
|
-
clearRefreshTimer();
|
|
389
|
-
if (providerRef.current) {
|
|
390
|
-
providerRef.current.destroy();
|
|
391
|
-
providerRef.current = null;
|
|
392
|
-
}
|
|
393
|
-
};
|
|
394
|
-
}, [autoConnect, clearRefreshTimer, connect]);
|
|
395
|
-
|
|
396
|
-
// Context value
|
|
397
|
-
const contextValue = useMemo<EditorContextValue>(
|
|
398
|
-
() => ({
|
|
399
|
-
doc,
|
|
400
|
-
provider: providerRef.current,
|
|
401
|
-
connectionState,
|
|
402
|
-
collaborators,
|
|
403
|
-
isSynced,
|
|
404
|
-
connect,
|
|
405
|
-
disconnect,
|
|
406
|
-
}),
|
|
407
|
-
[doc, connectionState, collaborators, isSynced, connect, disconnect],
|
|
408
|
-
);
|
|
432
|
+
const lazyEditorProvider = retryableLazyEditor(async () =>
|
|
433
|
+
createEditorProvider(await loadEditorProviderPeers()),
|
|
434
|
+
);
|
|
409
435
|
|
|
436
|
+
/**
|
|
437
|
+
* EditorProvider wraps children with Hocuspocus collaboration context.
|
|
438
|
+
* Manages WebSocket connection, Y.Doc, and awareness state. Loads `yjs` and
|
|
439
|
+
* `@hocuspocus/provider` — and no tiptap package — before it renders children,
|
|
440
|
+
* because every child hook reads a context that only the loaded provider can
|
|
441
|
+
* supply. A consumer that drives its own editor from that context therefore
|
|
442
|
+
* needs those two peers alone.
|
|
443
|
+
*/
|
|
444
|
+
export function EditorProvider(props: EditorProviderProps) {
|
|
445
|
+
const LazyEditorProvider = lazyEditorProvider();
|
|
410
446
|
return (
|
|
411
|
-
<
|
|
412
|
-
{
|
|
413
|
-
</
|
|
447
|
+
<Suspense fallback={null}>
|
|
448
|
+
<LazyEditorProvider {...props} />
|
|
449
|
+
</Suspense>
|
|
414
450
|
);
|
|
415
451
|
}
|
|
416
452
|
|