From 919ff6af9013b06e69a8298c432509754d120b93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Mon, 13 Jul 2026 17:19:20 -0300 Subject: [PATCH 01/24] grid-aware resize snapping in camera dock --- .../ui/components/webcam/component.tsx | 94 ++++++++++++++++++- 1 file changed, 89 insertions(+), 5 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/webcam/component.tsx b/bigbluebutton-html5/imports/ui/components/webcam/component.tsx index 9843c6dc7d5d..a944f86a5a7b 100644 --- a/bigbluebutton-html5/imports/ui/components/webcam/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/webcam/component.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { Resizable } from 're-resizable'; import Draggable, { DraggableEvent } from 'react-draggable'; @@ -31,6 +31,22 @@ const intlMessages = defineMessages({ }, }); +const CAMERA_DOCK_GRID_SNAP_TOLERANCE = 12; +const CAMERA_DOCK_GRID_SETTLE_DELAY = 100; + +const snapCameraDockDimensionToGrid = ( + dockSize: number, + gridSize: number | undefined, + minSize: number, + maxSize: number, +) => { + if (!gridSize || dockSize - gridSize <= CAMERA_DOCK_GRID_SNAP_TOLERANCE) { + return dockSize; + } + + return Math.min(Math.max(gridSize, minSize), maxSize); +}; + interface WebcamComponentProps { cameraDock: Output['cameraDock']; swapLayout: boolean; @@ -40,6 +56,7 @@ interface WebcamComponentProps { isPresenter: boolean; displayPresentation: boolean; cameraOptimalGridSize: Input['cameraDock']['cameraOptimalGridSize']; + snapToCameraGrid: boolean; isRTL: boolean; } @@ -52,6 +69,7 @@ const WebcamComponent: React.FC = ({ isPresenter, displayPresentation, cameraOptimalGridSize: cameraSize, + snapToCameraGrid, isRTL, }) => { const [isResizing, setIsResizing] = useState(false); @@ -60,8 +78,13 @@ const WebcamComponent: React.FC = ({ const [resizeStart, setResizeStart] = useState({ width: 0, height: 0 }); const [cameraMaxWidth, setCameraMaxWidth] = useState(0); const [draggedAtLeastOneTime, setDraggedAtLeastOneTime] = useState(false); + const cameraDockRef = useRef(cameraDock); + const cameraSizeRef = useRef(cameraSize); const intl = useIntl(); + cameraDockRef.current = cameraDock; + cameraSizeRef.current = cameraSize; + const lastSize = Storage.getItem('webcamSize') || { width: 0, height: 0 }; const { height: lastHeight } = lastSize as { width: number, height: number }; @@ -150,6 +173,51 @@ const WebcamComponent: React.FC = ({ } }; + const snapCameraDockToGrid = () => { + if (!snapToCameraGrid) return; + + const currentCameraDock = cameraDockRef.current; + const currentCameraSize = cameraSizeRef.current; + const isCurrentCameraTopOrBottom = currentCameraDock.position === CAMERADOCK_POSITION.CONTENT_TOP + || currentCameraDock.position === CAMERADOCK_POSITION.CONTENT_BOTTOM; + const isCurrentCameraLeftOrRight = currentCameraDock.position === CAMERADOCK_POSITION.CONTENT_LEFT + || currentCameraDock.position === CAMERADOCK_POSITION.CONTENT_RIGHT; + const isCurrentCameraSidebar = currentCameraDock.position + === CAMERADOCK_POSITION.SIDEBAR_CONTENT_BOTTOM; + const currentCameraMaxWidth = (isPresenter && currentCameraDock.presenterMaxWidth) + ? currentCameraDock.presenterMaxWidth + : currentCameraDock.maxWidth; + + const width = isCurrentCameraLeftOrRight + ? snapCameraDockDimensionToGrid( + currentCameraDock.width, + currentCameraSize?.width, + currentCameraDock.minWidth, + currentCameraMaxWidth, + ) + : currentCameraDock.width; + const height = (isCurrentCameraTopOrBottom || isCurrentCameraSidebar) + ? snapCameraDockDimensionToGrid( + currentCameraDock.height, + currentCameraSize?.height, + currentCameraDock.minHeight, + currentCameraDock.maxHeight, + ) + : currentCameraDock.height; + + if (width === currentCameraDock.width && height === currentCameraDock.height) return; + + layoutContextDispatch({ + type: ACTIONS.SET_CAMERA_DOCK_SIZE, + value: { + width, + height, + browserWidth: window.innerWidth, + browserHeight: window.innerHeight, + }, + }); + }; + const handleWebcamDragStart = () => { setIsDragging(true); document.body.style.overflow = 'hidden'; @@ -255,10 +323,23 @@ const WebcamComponent: React.FC = ({ onResizeStop={() => { setResizeStart({ width: 0, height: 0 }); setTimeout(() => setIsResizing(false), 500); - layoutContextDispatch({ - type: ACTIONS.SET_CAMERA_DOCK_IS_RESIZING, - value: false, - }); + const stopCameraDockResize = () => { + layoutContextDispatch({ + type: ACTIONS.SET_CAMERA_DOCK_IS_RESIZING, + value: false, + }); + }; + + if (snapToCameraGrid) { + // Let the throttled grid calculation observe the final pointer size before + // compacting it. This keeps larger row/column transitions reachable. + setTimeout(() => { + snapCameraDockToGrid(); + stopCameraDockResize(); + }, CAMERA_DOCK_GRID_SETTLE_DELAY); + } else { + stopCameraDockResize(); + } }} enable={{ top: !isFullScreen && !isDragging && !swapLayout && cameraDock?.resizableEdge?.top, @@ -343,6 +424,8 @@ const WebcamContainer: React.FC = () => { const { selectedLayout } = useSettings(SETTINGS.APPLICATION) as { selectedLayout: string }; const isVideoFocus = selectedLayout === LAYOUT_TYPE.VIDEO_FOCUS; const isUnifiedLayout = selectedLayout === LAYOUT_TYPE.UNIFIED_LAYOUT; + const snapToCameraGrid = selectedLayout === LAYOUT_TYPE.CUSTOM_LAYOUT + || selectedLayout === LAYOUT_TYPE.UNIFIED_LAYOUT; const isGridEnabled = isVideoFocus || (isUnifiedLayout && !presentationIsOpen); @@ -369,6 +452,7 @@ const WebcamContainer: React.FC = () => { focusedId: cameraDock.focusedId, cameraDock, cameraOptimalGridSize, + snapToCameraGrid, layoutContextDispatch, fullscreen, isPresenter: currentUserData?.presenter ?? false, From ae97e55497dd93b3984a64122148608f4f8dab39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Wed, 15 Jul 2026 16:40:42 -0300 Subject: [PATCH 02/24] fix clear timeout --- .../imports/ui/components/webcam/component.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/webcam/component.tsx b/bigbluebutton-html5/imports/ui/components/webcam/component.tsx index a944f86a5a7b..ef0167290e84 100644 --- a/bigbluebutton-html5/imports/ui/components/webcam/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/webcam/component.tsx @@ -80,6 +80,7 @@ const WebcamComponent: React.FC = ({ const [draggedAtLeastOneTime, setDraggedAtLeastOneTime] = useState(false); const cameraDockRef = useRef(cameraDock); const cameraSizeRef = useRef(cameraSize); + const cameraDockGridSettleTimeoutRef = useRef | null>(null); const intl = useIntl(); cameraDockRef.current = cameraDock; @@ -108,6 +109,12 @@ const WebcamComponent: React.FC = ({ }; }, []); + useEffect(() => () => { + if (cameraDockGridSettleTimeoutRef.current !== null) { + clearTimeout(cameraDockGridSettleTimeoutRef.current); + } + }, []); + useEffect(() => { setIsFullScreen(fullscreen.group === 'webcams'); }, [fullscreen]); @@ -309,6 +316,10 @@ const WebcamComponent: React.FC = ({ height: isDragging ? cameraSize?.height : cameraDock.height, }} onResizeStart={() => { + if (cameraDockGridSettleTimeoutRef.current !== null) { + clearTimeout(cameraDockGridSettleTimeoutRef.current); + cameraDockGridSettleTimeoutRef.current = null; + } setIsResizing(true); setResizeStart({ width: cameraDock.width, height: cameraDock.height }); onResizeHandle(cameraDock.width, cameraDock.height); @@ -333,7 +344,11 @@ const WebcamComponent: React.FC = ({ if (snapToCameraGrid) { // Let the throttled grid calculation observe the final pointer size before // compacting it. This keeps larger row/column transitions reachable. - setTimeout(() => { + if (cameraDockGridSettleTimeoutRef.current !== null) { + clearTimeout(cameraDockGridSettleTimeoutRef.current); + } + cameraDockGridSettleTimeoutRef.current = setTimeout(() => { + cameraDockGridSettleTimeoutRef.current = null; snapCameraDockToGrid(); stopCameraDockResize(); }, CAMERA_DOCK_GRID_SETTLE_DELAY); From 7065b5a147461accc0157a57d35c3e23bc1166cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Thu, 16 Jul 2026 15:39:45 -0300 Subject: [PATCH 03/24] fix resize when camera is on left or right of presentation --- .../ui/components/webcam/component.tsx | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/webcam/component.tsx b/bigbluebutton-html5/imports/ui/components/webcam/component.tsx index ef0167290e84..6562f910693e 100644 --- a/bigbluebutton-html5/imports/ui/components/webcam/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/webcam/component.tsx @@ -187,23 +187,8 @@ const WebcamComponent: React.FC = ({ const currentCameraSize = cameraSizeRef.current; const isCurrentCameraTopOrBottom = currentCameraDock.position === CAMERADOCK_POSITION.CONTENT_TOP || currentCameraDock.position === CAMERADOCK_POSITION.CONTENT_BOTTOM; - const isCurrentCameraLeftOrRight = currentCameraDock.position === CAMERADOCK_POSITION.CONTENT_LEFT - || currentCameraDock.position === CAMERADOCK_POSITION.CONTENT_RIGHT; - const isCurrentCameraSidebar = currentCameraDock.position - === CAMERADOCK_POSITION.SIDEBAR_CONTENT_BOTTOM; - const currentCameraMaxWidth = (isPresenter && currentCameraDock.presenterMaxWidth) - ? currentCameraDock.presenterMaxWidth - : currentCameraDock.maxWidth; - - const width = isCurrentCameraLeftOrRight - ? snapCameraDockDimensionToGrid( - currentCameraDock.width, - currentCameraSize?.width, - currentCameraDock.minWidth, - currentCameraMaxWidth, - ) - : currentCameraDock.width; - const height = (isCurrentCameraTopOrBottom || isCurrentCameraSidebar) + + const height = isCurrentCameraTopOrBottom ? snapCameraDockDimensionToGrid( currentCameraDock.height, currentCameraSize?.height, @@ -212,12 +197,12 @@ const WebcamComponent: React.FC = ({ ) : currentCameraDock.height; - if (width === currentCameraDock.width && height === currentCameraDock.height) return; + if (height === currentCameraDock.height) return; layoutContextDispatch({ type: ACTIONS.SET_CAMERA_DOCK_SIZE, value: { - width, + width: currentCameraDock.width, height, browserWidth: window.innerWidth, browserHeight: window.innerHeight, @@ -341,7 +326,7 @@ const WebcamComponent: React.FC = ({ }); }; - if (snapToCameraGrid) { + if (snapToCameraGrid && isCameraTopOrBottom) { // Let the throttled grid calculation observe the final pointer size before // compacting it. This keeps larger row/column transitions reachable. if (cameraDockGridSettleTimeoutRef.current !== null) { From 9ccf337bcccdcb7350ace3e994293cab8438d9af Mon Sep 17 00:00:00 2001 From: Claudio Promptoso Date: Fri, 17 Jul 2026 11:57:45 +0000 Subject: [PATCH 04/24] test: add BlockNote shared notes e2e tests (backport of #25165) Backport of #25165 (merged on v4.0.x-develop) to v3.0.x-develop, bringing the BlockNote shared-notes e2e coverage to the 3.0 line. Cherry-picked a9c0cd5438 with two conflicts resolved: - core/elements.ts: added the BlockNote selectors block at the shared-notes export insertion point. - sharednotes/blocknote/util.ts (add/add): merged the new 4.0 helpers (startSharedNotesBlockNote, getBlockNoteReadOnlyLocator) with the existing 3.0 helpers already present from #25225, keeping a single getBlockNoteEditorLocator. 3.0 adaptations in sharednotes.ts for element renames that only exist on 4.0: - sharedNotesSidebarButton to sharedNotes - messagesSidebarButton to chatButton - lock flow uses manageUsers + lockViewersButton (the 3.0 lock modal has no participant permissions tab) - make-presenter uses userListItem instead of usersListSidebarButton + moreOptionsUserItemButton The "Pin and unpin notes onto whiteboard" scenario is marked fixme on 3.0: a viewer whiteboard does not re-sync to the presenter presentation state after the presenter unpins shared notes (the canvas is not restored). That sync behavior only exists on 4.0. --- .../components/bn-shared-notes/component.tsx | 1 + .../playwright/core/elements.ts | 11 + .../playwright/core/setup/fixtures.ts | 24 +- .../sharednotes/blocknote/sharednotes.spec.ts | 71 +++- .../sharednotes/blocknote/sharednotes.ts | 329 +++++++++++++++++- .../playwright/sharednotes/blocknote/util.ts | 13 + .../sharednotes/etherpad/sharednotes.spec.ts | 45 ++- 7 files changed, 462 insertions(+), 32 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/bn-shared-notes/component.tsx b/bigbluebutton-html5/imports/ui/components/bn-shared-notes/component.tsx index d334ab42c463..d45ea9385b52 100644 --- a/bigbluebutton-html5/imports/ui/components/bn-shared-notes/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/bn-shared-notes/component.tsx @@ -502,6 +502,7 @@ function BlockNoteApp(props: BlockNoteAppProps): React.ReactElement { ref={toolbarRef} role="toolbar" className="bn-toolbar-row" + data-test="blockNoteToolbar" onKeyDown={(e) => { if (e.key === 'Escape') editor.focus(); }} > diff --git a/bigbluebutton-tests/playwright/core/elements.ts b/bigbluebutton-tests/playwright/core/elements.ts index 77b27a79cc72..e8f724f0cdea 100644 --- a/bigbluebutton-tests/playwright/core/elements.ts +++ b/bigbluebutton-tests/playwright/core/elements.ts @@ -255,6 +255,17 @@ export const elements = { unpinNotes: 'button[data-test="unpinNotes"]', exportetherpad: 'span[id="exportetherpad"]', exporthtml: 'span[id="exporthtml"]', + + // BlockNote specific + blockNoteContainer: '#bn-notes-scroll-container', + blockNoteEditor: '#bn-notes-scroll-container .bn-editor', + blockNoteEditable: '#bn-notes-scroll-container .bn-editor[contenteditable="true"]', + blockNoteReadOnly: '#bn-notes-scroll-container .bn-editor[contenteditable="false"]', + blockNoteToolbar: 'div[data-test="blockNoteToolbar"]', + blockNoteUnderlineButton: 'div[data-test="blockNoteToolbar"] button[aria-label="Underline"]', + notesConnectionError: '[data-test="notesError"]', + notesRetryButton: 'button[data-test="notesRetryButton"]', + // Notifications smallToastMsg: 'div[data-test="toastSmallMsg"]', closeToastBtn: 'i[data-test="closeToastBtn"]', diff --git a/bigbluebutton-tests/playwright/core/setup/fixtures.ts b/bigbluebutton-tests/playwright/core/setup/fixtures.ts index 075826f57d98..f99d0149b0e1 100644 --- a/bigbluebutton-tests/playwright/core/setup/fixtures.ts +++ b/bigbluebutton-tests/playwright/core/setup/fixtures.ts @@ -1,4 +1,4 @@ -import { test as base } from '@playwright/test'; +import { test as base, type Video } from '@playwright/test'; interface TestFixtures { sharedBeforeEachTestHook: void; @@ -6,12 +6,30 @@ interface TestFixtures { const testWithValidation = base.extend({ sharedBeforeEachTestHook: [ - async ({ browser }, use) => { + async ({ browser }, use, testInfo) => { // Before test await use(); - // After test + // After test — collect video refs before closing (videos finalize on context close) const contexts = browser.contexts(); + const videos: Video[] = []; + for (const ctx of contexts) { + for (const pg of ctx.pages()) { + const v = pg.video(); + if (v) videos.push(v); + } + } await Promise.all(contexts.map((context) => context.close())); + // Only attach videos for failed/timed-out tests to avoid bloating CI artifacts on passing runs + if (testInfo.status !== 'passed') { + for (let i = 0; i < videos.length; i++) { + try { + const videoPath = await videos[i].path(); + await testInfo.attach(`video-${i + 1}`, { path: videoPath, contentType: 'video/webm' }); + } catch { + // skip if video file unavailable + } + } + } }, { scope: 'test', auto: true }, ], diff --git a/bigbluebutton-tests/playwright/sharednotes/blocknote/sharednotes.spec.ts b/bigbluebutton-tests/playwright/sharednotes/blocknote/sharednotes.spec.ts index f843a6475cba..6cbe490c8bac 100644 --- a/bigbluebutton-tests/playwright/sharednotes/blocknote/sharednotes.spec.ts +++ b/bigbluebutton-tests/playwright/sharednotes/blocknote/sharednotes.spec.ts @@ -2,19 +2,72 @@ import { initializePages, linkIssue } from '../../core/helpers'; import { test } from '../../core/setup/fixtures'; import { BlockNoteSharedNotes } from './sharednotes'; -test.describe('Shared Notes - BlockNote', { tag: '@ci' }, () => { - let sharedNotes: BlockNoteSharedNotes; +const CREATE_PARAMETER = 'sharedNotesEditor=blockNote'; - test.beforeEach(async ({ browser, context }, testInfo) => { - sharedNotes = new BlockNoteSharedNotes(browser, context); - await initializePages(sharedNotes, browser, { - createParameter: 'sharedNotesEditor=blockNote', - testInfo, - }); +test.describe.parallel('Shared Notes - BlockNote', { tag: '@ci' }, () => { + test('Open shared notes', async ({ browser, context }, testInfo) => { + const sharedNotes = new BlockNoteSharedNotes(browser, context); + await initializePages(sharedNotes, browser, { isMultiUser: true, createParameter: CREATE_PARAMETER, testInfo }); + await sharedNotes.openSharedNotes(); }); - test('Export empty shared notes as PDF returns a PDF, not an error', async () => { + test('Type in shared notes', async ({ browser, context }, testInfo) => { + const sharedNotes = new BlockNoteSharedNotes(browser, context); + await initializePages(sharedNotes, browser, { isMultiUser: true, createParameter: CREATE_PARAMETER, testInfo }); + await sharedNotes.typeInSharedNotes(); + }); + + test('Format text in shared notes', async ({ browser, context }, testInfo) => { + const sharedNotes = new BlockNoteSharedNotes(browser, context); + await initializePages(sharedNotes, browser, { isMultiUser: true, createParameter: CREATE_PARAMETER, testInfo }); + await sharedNotes.formatTextInSharedNotes(); + }); + + test('Export shared notes as PDF', async ({ browser, context, browserName }, testInfo) => { + test.skip(browserName === 'firefox', 'window.open popup handling differs on Firefox'); + const sharedNotes = new BlockNoteSharedNotes(browser, context); + await initializePages(sharedNotes, browser, { isMultiUser: true, createParameter: CREATE_PARAMETER, testInfo }); + await sharedNotes.exportSharedNotesAsPDF(); + }); + + test('Export empty shared notes as PDF returns a PDF, not an error', async ({ browser, context }, testInfo) => { linkIssue(25122); + const sharedNotes = new BlockNoteSharedNotes(browser, context); + await initializePages(sharedNotes, browser, { createParameter: CREATE_PARAMETER, testInfo }); await sharedNotes.exportEmptyNotesAsPDF(); }); + + test('Convert notes to presentation', async ({ browser, context }, testInfo) => { + const sharedNotes = new BlockNoteSharedNotes(browser, context); + await initializePages(sharedNotes, browser, { isMultiUser: true, createParameter: CREATE_PARAMETER, testInfo }); + await sharedNotes.convertNotesToWhiteboard(); + }); + + test('Multiusers edit', async ({ browser, context }, testInfo) => { + const sharedNotes = new BlockNoteSharedNotes(browser, context); + await initializePages(sharedNotes, browser, { isMultiUser: true, createParameter: CREATE_PARAMETER, testInfo }); + await sharedNotes.editSharedNotesWithMoreThanOneUser(); + }); + + test('See notes without edit permission', async ({ browser, context }, testInfo) => { + const sharedNotes = new BlockNoteSharedNotes(browser, context); + await initializePages(sharedNotes, browser, { isMultiUser: true, createParameter: CREATE_PARAMETER, testInfo }); + await sharedNotes.seeNotesWithoutEditPermission(); + }); + + // different failures in CI and local + // local: not able to click on "unpin" button + // CI: not restoring presentation for viewer after unpinning notes + test('Pin and unpin notes onto whiteboard', async ({ browser, context, browserName }, testInfo) => { + test.skip(browserName === 'firefox', 'Webcams does not work properly, due to heavy firefox for testing'); + // On BBB 3.0 the viewer whiteboard is not restored to the presenter presentation state after the + // presenter unpins the shared notes (observed consistently across 3 runs; the 4.0 suite passes the + // same scenario). Whether this is a genuine 3.0 sync gap or a test adaptation issue is not yet + // determined, so the scenario is kept as fixme in the 3.0 backport of #25165 rather than reported + // as a passing case. The make-presenter / second-unpin path below is therefore not exercised here. + test.fixme(true, 'BBB 3.0 viewer presentation is not restored after the presenter unpins shared notes (observed, root cause undetermined)'); + const sharedNotes = new BlockNoteSharedNotes(browser, context); + await initializePages(sharedNotes, browser, { isMultiUser: true, createParameter: CREATE_PARAMETER, testInfo }); + await sharedNotes.pinAndUnpinNotesOntoWhiteboard(); + }); }); diff --git a/bigbluebutton-tests/playwright/sharednotes/blocknote/sharednotes.ts b/bigbluebutton-tests/playwright/sharednotes/blocknote/sharednotes.ts index b89db663a018..38f129976917 100644 --- a/bigbluebutton-tests/playwright/sharednotes/blocknote/sharednotes.ts +++ b/bigbluebutton-tests/playwright/sharednotes/blocknote/sharednotes.ts @@ -1,10 +1,122 @@ import { expect, Response } from '@playwright/test'; -import { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } from '../../core/constants'; +import { ELEMENT_WAIT_EXTRA_LONG_TIME, ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } from '../../core/constants'; import { elements as e } from '../../core/elements'; import { MultiUsers } from '../../user/multiusers'; +import { getBlockNoteEditorLocator, getBlockNoteReadOnlyLocator, startSharedNotesBlockNote } from './util'; export class BlockNoteSharedNotes extends MultiUsers { + async openSharedNotes() { + const { sharedNotesEnabled } = this.modPage.settings || {}; + + if (!sharedNotesEnabled) { + await this.modPage.hasElement(e.chatButton, 'should display the public chat button'); + await this.modPage.wasRemoved(e.sharedNotes, 'should not display the shared notes button'); + return; + } + await startSharedNotesBlockNote(this.modPage); + const editorLocator = getBlockNoteEditorLocator(this.modPage); + await expect(editorLocator, 'should display the BlockNote editor in editable mode').toBeVisible({ + timeout: ELEMENT_WAIT_TIME, + }); + + await this.modPage.waitAndClick(e.hideNotesLabel); + await this.modPage.wasRemoved(e.hideNotesLabel, 'should not display the hide notes label'); + } + + async typeInSharedNotes() { + const { sharedNotesEnabled } = this.modPage.settings || {}; + + if (!sharedNotesEnabled) { + await this.modPage.hasElement(e.chatButton, 'should display the public chat button'); + await this.modPage.wasRemoved(e.sharedNotes, 'should not display the shared notes button'); + return; + } + await startSharedNotesBlockNote(this.modPage); + const editorLocator = getBlockNoteEditorLocator(this.modPage); + await editorLocator.click(); + await editorLocator.pressSequentially(e.message); + await expect(editorLocator, 'should contain the typed text on shared notes').toContainText(e.message, { + timeout: ELEMENT_WAIT_TIME, + }); + + await editorLocator.press('Control+Z'); + await editorLocator.press('Control+Z'); + await editorLocator.press('Control+Z'); + + await this.modPage.waitAndClick(e.hideNotesLabel); + await this.modPage.wasRemoved(e.hideNotesLabel, 'should not display the hide notes label'); + } + + async formatTextInSharedNotes() { + const { sharedNotesEnabled } = this.modPage.settings || {}; + + if (!sharedNotesEnabled) { + await this.modPage.hasElement(e.chatButton, 'should display the public chat button'); + await this.modPage.wasRemoved(e.sharedNotes, 'should not display the shared notes button'); + return; + } + await startSharedNotesBlockNote(this.modPage); + const editorLocator = getBlockNoteEditorLocator(this.modPage); + await editorLocator.click(); + await editorLocator.pressSequentially(e.message); + + await editorLocator.press('Control+Z'); + await expect(editorLocator, 'should not contain any text after undoing').not.toContainText(e.message, { + timeout: ELEMENT_WAIT_TIME, + }); + // Re-type so we have content to format (Y.js collaborative redo is not reliable in tests) + await editorLocator.pressSequentially(e.message); + await expect(editorLocator, 'should contain the message again after re-typing').toContainText(e.message, { + timeout: ELEMENT_WAIT_TIME, + }); + + await this.formatBlockNoteMessage(); + const html = await editorLocator.innerHTML(); + + await expect(html.includes(''), 'should include underline formatting').toBeTruthy(); + await expect(html.includes(''), 'should include bold formatting').toBeTruthy(); + await expect(html.includes(''), 'should include italic formatting').toBeTruthy(); + + await editorLocator.press('Control+Z'); + await editorLocator.press('Control+Z'); + await editorLocator.press('Control+Z'); + + await this.modPage.waitAndClick(e.hideNotesLabel); + await this.modPage.wasRemoved(e.hideNotesLabel, 'should not display the hide notes label'); + } + + async exportSharedNotesAsPDF() { + const { sharedNotesEnabled } = this.modPage.settings || {}; + + if (!sharedNotesEnabled) { + await this.modPage.hasElement(e.chatButton, 'should display the public chat button'); + await this.modPage.wasRemoved(e.sharedNotes, 'should not display the shared notes button'); + return; + } + await startSharedNotesBlockNote(this.modPage); + const editorLocator = getBlockNoteEditorLocator(this.modPage); + await editorLocator.click(); + await editorLocator.pressSequentially(e.message); + + await this.modPage.waitAndClick(e.notesOptions); + await this.modPage.hasElement(e.exportNotesAsPDF, 'should display the export as PDF option'); + + // Intercept the outgoing request — the server responds with Content-Disposition: attachment + // so the popup tab never navigates; checking the request URL is the reliable approach. + const [request] = await Promise.all([ + this.modPage.page.context().waitForEvent('request', { + predicate: (req) => req.url().includes('/hocuspocus/api/documents/') && req.url().includes('/export/pdf'), + timeout: ELEMENT_WAIT_EXTRA_LONG_TIME, + }), + this.modPage.waitAndClick(e.exportNotesAsPDF), + ]); + await expect(request.url(), 'should request the PDF export endpoint').toContain('/export/pdf'); + + await this.modPage.waitAndClick(e.hideNotesLabel); + await this.modPage.wasRemoved(e.hideNotesLabel, 'should not display the hide notes label'); + } + // Regression for https://github.com/bigbluebutton/bigbluebutton/issues/25122: // exporting an empty BlockNote shared note must return a file, not an error // page ("Export failed: Document is empty..."). @@ -76,4 +188,219 @@ export class BlockNoteSharedNotes extends MultiUsers { if (body) body(await response.text()); } } + + async convertNotesToWhiteboard() { + const { sharedNotesEnabled } = this.modPage.settings || {}; + + if (!sharedNotesEnabled) { + await this.modPage.hasElement(e.chatButton, 'should display the public chat button'); + await this.modPage.wasRemoved(e.sharedNotes, 'should not display the shared notes button'); + return; + } + await startSharedNotesBlockNote(this.modPage); + const editorLocator = getBlockNoteEditorLocator(this.modPage); + await editorLocator.click(); + await editorLocator.pressSequentially('test'); + await expect(editorLocator, 'should register the typed text before converting to whiteboard').toContainText( + 'test', + { timeout: ELEMENT_WAIT_TIME }, + ); + + await this.modPage.waitAndClick(e.notesOptions); + await this.modPage.waitAndClick(e.sendNotesToWhiteboard); + + await this.modPage.hasText( + e.currentSlideText, + /test/, + 'should the slide contain the text "test" for the moderator', + 30000, + ); + await this.userPage.hasText( + e.currentSlideText, + /test/, + 'should the slide contain the text "test" for the attendee', + 20000, + ); + + await editorLocator.press('Control+Z'); + + await this.modPage.waitAndClick(e.hideNotesLabel); + await this.modPage.wasRemoved(e.hideNotesLabel, 'should not display the hide notes label button'); + } + + async editSharedNotesWithMoreThanOneUser() { + const { sharedNotesEnabled } = this.modPage.settings || {}; + + if (!sharedNotesEnabled) { + await this.modPage.hasElement(e.chatButton, 'should display the public chat button'); + await this.modPage.wasRemoved(e.sharedNotes, 'should not display the shared notes button'); + return; + } + // Open notes for both users before any typing so Hocuspocus registers both sessions + await startSharedNotesBlockNote(this.userPage); + const userEditorLocator = getBlockNoteEditorLocator(this.userPage); + + await startSharedNotesBlockNote(this.modPage); + const modEditorLocator = getBlockNoteEditorLocator(this.modPage); + await modEditorLocator.click(); + await modEditorLocator.pressSequentially('Hello'); + + // user waits for mod's text to sync, then selects all and replaces + await expect(userEditorLocator, 'should sync mod content to user before editing').toContainText('Hello', { + timeout: ELEMENT_WAIT_TIME, + }); + await userEditorLocator.click(); + await userEditorLocator.press('Control+A'); + await userEditorLocator.pressSequentially('Jello'); + + await expect(modEditorLocator, 'should the shared notes contain the text "Jello" for the moderator').toContainText( + /Jello/, + { timeout: ELEMENT_WAIT_TIME }, + ); + await expect(userEditorLocator, 'should the shared notes contain the text "Jello" for the attendee').toContainText( + /Jello/, + { timeout: ELEMENT_WAIT_TIME }, + ); + + await this.modPage.waitAndClick(e.hideNotesLabel); + await this.modPage.wasRemoved(e.hideNotesLabel, 'should not display the hide notes button for the moderator'); + await this.userPage.waitAndClick(e.hideNotesLabel); + await this.userPage.wasRemoved(e.hideNotesLabel, 'should not display the hide notes button for the attendee'); + } + + async seeNotesWithoutEditPermission() { + const { sharedNotesEnabled } = this.modPage.settings || {}; + + if (!sharedNotesEnabled) { + await this.modPage.hasElement(e.chatButton, 'should display the public chat button'); + await this.modPage.wasRemoved(e.sharedNotes, 'should not display the shared notes button'); + return; + } + // type on shared notes as moderator + await startSharedNotesBlockNote(this.modPage); + const modEditorLocator = getBlockNoteEditorLocator(this.modPage); + await modEditorLocator.click(); + await modEditorLocator.pressSequentially('Hello'); + + // open user notes to join the Hocuspocus session + await startSharedNotesBlockNote(this.userPage); + + // lock shared notes editing for viewers + await this.modPage.waitAndClick(e.manageUsers); + await this.modPage.waitAndClick(e.lockViewersButton); + await this.modPage.waitAndClickElement(e.lockEditSharedNotes); + await this.modPage.waitAndClick(e.applyLockSettings); + + // attendee's editor should become read-only and still show content + const userReadOnlyLocator = getBlockNoteReadOnlyLocator(this.userPage); + await expect( + userReadOnlyLocator, + 'should display the text "Hello" in read-only mode for the attendee', + ).toContainText(/Hello/, { timeout: 20000 }); + await this.userPage.wasRemoved( + e.blockNoteToolbar, + 'should not display the BlockNote toolbar when shared notes are locked for editing', + ); + } + + async pinAndUnpinNotesOntoWhiteboard() { + const { sharedNotesEnabled } = this.modPage.settings || {}; + + if (!sharedNotesEnabled) { + await this.modPage.hasElement(e.chatButton, 'should display the public chat button'); + await this.modPage.wasRemoved(e.sharedNotes, 'should not display the shared notes button'); + return; + } + await this.modPage.waitForSelector(e.whiteboard); + await this.userPage.waitForSelector(e.whiteboard); + // user minimize presentation + await this.userPage.waitAndClick(e.minimizePresentation); + await this.userPage.hasElement( + e.restorePresentation, + 'should display the restore presentation button for the attendee', + ); + // type on shared notes as moderator + await startSharedNotesBlockNote(this.modPage); + const editorLocator = getBlockNoteEditorLocator(this.modPage); + await editorLocator.click(); + await editorLocator.pressSequentially('Hello'); + await expect(editorLocator, 'should register the typed text before pinning').toContainText(/Hello/, { + timeout: ELEMENT_WAIT_TIME, + }); + // pin notes + await this.modPage.waitAndClick(e.notesOptions); + await this.modPage.waitAndClick(e.pinNotes); + await this.modPage.hasElement(e.unpinNotes, 'should display the unpin notes button'); + await this.userPage.hasElement( + e.minimizePresentation, + 'should display the minimize presentation button for the attendee', + ); + // check text content on pinned shared notes + const userEditorLocator = getBlockNoteEditorLocator(this.userPage); + await expect(editorLocator, 'should display the text "Hello" on the shared notes for the moderator').toContainText( + /Hello/, + { timeout: 20000 }, + ); + await expect( + userEditorLocator, + 'should display the text "Hello" on the shared notes for the attendee', + ).toContainText(/Hello/); + // unpin notes + await this.modPage.closeAllToastNotifications(); + await this.modPage.waitAndClick(e.unpinNotes); + await this.modPage.hasElement(e.whiteboard, 'should restore the presentation for the moderator (previous state)'); + await this.userPage.hasElement( + e.whiteboard, + 'should restore the presentation for the attendee as it syncs to presenter state', + ); + // pin notes again as moderator + await startSharedNotesBlockNote(this.modPage); + await this.modPage.waitAndClick(e.notesOptions); + await this.modPage.waitAndClick(e.pinNotes); + await this.modPage.hasElement( + e.unpinNotes, + 'should display the unpin notes button for the moderator after pinning the notes again', + ); + // make viewer as presenter and unpin notes + await this.modPage.waitAndClick(e.userListItem); + await this.modPage.waitAndClick(e.makePresenter); + await this.userPage.closeAllToastNotifications(); + await this.userPage.waitAndClick(e.unpinNotes); + await this.userPage.hasElement(e.whiteboard, 'should restore the presentation for the attendee (previous state)'); + await this.modPage.hasElement(e.whiteboard, 'should restore the presentation for the moderator (previous state)'); + } + + async formatBlockNoteMessage() { + // U for '!' — BlockNote has no Ctrl+U shortcut; click the toolbar button instead. + // The static toolbar uses e.preventDefault() on mousedown to preserve selection. + await this.modPage.down('Shift'); + await this.modPage.press('ArrowLeft'); + await this.modPage.up('Shift'); + await this.modPage.page.locator(e.blockNoteUnderlineButton).click(); + await this.modPage.press('ArrowLeft'); + + // B for 'World' + await this.modPage.down('Shift'); + let i = 5; + while (i > 0) { + await this.modPage.press('ArrowLeft'); + i--; + } + await this.modPage.up('Shift'); + await this.modPage.press('Control+B'); + await this.modPage.press('ArrowLeft'); + + await this.modPage.press('ArrowLeft'); + + // I for 'Hello' + await this.modPage.down('Shift'); + i = 5; + while (i > 0) { + await this.modPage.press('ArrowLeft'); + i--; + } + await this.modPage.up('Shift'); + await this.modPage.press('Control+I'); + await this.modPage.press('ArrowLeft'); + } } diff --git a/bigbluebutton-tests/playwright/sharednotes/blocknote/util.ts b/bigbluebutton-tests/playwright/sharednotes/blocknote/util.ts index 2890674d6bf8..4679c1ee818f 100644 --- a/bigbluebutton-tests/playwright/sharednotes/blocknote/util.ts +++ b/bigbluebutton-tests/playwright/sharednotes/blocknote/util.ts @@ -71,3 +71,16 @@ export function readLinkAndCursorState(testPage: Page) { { sel: BLOCKNOTE_EDITOR, wordJoiner: WORD_JOINER }, ); } + +// Helpers backported from #25165. On 3.0 the shared-notes sidebar button carries +// data-test="sharedNotes" (renamed to sharedNotesSidebarButton on 4.0), so this +// helper opens the panel via e.sharedNotes to match the 3.0 client. +export async function startSharedNotesBlockNote(testPage: Page) { + await testPage.waitAndClick(e.sharedNotes); + await testPage.waitForSelector(e.hideNotesLabel, ELEMENT_WAIT_LONGER_TIME); + await testPage.hasElement(e.blockNoteEditable, 'should display the BlockNote editor', ELEMENT_WAIT_LONGER_TIME); +} + +export function getBlockNoteReadOnlyLocator(testPage: Page) { + return testPage.page.locator(e.blockNoteReadOnly); +} diff --git a/bigbluebutton-tests/playwright/sharednotes/etherpad/sharednotes.spec.ts b/bigbluebutton-tests/playwright/sharednotes/etherpad/sharednotes.spec.ts index 79bf5d423ac3..326816b91661 100644 --- a/bigbluebutton-tests/playwright/sharednotes/etherpad/sharednotes.spec.ts +++ b/bigbluebutton-tests/playwright/sharednotes/etherpad/sharednotes.spec.ts @@ -2,52 +2,59 @@ import { initializePages } from '../../core/helpers'; import { test } from '../../core/setup/fixtures'; import { SharedNotes } from './sharednotes'; -test.describe.parallel('Shared Notes - Etherpad', { tag: '@ci' }, () => { - let sharedNotes: SharedNotes; - - test.beforeEach(async ({ browser, context }, testInfo) => { - sharedNotes = new SharedNotes(browser, context); - await initializePages(sharedNotes, browser, { - isMultiUser: true, - createParameter: 'sharedNotesEditor=etherpad', - testInfo, - }); - }); +const CREATE_PARAMETER = 'sharedNotesEditor=etherpad'; - test('Open shared notes', async () => { +test.describe.parallel('Shared Notes - Etherpad', { tag: '@ci' }, () => { + test('Open shared notes', async ({ browser, context }, testInfo) => { + const sharedNotes = new SharedNotes(browser, context); + await initializePages(sharedNotes, browser, { isMultiUser: true, createParameter: CREATE_PARAMETER, testInfo }); await sharedNotes.openSharedNotes(); }); - test('Type in shared notes', async ({ browserName }) => { + test('Type in shared notes', async ({ browser, context, browserName }, testInfo) => { test.skip(browserName === 'firefox', 'Firefox has different fonts on local and ci'); + const sharedNotes = new SharedNotes(browser, context); + await initializePages(sharedNotes, browser, { isMultiUser: true, createParameter: CREATE_PARAMETER, testInfo }); await sharedNotes.typeInSharedNotes(); }); - test('Formate text in shared notes', async () => { + test('Format text in shared notes', async ({ browser, context }, testInfo) => { + const sharedNotes = new SharedNotes(browser, context); + await initializePages(sharedNotes, browser, { isMultiUser: true, createParameter: CREATE_PARAMETER, testInfo }); await sharedNotes.formatTextInSharedNotes(); }); - test('Export shared notes', async () => { + test('Export shared notes', async ({ browser, context }, testInfo) => { + const sharedNotes = new SharedNotes(browser, context); + await initializePages(sharedNotes, browser, { isMultiUser: true, createParameter: CREATE_PARAMETER, testInfo }); await sharedNotes.exportSharedNotes(); }); - test('Convert notes to presentation', async () => { + test('Convert notes to presentation', async ({ browser, context }, testInfo) => { + const sharedNotes = new SharedNotes(browser, context); + await initializePages(sharedNotes, browser, { isMultiUser: true, createParameter: CREATE_PARAMETER, testInfo }); await sharedNotes.convertNotesToWhiteboard(); }); - test('Multiusers edit', async () => { + test('Multiusers edit', async ({ browser, context }, testInfo) => { + const sharedNotes = new SharedNotes(browser, context); + await initializePages(sharedNotes, browser, { isMultiUser: true, createParameter: CREATE_PARAMETER, testInfo }); await sharedNotes.editSharedNotesWithMoreThanOneUSer(); }); - test('See notes without edit permission', async () => { + test('See notes without edit permission', async ({ browser, context }, testInfo) => { + const sharedNotes = new SharedNotes(browser, context); + await initializePages(sharedNotes, browser, { isMultiUser: true, createParameter: CREATE_PARAMETER, testInfo }); await sharedNotes.seeNotesWithoutEditPermission(); }); // different failures in CI and local // local: not able to click on "unpin" button // CI: not restoring presentation for viewer after unpinning notes - test('Pin and unpin notes onto whiteboard', async ({ browserName }) => { + test('Pin and unpin notes onto whiteboard', async ({ browser, context, browserName }, testInfo) => { test.skip(browserName === 'firefox', 'Webcams does not work properly, due to heavy firefox for testing'); + const sharedNotes = new SharedNotes(browser, context); + await initializePages(sharedNotes, browser, { isMultiUser: true, createParameter: CREATE_PARAMETER, testInfo }); await sharedNotes.pinAndUnpinNotesOntoWhiteboard(); }); }); From b09778936d212f3c96b8f7d92936ee2fa25e1453 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:04:41 +0900 Subject: [PATCH 05/24] chore(client): fix children PropType in PresentationDownloadDropdownWrapper (#25407) --- .../presentation-download-dropdown-wrapper/component.jsx | 2 +- docs/docs/new-features.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/presentation-download-dropdown/presentation-download-dropdown-wrapper/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/presentation-download-dropdown/presentation-download-dropdown-wrapper/component.jsx index bb0b35ad6bae..c7cdb502fc8d 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/presentation-download-dropdown/presentation-download-dropdown-wrapper/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/presentation-download-dropdown/presentation-download-dropdown-wrapper/component.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import Styled from './styles'; const propTypes = { - children: PropTypes.shape({}).isRequired, + children: PropTypes.node.isRequired, }; function PresentationDownloadDropdownWrapper({ children }) { return ( diff --git a/docs/docs/new-features.md b/docs/docs/new-features.md index b3cea238a304..ffd7bfae01c5 100644 --- a/docs/docs/new-features.md +++ b/docs/docs/new-features.md @@ -307,6 +307,7 @@ For full details on what is new in BigBlueButton 3.0, see the release notes. Recent releases: +- [3.0.32](https://github.com/bigbluebutton/bigbluebutton/releases/tag/v3.0.32) - [3.0.31](https://github.com/bigbluebutton/bigbluebutton/releases/tag/v3.0.31) - [3.0.30](https://github.com/bigbluebutton/bigbluebutton/releases/tag/v3.0.30) - [3.0.29](https://github.com/bigbluebutton/bigbluebutton/releases/tag/v3.0.29) From d47ee9cf944836929e5a325fd7b17ce9e4ee1c90 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:52:21 +0000 Subject: [PATCH 06/24] build(deps): bump axios from 1.16.0 to 1.18.0 in /bbb-export-annotations Bumps [axios](https://github.com/axios/axios) from 1.16.0 to 1.18.0. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.16.0...v1.18.0) --- updated-dependencies: - dependency-name: axios dependency-version: 1.18.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- bbb-export-annotations/package-lock.json | 64 +++++++++++++++++++----- bbb-export-annotations/package.json | 2 +- 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/bbb-export-annotations/package-lock.json b/bbb-export-annotations/package-lock.json index 8a9f58577803..c59eb4196220 100644 --- a/bbb-export-annotations/package-lock.json +++ b/bbb-export-annotations/package-lock.json @@ -9,7 +9,7 @@ "version": "2.0", "dependencies": { "@svgdotjs/svg.js": "^3.2.4", - "axios": "^1.16.0", + "axios": "^1.18.0", "form-data": "^4.0.4", "opentype.js": "^1.3.4", "perfect-freehand": "^1.2.2", @@ -166,6 +166,18 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", @@ -218,13 +230,14 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "node_modules/axios": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", - "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz", + "integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, @@ -381,7 +394,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -941,6 +953,19 @@ "node": ">= 0.4" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/ignore": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", @@ -1141,7 +1166,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/natural-compare": { @@ -1653,6 +1677,14 @@ "dev": true, "requires": {} }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + } + }, "ajv": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", @@ -1691,12 +1723,13 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "axios": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", - "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz", + "integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==", "requires": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, @@ -1808,7 +1841,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "requires": { "ms": "^2.1.3" } @@ -2194,6 +2226,15 @@ "function-bind": "^1.1.2" } }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, "ignore": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", @@ -2335,8 +2376,7 @@ "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "natural-compare": { "version": "1.4.0", diff --git a/bbb-export-annotations/package.json b/bbb-export-annotations/package.json index 7bba016706c1..56bda2f30157 100644 --- a/bbb-export-annotations/package.json +++ b/bbb-export-annotations/package.json @@ -9,7 +9,7 @@ }, "dependencies": { "@svgdotjs/svg.js": "^3.2.4", - "axios": "^1.16.0", + "axios": "^1.18.0", "form-data": "^4.0.4", "opentype.js": "^1.3.4", "perfect-freehand": "^1.2.2", From f14cadf4732d375e8377f3e9bfb3d69a9e5b0a31 Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Mon, 20 Jul 2026 14:52:46 -0400 Subject: [PATCH 07/24] fix(docs): use valid font-weight for Open Sans SemiBold @font-face (#25465) `bolder` is a relative keyword that is only valid on the font-weight *property*, not the @font-face *descriptor*. Browsers discard the invalid descriptor, so the SemiBold and SemiBoldItalic faces fell back to weight 400/normal and collided with the Regular faces. Register them at their correct absolute weight (600) instead. Fixes SonarQube css:S8775 (At-rule descriptor values should be valid). Co-authored-by: Claude Opus 4.8 (1M context) --- docs/src/css/custom.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css index d9b6910e890e..f06eb823fc44 100644 --- a/docs/src/css/custom.css +++ b/docs/src/css/custom.css @@ -71,14 +71,14 @@ html { @font-face { font-family: 'Open Sans'; src: url('/static/fonts/OpenSans-SemiBold.ttf'); - font-weight: bolder; + font-weight: 600; font-style: normal; } @font-face { font-family: 'Open Sans'; src: url('/static/fonts/OpenSans-SemiBoldItalic.ttf'); - font-weight: bolder; + font-weight: 600; font-style: italic; } From 2f03533f11b1757176b0df9e3c6548419313031f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:14:40 -0400 Subject: [PATCH 08/24] build(deps): bump axios in /bbb-shared-notes-server (#25470) Bumps [axios](https://github.com/axios/axios) from 1.16.1 to 1.18.0. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.16.1...v1.18.0) --- updated-dependencies: - dependency-name: axios dependency-version: 1.18.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- bbb-shared-notes-server/package-lock.json | 8 ++++---- bbb-shared-notes-server/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bbb-shared-notes-server/package-lock.json b/bbb-shared-notes-server/package-lock.json index fa0f177b9b80..4b5024b6a9fc 100644 --- a/bbb-shared-notes-server/package-lock.json +++ b/bbb-shared-notes-server/package-lock.json @@ -15,7 +15,7 @@ "@hocuspocus/server": "^4.0.0", "@types/express": "^5.0.6", "@types/node": "^22.19.15", - "axios": "^1.15.2", + "axios": "^1.18.0", "claude": "^0.1.2", "express": "^5.2.1", "express-rate-limit": "^8.5.1", @@ -2227,9 +2227,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", - "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz", + "integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", diff --git a/bbb-shared-notes-server/package.json b/bbb-shared-notes-server/package.json index 399e220edea5..b9efd5d3b468 100644 --- a/bbb-shared-notes-server/package.json +++ b/bbb-shared-notes-server/package.json @@ -39,7 +39,7 @@ "@hocuspocus/server": "^4.0.0", "@types/express": "^5.0.6", "@types/node": "^22.19.15", - "axios": "^1.15.2", + "axios": "^1.18.0", "claude": "^0.1.2", "express": "^5.2.1", "express-rate-limit": "^8.5.1", From 087090c073e5e12f07af99f0c128826eccac8a79 Mon Sep 17 00:00:00 2001 From: Samuel Weirich <4281791+samuelwei@users.noreply.github.com> Date: Tue, 21 Jul 2026 01:36:44 +0200 Subject: [PATCH 09/24] Docs: Clarify voiceBridge range (#25464) See docs of the package used to create random numbers (https://commons.apache.org/proper/commons-lang/apidocs/org/apache/commons/lang3/RandomStringUtils.html#randomNumeric(int)) Each pos. in the created numeric string can be any valid digit (0-9) The result can be: 00000, 00001,..., 99999 --- docs/docs/data/create.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/data/create.tsx b/docs/docs/data/create.tsx index fbb9b05fa920..236c8d4e5358 100644 --- a/docs/docs/data/create.tsx +++ b/docs/docs/data/create.tsx @@ -56,7 +56,7 @@ const createEndpointTableData = [ "name": "voiceBridge", "required": false, "type": "String", - "description": (<>Voice conference number for the FreeSWITCH voice conference associated with this meeting. This must be a 5-digit number in the range 10000 to 99999. If you add a phone number to your BigBlueButton server, This parameter sets the personal identification number (PIN) that FreeSWITCH will prompt for a phone-only user to enter. If you want to change this range, edit FreeSWITCH dialplan and defaultNumDigitsForTelVoice of bigbluebutton.properties.

The voiceBridge number must be different for every meeting.

This parameter is optional. If you do not specify a voiceBridge number, then BigBlueButton will assign a random unused number for the meeting.

If do you pass a voiceBridge number, then you must ensure that each meeting has a unique voiceBridge number; otherwise, reusing same voiceBridge number for two different meetings will cause users from one meeting to appear as phone users in the other, which will be very confusing to users in both meetings.) + "description": (<>Voice conference number for the FreeSWITCH voice conference associated with this meeting. This must be a 5-digit numeric string in the range 00000 to 99999. If you add a phone number to your BigBlueButton server, This parameter sets the personal identification number (PIN) that FreeSWITCH will prompt for a phone-only user to enter. If you want to change this range, edit FreeSWITCH dialplan and defaultNumDigitsForTelVoice of bigbluebutton.properties.

The voiceBridge number must be different for every meeting.

This parameter is optional. If you do not specify a voiceBridge number, then BigBlueButton will assign a random unused number for the meeting.

If do you pass a voiceBridge number, then you must ensure that each meeting has a unique voiceBridge number; otherwise, reusing same voiceBridge number for two different meetings will cause users from one meeting to appear as phone users in the other, which will be very confusing to users in both meetings.) }, { "name": "maxParticipants", From 75da80557b1b4d239e845fb867fdccb6a06d8b44 Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Tue, 21 Jul 2026 14:35:03 -0400 Subject: [PATCH 10/24] docs: Documentation catchup on 3.0 new features/configs since 3.0.28 (#25442) * docs: document 3.0 changes since v3.0.28 (shared-notes Markdown, plugin button styles) Covers documentation-worthy changes landed on v3.0.x-develop since the v3.0.28 tag that were not yet reflected in the docs: - new-features.md: shared notes Markdown import/export (BlockNote), the new public.sharedNotes.importMarkdownEnabled / exportMarkdownEnabled client settings, the maxSharedNotesInitialContentUrlPayloadSize bbb-web property, the html5PluginSdkVersion bump, and 3.0.32 in recent releases. - customize.md: "Enable Markdown import/export in shared notes" config section. - plugins.md: configurable button styles (color/circle/hideLabel/size/style) for nav bar, actions bar, and presentation toolbar plugin buttons. The shared-notes Markdown API/seeding path and the guest-lobby-position, strict-override-validation, per-meeting recording-format, and sessionToken items were already documented on develop, so they are not duplicated here. Co-Authored-By: Claude Opus 4.8 (1M context) * docs: fix inverted Markdown initial-content precedence in create.tsx create.tsx claimed the inline `sharedNotesInitialContentMarkdown` create parameter "takes precedence over sharedNotesInitialContentMarkdownUrl". That is backwards. MeetingService.getSharedNotesInitialContentMarkdown() checks the URL first and returns its content when non-empty, so the URL wins; the inline create parameter only wins over the POST-body module. Corrected both create.tsx rows to state the actual order (URL > inline create parameter > POST module), matching the already-correct description in development/api.md. Co-Authored-By: Claude Opus 4.8 (1M context) * docs: move shared-notes Markdown config into the new-features BlockNote section; add property to configs table - new-features.md: the Markdown import/export subsection under BlockNote Shared Notes is now self-contained, including the bbb-html5.yml toggles (public.sharedNotes.importMarkdownEnabled / exportMarkdownEnabled) and how to enable them. Noted disabled-by-default in 3.0. - customize.md: removed the separate "Enable Markdown import/export in shared notes" section (superseded by the new-features text) and added the maxSharedNotesInitialContentUrlPayloadSize property to the "Other meeting configs available" bbb-web.properties table. Co-Authored-By: Claude Opus 4.8 (1M context) * docs: pin shared-notes Markdown to 3.0.33, retitle BlockNote heading, tweak SDK line - Reference BigBlueButton 3.0.33 (the release that ships the shared-notes Markdown import/export) instead of "next release after 3.0.32". - Retitle heading to "Import and export BlockNote shared notes as Markdown". - Add 3.0.33 to Recent releases (tag not published yet). - Drop the from-version on the html5PluginSdkVersion line. Co-Authored-By: Claude Opus 4.8 (1M context) * Update docs/docs/plugins.md Co-authored-by: Guilherme Pereira Leme <69865537+GuiLeme@users.noreply.github.com> * Update docs/docs/plugins.md Co-authored-by: Guilherme Pereira Leme <69865537+GuiLeme@users.noreply.github.com> * Update docs/docs/plugins.md Co-authored-by: Guilherme Pereira Leme <69865537+GuiLeme@users.noreply.github.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) Co-authored-by: Guilherme Pereira Leme <69865537+GuiLeme@users.noreply.github.com> --- docs/docs/administration/customize.md | 1 + docs/docs/data/create.tsx | 4 ++-- docs/docs/new-features.md | 27 +++++++++++++++++++++++++++ docs/docs/plugins.md | 10 ++++++++++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/docs/docs/administration/customize.md b/docs/docs/administration/customize.md index 0878fda3252d..4222ee4d954a 100644 --- a/docs/docs/administration/customize.md +++ b/docs/docs/administration/customize.md @@ -1569,6 +1569,7 @@ These configs can be set in `/etc/bigbluebutton/bbb-web.properties`. The table i | `learningDashboardCleanupDelayInMinutes` | Minutes the Learning Dashboard remains available after the meeting ends | Integer (0=keep permanently) | 2 _`overwritable`_ | | `disabledFeatures` | Comma-separated list of features to disable (see [`/create` docs](/development/api/#create) for the full list of feature names) | csv | _(empty)_ _`overwritable`_ | | `sharedNotesEditor` | Type of shared notes editor to use | etherpad, blockNote | etherpad _`overwritable`_ | +| `maxSharedNotesInitialContentUrlPayloadSize` | Maximum size (in KiB) of the response fetched when seeding shared-notes initial content from `sharedNotesInitialContentJsonUrl` / `sharedNotesInitialContentMarkdownUrl` | Integer (KiB) | 1024 | | `allowOverrideClientSettingsOnCreateCall` | Allow `clientSettingsOverride` / `clientSettingsOverrideJsonUrl` to be passed on `/create` | true/false | false | | `clientSettingsOverrideStrictValidation` | When true, reject the `/create` call (`bbb-web`) and refuse `bbb-apps-akka` boot if a client settings override has unknown or malformed keys. Intended for test/staging (see [Validating client settings overrides](#validating-client-settings-overrides)) | true/false | false | | `clientSettingsFilePath` | Path to the `settings.yml` catalog used as the schema for the strict client-settings override validation above | path | `/usr/share/bigbluebutton/html5-client/private/config/settings.yml` | diff --git a/docs/docs/data/create.tsx b/docs/docs/data/create.tsx index 236c8d4e5358..319a30d9d9be 100644 --- a/docs/docs/data/create.tsx +++ b/docs/docs/data/create.tsx @@ -390,13 +390,13 @@ const createEndpointTableData = [ "name": "sharedNotesInitialContentMarkdown", "required": false, "type": "String", - "description": (<>Raw markdown used as the shared-notes initial content (Only applicable for when `sharedNotesEditor=blockNote`, ignored otherwise). Takes precedence over `sharedNotesInitialContentMarkdownUrl` when both are provided.) + "description": (<>Raw markdown used as the shared-notes initial content (Only applicable for when `sharedNotesEditor=blockNote`, ignored otherwise). When `sharedNotesInitialContentMarkdownUrl` is also provided, the URL takes precedence over this inline value; this inline parameter in turn takes precedence over the `sharedNotesInitialContentMarkdown` POST module.) }, { "name": "sharedNotesInitialContentMarkdownUrl", "required": false, "type": "String", - "description": (<>Url from which the shared-notes will fetch the initial content as markdown (Only applicable for when `sharedNotesEditor=blockNote`, ignored otherwise). The URL must be `https` (`fetchUrlSupportedProtocols`), is capped by `maxSharedNotesInitialContentUrlPayloadSize` (default 1024 KiB) and has a 6000 ms timeout; a URL that violates these yields empty initial content silently.) + "description": (<>Url from which the shared-notes will fetch the initial content as markdown (Only applicable for when `sharedNotesEditor=blockNote`, ignored otherwise). When provided, it takes precedence over the inline `sharedNotesInitialContentMarkdown` create parameter and POST module. The URL must be `https` (`fetchUrlSupportedProtocols`), is capped by `maxSharedNotesInitialContentUrlPayloadSize` (default 1024 KiB) and has a 6000 ms timeout; a URL that violates these yields empty initial content silently.) }, { "name": "disabledFeatures", diff --git a/docs/docs/new-features.md b/docs/docs/new-features.md index ffd7bfae01c5..6c9472319ea5 100644 --- a/docs/docs/new-features.md +++ b/docs/docs/new-features.md @@ -143,6 +143,25 @@ To enable it, you would first need to install the optional package via At this point you can use it in a specific session by passing `sharedNotesEditor=blockNote` on the `/create` call. If you have made up your mind and would like to use it for all sessions, add the same line (`sharedNotesEditor=blockNote`) to `/etc/bigbluebutton/bbb-web.properties` and restart BigBlueButton via `$ sudo bbb-conf --restart` +#### Import and export BlockNote shared notes as Markdown + +The BlockNote shared notes editor can now exchange content as Markdown (available in BigBlueButton 3.0.33). From the shared notes options menu, the presenter can choose **Import from Markdown**, which opens a dialog to either upload a Markdown file (drag-and-drop or file picker) or paste Markdown directly. The imported content can be **appended** to the existing notes (the default, so importing never destroys what is already there) or **replace** the whole document. Separately, an **Export notes as Markdown** option downloads the current notes as a `.md` file. + +Both options are **disabled by default** in BigBlueButton 3.0 so that a minor upgrade does not add new menu buttons unexpectedly. Enable either or both in `/etc/bigbluebutton/bbb-html5.yml` and restart with `sudo bbb-conf --restart`: + +```yaml +public: + sharedNotes: + importMarkdownEnabled: true + exportMarkdownEnabled: true +``` + +These toggles only affect the BlockNote editor; they are ignored when Etherpad is used. + +Integrations can also seed a session's shared notes with Markdown at creation time using the `sharedNotesInitialContentMarkdown` / `sharedNotesInitialContentMarkdownUrl` create parameters (or a `sharedNotesInitialContentMarkdown` POST module). See the [Create API parameters](/development/api/#get-post-create) for details. + + + ### Engagement @@ -307,6 +326,7 @@ For full details on what is new in BigBlueButton 3.0, see the release notes. Recent releases: +- [3.0.33](https://github.com/bigbluebutton/bigbluebutton/releases/tag/v3.0.33) - [3.0.32](https://github.com/bigbluebutton/bigbluebutton/releases/tag/v3.0.32) - [3.0.31](https://github.com/bigbluebutton/bigbluebutton/releases/tag/v3.0.31) - [3.0.30](https://github.com/bigbluebutton/bigbluebutton/releases/tag/v3.0.30) @@ -400,6 +420,11 @@ In BigBlueButton 3.0.0-alpha.5 we replaced the JOIN parameter `defaultLayout` wi - Client settings.yml: `showGuestLobbyWaitingQueuePosition`. Defaults to `true` +#### Added new settings to enable Markdown import/export in shared notes + +- Client settings.yml: `public.sharedNotes.importMarkdownEnabled`. Defaults to `false`. When `true`, presenters see an **Import from Markdown** option in the BlockNote shared notes menu. +- Client settings.yml: `public.sharedNotes.exportMarkdownEnabled`. Defaults to `false`. When `true`, an **Export notes as Markdown** option is shown in the BlockNote shared notes menu. + #### Added new setting and userdata to allow skipping echo test if session has valid input/output devices stored - Client settings.yml: `skipEchoTestIfPreviousDevice`. Defaults to `false` @@ -447,6 +472,7 @@ Modified/added events - `muteOnStart` default value changed to `true` - which helps now that `transparentListenOnly` is enabled by default too. See [PR 20848](https://github.com/bigbluebutton/bigbluebutton/issues/20848) for more info. - `insertDocumentSupportedProtocols` renamed to `fetchUrlSupportedProtocols` - `insertDocumentBlockedHosts` renamed to `fetchUrlBlockedExternalHosts` +- `html5PluginSdkVersion` bumped to `0.0.103` #### Added - `pluginManifestFetchTimeout` added @@ -499,6 +525,7 @@ Modified/added events - `pluginManifestCacheRefreshIntervalMinutes` added in BBB 3.0.27 - `clientSettingsOverrideStrictValidation` added in BBB 3.0.30 - `clientSettingsFilePath` added in BBB 3.0.30 +- `maxSharedNotesInitialContentUrlPayloadSize` added — caps the size (in KiB, default `1024`) of the response fetched by `sharedNotesInitialContentJsonUrl` / `sharedNotesInitialContentMarkdownUrl` ### Removed support for POST requests on `join` endpoint and Content-Type headers are now required diff --git a/docs/docs/plugins.md b/docs/docs/plugins.md index 9061558d8cbe..a5d705371dce 100644 --- a/docs/docs/plugins.md +++ b/docs/docs/plugins.md @@ -829,6 +829,16 @@ That being said, here are the extensible areas we have so far: Mind that no plugin will interfere into another's extensible area. So feel free to set whatever you need into a certain plugin with no worries. +#### Configurable button styles + +Plugin-provided **nav bar**, **actions bar**, and **presentation toolbar** buttons accept a few optional style fields that control the rendered button's shape. They are read when present and fall back to the previous defaults when omitted, so older plugin objects keep rendering exactly as before (requires plugin SDK `0.0.100` or later): + +- `color` — button color (e.g. `primary`, `default`). Default: `primary` in the nav bar and actions bar, `default` in the presentation toolbar. +- `circle` — render as a circular icon button. Default: `true` in the actions bar, `false` in the presentation toolbar and nav-bar. +- `hideLabel` — hide the text label and show only the icon. Default: `true` in the actions bar, `false` in the presentation toolbar and nav-bar. +- `size` — button size (`sm`, `md`, `lg`). Default: `lg` in the actions bar, `md` in the presentation toolbar and nav-bar. +- `style` — an inline CSS style object applied to the button. + ### Auxiliaries: - `getSessionToken`: returns the user session token located on the user's URL. From 8749458c352b6d0bed458e0403a3c92da8f0dd76 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 15:20:05 -0400 Subject: [PATCH 11/24] build(deps): bump axios in /bigbluebutton-tests/playwright (#25474) Bumps [axios](https://github.com/axios/axios) from 1.16.0 to 1.18.0. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.16.0...v1.18.0) --- updated-dependencies: - dependency-name: axios dependency-version: 1.18.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../playwright/package-lock.json | 36 +++++++++++++++---- bigbluebutton-tests/playwright/package.json | 2 +- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/bigbluebutton-tests/playwright/package-lock.json b/bigbluebutton-tests/playwright/package-lock.json index f5d9ce52a497..0d1378c3d91d 100644 --- a/bigbluebutton-tests/playwright/package-lock.json +++ b/bigbluebutton-tests/playwright/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@playwright/test": "^1.56.0", "@swc/core": "^1.13.5", - "axios": "^1.16.0", + "axios": "^1.18.0", "chalk": "^4.1.2", "deep-equal": "^2.2.3", "dotenv": "^16.4.5", @@ -791,6 +791,18 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", @@ -993,13 +1005,14 @@ } }, "node_modules/axios": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", - "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz", + "integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, @@ -1330,7 +1343,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2720,6 +2732,19 @@ "dev": true, "license": "ISC" }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/husky": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/husky/-/husky-1.3.1.tgz", @@ -3532,7 +3557,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/natural-compare": { diff --git a/bigbluebutton-tests/playwright/package.json b/bigbluebutton-tests/playwright/package.json index 7366200302fb..10732ba5abd0 100644 --- a/bigbluebutton-tests/playwright/package.json +++ b/bigbluebutton-tests/playwright/package.json @@ -28,7 +28,7 @@ }, "dependencies": { "@playwright/test": "^1.56.0", - "axios": "^1.16.0", + "axios": "^1.18.0", "chalk": "^4.1.2", "deep-equal": "^2.2.3", "dotenv": "^16.4.5", From a41965a97d1b13677d6db9f1f80b0d5a31290aae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 15:21:00 -0400 Subject: [PATCH 12/24] build(deps): bump ch.qos.logback:logback-core in /bbb-recording-imex (#25427) Bumps [ch.qos.logback:logback-core](https://github.com/qos-ch/logback) from 1.5.33 to 1.5.34. - [Release notes](https://github.com/qos-ch/logback/releases) - [Commits](https://github.com/qos-ch/logback/compare/v_1.5.33...v_1.5.34) --- updated-dependencies: - dependency-name: ch.qos.logback:logback-core dependency-version: 1.5.34 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- bbb-recording-imex/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bbb-recording-imex/pom.xml b/bbb-recording-imex/pom.xml index 297408fbc042..698b53466c7d 100644 --- a/bbb-recording-imex/pom.xml +++ b/bbb-recording-imex/pom.xml @@ -75,7 +75,7 @@ ch.qos.logback logback-core - 1.5.33 + 1.5.34 org.slf4j From c963ec36e43ccff32f8e06094f38b4d9d69dfe32 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 17:40:18 -0400 Subject: [PATCH 13/24] build(deps): bump linkify-it from 5.0.1 to 5.0.2 in /bigbluebutton-html5 (#25483) Bumps [linkify-it](https://github.com/markdown-it/linkify-it) from 5.0.1 to 5.0.2. - [Changelog](https://github.com/markdown-it/linkify-it/blob/master/CHANGELOG.md) - [Commits](https://github.com/markdown-it/linkify-it/compare/5.0.1...5.0.2) --- updated-dependencies: - dependency-name: linkify-it dependency-version: 5.0.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- bigbluebutton-html5/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bigbluebutton-html5/package-lock.json b/bigbluebutton-html5/package-lock.json index c541f890e2b1..9d0622a6f4d1 100644 --- a/bigbluebutton-html5/package-lock.json +++ b/bigbluebutton-html5/package-lock.json @@ -11715,9 +11715,9 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "node_modules/linkify-it": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz", - "integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", "funding": [ { "type": "github", From def2124501c2a7aa07cb284cc8557c8d939c9661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Wed, 22 Jul 2026 10:17:00 -0300 Subject: [PATCH 14/24] add anon row check in learning dashboard csv test --- .../playwright/learningdashboard/learningdashboard.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bigbluebutton-tests/playwright/learningdashboard/learningdashboard.ts b/bigbluebutton-tests/playwright/learningdashboard/learningdashboard.ts index cb10284b8182..e8c94b1a6391 100644 --- a/bigbluebutton-tests/playwright/learningdashboard/learningdashboard.ts +++ b/bigbluebutton-tests/playwright/learningdashboard/learningdashboard.ts @@ -193,5 +193,8 @@ export class LearningDashboard extends MultiUsers { ]; await checkTextContent(dataCSV.content, dataToCheck); + expect(dataCSV.content, 'should not include an anonymous row when no anonymous polls were created').not.toMatch( + /^"Anonymous"(?:,|$)/m, + ); } } From 2cbff28081ac2dfc239efe3929cbc10bfc5a7ed8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 11:18:56 -0400 Subject: [PATCH 15/24] build(deps): bump fast-uri from 3.1.2 to 3.1.4 in /docs (#25486) Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.2 to 3.1.4. - [Release notes](https://github.com/fastify/fast-uri/releases) - [Commits](https://github.com/fastify/fast-uri/compare/v3.1.2...v3.1.4) --- updated-dependencies: - dependency-name: fast-uri dependency-version: 3.1.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index 1c35f943915d..4210f84c2450 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -9556,9 +9556,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", "funding": [ { "type": "github", From 637b9b75c7c12422a95cda01c301d45520cac3bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 11:19:31 -0400 Subject: [PATCH 16/24] build(deps): bump svgo from 3.3.3 to 3.3.4 in /docs (#25489) Bumps [svgo](https://github.com/svg/svgo) from 3.3.3 to 3.3.4. - [Release notes](https://github.com/svg/svgo/releases) - [Commits](https://github.com/svg/svgo/compare/v3.3.3...v3.3.4) --- updated-dependencies: - dependency-name: svgo dependency-version: 3.3.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index 4210f84c2450..07e4478c3180 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -18187,9 +18187,9 @@ "license": "MIT" }, "node_modules/svgo": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.3.tgz", - "integrity": "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.4.tgz", + "integrity": "sha512-GsNRis4e8jxn2Y9ENz/8lbJ93CstG8svtMnuRaHbiF2LTJ5tK0/q3t/URPq9Zc7zVWBJnNnJMIp6bevK7bSmNg==", "license": "MIT", "dependencies": { "commander": "^7.2.0", From a000f4469efdb8d19ff920a6425fa6d6c5e67f15 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 11:20:04 -0400 Subject: [PATCH 17/24] build(deps): bump shell-quote from 1.8.4 to 1.10.0 in /docs (#25485) Bumps [shell-quote](https://github.com/ljharb/shell-quote) from 1.8.4 to 1.10.0. - [Changelog](https://github.com/ljharb/shell-quote/blob/main/CHANGELOG.md) - [Commits](https://github.com/ljharb/shell-quote/compare/v1.8.4...v1.10.0) --- updated-dependencies: - dependency-name: shell-quote dependency-version: 1.10.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index 07e4478c3180..f089faec11dc 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -17708,9 +17708,9 @@ } }, "node_modules/shell-quote": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", - "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", "license": "MIT", "engines": { "node": ">= 0.4" From f176b848bb063bf9ede7661eb266a98fec98ce08 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 11:20:27 -0400 Subject: [PATCH 18/24] build(deps): bump body-parser from 1.20.5 to 1.20.6 in /docs (#25484) Bumps [body-parser](https://github.com/expressjs/body-parser) from 1.20.5 to 1.20.6. - [Release notes](https://github.com/expressjs/body-parser/releases) - [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md) - [Commits](https://github.com/expressjs/body-parser/compare/1.20.5...1.20.6) --- updated-dependencies: - dependency-name: body-parser dependency-version: 1.20.6 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index f089faec11dc..02303db71c5a 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -6658,9 +6658,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", - "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", From aac5577a449f20d8aff5c3f9f91773c7410b8a0d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 11:20:44 -0400 Subject: [PATCH 19/24] build(deps-dev): bump brace-expansion in /bbb-shared-notes-server (#25478) Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 5.0.6 to 5.0.7. - [Release notes](https://github.com/juliangruber/brace-expansion/releases) - [Commits](https://github.com/juliangruber/brace-expansion/compare/v5.0.6...v5.0.7) --- updated-dependencies: - dependency-name: brace-expansion dependency-version: 5.0.7 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- bbb-shared-notes-server/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bbb-shared-notes-server/package-lock.json b/bbb-shared-notes-server/package-lock.json index 4b5024b6a9fc..314da983c63f 100644 --- a/bbb-shared-notes-server/package-lock.json +++ b/bbb-shared-notes-server/package-lock.json @@ -2311,9 +2311,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "dev": true, "license": "MIT", "dependencies": { From ebf75d610814912e911aeb4879352220b82d93aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Wed, 22 Jul 2026 13:12:21 -0300 Subject: [PATCH 20/24] fix(learning-dashboard): omit anonymous CSV row when no anonymous polls exist --- bbb-learning-dashboard/src/services/UserService.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bbb-learning-dashboard/src/services/UserService.js b/bbb-learning-dashboard/src/services/UserService.js index 236b6c0a01d8..0b8c091547ce 100644 --- a/bbb-learning-dashboard/src/services/UserService.js +++ b/bbb-learning-dashboard/src/services/UserService.js @@ -217,7 +217,9 @@ export function makeUserCSVData(users, polls, intl) { // Add the anonymous answers anonymousRecord += `,"${pollValues[i].anonymousAnswers.join('\r\n')}"`; } - userRecords.Anonymous = anonymousRecord; + if (pollValues.some((poll) => poll.anonymous)) { + userRecords.Anonymous = anonymousRecord; + } return [ header, From f95ae6e0d54ddd23d00b2acdfa1dcc709b51e8e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Wed, 22 Jul 2026 13:51:33 -0300 Subject: [PATCH 21/24] Merge pull request #25435 from ramonlsouza/i-25425 fix: Presentation download: filename containing "&" is truncated and saved without its extension --- .../MakePresentationDownloadReqMsgHdlr.scala | 15 ++++++------ ...tionConversionCompletedSysPubMsgHdlr.scala | 8 +++---- .../PresentationDownloadUrlBuilder.scala | 23 +++++++++++++++++++ 3 files changed, 34 insertions(+), 12 deletions(-) create mode 100644 akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationDownloadUrlBuilder.scala diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/MakePresentationDownloadReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/MakePresentationDownloadReqMsgHdlr.scala index 08e9f051a28b..1c8dea01d988 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/MakePresentationDownloadReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/MakePresentationDownloadReqMsgHdlr.scala @@ -12,7 +12,6 @@ import org.bigbluebutton.core.running.LiveMeeting import org.bigbluebutton.core.util.RandomStringGenerator import java.io.File -import java.net.URI trait MakePresentationDownloadReqMsgHdlr extends RightsManagementTrait { this: PresentationPodHdlrs => @@ -173,16 +172,18 @@ trait MakePresentationDownloadReqMsgHdlr extends RightsManagementTrait { bus.outGW.send(buildStoreAnnotationsInRedisSysMsg(annotations, liveMeeting)) } else { // Return existing uploaded file directly - val convertedFileName = new URI(null, null, currentPres.get.filenameConverted, null).getRawPath - val originalFilename = new URI(null, null, currentPres.get.name, null).getRawPath + val convertedFileName = currentPres.get.filenameConverted + val originalFilename = currentPres.get.name val originalFileExt = originalFilename.split("\\.").last val convertedFileExt = if (convertedFileName != "") convertedFileName.split("\\.").last else "" - val convertedFileURI = if (convertedFileName != "") List("presentation", "download", meetingId, - s"${presId}?presFilename=${presId}.${convertedFileExt}&filename=$convertedFileName").mkString("", File.separator, "") + val convertedFileURI = if (convertedFileName != "") PresentationDownloadUrlBuilder.buildFileUri( + meetingId, presId, convertedFileExt, convertedFileName + ) else "" - val originalFileURI = List("presentation", "download", meetingId, - s"${presId}?presFilename=${presId}.${originalFileExt}&filename=$originalFilename").mkString("", File.separator, "") + val originalFileURI = PresentationDownloadUrlBuilder.buildFileUri( + meetingId, presId, originalFileExt, originalFilename + ) val event = buildNewPresFileAvailable("", originalFileURI, convertedFileURI, presId, m.body.fileStateType) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationConversionCompletedSysPubMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationConversionCompletedSysPubMsgHdlr.scala index 154b4ab20a46..4437d6212591 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationConversionCompletedSysPubMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationConversionCompletedSysPubMsgHdlr.scala @@ -8,8 +8,6 @@ import org.bigbluebutton.core.models.PresentationInPod import org.bigbluebutton.core.running.LiveMeeting import org.bigbluebutton.core2.message.senders.MsgBuilder -import java.io.File -import java.net.URI import java.time.{Instant, Duration} trait PresentationConversionCompletedSysPubMsgHdlr { @@ -61,9 +59,9 @@ trait PresentationConversionCompletedSysPubMsgHdlr { PresPresentationDAO.updatePages(presWithConvertedName) if (pres.downloadable) { - val originalFilename = new URI(null, null, pres.name, null).getRawPath - val originalFileURI = List("presentation", "download", meetingId, - s"${pres.id}?presFilename=${pres.id}.${originalDownloadableExtension}&filename=$originalFilename").mkString("", File.separator, "") + val originalFileURI = PresentationDownloadUrlBuilder.buildFileUri( + meetingId, pres.id, originalDownloadableExtension, pres.name + ) PresPresentationDAO.updateDownloadUri(pres.id, originalFileURI) } if(pres.current) { diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationDownloadUrlBuilder.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationDownloadUrlBuilder.scala new file mode 100644 index 000000000000..f29d5e3edeb6 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationDownloadUrlBuilder.scala @@ -0,0 +1,23 @@ +package org.bigbluebutton.core.apps.presentationpod + +import java.net.URLEncoder +import java.nio.charset.StandardCharsets + +object PresentationDownloadUrlBuilder { + private def encodeQueryValue(value: String): String = { + URLEncoder.encode(value, StandardCharsets.UTF_8).replace("+", "%20") + } + + def buildFileUri( + meetingId: String, + presentationId: String, + fileExtension: String, + downloadFilename: String + ): String = { + val storedFilename = encodeQueryValue(s"${presentationId}.${fileExtension}") + val encodedDownloadFilename = encodeQueryValue(downloadFilename) + + s"presentation/download/${meetingId}/${presentationId}" + + s"?presFilename=${storedFilename}&filename=${encodedDownloadFilename}" + } +} From ee2852906290eea307e5e21d1c13c5071e0d07d2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 14:35:58 -0400 Subject: [PATCH 22/24] build(deps-dev): bump fast-uri in /bigbluebutton-html5 (#25499) Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.2 to 3.1.4. - [Release notes](https://github.com/fastify/fast-uri/releases) - [Commits](https://github.com/fastify/fast-uri/compare/v3.1.2...v3.1.4) --- updated-dependencies: - dependency-name: fast-uri dependency-version: 3.1.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- bigbluebutton-html5/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bigbluebutton-html5/package-lock.json b/bigbluebutton-html5/package-lock.json index 9d0622a6f4d1..8c1bb3a2fc1c 100644 --- a/bigbluebutton-html5/package-lock.json +++ b/bigbluebutton-html5/package-lock.json @@ -9585,9 +9585,9 @@ "dev": true }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", "dev": true, "funding": [ { From c4cfd307a6f5cb12e9a42ab5f6a4922e520d4812 Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Wed, 22 Jul 2026 21:03:24 -0400 Subject: [PATCH 23/24] docs: add missing create params (#25082) * docs: add missing create params * docs: drop copyright and logoutTimer from create supported params --- docs/docs/data/create.tsx | 62 +++++++++++++++++++++++++++++++++++- docs/docs/development/api.md | 6 ++-- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/docs/docs/data/create.tsx b/docs/docs/data/create.tsx index 319a30d9d9be..f5c36ee356ee 100644 --- a/docs/docs/data/create.tsx +++ b/docs/docs/data/create.tsx @@ -76,6 +76,12 @@ const createEndpointTableData = [ "type": "String", "description": (<>The URL that the BigBlueButton client will go to after users click the OK button on the ‘You have been logged out message’. This overrides the value for bigbluebutton.web.logoutURL in bigbluebutton.properties.) }, + { + "name": "meetingEndedURL", + "required": false, + "type": "String", + "description": (<>Server-to-server callback URL that BigBlueButton will invoke when the meeting ends. Useful for third-party integrations that need to react to meeting termination. (added 2.2)) + }, { "name": "record", "required": false, @@ -126,6 +132,30 @@ const createEndpointTableData = [ "default": false, "description": (<>If set to false, breakout rooms will not be recorded.) }, + { + "name": "breakoutRoomsCaptureSlides", + "required": false, + "type": "Boolean", + "description": (<>If set to true, the current slide (with annotations) from each breakout room is exported back to the parent meeting's presentation when the breakout ends. The server-side default is taken from defaultBreakoutRoomsCaptureSlides. (added 2.6)) + }, + { + "name": "breakoutRoomsCaptureSlidesFilename", + "required": false, + "type": "String", + "description": (<>Filename template for slides captured from breakout rooms when breakoutRoomsCaptureSlides=true. (added 2.6)) + }, + { + "name": "breakoutRoomsCaptureNotes", + "required": false, + "type": "Boolean", + "description": (<>If set to true, the shared notes from each breakout room are exported back to the parent meeting's presentation when the breakout ends. The server-side default is taken from defaultBreakoutRoomsCaptureNotes. (added 2.6)) + }, + { + "name": "breakoutRoomsCaptureNotesFilename", + "required": false, + "type": "String", + "description": (<>Filename template for shared notes captured from breakout rooms when breakoutRoomsCaptureNotes=true. (added 2.6)) + }, { "name": "meta", "required": false, @@ -240,7 +270,7 @@ const createEndpointTableData = [ "required": false, "type": "Boolean", "default": false, - "description": (<>Setting to true will disable notes in the meeting. (added 2.2)) + "description": (<>Setting to true will disable notes in the meeting. (added 2.2)

Note: lockSettingsDisableNote (singular) is accepted as a deprecated alias and logs a deprecation warning on the server.) }, { "name": "lockSettingsHideUserList", @@ -347,6 +377,30 @@ const createEndpointTableData = [ "default": 0, "description": (<>Setting to 0 will disable this threshold. Defines the max number of webcams a meeting can have simultaneously. (added 2.5.0)) }, + { + "name": "maxPinnedCameras", + "required": false, + "type": "Number", + "description": (<>Per-meeting override of the maxPinnedCameras property in bigbluebutton.properties. Caps how many cameras can be pinned simultaneously in this meeting. Only positive values are applied. (added 2.6)) + }, + { + "name": "cameraBridge", + "required": false, + "type": "String", + "description": (<>Per-meeting override of the cameraBridge property. Selects the media bridge used for camera streams. Valid values: bbb-webrtc-sfu, livekit. (added 3.0)) + }, + { + "name": "screenShareBridge", + "required": false, + "type": "String", + "description": (<>Per-meeting override of the screenShareBridge property. Selects the media bridge used for screen share streams. Valid values: bbb-webrtc-sfu, livekit. (added 3.0)) + }, + { + "name": "audioBridge", + "required": false, + "type": "String", + "description": (<>Per-meeting override of the audioBridge property. Selects the media bridge used for audio streams. Valid values: bbb-webrtc-sfu, livekit, freeswitch. (added 3.0)) + }, { "name": "meetingExpireIfNoUserJoinedInMinutes", "required": false, @@ -373,6 +427,12 @@ const createEndpointTableData = [ "type": "String", "description": (<>Pass a URL to an image which will then be visible in the area above the participants list if displayBrandingArea is set to true in bbb-html5's configuration) }, + { + "name": "darklogo", + "required": false, + "type": "String", + "description": (<>Like logo, but used when the client is in dark mode. If only logo is provided, it is used in both light and dark modes. (added 3.0)) + }, { "name": "sharedNotesEditor", "required": false, diff --git a/docs/docs/development/api.md b/docs/docs/development/api.md index 5856112d5437..74fb5aad834b 100644 --- a/docs/docs/development/api.md +++ b/docs/docs/development/api.md @@ -71,7 +71,7 @@ Updated in 2.0: Updated in 2.2: -- **create** - Added `endWhenNoModerator`. +- **create** - Added `endWhenNoModerator`, `meetingEndedURL`. - **getRecordingTextTracks** - Get a list of the caption/subtitle files currently available for a recording. - **putRecordingTextTrack** - Upload a caption or subtitle file to add it to the recording. If there is any existing track with the same values for kind and lang, it will be replaced. @@ -99,7 +99,7 @@ Updated in 2.5: Updated in 2.6: -- **create** - **Added:** `notifyRecordingIsOn`, `presentationUploadExternalUrl`, `presentationUploadExternalDescription`, `recordFullDurationMedia` (v2.6.9); `disabledFeaturesExclude`(2.6.9); Added `liveTranscription` and `presentation` as options for `disabledFeatures`. +- **create** - **Added:** `notifyRecordingIsOn`, `presentationUploadExternalUrl`, `presentationUploadExternalDescription`, `recordFullDurationMedia` (v2.6.9); `disabledFeaturesExclude`(2.6.9); `maxPinnedCameras`, `breakoutRoomsCaptureSlides`, `breakoutRoomsCaptureSlidesFilename`, `breakoutRoomsCaptureNotes`, `breakoutRoomsCaptureNotesFilename`; Added `liveTranscription` and `presentation` as options for `disabledFeatures`. - **getRecordings** - **Added:** Added support for pagination using `offset`, `limit` @@ -114,7 +114,7 @@ Updated in 2.7: Updated in 3.0: - **create** - - **Added parameters:** `loginURL`, `pluginManifests`, `pluginManifestsFetchUrl`, `presentationConversionCacheEnabled`, `maxNumPages`, `multiUserWhiteboardEnabled`, `clientSettingsOverrideJsonUrl`, `sharedNotesEditor`. + - **Added parameters:** `loginURL`, `pluginManifests`, `pluginManifestsFetchUrl`, `presentationConversionCacheEnabled`, `maxNumPages`, `multiUserWhiteboardEnabled`, `clientSettingsOverrideJsonUrl`, `sharedNotesEditor`, `cameraBridge`, `screenShareBridge`, `audioBridge`, `darklogo`. - **Added options:** Parameter `meetingLayout` supports a few new options: CAMERAS_ONLY, PARTICIPANTS_AND_CHAT_ONLY, PRESENTATION_ONLY, MEDIA_ONLY; - **Added options:** Parameter `disabledFeatures` supports a few new options: `infiniteWhiteboard`, `deleteChatMessage`, `editChatMessage`, `replyChatMessage`, `chatMessageReactions`, `raiseHand`, `userReactions`, `chatEmojiPicker`, `quizzes`; - **Added POST module:** `clientSettingsOverride` (gated by the server-side setting `allowOverrideClientSettingsOnCreateCall` in `bbb-web.properties`); From 23809e82050c5cec630392c30d8b89c6d2931dad Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Thu, 23 Jul 2026 10:15:45 -0400 Subject: [PATCH 24/24] fix(bbb-web): fall back to meeting logoutURL on API error redirects When an API error redirects the user (guestDeniedAccess, maxParticipantsReached, mismatchCreateTimeParam) and neither errorRedirectUrl nor logoutURL was passed on the join request, respondWithRedirect() jumped straight to the server default logout URL, ignoring the logoutURL stored on the meeting at create time. Pass the meeting's effective logout URL down so the redirect precedence becomes: errorRedirectUrl (join) > logoutURL (join) > logoutURL (create) > server default. The create-time value is not URL-validated by processLogoutUrl(), so it is only used when it passes ValidationService.isValidURL(). Closes #23380 Co-Authored-By: Claude Fable 5 --- .../web/controllers/ApiController.groovy | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy index 5630fb611dff..d9859305de37 100755 --- a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy +++ b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy @@ -379,7 +379,7 @@ class ApiController { } if (createTime != meeting.getCreateTime()) { // BEGIN - backward compatibility - invalid("mismatchCreateTimeParam", "The createTime parameter submitted mismatches with the current meeting.", redirectClient, errorRedirectUrl); + invalid("mismatchCreateTimeParam", "The createTime parameter submitted mismatches with the current meeting.", redirectClient, errorRedirectUrl, true, meeting.getLogoutUrl()); return // END - backward compatibility @@ -578,7 +578,7 @@ class ApiController { if (hasReachedMaxParticipants(meeting, us)) { // BEGIN - backward compatibility - invalid("maxParticipantsReached", "The number of participants allowed for this meeting has been reached.", redirectClient, errorRedirectUrl) + invalid("maxParticipantsReached", "The number of participants allowed for this meeting has been reached.", redirectClient, errorRedirectUrl, true, us.logoutUrl) return // END - backward compatibility @@ -635,7 +635,7 @@ class ApiController { // have it wait for approval. String destUrl = us.clientUrl if (guestStatusVal == GuestPolicy.DENY) { - invalid("guestDeniedAccess", "You have been denied access to this meeting based on the meeting's guest policy", redirectClient, errorRedirectUrl) + invalid("guestDeniedAccess", "You have been denied access to this meeting based on the meeting's guest policy", redirectClient, errorRedirectUrl, true, us.logoutUrl) return } @@ -2101,7 +2101,7 @@ class ApiController { } //TODO: method added for backward compatibility, it will be removed in next versions after 0.8 - private void invalid(key, msg, redirectResponse = false, errorRedirectUrl = "", useLogoutUrl = true) { + private void invalid(key, msg, redirectResponse = false, errorRedirectUrl = "", useLogoutUrl = true, meetingLogoutUrl = "") { // Note: This xml scheme will be DEPRECATED. log.debug CONTROLLER_NAME + "#invalid " + msg if (redirectResponse) { @@ -2114,7 +2114,7 @@ class ApiController { JSONArray errorsJSONArray = new JSONArray(errors) log.debug "JSON Errors {}", errorsJSONArray.toString() - respondWithRedirect(errorsJSONArray, errorRedirectUrl, useLogoutUrl) + respondWithRedirect(errorsJSONArray, errorRedirectUrl, useLogoutUrl, meetingLogoutUrl) } else { response.addHeader("Cache-Control", "no-cache") withFormat { @@ -2149,9 +2149,16 @@ class ApiController { return newURL; } - private void respondWithRedirect(errorsJSONArray, redirectUrl = "", useLogoutUrl = true) { + private void respondWithRedirect(errorsJSONArray, redirectUrl = "", useLogoutUrl = true, meetingLogoutUrl = "") { String uriString = paramsProcessorUtil.getDefaultLogoutUrl(); + // The logoutURL stored on the meeting at create is not URL-validated, so + // only fall back to it when it can actually serve as a redirect target + if (useLogoutUrl && !StringUtils.isEmpty(meetingLogoutUrl) + && ServiceUtils.getValidationService().isValidURL(meetingLogoutUrl)) { + uriString = meetingLogoutUrl; + } + if (useLogoutUrl && !StringUtils.isEmpty(params.logoutURL)) { try { uriString = params.logoutURL;