@ngenux/ngage-whiteboarding 1.0.8 → 1.0.10
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/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +269 -124
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +275 -122
- package/dist/index.js.map +1 -1
- package/dist/src/components/Whiteboard/index.d.ts +5 -0
- package/dist/src/components/Whiteboard/index.d.ts.map +1 -1
- package/dist/src/context/WhiteboardContext.d.ts +7 -0
- package/dist/src/context/WhiteboardContext.d.ts.map +1 -1
- package/dist/src/utils/socket-utility.d.ts +6 -1
- package/dist/src/utils/socket-utility.d.ts.map +1 -1
- package/dist/utils/index.d.ts +13 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/socket-utility.esm.js +168 -0
- package/dist/utils/socket-utility.esm.js.map +1 -0
- package/dist/utils/socket-utility.js +177 -0
- package/dist/utils/socket-utility.js.map +1 -0
- package/dist/utils/src/components/Shapes/Arrow.d.ts +11 -0
- package/dist/utils/src/components/Shapes/Arrow.d.ts.map +1 -0
- package/dist/utils/src/components/Shapes/Ellipse.d.ts +11 -0
- package/dist/utils/src/components/Shapes/Ellipse.d.ts.map +1 -0
- package/dist/utils/src/components/Shapes/ErasedShape.d.ts +11 -0
- package/dist/utils/src/components/Shapes/ErasedShape.d.ts.map +1 -0
- package/dist/utils/src/components/Shapes/FreehandDrawing.d.ts +11 -0
- package/dist/utils/src/components/Shapes/FreehandDrawing.d.ts.map +1 -0
- package/dist/utils/src/components/Shapes/Line.d.ts +11 -0
- package/dist/utils/src/components/Shapes/Line.d.ts.map +1 -0
- package/dist/utils/src/components/Shapes/Rectangle.d.ts +11 -0
- package/dist/utils/src/components/Shapes/Rectangle.d.ts.map +1 -0
- package/dist/utils/src/components/Whiteboard/Board.d.ts +15 -0
- package/dist/utils/src/components/Whiteboard/Board.d.ts.map +1 -0
- package/dist/utils/src/components/Whiteboard/Toolbar.d.ts +21 -0
- package/dist/utils/src/components/Whiteboard/Toolbar.d.ts.map +1 -0
- package/dist/utils/src/components/Whiteboard/index.d.ts +11 -0
- package/dist/utils/src/components/Whiteboard/index.d.ts.map +1 -0
- package/dist/utils/src/context/WhiteboardContext.d.ts +128 -0
- package/dist/utils/src/context/WhiteboardContext.d.ts.map +1 -0
- package/dist/utils/src/hooks/useCapture.d.ts +4 -0
- package/dist/utils/src/hooks/useCapture.d.ts.map +1 -0
- package/dist/utils/src/hooks/useCollaborativeWhiteboard.d.ts +27 -0
- package/dist/utils/src/hooks/useCollaborativeWhiteboard.d.ts.map +1 -0
- package/dist/utils/src/lib/utils.d.ts +3 -0
- package/dist/utils/src/lib/utils.d.ts.map +1 -0
- package/dist/utils/src/types/index.d.ts +123 -0
- package/dist/utils/src/types/index.d.ts.map +1 -0
- package/dist/utils/src/utils/compression.d.ts +14 -0
- package/dist/utils/src/utils/compression.d.ts.map +1 -0
- package/dist/utils/src/utils/socket-utility.d.ts +11 -0
- package/dist/utils/src/utils/socket-utility.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { WhiteboardProvider } from './src/context/WhiteboardContext';
|
|
|
4
4
|
import { useCapture } from './src/hooks/useCapture';
|
|
5
5
|
import { cn } from './src/lib/utils';
|
|
6
6
|
export { Whiteboard, WhiteboardProvider, useCapture as useWhiteboardStream, cn };
|
|
7
|
+
export { isSocketConnected, getSocket, onSocketStatusChange, onSend, onReceive, leaveRoom, disconnectSocket, waitForSocket } from './src/utils/socket-utility';
|
|
7
8
|
export type { WhiteboardProps } from './src/components/Whiteboard/index';
|
|
8
9
|
export type { ShapeProps, ToolType, StrokeStyle, WhiteboardState, DrawingAction, CompressedData } from './src/types';
|
|
9
10
|
export interface WhiteboardProviderProps {
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.tsx"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,EAAE,EAAE,MAAM,iBAAiB,CAAC;AAErC,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,UAAU,IAAI,mBAAmB,EAAE,EAAE,EAAE,CAAC;AAGjF,YAAY,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AACzE,YAAY,EACV,UAAU,EACV,QAAQ,EACR,WAAW,EACX,eAAe,EACf,aAAa,EACb,cAAc,EACf,MAAM,aAAa,CAAC;AAGrB,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.tsx"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,EAAE,EAAE,MAAM,iBAAiB,CAAC;AAErC,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,UAAU,IAAI,mBAAmB,EAAE,EAAE,EAAE,CAAC;AAGjF,OAAO,EACL,iBAAiB,EACjB,SAAS,EACT,oBAAoB,EACpB,MAAM,EACN,SAAS,EACT,SAAS,EACT,gBAAgB,EAChB,aAAa,EACd,MAAM,4BAA4B,CAAC;AAGpC,YAAY,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AACzE,YAAY,EACV,UAAU,EACV,QAAQ,EACR,WAAW,EACX,eAAe,EACf,aAAa,EACb,cAAc,EACf,MAAM,aAAa,CAAC;AAGrB,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB"}
|
package/dist/index.esm.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
2
|
import * as React from 'react';
|
|
3
|
-
import React__default, { createContext, useReducer, useContext, useState,
|
|
3
|
+
import React__default, { createContext, useRef, useReducer, useEffect, useContext, useState, useMemo, forwardRef, useCallback, useImperativeHandle, createElement } from 'react';
|
|
4
4
|
|
|
5
5
|
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
|
6
6
|
|
|
@@ -34911,6 +34911,21 @@ const whiteboardReducer = (state, action) => {
|
|
|
34911
34911
|
userUndoStacks: {}, // Clear all user undo stacks as well
|
|
34912
34912
|
};
|
|
34913
34913
|
}
|
|
34914
|
+
case 'RESET_EMPTY_ROOM': {
|
|
34915
|
+
// Same visual reset as a fresh board, but lastClearTimestamp stays 0 so shapes from
|
|
34916
|
+
// sync_state (drawn before this client joined) are not rejected as pre-clear stale.
|
|
34917
|
+
return {
|
|
34918
|
+
...state,
|
|
34919
|
+
shapes: [],
|
|
34920
|
+
history: [[]],
|
|
34921
|
+
historyIndex: 0,
|
|
34922
|
+
selectedShapeId: undefined,
|
|
34923
|
+
currentDrawingShape: undefined,
|
|
34924
|
+
activeDrawings: {},
|
|
34925
|
+
userUndoStacks: {},
|
|
34926
|
+
lastClearTimestamp: 0,
|
|
34927
|
+
};
|
|
34928
|
+
}
|
|
34914
34929
|
case 'UNDO': {
|
|
34915
34930
|
if (state.historyIndex > 0) {
|
|
34916
34931
|
return {
|
|
@@ -35116,8 +35131,120 @@ const whiteboardReducer = (state, action) => {
|
|
|
35116
35131
|
}
|
|
35117
35132
|
};
|
|
35118
35133
|
const WhiteboardContext = createContext(undefined);
|
|
35119
|
-
|
|
35120
|
-
|
|
35134
|
+
// Helper function to get storage key for a room
|
|
35135
|
+
const getStorageKey = (roomId) => {
|
|
35136
|
+
return roomId ? `whiteboard-state-${roomId}` : 'whiteboard-state-default';
|
|
35137
|
+
};
|
|
35138
|
+
// Helper function to load persisted state from sessionStorage
|
|
35139
|
+
const loadPersistedState = (roomId) => {
|
|
35140
|
+
try {
|
|
35141
|
+
const key = getStorageKey(roomId);
|
|
35142
|
+
const persisted = sessionStorage.getItem(key);
|
|
35143
|
+
if (persisted) {
|
|
35144
|
+
const parsed = JSON.parse(persisted);
|
|
35145
|
+
return parsed;
|
|
35146
|
+
}
|
|
35147
|
+
}
|
|
35148
|
+
catch (error) {
|
|
35149
|
+
console.error('[PERSISTENCE] Failed to load persisted state:', error);
|
|
35150
|
+
}
|
|
35151
|
+
return null;
|
|
35152
|
+
};
|
|
35153
|
+
// Helper function to save state to sessionStorage
|
|
35154
|
+
const savePersistedState = (state, roomId) => {
|
|
35155
|
+
try {
|
|
35156
|
+
const key = getStorageKey(roomId);
|
|
35157
|
+
// Only persist essential state (shapes, history, background, canvas size)
|
|
35158
|
+
const stateToPersist = {
|
|
35159
|
+
shapes: state.shapes,
|
|
35160
|
+
history: state.history,
|
|
35161
|
+
historyIndex: state.historyIndex,
|
|
35162
|
+
backgroundColor: state.backgroundColor,
|
|
35163
|
+
canvasSize: state.canvasSize,
|
|
35164
|
+
lastClearTimestamp: state.lastClearTimestamp,
|
|
35165
|
+
userUndoStacks: state.userUndoStacks,
|
|
35166
|
+
};
|
|
35167
|
+
sessionStorage.setItem(key, JSON.stringify(stateToPersist));
|
|
35168
|
+
}
|
|
35169
|
+
catch (error) {
|
|
35170
|
+
console.error('[PERSISTENCE] Failed to save persisted state:', error);
|
|
35171
|
+
}
|
|
35172
|
+
};
|
|
35173
|
+
// Helper function to clear persisted state
|
|
35174
|
+
const clearPersistedState = (roomId) => {
|
|
35175
|
+
try {
|
|
35176
|
+
const key = getStorageKey(roomId);
|
|
35177
|
+
sessionStorage.removeItem(key);
|
|
35178
|
+
}
|
|
35179
|
+
catch (error) {
|
|
35180
|
+
console.error('[PERSISTENCE] Failed to clear persisted state:', error);
|
|
35181
|
+
}
|
|
35182
|
+
};
|
|
35183
|
+
const WhiteboardProvider = ({ children, webSocketUrl, roomId: initialRoomId }) => {
|
|
35184
|
+
const [currentRoomId, setCurrentRoomId] = React__default.useState(initialRoomId);
|
|
35185
|
+
// Track if a clear action was performed
|
|
35186
|
+
const clearActionRef = useRef(false);
|
|
35187
|
+
// Load persisted state if available, otherwise use initialState
|
|
35188
|
+
const getInitialState = () => {
|
|
35189
|
+
const persisted = loadPersistedState(currentRoomId);
|
|
35190
|
+
if (persisted) {
|
|
35191
|
+
// Merge persisted state with current initialState to ensure all properties exist
|
|
35192
|
+
return {
|
|
35193
|
+
...initialState,
|
|
35194
|
+
...persisted,
|
|
35195
|
+
// Reset transient state
|
|
35196
|
+
isDrawing: false,
|
|
35197
|
+
currentDrawingShape: undefined,
|
|
35198
|
+
selectedShapeId: undefined,
|
|
35199
|
+
activeDrawings: {},
|
|
35200
|
+
captureEnabled: true,
|
|
35201
|
+
};
|
|
35202
|
+
}
|
|
35203
|
+
return initialState;
|
|
35204
|
+
};
|
|
35205
|
+
const [state, dispatch] = useReducer((s, action) => {
|
|
35206
|
+
// Track clear action
|
|
35207
|
+
if (action.type === 'CLEAR_CANVAS') {
|
|
35208
|
+
clearActionRef.current = true;
|
|
35209
|
+
}
|
|
35210
|
+
return whiteboardReducer(s, action);
|
|
35211
|
+
}, getInitialState());
|
|
35212
|
+
const prevRoomIdRef = useRef(currentRoomId);
|
|
35213
|
+
// Save state to sessionStorage whenever it changes (debounced)
|
|
35214
|
+
useEffect(() => {
|
|
35215
|
+
const timeoutId = setTimeout(() => {
|
|
35216
|
+
// Only save empty shapes if a clear action was performed
|
|
35217
|
+
if (state.shapes.length > 0 || clearActionRef.current) {
|
|
35218
|
+
savePersistedState(state, currentRoomId);
|
|
35219
|
+
clearActionRef.current = false;
|
|
35220
|
+
}
|
|
35221
|
+
}, 500); // Debounce to avoid excessive writes
|
|
35222
|
+
return () => clearTimeout(timeoutId);
|
|
35223
|
+
}, [state.shapes, state.history, state.backgroundColor, state.canvasSize, state.lastClearTimestamp, currentRoomId]);
|
|
35224
|
+
// Handle room changes - load new room state or clear if switching rooms
|
|
35225
|
+
useEffect(() => {
|
|
35226
|
+
if (prevRoomIdRef.current !== currentRoomId) {
|
|
35227
|
+
// Save current room state before switching
|
|
35228
|
+
if (prevRoomIdRef.current) {
|
|
35229
|
+
savePersistedState(state, prevRoomIdRef.current);
|
|
35230
|
+
}
|
|
35231
|
+
// Load new room state
|
|
35232
|
+
const newRoomState = loadPersistedState(currentRoomId);
|
|
35233
|
+
if (newRoomState) {
|
|
35234
|
+
dispatch({ type: 'SET_SHAPES', payload: newRoomState.shapes || [] });
|
|
35235
|
+
dispatch({ type: 'SET_BACKGROUND_COLOR', payload: newRoomState.backgroundColor || '#FFFFFF' });
|
|
35236
|
+
if (newRoomState.lastClearTimestamp) {
|
|
35237
|
+
dispatch({ type: 'SET_CLEAR_TIMESTAMP', payload: newRoomState.lastClearTimestamp });
|
|
35238
|
+
}
|
|
35239
|
+
}
|
|
35240
|
+
else {
|
|
35241
|
+
// No persisted state for new room — empty locally but do not use CLEAR_CANVAS
|
|
35242
|
+
// (that sets lastClearTimestamp = now and breaks applying peer sync_state for pre-join strokes).
|
|
35243
|
+
dispatch({ type: 'RESET_EMPTY_ROOM' });
|
|
35244
|
+
}
|
|
35245
|
+
prevRoomIdRef.current = currentRoomId;
|
|
35246
|
+
}
|
|
35247
|
+
}, [currentRoomId, state]);
|
|
35121
35248
|
const normalizePoint = (point) => {
|
|
35122
35249
|
return {
|
|
35123
35250
|
x: point.x / state.canvasSize.width,
|
|
@@ -35198,7 +35325,7 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35198
35325
|
};
|
|
35199
35326
|
};
|
|
35200
35327
|
// Store reference to current queue action for state requests
|
|
35201
|
-
|
|
35328
|
+
const currentQueueActionRef = useRef(null);
|
|
35202
35329
|
// Type guard and shape normalizer to ensure we have a complete ShapeProps object
|
|
35203
35330
|
const isCompleteShape = (obj) => {
|
|
35204
35331
|
if (!obj) {
|
|
@@ -35254,13 +35381,17 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35254
35381
|
return true;
|
|
35255
35382
|
};
|
|
35256
35383
|
const requestStateFromPeers = () => {
|
|
35257
|
-
if (
|
|
35258
|
-
|
|
35259
|
-
|
|
35260
|
-
|
|
35261
|
-
|
|
35262
|
-
|
|
35263
|
-
|
|
35384
|
+
if (currentQueueActionRef.current) {
|
|
35385
|
+
setTimeout(() => {
|
|
35386
|
+
if (currentQueueActionRef.current) {
|
|
35387
|
+
currentQueueActionRef.current({
|
|
35388
|
+
type: 'request_state',
|
|
35389
|
+
payload: '',
|
|
35390
|
+
requesterId: state.userId,
|
|
35391
|
+
timestamp: Date.now(),
|
|
35392
|
+
});
|
|
35393
|
+
}
|
|
35394
|
+
}, 2000);
|
|
35264
35395
|
}
|
|
35265
35396
|
else {
|
|
35266
35397
|
console.warn('[STATE_SYNC] No queue action available for state request');
|
|
@@ -35292,9 +35423,9 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35292
35423
|
switch (action.type) {
|
|
35293
35424
|
case 'request_state':
|
|
35294
35425
|
// Another user is requesting current state, send our state to them
|
|
35295
|
-
if (typeof action.payload === 'string' && action.requesterId &&
|
|
35426
|
+
if (typeof action.payload === 'string' && action.requesterId && currentQueueActionRef.current) {
|
|
35296
35427
|
const currentState = getCurrentSyncState();
|
|
35297
|
-
|
|
35428
|
+
currentQueueActionRef.current({
|
|
35298
35429
|
type: 'sync_state',
|
|
35299
35430
|
payload: currentState,
|
|
35300
35431
|
requesterId: action.requesterId, // Send specifically to the requester
|
|
@@ -35368,16 +35499,19 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35368
35499
|
console.warn(`[APPLY_ACTION] Failed to delete shape - payload not string:`, action.payload);
|
|
35369
35500
|
}
|
|
35370
35501
|
break;
|
|
35371
|
-
case 'clear':
|
|
35372
|
-
//
|
|
35502
|
+
case 'clear': {
|
|
35503
|
+
// All users (not just host) must clear their local state when a collaborative clear is received
|
|
35373
35504
|
dispatch({ type: 'CLEAR_CANVAS' });
|
|
35374
35505
|
dispatch({ type: 'CLEAR_ACTIVE_DRAWINGS' });
|
|
35506
|
+
// Clear persisted state in sessionStorage
|
|
35507
|
+
clearPersistedState(currentRoomId);
|
|
35375
35508
|
// Update last clear time to prevent conflicting actions
|
|
35376
35509
|
if (typeof action.payload === 'object' && action.payload !== null && 'timestamp' in action.payload) {
|
|
35377
35510
|
const clearTimestamp = action.payload.timestamp;
|
|
35378
35511
|
dispatch({ type: 'SET_CLEAR_TIMESTAMP', payload: clearTimestamp });
|
|
35379
35512
|
}
|
|
35380
35513
|
break;
|
|
35514
|
+
}
|
|
35381
35515
|
case 'undo':
|
|
35382
35516
|
case 'redo':
|
|
35383
35517
|
if (Array.isArray(action.payload)) {
|
|
@@ -35553,8 +35687,9 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35553
35687
|
getCurrentSyncState,
|
|
35554
35688
|
requestStateFromPeers,
|
|
35555
35689
|
setQueueAction: (queueAction) => {
|
|
35556
|
-
|
|
35690
|
+
currentQueueActionRef.current = queueAction;
|
|
35557
35691
|
},
|
|
35692
|
+
setCurrentRoomId,
|
|
35558
35693
|
normalizePoint,
|
|
35559
35694
|
denormalizePoint,
|
|
35560
35695
|
webSocketUrl,
|
|
@@ -37069,32 +37204,6 @@ const Eraser = createLucideIcon("Eraser", [
|
|
|
37069
37204
|
*/
|
|
37070
37205
|
|
|
37071
37206
|
|
|
37072
|
-
const LockOpen = createLucideIcon("LockOpen", [
|
|
37073
|
-
["rect", { width: "18", height: "11", x: "3", y: "11", rx: "2", ry: "2", key: "1w4ew1" }],
|
|
37074
|
-
["path", { d: "M7 11V7a5 5 0 0 1 9.9-1", key: "1mm8w8" }]
|
|
37075
|
-
]);
|
|
37076
|
-
|
|
37077
|
-
/**
|
|
37078
|
-
* @license lucide-react v0.460.0 - ISC
|
|
37079
|
-
*
|
|
37080
|
-
* This source code is licensed under the ISC license.
|
|
37081
|
-
* See the LICENSE file in the root directory of this source tree.
|
|
37082
|
-
*/
|
|
37083
|
-
|
|
37084
|
-
|
|
37085
|
-
const Lock = createLucideIcon("Lock", [
|
|
37086
|
-
["rect", { width: "18", height: "11", x: "3", y: "11", rx: "2", ry: "2", key: "1w4ew1" }],
|
|
37087
|
-
["path", { d: "M7 11V7a5 5 0 0 1 10 0v4", key: "fwvmzm" }]
|
|
37088
|
-
]);
|
|
37089
|
-
|
|
37090
|
-
/**
|
|
37091
|
-
* @license lucide-react v0.460.0 - ISC
|
|
37092
|
-
*
|
|
37093
|
-
* This source code is licensed under the ISC license.
|
|
37094
|
-
* See the LICENSE file in the root directory of this source tree.
|
|
37095
|
-
*/
|
|
37096
|
-
|
|
37097
|
-
|
|
37098
37207
|
const Minus = createLucideIcon("Minus", [["path", { d: "M5 12h14", key: "1ays0h" }]]);
|
|
37099
37208
|
|
|
37100
37209
|
/**
|
|
@@ -37175,7 +37284,7 @@ const Undo2 = createLucideIcon("Undo2", [
|
|
|
37175
37284
|
]);
|
|
37176
37285
|
|
|
37177
37286
|
// Top Toolbar Component
|
|
37178
|
-
const TopToolbar = ({ queueAction, handleExportImage, handleClear, handleLockToggle, isAdmin = false, hasToolAccess = false, isGloballyUnlocked =
|
|
37287
|
+
const TopToolbar = ({ queueAction, handleExportImage, handleClear, handleLockToggle, isAdmin = false, hasToolAccess = false, isGloballyUnlocked = true, shouldBeOpenByDefault = true, hasVideoBackground = false }) => {
|
|
37179
37288
|
const { state, dispatch } = useWhiteboard();
|
|
37180
37289
|
const [isVisible, setIsVisible] = useState(shouldBeOpenByDefault);
|
|
37181
37290
|
const [isInitialized, setIsInitialized] = useState(false);
|
|
@@ -37251,9 +37360,7 @@ const TopToolbar = ({ queueAction, handleExportImage, handleClear, handleLockTog
|
|
|
37251
37360
|
if (!isInitialized) {
|
|
37252
37361
|
return null;
|
|
37253
37362
|
}
|
|
37254
|
-
return (jsxs(Fragment, { children: [jsxs("div", { className: "absolute top-5 left-1/2 transform -translate-x-1/2 flex flex-col items-center z-10", children: [!isVisible && (jsx("button", { className: "w-10 h-10 flex items-center justify-center bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-300", onClick: handleToggleVisibility, title: "Show Tools", children: jsx(ChevronDown, { size: 16, className: "text-current" }) })), isVisible && (jsx("div", { className: "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg", children: jsxs("div", { className: "flex items-center gap-1 p-1", children: [
|
|
37255
|
-
? 'bg-green-100 dark:bg-green-900/50 text-green-600 dark:text-green-400 hover:bg-green-200 dark:hover:bg-green-900/70'
|
|
37256
|
-
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'}`, onClick: handleLockToggle, title: isGloballyUnlocked ? 'Whiteboard unlocked for all users - Click to lock' : 'Whiteboard locked - Click to unlock for all users', children: isGloballyUnlocked ? jsx(LockOpen, { size: 16, className: "text-current" }) : jsx(Lock, { size: 16, className: "text-current" }) })), jsx("button", { className: `w-10 h-10 flex items-center justify-center rounded ${hasToolAccess ? 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-300' : 'opacity-50 cursor-not-allowed text-gray-400 dark:text-gray-600'}`, onClick: handleUndo, disabled: !hasToolAccess, title: hasToolAccess ? 'Undo' : 'Access restricted', children: jsx(Undo2, { size: 16, className: "text-current" }) }), jsx("button", { className: `w-10 h-10 flex items-center justify-center rounded ${hasToolAccess ? 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-300' : 'opacity-50 cursor-not-allowed text-gray-400 dark:text-gray-600'}`, onClick: handleRedo, disabled: !hasToolAccess, title: hasToolAccess ? 'Redo' : 'Access restricted', children: jsx(Redo2, { size: 16, className: "text-current" }) }), jsx("div", { className: "w-px h-6 bg-gray-300 dark:bg-gray-600 mx-1" }), tools.map((tool) => (jsx("button", { className: `w-10 h-10 flex items-center justify-center rounded transition-colors ${!hasToolAccess
|
|
37363
|
+
return (jsxs(Fragment, { children: [jsxs("div", { className: "absolute top-5 left-1/2 transform -translate-x-1/2 flex flex-col items-center z-10", children: [!isVisible && (jsx("button", { className: "w-10 h-10 flex items-center justify-center bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-300", onClick: handleToggleVisibility, title: "Show Tools", children: jsx(ChevronDown, { size: 16, className: "text-current" }) })), isVisible && (jsx("div", { className: "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg", children: jsxs("div", { className: "flex items-center gap-1 p-1", children: [jsx("button", { className: `w-10 h-10 flex items-center justify-center rounded ${hasToolAccess ? 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-300' : 'opacity-50 cursor-not-allowed text-gray-400 dark:text-gray-600'}`, onClick: handleUndo, disabled: !hasToolAccess, title: hasToolAccess ? 'Undo' : 'Access restricted', children: jsx(Undo2, { size: 16, className: "text-current" }) }), jsx("button", { className: `w-10 h-10 flex items-center justify-center rounded ${hasToolAccess ? 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-300' : 'opacity-50 cursor-not-allowed text-gray-400 dark:text-gray-600'}`, onClick: handleRedo, disabled: !hasToolAccess, title: hasToolAccess ? 'Redo' : 'Access restricted', children: jsx(Redo2, { size: 16, className: "text-current" }) }), jsx("div", { className: "w-px h-6 bg-gray-300 dark:bg-gray-600 mx-1" }), tools.map((tool) => (jsx("button", { className: `w-10 h-10 flex items-center justify-center rounded transition-colors ${!hasToolAccess
|
|
37257
37364
|
? 'opacity-50 cursor-not-allowed text-gray-400 dark:text-gray-600'
|
|
37258
37365
|
: state.tool === tool.type
|
|
37259
37366
|
? 'bg-purple-100 dark:bg-purple-900/50 text-purple-600 dark:text-purple-300'
|
|
@@ -42311,6 +42418,7 @@ Object.assign(lookup, {
|
|
|
42311
42418
|
let socket = null;
|
|
42312
42419
|
const joinedRooms = new Set();
|
|
42313
42420
|
const setupCallbacks = new Map();
|
|
42421
|
+
const statusChangeCallbacks = new Set();
|
|
42314
42422
|
let currentWebSocketUrl = undefined;
|
|
42315
42423
|
// Initialize socket connection
|
|
42316
42424
|
const initializeSocket = (webSocketUrl) => {
|
|
@@ -42326,7 +42434,6 @@ const initializeSocket = (webSocketUrl) => {
|
|
|
42326
42434
|
setupCallbacks.clear();
|
|
42327
42435
|
}
|
|
42328
42436
|
currentWebSocketUrl = webSocketUrl;
|
|
42329
|
-
console.log('[SOCKET] Using socket server URL:', socketServerUrl);
|
|
42330
42437
|
if (!socketServerUrl) {
|
|
42331
42438
|
console.error('[SOCKET] No socket server URL provided');
|
|
42332
42439
|
}
|
|
@@ -42337,29 +42444,21 @@ const initializeSocket = (webSocketUrl) => {
|
|
|
42337
42444
|
reconnectionDelay: 1000,
|
|
42338
42445
|
});
|
|
42339
42446
|
socket.on('connect', () => {
|
|
42340
|
-
|
|
42341
|
-
// Re-join all rooms after reconnection
|
|
42447
|
+
statusChangeCallbacks.forEach(callback => callback(true));
|
|
42342
42448
|
joinedRooms.forEach(roomId => {
|
|
42343
42449
|
socket.emit('join-room', roomId);
|
|
42344
|
-
console.log('[SOCKET] Re-joined room:', roomId);
|
|
42345
42450
|
});
|
|
42346
42451
|
});
|
|
42347
42452
|
socket.on('disconnect', () => {
|
|
42348
|
-
|
|
42453
|
+
statusChangeCallbacks.forEach(callback => callback(false));
|
|
42349
42454
|
});
|
|
42350
|
-
socket.on('connect_error', (
|
|
42351
|
-
|
|
42455
|
+
socket.on('connect_error', () => {
|
|
42456
|
+
// Connection error handled by Socket.IO reconnection logic
|
|
42352
42457
|
});
|
|
42353
42458
|
// Set up the global receive-message listener once
|
|
42354
42459
|
socket.on('receive-message', (message) => {
|
|
42355
42460
|
const callback = setupCallbacks.get(message.roomId);
|
|
42356
42461
|
if (callback) {
|
|
42357
|
-
console.log('[SOCKET] Received message from room:', message.roomId, {
|
|
42358
|
-
compression: message.data.compressionType,
|
|
42359
|
-
originalSize: message.data.originalSize,
|
|
42360
|
-
compressedSize: message.data.compressedSize,
|
|
42361
|
-
from: message.from
|
|
42362
|
-
});
|
|
42363
42462
|
callback(message.data);
|
|
42364
42463
|
}
|
|
42365
42464
|
});
|
|
@@ -42393,18 +42492,23 @@ const onSend = (roomId, data, webSocketUrl) => {
|
|
|
42393
42492
|
console.error('[SOCKET] Error sending message:', error);
|
|
42394
42493
|
}
|
|
42395
42494
|
};
|
|
42396
|
-
const onReceive = (roomId, callback, webSocketUrl) => {
|
|
42495
|
+
const onReceive = (roomId, callback, webSocketUrl, onRoomJoined) => {
|
|
42397
42496
|
const socketInstance = initializeSocket(webSocketUrl);
|
|
42398
42497
|
// Store the callback for this room
|
|
42399
42498
|
setupCallbacks.set(roomId, callback);
|
|
42400
|
-
// Only join the room if we haven't already
|
|
42401
42499
|
if (!joinedRooms.has(roomId)) {
|
|
42402
42500
|
socketInstance.emit('join-room', roomId);
|
|
42403
42501
|
joinedRooms.add(roomId);
|
|
42404
|
-
|
|
42502
|
+
if (onRoomJoined) {
|
|
42503
|
+
setTimeout(() => {
|
|
42504
|
+
onRoomJoined();
|
|
42505
|
+
}, 100);
|
|
42506
|
+
}
|
|
42405
42507
|
}
|
|
42406
42508
|
else {
|
|
42407
|
-
|
|
42509
|
+
if (onRoomJoined) {
|
|
42510
|
+
onRoomJoined();
|
|
42511
|
+
}
|
|
42408
42512
|
}
|
|
42409
42513
|
};
|
|
42410
42514
|
const leaveRoom = (roomId) => {
|
|
@@ -42421,24 +42525,74 @@ const disconnectSocket = () => {
|
|
|
42421
42525
|
socket = null;
|
|
42422
42526
|
joinedRooms.clear();
|
|
42423
42527
|
setupCallbacks.clear();
|
|
42424
|
-
console.log('[SOCKET] Socket disconnected and cleaned up');
|
|
42425
42528
|
}
|
|
42426
42529
|
};
|
|
42530
|
+
// Get current socket connection status
|
|
42531
|
+
const isSocketConnected = () => {
|
|
42532
|
+
return socket?.connected ?? false;
|
|
42533
|
+
};
|
|
42534
|
+
// Get current socket instance
|
|
42535
|
+
const getSocket = () => {
|
|
42536
|
+
return socket;
|
|
42537
|
+
};
|
|
42538
|
+
// Subscribe to connection status changes
|
|
42539
|
+
const onSocketStatusChange = (callback) => {
|
|
42540
|
+
const socketInstance = socket || initializeSocket();
|
|
42541
|
+
statusChangeCallbacks.add(callback);
|
|
42542
|
+
callback(socketInstance.connected);
|
|
42543
|
+
return () => {
|
|
42544
|
+
statusChangeCallbacks.delete(callback);
|
|
42545
|
+
};
|
|
42546
|
+
};
|
|
42547
|
+
const waitForSocket = (webSocketUrl, timeoutMs = 5000) => {
|
|
42548
|
+
return new Promise((resolve) => {
|
|
42549
|
+
const socketInstance = initializeSocket(webSocketUrl);
|
|
42550
|
+
if (socketInstance.connected) {
|
|
42551
|
+
resolve();
|
|
42552
|
+
return;
|
|
42553
|
+
}
|
|
42554
|
+
const handleConnect = () => {
|
|
42555
|
+
socketInstance.off('connect', handleConnect);
|
|
42556
|
+
clearTimeout(timeoutHandle);
|
|
42557
|
+
resolve();
|
|
42558
|
+
};
|
|
42559
|
+
socketInstance.on('connect', handleConnect);
|
|
42560
|
+
const timeoutHandle = setTimeout(() => {
|
|
42561
|
+
socketInstance.off('connect', handleConnect);
|
|
42562
|
+
resolve();
|
|
42563
|
+
}, timeoutMs);
|
|
42564
|
+
});
|
|
42565
|
+
};
|
|
42427
42566
|
|
|
42428
|
-
const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transparentBackground = false, videoStream }) => {
|
|
42429
|
-
const { state, dispatch, setQueueAction, requestStateFromPeers, webSocketUrl } = useWhiteboard();
|
|
42567
|
+
const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transparentBackground = false, videoStream, externalApiRef }) => {
|
|
42568
|
+
const { state, dispatch, setQueueAction, requestStateFromPeers, setCurrentRoomId, webSocketUrl } = useWhiteboard();
|
|
42430
42569
|
const [lastCollaborativeAction, setLastCollaborativeAction] = useState(null);
|
|
42431
42570
|
const [hasRequestedState, setHasRequestedState] = useState(false);
|
|
42432
42571
|
const [lastStateRequestTime, setLastStateRequestTime] = useState(0);
|
|
42433
|
-
const [
|
|
42572
|
+
const [isSocketConnected, setIsSocketConnected] = useState(false);
|
|
42573
|
+
const [isRoomJoined, setIsRoomJoined] = useState(false);
|
|
42574
|
+
const [isGloballyUnlocked, setIsGloballyUnlocked] = useState(true); // Global unlock status
|
|
42434
42575
|
const [syncedAllowedUsers, setSyncedAllowedUsers] = useState(allowedUsers); // Synced allowed users from collaboration
|
|
42435
42576
|
const isCollaborativeUpdateRef = useRef(false); // Track if update came from collaboration
|
|
42436
42577
|
const lastClearTimeRef = useRef(0); // Track when last clear happened
|
|
42437
42578
|
const boardRef = useRef(null);
|
|
42579
|
+
useEffect(() => {
|
|
42580
|
+
const unsubscribe = onSocketStatusChange((connected) => {
|
|
42581
|
+
setIsSocketConnected(connected);
|
|
42582
|
+
});
|
|
42583
|
+
return () => {
|
|
42584
|
+
console.log('[WHITEBOARD] Unsubscribing from socket status listener');
|
|
42585
|
+
unsubscribe();
|
|
42586
|
+
};
|
|
42587
|
+
}, []); // Empty dependencies - single listener for component lifecycle
|
|
42438
42588
|
// Set userId in context when component mounts or userId changes
|
|
42439
42589
|
useEffect(() => {
|
|
42440
42590
|
dispatch({ type: 'SET_USER_ID', payload: userId });
|
|
42441
42591
|
}, [userId, dispatch]);
|
|
42592
|
+
// Set current roomId in context for persistence
|
|
42593
|
+
useEffect(() => {
|
|
42594
|
+
setCurrentRoomId(roomId);
|
|
42595
|
+
}, [roomId, setCurrentRoomId]);
|
|
42442
42596
|
// Set background color based on transparentBackground prop or videoStream presence
|
|
42443
42597
|
useEffect(() => {
|
|
42444
42598
|
if (transparentBackground || videoStream) {
|
|
@@ -42515,7 +42669,9 @@ const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transp
|
|
|
42515
42669
|
setHasRequestedState(false);
|
|
42516
42670
|
}
|
|
42517
42671
|
callback(data);
|
|
42518
|
-
}, webSocketUrl)
|
|
42672
|
+
}, webSocketUrl, () => {
|
|
42673
|
+
setIsRoomJoined(true);
|
|
42674
|
+
});
|
|
42519
42675
|
}, [roomId, webSocketUrl]);
|
|
42520
42676
|
// Initialize the collaborative whiteboard hook
|
|
42521
42677
|
const collaborativeConfig = useMemo(() => ({
|
|
@@ -42552,48 +42708,43 @@ const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transp
|
|
|
42552
42708
|
});
|
|
42553
42709
|
}
|
|
42554
42710
|
}, [allowedUsers, queueAction, userId, syncedAllowedUsers]);
|
|
42555
|
-
// Request state from peers when
|
|
42711
|
+
// Request state from peers when room is successfully joined AND socket is connected
|
|
42712
|
+
// Need both conditions to ensure message can actually be sent
|
|
42556
42713
|
useEffect(() => {
|
|
42557
|
-
if (
|
|
42558
|
-
|
|
42559
|
-
|
|
42560
|
-
|
|
42561
|
-
|
|
42562
|
-
|
|
42563
|
-
if (now - lastStateRequestTime > 5000) {
|
|
42564
|
-
requestStateFromPeers();
|
|
42565
|
-
setHasRequestedState(true);
|
|
42566
|
-
setLastStateRequestTime(now);
|
|
42567
|
-
}
|
|
42568
|
-
}, 500);
|
|
42569
|
-
return () => clearTimeout(requestTimer);
|
|
42714
|
+
if (!isRoomJoined || !isSocketConnected || hasRequestedState || state.shapes.length > 0 || !queueAction) {
|
|
42715
|
+
return;
|
|
42716
|
+
}
|
|
42717
|
+
const now = Date.now();
|
|
42718
|
+
if (now - lastClearTimeRef.current < 10000) {
|
|
42719
|
+
return;
|
|
42570
42720
|
}
|
|
42571
|
-
|
|
42721
|
+
requestStateFromPeers();
|
|
42722
|
+
setHasRequestedState(true);
|
|
42723
|
+
setLastStateRequestTime(Date.now());
|
|
42724
|
+
}, [isRoomJoined, isSocketConnected, state.shapes.length, hasRequestedState, queueAction, requestStateFromPeers, userId, roomId]);
|
|
42572
42725
|
// Reset request flag when room changes
|
|
42573
42726
|
useEffect(() => {
|
|
42574
42727
|
setHasRequestedState(false);
|
|
42575
42728
|
setLastStateRequestTime(0);
|
|
42729
|
+
setIsRoomJoined(false);
|
|
42576
42730
|
}, [roomId]);
|
|
42577
|
-
//
|
|
42731
|
+
// Retry state request if canvas is still empty (e.g. first request raced before host had queueAction).
|
|
42732
|
+
// Intentionally does NOT gate on hasRequestedState — the initial effect sets that immediately while send is delayed 2s.
|
|
42578
42733
|
useEffect(() => {
|
|
42579
|
-
if (queueAction
|
|
42580
|
-
|
|
42581
|
-
|
|
42582
|
-
|
|
42583
|
-
|
|
42584
|
-
|
|
42585
|
-
|
|
42586
|
-
}
|
|
42587
|
-
if (now - lastStateRequestTime > 10000) {
|
|
42588
|
-
const reconnectTimer = setTimeout(() => {
|
|
42589
|
-
requestStateFromPeers();
|
|
42590
|
-
setHasRequestedState(true);
|
|
42591
|
-
setLastStateRequestTime(now);
|
|
42592
|
-
}, 2000);
|
|
42593
|
-
return () => clearTimeout(reconnectTimer);
|
|
42594
|
-
}
|
|
42734
|
+
if (!isRoomJoined || !isSocketConnected || state.shapes.length > 0 || !queueAction) {
|
|
42735
|
+
return;
|
|
42736
|
+
}
|
|
42737
|
+
const now = Date.now();
|
|
42738
|
+
// Don't retry if we recently cleared
|
|
42739
|
+
if (now - lastClearTimeRef.current < 10000) {
|
|
42740
|
+
return;
|
|
42595
42741
|
}
|
|
42596
|
-
|
|
42742
|
+
const retryTimer = setTimeout(() => {
|
|
42743
|
+
requestStateFromPeers();
|
|
42744
|
+
setLastStateRequestTime(Date.now());
|
|
42745
|
+
}, 4500);
|
|
42746
|
+
return () => clearTimeout(retryTimer);
|
|
42747
|
+
}, [isRoomJoined, isSocketConnected, state.shapes.length, queueAction, requestStateFromPeers, roomId]);
|
|
42597
42748
|
// Cleanup stale active drawings periodically
|
|
42598
42749
|
useEffect(() => {
|
|
42599
42750
|
const cleanupInterval = setInterval(() => {
|
|
@@ -42692,6 +42843,22 @@ const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transp
|
|
|
42692
42843
|
});
|
|
42693
42844
|
}
|
|
42694
42845
|
}, [isAdmin, dispatch, queueAction, userId]);
|
|
42846
|
+
// Expose a minimal imperative API for host apps that need to perform the same
|
|
42847
|
+
// collaborative clear as the toolbar button (e.g., on "stop screenshare with annotations").
|
|
42848
|
+
useEffect(() => {
|
|
42849
|
+
if (!externalApiRef) {
|
|
42850
|
+
return;
|
|
42851
|
+
}
|
|
42852
|
+
externalApiRef.current = {
|
|
42853
|
+
clear: handleClear,
|
|
42854
|
+
getRoomId: () => roomId,
|
|
42855
|
+
};
|
|
42856
|
+
return () => {
|
|
42857
|
+
if (externalApiRef.current?.getRoomId?.() === roomId) {
|
|
42858
|
+
externalApiRef.current = null;
|
|
42859
|
+
}
|
|
42860
|
+
};
|
|
42861
|
+
}, [externalApiRef, handleClear, roomId]);
|
|
42695
42862
|
const handleLockToggle = useCallback(() => {
|
|
42696
42863
|
if (!isAdmin) {
|
|
42697
42864
|
return;
|
|
@@ -42774,28 +42941,6 @@ const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transp
|
|
|
42774
42941
|
leaveRoom(roomId);
|
|
42775
42942
|
};
|
|
42776
42943
|
}, [roomId]);
|
|
42777
|
-
// Clear all canvases on component unmount if admin leaves
|
|
42778
|
-
useEffect(() => {
|
|
42779
|
-
return () => {
|
|
42780
|
-
// If admin leaves, clear all users' canvases
|
|
42781
|
-
if (isAdmin && queueAction) {
|
|
42782
|
-
const clearTimestamp = Date.now();
|
|
42783
|
-
// Clear local state immediately
|
|
42784
|
-
dispatch({ type: 'CLEAR_CANVAS' });
|
|
42785
|
-
dispatch({ type: 'CLEAR_ACTIVE_DRAWINGS' });
|
|
42786
|
-
// Send clear action to all users
|
|
42787
|
-
queueAction({
|
|
42788
|
-
type: 'clear',
|
|
42789
|
-
payload: {
|
|
42790
|
-
timestamp: clearTimestamp,
|
|
42791
|
-
adminId: userId,
|
|
42792
|
-
},
|
|
42793
|
-
userId: userId,
|
|
42794
|
-
timestamp: clearTimestamp,
|
|
42795
|
-
});
|
|
42796
|
-
}
|
|
42797
|
-
};
|
|
42798
|
-
}, [isAdmin, queueAction, userId, dispatch]);
|
|
42799
42944
|
// Global cleanup on app unmount
|
|
42800
42945
|
useEffect(() => {
|
|
42801
42946
|
const handleBeforeUnload = () => {
|
|
@@ -45838,5 +45983,5 @@ function cn(...inputs) {
|
|
|
45838
45983
|
return twMerge(clsx(inputs));
|
|
45839
45984
|
}
|
|
45840
45985
|
|
|
45841
|
-
export { Whiteboard, WhiteboardProvider, cn, useCapture as useWhiteboardStream };
|
|
45986
|
+
export { Whiteboard, WhiteboardProvider, cn, disconnectSocket, getSocket, isSocketConnected, leaveRoom, onReceive, onSend, onSocketStatusChange, useCapture as useWhiteboardStream, waitForSocket };
|
|
45842
45987
|
//# sourceMappingURL=index.esm.js.map
|