@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.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 {
@@ -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
@@ -34757,12 +34757,15 @@ const whiteboardReducer = (state, action) => {
34757
34757
  // Prevent duplicate shapes during simultaneous drawing
34758
34758
  const newShape = action.payload;
34759
34759
  const existingShapeIndex = state.shapes.findIndex(shape => shape.id === newShape.id);
34760
+ // Ensure shape has timestamp for proper ordering and stale action filtering
34761
+ const timestamp = newShape.timestamp || Date.now();
34760
34762
  // If shape already exists, update it instead of adding duplicate
34761
34763
  if (existingShapeIndex >= 0) {
34762
34764
  const updatedShapes = [...state.shapes];
34763
34765
  updatedShapes[existingShapeIndex] = {
34764
34766
  ...newShape,
34765
- drawingSessionId: newShape.drawingSessionId || `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
34767
+ timestamp,
34768
+ drawingSessionId: newShape.drawingSessionId || `session-${timestamp}-${Math.random().toString(36).substr(2, 9)}`
34766
34769
  };
34767
34770
  return {
34768
34771
  ...state,
@@ -34770,10 +34773,11 @@ const whiteboardReducer = (state, action) => {
34770
34773
  currentDrawingShape: undefined,
34771
34774
  };
34772
34775
  }
34773
- // Add new shape with session tracking
34776
+ // Add new shape with session tracking and timestamp
34774
34777
  const shapeWithSession = {
34775
34778
  ...newShape,
34776
- drawingSessionId: newShape.drawingSessionId || `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
34779
+ timestamp,
34780
+ drawingSessionId: newShape.drawingSessionId || `session-${timestamp}-${Math.random().toString(36).substr(2, 9)}`
34777
34781
  };
34778
34782
  const newShapes = [...state.shapes, shapeWithSession];
34779
34783
  const newHistory = state.history.slice(0, state.historyIndex + 1);
@@ -34967,11 +34971,13 @@ const whiteboardReducer = (state, action) => {
34967
34971
  const userShapes = state.shapes
34968
34972
  .filter(shape => shape.userId === userId)
34969
34973
  .sort((a, b) => {
34970
- // Sort by id (which contains timestamp) or any timestamp property
34971
- return a.id.localeCompare(b.id);
34974
+ // Sort by timestamp to ensure LIFO (Last In First Out) order
34975
+ const timestampA = a.timestamp || 0;
34976
+ const timestampB = b.timestamp || 0;
34977
+ return timestampA - timestampB;
34972
34978
  });
34973
34979
  if (userShapes.length > 0) {
34974
- // Get the most recent shape
34980
+ // Get the most recent shape (last in sorted array)
34975
34981
  const lastUserShape = userShapes[userShapes.length - 1];
34976
34982
  // Check if there are multiple shapes that were part of the same drawing session
34977
34983
  // Use drawingSessionId if available, otherwise fall back to time-based detection
@@ -35249,12 +35255,16 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
35249
35255
  };
35250
35256
  const requestStateFromPeers = () => {
35251
35257
  if (currentQueueAction) {
35252
- currentQueueAction({
35253
- type: 'request_state',
35254
- payload: '',
35255
- requesterId: state.userId,
35256
- timestamp: Date.now(),
35257
- });
35258
+ setTimeout(() => {
35259
+ if (currentQueueAction) {
35260
+ currentQueueAction({
35261
+ type: 'request_state',
35262
+ payload: '',
35263
+ requesterId: state.userId,
35264
+ timestamp: Date.now(),
35265
+ });
35266
+ }
35267
+ }, 2000);
35258
35268
  }
35259
35269
  else {
35260
35270
  console.warn('[STATE_SYNC] No queue action available for state request');
@@ -35265,9 +35275,16 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
35265
35275
  // Drawing actions should be filtered if they're older than the last clear
35266
35276
  if (action.type === 'start_draw' || action.type === 'continue_draw' || action.type === 'end_draw' || action.type === 'add') {
35267
35277
  const actionTimestamp = action.timestamp || 0;
35268
- if (actionTimestamp <= state.lastClearTimestamp) {
35278
+ if (actionTimestamp > 0 && actionTimestamp <= state.lastClearTimestamp) {
35269
35279
  return true;
35270
35280
  }
35281
+ // Also check timestamp in the payload if it exists
35282
+ if (typeof action.payload === 'object' && action.payload !== null && !Array.isArray(action.payload)) {
35283
+ const payloadTimestamp = action.payload.timestamp || 0;
35284
+ if (payloadTimestamp > 0 && payloadTimestamp <= state.lastClearTimestamp) {
35285
+ return true;
35286
+ }
35287
+ }
35271
35288
  }
35272
35289
  return false;
35273
35290
  };
@@ -35297,10 +35314,15 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
35297
35314
  if (syncState.shapes.length === 0) {
35298
35315
  return; // Don't apply empty state or keep requesting
35299
35316
  }
35300
- // Only apply if the received state has more shapes or we have no shapes
35301
- if (state.shapes.length === 0 || syncState.shapes.length > state.shapes.length) {
35317
+ // Filter out shapes that are older than our last clear
35318
+ const validShapes = syncState.shapes.filter(shape => {
35319
+ const shapeTimestamp = shape.timestamp || 0;
35320
+ return shapeTimestamp === 0 || shapeTimestamp > state.lastClearTimestamp;
35321
+ });
35322
+ // Only apply if the received state has valid shapes
35323
+ if (validShapes.length > 0 && (state.shapes.length === 0 || validShapes.length > state.shapes.length)) {
35302
35324
  // All shapes from sync_state should have normalized coordinates, denormalize them
35303
- const denormalizedShapes = syncState.shapes.map((shape, index) => {
35325
+ const denormalizedShapes = validShapes.map((shape, index) => {
35304
35326
  return denormalizeShape(shape);
35305
35327
  });
35306
35328
  // Apply the synchronized state
@@ -35318,7 +35340,8 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
35318
35340
  const denormalizedShape = denormalizeShape(action.payload);
35319
35341
  // Additional check to prevent adding shapes from before the last clear
35320
35342
  const shapeTimestamp = denormalizedShape.timestamp || action.timestamp || 0;
35321
- if (shapeTimestamp <= state.lastClearTimestamp) {
35343
+ if (shapeTimestamp > 0 && shapeTimestamp <= state.lastClearTimestamp) {
35344
+ console.warn(`[APPLY_ACTION] Skipping stale shape from before clear - shape timestamp: ${shapeTimestamp}, clear timestamp: ${state.lastClearTimestamp}`);
35322
35345
  break;
35323
35346
  }
35324
35347
  dispatch({ type: 'ADD_SHAPE', payload: denormalizedShape });
@@ -35372,6 +35395,11 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
35372
35395
  case 'start_draw':
35373
35396
  if (typeof action.payload !== 'string' && !Array.isArray(action.payload) && isCompleteShape(action.payload)) {
35374
35397
  const denormalizedShape = denormalizeShape(action.payload);
35398
+ // Check if this shape is from before the last clear
35399
+ const shapeTimestamp = denormalizedShape.timestamp || action.timestamp || 0;
35400
+ if (shapeTimestamp > 0 && shapeTimestamp <= state.lastClearTimestamp) {
35401
+ break; // Skip stale shapes
35402
+ }
35375
35403
  // Only apply collaborative start_draw if it's from another user
35376
35404
  if (denormalizedShape.userId !== state.userId) {
35377
35405
  // Add to active drawings for real-time collaborative visibility
@@ -35385,6 +35413,11 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
35385
35413
  case 'continue_draw':
35386
35414
  if (typeof action.payload !== 'string' && !Array.isArray(action.payload) && isCompleteShape(action.payload)) {
35387
35415
  const denormalizedShape = denormalizeShape(action.payload);
35416
+ // Check if this shape is from before the last clear
35417
+ const shapeTimestamp = denormalizedShape.timestamp || action.timestamp || 0;
35418
+ if (shapeTimestamp > 0 && shapeTimestamp <= state.lastClearTimestamp) {
35419
+ break; // Skip stale shapes
35420
+ }
35388
35421
  // Only apply collaborative drawing updates if it's not from the current user
35389
35422
  // to avoid interfering with local real-time drawing
35390
35423
  if (denormalizedShape.userId !== state.userId) {
@@ -35399,6 +35432,12 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
35399
35432
  case 'end_draw':
35400
35433
  if (typeof action.payload !== 'string' && !Array.isArray(action.payload) && isCompleteShape(action.payload)) {
35401
35434
  const denormalizedShape = denormalizeShape(action.payload);
35435
+ // Check if this shape is from before the last clear
35436
+ const shapeTimestamp = denormalizedShape.timestamp || action.timestamp || 0;
35437
+ if (shapeTimestamp > 0 && shapeTimestamp <= state.lastClearTimestamp) {
35438
+ console.warn(`[APPLY_ACTION] Skipping stale end_draw from before clear - shape timestamp: ${shapeTimestamp}, clear timestamp: ${state.lastClearTimestamp}`);
35439
+ break; // Skip stale shapes
35440
+ }
35402
35441
  // Only apply collaborative end_draw if it's from another user
35403
35442
  // Local user's end_draw is handled directly in Board component via ADD_SHAPE
35404
35443
  if (denormalizedShape.userId !== state.userId) {
@@ -36426,7 +36465,8 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
36426
36465
  }
36427
36466
  // Create new shape ID for regular drawing tools
36428
36467
  const newShapeId = v4();
36429
- const newDrawingSessionId = `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
36468
+ const timestamp = Date.now();
36469
+ const newDrawingSessionId = `session-${timestamp}-${Math.random().toString(36).substr(2, 9)}`;
36430
36470
  setCurrentShapeId(newShapeId);
36431
36471
  setCurrentDrawingSessionId(newDrawingSessionId);
36432
36472
  // Start drawing
@@ -36443,6 +36483,7 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
36443
36483
  strokeStyle: state.strokeStyle,
36444
36484
  opacity: state.opacity,
36445
36485
  drawingSessionId: newDrawingSessionId,
36486
+ timestamp,
36446
36487
  // Initialize transformation properties
36447
36488
  x: 0,
36448
36489
  y: 0,
@@ -36555,6 +36596,7 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
36555
36596
  strokeStyle: state.strokeStyle,
36556
36597
  opacity: state.opacity,
36557
36598
  drawingSessionId: currentDrawingSessionId,
36599
+ timestamp: Date.now(),
36558
36600
  // Initialize transformation properties
36559
36601
  x: 0,
36560
36602
  y: 0,
@@ -36634,6 +36676,7 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
36634
36676
  strokeStyle: state.strokeStyle,
36635
36677
  opacity: state.opacity,
36636
36678
  drawingSessionId: currentDrawingSessionId,
36679
+ timestamp: Date.now(),
36637
36680
  // Initialize transformation properties
36638
36681
  x: 0,
36639
36682
  y: 0,
@@ -37030,32 +37073,6 @@ const Eraser = createLucideIcon("Eraser", [
37030
37073
  */
37031
37074
 
37032
37075
 
37033
- const LockOpen = createLucideIcon("LockOpen", [
37034
- ["rect", { width: "18", height: "11", x: "3", y: "11", rx: "2", ry: "2", key: "1w4ew1" }],
37035
- ["path", { d: "M7 11V7a5 5 0 0 1 9.9-1", key: "1mm8w8" }]
37036
- ]);
37037
-
37038
- /**
37039
- * @license lucide-react v0.460.0 - ISC
37040
- *
37041
- * This source code is licensed under the ISC license.
37042
- * See the LICENSE file in the root directory of this source tree.
37043
- */
37044
-
37045
-
37046
- const Lock = createLucideIcon("Lock", [
37047
- ["rect", { width: "18", height: "11", x: "3", y: "11", rx: "2", ry: "2", key: "1w4ew1" }],
37048
- ["path", { d: "M7 11V7a5 5 0 0 1 10 0v4", key: "fwvmzm" }]
37049
- ]);
37050
-
37051
- /**
37052
- * @license lucide-react v0.460.0 - ISC
37053
- *
37054
- * This source code is licensed under the ISC license.
37055
- * See the LICENSE file in the root directory of this source tree.
37056
- */
37057
-
37058
-
37059
37076
  const Minus = createLucideIcon("Minus", [["path", { d: "M5 12h14", key: "1ays0h" }]]);
37060
37077
 
37061
37078
  /**
@@ -37136,7 +37153,7 @@ const Undo2 = createLucideIcon("Undo2", [
37136
37153
  ]);
37137
37154
 
37138
37155
  // Top Toolbar Component
37139
- const TopToolbar = ({ queueAction, handleExportImage, handleClear, handleLockToggle, isAdmin = false, hasToolAccess = false, isGloballyUnlocked = false, shouldBeOpenByDefault = true, hasVideoBackground = false }) => {
37156
+ const TopToolbar = ({ queueAction, handleExportImage, handleClear, handleLockToggle, isAdmin = false, hasToolAccess = false, isGloballyUnlocked = true, shouldBeOpenByDefault = true, hasVideoBackground = false }) => {
37140
37157
  const { state, dispatch } = useWhiteboard();
37141
37158
  const [isVisible, setIsVisible] = useState(shouldBeOpenByDefault);
37142
37159
  const [isInitialized, setIsInitialized] = useState(false);
@@ -37212,9 +37229,7 @@ const TopToolbar = ({ queueAction, handleExportImage, handleClear, handleLockTog
37212
37229
  if (!isInitialized) {
37213
37230
  return null;
37214
37231
  }
37215
- 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: [isAdmin && (jsx("button", { className: `w-10 h-10 flex items-center justify-center rounded transition-colors ${isGloballyUnlocked
37216
- ? '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'
37217
- : '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
37232
+ 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
37218
37233
  ? 'opacity-50 cursor-not-allowed text-gray-400 dark:text-gray-600'
37219
37234
  : state.tool === tool.type
37220
37235
  ? 'bg-purple-100 dark:bg-purple-900/50 text-purple-600 dark:text-purple-300'
@@ -42272,6 +42287,7 @@ Object.assign(lookup, {
42272
42287
  let socket = null;
42273
42288
  const joinedRooms = new Set();
42274
42289
  const setupCallbacks = new Map();
42290
+ const statusChangeCallbacks = new Set();
42275
42291
  let currentWebSocketUrl = undefined;
42276
42292
  // Initialize socket connection
42277
42293
  const initializeSocket = (webSocketUrl) => {
@@ -42287,7 +42303,6 @@ const initializeSocket = (webSocketUrl) => {
42287
42303
  setupCallbacks.clear();
42288
42304
  }
42289
42305
  currentWebSocketUrl = webSocketUrl;
42290
- console.log('[SOCKET] Using socket server URL:', socketServerUrl);
42291
42306
  if (!socketServerUrl) {
42292
42307
  console.error('[SOCKET] No socket server URL provided');
42293
42308
  }
@@ -42298,29 +42313,21 @@ const initializeSocket = (webSocketUrl) => {
42298
42313
  reconnectionDelay: 1000,
42299
42314
  });
42300
42315
  socket.on('connect', () => {
42301
- console.log('[SOCKET] Connected to server');
42302
- // Re-join all rooms after reconnection
42316
+ statusChangeCallbacks.forEach(callback => callback(true));
42303
42317
  joinedRooms.forEach(roomId => {
42304
42318
  socket.emit('join-room', roomId);
42305
- console.log('[SOCKET] Re-joined room:', roomId);
42306
42319
  });
42307
42320
  });
42308
42321
  socket.on('disconnect', () => {
42309
- console.log('[SOCKET] Disconnected from server');
42322
+ statusChangeCallbacks.forEach(callback => callback(false));
42310
42323
  });
42311
- socket.on('connect_error', (error) => {
42312
- console.error('[SOCKET] Connection error:', error);
42324
+ socket.on('connect_error', () => {
42325
+ // Connection error handled by Socket.IO reconnection logic
42313
42326
  });
42314
42327
  // Set up the global receive-message listener once
42315
42328
  socket.on('receive-message', (message) => {
42316
42329
  const callback = setupCallbacks.get(message.roomId);
42317
42330
  if (callback) {
42318
- console.log('[SOCKET] Received message from room:', message.roomId, {
42319
- compression: message.data.compressionType,
42320
- originalSize: message.data.originalSize,
42321
- compressedSize: message.data.compressedSize,
42322
- from: message.from
42323
- });
42324
42331
  callback(message.data);
42325
42332
  }
42326
42333
  });
@@ -42354,18 +42361,23 @@ const onSend = (roomId, data, webSocketUrl) => {
42354
42361
  console.error('[SOCKET] Error sending message:', error);
42355
42362
  }
42356
42363
  };
42357
- const onReceive = (roomId, callback, webSocketUrl) => {
42364
+ const onReceive = (roomId, callback, webSocketUrl, onRoomJoined) => {
42358
42365
  const socketInstance = initializeSocket(webSocketUrl);
42359
42366
  // Store the callback for this room
42360
42367
  setupCallbacks.set(roomId, callback);
42361
- // Only join the room if we haven't already
42362
42368
  if (!joinedRooms.has(roomId)) {
42363
42369
  socketInstance.emit('join-room', roomId);
42364
42370
  joinedRooms.add(roomId);
42365
- console.log('[SOCKET] Joined room:', roomId);
42371
+ if (onRoomJoined) {
42372
+ setTimeout(() => {
42373
+ onRoomJoined();
42374
+ }, 100);
42375
+ }
42366
42376
  }
42367
42377
  else {
42368
- console.log('[SOCKET] Already in room:', roomId);
42378
+ if (onRoomJoined) {
42379
+ onRoomJoined();
42380
+ }
42369
42381
  }
42370
42382
  };
42371
42383
  const leaveRoom = (roomId) => {
@@ -42382,20 +42394,66 @@ const disconnectSocket = () => {
42382
42394
  socket = null;
42383
42395
  joinedRooms.clear();
42384
42396
  setupCallbacks.clear();
42385
- console.log('[SOCKET] Socket disconnected and cleaned up');
42386
42397
  }
42387
42398
  };
42399
+ // Get current socket connection status
42400
+ const isSocketConnected = () => {
42401
+ return socket?.connected ?? false;
42402
+ };
42403
+ // Get current socket instance
42404
+ const getSocket = () => {
42405
+ return socket;
42406
+ };
42407
+ // Subscribe to connection status changes
42408
+ const onSocketStatusChange = (callback) => {
42409
+ const socketInstance = socket || initializeSocket();
42410
+ statusChangeCallbacks.add(callback);
42411
+ callback(socketInstance.connected);
42412
+ return () => {
42413
+ statusChangeCallbacks.delete(callback);
42414
+ };
42415
+ };
42416
+ const waitForSocket = (webSocketUrl, timeoutMs = 5000) => {
42417
+ return new Promise((resolve) => {
42418
+ const socketInstance = initializeSocket(webSocketUrl);
42419
+ if (socketInstance.connected) {
42420
+ resolve();
42421
+ return;
42422
+ }
42423
+ const handleConnect = () => {
42424
+ socketInstance.off('connect', handleConnect);
42425
+ clearTimeout(timeoutHandle);
42426
+ resolve();
42427
+ };
42428
+ socketInstance.on('connect', handleConnect);
42429
+ const timeoutHandle = setTimeout(() => {
42430
+ socketInstance.off('connect', handleConnect);
42431
+ resolve();
42432
+ }, timeoutMs);
42433
+ });
42434
+ };
42388
42435
 
42389
42436
  const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transparentBackground = false, videoStream }) => {
42390
42437
  const { state, dispatch, setQueueAction, requestStateFromPeers, webSocketUrl } = useWhiteboard();
42391
42438
  const [lastCollaborativeAction, setLastCollaborativeAction] = useState(null);
42392
42439
  const [hasRequestedState, setHasRequestedState] = useState(false);
42393
42440
  const [lastStateRequestTime, setLastStateRequestTime] = useState(0);
42394
- const [isGloballyUnlocked, setIsGloballyUnlocked] = useState(false); // Global unlock status
42441
+ const [isSocketConnected, setIsSocketConnected] = useState(false);
42442
+ const [isRoomJoined, setIsRoomJoined] = useState(false);
42443
+ const [isGloballyUnlocked, setIsGloballyUnlocked] = useState(true); // Global unlock status
42395
42444
  const [syncedAllowedUsers, setSyncedAllowedUsers] = useState(allowedUsers); // Synced allowed users from collaboration
42396
42445
  const isCollaborativeUpdateRef = useRef(false); // Track if update came from collaboration
42397
42446
  const lastClearTimeRef = useRef(0); // Track when last clear happened
42398
42447
  const boardRef = useRef(null);
42448
+ useEffect(() => {
42449
+ const unsubscribe = onSocketStatusChange((connected) => {
42450
+ setIsSocketConnected(connected);
42451
+ });
42452
+ return () => {
42453
+ console.log('[WHITEBOARD] Unsubscribing from socket status listener');
42454
+ unsubscribe();
42455
+ };
42456
+ }, []); // Empty dependencies - single listener for component lifecycle
42399
42457
  // Set userId in context when component mounts or userId changes
42400
42458
  useEffect(() => {
42401
42459
  dispatch({ type: 'SET_USER_ID', payload: userId });
@@ -42476,7 +42534,9 @@ const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transp
42476
42534
  setHasRequestedState(false);
42477
42535
  }
42478
42536
  callback(data);
42479
- }, webSocketUrl);
42537
+ }, webSocketUrl, () => {
42538
+ setIsRoomJoined(true);
42539
+ });
42480
42540
  }, [roomId, webSocketUrl]);
42481
42541
  // Initialize the collaborative whiteboard hook
42482
42542
  const collaborativeConfig = useMemo(() => ({
@@ -42513,48 +42573,42 @@ const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transp
42513
42573
  });
42514
42574
  }
42515
42575
  }, [allowedUsers, queueAction, userId, syncedAllowedUsers]);
42516
- // Request state from peers when joining/rejoining the room
42576
+ // Request state from peers when room is successfully joined AND socket is connected
42577
+ // Need both conditions to ensure message can actually be sent
42517
42578
  useEffect(() => {
42518
- if (queueAction && typeof queueAction === 'function' && !hasRequestedState) {
42519
- const requestTimer = setTimeout(() => {
42520
- const now = Date.now();
42521
- if (now - lastClearTimeRef.current < 10000) {
42522
- return;
42523
- }
42524
- if (now - lastStateRequestTime > 5000) {
42525
- requestStateFromPeers();
42526
- setHasRequestedState(true);
42527
- setLastStateRequestTime(now);
42528
- }
42529
- }, 500);
42530
- return () => clearTimeout(requestTimer);
42579
+ if (!isRoomJoined || !isSocketConnected || hasRequestedState || state.shapes.length > 0 || !queueAction) {
42580
+ return;
42581
+ }
42582
+ const now = Date.now();
42583
+ if (now - lastClearTimeRef.current < 10000) {
42584
+ return;
42531
42585
  }
42532
- }, [roomId, queueAction, requestStateFromPeers, hasRequestedState, lastStateRequestTime]);
42586
+ requestStateFromPeers();
42587
+ setHasRequestedState(true);
42588
+ setLastStateRequestTime(Date.now());
42589
+ }, [isRoomJoined, isSocketConnected, state.shapes.length, hasRequestedState, queueAction, requestStateFromPeers, userId, roomId]);
42533
42590
  // Reset request flag when room changes
42534
42591
  useEffect(() => {
42535
42592
  setHasRequestedState(false);
42536
42593
  setLastStateRequestTime(0);
42594
+ setIsRoomJoined(false);
42537
42595
  }, [roomId]);
42538
- // Monitor for empty canvas and request state only if we haven't tried recently
42596
+ // Retry state request if canvas is still empty after 3 seconds
42539
42597
  useEffect(() => {
42540
- if (queueAction &&
42541
- typeof queueAction === 'function' &&
42542
- state.shapes.length === 0 &&
42543
- !hasRequestedState) {
42544
- const now = Date.now();
42545
- if (now - lastClearTimeRef.current < 10000) {
42546
- return;
42547
- }
42548
- if (now - lastStateRequestTime > 10000) {
42549
- const reconnectTimer = setTimeout(() => {
42550
- requestStateFromPeers();
42551
- setHasRequestedState(true);
42552
- setLastStateRequestTime(now);
42553
- }, 2000);
42554
- return () => clearTimeout(reconnectTimer);
42555
- }
42598
+ if (!isRoomJoined || !isSocketConnected || hasRequestedState || state.shapes.length > 0 || !queueAction) {
42599
+ return;
42556
42600
  }
42557
- }, [queueAction, state.shapes.length, requestStateFromPeers, hasRequestedState, lastStateRequestTime]);
42601
+ const now = Date.now();
42602
+ // Don't retry if we recently cleared
42603
+ if (now - lastClearTimeRef.current < 10000) {
42604
+ return;
42605
+ }
42606
+ const retryTimer = setTimeout(() => {
42607
+ requestStateFromPeers();
42608
+ setLastStateRequestTime(Date.now());
42609
+ }, 3000);
42610
+ return () => clearTimeout(retryTimer);
42611
+ }, [isRoomJoined, isSocketConnected, state.shapes.length, queueAction, requestStateFromPeers, lastStateRequestTime, userId]);
42558
42612
  // Cleanup stale active drawings periodically
42559
42613
  useEffect(() => {
42560
42614
  const cleanupInterval = setInterval(() => {
@@ -42735,6 +42789,28 @@ const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transp
42735
42789
  leaveRoom(roomId);
42736
42790
  };
42737
42791
  }, [roomId]);
42792
+ // Clear all canvases on component unmount if admin leaves
42793
+ useEffect(() => {
42794
+ return () => {
42795
+ // If admin leaves, clear all users' canvases
42796
+ if (isAdmin && queueAction) {
42797
+ const clearTimestamp = Date.now();
42798
+ // Clear local state immediately
42799
+ dispatch({ type: 'CLEAR_CANVAS' });
42800
+ dispatch({ type: 'CLEAR_ACTIVE_DRAWINGS' });
42801
+ // Send clear action to all users
42802
+ queueAction({
42803
+ type: 'clear',
42804
+ payload: {
42805
+ timestamp: clearTimestamp,
42806
+ adminId: userId,
42807
+ },
42808
+ userId: userId,
42809
+ timestamp: clearTimestamp,
42810
+ });
42811
+ }
42812
+ };
42813
+ }, [isAdmin, userId, dispatch]);
42738
42814
  // Global cleanup on app unmount
42739
42815
  useEffect(() => {
42740
42816
  const handleBeforeUnload = () => {
@@ -45777,5 +45853,5 @@ function cn(...inputs) {
45777
45853
  return twMerge(clsx(inputs));
45778
45854
  }
45779
45855
 
45780
- export { Whiteboard, WhiteboardProvider, cn, useCapture as useWhiteboardStream };
45856
+ export { Whiteboard, WhiteboardProvider, cn, disconnectSocket, getSocket, isSocketConnected, leaveRoom, onReceive, onSend, onSocketStatusChange, useCapture as useWhiteboardStream, waitForSocket };
45781
45857
  //# sourceMappingURL=index.esm.js.map