From f07921675fdcbf663a9141ceef748dd143e968da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Laniel?= Date: Fri, 12 Jun 2026 11:03:55 +0200 Subject: [PATCH 001/121] Fix allowPromoteGuestToModerator --- bbb-graphql-server/metadata/query_collections.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/bbb-graphql-server/metadata/query_collections.yaml b/bbb-graphql-server/metadata/query_collections.yaml index b86684a20243..fdbfed08c10d 100644 --- a/bbb-graphql-server/metadata/query_collections.yaml +++ b/bbb-graphql-server/metadata/query_collections.yaml @@ -52,6 +52,7 @@ allowModsToEjectCameras allowModsToUnmuteUsers authenticatedGuest + allowPromoteGuestToModerator maxUserConcurrentAccesses maxUsers meetingLayout From f8b065e401632491ed6ffe4aef2be70d29967cb4 Mon Sep 17 00:00:00 2001 From: Tainan Felipe Date: Mon, 15 Jun 2026 22:34:14 -0300 Subject: [PATCH 002/121] fix(shared-notes): prevent collaboration cursor name from leaking into links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the BlockNote shared notes editor, y-prosemirror renders the remote collaboration cursor as a ProseMirror widget decoration with `side: 10` and no `marks`, so ProseMirror wraps the widget in the marks of the node that follows the caret. When a remote user's caret sits inside a link, that node carries the `link` mark, so the cursor — including the user's name and the U+2060 word-joiner separators around it — was rendered inside the , leaking the name into the link's visible text (issue #25225). Render the cursor through BlockNote's supported `renderCursor` hook so the element carries no document text: the collaborator's name is held in a `data-cursor-name` attribute and shown as CSS `::after` pseudo-content (never part of `textContent`/`href`), and the U+2060 separators are dropped. Presence rendering (caret + name label, including the activity/hover label) is preserved. Adds a two-user Playwright regression test that places one user's cursor inside a link and asserts the other user's view of that link contains no collaborator name and no word-joiner separators in its text or href. Co-authored-by: Tainan Felipe --- .../components/bn-shared-notes/component.tsx | 38 ++++++++++ .../sharednotes/blocknote/blocknote.spec.ts | 22 ++++++ .../sharednotes/blocknote/blocknote.ts | 74 +++++++++++++++++++ .../playwright/sharednotes/blocknote/util.ts | 51 +++++++++++++ 4 files changed, 185 insertions(+) create mode 100644 bigbluebutton-tests/playwright/sharednotes/blocknote/blocknote.spec.ts create mode 100644 bigbluebutton-tests/playwright/sharednotes/blocknote/blocknote.ts create mode 100644 bigbluebutton-tests/playwright/sharednotes/blocknote/util.ts 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 c2ef004279be..cea08ad82839 100644 --- a/bigbluebutton-html5/imports/ui/components/bn-shared-notes/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/bn-shared-notes/component.tsx @@ -107,6 +107,37 @@ const fixCursorAtOriginExtension = Extension.create({ }, }); +// The remote collaboration cursor is a ProseMirror *widget decoration*. y-prosemirror +// renders it with `side: 10` and no `marks`, so ProseMirror wraps the widget in the +// marks of the node that follows the caret. When a remote user's caret sits inside (or +// at the edge of) a link, that following node carries the `link` mark, so the cursor +// widget — and therefore the user's name and the U+2060 word-joiner separators around +// it — is rendered *inside* the , polluting the link's visible text (issue #25225). +// +// y-prosemirror hardcodes the decoration spec and BlockNote exposes no hook for it, so +// we use BlockNote's supported `renderCursor` hook to render a cursor that carries no +// document text: the name lives in a `data-cursor-name` attribute shown via the CSS +// `::after` rule below (pseudo-content is never part of `textContent`), and the U+2060 +// separators are omitted. The widget may still be positioned inside the link mark, but +// it no longer leaks the name (or any separator characters) into the link's text/href. +const renderCollaborationCursor = (user: { name: string; color: string }) => { + const cursorElement = document.createElement('span'); + cursorElement.classList.add('bn-collaboration-cursor__base'); + + const caret = document.createElement('span'); + caret.classList.add('bn-collaboration-cursor__caret'); + caret.setAttribute('style', `background-color: ${user.color}`); + + const label = document.createElement('span'); + label.classList.add('bn-collaboration-cursor__label'); + label.setAttribute('style', `background-color: ${user.color}`); + label.setAttribute('data-cursor-name', user.name ?? ''); + + caret.appendChild(label); + cursorElement.appendChild(caret); + return cursorElement; +}; + const intlMessages = defineMessages({ payloadSizeError: { id: 'app.notes.blocknote.payloadSizeError', @@ -237,6 +268,7 @@ function BlockNoteApp(props: BlockNoteAppProps): React.ReactElement { name: userName || '', color: userColor || '', }, + renderCursor: renderCollaborationCursor, }, schema, dictionary: { @@ -377,6 +409,12 @@ function BlockNoteApp(props: BlockNoteAppProps): React.ReactElement { .bn-collaboration-cursor__label { color: ${colorWhite} !important; } + /* The collaborator's name is held in a data attribute (see + renderCollaborationCursor) and rendered as pseudo-content so it never + becomes part of a surrounding link's text/href — issue #25225. */ + .bn-collaboration-cursor__label::after { + content: attr(data-cursor-name); + } .bn-collaboration-cursor__caret { overflow: visible !important; } diff --git a/bigbluebutton-tests/playwright/sharednotes/blocknote/blocknote.spec.ts b/bigbluebutton-tests/playwright/sharednotes/blocknote/blocknote.spec.ts new file mode 100644 index 000000000000..97fe8a8a03e1 --- /dev/null +++ b/bigbluebutton-tests/playwright/sharednotes/blocknote/blocknote.spec.ts @@ -0,0 +1,22 @@ +import { initializePages } from '../../core/helpers'; +import { test } from '../../core/setup/fixtures'; +import { BlockNoteSharedNotes } from './blocknote'; + +test.describe.parallel('Shared Notes - BlockNote', { tag: '@ci' }, () => { + let blockNoteSharedNotes: BlockNoteSharedNotes; + + test.beforeEach(async ({ browser, context }, testInfo) => { + blockNoteSharedNotes = new BlockNoteSharedNotes(browser, context); + await initializePages(blockNoteSharedNotes, browser, { + isMultiUser: true, + createParameter: 'sharedNotesEditor=blocknote', + testInfo, + }); + }); + + // Regression test for #25225 — BlockNote: a remote user's collaboration-cursor + // name must not be embedded in a link when their caret is positioned inside it. + test("Collaboration cursor must not embed a user's name in a link", async () => { + await blockNoteSharedNotes.collaborationCursorMustNotEmbedInLink(); + }); +}); diff --git a/bigbluebutton-tests/playwright/sharednotes/blocknote/blocknote.ts b/bigbluebutton-tests/playwright/sharednotes/blocknote/blocknote.ts new file mode 100644 index 000000000000..b2cdbf63ac85 --- /dev/null +++ b/bigbluebutton-tests/playwright/sharednotes/blocknote/blocknote.ts @@ -0,0 +1,74 @@ +import { expect } from '@playwright/test'; + +import { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } from '../../core/constants'; +import { elements as e } from '../../core/elements'; +import { MultiUsers } from '../../user/multiusers'; +import { + getBlockNoteEditorLocator, + getBlockNoteLinkLocator, + readLinkAndCursorState, + startBlockNoteSharedNotes, +} from './util'; + +const LINK_URL = 'https://github.com/bigbluebutton/bigbluebutton/issues/25175'; +const WORD_JOINER = '⁠'; + +export class BlockNoteSharedNotes extends MultiUsers { + // Reproduces issue #25225: when a remote user's caret sits inside a link, that + // user's collaboration-cursor name (and the U+2060 word-joiner separators around + // it) must NOT be embedded in the link's text or href as seen by other users. + async collaborationCursorMustNotEmbedInLink() { + 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 startBlockNoteSharedNotes(this.modPage); + await startBlockNoteSharedNotes(this.userPage); + + // The moderator creates a link by typing a URL followed by a space (autolink). + const modEditor = getBlockNoteEditorLocator(this.modPage); + await modEditor.click(); + await this.modPage.page.keyboard.type(`${LINK_URL} `); + + // The link must sync to the attendee's editor. + await expect(getBlockNoteLinkLocator(this.userPage), 'should sync the pasted link to the attendee').toHaveCount(1, { + timeout: ELEMENT_WAIT_LONGER_TIME, + }); + + // The moderator clicks the link and nudges the caret a few characters inward, so + // the node following the caret is link-marked (the condition that makes + // y-prosemirror wrap the cursor widget in the link mark). + await getBlockNoteLinkLocator(this.modPage).first().click(); + await this.modPage.page.keyboard.press('ArrowLeft'); + await this.modPage.page.keyboard.press('ArrowLeft'); + await this.modPage.page.keyboard.press('ArrowLeft'); + + // Allow awareness to broadcast and the attendee to render the remote cursor. + // Read via evaluate so the attendee page is not focused/activated (which would + // blur the moderator editor and stop its cursor from broadcasting). + await this.userPage.page.waitForTimeout(ELEMENT_WAIT_TIME); + const state = await readLinkAndCursorState(this.userPage); + + // Presence guard: the moderator's remote cursor must actually be rendered for the + // attendee — otherwise a clean link would be a false pass. + expect(state.remoteCursorCount, 'attendee should render the moderator collaboration cursor').toBeGreaterThan(0); + + // The bug: the cursor name and U+2060 separators must not pollute the link text. + expect(state.linkTextHasWordJoiner, 'link text must not contain U+2060 word-joiner separator characters').toBe( + false, + ); + expect(state.linkText, "link text must not contain the other user's name").not.toContain(this.modPage.username); + expect(state.linkText, 'link text should equal the original URL').toBe(LINK_URL); + + // ...nor the href / navigation target. + expect(state.linkHref, "link href must not contain the other user's name").not.toContain(this.modPage.username); + expect(state.linkHref, 'link href must not contain U+2060').not.toContain(WORD_JOINER); + expect(state.linkHref, 'link href should equal the original URL').toBe(LINK_URL); + + await this.modPage.waitAndClick(e.hideNotesLabel); + await this.modPage.wasRemoved(e.hideNotesLabel, 'should not display the hide notes label'); + } +} diff --git a/bigbluebutton-tests/playwright/sharednotes/blocknote/util.ts b/bigbluebutton-tests/playwright/sharednotes/blocknote/util.ts new file mode 100644 index 000000000000..a2c0de96aa8c --- /dev/null +++ b/bigbluebutton-tests/playwright/sharednotes/blocknote/util.ts @@ -0,0 +1,51 @@ +import { Locator } from '@playwright/test'; + +import { ELEMENT_WAIT_LONGER_TIME } from '../../core/constants'; +import { elements as e } from '../../core/elements'; +import { Page } from '../../core/page'; + +// The BlockNote editor renders inline (no Etherpad iframes); its editable area +// is the ProseMirror root `.bn-editor` inside the shared-notes panel. +export const BLOCKNOTE_EDITOR = '[data-test="notes"] .bn-editor'; + +export async function startBlockNoteSharedNotes(testPage: Page): Promise { + await testPage.waitAndClick(e.sharedNotes); + await testPage.waitForSelector(e.hideNotesLabel, ELEMENT_WAIT_LONGER_TIME); + await testPage.waitForSelector(BLOCKNOTE_EDITOR, ELEMENT_WAIT_LONGER_TIME); +} + +export function getBlockNoteEditorLocator(testPage: Page): Locator { + return testPage.page.locator(BLOCKNOTE_EDITOR); +} + +export function getBlockNoteLinkLocator(testPage: Page): Locator { + return testPage.page.locator(`${BLOCKNOTE_EDITOR} a`); +} + +/** + * Reads the rendered state of the first link in another user's BlockNote + * editor, plus whether a remote collaboration cursor is present. Runs via + * `evaluate` so it never focuses/activates the page (which would blur the + * cursor owner's editor and stop their awareness cursor from broadcasting). + */ +export function readLinkAndCursorState(testPage: Page) { + return testPage.page.evaluate((sel: string) => { + const WORD_JOINER = '⁠'; + const anchor = document.querySelector(`${sel} a`) as HTMLAnchorElement | null; + const cursorBases = document.querySelectorAll( + `${sel} .bn-collaboration-cursor__base, ${sel} .bn-collaboration-cursor__caret`, + ); + return { + hasAnchor: !!anchor, + linkText: anchor ? (anchor.textContent ?? '') : '', + linkHref: anchor ? (anchor.getAttribute('href') ?? '') : '', + linkTextHasWordJoiner: anchor ? (anchor.textContent ?? '').includes(WORD_JOINER) : false, + cursorWidgetInsideLink: anchor + ? !!anchor.querySelector( + '.bn-collaboration-cursor__base, .bn-collaboration-cursor__caret, .bn-collaboration-cursor__label', + ) + : false, + remoteCursorCount: cursorBases.length, + }; + }, BLOCKNOTE_EDITOR); +} From 1531aa4eac9abce05c2fe2154a03546c5dc87182 Mon Sep 17 00:00:00 2001 From: Claudio Promptoso Date: Thu, 11 Jun 2026 14:28:22 +0000 Subject: [PATCH 003/121] fix(chat): preserve text before links in reply message preview The reply intention preview and the quote inside sent replies extract the first visible line of the original message with getFirstVisibleLineHtml. The function treated the inline tags and as block-level, walked the root children in reverse order and removed every node that precedes the last "block" found - so a message like "Hello https://example.com" was previewed as just the link. Rewrite the extraction to walk the DOM in document order and truncate at the first visual line break (newline in a non-whitespace-only text node,
, or the end of the first nested block element), keeping inline content of the first line intact. Lists now preview their first item instead of the last one. Add an e2e scenario covering both previews when replying to a message with text followed by a link. Co-authored-by: Tainan Felipe --- .../components/chat/chat-graphql/service.ts | 69 +++++++++++-------- .../playwright/chat/chat.spec.ts | 6 ++ .../playwright/chat/messageActions.ts | 45 ++++++++++++ .../playwright/core/elements.ts | 1 + 4 files changed, 92 insertions(+), 29 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/service.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/service.ts index 2ac183309bd9..b7fc87823fd7 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/service.ts +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/service.ts @@ -1,3 +1,42 @@ +const lineBreakingTags = new Set(['P', 'DIV', 'PRE', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'UL', 'OL', 'LI', 'BLOCKQUOTE', 'TABLE', 'TR', 'TD']); + +// Truncates the subtree at the first visual line break: a newline inside a +// text node (rendered as a break by `white-space: pre-wrap`), a
, or the +// end of the first nested line-breaking element. Returns true when the line +// ended inside the given node, so callers must drop the siblings that follow. +const truncateAtFirstLineBreak = (node: Node): boolean => { + if (node.nodeType === Node.TEXT_NODE) { + const text = node.textContent || ''; + // whitespace-only nodes are markup formatting between elements, not a visual break + if (!text.trim()) return false; + const newlineIndex = text.indexOf('\n'); + if (newlineIndex === -1) return false; + // eslint-disable-next-line no-param-reassign + node.textContent = text.slice(0, newlineIndex); + return true; + } + + if (node.nodeType !== Node.ELEMENT_NODE) return false; + + const element = node as Element; + if (element.tagName === 'BR') { + element.remove(); + return true; + } + + let lineEnded = false; + Array.from(element.childNodes).forEach((child) => { + if (lineEnded) { + child.remove(); + return; + } + lineEnded = truncateAtFirstLineBreak(child) + || (child.nodeType === Node.ELEMENT_NODE && lineBreakingTags.has((child as Element).tagName)); + }); + + return lineEnded; +}; + export const getFirstVisibleLineHtml = (htmlContent: string): string => { if (!htmlContent.trim()) return ''; @@ -6,35 +45,7 @@ export const getFirstVisibleLineHtml = (htmlContent: string): string => { const root = doc.body.firstElementChild; if (!root) return ''; - const displayBlockTags = new Set(['P', 'A', 'DIV', 'PRE', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'UL', 'OL', 'LI', 'BLOCKQUOTE', 'TABLE', 'TR', 'TD', 'CODE']); - - const children = Array.from(root.childNodes); - let keptBlocks = 0; - - for (let i = children.length - 1; i >= 0; i -= 1) { - const node = children[i]; - - if (node.nodeType === Node.ELEMENT_NODE) { - const tagName = (node as Element).tagName.toUpperCase(); - if (displayBlockTags.has(tagName)) { - if (keptBlocks === 0) { - keptBlocks = 1; - } else { - node.remove(); - } - } else if (keptBlocks > 0) { - node.remove(); - } - } else if (node.nodeType === Node.TEXT_NODE) { - if (keptBlocks > 0) { - node.remove(); - } else { - const text = node.textContent || ''; - const firstLine = text.split(/\n/)[0]; - node.textContent = firstLine; - } - } - } + truncateAtFirstLineBreak(root); return root.outerHTML; }; diff --git a/bigbluebutton-tests/playwright/chat/chat.spec.ts b/bigbluebutton-tests/playwright/chat/chat.spec.ts index 4cb0ec35d127..93869b34bb25 100644 --- a/bigbluebutton-tests/playwright/chat/chat.spec.ts +++ b/bigbluebutton-tests/playwright/chat/chat.spec.ts @@ -203,6 +203,12 @@ test.describe.parallel('Chat', { tag: '@ci' }, () => { await message.replyMessage(); }); + test('Reply to a message with text followed by a link', async ({ browser, context, page }, testInfo) => { + const message = new MessageActions(browser, context); + await message.initModPage(page, { testInfo }); + await message.replyMessageWithTextBeforeLink(); + }); + test('Cancel a reply to a message', async ({ browser, context, page }, testInfo) => { const message = new MessageActions(browser, context); await message.initModPage(page, { testInfo }); diff --git a/bigbluebutton-tests/playwright/chat/messageActions.ts b/bigbluebutton-tests/playwright/chat/messageActions.ts index f50aa0a500df..18a09017bbb1 100644 --- a/bigbluebutton-tests/playwright/chat/messageActions.ts +++ b/bigbluebutton-tests/playwright/chat/messageActions.ts @@ -417,6 +417,51 @@ export class MessageActions extends Chat { await expect(messageReplied, 'should display the replied message content in the message sent').toBeVisible(); } + async replyMessageWithTextBeforeLink() { + await openPublicChat(this.modPage); + // send a message containing text followed by a link + await this.modPage.fill(e.chatBox, e.messageWithTextBeforeLink); + await this.modPage.waitAndClick(e.sendButton); + await this.modPage.hasElement( + e.chatUserMessageText, + 'should display the message sent by the moderator on the public chat', + ); + await checkLastMessageSent(this.modPage, e.messageWithTextBeforeLink); + const initialMessagesCount = await this.modPage.getSelectorCount(e.chatUserMessageText); + // hover message and click to reply + const lastMessageItem = this.modPage.page.locator(e.chatMessageItem).last(); + await lastMessageItem.hover(); + await expect( + lastMessageItem.locator(e.messageToolbar), + 'should display the message toolbar when hovering a message', + ).toBeVisible(); + await this.modPage.waitAndClick(e.replyMessageButton); + // the reply intention preview must keep the whole first line: the text and the link + await this.modPage.hasElement( + e.chatReplyIntentionContainerContent, + 'should display the chat reply intention container content', + ); + await this.modPage.hasText( + e.chatReplyIntentionContainerContent, + e.messageWithTextBeforeLink, + 'should display the full first line (text and link) in the reply intention preview', + ); + // reply the message + await this.modPage.fill(e.chatBox, e.message2); + await this.modPage.waitAndClick(e.sendButton); + await this.modPage.hasElementCount( + e.chatUserMessageText, + initialMessagesCount + 1, + 'should display the reply as a new message', + ); + // the quote inside the sent reply must keep the whole first line as well + const messageReplied = this.modPage.page.locator(e.chatMessageItem).last().locator(e.chatMessageReplied); + await expect( + messageReplied, + 'should display the full first line (text and link) in the replied message quote', + ).toContainText(e.messageWithTextBeforeLink); + } + async cancelReplyMessage() { await openPublicChat(this.modPage); // send a message diff --git a/bigbluebutton-tests/playwright/core/elements.ts b/bigbluebutton-tests/playwright/core/elements.ts index 0519b3d5a8ed..25885b508f27 100644 --- a/bigbluebutton-tests/playwright/core/elements.ts +++ b/bigbluebutton-tests/playwright/core/elements.ts @@ -188,6 +188,7 @@ export const elements = { testMessage: 'Just a test', message1: 'Hello User2', message2: 'Hello User1', + messageWithTextBeforeLink: 'Hello https://example.com', publicMessage1: 'This is a Public Message from User1', publicMessage2: 'This is a Public Message from User2', uniqueCharacterMessage: 'A', From cc0cfc21208bbd35c4b0b1d0b68a9d4efb828b08 Mon Sep 17 00:00:00 2001 From: Tainan Felipe Date: Tue, 16 Jun 2026 11:06:29 -0300 Subject: [PATCH 004/121] test(shared-notes): harden #25225 regression test per review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Assert the remote cursor is positioned inside the link (the precondition that proves the bug condition is actually exercised — guards against a false pass on a clean-but-cursorless link). - Poll for the remote cursor instead of a fixed sleep (robust; still reads via page.evaluate so the cursor owner's editor keeps focus and keeps broadcasting its cursor). - Count only `.bn-collaboration-cursor__base` (one per cursor) and drop the unused `hasAnchor` field. - Add a TODO pointing at the upstream y-prosemirror `marks: []` fix. Co-authored-by: Tainan Felipe --- .../components/bn-shared-notes/component.tsx | 3 +++ .../sharednotes/blocknote/blocknote.ts | 26 +++++++++++++------ .../playwright/sharednotes/blocknote/util.ts | 6 ++--- 3 files changed, 23 insertions(+), 12 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 cea08ad82839..50fe1539c580 100644 --- a/bigbluebutton-html5/imports/ui/components/bn-shared-notes/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/bn-shared-notes/component.tsx @@ -107,6 +107,9 @@ const fixCursorAtOriginExtension = Extension.create({ }, }); +// TODO: remove this workaround once y-prosemirror's cursor decoration can set `marks: []` +// (the upstream-correct fix is `marks: []` on the cursor `Decoration.widget` in +// y-prosemirror/src/plugins/cursor-plugin.js; related: https://github.com/yjs/y-prosemirror/issues/174). // The remote collaboration cursor is a ProseMirror *widget decoration*. y-prosemirror // renders it with `side: 10` and no `marks`, so ProseMirror wraps the widget in the // marks of the node that follows the caret. When a remote user's caret sits inside (or diff --git a/bigbluebutton-tests/playwright/sharednotes/blocknote/blocknote.ts b/bigbluebutton-tests/playwright/sharednotes/blocknote/blocknote.ts index b2cdbf63ac85..819dcacfa19b 100644 --- a/bigbluebutton-tests/playwright/sharednotes/blocknote/blocknote.ts +++ b/bigbluebutton-tests/playwright/sharednotes/blocknote/blocknote.ts @@ -1,6 +1,6 @@ import { expect } from '@playwright/test'; -import { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } from '../../core/constants'; +import { ELEMENT_WAIT_LONGER_TIME } from '../../core/constants'; import { elements as e } from '../../core/elements'; import { MultiUsers } from '../../user/multiusers'; import { @@ -10,6 +10,7 @@ import { startBlockNoteSharedNotes, } from './util'; +// URL mirrors the link shown in the original #25225 report (it referenced issue 25175). const LINK_URL = 'https://github.com/bigbluebutton/bigbluebutton/issues/25175'; const WORD_JOINER = '⁠'; @@ -46,15 +47,24 @@ export class BlockNoteSharedNotes extends MultiUsers { await this.modPage.page.keyboard.press('ArrowLeft'); await this.modPage.page.keyboard.press('ArrowLeft'); - // Allow awareness to broadcast and the attendee to render the remote cursor. - // Read via evaluate so the attendee page is not focused/activated (which would - // blur the moderator editor and stop its cursor from broadcasting). - await this.userPage.page.waitForTimeout(ELEMENT_WAIT_TIME); + // Poll until the attendee renders the moderator's remote cursor. The read runs + // inside readLinkAndCursorState via page.evaluate, which never focuses/activates + // the attendee page, so the moderator editor keeps focus and keeps broadcasting + // its cursor. This presence guard also prevents a clean-but-cursorless link from + // being a false pass. + await expect + .poll(async () => (await readLinkAndCursorState(this.userPage)).remoteCursorCount, { + message: 'attendee should render the moderator collaboration cursor', + timeout: ELEMENT_WAIT_LONGER_TIME, + }) + .toBeGreaterThan(0); const state = await readLinkAndCursorState(this.userPage); - // Presence guard: the moderator's remote cursor must actually be rendered for the - // attendee — otherwise a clean link would be a false pass. - expect(state.remoteCursorCount, 'attendee should render the moderator collaboration cursor').toBeGreaterThan(0); + // Precondition: the cursor widget is actually positioned inside the link — the exact + // condition that triggers #25225. The fix neutralises the text leak without + // relocating the widget, so this stays true; a future upstream `marks: []` fix would + // move the widget out of the
and this guard should then be relaxed. + expect(state.cursorWidgetInsideLink, 'precondition: the remote cursor must sit inside the link').toBe(true); // The bug: the cursor name and U+2060 separators must not pollute the link text. expect(state.linkTextHasWordJoiner, 'link text must not contain U+2060 word-joiner separator characters').toBe( diff --git a/bigbluebutton-tests/playwright/sharednotes/blocknote/util.ts b/bigbluebutton-tests/playwright/sharednotes/blocknote/util.ts index a2c0de96aa8c..2e35cd414546 100644 --- a/bigbluebutton-tests/playwright/sharednotes/blocknote/util.ts +++ b/bigbluebutton-tests/playwright/sharednotes/blocknote/util.ts @@ -32,11 +32,9 @@ export function readLinkAndCursorState(testPage: Page) { return testPage.page.evaluate((sel: string) => { const WORD_JOINER = '⁠'; const anchor = document.querySelector(`${sel} a`) as HTMLAnchorElement | null; - const cursorBases = document.querySelectorAll( - `${sel} .bn-collaboration-cursor__base, ${sel} .bn-collaboration-cursor__caret`, - ); + // One `__base` element per remote cursor — count bases only for an accurate count. + const cursorBases = document.querySelectorAll(`${sel} .bn-collaboration-cursor__base`); return { - hasAnchor: !!anchor, linkText: anchor ? (anchor.textContent ?? '') : '', linkHref: anchor ? (anchor.getAttribute('href') ?? '') : '', linkTextHasWordJoiner: anchor ? (anchor.textContent ?? '').includes(WORD_JOINER) : false, From 6df858d2b89f06aa758a30d0c78bc5a296ce1588 Mon Sep 17 00:00:00 2001 From: Tainan Felipe Date: Tue, 16 Jun 2026 15:52:53 -0300 Subject: [PATCH 005/121] test(shared-notes): make #25225 cursor positioning deterministic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The click-based caret positioning was flaky: clicking the link opens BlockNote's link toolbar, which steals editor focus and stops y-prosemirror from broadcasting the moderator's cursor — so the attendee sometimes saw the cursor at the link's end (outside the ) and the test failed on the precondition instead of reaching the bug assertion. Place the caret inside the link by keyboard only, and fold the presence guard + inside-link precondition + sync wait into a single expect.poll(cursorWidgetInsideLink).toBe(true). Verified: RED on an unpatched 3.0 server (fails on the U+2060 assertion, reproducing #25225) and GREEN on the patched build. Co-authored-by: Tainan Felipe --- .../sharednotes/blocknote/blocknote.ts | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/bigbluebutton-tests/playwright/sharednotes/blocknote/blocknote.ts b/bigbluebutton-tests/playwright/sharednotes/blocknote/blocknote.ts index 819dcacfa19b..7c3f5ce3b48b 100644 --- a/bigbluebutton-tests/playwright/sharednotes/blocknote/blocknote.ts +++ b/bigbluebutton-tests/playwright/sharednotes/blocknote/blocknote.ts @@ -39,33 +39,31 @@ export class BlockNoteSharedNotes extends MultiUsers { timeout: ELEMENT_WAIT_LONGER_TIME, }); - // The moderator clicks the link and nudges the caret a few characters inward, so - // the node following the caret is link-marked (the condition that makes - // y-prosemirror wrap the cursor widget in the link mark). - await getBlockNoteLinkLocator(this.modPage).first().click(); - await this.modPage.page.keyboard.press('ArrowLeft'); - await this.modPage.page.keyboard.press('ArrowLeft'); - await this.modPage.page.keyboard.press('ArrowLeft'); + // Place the moderator's caret a few characters inside the link — keyboard only. + // (Clicking the link opens BlockNote's link toolbar, which steals editor focus and + // stops y-prosemirror from broadcasting the cursor.) After typing, the caret sits + // after the trailing space; stepping left past the space and into the URL makes the + // node *after* the caret link-marked, which is what makes y-prosemirror wrap the + // remote cursor widget in the link mark — the #25225 trigger. + for (let i = 0; i < 9; i += 1) { + await this.modPage.page.keyboard.press('ArrowLeft'); + } - // Poll until the attendee renders the moderator's remote cursor. The read runs - // inside readLinkAndCursorState via page.evaluate, which never focuses/activates - // the attendee page, so the moderator editor keeps focus and keeps broadcasting - // its cursor. This presence guard also prevents a clean-but-cursorless link from - // being a false pass. + // Wait until the attendee actually renders the moderator's cursor *inside* the link. + // This single poll is the presence guard + the bug precondition + a sync wait: a + // remote cursor exists and sits within the (the exact #25225 condition), so a + // clean link cannot be a false pass. Reading via page.evaluate never focuses the + // attendee page, so the moderator editor keeps focus and keeps broadcasting. + // (A future upstream `marks: []` fix would render the widget *outside* the ; + // this precondition would then need relaxing.) await expect - .poll(async () => (await readLinkAndCursorState(this.userPage)).remoteCursorCount, { - message: 'attendee should render the moderator collaboration cursor', + .poll(async () => (await readLinkAndCursorState(this.userPage)).cursorWidgetInsideLink, { + message: 'attendee should render the moderator cursor inside the link', timeout: ELEMENT_WAIT_LONGER_TIME, }) - .toBeGreaterThan(0); + .toBe(true); const state = await readLinkAndCursorState(this.userPage); - // Precondition: the cursor widget is actually positioned inside the link — the exact - // condition that triggers #25225. The fix neutralises the text leak without - // relocating the widget, so this stays true; a future upstream `marks: []` fix would - // move the widget out of the and this guard should then be relaxed. - expect(state.cursorWidgetInsideLink, 'precondition: the remote cursor must sit inside the link').toBe(true); - // The bug: the cursor name and U+2060 separators must not pollute the link text. expect(state.linkTextHasWordJoiner, 'link text must not contain U+2060 word-joiner separator characters').toBe( false, From f2cbae3420c4601f1c5b6fe2d0f982e6d7c8d530 Mon Sep 17 00:00:00 2001 From: Claudio Promptoso Date: Mon, 1 Jun 2026 16:40:59 +0000 Subject: [PATCH 006/121] fix(video-provider): show raised hand indicator on audio-only tiles The raised hand indicator was only computed for GRID and STREAM tile types, so audio-only tiles never displayed it even though the user data (raiseHand) and the render path were already in place. Include the AUDIO_ONLY tile type in the raiseHand condition. Add a data-test hook to the indicator and an end-to-end test covering an audio-only tile raising its hand. Closes #25169 --- .../video-list/video-list-item/component.tsx | 5 ++- .../playwright/core/elements.ts | 2 + .../playwright/core/media/fakespeech.wav | Bin 0 -> 1152078 bytes .../playwright/core/setup/browsersConfig.ts | 2 + .../playwright/user/multiusers.ts | 37 ++++++++++++++++++ .../playwright/user/user.spec.ts | 6 +++ 6 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 bigbluebutton-tests/playwright/core/media/fakespeech.wav diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.tsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.tsx index 6349893446ca..0ede88b4c46d 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.tsx @@ -169,7 +169,8 @@ const VideoListItem: React.FC = (props) => { const { animations, webcamBorderHighlightColor } = Settings.application; const talking = voiceUser?.talking; const raiseHand = (stream.type === VIDEO_TYPES.GRID && stream?.raiseHand) - || (stream.type === VIDEO_TYPES.STREAM && stream.user?.raiseHand); + || (stream.type === VIDEO_TYPES.STREAM && stream.user?.raiseHand) + || (stream.type === VIDEO_TYPES.AUDIO_ONLY && stream.user?.raiseHand); const { data: currentUser } = useCurrentUser((u) => ({ userId: u.userId, pinned: u.pinned, @@ -321,7 +322,7 @@ const VideoListItem: React.FC = (props) => { if (!raiseHand) return null; return ( - + {raisedHandPosition > 0 && {raisedHandPosition}} diff --git a/bigbluebutton-tests/playwright/core/elements.ts b/bigbluebutton-tests/playwright/core/elements.ts index d2e59cc2bde7..b5dfd4409e11 100644 --- a/bigbluebutton-tests/playwright/core/elements.ts +++ b/bigbluebutton-tests/playwright/core/elements.ts @@ -535,7 +535,9 @@ export const elements = { nextPageVideoPagination: 'button[data-test="nextPageVideoPaginationBtn"]', previousPageVideoPagination: 'button[data-test="previousPageVideoPaginationBtn"]', videoQualitySelector: 'select[id="setQuality"]', + webcamItem: 'div[data-test="webcamItem"]', webcamItemTalkingUser: 'div[data-test="webcamItemTalkingUser"]', + webcamItemRaisedHand: 'div[data-test="webcamItemRaisedHand"]', webcamSettingsModal: 'div[data-test="webcamSettingsModal"]', dropdownWebcamButton: 'div[data-test="dropdownWebcamButton"]', webcamMirroredVideoPreview: 'video[data-test="mirroredVideoPreview"]', diff --git a/bigbluebutton-tests/playwright/core/media/fakespeech.wav b/bigbluebutton-tests/playwright/core/media/fakespeech.wav new file mode 100644 index 0000000000000000000000000000000000000000..b55f9add4af622ca6dcaf1d0bb7bacd89e6678b8 GIT binary patch literal 1152078 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

qY@Os z^knWc6>-{3`w>pH^sqdv@!Dv$$)g;8`V65KZAd?lbKYW(b||goJR2 zAHwHF#%c;zfV;u=VQuVE=6mta{X|dl71@fqfxFpz=*-Lv+;wTr?qd^i-MD?6$k*U; zii$V*tQa@@2$O}S!X{w{J{yH4!X%-$P*cbxsQht$B43T?`8`}0E+H4=pMN|5ybY?C z6^ZfBF`ta%7xA0;J^TTFKOe=f2TkM#q$+}l@y~lIxfEY;e{f=qe;%9=+!DMWw4)x#5Y%$hl``rC^`@E; z72hsu8ByWpo?Zm1M9^qt?!s-QLGV6`LV)m-wNby6K~0rkXqB$WOl7V!iP?JW6!s|V z#m;&q%qQcx#oSh83{G?BxHH^QZYQ@2Yn48@3z>ssxohka+~Cc@zGr@6+Av9&v-BW{ zqL-;bsG5BORm5%NMrbN-m>BuZn=p4$8^=*MF|mGB`&$0_aWWOv7d2Xwp(bk-O)-_2 z3CtNrU`w*$>@xN|>%)AKpR3Nb;(BlcxS`w-u0LX$7F-oBHy026@HD%S?T$E=W=}9< znR2*Uza3s`Qu-7snT1hjP$4@hc?e#NmDqwxG=#Yl%Iw$m&&%OWz9UOf)2N43Ho70Z zkEWTj%umc7#>J##>!7ODa@@Ut!fI?RE*18sa&o!2>|8q33gEc+>=kw^-ggJKC@Ztq znT2?ZX_-g#&!}T7)BC9&R4VEmIS^IhFTxv3hultoSR`N9KmQT6u#=`=M3mf-!xI zS;O>UiZe7S35}#n(h9X1l3!9h<50+8_n>bSfde)RF`X9%OFct0_M%#A7Sx7Vf-0?P zwU?;lIRq|!D%Bt3pU3#;e>4BQqB$N$UR-E$^B^gtf-SU!d`PCC8d1}!)427Lmu^E( z$N2M%mYHl!Ri-^Nl!;`PGn+9M9b%3!hnaoMHfA+5hZ)6mW9l&ZnfRz4bc$X?hoeSz z98@e>g%Rg_^UwQ}+einQiE2U3piWRhDkEKo9s)7?1pSicnJ}ygt251+E=*r$Ff$CF zK^S2=F-@2%c)k=EX`axB>4o%TUF*dP3c;T}Cb5IodF-qgGEV2h%U^g{#nY5p8ZC>KPb;m}(%NVPP{(~8 zAq>unl-9V7;N$-GLZUhU^2m<0?s0InXah z;9Z=f98@eiJ8ILnLMxm=&&N*CHhM38h(3ard4S$YZ;bKJ;rvFxPt8FRh;$Z1%q&6J zh`Y#t4l=8n$>1sNM+NuxkWJzmZ}emEg@@@a^irq*YQg!e82_Bn6Cg?|r8k0)F;(BF zU%-9H`0zqoLWJ1_$B= zsu1I!U&ESZFIH#ssIgQp^yE^QGbHLYw4${z8Qa2)ND4Re9Hf|`n2)m)99ru(%ph%{ zWF~}=dDd8sit=rcnM`N!sL6j3Rrwd|6QG=Q&>Lcxy_8-Mqe?bZ>CdcZ#dihZn^eIl z(@yUP;bft{4Pwo6orWJ$9LCa6NMe6?|NICR-QpFZf3(p>BE~^n*1ZNc02&yPT5~d@ml%lc!KvIcp!n+1+Aq z0RLnSo^^JNy$uAOBldaw78qK(ErE!Z2ls3Xuu`Vt+#drU)^$?hzuW{At_8?>+;UAf zC8%}Hko#B!^79Q)G82IVRuym35Rfo8fHi!Dd=48Vz;8)UWd{MQINrJPR7E`H!FDeJ zzcdH38cDE&2J+I^;V*3hy=4S=XSKl1OpgAJ2gDIDvc?b{;Lm*pkM1Qje!d4M_+#+T zccLB{bmN0XS_ApP3GfDv;p?%8B;YJphNs>S{LdBeWKV!$_7pq`7biCXI{wmNO_^X@ zGGW!Jsif$W=P3*R?n5xZ4#O@lCdYw5)eLOVY~aKGx%lUEKJ(APql?8^yazhi3-UGj z2G48!>M@vhpZVv17XJCqw9L=^^MSZEr^8|{C)dE5 z10883sLC^ulU)jG{c5ar4SrlnE(4qHGyi-){M8xY;&nh(peR`D3K2#`a0+q3B18v0 z+-CT`f99Wm`=0XN5ht_*7=E4cS zLrR15+8XcLAiQ-maZ{|qowx-#sXgRAJbOWa+m3s39X$B?nW+1)* zo%bE+%}4Q;&jmHEBTJ1>B#v-~>aZ9Fg3%FH5vLE2i zK$LR?9CFq55?_K`*O=&m2xK`p_OZ~aHIbFfNEU*{X@al1KfHihxEt5w?%#{3>o|D| zIgOv6(}+zDllu{)Y`{9_A}cfq_i1z3o}%QJAY{6*U!VEsEkJ(G?j}OS{LHxkPWxJC zHpn17K?kpo{VL()1s6NLlhR4zBtot-A%0Baq(I$03nJuV;DXn4+JKfa0&nJOXD{~h z323K@@Lw#C6Ve~l?2WjMALIT=f%&02L^om_&gy>R3TVU}ZjSue#TMX7kAg3=3TN^V zc@ELX14JBgq>AX-z@vjT9Y7TFnScJ7fBv!g=c_*R&yi=W1HN!aWFz~c4`(QzA;>}Y zp}K=0+6=7s(%{>tp=2sd-X~9jEIt#rNfUU4pZVwU+}Eh9t%2Xt0k$-o<8xH|wtW-? z&4u76_Xn%BrCr~yZdU{)yfmJ2c4aUb8`*8`Z|tF97p|~(A#47`cI^~SA#{)QMxAaq zJRjG|=vH>SyK~^3KXyGtUfhtw5DA?lf<#h$=UbA)apN7ve%Ppij3fqSoPc1<8 zZU=RcIz`1&m#M3GF5y=v@jHJd{<%h^AuAxNpN1R%DtgYopz@=Cx-)8%^Fc~Hgs#wg z)GJD*Y>J{;nxh$-KwplZdWz29Q`9bMCF((aaB@o{Q^`<|aOW(67f~1dFNVBDY(X^M zjL3uf$1QiWI~p0^-0*QeHviltGNT{7AN-nQ$PIfzq_0IqBm2La`jNUqy`(Hkrjvmh zkOTDILUa*)H)q!5bR^pF6MPL4ElMPTvSrm@C;iQDY|Ux7Hmi77Xyc);4Ps>c|_d zE!HmUpmhe^;pdiV`RvT7_%yZq+4E4FzGIW{5I*zINir|l1}F7L>|!G1{J*9q;Eeu6 zy~FfN7IZ{3K=0o`dOSUgUQB;Sucp_aa=w(Fi>mq%@ZOu!<*~yFz(;K zb?5;YVs*8eTeYl;Rw+!Y7q_CUvfza`K!-qYkf)b`A%4+%hyBcJH?RlWt5JKk?QDp| zCc;;L=VSoMezJSmjkr09cEo%{9W;FL&g4RP+$OqO>QlqebrVa4s4qacs*6>OL5ILb zbn%?QblM&I0sWA^Pv4?1(WmHr=p2|!52riQ)o^+h`X#zq7E?V@AxVzPC9q>7nZNXXBp_M;0SKdRP`xcc@h8tDjAuLpOE-bX8Bo z3hpotlY-69R%V;99oTO$Z`GUa!FFPsv(=zak_J8I578kx2i=3k7?HU|FQ8k}>FE2^ zXa4!e=ARED&!exkE47=V>8kWJ+~l2YvJB(PJKB2~OZVoXF8o z)O&$``hDyYb}-uzC)fqmXD-tMeUcB*QQwIEv+>W*lIhTGb(BhobGD9-qYK~-+lDFT z4CtmGiFv|HSY3QB3s)SJow{5jt|7D#Dse?HJ0N3n;2gV#9mH0LdeB33;df**W0mvZ z8T`5U=cmZ@=thmD($hWZL$u7)NB8k9Ou5v>MCV@iIc6gAW4<$*8_CV$mU64{tl;Kj zy15_Mk}J)n!JeLD|NQ*(bcpe7WYGi<`@B!wk<| zsAYz@Fc-wnck%f)ZZ_AGtA=&HW&hm#^Zj@qhr+5Bh41}}E(v>jm&w9LvzysBY-X+r zHvv;hH!zc)kpGG=%$MOS@a6d^=v$@b1^yLx5>rgQxH5Q8W7(-}RhD9RGVPfJ%s#p` z;?33Q@Og+nprqs$q6+Z{m9#|eMq~gUgYll$J`852Z0#`HfRu3@^HoI+Q$MIr&>QPH zbWVR9IThI&Sss}inI4%DnHO0Z*%>((c^Ogk0(u*LmVQPTj7r92Al>|>rWGgw0r znMK*lY}VaB>GcbJ>B)MKhL z9YcGWF3d3|C3f)#md7cbjIW>I^YG32k^Bn&2mS(o4+O>+{1g5be}>AP=(k@r(}jOx0%8?br0D*m@qrQxm*!GnqSNx z$I3V%6%-K)3&n*ZLLMQVAPaH)d44tDAE(y?J&$o*Nz9%vVJpH1n#}ynKW|F^dHCni z)P1TJI?2Cerr=(zgBydzEwP$=$K`{bzL)m7yQt z@)!9fd^6je|`!C`-WN;jnwXkkB7H~*Mz?duMTex9}eFP>*2Io4Q+(>gXU=E zBQqmUBPI34I$?A)E*s^|J!T&3drZ8pvA@FXMK1J}6h@zUX=LRpQXh(c9{8mB=gZ&o z&kO$z_~*kI3)8dj(8qlZRnEQW;GOSEP>FnlNslX5HS2^~9`}DW}Un7p|;{Ic5=Fh+`7aQqA$r)bEwb!^9|xFF@scB8YnH4wn;~%)6yyF zptMn%C4DPZgs#vnaXz$LTwyKllPCNDUW9J@|KXoIzu=#b`sDcMPr*OG0-t9aG{47C zugTivb|MKe09B9D;0-9ysefSBGLINVz+29*?}?O+T-G{kJm_u>!&3ODI!3LfW>B5b zgV4Fq;n0E5@z9mf>yQlEbbEEFdSCr2JRp2AoJX6hF_FHJcadn_uMaRdbFP`qI&3w= z)axwFo?QejGakB9s!<<`f1Z||%tR2I9;Lp7YU?wi0vNzjN{kk4AHDb>56 zgAWNc36%=v3Vj*M63Q2<7-}0DAKDdq7s{pfRZoIJJS_Yy+(0|4RYasxOTVK>8;rTc zEN0!ZhS=$ylTLRx32}gEPs-F5>O=9*)Bl2hUYA^t`bR@VSe)}I@z0~#SSBYko_+{L z)5+w0RC!0CMwiX`wD{-A{|5Z?uh}1&L`(;I7sXPw$wjET=5Yr*r%+e@bol36SON5p z?`fSXOHCp#qu;BIyUqz`dw*lVBPlFBIYw}~b-Rx5)9#e>JMhzvG5r^D|4(og>{Bv2X z3I6#B@N2*P6@Klf#Xq0P?1c^=Nu?u86Rq6g&I)L?zA_Vc|5vqt-3_gPKU~ z9l95+9XyY?>ZyOEKdb*v+`_mHamC}(#QEZ+xCC*T<0{7WjN1_Bk1Oq8d8>{(EMP$;B-XCTl{tWPTbD8 z0dZC0GR1l0s5m+<9;nN;;)cf^h!g!S{0IEm0?PyGgByaSLU%(W)jY^8uhcq3a_g>s z!B}gKv|8Fxh?ZUVK5>v-N)196%kRrSfA*e#zLS58o~7z+UuH3V2)#QLnT{yswsMBs zE3H$e-$-TD&__oOYP428yhII$8ikI4D7`&UJP_;e=KsQfI&NZI!?;{=iQ-tSQjSX> zS01M|CN30L!T+6K4h#+0fziP)LR&)hR00*~Xf0PH9674bHaeT-thBac-*pbS%ZOoQ zOR5B&l=;2*=g$AbKhMlJ=EkwxKVklP8ll;H{`tF4oPYlF{qd>u&&|I9|2+FA&p%Hl zH2GWb&kz3%_~&oAFF$eq`9)+R>wW zJ#aZtHn=|+6*?7ap&IIvaAnQZwncjA`3=)JZqBy8whKA3`@r2vKp~gPiO%E?!$1G- zZ@@o4{>k&t=YuAd?Qg(8ulC9F&r|T7!9NfGCHUvj@<}_HKu=Q;AU)D1(%GN;>7SyhN@p2R+j~89ke%yx7Nd;uJyU>+zdFBdX6X z|BLX?^UK5JBeEf9QK~6lE4`KeN>8ON_AsNO%KPO3at`^Lr=KUjXPJ~mS}Ud%rwbOS zeh0YZe-Zw9P7rw?dveRY<&E-PnNiXzd6gncLA-|v6u*32o+j6kMR~WUxyJwrHMz7D zq~7V^5q8GROLlHFdxOc&44{ugjkpoH6#Z8@+}_TQwrDr7R+$l_ma#!s@J`;>>S$-e z)xww5wyGIg6e=Hz1h)tK2P+5D1(~29{T(j@YQPiB8mt=}8$246&?&h!lu#Y4zEwMh zUxdHWTy16~r+!ZFVI((?o4qkPaLbIMU>7IL@jB+b^y1Yle zCcl!ycwWob<-PJuxwV{Ge(0GEi*paGS3#OCCKM+Nq|hBK;-cI<@Xss$7ykL?KPmrw zqUWN=Czk;ee3HCE-Xw38zn2%{1UHj&%9>}FC)$$|R<(r`6o-ktIOhN1pSSvp@Xrf^ zBmM92&v&p%!9Tx5XZsWK&nNsB{`vj?y%;ZGX6aIHWF8tKqgh8k!tx87hi9Qw{-ugCA4lvlgLA zp(7zmt)eba18SY{p7592Vl8Q8ek83PqnE@qPqgX5EKd!PB<4A#+-L4oq9A!29O8`N zmb76^W;&aJ+r$+FK`}pwtx3h9;yqB_$4fs+@xcY}3(or<&pFR^&kfH7&q2>>&qxs7 z(|ewPVqZtH#3f=8@tn|9c)+*e?_#?7D4Um^k6Fpq$S0?!29j5yq&~rYf{ywnwr#br z4x3rcIRITs!_QUq?77d?}8_Ds#i0{LDX3_b>6!>wpbj6rCRNn11waDvDYPE!XD6 zJ~z2Lz`13Yuvb{3)zf@v)HP1)74#F4>X92-SB=wFhiimwb-UVAEvky@Lre^OA6gQc zkJYWi?7*E6t>#lZqkAr(mJZJihr>;@<67a!=12~GvtHOZYSc5In?tOm(Cn|}ymcnK zIf%1F7wGJ-MCSar_~&WNApKG5=BB-2w{FAoN1iu=bfb%;iRM zW1gNuUlGY2IjXhLT+Em?3@hQY>I}7&T3Ahj>4>+XXQAhzK!{b-s%6w@b%lCYO&{(S zJ`hf(_1EreH6jNiMf6?R$3sRn^Oo5aRjhe-ZYYTTEBtd_DkJrj7E1@D>(UF!kLLw` z^&@_#Cpg-?bX*)PW*4sreS`$U8onTZkSmX~T8iDxWW$`KME9odk(J0z(EA;X(^|vX zjef!D7HRb{-x!^Y7kV^imBvI;Ms{h9F;BN8+%cRPb4q*EscJX1j#^4Bq!v_5sMXc4 z)lq7UdPhwht`}a2yRoFUP_wo6k;{>an0Agb4jL8A3ubfc4W^Y6KqKv6;h)F+oBZ?r zOnzn^osynRnPf-u3Y0~^cayrK(52kQzGPLfel!c4J2B;QP_L@rj`WPgi)_`JY98${ zW**9i6NI1Py<4l!Qpc;K)d}icb%T0JeWNDFd7cnH7fzye&<=ZO z&OBw-#$-e%J7SM>{x$yjI?O@dR4ulK2lZH7tS4Sj1?XZ zlUhY>ruI}TiK#PM@220@YZ<37C3(!OWSz4b*!S%&PQ)1lW%K1kF6hiw04wUl@Xt#y z`=NTajmn04WD;@``mcJsFQFTJ&8}siu_~cvSKYi~v^M2im^!jl_oZW!*3Q+hINg$v&{qmRCKG`MkVuzSsJrS zOb^yHkD5`?cPj)sN>ONim2r=|Rf(U7dLVp! z=AXadGkoTs|C9W4hMb7`^LbEoUgP9;_SzNf%h0^g&8hHtP8wZcdwziRNUz_CERM8| zbD>dTQTl{j|~AJZ+11LDRI1kp_{eP{4=>eT+4_q1Q2D48`nWUN_5F zo2@jUgR#)Ef9JG!@3{5h36vpsk=dXkr_clFCvZ*JxwJGY;$hFfXY__D9A=nnd#A zJ_%{}v{>zgc2qm5UDO_F2D(;?;(IV7aw0XIoy0~R<^cUsh|}a zu{*-*RE7F>9*`>%lVhNX{9*X#+vw`_V<_Ax)H0BM4uPw1-~9&t9y6Sb@CB;F;~8qD zwDy~=Oy1lI3!51EtOJT5xtBt-FR#iHD{Rtvn~_|Q`)2Kw{~OaxRcLa4*inB#4Dl^G%IsbbD`te8eGA2 z;9y>2z65#q2rEHhW;}NY$}{QtDty<^{BtM^aolleEEPjlXDLXjD!q^{38k;0R4QsW zv`ycD+K`&q2_EPRXQY$H*$=XwWi16Q@}0TRtZF*OPUDZ{pZDe#a|gLQoXQD&Vm>9G znoj|GJh>vL&gY1mI=_av;&;?E_!vS>ZHfrspyg}qmL;) z-q&h+rw_nC4?{QU-{qfQ{>(qO>)4dN9fZ@g)=hJs*~Cn3J~Or(ql_j-KHMiE{g!@0 z->q-d*Xe5!3-8pA>et}A$wm&NjxoqsZ`?5w!Y7;n#UvlJPM0C>C9{hrW z?iBZ_TN1p3w?w)BFaLby|B!z^;1A`W_x&gN=Y9T7{&~m05dS<2TY+r>^}UhMQ<}>z zfPI<8PJjwsXK?O|vME>{cg{N8JXO(8cAs8FccQ<7O5}Io#wVuELV>a%`2tp_2I7xx z(BMq!o(2D@kn`4F3k~nI_C0I4)gCn72qL68W*4(8POW1+Fizs+erL=zrWzCROf%*i zt6_i7egOXYeC7wJMCwceWZVj|WuY`!i>(cfz4FjT`if1=+ROuxw3jgbpoEzgJrDb# z)?1y9PhSFUvJQNJli(2+B?H7dq9u_SMClQrt@uGiY=f!So5)TyvXjHYuClsgZqj4j zF}InM%#LOyGdsEnT;rYb(71uj+a=?wao2cZL=2Ca-Ykx{caXWt2NPNkp?E#Ws&1vjZeB7so72tSW^=5x zFnro{W(qSIKGK=l@OfqYejoh#X7eJlacQCSG5~qUdsbq2Ph+90O@f6o%sC9!(tqcl zFNbFP-;sYl_>;H zw2L>@#m@nJeiO>yJMibDVNI*x?{n}|cSDoE4Rrh&>N0qK-N+(f5S=6D!0Kcn;*iH4 z0Wwfx_cQ-|pS9YWX^pUYS)HxcRx_)y)dX7ZZK3Gi*BWijvDQP8|GE{l;@P=jwR(dy zaoB!kf8msIzJXHoc@VR5yRAXxIqvFiHqZ~J5{HNYksgXa!^jwrcPM1kYC~&qKDC#+ zL50D2&q9|#)Y}?5?){cbt_(6eE-E_oF@2@thp6`clZPbuic(A}pnPxcgyfkV^=aHa=hg=N6Tps4%g51{vb3u;7q(m*C~ zH8Bt@uuO#E#=0v(cdF@Tb}djYV?a1*1JYM2#{&7`puG~r?{Dp9pqUi1v%o{}*}P34 z-ZXH}I~Hww>_m1tJC|J&XS4$t19R}5IcvYPMP#X~;iS$6N$PKH*} zCF&w8uo}F9Fp-KZi~Rj0ux4V(ckl|bLT|MtPVGc$DYY3pdJd$I`%sa3MZLlE3TwLu zBF1O_c_FYxF!Lyxj65{ zISIgZYwQk2Ugx;`%8dtFMPuyba`5GTi+^4px_SRP|GXMllnRIur?AFRL~Ei1-pDYN z#WsPO-3fJ&OfKU-hJU#lQC~NFm4(5hmq6utV8_}Azyx23l}yL!9S!!$a6F@sdz)&{ z1q)@Py&t*%+x9z~0ZTj&zC-Q6x?AMzajt{$n#3)PH4cRDc+h?5vdGm}0X2IHu?cTY z9N`0dwkl5SNHAD-LJ9ma$YwG!GdZYYR3-S?O>y^i0NtZ2)rIN=Mo3F2zE{UPSO6UD z#1u`%f$)A56uX(=)HDa%F9XSvPr=Mw4K{c?P&v~h{@2KI6nXOJ`1S%FpXM}Se&eGIq(Pj{R95sSXiEY#AQ%t88QVj+!e^iWLMnd zQ$c~+06ymL$v;2uZg&^B!@$z54jY)t<=l79P3M@i6%^Ac;F(6FVp$iu;8C!cIkBD$ zutsU{q{XkkbaFWP;W<=rYJ;ZP#TnpC!1pG`IpkcyNhP2Flhdt$HTH$Ku^!*eyCCu; z1f#bi(TW&A%p|@ij({iaCwNpU@{r}o2CzT<;Q!2qe;tDw$pO$Y&yg3%%V20-Bd?H` zaF?DYkDzM16@JY^@Op=m-+&}m15D7LX_+>x_0RnC<-~ZzX&{vnlrQA2DRU8Ka`#Vq}M>%~!BWeLoWL2jeR$9abBnXvC+S+qgGabduzT&29-#?47x3giFe&%NPMNHfVG^|RnsaeTnqyUQB zBls4d`RDyW46f-GbH4;*fkJNb0VuYI5qGVHZ#KgjhfG^P5Q(ETy#JA|6tB6j)PV24nXM?CWkQk}q{;;a(Vy z+o9+CHFf|i`)ho3f<5g4%R2<#%~bH@A(F{)>WYvRaROW8w(Rqs@jeCq=vM)tg4<4kz#Kks7?+=CquPq%*mw8c8Rz<=utKYSeadhvUb z{c(J6p28y+-4vi2mW6-O4cVi4_5IiXXZ`IFh ztc07gE#Aof$b3%(XKx->w+ua$-{E69e1rv{O;17neK6S*H)>Pdk0rs|P75_ii+D*~ zB@PoY#5}}TU2#X1MD{8^p(7$V15aRyI~M*Y*f@7zW#03o+MQ4KviNf3WLa8DqwGy@UBhvT2O z{LDWm6zoA1ZjCPRVP=u5u}3EnZ#+WQ&OokNqWlYJLi}1n&e6c1J;vWTiNCuV zH_R~HCG}9B%S3t*NnVA$UJC2_HBpJkMkpY;T!1~8gYR@R+>aSu5fRoEQ0!MCzUqf4 zvKAtaoKPQ(?=X&ztiTIo0PbQY=O#YxAnWtgeq)D`vyze1`3kW~RYWS^;x<_ds~qdR zfbB^G-=YyBkeTil_b2zQs}R|d+vx;K@JiVFyM#e}0sCJC-q`^7Z>vEqJxkst-;x&C zgo)7kpBY)6Jjmn}#FHN^-fYOMrobE$LuuqQL|(_pE%5lplHJLMSY-xycCT?GZ9xRr z3pYqXoK_7n)^6O81CW{c%s)SaJmWTdoxL2jjk(B6&O-KhzP-d=g$&(p`xvr3kL-}m zJ1N02t^!|Surm+d;6=of@!(t3cE5!`wG-b_(@l;3?>6X*T1}iJp2Nqsg_D)Zw! zbI^jki0{yRocAX1my(bn#338-KC~u^V`?DIJ&oGH5cqQW-A{#oo*&-b3%u@&8)?h=o7?cjuqK z_@wyf8<9D6$xI;GcEcMOgF4j{%ApdW9$6IC$Od#Px&zo=(RjL`bEO$7ljT5rPe;qB z+TEfK;4K|awV;aPwt0gp!2-lCrID|D3~Kghq8?6b034uci1l)#i}wO@a(!SI(>P&d z#Mhy|(FSzz45-h&v94N&(Brbinr@8&BfP)W2Ty-=`;G-Od?|XZkD#&~XG!*#c6ro+ z#>3uT0U10CGMK}hjm}-i>y|{P@CxM3d7>!dpCzyhKl9I9<6iu|`R6nLRQ&UTWT$_~ zKQHjR@y~}KD-e&kff`z8H=k=GGM?$QM26TxePgNJ)2;-YVnSPfr#06aXte>)y^xg| zlayYI!|WD;56)7oq*f-Ypj8E(0|U@2umd}8Kzpt-YSPPa8eBUMJf`{ZdpP(Rz1@wN zkxYyH+zj~7G`hfgz`MDLd6y`h(}kd)#ZgI7A8$zy1-pJ19ZNr^Rhq#xU{WR(ddX5V z$(e*;@0s*V`YL?@y>sKy?@^J?KwGG!uLhaAGL@8if_i*^vJ9#O7tnjz9F?qR@XNnN zcEUk5U=nO<8s`=6^|7coX0=^>#ha~3=zXYW<+hSwRqxDu=*BvQ$(2KRj+y7oYv`I+ zO$mKk#jHkFKWhOxBA#0b>?pe%yo3vw7AS^X$p*yU>EJWY!)c9=Y`_Fm(Fsk%u{r> ztVLf-OOWq9^c}p*13?{(Pu)c3paYqWd`E1BS62`o-5ylxO1Q3b1Uc_&s7BpH<*_?v zCS`QfZMDW>hf1K!-Z3AdOKyj`65WFn&3}!5UIm$-BMu3#co;JD1bm%g#6iL(qR2t; ztV8ImZ;x(<3*ZswLwCm{(7|ug7U+_NnL1$a4q(PJvzf&pVXiQ3@WRQ z?ez9rYri$wYK4>NgA(XzGsc`@4n)^rBlMw{F-xF7Fv=`v)-aozT`>bO-`tKJR?XyA z8S9_rpGSQV{&`QT5PbGsWN+BCx5$pQLr48}#0Aw|!94;SSORb3epJKD*n)ilr1p+h zVe~{kF!y5j`k0N)Qs@*+YD(yf)QvEDA#D(_5}Fy!B4%x~8~P?UnO9BM%#N;}DX_Js zRS>oHEw&#W%6$>hkmwei>Rxlx{0INMI5Nh^J`w);L7PJrXg2PUEa=`pfErH&Vg>Pr zD2z3J=ARd48?n9EiJ-Q|fT{c=djNFX9qf8^D~|^4s}`G+6+uqe#*AcY!V29+XI%^Q z^<053Q5NTQJ#v&XxepO*QsOwObQ#=>cpra%{`qmsi>{FGa9&F~(@=%Y?~X&hH8;92 zt`S*bbIy^eev^Mb`!B;k?*{)qKCGroDzm}9gsvKp3tLEoTH)VpIou%ccJ^E-Jl(-Va$o|bxF zeKzLl@9L6K3^R4>jc3MJX0*A>WURXAqcp9$c8o1xCyzQA5NADh%Oe-Skzd1$PDc&? z75=&ZpXQ(MVTWL@-ez_&UD0`d9vzbz=+jg*{DH0Lw~Rw~P&V9=U!!WZ93A2J(QT9o zv&+4#G}bwDh?&=XWvnqe7}*U~Ka6#B)hp>)_4v9Sc^i3xx!uQ+R}nqp(KBFbuq`I= zcInSCIoKGp1hqhN_23G$!}M%lR$-`S^d6 zf8K^>KRW;X_OI~Iw}O8@Ap0LLyYO;@lSKpI z2-}2{!ZqQ(@IbgF#0vX_<-#zbF($M%%x4dRZplk-K3A4|#*Sq(uzNAR^hfZ|ds?ro zcDN^-p?9sRd&_N#j;p5R4YDC3+-jIvF2t;565|$l%$DbtWA?lU*i; z)DMVN_Ys9L(VNfRif+^`$S3YZ%y`1AYd$vmVfH*muYt+)<&nD3_xM-&=SzOUKhOPd z^3SIUCxnQQUMwfJ5_^fm#WCV2aiG{)tOLD$MZ7O;5PAr?Fl|2{dV;UH$y^rhAn1zs znJ&y9!9VY0y|lU^1KS^Q)&%s?Eg~|Znpgl+5GCjz>EgJVtI`N?RlXwl!Pf%@HFHRCWh($$CJR^)3N(%vgF<*v%h<_aql(Xzm z;Q#pi^Oc}tthaI_(_9X{uFcVj-5zN*L-|L1DPf-QLdb^|ZWW)338X?&J*kt_ z2aNE(Qdg;wR6>)M^bMx8x1w0LPi$lT}VmtBszZC!cuG!t9P}$4voOb?? z{#7hAO&w|`oe9xyNz7nZ;*N7A`K^2!VT|xhC?(Dm?~3W58#qJyLAo!QlGl^elgyLA zLwR0;^}bY!mI^>qaJ|@AB*hg%2@sW9@K2!&@@M3qCt<#4im@lyn%o7hCVzr2g3~97 z9mNBpS85>5k#RVG^@5m0=72I(SDI&*F$J)Xb+{W`GyVo&6N+?MVLP9T#ig;*8Hx84 z@wD}fhVovFXS-*MXBG4@dwQySQhAIl>a-Sds-d(UkTBWI9{$mQiqa%nk_oJ0Uw0)eyJT!>>}vW?Sv-Jc0L;vhIuH{er*1ESInnxGV5VRZJpf!y&v1%c0@w*Fxj0- z0gcEmjF;WWR_5+;-Ju>dOmKugxHD=>nQ}tqJ9lavOP|JVBl+Pm~AC?XlYA z@)OTmyq^g@`=q9@J|jgLs;vU3O0T*4++OzM^Un_&?V*9O&+2R^$GpT)Hy`nom`_%r zOlmD%7dn_r*^=B%tkLDi3$i#~w8hTQz|7$p?l}w9)=F|;c`?*!&&rqOOY#Y5x6YJ1 z%Y|gqv(wWN3d38Z%F=as1rMRD6w8<3S7ImsZ2a>l%oO+l7r8DxAxy>HI9H4>jgVeT z)uHOCdWy^a?z#i%kkfFACrHsYqKH+p)fekC~Uqmzq6XzNibx^J zx^v`ssw{2No0-brjb$S|8fsL@MxN7`cegf9GQ7j;Bm$G^0dt6UbIY!pxf=Xv)78Kl1DVL$*ep1<{ z%v3rm`IMl%UT!EGo>`tuo){^ov|9W^9ELljBtI7%;*Y~WKm4EkbHBdoSNP{!>1ZYk zdzYQc72_Z9!-bSsV^Mf;g>h>nhT``bXw)~u3J)t!6i1Q0UN7ejDp!?_$^fMV^vzeo zCOz`>#%(!7(!};+tdLij%s=M-2>y9fBQaDzWZ#*0cvq%zPH{t_zKPvt_&5M{6OT1n{5MY1aYNUNIEK2^c?o&ht9qw*9K4Fwi4f4+}q6ijdy^zueYHMBE2LFb4&i)csYgF?U=dM49|P0C&7 zCi2CEC&C0Vv$Plcc-m7=-XW(1UEr*e$XnYx601Arz2d#;z2rUUUE%HTE$?N$`;_(y zuPm0c$}yfSo(0&+cH#l34Y%Sy7XSQ)+9#Y63X6jx`SpOl!x&^%v=X5&bh|UwZA@f` z{=<1{5#52w#Rl1JTsJ-)GK_u2B+@3S6nxSO@;*5;?ut7~R_xm{?`iJ~ukB@hq%YvT z=H2Waig#02)_|_?K<*ANw!7z%R1M?^QEVox!wvGW_~$vom%>A}yph+So< z^riD<_hs{?@$tUL-W_;5^LrmF3B!7VKhZy;)*k4MFxFRq7@2`|ON<^vYUF5yvP2el; zYvSwX>xqwMz9?T}-+k{=Z#{5p7Am=v-Pp;EU@HuhZX@gbNAb@W>R%iAP=mc;ZMMfd zZQUp$DH$M7Q_JX{Oj*_k#m9MkJwX(Ai>)L_n&Zg;Z!Ntt18c17UFm)9O@}?~;T!9l z?wjHp=KI=L(x>=hy<MZWcTmioqFl?8n!-paP{eU>WOl}++j@&XU(X^AL=6{`zV zaZ7$|{&_pFi44V*Hoi%(%A)RLp##>H6M4c}wmBdqfW-#lM?Ul#1-L{PI1Dpf#~E)Bbrz|&Pai0sE7#XoO@5nUYCaCdZct;I!tFhKk1pUMR!&;a0O383SrkrKvXLaA?#YbRIz4 z>PxGJ+0R%5jzZ!{eQi;)A&5FAkm%%^Kf6reda3WA8cr6$m@~S)4_Tg08ZEbm^vtG>bnorHc))IS&(*mlk zX%S=HrS{RYn9lIb1^y>wJgSPW_`Oua^TIPgrob}`Db2il-t*5V`}X~*`RCt(f1XQs zeuaPDkjcT4TrBR$ra}_&oHz*7mII!;$mX@2PYH1{6}^Tf)am`wL% zLF`dj*{Y=Tj)JF?)z`(h+;)X@xs-yzEmZDz?(rd%zpd zSI;*Ix5o`woiOaq16ZI1@C7pXu6c)eQ+PKZ%d=TdFHiQok;+N4#k;~E!#}U9r#C{z z8FQ62+-~{){&0!AUHq}&s31K$m-p&E96UJJ`m5eP{$JelP!a$aSflGxh^u}4y0 z1K$MS7T;OCi+6mN;qxqnhn~;(2H$~_-fLj?JqO?If~T-&w)7GtsiDF_{*U0F@6^W_ zjm+GZXg{_OIm_H3L^HAo_$F`Zqs$z(9hU>04>3YpF(L8+jXiOmK{A1Cz;mUVca=Bf zEr54&j&D2m@~rPTex8lb^ZWep+N;3o^i>>0S#MA)*)7GB+KAr^Z~2eUKmRc@3Ji(# zre?-kYwc0!iHssrLXYhd^*ueDsf(QXD{d1`YeslGL!{J*Bg@OD zbNtcGuK0`iqa)&vc*wP`MV&62@JN^?mPDm|fX5@xfZZ7h-@J-`sxFfSb-+eza zku9zKwv2h6gYH72H(7(q1fAKl%mTJ8G6K)|WkPNE=F6mFV7E1pugF!glS#bYkn;%R z4yogd_6_xo@C|^+TG5vRHfNc)F0wmwlrNQya&~zcs*};uNin6^L5M*&*2m|cAJ!)s zjm@lw;J|wF5<{t$Ovrqr1gxE9*M=o3Bn)4KcB0&L}#{U zp0pO)-=N<-H9C~fkV~m==`xIuz0NM;n(~RzPu5dRii&s@&s9$&)OJcMs}%xS$l0*3 zmNzvlO-Ww~tT3rBvIYDbNo;tn|ML&jyl~8)IO%kq|!+_iu2mSJ0E$-cgWHu@g?*5e1`XiHwIpLKJOD{ zvXWK#L9QSlL&w1Pl27V}T2>;V1?qM8pB0Vy2tp*m}|DR^4J7s?pL|}h$>_XD6sCPM>17d zg*(HI;tL87h4JE-sPI(v{N$-79{{y>jB-=Sh@AOs?+;knD@4vOyw|<^yfd)!G~TPq zaO5HPV2#K?dv-}_rQzZo)awTGzr{aaX5KWC8cp=&k#|~IZ84~39o3s4DP9Y<3mSoi zfeHcBzt2C`-@;$YpVOb&pUq#)-@rfEztR8NpC26NM~FT41q+6Dhsvur)!v}=9npG4 zvguFtHAWXRzvWtI>;+C|w;18UT;y7+7y1M#_9#1oE6Tq{CNfIABleVN&o~cpg;;`Bv{d#9n1l8#t_di(2zmcsi#+VLX8P@(j^0R2F`Xe_qiU zU~j@SL^`vTu~L5(DH55iJr7q7Z&fp>i$ZBa>w_hOcLQSs#R3ukVgDR|Z|q(Je?xy; zL{>}uXZ>uTN?=~VAE*~R6f6|l8H!S45n*xK2CZ>K){p4JjWY0BW373=sDFG-<)Sru zA2X0G%)R55^VNlbFkQ?d?UtgzI?ao|09S6Ptb#v~##`On9rkAip0VB@h&nQQUn}dC z7N`uYmW#IL=D;6&xuW@9J2tItE7LP1b?GX~EF z1_z1+h`>ec+#LUS{}}%?|4RQ+|66~CK)b-k03B=-JRK~7eJrM)RGWph@O-UI#2;Cu zw=hzgx6S2NM|7DP&M|iyx_2{Duc!ERSsR966VKz*7NhEC$}PPwXz71kvWV`R4(n zurWfv5Xl@Fp*;eHc)wae-4QAox)AIZOb9ag;6Uj>{J=|ptp9+2um7O`g8z*_QJ{Qa zMBr2)S+G~|MzBKYhfr~_!Rv=#g~w|-BR3-BL0XI$JIp>-5!B-^Mo-!op-RCAz9IdKQA^`up?G``NqR^Up_s7xjDd&(r=*`RDD)%$QML zPB(#1cbM(YCFS?>4bXcvNMxi@=nw1y&hm%hpPvHbtevq-kEcgRE^5WK4dG1TRca3P zhfsr%9$Xo0984N~5ZE4=j=Q2upkttWV0d70;7A}8$c2-7B$xs;+b5w$>P4K@d*SXH zA6XTttjFn#jasM-?6>;ZdBJpxal2x6@EQ3X)r?NaoM8IGJ3oc49Zpz;8pu{Lue2WB z9>YENQD6Qr{PRr)W3SE+I6 z!hXn1U~|A1XotR?(Sis4^B;zPzU4pp=Vi@BhM_mpkAlLtN=u_H3ug)MR%@wo;0{*} zv7uAJIl<^)jbM>rj$rm+{$Rym+u*q1?w~)IFEliCDU?f{ty1B>;b6FrMu10{Nk0Ui zl{Pn+4J_8)Zh!57{o@WJ@{v!-*;F|?j-Jm%vG>_Q@ak6ad4!!p0rY&Pl158U(DC`9 z_~*}!ipKY#nT?9*S}!de9uoG3SF07&x1pt>Mxhj;r@{R=nInU}gFS+Mf}?{=f(L`I zgXu%9LTf@AJh{zkvha9NUWaK6)|eJEg|$(0on;oa9$Ql}dG^X#2qLI~)7pTd==F4M zCdkahP9Em!@OSt&nAT}19v5>-Q>AC9GJgpE`6bLn&caT%*RO#meNwBcT?u~;n)G6| zoT`RmLeZfj@M2#CV}l2tFsxL z-uuQdGo5uDwYo2$uiVv53T^smGAT#`?P-Zwj~v!L_FInNKNkOd;D7MXvwSrEdFzkP zKR*IBgxT-;=b_(?e}39%`5XN6T@`2_WPv|XN z7P5N;F-dmoyjs9zReu=v#Dq(9FJJ-?5$0abAPp z!>1Ak2p2I?*A+L(b1?<_06R(}F$uCL7!E@3TsjT&9g~q=#wOt= zayr+BKZahcewh4>FV+=51pj=(|B!!P;UDtPkNztEJeI0Y-=Lc^*O@w)_~-P0^3O~BL;m^I z_x$sE(739A3Bqh3MY?Po@M@ERJ-nAEL5?0GY=!s3VHUEaSW9doHph%vbxeS!!+Us5 zSSR$tTuYdb;al<^ej8Vh^RwgGRBQ}WlsN(h+IgxL^%Jy{&O=@BB&ZI@pqF&at_U@V zx?n1HHcd>AXEjdho%ICzK}->5i@ek}YXi0FT4s&c-h^+3e+r)s$A)hr?q{?NT4k+| zwqAP-c6R5;&Ik`BjD30vW0>(0wVjh@am;4t0HHE7R8liSt1%;#Ofy2=IxSSa5<>}! zfYQ@FrV?gdd|Vgq2qq<4^J`J1{5|>S-$2=;FZ7Z|fMh@0N^fn&oa9|&fRWtzQSYcH z(a%IiMao5_$Q5l3ywR3gc`c8Y34VK8EerVd)wNFW-*#$GwUm*@h^iiJg|Eup8Al#AwmL>8nbKb1et zJA8K7&EA*``A*n^DeYatW?_Xe1vBZjg|Gfe{yFc}^Oge>eX5rau+`Ji&kC zpC9_S_~)rWqW-`9^DV!@KmUm;iiuYqdS%C$v}_;tB%7FPj!FA_cr&Z<1NasEQT{$3 zM!q1PkPsg{)B_&!$Kf#y=4yTA@(v!aWoHx_(>*dUG8y@s!_cKspuiKYuhJjt8H`S#+)`#ebDc?9Es)bm z4TYPhc6F%nrh*cFoZAG1n(R=8HprILVJZXYg171X(4D)5X^?N(U6^{w5AOF82zJsRq5&^jh(&sT5B)-f5kwRP_3r+AxtX`BOVdOp*$*+^`Lpk&`qJ2sMD31 zS z+(>Af4CZ=qtueKm7jrB@bmOmJd$Gk>nmx}UN2I!ZjGM?(I z^)7loozib=8^L*RtCfcqR$@)S{GF_dssfD?x0XW7t5t_bod#R;MDsvnaUi(MpP{1I z3;L#}Q3?9sFOW+f16BCU_An^8rzd_q|2#Q$6$+HO$R|+3EJnNsO|GIXg2B=dJn5rQ zYt05PvI?tAZ@kxcLF=oAo?4f+8`@57t~L~!Uk$ZtS~;x@bR#Nj^|Urxe{HI^0Y32~ z81^Nh6&VGkNFOv_CmWXxpVnj<@DXV5&d^8dee_Ox8@;98N^hrk(DtP*(3j^zZ;Fd`r^<#-b?z)EL3Z19(A5l zsmyeJ`k%o+-wQ?lk|ar<1{Jh2;UF%7%2&&dXWxdWZVjH`SMwlZyLwRjmW+$AKf|Dp zT-Znob?A5cE&Y_fPv4ByuEb{zzT2ap#3bt*-P997b+JCQz?VXm_KT6stZEL0ZsH3w zF+9^GXbf9c5vWD)0vjV6{8lXg{D03s?+LC#EdM;d^$x6$`DSmty0SqT`-^c4sui*P z^Lxf8C?h2USE3#iqUV_VFo#Wm9$E)c)Ou(nkl>!R#N9svjjv>2)eI&!fSbdBQuAx@ z&v$^K%3zJn@QU1xIZzKQdIw?_Z2LPR9;i?4$jMM~zm1;WBv6!XMD>Rf@;d4Ww9fx= z{&~9pmVa&{vix!U^ZzgZg&P&YeeKEIN~w9+`6plT=bF zFPH%>sovmS%%qlqP_vUdfPIa|=O`lF-PC5VFy>Jc@RI64)qzT4ddf`&a1suDr`0qe z|Nr>sA7c6EvHWvWHJw%pXlho(IUNWt<3=zVpCigi4iZ>XP`DN&=kXBysN~=sHAn7a zDVSvUp>CQ07PcDHqep;*xrID|bN3E5NI;e?EtMS{sp6m{Q~(d65`HX8l>iSc2l!Hn zKta=xm%azp>FqeDqjBbIgPWNY(du(#$ku|a)Cuui7DRL(z*gD}l6?=vb$RWCAV@w& z{IwA(-9vCQYk_Q$#YzHF_CLx$XYDkx{Bv-w39vx(fOgt~>Ioals(^VBLjf+>5-{e7pmPfzI0r7PArJ(CT2? zSF|eNQwgl8T4297gMI1&M%q}|suiF`9fJn#3rj?8E(I)EWyGrkVAob7cXbm{D@`OL z@}U;m4y@o=h+dBoHxb(zQ2t6o=0nAz9x?^7{PU%tgHH!t?fPd_5kF%E` z8*m;Ik`iL`^vHnLL0(`4h?W}=qhBXp{Xvw@2BuUM(3byq{PPXqQw@iQtOMp)dSuNd zya+DfU9j|TwB>wuYTIM$IIj<&C3^&?Zxv4LRIG9+SbN{d;2p8HZ=Y}6>>gP8U=Xz@ zgRH#*^y!0$3GZOK?B9seIl&=rh#c!EFh{rAr|kQvV{t?ZoYtyDYhoZs#8Kd`T|oT$ z6%lMaGA&p^Wx$ATh8j>GBwJCHPwOotixjUS=ey+TC%kzcJp6Vjq)% ziKW69-2$~~3)ooSh|&!~amq@>CrtYdGTDbvyPpn%Z7lyhmVX}0KOYRn;X3;$YObHY z`;WXt72L=ORA5$vJAV}xQ3B&V8JV3dPF4rGv=b_ABXJvN;}%AdYZ2pZf#UmC{J0T+ z7logljb9y!-_;qvyE^`my_YyhJA0ye1$lO(s@iAFn~HRtGI3J>K$FurFPaO`iaJ8p}WbpZMpOWBKRkZt>wZ zK7lviX)VP~8~`?SU8|f`0GZPiR(#6^Lbz?}c>OE*GLTVaEf09UsjX~QVZ^bG5d91R z&1AK80EEo9sO%&`?y4pt;&GsI9z#Yzu@izWUz6yD7yD`=ns`8j|LOemyRrOp@Uq@m zm#iJu0#t)L!HO2g4*mw>f(*L*WAMK(nx{}VIE-r7LHrnPo;ELmmGH#;fSu*7#MsYL zu(w@7!CQjY%kLnZIqXb`UE70Gv)Vol4j7Bhgo@xSjz392GhgQ|y2a}#_TQT3@>U}cvAEju%EmM%)g1ltYrC`e7y;7yu< z+>!>>omcobuLYsEJziO9;Xj_jcP#}Kw5eUxPKs#tF=oS7m(pGc4EazMM5U0yl5@hLGf#$Z}zJ%C3AzlS7i807(Un0K1 zZ{#KGBBwKp+y<`XYfzVeLk_MCb}W{EzJXeZ%z1yR70zyMDj`noBb?c#c(XJn^MmU8 z*XEx;!ReX~4^STdz=D-IhT6w?v%6W}EQQ)+QqyHxMi?}xH_&-{ZoD+!8lMf(AWW~B z!pvosH=CkTHx)fB=g|n<0ZWY-qNtL*o{F0-fO?M6A3~-=_AU=G7H-gCwB5p8&V19Wr4gSj$V1Tgeaq zaT)o!_Q=6lU^*;;cPobI<+-`voNjgkkvJvJ=Tq$7MzGvR7~PE)Ms1^#Q5wXs;zmiM z9D2zb8tsh%AWts`L;QyE75kajY-kQQ*O|YA@}A9VihiJ@n8{9uot%a&l49qC?^=YH z6^&}ekLI6`{6+cauTZHT63ajTCHd#)enI}Z63ag~x0^lC=kOI%R6~vOMqJ|=sGgJb zwt6W&l}>}&dl#L$hqc|>c1%v}(T-{twENIv10uc^PQ zx1m4(Ri(A$T2ZZ;Hd@=FJwQ)=d32~BK_6%tLBsK8Q$NiW5^KpMd{`rCL{B!I7 zntwhTFNi-s|GdrL_~*mP#Njb)>2w45l~~R*?C-V(QQs+L8&sP<8ltCy8AN=b!)iqd?!n_N-OBFB|US&<}3gAQa8IWI`X zL*xz6+)t!5QkE+pmEy2JerQ7N*HY`#bzA?};-BwFCga!OpAV#E%$Nqrp5!~C4e(%wUn90^~=5j$Vau5_F??acQo}3g4phu)d(lDu`)L5zmhICV@ zn>1RAlFmZ8ER)<;UL${!i{WOzhuYpYHL*4UdI^2NSByFSdDI{L^MiCjdMA~YT1h4% zry_qf!2W;=_#LyRdETgiHCEEEXpOZu>R_nUZdd9mIutEh%gMo9+$ar!Vr@PtspOPM z$rd>&fs|FMBz4EiuSkMiRh}(BmkYsrhLvXOIkm91Rm+5#rkLlSn{)*Ig=&xZyb5^; z9lVRs5ik@J_N~p^Mh%?R3h@8cwcBcYRaRyyIhC982sxMhR$7O(6p?(=C-HajsJK(y zChiiW#hc<+F}_qv>ML!PzDkAViSiTd<#L5n`>SuEM)yc-uE+Ave+>WJrhCy(FnzF_ zOiqqLA9h{)kd@Y&YAT53?&wwZ6IyZj#xm+fr76^gC(GI8Thdsmn51Ffrh)EWR?I9W z7Jc|6!gpoFcH&h0iXs-3#z?oM?D!|X%FUF^N;x%JEra|)ZNxk=$3K6|wq|c*UUoN~ zk{(ZeBI}TQh(yE?`>|EVT4#F9fyN8Hj($ojqU}-hs|S^G$}PDolxMa`jc^A~i{qiB zm`=39&%;;3C!q;_Hheq$DeQ)RZAWp1_)N@#8+${_BQJoiV1)8j>8S?PAsVMI(6bpQ zL3WkRxmI5LhTR7hy6t3j>Mb>j{xSUX>5M|RqEAzqs0rj-)OgpR?$ORVg_p-9!>>1k zI!h64yP8$qq~ul(%QfXU(s(Jm^iZ6I75*l^hQ997@VM}xaNqF2@EB-E?hii!o2H^T z0XH?ZG+4SXm6Es0X_O^OGQ2snBM($lf31%M&+V|;9Mqv7%|AET?(AKr6tkLU=yud` zoYn~9x}DvgV7B1ii?dqc<&nsU&>v!ce!H4lU7*BM z=E^DLtx`GZId-Xl_z^4Y6D|WaOf&R8^c4D#uS0SuUbsNGE%x(XIJMXttIRCTlx%2y z2IT>YrA!0O?Pu`Me`j(sljs*zVQL!r480{|QKQRdjWq8VIgQEs2l$RXSmSKPp^TTE z(8bOq9T(e*E~xMh3Kt6t;U}S^p>?5!p}A1~+!#6uO_xR#l8BC|@;!T}`LXQfOtUtV^Sy zXu4V~jhi?pTs79d?D7AE=pD8 zt8!CBuw&HJ$o9lM|2&#Wz;vSbV_u{VxfE5`Joa$w0_v)rjDva{y^D4ZHfJ_s@&57` zskfwxlf<;*sc?7bxIYN3fHG~xQ07pA5G2S$2}4<+PTMmS6?zm(9qtuA6V3n+V?v)k zEDeE<@?tPgZ>R&b6j1t#FtV7B%vm6U+Ms3jAk$HIK_V`~{Am99H1ZZY_}ki>tRNIU zCK|W(O!{!`4t&Q#g;aXOsuT^>(14C| z-OyAhxh99Ty&f(sZh-Asi70ljTt#^Wic1#lrZ!5?i<H_H{>k#wo`{*8PXKsdCKv^&q-XR8v!mF{j{9I}x-4|O! zVR&LVOZXO)>dS{*p+~{}(7~PtUH&N`5bS|=G#e_36MPAKIRc8<4a6&A4e7SjPX2lP z^X2U4zX1QdAUl#dLko0eL;|PbKT04nKWM5@eu&gBXbH8B>JcTeGDLoXchq^Yl6W=T zA}qm{mJ2D^!%@Ko!F<8w!8k!*FmW(PuzGMnaAWXYFgMhdpN7hZcZaizo5Z}*&*h&- zvga5VQ;nWPogoRb2+_~pikbTKW;I`}^DAaFbI2s8y}Fi)^uFe>;aSSU0*B!$|CABLNXFU7u+11f#hkOf%> z)>+K+&*QLln3?ot*wzwce`1sU95ax0&1uF(-Kp2o7OJn2$C)h$@ghDamJknz%ff4P z3wc9Ff_;PegJkd?XbKwwQGwNgUEpH)1F6A3SrPmatQ^`LN*taZ_K0)E^wMFeiEPQ6 z@S^+~{PQO4LgqH@qbpLw$Q{TiC$g(sqs@axNYAbJ(e|mfQd3zY6LMSWvRG0)5H1=% z8)^p1$?9OkV1nS|z|O#oz~Dene1-&O1$GCX1ri6F1vdlewKhOWhx8zo`&zNLPJ$fQ_nEZ@+rCRnVYmfO3T2PIl6nzI7t4_*6z zY=)(GIbNJ^U*i392W&wQ(Q={3*ro<%s0B&RwvxYH7KZ1--adnJx9ZHuY z5v^8399JlOI8-_GG#DAo7JMF9h5f4#$P5-woIt`rra-wsH?UkD1u}xNa4%Rov^$h5 zyeC{%{Mr2TUhI125$&Z*Qr*dg#A)d4q_nD=!;KyK8!e62N?omdmP^QUrGVH-JQFUC zHI@(E4fcmUy&9MtsE3ttfq?(5|Gi)K#|3w&BUb)CP%OA0Xa#$PK7@LJt1wgix%~5e z@RDhmx^yIJAa~(Eve^xBBX^)1Af8@T8?T;K+)5L?kXfmdcmq-Q@lb`(tzZujT%rSm za7I0Wcm8YsQ~neF%l;RBCXg3L0+T$4F0(Z zt>KQ$eEKAM&VD0H5*_Us@R6^Kct%-$Al_G>kh6-A&q*nyq2h~ho$&ck)zIx=*Pt`F zC(tF3H6Z)1`gi$P`Iq_E_z(E+`l&!+*q?KORAAP84R#8>3-t|i;wli`0@51TotWmI zpXGVJ95z%%>1(uyY67*svKSF-5owMnh1-O0hw6nM zfN3HGw}GgXBJjq4$iK)x-ao=W+CSUB&41hP2viQt3A_tbL4215@2HfLwI>Y2lg_4;kip_NvLDbcbZH%1n~D-HxF zqIKvMD80_$R#?*bf&2cA{&67nw!^2le-c*t#Gf?KHn11`q2WP)@Lz*}PI38QTc^^y zsr#f!q$bMQovmr+9w;a~^deeMb%XL&&M6O(u8LX3>0uoVj6iTGBDn2Xqc8Bgf4P64 zznQ71o#k0_Z6#*;=hKDjyqB-Q z4a5uNDmp@wQYFdO#Athsb>94JBs5Ct-L)0!9VNcf5OkA(SVi0vP7t0DqC=B|$%6X> zEwIMR{`vlHSYc6ret!}CwJTW57yX_7K!Vq_N9d z9gv0Gq2GtbWihp{vO^Z7(y%(8VOfub@`ZK>OMr3K7dGdHe;#gP34eP3Z~lb-G}z1L z{;B@+{y3n!od{$@^qLZRpTgl=;RrFEbO)!ko{|(bf|%x?7vVayvzbH4oRd@{YPZECBG*}<5i@6lesM^;xR%U4m6>mi;GXAdtzE@u{^xt)Q! z0n5M7Kg3@ZchT#o{IoxgKdZmCf0REOezGMlW%Q4G8f9J^O zh!FOI?V6ga!A3C4=o8cnk|NU(rR~<%D02;13IQ#pRtr?66EZE=kd`A3XdFHjDj3=m zEFQcT=n?oWaM3@--^icKpTN&zm2Q7pe+8V@t^TmTBHoqm;K-mE92fd6yfa*1B&6Na z&*Gmq4_ZHP!a$~s}bGz24;-b|aKMk|_JOdccMK$U7@*dJ^U(Mrtn z&r=J{`Ptk__6w7UDM7cUMv*It-cl}lfdr8gKaJ0Z|`4#*flL)ln)UJL*MuNFti{1$UA|7fi!`;{>9k2a=3{p z{K@^9{H5Sc=HNAwD9|-=b4X{eUtt zfmxbpV~;^S@{$qMlj&8oVd`$hFXxj-NY`*$Cx^vQm(W{~2Fc*^K&60&xO^P^Vkz9p ztp2?49-R>xT!xQq4eD!};Ow9WS*+CIgW)EK&38#%#v9Ce3Ac(-;0QL7h>~KK{YrOK5_}N=a2D{EG--I&)}cW<<7GTlYyy851>O~eBR}O>H#)z39-N+e;a>8 z*x2^|;r`YBo7l%DfvvEt1A_1IlKdKq3@1acL?NPk~{ zFYM?H|4zKI5(SzEwgh-&4IT#@qu$dcOroCt_x$t2G0H#x%w^_Uu+y0%^jj(pRhVpv zisWYNCP)gIjfVOp?YOEd#gsAfb5>JmClG+#CfPfP6+uzD-oT)1RuLjAZg$;?92iG zcI@GC|3g0s-`P8G5NEY(@Mf?g=%-asXYMV=k@h1KnH(8`g&+}UjzRu;F@6|#06sG9 zf04g>t|!o|YZKKAice`F@046pPw{@ZI(D)ac+!J|DT0^rCa#WGi0XfiY{xDCQ)KCq zz~V**&IA&|{@x0f181@%s?04#Oo&1L_}j?D5tSGi2m2y68WO zf1WG``RB>OKi|#DOd)0fy@Ps>N%TgjPcJOfKoITG^r$P z3uXoV7&o#3IUPhd$}VC3Nd9>m?T$86uVHx1tLAL0AymJg5o^d!APc;qS20al4>*|} zd55r0C<(S;1LtjLb=P568uxJbHFpwEBhO^d9?wnBJ5Rvl_q_64@oe!#da8H?&oOrw zx68f8mCtp^S;V=+k;*Yac#lbfCERDW2s?^7OFQ5r|D%3>$kZ@VK3cz|CDFR7C(vIu zTK*_CL%puP_$b^X%!N0F8X@Yr5?mPU8LSa3jLLcTU?Eh`y9ei^^TUC-Z*7PScSPo@ zns`yH12%YnIgYX$RaYJzSUvSj&{kM!wz1OMFYUGHG5fLn^M>py<|Ca4QR{wEAqo)@ zsMcv_33D`Z0zZO(9#EHQwPKKeew<6n^+K%UW*X5esMll`vOBTE4nyx~gn8D8W3 z?YH&}q98PmMp5bM)AWzvpXXs0gYMP_`Q((;Q1T9RA}3?Au8g(ZG{HmsHTdU6(CHDE ziJ%`*B{4y05iN+rP%j%|J%pEBWjKs~CjUIg-}vWm^$|v5bGKO=`bpF59K>a!8+7*9 zP$lRe#XnEYOrgV6Gt}zRlM^uiQ3JL$p*0M>5H*b-!#{tZcQ;6ADdn~(qRXy^*g+gCZbjBHp;TX5BE834#1h$* zJ1V!7N|<&o3C8<>=AV~{0si^npUgi`@~7vYSNd=G=aoP|YiK?*yFm+lJT$&m61m93 zWJM|__~(iLHb2~^&y*X;Y2*-QA-75MrE!=B9U_gCreX^8i1Y;Wk%i@6m{AJI#g(ZbXP1N; z2J?6P^MghqkbR0m>#MAN)~*IEy87gg)>(f1VQB zJb%wWPYy1wVs?hrsRo_lJkS$H8^W$m>_K_#-X-dkUyUxXH2UZbnA78Q?w=bsmd<)2g2sEZVX z3EVzVO1?!C(6Fk*^k?QU+nMvw#rn)>49yCx6PaR?m6&(T9p*T*o|(*ag}y~9Mx@Wv z3+WDYZd!qo_Gqd+rUp-eV_yQSr9DJXA~W&AUS>A}H9IEw=apmm=YLB6`H)!tIiV4G zz%*L|76wV>0X1MMwV!%QadZ~?&&xm0V|N6DC7@Bi0Tj15pbZWHx#KoTQ@N<7 z)EH_bbrm|bap;_M6}l}w2ukfs=}pkAJxZTM9pMyxoIXhJfNh#fkAZ@FBf12ghNhw6 z5lt<|%dskziV8u~e?I=~3ZTpU_y45d-T~Ao+q!Bk1C_2cm^88c^CiYSDAv!0ChH<2 z%Gdr`^ks~PJ_p85Q&f#tpZN# zAh35=k$cH=X8DLv7fcl=2N{o&p9z70z@&y=RCqesIMot8Q zsySI6jMsRij2PoMu?EWRUD3^wi--$t^E+VjECHdn4VYwEL9G+5N1(iK0pV&CXkbmj zRxWI1wUUDf#(_m7nE~@N*k<28pYgqHTA-Z8g(^{2{JzTg{oSoduqQWLC&9lBqVqbV zT?WLT!S+0Rr+pQ)WdXFavY=!QCFT>mz_$Ez^3P|)^3Uz~L=JFrT7#NBhuBP_ne)h%pq%a}qj7Stkk`pusP5fGe0vkV^dfl*Roh+Udc^h9!9MLx zwgS7j7@3KTkBR!XU^abYgHHlGsu58Vr^ZDDK$MQQ*MbPt-)?S~M_*kcbh~~=t>Ub; z2mHD@P+1-b8c|CqgI5DRtE5%LDrn{Z{wajcty18G*RUFbh}s>js0ldZ>#Y6gcz9&_ zEf$?5x$R1zsSX6^c)fibwB?}f0_C{`(ExAt@t|1k0oUz0A_5ng600nSUZVD7KQa=e zy@j})o3WMypqL%Uy1so*;QNE*Zv6aeke8?9-QORczad!xETuH02RVROAdJTH&%g0& z^Vz9vm#u(&eakwHyoi-UEEsF^Kf$fXBN6yS5Hzb3J}sLq>s3_piu5?-k2G|I_o&OF-YZBd86tV0Dh# zH|_U$fBXj9S`H-QUSM`ECbkhLi95u5aQfV23Ni=i(EmoV??FbuZcc#xnTS$yZ9;0KIZ&b<8<80Zyfuj-#?4LuW~B*CBtD=yOJ$I+ANQI zoDs@FEE&Q?!WCH7HN*^fr}l`c3K3}tC!(E4=rY+3p34|e5Sze*J6JS5}ef8$eL_|?>LJ;n>VmJ4m%mBhUJjU z?O~4w8*jUP2LG*)?Zkh#5dQn^5%0{zd-5!Q^Oi3ifRw zRu=_dvKn72$>mt@T%JiiLJy!Vl-mL21F_3lj9Pq z{Q@k~J@#^Yg53vRveI`p_`liK+h9QMvo?U(Hv^t>sMQxM>Hu5X9C2F{e41J=VJNiFA3#)B#;}g`MNl%y~ts>b#t(T zYvJ>ElKbHO594zPQNbQ4O>e}WE+S`uK^cJ)+l;J+SSJh4D@}d{z4X8 z-&aOIWdp=QopD$0X^J8L|vi>Z2T(X2yq)UVh)}oKm0~ZaHYp$g==vpPar10hjqQji%}#M zQo~0@uTPNt1YYqYtn?iI6gJ~TPl1K%2&MMoSfv+C*T*=on_yl06OD=DIIRrv4pw~! zyvJZfUgcu>=ay>vK?r{V68J-Kv>$>|`yBl^pYehwEw`1_%7V8^HQdg=@Fpu@m9JUv zEe5x;7>K6>v6DOP-|a89hsX}5Qy0XnYhdf2pbqjI?0;3VEjXSt5t$ys34KC-Aq~<2 zK2l0#0&<`}Qh+LmPkwNGvmrl_3|Rq&lEDSPi(0@=`e8cBB){1NPI?seKnBkMRlZ_gY{kp`jWdUt1I*sw?RCdjZ+t(@9Ba-nWBKQ{l^%VUz3lmj&R-!TkQZ@zBpB~k zh!Bw&fAX#HvWHF;_QNETPE>4^Z1#)R4o5I zJIRm_VR!ZKg%&YK$3Y8i_IjDfnuM4DnWIiCR5wcxBQjz(3$Dd=!)w~ z52gPs{PQEoO8#p6^E74^v%eW-UNdbokJa8svcD@^iD1W2MQWh*H;) z4>0dilInpf>N#{lBt~~bD|#f@_50~-^ebAV872;sm`TB;hT3dWCO#uDI{ksZLr2qV zvCCcPN_0Bfpl(p>sKKBqCB{TVEdRW^6)%>5u6NcO>(#)}D4`eEOM*FE6Evpo`Y3&o zzDK{Qi+WO{tkJ_*Vq7+CBR?Xdb%>QxAzEK{4)e#9lu`=8JP)~9Z4e?6;OcX{Ocae-sHG;i) zOr=0y{XF^#O)~}1RXr1X^_a1kWNd!63fq+J#P((f;M0fg!nRMQlz zm^ZIri~{AIGAo(W;4M=lyLS-QwSm3H4%_9CV|_{F#s9?>^4H;?`^{3;OzVY}-=2tk zYi?pJahJ%7JlqvB1=XE8K+)(AoJrrL;G8n3>~WExnB(^Pz%vyHT$F2D?6cB3ab9ov8%!)|47Vs&w$p;w%%%GKiLir>MQa&#t5v17j*LNZBRT>S`-B0xaMC>JxC@i(#g2llE52qW1*l zoiXYe>kQqfYi=W2RJ_bxz&^L)(n3ja30oTz1>2b>(DYbEmxULJ<)7DtlKT`rmwrbZ zrRCOMsY}(Sp!B>_c7iP3Tqz3uFNdPae)%(|%{AGnq*jV6EukK$x z$%p!GT4E9U%*T^D*%Mu>wb4nQ30eX{rY^gUW#J{(abLN7P(a$qKZ1r^E}@ptSr{rz z6s8N)gz>^4p}kN=$RybO4SqS_o=?v|<>qqbIX}*M5lo= zM^YR$8TF8vL_A_HdXW~R4{o)Y#oPrp_+|Z{&p-cQw1?_Nb5vuS*$?ez$oDowb*d3w zxYaQWP>9*UBxFahuh1Ub@bj>T$%Tf(WMRMXSTKZyj*N~xj{J_?jtq|Y4o!G~ zb&nJ32)_xp`RTAhUtoWVaCZ^gvg~|J5^kdl(1)nv)KSz54x=i$4}ChjtUT6EGoSg- zvyXK`aTDvH*gM_3OVt-^a1SSVWtqf zolOhBXmD-#P+uUVk;9EX-fy+QRk>W-TU?`5FB4PDaoeX?oBNk`u9cJ@l_cz>j5_8^-V zHZ>tXl7GpU78VNcg?w1yF2@^3JZB+iePn+yUlmqmgk%uc)8Y%7X7& zM}4aFQ#{I6xgI!ud!$}cUP%Y-bG;po03$Q}Fio?Z);vw<1m{_VS&6OTYIpuNk z3y_v~!e7n@d2ogn4;hhk#!jQGc?+5lrZv~jN}M1Xp*|c*#Y1HBEAh{1RQ7VC*X7^! zudYGU)S~9l8Sttq#hzs=b7#3y{4PEf{NP)mv}2Cri6f1(rE|9PsPmaqcY0lkT}fQ= zT$JmB^MZ4Qv!}CwGvwIhXyPCptAr9DDz)U_a9!XTrq?$tW24y+y&rq*_C$Qtk$X|eFx%OU@ggFx!ae1B@j4$V zSi(T~j9ShePKT?pE6VlM72jRT-PS$SJ=s0OJq7B@9o-e&N!_nq8(eK&@m+_V&0u}T zINXkDf*=e>)YE`F#3p42GS_09f9^F78(qz$)@5s?osW1!EF!B?I<Kd)j$MdS-bRdggg1czSticv5@bx!1c}xCQrS zS9#YhXLILcM+3(hA-6Dx415^P`01SYuH{aD`o)T?yR-+?SwH-yADE>3Qw3 zJZ`Vo%XvedJDzQxA)XQ*(Y*#X>7{Fc%XE%#%8rhXYeHUO8vlwbz(um>W1N4UK)<2S zffi;mboFku`w~S+f;vM@qN^}Gdx9OvW#q5(1B3*Q%~<0pXIa-lS0?vV_ZxR1&j`;U z&nHiOZ!T|XZ$)nzZyxV&UcV>WGsaWW^Tj>ao!foVRo!*jS;4v2krF%kkk8FWau-47 ztNRz=pD&70{`om|j8;w;jNgq#W_v3S@>f@g#biecx-s+-W&~Rdnd5bQU1T^GIdVHs zIIFnMyGpt@yS<)Ho_!w8lh51MJI=ezy9u8t?-XxWZz(VDJ&O~Y&T|sC@`bCt>xr|r z^ME6XqrdR){PX8A$Ui@$3|EV4hIUe)Vl*<-TOsR|J(p-t=A#Ju5vy$tgtGs)!JGp1D=dve(w~luVR(HmG+xx(K(|g>z$~)Lw!OMCNdpde} z&oXyb_f}VC*J3B_Z10E`;t8$!4crG%>tddNK2)z{BtT#29&3W#gvd@>8$SD=5RQg3mf4<@`9&mqS4}rQnSPBhdh5wl{;X zw6B)0j<2#Wzc0S;t#`Y(n>VHRw5PQv=pN!$Ts>U(oyDCC9G`?DLL`5JGtpb{jemYB zM)>C~zVpvFX%TuABRN#+FW|L26zaR#D3ZQSuVebL#W|9X<_8H`9d{gqoeA--$cy+p zi)X$^@znAz^*;8-^A-0s_4V-e!&h@(Nnb+WGw%v-eXr?R;>qPXfSugtO74nuK0?-c zq;LlO^RnQdZ;4_4d3!Cd&KmcP9p)sfom~<=yFv0I6-D=B%CSD~4!4l6FE|_r9Ic&} zbG|DbqP4W1*;r#e?;7trZyM}jAKwJuEZ=lrq_2aol+WY4<{j_N>%D=vCya>Uv#Yu5 zq%);+fa8?l6sq$RKpgO+EA2<|&wGZ#p>g3<;&HL7luW)Q&ruqxskJZK34OlN6Lp?+ z@E;F}{p3ul3tf_l%RXf{ay|JR!W&_Mqpb6lbEGSdd!hR`aCpL=y599(zc;(Dg>Q^+ zsc#cLD|{2N%7Q)}x3V2#pB0|$p6%`|?!_+B)ylcg!8&RPGx$p!#pQ=>of3om^GE7t zZ8+Y`*-Z`|AII%w#1OI}e53#+wyo?Ct}IUr(Lz7u+9s3>D%f%<~!*-Ubrub5=wSpocr??%{drDUW;S z_vZ6;_bu`r^j-Dc@m=?w@U8I;^HuV3zC+ka!aLiO)U(j-c1OVO=P|-Rf1@7Irt6)MIZurl z*r#?hv4R{@)%<%u_ouQP7rDc@7y z7hk~l7JIqfHwrsR__llNdOvs~Jes?&`?;&KYmHNNRB}ueF7qs3gzLi2iBbOfdUcRi zL(d3u#8dN_wZa}rv?fd89jVa2Gi%uXTxp&aP7A{vIh}W%5wJST-RV5bJzj4=?^SOK zUn}1N+{NdRg}&Cl6tJ!Xym7oyo(!HScU<=noYq{w*8DaPOhMa z(T$iKEXiGik8CC+0?lohGmR_SRnPs{-O_X0Q`)=ID|^fMM)-EX;`n_gRvGd=^zDar zt?Hw2T5EgXczSyR?#}MpuA+$P{+@qcGsgJmJ>^`Aq?}UcYF+j6Mrvr*Jh7tfC?b+< z1&T{z#?PE)=W;Fibg2I?bX0K$oU>e+@%AX}+2u*&9g9dOtFN1HrSBr{qT(~Ko6mgF zzB!0}68g?~yLc(@Oiu#OG`HpIfJh*bv!i2+@QKgJf8(F;iDCYEQ!S^?8!wD#b2+lD zt%%a7xti2{dM7iUZN#PJ-}4&~nI~`_bGC+$oaFX-rg%(GJ8!f%j<3FND!j*iSRDy= z=Q%9UV#EUJe0RMgyve*=ltL(g4p~x zuXDM%*6h?6=AVxPZ8^8$FkhJ`tSEZ~(VQ#-zR4H*G&7%V&*k9#{8pizBR=v0O&VK)^HJ6UQ^t&AI!#?m6>0 zCpc~iNra~SH~zW$7vP`gV4MBo{PRrUpU?OU@XwF`1^DM*{{sB;=)IV;i%XUu zy4drr(`MMnXf)O5Xuqp*)yB$NS(WNZd&N}Z?64<1E0j8PAlNj>2crX{1GNKL0^We_ z*Zp)LQJ`R;HTd>-18G5le-JDmIv6S(jt4o)uS`qEIT1|a~X{9*IF1eZPl21yJ zQUxiV^j17AZV=~+lf-f2WO0GGMZ6$>7L!UfrOBX8Cz3nK(QRA!aF%h_bk=p&a+Y)Eb|!QN9A`0i*36OAaYGm* z

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�f) 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≤3@iVf1Kv96sOq=SiplC@`yFRcn(OVOOsp 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%0D^fycj{bFWuj@Rm8?A?C470p3 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 IyF+wr;X7bX$7E$!5AHkU-0rcY8J3|S-GG$ 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

{positionMessage}

- + {showPositionInWaitingQueue && ( + +

{positionMessage}

+
+ )} {hasCustomMessage && ( diff --git a/bigbluebutton-html5/imports/ui/core/initial-values/meetingClientSettings.ts b/bigbluebutton-html5/imports/ui/core/initial-values/meetingClientSettings.ts index 21e425ed6155..e59a4a79ce3a 100644 --- a/bigbluebutton-html5/imports/ui/core/initial-values/meetingClientSettings.ts +++ b/bigbluebutton-html5/imports/ui/core/initial-values/meetingClientSettings.ts @@ -42,6 +42,7 @@ export const meetingClientSettingsInitialValues: MeetingClientSettings = { skipMeetingEnded: false, dynamicGuestPolicy: true, enableGuestLobbyMessage: true, + showGuestLobbyWaitingQueuePosition: true, guestPolicyExtraAllowOptions: false, alwaysShowWaitingRoomUI: true, enableLimitOfViewersInWebcam: false, diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml index 437769bf4462..0505a9fff9aa 100755 --- a/bigbluebutton-html5/private/config/settings.yml +++ b/bigbluebutton-html5/private/config/settings.yml @@ -62,6 +62,7 @@ public: skipMeetingEnded: false dynamicGuestPolicy: true enableGuestLobbyMessage: true + showGuestLobbyWaitingQueuePosition: true guestPolicyExtraAllowOptions: false alwaysShowWaitingRoomUI: true enableLimitOfViewersInWebcam: false From e41e30ba68596b767ed58b870daa7e9507301f56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Fri, 19 Jun 2026 09:24:52 -0300 Subject: [PATCH 013/121] update docs with showGuestLobbyWaitingQueuePosition --- bigbluebutton-html5/private/config/settings.yml | 2 ++ docs/docs/administration/customize.md | 13 +++++++++++++ docs/docs/new-features.md | 4 ++++ 3 files changed, 19 insertions(+) diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml index 0505a9fff9aa..11302f14bcc3 100755 --- a/bigbluebutton-html5/private/config/settings.yml +++ b/bigbluebutton-html5/private/config/settings.yml @@ -62,6 +62,8 @@ public: skipMeetingEnded: false dynamicGuestPolicy: true enableGuestLobbyMessage: true + # Show the guest user's position in the lobby waiting queue. + # Set to false when users should not see their place in line. showGuestLobbyWaitingQueuePosition: true guestPolicyExtraAllowOptions: false alwaysShowWaitingRoomUI: true diff --git a/docs/docs/administration/customize.md b/docs/docs/administration/customize.md index 7a4af7ad5a81..0878fda3252d 100644 --- a/docs/docs/administration/customize.md +++ b/docs/docs/administration/customize.md @@ -1490,6 +1490,19 @@ You can overwrite the default guest policy in `/etc/bigbluebutton/bbb-web.proper # defaultGuestPolicy=ALWAYS_ACCEPT ``` + +#### Configure guest lobby queue position + +When the guest policy makes users wait for moderator approval, the HTML5 client shows each guest their position in the waiting queue by default. To hide this position, set `public.app.showGuestLobbyWaitingQueuePosition` to `false` in `/etc/bigbluebutton/bbb-html5.yml`. + +```yaml +public: + app: + showGuestLobbyWaitingQueuePosition: false +``` + +Restart BigBlueButton with `sudo bbb-conf --restart` for the change to take effect. + #### Show a custom logo on the client Ensure that the parameter `displayBrandingArea` is set to `true` in bbb-html5's configuration, restart BigBlueButton server with `sudo bbb-conf --restart` and pass `logo=` in Custom parameters when creating the meeting. diff --git a/docs/docs/new-features.md b/docs/docs/new-features.md index 712ccc008c1b..2eaaba93b02d 100644 --- a/docs/docs/new-features.md +++ b/docs/docs/new-features.md @@ -394,6 +394,10 @@ In BigBlueButton 2.6.17/2.7.5/3.0.0-alpha.5 we added a new configuration propert In BigBlueButton 3.0.0-alpha.5 we replaced the JOIN parameter `defaultLayout` with the JOIN parameter `userdata-bbb_default_layout`. If none provided the `meetingLayout` (passed on CREATE) will be used. If none passed, and if none passed there, the `defaultMeetingLayout` from bbb-web will be used. +#### Added new setting to control guest lobby waiting queue position + +- Client settings.yml: `showGuestLobbyWaitingQueuePosition`. Defaults to `true` + #### 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` From 504f1e96c123f115afef876433f4f5260e93b7a2 Mon Sep 17 00:00:00 2001 From: Tainan Felipe Date: Fri, 19 Jun 2026 13:02:09 -0300 Subject: [PATCH 014/121] test(shared-notes): dedupe WORD_JOINER constant in #25225 test Define WORD_JOINER once in util.ts and import it in blocknote.ts instead of declaring the same value in both files. The util.ts copy lived inside a page.evaluate (browser scope), so it is now passed into the evaluate as an argument. Addresses review feedback on PR #25285. --- .../sharednotes/blocknote/blocknote.ts | 2 +- .../playwright/sharednotes/blocknote/util.ts | 40 +++++++++++-------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/bigbluebutton-tests/playwright/sharednotes/blocknote/blocknote.ts b/bigbluebutton-tests/playwright/sharednotes/blocknote/blocknote.ts index 7c3f5ce3b48b..5105b550a83f 100644 --- a/bigbluebutton-tests/playwright/sharednotes/blocknote/blocknote.ts +++ b/bigbluebutton-tests/playwright/sharednotes/blocknote/blocknote.ts @@ -8,11 +8,11 @@ import { getBlockNoteLinkLocator, readLinkAndCursorState, startBlockNoteSharedNotes, + WORD_JOINER, } from './util'; // URL mirrors the link shown in the original #25225 report (it referenced issue 25175). const LINK_URL = 'https://github.com/bigbluebutton/bigbluebutton/issues/25175'; -const WORD_JOINER = '⁠'; export class BlockNoteSharedNotes extends MultiUsers { // Reproduces issue #25225: when a remote user's caret sits inside a link, that diff --git a/bigbluebutton-tests/playwright/sharednotes/blocknote/util.ts b/bigbluebutton-tests/playwright/sharednotes/blocknote/util.ts index 2e35cd414546..a97267668ec4 100644 --- a/bigbluebutton-tests/playwright/sharednotes/blocknote/util.ts +++ b/bigbluebutton-tests/playwright/sharednotes/blocknote/util.ts @@ -8,6 +8,10 @@ import { Page } from '../../core/page'; // is the ProseMirror root `.bn-editor` inside the shared-notes panel. export const BLOCKNOTE_EDITOR = '[data-test="notes"] .bn-editor'; +// U+2060 word-joiner: y-prosemirror wraps the remote collaboration-cursor name +// in these invisible separators, which is what #25225 leaked into the link. +export const WORD_JOINER = '⁠'; + export async function startBlockNoteSharedNotes(testPage: Page): Promise { await testPage.waitAndClick(e.sharedNotes); await testPage.waitForSelector(e.hideNotesLabel, ELEMENT_WAIT_LONGER_TIME); @@ -29,21 +33,23 @@ export function getBlockNoteLinkLocator(testPage: Page): Locator { * cursor owner's editor and stop their awareness cursor from broadcasting). */ export function readLinkAndCursorState(testPage: Page) { - return testPage.page.evaluate((sel: string) => { - const WORD_JOINER = '⁠'; - const anchor = document.querySelector(`${sel} a`) as HTMLAnchorElement | null; - // One `__base` element per remote cursor — count bases only for an accurate count. - const cursorBases = document.querySelectorAll(`${sel} .bn-collaboration-cursor__base`); - return { - linkText: anchor ? (anchor.textContent ?? '') : '', - linkHref: anchor ? (anchor.getAttribute('href') ?? '') : '', - linkTextHasWordJoiner: anchor ? (anchor.textContent ?? '').includes(WORD_JOINER) : false, - cursorWidgetInsideLink: anchor - ? !!anchor.querySelector( - '.bn-collaboration-cursor__base, .bn-collaboration-cursor__caret, .bn-collaboration-cursor__label', - ) - : false, - remoteCursorCount: cursorBases.length, - }; - }, BLOCKNOTE_EDITOR); + return testPage.page.evaluate( + ({ sel, wordJoiner }: { sel: string; wordJoiner: string }) => { + const anchor = document.querySelector(`${sel} a`) as HTMLAnchorElement | null; + // One `__base` element per remote cursor — count bases only for an accurate count. + const cursorBases = document.querySelectorAll(`${sel} .bn-collaboration-cursor__base`); + return { + linkText: anchor ? (anchor.textContent ?? '') : '', + linkHref: anchor ? (anchor.getAttribute('href') ?? '') : '', + linkTextHasWordJoiner: anchor ? (anchor.textContent ?? '').includes(wordJoiner) : false, + cursorWidgetInsideLink: anchor + ? !!anchor.querySelector( + '.bn-collaboration-cursor__base, .bn-collaboration-cursor__caret, .bn-collaboration-cursor__label', + ) + : false, + remoteCursorCount: cursorBases.length, + }; + }, + { sel: BLOCKNOTE_EDITOR, wordJoiner: WORD_JOINER }, + ); } From c7d5daab76c4fbc29bac6bfc53a42fe67d0d3981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Fri, 19 Jun 2026 13:43:29 -0300 Subject: [PATCH 015/121] chore: BigBlueButton release version to 3.0.31 --- bigbluebutton-config/bigbluebutton-release | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigbluebutton-config/bigbluebutton-release b/bigbluebutton-config/bigbluebutton-release index b1586696c090..688f1f3d594e 100644 --- a/bigbluebutton-config/bigbluebutton-release +++ b/bigbluebutton-config/bigbluebutton-release @@ -1 +1 @@ -BIGBLUEBUTTON_RELEASE=3.0.30 +BIGBLUEBUTTON_RELEASE=3.0.31 From 62b0d7bf552d67b5ec8197f42e690e8740299300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Fri, 19 Jun 2026 13:44:34 -0300 Subject: [PATCH 016/121] chore: BigBlueButton release version to 3.0.31 --- bigbluebutton-config/bigbluebutton-release | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigbluebutton-config/bigbluebutton-release b/bigbluebutton-config/bigbluebutton-release index b1586696c090..688f1f3d594e 100644 --- a/bigbluebutton-config/bigbluebutton-release +++ b/bigbluebutton-config/bigbluebutton-release @@ -1 +1 @@ -BIGBLUEBUTTON_RELEASE=3.0.30 +BIGBLUEBUTTON_RELEASE=3.0.31 From 8a99394b9c406b44d562c635fbeb6e7067729328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Fri, 19 Jun 2026 16:30:28 -0300 Subject: [PATCH 017/121] include release 3.0.31 --- docs/docs/new-features.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/docs/new-features.md b/docs/docs/new-features.md index 2eaaba93b02d..b3cea238a304 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.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) - [3.0.28](https://github.com/bigbluebutton/bigbluebutton/releases/tag/v3.0.28) From fd9cb92e2e3042a384f9bfd10df46b63dbe95bac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 21 Jun 2026 14:51:41 +0000 Subject: [PATCH 018/121] build(deps): bump form-data in /bigbluebutton-tests/playwright Bumps [form-data](https://github.com/form-data/form-data) from 4.0.5 to 4.0.6. - [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md) - [Commits](https://github.com/form-data/form-data/compare/v4.0.5...v4.0.6) --- updated-dependencies: - dependency-name: form-data dependency-version: 4.0.6 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- bigbluebutton-tests/playwright/package-lock.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bigbluebutton-tests/playwright/package-lock.json b/bigbluebutton-tests/playwright/package-lock.json index b8e8ee0a23c7..06e2d9f5f9d7 100644 --- a/bigbluebutton-tests/playwright/package-lock.json +++ b/bigbluebutton-tests/playwright/package-lock.json @@ -2351,16 +2351,16 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -2702,9 +2702,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" From ffc71aef8827ddc05cb4ae20dc86bdc9c25b5edf Mon Sep 17 00:00:00 2001 From: Claudio Promptoso Date: Mon, 22 Jun 2026 20:57:03 +0000 Subject: [PATCH 019/121] Set MALLOC_ARENA_MAX=2 for bbb-web and bbb-apps-akka Ship MALLOC_ARENA_MAX=2 as a systemd drop-in via bbb-config for the two JVM services (bbb-web, bbb-apps-akka). glibc's default arena pool (8 x vCPU) causes 500-600MB+ of unnecessary off-heap RSS on bbb-web; clamping to 2 arenas reduces RSS p95 by 37% with zero regressions measured over 36h with three-host A/B controls. Non-JVM services (graphql-server, graphql-middleware, html5, pads, livekit) use their own allocators and are not affected. Operators can override with a higher-priority drop-in in /etc/systemd/system/.service.d/. Co-Authored-By: Tiago Jacobs --- build/packages-template/bbb-config/build.sh | 6 ++++++ build/packages-template/bbb-config/malloc-arena.conf | 2 ++ 2 files changed, 8 insertions(+) create mode 100644 build/packages-template/bbb-config/malloc-arena.conf diff --git a/build/packages-template/bbb-config/build.sh b/build/packages-template/bbb-config/build.sh index 5563c026a49c..bf1664c9a888 100755 --- a/build/packages-template/bbb-config/build.sh +++ b/build/packages-template/bbb-config/build.sh @@ -59,6 +59,12 @@ for unit in freeswitch nginx redis-server postgresql; do cp bigbluebutton.conf "staging/usr/lib/systemd/system/${unit}.service.d/" done +# MALLOC_ARENA_MAX=2 for JVM services - reduces glibc malloc arena overhead +for svc in bbb-web bbb-apps-akka; do + mkdir -p "staging/usr/lib/systemd/system/${svc}.service.d" + cp malloc-arena.conf "staging/usr/lib/systemd/system/${svc}.service.d/" +done + . ./opts-$DISTRO.sh # diff --git a/build/packages-template/bbb-config/malloc-arena.conf b/build/packages-template/bbb-config/malloc-arena.conf new file mode 100644 index 000000000000..cc9392a96fc7 --- /dev/null +++ b/build/packages-template/bbb-config/malloc-arena.conf @@ -0,0 +1,2 @@ +[Service] +Environment="MALLOC_ARENA_MAX=2" From 87e90195f52a8720851fc6a7b7306ce4ed2d4aae Mon Sep 17 00:00:00 2001 From: Claudio Promptoso Date: Mon, 22 Jun 2026 22:24:39 +0000 Subject: [PATCH 020/121] fix(bbb-graphql-server): lower connection_lifetime to limit Postgres memory growth The BigBlueButton source in bbb-graphql-server metadata keeps each Postgres connection alive for 1 hour (connection_lifetime: 3600). While a connection stays open, Postgres holds onto the memory it used and never returns it to the system, so a 1-hour lifetime with up to 100 connections lets several GB pile up across a workday and pushes hosts into swap. Lowering it to 300 (the value validated in the investigation) recycles connections while still under load, so per-connection memory stays bounded. The cost is one reconnect per connection every 5 minutes, negligible next to the graphql server's constant query polling. Co-Authored-By: Tiago Jacobs --- bbb-graphql-server/metadata/databases/databases.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bbb-graphql-server/metadata/databases/databases.yaml b/bbb-graphql-server/metadata/databases/databases.yaml index 68c9a1563eec..ead496340332 100644 --- a/bbb-graphql-server/metadata/databases/databases.yaml +++ b/bbb-graphql-server/metadata/databases/databases.yaml @@ -19,7 +19,7 @@ database_url: postgres://bbb_hasura:bbb_hasura@127.0.0.1:5432/bbb_graphql isolation_level: read-committed pool_settings: - connection_lifetime: 3600 + connection_lifetime: 300 max_connections: 100 use_prepared_statements: true tables: "!include BigBlueButton/tables/tables.yaml" From cc5eb3b092e2785e12c33d77b03012dd88e0c5aa Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 06:58:47 +0000 Subject: [PATCH 021/121] Translate en.json in fr 100% translated source file: 'en.json' on 'fr'. --- bigbluebutton-html5/public/locales/fr.json | 229 ++++++++++++--------- 1 file changed, 132 insertions(+), 97 deletions(-) diff --git a/bigbluebutton-html5/public/locales/fr.json b/bigbluebutton-html5/public/locales/fr.json index 5aa6c17e27be..24a91c1f2001 100644 --- a/bigbluebutton-html5/public/locales/fr.json +++ b/bigbluebutton-html5/public/locales/fr.json @@ -9,14 +9,14 @@ "app.chat.locked": "La discussion est verrouillée, les messages ne peuvent pas être envoyés", "app.chat.inputLabel": "Saisie des messages pour la discussion {chatName}", "app.chat.emojiButtonLabel": "Sélecteur d'émoticônes", - "app.chat.loadMoreButtonLabel": "En savoir plus", + "app.chat.loadMoreButtonLabel": "D'avantage d'informations", "app.chat.inputPlaceholder": "Message {chatName}", "app.chat.titlePublic": "Discussion publique", "app.chat.titlePrivate": "Discussion privée avec {participantName}", "app.chat.partnerDisconnected": "{participantName} a quitté la session", "app.chat.closeChatLabel": "Fermer {chatName}", - "app.chat.hideChatLabel": "Cacher {chatName}", - "app.chat.moreMessages": "Plus de messages ci-dessous", + "app.chat.hideChatLabel": "Occulter {chatName}", + "app.chat.moreMessages": "Davantage de messages ci-dessous", "app.chat.dropdown.options": "Options de discussion", "app.chat.dropdown.clear": "Effacer", "app.chat.dropdown.copy": "Copier", @@ -24,11 +24,11 @@ "app.chat.label": "Discussion", "app.chat.offline": "Déconnecté", "app.chat.pollResult": "Résultats du sondage", - "app.chat.quizResult": "Résultats du sondage", - "app.chat.breakoutDurationUpdated": "Le temps de pause est maintenant de {timeInMinutes} minutes.", - "app.chat.breakoutDurationUpdatedModerator": "Le temps de pause alloué aux salles de discussion est désormais de {timeInMinutes} minutes, et une notification a été envoyée.", + "app.chat.quizResult": "Résultats du Quiz", + "app.chat.breakoutDurationUpdated": "Le temps de travail en groupe est maintenant de {timeInMinutes} minutes.", + "app.chat.breakoutDurationUpdatedModerator": "Le temps alloué aux salles de travail en groupe est désormais de {timeInMinutes} minutes, une notification a été envoyée.", "app.chat.emptyLogLabel": "Journal de discussion vide", - "app.chat.away": "Est parti", + "app.chat.away": "Est occupé", "app.chat.notAway": "est disponible", "app.chat.isPresenter": "{presenterName} est désormais le présentateur", "app.chat.isPresenterSetBy": "{presenterName} est désormais le présentateur, désigné par {assignedByName}", @@ -88,19 +88,19 @@ "app.emojiPicker.categories.flags": "Drapeaux", "app.emojiPicker.categories.recent": "Utilisés fréquemment", "app.emojiPicker.categories.search": "Rechercher des résultats", - "app.emojiPicker.skintones.1": "Teinte par défaut", - "app.emojiPicker.skintones.2": "Teinte claire", - "app.emojiPicker.skintones.3": "Ton mat-clair", - "app.emojiPicker.skintones.4": "Ton mat", - "app.emojiPicker.skintones.5": "Ton mat-foncé", - "app.emojiPicker.skintones.6": "Ton foncé", + "app.emojiPicker.skintones.1": "Teinte de peau par défaut", + "app.emojiPicker.skintones.2": "Teinte de peau claire", + "app.emojiPicker.skintones.3": "Teinte de peau mat-clair", + "app.emojiPicker.skintones.4": "Teinte de peau mate", + "app.emojiPicker.skintones.5": "Teinte de peau mate-foncée", + "app.emojiPicker.skintones.6": "Teinte de peau foncée", "app.errorBoundary.attemptingToRecover": "Une erreur s'est produite. tentative de récupération...", "app.errorBoundary.unableToRecover": "Récupération impossible après plusieurs tentatives.", "app.errorBoundary.reloadPage": "Recharger la page", "app.timer.title": "Temps", "app.timer.stopwatch.title": "Chronomètre", "app.timer.timer.title": "Minuterie", - "app.timer.hideTimerLabel": "Cacher le temps", + "app.timer.hideTimerLabel": "Masquer le temps", "app.timer.button.stopwatch": "Chronomètre", "app.timer.button.timer": "Minuterie", "app.timer.button.start": "Démarrer", @@ -109,11 +109,11 @@ "app.timer.hours": "heures", "app.timer.minutes": "minutes", "app.timer.seconds": "secondes", - "app.timer.songs": "Chansons", - "app.timer.noTrack": "Pas de chansons", - "app.timer.track1": "Détente", + "app.timer.songs": "Musique", + "app.timer.noTrack": "Pas de musique", + "app.timer.track1": "Apaisante", "app.timer.track2": "Calme", - "app.timer.track3": "Heureux", + "app.timer.track3": "Joyeuse", "app.confirmation.skipConfirm": "Ne plus demander", "app.confirmation.virtualBackground.title": "Préparer un nouvel arrière-plan virtuel", "app.confirmation.virtualBackground.description": "{fileName} sera ajouté comme arrière-plan virtuel. Continuer ?", @@ -126,11 +126,14 @@ "app.notes.label": "Notes", "app.notes.hide": "Masquer les notes", "app.notes.locked": "Verrouillé", - "app.notes.disabled": "Épinglé dans l'espace média", + "app.notes.disabled": "Épinglée dans l'espace média", "app.notes.notesDropdown.covertAndUpload": "Retranscrire les notes en présentation à l'écran", + "app.notes.notesDropdown.exportAsPDF": "Exporter les notes au format PDF", "app.notes.notesDropdown.pinNotes": "Épingler les notes sur le tableau blanc", "app.notes.notesDropdown.unpinNotes": "Désépingler les notes", "app.notes.notesDropdown.notesOptions": "Options des notes", + "app.notes.blocknote.payloadSizeError": "Le contenu du presse-papiers est trop volumineux ({sizeKB} KB). La taille maximale autorisée pour le collage est de {maxKB} KB.", + "app.notes.blocknote.maxCharCountError": "Limite de saisie atteinte. Vous ne pouvez pas saisir plus de {maxCharCount} caractères.", "app.pads.hint": "Appuyez sur Echap pour sélectionner la barre d'outils de l'éditeur", "app.user.activityCheck": "Vérification de l'activité de l'utilisateur", "app.user.activityCheck.label": "Vérifier si l'utilisateur est toujours connecté ({responseDelay})", @@ -146,8 +149,8 @@ "app.userList.presenter": "Présentateur", "app.userList.you": "Vous", "app.userList.locked": "Verrouillé", - "app.userList.byModerator": "(par modérateur)", - "app.userList.label": "Liste d'utilisateurs", + "app.userList.byModerator": "(par un modérateur)", + "app.userList.label": "Liste des utilisateurs", "app.userList.toggleCompactView.label": "Basculer vers/depuis le mode d'affichage compact", "app.userList.moderator": "Modérateur", "app.userList.mobile": "Mobile", @@ -156,12 +159,12 @@ "app.userList.menuTitleContext": "Options disponibles", "app.userList.chatListItem.unreadSingular": "Un nouveau message", "app.userList.chatListItem.unreadPlural": "{unreadCount} nouveaux messages", - "app.userList.menu.away": "Se définir comme parti", + "app.userList.menu.away": "Se définir comme occupé", "app.userList.menu.notAway": "Se définir comme actif", "app.userList.menu.chat.label": "Démarrer une discussion privée", "app.userList.menu.clearStatus.label": "Effacer le statut", "app.userList.menu.removeUser.label": "Retirer l'utilisateur", - "app.userList.menu.removeConfirmation.label": "Supprimer l'utilisateur ({userName})", + "app.userList.menu.removeConfirmation.label": "Révoquer l'utilisateur ({userName})", "app.userlist.menu.removeConfirmation.desc": "Empêcher cet utilisateur de rejoindre la réunion à nouveau.", "app.userList.menu.muteUserAudio.label": "Rendre l'utilisateur silencieux", "app.userList.menu.unmuteUserAudio.label": "Permettre l'activation du microphone", @@ -179,19 +182,19 @@ "app.userList.menu.lockPublicChat.label": "Verrouiller la discussion publique", "app.userList.menu.unlockPublicChat.label": "Déverrouiller la discussion publique", "app.userList.userOptions.manageUsersLabel": "Gérer les utilisateurs", - "app.userList.userOptions.usersJoinMutedEnableLabel": "Activer \"les participants rejoignent la conférence muets\"", - "app.userList.userOptions.usersJoinMutedEnableDesc": "Les participants qui rejoignent la réunion sont muets par défaut", - "app.userList.userOptions.usersJoinMutedDisableLabel": "Désactiver \"les participants rejoignent la conférence muets\"", - "app.userList.userOptions.usersJoinMutedDisableDesc": "Les participants qui rejoignent la réunion ne sont pas muets par défaut", + "app.userList.userOptions.usersJoinMutedEnableLabel": "Activer \"les participants joignent la réunion en mode silencieux\"", + "app.userList.userOptions.usersJoinMutedEnableDesc": "Par défaut les participants joignent la réunion en mode silencieux", + "app.userList.userOptions.usersJoinMutedDisableLabel": "Désactiver \"les participants joignent la réunion en mode silencieux\"", + "app.userList.userOptions.usersJoinMutedDisableDesc": "Par défaut les participants joignent la réunion avec le micro ouvert", "app.userList.userOptions.clearAllReactionsLabel": "Effacer toutes les réactions", - "app.userList.userOptions.clearAllReactionsDesc": "Supprime tous les émoticônes de réaction des utilisateurs", + "app.userList.userOptions.clearAllReactionsDesc": "Effacer tous les émoticônes de réaction des utilisateurs", "app.userList.userOptions.clearedReactions": "Toutes les réactions des utilisateurs ont été effacées", "app.userList.userOptions.muteAllExceptPresenterLabel": "Rendre silencieux tous les utilisateurs sauf le présentateur", "app.userList.userOptions.muteAllExceptPresenterDesc": "Rend silencieux tous les participants de la réunion sauf le présentateur", "app.userList.userOptions.lockViewersLabel": "Gestion des interactions", "app.userList.userOptions.lockViewersDesc": "Verrouiller certaines fonctionnalités des participants", - "app.userList.userOptions.guestPolicyLabel": "Gestion des accès", - "app.userList.userOptions.guestPolicyDesc": "Modifier la politique d'accès à la réunion pour les invités", + "app.userList.userOptions.guestPolicyLabel": "Règles d'accès", + "app.userList.userOptions.guestPolicyDesc": "Modifier la règle d'accès à la réunion pour les invités", "app.userList.userOptions.disableCam": "Les webcams des participants sont désactivées", "app.userList.userOptions.disableMic": "Les microphones des participants sont désactivés", "app.userList.userOptions.disablePrivChat": "La discussion privée est désactivée", @@ -204,7 +207,7 @@ "app.userList.userOptions.enablePrivChat": "La discussion privée est activée", "app.userList.userOptions.enablePubChat": "La discussion publique est activée", "app.userList.userOptions.enableNotes": "Les notes partagées sont maintenant activées", - "app.userList.userOptions.showUserList": "La liste des utilisateurs est maintenant affichée aux participants", + "app.userList.userOptions.showUserList": "La liste des utilisateurs s'affiche désormais pour les participants", "app.userList.userOptions.enableOnlyModeratorWebcam": "Vous pouvez désormais activer votre webcam, tout le monde vous verra", "app.userList.userOptions.savedNames.title": "Liste des utilisateurs connectés à la session {meetingName} à {dateAndTime}", "app.userList.userOptions.sortedFirstName.heading": "Tri par prénom :", @@ -218,14 +221,14 @@ "app.media.screenshare.endDueToDataSaving": "Le partage d'écran est arrêté pour économiser le flux de données.", "app.media.screenshare.unavailable": "Partage d'écran indisponible", "app.media.screenshare.notSupported": "Le partage d'écran n'est pas disponible pour ce navigateur.", - "app.media.screenshare.autoplayBlockedDesc": "Nous avons besoin de votre permission pour vous montrer l'écran du présentateur.", + "app.media.screenshare.autoplayBlockedDesc": "Nous avons besoin de votre permission pour que s'affiche l'écran du présentateur.", "app.media.screenshare.autoplayAllowLabel": "Voir l'écran partagé", "app.media.cameraAsContent.start": "La caméra actuelle a commencé à fonctionner", "app.media.cameraAsContent.end": "La caméra actuelle a cessé de fonctionner", - "app.media.cameraAsContent.endDueToDataSaving": "La caméra actuelle a cessé de fonctionner en raison de l'économie de données", - "app.media.cameraAsContent.autoplayBlockedDesc": "Nous avons besoin de votre autorisation pour vous montrer la caméra du présentateur.", + "app.media.cameraAsContent.endDueToDataSaving": "La caméra actuelle a cessé de fonctionner pour économiser des données", + "app.media.cameraAsContent.autoplayBlockedDesc": "Nous avons besoin de votre autorisation pour afficher la caméra du présentateur.", "app.media.cameraAsContent.autoplayAllowLabel": "Voir la caméra actuelle", - "app.media.mediaReconnecting": "Connection issue detected. Restoring your audio and video...", + "app.media.mediaReconnecting": "Problème de connexion détecté. Rétablissement du son et de l'image...", "app.screenshare.presenterLoadingLabel": "Votre partage d'écran est en cours de chargement", "app.screenshare.viewerLoadingLabel": "L'écran du présentateur est en cours de chargement", "app.screenshare.presenterSharingLabel": "Vous partagez désormais votre écran", @@ -238,12 +241,12 @@ "app.cameraAsContent.presenterLoadingLabel": "Votre caméra est en cours de chargement", "app.cameraAsContent.viewerLoadingLabel": "La caméra du présentateur est en cours de chargement", "app.cameraAsContent.presenterSharingLabel": "Vous présentez maintenant votre caméra", - "app.meeting.ended": "Cette réunion est terminée", + "app.meeting.ended": "Cette session est terminée", "app.meeting.endedFromAPI": "La session est terminée — fermée par le système", "app.meeting.endedWhenNoUserJoined": "La session a pris fin car aucun utilisateur ne s'est connecté", "app.meeting.endedWhenLastUserLeft": "La session a pris fin car le dernier utilisateur s'est déconnecté", "app.meeting.endedAfterExceedingDuration": "La session a pris fin en raison du dépassement de la durée maximale", - "app.meeting.breakoutEndedAfterExceedingDuration": "La pause est terminée car la durée maximale a été dépassée", + "app.meeting.breakoutEndedAfterExceedingDuration": "Le travail en groupe est terminé car sa durée maximale a été dépassée", "app.meeting.breakoutEndedByModerator": "La discussion est terminée — clôturée par le modérateur", "app.meeting.endedDueNoAuthed": "La session a pris fin car aucun utilisateur authentifié n'était présent dans la salle", "app.meeting.endedDueNoModerators": "La réunion est terminée faute de modérateur", @@ -256,7 +259,7 @@ "app.meeting.endedMessage": "Vous serez redirigé vers l'écran d'accueil", "app.meeting.alertMeetingEndsUnderMinutesSingular": "La réunion se terminera dans une minute.", "app.meeting.alertMeetingEndsUnderMinutesPlural": "La session va se terminer dans {timeInMinutes} minutes.", - "app.meeting.alertBreakoutEndsUnderMinutesPlural": "La pause va se terminer dans {timeInMinutes} minutes.", + "app.meeting.alertBreakoutEndsUnderMinutesPlural": "Le temps de travail en groupe se termine dans {timeInMinutes} minutes.", "app.meeting.alertBreakoutEndsUnderMinutesSingular": "Les sessions en groupes de travail se terminent dans une minute.", "app.presentation.hide": "Masquer la présentation", "app.presentation.newCurrentPresentationNotification": "La présentation actuelle est désormais : {presentationName}", @@ -268,6 +271,7 @@ "app.presentation.endSlideContent": "Fin du diaporama", "app.presentation.changedSlideContent": "Présentation modifiée en diapositive : {slideNumber}", "app.presentation.emptySlideContent": "La diapositive courante n'a pas de contenu", + "app.presentation.options.label": "Options du tableau blanc", "app.presentation.options.fullscreen": "Présentation en plein écran", "app.presentation.options.exitFullscreen": "Sortir du plein écran", "app.presentation.options.minimize": "Réduire", @@ -308,7 +312,7 @@ "app.presentation.modal.clearAnnotationsCancelLabel": "Annuler", "app.presentation.modal.clearAnnotationsConfirmLabel": "Effacer", "app.presentationUploder.title": "Présentation", - "app.presentationUploder.message": "En tant que présentateur vous avez la possibilité de téléverser tout fichier Office ou PDF. Nous recommandons l'utilisation des fichiers PDF pour de meilleurs résultats. Veuillez vérifier que votre document est sélectionné en utilisant le bouton en bas à gauche.", + "app.presentationUploder.message": "En tant que présentateur vous avez la possibilité de téléverser tout fichier Office ou PDF. Nous recommandons l'utilisation des fichiers PDF pour de meilleurs résultats. Veuillez vérifier que votre document est sélectionné en utilisant la case à cocher sur le côté gauche.", "app.presentationUploader.exportHint": "En sélectionnant \"Envoyer dans la discussion\", les utilisateurs disposeront d'un lien de téléchargement de la discussion publique.", "app.presentationUploader.exportToastHeader": "Envoi vers la discussion ({numberOfPresentations} élément)", "app.presentationUploader.exportToastHeaderPlural": "Envoi vers la discussion ({numberOfPresentations} éléments)", @@ -319,14 +323,14 @@ "app.presentationUploader.sent": "Envoyé", "app.presentationUploader.exportingTimeout": "L'export dure trop longtemps...", "app.presentationUploader.export": "Envoi dans la discussion", - "app.presentationUploader.exportCurrentStatePresentation": "Envoyer un lien de téléchargement pour la présentation dans son état actuel", + "app.presentationUploader.exportCurrentStatePresentation": "Envoyer un lien de téléchargement pour la présentation avec les annotations actuelles", "app.presentationUploader.enableOriginalPresentationDownload": "Activer le téléchargement de la présentation ({fileExtension})", "app.presentationUploader.disableOriginalPresentationDownload": "Désactiver le téléchargement de la présentation originale ({fileExtension})", - "app.presentationUploader.export.withWhiteboardAnnotations": "Dans l'état actuel", + "app.presentationUploader.export.withWhiteboardAnnotations": "Avec les annotations actuelles", "app.presentationUploader.dropdownExportOptions": "Exporter les options", "app.presentationUploader.dropdownExportOptionsUncomplete": "Désactivé jusqu'à la fin du téléversement", "app.presentationUploader.export.linkAvailable": "Lien pour télécharger {presentationName} disponible dans la discussion publique.", - "app.presentationUploader.currentPresentationLabel": "Présentation courante", + "app.presentationUploader.currentPresentationLabel": "Présentation actuelle", "app.presentationUploder.extraHint": "IMPORTANT : chaque fichier ne doit pas dépasser {maxFileSize} Mo et {maxFilePages} pages.", "app.presentationUploder.uploadLabel": "Téléverser", "app.presentationUploder.confirmLabel": "Confirmer", @@ -341,7 +345,7 @@ "app.presentationUploder.externalUploadLabel": "Parcourir les fichiers", "app.presentationUploder.fileToUpload": "Prêt à être chargé...", "app.presentationUploder.currentBadge": "En cours", - "app.presentationUploder.rejectedError": "Le(s) fichier(s) sélectionné(s) a (ont) été rejeté()s. Veuillez vérifier leur format.", + "app.presentationUploder.rejectedError": "Le(s) fichier(s) sélectionné(s) a (ont) été rejeté(s). Veuillez vérifier leur format.", "app.presentationUploder.connectionClosedError": "Interruption due à une connectivité faible. Veuillez essayer à nouveau.", "app.presentationUploder.upload.progress": "Téléchargement en cours ({progress}%)", "app.presentationUploder.conversion.204": "Pas de contenu a capturer", @@ -377,7 +381,7 @@ "app.presentationUploder.completed": "{numberOfCompleteUploads} téléchargements terminés", "app.presentationUploder.clearErrors": "Effacer les erreurs", "app.presentationUploder.clearErrorsDesc": "Efface les échecs de téléversement des présentations", - "app.presentationUploder.uploadViewTitle": "Charger la présentation", + "app.presentationUploder.uploadViewTitle": "Téléverser la présentation", "app.poll.questionAndoptions.label": "Intitulé de la question à afficher\nA. Réponse *\nB. Réponse (optionnelle)\nC. Réponse (optionnelle)\nD. Réponse (optionnelle)\nE. Réponse (optionnelle)", "app.poll.customInput.label": "Saisie personnalisée", "app.poll.customInputInstructions.label": "Saisie personnalisée est activée - Ecrivez la question et la (les) réponse(s) dans le format proposé ou glissez-déposez un fichier texte dans le même format. ", @@ -387,9 +391,9 @@ "app.poll.quickPollTitle": "Sondage rapide", "app.poll.hidePollDesc": "Masque le volet du menu du sondage", "app.poll.quickPollInstruction": "Sélectionnez une option ci-dessous pour démarrer votre sondage.", - "app.poll.activePollInstruction": "Laissez cette fenêtre ouverte afin de voir en direct les réponses à votre sondage. Lorsque vous êtes prêt, sélectionnez « Publier les résultats du sondage » pour publier les résultats et terminer le sondage.", - "app.poll.dragDropPollInstruction": "Pour renseigner les valeurs du sondage, faites glisser un fichier texte avec les valeurs sur le champ en surbrillance", - "app.poll.customPollTextArea": "Renseigner les valeurs du sondage", + "app.poll.activePollInstruction": "Laissez cette fenêtre ouverte afin de voir en direct les réponses à votre sondage. Lorsque vous êtes prêt, sélectionnez « Publier les résultats du sondage » pour publier les résultats et clore le sondage.", + "app.poll.dragDropPollInstruction": "Pour renseigner les options de réponse du sondage, faites glisser un fichier texte avec les options sur le champ en surbrillance", + "app.poll.customPollTextArea": "Renseigner les options de réponse du sondage", "app.poll.publishLabel": "Publier les résultats", "app.poll.cancelPollLabel": "Annuler", "app.poll.backLabel": "Débuter un sondage", @@ -410,8 +414,8 @@ "app.poll.secretPoll.label": "Sondage Anonyme", "app.poll.secretPoll.isSecretLabel": "Le sondage est anonyme - vous ne pourrez pas voir les réponses individuelles.", "app.poll.questionErr": "Vous devez soumettre une question.", - "app.poll.optionErr": "Renseignez une option de sondage", - "app.poll.minOptionsErr": "Au moins une option est requise", + "app.poll.optionErr": "Renseignez une option de réponse", + "app.poll.minOptionsErr": "Au moins une option de réponse est requise", "app.poll.startPollDesc": "Commencer le sondage", "app.poll.showRespDesc": "Affiche la configuration des réponses", "app.poll.addRespDesc": "Ajoute une option de réponse au sondage", @@ -441,7 +445,7 @@ "app.poll.liveResult.usersTitle": "Utilisateurs", "app.poll.liveResult.responsesTitle": "Réponse", "app.poll.liveResult.secretLabel": "Ceci est un sondage anonyme. Les réponses individuelles ne seront pas affichées.", - "app.poll.removePollOpt": "Option de sondage supprimée {option}", + "app.poll.removePollOpt": "Option de réponse supprimée {option}", "app.poll.emptyPollOpt": "Vide", "app.poll.info": "Mode sondage : recueillez les opinions et consultez la répartition des réponses. Aucune réponse correcte n'est requise.", "app.poll.quiz.info": "Mode quiz : sélectionnez la bonne réponse pour chaque question. Les résultats indiqueront qui a répondu correctement.", @@ -501,6 +505,7 @@ "app.navBar.optionsDropdown.endMeetingLabel": "Mettre fin à la réunion", "app.navBar.optionsDropdown.endMeetingForAllLabel": "Mettre fin à la réunion pour tous les utilisateurs", "app.navBar.optionsDropdown.presenceLabel": "Je suis", + "app.navBar.optionsDropdown.presenceLabelDesc": "Réglez votre statut sur {status}", "app.navBar.userListToggleBtnLabel": "Masquer ou afficher la liste des utilisateurs", "app.navBar.toggleUserList.ariaLabel": "Afficher les utilisateurs ou les messages", "app.navBar.toggleUserList.newMessages": "avec notification des nouveaux messages", @@ -516,7 +521,7 @@ "app.leaveConfirmation.confirmLabel": "Quitter", "app.leaveConfirmation.confirmDesc": "Vous déconnecte de la réunion", "app.endMeeting.title": "Fin {meetingTitle}", - "app.endMeeting.description": "Cette action mettra fin à la session de {numberOfUsers} utilisateurs actifs. Êtes-vous sûr de vouloir mettre fin à cette session ?", + "app.endMeeting.description": "Cette action mettra fin à la session de(s) {numberOfUsers} utilisateur(s) actif(s). Êtes-vous sûr de vouloir mettre fin à cette session ?", "app.endMeeting.noUserDescription": "Êtes-vous sûr de vouloir mettre fin à la séance ?", "app.endMeeting.contentWarning": "Les messages de discussion, les notes partagées, le contenu du tableau blanc et les documents partagés lors de cette séance ne seront plus directement accessibles", "app.endMeeting.yesLabel": "Mettre fin à la réunion pour tous les utilisateurs", @@ -534,13 +539,13 @@ "app.mobileAppModal.openApp": "Oui, ouvrir l'application maintenant", "app.mobileAppModal.obtainUrlMsg": "Récupération de l'adresse de la réunion", "app.mobileAppModal.obtainUrlErrorMsg": "Une erreur est survenue lors de la récupération de l'adresse de la réunion", - "app.mobileAppModal.openStore": "Non, télécharger l'application", + "app.mobileAppModal.openStore": "Non, télécharger depuis le magasin d'applications", "app.mobileAppModal.dismissLabel": "Annuler", "app.mobileAppModal.dismissDesc": "Fermer", "app.mobileAppModal.userConnectedWithSameId": "L'utilisateur {userName} vient de se connecter en utilisant le même identifiant que vous.", "app.actionsBar.changeStatusLabel": "Changer de statut", "app.actionsBar.muteLabel": "Rendre silencieux", - "app.actionsBar.unmuteLabel": "Permettre l'activation du microphone", + "app.actionsBar.unmuteLabel": "Activation du microphone", "app.actionsBar.unmuteAndSetActiveLabel": "Activez votre microphone et définissez-vous comme actif", "app.actionsBar.camOffLabel": "Caméra éteinte", "app.actionsBar.raiseLabel": "Lever la main", @@ -551,7 +556,7 @@ "app.actionsBar.actionsDropdown.minimizePresentationDesc": "Bouton utilisé pour réduire la fenêtre de présentation", "app.actionsBar.actionsDropdown.layoutModal": "Fenêtre de paramétrage de la mise en page", "app.actionsBar.actionsDropdown.shareCameraAsContent": "Partager la caméra en tant que contenu", - "app.actionsBar.actionsDropdown.unshareCameraAsContent": "Cesser de partager l'appareil photo en tant que contenu", + "app.actionsBar.actionsDropdown.unshareCameraAsContent": "Cesser de partager la caméra en tant que contenu", "app.actionsBar.actionsDropdown.breakoutRoomInvitationLabel": "Invitation pour une réunion en groupe de travail", "app.actionsBar.actionsDropdown.breakoutRoomInvitationDesc": "Inviter les participants à une réunion en groupe de travail", "app.screenshare.screenShareLabel": "Partage d'écran", @@ -717,7 +722,7 @@ "app.actionsBar.reactions.lowHand": "Baisser la main", "app.actionsBar.reactions.lowUserHand": "Baisser la main levée par l'utilisateur", "app.actionsBar.reactions.autoCloseReactionsBarLabel": "Fermeture automatique de la barre de réactions", - "app.actionsBar.reactions.away": "absent·e", + "app.actionsBar.reactions.away": "occupé", "app.actionsBar.reactions.available": "disponible", "app.actionsBar.captions.start": "Démarrer l'affichage des sous-titres SEM", "app.actionsBar.captions.stop": "Arrêter l'affichage des sous-titres SEM", @@ -835,7 +840,7 @@ "app.audio.listenOnly.backLabel": "Retour", "app.audio.listenOnly.closeLabel": "Fermer", "app.audio.permissionsOverlay.title": "Autoriser BigBlueButton à utiliser votre micro", - "app.audio.permissionsOverlay.hint": "Il est nécessaire que vous nous autorisiez à utiliser vos appareils multimédias pour que vous puissiez participer à la réunion", + "app.audio.permissionsOverlay.hint": "Il est nécessaire que vous nous autorisiez à utiliser vos périphériques multimédias pour que vous puissiez participer à la réunion :)", "app.audio.captions.button.start": "Initier un sous titrage SME", "app.audio.captions.button.stop": "Arrêter le sous titrage SME", "app.audio.captions.button.language": "Langue", @@ -869,7 +874,7 @@ "app.audio.captions.select.fi-FI": "finlandais", "app.audio.captions.select.fr-FR": "Français", "app.audio.captions.select.gl-ES": "galicien", - "app.audio.captions.select.de-DE": "Allemand", + "app.audio.captions.select.de-DE": "allemand", "app.audio.captions.select.el-GR": "grec", "app.audio.captions.select.gu-IN": "gujarati", "app.audio.captions.select.hi-IN": "hindi", @@ -891,7 +896,7 @@ "app.audio.captions.select.ml-IN": "malayalam", "app.audio.captions.select.mr-IN": "marathi", "app.audio.captions.select.mn-MN": "mongol", - "app.audio.captions.select.ne-NP": "népalais\n", + "app.audio.captions.select.ne-NP": "népalais", "app.audio.captions.select.no-NO": "norvégien", "app.audio.captions.select.fa-IR": "persan", "app.audio.captions.select.pl-PL": "polonais", @@ -971,7 +976,7 @@ "app.guest.windowTitle": "Salon d'attente", "app.guest.missingToken": "L'invité n'a pas de jeton de session", "app.guest.missingSession": "Invité sans session", - "app.guest.missingMeeting": "La séance n'existe pas.", + "app.guest.missingMeeting": "La session n'existe pas.", "app.guest.meetingEnded": "Cette réunion est terminée", "app.guest.guestWait": "Veuillez attendre qu'un modérateur approuve votre accès à la réunion.", "app.guest.guestDeny": "L'accès de l'invité à la réunion a été refusé.", @@ -993,7 +998,7 @@ "app.userList.guest.denyEveryone": "Rejeter toutes les demandes", "app.userList.guest.pendingUsers": "{usersCount} Utilisateurs en attente", "app.userList.guest.noPendingUsers": "Aucun utilisateur en attente...", - "app.userList.guest.pendingGuestUsers": "{usersCount} Utilisateurs invités en attente", + "app.userList.guest.pendingGuestUsers": "{usersCount} utilisateurs invités en attente", "app.userList.guest.pendingGuestAlert": "A rejoint la séance et attend votre approbation.", "app.userList.guest.rememberChoice": "Se rappeler du choix", "app.userList.guest.emptyMessage": "Il n'y a actuellement aucun message ", @@ -1003,26 +1008,27 @@ "app.userList.guest.acceptLabel": "Accepter", "app.userList.guest.denyLabel": "Refuser", "app.userList.guest.feedbackMessage": "Mesure prise :", + "app.userList.customLogoAlt": "Logo personnalisé", "app.user-info.title": "Recherche dans l'annuaire", "app.toast.breakoutRoomEnded": "La session en groupe de travail est terminée. Veuillez rejoindre l'audio.", - "app.toast.breakoutContentUnchangedNotConverted": "One or more breakout rooms had no whiteboard/shared notes updates, so they weren't converted to slides.", + "app.toast.breakoutContentUnchangedNotConverted": "Une ou plusieurs salles de travail en groupe n'ont pas de notes/annotations supplémentaires, elles n'ont pas été converties en diapositives.", "app.toast.chat.public": "Nouveau message de discussion publique", "app.toast.chat.private": "Nouveau message de discussion privée", "app.toast.chat.system": "Système", "app.toast.chat.poll": "Résultats du sondage", "app.toast.chat.pollClick": "Les résultats du sondage ont été publiés. Cliquez ici pour les voir.", - "app.toast.usersJoinMutedOn.label": "\"Les participants rejoignent la réunion silencieux\" a été activé.", - "app.toast.usersJoinMutedOff.label": "\"Les participants rejoignent la réunion silencieux\" a été désactivé.", + "app.toast.usersJoinMutedOn.label": "\"Les participants rejoignent la réunion en mode silencieux\" a été activé.", + "app.toast.usersJoinMutedOff.label": "\"Les participants rejoignent la réunion en mode silencieux\" a été désactivé.", "app.toast.muteAllViewers.label": "Les spectateurs sont en mode silencieux", "app.toast.wakeLock.acquireSuccess": "Verrouillage du réveil actif ! Vous pouvez le désactiver dans le menu des paramètres.", - "app.toast.wakeLock.acquireFailed": "Erreur d'acquisition du verrouillage de réveil.", - "app.toast.wakeLock.notSupported": "Votre navigateur ne prend pas en charge les verrous de réveil", + "app.toast.wakeLock.acquireFailed": "Erreur d'acquisition du verrouillage de mise en veille.", + "app.toast.wakeLock.notSupported": "Votre navigateur ne prend pas en charge le verrouillage de mise en veille.", "app.toast.wakeLock.disclaimer": "{error}. Vous serez déconnecté de l'appel une fois votre écran éteint.", "app.toast.setEmoji.raiseHand": "Vous avez levé la main", "app.toast.setEmoji.lowerHand": "Votre main a été abaissée", "app.toast.raisedHandNext.label": "Votre main levée est la suivante dans la file", - "app.toast.setEmoji.away": "Vous avez défini votre statut comme étant parti", - "app.toast.setEmoji.notAway": "Votre statut « absent » a été supprimé", + "app.toast.setEmoji.away": "Vous avez défini votre statut comme étant occupé", + "app.toast.setEmoji.notAway": "Votre statut « occupé » a été supprimé", "app.toast.promotedLabel": "Vous avez été promu comme modérateur", "app.toast.demotedLabel": "Vous avez été rétrogradé comme participant", "app.notification.recordingStart": "Cette réunion est maintenant enregistrée", @@ -1079,8 +1085,8 @@ "app.shortcut-help.zoomOut": "Zoom arrière", "app.shortcut-help.zoomFit": "Réinitialiser le zoom", "app.shortcut-help.zoomSelect": "Zoomer sur la sélection", - "app.shortcut-help.flipH": "Renversement horizontal", - "app.shortcut-help.flipV": "Renversement vertical", + "app.shortcut-help.flipH": "Inverser horizontalement", + "app.shortcut-help.flipV": "Inverser verticalement", "app.shortcut-help.lock": "Verrouiller/déverrouiller", "app.shortcut-help.moveToFront": "Déplacer au premier plan", "app.shortcut-help.moveToBack": "Déplacer à l'arrière-plan", @@ -1113,13 +1119,13 @@ "app.lock-viewers.locked": "Verrouillé", "app.lock-viewers.hideViewersCursor": "Voir les pointeurs des autres participants", "app.lock-viewers.hideAnnotationsLabel": "Voir les annotations des autres spectateurs", - "app.guest-policy.ariaTitle": "Fenêtre des paramètres de gestion des accès", + "app.guest-policy.ariaTitle": "Fenêtre des paramètres des règles d'accès", "app.guest-policy.title": "Gestion des accès", - "app.guest-policy.description": "Modifier la politique d'accès à la réunion pour les invités", + "app.guest-policy.description": "Modifier la règle d'accès à la réunion pour les invités", "app.guest-policy.button.askModerator": "Demander au modérateur", "app.guest-policy.button.alwaysAccept": "Toujours accepter", "app.guest-policy.button.alwaysDeny": "Toujours refuser", - "app.guest-policy.policyBtnDesc": "Définit la politique d'accès à la réunion pour les invités", + "app.guest-policy.policyBtnDesc": "Définit la règle d'accès à la réunion pour les invités", "app.guest-policy.feedbackMessage": "La politique à l'égard des invités est maintenant :", "app.connection-status.ariaTitle": "État de la connexion visible", "app.connection-status.title": "État de la connexion", @@ -1173,7 +1179,7 @@ "app.recording.notify.continue": "Poursuivre", "app.recording.notify.leave": "Quitter la réunion", "app.recording.notify.continueLabel": "Accepter l'enregistrement et poursuivre", - "app.recording.notify.leaveLabel": "Ne pas accepter l'enregistrement et quitter la réunion.", + "app.recording.notify.leaveLabel": "Refuser l'enregistrement et quitter la réunion.", "app.videoPreview.cameraLabel": "Caméra", "app.videoPreview.profileLabel": "Qualité", "app.videoPreview.quality.low": "Bas", @@ -1191,7 +1197,7 @@ "app.videoPreview.webcamPreviewLabel": "Aperçu de la webcam", "app.videoPreview.webcamSettingsTitle": "Paramètres de la webcam", "app.videoPreview.webcamEffectsTitle": "Réglages de l'image pour la webcam", - "app.videoPreview.cameraAsContentSettingsTitle": "Caméra présente", + "app.videoPreview.cameraAsContentSettingsTitle": "Caméra actuelle", "app.videoPreview.webcamVirtualBackgroundLabel": "Paramètres d'arrière-plan virtuel", "app.videoPreview.webcamVirtualBackgroundDisabledLabel": "Le périphérique ne prend pas en charge les arrière-plans virtuels", "app.videoPreview.webcamNotFoundLabel": "Webcam introuvable", @@ -1210,24 +1216,24 @@ "app.video.iceConnectionStateError": "Échec de connexion (erreur ICE 1107)", "app.video.permissionError": "Erreur lors du partage de la webcam. Veuillez vérifier les permissions", "app.video.sharingError": "Erreur lors du partage de la Webcam", - "app.video.abortError": "Un problème empêchant l'utilisation de cet appareil est survenu", - "app.video.overconstrainedError": "Aucun appareil ne répond au critère demandé", - "app.video.securityError": "Le support média est désactivé sur ce document", - "app.video.typeError": "La liste des contraintes spécifiées est vide ou toutes les contraintes définies ont une valeur fausse", + "app.video.abortError": "Un problème empêchant l'utilisation de votre caméra est survenu", + "app.video.overconstrainedError": "Votre caméra ne prend pas en charge cette définition", + "app.video.securityError": "Votre navigateur a désactivé la caméra. Essayez avec une autre navigateur.", + "app.video.typeError": "La définition de la caméra n'est pas valide. Contactez votre administrateur.", "app.video.notFoundError": "Webcam introuvable. Assurez-vous qu'elle est bien connectée", - "app.video.notAllowed": "Permission manquante pour partager la Webcam. Veuillez vérifier les permissions dans votre navigateur", - "app.video.notSupportedError": "La vidéo de la webcam peut être partagée uniquement avec des sources sûres ; assurez-vous que votre certificat SSL est valide", + "app.video.notAllowed": "Une autorisation d'accès à la caméra doit être donnée", + "app.video.notSupportedError": "Le navigateur n'est pas pris en charge. Essayez avec un autre navigateur ou un autre appareil.", "app.video.notReadableError": "Impossible d'obtenir la vidéo de la webcam. Assurez-vous qu'aucun autre programme n'utilise la webcam", "app.video.timeoutError": "Le navigateur n'a pas répondu à temps", "app.video.genericError": "Une erreur inconnue s'est produite avec l'appareil ({error})", "app.video.inactiveError": "Votre webcam s'est brusquement éteinte. Veuillez vérifier les autorisations données au navigateur.", "app.video.mediaTimedOutError": "Le flux de votre webcam a été interrompu. Essayer de le partager à nouveau", "app.video.mediaFlowTimeout1020": "Le média n'a pas pu atteindre le serveur (erreur 1020)", - "app.video.suggestWebcamLock": "Appliquer le paramètre de verrouillage aux webcams des participants ?", + "app.video.suggestWebcamLock": "Forcer le verrouillage des webcams des participants ?", "app.video.suggestWebcamLockReason": "(cela améliorera la stabilité de la session)", "app.video.enable": "Activer", "app.video.cancel": "Annuler", - "app.video.swapCam": "Échanger", + "app.video.swapCam": "Permuter", "app.video.swapCamDesc": "Permuter les Webcams", "app.video.videoLocked": "Partage webcam verrouillé", "app.video.videoButtonDesc": "Partager webcam", @@ -1243,7 +1249,7 @@ "app.video.virtualBackground.board": "Tableau", "app.video.virtualBackground.coffeeshop": "Café", "app.video.virtualBackground.background": "Arrière-plan", - "app.video.virtualBackground.backgroundWithIndex": "Contexte {backgroundIndex}", + "app.video.virtualBackground.backgroundWithIndex": "Arrière plan {backgroundIndex}", "app.video.virtualBackground.custom": "Téléverser depuis votre ordinateur", "app.video.virtualBackground.remove": "Supprimer l'image qui a été ajoutée", "app.video.virtualBackground.genericError": "Échec de l'application de l'effet de caméra. Réessayez. ", @@ -1254,7 +1260,7 @@ "app.video.virtualBackground.uploaded": "Chargé", "app.video.virtualBackground.uploading": "En cours de chargement...", "app.video.virtualBackground.button.customDesc": "Ajoute un nouveau arrière-plan virtuel", - "app.video.camCapReached": "Vous ne pouvez pas partager plus de caméras", + "app.video.camCapReached": "Vous ne pouvez pas partager davantage de caméras", "app.video.meetingCamCapReached": "La conférence a atteint la limite du nombre de caméras actives simultanément", "app.video.dropZoneLabel": "Déposer ici", "app.video.cameraAriaLabel": "Caméras", @@ -1311,11 +1317,11 @@ "app.whiteboard.toolbar.multiUserLimitHasBeenReached": "La session dépasse la limite de {numberOfUsers} utilisateurs pour démarrer le tableau blanc multi-utilisateurs. Vous pouvez toujours sélectionner individuellement les utilisateurs qui souhaitent participer.", "app.whiteboard.toolbar.multiUserLimitHasBeenReachedNotification": "Le nombre maximal d'utilisateurs pour le tableau blanc multi-utilisateurs a été atteint ({numberOfUsers}).", "app.whiteboard.available": "Le tableau blanc est désormais disponible !", - "app.whiteboard.unavailable": "L'accès au tableau blanc a été retiré.", + "app.whiteboard.unavailable": "L'accès au tableau blanc a été désactivé.", "app.whiteboard.toolbar.infiniteWhiteboardOn": "Activer le mode infini du tableau blanc", "app.whiteboard.toolbar.infiniteWhiteboardOff": "Désactiver le mode infini du tableau blanc", - "app.whiteboard.toolbar.palmRejectionOn": "Contacts de la paume de la main sur les écrans tactiles : Désactivés", - "app.whiteboard.toolbar.palmRejectionOff": "Contacts de la paume de la main sur écrans tactiles : Activé", + "app.whiteboard.toolbar.palmRejectionOn": "Protection contre l'interférence de la paume de la main activée", + "app.whiteboard.toolbar.palmRejectionOff": "Protection contre l'interférence de la paume de la main désactivée", "app.whiteboard.toolbar.fontSize": "Liste de tailles de police", "app.whiteboard.toolbarAriaLabel": "Outils de présentation", "app.feedback.title": "Vous avez quitté la réunion", @@ -1331,16 +1337,16 @@ "app.videoDock.webcamFocusDesc": "Centrer sur la webcam sélectionnée", "app.videoDock.webcamUnfocusLabel": "Arrêter le centrage", "app.videoDock.webcamUnfocusDesc": "Arrêter de centrer sur la webcam sélectionnée", - "app.videoDock.webcamDisableLabel": "Désactiver la vision de soi", - "app.videoDock.webcamDisableLabelAllCams": "Désactiver l'affichage de soi (toutes les caméras)", - "app.videoDock.webcamEnableLabel": "Activer la vision de soi", - "app.videoDock.webcamDisableDesc": "Vision de soi désactivée", + "app.videoDock.webcamDisableLabel": "Désactiver le retour de votre webcam", + "app.videoDock.webcamDisableLabelAllCams": "Désactiver les retours caméras (toutes les caméras)", + "app.videoDock.webcamEnableLabel": "Activer le retrour de votre caméra.", + "app.videoDock.webcamDisableDesc": "Retour caméra désactivée", "app.videoDock.webcamDisableWarning": "Votre webcam est toujours visible pour les autres ! Seule votre vue locale est désactivée.", "app.videoDock.webcamPinLabel": "Épingler", "app.videoDock.webcamPinDesc": "Épingler la caméra sélectionnée", "app.videoDock.webcamFullscreenLabel": "Webcam en plein écran", "app.videoDock.webcamExitFullscreenLabel": "Sortir la webcam du mode plein écran", - "app.videoDock.webcamSqueezedButtonLabel": "Options de la webcam", + "app.videoDock.webcamSqueezedButtonLabel": "Vue resserrée de la webcam", "app.videoDock.webcamUnpinLabel": "Désépingler", "app.videoDock.webcamUnpinLabelDisabled": "Seuls les modérateurs peuvent désépingler des utilisateurs", "app.videoDock.webcamUnpinDesc": "Désépingler la caméra sélectionnée", @@ -1350,7 +1356,7 @@ "app.videoDock.plugin.helpers.seeMoreItems": "Voir plus d'éléments", "app.createBreakoutRoom.title": "Salles pour les groupes de travail", "app.createBreakoutRoom.ariaTitle": "Masquer les salles pour les groupes de travail", - "app.createBreakoutRoom.breakoutRoomLabel": "Salles de réunion {roomNumber}", + "app.createBreakoutRoom.breakoutRoomLabel": "Salles de groupes de travail {roomNumber}", "app.createBreakoutRoom.askToJoin": "Demander à rejoindre une réunion", "app.createBreakoutRoom.generatingURL": "Générer l'URL", "app.createBreakoutRoom.generatingURLMessage": "Nous sommes en train de générer une URL pour rejoindre le groupe de travail sélectionné. Cela peut prendre quelques secondes...", @@ -1371,17 +1377,18 @@ "app.createBreakoutRoom.resetAssignmentsDesc": "Réinitialiser toutes les affectations des utilisateurs aux salles", "app.createBreakoutRoom.endAllBreakouts": "Clore tous les groupes de travail", "app.createBreakoutRoom.chatTitleMsgAllRooms": "toutes les salles", - "app.createBreakoutRoom.msgToBreakoutsSent": "Le message a été envoyé à {numberOfBreakouts} salles de discussion", + "app.createBreakoutRoom.msgToBreakoutsSent": "Le message a été envoyé à {numberOfBreakouts} groupes de travail", "app.createBreakoutRoom.doneLabel": "Terminé", "app.createBreakoutRoom.nextLabel": "Suivant", "app.createBreakoutRoom.minusRoomTime": "Diminuer la durée des groupes de travail à", "app.createBreakoutRoom.addRoomTime": "Augmenter la durée des groupes de travail à", "app.createBreakoutRoom.addParticipantLabel": "+ Ajouter un participant", - "app.createBreakoutRoom.freeJoin": "Autoriser les participants à choisir la salle de réunion privée qu'ils souhaitent rejoindre", + "app.createBreakoutRoom.freeJoin": "Autoriser les participants à choisir leur groupe de travail", "app.createBreakoutRoom.manageRoomsLabel": "Gérer les salles", "app.createBreakoutRoom.captureNotes": "Faire une capture des notes partagées lors de la fermeture des groupes de travail", "app.createBreakoutRoom.captureSlides": "Faire une capture du tableau blanc lors de la fermeture des groupes de travail", "app.createBreakoutRoom.sendInvitationToMods": "Envoyer l'invitation aux modérateurs désignés", + "app.createBreakoutRoom.inheritLockSettings": "Bloquer pour tous les paramètres dans leur état actuel", "app.createBreakoutRoom.leastOneWarnBreakout": "Vous devez placer au moins un participant dans chaque groupe de travail", "app.createBreakoutRoom.minimumDurationWarnBreakout": "La durée minimale d'une salle de discussion est de {timeInMinutes} minutes.", "app.createBreakoutRoom.timeCannotExceedMainRoom": "La durée de la salle de discussion ne peut pas dépasser le temps restant de la session.", @@ -1441,11 +1448,11 @@ "app.layout.modal.layoutBtnDesc": "Défini la mise en page en tant qu'option sélectionnée", "app.layout.modal.layoutToastLabelAuto": "Mise à jour automatique de la mise en page activée, les mises à jour de la mise en page sont maintenant appliquées à tous", "app.layout.modal.layoutToastLabelAutoOff": "Mise à jour automatique de la mise en page désactivée", - "app.layout.style.custom": "Personnalisé", + "app.layout.style.custom": "Personnalisée", "app.layout.style.smart": "Mise en page intelligente", "app.layout.style.presentationFocus": "Centrer sur la présentation", "app.layout.style.videoFocus": "Centrer sur la vidéo", - "app.layout.style.customPush": "Personnalisé (applique la mise en page à tous les utilisateurs)", + "app.layout.style.customPush": "Personnalisée (applique la mise en page à tous les utilisateurs)", "app.layout.style.smartPush": "Mise en page automatique (applique la mise en page à tous les utilisateurs)", "app.layout.style.presentationFocusPush": "Centre sur la présentation (applique la mise en page à tous les utilisateurs)", "app.layout.style.videoFocusPush": "Centrer sur la vidéo (applique la mise en page à tous les utilisateurs)", @@ -1578,6 +1585,34 @@ "app.learningDashboard.statusTimelineTable.setAt": "Régler à", "app.learningDashboard.errors.invalidToken": "Jeton de session invalide", "app.learningDashboard.errors.dataUnavailable": "Les données ne sont plus disponibles", + "app.learningDashboard.closeHelpModal": "Fermer la fenêtre d'aide du tableau de bord", + "app.learningDashboard.helpButtonLabel": "Aide", + "app.learningDashboard.help.title": "Aide du tableau de bord d'apprentissage", + "app.learningDashboard.help.activityScore.title": "Comment est réalisé le calcul du score d'activité", + "app.learningDashboard.help.activityScore.desc": "Le score d'activité d'un individu est calculé en fonction de sa participation lors de la séance. Ce sont 5 indicateurs normalisés, chacun pouvant totaliser 2 points, qui permettent d'évaluer la participation. Pour normaliser chaque indicateur, la participation la plus importante se voit octroyer 2 points, les scores plus bas sont calculés en proportion de celle-ci.", + "app.learningDashboard.help.activityScore.metric": "Métrique", + "app.learningDashboard.help.activityScore.formula": "Formule", + "app.learningDashboard.help.activityScore.maxValue": "Valeur maximale", + "app.learningDashboard.help.activityScore.talkingTime": "Temps de parole", + "app.learningDashboard.help.activityScore.messagesSent": "Messages envoyés", + "app.learningDashboard.help.activityScore.raiseHandEvents": "Mains levées", + "app.learningDashboard.help.activityScore.reactions": "Réactions", + "app.learningDashboard.help.activityScore.pollAnswers": "Réponses aux sondages/quiz", + "app.learningDashboard.help.activityScore.totalActivityScore": "Score d'activité total", + "app.learningDashboard.help.activityScore.userTalkingTime": "Temps de parole du participant", + "app.learningDashboard.help.activityScore.userMessagesSent": "Messages envoyés par le participant", + "app.learningDashboard.help.activityScore.userRaisedHands": "Mains levées par le participant ", + "app.learningDashboard.help.activityScore.userReactions": "Réactions du participant", + "app.learningDashboard.help.activityScore.userPollAnswers": "Réponses du participant aux sondages", + "app.learningDashboard.help.activityScore.maxTalkingTime": "Temps de parole maximum", + "app.learningDashboard.help.activityScore.maxMessagesSent": "Nombre maximum de messages envoyés", + "app.learningDashboard.help.activityScore.maxRaisedHands": "Nombre maximum de mains levées", + "app.learningDashboard.help.activityScore.maxReactions": "Nombre maximum de réactions", + "app.learningDashboard.help.activityScore.maxPollAnswers": "Nombre maximum de réponses aux sondages", + "app.learningDashboard.help.activityScore.maxMetricPointsLabel": "2 points", + "app.learningDashboard.help.activityScore.maxPointsLabel": "10 points", + "app.learningDashboard.help.activityScore.sumLabel": "Somme des scores aux indicateurs ci-dessus", + "app.learningDashboard.help.activityScore.moderatorWarning": "Les modérateurs ont un score de 0 par défaut et ne participent pas au calcul des valeurs pour les indicateurs.", "app.sessionDetails.title": "Détails de la session", "app.sessionDetails.dismissLabel": "Fermer", "app.sessionDetails.dismissDesc": "Fermer la fenêtre de dialogue", From 4c4c10e9f70e7cd7f7b4ed5fbc14db1e82c38563 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 07:39:31 +0000 Subject: [PATCH 022/121] Translate en.json in fr 100% translated source file: 'en.json' on 'fr'. --- bigbluebutton-html5/public/locales/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigbluebutton-html5/public/locales/fr.json b/bigbluebutton-html5/public/locales/fr.json index 24a91c1f2001..f8fa7b575b57 100644 --- a/bigbluebutton-html5/public/locales/fr.json +++ b/bigbluebutton-html5/public/locales/fr.json @@ -1588,7 +1588,7 @@ "app.learningDashboard.closeHelpModal": "Fermer la fenêtre d'aide du tableau de bord", "app.learningDashboard.helpButtonLabel": "Aide", "app.learningDashboard.help.title": "Aide du tableau de bord d'apprentissage", - "app.learningDashboard.help.activityScore.title": "Comment est réalisé le calcul du score d'activité", + "app.learningDashboard.help.activityScore.title": "Comment est calculé le score d'activité", "app.learningDashboard.help.activityScore.desc": "Le score d'activité d'un individu est calculé en fonction de sa participation lors de la séance. Ce sont 5 indicateurs normalisés, chacun pouvant totaliser 2 points, qui permettent d'évaluer la participation. Pour normaliser chaque indicateur, la participation la plus importante se voit octroyer 2 points, les scores plus bas sont calculés en proportion de celle-ci.", "app.learningDashboard.help.activityScore.metric": "Métrique", "app.learningDashboard.help.activityScore.formula": "Formule", From 94910b7581b248adedb9852e13232c646e1bc4c1 Mon Sep 17 00:00:00 2001 From: Claudio Promptoso Date: Tue, 23 Jun 2026 05:35:42 +0000 Subject: [PATCH 023/121] fix(bbb-export-annotations): add viewBox to background slides missing one to prevent ODF export cropping (#25303) Exported ODF slides with an image rendered cropped into the top-left corner. The background slide is loaded as an sized to the slide, but CairoSVG only rescales the referenced SVG to that box when the file carries a viewBox. Slides converted without one keep their intrinsic size and render cropped. When the slide SVG lacks a viewBox, derive one from its own width and height so the background scales to fill the slide. This is a no-op for slides that already carry a viewBox, so current decks are unaffected. --- bbb-export-annotations/workers/process.js | 43 +++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/bbb-export-annotations/workers/process.js b/bbb-export-annotations/workers/process.js index b923d3598147..dd76f6d2e686 100644 --- a/bbb-export-annotations/workers/process.js +++ b/bbb-export-annotations/workers/process.js @@ -295,6 +295,39 @@ async function overlayAnnotations(svg, slideAnnotations) { } } +/** + * Ensure a slide background SVG carries a viewBox. + * + * The background slide is loaded as an `` sized to the slide, but + * CairoSVG only rescales the referenced SVG to that box when the file is + * resolution-independent (carries a viewBox). Slides converted without one + * keep their intrinsic size and render cropped into the top-left corner. + * When the attribute is missing, derive it from the SVG's own width and + * height so the background scales to fill the slide. See issue #25303. + * @param {string} file Path of the slide SVG. + * @param {number} page Page number (for logging). + */ +function ensureSlideViewBox(file, page) { + try { + const svg = fs.readFileSync(file, {encoding: 'utf-8'}); + const svgTag = svg.match(/]*>/)?.[0]; + + if (!svgTag || /viewBox=/.test(svgTag)) return; + + const width = svgTag.match(/width="([\d.]+)/)?.[1]; + const height = svgTag.match(/height="([\d.]+)/)?.[1]; + + if (!width || !height) return; + + const patchedTag = svgTag.replace( + ' Date: Tue, 23 Jun 2026 14:11:30 +0000 Subject: [PATCH 024/121] harden width/height regex in ensureSlideViewBox (CodeRabbit review) --- bbb-export-annotations/workers/process.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bbb-export-annotations/workers/process.js b/bbb-export-annotations/workers/process.js index dd76f6d2e686..261c210c238f 100644 --- a/bbb-export-annotations/workers/process.js +++ b/bbb-export-annotations/workers/process.js @@ -314,8 +314,8 @@ function ensureSlideViewBox(file, page) { if (!svgTag || /viewBox=/.test(svgTag)) return; - const width = svgTag.match(/width="([\d.]+)/)?.[1]; - const height = svgTag.match(/height="([\d.]+)/)?.[1]; + const width = svgTag.match(/(? Date: Tue, 23 Jun 2026 16:20:20 -0300 Subject: [PATCH 025/121] add document-title-manager to handle title changes in each page --- .../imports/ui/components/app/component.jsx | 2 + .../app/document-title-manager/component.tsx | 219 ++++++++++++++++++ .../app/document-title-manager/service.ts | 50 ++++ .../component.tsx | 1 + .../create-breakout-room/component.tsx | 5 + .../common/modal/fullscreen/component.jsx | 49 +++- .../common/modal/simple/component.jsx | 42 ++++ .../ui/components/nav-bar/component.jsx | 25 -- .../ui/components/nav-bar/container.jsx | 33 +-- .../presentation-uploader/component.jsx | 12 +- .../ui/components/settings/component.jsx | 1 + 11 files changed, 374 insertions(+), 65 deletions(-) create mode 100644 bigbluebutton-html5/imports/ui/components/app/document-title-manager/component.tsx create mode 100644 bigbluebutton-html5/imports/ui/components/app/document-title-manager/service.ts diff --git a/bigbluebutton-html5/imports/ui/components/app/component.jsx b/bigbluebutton-html5/imports/ui/components/app/component.jsx index 7d6b7489d6b6..f657190f4a23 100644 --- a/bigbluebutton-html5/imports/ui/components/app/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/component.jsx @@ -47,6 +47,7 @@ import VoiceActivityAdapter from '../../core/adapters/voice-activity'; import LayoutObserver from '../layout/observer'; import BBBLiveKitRoomContainer from '/imports/ui/components/livekit/component'; import RaiseHandNotifier from '/imports/ui/components/raisehand-notifier/container'; +import DocumentTitleManager from './document-title-manager/component'; const intlMessages = defineMessages({ userListLabel: { @@ -364,6 +365,7 @@ class App extends Component { }} > + diff --git a/bigbluebutton-html5/imports/ui/components/app/document-title-manager/component.tsx b/bigbluebutton-html5/imports/ui/components/app/document-title-manager/component.tsx new file mode 100644 index 000000000000..2f57503365d1 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/app/document-title-manager/component.tsx @@ -0,0 +1,219 @@ +import React, { + useContext, useEffect, useMemo, useState, +} from 'react'; +import { defineMessages, useIntl } from 'react-intl'; +import * as PluginSdk from 'bigbluebutton-html-plugin-sdk'; +import { GenericContentType } from 'bigbluebutton-html-plugin-sdk/dist/cjs/extensible-areas/generic-content-item/enums'; +import getFromUserSettings from '/imports/ui/services/users-settings'; +import { PluginsContext } from '/imports/ui/components/components-data/plugin-context/context'; +import { layoutSelect, layoutSelectInput } from '/imports/ui/components/layout/context'; +import { PANELS } from '/imports/ui/components/layout/enums'; +import { Layout, Input } from '/imports/ui/components/layout/layoutTypes'; +import useMeeting from '/imports/ui/core/hooks/useMeeting'; +import useChat from '/imports/ui/core/hooks/useChat'; +import { Chat } from '/imports/ui/Types/chat'; +import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook'; +import { useStorageKey } from '/imports/ui/services/storage/hooks'; +import { + DOCUMENT_TITLE_VIEW_CHANGED, + getActiveDocumentTitleView, +} from './service'; + +const intlMessages = defineMessages({ + defaultViewLabel: { + id: 'app.title.defaultViewLabel', + description: 'view name appended to document title', + }, + userListLabel: { + id: 'app.userList.label', + description: 'view name appended to document title', + }, + chatLabel: { + id: 'app.chat.label', + description: 'view name appended to document title', + }, + publicChatTitle: { + id: 'app.chat.titlePublic', + description: 'view name appended to document title', + }, + privateChatTitle: { + id: 'app.chat.titlePrivate', + description: 'view name appended to document title', + }, + pollTitle: { + id: 'app.poll.pollPaneTitle', + description: 'view name appended to document title', + }, + captionsTitle: { + id: 'app.userList.captionsTitle', + description: 'view name appended to document title', + }, + notesTitle: { + id: 'app.notes.title', + description: 'view name appended to document title', + }, + breakoutRoomsTitle: { + id: 'app.createBreakoutRoom.title', + description: 'view name appended to document title', + }, + timerTitle: { + id: 'app.timer.title', + description: 'view name appended to document title', + }, + waitingUsersTitle: { + id: 'app.userList.guest.waitingUsers', + description: 'view name appended to document title', + }, + uploadPresentationTitle: { + id: 'app.presentationUploder.uploadViewTitle', + description: 'view name appended to document title', + }, + defaultBreakoutName: { + id: 'app.createBreakoutRoom.room', + description: 'default breakout room name', + }, +}); + +const getClientTitle = () => { + const publicConfig = window.meetingClientSettings?.public; + return getFromUserSettings('bbb_client_title', publicConfig?.app?.clientTitle || 'BigBlueButton'); +}; + +const getChatTitle = ( + chatId: string, + chats: Array> | null | undefined, + intl: ReturnType, +) => { + const chatConfig = window.meetingClientSettings.public.chat; + const isPublicChatId = chatId === chatConfig.public_id || chatId === chatConfig.public_group_id; + const selectedChat = chats?.find((chat) => ( + chat.chatId === chatId + || (isPublicChatId && (chat.chatId === chatConfig.public_id || chat.chatId === chatConfig.public_group_id)) + )); + + if (selectedChat?.public || isPublicChatId) { + return intl.formatMessage(intlMessages.publicChatTitle); + } + + if (selectedChat?.participant?.name) { + return intl.formatMessage(intlMessages.privateChatTitle, { + participantName: selectedChat.participant.name, + }); + } + + return intl.formatMessage(intlMessages.chatLabel); +}; + +const DocumentTitleManager: React.FC = () => { + const intl = useIntl(); + const idChatOpen = layoutSelect((i: Layout) => i.idChatOpen); + const sidebarNavigation = layoutSelectInput((i: Input) => i.sidebarNavigation); + const sidebarContent = layoutSelectInput((i: Input) => i.sidebarContent); + const uploadPresentationOpen = !!useStorageKey('showUploadPresentationView'); + const [registeredViewTitle, setRegisteredViewTitle] = useState(getActiveDocumentTitleView()); + const { pluginsExtensibleAreasAggregatedState } = useContext(PluginsContext); + const { data: meeting } = useMeeting((m) => ({ + name: m.name, + breakoutPolicies: { + sequence: m.breakoutPolicies?.sequence, + }, + })); + const { data: chats } = useChat((chat) => ({ + chatId: chat.chatId, + participant: chat.participant, + public: chat.public, + })) as GraphqlDataHookSubscriptionResponse[]>; + + const genericSidekickContentTitle = useMemo(() => { + const { sidebarContentPanel } = sidebarContent; + if (!sidebarContentPanel.startsWith(PANELS.GENERIC_CONTENT_SIDEKICK)) return null; + + const genericSidekickContentId = sidebarContentPanel.replace(PANELS.GENERIC_CONTENT_SIDEKICK, ''); + const genericContentItems = pluginsExtensibleAreasAggregatedState.genericContentItems || []; + const genericSidekickContentItems = genericContentItems + .filter((item) => item.type === GenericContentType.SIDEKICK_AREA) as PluginSdk.GenericContentSidekickArea[]; + const genericContentItem = genericSidekickContentItems.find((item) => item.id === genericSidekickContentId); + + return genericContentItem?.name || null; + }, [pluginsExtensibleAreasAggregatedState.genericContentItems, sidebarContent]); + + const activeViewTitle = useMemo(() => { + if (registeredViewTitle) return registeredViewTitle; + if (uploadPresentationOpen) return intl.formatMessage(intlMessages.uploadPresentationTitle); + + switch (sidebarContent.sidebarContentPanel) { + case PANELS.CHAT: + return getChatTitle(idChatOpen, chats, intl); + case PANELS.POLL: + return intl.formatMessage(intlMessages.pollTitle); + case PANELS.CAPTIONS: + return intl.formatMessage(intlMessages.captionsTitle); + case PANELS.BREAKOUT: + return intl.formatMessage(intlMessages.breakoutRoomsTitle); + case PANELS.SHARED_NOTES: + return intl.formatMessage(intlMessages.notesTitle); + case PANELS.TIMER: + return intl.formatMessage(intlMessages.timerTitle); + case PANELS.WAITING_USERS: + return intl.formatMessage(intlMessages.waitingUsersTitle); + default: + if (genericSidekickContentTitle) return genericSidekickContentTitle; + } + + if (sidebarNavigation.isOpen && sidebarNavigation.sidebarNavPanel === PANELS.USERLIST) { + return intl.formatMessage(intlMessages.userListLabel); + } + + return intl.formatMessage(intlMessages.defaultViewLabel); + }, [ + chats, + genericSidekickContentTitle, + idChatOpen, + intl, + sidebarContent.sidebarContentPanel, + sidebarNavigation.isOpen, + sidebarNavigation.sidebarNavPanel, + registeredViewTitle, + uploadPresentationOpen, + ]); + + const documentTitle = useMemo(() => { + const titleSegments = [getClientTitle()]; + const meetingName = meeting?.name?.trim(); + const breakoutNum = meeting?.breakoutPolicies?.sequence; + + if (meetingName) { + if (breakoutNum && breakoutNum > 0) { + const defaultBreakoutName = intl.formatMessage(intlMessages.defaultBreakoutName, { + roomNumber: breakoutNum, + }); + + titleSegments.push(meetingName === defaultBreakoutName ? `${breakoutNum}` : meetingName); + } else { + titleSegments.push(meetingName); + } + } + + if (activeViewTitle) titleSegments.push(activeViewTitle); + + return titleSegments.join(' - '); + }, [activeViewTitle, intl, meeting]); + + useEffect(() => { + document.title = documentTitle; + }, [documentTitle]); + + useEffect(() => { + const handleDocumentTitleViewChanged = (event: Event) => { + const customEvent = event as CustomEvent<{ activeTitle: string | null }>; + setRegisteredViewTitle(customEvent.detail.activeTitle); + }; + + window.addEventListener(DOCUMENT_TITLE_VIEW_CHANGED, handleDocumentTitleViewChanged); + return () => window.removeEventListener(DOCUMENT_TITLE_VIEW_CHANGED, handleDocumentTitleViewChanged); + }, []); + + return null; +}; + +export default DocumentTitleManager; diff --git a/bigbluebutton-html5/imports/ui/components/app/document-title-manager/service.ts b/bigbluebutton-html5/imports/ui/components/app/document-title-manager/service.ts new file mode 100644 index 000000000000..e43bacbecee7 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/app/document-title-manager/service.ts @@ -0,0 +1,50 @@ +export const DOCUMENT_TITLE_VIEW_CHANGED = 'bbb-document-title-view-changed'; + +type DocumentTitleView = { + title: string; + sequence: number; +}; + +const registeredTitleViews = new Map(); +let sequence = 0; + +const notifyDocumentTitleViewChanged = () => { + window.dispatchEvent(new CustomEvent(DOCUMENT_TITLE_VIEW_CHANGED, { + detail: { + activeTitle: getActiveDocumentTitleView(), + }, + })); +}; + +export const getActiveDocumentTitleView = (): string | null => { + const views = Array.from(registeredTitleViews.values()); + if (views.length === 0) return null; + + views.sort((a, b) => b.sequence - a.sequence); + return views[0].title; +}; + +export const registerDocumentTitleView = (id: string, title: string) => { + if (!id || !title) return; + + const existingView = registeredTitleViews.get(id); + registeredTitleViews.set(id, { + title, + sequence: existingView?.sequence || sequence + 1, + }); + + if (!existingView) sequence += 1; + notifyDocumentTitleViewChanged(); +}; + +export const unregisterDocumentTitleView = (id: string) => { + if (!registeredTitleViews.delete(id)) return; + notifyDocumentTitleViewChanged(); +}; + +let idSequence = 0; + +export const createDocumentTitleViewId = (prefix: string) => { + idSequence += 1; + return `${prefix}-${idSequence}`; +}; diff --git a/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/breakout-join-confirmation-graphql/component.tsx b/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/breakout-join-confirmation-graphql/component.tsx index b288d2311560..30012057020d 100644 --- a/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/breakout-join-confirmation-graphql/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/breakout-join-confirmation-graphql/component.tsx @@ -224,6 +224,7 @@ const BreakoutJoinConfirmation: React.FC = ({ return ( = ({ ? intl.formatMessage(intlMessages.updateTitle) : intl.formatMessage(intlMessages.breakoutRoomTitle) } + documentTitle={ + isUpdate + ? intl.formatMessage(intlMessages.updateTitle) + : intl.formatMessage(intlMessages.breakoutRoomTitle) + } confirm={ { label: isUpdate diff --git a/bigbluebutton-html5/imports/ui/components/common/modal/fullscreen/component.jsx b/bigbluebutton-html5/imports/ui/components/common/modal/fullscreen/component.jsx index a203f5ba5d56..94c757dd57e2 100644 --- a/bigbluebutton-html5/imports/ui/components/common/modal/fullscreen/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/common/modal/fullscreen/component.jsx @@ -2,6 +2,11 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl } from 'react-intl'; import Styled from './styles'; +import { + createDocumentTitleViewId, + registerDocumentTitleView, + unregisterDocumentTitleView, +} from '/imports/ui/components/app/document-title-manager/service'; const intlMessages = defineMessages({ modalClose: { @@ -38,6 +43,10 @@ const propTypes = { }), preventClosing: PropTypes.bool, shouldCloseOnOverlayClick: PropTypes.bool, + documentTitle: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.string, + ]), }; const defaultProps = { @@ -50,20 +59,31 @@ const defaultProps = { disabled: false, }, preventClosing: false, + documentTitle: false, }; class ModalFullscreen extends PureComponent { constructor(props) { super(props); this.previousFocus = null; + this.documentTitleViewId = createDocumentTitleViewId('fullscreen-modal'); this.handleAction = this.handleAction.bind(this); } + componentDidMount() { + this.updateDocumentTitleView(); + } + componentDidUpdate(prevProps) { const { isOpen } = this.props; if (!prevProps.isOpen && isOpen) { this.previousFocus = document.activeElement; } + this.updateDocumentTitleView(); + } + + componentWillUnmount() { + unregisterDocumentTitleView(this.documentTitleViewId); } handleAction(name) { @@ -95,6 +115,32 @@ class ModalFullscreen extends PureComponent { }, 0); } + getDocumentTitle() { + const { + documentTitle, + title, + } = this.props; + + if (!documentTitle) return null; + if (typeof documentTitle === 'string') return documentTitle; + return title; + } + + updateDocumentTitleView() { + const { + isOpen, + preventClosing, + } = this.props; + const documentTitle = this.getDocumentTitle(); + + if ((isOpen || preventClosing) && documentTitle) { + registerDocumentTitleView(this.documentTitleViewId, documentTitle); + return; + } + + unregisterDocumentTitleView(this.documentTitleViewId); + } + render() { const { intl, @@ -103,6 +149,7 @@ class ModalFullscreen extends PureComponent { dismiss, className, children, + documentTitle, isOpen, preventClosing, ...otherProps @@ -119,7 +166,7 @@ class ModalFullscreen extends PureComponent { id="fsmodal" isOpen={isOpen || preventClosing} contentLabel={title} - overlayClassName={"fullscreenModalOverlay"} + overlayClassName="fullscreenModalOverlay" {...otherProps} > diff --git a/bigbluebutton-html5/imports/ui/components/common/modal/simple/component.jsx b/bigbluebutton-html5/imports/ui/components/common/modal/simple/component.jsx index 51175675adbc..2c1a0831e420 100755 --- a/bigbluebutton-html5/imports/ui/components/common/modal/simple/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/common/modal/simple/component.jsx @@ -4,6 +4,11 @@ import { defineMessages, injectIntl } from 'react-intl'; import FocusTrap from 'focus-trap-react'; import Styled from './styles'; import deviceInfo from '/imports/utils/deviceInfo'; +import { + createDocumentTitleViewId, + registerDocumentTitleView, + unregisterDocumentTitleView, +} from '/imports/ui/components/app/document-title-manager/service'; const intlMessages = defineMessages({ modalClose: { @@ -26,6 +31,10 @@ const propTypes = { shouldShowCloseButton: PropTypes.bool, overlayClassName: PropTypes.string, modalIsOpen: PropTypes.bool, + documentTitle: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.string, + ]), }; const defaultProps = { @@ -38,6 +47,7 @@ const defaultProps = { overlayClassName: 'modalOverlay', headerPosition: 'inner', modalIsOpen: false, + documentTitle: false, }; class ModalSimple extends Component { @@ -45,6 +55,7 @@ class ModalSimple extends Component { super(props); this.modalRef = React.createRef(); this.previousFocus = null; + this.documentTitleViewId = createDocumentTitleViewId('simple-modal'); this.handleDismiss = this.handleDismiss.bind(this); this.handleRequestClose = this.handleRequestClose.bind(this); this.handleOutsideClick = this.handleOutsideClick.bind(this); @@ -52,6 +63,7 @@ class ModalSimple extends Component { componentDidMount() { document.addEventListener('mousedown', this.handleOutsideClick, false); + this.updateDocumentTitleView(); } componentDidUpdate(prevProps) { @@ -59,10 +71,12 @@ class ModalSimple extends Component { if (!prevProps.modalIsOpen && modalIsOpen) { this.previousFocus = document.activeElement; } + this.updateDocumentTitleView(); } componentWillUnmount() { document.removeEventListener('mousedown', this.handleOutsideClick, false); + unregisterDocumentTitleView(this.documentTitleViewId); } handleDismiss() { @@ -93,6 +107,33 @@ class ModalSimple extends Component { } } + getDocumentTitle() { + const { + contentLabel, + documentTitle, + title, + } = this.props; + + if (!documentTitle) return null; + if (typeof documentTitle === 'string') return documentTitle; + return title || contentLabel || null; + } + + updateDocumentTitleView() { + const { + isOpen, + modalIsOpen, + } = this.props; + const documentTitle = this.getDocumentTitle(); + + if ((isOpen || modalIsOpen) && documentTitle) { + registerDocumentTitleView(this.documentTitleViewId, documentTitle); + return; + } + + unregisterDocumentTitleView(this.documentTitleViewId); + } + render() { const { id, @@ -105,6 +146,7 @@ class ModalSimple extends Component { onRequestClose, shouldShowCloseButton, contentLabel, + documentTitle, headerPosition, 'data-test': dataTest, children, diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx index f72a08aae719..6b77539a0fb9 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx @@ -41,10 +41,6 @@ const intlMessages = defineMessages({ id: 'app.navBar.toggleUserList.newMsgAria', description: 'label for new message screen reader alert', }, - defaultBreakoutName: { - id: 'app.createBreakoutRoom.room', - description: 'default breakout room name', - }, leaveMeetingLabel: { id: 'app.navBar.leaveMeetingBtnLabel', description: 'Leave meeting button label', @@ -67,9 +63,6 @@ const propTypes = { presentationTitle: PropTypes.string, hasUnreadMessages: PropTypes.bool, shortcuts: PropTypes.string, - breakoutNum: PropTypes.number, - breakoutName: PropTypes.string, - meetingName: PropTypes.string, pluginNavBarItems: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.string, })).isRequired, @@ -179,26 +172,8 @@ class NavBar extends Component { componentDidMount() { const { shortcuts: TOGGLE_USERLIST_AK, - intl, - breakoutNum, - breakoutName, - meetingName, } = this.props; - if (breakoutNum && breakoutNum > 0) { - if (breakoutName && meetingName) { - const defaultBreakoutName = intl.formatMessage(intlMessages.defaultBreakoutName, { - roomNumber: breakoutNum, - }); - - if (breakoutName === defaultBreakoutName) { - document.title = `${breakoutNum} - ${meetingName}`; - } else { - document.title = `${breakoutName} - ${meetingName}`; - } - } - } - const { isFirefox } = browserInfo; const { isMacos } = deviceInfo; diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx index 129c59ddde8d..6b198c446772 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx @@ -1,5 +1,5 @@ import React, { useContext } from 'react'; -import { defineMessages, useIntl } from 'react-intl'; +import { useReactiveVar } from '@apollo/client'; import Auth from '/imports/ui/services/auth'; import getFromUserSettings from '/imports/ui/services/users-settings'; import NavBar from './component'; @@ -11,21 +11,11 @@ import useChat from '/imports/ui/core/hooks/useChat'; import useHasUnreadNotes from '../notes/hooks/useHasUnreadNotes'; import { useShortcut } from '../../core/hooks/useShortcut'; import useMeeting from '../../core/hooks/useMeeting'; -import { registerTitleView } from '/imports/utils/dom-utils'; -import { useReactiveVar } from '@apollo/client'; import connectionStatus from '../../core/graphql/singletons/connectionStatus'; -const intlMessages = defineMessages({ - defaultViewLabel: { - id: 'app.title.defaultViewLabel', - description: 'view name appended to document title', - }, -}); - const NavBarContainer = ({ children, ...props }) => { const { pluginsExtensibleAreasAggregatedState } = useContext(PluginsContext); const unread = useHasUnreadNotes(); - const intl = useIntl(); const sidebarContent = layoutSelectInput((i) => i.sidebarContent); const sidebarNavigation = layoutSelectInput((i) => i.sidebarNavigation); @@ -57,7 +47,6 @@ const NavBarContainer = ({ children, ...props }) => { const hideNavBar = getFromUserSettings('bbb_hide_nav_bar', false); const PUBLIC_CONFIG = window.meetingClientSettings.public; - const CLIENT_TITLE = getFromUserSettings('bbb_client_title', PUBLIC_CONFIG.app.clientTitle); const IS_DIRECT_LEAVE_BUTTON_ENABLED = getFromUserSettings( 'bbb_direct_leave_button', PUBLIC_CONFIG.app.defaultSettings.application.directLeaveButton, @@ -68,32 +57,15 @@ const NavBarContainer = ({ children, ...props }) => { ); let meetingTitle; - let breakoutNum; - let breakoutName; - let meetingName; const connected = useReactiveVar(connectionStatus.getConnectedStatusVar()); const { data: meeting } = useMeeting((m) => ({ name: m.name, meetingId: m.meetingId, - breakoutPolicies: { - sequence: m.breakoutPolicies.sequence, - }, })); if (meeting) { meetingTitle = meeting.name; - const titleString = `${CLIENT_TITLE} - ${meetingTitle}`; - document.title = titleString; - registerTitleView(intl.formatMessage(intlMessages.defaultViewLabel)); - - if (meeting.breakoutPolicies) { - breakoutNum = meeting.breakoutPolicies.sequence; - if (breakoutNum > 0) { - breakoutName = meetingTitle; - meetingName = meetingTitle.replace(`(${breakoutName})`, '').trim(); - } - } } if (hideNavBar || navBar.display === false) return null; @@ -122,9 +94,6 @@ const NavBarContainer = ({ children, ...props }) => { shortcuts: toggleUserList, meetingId: meeting?.meetingId, presentationTitle: meetingTitle, - breakoutNum, - breakoutName, - meetingName, isDirectLeaveButtonEnabled: IS_DIRECT_LEAVE_BUTTON_ENABLED, // TODO: Remove/Replace isConnected: connected, diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx index 847383b06750..b504645bb92a 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx @@ -7,7 +7,6 @@ import Button from '/imports/ui/components/common/button/component'; import update from 'immutability-helper'; import logger from '/imports/startup/client/logger'; import { toast } from 'react-toastify'; -import { registerTitleView, unregisterTitleView } from '/imports/utils/dom-utils'; import Styled from './styles'; import PresentationDownloadDropdown from './presentation-download-dropdown/component'; import { getSettingsSingletonInstance } from '/imports/ui/services/settings'; @@ -310,7 +309,11 @@ class PresentationUploader extends Component { } componentDidUpdate(prevProps) { - const { isOpen, presentations: propPresentations, currentPresentation, intl } = this.props; + const { + isOpen, + presentations: propPresentations, + currentPresentation, + } = this.props; const { presentations } = this.state; const { presentations: prevPropPresentations } = prevProps; @@ -444,13 +447,8 @@ class PresentationUploader extends Component { }); } - if (!isOpen && prevProps.isOpen) { - unregisterTitleView(); - } - // Updates presentation list when modal opens to avoid missing presentations if (isOpen && !prevProps.isOpen) { - registerTitleView(intl.formatMessage(intlMessages.uploadViewTitle)); const focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; const modal = document.getElementById('upload-modal'); const firstFocusableElement = modal?.querySelectorAll(focusableElements)[0]; diff --git a/bigbluebutton-html5/imports/ui/components/settings/component.jsx b/bigbluebutton-html5/imports/ui/components/settings/component.jsx index cdf84c1d5fba..db3b045f99f4 100644 --- a/bigbluebutton-html5/imports/ui/components/settings/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/settings/component.jsx @@ -336,6 +336,7 @@ class Settings extends Component { return ( { this.updateSettings(current, intlMessages.savedAlertLabel, setLocalSettings); From 06048be0c6b801900e4e3dc3c5786d8fae1ce20b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Tue, 23 Jun 2026 16:30:32 -0300 Subject: [PATCH 026/121] add tests for title changes --- .../accessibility/document-title.spec.ts | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 bigbluebutton-tests/playwright/accessibility/document-title.spec.ts diff --git a/bigbluebutton-tests/playwright/accessibility/document-title.spec.ts b/bigbluebutton-tests/playwright/accessibility/document-title.spec.ts new file mode 100644 index 000000000000..2651d4ddaf23 --- /dev/null +++ b/bigbluebutton-tests/playwright/accessibility/document-title.spec.ts @@ -0,0 +1,88 @@ +import { expect } from '@playwright/test'; + +import { openPrivateChat } from '../chat/util'; +import { elements as e } from '../core/elements'; +import { test } from '../core/setup/fixtures'; +import { openSettings } from '../options/util'; +import { MultiUsers } from '../user/multiusers'; + +test.describe.parallel('Accessible routing', { tag: '@ci' }, () => { + test('updates the document title for active client views', async ({ browser, context, page }, testInfo) => { + const session = new MultiUsers(browser, context); + await session.initModPage(page, { testInfo }); + const { modPage } = session; + + await expect(modPage.page, 'initial meeting title should include the active view').toHaveTitle( + / - (User list|Public Chat|Default presentation view)$/, + ); + + if (modPage.settings?.chatEnabled) { + if (!(await modPage.page.locator(e.hidePublicChat).isVisible())) { + if (!(await modPage.page.locator(e.chatButton).first().isVisible())) { + await modPage.waitAndClick(e.userListToggleBtn); + } + + if (!(await modPage.page.locator(e.hidePublicChat).isVisible())) { + await modPage.waitAndClick(e.chatButton); + } + } + + await modPage.hasElement(e.hidePublicChat, 'should display public chat'); + await expect(modPage.page, 'public chat should be reflected in the document title').toHaveTitle( + / - Public Chat$/, + ); + } + + if (modPage.settings?.sharedNotesEnabled) { + await modPage.waitAndClick(e.sharedNotes); + await modPage.hasElement(e.sharedNotesBackground, 'should display shared notes'); + await expect(modPage.page, 'shared notes should be reflected in the document title').toHaveTitle( + / - Shared Notes$/, + ); + } + + if (modPage.settings?.pollEnabled) { + await modPage.waitAndClick(e.actions); + await modPage.waitAndClick(e.polling); + await modPage.hasElement(e.hidePollDesc, 'should display the polling panel'); + await expect(modPage.page, 'polling should be reflected in the document title').toHaveTitle(/ - Polling$/); + } + + await modPage.waitAndClick(e.actions); + await modPage.waitAndClick(e.managePresentations); + await modPage.hasElement(e.presentationFileUpload, 'should display the presentation upload view'); + await expect(modPage.page, 'presentation upload should be reflected in the document title').toHaveTitle( + / - Upload Presentation$/, + ); + }); + + test('updates the document title for private chat', async ({ browser, context, page }, testInfo) => { + const session = new MultiUsers(browser, context); + await session.initPages(page, testInfo); + const { modPage } = session; + + test.skip(!modPage.settings?.chatEnabled, 'Chat is disabled'); + await openPrivateChat(modPage); + await modPage.hasElement(e.hidePrivateChat, 'should display private chat'); + await expect(modPage.page, 'private chat participant should be reflected in the document title').toHaveTitle( + / - Private Chat with Attendee$/, + ); + }); + + test('updates the document title for route-like modals', async ({ browser, context, page }, testInfo) => { + const session = new MultiUsers(browser, context); + await session.initModPage(page, { testInfo }); + const { modPage } = session; + + await openSettings(modPage); + await expect(modPage.page, 'settings modal should be reflected in the document title').toHaveTitle(/ - Settings$/); + await modPage.waitAndClick(e.modalDismissButton); + + await modPage.waitAndClick(e.manageUsers); + await modPage.waitAndClick(e.createBreakoutRooms); + await expect(modPage.page, 'breakout room creation modal should be reflected in the document title').toHaveTitle( + / - Breakout Rooms$/, + ); + await modPage.waitAndClick(e.modalDismissButton); + }); +}); From 3b6395d87b2e7ba3c956cf481390873f3001e10f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Tue, 23 Jun 2026 17:12:57 -0300 Subject: [PATCH 027/121] avoid no-op title events and guard optional breakout title test --- .../components/app/document-title-manager/service.ts | 2 ++ .../playwright/accessibility/document-title.spec.ts | 12 +++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/app/document-title-manager/service.ts b/bigbluebutton-html5/imports/ui/components/app/document-title-manager/service.ts index e43bacbecee7..07f0fc3340bc 100644 --- a/bigbluebutton-html5/imports/ui/components/app/document-title-manager/service.ts +++ b/bigbluebutton-html5/imports/ui/components/app/document-title-manager/service.ts @@ -28,6 +28,8 @@ export const registerDocumentTitleView = (id: string, title: string) => { if (!id || !title) return; const existingView = registeredTitleViews.get(id); + if (existingView?.title === title) return; + registeredTitleViews.set(id, { title, sequence: existingView?.sequence || sequence + 1, diff --git a/bigbluebutton-tests/playwright/accessibility/document-title.spec.ts b/bigbluebutton-tests/playwright/accessibility/document-title.spec.ts index 2651d4ddaf23..ef8bd9ae8a8a 100644 --- a/bigbluebutton-tests/playwright/accessibility/document-title.spec.ts +++ b/bigbluebutton-tests/playwright/accessibility/document-title.spec.ts @@ -79,10 +79,12 @@ test.describe.parallel('Accessible routing', { tag: '@ci' }, () => { await modPage.waitAndClick(e.modalDismissButton); await modPage.waitAndClick(e.manageUsers); - await modPage.waitAndClick(e.createBreakoutRooms); - await expect(modPage.page, 'breakout room creation modal should be reflected in the document title').toHaveTitle( - / - Breakout Rooms$/, - ); - await modPage.waitAndClick(e.modalDismissButton); + if (await modPage.page.locator(e.createBreakoutRooms).isVisible()) { + await modPage.waitAndClick(e.createBreakoutRooms); + await expect(modPage.page, 'breakout room creation modal should be reflected in the document title').toHaveTitle( + / - Breakout Rooms$/, + ); + await modPage.waitAndClick(e.modalDismissButton); + } }); }); From 4c39d124b4e997a0bff571e6e2643b644fe1e4b5 Mon Sep 17 00:00:00 2001 From: Claudio Promptoso Date: Wed, 24 Jun 2026 14:25:13 +0000 Subject: [PATCH 028/121] Add analytics logging for breakout move-user request ChangeUserBreakoutReqMsg (the moderator move-user request) was missing from AnalyticsActor's case branches, so move-user actions left no trace in bbb-apps-akka.log (unlike create/end/time breakout actions). Add the case following the existing pattern. Only the inbound request is logged -- it captures who moved whom and the from/to breakout rooms -- without the join URL; the outbound ChangeUserBreakoutEvtMsg and BreakoutRoomJoinURLEvtMsg carry signed join URLs (participant name + meeting password) and are intentionally not logged to avoid writing credentials to the log. --- .../src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala index 52fb1be580a7..ec190bb481fb 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala @@ -231,6 +231,7 @@ class AnalyticsActor(val includeChat: Boolean) extends Actor with ActorLogging { // Breakouts case m: CreateBreakoutRoomsCmdMsg => logMessage(msg) + case m: ChangeUserBreakoutReqMsg => logMessage(msg) case _ => // ignore message } From 5b826fa4f4d3a5f9d11d9da12b93ab1d7cd3989a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Wed, 24 Jun 2026 15:11:44 -0300 Subject: [PATCH 029/121] update components to have better contrast between background and borders --- .../actions-bar/reactions-button/styles.js | 4 +-- .../audio-captions/captions/styles.ts | 4 +-- .../audio/device-selector/styles.js | 4 +-- .../styles.ts | 4 +-- .../breakout-room/breakout-room/styles.ts | 13 +++++---- .../room-user-list/styles.ts | 4 +-- .../create-breakout-room/styles.ts | 13 +++++---- .../chat-editing-warning/styles.ts | 4 +-- .../chat-offline-indicator/styles.ts | 4 +-- .../chat-graphql/chat-message-form/styles.ts | 10 +++---- .../message-content/text-content/styles.ts | 4 +-- .../reaction-item/styles.tsx | 6 ++--- .../chat-message/message-replied/styles.tsx | 7 ++--- .../chat-message/message-toolbar/styles.ts | 6 ++--- .../page/chat-message/styles.ts | 7 ++--- .../chat-reply-intention/styles.tsx | 6 ++--- .../common/modal/fullscreen/styles.js | 4 +-- .../components/common/modal/header/styles.js | 4 +-- .../connection-status/modal/styles.js | 4 +-- .../modal/styles.ts | 7 +++-- .../ui/components/lock-viewers/styles.js | 4 +-- .../pads/pads-graphql/content/styles.ts | 6 ++--- .../ui/components/pads/pads-graphql/styles.ts | 4 +-- .../imports/ui/components/poll/styles.ts | 27 +++++++++---------- .../imports/ui/components/polling/styles.ts | 12 ++++----- .../presentation-toolbar/styles.js | 4 +-- .../presentation-uploader/styles.js | 5 ++-- .../settings/submenus/application/styles.js | 6 ++--- .../ui/components/settings/submenus/styles.js | 4 +-- .../ui/components/text-input/styles.js | 9 +++---- .../ui/components/timer/panel/styles.ts | 12 ++++----- .../user-list/user-list-content/styles.js | 4 +-- .../user-messages/chat-list/styles.ts | 4 +-- .../ui/components/video-preview/styles.js | 3 ++- .../virtual-background/styles.js | 4 +-- .../waiting-users-graphql/styles.ts | 6 ++--- .../stylesheets/styled-components/palette.js | 16 ++++++----- 37 files changed, 122 insertions(+), 127 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/reactions-button/styles.js b/bigbluebutton-html5/imports/ui/components/actions-bar/reactions-button/styles.js index 828d95758712..527cf082c0c0 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/reactions-button/styles.js +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/reactions-button/styles.js @@ -2,9 +2,9 @@ import styled from 'styled-components'; import Button from '/imports/ui/components/common/button/component'; import { + colorBorder, colorWhite, colorGrayDark, - colorGrayLightest, btnPrimaryColor, btnPrimaryActiveBg, } from '/imports/ui/stylesheets/styled-components/palette'; @@ -69,7 +69,7 @@ const ReactionsButtonWrapper = styled(ButtonWrapper)` ${({ isMobile }) => !isMobile && ` - border: 1px solid ${colorGrayLightest}; + border: 1px solid ${colorBorder}; padding: 1rem 0.5rem; width: auto; `} diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/captions/styles.ts b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/captions/styles.ts index 9d4c48b21300..e1a14469407a 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/captions/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/captions/styles.ts @@ -5,7 +5,7 @@ import { import { colorGrayLabel, colorWhite, - colorGrayLighter, + colorBorder, colorPrimary, } from '/imports/ui/stylesheets/styled-components/palette'; @@ -18,7 +18,7 @@ const CaptionsSelector = styled.div` const Select = styled.select` background-color: ${colorWhite}; - border: 0.1rem solid ${colorGrayLighter}; + border: 0.1rem solid ${colorBorder}; border-radius: ${borderSize}; color: ${colorGrayLabel}; width: 100%; diff --git a/bigbluebutton-html5/imports/ui/components/audio/device-selector/styles.js b/bigbluebutton-html5/imports/ui/components/audio/device-selector/styles.js index 69932a1c6970..50e4e7a76527 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/device-selector/styles.js +++ b/bigbluebutton-html5/imports/ui/components/audio/device-selector/styles.js @@ -5,13 +5,13 @@ import { import { colorGrayLabel, colorWhite, - colorGrayLighter, + colorBorder, colorPrimary, } from '/imports/ui/stylesheets/styled-components/palette'; const Select = styled.select` background-color: ${colorWhite}; - border: 0.1rem solid ${colorGrayLighter}; + border: 0.1rem solid ${colorBorder}; border-radius: ${borderSize}; color: ${colorGrayLabel}; width: 100%; diff --git a/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/breakout-join-confirmation-graphql/styles.ts b/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/breakout-join-confirmation-graphql/styles.ts index a30b4665de26..6ad9cc170b52 100644 --- a/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/breakout-join-confirmation-graphql/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/breakout-join-confirmation-graphql/styles.ts @@ -1,5 +1,5 @@ import styled from 'styled-components'; -import { colorWhite, colorGrayLighter } from '/imports/ui/stylesheets/styled-components/palette'; +import { colorWhite, colorBorder } from '/imports/ui/stylesheets/styled-components/palette'; const SelectParent = styled.div` display: flex; @@ -11,7 +11,7 @@ const Select = styled.select` background-color: ${colorWhite}; width: 50%; margin: 1rem; - border-color: ${colorGrayLighter}; + border-color: ${colorBorder}; `; export default { diff --git a/bigbluebutton-html5/imports/ui/components/breakout-room/breakout-room/styles.ts b/bigbluebutton-html5/imports/ui/components/breakout-room/breakout-room/styles.ts index 099f9ee52032..e2bc51949c3b 100644 --- a/bigbluebutton-html5/imports/ui/components/breakout-room/breakout-room/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/breakout-room/breakout-room/styles.ts @@ -9,14 +9,13 @@ import { smPaddingY, } from '/imports/ui/stylesheets/styled-components/general'; import { + colorBorder, colorPrimary, colorGray, colorDanger, userListBg, colorWhite, colorGrayLighter, - colorGrayLightest, - colorBlueLight, listItemBgHover, colorText, } from '/imports/ui/stylesheets/styled-components/palette'; @@ -184,7 +183,7 @@ const DurationContainer = styled.div` border-radius: ${borderRadius}; margin-bottom: ${jumboPaddingY}; padding: 10px; - box-shadow: 0 0 1px 1px ${colorGrayLightest}; + box-shadow: 0 0 1px 1px ${colorBorder}; `; const SetTimeContainer = styled.div` @@ -193,7 +192,7 @@ const SetTimeContainer = styled.div` const SetDurationInput = styled.input` flex: 1; - border: 1px solid ${colorGrayLighter}; + border: 1px solid ${colorBorder}; width: 50%; text-align: center; padding: .25rem; @@ -208,7 +207,7 @@ const SetDurationInput = styled.input` &:focus { border-radius: ${borderSize}; - box-shadow: 0 0 0 ${borderSize} ${colorBlueLight}, inset 0 0 0 1px ${colorPrimary}; + box-shadow: 0 0 0 ${borderSize} ${colorPrimary}, inset 0 0 0 1px ${colorPrimary}; } &:disabled, @@ -294,7 +293,7 @@ const Input = styled(TextareaAutosize)` line-height: 1; min-height: 2.5rem; max-height: 10rem; - border: 1px solid ${colorGrayLighter}; + border: 1px solid ${colorBorder}; &:disabled, &[disabled] { @@ -305,7 +304,7 @@ const Input = styled(TextareaAutosize)` &:focus { border-radius: ${borderSize}; - box-shadow: 0 0 0 ${borderSize} ${colorBlueLight}, inset 0 0 0 1px ${colorPrimary}; + box-shadow: 0 0 0 ${borderSize} ${colorPrimary}, inset 0 0 0 1px ${colorPrimary}; } &:hover, diff --git a/bigbluebutton-html5/imports/ui/components/breakout-room/create-breakout-room/breakout-room-user-assignment-mobile/room-user-list/styles.ts b/bigbluebutton-html5/imports/ui/components/breakout-room/create-breakout-room/breakout-room-user-assignment-mobile/room-user-list/styles.ts index 2ac01f4f5782..f933d1bfa3a5 100644 --- a/bigbluebutton-html5/imports/ui/components/breakout-room/create-breakout-room/breakout-room-user-assignment-mobile/room-user-list/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/breakout-room/create-breakout-room/breakout-room-user-assignment-mobile/room-user-list/styles.ts @@ -2,8 +2,8 @@ import styled from 'styled-components'; import Button from '/imports/ui/components/common/button/component'; import { TextElipsis, TitleElipsis } from '/imports/ui/stylesheets/styled-components/placeholders'; import { + colorBorder, colorWhite, - colorGrayLighter, colorGrayLight, } from '/imports/ui/stylesheets/styled-components/palette'; import { borderSize } from '/imports/ui/stylesheets/styled-components/general'; @@ -124,7 +124,7 @@ const SelectUserScreen = styled.div` const Header = styled.header` display: flex; padding: ${lineHeightComputed} 0; - border-bottom: ${borderSize} solid ${colorGrayLighter}; + border-bottom: ${borderSize} solid ${colorBorder}; margin: 0 1rem 0 1rem; `; diff --git a/bigbluebutton-html5/imports/ui/components/breakout-room/create-breakout-room/styles.ts b/bigbluebutton-html5/imports/ui/components/breakout-room/create-breakout-room/styles.ts index 16564a34331f..3b44b5e04bb1 100644 --- a/bigbluebutton-html5/imports/ui/components/breakout-room/create-breakout-room/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/breakout-room/create-breakout-room/styles.ts @@ -5,14 +5,13 @@ import HoldButton from '/imports/ui/components/presentation/presentation-toolbar import Button from '/imports/ui/components/common/button/component'; import { FlexRow, FlexColumn } from '/imports/ui/stylesheets/styled-components/placeholders'; import { + colorBorder, colorDanger, colorGray, colorGrayLight, - colorGrayLighter, colorWhite, colorPrimary, colorBlueLight, - colorGrayLightest, } from '/imports/ui/stylesheets/styled-components/palette'; import { fontSizeSmall, fontSizeBase, fontSizeSmaller } from '/imports/ui/stylesheets/styled-components/typography'; import { @@ -95,7 +94,7 @@ const BreakoutNameInput = styled.input` color: ${colorGray}; opacity: 1; } - border: 1px solid ${colorGrayLightest}; + border: 1px solid ${colorBorder}; margin-bottom: 1rem; ${({ readOnly }) => readOnly && ` @@ -106,7 +105,7 @@ const BreakoutNameInput = styled.input` const BreakoutBox = styled(ScrollboxVertical)` max-width: 13rem; height: 10rem; - border: 1px solid ${colorGrayLightest}; + border: 1px solid ${colorBorder}; border-radius: ${borderRadius}; padding: ${lgPaddingY} 0; @@ -166,7 +165,7 @@ const FormLabel = styled.p` const InputRooms = styled.select` background-color: ${colorWhite}; color: ${colorGray}; - border: 1px solid ${colorGrayLighter}; + border: 1px solid ${colorBorder}; border-radius: ${borderRadius}; width: 100%; padding-top: .25rem; @@ -207,7 +206,7 @@ const DurationArea = styled.div` const DurationInput = styled.input` background-color: ${colorWhite}; color: ${colorGray}; - border: 1px solid ${colorGrayLighter}; + border: 1px solid ${colorBorder}; border-radius: ${borderRadius}; width: 100%; text-align: left; @@ -261,7 +260,7 @@ const Separator = styled.div` width: 100%; height: 1px; margin: 1rem 0; - border: 1px solid ${colorGrayLightest}; + border: 1px solid ${colorBorder}; `; const FreeJoinCheckbox = styled.input` diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-editing-warning/styles.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-editing-warning/styles.ts index d55c0b599fe6..b52e7b7cce41 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-editing-warning/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-editing-warning/styles.ts @@ -1,5 +1,5 @@ import styled from 'styled-components'; -import { colorGrayLight, colorWhite } from '/imports/ui/stylesheets/styled-components/palette'; +import { colorBorder, colorGrayLight, colorWhite } from '/imports/ui/stylesheets/styled-components/palette'; import { xlPadding, xsPadding } from '/imports/ui/stylesheets/styled-components/general'; export const Root = styled.div` @@ -49,7 +49,7 @@ export const Cancel = styled.button` } &:focus { - box-shadow: inset 0 0 0.125rem ${colorGrayLight}; + box-shadow: inset 0 0 0.125rem ${colorBorder}; } `; diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/chat-offline-indicator/styles.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/chat-offline-indicator/styles.ts index 9632b7452439..d4080e550b3c 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/chat-offline-indicator/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/chat-offline-indicator/styles.ts @@ -1,5 +1,5 @@ import styled from 'styled-components'; -import { colorText, colorGrayLighter } from '/imports/ui/stylesheets/styled-components/palette'; +import { colorText, colorBorder } from '/imports/ui/stylesheets/styled-components/palette'; export const ChatOfflineIndicator = styled.div` display: flex; @@ -10,7 +10,7 @@ export const ChatOfflineIndicator = styled.div` margin-top: 0.2rem; padding: 0.5rem; border-radius: 2px; - border-top: 1px solid ${colorGrayLighter}; + border-top: 1px solid ${colorBorder}; & > span { color: ${colorText}; font-size: 1rem; diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/styles.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/styles.ts index ebbfe20bdc30..aa3a2db2fc5f 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/styles.ts @@ -1,11 +1,9 @@ import styled from 'styled-components'; import { - colorBlueLight, colorText, - colorGrayLighter, + colorBorder, colorDanger, colorGrayDark, - colorGrayLightest, } from '/imports/ui/stylesheets/styled-components/palette'; import { smPaddingX, @@ -124,7 +122,7 @@ position: absolute; bottom: calc(100% + 0.5rem); left: 0; right: 0; -border: 1px solid ${colorGrayLighter}; +border: 1px solid ${colorBorder}; border-radius: ${borderRadius}; box-shadow: 0 2px 10px rgba(0,0,0,0.1); z-index: 1000; @@ -169,7 +167,7 @@ const InputWrapper = styled.div` min-width: 0; z-index: 0; padding: 1px 0 1px 1px; - border: 1px solid ${colorGrayLightest}; + border: 1px solid ${colorBorder}; [dir='ltr'] & { border-radius: 0.75rem 0 0 0.75rem; @@ -180,7 +178,7 @@ const InputWrapper = styled.div` } &:focus-within { - border: 1px solid ${colorBlueLight}; + border: 1px solid ${colorBorder}; } `; diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-content/text-content/styles.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-content/text-content/styles.ts index 8c74d069e4d7..94122b0897cc 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-content/text-content/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-content/text-content/styles.ts @@ -1,7 +1,7 @@ import styled from 'styled-components'; import { colorDangerDark, - colorGrayLightest, + colorBorder, colorOffWhite, colorText, } from '/imports/ui/stylesheets/styled-components/palette'; @@ -30,7 +30,7 @@ export const ChatMessage = styled.div` & pre:has(code), p code:not(pre > code) { background-color: ${colorOffWhite}; - border: solid 1px ${colorGrayLightest}; + border: solid 1px ${colorBorder}; border-radius: 4px; padding: 2px; margin: 0; diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-reactions/reaction-item/styles.tsx b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-reactions/reaction-item/styles.tsx index 570fee1640bf..9a0d96c9e831 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-reactions/reaction-item/styles.tsx +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-reactions/reaction-item/styles.tsx @@ -1,5 +1,5 @@ import styled from 'styled-components'; -import { colorGrayLightest, colorOffWhite, colorGrayLighter } from '/imports/ui/stylesheets/styled-components/palette'; +import { colorBorder, colorOffWhite } from '/imports/ui/stylesheets/styled-components/palette'; const EmojiWrapper = styled.button<{ highlighted: boolean }>` background: none; @@ -8,7 +8,7 @@ const EmojiWrapper = styled.button<{ highlighted: boolean }>` line-height: 1; display: flex; flex-wrap: nowrap; - border: 1px solid ${colorGrayLightest}; + border: 1px solid ${colorBorder}; cursor: pointer; ${({ highlighted }) => highlighted && ` @@ -26,7 +26,7 @@ const EmojiWrapper = styled.button<{ highlighted: boolean }>` } &:hover { - border: 1px solid ${colorGrayLighter}; + border: 1px solid ${colorBorder}; } `; diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-replied/styles.tsx b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-replied/styles.tsx index 40852f0dca32..1e08fecc858c 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-replied/styles.tsx +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-replied/styles.tsx @@ -1,8 +1,9 @@ import styled from 'styled-components'; import { colorDangerDark, + colorBorder, colorGrayLight, - colorGrayLightest, colorOffWhite, colorPrimary, colorText, colorWhite, + colorOffWhite, colorPrimary, colorText, colorWhite, } from '/imports/ui/stylesheets/styled-components/palette'; import { $3xlPadding, smPadding } from '/imports/ui/stylesheets/styled-components/general'; @@ -10,7 +11,7 @@ const Container = styled.div` border-top-left-radius: 0.5rem; border-top-right-radius: 0.5rem; background-color: ${colorWhite}; - box-shadow: inset 0 0 0 1px ${colorGrayLightest}; + box-shadow: inset 0 0 0 1px ${colorBorder}; padding: ${smPadding} ${$3xlPadding}; position: relative; overflow: hidden; @@ -52,7 +53,7 @@ export const HtmlContent = styled.div` & pre:has(code), p code:not(pre > code) { background-color: ${colorOffWhite}; - border: solid 1px ${colorGrayLightest}; + border: solid 1px ${colorBorder}; border-radius: 4px; padding: 2px; margin: 0; diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-toolbar/styles.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-toolbar/styles.ts index 20d83b4cc2df..3aae2335adec 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-toolbar/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-toolbar/styles.ts @@ -1,6 +1,6 @@ import styled, { css } from 'styled-components'; import { - colorGrayLighter, + colorBorder, colorGrayLightest, colorWhite, } from '/imports/ui/stylesheets/styled-components/palette'; @@ -50,7 +50,7 @@ const Container = styled.div` display: flex; border-radius: 1rem; background-color: ${colorWhite}; - box-shadow: 0 0.125rem 0.125rem 0 ${colorGrayLighter}; + box-shadow: 0 0.125rem 0.125rem 0 ${colorBorder}; padding: ${smPadding} ${lgPadding}; gap: ${smPadding}; `; @@ -60,7 +60,7 @@ const EmojiPickerWrapper = styled.div` bottom: calc(100% + 0.5rem); left: 0; right: 0; - border: 1px solid ${colorGrayLighter}; + border: 1px solid ${colorBorder}; border-radius: ${borderRadius}; box-shadow: 0 0.125rem 10px rgba(0,0,0,0.1); z-index: 1000; diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/styles.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/styles.ts index dfe9f5408bea..81099f82e48f 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/styles.ts @@ -20,8 +20,9 @@ import { colorSuccess, colorBlueLightest, colorGrayLight, - colorGrayLightest, + colorBorder, colorGrayDark, + colorSelectedCorrectAnswerText, emphasizedMessageBackgroundColor, highlightedMessageBorderColor, } from '/imports/ui/stylesheets/styled-components/palette'; @@ -82,7 +83,7 @@ export const ChatWrapper = styled.div` `} ${({ messageHighlight }) => messageHighlight && ` background-color: #fef9f1; - border-left: 2px solid #f5c67f; + border-left: 2px solid ${colorSelectedCorrectAnswerText}; border-radius: 0px 3px 3px 0px; padding: 8px 2px; `} @@ -274,7 +275,7 @@ export const PluginInformationMetadata = styled.div` export const DeleteMessage = styled.span` color: ${colorGrayLight}; padding: ${mdPadding} ${xlPadding}; - border: 1px solid ${colorGrayLightest}; + border: 1px solid ${colorBorder}; border-radius: 0.375rem; `; diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-reply-intention/styles.tsx b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-reply-intention/styles.tsx index 03d5dd179d2d..f18e734af411 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-reply-intention/styles.tsx +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-reply-intention/styles.tsx @@ -1,7 +1,7 @@ import styled, { css } from 'styled-components'; import { colorDangerDark, - colorGrayLightest, colorOffWhite, + colorBorder, colorOffWhite, colorPrimary, colorText, colorWhite, @@ -14,7 +14,7 @@ import EmojiButton from '../chat-message-list/page/chat-message/message-toolbar/ const Container = styled.div<{ $hidden: boolean; $animations: boolean }>` border-radius: 0.375rem; background-color: ${colorWhite}; - box-shadow: inset 0 0 0 1px ${colorGrayLightest}; + box-shadow: inset 0 0 0 1px ${colorBorder}; display: flex; align-items: center; overflow: hidden; @@ -76,7 +76,7 @@ const HtmlContent = styled.div` & pre:has(code), p code:not(pre > code) { background-color: ${colorOffWhite}; - border: solid 1px ${colorGrayLightest}; + border: solid 1px ${colorBorder}; border-radius: 4px; padding: 2px; margin: 0; diff --git a/bigbluebutton-html5/imports/ui/components/common/modal/fullscreen/styles.js b/bigbluebutton-html5/imports/ui/components/common/modal/fullscreen/styles.js index 66467cccc5a2..943c65cbefe0 100644 --- a/bigbluebutton-html5/imports/ui/components/common/modal/fullscreen/styles.js +++ b/bigbluebutton-html5/imports/ui/components/common/modal/fullscreen/styles.js @@ -12,7 +12,7 @@ import { fontSizeLarger, } from '/imports/ui/stylesheets/styled-components/typography'; import { - colorGrayLightest, + colorBorder, colorText, } from '/imports/ui/stylesheets/styled-components/palette'; @@ -34,7 +34,7 @@ const FullscreenModal = styled(Styled.BaseModal)` const Header = styled.header` display: flex; padding: ${lineHeightComputed} 0; - border-bottom: ${borderSize} solid ${colorGrayLightest}; + border-bottom: ${borderSize} solid ${colorBorder}; `; const Title = styled.h1` diff --git a/bigbluebutton-html5/imports/ui/components/common/modal/header/styles.js b/bigbluebutton-html5/imports/ui/components/common/modal/header/styles.js index 927b667da8e6..749220c105b1 100644 --- a/bigbluebutton-html5/imports/ui/components/common/modal/header/styles.js +++ b/bigbluebutton-html5/imports/ui/components/common/modal/header/styles.js @@ -3,8 +3,8 @@ import Button from '/imports/ui/components/common/button/component'; import { TitleElipsis } from '/imports/ui/stylesheets/styled-components/placeholders'; import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints'; import { + colorBorder, colorGrayDark, - colorGrayLighter, colorText, } from '/imports/ui/stylesheets/styled-components/palette'; import { @@ -36,7 +36,7 @@ const Header = styled.header` ${({ $hideBorder }) => !$hideBorder && ` padding: calc(${lineHeightComputed} / 2) 0; - border-bottom: ${borderSize} solid ${colorGrayLighter}; + border-bottom: ${borderSize} solid ${colorBorder}; `} `; diff --git a/bigbluebutton-html5/imports/ui/components/connection-status/modal/styles.js b/bigbluebutton-html5/imports/ui/components/connection-status/modal/styles.js index 3d04ed58c5a2..e81741a263dc 100644 --- a/bigbluebutton-html5/imports/ui/components/connection-status/modal/styles.js +++ b/bigbluebutton-html5/imports/ui/components/connection-status/modal/styles.js @@ -1,9 +1,9 @@ import styled from 'styled-components'; import ModalSimple from '/imports/ui/components/common/modal/simple/component'; import { + colorBorder, colorOffWhite, colorGrayDark, - colorGrayLightest, colorPrimary, colorWhite, btnPrimaryActiveBg, @@ -39,7 +39,7 @@ const Item = styled.li` display: flex; width: 100%; height: 4rem; - border-bottom: 1px solid ${colorGrayLightest}; + border-bottom: 1px solid ${colorBorder}; ${({ last }) => last && ` border: none; diff --git a/bigbluebutton-html5/imports/ui/components/external-video-player/external-video-player-graphql/modal/styles.ts b/bigbluebutton-html5/imports/ui/components/external-video-player/external-video-player-graphql/modal/styles.ts index 41ff2f1286e9..aa8881bc02b8 100644 --- a/bigbluebutton-html5/imports/ui/components/external-video-player/external-video-player-graphql/modal/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/external-video-player/external-video-player-graphql/modal/styles.ts @@ -7,9 +7,8 @@ import { } from '/imports/ui/stylesheets/styled-components/general'; import { colorText, - colorGrayLighter, + colorBorder, colorGray, - colorBlueLight, colorPrimary, } from '/imports/ui/stylesheets/styled-components/palette'; import { fontSizeSmall } from '/imports/ui/stylesheets/styled-components/typography'; @@ -60,7 +59,7 @@ const VideoUrl = styled.div` width: 100%; font-family: inherit; font-weight: inherit; - border: 1px solid ${colorGrayLighter}; + border: 1px solid ${colorBorder}; border-radius: ${borderRadius}; ${({ animations }) => animations && ` @@ -70,7 +69,7 @@ const VideoUrl = styled.div` &:focus { outline: none; border-radius: ${borderSize}; - box-shadow: 0 0 0 ${borderSize} ${colorBlueLight}, inset 0 0 0 1px ${colorPrimary}; + box-shadow: 0 0 0 ${borderSize} ${colorPrimary}, inset 0 0 0 1px ${colorPrimary}; } } diff --git a/bigbluebutton-html5/imports/ui/components/lock-viewers/styles.js b/bigbluebutton-html5/imports/ui/components/lock-viewers/styles.js index 90efe200f144..81993d04637e 100644 --- a/bigbluebutton-html5/imports/ui/components/lock-viewers/styles.js +++ b/bigbluebutton-html5/imports/ui/components/lock-viewers/styles.js @@ -9,7 +9,7 @@ import { mdPaddingX, } from '../../stylesheets/styled-components/general'; import { fontSizeBase, fontSizeSmall } from '/imports/ui/stylesheets/styled-components/typography'; -import { colorGray, colorGrayLabel, colorGrayLighter } from '../../stylesheets/styled-components/palette'; +import { colorBorder, colorGray, colorGrayLabel } from '../../stylesheets/styled-components/palette'; import ModalSimple from '/imports/ui/components/common/modal/simple/component'; import Button from '/imports/ui/components/common/button/component'; @@ -36,7 +36,7 @@ const Description = styled.div` const Form = styled.div` display: flex; flex-flow: column; - border-bottom: 1px solid ${colorGrayLighter}; + border-bottom: 1px solid ${colorBorder}; `; const SubHeader = styled.header` diff --git a/bigbluebutton-html5/imports/ui/components/pads/pads-graphql/content/styles.ts b/bigbluebutton-html5/imports/ui/components/pads/pads-graphql/content/styles.ts index 90e698fdc10a..4d0b0b17541d 100644 --- a/bigbluebutton-html5/imports/ui/components/pads/pads-graphql/content/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/pads/pads-graphql/content/styles.ts @@ -1,7 +1,7 @@ import styled from 'styled-components'; import { colorGray, - colorGrayLightest, + colorBorder, } from '/imports/ui/stylesheets/styled-components/palette'; const Wrapper = styled.div` @@ -43,8 +43,8 @@ white-space: normal; const Iframe = styled.iframe` border-width: 0; width: 100%; - border-top: 1px solid ${colorGrayLightest}; - border-bottom: 1px solid ${colorGrayLightest}; + border-top: 1px solid ${colorBorder}; + border-bottom: 1px solid ${colorBorder}; `; export default { diff --git a/bigbluebutton-html5/imports/ui/components/pads/pads-graphql/styles.ts b/bigbluebutton-html5/imports/ui/components/pads/pads-graphql/styles.ts index 928aa8cb1806..0cc51bbc05d6 100644 --- a/bigbluebutton-html5/imports/ui/components/pads/pads-graphql/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/pads/pads-graphql/styles.ts @@ -5,7 +5,7 @@ import { } from '/imports/ui/stylesheets/styled-components/general'; import { colorGray, - colorGrayLightest, + colorBorder, } from '/imports/ui/stylesheets/styled-components/palette'; import { fontSizeSmall } from '/imports/ui/stylesheets/styled-components/typography'; @@ -38,7 +38,7 @@ const IFrame = styled.iframe` height: auto; overflow: hidden; border-style: none; - border-bottom: 1px solid ${colorGrayLightest}; + border-bottom: 1px solid ${colorBorder}; padding-bottom: 5px; `; diff --git a/bigbluebutton-html5/imports/ui/components/poll/styles.ts b/bigbluebutton-html5/imports/ui/components/poll/styles.ts index af1856079021..99e1a06d8ddc 100644 --- a/bigbluebutton-html5/imports/ui/components/poll/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/poll/styles.ts @@ -19,8 +19,8 @@ import { import { colorText, colorBlueLight, + colorBorder, colorGray, - colorGrayLight, colorGrayLighter, colorGrayLightest, colorDanger, @@ -46,7 +46,6 @@ import { colorSelectedCorrectAnswerBgActive, colorGreen600, colorGreen100, - colorBlueLighter, colorBlueLightest, } from '/imports/ui/stylesheets/styled-components/palette'; import { @@ -79,7 +78,7 @@ const PollOptionInput = styled.input` &:focus { outline: none; border-radius: ${borderSize}; - box-shadow: 0 0 0 ${borderSize} ${colorBlueLight}, inset 0 0 0 1px ${colorPrimary}; + box-shadow: 0 0 0 ${borderSize} ${colorPrimary}, inset 0 0 0 1px ${colorPrimary}; } width: 100%; @@ -88,8 +87,8 @@ const PollOptionInput = styled.input` padding: calc(${smPaddingY} * 2) ${smPaddingX}; border-radius: ${borderRadius}; font-size: ${fontSizeBase}; - border: 1px solid ${colorGrayLighter}; - box-shadow: 0 0 0 1px ${colorGrayLighter}; + border: 1px solid ${colorBorder}; + box-shadow: 0 0 0 1px ${colorBorder}; ${({ isCorrect }) => isCorrect && ` background-color: rgb(240, 253, 244); @@ -132,7 +131,7 @@ const PollQuestionArea = styled.textarea` &:focus { outline: none; border-radius: ${borderSize}; - box-shadow: 0 0 0 ${borderSize} ${colorBlueLight}, inset 0 0 0 1px ${colorPrimary}; + box-shadow: 0 0 0 ${borderSize} ${colorPrimary}, inset 0 0 0 1px ${colorPrimary}; } width: 100%; @@ -141,8 +140,8 @@ const PollQuestionArea = styled.textarea` padding: calc(${smPaddingY} * 2) ${smPaddingX}; border-radius: ${borderRadius}; font-size: ${fontSizeBase}; - border: 1px solid ${colorGrayLighter}; - box-shadow: 0 0 0 1px ${colorGrayLighter}; + border: 1px solid ${colorBorder}; + box-shadow: 0 0 0 1px ${colorBorder}; ${({ hasError }) => hasError && ` border-color: ${colorDanger}; @@ -178,7 +177,7 @@ const ResponseType = styled.div` // @ts-ignore - Button is a JS Component const PollConfigButton = styled(Button)` - border: solid ${colorGrayLight} 1px; + border: solid ${colorBorder} 1px; min-height: ${pollInputHeight}; font-size: ${fontSizeBase}; white-space: pre-wrap; @@ -425,7 +424,7 @@ const AnonymousRow = styled(Row)` const ResultLeft = styled.td` padding: 0 .5rem 0 0; - border-bottom: 1px solid ${colorGrayLightest}; + border-bottom: 1px solid ${colorBorder}; [dir="rtl"] & { padding: 0 0 0 .5rem; @@ -464,14 +463,14 @@ const Left = styled.div` const Center = styled.div` position: relative; flex: 3; - border-left: 1px solid ${colorGrayLighter}; + border-left: 1px solid ${colorBorder}; border-right : none; width: 100%; height: 100%; [dir="rtl"] & { border-left: none; - border-right: 1px solid ${colorGrayLighter}; + border-right: 1px solid ${colorBorder}; } padding: ${smPaddingY}; @@ -711,7 +710,7 @@ const QuizCorrectAnswerCheckbox = styled.input` appearance: none; aspect-ratio: 1; background: var(--backgroundColor, Field); - border: 1px solid var(--borderColor, ${colorGrayLight}); + border: 1px solid var(--borderColor, ${colorBorder}); border-radius: 50%; box-sizing: border-box; font-size: 1em; @@ -756,7 +755,7 @@ const InfoBoxContainer = styled.div` color: ${colorBlueLight}; background-color: ${colorBlueLightest}; - border: 1px solid ${colorBlueLighter}; + border: 1px solid ${colorPrimary}; ${({ isQuiz }) => isQuiz && ` background-color: ${colorInfoBoxQuizBg}; diff --git a/bigbluebutton-html5/imports/ui/components/polling/styles.ts b/bigbluebutton-html5/imports/ui/components/polling/styles.ts index 0a554674a349..33ada47c16c2 100644 --- a/bigbluebutton-html5/imports/ui/components/polling/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/polling/styles.ts @@ -23,9 +23,7 @@ import { } from '/imports/ui/stylesheets/styled-components/typography'; import { colorText, - colorBlueLight, - colorGrayLighter, - colorOffWhite, + colorBorder, colorGrayDark, colorWhite, colorPrimary, @@ -76,7 +74,7 @@ const TypedResponseInput = styled.input` &:focus { outline: none; border-radius: ${borderSize}; - box-shadow: 0 0 0 ${borderSize} ${colorBlueLight}, + box-shadow: 0 0 0 ${borderSize} ${colorPrimary}, inset 0 0 0 1px ${colorPrimary}; } @@ -85,8 +83,8 @@ const TypedResponseInput = styled.input` padding: calc(${smPaddingY} * 2.5) calc(${smPaddingX} * 1.25); border-radius: ${borderRadius}; font-size: ${fontSizeBase}; - border: 1px solid ${colorGrayLighter}; - box-shadow: 0 0 0 1px ${colorGrayLighter}; + border: 1px solid ${colorBorder}; + box-shadow: 0 0 0 1px ${colorBorder}; margin-bottom: 1rem; `; @@ -158,7 +156,7 @@ const PollingContainer = styled.aside<{ autoWidth: boolean }>` position: absolute; z-index: ${pollIndex}; - border: 1px solid ${colorOffWhite}; + border: 1px solid ${colorBorder}; border-radius: ${borderRadius}; box-shadow: ${colorGrayDark} 0px 0px ${lgPaddingY}; align-items: center; diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.js b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.js index 01fb2e78ce57..627afbe81c3a 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.js +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.js @@ -1,8 +1,8 @@ import styled from 'styled-components'; import QuickPollDropdownContainer from '/imports/ui/components/actions-bar/quick-poll-dropdown/container'; import { + colorBorder, colorOffWhite, - colorBlueLightest, toolbarButtonColor, colorDanger, colorWhite, @@ -25,7 +25,7 @@ const PresentationToolbarWrapper = styled.div` align-self: center; z-index: 1; background-color: ${colorOffWhite}; - border-top: 1px solid ${colorBlueLightest}; + border-top: 1px solid ${colorBorder}; min-width: fit-content; width: 100%; bottom: 0px; diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/styles.js b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/styles.js index d2019d888441..aa3ff474b899 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/styles.js +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/styles.js @@ -29,6 +29,7 @@ import { fontSizeLarger, } from '/imports/ui/stylesheets/styled-components/typography'; import { + colorBorder, colorGrayLight, colorGrayDark, colorPrimary, @@ -125,7 +126,7 @@ const Table = styled.table` } > tr { - border-bottom: 1px solid ${colorGrayLight}; + border-bottom: 1px solid ${colorBorder}; &:last-child { border-bottom: 0; @@ -406,7 +407,7 @@ const ModalHeader = styled.div` display: flex; flex-direction: row; justify-content: space-between; - border-bottom:${borderSize} solid ${colorGrayLighter}; + border-bottom:${borderSize} solid ${colorBorder}; margin-bottom: 2rem; padding: ${mdPaddingX} 0; diff --git a/bigbluebutton-html5/imports/ui/components/settings/submenus/application/styles.js b/bigbluebutton-html5/imports/ui/components/settings/submenus/application/styles.js index df286bfd0f5e..b72f10c99157 100644 --- a/bigbluebutton-html5/imports/ui/components/settings/submenus/application/styles.js +++ b/bigbluebutton-html5/imports/ui/components/settings/submenus/application/styles.js @@ -1,9 +1,9 @@ import styled from 'styled-components'; import { + colorBorder, colorGrayLabel, colorPrimary, colorWhite, - colorGrayLighter, } from '/imports/ui/stylesheets/styled-components/palette'; import { borderSize, borderSizeLarge } from '/imports/ui/stylesheets/styled-components/general'; import SpinnerStyles from '/imports/ui/components/common/loading-screen/styles'; @@ -37,7 +37,7 @@ const Bounce2 = styled(SpinnerStyles.Bounce2)``; const Separator = styled.hr` margin: 2.5rem 0; - border: 1px solid ${colorGrayLighter}; + border: 1px solid ${colorBorder}; opacity: 0.25; `; @@ -67,7 +67,7 @@ const LocalesDropdownSelect = styled.div` background-color: ${colorWhite}; border: ${borderSize} solid ${colorWhite}; border-radius: ${borderSize}; - border-bottom: 0.1rem solid ${colorGrayLighter}; + border-bottom: 0.1rem solid ${colorBorder}; color: ${colorGrayLabel}; width: 100%; height: 1.75rem; diff --git a/bigbluebutton-html5/imports/ui/components/settings/submenus/styles.js b/bigbluebutton-html5/imports/ui/components/settings/submenus/styles.js index 82dcabf1ab6b..527bc54b5f13 100644 --- a/bigbluebutton-html5/imports/ui/components/settings/submenus/styles.js +++ b/bigbluebutton-html5/imports/ui/components/settings/submenus/styles.js @@ -1,10 +1,10 @@ import styled from 'styled-components'; import { + colorBorder, colorGrayDark, colorGrayLabel, colorPrimary, colorWhite, - colorGrayLighter, } from '/imports/ui/stylesheets/styled-components/palette'; import { borderSize, borderSizeLarge } from '/imports/ui/stylesheets/styled-components/general'; @@ -90,7 +90,7 @@ const Select = styled.select` background-color: ${colorWhite}; border: ${borderSize} solid ${colorWhite}; border-radius: ${borderSize}; - border-bottom: 0.1rem solid ${colorGrayLighter}; + border-bottom: 0.1rem solid ${colorBorder}; color: ${colorGrayLabel}; width: 100%; height: 1.75rem; diff --git a/bigbluebutton-html5/imports/ui/components/text-input/styles.js b/bigbluebutton-html5/imports/ui/components/text-input/styles.js index 92f4fd0fa658..8cee10c8c048 100644 --- a/bigbluebutton-html5/imports/ui/components/text-input/styles.js +++ b/bigbluebutton-html5/imports/ui/components/text-input/styles.js @@ -7,8 +7,7 @@ import { } from '/imports/ui/stylesheets/styled-components/general'; import { colorText, - colorGrayLighter, - colorBlueLight, + colorBorder, colorPrimary, } from '/imports/ui/stylesheets/styled-components/palette'; import { fontSizeBase } from '/imports/ui/stylesheets/styled-components/typography'; @@ -34,8 +33,8 @@ const TextArea = styled(TextareaAutosize)` font-size: ${fontSizeBase}; min-height: 2.5rem; max-height: 10rem; - border: 1px solid ${colorGrayLighter}; - box-shadow: 0 0 0 1px ${colorGrayLighter}; + border: 1px solid ${colorBorder}; + box-shadow: 0 0 0 1px ${colorBorder}; &:hover { outline: transparent; @@ -53,7 +52,7 @@ const TextArea = styled(TextareaAutosize)` &:focus { outline: none; border-radius: ${borderSize}; - box-shadow: 0 0 0 ${borderSize} ${colorBlueLight}, inset 0 0 0 1px ${colorPrimary}; + box-shadow: 0 0 0 ${borderSize} ${colorPrimary}, inset 0 0 0 1px ${colorPrimary}; } `; diff --git a/bigbluebutton-html5/imports/ui/components/timer/panel/styles.ts b/bigbluebutton-html5/imports/ui/components/timer/panel/styles.ts index f03eb9b111df..af0146699999 100644 --- a/bigbluebutton-html5/imports/ui/components/timer/panel/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/timer/panel/styles.ts @@ -7,11 +7,9 @@ import { borderRadius, } from '../../../stylesheets/styled-components/general'; import { + colorBorder, colorGrayDark, - colorGrayLighter, - colorGrayLightest, colorGray, - colorBlueLight, colorWhite, colorPrimary, } from '../../../stylesheets/styled-components/palette'; @@ -87,8 +85,8 @@ const TimerContent = styled.div` `; const TimerCurrent = styled.span` - border-bottom: 1px solid ${colorGrayLightest}; - border-top: 1px solid ${colorGrayLightest}; + border-bottom: 1px solid ${colorBorder}; + border-top: 1px solid ${colorBorder}; display: flex; font-size: xxx-large; justify-content: center; @@ -211,7 +209,7 @@ const TimerControlButton = styled(Button)` const TimerInput = styled.input` flex: 1; - border: 1px solid ${colorGrayLighter}; + border: 1px solid ${colorBorder}; width: 50%; text-align: center; padding: .25rem; @@ -226,7 +224,7 @@ const TimerInput = styled.input` &:focus { border-radius: ${borderSize}; - box-shadow: 0 0 0 ${borderSize} ${colorBlueLight}, inset 0 0 0 1px ${colorPrimary}; + box-shadow: 0 0 0 ${borderSize} ${colorPrimary}, inset 0 0 0 1px ${colorPrimary}; } &:disabled, diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/styles.js b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/styles.js index 8366163d190c..76b766c5647b 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/styles.js +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/styles.js @@ -10,13 +10,13 @@ import { mdPaddingX, } from '/imports/ui/stylesheets/styled-components/general'; import { + colorBorder, colorPrimary, userListBg, colorWhite, colorOffWhite, colorGrayDark, colorGrayLight, - colorGrayLighter, unreadMessagesBg, colorGrayLightest, } from '/imports/ui/stylesheets/styled-components/palette'; @@ -161,7 +161,7 @@ const Separator = styled.hr` margin: 1rem auto; width: 2.2rem; border: 0; - border-top: 1px solid ${colorGrayLighter}; + border-top: 1px solid ${colorBorder}; `; export default { diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-messages/chat-list/styles.ts b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-messages/chat-list/styles.ts index 3395dbf0ef08..11a7a5809e39 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-messages/chat-list/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-messages/chat-list/styles.ts @@ -1,7 +1,7 @@ import styled from 'styled-components'; import { colorGray, - colorGrayLighter, + colorBorder, colorPrimary, userListBg, } from '/imports/ui/stylesheets/styled-components/palette'; @@ -34,7 +34,7 @@ const Separator = styled.hr` margin: 1rem auto; width: 2.2rem; border: 0; - border-top: 1px solid ${colorGrayLighter}; + border-top: 1px solid ${colorBorder}; `; const MessagesTitle = styled.h2` diff --git a/bigbluebutton-html5/imports/ui/components/video-preview/styles.js b/bigbluebutton-html5/imports/ui/components/video-preview/styles.js index e00b64fce098..582034810d0a 100644 --- a/bigbluebutton-html5/imports/ui/components/video-preview/styles.js +++ b/bigbluebutton-html5/imports/ui/components/video-preview/styles.js @@ -8,6 +8,7 @@ import { lgPaddingY, } from '/imports/ui/stylesheets/styled-components/general'; import { + colorBorder, colorGrayLabel, colorWhite, colorBlack, @@ -122,7 +123,7 @@ const Select = styled.select` background-color: ${colorWhite}; border: ${borderSize} solid ${colorWhite}; border-radius: ${borderSize}; - border-bottom: 0.1rem solid ${colorGrayLighter}; + border-bottom: 0.1rem solid ${colorBorder}; color: ${colorGrayLabel}; width: 100%; height: 1.75rem; diff --git a/bigbluebutton-html5/imports/ui/components/video-preview/virtual-background/styles.js b/bigbluebutton-html5/imports/ui/components/video-preview/virtual-background/styles.js index 74d009ba9397..025c0f5de0a7 100644 --- a/bigbluebutton-html5/imports/ui/components/video-preview/virtual-background/styles.js +++ b/bigbluebutton-html5/imports/ui/components/video-preview/virtual-background/styles.js @@ -6,11 +6,11 @@ import { smPaddingY, } from '/imports/ui/stylesheets/styled-components/general'; import { + colorBorder, userThumbnailBorder, btnPrimaryBorder, btnDefaultColor, colorGrayLabel, - colorGrayLighter, colorPrimary, colorWhite, } from '/imports/ui/stylesheets/styled-components/palette'; @@ -99,7 +99,7 @@ const Select = styled.select` background-color: ${colorWhite}; border: ${borderSize} solid ${colorWhite}; border-radius: ${borderSize}; - border-bottom: 0.1rem solid ${colorGrayLighter}; + border-bottom: 0.1rem solid ${colorBorder}; color: ${colorGrayLabel}; width: 100%; height: 1.75rem; diff --git a/bigbluebutton-html5/imports/ui/components/waiting-users/waiting-users-graphql/styles.ts b/bigbluebutton-html5/imports/ui/components/waiting-users/waiting-users-graphql/styles.ts index 92ca3625747f..151212bfadbb 100644 --- a/bigbluebutton-html5/imports/ui/components/waiting-users/waiting-users-graphql/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/waiting-users/waiting-users-graphql/styles.ts @@ -3,9 +3,9 @@ import { colorPrimary, listItemBgHover, itemFocusBorder, + colorBorder, colorGray, colorWhite, - colorGrayLightest, colorOffWhite, userListBg, colorSuccess, @@ -216,7 +216,7 @@ const Panel = styled.div` `; const LobbyMessage = styled.div` - border-bottom: 1px solid ${colorGrayLightest}; + border-bottom: 1px solid ${colorBorder}; margin: 2px 2px 0 2px; & > p { @@ -229,7 +229,7 @@ const LobbyMessage = styled.div` `; const PrivateLobbyMessage = styled.div` - border-bottom: 1px solid ${colorGrayLightest}; + border-bottom: 1px solid ${colorBorder}; display: none; & > p { background-color: ${colorOffWhite}; diff --git a/bigbluebutton-html5/imports/ui/stylesheets/styled-components/palette.js b/bigbluebutton-html5/imports/ui/stylesheets/styled-components/palette.js index 973664d24f44..8e3964768642 100644 --- a/bigbluebutton-html5/imports/ui/stylesheets/styled-components/palette.js +++ b/bigbluebutton-html5/imports/ui/stylesheets/styled-components/palette.js @@ -8,6 +8,7 @@ const colorGrayDark = 'var(--color-gray-dark, #06172A)'; const colorGrayLight = 'var(--color-gray-light, #8B9AA8)'; const colorGrayLighter = 'var(--color-gray-lighter, #A7B3BD)'; const colorGrayLightest = 'var(--color-gray-lightest, #D4D9DF)'; +const colorBorder = 'var(--color-border, #7E8C99)'; const colorBlueLight = 'var(--color-blue-light, #54a1f3)'; const colorBlueLighter = 'var(--color-blue-lighter, #92BCEA)'; @@ -38,7 +39,7 @@ const colorLink = `var(--color-link, ${colorPrimary})`; const listItemBgHover = 'var(--list-item-bg-hover, #DCE4ED)'; const colorTipBg = 'var(--color-tip-bg, #333333)'; -const itemFocusBorder = `var(--item-focus-border, ${colorBlueLighter})`; +const itemFocusBorder = `var(--item-focus-border, ${colorPrimary})`; const btnDefaultColor = `var(--btn-default-color, ${colorGray})`; const btnDefaultBg = `var(--btn-default-bg, ${colorWhite})`; @@ -82,21 +83,21 @@ const btnMutedBg = `var(--btn-muted-bg, ${colorMutedBackground})`; const toolbarButtonColor = `var(--toolbar-button-color, ${btnDefaultColor})`; const toolbarButtonColorDisabled = `var(--toolbar-button-color, ${colorGrayLight})`; -const userThumbnailBorder = `var(--user-thumbnail-border, ${colorGrayLight})`; +const userThumbnailBorder = `var(--user-thumbnail-border, ${colorBorder})`; const loaderBg = `var(--loader-bg, ${colorGrayDark})`; const loaderBullet = `var(--loader-bullet, ${colorWhite})`; const systemMessageBackgroundColor = 'var(--system-message-background-color, #F9FBFC)'; -const systemMessageBorderColor = 'var(--system-message-border-color, #C5CDD4)'; +const systemMessageBorderColor = `var(--system-message-border-color, ${colorBorder})`; const systemMessageFontColor = `var(--system-message-font-color, ${colorGrayDark})`; const highlightedMessageBackgroundColor = 'var(--system-message-background-color, #fef9f1)'; -const highlightedMessageBorderColor = 'var(--system-message-border-color, #B5D3F7)'; +const highlightedMessageBorderColor = `var(--highlighted-message-border-color, ${colorBorder})`; const emphasizedMessageBackgroundColor = 'var(--emphasized-message-background-color, #E9F1F9)'; const colorHeading = `var(--color-heading, ${colorGrayDark})`; const palettePlaceholderText = 'var(--palette-placeholder-text, #787675)'; const pollAnnotationGray = 'var(--poll-annotation-gray, #333333)'; -const toolbarButtonBorderColor = `var(--toolbar-button-border-color, ${colorGrayLighter})`; +const toolbarButtonBorderColor = `var(--toolbar-button-border-color, ${colorBorder})`; const toolbarListColor = `var(--toolbar-list-color, ${colorGray})`; const toolbarButtonBg = `var(--toolbar-button-bg, ${btnDefaultBg})`; const toolbarListBg = 'var(--toolbar-list-bg, #DDD)'; @@ -105,7 +106,7 @@ const colorContentBackground = 'var(--color-content-background, #1B2A3A)'; const dropdownBg = `var(--dropdown-bg, ${colorWhite})`; -const pollStatsBorderColor = 'var(--poll-stats-border-color, #D4D9DF)'; +const pollStatsBorderColor = `var(--poll-stats-border-color, ${colorBorder})`; const pollBlue = `var(--poll-blue, ${colorPrimary})`; const toastDefaultColor = `var(--toast-default-color, ${colorWhite})`; @@ -137,7 +138,7 @@ const darkCyanLime = 'var(--dark-cyan-lime, #16A34A)'; const colorInfoBoxQuizText = 'var(--color-info-box-quiz-text, #15803D)'; const colorInfoBoxQuizBg = 'var(--color-info-box-quiz-bg, #F0FDF4)'; -const colorInfoBoxQuizBorder = 'var(--color-info-box-quiz-border, #BBF7D0)'; +const colorInfoBoxQuizBorder = `var(--color-info-box-quiz-border, ${colorSuccess})`; const colorSelectedCorrectAnswerText = 'var(--color-selected-correct-answer-text, #A16207)'; const colorSelectedCorrectAnswerBg = 'var(--color-selected-correct-answer-bg, #FEF9C3)'; @@ -157,6 +158,7 @@ export { colorGrayLight, colorGrayLighter, colorGrayLightest, + colorBorder, colorTransparent, colorBlueLight, colorBlueLighter, From 11525e28e95d5c59d1d832a7a94cca9e9b7efc56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Wed, 24 Jun 2026 15:52:34 -0300 Subject: [PATCH 030/121] add non-text constrast test --- .../accessibility/nonTextContrast.spec.ts | 21 + .../accessibility/nonTextContrast.ts | 444 ++++++++++++++++++ 2 files changed, 465 insertions(+) create mode 100644 bigbluebutton-tests/playwright/accessibility/nonTextContrast.spec.ts create mode 100644 bigbluebutton-tests/playwright/accessibility/nonTextContrast.ts diff --git a/bigbluebutton-tests/playwright/accessibility/nonTextContrast.spec.ts b/bigbluebutton-tests/playwright/accessibility/nonTextContrast.spec.ts new file mode 100644 index 000000000000..bbc195b0b8a1 --- /dev/null +++ b/bigbluebutton-tests/playwright/accessibility/nonTextContrast.spec.ts @@ -0,0 +1,21 @@ +import { expect } from '@playwright/test'; + +import { Page } from '../core/page'; +import { test } from '../core/setup/fixtures'; +import { findNonTextContrastViolations, formatNonTextContrastViolations } from './nonTextContrast'; + +test.describe.parallel('Accessibility', { tag: '@ci' }, () => { + test('visible non-text boundaries have sufficient contrast', async ({ browser, page }, testInfo) => { + const meetingPage = new Page(browser, page, testInfo); + await meetingPage.init(true, { testInfo }); + + const violations = await findNonTextContrastViolations(meetingPage.page); + + await testInfo.attach('non-text-contrast-violations.json', { + body: JSON.stringify(violations, null, 2), + contentType: 'application/json', + }); + + expect(violations, formatNonTextContrastViolations(violations)).toEqual([]); + }); +}); diff --git a/bigbluebutton-tests/playwright/accessibility/nonTextContrast.ts b/bigbluebutton-tests/playwright/accessibility/nonTextContrast.ts new file mode 100644 index 000000000000..cfede66f70eb --- /dev/null +++ b/bigbluebutton-tests/playwright/accessibility/nonTextContrast.ts @@ -0,0 +1,444 @@ +import { Page as PlaywrightPage } from '@playwright/test'; + +const MIN_NON_TEXT_CONTRAST_RATIO = 3; + +export interface NonTextContrastViolation { + selector: string; + element: string; + elementName: string; + ancestorContext: string; + domPath: string; + property: string; + color: string; + backgroundColor: string; + ratio: number; + requiredRatio: number; + cssRule?: string; + cssDeclaration?: string; + stylesheet?: string; + text: string; +} + +export interface NonTextContrastOptions { + ignoredSelectors?: string[]; + minRatio?: number; +} + +export function formatNonTextContrastViolations(violations: NonTextContrastViolation[]): string { + const details = violations + .slice(0, 20) + .map((violation) => + [ + `${violation.elementName} (${violation.selector})`, + `${violation.property}: ${violation.color}`, + `background: ${violation.backgroundColor}`, + `ratio: ${violation.ratio}:1`, + violation.ancestorContext ? `context: ${violation.ancestorContext}` : undefined, + violation.cssRule ? `rule: ${violation.cssRule}` : undefined, + violation.cssDeclaration ? `declaration: ${violation.cssDeclaration}` : undefined, + violation.stylesheet ? `stylesheet: ${violation.stylesheet}` : undefined, + ] + .filter(Boolean) + .join(' | '), + ) + .join('\n'); + + const suffix = violations.length > 20 ? `\n...and ${violations.length - 20} more` : ''; + return `Found ${violations.length} non-text contrast issue(s):\n${details}${suffix}`; +} + +export async function findNonTextContrastViolations( + page: PlaywrightPage, + options: NonTextContrastOptions = {}, +): Promise { + const { ignoredSelectors = [], minRatio = MIN_NON_TEXT_CONTRAST_RATIO } = options; + return page.evaluate( + ({ ignoredSelectors: ignored, minRatio: minimumRatio }) => { + type Rgba = { + r: number; + g: number; + b: number; + a: number; + }; + + type Candidate = { + property: string; + color: string; + sourceProperties: string[]; + }; + + type CssSource = { + cssRule?: string; + cssDeclaration?: string; + stylesheet?: string; + }; + + type Violation = { + selector: string; + element: string; + elementName: string; + ancestorContext: string; + domPath: string; + property: string; + color: string; + backgroundColor: string; + ratio: number; + requiredRatio: number; + cssRule?: string; + cssDeclaration?: string; + stylesheet?: string; + text: string; + }; + + const TRANSPARENT_ALPHA_THRESHOLD = 0.01; + const MIN_VISIBLE_SIZE = 1; + + const parseCssColor = (color: string): Rgba | null => { + const match = color.match(/^rgba?\((.*)\)$/); + if (!match) return null; + + const parts = match[1] + .replace('/', ' ') + .split(/[,\s]+/) + .filter(Boolean); + + if (parts.length < 3) return null; + + const readChannel = (value: string) => { + if (value.endsWith('%')) return (parseFloat(value) / 100) * 255; + return parseFloat(value); + }; + + const alpha = parts[3] === undefined ? 1 : parseFloat(parts[3]); + return { + r: readChannel(parts[0]), + g: readChannel(parts[1]), + b: readChannel(parts[2]), + a: Number.isNaN(alpha) ? 1 : alpha, + }; + }; + + const isTransparent = (color: Rgba | null) => !color || color.a <= TRANSPARENT_ALPHA_THRESHOLD; + + const composite = (foreground: Rgba, background: Rgba): Rgba => { + const alpha = foreground.a + background.a * (1 - foreground.a); + if (alpha <= TRANSPARENT_ALPHA_THRESHOLD) return { r: 255, g: 255, b: 255, a: 1 }; + + return { + r: (foreground.r * foreground.a + background.r * background.a * (1 - foreground.a)) / alpha, + g: (foreground.g * foreground.a + background.g * background.a * (1 - foreground.a)) / alpha, + b: (foreground.b * foreground.a + background.b * background.a * (1 - foreground.a)) / alpha, + a: alpha, + }; + }; + + const normalize = (value: number) => { + const channel = value / 255; + return channel <= 0.03928 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4; + }; + + const luminance = (color: Rgba) => + 0.2126 * normalize(color.r) + 0.7152 * normalize(color.g) + 0.0722 * normalize(color.b); + + const contrastRatio = (first: Rgba, second: Rgba) => { + const firstLuminance = luminance(first); + const secondLuminance = luminance(second); + const lighter = Math.max(firstLuminance, secondLuminance); + const darker = Math.min(firstLuminance, secondLuminance); + return (lighter + 0.05) / (darker + 0.05); + }; + + const colorToRgbString = (color: Rgba) => + `rgb(${Math.round(color.r)}, ${Math.round(color.g)}, ${Math.round(color.b)})`; + + const effectiveBackground = (element: Element | null): Rgba => { + const base = { r: 255, g: 255, b: 255, a: 1 }; + if (!element) return base; + + const chain: Element[] = []; + let current: Element | null = element; + while (current) { + chain.push(current); + current = current.parentElement; + } + + return chain.reverse().reduce((background, item) => { + const color = parseCssColor(window.getComputedStyle(item).backgroundColor); + if (isTransparent(color)) return background; + return composite(color as Rgba, background); + }, base); + }; + + const cssEscape = (value: string) => { + if (window.CSS?.escape) return window.CSS.escape(value); + return value.replace(/[^a-zA-Z0-9_-]/g, '\\$&'); + }; + + const selectorFor = (element: Element) => { + const dataTest = element.getAttribute('data-test'); + if (dataTest) return `${element.tagName.toLowerCase()}[data-test="${dataTest}"]`; + if (element.id) return `${element.tagName.toLowerCase()}#${cssEscape(element.id)}`; + + const parent = element.parentElement; + const tagName = element.tagName.toLowerCase(); + if (!parent) return tagName; + + const siblings = Array.from(parent.children).filter((child) => child.tagName === element.tagName); + const index = siblings.indexOf(element) + 1; + return `${tagName}:nth-of-type(${index})`; + }; + + const domPathFor = (element: Element) => { + const path: string[] = []; + let current: Element | null = element; + + while (current && current !== document.body) { + path.unshift(selectorFor(current)); + current = current.parentElement; + } + + return `body > ${path.join(' > ')}`; + }; + + const textFor = (element: Element) => (element.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 80); + + const attributeNameFor = (element: Element) => { + const labelledBy = element.getAttribute('aria-labelledby'); + if (labelledBy) { + const name = labelledBy + .split(/\s+/) + .map((id) => document.getElementById(id)?.textContent?.trim()) + .filter(Boolean) + .join(' '); + if (name) return name; + } + + return ( + element.getAttribute('aria-label') || + element.getAttribute('title') || + element.getAttribute('alt') || + element.getAttribute('placeholder') || + element.getAttribute('name') || + '' + ); + }; + + const labelFor = (element: Element) => { + const dataTest = element.getAttribute('data-test'); + if (dataTest) return `[data-test="${dataTest}"]`; + if (element.id) return `#${element.id}`; + if (element.className && typeof element.className === 'string') + return `.${element.className.trim().split(/\s+/)[0]}`; + return element.tagName.toLowerCase(); + }; + + const elementNameFor = (element: Element) => { + const parts = [element.tagName.toLowerCase()]; + const role = element.getAttribute('role'); + const dataTest = element.getAttribute('data-test'); + const { id } = element; + const name = attributeNameFor(element) || textFor(element); + + if (role) parts.push(`role="${role}"`); + if (dataTest) parts.push(`data-test="${dataTest}"`); + if (id) parts.push(`id="${id}"`); + if (name) parts.push(`name="${name.slice(0, 80)}"`); + + return parts.join(' '); + }; + + const contextLabelFor = (element: Element) => { + const dataTest = element.getAttribute('data-test'); + const role = element.getAttribute('role'); + const ariaLabel = element.getAttribute('aria-label'); + const { id } = element; + const tagName = element.tagName.toLowerCase(); + + if (dataTest) return `${tagName}[data-test="${dataTest}"]`; + if (id) return `${tagName}#${id}`; + if (role) return `${tagName}[role="${role}"]`; + if (ariaLabel) return `${tagName}[aria-label="${ariaLabel}"]`; + return ''; + }; + + const ancestorContextFor = (element: Element) => { + const context: string[] = []; + let current = element.parentElement; + + while (current && current !== document.body && context.length < 5) { + const label = contextLabelFor(current); + if (label) context.unshift(label); + current = current.parentElement; + } + + return context.join(' > '); + }; + + const stylesheetNameFor = (styleSheet: CSSStyleSheet) => { + if (styleSheet.href) return styleSheet.href; + + const { ownerNode } = styleSheet; + if (ownerNode instanceof Element) { + const dataStyled = ownerNode.getAttribute('data-styled'); + const { id } = ownerNode; + const nodeName = ownerNode.nodeName.toLowerCase(); + if (dataStyled) return `${nodeName}[data-styled="${dataStyled}"]`; + if (id) return `${nodeName}#${id}`; + return nodeName; + } + + return 'inline stylesheet'; + }; + + const findCssSourceInRules = ( + element: Element, + candidate: Candidate, + rules: CSSRuleList, + styleSheet: CSSStyleSheet, + ): CssSource | null => { + for (let index = rules.length - 1; index >= 0; index--) { + const rule = rules[index]; + let source: CssSource | null = null; + + if ('cssRules' in rule) { + source = findCssSourceInRules(element, candidate, (rule as CSSGroupingRule).cssRules, styleSheet); + } + + if (!source && rule instanceof CSSStyleRule) { + let matches = false; + try { + matches = element.matches(rule.selectorText); + } catch { + matches = false; + } + + const hasSourceProperty = + matches && candidate.sourceProperties.some((property) => rule.style.getPropertyValue(property)); + + if (hasSourceProperty) { + const property = candidate.sourceProperties.find((sourceProperty) => + rule.style.getPropertyValue(sourceProperty), + ); + const value = property ? rule.style.getPropertyValue(property).trim() : ''; + source = { + cssRule: rule.selectorText, + cssDeclaration: property && value ? `${property}: ${value}` : undefined, + stylesheet: stylesheetNameFor(styleSheet), + }; + } + } + + if (source) return source; + } + + return null; + }; + + const findCssSource = (element: Element, candidate: Candidate): CssSource => { + const styleAttribute = element.getAttribute('style') || ''; + const hasInlineSource = candidate.sourceProperties.some((property) => styleAttribute.includes(property)); + if (hasInlineSource) return { cssRule: 'style attribute', stylesheet: 'inline style' }; + + for (let index = document.styleSheets.length - 1; index >= 0; index--) { + const styleSheet = document.styleSheets[index] as CSSStyleSheet; + try { + const source = findCssSourceInRules(element, candidate, styleSheet.cssRules, styleSheet); + if (source) return source; + } catch { + // Cross-origin stylesheets can block cssRules access. + } + } + + return {}; + }; + + const isVisible = (element: Element) => { + const style = window.getComputedStyle(element); + const rect = element.getBoundingClientRect(); + return ( + style.display !== 'none' && + style.visibility !== 'hidden' && + parseFloat(style.opacity) > 0 && + rect.width >= MIN_VISIBLE_SIZE && + rect.height >= MIN_VISIBLE_SIZE + ); + }; + + const isIgnored = (element: Element) => + ignored.some((selector) => { + try { + return element.matches(selector) || Boolean(element.closest(selector)); + } catch { + return false; + } + }); + + const borderCandidates = (style: CSSStyleDeclaration): Candidate[] => { + const sides = ['top', 'right', 'bottom', 'left'] as const; + return sides.flatMap((side) => { + const width = parseFloat(style.getPropertyValue(`border-${side}-width`)); + const borderStyle = style.getPropertyValue(`border-${side}-style`); + const color = style.getPropertyValue(`border-${side}-color`); + if (width < 1 || borderStyle === 'none' || borderStyle === 'hidden') return []; + return [ + { + property: `border-${side}-color`, + color, + sourceProperties: [`border-${side}-color`, `border-${side}`, 'border-color', 'border'], + }, + ]; + }); + }; + + const outlineCandidates = (style: CSSStyleDeclaration): Candidate[] => { + const width = parseFloat(style.outlineWidth); + if (width < 1 || style.outlineStyle === 'none' || style.outlineStyle === 'hidden') return []; + return [ + { property: 'outline-color', color: style.outlineColor, sourceProperties: ['outline-color', 'outline'] }, + ]; + }; + + const elements = Array.from(document.body.querySelectorAll('*')); + const violations: Violation[] = []; + + elements.forEach((element) => { + if (!isVisible(element) || isIgnored(element)) return; + if (element.closest('svg, canvas, video, iframe')) return; + + const style = window.getComputedStyle(element); + const background = effectiveBackground(element.parentElement); + const candidates = [...borderCandidates(style), ...outlineCandidates(style)]; + + candidates.forEach((candidate) => { + const parsedColor = parseCssColor(candidate.color); + if (isTransparent(parsedColor)) return; + + const color = composite(parsedColor as Rgba, background); + const ratio = contrastRatio(color, background); + if (ratio >= minimumRatio) return; + const source = findCssSource(element, candidate); + + violations.push({ + selector: selectorFor(element), + element: labelFor(element), + elementName: elementNameFor(element), + ancestorContext: ancestorContextFor(element), + domPath: domPathFor(element), + property: candidate.property, + color: colorToRgbString(color), + backgroundColor: colorToRgbString(background), + ratio: Number(ratio.toFixed(2)), + requiredRatio: minimumRatio, + cssRule: source.cssRule, + cssDeclaration: source.cssDeclaration, + stylesheet: source.stylesheet, + text: textFor(element), + }); + }); + }); + + return violations; + }, + { ignoredSelectors, minRatio }, + ); +} From d4511535f4e3f09c95cc2ee3b60489e924083c94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Wed, 24 Jun 2026 15:55:09 -0300 Subject: [PATCH 031/121] restore distinct hover border for chat reactions --- .../chat-message/message-reactions/reaction-item/styles.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-reactions/reaction-item/styles.tsx b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-reactions/reaction-item/styles.tsx index 9a0d96c9e831..58e853b99a73 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-reactions/reaction-item/styles.tsx +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-reactions/reaction-item/styles.tsx @@ -1,5 +1,5 @@ import styled from 'styled-components'; -import { colorBorder, colorOffWhite } from '/imports/ui/stylesheets/styled-components/palette'; +import { colorBorder, colorOffWhite, colorPrimary } from '/imports/ui/stylesheets/styled-components/palette'; const EmojiWrapper = styled.button<{ highlighted: boolean }>` background: none; @@ -26,7 +26,7 @@ const EmojiWrapper = styled.button<{ highlighted: boolean }>` } &:hover { - border: 1px solid ${colorBorder}; + border: 1px solid ${colorPrimary}; } `; From ecde87da5271273c5720de034dc74c2de454aaa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Wed, 24 Jun 2026 16:11:28 -0300 Subject: [PATCH 032/121] check border contrast against inner and outer backgrounds --- .../accessibility/nonTextContrast.ts | 70 +++++++++++++------ 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/bigbluebutton-tests/playwright/accessibility/nonTextContrast.ts b/bigbluebutton-tests/playwright/accessibility/nonTextContrast.ts index cfede66f70eb..36ac0135646b 100644 --- a/bigbluebutton-tests/playwright/accessibility/nonTextContrast.ts +++ b/bigbluebutton-tests/playwright/accessibility/nonTextContrast.ts @@ -11,6 +11,7 @@ export interface NonTextContrastViolation { property: string; color: string; backgroundColor: string; + adjacentTo: string; ratio: number; requiredRatio: number; cssRule?: string; @@ -31,7 +32,7 @@ export function formatNonTextContrastViolations(violations: NonTextContrastViola [ `${violation.elementName} (${violation.selector})`, `${violation.property}: ${violation.color}`, - `background: ${violation.backgroundColor}`, + `${violation.adjacentTo}: ${violation.backgroundColor}`, `ratio: ${violation.ratio}:1`, violation.ancestorContext ? `context: ${violation.ancestorContext}` : undefined, violation.cssRule ? `rule: ${violation.cssRule}` : undefined, @@ -64,6 +65,7 @@ export async function findNonTextContrastViolations( type Candidate = { property: string; color: string; + isBorder: boolean; sourceProperties: string[]; }; @@ -82,6 +84,7 @@ export async function findNonTextContrastViolations( property: string; color: string; backgroundColor: string; + adjacentTo: string; ratio: number; requiredRatio: number; cssRule?: string; @@ -151,6 +154,12 @@ export async function findNonTextContrastViolations( const colorToRgbString = (color: Rgba) => `rgb(${Math.round(color.r)}, ${Math.round(color.g)}, ${Math.round(color.b)})`; + const isSameColor = (first: Rgba, second: Rgba) => + Math.round(first.r) === Math.round(second.r) && + Math.round(first.g) === Math.round(second.g) && + Math.round(first.b) === Math.round(second.b) && + Math.round(first.a * 100) === Math.round(second.a * 100); + const effectiveBackground = (element: Element | null): Rgba => { const base = { r: 255, g: 255, b: 255, a: 1 }; if (!element) return base; @@ -384,6 +393,7 @@ export async function findNonTextContrastViolations( { property: `border-${side}-color`, color, + isBorder: true, sourceProperties: [`border-${side}-color`, `border-${side}`, 'border-color', 'border'], }, ]; @@ -394,7 +404,12 @@ export async function findNonTextContrastViolations( const width = parseFloat(style.outlineWidth); if (width < 1 || style.outlineStyle === 'none' || style.outlineStyle === 'hidden') return []; return [ - { property: 'outline-color', color: style.outlineColor, sourceProperties: ['outline-color', 'outline'] }, + { + property: 'outline-color', + color: style.outlineColor, + isBorder: false, + sourceProperties: ['outline-color', 'outline'], + }, ]; }; @@ -406,33 +421,42 @@ export async function findNonTextContrastViolations( if (element.closest('svg, canvas, video, iframe')) return; const style = window.getComputedStyle(element); - const background = effectiveBackground(element.parentElement); + const outerBackground = effectiveBackground(element.parentElement); + const innerBackground = effectiveBackground(element); const candidates = [...borderCandidates(style), ...outlineCandidates(style)]; candidates.forEach((candidate) => { const parsedColor = parseCssColor(candidate.color); if (isTransparent(parsedColor)) return; - const color = composite(parsedColor as Rgba, background); - const ratio = contrastRatio(color, background); - if (ratio >= minimumRatio) return; - const source = findCssSource(element, candidate); - - violations.push({ - selector: selectorFor(element), - element: labelFor(element), - elementName: elementNameFor(element), - ancestorContext: ancestorContextFor(element), - domPath: domPathFor(element), - property: candidate.property, - color: colorToRgbString(color), - backgroundColor: colorToRgbString(background), - ratio: Number(ratio.toFixed(2)), - requiredRatio: minimumRatio, - cssRule: source.cssRule, - cssDeclaration: source.cssDeclaration, - stylesheet: source.stylesheet, - text: textFor(element), + const adjacentColors = [{ name: 'outer background', color: outerBackground }]; + if (candidate.isBorder && !isSameColor(innerBackground, outerBackground)) { + adjacentColors.push({ name: 'inner background', color: innerBackground }); + } + + adjacentColors.forEach((adjacentColor) => { + const color = composite(parsedColor as Rgba, adjacentColor.color); + const ratio = contrastRatio(color, adjacentColor.color); + if (ratio >= minimumRatio) return; + const source = findCssSource(element, candidate); + + violations.push({ + selector: selectorFor(element), + element: labelFor(element), + elementName: elementNameFor(element), + ancestorContext: ancestorContextFor(element), + domPath: domPathFor(element), + property: candidate.property, + color: colorToRgbString(color), + backgroundColor: colorToRgbString(adjacentColor.color), + adjacentTo: adjacentColor.name, + ratio: Number(ratio.toFixed(2)), + requiredRatio: minimumRatio, + cssRule: source.cssRule, + cssDeclaration: source.cssDeclaration, + stylesheet: source.stylesheet, + text: textFor(element), + }); }); }); }); From 4bac6ad5565463f575ff3b0a041a4041eea6dbea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Thu, 25 Jun 2026 08:15:14 -0300 Subject: [PATCH 033/121] update shared notes test screenshot --- .../sharednotes-type-Chromium-linux.png | Bin 4651 -> 4405 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/bigbluebutton-tests/playwright/sharednotes/etherpad/sharednotes.spec.ts-snapshots/sharednotes-type-Chromium-linux.png b/bigbluebutton-tests/playwright/sharednotes/etherpad/sharednotes.spec.ts-snapshots/sharednotes-type-Chromium-linux.png index 80fb8e20d15f901e88f3c58e0035d8b2d81ee42e..d20d77caa47692ad37c1a06b7b2a106ec1811feb 100644 GIT binary patch literal 4405 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 d-#cAz0mY;vtbh5?v{tczdfaPF)))iW^L{EfX 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 Date: Fri, 26 Jun 2026 16:53:29 -0300 Subject: [PATCH 034/121] [update-sdk-version-0.0.102] sync sdk version last published --- bigbluebutton-html5/package-lock.json | 8 ++++---- bigbluebutton-html5/package.json | 2 +- .../grails-app/conf/bigbluebutton.properties | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bigbluebutton-html5/package-lock.json b/bigbluebutton-html5/package-lock.json index 6b17ef822b2a..cf3f007a155e 100644 --- a/bigbluebutton-html5/package-lock.json +++ b/bigbluebutton-html5/package-lock.json @@ -36,7 +36,7 @@ "@types/react": "^18.2.18", "autoprefixer": "^10.4.4", "babel-runtime": "~6.26.0", - "bigbluebutton-html-plugin-sdk": "0.0.100", + "bigbluebutton-html-plugin-sdk": "0.0.102", "bowser": "^2.12.0", "browser-bunyan": "^1.8.0", "classnames": "^2.2.6", @@ -6804,9 +6804,9 @@ } }, "node_modules/bigbluebutton-html-plugin-sdk": { - "version": "0.0.100", - "resolved": "https://registry.npmjs.org/bigbluebutton-html-plugin-sdk/-/bigbluebutton-html-plugin-sdk-0.0.100.tgz", - "integrity": "sha512-Ge/CXQBCMwdfR6qjx4VI4SuCmEoH7rMW0t4d01ENiYi+mxLHi2HW8hiZvsEnlaw1D8KTN7ETSjhiHzL6J5070A==", + "version": "0.0.102", + "resolved": "https://registry.npmjs.org/bigbluebutton-html-plugin-sdk/-/bigbluebutton-html-plugin-sdk-0.0.102.tgz", + "integrity": "sha512-6Bkgk6faaVlQdsKIEGBk93+QDOtKOoO6sgmu9GeY4vdJcILTam9A2mSKKYH+tC3FBYPb37MKveuARWn5oxtsdQ==", "license": "LGPL-3.0", "dependencies": { "@apollo/client": "^3.8.7", diff --git a/bigbluebutton-html5/package.json b/bigbluebutton-html5/package.json index 03928c557259..5445f7288d4d 100644 --- a/bigbluebutton-html5/package.json +++ b/bigbluebutton-html5/package.json @@ -79,7 +79,7 @@ "@types/react": "^18.2.18", "autoprefixer": "^10.4.4", "babel-runtime": "~6.26.0", - "bigbluebutton-html-plugin-sdk": "0.0.100", + "bigbluebutton-html-plugin-sdk": "0.0.102", "bowser": "^2.12.0", "browser-bunyan": "^1.8.0", "classnames": "^2.2.6", diff --git a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties index 2ce3f5fb04cb..27444e55f33b 100644 --- a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties +++ b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties @@ -599,4 +599,4 @@ pluginManifestCacheRefreshIntervalMinutes=60 # List the parameters without including the 'userdata-' prefix getJoinUrlUserdataBlocklist=bbb_record_permission,bbb_record_video,bbb_fullaudio_bridge,bbb_transparent_listen_only,bbb_multi_user_pen_only,bbb_presenter_tools,bbb_multi_user_tools -html5PluginSdkVersion=0.0.100 +html5PluginSdkVersion=0.0.102 From 745e9e8cd38c80ea036b97c928186c32d0a74fdd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 14:01:47 +0000 Subject: [PATCH 035/121] build(deps): bump ws from 7.5.10 to 7.5.11 in /docs Bumps [ws](https://github.com/websockets/ws) from 7.5.10 to 7.5.11. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/7.5.10...7.5.11) --- updated-dependencies: - dependency-name: ws dependency-version: 7.5.11 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- docs/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index 7de247ff9c07..82223342ef25 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -19446,9 +19446,9 @@ } }, "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -19710,9 +19710,9 @@ } }, "node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", "license": "MIT", "engines": { "node": ">=8.3.0" From d866ef8e3d9f88862958279c4a6dd287fed552a8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 14:01:51 +0000 Subject: [PATCH 036/121] build(deps): bump form-data in /bbb-learning-dashboard Bumps [form-data](https://github.com/form-data/form-data) from 3.0.4 to 3.0.5. - [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md) - [Commits](https://github.com/form-data/form-data/compare/v3.0.4...v3.0.5) --- updated-dependencies: - dependency-name: form-data dependency-version: 3.0.5 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- bbb-learning-dashboard/package-lock.json | 29 ++++++++++++------------ 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/bbb-learning-dashboard/package-lock.json b/bbb-learning-dashboard/package-lock.json index f1e49f482e97..7b20af2ce4c0 100644 --- a/bbb-learning-dashboard/package-lock.json +++ b/bbb-learning-dashboard/package-lock.json @@ -8041,15 +8041,14 @@ } }, "node_modules/form-data": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz", - "integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==", - "license": "MIT", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.5.tgz", + "integrity": "sha512-j23EibVLnp4zNXGW7LjryXYa2X6U/M96yoOX+ybZxwkYajdxRNEqYY3zhh7y0i6kfISKS2jr+EJq1YTUDEv5+w==", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", + "hasown": "^2.0.4", "mime-types": "^2.1.35" }, "engines": { @@ -8435,9 +8434,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dependencies": { "function-bind": "^1.1.2" }, @@ -22600,14 +22599,14 @@ } }, "form-data": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz", - "integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.5.tgz", + "integrity": "sha512-j23EibVLnp4zNXGW7LjryXYa2X6U/M96yoOX+ybZxwkYajdxRNEqYY3zhh7y0i6kfISKS2jr+EJq1YTUDEv5+w==", "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", + "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, @@ -22829,9 +22828,9 @@ } }, "hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "requires": { "function-bind": "^1.1.2" } From 55f2bdeb49f5d113016ba4e68269b66c4694b479 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 14:01:55 +0000 Subject: [PATCH 037/121] build(deps): bump ws in /bbb-learning-dashboard Bumps and [ws](https://github.com/websockets/ws). These dependencies needed to be updated together. Updates `ws` from 8.17.1 to 8.21.0 - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/8.17.1...8.21.0) Updates `ws` from 7.5.10 to 7.5.11 - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/8.17.1...8.21.0) --- updated-dependencies: - dependency-name: ws dependency-version: 7.5.11 dependency-type: indirect - dependency-name: ws dependency-version: 8.21.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- bbb-learning-dashboard/package-lock.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/bbb-learning-dashboard/package-lock.json b/bbb-learning-dashboard/package-lock.json index f1e49f482e97..e39f795049de 100644 --- a/bbb-learning-dashboard/package-lock.json +++ b/bbb-learning-dashboard/package-lock.json @@ -17131,9 +17131,9 @@ } }, "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "engines": { "node": ">=10.0.0" }, @@ -17671,9 +17671,9 @@ } }, "node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", "engines": { "node": ">=8.3.0" }, @@ -28011,9 +28011,9 @@ } }, "ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "requires": {} } } @@ -28352,9 +28352,9 @@ } }, "ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", "requires": {} }, "xml-name-validator": { From 72e551168473386d89e703b9e74d93765037a38d Mon Sep 17 00:00:00 2001 From: Claudio Promptoso Date: Tue, 30 Jun 2026 00:30:27 +0000 Subject: [PATCH 038/121] fix(whiteboard): prevent crash when slide changes while a viewer is editing When the presenter changed the slide while a viewer was mid-edit on the whiteboard (select.editing_shape), the [curPageId] effect mutated the tldraw store (cleanupStore + setCurrentPage) and removed the shape being edited without first taking the editor out of editing_shape. The editor was left with a dangling editingShapeId, so the viewer's next pointer-down hit tldraw's "Expected an editing shape!" assertion and tripped the error boundary. Commit the in-progress edit via editor.complete() before the store mutation so tldraw runs EditingShape.onExit while the shape still exists. The call is guarded by getEditingShape() so a normal slide change (no active edit) never resets the presenter's current tool. Add a Playwright regression test (extends MultiUsers) that drives the race and asserts the viewer never throws "Expected an editing shape!" and the whiteboard stays mounted. Fixes #25332 --- .../ui/components/whiteboard/component.jsx | 11 +++ .../playwright/core/elements.ts | 3 + .../whiteboard/slideChangeWhileEditing.ts | 86 +++++++++++++++++++ .../playwright/whiteboard/whiteboard.spec.ts | 8 ++ 4 files changed, 108 insertions(+) create mode 100644 bigbluebutton-tests/playwright/whiteboard/slideChangeWhileEditing.ts diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 67102b7e21fd..df915424b4a2 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -2270,6 +2270,17 @@ const Whiteboard = React.memo((props) => { React.useEffect(() => { const formattedPageId = parseInt(curPageIdRef.current, 10); if (tlEditorRef.current && formattedPageId !== 0) { + // If a viewer is mid-edit (select.editing_shape) when the slide changes, + // the store mutation below (cleanupStore + setCurrentPage) removes the shape + // being edited out from under tldraw, leaving the editor in editing_shape with + // a dangling editingShapeId. The next pointer-down then hits EditingShape's + // `Expected an editing shape!` assertion and crashes the client (issue 25332). + // Commit the in-progress edit first so tldraw exits editing_shape (running + // EditingShape.onExit) while the shape still exists. Guarded so a normal slide + // change (no active edit) never resets the presenter's current tool. + if (tlEditorRef.current.getEditingShape()) { + tlEditorRef.current.complete(); + } tlEditorRef.current.store.mergeRemoteChanges(() => { tlEditorRef.current.batch(() => { const currentPageId = `page:${formattedPageId}`; diff --git a/bigbluebutton-tests/playwright/core/elements.ts b/bigbluebutton-tests/playwright/core/elements.ts index 761c1b20bb6a..01d67a08f693 100644 --- a/bigbluebutton-tests/playwright/core/elements.ts +++ b/bigbluebutton-tests/playwright/core/elements.ts @@ -587,6 +587,9 @@ export const elements = { wbMoveToFront: 'button[data-testid="menu-item.bring-to-front"]', wbPaste: 'button[data-testid="menu-item.paste"]', wbTextTrue: 'div[data-hastext="true"]', + // tldraw renders this textarea only while a shape is actively being edited + // (select.editing_shape state) - used to gate on "is editing", not just "has text" + wbEditingTextArea: 'textarea.tl-text-input', wbDrawnArrow: 'div[data-shape-type="arrow"]', wbAutoHideToggleBtn: 'input[data-test="autoHideToolbarToggleBtn"]', turnInfiniteWhiteboardOn: 'button[data-test="turnInfiniteWhiteboardOn"]', diff --git a/bigbluebutton-tests/playwright/whiteboard/slideChangeWhileEditing.ts b/bigbluebutton-tests/playwright/whiteboard/slideChangeWhileEditing.ts new file mode 100644 index 000000000000..c23a9722db5c --- /dev/null +++ b/bigbluebutton-tests/playwright/whiteboard/slideChangeWhileEditing.ts @@ -0,0 +1,86 @@ +import { expect } from '@playwright/test'; + +import { ELEMENT_WAIT_LONGER_TIME } from '../core/constants'; +import { elements as e } from '../core/elements'; +import { MultiUsers } from '../user/multiusers'; + +export class SlideChangeWhileEditing extends MultiUsers { + // Regression test for issue 25332. + // A viewer editing a whiteboard text shape used to crash the client with + // "Expected an editing shape!" when the presenter changed the slide while the + // viewer was still mid-edit: the slide-change effect mutated the tldraw store + // (removing the shape being edited) without first taking the editor out of the + // select.editing_shape state, so the viewer's next pointer-down hit tldraw's + // editing-shape assertion and tripped the error boundary. + async crashOnSlideChangeWhileEditing() { + // The viewer is the page that crashed, so watch it for uncaught errors. + const viewerPageErrors: string[] = []; + this.userPage.page.on('pageerror', (err) => viewerPageErrors.push(err.message)); + + await this.modPage.hasElement( + e.whiteboard, + 'should display the whiteboard for the presenter', + ELEMENT_WAIT_LONGER_TIME, + ); + await this.userPage.hasElement(e.whiteboard, 'should display the whiteboard for the viewer'); + // Changing slides requires a deck with at least two slides; the default + // presentation provides them (same assumption as the skipSlide test). + await this.modPage.hasElement(e.nextSlide, 'should display the next-slide control (deck has >= 2 slides)'); + + // Presenter grants multi-user whiteboard so the viewer can draw. + await this.modPage.waitAndClick(e.multiUsersWhiteboardOn); + + // Viewer enters select.editing_shape: pick the text tool, click the canvas and + // type some text. Do NOT click away or press Escape/Enter - that would commit + // the edit and leave editing_shape, defeating the precondition. + await this.userPage.waitAndClick(e.wbTextShape); + const wbBox = await this.userPage.page.locator(e.whiteboard).boundingBox(); + if (!wbBox) throw new Error('whiteboard boundingBox is null'); + const textX = wbBox.x + 0.4 * wbBox.width; + const textY = wbBox.y + 0.4 * wbBox.height; + await this.userPage.page.mouse.click(textX, textY); + await this.userPage.page.waitForTimeout(300); + await this.userPage.page.keyboard.type('Hello'); + + // Deterministic precondition: the text shape exists (wbTextTrue) AND the editor + // is actively editing it (tldraw only renders the editing textarea while + // isEditing is true). Asserting both avoids racing the slide change against an + // edit that has not actually entered editing_shape yet. + await this.userPage.hasElement(e.wbTextTrue, 'viewer should have a text shape with content'); + await this.userPage.hasElement(e.wbEditingTextArea, 'viewer should be actively editing the text shape'); + + // The race: presenter advances the slide while the viewer is mid-edit. Give the + // slide-change effect time to run on the viewer (it mutates the tldraw store). + await this.modPage.waitAndClick(e.nextSlide); + await this.userPage.page.waitForTimeout(2500); + + // The viewer's next pointer-down on the canvas is what used to throw + // "Expected an editing shape!". Probe a few spots (incl. where the now-removed + // text shape was) so the pointer-down resolves to the dangling editing shape. + const probePoints = [ + [0.5, 0.5], + [0.6, 0.55], + [0.4, 0.4], + ]; + for (const [fx, fy] of probePoints) { + await this.userPage.page.mouse.move(wbBox.x + fx * wbBox.width, wbBox.y + fy * wbBox.height); + await this.userPage.page.mouse.down(); + await this.userPage.page.waitForTimeout(150); + await this.userPage.page.mouse.up(); + await this.userPage.page.waitForTimeout(800); + } + // Give the client a beat to surface any async error / error boundary. + await this.userPage.page.waitForTimeout(1000); + + const editingShapeErrors = viewerPageErrors.filter((m) => /Expected an editing shape/i.test(m)); + expect( + editingShapeErrors, + `viewer must not throw "Expected an editing shape!" on slide change while editing (got: ${editingShapeErrors.join(' | ')})`, + ).toHaveLength(0); + // The whiteboard must still be mounted (not replaced by the error-boundary fallback). + await this.userPage.hasElement( + e.whiteboard, + 'whiteboard should still be mounted on the viewer after the slide change', + ); + } +} diff --git a/bigbluebutton-tests/playwright/whiteboard/whiteboard.spec.ts b/bigbluebutton-tests/playwright/whiteboard/whiteboard.spec.ts index 1d33600f9ce5..0dbe9b94261e 100644 --- a/bigbluebutton-tests/playwright/whiteboard/whiteboard.spec.ts +++ b/bigbluebutton-tests/playwright/whiteboard/whiteboard.spec.ts @@ -6,6 +6,7 @@ import { ChangeStyles } from './changeStyles'; import { DrawShape } from './drawShape'; import { ShapeOptions } from './shapeOptions'; import { ShapeTools } from './shapeTools'; +import { SlideChangeWhileEditing } from './slideChangeWhileEditing'; import { TextShape } from './textShape'; import { WhiteboardResize } from './whiteboardResize'; @@ -162,6 +163,13 @@ test.describe.parallel('Whiteboard tools', { tag: '@ci' }, () => { await textShape.realTimeTextTyping(); }); + test('No crash on slide change while a viewer is editing', async ({ browser, context, page }, testInfo) => { + const slideChange = new SlideChangeWhileEditing(browser, context); + await slideChange.initModPage(page, { testInfo }); + await slideChange.initUserPage(context, { testInfo }); + await slideChange.crashOnSlideChangeWhileEditing(); + }); + test.describe.parallel('Shape Options', () => { test('Duplicate', async ({ browser, context, page }, testInfo) => { const shapeOptions = new ShapeOptions(browser, context); From b34a72d48abac70f0a438a8f144a6c74f80b2966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Tue, 30 Jun 2026 16:06:39 -0300 Subject: [PATCH 039/121] fix visible focus indicator for chat message input wrapper --- .../components/chat/chat-graphql/chat-message-form/styles.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/styles.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/styles.ts index aa3a2db2fc5f..5c8b3c985a56 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/styles.ts @@ -2,6 +2,7 @@ import styled from 'styled-components'; import { colorText, colorBorder, + colorPrimary, colorDanger, colorGrayDark, } from '/imports/ui/stylesheets/styled-components/palette'; @@ -178,7 +179,8 @@ const InputWrapper = styled.div` } &:focus-within { - border: 1px solid ${colorBorder}; + border-color: ${colorPrimary}; + box-shadow: 0 0 0 ${borderSize} ${colorPrimary}; } `; From d8a752a924d00d4aa9ef709756fe8fd67fa8b7d7 Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Wed, 1 Jul 2026 10:29:56 -0300 Subject: [PATCH 040/121] bump Hasura version from 2.48.14 to 2.49.3 --- build/packages-template/bbb-graphql-server/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/packages-template/bbb-graphql-server/build.sh b/build/packages-template/bbb-graphql-server/build.sh index ff867c0e6981..e47f7d8294bb 100755 --- a/build/packages-template/bbb-graphql-server/build.sh +++ b/build/packages-template/bbb-graphql-server/build.sh @@ -22,7 +22,7 @@ for dir in $DIRS; do mkdir -p staging$dir done -HASURA_VERSION=v2.48.14 +HASURA_VERSION=v2.49.3 git clone --branch $HASURA_VERSION https://github.com/iMDT/hasura-graphql-engine.git cat hasura-graphql-engine/hasura-graphql.part-a* > hasura-graphql From cee353bb6530910cd9020a3ad728feaf10fe167b Mon Sep 17 00:00:00 2001 From: Tainan Felipe Date: Wed, 1 Jul 2026 11:50:31 -0300 Subject: [PATCH 041/121] fix(quick-poll): keep parenthetical text in Smart Slides poll question (#25320) The Smart Slides / Quick Poll parser stripped any parenthesised clause from the poll question and, because the regex also removed one space on each side, joined the two surrounding words together - e.g. "the dose (one per day) for adults" rendered as "the dosefor adults", and "How long (in min) for ..." rendered as "How longfor ...". The stripping regex was added incidentally in 011a41f6d1 ("restrict poll item length to 30 chars"), unrelated to that commit's stated purpose; before it the question was passed through verbatim (412221c66). Restore that behaviour by no longer stripping parentheticals. Correct-answer ("__"), URL and option (A-D) parsing use separate code paths and are unaffected. Add Playwright regression tests (lettered poll + typed response) that upload a repro deck and assert the parenthetical and its surrounding spaces are kept. Co-authored-by: Tainan Felipe --- .../quick-poll-dropdown/component.jsx | 2 +- .../playwright/core/elements.ts | 1 + .../core/media/smart-slides-bug-repro-1.pdf | Bin 0 -> 23931 bytes .../playwright/polling/poll.ts | 65 ++++++++++++++++++ .../playwright/polling/polling.spec.ts | 8 +++ 5 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 bigbluebutton-tests/playwright/core/media/smart-slides-bug-repro-1.pdf diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx index 2e2a4532c7ac..bd612635cc03 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx @@ -341,7 +341,7 @@ const QuickPollDropdown = (props) => { poll, })); - const pollQuestion = (question?.length > 0 && question[0]?.replace(/ *\([^)]*\) */g, '')) || ''; + const pollQuestion = (question?.length > 0 && question[0]) || ''; const slideId = currentSlide.id; diff --git a/bigbluebutton-tests/playwright/core/elements.ts b/bigbluebutton-tests/playwright/core/elements.ts index 01d67a08f693..6606a9346dba 100644 --- a/bigbluebutton-tests/playwright/core/elements.ts +++ b/bigbluebutton-tests/playwright/core/elements.ts @@ -310,6 +310,7 @@ export const elements = { errorNoValueInput: 'div[data-test="errorNoValueInput"]', smartSlides1: 'smartSlidesPresentation.pdf', smartSlides2: 'SmartSlides.pdf', + smartSlidesBugRepro1: 'smart-slides-bug-repro-1.pdf', responsePollQuestion: 'div[data-test="pollQuestion"]', firstPollAnswerOptionBtn: `${pollAnswersOption}>>nth=0`, secondPollAnswerOptionBtn: `${pollAnswersOption}>>nth=1`, diff --git a/bigbluebutton-tests/playwright/core/media/smart-slides-bug-repro-1.pdf b/bigbluebutton-tests/playwright/core/media/smart-slides-bug-repro-1.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b8b3d7173b7ca4fbb2375e0bc6094943ece99145 GIT binary patch literal 23931 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 mg/m3) is a separate, out-of-scope issue (Bug 2); + // we only assert that the parenthetical and its surrounding spaces are kept. + async parentheticalQuestionTypedResponse() { + await this.modPage.waitForSelector(e.whiteboard, ELEMENT_WAIT_LONGER_TIME); + await util.uploadSPresentationForTestingPolls(this.modPage, e.smartSlidesBugRepro1); + await this.userPage.hasElement(e.userListItem, 'should display the user list item for the attendee'); + await this.modPage.closeAllToastNotifications(); + await this.modPage.page.waitForTimeout(5000); + + // Slide 4 — typed response: "How long (in min) for a 2 mg/m3 sample?" + await this.modPage.selectSlide('Slide 4'); + await this.modPage.hasElement( + e.quickPoll, + 'should display the quick poll button once the smart slides deck is converted', + ELEMENT_WAIT_EXTRA_LONG_TIME, + ); + // Let the current slide propagate to the quick-poll dropdown before triggering it, otherwise + // the click can capture the previous slide's (stale) parsed question. + await this.modPage.page.waitForTimeout(3000); + await this.modPage.waitAndClick(e.quickPoll, ELEMENT_WAIT_LONGER_TIME); + const typedQuestion = this.modPage.page.locator(e.pollQuestionArea); + await expect( + typedQuestion, + 'the typed-response question should keep the "(in min)" clause', + ).toHaveValue(/How long \(in min\) for/, { timeout: ELEMENT_WAIT_TIME }); + await expect( + typedQuestion, + 'the words around the parenthetical must not be joined together (no "longfor")', + ).not.toHaveValue(/longfor/, { timeout: ELEMENT_WAIT_TIME }); + } + async pollResultsOnChat() { const { pollChatMessage } = this.modPage.settings || {}; diff --git a/bigbluebutton-tests/playwright/polling/polling.spec.ts b/bigbluebutton-tests/playwright/polling/polling.spec.ts index c490a944b3b8..5a4084c31b3b 100644 --- a/bigbluebutton-tests/playwright/polling/polling.spec.ts +++ b/bigbluebutton-tests/playwright/polling/polling.spec.ts @@ -83,6 +83,14 @@ test.describe.parallel('Polling', { tag: '@ci' }, () => { await polling.typeResponse(); }); + test('Parenthetical question preserved - lettered poll - issue 25320', async () => { + await polling.parentheticalQuestionLetterPoll(); + }); + + test('Parenthetical question preserved - typed response - issue 25320', async () => { + await polling.parentheticalQuestionTypedResponse(); + }); + test('Hiding pools - Poll anywhere in the slide', async () => { await polling.pollAnywhereSlide(); }); From c668458f48866c0983137bd4e9772bf17cfd9069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Wed, 1 Jul 2026 13:13:09 -0300 Subject: [PATCH 042/121] chore: BigBlueButton release version to 3.0.32 --- bigbluebutton-config/bigbluebutton-release | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigbluebutton-config/bigbluebutton-release b/bigbluebutton-config/bigbluebutton-release index 688f1f3d594e..811bafdd44db 100644 --- a/bigbluebutton-config/bigbluebutton-release +++ b/bigbluebutton-config/bigbluebutton-release @@ -1 +1 @@ -BIGBLUEBUTTON_RELEASE=3.0.31 +BIGBLUEBUTTON_RELEASE=3.0.32 From af33e91cae1fc83cc2efec49b258859d14f67b18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Wed, 1 Jul 2026 16:57:18 -0300 Subject: [PATCH 043/121] update export-annotations deps --- bbb-export-annotations/package-lock.json | 96 +++++++++++++----------- 1 file changed, 54 insertions(+), 42 deletions(-) diff --git a/bbb-export-annotations/package-lock.json b/bbb-export-annotations/package-lock.json index ea471038a78c..8a9f58577803 100644 --- a/bbb-export-annotations/package-lock.json +++ b/bbb-export-annotations/package-lock.json @@ -254,9 +254,9 @@ ] }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "dependencies": { @@ -752,16 +752,16 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -930,9 +930,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -1032,10 +1032,20 @@ "dev": true }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -1094,19 +1104,21 @@ } }, "node_modules/mime-db": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", - "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "2.1.34", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", - "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { - "mime-db": "1.51.0" + "mime-db": "1.52.0" }, "engines": { "node": ">= 0.6" @@ -1700,9 +1712,9 @@ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, "brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "requires": { "balanced-match": "^1.0.0", @@ -2059,15 +2071,15 @@ } }, "form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" } }, "fs.realpath": { @@ -2175,9 +2187,9 @@ } }, "hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "requires": { "function-bind": "^1.1.2" } @@ -2249,9 +2261,9 @@ "dev": true }, "js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, "requires": { "argparse": "^2.0.1" @@ -2299,16 +2311,16 @@ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" }, "mime-db": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", - "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==" + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" }, "mime-types": { - "version": "2.1.34", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", - "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "requires": { - "mime-db": "1.51.0" + "mime-db": "1.52.0" } }, "minimatch": { From adb206862ebffe1d706dc3480f9e296df2303ffa Mon Sep 17 00:00:00 2001 From: Claudio Promptoso Date: Wed, 1 Jul 2026 22:57:34 +0000 Subject: [PATCH 044/121] fix(bbb-export-annotations): rasterize background slides to prevent ODF export cropping (#25303) Slides converted from office documents (ODF/ODP) carry absolute units (e.g. width="720pt") and embed a single high-resolution image. When such a slide is referenced from the annotated SVG as an , CairoSVG renders it at its intrinsic size and crops it into the top-left corner, even when the slide already has a viewBox. This is the case that PR #25315's ensureSlideViewBox could not reach, because it is a no-op when a viewBox is already present. Rasterize the background slide to a PNG before compositing: a raster image always scales to fill the box. The slide's viewBox is still ensured first, so slides that lack one keep filling (no #25315 regression). The PNG is rendered at least as large as the largest embedded raster, because CairoSVG (< 2.7) crops the slide the same way when it has to downscale an embedded raster below its native size, and the final resolution toPx(slideWidth) can be well below it. The background-slide handling moves to lib/utils/slide-background.js and gains a focused test suite (node:test) covering the viewBox, no-viewBox and embedded-raster cases. --- .../lib/utils/slide-background.js | 122 ++++++++++ bbb-export-annotations/package-lock.json | 19 +- bbb-export-annotations/package.json | 4 +- .../tests/slide-background.test.js | 226 ++++++++++++++++++ bbb-export-annotations/workers/process.js | 70 +++--- 5 files changed, 398 insertions(+), 43 deletions(-) create mode 100644 bbb-export-annotations/lib/utils/slide-background.js create mode 100644 bbb-export-annotations/tests/slide-background.test.js diff --git a/bbb-export-annotations/lib/utils/slide-background.js b/bbb-export-annotations/lib/utils/slide-background.js new file mode 100644 index 000000000000..24bf9056c236 --- /dev/null +++ b/bbb-export-annotations/lib/utils/slide-background.js @@ -0,0 +1,122 @@ +import cp from 'child_process'; +import fs from 'fs'; + +/** + * Ensure a slide background SVG carries a viewBox. + * + * A slide whose root `` declares a width/height but no viewBox has no + * intrinsic mapping from its user coordinates to that box, so when it is + * rasterized (or referenced) it renders into the top-left corner and leaves the + * rest blank. Deriving a viewBox from the declared width/height restores that + * mapping so the slide content fills the whole slide. See issue #25303 and the + * regression guarded by PR #25315. + * + * The file is patched in place. A missing width/height, an already present + * viewBox, or an unreadable file is a no-op (nothing to derive from). + * + * @param {string} file Path of the slide SVG. + */ +export function ensureSlideViewBox(file) { + const svg = fs.readFileSync(file, {encoding: 'utf-8'}); + const svgTag = svg.match(/]*>/)?.[0]; + + if (!svgTag || /viewBox=/.test(svgTag)) return; + + const width = svgTag.match(/(?` embedded in + * a slide SVG. Returns 0 when the slide has no embedded raster (pure vector) or + * cannot be read. + * @param {string} file Path of the slide SVG. + * @return {number} Largest embedded raster width, or 0. + */ +export function largestEmbeddedRasterWidth(file) { + try { + const svg = fs.readFileSync(file, {encoding: 'utf-8'}); + let maxWidth = 0; + const imageTag = /]*>/gi; + const widthAttr = /(? maxWidth) maxWidth = width; + } + return maxWidth; + } catch (error) { + return 0; + } +} + +/** + * Rasterize a background slide SVG to a PNG so it composites without cropping. + * + * The background slide is later embedded in the annotated SVG as an `` + * sized to the slide canvas. CairoSVG only rescales a *referenced* SVG to that + * box when it is resolution-independent; slides carrying absolute units (e.g. + * `width="720pt"`) keep their intrinsic size and render cropped into the + * top-left corner even when they have a viewBox (issue #25303, the case PR + * #25315 could not reach). A raster image always scales to fill the `` + * box, so rasterizing the slide first sidesteps that. The slide's viewBox is + * ensured first so that slides missing one still fill the raster. + * + * The rasterization itself must avoid a second CairoSVG (< 2.7) quirk: when it + * *downscales* an embedded raster below its native pixel size it crops the + * slide the same way. Office-document slides embed a single high-resolution + * image (e.g. a 2048px picture in a 720pt viewBox), so the final resolution + * `toPx(slideWidth)` can fall well below it. We therefore render at least as + * large as the biggest embedded raster; the composite scales the PNG down + * cleanly afterwards, keeping the background sharp. + * + * @param {string} svgPath Path of the slide background SVG. + * @param {string} pngPath Destination path for the rasterized PNG. + * @param {Object} options + * @param {number} options.width Target output width in pixels (final render). + * @param {number} options.height Target output height in pixels. + * @param {string} options.cairosvg Path to the CairoSVG executable. + * @param {boolean} [options.unsafe=false] Pass CairoSVG's `-u` flag, needed + * from CairoSVG 2.7.0 onwards to allow loading external resources. + * @return {string} Path of the rasterized PNG. + * @throws {Error} If CairoSVG cannot be spawned or exits non-zero. + */ +export function rasterizeSlideBackground(svgPath, pngPath, { + width, height, cairosvg, unsafe = false, +}) { + ensureSlideViewBox(svgPath); + + // Never render below the largest embedded raster's native width, otherwise + // CairoSVG crops the slide (issue #25303). Keep the slide's aspect ratio. + let renderWidth = width; + let renderHeight = height; + const maxRasterWidth = largestEmbeddedRasterWidth(svgPath); + if (maxRasterWidth > renderWidth) { + renderWidth = maxRasterWidth; + renderHeight = Math.round(maxRasterWidth * height / width); + } + + const args = [ + svgPath, + '--output-width', renderWidth, + '--output-height', renderHeight, + ...(unsafe ? ['-u'] : []), + '-o', pngPath, + ]; + + const result = cp.spawnSync(cairosvg, args, {shell: false}); + + if (result.error) throw result.error; + + if (result.status !== 0) { + const stderr = result.stderr?.toString().trim(); + throw new Error(`CairoSVG exited with status ${result.status}: ${stderr}`); + } + + return pngPath; +} diff --git a/bbb-export-annotations/package-lock.json b/bbb-export-annotations/package-lock.json index ea471038a78c..bf9c9969c04c 100644 --- a/bbb-export-annotations/package-lock.json +++ b/bbb-export-annotations/package-lock.json @@ -20,7 +20,8 @@ }, "devDependencies": { "eslint": "^8.20.0", - "eslint-config-google": "^0.14.0" + "eslint-config-google": "^0.14.0", + "pngjs": "^7.0.0" }, "engines": { "node": ">=18.16.0", @@ -1220,6 +1221,16 @@ "integrity": "sha512-eh31l019WICQ03pkF3FSzHxB8n07ItqIQ++G5UV8JX0zVOXzgTGCqnRR0jJ2h9U8/2uW4W4mtGJELt9kEV0CFQ==", "license": "MIT" }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -2395,6 +2406,12 @@ "resolved": "https://registry.npmjs.org/perfect-freehand/-/perfect-freehand-1.2.2.tgz", "integrity": "sha512-eh31l019WICQ03pkF3FSzHxB8n07ItqIQ++G5UV8JX0zVOXzgTGCqnRR0jJ2h9U8/2uW4W4mtGJELt9kEV0CFQ==" }, + "pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/bbb-export-annotations/package.json b/bbb-export-annotations/package.json index 7bba016706c1..1c4a35f8f380 100644 --- a/bbb-export-annotations/package.json +++ b/bbb-export-annotations/package.json @@ -4,6 +4,7 @@ "description": "BigBlueButton's Presentation Annotation Exporter", "scripts": { "start": "node master.js", + "test": "node --test tests/", "lint:fix": "eslint --fix '**/*.js' --ignore-pattern 'node_modules/'", "lint": "eslint '**/*.js' --ignore-pattern 'node_modules/'" }, @@ -20,7 +21,8 @@ }, "devDependencies": { "eslint": "^8.20.0", - "eslint-config-google": "^0.14.0" + "eslint-config-google": "^0.14.0", + "pngjs": "^7.0.0" }, "engines": { "node": ">=18.16.0", diff --git a/bbb-export-annotations/tests/slide-background.test.js b/bbb-export-annotations/tests/slide-background.test.js new file mode 100644 index 000000000000..eb99a161c567 --- /dev/null +++ b/bbb-export-annotations/tests/slide-background.test.js @@ -0,0 +1,226 @@ +import {test, before, after} from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import {PNG} from 'pngjs'; +import { + ensureSlideViewBox, + largestEmbeddedRasterWidth, + rasterizeSlideBackground, +} from '../lib/utils/slide-background.js'; + +/** + * Build a base64 data URI for a solid-colour PNG of the given size. + * @param {number} w Width. + * @param {number} h Height. + * @param {number[]} rgb Colour as [r, g, b]. + * @return {string} A `data:image/png;base64,...` URI. + */ +function pngDataUri(w, h, [r, g, b]) { + const png = new PNG({width: w, height: h}); + for (let i = 0; i < png.data.length; i += 4) { + png.data[i] = r; + png.data[i + 1] = g; + png.data[i + 2] = b; + png.data[i + 3] = 255; + } + return 'data:image/png;base64,' + PNG.sync.write(png).toString('base64'); +} + +const config = JSON.parse(fs.readFileSync('./config/settings.json', 'utf8')); +const cairosvg = config.shared.cairosvg; + +// Slide body: the whole slide area (720 x 405 user units) painted solid blue, +// so any exported pixel that is not blue means the slide did not fill the box. +const SLIDE_BODY = + '\n'; + +// A slide that carries absolute units (720pt) AND a viewBox. This is the shape +// of the reporter's ODF slide (issue #25303): when referenced from an , +// CairoSVG renders it at its intrinsic pixel size and crops it into the +// top-left corner - the gap PR #25315 could not reach. +const SLIDE_WITH_VIEWBOX = + '\n' + + '\n' + + SLIDE_BODY + '\n'; + +// The same slide with NO viewBox: the case PR #25315 introduced ensureSlideVie- +// wBox for. Without a viewBox the content has no mapping to the canvas and +// rasterizing alone would crop it, so ensureSlideViewBox must still run. +const SLIDE_WITHOUT_VIEWBOX = + '\n' + + '\n' + + SLIDE_BODY + '\n'; + +// Slide geometry in tldraw coordinates (a 16:9 slide) and the values process.js +// derives from it. See workers/process.js. +const SLIDE_WIDTH = 1440; +const SLIDE_HEIGHT = 810; + +/** + * Converts points to pixels exactly as process.js does. + * @param {number} pt Measurement in points. + * @return {number} Measurement in pixels. + */ +function toPx(pt) { + return (pt / config.process.pointsPerInch) * config.process.pixelsPerInch; +} + +const OUTPUT_WIDTH = toPx(SLIDE_WIDTH); // 1920 +const OUTPUT_HEIGHT = toPx(SLIDE_HEIGHT); // 1080 + +// The (smaller) box the composite canvas is sized to. The fix must NOT use +// these for the rasterization or the background would be upscaled and blurry. +const maxImageWidth = config.process.maxImageWidth; +const maxImageHeight = config.process.maxImageHeight; +const ratio = Math.min(maxImageWidth / SLIDE_WIDTH, + maxImageHeight / SLIDE_HEIGHT); +const scaledWidth = SLIDE_WIDTH * ratio; + +let workdir; + +before(() => { + workdir = fs.mkdtempSync(path.join(os.tmpdir(), 'bbb-export-test-')); +}); + +after(() => { + fs.rmSync(workdir, {recursive: true, force: true}); +}); + +/** + * Returns the RGBA components of a single pixel. + * @param {PNG} png Decoded image. + * @param {number} x Column. + * @param {number} y Row. + * @return {number[]} [r, g, b, a]. + */ +function pixel(png, x, y) { + const idx = (png.width * y + x) << 2; + return [png.data[idx], png.data[idx + 1], + png.data[idx + 2], png.data[idx + 3]]; +} + +/** + * Rasterizes a slide SVG and asserts the resulting PNG is the final render + * resolution and covered edge to edge with the opaque blue of the slide. A + * full-resolution raster covering its whole area is what guarantees the + * composited slide fills the box on any CairoSVG version. + * @param {string} svg Slide SVG contents. + * @param {string} tag Unique file suffix. + */ +function assertFillsBox(svg, tag) { + const svgPath = path.join(workdir, `slide-${tag}.svg`); + fs.writeFileSync(svgPath, svg); + + const png = rasterizeSlideBackground( + svgPath, path.join(workdir, `slide-${tag}.png`), + {width: OUTPUT_WIDTH, height: OUTPUT_HEIGHT, cairosvg}); + + const image = PNG.sync.read(fs.readFileSync(png)); + + // Must match the SVG->PDF pass (toPx), not the smaller composite box, so the + // background is never upscaled from a low-resolution raster. + assert.equal(image.width, OUTPUT_WIDTH); + assert.equal(image.height, OUTPUT_HEIGHT); + assert.notEqual(image.width, scaledWidth); + + const w = image.width; + const h = image.height; + const samples = [ + [1, 1], [w - 2, 1], [1, h - 2], [w - 2, h - 2], + [w >> 1, 1], [w >> 1, h - 2], [1, h >> 1], [w - 2, h >> 1], + [w >> 1, h >> 1], + ]; + + for (const [x, y] of samples) { + const [r, g, b, a] = pixel(image, x, y); + assert.ok(a > 250, `pixel (${x},${y}) is transparent (uncovered)`); + assert.ok(b > 200 && r < 50 && g < 50, + `pixel (${x},${y}) is not the slide blue: rgba(${r},${g},${b},${a})`); + } +} + +test('slide with a viewBox rasterizes filling the whole box (issue #25303)', + () => assertFillsBox(SLIDE_WITH_VIEWBOX, 'vb')); + +test('slide without a viewBox still fills the box (no #25315 regression)', + () => assertFillsBox(SLIDE_WITHOUT_VIEWBOX, 'novb')); + +test('ensureSlideViewBox derives a viewBox only when one is missing', () => { + const missing = path.join(workdir, 'missing.svg'); + fs.writeFileSync(missing, SLIDE_WITHOUT_VIEWBOX); + ensureSlideViewBox(missing); + assert.match(fs.readFileSync(missing, 'utf8'), + /viewBox="0 0 720 405"/); + + // A slide that already has a viewBox is left untouched. + const present = path.join(workdir, 'present.svg'); + fs.writeFileSync(present, SLIDE_WITH_VIEWBOX); + ensureSlideViewBox(present); + assert.equal(fs.readFileSync(present, 'utf8'), SLIDE_WITH_VIEWBOX); +}); + +/** + * A slide shaped like an office-document export: a small (720pt) viewBox whose + * entire area is a single high-resolution embedded raster (2048px wide, scaled + * down into the viewBox). CairoSVG < 2.7 crops this when asked to downscale the + * raster below its native size, which is what issue #25303 reports. + * @return {string} The slide SVG. + */ +function slideWithRaster() { + const uri = pngDataUri(2048, 1152, [0, 0, 255]); + return '\n' + + '\n' + + `` + + '\n\n`; +} + +test('reads the largest embedded raster width', () => { + const svgPath = path.join(workdir, 'raster.svg'); + fs.writeFileSync(svgPath, slideWithRaster()); + assert.equal(largestEmbeddedRasterWidth(svgPath), 2048); + + const vector = path.join(workdir, 'vector.svg'); + fs.writeFileSync(vector, SLIDE_WITH_VIEWBOX); + assert.equal(largestEmbeddedRasterWidth(vector), 0); +}); + +test('renders at the embedded raster resolution when it exceeds the target', + () => { + const svgPath = path.join(workdir, 'raster.svg'); + fs.writeFileSync(svgPath, slideWithRaster()); + + // Target is the small final resolution; the raster (2048px) is larger, so + // the slide must be rendered at 2048 to avoid CairoSVG's downscale crop. + const png = rasterizeSlideBackground( + svgPath, path.join(workdir, 'raster.png'), + {width: 960, height: 540, cairosvg}); + const image = PNG.sync.read(fs.readFileSync(png)); + assert.equal(image.width, 2048); + assert.equal(image.height, Math.round(2048 * 540 / 960)); + + // Still fills to the corners with the embedded blue (sample just inside + // the edge to avoid the anti-aliased border pixel). + for (const [x, y] of [[5, 5], [image.width - 6, image.height - 6]]) { + const [r, g, b, a] = pixel(image, x, y); + assert.ok(a > 250 && b > 200 && r < 50 && g < 50, + `corner (${x},${y}) not filled: rgba(${r},${g},${b},${a})`); + } + }); + +test('throws when CairoSVG cannot produce the raster', () => { + const svgPath = path.join(workdir, 'err.svg'); + fs.writeFileSync(svgPath, SLIDE_WITH_VIEWBOX); + + assert.throws(() => rasterizeSlideBackground( + svgPath, path.join(workdir, 'err.png'), + {width: OUTPUT_WIDTH, height: OUTPUT_HEIGHT, cairosvg: '/nonexistent'})); +}); diff --git a/bbb-export-annotations/workers/process.js b/bbb-export-annotations/workers/process.js index 261c210c238f..924bc5f8b7bd 100644 --- a/bbb-export-annotations/workers/process.js +++ b/bbb-export-annotations/workers/process.js @@ -4,6 +4,7 @@ import {createSVGWindow} from 'svgdom'; import {SVG as svgCanvas, registerWindow} from '@svgdotjs/svg.js'; import cp from 'child_process'; import WorkerStarter from '../lib/utils/worker-starter.js'; +import {rasterizeSlideBackground} from '../lib/utils/slide-background.js'; import {workerData} from 'worker_threads'; import path from 'path'; import sanitize from 'sanitize-filename'; @@ -295,39 +296,6 @@ async function overlayAnnotations(svg, slideAnnotations) { } } -/** - * Ensure a slide background SVG carries a viewBox. - * - * The background slide is loaded as an `` sized to the slide, but - * CairoSVG only rescales the referenced SVG to that box when the file is - * resolution-independent (carries a viewBox). Slides converted without one - * keep their intrinsic size and render cropped into the top-left corner. - * When the attribute is missing, derive it from the SVG's own width and - * height so the background scales to fill the slide. See issue #25303. - * @param {string} file Path of the slide SVG. - * @param {number} page Page number (for logging). - */ -function ensureSlideViewBox(file, page) { - try { - const svg = fs.readFileSync(file, {encoding: 'utf-8'}); - const svgTag = svg.match(/]*>/)?.[0]; - - if (!svgTag || /viewBox=/.test(svgTag)) return; - - const width = svgTag.match(/(? + // sized to the canvas. When the background is itself an SVG, CairoSVG only + // rescales it to that box if it is resolution-independent; slides carrying + // absolute units (e.g. width="720pt") keep their intrinsic size and render + // cropped into the top-left corner (issue #25303). Rasterizing the slide to + // a PNG first sidesteps this: a raster image always scales to the + // box. The helper ensures the slide has a viewBox (so slides missing one + // still fill the raster) and renders at the same resolution as the final + // SVG->PDF pass (toPx of the slide dims) so the background stays sharp. + let backgroundSlide = + `${dropbox}/slide${currentSlide.page}.${backgroundFormat}`; + if (backgroundFormat === 'svg') { - ensureSlideViewBox( - `${dropbox}/slide${currentSlide.page}.${backgroundFormat}`, - currentSlide.page, - ); + try { + backgroundSlide = rasterizeSlideBackground( + backgroundSlide, + `${dropbox}/slide${currentSlide.page}-bg.png`, + { + width: toPx(slideWidth), + height: toPx(slideHeight), + cairosvg: config.shared.cairosvg, + unsafe: config.process.cairoSVGUnsafeFlag, + }); + } catch (error) { + logger.error(`Rasterizing slide ${currentSlide.page} ` + + `failed for job ${jobId}: ${error.message}`); + statusUpdate.setError(); + } } // Add the image element canvas - .image(`file://${dropbox}/slide${currentSlide.page}.${backgroundFormat}`) + .image(`file://${backgroundSlide}`) .size(scaledWidth, scaledHeight); // Add a group element with class 'whiteboard' From 219a10dedd98191dff50213b326b69f0fbf340dd Mon Sep 17 00:00:00 2001 From: Claudio Promptoso Date: Thu, 2 Jul 2026 00:18:03 +0000 Subject: [PATCH 045/121] refactor(bbb-export-annotations): rasterize the already-validated slide path Pass the SVG path that was validated by fs.existsSync (svgBackgroundSlide) to rasterizeSlideBackground, instead of a separately-derived dropbox path that resolves to the same file. Reading the code now makes it evident which file feeds the raster without cross-referencing collector.js. Also reuse the bgImagePath base for the default and output paths. No functional change. --- bbb-export-annotations/workers/process.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bbb-export-annotations/workers/process.js b/bbb-export-annotations/workers/process.js index 924bc5f8b7bd..24f8f2c179c6 100644 --- a/bbb-export-annotations/workers/process.js +++ b/bbb-export-annotations/workers/process.js @@ -388,14 +388,15 @@ async function processPresentationAnnotations() { // box. The helper ensures the slide has a viewBox (so slides missing one // still fill the raster) and renders at the same resolution as the final // SVG->PDF pass (toPx of the slide dims) so the background stays sharp. - let backgroundSlide = - `${dropbox}/slide${currentSlide.page}.${backgroundFormat}`; + let backgroundSlide = `${bgImagePath}.${backgroundFormat}`; if (backgroundFormat === 'svg') { try { + // Rasterize the same SVG we validated above (svgBackgroundSlide), not + // the dropbox copy, so it is clear which file feeds the raster. backgroundSlide = rasterizeSlideBackground( - backgroundSlide, - `${dropbox}/slide${currentSlide.page}-bg.png`, + svgBackgroundSlide, + `${bgImagePath}-bg.png`, { width: toPx(slideWidth), height: toPx(slideHeight), From ca8f7e93270340894f135aaeabe9e3ca40f36314 Mon Sep 17 00:00:00 2001 From: Claudio Promptoso Date: Thu, 2 Jul 2026 00:54:23 +0000 Subject: [PATCH 046/121] refactor(bbb-export-annotations): move regression tests to a dedicated issue Drop the unit-test suite, the pngjs devDependency and the test script added in the previous commit. Unit-test coverage for bbb-export-annotations will be proposed in a separate issue and picked up in its own session, keeping this PR focused on the fix only. --- bbb-export-annotations/package-lock.json | 19 +- bbb-export-annotations/package.json | 4 +- .../tests/slide-background.test.js | 226 ------------------ 3 files changed, 2 insertions(+), 247 deletions(-) delete mode 100644 bbb-export-annotations/tests/slide-background.test.js diff --git a/bbb-export-annotations/package-lock.json b/bbb-export-annotations/package-lock.json index bf9c9969c04c..ea471038a78c 100644 --- a/bbb-export-annotations/package-lock.json +++ b/bbb-export-annotations/package-lock.json @@ -20,8 +20,7 @@ }, "devDependencies": { "eslint": "^8.20.0", - "eslint-config-google": "^0.14.0", - "pngjs": "^7.0.0" + "eslint-config-google": "^0.14.0" }, "engines": { "node": ">=18.16.0", @@ -1221,16 +1220,6 @@ "integrity": "sha512-eh31l019WICQ03pkF3FSzHxB8n07ItqIQ++G5UV8JX0zVOXzgTGCqnRR0jJ2h9U8/2uW4W4mtGJELt9kEV0CFQ==", "license": "MIT" }, - "node_modules/pngjs": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", - "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.19.0" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -2406,12 +2395,6 @@ "resolved": "https://registry.npmjs.org/perfect-freehand/-/perfect-freehand-1.2.2.tgz", "integrity": "sha512-eh31l019WICQ03pkF3FSzHxB8n07ItqIQ++G5UV8JX0zVOXzgTGCqnRR0jJ2h9U8/2uW4W4mtGJELt9kEV0CFQ==" }, - "pngjs": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", - "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", - "dev": true - }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/bbb-export-annotations/package.json b/bbb-export-annotations/package.json index 1c4a35f8f380..7bba016706c1 100644 --- a/bbb-export-annotations/package.json +++ b/bbb-export-annotations/package.json @@ -4,7 +4,6 @@ "description": "BigBlueButton's Presentation Annotation Exporter", "scripts": { "start": "node master.js", - "test": "node --test tests/", "lint:fix": "eslint --fix '**/*.js' --ignore-pattern 'node_modules/'", "lint": "eslint '**/*.js' --ignore-pattern 'node_modules/'" }, @@ -21,8 +20,7 @@ }, "devDependencies": { "eslint": "^8.20.0", - "eslint-config-google": "^0.14.0", - "pngjs": "^7.0.0" + "eslint-config-google": "^0.14.0" }, "engines": { "node": ">=18.16.0", diff --git a/bbb-export-annotations/tests/slide-background.test.js b/bbb-export-annotations/tests/slide-background.test.js deleted file mode 100644 index eb99a161c567..000000000000 --- a/bbb-export-annotations/tests/slide-background.test.js +++ /dev/null @@ -1,226 +0,0 @@ -import {test, before, after} from 'node:test'; -import assert from 'node:assert/strict'; -import fs from 'fs'; -import os from 'os'; -import path from 'path'; -import {PNG} from 'pngjs'; -import { - ensureSlideViewBox, - largestEmbeddedRasterWidth, - rasterizeSlideBackground, -} from '../lib/utils/slide-background.js'; - -/** - * Build a base64 data URI for a solid-colour PNG of the given size. - * @param {number} w Width. - * @param {number} h Height. - * @param {number[]} rgb Colour as [r, g, b]. - * @return {string} A `data:image/png;base64,...` URI. - */ -function pngDataUri(w, h, [r, g, b]) { - const png = new PNG({width: w, height: h}); - for (let i = 0; i < png.data.length; i += 4) { - png.data[i] = r; - png.data[i + 1] = g; - png.data[i + 2] = b; - png.data[i + 3] = 255; - } - return 'data:image/png;base64,' + PNG.sync.write(png).toString('base64'); -} - -const config = JSON.parse(fs.readFileSync('./config/settings.json', 'utf8')); -const cairosvg = config.shared.cairosvg; - -// Slide body: the whole slide area (720 x 405 user units) painted solid blue, -// so any exported pixel that is not blue means the slide did not fill the box. -const SLIDE_BODY = - '\n'; - -// A slide that carries absolute units (720pt) AND a viewBox. This is the shape -// of the reporter's ODF slide (issue #25303): when referenced from an , -// CairoSVG renders it at its intrinsic pixel size and crops it into the -// top-left corner - the gap PR #25315 could not reach. -const SLIDE_WITH_VIEWBOX = - '\n' + - '\n' + - SLIDE_BODY + '\n'; - -// The same slide with NO viewBox: the case PR #25315 introduced ensureSlideVie- -// wBox for. Without a viewBox the content has no mapping to the canvas and -// rasterizing alone would crop it, so ensureSlideViewBox must still run. -const SLIDE_WITHOUT_VIEWBOX = - '\n' + - '\n' + - SLIDE_BODY + '\n'; - -// Slide geometry in tldraw coordinates (a 16:9 slide) and the values process.js -// derives from it. See workers/process.js. -const SLIDE_WIDTH = 1440; -const SLIDE_HEIGHT = 810; - -/** - * Converts points to pixels exactly as process.js does. - * @param {number} pt Measurement in points. - * @return {number} Measurement in pixels. - */ -function toPx(pt) { - return (pt / config.process.pointsPerInch) * config.process.pixelsPerInch; -} - -const OUTPUT_WIDTH = toPx(SLIDE_WIDTH); // 1920 -const OUTPUT_HEIGHT = toPx(SLIDE_HEIGHT); // 1080 - -// The (smaller) box the composite canvas is sized to. The fix must NOT use -// these for the rasterization or the background would be upscaled and blurry. -const maxImageWidth = config.process.maxImageWidth; -const maxImageHeight = config.process.maxImageHeight; -const ratio = Math.min(maxImageWidth / SLIDE_WIDTH, - maxImageHeight / SLIDE_HEIGHT); -const scaledWidth = SLIDE_WIDTH * ratio; - -let workdir; - -before(() => { - workdir = fs.mkdtempSync(path.join(os.tmpdir(), 'bbb-export-test-')); -}); - -after(() => { - fs.rmSync(workdir, {recursive: true, force: true}); -}); - -/** - * Returns the RGBA components of a single pixel. - * @param {PNG} png Decoded image. - * @param {number} x Column. - * @param {number} y Row. - * @return {number[]} [r, g, b, a]. - */ -function pixel(png, x, y) { - const idx = (png.width * y + x) << 2; - return [png.data[idx], png.data[idx + 1], - png.data[idx + 2], png.data[idx + 3]]; -} - -/** - * Rasterizes a slide SVG and asserts the resulting PNG is the final render - * resolution and covered edge to edge with the opaque blue of the slide. A - * full-resolution raster covering its whole area is what guarantees the - * composited slide fills the box on any CairoSVG version. - * @param {string} svg Slide SVG contents. - * @param {string} tag Unique file suffix. - */ -function assertFillsBox(svg, tag) { - const svgPath = path.join(workdir, `slide-${tag}.svg`); - fs.writeFileSync(svgPath, svg); - - const png = rasterizeSlideBackground( - svgPath, path.join(workdir, `slide-${tag}.png`), - {width: OUTPUT_WIDTH, height: OUTPUT_HEIGHT, cairosvg}); - - const image = PNG.sync.read(fs.readFileSync(png)); - - // Must match the SVG->PDF pass (toPx), not the smaller composite box, so the - // background is never upscaled from a low-resolution raster. - assert.equal(image.width, OUTPUT_WIDTH); - assert.equal(image.height, OUTPUT_HEIGHT); - assert.notEqual(image.width, scaledWidth); - - const w = image.width; - const h = image.height; - const samples = [ - [1, 1], [w - 2, 1], [1, h - 2], [w - 2, h - 2], - [w >> 1, 1], [w >> 1, h - 2], [1, h >> 1], [w - 2, h >> 1], - [w >> 1, h >> 1], - ]; - - for (const [x, y] of samples) { - const [r, g, b, a] = pixel(image, x, y); - assert.ok(a > 250, `pixel (${x},${y}) is transparent (uncovered)`); - assert.ok(b > 200 && r < 50 && g < 50, - `pixel (${x},${y}) is not the slide blue: rgba(${r},${g},${b},${a})`); - } -} - -test('slide with a viewBox rasterizes filling the whole box (issue #25303)', - () => assertFillsBox(SLIDE_WITH_VIEWBOX, 'vb')); - -test('slide without a viewBox still fills the box (no #25315 regression)', - () => assertFillsBox(SLIDE_WITHOUT_VIEWBOX, 'novb')); - -test('ensureSlideViewBox derives a viewBox only when one is missing', () => { - const missing = path.join(workdir, 'missing.svg'); - fs.writeFileSync(missing, SLIDE_WITHOUT_VIEWBOX); - ensureSlideViewBox(missing); - assert.match(fs.readFileSync(missing, 'utf8'), - /viewBox="0 0 720 405"/); - - // A slide that already has a viewBox is left untouched. - const present = path.join(workdir, 'present.svg'); - fs.writeFileSync(present, SLIDE_WITH_VIEWBOX); - ensureSlideViewBox(present); - assert.equal(fs.readFileSync(present, 'utf8'), SLIDE_WITH_VIEWBOX); -}); - -/** - * A slide shaped like an office-document export: a small (720pt) viewBox whose - * entire area is a single high-resolution embedded raster (2048px wide, scaled - * down into the viewBox). CairoSVG < 2.7 crops this when asked to downscale the - * raster below its native size, which is what issue #25303 reports. - * @return {string} The slide SVG. - */ -function slideWithRaster() { - const uri = pngDataUri(2048, 1152, [0, 0, 255]); - return '\n' + - '\n' + - `` + - '\n\n`; -} - -test('reads the largest embedded raster width', () => { - const svgPath = path.join(workdir, 'raster.svg'); - fs.writeFileSync(svgPath, slideWithRaster()); - assert.equal(largestEmbeddedRasterWidth(svgPath), 2048); - - const vector = path.join(workdir, 'vector.svg'); - fs.writeFileSync(vector, SLIDE_WITH_VIEWBOX); - assert.equal(largestEmbeddedRasterWidth(vector), 0); -}); - -test('renders at the embedded raster resolution when it exceeds the target', - () => { - const svgPath = path.join(workdir, 'raster.svg'); - fs.writeFileSync(svgPath, slideWithRaster()); - - // Target is the small final resolution; the raster (2048px) is larger, so - // the slide must be rendered at 2048 to avoid CairoSVG's downscale crop. - const png = rasterizeSlideBackground( - svgPath, path.join(workdir, 'raster.png'), - {width: 960, height: 540, cairosvg}); - const image = PNG.sync.read(fs.readFileSync(png)); - assert.equal(image.width, 2048); - assert.equal(image.height, Math.round(2048 * 540 / 960)); - - // Still fills to the corners with the embedded blue (sample just inside - // the edge to avoid the anti-aliased border pixel). - for (const [x, y] of [[5, 5], [image.width - 6, image.height - 6]]) { - const [r, g, b, a] = pixel(image, x, y); - assert.ok(a > 250 && b > 200 && r < 50 && g < 50, - `corner (${x},${y}) not filled: rgba(${r},${g},${b},${a})`); - } - }); - -test('throws when CairoSVG cannot produce the raster', () => { - const svgPath = path.join(workdir, 'err.svg'); - fs.writeFileSync(svgPath, SLIDE_WITH_VIEWBOX); - - assert.throws(() => rasterizeSlideBackground( - svgPath, path.join(workdir, 'err.png'), - {width: OUTPUT_WIDTH, height: OUTPUT_HEIGHT, cairosvg: '/nonexistent'})); -}); From 52c89ece78d0697331aad31730703aac550702dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Thu, 2 Jul 2026 08:30:41 -0300 Subject: [PATCH 047/121] update bbb-shared-notes-server dependencies --- bbb-shared-notes-server/package-lock.json | 246 +++++++++++----------- 1 file changed, 128 insertions(+), 118 deletions(-) diff --git a/bbb-shared-notes-server/package-lock.json b/bbb-shared-notes-server/package-lock.json index 5d1fc2c934b1..fa0f177b9b80 100644 --- a/bbb-shared-notes-server/package-lock.json +++ b/bbb-shared-notes-server/package-lock.json @@ -3215,16 +3215,16 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -3430,9 +3430,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -3654,9 +3654,19 @@ } }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -5453,9 +5463,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -5470,9 +5480,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/android-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -5487,9 +5497,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/android-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -5504,9 +5514,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/android-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -5521,9 +5531,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -5538,9 +5548,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -5555,9 +5565,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -5572,9 +5582,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -5589,9 +5599,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -5606,9 +5616,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -5623,9 +5633,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -5640,9 +5650,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -5657,9 +5667,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -5674,9 +5684,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -5691,9 +5701,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -5708,9 +5718,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -5725,9 +5735,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -5742,9 +5752,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -5759,9 +5769,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -5776,9 +5786,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -5793,9 +5803,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -5810,9 +5820,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -5827,9 +5837,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/sunos-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -5844,9 +5854,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/win32-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -5861,9 +5871,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/win32-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -5878,9 +5888,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/win32-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", - "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -5895,9 +5905,9 @@ } }, "node_modules/tsx/node_modules/esbuild": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", - "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5908,32 +5918,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.0", - "@esbuild/android-arm": "0.28.0", - "@esbuild/android-arm64": "0.28.0", - "@esbuild/android-x64": "0.28.0", - "@esbuild/darwin-arm64": "0.28.0", - "@esbuild/darwin-x64": "0.28.0", - "@esbuild/freebsd-arm64": "0.28.0", - "@esbuild/freebsd-x64": "0.28.0", - "@esbuild/linux-arm": "0.28.0", - "@esbuild/linux-arm64": "0.28.0", - "@esbuild/linux-ia32": "0.28.0", - "@esbuild/linux-loong64": "0.28.0", - "@esbuild/linux-mips64el": "0.28.0", - "@esbuild/linux-ppc64": "0.28.0", - "@esbuild/linux-riscv64": "0.28.0", - "@esbuild/linux-s390x": "0.28.0", - "@esbuild/linux-x64": "0.28.0", - "@esbuild/netbsd-arm64": "0.28.0", - "@esbuild/netbsd-x64": "0.28.0", - "@esbuild/openbsd-arm64": "0.28.0", - "@esbuild/openbsd-x64": "0.28.0", - "@esbuild/openharmony-arm64": "0.28.0", - "@esbuild/sunos-x64": "0.28.0", - "@esbuild/win32-arm64": "0.28.0", - "@esbuild/win32-ia32": "0.28.0", - "@esbuild/win32-x64": "0.28.0" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/twemoji": { From 1f7bcb8b817e20ed68810eb3097b59141cb017c0 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Fri, 3 Jul 2026 09:26:11 -0300 Subject: [PATCH 048/121] [update-sdk-version-0.0.103] update SDK version to 0.0.103 --- bigbluebutton-html5/package-lock.json | 8 ++++---- bigbluebutton-html5/package.json | 2 +- .../grails-app/conf/bigbluebutton.properties | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bigbluebutton-html5/package-lock.json b/bigbluebutton-html5/package-lock.json index 04b2a9dbf470..1d87546d92ca 100644 --- a/bigbluebutton-html5/package-lock.json +++ b/bigbluebutton-html5/package-lock.json @@ -36,7 +36,7 @@ "@types/react": "^18.2.18", "autoprefixer": "^10.4.4", "babel-runtime": "~6.26.0", - "bigbluebutton-html-plugin-sdk": "0.0.102", + "bigbluebutton-html-plugin-sdk": "0.0.103", "bowser": "^2.12.0", "browser-bunyan": "^1.8.0", "classnames": "^2.2.6", @@ -6804,9 +6804,9 @@ } }, "node_modules/bigbluebutton-html-plugin-sdk": { - "version": "0.0.102", - "resolved": "https://registry.npmjs.org/bigbluebutton-html-plugin-sdk/-/bigbluebutton-html-plugin-sdk-0.0.102.tgz", - "integrity": "sha512-6Bkgk6faaVlQdsKIEGBk93+QDOtKOoO6sgmu9GeY4vdJcILTam9A2mSKKYH+tC3FBYPb37MKveuARWn5oxtsdQ==", + "version": "0.0.103", + "resolved": "https://registry.npmjs.org/bigbluebutton-html-plugin-sdk/-/bigbluebutton-html-plugin-sdk-0.0.103.tgz", + "integrity": "sha512-Nl7tuc/voIdLLIR/in987T+wFYNwdmy7EUzB3GO4vyo4r6etvwRdKbOk44ozGvfgBOcKmmH/Ogn78VU5JiA30Q==", "license": "LGPL-3.0", "dependencies": { "@apollo/client": "^3.8.7", diff --git a/bigbluebutton-html5/package.json b/bigbluebutton-html5/package.json index 5445f7288d4d..208038d4bbc9 100644 --- a/bigbluebutton-html5/package.json +++ b/bigbluebutton-html5/package.json @@ -79,7 +79,7 @@ "@types/react": "^18.2.18", "autoprefixer": "^10.4.4", "babel-runtime": "~6.26.0", - "bigbluebutton-html-plugin-sdk": "0.0.102", + "bigbluebutton-html-plugin-sdk": "0.0.103", "bowser": "^2.12.0", "browser-bunyan": "^1.8.0", "classnames": "^2.2.6", diff --git a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties index 27444e55f33b..543c233d0cb7 100644 --- a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties +++ b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties @@ -599,4 +599,4 @@ pluginManifestCacheRefreshIntervalMinutes=60 # List the parameters without including the 'userdata-' prefix getJoinUrlUserdataBlocklist=bbb_record_permission,bbb_record_video,bbb_fullaudio_bridge,bbb_transparent_listen_only,bbb_multi_user_pen_only,bbb_presenter_tools,bbb_multi_user_tools -html5PluginSdkVersion=0.0.102 +html5PluginSdkVersion=0.0.103 From b24d47d9219b04ce5e2eb3fd1a6c25cc3dd46c69 Mon Sep 17 00:00:00 2001 From: germanocaumo Date: Tue, 2 Jun 2026 13:11:49 -0300 Subject: [PATCH 049/121] feat(plugins): support configurable button styles Read optional style props from plugin-provided nav bar, actions bar, and presentation toolbar buttons. The new color, circle, hideLabel, size, and style fields allow plugins to control the rendered button shape while preserving the previous defaults when older SDK/plugin objects do not provide them. --- .../imports/ui/components/actions-bar/component.jsx | 8 +++++--- .../imports/ui/components/nav-bar/component.jsx | 8 ++++++-- .../presentation/presentation-toolbar/component.jsx | 7 ++++--- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx index 43f39703d6a6..b27fa24dd451 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx @@ -47,11 +47,13 @@ class ActionsBar extends PureComponent { buttonProps = { key: `${plugin.type}-${plugin.id}`, onClick: plugin.onClick, - hideLabel: true, + hideLabel: plugin.hideLabel !== false, color: plugin.color || 'primary', - size: 'lg', - circle: true, + size: plugin.size || 'lg', + circle: plugin.circle !== false, + style: plugin.style, label: plugin.tooltip, + tooltipLabel: plugin.tooltip, dataTest: plugin.dataTest, }; if (typeof plugin?.icon === 'string') { diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx index 6b77539a0fb9..36c9c7f97cc5 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx @@ -101,8 +101,12 @@ const renderPluginItems = (pluginItems) => { disabled={pluginItem.disabled} label={pluginItem.label} aria-label={pluginItem.tooltip} - color="primary" - tooltip={pluginItem.tooltip} + color={pluginItem.color || 'primary'} + circle={pluginItem.circle !== false} + hideLabel={pluginItem.hideLabel !== false} + size={pluginItem.size || 'sm'} + style={pluginItem.style} + tooltipLabel={pluginItem.tooltip} onClick={pluginItem.onClick} dataTest={pluginItem.dataTest} {...navBarIconProps} diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx index 4a41cb30cfe9..56f89a7c600d 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx @@ -252,6 +252,10 @@ class PresentationToolbar extends PureComponent { key={ppbId} style={{ marginLeft: '2px', ...ppb.style }} label={ppb.label} + color={ppb.color || 'default'} + circle={ppb.circle === true} + hideLabel={ppb.hideLabel === true} + size={ppb.size || 'md'} onClick={ppb.onClick} tooltipLabel={ppb.tooltip} dataTest={ppb.dataTest} @@ -343,9 +347,6 @@ class PresentationToolbar extends PureComponent { allowInfiniteWhiteboard, allowInfiniteWhiteboardInBreakouts, infiniteWhiteboardIcon, - resetSlide, - zoomChanger, - tldrawAPI, maxNumberOfActiveUsers, numberOfJoinedUsers, } = this.props; From fce50d560084445a57a150c0cc04b7758a4cede5 Mon Sep 17 00:00:00 2001 From: germanocaumo Date: Thu, 4 Jun 2026 07:18:42 -0300 Subject: [PATCH 050/121] fix(plugins): preserve nav bar plugin button defaults Keep nav bar plugin buttons backward-compatible when older plugins do not provide the new style props. Default circle and hideLabel to false, and size to md, matching the previous Button defaults while still allowing plugins to opt in to custom styling. --- .../imports/ui/components/nav-bar/component.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx index 36c9c7f97cc5..825e4afddf02 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx @@ -102,9 +102,9 @@ const renderPluginItems = (pluginItems) => { label={pluginItem.label} aria-label={pluginItem.tooltip} color={pluginItem.color || 'primary'} - circle={pluginItem.circle !== false} - hideLabel={pluginItem.hideLabel !== false} - size={pluginItem.size || 'sm'} + circle={pluginItem.circle === true} + hideLabel={pluginItem.hideLabel === true} + size={pluginItem.size || 'md'} style={pluginItem.style} tooltipLabel={pluginItem.tooltip} onClick={pluginItem.onClick} From e3984d6256dd5808e4ab2a90d04f15d40a0f80a8 Mon Sep 17 00:00:00 2001 From: germanocaumo Date: Mon, 8 Jun 2026 08:44:02 -0300 Subject: [PATCH 051/121] fix(plugins): use action bar button labels Use plugin.label as the visible text for plugin-provided action bar buttons, falling back to plugin.tooltip for older plugin objects. Keep tooltipLabel sourced from plugin.tooltip so visible labels and hover text can be configured independently. --- .../imports/ui/components/actions-bar/component.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx index b27fa24dd451..0fe1ae5a29a2 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx @@ -52,7 +52,7 @@ class ActionsBar extends PureComponent { size: plugin.size || 'lg', circle: plugin.circle !== false, style: plugin.style, - label: plugin.tooltip, + label: plugin.label || plugin.tooltip, tooltipLabel: plugin.tooltip, dataTest: plugin.dataTest, }; From 05b600d979b9b3054addf0505c13834ad8bd549c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:45:52 -0400 Subject: [PATCH 052/121] build(deps): bump ch.qos.logback:logback-core in /bbb-recording-imex (#25368) Bumps [ch.qos.logback:logback-core](https://github.com/qos-ch/logback) from 1.5.25 to 1.5.33. - [Release notes](https://github.com/qos-ch/logback/releases) - [Commits](https://github.com/qos-ch/logback/compare/v_1.5.25...v_1.5.33) --- updated-dependencies: - dependency-name: ch.qos.logback:logback-core dependency-version: 1.5.33 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 ad4679651d05..297408fbc042 100644 --- a/bbb-recording-imex/pom.xml +++ b/bbb-recording-imex/pom.xml @@ -75,7 +75,7 @@ ch.qos.logback logback-core - 1.5.25 + 1.5.33 org.slf4j From c18d465cbf21a0ae918ced59d68deb38dc5363b7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:47:11 -0400 Subject: [PATCH 053/121] build(deps): bump js-yaml in /bbb-learning-dashboard (#25369) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.14.2 to 3.15.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/3.14.2...3.15.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 3.15.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- bbb-learning-dashboard/package-lock.json | 36 +++++++++++++++--------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/bbb-learning-dashboard/package-lock.json b/bbb-learning-dashboard/package-lock.json index 62152c23a88e..5d26b2d7e70f 100644 --- a/bbb-learning-dashboard/package-lock.json +++ b/bbb-learning-dashboard/package-lock.json @@ -11014,10 +11014,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", - "license": "MIT", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", + "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -14597,10 +14596,19 @@ } }, "node_modules/react-scripts/node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "license": "MIT", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "dependencies": { "argparse": "^2.0.1" }, @@ -24385,9 +24393,9 @@ "version": "4.0.0" }, "js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", + "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -26368,9 +26376,9 @@ "version": "5.2.0" }, "js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "requires": { "argparse": "^2.0.1" } From fd847d84ec4fbfe07fc44a32a7d2bfba2dc4fa5e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:47:45 -0400 Subject: [PATCH 054/121] build(deps-dev): bump js-yaml in /bigbluebutton-tests/playwright (#25358) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.14.2 to 3.15.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/3.14.2...3.15.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 3.15.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../playwright/package-lock.json | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/bigbluebutton-tests/playwright/package-lock.json b/bigbluebutton-tests/playwright/package-lock.json index 06e2d9f5f9d7..9ac86627ec26 100644 --- a/bigbluebutton-tests/playwright/package-lock.json +++ b/bigbluebutton-tests/playwright/package-lock.json @@ -1234,9 +1234,9 @@ } }, "node_modules/cosmiconfig/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", + "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", "dev": true, "license": "MIT", "dependencies": { @@ -3338,10 +3338,20 @@ "license": "ISC" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" From 6a12bd767e4394640581a2c37d2d445efddac422 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:49:37 -0400 Subject: [PATCH 055/121] build(deps): bump concurrent-ruby in /record-and-playback/core (#25307) Bumps [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby) from 1.3.4 to 1.3.7. - [Release notes](https://github.com/ruby-concurrency/concurrent-ruby/releases) - [Changelog](https://github.com/ruby-concurrency/concurrent-ruby/blob/master/CHANGELOG.md) - [Commits](https://github.com/ruby-concurrency/concurrent-ruby/compare/v1.3.4...v1.3.7) --- updated-dependencies: - dependency-name: concurrent-ruby dependency-version: 1.3.7 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- record-and-playback/core/Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/record-and-playback/core/Gemfile.lock b/record-and-playback/core/Gemfile.lock index e8962a781ac6..dd1fae529ec8 100644 --- a/record-and-playback/core/Gemfile.lock +++ b/record-and-playback/core/Gemfile.lock @@ -13,7 +13,7 @@ GEM csv rexml builder (3.2.4) - concurrent-ruby (1.3.4) + concurrent-ruby (1.3.7) crass (1.0.6) csv (3.3.5) fastimage (2.2.6) From ca46266d4f9a8ce9207fc831f476388e2f7392d3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:50:46 -0400 Subject: [PATCH 056/121] build(deps): bump launch-editor in /bbb-learning-dashboard (#25295) Bumps [launch-editor](https://github.com/vitejs/launch-editor) from 2.11.1 to 2.14.1. - [Commits](https://github.com/vitejs/launch-editor/compare/v2.11.1...v2.14.1) --- updated-dependencies: - dependency-name: launch-editor dependency-version: 2.14.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- bbb-learning-dashboard/package-lock.json | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/bbb-learning-dashboard/package-lock.json b/bbb-learning-dashboard/package-lock.json index 5d26b2d7e70f..979e2560f90d 100644 --- a/bbb-learning-dashboard/package-lock.json +++ b/bbb-learning-dashboard/package-lock.json @@ -11179,13 +11179,12 @@ } }, "node_modules/launch-editor": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.11.1.tgz", - "integrity": "sha512-SEET7oNfgSaB6Ym0jufAdCeo3meJVeCaaDyzRygy0xsp2BFKCprcfHljTq4QkzTLUxEKkFK6OK4811YM2oSrRg==", - "license": "MIT", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz", + "integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==", "dependencies": { "picocolors": "^1.1.1", - "shell-quote": "^1.8.3" + "shell-quote": "^1.8.4" } }, "node_modules/leven": { @@ -24496,12 +24495,12 @@ } }, "launch-editor": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.11.1.tgz", - "integrity": "sha512-SEET7oNfgSaB6Ym0jufAdCeo3meJVeCaaDyzRygy0xsp2BFKCprcfHljTq4QkzTLUxEKkFK6OK4811YM2oSrRg==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz", + "integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==", "requires": { "picocolors": "^1.1.1", - "shell-quote": "^1.8.3" + "shell-quote": "^1.8.4" } }, "leven": { From 36699d84a45b614a5b08f966126e8a72a3d1fc51 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:02:36 -0400 Subject: [PATCH 057/121] build(deps): bump launch-editor from 2.12.0 to 2.14.1 in /docs (#25296) Bumps [launch-editor](https://github.com/vitejs/launch-editor) from 2.12.0 to 2.14.1. - [Commits](https://github.com/vitejs/launch-editor/compare/v2.12.0...v2.14.1) --- updated-dependencies: - dependency-name: launch-editor dependency-version: 2.14.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/package-lock.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index 82223342ef25..2b607bf7ec32 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -11593,13 +11593,13 @@ } }, "node_modules/launch-editor": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz", - "integrity": "sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz", + "integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==", "license": "MIT", "dependencies": { "picocolors": "^1.1.1", - "shell-quote": "^1.8.3" + "shell-quote": "^1.8.4" } }, "node_modules/layout-base": { From d6bd7faa336c5743283e300c1cfc9758572d253f Mon Sep 17 00:00:00 2001 From: Stefan Auditor Date: Mon, 6 Jul 2026 20:11:28 +0200 Subject: [PATCH 058/121] fix(recording): Ruby 3.2 File.exists? deprecation in BigBlueButton recording sanity worker (#25338) * Fix Ruby 3.2 File.exists? deprecation in BigBlueButton recording sanity worker * Fixes wording * Fixes code style nitpicks --- record-and-playback/core/scripts/sanity/sanity.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/record-and-playback/core/scripts/sanity/sanity.rb b/record-and-playback/core/scripts/sanity/sanity.rb index c9080d7526e0..36897879a5d2 100755 --- a/record-and-playback/core/scripts/sanity/sanity.rb +++ b/record-and-playback/core/scripts/sanity/sanity.rb @@ -50,7 +50,8 @@ def check_events_xml(raw_dir,meeting_id) filepath = "#{raw_dir}/#{meeting_id}/events.xml" - raise Exception, "Events file doesn't exists." if not File.exists?(filepath) + raise Exception, "Events file doesn't exist." unless File.exist?(filepath) + bad_doc = Nokogiri::XML(File.open(filepath)) { |config| config.options = Nokogiri::XML::ParseOptions::STRICT } end From 082d9897279e88adc8fdcbf9333ef7b1170d2aea Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Tue, 7 Jul 2026 03:44:03 +0900 Subject: [PATCH 059/121] Update deploy.sh (#25372) --- akka-bbb-apps/deploy.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/akka-bbb-apps/deploy.sh b/akka-bbb-apps/deploy.sh index 219884fc5f38..a4484b6c09fd 100755 --- a/akka-bbb-apps/deploy.sh +++ b/akka-bbb-apps/deploy.sh @@ -9,5 +9,7 @@ echo '' echo '----------------' echo 'bbb-apps-akka updated' +sudo systemctl enable bbb-apps-akka.service + sudo service bbb-apps-akka start echo 'starting service bbb-apps-akka' From ddeb4f337ed2b5af4ad50fbd58a83420b044f374 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 16:47:00 -0400 Subject: [PATCH 060/121] build(deps): bump jwt from 2.5.0 to 2.10.3 in /record-and-playback/core (#25218) Bumps [jwt](https://github.com/jwt/ruby-jwt) from 2.5.0 to 2.10.3. - [Release notes](https://github.com/jwt/ruby-jwt/releases) - [Changelog](https://github.com/jwt/ruby-jwt/blob/main/CHANGELOG.md) - [Commits](https://github.com/jwt/ruby-jwt/compare/v2.5.0...v2.10.3) --- updated-dependencies: - dependency-name: jwt dependency-version: 2.10.3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- record-and-playback/core/Gemfile | 2 +- record-and-playback/core/Gemfile.lock | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/record-and-playback/core/Gemfile b/record-and-playback/core/Gemfile index 38f5cab75d4c..bda34f4ca784 100644 --- a/record-and-playback/core/Gemfile +++ b/record-and-playback/core/Gemfile @@ -23,7 +23,7 @@ gem 'builder', '~> 3.2' gem 'fastimage', '~> 2.1' gem 'java_properties' gem 'journald-logger', '~> 3.0' -gem 'jwt', '~> 2.2' +gem 'jwt', '~> 2.10' gem 'locale', '~> 2.1' gem 'loofah', '~> 2.19.1' gem 'nokogiri', '~> 1.16.5' diff --git a/record-and-playback/core/Gemfile.lock b/record-and-playback/core/Gemfile.lock index dd1fae529ec8..92618239cff3 100644 --- a/record-and-playback/core/Gemfile.lock +++ b/record-and-playback/core/Gemfile.lock @@ -8,6 +8,7 @@ GEM minitest (>= 5.1) tzinfo (~> 2.0) ast (2.4.2) + base64 (0.3.0) bbbevents (2.0.3) activesupport (>= 5.0.0.1, < 8) csv @@ -25,7 +26,8 @@ GEM journald-native (~> 1.0) journald-native (1.0.12) json (2.7.5) - jwt (2.5.0) + jwt (2.10.3) + base64 language_server-protocol (3.17.0.3) locale (2.1.3) loofah (2.19.1) @@ -99,7 +101,7 @@ DEPENDENCIES fastimage (~> 2.1) java_properties journald-logger (~> 3.0) - jwt (~> 2.2) + jwt (~> 2.10) locale (~> 2.1) loofah (~> 2.19.1) minitest (~> 5.14.1) From 9ffa172438a1af15ad78df028630bb00ce56edf4 Mon Sep 17 00:00:00 2001 From: Claudio Promptoso Date: Mon, 6 Jul 2026 04:18:43 +0000 Subject: [PATCH 061/121] feat(shared-notes): markdown export, init-from-markdown, and import modal Add three Markdown capabilities to BlockNote shared notes: - Export as Markdown: new kebab item mirroring Export as PDF, hitting the existing /export/md document endpoint. - Init from Markdown on create: new sharedNotesInitialContentMarkdown and sharedNotesInitialContentMarkdownUrl create params, carried as a raw string through the web/akka pipeline and converted to blocks on the shared-notes server (where BlockNote is available) before seeding the empty document. - Import from Markdown: presenter-only kebab item opens a modal with a textarea that replaces the current notes client-side via editor.replaceBlocks, which propagates to every client through the shared Yjs document. Replacing a non-empty document requires an explicit confirmation. --- .../core/apps/pads/PadslHdlrHelpers.scala | 13 ++- .../core/running/MeetingActor.scala | 3 +- .../common2/domain/Meeting2x.scala | 1 + .../bigbluebutton/common2/msgs/PadsMsgs.scala | 2 +- .../java/org/bigbluebutton/api/ApiParams.java | 2 + .../org/bigbluebutton/api/MeetingService.java | 45 +++++++- .../api/ParamsProcessorUtil.java | 26 +++++ .../org/bigbluebutton/api/domain/Meeting.java | 37 ++++++ .../bigbluebutton/api2/IBbbWebApiGWApp.java | 1 + .../bigbluebutton/api2/BbbWebApiGWApp.scala | 2 + bbb-shared-notes-server/src/redis/handler.ts | 21 ++-- .../src/redis/service/pushInitialContent.ts | 51 ++++++++- .../components/bn-shared-notes/component.tsx | 10 ++ .../bn-shared-notes/import-context.tsx | 21 ++++ .../markdown-import-modal/component.tsx | 107 ++++++++++++++++++ .../markdown-import-modal/styles.ts | 39 +++++++ .../imports/ui/components/notes/component.tsx | 104 +++++++++-------- .../notes/notes-dropdown/component.tsx | 32 ++++++ bigbluebutton-html5/public/locales/en.json | 7 ++ .../web/controllers/ApiController.groovy | 4 + docs/docs/development/api.md | 29 +++++ 21 files changed, 488 insertions(+), 69 deletions(-) create mode 100644 bigbluebutton-html5/imports/ui/components/bn-shared-notes/import-context.tsx create mode 100644 bigbluebutton-html5/imports/ui/components/bn-shared-notes/markdown-import-modal/component.tsx create mode 100644 bigbluebutton-html5/imports/ui/components/bn-shared-notes/markdown-import-modal/styles.ts diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/pads/PadslHdlrHelpers.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/pads/PadslHdlrHelpers.scala index 3b776db2f637..a69298ba8708 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/pads/PadslHdlrHelpers.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/pads/PadslHdlrHelpers.scala @@ -30,16 +30,17 @@ object PadslHdlrHelpers { } def broadcastBNSharedNotesCreateCmdMsg( - outGW: OutMsgRouter, - meetingId: String, - externalId: String, - model: String, - sharedNotesInitialContentJson: Vector[AnyRef] + outGW: OutMsgRouter, + meetingId: String, + externalId: String, + model: String, + sharedNotesInitialContentJson: Vector[AnyRef], + sharedNotesInitialContentMarkdown: String ): Unit = { val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka") val envelope = BbbCoreEnvelope(BNSharedNotesCreateCmdMsg.NAME, routing) val header = BbbCoreHeaderWithMeetingId(BNSharedNotesCreateCmdMsg.NAME, meetingId) - val body = BNSharedNotesCreateCmdMsgBody(externalId, model, sharedNotesInitialContentJson) + val body = BNSharedNotesCreateCmdMsgBody(externalId, model, sharedNotesInitialContentJson, sharedNotesInitialContentMarkdown) val event = BNSharedNotesCreateCmdMsg(header, body) val msgEvent = BbbCommonEnvCoreMsg(envelope, event) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index 5976dc7ec7dd..9c8695f2a746 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -370,7 +370,8 @@ class MeetingActor( } else { PadslHdlrHelpers.broadcastBNSharedNotesCreateCmdMsg( outGW, liveMeeting.props.meetingProp.intId, - sharedNotesPadId, sharedNotesPadId, liveMeeting.props.meetingProp.sharedNotesInitialContentJson + sharedNotesPadId, sharedNotesPadId, liveMeeting.props.meetingProp.sharedNotesInitialContentJson, + liveMeeting.props.meetingProp.sharedNotesInitialContentMarkdown ) } } diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Meeting2x.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Meeting2x.scala index da06f1b58213..f0462bd4e7d8 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Meeting2x.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Meeting2x.scala @@ -14,6 +14,7 @@ case class MeetingProp( intId: String, meetingCameraCap: Int, sharedNotesInitialContentJson: Vector[AnyRef], + sharedNotesInitialContentMarkdown: String = "", sharedNotesEditor: String, maxPinnedCameras: Int, cameraBridge: String, diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PadsMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PadsMsgs.scala index f432eeba661c..1fe23260d9ae 100644 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PadsMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PadsMsgs.scala @@ -29,7 +29,7 @@ case class PadCreateReqMsgBody(externalId: String, name: String) // apps -> shared-notes-server object BNSharedNotesCreateCmdMsg { val NAME = "BNSharedNotesCreateCmdMsg" } case class BNSharedNotesCreateCmdMsg(header: BbbCoreHeaderWithMeetingId, body: BNSharedNotesCreateCmdMsgBody) extends BbbCoreMsg -case class BNSharedNotesCreateCmdMsgBody(externalId: String, model: String, initialContentJson: Vector[AnyRef]) +case class BNSharedNotesCreateCmdMsgBody(externalId: String, model: String, initialContentJson: Vector[AnyRef], initialContentMarkdown: String = "") // shared-notes-server -> apps object BNSharedNotesCreatedEvtMsg { val NAME = "BNSharedNotesCreatedEvtMsg" } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java index 67510cd7739f..ac235854fd62 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java @@ -24,6 +24,8 @@ public class ApiParams { public static final String ALLOW_START_STOP_RECORDING = "allowStartStopRecording"; public static final String SHARED_NOTES_EDITOR = "sharedNotesEditor"; public static final String SHARED_NOTES_INITIAL_CONTENT_JSON_URL = "sharedNotesInitialContentJsonUrl"; + public static final String SHARED_NOTES_INITIAL_CONTENT_MARKDOWN = "sharedNotesInitialContentMarkdown"; + public static final String SHARED_NOTES_INITIAL_CONTENT_MARKDOWN_URL = "sharedNotesInitialContentMarkdownUrl"; public static final String ATTENDEE_PW = "attendeePW"; public static final String AUTO_START_RECORDING = "autoStartRecording"; public static final String BANNER_COLOR = "bannerColor"; diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java index 8afcedf816b3..a518fa6b457f 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java @@ -437,6 +437,48 @@ public ArrayList parseSharedNotesInitialContent(String content) { return initialContent; } + public String getSharedNotesInitialContentMarkdown(Meeting m) { + String sharedNotesInitialContentMarkdownUrl = m.getSharedNotesInitialContentMarkdownUrl(); + + if (!sharedNotesInitialContentMarkdownUrl.isEmpty()) { + return requestSharedNotesInitialContentMarkdownFromUrl(m.getInternalId(), sharedNotesInitialContentMarkdownUrl); + } + + // Raw markdown can arrive either as a create param or, for larger content, in the POST + // body via the `sharedNotesInitialContentMarkdown` xml module. The create param wins when + // both are present; the payload is the fallback (mirrors sharedNotesInitialContentJson). + String markdownFromParam = m.getSharedNotesInitialContentMarkdown(); + if (markdownFromParam != null && !markdownFromParam.isEmpty()) { + return markdownFromParam; + } + + String markdownFromPayload = m.getSharedNotesInitialContentMarkdownFromPayload(); + return markdownFromPayload != null ? markdownFromPayload : ""; + } + + public String requestSharedNotesInitialContentMarkdownFromUrl(String meetingId, String initialContentMarkdownUrl) { + String initialContent = ""; + if (!initialContentMarkdownUrl.isEmpty()) { + try { + String finalInitialContentMarkdownUrl = redirectFollower.followRedirect( + meetingId, initialContentMarkdownUrl, 0, initialContentMarkdownUrl, sharedNotesRedirectValidator, 6000 + ); + + URL url = new URL(finalInitialContentMarkdownUrl); + try (BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()))) { + initialContent = in.lines().collect(Collectors.joining("\n")); + } + } catch (MalformedURLException e) { + log.error( + "Malformed URL for sharedNotesInitialContentMarkdownUrl: [{}]", initialContentMarkdownUrl); + } catch (IOException e) { + log.error( + "Something went wrong while processing [{}]. Error: {}", initialContentMarkdownUrl, e.getMessage()); + } + } + return initialContent; + } + public Map requestPluginManifests(Meeting m) { Map pluginsResult = new ConcurrentHashMap<>(); Map metadata = m.getMetadata(); @@ -568,6 +610,7 @@ public synchronized boolean createMeeting(Meeting m, Map plugins m.setPlugins(pluginsMap); m.setSharedNotesInitialContentJson(sharedNotesInitialContentMap); + m.setSharedNotesInitialContentMarkdown(getSharedNotesInitialContentMarkdown(m)); handle(new CreateMeeting(m)); return true; } @@ -645,7 +688,7 @@ private void handleCreateMeeting(Meeting m) { gw.createMeeting(m.getInternalId(), m.getExternalId(), m.getParentMeetingId(), m.getName(), m.isRecord(), m.getTelVoice(), m.getDuration(), m.getAutoStartRecording(), m.getAllowStartStopRecording(), - m.getSharedNotesInitialContentJson(), m.getSharedNotesEditor(), m.getRecordFullDurationMedia(), + m.getSharedNotesInitialContentJson(), m.getSharedNotesInitialContentMarkdown(), m.getSharedNotesEditor(), m.getRecordFullDurationMedia(), m.getWebcamsOnlyForModerator(), m.getMultiUserWhiteboardEnabled(), m.getMeetingCameraCap(), m.getUserCameraCap(), m.getMaxPinnedCameras(), m.getCameraBridge(), m.getScreenShareBridge(), diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java index 2e2668900e3e..9691fb783dbe 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java @@ -659,6 +659,30 @@ boolean record = processRecordMeeting(params.get(ApiParams.RECORD)); } } + String sharedNotesInitialContentMarkdown = ""; + if (!StringUtils.isEmpty(params.get(ApiParams.SHARED_NOTES_INITIAL_CONTENT_MARKDOWN))) { + try { + sharedNotesInitialContentMarkdown = params + .get(ApiParams.SHARED_NOTES_INITIAL_CONTENT_MARKDOWN); + } catch (Exception ex) { + log.warn( + "Invalid param [sharedNotesInitialContentMarkdown] for meeting=[{}]", + internalMeetingId); + } + } + + String sharedNotesInitialContentMarkdownUrl = ""; + if (!StringUtils.isEmpty(params.get(ApiParams.SHARED_NOTES_INITIAL_CONTENT_MARKDOWN_URL))) { + try { + sharedNotesInitialContentMarkdownUrl = params + .get(ApiParams.SHARED_NOTES_INITIAL_CONTENT_MARKDOWN_URL); + } catch (Exception ex) { + log.warn( + "Invalid param [sharedNotesInitialContentMarkdownUrl] for meeting=[{}]", + internalMeetingId); + } + } + String sharedNotesEditor = defaultSharedNotesEditor; if (!StringUtils.isEmpty(params.get(ApiParams.SHARED_NOTES_EDITOR))) { try { @@ -992,6 +1016,8 @@ boolean record = processRecordMeeting(params.get(ApiParams.RECORD)); .withAllowStartStopRecording(allowStartStoptRec) .withSharedNotesEditor(sharedNotesEditor) .withSharedNotesInitialContentJsonUrl(sharedNotesInitialContentJsonUrl) + .withSharedNotesInitialContentMarkdown(sharedNotesInitialContentMarkdown) + .withSharedNotesInitialContentMarkdownUrl(sharedNotesInitialContentMarkdownUrl) .withPresentationConversionCacheEnabled(presentationCacheEnabled) .withRecordFullDurationMedia(_recordFullDurationMedia) .withWebcamsOnlyForModerator(webcamsOnlyForMod) diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java index 00d47e0c58d5..117fc821c5fc 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java @@ -72,6 +72,8 @@ public class Meeting { private String sharedNotesEditor = "etherpad"; private String sharedNotesInitialContentJsonUrl = ""; private ArrayList sharedNotesInitialContentJson; + private String sharedNotesInitialContentMarkdownUrl = ""; + private String sharedNotesInitialContentMarkdown = ""; private boolean presentationConversionCacheEnabled = false; private boolean recordFullDurationMedia = false; private boolean haveRecordingMarks = false; @@ -135,6 +137,7 @@ public class Meeting { private String meetingEndedCallbackURL = ""; private String sharedNotesInitialContentJsonFromPayload; + private String sharedNotesInitialContentMarkdownFromPayload = ""; private String overrideClientSettings = ""; @@ -176,6 +179,8 @@ record = builder.record; allowStartStopRecording = builder.allowStartStopRecording; sharedNotesEditor = builder.sharedNotesEditor; sharedNotesInitialContentJsonUrl = builder.sharedNotesInitialContentJsonUrl; + sharedNotesInitialContentMarkdownUrl = builder.sharedNotesInitialContentMarkdownUrl; + sharedNotesInitialContentMarkdown = builder.sharedNotesInitialContentMarkdown; presentationConversionCacheEnabled = builder.presentationConversionCacheEnabled; recordFullDurationMedia = builder.recordFullDurationMedia; webcamsOnlyForModerator = builder.webcamsOnlyForModerator; @@ -696,6 +701,18 @@ public void setSharedNotesInitialContentJson(ArrayList initialContentJso sharedNotesInitialContentJson = initialContentJson; } + public String getSharedNotesInitialContentMarkdownUrl() { + return sharedNotesInitialContentMarkdownUrl; + } + + public String getSharedNotesInitialContentMarkdown() { + return sharedNotesInitialContentMarkdown; + } + + public void setSharedNotesInitialContentMarkdown(String initialContentMarkdown) { + sharedNotesInitialContentMarkdown = initialContentMarkdown; + } + public boolean isPresentationConversionCacheEnabled() { return presentationConversionCacheEnabled; } @@ -1011,6 +1028,14 @@ public String getSharedNotesInitialContentJsonFromPayload() { public void setSharedNotesInitialContentJsonFromPayload(String sharedNotesInitialContentJsonFromPayload) { this.sharedNotesInitialContentJsonFromPayload = sharedNotesInitialContentJsonFromPayload; } + + public String getSharedNotesInitialContentMarkdownFromPayload() { + return sharedNotesInitialContentMarkdownFromPayload; + } + + public void setSharedNotesInitialContentMarkdownFromPayload(String sharedNotesInitialContentMarkdownFromPayload) { + this.sharedNotesInitialContentMarkdownFromPayload = sharedNotesInitialContentMarkdownFromPayload; + } /*** * Meeting Builder @@ -1027,6 +1052,8 @@ public static class Builder { private boolean allowStartStopRecording; private String sharedNotesEditor; private String sharedNotesInitialContentJsonUrl; + private String sharedNotesInitialContentMarkdownUrl; + private String sharedNotesInitialContentMarkdown; private boolean presentationConversionCacheEnabled; private boolean webcamsOnlyForModerator; private boolean multiUserWhiteboardEnabled; @@ -1124,6 +1151,16 @@ public Builder withSharedNotesInitialContentJsonUrl(String initialContent) { return this; } + public Builder withSharedNotesInitialContentMarkdownUrl(String initialContent) { + this.sharedNotesInitialContentMarkdownUrl = initialContent; + return this; + } + + public Builder withSharedNotesInitialContentMarkdown(String initialContent) { + this.sharedNotesInitialContentMarkdown = initialContent; + return this; + } + public Builder withPresentationConversionCacheEnabled(boolean cacheEnabled) { this.presentationConversionCacheEnabled = cacheEnabled; return this; diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api2/IBbbWebApiGWApp.java b/bbb-common-web/src/main/java/org/bigbluebutton/api2/IBbbWebApiGWApp.java index f203d570b850..db363e5bd1f4 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api2/IBbbWebApiGWApp.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api2/IBbbWebApiGWApp.java @@ -21,6 +21,7 @@ void createMeeting(String meetingID, String externalMeetingID, String voiceBridge, Integer duration, Boolean autoStartRecording, Boolean allowStartStopRecording, ArrayList sharedNotesInitialContentJson, + String sharedNotesInitialContentMarkdown, String sharedNotesEditor, Boolean recordFullDurationMedia, Boolean webcamsOnlyForModerator, diff --git a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/BbbWebApiGWApp.scala b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/BbbWebApiGWApp.scala index 5193a75b599a..658b69a55d84 100755 --- a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/BbbWebApiGWApp.scala +++ b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/BbbWebApiGWApp.scala @@ -127,6 +127,7 @@ class BbbWebApiGWApp( autoStartRecording: java.lang.Boolean, allowStartStopRecording: java.lang.Boolean, sharedNotesInitialContentJson: java.util.ArrayList[AnyRef], + sharedNotesInitialContentMarkdown: java.lang.String, sharedNotesEditor: java.lang.String, recordFullDurationMedia: java.lang.Boolean, webcamsOnlyForModerator: java.lang.Boolean, @@ -188,6 +189,7 @@ class BbbWebApiGWApp( intId = meetingId, meetingCameraCap = meetingCameraCap.intValue(), sharedNotesInitialContentJson = sharedNotesInitialContentJsonVector, + sharedNotesInitialContentMarkdown = sharedNotesInitialContentMarkdown, sharedNotesEditor = sharedNotesEditor, maxPinnedCameras = maxPinnedCameras.intValue(), cameraBridge, diff --git a/bbb-shared-notes-server/src/redis/handler.ts b/bbb-shared-notes-server/src/redis/handler.ts index 7e7e35033780..97e7a18ecdb1 100644 --- a/bbb-shared-notes-server/src/redis/handler.ts +++ b/bbb-shared-notes-server/src/redis/handler.ts @@ -104,17 +104,19 @@ const handleSharedNotesCreate = async (header: MessageHeader, body: MessageBody) externalId, model, initialContentJson, + initialContentMarkdown, } = body; const padId = `${documentNamePrefix}${meetingId}`; - const validateInitialContentNotEmpty = (): boolean => { - return initialContentJson !== undefined - && initialContentJson !== null - && typeof initialContentJson === "object" - && Object.keys(initialContentJson).length > 0 - } - if (validateInitialContentNotEmpty()) { + const hasInitialContentJson = initialContentJson !== undefined + && initialContentJson !== null + && typeof initialContentJson === "object" + && Object.keys(initialContentJson).length > 0; + const hasInitialContentMarkdown = typeof initialContentMarkdown === "string" + && initialContentMarkdown.length > 0; + + if (hasInitialContentMarkdown || hasInitialContentJson) { logger.debug( 'Received initial content', { padId, @@ -122,7 +124,10 @@ const handleSharedNotesCreate = async (header: MessageHeader, body: MessageBody) } ); try { - const statusReturn = await pushInitialContent(padId, initialContentJson); + const statusReturn = await pushInitialContent(padId, { + initialContentJson: hasInitialContentJson ? initialContentJson : undefined, + initialContentMarkdown: hasInitialContentMarkdown ? initialContentMarkdown : undefined, + }); if (statusReturn.error) { logger.error('Error found, see details', { logCode: statusReturn.statusCode, diff --git a/bbb-shared-notes-server/src/redis/service/pushInitialContent.ts b/bbb-shared-notes-server/src/redis/service/pushInitialContent.ts index f05d2886992d..ac0f62af2993 100644 --- a/bbb-shared-notes-server/src/redis/service/pushInitialContent.ts +++ b/bbb-shared-notes-server/src/redis/service/pushInitialContent.ts @@ -4,10 +4,17 @@ import { Logger } from "../../common/logger"; const logger = new Logger('redis.service.pushInitialContent'); -export async function pushInitialContent(padId: string, initialContentJson: any): Promise<{ statusCode: string; error?: string; }> { +interface InitialContent { + // BlockNote blocks already parsed on the web tier (sharedNotesInitialContentJson). + initialContentJson?: any; + // Raw markdown carried untouched from the create API (sharedNotesInitialContentMarkdown); + // BlockNote only exists here, so the markdown -> blocks conversion happens on this tier. + initialContentMarkdown?: string; +} + +export async function pushInitialContent(padId: string, content: InitialContent): Promise<{ statusCode: string; error?: string; }> { const documentName = padId; - const initialBlocks = initialContentJson; let connection: Awaited> | null = null; try { connection = await hocuspocus.openDirectConnection(documentName); @@ -34,12 +41,44 @@ export async function pushInitialContent(padId: string, initialContentJson: any) // Create a ServerBlockNoteEditor instance const editor = ServerBlockNoteEditor.create(); - // Convert blocks to Yjs XML Fragment directly in our document's fragment - editor.blocksToYXmlFragment(initialBlocks, fragment); + // JSON (already parsed on the web tier) takes precedence. The raw markdown is only a + // fallback, used when JSON is absent, empty, or fails to convert to a valid document. + // Markdown must be parsed to blocks here because BlockNote is unavailable on the web + // tier that builds the message. + const jsonBlocks = content.initialContentJson; + const hasJsonBlocks = Array.isArray(jsonBlocks) && jsonBlocks.length > 0; + + if (hasJsonBlocks) { + try { + editor.blocksToYXmlFragment(jsonBlocks, fragment); + logger.info('Document seeded from initial JSON content', { documentName }); + return { + statusCode: "document_loaded", + }; + } catch (jsonError) { + // Do not crash on malformed JSON blocks; drop anything partially written and + // fall through to the markdown fallback below. + logger.warn('Failed to seed document from JSON, falling back to markdown', { + documentName, + error: jsonError instanceof Error ? jsonError.message : String(jsonError), + }); + if (fragment.length > 0) fragment.delete(0, fragment.length); + } + } + + if (content.initialContentMarkdown) { + const markdownBlocks = await editor.tryParseMarkdownToBlocks(content.initialContentMarkdown); + editor.blocksToYXmlFragment(markdownBlocks, fragment); + logger.info('Document seeded from initial markdown content', { documentName }); + return { + statusCode: "document_loaded", + }; + } - logger.info('Document created/loaded successfully', { documentName }); + logger.warn('No usable initial content to seed document', { documentName }); return { - statusCode: "document_loaded", + statusCode: "no_initial_content", + error: 'No usable initial content (JSON empty/invalid and no markdown fallback)', } } catch (error) { logger.error('Error creating document', { error, documentName }); 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 50fe1539c580..d334ab42c463 100644 --- a/bigbluebutton-html5/imports/ui/components/bn-shared-notes/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/bn-shared-notes/component.tsx @@ -35,6 +35,8 @@ import useCurrentUser from '../../core/hooks/useCurrentUser'; import logger from '/imports/startup/client/logger'; import { notify } from '../../services/notification'; import TextAlignSelect from './text-align-select/component'; +import MarkdownImportModal from './markdown-import-modal/component'; +import { useSharedNotesImport } from './import-context'; // Force-retain `Awareness` against a webpack tree-shaking interaction that // otherwise drops this class while keeping its `extends Observable` expression, @@ -204,6 +206,8 @@ function BlockNoteApp(props: BlockNoteAppProps): React.ReactElement { const intl = useIntl(); + const { isImportModalOpen, closeImportModal } = useSharedNotesImport(); + const blockNoteLocale = useBlockNoteLocaleLanguage(); const [notificationErrorMessage, setNotificationErrorMessage] = React.useState(null); @@ -518,6 +522,12 @@ function BlockNoteApp(props: BlockNoteAppProps): React.ReactElement { )} + {isImportModalOpen && editable && ( + + )} ); } diff --git a/bigbluebutton-html5/imports/ui/components/bn-shared-notes/import-context.tsx b/bigbluebutton-html5/imports/ui/components/bn-shared-notes/import-context.tsx new file mode 100644 index 000000000000..29363b204c47 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/bn-shared-notes/import-context.tsx @@ -0,0 +1,21 @@ +import { createContext, useContext } from 'react'; + +// Bridges the notes kebab menu (which owns the "Import from Markdown" trigger) and +// the BlockNote editor (which owns the document). The kebab lives in a separate +// subtree from the editor, so the open/close state is lifted to the shared Notes +// parent and shared through this context. The modal itself is rendered next to the +// editor so it can call editor.replaceBlocks directly, keeping the editor instance +// encapsulated inside bn-shared-notes. +export interface SharedNotesImportContextValue { + isImportModalOpen: boolean; + openImportModal: () => void; + closeImportModal: () => void; +} + +export const SharedNotesImportContext = createContext({ + isImportModalOpen: false, + openImportModal: () => {}, + closeImportModal: () => {}, +}); + +export const useSharedNotesImport = (): SharedNotesImportContextValue => useContext(SharedNotesImportContext); diff --git a/bigbluebutton-html5/imports/ui/components/bn-shared-notes/markdown-import-modal/component.tsx b/bigbluebutton-html5/imports/ui/components/bn-shared-notes/markdown-import-modal/component.tsx new file mode 100644 index 000000000000..8e4212ba87be --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/bn-shared-notes/markdown-import-modal/component.tsx @@ -0,0 +1,107 @@ +import * as React from 'react'; +import { defineMessages, useIntl } from 'react-intl'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { BlockNoteEditor } from '@blocknote/core'; +import ModalSimple from '/imports/ui/components/common/modal/simple/component'; +import Button from '/imports/ui/components/common/button/component'; +import Styled from './styles'; + +const intlMessages = defineMessages({ + title: { + id: 'app.notes.importModal.title', + description: 'Title for the import from markdown modal', + }, + placeholder: { + id: 'app.notes.importModal.placeholder', + description: 'Placeholder for the markdown import textarea', + }, + replaceWarning: { + id: 'app.notes.importModal.replaceWarning', + description: 'Warning shown before replacing existing notes', + }, + importLabel: { + id: 'app.notes.importModal.import', + description: 'Label for the import confirmation button', + }, + cancelLabel: { + id: 'app.notes.importModal.cancel', + description: 'Label for the cancel button', + }, +}); + +interface MarkdownImportModalProps { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + editor: BlockNoteEditor; + onClose: () => void; +} + +const MarkdownImportModal: React.FC = ({ editor, onClose }) => { + const intl = useIntl(); + const [markdown, setMarkdown] = React.useState(''); + const [confirming, setConfirming] = React.useState(false); + + const documentHasContent = (): boolean => editor.blocksToMarkdownLossy(editor.document).trim().length > 0; + + const applyImport = async () => { + // On the client BlockNote editor this is synchronous, but await keeps it correct + // if a future version returns a Promise. + const blocks = await editor.tryParseMarkdownToBlocks(markdown); + // Replacing every current top-level block mutates the shared Yjs fragment, so the + // change propagates through Hocuspocus to every connected client. + editor.replaceBlocks(editor.document, blocks); + onClose(); + }; + + const handleImportClick = () => { + // Non-empty document: require an explicit confirmation before overwriting. + if (documentHasContent() && !confirming) { + setConfirming(true); + return; + } + applyImport(); + }; + + return ( + + + ) => setMarkdown(e.target.value)} + /> + {confirming && ( + + {intl.formatMessage(intlMessages.replaceWarning)} + + )} + + + + + + + ); +}; + +export default MarkdownImportModal; diff --git a/bigbluebutton-html5/imports/ui/components/bn-shared-notes/markdown-import-modal/styles.ts b/bigbluebutton-html5/imports/ui/components/bn-shared-notes/markdown-import-modal/styles.ts new file mode 100644 index 000000000000..6fe0322364d4 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/bn-shared-notes/markdown-import-modal/styles.ts @@ -0,0 +1,39 @@ +import styled from 'styled-components'; +import { colorGrayLighter, colorDanger } from '/imports/ui/stylesheets/styled-components/palette'; + +const Container = styled.div` + display: flex; + flex-direction: column; + gap: 0.75rem; + min-width: 20rem; +`; + +const Textarea = styled.textarea` + width: 100%; + min-height: 12rem; + resize: vertical; + padding: 0.5rem; + border: 1px solid ${colorGrayLighter}; + border-radius: 0.25rem; + font-family: monospace; + font-size: 0.875rem; +`; + +const Warning = styled.p` + margin: 0; + color: ${colorDanger}; + font-weight: 600; +`; + +const Actions = styled.div` + display: flex; + justify-content: flex-end; + gap: 0.5rem; +`; + +export default { + Container, + Textarea, + Warning, + Actions, +}; diff --git a/bigbluebutton-html5/imports/ui/components/notes/component.tsx b/bigbluebutton-html5/imports/ui/components/notes/component.tsx index e05b469588df..31c84af1bee7 100644 --- a/bigbluebutton-html5/imports/ui/components/notes/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/notes/component.tsx @@ -24,6 +24,7 @@ import { useStorageKey } from '/imports/ui/services/storage/hooks'; import useMeeting from '../../core/hooks/useMeeting'; import { GET_PAD_ID, GetPadIdQueryResponse } from './queries'; import BlockNoteContainer from '../bn-shared-notes/component'; +import { SharedNotesImportContext } from '../bn-shared-notes/import-context'; const intlMessages = defineMessages({ hide: { @@ -83,8 +84,17 @@ const NotesGraphql: React.FC = (props) => { isPresentationEnabled, } = props; const [shouldRenderNotes, setShouldRenderNotes] = useState(false); + const [isImportModalOpen, setIsImportModalOpen] = useState(false); const intl = useIntl(); + // Shared between the kebab menu (opens the modal) and the BlockNote editor + // (renders the modal and applies the import). See import-context.tsx. + const importContextValue = React.useMemo(() => ({ + isImportModalOpen, + openImportModal: () => setIsImportModalOpen(true), + closeImportModal: () => setIsImportModalOpen(false), + }), [isImportModalOpen]); + const { isChrome } = browserInfo; const isOnMediaArea = area === 'media'; const style = isOnMediaArea ? { @@ -140,54 +150,56 @@ const NotesGraphql: React.FC = (props) => { const isEtherpadSharedNotes = sharedNotesEditor === 'etherpad'; return (shouldRenderNotes || shouldShowSharedNotesOnPresentationArea) && ( - - {!isOnMediaArea ? ( + + + {!isOnMediaArea ? ( // @ts-ignore Until everything in Typescript - <> -

{intl.formatMessage(intlMessages.title)}

-
{ - layoutContextDispatch({ - type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN, - value: false, - }); - layoutContextDispatch({ - type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL, - value: PANELS.NONE, - }); - }, - 'data-test': 'hideNotesLabel', - 'aria-label': intl.formatMessage(intlMessages.hide), - label: intl.formatMessage(intlMessages.title), - }} - data-test="notesHeader" - rightButtonProps={null} - customRightButton={( - + <> +

{intl.formatMessage(intlMessages.title)}

+
{ + layoutContextDispatch({ + type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN, + value: false, + }); + layoutContextDispatch({ + type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL, + value: PANELS.NONE, + }); + }, + 'data-test': 'hideNotesLabel', + 'aria-label': intl.formatMessage(intlMessages.hide), + label: intl.formatMessage(intlMessages.title), + }} + data-test="notesHeader" + rightButtonProps={null} + customRightButton={( + )} - /> - - ) : renderHeaderOnMedia()} - { isEtherpadSharedNotes - ? ( - - ) : } - + /> + + ) : renderHeaderOnMedia()} + { isEtherpadSharedNotes + ? ( + + ) : } + + ); }; diff --git a/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/component.tsx b/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/component.tsx index 93d230d633b7..b37569c6ad94 100644 --- a/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/component.tsx @@ -12,6 +12,7 @@ import { import Service from './service'; import useDeduplicatedSubscription from '/imports/ui/core/hooks/useDeduplicatedSubscription'; import Auth from '/imports/ui/services/auth'; +import { useSharedNotesImport } from '../../bn-shared-notes/import-context'; const DEBOUNCE_TIMEOUT = 15000; @@ -24,6 +25,14 @@ const intlMessages = defineMessages({ id: 'app.notes.notesDropdown.exportAsPDF', description: 'Export shared notes as a PDF file', }, + exportAsMarkdownLabel: { + id: 'app.notes.notesDropdown.exportAsMarkdown', + description: 'Export shared notes as a Markdown file', + }, + importFromMarkdownLabel: { + id: 'app.notes.notesDropdown.importFromMarkdown', + description: 'Import shared notes content from Markdown', + }, pinNotes: { id: 'app.notes.notesDropdown.pinNotes', description: 'Label for pin shared notes button', @@ -55,12 +64,14 @@ const NotesDropdownGraphql: React.FC = (props) => { } = props; const [converterButtonDisabled, setConverterButtonDisabled] = useState(false); const intl = useIntl(); + const { openImportModal } = useSharedNotesImport(); const NOTES_IS_PINNABLE = window.meetingClientSettings.public.notes.pinnable; const getAvailableActions = () => { const uploadIcon = 'upload'; const pinIcon = 'presentation'; const downloadIcon = 'download'; + const importIcon = 'copy'; const menuItems = []; @@ -79,6 +90,18 @@ const NotesDropdownGraphql: React.FC = (props) => { }, }, ); + + if (!isEtherpadSharedNotes) { + menuItems.push( + { + key: uniqueId('notes-option-'), + icon: importIcon, + dataTest: 'importNotesFromMarkdown', + label: intl.formatMessage(intlMessages.importFromMarkdownLabel), + onClick: () => openImportModal(), + }, + ); + } } if (!isEtherpadSharedNotes) { @@ -96,6 +119,15 @@ const NotesDropdownGraphql: React.FC = (props) => { window.open(`https://${hocuspocusServerHostname}/hocuspocus/api/documents/${padId}/export/pdf?sessionToken=${sessionToken}`); }, }, + { + key: uniqueId('notes-option-'), + icon: downloadIcon, + dataTest: 'exportNotesAsMarkdown', + label: intl.formatMessage(intlMessages.exportAsMarkdownLabel), + onClick: () => { + window.open(`https://${hocuspocusServerHostname}/hocuspocus/api/documents/${padId}/export/md?sessionToken=${sessionToken}`); + }, + }, ); } diff --git a/bigbluebutton-html5/public/locales/en.json b/bigbluebutton-html5/public/locales/en.json index b6fda6361649..4c2b78fe69af 100755 --- a/bigbluebutton-html5/public/locales/en.json +++ b/bigbluebutton-html5/public/locales/en.json @@ -129,9 +129,16 @@ "app.notes.disabled": "Pinned on media area", "app.notes.notesDropdown.covertAndUpload": "Convert notes to presentation", "app.notes.notesDropdown.exportAsPDF": "Export notes as PDF", + "app.notes.notesDropdown.exportAsMarkdown": "Export notes as Markdown", + "app.notes.notesDropdown.importFromMarkdown": "Import from Markdown", "app.notes.notesDropdown.pinNotes": "Pin notes onto whiteboard", "app.notes.notesDropdown.unpinNotes": "Unpin notes", "app.notes.notesDropdown.notesOptions": "Notes options", + "app.notes.importModal.title": "Import from Markdown", + "app.notes.importModal.placeholder": "Paste your Markdown content here", + "app.notes.importModal.replaceWarning": "This will replace the current notes. Continue?", + "app.notes.importModal.import": "Import", + "app.notes.importModal.cancel": "Cancel", "app.notes.blocknote.payloadSizeError": "Clipboard content too large ({sizeKB}KB). Maximum size for pasting is {maxKB}KB.", "app.notes.blocknote.maxCharCountError": "Input limit reached. You cannot type more than {maxCharCount} characters.", "app.pads.hint": "Press Esc to focus the pad's toolbar", 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 a1f7e535e662..45f06c6f38cd 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 @@ -226,6 +226,10 @@ class ApiController { newMeeting.setSharedNotesInitialContentJsonFromPayload(xmlModules.get("sharedNotesInitialContentJson").text()) } + if(xmlModules.containsKey("sharedNotesInitialContentMarkdown")) { + newMeeting.setSharedNotesInitialContentMarkdownFromPayload(xmlModules.get("sharedNotesInitialContentMarkdown").text()) + } + ApiErrors errors = new ApiErrors() // Strict client-settings override validation (test/staging only, off by default): reject the diff --git a/docs/docs/development/api.md b/docs/docs/development/api.md index 2296776613b0..cc41f15fde1b 100644 --- a/docs/docs/development/api.md +++ b/docs/docs/development/api.md @@ -416,6 +416,35 @@ If you choose the second option (sending content directly), the POST request pay Pay close attention: the initial content JSON structure must be as described in [BlockNote's documentation](https://www.blocknotejs.org/docs/foundations/document-structure?utm_source=chatgpt.com#block-properties). The same applies to the create parameter `sharedNotesInitialContentJsonUrl` (the content inside the URL must have the same structure). +Alternatively, the initial content can be provided as plain Markdown instead of the BlockNote JSON structure. The Markdown is carried untouched through the pipeline and converted to BlockNote blocks on the shared-notes server before seeding the empty document. There are three ways to provide it: + +| Parameter | Type | Description | +| --- | --- | --- | +| `sharedNotesInitialContentMarkdown` | create parameter | Raw Markdown sent inline as a `create` parameter. Suitable for short content that fits within URL length limits. | +| `sharedNotesInitialContentMarkdownUrl` | create parameter | URL from which the raw Markdown will be fetched by the server. | +| `sharedNotesInitialContentMarkdown` | POST module | Raw Markdown sent in the POST body via an xml module, for content too large for a query string. | + +The expected format is raw Markdown (headings, lists, emphasis, etc.), not the BlockNote JSON structure. + +To send the Markdown in the POST body, use the same `` envelope as the JSON variant: + +```xml + + + + + +``` + +**Precedence:** the BlockNote JSON initial content takes precedence over the Markdown. When both JSON and Markdown are supplied, the JSON is used to seed the document, and the Markdown is only used as a fallback when the JSON is absent, empty, or cannot be converted to a valid document. Within the Markdown options, the create parameter `sharedNotesInitialContentMarkdownUrl` is resolved first, then the inline `sharedNotesInitialContentMarkdown` create parameter, and finally the POST module. + #### Pre-upload Slides You can upload slides within the create call. If you do this, the BigBlueButton server will immediately download and process the slides. From d7f769dbb64f3d641d65db2f219381ab2f76180a Mon Sep 17 00:00:00 2001 From: Claudio Promptoso Date: Mon, 6 Jul 2026 04:35:46 +0000 Subject: [PATCH 062/121] test(shared-notes): cover markdown export, init-from-markdown, and import Playwright coverage for the three markdown features, run against a live from-source BlockNote shared-notes environment: - export downloads a .md file whose content matches the editor; - a room created with sharedNotesInitialContentMarkdown renders the seeded heading and list; - the import modal replaces a non-empty document only after confirmation; - an import propagates to a second connected user through the shared document. --- .../playwright/core/elements.ts | 6 + .../playwright/core/helpers.ts | 18 ++ .../sharednotes/blocknote/markdown.spec.ts | 70 +++++++ .../sharednotes/blocknote/markdown.ts | 193 ++++++++++++++++++ 4 files changed, 287 insertions(+) create mode 100644 bigbluebutton-tests/playwright/sharednotes/blocknote/markdown.spec.ts create mode 100644 bigbluebutton-tests/playwright/sharednotes/blocknote/markdown.ts diff --git a/bigbluebutton-tests/playwright/core/elements.ts b/bigbluebutton-tests/playwright/core/elements.ts index 01d67a08f693..b9859d8568ea 100644 --- a/bigbluebutton-tests/playwright/core/elements.ts +++ b/bigbluebutton-tests/playwright/core/elements.ts @@ -234,6 +234,12 @@ export const elements = { currentSlideText: 'span[id="currentSlideText"]', notesOptions: 'button[data-test="notesOptionsMenu"]', exportNotesAsPDF: '[data-test="exportNotesAsPDF"]', + exportNotesAsMarkdown: '[data-test="exportNotesAsMarkdown"]', + importNotesFromMarkdown: '[data-test="importNotesFromMarkdown"]', + notesImportMarkdownTextarea: 'textarea[data-test="notesImportMarkdownTextarea"]', + notesImportMarkdownConfirm: 'button[data-test="notesImportMarkdownConfirm"]', + notesImportMarkdownCancel: 'button[data-test="notesImportMarkdownCancel"]', + notesImportMarkdownWarning: '[data-test="notesImportMarkdownWarning"]', showMoreSharedNotesButton: 'span[class="show-more-icon-btn"]', exportSharedNotesButton: 'li[data-key="import_export"] button', exportPlainButton: 'a[id="exportplaina"] span', diff --git a/bigbluebutton-tests/playwright/core/helpers.ts b/bigbluebutton-tests/playwright/core/helpers.ts index dc882eeaacba..a1547eb37e7e 100644 --- a/bigbluebutton-tests/playwright/core/helpers.ts +++ b/bigbluebutton-tests/playwright/core/helpers.ts @@ -128,6 +128,24 @@ export async function createMeeting(createParameter?: string, customMeetingId?: return xmlResponse.response.meetingID[0]; } +// Create a meeting sending an xml `` payload in the POST body (e.g. +// sharedNotesInitialContentJson / sharedNotesInitialContentMarkdown). The checksum +// only covers the query string, so createMeetingUrl still yields a valid URL. +export async function createMeetingWithModules( + modulesXml: string, + createParameter?: string, + customMeetingId?: string, +): Promise { + const url = createMeetingUrl(createParameter, customMeetingId); + const response = await axios.post(url, modulesXml, { + adapter: 'http', + headers: { 'Content-Type': 'application/xml' }, + }); + expect(response.status).toEqual(200); + const xmlResponse = await xml2js.parseStringPromise(response.data); + return xmlResponse.response.meetingID[0]; +} + export function getJoinURL({ meetingID, fullName, options }: GetJoinUrlProp): string { const { isModerator, joinParameter, skipSessionDetailsModal } = options || {}; diff --git a/bigbluebutton-tests/playwright/sharednotes/blocknote/markdown.spec.ts b/bigbluebutton-tests/playwright/sharednotes/blocknote/markdown.spec.ts new file mode 100644 index 000000000000..3d5d05cc333a --- /dev/null +++ b/bigbluebutton-tests/playwright/sharednotes/blocknote/markdown.spec.ts @@ -0,0 +1,70 @@ +import { initializePages } from '../../core/helpers'; +import { test } from '../../core/setup/fixtures'; +import { INIT_MARKDOWN_HEADING, INIT_MARKDOWN_ITEM, markdownCreateParameter, MarkdownSharedNotes } from './markdown'; + +test.describe.parallel('Shared Notes - BlockNote Markdown', { tag: '@ci' }, () => { + test.describe('Export and import', () => { + let markdownSharedNotes: MarkdownSharedNotes; + + test.beforeEach(async ({ browser, context }, testInfo) => { + markdownSharedNotes = new MarkdownSharedNotes(browser, context); + await initializePages(markdownSharedNotes, browser, { + isMultiUser: true, + createParameter: 'sharedNotesEditor=blocknote', + testInfo, + }); + }); + + test('Export shared notes as Markdown', async () => { + await markdownSharedNotes.exportAsMarkdown(); + }); + + test('Import from Markdown replaces a non-empty document after confirmation', async () => { + await markdownSharedNotes.importFromMarkdownReplacesWithConfirmation(); + }); + + test('Import from Markdown propagates to other connected users', async () => { + await markdownSharedNotes.importFromMarkdownPropagatesToOtherUser(); + }); + }); + + test.describe('Init from Markdown create parameter', () => { + let markdownSharedNotes: MarkdownSharedNotes; + + test.beforeEach(async ({ browser, context }, testInfo) => { + markdownSharedNotes = new MarkdownSharedNotes(browser, context); + const initialMarkdown = `# ${INIT_MARKDOWN_HEADING}\n\n- ${INIT_MARKDOWN_ITEM}`; + await initializePages(markdownSharedNotes, browser, { + isMultiUser: false, + createParameter: markdownCreateParameter(initialMarkdown), + testInfo, + }); + }); + + test('Shared notes are seeded from the Markdown create parameter', async () => { + await markdownSharedNotes.initFromMarkdown(); + }); + }); + + test.describe('Init content precedence (JSON over Markdown)', () => { + let markdownSharedNotes: MarkdownSharedNotes; + + test.beforeEach(async ({ browser, context }) => { + // The meeting is created per-test with a POST modules payload, so nothing + // is created here. + markdownSharedNotes = new MarkdownSharedNotes(browser, context); + }); + + test('JSON initial content takes precedence over Markdown', async () => { + await markdownSharedNotes.precedenceJsonWinsOverMarkdown(); + }); + + test('Falls back to Markdown when the JSON initial content is invalid', async () => { + await markdownSharedNotes.invalidJsonFallsBackToMarkdown(); + }); + + test('Shared notes are seeded from the Markdown POST payload module', async () => { + await markdownSharedNotes.initFromMarkdownPayload(); + }); + }); +}); diff --git a/bigbluebutton-tests/playwright/sharednotes/blocknote/markdown.ts b/bigbluebutton-tests/playwright/sharednotes/blocknote/markdown.ts new file mode 100644 index 000000000000..41a9d6280927 --- /dev/null +++ b/bigbluebutton-tests/playwright/sharednotes/blocknote/markdown.ts @@ -0,0 +1,193 @@ +import { expect } from '@playwright/test'; + +import { ELEMENT_WAIT_LONGER_TIME } from '../../core/constants'; +import { elements as e } from '../../core/elements'; +import { createMeetingWithModules } from '../../core/helpers'; +import { MultiUsers } from '../../user/multiusers'; +import { getBlockNoteEditorLocator, startBlockNoteSharedNotes } from './util'; + +// Markdown seeded through the sharedNotesInitialContentMarkdown create parameter. +// Kept in sync with the value passed in markdown.spec.ts. +export const INIT_MARKDOWN_HEADING = 'Seeded Markdown Heading'; +export const INIT_MARKDOWN_ITEM = 'seeded list item'; + +// Distinct texts so precedence can be asserted by which one renders. +export const JSON_WINS_TEXT = 'JSON content wins over markdown'; +export const MARKDOWN_LOSER_HEADING = 'Markdown that should lose'; +export const MARKDOWN_FALLBACK_HEADING = 'Markdown Fallback Heading'; +export const PAYLOAD_MARKDOWN_HEADING = 'Payload Markdown Heading'; + +// A valid single-paragraph BlockNote document (same structure documented for +// the sharedNotesInitialContentJson module in docs/development/api.md). +function jsonModule(text: string): string { + const blocks = [ + { + id: '00000000-0000-0000-0000-000000000000', + type: 'paragraph', + props: { textAlignment: 'left', backgroundColor: 'default', textColor: 'default' }, + content: [{ type: 'text', text, styles: {} }], + children: [], + }, + ]; + const json = JSON.stringify(blocks); + return ``; +} + +function invalidJsonModule(): string { + const bad = 'this is not valid blocknote json {{{ '; + return ``; +} + +function markdownModule(markdown: string): string { + return ``; +} + +// Build the create query string that seeds shared notes from a raw markdown param. +export function markdownCreateParameter(markdown: string): string { + return `sharedNotesEditor=blocknote&sharedNotesInitialContentMarkdown=${encodeURIComponent(markdown)}`; +} + +export class MarkdownSharedNotes extends MultiUsers { + // Feature: "Export as Markdown" kebab item downloads a .md file with the notes. + async exportAsMarkdown() { + await startBlockNoteSharedNotes(this.modPage); + + const editor = getBlockNoteEditorLocator(this.modPage); + await editor.click(); + const noteText = 'Hello Markdown Export'; + await this.modPage.page.keyboard.type(noteText); + await expect(editor, 'the typed note should be visible in the editor').toContainText(noteText); + + await this.modPage.waitAndClick(e.notesOptions); + const download = await this.modPage.handleDownload( + this.modPage.page.locator(e.exportNotesAsMarkdown), + undefined, + ELEMENT_WAIT_LONGER_TIME, + ); + + if (!download?.download) throw new Error('Markdown export download did not start'); + const extension = download.download.suggestedFilename().split('.').pop(); + expect(extension, 'the exported file should have a .md extension').toBe('md'); + expect(download.content, 'the exported markdown should contain the typed note').toContain(noteText); + } + + // Feature: a room created with sharedNotesInitialContentMarkdown renders the + // converted blocks (heading + list) when the shared notes open. + async initFromMarkdown() { + await startBlockNoteSharedNotes(this.modPage); + + const editor = getBlockNoteEditorLocator(this.modPage); + await expect(editor, 'should render the seeded heading').toContainText(INIT_MARKDOWN_HEADING, { + timeout: ELEMENT_WAIT_LONGER_TIME, + }); + await expect(editor, 'should render the seeded list item').toContainText(INIT_MARKDOWN_ITEM); + } + + // Feature: "Import from Markdown" replaces a non-empty document, but only after + // an explicit confirmation. + async importFromMarkdownReplacesWithConfirmation() { + await startBlockNoteSharedNotes(this.modPage); + + const editor = getBlockNoteEditorLocator(this.modPage); + await editor.click(); + const originalText = 'Original content to be replaced'; + await this.modPage.page.keyboard.type(originalText); + await expect(editor).toContainText(originalText); + + await this.modPage.waitAndClick(e.notesOptions); + await this.modPage.waitAndClick(e.importNotesFromMarkdown); + + const importedMarkdown = '# Imported Heading\n\nImported paragraph body'; + await this.modPage.page.locator(e.notesImportMarkdownTextarea).fill(importedMarkdown); + + // Document is not empty, so the first click only asks for confirmation. + await this.modPage.waitAndClick(e.notesImportMarkdownConfirm); + await this.modPage.hasElement(e.notesImportMarkdownWarning, 'should warn before replacing a non-empty document'); + + // Confirming applies the replacement. + await this.modPage.waitAndClick(e.notesImportMarkdownConfirm); + + await expect(editor, 'the imported heading should render').toContainText('Imported Heading', { + timeout: ELEMENT_WAIT_LONGER_TIME, + }); + await expect(editor, 'the imported paragraph should render').toContainText('Imported paragraph body'); + await expect(editor, 'the original content should have been replaced').not.toContainText(originalText); + } + + // Feature: importing markdown propagates to every connected client through the + // shared Yjs document (proves the client-side replace is collaborative). + async importFromMarkdownPropagatesToOtherUser() { + await startBlockNoteSharedNotes(this.modPage); + await startBlockNoteSharedNotes(this.userPage); + + await this.modPage.waitAndClick(e.notesOptions); + await this.modPage.waitAndClick(e.importNotesFromMarkdown); + + const importedMarkdown = '# Shared Heading\n\nContent visible to everyone'; + await this.modPage.page.locator(e.notesImportMarkdownTextarea).fill(importedMarkdown); + // The presenter's document starts empty, so import applies without confirmation. + await this.modPage.waitAndClick(e.notesImportMarkdownConfirm); + + const userEditor = getBlockNoteEditorLocator(this.userPage); + await expect(userEditor, 'the attendee should see the imported heading').toContainText('Shared Heading', { + timeout: ELEMENT_WAIT_LONGER_TIME, + }); + await expect(userEditor, 'the attendee should see the imported body').toContainText('Content visible to everyone'); + } + + // Creates a meeting with the given modules/params, then joins as moderator. + private async createAndJoin(modulesXml: string, createParameter: string) { + const meetingId = await createMeetingWithModules(modulesXml, createParameter); + const context = await this.browser.newContext(); + const page = await context.newPage(); + await this.initModPage(page, { meetingId }); + } + + // Feature: when both JSON and Markdown initial content are supplied, the JSON + // takes precedence and the Markdown is ignored. + async precedenceJsonWinsOverMarkdown() { + const markdown = `# ${MARKDOWN_LOSER_HEADING}`; + const createParameter = markdownCreateParameter(markdown); + await this.createAndJoin(jsonModule(JSON_WINS_TEXT), createParameter); + + await startBlockNoteSharedNotes(this.modPage); + const editor = getBlockNoteEditorLocator(this.modPage); + await expect(editor, 'the JSON initial content should be used').toContainText(JSON_WINS_TEXT, { + timeout: ELEMENT_WAIT_LONGER_TIME, + }); + await expect(editor, 'the markdown should be ignored when JSON is present').not.toContainText( + MARKDOWN_LOSER_HEADING, + ); + } + + // Feature: when the JSON initial content is invalid, seeding falls back to the + // Markdown instead of leaving the document empty. + async invalidJsonFallsBackToMarkdown() { + const markdown = `# ${MARKDOWN_FALLBACK_HEADING}`; + const createParameter = markdownCreateParameter(markdown); + await this.createAndJoin(invalidJsonModule(), createParameter); + + await startBlockNoteSharedNotes(this.modPage); + const editor = getBlockNoteEditorLocator(this.modPage); + await expect(editor, 'the markdown fallback should render when JSON is invalid').toContainText( + MARKDOWN_FALLBACK_HEADING, + { timeout: ELEMENT_WAIT_LONGER_TIME }, + ); + } + + // Feature: Markdown initial content can also be provided in the POST body via the + // sharedNotesInitialContentMarkdown xml module (for content too large for a query + // string), mirroring sharedNotesInitialContentJson. + async initFromMarkdownPayload() { + const markdown = `# ${PAYLOAD_MARKDOWN_HEADING}\n\n- ${INIT_MARKDOWN_ITEM}`; + await this.createAndJoin(markdownModule(markdown), 'sharedNotesEditor=blocknote'); + + await startBlockNoteSharedNotes(this.modPage); + const editor = getBlockNoteEditorLocator(this.modPage); + await expect(editor, 'the markdown from the POST payload should seed the document').toContainText( + PAYLOAD_MARKDOWN_HEADING, + { timeout: ELEMENT_WAIT_LONGER_TIME }, + ); + await expect(editor, 'the markdown list item should render').toContainText(INIT_MARKDOWN_ITEM); + } +} From c7caec6442d79ca69a9bc3d85a410fa3b8dd0365 Mon Sep 17 00:00:00 2001 From: Claudio Promptoso Date: Tue, 7 Jul 2026 05:32:36 +0000 Subject: [PATCH 063/121] harden shared-notes initial-content URL fetch Route both shared-notes initial-content URL fetches (the pre-existing JSON path and the new markdown path) through the same DNS-pinned fetch already used by plugin, presentation and callback URLs: followRedirectSecure plus SecureUrlDownloader. SharedNotesRedirectValidatorService now extends BaseUrlRedirectValidator, so it applies the shared protocol allowlist, blocked/local host rules and address-resolution checks, and the fetch is capped by maxSharedNotesInitialContentUrlPayloadSize. Both paths now go through one small fetchUrlContent helper instead of an ad-hoc URL.openStream() reader. Shared notes was the only caller of the non-secure followRedirect, so it is removed, leaving followRedirectSecure as the single fetch entry point. Docs: add the sharedNotesInitialContentMarkdown and sharedNotesInitialContentMarkdownUrl create params to the API table, and de-indent the CDATA markdown example so it pastes at the left margin. Also drop dead try/catch blocks around plain map lookups introduced with the markdown params in ParamsProcessorUtil. --- .../org/bigbluebutton/api/MeetingService.java | 89 +++++++++---------- .../api/ParamsProcessorUtil.java | 20 +---- .../api/service/RedirectFollowerService.java | 57 ------------ .../SharedNotesRedirectValidatorService.java | 50 +++-------- .../grails-app/conf/bigbluebutton.properties | 5 ++ .../grails-app/conf/spring/resources.xml | 5 ++ docs/docs/data/create.tsx | 12 +++ docs/docs/development/api.md | 8 +- 8 files changed, 84 insertions(+), 162 deletions(-) diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java index a518fa6b457f..5d4c2cab5ce2 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java @@ -22,7 +22,6 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URI; -import java.net.URL; import java.util.*; import java.util.Map.Entry; import java.util.concurrent.*; @@ -51,6 +50,8 @@ import org.bigbluebutton.api.util.ParsedPluginManifest; import org.bigbluebutton.api.util.PluginUtils; import org.bigbluebutton.api.service.RedirectFollowerService; +import org.bigbluebutton.api.service.SecureUrlDownloader; +import org.bigbluebutton.api.service.ValidatedUrl; import org.bigbluebutton.api2.IBbbWebApiGWApp; import org.bigbluebutton.api2.domain.UploadedTrack; import org.bigbluebutton.common2.redis.RedisStorageService; @@ -68,7 +69,6 @@ import java.io.BufferedReader; import java.io.InputStreamReader; -import java.util.stream.Collectors; import org.springframework.data.domain.*; @@ -110,6 +110,8 @@ public class MeetingService implements MessageListener { private PresentationUrlDownloadService presDownloadService; private RedirectFollowerService redirectFollower; private SharedNotesRedirectValidatorService sharedNotesRedirectValidator; + private SecureUrlDownloader secureUrlDownloader; + private int maxSharedNotesInitialContentUrlPayloadSize; private IBbbWebApiGWApp gw; @@ -397,30 +399,35 @@ public ArrayList getSharedNotesInitialContent(Meeting m) { return initialContent; } - public ArrayList requestSharedNotesInitialContentFromUrl(String meetingId, String initialContentJsonUrl) { - ArrayList initialContent = new ArrayList<>(); - if (!initialContentJsonUrl.isEmpty()) { - try { - String finalInitialContentJsonUrl = redirectFollower.followRedirect( - meetingId, initialContentJsonUrl, 0, initialContentJsonUrl, sharedNotesRedirectValidator, 6000 - ); - - URL url = new URL(finalInitialContentJsonUrl); - String content; - try (BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()))) { - content = in.lines().collect(Collectors.joining("\n")); - } - initialContent = parseSharedNotesInitialContent(content); - } catch (MalformedURLException e) { - log.error( - "Malformed URL for sharedNotesInitialContentJsonUrl: [{}]", initialContentJsonUrl); - } catch (IOException e) { - log.error( - "Something went wrong while processing [{}]. Error: {}", initialContentJsonUrl, e.getMessage()); - } - } - return initialContent; - } + public ArrayList requestSharedNotesInitialContentFromUrl(String meetingId, String initialContentJsonUrl) { + if (initialContentJsonUrl.isEmpty()) { + return new ArrayList<>(); + } + String content = fetchUrlContent(meetingId, initialContentJsonUrl); + return parseSharedNotesInitialContent(content); + } + + /** + * Fetches the content at the given URL using the DNS-pinned, security-validated fetch path + * (protocol allowlist, blocked/local host rules, and rebinding protection). Returns an empty + * string when validation fails, the request errors, or the payload exceeds the configured cap. + */ + private String fetchUrlContent(String meetingId, String url) { + ValidatedUrl validatedUrl = redirectFollower.followRedirectSecure( + meetingId, url, 0, url, sharedNotesRedirectValidator, 6000 + ); + + if (validatedUrl == null) { + log.error("Failed to validate and resolve URL [{}] for meeting [{}]", url, meetingId); + return ""; + } + + String content = secureUrlDownloader.downloadToString( + meetingId, validatedUrl, 6000, maxSharedNotesInitialContentUrlPayloadSize + ); + + return content != null ? content : ""; + } public ArrayList parseSharedNotesInitialContent(String content) { ArrayList initialContent = null; @@ -457,26 +464,10 @@ public String getSharedNotesInitialContentMarkdown(Meeting m) { } public String requestSharedNotesInitialContentMarkdownFromUrl(String meetingId, String initialContentMarkdownUrl) { - String initialContent = ""; - if (!initialContentMarkdownUrl.isEmpty()) { - try { - String finalInitialContentMarkdownUrl = redirectFollower.followRedirect( - meetingId, initialContentMarkdownUrl, 0, initialContentMarkdownUrl, sharedNotesRedirectValidator, 6000 - ); - - URL url = new URL(finalInitialContentMarkdownUrl); - try (BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()))) { - initialContent = in.lines().collect(Collectors.joining("\n")); - } - } catch (MalformedURLException e) { - log.error( - "Malformed URL for sharedNotesInitialContentMarkdownUrl: [{}]", initialContentMarkdownUrl); - } catch (IOException e) { - log.error( - "Something went wrong while processing [{}]. Error: {}", initialContentMarkdownUrl, e.getMessage()); - } + if (initialContentMarkdownUrl.isEmpty()) { + return ""; } - return initialContent; + return fetchUrlContent(meetingId, initialContentMarkdownUrl); } public Map requestPluginManifests(Meeting m) { @@ -1713,5 +1704,13 @@ public void setRedirectFollower(RedirectFollowerService redirectFollower) { public void setSharedNotesRedirectValidator(SharedNotesRedirectValidatorService sharedNotesRedirectValidator) { this.sharedNotesRedirectValidator = sharedNotesRedirectValidator; } + + public void setSecureUrlDownloader(SecureUrlDownloader secureUrlDownloader) { + this.secureUrlDownloader = secureUrlDownloader; + } + + public void setMaxSharedNotesInitialContentUrlPayloadSize(int maxSharedNotesInitialContentUrlPayloadSize) { + this.maxSharedNotesInitialContentUrlPayloadSize = maxSharedNotesInitialContentUrlPayloadSize; + } } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java index 9691fb783dbe..5be855ade659 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java @@ -661,26 +661,14 @@ boolean record = processRecordMeeting(params.get(ApiParams.RECORD)); String sharedNotesInitialContentMarkdown = ""; if (!StringUtils.isEmpty(params.get(ApiParams.SHARED_NOTES_INITIAL_CONTENT_MARKDOWN))) { - try { - sharedNotesInitialContentMarkdown = params - .get(ApiParams.SHARED_NOTES_INITIAL_CONTENT_MARKDOWN); - } catch (Exception ex) { - log.warn( - "Invalid param [sharedNotesInitialContentMarkdown] for meeting=[{}]", - internalMeetingId); - } + sharedNotesInitialContentMarkdown = params + .get(ApiParams.SHARED_NOTES_INITIAL_CONTENT_MARKDOWN); } String sharedNotesInitialContentMarkdownUrl = ""; if (!StringUtils.isEmpty(params.get(ApiParams.SHARED_NOTES_INITIAL_CONTENT_MARKDOWN_URL))) { - try { - sharedNotesInitialContentMarkdownUrl = params - .get(ApiParams.SHARED_NOTES_INITIAL_CONTENT_MARKDOWN_URL); - } catch (Exception ex) { - log.warn( - "Invalid param [sharedNotesInitialContentMarkdownUrl] for meeting=[{}]", - internalMeetingId); - } + sharedNotesInitialContentMarkdownUrl = params + .get(ApiParams.SHARED_NOTES_INITIAL_CONTENT_MARKDOWN_URL); } String sharedNotesEditor = defaultSharedNotesEditor; diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/service/RedirectFollowerService.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/RedirectFollowerService.java index a656f3cb1d1d..269e8ce242b4 100644 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/service/RedirectFollowerService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/RedirectFollowerService.java @@ -12,7 +12,6 @@ import org.apache.http.impl.client.HttpClients; import java.io.IOException; -import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.net.UnknownHostException; @@ -21,62 +20,6 @@ public class RedirectFollowerService { private static final Logger log = LoggerFactory.getLogger(RedirectFollowerService.class); private static final int MAX_REDIRECTS = 5; - public String followRedirect( - String meetingId, String redirectUrl, - int redirectCount, String origUrl, - RedirectValidator redirectValidator, - int downloadReadTimeoutInMs - ) { - if (redirectCount > MAX_REDIRECTS) { - log.error("Max redirect reached for meeting=[{}] with url=[{}]", - meetingId, origUrl); - return null; - } - - if(!redirectValidator.isRedirectValid(redirectUrl)) return null; - - URL attemptUrl; - try { - attemptUrl = new URL(redirectUrl); - } catch (MalformedURLException e) { - log.error("Malformed url=[{}] for meeting=[{}]", redirectUrl, meetingId, e); - return null; - } - - HttpURLConnection conn; - try { - conn = (HttpURLConnection) attemptUrl.openConnection(); - conn.setReadTimeout(downloadReadTimeoutInMs); - conn.addRequestProperty("Accept-Language", "en-US,en;q=0.8"); - conn.addRequestProperty("User-Agent", "Mozilla"); - conn.setInstanceFollowRedirects(false); - - // normally, 3xx is redirect - int status = conn.getResponseCode(); - if (status != HttpURLConnection.HTTP_OK) { - if (status == HttpURLConnection.HTTP_MOVED_TEMP - || status == HttpURLConnection.HTTP_MOVED_PERM - || status == HttpURLConnection.HTTP_SEE_OTHER) { - String newUrl = conn.getHeaderField("Location"); - return followRedirect( - meetingId, newUrl, redirectCount + 1, - origUrl, redirectValidator, downloadReadTimeoutInMs - ); - } else { - log.error( - "Invalid HTTP response=[{}] for url=[{}] with meeting[{}]", - status, redirectUrl, meetingId); - return null; - } - } else { - return redirectUrl; - } - } catch (IOException e) { - log.error("IOException for url=[{}] with meeting[{}]", redirectUrl, meetingId, e); - return null; - } - } - public ValidatedUrl followRedirectSecure( String meetingId, String redirectUrl, int redirectCount, String origUrl, diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/SharedNotesRedirectValidatorService.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/SharedNotesRedirectValidatorService.java index 6797a83fb9d6..fd88d87e35d8 100644 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/SharedNotesRedirectValidatorService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/SharedNotesRedirectValidatorService.java @@ -1,43 +1,13 @@ package org.bigbluebutton.api.service.impl; -import org.apache.commons.validator.routines.InetAddressValidator; -import org.bigbluebutton.api.service.RedirectValidator; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.net.InetAddress; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.UnknownHostException; - -public class SharedNotesRedirectValidatorService implements RedirectValidator { - private static final Logger log = LoggerFactory.getLogger(SharedNotesRedirectValidatorService.class); - public boolean isRedirectValid(String redirectUrl) { - log.info("Validating redirect URL [{}]", redirectUrl); - URL url; - - try { - url = new URL(redirectUrl); - } catch(MalformedURLException e) { - log.error("Malformed URL [{}]", redirectUrl); - return false; - } - - try { - InetAddress[] addresses = InetAddress.getAllByName(url.getHost()); - InetAddressValidator validator = InetAddressValidator.getInstance(); - - for(InetAddress address: addresses) { - if(!validator.isValid(address.getHostAddress())) { - log.error("Invalid address [{}]", address.getHostAddress()); - return false; - } - } - } catch(UnknownHostException e) { - log.error("Unknown host [{}]", url.getHost()); - return false; - } - - return true; - } +/** + * Redirect validator for shared notes initial-content URLs + * (sharedNotesInitialContentJsonUrl, sharedNotesInitialContentMarkdownUrl). + * + * Extends {@link BaseUrlRedirectValidator} with the full set of security + * checks (protocol allowlist, blocked hosts, allowed local hosts, DNS + * rebinding protection via IP address resolution and validation). + */ +public class SharedNotesRedirectValidatorService extends BaseUrlRedirectValidator { + // No additional behaviour needed beyond what BaseUrlRedirectValidator provides. } diff --git a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties index 543c233d0cb7..930f5c04f6f7 100644 --- a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties +++ b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties @@ -112,6 +112,11 @@ clientSettingsOverrideJsonUrlResponseTimeout=15 #------------------------------------ maxClientSettingsOverrideJsonUrlPayloadSize=1024 +#------------------------------------ +# The response from a shared notes initial-content fetch URL (JSON or markdown) shouldn't have more than this size (in KiB) +#------------------------------------ +maxSharedNotesInitialContentUrlPayloadSize=1024 + #------------------------------------ # Timeout(secs) to wait for pdf to svg conversion (timeout for each tool called during the process) #------------------------------------ diff --git a/bigbluebutton-web/grails-app/conf/spring/resources.xml b/bigbluebutton-web/grails-app/conf/spring/resources.xml index 1c094bdc0129..2b35a335d472 100755 --- a/bigbluebutton-web/grails-app/conf/spring/resources.xml +++ b/bigbluebutton-web/grails-app/conf/spring/resources.xml @@ -44,6 +44,9 @@ with BigBlueButton; if not, see . + + + . + + diff --git a/docs/docs/data/create.tsx b/docs/docs/data/create.tsx index 59bbe869ba08..f53c219fd471 100644 --- a/docs/docs/data/create.tsx +++ b/docs/docs/data/create.tsx @@ -386,6 +386,18 @@ const createEndpointTableData = [ "type": "String", "description": (<>Url from which the shared-notes will fetch the initial content (Only applicable for when `sharedNotesEditor=blockNote`, ignored otherwise)) }, + { + "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.) + }, + { + "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)) + }, { "name": "disabledFeatures", "required": false, diff --git a/docs/docs/development/api.md b/docs/docs/development/api.md index cc41f15fde1b..f2747124e6a8 100644 --- a/docs/docs/development/api.md +++ b/docs/docs/development/api.md @@ -432,12 +432,12 @@ To send the Markdown in the POST body, use the same `` envelope as the From e0e78edff5560fd80948d01c6dae32b38010e8cc Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Tue, 7 Jul 2026 22:22:54 +0900 Subject: [PATCH 064/121] fix(document): add --build for development scripts of bbb-web (#25379) * Update dev-guide.md * Update dev-guide.md --- docs/docs/development/dev-guide.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/docs/development/dev-guide.md b/docs/docs/development/dev-guide.md index 9034099e2cd9..12987318ef94 100644 --- a/docs/docs/development/dev-guide.md +++ b/docs/docs/development/dev-guide.md @@ -347,13 +347,14 @@ cd ~/dev/bigbluebutton/bigbluebutton-web To rebuild and deploy your changes, replacing the existing `bbb-web` in `/usr/share/bbb-web`: ```bash -./deploy_to_usr_share.sh +./deploy_to_usr_share.sh --build ``` +The --build option builds bbb-common-web, which is required when building bbb-web for the first time. After that, if you have not changed the bbb-common-web source code, you can omit the --build option. Alternatively, to run bbb-web in development mode on port 8090 without replacing the deployed files: ```bash -./run-dev.sh +./run-dev.sh --build ``` ## Developing Akka-Apps From 83f698e4f406784f2c2900742197b055dc8b2066 Mon Sep 17 00:00:00 2001 From: Claudio Promptoso Date: Tue, 7 Jul 2026 17:00:02 +0000 Subject: [PATCH 065/121] feat(shared-notes): add file upload to the Markdown import modal The "Import from Markdown" dialog previously only accepted content pasted into a textarea. This adds a file upload path as the primary way to bring in notes, while keeping the textarea as an optional secondary path so nothing that worked before changes. Layout is stacked and compact: a drag-and-drop zone on top, an "or paste it below" divider, then the existing textarea. Both feed the same markdown state, so the parse/replace pipeline and the non-empty replace confirmation are untouched. The dropzone reuses the same react-dropzone component the presentation uploader relies on. Files are validated by extension (.md/.markdown), capped at 1MB, and read with FileReader. UX states are covered: idle, drag-over, file-loaded chip (name/size/remove), wrong-type and read-error alerts, and an empty-file notice that keeps import disabled. Errors and warnings use role=alert. Adds Playwright coverage for uploading a valid file, rejecting a wrong type, and disabling import for an empty file, alongside the existing paste tests. --- .../markdown-import-modal/component.tsx | 188 +++++++++++++++++- .../markdown-import-modal/styles.ts | 150 +++++++++++++- bigbluebutton-html5/public/locales/en.json | 10 + .../playwright/core/elements.ts | 5 + .../sharednotes/blocknote/markdown.spec.ts | 12 ++ .../sharednotes/blocknote/markdown.ts | 80 ++++++++ 6 files changed, 442 insertions(+), 3 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/bn-shared-notes/markdown-import-modal/component.tsx b/bigbluebutton-html5/imports/ui/components/bn-shared-notes/markdown-import-modal/component.tsx index 8e4212ba87be..2de6f39216e6 100644 --- a/bigbluebutton-html5/imports/ui/components/bn-shared-notes/markdown-import-modal/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/bn-shared-notes/markdown-import-modal/component.tsx @@ -4,8 +4,15 @@ import { defineMessages, useIntl } from 'react-intl'; import { BlockNoteEditor } from '@blocknote/core'; import ModalSimple from '/imports/ui/components/common/modal/simple/component'; import Button from '/imports/ui/components/common/button/component'; +import Icon from '/imports/ui/components/common/icon/component'; import Styled from './styles'; +// A Markdown file larger than this is rejected before it is read into memory. +const MAX_FILE_SIZE = 1024 * 1024; // 1MB +const ACCEPTED_EXTENSIONS = ['.md', '.markdown']; + +type ImportError = 'invalidType' | 'tooLarge' | 'readFailed' | 'empty'; + const intlMessages = defineMessages({ title: { id: 'app.notes.importModal.title', @@ -27,8 +34,62 @@ const intlMessages = defineMessages({ id: 'app.notes.importModal.cancel', description: 'Label for the cancel button', }, + dropzoneLabel: { + id: 'app.notes.importModal.dropzone.label', + description: 'Instruction to drag and drop a markdown file', + }, + dropzoneBrowse: { + id: 'app.notes.importModal.dropzone.browse', + description: 'Call to action to open the file browser', + }, + dropzoneHint: { + id: 'app.notes.importModal.dropzone.hint', + description: 'Hint listing the accepted file extensions', + }, + dropzoneActive: { + id: 'app.notes.importModal.dropzone.active', + description: 'Shown while a file is being dragged over the dropzone', + }, + orDivider: { + id: 'app.notes.importModal.orDivider', + description: 'Divider between the dropzone and the paste textarea', + }, + removeFile: { + id: 'app.notes.importModal.fileLoaded.remove', + description: 'Label for the button that removes the loaded file', + }, + errorInvalidType: { + id: 'app.notes.importModal.error.invalidType', + description: 'Error shown when a non-markdown file is selected', + }, + errorTooLarge: { + id: 'app.notes.importModal.error.tooLarge', + description: 'Error shown when the selected file exceeds the size limit', + }, + errorReadFailed: { + id: 'app.notes.importModal.error.readFailed', + description: 'Error shown when the file could not be read', + }, + errorEmpty: { + id: 'app.notes.importModal.error.empty', + description: 'Message shown when the selected file has no content', + }, }); +const errorMessageIds: Record = { + invalidType: 'errorInvalidType', + tooLarge: 'errorTooLarge', + readFailed: 'errorReadFailed', + empty: 'errorEmpty', +}; + +// Human readable file size for the loaded-file chip. +function formatBytes(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} + interface MarkdownImportModalProps { // eslint-disable-next-line @typescript-eslint/no-explicit-any editor: BlockNoteEditor; @@ -37,9 +98,62 @@ interface MarkdownImportModalProps { const MarkdownImportModal: React.FC = ({ editor, onClose }) => { const intl = useIntl(); + // `markdown` is the single source of truth fed to the parse pipeline, whether it + // came from an uploaded file or from the textarea below. const [markdown, setMarkdown] = React.useState(''); + const [fileName, setFileName] = React.useState(null); + const [fileSize, setFileSize] = React.useState(null); + const [error, setError] = React.useState(null); const [confirming, setConfirming] = React.useState(false); + const clearFile = React.useCallback(() => { + setFileName(null); + setFileSize(null); + }, []); + + const loadFile = React.useCallback((file: File) => { + const name = file.name.toLowerCase(); + const isMarkdown = ACCEPTED_EXTENSIONS.some((ext) => name.endsWith(ext)); + // Error paths never touch `markdown`: text the user may have typed in the textarea + // must survive a rejected drop. Only the chip (file metadata) is cleared. + if (!isMarkdown) { + setError('invalidType'); + clearFile(); + return; + } + if (file.size > MAX_FILE_SIZE) { + setError('tooLarge'); + clearFile(); + return; + } + + const reader = new FileReader(); + reader.onerror = () => { + setError('readFailed'); + clearFile(); + }; + reader.onload = () => { + const text = typeof reader.result === 'string' ? reader.result : ''; + setFileName(file.name); + setFileSize(file.size); + setMarkdown(text); + // Empty file: keep the chip so the user sees what they loaded, but flag it and + // leave the import button disabled (markdown is empty). + setError(text.trim().length === 0 ? 'empty' : null); + }; + reader.readAsText(file); + }, [clearFile]); + + // react-dropzone (the legacy version this repo pins, as used by the presentation + // uploader) hands onDrop both the accepted and the rejected files. `accept` only + // hints the native file picker to default to .md/.markdown; it is not authoritative. + // A file that fails the hint lands in rejectedFiles, so it is still forwarded to + // loadFile, which is the source of truth for the extension check and the error. + const onDrop = (acceptedFiles: File[], rejectedFiles: File[]) => { + const file = acceptedFiles[0] ?? rejectedFiles?.[0]; + if (file) loadFile(file); + }; + const documentHasContent = (): boolean => editor.blocksToMarkdownLossy(editor.document).trim().length > 0; const applyImport = async () => { @@ -61,6 +175,21 @@ const MarkdownImportModal: React.FC = ({ editor, onClo applyImport(); }; + const handleTextareaChange = (e: React.ChangeEvent) => { + setMarkdown(e.target.value); + // Typing supersedes any file-load error/message (e.g. filling in an empty file). + setError(null); + }; + + const handleRemoveFile = () => { + clearFile(); + setMarkdown(''); + setError(null); + }; + + // The red dashed border only applies to hard errors, not the empty-file notice. + const hasHardError = error !== null && error !== 'empty'; + return ( = ({ editor, onClo data-test="notesImportMarkdownModal" > + + {({ isDragActive }: { isDragActive: boolean }) => ( + <> + + {isDragActive ? ( + + {intl.formatMessage(intlMessages.dropzoneActive)} + + ) : ( + + {intl.formatMessage(intlMessages.dropzoneLabel)} + {' '} + {intl.formatMessage(intlMessages.dropzoneBrowse)} + + )} + + {intl.formatMessage(intlMessages.dropzoneHint)} + + + )} + + + {fileName && ( + + + {fileName} + {fileSize !== null && {formatBytes(fileSize)}} + + + + + )} + + {error && ( + + {intl.formatMessage(intlMessages[errorMessageIds[error]])} + + )} + + {intl.formatMessage(intlMessages.orDivider)} + ) => setMarkdown(e.target.value)} + onChange={handleTextareaChange} /> {confirming && ( diff --git a/bigbluebutton-html5/imports/ui/components/bn-shared-notes/markdown-import-modal/styles.ts b/bigbluebutton-html5/imports/ui/components/bn-shared-notes/markdown-import-modal/styles.ts index 6fe0322364d4..e44c014ee132 100644 --- a/bigbluebutton-html5/imports/ui/components/bn-shared-notes/markdown-import-modal/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/bn-shared-notes/markdown-import-modal/styles.ts @@ -1,5 +1,16 @@ import styled from 'styled-components'; -import { colorGrayLighter, colorDanger } from '/imports/ui/stylesheets/styled-components/palette'; +// eslint-disable-next-line import/no-extraneous-dependencies +import Dropzone from 'react-dropzone'; +import Icon from '/imports/ui/components/common/icon/component'; +import { + colorGray, + colorGrayLight, + colorGrayLighter, + colorGrayLightest, + colorDanger, + colorPrimary, + colorOffWhite, +} from '/imports/ui/stylesheets/styled-components/palette'; const Container = styled.div` display: flex; @@ -8,9 +19,133 @@ const Container = styled.div` min-width: 20rem; `; +interface DropzoneProps { + $hasError: boolean; +} + +// Primary import path: the same react-dropzone target the presentation uploader uses. +// The `.isDragActive` class is applied by Dropzone via the activeClassName prop. +// Compact padding keeps the stacked layout tight next to the optional textarea below. +const DropzoneRoot = styled(Dropzone)` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.25rem; + padding: 0.75rem; + text-align: center; + cursor: pointer; + border: 2px dashed ${colorGrayLighter}; + border-radius: 0.25rem; + color: ${colorGray}; + transition: border-color 0.15s ease, background-color 0.15s ease; + + &:hover, + &:focus-within { + border-color: ${colorPrimary}; + } + + &.isDragActive { + border-color: ${colorPrimary}; + background-color: rgba(15, 112, 215, 0.08); + } + + ${({ $hasError }) => $hasError && ` + border-color: ${colorDanger}; + `} +`; + +const DropzoneIcon = styled(Icon)` + font-size: 1.5rem; + line-height: 1; + color: ${colorGrayLight}; +`; + +const DropzoneLabel = styled.span` + font-size: 0.875rem; +`; + +const Browse = styled.span` + color: ${colorPrimary}; + text-decoration: underline; +`; + +const DropzoneHint = styled.span` + font-size: 0.75rem; + color: ${colorGrayLight}; +`; + +const Divider = styled.div` + display: flex; + align-items: center; + gap: 0.5rem; + color: ${colorGrayLight}; + font-size: 0.75rem; + + &::before, + &::after { + content: ''; + flex: 1; + height: 1px; + background-color: ${colorGrayLightest}; + } +`; + +const FileChip = styled.div` + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + background-color: ${colorOffWhite}; + border: 1px solid ${colorGrayLightest}; + border-radius: 0.25rem; + font-size: 0.875rem; +`; + +const FileName = styled.span` + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: 600; +`; + +const FileSize = styled.span` + color: ${colorGrayLight}; + font-size: 0.75rem; + white-space: nowrap; +`; + +const FileRemove = styled.button` + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.25rem; + background: transparent; + border: none; + border-radius: 0.25rem; + cursor: pointer; + color: ${colorGray}; + + &:hover { + color: ${colorDanger}; + } + + &:focus-visible { + outline: 2px solid ${colorPrimary}; + } +`; + +const Error = styled.p` + margin: 0; + color: ${colorDanger}; + font-size: 0.8125rem; + font-weight: 600; +`; + const Textarea = styled.textarea` width: 100%; - min-height: 12rem; + min-height: 8rem; resize: vertical; padding: 0.5rem; border: 1px solid ${colorGrayLighter}; @@ -33,6 +168,17 @@ const Actions = styled.div` export default { Container, + Dropzone: DropzoneRoot, + DropzoneIcon, + DropzoneLabel, + Browse, + DropzoneHint, + Divider, + FileChip, + FileName, + FileSize, + FileRemove, + Error, Textarea, Warning, Actions, diff --git a/bigbluebutton-html5/public/locales/en.json b/bigbluebutton-html5/public/locales/en.json index 4c2b78fe69af..aeddc7cd67ec 100755 --- a/bigbluebutton-html5/public/locales/en.json +++ b/bigbluebutton-html5/public/locales/en.json @@ -139,6 +139,16 @@ "app.notes.importModal.replaceWarning": "This will replace the current notes. Continue?", "app.notes.importModal.import": "Import", "app.notes.importModal.cancel": "Cancel", + "app.notes.importModal.dropzone.label": "Drag and drop a Markdown file here", + "app.notes.importModal.dropzone.browse": "browse files", + "app.notes.importModal.dropzone.hint": "Accepted files: .md, .markdown", + "app.notes.importModal.dropzone.active": "Drop the file to load it", + "app.notes.importModal.orDivider": "or paste it below", + "app.notes.importModal.fileLoaded.remove": "Remove file", + "app.notes.importModal.error.invalidType": "Please select a .md or .markdown file", + "app.notes.importModal.error.tooLarge": "This file is too large", + "app.notes.importModal.error.readFailed": "Could not read the file", + "app.notes.importModal.error.empty": "The selected file is empty", "app.notes.blocknote.payloadSizeError": "Clipboard content too large ({sizeKB}KB). Maximum size for pasting is {maxKB}KB.", "app.notes.blocknote.maxCharCountError": "Input limit reached. You cannot type more than {maxCharCount} characters.", "app.pads.hint": "Press Esc to focus the pad's toolbar", diff --git a/bigbluebutton-tests/playwright/core/elements.ts b/bigbluebutton-tests/playwright/core/elements.ts index b9859d8568ea..bd197ac843e0 100644 --- a/bigbluebutton-tests/playwright/core/elements.ts +++ b/bigbluebutton-tests/playwright/core/elements.ts @@ -240,6 +240,11 @@ export const elements = { notesImportMarkdownConfirm: 'button[data-test="notesImportMarkdownConfirm"]', notesImportMarkdownCancel: 'button[data-test="notesImportMarkdownCancel"]', notesImportMarkdownWarning: '[data-test="notesImportMarkdownWarning"]', + notesImportMarkdownDropzone: '[data-test="notesImportMarkdownDropzone"]', + notesImportMarkdownFileInput: 'input[data-test="notesImportMarkdownFileInput"]', + notesImportMarkdownFileLoaded: '[data-test="notesImportMarkdownFileLoaded"]', + notesImportMarkdownFileRemove: 'button[data-test="notesImportMarkdownFileRemove"]', + notesImportMarkdownError: '[data-test="notesImportMarkdownError"]', showMoreSharedNotesButton: 'span[class="show-more-icon-btn"]', exportSharedNotesButton: 'li[data-key="import_export"] button', exportPlainButton: 'a[id="exportplaina"] span', diff --git a/bigbluebutton-tests/playwright/sharednotes/blocknote/markdown.spec.ts b/bigbluebutton-tests/playwright/sharednotes/blocknote/markdown.spec.ts index 3d5d05cc333a..b1bd8ac78d48 100644 --- a/bigbluebutton-tests/playwright/sharednotes/blocknote/markdown.spec.ts +++ b/bigbluebutton-tests/playwright/sharednotes/blocknote/markdown.spec.ts @@ -26,6 +26,18 @@ test.describe.parallel('Shared Notes - BlockNote Markdown', { tag: '@ci' }, () = test('Import from Markdown propagates to other connected users', async () => { await markdownSharedNotes.importFromMarkdownPropagatesToOtherUser(); }); + + test('Import from Markdown loads a valid uploaded .md file', async () => { + await markdownSharedNotes.importFromMarkdownUploadFile(); + }); + + test('Import from Markdown rejects a non-markdown uploaded file', async () => { + await markdownSharedNotes.importFromMarkdownUploadWrongType(); + }); + + test('Import from Markdown keeps import disabled for an empty uploaded file', async () => { + await markdownSharedNotes.importFromMarkdownUploadEmptyFile(); + }); }); test.describe('Init from Markdown create parameter', () => { diff --git a/bigbluebutton-tests/playwright/sharednotes/blocknote/markdown.ts b/bigbluebutton-tests/playwright/sharednotes/blocknote/markdown.ts index 41a9d6280927..49a43700cdee 100644 --- a/bigbluebutton-tests/playwright/sharednotes/blocknote/markdown.ts +++ b/bigbluebutton-tests/playwright/sharednotes/blocknote/markdown.ts @@ -135,6 +135,86 @@ export class MarkdownSharedNotes extends MultiUsers { await expect(userEditor, 'the attendee should see the imported body').toContainText('Content visible to everyone'); } + // Opens shared notes and the "Import from Markdown" modal as moderator. + private async openImportModal() { + await startBlockNoteSharedNotes(this.modPage); + await this.modPage.waitAndClick(e.notesOptions); + await this.modPage.waitAndClick(e.importNotesFromMarkdown); + } + + // Loads a file into the dropzone via the hidden file input (drag is not simulated + // because it is flaky; setInputFiles exercises the same onDrop path). + private async importFromMarkdownUpload({ + name, + mimeType, + buffer, + }: { + name: string; + mimeType: string; + buffer: Buffer; + }) { + await this.modPage.page.setInputFiles(e.notesImportMarkdownFileInput, { name, mimeType, buffer }); + } + + // Feature: uploading a valid .md file loads its content and imports it into the editor. + async importFromMarkdownUploadFile() { + await this.openImportModal(); + + const uploadedMarkdown = '# Uploaded Heading\n\nUploaded from a file'; + await this.importFromMarkdownUpload({ + name: 'notes.md', + mimeType: 'text/markdown', + buffer: Buffer.from(uploadedMarkdown), + }); + + // The loaded-file chip confirms the upload was accepted. + await this.modPage.hasElement(e.notesImportMarkdownFileLoaded, 'the loaded-file chip should appear'); + + // The presenter's document starts empty, so import applies without confirmation. + await this.modPage.waitAndClick(e.notesImportMarkdownConfirm); + + const editor = getBlockNoteEditorLocator(this.modPage); + await expect(editor, 'the uploaded heading should render').toContainText('Uploaded Heading', { + timeout: ELEMENT_WAIT_LONGER_TIME, + }); + await expect(editor, 'the uploaded body should render').toContainText('Uploaded from a file'); + } + + // Feature: uploading a non-markdown file surfaces an error and imports nothing. + async importFromMarkdownUploadWrongType() { + await this.openImportModal(); + + await this.importFromMarkdownUpload({ + name: 'notes.txt', + mimeType: 'text/plain', + buffer: Buffer.from('this is not markdown'), + }); + + await this.modPage.hasElement(e.notesImportMarkdownError, 'a wrong-type error should be shown'); + await expect( + this.modPage.page.locator(e.notesImportMarkdownConfirm), + 'import should stay disabled for a rejected file', + ).toHaveAttribute('aria-disabled', 'true'); + } + + // Feature: uploading an empty markdown file shows the chip but keeps import disabled. + async importFromMarkdownUploadEmptyFile() { + await this.openImportModal(); + + await this.importFromMarkdownUpload({ + name: 'empty.md', + mimeType: 'text/markdown', + buffer: Buffer.from(''), + }); + + await this.modPage.hasElement(e.notesImportMarkdownFileLoaded, 'the chip should appear for an empty file'); + await this.modPage.hasElement(e.notesImportMarkdownError, 'an empty-file notice should be shown'); + await expect( + this.modPage.page.locator(e.notesImportMarkdownConfirm), + 'import should be disabled for an empty file', + ).toHaveAttribute('aria-disabled', 'true'); + } + // Creates a meeting with the given modules/params, then joins as moderator. private async createAndJoin(modulesXml: string, createParameter: string) { const meetingId = await createMeetingWithModules(modulesXml, createParameter); From 1e6396a799bc0a5153cd3a0a18e978093753321f Mon Sep 17 00:00:00 2001 From: Claudio Promptoso Date: Tue, 7 Jul 2026 18:16:49 +0000 Subject: [PATCH 066/121] feat(shared-notes): choose Append or Replace when importing Markdown Importing Markdown into the shared notes previously popped a second confirmation ("This will replace the current notes. Continue?") whenever the document was not empty. That is an extra click and only ever offered one outcome: overwrite. Replace it with an explicit Append/Replace radio inside the modal, defaulting to Append so an import never silently destroys existing notes: - Append (default): the parsed blocks are inserted after the current content via editor.insertBlocks. - Replace: the whole document is overwritten via editor.replaceBlocks (the previous behaviour), now without a separate confirmation step. The Import button stays enabled as long as the Markdown is non-empty. Updates the i18n keys (drops importModal.replaceWarning, adds the importMode.* strings) and the Playwright coverage: the old confirmation test becomes separate Append and Replace tests. --- .../markdown-import-modal/component.tsx | 103 ++++++++++++++---- .../markdown-import-modal/styles.ts | 52 ++++++++- bigbluebutton-html5/public/locales/en.json | 6 +- .../playwright/core/elements.ts | 4 +- .../sharednotes/blocknote/markdown.spec.ts | 8 +- .../sharednotes/blocknote/markdown.ts | 45 ++++++-- 6 files changed, 178 insertions(+), 40 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/bn-shared-notes/markdown-import-modal/component.tsx b/bigbluebutton-html5/imports/ui/components/bn-shared-notes/markdown-import-modal/component.tsx index 2de6f39216e6..89c85489c47e 100644 --- a/bigbluebutton-html5/imports/ui/components/bn-shared-notes/markdown-import-modal/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/bn-shared-notes/markdown-import-modal/component.tsx @@ -13,6 +13,10 @@ const ACCEPTED_EXTENSIONS = ['.md', '.markdown']; type ImportError = 'invalidType' | 'tooLarge' | 'readFailed' | 'empty'; +// How the parsed markdown is written into the current notes. `append` (the default) +// adds it after the existing content; `replace` overwrites the whole document. +type ImportMode = 'append' | 'replace'; + const intlMessages = defineMessages({ title: { id: 'app.notes.importModal.title', @@ -22,9 +26,25 @@ const intlMessages = defineMessages({ id: 'app.notes.importModal.placeholder', description: 'Placeholder for the markdown import textarea', }, - replaceWarning: { - id: 'app.notes.importModal.replaceWarning', - description: 'Warning shown before replacing existing notes', + importModeLabel: { + id: 'app.notes.importModal.importMode.label', + description: 'Label for the append/replace import mode selector', + }, + importModeAppendLabel: { + id: 'app.notes.importModal.importMode.append.label', + description: 'Label for the append import mode option', + }, + importModeAppendDescription: { + id: 'app.notes.importModal.importMode.append.description', + description: 'Description of the append import mode option', + }, + importModeReplaceLabel: { + id: 'app.notes.importModal.importMode.replace.label', + description: 'Label for the replace import mode option', + }, + importModeReplaceDescription: { + id: 'app.notes.importModal.importMode.replace.description', + description: 'Description of the replace import mode option', }, importLabel: { id: 'app.notes.importModal.import', @@ -104,7 +124,8 @@ const MarkdownImportModal: React.FC = ({ editor, onClo const [fileName, setFileName] = React.useState(null); const [fileSize, setFileSize] = React.useState(null); const [error, setError] = React.useState(null); - const [confirming, setConfirming] = React.useState(false); + // Default to `append` so importing never silently destroys existing notes. + const [importMode, setImportMode] = React.useState('append'); const clearFile = React.useCallback(() => { setFileName(null); @@ -154,25 +175,21 @@ const MarkdownImportModal: React.FC = ({ editor, onClo if (file) loadFile(file); }; - const documentHasContent = (): boolean => editor.blocksToMarkdownLossy(editor.document).trim().length > 0; - const applyImport = async () => { // On the client BlockNote editor this is synchronous, but await keeps it correct // if a future version returns a Promise. const blocks = await editor.tryParseMarkdownToBlocks(markdown); - // Replacing every current top-level block mutates the shared Yjs fragment, so the - // change propagates through Hocuspocus to every connected client. - editor.replaceBlocks(editor.document, blocks); - onClose(); - }; - - const handleImportClick = () => { - // Non-empty document: require an explicit confirmation before overwriting. - if (documentHasContent() && !confirming) { - setConfirming(true); - return; + // Both paths mutate the shared Yjs fragment, so the change propagates through + // Hocuspocus to every connected client. + if (importMode === 'replace') { + editor.replaceBlocks(editor.document, blocks); + } else { + // Append: insert the parsed blocks right after the last existing block. The + // document always has at least one (trailing) block to anchor to. + const lastBlock = editor.document[editor.document.length - 1]; + editor.insertBlocks(blocks, lastBlock, 'after'); } - applyImport(); + onClose(); }; const handleTextareaChange = (e: React.ChangeEvent) => { @@ -263,11 +280,49 @@ const MarkdownImportModal: React.FC = ({ editor, onClo value={markdown} onChange={handleTextareaChange} /> - {confirming && ( - - {intl.formatMessage(intlMessages.replaceWarning)} - - )} + + {intl.formatMessage(intlMessages.importModeLabel)} + + setImportMode('append')} + data-test="notesImportMarkdownAppendMode" + /> + + + {intl.formatMessage(intlMessages.importModeAppendLabel)} + + + {intl.formatMessage(intlMessages.importModeAppendDescription)} + + + + + setImportMode('replace')} + data-test="notesImportMarkdownReplaceMode" + /> + + + {intl.formatMessage(intlMessages.importModeReplaceLabel)} + + + {intl.formatMessage(intlMessages.importModeReplaceDescription)} + + + +

gNxMrC?>I1_A8r^UsGG=e{8Syt@JlC91BxzDw_5q&A?d};o9Zsg4WKmWY^cfmhjVQ6|~eZA(>x~UJ9s>)fpynIb+DjDGMmH^9sTcA&% zOdvGCpjzCvfYe z{=5FGcIdzQbMVh6g13B7Ylyzj?}>jt4n(ZdKltarfqy;)S)GkqN&S&N&5)|wSC z6Y6X8 z&kIBm|NQ9R@y{#UAMEi?2I#Z2A$e*kGUwOmR?OGspI7=j{&|8g#6Lgu3;&$_PWb0j ze&L_D{=q;0L-WsLMhXACxN!`s3Fp!C`Os=>tLQO{2_4f+&~GXMR#Xk>>ipoJ=W#c5 z4{-l+_~&_`K3vo};go|GXEmto)xzXJb^0P*5>t}tFj;N0O}Il`e7+sOhyTGp--9`` zHlS$}t|R;aJ_rAtn}m6@TWog_m%l#$ys2@)C}?gnQ(AMan9xZ1+x&SaXbFBB{`vJE z{PRfg&z~A)FzK2I3?|iT38qLn=KwUmmO^p&+wjkKMG61BDpdOtLD}_#RU2%WY|!`% zA^H<~P9{0P==IZ)nr=$$bUA|tLJuQk$gYjHG9 zeXL#p)qStJM?IjPLN@iC%4?~?y6*wP`yDL?@+MRCn|i3x(KuwpGTWNRLFyd@UAkhR z*a%Rozvkp277!G4x_=-4d^*>TD+rCK>mU@@VdJnDn4wH|<_SF>3X^xC;uA_80Xx=# zYC#_221q>7osFQ*goELk3k=YaW;XK)$lzt6&URQIf*e5%aKX3ZJ=V`l~IvqgMKMP*M=62Ntb3OGdG!dAVp8YjPIA`pXYRh15#$JHY3H;j*>CMq z;PN<#BzIvJv;*|llR`cB43!9Kth;E6DZ`Aztep!=Pu_t|_+(ZtpBG-iL#P#6@ zas#;DTt}`E=E}mj*qk5T_zT&NY<`x(Y|<#qoIBv8wt>>l=lJLPcg#Oe$7SKNa+#oE z77u6eBlOy~W4&A94LwkwS;#bFQZY}V)>adGwFf~XP6jr3U+@PmW7ni2E<)8jC0NV- z?6meR-1GUZ2y->`OOrxXaH-MS$YzknReiNSOmCu>&@-T(=h7`r(IidP2wl_@>tT9% zy^TH|dV>#iFEj{y7#oduMh3GZ^i3_Zlr;^S+F>~J*X=Y=-oEIhAbLXoH!<|qzdrxG z)(`&qcg#Oe#)Q-Rpw?S~j!s{tKF>dI3TDC`YnJu1l>j&Fa_qKzrpLSwCCqV9GA(0d zFk-=`e9)gj`|_H874@kH`fFXqe0eDJP3su_jK#)j%+sd?v3LwLAPh4X^xd~xij^0t z{>NZFszCAov6BWm)jL7WE)Ff=$nwt*M~Z)b0sQl_KltZA_~%AkGrL*c>;diFqvi)p zCY7~*@Xvog|9nBD_~+~XApG;C^fY=Xw9jinAv~0(=_k;sp95{wvLIyypy@vYK5-6k zGcUnsZ4HfKf;b08S97os9s2@EUL8QSh>2P41J(?yGor5amI&SGYp4vbFsGVB%l zH#X~;waq$aeX#4>fpR|_n&xYuDSXQen9-~(Ru$Z+b3w6xi>gjhyAzb6FMwj3*=Y>T z<`a(Uqz7wlBD9nIL~?L+zApd#gMA1J?k(*Cb}ajybpot`K~^KHh?NTO88jb4<$9mF z(OiZzHyzaaDbQq{Z7#yw?L?L8miZ1^kO`ssTFvTWO|^Dl4x0c2rivXk{PPra3~124 zhNj&vD25LJowxw>=_T?axRV3O>SS6l>@I^M)srX#%F%o9KqouRKsfO@55XiE3(imh zJ1%siFI$_S0NxE6;6pvhQqu;xXqI#|QW z__<4{TEzx0v=PWfo4`>ktZ>dl5jznGi*2A!y94ngNv0x8k*z^3TZ8ENIZ0s8 zWuwY~bkPaCi%H-ltfDq!NAAPTa*)~&rM|7yTCgytM}mLe=M(?j3;y}3AN+Hiw;%lT zAN=#}$X*4U=wOjn1j&93@%#AaAIT?R*Bt?+cs?STu4Fxsv(u9?KqJ0K9D?l_iF03p z$V@~h0-&Dma^`}0+87MIbl?t1pg|u6n`Z_%r|rREDGdriIuPokgL!5{J1@d|54PD~ zp7;1%L7yXs? zmzV(x>v>oaf{agQ#qCp{?1D`5ED$kwBkyw^JgK*20F)O3+$4-P;Ff~?*N&(HUnd@6!fG7@v0}W_8GO?GP707R z{m_s-W^V+OZ6w%y&A`Vj308I{JEffvK7!8{aO-k-T%Z$0gFQ_Oi=Per?DC+8wMG79 zg1yAv0X?rL=wyw7-&=9;2YdY+{@_T|Cij9?{*GYCgm}vmU=X!~9iNE*-|O(6$6?oR zkq>d>zJ{jcdpz&(*)uThZvNn(gTVQNfBu7i-U5DTT@W#=+tu(?$7gl%l@^Fvdg6{6 zXU{_Ze;ZEowNE5s0VLcoaO{8Z&sV`mzf3$M6wnqE;Qr713pKj~NNKs^z zg`b}Jpl46Vy)+u%>j|=U6VR|q<4#S7+t~$*+cR+D_7SVVr|M6%#Lb$QNJ)4J8F%Yx z#0B#Z9dvf;;rE`+N#=MR!+wMN|BSuQ-hi8O23Bb(;-zj_vo=_{W}luGU=z2uyMRGF z7*y?Pplh!MujM4TiZ6a4N@oIxxTe$A8SKn()`8}D8+9y>NDOVtvP2`I2S~(=KIeV4w=)S*=q)cvxu#B zfmpBz`*|qhtHy{v@_`HzEGNOH4t$@$=Ti45>{S|MWp!9R~nr2<{L09BHz zKvkn^QMK{ZqN;-lUYaTdDsvj-EIpJ4&ifT)23C;cVRLF@Ekkj`J|m8UHZ~l-KrtdY z>K_kbyXHB4ojOilCjq?G`=}5s#Y(hCe32iN@94ILO2!@Q9BLw4t<}iA&P8|pbo4jO zux48e&>^w`8QJ5YLOivC;Jhco$t{ah+#k`%Hq2uG;Gcg0T_FtQwJzjjWNFSI!}06* z=Oe*4ZA;a`nr5exP#h@BS3roL4R5{`pZ2 zoqZZN>PT3`(on7su_f!CbqoZ}x#)H2ZMC%;S~XDJDrFTz{=A4)94y{S=qYFl+WSC| z-j`TAku`q~GH*gVmt7Nip&5ur?n1pRrBep|Wizm6o;e=e>9t{}S0Z;6AmYKlX^4nv z33ThfF8{oyo!yQNhW#DuAn2?U@P5rvp(=(tReE$3CP2j}CZ5<C6*>O-?H~Me z+&zD6{(1fXA^v$Cx&#RKF~E7hPHjWYs|{6@icfvO$(~9!C9^@({37b-t^Qg+Zw8Ot zMh)W`47vSq$vZnr1Vz zn>h-c!5yf`el$a@Fsmlmyem+9HLdTBe}3wJfPW6P2KeAfQES}=-eN7(RUN43Z!*W3 zZOrm!7BeoW?jMbZpktrL1kzzV$BpyGP2(B*$^7nb{YNc?Wf!k*HwAuWw_q1G#?DM++Qt*`!FQks zYlbn_7-kH@Gt?M^UgpKd7R;AFz_e!^knZcEBV--=B6w7t+k$0(6PBbbs?|q9b1Uo& zLPvt*6o!R7glW)%=sx|D{PSnOf`2~r8}QE$!n^r;{PUxT=L^`TwGVzvG0SCLM(0Xv zGdFr79~*nHdOeKVMqwkJ5zi3N7pdtodLbaI`nk&{aJNoVWXo4ccpVwhX2M zT7nVT9ZzSrHCrD52b4DPCd>6N0HrW43R#8jB z)PGYmiFpr{vL-l#66oNw^iJrT&!9&KbK#YCSG%HJ)Gle)p+EmoqxD33UcD}uzU%Zm z=&3Je3^4YB3|<8H@)h&*{PSdyJe5l4SAMmPxwGvu7cd^3}j^YIok>;PD<`(^C{&{Kp87AP8Sw}DnDw^AI z7GoGE^Z__)hIU9BgZ0a(aoThBH1wX9s58~c>J)XBx=h`no>$+fUaV?k@Q2Sr392-1 z+nahSSk<#&f&MrC`D3aWy_mM39KDvYnDXol_8xR2>wxom9NhYZpt{!uwRbo_nV*Sg z3O|DH&NtwT@JV?E{P3CBw<);C>|C}AOR*c7+L%~cLKmeU!MBS|ttX3-x3F7DV!D&o zIe=W%TWhG5*xG~6T*FvolrjSPQoW|`)6Zh2tg4no`=B0E7pa5QR%#8kv|2Qubx18C0`#C+XWP1XwNqx4&P26%iAjQ_?zfBHN5=i7cW|2*+m=AUn96U8x+ zRmXXbjJ|`&F^RPq6Os>&K1O_FFD9@=eIM?bwAxE`t=bLi7GDi2HF5fjA~tVB6hM1_s|UOxt0f$lK+l>p8cQr=MN*zKPNzQjST-hg|*2nVqP=a z8mzGd^Nw$DXO_@(bsOF=kIJeym5s^-rHj%)sjQU2Q%R|>bXLYH>y&E>rRG#SshdFl zDxyu%UTQ`4MbLz51J9?JxeeM^^U*p3d5WDF2+%0H<-B zo)%}ZvzACZrw&yMs)Tw;K9>fAbIg^PQ38pHGh@|NMqo0eiKCaS~qmb*-7EsFT!8>Q$w`l396)z1dFA zD~HJMz^L6XZI(7jTcm^1Rq3M?Q!XTT0ZHkDoLd>G+*k6c^HomkhP$Vsepjz&JTh9E zesh2&VumKObI7R*wrdZP``!HWL7({ND_oUaetrUQ&ccpAyP8xuYB=8`61cr8G+F zA~lohOO2#9Qh#Zdv`2a_#lzm6Cf}DcD#Mf~N=bFI8VWlSO`o9uJ^#F={g3?f81Qqx zHvhbZc>|O6hxG!`sxP9QRqLo9l`%>N<+?mf&L`_ww+VQ=qEZ?uF4$5b_%n@E1eD@& z_>L;&m4|}jmL5OxQK_SzQ;R`=pfDor%J6wQB5s}qW!f`#GuV$g(EYwZwxn$E&r?Q< zf4)fQ_&>luKN(5>`4}?-bGfJBjc?M@X=~Ih>OQ3cxMw5e4DwxRvQ$ZmBRvo9hbrc9 z(51Tsdjy9DX9sr%9|V0;DQTp1QA#HFl5c@AuwDsO=Yr+7SWB;eo`23ol7F5g()@GU zY-wD=UfrOj)E27A)Rjth<&@l1Wb()Mvwj{PYj8n~CimX*xbFpFQwQ;Ye)F!L6+D|d$!LmVT9v1YC^&%s*d6 zSB)h9Jhx~@l7GI(h-S3bPig73sqn?SDIeudvL=m|l7mLvE|@fUC$K2cAy6`qCJ-wi z;E5eb8z_aZEDGEUga$hVj|YDX|NI6u`V;?rLZtZT6(Y$$zhR^{`sp{2!I-6zYA4*P zP34zTSBaOF1YLN&dNH6a`b^l~z<+jHt1b@&vr)+qhH1gQJ3JgV&&! zTs$BK?)i86=ljR|NBYP5=Yo%U%g+W1Vh5iMB*ne^JXjqbQ3bF+nkv7Bf4(u2{PWk5 z=ASn*))#Zy)Rp~xf$QRreXc|Zqc;sL2ALei2ujVi7FX#W+-_bwG zzt8XYhXwitE(J0Mrv)jguVhF=oiy&-2eGe&U}m{nh;Q(|_fk2P4TpA8ee~ zW9u!o{m5_h!8%rzPr<@p2-XkEfmwlK0TpXF*k8k+4ZPlH{t$mWe@1_Ke|P_C|0{o% zz>vU$Kyj#Ir<2x5Ve(=5XGKzGtDonew}-CRpPzruMUsC$%v;d=UYsUo7LRx;c+LwY zgdOgrAlcvJv+{$uGi)@rDl?frht8~mWOrh%^TdvCSGC5QXN<4UKYtP_{y72u`7rR$ z?|+y4^XC7=Ki|vrXY)mZf9}Ft{&D!{wIazs=l+g=zTrFPpJ$2$|9rD=fUl@e_pb1k z@jeziiIS(2=c!OZ*ywh<>$}$Q5zsm7$gV;iHiRxjwI^p2$1&TQ#4d03H#b3{{%i8j zTmKXPys?`R77IB%hdsr_ePVj=MDWh?`Ud)T`Cj_mA)z4|LehsM4dFon+wSY*%i(+G z9pz2#-6G}@cX-lzrU{C>ihDUSAbI#c+z$2wlboqW4@M2-7U~}voSOC!y4BdsAO&%bgU~1=E@XsfKe_oIYVIO|tpJ)72 z^Uv2sl7IflH`$ldcNu5$rPx%w?kVV5Ajs~L?uo7|ypJ!z^<QA3h_zEeP*?^1O8f|JH~W%_&g7=+Q-jFG#1ZGI&Dv?KissknpSNd1-Od8qsC~Asc;FeQ&%yy}H;{eBvqXSuW`AQtt7t^E}Jv;aag%nZ5Kw%rzw^ zix7>SA@&mMxcL_9h+m(7zBH2j^CTe`ecgT0e2cuPyo<#cVjt|*EW$wdNter2jPJ>< zWUn#?os2F@H6@1-i=D&hyP~bs=C|OVw}NN>Tlwc}LVASc40-FD1`BfBTfuuyEF`W0 z_q3U?-R-!FxCZcBxo2#2HZRkV9zrc8j}ng^()o7$^9(HUANl8ne2aaGuP~Udn?i1c zgojumc1R%PZphA%ks)P6=#UM*%DxxgPVjeHi#I%ZJ=29(?(FVft{wabE*V#m?Zqsh zk5ErBpA||JaT?o$t!3tE<6H30k4BPz9w+3uueFczP4dR_P84lVb9e&rgck1guJ`;; zd>w8yyN$U=6LfN_7}@2nqTTe}w`7QY8!`YgV;-9Asxrrzwp>Lfxn|HIALLBe; zfULlD_XAg`s~$gv`+w}61(a0B*0zsJEABqHySux)yGyXa-3NDfcZb24JDVLBKgRq%#=o1{jcDk zmtx~`Pq?*wr!V54cW`HRKXpxXWkGaa*?GZH(y`ed8zkbxqEoCcO#0|Q(tW~z{CWKI zdElSdLdBI~ud~a!j(is3wXo=S@XxQf_q*r1JGpbX-?`?winy*jTRLAjnmaDq^V(MnvQ;m z&Fk8C*`nK;i|d5K@eSR8ul2w@0OcgqunoAzQ`-8L6n^lZnsPIIc ze_mHwC)tsAed%rOd+!^JYR^8oP2ltV^EPxLyg$B_f8O^a|NO0M9rgc)e;z40yzRZ0y`_AId}aMN{9WW|ft`WY!GxjT;-6pt1pmC)--v%ciLDg{ z{&~17hwG_xyfeLXpQE(nxV?~lEi7wi@t6=>Xu>Z=*K#8G$dU98>M69C3KQ+EndVXG z>!#MD#y@W&O_#1oDZG8WH@zi&k-j|s1O6)Vb9rL z)!3EHb=aa@$bxu8<9p`EgF*O0$ER9rPrydXnHk$*0@jyStI z<098u0&6U9KVZvjn<`2|IbkmUjLX1vV%IQFFx6895#3hei51%_WA-&R>d&<=Euvo|w`W*kf8k5*vr_D*$UWJi_yd`!YMv6-=5pb z2ARA}IK7#Ag-P_f#02X&bS*IHhuVNw{Zjt<@L?$P-D=zZLH=L`S zot^2Nw;V$qNgZ45MeK)d1yTJ-APx|2^Evnt+(q=XG-ejj_i!U?lM{)P7Ht(WdmH~9 z|NNOZr!T_y%2(5W++X~6^Ut4we_juEXAjqk7ljo6f zwsWj=oAa{sg;R2RoKKwRoa>x}QQZ-p`|(P)JC@mVqIV~sZL^q694)-!OY`%%H*7w3 zD06}q>2lO)@)+`0g{}VPZbQY~Zq)ea)6w}MqT^$kkMOtlU-p-mPlNG(Kk)nb=WbB3 zI&*PB zsO0e3C)rclHz5bPK}>;6tIXHpH*qXii(SIJp|c}ujU*LlVD+|knJT7~hoZI|TWhIC zDt{LLydfQfInMNAQ*bA__BSvY~5_PZ255x@7q&4nqWP<9M>JM9RYma zIBqz0JEl6CInp4*Ivo{(`?lUT(KcC(iB39F=)&LN3UISnFH@G8OTVV_Q6oWDN=US} zHp8}7Fy`uSF~d0z^8$^OW1#S@2qr^?AZ;L0u8QuY+5QTC)_=-36Fp^>effNud>MT? z(P7rYHyRO+$Cm?rNtgUtJr(xdgJw1ylj5<$0E+)U6&noN? za*6B3#I_N(r?!IjvACBmM=?i7$2i9l$418%$6Ci+oZyCz%#M(Kr@b?3bcbw>ZGLfx z$crNdi|@+c<4SX@SdMMR?4q4?6KWH9XO)O~)@x7!#zHk9haOe_d2)Xf|4M(*Usc|O zzVk5wGB_y6V2#O?9ZEITqfXWG>yPybMnSN^=2+zkg;+(_qDXoTU6t`MQ?QeVxf=Xk zzL{`es4bolv)}|ix8=4EgpKvs6FCYy>Nr|st!*9k5#5Agg~#k;>?Q00+d^A0+ikIv z7!>*o3jY)T5ZtisY&^CXX0zceZKv){T+N}LaZ&hBd&vXJhLCbNv>m$ zV?S%_i#p_fv6lEm=!ASn2duF=cYsaJMli4Ga`ZCFfI8Y*_>a0^)^TQ2PKFKlKNQdnOV$S zU>N#;!#~d{_eP8p4|5<_(QUplm^!o+_i;UD>^{#wkI!soO0t*P#@u5Le@4E#a6m{S zjuP+V#P+uBvPrh&_Hx+I0rm*{1p8=vKYL4iNqa*3Yujc_4y3kS5&OZaZWBtQ5?P18 zj(t4I=4V%;Qr8<=A4RBzxRH&By;ef2FZ4YAJN|h)bk9k0(ZI}r9H<{W9?Xj=yG+U^ zC6D^g{PQaR!9TwY{<*@8K*TedbMvEljc+5I7cz+>#A{+wTQl1n+ZmhR7R#Oix&LCY zq&e(K?Uem4dYJp6^C2j%!AVtxnL=h^KVJ^>l9jN=BJ5@+88Z$lJPlE+OGS<*URjl4 zTVtF3(F^hC^Uo)O1-g;R%kF23aC^BN{4ze8&{Mb~WEA_0$3z}BrMqpu?SSou?Ire9 z!ar}=_Txvo<7ar=adCi{QM@YjLalWbpNBulmBLvq#zrz3n0d4fvCadiEN_BJ&Rp?5n45pn6qYJyV_OsSVe~W3Q7@%$xh7wJC zXeQ4f(}U(wiM~y@X5KPCvG3V-=s2&;NAih;e!?Xoh1da}pgnjvFgQ>u5Cc8SN52aHd=Yxh#&QbR zj6cdJ7kUdPg=k`Rae{bAd?QA;Wx~EyhK;FWD}lOWY?}lhGhJ+e7sn-Gn2<%d4DV>? z7a-2r0siS;CO@+c-eU=-vqzC3s6*X=QtK|LcrJkYT7=H%L%{(Z4dsW0PzpGWIc$4q zd$4sdaqu>}t2+jY1`-8K`K5eEzJ^!-V_BBnfvo6d9)oE~B3K%8hEIbID zRrMsMBtOSLuSwqo|NJ^rjXi-^^D@p3X7))w3B17$AtdAw+lW)q>2eJ#RYlTfi2=+M zoEJBUBgMLi#h(g`h1voqY~-u+9&R+3kXwVEq}@y*W-pzO-bQ7lmSAdNJp4yEB7vsn zb!f_+(aWJ`S4q37Hd7^Kq>@ZI5Na86hYkjN1#@AB;CNt8phuuKCh4*U(&3X8YpxUM ziBozapa=240uS zEagporN@bJC5PA@|Qsk z<+-_RdUglac!(|m?@^T61D%$QP{LgVWyonz5gcdOjIp{~p8=ZSMzxrFL+J^I>)ucs zbSz&AP72lvCJ*{B3%MaMBQPS+AM=uf0^AOH8j5I#SKhN-4{&^9xs#sraB-XI&H3q3#w5AYlKro02y zg?L*ta>}{;< zRuZa%)mLgA?Ig4Ww(6OTRYoePq^1PhH6@fx!=RU#6e>Hhpv6HjU77n#S#|?@u{t2O zi;b7kD*h!OCe#&1Vzs2`&aPK#|L)=p9K?z>f%-PFqB0Zs@zm^ zK@&t(zaIZQy`b?Y`0;!t^v&%BlQ=H7k8Q&0%uFT|a}c_G_o0(1lLJVW97n_;W zB@`WZL;dH^=bt<1zVuVNB4VECY$x_On}BP_ZRJ8-9^A}j{AElqCBm7kgd5sIXeBfi zY6&HT41z;=!SCb8@wNH*cvVe?ZOD5ba)a&tL20mrVY_P&p&TT{Y>SD76-?)WsWgP*>G6c*jyuS zG5VF_@fG>L{1W(+2Ydke0=E!Th$iqtkblG<$BS$LUlsYdr`$@e6_*$ptHEprWFNzs zILub?PoF~Z)CEO|Y~*2Z6&`}4Kw6VQqWQr;&jU{OBY1|Tu$_HFokQ*L=@RM}8V{Y8 z!=WcoX~_@2xLkRtgh3Z;t4cwSV6{f+jluK#HU4=_hZNl;e7G^SOiET~6kBJ~q~sgipxFKvvwtUE?CTKc9a- zHj4c7>faIn{0w57c%W+cg3i}7HKo>CTdVoB!qAueE&h4WAN+InF{`uD(KGggf8LWR z%+Sm+dN^H_R;eviTPhJ4?}NxJj@iRM0|JM^HR=}SO)PpQ9z zj^{wc>dCaAdP&`^PE-3q*`&5w0h$rT)Z$ROs(~oJCsb=z!Y95~V`_z=6}d>ern#Y6 z8=;?pPHipd>^wJeK)LXonaJvB?SUe817Z#GbY-BQe;@D3Zs0^3R1tb0y@&SG8JNb1 z)ps+G8JwoOn;^V6PI~Fub|t} z>7dlMjA{hU{mKNf_11c3Jp`4&8Hi&`Xi0wX&;KX* z=RLK_+9vH1^l6hptG6!{81L(`q4Yk=I0D*2QK+RJ23w(wHP*Ud#U$#0&T*GW25QG* zXtpPTI_FgC3`Nj6>6Y{q=bOZM?hnb_y5vY4^$N8MijDV70 zO(r)J8+|LM=|yx8x+EPF706YfrRD}xVjl>9MZoFZ2g+eS=AUsuP>U=8bzvD=!h8Q``RCiU!`Q!P(7lWr|9l)+Eh?FrYKHv4A?h8f zTSe(s^l0cKAEWQn0oukSWYRG?pv+nXpMp#t>~M1AQZ4!={WHCdoH1|VExf-YsFQNH88r80{wW}5`|w7t zZ%#CiL1!nY)d_s1`&K->2qK98b^dvDIy0&l&FS9sSb82RLEGtl&;mOO&9j5_E_xIF zSMkqRLv1%|{PV$}f8GF-CIfOY;nZwuH+7Q=P|@hLAh=hhn}X%uhaN(Yq9gDbMGwX6 zx(n7{oi2hi8k1I_qRCg7;eVt9|D!*MIp+zP zNDS4{HaK4!z^Z*ux~TM2DNyOdsj<}Ghks7|;Ga`np)orgyE+N+&@_Ba!uP}RV?R+X zs9I3>&W_5eNO{RyG@Ri?GolcDWWc%v!s0lq9pbH2P#1av zGV2Nu6+3}7lNUUC8>l=FjSGl@HiJSi=YN`iehY-KkYPg{kq!LmMrJs?|7!UEho*_Q zRRL(j_O<3%yRF-nW+ejes3Gp;JYqL-6I7m9u=W+ey&i}f-a0T!u97dv5Gf#TNr5^| zE~+3^4EMAQKBd4hDTKH*6BS0q#SW_EEBKzH<#1|{<@7!Rw>$!1@$&Z>ZHkq+7)HgE_$peEflu7O>078RS* z#u?*0$XK^PKYIg0I0NED3h1MkMs(8!O6l`KJNp@YSk_8z6}B3HrZy81@(=#`pTj>d z^n-s6=0au=6~kbSlH!vL-=#4#fu~gvXS5pbVF#S>2ylJYn}@*ndu0+<9Q3f22EDPD zH5nSpN3DBS$cjN^B1(gL+80dZwYWRC2#MfPsmMl_Bx}QC_9jPxsJs}Q$erW?5be*A z7s)GN4PM7xy$oM^nmj`8A-9k#$+_eNWN>~0NvtwBl4;18xQ#EsZvH&~ycD!Q<64yU z78TOtpw2A?rDr&7NIOskYl1CU5;VemcyH$X_{oFLts+=oC6LXUnH@o09tE}g73NMb z)9!#*$D-Pq6*jvuXrmLXl~6OiX?c;`NeR1C3!392iTT7fuq_`#S=mk|!759Dl-LsV z%fTS+%|br$>%->|zTZV|#g8wArI`RqS`XN(+MtQ$L^rYvJ)Td%7yGcr zu|#*|kV=3$7K2c%ht^4J6S7!?;X|rhKltZ=H2-`ge8?8!1aTYGY>JG9$R{`Uqz3XK zop5G{k>gM$nFA*HQZN)(;HuOYSmi(X=VL&0pGMBae$K_$Y{Uvva9c;h+JuuGvEv^Y@Ap@As$L}gt~B1+{l=OVLbzjbT@Jz6F{(SfoPzBl?Du33K^}3px7QZw}ZMk z&zu5&=3v;BZXYS{ZLyziu!rsOU)?`4-p9Z`ExT$h-K#ixkssOM)8H3|;JkMKxW+bEV=H{M!|Ck`GWY;QHxtb{ zIM-Xu1Lk?eOYcCBj1E$49)J zM0O%>ReG>4%V2k!VVA2Sm%eG9EbZj09Lg<*%+kFlDNmI!INf5 zAMpTm%3bKR_`yFfgV;5RWk>Y-4A$cqPTwkc_KEP6{cut{VE>w$4H22v!>2xMPc!Ui zCr~jz*qx~$^R5GH`!pz^ZxEAz*vWk8Mr?tY^#}j_F%kM031lUU;q*1bUF(lqHx;LI z8GQa`P+NB)zS#qgb|kqCuZ`7+5oW_44aco*PktZ#^QW*m(a4l!9$1|E_*3`6O`Hbb zxCU{}ZbUf8$y4BIefXS4gm;+SOK!(&Yzb~^1pYQU<6c&Tef<#8S+HN1iM_a!<3QzW zKolp^5^h2Ucm1%n8mFlj81H4QELMEXFMWvUj+tA{#pWbfn{GIx_3+**1?F3Byc9Em zZJQ3cn~Y`_ysGk{$GIH1;7xFr`@jOvf`#1+n)G8+F#fBT z$>Aew!Lp7=XXFJK7n0cixWK-JKvhDhH5}+ zM6LnUkB*||IT1Tq7x8g2f+1eOs&9h#=wmgqN`TJehUd6vo-jAVGmb_a(*pUA5_ox} zH4}sDZU-UUGBiWM`(FX^lYr)u6Ftt!u(x@^d9MS{*%vgErSLl!5c61Od@BbmPdmgr zi>>|02n4NIL^iPGI^f0rzrjC0MBF1(A~y0K<;mvYK~F+tx*Ip|9{G;cNs)>LUUX_? zIkF-9ksF_!R958I!!Q>Zjbf-ExZu~xW3Wr}afiE*br2JTA$om-H^^p0i#-tUqpP7S)eH5FLHGZar-}as{<-&`_~(bQ9|p*h*{KSs!iJ;&Y!2$-+o^+~fM1}lP}lIejPFk3 zN4DcKTi+t~z_)~x8ASI9fa7R&=Jwl1;*ZuEH^CgvhOuRw$mDl*DztRdLP?1)ouf7%@?Wr)N4vc#|uTX(6wZs_M}o0 zy6?UZ{`oTEXT&(E$trj)Erfl1g$hqrtgth(|0}5dU}3+cbjnW02Q?rQorBJUNq{`) zmB>t|q7z^?(7@`iVNX|5<8Vi-;`chKm&h*9Cwn6H`8xb_4OR2hsAn}WdV|@r3wD@< zRj!KoXBoVuZl<&9AmZ6>J%x`ffm5^uFDr}4NY+E_xdC3lB2!W2sZL;)u19a(Q&3)F zq4Js^HRjr=y|$q{qW;>M?tl)0?~Q++4RQD++@zOeEad!KQ4u(!KU421C)QUKt7}j9 zqsP$G==rFbub@}b%fN4)i7xzsSnv12Kd14^ZHH{eSyDrnYb|OpY{>=6N5!FY(KWD& zk?0WEfG(ah^i}#U{g8e{KfrX;Ww5gMpmSg*s_Jd&ia5PaXm}i@=7XSGhDt!aMkFyB z@9pHMW^KoNyetuoxQR%sJ)$nd{O|bZ&CroFL*EXXxT+^J${9Vt%)Ve)MmFTQX5+ka zh%~cH0EFDJcu(~U$KCrX#Fmt{MaZ_>9hAx4b$bF=M zk6dSUgMT!UlZhJt{3-b7BB;d$qQE~7$DijpnHU~_1O?R?Dh6E;Jy>Jtb(l?m3G#9R zCNsJnD>Aj1hD;ND8sfVOnAFLLRsR+IbLB7RpFjPo{PV%uENvSI_p+8iFRph2Rs5`O z={XS*tw8*f6nWVN=6y2-*ww47H&#}}%#rXNg^-^*@Tc?73rCTEp7)#MpFbfNAnJ)m zo`l`WMZC6_SatDwJdO_Ayr$3Ch-fsk@g9WBetJo$L_E;8YGXjNEutmGWZ@eyTTg%l zzE$0*Zc?|Y`_xnFO|U;XEe!pyZM2EnE_9y7*DHf&d{Ec*0_guZXmIHJn2y(JTJ$9z zuxxnwEFsG6j71AHhHO z{sH{+48OrYpZ1;c&(G*R^rWC>4%BjJuQBt`M$M?o%3&~xJ1S+tC5(>r+`G_I%}p}bI%fR;8Jle{UlF4}%A8aU(o_4r1(an;C;EUa!evyNHmiAm_7 ztB1UB667)NVuElg{hBUw^$+2n7gDAv9%w@CRFi4rHB0NRztUS8_l<_A#x}5SL7VnA zQJ=g?)}}5|Ux$Cbk}Z;XH53p*K3 z`B1HVfgavl=zTbc3e;ib*Zx2My!GFVf8O(-_~)gg$UmP26^w6*e{Nzv<~`Mtx`F!G zZglR(8vkrAk2 z?M4UnI3tAl*6UH?pO2UK|H41N@=fv2M^SH4Ro{-fYhUXgIvG|#i$7d{j2z7ou)()O z&tq4pWavt;J(OqGK{rSY9F<4P)#NbQ^gr~U19kWS6k4zP-}oJJF1eMwP<{Y4^S*(L zfo#E<&=u?j<=xJTr1U{XWQLX=nnDdw$D3y6ur69Zp|fKHS%G>^4W{EVYnlAeEvd|1 zg!(05pl=!m4YN8>X=?+%axJKtCA9~kl{o=4KE}2c3Q@W+ zT}Ufz=L_(AxIEkjHW@pCQBY4fMx}vj>kIS>FGu%d3-btS7a#cN4Wh_DFa6E%&keRC zdmZzXOHui13AMJwWN+dU`sPNNkBwr+BAwRTMv;I1?wjPFFQs(!JpZ45;s$hVG{MUw zuHGBU-zC7YPp-@i#SBe{w%7VViNHhbQZD(me>G^m#r+BWhVP~CzVEK@sW0e@;m-xm z`z+|vCxy23NjXhmV!(p-t~b~-WQN8m$<#=-7P7Fj^uoq7W0F|_9hg&)XMI6Vrt+ey z@pbs;7nv+f1Qd7jQRB&nP}><{-9Y!kAmf_;fq(w$Z^1uL2(`}(s9gR!|6Knf{&}N+ z@Xs@SWBl`jm`ra=MV-C%ZKOil%XmHhR?g=eK>yKJw2~$>U^Q?h)_@`UZKZ zVdYRRDm~RiQ2Oenr!{UHlg+Z0Y3(37A#?RL`R5|rkXeUG*pfJ{SBXSK3u_IO1q&LZ z^=n#MZ9tUx=VQS?PxMXk&zCb^)Wn8Ur_mEp9dE3gcwx0L)QPH3Jbl@ym@?6;4@qL zZ~5!XkK}FvF}N~VAtZ;ED-G0GP>q%lOv%;`+z?ozVUcgLY)Spcz~ps;@*-&#Gg!N_q^;Q!X<*TY1qPaE_coHG(ef zqp!d}Kf}f5>a(+%YpC~>p$3qf!T*S7RRnE$m+sTDY2DNv3ZYa9Eel$K=7FsWU!(rOBY3K9J&Jm8-l2*cj)0#ry~TWuSN3Ip7Fx z4mJw8mE+1VwS*?<7xY=ipTR$m!#C!Zun(C;Oig+WwU>NHBqgeXmbuM%fqFn)ZH9VX zNu;z5?FvQ{QG@Xe6PHdyoJ4rv|k!8wUsJMMWsSgX{kO|yHvU(>4eHOaknaBo21e9rC81aotphoP2u6aRa1QbkD1qTHl$ra>%SmPdFS>HYH zU~dNRL#(K?R7y%K#g(E-v86Op38@2EE_b9o^|OJqP-;5lk8^o73;|2zlx(^uf1zmN)eXM)h%+4sWN30#Fq za^}G0z@T7u)PYtjozz?!iPO5s=z(Y_G2tbSle4I=!$0?ZI#M|va)#>R zh0F!o%9s2l{Re%ed{?}kywSY}q~18APU(f`g6D|mkms!Dk%yJCV=p&KmQ>HX-ZAIae8BDeTkT^+d+&cn^U=Ip1I2W+5Ge3S}%2@ z@+Ooe)H`@0kR;Gwe&ny_Kj|y)y9&ye&AUx%FJ+K|p7Wk9o+Y07p5>lM&vg$e<%Rt@ zAtm(=@xBMO?zyjIn_W_;dK@CAhwy!2d%zxEa!*|Qu!7F$-NKK{0(lgI)&m7N4&p^*m&lJxF&sC2om6fJQ zFQl^GZQgXgRle;0qy8o`6<8l=0=3g?p}9&+HM_<@r)Hxu21?3bpMO3Y>fatFDO3KR z_~)6-8b*J8rFLDlsYR6mP=^+Rbpsn^w;b+&=4}i3| zPo6QJ^`3j41X43;yA;#g-|O*q^!a@w{7K}4a{EBS;N{?yP;I=J->C<+>3S!ltQm&) z$1P$9`E~i{G?x>$bv$Zzw=h+hoG59vMbx?t3d%OEfZ9b_6?z%W9PAr7FQ=C$_%&Zw zpXBZ5jpp4X)dL&+B2Ha(1 zqyp!?3U+n1=Y=Ob$Y8Ie+TL^CO1_)$AU4oW8V6zq&x2c6Pf4RH>UnLYKGbMt7O|2* z+58T=8OrQa<#({G`+YflTfK$A zIO`@Qk}iW|-@;SKlhPB{6WfyDm?W(K@L8Cm_F|bdLE%)@_ z_f_+q_tx@0lZHv@rMsSap4M1nQjgnX^ThC^_LT8-_pHO6EGo^DC~psVke)uyzYy`x zYk7X4b})A6PH3glTdky})(KQ4_nQl>fkYFsFqM!F(ifPo!9Op~_Xd^c*ZJp_wZ7_V zybh;(Ky?n&A9i^P~W! z@19i2d)!;jciq<+Z;%~w>i`%9!9}6=h+ADyu{)?QFb0~9t-?f7ys_?5yXmRSAI(2E zh2p{hejE4nKlta#&0c8tBCT9ygM5ONrr;aC!CzglBDjl8_o>Dli>pebCX+-UI?;x-49qxM80(32XbanIauEhGj(3%ej{AMcs3!#{7pMSPNf-cTJ2x_}le7#tS3 zB&U-{`#rv9zB}G#UP&5<`1G-7v8SD;GK2W*UWGESI9L3Hn4djLN%1+ zs$V^$Ez<`ZO+NY$lekOnp=Q!QF~80~f9PoHyy7ej`uBTR9(Q;5GWQAhefMkkOZOeD zalZSv_~$1%4cfgmn7;G^RCyl#;y+q_^dGlB`i~x9w*&*l1Jl7ctBKb~Zr@ICG4C}g zTuLt8@XW=|mB39*>`CNF<0%4fG8M0pIEdO$O4+?DyeWO_eMOMp8Hn25oxqG>9gwnK zD|^++T06ajksSIvAN`b=P)BxHr4cx^KJhx_>YKJlkja z=lzwfAqhOtfq@G+tr33N*UtCc+XWe&g;E(wMO;1tez6GlGQB4|yhmHC@vJAB)I{2Y z9M2T5!#5cj^hkd_#O7N9?SpAUZ$dki5o%p6i_RNQj6>!^YXH%Z%!^s1xAYO_^ZfH= zF0U&W_HBlH8}{<7`#AnR9sixv{agI=8Nb3mcNjVK#@aY_zv2&N3w00f1qiQ|eAJ)G zztES!x5%5-dq(;RJ9*Eu!qdl7&r=$AvACxOqVw6Fa~>BW+AWC9hj>HYKJbxq{CVZO zcu5usYQcS>;ppH=p#`*a`buNC*%a@Ogk+GsM6IC*GoR<5_i>$ax!q;m;qF=P4ep&- z;d1P13wKtm@$>xiQ=j3VlWHDBJKKXsph94YOv-KjHxLP2^)~lv$Zk}WI9S)Yp5C5j zp4zanEje5muav%G7*>!~ZbySlrVdlGKqeD@Uh@8h4VY$mn=Gm73s zUB~2Wa-y`=!JLd7-4iXQR!Z%s>lo!!5mfBu$B%Qa>vGW(zb9-YcZHbF&loq5^N^|X2&Edtyq zEmRP(`Ncr0z)<;_zXrI9<$RaCt-TChlFe{l-*^stmf;?b$Jf{9pXYUraGh|Ou5>uD z4RJ~vyDPi1xoz%iu3zJyKlum$ye2pPGyHQ-Ev5_wc`#0}J#yyh+(yqs};BEYEhGhOZReaozFVZnx^X=~@rZ@~`~!g8#xlU&1{C|GfGq_~+BqpV3WT z4>V9EP$RHIPAX3UIT9Yh7wK)_<&mWuCRLTvNiNCqXdW6l*WB zhn2l{d0a1DH(YyMpXZ+s`h|Z!2>kPx-#q`kB&fqrovB?7U87tpT)SOITnAlST(ex= zT_s$f=bvZfI)8?L-b`JFOl1Dh1k|z02ezUUX}&+He~qsQ&g&>|e$)mIgA3jh*}B@& z*W;h>wP$zCcUX=(&SlQ$&ZMqNt}d>Tt||D8a&>i8b^SK~e9A}udHG-EpRZBheN+7N zrcvgfhd#+a_o;c+p~@vxB!>ha2Py{kqFZOXzli^$uahsP?+7UG#gRRKC0)R)akq3( zx`>+O*X5r#0ss8`7xK@Cf0BQ`Qx9qdw4v%bC5h4_^m8zKa4xLtFqx6Z`IGo}`5O5g zSmh{h4dk*MURCl+e&jh4qPo?=y9`y=ufadR;D`(UdBhj+&tHDzpT7kiJmyxS$?ozFGy9fjxRp%%ZO_cqiu+aD*5$qiZ$%9cLSmiJ0_xo1*T7i{qcyD<3 zdDnYadeiVJu0ez1D*EG*9z zxrMCx=lM%vFSq&nB8E-wV^A&ly8QFo;Ga(h|2*tp_~-KP=AZjM!#^*n&HtwO=NJEp zf4=Dp_~$$-JfG&DzfjAl>y$Xk@Q@Ph5ex(d23&y^a%p7H7b0Vq(Er@G*Eh#E%-0jO zpK#v@-vZx3-y7c_%RfK()%H^x6tV{iS%{PP+g`R8N7KQH?$ z{PR6}0=>WXKrILBnoe01N)lQU%n;lcs1@+b^W=)~mPgT((-a*pN&F5!?Wge1B>1n! z{&D^z{y&a?9#0$uGI(CwXxl|wG<#WlFRXKy{ha-V{igk*{h)n?eHePrKgU1M#?FN% z#;5t`8PL_jseKh+s2ef^!-BEkMJfekd70cmPAES{C&zey7k>kP6}-M`pgXxAEYfkm z;{Vq8=P^FXKey>Uv`1=9^^8(lIT5NHx)tmg#l>>&nP3|t|mu=`9IP72RU*MmO z)vd%)Kli`zd(rWb4E;-;(f4pqP7dPqfk3=qpWuUFrO?4pKFn)_zpYl#ZfPBL(wJ#v zf!1kD%%9I8a-z$uH8SU4i+`T+GyHQ}57*wQ?bVk`XC)LG6-pG^8LSs1gPQ|w1E~YA zZgkmI!sN$rc|9oFe%kizwyt5zmtEy z?=R<{_aMw)_~)^~Kacqt{<$NH{PV+~;h&HD3;5^f>C)gvrvDZG`9NWda34(Y)Z&-( z&nwX9zajql*pK}4d;h^de*yk^9bv@(0RKFLP($b`Oca(1n}wbDY!a3T6NK(UHR12W zKfm<5_~%o;1OEA9@Xy;Y$A5!=9_>5kpX*dJumzKYLVKA>!}ekivm#d!`YeYyKbHbj z@Q(Z#ei6T!-^(B55Ac!vT7DKkh;NE2M-2Wkw}I=y<>q|sTDA%6Vs|mk8J<~1m!Y3P z{Ut870UG@`!BQbH$&=pN4-M0|#z-Rt=Iop3l)g%eG&is4)lnVQ-`6lX@R1WkGN*_u%cTVq0a6xrW$#SyZR_ShyF;L zhAC}YJERT;Q#pqEP}#1GS2`$_p{kQYiLE%XuMYGvrNDf1MWvk*RsMMmY8}PU^`Mui z(`A{-%ylLnTbrH49%E%TIXLefxY68VC<`41)&4wpmOIAn;#P5!!GEvCW#L%v8oQM3 zz-B>hZXMGKYK!OS0U$14p$0&S`6S2_E^-sJlzrB8C`8^g`S^@b zSY;K>rCn5KsV&uR4b(*R zX{tBYcoyu8MC4%*&z!_o=nE6pN~p-H#&V;EL7|toot_Ml-NVjYF4U z4rsyOH*z)q$!ZyIATEh*`%K?Y9>Rl%iNJE&lh+GTaM zI$UiIQhi1>9`@8wf{Lt!6hd{ViPh|CMZC+#LvQe&>VyVixVBMyqh-{)K;P8R%NWz3 zshu4}pR3Ro><`*<7|{z_*omQJc7Sw4zk3ywk@Mk{9iUA*Khup_$=txCRu;Av8_rH; z*RhA#i|ie&_9?#Z;QK@DdN9I!;Z$d1dG;2whWY*c^V)wI|9sac_~&VVm480qyW^iL zWD#m8)N^TQ3iqWqgP0wUDa~|cX5rM{WfUd`n;H|3CD@8=Rkj*i39HVB{;}As!8`;> zdjZp%sSNtEhu#ad-b!?I`Z9Qv)hHKr3OvFBq!%olCLjQvg|2!&Fosrxosh)51*PY@ zMgmZlm*cb*(Cz57->Qw%+G^#rj9M&s&Ufk~XkT7YFRRyZI$x?G)vhJ~!};e`neNO~ zW+QWgxx>6=REA~+R>avP*#Pr`xyBsAnT}xEH zdtRZ^(lud!mSJaJVsat{lMiRH3Dc1YX9h5X@#zl>(;iC06~K2-!qChUP^K4x##{wj z$}-OUWU3J;6YubU?TdY+$fMvklm#vGj5Qu)tQgi+@SmE3C-@$^?Q ztgGimwwcghXjil&+74|kXzz>hS%&YvP5${hXt;mipP%{y{(0}ejDLQLzD2*FeY8qj zcvV~Yw-^6?3(DWy=>_x%=wVj?J)5H+P?1o6Zw?)QmbwByV`s3o81ftxv76#XdcesU z3R+ie>jBtLeLzf%V?M`TjyGDuU&b|j`ekVG&ejJbI;gFe!k%W*Qz1@B0xue-XV7y& zIk~#t5}Myr^!3>JSGvQ(Ppm#kTyEX-ytg{ibtLjkP`)QaYrY=Vncn#8_~$n8>f_Ui z>7;ZLIw2h!y89ID=reF74&n7O4Q!Vt;1h>Y6!ieyf>~rYP>sV#6;z59L_e?`Q-S|+ z0qns(u&$}0#eWr4)=^kx5h!8kAU_-gMLGgI(-6w~d5pA(5ZnfD5a<$Bbxk*Q+OQk3 zpo1E9{&^xO=C}PM|NH=Tj=DiTfQr;>>McI6v9|kAJ~>V816^V+HJa*1H2}3HHD#mT zfLgE?P5^8#-DAfgVDh2V&L;8l!=vU*`m+k4G(hGZ+*eq$OE>hM4ZKa7z)-v z9wIuF%#MM`(+5Pdtd&MuBuy9z5Qdcy~PlKROax^V5+P>1%YsYML1J5%tx< zS3{$j(GL4O&=?D9_*yWRE`z{CnhC+wst&I71e}ZW;Mm5na$$`Fz-~B>8|fx;f~hb7 zq_d--TZ*_n<*~|9Ak-ZMUBOG*z!J&wLf|&CJ=rzwsA13f@R1DnFq_CW+uy3hAeNO_yM>L9u zM%jDv5!iKy;eFKCq}&5XGec*+e34fTX$< zbi8k=6M?TaE|+@dEmryFfY_i=AwY zKT!^l7XOdYdj*loHgM@@!18PB-#z; z68MsF$O-oYRk<@rn5}U-KYTuLvpa!q+XuTCVNSzIT?^Xj32+skVIM^+p_SPxZq>Fr zfc-ZEoT?+>$$Ko0NQ~239&hz3e&&&j5a}(&*J8v7bHF#90Lwc7QCwS`>&mdKSwXUQkvj1T zd4|KpdJs>CBG#!z6hZbXI$q0Y#bFjYUi1F5u>+xBK@0R07=3wn( zz!B?#Rn`YLvLKlek&b{j@@ln49WTU zn>>q%?t}lx1WIp1Fwe%~PZJ5R_IKc)H%CQqinSKD^^PT5E+P$46h5*mqOQg8+82mt zKltY$Ox(p=yktcp zJCx0J+{hE~UsLcxXpE>eqZJD;=I7=`c*j*Zr^9hdTi}G2MdmI$-cc#=+KP)zWlVfx z;k$&WyQc+pq5$H%8fJ5|D_GJ~5%cT@JMt+wLoxAxEQSAbpI>=@jK*2rOI!twn1ecZ zPP`=>!!r&C;e9#cw?pXhy@7STBt3Xlh49u=Nd=5sFJ2)}LFE0O_~$nXKjFk46h_?A z0bJ>6$fHLhmvs$q#rIeh1$|jJ6_bkd@e>o@+mLfqvGOPQH7D_FSK#*y#_z2O+D9tV zftTcUys{R8=iC|FfAUBxag!~3c?BATjrTV+PAGCJt) zis3<+ zKJ4arc$0eA%j9@5KP8TVu0Dcj`EU8F`c{4`0ixBrs1Pj0PIQ8ACtV@EX|$ALKEkA;(z_ zZ;)x$4!l}5s6-YeniIp3MLUVtmGBk#=TH&*)A{G|a9;nH`R8$A_nV-izXZ1QTj!rw zK?c1CssoFWeZ6XUjc8_8WX^lw#O{Nhpbf9)`qnV`scXnzr6+3OowouJKoELgrC=5R zuKe@m-y#28{Q~}Z$$#RX*Zq$8=Z8^w_2@RRQc9r?GzRwes$m#u5CIKFPT{WULZ$2{ zWRR|)ex48Sp9R=Q67T=-iGMyyU!d>MFX`{GpV^JtVDqjpel~O?J@W1mh%4Sh%d{@C zorkTEl^OZD*?4tOpX8tC{)K-&_Z#D%H~k9y^Yh;r|2&zV|6lm$ML=y{fT}HvsJ%O? zDbLJgh#6<$v_?neC4xAPeJn_ZBYXNi@y|n1wxLU|2{W8o!5jtAR%Qe?E}Mc)4{frHY#KHR8v}ijZ<#AhBzi-8 zpwliT6Togxp&QUC=;!EeY6mvfpU*#MKgmBoO-9F?VlGI%iRt>7vpa`w>|9Js@OmTB z*=jNg*_>=Swm#d2{fX^~Pgk}b+X#KGdD)~a%|2rGGc(XVSbz~RQ$Lq(LMNvmQVZ}- zi2;JkU@{l^619iQsDErlr$;>N3_3G%p(AOj(FlDjH}qN9jpX_(^ahU5>S&oXUVEw@ zSJ$aC(2*Ukb_Acewc1YYst!O0_7e5C_~%K@4#=l-=(bpfZjX}0G~_0-Bgb}@jE4?@ zHRwaiNr&SmN_2XrDKiC~t8bZDSV>*B2RoKsfX?ekc0YT7-N$Zc*RXTg5o{;)^JYf3 z>jP#RGmNRi#AEKFH?lGMdaj|Pt|S#g_eev$ocEx<9FI7T+D@2t3E7Gq=2N_ys-Q3D zkUl^!sGI129E%mE))e(;b*(xEYpbaiQM0Hi(PN$vUn%i@5w!+>dbJce?A^(^*-JYxs7M~OuYgomiB6Ww0xSO9#bc)&D7kgOMRo9$E?d@WwJ6#8LA9X zMkQL5Vv#9w1Z1B{^pU*$f z_ucT%b7FRB8m5+bOmK}?udAsr>vBd5(+BEz^+LuXM0Op}{aFy*2ri;Gsubl>3y+U} z&4*NRdNFN4IeIN)FcsMum}5x-!uu3VU-`I%pt?8UyYnOX$^1-wrtl;Ao_u4z7@w36 zg7ZET=QfOc$j-&w1jTM->Y@8^6pCro;STO>_nhGos2r-x0bC zqJ3c{T-mAwltSug^@f^J8>ZdU3gTTtq0{i9Q2v zGbhu5IR=l|86JY?DskiS=eM|Qd_#UHzl7h%U*zxeFZh@IQ~ox8hTo1k$#A|hi1u$W zk=6_M_6@s;t-`8^`bsfR>7nQm{Tlr95uvT2=b>bnM_Z!2Qu4x|JW%s%GqsS`2ystl z&W3I3!E`-U0Mq-TyI?)fYE=Q*r3 zR&vw}-Q);l`a7dfwF+i%(m+eV3&n)>EX&n}1%1!u;Jfmx`8&KIWD%+eZH0cqXv}1f z7e)%bg_c4&A&o!?7x@KzOFkukAC|Wy=fRYIKCnQ$F#>cwGSPdeGSnqh@*WZGkTnlS zy<@oPG-qNOBC7oJD?u`pEA&%n6Ubl1Fj4qiDXuP3X{|lt*b@3yJ&iHnh=n}7U=7Fo zM?WG!bR}PtEzoaW8J;66Gml{rtDR<3a|5`axx{=c?9*dDK33CRm?3NxP72qB2f{<) zws1k%BPxgYRb|zx=uf;zf65I>AS*g&J(6dkxWjjmEWHPI<%`nxP7?$)X?-HsD zlY|pONJuW06q|}Y#3ABHak$t|Y%kUjbBIpyfv{HSinIKjp94L?H?UOcxPxqU5WqTs zulVQl&)e!RL8_6A-gsw4STXP>NQG)*PIOflr1#MUnVn!|Ek&GqzKAdi zCzKZRh^@r2;%aEM{48F_=V$SdxK@{PWd%R?N+mK<`;2beZ)=1#&8x6mPz4^iJ&KZYDRo zkqyH%vd%TbEPZ^Tldwy$gu-G^afNt6ltjBNu`P`)y)C6Ju8p!i#hRy!&2WOB2y=ys z&=sD;=jDIqTEO=7MXWxU`C9z*b%A$*yuneyyTJmX`5{i}0o&6=y{$IX9%yZJuRaiZ z*z<8(4_eiUr$jis#|$crUXS~@hbhSJVza}h#)d|~Bfgk0OL!?{7sJKP;!`n(Esw3H zt(`60*3Z_<*3nkiR>&6L_DV3ZYejCo67Cw0rD()xBOK8ef;ybm|2=?rAHmI4(h{$spyDvGUHub7*o1MxSd=E z>6*mBu?;u^7@NQp~@ zLhzVP_~+dJWA84Y{p#k+ z33xaEI=e}l`pj8(Yd5L2l3uv)z4vK{S)?g@6ZN4xP!aQ=`sc3S@y{0pAAX^KzAgg) zeCD6-pI615dloo|E^Q+NeFEJAy#mA0vu$_aLBN9!i=%>IaGjvINou(gtz_%AoV!SX}4(K;~et;AvdtU>G&EmGep&^^95-23RMo zc@`S^&AaA6D}{Z=?(D=tzI!WDqP9>qQ2jUs9g{G(aSymw{0qK?a7QRDZW7(DR<4~c z%T*9pv{X7JJ(dEJEm`>WG5*_9{B9x1!vCTbu5zPTO1vdB6`u0V`0vpBWG^CmqnVfJ zV)7;a`8wRa&-v%g;h*1*z(22P1S0Uy6C*ZN8M9+Y-p6+r-U>~{3u0E+1iXhCrOwhi z>8`}cDdl`}DY>j%OwK09l>^dgX_{0+lBEN#mM+G%P|PIm!n%DYpMhV%@#y*eNBr}G z0U_|fe-M)59RCb-{alN#yHEXINQ^@RX9DqpU4mDFc|&VLNtK043Uve4c2~8Is88Q$ z)I{HeHCAoper$JI5HWF%+?h&1pP<__Zmh`5;y!jp_n^UoE%ZdMvMR1EEY=ltvT)AoL&_YcB}I3^R)z;XhTB^4?F;r` zCp+fIIb?ZCr&iN7P(QzzEsXcEGw<+Y1X&y-T4HmRKTq|^KkxY)|NQi4{PWw?SUNBBl$pq8vET>o(y+5#}M-a~Oc=R57Y*Gbg#0yv0uu5{t{kWUkpR zR%(y8E}`5?UM1hh^<{RKLI3Z1?i%hA?zC>keN$c_*OT4y9;u$>!_UO!8Yj|X58)wS zl3&5ui1K~8e_jgx&F}ekqYw5(bU7dIn~(n3|KC49=TC}p_Zf7oi@{2v8=)48s?1Px zVRRj%=P}+G%ghE=T+|axbE*;^#LLGZrlZrFnWn6p+r`!ARerLN3^RL1*J@V+X@vAt zDug%i3a+rAyM=qOd#Zbmd!~D|yR*BJJDK~Hyhd&$3-UUtq;wPW$bHNrr-ZD+4Bp56 zG5Ls_GgL?mP~ zB0cZ*6>wbQTeqz_c0DHn@`e_Yjo{YZ#_UmwRoE3=1^zuhPDqNGBbRH3D}yu_vqb}W zqa2cRx!c3}*x^14QR+H=-R_?5ZtKqB4#?}|ITTp^sV*1KzH+D{s;b&fo*~G!F9pxp`)R?N>G`r7T3bG z650@+vR#8*3ie%BgzR`HpK`glKjNRy z2-iP<=F1(9f4&OVSYsc3 z2mX)#x%?ad{JcIcT>pFtTaR<27h5ZV7nh59u+JzX9g}il&M@U_?)mO}?&zL^FhII^ z`gwYL+Iy5z5NoZUN3vx zZQMKEx;uxbIrO20o^|*v@r=iH7J+ec8c%FW_c6ScPcVx-a8-5f6%$}|JKl9Iv|H3~H({||n!}ZU7 z>^7`elOw{|ON{H<>?(rUv9!Ej{>DAZeczo9_iYK>p;sQu!+J?5XSY3DJ%b=C>F!nT za_&cR7da?(k)F6Jxi*WU*hpB%hjD+zKfe?IInIn`D-;0x7sj(U@h*fGv%=&(JY0qRBS&ru$Z&7a*Z#8&eIlM8wFFcz)9X&}r zC)~~4K8!q?)Jggd5uZ8YTOq$Nls}5yWKOmXGn+n+?m}_NB19W!ioMr*YQ`{s&p+P@ z&HcCjIrYguKNXh5H|#h5`3m^wiQu2F{mDN+i`{ZRq6V3sBIq0RN~RlIfFt-rd>`zQ zZ;E}<#e6l)p>1+HT%+o);+gNc?}_0p=&kSV1W~dR%(KGY*xn!T1lROf7E5*^mDD+|T&uFCy^ITl~U5FNm7@F!C(5nC`|DXFc3a zZZ=<2aEW`wCNA4G6CTEDIfZ)~uCaz^x#uNpzAE0X-qGHv-bvme&_IiL-QFuuma==U zW8D*wd&%#mhSG6YGFMOWI3fcT5f}J!|GYuC{(1AT9iRO355Muxr@}u^iZ$KQU-{>g zskU?>CK~&IUBh+aGYZdz*cK^zo3;9XpTHu9lJ}jgUF_AQ+34JS#n6p6uQ>5EeIj4|$Jy_jy<2jyCY7#Pd4U zliPC@@8k=)g?v*gjJ;M!EGuW%-CiTBO?EN zS6Cxl<3+gH9U#2thYOw$|Jfea^0|-hPg-VN-zbc(LlGfZ87LWi8=N1irnuFs>O8Ft zPA8+8FU=FyGVGQcp@Ug`Do9i};VjIjH$J|H~P+zH%svD@k`eecTQ8X;cBEcwU$^H>r1 z=VQY0&!@mYPmSJum*Jle|CN8`V> zT-3eWoeGNEeNS3%TfB|OVf4O-L;M2waw~2Q) z-o;1WfLHhW@t?&14lEv=Op3-7ZAl;hp1#r*G}>O=AXy@tbd;9lYj2OKkxTh|6IhF zbSMJ<{7MA=`6bMe^@VuiMX|ptxpW9`|ty_+BrK6iJ)dZ(RyOUf_JbiKrRY#(74|DKE3Kd<^l{(0bIDXd-PaQyRQ+Vlwg z^XXh8J_XMI=ZIxpVXi6IC-0Q=y0^NMc}95dL3QomUFtoDcTx44xSKzC4|}I$?Gwj) z($mgEd8W8yxhKmu_F2bW@m#INjlw%VH6O8mJ|RN?{AmRK`S=L@^Li2b=gE2!t2wg`$i||HRDpqy{TvMf}_s<7vJHqkL7eV7SBJj`mebzru^T|IS z5rKc+g3HK%;5P~_aZ+I0A_D(> z>rehU3;#S1tfldxqCfiQRrTbb{PQ)R_0L~(oA{1GD)ELm(3J%H$Wrn-xinS+i4ot3 z8vi`+Z~XJ>|Dk{W-aiDVJqLqrU`bt7rl_^FB>Fr3$Y=K-Uc_2g++-U#@OhQiUPI?qmlTFM$=9Xe5;Kpvb zkxO$;km6z|84a=IAKX#ppBMkEf4=mSe?BV$|6IZ~UiX#qAN7|F+zE6JMhopmBr>6T zOP!-N($nINJZ;Xiy4a;rnH?a`k&CFVbTP)m-e%|HgeMmA$+{y(w-d3}TT)&58dm2k z-GnD<{PVH!&!dIopI?N3UNId1JeKd6i4`Mswtu5vaLe;q)TPkqM~R)NXnhQ;wCnbKGz~H^!YYVrr~+Do9tPit=GO4Wg}4 z8B)imx13x~u82k6fj9FmfPE;Jh`@ZtvtLTJ%^lbkC>*?m z*@IIyDD{woenK0f7c+SCm**cXsmyeU-pBM~^KkFEC442pCrlC3x^}w?OMj(*J_yP? z;ky>L3C{c2uu)-C!d8TxfDWG0*8-=pl)t(EyuToFcnTnYr$I;w%~gu2AJr8YbrKl& zj3s6pWSQyq31>1Ut=nHz%p5W9Z5~2Uf z8f0H(WL`1L*}9y-Z|Ca@L1Chp$h89To%xa|caqP@@!gT@pYQtQpAXkBMc|*Gh`>M3 z;UDLJ?XL*~wOH_EuxjW@XrPiM{&@nugLX_!3d#OWs3AP*n!$U4P5~~k&R@sx z@n7=I#XM2Tmlr4J8GU(hdfw4D6PX_(a(Gtwi9qYX^+5UHIb7qzP){YAx=pQ*s+T=_ zCnJsd*j$1bT?*%kvw~<%enUN_meBPWFET#A<>K=D_}a+58X(fH5iZl!LAoYok%!As zp zE23iQu-(_miC&3o$YxYL`T{+W$-v%Z`*8_Sol_3=4^5EKS_jedKhHlu{~!40VxS%} zJz@q21m6eShF-$4ca-UBChdaORgZ6+#Hf=7eU_%$WpRGK3)vni&|`iyvala9W7&)l zuv+p8KSGeiaUz8rnbbk;7U6@8voFY~++x_UH3w zf@z-<8D=f~W3a*r^Jhn1(zQTVx|kagMLJje@;;GeGwD1oZMEx~x9u_00!tT4F7WKguK=^yp!$ZdUMPO=KxZ|r$ac|ya} zT8ko4ZC;h}Gt+PkG!ra*g5|>{&~h< z_~$1BlOpiX%LV5JeZgv>J)tzp0wtb07x!_4Rv6VhoeY<`-mHW^Lv!q+&I@M}ksE#G zhoCazINcO^pHncpZsu}BP|S%MfXMUDkD@1ffASXk43Br7+r{ifmIYn$IIN`4`R9Qj z{qv_^=$|XbbTb=zwsg1Spr3I=!XcKCB~Uv!n2wK1rIPG*wlVjFYr#K2_RbL@jW`<0 zdlpw8*IvY!lS}2KHqt<8oHPlm_Wn{!otuR#vn7Uwauz(zqcnk8Hw{md(^lrN3GIzx-FwJBasz3mGkmrcmwBl zmxN5HzPcqQbw#y*p5Qb7`Sb|<^ZQ@upC@BBF?rDmp$K;Xm60p>Xz*%p2pPoz;z^Nr z6?1iWEp#1r-EqCbJq^RJcU*_?JKgaoyz7)WP|PUa6nYCWgf)B~{s@fMlWZ||50jBu zNW17>P*2K2S&T~z!qZyW-eaY*rkS+a8~%B_aQySPsM-0ve}4aW{PTw9J5(#hKuvF9 zbfRgGeo3>C?{N^7u6OBH=>E`!{lKBX1JL zhz;m@F~as)&8_QZS@SS@2kh42&%?mHuXaZSWSi1Jk(6VhA)(Ttn4uTgTdcuMI5s#k zI4(FlxFL8p_%4_rR2j3zg-~3jjdDmyqz*x)WIgSoR!TpH3c@o+Ra8c_g-bBTPJq6q zh0qnM8S*`bP%=H6jw=5=6TJ-8*`rAnof_|;bIM+H@myrZw8s68f8HrnG?XZ02VX(1 zzlB}@lVC9D4P_10!xbJ45lU%gy7CNhxkW0ib;4S%s(uF7c)}=SUNmb#lk8xt_9!PN zdbMUI_mQPwMMa%|o*&iO8L8zz)(`e_-k=BjZL2DJ2$VC=!MePO4iI7J_mE6GthQ9W z>Jg>4l1tG;r$h5XJwmlZMQ~l|@X3m6t`q7Rnjbn7GD5kPKFU$Wi+|#-p#xuWcr_K!Z?6Wrk=CJZQ#I+R^3N}a z>z~&&9~d1B6Wu)0>u2EA#=r?>cQu3hTG^uXS1K!M6kd53x*NKJ_wRD(d+hreC8bhM z>7}eyo+zo*_UcZRN2UEfEulU{f34TS`&baQ*%_@h);H*qnhKq0QlgV-O7u%iiY_~` z=&16~zk_P?6uqS1pj*NRbfyoWtC5MG7cOfil(y|g8RMbePfws9*4k=uwe#u-wWR7& zZz4y2h|)wUrDRjSQIcZio)-77lF|6RCBvt9qXZ7b@;FmPRoveTw`}E8V#$=kHQ$jqh6L*Lc`6~Qm{w#0vQRAPtHZ@}k ztjIHZ2l&GWF?yuH7`sqyp=MD@^?PLx>X61L{gm!XH@tx(l{v~*oa87=-Lm_kBZDzCOWzV zonjN9&T%_x(DU$JFk}AT{PU@w@z0-9Js=WIfhDm9UH=a`<>4zfhqXM!iearb3z@fJ zU&c1}=`HkV`cbV9JmsKzKpmsj$IdI78c-f8mzC4X3FVA(4Ns_!+WP$X8BA4Asiam^ z8>wB^66;+ss-`rCVwYUb+=|-4v6gDJ!KhQgIpk!8ToD`n*MnqB>I#(~6|Yg@pZAA- zx&%7RF}Mnk@U)W7R67;c0+p<1CaM|HXQwH8L2bhb8(Y7qjl+ByQ~LpNn(=B|wIaGA ze4{2*Efm?eOWW_E#sWE7W7^Q`N2I z$CJHMd!(hrYH7Pp88wZy24yya=a(4I$aAYAy74APC;X32eeAL_q5@qf8$+J^EBx~% zW@+<-G2f_Q*!nJX@ye!$u&O-LLLb524oc4$jAUEsa(I{aIFO z-)k}Ss`@nap-FCZHjWsv%ns&BGX<{k5i0Cgp?hm*tmyKg3nm5o;w+ht8ViT67(ExU zfnw-hb`M<>+OVtGM{HcU;61s8=wx!A3nCi(pZVu^sUZ>i=MTQnKi{T3(7bv{bT-?s zTd0MYV0>q!HT#=a&<$c3dT1Ab2cy~boqaGC2cTZ50K@=={PXCic+!Wg?p6l$Yg-28 zJ(c;&*k%mET0NPepqg{5K11)1o`SXY3VI2wYm1}PRSnEBJ@g6sYW=kS2GwPSjZVfA z;}#VA3g$TTJo?nuLZ6)%xQ|2NWh8>lv)_@32E;nV>B^vg{zI$=x}lGWMHQh3(fjED zodF&6CZXHM6NX0)ZYj1gy6+7~Kc(5|C^#3k`#5ylYY*o>KWdXT<~rhE17I)5WqzPn zLF`ROzob?`T#iFsgyxg`SN#(&Kn}_SX?cy+!b)cSU@kRVo0%bIpEPD+9b3Xkf|2%- zepcV1FV|=5llAfVOws4+t1$jv(4S$xNP(3g^3;rThK1VJ_OQjDU{r01f8%>Ampuse z^)aB=t%Z$I6avX@B01R}>Q!X;=gA`U&zt@S{(0H|I{&=T|G0mC#2Rf?vl3aa&3)!X zvjsH!xMt9}Z5)6FKE)V}^>!PhnbFv2Vzj_>+}jvs%rQ0@r_tMjHdEtG>|rj3S!|hE z@Z8U{uA%?TU*(^ZU*Mm=`iy_R!#IXY*5`N&m zV#y3IaB7J-XN zh+e`u(3!OeJ_VUPOeQ8d6CLy6EBXq0pw2=++iK9kDReSjOZ9_%8;5!Xo#@~G`MLkV zKkxmce;(K5FcQ7PH9bb3peM#lUZH_c&nQP6%=u+=D zqgh$;KKDcf;26}9ICgQn3rwj?cviE(Rh^E=j*6$X5uT!>gpWu;Rznxw4Ul&zbmXdr zUW0R~ebjeU2+n(27~oauru4UTUwSA#njVMGX!J(xM|Y;1Lkcf~XEY|Qp+E6q^c5aJ z)rHdR!jrlidRX}W`TO7T&oBOC{PPJgSiaLCEKFk?Q&yD3wNsZ3M>bR~@n|J?qE_~$2JWR1od zR#_q)Arh~hb2vepfR#uU%#^Vm4eH8qdo8@gez+S|AX}uf%aq|LB zY|i1%U4o4DJyvaRaT3SaF<{!|fKgl@;^HXGx4Y02^0m!6$zizs+5Y*_DDlq|;l%Hs z?w@D3i@;52fWFrQ&}VlU?&1Zg(mHm?>7A0;C4cqL!w4QH71`jl*CyM*!X6E0aVa{x z?j{dIw7*DRC2!z#FY^5J%uZbNbAN}kic^SlFNX>_!tMoOxiLC}SF}q)Bg|*#wsU^^ zTk|AmHAdF&93NsksfdEG zWjjI5UP$b~iO(Z+R+h*lxXKcc5?eyQ90F-?4o>veqXYUL$a_a{lJKw3QGCA_(USGh zZ0AC;9SV=H6@D_M;7}%qV5`BDz6@n_8AR3Yh#{4LIu?V_oJYxXPXOO8l+I;tA{uzqJW0@PC4TUhXsg`JAu*Ire2eAu5i>jI+>Q2g~FX zZ15Mzd2qq8&j$0fK2AMGI`a_?ID@@%2wG+;q5yWxZE&JB{gY&W70;MLh_Hk+kolpX z)+U>i-{Q?23T=1_-p+YgBP{;(S%_zJCOH|uKa}iEcEbOz7BX|b`sXnT%XtoqbRW*{ zCt-|lfz?0(Ck+f+$_azKa@9VDw{i{EX49}+8iH}9+b89{EqwPjc3X@_-$KCpm+?Lp zEXC7i2 z(-8F<2^qWxV$zMt>X>zM!@rF~vZSB*0c!SMtaK(5{a~I}Ch`!;2^ZnVsr@NuvojaY zz|2Gf*|6AhkF$JFFK8*N};YkT( zeDYt{SQc+&3%rqi5#9glpMUkwhhXpZBY5QP+(HJ%O8AGpV9b`q9wZUoM?aMI zixA4z+4C_&4#sHH2J4+#$ek=>m%s|A(5FvPd|wv7R||jE7H{hiWacb@!*>wQ@*_Kl z86>Hb3-9FL%|FlT#CPaVE4q{Rc6%votD(5UR`BYp*rjk~xv>|{WM{z6E4`i3&VpT4 zKIAx;gNoS%Z)ab;nRD=yIe@Etg7b2Z^9@GVT6iZ%Ig62>eBF5mEi*ah$XY~aVhr*I z_Yv2KR|J7kBLi6kV@NBEHlr{PE`v9!%7R&oh1X&us}pxMLZXhFl{J;wNFV z@KIvhNpWxU;z_Q9r?wwJzW2s`0Afy?b;G^@N(yb^VDI8Z-`QG7DplaasoyliQQEJ%!^$SSzd-6)MZpq zhLCX|m-+_L{z6n~suJ~Q`RC`q`sda<#3C;u&njfOA;TB78)9uV3v1q6Soi$h{PS~I ziI2vNSQg`m*A60rdcxX_bE&abU#o-F1lb#vtTI*!oOKklN?PS$GS>y=Gzbxr?zUl~WEoty#`NJgpLuov23)Ay#9B=|eBYKi@w;f|=AUP_;#wq5Umuv4aX!8e+2fndjpim~=I%C+AOqwau2uV)<|K&zoZ1ZDM>}jEuT+7*h=M zHZrMaBS*D4t|zaV#*A-zkn=&B1im=aZN@{cPHwZj`FHcrOOP()L(WD%LJH!Av%%?x z=!A`!%mnO~l0uZ+gY2X_R(i`p_xvs91mr$cG_xW@i9+`BL*u$}7L_YU@i}Q+M9$JP zBWQ@`f0KV66ZMiStj?&Jl&uHmHggPeI|?B$$3~{kdDzPgBPAPONqb(nO>f>a-BJNlUX&$i%W!lJJxNq#m2*Z+BbAZYsAl}#{PV?RT{0SZ264b##2aUsQ`hl2rx4r7Yx}W! z>ttoNKA1Z&(v&o#n?D%ajj={^qll5zV2rn@2sn+L`R)2v)pZ>4%&)0u}f4&YTZKtpPIqviKs5L8r4E6oU11*Me`5G$DI$;MI9TBj7 zh@-c|sboB&D{+=chBKNAWHRLVouFdUP3X1sM>;3d6H?zhRJznchWSR+-Unc4rsWDk z*{K29whn6O%X0a+Z#Xyi0WSDzb^u$6O~5{eo7#>^4ds0fU5R$6O;kflpteG{ zajGG?u_f7I?0)tmRH;f_4{jE!3D2W4{S6o3LU6R-;QRCV zy;)p$2z5!h_i#1`vqf1O^GqiuDRT)KlPT$QRA(v%wGDC2kH`-Ct$%(2YtW?TMLcmi zj5nxxXrpJ;gW54|rq)p_r={1TBRuzBeTIt9r_jtabSg`Us=?-{z}p4SK8aorwFLL{ zRH(>0gr4oK%tJ7hdRaHE+=#-Ob~BuTq{pd8kf=uWK|`%j^Xbmn>#UM#4t|jgY^99XL1BrP*2OGaoRKWEb0iCs??CMCrHS4Dt%j-U3*UC$@H)Rm``ji_2o% z(ue9<^{3hbt-h8>d#Y|l^>RbhwI)?XRZ~7H?@(>7D=sw|x`Q@W|9<}YFe*B#T{Dt< ziIT)+oZ&yiNuz_EV^V7yc8L#-enxy_zusJz^#fWLEuHp4U9a}QeT$EJ-doB+WxcXQ znUDJAmC6p(^*&WtHIv!^Rm*2p0prjt?U|Mjm6GpKVZY5tfKkk6Hn(nAWpVcN=lSOc z{s;VX6}6xoH6EEfQ;C?wOq}J%qoSe=!oWN~GJaIR! z@igjQ8TtwQ z-TZSEH3)v3!oMV%5#KvCor^euI%X9?U3O9Pq*2khgPBp+rfFHUt7>00wfY_UlGIQV zC@(_?Lvus@L#;z~L)AjnL-j))Lc>B!aMgbFgKDmFLo{pg?fi$woC&oBJ*Ue0TrnBKA~TW3*~eG)UrCB3fxTu5m$a8#H0JgH06nKi~I{_RnKPp?}_$et@;W4l*%00ug~4 z&OT&NPq0+fx863&8%Om*=;=`meM{=AAE23MRHD{D--hmjQRAPFqTZnzYA5<%^>ZE| zlVO$RwtAV55u-VdmFiBcLib|kzoE28=a~)Y8}!fe&(Go-w_u;KPR*(wz`XE0G%A!a zbT>E^y&|K=KhN=_f8H=s{Bx7-$lhj(GAn47Zb==*)7po)hP=7a_7j`}EI}W$w#HRF ztDCe`+G4CnS1UP{GocnCHnbUi+7bn$!atuHCI0ys<`w+&W$0v9lUz@bL|yE#Si6a} z4{`B6#y!-}Z_$!#v(=dDOeL|hF;pV-C^$NpEBFS&d(`;nyCdB{zrti?#?enu#WsSNE5N}HHx(E4Gnoh%Y#LO&)_JR z4@9a zwFPom8TehWDLM>~38V|$@Q?OKrGKtO zvVWeB8A6|;M5+Qgo;c@7P6c}+&UE6MO^uDZt<}&rs&2JAc2hM&mxI-V_aW4K0z1$% zIr9DU$?(rZ=)_+!(*5(H%qd!+%TZ&FWm|Ju-x(qdt z+)6w}*Vc;mFl(>rH?kSs^j#XERZ&+cPN;e4La=af54vEV@;88TvfLLn{&_wwEK>dR zxO`)78P0tYF*WJ2)B&6?Cnc&n!|WZ_OPmMPMW^W7T4JrOx>t#&bPnAN7RNQ_2^{iQ z@xS&(m46O{^ZPC7n2 ziCxheXl^lH=qdE3+6wi(Qb?H*@&)S#PhzFI-(S*y+tTp@Y9o0E^G8OfHr~iB$OedMRM1Cjr!hm;Q`U#LP}|_OK*_)n%>OqanML;<4vT#M zd*{&^UagegaNqvn!_h{vef$!OQG2AHdHcIDEG zX#>$6T2$&H3g8X)3Ox5W^*{A>#eLiu7TNxJVfg0{Kl$gGBHcgFg|T%4x?+D%+BmH% z>9obFb%%K$IY$Nb&e~e_rIK0c7rGQoADk30{9XNFzW%;wzCY%l4=`$&87vOfJ%^nI zL|@1lS!kZQ%WPo#az%gTpO1_L|GZCx{`qY+mRbk8Nm#Iaa9to)V6>n1kMkw;eVKpm z`O!bmhqLro;rQp#BGo^S@uPqK5}m;E(v7Gg`VOfsh|9FMZX09eAs9F^O(qNSQ!QWdAgtc^T(J! zQaB~-R@PX|k>Bg0UJxSaRwane?9+qqF|r===kRZbuy`%38|1++_s=Ir;GcJk0{^@{ zJA>IzKcpxs1zC(}h85j1>!kU{h-DPjyK1Y|$4V-tW9VQocCcsQA@r+DzFNNLVZ+1H zLv~&8vGvD7ACrFcesq0|@iFzsG9SBt-0<<^$D&~o`{xyL(nFvZ{~>#kGl*zH7N!!? z3Vj*YS$D2*r2FT+xi#z!#-NkY#i8SzpSq| zhMJA7!gdn)Q4fj35JCIXwV8}8!Cm5J@ePEyk>a1{{K-GB!uDYn(Z{GK=uDTKDCRV? zhgvJlvxZMksz;T7-p(jtCWX!Sz&_+GCI+Jyc0oEW^MN_ePUjj%x_@4u8_(`!?$HFD zf+|5aCx$vp?W5KcQ!uj{4fUw<&odbus$vdXi|j#8W1yC8ZM z7v9i1zEh<8=jp?lKkj~-KO)mVpY@}EzE7WGv@=Us$!)@U;2a_r{N$hC{>eXI@}qzL zjl5OP?q1`L<>}?Q;EC(4?H%Xc>b>B->%H&2;XUM?@9pHx;eGE};3?v{?QZFQDL0p| zN_nMOt~X*nafon~H@K`^Gj<}glfFyYWHQVj&0*E7f^Xt85*g+7$o9|E{^+0AF|wGv z_0&3QFLnkJ4avMvG2YR~nVD=0F2g7PyhWt?=d;-p%zHWxU4Uv#4k4B~ClL{d8vne= zul)1XpY_jQ!9VX5$^Q8{R!2T>73fOy$-~%xa87!=iq#t@JePG+j~f5{t$z47{&^d^ z5Uk9o^3OA~4VlsO7V0)ij*=6loeuU?#OR(PB2Y??D*t?((b&v^e#6i0BhEac$1nWz z)SvwGYCrnt7b3+!Z^BMy4kBAQI+c%Xf)mLN_I1lN)0%aRaqvE(#y_v~lYc(!7yfw- z_~(z{5J$d$ev}DA)k}G*2f2Va;k-p%YZ0rXxd2Zqr$>!{o+d*7yxuST^P7?4pMPXh z{vv)==TrRZ5%iWydQ|x5Igo31(wJekwDQ<8`nK*Q#*z)GOteX#fjwA*ixCC>d1B0w zqv&1KGcpEQm}u|Jwoh0AQ~@?HX6RAnpU?W4f8P67{`pyM*w6g)@_*1jAH<*H1g;D_ zgxN*ErV`?`t}n61xovZH9;*Y!*1LLC`RDE7pWE=y=YPgOAHwC00{?tH^OR0VSHX&I zJI+61J7w&C)@Jhsyntr<3hgbgJwK(@}#K#{;ze-^-OPly!%d?a@jZblPk3GxLKQnks+ z#2NGtDrEPuqS`;N_!S;bKZbu^301HE-#=gbGynVs{PTp7?w@C3 z`qQVeYOO~uLH<={rxzq1(W-5(GIYHfss%h+5A~r^UHR+$^Y2~xrKz}=Ddi^eRC%v_ zTYe*lakz9a9Gr_0UcH1cCyw-30^BF(+=MVlN{&~I#{PU^5^3P90lb-B) z?#c~EdcX8hN+cJS>&UHPy|(-{$elvkN!1zHgr|(9k*%CF0x(wO$2Sc?&f#8B*#UO)>fiL&Z z*Zs;rzxShm-t0I2d0cTYWbnMMF|MnwXi{0Jx3o~&D_xZCNZ(0Ur6bZRX}DBJN+~_V zU99dh#KmHM@siL$c+A5V=W4=~&c@DX2&OsxXZz>V;GbU&$3HLe3;+D^f31H$@RNUD zB^>{J#6QA6?|@UR*;W?&oZZrK5ZlR#e&wwZIiF1b%RkTa5Ao06M&O^n{U7nqm%tz% z&uLsU{sf;~=p&pFqKVbTiQ*CQtr*>v3A03Hj4@SQC0touv0Y)}X>q35KujQB6NU>} zgzJ1)Ug8&V8M$3-0rmis9})9E-#@<)j(=X0{*JEC+-9n?r`f#R3QpoX@@M!YLMLID zpb9y}*5Y(zy4=E*>LTfKL`8fqUJ^Hnqr|#m3h|k+RH!X*!e+iY|B)NRCFItzIoW+o zA;cj6JpcU4XZ`b@ghh;mV=@ir12gHE%tVG|`>-$AYTPa^4rcR{d`!#-3xzv^j1!F} z;vjLBxDr>kSzIH|6Gw_|#gbxt@wu>3_*O_G+{NsgmA}q);dpL7o1Wc;Ydl8(RsMPY zaQyR9a3eE7inQ70+z~E5?&Ut7#2N7*VVm$!;KbBo5wWURUu-1S6DwgxOfC}Q9bv7| zL&z(H_)UCMUdCy1EiQ~5%O+tr;aNQaFYT}N&zC!?e&L@_pdGp^x*?TiH?dx>19yyz zjh)gO{w1GMs3VNT-MT2e5OiFtOB6&$_<;MnPnapR#}yL7F@6xAi+{#V;qq{I*q*G1 zUBTpHj?-1>Kifaw5}|*-pHBA=@z19~vkA{XfBIkRpP&APe?I1S{PVWVNhT@V3nOf7 zt`WDCd&tG(EAsvLWtdNX;6pqoL=$4-ix(8+h@RqC@&oy*d}2JOtGQNOVnnQluo>8E zOfM!5vmG6Go>6~=fBr5U|J7Um&X`1mS4he zTvARmK&!fisYn%tb9UC%bb+qjo$&hX3{R1Q9+{!IV8@lXDF zrGLDC{=@%(e||q4|GYZ2fnw--^ah-2m1U-4-4lW|i~E zVY-0&w7uLKZYuouT3i;6+CNho%s7n!sFO1ax}p!LDF0qgE>(mkV)>=2*$};|6nsxV~Ixt|?c6 z%g)8-eC&C4G259f$TI91W(@P!`{&uwq5q+s8HYWu&(<7r<1+u zLp05lWQL&!mXG-cF7_Z?=PC9HYoeD~GA=!`RI+iI(Zehr$Dxba4R$B)cUxScgi7qi zOiOh5dO|OvYtu4)nCeRX75;fPdpJ~(wCF>1*-1(CCe9Iw$9%->4wg0cNh*&U@goi(^2nxl)cK{!__{+*FAiH6n)%gU=*&6_y*5@&#Xg_x55m~ zoJ6m%qO?wJquNr5V7w0|vycyniI5FmK_4mY1Yxj5rGK6s-5LMopLhJMf4=7*?w{*q z5o#DZ@6osi{phXqb2=X0t**=*Jhk^3jfuggX7jKm*otgbwiDbt;#XMq; zF^iZ!Ol9W3!9TA|O{C6Yge^#SrB~25Xp%{fUZvfc>C9&4G;rQYJ{BK~KTfbXHoW4&f(Ol47Y7=(72D^3M}f^{DC8d5WZS(kgRX#k6O_+{YjSggn;4=UtOnasw zQvuhP1S8E;`Z#p$zSvWxq=R_or&5im)YNWBH{m}u zwbR%iti9-?Si?$bh0Js2GIM}g7aeL7pgZ)J`{!f+NBnd1f7(A^2~C=&vQYIgVyvYu zQ6J%gXU6G8bGi>bo?bw&qj%B=u}3+9C-Vrsm)=4zr)SW^VTRX%9{$xoUrKZ#ilC$E zeP<)qAQhoTef7^{qekSbe_q{cjsDj&A-rF(URxpzt%?xsW}vt5BU^xlR}XgG2B>2M z{NozL0Q9H5gzl}0$l}mk$CH~N@q8dXRC=luW|Lmjcxo}Vg*r@Kr0!tt{sNuP-{JFy zdQLs0ZeiVUklIMir^Zm-s0LIq%o#4~EqWHOhcMZSECwg`ukg>a;~ED$tDK8Yz)1vg zwLSXN?jr6%080gLupPvqb>tcH8A(tHU=5X{8e>KuN==~_Vs76`?SX557@vdGUUYh2 zPc6lqI-2TDwW6v~1)&qkR2astJ&-i}k@d(tWGpg_xBw|;7_5OjM09jAI|+yEtAGB; zx(u~uD-?oxQR1Ix`XBJmW0F~Mm94PSoJDScHGGqN3Flp);!r85j8rZ>kHzqwmcgeK zRUAD$bHjg62}4W7`t&t<2VJK(<2fA$JF_a>%mi4iK7zjW)jtnHgFX%^aTeskP7vG6 zB0np=oyd-kd_h6%(DV@JS0fDIuQv;98xq zH^Z(O4I8N?ck*25v7TykX@${>xtvU_rzEK{HuT70q=1Y?EW)jh2tU>c!rlC5ichu5WR>dL>VG0 z-bjLY>0E_Jy385x^nf`3)j#iMcZ9Fp96gB}+l}zm6k}4`ul~7H+^Ow!aE8Jj+~6E{ zzH>f09FZ8kDa&Kr=|zkumJmCkf!rfLK+TMS@jp9O)|JTyWNYY|{V=Ihx?U&ecWea98GN^Bhz;Z+%i+GX$4YxN#?!@!5^TqH9VJhZXR!|d*XJD8?8o5T?8I(t8AjZ3 z_-TAgHpacoPo^Pbk`A2l>%;+MKTW{TumSb}X$db8bnZFFoVCsrtSp;2We|h;U+~Xk zI~Gjut8m)a;5i+Er?ds0(6aE|vtzH70=ucW*oDNzClf9lXy>X=+2!Jcd;>M z#t{(SSK^sGiX7iN2f$hU|A-6OBzp6Vz3Mle{>U2wKIGn~FoBN(&kAlQW<%$>HkLY^IiJyr|5x?KW- zR$3S#F~9ofb~@}gD`2J94L_a5_AcDZR~UPeIt4KD^u(Rq>RfT&J8mK));euryssdR z5)TNSh>h{4JlPzpn z_7;1#JqROaHM=10U>xj>73-CC-@0y{vyMY(+mF+(z4-Moyy^4Mb{-%;pjx~g4;{n{ zW4vvTXLdf!>?`(LTXfQV_0Qq1l_pz~L-20xA}^88aR;N}Ugg3Wadkvp+ah_iHS!W(~EIIzpYLE>kyfB5;ekj_=OkcXr}W=HN48=8OM{n$B_P_n}T3`M1Nb%2C z;CV8L)QDO1#=7PtVvZi%k*d_UINw=?tc{!0D^$TqbbLA$>Kt;=dFXuj%83)%Z|DTH zo3?P`a|`!$H8lb6XjS~PZt4}H%L~b#So|s`j=AtT#?WQZ`dV6LVZDC!&;Kv}x!Wo1bU_5^hQlMrq$9Bi_mM;#xf$lggILkT zLd?GvH4d8R73u@!rqd!-~ zN73`?O~~RoPv4~P(~s!K^bho1`Z|4<-jB?I+4NAlEnN{$uNyr)PT*XxJ5ERvP;ao3 z7=vg?a`b}O35U2W5e?e%Qm4I>18dbYSa1F{{<%yQraIvi^)#{|;vu`C2|XAY5WDG1 z^b!yhO z^JJ0apBKg)x(IcVELQE^?e*xFl+0;}%)}c`bfOY5jyOe7n6rCfSNMWVj2V9y3)j#iVv^DA)6^!CWA)}yC81?Fv zjRsJ`hZu8=9T4t=a3hPGouGo^LqMkuUWMR4|Mw7d=3t4ham=Vk>E{%cej* zStd3kn+7#kF_0Jej=2HpeGbzDnRcm|5PcF^h7ITx^b4%}+EHn!r8~4`$LA@fPMnR`nEI!F|SN5~3ck99y4l!*)TvRZq4n+m3C7JlDKz zQkI4&aEO_O?7;$z$Xv%b+yvQJkElh+u>3v$ys?$Qx^2!e8<@$>*Tybmq*2GnWbno_ z=ITXq}WtFxjLLp9S zcd+-^oKp)cO&^q+3|cm!wq03L26! ztjzW}6V04fFsvUTtTJIqC9Hq#h6 zqK}YKUl$(6NvaQ(4t)|v;+)|=Vk_B+dx#Ncb?(}uaE*_U%TXNJp6ksPW)kzBu@L5R zdLyWx)aSs%t_WK?Iuy;9+Fk9Mc3Hck-PC^2K4`R_2%U%;!1N8@KaU1AJAD5<#y`S8 ze`w`JB=(J6##xU1-G+#;#zK_hESUmx_a!Ph-Hkp<%g8#Oi*D!f*&3+m+=uGdXk1R% z?%#65xf$GIZWTVuxVflq?#(sfio*q0;k++EeMDCDEBQP4=REG@SO2^S#?uE(TJ~FZ z3;T{u&DBSJ-45rz!MP@du=yFc2Y57JMj(9E_lSJ z9#fT&XPuB4ggTEr>}2*an-vvihdGDK$A8N&KwaxARJtY>GNTf^ppZ|^E{1$ntRptV2x#ZN&U4pQ!AzE>JA9+`5@WdRyHe> zlx|96r7C3Qa!M7YkB# zJqgj}5tu*v6Ct82`G#ykeUEEALT6zXpkG2e_8dl=f!q}?G2aUJ=?NbnSJPdXC2U6* zjN8Hw!Xx3Xa9P-oJ`h8MdO`+NFdyUx@VWWdm{m$}FWAv+N_Ib1J`d?msOkCh{qq)( z1{xxFt%37Bdehz|>XYA*wW(`V1=IuNM&(g#)C)dkOQGsh<;w9haSs!rqs3Tszqluu zLTq$Q$R_3#vx?uKo0l$pkLw;OR1@L|H~C2zbw6;kx%}L1tZg}VHrDE!v9pZGKR**C z{&^Z}wUrio<}A*3CogJ?5WF<3r75 z#Qu3D_~)%7)jw|s-=ve}v-)74H5Pev3oz{ej zdvE6c@7y~xH}g#r5+ME5)6jH7p6uSW*IIkY`>O{2dAa|_KOgct@Xv!#6!O?h@U|Xv zY7sAqzVIG%sVI6g&f_1?KQ9k%c4_^rR#STdi|SQ3E439u*(G6 z%Y{L*Jtj@X>Pkr&z|Z#MNr=x%f>JzLIwGl3L3xyXUCs<*`X{B48m*Srj%p?JGkOi< zq0tTZ=J{5Rf6~9YOtziCD9b4+$Etw*T&u6-O61Tx{xW| zig;F>DpnO4aVHd_Oz6^NaP8&`^9P|vvxQB?PGvN@1#ZphK|OtgUg0&MEw{E$TIsBb zW)K;-&%-|-Y9>P^x{2-BE1a^#8)6cf6*D@WX%6%8e+K`&*k6EuUgj^rKd+|m{tW!{ z+NNc$x9Zs}qMg=6TvVI7Q%Nw}*@^jc_~(Z|3;+Cp+0jaDN87`kT=0?e$x4(?{dxTJ z^Ph!(-q*}v-M41g6~K?$Lv%&v>d)k#-~Ju>=g-wqT1NdxeVCCGHTT6eiYt@OX}&s+X3{PTClN_feH_6>WcQ-km!Up}5HMeFodrUjHf z_i&B)uaAE|N{*wfSIVfb)oEH0{iD9hXlf?5Zd>#1hE5Xn4J{#?LSyGP{b%ye6M=vJ z?04j!H#QQQx6HX#BM{x5IqQfnWOk^}Y^S?I*XtqsNAu5f>cg~azbpTIjn-I?Yg{vC zLJKpA{mkCrd#VgcC}Y*&Ro$P8is z4E}ivt>f>)KR+h7Q{t$n)FE0S-O!I4lgxTnT03l?bY>B)$y}5}U7%+&^}((CGx+CY zel!2Pq;yB>D<@OVDFfBqT2R}i4>HP`anUcZ%O2}AATpyX@DjC%ZpUO}L+qcyKVS2^ z^Uu5Lc?=S7>vF3%qMhVKh&V&er#jLFpoVk-mi3S3pO4kf{meiA;dkYqf7A~elR*T{ zjhymhM08WgR#ZM{2Hj--4F366@XvGnZv69AN)t61>dzbWzD5NzDf&Jx;I=%Fs7+>~ z2q;~xVS0k?`p5In6KM6+g}*cZyp5jIV9Yz_R%?RY$|*`DB!lF6Y8l;=DaHD@o7_CU zj_Y&t&$IrWe}4OC{`pYw&t1P8|9qxePfMx)q#yfl{J{?NpT|GnsJ{K?`R8}k_@Ux0r;Rz0CGN>zEO zq(mA-PKNV^cZG_Ct^~UV69g{?CI#vTvIpV?*Z>vq1kwb`26_cH2O@#8!IeQzXjsS$ zjSeS>Y>U*92zjU6RmrBx>IrR@{$=sckE^CqL>VRjC}ot!M*`uN;Rm6XpP>JYgTt{J`=Y85hqOM_K{9IWf2K>t9?Kt0&lHi035Re|f+#|FV4 zg6>eC(A!YE@W=4*ND}F=)Lu@cTvMj2b+r`wm&HFHr0rEzrMNOhelC@e)<)t*CWpP@ z1)=QlAcJ6CBZ1>s)3!A(If)HU=lR6l$_+$lmx%cOF$ByUvO zsTt6#eL(+`_~*yKKQD^d{F?loJW_fUsU0~Vt{DC?)Gow?HV0b;QwKi;P6Srt98ST< zn!u^RPl2dlo8VTgv18~~s0u7={m8q>7+9Sf@>r#~YQslP)W0cg!v64zP|?uy;9U5SB*C9xXZ8iQVh@i6?gvQhZjazTysI5U*F$B)$HNsOcOu=T zSn^(EB9p4O)cIOd{Y&GYUr=MKO_go1JKdzakt*29>fuMB0ioofOTlqCmC1uz;A!Ay z;6~taKnWy<#T_0z4IXi~(2Y>(@WF7g$oWW9Ntc$&6%}3Cq4v``$COEuF$dI$Y70N#-K0g1dIS3Oc=};Y!RFjycJ9x>L0orDihup z&L24%sV=>i#>?52hsq4KvgXz=>t7oGe3kl1DXlC4HM;|LvQFf7xJ_6OEee$jnZZNY z%Vxn6!K}e_!OX$J!TP}=IE^2IxkF<@&qI~M2f}&aAefza!}XHJm&HFX@Xhnj3+SW1VgC7EQ!xq~BlXK#N@RDgD!G(J zu&$#(be$YY5!oMZ8uo-w;f<{w$`ta1^k66$2~we?p<s_>yYD$po-^)$p8&VzVL8NPh!>Ozv_JuEp z7Qjzb4HXDw4P^=C4^;_u3C#&z2nmS$R)>j5yU6uOCFugz_*m|v#8P*tjWk~0r*}2J zEdKd^Ew>TQVTgiyflF-ReB$Pee zD|{lHBr+)SB2r)a5qHSPau0=9SF7cZ7ms* z#gVcRHM}c45HW1(FcS`j-h|$Tq>wwDDO@`|E_^a9L>fkRMB++=rH@hv`3?5bQD&)G zwP@{oJ+X1x=z|%M|Lni&g8r+U)HrluKV>F>nthRLgT9^7F1IjIpwNT-`T6I~w9{%X zb&ZlrSuSUg_enL78(0vjjB|M*JTKfWTrFHMoHd*!oF`m9+&VlS5l%3iD>5|lV@ZBin6f=nvI6b5TdrR~vb>H&S0@ul(4 zO|89lTdj&)NEPL#+*Y=vB~lqlifoQ_j}$@_`!;+fd=$HPFnl)rFl>g?M(ReUVHZZ;h z7FK&AGcZbt2QN|;l=szAV=1ZhBC;zo1^d}JQXRLi+L6}q7>nWiwMZVRzjO+3YEStl zGCEt8bm}Uc$IV(n{es@yP|$t#CGpRn>vi>uS~=~kT1~yDbWu2Ety~Rn>@K{Eg(XpX z7C8~w6j=&8v^cT>_o;^wTFNDLlr~5~siZs)xsFE2u@=CTT~=+ImR~=q*D+ojUnc)N zskTbZsh+~Dpd+u7>&sp_TAC%blnP+#!;XB6ypFtygd(hz3iBY{q?OV`DYe{1K8l>x z0Og5NRXwH_)b?T@kLi_-d&ZZ?KkuZ!)w*e_I!;Zd?o}Epq_SP^Ag7Z*U}k5U)K#j5 z%vOFWpHx(;Dz%kHOPjI6cyb+ik^ELJrYuw}rLB4can3QV7}i+MxN0;pWBBJ$>>tfP zXN>VsepsgE(k`eSR5x<3?UdxoeR;XuRW2bXlWpm(^gz0WTmN%OlKgTGxgq@GahXuc zDYKN9$jdENX|1dFK&zpj)5{pAjPlT>sf$U_&bEpkv$(_xA{+FZ%0egM&*Gm~Gj8fF z^`JIZOQjuE+o*nYxb#!&eBju8eqcV9oXAK6p#d$%dRy>8~7D{P-6~ z)s)(3t&bL8JEXQ(6XP~A zU8$?2RwD8xd5b((9wQHy2g<|b$?^*Mko-t?DFu}7%4Q{~6jmpx&(xyYQjO8O=?`%8 zIAs(y_nG;veb$%4Kd)^*!eq9o&(JgJ=d{kq*d0@QshQPx$~I+?QcX#(@XAl}efg>! zEnkuE$?s)GiNch2UuA=W-U79gx?AP7X4(NQsXknPhfMN0qX=fRvsmk`@9fofT4;NG zDg5(C`anI2epKtACDbmeW7IOLsNO`6{BWhYQdY^Se5a&D)fY^XEOFCD@X>joIu!gMY3Y)1f|d zPVcP8(GSCVq|qL#OVrkCc9leAuuoZ{j8_IKy)bLvUm2s!SGJ>)qbN~oJ;dUd)L2?W zZJnlTwe-!p7y3eXj8f)SGX-=)za;*7G4qbm+lX%*&|B-V^y6B8EsrLtht%#6O~R466P&|m0< zjM+xesAcX!zvn2>dm7rOF$cQB@e@Oc_n7HDi8;x+AlLrU{PQ%{QL}|fnd^;OhNExC ztbHo|k+xFnq~+H{?HS^3)if#||I7U@tqH)pioAu1)xOWt` zCPNkcOX8nTu!>kgbA#E+OldwbRv8_Q+y-e}*Ec|Iua#aFYe}TLbW2k-2{x3_J$iCI z7j)b@L49wh{s?}txY5_xYJ4!VnBC3YnBy&P&Bolvm&89mZ1u9TSg*hzZe^wgd3c90 z6#J6OP%zE8U7xKFf~H_Sy)rZ-O6sNb3VLn$nBMwieI0z_drX%VGrAhfja%5o%H~A# zqUpElLTBeS_A!Qk9>YJ6;h)Fw&;LFD{6Gx<-1y0OWjr&U7|)G2M!+zk(VEQ6X;y?n z=m>bllja*()Ph!9Yo>L=3geF4$exC%JcfTB!#^JYrS@_3L_Fi@k@P^iJG3rq(Z%UZ zbX;14KJiiL3J<0lfHxshPszO?^fUt*h9s{c8~9h|pSJ-;DWM&P_WCyH@%FYFS|zPa z*0;q!9}k*Rad4DO5O|J(B0bUSX*IDbTKTOE(2@2-XPK}}Q#TFMHfhUk#j{dd*{$MM z4FCK~;GYkGB6kkQ>%0S3VI36tJJ^*#9*%44xI0868@v{T`mxqPt2=1*&8x?8ei){ zwZ`|$z|y1yJv)SdcMSBf31nxoDw&n^f*^GPjQ7z*OQI-|gpfh2+3HMoI)di*9rV6l zz@M$PCx9hg3nb}OAS+8&4FCK9h%c9|d)PtM62W`R3I23bDDO|RH{j>>)VA$pAb~Xk zg=?X6z`5@jPBQS0nh=Ab6?uTTODIr^is7Fp!5bMxeFy5t8`&LR+`T7pTI3Dmk6{&@`l zyt6&fo(NvuX0SwWLK~WJ5;)nMa$qdR@Xwz@S^00&)12Vw)BzW%7l;y5$oXJ?ZX$P( zpM!s%-iZwc(L4LLeFg-K+Y^@m&H>ap0qk>gN;GY?g~}! z(I5&ev$xnsz#V*MOQ_8y2Scp__OTB<)GFtoa~(dC1}!r;NNcS@vY$b$hvM*cs79Jl z$xcn?#wu(7%yu0FYV>q)=vI*HKseooweA7~bq5GOo8S$XK{b3TIg0ELulW1$&llK} z?GbiAFu>b^z0?>htpoaJZTwmve`)01jUK{!ElkLeRudhy;#7AOCM}Ye%P! zQvwv|Bn}UX?MwK8GuW9;*rD0*3nTEp_QL7w1Pjy-Jdbu@Om~4j>I*{ZSVV9OVWW1~ zM-h!bv_C?HHnx)%1kTD%3s{|rKcC2G=eeUfv554Dm}?Uq!CRXh!#{`JnTg$73|q4j zwr3?Q?NaRLZ0zM&atQoP2Qag%k;M`BrUHZ5B;MhjI6-U%&tN3@e|3rCAWFvuE%TXk z5!CLb&N%0LSi$nJhAF_!RB@_qA-3KF=JF!!(r9}StWIa_Ukkel@@@_BG(rsC68qWJ z?hSVMI8g7Ffzo&on$Y*bt+jvN$%0Nbr!^?Y)10-=Ves5O{Omt+5fzCRL_cB*v5MFa zYxoRa(nTf(#k>gKzDD4&^~Rez0V`aH^{oL5{0At$Z^o}1@TcYY>P&q1*YnSFe-{4v zn7;`Bd?QG?4;;zy;Y5~zkNlpPfE)9Ekdb5f=hs1)cnG%bQ#?=b*&W=hF5+&r4{z^s zytSinGFyV4S{P*T*rWl*STraq3yBd#dqe`cK$JFdBF{QMI5R;gZ-!e|R?vuJ_~(r6 zScawH{;yf4McAzEwd30}P zol&@foN`_`4DlUN7C+;$h@em5{E)a^6-IpXJvk9xa6fqkJ1B!Hkq}vqT%g*PqpDK1 zsrsM_HpH)Wzyz;A6$dRl130d3kfdMZgdWCjPJz#9h`mfr(&S6xH2&*jh&DuNA`L+h zkKwx(g9_TnDTFA;K%{aE@7j2HzB*vpro#?e)_d&RMdTTGS{tkt=x&^cyyR?aE=Yb$ ztu@vbWGYW12l?EJfb*UV9OFvJN)N$pWS9LT@{qBRNQY(fE2sy`{>A36Va0urbY-(TF#ZB939om%o*lbbErAM?2BiBIm{eq&cs)@nJ1uF8Zbq$Qp%zZ zGy(SZre#^_kpUWxjMhUthJXI8@XuMWx%)U{PQ1> zLy6&^FQ%?hG@YAnNlylK`5tZ3$(RC6O{P6FkeR^DVU{o}n3Z4@EnucGBbo0(doRmm zWZcX<5VM!kz36guLi#1OjT#7F!c!NK7pYFhBCilranBNobFg@29NIa7d}8yAet#!MrIe?A#~yEhF7ox6?AspctkO5{UreR~Z5oQzGy z@XurT=P~^A?a11a!DzE(*y;k|rPd^X+@D%2<1{ehm3xxL>=$p@{ z`}7amWo@^%LYtwD(?;MKt4+tm=ML=xxbK3VLvNx_(vRv2_zAs?y@rKJ-r44JGcV%1 z$5#HYo_{_E^Yo825#+)in4);CXEeHDm5f>2TmuVR%lZMCf;Qh2|9n0sUD9GA>xdD{ zY-=6?Q>icF^nA#In08C&G-#RA35jS(9wJjvL(#Qake)@qr@saMIfL5Cb-ReO23hm| z#8aXYYT=2o#;0Iqt)MNs9pvGx@Q6 zAHEr1l26Gi;JnY{nsZUyQ+5$slcm_LOhd+@SJI{EC+KyJPi;mu{tnR<`XzImjLs3{ zs@`LgF@<%&Y-k!r4F6pCKl0B{eFOaS=L)N4RU2b!`5fjYtAJbk5}F<}!8ogiX{RLS z7&BzHvTj)A?ZcRM{kr+*`TiIFc|7Fd|8xHQYvrFOQ0qhS;+{y$R-;+MC}Z|9sp3$UpD!mGjS) zZ=QeNPFgBGftvXM`I?+lnFn3LeriDN1}fhGoipYdnUHgD^cUctC$$cI1N`%T(pf1b zw3J@U^^}WBdBk@$pn=sDI+#n0Lgqblh?NEtG~Jy9#1Wz`=|*O>HXWvCFqzn6Y%T5) z*Oq_Hw|3oel@qoIUa_sXTeQR??q2Q{?lbOZ?ugrVTln=E{b5EYpI~VTgQkP^UR{?nV4=Dc3wF%iK65iat2j^en<~vlCyi+YTOg93m@Wp zxjwjB2+=|gakBVC%;N6u-sryXW;{`zf}XOT3Z9aloSuZ9i2JO2rn|P=<31#|5gBob zkX6{@D(KqHXXY1kJlBoA%H&`s($A@a)D-e5I-D%FxN$Q6{!N+b2XIucyIV|rV|XC5#+d=vch zQ<1dNWXXg+eMBAz9p!~!o?g@X=*f(;M&EzY-~F2T=T{>Ir1erNDD^Ei!9SP7t-;4?Bt4UQ%7U^^sjN!sTCLH~ z{PP+A;Gg#(3Zl2;G&P>C$Z&`PdUN0LSNT4!xWY!Eka$8Y<=*d3@0sLz<;mwA;yvK~ z;En6c?knLd=PT*U=}X`Xc#nEVdy9F0^33*R_ndH7bzc(8ihG2V!f@9EJ|{n%yU507 zYcn(GXo{nXk$s3w&NDl)UCWwcUNGE74Sj+3>-py!!wI25?1A=olzdQbsMyK|DDlVA zFM>K$#f*b)yw!F$rvSPGE|GJorgUoN8A!wpI4^&QZ|mZPRYHF8q*&g4!kyQ%#AAAD zdKY;gcw_sD_?q~7_y+p=`8xS(VZDU!ELNG<`_MDc8_1w-Ya%L{1fZq_y7+8##@h`X24q@z2+V2oUZrg-b<_Mao0(w43aK z>R?mVk6ZJ9@Xs%s^Q|^tHUGR#=wK)*w9d6~--s2NB&ETb{8#?@#BYFqJ|J{ER6M*R zoDHhkrTz#0d0D)z&%b*9`Mi)7>K1+*?i$e})1_?kb$OVQ3sK!h+(Ggf1o-DGzEb`< z14dS7kcaGG!_bjX*6`YJ#>nnSMX;47A=C3tS*5np(jkL>(3td9^3PX;2X;F^1q%cR z2G0ajfClmr)VkNpS#A!s z(_88ytu^@P^xygCt(_uY9shg_*nCOAv)cv6;-Ju{P?vBdJO*@yV^SwMsd618u>atn zA2nv1U9Adslw&*hh&|*q>g(j6mk;d=oyEievM!aK=9&A<59|Q01W&n+xOxex#Is@>x8@$>;k-k=@4Z!hYkWapE`NLf zbpK}m5&sGQ0slJxIDcb*l>douhA*G*nzyC*wWqb`rn{JXsVEEOglVoHc^_Yv>(8!X zuFx`-lqycPBt|)F?Q`f|jc*n=y6MZc-@-p16S6}i!v4q_@XkI+i{zS$SG}e#)Y|I> zp*Hx&JY}u4hdE7&BB&}$)Ma`N)1NKR#o}-Bvt3mLQ&=OGb3bx-^@KcKy^p+Qe9L^F ze7XHy{0sd1{g?eW{nz}*{j2?h{pJ0<|DdlGqMn)FB;Excucx2;o|s!4EnIQMaaHHX zaR=BpOl+nA-IN+mt{_f0&ym^5U{y2w8LRZ`noIjF{PXBg)$m<-5D~PKW^!EQ1*WSF zVOv%GlCjntWwo@6J1Gc@ctGx_X474ml5AXLuGaAFU1@}y!f-LI`>4B?=bopb_mVfC zZ;tPcF9+7R1Z%tNf8l@bzvVydpYLzy&xoCz5+4USV}^#iq6Fd=m^-z_2l!rBCd@>1JMzexN~`qcuIKp zdei#G`X2f+_&fL)`%n5G`#<`F{@2*ct^N_%Nz%U+5$ro}Kdk`lMg}6@aY~~>S7;{Z&$dW`$XQaK-I*qQ!-_Aep1-sKH%z!Ki&05wg}%b9VtaALc*b?gHCV_h-V*z{{hpRz@u@%J*ZpDqegZq$!k@x_#n;Cd%eUP7op*&Nj%Og=)*Rw6;jGK$D#iEX z*0VPlgHA=4qFR$9i51Rq`>91+>CI}!Kz#!=zJERcyh`XysABkbxLd?4?UCBaU=%3J z)lR6UdySXIF>|Rk)NbY!BU0jy^^iJ1&tf{Vg*l$T%ujb!7HnaoSk3*$JbdtqHG`Dy6~v%89rxtjo&w(O-c-KPz6ZXH{?7gt{tGyZs^7$JKJg#*&+<3& zC-9&4b@Wlb>E3wWX&&3%)_qb;B(@VayME%+@r}4i>~7`(P0(qm(qt=QgtNjvZap_$ zW)7o?K3V%M{PPySz&~frC+1OWsXf$bMifWql}X*9w=rYbdR%JM1lGG+3US55Vsp3Z zp5XC&CwWb8Yu^!{&tKa=8Q$YAtd0!3^Ar|np}(y^t^c-fs4uDS2XAigc4XD3xIZE* zFh_XgO6_XG&*Vlchw=5VX|&;G+E9+C&Cd2|n^ESr#f&TZtox-tm&g>hnLR5u!Wo_bo~Z7t?o3?G@_ z-wS^6ENtt0|6Bh9tZ@nUHk<#YZyIs~r@Ym?S3G4r+uiZq-Nh4vSE%Kh&Yy$-$nvx&0sU z6DaPx?QP?I<7wr&<}To#E4~*BBD-^(H@F;JOLhvgo4!xkP#7plv~otNd16QWNEz&h`xC z=w2Y&DXae${&|E%vBSy`7*JP zJK9~(bJJ7VyT_Z**TuINelinQ+08%9KN2f!hiERT|2h0;ZJ*|y>rL<7>dER^jyRyZ zcwR^*bZ~9qf8sK5&Dm+pVfqafn<_{)M@8}n`#O{@Gnn;_iTeMIf4=r-{&|{z@Xv4l z0{=XNX`0uqP4+0KF_8zA<)_qcdMs0$P0l^#mh+8VUSXfmRMf=D?u4HCo><5wKk*ju zjq#oJ+3=JV{7vwdHuG2W=k$yITfTX|s@TVQ-Ynj|oP;>tPB|Z-15n)RVlKwp%IT%FU&}x56#5bT^ArDue=Zqk%!O7DyBsF7 zBg6$nJKxhK86RxxBCa7H&vh0z$%LqgS9ITWH}L$3=zNWr@Ks0LcFAY@Qozy__ZP(q z6Zs>)Q@-)OQpldq_Ga}S!tLXTJBNFbXbSa&4KA54!Vg9bJ-|dUwdoP);k|?VM;50( zGRoV`r=Yh~)hB?A_3QZO-NAS-^$Yy-S!Nsb)q9*L&Te7?*@Vi9sjG9$bhb7ZhmYom zxiSk^h3+Ebp5ad5neHLI9lWQ!34Bd`3w#&gD+PZ-e`3Giulw%$Hp8>z_Pz8@@n-TK z_LTFSaTjrKf@SR{oOZ=`HRo4y&)DSfkz?sS)Jtd$79%=2^XyYr#7t*4HfHOekAL3M z%nnV3AMMpnU!o$JgnCEqrAIOqSr7WDNAme#cg73p5bsoSUv*dU9QCA!-Ms6K^0mZ$ z@-SBR-WTw__1*Cu^v&`$^QH3L@{aIEdG~wDdoH@my7!8y#gW28S5DVZ{tV~h%Cp0n zJ@h+d1S*pQi1p5Go3ryxuQoVDkO{=bzUD|9s0o_~#XWfq$M0bcKu7Lc5bw zh!CMjvX1IS7hy>DBs-idz<=Q9yNU@9gkB=$9`EKn13b??WxNZ$FTLqd`xxh2>pS2( z={x4z;hT@US81QichdX4H=%cjr!>}B$$eDJCe9Lqu8OWj{3|XCdLcG3FX*InbwqSK zQU8eVl(z?3Tg}%-8l$DYTKm8A&)L88&ujl8|NPRw)IZu#*=U(QzzkyZb04`Cd{tM- zHC@Oc?iGu<58=kV#N&ACdsibKNC^wm9XIEhc*gm@$E_pE_rbf#+tkZ@*Ln(j&SH(H z#C+m%ffAazcJee|g`3GS;Y}Nk#_gg|hzR|Notqn+5F8tv9$Xzf75o^C3bhVx4pFG_TnHD5?2Qyg z|4w6BkryZ>(S@}dWVa;7ePg-V-pXO?_9=-l>p4k? z$HXeK9hIJbMXz8Q!tNYI@0H+M>ne&Ha~<)fSjBzNoys%VbH$U;ThBYuyUTms`^Fpe z2D~qj*ZKkZf{I?jdjz*+w`a9GkNcolP~0x062`mU@#WB+^b=c%9mSlb1-c?No;(fz zQOp@^AFy<@s5#1rhTZus{PVVd0si^GU*Mk~F#CY#^2y%pbS5&AugH~DV>%9VhUtsw z{2bSb=Uj_inS>oePI04{#680O*qz5S+;hUCdop;dc-wmW;7-!Z+ZubA)+>1qqpF{$TpqT zn`=kZD0PZLDE(zs8ZO0>)<-HvBB*S%1uNSM-3c9rm01_s6xxsYMhc|?{e40BIr5LQ zLF?@xJ(8Np&*W|jiQc3F$eEAPbAiAx*Q{c((BvE7p-Bp1hsFVXQi$$ic3Ll3Zy{C!7_si_^qcVm|i}*w}zO znWvbizNa15+QHKZ(M=Rqc-lR|UD_>+%fyo6eW9zMxCXm4z6bx5tH$kS6Cn$EgU&*a zqV7P?bqI0AN#pdf&s(Xi{^kuMmoZ2G_5AamzsNuDVkLkIY!@dXagyjpCZvv0?P!77 zfE?CCwkJpM6L^nnqKgo^3%7*=;&j}c)4SWbm$)yuKf-4w#oAJ%ua)+^LXBj)yS_Vt zJ6aqlrV@_`b%htMuE=+E#u{sLN7;1jB<2IuT~<>T)X{!F{y8aCk`{oRT~pqNPQk@W zB6R^c_CJ8Pd_(VQh?wQ6ipuhQy9Bg7rV{zc`{ZyciatrVWOUp?(r{b3eEdE>k81;R z%fp1nSmk)}nivapjec0?e)lE!9rsuhZD|jBky1 zwGMZh&B-oe2&NT%9Gd8Z$Xi4XViIPSOWI2<%WQ6*G%_2%g@2y#7x?EntgBWpJAt#` zX#$mX$VcM;k+0=C>Pjz+!z#0j{l)#rF{gG{bhpQQJJCHA zQTrfw8+RFZQuljtyVzY!C*BYSArG+2RSuQN`uuIK7IzL&z&a)lGk|^wy|ZOFkxhw1 zP{|!=-?xfdD^1btZTuGgd5Qmpe_jJ>u2Y;W&}ZocHMUjAoL{FqFe)>e^>Q;fKR=!~ zP`A76$|{T%ZV4&H7UBZ&q8JwAxicg8UlO&|-0qZa%KcE>CJqvdiVC{Y>LQ;r&y~$} zgs;e7;;Lee#o6slDrPd&hMJ&O_Z>N&cn96T?Z^oXwqBUkF>{{PSg9w~=WD-?e}3c_ z_~&!2?9e)G<2cCepuOv0QyzH(XhS!NO^QhfV1v zE)kE4cf_~Y(;$AmBObw5df_{~ct#i^WD#z<`k~gkp3l!8 zUO56H@o-E^Hq~0{Svv7yo?b zUx0sp?7#5Oul<96Ugn?p=e-G=7y~`1nWzuUq2n@B7?$nNzGiE3d$?bY=PqRg=0#)*vrKH|ad|p-)3E>9keeiiS?b zZ{eRuflmCt@y}N~QQ(h6LDw@SwFo;onRe*!na4~8b_?t0IwQ7=kDJnZ{tX}Hs_z5@jd!IX4q*F3skjULJ{$E@y{DT4gPE9pTAT+f9Ib+`!D?SrqFy# z4mS8Js|wV3lS2tV;55Q5D+|aTI(j~iQ)%eo^aDB%bm#6d8Q7kfCREuxTo-OBcM&Qr z@%dbQCB6yYf$zq5=Q~25r5ay=Plg)#4Q@Txk1NS>+*x)MTbTU>y}BxlL@x%J@F_K% zN>1&Ew&_pMGEPD4b!s?op+=VydF95a+ATNBnV*bBMrBam|F8V>yT8CcF9qI$=yZn` ze?ej?h$ijG^JGS90`-b2NiRTdpd>S$d4Ra5J-e2DivEa-Tpw-;ca(d`Nyx^=!@5%N zN%=U)iU+t`+&*p&eh$Sr4|kJY$TngVu$P%pOdjSLJ%!GT-hkm$YU%_O&mB-y3KF-R z-k{TL1$8E1&4M!2Bj|VLHlG=@jY9M?x8M-WGU3QedfT4$}AmRC!o#n-&x+IzHk z_&g63OFC+kpb+&)^J9v1h<;GF^wP#;F<2WG92w=>To>ouK9s&N`^b>QJ|?jh?y~{`s%vpO1tpdBo(BJr#ro))4wAZPSIAp3FMt4kop-vvuK5X0bo8$JuM>4t>tP#K!}C zew^LR&Sv|u^^o7-*?Y`JrVmq$p_$Y4Xu1TgQ#+^*R5I!^$QjwmN1$&MCf+*pLF$rV z6YAT7{ki$)oxzZD6OD)^y&HneJ$*7wEj}3j3`8c=u?9(sAc3bJ3*)IxtYdlga6`3D=&C9XTg(f z>TCcTqXaP-Jb=_>Cs406vN$!8is7Hf@XurT=bwXrUK?8Ai$Dkeh{>c%Ai-`1M#s0~mUt_X|s6m+6K;1|*4 z1(4n9g6;a)S?zR!vbh4H-Z+r=lG$&qy|^zlva(q$VxRrc#q0+~-I8V|=wfiNME{91eW?Z10qQO#Q?a1JUzDx^4gPL)4F7x| zwHSJu%|U!gLP?;&tRsg)eI^qacGr+Y>IdRjQc&xTIy0O$;LEw4$Dm_QhQ4YH|NO7c zKVNF?wk}z(EgIyBVsD~!$7XPv0mQ~Rl%)F!Yn=1}9PepFkW;v%3E zd8i<4*FJC<2ZB75A8gSeXkhEW=W9yj2b=Fb6mge<9u>nszhWJQ+Wcye+-E>hdJOLP zBk+ud4)io|mR5rHaS-gK2i7Nx0V^vf7$R-3i;M03_8qW66FLQOTO9;C-!ZV&Smf#} zfq*!b*otaT0NmQFWM$-jM}dC5hrEm)2^DpNF)PQ;lxMLYq6I3Z}4 z6_IV~51Q9XayRlJx6z>$#sr>7#labk;h!%>>^lHN;_75UG7Txh;@p8A+$zvryC9pK zmq-Nu;1h7x);m*>S*{P(N|YmlL3|$;aVtom^4wr6 z-9fyw20Pgs1j4f5BPVny=O@%E&LGdd3cS|Q;4O86$EyP>cp3PnF^lUG0>|+$nB&his0_k6}E2) zu?xK)G5qtdiGRKcc=-{VOnI(W!Tp9P;OlO5885WCzay` zQSzOA54}W(z$#mbb2b@uph2K2|Bag64)*qM&)>M&UBO@;fGp-jkhTB1{PSAK1PsDG z?r%it>&{z;AQHf~mIe*1JBXQI8~=Pk4FBBOikr>@M06r1GxH;+YXdJi9TD^abhtbN z5yXX4l?kV-0z74F)VT&AyC1_pkKvyWM=i1jQGiHA@aWsQg$|6rMLT_+=1v9N-IIY0 z8nz$Xm%yaoh}d;9?u0$<_K0`tqIa^qT^bQi(Vw3Z_`CwXRu|vvfLqyc(2Ev>Wq%la z%%`@5dU;AGuT$A+V=|~@ZBdJnmSn z?OJ#z^Mccw+>UFzK?slGpI64|?1`VwGGqj<*l%qHr?N0IbbXLB-|k#RjmJx5L2jo5 zD8Z|dJAXvz=*G%HRzh?(5b@huyra?NBl06@k^+ddDUiX*O65c*Cm)_XR1W0Vqc9g3 z3*=!1>fyJ^)8uwUeB)4$uMbju6e7D1@JQPc)%7Oo5&6)Et>BKe&shMw&=_~+l(=8L zvM<`(?0KjLw@1yV2zD?5W_}gW-5(;$aRHf}Bgj_m$8!)_ty9Qb-2kQK9mw{)od^oz z#c&(zWRHZs+Ks%&2U~E`GKKju!5jgtYnMI8?vD&z2@u|W*uiI*;5=k)vX)rWk%JtJ3QiBJD>&0#@p~Wq zc{H+-i>-~=*Gtw@a1*@vcO~p5_;1X`Ufx0`G7;X_82wA5+Q)E``opv5!AyvPEaOq|f~HynthVS{Due0f%vNeJ!sA(Q>3?d!!hy8F5 zK25$Oeb|v2R5xl8wU#Pg-!38gluF4qx`3=<1 z3nJIM6#Gab_t+9|>S6LN84q>Sw$wztqgSaF$S$^;ROF0(dH zPEk2b0SlVMX+fhf7p8fVqFGFgs!%iLy8Mps-FGY9%vD0DAB zHm)1zjZ?;PJg1FI#%(AgN`_#jG7Fpa(fhIxXY@7HuZzQnF14;<=DaX&UorgitcYHI z5B~X!&&od^Vazvn89#!&lf*1#c0~_XwCR|65D~3KtdtVb`cmA!)1dcqz4HOH5xvo+ zY=Fx%4D}70EP|fmo$v;!={ocn^i(~jUFfo^!E|RPV(xA)bB?)*NuQU@YkWLrZZYSX zeatFm624!9$%c3LF})LAt+k;9^@iGt*dRZpqL$v5%te00U8@Z`>hI!SP}OlcCvl%I zhIyXD)-bEI<+85f|Fol7z;r`T=>U2^`x*_65=JH?ks+ejUV|oyimq}F%3x`Y0!B@v z3;N6dAOF0`AI3lL!&GC^GBP^SX3~x6H1un9nsuboV@_!qqMlgfIbs-W>w9MPmG<$1f!Kv+(>CK#s~ejeil9RJN2#lHuSk3Mz`x- zD3^2SY%OiHH>MiGiVJ4zFZ0|$(&t_0cx`imO z4BppGWE0Xu9z#33 zatpXgm{DrZmEuxjPcN}c*bbO52s1mFZp?SgJw$tjVPR)dg)nSa z4z&Y$%>cTCGD91xE%G@laEE__+eSJ}y!JLznwRj#-6?|7G2U#U>#kwidrTu zwq~gx)tBls5U}2>n(Ee~Fg4gpo2cyp&pw6TQ2+h;=NT|*Rtl<5WkCwh#i!z3{ClVn zt>AiNKi%9_b~;;`r7^GE4oVP*=vKIMuA|CP&(Is0h}?nu@^fT7;yYXHiuMbvF|~C9 zGZ7whH#|lh5+7DdN);xdW1d@lAM9M**2$ybJcEXM-Y!Ew{}DO z;sfHA?8Ys9gq}lxp)J-LY00z~>UMRs+C(j+rd0nZ{&`n^G3Hv|V$wA^v^xs8ih!e- z6Kc>NSAf65uj2>c?e##u19Bd1E?&$(h)gcFjWWC(1x=rx7wb z-y;`3)kvEN^>?*_N@9AoaZ^>r+C=6LOSR+P0J(*ZXzoy0=Lq>TO!wx+D6tGlqmf-0-t zR<MX%4tKi}axhspXU&_BQLy5c(MTHzY*YUs+0 z3FgC?Ue3qAgI6idy=KR;QS3oPK9A|Hw2E0!j+%se$P6MbF$cX!3sKuxg`49}-109O z&7d(jSZWq4qV#KN_i!t5?dh^RhDI4=9I)rE~U0I5M;9l%ID#q zKXjR{cu-8p2?hEb!goSkD9_w;?S#faO;-ZfEq)rT?k8>@SBSgK_Fy@79%A*abUykZ z@+e2hqKN4V5__CH&JJ{=Zbv?GFV=X@sAIg)`|GiwgH;{c(JR#2s#`s;Oi>zOeWdb4 zJ|pjx*U78owemLk1V|>DoJy&xj8G0KpM!s%1{xR5h2GHd8Y_&3eo!Z&wvZbd=1*Lk zT;IF0yWa2%;aNU#Q@D&A7_sbArZW?w`(Y>hQejl#-w-W{drlqa5^AX@aECvNSo5?| z)wrX#({*j8mR-B14p7snca>S#tt84D`LMh|9wfJu8^|@~T5=<~vpf>i$TM;ndQh#D z-FG!!bD+%a16S&xA9yRjtiTF$wGUmUvR=j*92EdSD0Udy?n+E=3;Rx*sSa> zrT|pT^3yv|4O$5=IR*NOeVuo9bNiN6%{q^%?9)aC zl*jT0tgp75TTUX2G6^kEPL3yMlFQ3ou<~e`#~J-R{PTd|7L$wV#Y|#aF`-C_FR|u1 zLQA~CFIf@#{eSJ< zd2|#-9tZHrOlG>LuZbcA1v#QfkW+Zk<%ocRfFyuP(B)hM5fagG$fZF7VZ#*!dkXBLZ{Pd_{8v<0)vv0$ zzn>E9{|f!F{v*D4Uy1dcWqM!tw(_1dr?Hf1 z)iOF8{|o;-3%Mo+@-zGLgqzPb;Xc3@_eW9p91ZOjy2sHP>4{;3Qf=Qbon_u?;elQ{vo?R8zto%bNC%@#Tqd8WPWdAQZe z#)&{u^=`aD{aVnATJ99StpC^-?b~a0wT^oSB5gRwY+;rd*+xghf(bPrt$Tt+vT)|X zFC2gB%7($L-N$Z+e;y$W5f&m_`J&(#6tS*&FWeowcuhDetVW7ilt7X3H4v%0Yx!ub zn9p+MuII6eBs)v+Eqb1#fn%oqJKU=0b{u$Ka{YYpn(XYdejAH-y&wKmJ{F(S_^4I(4#|4+z zUQ7|Qk-WE6%oDeZ>yZjR4hCog@rtk>{VYMK>F55ApUMj`l@xBaE8u()HBXn&b=ZYG zhqSi)LteI>z=)hxbqTistC3@t=KsR?xNn>FptZ`|$h#4F%7sRX;WTpDP*$H^&^POu z`ar#l-db;}N9yhL9(syC2ieJ$dMkGO{PV*#_~-H;!aqMJI%@FGhYL>O?LUNnJ`~1B zjALF~NN4vsC&6=j&@st=6qP`# zEx$UnIteu^3-m&Qd%!mk^Xh}vLa)R7s_8ef48>T>y0FXoV!f9x=qKQOO+yav)3gsw zpm&0QzC(CYIE|{{53IM z|DBKE=W`4_xgDplLqfMWES$Q|3n{VHx205Xt|Gxs!P$YY{E_}CzEe1tTILP$jxn!b z#wlmx7{}HjN!g^ibS&*i!)PTG*JI=eDIyo)MMcm=I+qsFdiq%X_W0)lbci1E)A9g0 zNsf^tphztjtHgoQR>_c})B`H%VP_)@LC z-n!ll^kbq?$a=6MeTZ(*Wi*Cb&@i*e)1)1#Pr`_Y)FmxRJefc?k#f?SPN!ef-|nAZ z!hAinK)6b z5;BqCn(8j*+hRV*KyOKj&}gpLe{0e|`>0^`0O7^YlC3KTqJj(D~X32Vn9PijO13T#-}d zeAy?rgXJ;J`u3FVAe3h=}}Ab%|Nmo8xyxgb0x?1CFH9yUfe_ZQb1-22oI z?dO``aas1ULy-gXJTpl zw0T;Z)<^52MQgFzKq8efF4uzhHC@@H z99Bw{Gs;O=E^FX}b;sD;BaeV`vQlae^E6U?3zGeC_kQFLB1O~nE!5Ltq01bf+JpEe zdXO!rdKX6Iy}{m4BlhARAljPYJz+L6ry8X!j_pMo_t2j70+~jdlTvMsHcIQRMQY(% zm{wbBs>Q&}UWu{Ugxs8e-Uug*lVB_53Z2CxVyv`BY66kxyc~&sTLpKhLJ2BPSf@le zrEG^h_=Mt`v#|cP#F2nU9k_IDw=0`nK^@AUE5$`m_`DR*$SRn^s zhwL)a*a^LxegNzLmn5CsMfT&_jMilBs`|NlNIj^2s+Ov^%0FMq_i$Ib7YGl*t&5g+ zNiDGIQ#lrr;%AV+ns~Z+`g#)JfwlA0@{}vNaL^h+SWK2Fs&-3E7f&Mve3|0}K+tUe(bUH-I zbeLz4dul@=UJbP_D6f!PLy&4O<-%6TLdElldn$i~R8Ws-$KRO-dMs6eW+eSljs@p0^!IuZK!s?c17K-E>~x&nd)?P zk-AMSQQcZkZIM>4^(4RDKTqP_5Z#^?s)RSh2c(TsguDoCOj6duTW$=;Z7e+3g`WAI zY0yABdt^^Bl%=-H3DiAInh0ZYFjSsK!UXprUgYC(F0kLJ;f=H>9WU9JhwOtt$W^zj zN(#Oj*yumy6MS8*Oz#1cH~Zrhz{4i$S7*0TKTBQk5QR|`E@0# z_U+Qc(pzGPn2cSB%iRYe=yAL+5rw(+HAkNP48GQGZ0lB?hN?9$a31d*b@Yw4HhU|~ zR%V7##2#kzbwBnT8ua!sl8eKc_YL=PPRP&tI1PzZ?JjP!0b1o9M?I`{xhc q@&0+*kN$bD8~W$3)##u1xdZ(3;ode@z$*5w_fHQD4t7Pl&_4nFkG}K( literal 0 HcmV?d00001 diff --git a/bigbluebutton-tests/playwright/core/setup/browsersConfig.ts b/bigbluebutton-tests/playwright/core/setup/browsersConfig.ts index c1198765c782..b6e7e50198e0 100644 --- a/bigbluebutton-tests/playwright/core/setup/browsersConfig.ts +++ b/bigbluebutton-tests/playwright/core/setup/browsersConfig.ts @@ -19,6 +19,8 @@ export const chromiumConfig: Project = { args: [ ...chromiumBaseArgs, `--use-file-for-fake-video-capture=${path.join(__dirname, '../media/video.y4m')}`, + `--use-file-for-fake-audio-capture=${path.join(__dirname, '../media/fakespeech.wav')}`, + '--font-render-hinting=none', ], }, }, diff --git a/bigbluebutton-tests/playwright/user/multiusers.ts b/bigbluebutton-tests/playwright/user/multiusers.ts index a732048c5c7e..20eff7e52fcc 100644 --- a/bigbluebutton-tests/playwright/user/multiusers.ts +++ b/bigbluebutton-tests/playwright/user/multiusers.ts @@ -183,6 +183,43 @@ export class MultiUsers { await this.userPage.hasElement(e.raiseHandBtn, 'should display the raise hand button after rejection'); } + async raiseHandIndicatorOnAudioOnlyTile() { + // Regression test for issue 25169: the raised hand indicator must also show + // on audio-only tiles, not only on webcam tiles. + // Requires `public.kurento.cameraSortingModes.showAudioOnlyOnFirstPage` enabled + // in settings.yml so audio-only users get a placeholder tile in the video grid. + await this.modPage.waitForSelector(e.whiteboard); + + // Presenter shares a webcam so the camera dock (and the audio-only tile) is visible. + // Audio-only tiles are only surfaced in the unified layout, which is the default + // (public.app.defaultSettings.application.selectedLayout = "unified"). + await this.modPage.shareWebcam(); + + // An attendee joins with microphone and unmutes. Speaking takes the audio floor, + // which surfaces the attendee's audio-only tile on the first page of the grid + // (showAudioOnlyOnFirstPage only lists users with a floor history). + await this.initUserPage(); + await this.userPage.waitForSelector(e.whiteboard); + await this.userPage.waitAndClick(e.joinAudio); + await this.userPage.joinMicrophone(); + + // The presenter's grid must contain the audio-only tile (placeholder with avatar). + const audioOnlyTiles = this.modPage.page + .locator(`${e.webcamItem}, ${e.webcamItemTalkingUser}`) + .filter({ has: this.modPage.page.locator(e.webcamConnecting) }); + await expect(audioOnlyTiles, 'should display the audio-only tile in the presenter grid').not.toHaveCount(0); + + // The audio-only attendee raises hand. + await this.userPage.waitAndClick(e.raiseHandBtn); + await this.userPage.hasElement(e.lowerHandBtn, 'should display the lower hand button after raising the hand'); + + // The raised hand indicator must appear on exactly one audio-only tile. + await expect( + audioOnlyTiles.filter({ has: this.modPage.page.locator(e.webcamItemRaisedHand) }), + 'should display the raised hand indicator on the audio-only tile', + ).toHaveCount(1); + } + async toggleUserList() { await this.modPage.hasElement(e.chatBox, 'should display the public chat box for the moderator'); await this.modPage.hasElement(e.chatButton, 'should display the public chat button for the moderator'); diff --git a/bigbluebutton-tests/playwright/user/user.spec.ts b/bigbluebutton-tests/playwright/user/user.spec.ts index 8b640505a29b..b4b6c3f08a17 100644 --- a/bigbluebutton-tests/playwright/user/user.spec.ts +++ b/bigbluebutton-tests/playwright/user/user.spec.ts @@ -24,6 +24,12 @@ test.describe.parallel('User', { tag: '@ci' }, () => { await multiusers.raiseHandRejected(); }); + test('Raise hand indicator on audio-only tile', async ({ browser, context, page }, testInfo) => { + const multiusers = new MultiUsers(browser, context); + await multiusers.initModPage(page, { testInfo }); + await multiusers.raiseHandIndicatorOnAudioOnlyTile(); + }); + test('Toggle user list', async ({ browser, context, page }, testInfo) => { const multiusers = new MultiUsers(browser, context); await multiusers.initModPage(page, { testInfo }); From a8a85a147699b9e56ced60f983ddc7fc78e484c0 Mon Sep 17 00:00:00 2001 From: Claudio Promptoso Date: Tue, 9 Jun 2026 13:59:14 +0000 Subject: [PATCH 007/121] test(video): skip audio-only raised hand test when feature disabled Address review feedback (Arthurk12): the audio-only tile only appears when public.kurento.cameraSortingModes.showAudioOnlyOnFirstPage is enabled. Expose that setting in the playwright settings helper and skip the test (instead of failing with an opaque "tile never appeared" error) when it is disabled, mirroring the existing pattern already used for other client-setting-dependent tests (e.g. emojiRainTest). Ref: bigbluebutton/bigbluebutton#25169 --- bigbluebutton-tests/playwright/core/settings.ts | 4 ++++ bigbluebutton-tests/playwright/user/multiusers.ts | 9 ++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/bigbluebutton-tests/playwright/core/settings.ts b/bigbluebutton-tests/playwright/core/settings.ts index 4d62df742730..d32001d999bb 100644 --- a/bigbluebutton-tests/playwright/core/settings.ts +++ b/bigbluebutton-tests/playwright/core/settings.ts @@ -37,6 +37,8 @@ export interface Settings { skipVideoPreview?: boolean; skipVideoPreviewOnFirstJoin?: boolean; skipVideoPreviewIfPreviousDevice?: boolean; + // Camera sorting + showAudioOnlyOnFirstPage?: boolean; // Emoji emojiRain?: boolean; // Whiteboard @@ -91,6 +93,8 @@ export async function generateSettingsData(page: Page): Promise Date: Tue, 16 Jun 2026 17:52:03 -0300 Subject: [PATCH 008/121] [svg-notification] fix custom SVG not appearing in notification (toast) --- .../ui/components/common/toast/component.jsx | 20 +++++++++++++++++-- .../ui/components/common/toast/styles.js | 9 +++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/common/toast/component.jsx b/bigbluebutton-html5/imports/ui/components/common/toast/component.jsx index a0775bbde788..4bf255c265ec 100755 --- a/bigbluebutton-html5/imports/ui/components/common/toast/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/common/toast/component.jsx @@ -1,10 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import Icon from '/imports/ui/components/common/icon/component'; +import { PluginButtonIcon } from '/imports/ui/components/plugins/plugin-icon/styles'; import Styled from './styles'; const propTypes = { - icon: PropTypes.string, + icon: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), message: PropTypes.node.isRequired, }; @@ -16,6 +17,21 @@ const defaultIcons = { default: 'about', }; +const renderIcon = (icon, type) => { + if (icon && typeof icon === 'object' && 'svgContent' in icon) { + return ( + + {icon.svgContent} + + ); + } + + let iconName = icon; + if (icon && typeof icon === 'object' && 'iconName' in icon) iconName = icon.iconName; + + return ; +}; + const Toast = ({ icon = null, type, @@ -26,7 +42,7 @@ const Toast = ({ - + {renderIcon(icon, type)} {message} diff --git a/bigbluebutton-html5/imports/ui/components/common/toast/styles.js b/bigbluebutton-html5/imports/ui/components/common/toast/styles.js index a0c43d89aabb..85bd04d17fe8 100644 --- a/bigbluebutton-html5/imports/ui/components/common/toast/styles.js +++ b/bigbluebutton-html5/imports/ui/components/common/toast/styles.js @@ -113,6 +113,14 @@ const ToastIcon = styled.div` `} `; +const ToastCustomIcon = styled.span` + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; +`; + const ToastMessage = styled.div` margin-top: auto; margin-bottom: auto; @@ -211,6 +219,7 @@ export default { CloseIcon, ToastContainer, ToastIcon, + ToastCustomIcon, ToastMessage, BackgroundColorInherit, Separator, From 818339f8d6f6329ed10782bbea1d700e3a0d53af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Wed, 17 Jun 2026 14:16:23 -0300 Subject: [PATCH 009/121] adjust raise-hand audio-only tile test to use unified layout --- bigbluebutton-tests/playwright/user/multiusers.ts | 4 ++-- bigbluebutton-tests/playwright/user/user.spec.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bigbluebutton-tests/playwright/user/multiusers.ts b/bigbluebutton-tests/playwright/user/multiusers.ts index 56dc5a01cb80..9157fc96d0ad 100644 --- a/bigbluebutton-tests/playwright/user/multiusers.ts +++ b/bigbluebutton-tests/playwright/user/multiusers.ts @@ -198,8 +198,8 @@ export class MultiUsers { await this.modPage.waitForSelector(e.whiteboard); // Presenter shares a webcam so the camera dock (and the audio-only tile) is visible. - // Audio-only tiles are only surfaced in the unified layout, which is the default - // (public.app.defaultSettings.application.selectedLayout = "unified"). + // Audio-only tiles are only surfaced in the unified layout; the spec creates + // this meeting with meetingLayout=UNIFIED_LAYOUT. await this.modPage.shareWebcam(); // An attendee joins with microphone and unmutes. Speaking takes the audio floor, diff --git a/bigbluebutton-tests/playwright/user/user.spec.ts b/bigbluebutton-tests/playwright/user/user.spec.ts index b4b6c3f08a17..b64e2f7791e8 100644 --- a/bigbluebutton-tests/playwright/user/user.spec.ts +++ b/bigbluebutton-tests/playwright/user/user.spec.ts @@ -26,7 +26,7 @@ test.describe.parallel('User', { tag: '@ci' }, () => { test('Raise hand indicator on audio-only tile', async ({ browser, context, page }, testInfo) => { const multiusers = new MultiUsers(browser, context); - await multiusers.initModPage(page, { testInfo }); + await multiusers.initModPage(page, { createParameter: 'meetingLayout=UNIFIED_LAYOUT', testInfo }); await multiusers.raiseHandIndicatorOnAudioOnlyTile(); }); From f820571c96b4f35ce165944b594849b8860ad37d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 19:36:11 +0000 Subject: [PATCH 010/121] build(deps): bump markdown-it in /bigbluebutton-html5 Bumps [markdown-it](https://github.com/markdown-it/markdown-it) from 14.1.1 to 14.2.0. - [Changelog](https://github.com/markdown-it/markdown-it/blob/master/CHANGELOG.md) - [Commits](https://github.com/markdown-it/markdown-it/compare/14.1.1...14.2.0) --- updated-dependencies: - dependency-name: markdown-it dependency-version: 14.2.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- bigbluebutton-html5/package-lock.json | 34 +++++++++++++++++++++------ 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/bigbluebutton-html5/package-lock.json b/bigbluebutton-html5/package-lock.json index 6b17ef822b2a..3377cd6bd0d3 100644 --- a/bigbluebutton-html5/package-lock.json +++ b/bigbluebutton-html5/package-lock.json @@ -11692,9 +11692,19 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz", + "integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { "uc.micro": "^2.0.0" @@ -12133,14 +12143,24 @@ "license": "MIT" }, "node_modules/markdown-it": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", - "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz", + "integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", - "linkify-it": "^5.0.0", + "linkify-it": "^5.0.1", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" From a33e4cbadcca4f0d332aa0b6b57ef11071cde19e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Thu, 18 Jun 2026 09:34:05 -0300 Subject: [PATCH 011/121] remove font-render-hinting flag from tests --- bigbluebutton-tests/playwright/core/setup/browsersConfig.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/bigbluebutton-tests/playwright/core/setup/browsersConfig.ts b/bigbluebutton-tests/playwright/core/setup/browsersConfig.ts index b6e7e50198e0..b061ef74a7ff 100644 --- a/bigbluebutton-tests/playwright/core/setup/browsersConfig.ts +++ b/bigbluebutton-tests/playwright/core/setup/browsersConfig.ts @@ -20,7 +20,6 @@ export const chromiumConfig: Project = { ...chromiumBaseArgs, `--use-file-for-fake-video-capture=${path.join(__dirname, '../media/video.y4m')}`, `--use-file-for-fake-audio-capture=${path.join(__dirname, '../media/fakespeech.wav')}`, - '--font-render-hinting=none', ], }, }, From d87fe9207f947be9a2b63e0bd7aeee3b02ace188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Fri, 19 Jun 2026 08:28:23 -0300 Subject: [PATCH 012/121] add setting to not display guest lobby position --- .../imports/ui/Types/meetingClientSettings.ts | 1 + .../join-handler/guest-wait/component.tsx | 13 +++++++++---- .../ui/core/initial-values/meetingClientSettings.ts | 1 + bigbluebutton-html5/private/config/settings.yml | 1 + 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/Types/meetingClientSettings.ts b/bigbluebutton-html5/imports/ui/Types/meetingClientSettings.ts index 420d7403cbf1..9e9cc7765b4d 100644 --- a/bigbluebutton-html5/imports/ui/Types/meetingClientSettings.ts +++ b/bigbluebutton-html5/imports/ui/Types/meetingClientSettings.ts @@ -63,6 +63,7 @@ export interface App { skipMeetingEnded: boolean dynamicGuestPolicy: boolean enableGuestLobbyMessage: boolean + showGuestLobbyWaitingQueuePosition: boolean guestPolicyExtraAllowOptions: boolean alwaysShowWaitingRoomUI: boolean enableLimitOfViewersInWebcam: boolean diff --git a/bigbluebutton-html5/imports/ui/components/join-handler/guest-wait/component.tsx b/bigbluebutton-html5/imports/ui/components/join-handler/guest-wait/component.tsx index 4960abf7999c..4e0e9f2f42ca 100644 --- a/bigbluebutton-html5/imports/ui/components/join-handler/guest-wait/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/join-handler/guest-wait/component.tsx @@ -93,6 +93,8 @@ const GuestWait: React.FC = (props) => { const lobbyMessageRef = useRef(''); const positionInWaitingQueueRef = useRef(''); const loadingContextInfo = useContext(LoadingContext); + const showPositionInWaitingQueue = window.meetingClientSettings + .public.app.showGuestLobbyWaitingQueuePosition !== false; const updateLobbyMessage = useCallback((message: string | null) => { if (!message) { @@ -168,7 +170,7 @@ const GuestWait: React.FC = (props) => { // WAIT updateLobbyMessage(guestLobbyMessage || ''); - if (positionInWaitingQueue) { + if (showPositionInWaitingQueue && positionInWaitingQueue) { updatePositionInWaitingQueue(positionInWaitingQueue); } }, [ @@ -177,6 +179,7 @@ const GuestWait: React.FC = (props) => { logoutUrl, positionInWaitingQueue, intl, + showPositionInWaitingQueue, updateLobbyMessage, updatePositionInWaitingQueue, ]); @@ -187,9 +190,11 @@ const GuestWait: React.FC = (props) => { {intl.formatMessage(intlMessages.windowTitle)} - -