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; dropShadowColor: #000000;
} }
.defaultControlBarStyle {
color : #0B333C;
}
.darkControlBarStyle {
color : #ffffff;
}
Panel { Panel {
borderColor: #dfdfdf; borderColor: #dfdfdf;
borderAlpha: 1; borderAlpha: 1;
@ -755,6 +763,18 @@ MDIWindow { /*None of the following properties are overridden by the MDIWindow c
borderThicknessRight: 3; borderThicknessRight: 3;
} }
.videoDockStyleFocusChatLayout {
borderStyle : none;
borderColor: #42444c;
backgroundColor: #42444c;
}
.videoDockStyleNoFocusChatLayout {
borderStyle : none;
borderColor: #42444c;
backgroundColor: #42444c;
}
.presentationSlideViewStyle { .presentationSlideViewStyle {
backgroundColor: #b9babc; backgroundColor: #b9babc;
} }
@ -865,6 +885,14 @@ MDIWindow { /*None of the following properties are overridden by the MDIWindow c
borderThicknessRight: 3; borderThicknessRight: 3;
} }
.defaultShellStyle {
backgroundColor: #fefeff;
}
.darkShellStyle {
backgroundColor: #42444c;
}
.mdiWindowTitle { .mdiWindowTitle {
color: #3f3f41; color: #3f3f41;
fontFamily: Arial; 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.accessibility.notes.notesview.input = Notes input
bbb.shortcuthelp.title = Shortcut Keys bbb.shortcuthelp.title = Shortcut Keys
bbb.shortcuthelp.titleBar = Shortcut Keys Window Title Bar
bbb.shortcuthelp.minimizeBtn.accessibilityName = Minimize the Shortcut Help Window bbb.shortcuthelp.minimizeBtn.accessibilityName = Minimize the Shortcut Help Window
bbb.shortcuthelp.maximizeRestoreBtn.accessibilityName = Maximize the Shortcut Help Window bbb.shortcuthelp.maximizeRestoreBtn.accessibilityName = Maximize the Shortcut Help Window
bbb.shortcuthelp.closeBtn.accessibilityName = Close 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.general = Global shortcuts
bbb.shortcuthelp.dropdown.presentation = Presentation shortcuts bbb.shortcuthelp.dropdown.presentation = Presentation shortcuts
bbb.shortcuthelp.dropdown.chat = Chat shortcuts bbb.shortcuthelp.dropdown.chat = Chat shortcuts
bbb.shortcuthelp.dropdown.users = Users shortcuts bbb.shortcuthelp.dropdown.users = Users shortcuts
bbb.shortcuthelp.dropdown.caption = Closed Caption 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.shortcut = Shortcut
bbb.shortcuthelp.headers.function = Function 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 = 82
bbb.shortcutkey.raiseHand.function = Raise your hand 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.upload.function = Upload presentation
bbb.shortcutkey.present.previous = 65 bbb.shortcutkey.present.previous = 65
bbb.shortcutkey.present.previous.function = Go to previous slide 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.next.function = Go to next slide
bbb.shortcutkey.present.fitWidth = 70 bbb.shortcutkey.present.fitWidth = 70
bbb.shortcutkey.present.fitWidth.function = Fit slides to width 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.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.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.kick.function = Kick selected person from the meeting
bbb.shortcutkey.users.mute = 83 bbb.shortcutkey.users.mute = 83
bbb.shortcutkey.users.mute.function = Mute or unmute selected person bbb.shortcutkey.users.mute.function = Mute or unmute selected person
bbb.shortcutkey.users.muteall = 65 bbb.shortcutkey.users.muteall = 65
bbb.shortcutkey.users.muteall.function = Mute or unmute all users 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 = 65
bbb.shortcutkey.users.muteAllButPres.function = Mute everyone but the Presenter bbb.shortcutkey.users.muteAllButPres.function = Mute everyone but the Presenter
bbb.shortcutkey.users.breakoutRooms = 75 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.focusBreakoutRooms.function = Focus to breakout rooms list
bbb.shortcutkey.users.listenToBreakoutRoom = 76 bbb.shortcutkey.users.listenToBreakoutRoom = 76
bbb.shortcutkey.users.listenToBreakoutRoom.function = Listen to selected breakout room 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.users.joinBreakoutRoom.function = Join selected breakout room
bbb.shortcutkey.chat.focusTabs = 89 bbb.shortcutkey.chat.focusTabs = 89
bbb.shortcutkey.chat.focusTabs.function = Focus to chat tabs 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.focusBox.function = Focus to chat box
bbb.shortcutkey.chat.changeColour = 67 bbb.shortcutkey.chat.changeColour = 67
bbb.shortcutkey.chat.changeColour.function = Focus to font color picker. bbb.shortcutkey.chat.changeColour.function = Focus to font color picker.

View File

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

View File

@ -23,8 +23,6 @@ package org.bigbluebutton.common
import flexlib.mdi.containers.MDIWindow; import flexlib.mdi.containers.MDIWindow;
import flexlib.mdi.managers.MDIManager; import flexlib.mdi.managers.MDIManager;
import mx.utils.ObjectUtil;
/** /**
* This class exists so we can properly handle context menus on MDIWindow * 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 * 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 users:ArrayCollection;
public var invitedRecently : Boolean;
// Can be one of three following values self, none, other // Can be one of three following values self, none, other
public var listenStatus:String = NONE; 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.as3commons.logging.api.getClassLogger;
import org.bigbluebutton.common.Role; import org.bigbluebutton.common.Role;
import org.bigbluebutton.core.BBB; import org.bigbluebutton.core.BBB;
import org.bigbluebutton.core.managers.UserManager;
import org.bigbluebutton.core.model.Config; import org.bigbluebutton.core.model.Config;
import org.bigbluebutton.core.model.MeetingModel; import org.bigbluebutton.core.model.MeetingModel;
import org.bigbluebutton.core.vo.CameraSettingsVO; import org.bigbluebutton.core.vo.CameraSettingsVO;
@ -568,8 +567,21 @@ package org.bigbluebutton.main.model.users {
} }
breakoutRooms.addItem(newRoom); breakoutRooms.addItem(newRoom);
sortBreakoutRooms(); 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 { private function sortBreakoutRooms() : void {
var sort:Sort = new Sort(); var sort:Sort = new Sort();
sort.fields = [new SortField("sequence", true, false, true)]; 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="{LockControlEvent.OPEN_LOCK_SETTINGS}" method="openLockSettingsWindow" />
<mate:Listener type="{BreakoutRoomEvent.OPEN_BREAKOUT_ROOMS_PANEL}" method="openBreakoutRoomsWindow" /> <mate:Listener type="{BreakoutRoomEvent.OPEN_BREAKOUT_ROOMS_PANEL}" method="openBreakoutRoomsWindow" />
<mate:Listener type="{InvalidAuthTokenEvent.INVALID_AUTH_TOKEN}" method="handleInvalidAuthToken" /> <mate:Listener type="{InvalidAuthTokenEvent.INVALID_AUTH_TOKEN}" method="handleInvalidAuthToken" />
<mate:Listener type="{SwitchedLayoutEvent.SWITCHED_LAYOUT_EVENT}" method="onLayoutChanged" />
<mx:Script> <mx:Script>
<![CDATA[ <![CDATA[
@ -101,6 +103,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
import org.bigbluebutton.core.PopUpUtil; import org.bigbluebutton.core.PopUpUtil;
import org.bigbluebutton.core.UsersUtil; import org.bigbluebutton.core.UsersUtil;
import org.bigbluebutton.core.events.LockControlEvent; import org.bigbluebutton.core.events.LockControlEvent;
import org.bigbluebutton.core.events.SwitchedLayoutEvent;
import org.bigbluebutton.core.managers.UserManager; import org.bigbluebutton.core.managers.UserManager;
import org.bigbluebutton.core.vo.LockSettingsVO; import org.bigbluebutton.core.vo.LockSettingsVO;
import org.bigbluebutton.main.events.AppVersionEvent; 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); 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> </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', private var chatResource:Array = ['bbb.shortcutkey.chat.focusTabs', 'bbb.shortcutkey.chat.focusBox', 'bbb.shortcutkey.chat.sendMessage',
'bbb.shortcutkey.chat.closePrivate']; '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.kick', 'bbb.shortcutkey.users.muteall', 'bbb.shortcutkey.users.focusBreakoutRooms',
'bbb.shortcutkey.users.listenToBreakoutRoom', 'bbb.shortcutkey.users.joinBreakoutRoom']; '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{ private function populateModules():void{
categoryAC.addItem(generalString); categoryAC.addItem(generalString);
if (ShortcutOptions.usersActive)
categoryAC.addItem(userString);
if (ShortcutOptions.presentationActive) if (Capabilities.playerType == "ActiveX") {
categoryAC.addItem(presentationString); if (ShortcutOptions.usersActive)
categoryAC.addItem(userString);
if (ShortcutOptions.chatActive)
categoryAC.addItem(chatString); if (ShortcutOptions.presentationActive)
categoryAC.addItem(presentationString);
if (ShortcutOptions.chatActive)
categoryAC.addItem(chatString);
} else {
browserWarningText.visible = browserWarningText.includeInLayout = true;
}
} }
private function reloadKeys():void { private function reloadKeys():void {
@ -249,10 +254,10 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
]]> ]]>
</mx:Script> </mx:Script>
<common:TabIndexer id="headerIndexer" startIndex="101" tabIndices="{[minimizeBtn, maximizeRestoreBtn, closeBtn]}"/> <common:TabIndexer id="headerIndexer" startIndex="115" tabIndices="{[minimizeBtn, maximizeRestoreBtn, closeBtn]}"/>
<common:TabIndexer startIndex="115" tabIndices="{[categories, keyList]}"/> <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" editable="false"
change="changeArray()"> change="changeArray()">
<mx:ArrayCollection id="categoryAC"> <mx:ArrayCollection id="categoryAC">
@ -264,6 +269,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
--> -->
</mx:ArrayCollection> </mx:ArrayCollection>
</mx:ComboBox> </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:DataGrid id="keyList" draggableColumns="false" dataProvider="{shownKeys}" width="100%" height="100%">
<mx:columns> <mx:columns>
<mx:DataGridColumn dataField="shortcut" width="150" headerText="{ResourceUtil.getInstance().getString('bbb.shortcuthelp.headers.shortcut')}"/> <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 { private function focusWindow(e:ShortcutEvent):void {
focusManager.setFocus(titleBarOverlay); if (this.visible) {
focusManager.setFocus(titleBarOverlay);
}
} }
]]> ]]>
</mx:Script> </mx:Script>

View File

@ -120,7 +120,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
} }
private function focusWindow(e:ShortcutEvent):void{ private function focusWindow(e:ShortcutEvent):void{
focusManager.setFocus(titleBarOverlay); if (this.visible) {
focusManager.setFocus(titleBarOverlay);
}
} }
private function fullScreenHandler(evt:FullScreenEvent):void { 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); var e:SyncLayoutEvent = new SyncLayoutEvent(layout);
_globalDispatcher.dispatchEvent(e); _globalDispatcher.dispatchEvent(e);
} }
} }
private function applyLayout(layout:LayoutDefinition):void { private function applyLayout(layout:LayoutDefinition):void {
_detectContainerChange = false; _detectContainerChange = false;
if (layout != null) { if (layout != null) {
layout.applyToCanvas(_canvas); layout.applyToCanvas(_canvas);
dispatchSwitchedLayoutEvent(layout.name); dispatchSwitchedLayoutEvent(layout.name);
} }
//trace(LOG + " applyLayout layout [" + layout.name + "]"); //trace(LOG + " applyLayout layout [" + layout.name + "]");
updateCurrentLayout(layout); updateCurrentLayout(layout);
_detectContainerChange = true; _detectContainerChange = true;
} }
public function handleLockLayoutEvent(e: LockLayoutEvent):void { 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{ private function focusWindow(e:ShortcutEvent):void{
focusManager.setFocus(titleBarOverlay); if (this.visible) {
focusManager.setFocus(titleBarOverlay);
}
} }
private function resizeHandler():void { private function resizeHandler():void {

View File

@ -20,10 +20,11 @@ package org.bigbluebutton.modules.users.services
{ {
import com.asfusion.mate.events.Dispatcher; import com.asfusion.mate.events.Dispatcher;
import flash.utils.setTimeout;
import org.as3commons.lang.StringUtils; import org.as3commons.lang.StringUtils;
import org.as3commons.logging.api.ILogger; import org.as3commons.logging.api.ILogger;
import org.as3commons.logging.api.getClassLogger; import org.as3commons.logging.api.getClassLogger;
import org.bigbluebutton.common.Role;
import org.bigbluebutton.core.BBB; import org.bigbluebutton.core.BBB;
import org.bigbluebutton.core.EventConstants; import org.bigbluebutton.core.EventConstants;
import org.bigbluebutton.core.UsersUtil; import org.bigbluebutton.core.UsersUtil;
@ -649,11 +650,17 @@ package org.bigbluebutton.modules.users.services
private function handleBreakoutRoomJoinURL(msg:Object):void{ private function handleBreakoutRoomJoinURL(msg:Object):void{
var map:Object = JSON.parse(msg.msg); 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); var event : BreakoutRoomEvent = new BreakoutRoomEvent(BreakoutRoomEvent.BREAKOUT_JOIN_URL);
event.joinURL = map.redirectJoinURL; event.joinURL = map.redirectJoinURL;
var externalMeetingId : String = StringUtils.substringBetween(event.joinURL, "meetingID=", "&"); event.breakoutMeetingSequence = sequence;
event.breakoutMeetingSequence = UserManager.getInstance().getConference().getBreakoutRoomByExternalId(externalMeetingId).sequence;
dispatcher.dispatchEvent(event); 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{ private function handleUpdateBreakoutUsers(msg:Object):void{

View File

@ -155,7 +155,7 @@
var ls:LockSettingsVO = UserManager.getInstance().getConference().getLockSettings(); var ls:LockSettingsVO = UserManager.getInstance().getConference().getLockSettings();
if (data != null) { 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.voiceJoined) {
if (data.listenOnly) { if (data.listenOnly) {

View File

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

View File

@ -128,7 +128,6 @@
private var joinAlert : Alert; private var joinAlert : Alert;
private const FOCUS_USERS_LIST:String = "Focus Users List";
private const MAKE_PRESENTER:String = "Make Presenter"; private const MAKE_PRESENTER:String = "Make Presenter";
private const KICK_USER:String = "Kick User"; private const KICK_USER:String = "Kick User";
private const MUTE_USER:String = "Mute User"; private const MUTE_USER:String = "Mute User";
@ -440,9 +439,10 @@
private function loadKeyCombos(modifier:String):void { private function loadKeyCombos(modifier:String):void {
keyCombos = new Object(); // always start with a fresh array bbb.shortcutkey.users.muteall 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.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.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.muteall') as String)] = MUTE_ALL_USER;
keyCombos[modifier + (ResourceUtil.getInstance().getString('bbb.shortcutkey.users.focusBreakoutRooms') as String)] = FOCUS_BREAKOUT_ROOMS_LIST; 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); var keyPress:String = KeyboardUtil.buildPressedKeys(e);
if (keyCombos[keyPress]) { if (keyCombos[keyPress]) {
switch (keyCombos[keyPress]) { switch (keyCombos[keyPress]) {
case FOCUS_USERS_LIST:
remoteFocusUsers();
break;
case MAKE_PRESENTER: case MAKE_PRESENTER:
remoteMakePresenter(); remoteMakePresenter();
break; break;
@ -498,7 +495,9 @@
} }
private function focusWindow(e:ShortcutEvent):void { private function focusWindow(e:ShortcutEvent):void {
focusManager.setFocus(titleBarOverlay); if (this.visible) {
focusManager.setFocus(titleBarOverlay);
}
} }
public function remoteRaiseHand(e:ShortcutEvent):void{ public function remoteRaiseHand(e:ShortcutEvent):void{
@ -536,7 +535,7 @@
} }
public function remoteKickUser():void { 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; var selData:Object = usersGrid.selectedItem;
if (!selData.me) if (!selData.me)
@ -557,13 +556,8 @@
} }
} }
public function remoteFocusUsers():void {
focusManager.setFocus(usersGrid);
usersGrid.drawFocus(true);
}
public function remoteFocusBreakoutRooms() : void { public function remoteFocusBreakoutRooms() : void {
if (roomsGrid && roomsGrid.visible) { if (roomsGrid && roomsBox.visible) {
focusManager.setFocus(roomsGrid); focusManager.setFocus(roomsGrid);
roomsGrid.drawFocus(true); roomsGrid.drawFocus(true);
} }
@ -636,8 +630,8 @@
</views:BBBDataGrid> </views:BBBDataGrid>
<mx:VBox id="roomsBox" styleName="breakoutRoomsBox" <mx:VBox id="roomsBox" styleName="breakoutRoomsBox"
visible="{breakoutRoomsList.length > 0 &amp;&amp; amIModerator}" visible="{breakoutRoomsList.length > 0}"
includeInLayout="{breakoutRoomsList.length > 0 &amp;&amp; amIModerator}" includeInLayout="{breakoutRoomsList.length > 0}"
horizontalScrollPolicy="off" horizontalScrollPolicy="off"
width="100%" height="180"> width="100%" height="180">
<mx:HBox width="100%"> <mx:HBox width="100%">
@ -658,7 +652,6 @@
showDataTips="true" showDataTips="true"
headerText="{ResourceUtil.getInstance().getString('bbb.users.roomsGrid.users')}"/> headerText="{ResourceUtil.getInstance().getString('bbb.users.roomsGrid.users')}"/>
<mx:DataGridColumn dataField="meetingId" <mx:DataGridColumn dataField="meetingId"
visible="{amIModerator}"
headerText="{ResourceUtil.getInstance().getString('bbb.users.roomsGrid.action')}" headerText="{ResourceUtil.getInstance().getString('bbb.users.roomsGrid.action')}"
itemRenderer="org.bigbluebutton.modules.users.views.RoomActionsRenderer"/> itemRenderer="org.bigbluebutton.modules.users.views.RoomActionsRenderer"/>
</views:columns> </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()"> creationComplete="onCreationComplete()">
<mate:Listener type="{ShortcutEvent.FOCUS_VIDEO_WINDOW}" method="focusWindow" /> <mate:Listener type="{ShortcutEvent.FOCUS_VIDEO_WINDOW}" method="focusWindow" />
<mate:Listener type="{SwitchedLayoutEvent.SWITCHED_LAYOUT_EVENT}" method="onLayoutChanged" />
<mx:Script> <mx:Script>
<![CDATA[ <![CDATA[
import com.asfusion.mate.events.Dispatcher; 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 mx.core.UIComponent;
import org.bigbluebutton.core.KeyboardUtil; import org.bigbluebutton.core.KeyboardUtil;
import org.bigbluebutton.core.events.SwitchedLayoutEvent;
import org.bigbluebutton.main.events.ShortcutEvent; import org.bigbluebutton.main.events.ShortcutEvent;
import org.bigbluebutton.main.views.MainCanvas; import org.bigbluebutton.main.views.MainCanvas;
import org.bigbluebutton.modules.videoconf.model.VideoConfOptions; 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 keyCombos:Object;
private var disp:Dispatcher = new Dispatcher(); private var disp:Dispatcher = new Dispatcher();
private var darkMode:Boolean;
private function onCreationComplete():void { private function onCreationComplete():void {
hotkeyCapture(); hotkeyCapture();
titleBarOverlay.tabIndex = videoOptions.baseTabIndex; titleBarOverlay.tabIndex = videoOptions.baseTabIndex;
@ -89,7 +93,11 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
override protected function resourcesChanged():void { override protected function resourcesChanged():void {
super.resourcesChanged(); 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) { if (titleBarOverlay != null) {
titleBarOverlay.accessibilityName = ResourceUtil.getInstance().getString('bbb.videoDock.titleBar'); 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 { 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 { private function remoteMinimize(e:ShortcutEvent):void {

View File

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

View File

@ -56,7 +56,6 @@ modules@0.7.9
modules-runtime@0.7.9 modules-runtime@0.7.9
mongo@1.1.15 mongo@1.1.15
mongo-id@1.0.6 mongo-id@1.0.6
mrt:external-file-loader@0.1.4
nathantreid:css-modules@2.4.0 nathantreid:css-modules@2.4.0
npm-mongo@2.2.16_1 npm-mongo@2.2.16_1
observe-sequence@1.0.15 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> <head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BBB - HTML5 Client</title> <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> </head>
<body> <body style="background-color: #2A2D33">
<div id="app" role="application"></div> <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> </body>

View File

@ -1,90 +1,9 @@
import React from 'react'; import React from 'react';
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
import { render } from 'react-dom'; import { render } from 'react-dom';
import { showModal } from '/imports/ui/components/app/service'; import { renderRoutes } from '/imports/startup/client/routes';
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));
};
Meteor.startup(() => { Meteor.startup(() => {
render(renderRoutes(), document.getElementById('app'));
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);
});
}); });

View File

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

View File

@ -1,4 +1,4 @@
import _ from 'underscore'; import _ from 'lodash';
import Captions from '/imports/api/captions'; import Captions from '/imports/api/captions';
import Logger from '/imports/startup/server/logger'; import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check'; 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 }) { export default function handleValidateAuthToken({ payload }) {
const meetingId = payload.meeting_id; const meetingId = payload.meeting_id;
const userId = payload.userid; const userId = payload.userid;
const validStatus = payload.valid; const validStatus = JSON.parse(payload.valid);
check(meetingId, String); check(meetingId, String);
check(userId, String); check(userId, String);
check(validStatus, String); check(validStatus, Boolean);
const selector = { const selector = {
meetingId, meetingId,
@ -21,14 +21,11 @@ export default function handleValidateAuthToken({ payload }) {
const User = Users.findOne(selector); const User = Users.findOne(selector);
if (!User) { // If we dont find the user on our collection is a flash user and we can skip
throw new Meteor.Error( if (!User) return;
'user-not-found', `You need a valid user to be able validate the token`);
}
if (User.validated === validStatus) { // User already flagged so we skip
return; if (User.validated === validStatus) return;
}
const modifier = { const modifier = {
$set: { $set: {

View File

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

View File

@ -21,11 +21,12 @@ export default function userLeaving(credentials, userId) {
check(requesterUserId, String); check(requesterUserId, String);
check(userId, String); check(userId, String);
const User = Users.findOne({ const selector = {
meetingId, meetingId,
userId, userId,
}); };
const User = Users.findOne(selector);
if (!User) { if (!User) {
throw new Meteor.Error( throw new Meteor.Error(
'user-not-found', `You need a valid user to be able to toggle audio`); '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); 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 = { let payload = {
meeting_id: meetingId, meeting_id: meetingId,
userid: userId, userid: userId,

View File

@ -42,7 +42,7 @@ export default function validateAuthToken(credentials) {
reply_to: `${meetingId}/${requesterUserId}`, 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); return RedisPubSub.publish(CHANNEL, EVENT_NAME, payload, header);
}; };

View File

@ -18,7 +18,7 @@ export default function createDummyUser(meetingId, userId, authToken) {
userId, userId,
authToken, authToken,
clientType: 'HTML5', clientType: 'HTML5',
validated: false, validated: null,
}; };
const cb = (err, numChanged) => { 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 { isAllowedTo } from '/imports/startup/server/userPermissions';
import userLeaving from './methods/userLeaving'; 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) { Meteor.publish('users', function (credentials) {
const { meetingId, requesterUserId, requesterToken } = credentials; const { meetingId, requesterUserId, requesterToken } = credentials;
@ -14,12 +35,9 @@ Meteor.publish('users', function (credentials) {
check(requesterUserId, String); check(requesterUserId, String);
check(requesterToken, String); check(requesterToken, String);
validateAuthToken(credentials); if (!isAllowedTo('subscribeUsers', credentials)) {
this.error(new Meteor.Error(402, "The user was not authorized to subscribe for 'Users'"));
// 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'"));
// }
this.onStop(() => { this.onStop(() => {
userLeaving(credentials, requesterUserId); 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 { Router, Route, Redirect, IndexRoute, useRouterHistory } from 'react-router';
import { createHistory } from 'history'; import { createHistory } from 'history';
// route components import { joinRouteHandler, logoutRouteHandler, authenticatedRouteHandler } from './auth';
import AppContainer from '/imports/ui/components/app/container'; import Base from './base';
import { subscribeToCollections, setCredentials } from '/imports/ui/components/app/service';
import LoadingScreen from '/imports/ui/components/loading-screen/component';
import ChatContainer from '/imports/ui/components/chat/container'; import ChatContainer from '/imports/ui/components/chat/container';
import UserListContainer from '/imports/ui/components/user-list/container'; import UserListContainer from '/imports/ui/components/user-list/container';
const browserHistory = useRouterHistory(createHistory)({ const browserHistory = useRouterHistory(createHistory)({
// Name displayed in the brower URL
basename: Meteor.settings.public.app.basename, basename: Meteor.settings.public.app.basename,
}); });
export const renderRoutes = () => ( export const renderRoutes = () => (
<Router history={browserHistory}> <Router history={browserHistory}>
<Route path="/join/:meetingID/:userID/:authToken" onEnter={setCredentials} /> <Route path="/logout" onEnter={logoutRouteHandler} />
<Route path="/" onEnter={() => { <Route path="/join/:meetingID/:userID/:authToken"
subscribeToCollections(); component={LoadingScreen} onEnter={joinRouteHandler} />
}} <Route path="/" component={Base} onEnter={authenticatedRouteHandler} >
getComponent={(nextState, cb) => {
subscribeToCollections(() => cb(null, AppContainer));
}}>
<IndexRoute components={{}} /> <IndexRoute components={{}} />
<Route name="users" path="users" components={{ userList: UserListContainer }} />
<Route name="users" path="users" getComponents={(nextState, cb) => { <Route name="chat" path="users/chat/:chatID" components={{
subscribeToCollections(() => cb(null, { userList: UserListContainer,
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" /> <Redirect from="users/chat" to="/users/chat/public" />
</Route> </Route>
<Redirect from="*" to="/" /> <Route name="error" path="/error/:errorCode" component={Base}/>
<Redirect from="*" to="/error/404" />
</Router> </Router>
); );

View File

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

View File

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

View File

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

View File

@ -1,15 +1,10 @@
import React, { Component, PropTypes } from 'react'; 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 Button from '/imports/ui/components/button/component';
import styles from './styles.scss'; import styles from './styles.scss';
import EmojiContainer from './emoji-menu/container'; import EmojiContainer from './emoji-menu/container';
import ActionsDropdown from './actions-dropdown/component'; 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 JoinAudioOptionsContainer from './audio-menu/container';
import MuteAudioContainer from './mute-button/container'; import MuteAudioContainer from './mute-button/container';
import { exitAudio } from '/imports/api/phone';
import JoinVideo from './video-button/component'; import JoinVideo from './video-button/component';
export default class ActionsBar extends Component { export default class ActionsBar extends Component {
@ -17,10 +12,6 @@ export default class ActionsBar extends Component {
super(props); super(props);
} }
openJoinAudio() {
return showModal(<Audio handleJoinListenOnly={this.props.handleJoinListenOnly} />)
}
renderForPresenter() { renderForPresenter() {
return ( return (
<div className={styles.actionsbar}> <div className={styles.actionsbar}>
@ -30,14 +21,15 @@ export default class ActionsBar extends Component {
<div className={styles.center}> <div className={styles.center}>
<MuteAudioContainer /> <MuteAudioContainer />
<JoinAudioOptionsContainer <JoinAudioOptionsContainer
open={this.openJoinAudio.bind(this)} handleJoinAudio={this.props.handleOpenJoinAudio}
close={() => {exitAudio();}} handleCloseAudio={this.props.handleExitAudio}
/> />
{/*<JoinVideo />*/} {/*<JoinVideo />*/}
<EmojiContainer /> <EmojiContainer />
</div> </div>
<div className={styles.right}> <div className={styles.hidden}>
<ActionsDropdown />
</div> </div>
</div> </div>
); );
@ -49,15 +41,13 @@ export default class ActionsBar extends Component {
<div className={styles.center}> <div className={styles.center}>
<MuteAudioContainer /> <MuteAudioContainer />
<JoinAudioOptionsContainer <JoinAudioOptionsContainer
open={this.openJoinAudio.bind(this)} handleJoinAudio={this.props.handleOpenJoinAudio}
close={() => {exitAudio();}} handleCloseAudio={this.props.handleExitAudio}
/> />
{/*<JoinVideo />*/} {/*<JoinVideo />*/}
<EmojiContainer /> <EmojiContainer />
</div> </div>
<div className={styles.right}>
</div>
</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 { createContainer } from 'meteor/react-meteor-data';
import ActionsBar from './component'; import ActionsBar from './component';
import Service from './service'; import Service from './service';
import { joinListenOnly } from '/imports/api/phone';
class ActionsBarContainer extends Component { class ActionsBarContainer extends Component {
constructor(props) { constructor(props) {
@ -10,19 +9,23 @@ class ActionsBarContainer extends Component {
} }
render() { render() {
const handleJoinListenOnly = () => joinListenOnly();
return ( return (
<ActionsBar <ActionsBar
handleJoinListenOnly={handleJoinListenOnly} {...this.props}>
{...this.props}> {this.props.children}
{this.props.children}
</ActionsBar> </ActionsBar>
); );
} }
} }
export default createContainer(() => { export default createContainer(() => {
let data = Service.isUserPresenter(); const isPresenter = Service.isUserPresenter();
return data; const handleExitAudio = () => Service.handleExitAudio();
const handleOpenJoinAudio = () => Service.handleJoinAudio();
return {
isUserPresenter: isPresenter,
handleExitAudio: handleExitAudio,
handleOpenJoinAudio: handleOpenJoinAudio,
};
}, ActionsBarContainer); }, ActionsBarContainer);

View File

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

View File

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

View File

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

View File

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

View File

@ -1,214 +1,125 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import _ from 'lodash';
import LoadingScreen from '../loading-screen/component';
import KickedScreen from '../kicked-screen/component';
import NotificationsBarContainer from '../notifications-bar/container'; import NotificationsBarContainer from '../notifications-bar/container';
import AudioNotificationContainer from '../audio-notification/container'; import AudioNotificationContainer from '../audio-notification/container';
import ChatNotificationContainer from '../chat/notification/container';
import LocalStorage from '/imports/ui/services/storage/local.js';
import Button from '../button/component'; import Button from '../button/component';
import styles from './styles'; import styles from './styles';
import cx from 'classnames'; import cx from 'classnames';
const propTypes = { const propTypes = {
init: PropTypes.func.isRequired,
fontSize: PropTypes.string,
navbar: PropTypes.element, navbar: PropTypes.element,
sidebar: PropTypes.element, sidebar: PropTypes.element,
sidebarRight: PropTypes.element,
media: PropTypes.element, media: PropTypes.element,
actionsbar: PropTypes.element, actionsbar: PropTypes.element,
captions: PropTypes.element,
modal: PropTypes.element, modal: PropTypes.element,
unreadMessageCount: PropTypes.array, };
openChats: PropTypes.array,
const defaultProps = {
fontSize: '16px',
}; };
export default class App extends Component { export default class App extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
compactUserList: false, //TODO: Change this on userlist resize (?) 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() { componentDidMount() {
this.setDefaultSettings(); document.getElementsByTagName('html')[0].style.fontSize = this.props.fontSize;
this.setHtmlFontSize(this.props.fontSize);
} }
renderNavBar() { renderNavBar() {
const { navbar } = this.props; const { navbar } = this.props;
if (navbar) { if (!navbar) return null;
return (
<header className={styles.navbar}>
{navbar}
</header>
);
}
return false; return (
<header className={styles.navbar}>
{navbar}
</header>
);
} }
renderSidebar() { renderSidebar() {
const { sidebar } = this.props; const { sidebar } = this.props;
if (sidebar) { if (!sidebar) return null;
return (
<aside className={styles.sidebar}>
{sidebar}
</aside>
);
}
return false; return (
<aside className={styles.sidebar}>
{sidebar}
</aside>
);
} }
renderUserList() { renderUserList() {
let { userList } = this.props; let { userList } = this.props;
const { compactUserList } = this.state; const { compactUserList } = this.state;
if (!userList) return;
let userListStyle = {}; let userListStyle = {};
userListStyle[styles.compact] = compactUserList; userListStyle[styles.compact] = compactUserList;
if (userList) { userList = React.cloneElement(userList, {
userList = React.cloneElement(userList, { compact: compactUserList,
compact: compactUserList, });
});
return ( return (
<nav className={cx(styles.userList, userListStyle)}> <nav className={cx(styles.userList, userListStyle)}>
{userList} {userList}
</nav> </nav>
); );
}
return false;
} }
renderChat() { renderChat() {
const { chat } = this.props; const { chat } = this.props;
if (chat) { if (!chat) return null;
return (
<section className={styles.chat} role="log">
{chat}
</section>
);
}
return false; return (
<section className={styles.chat} role="log">
{chat}
</section>
);
} }
renderMedia() { renderMedia() {
const { media } = this.props; const { media } = this.props;
if (media) { if (!media) return null;
return (
<section className={styles.media}>
{media}
</section>
);
}
return false; return (
} <section className={styles.media}>
{media}
renderClosedCaptions() { </section>
const { captions } = this.props; );
if (captions && this.props.getCaptionsStatus()) {
return (
<section className={styles.closedCaptions}>
{captions}
</section>
);
}
} }
renderActionsBar() { renderActionsBar() {
const { actionsbar } = this.props; const { actionsbar } = this.props;
if (actionsbar) { if (!actionsbar) return null;
return (
<section className={styles.actionsbar}>
{actionsbar}
</section>
);
}
return false;
}
renderAudioElement() {
return ( 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() { render() {
if (this.props.wasKicked) { const { modal, params } = this.props;
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/>;
}
return ( return (
<main className={styles.main}> <main className={styles.main}>
@ -223,13 +134,14 @@ export default class App extends Component {
{this.renderActionsBar()} {this.renderActionsBar()}
</div> </div>
{this.renderSidebar()} {this.renderSidebar()}
{this.renderClosedCaptions()}
</section> </section>
{this.renderAudioElement()} {modal}
{this.renderModal()} <audio id="remote-media" autoPlay="autoplay"></audio>
<ChatNotificationContainer currentChatID={params.chatID} />
</main> </main>
); );
} }
} }
App.propTypes = propTypes; App.propTypes = propTypes;
App.defaultProps = defaultProps;

View File

@ -1,32 +1,42 @@
import React, { Component, PropTypes, cloneElement } from 'react'; import React, { Component, PropTypes, cloneElement } from 'react';
import { createContainer } from 'meteor/react-meteor-data'; import { createContainer } from 'meteor/react-meteor-data';
import App from './component'; import { withRouter } from 'react-router';
import { import { defineMessages, injectIntl } from 'react-intl';
subscribeForData,
wasUserKicked,
redirectToLogoutUrl,
getModal,
getCaptionsStatus,
getFontSize,
} from './service';
import { setDefaultSettings, getSettingsFor } from '/imports/ui/components/settings/service';
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 NavBarContainer from '../nav-bar/container';
import ActionsBarContainer from '../actions-bar/container'; import ActionsBarContainer from '../actions-bar/container';
import MediaContainer from '../media/container'; import MediaContainer from '../media/container';
import ClosedCaptionsContainer from '../closed-captions/container'; import AudioModalContainer from '../audio-modal/container';
import UserListService from '../user-list/service'; import ClosedCaptionsContainer from '/imports/ui/components/closed-captions/container';
import Auth from '/imports/ui/services/auth';
const defaultProps = { const defaultProps = {
navbar: <NavBarContainer />, navbar: <NavBarContainer />,
actionsbar: <ActionsBarContainer />, actionsbar: <ActionsBarContainer />,
media: <MediaContainer />, 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 { class AppContainer extends Component {
render() { render() {
// inject location on the navbar container // inject location on the navbar container
@ -38,52 +48,44 @@ class AppContainer extends Component {
</App> </App>
); );
} }
}
let loading = true;
const loadingDep = new Tracker.Dependency;
const getLoading = () => {
loadingDep.depend();
return loading;
}; };
const setLoading = (val) => { const APP_CONFIG = Meteor.settings.public.app;
if (val !== loading) {
loading = val; const init = () => {
loadingDep.changed(); setDefaultSettings();
if (APP_CONFIG.autoJoinAudio) {
showModal(<AudioModalContainer />);
} }
}; };
const checkUnreadMessages = () => { export default withRouter(injectIntl(createContainer(({ router, intl, baseControls }) => {
return UserListService.getOpenChats().map(chat=> chat.unreadCounter) // Check if user is kicked out of the session
.filter(userID => userID !== Auth.userID); Users.find({ userId: Auth.userID }).observeChanges({
}; removed() {
Auth.clearCredentials()
.then(() => {
router.push('/error/403');
baseControls.updateErrorState(
intl.formatMessage(intlMessages.kickedMessage),
);
});
},
});
const openChats = (chatID) => { // Close the widow when the current breakout room ends
// get currently opened chatID Breakouts.find({ breakoutMeetingId: Auth.meetingID }).observeChanges({
return UserListService.getOpenChats(chatID).map(chat => chat.id); removed(old) {
} Auth.clearCredentials().then(window.close);
},
export default createContainer(({ params }) => {
Promise.all(subscribeForData())
.then(() => {
setLoading(false);
}); });
return { return {
wasKicked: wasUserKicked(), init,
isLoading: getLoading(), sidebar: getCaptionsStatus() ? <ClosedCaptionsContainer /> : null,
modal: getModal(), modal: getModal(),
unreadMessageCount: checkUnreadMessages(),
openChats: openChats(params.chatID),
openChat: params.chatID,
getCaptionsStatus,
redirectToLogoutUrl,
setDefaultSettings,
fontSize: getFontSize(), fontSize: getFontSize(),
applicationSettings: getSettingsFor('application'),
}; };
}, AppContainer); }, AppContainer)));
AppContainer.defaultProps = defaultProps; 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 Breakouts from '/imports/api/breakouts';
import Storage from '/imports/ui/services/storage/session';
import SettingsService from '/imports/ui/components/settings/service'; import SettingsService from '/imports/ui/components/settings/service';
function setCredentials(nextState, replace) { let currentModal = {
if (nextState && nextState.params.authToken) { component: null,
const { meetingID, userID, authToken } = nextState.params; tracker: new Tracker.Dependency,
Auth.setCredentials(meetingID, userID, authToken);
replace({
pathname: '/',
});
}
}; };
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 = () => { const getModal = () => {
modalDep.depend(); currentModal.tracker.depend();
return modal; return currentModal.component;
}; };
const showModal = (val) => { const showModal = (component) => {
if (val !== modal) { if (currentModal.component !== component) {
modal = val; currentModal.component = component;
modalDep.changed(); currentModal.tracker.changed();
} }
}; };
@ -121,22 +23,21 @@ const clearModal = () => {
}; };
const getCaptionsStatus = () => { const getCaptionsStatus = () => {
const settings = Storage.getItem('settings_cc'); const settings = SettingsService.getSettingsFor('cc');
return settings ? settings.closedCaptions : false; return settings ? settings.closedCaptions : false;
}; };
const getFontSize = () => { const getFontSize = () => {
const settings = SettingsService.getSettingsFor('application'); 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 { export {
subscribeForData,
setCredentials,
subscribeFor,
subscribeToCollections,
wasUserKicked,
redirectToLogoutUrl,
getModal, getModal,
showModal, showModal,
clearModal, 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 AudioStreamVolume from '/imports/ui/components/audio/audio-stream-volume/component';
import EnterAudioContainer from '/imports/ui/components/enter-audio/container'; import EnterAudioContainer from '/imports/ui/components/enter-audio/container';
import AudioTestContainer from '/imports/ui/components/audio-test/container'; import AudioTestContainer from '/imports/ui/components/audio-test/container';
import cx from 'classnames';
class AudioSettings extends React.Component { class AudioSettings extends React.Component {
constructor(props) { constructor(props) {
@ -20,7 +21,7 @@ class AudioSettings extends React.Component {
this.state = { this.state = {
inputDeviceId: undefined, inputDeviceId: undefined,
} };
} }
chooseAudio() { chooseAudio() {
@ -30,7 +31,7 @@ class AudioSettings extends React.Component {
handleInputChange(deviceId) { handleInputChange(deviceId) {
console.log(`INPUT DEVICE CHANGED: ${deviceId}`); console.log(`INPUT DEVICE CHANGED: ${deviceId}`);
this.setState({ this.setState({
inputDeviceId: deviceId inputDeviceId: deviceId,
}); });
} }
@ -50,7 +51,7 @@ class AudioSettings extends React.Component {
return ( return (
<div> <div>
<div className={styles.center}> <div className={styles.topRow}>
<Button className={styles.backBtn} <Button className={styles.backBtn}
label={intl.formatMessage(intlMessages.backLabel)} label={intl.formatMessage(intlMessages.backLabel)}
icon={'left_arrow'} icon={'left_arrow'}
@ -59,47 +60,69 @@ class AudioSettings extends React.Component {
ghost={true} ghost={true}
onClick={this.chooseAudio} onClick={this.chooseAudio}
/> />
<div className={styles.title}> <div className={cx(styles.title, styles.chooseAudio)}>
<FormattedMessage <FormattedMessage
id="app.audio.audioSettings.titleLabel" id="app.audio.audioSettings.titleLabel"
/> />
</div> </div>
</div> </div>
<div className={styles.audioNote}>
<FormattedMessage <div className={styles.form}>
id="app.audio.audioSettings.descriptionLabel"
/> <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>
<div className={styles.containerLeftHalfContent}>
<span className={styles.heading}> <div className={styles.enterAudio}>
<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 />
<EnterAudioContainer isFullAudio={true}/> <EnterAudioContainer isFullAudio={true}/>
</div> </div>
</div> </div>

View File

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

View File

@ -1,159 +1,164 @@
@import "../../stylesheets/variables/_all"; @import "../../stylesheets/variables/_all";
.center { .center {
text-align: center; display: flex;
font-size: $font-size-large; justify-content: center;
padding-top: 40px; align-items: center;
padding-top: 2rem;
} }
.closeBtn { .closeBtn {
position: absolute; background-color: #FFFFFF;
right: 10px; border: none;
top: 10px; display: flex;
justify-content: flex-end;
i {
color: $color-gray-light;
}
} }
// Modifies the close button style Button.audioBtn {
Button.closeBtn span:first-child { i{
color: $color-gray-light; color: #3c5764;
background: none; }
background-color: $color-primary;
border: none;
box-shadow: none;
} }
// Modifies the audio button icon colour // Modifies the audio button icon colour
Button.audioBtn span:first-child { Button.audioBtn span:first-child {
color: #25385D; color: #1b3c4b;
border: 5px solid $color-white; background-color: #f1f8ff;
background-color: $color-primary;
box-shadow: none; box-shadow: none;
border: 5px solid #f1f8ff;
} }
// When hovering over a button of class audioBtn, change the border colour of first span-child // When hovering over a button of class audioBtn, change the border colour of first span-child
Button.audioBtn:hover span:first-child { Button.audioBtn:hover span:first-child {
border: 5px solid $color-primary; border: 5px solid $color-primary;
background-color: #f1f8ff;
} }
// Modifies the button label text // Modifies the button label text
Button.audioBtn span:last-child { Button.audioBtn span:last-child {
color: $color-gray-dark; color: black;
font-size: 30%; font-size: 0.8rem;
font-weight: 500;
} }
Button.audioBtn:first-of-type { Button.audioBtn:first-of-type {
margin-right: 70px; margin-right: 5%;
}
Button.audioBtn:last-of-type {
margin-left: 5%;
} }
.inner { .inner {
padding: 10px; padding: 1em;
min-height: 350px; min-height: 20rem;
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;
} }
.backBtn { .backBtn {
position: absolute;
left: 10px;
top: 10px;
border: none; border: none;
box-shadow: none; i {
color: $color-primary;
}
} }
.playSound { .topRow {
border: none; align-items: center;
box-shadow: none; display: flex;
}
.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;
} }
.audioNote { .audioNote {
color: $color-text; color: $color-text;
display: inline-block; display: inline-block;
margin-top: 1.75em; font-size: 0.9rem;
margin-bottom: 2em;
} }
.heading { .title {
font-weight: 700; text-align: center;
font-size: $font-size-small; margin: auto;
display: inline-block; color: black;
margin-bottom: .5em; 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; position: relative;
left: 11em; display: flex;
top: 3em; 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 ( return (
<section className={styles.chat}> <section className={styles.chat}>
<header className={styles.header}> <header className={styles.header}>
<Link className={styles.closeChat} to="/users"> <div className={styles.title}>
<Icon iconName="left_arrow" /> {title} <Link to="/users">
</Link> <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> </header>
<MessageList <MessageList
chatId={chatID} chatId={chatID}
messages={messages} messages={messages}

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import _ from 'underscore'; import _ from 'lodash';
const propTypes = { const propTypes = {
text: PropTypes.string.isRequired, 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 Auth from '/imports/ui/services/auth';
import UnreadMessages from '/imports/ui/services/unread-messages'; import UnreadMessages from '/imports/ui/services/unread-messages';
import Storage from '/imports/ui/services/storage/session';
import { callServer } from '/imports/ui/services/api'; import { callServer } from '/imports/ui/services/api';
import _ from 'lodash';
const CHAT_CONFIG = Meteor.settings.public.chat; const CHAT_CONFIG = Meteor.settings.public.chat;
const GROUPING_MESSAGES_WINDOW = CHAT_CONFIG.grouping_messages_window; 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); 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 */ /* TODO: Same map is done in the user-list/service we should share this someway */
const mapUser = (user) => ({ const mapUser = (user) => ({
@ -193,6 +198,13 @@ const sendMessage = (receiverID, message) => {
from_color: 0, 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); callServer('sendChat', messagePayload);
return messagePayload; return messagePayload;
@ -215,6 +227,17 @@ const updateUnreadMessage = (receiverID, timestamp) => {
return UnreadMessages.update(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 { export default {
getPublicMessages, getPublicMessages,
getPrivateMessages, getPrivateMessages,
@ -226,4 +249,5 @@ export default {
updateScrollPosition, updateScrollPosition,
updateUnreadMessage, updateUnreadMessage,
sendMessage, sendMessage,
closePrivateChat,
}; };

View File

@ -6,23 +6,38 @@
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;
flex-direction: column; flex-direction: column;
justify-content: space-around;
overflow: hidden; overflow: hidden;
} }
.closeChat {
text-decoration: none;
}
.header { .header {
margin-top: $lg-padding-x - $sm-padding-x; margin-top: $lg-padding-x - $sm-padding-x;
margin-bottom: $lg-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-"],
> [class*=" icon-bbb-"] { > [class*=" icon-bbb-"] {
font-size: 85%; font-size: 85%;
} }
} }
[class='icon-bbb-left_arrow'] { .closeIcon {
padding-bottom: 5px; 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 styles from './styles';
import DropdownTrigger from './trigger/component'; import DropdownTrigger from './trigger/component';
import DropdownContent from './content/component'; import DropdownContent from './content/component';
import Button from '/imports/ui/components/button/component';
import cx from 'classnames'; import cx from 'classnames';
import { defineMessages, injectIntl } from 'react-intl';
const FOCUSABLE_CHILDREN = `[tabindex]:not([tabindex="-1"]), a, input, button`; const FOCUSABLE_CHILDREN = `[tabindex]:not([tabindex="-1"]), a, input, button`;
const intlMessages = defineMessages({
close: {
id: 'app.dropdown.close',
defaultMessage: 'Close',
},
});
const propTypes = { const propTypes = {
/** /**
* The dropdown needs a trigger and a content component as childrens * The dropdown needs a trigger and a content component as childrens
@ -44,7 +53,7 @@ const defaultProps = {
isOpen: false, isOpen: false,
}; };
export default class Dropdown extends Component { class Dropdown extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { isOpen: false, }; this.state = { isOpen: false, };
@ -113,7 +122,7 @@ export default class Dropdown extends Component {
} }
render() { render() {
const { children, className, style } = this.props; const { children, className, style, intl } = this.props;
let trigger = children.find(x => x.type === DropdownTrigger); let trigger = children.find(x => x.type === DropdownTrigger);
let content = children.find(x => x.type === DropdownContent); 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)}> <div style={style} className={cx(styles.dropdown, className)}>
{trigger} {trigger}
{content} {content}
{ this.state.isOpen ?
<Button
className={styles.close}
label={intl.formatMessage(intlMessages.close)}
size={'lg'}
color={'default'}
onClick={this.handleHide}
/> : null }
</div> </div>
); );
} }
@ -144,3 +161,4 @@ export default class Dropdown extends Component {
Dropdown.propTypes = propTypes; Dropdown.propTypes = propTypes;
Dropdown.defaultProps = defaultProps; Dropdown.defaultProps = defaultProps;
export default injectIntl(Dropdown);

View File

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

View File

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

View File

@ -10,6 +10,11 @@
padding: ($line-height-computed / 2); padding: ($line-height-computed / 2);
display: flex; display: flex;
flex-direction: column; 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; align-items: center;
justify-content: flex-start; justify-content: flex-start;
@include mq($small-only) {
padding: ($line-height-computed / 1.5) 0;
justify-content: center;
}
&:first-child { &:first-child {
padding-top: 0; padding-top: 0;
} }

View File

@ -1,4 +1,5 @@
@import "../../stylesheets/variables/_all"; @import "../../stylesheets/variables/_all";
@import "../../stylesheets/mixins/_scrollable";
$dropdown-bg: $color-white; $dropdown-bg: $color-white;
$dropdown-color: $color-text; $dropdown-color: $color-text;
@ -18,7 +19,6 @@ $dropdown-caret-height: 8px;
box-shadow: 0 6px 12px rgba(0, 0, 0, .175); box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
border: 1px solid rgba(0, 0, 0, .15); border: 1px solid rgba(0, 0, 0, .15);
padding: $line-height-computed / 2; padding: $line-height-computed / 2;
// min-width: 150px;
z-index: 1000; z-index: 1000;
&:after, &:before { &:after, &:before {
@ -35,10 +35,55 @@ $dropdown-caret-height: 8px;
&[aria-expanded="true"] { &[aria-expanded="true"] {
display: block; 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 {} .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 /* 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 React, { Component } from 'react';
import { withRouter } from 'react-router';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import Auth from '/imports/ui/services/auth';
import Modal from '/imports/ui/components/modal/component'; 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({ const intlMessages = defineMessages({
title: { title: {
@ -33,33 +31,19 @@ const intlMessages = defineMessages({
}); });
class LeaveConfirmation extends Component { 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() { render() {
const { intl } = this.props; const { intl, router } = this.props;
return ( return (
<Modal <Modal
title={intl.formatMessage(intlMessages.title)} title={intl.formatMessage(intlMessages.title)}
confirm={{ confirm={{
callback: this.handleLeaveConfirmation, callback: () => router.push('/logout'),
label: intl.formatMessage(intlMessages.confirmLabel), label: intl.formatMessage(intlMessages.confirmLabel),
description: intl.formatMessage(intlMessages.confirmDesc), description: intl.formatMessage(intlMessages.confirmDesc),
}} }}
dismiss={{ dismiss={{
callback: this.handleCancleLogout, callback: () => null,
label: intl.formatMessage(intlMessages.dismissLabel), label: intl.formatMessage(intlMessages.dismissLabel),
description: intl.formatMessage(intlMessages.dismissDesc), 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() { handleDismiss() {
const { dismiss } = this.props; const { dismiss } = this.props;
dismiss.callback(...arguments); if (dismiss && dismiss.callback) {
dismiss.callback(...arguments);
}
this.setState({ isOpen: false }); this.setState({ isOpen: false });
clearModal(); clearModal();
} }

View File

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

View File

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

View File

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

View File

@ -53,44 +53,16 @@ const intlMessages = defineMessages({
id: 'app.navBar.settingsDropdown.leaveSessionDesc', id: 'app.navBar.settingsDropdown.leaveSessionDesc',
defaultMessage: 'Leave the meeting', 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 openSettings = () => showModal(<SettingsMenuContainer />);
const openAbout = () => showModal(<AboutContainer />); const openAbout = () => showModal(<AboutContainer />);
@ -103,7 +75,17 @@ class SettingsDropdown extends Component {
} }
render() { 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 ( return (
<Dropdown ref="dropdown"> <Dropdown ref="dropdown">
<DropdownTrigger> <DropdownTrigger>
@ -124,9 +106,9 @@ class SettingsDropdown extends Component {
<DropdownList> <DropdownList>
<DropdownListItem <DropdownListItem
icon="fullscreen" icon="fullscreen"
label={intl.formatMessage(intlMessages.fullscreenLabel)} label={fullScreenLabel}
description={intl.formatMessage(intlMessages.fullscreenDesc)} description={fullScreenDesc}
onClick={toggleFullScreen.bind(this)} onClick={this.props.handleToggleFullscreen}
/> />
<DropdownListItem <DropdownListItem
icon="more" 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 { createContainer } from 'meteor/react-meteor-data';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import _ from 'underscore'; import _ from 'lodash';
import NavBarService from '../nav-bar/service'; import NavBarService from '../nav-bar/service';
import Auth from '/imports/ui/services/auth'; import Auth from '/imports/ui/services/auth';
import { humanizeSeconds } from '/imports/utils/humanizeSeconds'; import { humanizeSeconds } from '/imports/utils/humanizeSeconds';

View File

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

View File

@ -1,5 +1,4 @@
import React, { Component, PropTypes } from 'react'; import React, { Component } from 'react';
import { FormattedMessage } from 'react-intl';
import Modal from '/imports/ui/components/modal/component'; import Modal from '/imports/ui/components/modal/component';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; 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 Participants from '/imports/ui/components/settings/submenus/participants/component';
import Video from '/imports/ui/components/settings/submenus/video/component'; import Video from '/imports/ui/components/settings/submenus/video/component';
import Button from '../button/component';
import Icon from '../icon/component'; import Icon from '../icon/component';
import styles from './styles'; import styles from './styles';
import cx from 'classnames';
const propTypes = { const propTypes = {
}; };

View File

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

View File

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

View File

@ -56,3 +56,13 @@
padding-top: 0; padding-top: 0;
padding-right: 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 BaseMenu from '../base/component';
import styles from '../styles.scss'; import styles from '../styles.scss';
@ -46,7 +46,7 @@ export default class AudioMenu extends BaseMenu {
render() { render() {
return ( return (
<div className={styles.tabContent}> <div>
<div className={styles.header}> <div className={styles.header}>
<h3 className={styles.title}>Audio</h3> <h3 className={styles.title}>Audio</h3>
</div> </div>

View File

@ -2,15 +2,20 @@ import Users from '/imports/api/users';
import Chat from '/imports/api/chat'; import Chat from '/imports/api/chat';
import Auth from '/imports/ui/services/auth'; import Auth from '/imports/ui/services/auth';
import UnreadMessages from '/imports/ui/services/unread-messages'; 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 { EMOJI_STATUSES } from '/imports/utils/statuses.js';
import { callServer } from '/imports/ui/services/api'; import { callServer } from '/imports/ui/services/api';
import _ from 'lodash';
const CHAT_CONFIG = Meteor.settings.public.chat; const CHAT_CONFIG = Meteor.settings.public.chat;
const USER_CONFIG = Meteor.settings.public.user; const USER_CONFIG = Meteor.settings.public.user;
const ROLE_MODERATOR = USER_CONFIG.role_moderator; const ROLE_MODERATOR = USER_CONFIG.role_moderator;
const PRIVATE_CHAT_TYPE = CHAT_CONFIG.type_private; 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 */ /* TODO: Same map is done in the chat/service we should share this someway */
const mapUser = user => ({ const mapUser = user => ({
@ -164,13 +169,12 @@ const getUsers = () => {
.fetch(); .fetch();
return users return users
.map(u => u.user) .map(u => u.user)
.map(mapUser) .map(mapUser)
.sort(sortUsers); .sort(sortUsers);
}; };
const getOpenChats = chatID => { const getOpenChats = chatID => {
window.Users = Users;
let openChats = Chat let openChats = Chat
.find({ 'message.chat_type': PRIVATE_CHAT_TYPE }) .find({ 'message.chat_type': PRIVATE_CHAT_TYPE })
@ -178,6 +182,7 @@ const getOpenChats = chatID => {
.map(mapOpenChats); .map(mapOpenChats);
let currentUserId = Auth.userID; let currentUserId = Auth.userID;
if (chatID) { if (chatID) {
openChats.push(chatID); openChats.push(chatID);
} }
@ -193,6 +198,28 @@ const getOpenChats = chatID => {
return op; 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({ openChats.push({
id: 'public', id: 'public',
name: 'Public Chat', name: 'Public Chat',

View File

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

View File

@ -8,7 +8,7 @@ function callServer(name) {
return false; 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. // slice off the first element. That is the function name but we already have that.
const args = Array.prototype.slice.call(arguments, 1); 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 Storage from '/imports/ui/services/storage/session';
import Users from '/imports/api/users';
import { callServer } from '/imports/ui/services/api'; import { callServer } from '/imports/ui/services/api';
class Auth { class Auth {
@ -6,11 +10,10 @@ class Auth {
this._meetingID = Storage.getItem('meetingID'); this._meetingID = Storage.getItem('meetingID');
this._userID = Storage.getItem('userID'); this._userID = Storage.getItem('userID');
this._authToken = Storage.getItem('authToken'); this._authToken = Storage.getItem('authToken');
this._logoutURL = Storage.getItem('logoutURL'); this._loggedIn = {
value: false,
if (!this._logoutURL) { tracker: new Tracker.Dependency,
this._setLogOut(); };
}
} }
get meetingID() { get meetingID() {
@ -40,22 +43,17 @@ class Auth {
Storage.setItem('authToken', this._authToken); Storage.setItem('authToken', this._authToken);
} }
get logoutURL() { get loggedIn() {
return this._logoutURL; this._loggedIn.tracker.depend();
return this._loggedIn.value;
} }
set logoutURL(logoutURL) { set loggedIn(value) {
this._logoutURL = logoutURL; this._loggedIn.value = value;
Storage.setItem('logoutURL', this._logoutURL); this._loggedIn.tracker.changed();
} }
setCredentials(meeting, user, token) { get credentials() {
this.meetingID = meeting;
this.userID = user;
this.token = token;
}
getCredentials() {
return { return {
meetingId: this.meetingID, meetingId: this.meetingID,
requesterUserId: this.userID, requesterUserId: this.userID,
@ -63,49 +61,114 @@ class Auth {
}; };
} }
clearCredentials(callback) { set credentials(value) {
throw 'Credentials are read-only';
}
clearCredentials() {
this.meetingID = null; this.meetingID = null;
this.userID = null; this.userID = null;
this.token = null; this.token = null;
this.loggedIn = false;
if (typeof callback === 'function') { return Promise.resolve(...arguments);
return callback(); };
logout() {
if (!this.loggedIn) {
return Promise.resolve();
} }
};
completeLogout() { return new Promise((resolve, reject) => {
let logoutURL = this.logoutURL; callServer('userLogout', () => {
callServer('userLogout'); this.fetchLogoutUrl()
.then(this.clearCredentials)
this.clearCredentials(() => { .then(resolve);
document.location.href = logoutURL; });
}); });
}; };
_setLogOut() { authenticate(meetingID, userID, token) {
let request; if (arguments.length) {
let handleLogoutUrlError; this.meetingID = meetingID;
this.userID = userID;
this.token = token;
}
handleLogoutUrlError = function () { return this._subscribeToCurrentUser()
console.log('Error : could not find the logoutURL'); .then(this._addObserverToValidatedField.bind(this));
this.logoutURL = document.location.hostname; }
};
// obtain the logoutURL _subscribeToCurrentUser() {
request = $.ajax({ const credentials = this.credentials;
dataType: 'json',
url: '/bigbluebutton/api/enter', 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 => { _addObserverToValidatedField(prevComp) {
if (data.response.logoutURL != null) { return new Promise((resolve, reject) => {
this.logoutURL = data.response.logoutURL; const validationTimeout = setTimeout(() => {
} else { this.clearCredentials();
return handleLogoutUrlError(); 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 { Tracker } from 'meteor/tracker';
import { EJSON } from 'meteor/ejson'; import { EJSON } from 'meteor/ejson';

View File

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

View File

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

View File

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

View File

@ -12,7 +12,6 @@
"app.chat.titlePrivate": "Private Chat with {name}", "app.chat.titlePrivate": "Private Chat with {name}",
"app.chat.partnerDisconnected": "{name} has left the meeting", "app.chat.partnerDisconnected": "{name} has left the meeting",
"app.chat.moreMessages": "More messages below", "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.prevSlideLabel": "Previous slide",
"app.presentation.presentationToolbar.prevSlideDescrip": "Change the presentation to the previous slide", "app.presentation.presentationToolbar.prevSlideDescrip": "Change the presentation to the previous slide",
"app.presentation.presentationToolbar.nextSlideLabel": "Next slide", "app.presentation.presentationToolbar.nextSlideLabel": "Next slide",
@ -37,6 +36,8 @@
"app.navBar.settingsDropdown.settingsDesc": "Change the general settings", "app.navBar.settingsDropdown.settingsDesc": "Change the general settings",
"app.navBar.settingsDropdown.aboutDesc": "Show information about the client", "app.navBar.settingsDropdown.aboutDesc": "Show information about the client",
"app.navBar.settingsDropdown.leaveSessionDesc": "Leave the meeting", "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.title": "Leave Session",
"app.leaveConfirmation.message": "Do you want to leave this meeting?", "app.leaveConfirmation.message": "Do you want to leave this meeting?",
"app.leaveConfirmation.confirmLabel": "Leave", "app.leaveConfirmation.confirmLabel": "Leave",
@ -109,5 +110,11 @@
"app.audio.audioSettings.speakerSourceLabel": "Speaker source", "app.audio.audioSettings.speakerSourceLabel": "Speaker source",
"app.audio.audioSettings.microphoneStreamLabel": "Your audio stream volume", "app.audio.audioSettings.microphoneStreamLabel": "Your audio stream volume",
"app.audio.listenOnly.backLabel": "Back", "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"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
<metadata> <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 By Ghazi
Copyright (c) 2016, BlindSide Networks Inc. Copyright (c) 2016, BlindSide Networks Inc.
</metadata> </metadata>
@ -19,7 +19,7 @@ Copyright (c) 2016, BlindSide Networks Inc.
panose-1="2 0 5 3 0 0 0 0 0 0" panose-1="2 0 5 3 0 0 0 0 0 0"
ascent="819" ascent="819"
descent="-205" descent="-205"
bbox="0 -205 1212 820" bbox="0 -205 1024 820"
underline-thickness="51" underline-thickness="51"
underline-position="-102" underline-position="-102"
unicode-range="U+0020-E932" 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="space" unicode=" " horiz-adv-x="524"
/> />
<glyph glyph-name="logout" unicode="&#xe900;" <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 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 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" /> 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;" <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 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" /> c-61 0 -110 49 -110 109z" />
<glyph glyph-name="promote" unicode="&#xe903;" horiz-adv-x="1028" <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 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 577 -139 864 -208z" /> 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;" <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 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 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" /> v-465zM642 74v318l-314 -318h314z" />
<glyph glyph-name="user" unicode="&#xe905;" <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" /> 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;" <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 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 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" /> 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;" <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 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-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 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 100 -73 189 -177 207v-420z" /> c0 81 -60 156 -146 171v-351z" />
<glyph glyph-name="left_arrow" unicode="&#xe90d;" <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" /> 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;" <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