diff --git a/bbb-graphql-server/bbb_schema.sql b/bbb-graphql-server/bbb_schema.sql
index 6eedd8f1163a..3a2450a4b616 100644
--- a/bbb-graphql-server/bbb_schema.sql
+++ b/bbb-graphql-server/bbb_schema.sql
@@ -1685,7 +1685,10 @@ SELECT pres_presentation."meetingId",
pres_page."infiniteWhiteboard",
pres_page."fitToWidth",
(
- select array_agg("nextPages"."urlsJson"->>'svg')
+ select array_agg(
+ "nextPages"."urlsJson"->>'svg'
+ ORDER BY "nextPages".num
+ )
from pres_page "nextPages"
where "nextPages"."presentationId" = pres_page."presentationId"
and "nextPages".num > pres_page."num"
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
index 0fe1ae5a29a2..e411fc54043a 100755
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
@@ -159,6 +159,7 @@ class ActionsBar extends PureComponent {
showScreenshareQuickSwapButton,
isReactionsButtonEnabled,
isRaiseHandEnabled,
+ isPresentationDetached,
} = this.props;
const Settings = getSettingsSingletonInstance();
@@ -256,6 +257,7 @@ class ActionsBar extends PureComponent {
hasPinnedSharedNotes={isSharedNotesPinned}
hasGenericContent={hasGenericContent}
hasCameraAsContent={hasCameraAsContent}
+ isPresentationDetached={isPresentationDetached}
/>
)
: null}
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/presentation-options/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/presentation-options/component.jsx
index 737fb24d568d..a6dd5b31ffe4 100755
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/presentation-options/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/presentation-options/component.jsx
@@ -45,6 +45,7 @@ const PresentationOptionsContainer = ({
hasPinnedSharedNotes,
hasGenericContent,
hasCameraAsContent,
+ isPresentationDetached,
}) => {
let buttonType = 'presentation';
if (hasExternalVideo) {
@@ -100,7 +101,7 @@ const PresentationOptionsContainer = ({
}
}}
id="restore-presentation"
- disabled={!isThereCurrentPresentation}
+ disabled={!isThereCurrentPresentation || isPresentationDetached}
data-test={!presentationIsOpen ? 'restorePresentation' : 'minimizePresentation'}
/>
);
diff --git a/bigbluebutton-html5/imports/ui/components/app/component.jsx b/bigbluebutton-html5/imports/ui/components/app/component.jsx
index f657190f4a23..87739ccf4950 100644
--- a/bigbluebutton-html5/imports/ui/components/app/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/app/component.jsx
@@ -285,6 +285,7 @@ class App extends Component {
const {
hideActionsBar,
presentationIsOpen,
+ isPresentationDetached,
} = this.props;
if (hideActionsBar) return null;
@@ -293,6 +294,7 @@ class App extends Component {
);
}
@@ -337,6 +339,9 @@ class App extends Component {
isNotificationEnabled,
isNonMediaLayout,
isRaiseHandEnabled,
+ popupWindow,
+ isPresentationDetached,
+ toggleDetachPresentation,
currentUserHasVoice,
} = this.props;
@@ -388,6 +393,9 @@ class App extends Component {
fitToWidth={presentationFitToWidth}
darkTheme={darkTheme}
presentationIsOpen={presentationIsOpen}
+ popupWindow={popupWindow}
+ isPresentationDetached={isPresentationDetached}
+ toggleDetachPresentation={toggleDetachPresentation}
/>
)
: null
diff --git a/bigbluebutton-html5/imports/ui/components/app/container.jsx b/bigbluebutton-html5/imports/ui/components/app/container.jsx
index 98c847692d8a..82c8020b0117 100755
--- a/bigbluebutton-html5/imports/ui/components/app/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/app/container.jsx
@@ -1,4 +1,4 @@
-import React, { useEffect } from 'react';
+import React, { useEffect, useState } from 'react';
import { useMutation, useReactiveVar } from '@apollo/client';
import AudioCaptionsLiveContainer from '/imports/ui/components/audio/audio-graphql/audio-captions/live/component';
import getFromUserSettings from '/imports/ui/services/users-settings';
@@ -122,9 +122,24 @@ const AppContainer = (props) => {
const shouldShowScreenshare = (viewScreenshare || isPresenter)
&& (currentMeeting?.componentsFlags?.hasScreenshare
|| currentMeeting?.componentsFlags?.hasCameraAsContent) && showScreenshare;
- const shouldShowPresentation = (!shouldShowScreenshare && !isSharedNotesPinned
- && !shouldShowExternalVideo && !shouldShowGenericMainContent
- && (presentationIsOpen || presentationRestoreOnUpdate)) && isPresentationEnabled;
+
+ const [popupWindow, setPopupWindow] = useState(null);
+ const [isPresentationDetached, setIsPresentationDetached] = useState(false);
+
+ const toggleDetachPresentation = (popup) => {
+ setPopupWindow(popup);
+ setIsPresentationDetached(Boolean(popup));
+ };
+
+ const hasPresentationContent =
+ (presentationIsOpen || presentationRestoreOnUpdate) && isPresentationEnabled;
+ const noOtherMainContent =
+ !shouldShowScreenshare && !isSharedNotesPinned &&
+ !shouldShowExternalVideo && !shouldShowGenericMainContent;
+ const shouldShowPresentation = isPresentationDetached
+ ? hasPresentationContent
+ : noOtherMainContent && hasPresentationContent;
+
const currentPageInfoData = currentPageInfo?.pres_page_curr[0] ?? {};
const fitToWidth = currentPageInfoData?.fitToWidth ?? false;
const pageId = currentPageInfoData?.pageId ?? '';
@@ -179,6 +194,9 @@ const AppContainer = (props) => {
isBreakout: currentMeeting?.isBreakout ?? false,
meetingName: currentMeeting?.name ?? '',
meetingId: currentMeeting?.meetingId ?? '',
+ isPresentationDetached,
+ popupWindow,
+ toggleDetachPresentation,
}}
{...props}
/>
diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/component.tsx b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/component.tsx
index 3deed616255d..f9199fd8f136 100644
--- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/component.tsx
+++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/component.tsx
@@ -38,6 +38,8 @@ import { CHAT_DELETE_REACTION_MUTATION, CHAT_SEND_REACTION_MUTATION } from './pa
import logger from '/imports/startup/client/logger';
import { ChatLoading } from '../component';
import Storage from '/imports/ui/services/storage/in-memory';
+import { originalHTMLElement } from '/imports/utils/HTMLElementBackup';
+import { originalRAF } from '/imports/utils/animationFrameBackup';
const PAGE_SIZE = 50;
const CLEANUP_TIMEOUT = 3000;
@@ -69,7 +71,7 @@ interface ChatListProps {
}
const isElement = (el: unknown): el is HTMLElement => {
- return el instanceof HTMLElement;
+ return el instanceof originalHTMLElement;
};
const isMap = (map: unknown): map is Map => {
@@ -439,7 +441,7 @@ const ChatMessageList: React.FC = ({
const value = (timestamp - initialTimestamp) / 300;
if (value <= 1) {
container.scrollTop = initialPosition + (value * scrollPositionDiff);
- requestAnimationFrame(animateScrollPosition);
+ originalRAF(animateScrollPosition);
} else {
container.scrollTop = container.scrollHeight - container.offsetHeight;
setIsScrollingDisabled(false);
@@ -456,10 +458,10 @@ const ChatMessageList: React.FC = ({
initialTimestamp = timestamp;
initialPosition = scrollTop;
scrollPositionDiff = scrollHeight - offsetHeight - scrollTop;
- requestAnimationFrame(animateScrollPosition);
+ originalRAF(animateScrollPosition);
};
- requestAnimationFrame(startScrollAnimation);
+ originalRAF(startScrollAnimation);
}, []);
const renderUnreadNotification = useMemo(() => {
diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/component.tsx b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/component.tsx
index a186e919b472..0542694ee40c 100644
--- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/component.tsx
+++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/component.tsx
@@ -54,6 +54,7 @@ import { isMobile } from '/imports/utils/deviceInfo';
import { layoutSelect } from '/imports/ui/components/layout/context';
import { Layout } from '/imports/ui/components/layout/layoutTypes';
import { useModalRegistration } from '/imports/ui/core/singletons/modalController';
+import { originalRAF, originalCAF } from '/imports/utils/animationFrameBackup';
interface ChatMessageProps {
message: Message;
@@ -247,7 +248,7 @@ const ChatMessage = React.forwardRef(({
useImperativeHandle(ref, () => ({
requestFocus() {
setTimeout(() => {
- requestAnimationFrame(startScrollAnimation);
+ originalRAF(startScrollAnimation);
}, 0);
},
sequence: message.messageSequence,
@@ -255,20 +256,20 @@ const ChatMessage = React.forwardRef(({
const startScrollAnimation = (timestamp: number) => {
if ((containerRef.current?.offsetTop || 0) > (scrollRef.current?.scrollTop || 0)) {
- requestAnimationFrame(startBackgroundAnimation);
+ originalRAF(startBackgroundAnimation);
return;
}
animationInitialScrollPosition.current = scrollRef.current?.scrollTop || 0;
animationScrollPositionDiff.current = (scrollRef.current?.scrollTop || 0)
- ((containerRef.current?.offsetTop || 0) - ((scrollRef.current?.offsetHeight || 0) / 2));
animationInitialTimestamp.current = timestamp;
- requestAnimationFrame(animateScrollPosition);
+ originalRAF(animateScrollPosition);
};
const startBackgroundAnimation = (timestamp: number) => {
animationInitialTimestamp.current = timestamp;
animationInitialBgColor.current = containerRef.current?.style.backgroundColor ?? '';
- requestAnimationFrame(animateBackgroundColor);
+ originalRAF(animateBackgroundColor);
};
const animateScrollPosition = (timestamp: number) => {
@@ -280,10 +281,10 @@ const ChatMessage = React.forwardRef(({
if (!scrollContainer || !messageContainer) return;
if (value <= 1) {
scrollContainer.scrollTop = initialPosition - (value * diff);
- requestAnimationFrame(animateScrollPosition);
+ originalRAF(animateScrollPosition);
} else {
scrollContainer.scrollTop = initialPosition - diff;
- requestAnimationFrame(startBackgroundAnimation);
+ originalRAF(startBackgroundAnimation);
}
};
@@ -292,7 +293,7 @@ const ChatMessage = React.forwardRef(({
const value = (timestamp - animationInitialTimestamp.current) / ANIMATION_DURATION;
if (value < 1) {
chatMessageContentWrapperRef.current.style.backgroundColor = `rgb(${colorBlueLighterChannel} / ${1 - value})`;
- requestAnimationFrame(animateBackgroundColor);
+ originalRAF(animateBackgroundColor);
} else {
chatMessageContentWrapperRef.current.style.backgroundColor = animationInitialBgColor.current;
}
@@ -323,7 +324,7 @@ const ChatMessage = React.forwardRef(({
},
) => {
if (currentFrame < stabilityFrames) {
- const frameId = requestAnimationFrame(() => {
+ const frameId = originalRAF(() => {
pollScrollEndEvent(setFrameId, {
stabilityFrames,
currentFrame: currentFrame + 1,
@@ -340,10 +341,10 @@ const ChatMessage = React.forwardRef(({
const startScrollEndEventPolling = useCallback(() => {
if (scrollEndFrameRef.current != null) {
- cancelAnimationFrame(scrollEndFrameRef.current);
+ originalCAF(scrollEndFrameRef.current);
scrollEndFrameRef.current = undefined;
}
- scrollEndFrameRef.current = requestAnimationFrame(() => {
+ scrollEndFrameRef.current = originalRAF(() => {
pollScrollEndEvent((frameId) => {
scrollEndFrameRef.current = frameId;
});
@@ -364,7 +365,7 @@ const ChatMessage = React.forwardRef(({
return () => {
scrollRef?.current?.removeEventListener('scroll', callbackFunction);
if (scrollEndFrameRef.current !== undefined) {
- cancelAnimationFrame(scrollEndFrameRef.current);
+ originalCAF(scrollEndFrameRef.current);
scrollEndFrameRef.current = undefined;
}
};
diff --git a/bigbluebutton-html5/imports/ui/components/common/fullscreen-button/service.js b/bigbluebutton-html5/imports/ui/components/common/fullscreen-button/service.js
index 550df065a10c..964139b8a894 100644
--- a/bigbluebutton-html5/imports/ui/components/common/fullscreen-button/service.js
+++ b/bigbluebutton-html5/imports/ui/components/common/fullscreen-button/service.js
@@ -1,25 +1,25 @@
-function getFullscreenElement() {
- if (document.fullscreenElement) return document.fullscreenElement;
- if (document.webkitFullscreenElement) return document.webkitFullscreenElement;
- if (document.mozFullScreenElement) return document.mozFullScreenElement;
- if (document.msFullscreenElement) return document.msFullscreenElement;
+function getFullscreenElement(d = document) {
+ if (d.fullscreenElement) return d.fullscreenElement;
+ if (d.webkitFullscreenElement) return d.webkitFullscreenElement;
+ if (d.mozFullScreenElement) return d.mozFullScreenElement;
+ if (d.msFullscreenElement) return d.msFullscreenElement;
return null;
}
-const isFullScreen = (element) => {
- if (getFullscreenElement() && getFullscreenElement() === element) {
+const isFullScreen = (element, doc = document) => {
+ if (getFullscreenElement(doc) && getFullscreenElement(doc) === element) {
return true;
}
return false;
};
-function cancelFullScreen() {
- if (document.exitFullscreen) {
- document.exitFullscreen();
- } else if (document.mozCancelFullScreen) {
- document.mozCancelFullScreen();
- } else if (document.webkitExitFullscreen) {
- document.webkitExitFullscreen();
+function cancelFullScreen(doc = document) {
+ if (doc.exitFullscreen) {
+ doc.exitFullscreen();
+ } else if (doc.mozCancelFullScreen) {
+ doc.mozCancelFullScreen();
+ } else if (doc.webkitExitFullscreen) {
+ doc.webkitExitFullscreen();
}
}
@@ -39,13 +39,20 @@ function fullscreenRequest(element) {
element.focus();
}
-const toggleFullScreen = (ref = null) => {
- const element = ref || document.documentElement;
-
- if (isFullScreen(element)) {
- cancelFullScreen();
+const toggleFullScreen = (ref = null, isDetached = false, p) => {
+ const element = isDetached ? p.document.documentElement : (ref || document.documentElement);
+ if (isDetached) {
+ if (isFullScreen(element, p.document)) {
+ cancelFullScreen(p.document);
+ } else {
+ fullscreenRequest(element);
+ }
} else {
- fullscreenRequest(element);
+ if (isFullScreen(element)) {
+ cancelFullScreen();
+ } else {
+ fullscreenRequest(element);
+ }
}
};
diff --git a/bigbluebutton-html5/imports/ui/components/emoji-rain/component.jsx b/bigbluebutton-html5/imports/ui/components/emoji-rain/component.jsx
index 605c648796f0..b2d08f92e3f6 100644
--- a/bigbluebutton-html5/imports/ui/components/emoji-rain/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/emoji-rain/component.jsx
@@ -2,6 +2,7 @@ import React, { useRef, useState, useEffect } from 'react';
import { getSettingsSingletonInstance } from '/imports/ui/services/settings';
import Service from './service';
import logger from '/imports/startup/client/logger';
+//import { originalRAF } from '/imports/utils/animationFrameBackup';
const EmojiRain = ({ reactions }) => {
const Settings = getSettingsSingletonInstance();
@@ -57,6 +58,8 @@ const EmojiRain = ({ reactions }) => {
}
requestAnimationFrame(() => setTimeout(() => flyingEmojis.forEach((emoji) => {
+ // No effect observed (emoji rain works without using originalRAF). So removed.
+ //originalRAF(() => setTimeout(() => flyingEmojis.forEach((emoji) => {
const { shapeElement, endPosition } = emoji;
shapeElement.style.left = `${endPosition.x}px`;
shapeElement.style.top = `${endPosition.y}px`;
diff --git a/bigbluebutton-html5/imports/ui/components/layout/layout-manager/customLayout.jsx b/bigbluebutton-html5/imports/ui/components/layout/layout-manager/customLayout.jsx
index 91cc6a564f56..72a23a287f71 100644
--- a/bigbluebutton-html5/imports/ui/components/layout/layout-manager/customLayout.jsx
+++ b/bigbluebutton-html5/imports/ui/components/layout/layout-manager/customLayout.jsx
@@ -461,10 +461,13 @@ const CustomLayout = (props) => {
}
if (
- fullscreenElement === 'Presentation' ||
- fullscreenElement === 'Screenshare' ||
- fullscreenElement === 'ExternalVideo' ||
- fullscreenElement === 'GenericContent'
+ (fullscreenElement === 'Presentation' ||
+ fullscreenElement === 'Screenshare' ||
+ fullscreenElement === 'ExternalVideo' ||
+ fullscreenElement === 'GenericContent') &&
+ // this is indispensable for showing a normal-sized operatable external video
+ // when popup is fullscreen within the sub-monitor
+ document.getElementById('presentationInnerWrapper')
) {
mediaBounds.width = windowWidth();
mediaBounds.height = windowHeight();
@@ -803,4 +806,4 @@ const CustomLayout = (props) => {
return null;
};
-export default CustomLayout;
\ No newline at end of file
+export default CustomLayout;
diff --git a/bigbluebutton-html5/imports/ui/components/presentation/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/component.jsx
index 9337a7c6145e..bdbb1fcbf7f0 100755
--- a/bigbluebutton-html5/imports/ui/components/presentation/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/presentation/component.jsx
@@ -1,4 +1,5 @@
import React, { PureComponent } from 'react';
+import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import WhiteboardContainer from '/imports/ui/components/whiteboard/container';
import { HUNDRED_PERCENT, MAX_PERCENT, MIN_PERCENT } from '/imports/utils/slideCalcUtils';
@@ -6,6 +7,7 @@ import { SPACE } from '/imports/utils/keyCodes';
import { defineMessages, injectIntl } from 'react-intl';
import Session from '/imports/ui/services/storage/in-memory';
import PresentationToolbarContainer from './presentation-toolbar/container';
+import PresentationMenuContainer from './presentation-menu/container';
import PresentationMenu from './presentation-menu/container';
import DownloadPresentationButton from './download-presentation-button/component';
import Styled from './styles';
@@ -18,9 +20,11 @@ import browserInfo from '/imports/utils/browserInfo';
import { addAlert } from '../screenreader-alert/service';
import { debounce } from '/imports/utils/debounce';
import { throttle } from '/imports/utils/throttle';
+import { originalRAF, originalCAF } from '/imports/utils/animationFrameBackup';
import LocatedErrorBoundary from '/imports/ui/components/common/error-boundary/located-error-boundary/component';
import FallbackView from '/imports/ui/components/common/fallback-errors/fallback-view/component';
import TooltipContainer from '/imports/ui/components/common/tooltip/container';
+import { StyleSheetManager } from 'styled-components';
const intlMessages = defineMessages({
presentationLabel: {
@@ -47,6 +51,26 @@ const intlMessages = defineMessages({
id: 'app.presentation.emptySlideContent',
description: 'No content available for slide',
},
+ currentSlide: {
+ id: 'app.presentation.currentSlide',
+ description: 'Label for current slide',
+ },
+ nextSlide: {
+ id: 'app.presentation.nextSlide',
+ description: 'Label for next slide',
+ },
+ noCurrentSlide: {
+ id: 'app.presentation.noCurrentSlide',
+ description: 'Message shown when current slide is unavailable',
+ },
+ noNextSlide: {
+ id: 'app.presentation.noNextSlide',
+ description: 'Message shown when next slide is unavailable',
+ },
+ noNotes: {
+ id: 'app.presentation.noNotes',
+ description: 'Message shown when slide notes are unavailable',
+ },
presentationHeader: {
id: 'playback.player.presentation.wrapper.aria',
description: 'Aria label for header navigation',
@@ -58,9 +82,9 @@ const FULLSCREEN_CHANGE_EVENT = isSafari
? 'webkitfullscreenchange'
: 'fullscreenchange';
-const getToolbarHeight = () => {
+const getToolbarHeight = (doc = document) => {
let height = 0;
- const toolbarEl = document.getElementById('presentationToolbarWrapper');
+ const toolbarEl = doc.getElementById('presentationToolbarWrapper');
if (toolbarEl) {
const { clientHeight } = toolbarEl;
height = clientHeight;
@@ -68,6 +92,101 @@ const getToolbarHeight = () => {
return height;
};
+const getStyleSheetCssText = (style) => {
+ try {
+ const rules = Array.from(style.sheet?.cssRules ?? []);
+
+ if (rules.length > 0) {
+ return rules.map((rule) => rule.cssText).join('\n');
+ }
+ } catch (error) {
+ console.warn(
+ 'Failed to read a DarkReader CSS rule for the presentation popup',
+ error,
+ );
+ }
+
+ return style.textContent || '';
+};
+
+const syncDarkReaderStylesToPopup = (sourceDocument, targetDocument) => {
+ // Remove the previous DarkReader styles from the popup.
+ targetDocument
+ .querySelectorAll(
+ 'style.darkreader, style[data-popup-darkreader-mirror]',
+ )
+ .forEach((style) => style.remove());
+
+ // Synchronize DarkReader attributes on .
+ Array.from(targetDocument.documentElement.attributes)
+ .filter((attr) => attr.name.startsWith('data-darkreader'))
+ .forEach((attr) => {
+ targetDocument.documentElement.removeAttribute(attr.name);
+ });
+
+ Array.from(sourceDocument.documentElement.attributes)
+ .filter((attr) => attr.name.startsWith('data-darkreader'))
+ .forEach((attr) => {
+ targetDocument.documentElement.setAttribute(
+ attr.name,
+ attr.value,
+ );
+ });
+
+ const darkReaderStyles = Array.from(
+ sourceDocument.querySelectorAll('style.darkreader'),
+ );
+
+ // DarkReader is disabled.
+ if (darkReaderStyles.length === 0) return;
+
+ const syncStyles = darkReaderStyles.filter(
+ (style) => style.classList.contains('darkreader--sync'),
+ );
+
+ const nonSyncStyles = darkReaderStyles.filter(
+ (style) => !style.classList.contains('darkreader--sync'),
+ );
+
+ // Copy the non-sync DarkReader styles.
+ nonSyncStyles.forEach((sourceStyle) => {
+ const targetStyle = targetDocument.createElement('style');
+
+ Array.from(sourceStyle.attributes).forEach((attr) => {
+ targetStyle.setAttribute(attr.name, attr.value);
+ });
+
+ targetStyle.textContent = getStyleSheetCssText(sourceStyle);
+
+ targetDocument.head.appendChild(targetStyle);
+ });
+
+ // Collect all darkreader--sync rules into one stylesheet.
+ const syncCssText = syncStyles
+ .flatMap((style) => {
+ try {
+ return Array.from(style.sheet?.cssRules ?? [])
+ .map((rule) => rule.cssText);
+ } catch (error) {
+ console.warn(
+ 'Failed to read a DarkReader sync rule for the presentation popup',
+ error,
+ );
+ return [];
+ }
+ })
+ .join('\n');
+
+ if (syncCssText === '') return;
+
+ const mirror = targetDocument.createElement('style');
+ mirror.setAttribute('data-popup-darkreader-mirror', '');
+ mirror.textContent = syncCssText;
+
+ // Keep DarkReader overrides at the end of .
+ targetDocument.head.appendChild(mirror);
+};
+
const IGNORE_PRESENTATION_RESTORATION_TIMEOUT = 5000;
class Presentation extends PureComponent {
@@ -85,6 +204,9 @@ class Presentation extends PureComponent {
isToolbarVisible: true,
hadPresentation: false,
ignorePresentationRestoring: true,
+ currentSlideNote: '',
+ presenterView: null,
+ presenterAnnotations: null,
};
const PAN_ZOOM_INTERV = window.meetingClientSettings.public.presentation.panZoomInterval || 200;
@@ -102,10 +224,16 @@ class Presentation extends PureComponent {
this.setIsToolbarVisible = this.setIsToolbarVisible.bind(this);
this.handlePanShortcut = this.handlePanShortcut.bind(this);
this.renderPresentationMenu = this.renderPresentationMenu.bind(this);
+ this.renderPresentationContents = this.renderPresentationContents.bind(this);
+ this.detachPresentation = this.detachPresentation.bind(this);
this.onResize = () => setTimeout(this.handleResize.bind(this), 0);
this.setPresentationRef = this.setPresentationRef.bind(this);
this.setTldrawIsMounting = this.setTldrawIsMounting.bind(this);
+ this.loadCurrentSlideNote = this.loadCurrentSlideNote.bind(this);
+ this.handlePresentationNotesUpdated = this.handlePresentationNotesUpdated.bind(this);
+ this.handlePresenterViewChange = this.handlePresenterViewChange.bind(this);
+ this.presenterAnnotationsObjectUrl = null;
Session.setItem('componentPresentationWillUnmount', false);
}
@@ -195,6 +323,13 @@ class Presentation extends PureComponent {
});
}
+ window.addEventListener(
+ 'presentationNotesUpdated',
+ this.handlePresentationNotesUpdated,
+ );
+
+ this.loadCurrentSlideNote();
+
setTimeout(() => {
this.setState({ ignorePresentationRestoring: false });
}, IGNORE_PRESENTATION_RESTORATION_TIMEOUT);
@@ -209,6 +344,7 @@ class Presentation extends PureComponent {
restoreOnUpdate,
layoutContextDispatch,
userIsPresenter,
+ isPresentationDetached,
presentationBounds,
numCameras,
intl,
@@ -232,7 +368,38 @@ class Presentation extends PureComponent {
presentationBounds: prevPresentationBounds,
} = prevProps;
- if (numCameras !== prevNumCameras) {
+ const presenterRoleLost = (
+ prevProps.userIsPresenter
+ && !userIsPresenter
+ );
+
+ const presenterToolClosed = (
+ prevProps.isPresentationDetached
+ && !isPresentationDetached
+ );
+
+ const presenterToolOpened = (
+ !prevProps.isPresentationDetached
+ && isPresentationDetached
+ );
+
+ const presenterAnnotationSlideChanged = (
+ prevProps.currentSlide?.id !== currentSlide?.id
+ || prevProps.currentPresentationId !== currentPresentationId
+ );
+
+ if (
+ presenterToolClosed
+ || presenterAnnotationSlideChanged
+ ) {
+ this.clearPresenterAnnotations();
+ }
+
+ if (presenterToolClosed) {
+ this.setState({ currentSlideNote: '' });
+ }
+
+ if (numCameras !== prevNumCameras && !isPresentationDetached) {
this.onResize();
}
@@ -316,23 +483,50 @@ class Presentation extends PureComponent {
});
}
+ if (presenterRoleLost && isPresentationDetached) {
+ this.detachPresentation();
+ }
+
if (
(zoom <= HUNDRED_PERCENT && isPanning && !fitToWidth)
- || (!userIsPresenter && prevProps.userIsPresenter)
+ || presenterRoleLost
) {
this.setIsPanning();
}
- if (!userIsPresenter && prevProps.userIsPresenter && fitToWidth) {
+ if (presenterRoleLost && fitToWidth) {
setPresentationFitToWidth(false);
}
+
+ const slideChanged = (
+ currentSlide?.svgUri !== prevProps.currentSlide?.svgUri
+ );
+
+ if (
+ presenterToolOpened
+ || (isPresentationDetached && slideChanged)
+ ) {
+ this.loadCurrentSlideNote();
+ }
}
componentWillUnmount() {
Session.setItem('componentPresentationWillUnmount', true);
- const { fullscreenContext, layoutContextDispatch } = this.props;
+ const {
+ fullscreenContext,
+ layoutContextDispatch,
+ isPresentationDetached,
+ popupWindow,
+ } = this.props;
+
+ const targetWin = isPresentationDetached && popupWindow ? popupWindow : window;
+ targetWin.removeEventListener('resize', this.onResize, false);
+
+ window.removeEventListener(
+ 'presentationNotesUpdated',
+ this.handlePresentationNotesUpdated,
+ );
- window.removeEventListener('resize', this.onResize, false);
if (this.refPresentationContainer) {
this.refPresentationContainer.removeEventListener(
FULLSCREEN_CHANGE_EVENT,
@@ -348,6 +542,11 @@ class Presentation extends PureComponent {
);
}
+ if (this.presenterAnnotationsObjectUrl) {
+ URL.revokeObjectURL(this.presenterAnnotationsObjectUrl);
+ this.presenterAnnotationsObjectUrl = null;
+ }
+
if (fullscreenContext) {
layoutContextDispatch({
type: ACTIONS.SET_FULLSCREEN_ELEMENT,
@@ -359,6 +558,306 @@ class Presentation extends PureComponent {
}
}
+ detachPresentation() {
+ const {
+ slidePosition,
+ isPresentationDetached,
+ popupWindow,
+ toggleDetachPresentation,
+ } = this.props;
+
+
+ if (!isPresentationDetached) {
+ // Quit fullscreen first when detach fullscreen presentation
+ // This will however keep the popup window size same as fullscreen.
+ if (window.document.fullscreenElement != null) {
+ this.onFullscreenChange();
+ }
+
+ const svgDimensions = this.calculateSize(slidePosition);
+ const toolbarHeight = getToolbarHeight();
+ const popup = window.open('', '_blank',
+ `innerwidth=${svgDimensions.width},innerheight=${svgDimensions.height + toolbarHeight},resizable,scrollbars`);
+ if (!popup) return;
+ popup.document.title = 'BigBlueButton Portal Window';
+ // unnecessary div
+ //const container = popup.document.createElement('div');
+ //popup.document.body.appendChild(container);
+
+ // Copying the attributes of , so that the bbb-icons font looks a bit smaller
+ const mainHtml = document.documentElement; // メインウィンドウの
+ const popupHtml = popup.document.documentElement;
+ // class
+ popupHtml.className = mainHtml.className;
+ // style, which includes font-size: 14px
+ popupHtml.style.cssText = mainHtml.style.cssText;
+
+ //// dir
+ //if (mainHtml.hasAttribute('dir')) {
+ // popupHtml.setAttribute('dir', mainHtml.getAttribute('dir'));
+ //} else {
+ // popupHtml.removeAttribute('dir');
+ //}
+ //// lang
+ //if (mainHtml.hasAttribute('lang')) {
+ // popupHtml.setAttribute('lang', mainHtml.getAttribute('lang'));
+ //} else {
+ // popupHtml.removeAttribute('lang');
+ //}
+ // Copy all attributes ( extensions including DarkReader, which may not work anyway)
+ for (const attr of mainHtml.attributes) {
+ popupHtml.setAttribute(attr.name, attr.value);
+ }
+
+ // headの中身をコピー
+ const headElements = document.head.cloneNode(true).childNodes;
+ headElements.forEach((node) => {
+ // script要素など重複実行したくないものを除外する
+ if (node.nodeName !== 'SCRIPT') {
+ popup.document.head.appendChild(node.cloneNode(true));
+ }
+ });
+
+ // Firefox specific configuration
+ const isFirefox = navigator.userAgent.toLowerCase().includes('firefox');
+ if (isFirefox) {
+ // Add base URL (perhaps only necessary for Firefox to show tldraw icons
+ const base = popup.document.createElement('base');
+ base.href = window.location.origin + '/';
+ popup.document.head.appendChild(base);
+
+ // Explicitely copy bbb-icons.css to show bbb-icons
+ fetch('stylesheets/bbb-icons.css')
+ .then(res => res.text())
+ .then(css => {
+ const style = popup.document.createElement('style');
+ style.textContent = css;
+ popup.document.head.appendChild(style);
+ });
+ // Explicitly set FontFace to show bbb-icons
+ const fonts = [
+ { name: 'bbb-icons', url: '/html5client/fonts/BbbIcons/bbb-icons.woff2' },
+ ];
+ fonts.forEach(({ name, url }) => {
+ const font = new FontFace(name, `url(${window.location.origin}${url})`);
+ font.load().then(loaded => popup.document.fonts.add(loaded));
+ });
+ }
+
+ // 追加: document.styleSheets からすべての stylesheet を popup に複製
+ //Array.from(document.styleSheets).forEach((styleSheet) => {
+ // try {
+ // if (styleSheet.href) {
+ // // 形式
+ // const link = popup.document.createElement('link');
+ // link.rel = 'stylesheet';
+ // link.href = styleSheet.href;
+ // popup.document.head.appendChild(link);
+ // } else if (styleSheet.cssRules) {
+ // //