diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
index 853e37ab1409..eee0d6a4f3fc 100755
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
@@ -154,6 +154,7 @@ class ActionsBar extends PureComponent {
showScreenshareQuickSwapButton,
isReactionsButtonEnabled,
isRaiseHandEnabled,
+ isPresentationDetached,
} = this.props;
const Settings = getSettingsSingletonInstance();
@@ -251,6 +252,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 ff05f294b778..ec92e13c81ec 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
@@ -46,6 +46,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 5c652645d1c3..c1712198c291 100644
--- a/bigbluebutton-html5/imports/ui/components/app/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/app/component.jsx
@@ -126,12 +126,14 @@ class App extends Component {
};
this.timeOffsetInterval = null;
+ this.isPopupOnPreparation = false;
this.setPresentationFitToWidth = this.setPresentationFitToWidth.bind(this);
this.setAudioModalIsOpen = this.setAudioModalIsOpen.bind(this);
this.setVideoPreviewModalIsOpen = this.setVideoPreviewModalIsOpen.bind(this);
this.customPollShortcutHandler = this.customPollShortcutHandler.bind(this);
this.logJoin = this.logJoin.bind(this);
+ this.handlePopupPreparing = this.handlePopupPreparing.bind(this);
}
componentDidMount() {
@@ -274,6 +276,10 @@ class App extends Component {
}
}
+ handlePopupPreparing(b) {
+ this.isPopupOnPreparation = b;
+ }
+
renderDarkMode() {
const { darkTheme } = this.props;
@@ -284,6 +290,7 @@ class App extends Component {
const {
hideActionsBar,
presentationIsOpen,
+ isPresentationDetached,
} = this.props;
if (hideActionsBar) return null;
@@ -292,6 +299,7 @@ class App extends Component {
);
}
@@ -336,6 +344,9 @@ class App extends Component {
isNotificationEnabled,
isNonMediaLayout,
isRaiseHandEnabled,
+ popupWindow,
+ isPresentationDetached,
+ toggleDetachPresentation,
} = this.props;
const {
@@ -366,7 +377,7 @@ class App extends Component {
-
+
@@ -386,6 +397,10 @@ class App extends Component {
fitToWidth={presentationFitToWidth}
darkTheme={darkTheme}
presentationIsOpen={presentationIsOpen}
+ popupWindow={popupWindow}
+ isPresentationDetached={isPresentationDetached}
+ toggleDetachPresentation={toggleDetachPresentation}
+ onPopupPreparing={this.handlePopupPreparing}
/>
)
: null
diff --git a/bigbluebutton-html5/imports/ui/components/app/container.jsx b/bigbluebutton-html5/imports/ui/components/app/container.jsx
index f538a3d2439a..0950abc58e97 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';
@@ -106,9 +106,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 ?? '';
@@ -161,6 +176,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 4eaa1572cba4..6dc2fbed1c2e 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
@@ -40,6 +40,8 @@ import { ChatLoading } from '../component';
import Storage from '/imports/ui/services/storage/in-memory';
import browserInfo from '/imports/utils/browserInfo';
import deviceInfo from '/imports/utils/deviceInfo';
+import { originalHTMLElement } from '/imports/utils/HTMLElementBackup';
+import { originalRAF, originalCAF } from '/imports/utils/animationFrameBackup';
const PAGE_SIZE = 50;
const CLEANUP_TIMEOUT = 3000;
@@ -71,7 +73,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 => {
@@ -403,7 +405,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);
@@ -420,10 +422,10 @@ const ChatMessageList: React.FC = ({
initialTimestamp = timestamp;
initialPosition = scrollTop;
scrollPositionDiff = scrollHeight - offsetHeight - scrollTop;
- requestAnimationFrame(animateScrollPosition);
+ originalRAF(animateScrollPosition);
};
- requestAnimationFrame(startScrollAnimation);
+ originalRAF(startScrollAnimation);
}, []);
const renderUnreadNotification = useMemo(() => {
@@ -537,7 +539,7 @@ const ChatMessageList: React.FC = ({
},
) => {
if (currentFrame < stabilityFrames) {
- const frameId = requestAnimationFrame(() => {
+ const frameId = originalRAF(() => {
pollScrollEndEvent(setFrameId, onScrollEnd, {
stabilityFrames,
currentFrame: currentFrame + 1,
@@ -552,10 +554,10 @@ const ChatMessageList: React.FC = ({
const startScrollEndEventPolling = useCallback((onScrollEnd: () => void) => {
if (scrollEndFrameRef.current != null) {
- cancelAnimationFrame(scrollEndFrameRef.current);
+ originalCAF(scrollEndFrameRef.current);
scrollEndFrameRef.current = undefined;
}
- scrollEndFrameRef.current = requestAnimationFrame(() => {
+ scrollEndFrameRef.current = originalRAF(() => {
pollScrollEndEvent((frameId) => {
scrollEndFrameRef.current = frameId;
}, onScrollEnd);
@@ -608,7 +610,7 @@ const ChatMessageList: React.FC = ({
clearInterval(scrollActivityCheckInterval.current);
}
if (scrollEndFrameRef.current) {
- cancelAnimationFrame(scrollEndFrameRef.current);
+ originalCAF(scrollEndFrameRef.current);
}
}, []);
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 a02bec3b3e5a..0092fca7428b 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
@@ -47,6 +47,7 @@ import { EmojiPicker, EmojiPickerWrapper } from './message-toolbar/styles';
import { isMobile } from '/imports/utils/deviceInfo';
import { layoutSelect } from '/imports/ui/components/layout/context';
import { Layout } from '/imports/ui/components/layout/layoutTypes';
+import { originalRAF, originalCAF } from '/imports/utils/animationFrameBackup';
interface ChatMessageProps {
message: Message;
@@ -228,7 +229,7 @@ const ChatMessage = React.forwardRef(({
useImperativeHandle(ref, () => ({
requestFocus() {
setTimeout(() => {
- requestAnimationFrame(startScrollAnimation);
+ originalRAF(startScrollAnimation);
}, 0);
},
sequence: message.messageSequence,
@@ -236,20 +237,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) => {
@@ -261,10 +262,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);
}
};
@@ -273,7 +274,7 @@ const ChatMessage = React.forwardRef(({
const value = (timestamp - animationInitialTimestamp.current) / ANIMATION_DURATION;
if (value < 1) {
messageContentRef.current.style.backgroundColor = `rgb(${colorBlueLighterChannel} / ${1 - value})`;
- requestAnimationFrame(animateBackgroundColor);
+ originalRAF(animateBackgroundColor);
} else {
messageContentRef.current.style.backgroundColor = animationInitialBgColor.current;
}
@@ -304,7 +305,7 @@ const ChatMessage = React.forwardRef(({
},
) => {
if (currentFrame < stabilityFrames) {
- const frameId = requestAnimationFrame(() => {
+ const frameId = originalRAF(() => {
pollScrollEndEvent(setFrameId, {
stabilityFrames,
currentFrame: currentFrame + 1,
@@ -321,10 +322,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;
});
@@ -345,7 +346,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 34360f104cae..10cc75bded83 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/notifications-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/notifications-bar/container.jsx
index d4a710d75dc5..54d820ae91fb 100644
--- a/bigbluebutton-html5/imports/ui/components/notifications-bar/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/notifications-bar/container.jsx
@@ -40,7 +40,7 @@ const intlMessages = defineMessages({
const STATUS_CRITICAL = 'critical';
const COLOR_PRIMARY = 'primary';
-const NotificationsBarContainer = () => {
+const NotificationsBarContainer = ({ isPopupOnPreparation }) => {
const intl = useIntl();
const { hasNotification } = layoutSelectInput((i) => i.notificationsBar);
@@ -61,6 +61,14 @@ const NotificationsBarContainer = () => {
const errorMessage = useMemo(() => {
const isCritical = rttStatus === STATUS_CRITICAL;
+ // When subscriptionFailed error comes when the popup is under preparation,
+ // return null and clear the stacked subscriptionFaied error.
+ if (isPopupOnPreparation && subscriptionFailed) {
+ console.log("Warning: Ignoring an initial graphql subscription failure (3006) on popup.");
+ connectionStatus.getSubscriptionFailedVar()(false);
+ return null;
+ }
+
if (!connected) {
const code = isCritical ? 3002 : 3001;
const msg = intl.formatMessage(
@@ -137,6 +145,7 @@ const NotificationsBarContainer = () => {
rttStatus,
subscriptionFailed,
intl,
+ isPopupOnPreparation,
]);
const meetingMessage = useMemo(() => {
diff --git a/bigbluebutton-html5/imports/ui/components/presentation/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/component.jsx
index 1fbad7ddadc0..154b9950f1c7 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: {
@@ -58,9 +62,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;
@@ -102,6 +106,8 @@ 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);
@@ -330,9 +336,19 @@ class Presentation extends PureComponent {
componentWillUnmount() {
Session.setItem('componentPresentationWillUnmount', true);
- const { fullscreenContext, layoutContextDispatch } = this.props;
+ const {
+ fullscreenContext,
+ layoutContextDispatch,
+ isPresentationDetached,
+ popupWindow,
+ } = this.props;
- window.removeEventListener('resize', this.onResize, false);
+ if (isPresentationDetached) {
+ popupWindow.removeEventListener('resize', this.onResize, false);
+ } else {
+ window.removeEventListener('resize', this.onResize, false);
+ }
+
if (this.refPresentationContainer) {
this.refPresentationContainer.removeEventListener(
FULLSCREEN_CHANGE_EVENT,
@@ -359,6 +375,257 @@ class Presentation extends PureComponent {
}
}
+ detachPresentation() {
+ const {
+ slidePosition,
+ isPresentationDetached,
+ popupWindow,
+ toggleDetachPresentation,
+ onPopupPreparing,
+ } = 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';
+ const container = popup.document.createElement('div');
+ popup.document.body.appendChild(container);
+
+ // popup window is still in preparation, so some graphql subscription fails,
+ // which then will show the notification bar. We want to surpress it.
+ onPopupPreparing?.(true);
+
+ // 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('dir');
+ }
+
+ // 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) {
+ // //