Merge branch 'master' into record-indicator
This commit is contained in:
commit
1a369f0fc7
@ -7,6 +7,7 @@ import handleMeetingLocksChange from './handlers/meetingLockChange';
|
||||
import handleUserLockChange from './handlers/userLockChange';
|
||||
import handleRecordingStatusChange from './handlers/recordingStatusChange';
|
||||
import handleRecordingTimerChange from './handlers/recordingTimerChange';
|
||||
import handleTimeRemainingUpdate from './handlers/timeRemainingUpdate';
|
||||
import handleChangeWebcamOnlyModerator from './handlers/webcamOnlyModerator';
|
||||
|
||||
RedisPubSub.on('MeetingCreatedEvtMsg', handleMeetingCreation);
|
||||
@ -18,3 +19,4 @@ RedisPubSub.on('UserLockedInMeetingEvtMsg', handleUserLockChange);
|
||||
RedisPubSub.on('RecordingStatusChangedEvtMsg', handleRecordingStatusChange);
|
||||
RedisPubSub.on('UpdateRecordingTimerEvtMsg', handleRecordingTimerChange);
|
||||
RedisPubSub.on('WebcamsOnlyForModeratorChangedEvtMsg', handleChangeWebcamOnlyModerator);
|
||||
RedisPubSub.on('MeetingTimeRemainingUpdateEvtMsg', handleTimeRemainingUpdate);
|
||||
|
@ -3,6 +3,8 @@ import addMeeting from '../modifiers/addMeeting';
|
||||
|
||||
export default function handleMeetingCreation({ body }) {
|
||||
const meeting = body.props;
|
||||
const durationInSecods = (meeting.durationProps.duration * 60);
|
||||
meeting.durationProps.timeRemaining = durationInSecods;
|
||||
check(meeting, Object);
|
||||
|
||||
return addMeeting(meeting);
|
||||
|
@ -0,0 +1,30 @@
|
||||
import { check } from 'meteor/check';
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default function handleTimeRemainingUpdate({ body }, meetingId) {
|
||||
check(meetingId, String);
|
||||
|
||||
check(body, {
|
||||
timeLeftInSec: Number,
|
||||
});
|
||||
const { timeLeftInSec } = body;
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
};
|
||||
|
||||
const modifier = {
|
||||
$set: {
|
||||
'durationProps.timeRemaining': timeLeftInSec,
|
||||
},
|
||||
};
|
||||
|
||||
const cb = (err) => {
|
||||
if (err) {
|
||||
Logger.error(`Changing recording time: ${err}`);
|
||||
}
|
||||
};
|
||||
|
||||
return Meetings.upsert(selector, modifier, cb);
|
||||
}
|
@ -36,6 +36,7 @@ export default function addMeeting(meeting) {
|
||||
userInactivityInspectTimerInMinutes: Number,
|
||||
userInactivityThresholdInMinutes: Number,
|
||||
userActivitySignResponseDelayInMinutes: Number,
|
||||
timeRemaining: Number,
|
||||
},
|
||||
welcomeProp: {
|
||||
welcomeMsg: String,
|
||||
|
@ -58,10 +58,10 @@ export default function addUser(meetingId, user) {
|
||||
let userRole = user.role;
|
||||
|
||||
if (
|
||||
dummyUser &&
|
||||
dummyUser.clientType === 'HTML5' &&
|
||||
userRole === ROLE_MODERATOR &&
|
||||
!ALLOW_HTML5_MODERATOR
|
||||
dummyUser
|
||||
&& dummyUser.clientType === 'HTML5'
|
||||
&& userRole === ROLE_MODERATOR
|
||||
&& !ALLOW_HTML5_MODERATOR
|
||||
) {
|
||||
userRole = ROLE_VIEWER;
|
||||
}
|
||||
@ -110,6 +110,8 @@ export default function addUser(meetingId, user) {
|
||||
|
||||
if (userRole === ROLE_MODERATOR) {
|
||||
changeRole(ROLE_MODERATOR, true, userId, meetingId);
|
||||
} else {
|
||||
changeRole(ROLE_MODERATOR, false, userId, meetingId);
|
||||
}
|
||||
|
||||
if (Meeting.usersProp.guestPolicy === GUEST_ALWAYS_ACCEPT) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import injectNotify from '/imports/ui/components/toast/inject-notify/component';
|
||||
import humanizeSeconds from '/imports/utils/humanizeSeconds';
|
||||
import _ from 'lodash';
|
||||
import BreakoutRemainingTimeComponent from './component';
|
||||
@ -77,7 +78,14 @@ const startCounter = (sec, set, get, interval) => {
|
||||
};
|
||||
|
||||
|
||||
export default injectIntl(withTracker(({ breakoutRoom, intl, messageDuration }) => {
|
||||
export default injectNotify(injectIntl(withTracker(({
|
||||
breakoutRoom,
|
||||
intl,
|
||||
notify,
|
||||
messageDuration,
|
||||
timeEndedMessage,
|
||||
alertMessageUnderOneMinute,
|
||||
}) => {
|
||||
const data = {};
|
||||
if (breakoutRoom) {
|
||||
const roomRemainingTime = breakoutRoom.timeRemaining;
|
||||
@ -94,16 +102,17 @@ export default injectIntl(withTracker(({ breakoutRoom, intl, messageDuration })
|
||||
clearInterval(timeRemainingInterval);
|
||||
}
|
||||
|
||||
if (timeRemaining) {
|
||||
if (timeRemaining >= 0) {
|
||||
if (timeRemaining > 0) {
|
||||
const time = getTimeRemaining();
|
||||
if (time === (1 * 60) && alertMessageUnderOneMinute) notify(alertMessageUnderOneMinute, 'info', 'rooms');
|
||||
data.message = intl.formatMessage(messageDuration, { 0: humanizeSeconds(time) });
|
||||
} else {
|
||||
clearInterval(timeRemainingInterval);
|
||||
data.message = intl.formatMessage(intlMessages.breakoutWillClose);
|
||||
data.message = intl.formatMessage(timeEndedMessage || intlMessages.breakoutWillClose);
|
||||
}
|
||||
} else if (breakoutRoom) {
|
||||
data.message = intl.formatMessage(intlMessages.calculatingBreakoutTimeRemaining);
|
||||
}
|
||||
return data;
|
||||
})(breakoutRemainingTimeContainer));
|
||||
})(breakoutRemainingTimeContainer)));
|
||||
|
@ -12,6 +12,10 @@ const intlMessages = defineMessages({
|
||||
id: 'app.createBreakoutRoom.title',
|
||||
description: 'breakout title',
|
||||
},
|
||||
breakoutAriaTitle: {
|
||||
id: 'app.createBreakoutRoom.ariaTitle',
|
||||
description: 'breakout aria title',
|
||||
},
|
||||
breakoutDuration: {
|
||||
id: 'app.createBreakoutRoom.duration',
|
||||
description: 'breakout duration time',
|
||||
@ -119,7 +123,7 @@ class BreakoutRoom extends Component {
|
||||
this.setState({ joinedAudioOnly: false, breakoutId });
|
||||
}
|
||||
|
||||
renderUserActions(breakoutId) {
|
||||
renderUserActions(breakoutId, number) {
|
||||
const {
|
||||
isMicrophoneUser,
|
||||
isModerator,
|
||||
@ -145,6 +149,9 @@ class BreakoutRoom extends Component {
|
||||
label={generated && requestedBreakoutId === breakoutId
|
||||
? intl.formatMessage(intlMessages.generatedURL)
|
||||
: intl.formatMessage(intlMessages.breakoutJoin)}
|
||||
aria-label={generated && requestedBreakoutId === breakoutId
|
||||
? intl.formatMessage(intlMessages.generatedURL)
|
||||
: `${intl.formatMessage(intlMessages.breakoutJoin)} ${number}`}
|
||||
onClick={() => this.getBreakoutURL(breakoutId)}
|
||||
disabled={disable}
|
||||
className={styles.joinButton}
|
||||
@ -186,13 +193,13 @@ class BreakoutRoom extends Component {
|
||||
|
||||
const roomItems = breakoutRooms.map(item => (
|
||||
<div className={styles.content} key={`breakoutRoomList-${item.breakoutId}`}>
|
||||
<span>{intl.formatMessage(intlMessages.breakoutRoom, item.sequence.toString())}</span>
|
||||
<span aria-hidden>{intl.formatMessage(intlMessages.breakoutRoom, item.sequence.toString())}</span>
|
||||
{waiting && requestedBreakoutId === item.breakoutId ? (
|
||||
<span>
|
||||
{intl.formatMessage(intlMessages.generatingURL)}
|
||||
<span className={styles.connectingAnimation} />
|
||||
</span>
|
||||
) : this.renderUserActions(item.breakoutId)}
|
||||
) : this.renderUserActions(item.breakoutId, item.sequence.toString())}
|
||||
</div>
|
||||
));
|
||||
|
||||
@ -217,12 +224,13 @@ class BreakoutRoom extends Component {
|
||||
} = this.props;
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
<div className={styles.header} role="button" onClick={closeBreakoutPanel} >
|
||||
<span>
|
||||
<Icon iconName="left_arrow" />
|
||||
{intl.formatMessage(intlMessages.breakoutTitle)}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
icon="left_arrow"
|
||||
label={intl.formatMessage(intlMessages.breakoutTitle)}
|
||||
aria-label={intl.formatMessage(intlMessages.breakoutAriaTitle)}
|
||||
className={styles.header}
|
||||
onClick={closeBreakoutPanel}
|
||||
/>
|
||||
{this.renderBreakoutRooms()}
|
||||
{this.renderDuration()}
|
||||
{
|
||||
|
@ -23,8 +23,14 @@
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
margin-bottom: 2rem;
|
||||
cursor: pointer;
|
||||
margin-right: auto;
|
||||
padding-left: 0;
|
||||
|
||||
> i {
|
||||
color: var(--color-gray-dark);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
|
@ -15,11 +15,11 @@ const isUrlValid = (url) => {
|
||||
|
||||
const getUrlFromVideoId = id => (id ? `${YOUTUBE_PREFIX}${id}` : '');
|
||||
|
||||
// https://stackoverflow.com/questions/3452546/how-do-i-get-the-youtube-video-id-from-a-url
|
||||
const videoIdFromUrl = (url) => {
|
||||
const urlObj = new URL(url);
|
||||
const params = new URLSearchParams(urlObj.search);
|
||||
|
||||
return params.get('v');
|
||||
const regExp = /.*(?:youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=)([^#\&\?]*).*/;
|
||||
const match = url.match(regExp);
|
||||
return (match && match[1].length == 11) ? match[1] : false;
|
||||
};
|
||||
|
||||
const startWatching = (url) => {
|
||||
|
@ -294,7 +294,7 @@ class NavBar extends PureComponent {
|
||||
<div className={styles.center}>
|
||||
{this.renderPresentationTitle()}
|
||||
{recordProps.record
|
||||
? <span className={styles.presentationTitleSeparator}>|</span>
|
||||
? <span className={styles.presentationTitleSeparator} aria-hidden>|</span>
|
||||
: null}
|
||||
<RecordingIndicator
|
||||
{...recordProps}
|
||||
|
@ -88,7 +88,6 @@ class RecordingIndicator extends React.PureComponent {
|
||||
{recording
|
||||
? <span aria-hidden>{humanizeSeconds(time)}</span> : <span>{buttonTitle}</span>}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
) : null }
|
||||
|
||||
|
@ -9,7 +9,6 @@ const COLORS = [
|
||||
];
|
||||
|
||||
const propTypes = {
|
||||
children: PropTypes.string.isRequired,
|
||||
color: PropTypes.oneOf(COLORS),
|
||||
};
|
||||
|
||||
@ -18,14 +17,14 @@ const defaultProps = {
|
||||
};
|
||||
|
||||
const NotificationsBar = (props) => {
|
||||
const { color } = props;
|
||||
const { color, children } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
className={cx(styles.notificationsBar, styles[color])}
|
||||
>
|
||||
{props.children}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -4,8 +4,9 @@ import React from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import _ from 'lodash';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import humanizeSeconds from '/imports/utils/humanizeSeconds';
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import NavBarService from '../nav-bar/service';
|
||||
import BreakoutRemainingTime from '/imports/ui/components/breakout-room/breakout-remaining-time/container';
|
||||
|
||||
import NotificationsBar from './component';
|
||||
|
||||
@ -43,6 +44,22 @@ const intlMessages = defineMessages({
|
||||
id: 'app.calculatingBreakoutTimeRemaining',
|
||||
description: 'Message that tells that the remaining time is being calculated',
|
||||
},
|
||||
meetingTimeRemaining: {
|
||||
id: 'app.meeting.meetingTimeRemaining',
|
||||
description: 'Message that tells how much time is remaining for the meeting',
|
||||
},
|
||||
meetingWillClose: {
|
||||
id: 'app.meeting.meetingTimeHasEnded',
|
||||
description: 'Message that tells time has ended and meeting will close',
|
||||
},
|
||||
alertMeetingEndsUnderOneMinute: {
|
||||
id: 'app.meeting.alertMeetingEndsUnderOneMinute',
|
||||
description: 'Alert that tells that the meeting end under a minute',
|
||||
},
|
||||
alertBreakoutEndsUnderOneMinute: {
|
||||
id: 'app.meeting.alertBreakoutEndsUnderOneMinute',
|
||||
description: 'Alert that tells that the breakout end under a minute',
|
||||
},
|
||||
});
|
||||
|
||||
const NotificationsBarContainer = (props) => {
|
||||
@ -60,22 +77,14 @@ const NotificationsBarContainer = (props) => {
|
||||
};
|
||||
|
||||
let retrySeconds = 0;
|
||||
let timeRemaining = 0;
|
||||
const retrySecondsDep = new Tracker.Dependency();
|
||||
const timeRemainingDep = new Tracker.Dependency();
|
||||
let retryInterval = null;
|
||||
let timeRemainingInterval = null;
|
||||
|
||||
const getRetrySeconds = () => {
|
||||
retrySecondsDep.depend();
|
||||
return retrySeconds;
|
||||
};
|
||||
|
||||
const getTimeRemaining = () => {
|
||||
timeRemainingDep.depend();
|
||||
return timeRemaining;
|
||||
};
|
||||
|
||||
const setRetrySeconds = (sec = 0) => {
|
||||
if (sec !== retrySeconds) {
|
||||
retrySeconds = sec;
|
||||
@ -83,13 +92,6 @@ const setRetrySeconds = (sec = 0) => {
|
||||
}
|
||||
};
|
||||
|
||||
const setTimeRemaining = (sec = 0) => {
|
||||
if (sec !== timeRemaining) {
|
||||
timeRemaining = sec;
|
||||
timeRemainingDep.changed();
|
||||
}
|
||||
};
|
||||
|
||||
const startCounter = (sec, set, get, interval) => {
|
||||
clearInterval(interval);
|
||||
set(sec);
|
||||
@ -137,37 +139,38 @@ export default injectIntl(withTracker(({ intl }) => {
|
||||
const currentBreakout = breakouts.find(b => b.breakoutId === meetingId);
|
||||
|
||||
if (currentBreakout) {
|
||||
const roomRemainingTime = currentBreakout.timeRemaining;
|
||||
|
||||
if (!timeRemainingInterval && roomRemainingTime) {
|
||||
timeRemainingInterval = startCounter(
|
||||
roomRemainingTime,
|
||||
setTimeRemaining,
|
||||
getTimeRemaining,
|
||||
timeRemainingInterval,
|
||||
);
|
||||
}
|
||||
} else if (timeRemainingInterval) {
|
||||
clearInterval(timeRemainingInterval);
|
||||
}
|
||||
|
||||
timeRemaining = getTimeRemaining();
|
||||
|
||||
if (timeRemaining) {
|
||||
if (timeRemaining > 0) {
|
||||
data.message = intl.formatMessage(
|
||||
intlMessages.breakoutTimeRemaining,
|
||||
{ 0: humanizeSeconds(timeRemaining) },
|
||||
);
|
||||
} else {
|
||||
clearInterval(timeRemainingInterval);
|
||||
data.message = intl.formatMessage(intlMessages.breakoutWillClose);
|
||||
}
|
||||
} else if (currentBreakout) {
|
||||
data.message = intl.formatMessage(intlMessages.calculatingBreakoutTimeRemaining);
|
||||
data.message = (
|
||||
<BreakoutRemainingTime
|
||||
breakoutRoom={currentBreakout}
|
||||
messageDuration={intlMessages.breakoutTimeRemaining}
|
||||
timeEndedMessage={intlMessages.breakoutWillClose}
|
||||
alertMessageUnderOneMinute={intl.formatMessage(intlMessages.alertBreakoutEndsUnderOneMinute)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const Meeting = Meetings.findOne({ meetingId: Auth.meetingID });
|
||||
|
||||
if (Meeting) {
|
||||
const { timeRemaining } = Meeting.durationProps;
|
||||
const { isBreakout } = Meeting.meetingProp;
|
||||
const underThirtyMin = timeRemaining && timeRemaining <= (30 * 60);
|
||||
|
||||
if (underThirtyMin && !isBreakout) {
|
||||
data.message = (
|
||||
<BreakoutRemainingTime
|
||||
breakoutRoom={Meeting.durationProps}
|
||||
messageDuration={intlMessages.meetingTimeRemaining}
|
||||
timeEndedMessage={intlMessages.meetingWillClose}
|
||||
alertMessageUnderOneMinute={intl.formatMessage(intlMessages.alertMeetingEndsUnderOneMinute)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
data.color = 'primary';
|
||||
return data;
|
||||
})(NotificationsBarContainer));
|
||||
|
@ -242,6 +242,7 @@ class Poll extends Component {
|
||||
color="default"
|
||||
onClick={this.toggleCustomFields}
|
||||
label={intl.formatMessage(intlMessages.customPollLabel)}
|
||||
aria-expanded={customPollReq}
|
||||
/>
|
||||
{!customPollReq ? null : this.renderCustomView()}
|
||||
</div>
|
||||
|
@ -131,12 +131,12 @@ class LiveResult extends Component {
|
||||
)
|
||||
}
|
||||
<div className={styles.container}>
|
||||
<div className={styles.usersHeading}>
|
||||
<h3 className={styles.usersHeading}>
|
||||
{intl.formatMessage(intlMessages.usersTitle)}
|
||||
</div>
|
||||
<div className={styles.responseHeading}>
|
||||
</h3>
|
||||
<h3 className={styles.responseHeading}>
|
||||
{intl.formatMessage(intlMessages.responsesTitle)}
|
||||
</div>
|
||||
</h3>
|
||||
{userAnswers}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
||||
import { TransitionGroup, CSSTransition } from 'react-transition-group';
|
||||
import WhiteboardOverlayContainer from '/imports/ui/components/whiteboard/whiteboard-overlay/container';
|
||||
import WhiteboardToolbarContainer from '/imports/ui/components/whiteboard/whiteboard-toolbar/container';
|
||||
@ -10,11 +11,25 @@ import PresentationToolbarContainer from './presentation-toolbar/container';
|
||||
import PresentationOverlayContainer from './presentation-overlay/container';
|
||||
import Slide from './slide/component';
|
||||
import { styles } from './styles.scss';
|
||||
import MediaService from '../media/service';
|
||||
import MediaService, { shouldEnableSwapLayout } from '../media/service';
|
||||
import PresentationCloseButton from './presentation-close-button/component';
|
||||
import FullscreenButton from '../video-provider/fullscreen-button/component';
|
||||
|
||||
export default class PresentationArea extends Component {
|
||||
const intlMessages = defineMessages({
|
||||
presentationLabel: {
|
||||
id: 'app.presentationUploder.title',
|
||||
description: 'presentation area element label',
|
||||
},
|
||||
});
|
||||
|
||||
const isFullscreen = () => document.fullscreenElement !== null;
|
||||
|
||||
const renderPresentationClose = () => {
|
||||
if (!shouldEnableSwapLayout() || isFullscreen()) return null;
|
||||
return <PresentationCloseButton toggleSwapLayout={MediaService.toggleSwapLayout} />;
|
||||
};
|
||||
|
||||
class PresentationArea extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@ -64,6 +79,7 @@ export default class PresentationArea extends Component {
|
||||
const presentationSizes = {};
|
||||
|
||||
if (refPresentationArea && refWhiteboardArea) {
|
||||
const { userIsPresenter, multiUser } = this.props;
|
||||
// By default presentation sizes are equal to the sizes of the refPresentationArea
|
||||
// direct parent of the svg wrapper
|
||||
let { clientWidth, clientHeight } = refPresentationArea;
|
||||
@ -71,7 +87,7 @@ export default class PresentationArea extends Component {
|
||||
// if a user is a presenter - this means there is a whiteboard toolbar on the right
|
||||
// and we have to get the width/height of the refWhiteboardArea
|
||||
// (inner hidden div with absolute position)
|
||||
if (this.props.userIsPresenter || this.props.multiUser) {
|
||||
if (userIsPresenter || multiUser) {
|
||||
({ clientWidth, clientHeight } = refWhiteboardArea);
|
||||
}
|
||||
|
||||
@ -106,9 +122,10 @@ export default class PresentationArea extends Component {
|
||||
}
|
||||
|
||||
calculateSize() {
|
||||
const originalWidth = this.props.currentSlide.calculatedData.width;
|
||||
const originalHeight = this.props.currentSlide.calculatedData.height;
|
||||
const { presentationHeight, presentationWidth } = this.state;
|
||||
const { currentSlide } = this.props;
|
||||
const originalWidth = currentSlide.calculatedData.width;
|
||||
const originalHeight = currentSlide.calculatedData.height;
|
||||
|
||||
let adjustedWidth;
|
||||
let adjustedHeight;
|
||||
@ -139,13 +156,14 @@ export default class PresentationArea extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
zoomChanger(zoom) {
|
||||
let newZoom = zoom;
|
||||
const isDifferent = newZoom !== this.state.zoom;
|
||||
zoomChanger(incomingZoom) {
|
||||
const { zoom } = this.state;
|
||||
let newZoom = incomingZoom;
|
||||
const isDifferent = newZoom !== zoom;
|
||||
|
||||
if (newZoom <= HUNDRED_PERCENT) {
|
||||
newZoom = HUNDRED_PERCENT;
|
||||
} else if (zoom >= MAX_PERCENT) {
|
||||
} else if (incomingZoom >= MAX_PERCENT) {
|
||||
newZoom = MAX_PERCENT;
|
||||
}
|
||||
if (isDifferent) this.setState({ zoom: newZoom });
|
||||
@ -167,23 +185,29 @@ export default class PresentationArea extends Component {
|
||||
}
|
||||
|
||||
fitToWidthHandler() {
|
||||
this.setState({
|
||||
fitToWidth: !this.state.fitToWidth,
|
||||
});
|
||||
const { fitToWidth } = this.state;
|
||||
this.setState({ fitToWidth: !fitToWidth });
|
||||
}
|
||||
|
||||
isPresentationAccessible() {
|
||||
const { currentSlide } = this.props;
|
||||
// sometimes tomcat publishes the slide url, but the actual file is not accessible (why?)
|
||||
return currentSlide && currentSlide.calculatedData;
|
||||
}
|
||||
|
||||
// renders the whole presentation area
|
||||
renderPresentationArea() {
|
||||
if (!this.isPresentationAccessible()) {
|
||||
return null;
|
||||
}
|
||||
const { fitToWidth } = this.state;
|
||||
const { podId, currentSlide } = this.props;
|
||||
if (!this.isPresentationAccessible()) return null;
|
||||
|
||||
// to control the size of the svg wrapper manually
|
||||
// and adjust cursor's thickness, so that svg didn't scale it automatically
|
||||
const adjustedSizes = this.calculateSize();
|
||||
// a reference to the slide object
|
||||
const slideObj = this.props.currentSlide;
|
||||
const slideObj = currentSlide;
|
||||
|
||||
const presentationCloseButton = this.renderPresentationClose();
|
||||
const presentationCloseButton = renderPresentationClose();
|
||||
const presentationFullscreenButton = this.renderPresentationFullscreen();
|
||||
|
||||
// retrieving the pre-calculated data from the slide object
|
||||
@ -196,7 +220,7 @@ export default class PresentationArea extends Component {
|
||||
viewBoxHeight,
|
||||
imageUri,
|
||||
} = slideObj.calculatedData;
|
||||
const svgDimensions = this.state.fitToWidth ? {
|
||||
const svgDimensions = fitToWidth ? {
|
||||
position: 'absolute',
|
||||
width: 'inherit',
|
||||
} : {
|
||||
@ -226,8 +250,10 @@ export default class PresentationArea extends Component {
|
||||
>
|
||||
<svg
|
||||
data-test="whiteboard"
|
||||
width={width}
|
||||
height={height}
|
||||
{...{
|
||||
width,
|
||||
height,
|
||||
}}
|
||||
ref={(ref) => { if (ref != null) { this.svggroup = ref; } }}
|
||||
viewBox={`${x} ${y} ${viewBoxWidth} ${viewBoxHeight}`}
|
||||
version="1.1"
|
||||
@ -246,18 +272,20 @@ export default class PresentationArea extends Component {
|
||||
svgHeight={height}
|
||||
/>
|
||||
<AnnotationGroupContainer
|
||||
width={width}
|
||||
height={height}
|
||||
{...{
|
||||
width,
|
||||
height,
|
||||
}}
|
||||
whiteboardId={slideObj.id}
|
||||
/>
|
||||
<CursorWrapperContainer
|
||||
podId={this.props.podId}
|
||||
podId={podId}
|
||||
whiteboardId={slideObj.id}
|
||||
widthRatio={slideObj.widthRatio}
|
||||
physicalWidthRatio={adjustedSizes.width / width}
|
||||
slideWidth={width}
|
||||
slideHeight={height}
|
||||
radius={this.state.fitToWidth ? 2 : 5}
|
||||
radius={fitToWidth ? 2 : 5}
|
||||
/>
|
||||
</g>
|
||||
{this.renderOverlays(slideObj, adjustedSizes)}
|
||||
@ -269,9 +297,14 @@ export default class PresentationArea extends Component {
|
||||
}
|
||||
|
||||
renderOverlays(slideObj, adjustedSizes) {
|
||||
if (!this.props.userIsPresenter && !this.props.multiUser) {
|
||||
return null;
|
||||
}
|
||||
const {
|
||||
userIsPresenter, multiUser, podId, currentSlide,
|
||||
} = this.props;
|
||||
const {
|
||||
delta, zoom, touchZoom, fitToWidth,
|
||||
} = this.state;
|
||||
|
||||
if (!userIsPresenter && !multiUser) return null;
|
||||
|
||||
// retrieving the pre-calculated data from the slide object
|
||||
const {
|
||||
@ -285,24 +318,31 @@ export default class PresentationArea extends Component {
|
||||
|
||||
return (
|
||||
<PresentationOverlayContainer
|
||||
podId={this.props.podId}
|
||||
currentSlideNum={this.props.currentSlide.num}
|
||||
slide={slideObj}
|
||||
{...{
|
||||
podId,
|
||||
touchZoom,
|
||||
fitToWidth,
|
||||
zoom,
|
||||
delta,
|
||||
viewBoxWidth,
|
||||
viewBoxHeight,
|
||||
adjustedSizes,
|
||||
}}
|
||||
currentSlideNum={currentSlide.num}
|
||||
whiteboardId={slideObj.id}
|
||||
slide={slideObj}
|
||||
slideWidth={width}
|
||||
slideHeight={height}
|
||||
delta={this.state.delta}
|
||||
viewBoxWidth={viewBoxWidth}
|
||||
viewBoxHeight={viewBoxHeight}
|
||||
zoom={this.state.zoom}
|
||||
zoomChanger={this.zoomChanger}
|
||||
adjustedSizes={adjustedSizes}
|
||||
getSvgRef={this.getSvgRef}
|
||||
presentationSize={this.getPresentationSizesAvailable()}
|
||||
touchZoom={this.state.touchZoom}
|
||||
fitToWidth={this.state.fitToWidth}
|
||||
>
|
||||
<WhiteboardOverlayContainer
|
||||
{...{
|
||||
zoom,
|
||||
viewBoxWidth,
|
||||
viewBoxHeight,
|
||||
}}
|
||||
getSvgRef={this.getSvgRef}
|
||||
whiteboardId={slideObj.id}
|
||||
slideWidth={width}
|
||||
@ -310,11 +350,8 @@ export default class PresentationArea extends Component {
|
||||
viewBoxX={x}
|
||||
viewBoxY={y}
|
||||
pointChanger={this.pointUpdate}
|
||||
viewBoxWidth={viewBoxWidth}
|
||||
viewBoxHeight={viewBoxHeight}
|
||||
physicalSlideWidth={(adjustedSizes.width / slideObj.widthRatio) * 100}
|
||||
physicalSlideHeight={(adjustedSizes.height / slideObj.heightRatio) * 100}
|
||||
zoom={this.state.zoom}
|
||||
zoomChanger={this.zoomChanger}
|
||||
touchUpdate={this.touchUpdate}
|
||||
/>
|
||||
@ -322,42 +359,34 @@ export default class PresentationArea extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
isPresentationAccessible() {
|
||||
// sometimes tomcat publishes the slide url, but the actual file is not accessible (why?)
|
||||
return this.props.currentSlide && this.props.currentSlide.calculatedData;
|
||||
};
|
||||
|
||||
isFullscreen() {
|
||||
return document.fullscreenElement !== null;
|
||||
}
|
||||
|
||||
renderPresentationClose() {
|
||||
if (!MediaService.shouldEnableSwapLayout() || this.isFullscreen()) {
|
||||
return null;
|
||||
}
|
||||
return <PresentationCloseButton toggleSwapLayout={MediaService.toggleSwapLayout} />;
|
||||
};
|
||||
|
||||
renderPresentationFullscreen() {
|
||||
if (this.isFullscreen()) {
|
||||
return null;
|
||||
}
|
||||
const { intl } = this.props;
|
||||
if (isFullscreen()) return null;
|
||||
|
||||
const full = () => this.refPresentationContainer.requestFullscreen();
|
||||
|
||||
return <FullscreenButton handleFullscreen={full} dark />;
|
||||
return (
|
||||
<FullscreenButton
|
||||
handleFullscreen={full}
|
||||
elementName={intl.formatMessage(intlMessages.presentationLabel)}
|
||||
dark
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderPresentationToolbar() {
|
||||
if (!this.props.currentSlide) {
|
||||
return null;
|
||||
}
|
||||
const { zoom } = this.state;
|
||||
const { currentSlide, podId } = this.props;
|
||||
if (!currentSlide) return null;
|
||||
|
||||
return (
|
||||
<PresentationToolbarContainer
|
||||
podId={this.props.podId}
|
||||
currentSlideNum={this.props.currentSlide.num}
|
||||
presentationId={this.props.currentSlide.presentationId}
|
||||
zoom={this.state.zoom}
|
||||
{...{
|
||||
podId,
|
||||
zoom,
|
||||
}}
|
||||
currentSlideNum={currentSlide.num}
|
||||
presentationId={currentSlide.presentationId}
|
||||
zoomChanger={this.zoomChanger}
|
||||
fitToWidthHandler={this.fitToWidthHandler}
|
||||
/>
|
||||
@ -365,24 +394,27 @@ export default class PresentationArea extends Component {
|
||||
}
|
||||
|
||||
renderWhiteboardToolbar() {
|
||||
if (!this.isPresentationAccessible()) {
|
||||
return null;
|
||||
}
|
||||
const { currentSlide } = this.props;
|
||||
if (!this.isPresentationAccessible()) return null;
|
||||
|
||||
const adjustedSizes = this.calculateSize();
|
||||
return (
|
||||
<WhiteboardToolbarContainer
|
||||
whiteboardId={this.props.currentSlide.id}
|
||||
whiteboardId={currentSlide.id}
|
||||
height={adjustedSizes.height}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { showSlide } = this.state;
|
||||
const { userIsPresenter, multiUser } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={(ref) => { this.refPresentationContainer = ref; }}
|
||||
className={styles.presentationContainer}>
|
||||
className={styles.presentationContainer}
|
||||
>
|
||||
<div
|
||||
ref={(ref) => { this.refPresentationArea = ref; }}
|
||||
className={styles.presentationArea}
|
||||
@ -391,12 +423,8 @@ export default class PresentationArea extends Component {
|
||||
ref={(ref) => { this.refWhiteboardArea = ref; }}
|
||||
className={styles.whiteboardSizeAvailable}
|
||||
/>
|
||||
{this.state.showSlide
|
||||
? this.renderPresentationArea()
|
||||
: null }
|
||||
{this.props.userIsPresenter || this.props.multiUser
|
||||
? this.renderWhiteboardToolbar()
|
||||
: null }
|
||||
{showSlide ? this.renderPresentationArea() : null }
|
||||
{userIsPresenter || multiUser ? this.renderWhiteboardToolbar() : null }
|
||||
</div>
|
||||
{this.renderPresentationToolbar()}
|
||||
</div>
|
||||
@ -404,7 +432,10 @@ export default class PresentationArea extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(PresentationArea);
|
||||
|
||||
PresentationArea.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
podId: PropTypes.string.isRequired,
|
||||
// Defines a boolean value to detect whether a current user is a presenter
|
||||
userIsPresenter: PropTypes.bool.isRequired,
|
||||
|
@ -1,5 +1,13 @@
|
||||
@import "/imports/ui/stylesheets/variables/_all";
|
||||
|
||||
.visuallyhidden {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px; width: 1px;
|
||||
margin: -1px; padding: 0; border: 0;
|
||||
}
|
||||
|
||||
.presentationToolbarWrapper,
|
||||
.presentationControls,
|
||||
.zoomWrapper {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import { styles } from '../styles.scss';
|
||||
import HoldButton from './holdButton/component';
|
||||
@ -8,48 +8,30 @@ import HoldButton from './holdButton/component';
|
||||
const DELAY_MILLISECONDS = 200;
|
||||
const STEP_TIME = 100;
|
||||
|
||||
export default class ZoomTool extends Component {
|
||||
static renderAriaLabelsDescs() {
|
||||
return (
|
||||
<div hidden key="hidden-div">
|
||||
<div id="zoomInLabel">
|
||||
<FormattedMessage
|
||||
id="app.presentation.presentationToolbar.zoomInLabel"
|
||||
description="Aria label for increment zoom level"
|
||||
defaultMessage="Increment zoom"
|
||||
/>
|
||||
</div>
|
||||
<div id="zoomInDesc">
|
||||
<FormattedMessage
|
||||
id="app.presentation.presentationToolbar.zoomInDesc"
|
||||
description="Aria description for increment zoom level"
|
||||
defaultMessage="Increment zoom"
|
||||
/>
|
||||
</div>
|
||||
<div id="zoomOutLabel">
|
||||
<FormattedMessage
|
||||
id="app.presentation.presentationToolbar.zoomOutLabel"
|
||||
description="Aria label for decrement zoom level"
|
||||
defaultMessage="Decrement zoom"
|
||||
/>
|
||||
</div>
|
||||
<div id="zoomOutDesc">
|
||||
<FormattedMessage
|
||||
id="app.presentation.presentationToolbar.zoomOutDesc"
|
||||
description="Aria description for decrement zoom level"
|
||||
defaultMessage="Decrement zoom"
|
||||
/>
|
||||
</div>
|
||||
<div id="zoomIndicator">
|
||||
<FormattedMessage
|
||||
id="app.presentation.presentationToolbar.zoomIndicator"
|
||||
description="Aria label for current zoom level"
|
||||
defaultMessage="Current zoom level"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const intlMessages = defineMessages({
|
||||
zoomInLabel: {
|
||||
id: 'app.presentation.presentationToolbar.zoomInLabel',
|
||||
description: 'Aria label for increment zoom level',
|
||||
},
|
||||
zoomInDesc: {
|
||||
id: 'app.presentation.presentationToolbar.zoomInDesc',
|
||||
description: 'Aria description for increment zoom level',
|
||||
},
|
||||
zoomOutLabel: {
|
||||
id: 'app.presentation.presentationToolbar.zoomOutLabel',
|
||||
description: 'Aria label for decrement zoom level',
|
||||
},
|
||||
zoomOutDesc: {
|
||||
id: 'app.presentation.presentationToolbar.zoomOutDesc',
|
||||
description: 'Aria description for decrement zoom level',
|
||||
},
|
||||
zoomIndicator: {
|
||||
id: 'app.presentation.presentationToolbar.zoomIndicator',
|
||||
description: 'Aria label for current zoom level',
|
||||
},
|
||||
});
|
||||
|
||||
class ZoomTool extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.increment = this.increment.bind(this);
|
||||
@ -64,6 +46,7 @@ export default class ZoomTool extends Component {
|
||||
mouseHolding: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const isDifferent = this.props.value !== this.state.value;
|
||||
if (isDifferent) this.onChanger(this.props.value);
|
||||
@ -100,6 +83,7 @@ export default class ZoomTool extends Component {
|
||||
const increaseZoom = this.state.value + step;
|
||||
this.onChanger(increaseZoom);
|
||||
}
|
||||
|
||||
decrement() {
|
||||
const {
|
||||
step,
|
||||
@ -144,10 +128,10 @@ export default class ZoomTool extends Component {
|
||||
value,
|
||||
minBound,
|
||||
maxBound,
|
||||
intl,
|
||||
} = this.props;
|
||||
return (
|
||||
[
|
||||
ZoomTool.renderAriaLabelsDescs(),
|
||||
(
|
||||
<HoldButton
|
||||
key="zoom-tool-1"
|
||||
@ -157,10 +141,8 @@ export default class ZoomTool extends Component {
|
||||
>
|
||||
<Button
|
||||
key="zoom-tool-1"
|
||||
aria-labelledby="zoomOutLabel"
|
||||
aria-describedby="zoomOutDesc"
|
||||
role="button"
|
||||
label="-"
|
||||
aria-label={intl.formatMessage(intlMessages.zoomOutLabel)}
|
||||
label={intl.formatMessage(intlMessages.zoomOutLabel)}
|
||||
icon="minus"
|
||||
onClick={() => {}}
|
||||
disabled={(value <= minBound)}
|
||||
@ -172,11 +154,12 @@ export default class ZoomTool extends Component {
|
||||
(
|
||||
<span
|
||||
key="zoom-tool-2"
|
||||
aria-labelledby="zoomIndicator"
|
||||
aria-describedby={this.state.value}
|
||||
className={styles.zoomPercentageDisplay}
|
||||
>
|
||||
{`${this.state.value}%`}
|
||||
<span className={styles.visuallyhidden}>
|
||||
{`${intl.formatMessage(intlMessages.zoomIndicator)} ${this.state.value}%`}
|
||||
</span>
|
||||
<span aria-hidden>{`${this.state.value}%`}</span>
|
||||
</span>
|
||||
),
|
||||
(
|
||||
@ -188,10 +171,8 @@ export default class ZoomTool extends Component {
|
||||
>
|
||||
<Button
|
||||
key="zoom-tool-3"
|
||||
aria-labelledby="zoomInLabel"
|
||||
aria-describedby="zoomInDesc"
|
||||
role="button"
|
||||
label="+"
|
||||
aria-label={intl.formatMessage(intlMessages.zoomInLabel)}
|
||||
label={intl.formatMessage(intlMessages.zoomInLabel)}
|
||||
icon="plus"
|
||||
onClick={() => {}}
|
||||
disabled={(value >= maxBound)}
|
||||
@ -205,6 +186,8 @@ export default class ZoomTool extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(ZoomTool);
|
||||
|
||||
const propTypes = {
|
||||
value: PropTypes.number.isRequired,
|
||||
change: PropTypes.func.isRequired,
|
||||
|
@ -1,8 +1,17 @@
|
||||
import React from 'react';
|
||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import FullscreenButton from '../video-provider/fullscreen-button/component';
|
||||
import { styles } from './styles';
|
||||
|
||||
export default class ScreenshareComponent extends React.Component {
|
||||
const intlMessages = defineMessages({
|
||||
screenShareLabel: {
|
||||
id: 'app.screenshare.screenShareLabel',
|
||||
description: 'screen share area element label',
|
||||
},
|
||||
});
|
||||
|
||||
class ScreenshareComponent extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
@ -13,18 +22,21 @@ export default class ScreenshareComponent extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.presenterScreenshareHasStarted();
|
||||
const { presenterScreenshareHasStarted } = this.props;
|
||||
presenterScreenshareHasStarted();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.isPresenter && !nextProps.isPresenter) {
|
||||
this.props.unshareScreen();
|
||||
const { isPresenter, unshareScreen } = this.props;
|
||||
if (isPresenter && !nextProps.isPresenter) {
|
||||
unshareScreen();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.presenterScreenshareHasEnded();
|
||||
this.props.unshareScreen();
|
||||
const { presenterScreenshareHasEnded, unshareScreen } = this.props;
|
||||
presenterScreenshareHasEnded();
|
||||
unshareScreen();
|
||||
}
|
||||
|
||||
onVideoLoad() {
|
||||
@ -32,24 +44,29 @@ export default class ScreenshareComponent extends React.Component {
|
||||
}
|
||||
|
||||
renderFullscreenButton() {
|
||||
const { intl } = this.props;
|
||||
const full = () => {
|
||||
if (!this.videoTag) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.videoTag) return;
|
||||
this.videoTag.requestFullscreen();
|
||||
};
|
||||
return <FullscreenButton handleFullscreen={full} />;
|
||||
|
||||
return (
|
||||
<FullscreenButton
|
||||
handleFullscreen={full}
|
||||
elementName={intl.formatMessage(intlMessages.screenShareLabel)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loaded } = this.state;
|
||||
const style = {
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
};
|
||||
|
||||
return (
|
||||
[!this.state.loaded ? (<div key="screenshareArea" innerStyle={style} className={styles.connecting} />) : null,
|
||||
[!loaded ? (<div key="screenshareArea" innerStyle={style} className={styles.connecting} />) : null,
|
||||
this.renderFullscreenButton(),
|
||||
(
|
||||
<video
|
||||
@ -65,3 +82,13 @@ export default class ScreenshareComponent extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(ScreenshareComponent);
|
||||
|
||||
ScreenshareComponent.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
isPresenter: PropTypes.bool.isRequired,
|
||||
unshareScreen: PropTypes.func.isRequired,
|
||||
presenterScreenshareHasEnded: PropTypes.func.isRequired,
|
||||
presenterScreenshareHasStarted: PropTypes.func.isRequired,
|
||||
};
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import cx from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { styles } from './styles';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
@ -11,22 +12,43 @@ const intlMessages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
const FullscreenButtonComponent = ({ intl, handleFullscreen, dark }) => (
|
||||
<div className={cx(styles.wrapper, dark ? styles.dark : null)}>
|
||||
<Button
|
||||
role="button"
|
||||
aria-labelledby="fullscreenButtonLabel"
|
||||
aria-describedby="fullscreenButtonDesc"
|
||||
color="default"
|
||||
icon="fullscreen"
|
||||
size="sm"
|
||||
onClick={handleFullscreen}
|
||||
label={intl.formatMessage(intlMessages.fullscreenButton)}
|
||||
hideLabel
|
||||
circle
|
||||
className={styles.button}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
const propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
handleFullscreen: PropTypes.func.isRequired,
|
||||
dark: PropTypes.bool,
|
||||
elementName: PropTypes.string,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
dark: false,
|
||||
elementName: '',
|
||||
};
|
||||
|
||||
const FullscreenButtonComponent = ({
|
||||
intl, handleFullscreen, dark, elementName,
|
||||
}) => {
|
||||
const formattedLabel = intl.formatMessage(
|
||||
intlMessages.fullscreenButton,
|
||||
({ 0: elementName ? elementName.toLowerCase() : '' }),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx(styles.wrapper, dark ? styles.dark : null)}>
|
||||
<Button
|
||||
color="default"
|
||||
icon="fullscreen"
|
||||
size="sm"
|
||||
onClick={handleFullscreen}
|
||||
label={formattedLabel}
|
||||
hideLabel
|
||||
circle
|
||||
className={styles.button}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
FullscreenButtonComponent.propTypes = propTypes;
|
||||
FullscreenButtonComponent.defaultProps = defaultProps;
|
||||
|
||||
export default injectIntl(FullscreenButtonComponent);
|
||||
|
@ -10,7 +10,7 @@ import DropdownListTitle from '/imports/ui/components/dropdown/list/title/compon
|
||||
import DropdownListSeparator from '/imports/ui/components/dropdown/list/separator/component';
|
||||
import DropdownListItem from '/imports/ui/components/dropdown/list/item/component';
|
||||
import Icon from '/imports/ui/components/icon/component';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import VideoListItemStats from './video-list-item-stats/component';
|
||||
import FullscreenButton from '../../fullscreen-button/component';
|
||||
import { styles } from '../styles';
|
||||
@ -36,7 +36,8 @@ class VideoListItem extends Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.onMount(this.videoTag);
|
||||
const { onMount } = this.props;
|
||||
onMount(this.videoTag);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
@ -45,7 +46,12 @@ class VideoListItem extends Component {
|
||||
const p = elem.play();
|
||||
if (p && (typeof Promise !== 'undefined') && (p instanceof Promise)) {
|
||||
// Catch exception when playing video
|
||||
p.catch((e) => {});
|
||||
p.catch((e) => {
|
||||
logger.error(
|
||||
{ logCode: 'videolistitem_component_play_error' },
|
||||
`Error playing video: ${JSON.stringify(e)}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -58,20 +64,10 @@ class VideoListItem extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
toggleStats() {
|
||||
const { getStats, stopGettingStats } = this.props;
|
||||
if (this.state.showStats) {
|
||||
stopGettingStats();
|
||||
} else {
|
||||
getStats(this.videoTag, this.setStats);
|
||||
}
|
||||
|
||||
this.setState({ showStats: !this.state.showStats });
|
||||
}
|
||||
|
||||
setStats(updatedStats) {
|
||||
const { stats } = this.state;
|
||||
const { audio, video } = updatedStats;
|
||||
this.setState({ stats: { ...this.state.stats, video, audio } });
|
||||
this.setState({ stats: { ...stats, video, audio } });
|
||||
}
|
||||
|
||||
getAvailableActions() {
|
||||
@ -98,11 +94,23 @@ class VideoListItem extends Component {
|
||||
]);
|
||||
}
|
||||
|
||||
toggleStats() {
|
||||
const { getStats, stopGettingStats } = this.props;
|
||||
const { showStats } = this.state;
|
||||
if (showStats) {
|
||||
stopGettingStats();
|
||||
} else {
|
||||
getStats(this.videoTag, this.setStats);
|
||||
}
|
||||
this.setState({ showStats: !showStats });
|
||||
}
|
||||
|
||||
renderFullscreenButton() {
|
||||
const { user } = this.props;
|
||||
const full = () => {
|
||||
this.videoTag.requestFullscreen();
|
||||
};
|
||||
return <FullscreenButton handleFullscreen={full} />;
|
||||
return <FullscreenButton handleFullscreen={full} elementName={user.name} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import _ from 'lodash';
|
||||
import cx from 'classnames';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
||||
import Dropdown from '/imports/ui/components/dropdown/component';
|
||||
import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component';
|
||||
import DropdownContent from '/imports/ui/components/dropdown/content/component';
|
||||
import DropdownList from '/imports/ui/components/dropdown/list/component';
|
||||
import DropdownListItem from '/imports/ui/components/dropdown/list/item/component';
|
||||
import { styles } from './styles';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
videoMenu: {
|
||||
id: 'app.video.videoMenu',
|
||||
description: 'video menu label',
|
||||
},
|
||||
videoMenuDesc: {
|
||||
id: 'app.video.videoMenuDesc',
|
||||
description: 'video menu description',
|
||||
},
|
||||
videoMenuDisabled: {
|
||||
id: 'app.video.videoMenuDisabled',
|
||||
description: 'video menu label',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
isSharingVideo: PropTypes.bool.isRequired,
|
||||
videoItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
};
|
||||
|
||||
const JoinVideoOptions = ({
|
||||
intl,
|
||||
isSharingVideo,
|
||||
videoItems,
|
||||
videoShareAllowed,
|
||||
}) => {
|
||||
const menuItems = videoItems
|
||||
.filter(item => !item.disabled)
|
||||
.map(item => (
|
||||
<DropdownListItem
|
||||
key={_.uniqueId('video-menu-')}
|
||||
className={styles.item}
|
||||
description={item.description}
|
||||
onClick={item.click}
|
||||
tabIndex={-1}
|
||||
id={item.id}
|
||||
>
|
||||
<img src={item.iconPath} className={styles.imageSize} alt="video menu icon" aria-hidden />
|
||||
<span className={styles.label}>{item.label}</span>
|
||||
</DropdownListItem>
|
||||
));
|
||||
return (
|
||||
<Dropdown
|
||||
autoFocus
|
||||
>
|
||||
<DropdownTrigger tabIndex={0}>
|
||||
<Button
|
||||
label={!videoShareAllowed
|
||||
? intl.formatMessage(intlMessages.videoMenuDisabled)
|
||||
: intl.formatMessage(intlMessages.videoMenu)
|
||||
}
|
||||
className={cx(styles.button, isSharingVideo || styles.ghostButton)}
|
||||
onClick={() => null}
|
||||
hideLabel
|
||||
aria-label={intl.formatMessage(intlMessages.videoMenuDesc)}
|
||||
color={isSharingVideo ? 'primary' : 'default'}
|
||||
icon={isSharingVideo ? 'video' : 'video_off'}
|
||||
ghost={!isSharingVideo}
|
||||
size="lg"
|
||||
circle
|
||||
disabled={!videoShareAllowed}
|
||||
/>
|
||||
</DropdownTrigger>
|
||||
<DropdownContent placement="top">
|
||||
<DropdownList horizontal>
|
||||
{menuItems}
|
||||
</DropdownList>
|
||||
</DropdownContent>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
JoinVideoOptions.propTypes = propTypes;
|
||||
export default injectIntl(JoinVideoOptions);
|
@ -61,7 +61,11 @@
|
||||
"app.media.screenshare.end": "Screenshare has ended",
|
||||
"app.media.screenshare.safariNotSupported": "Screenshare is currently not supported by Safari. Please, use Firefox or Google Chrome.",
|
||||
"app.meeting.ended": "This session has ended",
|
||||
"app.meeting.meetingTimeRemaining": "Meeting time remaining: {0}",
|
||||
"app.meeting.meetingTimeHasEnded": "Time ended. Meeting will close soon",
|
||||
"app.meeting.endedMessage": "You will be forwarded back to the home screen",
|
||||
"app.meeting.alertMeetingEndsUnderOneMinute": "Meeting is closing in a minute.",
|
||||
"app.meeting.alertBreakoutEndsUnderOneMinute": "Breakout is closing in a minute.",
|
||||
"app.presentation.close": "Close presentation",
|
||||
"app.presentation.presentationToolbar.prevSlideLabel": "Previous slide",
|
||||
"app.presentation.presentationToolbar.prevSlideDesc": "Change the presentation to the previous slide",
|
||||
@ -79,7 +83,7 @@
|
||||
"app.presentation.presentationToolbar.zoomInDesc": "Zoom in the presentation",
|
||||
"app.presentation.presentationToolbar.zoomOutLabel": "Zoom out",
|
||||
"app.presentation.presentationToolbar.zoomOutDesc": "Zoom out of the presentation",
|
||||
"app.presentation.presentationToolbar.zoomIndicator": "Show the zoom percentage",
|
||||
"app.presentation.presentationToolbar.zoomIndicator": "Current zoom percentage",
|
||||
"app.presentation.presentationToolbar.fitToWidth": "Fit to width",
|
||||
"app.presentation.presentationToolbar.goToSlide": "Slide {0}",
|
||||
"app.presentationUploder.title": "Presentation",
|
||||
@ -174,6 +178,7 @@
|
||||
"app.actionsBar.label": "Actions bar",
|
||||
"app.actionsBar.actionsDropdown.restorePresentationLabel": "Restore presentation",
|
||||
"app.actionsBar.actionsDropdown.restorePresentationDesc": "Button to restore presentation after it has been closed",
|
||||
"app.screenshare.screenShareLabel" : "Screen share",
|
||||
"app.submenu.application.applicationSectionTitle": "Application",
|
||||
"app.submenu.application.animationsLabel": "Animations",
|
||||
"app.submenu.application.audioAlertLabel": "Audio Alerts for Chat",
|
||||
@ -429,7 +434,7 @@
|
||||
"app.video.stats.rtt": "RTT",
|
||||
"app.video.stats.encodeUsagePercent": "Encode usage",
|
||||
"app.video.stats.currentDelay": "Current delay",
|
||||
"app.fullscreenButton.label": "Make element fullscreen",
|
||||
"app.fullscreenButton.label": "Make {0} fullscreen",
|
||||
"app.deskshare.iceConnectionStateError": "Error 1108: ICE connection failed when sharing screen",
|
||||
"app.sfu.mediaServerConnectionError2000": "Error 2000: Unable to connect to media server",
|
||||
"app.sfu.mediaServerOffline2001": "Error 2001: Media server is offline. Please try again later.",
|
||||
@ -481,8 +486,9 @@
|
||||
"app.videoDock.webcamUnfocusDesc": "Unfocus the selected webcam",
|
||||
"app.invitation.title": "Breakout room invitation",
|
||||
"app.invitation.confirm": "Invite",
|
||||
"app.createBreakoutRoom.title": "Breakout rooms",
|
||||
"app.createBreakoutRoom.breakoutRoomLabel": "Breakout rooms {0}",
|
||||
"app.createBreakoutRoom.title": "Breakout Rooms",
|
||||
"app.createBreakoutRoom.ariaTitle": "Hide Breakout Rooms",
|
||||
"app.createBreakoutRoom.breakoutRoomLabel": "Breakout Rooms {0}",
|
||||
"app.createBreakoutRoom.generatingURL": "Generating URL",
|
||||
"app.createBreakoutRoom.generatedURL": "Generated",
|
||||
"app.createBreakoutRoom.duration": "Duration {0}",
|
||||
|
@ -14,12 +14,47 @@ sdk install gradle 5.1.1
|
||||
sdk install grails 3.3.9
|
||||
```
|
||||
|
||||
To run the application from its source code : `grails prod run-app`
|
||||
### Development
|
||||
|
||||
To run the application on a different port use : `grails -port=8989 prod run-app`
|
||||
Build `bbb-common-message`
|
||||
|
||||
```
|
||||
cd /bigbluebutton/bbb-common-message
|
||||
|
||||
./deploy.sh
|
||||
```
|
||||
|
||||
Build `bbb-common-web`
|
||||
|
||||
```
|
||||
cd bigbluebutton/bbb-common-web
|
||||
|
||||
./deploy.sh
|
||||
```
|
||||
|
||||
Build and run `bbb-web`
|
||||
|
||||
```
|
||||
cd bigbluebutton/bigbluebutton-web
|
||||
|
||||
# Make sure you don't have old libs lying around. Might cause issues.
|
||||
# You need to to this only once to cleanup lib dir.
|
||||
|
||||
rm lib/*
|
||||
|
||||
./build.sh
|
||||
|
||||
# This will listen on port 8989 so you need to adjust your nginx config.
|
||||
# If you've setup your nginx config to bbb-web dev, you don't need to do anything.
|
||||
|
||||
./run.sh
|
||||
|
||||
```
|
||||
|
||||
To run unit tests: `grails test-app --stacktrace`
|
||||
|
||||
### Production
|
||||
|
||||
To package the application for production:
|
||||
|
||||
1. Compile the application and package it use `grails assemble`
|
||||
|
Loading…
Reference in New Issue
Block a user