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) => {