@ngenux/ngage-whiteboarding 1.0.7 → 1.0.9
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/README.md +344 -344
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +178 -102
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +185 -101
- package/dist/index.js.map +1 -1
- package/dist/src/components/Whiteboard/Board.d.ts.map +1 -1
- package/dist/src/components/Whiteboard/index.d.ts.map +1 -1
- 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/src/utils/video-coordinates.d.ts +0 -36
- package/dist/src/utils/video-coordinates.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -34777,12 +34777,15 @@ const whiteboardReducer = (state, action) => {
|
|
|
34777
34777
|
// Prevent duplicate shapes during simultaneous drawing
|
|
34778
34778
|
const newShape = action.payload;
|
|
34779
34779
|
const existingShapeIndex = state.shapes.findIndex(shape => shape.id === newShape.id);
|
|
34780
|
+
// Ensure shape has timestamp for proper ordering and stale action filtering
|
|
34781
|
+
const timestamp = newShape.timestamp || Date.now();
|
|
34780
34782
|
// If shape already exists, update it instead of adding duplicate
|
|
34781
34783
|
if (existingShapeIndex >= 0) {
|
|
34782
34784
|
const updatedShapes = [...state.shapes];
|
|
34783
34785
|
updatedShapes[existingShapeIndex] = {
|
|
34784
34786
|
...newShape,
|
|
34785
|
-
|
|
34787
|
+
timestamp,
|
|
34788
|
+
drawingSessionId: newShape.drawingSessionId || `session-${timestamp}-${Math.random().toString(36).substr(2, 9)}`
|
|
34786
34789
|
};
|
|
34787
34790
|
return {
|
|
34788
34791
|
...state,
|
|
@@ -34790,10 +34793,11 @@ const whiteboardReducer = (state, action) => {
|
|
|
34790
34793
|
currentDrawingShape: undefined,
|
|
34791
34794
|
};
|
|
34792
34795
|
}
|
|
34793
|
-
// Add new shape with session tracking
|
|
34796
|
+
// Add new shape with session tracking and timestamp
|
|
34794
34797
|
const shapeWithSession = {
|
|
34795
34798
|
...newShape,
|
|
34796
|
-
|
|
34799
|
+
timestamp,
|
|
34800
|
+
drawingSessionId: newShape.drawingSessionId || `session-${timestamp}-${Math.random().toString(36).substr(2, 9)}`
|
|
34797
34801
|
};
|
|
34798
34802
|
const newShapes = [...state.shapes, shapeWithSession];
|
|
34799
34803
|
const newHistory = state.history.slice(0, state.historyIndex + 1);
|
|
@@ -34987,11 +34991,13 @@ const whiteboardReducer = (state, action) => {
|
|
|
34987
34991
|
const userShapes = state.shapes
|
|
34988
34992
|
.filter(shape => shape.userId === userId)
|
|
34989
34993
|
.sort((a, b) => {
|
|
34990
|
-
// Sort by
|
|
34991
|
-
|
|
34994
|
+
// Sort by timestamp to ensure LIFO (Last In First Out) order
|
|
34995
|
+
const timestampA = a.timestamp || 0;
|
|
34996
|
+
const timestampB = b.timestamp || 0;
|
|
34997
|
+
return timestampA - timestampB;
|
|
34992
34998
|
});
|
|
34993
34999
|
if (userShapes.length > 0) {
|
|
34994
|
-
// Get the most recent shape
|
|
35000
|
+
// Get the most recent shape (last in sorted array)
|
|
34995
35001
|
const lastUserShape = userShapes[userShapes.length - 1];
|
|
34996
35002
|
// Check if there are multiple shapes that were part of the same drawing session
|
|
34997
35003
|
// Use drawingSessionId if available, otherwise fall back to time-based detection
|
|
@@ -35269,12 +35275,16 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35269
35275
|
};
|
|
35270
35276
|
const requestStateFromPeers = () => {
|
|
35271
35277
|
if (currentQueueAction) {
|
|
35272
|
-
|
|
35273
|
-
|
|
35274
|
-
|
|
35275
|
-
|
|
35276
|
-
|
|
35277
|
-
|
|
35278
|
+
setTimeout(() => {
|
|
35279
|
+
if (currentQueueAction) {
|
|
35280
|
+
currentQueueAction({
|
|
35281
|
+
type: 'request_state',
|
|
35282
|
+
payload: '',
|
|
35283
|
+
requesterId: state.userId,
|
|
35284
|
+
timestamp: Date.now(),
|
|
35285
|
+
});
|
|
35286
|
+
}
|
|
35287
|
+
}, 2000);
|
|
35278
35288
|
}
|
|
35279
35289
|
else {
|
|
35280
35290
|
console.warn('[STATE_SYNC] No queue action available for state request');
|
|
@@ -35285,9 +35295,16 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35285
35295
|
// Drawing actions should be filtered if they're older than the last clear
|
|
35286
35296
|
if (action.type === 'start_draw' || action.type === 'continue_draw' || action.type === 'end_draw' || action.type === 'add') {
|
|
35287
35297
|
const actionTimestamp = action.timestamp || 0;
|
|
35288
|
-
if (actionTimestamp <= state.lastClearTimestamp) {
|
|
35298
|
+
if (actionTimestamp > 0 && actionTimestamp <= state.lastClearTimestamp) {
|
|
35289
35299
|
return true;
|
|
35290
35300
|
}
|
|
35301
|
+
// Also check timestamp in the payload if it exists
|
|
35302
|
+
if (typeof action.payload === 'object' && action.payload !== null && !Array.isArray(action.payload)) {
|
|
35303
|
+
const payloadTimestamp = action.payload.timestamp || 0;
|
|
35304
|
+
if (payloadTimestamp > 0 && payloadTimestamp <= state.lastClearTimestamp) {
|
|
35305
|
+
return true;
|
|
35306
|
+
}
|
|
35307
|
+
}
|
|
35291
35308
|
}
|
|
35292
35309
|
return false;
|
|
35293
35310
|
};
|
|
@@ -35317,10 +35334,15 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35317
35334
|
if (syncState.shapes.length === 0) {
|
|
35318
35335
|
return; // Don't apply empty state or keep requesting
|
|
35319
35336
|
}
|
|
35320
|
-
//
|
|
35321
|
-
|
|
35337
|
+
// Filter out shapes that are older than our last clear
|
|
35338
|
+
const validShapes = syncState.shapes.filter(shape => {
|
|
35339
|
+
const shapeTimestamp = shape.timestamp || 0;
|
|
35340
|
+
return shapeTimestamp === 0 || shapeTimestamp > state.lastClearTimestamp;
|
|
35341
|
+
});
|
|
35342
|
+
// Only apply if the received state has valid shapes
|
|
35343
|
+
if (validShapes.length > 0 && (state.shapes.length === 0 || validShapes.length > state.shapes.length)) {
|
|
35322
35344
|
// All shapes from sync_state should have normalized coordinates, denormalize them
|
|
35323
|
-
const denormalizedShapes =
|
|
35345
|
+
const denormalizedShapes = validShapes.map((shape, index) => {
|
|
35324
35346
|
return denormalizeShape(shape);
|
|
35325
35347
|
});
|
|
35326
35348
|
// Apply the synchronized state
|
|
@@ -35338,7 +35360,8 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35338
35360
|
const denormalizedShape = denormalizeShape(action.payload);
|
|
35339
35361
|
// Additional check to prevent adding shapes from before the last clear
|
|
35340
35362
|
const shapeTimestamp = denormalizedShape.timestamp || action.timestamp || 0;
|
|
35341
|
-
if (shapeTimestamp <= state.lastClearTimestamp) {
|
|
35363
|
+
if (shapeTimestamp > 0 && shapeTimestamp <= state.lastClearTimestamp) {
|
|
35364
|
+
console.warn(`[APPLY_ACTION] Skipping stale shape from before clear - shape timestamp: ${shapeTimestamp}, clear timestamp: ${state.lastClearTimestamp}`);
|
|
35342
35365
|
break;
|
|
35343
35366
|
}
|
|
35344
35367
|
dispatch({ type: 'ADD_SHAPE', payload: denormalizedShape });
|
|
@@ -35392,6 +35415,11 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35392
35415
|
case 'start_draw':
|
|
35393
35416
|
if (typeof action.payload !== 'string' && !Array.isArray(action.payload) && isCompleteShape(action.payload)) {
|
|
35394
35417
|
const denormalizedShape = denormalizeShape(action.payload);
|
|
35418
|
+
// Check if this shape is from before the last clear
|
|
35419
|
+
const shapeTimestamp = denormalizedShape.timestamp || action.timestamp || 0;
|
|
35420
|
+
if (shapeTimestamp > 0 && shapeTimestamp <= state.lastClearTimestamp) {
|
|
35421
|
+
break; // Skip stale shapes
|
|
35422
|
+
}
|
|
35395
35423
|
// Only apply collaborative start_draw if it's from another user
|
|
35396
35424
|
if (denormalizedShape.userId !== state.userId) {
|
|
35397
35425
|
// Add to active drawings for real-time collaborative visibility
|
|
@@ -35405,6 +35433,11 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35405
35433
|
case 'continue_draw':
|
|
35406
35434
|
if (typeof action.payload !== 'string' && !Array.isArray(action.payload) && isCompleteShape(action.payload)) {
|
|
35407
35435
|
const denormalizedShape = denormalizeShape(action.payload);
|
|
35436
|
+
// Check if this shape is from before the last clear
|
|
35437
|
+
const shapeTimestamp = denormalizedShape.timestamp || action.timestamp || 0;
|
|
35438
|
+
if (shapeTimestamp > 0 && shapeTimestamp <= state.lastClearTimestamp) {
|
|
35439
|
+
break; // Skip stale shapes
|
|
35440
|
+
}
|
|
35408
35441
|
// Only apply collaborative drawing updates if it's not from the current user
|
|
35409
35442
|
// to avoid interfering with local real-time drawing
|
|
35410
35443
|
if (denormalizedShape.userId !== state.userId) {
|
|
@@ -35419,6 +35452,12 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35419
35452
|
case 'end_draw':
|
|
35420
35453
|
if (typeof action.payload !== 'string' && !Array.isArray(action.payload) && isCompleteShape(action.payload)) {
|
|
35421
35454
|
const denormalizedShape = denormalizeShape(action.payload);
|
|
35455
|
+
// Check if this shape is from before the last clear
|
|
35456
|
+
const shapeTimestamp = denormalizedShape.timestamp || action.timestamp || 0;
|
|
35457
|
+
if (shapeTimestamp > 0 && shapeTimestamp <= state.lastClearTimestamp) {
|
|
35458
|
+
console.warn(`[APPLY_ACTION] Skipping stale end_draw from before clear - shape timestamp: ${shapeTimestamp}, clear timestamp: ${state.lastClearTimestamp}`);
|
|
35459
|
+
break; // Skip stale shapes
|
|
35460
|
+
}
|
|
35422
35461
|
// Only apply collaborative end_draw if it's from another user
|
|
35423
35462
|
// Local user's end_draw is handled directly in Board component via ADD_SHAPE
|
|
35424
35463
|
if (denormalizedShape.userId !== state.userId) {
|
|
@@ -36446,7 +36485,8 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
|
|
|
36446
36485
|
}
|
|
36447
36486
|
// Create new shape ID for regular drawing tools
|
|
36448
36487
|
const newShapeId = v4();
|
|
36449
|
-
const
|
|
36488
|
+
const timestamp = Date.now();
|
|
36489
|
+
const newDrawingSessionId = `session-${timestamp}-${Math.random().toString(36).substr(2, 9)}`;
|
|
36450
36490
|
setCurrentShapeId(newShapeId);
|
|
36451
36491
|
setCurrentDrawingSessionId(newDrawingSessionId);
|
|
36452
36492
|
// Start drawing
|
|
@@ -36463,6 +36503,7 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
|
|
|
36463
36503
|
strokeStyle: state.strokeStyle,
|
|
36464
36504
|
opacity: state.opacity,
|
|
36465
36505
|
drawingSessionId: newDrawingSessionId,
|
|
36506
|
+
timestamp,
|
|
36466
36507
|
// Initialize transformation properties
|
|
36467
36508
|
x: 0,
|
|
36468
36509
|
y: 0,
|
|
@@ -36575,6 +36616,7 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
|
|
|
36575
36616
|
strokeStyle: state.strokeStyle,
|
|
36576
36617
|
opacity: state.opacity,
|
|
36577
36618
|
drawingSessionId: currentDrawingSessionId,
|
|
36619
|
+
timestamp: Date.now(),
|
|
36578
36620
|
// Initialize transformation properties
|
|
36579
36621
|
x: 0,
|
|
36580
36622
|
y: 0,
|
|
@@ -36654,6 +36696,7 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
|
|
|
36654
36696
|
strokeStyle: state.strokeStyle,
|
|
36655
36697
|
opacity: state.opacity,
|
|
36656
36698
|
drawingSessionId: currentDrawingSessionId,
|
|
36699
|
+
timestamp: Date.now(),
|
|
36657
36700
|
// Initialize transformation properties
|
|
36658
36701
|
x: 0,
|
|
36659
36702
|
y: 0,
|
|
@@ -37050,32 +37093,6 @@ const Eraser = createLucideIcon("Eraser", [
|
|
|
37050
37093
|
*/
|
|
37051
37094
|
|
|
37052
37095
|
|
|
37053
|
-
const LockOpen = createLucideIcon("LockOpen", [
|
|
37054
|
-
["rect", { width: "18", height: "11", x: "3", y: "11", rx: "2", ry: "2", key: "1w4ew1" }],
|
|
37055
|
-
["path", { d: "M7 11V7a5 5 0 0 1 9.9-1", key: "1mm8w8" }]
|
|
37056
|
-
]);
|
|
37057
|
-
|
|
37058
|
-
/**
|
|
37059
|
-
* @license lucide-react v0.460.0 - ISC
|
|
37060
|
-
*
|
|
37061
|
-
* This source code is licensed under the ISC license.
|
|
37062
|
-
* See the LICENSE file in the root directory of this source tree.
|
|
37063
|
-
*/
|
|
37064
|
-
|
|
37065
|
-
|
|
37066
|
-
const Lock = createLucideIcon("Lock", [
|
|
37067
|
-
["rect", { width: "18", height: "11", x: "3", y: "11", rx: "2", ry: "2", key: "1w4ew1" }],
|
|
37068
|
-
["path", { d: "M7 11V7a5 5 0 0 1 10 0v4", key: "fwvmzm" }]
|
|
37069
|
-
]);
|
|
37070
|
-
|
|
37071
|
-
/**
|
|
37072
|
-
* @license lucide-react v0.460.0 - ISC
|
|
37073
|
-
*
|
|
37074
|
-
* This source code is licensed under the ISC license.
|
|
37075
|
-
* See the LICENSE file in the root directory of this source tree.
|
|
37076
|
-
*/
|
|
37077
|
-
|
|
37078
|
-
|
|
37079
37096
|
const Minus = createLucideIcon("Minus", [["path", { d: "M5 12h14", key: "1ays0h" }]]);
|
|
37080
37097
|
|
|
37081
37098
|
/**
|
|
@@ -37156,7 +37173,7 @@ const Undo2 = createLucideIcon("Undo2", [
|
|
|
37156
37173
|
]);
|
|
37157
37174
|
|
|
37158
37175
|
// Top Toolbar Component
|
|
37159
|
-
const TopToolbar = ({ queueAction, handleExportImage, handleClear, handleLockToggle, isAdmin = false, hasToolAccess = false, isGloballyUnlocked =
|
|
37176
|
+
const TopToolbar = ({ queueAction, handleExportImage, handleClear, handleLockToggle, isAdmin = false, hasToolAccess = false, isGloballyUnlocked = true, shouldBeOpenByDefault = true, hasVideoBackground = false }) => {
|
|
37160
37177
|
const { state, dispatch } = useWhiteboard();
|
|
37161
37178
|
const [isVisible, setIsVisible] = React.useState(shouldBeOpenByDefault);
|
|
37162
37179
|
const [isInitialized, setIsInitialized] = React.useState(false);
|
|
@@ -37232,9 +37249,7 @@ const TopToolbar = ({ queueAction, handleExportImage, handleClear, handleLockTog
|
|
|
37232
37249
|
if (!isInitialized) {
|
|
37233
37250
|
return null;
|
|
37234
37251
|
}
|
|
37235
|
-
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "absolute top-5 left-1/2 transform -translate-x-1/2 flex flex-col items-center z-10", children: [!isVisible && (jsxRuntime.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: jsxRuntime.jsx(ChevronDown, { size: 16, className: "text-current" }) })), isVisible && (jsxRuntime.jsx("div", { className: "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg", children: jsxRuntime.jsxs("div", { className: "flex items-center gap-1 p-1", children: [
|
|
37236
|
-
? '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'
|
|
37237
|
-
: '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 ? jsxRuntime.jsx(LockOpen, { size: 16, className: "text-current" }) : jsxRuntime.jsx(Lock, { size: 16, className: "text-current" }) })), jsxRuntime.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: jsxRuntime.jsx(Undo2, { size: 16, className: "text-current" }) }), jsxRuntime.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: jsxRuntime.jsx(Redo2, { size: 16, className: "text-current" }) }), jsxRuntime.jsx("div", { className: "w-px h-6 bg-gray-300 dark:bg-gray-600 mx-1" }), tools.map((tool) => (jsxRuntime.jsx("button", { className: `w-10 h-10 flex items-center justify-center rounded transition-colors ${!hasToolAccess
|
|
37252
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "absolute top-5 left-1/2 transform -translate-x-1/2 flex flex-col items-center z-10", children: [!isVisible && (jsxRuntime.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: jsxRuntime.jsx(ChevronDown, { size: 16, className: "text-current" }) })), isVisible && (jsxRuntime.jsx("div", { className: "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg", children: jsxRuntime.jsxs("div", { className: "flex items-center gap-1 p-1", children: [jsxRuntime.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: jsxRuntime.jsx(Undo2, { size: 16, className: "text-current" }) }), jsxRuntime.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: jsxRuntime.jsx(Redo2, { size: 16, className: "text-current" }) }), jsxRuntime.jsx("div", { className: "w-px h-6 bg-gray-300 dark:bg-gray-600 mx-1" }), tools.map((tool) => (jsxRuntime.jsx("button", { className: `w-10 h-10 flex items-center justify-center rounded transition-colors ${!hasToolAccess
|
|
37238
37253
|
? 'opacity-50 cursor-not-allowed text-gray-400 dark:text-gray-600'
|
|
37239
37254
|
: state.tool === tool.type
|
|
37240
37255
|
? 'bg-purple-100 dark:bg-purple-900/50 text-purple-600 dark:text-purple-300'
|
|
@@ -42292,6 +42307,7 @@ Object.assign(lookup, {
|
|
|
42292
42307
|
let socket = null;
|
|
42293
42308
|
const joinedRooms = new Set();
|
|
42294
42309
|
const setupCallbacks = new Map();
|
|
42310
|
+
const statusChangeCallbacks = new Set();
|
|
42295
42311
|
let currentWebSocketUrl = undefined;
|
|
42296
42312
|
// Initialize socket connection
|
|
42297
42313
|
const initializeSocket = (webSocketUrl) => {
|
|
@@ -42307,7 +42323,6 @@ const initializeSocket = (webSocketUrl) => {
|
|
|
42307
42323
|
setupCallbacks.clear();
|
|
42308
42324
|
}
|
|
42309
42325
|
currentWebSocketUrl = webSocketUrl;
|
|
42310
|
-
console.log('[SOCKET] Using socket server URL:', socketServerUrl);
|
|
42311
42326
|
if (!socketServerUrl) {
|
|
42312
42327
|
console.error('[SOCKET] No socket server URL provided');
|
|
42313
42328
|
}
|
|
@@ -42318,29 +42333,21 @@ const initializeSocket = (webSocketUrl) => {
|
|
|
42318
42333
|
reconnectionDelay: 1000,
|
|
42319
42334
|
});
|
|
42320
42335
|
socket.on('connect', () => {
|
|
42321
|
-
|
|
42322
|
-
// Re-join all rooms after reconnection
|
|
42336
|
+
statusChangeCallbacks.forEach(callback => callback(true));
|
|
42323
42337
|
joinedRooms.forEach(roomId => {
|
|
42324
42338
|
socket.emit('join-room', roomId);
|
|
42325
|
-
console.log('[SOCKET] Re-joined room:', roomId);
|
|
42326
42339
|
});
|
|
42327
42340
|
});
|
|
42328
42341
|
socket.on('disconnect', () => {
|
|
42329
|
-
|
|
42342
|
+
statusChangeCallbacks.forEach(callback => callback(false));
|
|
42330
42343
|
});
|
|
42331
|
-
socket.on('connect_error', (
|
|
42332
|
-
|
|
42344
|
+
socket.on('connect_error', () => {
|
|
42345
|
+
// Connection error handled by Socket.IO reconnection logic
|
|
42333
42346
|
});
|
|
42334
42347
|
// Set up the global receive-message listener once
|
|
42335
42348
|
socket.on('receive-message', (message) => {
|
|
42336
42349
|
const callback = setupCallbacks.get(message.roomId);
|
|
42337
42350
|
if (callback) {
|
|
42338
|
-
console.log('[SOCKET] Received message from room:', message.roomId, {
|
|
42339
|
-
compression: message.data.compressionType,
|
|
42340
|
-
originalSize: message.data.originalSize,
|
|
42341
|
-
compressedSize: message.data.compressedSize,
|
|
42342
|
-
from: message.from
|
|
42343
|
-
});
|
|
42344
42351
|
callback(message.data);
|
|
42345
42352
|
}
|
|
42346
42353
|
});
|
|
@@ -42374,18 +42381,23 @@ const onSend = (roomId, data, webSocketUrl) => {
|
|
|
42374
42381
|
console.error('[SOCKET] Error sending message:', error);
|
|
42375
42382
|
}
|
|
42376
42383
|
};
|
|
42377
|
-
const onReceive = (roomId, callback, webSocketUrl) => {
|
|
42384
|
+
const onReceive = (roomId, callback, webSocketUrl, onRoomJoined) => {
|
|
42378
42385
|
const socketInstance = initializeSocket(webSocketUrl);
|
|
42379
42386
|
// Store the callback for this room
|
|
42380
42387
|
setupCallbacks.set(roomId, callback);
|
|
42381
|
-
// Only join the room if we haven't already
|
|
42382
42388
|
if (!joinedRooms.has(roomId)) {
|
|
42383
42389
|
socketInstance.emit('join-room', roomId);
|
|
42384
42390
|
joinedRooms.add(roomId);
|
|
42385
|
-
|
|
42391
|
+
if (onRoomJoined) {
|
|
42392
|
+
setTimeout(() => {
|
|
42393
|
+
onRoomJoined();
|
|
42394
|
+
}, 100);
|
|
42395
|
+
}
|
|
42386
42396
|
}
|
|
42387
42397
|
else {
|
|
42388
|
-
|
|
42398
|
+
if (onRoomJoined) {
|
|
42399
|
+
onRoomJoined();
|
|
42400
|
+
}
|
|
42389
42401
|
}
|
|
42390
42402
|
};
|
|
42391
42403
|
const leaveRoom = (roomId) => {
|
|
@@ -42402,20 +42414,66 @@ const disconnectSocket = () => {
|
|
|
42402
42414
|
socket = null;
|
|
42403
42415
|
joinedRooms.clear();
|
|
42404
42416
|
setupCallbacks.clear();
|
|
42405
|
-
console.log('[SOCKET] Socket disconnected and cleaned up');
|
|
42406
42417
|
}
|
|
42407
42418
|
};
|
|
42419
|
+
// Get current socket connection status
|
|
42420
|
+
const isSocketConnected = () => {
|
|
42421
|
+
return socket?.connected ?? false;
|
|
42422
|
+
};
|
|
42423
|
+
// Get current socket instance
|
|
42424
|
+
const getSocket = () => {
|
|
42425
|
+
return socket;
|
|
42426
|
+
};
|
|
42427
|
+
// Subscribe to connection status changes
|
|
42428
|
+
const onSocketStatusChange = (callback) => {
|
|
42429
|
+
const socketInstance = socket || initializeSocket();
|
|
42430
|
+
statusChangeCallbacks.add(callback);
|
|
42431
|
+
callback(socketInstance.connected);
|
|
42432
|
+
return () => {
|
|
42433
|
+
statusChangeCallbacks.delete(callback);
|
|
42434
|
+
};
|
|
42435
|
+
};
|
|
42436
|
+
const waitForSocket = (webSocketUrl, timeoutMs = 5000) => {
|
|
42437
|
+
return new Promise((resolve) => {
|
|
42438
|
+
const socketInstance = initializeSocket(webSocketUrl);
|
|
42439
|
+
if (socketInstance.connected) {
|
|
42440
|
+
resolve();
|
|
42441
|
+
return;
|
|
42442
|
+
}
|
|
42443
|
+
const handleConnect = () => {
|
|
42444
|
+
socketInstance.off('connect', handleConnect);
|
|
42445
|
+
clearTimeout(timeoutHandle);
|
|
42446
|
+
resolve();
|
|
42447
|
+
};
|
|
42448
|
+
socketInstance.on('connect', handleConnect);
|
|
42449
|
+
const timeoutHandle = setTimeout(() => {
|
|
42450
|
+
socketInstance.off('connect', handleConnect);
|
|
42451
|
+
resolve();
|
|
42452
|
+
}, timeoutMs);
|
|
42453
|
+
});
|
|
42454
|
+
};
|
|
42408
42455
|
|
|
42409
42456
|
const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transparentBackground = false, videoStream }) => {
|
|
42410
42457
|
const { state, dispatch, setQueueAction, requestStateFromPeers, webSocketUrl } = useWhiteboard();
|
|
42411
42458
|
const [lastCollaborativeAction, setLastCollaborativeAction] = React.useState(null);
|
|
42412
42459
|
const [hasRequestedState, setHasRequestedState] = React.useState(false);
|
|
42413
42460
|
const [lastStateRequestTime, setLastStateRequestTime] = React.useState(0);
|
|
42414
|
-
const [
|
|
42461
|
+
const [isSocketConnected, setIsSocketConnected] = React.useState(false);
|
|
42462
|
+
const [isRoomJoined, setIsRoomJoined] = React.useState(false);
|
|
42463
|
+
const [isGloballyUnlocked, setIsGloballyUnlocked] = React.useState(true); // Global unlock status
|
|
42415
42464
|
const [syncedAllowedUsers, setSyncedAllowedUsers] = React.useState(allowedUsers); // Synced allowed users from collaboration
|
|
42416
42465
|
const isCollaborativeUpdateRef = React.useRef(false); // Track if update came from collaboration
|
|
42417
42466
|
const lastClearTimeRef = React.useRef(0); // Track when last clear happened
|
|
42418
42467
|
const boardRef = React.useRef(null);
|
|
42468
|
+
React.useEffect(() => {
|
|
42469
|
+
const unsubscribe = onSocketStatusChange((connected) => {
|
|
42470
|
+
setIsSocketConnected(connected);
|
|
42471
|
+
});
|
|
42472
|
+
return () => {
|
|
42473
|
+
console.log('[WHITEBOARD] Unsubscribing from socket status listener');
|
|
42474
|
+
unsubscribe();
|
|
42475
|
+
};
|
|
42476
|
+
}, []); // Empty dependencies - single listener for component lifecycle
|
|
42419
42477
|
// Set userId in context when component mounts or userId changes
|
|
42420
42478
|
React.useEffect(() => {
|
|
42421
42479
|
dispatch({ type: 'SET_USER_ID', payload: userId });
|
|
@@ -42496,7 +42554,9 @@ const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transp
|
|
|
42496
42554
|
setHasRequestedState(false);
|
|
42497
42555
|
}
|
|
42498
42556
|
callback(data);
|
|
42499
|
-
}, webSocketUrl)
|
|
42557
|
+
}, webSocketUrl, () => {
|
|
42558
|
+
setIsRoomJoined(true);
|
|
42559
|
+
});
|
|
42500
42560
|
}, [roomId, webSocketUrl]);
|
|
42501
42561
|
// Initialize the collaborative whiteboard hook
|
|
42502
42562
|
const collaborativeConfig = React.useMemo(() => ({
|
|
@@ -42533,48 +42593,42 @@ const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transp
|
|
|
42533
42593
|
});
|
|
42534
42594
|
}
|
|
42535
42595
|
}, [allowedUsers, queueAction, userId, syncedAllowedUsers]);
|
|
42536
|
-
// Request state from peers when
|
|
42596
|
+
// Request state from peers when room is successfully joined AND socket is connected
|
|
42597
|
+
// Need both conditions to ensure message can actually be sent
|
|
42537
42598
|
React.useEffect(() => {
|
|
42538
|
-
if (
|
|
42539
|
-
|
|
42540
|
-
|
|
42541
|
-
|
|
42542
|
-
|
|
42543
|
-
|
|
42544
|
-
if (now - lastStateRequestTime > 5000) {
|
|
42545
|
-
requestStateFromPeers();
|
|
42546
|
-
setHasRequestedState(true);
|
|
42547
|
-
setLastStateRequestTime(now);
|
|
42548
|
-
}
|
|
42549
|
-
}, 500);
|
|
42550
|
-
return () => clearTimeout(requestTimer);
|
|
42599
|
+
if (!isRoomJoined || !isSocketConnected || hasRequestedState || state.shapes.length > 0 || !queueAction) {
|
|
42600
|
+
return;
|
|
42601
|
+
}
|
|
42602
|
+
const now = Date.now();
|
|
42603
|
+
if (now - lastClearTimeRef.current < 10000) {
|
|
42604
|
+
return;
|
|
42551
42605
|
}
|
|
42552
|
-
|
|
42606
|
+
requestStateFromPeers();
|
|
42607
|
+
setHasRequestedState(true);
|
|
42608
|
+
setLastStateRequestTime(Date.now());
|
|
42609
|
+
}, [isRoomJoined, isSocketConnected, state.shapes.length, hasRequestedState, queueAction, requestStateFromPeers, userId, roomId]);
|
|
42553
42610
|
// Reset request flag when room changes
|
|
42554
42611
|
React.useEffect(() => {
|
|
42555
42612
|
setHasRequestedState(false);
|
|
42556
42613
|
setLastStateRequestTime(0);
|
|
42614
|
+
setIsRoomJoined(false);
|
|
42557
42615
|
}, [roomId]);
|
|
42558
|
-
//
|
|
42616
|
+
// Retry state request if canvas is still empty after 3 seconds
|
|
42559
42617
|
React.useEffect(() => {
|
|
42560
|
-
if (queueAction
|
|
42561
|
-
|
|
42562
|
-
state.shapes.length === 0 &&
|
|
42563
|
-
!hasRequestedState) {
|
|
42564
|
-
const now = Date.now();
|
|
42565
|
-
if (now - lastClearTimeRef.current < 10000) {
|
|
42566
|
-
return;
|
|
42567
|
-
}
|
|
42568
|
-
if (now - lastStateRequestTime > 10000) {
|
|
42569
|
-
const reconnectTimer = setTimeout(() => {
|
|
42570
|
-
requestStateFromPeers();
|
|
42571
|
-
setHasRequestedState(true);
|
|
42572
|
-
setLastStateRequestTime(now);
|
|
42573
|
-
}, 2000);
|
|
42574
|
-
return () => clearTimeout(reconnectTimer);
|
|
42575
|
-
}
|
|
42618
|
+
if (!isRoomJoined || !isSocketConnected || hasRequestedState || state.shapes.length > 0 || !queueAction) {
|
|
42619
|
+
return;
|
|
42576
42620
|
}
|
|
42577
|
-
|
|
42621
|
+
const now = Date.now();
|
|
42622
|
+
// Don't retry if we recently cleared
|
|
42623
|
+
if (now - lastClearTimeRef.current < 10000) {
|
|
42624
|
+
return;
|
|
42625
|
+
}
|
|
42626
|
+
const retryTimer = setTimeout(() => {
|
|
42627
|
+
requestStateFromPeers();
|
|
42628
|
+
setLastStateRequestTime(Date.now());
|
|
42629
|
+
}, 3000);
|
|
42630
|
+
return () => clearTimeout(retryTimer);
|
|
42631
|
+
}, [isRoomJoined, isSocketConnected, state.shapes.length, queueAction, requestStateFromPeers, lastStateRequestTime, userId]);
|
|
42578
42632
|
// Cleanup stale active drawings periodically
|
|
42579
42633
|
React.useEffect(() => {
|
|
42580
42634
|
const cleanupInterval = setInterval(() => {
|
|
@@ -42755,6 +42809,28 @@ const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transp
|
|
|
42755
42809
|
leaveRoom(roomId);
|
|
42756
42810
|
};
|
|
42757
42811
|
}, [roomId]);
|
|
42812
|
+
// Clear all canvases on component unmount if admin leaves
|
|
42813
|
+
React.useEffect(() => {
|
|
42814
|
+
return () => {
|
|
42815
|
+
// If admin leaves, clear all users' canvases
|
|
42816
|
+
if (isAdmin && queueAction) {
|
|
42817
|
+
const clearTimestamp = Date.now();
|
|
42818
|
+
// Clear local state immediately
|
|
42819
|
+
dispatch({ type: 'CLEAR_CANVAS' });
|
|
42820
|
+
dispatch({ type: 'CLEAR_ACTIVE_DRAWINGS' });
|
|
42821
|
+
// Send clear action to all users
|
|
42822
|
+
queueAction({
|
|
42823
|
+
type: 'clear',
|
|
42824
|
+
payload: {
|
|
42825
|
+
timestamp: clearTimestamp,
|
|
42826
|
+
adminId: userId,
|
|
42827
|
+
},
|
|
42828
|
+
userId: userId,
|
|
42829
|
+
timestamp: clearTimestamp,
|
|
42830
|
+
});
|
|
42831
|
+
}
|
|
42832
|
+
};
|
|
42833
|
+
}, [isAdmin, userId, dispatch]);
|
|
42758
42834
|
// Global cleanup on app unmount
|
|
42759
42835
|
React.useEffect(() => {
|
|
42760
42836
|
const handleBeforeUnload = () => {
|
|
@@ -45800,5 +45876,13 @@ function cn(...inputs) {
|
|
|
45800
45876
|
exports.Whiteboard = Whiteboard;
|
|
45801
45877
|
exports.WhiteboardProvider = WhiteboardProvider;
|
|
45802
45878
|
exports.cn = cn;
|
|
45879
|
+
exports.disconnectSocket = disconnectSocket;
|
|
45880
|
+
exports.getSocket = getSocket;
|
|
45881
|
+
exports.isSocketConnected = isSocketConnected;
|
|
45882
|
+
exports.leaveRoom = leaveRoom;
|
|
45883
|
+
exports.onReceive = onReceive;
|
|
45884
|
+
exports.onSend = onSend;
|
|
45885
|
+
exports.onSocketStatusChange = onSocketStatusChange;
|
|
45803
45886
|
exports.useWhiteboardStream = useCapture;
|
|
45887
|
+
exports.waitForSocket = waitForSocket;
|
|
45804
45888
|
//# sourceMappingURL=index.js.map
|