Merge branch 'master' of github.com:bigbluebutton/bigbluebutton into perroned-merge-webrtc-screenshare-2

This commit is contained in:
Ubuntu 2017-03-24 19:14:21 +00:00
commit 5e0a5f019c
114 changed files with 2062 additions and 1672 deletions

View File

@ -15,6 +15,14 @@ ApplicationControlBar {
dropShadowColor: #000000;
}
.defaultControlBarStyle {
color : #0B333C;
}
.darkControlBarStyle {
color : #ffffff;
}
Panel {
borderColor: #dfdfdf;
borderAlpha: 1;
@ -755,6 +763,18 @@ MDIWindow { /*None of the following properties are overridden by the MDIWindow c
borderThicknessRight: 3;
}
.videoDockStyleFocusChatLayout {
borderStyle : none;
borderColor: #42444c;
backgroundColor: #42444c;
}
.videoDockStyleNoFocusChatLayout {
borderStyle : none;
borderColor: #42444c;
backgroundColor: #42444c;
}
.presentationSlideViewStyle {
backgroundColor: #b9babc;
}
@ -865,6 +885,14 @@ MDIWindow { /*None of the following properties are overridden by the MDIWindow c
borderThicknessRight: 3;
}
.defaultShellStyle {
backgroundColor: #fefeff;
}
.darkShellStyle {
backgroundColor: #42444c;
}
.mdiWindowTitle {
color: #3f3f41;
fontFamily: Arial;

View File

@ -489,14 +489,17 @@ bbb.accessibility.chat.initialDescription = Please use the arrow keys to navigat
bbb.accessibility.notes.notesview.input = Notes input
bbb.shortcuthelp.title = Shortcut Keys
bbb.shortcuthelp.titleBar = Shortcut Keys Window Title Bar
bbb.shortcuthelp.minimizeBtn.accessibilityName = Minimize the Shortcut Help Window
bbb.shortcuthelp.maximizeRestoreBtn.accessibilityName = Maximize the Shortcut Help Window
bbb.shortcuthelp.closeBtn.accessibilityName = Close the Shortcut Help Window
bbb.shortcuthelp.dropdown.accessibilityName = Shortcut Category
bbb.shortcuthelp.dropdown.general = Global shortcuts
bbb.shortcuthelp.dropdown.presentation = Presentation shortcuts
bbb.shortcuthelp.dropdown.chat = Chat shortcuts
bbb.shortcuthelp.dropdown.users = Users shortcuts
bbb.shortcuthelp.dropdown.caption = Closed Caption shortcuts
bbb.shortcuthelp.browserWarning.text = The full list of shortcuts are only supported in Internet Explorer.
bbb.shortcuthelp.headers.shortcut = Shortcut
bbb.shortcuthelp.headers.function = Function
@ -539,7 +542,7 @@ bbb.shortcutkey.logout.function = Log out of this meeting
bbb.shortcutkey.raiseHand = 82
bbb.shortcutkey.raiseHand.function = Raise your hand
bbb.shortcutkey.present.upload = 85
bbb.shortcutkey.present.upload = 89
bbb.shortcutkey.present.upload.function = Upload presentation
bbb.shortcutkey.present.previous = 65
bbb.shortcutkey.present.previous.function = Go to previous slide
@ -549,19 +552,17 @@ bbb.shortcutkey.present.next = 69
bbb.shortcutkey.present.next.function = Go to next slide
bbb.shortcutkey.present.fitWidth = 70
bbb.shortcutkey.present.fitWidth.function = Fit slides to width
bbb.shortcutkey.present.fitPage = 80
bbb.shortcutkey.present.fitPage = 82
bbb.shortcutkey.present.fitPage.function = Fit slides to page
bbb.shortcutkey.users.makePresenter = 80
bbb.shortcutkey.users.makePresenter = 89
bbb.shortcutkey.users.makePresenter.function = Make selected person presenter
bbb.shortcutkey.users.kick = 75
bbb.shortcutkey.users.kick = 69
bbb.shortcutkey.users.kick.function = Kick selected person from the meeting
bbb.shortcutkey.users.mute = 83
bbb.shortcutkey.users.mute.function = Mute or unmute selected person
bbb.shortcutkey.users.muteall = 65
bbb.shortcutkey.users.muteall.function = Mute or unmute all users
bbb.shortcutkey.users.focusUsers = 85
bbb.shortcutkey.users.focusUsers.function = Focus to users list
bbb.shortcutkey.users.muteAllButPres = 65
bbb.shortcutkey.users.muteAllButPres.function = Mute everyone but the Presenter
bbb.shortcutkey.users.breakoutRooms = 75
@ -570,12 +571,12 @@ bbb.shortcutkey.users.focusBreakoutRooms = 82
bbb.shortcutkey.users.focusBreakoutRooms.function = Focus to breakout rooms list
bbb.shortcutkey.users.listenToBreakoutRoom = 76
bbb.shortcutkey.users.listenToBreakoutRoom.function = Listen to selected breakout room
bbb.shortcutkey.users.joinBreakoutRoom = 74
bbb.shortcutkey.users.joinBreakoutRoom = 79
bbb.shortcutkey.users.joinBreakoutRoom.function = Join selected breakout room
bbb.shortcutkey.chat.focusTabs = 89
bbb.shortcutkey.chat.focusTabs.function = Focus to chat tabs
bbb.shortcutkey.chat.focusBox = 66
bbb.shortcutkey.chat.focusBox = 77
bbb.shortcutkey.chat.focusBox.function = Focus to chat box
bbb.shortcutkey.chat.changeColour = 67
bbb.shortcutkey.chat.changeColour.function = Focus to font color picker.

View File

@ -81,7 +81,7 @@ function determineGlobalAlternateModifier()
// modifier = "control+alt";
//}
else{
modifier = "control+shift";
modifier = "control+shift+";
}
return modifier;
}

View File

@ -23,8 +23,6 @@ package org.bigbluebutton.common
import flexlib.mdi.containers.MDIWindow;
import flexlib.mdi.managers.MDIManager;
import mx.utils.ObjectUtil;
/**
* This class exists so we can properly handle context menus on MDIWindow
* instances. Also, we'll be able in the future to properly handle shortcuts

View File

@ -38,6 +38,8 @@ package org.bigbluebutton.main.model.users {
public var users:ArrayCollection;
public var invitedRecently : Boolean;
// Can be one of three following values self, none, other
public var listenStatus:String = NONE;

View File

@ -30,7 +30,6 @@ package org.bigbluebutton.main.model.users {
import org.as3commons.logging.api.getClassLogger;
import org.bigbluebutton.common.Role;
import org.bigbluebutton.core.BBB;
import org.bigbluebutton.core.managers.UserManager;
import org.bigbluebutton.core.model.Config;
import org.bigbluebutton.core.model.MeetingModel;
import org.bigbluebutton.core.vo.CameraSettingsVO;
@ -568,8 +567,21 @@ package org.bigbluebutton.main.model.users {
}
breakoutRooms.addItem(newRoom);
sortBreakoutRooms();
}
}
public function setLastBreakoutRoomInvitation(sequence:int):void {
var aRoom:BreakoutRoom;
for (var i:int = 0; i < breakoutRooms.length; i++) {
aRoom = breakoutRooms.getItemAt(i) as BreakoutRoom;
if (aRoom.sequence != sequence) {
aRoom.invitedRecently = false;
} else {
aRoom.invitedRecently = true;
}
}
sortBreakoutRooms();
}
private function sortBreakoutRooms() : void {
var sort:Sort = new Sort();
sort.fields = [new SortField("sequence", true, false, true)];

View File

@ -68,6 +68,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<mate:Listener type="{LockControlEvent.OPEN_LOCK_SETTINGS}" method="openLockSettingsWindow" />
<mate:Listener type="{BreakoutRoomEvent.OPEN_BREAKOUT_ROOMS_PANEL}" method="openBreakoutRoomsWindow" />
<mate:Listener type="{InvalidAuthTokenEvent.INVALID_AUTH_TOKEN}" method="handleInvalidAuthToken" />
<mate:Listener type="{SwitchedLayoutEvent.SWITCHED_LAYOUT_EVENT}" method="onLayoutChanged" />
<mx:Script>
<![CDATA[
@ -101,6 +103,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
import org.bigbluebutton.core.PopUpUtil;
import org.bigbluebutton.core.UsersUtil;
import org.bigbluebutton.core.events.LockControlEvent;
import org.bigbluebutton.core.events.SwitchedLayoutEvent;
import org.bigbluebutton.core.managers.UserManager;
import org.bigbluebutton.core.vo.LockSettingsVO;
import org.bigbluebutton.main.events.AppVersionEvent;
@ -680,6 +683,17 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
ExternalInterface.call("chatLinkClicked", e.text);
}
}
private function onLayoutChanged(e:SwitchedLayoutEvent):void {
if (e.layoutID != "bbb.layout.name.videochat") {
this.styleName = "defaultShellStyle";
controlBar.styleName = "defaultControlBarStyle";
} else {
this.styleName = "darkShellStyle";
controlBar.styleName = "darkControlBarStyle";
}
}
]]>
</mx:Script>

View File

@ -71,7 +71,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
private var chatResource:Array = ['bbb.shortcutkey.chat.focusTabs', 'bbb.shortcutkey.chat.focusBox', 'bbb.shortcutkey.chat.sendMessage',
'bbb.shortcutkey.chat.closePrivate'];
private var userResource:Array = ['bbb.shortcutkey.users.focusUsers', 'bbb.shortcutkey.users.makePresenter', 'bbb.shortcutkey.users.mute',
private var userResource:Array = ['bbb.shortcutkey.users.makePresenter', 'bbb.shortcutkey.users.mute',
'bbb.shortcutkey.users.kick', 'bbb.shortcutkey.users.muteall', 'bbb.shortcutkey.users.focusBreakoutRooms',
'bbb.shortcutkey.users.listenToBreakoutRoom', 'bbb.shortcutkey.users.joinBreakoutRoom'];
@ -99,14 +99,19 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
private function populateModules():void{
categoryAC.addItem(generalString);
if (ShortcutOptions.usersActive)
categoryAC.addItem(userString);
if (ShortcutOptions.presentationActive)
categoryAC.addItem(presentationString);
if (ShortcutOptions.chatActive)
categoryAC.addItem(chatString);
if (Capabilities.playerType == "ActiveX") {
if (ShortcutOptions.usersActive)
categoryAC.addItem(userString);
if (ShortcutOptions.presentationActive)
categoryAC.addItem(presentationString);
if (ShortcutOptions.chatActive)
categoryAC.addItem(chatString);
} else {
browserWarningText.visible = browserWarningText.includeInLayout = true;
}
}
private function reloadKeys():void {
@ -249,10 +254,10 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
]]>
</mx:Script>
<common:TabIndexer id="headerIndexer" startIndex="101" tabIndices="{[minimizeBtn, maximizeRestoreBtn, closeBtn]}"/>
<common:TabIndexer startIndex="115" tabIndices="{[categories, keyList]}"/>
<common:TabIndexer id="headerIndexer" startIndex="115" tabIndices="{[minimizeBtn, maximizeRestoreBtn, closeBtn]}"/>
<common:TabIndexer startIndex="119" tabIndices="{[categories, keyList]}"/>
<mx:ComboBox id="categories" labelField="Please select an area for which to view shortcut keys: "
<mx:ComboBox id="categories" accessibilityName="{ResourceUtil.getInstance().getString('bbb.shortcuthelp.dropdown.accessibilityName')}"
editable="false"
change="changeArray()">
<mx:ArrayCollection id="categoryAC">
@ -264,6 +269,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
-->
</mx:ArrayCollection>
</mx:ComboBox>
<mx:Text id="browserWarningText" visible="false" includeInLayout="false" text="{ResourceUtil.getInstance().getString('bbb.shortcuthelp.browserWarning.text')}" />
<mx:DataGrid id="keyList" draggableColumns="false" dataProvider="{shownKeys}" width="100%" height="100%">
<mx:columns>
<mx:DataGridColumn dataField="shortcut" width="150" headerText="{ResourceUtil.getInstance().getString('bbb.shortcuthelp.headers.shortcut')}"/>

View File

@ -235,7 +235,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
}
private function focusWindow(e:ShortcutEvent):void {
focusManager.setFocus(titleBarOverlay);
if (this.visible) {
focusManager.setFocus(titleBarOverlay);
}
}
]]>
</mx:Script>

View File

@ -120,7 +120,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
}
private function focusWindow(e:ShortcutEvent):void{
focusManager.setFocus(titleBarOverlay);
if (this.visible) {
focusManager.setFocus(titleBarOverlay);
}
}
private function fullScreenHandler(evt:FullScreenEvent):void {

View File

@ -1,17 +0,0 @@
package org.bigbluebutton.modules.layout.events
{
import flash.events.Event;
public class LayoutChangedEvent extends Event
{
public static const LAYOUT_CHANGED:String = "layout changed event";
public var layoutName:String;
public function LayoutChangedEvent(layoutName:String)
{
super(LAYOUT_CHANGED, true, false);
this.layoutName = layoutName;
}
}
}

View File

@ -219,18 +219,18 @@ package org.bigbluebutton.modules.layout.managers
var e:SyncLayoutEvent = new SyncLayoutEvent(layout);
_globalDispatcher.dispatchEvent(e);
}
}
private function applyLayout(layout:LayoutDefinition):void {
_detectContainerChange = false;
if (layout != null) {
layout.applyToCanvas(_canvas);
dispatchSwitchedLayoutEvent(layout.name);
}
//trace(LOG + " applyLayout layout [" + layout.name + "]");
updateCurrentLayout(layout);
_detectContainerChange = true;
}
}
private function applyLayout(layout:LayoutDefinition):void {
_detectContainerChange = false;
if (layout != null) {
layout.applyToCanvas(_canvas);
dispatchSwitchedLayoutEvent(layout.name);
}
//trace(LOG + " applyLayout layout [" + layout.name + "]");
updateCurrentLayout(layout);
_detectContainerChange = true;
}
public function handleLockLayoutEvent(e: LockLayoutEvent):void {
@ -282,47 +282,48 @@ package org.bigbluebutton.modules.layout.managers
}
}
}
private function onContainerResized(e:ResizeEvent):void {
//trace(LOG + "Canvas is changing as user is resizing browser");
/*
* the main canvas has been resized
* while the user is resizing the window, this event is dispatched
* multiple times, so we use a timer to re-apply the current layout
* only once, when the user finished his action
*/
_applyCurrentLayoutTimer.reset();
_applyCurrentLayoutTimer.start();
}
private function onActionOverWindowFinished(e:MDIManagerEvent):void {
if (LayoutDefinition.ignoreWindow(e.window))
return;
checkPermissionsOverWindow(e.window);
//trace(LOG + "Window is being resized. Event=[" + e.type + "]");
//updateCurrentLayout(null);
/*
* All events must be delayed because the window doesn't actually
* change size until after the animation has finished.
*/
_sendCurrentLayoutUpdateTimer.reset();
_sendCurrentLayoutUpdateTimer.start();
}
private function updateCurrentLayout(layout:LayoutDefinition):LayoutDefinition {
//trace(LOG + "updateCurrentLayout");
if (layout != null) {
if (_currentLayout) _currentLayout.currentLayout = false;
_currentLayout = layout;
//trace(LOG + "updateCurrentLayout - currentLayout = [" + layout.name + "]");
layout.currentLayout = true;
} else {
_currentLayout = LayoutDefinition.getLayout(_canvas, ResourceUtil.getInstance().getString('bbb.layout.combo.customName'));
//trace(LOG + "updateCurrentLayout - layout is NULL! Setting currentLayout = [" + _currentLayout.name + "]");
}
return _currentLayout;
}
private function onContainerResized(e:ResizeEvent):void {
//trace(LOG + "Canvas is changing as user is resizing browser");
/*
* the main canvas has been resized
* while the user is resizing the window, this event is dispatched
* multiple times, so we use a timer to re-apply the current layout
* only once, when the user finished his action
*/
_applyCurrentLayoutTimer.reset();
_applyCurrentLayoutTimer.start();
}
private function onActionOverWindowFinished(e:MDIManagerEvent):void {
if (LayoutDefinition.ignoreWindow(e.window))
return;
checkPermissionsOverWindow(e.window);
//trace(LOG + "Window is being resized. Event=[" + e.type + "]");
//updateCurrentLayout(null);
/*
* All events must be delayed because the window doesn't actually
* change size until after the animation has finished.
*/
_sendCurrentLayoutUpdateTimer.reset();
_sendCurrentLayoutUpdateTimer.start();
}
private function updateCurrentLayout(layout:LayoutDefinition):LayoutDefinition {
//trace(LOG + "updateCurrentLayout");
if (layout != null) {
if (_currentLayout)
_currentLayout.currentLayout = false;
_currentLayout = layout;
//trace(LOG + "updateCurrentLayout - currentLayout = [" + layout.name + "]");
layout.currentLayout = true;
} else {
_currentLayout = LayoutDefinition.getLayout(_canvas, ResourceUtil.getInstance().getString('bbb.layout.combo.customName'));
//trace(LOG + "updateCurrentLayout - layout is NULL! Setting currentLayout = [" + _currentLayout.name + "]");
}
return _currentLayout;
}
}
}

