Merge remote-tracking branch 'bigbluebutton/master' into merge-master-into-2x

# Conflicts:
#	bigbluebutton-client/resources/config.xml.template
#	bigbluebutton-client/src/org/bigbluebutton/modules/phone/PhoneOptions.as
#	bigbluebutton-client/src/org/bigbluebutton/modules/phone/views/components/ToolbarButton.mxml
#	record-and-playback/core/scripts/rap-process-worker.rb
This commit is contained in:
Ghazi Triki 2017-05-22 17:35:24 +01:00
commit 7d283cd154
68 changed files with 875 additions and 620 deletions

View File

@ -93,8 +93,17 @@ if (request.getParameterMap().isEmpty()) {
//
String username = request.getParameter("username");
String meetingname = request.getParameter("meetingname");
boolean isModerator = Boolean.parseBoolean(request.getParameter("isModerator"));
// set defaults and overwrite them if custom values exist
String meetingname = "Demo Meeting";
if (request.getParameter("meetingname") != null) {
meetingname = request.getParameter("meetingname");
}
boolean isModerator = false;
if (request.getParameter("isModerator") != null) {
isModerator = Boolean.parseBoolean(request.getParameter("isModerator"));
}
String joinURL = getJoinURLHTML5(username, meetingname, "false", null, null, null, isModerator);
Document doc = null;

View File

@ -465,7 +465,7 @@
file="BigBlueButton.html"
height="100%"
width="100%"
bgcolor="grey"
bgcolor="white"
application="BBB"
swf="BigBlueButton"
version-major="10"

View File

@ -55,8 +55,6 @@
uri="rtmp://HOST/sip"
autoJoin="true"
listenOnlyMode="true"
forceListenOnly="false"
presenterShareOnly="false"
skipCheck="false"
showButton="true"
enabledEchoCancel="true"
@ -71,7 +69,6 @@
uri="rtmp://HOST/video"
dependson = "UsersModule"
baseTabIndex="401"
presenterShareOnly = "false"
controlsForPresenter = "false"
autoStart = "false"
skipCamSettingsCheck="false"

View File

@ -43,7 +43,7 @@
var flashvars = {};
var params = {};
params.quality = "high";
params.bgcolor = "#869ca7";
params.bgcolor = "#FFFFFF";
params.allowfullscreen = "true";
params.allowfullscreeninteractive = "true";
@ -76,7 +76,7 @@
var fillContent = function(){
var content = document.getElementById("content");
if (content) {
content.innerHTML = '<object type="application/x-shockwave-flash" id="BigBlueButton" name="BigBlueButton" tabindex="0" data="BigBlueButton.swf?v=VERSION" style="position: relative; top: 0.5px;" width="100%" height="100%" align="middle"><param name="quality" value="high"><param name="bgcolor" value="#869ca7"><param name="allowfullscreen" value="true"><param name="wmode" value="window"><param name="allowscriptaccess" value="true"><param name="seamlesstabbing" value="true"></object>';
content.innerHTML = '<object type="application/x-shockwave-flash" id="BigBlueButton" name="BigBlueButton" tabindex="0" data="BigBlueButton.swf?v=VERSION" style="position: relative; top: 0.5px;" width="100%" height="100%" align="middle"><param name="quality" value="high"><param name="bgcolor" value="#FFFFFF"><param name="allowfullscreen" value="true"><param name="wmode" value="window"><param name="allowscriptaccess" value="true"><param name="seamlesstabbing" value="true"></object>';
}
};
} else {
@ -197,7 +197,7 @@
<button id="enterFlash" type="button" class="visually-hidden" onclick="startFlashFocus();">Set focus to client</button>
<div id="content">
<div id="altFlash" style="width:50%; margin-left: auto; margin-right: auto; ">
<h3>You need Adobe Flash installed and enabled in order to use this client.</h3>
You need Adobe Flash installed and enabled in order to use this client.
<br/>
<div style="width:50%; margin-left: auto; margin-right: auto; ">
<a href="http://www.adobe.com/go/getflashplayer">

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
<?xml version="1.0" ?>
<locales>
<locale code="ar_SY" name="Arabic (Syria)"/>
<locale code="ar" name="Arabic"/>
<locale code="az_AZ" name="Azerbaijani"/>
<locale code="eu_EU" name="Basque" />
<locale code="bn_BN" name="Bengali" />
@ -50,5 +50,5 @@
<locale code="tr_TR" name="Turkish"/>
<locale code="uk_UA" name="Ukrainian"/>
<locale code="vi_VN" name="Vietnamese"/>
<locale code="cy_GB" name="Welsh"/>
<locale code="cy_GB" name="Welsh"/>
</locales>

View File

@ -23,11 +23,16 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
xmlns:views="*"
pageTitle="BigBlueButton"
layout="absolute"
preinitialize="LogUtil.initLogging(true)"
preinitialize="init()"
preloader="org.bigbluebutton.main.model.BigBlueButtonPreloader">
<mx:Script>
<![CDATA[
import org.bigbluebutton.common.LogUtil;
private function init():void {
LogUtil.initLogging(true);
setStyle('backgroundColor', '0xFFFFFF');
}
]]>
</mx:Script>

View File

@ -69,11 +69,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
return _attributes.userrole as String;
}
public function get presenterShareOnly():Boolean{
if (_attributes.presenterShareOnly == "true") return true;
else return false;
}
public function start(attributes:Object):void {
LOGGER.debug("Starting Video Module");
_attributes = attributes;

View File

@ -26,10 +26,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
initialize="init()"
styleName="micSettingsWindowStyle"
showCloseButton="false">
<mate:Listener type="{MadePresenterEvent.SWITCH_TO_PRESENTER_MODE}" method="handleBecomePresenter" />
<mate:Listener type="{MadePresenterEvent.SWITCH_TO_VIEWER_MODE}" method="handleBecomeViewer" />
<mx:Script>
<![CDATA[
import com.asfusion.mate.events.Dispatcher;
@ -37,10 +34,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
import org.as3commons.logging.api.ILogger;
import org.as3commons.logging.api.getClassLogger;
import org.bigbluebutton.core.PopUpUtil;
import org.bigbluebutton.core.UsersUtil;
import org.bigbluebutton.core.managers.UserManager;
import org.bigbluebutton.main.api.JSAPI;
import org.bigbluebutton.main.events.MadePresenterEvent;
import org.bigbluebutton.main.model.users.Conference;
import org.bigbluebutton.modules.phone.PhoneOptions;
import org.bigbluebutton.modules.phone.events.AudioSelectionWindowEvent;
@ -57,14 +52,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
browserInfo = JSAPI.getInstance().getBrowserInfo();
var conference:Conference = UserManager.getInstance().getConference();
if (!phoneOptions.listenOnlyMode) btnListenOnly.enabled = false;
if (
(phoneOptions.presenterShareOnly && !UsersUtil.amIPresenter() && !UsersUtil.amIModerator())
) {
btnMicrophone.enabled = false;
}
if (!phoneOptions.listenOnlyMode)
btnListenOnly.enabled = false;
if (phoneOptions.showPhoneOption) {
txtPhone.text=ResourceUtil.getInstance().getString('bbb.audioSelection.txtPhone.text', [conference.dialNumber, conference.voiceBridge]);
} else {
@ -123,14 +113,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
PopUpUtil.removePopUp(this);
}
private function handleBecomePresenter(e:MadePresenterEvent):void {
if (phoneOptions.presenterShareOnly) btnMicrophone.enabled = true;
}
private function handleBecomeViewer(e:MadePresenterEvent):void {
if (phoneOptions.presenterShareOnly && !UsersUtil.amIModerator()) btnMicrophone.enabled = false;
}
]]>
</mx:Script>

View File

@ -1,13 +1,13 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
*
* Copyright (c) 2012 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.
@ -19,7 +19,7 @@
package org.bigbluebutton.modules.phone
{
import org.bigbluebutton.core.BBB;
public class PhoneOptions {
static public var firstAudioJoin:Boolean = true;
@ -30,13 +30,13 @@ package org.bigbluebutton.modules.phone
[Bindable]
public var autoJoin:Boolean = false;
[Bindable]
public var skipCheck:Boolean = false;
[Bindable]
public var enabledEchoCancel:Boolean = false;
[Bindable]
public var useWebRTCIfAvailable:Boolean = true;
@ -46,9 +46,6 @@ package org.bigbluebutton.modules.phone
[Bindable]
public var listenOnlyMode:Boolean = true;
[Bindable]
public var presenterShareOnly:Boolean = false;
[Bindable]
public var showPhoneOption:Boolean = false;
@ -59,17 +56,17 @@ package org.bigbluebutton.modules.phone
public var forceListenOnly:Boolean = false;
public var showWebRTCStats:Boolean = false;
public function PhoneOptions() {
parseOptions();
}
public function parseOptions():void {
var vxml:XML = BBB.getConfigForModule("PhoneModule");
if (vxml != null) {
if (vxml.@uri != undefined) {
uri = vxml.@uri.toString();
}
if (vxml.@uri != undefined) {
uri = vxml.@uri.toString();
}
if (vxml.@showButton != undefined) {
showButton = (vxml.@showButton.toString().toUpperCase() == "TRUE") ? true : false;
}
@ -91,12 +88,6 @@ package org.bigbluebutton.modules.phone
if (vxml.@listenOnlyMode != undefined) {
listenOnlyMode = (vxml.@listenOnlyMode.toString().toUpperCase() == "TRUE");
}
if (vxml.@forceListenOnly != undefined) {
forceListenOnly = (vxml.@forceListenOnly.toString().toUpperCase() == "TRUE");
}
if (vxml.@presenterShareOnly != undefined) {
presenterShareOnly = (vxml.@presenterShareOnly.toString().toUpperCase() == "TRUE");
}
if (vxml.@showPhoneOption != undefined) {
showPhoneOption = (vxml.@showPhoneOption.toString().toUpperCase() == "TRUE");
}
@ -109,6 +100,6 @@ package org.bigbluebutton.modules.phone
showWebRTCStats = (vxml.@showWebRTCStats.toString().toUpperCase() == "TRUE");
}
}
}
}
}
}

View File

