@ngenux/ngage-whiteboarding 1.0.9 → 1.0.10
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 +170 -40
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +169 -39
- package/dist/index.js.map +1 -1
- package/dist/src/components/Whiteboard/index.d.ts +5 -0
- package/dist/src/components/Whiteboard/index.d.ts.map +1 -1
- package/dist/src/context/WhiteboardContext.d.ts +7 -0
- package/dist/src/context/WhiteboardContext.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
2
|
import * as React from 'react';
|
|
3
|
-
import React__default, { createContext, useReducer, useContext, useState,
|
|
3
|
+
import React__default, { createContext, useRef, useReducer, useEffect, useContext, useState, useMemo, forwardRef, useCallback, useImperativeHandle, createElement } from 'react';
|
|
4
4
|
|
|
5
5
|
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
|
6
6
|
|
|
@@ -34911,6 +34911,21 @@ const whiteboardReducer = (state, action) => {
|
|
|
34911
34911
|
userUndoStacks: {}, // Clear all user undo stacks as well
|
|
34912
34912
|
};
|
|
34913
34913
|
}
|
|
34914
|
+
case 'RESET_EMPTY_ROOM': {
|
|
34915
|
+
// Same visual reset as a fresh board, but lastClearTimestamp stays 0 so shapes from
|
|
34916
|
+
// sync_state (drawn before this client joined) are not rejected as pre-clear stale.
|
|
34917
|
+
return {
|
|
34918
|
+
...state,
|
|
34919
|
+
shapes: [],
|
|
34920
|
+
history: [[]],
|
|
34921
|
+
historyIndex: 0,
|
|
34922
|
+
selectedShapeId: undefined,
|
|
34923
|
+
currentDrawingShape: undefined,
|
|
34924
|
+
activeDrawings: {},
|
|
34925
|
+
userUndoStacks: {},
|
|
34926
|
+
lastClearTimestamp: 0,
|
|
34927
|
+
};
|
|
34928
|
+
}
|
|
34914
34929
|
case 'UNDO': {
|
|
34915
34930
|
if (state.historyIndex > 0) {
|
|
34916
34931
|
return {
|
|
@@ -35116,8 +35131,120 @@ const whiteboardReducer = (state, action) => {
|
|
|
35116
35131
|
}
|
|
35117
35132
|
};
|
|
35118
35133
|
const WhiteboardContext = createContext(undefined);
|
|
35119
|
-
|
|
35120
|
-
|
|
35134
|
+
// Helper function to get storage key for a room
|
|
35135
|
+
const getStorageKey = (roomId) => {
|
|
35136
|
+
return roomId ? `whiteboard-state-${roomId}` : 'whiteboard-state-default';
|
|
35137
|
+
};
|
|
35138
|
+
// Helper function to load persisted state from sessionStorage
|
|
35139
|
+
const loadPersistedState = (roomId) => {
|
|
35140
|
+
try {
|
|
35141
|
+
const key = getStorageKey(roomId);
|
|
35142
|
+
const persisted = sessionStorage.getItem(key);
|
|
35143
|
+
if (persisted) {
|
|
35144
|
+
const parsed = JSON.parse(persisted);
|
|
35145
|
+
return parsed;
|
|
35146
|
+
}
|
|
35147
|
+
}
|
|
35148
|
+
catch (error) {
|
|
35149
|
+
console.error('[PERSISTENCE] Failed to load persisted state:', error);
|
|
35150
|
+
}
|
|
35151
|
+
return null;
|
|
35152
|
+
};
|
|
35153
|
+
// Helper function to save state to sessionStorage
|
|
35154
|
+
const savePersistedState = (state, roomId) => {
|
|
35155
|
+
try {
|
|
35156
|
+
const key = getStorageKey(roomId);
|
|
35157
|
+
// Only persist essential state (shapes, history, background, canvas size)
|
|
35158
|
+
const stateToPersist = {
|
|
35159
|
+
shapes: state.shapes,
|
|
35160
|
+
history: state.history,
|
|
35161
|
+
historyIndex: state.historyIndex,
|
|
35162
|
+
backgroundColor: state.backgroundColor,
|
|
35163
|
+
canvasSize: state.canvasSize,
|
|
35164
|
+
lastClearTimestamp: state.lastClearTimestamp,
|
|
35165
|
+
userUndoStacks: state.userUndoStacks,
|
|
35166
|
+
};
|
|
35167
|
+
sessionStorage.setItem(key, JSON.stringify(stateToPersist));
|
|
35168
|
+
}
|
|
35169
|
+
catch (error) {
|
|
35170
|
+
console.error('[PERSISTENCE] Failed to save persisted state:', error);
|
|
35171
|
+
}
|
|
35172
|
+
};
|
|
35173
|
+
// Helper function to clear persisted state
|
|
35174
|
+
const clearPersistedState = (roomId) => {
|
|
35175
|
+
try {
|
|
35176
|
+
const key = getStorageKey(roomId);
|
|
35177
|
+
sessionStorage.removeItem(key);
|
|
35178
|
+
}
|
|
35179
|
+
catch (error) {
|
|
35180
|
+
console.error('[PERSISTENCE] Failed to clear persisted state:', error);
|
|
35181
|
+
}
|
|
35182
|
+
};
|
|
35183
|
+
const WhiteboardProvider = ({ children, webSocketUrl, roomId: initialRoomId }) => {
|
|
35184
|
+
const [currentRoomId, setCurrentRoomId] = React__default.useState(initialRoomId);
|
|
35185
|
+
// Track if a clear action was performed
|
|
35186
|
+
const clearActionRef = useRef(false);
|
|
35187
|
+
// Load persisted state if available, otherwise use initialState
|
|
35188
|
+
const getInitialState = () => {
|
|
35189
|
+
const persisted = loadPersistedState(currentRoomId);
|
|
35190
|
+
if (persisted) {
|
|
35191
|
+
// Merge persisted state with current initialState to ensure all properties exist
|
|
35192
|
+
return {
|
|
35193
|
+
...initialState,
|
|
35194
|
+
...persisted,
|
|
35195
|
+
// Reset transient state
|
|
35196
|
+
isDrawing: false,
|
|
35197
|
+
currentDrawingShape: undefined,
|
|
35198
|
+
selectedShapeId: undefined,
|
|
35199
|
+
activeDrawings: {},
|
|
35200
|
+
captureEnabled: true,
|
|
35201
|
+
};
|
|
35202
|
+
}
|
|
35203
|
+
return initialState;
|
|
35204
|
+
};
|
|
35205
|
+
const [state, dispatch] = useReducer((s, action) => {
|
|
35206
|
+
// Track clear action
|
|
35207
|
+
if (action.type === 'CLEAR_CANVAS') {
|
|
35208
|
+
clearActionRef.current = true;
|
|
35209
|
+
}
|
|
35210
|
+
return whiteboardReducer(s, action);
|
|
35211
|
+
}, getInitialState());
|
|
35212
|
+
const prevRoomIdRef = useRef(currentRoomId);
|
|
35213
|
+
// Save state to sessionStorage whenever it changes (debounced)
|
|
35214
|
+
useEffect(() => {
|
|
35215
|
+
const timeoutId = setTimeout(() => {
|
|
35216
|
+
// Only save empty shapes if a clear action was performed
|
|
35217
|
+
if (state.shapes.length > 0 || clearActionRef.current) {
|
|
35218
|
+
savePersistedState(state, currentRoomId);
|
|
35219
|
+
clearActionRef.current = false;
|
|
35220
|
+
}
|
|
35221
|
+
}, 500); // Debounce to avoid excessive writes
|
|
35222
|
+
return () => clearTimeout(timeoutId);
|
|
35223
|
+
}, [state.shapes, state.history, state.backgroundColor, state.canvasSize, state.lastClearTimestamp, currentRoomId]);
|
|
35224
|
+
// Handle room changes - load new room state or clear if switching rooms
|
|
35225
|
+
useEffect(() => {
|
|
35226
|
+
if (prevRoomIdRef.current !== currentRoomId) {
|
|
35227
|
+
// Save current room state before switching
|
|
35228
|
+
if (prevRoomIdRef.current) {
|
|
35229
|
+
savePersistedState(state, prevRoomIdRef.current);
|
|
35230
|
+
}
|
|
35231
|
+
// Load new room state
|
|
35232
|
+
const newRoomState = loadPersistedState(currentRoomId);
|
|
35233
|
+
if (newRoomState) {
|
|
35234
|
+
dispatch({ type: 'SET_SHAPES', payload: newRoomState.shapes || [] });
|
|
35235
|
+
dispatch({ type: 'SET_BACKGROUND_COLOR', payload: newRoomState.backgroundColor || '#FFFFFF' });
|
|
35236
|
+
if (newRoomState.lastClearTimestamp) {
|
|
35237
|
+
dispatch({ type: 'SET_CLEAR_TIMESTAMP', payload: newRoomState.lastClearTimestamp });
|
|
35238
|
+
}
|
|
35239
|
+
}
|
|
35240
|
+
else {
|
|
35241
|
+
// No persisted state for new room — empty locally but do not use CLEAR_CANVAS
|
|
35242
|
+
// (that sets lastClearTimestamp = now and breaks applying peer sync_state for pre-join strokes).
|
|
35243
|
+
dispatch({ type: 'RESET_EMPTY_ROOM' });
|
|
35244
|
+
}
|
|
35245
|
+
prevRoomIdRef.current = currentRoomId;
|
|
35246
|
+
}
|
|
35247
|
+
}, [currentRoomId, state]);
|
|
35121
35248
|
const normalizePoint = (point) => {
|
|
35122
35249
|
return {
|
|
35123
35250
|
x: point.x / state.canvasSize.width,
|
|
@@ -35198,7 +35325,7 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35198
35325
|
};
|
|
35199
35326
|
};
|
|
35200
35327
|
// Store reference to current queue action for state requests
|
|
35201
|
-
|
|
35328
|
+
const currentQueueActionRef = useRef(null);
|
|
35202
35329
|
// Type guard and shape normalizer to ensure we have a complete ShapeProps object
|
|
35203
35330
|
const isCompleteShape = (obj) => {
|
|
35204
35331
|
if (!obj) {
|
|
@@ -35254,10 +35381,10 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35254
35381
|
return true;
|
|
35255
35382
|
};
|
|
35256
35383
|
const requestStateFromPeers = () => {
|
|
35257
|
-
if (
|
|
35384
|
+
if (currentQueueActionRef.current) {
|
|
35258
35385
|
setTimeout(() => {
|
|
35259
|
-
if (
|
|
35260
|
-
|
|
35386
|
+
if (currentQueueActionRef.current) {
|
|
35387
|
+
currentQueueActionRef.current({
|
|
35261
35388
|
type: 'request_state',
|
|
35262
35389
|
payload: '',
|
|
35263
35390
|
requesterId: state.userId,
|
|
@@ -35296,9 +35423,9 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35296
35423
|
switch (action.type) {
|
|
35297
35424
|
case 'request_state':
|
|
35298
35425
|
// Another user is requesting current state, send our state to them
|
|
35299
|
-
if (typeof action.payload === 'string' && action.requesterId &&
|
|
35426
|
+
if (typeof action.payload === 'string' && action.requesterId && currentQueueActionRef.current) {
|
|
35300
35427
|
const currentState = getCurrentSyncState();
|
|
35301
|
-
|
|
35428
|
+
currentQueueActionRef.current({
|
|
35302
35429
|
type: 'sync_state',
|
|
35303
35430
|
payload: currentState,
|
|
35304
35431
|
requesterId: action.requesterId, // Send specifically to the requester
|
|
@@ -35372,16 +35499,19 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35372
35499
|
console.warn(`[APPLY_ACTION] Failed to delete shape - payload not string:`, action.payload);
|
|
35373
35500
|
}
|
|
35374
35501
|
break;
|
|
35375
|
-
case 'clear':
|
|
35376
|
-
//
|
|
35502
|
+
case 'clear': {
|
|
35503
|
+
// All users (not just host) must clear their local state when a collaborative clear is received
|
|
35377
35504
|
dispatch({ type: 'CLEAR_CANVAS' });
|
|
35378
35505
|
dispatch({ type: 'CLEAR_ACTIVE_DRAWINGS' });
|
|
35506
|
+
// Clear persisted state in sessionStorage
|
|
35507
|
+
clearPersistedState(currentRoomId);
|
|
35379
35508
|
// Update last clear time to prevent conflicting actions
|
|
35380
35509
|
if (typeof action.payload === 'object' && action.payload !== null && 'timestamp' in action.payload) {
|
|
35381
35510
|
const clearTimestamp = action.payload.timestamp;
|
|
35382
35511
|
dispatch({ type: 'SET_CLEAR_TIMESTAMP', payload: clearTimestamp });
|
|
35383
35512
|
}
|
|
35384
35513
|
break;
|
|
35514
|
+
}
|
|
35385
35515
|
case 'undo':
|
|
35386
35516
|
case 'redo':
|
|
35387
35517
|
if (Array.isArray(action.payload)) {
|
|
@@ -35557,8 +35687,9 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
|
|
|
35557
35687
|
getCurrentSyncState,
|
|
35558
35688
|
requestStateFromPeers,
|
|
35559
35689
|
setQueueAction: (queueAction) => {
|
|
35560
|
-
|
|
35690
|
+
currentQueueActionRef.current = queueAction;
|
|
35561
35691
|
},
|
|
35692
|
+
setCurrentRoomId,
|
|
35562
35693
|
normalizePoint,
|
|
35563
35694
|
denormalizePoint,
|
|
35564
35695
|
webSocketUrl,
|
|
@@ -42433,8 +42564,8 @@ const waitForSocket = (webSocketUrl, timeoutMs = 5000) => {
|
|
|
42433
42564
|
});
|
|
42434
42565
|
};
|
|
42435
42566
|
|
|
42436
|
-
const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transparentBackground = false, videoStream }) => {
|
|
42437
|
-
const { state, dispatch, setQueueAction, requestStateFromPeers, webSocketUrl } = useWhiteboard();
|
|
42567
|
+
const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transparentBackground = false, videoStream, externalApiRef }) => {
|
|
42568
|
+
const { state, dispatch, setQueueAction, requestStateFromPeers, setCurrentRoomId, webSocketUrl } = useWhiteboard();
|
|
42438
42569
|
const [lastCollaborativeAction, setLastCollaborativeAction] = useState(null);
|
|
42439
42570
|
const [hasRequestedState, setHasRequestedState] = useState(false);
|
|
42440
42571
|
const [lastStateRequestTime, setLastStateRequestTime] = useState(0);
|
|
@@ -42458,6 +42589,10 @@ const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transp
|
|
|
42458
42589
|
useEffect(() => {
|
|
42459
42590
|
dispatch({ type: 'SET_USER_ID', payload: userId });
|
|
42460
42591
|
}, [userId, dispatch]);
|
|
42592
|
+
// Set current roomId in context for persistence
|
|
42593
|
+
useEffect(() => {
|
|
42594
|
+
setCurrentRoomId(roomId);
|
|
42595
|
+
}, [roomId, setCurrentRoomId]);
|
|
42461
42596
|
// Set background color based on transparentBackground prop or videoStream presence
|
|
42462
42597
|
useEffect(() => {
|
|
42463
42598
|
if (transparentBackground || videoStream) {
|
|
@@ -42593,9 +42728,10 @@ const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transp
|
|
|
42593
42728
|
setLastStateRequestTime(0);
|
|
42594
42729
|
setIsRoomJoined(false);
|
|
42595
42730
|
}, [roomId]);
|
|
42596
|
-
// Retry state request if canvas is still empty
|
|
42731
|
+
// Retry state request if canvas is still empty (e.g. first request raced before host had queueAction).
|
|
42732
|
+
// Intentionally does NOT gate on hasRequestedState — the initial effect sets that immediately while send is delayed 2s.
|
|
42597
42733
|
useEffect(() => {
|
|
42598
|
-
if (!isRoomJoined || !isSocketConnected ||
|
|
42734
|
+
if (!isRoomJoined || !isSocketConnected || state.shapes.length > 0 || !queueAction) {
|
|
42599
42735
|
return;
|
|
42600
42736
|
}
|
|
42601
42737
|
const now = Date.now();
|
|
@@ -42606,9 +42742,9 @@ const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transp
|
|
|
42606
42742
|
const retryTimer = setTimeout(() => {
|
|
42607
42743
|
requestStateFromPeers();
|
|
42608
42744
|
setLastStateRequestTime(Date.now());
|
|
42609
|
-
},
|
|
42745
|
+
}, 4500);
|
|
42610
42746
|
return () => clearTimeout(retryTimer);
|
|
42611
|
-
}, [isRoomJoined, isSocketConnected, state.shapes.length, queueAction, requestStateFromPeers,
|
|
42747
|
+
}, [isRoomJoined, isSocketConnected, state.shapes.length, queueAction, requestStateFromPeers, roomId]);
|
|
42612
42748
|
// Cleanup stale active drawings periodically
|
|
42613
42749
|
useEffect(() => {
|
|
42614
42750
|
const cleanupInterval = setInterval(() => {
|
|
@@ -42707,6 +42843,22 @@ const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transp
|
|
|
42707
42843
|
});
|
|
42708
42844
|
}
|
|
42709
42845
|
}, [isAdmin, dispatch, queueAction, userId]);
|
|
42846
|
+
// Expose a minimal imperative API for host apps that need to perform the same
|
|
42847
|
+
// collaborative clear as the toolbar button (e.g., on "stop screenshare with annotations").
|
|
42848
|
+
useEffect(() => {
|
|
42849
|
+
if (!externalApiRef) {
|
|
42850
|
+
return;
|
|
42851
|
+
}
|
|
42852
|
+
externalApiRef.current = {
|
|
42853
|
+
clear: handleClear,
|
|
42854
|
+
getRoomId: () => roomId,
|
|
42855
|
+
};
|
|
42856
|
+
return () => {
|
|
42857
|
+
if (externalApiRef.current?.getRoomId?.() === roomId) {
|
|
42858
|
+
externalApiRef.current = null;
|
|
42859
|
+
}
|
|
42860
|
+
};
|
|
42861
|
+
}, [externalApiRef, handleClear, roomId]);
|
|
42710
42862
|
const handleLockToggle = useCallback(() => {
|
|
42711
42863
|
if (!isAdmin) {
|
|
42712
42864
|
return;
|
|
@@ -42789,28 +42941,6 @@ const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transp
|
|
|
42789
42941
|
leaveRoom(roomId);
|
|
42790
42942
|
};
|
|
42791
42943
|
}, [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]);
|
|
42814
42944
|
// Global cleanup on app unmount
|
|
42815
42945
|
useEffect(() => {
|
|
42816
42946
|
const handleBeforeUnload = () => {
|