f909c317cd
fix: viewing shared screen in full screen mode will break the interface when screen share closes
913 lines
27 KiB
JavaScript
Executable File
913 lines
27 KiB
JavaScript
Executable File
import React, { PureComponent } from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import WhiteboardOverlayContainer from '/imports/ui/components/whiteboard/whiteboard-overlay/container';
|
|
import WhiteboardToolbarContainer from '/imports/ui/components/whiteboard/whiteboard-toolbar/container';
|
|
import { HUNDRED_PERCENT, MAX_PERCENT } from '/imports/utils/slideCalcUtils';
|
|
import { defineMessages, injectIntl } from 'react-intl';
|
|
import { toast } from 'react-toastify';
|
|
import PresentationToolbarContainer from './presentation-toolbar/container';
|
|
import CursorWrapperContainer from './cursor/cursor-wrapper-container/container';
|
|
import AnnotationGroupContainer from '../whiteboard/annotation-group/container';
|
|
import PresentationOverlayContainer from './presentation-overlay/container';
|
|
import Slide from './slide/component';
|
|
import { styles } from './styles.scss';
|
|
import toastStyles from '/imports/ui/components/toast/styles';
|
|
import MediaService, { shouldEnableSwapLayout } from '../media/service';
|
|
import PresentationCloseButton from './presentation-close-button/component';
|
|
import DownloadPresentationButton from './download-presentation-button/component';
|
|
import FullscreenService from '../fullscreen-button/service';
|
|
import FullscreenButtonContainer from '../fullscreen-button/container';
|
|
import Icon from '/imports/ui/components/icon/component';
|
|
import PollingContainer from '/imports/ui/components/polling/container';
|
|
import { ACTIONS, LAYOUT_TYPE } from '../layout/enums';
|
|
import DEFAULT_VALUES from '../layout/defaultValues';
|
|
|
|
const intlMessages = defineMessages({
|
|
presentationLabel: {
|
|
id: 'app.presentationUploder.title',
|
|
description: 'presentation area element label',
|
|
},
|
|
changeNotification: {
|
|
id: 'app.presentation.notificationLabel',
|
|
description: 'label displayed in toast when presentation switches',
|
|
},
|
|
downloadLabel: {
|
|
id: 'app.presentation.downloadLabel',
|
|
description: 'label for downloadable presentations',
|
|
},
|
|
slideContentStart: {
|
|
id: 'app.presentation.startSlideContent',
|
|
description: 'Indicate the slide content start',
|
|
},
|
|
slideContentEnd: {
|
|
id: 'app.presentation.endSlideContent',
|
|
description: 'Indicate the slide content end',
|
|
},
|
|
noSlideContent: {
|
|
id: 'app.presentation.emptySlideContent',
|
|
description: 'No content available for slide',
|
|
},
|
|
});
|
|
|
|
const ALLOW_FULLSCREEN = Meteor.settings.public.app.allowFullscreen;
|
|
|
|
class Presentation extends PureComponent {
|
|
constructor() {
|
|
super();
|
|
|
|
this.state = {
|
|
presentationWidth: 0,
|
|
presentationHeight: 0,
|
|
showSlide: false,
|
|
zoom: 100,
|
|
fitToWidth: false,
|
|
isFullscreen: false,
|
|
};
|
|
|
|
this.currentPresentationToastId = null;
|
|
|
|
this.getSvgRef = this.getSvgRef.bind(this);
|
|
this.setFitToWidth = this.setFitToWidth.bind(this);
|
|
this.zoomChanger = this.zoomChanger.bind(this);
|
|
this.updateLocalPosition = this.updateLocalPosition.bind(this);
|
|
this.panAndZoomChanger = this.panAndZoomChanger.bind(this);
|
|
this.fitToWidthHandler = this.fitToWidthHandler.bind(this);
|
|
this.onFullscreenChange = this.onFullscreenChange.bind(this);
|
|
this.getPresentationSizesAvailable = this.getPresentationSizesAvailable.bind(this);
|
|
this.handleResize = this.handleResize.bind(this);
|
|
|
|
this.onResize = () => setTimeout(this.handleResize.bind(this), 0);
|
|
this.renderCurrentPresentationToast = this.renderCurrentPresentationToast.bind(this);
|
|
}
|
|
|
|
static getDerivedStateFromProps(props, state) {
|
|
const { prevProps } = state;
|
|
const stateChange = { prevProps: props };
|
|
|
|
if (props.userIsPresenter
|
|
&& (!prevProps || !prevProps.userIsPresenter)
|
|
&& props.currentSlide
|
|
&& props.slidePosition) {
|
|
let potentialZoom = 100 / (props.slidePosition.viewBoxWidth / props.slidePosition.width);
|
|
potentialZoom = Math.max(HUNDRED_PERCENT, Math.min(MAX_PERCENT, potentialZoom));
|
|
stateChange.zoom = potentialZoom;
|
|
}
|
|
|
|
if (!prevProps) return stateChange;
|
|
|
|
// When presenter is changed or slide changed we reset localPosition
|
|
if (prevProps.currentSlide.id !== props.currentSlide.id
|
|
|| prevProps.userIsPresenter !== props.userIsPresenter) {
|
|
stateChange.localPosition = undefined;
|
|
}
|
|
|
|
return stateChange;
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.getInitialPresentationSizes();
|
|
this.refPresentationContainer.addEventListener('fullscreenchange', this.onFullscreenChange);
|
|
window.addEventListener('resize', this.onResize, false);
|
|
|
|
const {
|
|
currentSlide, slidePosition, layoutContextDispatch,
|
|
} = this.props;
|
|
|
|
layoutContextDispatch({
|
|
type: ACTIONS.SET_PRESENTATION_NUM_CURRENT_SLIDE,
|
|
value: currentSlide.num,
|
|
});
|
|
layoutContextDispatch({
|
|
type: ACTIONS.SET_PRESENTATION_CURRENT_SLIDE_SIZE,
|
|
value: {
|
|
width: slidePosition.width,
|
|
height: slidePosition.height,
|
|
},
|
|
});
|
|
}
|
|
|
|
componentDidUpdate(prevProps) {
|
|
const {
|
|
currentPresentation,
|
|
slidePosition,
|
|
layoutSwapped,
|
|
currentSlide,
|
|
publishedPoll,
|
|
isViewer,
|
|
toggleSwapLayout,
|
|
restoreOnUpdate,
|
|
layoutContextDispatch,
|
|
userIsPresenter,
|
|
presentationBounds,
|
|
numCameras,
|
|
} = this.props;
|
|
|
|
const {
|
|
numCameras: prevNumCameras,
|
|
presentationBounds: prevPresentationBounds,
|
|
} = prevProps;
|
|
|
|
if (numCameras !== prevNumCameras) {
|
|
this.onResize();
|
|
}
|
|
|
|
if (prevProps?.slidePosition && slidePosition) {
|
|
const { width: prevWidth, height: prevHeight } = prevProps.slidePosition;
|
|
const { width: currWidth, height: currHeight } = slidePosition;
|
|
|
|
if (prevWidth !== currWidth || prevHeight !== currHeight) {
|
|
layoutContextDispatch({
|
|
type: ACTIONS.SET_PRESENTATION_CURRENT_SLIDE_SIZE,
|
|
value: {
|
|
width: currWidth,
|
|
height: currHeight,
|
|
},
|
|
});
|
|
}
|
|
|
|
const downloadableOn = !prevProps.currentPresentation.downloadable
|
|
&& currentPresentation.downloadable;
|
|
|
|
const shouldCloseToast = !(currentPresentation.downloadable && !userIsPresenter);
|
|
|
|
if (
|
|
prevProps.currentPresentation.name !== currentPresentation.name
|
|
|| (downloadableOn && !userIsPresenter)
|
|
) {
|
|
if (this.currentPresentationToastId) {
|
|
toast.update(this.currentPresentationToastId, {
|
|
autoClose: shouldCloseToast,
|
|
render: this.renderCurrentPresentationToast(),
|
|
});
|
|
} else {
|
|
this.currentPresentationToastId = toast(this.renderCurrentPresentationToast(), {
|
|
onClose: () => { this.currentPresentationToastId = null; },
|
|
autoClose: shouldCloseToast,
|
|
className: toastStyles.actionToast,
|
|
});
|
|
}
|
|
}
|
|
|
|
const downloadableOff = prevProps.currentPresentation.downloadable
|
|
&& !currentPresentation.downloadable;
|
|
|
|
if (this.currentPresentationToastId && downloadableOff) {
|
|
toast.update(this.currentPresentationToastId, {
|
|
autoClose: true,
|
|
render: this.renderCurrentPresentationToast(),
|
|
});
|
|
}
|
|
|
|
if (layoutSwapped && restoreOnUpdate && isViewer && currentSlide) {
|
|
const slideChanged = currentSlide.id !== prevProps.currentSlide.id;
|
|
const positionChanged = slidePosition
|
|
.viewBoxHeight !== prevProps.slidePosition.viewBoxHeight
|
|
|| slidePosition.viewBoxWidth !== prevProps.slidePosition.viewBoxWidth;
|
|
const pollPublished = publishedPoll && !prevProps.publishedPoll;
|
|
if (slideChanged || positionChanged || pollPublished) {
|
|
toggleSwapLayout(layoutContextDispatch);
|
|
}
|
|
}
|
|
|
|
if (presentationBounds !== prevPresentationBounds) this.onResize();
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
const { fullscreenContext, layoutContextDispatch } = this.props;
|
|
|
|
window.removeEventListener('resize', this.onResize, false);
|
|
this.refPresentationContainer.removeEventListener('fullscreenchange', this.onFullscreenChange);
|
|
|
|
if (fullscreenContext) {
|
|
layoutContextDispatch({
|
|
type: ACTIONS.SET_FULLSCREEN_ELEMENT,
|
|
value: {
|
|
element: '',
|
|
group: '',
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
handleResize() {
|
|
const presentationSizes = this.getPresentationSizesAvailable();
|
|
if (Object.keys(presentationSizes).length > 0) {
|
|
// updating the size of the space available for the slide
|
|
this.setState({
|
|
presentationHeight: presentationSizes.presentationHeight,
|
|
presentationWidth: presentationSizes.presentationWidth,
|
|
});
|
|
}
|
|
}
|
|
|
|
onFullscreenChange() {
|
|
const { isFullscreen } = this.state;
|
|
const newIsFullscreen = FullscreenService.isFullScreen(this.refPresentationContainer);
|
|
if (isFullscreen !== newIsFullscreen) {
|
|
this.setState({ isFullscreen: newIsFullscreen });
|
|
}
|
|
}
|
|
|
|
// returns a ref to the svg element, which is required by a WhiteboardOverlay
|
|
// to transform screen coordinates to svg coordinate system
|
|
getSvgRef() {
|
|
return this.svggroup;
|
|
}
|
|
|
|
getToolbarHeight() {
|
|
const { refPresentationToolbar } = this;
|
|
let height = 0;
|
|
if (refPresentationToolbar) {
|
|
const { clientHeight } = refPresentationToolbar;
|
|
height = clientHeight;
|
|
}
|
|
return height;
|
|
}
|
|
|
|
getPresentationSizesAvailable() {
|
|
const {
|
|
presentationBounds,
|
|
presentationAreaSize: newPresentationAreaSize,
|
|
} = this.props;
|
|
const presentationSizes = {
|
|
presentationWidth: 0,
|
|
presentationHeight: 0,
|
|
};
|
|
|
|
if (newPresentationAreaSize) {
|
|
presentationSizes.presentationWidth = newPresentationAreaSize.presentationAreaWidth;
|
|
presentationSizes.presentationHeight = newPresentationAreaSize
|
|
.presentationAreaHeight - (this.getToolbarHeight() || 0);
|
|
return presentationSizes;
|
|
}
|
|
|
|
presentationSizes.presentationWidth = presentationBounds.width;
|
|
presentationSizes.presentationHeight = presentationBounds.height;
|
|
return presentationSizes;
|
|
}
|
|
|
|
getInitialPresentationSizes() {
|
|
// determining the presentationWidth and presentationHeight (available
|
|
// space for the svg) on the initial load
|
|
|
|
const presentationSizes = this.getPresentationSizesAvailable();
|
|
if (Object.keys(presentationSizes).length > 0) {
|
|
// setting the state of the available space for the svg
|
|
// and set the showSlide to true to start rendering the slide
|
|
this.setState({
|
|
presentationHeight: presentationSizes.presentationHeight,
|
|
presentationWidth: presentationSizes.presentationWidth,
|
|
showSlide: true,
|
|
});
|
|
}
|
|
}
|
|
|
|
setFitToWidth(fitToWidth) {
|
|
this.setState({ fitToWidth });
|
|
}
|
|
|
|
calculateSize(viewBoxDimensions) {
|
|
const {
|
|
presentationHeight,
|
|
presentationWidth,
|
|
fitToWidth,
|
|
} = this.state;
|
|
|
|
const {
|
|
userIsPresenter,
|
|
currentSlide,
|
|
slidePosition,
|
|
} = this.props;
|
|
|
|
if (!currentSlide || !slidePosition) {
|
|
return { width: 0, height: 0 };
|
|
}
|
|
|
|
const originalWidth = slidePosition.width;
|
|
const originalHeight = slidePosition.height;
|
|
const viewBoxWidth = viewBoxDimensions.width;
|
|
const viewBoxHeight = viewBoxDimensions.height;
|
|
|
|
let svgWidth;
|
|
let svgHeight;
|
|
|
|
if (!userIsPresenter) {
|
|
svgWidth = (presentationHeight * viewBoxWidth) / viewBoxHeight;
|
|
if (presentationWidth < svgWidth) {
|
|
svgHeight = (presentationHeight * presentationWidth) / svgWidth;
|
|
svgWidth = presentationWidth;
|
|
} else {
|
|
svgHeight = presentationHeight;
|
|
}
|
|
} else if (!fitToWidth) {
|
|
svgWidth = (presentationHeight * originalWidth) / originalHeight;
|
|
if (presentationWidth < svgWidth) {
|
|
svgHeight = (presentationHeight * presentationWidth) / svgWidth;
|
|
svgWidth = presentationWidth;
|
|
} else {
|
|
svgHeight = presentationHeight;
|
|
}
|
|
} else {
|
|
svgWidth = presentationWidth;
|
|
svgHeight = (svgWidth * originalHeight) / originalWidth;
|
|
if (svgHeight > presentationHeight) svgHeight = presentationHeight;
|
|
}
|
|
|
|
if (typeof svgHeight !== 'number' || typeof svgWidth !== 'number') {
|
|
return { width: 0, height: 0 };
|
|
}
|
|
|
|
return {
|
|
width: svgWidth,
|
|
height: svgHeight,
|
|
};
|
|
}
|
|
|
|
zoomChanger(incomingZoom) {
|
|
const {
|
|
zoom,
|
|
} = this.state;
|
|
|
|
let newZoom = incomingZoom;
|
|
|
|
if (newZoom <= HUNDRED_PERCENT) {
|
|
newZoom = HUNDRED_PERCENT;
|
|
} else if (incomingZoom >= MAX_PERCENT) {
|
|
newZoom = MAX_PERCENT;
|
|
}
|
|
|
|
if (newZoom !== zoom) this.setState({ zoom: newZoom });
|
|
}
|
|
|
|
fitToWidthHandler() {
|
|
const {
|
|
fitToWidth,
|
|
} = this.state;
|
|
|
|
this.setState({
|
|
fitToWidth: !fitToWidth,
|
|
zoom: HUNDRED_PERCENT,
|
|
});
|
|
}
|
|
|
|
isPresentationAccessible() {
|
|
const {
|
|
currentSlide,
|
|
slidePosition,
|
|
} = this.props;
|
|
// sometimes tomcat publishes the slide url, but the actual file is not accessible
|
|
return currentSlide && slidePosition;
|
|
}
|
|
|
|
updateLocalPosition(x, y, width, height, zoom) {
|
|
this.setState({
|
|
localPosition: {
|
|
x, y, width, height,
|
|
},
|
|
zoom,
|
|
});
|
|
}
|
|
|
|
panAndZoomChanger(w, h, x, y) {
|
|
const {
|
|
currentSlide,
|
|
podId,
|
|
zoomSlide,
|
|
} = this.props;
|
|
|
|
zoomSlide(currentSlide.num, podId, w, h, x, y);
|
|
}
|
|
|
|
renderPresentationClose() {
|
|
const { isFullscreen } = this.state;
|
|
const {
|
|
layoutType,
|
|
fullscreenContext,
|
|
layoutContextDispatch,
|
|
} = this.props;
|
|
|
|
if (!shouldEnableSwapLayout()
|
|
|| isFullscreen
|
|
|| fullscreenContext
|
|
|| layoutType === LAYOUT_TYPE.PRESENTATION_FOCUS) {
|
|
return null;
|
|
}
|
|
return (
|
|
<PresentationCloseButton
|
|
toggleSwapLayout={MediaService.toggleSwapLayout}
|
|
layoutContextDispatch={layoutContextDispatch}
|
|
/>
|
|
);
|
|
}
|
|
|
|
renderOverlays(slideObj, svgDimensions, viewBoxPosition, viewBoxDimensions, physicalDimensions) {
|
|
const {
|
|
userIsPresenter,
|
|
multiUser,
|
|
podId,
|
|
currentSlide,
|
|
slidePosition,
|
|
} = this.props;
|
|
|
|
const {
|
|
zoom,
|
|
fitToWidth,
|
|
} = this.state;
|
|
|
|
if (!userIsPresenter && !multiUser) {
|
|
return null;
|
|
}
|
|
|
|
// retrieving the pre-calculated data from the slide object
|
|
const {
|
|
width,
|
|
height,
|
|
} = slidePosition;
|
|
|
|
return (
|
|
<PresentationOverlayContainer
|
|
podId={podId}
|
|
userIsPresenter={userIsPresenter}
|
|
currentSlideNum={currentSlide.num}
|
|
slide={slideObj}
|
|
slideWidth={width}
|
|
slideHeight={height}
|
|
viewBoxX={viewBoxPosition.x}
|
|
viewBoxY={viewBoxPosition.y}
|
|
viewBoxWidth={viewBoxDimensions.width}
|
|
viewBoxHeight={viewBoxDimensions.height}
|
|
physicalSlideWidth={physicalDimensions.width}
|
|
physicalSlideHeight={physicalDimensions.height}
|
|
svgWidth={svgDimensions.width}
|
|
svgHeight={svgDimensions.height}
|
|
zoom={zoom}
|
|
zoomChanger={this.zoomChanger}
|
|
updateLocalPosition={this.updateLocalPosition}
|
|
panAndZoomChanger={this.panAndZoomChanger}
|
|
getSvgRef={this.getSvgRef}
|
|
fitToWidth={fitToWidth}
|
|
>
|
|
<WhiteboardOverlayContainer
|
|
getSvgRef={this.getSvgRef}
|
|
userIsPresenter={userIsPresenter}
|
|
whiteboardId={slideObj.id}
|
|
slide={slideObj}
|
|
slideWidth={width}
|
|
slideHeight={height}
|
|
viewBoxX={viewBoxPosition.x}
|
|
viewBoxY={viewBoxPosition.y}
|
|
viewBoxWidth={viewBoxDimensions.width}
|
|
viewBoxHeight={viewBoxDimensions.height}
|
|
physicalSlideWidth={physicalDimensions.width}
|
|
physicalSlideHeight={physicalDimensions.height}
|
|
zoom={zoom}
|
|
zoomChanger={this.zoomChanger}
|
|
/>
|
|
</PresentationOverlayContainer>
|
|
);
|
|
}
|
|
|
|
// renders the whole presentation area
|
|
renderPresentation(svgDimensions, viewBoxDimensions) {
|
|
const {
|
|
intl,
|
|
podId,
|
|
currentSlide,
|
|
slidePosition,
|
|
userIsPresenter,
|
|
layoutSwapped,
|
|
} = this.props;
|
|
|
|
const {
|
|
localPosition,
|
|
} = this.state;
|
|
|
|
if (!this.isPresentationAccessible()) {
|
|
return null;
|
|
}
|
|
|
|
// retrieving the pre-calculated data from the slide object
|
|
const {
|
|
width,
|
|
height,
|
|
} = slidePosition;
|
|
|
|
const {
|
|
imageUri,
|
|
content,
|
|
} = currentSlide;
|
|
|
|
let viewBoxPosition;
|
|
|
|
if (userIsPresenter && localPosition) {
|
|
viewBoxPosition = {
|
|
x: localPosition.x,
|
|
y: localPosition.y,
|
|
};
|
|
} else {
|
|
viewBoxPosition = {
|
|
x: slidePosition.x,
|
|
y: slidePosition.y,
|
|
};
|
|
}
|
|
|
|
const widthRatio = viewBoxDimensions.width / width;
|
|
const heightRatio = viewBoxDimensions.height / height;
|
|
|
|
const physicalDimensions = {
|
|
width: (svgDimensions.width / widthRatio),
|
|
height: (svgDimensions.height / heightRatio),
|
|
};
|
|
|
|
const svgViewBox = `${viewBoxPosition.x} ${viewBoxPosition.y} `
|
|
+ `${viewBoxDimensions.width} ${Number.isNaN(viewBoxDimensions.height) ? 0 : viewBoxDimensions.height}`;
|
|
|
|
const slideContent = content ? `${intl.formatMessage(intlMessages.slideContentStart)}
|
|
${content}
|
|
${intl.formatMessage(intlMessages.slideContentEnd)}` : intl.formatMessage(intlMessages.noSlideContent);
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
width: svgDimensions.width < 0 ? 0 : svgDimensions.width,
|
|
height: svgDimensions.height < 0 ? 0 : svgDimensions.height,
|
|
textAlign: 'center',
|
|
display: layoutSwapped ? 'none' : 'block',
|
|
}}
|
|
>
|
|
<span id="currentSlideText" className={styles.visuallyHidden}>{slideContent}</span>
|
|
{this.renderPresentationClose()}
|
|
{this.renderPresentationDownload()}
|
|
{this.renderPresentationFullscreen()}
|
|
<svg
|
|
key={currentSlide.id}
|
|
data-test="whiteboard"
|
|
width={svgDimensions.width < 0 ? 0 : svgDimensions.width}
|
|
height={svgDimensions.height < 0 ? 0 : svgDimensions.height}
|
|
ref={(ref) => { if (ref != null) { this.svggroup = ref; } }}
|
|
viewBox={svgViewBox}
|
|
version="1.1"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
className={styles.svgStyles}
|
|
>
|
|
<defs>
|
|
<clipPath id="viewBox">
|
|
<rect x={viewBoxPosition.x} y={viewBoxPosition.y} width="100%" height="100%" fill="none" />
|
|
</clipPath>
|
|
</defs>
|
|
<g clipPath="url(#viewBox)">
|
|
<Slide
|
|
imageUri={imageUri}
|
|
svgWidth={width}
|
|
svgHeight={height}
|
|
/>
|
|
<AnnotationGroupContainer
|
|
{...{
|
|
width,
|
|
height,
|
|
}}
|
|
published
|
|
whiteboardId={currentSlide.id}
|
|
/>
|
|
<AnnotationGroupContainer
|
|
{...{
|
|
width,
|
|
height,
|
|
}}
|
|
published={false}
|
|
whiteboardId={currentSlide.id}
|
|
/>
|
|
<CursorWrapperContainer
|
|
podId={podId}
|
|
whiteboardId={currentSlide.id}
|
|
widthRatio={widthRatio}
|
|
physicalWidthRatio={svgDimensions.width / width}
|
|
slideWidth={width}
|
|
slideHeight={height}
|
|
/>
|
|
</g>
|
|
{this.renderOverlays(
|
|
currentSlide,
|
|
svgDimensions,
|
|
viewBoxPosition,
|
|
viewBoxDimensions,
|
|
physicalDimensions,
|
|
)}
|
|
</svg>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
renderPresentationToolbar(svgWidth) {
|
|
const {
|
|
currentSlide,
|
|
podId,
|
|
fullscreenElementId,
|
|
isMobile,
|
|
layoutType,
|
|
numCameras,
|
|
} = this.props;
|
|
const { zoom, fitToWidth, isFullscreen } = this.state;
|
|
|
|
if (!currentSlide) return null;
|
|
|
|
const { presentationToolbarMinWidth } = DEFAULT_VALUES;
|
|
|
|
const toolbarWidth = ((this.refWhiteboardArea && svgWidth > presentationToolbarMinWidth)
|
|
|| isMobile
|
|
|| (layoutType === LAYOUT_TYPE.VIDEO_FOCUS && numCameras > 0))
|
|
? svgWidth
|
|
: presentationToolbarMinWidth;
|
|
return (
|
|
<PresentationToolbarContainer
|
|
{...{
|
|
fitToWidth,
|
|
zoom,
|
|
podId,
|
|
currentSlide,
|
|
fullscreenElementId,
|
|
toolbarWidth,
|
|
}}
|
|
isFullscreen={isFullscreen}
|
|
fullscreenRef={this.refPresentationContainer}
|
|
currentSlideNum={currentSlide.num}
|
|
presentationId={currentSlide.presentationId}
|
|
zoomChanger={this.zoomChanger}
|
|
fitToWidthHandler={this.fitToWidthHandler}
|
|
/>
|
|
);
|
|
}
|
|
|
|
renderWhiteboardToolbar(svgDimensions) {
|
|
const { currentSlide } = this.props;
|
|
if (!this.isPresentationAccessible()) return null;
|
|
|
|
return (
|
|
<WhiteboardToolbarContainer
|
|
whiteboardId={currentSlide.id}
|
|
height={svgDimensions.height}
|
|
/>
|
|
);
|
|
}
|
|
|
|
renderPresentationDownload() {
|
|
const { presentationIsDownloadable, downloadPresentationUri } = this.props;
|
|
|
|
if (!presentationIsDownloadable) return null;
|
|
|
|
const handleDownloadPresentation = () => {
|
|
window.open(downloadPresentationUri);
|
|
};
|
|
|
|
return (
|
|
<DownloadPresentationButton
|
|
handleDownloadPresentation={handleDownloadPresentation}
|
|
dark
|
|
/>
|
|
);
|
|
}
|
|
|
|
renderPresentationFullscreen() {
|
|
const {
|
|
intl,
|
|
userIsPresenter,
|
|
fullscreenElementId,
|
|
} = this.props;
|
|
const { isFullscreen } = this.state;
|
|
|
|
if (userIsPresenter || !ALLOW_FULLSCREEN) return null;
|
|
|
|
return (
|
|
<FullscreenButtonContainer
|
|
fullscreenRef={this.refPresentationContainer}
|
|
elementName={intl.formatMessage(intlMessages.presentationLabel)}
|
|
elementId={fullscreenElementId}
|
|
isFullscreen={isFullscreen}
|
|
dark
|
|
bottom
|
|
/>
|
|
);
|
|
}
|
|
|
|
renderCurrentPresentationToast() {
|
|
const {
|
|
intl, currentPresentation, userIsPresenter, downloadPresentationUri,
|
|
} = this.props;
|
|
const { downloadable } = currentPresentation;
|
|
|
|
return (
|
|
<div className={styles.innerToastWrapper}>
|
|
<div className={styles.toastIcon}>
|
|
<div className={styles.iconWrapper}>
|
|
<Icon iconName="presentation" />
|
|
</div>
|
|
</div>
|
|
|
|
<div className={styles.toastTextContent} data-test="toastSmallMsg">
|
|
<div>{`${intl.formatMessage(intlMessages.changeNotification)}`}</div>
|
|
<div className={styles.presentationName}>{`${currentPresentation.name}`}</div>
|
|
</div>
|
|
|
|
{downloadable && !userIsPresenter
|
|
? (
|
|
<span className={styles.toastDownload}>
|
|
<div className={toastStyles.separator} />
|
|
<a
|
|
className={styles.downloadBtn}
|
|
aria-label={`${intl.formatMessage(intlMessages.downloadLabel)} ${currentPresentation.name}`}
|
|
href={downloadPresentationUri}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
{intl.formatMessage(intlMessages.downloadLabel)}
|
|
</a>
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
userIsPresenter,
|
|
multiUser,
|
|
slidePosition,
|
|
presentationBounds,
|
|
fullscreenContext,
|
|
isMobile,
|
|
layoutType,
|
|
numCameras,
|
|
} = this.props;
|
|
|
|
const {
|
|
showSlide,
|
|
// fitToWidth,
|
|
// presentationWidth,
|
|
isFullscreen,
|
|
localPosition,
|
|
} = this.state;
|
|
|
|
let viewBoxDimensions;
|
|
|
|
if (userIsPresenter && localPosition) {
|
|
viewBoxDimensions = {
|
|
width: localPosition.width,
|
|
height: localPosition.height,
|
|
};
|
|
} else if (slidePosition) {
|
|
viewBoxDimensions = {
|
|
width: slidePosition.viewBoxWidth,
|
|
height: slidePosition.viewBoxHeight,
|
|
};
|
|
} else {
|
|
viewBoxDimensions = {
|
|
width: 0,
|
|
height: 0,
|
|
};
|
|
}
|
|
|
|
const svgDimensions = this.calculateSize(viewBoxDimensions);
|
|
const svgHeight = svgDimensions.height;
|
|
const svgWidth = svgDimensions.width;
|
|
|
|
const toolbarHeight = this.getToolbarHeight();
|
|
|
|
const { presentationToolbarMinWidth } = DEFAULT_VALUES;
|
|
|
|
const isLargePresentation = (svgWidth > presentationToolbarMinWidth || isMobile)
|
|
&& !(layoutType === LAYOUT_TYPE.VIDEO_FOCUS && numCameras > 0);
|
|
|
|
const containerWidth = isLargePresentation
|
|
? svgWidth
|
|
: presentationToolbarMinWidth;
|
|
|
|
return (
|
|
<div
|
|
ref={(ref) => { this.refPresentationContainer = ref; }}
|
|
className={styles.presentationContainer}
|
|
style={{
|
|
top: presentationBounds.top,
|
|
left: presentationBounds.left,
|
|
right: presentationBounds.right,
|
|
width: presentationBounds.width,
|
|
height: presentationBounds.height,
|
|
zIndex: fullscreenContext ? presentationBounds.zIndex : undefined,
|
|
backgroundColor: '#06172A',
|
|
}}
|
|
>
|
|
{isFullscreen && <PollingContainer />}
|
|
|
|
<div
|
|
ref={(ref) => { this.refPresentation = ref; }}
|
|
className={styles.presentation}
|
|
>
|
|
<div
|
|
ref={(ref) => { this.refWhiteboardArea = ref; }}
|
|
className={styles.whiteboardSizeAvailable}
|
|
/>
|
|
<div
|
|
className={styles.svgContainer}
|
|
style={{
|
|
height: svgHeight + toolbarHeight,
|
|
}}
|
|
>
|
|
{showSlide && svgWidth > 0 && svgHeight > 0
|
|
? this.renderPresentation(svgDimensions, viewBoxDimensions)
|
|
: null}
|
|
{showSlide && (userIsPresenter || multiUser)
|
|
? this.renderWhiteboardToolbar(svgDimensions)
|
|
: null}
|
|
{showSlide && userIsPresenter && svgWidth > 0 && svgHeight > 0
|
|
? (
|
|
<div
|
|
className={styles.presentationToolbar}
|
|
ref={(ref) => { this.refPresentationToolbar = ref; }}
|
|
style={
|
|
{
|
|
width: containerWidth,
|
|
}
|
|
}
|
|
>
|
|
{this.renderPresentationToolbar(svgWidth)}
|
|
</div>
|
|
)
|
|
: null}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default injectIntl(Presentation);
|
|
|
|
Presentation.propTypes = {
|
|
podId: PropTypes.string.isRequired,
|
|
// Defines a boolean value to detect whether a current user is a presenter
|
|
userIsPresenter: PropTypes.bool.isRequired,
|
|
currentSlide: PropTypes.shape({
|
|
presentationId: PropTypes.string.isRequired,
|
|
current: PropTypes.bool.isRequired,
|
|
num: PropTypes.number.isRequired,
|
|
id: PropTypes.string.isRequired,
|
|
imageUri: PropTypes.string.isRequired,
|
|
}),
|
|
slidePosition: PropTypes.shape({
|
|
x: PropTypes.number.isRequired,
|
|
y: PropTypes.number.isRequired,
|
|
height: PropTypes.number.isRequired,
|
|
width: PropTypes.number.isRequired,
|
|
viewBoxWidth: PropTypes.number.isRequired,
|
|
viewBoxHeight: PropTypes.number.isRequired,
|
|
}),
|
|
// current multi-user status
|
|
multiUser: PropTypes.bool.isRequired,
|
|
};
|
|
|
|
Presentation.defaultProps = {
|
|
currentSlide: undefined,
|
|
slidePosition: undefined,
|
|
};
|