@ -13,7 +13,6 @@
import org.bigbluebutton.common.Media;
import org.bigbluebutton.core.UsersUtil;
import org.bigbluebutton.core.events.VoiceConfEvent;
import org.bigbluebutton.main.api.JSLog;
import org.bigbluebutton.modules.phone.PhoneOptions;
import org.bigbluebutton.modules.phone.events.FlashCallConnectedEvent;
import org.bigbluebutton.modules.phone.events.FlashCallDisconnectedEvent;
@ -388,20 +387,7 @@
}
hangup();
}
public function handleBecomeViewer():void {
LOGGER.debug("Handling BecomeViewer, current state: {0}, using flash: {1}", [state, usingFlash]);
if (options.presenterShareOnly) {
if (!usingFlash || state != IN_CONFERENCE || UsersUtil.amIModerator()) return;
LOGGER.debug("handleBecomeViewer leaving flash with mic and joining listen only stream");
hangup();
var command:JoinVoiceConferenceCommand = new JoinVoiceConferenceCommand();
command.mic = false;
dispatcher.dispatchEvent(command);
}
}
public function handleFlashVoiceConnected():void {
switch (state) {
case JOIN_VOICE_CONFERENCE:

View File

@ -172,20 +172,6 @@ package org.bigbluebutton.modules.phone.managers
ExternalInterface.call("leaveWebRTCVoiceConference");
}
public function handleBecomeViewer():void {
LOGGER.debug("handleBecomeViewer received");
if (options.presenterShareOnly) {
if (!usingWebRTC || model.state != Constants.IN_CONFERENCE || UsersUtil.amIModerator()) return;
LOGGER.debug("handleBecomeViewer leaving WebRTC and joining listen only stream");
ExternalInterface.call("leaveWebRTCVoiceConference");
var command:JoinVoiceConferenceCommand = new JoinVoiceConferenceCommand();
command.mic = false;
dispatcher.dispatchEvent(command);
}
}
public function handleUseFlashModeCommand():void {
usingWebRTC = false;
}

View File

@ -27,7 +27,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<![CDATA[
import mx.events.FlexEvent;
import org.bigbluebutton.main.events.MadePresenterEvent;
import org.bigbluebutton.main.events.BBBEvent;
import org.bigbluebutton.main.model.users.events.ConnectionFailedEvent;
import org.bigbluebutton.modules.phone.events.FlashCallConnectedEvent;
@ -118,10 +117,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<MethodInvoker generator="{FlashCallManager}" method="handleUseFlashModeCommand"/>
</EventHandlers>
<EventHandlers type="{MadePresenterEvent.SWITCH_TO_VIEWER_MODE}">
<MethodInvoker generator="{FlashCallManager}" method="handleBecomeViewer"/>
</EventHandlers>
<EventHandlers type="{BBBEvent.RECONNECT_SIP_SUCCEEDED_EVENT}">
<MethodInvoker generator="{FlashCallManager}" method="handleReconnectSIPSucceededEvent"/>
</EventHandlers>

View File

@ -26,8 +26,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<mx:Script>
<![CDATA[
import mx.events.FlexEvent;
import org.bigbluebutton.main.events.MadePresenterEvent;
import org.bigbluebutton.main.model.users.events.ConnectionFailedEvent;
import org.bigbluebutton.modules.phone.events.JoinVoiceConferenceCommand;
import org.bigbluebutton.modules.phone.events.LeaveVoiceConferenceCommand;
@ -116,7 +115,4 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<MethodInvoker generator="{WebRTCCallManager}" method="handleLeaveVoiceConferenceCommand"/>
</EventHandlers>
<EventHandlers type="{MadePresenterEvent.SWITCH_TO_VIEWER_MODE}">
<MethodInvoker generator="{WebRTCCallManager}" method="handleBecomeViewer"/>
</EventHandlers>
</EventMap>

View File

@ -89,32 +89,30 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
var conference:Conference = UserManager.getInstance().getConference();
var thisUser:BBBUser = conference.getMyUser();
return ((phoneOptions.presenterShareOnly && !UsersUtil.amIPresenter() && !UsersUtil.amIModerator())
|| thisUser.disableMyMic);
return thisUser.disableMyMic;
}
private function get defaultListenOnlyMode():Boolean {
return (phoneOptions.listenOnlyMode && phoneOptions.forceListenOnly);
}
public function remoteClick(event:ShortcutEvent):void{
public function remoteClick(event:ShortcutEvent):void {
this.selected = true;
startPhone();
}
private function mouseOverHandler(event:MouseEvent):void {
if(_currentState == ACTIVE_STATE)
this.styleName = "voiceConfInactiveButtonStyle";
if (_currentState == ACTIVE_STATE)
this.styleName = "voiceConfInactiveButtonStyle";
else
this.styleName = "voiceConfActiveButtonStyle";
this.styleName = "voiceConfActiveButtonStyle";
}
private function mouseOutHandler(event:MouseEvent):void {
if(_currentState == ACTIVE_STATE)
this.styleName = "voiceConfActiveButtonStyle";
if (_currentState == ACTIVE_STATE)
this.styleName = "voiceConfActiveButtonStyle";
else
this.styleName = "voiceConfDefaultButtonStyle";
this.styleName = "voiceConfDefaultButtonStyle";
}
private function joinAudio():void {
@ -135,16 +133,30 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
LOGGER.debug("Sending Leave Conference command");
dispatcher.dispatchEvent(new LeaveVoiceConferenceCommand());
}
private function onCreationComplete():void {
var conference:Conference = UserManager.getInstance().getConference();
var thisUser:BBBUser = conference.getMyUser();
// when the button is added to the stage display the audio selection window if auto join is true
if (phoneOptions.autoJoin) {
joinAudio();
} else {
joinDefaultListenOnlyMode();
if (phoneOptions.skipCheck || thisUser.disableMyMic) {
var command:JoinVoiceConferenceCommand = new JoinVoiceConferenceCommand();
if (thisUser.disableMyMic) {
command.mic = false;
} else {
command.mic = true;
}
dispatcher.dispatchEvent(command);
} else {
LOGGER.debug("Sending Show Audio Selection command");
dispatcher.dispatchEvent(new AudioSelectionWindowEvent(AudioSelectionWindowEvent.SHOW_AUDIO_SELECTION));
}
}
}
private function onUserJoinedConference():void {
PhoneOptions.firstAudioJoin = false;

View File

@ -145,15 +145,6 @@ package org.bigbluebutton.modules.videoconf.maps
private function displayToolbarButton():void {
button.isPresenter = true;
if (options.presenterShareOnly) {
if (UsersUtil.amIPresenter()) {
button.isPresenter = true;
} else {
button.isPresenter = false;
}
}
}
private function addToolbarButton():void{
@ -457,9 +448,6 @@ package org.bigbluebutton.modules.videoconf.maps
if (options.showButton){
LOGGER.debug("****************** Switching to viewer. Show video button?=[{0}]", [UsersUtil.amIPresenter()]);
displayToolbarButton();
if (_myCamSettings.length > 0 && options.presenterShareOnly) {
stopBroadcasting();
}
}
}

View File

@ -1,13 +1,13 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
*
* Copyright (c) 2012 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.
@ -18,85 +18,81 @@
*/
package org.bigbluebutton.modules.videoconf.model
{
import flash.external.ExternalInterface;
import org.bigbluebutton.core.BBB;
import org.bigbluebutton.main.api.JSAPI;
public class VideoConfOptions
{
public class VideoConfOptions {
public var uri:String = "rtmp://localhost/video";
[Bindable]
public var autoStart:Boolean = false;
[Bindable]
public var showCloseButton:Boolean = true;
[Bindable]
public var showButton:Boolean = true;
[Bindable]
public var publishWindowVisible:Boolean = true;
[Bindable]
public var viewerWindowMaxed:Boolean = false;
[Bindable]
public var viewerWindowLocation:String = "middle";
[Bindable]
public var smoothVideo:Boolean = false;
[Bindable]
public var applyConvolutionFilter:Boolean = false;
[Bindable]
public var convolutionFilter:Array = [-1, 0, -1, 0, 6, 0, -1, 0, -1];
[Bindable]
public var filterBias:Number = 0;
[Bindable]
public var filterDivisor:Number = 4;
[Bindable] public var baseTabIndex:int;
[Bindable]
public var presenterShareOnly:Boolean = false;
public var baseTabIndex:int;
[Bindable]
public var controlsForPresenter:Boolean = false;
public var controlsForPresenter:Boolean = false;
[Bindable]
public var displayAvatar:Boolean = false;
[Bindable]
public var focusTalking:Boolean = false;
[Bindable]
public var skipCamSettingsCheck:Boolean = false;
[Bindable]
public var skipCamSettingsCheck:Boolean = false;
[Bindable]
public var glowColor:String = "0x4A931D";
[Bindable]
public var glowBlurSize:Number = 30.0;
[Bindable]
public var priorityRatio:Number = 2/3;
[Bindable]
public var priorityRatio:Number = 2 / 3;
public function VideoConfOptions() {
parseOptions();
}
public function parseOptions():void {
var browserInfo : Array = JSAPI.getInstance().getBrowserInfo();
var browserInfo:Array = JSAPI.getInstance().getBrowserInfo();
var vxml:XML = BBB.getConfigForModule("VideoconfModule");
if (vxml != null) {
if (vxml.@uri != undefined) {
uri = vxml.@uri.toString();
}
}
if (vxml.@showCloseButton != undefined) {
showCloseButton = (vxml.@showCloseButton.toString().toUpperCase() == "TRUE") ? true : false;
}
@ -112,25 +108,22 @@ package org.bigbluebutton.modules.videoconf.model
}
if (vxml.@publishWindowVisible != undefined) {
publishWindowVisible = (vxml.@publishWindowVisible.toString().toUpperCase() == "TRUE") ? true : false;
}
if (vxml.@presenterShareOnly != undefined) {
presenterShareOnly = (vxml.@presenterShareOnly.toString().toUpperCase() == "TRUE") ? true : false;
}
if (vxml.@controlsForPresenter != undefined) {
controlsForPresenter = (vxml.@controlsForPresenter.toString().toUpperCase() == "TRUE") ? true : false;
}
}
if (vxml.@viewerWindowMaxed != undefined) {
viewerWindowMaxed = (vxml.@viewerWindowMaxed.toString().toUpperCase() == "TRUE") ? true : false;
}
if (vxml.@skipCamSettingsCheck != undefined) {
skipCamSettingsCheck = (vxml.@skipCamSettingsCheck.toString().toUpperCase() == "TRUE") ? true : false;
}
}
if (vxml.@skipCamSettingsCheck != undefined) {
skipCamSettingsCheck = (vxml.@skipCamSettingsCheck.toString().toUpperCase() == "TRUE") ? true : false;
}
if (vxml.@viewerWindowLocation != undefined) {
viewerWindowLocation = vxml.@viewerWindowLocation.toString().toUpperCase();
}
}
if (vxml.@viewerWindowLocation != undefined) {
viewerWindowLocation = vxml.@viewerWindowLocation.toString().toUpperCase();
}
}
if (vxml.@smoothVideo != undefined) {
smoothVideo = (vxml.@smoothVideo.toString().toUpperCase() == "TRUE") ? true : false;
}
@ -140,41 +133,40 @@ package org.bigbluebutton.modules.videoconf.model
if (vxml.@convolutionFilter != undefined) {
var f:Array = vxml.@convolutionFilter.split(",");
var fint:Array = new Array();
for (var i:int=0; i < f.length; i++) {
for (var i:int = 0; i < f.length; i++) {
convolutionFilter[i] = Number(f[i]);
}
}
if (vxml.@filterBias != undefined) {
filterBias = Number(vxml.@filterBias.toString());
}
}
if (vxml.@filterDivisor != undefined) {
filterDivisor = Number(vxml.@filterDivisor.toString());
}
}
if (vxml.@baseTabIndex != undefined) {
baseTabIndex = vxml.@baseTabIndex;
}
else{
} else {
baseTabIndex = 101;
}
if (vxml.@displayAvatar != undefined) {
displayAvatar = (vxml.@displayAvatar.toString().toUpperCase() == "TRUE") ? true : false;
}
if (vxml.@focusTalking != undefined) {
focusTalking = (vxml.@focusTalking.toString().toUpperCase() == "TRUE") ? true : false;
}
if (vxml.@glowColor != undefined) {
glowColor = vxml.@glowColor.toString();
}
if (vxml.@glowBlurSize != undefined) {
glowBlurSize = Number(vxml.@glowBlurSize.toString());
}
}
if (vxml.@priorityRatio != undefined) {
priorityRatio = Number(vxml.@priorityRatio.toString());
priorityRatio = Number(vxml.@priorityRatio.toString());
}
}
}

View File

@ -1 +1 @@
BIGBLUEBUTTON_RELEASE=1.1.0-RC
BIGBLUEBUTTON_RELEASE=1.1.0

View File

@ -262,7 +262,7 @@
<div class="row">
<div class="span twelve center">
<p>Copyright &copy; 2017 BigBlueButton Inc.<br>
<small>Version <a href="http://docs.bigbluebutton.org/">1.1.0-RC</a></small>
<small>Version <a href="http://docs.bigbluebutton.org/">1.1.0</a></small>
</p>
</div>
</div>

View File

@ -43,7 +43,7 @@ export default function addShape(meetingId, whiteboardId, shape) {
'shape.shape.id': shape.shape.id,
'shape.shape.y': shape.shape.y,
'shape.shape.calcedFontSize': shape.shape.calcedFontSize,
'shape.shape.text': shape.shape.text,
'shape.shape.text': shape.shape.text.replace(/[\r]/g, '\n'),
});
break;

View File

