@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.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
@@ -35285,9 +35291,16 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
35285
35291
  // Drawing actions should be filtered if they're older than the last clear
35286
35292
  if (action.type === 'start_draw' || action.type === 'continue_draw' || action.type === 'end_draw' || action.type === 'add') {
35287
35293
  const actionTimestamp = action.timestamp || 0;
35288
- if (actionTimestamp <= state.lastClearTimestamp) {
35294
+ if (actionTimestamp > 0 && actionTimestamp <= state.lastClearTimestamp) {
35289
35295
  return true;
35290
35296
  }
35297
+ // Also check timestamp in the payload if it exists
35298
+ if (typeof action.payload === 'object' && action.payload !== null && !Array.isArray(action.payload)) {
35299
+ const payloadTimestamp = action.payload.timestamp || 0;
35300
+ if (payloadTimestamp > 0 && payloadTimestamp <= state.lastClearTimestamp) {
35301
+ return true;
35302
+ }
35303
+ }
35291
35304
  }
35292
35305
  return false;
35293
35306
  };
@@ -35317,10 +35330,15 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
35317
35330
  if (syncState.shapes.length === 0) {
35318
35331
  return; // Don't apply empty state or keep requesting
35319
35332
  }
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) {
35333
+ // Filter out shapes that are older than our last clear
35334
+ const validShapes = syncState.shapes.filter(shape => {
35335
+ const shapeTimestamp = shape.timestamp || 0;
35336
+ return shapeTimestamp === 0 || shapeTimestamp > state.lastClearTimestamp;
35337
+ });
35338
+ // Only apply if the received state has valid shapes
35339
+ if (validShapes.length > 0 && (state.shapes.length === 0 || validShapes.length > state.shapes.length)) {
35322
35340
  // All shapes from sync_state should have normalized coordinates, denormalize them
35323
- const denormalizedShapes = syncState.shapes.map((shape, index) => {
35341
+ const denormalizedShapes = validShapes.map((shape, index) => {
35324
35342
  return denormalizeShape(shape);
35325
35343
  });
35326
35344
  // Apply the synchronized state
@@ -35338,7 +35356,8 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
35338
35356
  const denormalizedShape = denormalizeShape(action.payload);
35339
35357
  // Additional check to prevent adding shapes from before the last clear
35340
35358
  const shapeTimestamp = denormalizedShape.timestamp || action.timestamp || 0;
35341
- if (shapeTimestamp <= state.lastClearTimestamp) {
35359
+ if (shapeTimestamp > 0 && shapeTimestamp <= state.lastClearTimestamp) {
35360
+ console.warn(`[APPLY_ACTION] Skipping stale shape from before clear - shape timestamp: ${shapeTimestamp}, clear timestamp: ${state.lastClearTimestamp}`);
35342
35361
  break;
35343
35362
  }
35344
35363
  dispatch({ type: 'ADD_SHAPE', payload: denormalizedShape });
@@ -35392,6 +35411,11 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
35392
35411
  case 'start_draw':
35393
35412
  if (typeof action.payload !== 'string' && !Array.isArray(action.payload) && isCompleteShape(action.payload)) {
35394
35413
  const denormalizedShape = denormalizeShape(action.payload);
35414
+ // Check if this shape is from before the last clear
35415
+ const shapeTimestamp = denormalizedShape.timestamp || action.timestamp || 0;
35416
+ if (shapeTimestamp > 0 && shapeTimestamp <= state.lastClearTimestamp) {
35417
+ break; // Skip stale shapes
35418
+ }
35395
35419
  // Only apply collaborative start_draw if it's from another user
35396
35420
  if (denormalizedShape.userId !== state.userId) {
35397
35421
  // Add to active drawings for real-time collaborative visibility
@@ -35405,6 +35429,11 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
35405
35429
  case 'continue_draw':
35406
35430
  if (typeof action.payload !== 'string' && !Array.isArray(action.payload) && isCompleteShape(action.payload)) {
35407
35431
  const denormalizedShape = denormalizeShape(action.payload);
35432
+ // Check if this shape is from before the last clear
35433
+ const shapeTimestamp = denormalizedShape.timestamp || action.timestamp || 0;
35434
+ if (shapeTimestamp > 0 && shapeTimestamp <= state.lastClearTimestamp) {
35435
+ break; // Skip stale shapes
35436
+ }
35408
35437
  // Only apply collaborative drawing updates if it's not from the current user
35409
35438
  // to avoid interfering with local real-time drawing
35410
35439
  if (denormalizedShape.userId !== state.userId) {
@@ -35419,6 +35448,12 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
35419
35448
  case 'end_draw':
35420
35449
  if (typeof action.payload !== 'string' && !Array.isArray(action.payload) && isCompleteShape(action.payload)) {
35421
35450
  const denormalizedShape = denormalizeShape(action.payload);
35451
+ // Check if this shape is from before the last clear
35452
+ const shapeTimestamp = denormalizedShape.timestamp || action.timestamp || 0;
35453
+ if (shapeTimestamp > 0 && shapeTimestamp <= state.lastClearTimestamp) {
35454
+ console.warn(`[APPLY_ACTION] Skipping stale end_draw from before clear - shape timestamp: ${shapeTimestamp}, clear timestamp: ${state.lastClearTimestamp}`);
35455
+ break; // Skip stale shapes
35456
+ }
35422
35457
  // Only apply collaborative end_draw if it's from another user
35423
35458
  // Local user's end_draw is handled directly in Board component via ADD_SHAPE
35424
35459
  if (denormalizedShape.userId !== state.userId) {
@@ -36446,7 +36481,8 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
36446
36481
  }
36447
36482
  // Create new shape ID for regular drawing tools
36448
36483
  const newShapeId = v4();
36449
- const newDrawingSessionId = `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
36484
+ const timestamp = Date.now();
36485
+ const newDrawingSessionId = `session-${timestamp}-${Math.random().toString(36).substr(2, 9)}`;
36450
36486
  setCurrentShapeId(newShapeId);
36451
36487
  setCurrentDrawingSessionId(newDrawingSessionId);
36452
36488
  // Start drawing
@@ -36463,6 +36499,7 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
36463
36499
  strokeStyle: state.strokeStyle,
36464
36500
  opacity: state.opacity,
36465
36501
  drawingSessionId: newDrawingSessionId,
36502
+ timestamp,
36466
36503
  // Initialize transformation properties
36467
36504
  x: 0,
36468
36505
  y: 0,
@@ -36575,6 +36612,7 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
36575
36612
  strokeStyle: state.strokeStyle,
36576
36613
  opacity: state.opacity,
36577
36614
  drawingSessionId: currentDrawingSessionId,
36615
+ timestamp: Date.now(),
36578
36616
  // Initialize transformation properties
36579
36617
  x: 0,
36580
36618
  y: 0,
@@ -36654,6 +36692,7 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
36654
36692
  strokeStyle: state.strokeStyle,
36655
36693
  opacity: state.opacity,
36656
36694
  drawingSessionId: currentDrawingSessionId,
36695
+ timestamp: Date.now(),
36657
36696
  // Initialize transformation properties
36658
36697
  x: 0,
36659
36698
  y: 0,
@@ -42755,6 +42794,28 @@ const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transp
42755
42794
  leaveRoom(roomId);
42756
42795
  };
42757
42796
  }, [roomId]);
42797
+ // Clear all canvases on component unmount if admin leaves
42798
+ React.useEffect(() => {
42799
+ return () => {
42800
+ // If admin leaves, clear all users' canvases
42801
+ if (isAdmin && queueAction) {
42802
+ const clearTimestamp = Date.now();
42803
+ // Clear local state immediately
42804
+ dispatch({ type: 'CLEAR_CANVAS' });
42805
+ dispatch({ type: 'CLEAR_ACTIVE_DRAWINGS' });
42806
+ // Send clear action to all users
42807
+ queueAction({
42808
+ type: 'clear',
42809
+ payload: {
42810
+ timestamp: clearTimestamp,
42811
+ adminId: userId,
42812
+ },
42813
+ userId: userId,
42814
+ timestamp: clearTimestamp,
42815
+ });
42816
+ }
42817
+ };
42818
+ }, [isAdmin, queueAction, userId, dispatch]);
42758
42819
  // Global cleanup on app unmount
42759
42820
  React.useEffect(() => {
42760
42821
  const handleBeforeUnload = () => {