@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.
Files changed (53) hide show
  1. package/README.md +344 -344
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.esm.js +178 -102
  5. package/dist/index.esm.js.map +1 -1
  6. package/dist/index.js +185 -101
  7. package/dist/index.js.map +1 -1
  8. package/dist/src/components/Whiteboard/Board.d.ts.map +1 -1
  9. package/dist/src/components/Whiteboard/index.d.ts.map +1 -1
  10. package/dist/src/context/WhiteboardContext.d.ts.map +1 -1
  11. package/dist/src/utils/socket-utility.d.ts +6 -1
  12. package/dist/src/utils/socket-utility.d.ts.map +1 -1
  13. package/dist/utils/index.d.ts +13 -0
  14. package/dist/utils/index.d.ts.map +1 -0
  15. package/dist/utils/socket-utility.esm.js +168 -0
  16. package/dist/utils/socket-utility.esm.js.map +1 -0
  17. package/dist/utils/socket-utility.js +177 -0
  18. package/dist/utils/socket-utility.js.map +1 -0
  19. package/dist/utils/src/components/Shapes/Arrow.d.ts +11 -0
  20. package/dist/utils/src/components/Shapes/Arrow.d.ts.map +1 -0
  21. package/dist/utils/src/components/Shapes/Ellipse.d.ts +11 -0
  22. package/dist/utils/src/components/Shapes/Ellipse.d.ts.map +1 -0
  23. package/dist/utils/src/components/Shapes/ErasedShape.d.ts +11 -0
  24. package/dist/utils/src/components/Shapes/ErasedShape.d.ts.map +1 -0
  25. package/dist/utils/src/components/Shapes/FreehandDrawing.d.ts +11 -0
  26. package/dist/utils/src/components/Shapes/FreehandDrawing.d.ts.map +1 -0
  27. package/dist/utils/src/components/Shapes/Line.d.ts +11 -0
  28. package/dist/utils/src/components/Shapes/Line.d.ts.map +1 -0
  29. package/dist/utils/src/components/Shapes/Rectangle.d.ts +11 -0
  30. package/dist/utils/src/components/Shapes/Rectangle.d.ts.map +1 -0
  31. package/dist/utils/src/components/Whiteboard/Board.d.ts +15 -0
  32. package/dist/utils/src/components/Whiteboard/Board.d.ts.map +1 -0
  33. package/dist/utils/src/components/Whiteboard/Toolbar.d.ts +21 -0
  34. package/dist/utils/src/components/Whiteboard/Toolbar.d.ts.map +1 -0
  35. package/dist/utils/src/components/Whiteboard/index.d.ts +11 -0
  36. package/dist/utils/src/components/Whiteboard/index.d.ts.map +1 -0
  37. package/dist/utils/src/context/WhiteboardContext.d.ts +128 -0
  38. package/dist/utils/src/context/WhiteboardContext.d.ts.map +1 -0
  39. package/dist/utils/src/hooks/useCapture.d.ts +4 -0
  40. package/dist/utils/src/hooks/useCapture.d.ts.map +1 -0
  41. package/dist/utils/src/hooks/useCollaborativeWhiteboard.d.ts +27 -0
  42. package/dist/utils/src/hooks/useCollaborativeWhiteboard.d.ts.map +1 -0
  43. package/dist/utils/src/lib/utils.d.ts +3 -0
  44. package/dist/utils/src/lib/utils.d.ts.map +1 -0
  45. package/dist/utils/src/types/index.d.ts +123 -0
  46. package/dist/utils/src/types/index.d.ts.map +1 -0
  47. package/dist/utils/src/utils/compression.d.ts +14 -0
  48. package/dist/utils/src/utils/compression.d.ts.map +1 -0
  49. package/dist/utils/src/utils/socket-utility.d.ts +11 -0
  50. package/dist/utils/src/utils/socket-utility.d.ts.map +1 -0
  51. package/package.json +1 -1
  52. package/dist/src/utils/video-coordinates.d.ts +0 -36
  53. 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
- drawingSessionId: newShape.drawingSessionId || `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
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
- drawingSessionId: newShape.drawingSessionId || `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
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 id (which contains timestamp) or any timestamp property
34991
- return a.id.localeCompare(b.id);
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
- currentQueueAction({
35273
- type: 'request_state',
35274
- payload: '',
35275
- requesterId: state.userId,
35276
- timestamp: Date.now(),
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
- // Only apply if the received state has more shapes or we have no shapes
35321
- if (state.shapes.length === 0 || syncState.shapes.length > state.shapes.length) {
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 = syncState.shapes.map((shape, index) => {
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 newDrawingSessionId = `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
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 = false, shouldBeOpenByDefault = true, hasVideoBackground = false }) => {
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: [isAdmin && (jsxRuntime.jsx("button", { className: `w-10 h-10 flex items-center justify-center rounded transition-colors ${isGloballyUnlocked
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
- console.log('[SOCKET] Connected to server');
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
- console.log('[SOCKET] Disconnected from server');
42342
+ statusChangeCallbacks.forEach(callback => callback(false));
42330
42343
  });
42331
- socket.on('connect_error', (error) => {
42332
- console.error('[SOCKET] Connection error:', error);
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
- console.log('[SOCKET] Joined room:', roomId);
42391
+ if (onRoomJoined) {
42392
+ setTimeout(() => {
42393
+ onRoomJoined();
42394
+ }, 100);
42395
+ }
42386
42396
  }
42387
42397
  else {
42388
- console.log('[SOCKET] Already in room:', roomId);
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 [isGloballyUnlocked, setIsGloballyUnlocked] = React.useState(false); // Global unlock status
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 joining/rejoining the room
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 (queueAction && typeof queueAction === 'function' && !hasRequestedState) {
42539
- const requestTimer = setTimeout(() => {
42540
- const now = Date.now();
42541
- if (now - lastClearTimeRef.current < 10000) {
42542
- return;
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
- }, [roomId, queueAction, requestStateFromPeers, hasRequestedState, lastStateRequestTime]);
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
- // Monitor for empty canvas and request state only if we haven't tried recently
42616
+ // Retry state request if canvas is still empty after 3 seconds
42559
42617
  React.useEffect(() => {
42560
- if (queueAction &&
42561
- typeof queueAction === 'function' &&
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
- }, [queueAction, state.shapes.length, requestStateFromPeers, hasRequestedState, lastStateRequestTime]);
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