From c7ef0638e824614a10f88e6aa9213d71486a8207 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:23:20 +0900 Subject: [PATCH 01/88] Update mutations.jsx --- .../imports/ui/components/presentation/mutations.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx b/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx index 6e11136d3a83..aae509f6375e 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx @@ -97,6 +97,7 @@ export const PRESENTATION_PUBLISH_CURSOR = gql` whiteboardId: $whiteboardId, xPercent: $xPercent, yPercent: $yPercent, + laserType: $laserType, ) } `; From c3e13c029653f12ff03e6ceadd9239078d94ce62 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:57:01 +0900 Subject: [PATCH 02/88] Update component.jsx --- .../ui/components/whiteboard/component.jsx | 274 +++++++++++++++++- 1 file changed, 273 insertions(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index add22ab1fdac..2cbf56032a1e 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -212,6 +212,9 @@ const Whiteboard = React.memo((props) => { const isMountedPollingFrameRef = React.useRef(null); const hasZoomSyncedRef = useRef(false); const currentUserRef = useRef(currentUser); + const currentLaserTypeRef = React.useRef(null); + const laserLayerRef = React.useRef(null); + const laserElRef = React.useRef(null); currentUserRef.current = currentUser; @@ -864,6 +867,7 @@ const Whiteboard = React.memo((props) => { const [cursorPosition, updateCursorPosition] = useCursor( publishCursorUpdate, whiteboardIdRef.current, + laserMode, ); const setCamera = (zoom, x = 0, y = 0) => { @@ -1764,6 +1768,61 @@ const Whiteboard = React.memo((props) => { } }; + const makeLaserSvg = ({ color, cx, cy, r }, id) => { + const width = cx * 2; + const height = cy * 2; + + // On Windows and Linux, it darkens towards the edge + return ` + + + + + + + + + + `.replace(/\s+/g, ' ').trim(); + }; + + const laserDefs = { + redSmall: { color: 'red', cx: 16, cy: 16, r: 14 }, + greenSmall: { color: 'lime', cx: 16, cy: 16, r: 14 }, + redLarge: { color: 'red', cx: 24, cy: 24, r: 22 }, + greenLarge: { color: 'lime', cx: 24, cy: 24, r: 22 }, + }; + + const laserSvgs = Object.fromEntries( + Object.entries(laserDefs).map(([key, def]) => [ + key, + makeLaserSvg(def, key), + ]) + ); + + const svgToCursor = (svg, x, y) => + `url("data:image/svg+xml;utf8,${encodeURIComponent(svg)}") ${x} ${y}, auto`; + + const cursorLasers = Object.fromEntries( + Object.entries(laserDefs).map(([key, def]) => [ + key, + svgToCursor(laserSvgs[key], def.cx, def.cy), + ]) + ); + + const createLaserElement = (svgString, targetDoc, def) => { + const wrapper = targetDoc.createElement('div'); + wrapper.className = 'custom-laser'; + wrapper.innerHTML = svgString; + const el = wrapper.firstChild; + Object.assign(el.style, { + position: 'absolute', + pointerEvents: 'none', + overflow: 'visible', + }); + return el; + }; + useMouseEvents( { whiteboardRef, tlEditorRef, isWheelZoomRef, initialZoomRef, isPresenterRef, @@ -1952,6 +2011,151 @@ const Whiteboard = React.memo((props) => { } }, [currentPresentationPage, isPresenter]); + React.useEffect(() => { + const targetDoc = document; + const presentationWrapper = targetDoc.querySelector('#presentationInnerWrapper'); + if (!presentationWrapper) return; + if (!isPresenter) return; + + let menuEl = null; + + const handleOutsideClick = (e) => { + if (!menuEl) return; + if (menuEl.contains(e.target)) return; + removeMenu(); + }; + + const removeMenu = () => { + if (menuEl) { + menuEl.remove(); + menuEl = null; + targetDoc.removeEventListener('mousedown', handleOutsideClick); + } + }; + + const createMenu = (x, y) => { + removeMenu(); + + const container = + targetDoc.fullscreenElement || + targetDoc.querySelector('#presentationInnerWrapper') || + targetDoc.body; + + menuEl = targetDoc.createElement('div'); + + Object.assign(menuEl.style, { + position: 'fixed', + left: `${x}px`, + top: `${y}px`, + background: '#1e1e1e', + color: '#fff', + padding: '6px', + borderRadius: '8px', + boxShadow: '0 4px 12px rgba(0,0,0,0.3)', + zIndex: 99999, + }); + + const items = [ + { key: 'redSmall', label: '🔴', size: 10 }, + { key: 'greenSmall', label: '🟢', size: 10 }, + { key: 'redLarge', label: '🔴', size: 18 }, + { key: 'greenLarge', label: '🟢', size: 18 }, + { key: '', label: '✋', size: 14 }, + ]; + + items.forEach(({ key, label, size }) => { + const item = targetDoc.createElement('div'); + item.innerHTML = `${label}`; + + Object.assign(item.style, { + padding: '6px 10px', + cursor: 'pointer', + borderRadius: '4px', + textAlign: 'center', + }); + + item.onmouseenter = () => (item.style.background = '#333'); + item.onmouseleave = () => (item.style.background = 'transparent'); + + item.onclick = () => { + setLaserMode(key); + removeMenu(); + }; + + menuEl.appendChild(item); + }); + + container.appendChild(menuEl); + + // compensation at the window edge + const rect = menuEl.getBoundingClientRect(); + const vw = targetDoc.defaultView.innerWidth; + const vh = targetDoc.defaultView.innerHeight; + + if (rect.right > vw) { + menuEl.style.left = `${vw - rect.width - 8}px`; + } + if (rect.bottom > vh) { + menuEl.style.top = `${vh - rect.height - 50}px`; + } + + targetDoc.addEventListener('mousedown', handleOutsideClick); + }; + + const handleContextMenu = (e) => { + const tool = tlEditorRef.current?.getCurrentToolId?.(); + if (tool !== 'hand') return; + if (!presentationWrapper.contains(e.target)) return; + e.preventDefault(); + createMenu(e.clientX, e.clientY); + }; + + targetDoc.addEventListener('contextmenu', handleContextMenu); + + return () => { + targetDoc.removeEventListener('contextmenu', handleContextMenu); + removeMenu(); + }; + }, [popupWindow, isPresenter]); + + React.useEffect(() => { + const targetDoc = document; + if (!isPresenter) return; + + const applyLaser = () => { + const el = targetDoc.querySelector('.tl-container'); + if (!el) return false; + + const laser = cursorLasers[laserMode]; + + if (laser) { + el.style.setProperty('--tl-cursor-grab', laser); + el.style.setProperty('--tl-cursor-grabbing', laser); + } else { + el.style.removeProperty('--tl-cursor-grab'); + el.style.removeProperty('--tl-cursor-grabbing'); + } + + return true; + }; + + // don't obeserve if already done + if (applyLaser()) return; + + const observer = new MutationObserver(() => { + if (applyLaser()) { + observer.disconnect(); + } + }); + + observer.observe(targetDoc.body, { + childList: true, + subtree: true, + }); + + return () => observer.disconnect() + }, [laserMode, isPresenter, popupWindow]); + React.useEffect(() => { if (tlEditorRef.current) { const useElement = document.querySelector('.tl-cursor use'); @@ -1979,7 +2183,7 @@ const Whiteboard = React.memo((props) => { const updatedPresences = otherCursors .map(({ - userId, xPercent, yPercent, presenter, name, isModerator, + userId, xPercent, yPercent, laserType, presenter, name, isModerator, }) => { const id = InstancePresenceRecordType.createId(userId); const active = xPercent !== -1 && yPercent !== -1; @@ -2028,6 +2232,74 @@ const Whiteboard = React.memo((props) => { } }, [otherCursors, whiteboardWriters]); + // Show viewers Laser SVG + React.useEffect(() => { + if (isPresenter) return; + if (isMultiUserActive) return; + + const tlContainer = document.querySelector('.tl-container'); + + let layer = laserLayerRef.current; + if (!layer || !document.contains(layer)) { + layer = document.querySelector('.tl-overlays > .tl-html-layer'); + laserLayerRef.current = layer; + } + + let laserEl = laserElRef.current; + + const presenterCursor = otherCursors.find(c => c.presenter); + const laserKey = presenterCursor?.laserType; + const laserDef = laserDefs[laserKey]; + + const changed = laserKey !== currentLaserTypeRef.current; + if (changed) { + currentLaserTypeRef.current = laserKey; + laserEl?.remove(); + laserEl = null; + laserElRef.current = null; + const defaultPointer = document.getElementById('redPointer'); + if (!laserDef) { + // Presenter uses hand tool and the default red pointer is visible for viewers + defaultPointer.style.setProperty('display', 'block'); + } else { + // Presenter uses laser pointer and the default red pointer is invisible for viewers + defaultPointer.style.setProperty('display', 'none'); + } + } + + if (!laserDef) return; + + if (!laserEl && layer) { + laserEl = createLaserElement(laserSvgs[laserKey], document, laserDef); + layer.appendChild(laserEl); + laserElRef.current = laserEl; + } + + // Place the laser SVG at where the invisible redPointer, + // whose position is not very precise, is located + const cursorEl = document.querySelector('.tl-collaborator__cursor'); + if (!cursorEl) return; + + const zoom = parseFloat(getComputedStyle(tlContainer).getPropertyValue('--tl-zoom')) || 1; + + const transform = cursorEl.style.transform; + if (!transform) return; + + const match = transform.match(/translate\(([^,]+)px,\s*([^)]+)px\)/); + if (!match) return; + + const x = parseFloat(match[1]); + const y = parseFloat(match[2]); + + // Keep cursor size regardless of the slide zoom or window size change, + // the same behaviour as the presenter's CSS-based laser pointer. + laserEl.style.transform = ` + translate(${x - laserDef.cx}px, ${y - laserDef.cy}px) + scale(${1/zoom}) + `; + return; + }, [otherCursors, isPresenter]); + const updateStore = (pages, cameras) => { tlEditorRef.current.store.put(pages); tlEditorRef.current.store.put(cameras); From 842c2fb66afc4776de649c2ffd523807323a075e Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:57:37 +0900 Subject: [PATCH 03/88] Update container.jsx --- .../imports/ui/components/whiteboard/container.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx index 719c1aa5ff1b..41809f9cbcc8 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx @@ -231,7 +231,7 @@ const WhiteboardContainer = (props) => { }; const publishCursorUpdate = useCallback((payload) => { - const { whiteboardId, xPercent, yPercent } = payload; + const { whiteboardId, xPercent, yPercent, laserType } = payload; if (!whiteboardId || !xPercent || !yPercent || !(hasWBAccess || isPresenter)) return; @@ -240,6 +240,7 @@ const WhiteboardContainer = (props) => { whiteboardId, xPercent, yPercent, + laserType, }, }); }, [hasWBAccess, isPresenter]); From a7346b3d4d3894b87ca385555ae5848e3771095b Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:58:37 +0900 Subject: [PATCH 04/88] Update hooks.js --- .../imports/ui/components/whiteboard/hooks.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/hooks.js b/bigbluebutton-html5/imports/ui/components/whiteboard/hooks.js index 08e7ea6cb6bb..5299dabe4616 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/hooks.js +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/hooks.js @@ -7,7 +7,7 @@ const hasBackgroundImageUrl = (el) => { return bg.includes('url('); }; -const useCursor = (publishCursorUpdate, whiteboardId) => { +const useCursor = (publishCursorUpdate, whiteboardId, laserMode) => { const [cursorPosition, setCursorPosition] = useState({ x: '', y: '' }); const updateCursorPosition = (newX, newY) => { @@ -22,8 +22,9 @@ const useCursor = (publishCursorUpdate, whiteboardId) => { whiteboardId, xPercent: cursorPosition?.x, yPercent: cursorPosition?.y, + laserType: laserMode, }); - }, [cursorPosition, publishCursorUpdate, whiteboardId]); + }, [cursorPosition, publishCursorUpdate, whiteboardId, laserMode]); return [cursorPosition, updateCursorPosition]; }; From d920770023eac4e9b6485e74202181c8ec81da4a Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:59:01 +0900 Subject: [PATCH 05/88] Update hooks.ts --- bigbluebutton-html5/imports/ui/components/whiteboard/hooks.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/hooks.ts b/bigbluebutton-html5/imports/ui/components/whiteboard/hooks.ts index b76ef2b6b16b..3ea898d72cb7 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/hooks.ts +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/hooks.ts @@ -86,6 +86,7 @@ export const useMergedCursorData = () => { return { ...coordinates, ...cursor, + laserType: coordinates.laserType, }; } From 836a0e10bffbfac45bc1a0848d217c6c81cc7391 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:59:34 +0900 Subject: [PATCH 06/88] Update queries.ts --- bigbluebutton-html5/imports/ui/components/whiteboard/queries.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/queries.ts b/bigbluebutton-html5/imports/ui/components/whiteboard/queries.ts index c9d0042bb5e8..777665ffc7a6 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/queries.ts +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/queries.ts @@ -220,6 +220,7 @@ export const CURRENT_PAGE_CURSORS_COORDINATES_STREAM = gql` batch_size: 100) { xPercent yPercent + laserType userId } } From 453e2e2f4c6b786c71a37eb66db53112d580e9d5 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:00:52 +0900 Subject: [PATCH 07/88] Update SendCursorPositionPubMsgHdlr.scala --- .../core/apps/whiteboard/SendCursorPositionPubMsgHdlr.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/SendCursorPositionPubMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/SendCursorPositionPubMsgHdlr.scala index 373b5926112c..69dfd8211e51 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/SendCursorPositionPubMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/SendCursorPositionPubMsgHdlr.scala @@ -17,7 +17,7 @@ trait SendCursorPositionPubMsgHdlr extends RightsManagementTrait { val envelope = BbbCoreEnvelope(SendCursorPositionEvtMsg.NAME, routing) val header = BbbClientMsgHeader(SendCursorPositionEvtMsg.NAME, liveMeeting.props.meetingProp.intId, msg.header.userId) - val body = SendCursorPositionEvtMsgBody(msg.body.whiteboardId, userIsViewer, msg.body.xPercent, msg.body.yPercent) + val body = SendCursorPositionEvtMsgBody(msg.body.whiteboardId, userIsViewer, msg.body.xPercent, msg.body.yPercent, msg.body.laserType) val event = SendCursorPositionEvtMsg(header, body) val msgEvent = BbbCommonEnvCoreMsg(envelope, event) bus.outGW.send(msgEvent) From 2ee2427644514b0a820a27d8e3115455e8b18b07 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:06:12 +0900 Subject: [PATCH 08/88] Update WhiteboardCursorMoveRecordEvent.scala --- .../record/events/WhiteboardCursorMoveRecordEvent.scala | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/WhiteboardCursorMoveRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/WhiteboardCursorMoveRecordEvent.scala index 1dc4dc7fe095..94bab50c88d6 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/WhiteboardCursorMoveRecordEvent.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/WhiteboardCursorMoveRecordEvent.scala @@ -35,10 +35,15 @@ class WhiteboardCursorMoveRecordEvent extends AbstractWhiteboardRecordEvent { def setYPercent(percent: Double) { eventMap.put(Y_OFFSET, percent.toString) } + + def setLaserType(laserType: String) { + eventMap.put(LASER_TYPE, laserType) + } } object WhiteboardCursorMoveRecordEvent { protected final val USER_ID = "userId" protected final val X_OFFSET = "xOffset" protected final val Y_OFFSET = "yOffset" -} \ No newline at end of file + protected final val LASER_TYPE = "laserType" +} From 594519c5da6d8a75c0ccbff414b70bedeaf94236 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:07:55 +0900 Subject: [PATCH 09/88] Update RedisRecorderActor.scala --- .../org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala index 6475fab18c07..75bde14e93d3 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala @@ -377,6 +377,7 @@ class RedisRecorderActor( ev.setUserId(msg.header.userId) ev.setXPercent(msg.body.xPercent) ev.setYPercent(msg.body.yPercent) + ev.setLaserType(msg.body.laserType) record(msg.header.meetingId, ev.toMap.asJava) } From 84476178e3f91bfd09c02867df759ab59d2ec649 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:09:08 +0900 Subject: [PATCH 10/88] Update bbb_schema.sql --- bbb-graphql-server/bbb_schema.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/bbb-graphql-server/bbb_schema.sql b/bbb-graphql-server/bbb_schema.sql index ab3a63632368..3518438d6390 100644 --- a/bbb-graphql-server/bbb_schema.sql +++ b/bbb-graphql-server/bbb_schema.sql @@ -1674,6 +1674,7 @@ CREATE UNLOGGED TABLE "pres_page_cursor" ( "userId" varchar(50), "xPercent" numeric, "yPercent" numeric, + "laserType" varchar(50), "lastUpdatedAt" timestamp with time zone DEFAULT now(), CONSTRAINT "pres_page_cursor_pkey" PRIMARY KEY ("pageId","meetingId","userId"), FOREIGN KEY ("meetingId", "userId") REFERENCES "user"("meetingId","userId") ON DELETE CASCADE From 6d0df13a743d4e535c8b0ed2d6f9b50cca5f6764 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:09:44 +0900 Subject: [PATCH 11/88] Update actions.graphql --- bbb-graphql-server/metadata/actions.graphql | 1 + 1 file changed, 1 insertion(+) diff --git a/bbb-graphql-server/metadata/actions.graphql b/bbb-graphql-server/metadata/actions.graphql index 8931dded6ace..f41cc7f707f5 100644 --- a/bbb-graphql-server/metadata/actions.graphql +++ b/bbb-graphql-server/metadata/actions.graphql @@ -452,6 +452,7 @@ type Mutation { whiteboardId: String! xPercent: Float! yPercent: Float! + laserType: String! ): Boolean } From ccbd4b6fb9e3fa22c4efea70e809b4328342ef5a Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:10:38 +0900 Subject: [PATCH 12/88] Update public_v_pres_page_cursor.yaml --- .../BigBlueButton/tables/public_v_pres_page_cursor.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_pres_page_cursor.yaml b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_pres_page_cursor.yaml index 2aabbf109416..e7ec39cef9e2 100644 --- a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_pres_page_cursor.yaml +++ b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_pres_page_cursor.yaml @@ -29,6 +29,7 @@ select_permissions: - userId - xPercent - yPercent + - laserType filter: _and: - meetingId: From f7b4d4622eda0aaf720a12f251c76e978903d66e Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:11:52 +0900 Subject: [PATCH 13/88] Update presPageCursorStream.go --- .../internal/streaming_server/presPageCursorStream.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bbb-graphql-middleware/internal/streaming_server/presPageCursorStream.go b/bbb-graphql-middleware/internal/streaming_server/presPageCursorStream.go index 9dc3aa94da1e..dc3c5c62e966 100644 --- a/bbb-graphql-middleware/internal/streaming_server/presPageCursorStream.go +++ b/bbb-graphql-middleware/internal/streaming_server/presPageCursorStream.go @@ -18,11 +18,13 @@ func HandleSendCursorPositionEvtMsg(receivedMessage common.RedisMessage, browser receivedCursorIsFromViewer := receivedMessage.Core.Body["userIsViewer"].(bool) xPercent := receivedMessage.Core.Body["xPercent"].(float64) yPercent := receivedMessage.Core.Body["yPercent"].(float64) + laserType := receivedMessage.Core.Body["laserType"].(string) item := map[string]any{ "xPercent": xPercent, "yPercent": yPercent, "userId": receivedMessage.Core.Header.UserId, + "laserType": laserType, "__typename": "pres_page_cursor", } From ef6b17fa6e634b27277acc1b904ed72d41a41bfa Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:12:53 +0900 Subject: [PATCH 14/88] Update presentationPublishCursor.ts --- bbb-graphql-actions/src/actions/presentationPublishCursor.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bbb-graphql-actions/src/actions/presentationPublishCursor.ts b/bbb-graphql-actions/src/actions/presentationPublishCursor.ts index e8a07cf5223b..1c468717c30e 100644 --- a/bbb-graphql-actions/src/actions/presentationPublishCursor.ts +++ b/bbb-graphql-actions/src/actions/presentationPublishCursor.ts @@ -7,6 +7,7 @@ export default function buildRedisMessage(sessionVariables: Record Date: Fri, 27 Mar 2026 18:40:18 +0900 Subject: [PATCH 15/88] Update PresPageCursorDAO.scala --- .../org/bigbluebutton/core/db/PresPageCursorDAO.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PresPageCursorDAO.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PresPageCursorDAO.scala index 2a6a19a03397..43fbf990d386 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PresPageCursorDAO.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PresPageCursorDAO.scala @@ -8,24 +8,26 @@ case class PresPageCursorDbModel( userId: String, xPercent: Double, yPercent: Double, + laserType: String, lastUpdatedAt: java.sql.Timestamp = new java.sql.Timestamp(System.currentTimeMillis()) ) class PresPageCursorDbTableDef(tag: Tag) extends Table[PresPageCursorDbModel](tag, None, "pres_page_cursor") { override def * = ( - pageId, meetingId, userId, xPercent, yPercent, lastUpdatedAt + pageId, meetingId, userId, xPercent, yPercent, laserType, lastUpdatedAt ) <> (PresPageCursorDbModel.tupled, PresPageCursorDbModel.unapply) val pageId = column[String]("pageId", O.PrimaryKey) val meetingId = column[String]("meetingId", O.PrimaryKey) val userId = column[String]("userId", O.PrimaryKey) val xPercent = column[Double]("xPercent") val yPercent = column[Double]("yPercent") + val laserType = column[String]("laserType") val lastUpdatedAt = column[java.sql.Timestamp]("lastUpdatedAt") } object PresPageCursorDAO { - def insertOrUpdate(pageId: String, meetingId: String, userId: String, xPercent: Double, yPercent: Double) = { + def insertOrUpdate(pageId: String, meetingId: String, userId: String, xPercent: Double, yPercent: Double, laserType: String) = { DatabaseConnection.enqueue( TableQuery[PresPageCursorDbTableDef].insertOrUpdate( PresPageCursorDbModel( @@ -34,6 +36,7 @@ object PresPageCursorDAO { userId = userId, xPercent = xPercent, yPercent = yPercent, + laserType = laserType, lastUpdatedAt = new java.sql.Timestamp(System.currentTimeMillis()), ) ) From 83e85a870c4255c95952028a704e58f431830406 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:50:27 +0900 Subject: [PATCH 16/88] Remove popupWindow! To adopt this PR to popup again, you need: - to change -> "const targetDoc = popupWindow?.document || document;" (at two places whare "const targetDoc = document"; is writted) - to add-> "popupWindow" at the ends of two React.useEffect()s which defines "const targetDoc" (same functions above) --- .../imports/ui/components/whiteboard/component.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 2cbf56032a1e..e379efbd62ab 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2116,7 +2116,7 @@ const Whiteboard = React.memo((props) => { targetDoc.removeEventListener('contextmenu', handleContextMenu); removeMenu(); }; - }, [popupWindow, isPresenter]); + }, [isPresenter]); React.useEffect(() => { const targetDoc = document; @@ -2154,7 +2154,7 @@ const Whiteboard = React.memo((props) => { }); return () => observer.disconnect() - }, [laserMode, isPresenter, popupWindow]); + }, [laserMode, isPresenter]); React.useEffect(() => { if (tlEditorRef.current) { From a7de71a5521fbd240cf01e3500ddc3821de54bd6 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:12:58 +0900 Subject: [PATCH 17/88] Remove unnecessary mutation observer --- .../ui/components/whiteboard/component.jsx | 44 +++++-------------- 1 file changed, 11 insertions(+), 33 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index e379efbd62ab..9943a7705847 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2121,39 +2121,17 @@ const Whiteboard = React.memo((props) => { React.useEffect(() => { const targetDoc = document; if (!isPresenter) return; - - const applyLaser = () => { - const el = targetDoc.querySelector('.tl-container'); - if (!el) return false; - - const laser = cursorLasers[laserMode]; - - if (laser) { - el.style.setProperty('--tl-cursor-grab', laser); - el.style.setProperty('--tl-cursor-grabbing', laser); - } else { - el.style.removeProperty('--tl-cursor-grab'); - el.style.removeProperty('--tl-cursor-grabbing'); - } - - return true; - }; - - // don't obeserve if already done - if (applyLaser()) return; - - const observer = new MutationObserver(() => { - if (applyLaser()) { - observer.disconnect(); - } - }); - - observer.observe(targetDoc.body, { - childList: true, - subtree: true, - }); - - return () => observer.disconnect() + const el = targetDoc.querySelector('.tl-container'); + if (!el) return; + + const laser = cursorLasers[laserMode]; + if (laser) { + el.style.setProperty('--tl-cursor-grab', laser); + el.style.setProperty('--tl-cursor-grabbing', laser); + } else { + el.style.removeProperty('--tl-cursor-grab'); + el.style.removeProperty('--tl-cursor-grabbing'); + } }, [laserMode, isPresenter]); React.useEffect(() => { From 3e0ec1ef21e3eb2d10dba359d92b5cb0b3bb3d82 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Fri, 27 Mar 2026 23:20:54 +0900 Subject: [PATCH 18/88] fix missing argv --- .../imports/ui/components/presentation/mutations.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx b/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx index aae509f6375e..bf0b83cd2c3c 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx @@ -92,7 +92,7 @@ export const PRES_ANNOTATION_SUBMIT = gql` `; export const PRESENTATION_PUBLISH_CURSOR = gql` - mutation PresentationPublishCursor($whiteboardId: String!, $xPercent: Float!, $yPercent: Float!) { + mutation PresentationPublishCursor($whiteboardId: String!, $xPercent: Float!, $yPercent: Float!, $laserType: String!) { presentationPublishCursor( whiteboardId: $whiteboardId, xPercent: $xPercent, From c8bd843066bc3a6e4a202edf1a83522e2bf02c92 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Fri, 27 Mar 2026 23:23:24 +0900 Subject: [PATCH 19/88] Update component.jsx --- .../imports/ui/components/whiteboard/component.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 9943a7705847..55fa1a8215e0 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2060,7 +2060,7 @@ const Whiteboard = React.memo((props) => { { key: 'greenSmall', label: '🟢', size: 10 }, { key: 'redLarge', label: '🔴', size: 18 }, { key: 'greenLarge', label: '🟢', size: 18 }, - { key: '', label: '✋', size: 14 }, + { key: '', label: '✋', size: 14 }, ]; items.forEach(({ key, label, size }) => { From 04ff5ee4fceac0340bf0fa266bd1883928a3072e Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Fri, 27 Mar 2026 23:28:56 +0900 Subject: [PATCH 20/88] comment --- .../imports/ui/components/whiteboard/component.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 55fa1a8215e0..134a65908ebd 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2245,7 +2245,7 @@ const Whiteboard = React.memo((props) => { } } - if (!laserDef) return; + if (!laserDef) return; // hand tool, so laser pointer is drawn if (!laserEl && layer) { laserEl = createLaserElement(laserSvgs[laserKey], document, laserDef); From 248663f8e1e41af9839314cafc21a3bae99ef59b Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Fri, 27 Mar 2026 23:31:48 +0900 Subject: [PATCH 21/88] add missing parameter --- bigbluebutton-html5/imports/ui/components/whiteboard/queries.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/queries.ts b/bigbluebutton-html5/imports/ui/components/whiteboard/queries.ts index 777665ffc7a6..8593a07aa084 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/queries.ts +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/queries.ts @@ -4,6 +4,7 @@ import { gql } from '@apollo/client'; export interface CursorCoordinates { xPercent: number; yPercent: number; + laserType: string; userId: string; } From 4dcefaefcba1dce33121960c223ab4aeda465cff Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sat, 28 Mar 2026 00:06:53 +0900 Subject: [PATCH 22/88] Update WhiteboardMsgs.scala --- .../scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala index 107c04640207..0d75e73adead 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala @@ -40,7 +40,7 @@ case class GetWhiteboardAnnotationsReqMsgBody(whiteboardId: String) object SendCursorPositionPubMsg { val NAME = "SendCursorPositionPubMsg" } case class SendCursorPositionPubMsg(header: BbbClientMsgHeader, body: SendCursorPositionPubMsgBody) extends StandardMsg -case class SendCursorPositionPubMsgBody(whiteboardId: String, xPercent: Double, yPercent: Double) +case class SendCursorPositionPubMsgBody(whiteboardId: String, xPercent: Double, yPercent: Double, laserType: String) object SendWhiteboardAnnotationsPubMsg { val NAME = "SendWhiteboardAnnotationsPubMsg" } case class SendWhiteboardAnnotationsPubMsg(header: BbbClientMsgHeader, body: SendWhiteboardAnnotationsPubMsgBody) extends StandardMsg @@ -70,7 +70,7 @@ case class GetWhiteboardAnnotationsRespMsgBody(whiteboardId: String, annotations object SendCursorPositionEvtMsg { val NAME = "SendCursorPositionEvtMsg" } case class SendCursorPositionEvtMsg(header: BbbClientMsgHeader, body: SendCursorPositionEvtMsgBody) extends BbbCoreMsg -case class SendCursorPositionEvtMsgBody(whiteboardId: String, userIsViewer: Boolean, xPercent: Double, yPercent: Double) +case class SendCursorPositionEvtMsgBody(whiteboardId: String, userIsViewer: Boolean, xPercent: Double, yPercent: Double, laserType: String) object SendWhiteboardAnnotationsEvtMsg { val NAME = "SendWhiteboardAnnotationsEvtMsg" } case class SendWhiteboardAnnotationsEvtMsg(header: BbbClientMsgHeader, body: SendWhiteboardAnnotationsEvtMsgBody) extends BbbCoreMsg From 073cb28243b2a6f51c907d15569dd3b3b32102f4 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sat, 28 Mar 2026 09:14:00 +0900 Subject: [PATCH 23/88] Update settings.yml --- bigbluebutton-html5/private/config/settings.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml index bd1df7ff3235..e0bd8877ab31 100755 --- a/bigbluebutton-html5/private/config/settings.yml +++ b/bigbluebutton-html5/private/config/settings.yml @@ -1073,6 +1073,8 @@ public: allowInfiniteWhiteboard: false allowInfiniteWhiteboardInBreakouts: false pointerDiameter: 5 + laserRadiusSmall: 10 + laserRadiusLarge: 16 maxStickyNoteLength: 1000 # limit number of annotations per slide maxNumberOfAnnotations: 300 From 1f6bed549eab1af92c8669078d89fde882fa05e4 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sat, 28 Mar 2026 09:15:57 +0900 Subject: [PATCH 24/88] Update meetingClientSettings.ts --- bigbluebutton-html5/imports/ui/Types/meetingClientSettings.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bigbluebutton-html5/imports/ui/Types/meetingClientSettings.ts b/bigbluebutton-html5/imports/ui/Types/meetingClientSettings.ts index 8d46d017cf2e..b596a416252a 100644 --- a/bigbluebutton-html5/imports/ui/Types/meetingClientSettings.ts +++ b/bigbluebutton-html5/imports/ui/Types/meetingClientSettings.ts @@ -751,6 +751,8 @@ export interface Whiteboard { annotationsQueueProcessInterval: number cursorInterval: number pointerDiameter: number + laserRadiusSmall: number + laserRadiusLarge: number maxStickyNoteLength: number maxNumberOfAnnotations: number maxNumberOfActiveUsers: number From e4d347fe4f5cb04772ac2f545ffd1c8cd8fa55f6 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sat, 28 Mar 2026 09:16:59 +0900 Subject: [PATCH 25/88] Update meetingClientSettings.ts --- .../imports/ui/core/initial-values/meetingClientSettings.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bigbluebutton-html5/imports/ui/core/initial-values/meetingClientSettings.ts b/bigbluebutton-html5/imports/ui/core/initial-values/meetingClientSettings.ts index b2751541c232..8e7c144f2b35 100644 --- a/bigbluebutton-html5/imports/ui/core/initial-values/meetingClientSettings.ts +++ b/bigbluebutton-html5/imports/ui/core/initial-values/meetingClientSettings.ts @@ -827,6 +827,8 @@ export const meetingClientSettingsInitialValues: MeetingClientSettings = { annotationsQueueProcessInterval: 60, cursorInterval: 150, pointerDiameter: 5, + laserRadiusSmall: 10, + laserRadiusLarge: 16, maxStickyNoteLength: 1000, maxNumberOfAnnotations: 300, maxNumberOfActiveUsers: 25, From 900a3ed31bd57e3fa95cbca500e0013f5ec706b0 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sat, 28 Mar 2026 09:20:28 +0900 Subject: [PATCH 26/88] Update component.jsx --- .../imports/ui/components/whiteboard/component.jsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 134a65908ebd..c14048e1aa9d 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -170,6 +170,8 @@ const Whiteboard = React.memo((props) => { setEditor, lockToolbarTools, layoutChanged, + laserRadiusSmall, + laserRadiusLarge, } = props; clearTldrawCache(); @@ -1787,10 +1789,10 @@ const Whiteboard = React.memo((props) => { }; const laserDefs = { - redSmall: { color: 'red', cx: 16, cy: 16, r: 14 }, - greenSmall: { color: 'lime', cx: 16, cy: 16, r: 14 }, - redLarge: { color: 'red', cx: 24, cy: 24, r: 22 }, - greenLarge: { color: 'lime', cx: 24, cy: 24, r: 22 }, + redSmall: { color: 'red', cx: laserRadiusSmall+2, cy: laserRadiusSmall+2, r: laserRadiusSmall }, + greenSmall: { color: 'lime', cx: laserRadiusSmall+2, cy: laserRadiusSmall+2, r: laserRadiusSmall }, + redLarge: { color: 'red', cx: laserRadiusLarge+2, cy: laserRadiusLarge+2, r: laserRadiusLarge }, + greenLarge: { color: 'lime', cx: laserRadiusLarge+2, cy: laserRadiusLarge+2, r: laserRadiusLarge }, }; const laserSvgs = Object.fromEntries( @@ -2464,6 +2466,8 @@ Whiteboard.propTypes = { presentationAreaHeight: PropTypes.number.isRequired, presentationAreaWidth: PropTypes.number.isRequired, maxNumberOfAnnotations: PropTypes.number.isRequired, + laserRadiusSmall: PropTypes.number.isRequired, + laserRadiusLarge: PropTypes.number.isRequired, setTldrawIsMounting: PropTypes.func.isRequired, presentationId: PropTypes.string, setTldrawAPI: PropTypes.func.isRequired, From feb343fd0b7e163584e849b3561edc9ee348dad6 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sat, 28 Mar 2026 09:21:59 +0900 Subject: [PATCH 27/88] Update container.jsx --- .../imports/ui/components/whiteboard/container.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx index 41809f9cbcc8..3f32cbb9dccb 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx @@ -461,7 +461,7 @@ const WhiteboardContainer = (props) => { const sidebarNavigationWidth = layoutSelect( (i) => i?.output?.sidebarNavigation?.width, ); - const { maxStickyNoteLength, maxNumberOfAnnotations, lockToolbarTools } = WHITEBOARD_CONFIG; + const { maxStickyNoteLength, maxNumberOfAnnotations, lockToolbarTools, laserRadiusSmall, laserRadiusLarge } = WHITEBOARD_CONFIG; const fontFamily = WHITEBOARD_CONFIG.styles.text.family; const { colorStyle, dashStyle, fillStyle, fontStyle, sizeStyle, @@ -507,6 +507,8 @@ const WhiteboardContainer = (props) => { maxStickyNoteLength, maxNumberOfAnnotations, lockToolbarTools, + laserRadiusSmall, + laserRadiusLarge, fontFamily, colorStyle, dashStyle, From f4cb38e0dc87743c108d13e8839f33d64437e1e1 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sun, 29 Mar 2026 10:47:17 +0900 Subject: [PATCH 28/88] Use getCamera to get the zoom value --- .../imports/ui/components/whiteboard/component.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index c14048e1aa9d..ba4a2f9097c1 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2260,7 +2260,8 @@ const Whiteboard = React.memo((props) => { const cursorEl = document.querySelector('.tl-collaborator__cursor'); if (!cursorEl) return; - const zoom = parseFloat(getComputedStyle(tlContainer).getPropertyValue('--tl-zoom')) || 1; + //const zoom = parseFloat(getComputedStyle(tlContainer).getPropertyValue('--tl-zoom')) || 1; + const { z: zoom } = tlEditorRef.current.getCamera(); const transform = cursorEl.style.transform; if (!transform) return; From 962039a492abd02cd51302d16487e7c9b388cf4a Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sun, 29 Mar 2026 10:59:15 +0900 Subject: [PATCH 29/88] Use xPercent and yPercent for viewer's cursor position More simple implementation, although the values are slightly different from the values taken from the 'translate' value of the finally rendered HTML. --- .../ui/components/whiteboard/component.jsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index ba4a2f9097c1..d29c2dbf4c3c 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2263,14 +2263,14 @@ const Whiteboard = React.memo((props) => { //const zoom = parseFloat(getComputedStyle(tlContainer).getPropertyValue('--tl-zoom')) || 1; const { z: zoom } = tlEditorRef.current.getCamera(); - const transform = cursorEl.style.transform; - if (!transform) return; - - const match = transform.match(/translate\(([^,]+)px,\s*([^)]+)px\)/); - if (!match) return; - - const x = parseFloat(match[1]); - const y = parseFloat(match[2]); + //const transform = cursorEl.style.transform; + //if (!transform) return; + //const match = transform.match(/translate\(([^,]+)px,\s*([^)]+)px\)/); + //if (!match) return; + //const x = parseFloat(match[1]); + //const y = parseFloat(match[2]); + const x = presenterCursor.xPercent; + const y = presenterCursor.yPercent; // Keep cursor size regardless of the slide zoom or window size change, // the same behaviour as the presenter's CSS-based laser pointer. From ba0995015079ed27cf76134b7cb62945276df305 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sun, 29 Mar 2026 11:01:11 +0900 Subject: [PATCH 30/88] update comment --- .../imports/ui/components/whiteboard/component.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index d29c2dbf4c3c..41689128b390 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2273,7 +2273,7 @@ const Whiteboard = React.memo((props) => { const y = presenterCursor.yPercent; // Keep cursor size regardless of the slide zoom or window size change, - // the same behaviour as the presenter's CSS-based laser pointer. + // the same behaviour as the presenter's CSS-based laser pointer and a real laser. laserEl.style.transform = ` translate(${x - laserDef.cx}px, ${y - laserDef.cy}px) scale(${1/zoom}) From 875f6df6a6f3eff4d40538dfbe791b2acc8f2c87 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Tue, 31 Mar 2026 22:56:51 +0900 Subject: [PATCH 31/88] adjust laser color --- .../imports/ui/components/whiteboard/component.jsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 41689128b390..5e8af711994d 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -1788,11 +1788,16 @@ const Whiteboard = React.memo((props) => { `.replace(/\s+/g, ' ').trim(); }; + const c = { + red: 'rgba(255, 20, 20, 0.9)', + green: 'rgba(20, 255, 20, 0.9)', + }; + const laserDefs = { - redSmall: { color: 'red', cx: laserRadiusSmall+2, cy: laserRadiusSmall+2, r: laserRadiusSmall }, - greenSmall: { color: 'lime', cx: laserRadiusSmall+2, cy: laserRadiusSmall+2, r: laserRadiusSmall }, - redLarge: { color: 'red', cx: laserRadiusLarge+2, cy: laserRadiusLarge+2, r: laserRadiusLarge }, - greenLarge: { color: 'lime', cx: laserRadiusLarge+2, cy: laserRadiusLarge+2, r: laserRadiusLarge }, + redSmall: { color: c.red, cx: laserRadiusSmall+2, cy: laserRadiusSmall+2, r: laserRadiusSmall }, + greenSmall: { color: c.green, cx: laserRadiusSmall+2, cy: laserRadiusSmall+2, r: laserRadiusSmall }, + redLarge: { color: c.red, cx: laserRadiusLarge+2, cy: laserRadiusLarge+2, r: laserRadiusLarge }, + greenLarge: { color: c.green, cx: laserRadiusLarge+2, cy: laserRadiusLarge+2, r: laserRadiusLarge }, }; const laserSvgs = Object.fromEntries( From 3026fcb584dfbedcb7e9908f61e0fb7e4bc09d7f Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Tue, 31 Mar 2026 23:08:12 +0900 Subject: [PATCH 32/88] opacity adjustment --- .../imports/ui/components/whiteboard/component.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 5e8af711994d..3cf9ed6fcb23 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -1779,8 +1779,8 @@ const Whiteboard = React.memo((props) => { - - + + @@ -1789,8 +1789,8 @@ const Whiteboard = React.memo((props) => { }; const c = { - red: 'rgba(255, 20, 20, 0.9)', - green: 'rgba(20, 255, 20, 0.9)', + red: 'rgba(255, 20, 20)', + green: 'rgba(20, 255, 20)', }; const laserDefs = { From f85db085c5d07373b9597b63c062dd50705ea727 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Wed, 1 Apr 2026 11:46:02 +0900 Subject: [PATCH 33/88] Pseudo-non-linear gradient --- .../imports/ui/components/whiteboard/component.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 3cf9ed6fcb23..1fe3d725c9cc 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -1780,6 +1780,7 @@ const Whiteboard = React.memo((props) => { + From 0fa212b9c7cb630ac84e75d6ff7d110f0984f567 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sat, 4 Apr 2026 13:19:24 +0900 Subject: [PATCH 34/88] Fix: add useState for laserMode This line was missing by mistake. --- .../imports/ui/components/whiteboard/component.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 1fe3d725c9cc..2701ad3f46e3 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -178,6 +178,7 @@ const Whiteboard = React.memo((props) => { const [isMounting, setIsMounting] = React.useState(true); const [cursorType, setCursorType] = React.useState(''); + const [laserMode, setLaserMode] = React.useState(''); if (isMounting) { setDefaultEditorAssetUrls(getCustomEditorAssetUrls()); From 7777337ed5aae364a70bc6c910cb1cd4aa953712 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sat, 4 Apr 2026 14:22:32 +0900 Subject: [PATCH 35/88] Show viewer's laser also when multiuser mode is active By showing it, - the appearance of viewer's cursor is consistent with the presenter's - the problem that the laser remains at the presentation edge when presenter moves the cursor out of the presentation container is solved --- .../imports/ui/components/whiteboard/component.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 2701ad3f46e3..495b163e5b51 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2222,7 +2222,7 @@ const Whiteboard = React.memo((props) => { // Show viewers Laser SVG React.useEffect(() => { if (isPresenter) return; - if (isMultiUserActive) return; + //if (isMultiUserActive) return; const tlContainer = document.querySelector('.tl-container'); From ba543ebd880aa29b3cdd92c9b5e6fab9848d681d Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sat, 4 Apr 2026 14:36:30 +0900 Subject: [PATCH 36/88] Fix: don't refresh laser when its position is (-1, -1) When the cursor crosses the upper or left border of the presentation, BBB place the cursor at (-1, -1). This behaviour makes the laser 'jump' to the upper-left position. --- .../imports/ui/components/whiteboard/component.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 495b163e5b51..d90b0f49d226 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2278,6 +2278,7 @@ const Whiteboard = React.memo((props) => { //const y = parseFloat(match[2]); const x = presenterCursor.xPercent; const y = presenterCursor.yPercent; + if (x == -1 || y == -1) return; // Keep cursor size regardless of the slide zoom or window size change, // the same behaviour as the presenter's CSS-based laser pointer and a real laser. From c25b27f15bdd17610d46c50d910fba0b2609a4a6 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sat, 4 Apr 2026 14:42:10 +0900 Subject: [PATCH 37/88] chore: == -> === --- .../imports/ui/components/whiteboard/component.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index d90b0f49d226..883fb67391d5 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2278,7 +2278,7 @@ const Whiteboard = React.memo((props) => { //const y = parseFloat(match[2]); const x = presenterCursor.xPercent; const y = presenterCursor.yPercent; - if (x == -1 || y == -1) return; + if (x === -1 || y === -1) return; // Keep cursor size regardless of the slide zoom or window size change, // the same behaviour as the presenter's CSS-based laser pointer and a real laser. From 5a156ba9bcde1c07827931259b5c3471385fce2e Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sat, 4 Apr 2026 17:51:39 +0900 Subject: [PATCH 38/88] Remove viewer's laser at page change & cursor placed outside With this change, the viewer's laser will be created every time as the page changes. Is there any better implementation??? --- .../ui/components/whiteboard/component.jsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 883fb67391d5..c14521996835 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -1777,7 +1777,7 @@ const Whiteboard = React.memo((props) => { // On Windows and Linux, it darkens towards the edge return ` - + @@ -2219,6 +2219,12 @@ const Whiteboard = React.memo((props) => { } }, [otherCursors, whiteboardWriters]); + const removeViewerLaser = () => { + laserElRef.current = null; + const lasers = document.querySelectorAll('.bbb-laser-pointer'); + lasers.forEach(el => el.remove()); + }; + // Show viewers Laser SVG React.useEffect(() => { if (isPresenter) return; @@ -2278,7 +2284,10 @@ const Whiteboard = React.memo((props) => { //const y = parseFloat(match[2]); const x = presenterCursor.xPercent; const y = presenterCursor.yPercent; - if (x === -1 || y === -1) return; + if (x === -1 || y === -1) { + removeViewerLaser(); + return; + } // Keep cursor size regardless of the slide zoom or window size change, // the same behaviour as the presenter's CSS-based laser pointer and a real laser. @@ -2289,6 +2298,10 @@ const Whiteboard = React.memo((props) => { return; }, [otherCursors, isPresenter]); + React.useEffect(() => { + removeViewerLaser(); + }, [curPageId]); + const updateStore = (pages, cameras) => { tlEditorRef.current.store.put(pages); tlEditorRef.current.store.put(cameras); From 33ed02460527db84d89a94a9c78dcf528efeaea2 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sun, 5 Apr 2026 09:12:22 +0900 Subject: [PATCH 39/88] add touchstart for mobile devices --- .../imports/ui/components/whiteboard/component.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index c14521996835..0c5fee66ca8c 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2039,6 +2039,7 @@ const Whiteboard = React.memo((props) => { menuEl.remove(); menuEl = null; targetDoc.removeEventListener('mousedown', handleOutsideClick); + targetDoc.removeEventListener('touchstart', handleOutsideClick); } }; @@ -2109,6 +2110,7 @@ const Whiteboard = React.memo((props) => { } targetDoc.addEventListener('mousedown', handleOutsideClick); + targetDoc.addEventListener('touchstart', handleOutsideClick); }; const handleContextMenu = (e) => { From 25bf5b7bb96ecd40cdcbe32f2484e18b1986f05b Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sun, 5 Apr 2026 20:14:21 +0900 Subject: [PATCH 40/88] For Android From 22cb3a12676aa124c06a753f9a7803ea4be024cd Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sun, 26 Apr 2026 20:08:00 +0900 Subject: [PATCH 41/88] Update comment --- .../imports/ui/components/whiteboard/component.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index e53cb70817b5..f865eeec14e5 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2421,8 +2421,7 @@ const Whiteboard = React.memo((props) => { laserElRef.current = laserEl; } - // Place the laser SVG at where the invisible redPointer, - // whose position is not very precise, is located + // Place the laser SVG at the position of redPointer, which is invisible. const cursorEl = document.querySelector('.tl-collaborator__cursor'); if (!cursorEl) return; From 5ee36eb8983822c5671db11cae17564f9ac57e48 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sun, 26 Apr 2026 20:11:56 +0900 Subject: [PATCH 42/88] update comment --- .../imports/ui/components/whiteboard/component.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index f865eeec14e5..1f1e37218c0d 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2442,7 +2442,7 @@ const Whiteboard = React.memo((props) => { } // Keep cursor size regardless of the slide zoom or window size change, - // the same behaviour as the presenter's CSS-based laser pointer and a real laser. + // similar to the pointer of the presenter (CSS-based) or the one in the real world. laserEl.style.transform = ` translate(${x - laserDef.cx}px, ${y - laserDef.cy}px) scale(${1/zoom}) From 7f05454be1faa075e24224b626167a875262d29c Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sun, 26 Apr 2026 23:24:02 +0900 Subject: [PATCH 43/88] Refactor menu - 1 Draw menu under the React architecture, not by directly drawing DOM. --- .../ui/components/whiteboard/component.jsx | 163 ++++++++---------- 1 file changed, 76 insertions(+), 87 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 1f1e37218c0d..2d57e70de065 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -181,6 +181,9 @@ const Whiteboard = React.memo((props) => { const [cursorType, setCursorType] = React.useState(''); const [cursorZoom, setCursorZoom] = React.useState({ slideZoom: 1, containerZoom: 1 }); const updateCursorZoomRef = React.useRef(null); + const [laserMenuVisible, setLaserMenuVisible] = React.useState(false); + const [laserMenuPos, setLaserMenuPos] = React.useState({ x: 0, y: 0 }); + const laserMenuRef = React.useRef(null); const [laserMode, setLaserMode] = React.useState(''); if (isMounting) { @@ -228,6 +231,14 @@ const Whiteboard = React.memo((props) => { currentUserRef.current = currentUser; + const laserItems = [ + { key: 'redSmall', label: '🔴', size: 10 }, + { key: 'greenSmall', label: '🟢', size: 10 }, + { key: 'redLarge', label: '🔴', size: 18 }, + { key: 'greenLarge', label: '🟢', size: 18 }, + { key: '', label: '✋', size: 14 }, + ]; + const [pageZoomMap, setPageZoomMap] = useState(() => { try { const saved = localStorage.getItem('pageZoomMap'); @@ -2177,108 +2188,65 @@ const Whiteboard = React.memo((props) => { if (!presentationWrapper) return; if (!isPresenter) return; - let menuEl = null; + const handleContextMenu = (e) => { + const tool = tlEditorRef.current?.getCurrentToolId?.(); + if (tool !== 'hand') return; + if (!presentationWrapper.contains(e.target)) return; - const handleOutsideClick = (e) => { - if (!menuEl) return; - if (menuEl.contains(e.target)) return; - removeMenu(); - }; + e.preventDefault(); - const removeMenu = () => { - if (menuEl) { - menuEl.remove(); - menuEl = null; - targetDoc.removeEventListener('mousedown', handleOutsideClick); - targetDoc.removeEventListener('touchstart', handleOutsideClick); - } + setLaserMenuPos({ x: e.clientX, y: e.clientY }); + setLaserMenuVisible(true); }; - const createMenu = (x, y) => { - removeMenu(); - - const container = - targetDoc.fullscreenElement || - targetDoc.querySelector('#presentationInnerWrapper') || - targetDoc.body; - - menuEl = targetDoc.createElement('div'); - - Object.assign(menuEl.style, { - position: 'fixed', - left: `${x}px`, - top: `${y}px`, - background: '#1e1e1e', - color: '#fff', - padding: '6px', - borderRadius: '8px', - boxShadow: '0 4px 12px rgba(0,0,0,0.3)', - zIndex: 99999, - }); + targetDoc.addEventListener('contextmenu', handleContextMenu); - const items = [ - { key: 'redSmall', label: '🔴', size: 10 }, - { key: 'greenSmall', label: '🟢', size: 10 }, - { key: 'redLarge', label: '🔴', size: 18 }, - { key: 'greenLarge', label: '🟢', size: 18 }, - { key: '', label: '✋', size: 14 }, - ]; - - items.forEach(({ key, label, size }) => { - const item = targetDoc.createElement('div'); - item.innerHTML = `${label}`; - - Object.assign(item.style, { - padding: '6px 10px', - cursor: 'pointer', - borderRadius: '4px', - textAlign: 'center', - }); + return () => { + targetDoc.removeEventListener('contextmenu', handleContextMenu); + }; + }, [isPresenter]); - item.onmouseenter = () => (item.style.background = '#333'); - item.onmouseleave = () => (item.style.background = 'transparent'); + React.useEffect(() => { + if (!laserMenuVisible) return; - item.onclick = () => { - setLaserMode(key); - removeMenu(); - }; + const targetDoc = document; - menuEl.appendChild(item); - }); + const handleOutsideClick = (e) => { + if (laserMenuRef.current?.contains(e.target)) return; + setLaserMenuVisible(false); + }; - container.appendChild(menuEl); + targetDoc.addEventListener('mousedown', handleOutsideClick); + targetDoc.addEventListener('touchstart', handleOutsideClick); - // compensation at the window edge - const rect = menuEl.getBoundingClientRect(); - const vw = targetDoc.defaultView.innerWidth; - const vh = targetDoc.defaultView.innerHeight; + return () => { + targetDoc.removeEventListener('mousedown', handleOutsideClick); + targetDoc.removeEventListener('touchstart', handleOutsideClick); + }; + }, [laserMenuVisible]); - if (rect.right > vw) { - menuEl.style.left = `${vw - rect.width - 8}px`; - } - if (rect.bottom > vh) { - menuEl.style.top = `${vh - rect.height - 50}px`; - } + React.useEffect(() => { + // compensation at the window edge + if (!laserMenuVisible) return; + const targetDoc = document; - targetDoc.addEventListener('mousedown', handleOutsideClick); - targetDoc.addEventListener('touchstart', handleOutsideClick); - }; + const el = laserMenuRef.current; + if (!el) return; - const handleContextMenu = (e) => { - const tool = tlEditorRef.current?.getCurrentToolId?.(); - if (tool !== 'hand') return; - if (!presentationWrapper.contains(e.target)) return; - e.preventDefault(); - createMenu(e.clientX, e.clientY); - }; + const rect = el.getBoundingClientRect(); + const vw = targetDoc.defaultView.innerWidth; + const vh = targetDoc.defaultView.innerHeight; - targetDoc.addEventListener('contextmenu', handleContextMenu); + let x = laserMenuPos.x; + let y = laserMenuPos.y; - return () => { - targetDoc.removeEventListener('contextmenu', handleContextMenu); - removeMenu(); - }; - }, [isPresenter]); + if (rect.right > vw) x = vw - rect.width - 8; + if (rect.bottom > vh) y = vh - rect.height - 50; + + if (x !== laserMenuPos.x || y !== laserMenuPos.y) { + setLaserMenuPos({ x, y }); + } + }, [laserMenuVisible]); React.useEffect(() => { const targetDoc = document; @@ -2632,6 +2600,27 @@ const Whiteboard = React.memo((props) => { hiddenGeoShapes, }} /> + {laserMenuVisible && ( + + {laserItems.map(({ key, label, size }) => ( + { + setLaserMode(key); + setLaserMenuVisible(false); + }} + > + {label} + + ))} + + )} ); }); From 59a588c9db1a4700b029b73534571db0a1c1cf5d Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sun, 26 Apr 2026 23:26:39 +0900 Subject: [PATCH 44/88] Refactor menu - 2 Draw menu under the React architecture, not by directly drawing DOM. Here, moving styles to styles.js --- .../ui/components/whiteboard/styles.js | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/styles.js b/bigbluebutton-html5/imports/ui/components/whiteboard/styles.js index 88e064c4b8ed..14d013b3014a 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/styles.js +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/styles.js @@ -264,7 +264,31 @@ const EditableWBWrapper = styled.div` } `; +const LaserContextMenu = styled.div` + position: fixed !important; + height: auto !important; + background: #1e1e1e; + color: #fff; + padding: 6px; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + z-index: 99999; +`; + +const LaserMenuItem = styled.div` + padding: 6px 10px; + cursor: pointer; + border-radius: 4px; + text-align: center; + + &:hover { + background: #333; + } +`; + export default { TldrawV2GlobalStyle, EditableWBWrapper, + LaserContextMenu, + LaserMenuItem, }; From 741d0b791e7467451383ec66a18973326abd929d Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sun, 26 Apr 2026 23:35:19 +0900 Subject: [PATCH 45/88] remove RF --- .../imports/ui/components/whiteboard/component.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 2d57e70de065..e2a0ce6fd4cd 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2208,7 +2208,6 @@ const Whiteboard = React.memo((props) => { React.useEffect(() => { if (!laserMenuVisible) return; - const targetDoc = document; const handleOutsideClick = (e) => { From eb7eba0b711a9c65f03d733548cf8ab6ce2079d1 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Thu, 30 Apr 2026 18:39:32 +0900 Subject: [PATCH 46/88] Remove unnecessary cursorEl cursorEl is not necessary more. --- .../imports/ui/components/whiteboard/component.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index e2a0ce6fd4cd..6f27c1ceaec3 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2389,8 +2389,8 @@ const Whiteboard = React.memo((props) => { } // Place the laser SVG at the position of redPointer, which is invisible. - const cursorEl = document.querySelector('.tl-collaborator__cursor'); - if (!cursorEl) return; + //const cursorEl = document.querySelector('.tl-collaborator__cursor'); + //if (!cursorEl) return; //const zoom = parseFloat(getComputedStyle(tlContainer).getPropertyValue('--tl-zoom')) || 1; const { z: zoom } = tlEditorRef.current.getCamera(); From 5dd34e69f38ffdfa82707cc22b7407b616b224d6 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sat, 2 May 2026 10:40:52 +0900 Subject: [PATCH 47/88] Remove unnecessary attribute in hooks.ts It is included in ...coordinates. --- bigbluebutton-html5/imports/ui/components/whiteboard/hooks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/hooks.ts b/bigbluebutton-html5/imports/ui/components/whiteboard/hooks.ts index 3ea898d72cb7..4f35590e09b1 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/hooks.ts +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/hooks.ts @@ -86,7 +86,7 @@ export const useMergedCursorData = () => { return { ...coordinates, ...cursor, - laserType: coordinates.laserType, + //laserType: coordinates.laserType, }; } From 585d6b245edd02755ecc3dceac2b85ae71c0dfd9 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sat, 2 May 2026 19:13:45 +0900 Subject: [PATCH 48/88] swap args (chore) --- .../internal/streaming_server/presPageCursorStream.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bbb-graphql-middleware/internal/streaming_server/presPageCursorStream.go b/bbb-graphql-middleware/internal/streaming_server/presPageCursorStream.go index dc3c5c62e966..0a418d81d43c 100644 --- a/bbb-graphql-middleware/internal/streaming_server/presPageCursorStream.go +++ b/bbb-graphql-middleware/internal/streaming_server/presPageCursorStream.go @@ -23,8 +23,8 @@ func HandleSendCursorPositionEvtMsg(receivedMessage common.RedisMessage, browser item := map[string]any{ "xPercent": xPercent, "yPercent": yPercent, - "userId": receivedMessage.Core.Header.UserId, "laserType": laserType, + "userId": receivedMessage.Core.Header.UserId, "__typename": "pres_page_cursor", } From 69911c7faef84e2190f11d4d0c7447858e9d21f8 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Mon, 4 May 2026 11:43:38 +0900 Subject: [PATCH 49/88] Fallback undefined laserType to '' --- bbb-graphql-actions/src/actions/presentationPublishCursor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bbb-graphql-actions/src/actions/presentationPublishCursor.ts b/bbb-graphql-actions/src/actions/presentationPublishCursor.ts index 1c468717c30e..4f26a173f7ca 100644 --- a/bbb-graphql-actions/src/actions/presentationPublishCursor.ts +++ b/bbb-graphql-actions/src/actions/presentationPublishCursor.ts @@ -28,7 +28,7 @@ export default function buildRedisMessage(sessionVariables: Record Date: Mon, 4 May 2026 11:44:42 +0900 Subject: [PATCH 50/88] Revert the fallback in the previous change --- bbb-graphql-actions/src/actions/presentationPublishCursor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bbb-graphql-actions/src/actions/presentationPublishCursor.ts b/bbb-graphql-actions/src/actions/presentationPublishCursor.ts index 4f26a173f7ca..1c468717c30e 100644 --- a/bbb-graphql-actions/src/actions/presentationPublishCursor.ts +++ b/bbb-graphql-actions/src/actions/presentationPublishCursor.ts @@ -28,7 +28,7 @@ export default function buildRedisMessage(sessionVariables: Record Date: Mon, 4 May 2026 13:25:13 +0900 Subject: [PATCH 51/88] Fix: show laser on mobile devices and show menu by long tap 1) Showing laser pointer when the presenter uses mobile devices, which did not show anything previously. But the laser pointer does not respond very quickly because here the clients's information is recruited, not directly reflecting the presenter's touch. 2) Now the presenter can show the pull down menu to select laser. --- .../ui/components/whiteboard/component.jsx | 54 +++++++++++++++---- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 6f27c1ceaec3..f35cc11b2859 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2194,33 +2194,63 @@ const Whiteboard = React.memo((props) => { if (!presentationWrapper.contains(e.target)) return; e.preventDefault(); + e.stopPropagation(); setLaserMenuPos({ x: e.clientX, y: e.clientY }); setLaserMenuVisible(true); }; - targetDoc.addEventListener('contextmenu', handleContextMenu); + let timer = null; + + const handleTouchStart = (e) => { + const tool = tlEditorRef.current?.getCurrentToolId?.(); + if (tool !== 'hand') return; + if (!presentationWrapper.contains(e.target)) return; + + const touch = e.touches[0]; + + timer = setTimeout(() => { + setLaserMenuPos({ + x: touch.clientX, + y: touch.clientY, + }); + setLaserMenuVisible(true); + }, 500); + }; + + const cancel = () => { + clearTimeout(timer); + }; + + presentationWrapper.addEventListener('contextmenu', handleContextMenu, true); + presentationWrapper.addEventListener('touchstart', handleTouchStart, true); + presentationWrapper.addEventListener('touchend', cancel, true); + presentationWrapper.addEventListener('touchmove', cancel, true); return () => { - targetDoc.removeEventListener('contextmenu', handleContextMenu); + presentationWrapper.removeEventListener('contextmenu', handleContextMenu, true); + presentationWrapper.removeEventListener('touchstart', handleTouchStart, true); + presentationWrapper.removeEventListener('touchend', cancel, true); + presentationWrapper.removeEventListener('touchmove', cancel, true); }; }, [isPresenter]); React.useEffect(() => { if (!laserMenuVisible) return; + const targetDoc = document; + const presentationWrapper = targetDoc.querySelector('#presentationInnerWrapper'); + if (!presentationWrapper) return; const handleOutsideClick = (e) => { if (laserMenuRef.current?.contains(e.target)) return; setLaserMenuVisible(false); }; - targetDoc.addEventListener('mousedown', handleOutsideClick); - targetDoc.addEventListener('touchstart', handleOutsideClick); + presentationWrapper.addEventListener('pointerdown', handleOutsideClick, true); return () => { - targetDoc.removeEventListener('mousedown', handleOutsideClick); - targetDoc.removeEventListener('touchstart', handleOutsideClick); + presentationWrapper.removeEventListener('pointerdown', handleOutsideClick, true); }; }, [laserMenuVisible]); @@ -2347,7 +2377,7 @@ const Whiteboard = React.memo((props) => { // Show viewers Laser SVG React.useEffect(() => { - if (isPresenter) return; + if (isPresenter && !isPhone) return; //if (isMultiUserActive) return; const tlContainer = document.querySelector('.tl-container'); @@ -2361,6 +2391,8 @@ const Whiteboard = React.memo((props) => { let laserEl = laserElRef.current; const presenterCursor = otherCursors.find(c => c.presenter); + if (!presenterCursor) return; + const laserKey = presenterCursor?.laserType; const laserDef = laserDefs[laserKey]; @@ -2380,7 +2412,8 @@ const Whiteboard = React.memo((props) => { } } - if (!laserDef) return; // hand tool, so laser pointer is drawn + if (!layer) return; + if (!laserDef) return; // meaning that hand tool is selected, so we move forward to draw laser pointer if (!laserEl && layer) { laserEl = createLaserElement(laserSvgs[laserKey], document, laserDef); @@ -2388,12 +2421,13 @@ const Whiteboard = React.memo((props) => { laserElRef.current = laserEl; } - // Place the laser SVG at the position of redPointer, which is invisible. + // Now we place the laser SVG at the position of redPointer, which is invisible. + //const cursorEl = document.querySelector('.tl-collaborator__cursor'); //if (!cursorEl) return; //const zoom = parseFloat(getComputedStyle(tlContainer).getPropertyValue('--tl-zoom')) || 1; - const { z: zoom } = tlEditorRef.current.getCamera(); + const { z: zoom } = tlEditorRef.current ? tlEditorRef.current.getCamera() : 1; //const transform = cursorEl.style.transform; //if (!transform) return; From 244b59e07e93184252c0743b848e270df1da4edb Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Mon, 4 May 2026 14:01:56 +0900 Subject: [PATCH 52/88] [option] hide laser when non-hand tool is used by a presenter on mobile --- .../imports/ui/components/whiteboard/component.jsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index f35cc11b2859..8fe50bae9f58 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2380,6 +2380,11 @@ const Whiteboard = React.memo((props) => { if (isPresenter && !isPhone) return; //if (isMultiUserActive) return; + //Comment out below if we do not want to show laser when a presenter uses drawing tools on mobile devices. + // Note a problem that the laser remains on the screen after switching to drawing tools. + //const tool = tlEditorRef.current?.getCurrentToolId?.(); + //if (isPresenter && isPhone && tool !== 'hand') return; + const tlContainer = document.querySelector('.tl-container'); let layer = laserLayerRef.current; From 6c84deb2ad288a6764db992c0c07455589e33379 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Mon, 4 May 2026 14:16:12 +0900 Subject: [PATCH 53/88] Add isMobile at container --- .../imports/ui/components/whiteboard/container.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx index 1afd9fc6197b..cc06fa90ce80 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx @@ -434,7 +434,7 @@ const WhiteboardContainer = (props) => { const bgShape = []; - const { isIphone, isPhone } = deviceInfo; + const { isIphone, isPhone, isMobile } = deviceInfo; const assetId = AssetRecordType.createId(curPageNum); const assets = [{ @@ -536,6 +536,7 @@ const WhiteboardContainer = (props) => { toggleToolsAnimations, isIphone, isPhone, + isMobile, currentPresentationPage, numberOfPages: currentPresentationPage?.totalPages, presentationId, From 9a22b4b89a1c50de2cc50727c1454b2887557f45 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Mon, 4 May 2026 14:17:45 +0900 Subject: [PATCH 54/88] [fix] Show laser also on tablets --- .../imports/ui/components/whiteboard/component.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 8fe50bae9f58..c2db61aca209 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -167,6 +167,7 @@ const Whiteboard = React.memo((props) => { isInfiniteWhiteboard, whiteboardWriters, isPhone, + isMobile, setEditor, lockToolbarTools, layoutChanged, @@ -2377,7 +2378,7 @@ const Whiteboard = React.memo((props) => { // Show viewers Laser SVG React.useEffect(() => { - if (isPresenter && !isPhone) return; + if (isPresenter && !isMobile) return; //if (isMultiUserActive) return; //Comment out below if we do not want to show laser when a presenter uses drawing tools on mobile devices. From 10c5d1e2e1629b5524d8094c45f690177952163f Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Mon, 4 May 2026 14:38:15 +0900 Subject: [PATCH 55/88] [fix] remove viewer's laser when become the presenter --- .../imports/ui/components/whiteboard/component.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index c2db61aca209..3b6714be28a6 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2283,6 +2283,8 @@ const Whiteboard = React.memo((props) => { if (!isPresenter) return; const el = targetDoc.querySelector('.tl-container'); if (!el) return; + + removeViewerLaser(); const laser = cursorLasers[laserMode]; if (laser) { From f7bb8180566aaff30e8445310c437a6fd755b9c6 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Mon, 4 May 2026 14:57:34 +0900 Subject: [PATCH 56/88] [fix] viewer's cursor on popup window --- .../imports/ui/components/whiteboard/component.jsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 3b6714be28a6..206752fff09b 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2383,16 +2383,18 @@ const Whiteboard = React.memo((props) => { if (isPresenter && !isMobile) return; //if (isMultiUserActive) return; + const targetDoc = isPresentationDetached && popupWindow?.document ? popupWindow.document : document; + //Comment out below if we do not want to show laser when a presenter uses drawing tools on mobile devices. // Note a problem that the laser remains on the screen after switching to drawing tools. //const tool = tlEditorRef.current?.getCurrentToolId?.(); //if (isPresenter && isPhone && tool !== 'hand') return; - const tlContainer = document.querySelector('.tl-container'); + const tlContainer = targetDoc.querySelector('.tl-container'); let layer = laserLayerRef.current; - if (!layer || !document.contains(layer)) { - layer = document.querySelector('.tl-overlays > .tl-html-layer'); + if (!layer || !targetDoc.contains(layer)) { + layer = targetDoc.querySelector('.tl-overlays > .tl-html-layer'); laserLayerRef.current = layer; } @@ -2424,7 +2426,7 @@ const Whiteboard = React.memo((props) => { if (!laserDef) return; // meaning that hand tool is selected, so we move forward to draw laser pointer if (!laserEl && layer) { - laserEl = createLaserElement(laserSvgs[laserKey], document, laserDef); + laserEl = createLaserElement(laserSvgs[laserKey], targetDoc, laserDef); layer.appendChild(laserEl); laserElRef.current = laserEl; } From f402f54bca384f6187d8c87cc67c85bdec5b322e Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Mon, 4 May 2026 14:58:20 +0900 Subject: [PATCH 57/88] isPhone -> isMobile (commented out anyway) --- .../imports/ui/components/whiteboard/component.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 206752fff09b..ff9e084a171a 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2388,7 +2388,7 @@ const Whiteboard = React.memo((props) => { //Comment out below if we do not want to show laser when a presenter uses drawing tools on mobile devices. // Note a problem that the laser remains on the screen after switching to drawing tools. //const tool = tlEditorRef.current?.getCurrentToolId?.(); - //if (isPresenter && isPhone && tool !== 'hand') return; + //if (isPresenter && isMobile && tool !== 'hand') return; const tlContainer = targetDoc.querySelector('.tl-container'); From 373084e4fb397bc139cdb6bbaa572400682432eb Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Mon, 4 May 2026 15:17:43 +0900 Subject: [PATCH 58/88] [fix] targetDoc for removeViewersLaser --- .../imports/ui/components/whiteboard/component.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index ff9e084a171a..ea66d0c894f2 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2374,7 +2374,8 @@ const Whiteboard = React.memo((props) => { const removeViewerLaser = () => { laserElRef.current = null; - const lasers = document.querySelectorAll('.bbb-laser-pointer'); + const targetDoc = isPresentationDetached && popupWindow?.document ? popupWindow.document : document; + const lasers = targetDoc.querySelectorAll('.bbb-laser-pointer'); lasers.forEach(el => el.remove()); }; From 3a2769830769cdebbaa90b753647ff8c67dfa09e Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Mon, 4 May 2026 20:30:15 +0900 Subject: [PATCH 59/88] [refactor] remove an unnecessary arg --- .../imports/ui/components/whiteboard/component.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index ea66d0c894f2..df7202276976 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -1974,7 +1974,7 @@ const Whiteboard = React.memo((props) => { ]) ); - const createLaserElement = (svgString, targetDoc, def) => { + const createLaserElement = (svgString, targetDoc) => { const wrapper = targetDoc.createElement('div'); wrapper.className = 'custom-laser'; wrapper.innerHTML = svgString; @@ -2427,7 +2427,7 @@ const Whiteboard = React.memo((props) => { if (!laserDef) return; // meaning that hand tool is selected, so we move forward to draw laser pointer if (!laserEl && layer) { - laserEl = createLaserElement(laserSvgs[laserKey], targetDoc, laserDef); + laserEl = createLaserElement(laserSvgs[laserKey], targetDoc); layer.appendChild(laserEl); laserElRef.current = laserEl; } From 2dc7358cce15535719e3116de1b3d2502e2f8302 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Wed, 6 May 2026 21:57:14 +0900 Subject: [PATCH 60/88] [Fix] Better Laser for mobile presenter - Store the cursor position - Show Laser as a React element using the stored cursor position - Change the class name to avoid being removed by removeViewerLaser(). --- .../ui/components/whiteboard/component.jsx | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index df7202276976..b6414978fec2 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -186,6 +186,7 @@ const Whiteboard = React.memo((props) => { const [laserMenuPos, setLaserMenuPos] = React.useState({ x: 0, y: 0 }); const laserMenuRef = React.useRef(null); const [laserMode, setLaserMode] = React.useState(''); + const [presenterCursorPoint, setPresenterCursorPoint] = React.useState( { x: -1, y: -1} ); if (isMounting) { setDefaultEditorAssetUrls(getCustomEditorAssetUrls()); @@ -2372,6 +2373,17 @@ const Whiteboard = React.memo((props) => { } }, [otherCursors, whiteboardWriters]); + // Store presenter's cursor position to draw laser for mobile presenter + React.useEffect(() => { + tlEditorRef.current?.store.listen(({ changes }) => { + const p = tlEditorRef.current?.inputs.currentPagePoint; + if (p && tlEditorRef.current) { + const screenPos = tlEditorRef.current.pageToScreen(p); + setPresenterCursorPoint( {x: screenPos.x, y: screenPos.y} ); + } + }) + }, [tlEditorRef.current]); + const removeViewerLaser = () => { laserElRef.current = null; const targetDoc = isPresentationDetached && popupWindow?.document ? popupWindow.document : document; @@ -2381,7 +2393,7 @@ const Whiteboard = React.memo((props) => { // Show viewers Laser SVG React.useEffect(() => { - if (isPresenter && !isMobile) return; + if (isPresenter) return; //if (isMultiUserActive) return; const targetDoc = isPresentationDetached && popupWindow?.document ? popupWindow.document : document; @@ -2644,6 +2656,28 @@ const Whiteboard = React.memo((props) => { hiddenGeoShapes, }} /> + { (isPresenter && isMobile) && (() => { + const svg = laserSvgs[laserMode]; + if (!svg) return null; + const tool = tlEditorRef.current?.getCurrentToolId?.(); + if (tool !== 'hand') return; + const svgMobilePresenter = svg.replace( + 'bbb-laser-pointer', + 'bbb-laser-pointer-mobile-presenter' + ); + return ( +
+ ); + })()} {laserMenuVisible && ( Date: Wed, 6 May 2026 21:59:01 +0900 Subject: [PATCH 61/88] [fix] remove popups which were accidentally contaminated --- .../imports/ui/components/whiteboard/component.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index b6414978fec2..6dfc83f19239 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2386,7 +2386,7 @@ const Whiteboard = React.memo((props) => { const removeViewerLaser = () => { laserElRef.current = null; - const targetDoc = isPresentationDetached && popupWindow?.document ? popupWindow.document : document; + const targetDoc = document; const lasers = targetDoc.querySelectorAll('.bbb-laser-pointer'); lasers.forEach(el => el.remove()); }; @@ -2396,7 +2396,7 @@ const Whiteboard = React.memo((props) => { if (isPresenter) return; //if (isMultiUserActive) return; - const targetDoc = isPresentationDetached && popupWindow?.document ? popupWindow.document : document; + const targetDoc = document; //Comment out below if we do not want to show laser when a presenter uses drawing tools on mobile devices. // Note a problem that the laser remains on the screen after switching to drawing tools. From 0f70f31f60b0146e5c5ff9f1bb9bd91583036ad9 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sat, 16 May 2026 10:05:42 +0900 Subject: [PATCH 62/88] [chore] Update bbb-graphql-schema.md --- bbb-graphql-server/bbb-graphql-schema.md | 1 + 1 file changed, 1 insertion(+) diff --git a/bbb-graphql-server/bbb-graphql-schema.md b/bbb-graphql-server/bbb-graphql-schema.md index 7496aa42c4ea..b364317901a0 100644 --- a/bbb-graphql-server/bbb-graphql-schema.md +++ b/bbb-graphql-server/bbb-graphql-schema.md @@ -504,6 +504,7 @@ Permission: Restricted to Poll Owner or User Viewing Self-Related Data - `userId` - `xPercent` - `yPercent` +- `laserType` ### Relationships: - `user: Object` [Type User](#type-user) From 2ef6b5f1bf21a126f08ee995874286287c8f6e15 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sat, 16 May 2026 18:13:49 +0900 Subject: [PATCH 63/88] Update hooks.ts (laserType removed) This might have added some stability in the code. But almost surely unnecessary (always included in the args above). --- bigbluebutton-html5/imports/ui/components/whiteboard/hooks.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/hooks.ts b/bigbluebutton-html5/imports/ui/components/whiteboard/hooks.ts index 4f35590e09b1..b76ef2b6b16b 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/hooks.ts +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/hooks.ts @@ -86,7 +86,6 @@ export const useMergedCursorData = () => { return { ...coordinates, ...cursor, - //laserType: coordinates.laserType, }; } From ab8b9ae2bf90fde71471bda25ebb5be8072702eb Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sat, 16 May 2026 21:06:29 +0900 Subject: [PATCH 64/88] [configure color] add settings --- bigbluebutton-html5/private/config/settings.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml index 1c36f6a807b5..cfce0a6e88e0 100755 --- a/bigbluebutton-html5/private/config/settings.yml +++ b/bigbluebutton-html5/private/config/settings.yml @@ -1092,6 +1092,8 @@ public: pointerDiameter: 5 laserRadiusSmall: 10 laserRadiusLarge: 16 + laserRedColor: '#ff1414' + laserGreenColor: '#14ff14' maxStickyNoteLength: 1000 # limit number of annotations per slide maxNumberOfAnnotations: 300 From e48abf62d6f8b5359e39f37c69634c2a2af90a04 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sat, 16 May 2026 21:55:07 +0900 Subject: [PATCH 65/88] [configure color] acquire props and use them --- .../ui/components/whiteboard/component.jsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 6dfc83f19239..8650e0b5520f 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -174,6 +174,8 @@ const Whiteboard = React.memo((props) => { pointerDiameter = 5, laserRadiusSmall, laserRadiusLarge, + laserRedColor, + laserGreenColor, } = props; clearTldrawCache(); @@ -1946,16 +1948,11 @@ const Whiteboard = React.memo((props) => { `.replace(/\s+/g, ' ').trim(); }; - const c = { - red: 'rgba(255, 20, 20)', - green: 'rgba(20, 255, 20)', - }; - const laserDefs = { - redSmall: { color: c.red, cx: laserRadiusSmall+2, cy: laserRadiusSmall+2, r: laserRadiusSmall }, - greenSmall: { color: c.green, cx: laserRadiusSmall+2, cy: laserRadiusSmall+2, r: laserRadiusSmall }, - redLarge: { color: c.red, cx: laserRadiusLarge+2, cy: laserRadiusLarge+2, r: laserRadiusLarge }, - greenLarge: { color: c.green, cx: laserRadiusLarge+2, cy: laserRadiusLarge+2, r: laserRadiusLarge }, + redSmall: { color: laserRedColor, cx: laserRadiusSmall+2, cy: laserRadiusSmall+2, r: laserRadiusSmall }, + greenSmall: { color: laserGreenColor, cx: laserRadiusSmall+2, cy: laserRadiusSmall+2, r: laserRadiusSmall }, + redLarge: { color: laserRedColor, cx: laserRadiusLarge+2, cy: laserRadiusLarge+2, r: laserRadiusLarge }, + greenLarge: { color: laserGreenColor, cx: laserRadiusLarge+2, cy: laserRadiusLarge+2, r: laserRadiusLarge }, }; const laserSvgs = Object.fromEntries( From 6676ed9d792ca39c1a45f382e962bb17217f7d56 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sat, 16 May 2026 21:58:12 +0900 Subject: [PATCH 66/88] [configure color] provide values to component --- .../imports/ui/components/whiteboard/container.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx index cc06fa90ce80..dac1ee172b15 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx @@ -462,7 +462,9 @@ const WhiteboardContainer = (props) => { const sidebarNavigationWidth = layoutSelect( (i) => i?.output?.sidebarNavigation?.width, ); - const { maxStickyNoteLength, maxNumberOfAnnotations, lockToolbarTools, pointerDiameter, laserRadiusSmall, laserRadiusLarge } = WHITEBOARD_CONFIG; + const { maxStickyNoteLength, maxNumberOfAnnotations, lockToolbarTools, pointerDiameter, + laserRadiusSmall, laserRadiusLarge, laserRedColor, laserGreenColor, + } = WHITEBOARD_CONFIG; const fontFamily = WHITEBOARD_CONFIG.styles.text.family; const { colorStyle, dashStyle, fillStyle, fontStyle, sizeStyle, @@ -511,6 +513,8 @@ const WhiteboardContainer = (props) => { pointerDiameter, laserRadiusSmall, laserRadiusLarge, + laserRedColor, + laserGreenColor, fontFamily, colorStyle, dashStyle, From 32ce62fefae8ef3961b959870975d68efad218aa Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sat, 16 May 2026 22:00:20 +0900 Subject: [PATCH 67/88] [configure color] set default values --- .../imports/ui/core/initial-values/meetingClientSettings.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bigbluebutton-html5/imports/ui/core/initial-values/meetingClientSettings.ts b/bigbluebutton-html5/imports/ui/core/initial-values/meetingClientSettings.ts index 391568993062..7d8495c28cba 100644 --- a/bigbluebutton-html5/imports/ui/core/initial-values/meetingClientSettings.ts +++ b/bigbluebutton-html5/imports/ui/core/initial-values/meetingClientSettings.ts @@ -817,6 +817,8 @@ export const meetingClientSettingsInitialValues: MeetingClientSettings = { pointerDiameter: 5, laserRadiusSmall: 10, laserRadiusLarge: 16, + laserRedColor: '#ff1414', + laserGreenColor: '#14ff14', maxStickyNoteLength: 1000, maxNumberOfAnnotations: 300, maxNumberOfActiveUsers: 25, From e870f66ba085ef96bf98f1c7b18ca1d4ff06458b Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sat, 16 May 2026 22:01:28 +0900 Subject: [PATCH 68/88] [configure color] export and define setting --- bigbluebutton-html5/imports/ui/Types/meetingClientSettings.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bigbluebutton-html5/imports/ui/Types/meetingClientSettings.ts b/bigbluebutton-html5/imports/ui/Types/meetingClientSettings.ts index 9e005b193b96..cc4fe4577d2d 100644 --- a/bigbluebutton-html5/imports/ui/Types/meetingClientSettings.ts +++ b/bigbluebutton-html5/imports/ui/Types/meetingClientSettings.ts @@ -747,6 +747,8 @@ export interface Whiteboard { pointerDiameter: number laserRadiusSmall: number laserRadiusLarge: number + laserRedColor: string + laserGreenColor: string maxStickyNoteLength: number maxNumberOfAnnotations: number maxNumberOfActiveUsers: number From f4df14a91394613ad730ceae5d1580411535b143 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sat, 16 May 2026 23:50:56 +0900 Subject: [PATCH 69/88] Minor security fixes --- .../imports/ui/components/whiteboard/component.jsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 8650e0b5520f..36881e2ec6c2 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2424,11 +2424,11 @@ const Whiteboard = React.memo((props) => { laserElRef.current = null; const defaultPointer = document.getElementById('redPointer'); if (!laserDef) { - // Presenter uses hand tool and the default red pointer is visible for viewers - defaultPointer.style.setProperty('display', 'block'); + // Presenter uses hand tool, so the default red pointer is visible for viewers + defaultPointer?.style.setProperty('display', 'block'); } else { - // Presenter uses laser pointer and the default red pointer is invisible for viewers - defaultPointer.style.setProperty('display', 'none'); + // Presenter uses laser pointer, so the default red pointer is invisible for viewers + defaultPointer?.style.setProperty('display', 'none'); } } @@ -2705,6 +2705,7 @@ export default Whiteboard; Whiteboard.propTypes = { isPresenter: PropTypes.bool, isPhone: PropTypes.bool, + isMobile: PropTypes.bool, removeShapes: PropTypes.func.isRequired, persistShapeWrapper: PropTypes.func.isRequired, notifyNotAllowedChange: PropTypes.func.isRequired, @@ -2736,6 +2737,8 @@ Whiteboard.propTypes = { pointerDiameter: PropTypes.number, laserRadiusSmall: PropTypes.number.isRequired, laserRadiusLarge: PropTypes.number.isRequired, + laserRedColor: PropTypes.string.isRequired, + laserGreenColor: PropTypes.string.isRequired, setTldrawIsMounting: PropTypes.func.isRequired, presentationId: PropTypes.string, setTldrawAPI: PropTypes.func.isRequired, From 27e8068370cf717eff7b4001875e6fd7988b2d97 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sat, 4 Jul 2026 13:51:16 +0900 Subject: [PATCH 70/88] More PP-like laser SVG --- .../ui/components/whiteboard/component.jsx | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 2166ecdf2f09..f6be77f37b18 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -1991,6 +1991,7 @@ const Whiteboard = React.memo((props) => { const height = cy * 2; // On Windows and Linux, it darkens towards the edge + /* return ` @@ -2003,6 +2004,31 @@ const Whiteboard = React.memo((props) => { `.replace(/\s+/g, ' ').trim(); + */ + return ` + + + + + + + + + + + + + + + + + + + + + + + `.replace(/\s+/g, ' ').trim(); }; const laserDefs = { From b925720b1b9551d74171293da504d01171d8c8ac Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sat, 4 Jul 2026 13:54:19 +0900 Subject: [PATCH 71/88] More greenish color setting --- bigbluebutton-html5/private/config/settings.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml index 0ad989618258..dd4b5c97d6d9 100755 --- a/bigbluebutton-html5/private/config/settings.yml +++ b/bigbluebutton-html5/private/config/settings.yml @@ -1115,7 +1115,7 @@ public: laserRadiusSmall: 10 laserRadiusLarge: 16 laserRedColor: '#ff1414' - laserGreenColor: '#14ff14' + laserGreenColor: '#00ff00' maxStickyNoteLength: 1000 # limit number of annotations per slide maxNumberOfAnnotations: 300 From 9819f18c451c1de82a024acb3170ac493ca4c09e Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sat, 4 Jul 2026 15:09:36 +0900 Subject: [PATCH 72/88] unlisten store.listen according to the coderabbitai --- .../ui/components/whiteboard/component.jsx | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index f6be77f37b18..7b448699a9f9 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2464,13 +2464,19 @@ const Whiteboard = React.memo((props) => { // Store presenter's cursor position to draw laser for mobile presenter React.useEffect(() => { - tlEditorRef.current?.store.listen(({ changes }) => { - const p = tlEditorRef.current?.inputs.currentPagePoint; - if (p && tlEditorRef.current) { - const screenPos = tlEditorRef.current.pageToScreen(p); - setPresenterCursorPoint( {x: screenPos.x, y: screenPos.y} ); - } - }) + const editor = tlEditorRef.current; + if (!editor) return undefined; + + const unlisten = editor.store.listen(() => { + const p = editor.inputs.currentPagePoint; + if (!p) return; + const screenPos = editor.pageToScreen(p); + setPresenterCursorPoint({ x: screenPos.x, y: screenPos.y }); + }); + + return () => { + unlisten?.(); + }; }, [tlEditorRef.current]); const removeViewerLaser = () => { From 43fb8b72f1503059a442fff9f5f14849f4cb0e19 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sat, 4 Jul 2026 15:22:05 +0900 Subject: [PATCH 73/88] add clearTimeout at return() according to coderabbitai --- .../imports/ui/components/whiteboard/component.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 7b448699a9f9..b30586ab8366 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2319,6 +2319,7 @@ const Whiteboard = React.memo((props) => { presentationWrapper.addEventListener('touchmove', cancel, true); return () => { + clearTimeout(timer); presentationWrapper.removeEventListener('contextmenu', handleContextMenu, true); presentationWrapper.removeEventListener('touchstart', handleTouchStart, true); presentationWrapper.removeEventListener('touchend', cancel, true); From a8507b4a6e8ac66bb00725a06a25b64ebc30cd7c Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Tue, 18 Aug 2026 22:00:39 +0900 Subject: [PATCH 74/88] [general setting of laser color] whiteboard/component.jsx --- .../ui/components/whiteboard/component.jsx | 62 +++++++++++++------ 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 1996cfb340d0..6aeefc2fd4b9 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -29,6 +29,7 @@ import KEY_CODES from '/imports/utils/keyCodes'; import { debounce } from '/imports/utils/debounce'; import logger from '/imports/startup/client/logger'; import Styled from './styles'; +import Icon from '/imports/ui/components/common/icon/component'; import { mapLanguage, isValidShapeType, @@ -187,8 +188,7 @@ const Whiteboard = React.memo((props) => { pointerDiameter = 5, laserRadiusSmall, laserRadiusLarge, - laserRedColor, - laserGreenColor, + laserColors, } = props; const allowInfiniteWhiteboardPanForViewers = window.meetingClientSettings?.public?.whiteboard?.allowInfiniteWhiteboardPanForViewers; @@ -262,14 +262,6 @@ const Whiteboard = React.memo((props) => { currentUserRef.current = currentUser; - const laserItems = [ - { key: 'redSmall', label: '🔴', size: 10 }, - { key: 'greenSmall', label: '🟢', size: 10 }, - { key: 'redLarge', label: '🔴', size: 18 }, - { key: 'greenLarge', label: '🟢', size: 18 }, - { key: '', label: '✋', size: 14 }, - ]; - const [pageZoomMap, setPageZoomMap] = useState(() => { try { const saved = localStorage.getItem('pageZoomMap'); @@ -2043,12 +2035,24 @@ const Whiteboard = React.memo((props) => { `.replace(/\s+/g, ' ').trim(); }; - const laserDefs = { - redSmall: { color: laserRedColor, cx: laserRadiusSmall+2, cy: laserRadiusSmall+2, r: laserRadiusSmall }, - greenSmall: { color: laserGreenColor, cx: laserRadiusSmall+2, cy: laserRadiusSmall+2, r: laserRadiusSmall }, - redLarge: { color: laserRedColor, cx: laserRadiusLarge+2, cy: laserRadiusLarge+2, r: laserRadiusLarge }, - greenLarge: { color: laserGreenColor, cx: laserRadiusLarge+2, cy: laserRadiusLarge+2, r: laserRadiusLarge }, - }; + const laserSizes = [ + ['Small', laserRadiusSmall], + ['Large', laserRadiusLarge], + ]; + + const laserDefs = Object.fromEntries( + laserSizes.flatMap(([sizeName, radius]) => ( + laserColors.map((color, index) => [ + `color${index + 1}${sizeName}`, + { + color, + cx: radius + 2, + cy: radius + 2, + r: radius, + }, + ]) + )), + ); const laserSvgs = Object.fromEntries( Object.entries(laserDefs).map(([key, def]) => [ @@ -2057,8 +2061,12 @@ const Whiteboard = React.memo((props) => { ]) ); + const svgToDataUrl = (svg) => + `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`; + + const svgToCursor = (svg, x, y) => - `url("data:image/svg+xml;utf8,${encodeURIComponent(svg)}") ${x} ${y}, auto`; + `url("${svgToDataUrl(svg)}") ${x} ${y}, auto`; const cursorLasers = Object.fromEntries( Object.entries(laserDefs).map(([key, def]) => [ @@ -2910,7 +2918,7 @@ const Whiteboard = React.memo((props) => { top: laserMenuPos.y, }} > - {laserItems.map(({ key, label, size }) => ( + {Object.entries(laserDefs).map(([key, def]) => ( { @@ -2919,8 +2927,23 @@ const Whiteboard = React.memo((props) => { }} > {label} + ))} + { + setLaserMode(''); + setLaserMenuVisible(false); + }} + > + + )}
@@ -2964,8 +2987,7 @@ Whiteboard.propTypes = { pointerDiameter: PropTypes.number, laserRadiusSmall: PropTypes.number.isRequired, laserRadiusLarge: PropTypes.number.isRequired, - laserRedColor: PropTypes.string.isRequired, - laserGreenColor: PropTypes.string.isRequired, + laserColors: PropTypes.arrayOf(PropTypes.string).isRequired, setTldrawIsMounting: PropTypes.func.isRequired, presentationId: PropTypes.string, setTldrawAPI: PropTypes.func.isRequired, From e056299b9e41716606f47b840f588d9a24b4feae Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Tue, 18 Aug 2026 22:03:02 +0900 Subject: [PATCH 75/88] [general setting of laser color] whiteboard container --- .../imports/ui/components/whiteboard/container.jsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx index 3f6cf41b5b61..14542a7e0f0b 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx @@ -464,7 +464,7 @@ const WhiteboardContainer = (props) => { (i) => i?.output?.sidebarNavigation?.width, ); const { maxStickyNoteLength, maxNumberOfAnnotations, lockToolbarTools, pointerDiameter, - laserRadiusSmall, laserRadiusLarge, laserRedColor, laserGreenColor, + laserRadiusSmall, laserRadiusLarge, laserColors, } = WHITEBOARD_CONFIG; const fontFamily = WHITEBOARD_CONFIG.styles.text.family; const { @@ -514,8 +514,7 @@ const WhiteboardContainer = (props) => { pointerDiameter, laserRadiusSmall, laserRadiusLarge, - laserRedColor, - laserGreenColor, + laserColors, fontFamily, colorStyle, dashStyle, From 867888d0bc5e6a407281a32b3e9a5dd700ffade6 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Tue, 18 Aug 2026 22:05:13 +0900 Subject: [PATCH 76/88] [general setting of laser color] whiteboard/styles.js --- .../imports/ui/components/whiteboard/styles.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/styles.js b/bigbluebutton-html5/imports/ui/components/whiteboard/styles.js index 61b2db726912..d92f33c4e55b 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/styles.js +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/styles.js @@ -276,10 +276,13 @@ const LaserContextMenu = styled.div` `; const LaserMenuItem = styled.div` - padding: 6px 10px; + width: 36px; + height: 36px; cursor: pointer; border-radius: 4px; - text-align: center; + display: flex; + align-items: center; + justify-content: center; &:hover { background: #333; From a496d92fa84f70881f548fd9942020e7857a9208 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Tue, 18 Aug 2026 22:07:28 +0900 Subject: [PATCH 77/88] [general setting of laser color] Types/meetingClientSettings.ts --- bigbluebutton-html5/imports/ui/Types/meetingClientSettings.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/Types/meetingClientSettings.ts b/bigbluebutton-html5/imports/ui/Types/meetingClientSettings.ts index 8e0dd5192d9e..c6be51c6ce52 100644 --- a/bigbluebutton-html5/imports/ui/Types/meetingClientSettings.ts +++ b/bigbluebutton-html5/imports/ui/Types/meetingClientSettings.ts @@ -754,8 +754,7 @@ export interface Whiteboard { pointerDiameter: number laserRadiusSmall: number laserRadiusLarge: number - laserRedColor: string - laserGreenColor: string + laserColors: string[] maxStickyNoteLength: number maxNumberOfAnnotations: number maxNumberOfActiveUsers: number From 1b24d6d72bbf8ca2f40b60d126fc47f51866a3d8 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Tue, 18 Aug 2026 22:09:18 +0900 Subject: [PATCH 78/88] [general setting of laser color] initial-values/meetingClientSettings.ts --- .../imports/ui/core/initial-values/meetingClientSettings.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/core/initial-values/meetingClientSettings.ts b/bigbluebutton-html5/imports/ui/core/initial-values/meetingClientSettings.ts index 8ecda2ffda43..f1d71d4e043c 100644 --- a/bigbluebutton-html5/imports/ui/core/initial-values/meetingClientSettings.ts +++ b/bigbluebutton-html5/imports/ui/core/initial-values/meetingClientSettings.ts @@ -822,8 +822,10 @@ export const meetingClientSettingsInitialValues: MeetingClientSettings = { pointerDiameter: 5, laserRadiusSmall: 10, laserRadiusLarge: 16, - laserRedColor: '#ff1414', - laserGreenColor: '#14ff14', + laserColors: [ + '#ff1414', + '#00a080', + ], maxStickyNoteLength: 1000, maxNumberOfAnnotations: 300, maxNumberOfActiveUsers: 25, From e69964eabd0b5b45202598ef300bfb3e4020ffa8 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Tue, 18 Aug 2026 22:10:52 +0900 Subject: [PATCH 79/88] [general setting of laser color] config/setting.yml --- bigbluebutton-html5/private/config/settings.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml index 3278540365de..a1ea19285c29 100755 --- a/bigbluebutton-html5/private/config/settings.yml +++ b/bigbluebutton-html5/private/config/settings.yml @@ -1122,8 +1122,9 @@ public: pointerDiameter: 5 laserRadiusSmall: 10 laserRadiusLarge: 16 - laserRedColor: '#ff1414' - laserGreenColor: '#00ff00' + laserColors: + - '#ff1414' + - '#00a080' maxStickyNoteLength: 1000 # limit number of annotations per slide maxNumberOfAnnotations: 300 From acdd906ff62a3d48156d6ce67222742b412c458f Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Tue, 18 Aug 2026 22:13:55 +0900 Subject: [PATCH 80/88] [general setting of laser color] fix mistakes --- .../imports/ui/components/whiteboard/component.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 6aeefc2fd4b9..9241fa851e13 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2064,7 +2064,6 @@ const Whiteboard = React.memo((props) => { const svgToDataUrl = (svg) => `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`; - const svgToCursor = (svg, x, y) => `url("${svgToDataUrl(svg)}") ${x} ${y}, auto`; @@ -2926,7 +2925,6 @@ const Whiteboard = React.memo((props) => { setLaserMenuVisible(false); }} > - {label} Date: Wed, 19 Aug 2026 00:36:54 +0900 Subject: [PATCH 81/88] [chore] refactor zoom --- .../imports/ui/components/whiteboard/component.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 9241fa851e13..458dd57a6b11 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2565,7 +2565,8 @@ const Whiteboard = React.memo((props) => { //if (!cursorEl) return; //const zoom = parseFloat(getComputedStyle(tlContainer).getPropertyValue('--tl-zoom')) || 1; - const { z: zoom } = tlEditorRef.current ? tlEditorRef.current.getCamera() : 1; + //const { z: zoom } = tlEditorRef.current ? tlEditorRef.current.getCamera() : 1; + const zoom = tlEditorRef.current?.getCamera()?.z ?? 1; //const transform = cursorEl.style.transform; //if (!transform) return; From 293f854286474d03145aca91930c7dbd3616b46b Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Wed, 19 Aug 2026 01:09:22 +0900 Subject: [PATCH 82/88] [remove class="tl-collaborator__cursor-hint"] Remove that appears typically when the cursor moves near the right or the bottom edge of the viewer's screen. We do not need it when the laser is shown. --- .../imports/ui/components/whiteboard/component.jsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 458dd57a6b11..7f90c4af8770 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2534,6 +2534,12 @@ const Whiteboard = React.memo((props) => { const laserKey = presenterCursor?.laserType; const laserDef = laserDefs[laserKey]; + if (laserDef) { + tlContainer?.classList.add('bbb-laser-active'); + } else { + tlContainer?.classList.remove('bbb-laser-active'); + } + const changed = laserKey !== currentLaserTypeRef.current; if (changed) { currentLaserTypeRef.current = laserKey; From e13b572c69ac9123ebf9a5f5186a40f6346dbd23 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Wed, 19 Aug 2026 01:11:38 +0900 Subject: [PATCH 83/88] [remove class="tl-collaborator__cursor-hint"] @ styles.js --- .../imports/ui/components/whiteboard/styles.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/styles.js b/bigbluebutton-html5/imports/ui/components/whiteboard/styles.js index d92f33c4e55b..3ac5ddf93553 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/styles.js +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/styles.js @@ -191,6 +191,12 @@ const TldrawV2GlobalStyle = createGlobalStyle` display: flex; } + .tl-container.bbb-laser-active + .tl-collaborator__cursor-hint + use[href='#cursor_hint'][color='#FF0000'] { + display: none; + } + ${({ presentationHeight }) => { const minRange = { height: 345, top: 14 }; const maxRange = { height: 1200, top: 384 }; From 6ad6833331fefe5addbf729eb80be0aabe207fa0 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Wed, 19 Aug 2026 01:27:05 +0900 Subject: [PATCH 84/88] Add comment for removing tl-collaborator__cursor-hint --- .../imports/ui/components/whiteboard/component.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 7f90c4af8770..3c451b50c2d8 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2534,6 +2534,7 @@ const Whiteboard = React.memo((props) => { const laserKey = presenterCursor?.laserType; const laserDef = laserDefs[laserKey]; + // Hide if (laserDef) { tlContainer?.classList.add('bbb-laser-active'); } else { From ee079a9d97ee2e5a782aeac83cc082356d949918 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:50:57 +0900 Subject: [PATCH 85/88] [Laser on viewer's screen only when the presenter shows it] whiteboard/component --- .../imports/ui/components/whiteboard/component.jsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 3c451b50c2d8..0e6d03f182b3 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -937,10 +937,15 @@ const Whiteboard = React.memo((props) => { const language = React.useMemo(() => mapLanguage(locale?.toLowerCase() || 'en'), [locale]); + const getLaserType = React.useCallback(() => { + const tool = tlEditorRef.current?.getCurrentToolId?.(); + return tool === 'hand' ? laserMode : ''; + }, [laserMode]); + const updateCursorPosition = useCursor( publishCursorUpdate, whiteboardIdRef.current, - laserMode, + getLaserType, ); const setCamera = (zoom, x = 0, y = 0) => { From 2d201b774cc34b47dc979e7de60a4938698f93af Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:55:21 +0900 Subject: [PATCH 86/88] [Laser on viewer's screen only when the presenter shows it] whiteboard/hook.js --- .../imports/ui/components/whiteboard/hooks.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/hooks.js b/bigbluebutton-html5/imports/ui/components/whiteboard/hooks.js index 7f7c2cf13ba5..dbf6edcd56cf 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/hooks.js +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/hooks.js @@ -7,16 +7,16 @@ const hasBackgroundImageUrl = (el) => { return bg.includes('url('); }; -const useCursor = (publishCursorUpdate, whiteboardId, laserMode) => { +const useCursor = (publishCursorUpdate, whiteboardId, getLaserType) => { const publishRef = React.useRef(publishCursorUpdate); const whiteboardIdRef = React.useRef(whiteboardId); - const laserModeRef = React.useRef(laserMode); + const getLaserTypeRef = React.useRef(getLaserType); const pendingRef = React.useRef(null); const rafRef = React.useRef(null); useEffect(() => { publishRef.current = publishCursorUpdate; }, [publishCursorUpdate]); useEffect(() => { whiteboardIdRef.current = whiteboardId; }, [whiteboardId]); - useEffect(() => { laserModeRef.current = laserMode; }, [laserMode]); + useEffect(() => { getLaserTypeRef.current = getLaserType; }, [getLaserType]); useEffect(() => () => { if (rafRef.current) { @@ -26,7 +26,7 @@ const useCursor = (publishCursorUpdate, whiteboardId, laserMode) => { publishRef.current({ whiteboardId: whiteboardIdRef.current, ...pendingRef.current, - laserType: laserModeRef.current, + laserType: getLaserTypeRef.current?.() ?? '' }); pendingRef.current = null; } @@ -43,7 +43,7 @@ const useCursor = (publishCursorUpdate, whiteboardId, laserMode) => { publishRef.current({ whiteboardId: whiteboardIdRef.current, ...pendingRef.current, - laserType: laserModeRef.current, + laserType: getLaserTypeRef.current?.() ?? '' }); pendingRef.current = null; } From d43a49ab3dc28dac5ff1f0d144621041e0ad2885 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Thu, 20 Aug 2026 11:07:30 +0900 Subject: [PATCH 87/88] [Watch the zoom value of viewer's cursor to quickly update the size] --- .../ui/components/whiteboard/component.jsx | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 0e6d03f182b3..be4b78c39049 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -214,6 +214,7 @@ const Whiteboard = React.memo((props) => { const laserMenuRef = React.useRef(null); const [laserMode, setLaserMode] = React.useState(''); const [presenterCursorPoint, setPresenterCursorPoint] = React.useState( { x: -1, y: -1} ); + const [viewerLaserZoom, setViewerLaserZoom] = React.useState(1); if (isMounting) { setDefaultEditorAssetUrls(getCustomEditorAssetUrls()); @@ -2511,6 +2512,29 @@ const Whiteboard = React.memo((props) => { lasers.forEach(el => el.remove()); }; + React.useEffect(() => { + if (isPresenter) return undefined; + + const editor = tlEditorRef.current; + if (!editor) return undefined; + + let previousZoom = editor.getCamera().z; + setViewerLaserZoom(previousZoom); + + const unlisten = editor.store.listen(() => { + const zoom = editor.getCamera().z; + + if (zoom === previousZoom) return; + + previousZoom = zoom; + setViewerLaserZoom(zoom); + }); + + return () => { + unlisten?.(); + }; + }, [isPresenter, tlEditorRef.current]); + // Show viewers Laser SVG React.useEffect(() => { if (isPresenter) return; @@ -2578,7 +2602,7 @@ const Whiteboard = React.memo((props) => { //const zoom = parseFloat(getComputedStyle(tlContainer).getPropertyValue('--tl-zoom')) || 1; //const { z: zoom } = tlEditorRef.current ? tlEditorRef.current.getCamera() : 1; - const zoom = tlEditorRef.current?.getCamera()?.z ?? 1; + //const zoom = tlEditorRef.current?.getCamera()?.z ?? 1; //const transform = cursorEl.style.transform; //if (!transform) return; @@ -2597,10 +2621,10 @@ const Whiteboard = React.memo((props) => { // similar to the pointer of the presenter (CSS-based) or the one in the real world. laserEl.style.transform = ` translate(${x - laserDef.cx}px, ${y - laserDef.cy}px) - scale(${1/zoom}) + scale(${1/viewerLaserZoom}) `; return; - }, [otherCursors, isPresenter]); + }, [otherCursors, isPresenter, viewerLaserZoom]); React.useEffect(() => { removeViewerLaser(); From ab5d493119d59b337ec56ffaed82fa247c1f57f8 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Fri, 21 Aug 2026 00:56:14 +0900 Subject: [PATCH 88/88] [use getWhiteboardDocument for targetWin] whiteboard/component This change made the PR portable and easily mergeable to the popup PR. --- .../ui/components/whiteboard/component.jsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index be4b78c39049..3f22ae352050 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -263,6 +263,10 @@ const Whiteboard = React.memo((props) => { currentUserRef.current = currentUser; + const getWhiteboardDocument = () => ( + whiteboardRef.current?.ownerDocument || document + ); + const [pageZoomMap, setPageZoomMap] = useState(() => { try { const saved = localStorage.getItem('pageZoomMap'); @@ -2299,7 +2303,7 @@ const Whiteboard = React.memo((props) => { }, [currentPresentationPage, isPresenter, viewerCanPan]); React.useEffect(() => { - const targetDoc = document; + const targetDoc = getWhiteboardDocument(); const presentationWrapper = targetDoc.querySelector('#presentationInnerWrapper'); if (!presentationWrapper) return; if (!isPresenter) return; @@ -2355,7 +2359,7 @@ const Whiteboard = React.memo((props) => { React.useEffect(() => { if (!laserMenuVisible) return; - const targetDoc = document; + const targetDoc = getWhiteboardDocument(); const presentationWrapper = targetDoc.querySelector('#presentationInnerWrapper'); if (!presentationWrapper) return; @@ -2374,7 +2378,7 @@ const Whiteboard = React.memo((props) => { React.useEffect(() => { // compensation at the window edge if (!laserMenuVisible) return; - const targetDoc = document; + const targetDoc = getWhiteboardDocument(); const el = laserMenuRef.current; if (!el) return; @@ -2395,7 +2399,7 @@ const Whiteboard = React.memo((props) => { }, [laserMenuVisible]); React.useEffect(() => { - const targetDoc = document; + const targetDoc = getWhiteboardDocument(); if (!isPresenter) return; const el = targetDoc.querySelector('.tl-container'); if (!el) return; @@ -2507,7 +2511,7 @@ const Whiteboard = React.memo((props) => { const removeViewerLaser = () => { laserElRef.current = null; - const targetDoc = document; + const targetDoc = getWhiteboardDocument(); const lasers = targetDoc.querySelectorAll('.bbb-laser-pointer'); lasers.forEach(el => el.remove()); }; @@ -2540,7 +2544,7 @@ const Whiteboard = React.memo((props) => { if (isPresenter) return; //if (isMultiUserActive) return; - const targetDoc = document; + const targetDoc = getWhiteboardDocument(); //Comment out below if we do not want to show laser when a presenter uses drawing tools on mobile devices. // Note a problem that the laser remains on the screen after switching to drawing tools.