agora-appbuilder-core 4.1.15 → 4.1.16
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/package.json +1 -1
- package/template/defaultConfig.js +2 -2
- package/template/src/components/Controls.tsx +15 -5
- package/template/src/components/whiteboard/WhiteboardCanvas.tsx +30 -3
- package/template/src/components/whiteboard/WhiteboardConfigure.tsx +166 -9
- package/template/src/components/whiteboard/WhiteboardToolBox.tsx +30 -4
- package/template/src/components/whiteboard/WhiteboardWidget.tsx +6 -2
- package/template/src/pages/video-call/VideoComponent.tsx +9 -2
package/package.json
CHANGED
|
@@ -77,8 +77,8 @@ const DefaultConfig = {
|
|
|
77
77
|
CHAT_ORG_NAME: '',
|
|
78
78
|
CHAT_APP_NAME: '',
|
|
79
79
|
CHAT_URL: '',
|
|
80
|
-
CLI_VERSION: '3.1.
|
|
81
|
-
CORE_VERSION: '4.1.
|
|
80
|
+
CLI_VERSION: '3.1.16',
|
|
81
|
+
CORE_VERSION: '4.1.16',
|
|
82
82
|
DISABLE_LANDSCAPE_MODE: false,
|
|
83
83
|
STT_AUTO_START: false,
|
|
84
84
|
CLOUD_RECORDING_AUTO_START: false,
|
|
@@ -201,7 +201,7 @@ export const WhiteboardListener = () => {
|
|
|
201
201
|
);
|
|
202
202
|
|
|
203
203
|
return () => {
|
|
204
|
-
LocalEventEmitter.
|
|
204
|
+
LocalEventEmitter.off(
|
|
205
205
|
LocalEventsEnum.WHITEBOARD_ACTIVE_LOCAL,
|
|
206
206
|
WhiteboardCallBack,
|
|
207
207
|
);
|
|
@@ -227,8 +227,13 @@ export const WhiteboardListener = () => {
|
|
|
227
227
|
};
|
|
228
228
|
|
|
229
229
|
useEffect(() => {
|
|
230
|
-
|
|
231
|
-
|
|
230
|
+
if (whiteboardActive) {
|
|
231
|
+
dispatch({type: 'UserPin', value: [getWhiteboardUid()]});
|
|
232
|
+
if (currentLayout !== 'pinned') {
|
|
233
|
+
setLayout('pinned');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}, [whiteboardActive]);
|
|
232
237
|
|
|
233
238
|
const toggleWhiteboard = (
|
|
234
239
|
whiteboardActive: boolean,
|
|
@@ -420,8 +425,13 @@ const MoreButton = (props: {fields: ToolbarMoreButtonDefaultFields}) => {
|
|
|
420
425
|
};
|
|
421
426
|
|
|
422
427
|
useEffect(() => {
|
|
423
|
-
|
|
424
|
-
|
|
428
|
+
if (whiteboardActive) {
|
|
429
|
+
dispatch({type: 'UserPin', value: [getWhiteboardUid()]});
|
|
430
|
+
if (currentLayout !== 'pinned') {
|
|
431
|
+
setLayout('pinned');
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}, [whiteboardActive]);
|
|
425
435
|
|
|
426
436
|
const WhiteboardCallBack = ({status}) => {
|
|
427
437
|
if (status) {
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
whiteboardContext,
|
|
17
17
|
whiteboardPaper,
|
|
18
18
|
} from './WhiteboardConfigure';
|
|
19
|
-
import {StyleSheet, View, Text} from 'react-native';
|
|
19
|
+
import {StyleSheet, View, Text, ActivityIndicator} from 'react-native';
|
|
20
20
|
import {RoomPhase, ApplianceNames} from 'white-web-sdk';
|
|
21
21
|
import WhiteboardToolBox from './WhiteboardToolBox';
|
|
22
22
|
import WhiteboardWidget from './WhiteboardWidget';
|
|
@@ -28,7 +28,8 @@ const WhiteboardCanvas: React.FC<WhiteboardCanvasInterface> = ({
|
|
|
28
28
|
showToolbox,
|
|
29
29
|
}) => {
|
|
30
30
|
const wbSurfaceRef = useRef();
|
|
31
|
-
const {whiteboardRoom, boardColor} =
|
|
31
|
+
const {whiteboardRoom, boardColor, whiteboardRoomState} =
|
|
32
|
+
useContext(whiteboardContext);
|
|
32
33
|
|
|
33
34
|
useEffect(function () {
|
|
34
35
|
if (whiteboardPaper) {
|
|
@@ -37,12 +38,22 @@ const WhiteboardCanvas: React.FC<WhiteboardCanvasInterface> = ({
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
return () => {
|
|
40
|
-
|
|
41
|
+
if (whiteboardPaper?.parentElement === wbSurfaceRef?.current) {
|
|
42
|
+
wbSurfaceRef?.current?.removeChild(whiteboardPaper);
|
|
43
|
+
}
|
|
41
44
|
};
|
|
42
45
|
}, []);
|
|
43
46
|
|
|
47
|
+
const isSyncing = whiteboardRoomState === RoomPhase.Connecting;
|
|
48
|
+
|
|
44
49
|
return (
|
|
45
50
|
<>
|
|
51
|
+
{isSyncing && (
|
|
52
|
+
<View style={style.syncingOverlay}>
|
|
53
|
+
<ActivityIndicator size="large" color="#fff" />
|
|
54
|
+
<Text style={style.syncingText}>Syncing whiteboard...</Text>
|
|
55
|
+
</View>
|
|
56
|
+
)}
|
|
46
57
|
<WhiteboardWidget whiteboardRoom={whiteboardRoom} />
|
|
47
58
|
{showToolbox &&
|
|
48
59
|
//@ts-ignore
|
|
@@ -94,6 +105,22 @@ const style = StyleSheet.create({
|
|
|
94
105
|
paddingTop: 50,
|
|
95
106
|
paddingLeft: 20,
|
|
96
107
|
},
|
|
108
|
+
syncingOverlay: {
|
|
109
|
+
position: 'absolute',
|
|
110
|
+
width: '100%',
|
|
111
|
+
height: '100%',
|
|
112
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
113
|
+
display: 'flex',
|
|
114
|
+
justifyContent: 'center',
|
|
115
|
+
alignItems: 'center',
|
|
116
|
+
zIndex: 20,
|
|
117
|
+
borderRadius: 4,
|
|
118
|
+
},
|
|
119
|
+
syncingText: {
|
|
120
|
+
color: '#fff',
|
|
121
|
+
marginTop: 12,
|
|
122
|
+
fontSize: 14,
|
|
123
|
+
},
|
|
97
124
|
});
|
|
98
125
|
|
|
99
126
|
export default WhiteboardCanvas;
|
|
@@ -123,6 +123,9 @@ const WhiteboardConfigure: React.FC<WhiteboardPropsInterface> = props => {
|
|
|
123
123
|
useEffect(() => {
|
|
124
124
|
if (
|
|
125
125
|
whiteboardRoomState === RoomPhase.Connected &&
|
|
126
|
+
// In livestream, don't recenter the camera locally when whiteboard gets pinned.
|
|
127
|
+
// Followers must inherit the broadcaster's current viewport instead.
|
|
128
|
+
!$config.EVENT_MODE &&
|
|
126
129
|
pinnedUid &&
|
|
127
130
|
pinnedUid == whiteboardUidRef.current
|
|
128
131
|
) {
|
|
@@ -141,10 +144,20 @@ const WhiteboardConfigure: React.FC<WhiteboardPropsInterface> = props => {
|
|
|
141
144
|
boardColor: boardColorRemote,
|
|
142
145
|
whiteboardLastImageUploadPosition: whiteboardLastImageUploadPositionRemote,
|
|
143
146
|
} = useRoomInfo();
|
|
147
|
+
const shouldUseCursorAdapter = !($config.EVENT_MODE && !isHost);
|
|
144
148
|
const {currentLayout} = useLayout();
|
|
145
149
|
|
|
146
150
|
useEffect(() => {
|
|
147
151
|
try {
|
|
152
|
+
const setWritable =
|
|
153
|
+
typeof whiteboardRoom?.current?.setWritable === 'function'
|
|
154
|
+
? whiteboardRoom.current.setWritable.bind(whiteboardRoom.current)
|
|
155
|
+
: undefined;
|
|
156
|
+
|
|
157
|
+
if (!setWritable) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
148
161
|
if (
|
|
149
162
|
whiteboardRoomState === RoomPhase.Connected &&
|
|
150
163
|
isHost &&
|
|
@@ -157,9 +170,9 @@ const WhiteboardConfigure: React.FC<WhiteboardPropsInterface> = props => {
|
|
|
157
170
|
(activeUids[0] === getWhiteboardUid() ||
|
|
158
171
|
pinnedUid === getWhiteboardUid())
|
|
159
172
|
) {
|
|
160
|
-
|
|
173
|
+
setWritable(true);
|
|
161
174
|
} else {
|
|
162
|
-
|
|
175
|
+
setWritable(false);
|
|
163
176
|
}
|
|
164
177
|
}
|
|
165
178
|
} catch (error) {
|
|
@@ -170,7 +183,20 @@ const WhiteboardConfigure: React.FC<WhiteboardPropsInterface> = props => {
|
|
|
170
183
|
error,
|
|
171
184
|
);
|
|
172
185
|
}
|
|
173
|
-
|
|
186
|
+
// activeUids[0] (the max-slot uid) is the only element checked in the condition above —
|
|
187
|
+
// using the full activeUids array would re-run setWritable on every participant join/leave,
|
|
188
|
+
// briefly stalling the SDK draw queue and causing cumulative lag for attendees.
|
|
189
|
+
}, [currentLayout, isHost, whiteboardRoomState, activeUids?.[0], pinnedUid]);
|
|
190
|
+
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
if (whiteboardRoomState === RoomPhase.Connected) {
|
|
193
|
+
// Netless reads the bound element size for viewport math. Refresh when layout or
|
|
194
|
+
// pin state changes (those affect the whiteboard container size). Participant
|
|
195
|
+
// count changes do not affect container size in pinned layout, so activeUids.length
|
|
196
|
+
// is intentionally excluded to avoid redundant refreshes on every join.
|
|
197
|
+
whiteboardRoom.current?.refreshViewSize?.();
|
|
198
|
+
}
|
|
199
|
+
}, [whiteboardRoomState, currentLayout, pinnedUid]);
|
|
174
200
|
|
|
175
201
|
const BoardColorChangedCallBack = ({boardColor}) => {
|
|
176
202
|
setBoardColor(boardColor);
|
|
@@ -330,11 +356,18 @@ const WhiteboardConfigure: React.FC<WhiteboardPropsInterface> = props => {
|
|
|
330
356
|
const InitState = whiteboardRoomState;
|
|
331
357
|
try {
|
|
332
358
|
const index = randomIntFromInterval(0, 9);
|
|
359
|
+
const joinStartTs = Date.now();
|
|
333
360
|
setWhiteboardRoomState(RoomPhase.Connecting);
|
|
361
|
+
console.log('[whiteboard-lag] join:start', {
|
|
362
|
+
ts: joinStartTs,
|
|
363
|
+
isHost,
|
|
364
|
+
eventMode: $config.EVENT_MODE,
|
|
365
|
+
whiteboardUid: `${whiteboardUidRef.current}`,
|
|
366
|
+
});
|
|
334
367
|
logger.log(LogSource.Internals, 'WHITEBOARD', 'Trying to join room');
|
|
335
368
|
whiteWebSdkClient.current
|
|
336
369
|
.joinRoom({
|
|
337
|
-
cursorAdapter: cursorAdapter,
|
|
370
|
+
cursorAdapter: shouldUseCursorAdapter ? cursorAdapter : undefined,
|
|
338
371
|
uid: `${whiteboardUidRef.current}`,
|
|
339
372
|
uuid: room_uuid,
|
|
340
373
|
roomToken: room_token,
|
|
@@ -347,17 +380,125 @@ const WhiteboardConfigure: React.FC<WhiteboardPropsInterface> = props => {
|
|
|
347
380
|
},
|
|
348
381
|
})
|
|
349
382
|
.then(room => {
|
|
350
|
-
|
|
383
|
+
const joinSuccessTs = Date.now();
|
|
384
|
+
logger.log(
|
|
385
|
+
LogSource.Internals,
|
|
386
|
+
'WHITEBOARD',
|
|
387
|
+
'Join room successful',
|
|
388
|
+
isHost,
|
|
389
|
+
$config.EVENT_MODE,
|
|
390
|
+
);
|
|
391
|
+
console.log('[whiteboard-lag] join:success', {
|
|
392
|
+
ts: joinSuccessTs,
|
|
393
|
+
latencyMs: joinSuccessTs - joinStartTs,
|
|
394
|
+
isHost,
|
|
395
|
+
eventMode: $config.EVENT_MODE,
|
|
396
|
+
whiteboardUid: `${whiteboardUidRef.current}`,
|
|
397
|
+
});
|
|
351
398
|
whiteboardRoom.current = room;
|
|
352
|
-
|
|
353
|
-
|
|
399
|
+
if (shouldUseCursorAdapter) {
|
|
400
|
+
cursorAdapter.setRoom(room);
|
|
401
|
+
}
|
|
402
|
+
// In livestream: host who starts the whiteboard is Broadcaster (attendees follow their viewport),
|
|
403
|
+
// co-hosts are Followers (follow Broadcaster, auto-switch to Freedom when they interact with the board),
|
|
404
|
+
// If no Broadcaster exists in the room (e.g. all hosts dropped and rejoined), first host to join claims it.
|
|
405
|
+
// In meeting: everyone gets Freedom (independent viewport).
|
|
406
|
+
const noBroadcasterInRoom =
|
|
407
|
+
room.state.broadcastState.broadcasterId === undefined;
|
|
408
|
+
const viewMode = $config.EVENT_MODE
|
|
409
|
+
? isHost
|
|
410
|
+
? noBroadcasterInRoom
|
|
411
|
+
? ViewMode.Broadcaster
|
|
412
|
+
: ViewMode.Follower
|
|
413
|
+
: ViewMode.Follower
|
|
414
|
+
: ViewMode.Freedom;
|
|
415
|
+
console.log('[whiteboard-view-mode] initial', {
|
|
416
|
+
isHost,
|
|
417
|
+
eventMode: $config.EVENT_MODE,
|
|
418
|
+
whiteboardUid: `${whiteboardUidRef.current}`,
|
|
419
|
+
broadcasterId: room.state.broadcastState.broadcasterId,
|
|
420
|
+
noBroadcasterInRoom,
|
|
421
|
+
viewMode,
|
|
422
|
+
});
|
|
423
|
+
room.setViewMode(viewMode);
|
|
424
|
+
// In livestream, lock camera gestures for followers so touchpad pan/zoom
|
|
425
|
+
// cannot kick them out of follower mode into freedom.
|
|
426
|
+
room.disableCameraTransform =
|
|
427
|
+
$config.EVENT_MODE && viewMode === ViewMode.Follower;
|
|
428
|
+
console.log('[whiteboard-lag] viewmode:applied', {
|
|
429
|
+
ts: Date.now(),
|
|
430
|
+
isHost,
|
|
431
|
+
viewMode,
|
|
432
|
+
disableCameraTransform: room.disableCameraTransform,
|
|
433
|
+
broadcasterId: room.state.broadcastState.broadcasterId,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// In livestream, if the Broadcaster drops, the next host to detect it claims Broadcaster.
|
|
437
|
+
// hasSeenBroadcaster ensures we only react to an actual drop (not the transient
|
|
438
|
+
// undefined state during initial room sync before the Broadcaster is propagated).
|
|
439
|
+
if ($config.EVENT_MODE && isHost) {
|
|
440
|
+
let hasSeenBroadcaster = false;
|
|
441
|
+
room.callbacks.on('onRoomStateChanged', modifyState => {
|
|
442
|
+
const currentBroadcastState = room.state?.broadcastState;
|
|
443
|
+
if (currentBroadcastState?.broadcasterId !== undefined) {
|
|
444
|
+
hasSeenBroadcaster = true;
|
|
445
|
+
}
|
|
446
|
+
// broadcasterId becomes undefined only after a clean disconnect (unmount cleanup
|
|
447
|
+
// guarantees this), so this is a reliable signal that the Broadcaster dropped.
|
|
448
|
+
if (
|
|
449
|
+
hasSeenBroadcaster &&
|
|
450
|
+
currentBroadcastState?.broadcasterId === undefined
|
|
451
|
+
) {
|
|
452
|
+
console.log('[whiteboard-view-mode] promote-to-broadcaster', {
|
|
453
|
+
isHost,
|
|
454
|
+
whiteboardUid: `${whiteboardUidRef.current}`,
|
|
455
|
+
});
|
|
456
|
+
room.setViewMode(ViewMode.Broadcaster);
|
|
457
|
+
room.disableCameraTransform = false;
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
}
|
|
354
461
|
whiteboardRoom.current?.bindHtmlElement(whiteboardPaper);
|
|
462
|
+
console.log('[whiteboard-lag] bindHtmlElement', {
|
|
463
|
+
ts: Date.now(),
|
|
464
|
+
isHost,
|
|
465
|
+
viewMode,
|
|
466
|
+
});
|
|
467
|
+
whiteboardRoom.current?.refreshViewSize?.();
|
|
468
|
+
console.log('[whiteboard-lag] refreshViewSize:after-bind', {
|
|
469
|
+
ts: Date.now(),
|
|
470
|
+
isHost,
|
|
471
|
+
viewMode,
|
|
472
|
+
});
|
|
473
|
+
if ($config.EVENT_MODE && viewMode === ViewMode.Follower) {
|
|
474
|
+
// Late followers can occasionally mount before the broadcaster viewport
|
|
475
|
+
// is fully applied. Re-applying follower mode after the first bind/size
|
|
476
|
+
// refresh nudges Netless to sync the current broadcaster view immediately.
|
|
477
|
+
requestAnimationFrame(() => {
|
|
478
|
+
console.log('[whiteboard-lag] follower-resync:start', {
|
|
479
|
+
ts: Date.now(),
|
|
480
|
+
isHost,
|
|
481
|
+
});
|
|
482
|
+
room.refreshViewSize?.();
|
|
483
|
+
room.setViewMode(ViewMode.Follower);
|
|
484
|
+
console.log('[whiteboard-lag] follower-resync:done', {
|
|
485
|
+
ts: Date.now(),
|
|
486
|
+
isHost,
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
}
|
|
355
490
|
if (isHost && !isMobileUA()) {
|
|
356
491
|
whiteboardRoom.current?.setMemberState({
|
|
357
492
|
strokeColor: [0, 0, 0],
|
|
358
493
|
});
|
|
359
494
|
}
|
|
360
495
|
setWhiteboardRoomState(RoomPhase.Connected);
|
|
496
|
+
console.log('[whiteboard-lag] roomPhase:connected', {
|
|
497
|
+
ts: Date.now(),
|
|
498
|
+
isHost,
|
|
499
|
+
viewMode,
|
|
500
|
+
totalJoinLatencyMs: Date.now() - joinStartTs,
|
|
501
|
+
});
|
|
361
502
|
})
|
|
362
503
|
.catch(err => {
|
|
363
504
|
setWhiteboardRoomState(InitState);
|
|
@@ -378,11 +519,13 @@ const WhiteboardConfigure: React.FC<WhiteboardPropsInterface> = props => {
|
|
|
378
519
|
const InitState = whiteboardRoomState;
|
|
379
520
|
try {
|
|
380
521
|
setWhiteboardRoomState(RoomPhase.Disconnecting);
|
|
381
|
-
whiteboardRoom.current
|
|
522
|
+
const room = whiteboardRoom.current;
|
|
523
|
+
room
|
|
382
524
|
?.disconnect()
|
|
383
525
|
.then(() => {
|
|
526
|
+
room?.bindHtmlElement(null);
|
|
527
|
+
whiteboardRoom.current = {} as Room;
|
|
384
528
|
whiteboardUidRef.current = Date.now();
|
|
385
|
-
whiteboardRoom.current?.bindHtmlElement(null);
|
|
386
529
|
setWhiteboardRoomState(RoomPhase.Disconnected);
|
|
387
530
|
})
|
|
388
531
|
.catch(err => {
|
|
@@ -429,6 +572,20 @@ const WhiteboardConfigure: React.FC<WhiteboardPropsInterface> = props => {
|
|
|
429
572
|
}
|
|
430
573
|
}, [whiteboardActive]);
|
|
431
574
|
|
|
575
|
+
// Disconnect from whiteboard room when component unmounts (e.g. user leaves the call abruptly)
|
|
576
|
+
useEffect(() => {
|
|
577
|
+
return () => {
|
|
578
|
+
if (
|
|
579
|
+
whiteboardRoom.current &&
|
|
580
|
+
Object.keys(whiteboardRoom.current)?.length
|
|
581
|
+
) {
|
|
582
|
+
whiteboardRoom.current?.bindHtmlElement(null);
|
|
583
|
+
whiteboardRoom.current?.disconnect();
|
|
584
|
+
whiteboardRoom.current = {} as Room;
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
}, []);
|
|
588
|
+
|
|
432
589
|
const getWhiteboardUid = () => {
|
|
433
590
|
return whiteboardUidRef?.current;
|
|
434
591
|
};
|
|
@@ -261,6 +261,8 @@ const WhiteboardToolBox = ({whiteboardRoom}) => {
|
|
|
261
261
|
const [isColorContainerHovered, setColorContainerHovered] = useState(false);
|
|
262
262
|
const [isPencilBtnHovered, setPencilBtnHovered] = useState(false);
|
|
263
263
|
const [isPencilContainerHovered, setPencilContainerHovered] = useState(false);
|
|
264
|
+
const roomStateChangedRef = React.useRef(null);
|
|
265
|
+
const clearWhiteboardRef = React.useRef(null);
|
|
264
266
|
const handleSelect = (applicanceName: ApplianceNames) => {
|
|
265
267
|
if (applicanceName !== ApplianceNames.selector) {
|
|
266
268
|
setCursorColor(ColorPickerValues[selectedColor].rgb);
|
|
@@ -272,17 +274,41 @@ const WhiteboardToolBox = ({whiteboardRoom}) => {
|
|
|
272
274
|
};
|
|
273
275
|
|
|
274
276
|
useEffect(() => {
|
|
275
|
-
|
|
277
|
+
roomStateChangedRef.current = modifyState => {
|
|
276
278
|
setRoomState({
|
|
277
279
|
...whiteboardRoom?.current?.state,
|
|
278
280
|
...modifyState,
|
|
279
281
|
});
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
+
};
|
|
283
|
+
clearWhiteboardRef.current = () => {
|
|
282
284
|
whiteboardRoom.current?.cleanCurrentScene();
|
|
283
285
|
setShowWhiteboardClearAllPopup(false);
|
|
284
286
|
clearAllCallback();
|
|
285
|
-
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
whiteboardRoom?.current?.callbacks?.on(
|
|
290
|
+
'onRoomStateChanged',
|
|
291
|
+
roomStateChangedRef.current,
|
|
292
|
+
);
|
|
293
|
+
LocalEventEmitter.on(
|
|
294
|
+
LocalEventsEnum.CLEAR_WHITEBOARD,
|
|
295
|
+
clearWhiteboardRef.current,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
return () => {
|
|
299
|
+
if (roomStateChangedRef.current) {
|
|
300
|
+
whiteboardRoom?.current?.callbacks?.off(
|
|
301
|
+
'onRoomStateChanged',
|
|
302
|
+
roomStateChangedRef.current,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
if (clearWhiteboardRef.current) {
|
|
306
|
+
LocalEventEmitter.off(
|
|
307
|
+
LocalEventsEnum.CLEAR_WHITEBOARD,
|
|
308
|
+
clearWhiteboardRef.current,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
};
|
|
286
312
|
}, []);
|
|
287
313
|
|
|
288
314
|
useEffect(() => {
|
|
@@ -84,7 +84,7 @@ const WhiteboardWidget = ({whiteboardRoom}) => {
|
|
|
84
84
|
isWhiteboardOnFullScreen,
|
|
85
85
|
} = useContext(whiteboardContext);
|
|
86
86
|
const {
|
|
87
|
-
data: {whiteboard: {room_uuid} = {}},
|
|
87
|
+
data: {isHost, whiteboard: {room_uuid} = {}},
|
|
88
88
|
} = useRoomInfo();
|
|
89
89
|
const {store} = useContext(StorageContext);
|
|
90
90
|
|
|
@@ -267,7 +267,11 @@ const WhiteboardWidget = ({whiteboardRoom}) => {
|
|
|
267
267
|
) : (
|
|
268
268
|
<></>
|
|
269
269
|
)}
|
|
270
|
-
{
|
|
270
|
+
{/* In livestream, hide all whiteboard controls for attendees — they are Followers
|
|
271
|
+
and interacting with the board (zoom/pan) would break viewport sync with the host */}
|
|
272
|
+
{isWebInternal() &&
|
|
273
|
+
!isMobileUA() &&
|
|
274
|
+
!($config.EVENT_MODE && !isHost) ? (
|
|
271
275
|
<View style={style.widgetContainer}>
|
|
272
276
|
{whiteboardRoom.current?.isWritable ? (
|
|
273
277
|
<>
|
|
@@ -12,6 +12,7 @@ import Spacer from '../../atoms/Spacer';
|
|
|
12
12
|
import {useLiveStreamDataContext} from '../../components/contexts/LiveStreamDataContext';
|
|
13
13
|
import {useCustomization} from 'customization-implementation';
|
|
14
14
|
import useMount from '../../components/useMount';
|
|
15
|
+
import {whiteboardContext} from '../../components/whiteboard/WhiteboardConfigure';
|
|
15
16
|
|
|
16
17
|
const VideoComponent = () => {
|
|
17
18
|
const {dispatch} = useContext(DispatchContext);
|
|
@@ -19,6 +20,7 @@ const VideoComponent = () => {
|
|
|
19
20
|
const layoutsData = useLayoutsData();
|
|
20
21
|
const {currentLayout, setLayout} = useLayout();
|
|
21
22
|
const {activeUids, pinnedUid} = useContent();
|
|
23
|
+
const {whiteboardActive} = useContext(whiteboardContext);
|
|
22
24
|
const {rtcProps} = useContext(PropsContext);
|
|
23
25
|
const isDesktop = useIsDesktop();
|
|
24
26
|
const {audienceUids, hostUids} = useLiveStreamDataContext();
|
|
@@ -58,7 +60,12 @@ const VideoComponent = () => {
|
|
|
58
60
|
const currentLayoutRef = useRef(currentLayout);
|
|
59
61
|
const gridLayoutName = getGridLayoutName();
|
|
60
62
|
useEffect(() => {
|
|
61
|
-
|
|
63
|
+
// When only one participant is visible, reset pinning and revert to grid layout.
|
|
64
|
+
// Skip this reset when whiteboard is active: in EVENT_MODE the audience's own uid is
|
|
65
|
+
// filtered from activeUids, so activeUids.length === 1 (host only) even while the
|
|
66
|
+
// whiteboard uid is being added. Without this guard the effect would clear pinnedUid
|
|
67
|
+
// and switch to grid, preventing the whiteboard from appearing in the max/pinned slot.
|
|
68
|
+
if (activeUids && activeUids.length === 1 && !isCustomLayoutUsed && !whiteboardActive) {
|
|
62
69
|
if (pinnedUid) {
|
|
63
70
|
dispatch({type: 'UserPin', value: [0]});
|
|
64
71
|
dispatch({type: 'UserSecondaryPin', value: [0]});
|
|
@@ -67,7 +74,7 @@ const VideoComponent = () => {
|
|
|
67
74
|
setLayout(gridLayoutName);
|
|
68
75
|
}
|
|
69
76
|
}
|
|
70
|
-
}, [activeUids, isCustomLayoutUsed]);
|
|
77
|
+
}, [activeUids, isCustomLayoutUsed, whiteboardActive]);
|
|
71
78
|
|
|
72
79
|
useEffect(() => {
|
|
73
80
|
currentLayoutRef.current = currentLayout;
|