Merge branch 'bigbluebutton:develop' into develop

This commit is contained in:
test-erik 2021-10-05 12:14:20 +02:00 committed by GitHub
commit 7d1bf9b4dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
274 changed files with 45789 additions and 6969 deletions

View File

@ -11,7 +11,7 @@ stages:
# define which docker image to use for builds
default:
image: gitlab.senfcall.de:5050/senfcall-public/docker-bbb-build:v2021-08-10
image: gitlab.senfcall.de:5050/senfcall-public/docker-bbb-build:v2021-09-30
# This stage uses git to find out since when each package has been unmodified.
# it then checks an API endpoint on the package server to find out for which of

View File

@ -1,5 +1,8 @@
package org.bigbluebutton.core.apps
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
import org.bigbluebutton.core2.message.senders.MsgBuilder
object ScreenshareModel {
def resetDesktopSharingParams(status: ScreenshareModel) = {
status.broadcastingRTMP = false
@ -88,6 +91,24 @@ object ScreenshareModel {
def getHasAudio(status: ScreenshareModel): Boolean = {
status.hasAudio
}
def stop(outGW: OutMsgRouter, liveMeeting: LiveMeeting): Unit = {
if (isBroadcastingRTMP(liveMeeting.screenshareModel)) {
this.resetDesktopSharingParams(liveMeeting.screenshareModel)
val event = MsgBuilder.buildStopScreenshareRtmpBroadcastEvtMsg(
liveMeeting.props.meetingProp.intId,
getVoiceConf(liveMeeting.screenshareModel),
getScreenshareConf(liveMeeting.screenshareModel),
getRTMPBroadcastingUrl(liveMeeting.screenshareModel),
getScreenshareVideoWidth(liveMeeting.screenshareModel),
getScreenshareVideoHeight(liveMeeting.screenshareModel),
getTimestamp(liveMeeting.screenshareModel)
)
outGW.send(event)
}
}
}
class ScreenshareModel {

View File

@ -4,6 +4,7 @@ import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.running.{ MeetingActor, OutMsgRouter }
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
import org.bigbluebutton.core.models.{ Users2x, Roles }
trait RequestBreakoutJoinURLReqMsgHdlr extends RightsManagementTrait {
this: MeetingActor =>
@ -19,15 +20,22 @@ trait RequestBreakoutJoinURLReqMsgHdlr extends RightsManagementTrait {
for {
model <- state.breakout
room <- model.find(msg.body.breakoutId)
requesterUser <- Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId)
} yield {
BreakoutHdlrHelpers.sendJoinURL(
liveMeeting,
outGW,
msg.body.userId,
room.externalId,
room.sequence.toString(),
room.id
)
if (requesterUser.role == Roles.MODERATOR_ROLE || room.freeJoin) {
BreakoutHdlrHelpers.sendJoinURL(
liveMeeting,
outGW,
msg.body.userId,
room.externalId,
room.sequence.toString(),
room.id
)
} else {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to request breakout room URL for meeting."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)
}
}
}

View File

@ -1,9 +1,9 @@
package org.bigbluebutton.core.apps.externalvideo
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait, ExternalVideoModel }
import org.bigbluebutton.core.apps.{ ExternalVideoModel, PermissionCheck, RightsManagementTrait, ScreenshareModel }
import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.running.{ LiveMeeting }
import org.bigbluebutton.core.running.LiveMeeting
trait StartExternalVideoPubMsgHdlr extends RightsManagementTrait {
this: ExternalVideoApp2x =>
@ -28,6 +28,10 @@ trait StartExternalVideoPubMsgHdlr extends RightsManagementTrait {
val reason = "You need to be the presenter to start external videos"
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
} else {
//Stop ScreenShare if it's running
ScreenshareModel.stop(bus.outGW, liveMeeting)
ExternalVideoModel.setURL(liveMeeting.externalVideoModel, msg.body.externalVideoUrl)
broadcastEvent(msg)
}

View File

@ -1,9 +1,10 @@
package org.bigbluebutton.core.apps.externalvideo
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait, ExternalVideoModel }
import org.bigbluebutton.core.apps.{ ExternalVideoModel, PermissionCheck, RightsManagementTrait }
import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.running.{ LiveMeeting }
import org.bigbluebutton.core.running.LiveMeeting
import org.bigbluebutton.core2.message.senders.MsgBuilder
trait StopExternalVideoPubMsgHdlr extends RightsManagementTrait {
this: ExternalVideoApp2x =>
@ -11,25 +12,16 @@ trait StopExternalVideoPubMsgHdlr extends RightsManagementTrait {
def handle(msg: StopExternalVideoPubMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
log.info("Received StopExternalVideoPubMsgr meetingId={}", liveMeeting.props.meetingProp.intId)
def broadcastEvent() {
val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, liveMeeting.props.meetingProp.intId, "nodeJSapp")
val envelope = BbbCoreEnvelope(StopExternalVideoEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(StopExternalVideoEvtMsg.NAME, liveMeeting.props.meetingProp.intId, msg.header.userId)
val body = StopExternalVideoEvtMsgBody()
val event = StopExternalVideoEvtMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
bus.outGW.send(msgEvent)
}
if (permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "You need to be the presenter to stop external video"
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
} else {
ExternalVideoModel.clear(liveMeeting.externalVideoModel)
broadcastEvent()
//broadcastEvent
val msgEvent = MsgBuilder.buildStopExternalVideoEvtMsg(liveMeeting.props.meetingProp.intId, msg.header.userId)
bus.outGW.send(msgEvent)
}
}
}

View File

@ -4,32 +4,13 @@ import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.apps.ScreenshareModel
import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.running.LiveMeeting
import org.bigbluebutton.core2.message.senders.MsgBuilder
trait ScreenshareRtmpBroadcastStoppedVoiceConfEvtMsgHdlr {
this: ScreenshareApp2x =>
def handle(msg: ScreenshareRtmpBroadcastStoppedVoiceConfEvtMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
def broadcastEvent(voiceConf: String, screenshareConf: String,
stream: String, vidWidth: Int, vidHeight: Int,
timestamp: String): BbbCommonEnvCoreMsg = {
val routing = Routing.addMsgToClientRouting(
MessageTypes.BROADCAST_TO_MEETING,
liveMeeting.props.meetingProp.intId, "not-used"
)
val envelope = BbbCoreEnvelope(ScreenshareRtmpBroadcastStoppedEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(
ScreenshareRtmpBroadcastStoppedEvtMsg.NAME,
liveMeeting.props.meetingProp.intId, "not-used"
)
val body = ScreenshareRtmpBroadcastStoppedEvtMsgBody(voiceConf, screenshareConf,
stream, vidWidth, vidHeight, timestamp)
val event = ScreenshareRtmpBroadcastStoppedEvtMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
log.info("handleScreenshareRTMPBroadcastStoppedRequest: isBroadcastingRTMP=" +
ScreenshareModel.isBroadcastingRTMP(liveMeeting.screenshareModel) + " URL:" +
ScreenshareModel.getRTMPBroadcastingUrl(liveMeeting.screenshareModel))
@ -40,8 +21,12 @@ trait ScreenshareRtmpBroadcastStoppedVoiceConfEvtMsgHdlr {
ScreenshareModel.broadcastingRTMPStopped(liveMeeting.screenshareModel)
// notify viewers that RTMP broadcast stopped
val msgEvent = broadcastEvent(msg.body.voiceConf, msg.body.screenshareConf, msg.body.stream,
msg.body.vidWidth, msg.body.vidHeight, msg.body.timestamp)
val msgEvent = MsgBuilder.buildStopScreenshareRtmpBroadcastEvtMsg(
liveMeeting.props.meetingProp.intId,
msg.body.voiceConf, msg.body.screenshareConf, msg.body.stream,
msg.body.vidWidth, msg.body.vidHeight, msg.body.timestamp
)
bus.outGW.send(msgEvent)
} else {
log.info("STOP broadcast NOT ALLOWED when isBroadcastingRTMP=false")

View File

@ -12,7 +12,16 @@ trait ChangeUserEmojiCmdMsgHdlr extends RightsManagementTrait {
val outGW: OutMsgRouter
def handleChangeUserEmojiCmdMsg(msg: ChangeUserEmojiCmdMsg) {
if (msg.header.userId != msg.body.userId && permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
// Usually only moderators are allowed to change someone else's emoji status
// Exceptional case: Viewers who are presenter are allowed to lower someone else's raised hand:
val isViewerProhibitedFromLoweringOthersHand =
!(Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId).get.emoji.equals("raiseHand") &&
msg.body.emoji.equals("none")) ||
permissionFailed(PermissionCheck.VIEWER_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)
if (msg.header.userId != msg.body.userId &&
permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) &&
isViewerProhibitedFromLoweringOthersHand) {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to clear change user emoji status."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)

View File

@ -339,7 +339,8 @@ class MeetingActor(
state = state.update(tracker)
}
} else {
if (state.expiryTracker.moderatorHasJoined == true) {
if (state.expiryTracker.moderatorHasJoined == true &&
state.expiryTracker.lastModeratorLeftOnInMs == 0) {
log.info("All moderators have left. Setting setLastModeratorLeftOn(). meetingId=" + props.meetingProp.intId)
val tracker = state.expiryTracker.setLastModeratorLeftOn(TimeUtil.timeNowInMs())
state = state.update(tracker)

View File

@ -160,17 +160,32 @@ object MsgBuilder {
BbbCommonEnvCoreMsg(envelope, event)
}
def buildStopExternalVideoEvtMsg(meetingId: String): BbbCommonEnvCoreMsg = {
def buildStopExternalVideoEvtMsg(meetingId: String, userId: String = "not-used"): BbbCommonEnvCoreMsg = {
val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, meetingId, "nodeJSapp")
val envelope = BbbCoreEnvelope(StopExternalVideoEvtMsg.NAME, routing)
val body = StopExternalVideoEvtMsgBody()
val header = BbbClientMsgHeader(StopExternalVideoEvtMsg.NAME, meetingId, "not-used")
val header = BbbClientMsgHeader(StopExternalVideoEvtMsg.NAME, meetingId, userId)
val event = StopExternalVideoEvtMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
def buildStopScreenshareRtmpBroadcastEvtMsg(
meetingId: String,
voiceConf: String, screenshareConf: String,
stream: String, vidWidth: Int, vidHeight: Int,
timestamp: String
): BbbCommonEnvCoreMsg = {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, "not-used")
val envelope = BbbCoreEnvelope(ScreenshareRtmpBroadcastStoppedEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(ScreenshareRtmpBroadcastStoppedEvtMsg.NAME, meetingId, "not-used")
val body = ScreenshareRtmpBroadcastStoppedEvtMsgBody(voiceConf, screenshareConf, stream, vidWidth, vidHeight, timestamp)
val event = ScreenshareRtmpBroadcastStoppedEvtMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
def buildMeetingCreatedEvtMsg(meetingId: String, props: DefaultProps): BbbCommonEnvCoreMsg = {
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
val envelope = BbbCoreEnvelope(MeetingCreatedEvtMsg.NAME, routing)

View File

@ -10,7 +10,6 @@ import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@NotEmpty(message = "You must provide your password")
@Size(min = 2, max = 64, message = "Password must be between 8 and 20 characters")
@Constraint(validatedBy = {})
@Target(FIELD)

View File

@ -7,6 +7,7 @@ import org.bigbluebutton.api.model.shared.Checksum;
import org.bigbluebutton.api.model.shared.ModeratorPassword;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import java.util.Map;
public class EndMeeting extends RequestWithChecksum<EndMeeting.Params> {
@ -27,6 +28,7 @@ public class EndMeeting extends RequestWithChecksum<EndMeeting.Params> {
private String meetingID;
@PasswordConstraint
@NotEmpty(message = "You must provide the moderator password")
private String password;
@Valid

View File

@ -35,6 +35,7 @@ public class JoinMeeting extends RequestWithChecksum<JoinMeeting.Params> {
private String fullName;
@PasswordConstraint
@NotEmpty(message = "You must provide either the moderator or attendee password")
private String password;
@IsBooleanConstraint(message = "Guest must be a boolean value (true or false)")

View File

@ -67,6 +67,10 @@ public class PageToConvert {
return pres.getId();
}
public String getMeetingId() {
return pres.getMeetingId();
}
public PageToConvert convert() {
// Only create SWF files if the configuration requires it

View File

@ -1,13 +1,13 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
*
* Copyright (c) 2015 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
@ -46,6 +46,7 @@ public class PdfToSwfSlidesGenerationService {
PageConvertProgressMessage msg = new PageConvertProgressMessage(
pageToConvert.getPageNumber(),
pageToConvert.getPresId(),
pageToConvert.getMeetingId(),
new ArrayList<>());
presentationConversionCompletionService.handle(msg);
pageToConvert.getPageFile().delete();

View File

@ -36,11 +36,16 @@ public class PresentationConversionCompletionService {
if (msg instanceof PresentationConvertMessage) {
PresentationConvertMessage m = (PresentationConvertMessage) msg;
PresentationToConvert p = new PresentationToConvert(m.pres);
presentationsToConvert.put(p.getKey(), p);
} else if (msg instanceof PageConvertProgressMessage) {
String presentationToConvertKey = p.getKey() + "_" + m.pres.getMeetingId();
presentationsToConvert.put(presentationToConvertKey, p);
} else if (msg instanceof PageConvertProgressMessage) {
PageConvertProgressMessage m = (PageConvertProgressMessage) msg;
PresentationToConvert p = presentationsToConvert.get(m.presId);
String presentationToConvertKey = m.presId + "_" + m.meetingId;
PresentationToConvert p = presentationsToConvert.get(presentationToConvertKey);
if (p != null) {
p.incrementPagesCompleted();
notifier.sendConversionUpdateMessage(p.getPagesCompleted(), p.pres, m.page);
@ -52,7 +57,9 @@ public class PresentationConversionCompletionService {
}
private void handleEndProcessing(PresentationToConvert p) {
presentationsToConvert.remove(p.getKey());
String presentationToConvertKey = p.getKey() + "_" + p.pres.getMeetingId();
presentationsToConvert.remove(presentationToConvertKey);
Map<String, Object> logData = new HashMap<String, Object>();
logData = new HashMap<String, Object>();

View File

@ -7,9 +7,11 @@ public class PageConvertProgressMessage implements IPresentationCompletionMessag
public final String presId;
public final int page;
public final List<String> errors;
public final String meetingId;
public PageConvertProgressMessage(int page, String presId, List<String> errors) {
public PageConvertProgressMessage(int page, String presId, String meetingId, List<String> errors) {
this.presId = presId;
this.meetingId = meetingId;
this.page = page;
this.errors = errors;
}

File diff suppressed because it is too large Load Diff

View File

@ -36,3 +36,17 @@
transform: rotate(360deg);
}
}
.col-text-left {
text-align: left;
}
[dir="rtl"] .col-text-left {
text-align: right;
}
.col-text-right {
text-align: right;
}
[dir="rtl"] .col-text-right {
text-align: left;
}

View File

@ -6,29 +6,38 @@ import Card from './components/Card';
import UsersTable from './components/UsersTable';
import StatusTable from './components/StatusTable';
import PollsTable from './components/PollsTable';
import ErrorMessage from './components/ErrorMessage';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: true,
activitiesJson: {},
tab: 'overview',
meetingId: '',
learningDashboardAccessToken: '',
};
}
componentDidMount() {
this.fetchActivitiesJson();
this.setDashboardParams();
setInterval(() => {
this.fetchActivitiesJson();
}, 10000);
}
fetchActivitiesJson() {
setDashboardParams() {
let learningDashboardAccessToken = '';
let meetingId = '';
const urlSearchParams = new URLSearchParams(window.location.search);
const params = Object.fromEntries(urlSearchParams.entries());
if (typeof params.meeting === 'undefined') return;
let learningDashboardAccessToken = '';
if (typeof params.meeting !== 'undefined') {
meetingId = params.meeting;
}
if (typeof params.report !== 'undefined') {
learningDashboardAccessToken = params.report;
} else {
@ -38,22 +47,43 @@ class App extends React.Component {
cArr.forEach((val) => {
if (val.indexOf(`${cookieName}=`) === 0) learningDashboardAccessToken = val.substring((`${cookieName}=`).length);
});
// Extend AccessToken lifetime by 30d (in each access)
if (learningDashboardAccessToken !== '') {
const cookieExpiresDate = new Date();
cookieExpiresDate.setTime(cookieExpiresDate.getTime() + (3600000 * 24 * 30));
document.cookie = `learningDashboardAccessToken-${meetingId}=${learningDashboardAccessToken}; expires=${cookieExpiresDate.toGMTString()}; path=/;SameSite=None;Secure`;
}
}
this.setState({ learningDashboardAccessToken, meetingId }, this.fetchActivitiesJson);
}
fetchActivitiesJson() {
const { learningDashboardAccessToken, meetingId } = this.state;
if (learningDashboardAccessToken !== '') {
fetch(`${params.meeting}/${learningDashboardAccessToken}/learning_dashboard_data.json`)
fetch(`${meetingId}/${learningDashboardAccessToken}/learning_dashboard_data.json`)
.then((response) => response.json())
.then((json) => {
this.setState({ activitiesJson: json });
this.setState({ activitiesJson: json, loading: false });
document.title = `Learning Dashboard - ${json.name}`;
}).catch(() => {
this.setState({ loading: false });
});
} else {
this.setState({ loading: false });
}
}
render() {
const { activitiesJson, tab } = this.state;
const {
activitiesJson, tab, learningDashboardAccessToken, loading,
} = this.state;
const { intl } = this.props;
document.title = `${intl.formatMessage({ id: 'app.learningDashboard.dashboardTitle', defaultMessage: 'Learning Dashboard' })} - ${activitiesJson.name}`;
function totalOfRaiseHand() {
if (activitiesJson && activitiesJson.users) {
return Object.values(activitiesJson.users)
@ -131,6 +161,15 @@ class App extends React.Component {
return meetingAveragePoints;
}
function getErrorMessage() {
if (learningDashboardAccessToken === '') {
return intl.formatMessage({ id: 'app.learningDashboard.errors.invalidToken', defaultMessage: 'Invalid session token' });
}
return intl.formatMessage({ id: 'app.learningDashboard.errors.dataUnavailable', defaultMessage: 'Data is no longer available' });
}
if (loading === false && typeof activitiesJson.name === 'undefined') return <ErrorMessage message={getErrorMessage()} />;
return (
<div className="mx-10">
<div className="flex items-start justify-between pb-3">
@ -139,27 +178,35 @@ class App extends React.Component {
<br />
<span className="text-sm font-medium">{activitiesJson.name || ''}</span>
</h1>
<div className="mt-3 text-right px-4 py-1 text-gray-500 inline-block">
<div className="mt-3 col-text-right py-1 text-gray-500 inline-block">
<p className="font-bold">
<FormattedDate
value={activitiesJson.createdOn}
year="numeric"
month="short"
day="numeric"
/>
<div className="inline">
<FormattedDate
value={activitiesJson.createdOn}
year="numeric"
month="short"
day="numeric"
/>
</div>
&nbsp;&nbsp;
{
activitiesJson.endedOn > 0
? (
<span className="px-2 py-1 ml-3 font-semibold leading-tight text-red-700 bg-red-100 rounded-full">
<FormattedMessage id="app.learningDashboard.indicators.meetingStatusEnded" defaultMessage="Ended" />
</span>
)
: (
<span className="px-2 py-1 ml-3 font-semibold leading-tight text-green-700 bg-green-100 rounded-full">
<FormattedMessage id="app.learningDashboard.indicators.meetingStatusActive" defaultMessage="Active" />
</span>
)
}
activitiesJson.endedOn > 0
? (
<span className="px-2 py-1 font-semibold leading-tight text-red-700 bg-red-100 rounded-full">
<FormattedMessage id="app.learningDashboard.indicators.meetingStatusEnded" defaultMessage="Ended" />
</span>
)
: null
}
{
activitiesJson.endedOn === 0
? (
<span className="px-2 py-1 font-semibold leading-tight text-green-700 bg-green-100 rounded-full">
<FormattedMessage id="app.learningDashboard.indicators.meetingStatusActive" defaultMessage="Active" />
</span>
)
: null
}
</p>
<p>
<FormattedMessage id="app.learningDashboard.indicators.duration" defaultMessage="Duration" />
@ -174,8 +221,8 @@ class App extends React.Component {
<Card
name={
activitiesJson.endedOn === 0
? intl.formatMessage({ id: 'app.learningDashboard.indicators.participantsOnline', defaultMessage: 'Active Participants' })
: intl.formatMessage({ id: 'app.learningDashboard.indicators.participantsTotal', defaultMessage: 'Total Number Of Participants' })
? intl.formatMessage({ id: 'app.learningDashboard.indicators.usersOnline', defaultMessage: 'Active Users' })
: intl.formatMessage({ id: 'app.learningDashboard.indicators.usersTotal', defaultMessage: 'Total Number Of Users' })
}
number={Object.values(activitiesJson.users || {})
.filter((u) => activitiesJson.endedOn > 0 || u.leftOn === 0).length}
@ -282,7 +329,7 @@ class App extends React.Component {
</div>
<h1 className="block my-1 pr-2 text-xl font-semibold">
{ tab === 'overview' || tab === 'overview_activityscore'
? <FormattedMessage id="app.learningDashboard.participantsTable.title" defaultMessage="Overview" />
? <FormattedMessage id="app.learningDashboard.usersTable.title" defaultMessage="Overview" />
: null }
{ tab === 'status_timeline'
? <FormattedMessage id="app.learningDashboard.statusTimelineTable.title" defaultMessage="Status Timeline" />

View File

@ -0,0 +1,22 @@
import React from 'react';
function ErrorMessage(props) {
const { message } = props;
return (
<div className="container flex flex-col items-center px-6 mx-auto">
<svg className="w-12 h-12 my-8 text-gray-700" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M13.477 14.89A6 6 0 015.11 6.524l8.367 8.368zm1.414-1.414L6.524 5.11a6 6 0 018.367 8.367zM18 10a8 8 0 11-16 0 8 8 0 0116 0z"
clipRule="evenodd"
/>
</svg>
<h1 className="text-xl font-semibold text-gray-700 dark:text-gray-200">
{message}
</h1>
</div>
);
}
export default ErrorMessage;

View File

@ -15,11 +15,11 @@ class PollsTable extends React.Component {
}
return (
<table className="w-full whitespace-no-wrap">
<table className="w-full whitespace-nowrap">
<thead>
<tr className="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b bg-gray-100">
<tr className="text-xs font-semibold tracking-wide col-text-left text-gray-500 uppercase border-b bg-gray-100">
<th className="px-4 py-3">
<FormattedMessage id="app.learningDashboard.pollsTable.colParticipant" defaultMessage="Participant" />
<FormattedMessage id="app.learningDashboard.user" defaultMessage="User" />
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 inline"
@ -39,13 +39,21 @@ class PollsTable extends React.Component {
{ typeof allUsers === 'object' && Object.values(allUsers || {}).length > 0 ? (
Object.values(allUsers || {})
.filter((user) => Object.values(user.answers).length > 0)
.sort((a, b) => {
if (a.isModerator === false && b.isModerator === true) return 1;
if (a.isModerator === true && b.isModerator === false) return -1;
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
return 0;
})
.map((user) => (
<tr className="text-gray-700">
<td className="px-4 py-3">
<div className="flex items-center text-sm">
<div className="relative hidden w-8 h-8 mr-3 rounded-full md:block">
<div className="relative hidden w-8 h-8 rounded-full md:block">
<UserAvatar user={user} />
</div>
&nbsp;&nbsp;
<div>
<p className="font-semibold">{user.name}</p>
</div>

View File

@ -29,11 +29,11 @@ class StatusTable extends React.Component {
}
return (
<table className="w-full whitespace-no-wrap">
<table className="w-full whitespace-nowrap">
<thead>
<tr className="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b bg-gray-100">
<th className="px-4 py-3">
<FormattedMessage id="app.learningDashboard.statusTimelineTable.colParticipant" defaultMessage="Participant" />
<tr className="text-xs font-semibold tracking-wide text-gray-500 uppercase border-b bg-gray-100">
<th className="px-4 py-3 col-text-left">
<FormattedMessage id="app.learningDashboard.user" defaultMessage="User" />
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 inline"
@ -44,19 +44,27 @@ class StatusTable extends React.Component {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 13l-5 5m0 0l-5-5m5 5V6" />
</svg>
</th>
{ periods.map((period) => <th className="px-4 py-3 text-left">{ `${tsToHHmmss(period - firstRegisteredOnTime)}` }</th>) }
{ periods.map((period) => <th className="px-4 py-3 col-text-left">{ `${tsToHHmmss(period - firstRegisteredOnTime)}` }</th>) }
</tr>
</thead>
<tbody className="bg-white divide-y">
{ typeof allUsers === 'object' && Object.values(allUsers || {}).length > 0 ? (
Object.values(allUsers || {})
.sort((a, b) => {
if (a.isModerator === false && b.isModerator === true) return 1;
if (a.isModerator === true && b.isModerator === false) return -1;
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
return 0;
})
.map((user) => (
<tr className="text-gray-700">
<td className="px-4 py-3">
<div className="flex items-center text-sm">
<div className="relative hidden w-8 h-8 mr-3 rounded-full md:block">
<div className="relative hidden w-8 h-8 rounded-full md:block">
<UserAvatar user={user} />
</div>
&nbsp;&nbsp;
<div>
<p className="font-semibold">{user.name}</p>
</div>
@ -68,7 +76,7 @@ class StatusTable extends React.Component {
period,
period + spanMinutes);
return (
<td className="px-4 py-3 text-sm text-left">
<td className="px-4 py-3 text-sm col-text-left">
{
user.registeredOn > period && user.registeredOn < period + spanMinutes
? (

View File

@ -82,11 +82,11 @@ class UsersTable extends React.Component {
});
return (
<table className="w-full whitespace-no-wrap">
<table className="w-full whitespace-nowrap">
<thead>
<tr className="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b bg-gray-100">
<th className="px-4 py-3">
<FormattedMessage id="app.learningDashboard.participantsTable.colParticipant" defaultMessage="Participant" />
<th className="px-4 py-3 col-text-left">
<FormattedMessage id="app.learningDashboard.user" defaultMessage="User" />
{
tab === 'overview'
? (
@ -104,25 +104,25 @@ class UsersTable extends React.Component {
}
</th>
<th className="px-4 py-3 text-center">
<FormattedMessage id="app.learningDashboard.participantsTable.colOnline" defaultMessage="Online time" />
<FormattedMessage id="app.learningDashboard.usersTable.colOnline" defaultMessage="Online time" />
</th>
<th className="px-4 py-3 text-center">
<FormattedMessage id="app.learningDashboard.participantsTable.colTalk" defaultMessage="Talk time" />
<FormattedMessage id="app.learningDashboard.usersTable.colTalk" defaultMessage="Talk time" />
</th>
<th className="px-4 py-3 text-center">
<FormattedMessage id="app.learningDashboard.participantsTable.colWebcam" defaultMessage="Webcam Time" />
<FormattedMessage id="app.learningDashboard.usersTable.colWebcam" defaultMessage="Webcam Time" />
</th>
<th className="px-4 py-3 text-center">
<FormattedMessage id="app.learningDashboard.participantsTable.colMessages" defaultMessage="Messages" />
<FormattedMessage id="app.learningDashboard.usersTable.colMessages" defaultMessage="Messages" />
</th>
<th className="px-4 py-3 text-left">
<FormattedMessage id="app.learningDashboard.participantsTable.colEmojis" defaultMessage="Emojis" />
<th className="px-4 py-3 col-text-left">
<FormattedMessage id="app.learningDashboard.usersTable.colEmojis" defaultMessage="Emojis" />
</th>
<th className="px-4 py-3 text-center">
<FormattedMessage id="app.learningDashboard.participantsTable.colRaiseHands" defaultMessage="Raise Hand" />
<FormattedMessage id="app.learningDashboard.usersTable.colRaiseHands" defaultMessage="Raise Hand" />
</th>
<th className="px-4 py-3 text-center">
<FormattedMessage id="app.learningDashboard.participantsTable.colActivityScore" defaultMessage="Activity Score" />
<FormattedMessage id="app.learningDashboard.usersTable.colActivityScore" defaultMessage="Activity Score" />
{
tab === 'overview_activityscore'
? (
@ -139,8 +139,8 @@ class UsersTable extends React.Component {
: null
}
</th>
<th className="px-4 py-3">
<FormattedMessage id="app.learningDashboard.participantsTable.colStatus" defaultMessage="Status" />
<th className="px-4 py-3 text-center">
<FormattedMessage id="app.learningDashboard.usersTable.colStatus" defaultMessage="Status" />
</th>
</tr>
</thead>
@ -158,84 +158,83 @@ class UsersTable extends React.Component {
})
.map((user) => (
<tr key={user} className="text-gray-700">
<td className="px-4 py-3">
<div className="flex items-center text-sm">
<div className="relative hidden w-8 h-8 mr-3 rounded-full md:block">
{/* <img className="object-cover w-full h-full rounded-full" */}
{/* src="" */}
{/* alt="" loading="lazy" /> */}
<UserAvatar user={user} />
<div
className="absolute inset-0 rounded-full shadow-inner"
aria-hidden="true"
/>
</div>
<div>
<p className="font-semibold">
{user.name}
</p>
<p className="text-xs text-gray-600 dark:text-gray-400">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
/>
</svg>
<FormattedDate
value={user.registeredOn}
month="short"
day="numeric"
hour="2-digit"
minute="2-digit"
second="2-digit"
<td className="px-4 py-3 col-text-left text-sm">
<div className="inline-block relative w-8 h-8 rounded-full">
{/* <img className="object-cover w-full h-full rounded-full" */}
{/* src="" */}
{/* alt="" loading="lazy" /> */}
<UserAvatar user={user} />
<div
className="absolute inset-0 rounded-full shadow-inner"
aria-hidden="true"
/>
</div>
&nbsp;&nbsp;&nbsp;
<div className="inline-block">
<p className="font-semibold">
{user.name}
</p>
<p className="text-xs text-gray-600 dark:text-gray-400">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
/>
</p>
{
user.leftOn > 0
? (
<p className="text-xs text-gray-600 dark:text-gray-400">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
<FormattedDate
value={user.leftOn}
month="short"
day="numeric"
hour="2-digit"
minute="2-digit"
second="2-digit"
</svg>
<FormattedDate
value={user.registeredOn}
month="short"
day="numeric"
hour="2-digit"
minute="2-digit"
second="2-digit"
/>
</p>
{
user.leftOn > 0
? (
<p className="text-xs text-gray-600 dark:text-gray-400">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</p>
)
: null
}
</div>
</svg>
<FormattedDate
value={user.leftOn}
month="short"
day="numeric"
hour="2-digit"
minute="2-digit"
second="2-digit"
/>
</p>
)
: null
}
</div>
</td>
<td className="px-4 py-3 text-sm text-center items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 mr-1 inline"
className="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@ -247,6 +246,7 @@ class UsersTable extends React.Component {
d="M5.636 18.364a9 9 0 010-12.728m12.728 0a9 9 0 010 12.728m-9.9-2.829a5 5 0 010-7.07m7.072 0a5 5 0 010 7.07M13 12a1 1 0 11-2 0 1 1 0 012 0z"
/>
</svg>
&nbsp;
{ tsToHHmmss(
(user.leftOn > 0
? user.leftOn
@ -271,7 +271,7 @@ class UsersTable extends React.Component {
<span className="text-center">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 mr-1 inline"
className="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@ -283,6 +283,7 @@ class UsersTable extends React.Component {
d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"
/>
</svg>
&nbsp;
{ tsToHHmmss(user.talk.totalTime) }
</span>
) : null }
@ -293,7 +294,7 @@ class UsersTable extends React.Component {
<span className="text-center">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 mr-1 inline"
className="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@ -305,6 +306,7 @@ class UsersTable extends React.Component {
d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
&nbsp;
{ tsToHHmmss(getSumOfTime(user.webcams)) }
</span>
) : null }
@ -315,7 +317,7 @@ class UsersTable extends React.Component {
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 mr-1 inline"
className="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@ -327,14 +329,15 @@ class UsersTable extends React.Component {
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
/>
</svg>
&nbsp;
{user.totalOfMessages}
</span>
) : null }
</td>
<td className="px-4 py-3 text-sm text-left">
<td className="px-4 py-3 text-sm col-text-left">
{
Object.keys(usersEmojisSummary[user.intId] || {}).map((emoji) => (
<div className="text-xs">
<div className="text-xs whitespace-nowrap">
<i className={`${emojiConfigs[emoji].icon} text-sm`} />
&nbsp;
{ usersEmojisSummary[user.intId][emoji] }
@ -353,7 +356,7 @@ class UsersTable extends React.Component {
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 mr-1 inline"
className="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@ -365,6 +368,7 @@ class UsersTable extends React.Component {
d="M7 11.5V14m0-2.5v-6a1.5 1.5 0 113 0m-3 6a1.5 1.5 0 00-3 0v2a7.5 7.5 0 0015 0v-5a1.5 1.5 0 00-3 0m-6-3V11m0-5.5v-1a1.5 1.5 0 013 0v1m0 0V11m0-5.5a1.5 1.5 0 013 0v3m0 0V11"
/>
</svg>
&nbsp;
{user.emojis.filter((emoji) => emoji.name === 'raiseHand').length}
</span>
) : null }
@ -381,23 +385,23 @@ class UsersTable extends React.Component {
<rect width="12" height="12" x="70" fill={usersActivityScore[user.intId] === 10 ? '#047857' : '#e4e4e7'} />
</svg>
&nbsp;
<span className="text-xs bg-gray-200 rounded-full px-2 ml-1">
<span className="text-xs bg-gray-200 rounded-full px-2">
<FormattedNumber value={usersActivityScore[user.intId]} minimumFractionDigits="0" maximumFractionDigits="1" />
</span>
</td>
) : <td />
}
<td className="px-4 py-3 text-xs">
<td className="px-4 py-3 text-xs text-center">
{
user.leftOn > 0
? (
<span className="px-2 py-1 font-semibold leading-tight text-red-700 bg-red-100 rounded-full">
<FormattedMessage id="app.learningDashboard.participantsTable.userStatusOffline" defaultMessage="Offline" />
<FormattedMessage id="app.learningDashboard.usersTable.userStatusOffline" defaultMessage="Offline" />
</span>
)
: (
<span className="px-2 py-1 font-semibold leading-tight text-green-700 bg-green-100 rounded-full">
<FormattedMessage id="app.learningDashboard.participantsTable.userStatusOnline" defaultMessage="Online" />
<FormattedMessage id="app.learningDashboard.usersTable.userStatusOnline" defaultMessage="Online" />
</span>
)
}
@ -407,7 +411,7 @@ class UsersTable extends React.Component {
) : (
<tr className="text-gray-700">
<td colSpan="8" className="px-4 py-3 text-sm text-center">
<FormattedMessage id="app.learningDashboard.participantsTable.noUsers" defaultMessage="No users" />
<FormattedMessage id="app.learningDashboard.usersTable.noUsers" defaultMessage="No users" />
</td>
</tr>
)}

View File

@ -5,6 +5,8 @@ import { IntlProvider } from 'react-intl';
import App from './App';
import reportWebVitals from './reportWebVitals';
const RTL_LANGUAGES = ['ar', 'dv', 'fa', 'he'];
function getLanguage() {
let { language } = navigator;
@ -27,6 +29,7 @@ class Dashboard extends React.Component {
};
this.setMessages();
this.setRtl();
}
setMessages() {
@ -54,6 +57,14 @@ class Dashboard extends React.Component {
}).catch(() => {});
}
setRtl() {
const { intlLocale } = this.state;
if (RTL_LANGUAGES.includes(intlLocale)) {
document.body.parentNode.setAttribute('dir', 'rtl');
}
}
render() {
const { intlLocale, intlMessages } = this.state;

View File

@ -37,7 +37,7 @@ then
fi
cp "${source}" "$tempDir/file"
sudo /usr/bin/docker run --rm --network none --env="HOME=/tmp/" -w /tmp/ --user=$(printf %05d `id -u`) -v "$tempDir/":/data/ -v /usr/share/fonts/:/usr/share/fonts/:ro --rm bbb-soffice sh -c "/usr/bin/soffice -env:UserInstallation=file:///tmp/ $convertToParam --outdir /data /data/file"
sudo /usr/bin/docker run --rm --memory=1g --memory-swap=1g --network none --env="HOME=/tmp/" -w /tmp/ --user=$(printf %05d `id -u`) -v "$tempDir/":/data/ -v /usr/share/fonts/:/usr/share/fonts/:ro --rm bbb-soffice sh -c "/usr/bin/soffice -env:UserInstallation=file:///tmp/ $convertToParam --outdir /data /data/file"
cp "$tempDir/file.$convertTo" "${dest}"
rm -r "$tempDir/"

View File

@ -1,4 +1,4 @@
bigbluebutton ALL=(ALL) NOPASSWD: /usr/bin/docker run --rm --network none --env=HOME=/tmp/ -w /tmp/ --user=[0-9][0-9][0-9][0-9][0-9] -v /tmp/bbb-soffice-bigbluebutton/tmp.[0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z]/\:/data/ -v /usr/share/fonts/\:/usr/share/fonts/\:ro --rm bbb-soffice sh -c /usr/bin/soffice -env\:UserInstallation=file\:///tmp/ --convert-to pdf --outdir /data /data/file
etherpad ALL=(ALL) NOPASSWD: /usr/bin/docker run --rm --network none --env=HOME=/tmp/ -w /tmp/ --user=[0-9][0-9][0-9][0-9][0-9] -v /tmp/bbb-soffice-etherpad/tmp.[0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z]/\:/data/ -v /usr/share/fonts/\:/usr/share/fonts/\:ro --rm bbb-soffice sh -c /usr/bin/soffice -env\:UserInstallation=file\:///tmp/ --convert-to pdf --writer --outdir /data /data/file
etherpad ALL=(ALL) NOPASSWD: /usr/bin/docker run --rm --network none --env=HOME=/tmp/ -w /tmp/ --user=[0-9][0-9][0-9][0-9][0-9] -v /tmp/bbb-soffice-etherpad/tmp.[0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z]/\:/data/ -v /usr/share/fonts/\:/usr/share/fonts/\:ro --rm bbb-soffice sh -c /usr/bin/soffice -env\:UserInstallation=file\:///tmp/ --convert-to odt --writer --outdir /data /data/file
etherpad ALL=(ALL) NOPASSWD: /usr/bin/docker run --rm --network none --env=HOME=/tmp/ -w /tmp/ --user=[0-9][0-9][0-9][0-9][0-9] -v /tmp/bbb-soffice-etherpad/tmp.[0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z]/\:/data/ -v /usr/share/fonts/\:/usr/share/fonts/\:ro --rm bbb-soffice sh -c /usr/bin/soffice -env\:UserInstallation=file\:///tmp/ --convert-to doc --outdir /data /data/file
bigbluebutton ALL=(ALL) NOPASSWD: /usr/bin/docker run --rm --memory=1g --memory-swap=1g --network none --env=HOME=/tmp/ -w /tmp/ --user=[0-9][0-9][0-9][0-9][0-9] -v /tmp/bbb-soffice-bigbluebutton/tmp.[0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z]/\:/data/ -v /usr/share/fonts/\:/usr/share/fonts/\:ro --rm bbb-soffice sh -c /usr/bin/soffice -env\:UserInstallation=file\:///tmp/ --convert-to pdf --outdir /data /data/file
etherpad ALL=(ALL) NOPASSWD: /usr/bin/docker run --rm --memory=1g --memory-swap=1g --network none --env=HOME=/tmp/ -w /tmp/ --user=[0-9][0-9][0-9][0-9][0-9] -v /tmp/bbb-soffice-etherpad/tmp.[0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z]/\:/data/ -v /usr/share/fonts/\:/usr/share/fonts/\:ro --rm bbb-soffice sh -c /usr/bin/soffice -env\:UserInstallation=file\:///tmp/ --convert-to pdf --writer --outdir /data /data/file
etherpad ALL=(ALL) NOPASSWD: /usr/bin/docker run --rm --memory=1g --memory-swap=1g --network none --env=HOME=/tmp/ -w /tmp/ --user=[0-9][0-9][0-9][0-9][0-9] -v /tmp/bbb-soffice-etherpad/tmp.[0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z]/\:/data/ -v /usr/share/fonts/\:/usr/share/fonts/\:ro --rm bbb-soffice sh -c /usr/bin/soffice -env\:UserInstallation=file\:///tmp/ --convert-to odt --writer --outdir /data /data/file
etherpad ALL=(ALL) NOPASSWD: /usr/bin/docker run --rm --memory=1g --memory-swap=1g --network none --env=HOME=/tmp/ -w /tmp/ --user=[0-9][0-9][0-9][0-9][0-9] -v /tmp/bbb-soffice-etherpad/tmp.[0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z]/\:/data/ -v /usr/share/fonts/\:/usr/share/fonts/\:ro --rm bbb-soffice sh -c /usr/bin/soffice -env\:UserInstallation=file\:///tmp/ --convert-to doc --outdir /data /data/file

View File

@ -1 +1 @@
BIGBLUEBUTTON_RELEASE=2.4-beta-4
BIGBLUEBUTTON_RELEASE=2.4-rc-1

View File

@ -3,15 +3,15 @@
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
meteor-base@1.4.0
meteor-base@1.5.1
mobile-experience@1.1.0
mongo@1.10.0
mongo@1.12.0
reactive-var@1.0.11
standard-minifier-css@1.6.0
standard-minifier-js@2.6.0
standard-minifier-css@1.7.3
standard-minifier-js@2.6.1
es5-shim@4.8.0
ecmascript@0.14.3
ecmascript@0.15.3
shell-server@0.5.0
static-html

View File

@ -1 +1 @@
METEOR@1.10.2
METEOR@2.3.6

View File

@ -1,81 +1,81 @@
allow-deny@1.1.0
autoupdate@1.6.0
babel-compiler@7.5.3
autoupdate@1.7.0
babel-compiler@7.7.0
babel-runtime@1.5.0
base64@1.0.12
binary-heap@1.0.11
blaze-tools@1.0.10
boilerplate-generator@1.7.0
blaze-tools@1.1.2
boilerplate-generator@1.7.1
caching-compiler@1.2.2
caching-html-compiler@1.1.3
callback-hook@1.3.0
caching-html-compiler@1.2.1
callback-hook@1.3.1
cfs:reactive-list@0.0.9
check@1.3.1
ddp@1.4.0
ddp-client@2.3.3
ddp-client@2.5.0
ddp-common@1.4.0
ddp-server@2.3.2
ddp-server@2.4.1
deps@1.0.12
diff-sequence@1.1.1
dynamic-import@0.5.2
ecmascript@0.14.3
dynamic-import@0.7.1
ecmascript@0.15.3
ecmascript-runtime@0.7.0
ecmascript-runtime-client@0.10.0
ecmascript-runtime-server@0.9.0
ecmascript-runtime-client@0.11.1
ecmascript-runtime-server@0.10.1
ejson@1.1.1
es5-shim@4.8.0
fetch@0.1.1
geojson-utils@1.0.10
hot-code-push@1.0.4
html-tools@1.0.11
htmljs@1.0.11
http@1.4.2
id-map@1.1.0
html-tools@1.1.2
htmljs@1.1.1
http@1.4.4
id-map@1.1.1
inter-process-messaging@0.1.1
launch-screen@1.2.0
livedata@1.0.18
launch-screen@1.3.0
lmieulet:meteor-coverage@3.2.0
logging@1.1.20
logging@1.2.0
meteor@1.9.3
meteor-base@1.4.0
meteor-base@1.5.1
meteortesting:browser-tests@1.3.4
meteortesting:mocha@2.0.1
meteortesting:mocha@2.0.2
meteortesting:mocha-core@8.0.1
minifier-css@1.5.1
minifier-js@2.6.0
minimongo@1.6.0
minifier-css@1.5.4
minifier-js@2.6.1
minimongo@1.7.0
mobile-experience@1.1.0
mobile-status-bar@1.1.0
modern-browsers@0.1.5
modules@0.15.0
modules@0.16.0
modules-runtime@0.12.0
mongo@1.10.0
mongo-decimal@0.1.1
mongo@1.12.0
mongo-decimal@0.1.2
mongo-dev-server@1.1.0
mongo-id@1.0.7
mongo-id@1.0.8
nathantreid:css-modules@4.1.0
npm-mongo@3.7.1
npm-mongo@3.9.1
ordered-dict@1.1.0
promise@0.11.2
promise@0.12.0
random@1.2.0
react-fast-refresh@0.1.1
react-meteor-data@0.2.16
reactive-dict@1.3.0
reactive-var@1.0.11
reload@1.3.0
reload@1.3.1
retry@1.1.0
rocketchat:streamer@1.1.0
routepolicy@1.1.0
routepolicy@1.1.1
session@1.2.0
shell-server@0.5.0
socket-stream-client@0.3.0
spacebars-compiler@1.1.3
standard-minifier-css@1.6.0
standard-minifier-js@2.6.0
static-html@1.2.2
templating-tools@1.1.2
socket-stream-client@0.4.0
spacebars-compiler@1.3.0
standard-minifier-css@1.7.3
standard-minifier-js@2.6.1
static-html@1.3.2
templating-tools@1.2.1
tmeasday:check-npm-versions@0.3.2
tracker@1.2.0
underscore@1.0.10
url@1.3.1
webapp@1.9.1
webapp-hashing@1.0.9
url@1.3.2
webapp@1.11.1
webapp-hashing@1.1.0

View File

@ -74,6 +74,12 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
[hidden]:not([hidden="false"]) {
display: none !important;
}
textarea::-webkit-input-placeholder,
input::-webkit-input-placeholder {
color: var(--palette-placeholder-text);
opacity: 1;
}
</style>
<script>
document.addEventListener('gesturestart', function (e) {

View File

@ -18,6 +18,7 @@
/* eslint no-unused-vars: 0 */
import './wdyr';
import '../imports/ui/services/collection-hooks/collection-hooks';
import React from 'react';
import { Meteor } from 'meteor/meteor';
@ -33,6 +34,10 @@ import ChatAdapter from '/imports/ui/components/components-data/chat-context/ada
import UsersAdapter from '/imports/ui/components/components-data/users-context/adapter';
import GroupChatAdapter from '/imports/ui/components/components-data/group-chat-context/adapter';
import('/imports/api/audio/client/bridge/bridge-whitelist').catch(() => {
// bridge loading
});
Meteor.startup(() => {
// Logs all uncaught exceptions to the client logger
window.addEventListener('error', (e) => {

View File

@ -45,7 +45,7 @@ export ROOT_URL=http://127.0.0.1/html5client
export MONGO_OPLOG_URL=mongodb://127.0.1.1/local
export MONGO_URL=mongodb://127.0.1.1/meteor
export NODE_ENV=production
export NODE_VERSION=node-v12.16.1-linux-x64
export NODE_VERSION=node-v14.17.6-linux-x64
export SERVER_WEBSOCKET_COMPRESSION=0
export BIND_IP=127.0.0.1
PORT=$PORT /usr/share/$NODE_VERSION/bin/node --max-old-space-size=2048 --max_semi_space_size=128 main.js NODEJS_BACKEND_INSTANCE_ID=$INSTANCE_ID

View File

@ -45,7 +45,7 @@ export ROOT_URL=http://127.0.0.1/html5client
export MONGO_OPLOG_URL=mongodb://127.0.1.1/local
export MONGO_URL=mongodb://127.0.1.1/meteor
export NODE_ENV=production
export NODE_VERSION=node-v12.16.1-linux-x64
export NODE_VERSION=node-v14.17.6-linux-x64
export SERVER_WEBSOCKET_COMPRESSION=0
export BIND_IP=127.0.0.1
PORT=$PORT /usr/share/$NODE_VERSION/bin/node --max-old-space-size=2048 --max_semi_space_size=128 main.js

View File

@ -0,0 +1,16 @@
/**
* Bridge whitelist, needed for dynamically importing bridges (as modules).
*
* The code is intentionally unreachable, but its trigger Meteor's static
* analysis, which makes bridge module available to build process.
*
* For new bridges, we must append an import statement here.
*
* More information here:
*https://docs.meteor.com/packages/dynamic-import.html
*/
throw new Error();
/* eslint-disable no-unreachable */
// BRIDGES LIST

View File

@ -303,3 +303,5 @@ export default class KurentoAudioBridge extends BaseAudioBridge {
return Promise.resolve();
}
}
module.exports = KurentoAudioBridge;

View File

@ -1495,3 +1495,5 @@ export default class SIPBridge extends BaseAudioBridge {
return this.activeSession.updateAudioConstraints(constraints);
}
}
module.exports = SIPBridge;

View File

@ -15,24 +15,51 @@ export default function handleBreakoutJoinURL({ body }) {
breakoutId,
};
// only keep each users' last invitation
const newUsers = [];
const currentBreakout = Breakouts.findOne({ breakoutId }, { fields: { users: 1 } });
currentBreakout.users.forEach((item) => {
if (item.userId !== userId) {
newUsers.push(item);
}
});
newUsers.push({
userId,
redirectToHtml5JoinURL,
insertedTime: new Date().getTime(),
});
const modifier = {
$push: {
users: {
userId,
redirectToHtml5JoinURL,
insertedTime: new Date().getTime(),
},
$set: {
users: newUsers,
},
};
try {
const { insertedId, numberAffected } = Breakouts.upsert(selector, modifier);
const ATTEMPT_EVERY_MS = 1000;
if (insertedId) {
Logger.info(`Added breakout id=${breakoutId}`);
} else if (numberAffected) {
let numberAffected = 0;
const updateBreakout = Meteor.bindEnvironment(() => {
numberAffected = Breakouts.update(selector, modifier);
});
const updateBreakoutPromise = new Promise((resolve) => {
const updateBreakoutInterval = setInterval(() => {
updateBreakout();
if (numberAffected) {
resolve(clearInterval(updateBreakoutInterval));
}
}, ATTEMPT_EVERY_MS);
});
updateBreakoutPromise.then(() => {
Logger.info(`Upserted breakout id=${breakoutId}`);
}
});
} catch (err) {
Logger.error(`Adding breakout to collection: ${err}`);
}

View File

@ -8,4 +8,9 @@ if (Meteor.isServer) {
UsersTyping._ensureIndex({ meetingId: 1, isTypingTo: 1 });
}
// As we store chat in context, skip adding to mini mongo
if (Meteor.isClient) {
GroupChatMsg.onAdded = () => false;
}
export { GroupChatMsg, UsersTyping };

View File

@ -1,6 +1,7 @@
import { Meteor } from 'meteor/meteor';
const Polls = new Mongo.Collection('polls');
export const CurrentPoll = new Mongo.Collection('current-poll');
if (Meteor.isServer) {
// We can have just one active poll per meeting

View File

@ -14,11 +14,11 @@ function currentPoll(secretPoll) {
});
if (
!tokenValidation ||
tokenValidation.validationStatus !== ValidationStates.VALIDATED
!tokenValidation
|| tokenValidation.validationStatus !== ValidationStates.VALIDATED
) {
Logger.warn(
`Publishing Polls was requested by unauth connection ${this.connection.id}`
`Publishing Polls was requested by unauth connection ${this.connection.id}`,
);
return Polls.find({ meetingId: '' });
}
@ -41,15 +41,16 @@ function currentPoll(secretPoll) {
if ((hasPoll && hasPoll.secretPoll) || secretPoll) {
options.fields.responses = 0;
}
return Polls.find(selector, options);
Mongo.Collection._publishCursor(Polls.find(selector, options), this, 'current-poll');
return this.ready();
}
Logger.warn(
'Publishing current-poll was requested by non-moderator connection',
{ meetingId, userId, connectionId: this.connection.id },
);
return Polls.find({ meetingId: '' });
Mongo.Collection._publishCursor(Polls.find({ meetingId: '' }), this, 'current-poll');
return this.ready();
}
function publishCurrentPoll(...args) {
@ -65,11 +66,11 @@ function polls() {
});
if (
!tokenValidation ||
tokenValidation.validationStatus !== ValidationStates.VALIDATED
!tokenValidation
|| tokenValidation.validationStatus !== ValidationStates.VALIDATED
) {
Logger.warn(
`Publishing Polls was requested by unauth connection ${this.connection.id}`
`Publishing Polls was requested by unauth connection ${this.connection.id}`,
);
return Polls.find({ meetingId: '' });
}

View File

@ -1,6 +1,7 @@
import { Meteor } from 'meteor/meteor';
const Users = new Mongo.Collection('users');
export const CurrentUser = new Mongo.Collection('current-user');
if (Meteor.isServer) {
// types of queries for the users:

View File

@ -9,7 +9,8 @@ const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
function currentUser() {
if (!this.userId) {
return Users.find({ meetingId: '' });
Mongo.Collection._publishCursor(Users.find({ meetingId: '' }), this, 'current-user');
return this.ready();
}
const { meetingId, requesterUserId } = extractCredentials(this.userId);
@ -28,8 +29,8 @@ function currentUser() {
authToken: false, // Not asking for authToken from client side but also not exposing it
},
};
return Users.find(selector, options);
Mongo.Collection._publishCursor(Users.find(selector, options), this, 'current-user');
return this.ready();
}
function publishCurrentUser(...args) {
@ -39,7 +40,7 @@ function publishCurrentUser(...args) {
Meteor.publish('current-user', publishCurrentUser);
function users(role) {
function users() {
const tokenValidation = AuthTokenValidation.findOne({ connectionId: this.connection.id });
if (!tokenValidation || tokenValidation.validationStatus !== ValidationStates.VALIDATED) {

View File

@ -19,7 +19,7 @@ import AudioService from '/imports/ui/components/audio/service';
import { notify } from '/imports/ui/services/notification';
import deviceInfo from '/imports/utils/deviceInfo';
import getFromUserSettings from '/imports/ui/services/users-settings';
import { LayoutContextFunc } from '../../ui/components/layout/context';
import { layoutSelectInput, layoutDispatch } from '../../ui/components/layout/context';
import VideoService from '/imports/ui/components/video-provider/service';
import DebugWindow from '/imports/ui/components/debug-window/component';
import { ACTIONS, PANELS } from '../../ui/components/layout/enums';
@ -78,7 +78,12 @@ class Base extends Component {
}
componentDidMount() {
const { animations } = this.props;
const { animations, usersVideo, layoutContextDispatch } = this.props;
layoutContextDispatch({
type: ACTIONS.SET_NUM_CAMERAS,
value: usersVideo.length,
});
const {
userID: localUserId,
@ -178,7 +183,7 @@ class Base extends Component {
isMeteorConnected,
subscriptionsReady,
layoutContextDispatch,
layoutContextState,
sidebarContentPanel,
usersVideo,
} = this.props;
const {
@ -186,10 +191,6 @@ class Base extends Component {
meetingExisted,
} = this.state;
const { input } = layoutContextState;
const { sidebarContent } = input;
const { sidebarContentPanel } = sidebarContent;
if (usersVideo !== prevProps.usersVideo) {
layoutContextDispatch({
type: ACTIONS.SET_NUM_CAMERAS,
@ -372,7 +373,15 @@ class Base extends Component {
Base.propTypes = propTypes;
Base.defaultProps = defaultProps;
const BaseContainer = withTracker(() => {
const BaseContainer = (props) => {
const sidebarContent = layoutSelectInput((i) => i.sidebarContent);
const { sidebarContentPanel } = sidebarContent;
const layoutContextDispatch = layoutDispatch();
return <Base {...{ sidebarContentPanel, layoutContextDispatch, ...props }} />;
};
export default withTracker(() => {
const {
animations,
} = Settings.application;
@ -505,6 +514,4 @@ const BaseContainer = withTracker(() => {
codeError,
usersVideo,
};
})(LayoutContextFunc.withContext(Base));
export default BaseContainer;
})(BaseContainer);

View File

@ -1,16 +1,15 @@
import React, { useContext } from 'react';
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import Presentations from '/imports/api/presentations';
import PresentationUploaderService from '/imports/ui/components/presentation/presentation-uploader/service';
import PresentationPodService from '/imports/ui/components/presentation-pod/service';
import ActionsDropdown from './component';
import LayoutContext from '../../layout/context';
import { layoutSelectInput, layoutDispatch } from '../../layout/context';
const ActionsDropdownContainer = (props) => {
const layoutContext = useContext(LayoutContext);
const { layoutContextState, layoutContextDispatch } = layoutContext;
const { input } = layoutContextState;
const { sidebarContent, sidebarNavigation } = input;
const sidebarContent = layoutSelectInput((i) => i.sidebarContent);
const sidebarNavigation = layoutSelectInput((i) => i.sidebarNavigation);
const layoutContextDispatch = layoutDispatch();
return (
<ActionsDropdown {...{

View File

@ -12,7 +12,7 @@ import Service from './service';
import UserListService from '/imports/ui/components/user-list/service';
import ExternalVideoService from '/imports/ui/components/external-video-player/service';
import CaptionsService from '/imports/ui/components/captions/service';
import LayoutContext from '../layout/context';
import { layoutSelectOutput, layoutDispatch } from '../layout/context';
import MediaService, {
getSwapLayout,
@ -20,12 +20,11 @@ import MediaService, {
} from '../media/service';
const ActionsBarContainer = (props) => {
const actionsBarStyle = layoutSelectOutput((i) => i.actionBar);
const layoutContextDispatch = layoutDispatch();
const usingUsersContext = useContext(UsersContext);
const { users } = usingUsersContext;
const layoutContext = useContext(LayoutContext);
const { layoutContextState, layoutContextDispatch } = layoutContext;
const { output } = layoutContextState;
const { actionBar: actionsBarStyle } = output;
const currentUser = { userId: Auth.userID, emoji: users[Auth.meetingID][Auth.userID].emoji };

View File

@ -127,7 +127,10 @@ const intlMessages = defineMessages({
id: 'app.createBreakoutRoom.minimumDurationWarnBreakout',
description: 'minimum duration warning message label',
},
roomNameInputDesc: {
id: 'app.createBreakoutRoom.roomNameInputDesc',
description: 'aria description for room name change',
}
});
const BREAKOUT_LIM = Meteor.settings.public.app.breakouts.breakoutRoomLimit;
@ -253,7 +256,6 @@ class BreakoutRoom extends PureComponent {
roomList.removeEventListener('keydown', this.handleMoveEvent, true);
}
}
this.handleDismiss();
}
handleShiftUser(activeListSibling) {
@ -373,7 +375,7 @@ class BreakoutRoom extends PureComponent {
return;
}
this.setState({ preventClosing: false });
this.handleDismiss();
const rooms = _.range(1, numberOfRooms + 1).map((seq) => ({
users: this.getUserByRoom(seq).map((u) => u.userId),
@ -403,7 +405,7 @@ class BreakoutRoom extends PureComponent {
breakoutUsers.forEach((user) => sendInvitation(breakoutId, user.userId));
});
this.setState({ preventClosing: false });
this.handleDismiss();
}
onAssignRandomly() {
@ -617,8 +619,8 @@ class BreakoutRoom extends PureComponent {
return (
<div className={styles.boxContainer} key="rooms-grid-" ref={(r) => { this.listOfUsers = r; }}>
<div className={!leastOneUserIsValid ? styles.changeToWarn : null}>
<p className={styles.freeJoinLabel}>
<div role="alert" className={!leastOneUserIsValid ? styles.changeToWarn : null}>
<span className={styles.freeJoinLabel}>
<input
type="text"
readOnly
@ -627,7 +629,7 @@ class BreakoutRoom extends PureComponent {
intl.formatMessage(intlMessages.notAssigned, { 0: this.getUserByRoom(0).length })
}
/>
</p>
</span>
<div className={styles.breakoutBox} onDrop={drop(0)} onDragOver={allowDrop} tabIndex={0}>
{this.renderUserItemByRoom(0)}
</div>
@ -638,7 +640,7 @@ class BreakoutRoom extends PureComponent {
{
_.range(1, rooms + 1).map((value) => (
<div key={`room-${value}`}>
<p className={styles.freeJoinLabel}>
<span className={styles.freeJoinLabel}>
<input
type="text"
maxLength="255"
@ -648,9 +650,13 @@ class BreakoutRoom extends PureComponent {
value={this.getRoomName(value)}
onChange={changeRoomName(value)}
onBlur={changeRoomName(value)}
aria-label={intl.formatMessage(intlMessages.duration)}
aria-label={`${this.getRoomName(value)}`}
aria-describedby={this.getRoomName(value).length === 0 ? `room-error-${value}` : `room-input-${value}`}
/>
</p>
<div aria-hidden id={`room-input-${value}`} className={"sr-only"}>
{intl.formatMessage(intlMessages.roomNameInputDesc)}
</div>
</span>
<div className={styles.breakoutBox} onDrop={drop(value)} onDragOver={allowDrop} tabIndex={0}>
{this.renderUserItemByRoom(value)}
{isInvitation && this.renderJoinedUsers(value)}
@ -661,7 +667,7 @@ class BreakoutRoom extends PureComponent {
</span>
) : null}
{this.getRoomName(value).length === 0 ? (
<span className={styles.spanWarn}>
<span aria-hidden id={`room-error-${value}`} className={styles.spanWarn}>
{intl.formatMessage(intlMessages.roomNameEmptyIsValid)}
</span>
) : null}
@ -787,7 +793,7 @@ class BreakoutRoom extends PureComponent {
>
{intl.formatMessage(intlMessages.numberOfRoomsIsValid)}
</span>
<span id="randomlyAssignDesc" className="sr-only">
<span aria-hidden id="randomlyAssignDesc" className="sr-only">
{intl.formatMessage(intlMessages.randomlyAssignDesc)}
</span>
</React.Fragment>

View File

@ -1,13 +1,13 @@
import React, { useContext } from 'react';
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import { injectIntl } from 'react-intl';
import QuickPollDropdown from './component';
import LayoutContext from '../../layout/context';
import { layoutDispatch } from '../../layout/context';
import PollService from '/imports/ui/components/poll/service';
const QuickPollDropdownContainer = (props) => {
const layoutContext = useContext(LayoutContext);
const { layoutContextDispatch } = layoutContext;
const layoutContextDispatch = layoutDispatch();
return <QuickPollDropdown {...{ layoutContextDispatch, ...props }} />;
};

View File

@ -175,6 +175,7 @@ const ScreenshareButton = ({
className={cx(isVideoBroadcasting || styles.btn)}
disabled={(!isMeteorConnected && !isVideoBroadcasting) || !screenshareDataSavingSetting}
icon={isVideoBroadcasting ? 'desktop' : 'desktop_off'}
data-test={isVideoBroadcasting ? 'stopScreenShare' : 'startScreenShare'}
label={intl.formatMessage(vLabel)}
description={intl.formatMessage(vDescr)}
color={isVideoBroadcasting ? 'primary' : 'default'}

View File

@ -29,16 +29,11 @@ import PresentationAreaContainer from '../presentation/presentation-area/contain
import ScreenshareContainer from '../screenshare/container';
import ExternalVideoContainer from '../external-video-player/container';
import { styles } from './styles';
import {
LAYOUT_TYPE, DEVICE_TYPE, ACTIONS,
} from '../layout/enums';
import { DEVICE_TYPE, ACTIONS } from '../layout/enums';
import {
isMobile, isTablet, isTabletPortrait, isTabletLandscape, isDesktop,
} from '../layout/utils';
import CustomLayout from '../layout/layout-manager/customLayout';
import SmartLayout from '../layout/layout-manager/smartLayout';
import PresentationFocusLayout from '../layout/layout-manager/presentationFocusLayout';
import VideoFocusLayout from '../layout/layout-manager/videoFocusLayout';
import LayoutEngine from '../layout/layout-manager/layoutEngine';
import NavBarContainer from '../nav-bar/container';
import SidebarNavigationContainer from '../sidebar-navigation/container';
import SidebarContentContainer from '../sidebar-content/container';
@ -47,6 +42,7 @@ import ConnectionStatusService from '/imports/ui/components/connection-status/se
import { NAVBAR_HEIGHT, LARGE_NAVBAR_HEIGHT } from '/imports/ui/components/layout/defaultValues';
import Settings from '/imports/ui/services/settings';
import LayoutService from '/imports/ui/components/layout/service';
import { registerTitleView } from '/imports/utils/dom-utils';
const MOBILE_MEDIA = 'only screen and (max-width: 40em)';
const APP_CONFIG = Meteor.settings.public.app;
@ -99,6 +95,10 @@ const intlMessages = defineMessages({
id: 'app.whiteboard.annotations.poll',
description: 'message displayed when a poll is published',
},
defaultViewLabel: {
id: 'app.title.defaultViewLabel',
description: 'view name apended to document title',
},
});
const propTypes = {
@ -153,12 +153,14 @@ class App extends Component {
const { browserName } = browserInfo;
const { osName } = deviceInfo;
registerTitleView(intl.formatMessage(intlMessages.defaultViewLabel));
layoutContextDispatch({
type: ACTIONS.SET_IS_RTL,
value: isRTL,
});
MediaService.setSwapLayout();
MediaService.setSwapLayout(layoutContextDispatch);
Modal.setAppElement('#app');
const fontSize = isMobile() ? MOBILE_FONT_SIZE : DESKTOP_FONT_SIZE;
@ -443,22 +445,6 @@ class App extends Component {
) : null);
}
renderLayoutManager() {
const { layoutType } = this.props;
switch (layoutType) {
case LAYOUT_TYPE.CUSTOM_LAYOUT:
return <CustomLayout />;
case LAYOUT_TYPE.SMART_LAYOUT:
return <SmartLayout />;
case LAYOUT_TYPE.PRESENTATION_FOCUS:
return <PresentationFocusLayout />;
case LAYOUT_TYPE.VIDEO_FOCUS:
return <VideoFocusLayout />;
default:
return <CustomLayout />;
}
}
render() {
const {
customStyle,
@ -469,11 +455,12 @@ class App extends Component {
shouldShowScreenshare,
shouldShowExternalVideo,
isPresenter,
layoutType,
} = this.props;
return (
<>
{this.renderLayoutManager()}
<LayoutEngine layoutType={layoutType} />
<div
id="layout"
className={styles.layout}

View File

@ -1,4 +1,4 @@
import React, { useContext } from 'react';
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import { defineMessages, injectIntl } from 'react-intl';
import PropTypes from 'prop-types';
@ -12,9 +12,14 @@ import CaptionsService from '/imports/ui/components/captions/service';
import getFromUserSettings from '/imports/ui/services/users-settings';
import deviceInfo from '/imports/utils/deviceInfo';
import UserInfos from '/imports/api/users-infos';
import LayoutContext from '../layout/context';
import Settings from '/imports/ui/services/settings';
import MediaService from '/imports/ui/components/media/service';
import {
layoutSelect,
layoutSelectInput,
layoutSelectOutput,
layoutDispatch
} from '../layout/context';
import {
getFontSize,
@ -51,29 +56,31 @@ const endMeeting = (code) => {
};
const AppContainer = (props) => {
const layoutContext = useContext(LayoutContext);
const { layoutContextState, layoutContextDispatch } = layoutContext;
const {
actionsbar,
meetingLayout,
settingsLayout,
pushLayoutToEveryone,
currentUserId,
shouldShowPresentation: propsShouldShowPresentation,
presentationRestoreOnUpdate,
...otherProps
} = props;
const {
input,
output,
layoutType,
deviceType,
} = layoutContextState;
const { sidebarContent, sidebarNavigation } = input;
const { actionBar: actionsBarStyle, captions: captionsStyle } = output;
const { sidebarNavPanel } = sidebarNavigation;
const { sidebarContentPanel } = sidebarContent;
const sidebarNavigationIsOpen = sidebarNavigation.isOpen;
const sidebarContentIsOpen = sidebarContent.isOpen;
const sidebarContent = layoutSelectInput((i) => i.sidebarContent);
const sidebarNavigation = layoutSelectInput((i) => i.sidebarNavigation);
const actionsBarStyle = layoutSelectOutput((i) => i.actionBar);
const captionsStyle = layoutSelectOutput((i) => i.captions);
const presentation = layoutSelectInput((i) => i.presentation);
const layoutType = layoutSelect((i) => i.layoutType);
const deviceType = layoutSelect((i) => i.deviceType);
const layoutContextDispatch = layoutDispatch();
const { sidebarContentPanel, isOpen: sidebarContentIsOpen } = sidebarContent;
const { sidebarNavPanel, isOpen: sidebarNavigationIsOpen } = sidebarNavigation;
const { isOpen: presentationIsOpen } = presentation;
const shouldShowPresentation = propsShouldShowPresentation
&& (presentationIsOpen || presentationRestoreOnUpdate);
return currentUserId
? (
@ -93,6 +100,7 @@ const AppContainer = (props) => {
sidebarNavigationIsOpen,
sidebarContentPanel,
sidebarContentIsOpen,
shouldShowPresentation,
}}
{...otherProps}
/>
@ -162,7 +170,7 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
const { viewScreenshare } = Settings.dataSaving;
const shouldShowExternalVideo = MediaService.shouldShowExternalVideo();
const shouldShowScreenshare = MediaService.shouldShowScreenshare()
&& (viewScreenshare || MediaService.isUserPresenter()) && !shouldShowExternalVideo;
&& (viewScreenshare || MediaService.isUserPresenter());
let customStyleUrl = getFromUserSettings('bbb_custom_style_url', false);
if (!customStyleUrl && CUSTOM_STYLE_URL) {
@ -195,6 +203,10 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
shouldShowPresentation: !shouldShowScreenshare && !shouldShowExternalVideo,
shouldShowExternalVideo,
isLargeFont: Session.get('isLargeFont'),
presentationRestoreOnUpdate: getFromUserSettings(
'bbb_force_restore_presentation_on_new_events',
Meteor.settings.public.presentation.restoreOnUpdate,
),
};
})(AppContainer)));

View File

@ -79,6 +79,7 @@ class AudioControls extends PureComponent {
hideLabel
aria-label={intl.formatMessage(intlMessages.joinAudio)}
label={intl.formatMessage(intlMessages.joinAudio)}
data-test="joinAudio"
color="default"
ghost
icon="audio_off"

View File

@ -290,6 +290,7 @@ class InputStreamLiveSelector extends Component {
aria-label={intl.formatMessage(intlMessages.leaveAudio)}
label={intl.formatMessage(intlMessages.leaveAudio)}
accessKey={shortcuts.leaveaudio}
data-test="leaveAudio"
hideLabel
color="primary"
icon={isListenOnly ? 'listen' : 'audio_on'}

View File

@ -187,7 +187,7 @@ class AudioModal extends Component {
if (autoplayBlocked !== prevProps.autoplayBlocked) {
if (autoplayBlocked) {
this.setContent('autoplayBlocked');
this.setContent({ content: 'autoplayBlocked' });
} else {
closeModal();
}

View File

@ -1,15 +1,13 @@
import React, { useContext } from 'react';
import React from 'react';
import { Session } from 'meteor/session';
import { withTracker } from 'meteor/react-meteor-data';
import BannerComponent from './component';
import LayoutContext from '../layout/context';
import { layoutSelectInput, layoutDispatch } from '../layout/context';
const BannerContainer = (props) => {
const layoutContext = useContext(LayoutContext);
const { layoutContextState, layoutContextDispatch } = layoutContext;
const { input } = layoutContextState;
const { bannerBar } = input;
const bannerBar = layoutSelectInput((i) => i.bannerBar);
const { hasBanner } = bannerBar;
const layoutContextDispatch = layoutDispatch();
return <BannerComponent {...{ hasBanner, layoutContextDispatch, ...props }} />;
};

View File

@ -119,6 +119,10 @@ class BreakoutRoom extends PureComponent {
};
}
componentDidMount() {
if (this.panel) this.panel.firstChild.focus();
}
componentDidUpdate() {
const {
breakoutRoomUser,
@ -167,6 +171,7 @@ class BreakoutRoom extends PureComponent {
this.setState(
{
waiting: true,
generated: false,
requestedBreakoutId: breakoutId,
},
() => requestJoinURL(breakoutId),
@ -309,7 +314,7 @@ class BreakoutRoom extends PureComponent {
<Button
label={this.getBreakoutLabel(breakoutId)}
data-test="breakoutJoin"
aria-label={`${intl.formatMessage(intlMessages.breakoutJoin)} ${number}`}
aria-label={`${this.getBreakoutLabel(breakoutId)} ${this.props.breakoutRooms[number - 1]?.shortName }`}
onClick={() => {
this.getBreakoutURL(breakoutId);
// leave main room's audio,
@ -513,7 +518,7 @@ class BreakoutRoom extends PureComponent {
amIModerator,
} = this.props;
return (
<div className={styles.panel}>
<div className={styles.panel} ref={(n) => this.panel = n}>
<Button
icon="left_arrow"
label={intl.formatMessage(intlMessages.breakoutTitle)}

View File

@ -1,14 +1,14 @@
import React, { useContext } from 'react';
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import AudioService from '/imports/ui/components/audio/service';
import AudioManager from '/imports/ui/services/audio-manager';
import BreakoutComponent from './component';
import Service from './service';
import LayoutContext from '../layout/context';
import { layoutDispatch } from '../layout/context';
const BreakoutContainer = (props) => {
const layoutContext = useContext(LayoutContext);
const { layoutContextDispatch } = layoutContext;
const layoutContextDispatch = layoutDispatch();
return <BreakoutComponent {...{ layoutContextDispatch, ...props }} />;
};

View File

@ -1,18 +1,16 @@
import React, { useContext } from 'react';
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import { Session } from 'meteor/session';
import CaptionsService from '/imports/ui/components/captions/service';
import Pad from './component';
import Auth from '/imports/ui/services/auth';
import LayoutContext from '../../layout/context';
import { layoutSelectInput, layoutDispatch } from '../../layout/context';
import { ACTIONS, PANELS } from '../../layout/enums';
const PadContainer = (props) => {
const layoutContext = useContext(LayoutContext);
const { layoutContextDispatch, layoutContextState } = layoutContext;
const { input } = layoutContextState;
const { cameraDock } = input;
const cameraDock = layoutSelectInput((i) => i.cameraDock);
const { isResizing } = cameraDock;
const layoutContextDispatch = layoutDispatch();
const {
amIModerator,

View File

@ -3,15 +3,15 @@ import { withTracker } from 'meteor/react-meteor-data';
import { withModalMounter } from '/imports/ui/components/modal/service';
import CaptionsService from '/imports/ui/components/captions/service';
import WriterMenu from './component';
import LayoutContext from '../../layout/context';
import { layoutDispatch } from '../../layout/context';
import Auth from '/imports/ui/services/auth';
import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
const WriterMenuContainer = (props) => {
const layoutContext = useContext(LayoutContext);
const { layoutContextDispatch } = layoutContext;
const layoutContextDispatch = layoutDispatch();
const usingUsersContext = useContext(UsersContext);
const { users } = usingUsersContext;
const currentUser = users[Auth.meetingID][Auth.userID];

View File

@ -1,7 +1,7 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import ChatAlert from './component';
import LayoutContext from '../../layout/context';
import { layoutSelect, layoutSelectInput, layoutDispatch } from '../../layout/context';
import { PANELS } from '../../layout/enums';
import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
import { ChatContext } from '/imports/ui/components/components-data/chat-context/context';
@ -17,11 +17,11 @@ const propTypes = {
};
const ChatAlertContainer = (props) => {
const layoutContext = useContext(LayoutContext);
const { layoutContextState, layoutContextDispatch } = layoutContext;
const { idChatOpen, input } = layoutContextState;
const { sidebarContent } = input;
const idChatOpen = layoutSelect((i) => i.idChatOpen);
const sidebarContent = layoutSelectInput((i) => i.sidebarContent);
const { sidebarContentPanel } = sidebarContent;
const layoutContextDispatch = layoutDispatch();
const { audioAlertEnabled, pushAlertEnabled } = props;
let idChat = idChatOpen;

View File

@ -5,6 +5,8 @@ import _ from 'lodash';
import BBBMenu from "/imports/ui/components/menu/component";
import Button from '/imports/ui/components/button/component';
import { alertScreenReader } from '/imports/utils/dom-utils';
import ChatService from '../service';
const intlMessages = defineMessages({
@ -20,6 +22,14 @@ const intlMessages = defineMessages({
id: 'app.chat.dropdown.copy',
description: 'Copy button label',
},
copySuccess: {
id: 'app.chat.copySuccess',
description: 'aria success alert',
},
copyErr: {
id: 'app.chat.copyErr',
description: 'aria error alert',
},
options: {
id: 'app.chat.dropdown.options',
description: 'Chat Options',
@ -92,7 +102,11 @@ class ChatDropdown extends PureComponent {
label: intl.formatMessage(intlMessages.copy),
onClick: () => {
let chatHistory = ChatService.exportChat(timeWindowsValues, users, intl);
navigator.clipboard.writeText(chatHistory);
navigator.clipboard.writeText(chatHistory).then(() => {
alertScreenReader(intl.formatMessage(intlMessages.copySuccess));
}).catch(() => {
alertScreenReader(intl.formatMessage(intlMessages.copyErr));
});
}
}
)
@ -121,6 +135,7 @@ class ChatDropdown extends PureComponent {
if (!amIModerator && !ENABLE_SAVE_AND_COPY_PUBLIC_CHAT) return null;
return (
<>
<BBBMenu
trigger={
<Button
@ -148,6 +163,7 @@ class ChatDropdown extends PureComponent {
}}
actions={this.getAvailableActions()}
/>
</>
);
}
}

View File

@ -12,7 +12,7 @@ import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
import lockContextContainer from '/imports/ui/components/lock-viewers/context/container';
import Chat from '/imports/ui/components/chat/component';
import ChatService from './service';
import { LayoutContextFunc } from '../layout/context';
import { layoutSelect, layoutDispatch } from '../layout/context';
const CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_CHAT_KEY = CHAT_CONFIG.public_id;
@ -71,11 +71,12 @@ const ChatContainer = (props) => {
isChatLockedPublic,
isChatLockedPrivate,
users: propUsers,
layoutContextState,
layoutContextDispatch,
...restProps
} = props;
const { idChatOpen } = layoutContextState;
const idChatOpen = layoutSelect((i) => i.idChatOpen);
const layoutContextDispatch = layoutDispatch();
const isPublicChat = idChatOpen === PUBLIC_CHAT_KEY;
const chatID = idChatOpen;
@ -264,4 +265,4 @@ export default lockContextContainer(injectIntl(withTracker(({ intl, userLocks })
handleClosePrivateChat: ChatService.closePrivateChat,
},
};
})(LayoutContextFunc.withConsumer(ChatContainer))));
})(ChatContainer)));

View File

@ -1,17 +1,16 @@
import React, { useContext } from 'react';
import React from 'react';
import _ from 'lodash';
import { makeCall } from '/imports/ui/services/api';
import MessageForm from './component';
import ChatService from '/imports/ui/components/chat/service';
import LayoutContext from '../../layout/context';
import { layoutSelect } from '../../layout/context';
const CHAT_CONFIG = Meteor.settings.public.chat;
const START_TYPING_THROTTLE_INTERVAL = 2000;
const MessageFormContainer = (props) => {
const layoutContext = useContext(LayoutContext);
const { layoutContextState } = layoutContext;
const { idChatOpen } = layoutContextState;
const idChatOpen = layoutSelect((i) => i.idChatOpen);
const handleSendMessage = (message) => {
ChatService.setUserSentMessage(true);
return ChatService.sendGroupMessage(message, idChatOpen);

View File

@ -2,7 +2,7 @@ import React, { useContext } from 'react';
import TimeWindowChatItem from './component';
import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
import ChatService from '../../service';
import LayoutContext from '../../../layout/context';
import { layoutSelect } from '../../../layout/context';
import PollService from '/imports/ui/components/poll/service';
import Auth from '/imports/ui/services/auth';
@ -12,9 +12,9 @@ const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
const TimeWindowChatItemContainer = (props) => {
const { message, messageId } = props;
const layoutContext = useContext(LayoutContext);
const { layoutContextState } = layoutContext;
const { idChatOpen } = layoutContextState;
const idChatOpen = layoutSelect((i) => i.idChatOpen);
const usingUsersContext = useContext(UsersContext);
const { users } = usingUsersContext;
const {

View File

@ -125,7 +125,7 @@
flex-shrink: 0;
flex-grow: 0;
flex-basis: 3.5rem;
color: var(--color-gray-light);
color: var(--palette-placeholder-text);
text-transform: uppercase;
font-size: 75%;
margin: 0 0 0 calc(var(--line-height-computed) / 2);

View File

@ -5,6 +5,7 @@ import { UsersContext } from '../users-context/context';
import { makeCall } from '/imports/ui/services/api';
import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
import Auth from '/imports/ui/services/auth';
import CollectionEventsBroker from '/imports/ui/services/collection-hooks-callbacks/collection-hooks-callbacks';
let prevUserData = {};
let currentUserData = {};
@ -119,23 +120,19 @@ const Adapter = () => {
});
}, 1000, { trailing: true, leading: true });
Meteor.connection._stream.socket.addEventListener('message', (msg) => {
if (msg.data.indexOf('{"msg":"added","collection":"group-chat-msg"') !== -1) {
const parsedMsg = JSON.parse(msg.data);
if (parsedMsg.msg === 'added') {
const { fields } = parsedMsg;
if (fields.id === `${SYSTEM_CHAT_TYPE}-${CHAT_CLEAR_MESSAGE}`) {
messageQueue = [];
dispatch({
type: ACTIONS.REMOVED,
});
}
messageQueue.push(fields);
throttledDispatch();
}
const insertToContext = (fields) => {
if (fields.id === `${SYSTEM_CHAT_TYPE}-${CHAT_CLEAR_MESSAGE}`) {
messageQueue = [];
dispatch({
type: ACTIONS.REMOVED,
});
}
});
messageQueue.push(fields);
throttledDispatch();
};
CollectionEventsBroker.addListener('group-chat-msg', 'added', insertToContext);
}, [Meteor.status().connected, Meteor.connection._lastSessionId]);
return null;

View File

@ -1,5 +1,5 @@
import { useContext, useEffect } from 'react';
import Users from '/imports/api/users';
import Users, { CurrentUser } from '/imports/api/users';
import UsersPersistentData from '/imports/api/users-persistent-data';
import { UsersContext, ACTIONS } from './context';
import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
@ -8,11 +8,11 @@ const Adapter = () => {
const usingUsersContext = useContext(UsersContext);
const { dispatch } = usingUsersContext;
useEffect(()=> {
useEffect(() => {
const usersPersistentDataCursor = UsersPersistentData.find({}, { sort: { timestamp: 1 } });
usersPersistentDataCursor.observe({
added: (obj) => {
ChatLogger.debug("usersAdapter::observe::added_persistent_user", obj);
ChatLogger.debug('usersAdapter::observe::added_persistent_user', obj);
dispatch({
type: ACTIONS.ADDED_USER_PERSISTENT_DATA,
value: {
@ -21,7 +21,7 @@ const Adapter = () => {
});
},
changed: (obj) => {
ChatLogger.debug("usersAdapter::observe::changed_persistent_user", obj);
ChatLogger.debug('usersAdapter::observe::changed_persistent_user', obj);
dispatch({
type: ACTIONS.CHANGED_USER_PERSISTENT_DATA,
value: {
@ -29,15 +29,16 @@ const Adapter = () => {
},
});
},
removed: (obj) => {},
removed: () => {},
});
}, []);
useEffect(() => {
const usersCursor = Users.find({}, { sort: { timestamp: 1 } });
const CurrentUserCursor = CurrentUser.find({});
usersCursor.observe({
added: (obj) => {
ChatLogger.debug("usersAdapter::observe::added", obj);
ChatLogger.debug('usersAdapter::observe::added', obj);
dispatch({
type: ACTIONS.ADDED,
value: {
@ -54,6 +55,18 @@ const Adapter = () => {
});
},
});
CurrentUserCursor.observe({
added: (obj) => {
ChatLogger.debug('usersAdapter::observe::current-user::added', obj);
dispatch({
type: ACTIONS.ADDED,
value: {
user: obj,
},
});
},
});
}, []);
return null;

View File

@ -321,7 +321,7 @@ class ConnectionStatusComponent extends PureComponent {
{conn.offline ? ` (${intl.formatMessage(intlMessages.offline)})` : null}
</div>
</div>
<div className={styles.status}>
<div aria-label={`${intl.formatMessage(intlMessages.title)} ${conn.level}`} className={styles.status}>
<div className={styles.icon}>
<Icon level={conn.level} />
</div>

View File

@ -2,12 +2,13 @@ import React from 'react';
import { ChatContextProvider } from '/imports/ui/components/components-data/chat-context/context';
import { UsersContextProvider } from '/imports/ui/components/components-data/users-context/context';
import { GroupChatContextProvider } from '/imports/ui/components/components-data/group-chat-context/context';
import { LayoutContextProvider } from '/imports/ui/components/layout/context';
const providersList = [
ChatContextProvider,
GroupChatContextProvider,
UsersContextProvider,
LayoutContextProvider,
];
const ContextProvidersComponent = props => providersList.reduce((acc, Component) => (

View File

@ -1,18 +1,17 @@
import React, { useContext } from 'react';
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import { Session } from 'meteor/session';
import { getVideoUrl } from './service';
import ExternalVideoComponent from './component';
import LayoutContext from '../layout/context';
import { layoutSelectInput, layoutSelectOutput, layoutDispatch } from '../layout/context';
import MediaService, { getSwapLayout } from '/imports/ui/components/media/service';
const ExternalVideoContainer = (props) => {
const layoutManager = useContext(LayoutContext);
const { layoutContextState, layoutContextDispatch } = layoutManager;
const { output, input } = layoutContextState;
const { externalVideo } = output;
const { cameraDock } = input;
const externalVideo = layoutSelectOutput((i) => i.externalVideo);
const cameraDock = layoutSelectInput((i) => i.cameraDock);
const { isResizing } = cameraDock;
const layoutContextDispatch = layoutDispatch();
return (
<ExternalVideoComponent
{

View File

@ -1,17 +1,16 @@
import React, { useContext } from 'react';
import React from 'react';
import FullscreenButtonComponent from './component';
import LayoutContext from '../layout/context';
import { layoutSelect, layoutDispatch } from '../layout/context';
const FullscreenButtonContainer = (props) => <FullscreenButtonComponent {...props} />;
export default (props) => {
const isIphone = !!(navigator.userAgent.match(/iPhone/i));
const layoutContext = useContext(LayoutContext);
const { layoutContextState, layoutContextDispatch } = layoutContext;
const { fullscreen } = layoutContextState;
const fullscreen = layoutSelect((i) => i.fullscreen);
const { element: currentElement, group: currentGroup } = fullscreen;
const isFullscreen = !!currentElement;
const layoutContextDispatch = layoutDispatch();
return (
<FullscreenButtonContainer

View File

@ -7,7 +7,7 @@ import { setCustomLogoUrl, setModeratorOnlyMessage } from '/imports/ui/component
import { makeCall } from '/imports/ui/services/api';
import logger from '/imports/startup/client/logger';
import LoadingScreen from '/imports/ui/components/loading-screen/component';
import Users from '/imports/api/users';
import { CurrentUser } from '/imports/api/users';
const propTypes = {
children: PropTypes.element.isRequired,
@ -162,7 +162,7 @@ class JoinHandler extends Component {
return new Promise((resolve) => {
if (customdata.length) {
makeCall('addUserSettings', customdata).then(r => resolve(r));
makeCall('addUserSettings', customdata).then((r) => resolve(r));
}
resolve(true);
});
@ -190,8 +190,8 @@ class JoinHandler extends Component {
setModOnlyMessage(response);
Tracker.autorun(async (cd) => {
const user = Users.findOne({ userId: Auth.userID, approved: true }, { fields: { _id: 1 } });
const user = CurrentUser
.findOne({ userId: Auth.userID, approved: true }, { fields: { _id: 1 } });
if (user) {
await setCustomData(response);
cd.stop();

View File

@ -1,4 +1,5 @@
import React, { createContext, useReducer } from 'react';
import React, { useReducer } from 'react';
import { createContext, useContextSelector } from 'use-context-selector';
import PropTypes from 'prop-types';
import { ACTIONS } from '/imports/ui/components/layout/enums';
import DEFAULT_VALUES from '/imports/ui/components/layout/defaultValues';
@ -24,7 +25,7 @@ const providerPropTypes = {
]).isRequired,
};
const LayoutContext = createContext();
const LayoutContextSelector = createContext();
const initState = {
deviceType: null,
@ -333,12 +334,12 @@ const reducer = (state, action) => {
} = action.value;
const { sidebarNavigation } = state.output;
if (sidebarNavigation.display === display
&& sidebarNavigation.minWidth === width
&& sidebarNavigation.maxWidth === width
&& sidebarNavigation.minWidth === minWidth
&& sidebarNavigation.maxWidth === maxWidth
&& sidebarNavigation.width === width
&& sidebarNavigation.minHeight === height
&& sidebarNavigation.minHeight === minHeight
&& sidebarNavigation.height === height
&& sidebarNavigation.maxHeight === height
&& sidebarNavigation.maxHeight === maxHeight
&& sidebarNavigation.top === top
&& sidebarNavigation.left === left
&& sidebarNavigation.right === right
@ -1129,37 +1130,40 @@ const reducer = (state, action) => {
}
};
const ContextProvider = (props) => {
const LayoutContextProvider = (props) => {
const [layoutContextState, layoutContextDispatch] = useReducer(reducer, initState);
const { children } = props;
return (
<LayoutContext.Provider value={{
layoutContextState,
layoutContextDispatch,
}}
<LayoutContextSelector.Provider value={
[
layoutContextState,
layoutContextDispatch,
]
}
>
{children}
</LayoutContext.Provider>
</LayoutContextSelector.Provider>
);
};
ContextProvider.propTypes = providerPropTypes;
LayoutContextProvider.propTypes = providerPropTypes;
const withProvider = (Component) => (props) => (
<ContextProvider>
<Component {...props} />
</ContextProvider>
);
const withConsumer = (Component) => (props) => (
<LayoutContext.Consumer>
{(contexts) => <Component {...props} {...contexts} />}
</LayoutContext.Consumer>
);
export default LayoutContext;
export const LayoutContextFunc = {
withProvider,
withConsumer,
withContext: (Component) => withProvider(withConsumer(Component)),
const layoutSelect = (selector) => {
return useContextSelector(LayoutContextSelector, layout => selector(layout[0]));
};
const layoutSelectInput = (selector) => {
return useContextSelector(LayoutContextSelector, layout => selector(layout[0].input));
};
const layoutSelectOutput = (selector) => {
return useContextSelector(LayoutContextSelector, layout => selector(layout[0].output));
};
const layoutDispatch = () => {
return useContextSelector(LayoutContextSelector, layout => layout[1]);
};
export {
LayoutContextProvider,
layoutSelect,
layoutSelectInput,
layoutSelectOutput,
layoutDispatch,
}

View File

@ -1,28 +1,47 @@
import { Component } from 'react';
import { useEffect, useRef } from 'react';
import _ from 'lodash';
import { LayoutContextFunc } from '/imports/ui/components/layout/context';
import { layoutSelect, layoutSelectInput, layoutDispatch } from '/imports/ui/components/layout/context';
import DEFAULT_VALUES from '/imports/ui/components/layout/defaultValues';
import { INITIAL_INPUT_STATE } from '/imports/ui/components/layout/initState';
import {
DEVICE_TYPE, ACTIONS, CAMERADOCK_POSITION, PANELS,
} from '../enums';
import { ACTIONS, CAMERADOCK_POSITION, PANELS } from '../enums';
const windowWidth = () => window.document.documentElement.clientWidth;
const windowHeight = () => window.document.documentElement.clientHeight;
const min = (value1, value2) => (value1 <= value2 ? value1 : value2);
const max = (value1, value2) => (value1 >= value2 ? value1 : value2);
class CustomLayout extends Component {
constructor(props) {
super(props);
const CustomLayout = (props) => {
const { bannerAreaHeight, calculatesActionbarHeight, isMobile } = props;
this.throttledCalculatesLayout = _.throttle(() => this.calculatesLayout(),
50, { trailing: true, leading: true });
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
componentDidMount() {
this.init();
const { layoutContextDispatch } = this.props;
const input = layoutSelect((i) => i.input);
const deviceType = layoutSelect((i) => i.deviceType);
const isRTL = layoutSelect((i) => i.isRTL);
const fullscreen = layoutSelect((i) => i.fullscreen);
const fontSize = layoutSelect((i) => i.fontSize);
const currentPanelType = layoutSelect((i) => i.currentPanelType);
const presentationInput = layoutSelectInput((i) => i.presentation);
const sidebarNavigationInput = layoutSelectInput((i) => i.sidebarNavigation);
const sidebarContentInput = layoutSelectInput((i) => i.sidebarContent);
const cameraDockInput = layoutSelectInput((i) => i.cameraDock);
const actionbarInput = layoutSelectInput((i) => i.actionBar);
const navbarInput = layoutSelectInput((i) => i.navBar);
const layoutContextDispatch = layoutDispatch();
const prevDeviceType = usePrevious(deviceType);
const throttledCalculatesLayout = _.throttle(() => calculatesLayout(),
50, { trailing: true, leading: true });
useEffect(() => {
window.addEventListener('resize', () => {
layoutContextDispatch({
type: ACTIONS.SET_BROWSER_SIZE,
@ -32,43 +51,22 @@ class CustomLayout extends Component {
},
});
});
}
}, []);
shouldComponentUpdate(nextProps) {
const { layoutContextState } = this.props;
return layoutContextState.input !== nextProps.layoutContextState.input
|| layoutContextState.deviceType !== nextProps.layoutContextState.deviceType
|| layoutContextState.isRTL !== nextProps.layoutContextState.isRTL
|| layoutContextState.fontSize !== nextProps.layoutContextState.fontSize
|| layoutContextState.fullscreen !== nextProps.layoutContextState.fullscreen;
}
useEffect(() => {
if (deviceType === null) return;
componentDidUpdate(prevProps) {
const { layoutContextState } = this.props;
const { deviceType } = layoutContextState;
if (prevProps.layoutContextState.deviceType !== deviceType) {
this.init();
if (deviceType !== prevDeviceType) {
// reset layout if deviceType changed
// not all options is supported in all devices
init();
} else {
this.throttledCalculatesLayout();
throttledCalculatesLayout();
}
}
}, [input, deviceType, isRTL, fontSize, fullscreen]);
bannerAreaHeight() {
const { layoutContextState } = this.props;
const { input } = layoutContextState;
const { bannerBar, notificationsBar } = input;
const bannerHeight = bannerBar.hasBanner ? DEFAULT_VALUES.bannerHeight : 0;
const notificationHeight = notificationsBar.hasNotification ? DEFAULT_VALUES.bannerHeight : 0;
return bannerHeight + notificationHeight;
}
calculatesDropAreas(sidebarNavWidth, sidebarContentWidth, cameraDockBounds) {
const { layoutContextState } = this.props;
const { isRTL } = layoutContextState;
const { height: actionBarHeight } = this.calculatesActionbarHeight();
const calculatesDropAreas = (sidebarNavWidth, sidebarContentWidth, cameraDockBounds) => {
const { height: actionBarHeight } = calculatesActionbarHeight();
const mediaAreaHeight = windowHeight()
- (DEFAULT_VALUES.navBarHeight + actionBarHeight);
const mediaAreaWidth = windowWidth() - (sidebarNavWidth + sidebarContentWidth);
@ -125,40 +123,38 @@ class CustomLayout extends Component {
};
return dropZones;
}
};
init() {
const { layoutContextState, layoutContextDispatch } = this.props;
const { deviceType, input } = layoutContextState;
if (deviceType === DEVICE_TYPE.MOBILE) {
const init = () => {
if (isMobile) {
layoutContextDispatch({
type: ACTIONS.SET_LAYOUT_INPUT,
value: _.defaultsDeep({
sidebarNavigation: {
isOpen: false,
sidebarNavPanel: input.sidebarNavigation.sidebarNavPanel,
sidebarNavPanel: sidebarNavigationInput.sidebarNavPanel,
},
sidebarContent: {
isOpen: false,
sidebarContentPanel: input.sidebarContent.sidebarContentPanel,
sidebarContentPanel: sidebarContentInput.sidebarContentPanel,
},
sidebarContentHorizontalResizer: {
isOpen: false,
},
presentation: {
slidesLength: input.presentation.slidesLength,
isOpen: presentationInput.isOpen,
slidesLength: presentationInput.slidesLength,
currentSlide: {
...input.presentation.currentSlide,
...presentationInput.currentSlide,
},
},
cameraDock: {
numCameras: input.cameraDock.numCameras,
numCameras: cameraDockInput.numCameras,
},
}, INITIAL_INPUT_STATE),
});
} else {
const { sidebarContentPanel } = input.sidebarContent;
const { sidebarContentPanel } = sidebarContentInput;
layoutContextDispatch({
type: ACTIONS.SET_LAYOUT_INPUT,
@ -174,463 +170,171 @@ class CustomLayout extends Component {
isOpen: false,
},
presentation: {
slidesLength: input.presentation.slidesLength,
isOpen: presentationInput.isOpen,
slidesLength: presentationInput.slidesLength,
currentSlide: {
...input.presentation.currentSlide,
...presentationInput.currentSlide,
},
},
cameraDock: {
numCameras: input.cameraDock.numCameras,
numCameras: cameraDockInput.numCameras,
},
}, INITIAL_INPUT_STATE),
});
}
this.throttledCalculatesLayout();
}
throttledCalculatesLayout();
};
reset() {
this.init();
}
calculatesNavbarBounds(mediaAreaBounds) {
const { layoutContextState } = this.props;
const { isRTL } = layoutContextState;
return {
width: mediaAreaBounds.width,
height: DEFAULT_VALUES.navBarHeight,
top: DEFAULT_VALUES.navBarTop + this.bannerAreaHeight(),
left: !isRTL ? mediaAreaBounds.left : 0,
};
}
calculatesActionbarHeight() {
const { layoutContextState } = this.props;
const { fontSize } = layoutContextState;
const BASE_FONT_SIZE = 14; // 90% font size
const BASE_HEIGHT = DEFAULT_VALUES.actionBarHeight;
const PADDING = DEFAULT_VALUES.actionBarPadding;
const actionBarHeight = ((BASE_HEIGHT / BASE_FONT_SIZE) * fontSize);
return {
height: actionBarHeight + (PADDING * 2),
innerHeight: actionBarHeight,
padding: PADDING,
};
}
calculatesActionbarBounds(mediaAreaBounds) {
const { layoutContextState } = this.props;
const { input, isRTL } = layoutContextState;
const actionBarHeight = this.calculatesActionbarHeight();
return {
display: input.actionBar.hasActionBar,
width: mediaAreaBounds.width,
height: actionBarHeight.height,
innerHeight: actionBarHeight.innerHeight,
padding: actionBarHeight.padding,
top: windowHeight() - actionBarHeight.height,
left: !isRTL ? mediaAreaBounds.left : 0,
zIndex: 1,
};
}
calculatesSidebarNavWidth() {
const { layoutContextState } = this.props;
const { deviceType, input } = layoutContextState;
const {
sidebarNavMinWidth,
sidebarNavMaxWidth,
} = DEFAULT_VALUES;
let minWidth = 0;
let width = 0;
let maxWidth = 0;
if (input.sidebarNavigation.isOpen) {
if (deviceType === DEVICE_TYPE.MOBILE) {
minWidth = windowWidth();
width = windowWidth();
maxWidth = windowWidth();
} else {
if (input.sidebarNavigation.width === 0) {
width = min(max((windowWidth() * 0.2), sidebarNavMinWidth), sidebarNavMaxWidth);
} else {
width = min(max(input.sidebarNavigation.width, sidebarNavMinWidth), sidebarNavMaxWidth);
}
minWidth = sidebarNavMinWidth;
maxWidth = sidebarNavMaxWidth;
}
} else {
minWidth = 0;
width = 0;
maxWidth = 0;
}
return {
minWidth,
width,
maxWidth,
};
}
calculatesSidebarNavHeight() {
const { layoutContextState } = this.props;
const { deviceType, input } = layoutContextState;
let sidebarNavHeight = 0;
if (input.sidebarNavigation.isOpen) {
if (deviceType === DEVICE_TYPE.MOBILE) {
sidebarNavHeight = windowHeight() - DEFAULT_VALUES.navBarHeight;
} else {
sidebarNavHeight = windowHeight();
}
sidebarNavHeight -= this.bannerAreaHeight();
}
return sidebarNavHeight;
}
calculatesSidebarNavBounds() {
const { layoutContextState } = this.props;
const { deviceType, isRTL } = layoutContextState;
const { sidebarNavTop, navBarHeight, sidebarNavLeft } = DEFAULT_VALUES;
let top = sidebarNavTop + this.bannerAreaHeight();
if (deviceType === DEVICE_TYPE.MOBILE) {
top = navBarHeight + this.bannerAreaHeight();
}
return {
top,
left: !isRTL ? sidebarNavLeft : null,
right: isRTL ? sidebarNavLeft : null,
zIndex: deviceType === DEVICE_TYPE.MOBILE ? 10 : 2,
};
}
calculatesSidebarContentWidth() {
const { layoutContextState } = this.props;
const { deviceType, input } = layoutContextState;
const {
sidebarContentMinWidth,
sidebarContentMaxWidth,
} = DEFAULT_VALUES;
let minWidth = 0;
let width = 0;
let maxWidth = 0;
if (input.sidebarContent.isOpen) {
if (deviceType === DEVICE_TYPE.MOBILE) {
minWidth = windowWidth();
width = windowWidth();
maxWidth = windowWidth();
} else {
if (input.sidebarContent.width === 0) {
width = min(
max((windowWidth() * 0.2), sidebarContentMinWidth), sidebarContentMaxWidth,
);
} else {
width = min(max(input.sidebarContent.width, sidebarContentMinWidth),
sidebarContentMaxWidth);
}
minWidth = sidebarContentMinWidth;
maxWidth = sidebarContentMaxWidth;
}
} else {
minWidth = 0;
width = 0;
maxWidth = 0;
}
return {
minWidth,
width,
maxWidth,
};
}
calculatesSidebarContentHeight(cameraDockHeight) {
const { layoutContextState } = this.props;
const { deviceType, input } = layoutContextState;
const { presentation } = input;
const { isOpen } = presentation;
const calculatesSidebarContentHeight = (cameraDockHeight) => {
const { isOpen } = presentationInput;
let sidebarContentHeight = 0;
if (input.sidebarContent.isOpen) {
if (deviceType === DEVICE_TYPE.MOBILE) {
if (sidebarContentInput.isOpen) {
if (isMobile) {
sidebarContentHeight = windowHeight() - DEFAULT_VALUES.navBarHeight;
} else if (input.cameraDock.numCameras > 0
&& input.cameraDock.position === CAMERADOCK_POSITION.SIDEBAR_CONTENT_BOTTOM
} else if (cameraDockInput.numCameras > 0
&& cameraDockInput.position === CAMERADOCK_POSITION.SIDEBAR_CONTENT_BOTTOM
&& isOpen) {
sidebarContentHeight = windowHeight() - cameraDockHeight;
} else {
sidebarContentHeight = windowHeight();
}
sidebarContentHeight -= this.bannerAreaHeight();
sidebarContentHeight -= bannerAreaHeight();
}
return sidebarContentHeight;
}
};
calculatesSidebarContentBounds(sidebarNavWidth) {
const { layoutContextState } = this.props;
const { deviceType, isRTL } = layoutContextState;
let top = DEFAULT_VALUES.sidebarNavTop + this.bannerAreaHeight();
if (deviceType === DEVICE_TYPE.MOBILE) {
top = DEFAULT_VALUES.navBarHeight + this.bannerAreaHeight();
}
let left = deviceType === DEVICE_TYPE.MOBILE ? 0 : sidebarNavWidth;
left = !isRTL ? left : null;
let right = deviceType === DEVICE_TYPE.MOBILE ? 0 : sidebarNavWidth;
right = isRTL ? right : null;
return {
top,
left,
right,
zIndex: deviceType === DEVICE_TYPE.MOBILE ? 11 : 1,
};
}
calculatesMediaAreaBounds(sidebarNavWidth, sidebarContentWidth) {
const { layoutContextState } = this.props;
const { deviceType, isRTL } = layoutContextState;
const { navBarHeight } = DEFAULT_VALUES;
const { height: actionBarHeight } = this.calculatesActionbarHeight();
let left = 0;
let width = 0;
if (deviceType === DEVICE_TYPE.MOBILE) {
left = 0;
width = windowWidth();
} else {
left = !isRTL ? sidebarNavWidth + sidebarContentWidth : 0;
width = windowWidth() - sidebarNavWidth - sidebarContentWidth;
}
return {
width,
height: windowHeight() - (navBarHeight + actionBarHeight + this.bannerAreaHeight()),
top: DEFAULT_VALUES.navBarHeight + this.bannerAreaHeight(),
left,
};
}
calculatesCameraDockBounds(sidebarNavWidth, sidebarContentWidth, mediaAreaBounds) {
const { layoutContextState } = this.props;
const {
input, fullscreen, isRTL, deviceType,
} = layoutContextState;
const { presentation } = input;
const { isOpen } = presentation;
const { camerasMargin } = DEFAULT_VALUES;
const calculatesCameraDockBounds = (sidebarNavWidth, sidebarContentWidth, mediaAreaBounds) => {
const { baseCameraDockBounds } = props;
const sidebarSize = sidebarNavWidth + sidebarContentWidth;
const baseBounds = baseCameraDockBounds(mediaAreaBounds, sidebarSize);
// do not proceed if using values from LayoutEngine
if (Object.keys(baseBounds).length > 0) {
return baseBounds;
}
const {
camerasMargin,
cameraDockMinHeight,
cameraDockMinWidth,
navBarHeight,
presentationToolbarMinWidth,
} = DEFAULT_VALUES;
const cameraDockBounds = {};
if (input.cameraDock.numCameras > 0) {
if (!isOpen) {
cameraDockBounds.width = mediaAreaBounds.width;
cameraDockBounds.maxWidth = mediaAreaBounds.width;
cameraDockBounds.height = mediaAreaBounds.height;
cameraDockBounds.maxHeight = mediaAreaBounds.height;
cameraDockBounds.top = DEFAULT_VALUES.navBarHeight;
cameraDockBounds.left = !isRTL ? mediaAreaBounds.left : 0;
cameraDockBounds.right = isRTL ? sidebarSize : null;
let cameraDockHeight = 0;
let cameraDockWidth = 0;
if (cameraDockInput.isDragging) cameraDockBounds.zIndex = 99;
else cameraDockBounds.zIndex = 1;
const isCameraTop = cameraDockInput.position === CAMERADOCK_POSITION.CONTENT_TOP;
const isCameraBottom = cameraDockInput.position === CAMERADOCK_POSITION.CONTENT_BOTTOM;
const isCameraLeft = cameraDockInput.position === CAMERADOCK_POSITION.CONTENT_LEFT;
const isCameraRight = cameraDockInput.position === CAMERADOCK_POSITION.CONTENT_RIGHT;
const isCameraSidebar = cameraDockInput.position === CAMERADOCK_POSITION.SIDEBAR_CONTENT_BOTTOM;
if (isCameraTop || isCameraBottom) {
if (cameraDockInput.height === 0 || (isCameraTop && isMobile)) {
cameraDockHeight = min(
max((mediaAreaBounds.height * 0.2), cameraDockMinHeight),
(mediaAreaBounds.height - cameraDockMinHeight),
);
} else {
let cameraDockLeft = 0;
let cameraDockHeight = 0;
let cameraDockWidth = 0;
switch (input.cameraDock.position) {
case CAMERADOCK_POSITION.CONTENT_TOP: {
cameraDockLeft = mediaAreaBounds.left;
if (input.cameraDock.height === 0 || deviceType === DEVICE_TYPE.MOBILE) {
if (input.presentation.isOpen) {
cameraDockHeight = min(
max((mediaAreaBounds.height * 0.2), DEFAULT_VALUES.cameraDockMinHeight),
(mediaAreaBounds.height - DEFAULT_VALUES.cameraDockMinHeight),
);
} else {
cameraDockHeight = mediaAreaBounds.height;
}
} else {
cameraDockHeight = min(
max(input.cameraDock.height, DEFAULT_VALUES.cameraDockMinHeight),
(mediaAreaBounds.height - DEFAULT_VALUES.cameraDockMinHeight),
);
}
cameraDockBounds.top = DEFAULT_VALUES.navBarHeight;
cameraDockBounds.left = cameraDockLeft;
cameraDockBounds.right = isRTL ? sidebarSize : null;
cameraDockBounds.minWidth = mediaAreaBounds.width;
cameraDockBounds.width = mediaAreaBounds.width;
cameraDockBounds.maxWidth = mediaAreaBounds.width;
cameraDockBounds.minHeight = DEFAULT_VALUES.cameraDockMinHeight;
cameraDockBounds.height = cameraDockHeight - camerasMargin;
cameraDockBounds.maxHeight = mediaAreaBounds.height * 0.8;
break;
}
case CAMERADOCK_POSITION.CONTENT_RIGHT: {
if (input.cameraDock.width === 0) {
if (input.presentation.isOpen) {
cameraDockWidth = min(
max((mediaAreaBounds.width * 0.2), DEFAULT_VALUES.cameraDockMinWidth),
(mediaAreaBounds.width - DEFAULT_VALUES.cameraDockMinWidth),
);
} else {
cameraDockWidth = mediaAreaBounds.width;
}
} else {
cameraDockWidth = min(
max(input.cameraDock.width, DEFAULT_VALUES.cameraDockMinWidth),
(mediaAreaBounds.width - DEFAULT_VALUES.cameraDockMinWidth),
);
}
cameraDockBounds.top = DEFAULT_VALUES.navBarHeight;
const sizeValue = input.presentation.isOpen
? (mediaAreaBounds.left + mediaAreaBounds.width) - cameraDockWidth
: mediaAreaBounds.left;
cameraDockBounds.left = !isRTL ? sizeValue + camerasMargin : 0;
cameraDockBounds.right = isRTL ? sizeValue + sidebarSize + camerasMargin : null;
cameraDockBounds.minWidth = DEFAULT_VALUES.cameraDockMinWidth;
cameraDockBounds.width = cameraDockWidth - (camerasMargin * 2);
cameraDockBounds.maxWidth = mediaAreaBounds.width * 0.8;
cameraDockBounds.presenterMaxWidth = mediaAreaBounds.width
- DEFAULT_VALUES.presentationToolbarMinWidth
- camerasMargin;
cameraDockBounds.minHeight = DEFAULT_VALUES.cameraDockMinHeight;
cameraDockBounds.height = mediaAreaBounds.height;
cameraDockBounds.maxHeight = mediaAreaBounds.height;
break;
}
case CAMERADOCK_POSITION.CONTENT_BOTTOM: {
cameraDockLeft = mediaAreaBounds.left;
if (input.cameraDock.height === 0) {
if (input.presentation.isOpen) {
cameraDockHeight = min(
max((mediaAreaBounds.height * 0.2), DEFAULT_VALUES.cameraDockMinHeight),
(mediaAreaBounds.height - DEFAULT_VALUES.cameraDockMinHeight),
);
} else {
cameraDockHeight = mediaAreaBounds.height;
}
} else {
cameraDockHeight = min(
max(input.cameraDock.height, DEFAULT_VALUES.cameraDockMinHeight),
(mediaAreaBounds.height - DEFAULT_VALUES.cameraDockMinHeight),
);
}
cameraDockBounds.top = DEFAULT_VALUES.navBarHeight
+ mediaAreaBounds.height - cameraDockHeight;
cameraDockBounds.left = cameraDockLeft;
cameraDockBounds.right = isRTL ? sidebarSize : null;
cameraDockBounds.minWidth = mediaAreaBounds.width;
cameraDockBounds.width = mediaAreaBounds.width;
cameraDockBounds.maxWidth = mediaAreaBounds.width;
cameraDockBounds.minHeight = DEFAULT_VALUES.cameraDockMinHeight;
cameraDockBounds.height = cameraDockHeight - camerasMargin;
cameraDockBounds.maxHeight = mediaAreaBounds.height * 0.8;
break;
}
case CAMERADOCK_POSITION.CONTENT_LEFT: {
if (input.cameraDock.width === 0) {
if (input.presentation.isOpen) {
cameraDockWidth = min(
max((mediaAreaBounds.width * 0.2), DEFAULT_VALUES.cameraDockMinWidth),
(mediaAreaBounds.width - DEFAULT_VALUES.cameraDockMinWidth),
);
} else {
cameraDockWidth = mediaAreaBounds.width;
}
} else {
cameraDockWidth = min(
max(input.cameraDock.width, DEFAULT_VALUES.cameraDockMinWidth),
(mediaAreaBounds.width - DEFAULT_VALUES.cameraDockMinWidth),
);
}
cameraDockBounds.top = DEFAULT_VALUES.navBarHeight;
cameraDockBounds.left = mediaAreaBounds.left + camerasMargin;
cameraDockBounds.right = isRTL ? sidebarSize + (camerasMargin * 2) : null;
cameraDockBounds.minWidth = DEFAULT_VALUES.cameraDockMinWidth;
cameraDockBounds.width = cameraDockWidth - (camerasMargin * 2);
cameraDockBounds.maxWidth = mediaAreaBounds.width * 0.8;
cameraDockBounds.presenterMaxWidth = mediaAreaBounds.width
- DEFAULT_VALUES.presentationToolbarMinWidth
- camerasMargin;
cameraDockBounds.minHeight = mediaAreaBounds.height;
cameraDockBounds.height = mediaAreaBounds.height;
cameraDockBounds.maxHeight = mediaAreaBounds.height;
break;
}
case CAMERADOCK_POSITION.SIDEBAR_CONTENT_BOTTOM: {
if (input.cameraDock.height === 0) {
cameraDockHeight = min(
max((windowHeight() * 0.2), DEFAULT_VALUES.cameraDockMinHeight),
(windowHeight() - DEFAULT_VALUES.cameraDockMinHeight),
);
} else {
cameraDockHeight = min(
max(input.cameraDock.height, DEFAULT_VALUES.cameraDockMinHeight),
(windowHeight() - DEFAULT_VALUES.cameraDockMinHeight),
);
}
cameraDockBounds.top = windowHeight() - cameraDockHeight;
cameraDockBounds.left = !isRTL ? sidebarNavWidth : 0;
cameraDockBounds.right = isRTL ? sidebarNavWidth : 0;
cameraDockBounds.minWidth = sidebarContentWidth;
cameraDockBounds.width = sidebarContentWidth;
cameraDockBounds.maxWidth = sidebarContentWidth;
cameraDockBounds.minHeight = DEFAULT_VALUES.cameraDockMinHeight;
cameraDockBounds.height = cameraDockHeight;
cameraDockBounds.maxHeight = windowHeight() * 0.8;
break;
}
default: {
console.log('default');
}
}
if (fullscreen.group === 'webcams') {
cameraDockBounds.width = windowWidth();
cameraDockBounds.minWidth = windowWidth();
cameraDockBounds.maxWidth = windowWidth();
cameraDockBounds.height = windowHeight();
cameraDockBounds.minHeight = windowHeight();
cameraDockBounds.maxHeight = windowHeight();
cameraDockBounds.top = 0;
cameraDockBounds.left = 0;
cameraDockBounds.right = 0;
cameraDockBounds.zIndex = 99;
return cameraDockBounds;
}
if (input.cameraDock.isDragging) cameraDockBounds.zIndex = 99;
else cameraDockBounds.zIndex = 1;
cameraDockHeight = min(
max(cameraDockInput.height, cameraDockMinHeight),
(mediaAreaBounds.height - cameraDockMinHeight),
);
}
} else {
cameraDockBounds.width = 0;
cameraDockBounds.height = 0;
cameraDockBounds.top = navBarHeight + bannerAreaHeight();
cameraDockBounds.left = mediaAreaBounds.left;
cameraDockBounds.right = isRTL ? sidebarSize : null;
cameraDockBounds.minWidth = mediaAreaBounds.width;
cameraDockBounds.width = mediaAreaBounds.width;
cameraDockBounds.maxWidth = mediaAreaBounds.width;
cameraDockBounds.minHeight = cameraDockMinHeight;
cameraDockBounds.height = cameraDockHeight;
cameraDockBounds.maxHeight = mediaAreaBounds.height * 0.8;
if (isCameraBottom) {
cameraDockBounds.top += (mediaAreaBounds.height - cameraDockHeight);
}
return cameraDockBounds;
}
return cameraDockBounds;
}
if (isCameraLeft || isCameraRight) {
if (cameraDockInput.width === 0) {
cameraDockWidth = min(
max((mediaAreaBounds.width * 0.2), cameraDockMinWidth),
(mediaAreaBounds.width - cameraDockMinWidth),
);
} else {
cameraDockWidth = min(
max(cameraDockInput.width, cameraDockMinWidth),
(mediaAreaBounds.width - cameraDockMinWidth),
);
}
calculatesMediaBounds(sidebarNavWidth, sidebarContentWidth, cameraDockBounds) {
const { layoutContextState } = this.props;
const { input, fullscreen, isRTL } = layoutContextState;
const { presentation } = input;
const { isOpen } = presentation;
const { height: actionBarHeight } = this.calculatesActionbarHeight();
cameraDockBounds.top = navBarHeight + bannerAreaHeight();
cameraDockBounds.minWidth = cameraDockMinWidth;
cameraDockBounds.width = cameraDockWidth;
cameraDockBounds.maxWidth = mediaAreaBounds.width * 0.8;
cameraDockBounds.presenterMaxWidth = mediaAreaBounds.width
- presentationToolbarMinWidth
- camerasMargin;
cameraDockBounds.minHeight = cameraDockMinHeight;
cameraDockBounds.height = mediaAreaBounds.height;
cameraDockBounds.maxHeight = mediaAreaBounds.height;
// button size in vertical position
cameraDockBounds.height -= 20;
if (isCameraRight) {
const sizeValue = (mediaAreaBounds.left + mediaAreaBounds.width) - cameraDockWidth;
cameraDockBounds.left = !isRTL ? sizeValue - camerasMargin : 0;
cameraDockBounds.right = isRTL ? sizeValue + sidebarSize - camerasMargin : null;
} else if (isCameraLeft) {
cameraDockBounds.left = mediaAreaBounds.left + camerasMargin;
cameraDockBounds.right = isRTL ? sidebarSize + (camerasMargin * 2) : null;
}
return cameraDockBounds;
}
if (isCameraSidebar) {
if (cameraDockInput.height === 0) {
cameraDockHeight = min(
max((windowHeight() * 0.2), cameraDockMinHeight),
(windowHeight() - cameraDockMinHeight),
);
} else {
cameraDockHeight = min(
max(cameraDockInput.height, cameraDockMinHeight),
(windowHeight() - cameraDockMinHeight),
);
}
cameraDockBounds.top = windowHeight() - cameraDockHeight;
cameraDockBounds.left = !isRTL ? sidebarNavWidth : 0;
cameraDockBounds.right = isRTL ? sidebarNavWidth : 0;
cameraDockBounds.minWidth = sidebarContentWidth;
cameraDockBounds.width = sidebarContentWidth;
cameraDockBounds.maxWidth = sidebarContentWidth;
cameraDockBounds.minHeight = cameraDockMinHeight;
cameraDockBounds.height = cameraDockHeight;
cameraDockBounds.maxHeight = windowHeight() * 0.8;
}
return cameraDockBounds;
};
const calculatesMediaBounds = (sidebarNavWidth, sidebarContentWidth, cameraDockBounds) => {
const { isOpen } = presentationInput;
const { height: actionBarHeight } = calculatesActionbarHeight();
const mediaAreaHeight = windowHeight()
- (DEFAULT_VALUES.navBarHeight + actionBarHeight);
- (DEFAULT_VALUES.navBarHeight + actionBarHeight + bannerAreaHeight());
const mediaAreaWidth = windowWidth() - (sidebarNavWidth + sidebarContentWidth);
const mediaBounds = {};
const { element: fullscreenElement } = fullscreen;
@ -658,20 +362,20 @@ class CustomLayout extends Component {
const sidebarSize = sidebarNavWidth + sidebarContentWidth;
if (input.cameraDock.numCameras > 0 && !input.cameraDock.isDragging) {
switch (input.cameraDock.position) {
if (cameraDockInput.numCameras > 0 && !cameraDockInput.isDragging) {
switch (cameraDockInput.position) {
case CAMERADOCK_POSITION.CONTENT_TOP: {
mediaBounds.width = mediaAreaWidth;
mediaBounds.height = mediaAreaHeight - cameraDockBounds.height - camerasMargin;
mediaBounds.top = navBarHeight + cameraDockBounds.height + camerasMargin;
mediaBounds.top = navBarHeight + cameraDockBounds.height + camerasMargin + bannerAreaHeight();
mediaBounds.left = !isRTL ? sidebarSize : null;
mediaBounds.right = isRTL ? sidebarSize : null;
break;
}
case CAMERADOCK_POSITION.CONTENT_RIGHT: {
mediaBounds.width = mediaAreaWidth - cameraDockBounds.width - camerasMargin;
mediaBounds.width = mediaAreaWidth - cameraDockBounds.width - (camerasMargin * 2);
mediaBounds.height = mediaAreaHeight;
mediaBounds.top = navBarHeight;
mediaBounds.top = navBarHeight + bannerAreaHeight();
mediaBounds.left = !isRTL ? sidebarSize : null;
mediaBounds.right = isRTL ? sidebarSize - (camerasMargin * 2) : null;
break;
@ -679,15 +383,15 @@ class CustomLayout extends Component {
case CAMERADOCK_POSITION.CONTENT_BOTTOM: {
mediaBounds.width = mediaAreaWidth;
mediaBounds.height = mediaAreaHeight - cameraDockBounds.height - camerasMargin;
mediaBounds.top = navBarHeight - camerasMargin;
mediaBounds.top = navBarHeight - camerasMargin + bannerAreaHeight();
mediaBounds.left = !isRTL ? sidebarSize : null;
mediaBounds.right = isRTL ? sidebarSize : null;
break;
}
case CAMERADOCK_POSITION.CONTENT_LEFT: {
mediaBounds.width = mediaAreaWidth - cameraDockBounds.width - camerasMargin;
mediaBounds.width = mediaAreaWidth - cameraDockBounds.width - (camerasMargin * 2);
mediaBounds.height = mediaAreaHeight;
mediaBounds.top = navBarHeight;
mediaBounds.top = navBarHeight + bannerAreaHeight();
const sizeValue = sidebarNavWidth
+ sidebarContentWidth + mediaAreaWidth - mediaBounds.width;
mediaBounds.left = !isRTL ? sizeValue : null;
@ -697,7 +401,7 @@ class CustomLayout extends Component {
case CAMERADOCK_POSITION.SIDEBAR_CONTENT_BOTTOM: {
mediaBounds.width = mediaAreaWidth;
mediaBounds.height = mediaAreaHeight;
mediaBounds.top = navBarHeight;
mediaBounds.top = navBarHeight + bannerAreaHeight();
mediaBounds.left = !isRTL ? sidebarSize : null;
mediaBounds.right = isRTL ? sidebarSize : null;
break;
@ -710,7 +414,7 @@ class CustomLayout extends Component {
} else {
mediaBounds.width = mediaAreaWidth;
mediaBounds.height = mediaAreaHeight;
mediaBounds.top = DEFAULT_VALUES.navBarHeight + this.bannerAreaHeight();
mediaBounds.top = DEFAULT_VALUES.navBarHeight + bannerAreaHeight();
mediaBounds.left = !isRTL ? sidebarSize : null;
mediaBounds.right = isRTL ? sidebarSize : null;
}
@ -718,34 +422,38 @@ class CustomLayout extends Component {
return mediaBounds;
}
calculatesLayout() {
const { layoutContextState, layoutContextDispatch } = this.props;
const { deviceType, input, isRTL } = layoutContextState;
const { cameraDock } = input;
const { position: cameraPosition } = cameraDock;
const calculatesLayout = () => {
const {
calculatesNavbarBounds,
calculatesActionbarBounds,
calculatesSidebarNavWidth,
calculatesSidebarNavHeight,
calculatesSidebarNavBounds,
calculatesSidebarContentWidth,
calculatesSidebarContentBounds,
calculatesMediaAreaBounds,
isTablet,
} = props;
const { position: cameraPosition } = cameraDockInput;
const { camerasMargin, captionsMargin } = DEFAULT_VALUES;
const sidebarNavWidth = this.calculatesSidebarNavWidth();
const sidebarNavHeight = this.calculatesSidebarNavHeight();
const sidebarContentWidth = this.calculatesSidebarContentWidth();
const sidebarNavBounds = this
.calculatesSidebarNavBounds(sidebarNavWidth.width, sidebarContentWidth.width);
const sidebarContentBounds = this
.calculatesSidebarContentBounds(sidebarNavWidth.width, sidebarContentWidth.width);
const mediaAreaBounds = this
.calculatesMediaAreaBounds(sidebarNavWidth.width, sidebarContentWidth.width);
const navbarBounds = this.calculatesNavbarBounds(mediaAreaBounds);
const actionbarBounds = this.calculatesActionbarBounds(mediaAreaBounds);
const cameraDockBounds = this.calculatesCameraDockBounds(
const sidebarNavWidth = calculatesSidebarNavWidth();
const sidebarNavHeight = calculatesSidebarNavHeight();
const sidebarContentWidth = calculatesSidebarContentWidth();
const sidebarNavBounds = calculatesSidebarNavBounds();
const sidebarContentBounds = calculatesSidebarContentBounds(sidebarNavWidth.width);
const mediaAreaBounds = calculatesMediaAreaBounds(sidebarNavWidth.width, sidebarContentWidth.width);
const navbarBounds = calculatesNavbarBounds(mediaAreaBounds);
const actionbarBounds = calculatesActionbarBounds(mediaAreaBounds);
const cameraDockBounds = calculatesCameraDockBounds(
sidebarNavWidth.width, sidebarContentWidth.width, mediaAreaBounds,
);
const dropZoneAreas = this
.calculatesDropAreas(sidebarNavWidth.width, sidebarContentWidth.width, cameraDockBounds);
const sidebarContentHeight = this.calculatesSidebarContentHeight(cameraDockBounds.height);
const mediaBounds = this.calculatesMediaBounds(
const dropZoneAreas = calculatesDropAreas(sidebarNavWidth.width, sidebarContentWidth.width, cameraDockBounds);
const sidebarContentHeight = calculatesSidebarContentHeight(cameraDockBounds.height);
const mediaBounds = calculatesMediaBounds(
sidebarNavWidth.width, sidebarContentWidth.width, cameraDockBounds,
);
const { height: actionBarHeight } = this.calculatesActionbarHeight();
const { height: actionBarHeight } = calculatesActionbarHeight();
let horizontalCameraDiff = 0;
@ -760,19 +468,20 @@ class CustomLayout extends Component {
layoutContextDispatch({
type: ACTIONS.SET_NAVBAR_OUTPUT,
value: {
display: input.navBar.hasNavBar,
display: navbarInput.hasNavBar,
width: navbarBounds.width,
height: navbarBounds.height,
top: navbarBounds.top,
left: navbarBounds.left,
tabOrder: DEFAULT_VALUES.navBarTabOrder,
zIndex: navbarBounds.zIndex,
},
});
layoutContextDispatch({
type: ACTIONS.SET_ACTIONBAR_OUTPUT,
value: {
display: input.actionBar.hasActionBar,
display: actionbarInput.hasActionBar,
width: actionbarBounds.width,
height: actionbarBounds.height,
innerHeight: actionbarBounds.innerHeight,
@ -796,7 +505,7 @@ class CustomLayout extends Component {
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_NAVIGATION_OUTPUT,
value: {
display: input.sidebarNavigation.isOpen,
display: sidebarNavigationInput.isOpen,
minWidth: sidebarNavWidth.minWidth,
width: sidebarNavWidth.width,
maxWidth: sidebarNavWidth.maxWidth,
@ -805,8 +514,7 @@ class CustomLayout extends Component {
left: sidebarNavBounds.left,
right: sidebarNavBounds.right,
tabOrder: DEFAULT_VALUES.sidebarNavTabOrder,
isResizable: deviceType !== DEVICE_TYPE.MOBILE
&& deviceType !== DEVICE_TYPE.TABLET,
isResizable: !isMobile && !isTablet,
zIndex: sidebarNavBounds.zIndex,
},
});
@ -824,7 +532,7 @@ class CustomLayout extends Component {
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_OUTPUT,
value: {
display: input.sidebarContent.isOpen,
display: sidebarContentInput.isOpen,
minWidth: sidebarContentWidth.minWidth,
width: sidebarContentWidth.width,
maxWidth: sidebarContentWidth.maxWidth,
@ -832,10 +540,9 @@ class CustomLayout extends Component {
top: sidebarContentBounds.top,
left: sidebarContentBounds.left,
right: sidebarContentBounds.right,
currentPanelType: input.currentPanelType,
currentPanelType,
tabOrder: DEFAULT_VALUES.sidebarContentTabOrder,
isResizable: deviceType !== DEVICE_TYPE.MOBILE
&& deviceType !== DEVICE_TYPE.TABLET,
isResizable: !isMobile && !isTablet,
zIndex: sidebarContentBounds.zIndex,
},
});
@ -861,8 +568,8 @@ class CustomLayout extends Component {
layoutContextDispatch({
type: ACTIONS.SET_CAMERA_DOCK_OUTPUT,
value: {
display: input.cameraDock.numCameras > 0,
position: input.cameraDock.position,
display: cameraDockInput.numCameras > 0,
position: cameraDockInput.position,
minWidth: cameraDockBounds.minWidth,
width: cameraDockBounds.width,
maxWidth: cameraDockBounds.maxWidth,
@ -874,16 +581,15 @@ class CustomLayout extends Component {
left: cameraDockBounds.left,
right: cameraDockBounds.right,
tabOrder: 4,
isDraggable: deviceType !== DEVICE_TYPE.MOBILE
&& deviceType !== DEVICE_TYPE.TABLET,
isDraggable: !isMobile && !isTablet,
resizableEdge: {
top: input.cameraDock.position === CAMERADOCK_POSITION.CONTENT_BOTTOM
|| input.cameraDock.position === CAMERADOCK_POSITION.SIDEBAR_CONTENT_BOTTOM,
right: (!isRTL && input.cameraDock.position === CAMERADOCK_POSITION.CONTENT_LEFT)
|| (isRTL && input.cameraDock.position === CAMERADOCK_POSITION.CONTENT_RIGHT),
bottom: input.cameraDock.position === CAMERADOCK_POSITION.CONTENT_TOP,
left: (!isRTL && input.cameraDock.position === CAMERADOCK_POSITION.CONTENT_RIGHT)
|| (isRTL && input.cameraDock.position === CAMERADOCK_POSITION.CONTENT_LEFT),
top: cameraDockInput.position === CAMERADOCK_POSITION.CONTENT_BOTTOM
|| cameraDockInput.position === CAMERADOCK_POSITION.SIDEBAR_CONTENT_BOTTOM,
right: (!isRTL && cameraDockInput.position === CAMERADOCK_POSITION.CONTENT_LEFT)
|| (isRTL && cameraDockInput.position === CAMERADOCK_POSITION.CONTENT_RIGHT),
bottom: cameraDockInput.position === CAMERADOCK_POSITION.CONTENT_TOP,
left: (!isRTL && cameraDockInput.position === CAMERADOCK_POSITION.CONTENT_RIGHT)
|| (isRTL && cameraDockInput.position === CAMERADOCK_POSITION.CONTENT_LEFT),
},
zIndex: cameraDockBounds.zIndex,
},
@ -897,7 +603,7 @@ class CustomLayout extends Component {
layoutContextDispatch({
type: ACTIONS.SET_PRESENTATION_OUTPUT,
value: {
display: input.presentation.isOpen,
display: presentationInput.isOpen,
width: mediaBounds.width,
height: mediaBounds.height,
top: mediaBounds.top,
@ -931,11 +637,9 @@ class CustomLayout extends Component {
right: isRTL ? (mediaBounds.right + horizontalCameraDiff) : null,
},
});
}
};
render() {
return null;
}
}
return null;
};
export default LayoutContextFunc.withConsumer(CustomLayout);
export default CustomLayout;

View File

@ -0,0 +1,302 @@
import React from 'react';
import PropTypes from 'prop-types';
import { layoutSelect, layoutSelectInput } from '/imports/ui/components/layout/context';
import DEFAULT_VALUES from '/imports/ui/components/layout/defaultValues';
import { LAYOUT_TYPE, DEVICE_TYPE } from '/imports/ui/components/layout/enums';
import CustomLayout from '/imports/ui/components/layout/layout-manager/customLayout';
import SmartLayout from '/imports/ui/components/layout/layout-manager/smartLayout';
import PresentationFocusLayout from '/imports/ui/components/layout/layout-manager/presentationFocusLayout';
import VideoFocusLayout from '/imports/ui/components/layout/layout-manager/videoFocusLayout';
const propTypes = {
layoutType: PropTypes.string.isRequired,
};
const LayoutEngine = ({ layoutType }) => {
const bannerBarInput = layoutSelectInput((i) => i.bannerBar);
const notificationsBarInput = layoutSelectInput((i) => i.notificationsBar);
const cameraDockInput = layoutSelectInput((i) => i.cameraDock);
const presentationInput = layoutSelectInput((i) => i.presentation);
const actionbarInput = layoutSelectInput((i) => i.actionBar);
const sidebarNavigationInput = layoutSelectInput((i) => i.sidebarNavigation);
const sidebarContentInput = layoutSelectInput((i) => i.sidebarContent);
const fullscreen = layoutSelect((i) => i.fullscreen);
const isRTL = layoutSelect((i) => i.isRTL);
const fontSize = layoutSelect((i) => i.fontSize);
const deviceType = layoutSelect((i) => i.deviceType);
const isMobile = deviceType === DEVICE_TYPE.MOBILE;
const isTablet = deviceType === DEVICE_TYPE.TABLET;
const windowWidth = () => window.document.documentElement.clientWidth;
const windowHeight = () => window.document.documentElement.clientHeight;
const min = (value1, value2) => (value1 <= value2 ? value1 : value2);
const max = (value1, value2) => (value1 >= value2 ? value1 : value2);
const bannerAreaHeight = () => {
const { hasNotification } = notificationsBarInput;
const { hasBanner } = bannerBarInput;
const bannerHeight = hasBanner ? DEFAULT_VALUES.bannerHeight : 0;
const notificationHeight = hasNotification ? DEFAULT_VALUES.bannerHeight : 0;
return bannerHeight + notificationHeight;
};
const baseCameraDockBounds = (mediaAreaBounds, sidebarSize) => {
const { isOpen } = presentationInput;
const cameraDockBounds = {};
if (cameraDockInput.numCameras === 0) {
cameraDockBounds.width = 0;
cameraDockBounds.height = 0;
return cameraDockBounds;
}
if (!isOpen) {
cameraDockBounds.width = mediaAreaBounds.width;
cameraDockBounds.maxWidth = mediaAreaBounds.width;
cameraDockBounds.height = mediaAreaBounds.height;
cameraDockBounds.maxHeight = mediaAreaBounds.height;
cameraDockBounds.top = DEFAULT_VALUES.navBarHeight;
cameraDockBounds.left = !isRTL ? mediaAreaBounds.left : 0;
cameraDockBounds.right = isRTL ? sidebarSize : null;
}
if (fullscreen.group === 'webcams') {
cameraDockBounds.width = windowWidth();
cameraDockBounds.minWidth = windowWidth();
cameraDockBounds.maxWidth = windowWidth();
cameraDockBounds.height = windowHeight();
cameraDockBounds.minHeight = windowHeight();
cameraDockBounds.maxHeight = windowHeight();
cameraDockBounds.top = 0;
cameraDockBounds.left = 0;
cameraDockBounds.right = 0;
cameraDockBounds.zIndex = 99;
return cameraDockBounds;
}
return cameraDockBounds;
};
const calculatesNavbarBounds = (mediaAreaBounds) => {
const { navBarHeight, navBarTop } = DEFAULT_VALUES;
return {
width: mediaAreaBounds.width,
height: navBarHeight,
top: navBarTop + bannerAreaHeight(),
left: !isRTL ? mediaAreaBounds.left : 0,
zIndex: 1,
};
};
const calculatesActionbarHeight = () => {
const { actionBarHeight, actionBarPadding } = DEFAULT_VALUES;
const BASE_FONT_SIZE = 14; // 90% font size
const height = ((actionBarHeight / BASE_FONT_SIZE) * fontSize);
return {
height: height + (actionBarPadding * 2),
innerHeight: height,
padding: actionBarPadding,
};
};
const calculatesActionbarBounds = (mediaAreaBounds) => {
const actionBarHeight = calculatesActionbarHeight();
return {
display: actionbarInput.hasActionBar,
width: mediaAreaBounds.width,
height: actionBarHeight.height,
innerHeight: actionBarHeight.innerHeight,
padding: actionBarHeight.padding,
top: windowHeight() - actionBarHeight.height,
left: !isRTL ? mediaAreaBounds.left : 0,
zIndex: 1,
};
};
const calculatesSidebarNavWidth = () => {
const {
sidebarNavMinWidth,
sidebarNavMaxWidth,
} = DEFAULT_VALUES;
const { isOpen, width: sidebarNavWidth } = sidebarNavigationInput;
let minWidth = 0;
let width = 0;
let maxWidth = 0;
if (isOpen) {
if (isMobile) {
minWidth = windowWidth();
width = windowWidth();
maxWidth = windowWidth();
} else {
if (sidebarNavWidth === 0) {
width = min(max((windowWidth() * 0.2), sidebarNavMinWidth), sidebarNavMaxWidth);
} else {
width = min(max(sidebarNavWidth, sidebarNavMinWidth), sidebarNavMaxWidth);
}
minWidth = sidebarNavMinWidth;
maxWidth = sidebarNavMaxWidth;
}
}
return {
minWidth,
width,
maxWidth,
};
};
const calculatesSidebarNavHeight = () => {
const { navBarHeight } = DEFAULT_VALUES;
const { isOpen } = sidebarNavigationInput;
let sidebarNavHeight = 0;
if (isOpen) {
if (isMobile) {
sidebarNavHeight = windowHeight() - navBarHeight - bannerAreaHeight();
} else {
sidebarNavHeight = windowHeight() - bannerAreaHeight();
}
}
return sidebarNavHeight;
};
const calculatesSidebarNavBounds = () => {
const { sidebarNavTop, navBarHeight, sidebarNavLeft } = DEFAULT_VALUES;
let top = sidebarNavTop + bannerAreaHeight();
if (isMobile) {
top = navBarHeight + bannerAreaHeight();
}
return {
top,
left: !isRTL ? sidebarNavLeft : null,
right: isRTL ? sidebarNavLeft : null,
zIndex: isMobile ? 11 : 2,
};
};
const calculatesSidebarContentWidth = () => {
const {
sidebarContentMinWidth,
sidebarContentMaxWidth,
} = DEFAULT_VALUES;
const { isOpen, width: sidebarContentWidth } = sidebarContentInput;
let minWidth = 0;
let width = 0;
let maxWidth = 0;
if (isOpen) {
if (isMobile) {
minWidth = windowWidth();
width = windowWidth();
maxWidth = windowWidth();
} else {
if (sidebarContentWidth === 0) {
width = min(
max((windowWidth() * 0.2), sidebarContentMinWidth), sidebarContentMaxWidth,
);
} else {
width = min(max(sidebarContentWidth, sidebarContentMinWidth),
sidebarContentMaxWidth);
}
minWidth = sidebarContentMinWidth;
maxWidth = sidebarContentMaxWidth;
}
}
return {
minWidth,
width,
maxWidth,
};
};
const calculatesSidebarContentBounds = (sidebarNavWidth) => {
const { navBarHeight, sidebarNavTop } = DEFAULT_VALUES;
let top = sidebarNavTop + bannerAreaHeight();
if (isMobile) top = navBarHeight + bannerAreaHeight();
let left = isMobile ? 0 : sidebarNavWidth;
let right = isMobile ? 0 : sidebarNavWidth;
left = !isRTL ? left : null;
right = isRTL ? right : null;
const zIndex = isMobile ? 11 : 1;
return {
top,
left,
right,
zIndex,
};
};
const calculatesMediaAreaBounds = (sidebarNavWidth, sidebarContentWidth) => {
const { navBarHeight } = DEFAULT_VALUES;
const { height: actionBarHeight } = calculatesActionbarHeight();
let left = 0;
let width = 0;
if (isMobile) {
width = windowWidth();
} else {
left = !isRTL ? sidebarNavWidth + sidebarContentWidth : 0;
width = windowWidth() - sidebarNavWidth - sidebarContentWidth;
}
return {
width,
height: windowHeight() - (navBarHeight + actionBarHeight + bannerAreaHeight()),
top: navBarHeight + bannerAreaHeight(),
left,
};
};
const common = {
bannerAreaHeight,
baseCameraDockBounds,
calculatesNavbarBounds,
calculatesActionbarHeight,
calculatesActionbarBounds,
calculatesSidebarNavWidth,
calculatesSidebarNavHeight,
calculatesSidebarNavBounds,
calculatesSidebarContentWidth,
calculatesSidebarContentBounds,
calculatesMediaAreaBounds,
isMobile,
isTablet,
};
switch (layoutType) {
case LAYOUT_TYPE.CUSTOM_LAYOUT:
return <CustomLayout {...common} />;
case LAYOUT_TYPE.SMART_LAYOUT:
return <SmartLayout {...common} />;
case LAYOUT_TYPE.PRESENTATION_FOCUS:
return <PresentationFocusLayout {...common} />;
case LAYOUT_TYPE.VIDEO_FOCUS:
return <VideoFocusLayout {...common} />;
default:
return <CustomLayout {...common} />;
}
};
LayoutEngine.propTypes = propTypes;
export default LayoutEngine;

View File

@ -1,26 +1,47 @@
import React, { Component } from 'react';
import { throttle, defaultsDeep } from 'lodash';
import { LayoutContextFunc } from '/imports/ui/components/layout/context';
import { useEffect, useRef } from 'react';
import _ from 'lodash';
import { layoutDispatch, layoutSelect, layoutSelectInput } from '/imports/ui/components/layout/context';
import DEFAULT_VALUES from '/imports/ui/components/layout/defaultValues';
import { INITIAL_INPUT_STATE } from '/imports/ui/components/layout/initState';
import { DEVICE_TYPE, ACTIONS, PANELS } from '/imports/ui/components/layout/enums';
import { ACTIONS, PANELS } from '/imports/ui/components/layout/enums';
const windowWidth = () => window.document.documentElement.clientWidth;
const windowHeight = () => window.document.documentElement.clientHeight;
const min = (value1, value2) => (value1 <= value2 ? value1 : value2);
const max = (value1, value2) => (value1 >= value2 ? value1 : value2);
class PresentationFocusLayout extends Component {
constructor(props) {
super(props);
const PresentationFocusLayout = (props) => {
const { bannerAreaHeight, isMobile } = props;
this.throttledCalculatesLayout = throttle(() => this.calculatesLayout(),
50, { trailing: true, leading: true });
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
componentDidMount() {
this.init();
const { layoutContextDispatch } = this.props;
const input = layoutSelect((i) => i.input);
const deviceType = layoutSelect((i) => i.deviceType);
const isRTL = layoutSelect((i) => i.isRTL);
const fullscreen = layoutSelect((i) => i.fullscreen);
const fontSize = layoutSelect((i) => i.fontSize);
const currentPanelType = layoutSelect((i) => i.currentPanelType);
const presentationInput = layoutSelectInput((i) => i.presentation);
const sidebarNavigationInput = layoutSelectInput((i) => i.sidebarNavigation);
const sidebarContentInput = layoutSelectInput((i) => i.sidebarContent);
const cameraDockInput = layoutSelectInput((i) => i.cameraDock);
const actionbarInput = layoutSelectInput((i) => i.actionBar);
const navbarInput = layoutSelectInput((i) => i.navBar);
const layoutContextDispatch = layoutDispatch();
const prevDeviceType = usePrevious(deviceType);
const throttledCalculatesLayout = _.throttle(() => calculatesLayout(),
50, { trailing: true, leading: true });
useEffect(() => {
window.addEventListener('resize', () => {
layoutContextDispatch({
type: ACTIONS.SET_BROWSER_SIZE,
@ -30,73 +51,54 @@ class PresentationFocusLayout extends Component {
},
});
});
}
}, []);
shouldComponentUpdate(nextProps) {
const { layoutContextState } = this.props;
return layoutContextState.input !== nextProps.layoutContextState.input
|| layoutContextState.deviceType !== nextProps.layoutContextState.deviceType
|| layoutContextState.isRTL !== nextProps.layoutContextState.isRTL
|| layoutContextState.fontSize !== nextProps.layoutContextState.fontSize
|| layoutContextState.fullscreen !== nextProps.layoutContextState.fullscreen;
}
useEffect(() => {
if (deviceType === null) return;
componentDidUpdate(prevProps) {
const { layoutContextState } = this.props;
const { deviceType } = layoutContextState;
if (prevProps.layoutContextState.deviceType !== deviceType) {
this.init();
if (deviceType !== prevDeviceType) {
// reset layout if deviceType changed
// not all options is supported in all devices
init();
} else {
this.throttledCalculatesLayout();
throttledCalculatesLayout();
}
}
}, [input, deviceType, isRTL, fontSize, fullscreen]);
bannerAreaHeight() {
const { layoutContextState } = this.props;
const { input } = layoutContextState;
const { bannerBar, notificationsBar } = input;
const bannerHeight = bannerBar.hasBanner ? DEFAULT_VALUES.bannerHeight : 0;
const notificationHeight = notificationsBar.hasNotification ? DEFAULT_VALUES.bannerHeight : 0;
return bannerHeight + notificationHeight;
}
init() {
const { layoutContextState, layoutContextDispatch } = this.props;
const { deviceType, input } = layoutContextState;
if (deviceType === DEVICE_TYPE.MOBILE) {
const init = () => {
if (isMobile) {
layoutContextDispatch({
type: ACTIONS.SET_LAYOUT_INPUT,
value: defaultsDeep({
value: _.defaultsDeep({
sidebarNavigation: {
isOpen: false,
sidebarNavPanel: input.sidebarNavigation.sidebarNavPanel,
sidebarNavPanel: sidebarNavigationInput.sidebarNavPanel,
},
sidebarContent: {
isOpen: false,
sidebarContentPanel: input.sidebarContent.sidebarContentPanel,
sidebarContentPanel: sidebarContentInput.sidebarContentPanel,
},
SidebarContentHorizontalResizer: {
isOpen: false,
},
presentation: {
slidesLength: input.presentation.slidesLength,
isOpen: presentationInput.isOpen,
slidesLength: presentationInput.slidesLength,
currentSlide: {
...input.presentation.currentSlide,
...presentationInput.currentSlide,
},
},
cameraDock: {
numCameras: input.cameraDock.numCameras,
numCameras: cameraDockInput.numCameras,
},
}, INITIAL_INPUT_STATE),
});
} else {
const { sidebarContentPanel } = input.sidebarContent;
const { sidebarContentPanel } = sidebarContentInput;
layoutContextDispatch({
type: ACTIONS.SET_LAYOUT_INPUT,
value: defaultsDeep({
value: _.defaultsDeep({
sidebarNavigation: {
isOpen: true,
},
@ -108,174 +110,23 @@ class PresentationFocusLayout extends Component {
isOpen: false,
},
presentation: {
slidesLength: input.presentation.slidesLength,
isOpen: presentationInput.isOpen,
slidesLength: presentationInput.slidesLength,
currentSlide: {
...input.presentation.currentSlide,
...presentationInput.currentSlide,
},
},
cameraDock: {
numCameras: input.cameraDock.numCameras,
numCameras: cameraDockInput.numCameras,
},
}, INITIAL_INPUT_STATE),
});
}
this.throttledCalculatesLayout();
}
throttledCalculatesLayout();
};
reset() {
this.init();
}
calculatesNavbarBounds(mediaAreaBounds) {
const { layoutContextState } = this.props;
const { isRTL } = layoutContextState;
return {
width: mediaAreaBounds.width,
height: DEFAULT_VALUES.navBarHeight,
top: DEFAULT_VALUES.navBarTop + this.bannerAreaHeight(),
left: !isRTL ? mediaAreaBounds.left : 0,
zIndex: 1,
};
}
calculatesActionbarHeight() {
const { layoutContextState } = this.props;
const { fontSize } = layoutContextState;
const BASE_FONT_SIZE = 14; // 90% font size
const BASE_HEIGHT = DEFAULT_VALUES.actionBarHeight;
const PADDING = DEFAULT_VALUES.actionBarPadding;
const actionBarHeight = ((BASE_HEIGHT / BASE_FONT_SIZE) * fontSize);
return {
height: actionBarHeight + (PADDING * 2),
innerHeight: actionBarHeight,
padding: PADDING,
};
}
calculatesActionbarBounds(mediaAreaBounds) {
const { layoutContextState } = this.props;
const { input, isRTL } = layoutContextState;
const actionBarHeight = this.calculatesActionbarHeight();
return {
display: input.actionBar.hasActionBar,
width: mediaAreaBounds.width,
height: actionBarHeight.height,
innerHeight: actionBarHeight.innerHeight,
padding: actionBarHeight.padding,
top: windowHeight() - actionBarHeight.height,
left: !isRTL ? mediaAreaBounds.left : 0,
zIndex: 1,
};
}
calculatesSidebarNavWidth() {
const { layoutContextState } = this.props;
const { deviceType, input } = layoutContextState;
const {
sidebarNavMinWidth,
sidebarNavMaxWidth,
} = DEFAULT_VALUES;
let minWidth = 0;
let width = 0;
let maxWidth = 0;
if (input.sidebarNavigation.isOpen) {
if (deviceType === DEVICE_TYPE.MOBILE) {
minWidth = windowWidth();
width = windowWidth();
maxWidth = windowWidth();
} else {
if (input.sidebarNavigation.width === 0) {
width = min(max((windowWidth() * 0.2), sidebarNavMinWidth), sidebarNavMaxWidth);
} else {
width = min(max(input.sidebarNavigation.width, sidebarNavMinWidth), sidebarNavMaxWidth);
}
minWidth = sidebarNavMinWidth;
maxWidth = sidebarNavMaxWidth;
}
}
return {
minWidth,
width,
maxWidth,
};
}
calculatesSidebarNavHeight() {
const { layoutContextState } = this.props;
const { deviceType, input } = layoutContextState;
const { navBarHeight } = DEFAULT_VALUES;
let sidebarNavHeight = 0;
if (input.sidebarNavigation.isOpen) {
if (deviceType === DEVICE_TYPE.MOBILE) {
sidebarNavHeight = windowHeight() - navBarHeight - this.bannerAreaHeight();
} else {
sidebarNavHeight = windowHeight() - this.bannerAreaHeight();
}
}
return sidebarNavHeight;
}
calculatesSidebarNavBounds() {
const { layoutContextState } = this.props;
const { deviceType, isRTL } = layoutContextState;
const { sidebarNavTop, navBarHeight, sidebarNavLeft } = DEFAULT_VALUES;
let top = sidebarNavTop + this.bannerAreaHeight();
if (deviceType === DEVICE_TYPE.MOBILE) top = navBarHeight + this.bannerAreaHeight();
return {
top,
left: !isRTL ? sidebarNavLeft : null,
right: isRTL ? sidebarNavLeft : null,
zIndex: deviceType === DEVICE_TYPE.MOBILE ? 11 : 2,
};
}
calculatesSidebarContentWidth() {
const { layoutContextState } = this.props;
const { deviceType, input } = layoutContextState;
const {
sidebarContentMinWidth,
sidebarContentMaxWidth,
} = DEFAULT_VALUES;
let minWidth = 0;
let width = 0;
let maxWidth = 0;
if (input.sidebarContent.isOpen) {
if (deviceType === DEVICE_TYPE.MOBILE) {
minWidth = windowWidth();
width = windowWidth();
maxWidth = windowWidth();
} else {
if (input.sidebarContent.width === 0) {
width = min(
max((windowWidth() * 0.2), sidebarContentMinWidth), sidebarContentMaxWidth,
);
} else {
width = min(max(input.sidebarContent.width, sidebarContentMinWidth),
sidebarContentMaxWidth);
}
minWidth = sidebarContentMinWidth;
maxWidth = sidebarContentMaxWidth;
}
}
return {
minWidth,
width,
maxWidth,
};
}
calculatesSidebarContentHeight() {
const { layoutContextState } = this.props;
const { deviceType, input } = layoutContextState;
const calculatesSidebarContentHeight = () => {
const { isOpen } = presentationInput;
const {
navBarHeight,
sidebarContentMinHeight,
@ -283,22 +134,22 @@ class PresentationFocusLayout extends Component {
let height = 0;
let minHeight = 0;
let maxHeight = 0;
if (input.sidebarContent.isOpen) {
if (deviceType === DEVICE_TYPE.MOBILE) {
height = windowHeight() - navBarHeight - this.bannerAreaHeight();
if (sidebarContentInput.isOpen) {
if (isMobile) {
height = windowHeight() - navBarHeight - bannerAreaHeight();
minHeight = height;
maxHeight = height;
} else if (input.cameraDock.numCameras > 0) {
if (input.sidebarContent.height === 0) {
height = (windowHeight() * 0.75) - this.bannerAreaHeight();
} else if (cameraDockInput.numCameras > 0 && isOpen) {
if (sidebarContentInput.height === 0) {
height = (windowHeight() * 0.75) - bannerAreaHeight();
} else {
height = min(max(input.sidebarContent.height, sidebarContentMinHeight),
height = min(max(sidebarContentInput.height, sidebarContentMinHeight),
windowHeight());
}
minHeight = windowHeight() * 0.25 - this.bannerAreaHeight();
maxHeight = windowHeight() * 0.75 - this.bannerAreaHeight();
minHeight = windowHeight() * 0.25 - bannerAreaHeight();
maxHeight = windowHeight() * 0.75 - bannerAreaHeight();
} else {
height = windowHeight() - this.bannerAreaHeight();
height = windowHeight() - bannerAreaHeight();
minHeight = height;
maxHeight = height;
}
@ -308,130 +159,68 @@ class PresentationFocusLayout extends Component {
minHeight,
maxHeight,
};
}
};
calculatesSidebarContentBounds(sidebarNavWidth) {
const { layoutContextState } = this.props;
const { deviceType, isRTL } = layoutContextState;
const { navBarHeight, sidebarNavTop } = DEFAULT_VALUES;
let top = sidebarNavTop + this.bannerAreaHeight();
if (deviceType === DEVICE_TYPE.MOBILE) top = navBarHeight + this.bannerAreaHeight();
let left = deviceType === DEVICE_TYPE.MOBILE ? 0 : sidebarNavWidth;
let right = deviceType === DEVICE_TYPE.MOBILE ? 0 : sidebarNavWidth;
left = !isRTL ? left : null;
right = isRTL ? right : null;
const zIndex = deviceType === DEVICE_TYPE.MOBILE ? 11 : 1;
return {
top,
left,
right,
zIndex,
};
}
calculatesMediaAreaBounds(sidebarNavWidth, sidebarContentWidth) {
const { layoutContextState } = this.props;
const { deviceType, isRTL } = layoutContextState;
const { navBarHeight } = DEFAULT_VALUES;
const { height: actionBarHeight } = this.calculatesActionbarHeight();
let left = 0;
let width = 0;
if (deviceType === DEVICE_TYPE.MOBILE) {
left = 0;
width = windowWidth();
} else {
left = !isRTL ? sidebarNavWidth + sidebarContentWidth : 0;
width = windowWidth() - sidebarNavWidth - sidebarContentWidth;
}
return {
width,
height: windowHeight() - (navBarHeight + actionBarHeight + this.bannerAreaHeight()),
top: navBarHeight + this.bannerAreaHeight(),
left,
};
}
calculatesCameraDockBounds(
const calculatesCameraDockBounds = (
mediaBounds,
mediaAreaBounds,
sidebarNavWidth,
sidebarContentWidth,
sidebarContentHeight,
) {
const { layoutContextState } = this.props;
const {
deviceType, input, fullscreen, isRTL,
} = layoutContextState;
) => {
const { baseCameraDockBounds } = props;
const sidebarSize = sidebarNavWidth + sidebarContentWidth;
const baseBounds = baseCameraDockBounds(mediaAreaBounds, sidebarSize);
// do not proceed if using values from LayoutEngine
if (Object.keys(baseBounds).length > 0) {
return baseBounds;
}
const { cameraDockMinHeight } = DEFAULT_VALUES;
const cameraDockBounds = {};
if (input.cameraDock.numCameras > 0) {
let cameraDockHeight = 0;
let cameraDockHeight = 0;
if (fullscreen.group === 'webcams') {
cameraDockBounds.width = windowWidth();
cameraDockBounds.minWidth = windowWidth();
cameraDockBounds.maxWidth = windowWidth();
cameraDockBounds.height = windowHeight();
cameraDockBounds.minHeight = windowHeight();
cameraDockBounds.maxHeight = windowHeight();
cameraDockBounds.top = 0;
cameraDockBounds.left = 0;
cameraDockBounds.right = 0;
cameraDockBounds.zIndex = 99;
return cameraDockBounds;
}
if (deviceType === DEVICE_TYPE.MOBILE) {
cameraDockBounds.top = mediaAreaBounds.top + mediaBounds.height;
cameraDockBounds.left = 0;
cameraDockBounds.right = 0;
cameraDockBounds.minWidth = mediaAreaBounds.width;
cameraDockBounds.width = mediaAreaBounds.width;
cameraDockBounds.maxWidth = mediaAreaBounds.width;
cameraDockBounds.minHeight = DEFAULT_VALUES.cameraDockMinHeight;
cameraDockBounds.height = mediaAreaBounds.height - mediaBounds.height;
cameraDockBounds.maxHeight = mediaAreaBounds.height - mediaBounds.height;
} else {
if (input.cameraDock.height === 0) {
cameraDockHeight = min(
max((windowHeight() - sidebarContentHeight), DEFAULT_VALUES.cameraDockMinHeight),
(windowHeight() - DEFAULT_VALUES.cameraDockMinHeight),
);
} else {
cameraDockHeight = min(
max(input.cameraDock.height, DEFAULT_VALUES.cameraDockMinHeight),
(windowHeight() - DEFAULT_VALUES.cameraDockMinHeight),
);
}
cameraDockBounds.top = windowHeight() - cameraDockHeight;
cameraDockBounds.left = !isRTL ? sidebarNavWidth : 0;
cameraDockBounds.right = isRTL ? sidebarNavWidth : 0;
cameraDockBounds.minWidth = sidebarContentWidth;
cameraDockBounds.width = sidebarContentWidth;
cameraDockBounds.maxWidth = sidebarContentWidth;
cameraDockBounds.minHeight = DEFAULT_VALUES.cameraDockMinHeight;
cameraDockBounds.height = cameraDockHeight;
cameraDockBounds.maxHeight = windowHeight() - sidebarContentHeight;
cameraDockBounds.zIndex = 1;
}
if (isMobile) {
cameraDockBounds.top = mediaAreaBounds.top + mediaBounds.height;
cameraDockBounds.left = 0;
cameraDockBounds.right = 0;
cameraDockBounds.minWidth = mediaAreaBounds.width;
cameraDockBounds.width = mediaAreaBounds.width;
cameraDockBounds.maxWidth = mediaAreaBounds.width;
cameraDockBounds.minHeight = cameraDockMinHeight;
cameraDockBounds.height = mediaAreaBounds.height - mediaBounds.height;
cameraDockBounds.maxHeight = mediaAreaBounds.height - mediaBounds.height;
} else {
cameraDockBounds.width = 0;
cameraDockBounds.height = 0;
if (cameraDockInput.height === 0) {
cameraDockHeight = min(
max((windowHeight() - sidebarContentHeight), cameraDockMinHeight),
(windowHeight() - cameraDockMinHeight),
);
} else {
cameraDockHeight = min(
max(cameraDockInput.height, cameraDockMinHeight),
(windowHeight() - cameraDockMinHeight),
);
}
cameraDockBounds.top = windowHeight() - cameraDockHeight;
cameraDockBounds.left = !isRTL ? sidebarNavWidth : 0;
cameraDockBounds.right = isRTL ? sidebarNavWidth : 0;
cameraDockBounds.minWidth = sidebarContentWidth;
cameraDockBounds.width = sidebarContentWidth;
cameraDockBounds.maxWidth = sidebarContentWidth;
cameraDockBounds.minHeight = cameraDockMinHeight;
cameraDockBounds.height = cameraDockHeight;
cameraDockBounds.maxHeight = windowHeight() - sidebarContentHeight;
cameraDockBounds.zIndex = 1;
}
return cameraDockBounds;
}
};
calculatesMediaBounds(mediaAreaBounds, sidebarSize) {
const { layoutContextState } = this.props;
const {
deviceType, input, fullscreen, isRTL,
} = layoutContextState;
const calculatesMediaBounds = (mediaAreaBounds, sidebarSize) => {
const mediaBounds = {};
const { element: fullscreenElement } = fullscreen;
@ -445,43 +234,48 @@ class PresentationFocusLayout extends Component {
return mediaBounds;
}
if (deviceType === DEVICE_TYPE.MOBILE && input.cameraDock.numCameras > 0) {
if (isMobile && cameraDockInput.numCameras > 0) {
mediaBounds.height = mediaAreaBounds.height * 0.7;
} else {
mediaBounds.height = mediaAreaBounds.height;
}
mediaBounds.width = mediaAreaBounds.width;
mediaBounds.top = DEFAULT_VALUES.navBarHeight + this.bannerAreaHeight();
mediaBounds.top = DEFAULT_VALUES.navBarHeight + bannerAreaHeight();
mediaBounds.left = !isRTL ? mediaAreaBounds.left : null;
mediaBounds.right = isRTL ? sidebarSize : null;
mediaBounds.zIndex = 1;
return mediaBounds;
}
};
calculatesLayout() {
const { layoutContextState, layoutContextDispatch } = this.props;
const { deviceType, input, isRTL } = layoutContextState;
const calculatesLayout = () => {
const {
calculatesNavbarBounds,
calculatesActionbarBounds,
calculatesSidebarNavWidth,
calculatesSidebarNavHeight,
calculatesSidebarNavBounds,
calculatesSidebarContentWidth,
calculatesSidebarContentBounds,
calculatesMediaAreaBounds,
isTablet,
} = props;
const { captionsMargin } = DEFAULT_VALUES;
const sidebarNavWidth = this.calculatesSidebarNavWidth();
const sidebarNavHeight = this.calculatesSidebarNavHeight();
const sidebarContentWidth = this.calculatesSidebarContentWidth();
const sidebarNavBounds = this.calculatesSidebarNavBounds(
const sidebarNavWidth = calculatesSidebarNavWidth();
const sidebarNavHeight = calculatesSidebarNavHeight();
const sidebarContentWidth = calculatesSidebarContentWidth();
const sidebarNavBounds = calculatesSidebarNavBounds();
const sidebarContentBounds = calculatesSidebarContentBounds(sidebarNavWidth.width);
const mediaAreaBounds = calculatesMediaAreaBounds(
sidebarNavWidth.width, sidebarContentWidth.width,
);
const sidebarContentBounds = this.calculatesSidebarContentBounds(
sidebarNavWidth.width, sidebarContentWidth.width,
);
const mediaAreaBounds = this.calculatesMediaAreaBounds(
sidebarNavWidth.width, sidebarContentWidth.width,
);
const navbarBounds = this.calculatesNavbarBounds(mediaAreaBounds);
const actionbarBounds = this.calculatesActionbarBounds(mediaAreaBounds);
const navbarBounds = calculatesNavbarBounds(mediaAreaBounds);
const actionbarBounds = calculatesActionbarBounds(mediaAreaBounds);
const sidebarSize = sidebarContentWidth.width + sidebarNavWidth.width;
const mediaBounds = this.calculatesMediaBounds(mediaAreaBounds, sidebarSize);
const sidebarContentHeight = this.calculatesSidebarContentHeight();
const cameraDockBounds = this.calculatesCameraDockBounds(
const mediaBounds = calculatesMediaBounds(mediaAreaBounds, sidebarSize);
const sidebarContentHeight = calculatesSidebarContentHeight();
const cameraDockBounds = calculatesCameraDockBounds(
mediaBounds,
mediaAreaBounds,
sidebarNavWidth.width,
@ -492,7 +286,7 @@ class PresentationFocusLayout extends Component {
layoutContextDispatch({
type: ACTIONS.SET_NAVBAR_OUTPUT,
value: {
display: input.navBar.hasNavBar,
display: navbarInput.hasNavBar,
width: navbarBounds.width,
height: navbarBounds.height,
top: navbarBounds.top,
@ -505,7 +299,7 @@ class PresentationFocusLayout extends Component {
layoutContextDispatch({
type: ACTIONS.SET_ACTIONBAR_OUTPUT,
value: {
display: input.actionBar.hasActionBar,
display: actionbarInput.hasActionBar,
width: actionbarBounds.width,
height: actionbarBounds.height,
innerHeight: actionbarBounds.innerHeight,
@ -529,7 +323,7 @@ class PresentationFocusLayout extends Component {
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_NAVIGATION_OUTPUT,
value: {
display: input.sidebarNavigation.isOpen,
display: sidebarNavigationInput.isOpen,
minWidth: sidebarNavWidth.minWidth,
width: sidebarNavWidth.width,
maxWidth: sidebarNavWidth.maxWidth,
@ -538,8 +332,7 @@ class PresentationFocusLayout extends Component {
left: sidebarNavBounds.left,
right: sidebarNavBounds.right,
tabOrder: DEFAULT_VALUES.sidebarNavTabOrder,
isResizable: deviceType !== DEVICE_TYPE.MOBILE
&& deviceType !== DEVICE_TYPE.TABLET,
isResizable: !isMobile && !isTablet,
zIndex: sidebarNavBounds.zIndex,
},
});
@ -557,7 +350,7 @@ class PresentationFocusLayout extends Component {
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_OUTPUT,
value: {
display: input.sidebarContent.isOpen,
display: sidebarContentInput.isOpen,
minWidth: sidebarContentWidth.minWidth,
width: sidebarContentWidth.width,
maxWidth: sidebarContentWidth.maxWidth,
@ -567,10 +360,9 @@ class PresentationFocusLayout extends Component {
top: sidebarContentBounds.top,
left: sidebarContentBounds.left,
right: sidebarContentBounds.right,
currentPanelType: input.currentPanelType,
currentPanelType,
tabOrder: DEFAULT_VALUES.sidebarContentTabOrder,
isResizable: deviceType !== DEVICE_TYPE.MOBILE
&& deviceType !== DEVICE_TYPE.TABLET,
isResizable: !isMobile && !isTablet,
zIndex: sidebarContentBounds.zIndex,
},
});
@ -580,7 +372,7 @@ class PresentationFocusLayout extends Component {
value: {
top: false,
right: !isRTL,
bottom: input.cameraDock.numCameras > 0,
bottom: cameraDockInput.numCameras > 0,
left: isRTL,
},
});
@ -596,7 +388,7 @@ class PresentationFocusLayout extends Component {
layoutContextDispatch({
type: ACTIONS.SET_CAMERA_DOCK_OUTPUT,
value: {
display: input.cameraDock.numCameras > 0,
display: cameraDockInput.numCameras > 0,
minWidth: cameraDockBounds.minWidth,
width: cameraDockBounds.width,
maxWidth: cameraDockBounds.maxWidth,
@ -621,7 +413,7 @@ class PresentationFocusLayout extends Component {
layoutContextDispatch({
type: ACTIONS.SET_PRESENTATION_OUTPUT,
value: {
display: input.presentation.isOpen,
display: presentationInput.isOpen,
width: mediaBounds.width,
height: mediaBounds.height,
top: mediaBounds.top,
@ -655,13 +447,9 @@ class PresentationFocusLayout extends Component {
right: mediaBounds.right,
},
});
}
};
render() {
return (
<></>
);
}
}
return null;
};
export default LayoutContextFunc.withConsumer(PresentationFocusLayout);
export default PresentationFocusLayout;

View File

@ -1,26 +1,45 @@
import React, { Component } from 'react';
import { useEffect, useRef } from 'react';
import _ from 'lodash';
import { LayoutContextFunc } from '/imports/ui/components/layout/context';
import { layoutDispatch, layoutSelect, layoutSelectInput } from '/imports/ui/components/layout/context';
import DEFAULT_VALUES from '/imports/ui/components/layout/defaultValues';
import { INITIAL_INPUT_STATE } from '/imports/ui/components/layout/initState';
import { DEVICE_TYPE, ACTIONS, PANELS } from '/imports/ui/components/layout/enums';
import { ACTIONS, PANELS, CAMERADOCK_POSITION } from '/imports/ui/components/layout/enums';
const windowWidth = () => window.document.documentElement.clientWidth;
const windowHeight = () => window.document.documentElement.clientHeight;
const min = (value1, value2) => (value1 <= value2 ? value1 : value2);
const max = (value1, value2) => (value1 >= value2 ? value1 : value2);
class SmartLayout extends Component {
constructor(props) {
super(props);
const SmartLayout = (props) => {
const { bannerAreaHeight, isMobile } = props;
this.throttledCalculatesLayout = _.throttle(() => this.calculatesLayout(),
50, { trailing: true, leading: true });
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
componentDidMount() {
this.init();
const { layoutContextDispatch } = this.props;
const input = layoutSelect((i) => i.input);
const deviceType = layoutSelect((i) => i.deviceType);
const isRTL = layoutSelect((i) => i.isRTL);
const fullscreen = layoutSelect((i) => i.fullscreen);
const fontSize = layoutSelect((i) => i.fontSize);
const currentPanelType = layoutSelect((i) => i.currentPanelType);
const presentationInput = layoutSelectInput((i) => i.presentation);
const sidebarNavigationInput = layoutSelectInput((i) => i.sidebarNavigation);
const sidebarContentInput = layoutSelectInput((i) => i.sidebarContent);
const cameraDockInput = layoutSelectInput((i) => i.cameraDock);
const actionbarInput = layoutSelectInput((i) => i.actionBar);
const navbarInput = layoutSelectInput((i) => i.navBar);
const layoutContextDispatch = layoutDispatch();
const prevDeviceType = usePrevious(deviceType);
const throttledCalculatesLayout = _.throttle(() => calculatesLayout(),
50, { trailing: true, leading: true });
useEffect(() => {
window.addEventListener('resize', () => {
layoutContextDispatch({
type: ACTIONS.SET_BROWSER_SIZE,
@ -30,69 +49,50 @@ class SmartLayout extends Component {
},
});
});
}
}, []);
shouldComponentUpdate(nextProps) {
const { layoutContextState } = this.props;
return layoutContextState.input !== nextProps.layoutContextState.input
|| layoutContextState.deviceType !== nextProps.layoutContextState.deviceType
|| layoutContextState.isRTL !== nextProps.layoutContextState.isRTL
|| layoutContextState.fontSize !== nextProps.layoutContextState.fontSize
|| layoutContextState.fullscreen !== nextProps.layoutContextState.fullscreen;
}
useEffect(() => {
if (deviceType === null) return;
componentDidUpdate(prevProps) {
const { layoutContextState } = this.props;
const { deviceType } = layoutContextState;
if (prevProps.layoutContextState.deviceType !== deviceType) {
this.init();
if (deviceType !== prevDeviceType) {
// reset layout if deviceType changed
// not all options is supported in all devices
init();
} else {
this.throttledCalculatesLayout();
throttledCalculatesLayout();
}
}
}, [input, deviceType, isRTL, fontSize, fullscreen]);
bannerAreaHeight() {
const { layoutContextState } = this.props;
const { input } = layoutContextState;
const { bannerBar, notificationsBar } = input;
const bannerHeight = bannerBar.hasBanner ? DEFAULT_VALUES.bannerHeight : 0;
const notificationHeight = notificationsBar.hasNotification ? DEFAULT_VALUES.bannerHeight : 0;
return bannerHeight + notificationHeight;
}
init() {
const { layoutContextState, layoutContextDispatch } = this.props;
const { deviceType, input } = layoutContextState;
if (deviceType === DEVICE_TYPE.MOBILE) {
const init = () => {
if (isMobile) {
layoutContextDispatch({
type: ACTIONS.SET_LAYOUT_INPUT,
value: _.defaultsDeep({
sidebarNavigation: {
isOpen: false,
sidebarNavPanel: input.sidebarNavigation.sidebarNavPanel,
sidebarNavPanel: sidebarNavigationInput.sidebarNavPanel,
},
sidebarContent: {
isOpen: false,
sidebarContentPanel: input.sidebarContent.sidebarContentPanel,
sidebarContentPanel: sidebarContentInput.sidebarContentPanel,
},
SidebarContentHorizontalResizer: {
isOpen: false,
},
presentation: {
slidesLength: input.presentation.slidesLength,
isOpen: presentationInput.isOpen,
slidesLength: presentationInput.slidesLength,
currentSlide: {
...input.presentation.currentSlide,
...presentationInput.currentSlide,
},
},
cameraDock: {
numCameras: input.cameraDock.numCameras,
numCameras: cameraDockInput.numCameras,
},
}, INITIAL_INPUT_STATE),
});
} else {
const { sidebarContentPanel } = input.sidebarContent;
const { sidebarContentPanel } = sidebarContentInput;
layoutContextDispatch({
type: ACTIONS.SET_LAYOUT_INPUT,
@ -108,304 +108,89 @@ class SmartLayout extends Component {
isOpen: false,
},
presentation: {
slidesLength: input.presentation.slidesLength,
isOpen: presentationInput.isOpen,
slidesLength: presentationInput.slidesLength,
currentSlide: {
...input.presentation.currentSlide,
...presentationInput.currentSlide,
},
},
cameraDock: {
numCameras: input.cameraDock.numCameras,
numCameras: cameraDockInput.numCameras,
},
}, INITIAL_INPUT_STATE),
});
}
this.throttledCalculatesLayout();
}
throttledCalculatesLayout();
};
reset() {
this.init();
}
calculatesNavbarBounds(mediaAreaBounds) {
const { layoutContextState } = this.props;
const { isRTL } = layoutContextState;
return {
width: mediaAreaBounds.width,
height: DEFAULT_VALUES.navBarHeight,
top: DEFAULT_VALUES.navBarTop + this.bannerAreaHeight(),
left: !isRTL ? mediaAreaBounds.left : 0,
zIndex: 1,
};
}
calculatesActionbarHeight() {
const { layoutContextState } = this.props;
const { fontSize } = layoutContextState;
const BASE_FONT_SIZE = 14; // 90% font size
const BASE_HEIGHT = DEFAULT_VALUES.actionBarHeight;
const PADDING = DEFAULT_VALUES.actionBarPadding;
const actionBarHeight = ((BASE_HEIGHT / BASE_FONT_SIZE) * fontSize);
return {
height: actionBarHeight + (PADDING * 2),
innerHeight: actionBarHeight,
padding: PADDING,
};
}
calculatesActionbarBounds(mediaAreaBounds) {
const { layoutContextState } = this.props;
const { input, isRTL } = layoutContextState;
const actionBarHeight = this.calculatesActionbarHeight();
return {
display: input.actionBar.hasActionBar,
width: mediaAreaBounds.width,
height: actionBarHeight.height,
innerHeight: actionBarHeight.innerHeight,
padding: actionBarHeight.padding,
top: windowHeight() - actionBarHeight.height,
left: !isRTL ? mediaAreaBounds.left : 0,
zIndex: 1,
};
}
calculatesSidebarNavWidth() {
const { layoutContextState } = this.props;
const { deviceType, input } = layoutContextState;
const {
sidebarNavMinWidth,
sidebarNavMaxWidth,
} = DEFAULT_VALUES;
let minWidth = 0;
let width = 0;
let maxWidth = 0;
if (input.sidebarNavigation.isOpen) {
if (deviceType === DEVICE_TYPE.MOBILE) {
minWidth = windowWidth();
width = windowWidth();
maxWidth = windowWidth();
} else {
if (input.sidebarNavigation.width === 0) {
width = min(max((windowWidth() * 0.2), sidebarNavMinWidth), sidebarNavMaxWidth);
} else {
width = min(max(input.sidebarNavigation.width, sidebarNavMinWidth), sidebarNavMaxWidth);
}
minWidth = sidebarNavMinWidth;
maxWidth = sidebarNavMaxWidth;
}
}
return {
minWidth,
width,
maxWidth,
};
}
calculatesSidebarNavHeight() {
const { layoutContextState } = this.props;
const { deviceType, input } = layoutContextState;
let sidebarNavHeight = 0;
if (input.sidebarNavigation.isOpen) {
if (deviceType === DEVICE_TYPE.MOBILE) {
sidebarNavHeight = windowHeight() - DEFAULT_VALUES.navBarHeight;
} else {
sidebarNavHeight = windowHeight();
}
sidebarNavHeight -= this.bannerAreaHeight();
}
return sidebarNavHeight;
}
calculatesSidebarNavBounds() {
const { layoutContextState } = this.props;
const { deviceType, isRTL } = layoutContextState;
const { sidebarNavTop, navBarHeight, sidebarNavLeft } = DEFAULT_VALUES;
let top = sidebarNavTop + this.bannerAreaHeight();
if (deviceType === DEVICE_TYPE.MOBILE) top = navBarHeight + this.bannerAreaHeight();
return {
top,
left: !isRTL ? sidebarNavLeft : null,
right: isRTL ? sidebarNavLeft : null,
zIndex: deviceType === DEVICE_TYPE.MOBILE ? 11 : 2,
};
}
calculatesSidebarContentWidth() {
const { layoutContextState } = this.props;
const { deviceType, input } = layoutContextState;
const {
sidebarContentMinWidth,
sidebarContentMaxWidth,
} = DEFAULT_VALUES;
let minWidth = 0;
let width = 0;
let maxWidth = 0;
if (input.sidebarContent.isOpen) {
if (deviceType === DEVICE_TYPE.MOBILE) {
minWidth = windowWidth();
width = windowWidth();
maxWidth = windowWidth();
} else {
if (input.sidebarContent.width === 0) {
width = min(
max((windowWidth() * 0.2), sidebarContentMinWidth), sidebarContentMaxWidth,
);
} else {
width = min(max(input.sidebarContent.width, sidebarContentMinWidth),
sidebarContentMaxWidth);
}
minWidth = sidebarContentMinWidth;
maxWidth = sidebarContentMaxWidth;
}
}
return {
minWidth,
width,
maxWidth,
};
}
calculatesSidebarContentHeight() {
const { layoutContextState } = this.props;
const { deviceType, input } = layoutContextState;
const calculatesSidebarContentHeight = () => {
let sidebarContentHeight = 0;
if (input.sidebarContent.isOpen) {
if (deviceType === DEVICE_TYPE.MOBILE) {
if (sidebarContentInput.isOpen) {
if (isMobile) {
sidebarContentHeight = windowHeight() - DEFAULT_VALUES.navBarHeight;
} else {
sidebarContentHeight = windowHeight();
}
sidebarContentHeight -= this.bannerAreaHeight();
sidebarContentHeight -= bannerAreaHeight();
}
return sidebarContentHeight;
}
};
calculatesSidebarContentBounds(sidebarNavWidth) {
const { layoutContextState } = this.props;
const { deviceType, isRTL } = layoutContextState;
const { sidebarNavTop, navBarHeight } = DEFAULT_VALUES;
const calculatesCameraDockBounds = (mediaAreaBounds, mediaBounds, sidebarSize) => {
const { baseCameraDockBounds } = props;
let top = sidebarNavTop + this.bannerAreaHeight();
const baseBounds = baseCameraDockBounds(mediaAreaBounds, sidebarSize);
if (deviceType === DEVICE_TYPE.MOBILE) top = navBarHeight + this.bannerAreaHeight();
let left = deviceType === DEVICE_TYPE.MOBILE ? 0 : sidebarNavWidth;
left = !isRTL ? left : null;
let right = deviceType === DEVICE_TYPE.MOBILE ? 0 : sidebarNavWidth;
right = isRTL ? right : null;
return {
top,
left,
right,
zIndex: deviceType === DEVICE_TYPE.MOBILE ? 11 : 1,
};
}
calculatesMediaAreaBounds(sidebarNavWidth, sidebarContentWidth) {
const { layoutContextState } = this.props;
const { deviceType, isRTL } = layoutContextState;
const { navBarHeight } = DEFAULT_VALUES;
const { height: actionBarHeight } = this.calculatesActionbarHeight();
let left = 0;
let width = 0;
if (deviceType === DEVICE_TYPE.MOBILE) {
left = 0;
width = windowWidth();
} else {
left = !isRTL ? sidebarNavWidth + sidebarContentWidth : 0;
width = windowWidth() - sidebarNavWidth - sidebarContentWidth;
// do not proceed if using values from LayoutEngine
if (Object.keys(baseBounds).length > 0) {
baseBounds.isCameraHorizontal = false;
return baseBounds;
}
return {
width,
height: windowHeight() - (navBarHeight + actionBarHeight + this.bannerAreaHeight()),
top: navBarHeight + this.bannerAreaHeight(),
left,
};
}
calculatesCameraDockBounds(mediaAreaBounds, mediaBounds, sidebarSize) {
const { layoutContextState } = this.props;
const {
input, fullscreen, isRTL, deviceType,
} = layoutContextState;
const { presentation } = input;
const { isOpen } = presentation;
const { camerasMargin, presentationToolbarMinWidth } = DEFAULT_VALUES;
const cameraDockBounds = {};
cameraDockBounds.isCameraHorizontal = false;
const mediaBoundsWidth = (mediaBounds.width > presentationToolbarMinWidth
&& deviceType !== DEVICE_TYPE.MOBILE)
const mediaBoundsWidth = (mediaBounds.width > presentationToolbarMinWidth && !isMobile)
? mediaBounds.width
: presentationToolbarMinWidth;
if (input.cameraDock.numCameras > 0) {
cameraDockBounds.top = mediaAreaBounds.top;
cameraDockBounds.left = mediaAreaBounds.left;
cameraDockBounds.right = isRTL ? sidebarSize + (camerasMargin * 2) : null;
cameraDockBounds.zIndex = 1;
cameraDockBounds.top = mediaAreaBounds.top;
cameraDockBounds.left = mediaAreaBounds.left;
cameraDockBounds.right = isRTL ? sidebarSize + (camerasMargin * 2) : null;
cameraDockBounds.zIndex = 1;
if (!isOpen) {
cameraDockBounds.width = mediaAreaBounds.width;
cameraDockBounds.maxWidth = mediaAreaBounds.width;
cameraDockBounds.height = mediaAreaBounds.height;
cameraDockBounds.maxHeight = mediaAreaBounds.height;
} else if (mediaBounds.width < mediaAreaBounds.width) {
cameraDockBounds.width = mediaAreaBounds.width - mediaBoundsWidth;
cameraDockBounds.maxWidth = mediaAreaBounds.width * 0.8;
cameraDockBounds.height = mediaAreaBounds.height;
cameraDockBounds.maxHeight = mediaAreaBounds.height;
cameraDockBounds.left += camerasMargin;
cameraDockBounds.width -= (camerasMargin * 2);
cameraDockBounds.isCameraHorizontal = true;
} else {
cameraDockBounds.width = mediaAreaBounds.width;
cameraDockBounds.maxWidth = mediaAreaBounds.width;
cameraDockBounds.height = mediaAreaBounds.height - mediaBounds.height;
cameraDockBounds.maxHeight = mediaAreaBounds.height * 0.8;
cameraDockBounds.top += camerasMargin;
cameraDockBounds.height -= (camerasMargin * 2);
}
cameraDockBounds.minWidth = cameraDockBounds.width;
cameraDockBounds.minHeight = cameraDockBounds.height;
if (fullscreen.group === 'webcams') {
cameraDockBounds.width = windowWidth();
cameraDockBounds.minWidth = windowWidth();
cameraDockBounds.maxWidth = windowWidth();
cameraDockBounds.height = windowHeight();
cameraDockBounds.minHeight = windowHeight();
cameraDockBounds.maxHeight = windowHeight();
cameraDockBounds.top = 0;
cameraDockBounds.left = 0;
cameraDockBounds.right = 0;
cameraDockBounds.zIndex = 99;
}
if (mediaBounds.width < mediaAreaBounds.width) {
cameraDockBounds.width = mediaAreaBounds.width - mediaBoundsWidth;
cameraDockBounds.maxWidth = mediaAreaBounds.width * 0.8;
cameraDockBounds.height = mediaAreaBounds.height;
cameraDockBounds.maxHeight = mediaAreaBounds.height;
cameraDockBounds.left += camerasMargin;
cameraDockBounds.width -= (camerasMargin * 2);
cameraDockBounds.isCameraHorizontal = true;
cameraDockBounds.position = CAMERADOCK_POSITION.CONTENT_LEFT;
// button size in vertical position
cameraDockBounds.height -= 20;
} else {
cameraDockBounds.width = 0;
cameraDockBounds.height = 0;
cameraDockBounds.width = mediaAreaBounds.width;
cameraDockBounds.maxWidth = mediaAreaBounds.width;
cameraDockBounds.height = mediaAreaBounds.height - mediaBounds.height;
cameraDockBounds.maxHeight = mediaAreaBounds.height * 0.8;
cameraDockBounds.top += camerasMargin;
cameraDockBounds.height -= (camerasMargin * 2);
cameraDockBounds.position = CAMERADOCK_POSITION.CONTENT_TOP;
}
return cameraDockBounds;
}
cameraDockBounds.minWidth = cameraDockBounds.width;
cameraDockBounds.minHeight = cameraDockBounds.height;
calculatesSlideSize(mediaAreaBounds) {
const { layoutContextState } = this.props;
const { input } = layoutContextState;
const { presentation } = input;
const { currentSlide } = presentation;
return cameraDockBounds;
};
const calculatesSlideSize = (mediaAreaBounds) => {
const { currentSlide } = presentationInput;
if (currentSlide.size.width === 0 && currentSlide.size.height === 0) {
return {
@ -429,15 +214,10 @@ class SmartLayout extends Component {
width: slideWidth,
height: slideHeight,
};
}
};
calculatesMediaBounds(mediaAreaBounds, slideSize, sidebarSize) {
const { layoutContextState } = this.props;
const {
input, fullscreen, isRTL, deviceType,
} = layoutContextState;
const { presentation } = input;
const { isOpen } = presentation;
const calculatesMediaBounds = (mediaAreaBounds, slideSize, sidebarSize) => {
const { isOpen } = presentationInput;
const mediaBounds = {};
const { element: fullscreenElement } = fullscreen;
@ -461,9 +241,9 @@ class SmartLayout extends Component {
return mediaBounds;
}
if (input.cameraDock.numCameras > 0 && !input.cameraDock.isDragging) {
if (cameraDockInput.numCameras > 0 && !cameraDockInput.isDragging) {
if (slideSize.width !== 0 && slideSize.height !== 0) {
if (slideSize.width < mediaAreaBounds.width && deviceType !== DEVICE_TYPE.MOBILE) {
if (slideSize.width < mediaAreaBounds.width && !isMobile) {
if (slideSize.width < (mediaAreaBounds.width * 0.8)) {
mediaBounds.width = slideSize.width;
} else {
@ -508,30 +288,35 @@ class SmartLayout extends Component {
mediaBounds.zIndex = 1;
return mediaBounds;
}
};
calculatesLayout() {
const { layoutContextState, layoutContextDispatch } = this.props;
const { deviceType, input, isRTL } = layoutContextState;
const calculatesLayout = () => {
const {
calculatesNavbarBounds,
calculatesActionbarBounds,
calculatesSidebarNavWidth,
calculatesSidebarNavHeight,
calculatesSidebarNavBounds,
calculatesSidebarContentWidth,
calculatesSidebarContentBounds,
calculatesMediaAreaBounds,
isTablet,
} = props;
const { camerasMargin, captionsMargin } = DEFAULT_VALUES;
const sidebarNavWidth = this.calculatesSidebarNavWidth();
const sidebarNavHeight = this.calculatesSidebarNavHeight();
const sidebarContentWidth = this.calculatesSidebarContentWidth();
const sidebarContentHeight = this.calculatesSidebarContentHeight();
const sidebarNavBounds = this
.calculatesSidebarNavBounds(sidebarNavWidth.width, sidebarContentWidth.width);
const sidebarContentBounds = this
.calculatesSidebarContentBounds(sidebarNavWidth.width, sidebarContentWidth.width);
const mediaAreaBounds = this
.calculatesMediaAreaBounds(sidebarNavWidth.width, sidebarContentWidth.width);
const navbarBounds = this.calculatesNavbarBounds(mediaAreaBounds);
const actionbarBounds = this.calculatesActionbarBounds(mediaAreaBounds);
const slideSize = this.calculatesSlideSize(mediaAreaBounds);
const sidebarNavWidth = calculatesSidebarNavWidth();
const sidebarNavHeight = calculatesSidebarNavHeight();
const sidebarContentWidth = calculatesSidebarContentWidth();
const sidebarContentHeight = calculatesSidebarContentHeight();
const sidebarNavBounds = calculatesSidebarNavBounds();
const sidebarContentBounds = calculatesSidebarContentBounds(sidebarNavWidth.width);
const mediaAreaBounds = calculatesMediaAreaBounds(sidebarNavWidth.width, sidebarContentWidth.width);
const navbarBounds = calculatesNavbarBounds(mediaAreaBounds);
const actionbarBounds = calculatesActionbarBounds(mediaAreaBounds);
const slideSize = calculatesSlideSize(mediaAreaBounds);
const sidebarSize = sidebarContentWidth.width + sidebarNavWidth.width;
const mediaBounds = this.calculatesMediaBounds(mediaAreaBounds, slideSize, sidebarSize);
const cameraDockBounds = this
.calculatesCameraDockBounds(mediaAreaBounds, mediaBounds, sidebarSize);
const mediaBounds = calculatesMediaBounds(mediaAreaBounds, slideSize, sidebarSize);
const cameraDockBounds = calculatesCameraDockBounds(mediaAreaBounds, mediaBounds, sidebarSize);
const horizontalCameraDiff = cameraDockBounds.isCameraHorizontal
? cameraDockBounds.width + (camerasMargin * 2)
: 0;
@ -539,7 +324,7 @@ class SmartLayout extends Component {
layoutContextDispatch({
type: ACTIONS.SET_NAVBAR_OUTPUT,
value: {
display: input.navBar.hasNavBar,
display: navbarInput.hasNavBar,
width: navbarBounds.width,
height: navbarBounds.height,
top: navbarBounds.top,
@ -552,7 +337,7 @@ class SmartLayout extends Component {
layoutContextDispatch({
type: ACTIONS.SET_ACTIONBAR_OUTPUT,
value: {
display: input.actionBar.hasActionBar,
display: actionbarInput.hasActionBar,
width: actionbarBounds.width,
height: actionbarBounds.height,
innerHeight: actionbarBounds.innerHeight,
@ -576,7 +361,7 @@ class SmartLayout extends Component {
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_NAVIGATION_OUTPUT,
value: {
display: input.sidebarNavigation.isOpen,
display: sidebarNavigationInput.isOpen,
minWidth: sidebarNavWidth.minWidth,
width: sidebarNavWidth.width,
maxWidth: sidebarNavWidth.maxWidth,
@ -585,8 +370,7 @@ class SmartLayout extends Component {
left: sidebarNavBounds.left,
right: sidebarNavBounds.right,
tabOrder: DEFAULT_VALUES.sidebarNavTabOrder,
isResizable: deviceType !== DEVICE_TYPE.MOBILE
&& deviceType !== DEVICE_TYPE.TABLET,
isResizable: !isMobile && !isTablet,
zIndex: sidebarNavBounds.zIndex,
},
});
@ -604,7 +388,7 @@ class SmartLayout extends Component {
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_OUTPUT,
value: {
display: input.sidebarContent.isOpen,
display: sidebarContentInput.isOpen,
minWidth: sidebarContentWidth.minWidth,
width: sidebarContentWidth.width,
maxWidth: sidebarContentWidth.maxWidth,
@ -612,10 +396,9 @@ class SmartLayout extends Component {
top: sidebarContentBounds.top,
left: sidebarContentBounds.left,
right: sidebarContentBounds.right,
currentPanelType: input.currentPanelType,
currentPanelType,
tabOrder: DEFAULT_VALUES.sidebarContentTabOrder,
isResizable: deviceType !== DEVICE_TYPE.MOBILE
&& deviceType !== DEVICE_TYPE.TABLET,
isResizable: !isMobile && !isTablet,
zIndex: sidebarContentBounds.zIndex,
},
});
@ -641,7 +424,8 @@ class SmartLayout extends Component {
layoutContextDispatch({
type: ACTIONS.SET_CAMERA_DOCK_OUTPUT,
value: {
display: input.cameraDock.numCameras > 0,
display: cameraDockInput.numCameras > 0,
position: cameraDockBounds.position,
minWidth: cameraDockBounds.minWidth,
width: cameraDockBounds.width,
maxWidth: cameraDockBounds.maxWidth,
@ -666,7 +450,7 @@ class SmartLayout extends Component {
layoutContextDispatch({
type: ACTIONS.SET_PRESENTATION_OUTPUT,
value: {
display: input.presentation.isOpen,
display: presentationInput.isOpen,
width: mediaBounds.width,
height: mediaBounds.height,
top: mediaBounds.top,
@ -700,13 +484,9 @@ class SmartLayout extends Component {
right: isRTL ? (mediaBounds.right + horizontalCameraDiff) : null,
},
});
}
};
render() {
return (
<></>
);
}
}
return null;
};
export default LayoutContextFunc.withConsumer(SmartLayout);
export default SmartLayout;

View File

@ -1,26 +1,52 @@
import React, { Component } from 'react';
import { throttle, defaultsDeep } from 'lodash';
import { LayoutContextFunc } from '/imports/ui/components/layout/context';
import { useEffect, useRef } from 'react';
import _ from 'lodash';
import {
layoutDispatch,
layoutSelect,
layoutSelectInput,
layoutSelectOutput
} from '/imports/ui/components/layout/context';
import DEFAULT_VALUES from '/imports/ui/components/layout/defaultValues';
import { INITIAL_INPUT_STATE } from '/imports/ui/components/layout/initState';
import { DEVICE_TYPE, ACTIONS, PANELS } from '/imports/ui/components/layout/enums';
import { ACTIONS, PANELS } from '/imports/ui/components/layout/enums';
const windowWidth = () => window.document.documentElement.clientWidth;
const windowHeight = () => window.document.documentElement.clientHeight;
const min = (value1, value2) => (value1 <= value2 ? value1 : value2);
const max = (value1, value2) => (value1 >= value2 ? value1 : value2);
class VideoFocusLayout extends Component {
constructor(props) {
super(props);
const VideoFocusLayout = (props) => {
const { bannerAreaHeight, isMobile } = props;
this.throttledCalculatesLayout = throttle(() => this.calculatesLayout(),
50, { trailing: true, leading: true });
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
componentDidMount() {
this.init();
const { layoutContextDispatch } = this.props;
const input = layoutSelect((i) => i.input);
const deviceType = layoutSelect((i) => i.deviceType);
const isRTL = layoutSelect((i) => i.isRTL);
const fullscreen = layoutSelect((i) => i.fullscreen);
const fontSize = layoutSelect((i) => i.fontSize);
const currentPanelType = layoutSelect((i) => i.currentPanelType);
const presentationInput = layoutSelectInput((i) => i.presentation);
const sidebarNavigationInput = layoutSelectInput((i) => i.sidebarNavigation);
const sidebarContentInput = layoutSelectInput((i) => i.sidebarContent);
const cameraDockInput = layoutSelectInput((i) => i.cameraDock);
const actionbarInput = layoutSelectInput((i) => i.actionBar);
const navbarInput = layoutSelectInput((i) => i.navBar);
const layoutContextDispatch = layoutDispatch();
const sidebarContentOutput = layoutSelectOutput((i) => i.sidebarContent);
const prevDeviceType = usePrevious(deviceType);
const throttledCalculatesLayout = _.throttle(() => calculatesLayout(),
50, { trailing: true, leading: true });
useEffect(() => {
window.addEventListener('resize', () => {
layoutContextDispatch({
type: ACTIONS.SET_BROWSER_SIZE,
@ -30,77 +56,57 @@ class VideoFocusLayout extends Component {
},
});
});
}
}, []);
shouldComponentUpdate(nextProps) {
const { layoutContextState } = this.props;
return layoutContextState.input !== nextProps.layoutContextState.input
|| layoutContextState.deviceType !== nextProps.layoutContextState.deviceType
|| layoutContextState.isRTL !== nextProps.layoutContextState.isRTL
|| layoutContextState.fontSize !== nextProps.layoutContextState.fontSize
|| layoutContextState.fullscreen !== nextProps.layoutContextState.fullscreen;
}
useEffect(() => {
if (deviceType === null) return;
componentDidUpdate(prevProps) {
const { layoutContextState } = this.props;
const { deviceType } = layoutContextState;
if (prevProps.layoutContextState.deviceType !== deviceType) {
this.init();
if (deviceType !== prevDeviceType) {
// reset layout if deviceType changed
// not all options is supported in all devices
init();
} else {
this.throttledCalculatesLayout();
throttledCalculatesLayout();
}
}
}, [input, deviceType, isRTL, fontSize, fullscreen]);
bannerAreaHeight() {
const { layoutContextState } = this.props;
const { input } = layoutContextState;
const { bannerBar, notificationsBar } = input;
const bannerHeight = bannerBar.hasBanner ? DEFAULT_VALUES.bannerHeight : 0;
const notificationHeight = notificationsBar.hasNotification ? DEFAULT_VALUES.bannerHeight : 0;
return bannerHeight + notificationHeight;
}
init() {
const { layoutContextState, layoutContextDispatch } = this.props;
const { input } = layoutContextState;
const { deviceType } = layoutContextState;
if (deviceType === DEVICE_TYPE.MOBILE) {
const init = () => {
if (isMobile) {
layoutContextDispatch({
type: ACTIONS.SET_LAYOUT_INPUT,
value: defaultsDeep(
value: _.defaultsDeep(
{
sidebarNavigation: {
isOpen: false,
sidebarNavPanel: input.sidebarNavigation.sidebarNavPanel,
sidebarNavPanel: sidebarNavigationInput.sidebarNavPanel,
},
sidebarContent: {
isOpen: false,
sidebarContentPanel: input.sidebarContent.sidebarContentPanel,
sidebarContentPanel: sidebarContentInput.sidebarContentPanel,
},
SidebarContentHorizontalResizer: {
isOpen: false,
},
presentation: {
slidesLength: input.presentation.slidesLength,
isOpen: presentationInput.isOpen,
slidesLength: presentationInput.slidesLength,
currentSlide: {
...input.presentation.currentSlide,
...presentationInput.currentSlide,
},
},
cameraDock: {
numCameras: input.cameraDock.numCameras,
numCameras: cameraDockInput.numCameras,
},
},
INITIAL_INPUT_STATE,
),
});
} else {
const { sidebarContentPanel } = input.sidebarContent;
const { sidebarContentPanel } = sidebarContentInput;
layoutContextDispatch({
type: ACTIONS.SET_LAYOUT_INPUT,
value: defaultsDeep(
value: _.defaultsDeep(
{
sidebarNavigation: {
isOpen: true,
@ -113,206 +119,52 @@ class VideoFocusLayout extends Component {
isOpen: false,
},
presentation: {
slidesLength: input.presentation.slidesLength,
isOpen: presentationInput.isOpen,
slidesLength: presentationInput.slidesLength,
currentSlide: {
...input.presentation.currentSlide,
...presentationInput.currentSlide,
},
},
cameraDock: {
numCameras: input.cameraDock.numCameras,
numCameras: cameraDockInput.numCameras,
},
},
INITIAL_INPUT_STATE,
),
});
}
this.throttledCalculatesLayout();
}
throttledCalculatesLayout();
};
reset() {
this.init();
}
calculatesNavbarBounds(mediaAreaBounds) {
const { layoutContextState } = this.props;
const { isRTL } = layoutContextState;
return {
width: mediaAreaBounds.width,
height: DEFAULT_VALUES.navBarHeight,
top: DEFAULT_VALUES.navBarTop + this.bannerAreaHeight(),
left: !isRTL ? mediaAreaBounds.left : 0,
zIndex: 1,
};
}
calculatesActionbarHeight() {
const { layoutContextState } = this.props;
const { fontSize } = layoutContextState;
const BASE_FONT_SIZE = 14; // 90% font size
const BASE_HEIGHT = DEFAULT_VALUES.actionBarHeight;
const PADDING = DEFAULT_VALUES.actionBarPadding;
const actionBarHeight = ((BASE_HEIGHT / BASE_FONT_SIZE) * fontSize);
return {
height: actionBarHeight + (PADDING * 2),
innerHeight: actionBarHeight,
padding: PADDING,
};
}
calculatesActionbarBounds(mediaAreaBounds) {
const { layoutContextState } = this.props;
const { input, isRTL } = layoutContextState;
const actionBarHeight = this.calculatesActionbarHeight();
return {
display: input.actionBar.hasActionBar,
width: mediaAreaBounds.width,
height: actionBarHeight.height,
innerHeight: actionBarHeight.innerHeight,
padding: actionBarHeight.padding,
top: windowHeight() - actionBarHeight.height,
left: !isRTL ? mediaAreaBounds.left : 0,
zIndex: 1,
};
}
calculatesSidebarNavWidth() {
const { layoutContextState } = this.props;
const { deviceType, input } = layoutContextState;
const {
sidebarNavMinWidth,
sidebarNavMaxWidth,
} = DEFAULT_VALUES;
let minWidth = 0;
let width = 0;
let maxWidth = 0;
if (input.sidebarNavigation.isOpen) {
if (deviceType === DEVICE_TYPE.MOBILE) {
minWidth = windowWidth();
width = windowWidth();
maxWidth = windowWidth();
} else {
if (input.sidebarNavigation.width === 0) {
width = min(max((windowWidth() * 0.2), sidebarNavMinWidth), sidebarNavMaxWidth);
} else {
width = min(max(input.sidebarNavigation.width, sidebarNavMinWidth), sidebarNavMaxWidth);
}
minWidth = sidebarNavMinWidth;
maxWidth = sidebarNavMaxWidth;
}
}
return {
minWidth,
width,
maxWidth,
};
}
calculatesSidebarNavHeight() {
const { layoutContextState } = this.props;
const { deviceType, input } = layoutContextState;
let sidebarNavHeight = 0;
if (input.sidebarNavigation.isOpen) {
if (deviceType === DEVICE_TYPE.MOBILE) {
sidebarNavHeight = windowHeight() - DEFAULT_VALUES.navBarHeight;
} else {
sidebarNavHeight = windowHeight();
}
sidebarNavHeight -= this.bannerAreaHeight();
}
return sidebarNavHeight;
}
calculatesSidebarNavBounds() {
const { layoutContextState } = this.props;
const { deviceType, isRTL } = layoutContextState;
const { sidebarNavTop, navBarHeight, sidebarNavLeft } = DEFAULT_VALUES;
let top = sidebarNavTop + this.bannerAreaHeight();
if (deviceType === DEVICE_TYPE.MOBILE) top = navBarHeight + this.bannerAreaHeight();
return {
top,
left: !isRTL ? sidebarNavLeft : null,
right: isRTL ? sidebarNavLeft : null,
zIndex: deviceType === DEVICE_TYPE.MOBILE ? 10 : 2,
};
}
calculatesSidebarContentWidth() {
const { layoutContextState } = this.props;
const { deviceType, input } = layoutContextState;
const {
sidebarContentMinWidth,
sidebarContentMaxWidth,
} = DEFAULT_VALUES;
let minWidth = 0;
let width = 0;
let maxWidth = 0;
if (input.sidebarContent.isOpen) {
if (deviceType === DEVICE_TYPE.MOBILE) {
minWidth = windowWidth();
width = windowWidth();
maxWidth = windowWidth();
} else {
if (input.sidebarContent.width === 0) {
width = min(
max((windowWidth() * 0.2), sidebarContentMinWidth), sidebarContentMaxWidth,
);
} else {
width = min(max(input.sidebarContent.width, sidebarContentMinWidth),
sidebarContentMaxWidth);
}
minWidth = sidebarContentMinWidth;
maxWidth = sidebarContentMaxWidth;
}
}
return {
minWidth,
width,
maxWidth,
};
}
calculatesSidebarContentHeight() {
const { layoutContextState } = this.props;
const { deviceType, input, output } = layoutContextState;
const { sidebarContent: inputContent, presentation } = input;
const { sidebarContent: outputContent } = output;
const calculatesSidebarContentHeight = () => {
let minHeight = 0;
let height = 0;
let maxHeight = 0;
if (inputContent.isOpen) {
if (deviceType === DEVICE_TYPE.MOBILE) {
height = windowHeight() - DEFAULT_VALUES.navBarHeight - this.bannerAreaHeight();
if (sidebarContentInput.isOpen) {
if (isMobile) {
height = windowHeight() - DEFAULT_VALUES.navBarHeight - bannerAreaHeight();
minHeight = height;
maxHeight = height;
} else if (input.cameraDock.numCameras > 0 && presentation.isOpen) {
if (inputContent.height > 0 && inputContent.height < windowHeight()) {
height = inputContent.height - this.bannerAreaHeight();
} else if (cameraDockInput.numCameras > 0 && presentationInput.isOpen) {
if (sidebarContentInput.height > 0 && sidebarContentInput.height < windowHeight()) {
height = sidebarContentInput.height - bannerAreaHeight();
} else {
const { size: slideSize } = input.presentation.currentSlide;
let calculatedHeight = (windowHeight() - this.bannerAreaHeight()) * 0.3;
const { size: slideSize } = presentationInput.currentSlide;
let calculatedHeight = (windowHeight() - bannerAreaHeight()) * 0.3;
if (slideSize.height > 0 && slideSize.width > 0) {
calculatedHeight = (slideSize.height * outputContent.width) / slideSize.width;
calculatedHeight = (slideSize.height * sidebarContentOutput.width) / slideSize.width;
}
height = windowHeight() - calculatedHeight - this.bannerAreaHeight();
height = windowHeight() - calculatedHeight - bannerAreaHeight();
}
maxHeight = windowHeight() * 0.75 - this.bannerAreaHeight();
minHeight = windowHeight() * 0.25 - this.bannerAreaHeight();
maxHeight = windowHeight() * 0.75 - bannerAreaHeight();
minHeight = windowHeight() * 0.25 - bannerAreaHeight();
if (height > maxHeight) {
height = maxHeight;
}
} else {
height = windowHeight() - this.bannerAreaHeight();
height = windowHeight() - bannerAreaHeight();
maxHeight = height;
minHeight = height;
}
@ -322,118 +174,50 @@ class VideoFocusLayout extends Component {
height,
maxHeight,
};
}
};
calculatesSidebarContentBounds(sidebarNavWidth) {
const { layoutContextState } = this.props;
const { deviceType, isRTL } = layoutContextState;
const { sidebarNavTop, navBarHeight } = DEFAULT_VALUES;
const calculatesCameraDockBounds = (mediaAreaBounds, sidebarSize) => {
const { baseCameraDockBounds } = props;
let top = sidebarNavTop + this.bannerAreaHeight();
const baseBounds = baseCameraDockBounds(mediaAreaBounds, sidebarSize);
if (deviceType === DEVICE_TYPE.MOBILE) top = navBarHeight + this.bannerAreaHeight();
let left = deviceType === DEVICE_TYPE.MOBILE ? 0 : sidebarNavWidth;
let right = deviceType === DEVICE_TYPE.MOBILE ? 0 : sidebarNavWidth;
left = !isRTL ? left : null;
right = isRTL ? right : null;
return {
top,
left,
right,
zIndex: deviceType === DEVICE_TYPE.MOBILE ? 11 : 1,
};
}
calculatesMediaAreaBounds(sidebarNavWidth, sidebarContentWidth) {
const { layoutContextState } = this.props;
const { deviceType, isRTL } = layoutContextState;
const { navBarHeight } = DEFAULT_VALUES;
const { height: actionBarHeight } = this.calculatesActionbarHeight();
let left = 0;
let width = 0;
if (deviceType === DEVICE_TYPE.MOBILE) {
left = 0;
width = windowWidth();
} else {
left = !isRTL ? sidebarNavWidth + sidebarContentWidth : 0;
width = windowWidth() - sidebarNavWidth - sidebarContentWidth;
// do not proceed if using values from LayoutEngine
if (Object.keys(baseBounds).length > 0) {
return baseBounds;
}
return {
width,
height: windowHeight() - (navBarHeight + actionBarHeight + this.bannerAreaHeight()),
top: navBarHeight + this.bannerAreaHeight(),
left,
};
}
calculatesCameraDockBounds(mediaAreaBounds, sidebarSize) {
const { layoutContextState } = this.props;
const {
deviceType, input, fullscreen, isRTL,
} = layoutContextState;
const { cameraDock } = input;
const { numCameras } = cameraDock;
const { navBarHeight } = DEFAULT_VALUES;
const cameraDockBounds = {};
if (numCameras > 0) {
if (deviceType === DEVICE_TYPE.MOBILE) {
cameraDockBounds.minHeight = mediaAreaBounds.height * 0.7;
cameraDockBounds.height = mediaAreaBounds.height * 0.7;
cameraDockBounds.maxHeight = mediaAreaBounds.height * 0.7;
} else {
cameraDockBounds.minHeight = mediaAreaBounds.height;
cameraDockBounds.height = mediaAreaBounds.height;
cameraDockBounds.maxHeight = mediaAreaBounds.height;
}
cameraDockBounds.top = DEFAULT_VALUES.navBarHeight;
cameraDockBounds.left = !isRTL ? mediaAreaBounds.left : null;
cameraDockBounds.right = isRTL ? sidebarSize : null;
cameraDockBounds.minWidth = mediaAreaBounds.width;
cameraDockBounds.width = mediaAreaBounds.width;
cameraDockBounds.maxWidth = mediaAreaBounds.width;
cameraDockBounds.zIndex = 1;
if (fullscreen.group === 'webcams') {
cameraDockBounds.width = windowWidth();
cameraDockBounds.minWidth = windowWidth();
cameraDockBounds.maxWidth = windowWidth();
cameraDockBounds.height = windowHeight();
cameraDockBounds.minHeight = windowHeight();
cameraDockBounds.maxHeight = windowHeight();
cameraDockBounds.top = 0;
cameraDockBounds.left = 0;
cameraDockBounds.zIndex = 99;
}
return cameraDockBounds;
if (isMobile) {
cameraDockBounds.minHeight = mediaAreaBounds.height * 0.7;
cameraDockBounds.height = mediaAreaBounds.height * 0.7;
cameraDockBounds.maxHeight = mediaAreaBounds.height * 0.7;
} else {
cameraDockBounds.minHeight = mediaAreaBounds.height;
cameraDockBounds.height = mediaAreaBounds.height;
cameraDockBounds.maxHeight = mediaAreaBounds.height;
}
cameraDockBounds.top = 0;
cameraDockBounds.left = 0;
cameraDockBounds.minWidth = 0;
cameraDockBounds.height = 0;
cameraDockBounds.width = 0;
cameraDockBounds.maxWidth = 0;
cameraDockBounds.zIndex = 0;
return cameraDockBounds;
}
cameraDockBounds.top = navBarHeight;
cameraDockBounds.left = !isRTL ? mediaAreaBounds.left : null;
cameraDockBounds.right = isRTL ? sidebarSize : null;
cameraDockBounds.minWidth = mediaAreaBounds.width;
cameraDockBounds.width = mediaAreaBounds.width;
cameraDockBounds.maxWidth = mediaAreaBounds.width;
cameraDockBounds.zIndex = 1;
calculatesMediaBounds(
return cameraDockBounds;
};
const calculatesMediaBounds = (
mediaAreaBounds,
cameraDockBounds,
sidebarNavWidth,
sidebarContentWidth,
sidebarContentHeight,
) {
const { layoutContextState } = this.props;
const {
deviceType, input, fullscreen, isRTL,
} = layoutContextState;
) => {
const mediaBounds = {};
const { element: fullscreenElement } = fullscreen;
const sidebarSize = sidebarNavWidth + sidebarContentWidth;
@ -448,12 +232,12 @@ class VideoFocusLayout extends Component {
return mediaBounds;
}
if (deviceType === DEVICE_TYPE.MOBILE) {
if (isMobile) {
mediaBounds.height = mediaAreaBounds.height - cameraDockBounds.height;
mediaBounds.left = mediaAreaBounds.left;
mediaBounds.top = mediaAreaBounds.top + cameraDockBounds.height;
mediaBounds.width = mediaAreaBounds.width;
} else if (input.cameraDock.numCameras > 0) {
} else if (cameraDockInput.numCameras > 0) {
mediaBounds.height = windowHeight() - sidebarContentHeight;
mediaBounds.left = !isRTL ? sidebarNavWidth : 0;
mediaBounds.right = isRTL ? sidebarNavWidth : 0;
@ -463,50 +247,55 @@ class VideoFocusLayout extends Component {
} else {
mediaBounds.height = mediaAreaBounds.height;
mediaBounds.width = mediaAreaBounds.width;
mediaBounds.top = DEFAULT_VALUES.navBarHeight + this.bannerAreaHeight();
mediaBounds.top = DEFAULT_VALUES.navBarHeight + bannerAreaHeight();
mediaBounds.left = !isRTL ? mediaAreaBounds.left : null;
mediaBounds.right = isRTL ? sidebarSize : null;
mediaBounds.zIndex = 1;
}
return mediaBounds;
}
};
calculatesLayout() {
const { layoutContextState, layoutContextDispatch } = this.props;
const { deviceType, input, isRTL } = layoutContextState;
const calculatesLayout = () => {
const {
calculatesNavbarBounds,
calculatesActionbarBounds,
calculatesSidebarNavWidth,
calculatesSidebarNavHeight,
calculatesSidebarNavBounds,
calculatesSidebarContentWidth,
calculatesSidebarContentBounds,
calculatesMediaAreaBounds,
isTablet,
} = props;
const { captionsMargin } = DEFAULT_VALUES;
const sidebarNavWidth = this.calculatesSidebarNavWidth();
const sidebarNavHeight = this.calculatesSidebarNavHeight();
const sidebarContentWidth = this.calculatesSidebarContentWidth();
const sidebarNavBounds = this.calculatesSidebarNavBounds(
const sidebarNavWidth = calculatesSidebarNavWidth();
const sidebarNavHeight = calculatesSidebarNavHeight();
const sidebarContentWidth = calculatesSidebarContentWidth();
const sidebarNavBounds = calculatesSidebarNavBounds();
const sidebarContentBounds = calculatesSidebarContentBounds(sidebarNavWidth.width);
const mediaAreaBounds = calculatesMediaAreaBounds(
sidebarNavWidth.width, sidebarContentWidth.width,
);
const sidebarContentBounds = this.calculatesSidebarContentBounds(
sidebarNavWidth.width, sidebarContentWidth.width,
);
const mediaAreaBounds = this.calculatesMediaAreaBounds(
sidebarNavWidth.width, sidebarContentWidth.width,
);
const navbarBounds = this.calculatesNavbarBounds(mediaAreaBounds);
const actionbarBounds = this.calculatesActionbarBounds(mediaAreaBounds);
const navbarBounds = calculatesNavbarBounds(mediaAreaBounds);
const actionbarBounds = calculatesActionbarBounds(mediaAreaBounds);
const sidebarSize = sidebarContentWidth.width + sidebarNavWidth.width;
const cameraDockBounds = this.calculatesCameraDockBounds(mediaAreaBounds, sidebarSize);
const sidebarContentHeight = this.calculatesSidebarContentHeight();
const mediaBounds = this.calculatesMediaBounds(
const cameraDockBounds = calculatesCameraDockBounds(mediaAreaBounds, sidebarSize);
const sidebarContentHeight = calculatesSidebarContentHeight();
const mediaBounds = calculatesMediaBounds(
mediaAreaBounds,
cameraDockBounds,
sidebarNavWidth.width,
sidebarContentWidth.width,
sidebarContentHeight.height,
);
const isBottomResizable = input.cameraDock.numCameras > 0;
const isBottomResizable = cameraDockInput.numCameras > 0;
layoutContextDispatch({
type: ACTIONS.SET_NAVBAR_OUTPUT,
value: {
display: input.navBar.hasNavBar,
display: navbarInput.hasNavBar,
width: navbarBounds.width,
height: navbarBounds.height,
top: navbarBounds.top,
@ -519,7 +308,7 @@ class VideoFocusLayout extends Component {
layoutContextDispatch({
type: ACTIONS.SET_ACTIONBAR_OUTPUT,
value: {
display: input.actionBar.hasActionBar,
display: actionbarInput.hasActionBar,
width: actionbarBounds.width,
height: actionbarBounds.height,
innerHeight: actionbarBounds.innerHeight,
@ -543,7 +332,7 @@ class VideoFocusLayout extends Component {
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_NAVIGATION_OUTPUT,
value: {
display: input.sidebarNavigation.isOpen,
display: sidebarNavigationInput.isOpen,
minWidth: sidebarNavWidth.minWidth,
width: sidebarNavWidth.width,
maxWidth: sidebarNavWidth.maxWidth,
@ -552,8 +341,7 @@ class VideoFocusLayout extends Component {
left: sidebarNavBounds.left,
right: sidebarNavBounds.right,
tabOrder: DEFAULT_VALUES.sidebarNavTabOrder,
isResizable: deviceType !== DEVICE_TYPE.MOBILE
&& deviceType !== DEVICE_TYPE.TABLET,
isResizable: !isMobile && !isTablet,
zIndex: sidebarNavBounds.zIndex,
},
});
@ -571,7 +359,7 @@ class VideoFocusLayout extends Component {
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_OUTPUT,
value: {
display: input.sidebarContent.isOpen,
display: sidebarContentInput.isOpen,
minWidth: sidebarContentWidth.minWidth,
width: sidebarContentWidth.width,
maxWidth: sidebarContentWidth.maxWidth,
@ -581,10 +369,9 @@ class VideoFocusLayout extends Component {
top: sidebarContentBounds.top,
left: sidebarContentBounds.left,
right: sidebarContentBounds.right,
currentPanelType: input.currentPanelType,
currentPanelType,
tabOrder: DEFAULT_VALUES.sidebarContentTabOrder,
isResizable: deviceType !== DEVICE_TYPE.MOBILE
&& deviceType !== DEVICE_TYPE.TABLET,
isResizable: !isMobile && !isTablet,
zIndex: sidebarContentBounds.zIndex,
},
});
@ -610,7 +397,7 @@ class VideoFocusLayout extends Component {
layoutContextDispatch({
type: ACTIONS.SET_CAMERA_DOCK_OUTPUT,
value: {
display: input.cameraDock.numCameras > 0,
display: cameraDockInput.numCameras > 0,
minWidth: cameraDockBounds.minWidth,
width: cameraDockBounds.width,
maxWidth: cameraDockBounds.maxWidth,
@ -635,15 +422,14 @@ class VideoFocusLayout extends Component {
layoutContextDispatch({
type: ACTIONS.SET_PRESENTATION_OUTPUT,
value: {
display: input.presentation.isOpen,
display: presentationInput.isOpen,
width: mediaBounds.width,
height: mediaBounds.height,
top: mediaBounds.top,
left: mediaBounds.left,
right: isRTL ? mediaBounds.right : null,
tabOrder: DEFAULT_VALUES.presentationTabOrder,
isResizable: deviceType !== DEVICE_TYPE.MOBILE
&& deviceType !== DEVICE_TYPE.TABLET,
isResizable: !isMobile && !isTablet,
zIndex: mediaBounds.zIndex,
},
});
@ -680,11 +466,9 @@ class VideoFocusLayout extends Component {
right: isRTL ? mediaBounds.right : null,
},
});
}
};
render() {
return <></>;
}
}
return null;
};
export default LayoutContextFunc.withConsumer(VideoFocusLayout);
export default VideoFocusLayout;

View File

@ -30,7 +30,7 @@ const getLearningDashboardAccessToken = () => ((
const openLearningDashboardUrl = (lang) => {
const cookieExpiresDate = new Date();
cookieExpiresDate.setTime(cookieExpiresDate.getTime() + 3600000);
cookieExpiresDate.setTime(cookieExpiresDate.getTime() + (3600000 * 24 * 30)); // keep cookie 30d
document.cookie = `learningDashboardAccessToken-${Auth.meetingID}=${getLearningDashboardAccessToken()}; expires=${cookieExpiresDate.toGMTString()}; path=/`;
window.open(`/learning-dashboard/?meeting=${Auth.meetingID}&lang=${lang}`, '_blank');
};

View File

@ -47,9 +47,14 @@ const swapLayout = {
tracker: new Tracker.Dependency(),
};
const setSwapLayout = () => {
const setSwapLayout = (layoutContextDispatch) => {
swapLayout.value = getFromUserSettings('bbb_auto_swap_layout', LAYOUT_CONFIG.autoSwapLayout);
swapLayout.tracker.changed();
layoutContextDispatch({
type: ACTIONS.SET_PRESENTATION_IS_OPEN,
value: !swapLayout.value,
});
};
const toggleSwapLayout = (layoutContextDispatch) => {

View File

@ -9,6 +9,8 @@ import { Divider } from "@material-ui/core";
import Icon from "/imports/ui/components/icon/component";
import Button from "/imports/ui/components/button/component";
import { ENTER, SPACE } from "/imports/utils/keyCodes";
import { styles } from "./styles";
const intlMessages = defineMessages({
@ -106,6 +108,11 @@ class BBBMenu extends React.Component {
this.opts.autoFocus = !(['mouse', 'touch'].includes(e.nativeEvent.pointerType));
this.handleClick(e);
}}
onKeyPress={(e) => {
e.persist();
if (e.which !== ENTER) return null;
this.handleClick(e);
}}
accessKey={this.props?.accessKey}
>
{trigger}
@ -117,6 +124,7 @@ class BBBMenu extends React.Component {
open={Boolean(anchorEl)}
onClose={this.handleClose}
className={menuClasses.join(' ')}
style={{ zIndex: 9999 }}
>
{actionsItems}
{anchorEl && window.innerWidth < MAX_WIDTH &&

View File

@ -10,6 +10,11 @@
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
[dir="rtl"] & {
margin-right: .5rem;
margin-left: 1.65rem;
}
}
.closeBtn {

View File

@ -2,6 +2,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ReactModal from 'react-modal';
import { styles } from './styles.scss';
import { registerTitleView, unregisterTitleView } from '/imports/utils/dom-utils';
const propTypes = {
overlayClassName: PropTypes.string.isRequired,
@ -19,6 +20,15 @@ const defaultProps = {
};
export default class ModalBase extends Component {
componentDidMount() {
registerTitleView(this.props.contentLabel);
}
componentWillUnmount() {
unregisterTitleView();
}
render() {
if (!this.props.isOpen) return null;
@ -27,8 +37,8 @@ export default class ModalBase extends Component {
{...this.props}
parentSelector={() => {
if (document.fullscreenElement &&
document.fullscreenElement.nodeName &&
document.fullscreenElement.nodeName.toLowerCase() === 'div')
document.fullscreenElement.nodeName &&
document.fullscreenElement.nodeName.toLowerCase() === 'div')
return document.fullscreenElement;
else return document.body;
}}
@ -55,12 +65,12 @@ export const withModalState = ComponentToWrap =>
this.show = this.show.bind(this);
}
hide(cb = () => {}) {
hide(cb = () => { }) {
Promise.resolve(cb())
.then(() => this.setState({ isOpen: false }));
}
show(cb = () => {}) {
show(cb = () => { }) {
Promise.resolve(cb())
.then(() => this.setState({ isOpen: true }));
}

View File

@ -11,7 +11,7 @@ import { UsersContext } from '/imports/ui/components/components-data/users-conte
import NoteService from '/imports/ui/components/note/service';
import Service from './service';
import NavBar from './component';
import LayoutContext from '../layout/context';
import { layoutSelectInput, layoutSelectOutput, layoutDispatch } from '../layout/context';
const PUBLIC_CONFIG = Meteor.settings.public;
const ROLE_MODERATOR = PUBLIC_CONFIG.user.role_moderator;
@ -28,8 +28,6 @@ const checkUnreadMessages = ({
};
const NavBarContainer = ({ children, ...props }) => {
const layoutContext = useContext(LayoutContext);
const { layoutContextState, layoutContextDispatch } = layoutContext;
const usingChatContext = useContext(ChatContext);
const usingUsersContext = useContext(UsersContext);
const usingGroupChatContext = useContext(GroupChatContext);
@ -37,13 +35,15 @@ const NavBarContainer = ({ children, ...props }) => {
const { users } = usingUsersContext;
const { groupChat: groupChats } = usingGroupChatContext;
const { ...rest } = props;
const {
input, output,
} = layoutContextState;
const { sidebarContent, sidebarNavigation } = input;
const { sidebarNavPanel } = sidebarNavigation;
const sidebarContent = layoutSelectInput((i) => i.sidebarContent);
const sidebarNavigation = layoutSelectInput((i) => i.sidebarNavigation);
const navBar = layoutSelectOutput((i) => i.navBar);
const layoutContextDispatch = layoutDispatch();
const { sidebarContentPanel } = sidebarContent;
const { navBar } = output;
const { sidebarNavPanel } = sidebarNavigation;
const hasUnreadNotes = NoteService.hasUnreadNotes(sidebarContentPanel);
const hasUnreadMessages = checkUnreadMessages(
{ groupChatsMessages, groupChats, users: users[Auth.meetingID] },

View File

@ -1,4 +1,4 @@
import React, { useContext } from 'react';
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import VoiceUsers from '/imports/api/voice-users';
import Auth from '/imports/ui/services/auth';
@ -7,7 +7,7 @@ import TalkingIndicator from './component';
import { makeCall } from '/imports/ui/services/api';
import { meetingIsBreakout } from '/imports/ui/components/app/service';
import Service from './service';
import LayoutContext from '../../layout/context';
import { layoutSelectInput, layoutDispatch } from '../../layout/context';
const APP_CONFIG = Meteor.settings.public.app;
const { enableTalkingIndicator } = APP_CONFIG;
@ -15,12 +15,13 @@ const TALKING_INDICATOR_MUTE_INTERVAL = 500;
const TalkingIndicatorContainer = (props) => {
if (!enableTalkingIndicator) return null;
const layoutContext = useContext(LayoutContext);
const { layoutContextState, layoutContextDispatch } = layoutContext;
const { input } = layoutContextState;
const { sidebarContent, sidebarNavigation } = input;
const { sidebarNavPanel } = sidebarNavigation;
const sidebarContent = layoutSelectInput((i) => i.sidebarContent);
const { sidebarContentPanel } = sidebarContent;
const sidebarNavigation = layoutSelectInput((i) => i.sidebarNavigation);
const { sidebarNavPanel } = sidebarNavigation;
const layoutContextDispatch = layoutDispatch();
const sidebarNavigationIsOpen = sidebarNavigation.isOpen;
const sidebarContentIsOpen = sidebarContent.isOpen;
return (

View File

@ -1,15 +1,14 @@
import React, { useContext } from 'react';
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import Note from './component';
import NoteService from './service';
import LayoutContext from '../layout/context';
import { layoutSelectInput, layoutDispatch } from '../layout/context';
const NoteContainer = ({ children, ...props }) => {
const layoutContext = useContext(LayoutContext);
const { layoutContextDispatch, layoutContextState } = layoutContext;
const { input } = layoutContextState;
const { cameraDock } = input;
const cameraDock = layoutSelectInput((i) => i.cameraDock);
const { isResizing } = cameraDock;
const layoutContextDispatch = layoutDispatch();
return (
<Note {...{ layoutContextDispatch, isResizing, ...props }}>
{children}

View File

@ -1,13 +1,13 @@
import { Meteor } from 'meteor/meteor';
import { withTracker } from 'meteor/react-meteor-data';
import React, { useContext, useEffect } from 'react';
import React, { useEffect } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import _ from 'lodash';
import Auth from '/imports/ui/services/auth';
import Meetings, { MeetingTimeRemaining } from '/imports/api/meetings';
import BreakoutRemainingTime from '/imports/ui/components/breakout-room/breakout-remaining-time/container';
import { styles } from './styles.scss';
import LayoutContext from '../layout/context';
import { layoutSelectInput, layoutDispatch } from '../layout/context';
import { ACTIONS } from '../layout/enums';
import breakoutService from '/imports/ui/components/breakout-room/service';
@ -76,10 +76,10 @@ const intlMessages = defineMessages({
const NotificationsBarContainer = (props) => {
const { message, color } = props;
const layoutContext = useContext(LayoutContext);
const { layoutContextState, layoutContextDispatch } = layoutContext;
const { input } = layoutContextState;
const { notificationsBar } = input;
const notificationsBar = layoutSelectInput((i) => i.notificationsBar);
const layoutContextDispatch = layoutDispatch();
const { hasNotification } = notificationsBar;
useEffect(() => {

View File

@ -11,6 +11,7 @@ import LiveResult from './live-result/component';
import { styles } from './styles.scss';
import { PANELS, ACTIONS } from '../layout/enums';
import DragAndDrop from './dragAndDrop/component';
import { alertScreenReader } from '/imports/utils/dom-utils';
const intlMessages = defineMessages({
pollPaneTitle: {
@ -185,6 +186,14 @@ const intlMessages = defineMessages({
id: 'app.switch.offLabel',
description: 'label for toggle switch off state',
},
removePollOpt: {
id: 'app.poll.removePollOpt',
description: 'screen reader alert for removed poll option',
},
emptyPollOpt: {
id: 'app.poll.emptyPollOpt',
description: 'screen reader for blank poll option',
},
});
const POLL_SETTINGS = Meteor.settings.public.poll;
@ -295,10 +304,15 @@ class Poll extends Component {
}
handleRemoveOption(index) {
const { intl } = this.props;
const { optList } = this.state;
const list = [...optList];
const removed = list[index];
list.splice(index, 1);
this.setState({ optList: list });
this.setState({ optList: list }, () => {
alertScreenReader(`${intl.formatMessage(intlMessages.removePollOpt,
{ 0: removed.val || intl.formatMessage(intlMessages.emptyPollOpt) })}`);
});
}
handleAddOption() {
@ -395,7 +409,10 @@ class Poll extends Component {
this.handleRemoveOption(i);
}}
/>
<span className="sr-only" id={`option-${i}`}>{intl.formatMessage(intlMessages.deleteRespDesc, { 0: o.val })}</span>
<span className="sr-only" id={`option-${i}`}>
{intl.formatMessage(intlMessages.deleteRespDesc,
{ 0: (o.val || intl.formatMessage(intlMessages.emptyPollOpt)) })}
</span>
</>
)
: <div style={{ width: '40px' }} />}

View File

@ -8,14 +8,14 @@ import { Session } from 'meteor/session';
import Service from './service';
import Auth from '/imports/ui/services/auth';
import { UsersContext } from '../components-data/users-context/context';
import LayoutContext from '../layout/context';
import { layoutDispatch } from '../layout/context';
const CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_CHAT_KEY = CHAT_CONFIG.public_id;
const PollContainer = ({ ...props }) => {
const layoutContext = useContext(LayoutContext);
const { layoutContextDispatch } = layoutContext;
const layoutContextDispatch = layoutDispatch();
const usingUsersContext = useContext(UsersContext);
const { users } = usingUsersContext;

View File

@ -1,6 +1,6 @@
import Users from '/imports/api/users';
import Auth from '/imports/ui/services/auth';
import Polls from '/imports/api/polls';
import { CurrentPoll } from '/imports/api/polls';
import caseInsensitiveReducer from '/imports/utils/caseInsensitiveReducer';
import { defineMessages } from 'react-intl';
@ -217,7 +217,7 @@ export default {
{ fields: { presenter: 1 } },
).presenter,
pollTypes,
currentPoll: () => Polls.findOne({ meetingId: Auth.meetingID }),
currentPoll: () => CurrentPoll.findOne({ meetingId: Auth.meetingID }),
pollAnswerIds,
POLL_AVATAR_COLOR,
isDefaultPoll,

View File

@ -761,6 +761,7 @@ class Presentation extends PureComponent {
<span className={styles.toastDownload}>
<div className={toastStyles.separator} />
<a
data-test="toastDownload"
className={styles.downloadBtn}
aria-label={`${intl.formatMessage(intlMessages.downloadLabel)} ${currentPresentation.name}`}
href={downloadPresentationUri}

View File

@ -2,7 +2,6 @@ import React, { useContext } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import MediaService, { getSwapLayout, shouldEnableSwapLayout } from '/imports/ui/components/media/service';
import { notify } from '/imports/ui/services/notification';
import { Session } from 'meteor/session';
import PresentationService from './service';
import { Slides } from '/imports/api/slides';
import Presentation from '/imports/ui/components/presentation/component';
@ -11,25 +10,32 @@ import { UsersContext } from '../components-data/users-context/context';
import Auth from '/imports/ui/services/auth';
import Meetings from '/imports/api/meetings';
import getFromUserSettings from '/imports/ui/services/users-settings';
import LayoutContext from '../layout/context';
import {
layoutSelect,
layoutSelectInput,
layoutSelectOutput,
layoutDispatch,
} from '../layout/context';
import WhiteboardService from '/imports/ui/components/whiteboard/service';
import { DEVICE_TYPE } from '../layout/enums';
const ROLE_VIEWER = Meteor.settings.public.user.role_viewer;
const PresentationContainer = ({ presentationPodIds, mountPresentation, ...props }) => {
const fullscreenElementId = 'Presentation';
const layoutContext = useContext(LayoutContext);
const { layoutContextState, layoutContextDispatch } = layoutContext;
const {
input, output, layoutType, fullscreen, deviceType,
} = layoutContextState;
const { cameraDock } = input;
const { numCameras } = cameraDock;
const { presentation } = output;
const { element } = fullscreen;
const fullscreenContext = (element === fullscreenElementId);
const { layoutSwapped, podId } = props;
const cameraDock = layoutSelectInput((i) => i.cameraDock);
const presentation = layoutSelectOutput((i) => i.presentation);
const layoutType = layoutSelect((i) => i.layoutType);
const fullscreen = layoutSelect((i) => i.fullscreen);
const deviceType = layoutSelect((i) => i.deviceType);
const layoutContextDispatch = layoutDispatch();
const { numCameras } = cameraDock;
const { element } = fullscreen;
const fullscreenElementId = 'Presentation';
const fullscreenContext = (element === fullscreenElementId);
const isIphone = !!(navigator.userAgent.match(/iPhone/i));
const usingUsersContext = useContext(UsersContext);

View File

@ -37,6 +37,7 @@ const DownloadPresentationButton = ({
return (
<div className={wrapperClassName}>
<Button
data-test="presentationDownload"
color="default"
icon="template_download"
size="sm"

View File

@ -1,12 +1,9 @@
import React, { useContext } from 'react';
import LayoutContext from '../../layout/context';
import React from 'react';
import { layoutSelectOutput } from '../../layout/context';
import PresentationArea from './component';
const PresentationAreaContainer = () => {
const layoutManager = useContext(LayoutContext);
const { layoutContextState } = layoutManager;
const { output } = layoutContextState;
const { presentation } = output;
const presentation = layoutSelectOutput((i) => i.presentation);
return <PresentationArea {...{ ...presentation }} />;
};

View File

@ -23,6 +23,7 @@ const PresentationPlaceholder = ({
<div
ref={(ref) => setPresentationRef(ref)}
className={styles.presentationPlaceholder}
data-test="presentationPlaceholder"
style={{
top,
left,

View File

@ -13,6 +13,7 @@ import logger from '/imports/startup/client/logger';
import { notify } from '/imports/ui/services/notification';
import { toast } from 'react-toastify';
import _ from 'lodash';
import { registerTitleView, unregisterTitleView } from '/imports/utils/dom-utils';
import { styles } from './styles';
const { isMobile } = deviceInfo;
@ -214,6 +215,10 @@ const intlMessages = defineMessages({
id: 'app.presentationUploder.clearErrorsDesc',
description: 'aria description for button clearing upload error',
},
uploadViewTitle: {
id: 'app.presentationUploder.uploadViewTitle',
description: 'view name apended to document title',
}
});
class PresentationUploader extends Component {
@ -251,10 +256,16 @@ class PresentationUploader extends Component {
}
componentDidUpdate(prevProps) {
const { isOpen, presentations: propPresentations } = this.props;
const { isOpen, presentations: propPresentations, intl } = this.props;
const { presentations } = this.state;
if (!isOpen && prevProps.isOpen) {
unregisterTitleView();
}
// Updates presentation list when chat modal opens to avoid missing presentations
if (isOpen && !prevProps.isOpen) {
registerTitleView(intl.formatMessage(intlMessages.uploadViewTitle));
const focusableElements =
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
const modal = document.getElementById('upload-modal');
@ -728,6 +739,7 @@ class PresentationUploader extends Component {
const {
intl,
selectedToBeNextCurrent,
allowDownloadable
} = this.props;
const isActualCurrent = selectedToBeNextCurrent ? item.id === selectedToBeNextCurrent : item.isCurrent;
@ -747,6 +759,10 @@ class PresentationUploader extends Component {
[styles.tableItemError]: hasError,
[styles.tableItemAnimated]: isProcessing,
};
const itemActions = {
[styles.notDownloadable]: !allowDownloadable,
};
const formattedDownloadableLabel = !item.isDownloadable
? intl.formatMessage(intlMessages.isDownloadable)
@ -784,17 +800,21 @@ class PresentationUploader extends Component {
{this.renderPresentationItemStatus(item)}
</td>
{hasError ? null : (
<td className={styles.tableItemActions}>
<Button
disabled={disableActions}
className={isDownloadableStyle}
label={formattedDownloadableLabel}
aria-label={formattedDownloadableAriaLabel}
hideLabel
size="sm"
icon={item.isDownloadable ? 'download' : 'download-off'}
onClick={() => this.handleToggleDownloadable(item)}
/>
<td className={cx(styles.tableItemActions, itemActions)}>
{allowDownloadable ? (
<Button
disabled={disableActions}
className={isDownloadableStyle}
label={formattedDownloadableLabel}
data-test={item.isDownloadable ? 'disallowPresentationDownload' : 'allowPresentationDownload'}
aria-label={formattedDownloadableAriaLabel}
hideLabel
size="sm"
icon={item.isDownloadable ? 'download' : 'download-off'}
onClick={() => this.handleToggleDownloadable(item)}
/>
) : null
}
<Checkbox
ariaLabel={`${intl.formatMessage(intlMessages.setAsCurrentPresentation)} ${item.filename}`}
checked={item.isCurrent}
@ -807,6 +827,7 @@ class PresentationUploader extends Component {
disabled={disableActions}
className={cx(styles.itemAction, styles.itemActionRemove)}
label={intl.formatMessage(intlMessages.removePresentation)}
data-test="removePresentation"
aria-label={`${intl.formatMessage(intlMessages.removePresentation)} ${item.filename}`}
size="sm"
icon="delete"
@ -995,6 +1016,7 @@ class PresentationUploader extends Component {
/>
<Button
className={styles.confirm}
data-test="confirmManagePresentation"
color="primary"
onClick={() => this.handleConfirm(hasNewUpload)}
disabled={disableActions}

View File

@ -21,14 +21,15 @@ const PresentationUploaderContainer = (props) => (
export default withTracker(() => {
const currentPresentations = Service.getPresentations();
const {
dispatchDisableDownloadable,
dispatchEnableDownloadable,
dispatchTogglePresentationDownloadable,
} = Service;
dispatchDisableDownloadable,
dispatchEnableDownloadable,
dispatchTogglePresentationDownloadable,
} = Service;
return {
presentations: currentPresentations,
fileValidMimeTypes: PRESENTATION_CONFIG.uploadValidMimeTypes,
allowDownloadable: PRESENTATION_CONFIG.allowDownloadable,
handleSave: (presentations) => Service.persistPresentationChanges(
currentPresentations,
presentations,

View File

@ -103,6 +103,10 @@
}
}
.notDownloadable {
min-width: 48px;
}
.tableItemIcon > i {
font-size: 1.35rem;
}

View File

@ -269,7 +269,7 @@ class ScreenshareComponent extends React.Component {
{
isGloballyBroadcasting
? (
<div>
<div data-test="isSharingScreen">
{!switched
&& ScreenshareComponent.renderScreenshareContainerInside(
intl.formatMessage(intlMessages.presenterSharingLabel),

View File

@ -1,4 +1,4 @@
import React, { useContext } from 'react';
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import Users from '/imports/api/users/';
import Auth from '/imports/ui/services/auth';
@ -8,15 +8,15 @@ import {
isGloballyBroadcasting,
} from './service';
import ScreenshareComponent from './component';
import LayoutContext from '../layout/context';
import { layoutSelect, layoutSelectOutput, layoutDispatch } from '../layout/context';
const ScreenshareContainer = (props) => {
const fullscreenElementId = 'Screenshare';
const layoutContext = useContext(LayoutContext);
const { layoutContextState, layoutContextDispatch } = layoutContext;
const { output, fullscreen } = layoutContextState;
const { screenShare } = output;
const screenShare = layoutSelectOutput((i) => i.screenShare);
const fullscreen = layoutSelect((i) => i.fullscreen);
const layoutContextDispatch = layoutDispatch();
const { element } = fullscreen;
const fullscreenElementId = 'Screenshare';
const fullscreenContext = (element === fullscreenElementId);
if (isVideoBroadcasting()) {

Some files were not shown because too many files have changed in this diff Show More