@ngenux/ngage-whiteboarding 1.0.7 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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
@@ -35265,9 +35271,16 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
35265
35271
  // Drawing actions should be filtered if they're older than the last clear
35266
35272
  if (action.type === 'start_draw' || action.type === 'continue_draw' || action.type === 'end_draw' || action.type === 'add') {
35267
35273
  const actionTimestamp = action.timestamp || 0;
35268
- if (actionTimestamp <= state.lastClearTimestamp) {
35274
+ if (actionTimestamp > 0 && actionTimestamp <= state.lastClearTimestamp) {
35269
35275
  return true;
35270
35276
  }
35277
+ // Also check timestamp in the payload if it exists
35278
+ if (typeof action.payload === 'object' && action.payload !== null && !Array.isArray(action.payload)) {
35279
+ const payloadTimestamp = action.payload.timestamp || 0;
35280
+ if (payloadTimestamp > 0 && payloadTimestamp <= state.lastClearTimestamp) {
35281
+ return true;
35282
+ }
35283
+ }
35271
35284
  }
35272
35285
  return false;
35273
35286
  };
@@ -35297,10 +35310,15 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
35297
35310
  if (syncState.shapes.length === 0) {
35298
35311
  return; // Don't apply empty state or keep requesting
35299
35312
  }
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) {
35313
+ // Filter out shapes that are older than our last clear
35314
+ const validShapes = syncState.shapes.filter(shape => {
35315
+ const shapeTimestamp = shape.timestamp || 0;
35316
+ return shapeTimestamp === 0 || shapeTimestamp > state.lastClearTimestamp;
35317
+ });
35318
+ // Only apply if the received state has valid shapes
35319
+ if (validShapes.length > 0 && (state.shapes.length === 0 || validShapes.length > state.shapes.length)) {
35302
35320
  // All shapes from sync_state should have normalized coordinates, denormalize them
35303
- const denormalizedShapes = syncState.shapes.map((shape, index) => {
35321
+ const denormalizedShapes = validShapes.map((shape, index) => {
35304
35322
  return denormalizeShape(shape);
35305
35323
  });
35306
35324
  // Apply the synchronized state
@@ -35318,7 +35336,8 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
35318
35336
  const denormalizedShape = denormalizeShape(action.payload);
35319
35337
  // Additional check to prevent adding shapes from before the last clear
35320
35338
  const shapeTimestamp = denormalizedShape.timestamp || action.timestamp || 0;
35321
- if (shapeTimestamp <= state.lastClearTimestamp) {
35339
+ if (shapeTimestamp > 0 && shapeTimestamp <= state.lastClearTimestamp) {
35340
+ console.warn(`[APPLY_ACTION] Skipping stale shape from before clear - shape timestamp: ${shapeTimestamp}, clear timestamp: ${state.lastClearTimestamp}`);
35322
35341
  break;
35323
35342
  }
35324
35343
  dispatch({ type: 'ADD_SHAPE', payload: denormalizedShape });
@@ -35372,6 +35391,11 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
35372
35391
  case 'start_draw':
35373
35392
  if (typeof action.payload !== 'string' && !Array.isArray(action.payload) && isCompleteShape(action.payload)) {
35374
35393
  const denormalizedShape = denormalizeShape(action.payload);
35394
+ // Check if this shape is from before the last clear
35395
+ const shapeTimestamp = denormalizedShape.timestamp || action.timestamp || 0;
35396
+ if (shapeTimestamp > 0 && shapeTimestamp <= state.lastClearTimestamp) {
35397
+ break; // Skip stale shapes
35398
+ }
35375
35399
  // Only apply collaborative start_draw if it's from another user
35376
35400
  if (denormalizedShape.userId !== state.userId) {
35377
35401
  // Add to active drawings for real-time collaborative visibility
@@ -35385,6 +35409,11 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
35385
35409
  case 'continue_draw':
35386
35410
  if (typeof action.payload !== 'string' && !Array.isArray(action.payload) && isCompleteShape(action.payload)) {
35387
35411
  const denormalizedShape = denormalizeShape(action.payload);
35412
+ // Check if this shape is from before the last clear
35413
+ const shapeTimestamp = denormalizedShape.timestamp || action.timestamp || 0;
35414
+ if (shapeTimestamp > 0 && shapeTimestamp <= state.lastClearTimestamp) {
35415
+ break; // Skip stale shapes
35416
+ }
35388
35417
  // Only apply collaborative drawing updates if it's not from the current user
35389
35418
  // to avoid interfering with local real-time drawing
35390
35419
  if (denormalizedShape.userId !== state.userId) {
@@ -35399,6 +35428,12 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
35399
35428
  case 'end_draw':
35400
35429
  if (typeof action.payload !== 'string' && !Array.isArray(action.payload) && isCompleteShape(action.payload)) {
35401
35430
  const denormalizedShape = denormalizeShape(action.payload);
35431
+ // Check if this shape is from before the last clear
35432
+ const shapeTimestamp = denormalizedShape.timestamp || action.timestamp || 0;
35433
+ if (shapeTimestamp > 0 && shapeTimestamp <= state.lastClearTimestamp) {
35434
+ console.warn(`[APPLY_ACTION] Skipping stale end_draw from before clear - shape timestamp: ${shapeTimestamp}, clear timestamp: ${state.lastClearTimestamp}`);
35435
+ break; // Skip stale shapes
35436
+ }
35402
35437
  // Only apply collaborative end_draw if it's from another user
35403
35438
  // Local user's end_draw is handled directly in Board component via ADD_SHAPE
35404
35439
  if (denormalizedShape.userId !== state.userId) {
@@ -36426,7 +36461,8 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
36426
36461
  }
36427
36462
  // Create new shape ID for regular drawing tools
36428
36463
  const newShapeId = v4();
36429
- const newDrawingSessionId = `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
36464
+ const timestamp = Date.now();
36465
+ const newDrawingSessionId = `session-${timestamp}-${Math.random().toString(36).substr(2, 9)}`;
36430
36466
  setCurrentShapeId(newShapeId);
36431
36467
  setCurrentDrawingSessionId(newDrawingSessionId);
36432
36468
  // Start drawing
@@ -36443,6 +36479,7 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
36443
36479
  strokeStyle: state.strokeStyle,
36444
36480
  opacity: state.opacity,
36445
36481
  drawingSessionId: newDrawingSessionId,
36482
+ timestamp,
36446
36483
  // Initialize transformation properties
36447
36484
  x: 0,
36448
36485
  y: 0,
@@ -36555,6 +36592,7 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
36555
36592
  strokeStyle: state.strokeStyle,
36556
36593
  opacity: state.opacity,
36557
36594
  drawingSessionId: currentDrawingSessionId,
36595
+ timestamp: Date.now(),
36558
36596
  // Initialize transformation properties
36559
36597
  x: 0,
36560
36598
  y: 0,
@@ -36634,6 +36672,7 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
36634
36672
  strokeStyle: state.strokeStyle,
36635
36673
  opacity: state.opacity,
36636
36674
  drawingSessionId: currentDrawingSessionId,
36675
+ timestamp: Date.now(),
36637
36676
  // Initialize transformation properties
36638
36677
  x: 0,
36639
36678
  y: 0,
@@ -42735,6 +42774,28 @@ const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transp
42735
42774
  leaveRoom(roomId);
42736
42775
  };
42737
42776
  }, [roomId]);
42777
+ // Clear all canvases on component unmount if admin leaves
42778
+ useEffect(() => {
42779
+ return () => {
42780
+ // If admin leaves, clear all users' canvases
42781
+ if (isAdmin && queueAction) {
42782
+ const clearTimestamp = Date.now();
42783
+ // Clear local state immediately
42784
+ dispatch({ type: 'CLEAR_CANVAS' });
42785
+ dispatch({ type: 'CLEAR_ACTIVE_DRAWINGS' });
42786
+ // Send clear action to all users
42787
+ queueAction({
42788
+ type: 'clear',
42789
+ payload: {
42790
+ timestamp: clearTimestamp,
42791
+ adminId: userId,
42792
+ },
42793
+ userId: userId,
42794
+ timestamp: clearTimestamp,
42795
+ });
42796
+ }
42797
+ };
42798
+ }, [isAdmin, queueAction, userId, dispatch]);
42738
42799
  // Global cleanup on app unmount
42739
42800
  useEffect(() => {
42740
42801
  const handleBeforeUnload = () => {