@ -7,11 +7,11 @@ import muteToggle from '../methods/muteToggle';
export default function handleLockedStatusChange({ payload }) {
const meetingId = payload.meeting_id;
const userId = payload.userid;
const isLocked = arg.payload.locked;
const isLocked = payload.locked;
check(meetingId, String);
check(userId, String);
check(isLocked, String);
check(isLocked, Boolean);
const selector = {
meetingId,
@ -50,7 +50,7 @@ export default function handleLockedStatusChange({ payload }) {
muteToggle(credentials, userId, true);
}
return Logger.info(`Assigned locked status '${isLocked ? 'locked' : 'unlocked'}' id=${newPresenterId} meeting=${meetingId}`);
return Logger.info(`Assigned locked status '${isLocked ? 'locked' : 'unlocked'}' id=${userId} meeting=${meetingId}`);
}
};

View File

@ -29,7 +29,7 @@ export default function userLeaving(credentials, userId) {
const User = Users.findOne(selector);
if (!User) {
throw new Meteor.Error(
'user-not-found', `You need a valid user to be able to toggle audio`);
'user-not-found', `Could not find ${userId} in ${meetingId}: cannot complete userLeaving`);
}
if (User.user.connection_status === OFFLINE_CONNECTION_STATUS) {

View File

@ -1,5 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { isAllowedTo } from '/imports/startup/server/userPermissions';
import Logger from '/imports/startup/server/logger';
import userLeaving from './userLeaving';
@ -10,5 +11,9 @@ export default function userLogout(credentials) {
const { requesterUserId } = credentials;
return userLeaving(credentials, requesterUserId);
try {
userLeaving(credentials, requesterUserId);
} catch(e) {
Logger.error(`Exception while executing userLeaving: ${e}`);
}
};

View File

@ -40,7 +40,11 @@ Meteor.publish('users', function (credentials) {
}
this.onStop(() => {
userLeaving(credentials, requesterUserId);
try {
userLeaving(credentials, requesterUserId);
} catch(e) {
Logger.error(`Exception while executing userLeaving: ${e}`);
}
});
const selector = {

View File

@ -31,6 +31,12 @@ export function logoutRouteHandler(nextState, replace, callback) {
};
export function authenticatedRouteHandler(nextState, replace, callback) {
const credentialsSnapshot = {
meetingId: Auth.meetingID,
requesterUserId: Auth.userID,
requesterToken: Auth.token,
};
if (Auth.loggedIn) {
callback();
}
@ -40,7 +46,16 @@ export function authenticatedRouteHandler(nextState, replace, callback) {
Auth.authenticate()
.then(callback)
.catch(reason => {
logClient('error', { error: reason, method: 'authenticatedRouteHandler' });
logClient('error', { error: reason, method: 'authenticatedRouteHandler', credentialsSnapshot });
// make sure users who did not connect are not added to the meeting
// do **not** use the custom call - it relies on expired data
Meteor.call('userLogout', credentialsSnapshot, (error, result) => {
if (error) {
console.error('error');
}
});
replace({ pathname: `/error/${reason.error}` });
callback();
});

View File

@ -15,8 +15,9 @@ class IntlStartup extends Component {
this.state = {
messages: {},
appLocale : this.props.locale,
};
this.fetchLocalizedMessages = this.fetchLocalizedMessages.bind(this);
}
@ -27,7 +28,14 @@ class IntlStartup extends Component {
baseControls.updateLoadingState(true);
fetch(url)
.then(response => response.json())
.then(response => {
if (response.ok) {
return response.json();
} else {
this.setState({appLocale: 'en'});
return response.json();
}
})
.then(messages => {
this.setState({ messages }, () => {
baseControls.updateLoadingState(false);
@ -40,18 +48,19 @@ class IntlStartup extends Component {
}
componentWillMount() {
this.fetchLocalizedMessages(this.props.locale);
this.fetchLocalizedMessages(this.state.appLocale);
}
componentWillUpdate(nextProps, nextState) {
if (this.props.locale !== nextProps.locale) {
this.setState({appLocale: nextProps.locale});
this.fetchLocalizedMessages(nextProps.locale);
}
}
render() {
return (
<IntlProvider locale={this.props.locale} messages={this.state.messages}>
<IntlProvider locale={this.state.appLocale} messages={this.state.messages}>
{this.props.children}
</IntlProvider>
);

View File

@ -24,9 +24,8 @@ WebApp.connectHandlers.use('/locale', (req, res) => {
let defaultLocale = APP_CONFIG.defaultLocale;
let localeRegion = req.query.locale.split('-');
let messages = {};
let locales = [defaultLocale, localeRegion[0]];
let statusCode = 200;
if (localeRegion.length > 1) {
locales.push(`${localeRegion[0]}_${localeRegion[1].toUpperCase()}`);
}
@ -36,14 +35,15 @@ WebApp.connectHandlers.use('/locale', (req, res) => {
const data = Assets.getText(`locales/${locale}.json`);
messages = Object.assign(messages, JSON.parse(data));
} catch (e) {
// console.error(e);
// We dont really care about those errors since they will be a parse error
// or a file not found which is ok
//Variant Also Negotiates Status-Code, to alert the client that we
//do not support the following lang.
//https://en.wikipedia.org/wiki/Content_negotiation
statusCode = 506;
}
});
res.setHeader('Content-Type', 'application/json');
res.writeHead(200);
res.writeHead(statusCode);
res.end(JSON.stringify(messages));
});

View File

@ -129,12 +129,23 @@ export function isAllowedTo(action, credentials) {
});
const allowedToInitiateRequest =
null != user &&
authToken === user.authToken &&
user &&
user.authToken === authToken &&
user.validated &&
user.user.connection_status === 'online' &&
'HTML5' === user.clientType &&
null != user.user;
user.clientType === 'HTML5' &&
user.user &&
user.user.connection_status === 'online';
const listOfSafeActions = [
'logoutSelf',
];
const requestIsSafe = listOfSafeActions.includes(action);
if (requestIsSafe) {
logger.info(`permissions: requestIsSafe for ${action} by userId=${userId} allowed`);
return true;
}
if (allowedToInitiateRequest) {
let result = false;

View File

@ -18,6 +18,7 @@ const propTypes = {
class EmojiMenu extends Component {
constructor(props) {
super(props);
}
render() {
@ -28,8 +29,8 @@ class EmojiMenu extends Component {
} = this.props;
return (
<Dropdown ref="dropdown">
<DropdownTrigger>
<Dropdown autoFocus={true}>
<DropdownTrigger placeInTabOrder={true}>
<Button
role="button"
label={intl.formatMessage(intlMessages.statusTriggerLabel)}
@ -46,7 +47,7 @@ class EmojiMenu extends Component {
// even after the DropdownTrigger inject an onClick handler
onClick={() => null}>
<div id="currentStatus" hidden>
{intl.formatMessage(intlMessages.currentStatusDesc, { status: userEmojiStatus}) }
{intl.formatMessage(intlMessages.currentStatusDesc, { 0: userEmojiStatus}) }
</div>
</Button>
</DropdownTrigger>
@ -57,54 +58,63 @@ class EmojiMenu extends Component {
label={intl.formatMessage(intlMessages.raiseLabel)}
description={intl.formatMessage(intlMessages.raiseDesc)}
onClick={() => actions.setEmojiHandler('raiseHand')}
tabIndex={-1}
/>
<DropdownListItem
icon="happy"
label={intl.formatMessage(intlMessages.happyLabel)}
description={intl.formatMessage(intlMessages.happyDesc)}
onClick={() => actions.setEmojiHandler('happy')}
tabIndex={-1}
/>
<DropdownListItem
icon="undecided"
label={intl.formatMessage(intlMessages.undecidedLabel)}
description={intl.formatMessage(intlMessages.undecidedDesc)}
onClick={() => actions.setEmojiHandler('neutral')}
tabIndex={-1}
/>
<DropdownListItem
icon="sad"
label={intl.formatMessage(intlMessages.sadLabel)}
description={intl.formatMessage(intlMessages.sadDesc)}
onClick={() => actions.setEmojiHandler('sad')}
tabIndex={-1}
/>
<DropdownListItem
icon="confused"
label={intl.formatMessage(intlMessages.confusedLabel)}
description={intl.formatMessage(intlMessages.confusedDesc)}
onClick={() => actions.setEmojiHandler('confused')}
tabIndex={-1}
/>
<DropdownListItem
icon="time"
label={intl.formatMessage(intlMessages.awayLabel)}
description={intl.formatMessage(intlMessages.awayDesc)}
onClick={() => actions.setEmojiHandler('away')}
tabIndex={-1}
/>
<DropdownListItem
icon="thumbs_up"
label={intl.formatMessage(intlMessages.thumbsupLabel)}
description={intl.formatMessage(intlMessages.thumbsupDesc)}
onClick={() => actions.setEmojiHandler('thumbsUp')}
tabIndex={-1}
/>
<DropdownListItem
icon="thumbs_down"
label={intl.formatMessage(intlMessages.thumbsdownLabel)}
description={intl.formatMessage(intlMessages.thumbsdownDesc)}
onClick={() => actions.setEmojiHandler('thumbsDown')}
tabIndex={-1}
/>
<DropdownListItem
icon="applause"
label={intl.formatMessage(intlMessages.applauseLabel)}
description={intl.formatMessage(intlMessages.applauseDesc)}
onClick={() => actions.setEmojiHandler('applause')}
tabIndex={-1}
/>
<DropdownListSeparator />
<DropdownListItem
@ -112,6 +122,7 @@ class EmojiMenu extends Component {
label={intl.formatMessage(intlMessages.clearLabel)}
description={intl.formatMessage(intlMessages.clearDesc)}
onClick={() => actions.setEmojiHandler('none')}
tabIndex={-1}
/>
</DropdownList>
</DropdownContent>

View File

@ -33,7 +33,6 @@ const intlMessages = defineMessages({
});
const propTypes = {
init: PropTypes.func.isRequired,
fontSize: PropTypes.string,
navbar: PropTypes.element,
sidebar: PropTypes.element,
@ -52,8 +51,6 @@ class App extends Component {
this.state = {
compactUserList: false, //TODO: Change this on userlist resize (?)
};
props.init.call(this);
}
componentDidMount() {

View File

@ -31,7 +31,6 @@ const intlMessages = defineMessages({
kickedMessage: {
id: 'app.error.kicked',
description: 'Message when the user is kicked out of the meeting',
defaultMessage: 'You have been kicked out of the meeting',
},
});
@ -72,16 +71,7 @@ export default withRouter(injectIntl(withModalMounter(createContainer((
},
});
const APP_CONFIG = Meteor.settings.public.app;
const init = () => {
if (APP_CONFIG.autoJoinAudio) {
mountModal(<AudioModalContainer />);
}
};
return {
init,
sidebar: getCaptionsStatus() ? <ClosedCaptionsContainer /> : null,
fontSize: getFontSize(),
};

View File

@ -155,7 +155,7 @@ $bars-padding: $lg-padding-x - .45rem; // -.45 so user-list and chat title is al
@include mq($small-only) {
padding-bottom: $actionsbar-height;
margin-bottom: $actionsbar-height;
position: absolute;
position: relative;
}
}

View File

@ -16,16 +16,17 @@ class AudioContainer extends Component {
}
}
let didMountedAutoJoin = false;
export default withModalMounter(createContainer(({ mountModal }) => {
const APP_CONFIG = Meteor.settings.public.app;
return {
init: () => {
Service.init();
if (APP_CONFIG.autoJoinAudio) {
mountModal(<AudioModal handleJoinListenOnly={Service.joinListenOnly} />);
}
if (didMountedAutoJoin) return;
mountModal(<AudioModal handleJoinListenOnly={Service.joinListenOnly} />);
didMountedAutoJoin = true;
},
};
}, AudioContainer));

View File

@ -47,7 +47,7 @@ class Chat extends Component {
<Link
to="/users"
role="button"
aria-label={intl.formatMessage(intlMessages.hideChatLabel, { title: title })}>
aria-label={intl.formatMessage(intlMessages.hideChatLabel, { 0: title })}>
<Icon iconName="left_arrow"/> {title}
</Link>
</div>
@ -58,7 +58,7 @@ class Chat extends Component {
<Link
to="/users"
role="button"
aria-label={intl.formatMessage(intlMessages.closeChatLabel, { title: title })}>
aria-label={intl.formatMessage(intlMessages.closeChatLabel, { 0: title })}>
<Icon iconName="close" onClick={() => actions.handleClosePrivateChat(chatID)}/>
</Link>)
}

View File

@ -11,17 +11,14 @@ import ChatService from './service';
const intlMessages = defineMessages({
titlePublic: {
id: 'app.chat.titlePublic',
defaultMessage: 'Public Chat',
description: 'Public chat title',
},
titlePrivate: {
id: 'app.chat.titlePrivate',
defaultMessage: 'Private Chat with {name}',
description: 'Private chat title',
},
partnerDisconnected: {
id: 'app.chat.partnerDisconnected',
defaultMessage: '{name} has left the meeting',
description: 'System chat message when the private chat partnet disconnect from the meeting',
},
});
@ -65,7 +62,7 @@ export default injectIntl(createContainer(({ params, intl }) => {
let userMessage = messages.find(m => m.sender !== null);
let user = ChatService.getUser(chatID, '{{NAME}}');
title = intl.formatMessage(intlMessages.titlePrivate, { name: user.name });
title = intl.formatMessage(intlMessages.titlePrivate, { 0: user.name });
chatName = user.name;
if (!user.isOnline) {
@ -75,7 +72,7 @@ export default injectIntl(createContainer(({ params, intl }) => {
id,
content: [{
id,
text: intl.formatMessage(intlMessages.partnerDisconnected, { name: user.name }),
text: intl.formatMessage(intlMessages.partnerDisconnected, { 0: user.name }),
time,
},],
time,

View File

@ -105,9 +105,9 @@ class MessageForm extends Component {
<TextareaAutosize
className={styles.input}
id="message-input"
placeholder={intl.formatMessage(messages.inputPlaceholder, { name: chatName })}
placeholder={intl.formatMessage(messages.inputPlaceholder, { 0: chatName })}
aria-controls={this.props.chatAreaId}
aria-label={intl.formatMessage(messages.inputLabel, { name: chatTitle })}
aria-label={intl.formatMessage(messages.inputLabel, { 0: chatTitle })}
autoCorrect="off"
autoComplete="off"
spellCheck="true"

View File

@ -11,26 +11,28 @@ export default class ClosedCaptions extends React.Component {
renderCaptions(caption) {
let text = caption.captions;
const captionStyles = {
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
fontFamily: this.props.fontFamily,
fontSize: this.props.fontSize,
color: this.props.fontColor,
};
return (
<span
style={{ whiteSpace: 'pre-wrap', wordWrap: 'break-word', fontFamily: this.props.fontFamily, fontSize: this.props.fontSize, color: this.props.fontColor }}
style={captionStyles}
dangerouslySetInnerHTML={{ __html: text }}
key={caption.index}
/>
);
}
componentDidUpdate() {
const { ccScrollArea } = this.refs;
var node = findDOMNode(ccScrollArea);
node.scrollTop = node.scrollHeight;
}
componentWillUpdate() {
const { ccScrollArea } = this.refs;
var node = findDOMNode(ccScrollArea);
const node = findDOMNode(ccScrollArea);
// number 4 is for the border
// offset height includes the border, but scrollheight doesn't
this.shouldScrollBottom = node.scrollTop + node.offsetHeight - 4 === node.scrollHeight;
@ -39,22 +41,28 @@ export default class ClosedCaptions extends React.Component {
componentDidUpdate() {
if (this.shouldScrollBottom) {
const { ccScrollArea } = this.refs;
var node = findDOMNode(ccScrollArea);
node.scrollTop = node.scrollHeight
const node = findDOMNode(ccScrollArea);
node.scrollTop = node.scrollHeight;
}
}
render() {
const {
locale,
captions,
backgroundColor,
} = this.props;
return (
<div disabled className={styles.ccbox}>
<div className={styles.title}>
<p> {this.props.locale ? this.props.locale : 'Locale is not selected'} </p>
<p> {locale ? locale : 'Locale is not selected'} </p>
</div>
<div
ref="ccScrollArea"
className={styles.frame}
style={{background: this.props.backgroundColor}}>
{this.props.captions[this.props.locale] ? this.props.captions[this.props.locale].captions.map((caption) => (
style={{ background: backgroundColor }}>
{captions[locale] ? captions[locale].captions.map((caption) => (
this.renderCaptions(caption)
)) : null }
</div>

View File

@ -18,6 +18,5 @@ class ClosedCaptionsContainer extends Component {
}
export default createContainer(() => {
const data = ClosedCaptionsService.getCCData();
return data;
return ClosedCaptionsService.getCCData();
}, ClosedCaptionsContainer);

View File

@ -83,15 +83,18 @@ class Dropdown extends Component {
handleShow() {
this.setState({ isOpen: true }, this.handleStateCallback);
const contentElement = findDOMNode(this.refs.content);
contentElement.querySelector(FOCUSABLE_CHILDREN).focus();
}
handleHide() {
const { autoFocus } = this.props;
this.setState({ isOpen: false }, this.handleStateCallback);
const triggerElement = findDOMNode(this.refs.trigger);
triggerElement.focus();
if (autoFocus) {
const triggerElement = findDOMNode(this.refs.trigger);
triggerElement.focus();
}
}
componentDidMount () {
@ -122,7 +125,14 @@ class Dropdown extends Component {
}
render() {
const { children, className, style, intl } = this.props;
const {
children,
className,
style, intl,
hasPopup,
ariaLive,
ariaRelevant,
} = this.props;
let trigger = children.find(x => x.type === DropdownTrigger);
let content = children.find(x => x.type === DropdownContent);
@ -143,7 +153,12 @@ class Dropdown extends Component {
});
return (
<div style={style} className={cx(styles.dropdown, className)}>
<div
style={style}
className={cx(styles.dropdown, className)}
aria-live={ariaLive}
aria-relevant={ariaRelevant}
aria-haspopup={hasPopup}>
{trigger}
{content}
{ this.state.isOpen ?

View File

@ -78,7 +78,7 @@ export default class DropdownList extends Component {
nextActiveItemIndex = this.childrenRefs.length - 1;
}
if ([KEY_CODES.TAB, KEY_CODES.ESCAPE].includes(event.which)) {
if ([KEY_CODES.ESCAPE].includes(event.which)) {
nextActiveItemIndex = 0;
dropdownHide();
}

View File

@ -29,14 +29,16 @@ export default class DropdownListItem extends Component {
render() {
const { label, description, children, injectRef, tabIndex, onClick, onKeyDown,
className, style, separator, intl, } = this.props;
className, style, separator, intl, placeInTabOrder, } = this.props;
let index = (placeInTabOrder) ? 0 : -1;
return (
<li
ref={injectRef}
onClick={onClick}
onKeyDown={onKeyDown}
tabIndex={tabIndex}
tabIndex={index}
aria-labelledby={this.labelID}
aria-describedby={this.descID}
className={cx(styles.item, className)}

View File

@ -1,22 +1,22 @@
import React, { Component, PropTypes } from 'react';
import styles from '../styles';
const propTypes = {
description: PropTypes.string,
};
export default class DropdownListTitle extends Component {
render() {
const { intl, description } = this.props;
return (
<div>
<li className={styles.title} aria-describedby="labelContext">{this.props.children}</li>
<div id="labelContext" aria-label={description}></div>
</div>
);
}
}
DropdownListTitle.propTypes = propTypes;
import React, { Component, PropTypes } from 'react';
import styles from '../styles';
const propTypes = {
description: PropTypes.string,
};
export default class DropdownListTitle extends Component {
render() {
const { intl, description } = this.props;
return (
<div>
<li className={styles.title} aria-describedby="labelContext">{this.props.children}</li>
<div id="labelContext" aria-label={description}></div>
</div>
);
}
}
DropdownListTitle.propTypes = propTypes;

View File

@ -41,14 +41,16 @@ export default class DropdownTrigger extends Component {
}
render() {
const { children, style, className, } = this.props;
const { children, style, className, placeInTabOrder, } = this.props;
const TriggerComponent = React.Children.only(children);
let index = (placeInTabOrder) ? '0' : '-1';
const TriggerComponentBounded = React.cloneElement(children, {
onClick: this.handleClick,
onKeyDown: this.handleKeyDown,
'aria-haspopup': true,
tabIndex: '0',
tabIndex: index,
style: style,
className: cx(children.props.className, className),
});

View File

@ -83,8 +83,8 @@ class SettingsDropdown extends Component {
}
return (
<Dropdown ref="dropdown">
<DropdownTrigger>
<Dropdown autoFocus={true}>
<DropdownTrigger placeInTabOrder={true}>
<Button
label={intl.formatMessage(intlMessages.optionsLabel)}
icon="more"

View File

@ -27,32 +27,26 @@ const STATUS_OFFLINE = 'offline';
const intlMessages = defineMessages({
failedMessage: {
id: 'app.failedMessage',
defaultMessage: 'Apologies, trouble connecting to the server.',
description: 'Message when the client is trying to connect to the server',
description: 'Notification for connecting to server problems',
},
connectingMessage: {
id: 'app.connectingMessage',
defaultMessage: 'Connecting...',
description: 'Message when the client is trying to connect to the server',
description: 'Notification message for when client is connecting to server',
},
waitingMessage: {
id: 'app.waitingMessage',
defaultMessage: 'Disconnected. Trying to reconnect in {seconds} seconds...',
description: 'Message when the client is trying to reconnect to the server',
description: 'Notification message for disconnection with reconnection counter',
},
breakoutTimeRemaining: {
id: 'app.breakoutTimeRemainingMessage',
defaultMessage: 'Breakout Room time remaining: {time}',
description: 'Message that tells how much time is remaining for the breakout room',
},
breakoutWillClose: {
id: 'app.breakoutWillCloseMessage',
defaultMessage: 'Time ended. Breakout Room will close soon',
description: 'Message that tells time has ended and breakout will close',
},
calculatingBreakoutTimeRemaining: {
id: 'app.calculatingBreakoutTimeRemaining',
defaultMessage: 'Calculating remaining time...',
description: 'Message that tells that the remaining time is being calculated',
},
});
@ -146,7 +140,7 @@ export default injectIntl(createContainer(({ intl }) => {
retryInterval = startCounter(sec, setRetrySeconds, getRetrySeconds, retryInterval);
data.message = intl.formatMessage(
intlMessages.waitingMessage,
{ seconds: getRetrySeconds() }
{ 0: getRetrySeconds() }
);
break;
}

View File

@ -31,8 +31,8 @@ export default class DefaultContent extends Component {
<FormattedMessage
id="app.home.greeting"
description="Message to greet the user."
defaultMessage="Welcome {name}! Your presentation will begin shortly..."
values={{ name: 'James Bond' }}
defaultMessage="Welcome {0}! Your presentation will begin shortly..."
values={{ 0: 'James Bond' }}
/>
<br/>
Today is {' '}<FormattedDate value={Date.now()} />

View File

@ -2,6 +2,7 @@ import React, { Component } from 'react';
import Modal from '/imports/ui/components/modal/fullscreen/component';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import { defineMessages, injectIntl } from 'react-intl';
import { withModalMounter } from '../modal/service';
import ClosedCaptions from '/imports/ui/components/settings/submenus/closed-captions/component';
import Application from '/imports/ui/components/settings/submenus/application/container';
import Participants from '/imports/ui/components/settings/submenus/participants/component';
@ -105,6 +106,7 @@ class Settings extends Component {
title={intl.formatMessage(intlMessages.SettingsLabel)}
confirm={{
callback: (() => {
this.props.mountModal(null);
this.updateSettings(this.state.current);
}),
label: intl.formatMessage(intlMessages.SaveLabel),
@ -199,4 +201,4 @@ class Settings extends Component {
}
Settings.propTypes = propTypes;
export default injectIntl(Settings);
export default withModalMounter(injectIntl(Settings));

View File

@ -181,7 +181,7 @@ class ApplicationMenu extends BaseMenu {
defaultValue={this.state.settings.locale}
className={styles.select}
onChange={this.handleSelectChange.bind(this, 'locale', availableLocales)}>
<option>
<option disabled={true}>
{ availableLocales &&
availableLocales.length ?
intl.formatMessage(intlMessages.languageOptionLabel) :

View File

@ -1,5 +1,4 @@
import React from 'react';
import Modal from 'react-modal';
import styles from '../styles';
import cx from 'classnames';
import BaseMenu from '../base/component';
@ -128,6 +127,7 @@ class ClosedCaptionsMenu extends BaseMenu {
const {
locales,
intl,
isModerator,
} = this.props;
return (
@ -155,210 +155,215 @@ class ClosedCaptionsMenu extends BaseMenu {
</div>
</div>
</div>
<div className={cx(styles.row, styles.spacedLeft)}>
<div className={styles.col}>
<div className={styles.formElement}>
<label className={styles.label}>
{intl.formatMessage(intlMessages.takeOwnershipLabel)}
</label>
</div>
</div>
<div className={styles.col}>
<div className={cx(styles.formElement, styles.pullContentRight)}>
<Checkbox
onChange={() => this.handleToggle('takeOwnership')}
checked={this.state.settings.takeOwnership}
ariaLabelledBy={'takeOwnership'}
ariaLabel={intl.formatMessage(intlMessages.takeOwnershipLabel)}/>
</div>
</div>
</div>
<div className={cx(styles.row, styles.spacedLeft)}>
<div className={styles.col}>
<div className={styles.formElement}>
<label className={styles.label}>
{intl.formatMessage(intlMessages.languageLabel)}
</label>
</div>
</div>
<div className={styles.col}>
<div
className={cx(styles.formElement, styles.pullContentRight)}
aria-label={intl.formatMessage(intlMessages.languageLabel)}>
<select
defaultValue={locales ? locales.indexOf(this.state.settings.locale) : -1}
className={styles.select}
onChange={this.handleSelectChange.bind(this, 'locale', this.props.locales)}>
<option>
{ this.props.locales &&
this.props.locales.length ?
intl.formatMessage(intlMessages.localeOptionLabel) :
intl.formatMessage(intlMessages.noLocaleOptionLabel) }
</option>
{this.props.locales ? this.props.locales.map((locale, index) =>
<option key={index} value={index}>
{locale}
</option>
) : null }
</select>
</div>
</div>
</div>
<div className={cx(styles.row, styles.spacedLeft)}>
<div className={styles.col}>
<div className={styles.formElement}>
<label className={styles.label}>
{intl.formatMessage(intlMessages.fontFamilyLabel)}
</label>
</div>
</div>
<div className={styles.col}>
<div
className={cx(styles.formElement, styles.pullContentRight)}
aria-label={intl.formatMessage(intlMessages.fontFamilyLabel)}>
<select
defaultValue={FONT_FAMILIES.indexOf(this.state.settings.fontFamily)}
onChange={this.handleSelectChange.bind(this, 'fontFamily', FONT_FAMILIES)}
className={styles.select}>
<option value='-1' disabled>
{intl.formatMessage(intlMessages.fontFamilyOptionLabel)}
</option>
{
FONT_FAMILIES.map((family, index) =>
<option key={index} value={index}>
{family}
{ this.state.settings.enabled ?
<div>
{ isModerator ?
<div className={cx(styles.row, styles.spacedLeft)}>
<div className={styles.col}>
<div className={styles.formElement}>
<label className={styles.label}>
{intl.formatMessage(intlMessages.takeOwnershipLabel)}
</label>
</div>
</div>
<div className={styles.col}>
<div className={cx(styles.formElement, styles.pullContentRight)}>
<Checkbox
onChange={() => this.handleToggle('takeOwnership')}
checked={this.state.settings.takeOwnership}
ariaLabelledBy={'takeOwnership'}
ariaLabel={intl.formatMessage(intlMessages.takeOwnershipLabel)}/>
</div>
</div>
</div>
: null }
<div className={cx(styles.row, styles.spacedLeft)}>
<div className={styles.col}>
<div className={styles.formElement}>
<label className={styles.label}>
{intl.formatMessage(intlMessages.languageLabel)}
</label>
</div>
</div>
<div className={styles.col}>
<div
className={cx(styles.formElement, styles.pullContentRight)}
aria-label={intl.formatMessage(intlMessages.languageLabel)}>
<select
defaultValue={locales ? locales.indexOf(this.state.settings.locale) : -1}
className={styles.select}
onChange={this.handleSelectChange.bind(this, 'locale', this.props.locales)}>
<option>
{ this.props.locales &&
this.props.locales.length ?
intl.formatMessage(intlMessages.localeOptionLabel) :
intl.formatMessage(intlMessages.noLocaleOptionLabel) }
</option>
)
}
</select>
</div>
</div>
</div>
<div className={cx(styles.row, styles.spacedLeft)}>
<div className={styles.col}>
<div className={styles.formElement}>
<label className={styles.label}>
{intl.formatMessage(intlMessages.fontSizeLabel)}
</label>
</div>
</div>
<div className={styles.col}>
<div
className={cx(styles.formElement, styles.pullContentRight)}
aria-label={intl.formatMessage(intlMessages.fontSizeLabel)}>
<select
defaultValue={FONT_SIZES.indexOf(this.state.settings.fontSize)}
onChange={this.handleSelectChange.bind(this, 'fontSize', FONT_SIZES)}
className={styles.select}>
<option value='-1' disabled>
{intl.formatMessage(intlMessages.fontSizeOptionLabel)}
</option>
{
FONT_SIZES.map((size, index) =>
{this.props.locales ? this.props.locales.map((locale, index) =>
<option key={index} value={index}>
{size}
{locale}
</option>
)
}
</select>
</div>
</div>
</div>
<div className={cx(styles.row, styles.spacedLeft)}>
<div className={styles.col}>
<div className={styles.formElement}>
<label className={styles.label}>
{intl.formatMessage(intlMessages.backgroundColorLabel)}
</label>
</div>
</div>
<div className={styles.col}>
<div
className={cx(styles.formElement, styles.pullContentRight)}
aria-label={intl.formatMessage(intlMessages.backgroundColorLabel)}>
<div
tabIndex='0'
className={ styles.swatch }
onClick={
this.handleColorPickerClick.bind(this, 'displayBackgroundColorPicker')
}>
<div
className={styles.swatchInner}
style={ { background: this.state.settings.backgroundColor } }>
) : null }
</select>
</div>
</div>
{ this.state.displayBackgroundColorPicker ?
<div className={styles.colorPickerPopover}>
<div
className={styles.colorPickerOverlay}
onClick={ this.handleCloseColorPicker.bind(this) }>
</div>
<GithubPicker
onChange={this.handleColorChange.bind(this, 'backgroundColor')}
color={this.state.settings.backgroundColor}
colors={COLORS}
width={'140px'}
triangle={'top-right'}
/>
</div>
: null }
</div>
</div>
</div>
<div className={cx(styles.row, styles.spacedLeft)}>
<div className={styles.col}>
<div className={styles.formElement}>
<label className={styles.label}>
{intl.formatMessage(intlMessages.fontColorLabel)}
</label>
</div>
</div>
<div className={styles.col}>
<div
className={cx(styles.formElement, styles.pullContentRight)}
aria-label={intl.formatMessage(intlMessages.fontColorLabel)}>
<div
tabIndex='0'
className={ styles.swatch }
onClick={ this.handleColorPickerClick.bind(this, 'displayFontColorPicker') }>
<div
className={styles.swatchInner}
style={ { background: this.state.settings.fontColor } }>
<div className={cx(styles.row, styles.spacedLeft)}>
<div className={styles.col}>
<div className={styles.formElement}>
<label className={styles.label}>
{intl.formatMessage(intlMessages.fontFamilyLabel)}
</label>
</div>
</div>
{ this.state.displayFontColorPicker ?
<div className={styles.colorPickerPopover}>
<div
className={styles.colorPickerOverlay}
onClick={ this.handleCloseColorPicker.bind(this) }
>
</div>
<GithubPicker
onChange={this.handleColorChange.bind(this, 'fontColor')}
color={this.state.settings.fontColor}
colors={COLORS}
width={'140px'}
triangle={'top-right'}
/>
<div className={styles.col}>
<div
className={cx(styles.formElement, styles.pullContentRight)}
aria-label={intl.formatMessage(intlMessages.fontFamilyLabel)}>
<select
defaultValue={FONT_FAMILIES.indexOf(this.state.settings.fontFamily)}
onChange={this.handleSelectChange.bind(this, 'fontFamily', FONT_FAMILIES)}
className={styles.select}>
<option value='-1' disabled>
{intl.formatMessage(intlMessages.fontFamilyOptionLabel)}
</option>
{
FONT_FAMILIES.map((family, index) =>
<option key={index} value={index}>
{family}
</option>
)
}
</select>
</div>
: null }
</div>
</div>
<div className={cx(styles.row, styles.spacedLeft)}>
<div className={styles.col}>
<div className={styles.formElement}>
<label className={styles.label}>
{intl.formatMessage(intlMessages.fontSizeLabel)}
</label>
</div>
</div>
<div className={styles.col}>
<div
className={cx(styles.formElement, styles.pullContentRight)}
aria-label={intl.formatMessage(intlMessages.fontSizeLabel)}>
<select
defaultValue={FONT_SIZES.indexOf(this.state.settings.fontSize)}
onChange={this.handleSelectChange.bind(this, 'fontSize', FONT_SIZES)}
className={styles.select}>
<option value='-1' disabled>
{intl.formatMessage(intlMessages.fontSizeOptionLabel)}
</option>
{
FONT_SIZES.map((size, index) =>
<option key={index} value={index}>
{size}
</option>
)
}
</select>
</div>
</div>
</div>
<div className={cx(styles.row, styles.spacedLeft)}>
<div className={styles.col}>
<div className={styles.formElement}>
<label className={styles.label}>
{intl.formatMessage(intlMessages.backgroundColorLabel)}
</label>
</div>
</div>
<div className={styles.col}>
<div
className={cx(styles.formElement, styles.pullContentRight)}
aria-label={intl.formatMessage(intlMessages.backgroundColorLabel)}>
<div
tabIndex='0'
className={ styles.swatch }
onClick={
this.handleColorPickerClick.bind(this, 'displayBackgroundColorPicker')
}>
<div
className={styles.swatchInner}
style={ { background: this.state.settings.backgroundColor } }>
</div>
</div>
{ this.state.displayBackgroundColorPicker ?
<div className={styles.colorPickerPopover}>
<div
className={styles.colorPickerOverlay}
onClick={ this.handleCloseColorPicker.bind(this) }>
</div>
<GithubPicker
onChange={this.handleColorChange.bind(this, 'backgroundColor')}
color={this.state.settings.backgroundColor}
colors={COLORS}
width={'140px'}
triangle={'top-right'}
/>
</div>
: null }
</div>
</div>
</div>
<div className={cx(styles.row, styles.spacedLeft)}>
<div className={styles.col}>
<div className={styles.formElement}>
<label className={styles.label}>
{intl.formatMessage(intlMessages.fontColorLabel)}
</label>
</div>
</div>
<div className={styles.col}>
<div
className={cx(styles.formElement, styles.pullContentRight)}
aria-label={intl.formatMessage(intlMessages.fontColorLabel)}>
<div
tabIndex='0'
className={ styles.swatch }
onClick={ this.handleColorPickerClick.bind(this, 'displayFontColorPicker') }>
<div
className={styles.swatchInner}
style={ { background: this.state.settings.fontColor } }>
</div>
</div>
{ this.state.displayFontColorPicker ?
<div className={styles.colorPickerPopover}>
<div
className={styles.colorPickerOverlay}
onClick={ this.handleCloseColorPicker.bind(this) }
>
</div>
<GithubPicker
onChange={this.handleColorChange.bind(this, 'fontColor')}
color={this.state.settings.fontColor}
colors={COLORS}
width={'140px'}
triangle={'top-right'}
/>
</div>
: null }
</div>
</div>
</div>
<div
className={cx(styles.ccPreviewBox, styles.spacedLeft)}
role='presentation'
style={ { background: this.state.settings.backgroundColor } }>
<span style={this.getPreviewStyle()}>
Etiam porta sem malesuada magna mollis euis-mod.
Donec ullamcorper nulla non metus auctor fringilla.
</span>
</div>
</div>
</div>
<div
className={cx(styles.ccPreviewBox, styles.spacedLeft)}
role='presentation'
style={ { background: this.state.settings.backgroundColor } }>
<span style={this.getPreviewStyle()}>
Etiam porta sem malesuada magna mollis euis-mod.
Donec ullamcorper nulla non metus auctor fringilla.
</span>
</div>
: null }
</div>
</div>
);

View File

@ -14,6 +14,5 @@ class ClosedCaptionsMenuContainer extends Component {
}
export default createContainer(() => {
let data = Service.getClosedCaptionSettings();
return data;
return Service.getClosedCaptionSettings();
}, ClosedCaptionsMenuContainer);

View File

@ -1,22 +1,10 @@
import Storage from '/imports/ui/services/storage/session';
import Captions from '/imports/api/captions';
getClosedCaptionSettings = () => {
let ccSettings = {};
let ccEnabled = Storage.getItem('closedCaptions');
ccSettings.ccEnabled = !!ccEnabled;
//list of unique locales in the Captions Collection
let locales = _.uniq(Captions.find({}, {
sort: { locale: 1 },
fields: { locale: true },
}).fetch().map(function (obj) {
return obj.locale;
}), true);
//adding the list of active locales to the closed-captions settings object
ccSettings.locales = locales;
return ccSettings;
};

View File

@ -33,7 +33,7 @@ export default class UserAvatar extends Component {
return (
<div className={user.isOnline ? styles.userAvatar : styles.userLogout}
style={avatarStyles}>
style={avatarStyles} aria-hidden="true">
<div>
{this.renderAvatarContent()}
</div>

View File

@ -43,6 +43,7 @@ class ChatListItem extends Component {
openChat,
compact,
intl,
tabIndex,
} = this.props;
const linkPath = [PRIVATE_CHAT_PATH, chat.id].join('');
@ -57,12 +58,13 @@ class ChatListItem extends Component {
}
return (
<li className={cx(styles.chatListItem, linkClasses)}>
<Link
to={linkPath}
className={styles.chatListItemLink}
className={cx(styles.chatListItem, linkClasses)}
role="button"
aria-expanded={isCurrentChat}>
aria-expanded={isCurrentChat}
tabIndex={tabIndex}>
<div className={styles.chatListItemLink}>
{chat.icon ? this.renderChatIcon() : this.renderChatAvatar()}
<div className={styles.chatName}>
{!compact ? <span className={styles.chatNameMain}>{chat.name}</span> : null }
@ -71,15 +73,15 @@ class ChatListItem extends Component {
<div
className={styles.unreadMessages}
aria-label={isSingleMessage
? intl.formatMessage(intlMessages.unreadSingular, { count: chat.unreadCounter })
: intl.formatMessage(intlMessages.unreadPlural, { count: chat.unreadCounter })}>
? intl.formatMessage(intlMessages.unreadSingular, { 0: chat.unreadCounter })
: intl.formatMessage(intlMessages.unreadPlural, { 0: chat.unreadCounter })}>
<div className={styles.unreadMessagesText} aria-hidden="true">
{chat.unreadCounter}
</div>
</div>
: null}
</div>
</Link>
</li>
);
}

View File

@ -4,6 +4,7 @@
@extend %list-item;
cursor: pointer;
padding: 0;
text-decoration: none;
}
.chatListItemLink {

View File

@ -6,6 +6,7 @@ import cx from 'classnames';
import { defineMessages, injectIntl } from 'react-intl';
import UserListItem from './user-list-item/component.jsx';
import ChatListItem from './chat-list-item/component.jsx';
import KEY_CODES from '/imports/utils/keyCodes';
const propTypes = {
openChats: PropTypes.array.isRequired,
@ -30,6 +31,128 @@ class UserList extends Component {
this.state = {
compact: this.props.compact,
};
this.rovingIndex = this.rovingIndex.bind(this);
this.focusList = this.focusList.bind(this);
this.focusListItem = this.focusListItem.bind(this);
this.counter = -1;
}
focusList(activeElement, list) {
activeElement.tabIndex = -1;
this.counter = 0;
list.tabIndex = 0;
list.focus();
}
focusListItem(active, direction, element, count) {
function select() {
element.tabIndex = 0;
element.focus();
}
active.tabIndex = -1;
switch (direction) {
case 'down':
element.childNodes[this.counter].tabIndex = 0;
element.childNodes[this.counter].focus();
this.counter++;
break;
case 'up':
this.counter--;
element.childNodes[this.counter].tabIndex = 0;
element.childNodes[this.counter].focus();
break;
case 'upLoopUp':
case 'upLoopDown':
this.counter = count - 1;
select();
break;
case 'downLoopDown':
this.counter = -1;
select();
break;
case 'downLoopUp':
this.counter = 1;
select();
break;
}
}
rovingIndex(...Args) {
const { users, openChats } = this.props;
let active = document.activeElement;
let list;
let items;
let count;
switch (Args[1]) {
case 'users':
list = this._usersList;
items = this._userItems;
count = users.length;
break;
case 'messages':
list = this._msgsList;
items = this._msgItems;
count = openChats.length;
break;
}
if (Args[0].keyCode === KEY_CODES.ESCAPE
|| this.counter === -1
|| this.counter > count) {
this.focusList(active, list);
}
if (Args[0].keyCode === KEY_CODES.ENTER
|| Args[0].keyCode === KEY_CODES.ARROW_RIGHT
|| Args[0].keyCode === KEY_CODES.ARROW_LEFT) {
active.firstChild.click();
}
if (Args[0].keyCode === KEY_CODES.ARROW_DOWN) {
if (this.counter < count) {
this.focusListItem(active, 'down', items);
}else if (this.counter === count) {
this.focusListItem(active, 'downLoopDown', list);
}else if (this.counter === 0) {
this.focusListItem(active, 'downLoopUp', list);
}
}
if (Args[0].keyCode === KEY_CODES.ARROW_UP) {
if (this.counter < count && this.counter !== 0) {
this.focusListItem(active, 'up', items);
}else if (this.counter === 0) {
this.focusListItem(active, 'upLoopUp', list, count);
}else if (this.counter === count) {
this.focusListItem(active, 'upLoopDown', list, count);
}
}
}
componentDidMount() {
let _this = this;
if (!this.state.compact) {
this._msgsList.addEventListener('keypress', function (event) {
_this.rovingIndex.call(this, event, 'messages');
});
this._usersList.addEventListener('keypress', function (event) {
_this.rovingIndex.call(this, event, 'users');
});
}
}
componentWillUnmount() {
this._msgsList.removeEventListener('keypress', function (event) {}, false);
this._usersList.removeEventListener('keypress', function (event) {}, false);
}
render() {
@ -48,9 +171,9 @@ class UserList extends Component {
<div className={styles.header}>
{
!this.state.compact ?
<h2 className={styles.headerTitle}>
<div className={styles.headerTitle} role="banner">
{intl.formatMessage(intlMessages.participantsTitle)}
</h2> : null
</div> : null
}
</div>
);
@ -76,11 +199,14 @@ class UserList extends Component {
<div className={styles.messages}>
{
!this.state.compact ?
<h3 className={styles.smallTitle}>
<div className={styles.smallTitle} role="banner">
{intl.formatMessage(intlMessages.messagesTitle)}
</h3> : <hr className={styles.separator}></hr>
</div> : <hr className={styles.separator}></hr>
}
<div className={styles.scrollableList}>
<div
tabIndex={0}
className={styles.scrollableList}
ref={(r) => this._msgsList = r}>
<ReactCSSTransitionGroup
transitionName={listTransition}
transitionAppear={true}
@ -89,15 +215,18 @@ class UserList extends Component {
transitionAppearTimeout={0}
transitionEnterTimeout={0}
transitionLeaveTimeout={0}
component="ul"
component="div"
className={cx(styles.chatsList, styles.scrollableList)}>
<div ref={(r) => this._msgItems = r}>
{openChats.map(chat => (
<ChatListItem
compact={this.state.compact}
key={chat.id}
openChat={openChat}
chat={chat} />
chat={chat}
tabIndex={-1} />
))}
</div>
</ReactCSSTransitionGroup>
</div>
</div>
@ -111,6 +240,7 @@ class UserList extends Component {
isBreakoutRoom,
intl,
makeCall,
meeting,
} = this.props;
const userActions = {
@ -150,33 +280,41 @@ class UserList extends Component {
<div className={styles.participants}>
{
!this.state.compact ?
<h3 className={styles.smallTitle}>
<div className={styles.smallTitle} role="banner">
{intl.formatMessage(intlMessages.usersTitle)}
&nbsp;({users.length})
</h3> : <hr className={styles.separator}></hr>
</div> : <hr className={styles.separator}></hr>
}
<ReactCSSTransitionGroup
transitionName={listTransition}
transitionAppear={true}
transitionEnter={true}
transitionLeave={true}
transitionAppearTimeout={0}
transitionEnterTimeout={0}
transitionLeaveTimeout={0}
component="ul"
className={cx(styles.participantsList, styles.scrollableList)}>
{
users.map(user => (
<UserListItem
compact={this.state.compact}
key={user.id}
isBreakoutRoom={isBreakoutRoom}
user={user}
currentUser={currentUser}
userActions={userActions}
/>
))}
</ReactCSSTransitionGroup>
<div
className={styles.scrollableList}
tabIndex={0}
ref={(r) => this._usersList = r}>
<ReactCSSTransitionGroup
transitionName={listTransition}
transitionAppear={true}
transitionEnter={true}
transitionLeave={true}
transitionAppearTimeout={0}
transitionEnterTimeout={0}
transitionLeaveTimeout={0}
component="div"
className={cx(styles.participantsList, styles.scrollableList)}>
<div ref={(r) => this._userItems = r}>
{
users.map(user => (
<UserListItem
compact={this.state.compact}
key={user.id}
isBreakoutRoom={isBreakoutRoom}
user={user}
currentUser={currentUser}
userActions={userActions}
meeting={meeting}
/>))
}
</div>
</ReactCSSTransitionGroup>
</div>
</div>
);
}

View File

@ -3,6 +3,7 @@ import { createContainer } from 'meteor/react-meteor-data';
import { meetingIsBreakout } from '/imports/ui/components/app/service';
import { makeCall } from '/imports/ui/services/api';
import Service from './service.js';
import Meetings from '/imports/api/meetings';
import UserList from './component.jsx';
@ -17,12 +18,14 @@ class UserListContainer extends Component {
userActions,
isBreakoutRoom,
children,
meeting,
} = this.props;
return (
<UserList
compact={compact}
users={users}
meeting={meeting}
currentUser={currentUser}
openChats={openChats}
openChat={openChat}
@ -37,6 +40,7 @@ class UserListContainer extends Component {
export default createContainer(({ params }) => ({
users: Service.getUsers(),
meeting: Meetings.findOne({}),
currentUser: Service.getCurrentUser(),
openChats: Service.getOpenChats(params.chatID),
openChat: params.chatID,

View File

@ -32,7 +32,8 @@ const mapUser = user => ({
isListenOnly: user.listenOnly,
isSharingWebcam: user.webcam_stream.length,
isPhoneUser: user.phone_user,
isOnline: user.connection_status === 'online'
isOnline: user.connection_status === 'online',
isLocked: user.locked,
});
const mapOpenChats = chat => {

View File

@ -15,8 +15,8 @@ import DropdownContent from '/imports/ui/components/dropdown/content/component';
import DropdownList from '/imports/ui/components/dropdown/list/component';
import DropdownListItem from '/imports/ui/components/dropdown/list/item/component';
import DropdownListSeparator from '/imports/ui/components/dropdown/list/separator/component';
import DropdownListTitle from '/imports/ui/components/dropdown/list/title/component';
import DropdownListTitle from '/imports/ui/components/dropdown/list/title/component';
const propTypes = {
user: React.PropTypes.shape({
name: React.PropTypes.string.isRequired,
@ -46,10 +46,22 @@ const messages = defineMessages({
id: 'app.userlist.you',
description: 'Text for identifying your user',
},
locked: {
id: 'app.userlist.locked',
description: 'Text for identifying locked user',
},
menuTitleContext: {
id: 'app.userlist.menuTitleContext',
description: 'adds context to userListItem menu title',
},
userItemStatusAriaLabel: {
id: 'app.userlist.useritem.status.arialabel',
description: 'adds aria label for user and status',
},
userItemAriaLabel: {
id: 'app.userlist.useritem.nostatus.arialabel',
description: 'aria label for user',
},
});
const userActionsTransition = {
@ -167,7 +179,7 @@ class UserListItem extends Component {
if (!isDropdownVisible) {
const offsetPageTop =
(dropdownTrigger.offsetTop + dropdownTrigger.offsetHeight - scrollContainer.scrollTop);
nextState.dropdownOffset = window.innerHeight - offsetPageTop;
nextState.dropdownDirection = 'bottom';
}
@ -178,7 +190,7 @@ class UserListItem extends Component {
/**
* Check if the dropdown is visible and is opened by the user
*
*
* @return True if is visible and opened by the user.
*/
isDropdownActivedByUser() {
@ -188,8 +200,8 @@ class UserListItem extends Component {
/**
* Return true if the content fit on the screen, false otherwise.
*
* @param {number} contentOffSetTop
*
* @param {number} contentOffSetTop
* @param {number} contentOffsetHeight
* @return True if the content fit on the screen, false otherwise.
*/
@ -230,15 +242,81 @@ class UserListItem extends Component {
userItemContentsStyle[styles.userItemContentsCompact] = compact;
userItemContentsStyle[styles.active] = this.state.isActionsOpen;
const {
user,
intl,
} = this.props;
let you = (user.isCurrent) ? intl.formatMessage(messages.you) : null;
let presenter = (user.isPresenter)
? intl.formatMessage(messages.presenter)
: null;
let userAriaLabel = (user.emoji.status === 'none')
? intl.formatMessage(messages.userItemAriaLabel,
{ username: user.name, presenter: presenter, you: you, })
: intl.formatMessage(messages.userItemStatusAriaLabel,
{ username: user.name,
presenter: presenter,
you: you,
status: user.emoji.status, });
let actions = this.getAvailableActions();
let contents = (
<div
className={cx(styles.userListItem, userItemContentsStyle)}
aria-label={userAriaLabel}>
<div className={styles.userItemContents} aria-hidden="true">
<UserAvatar user={user} />
{this.renderUserName()}
{this.renderUserIcons()}
</div>
</div>
);
if (!actions.length) {
return contents;
}
const { dropdownOffset, dropdownDirection, dropdownVisible, } = this.state;
return (
<li
role="button"
aria-haspopup="true"
aria-live="assertive"
aria-relevant="additions"
className={cx(styles.userListItem, userItemContentsStyle)}>
{this.renderUserContents()}
</li>
<Dropdown
ref="dropdown"
isOpen={this.state.isActionsOpen}
onShow={this.onActionsShow}
onHide={this.onActionsHide}
className={styles.dropdown}
autoFocus={false}
hasPopup="true"
ariaLive="assertive"
ariaRelevant="additions">
<DropdownTrigger>
{contents}
</DropdownTrigger>
<DropdownContent
style={{
visibility: dropdownVisible ? 'visible' : 'hidden',
[dropdownDirection]: `${dropdownOffset}px`,
}}
className={styles.dropdownContent}
placement={`right ${dropdownDirection}`}>
<DropdownList>
{
[
(<DropdownListTitle
description={intl.formatMessage(messages.menuTitleContext)}
key={_.uniqueId('dropdown-list-title')}>
{user.name}
</DropdownListTitle>),
(<DropdownListSeparator key={_.uniqueId('action-separator')} />),
].concat(actions)
}
</DropdownList>
</DropdownContent>
</Dropdown>
);
}
@ -250,7 +328,7 @@ class UserListItem extends Component {
let actions = this.getAvailableActions();
let contents = (
<div tabIndex={0} className={styles.userItemContents}>
<div className={styles.userItemContents}>
<UserAvatar user={user} />
{this.renderUserName()}
{this.renderUserIcons()}
@ -269,7 +347,8 @@ class UserListItem extends Component {
isOpen={this.state.isActionsOpen}
onShow={this.onActionsShow}
onHide={this.onActionsHide}
className={styles.dropdown}>
className={styles.dropdown}
autoFocus={false}>
<DropdownTrigger>
{contents}
</DropdownTrigger>
@ -283,12 +362,12 @@ class UserListItem extends Component {
<DropdownList>
{
[
(<DropdownListTitle
description={intl.formatMessage(messages.menuTitleContext)}
key={_.uniqueId('dropdown-list-title')}>
{user.name}
</DropdownListTitle>),
[
(<DropdownListTitle
description={intl.formatMessage(messages.menuTitleContext)}
key={_.uniqueId('dropdown-list-title')}>
{user.name}
</DropdownListTitle>),
(<DropdownListSeparator key={_.uniqueId('action-separator')} />),
].concat(actions)
}
@ -303,6 +382,7 @@ class UserListItem extends Component {
user,
intl,
compact,
meeting,
} = this.props;
if (compact) {
@ -310,6 +390,7 @@ class UserListItem extends Component {
}
let userNameSub = [];
if (user.isPresenter) {
userNameSub.push(intl.formatMessage(messages.presenter));
}
@ -320,6 +401,8 @@ class UserListItem extends Component {
userNameSub = userNameSub.join(' ');
const { disablePrivateChat, disableCam, disableMic, lockedLayout, disablePublicChat } = meeting.roomLockSettings;
return (
<div className={styles.userName}>
<span className={styles.userNameMain}>
@ -327,6 +410,15 @@ class UserListItem extends Component {
</span>
<span className={styles.userNameSub}>
{userNameSub}
{(user.isLocked && (disablePrivateChat
|| disableCam
|| disableMic
|| lockedLayout
|| disablePublicChat)) ?
<span> {(user.isCurrent ? ' | ' : null)}
<Icon iconName='lock' />
{intl.formatMessage(messages.locked)}
</span> : null}
</span>
</div>
);
@ -394,6 +486,7 @@ class UserListItem extends Component {
label={action.label}
defaultMessage={action.label}
onClick={action.handler.bind(this, ...parameters)}
placeInTabOrder={true}
/>
);

View File

@ -38,6 +38,7 @@ export default class TextDrawComponent extends React.Component {
fontStretch: 'normal',
lineHeight: 'normal',
fontFamily: 'Arial',
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
wordBreak: 'normal',
textAlign: 'left',

View File

@ -4,7 +4,7 @@ import { Tracker } from 'meteor/tracker';
import Storage from '/imports/ui/services/storage/session';
import Users from '/imports/api/users';
import { makeCall } from '/imports/ui/services/api';
import { makeCall, logClient } from '/imports/ui/services/api';
const CONNECTION_TIMEOUT = Meteor.settings.public.app.connectionTimeout;
@ -89,10 +89,22 @@ class Auth {
}
return new Promise((resolve, reject) => {
makeCall('userLogout').then(() => {
this.fetchLogoutUrl()
const credentialsSnapshot = {
meetingId: this.meetingID,
requesterUserId: this.userID,
requesterToken: this.token,
};
// make sure users who did not connect are not added to the meeting
// do **not** use the custom call - it relies on expired data
Meteor.call('userLogout', credentialsSnapshot, (error, result) => {
if (error) {
logClient('error', { error, method: 'userLogout', credentialsSnapshot });
} else {
this.fetchLogoutUrl()
.then(this.clearCredentials)
.then(resolve);
}
});
});
};
@ -109,6 +121,13 @@ class Auth {
return new Promise((resolve, reject) => {
Tracker.autorun((c) => {
if (!(credentials.meetingId && credentials.requesterToken && credentials.requesterUserId)) {
return reject({
error: 500,
description: 'Authentication subscription failed due to missing credentials.',
});
}
setTimeout(() => {
c.stop();
reject({

4
bigbluebutton-html5/imports/utils/keyCodes.js Normal file → Executable file
View File

@ -4,6 +4,8 @@ export const TAB = 9;
export const ESCAPE = 27;
export const ARROW_UP = 38;
export const ARROW_DOWN = 40;
export const ARROW_RIGHT = 39;
export const ARROW_LEFT = 37;
export default {
SPACE,
@ -12,4 +14,6 @@ export default {
ESCAPE,
ARROW_UP,
ARROW_DOWN,
ARROW_RIGHT,
ARROW_LEFT,
};

View File

@ -2,4 +2,4 @@
app:
# Flag for HTTPS.
httpsConnection: true
connectionTimeout: 5000
connectionTimeout: 10000

14
bigbluebutton-html5/private/locales/bg_BG.json Normal file → Executable file
View File

@ -1,16 +1,16 @@
{
"app.home.greeting": "Добре дошли, {name}! Вашата презентация ще започне всеки момент",
"app.home.greeting": "Добре дошли, {0}! Вашата презентация ще започне всеки момент",
"app.userlist.usersTitle": "Потребители",
"app.userlist.participantsTitle": "Участници",
"app.userlist.messagesTitle": "Съобщения",
"app.userlist.presenter": "Лектор",
"app.userlist.you": "Вие",
"app.chat.submitLabel": "Изпрати",
"app.chat.inputLabel": "Въведи съобщение за {name}",
"app.chat.inputPlaceholder": "Съобщение {name}",
"app.chat.inputLabel": "Въведи съобщение за {0}",
"app.chat.inputPlaceholder": "Съобщение {0}",
"app.chat.titlePublic": "Общ чат",
"app.chat.titlePrivate": "Private Chat with {name}",
"app.chat.partnerDisconnected": "{name} has left the meeting",
"app.chat.titlePrivate": "Private Chat with {0}",
"app.chat.partnerDisconnected": "{0} has left the meeting",
"app.chat.moreMessages": "More messages below",
"app.kickMessage": "You have been kicked out of the meeting",
"app.whiteboard.slideControls.prevSlideLabel": "Previous slide",
@ -27,7 +27,7 @@
"app.whiteboard.slideControls.zoomDescrip": "Change the zoom level of the presentation",
"app.failedMessage": "Apologies, trouble connecting to the server.",
"app.connectingMessage": "Connecting...",
"app.waitingMessage": "Disconnected. Trying to reconnect in {seconds} seconds...",
"app.waitingMessage": "Disconnected. Trying to reconnect in {0} seconds...",
"app.navBar.settingsDropdown.optionsLabel": "Options",
"app.navBar.settingsDropdown.fullscreenLabel": "Make fullscreen",
"app.navBar.settingsDropdown.settingsLabel": "Open settings",
@ -89,7 +89,7 @@
"app.breakoutJoinConfirmation.confirmDesc": "Join you to the Breakout Room",
"app.breakoutJoinConfirmation.dismissLabel": "Cancel",
"app.breakoutJoinConfirmation.dismissDesc": "Closes and rejects Joining the Breakout Room",
"app.breakoutTimeRemainingMessage": "Breakout Room time remaining: {time}",
"app.breakoutTimeRemainingMessage": "Breakout Room time remaining: {0}",
"app.breakoutWillCloseMessage": "Time ended. Breakout Room will close soon",
"app.calculatingBreakoutTimeRemaining": "Calculating remaining time...",
"app.audioModal.microphoneLabel": "Microphone",

22
bigbluebutton-html5/private/locales/de.json Normal file → Executable file
View File

@ -1,5 +1,5 @@
{
"app.home.greeting": "Willkommen {name}! Ihre Präsentation startet in Kürze...",
"app.home.greeting": "Willkommen {0}! Ihre Präsentation startet in Kürze...",
"app.userlist.usersTitle": "Teilnehmer",
"app.userlist.participantsTitle": "Teilnehmer",
"app.userlist.messagesTitle": "Nachrichten",
@ -7,17 +7,17 @@
"app.userlist.you": "Sie",
"app.userlist.Label": "Teilnehmerliste",
"app.chat.submitLabel": "Nachricht senden",
"app.chat.inputLabel": "Chat-Nachricht eingeben für {name}",
"app.chat.inputPlaceholder": "Nachricht an {name}",
"app.chat.inputLabel": "Chat-Nachricht eingeben für {0}",
"app.chat.inputPlaceholder": "Nachricht an {0}",
"app.chat.titlePublic": "Öffentlicher Chat",
"app.chat.titlePrivate": "Privater Chat mit {name}",
"app.chat.partnerDisconnected": "{name} hat die Konferenz verlassen",
"app.chat.closeChatLabel": "Schließe {title}",
"app.chat.hideChatLabel": "Verstecke {title}",
"app.chat.titlePrivate": "Privater Chat mit {0}",
"app.chat.partnerDisconnected": "{0} hat die Konferenz verlassen",
"app.chat.closeChatLabel": "Schließe {0}",
"app.chat.hideChatLabel": "Verstecke {0}",
"app.chat.moreMessages": "Weitere Nachrichten",
"app.userlist.menuTitleContext": "verfügbare Optionen",
"app.userlist.chatlistitem.unreadSingular": "{count} neue Nachricht",
"app.userlist.chatlistitem.unreadPlural": "{count} neue Nachrichten",
"app.userlist.chatlistitem.unreadSingular": "{0} neue Nachricht",
"app.userlist.chatlistitem.unreadPlural": "{0} neue Nachrichten",
"app.chat.Label": "Chat",
"app.chat.emptyLogLabel": "Chat-Log ist leer",
"app.media.Label": "Medien",
@ -100,7 +100,7 @@
"app.submenu.closedCaptions.fontColorLabel": "Schriftfarbe",
"app.submenu.participants.muteAllLabel": "Alle stummschalten außer Präsentator",
"app.submenu.participants.lockAllLabel": "Alle Teilnehmer sperren",
"app.submenu.participants.lockItemLabel": "Teilnehmer {lockItem}",
"app.submenu.participants.lockItemLabel": "Teilnehmer {0}",
"app.submenu.participants.lockMicDesc": "Deaktiviert das Mikrofon für alle gesperrten Teilnehmer",
"app.submenu.participants.lockCamDesc": "Deaktiviert die Webcam für alle gesperrten Teilnehmer",
"app.submenu.participants.lockPublicChatDesc": "Deaktiviert den öffentlichen Chat für alle gesperrten Teilnehmer",
@ -158,7 +158,7 @@
"app.breakoutJoinConfirmation.confirmDesc": "Dem Breakout-Raum beitreten",
"app.breakoutJoinConfirmation.dismissLabel": "Abbrechen",
"app.breakoutJoinConfirmation.dismissDesc": "Beitritt zum Breakout-Raum ablehnen",
"app.breakoutTimeRemainingMessage": "Verbleibende Zeit für den Breakout-Raum: {time}",
"app.breakoutTimeRemainingMessage": "Verbleibende Zeit für den Breakout-Raum: {0}",
"app.breakoutWillCloseMessage": "Zeit abgelaufen. Der Breakout-Raum wird in Kürze geschlossen",
"app.calculatingBreakoutTimeRemaining": "Berechne die verbleibende Zeit...",
"app.audioModal.microphoneLabel": "Mit Mikrofon",

View File

@ -1,29 +1,32 @@
{
"app.home.greeting": "Welcome {name}! Your presentation will begin shortly...",
"app.home.greeting": "Welcome {0}! Your presentation will begin shortly...",
"app.userlist.usersTitle": "Users",
"app.userlist.participantsTitle": "Participants",
"app.userlist.messagesTitle": "Messages",
"app.userlist.presenter": "Presenter",
"app.userlist.you": "You",
"app.userlist.locked": "Locked",
"app.userlist.Label": "User List",
"app.chat.submitLabel": "Send Message",
"app.chat.inputLabel": "Message input for chat {name}",
"app.chat.inputPlaceholder": "Message {name}",
"app.chat.inputLabel": "Message input for chat {0}",
"app.chat.inputPlaceholder": "Message {0}",
"app.chat.titlePublic": "Public Chat",
"app.chat.titlePrivate": "Private Chat with {name}",
"app.chat.partnerDisconnected": "{name} has left the meeting",
"app.chat.closeChatLabel": "Close {title}",
"app.chat.hideChatLabel": "Hide {title}",
"app.chat.titlePrivate": "Private Chat with {0}",
"app.chat.partnerDisconnected": "{0} has left the meeting",
"app.chat.closeChatLabel": "Close {0}",
"app.chat.hideChatLabel": "Hide {0}",
"app.chat.moreMessages": "More messages below",
"app.userlist.menuTitleContext": "available options",
"app.userlist.chatlistitem.unreadSingular": "{count} New Message",
"app.userlist.chatlistitem.unreadPlural": "{count} New Messages",
"app.userlist.chatlistitem.unreadSingular": "{0} New Message",
"app.userlist.chatlistitem.unreadPlural": "{0} New Messages",
"app.userlist.menu.chat.label": "Chat",
"app.userlist.menu.clearStatus.label": "Clear Status",
"app.userlist.menu.makePresenter.label": "Make Presenter",
"app.userlist.menu.kickUser.label": "Kick user",
"app.userlist.menu.muteUserAudio.label": "Mute user",
"app.userlist.menu.unmuteUserAudio.label": "Unmute user",
"app.userlist.useritem.nostatus.arialabel": "{username} {presenter} {you}",
"app.userlist.useritem.status.arialabel": "{username} {presenter} {you} current status {status}",
"app.chat.Label": "Chat",
"app.chat.emptyLogLabel": "Chat log empty",
"app.media.Label": "Media",
@ -42,7 +45,7 @@
"app.polling.pollingTitle": "Polling Options",
"app.failedMessage": "Apologies, trouble connecting to the server.",
"app.connectingMessage": "Connecting...",
"app.waitingMessage": "Disconnected. Trying to reconnect in {seconds} seconds...",
"app.waitingMessage": "Disconnected. Trying to reconnect in {0} seconds...",
"app.navBar.settingsDropdown.optionsLabel": "Options",
"app.navBar.settingsDropdown.fullscreenLabel": "Make fullscreen",
"app.navBar.settingsDropdown.settingsLabel": "Open settings",
@ -107,7 +110,7 @@
"app.submenu.closedCaptions.fontColorLabel": "Font color",
"app.submenu.participants.muteAllLabel": "Mute all except the presenter",
"app.submenu.participants.lockAllLabel": "Lock all participants",
"app.submenu.participants.lockItemLabel": "Participants {lockItem}",
"app.submenu.participants.lockItemLabel": "Participants {0}",
"app.submenu.participants.lockMicDesc": "Disables the microphone for all locked participants",
"app.submenu.participants.lockCamDesc": "Disables the webcam for all locked participants",
"app.submenu.participants.lockPublicChatDesc": "Disables public chat for all locked participants",
@ -161,7 +164,7 @@
"app.actionsBar.emojiMenu.thumbsupDesc": "Change your status to thumbs up",
"app.actionsBar.emojiMenu.thumbsdownLabel": "Thumbs down",
"app.actionsBar.emojiMenu.thumbsdownDesc": "Change your status to thumbs down",
"app.actionsBar.currentStatusDesc": "current status {status}",
"app.actionsBar.currentStatusDesc": "current status {0}",
"app.audioNotification.audioFailedMessage": "Your audio connection failed to connect",
"app.audioNotification.mediaFailedMessage": "getUserMicMedia failed, Only secure origins are allowed",
"app.audioNotification.closeLabel": "Close",
@ -171,7 +174,7 @@
"app.breakoutJoinConfirmation.confirmDesc": "Join you to the Breakout Room",
"app.breakoutJoinConfirmation.dismissLabel": "Cancel",
"app.breakoutJoinConfirmation.dismissDesc": "Closes and rejects Joining the Breakout Room",
"app.breakoutTimeRemainingMessage": "Breakout Room time remaining: {time}",
"app.breakoutTimeRemainingMessage": "Breakout Room time remaining: {0}",
"app.breakoutWillCloseMessage": "Time ended. Breakout Room will close soon",
"app.calculatingBreakoutTimeRemaining": "Calculating remaining time...",
"app.audioModal.microphoneLabel": "Microphone",

14
bigbluebutton-html5/private/locales/pt_BR.json Normal file → Executable file
View File

@ -1,16 +1,16 @@
{
"app.home.greeting": "Bem-vindo {name}! Sua aprensentação começará em breve...",
"app.home.greeting": "Bem-vindo {0}! Sua aprensentação começará em breve...",
"app.userlist.usersTitle": "Users",
"app.userlist.participantsTitle": "Participantes",
"app.userlist.messagesTitle": "Mensagens",
"app.userlist.presenter": "Apresentador",
"app.userlist.you": "Você",
"app.chat.submitLabel": "Enviar Mensagem",
"app.chat.inputLabel": "Campo de mensagem para conversa {name}",
"app.chat.inputPlaceholder": "Message {name}",
"app.chat.inputLabel": "Campo de mensagem para conversa {0}",
"app.chat.inputPlaceholder": "Message {0}",
"app.chat.titlePublic": "Conversa Publíca",
"app.chat.titlePrivate": "Conversa Privada com {name}",
"app.chat.partnerDisconnected": "{name} saiu da sala",
"app.chat.titlePrivate": "Conversa Privada com {0}",
"app.chat.partnerDisconnected": "{0} saiu da sala",
"app.chat.moreMessages": "Mais mensagens abaixo",
"app.kickMessage": "Você foi expulso da apresentação",
"app.whiteboard.slideControls.prevSlideLabel": "Slide Anterior",
@ -27,7 +27,7 @@
"app.whiteboard.slideControls.zoomDescrip": "Change the zoom level of the presentation",
"app.failedMessage": "Desculpas, estamos com problemas para se conectar ao servidor.",
"app.connectingMessage": "Conectando...",
"app.waitingMessage": "Desconectado. Tentando reconectar em {seconds} segundos...",
"app.waitingMessage": "Desconectado. Tentando reconectar em {0} segundos...",
"app.navBar.settingsDropdown.optionsLabel": "Options",
"app.navBar.settingsDropdown.fullscreenLabel": "Make fullscreen",
"app.navBar.settingsDropdown.settingsLabel": "Open settings",
@ -89,7 +89,7 @@
"app.breakoutJoinConfirmation.confirmDesc": "Join you to the Breakout Room",
"app.breakoutJoinConfirmation.dismissLabel": "Cancel",
"app.breakoutJoinConfirmation.dismissDesc": "Closes and rejects Joining the Breakout Room",
"app.breakoutTimeRemainingMessage": "Breakout Room time remaining: {time}",
"app.breakoutTimeRemainingMessage": "Breakout Room time remaining: {0}",
"app.breakoutWillCloseMessage": "Time ended. Breakout Room will close soon",
"app.calculatingBreakoutTimeRemaining": "Calculating remaining time...",
"app.audioModal.microphoneLabel": "Microphone",

View File

@ -1,8 +1,8 @@
<recordID>${r.getId()}</recordID>
<#if r.getMeeting()??>
<meetingID>${r.getMeeting().getId()?html}</meetingID>
<externalMeetingID>${r.getMeeting().getExternalId()?html}</externalMeetingID>
<meetingID>${r.getMeeting().getExternalId()?html}</meetingID>
<internalMeetingID>${r.getMeeting().getId()?html}</internalMeetingID>
<name>${r.getMeeting().getName()?html}</name>
<isBreakout>${r.getMeeting().isBreakout()?c}</isBreakout>
<#else>

View File

@ -72,6 +72,7 @@ def process_archived_meetings(recording_dir)
if step_succeeded
BigBlueButton.logger.info("Process format #{process_type} succeeded for #{meeting_id}")
BigBlueButton.logger.info("Process took #{step_time}ms")
IO.write("#{recording_dir}/process/#{process_type}/#{meeting_id}/processing_time", step_time)
else
BigBlueButton.logger.info("Process format #{process_type} failed for #{meeting_id}")
BigBlueButton.logger.info("Process took #{step_time}ms")