View File

@ -246,7 +246,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
}
private function focusWindow(e:ShortcutEvent):void{
focusManager.setFocus(titleBarOverlay);
if (this.visible) {
focusManager.setFocus(titleBarOverlay);
}
}
private function resizeHandler():void {

View File

@ -20,10 +20,11 @@ package org.bigbluebutton.modules.users.services
{
import com.asfusion.mate.events.Dispatcher;
import flash.utils.setTimeout;
import org.as3commons.lang.StringUtils;
import org.as3commons.logging.api.ILogger;
import org.as3commons.logging.api.getClassLogger;
import org.bigbluebutton.common.Role;
import org.bigbluebutton.core.BBB;
import org.bigbluebutton.core.EventConstants;
import org.bigbluebutton.core.UsersUtil;
@ -649,11 +650,17 @@ package org.bigbluebutton.modules.users.services
private function handleBreakoutRoomJoinURL(msg:Object):void{
var map:Object = JSON.parse(msg.msg);
var externalMeetingId : String = StringUtils.substringBetween(map.redirectJoinURL, "meetingID=", "&");
var breakoutRoom : BreakoutRoom = UserManager.getInstance().getConference().getBreakoutRoomByExternalId(externalMeetingId);
var sequence : int = breakoutRoom.sequence;
var event : BreakoutRoomEvent = new BreakoutRoomEvent(BreakoutRoomEvent.BREAKOUT_JOIN_URL);
event.joinURL = map.redirectJoinURL;
var externalMeetingId : String = StringUtils.substringBetween(event.joinURL, "meetingID=", "&");
event.breakoutMeetingSequence = UserManager.getInstance().getConference().getBreakoutRoomByExternalId(externalMeetingId).sequence;
event.breakoutMeetingSequence = sequence;
dispatcher.dispatchEvent(event);
// We delay assigning last room invitation sequence to be sure it is handle in time by the item renderer
setTimeout(function() : void {UserManager.getInstance().getConference().setLastBreakoutRoomInvitation(sequence)}, 1000);
}
private function handleUpdateBreakoutUsers(msg:Object):void{

View File

@ -155,7 +155,7 @@
var ls:LockSettingsVO = UserManager.getInstance().getConference().getLockSettings();
if (data != null) {
kickUserBtn.visible = !data.me && rolledOver && options.allowKickUser;
kickUserBtn.visible = !data.me && rolledOver && options.allowKickUser && !UserManager.getInstance().getConference().isBreakout;
if (!data.voiceJoined) {
if (data.listenOnly) {

View File

@ -7,21 +7,26 @@
<mx:Script>
<![CDATA[
import com.asfusion.mate.events.Dispatcher;
import mx.events.FlexEvent;
import org.bigbluebutton.common.Images;
import org.bigbluebutton.core.managers.UserManager;
import org.bigbluebutton.main.events.BreakoutRoomEvent;
import org.bigbluebutton.main.model.users.BreakoutRoom;
import org.bigbluebutton.util.i18n.ResourceUtil;
private var globalDispatch:Dispatcher = new Dispatcher();
[Bindable]
private var images:Images = new Images();
[Bindable]
private var moderator:Boolean = false;
protected function onCreationCompleteHandler(event:FlexEvent):void {
moderator = UserManager.getInstance().getConference().amIModerator();
this.addEventListener(FlexEvent.DATA_CHANGE, dataChangeHandler);
}
@ -51,13 +56,19 @@
]]>
</mx:Script>
<mx:Button id="joinBtn" width="20" height="20"
includeInLayout="{UserManager.getInstance().getConference().breakoutRoomsReady}" visible="{joinBtn.includeInLayout}"
icon="{images.join}" toolTip="{ResourceUtil.getInstance().getString('bbb.users.roomsGrid.join')}"
click="requestBreakoutJoinUrl(event)"/>
<mx:Button id="listenBtn" toggle="true"
width="20" height="20"
visible="{data.listenStatus != BreakoutRoom.OTHER &amp;&amp; UserManager.getInstance().getConference().voiceJoined || data.listenStatus == BreakoutRoom.SELF}" includeInLayout="{listenBtn.visible}"
icon="{images.transfer}" toolTip="{ResourceUtil.getInstance().getString('bbb.users.roomsGrid.transfer')}"
click="listenToBreakoutRoom(event)"/>
<mx:Button id="joinBtn"
width="20"
height="20"
icon="{images.join}"
visible="{(UserManager.getInstance().getConference().breakoutRoomsReady &amp;&amp; moderator) || (!moderator &amp;&amp; data.invitedRecently)}"
toolTip="{ResourceUtil.getInstance().getString('bbb.users.roomsGrid.join')}"
click="requestBreakoutJoinUrl(event)" />
<mx:Button id="listenBtn"
toggle="true"
width="20"
height="20"
visible="{moderator &amp;&amp; data.listenStatus != BreakoutRoom.OTHER &amp;&amp; UserManager.getInstance().getConference().voiceJoined || data.listenStatus == BreakoutRoom.SELF}"
icon="{images.transfer}"
toolTip="{ResourceUtil.getInstance().getString('bbb.users.roomsGrid.transfer')}"
click="listenToBreakoutRoom(event)" />
</mx:HBox>

View File

@ -128,7 +128,6 @@
private var joinAlert : Alert;
private const FOCUS_USERS_LIST:String = "Focus Users List";
private const MAKE_PRESENTER:String = "Make Presenter";
private const KICK_USER:String = "Kick User";
private const MUTE_USER:String = "Mute User";
@ -440,9 +439,10 @@
private function loadKeyCombos(modifier:String):void {
keyCombos = new Object(); // always start with a fresh array bbb.shortcutkey.users.muteall
keyCombos[modifier + (ResourceUtil.getInstance().getString('bbb.shortcutkey.users.focusUsers') as String)] = FOCUS_USERS_LIST;
keyCombos[modifier + (ResourceUtil.getInstance().getString('bbb.shortcutkey.users.makePresenter') as String)] = MAKE_PRESENTER;
keyCombos[modifier + (ResourceUtil.getInstance().getString('bbb.shortcutkey.users.kick') as String)] = KICK_USER;
if (!UserManager.getInstance().getConference().isBreakout) {
keyCombos[modifier + (ResourceUtil.getInstance().getString('bbb.shortcutkey.users.kick') as String)] = KICK_USER;
}
keyCombos[modifier + (ResourceUtil.getInstance().getString('bbb.shortcutkey.users.mute') as String)] = MUTE_USER;
keyCombos[modifier + (ResourceUtil.getInstance().getString('bbb.shortcutkey.users.muteall') as String)] = MUTE_ALL_USER;
keyCombos[modifier + (ResourceUtil.getInstance().getString('bbb.shortcutkey.users.focusBreakoutRooms') as String)] = FOCUS_BREAKOUT_ROOMS_LIST;
@ -459,9 +459,6 @@
var keyPress:String = KeyboardUtil.buildPressedKeys(e);
if (keyCombos[keyPress]) {
switch (keyCombos[keyPress]) {
case FOCUS_USERS_LIST:
remoteFocusUsers();
break;
case MAKE_PRESENTER:
remoteMakePresenter();
break;
@ -498,7 +495,9 @@
}
private function focusWindow(e:ShortcutEvent):void {
focusManager.setFocus(titleBarOverlay);
if (this.visible) {
focusManager.setFocus(titleBarOverlay);
}
}
public function remoteRaiseHand(e:ShortcutEvent):void{
@ -536,7 +535,7 @@
}
public function remoteKickUser():void {
if (amIModerator && usersGrid.selectedIndex != -1 && partOptions.allowKickUser) {
if (amIModerator && usersGrid.selectedIndex != -1 && partOptions.allowKickUser && !UserManager.getInstance().getConference().isBreakout) {
var selData:Object = usersGrid.selectedItem;
if (!selData.me)
@ -557,13 +556,8 @@
}
}
public function remoteFocusUsers():void {
focusManager.setFocus(usersGrid);
usersGrid.drawFocus(true);
}
public function remoteFocusBreakoutRooms() : void {
if (roomsGrid && roomsGrid.visible) {
if (roomsGrid && roomsBox.visible) {
focusManager.setFocus(roomsGrid);
roomsGrid.drawFocus(true);
}
@ -636,8 +630,8 @@
</views:BBBDataGrid>
<mx:VBox id="roomsBox" styleName="breakoutRoomsBox"
visible="{breakoutRoomsList.length > 0 &amp;&amp; amIModerator}"
includeInLayout="{breakoutRoomsList.length > 0 &amp;&amp; amIModerator}"
visible="{breakoutRoomsList.length > 0}"
includeInLayout="{breakoutRoomsList.length > 0}"
horizontalScrollPolicy="off"
width="100%" height="180">
<mx:HBox width="100%">
@ -658,7 +652,6 @@
showDataTips="true"
headerText="{ResourceUtil.getInstance().getString('bbb.users.roomsGrid.users')}"/>
<mx:DataGridColumn dataField="meetingId"
visible="{amIModerator}"
headerText="{ResourceUtil.getInstance().getString('bbb.users.roomsGrid.action')}"
itemRenderer="org.bigbluebutton.modules.users.views.RoomActionsRenderer"/>
</views:columns>

View File

@ -1,260 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
You should have received a copy of the GNU Lesser General Public License along
with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
-->
<pubVid:VideoWindowItf
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns:pubVid="org.bigbluebutton.modules.videoconf.business.*"
implements="org.bigbluebutton.common.IBbbModuleWindow"
styleNameFocus="videoAvatarStyleFocus"
styleNameNoFocus="videoAvatarStyleNoFocus"
creationComplete="onCreationComplete()"
width="{defaultWidth + 6}" height="{defaultHeight + 6}"
xmlns:mate="http://mate.asfusion.com/"
resize="onResize()"
horizontalScrollPolicy="off"
verticalScrollPolicy="off"
layout="absolute">
<mate:Listener type="{BBBEvent.USER_VOICE_MUTED}" method="handleUserVoiceMutedEvent" />
<mate:Listener type="{EventConstants.USER_TALKING}" method="handleUserTalkingEvent" />
<mate:Listener type="{SwitchedPresenterEvent.SWITCHED_PRESENTER}" method="handleSwitchedPresenterEvent" />
<mate:Listener type="{MadePresenterEvent.SWITCH_TO_PRESENTER_MODE}" method="handleMadePresenterEvent" />
<mate:Listener type="{BBBEvent.USER_VOICE_JOINED}" method="handleNewRoleEvent" />
<mate:Listener type="{BBBEvent.USER_VOICE_LEFT}" method="handleNewRoleEvent" />
<mate:Listener type="{CloseAllWindowsEvent.CLOSE_ALL_WINDOWS}" method="closeWindow" />
<mx:Script>
<![CDATA[
import org.as3commons.logging.api.getClassLogger;
import org.as3commons.logging.api.ILogger;
import org.bigbluebutton.modules.users.views.UsersWindow;
import flexlib.mdi.events.MDIWindowEvent;
import mx.core.UIComponent;
import mx.events.ResizeEvent;
import org.bigbluebutton.common.Images;
import org.bigbluebutton.common.Role;
import org.bigbluebutton.common.events.CloseWindowEvent;
import org.bigbluebutton.common.events.LocaleChangeEvent;
import org.bigbluebutton.core.EventConstants;
import org.bigbluebutton.core.UsersUtil;
import org.bigbluebutton.core.events.CoreEvent;
import org.bigbluebutton.core.events.SwitchedLayoutEvent;
import org.bigbluebutton.core.managers.UserManager;
import org.bigbluebutton.main.events.BBBEvent;
import org.bigbluebutton.main.events.MadePresenterEvent;
import org.bigbluebutton.main.events.SwitchedPresenterEvent;
import org.bigbluebutton.main.views.MainCanvas;
import org.bigbluebutton.modules.videoconf.business.TalkingButtonOverlay;
import org.bigbluebutton.modules.videoconf.events.CloseAllWindowsEvent;
import org.bigbluebutton.modules.videoconf.events.OpenVideoWindowEvent;
import org.bigbluebutton.modules.videoconf.events.StartBroadcastEvent;
import org.bigbluebutton.modules.videoconf.model.VideoConfOptions;
import org.bigbluebutton.util.i18n.ResourceUtil;
private static const LOGGER:ILogger = getClassLogger(AvatarWindow);
[Bindable] private var defaultWidth:Number = 320;
[Bindable] private var defaultHeight:Number = 240;
[Bindable] public var glowColor:String = "";
[Bindable] public var glowBlurSize:Number = 0;
private var avatarWidth:Number = 320;
private var avatarHeight:Number = 240;
private var videoconfOptions:VideoConfOptions = new VideoConfOptions();
private var windowType:String = "AvatarWindowType";
override public function getWindowType():String {
return windowType;
}
private var loader:Loader;
private var request:URLRequest;
private function loadAvatar():void {
request = new URLRequest(UsersUtil.getAvatarURL());
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoadingComplete);
loader.load(request);
}
private function onLoadingComplete(event:Event):void {
// Save the size of the avatar image.
avatarWidth = loader.content.width;
avatarHeight = loader.content.height;
onResize();
}
private function onCreationComplete():void {
this.glowColor = videoconfOptions.glowColor;
this.glowBlurSize = videoconfOptions.glowBlurSize;
loader = new Loader();
_videoHolder = new UIComponent();
_videoHolder.width = avatarWidth;
_videoHolder.height = avatarHeight;
this.addChild(_videoHolder);
_videoHolder.addChild(loader);
_video = new Video();
_video.width = avatarWidth;
_video.height = avatarHeight;
this.minWidth = _minWidth;
this.minHeight = _minHeight;
maximizeRestoreBtn.visible = false;
this.showCloseButton = videoconfOptions.showCloseButton;
this.resizable = false;
setAspectRatio(avatarWidth, avatarHeight);
addEventListener(MDIWindowEvent.RESIZE_START, onResizeStart);
addEventListener(MDIWindowEvent.RESIZE_END, onResizeEnd);
addEventListener(MouseEvent.MOUSE_OVER, showButtons);
addEventListener(MouseEvent.MOUSE_OUT, hideButtons);
startPublishing();
loadAvatar();
}
override protected function onResize():void {
super.onResize();
if (loader != null && _videoHolder != null) {
if (avatarIsSmallerThanWindow()) {
// The avatar image is smaller than the window. Just center the image.
_video.width = loader.width = avatarWidth;
_video.height = loader.height = avatarHeight;
} else {
// The avatar is bigger than the window. Fit into the window maintaining aspect ratio.
fitAvatarToWindow();
}
_video.x = loader.x = (this.width - PADDING_HORIZONTAL - loader.width) / 2;
_video.y = loader.y = (this.height - PADDING_VERTICAL - loader.height) / 2;
}
}
private function fitAvatarToWindow():void {
if (_videoHolder.width - PADDING_HORIZONTAL < _videoHolder.height - PADDING_VERTICAL) {
fitToHeightAndAdjustWidthToMaintainAspectRatio();
} else {
fitToWidthAndAdjustHeightToMaintainAspectRatio();
}
}
private function avatarIsSmallerThanWindow():Boolean {
return (avatarWidth < _videoHolder.width - PADDING_HORIZONTAL) && (avatarHeight < _videoHolder.height - PADDING_VERTICAL);
}
private function fitToWidthAndAdjustHeightToMaintainAspectRatio():void {
var aspect:Number = avatarHeight / avatarWidth;
_video.width = loader.width = _videoHolder.width - PADDING_HORIZONTAL;
// Maintain aspect-ratio
_video.height = loader.height = loader.width * aspect;
}
private function fitToHeightAndAdjustWidthToMaintainAspectRatio():void {
var aspect:Number = avatarWidth / avatarHeight;
_video.height = loader.height = _videoHolder.height - PADDING_VERTICAL;
// Maintain aspect-ratio
_video.width = loader.width = loader.height * aspect;
}
private function handleMadePresenterEvent(event:MadePresenterEvent):void {
LOGGER.debug("******** Avatar: HandleMadePresenter event *********");
updateControlButtons();
}
private function handleSwitchedPresenterEvent(event:SwitchedPresenterEvent):void {
LOGGER.debug("******** Avatar: handleSwitchedPresenterEvent event *********");
updateControlButtons();
}
private function handleNewRoleEvent(event:Event):void {
updateControlButtons();
}
private function handleUserVoiceMutedEvent(event:BBBEvent):void {
if (event.payload.userID == userID) {
userMuted(event.payload.muted);
}
}
private function handleUserTalkingEvent(event:CoreEvent):void {
if (event.message.userID == userID) {
if (event.message.talking) {
notTalkingEffect.end();
talkingEffect.play([this]);
simulateClick();
} else {
talkingEffect.end();
notTalkingEffect.play([this]);
}
}
}
private function startPublishing():void{
this.resizable = true;
onResize();
createButtons();
addControlButtons();
updateButtonsPosition();
}
override public function close(event:MouseEvent=null):void{
closeThisWindow();
super.close(event);
}
private function closeWindow(e:CloseAllWindowsEvent):void{
closeThisWindow();
}
private function closeThisWindow():void {
LOGGER.debug("* Closing avatar window for user [{0}] *", [userID]);
}
]]>
</mx:Script>
<mx:Glow id="talkingEffect" duration="500" alphaFrom="1.0" alphaTo="0.3"
blurXFrom="0.0" blurXTo="{glowBlurSize}" blurYFrom="0.0" blurYTo="{glowBlurSize}" color="{glowColor}"/>
<mx:Glow id="notTalkingEffect" duration="500" alphaFrom="0.3" alphaTo="1.0"
blurXFrom="{glowBlurSize}" blurXTo="0.0" blurYFrom="{glowBlurSize}" blurYTo="0.0" color="{glowColor}"/>
</pubVid:VideoWindowItf>

View File

@ -33,7 +33,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
creationComplete="onCreationComplete()">
<mate:Listener type="{ShortcutEvent.FOCUS_VIDEO_WINDOW}" method="focusWindow" />
<mate:Listener type="{SwitchedLayoutEvent.SWITCHED_LAYOUT_EVENT}" method="onLayoutChanged" />
<mx:Script>
<![CDATA[
import com.asfusion.mate.events.Dispatcher;
@ -41,6 +42,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
import mx.core.UIComponent;
import org.bigbluebutton.core.KeyboardUtil;
import org.bigbluebutton.core.events.SwitchedLayoutEvent;
import org.bigbluebutton.main.events.ShortcutEvent;
import org.bigbluebutton.main.views.MainCanvas;
import org.bigbluebutton.modules.videoconf.model.VideoConfOptions;
@ -50,6 +52,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
private var keyCombos:Object;
private var disp:Dispatcher = new Dispatcher();
private var darkMode:Boolean;
private function onCreationComplete():void {
hotkeyCapture();
titleBarOverlay.tabIndex = videoOptions.baseTabIndex;
@ -89,7 +93,11 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
override protected function resourcesChanged():void {
super.resourcesChanged();
this.title = ResourceUtil.getInstance().getString("bbb.videodock.title");
if (!darkMode) {
this.title = ResourceUtil.getInstance().getString("bbb.videodock.title");
} else {
this.title = "";
}
if (titleBarOverlay != null) {
titleBarOverlay.accessibilityName = ResourceUtil.getInstance().getString('bbb.videoDock.titleBar');
@ -112,7 +120,24 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
}
private function focusWindow(e:ShortcutEvent):void {
focusManager.setFocus(titleBarOverlay);
if (this.visible) {
focusManager.setFocus(titleBarOverlay);
}
}
private function onLayoutChanged(e:SwitchedLayoutEvent):void {
if(e.layoutID != "bbb.layout.name.videochat"){
setStyle("styleNameFocus", "videoDockStyleFocus");
setStyle("styleNameNoFocus", "videoDockStyleNoFocus");
showControls = true;
this.title = ResourceUtil.getInstance().getString("bbb.videodock.title");
} else {
setStyle("styleNameFocus", "videoDockStyleFocusChatLayout");
setStyle("styleNameNoFocus", "videoDockStyleNoFocusChatLayout");
showControls = false;
this.title = "";
}
styleChanged("styleName");
}
private function remoteMinimize(e:ShortcutEvent):void {

View File

@ -9,7 +9,6 @@ arunoda:npm@0.2.6
amplify
blaze@2.1.8
francocatena:status
mrt:external-file-loader@0.1.4
mizzao:timesync
clinical:nightwatch
cfs:power-queue

View File

@ -56,7 +56,6 @@ modules@0.7.9
modules-runtime@0.7.9
mongo@1.1.15
mongo-id@1.0.6
mrt:external-file-loader@0.1.4
nathantreid:css-modules@2.4.0
npm-mongo@2.2.16_1
observe-sequence@1.0.15

View File

@ -1,43 +0,0 @@
html {
box-sizing: border-box;
}
*, *:before, *:after {
box-sizing: inherit;
}
body {
font-family: 'Source Sans Pro', Arial, sans-serif;
font-size: 1rem; /* 16px */
}
a {
color: inherit;
}
#app {
height: 100vh;
width: 100vw;
overflow: hidden;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
border: 0;
}
[hidden]:not([hidden="false"]) {
display: none !important;
}
/* DEBUG ONLY
* {
background-color: rgba(0, 0, 0, .025) !important;
}
*/

View File

@ -1,7 +1,55 @@
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BBB - HTML5 Client</title>
<style>
html {
box-sizing: border-box;
}
*, *:before, *:after {
box-sizing: inherit;
}
body {
font-family: 'Source Sans Pro', Arial, sans-serif;
font-size: 1rem; /* 16px */
background-color: #2A2D33;
}
a {
color: inherit;
}
#app {
height: 100vh;
width: 100vw;
overflow: hidden;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
border: 0;
}
[hidden]:not([hidden="false"]) {
display: none !important;
}
</style>
</head>
<body>
<body style="background-color: #2A2D33">
<div id="app" role="application"></div>
<script src="/client/lib/sip.js"></script>
<script src="/client/lib/bbb_webrtc_bridge_sip.js"></script>
<script src="/client/lib/bbblogger.js"></script>
<script src="/client/lib/jquery.json-2.4.min.js"></script>
<script src="/client/lib/jquery.FSRTC.js"></script>
<script src="/client/lib/jquery.verto.js"></script>
<script src="/client/lib/verto_extension.js"></script>
<script src="/client/lib/jquery.jsonrpcclient.js"></script>
</body>

View File

@ -1,90 +1,9 @@
import React from 'react';
import { Meteor } from 'meteor/meteor';
import { render } from 'react-dom';
import { showModal } from '/imports/ui/components/app/service';
import { renderRoutes } from '../imports/startup/client/routes.js';
import { IntlProvider } from 'react-intl';
import Singleton from '/imports/ui/services/storage/local.js';
import AudioModalContainer from '/imports/ui/components/audio-modal/container';
function loadUserSettings() {
const userSavedFontSize = Singleton.getItem('bbbSavedFontSizePixels');
if (userSavedFontSize) {
document.getElementsByTagName('html')[0].style.fontSize = userSavedFontSize;
}
}
function setAudio() {
const LOG_CONFIG = Meteor.settings || {};
let autoJoinAudio = LOG_CONFIG.public.app.autoJoinAudio;
if (autoJoinAudio) {
showModal( <AudioModalContainer /> );
}
}
function setMessages(data) {
let messages = data;
let defaultLocale = 'en';
render((
<IntlProvider locale={defaultLocale} messages={messages}>
{renderRoutes()}
</IntlProvider>
), document.getElementById('app'));
setAudio();
}
// Helper to load javascript libraries from the BBB server
function loadLib(libname, success, fail) {
const successCallback = function (cb) {
console.log(`successfully loaded lib - ${this}`);
if (typeof (cb) == 'function' || cb instanceof Function) {
cb();
}
};
const failCallback = function (cb, issue) {
console.error(`failed to load lib - ${this}`);
console.error(issue);
if (typeof (cb) == 'function' || cb instanceof Function) {
cb();
}
};
return Meteor.Loader.loadJs(`${window.location.origin}/client/lib/${libname}`,
successCallback.bind(libname, success), 10000).fail(failCallback.bind(libname, fail));
};
import { renderRoutes } from '/imports/startup/client/routes';
Meteor.startup(() => {
loadLib('sip.js');
loadLib('bbb_webrtc_bridge_sip.js');
loadLib('bbblogger.js');
loadLib('jquery.json-2.4.min.js');
loadLib('jquery.FSRTC.js');
loadLib('jquery.verto.js');
loadLib('verto_extension.js');
loadLib('jquery.jsonrpcclient.js');
loadUserSettings();
let browserLanguage = navigator.language; //set this manually to force localization in a specific language
let request = new Request
(`${window.location.origin}/html5client/locale?locale=${browserLanguage}`);
fetch(request, { method: 'GET' })
.then(function (response) {
return response.json();
})
.then(function (data) {
setMessages(data);
})
.catch(function error(err) {
console.log('request failed', err);
});
render(renderRoutes(), document.getElementById('app'));
});

View File

@ -1,9 +1,7 @@
import Breakouts from '/imports/api/breakouts';
import Users from '/imports/api/users';
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import RedisPubSub from '/imports/startup/server/redis';
import { XMLHttpRequest } from 'xmlhttprequest';
import xml2js from 'xml2js';
import url from 'url';
const xmlParser = new xml2js.Parser();

View File

@ -1,4 +1,4 @@
import _ from 'underscore';
import _ from 'lodash';
import Captions from '/imports/api/captions';
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';

View File

@ -8,11 +8,11 @@ import addChat from '/imports/api/chat/server/modifiers/addChat';
export default function handleValidateAuthToken({ payload }) {
const meetingId = payload.meeting_id;
const userId = payload.userid;
const validStatus = payload.valid;
const validStatus = JSON.parse(payload.valid);
check(meetingId, String);
check(userId, String);
check(validStatus, String);
check(validStatus, Boolean);
const selector = {
meetingId,
@ -21,14 +21,11 @@ export default function handleValidateAuthToken({ payload }) {
const User = Users.findOne(selector);
if (!User) {
throw new Meteor.Error(
'user-not-found', `You need a valid user to be able validate the token`);
}
// If we dont find the user on our collection is a flash user and we can skip
if (!User) return;
if (User.validated === validStatus) {
return;
}
// User already flagged so we skip
if (User.validated === validStatus) return;
const modifier = {
$set: {

View File

@ -5,6 +5,7 @@ import userLogout from './methods/userLogout';
import assignPresenter from './methods/assignPresenter';
import muteToggle from './methods/muteToggle';
import setEmojiStatus from './methods/setEmojiStatus';
import validateAuthToken from './methods/validateAuthToken';
Meteor.methods({
kickUser,
@ -12,6 +13,7 @@ Meteor.methods({
userLogout,
assignPresenter,
setEmojiStatus,
validateAuthToken,
muteUser: (...args) => muteToggle(...args, true),
unmuteUser: (...args) => muteToggle(...args, false),
});

View File

@ -21,11 +21,12 @@ export default function userLeaving(credentials, userId) {
check(requesterUserId, String);
check(userId, String);
const User = Users.findOne({
const selector = {
meetingId,
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`);
@ -39,6 +40,26 @@ export default function userLeaving(credentials, userId) {
listenOnlyToggle(credentials, false);
}
if (User.validated) {
const modifier = {
$set: {
validated: false,
},
};
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Invalidating user: ${err}`);
}
if (numChanged) {
return Logger.info(`Invalidate user=${userId} meeting=${meetingId}`);
}
};
return Users.update(selector, modifier, cb);
}
let payload = {
meeting_id: meetingId,
userid: userId,

View File

@ -42,7 +42,7 @@ export default function validateAuthToken(credentials) {
reply_to: `${meetingId}/${requesterUserId}`,
};
Logger.verbose(`User '${requesterUserId}' is trying to validate auth token for meeting '${meetingId}'`);
Logger.info(`User '${requesterUserId}' is trying to validate auth token for meeting '${meetingId}'`);
return RedisPubSub.publish(CHANNEL, EVENT_NAME, payload, header);
};

View File

@ -18,7 +18,7 @@ export default function createDummyUser(meetingId, userId, authToken) {
userId,
authToken,
clientType: 'HTML5',
validated: false,
validated: null,
};
const cb = (err, numChanged) => {

View File

@ -5,7 +5,28 @@ import Logger from '/imports/startup/server/logger';
import { isAllowedTo } from '/imports/startup/server/userPermissions';
import userLeaving from './methods/userLeaving';
import validateAuthToken from './methods/validateAuthToken';
Meteor.publish('current-user', function (credentials) {
const { meetingId, requesterUserId, requesterToken } = credentials;
check(meetingId, String);
check(requesterUserId, String);
check(requesterToken, String);
const selector = {
meetingId,
userId: requesterUserId,
authToken: requesterToken,
};
const options = {
fields: {
user: false,
},
};
return Users.find(selector, options);
});
Meteor.publish('users', function (credentials) {
const { meetingId, requesterUserId, requesterToken } = credentials;
@ -14,12 +35,9 @@ Meteor.publish('users', function (credentials) {
check(requesterUserId, String);
check(requesterToken, String);
validateAuthToken(credentials);
// TODO(auth): We need to fix the Authentication flow to enable ACL
// if (!isAllowedTo('subscribeUsers', credentials)) {
// this.error(new Meteor.Error(402, "The user was not authorized to subscribe for 'Users'"));
// }
if (!isAllowedTo('subscribeUsers', credentials)) {
this.error(new Meteor.Error(402, "The user was not authorized to subscribe for 'Users'"));
}
this.onStop(() => {
userLeaving(credentials, requesterUserId);

View File

@ -1,9 +0,0 @@
import en from './en.json';
import de from './de.json';
import ptBR from './pt-BR.json';
export default {
en: en,
de: de,
'pt-BR': ptBR,
};

View File

@ -1,17 +0,0 @@
{
"app.home.greeting": "Bem-vindo {name}! Sua aprensentação começará em breve...",
"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.titlePublic": "Conversa Publíca",
"app.chat.titlePrivate": "Conversa Privada com {name}",
"app.chat.partnerDisconnected": "{name} saiu da sala",
"app.chat.moreMessages": "Mais mensagens abaixo",
"app.kickMessage": "Você foi expulso da apresentação",
"app.failedMessage": "Desculpas, estamos com problemas para se conectar ao servidor.",
"app.connectingMessage": "Conectando...",
"app.waitingMessage": "Desconectado. Tentando reconectar em {seconds} segundos..."
}

View File

@ -0,0 +1,48 @@
import Auth from '/imports/ui/services/auth';
export function joinRouteHandler(nextState, replace, callback) {
if (!nextState || !nextState.params.authToken) {
replace({ pathname: '/error/404' });
callback();
}
const { meetingID, userID, authToken } = nextState.params;
Auth.authenticate(meetingID, userID, authToken)
.then(() => {
replace({ pathname: '/' });
callback();
})
.catch(() => {
replace({ pathname: '/error/401' });
callback();
});
};
export function logoutRouteHandler(nextState, replace, callback) {
const { meetingID, userID, authToken } = nextState.params;
Auth.logout()
.then(logoutURL => {
window.location = logoutURL || window.location.origin;
callback();
})
.catch(reason => {
console.error(reason);
replace({ pathname: '/error/500' });
callback();
});
};
export function authenticatedRouteHandler(nextState, replace, callback) {
if (Auth.loggedIn) {
callback();
}
Auth.authenticate()
.then(callback)
.catch(reason => {
console.error(reason);
replace({ pathname: '/error/401' });
callback();
});
};

View File

@ -0,0 +1,91 @@
import React, { Component } from 'react';
import { createContainer } from 'meteor/react-meteor-data';
import IntlStartup from './intl';
import Auth from '/imports/ui/services/auth';
import AppContainer from '/imports/ui/components/app/container';
import ErrorScreen from '/imports/ui/components/error-screen/component';
import LoadingScreen from '/imports/ui/components/loading-screen/component';
const BROWSER_LANGUAGE = window.navigator.userLanguage || window.navigator.language;
class Base extends Component {
constructor(props) {
super(props);
this.state = {
loading: false,
error: props.error || null,
};
this.updateLoadingState = this.updateLoadingState.bind(this);
this.updateErrorState = this.updateErrorState.bind(this);
}
updateLoadingState(loading = false) {
this.setState({
loading,
});
}
updateErrorState(error = null) {
this.setState({
error,
});
}
renderByState() {
const { updateLoadingState, updateErrorState } = this;
const stateControls = { updateLoadingState, updateErrorState };
const { loading, error } = this.state;
const { subscriptionsReady, errorCode } = this.props;
if (error || errorCode) {
return (<ErrorScreen code={errorCode}>{error}</ErrorScreen>);
}
if (loading || !subscriptionsReady) {
return (<LoadingScreen>{loading}</LoadingScreen>);
}
return (<AppContainer {...this.props} baseControls={stateControls}/>);
}
render() {
const { updateLoadingState, updateErrorState } = this;
const stateControls = { updateLoadingState, updateErrorState };
return (
<IntlStartup locale={BROWSER_LANGUAGE} baseControls={stateControls}>
{this.renderByState()}
</IntlStartup>
);
}
};
const SUBSCRIPTIONS_NAME = [
'users', 'chat', 'cursor', 'deskshare', 'meetings',
'polls', 'presentations', 'shapes', 'slides', 'captions', 'breakouts',
];
export default BaseContainer = createContainer(({ params }) => {
if (params.errorCode) return params;
if (!Auth.loggedIn) {
return {
errorCode: 401,
error: 'You are unauthorized to access this meeting',
};
}
const credentials = Auth.credentials;
const subscriptionsHandlers = SUBSCRIPTIONS_NAME.map(name => Meteor.subscribe(name, credentials));
return {
subscriptionsReady: subscriptionsHandlers.every(handler => handler.ready()),
};
}, Base);

View File

@ -0,0 +1,64 @@
import React, { Component, PropTypes } from 'react';
import { IntlProvider } from 'react-intl';
const propTypes = {
locale: PropTypes.string.isRequired,
};
const defaultProps = {
locale: 'en',
};
class IntlStartup extends Component {
constructor(props) {
super(props);
this.state = {
messages: {},
};
this.fetchLocalizedMessages = this.fetchLocalizedMessages.bind(this);
}
fetchLocalizedMessages(locale) {
const url = `/html5client/locale?locale=${locale}`;
const { baseControls } = this.props;
baseControls.updateLoadingState(true);
fetch(url)
.then(response => response.json())
.then(messages => {
this.setState({ messages }, () => {
baseControls.updateLoadingState(false);
});
})
.catch(reason => {
baseControls.updateErrorState(reason);
baseControls.updateLoadingState(false);
});
}
componentWillMount() {
this.fetchLocalizedMessages(this.props.locale);
}
componentWillUpdate(nextProps, nextState) {
if (this.props.locale !== nextProps.locale) {
this.fetchLocalizedMessages(nextProps.locale);
}
}
render() {
return (
<IntlProvider locale={this.props.locale} messages={this.state.messages}>
{this.props.children}
</IntlProvider>
);
}
};
export default IntlStartup;
IntlStartup.propTypes = propTypes;
IntlStartup.defaultProps = defaultProps;

View File

@ -2,45 +2,32 @@ import React from 'react';
import { Router, Route, Redirect, IndexRoute, useRouterHistory } from 'react-router';
import { createHistory } from 'history';
// route components
import AppContainer from '/imports/ui/components/app/container';
import { subscribeToCollections, setCredentials } from '/imports/ui/components/app/service';
import { joinRouteHandler, logoutRouteHandler, authenticatedRouteHandler } from './auth';
import Base from './base';
import LoadingScreen from '/imports/ui/components/loading-screen/component';
import ChatContainer from '/imports/ui/components/chat/container';
import UserListContainer from '/imports/ui/components/user-list/container';
const browserHistory = useRouterHistory(createHistory)({
// Name displayed in the brower URL
basename: Meteor.settings.public.app.basename,
});
export const renderRoutes = () => (
<Router history={browserHistory}>
<Route path="/join/:meetingID/:userID/:authToken" onEnter={setCredentials} />
<Route path="/" onEnter={() => {
subscribeToCollections();
}}
getComponent={(nextState, cb) => {
subscribeToCollections(() => cb(null, AppContainer));
}}>
<Route path="/logout" onEnter={logoutRouteHandler} />
<Route path="/join/:meetingID/:userID/:authToken"
component={LoadingScreen} onEnter={joinRouteHandler} />
<Route path="/" component={Base} onEnter={authenticatedRouteHandler} >
<IndexRoute components={{}} />
<Route name="users" path="users" getComponents={(nextState, cb) => {
subscribeToCollections(() => cb(null, {
userList: UserListContainer,
}));
<Route name="users" path="users" components={{ userList: UserListContainer }} />
<Route name="chat" path="users/chat/:chatID" components={{
userList: UserListContainer,
chat: ChatContainer,
}} />
<Route name="chat" path="users/chat/:chatID" getComponents={(nextState, cb) => {
subscribeToCollections(() => cb(null, {
userList: UserListContainer,
chat: ChatContainer,
}));
}} />
<Redirect from="users/chat" to="/users/chat/public" />
</Route>
<Redirect from="*" to="/" />
<Route name="error" path="/error/:errorCode" component={Base}/>
<Redirect from="*" to="/error/404" />
</Router>
);

View File

@ -1,5 +1,5 @@
import { Meteor } from 'meteor/meteor';
import Locales from '/imports/locales';
import _ from 'lodash';
import Logger from './logger';
import Redis from './redis';
@ -17,22 +17,32 @@ WebApp.connectHandlers.use('/check', (req, res, next) => {
});
WebApp.connectHandlers.use('/locale', (req, res) => {
let defaultLocale = 'en';
let [locale, region] = req.query.locale.split('-');
const APP_CONFIG = Meteor.settings.public.app;
const defaultMessages = Locales[defaultLocale];
let defaultLocale = APP_CONFIG.defaultLocale;
let localeRegion = _.snakeCase(req.query.locale).split('_');
let messages = {};
let messages = Object.assign(
{},
defaultMessages,
Locales[locale],
Locales[`${locale}-${region}`],
);
let locales = [defaultLocale, localeRegion[0]];
if (localeRegion.length > 1) {
locales.push(`${localeRegion[0]}_${localeRegion[1]}`);
}
locales.forEach(locale => {
try {
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
}
});
res.setHeader('Content-Type', 'application/json');
res.writeHead(200);
res.end(JSON.stringify(messages));
});
export const eventEmitter = Redis.emitter;

View File

@ -18,17 +18,17 @@ const intlMessages = defineMessages({
class JoinAudioOptions extends React.Component {
render() {
const {
close,
intl,
isInAudio,
isInListenOnly,
open,
handleJoinAudio,
handleCloseAudio,
} = this.props;
if (isInAudio || isInListenOnly) {
return (
<Button
onClick={close}
onClick={handleCloseAudio}
label={intl.formatMessage(intlMessages.leaveAudio)}
color={'danger'}
icon={'audio_off'}
@ -40,7 +40,7 @@ class JoinAudioOptions extends React.Component {
return (
<Button
onClick={open}
onClick={handleJoinAudio}
label={intl.formatMessage(intlMessages.joinAudio)}
color={'primary'}
icon={'audio_on'}

View File

@ -20,7 +20,7 @@ export default createContainer((params) => {
return {
isInAudio: user.voiceUser.joined,
isInListenOnly: user.listenOnly,
open: params.open,
close: params.close,
handleJoinAudio: params.handleJoinAudio,
handleCloseAudio: params.handleCloseAudio,
};
}, JoinAudioOptionsContainer);

View File

@ -1,15 +1,10 @@
import React, { Component, PropTypes } from 'react';
import { showModal } from '/imports/ui/components/app/service';
import Audio from '/imports/ui/components/audio-modal/component';
import Button from '/imports/ui/components/button/component';
import styles from './styles.scss';
import EmojiContainer from './emoji-menu/container';
import ActionsDropdown from './actions-dropdown/component';
import Auth from '/imports/ui/services/auth/index';
import Users from '/imports/api/users/index';
import JoinAudioOptionsContainer from './audio-menu/container';
import MuteAudioContainer from './mute-button/container';
import { exitAudio } from '/imports/api/phone';
import JoinVideo from './video-button/component';
export default class ActionsBar extends Component {
@ -17,10 +12,6 @@ export default class ActionsBar extends Component {
super(props);
}
openJoinAudio() {
return showModal(<Audio handleJoinListenOnly={this.props.handleJoinListenOnly} />)
}
renderForPresenter() {
return (
<div className={styles.actionsbar}>
@ -30,14 +21,15 @@ export default class ActionsBar extends Component {
<div className={styles.center}>
<MuteAudioContainer />
<JoinAudioOptionsContainer
open={this.openJoinAudio.bind(this)}
close={() => {exitAudio();}}
handleJoinAudio={this.props.handleOpenJoinAudio}
handleCloseAudio={this.props.handleExitAudio}
/>
{/*<JoinVideo />*/}
<EmojiContainer />
</div>
<div className={styles.right}>
<div className={styles.hidden}>
<ActionsDropdown />
</div>
</div>
);
@ -49,15 +41,13 @@ export default class ActionsBar extends Component {
<div className={styles.center}>
<MuteAudioContainer />
<JoinAudioOptionsContainer
open={this.openJoinAudio.bind(this)}
close={() => {exitAudio();}}
handleJoinAudio={this.props.handleOpenJoinAudio}
handleCloseAudio={this.props.handleExitAudio}
/>
{/*<JoinVideo />*/}
<EmojiContainer />
</div>
<div className={styles.right}>
</div>
</div>
);
}

View File

@ -1,8 +1,7 @@
import React, { Component, PropTypes } from 'react';
import React, { Component } from 'react';
import { createContainer } from 'meteor/react-meteor-data';
import ActionsBar from './component';
import Service from './service';
import { joinListenOnly } from '/imports/api/phone';
class ActionsBarContainer extends Component {
constructor(props) {
@ -10,19 +9,23 @@ class ActionsBarContainer extends Component {
}
render() {
const handleJoinListenOnly = () => joinListenOnly();
return (
<ActionsBar
handleJoinListenOnly={handleJoinListenOnly}
{...this.props}>
{this.props.children}
{...this.props}>
{this.props.children}
</ActionsBar>
);
}
}
export default createContainer(() => {
let data = Service.isUserPresenter();
return data;
const isPresenter = Service.isUserPresenter();
const handleExitAudio = () => Service.handleExitAudio();
const handleOpenJoinAudio = () => Service.handleJoinAudio();
return {
isUserPresenter: isPresenter,
handleExitAudio: handleExitAudio,
handleOpenJoinAudio: handleOpenJoinAudio,
};
}, ActionsBarContainer);

View File

@ -5,13 +5,13 @@ import { callServer } from '/imports/ui/services/api/index.js';
let getEmojiData = () => {
// Get userId and meetingId
const credentials = Auth.getCredentials();
const credentials = Auth.credentials;
const { requesterUserId: userId, meetingId } = credentials;
// Find the Emoji Status of this specific meeting and userid
const userEmojiStatus = Users.findOne({
meetingId: meetingId,
userId: userId,
meetingId: Auth.meetingID,
userId: Auth.userID,
}).user.emoji_status;
return {

View File

@ -6,10 +6,13 @@ export default class MuteAudio extends React.Component {
render() {
const { isInAudio, isMuted, callback, isTalking} = this.props;
if (!isInAudio) return null;
let label = !isMuted ? 'Mute' : 'Unmute';
let icon = !isMuted ? 'unmute' : 'mute';
let className = !isInAudio ? styles.invisible : null;
let tabIndex = !isInAudio ? -1 : 0;
let className = null;
if (isInAudio && isTalking) {
className = styles.circleGlow;

View File

@ -1,11 +1,16 @@
import React from 'react';
import AuthSingleton from '/imports/ui/services/auth/index.js';
import Users from '/imports/api/users';
import { joinListenOnly } from '/imports/api/phone';
import { showModal } from '/imports/ui/components/app/service';
import { exitAudio } from '/imports/api/phone';
import Audio from '/imports/ui/components/audio-modal/component';
let isUserPresenter = () => {
// check if user is a presenter
let isPresenter = Users.findOne({
userId: AuthSingleton.getCredentials().requesterUserId,
userId: AuthSingleton.userID,
}).user.presenter;
return {
@ -13,6 +18,17 @@ let isUserPresenter = () => {
};
};
const handleExitAudio = () => {
return exitAudio();
}
const handleJoinAudio = () => {
const handleJoinListenOnly = () => joinListenOnly();
return showModal(<Audio handleJoinListenOnly={handleJoinListenOnly} />);
}
export default {
isUserPresenter,
handleJoinAudio,
handleExitAudio,
};

View File

@ -7,7 +7,8 @@
.left,
.right,
.center {
.center,
.hidden {
display: flex;
flex-direction: row;
justify-content: center;
@ -19,7 +20,8 @@
}
.left,
.right {
.right,
.hidden {
flex: 0;
}
@ -27,7 +29,7 @@
flex: 1;
}
.invisible {
.hidden {
visibility: hidden;
}

View File

@ -1,214 +1,125 @@
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import LoadingScreen from '../loading-screen/component';
import KickedScreen from '../kicked-screen/component';
import _ from 'lodash';
import NotificationsBarContainer from '../notifications-bar/container';
import AudioNotificationContainer from '../audio-notification/container';
import LocalStorage from '/imports/ui/services/storage/local.js';
import ChatNotificationContainer from '../chat/notification/container';
import Button from '../button/component';
import styles from './styles';
import cx from 'classnames';
const propTypes = {
init: PropTypes.func.isRequired,
fontSize: PropTypes.string,
navbar: PropTypes.element,
sidebar: PropTypes.element,
sidebarRight: PropTypes.element,
media: PropTypes.element,
actionsbar: PropTypes.element,
captions: PropTypes.element,
modal: PropTypes.element,
unreadMessageCount: PropTypes.array,
openChats: PropTypes.array,
};
const defaultProps = {
fontSize: '16px',
};
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
compactUserList: false, //TODO: Change this on userlist resize (?)
};
this.setDefaultSettings = props.setDefaultSettings;
props.init.call(this);
}
setHtmlFontSize(size) {
document.getElementsByTagName('html')[0].style.fontSize = size;
};
componentDidMount() {
this.setDefaultSettings();
this.setHtmlFontSize(this.props.fontSize);
document.getElementsByTagName('html')[0].style.fontSize = this.props.fontSize;
}
renderNavBar() {
const { navbar } = this.props;
if (navbar) {
return (
<header className={styles.navbar}>
{navbar}
</header>
);
}
if (!navbar) return null;
return false;
return (
<header className={styles.navbar}>
{navbar}
</header>
);
}
renderSidebar() {
const { sidebar } = this.props;
if (sidebar) {
return (
<aside className={styles.sidebar}>
{sidebar}
</aside>
);
}
if (!sidebar) return null;
return false;
return (
<aside className={styles.sidebar}>
{sidebar}
</aside>
);
}
renderUserList() {
let { userList } = this.props;
const { compactUserList } = this.state;
if (!userList) return;
let userListStyle = {};
userListStyle[styles.compact] = compactUserList;
if (userList) {
userList = React.cloneElement(userList, {
compact: compactUserList,
});
userList = React.cloneElement(userList, {
compact: compactUserList,
});
return (
<nav className={cx(styles.userList, userListStyle)}>
{userList}
</nav>
);
}
return false;
return (
<nav className={cx(styles.userList, userListStyle)}>
{userList}
</nav>
);
}
renderChat() {
const { chat } = this.props;
if (chat) {
return (
<section className={styles.chat} role="log">
{chat}
</section>
);
}
if (!chat) return null;
return false;
return (
<section className={styles.chat} role="log">
{chat}
</section>
);
}
renderMedia() {
const { media } = this.props;
if (media) {
return (
<section className={styles.media}>
{media}
</section>
);
}
if (!media) return null;
return false;
}
renderClosedCaptions() {
const { captions } = this.props;
if (captions && this.props.getCaptionsStatus()) {
return (
<section className={styles.closedCaptions}>
{captions}
</section>
);
}
return (
<section className={styles.media}>
{media}
</section>
);
}
renderActionsBar() {
const { actionsbar } = this.props;
if (actionsbar) {
return (
<section className={styles.actionsbar}>
{actionsbar}
</section>
);
}
if (!actionsbar) return null;
return false;
}
renderAudioElement() {
return (
<audio id="remote-media" autoPlay="autoplay"></audio>
<section className={styles.actionsbar}>
{actionsbar}
</section>
);
}
renderModal() {
const { modal } = this.props;
if (modal) {
return (<div>{modal}</div>);
}
return false;
}
playSoundForUnreadMessages() {
const snd = new Audio('/html5client/resources/sounds/notify.mp3');
snd.play();
}
componentDidUpdate(prevProps) {
let { unreadMessageCount, openChats, openChat } = this.props;
unreadMessageCount.forEach((chat, i) => {
// When starting the new chat, if prevProps is undefined or null, it is assigned 0.
if (!prevProps.unreadMessageCount[i]) {
prevProps.unreadMessageCount[i] = 0;
}
// compare openChats(chatID) to chatID of currently opened chat room
if (openChats[i] !== openChat) {
let shouldPlaySound = this.props.applicationSettings.chatAudioNotifications;
if (shouldPlaySound && chat > prevProps.unreadMessageCount[i]) {
this.playSoundForUnreadMessages();
}
}
});
}
render() {
if (this.props.wasKicked) {
return (
<KickedScreen>
<FormattedMessage
id="app.kickMessage"
description="Message when the user is kicked out of the meeting"
defaultMessage="You have been kicked out of the meeting"
/>
<br/><br/>
<Button
label={'OK'}
onClick={this.props.redirectToLogoutUrl}/>
</KickedScreen>
);
}
if (this.props.isLoading) {
return <LoadingScreen/>;
}
const { modal, params } = this.props;
return (
<main className={styles.main}>
@ -223,13 +134,14 @@ export default class App extends Component {
{this.renderActionsBar()}
</div>
{this.renderSidebar()}
{this.renderClosedCaptions()}
</section>
{this.renderAudioElement()}
{this.renderModal()}
{modal}
<audio id="remote-media" autoPlay="autoplay"></audio>
<ChatNotificationContainer currentChatID={params.chatID} />
</main>
);
}
}
App.propTypes = propTypes;
App.defaultProps = defaultProps;

View File

@ -1,32 +1,42 @@
import React, { Component, PropTypes, cloneElement } from 'react';
import { createContainer } from 'meteor/react-meteor-data';
import App from './component';
import {
subscribeForData,
wasUserKicked,
redirectToLogoutUrl,
getModal,
getCaptionsStatus,
getFontSize,
} from './service';
import { setDefaultSettings, getSettingsFor } from '/imports/ui/components/settings/service';
import { withRouter } from 'react-router';
import { defineMessages, injectIntl } from 'react-intl';
import {
getModal,
showModal,
getFontSize,
getCaptionsStatus,
} from './service';
import { setDefaultSettings } from '../settings/service';
import Auth from '/imports/ui/services/auth';
import Users from '/imports/api/users';
import Breakouts from '/imports/api/breakouts';
import App from './component';
import NavBarContainer from '../nav-bar/container';
import ActionsBarContainer from '../actions-bar/container';
import MediaContainer from '../media/container';
import ClosedCaptionsContainer from '../closed-captions/container';
import UserListService from '../user-list/service';
import Auth from '/imports/ui/services/auth';
import AudioModalContainer from '../audio-modal/container';
import ClosedCaptionsContainer from '/imports/ui/components/closed-captions/container';
const defaultProps = {
navbar: <NavBarContainer />,
actionsbar: <ActionsBarContainer />,
media: <MediaContainer />,
//CCs UI is commented till the next pull request
captions: <ClosedCaptionsContainer />,
};
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',
},
});
class AppContainer extends Component {
render() {
// inject location on the navbar container
@ -38,52 +48,44 @@ class AppContainer extends Component {
</App>
);
}
}
let loading = true;
const loadingDep = new Tracker.Dependency;
const getLoading = () => {
loadingDep.depend();
return loading;
};
const setLoading = (val) => {
if (val !== loading) {
loading = val;
loadingDep.changed();
const APP_CONFIG = Meteor.settings.public.app;
const init = () => {
setDefaultSettings();
if (APP_CONFIG.autoJoinAudio) {
showModal(<AudioModalContainer />);
}
};
const checkUnreadMessages = () => {
return UserListService.getOpenChats().map(chat=> chat.unreadCounter)
.filter(userID => userID !== Auth.userID);
};
export default withRouter(injectIntl(createContainer(({ router, intl, baseControls }) => {
// Check if user is kicked out of the session
Users.find({ userId: Auth.userID }).observeChanges({
removed() {
Auth.clearCredentials()
.then(() => {
router.push('/error/403');
baseControls.updateErrorState(
intl.formatMessage(intlMessages.kickedMessage),
);
});
},
});
const openChats = (chatID) => {
// get currently opened chatID
return UserListService.getOpenChats(chatID).map(chat => chat.id);
}
export default createContainer(({ params }) => {
Promise.all(subscribeForData())
.then(() => {
setLoading(false);
// Close the widow when the current breakout room ends
Breakouts.find({ breakoutMeetingId: Auth.meetingID }).observeChanges({
removed(old) {
Auth.clearCredentials().then(window.close);
},
});
return {
wasKicked: wasUserKicked(),
isLoading: getLoading(),
init,
sidebar: getCaptionsStatus() ? <ClosedCaptionsContainer /> : null,
modal: getModal(),
unreadMessageCount: checkUnreadMessages(),
openChats: openChats(params.chatID),
openChat: params.chatID,
getCaptionsStatus,
redirectToLogoutUrl,
setDefaultSettings,
fontSize: getFontSize(),
applicationSettings: getSettingsFor('application'),
};
}, AppContainer);
}, AppContainer)));
AppContainer.defaultProps = defaultProps;

View File

@ -1,118 +1,20 @@
import { Meteor } from 'meteor/meteor';
import Auth from '/imports/ui/services/auth';
import Users from '/imports/api/users';
import Breakouts from '/imports/api/breakouts';
import Storage from '/imports/ui/services/storage/session';
import SettingsService from '/imports/ui/components/settings/service';
function setCredentials(nextState, replace) {
if (nextState && nextState.params.authToken) {
const { meetingID, userID, authToken } = nextState.params;
Auth.setCredentials(meetingID, userID, authToken);
replace({
pathname: '/',
});
}
let currentModal = {
component: null,
tracker: new Tracker.Dependency,
};
let dataSubscriptions = null;
function subscribeForData() {
if (dataSubscriptions) {
return dataSubscriptions;
}
const subNames = [
'users', 'chat', 'cursor', 'deskshare', 'meetings',
'polls', 'presentations', 'shapes', 'slides', 'captions', 'breakouts',
];
let subs = [];
subNames.forEach(name => subs.push(subscribeFor(name)));
dataSubscriptions = subs;
return subs;
};
function subscribeFor(collectionName) {
const credentials = Auth.getCredentials();
return new Promise((resolve, reject) => {
Meteor.subscribe(collectionName, credentials, {
onReady: (...args) => resolve(...args),
onStop: (...args) => reject(...args),
});
});
};
function subscribeToCollections(cb) {
subscribeFor('users')
.then(() => {
observeUserKick();
return Promise.all(subscribeForData())
.then(() => {
observeBreakoutEnd();
if (cb) {
return cb();
}
});
})
.catch(redirectToLogoutUrl);
};
function redirectToLogoutUrl(reason) {
console.error(reason);
console.log('Redirecting user to the logoutURL...');
document.location.href = Auth.logoutURL;
}
let wasKicked = false;
const wasKickedDep = new Tracker.Dependency;
function observeUserKick() {
Users.find().observe({
removed(old) {
if (old.userId === Auth.userID) {
Auth.clearCredentials(() => {
wasKicked = true;
wasKickedDep.changed();
});
}
},
});
}
function observeBreakoutEnd() {
Breakouts.find().observe({
removed(old) {
if (old.breakoutMeetingId === Auth.meetingID) {
// The breakout room expired. Closing the browser tab to return to the main room
window.close();
}
},
});
}
function meetingIsBreakout() {
const breakouts = Breakouts.find().fetch();
return (breakouts && breakouts.some(b => b.breakoutMeetingId === Auth.meetingID));
}
function wasUserKicked() {
wasKickedDep.depend();
return wasKicked;
}
let modal = null;
const modalDep = new Tracker.Dependency;
const getModal = () => {
modalDep.depend();
return modal;
currentModal.tracker.depend();
return currentModal.component;
};
const showModal = (val) => {
if (val !== modal) {
modal = val;
modalDep.changed();
const showModal = (component) => {
if (currentModal.component !== component) {
currentModal.component = component;
currentModal.tracker.changed();
}
};
@ -121,22 +23,21 @@ const clearModal = () => {
};
const getCaptionsStatus = () => {
const settings = Storage.getItem('settings_cc');
const settings = SettingsService.getSettingsFor('cc');
return settings ? settings.closedCaptions : false;
};
const getFontSize = () => {
const settings = SettingsService.getSettingsFor('application');
return settings ? settings.fontSize : '14px';
return settings ? settings.fontSize : '16px';
};
function meetingIsBreakout() {
const breakouts = Breakouts.find().fetch();
return (breakouts && breakouts.some(b => b.breakoutMeetingId === Auth.meetingID));
}
export {
subscribeForData,
setCredentials,
subscribeFor,
subscribeToCollections,
wasUserKicked,
redirectToLogoutUrl,
getModal,
showModal,
clearModal,

View File

@ -8,6 +8,7 @@ import DeviceSelector from '/imports/ui/components/audio/device-selector/compone
import AudioStreamVolume from '/imports/ui/components/audio/audio-stream-volume/component';
import EnterAudioContainer from '/imports/ui/components/enter-audio/container';
import AudioTestContainer from '/imports/ui/components/audio-test/container';
import cx from 'classnames';
class AudioSettings extends React.Component {
constructor(props) {
@ -20,7 +21,7 @@ class AudioSettings extends React.Component {
this.state = {
inputDeviceId: undefined,
}
};
}
chooseAudio() {
@ -30,7 +31,7 @@ class AudioSettings extends React.Component {
handleInputChange(deviceId) {
console.log(`INPUT DEVICE CHANGED: ${deviceId}`);
this.setState({
inputDeviceId: deviceId
inputDeviceId: deviceId,
});
}
@ -50,7 +51,7 @@ class AudioSettings extends React.Component {
return (
<div>
<div className={styles.center}>
<div className={styles.topRow}>
<Button className={styles.backBtn}
label={intl.formatMessage(intlMessages.backLabel)}
icon={'left_arrow'}
@ -59,47 +60,69 @@ class AudioSettings extends React.Component {
ghost={true}
onClick={this.chooseAudio}
/>
<div className={styles.title}>
<div className={cx(styles.title, styles.chooseAudio)}>
<FormattedMessage
id="app.audio.audioSettings.titleLabel"
/>
</div>
</div>
<div className={styles.audioNote}>
<FormattedMessage
id="app.audio.audioSettings.descriptionLabel"
/>
<div className={styles.form}>
<div className={styles.row}>
<div className={styles.audioNote}>
<FormattedMessage
id="app.audio.audioSettings.descriptionLabel"
/>
</div>
</div>
<div className={styles.row}>
<div className={styles.col}>
<div className={styles.formElement}>
<label className={cx(styles.label, styles.labelSmall)}>
Microphone source
</label>
<DeviceSelector
value={this.state.inputDeviceId}
className={styles.select}
kind="audioinput"
onChange={this.handleInputChange} />
</div>
</div>
<div className={styles.col}>
<div className={styles.formElement}>
<label className={cx(styles.label, styles.labelSmall)}>
Speaker source
</label>
<DeviceSelector
value={this.state.outputDeviceId}
className={styles.select}
kind="audiooutput"
onChange={this.handleOutputChange} />
</div>
</div>
</div>
<div className={styles.row}>
<div className={styles.col}>
<div className={styles.formElement}>
<label className={cx(styles.label, styles.labelSmall)}>
Your audio stream volume
</label>
<AudioStreamVolume
deviceId={this.state.inputDeviceId}
className={styles.audioMeter} />
</div>
</div>
<div className={styles.col}>
<label className={styles.label}>&nbsp;</label>
<AudioTestContainer/>
</div>
</div>
</div>
<div className={styles.containerLeftHalfContent}>
<span className={styles.heading}>
<FormattedMessage
id="app.audio.audioSettings.microphoneSourceLabel"
/>
</span>
<DeviceSelector
className={styles.item}
kind="audioinput"
onChange={this.handleInputChange} />
<span className={styles.heading}>
<FormattedMessage
id="app.audio.audioSettings.microphoneStreamLabel"
/>
</span>
<AudioStreamVolume
className={styles.item}
deviceId={this.state.inputDeviceId} />
</div>
<div className={styles.containerRightHalfContent}>
<span className={styles.heading}>
<FormattedMessage
id="app.audio.audioSettings.speakerSourceLabel"
/>
</span>
<DeviceSelector
className={styles.item}
kind="audiooutput"
onChange={this.handleOutputChange} />
<AudioTestContainer />
<div className={styles.enterAudio}>
<EnterAudioContainer isFullAudio={true}/>
</div>
</div>

View File

@ -43,22 +43,22 @@ class JoinAudio extends React.Component {
const { intl } = this.props;
return (
<div>
<div className={styles.center}>
<div className={styles.closeBtn}>
<Button className={styles.closeBtn}
label={intl.formatMessage(intlMessages.closeLabel)}
icon={'close'}
size={'lg'}
circle={true}
hideLabel={true}
onClick={this.handleClose}
/>
<div>
<FormattedMessage
</div>
<div className={styles.title}>
<FormattedMessage
id="app.audioModal.audioChoiceLabel"
description="app.audioModal.audioChoiceDescription"
defaultMessage="How would you like to join the audio?"
/>
</div>
/>
</div>
<div className={styles.center}>
<Button className={styles.audioBtn}
@ -68,6 +68,9 @@ class JoinAudio extends React.Component {
size={'jumbo'}
onClick={this.openAudio}
/>
<span className={styles.verticalLine}>
</span>
<Button className={styles.audioBtn}
label={intl.formatMessage(intlMessages.listenOnlyLabel)}
icon={'listen'}

View File

@ -1,159 +1,164 @@
@import "../../stylesheets/variables/_all";
.center {
text-align: center;
font-size: $font-size-large;
padding-top: 40px;
display: flex;
justify-content: center;
align-items: center;
padding-top: 2rem;
}
.closeBtn {
position: absolute;
right: 10px;
top: 10px;
background-color: #FFFFFF;
border: none;
display: flex;
justify-content: flex-end;
i {
color: $color-gray-light;
}
}
// Modifies the close button style
Button.closeBtn span:first-child {
color: $color-gray-light;
background: none;
background-color: $color-primary;
border: none;
box-shadow: none;
Button.audioBtn {
i{
color: #3c5764;
}
}
// Modifies the audio button icon colour
Button.audioBtn span:first-child {
color: #25385D;
border: 5px solid $color-white;
background-color: $color-primary;
color: #1b3c4b;
background-color: #f1f8ff;
box-shadow: none;
border: 5px solid #f1f8ff;
}
// When hovering over a button of class audioBtn, change the border colour of first span-child
Button.audioBtn:hover span:first-child {
border: 5px solid $color-primary;
background-color: #f1f8ff;
}
// Modifies the button label text
Button.audioBtn span:last-child {
color: $color-gray-dark;
font-size: 30%;
color: black;
font-size: 0.8rem;
font-weight: 500;
}
Button.audioBtn:first-of-type {
margin-right: 70px;
margin-right: 5%;
}
Button.audioBtn:last-of-type {
margin-left: 5%;
}
.inner {
padding: 10px;
min-height: 350px;
min-width: 500px;
}
// Audio settings menu
.half {
width: 50%;
float: left;
padding-top: 30px;
}
div.half label {
font-size: 0.75em;
font-weight: 600;
color: $color-primary;
margin-bottom: 5px;
padding: 1em;
min-height: 20rem;
}
.backBtn {
position: absolute;
left: 10px;
top: 10px;
border: none;
box-shadow: none;
i {
color: $color-primary;
}
}
.playSound {
border: none;
box-shadow: none;
}
.enterBtn {
position: absolute;
bottom: 10px;
right: 10px;
border: none;
box-shadow: none;
}
.containerLeftHalf {
width: 50%;
float: left;
}
.containerRightHalf {
width: 50%;
float: right;
}
.containerFull{
width: 100%;
float: right;
}
.containerLeftHalfContent {
@extend .containerLeftHalf;
@extend .row;
}
.containerRightHalfContent {
@extend .containerRightHalf;
@extend .row;
}
.row {
height: 42px;
}
.item {
display: block;
margin-bottom: $md-padding-y;
width: 85%;
margin-bottom: 2em;
border-bottom: 3px solid;
border-top:0px;
border-left: 0px;
border-right: 0px;
outline:0px;
border-color: $color-gray-dark;
text-decoration-color: $color-gray-dark;
}
.title {
color: $color-gray-dark;
font-weight: 700;
font-size: $font-size-large;
margin-top: -1em;
.topRow {
align-items: center;
display: flex;
}
.audioNote {
color: $color-text;
display: inline-block;
margin-top: 1.75em;
margin-bottom: 2em;
font-size: 0.9rem;
}
.heading {
font-weight: 700;
font-size: $font-size-small;
display: inline-block;
margin-bottom: .5em;
.title {
text-align: center;
margin: auto;
color: black;
font-weight: 400;
font-size: 1.3rem;
}
.joinButton {
.form {
display: flex;
flex-flow: column;
padding: 2em;
}
.row {
display: flex;
flex-flow: row;
flex-grow: 1;
justify-content: space-between;
margin-bottom: 0.7rem;
}
.col {
display: flex;
flex-grow: 1;
flex-basis: 0;
margin-right: 1rem;
&:last-child {
margin-right: 0;
padding-right: 0.1rem;
padding-left: 4rem;
}
}
.labelSmall {
color: black;
font-size: 0.7rem;
font-weight: 600;
margin-bottom: 0.3rem;
}
.formElement {
position: relative;
left: 11em;
top: 3em;
display: flex;
flex-flow: column;
flex-grow: 1;
}
.select {
background-color: #fff;
border: 0;
border-bottom: 0.1rem solid $color-gray-lighter;
color: $color-gray-light;
width: 100%;
// appearance: none;
height: 1.75rem;
}
.audioMeter {
width: 100%;
}
.pullContentRight {
display: flex;
justify-content: flex-end;
flex-flow: row;
}
.verticalLine {
color: #f3f6f9;
border-left: 1px solid;
height: 5rem;
}
.enterAudio {
display: flex;
justify-content: flex-end;
margin-right: 2rem;
}
.chooseAudio {
position:absolute;
left:50%;
transform: translate(-50%, 0);
}

View File

@ -28,11 +28,24 @@ export default class Chat extends Component {
return (
<section className={styles.chat}>
<header className={styles.header}>
<Link className={styles.closeChat} to="/users">
<Icon iconName="left_arrow" /> {title}
</Link>
<div className={styles.title}>
<Link to="/users">
<Icon iconName="left_arrow"/> {title}
</Link>
</div>
<div className={styles.closeIcon}>
{
((this.props.chatID == 'public') ?
null :
<Link to="/users">
<Icon iconName="close" onClick={() => actions.handleClosePrivateChat(chatID)}/>
</Link>)
}
</div>
</header>
<MessageList
chatId={chatID}
messages={messages}

View File

@ -95,6 +95,9 @@ export default injectIntl(createContainer(({ params, intl }) => {
isChatLocked,
scrollPosition,
actions: {
handleClosePrivateChat: chatID => ChatService.closePrivateChat(chatID),
handleSendMessage: message => {
let sentMessage = ChatService.sendMessage(chatID, message);
ChatService.updateScrollPosition(chatID, null); //null so its scrolls to bottom

View File

@ -1,7 +1,7 @@
import React, { Component, PropTypes } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import _ from 'underscore';
import _ from 'lodash';
import styles from './styles';
import Button from '/imports/ui/components/button/component';

View File

@ -1,5 +1,5 @@
import React, { Component, PropTypes } from 'react';
import _ from 'underscore';
import _ from 'lodash';
const propTypes = {
text: PropTypes.string.isRequired,

View File

@ -0,0 +1,44 @@
import React, { Component, PropTypes } from 'react';
import { createContainer } from 'meteor/react-meteor-data';
import _ from 'lodash';
import Auth from '/imports/ui/services/auth';
import UserListService from '/imports/ui/components/user-list/service';
import SettingsService from '/imports/ui/components/settings/service';
class ChatNotificationContainer extends Component {
constructor(props) {
super(props);
this.audio = new Audio('/html5client/resources/sounds/notify.mp3');
}
playAudio() {
if (this.props.disableAudio) return;
return _.debounce(() => this.audio.play(), this.audio.duration * 1000)();
}
componentDidUpdate(prevProps) {
if (this.props.unreadMessagesCount > prevProps.unreadMessagesCount) {
this.playAudio();
}
}
render() {
return null;
}
}
export default createContainer(({ currentChatID }) => {
const AppSettings = SettingsService.getSettingsFor('application');
const unreadMessagesCount = UserListService.getOpenChats()
.map(chat => chat.unreadCounter)
.filter(userID => userID !== Auth.userID)
.reduce((a, b) => a + b, 0);
return {
disableAudio: !AppSettings.chatAudioNotifications,
unreadMessagesCount,
};
}, ChatNotificationContainer);

View File

@ -4,8 +4,10 @@ import Meetings from '/imports/api/meetings';
import Auth from '/imports/ui/services/auth';
import UnreadMessages from '/imports/ui/services/unread-messages';
import Storage from '/imports/ui/services/storage/session';
import { callServer } from '/imports/ui/services/api';
import _ from 'lodash';
const CHAT_CONFIG = Meteor.settings.public.chat;
const GROUPING_MESSAGES_WINDOW = CHAT_CONFIG.grouping_messages_window;
@ -20,6 +22,9 @@ const PUBLIC_CHAT_USERNAME = CHAT_CONFIG.public_username;
const ScrollCollection = new Mongo.Collection(null);
// session for closed chat list
const CLOSED_CHAT_LIST_KEY = 'closedChatList';
/* TODO: Same map is done in the user-list/service we should share this someway */
const mapUser = (user) => ({
@ -193,6 +198,13 @@ const sendMessage = (receiverID, message) => {
from_color: 0,
};
let currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY);
// Remove the chat that user send messages from the session.
if (_.indexOf(currentClosedChats, receiver.id) > -1) {
Storage.setItem(CLOSED_CHAT_LIST_KEY, _.without(currentClosedChats, receiver.id));
}
callServer('sendChat', messagePayload);
return messagePayload;
@ -215,6 +227,17 @@ const updateUnreadMessage = (receiverID, timestamp) => {
return UnreadMessages.update(receiverID, timestamp);
};
const closePrivateChat = (chatID) => {
let currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY) || [];
if (_.indexOf(currentClosedChats, chatID) < 0) {
currentClosedChats.push(chatID);
Storage.setItem(CLOSED_CHAT_LIST_KEY, currentClosedChats);
}
};
export default {
getPublicMessages,
getPrivateMessages,
@ -226,4 +249,5 @@ export default {
updateScrollPosition,
updateUnreadMessage,
sendMessage,
closePrivateChat,
};

View File

@ -6,23 +6,38 @@
display: flex;
flex-grow: 1;
flex-direction: column;
justify-content: space-around;
overflow: hidden;
}
.closeChat {
text-decoration: none;
}
.header {
margin-top: $lg-padding-x - $sm-padding-x;
margin-bottom: $lg-padding-x;
display: flex;
flex-direction: row;
align-items: center;
a {
text-decoration: none;
}
}
.title {
@extend %text-elipsis;
width: 90%;
> [class^="icon-bbb-"],
> [class*=" icon-bbb-"] {
font-size: 85%;
}
}
[class='icon-bbb-left_arrow'] {
padding-bottom: 5px;
.closeIcon {
position: relative;
}
[class='icon-bbb-left_arrow'],
[class='icon-bbb-close']{
padding-bottom: 5px;
}

View File

@ -3,10 +3,19 @@ import { findDOMNode } from 'react-dom';
import styles from './styles';
import DropdownTrigger from './trigger/component';
import DropdownContent from './content/component';
import Button from '/imports/ui/components/button/component';
import cx from 'classnames';
import { defineMessages, injectIntl } from 'react-intl';
const FOCUSABLE_CHILDREN = `[tabindex]:not([tabindex="-1"]), a, input, button`;
const intlMessages = defineMessages({
close: {
id: 'app.dropdown.close',
defaultMessage: 'Close',
},
});
const propTypes = {
/**
* The dropdown needs a trigger and a content component as childrens
@ -44,7 +53,7 @@ const defaultProps = {
isOpen: false,
};
export default class Dropdown extends Component {
class Dropdown extends Component {
constructor(props) {
super(props);
this.state = { isOpen: false, };
@ -113,7 +122,7 @@ export default class Dropdown extends Component {
}
render() {
const { children, className, style } = this.props;
const { children, className, style, intl } = this.props;
let trigger = children.find(x => x.type === DropdownTrigger);
let content = children.find(x => x.type === DropdownContent);
@ -137,6 +146,14 @@ export default class Dropdown extends Component {
<div style={style} className={cx(styles.dropdown, className)}>
{trigger}
{content}
{ this.state.isOpen ?
<Button
className={styles.close}
label={intl.formatMessage(intlMessages.close)}
size={'lg'}
color={'default'}
onClick={this.handleHide}
/> : null }
</div>
);
}
@ -144,3 +161,4 @@ export default class Dropdown extends Component {
Dropdown.propTypes = propTypes;
Dropdown.defaultProps = defaultProps;
export default injectIntl(Dropdown);

View File

@ -40,7 +40,9 @@ export default class DropdownContent extends Component {
style={style}
aria-expanded={this.props['aria-expanded']}
className={cx(styles.content, styles[placementName], className)}>
{boundChildren}
<div className={styles.scrollable}>
{boundChildren}
</div>
</div>
);
}

View File

@ -1,6 +1,6 @@
import React, { Component, PropTypes } from 'react';
import styles from '../styles';
import _ from 'underscore';
import _ from 'lodash';
import cx from 'classnames';
import Icon from '/imports/ui/components/icon/component';

View File

@ -10,6 +10,11 @@
padding: ($line-height-computed / 2);
display: flex;
flex-direction: column;
@include mq($small-only) {
font-size: $font-size-large * 1.1;
padding: $line-height-computed;
}
}
@ -32,6 +37,11 @@
align-items: center;
justify-content: flex-start;
@include mq($small-only) {
padding: ($line-height-computed / 1.5) 0;
justify-content: center;
}
&:first-child {
padding-top: 0;
}

View File

@ -1,4 +1,5 @@
@import "../../stylesheets/variables/_all";
@import "../../stylesheets/mixins/_scrollable";
$dropdown-bg: $color-white;
$dropdown-color: $color-text;
@ -18,7 +19,6 @@ $dropdown-caret-height: 8px;
box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
border: 1px solid rgba(0, 0, 0, .15);
padding: $line-height-computed / 2;
// min-width: 150px;
z-index: 1000;
&:after, &:before {
@ -35,10 +35,55 @@ $dropdown-caret-height: 8px;
&[aria-expanded="true"] {
display: block;
}
@include mq($small-only) {
z-index: 1015;
border-radius: 0;
background-color: #fff;
box-shadow: none;
position: fixed;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
border: 0 !important;
padding: 0 !important;
margin: 0 0 $line-height-computed * 5.25 0 !important;
transform: translateX(0) translateY(0) !important;
&:after, &:before {
display: none !important;
}
}
}
.scrollable {
@include mq($small-only) {
@include scrollbox-vertical();
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
}
.trigger {}
.close {
display: none;
position: fixed;
bottom: 1rem;
z-index: 1011;
font-size: $font-size-large * 1.1;
width: calc(100% - #{($line-height-computed * 2)});
left: $line-height-computed;
box-shadow: 0 0 0 2rem #fff;
@include mq($small-only) {
display: block;
}
}
/* Placements
* ==========

View File

@ -0,0 +1,65 @@
import React, { Component, PropTypes } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import styles from './styles.scss';
const intlMessages = defineMessages({
500: {
id: 'app.error.500',
defaultMessage: 'Ops, something went wrong',
},
404: {
id: 'app.error.404',
defaultMessage: 'Not Found',
},
401: {
id: 'app.about.401',
defaultMessage: 'Unauthorized',
},
403: {
id: 'app.about.403',
defaultMessage: 'Forbidden',
},
});
const propTypes = {
code: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
};
const defaultProps = {
code: 500,
};
class ErrorScreen extends Component {
render() {
const { intl, code, children } = this.props;
let formatedMessage = intl.formatMessage(intlMessages[500]);
if (code in intlMessages) {
formatedMessage = intl.formatMessage(intlMessages[code]);
}
return (
<div className={styles.background}>
<h1 className={styles.code}>
{code}
</h1>
<h1 className={styles.message}>
{formatedMessage}
</h1>
<div className={styles.content}>
{children}
</div>
</div>
);
}
}
export default injectIntl(ErrorScreen);
ErrorScreen.propTypes = propTypes;
ErrorScreen.defaultProps = defaultProps;

View File

@ -1,18 +0,0 @@
import React, { Component } from 'react';
import styles from './styles.scss';
import Icon from '../icon/component';
class KickedScreen extends Component {
render() {
return (
<div className={styles.background}>
<Icon iconName="sad" className={styles.icon}/>
<div className={styles.message}>
{this.props.children}
</div>
</div>
);
}
}
export default KickedScreen;

View File

@ -1,9 +1,7 @@
import React, { Component } from 'react';
import { withRouter } from 'react-router';
import { defineMessages, injectIntl } from 'react-intl';
import Auth from '/imports/ui/services/auth';
import Modal from '/imports/ui/components/modal/component';
import LocalStorage from '/imports/ui/services/storage/local.js';
import { clearModal } from '/imports/ui/components/app/service';
const intlMessages = defineMessages({
title: {
@ -33,33 +31,19 @@ const intlMessages = defineMessages({
});
class LeaveConfirmation extends Component {
constructor(props) {
super(props);
this.handleLeaveConfirmation = this.handleLeaveConfirmation.bind(this);
this.handleCancleLogout = this.handleCancleLogout.bind(this);
}
handleLeaveConfirmation() {
Auth.completeLogout();
}
handleCancleLogout() {
clearModal();
}
render() {
const { intl } = this.props;
const { intl, router } = this.props;
return (
<Modal
title={intl.formatMessage(intlMessages.title)}
confirm={{
callback: this.handleLeaveConfirmation,
callback: () => router.push('/logout'),
label: intl.formatMessage(intlMessages.confirmLabel),
description: intl.formatMessage(intlMessages.confirmDesc),
}}
dismiss={{
callback: this.handleCancleLogout,
callback: () => null,
label: intl.formatMessage(intlMessages.dismissLabel),
description: intl.formatMessage(intlMessages.dismissDesc),
}}>
@ -69,4 +53,4 @@ class LeaveConfirmation extends Component {
}
};
export default injectIntl(LeaveConfirmation);
export default withRouter(injectIntl(LeaveConfirmation));

View File

@ -46,7 +46,10 @@ export default class Modal extends Component {
handleDismiss() {
const { dismiss } = this.props;
dismiss.callback(...arguments);
if (dismiss && dismiss.callback) {
dismiss.callback(...arguments);
}
this.setState({ isOpen: false });
clearModal();
}

View File

@ -1,5 +1,5 @@
import React, { Component, PropTypes } from 'react';
import _ from 'underscore';
import _ from 'lodash';
import cx from 'classnames';
import styles from './styles.scss';
@ -7,7 +7,7 @@ import { showModal } from '/imports/ui/components/app/service';
import Button from '../button/component';
import RecordingIndicator from './recording-indicator/component';
import SettingsDropdown from './settings-dropdown/component';
import SettingsDropdownContainer from './settings-dropdown/container';
import Icon from '/imports/ui/components/icon/component';
import BreakoutJoinConfirmation from '/imports/ui/components/breakout-join-confirmation/component';
import Dropdown from '/imports/ui/components/dropdown/component';
@ -85,7 +85,7 @@ class NavBar extends Component {
<RecordingIndicator beingRecorded={beingRecorded}/>
</div>
<div className={styles.right}>
<SettingsDropdown />
<SettingsDropdownContainer />
</div>
</div>
);

View File

@ -57,7 +57,7 @@ export default withRouter(createContainer(({ location, router }) => {
};
const breakouts = Service.getBreakouts();
const currentUserId = Auth.getCredentials().requesterUserId;
const currentUserId = Auth.userID;
return {
breakouts,

View File

@ -4,7 +4,7 @@ import Breakouts from '/imports/api/breakouts';
const getBreakouts = () => Breakouts.find().fetch();
const getBreakoutJoinURL = (breakout) => {
const currentUserId = Auth.getCredentials().requesterUserId;
const currentUserId = Auth.userID;
if (breakout.users) {
const user = breakout.users.find(user => user.userId === currentUserId);

View File

@ -53,44 +53,16 @@ const intlMessages = defineMessages({
id: 'app.navBar.settingsDropdown.leaveSessionDesc',
defaultMessage: 'Leave the meeting',
},
exitFullScreenDesc: {
id: 'app.navBar.settingsDropdown.exitFullScreenDesc',
defaultMessage: 'exit fullscreen mode',
},
exitFullScreenLabel: {
id: 'app.navBar.settingsDropdown.exitFullScreenLabel',
defaultMessage: 'Exit fullscreen',
},
});
const toggleFullScreen = () => {
let element = document.documentElement;
if (document.fullscreenEnabled
|| document.mozFullScreenEnabled
|| document.webkitFullscreenEnabled) {
// If the page is already fullscreen, exit fullscreen
if (document.fullscreenElement
|| document.webkitFullscreenElement
|| document.mozFullScreenElement
|| document.msFullscreenElement) {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
// If the page is not currently fullscreen, make fullscreen
} else {
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen();
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
}
}
}
};
const openSettings = () => showModal(<SettingsMenuContainer />);
const openAbout = () => showModal(<AboutContainer />);
@ -103,7 +75,17 @@ class SettingsDropdown extends Component {
}
render() {
const { intl } = this.props;
const { intl, isFullScreen } = this.props;
let fullScreenLabel = intl.formatMessage(intlMessages.fullscreenLabel);
let fullScreenDesc = intl.formatMessage(intlMessages.fullscreenDesc);
if (isFullScreen) {
fullScreenLabel = intl.formatMessage(intlMessages.exitFullScreenLabel);
fullScreenDesc = intl.formatMessage(intlMessages.exitFullScreenDesc);
}
return (
<Dropdown ref="dropdown">
<DropdownTrigger>
@ -124,9 +106,9 @@ class SettingsDropdown extends Component {
<DropdownList>
<DropdownListItem
icon="fullscreen"
label={intl.formatMessage(intlMessages.fullscreenLabel)}
description={intl.formatMessage(intlMessages.fullscreenDesc)}
onClick={toggleFullScreen.bind(this)}
label={fullScreenLabel}
description={fullScreenDesc}
onClick={this.props.handleToggleFullscreen}
/>
<DropdownListItem
icon="more"

View File

@ -0,0 +1,57 @@
import React, { Component } from 'react';
import SettingsDropdown from './component';
import Service from './service';
export default class SettingsDropdownContainer extends Component {
constructor(props) {
super(props);
this.state = {
isFullScreen: false,
};
this.handleFullscreenChange = this.handleFullscreenChange.bind(this);
}
componentDidMount() {
const fullscreenChangedEvents = ['fullscreenchange',
'webkitfullscreenchange',
'mozfullscreenchange',
'MSFullscreenChange', ];
fullscreenChangedEvents.forEach(event =>
document.addEventListener(event, this.handleFullscreenChange));
}
componentWillUnmount() {
const fullscreenChangedEvents = ['fullscreenchange',
'webkitfullscreenchange',
'mozfullscreenchange',
'MSFullscreenChange', ];
fullscreenChangedEvents.forEach(event =>
document.removeEventListener(event, this.fullScreenToggleCallback));
}
handleFullscreenChange() {
if (screen.height - 1 <= window.innerHeight) {
// browser is probably in fullscreen
this.setState({ isFullScreen: true });
}else {
this.setState({ isFullScreen: false });
}
}
render() {
const handleToggleFullscreen = Service.toggleFullScreen;
const isFullScreen = this.state.isFullScreen;
return (
<SettingsDropdown
handleToggleFullscreen={handleToggleFullscreen}
isFullScreen={isFullScreen}
/>
);
}
}

View File

@ -0,0 +1,40 @@
toggleFullScreen = () => {
let element = document.documentElement;
if (document.fullscreenEnabled
|| document.mozFullScreenEnabled
|| document.webkitFullscreenEnabled) {
// If the page is already fullscreen, exit fullscreen
if (document.fullscreenElement
|| document.webkitFullscreenElement
|| document.mozFullScreenElement
|| document.msFullscreenElement) {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
// If the page is not currently fullscreen, make fullscreen
} else {
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen();
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
}
}
}
};
export {
toggleFullScreen,
};

View File

@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor';
import { createContainer } from 'meteor/react-meteor-data';
import React, { Component, PropTypes } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import _ from 'underscore';
import _ from 'lodash';
import NavBarService from '../nav-bar/service';
import Auth from '/imports/ui/services/auth';
import { humanizeSeconds } from '/imports/utils/humanizeSeconds';

View File

@ -7,8 +7,8 @@ let getSlideData = (params) => {
const { currentSlideNum, presentationId } = params;
// Get userId and meetingId
const userId = AuthSingleton.getCredentials().requesterUserId;
const meetingId = AuthSingleton.getCredentials().meetingId;
const userId = AuthSingleton.userID;
const meetingId = AuthSingleton.meetingID;
// Find the user object of this specific meeting and userid
const currentUser = Users.findOne({

View File

@ -1,5 +1,4 @@
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import React, { Component } from 'react';
import Modal from '/imports/ui/components/modal/component';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
@ -9,10 +8,8 @@ import Application from '/imports/ui/components/settings/submenus/application/co
import Participants from '/imports/ui/components/settings/submenus/participants/component';
import Video from '/imports/ui/components/settings/submenus/video/component';
import Button from '../button/component';
import Icon from '../icon/component';
import styles from './styles';
import cx from 'classnames';
const propTypes = {
};

View File

@ -1,13 +1,13 @@
import React, { Component, PropTypes } from 'react';
import { createContainer } from 'meteor/react-meteor-data';
import _ from 'underscore';
import Settings from './component.jsx';
import _ from 'lodash';
import Settings from './component';
import {
getSettingsFor,
updateSettings,
getClosedCaptionLocales,
getUserRoles,
} from './service.js';
} from './service';
class SettingsContainer extends Component {
render() {

View File

@ -2,7 +2,7 @@ import Storage from '/imports/ui/services/storage/session';
import Users from '/imports/api/users';
import Captions from '/imports/api/captions';
import Auth from '/imports/ui/services/auth';
import _ from 'underscore';
import _ from 'lodash';
const updateSettings = (obj) => {
Object.keys(obj).forEach(k => Storage.setItem(`settings_${k}`, obj[k]));

View File

@ -56,3 +56,13 @@
padding-top: 0;
padding-right: 0;
}
.testAudioBtn {
border: none;
padding-left: 0;
i {
color: $color-primary;
}
background-color: transparent;
color: $color-primary;
}

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React from 'react';
import BaseMenu from '../base/component';
import styles from '../styles.scss';
@ -46,7 +46,7 @@ export default class AudioMenu extends BaseMenu {
render() {
return (
<div className={styles.tabContent}>
<div>
<div className={styles.header}>
<h3 className={styles.title}>Audio</h3>
</div>

View File

@ -2,15 +2,20 @@ import Users from '/imports/api/users';
import Chat from '/imports/api/chat';
import Auth from '/imports/ui/services/auth';
import UnreadMessages from '/imports/ui/services/unread-messages';
import Storage from '/imports/ui/services/storage/session';
import { EMOJI_STATUSES } from '/imports/utils/statuses.js';
import { callServer } from '/imports/ui/services/api';
import _ from 'lodash';
const CHAT_CONFIG = Meteor.settings.public.chat;
const USER_CONFIG = Meteor.settings.public.user;
const ROLE_MODERATOR = USER_CONFIG.role_moderator;
const PRIVATE_CHAT_TYPE = CHAT_CONFIG.type_private;
// session for closed chat list
const CLOSED_CHAT_LIST_KEY = 'closedChatList';
/* TODO: Same map is done in the chat/service we should share this someway */
const mapUser = user => ({
@ -164,13 +169,12 @@ const getUsers = () => {
.fetch();
return users
.map(u => u.user)
.map(mapUser)
.sort(sortUsers);
.map(u => u.user)
.map(mapUser)
.sort(sortUsers);
};
const getOpenChats = chatID => {
window.Users = Users;
let openChats = Chat
.find({ 'message.chat_type': PRIVATE_CHAT_TYPE })
@ -178,6 +182,7 @@ const getOpenChats = chatID => {
.map(mapOpenChats);
let currentUserId = Auth.userID;
if (chatID) {
openChats.push(chatID);
}
@ -193,6 +198,28 @@ const getOpenChats = chatID => {
return op;
});
let currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY) || [];
let filteredChatList = [];
openChats.forEach((op) => {
// When a new private chat message is received, ensure the conversation view is restored.
if (op.unreadCounter > 0) {
if (_.indexOf(currentClosedChats, op.id) > -1) {
Storage.setItem(CLOSED_CHAT_LIST_KEY, _.without(currentClosedChats, op.id));
}
}
// Compare openChats with session and push it into filteredChatList
// if one of the openChat is not in session.
// It will pass to openChats.
if (_.indexOf(currentClosedChats, op.id) < 0) {
filteredChatList.push(op);
}
});
openChats = filteredChatList;
openChats.push({
id: 'public',
name: 'Public Chat',

View File

@ -7,7 +7,7 @@ import { withRouter } from 'react-router';
import { defineMessages, injectIntl } from 'react-intl';
import styles from './styles.scss';
import cx from 'classnames';
import _ from 'underscore';
import _ from 'lodash';
import Dropdown from '/imports/ui/components/dropdown/component';
import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component';

View File

@ -8,7 +8,7 @@ function callServer(name) {
return false;
}
const credentials = Auth.getCredentials();
const credentials = Auth.credentials;
// slice off the first element. That is the function name but we already have that.
const args = Array.prototype.slice.call(arguments, 1);

View File

@ -1,4 +1,8 @@
import { Tracker } from 'meteor/tracker';
import Storage from '/imports/ui/services/storage/session';
import Users from '/imports/api/users';
import { callServer } from '/imports/ui/services/api';
class Auth {
@ -6,11 +10,10 @@ class Auth {
this._meetingID = Storage.getItem('meetingID');
this._userID = Storage.getItem('userID');
this._authToken = Storage.getItem('authToken');
this._logoutURL = Storage.getItem('logoutURL');
if (!this._logoutURL) {
this._setLogOut();
}
this._loggedIn = {
value: false,
tracker: new Tracker.Dependency,
};
}
get meetingID() {
@ -40,22 +43,17 @@ class Auth {
Storage.setItem('authToken', this._authToken);
}
get logoutURL() {
return this._logoutURL;
get loggedIn() {
this._loggedIn.tracker.depend();
return this._loggedIn.value;
}
set logoutURL(logoutURL) {
this._logoutURL = logoutURL;
Storage.setItem('logoutURL', this._logoutURL);
set loggedIn(value) {
this._loggedIn.value = value;
this._loggedIn.tracker.changed();
}
setCredentials(meeting, user, token) {
this.meetingID = meeting;
this.userID = user;
this.token = token;
}
getCredentials() {
get credentials() {
return {
meetingId: this.meetingID,
requesterUserId: this.userID,
@ -63,49 +61,114 @@ class Auth {
};
}
clearCredentials(callback) {
set credentials(value) {
throw 'Credentials are read-only';
}
clearCredentials() {
this.meetingID = null;
this.userID = null;
this.token = null;
this.loggedIn = false;
if (typeof callback === 'function') {
return callback();
return Promise.resolve(...arguments);
};
logout() {
if (!this.loggedIn) {
return Promise.resolve();
}
};
completeLogout() {
let logoutURL = this.logoutURL;
callServer('userLogout');
this.clearCredentials(() => {
document.location.href = logoutURL;
return new Promise((resolve, reject) => {
callServer('userLogout', () => {
this.fetchLogoutUrl()
.then(this.clearCredentials)
.then(resolve);
});
});
};
_setLogOut() {
let request;
let handleLogoutUrlError;
authenticate(meetingID, userID, token) {
if (arguments.length) {
this.meetingID = meetingID;
this.userID = userID;
this.token = token;
}
handleLogoutUrlError = function () {
console.log('Error : could not find the logoutURL');
this.logoutURL = document.location.hostname;
};
return this._subscribeToCurrentUser()
.then(this._addObserverToValidatedField.bind(this));
}
// obtain the logoutURL
request = $.ajax({
dataType: 'json',
url: '/bigbluebutton/api/enter',
_subscribeToCurrentUser() {
const credentials = this.credentials;
return new Promise((resolve, reject) => {
Tracker.autorun((c) => {
setTimeout(() => {
c.stop();
reject('Authentication subscription timeout.');
}, 2000);
const subscription = Meteor.subscribe('current-user', credentials);
if (!subscription.ready()) return;
resolve(c);
});
});
}
request.done(data => {
if (data.response.logoutURL != null) {
this.logoutURL = data.response.logoutURL;
} else {
return handleLogoutUrlError();
}
_addObserverToValidatedField(prevComp) {
return new Promise((resolve, reject) => {
const validationTimeout = setTimeout(() => {
this.clearCredentials();
reject('Authentication timeout.');
}, 2500);
const didValidate = () => {
this.loggedIn = true;
clearTimeout(validationTimeout);
prevComp.stop();
resolve();
};
Tracker.autorun((c) => {
const selector = { meetingId: this.meetingID, userId: this.userID };
const query = Users.find(selector);
if (query.count() && query.fetch()[0].validated) {
c.stop();
didValidate();
}
const handle = query.observeChanges({
changed: (id, fields) => {
if (id !== this.userID) return;
if (fields.validated === true) {
c.stop();
didValidate();
}
if (fields.validated === false) {
c.stop();
this.clearCredentials();
reject('Authentication failed.');
}
},
});
});
const credentials = this.credentials;
callServer('validateAuthToken', credentials);
});
}
return request.fail(() => handleLogoutUrlError());
fetchLogoutUrl() {
const url = `/bigbluebutton/api/enter`;
return fetch(url)
.then(response => response.json())
.then(data => Promise.resolve(data.response.logoutURL));
}
};

View File

@ -1,4 +1,4 @@
import _ from 'underscore';
import _ from 'lodash';
import { Tracker } from 'meteor/tracker';
import { EJSON } from 'meteor/ejson';

View File

@ -9,7 +9,7 @@ $color-primary: #299AD5 !default;
$color-success: #4DC0A2 !default;
$color-danger: #EC6365 !default;
$color-background: #2A2D36 !default;
$color-background: $color-gray-dark !default;
$color-text: #8A95A5 !default;
$color-heading: #4E525E !default;

View File

@ -15,7 +15,6 @@
"grunt-cli": "~1.2.0",
"hiredis": "^0.5.0",
"history": "~3.3.0",
"image-size": "~0.5.0",
"meteor-node-stubs": "^0.2.3",
"node-sass": "~3.8.0",
"probe-image-size": "~2.1.1",
@ -32,10 +31,9 @@
"react-tabs": "^0.8.2",
"react-toggle": "^2.2.0",
"redis": "^2.6.2",
"underscore": "~1.8.3",
"winston": "^2.3.1",
"xml2js": "^0.4.17",
"xmlhttprequest": "^1.8.0"
"lodash": "~4.17.4"
},
"devDependencies": {
"autoprefixer": "^6.3.6",

View File

@ -23,3 +23,5 @@ app:
# Name displayed in the brower URL
basename: '/html5client'
defaultLocale: 'en'

View File

@ -12,7 +12,6 @@
"app.chat.titlePrivate": "Private Chat with {name}",
"app.chat.partnerDisconnected": "{name} has left the meeting",
"app.chat.moreMessages": "More messages below",
"app.kickMessage": "You have been kicked out of the meeting",
"app.presentation.presentationToolbar.prevSlideLabel": "Previous slide",
"app.presentation.presentationToolbar.prevSlideDescrip": "Change the presentation to the previous slide",
"app.presentation.presentationToolbar.nextSlideLabel": "Next slide",
@ -37,6 +36,8 @@
"app.navBar.settingsDropdown.settingsDesc": "Change the general settings",
"app.navBar.settingsDropdown.aboutDesc": "Show information about the client",
"app.navBar.settingsDropdown.leaveSessionDesc": "Leave the meeting",
"app.navBar.settingsDropdown.exitFullScreenLabel": "Exit fullscreen",
"app.navBar.settingsDropdown.exitFullScreenDesc": "Exit fullscreen mode",
"app.leaveConfirmation.title": "Leave Session",
"app.leaveConfirmation.message": "Do you want to leave this meeting?",
"app.leaveConfirmation.confirmLabel": "Leave",
@ -109,5 +110,11 @@
"app.audio.audioSettings.speakerSourceLabel": "Speaker source",
"app.audio.audioSettings.microphoneStreamLabel": "Your audio stream volume",
"app.audio.listenOnly.backLabel": "Back",
"app.audio.listenOnly.closeLabel": "Close"
"app.audio.listenOnly.closeLabel": "Close",
"app.error.kicked": "You have been kicked out of the meeting",
"app.dropdown.close": "Close",
"app.error.500": "Ops, something went wrong",
"app.error.404": "Not found",
"app.error.401": "Unauthorized",
"app.error.403": "Forbidden"
}

View File

@ -5,7 +5,7 @@
-->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
<metadata>
Created by FontForge 20161004 at Fri Dec 30 11:22:51 2016
Created by FontForge 20161004 at Thu Mar 23 10:01:15 2017
By Ghazi
Copyright (c) 2016, BlindSide Networks Inc.
</metadata>
@ -19,7 +19,7 @@ Copyright (c) 2016, BlindSide Networks Inc.
panose-1="2 0 5 3 0 0 0 0 0 0"
ascent="819"
descent="-205"
bbox="0 -205 1212 820"
bbox="0 -205 1024 820"
underline-thickness="51"
underline-position="-102"
unicode-range="U+0020-E932"
@ -31,20 +31,20 @@ Copyright (c) 2016, BlindSide Networks Inc.
<glyph glyph-name="space" unicode=" " horiz-adv-x="524"
/>
<glyph glyph-name="logout" unicode="&#xe900;"
d="M160 -117h84c19 0 30 -14 30 -32c0 -22 -7 -34 -30 -34h-84c-83 0 -150 66 -150 150v670c0 83 67 150 150 150h84c16 0 30 -17 30 -32c0 -16 -9 -32 -30 -32h-84c-47 0 -87 -40 -87 -86v-667c0 -47 40 -87 87 -87zM676 -9c-6 -5 -14 -8 -22 -8c-7 0 -15 3 -21 8
s-8 12 -8 19c0 9 5 20 12 27l233 233h-625c-22 0 -35 14 -35 30c0 17 13 30 35 30h625l-233 233c-7 7 -11 16 -11 25s4 17 11 23c6 5 14 8 21 8c8 0 16 -3 22 -9c14 -13 284 -283 310 -310c-30 -30 -272 -270 -314 -309z" />
d="M160 -117h84c19 0 30 -14 30 -32c0 -16 -7 -34 -30 -34h-84c-83 0 -150 66 -150 150v670c0 83 67 150 150 150h84c16 0 30 -17 30 -32c0 -16 -9 -32 -30 -32h-84c-47 0 -87 -40 -87 -86v-667c0 -47 40 -87 87 -87zM676 -9c-6 -5 -14 -8 -22 -8c-7 0 -15 3 -21 8
s-8 12 -8 19c0 9 5 20 12 27l233 233h-625c-22 0 -35 12 -35 30s13 30 35 30h625l-233 233c-7 7 -11 16 -11 25s4 17 11 23c6 5 14 8 21 8c8 0 16 -3 22 -9l310 -310c-30 -30 -314 -309 -314 -309z" />
<glyph glyph-name="more" unicode="&#xe902;"
d="M17 307c0 60 49 109 109 109c61 0 110 -49 110 -109s-49 -109 -110 -109c-60 0 -109 49 -109 109zM403 307c0 60 49 109 109 109s109 -49 109 -109s-49 -109 -109 -109s-109 49 -109 109zM788 307c0 60 49 109 110 109c60 0 109 -49 109 -109s-49 -109 -109 -109
c-61 0 -110 49 -110 109z" />
<glyph glyph-name="promote" unicode="&#xe903;" horiz-adv-x="1028"
d="M993 601c10 -8 11 -18 11 -28v-522c0 -14 -10 -33 -24 -33h-4c0 0 -520 129 -529 129v0c0 -65 -58 -110 -119 -110c-65 0 -120 55 -120 120v44l-128 31v-9c0 -15 -18 -27 -32 -27c-17 0 -31 12 -31 29v184c0 14 17 28 31 28c18 0 32 -11 32 -28v-17l886 215
c7 2 18 1 27 -6zM331 92c20 0 55 7 55 58l-113 27v-30c0 -31 27 -55 58 -55zM944 82v450l-864 -211v-31c287 -69 577 -139 864 -208z" />
d="M993 601c10 -8 11 -18 11 -28v-523c0 -14 -5 -20 -11 -25c-10 -8 -14 -8 -17 -7l-529 129c0 -65 -58 -110 -119 -110c-65 0 -120 55 -120 120v44l-128 31v-9c0 -15 -18 -27 -32 -27c-17 0 -31 12 -31 29v184c0 14 17 28 31 28c18 0 32 -11 32 -28v-17l886 215
c7 2 18 1 27 -6zM331 92c20 0 55 7 55 58l-113 27v-30c0 -31 27 -55 58 -55zM944 82v450l-864 -211v-31c287 -69 864 -208 864 -208z" />
<glyph glyph-name="video_off" unicode="&#xe904;"
d="M963 78l-236 106c-10 5 -20 17 -20 31v167c0 14 6 21 20 27l236 106c4 2 9 3 13 3c16 0 31 -14 31 -34v-378c0 -18 -15 -31 -31 -31c-4 0 -9 1 -13 3zM945 153v280l-174 -78v-123zM915 662l-212 -208v-410c0 -17 -14 -31 -31 -31h-409s-110 -110 -113 -112
c-5 -4 -12 -6 -18 -6c-10 0 -20 5 -26 13c-4 5 -6 11 -6 17c0 9 5 19 13 27l61 61h-123c-17 0 -31 14 -31 31v526c0 17 14 30 31 30h625c17 0 31 -13 31 -30v-24l160 160c5 5 13 8 20 8c8 0 17 -4 22 -9c8 -8 13 -17 13 -26c0 -6 -2 -12 -7 -17zM81 74h155l406 407v58h-561
v-465zM642 74v318l-314 -318h314z" />
<glyph glyph-name="user" unicode="&#xe905;"
d="M860 85c79 -55 123 -143 123 -239c0 -17 -14 -31 -31 -31h-880c-17 0 -31 14 -31 31c0 96 44 184 123 239c85 58 198 86 348 86s263 -28 348 -86zM106 -123v-4h812c-10 65 -44 120 -95 158c-75 51 -171 75 -311 75s-236 -21 -311 -72c-55 -38 -88 -96 -95 -157zM512 239
d="M860 85c79 -55 123 -143 123 -239c0 -17 -14 -31 -31 -31h-880c-17 0 -31 14 -31 31c0 96 37 179 123 239c115 79 198 86 348 86s263 -28 348 -86zM106 -123v-4h812c-10 65 -44 120 -95 158c-75 51 -171 75 -311 75s-236 -21 -311 -72c-55 -38 -88 -96 -95 -157zM512 239
c-147 0 -263 115 -263 273c0 151 112 290 263 290c154 0 263 -136 263 -290c0 -153 -120 -273 -263 -273zM307 515c0 -105 89 -211 202 -211s201 85 201 211c0 118 -78 222 -198 222c-114 0 -205 -110 -205 -222z" />
<glyph glyph-name="up_arrow" unicode="&#xe906;"
d="M659 502c8 -8 12 -18 12 -29c0 -9 -3 -18 -10 -25c-8 -8 -18 -13 -29 -13c-10 0 -20 4 -28 12l-221 222v-816c0 -20 -20 -39 -40 -39s-39 19 -39 39v816l-222 -222c-8 -8 -17 -11 -27 -11s-20 4 -28 11c-8 8 -12 17 -12 27s4 20 12 28l290 286c8 8 17 13 27 13
@ -65,9 +65,9 @@ d="M58 -205c-24 0 -41 17 -41 41c0 10 3 20 10 27l396 449l-396 439c-8 8 -11 16 -11
d="M973 802c20 0 34 -14 37 -34v-662c0 -20 -14 -35 -34 -35h-375l222 -201c8 -7 17 -20 17 -33c0 -6 -2 -12 -7 -18s-14 -8 -23 -8c-12 0 -24 4 -32 11l-232 209v-190c0 -19 -11 -36 -32 -36c-17 0 -33 16 -33 36v190c-20 -19 -196 -174 -236 -210c-6 -5 -16 -9 -27 -9
c-9 0 -18 2 -23 9c-4 5 -6 11 -6 17c0 11 5 23 16 32l222 198h-376c-20 0 -34 14 -34 34v666c0 20 14 34 34 34h922zM940 136v427h-858v-427h858zM82 631h858v106h-858v-106z" />
<glyph glyph-name="listen" unicode="&#xe90c;"
d="M1096 310c72 -51 116 -133 116 -218c0 -150 -119 -277 -273 -277c-17 0 -31 14 -31 31v485c0 17 14 31 31 31c31 0 61 -7 92 -17c0 227 -233 388 -416 388c-190 0 -393 -146 -417 -375c31 10 61 17 92 17c17 0 31 -13 31 -30v-485c0 -17 -14 -31 -31 -31
c-89 0 -171 41 -222 113c-35 49 -52 104 -52 160c0 85 40 169 114 225c0 263 222 475 485 475c266 0 481 -222 481 -492zM256 -106v416c-89 -14 -160 -85 -174 -174c-2 -12 -3 -24 -3 -36c0 -99 73 -188 177 -206zM973 -123c89 17 160 85 174 177c2 12 3 24 3 36
c0 100 -73 189 -177 207v-420z" />
d="M911 310c62 -44 96 -109 96 -180c0 -123 -99 -229 -225 -229c-14 0 -24 10 -24 24v402c0 14 10 24 24 24c24 0 51 -6 75 -13c-10 170 -140 307 -311 324c-11 1 -21 1 -31 1c-178 0 -332 -135 -348 -315c24 7 51 14 75 14c14 0 24 -11 24 -24v-403c0 -14 -10 -24 -24 -24
c-75 0 -143 34 -184 92c-30 41 -44 88 -44 134c0 71 33 142 95 187c7 219 185 393 403 396c222 -3 399 -184 399 -410zM215 -34v344c-72 -10 -133 -68 -143 -143c-2 -10 -3 -20 -3 -30c0 -81 60 -156 146 -171zM809 -51c75 17 133 71 143 150c2 10 3 20 3 30
c0 81 -60 156 -146 171v-351z" />
<glyph glyph-name="left_arrow" unicode="&#xe90d;"
d="M481 -188c-10 0 -20 4 -30 14l-427 486l430 480c7 9 18 13 29 13c10 0 21 -3 29 -10c9 -7 13 -18 13 -29c0 -10 -3 -21 -10 -29l-385 -425l382 -435c7 -8 10 -18 10 -27c0 -10 -4 -21 -13 -28c-7 -7 -18 -10 -28 -10z" />
<glyph glyph-name="happy" unicode="&#xe90e;"

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

0
record-and-playback/core/scripts/README Executable file → Normal file
View File

0
record-and-playback/core/scripts/bigbluebutton.yml Executable file → Normal file
View File

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