Merge branch 'master' of github.com:bigbluebutton/bigbluebutton into perroned-merge-webrtc-screenshare-2
This commit is contained in:
commit
5e0a5f019c
@ -15,6 +15,14 @@ ApplicationControlBar {
|
||||
dropShadowColor: #000000;
|
||||
}
|
||||
|
||||
.defaultControlBarStyle {
|
||||
color : #0B333C;
|
||||
}
|
||||
|
||||
.darkControlBarStyle {
|
||||
color : #ffffff;
|
||||
}
|
||||
|
||||
Panel {
|
||||
borderColor: #dfdfdf;
|
||||
borderAlpha: 1;
|
||||
@ -755,6 +763,18 @@ MDIWindow { /*None of the following properties are overridden by the MDIWindow c
|
||||
borderThicknessRight: 3;
|
||||
}
|
||||
|
||||
.videoDockStyleFocusChatLayout {
|
||||
borderStyle : none;
|
||||
borderColor: #42444c;
|
||||
backgroundColor: #42444c;
|
||||
}
|
||||
|
||||
.videoDockStyleNoFocusChatLayout {
|
||||
borderStyle : none;
|
||||
borderColor: #42444c;
|
||||
backgroundColor: #42444c;
|
||||
}
|
||||
|
||||
.presentationSlideViewStyle {
|
||||
backgroundColor: #b9babc;
|
||||
}
|
||||
@ -865,6 +885,14 @@ MDIWindow { /*None of the following properties are overridden by the MDIWindow c
|
||||
borderThicknessRight: 3;
|
||||
}
|
||||
|
||||
.defaultShellStyle {
|
||||
backgroundColor: #fefeff;
|
||||
}
|
||||
|
||||
.darkShellStyle {
|
||||
backgroundColor: #42444c;
|
||||
}
|
||||
|
||||
.mdiWindowTitle {
|
||||
color: #3f3f41;
|
||||
fontFamily: Arial;
|
||||
|
@ -489,14 +489,17 @@ bbb.accessibility.chat.initialDescription = Please use the arrow keys to navigat
|
||||
bbb.accessibility.notes.notesview.input = Notes input
|
||||
|
||||
bbb.shortcuthelp.title = Shortcut Keys
|
||||
bbb.shortcuthelp.titleBar = Shortcut Keys Window Title Bar
|
||||
bbb.shortcuthelp.minimizeBtn.accessibilityName = Minimize the Shortcut Help Window
|
||||
bbb.shortcuthelp.maximizeRestoreBtn.accessibilityName = Maximize the Shortcut Help Window
|
||||
bbb.shortcuthelp.closeBtn.accessibilityName = Close the Shortcut Help Window
|
||||
bbb.shortcuthelp.dropdown.accessibilityName = Shortcut Category
|
||||
bbb.shortcuthelp.dropdown.general = Global shortcuts
|
||||
bbb.shortcuthelp.dropdown.presentation = Presentation shortcuts
|
||||
bbb.shortcuthelp.dropdown.chat = Chat shortcuts
|
||||
bbb.shortcuthelp.dropdown.users = Users shortcuts
|
||||
bbb.shortcuthelp.dropdown.caption = Closed Caption shortcuts
|
||||
bbb.shortcuthelp.browserWarning.text = The full list of shortcuts are only supported in Internet Explorer.
|
||||
bbb.shortcuthelp.headers.shortcut = Shortcut
|
||||
bbb.shortcuthelp.headers.function = Function
|
||||
|
||||
@ -539,7 +542,7 @@ bbb.shortcutkey.logout.function = Log out of this meeting
|
||||
bbb.shortcutkey.raiseHand = 82
|
||||
bbb.shortcutkey.raiseHand.function = Raise your hand
|
||||
|
||||
bbb.shortcutkey.present.upload = 85
|
||||
bbb.shortcutkey.present.upload = 89
|
||||
bbb.shortcutkey.present.upload.function = Upload presentation
|
||||
bbb.shortcutkey.present.previous = 65
|
||||
bbb.shortcutkey.present.previous.function = Go to previous slide
|
||||
@ -549,19 +552,17 @@ bbb.shortcutkey.present.next = 69
|
||||
bbb.shortcutkey.present.next.function = Go to next slide
|
||||
bbb.shortcutkey.present.fitWidth = 70
|
||||
bbb.shortcutkey.present.fitWidth.function = Fit slides to width
|
||||
bbb.shortcutkey.present.fitPage = 80
|
||||
bbb.shortcutkey.present.fitPage = 82
|
||||
bbb.shortcutkey.present.fitPage.function = Fit slides to page
|
||||
|
||||
bbb.shortcutkey.users.makePresenter = 80
|
||||
bbb.shortcutkey.users.makePresenter = 89
|
||||
bbb.shortcutkey.users.makePresenter.function = Make selected person presenter
|
||||
bbb.shortcutkey.users.kick = 75
|
||||
bbb.shortcutkey.users.kick = 69
|
||||
bbb.shortcutkey.users.kick.function = Kick selected person from the meeting
|
||||
bbb.shortcutkey.users.mute = 83
|
||||
bbb.shortcutkey.users.mute.function = Mute or unmute selected person
|
||||
bbb.shortcutkey.users.muteall = 65
|
||||
bbb.shortcutkey.users.muteall.function = Mute or unmute all users
|
||||
bbb.shortcutkey.users.focusUsers = 85
|
||||
bbb.shortcutkey.users.focusUsers.function = Focus to users list
|
||||
bbb.shortcutkey.users.muteAllButPres = 65
|
||||
bbb.shortcutkey.users.muteAllButPres.function = Mute everyone but the Presenter
|
||||
bbb.shortcutkey.users.breakoutRooms = 75
|
||||
@ -570,12 +571,12 @@ bbb.shortcutkey.users.focusBreakoutRooms = 82
|
||||
bbb.shortcutkey.users.focusBreakoutRooms.function = Focus to breakout rooms list
|
||||
bbb.shortcutkey.users.listenToBreakoutRoom = 76
|
||||
bbb.shortcutkey.users.listenToBreakoutRoom.function = Listen to selected breakout room
|
||||
bbb.shortcutkey.users.joinBreakoutRoom = 74
|
||||
bbb.shortcutkey.users.joinBreakoutRoom = 79
|
||||
bbb.shortcutkey.users.joinBreakoutRoom.function = Join selected breakout room
|
||||
|
||||
bbb.shortcutkey.chat.focusTabs = 89
|
||||
bbb.shortcutkey.chat.focusTabs.function = Focus to chat tabs
|
||||
bbb.shortcutkey.chat.focusBox = 66
|
||||
bbb.shortcutkey.chat.focusBox = 77
|
||||
bbb.shortcutkey.chat.focusBox.function = Focus to chat box
|
||||
bbb.shortcutkey.chat.changeColour = 67
|
||||
bbb.shortcutkey.chat.changeColour.function = Focus to font color picker.
|
||||
|
@ -81,7 +81,7 @@ function determineGlobalAlternateModifier()
|
||||
// modifier = "control+alt";
|
||||
//}
|
||||
else{
|
||||
modifier = "control+shift";
|
||||
modifier = "control+shift+";
|
||||
}
|
||||
return modifier;
|
||||
}
|
||||
|
@ -23,8 +23,6 @@ package org.bigbluebutton.common
|
||||
import flexlib.mdi.containers.MDIWindow;
|
||||
import flexlib.mdi.managers.MDIManager;
|
||||
|
||||
import mx.utils.ObjectUtil;
|
||||
|
||||
/**
|
||||
* This class exists so we can properly handle context menus on MDIWindow
|
||||
* instances. Also, we'll be able in the future to properly handle shortcuts
|
||||
|
@ -38,6 +38,8 @@ package org.bigbluebutton.main.model.users {
|
||||
|
||||
public var users:ArrayCollection;
|
||||
|
||||
public var invitedRecently : Boolean;
|
||||
|
||||
// Can be one of three following values self, none, other
|
||||
public var listenStatus:String = NONE;
|
||||
|
||||
|
@ -30,7 +30,6 @@ package org.bigbluebutton.main.model.users {
|
||||
import org.as3commons.logging.api.getClassLogger;
|
||||
import org.bigbluebutton.common.Role;
|
||||
import org.bigbluebutton.core.BBB;
|
||||
import org.bigbluebutton.core.managers.UserManager;
|
||||
import org.bigbluebutton.core.model.Config;
|
||||
import org.bigbluebutton.core.model.MeetingModel;
|
||||
import org.bigbluebutton.core.vo.CameraSettingsVO;
|
||||
@ -568,7 +567,20 @@ package org.bigbluebutton.main.model.users {
|
||||
}
|
||||
breakoutRooms.addItem(newRoom);
|
||||
sortBreakoutRooms();
|
||||
}
|
||||
}
|
||||
|
||||
public function setLastBreakoutRoomInvitation(sequence:int):void {
|
||||
var aRoom:BreakoutRoom;
|
||||
for (var i:int = 0; i < breakoutRooms.length; i++) {
|
||||
aRoom = breakoutRooms.getItemAt(i) as BreakoutRoom;
|
||||
if (aRoom.sequence != sequence) {
|
||||
aRoom.invitedRecently = false;
|
||||
} else {
|
||||
aRoom.invitedRecently = true;
|
||||
}
|
||||
}
|
||||
sortBreakoutRooms();
|
||||
}
|
||||
|
||||
private function sortBreakoutRooms() : void {
|
||||
var sort:Sort = new Sort();
|
||||
|
@ -69,6 +69,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
<mate:Listener type="{BreakoutRoomEvent.OPEN_BREAKOUT_ROOMS_PANEL}" method="openBreakoutRoomsWindow" />
|
||||
<mate:Listener type="{InvalidAuthTokenEvent.INVALID_AUTH_TOKEN}" method="handleInvalidAuthToken" />
|
||||
|
||||
<mate:Listener type="{SwitchedLayoutEvent.SWITCHED_LAYOUT_EVENT}" method="onLayoutChanged" />
|
||||
|
||||
<mx:Script>
|
||||
<![CDATA[
|
||||
import com.asfusion.mate.events.Dispatcher;
|
||||
@ -101,6 +103,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
import org.bigbluebutton.core.PopUpUtil;
|
||||
import org.bigbluebutton.core.UsersUtil;
|
||||
import org.bigbluebutton.core.events.LockControlEvent;
|
||||
import org.bigbluebutton.core.events.SwitchedLayoutEvent;
|
||||
import org.bigbluebutton.core.managers.UserManager;
|
||||
import org.bigbluebutton.core.vo.LockSettingsVO;
|
||||
import org.bigbluebutton.main.events.AppVersionEvent;
|
||||
@ -680,6 +683,17 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
ExternalInterface.call("chatLinkClicked", e.text);
|
||||
}
|
||||
}
|
||||
|
||||
private function onLayoutChanged(e:SwitchedLayoutEvent):void {
|
||||
if (e.layoutID != "bbb.layout.name.videochat") {
|
||||
this.styleName = "defaultShellStyle";
|
||||
controlBar.styleName = "defaultControlBarStyle";
|
||||
} else {
|
||||
this.styleName = "darkShellStyle";
|
||||
controlBar.styleName = "darkControlBarStyle";
|
||||
}
|
||||
}
|
||||
|
||||
]]>
|
||||
</mx:Script>
|
||||
|
||||
|
@ -71,7 +71,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
private var chatResource:Array = ['bbb.shortcutkey.chat.focusTabs', 'bbb.shortcutkey.chat.focusBox', 'bbb.shortcutkey.chat.sendMessage',
|
||||
'bbb.shortcutkey.chat.closePrivate'];
|
||||
|
||||
private var userResource:Array = ['bbb.shortcutkey.users.focusUsers', 'bbb.shortcutkey.users.makePresenter', 'bbb.shortcutkey.users.mute',
|
||||
private var userResource:Array = ['bbb.shortcutkey.users.makePresenter', 'bbb.shortcutkey.users.mute',
|
||||
'bbb.shortcutkey.users.kick', 'bbb.shortcutkey.users.muteall', 'bbb.shortcutkey.users.focusBreakoutRooms',
|
||||
'bbb.shortcutkey.users.listenToBreakoutRoom', 'bbb.shortcutkey.users.joinBreakoutRoom'];
|
||||
|
||||
@ -99,14 +99,19 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
private function populateModules():void{
|
||||
categoryAC.addItem(generalString);
|
||||
if (ShortcutOptions.usersActive)
|
||||
categoryAC.addItem(userString);
|
||||
|
||||
if (ShortcutOptions.presentationActive)
|
||||
categoryAC.addItem(presentationString);
|
||||
if (Capabilities.playerType == "ActiveX") {
|
||||
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 {
|
||||
@ -249,10 +254,10 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
]]>
|
||||
</mx:Script>
|
||||
|
||||
<common:TabIndexer id="headerIndexer" startIndex="101" tabIndices="{[minimizeBtn, maximizeRestoreBtn, closeBtn]}"/>
|
||||
<common:TabIndexer startIndex="115" tabIndices="{[categories, keyList]}"/>
|
||||
<common:TabIndexer id="headerIndexer" startIndex="115" tabIndices="{[minimizeBtn, maximizeRestoreBtn, closeBtn]}"/>
|
||||
<common:TabIndexer startIndex="119" tabIndices="{[categories, keyList]}"/>
|
||||
|
||||
<mx:ComboBox id="categories" labelField="Please select an area for which to view shortcut keys: "
|
||||
<mx:ComboBox id="categories" accessibilityName="{ResourceUtil.getInstance().getString('bbb.shortcuthelp.dropdown.accessibilityName')}"
|
||||
editable="false"
|
||||
change="changeArray()">
|
||||
<mx:ArrayCollection id="categoryAC">
|
||||
@ -264,6 +269,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
</mx:ArrayCollection>
|
||||
</mx:ComboBox>
|
||||
<mx:Text id="browserWarningText" visible="false" includeInLayout="false" text="{ResourceUtil.getInstance().getString('bbb.shortcuthelp.browserWarning.text')}" />
|
||||
<mx:DataGrid id="keyList" draggableColumns="false" dataProvider="{shownKeys}" width="100%" height="100%">
|
||||
<mx:columns>
|
||||
<mx:DataGridColumn dataField="shortcut" width="150" headerText="{ResourceUtil.getInstance().getString('bbb.shortcuthelp.headers.shortcut')}"/>
|
||||
|
@ -235,7 +235,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
}
|
||||
|
||||
private function focusWindow(e:ShortcutEvent):void {
|
||||
focusManager.setFocus(titleBarOverlay);
|
||||
if (this.visible) {
|
||||
focusManager.setFocus(titleBarOverlay);
|
||||
}
|
||||
}
|
||||
]]>
|
||||
</mx:Script>
|
||||
|
@ -120,7 +120,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
}
|
||||
|
||||
private function focusWindow(e:ShortcutEvent):void{
|
||||
focusManager.setFocus(titleBarOverlay);
|
||||
if (this.visible) {
|
||||
focusManager.setFocus(titleBarOverlay);
|
||||
}
|
||||
}
|
||||
|
||||
private function fullScreenHandler(evt:FullScreenEvent):void {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -219,18 +219,18 @@ package org.bigbluebutton.modules.layout.managers
|
||||
var e:SyncLayoutEvent = new SyncLayoutEvent(layout);
|
||||
_globalDispatcher.dispatchEvent(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function applyLayout(layout:LayoutDefinition):void {
|
||||
_detectContainerChange = false;
|
||||
if (layout != null) {
|
||||
layout.applyToCanvas(_canvas);
|
||||
dispatchSwitchedLayoutEvent(layout.name);
|
||||
}
|
||||
//trace(LOG + " applyLayout layout [" + layout.name + "]");
|
||||
updateCurrentLayout(layout);
|
||||
_detectContainerChange = true;
|
||||
}
|
||||
private function applyLayout(layout:LayoutDefinition):void {
|
||||
_detectContainerChange = false;
|
||||
if (layout != null) {
|
||||
layout.applyToCanvas(_canvas);
|
||||
dispatchSwitchedLayoutEvent(layout.name);
|
||||
}
|
||||
//trace(LOG + " applyLayout layout [" + layout.name + "]");
|
||||
updateCurrentLayout(layout);
|
||||
_detectContainerChange = true;
|
||||
}
|
||||
|
||||
public function handleLockLayoutEvent(e: LockLayoutEvent):void {
|
||||
|
||||
@ -283,46 +283,47 @@ 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 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;
|
||||
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();
|
||||
}
|
||||
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 + "]");
|
||||
}
|
||||
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;
|
||||
}
|
||||
return _currentLayout;
|
||||
}
|
||||
}
|
||||
}
|
@ -246,7 +246,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
}
|
||||
|
||||
private function focusWindow(e:ShortcutEvent):void{
|
||||
focusManager.setFocus(titleBarOverlay);
|
||||
if (this.visible) {
|
||||
focusManager.setFocus(titleBarOverlay);
|
||||
}
|
||||
}
|
||||
|
||||
private function resizeHandler():void {
|
||||
|
@ -20,10 +20,11 @@ package org.bigbluebutton.modules.users.services
|
||||
{
|
||||
import com.asfusion.mate.events.Dispatcher;
|
||||
|
||||
import flash.utils.setTimeout;
|
||||
|
||||
import org.as3commons.lang.StringUtils;
|
||||
import org.as3commons.logging.api.ILogger;
|
||||
import org.as3commons.logging.api.getClassLogger;
|
||||
import org.bigbluebutton.common.Role;
|
||||
import org.bigbluebutton.core.BBB;
|
||||
import org.bigbluebutton.core.EventConstants;
|
||||
import org.bigbluebutton.core.UsersUtil;
|
||||
@ -649,11 +650,17 @@ package org.bigbluebutton.modules.users.services
|
||||
|
||||
private function handleBreakoutRoomJoinURL(msg:Object):void{
|
||||
var map:Object = JSON.parse(msg.msg);
|
||||
var externalMeetingId : String = StringUtils.substringBetween(map.redirectJoinURL, "meetingID=", "&");
|
||||
var breakoutRoom : BreakoutRoom = UserManager.getInstance().getConference().getBreakoutRoomByExternalId(externalMeetingId);
|
||||
var sequence : int = breakoutRoom.sequence;
|
||||
|
||||
var event : BreakoutRoomEvent = new BreakoutRoomEvent(BreakoutRoomEvent.BREAKOUT_JOIN_URL);
|
||||
event.joinURL = map.redirectJoinURL;
|
||||
var externalMeetingId : String = StringUtils.substringBetween(event.joinURL, "meetingID=", "&");
|
||||
event.breakoutMeetingSequence = UserManager.getInstance().getConference().getBreakoutRoomByExternalId(externalMeetingId).sequence;
|
||||
event.breakoutMeetingSequence = sequence;
|
||||
dispatcher.dispatchEvent(event);
|
||||
|
||||
// We delay assigning last room invitation sequence to be sure it is handle in time by the item renderer
|
||||
setTimeout(function() : void {UserManager.getInstance().getConference().setLastBreakoutRoomInvitation(sequence)}, 1000);
|
||||
}
|
||||
|
||||
private function handleUpdateBreakoutUsers(msg:Object):void{
|
||||
|
@ -155,7 +155,7 @@
|
||||
var ls:LockSettingsVO = UserManager.getInstance().getConference().getLockSettings();
|
||||
|
||||
if (data != null) {
|
||||
kickUserBtn.visible = !data.me && rolledOver && options.allowKickUser;
|
||||
kickUserBtn.visible = !data.me && rolledOver && options.allowKickUser && !UserManager.getInstance().getConference().isBreakout;
|
||||
|
||||
if (!data.voiceJoined) {
|
||||
if (data.listenOnly) {
|
||||
|
@ -21,7 +21,12 @@
|
||||
[Bindable]
|
||||
private var images:Images = new Images();
|
||||
|
||||
[Bindable]
|
||||
private var moderator:Boolean = false;
|
||||
|
||||
protected function onCreationCompleteHandler(event:FlexEvent):void {
|
||||
moderator = UserManager.getInstance().getConference().amIModerator();
|
||||
|
||||
this.addEventListener(FlexEvent.DATA_CHANGE, dataChangeHandler);
|
||||
}
|
||||
|
||||
@ -51,13 +56,19 @@
|
||||
]]>
|
||||
</mx:Script>
|
||||
|
||||
<mx:Button id="joinBtn" width="20" height="20"
|
||||
includeInLayout="{UserManager.getInstance().getConference().breakoutRoomsReady}" visible="{joinBtn.includeInLayout}"
|
||||
icon="{images.join}" toolTip="{ResourceUtil.getInstance().getString('bbb.users.roomsGrid.join')}"
|
||||
click="requestBreakoutJoinUrl(event)"/>
|
||||
<mx:Button id="listenBtn" toggle="true"
|
||||
width="20" height="20"
|
||||
visible="{data.listenStatus != BreakoutRoom.OTHER && UserManager.getInstance().getConference().voiceJoined || data.listenStatus == BreakoutRoom.SELF}" includeInLayout="{listenBtn.visible}"
|
||||
icon="{images.transfer}" toolTip="{ResourceUtil.getInstance().getString('bbb.users.roomsGrid.transfer')}"
|
||||
click="listenToBreakoutRoom(event)"/>
|
||||
<mx:Button id="joinBtn"
|
||||
width="20"
|
||||
height="20"
|
||||
icon="{images.join}"
|
||||
visible="{(UserManager.getInstance().getConference().breakoutRoomsReady && moderator) || (!moderator && data.invitedRecently)}"
|
||||
toolTip="{ResourceUtil.getInstance().getString('bbb.users.roomsGrid.join')}"
|
||||
click="requestBreakoutJoinUrl(event)" />
|
||||
<mx:Button id="listenBtn"
|
||||
toggle="true"
|
||||
width="20"
|
||||
height="20"
|
||||
visible="{moderator && data.listenStatus != BreakoutRoom.OTHER && UserManager.getInstance().getConference().voiceJoined || data.listenStatus == BreakoutRoom.SELF}"
|
||||
icon="{images.transfer}"
|
||||
toolTip="{ResourceUtil.getInstance().getString('bbb.users.roomsGrid.transfer')}"
|
||||
click="listenToBreakoutRoom(event)" />
|
||||
</mx:HBox>
|
||||
|
27
bigbluebutton-client/src/org/bigbluebutton/modules/users/views/UsersWindow.mxml
Normal file → Executable file
27
bigbluebutton-client/src/org/bigbluebutton/modules/users/views/UsersWindow.mxml
Normal file → Executable file
@ -128,7 +128,6 @@
|
||||
|
||||
private var joinAlert : Alert;
|
||||
|
||||
private const FOCUS_USERS_LIST:String = "Focus Users List";
|
||||
private const MAKE_PRESENTER:String = "Make Presenter";
|
||||
private const KICK_USER:String = "Kick User";
|
||||
private const MUTE_USER:String = "Mute User";
|
||||
@ -440,9 +439,10 @@
|
||||
|
||||
private function loadKeyCombos(modifier:String):void {
|
||||
keyCombos = new Object(); // always start with a fresh array bbb.shortcutkey.users.muteall
|
||||
keyCombos[modifier + (ResourceUtil.getInstance().getString('bbb.shortcutkey.users.focusUsers') as String)] = FOCUS_USERS_LIST;
|
||||
keyCombos[modifier + (ResourceUtil.getInstance().getString('bbb.shortcutkey.users.makePresenter') as String)] = MAKE_PRESENTER;
|
||||
keyCombos[modifier + (ResourceUtil.getInstance().getString('bbb.shortcutkey.users.kick') as String)] = KICK_USER;
|
||||
if (!UserManager.getInstance().getConference().isBreakout) {
|
||||
keyCombos[modifier + (ResourceUtil.getInstance().getString('bbb.shortcutkey.users.kick') as String)] = KICK_USER;
|
||||
}
|
||||
keyCombos[modifier + (ResourceUtil.getInstance().getString('bbb.shortcutkey.users.mute') as String)] = MUTE_USER;
|
||||
keyCombos[modifier + (ResourceUtil.getInstance().getString('bbb.shortcutkey.users.muteall') as String)] = MUTE_ALL_USER;
|
||||
keyCombos[modifier + (ResourceUtil.getInstance().getString('bbb.shortcutkey.users.focusBreakoutRooms') as String)] = FOCUS_BREAKOUT_ROOMS_LIST;
|
||||
@ -459,9 +459,6 @@
|
||||
var keyPress:String = KeyboardUtil.buildPressedKeys(e);
|
||||
if (keyCombos[keyPress]) {
|
||||
switch (keyCombos[keyPress]) {
|
||||
case FOCUS_USERS_LIST:
|
||||
remoteFocusUsers();
|
||||
break;
|
||||
case MAKE_PRESENTER:
|
||||
remoteMakePresenter();
|
||||
break;
|
||||
@ -498,7 +495,9 @@
|
||||
}
|
||||
|
||||
private function focusWindow(e:ShortcutEvent):void {
|
||||
focusManager.setFocus(titleBarOverlay);
|
||||
if (this.visible) {
|
||||
focusManager.setFocus(titleBarOverlay);
|
||||
}
|
||||
}
|
||||
|
||||
public function remoteRaiseHand(e:ShortcutEvent):void{
|
||||
@ -536,7 +535,7 @@
|
||||
}
|
||||
|
||||
public function remoteKickUser():void {
|
||||
if (amIModerator && usersGrid.selectedIndex != -1 && partOptions.allowKickUser) {
|
||||
if (amIModerator && usersGrid.selectedIndex != -1 && partOptions.allowKickUser && !UserManager.getInstance().getConference().isBreakout) {
|
||||
var selData:Object = usersGrid.selectedItem;
|
||||
|
||||
if (!selData.me)
|
||||
@ -557,13 +556,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
public function remoteFocusUsers():void {
|
||||
focusManager.setFocus(usersGrid);
|
||||
usersGrid.drawFocus(true);
|
||||
}
|
||||
|
||||
public function remoteFocusBreakoutRooms() : void {
|
||||
if (roomsGrid && roomsGrid.visible) {
|
||||
if (roomsGrid && roomsBox.visible) {
|
||||
focusManager.setFocus(roomsGrid);
|
||||
roomsGrid.drawFocus(true);
|
||||
}
|
||||
@ -636,8 +630,8 @@
|
||||
</views:BBBDataGrid>
|
||||
|
||||
<mx:VBox id="roomsBox" styleName="breakoutRoomsBox"
|
||||
visible="{breakoutRoomsList.length > 0 && amIModerator}"
|
||||
includeInLayout="{breakoutRoomsList.length > 0 && amIModerator}"
|
||||
visible="{breakoutRoomsList.length > 0}"
|
||||
includeInLayout="{breakoutRoomsList.length > 0}"
|
||||
horizontalScrollPolicy="off"
|
||||
width="100%" height="180">
|
||||
<mx:HBox width="100%">
|
||||
@ -658,7 +652,6 @@
|
||||
showDataTips="true"
|
||||
headerText="{ResourceUtil.getInstance().getString('bbb.users.roomsGrid.users')}"/>
|
||||
<mx:DataGridColumn dataField="meetingId"
|
||||
visible="{amIModerator}"
|
||||
headerText="{ResourceUtil.getInstance().getString('bbb.users.roomsGrid.action')}"
|
||||
itemRenderer="org.bigbluebutton.modules.users.views.RoomActionsRenderer"/>
|
||||
</views:columns>
|
||||
|
@ -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>
|
@ -33,6 +33,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
creationComplete="onCreationComplete()">
|
||||
|
||||
<mate:Listener type="{ShortcutEvent.FOCUS_VIDEO_WINDOW}" method="focusWindow" />
|
||||
<mate:Listener type="{SwitchedLayoutEvent.SWITCHED_LAYOUT_EVENT}" method="onLayoutChanged" />
|
||||
|
||||
<mx:Script>
|
||||
<![CDATA[
|
||||
@ -41,6 +42,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
import mx.core.UIComponent;
|
||||
|
||||
import org.bigbluebutton.core.KeyboardUtil;
|
||||
import org.bigbluebutton.core.events.SwitchedLayoutEvent;
|
||||
import org.bigbluebutton.main.events.ShortcutEvent;
|
||||
import org.bigbluebutton.main.views.MainCanvas;
|
||||
import org.bigbluebutton.modules.videoconf.model.VideoConfOptions;
|
||||
@ -50,6 +52,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
private var keyCombos:Object;
|
||||
private var disp:Dispatcher = new Dispatcher();
|
||||
|
||||
private var darkMode:Boolean;
|
||||
|
||||
private function onCreationComplete():void {
|
||||
hotkeyCapture();
|
||||
titleBarOverlay.tabIndex = videoOptions.baseTabIndex;
|
||||
@ -89,7 +93,11 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
override protected function resourcesChanged():void {
|
||||
super.resourcesChanged();
|
||||
this.title = ResourceUtil.getInstance().getString("bbb.videodock.title");
|
||||
if (!darkMode) {
|
||||
this.title = ResourceUtil.getInstance().getString("bbb.videodock.title");
|
||||
} else {
|
||||
this.title = "";
|
||||
}
|
||||
|
||||
if (titleBarOverlay != null) {
|
||||
titleBarOverlay.accessibilityName = ResourceUtil.getInstance().getString('bbb.videoDock.titleBar');
|
||||
@ -112,7 +120,24 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
}
|
||||
|
||||
private function focusWindow(e:ShortcutEvent):void {
|
||||
focusManager.setFocus(titleBarOverlay);
|
||||
if (this.visible) {
|
||||
focusManager.setFocus(titleBarOverlay);
|
||||
}
|
||||
}
|
||||
|
||||
private function onLayoutChanged(e:SwitchedLayoutEvent):void {
|
||||
if(e.layoutID != "bbb.layout.name.videochat"){
|
||||
setStyle("styleNameFocus", "videoDockStyleFocus");
|
||||
setStyle("styleNameNoFocus", "videoDockStyleNoFocus");
|
||||
showControls = true;
|
||||
this.title = ResourceUtil.getInstance().getString("bbb.videodock.title");
|
||||
} else {
|
||||
setStyle("styleNameFocus", "videoDockStyleFocusChatLayout");
|
||||
setStyle("styleNameNoFocus", "videoDockStyleNoFocusChatLayout");
|
||||
showControls = false;
|
||||
this.title = "";
|
||||
}
|
||||
styleChanged("styleName");
|
||||
}
|
||||
|
||||
private function remoteMinimize(e:ShortcutEvent):void {
|
||||
|
@ -9,7 +9,6 @@ arunoda:npm@0.2.6
|
||||
amplify
|
||||
blaze@2.1.8
|
||||
francocatena:status
|
||||
mrt:external-file-loader@0.1.4
|
||||
mizzao:timesync
|
||||
clinical:nightwatch
|
||||
cfs:power-queue
|
||||
|
@ -56,7 +56,6 @@ modules@0.7.9
|
||||
modules-runtime@0.7.9
|
||||
mongo@1.1.15
|
||||
mongo-id@1.0.6
|
||||
mrt:external-file-loader@0.1.4
|
||||
nathantreid:css-modules@2.4.0
|
||||
npm-mongo@2.2.16_1
|
||||
observe-sequence@1.0.15
|
||||
|
@ -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;
|
||||
}
|
||||
*/
|
@ -1,7 +1,55 @@
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BBB - HTML5 Client</title>
|
||||
<style>
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
*, *:before, *:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Source Sans Pro', Arial, sans-serif;
|
||||
font-size: 1rem; /* 16px */
|
||||
background-color: #2A2D33;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0,0,0,0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
[hidden]:not([hidden="false"]) {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<body style="background-color: #2A2D33">
|
||||
<div id="app" role="application"></div>
|
||||
<script src="/client/lib/sip.js"></script>
|
||||
<script src="/client/lib/bbb_webrtc_bridge_sip.js"></script>
|
||||
<script src="/client/lib/bbblogger.js"></script>
|
||||
<script src="/client/lib/jquery.json-2.4.min.js"></script>
|
||||
<script src="/client/lib/jquery.FSRTC.js"></script>
|
||||
<script src="/client/lib/jquery.verto.js"></script>
|
||||
<script src="/client/lib/verto_extension.js"></script>
|
||||
<script src="/client/lib/jquery.jsonrpcclient.js"></script>
|
||||
</body>
|
||||
|
@ -1,90 +1,9 @@
|
||||
import React from 'react';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { render } from 'react-dom';
|
||||
import { showModal } from '/imports/ui/components/app/service';
|
||||
import { renderRoutes } from '../imports/startup/client/routes.js';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import Singleton from '/imports/ui/services/storage/local.js';
|
||||
import AudioModalContainer from '/imports/ui/components/audio-modal/container';
|
||||
|
||||
function loadUserSettings() {
|
||||
const userSavedFontSize = Singleton.getItem('bbbSavedFontSizePixels');
|
||||
|
||||
if (userSavedFontSize) {
|
||||
document.getElementsByTagName('html')[0].style.fontSize = userSavedFontSize;
|
||||
}
|
||||
}
|
||||
|
||||
function setAudio() {
|
||||
const LOG_CONFIG = Meteor.settings || {};
|
||||
let autoJoinAudio = LOG_CONFIG.public.app.autoJoinAudio;
|
||||
|
||||
if (autoJoinAudio) {
|
||||
showModal( <AudioModalContainer /> );
|
||||
}
|
||||
}
|
||||
|
||||
function setMessages(data) {
|
||||
let messages = data;
|
||||
let defaultLocale = 'en';
|
||||
|
||||
render((
|
||||
<IntlProvider locale={defaultLocale} messages={messages}>
|
||||
{renderRoutes()}
|
||||
</IntlProvider>
|
||||
), document.getElementById('app'));
|
||||
|
||||
setAudio();
|
||||
}
|
||||
|
||||
// Helper to load javascript libraries from the BBB server
|
||||
function loadLib(libname, success, fail) {
|
||||
const successCallback = function (cb) {
|
||||
console.log(`successfully loaded lib - ${this}`);
|
||||
if (typeof (cb) == 'function' || cb instanceof Function) {
|
||||
cb();
|
||||
}
|
||||
};
|
||||
|
||||
const failCallback = function (cb, issue) {
|
||||
console.error(`failed to load lib - ${this}`);
|
||||
console.error(issue);
|
||||
if (typeof (cb) == 'function' || cb instanceof Function) {
|
||||
cb();
|
||||
}
|
||||
};
|
||||
|
||||
return Meteor.Loader.loadJs(`${window.location.origin}/client/lib/${libname}`,
|
||||
successCallback.bind(libname, success), 10000).fail(failCallback.bind(libname, fail));
|
||||
};
|
||||
import { renderRoutes } from '/imports/startup/client/routes';
|
||||
|
||||
Meteor.startup(() => {
|
||||
|
||||
loadLib('sip.js');
|
||||
loadLib('bbb_webrtc_bridge_sip.js');
|
||||
loadLib('bbblogger.js');
|
||||
loadLib('jquery.json-2.4.min.js');
|
||||
loadLib('jquery.FSRTC.js');
|
||||
loadLib('jquery.verto.js');
|
||||
loadLib('verto_extension.js');
|
||||
loadLib('jquery.jsonrpcclient.js');
|
||||
|
||||
loadUserSettings();
|
||||
|
||||
let browserLanguage = navigator.language; //set this manually to force localization in a specific language
|
||||
let request = new Request
|
||||
(`${window.location.origin}/html5client/locale?locale=${browserLanguage}`);
|
||||
|
||||
fetch(request, { method: 'GET' })
|
||||
.then(function (response) {
|
||||
return response.json();
|
||||
})
|
||||
.then(function (data) {
|
||||
setMessages(data);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
console.log('request failed', err);
|
||||
});
|
||||
|
||||
render(renderRoutes(), document.getElementById('app'));
|
||||
});
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
import Breakouts from '/imports/api/breakouts';
|
||||
import Users from '/imports/api/users';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import { check } from 'meteor/check';
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import { XMLHttpRequest } from 'xmlhttprequest';
|
||||
|
||||
import xml2js from 'xml2js';
|
||||
import url from 'url';
|
||||
const xmlParser = new xml2js.Parser();
|
||||
|
@ -1,4 +1,4 @@
|
||||
import _ from 'underscore';
|
||||
import _ from 'lodash';
|
||||
import Captions from '/imports/api/captions';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import { check } from 'meteor/check';
|
||||
|
@ -8,11 +8,11 @@ import addChat from '/imports/api/chat/server/modifiers/addChat';
|
||||
export default function handleValidateAuthToken({ payload }) {
|
||||
const meetingId = payload.meeting_id;
|
||||
const userId = payload.userid;
|
||||
const validStatus = payload.valid;
|
||||
const validStatus = JSON.parse(payload.valid);
|
||||
|
||||
check(meetingId, String);
|
||||
check(userId, String);
|
||||
check(validStatus, String);
|
||||
check(validStatus, Boolean);
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
@ -21,14 +21,11 @@ export default function handleValidateAuthToken({ payload }) {
|
||||
|
||||
const User = Users.findOne(selector);
|
||||
|
||||
if (!User) {
|
||||
throw new Meteor.Error(
|
||||
'user-not-found', `You need a valid user to be able validate the token`);
|
||||
}
|
||||
// If we dont find the user on our collection is a flash user and we can skip
|
||||
if (!User) return;
|
||||
|
||||
if (User.validated === validStatus) {
|
||||
return;
|
||||
}
|
||||
// User already flagged so we skip
|
||||
if (User.validated === validStatus) return;
|
||||
|
||||
const modifier = {
|
||||
$set: {
|
||||
|
@ -5,6 +5,7 @@ import userLogout from './methods/userLogout';
|
||||
import assignPresenter from './methods/assignPresenter';
|
||||
import muteToggle from './methods/muteToggle';
|
||||
import setEmojiStatus from './methods/setEmojiStatus';
|
||||
import validateAuthToken from './methods/validateAuthToken';
|
||||
|
||||
Meteor.methods({
|
||||
kickUser,
|
||||
@ -12,6 +13,7 @@ Meteor.methods({
|
||||
userLogout,
|
||||
assignPresenter,
|
||||
setEmojiStatus,
|
||||
validateAuthToken,
|
||||
muteUser: (...args) => muteToggle(...args, true),
|
||||
unmuteUser: (...args) => muteToggle(...args, false),
|
||||
});
|
||||
|
@ -21,11 +21,12 @@ export default function userLeaving(credentials, userId) {
|
||||
check(requesterUserId, String);
|
||||
check(userId, String);
|
||||
|
||||
const User = Users.findOne({
|
||||
const selector = {
|
||||
meetingId,
|
||||
userId,
|
||||
});
|
||||
};
|
||||
|
||||
const User = Users.findOne(selector);
|
||||
if (!User) {
|
||||
throw new Meteor.Error(
|
||||
'user-not-found', `You need a valid user to be able to toggle audio`);
|
||||
@ -39,6 +40,26 @@ export default function userLeaving(credentials, userId) {
|
||||
listenOnlyToggle(credentials, false);
|
||||
}
|
||||
|
||||
if (User.validated) {
|
||||
const modifier = {
|
||||
$set: {
|
||||
validated: false,
|
||||
},
|
||||
};
|
||||
|
||||
const cb = (err, numChanged) => {
|
||||
if (err) {
|
||||
return Logger.error(`Invalidating user: ${err}`);
|
||||
}
|
||||
|
||||
if (numChanged) {
|
||||
return Logger.info(`Invalidate user=${userId} meeting=${meetingId}`);
|
||||
}
|
||||
};
|
||||
|
||||
return Users.update(selector, modifier, cb);
|
||||
}
|
||||
|
||||
let payload = {
|
||||
meeting_id: meetingId,
|
||||
userid: userId,
|
||||
|
@ -42,7 +42,7 @@ export default function validateAuthToken(credentials) {
|
||||
reply_to: `${meetingId}/${requesterUserId}`,
|
||||
};
|
||||
|
||||
Logger.verbose(`User '${requesterUserId}' is trying to validate auth token for meeting '${meetingId}'`);
|
||||
Logger.info(`User '${requesterUserId}' is trying to validate auth token for meeting '${meetingId}'`);
|
||||
|
||||
return RedisPubSub.publish(CHANNEL, EVENT_NAME, payload, header);
|
||||
};
|
||||
|
@ -18,7 +18,7 @@ export default function createDummyUser(meetingId, userId, authToken) {
|
||||
userId,
|
||||
authToken,
|
||||
clientType: 'HTML5',
|
||||
validated: false,
|
||||
validated: null,
|
||||
};
|
||||
|
||||
const cb = (err, numChanged) => {
|
||||
|
@ -5,7 +5,28 @@ import Logger from '/imports/startup/server/logger';
|
||||
import { isAllowedTo } from '/imports/startup/server/userPermissions';
|
||||
|
||||
import userLeaving from './methods/userLeaving';
|
||||
import validateAuthToken from './methods/validateAuthToken';
|
||||
|
||||
Meteor.publish('current-user', function (credentials) {
|
||||
const { meetingId, requesterUserId, requesterToken } = credentials;
|
||||
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
check(requesterToken, String);
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
userId: requesterUserId,
|
||||
authToken: requesterToken,
|
||||
};
|
||||
|
||||
const options = {
|
||||
fields: {
|
||||
user: false,
|
||||
},
|
||||
};
|
||||
|
||||
return Users.find(selector, options);
|
||||
});
|
||||
|
||||
Meteor.publish('users', function (credentials) {
|
||||
const { meetingId, requesterUserId, requesterToken } = credentials;
|
||||
@ -14,12 +35,9 @@ Meteor.publish('users', function (credentials) {
|
||||
check(requesterUserId, String);
|
||||
check(requesterToken, String);
|
||||
|
||||
validateAuthToken(credentials);
|
||||
|
||||
// TODO(auth): We need to fix the Authentication flow to enable ACL
|
||||
// if (!isAllowedTo('subscribeUsers', credentials)) {
|
||||
// this.error(new Meteor.Error(402, "The user was not authorized to subscribe for 'Users'"));
|
||||
// }
|
||||
if (!isAllowedTo('subscribeUsers', credentials)) {
|
||||
this.error(new Meteor.Error(402, "The user was not authorized to subscribe for 'Users'"));
|
||||
}
|
||||
|
||||
this.onStop(() => {
|
||||
userLeaving(credentials, requesterUserId);
|
||||
|
@ -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,
|
||||
};
|
@ -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..."
|
||||
}
|
48
bigbluebutton-html5/imports/startup/client/auth.js
Normal file
48
bigbluebutton-html5/imports/startup/client/auth.js
Normal 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();
|
||||
});
|
||||
};
|
91
bigbluebutton-html5/imports/startup/client/base.js
Normal file
91
bigbluebutton-html5/imports/startup/client/base.js
Normal 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);
|
64
bigbluebutton-html5/imports/startup/client/intl.js
Normal file
64
bigbluebutton-html5/imports/startup/client/intl.js
Normal 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;
|
@ -2,45 +2,32 @@ import React from 'react';
|
||||
import { Router, Route, Redirect, IndexRoute, useRouterHistory } from 'react-router';
|
||||
import { createHistory } from 'history';
|
||||
|
||||
// route components
|
||||
import AppContainer from '/imports/ui/components/app/container';
|
||||
import { subscribeToCollections, setCredentials } from '/imports/ui/components/app/service';
|
||||
import { joinRouteHandler, logoutRouteHandler, authenticatedRouteHandler } from './auth';
|
||||
import Base from './base';
|
||||
|
||||
import LoadingScreen from '/imports/ui/components/loading-screen/component';
|
||||
import ChatContainer from '/imports/ui/components/chat/container';
|
||||
import UserListContainer from '/imports/ui/components/user-list/container';
|
||||
|
||||
const browserHistory = useRouterHistory(createHistory)({
|
||||
// Name displayed in the brower URL
|
||||
basename: Meteor.settings.public.app.basename,
|
||||
});
|
||||
|
||||
export const renderRoutes = () => (
|
||||
<Router history={browserHistory}>
|
||||
<Route path="/join/:meetingID/:userID/:authToken" onEnter={setCredentials} />
|
||||
<Route path="/" onEnter={() => {
|
||||
subscribeToCollections();
|
||||
}}
|
||||
|
||||
getComponent={(nextState, cb) => {
|
||||
subscribeToCollections(() => cb(null, AppContainer));
|
||||
}}>
|
||||
<Route path="/logout" onEnter={logoutRouteHandler} />
|
||||
<Route path="/join/:meetingID/:userID/:authToken"
|
||||
component={LoadingScreen} onEnter={joinRouteHandler} />
|
||||
<Route path="/" component={Base} onEnter={authenticatedRouteHandler} >
|
||||
<IndexRoute components={{}} />
|
||||
|
||||
<Route name="users" path="users" getComponents={(nextState, cb) => {
|
||||
subscribeToCollections(() => cb(null, {
|
||||
userList: UserListContainer,
|
||||
}));
|
||||
<Route name="users" path="users" components={{ userList: UserListContainer }} />
|
||||
<Route name="chat" path="users/chat/:chatID" components={{
|
||||
userList: UserListContainer,
|
||||
chat: ChatContainer,
|
||||
}} />
|
||||
|
||||
<Route name="chat" path="users/chat/:chatID" getComponents={(nextState, cb) => {
|
||||
subscribeToCollections(() => cb(null, {
|
||||
userList: UserListContainer,
|
||||
chat: ChatContainer,
|
||||
}));
|
||||
}} />
|
||||
|
||||
<Redirect from="users/chat" to="/users/chat/public" />
|
||||
</Route>
|
||||
<Redirect from="*" to="/" />
|
||||
<Route name="error" path="/error/:errorCode" component={Base}/>
|
||||
<Redirect from="*" to="/error/404" />
|
||||
</Router>
|
||||
);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import Locales from '/imports/locales';
|
||||
import _ from 'lodash';
|
||||
import Logger from './logger';
|
||||
import Redis from './redis';
|
||||
|
||||
@ -17,22 +17,32 @@ WebApp.connectHandlers.use('/check', (req, res, next) => {
|
||||
});
|
||||
|
||||
WebApp.connectHandlers.use('/locale', (req, res) => {
|
||||
let defaultLocale = 'en';
|
||||
let [locale, region] = req.query.locale.split('-');
|
||||
const APP_CONFIG = Meteor.settings.public.app;
|
||||
|
||||
const defaultMessages = Locales[defaultLocale];
|
||||
let defaultLocale = APP_CONFIG.defaultLocale;
|
||||
let localeRegion = _.snakeCase(req.query.locale).split('_');
|
||||
let messages = {};
|
||||
|
||||
let messages = Object.assign(
|
||||
{},
|
||||
defaultMessages,
|
||||
Locales[locale],
|
||||
Locales[`${locale}-${region}`],
|
||||
);
|
||||
let locales = [defaultLocale, localeRegion[0]];
|
||||
|
||||
if (localeRegion.length > 1) {
|
||||
locales.push(`${localeRegion[0]}_${localeRegion[1]}`);
|
||||
}
|
||||
|
||||
locales.forEach(locale => {
|
||||
try {
|
||||
const data = Assets.getText(`locales/${locale}.json`);
|
||||
messages = Object.assign(messages, JSON.parse(data));
|
||||
} catch (e) {
|
||||
// console.error(e);
|
||||
// We dont really care about those errors since they will be a parse error
|
||||
// or a file not found which is ok
|
||||
}
|
||||
});
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.writeHead(200);
|
||||
res.end(JSON.stringify(messages));
|
||||
|
||||
});
|
||||
|
||||
export const eventEmitter = Redis.emitter;
|
||||
|
@ -18,17 +18,17 @@ const intlMessages = defineMessages({
|
||||
class JoinAudioOptions extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
close,
|
||||
intl,
|
||||
isInAudio,
|
||||
isInListenOnly,
|
||||
open,
|
||||
handleJoinAudio,
|
||||
handleCloseAudio,
|
||||
} = this.props;
|
||||
|
||||
if (isInAudio || isInListenOnly) {
|
||||
return (
|
||||
<Button
|
||||
onClick={close}
|
||||
onClick={handleCloseAudio}
|
||||
label={intl.formatMessage(intlMessages.leaveAudio)}
|
||||
color={'danger'}
|
||||
icon={'audio_off'}
|
||||
@ -40,7 +40,7 @@ class JoinAudioOptions extends React.Component {
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={open}
|
||||
onClick={handleJoinAudio}
|
||||
label={intl.formatMessage(intlMessages.joinAudio)}
|
||||
color={'primary'}
|
||||
icon={'audio_on'}
|
||||
|
@ -20,7 +20,7 @@ export default createContainer((params) => {
|
||||
return {
|
||||
isInAudio: user.voiceUser.joined,
|
||||
isInListenOnly: user.listenOnly,
|
||||
open: params.open,
|
||||
close: params.close,
|
||||
handleJoinAudio: params.handleJoinAudio,
|
||||
handleCloseAudio: params.handleCloseAudio,
|
||||
};
|
||||
}, JoinAudioOptionsContainer);
|
||||
|
@ -1,15 +1,10 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { showModal } from '/imports/ui/components/app/service';
|
||||
import Audio from '/imports/ui/components/audio-modal/component';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import styles from './styles.scss';
|
||||
import EmojiContainer from './emoji-menu/container';
|
||||
import ActionsDropdown from './actions-dropdown/component';
|
||||
import Auth from '/imports/ui/services/auth/index';
|
||||
import Users from '/imports/api/users/index';
|
||||
import JoinAudioOptionsContainer from './audio-menu/container';
|
||||
import MuteAudioContainer from './mute-button/container';
|
||||
import { exitAudio } from '/imports/api/phone';
|
||||
import JoinVideo from './video-button/component';
|
||||
|
||||
export default class ActionsBar extends Component {
|
||||
@ -17,10 +12,6 @@ export default class ActionsBar extends Component {
|
||||
super(props);
|
||||
}
|
||||
|
||||
openJoinAudio() {
|
||||
return showModal(<Audio handleJoinListenOnly={this.props.handleJoinListenOnly} />)
|
||||
}
|
||||
|
||||
renderForPresenter() {
|
||||
return (
|
||||
<div className={styles.actionsbar}>
|
||||
@ -30,14 +21,15 @@ export default class ActionsBar extends Component {
|
||||
<div className={styles.center}>
|
||||
<MuteAudioContainer />
|
||||
<JoinAudioOptionsContainer
|
||||
open={this.openJoinAudio.bind(this)}
|
||||
close={() => {exitAudio();}}
|
||||
handleJoinAudio={this.props.handleOpenJoinAudio}
|
||||
handleCloseAudio={this.props.handleExitAudio}
|
||||
|
||||
/>
|
||||
{/*<JoinVideo />*/}
|
||||
<EmojiContainer />
|
||||
</div>
|
||||
<div className={styles.right}>
|
||||
<div className={styles.hidden}>
|
||||
<ActionsDropdown />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -49,15 +41,13 @@ export default class ActionsBar extends Component {
|
||||
<div className={styles.center}>
|
||||
<MuteAudioContainer />
|
||||
<JoinAudioOptionsContainer
|
||||
open={this.openJoinAudio.bind(this)}
|
||||
close={() => {exitAudio();}}
|
||||
handleJoinAudio={this.props.handleOpenJoinAudio}
|
||||
handleCloseAudio={this.props.handleExitAudio}
|
||||
|
||||
/>
|
||||
{/*<JoinVideo />*/}
|
||||
<EmojiContainer />
|
||||
</div>
|
||||
<div className={styles.right}>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import { createContainer } from 'meteor/react-meteor-data';
|
||||
import ActionsBar from './component';
|
||||
import Service from './service';
|
||||
import { joinListenOnly } from '/imports/api/phone';
|
||||
|
||||
class ActionsBarContainer extends Component {
|
||||
constructor(props) {
|
||||
@ -10,19 +9,23 @@ class ActionsBarContainer extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const handleJoinListenOnly = () => joinListenOnly();
|
||||
|
||||
return (
|
||||
<ActionsBar
|
||||
handleJoinListenOnly={handleJoinListenOnly}
|
||||
{...this.props}>
|
||||
{this.props.children}
|
||||
{...this.props}>
|
||||
{this.props.children}
|
||||
</ActionsBar>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default createContainer(() => {
|
||||
let data = Service.isUserPresenter();
|
||||
return data;
|
||||
const isPresenter = Service.isUserPresenter();
|
||||
const handleExitAudio = () => Service.handleExitAudio();
|
||||
const handleOpenJoinAudio = () => Service.handleJoinAudio();
|
||||
|
||||
return {
|
||||
isUserPresenter: isPresenter,
|
||||
handleExitAudio: handleExitAudio,
|
||||
handleOpenJoinAudio: handleOpenJoinAudio,
|
||||
};
|
||||
}, ActionsBarContainer);
|
||||
|
@ -5,13 +5,13 @@ import { callServer } from '/imports/ui/services/api/index.js';
|
||||
let getEmojiData = () => {
|
||||
|
||||
// Get userId and meetingId
|
||||
const credentials = Auth.getCredentials();
|
||||
const credentials = Auth.credentials;
|
||||
const { requesterUserId: userId, meetingId } = credentials;
|
||||
|
||||
// Find the Emoji Status of this specific meeting and userid
|
||||
const userEmojiStatus = Users.findOne({
|
||||
meetingId: meetingId,
|
||||
userId: userId,
|
||||
meetingId: Auth.meetingID,
|
||||
userId: Auth.userID,
|
||||
}).user.emoji_status;
|
||||
|
||||
return {
|
||||
|
@ -6,10 +6,13 @@ export default class MuteAudio extends React.Component {
|
||||
|
||||
render() {
|
||||
const { isInAudio, isMuted, callback, isTalking} = this.props;
|
||||
|
||||
if (!isInAudio) return null;
|
||||
|
||||
let label = !isMuted ? 'Mute' : 'Unmute';
|
||||
let icon = !isMuted ? 'unmute' : 'mute';
|
||||
let className = !isInAudio ? styles.invisible : null;
|
||||
let tabIndex = !isInAudio ? -1 : 0;
|
||||
let className = null;
|
||||
|
||||
if (isInAudio && isTalking) {
|
||||
className = styles.circleGlow;
|
||||
|
@ -1,11 +1,16 @@
|
||||
import React from 'react';
|
||||
import AuthSingleton from '/imports/ui/services/auth/index.js';
|
||||
import Users from '/imports/api/users';
|
||||
import { joinListenOnly } from '/imports/api/phone';
|
||||
import { showModal } from '/imports/ui/components/app/service';
|
||||
import { exitAudio } from '/imports/api/phone';
|
||||
import Audio from '/imports/ui/components/audio-modal/component';
|
||||
|
||||
let isUserPresenter = () => {
|
||||
|
||||
// check if user is a presenter
|
||||
let isPresenter = Users.findOne({
|
||||
userId: AuthSingleton.getCredentials().requesterUserId,
|
||||
userId: AuthSingleton.userID,
|
||||
}).user.presenter;
|
||||
|
||||
return {
|
||||
@ -13,6 +18,17 @@ let isUserPresenter = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const handleExitAudio = () => {
|
||||
return exitAudio();
|
||||
}
|
||||
|
||||
const handleJoinAudio = () => {
|
||||
const handleJoinListenOnly = () => joinListenOnly();
|
||||
return showModal(<Audio handleJoinListenOnly={handleJoinListenOnly} />);
|
||||
}
|
||||
|
||||
export default {
|
||||
isUserPresenter,
|
||||
handleJoinAudio,
|
||||
handleExitAudio,
|
||||
};
|
||||
|
@ -7,7 +7,8 @@
|
||||
|
||||
.left,
|
||||
.right,
|
||||
.center {
|
||||
.center,
|
||||
.hidden {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
@ -19,7 +20,8 @@
|
||||
}
|
||||
|
||||
.left,
|
||||
.right {
|
||||
.right,
|
||||
.hidden {
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
@ -27,7 +29,7 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.invisible {
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
|
@ -1,214 +1,125 @@
|
||||
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import LoadingScreen from '../loading-screen/component';
|
||||
import KickedScreen from '../kicked-screen/component';
|
||||
import _ from 'lodash';
|
||||
|
||||
import NotificationsBarContainer from '../notifications-bar/container';
|
||||
import AudioNotificationContainer from '../audio-notification/container';
|
||||
|
||||
import LocalStorage from '/imports/ui/services/storage/local.js';
|
||||
import ChatNotificationContainer from '../chat/notification/container';
|
||||
|
||||
import Button from '../button/component';
|
||||
import styles from './styles';
|
||||
import cx from 'classnames';
|
||||
|
||||
const propTypes = {
|
||||
init: PropTypes.func.isRequired,
|
||||
fontSize: PropTypes.string,
|
||||
navbar: PropTypes.element,
|
||||
sidebar: PropTypes.element,
|
||||
sidebarRight: PropTypes.element,
|
||||
media: PropTypes.element,
|
||||
actionsbar: PropTypes.element,
|
||||
captions: PropTypes.element,
|
||||
modal: PropTypes.element,
|
||||
unreadMessageCount: PropTypes.array,
|
||||
openChats: PropTypes.array,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
fontSize: '16px',
|
||||
};
|
||||
|
||||
export default class App extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
compactUserList: false, //TODO: Change this on userlist resize (?)
|
||||
};
|
||||
|
||||
this.setDefaultSettings = props.setDefaultSettings;
|
||||
props.init.call(this);
|
||||
}
|
||||
|
||||
setHtmlFontSize(size) {
|
||||
document.getElementsByTagName('html')[0].style.fontSize = size;
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.setDefaultSettings();
|
||||
this.setHtmlFontSize(this.props.fontSize);
|
||||
document.getElementsByTagName('html')[0].style.fontSize = this.props.fontSize;
|
||||
}
|
||||
|
||||
renderNavBar() {
|
||||
const { navbar } = this.props;
|
||||
|
||||
if (navbar) {
|
||||
return (
|
||||
<header className={styles.navbar}>
|
||||
{navbar}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
if (!navbar) return null;
|
||||
|
||||
return false;
|
||||
return (
|
||||
<header className={styles.navbar}>
|
||||
{navbar}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
renderSidebar() {
|
||||
const { sidebar } = this.props;
|
||||
|
||||
if (sidebar) {
|
||||
return (
|
||||
<aside className={styles.sidebar}>
|
||||
{sidebar}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
if (!sidebar) return null;
|
||||
|
||||
return false;
|
||||
return (
|
||||
<aside className={styles.sidebar}>
|
||||
{sidebar}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
renderUserList() {
|
||||
let { userList } = this.props;
|
||||
const { compactUserList } = this.state;
|
||||
|
||||
if (!userList) return;
|
||||
|
||||
let userListStyle = {};
|
||||
userListStyle[styles.compact] = compactUserList;
|
||||
if (userList) {
|
||||
userList = React.cloneElement(userList, {
|
||||
compact: compactUserList,
|
||||
});
|
||||
userList = React.cloneElement(userList, {
|
||||
compact: compactUserList,
|
||||
});
|
||||
|
||||
return (
|
||||
<nav className={cx(styles.userList, userListStyle)}>
|
||||
{userList}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
return (
|
||||
<nav className={cx(styles.userList, userListStyle)}>
|
||||
{userList}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
renderChat() {
|
||||
const { chat } = this.props;
|
||||
|
||||
if (chat) {
|
||||
return (
|
||||
<section className={styles.chat} role="log">
|
||||
{chat}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
if (!chat) return null;
|
||||
|
||||
return false;
|
||||
return (
|
||||
<section className={styles.chat} role="log">
|
||||
{chat}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
renderMedia() {
|
||||
const { media } = this.props;
|
||||
|
||||
if (media) {
|
||||
return (
|
||||
<section className={styles.media}>
|
||||
{media}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
if (!media) return null;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
renderClosedCaptions() {
|
||||
const { captions } = this.props;
|
||||
if (captions && this.props.getCaptionsStatus()) {
|
||||
return (
|
||||
<section className={styles.closedCaptions}>
|
||||
{captions}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<section className={styles.media}>
|
||||
{media}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
renderActionsBar() {
|
||||
const { actionsbar } = this.props;
|
||||
|
||||
if (actionsbar) {
|
||||
return (
|
||||
<section className={styles.actionsbar}>
|
||||
{actionsbar}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
if (!actionsbar) return null;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
renderAudioElement() {
|
||||
return (
|
||||
<audio id="remote-media" autoPlay="autoplay"></audio>
|
||||
<section className={styles.actionsbar}>
|
||||
{actionsbar}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
renderModal() {
|
||||
const { modal } = this.props;
|
||||
|
||||
if (modal) {
|
||||
return (<div>{modal}</div>);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
playSoundForUnreadMessages() {
|
||||
const snd = new Audio('/html5client/resources/sounds/notify.mp3');
|
||||
snd.play();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
|
||||
let { unreadMessageCount, openChats, openChat } = this.props;
|
||||
|
||||
unreadMessageCount.forEach((chat, i) => {
|
||||
// When starting the new chat, if prevProps is undefined or null, it is assigned 0.
|
||||
if (!prevProps.unreadMessageCount[i]) {
|
||||
prevProps.unreadMessageCount[i] = 0;
|
||||
}
|
||||
|
||||
// compare openChats(chatID) to chatID of currently opened chat room
|
||||
if (openChats[i] !== openChat) {
|
||||
let shouldPlaySound = this.props.applicationSettings.chatAudioNotifications;
|
||||
|
||||
if (shouldPlaySound && chat > prevProps.unreadMessageCount[i]) {
|
||||
this.playSoundForUnreadMessages();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.wasKicked) {
|
||||
return (
|
||||
<KickedScreen>
|
||||
<FormattedMessage
|
||||
id="app.kickMessage"
|
||||
description="Message when the user is kicked out of the meeting"
|
||||
defaultMessage="You have been kicked out of the meeting"
|
||||
/>
|
||||
<br/><br/>
|
||||
<Button
|
||||
label={'OK'}
|
||||
onClick={this.props.redirectToLogoutUrl}/>
|
||||
</KickedScreen>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.isLoading) {
|
||||
return <LoadingScreen/>;
|
||||
}
|
||||
const { modal, params } = this.props;
|
||||
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
@ -223,13 +134,14 @@ export default class App extends Component {
|
||||
{this.renderActionsBar()}
|
||||
</div>
|
||||
{this.renderSidebar()}
|
||||
{this.renderClosedCaptions()}
|
||||
</section>
|
||||
{this.renderAudioElement()}
|
||||
{this.renderModal()}
|
||||
{modal}
|
||||
<audio id="remote-media" autoPlay="autoplay"></audio>
|
||||
<ChatNotificationContainer currentChatID={params.chatID} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
App.propTypes = propTypes;
|
||||
App.defaultProps = defaultProps;
|
||||
|
@ -1,32 +1,42 @@
|
||||
import React, { Component, PropTypes, cloneElement } from 'react';
|
||||
import { createContainer } from 'meteor/react-meteor-data';
|
||||
import App from './component';
|
||||
import {
|
||||
subscribeForData,
|
||||
wasUserKicked,
|
||||
redirectToLogoutUrl,
|
||||
getModal,
|
||||
getCaptionsStatus,
|
||||
getFontSize,
|
||||
} from './service';
|
||||
import { setDefaultSettings, getSettingsFor } from '/imports/ui/components/settings/service';
|
||||
import { withRouter } from 'react-router';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import {
|
||||
getModal,
|
||||
showModal,
|
||||
getFontSize,
|
||||
getCaptionsStatus,
|
||||
} from './service';
|
||||
|
||||
import { setDefaultSettings } from '../settings/service';
|
||||
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import Users from '/imports/api/users';
|
||||
import Breakouts from '/imports/api/breakouts';
|
||||
|
||||
import App from './component';
|
||||
import NavBarContainer from '../nav-bar/container';
|
||||
import ActionsBarContainer from '../actions-bar/container';
|
||||
import MediaContainer from '../media/container';
|
||||
import ClosedCaptionsContainer from '../closed-captions/container';
|
||||
import UserListService from '../user-list/service';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import AudioModalContainer from '../audio-modal/container';
|
||||
import ClosedCaptionsContainer from '/imports/ui/components/closed-captions/container';
|
||||
|
||||
const defaultProps = {
|
||||
navbar: <NavBarContainer />,
|
||||
actionsbar: <ActionsBarContainer />,
|
||||
media: <MediaContainer />,
|
||||
|
||||
//CCs UI is commented till the next pull request
|
||||
captions: <ClosedCaptionsContainer />,
|
||||
};
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
kickedMessage: {
|
||||
id: 'app.error.kicked',
|
||||
description: 'Message when the user is kicked out of the meeting',
|
||||
defaultMessage: 'You have been kicked out of the meeting',
|
||||
},
|
||||
});
|
||||
|
||||
class AppContainer extends Component {
|
||||
render() {
|
||||
// inject location on the navbar container
|
||||
@ -38,52 +48,44 @@ class AppContainer extends Component {
|
||||
</App>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let loading = true;
|
||||
const loadingDep = new Tracker.Dependency;
|
||||
|
||||
const getLoading = () => {
|
||||
loadingDep.depend();
|
||||
return loading;
|
||||
};
|
||||
|
||||
const setLoading = (val) => {
|
||||
if (val !== loading) {
|
||||
loading = val;
|
||||
loadingDep.changed();
|
||||
const APP_CONFIG = Meteor.settings.public.app;
|
||||
|
||||
const init = () => {
|
||||
setDefaultSettings();
|
||||
if (APP_CONFIG.autoJoinAudio) {
|
||||
showModal(<AudioModalContainer />);
|
||||
}
|
||||
};
|
||||
|
||||
const checkUnreadMessages = () => {
|
||||
return UserListService.getOpenChats().map(chat=> chat.unreadCounter)
|
||||
.filter(userID => userID !== Auth.userID);
|
||||
};
|
||||
export default withRouter(injectIntl(createContainer(({ router, intl, baseControls }) => {
|
||||
// Check if user is kicked out of the session
|
||||
Users.find({ userId: Auth.userID }).observeChanges({
|
||||
removed() {
|
||||
Auth.clearCredentials()
|
||||
.then(() => {
|
||||
router.push('/error/403');
|
||||
baseControls.updateErrorState(
|
||||
intl.formatMessage(intlMessages.kickedMessage),
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const openChats = (chatID) => {
|
||||
// get currently opened chatID
|
||||
return UserListService.getOpenChats(chatID).map(chat => chat.id);
|
||||
}
|
||||
|
||||
export default createContainer(({ params }) => {
|
||||
Promise.all(subscribeForData())
|
||||
.then(() => {
|
||||
setLoading(false);
|
||||
// Close the widow when the current breakout room ends
|
||||
Breakouts.find({ breakoutMeetingId: Auth.meetingID }).observeChanges({
|
||||
removed(old) {
|
||||
Auth.clearCredentials().then(window.close);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
wasKicked: wasUserKicked(),
|
||||
isLoading: getLoading(),
|
||||
init,
|
||||
sidebar: getCaptionsStatus() ? <ClosedCaptionsContainer /> : null,
|
||||
modal: getModal(),
|
||||
unreadMessageCount: checkUnreadMessages(),
|
||||
openChats: openChats(params.chatID),
|
||||
openChat: params.chatID,
|
||||
getCaptionsStatus,
|
||||
redirectToLogoutUrl,
|
||||
setDefaultSettings,
|
||||
fontSize: getFontSize(),
|
||||
applicationSettings: getSettingsFor('application'),
|
||||
};
|
||||
}, AppContainer);
|
||||
}, AppContainer)));
|
||||
|
||||
AppContainer.defaultProps = defaultProps;
|
||||
|
@ -1,118 +1,20 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import Users from '/imports/api/users';
|
||||
import Breakouts from '/imports/api/breakouts';
|
||||
import Storage from '/imports/ui/services/storage/session';
|
||||
import SettingsService from '/imports/ui/components/settings/service';
|
||||
|
||||
function setCredentials(nextState, replace) {
|
||||
if (nextState && nextState.params.authToken) {
|
||||
const { meetingID, userID, authToken } = nextState.params;
|
||||
Auth.setCredentials(meetingID, userID, authToken);
|
||||
replace({
|
||||
pathname: '/',
|
||||
});
|
||||
}
|
||||
let currentModal = {
|
||||
component: null,
|
||||
tracker: new Tracker.Dependency,
|
||||
};
|
||||
|
||||
let dataSubscriptions = null;
|
||||
function subscribeForData() {
|
||||
if (dataSubscriptions) {
|
||||
return dataSubscriptions;
|
||||
}
|
||||
|
||||
const subNames = [
|
||||
'users', 'chat', 'cursor', 'deskshare', 'meetings',
|
||||
'polls', 'presentations', 'shapes', 'slides', 'captions', 'breakouts',
|
||||
];
|
||||
|
||||
let subs = [];
|
||||
subNames.forEach(name => subs.push(subscribeFor(name)));
|
||||
|
||||
dataSubscriptions = subs;
|
||||
return subs;
|
||||
};
|
||||
|
||||
function subscribeFor(collectionName) {
|
||||
const credentials = Auth.getCredentials();
|
||||
return new Promise((resolve, reject) => {
|
||||
Meteor.subscribe(collectionName, credentials, {
|
||||
onReady: (...args) => resolve(...args),
|
||||
onStop: (...args) => reject(...args),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function subscribeToCollections(cb) {
|
||||
subscribeFor('users')
|
||||
.then(() => {
|
||||
observeUserKick();
|
||||
return Promise.all(subscribeForData())
|
||||
.then(() => {
|
||||
observeBreakoutEnd();
|
||||
if (cb) {
|
||||
return cb();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(redirectToLogoutUrl);
|
||||
};
|
||||
|
||||
function redirectToLogoutUrl(reason) {
|
||||
console.error(reason);
|
||||
console.log('Redirecting user to the logoutURL...');
|
||||
document.location.href = Auth.logoutURL;
|
||||
}
|
||||
|
||||
let wasKicked = false;
|
||||
const wasKickedDep = new Tracker.Dependency;
|
||||
|
||||
function observeUserKick() {
|
||||
Users.find().observe({
|
||||
removed(old) {
|
||||
if (old.userId === Auth.userID) {
|
||||
Auth.clearCredentials(() => {
|
||||
wasKicked = true;
|
||||
wasKickedDep.changed();
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function observeBreakoutEnd() {
|
||||
Breakouts.find().observe({
|
||||
removed(old) {
|
||||
if (old.breakoutMeetingId === Auth.meetingID) {
|
||||
// The breakout room expired. Closing the browser tab to return to the main room
|
||||
window.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function meetingIsBreakout() {
|
||||
const breakouts = Breakouts.find().fetch();
|
||||
return (breakouts && breakouts.some(b => b.breakoutMeetingId === Auth.meetingID));
|
||||
}
|
||||
|
||||
function wasUserKicked() {
|
||||
wasKickedDep.depend();
|
||||
return wasKicked;
|
||||
}
|
||||
|
||||
let modal = null;
|
||||
const modalDep = new Tracker.Dependency;
|
||||
|
||||
const getModal = () => {
|
||||
modalDep.depend();
|
||||
return modal;
|
||||
currentModal.tracker.depend();
|
||||
return currentModal.component;
|
||||
};
|
||||
|
||||
const showModal = (val) => {
|
||||
if (val !== modal) {
|
||||
modal = val;
|
||||
modalDep.changed();
|
||||
const showModal = (component) => {
|
||||
if (currentModal.component !== component) {
|
||||
currentModal.component = component;
|
||||
currentModal.tracker.changed();
|
||||
}
|
||||
};
|
||||
|
||||
@ -121,22 +23,21 @@ const clearModal = () => {
|
||||
};
|
||||
|
||||
const getCaptionsStatus = () => {
|
||||
const settings = Storage.getItem('settings_cc');
|
||||
const settings = SettingsService.getSettingsFor('cc');
|
||||
return settings ? settings.closedCaptions : false;
|
||||
};
|
||||
|
||||
const getFontSize = () => {
|
||||
const settings = SettingsService.getSettingsFor('application');
|
||||
return settings ? settings.fontSize : '14px';
|
||||
return settings ? settings.fontSize : '16px';
|
||||
};
|
||||
|
||||
function meetingIsBreakout() {
|
||||
const breakouts = Breakouts.find().fetch();
|
||||
return (breakouts && breakouts.some(b => b.breakoutMeetingId === Auth.meetingID));
|
||||
}
|
||||
|
||||
export {
|
||||
subscribeForData,
|
||||
setCredentials,
|
||||
subscribeFor,
|
||||
subscribeToCollections,
|
||||
wasUserKicked,
|
||||
redirectToLogoutUrl,
|
||||
getModal,
|
||||
showModal,
|
||||
clearModal,
|
||||
|
@ -8,6 +8,7 @@ import DeviceSelector from '/imports/ui/components/audio/device-selector/compone
|
||||
import AudioStreamVolume from '/imports/ui/components/audio/audio-stream-volume/component';
|
||||
import EnterAudioContainer from '/imports/ui/components/enter-audio/container';
|
||||
import AudioTestContainer from '/imports/ui/components/audio-test/container';
|
||||
import cx from 'classnames';
|
||||
|
||||
class AudioSettings extends React.Component {
|
||||
constructor(props) {
|
||||
@ -20,7 +21,7 @@ class AudioSettings extends React.Component {
|
||||
|
||||
this.state = {
|
||||
inputDeviceId: undefined,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
chooseAudio() {
|
||||
@ -30,7 +31,7 @@ class AudioSettings extends React.Component {
|
||||
handleInputChange(deviceId) {
|
||||
console.log(`INPUT DEVICE CHANGED: ${deviceId}`);
|
||||
this.setState({
|
||||
inputDeviceId: deviceId
|
||||
inputDeviceId: deviceId,
|
||||
});
|
||||
}
|
||||
|
||||
@ -50,7 +51,7 @@ class AudioSettings extends React.Component {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.center}>
|
||||
<div className={styles.topRow}>
|
||||
<Button className={styles.backBtn}
|
||||
label={intl.formatMessage(intlMessages.backLabel)}
|
||||
icon={'left_arrow'}
|
||||
@ -59,47 +60,69 @@ class AudioSettings extends React.Component {
|
||||
ghost={true}
|
||||
onClick={this.chooseAudio}
|
||||
/>
|
||||
<div className={styles.title}>
|
||||
<div className={cx(styles.title, styles.chooseAudio)}>
|
||||
<FormattedMessage
|
||||
id="app.audio.audioSettings.titleLabel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.audioNote}>
|
||||
<FormattedMessage
|
||||
id="app.audio.audioSettings.descriptionLabel"
|
||||
/>
|
||||
|
||||
<div className={styles.form}>
|
||||
|
||||
<div className={styles.row}>
|
||||
<div className={styles.audioNote}>
|
||||
<FormattedMessage
|
||||
id="app.audio.audioSettings.descriptionLabel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.row}>
|
||||
<div className={styles.col}>
|
||||
<div className={styles.formElement}>
|
||||
<label className={cx(styles.label, styles.labelSmall)}>
|
||||
Microphone source
|
||||
</label>
|
||||
<DeviceSelector
|
||||
value={this.state.inputDeviceId}
|
||||
className={styles.select}
|
||||
kind="audioinput"
|
||||
onChange={this.handleInputChange} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.col}>
|
||||
<div className={styles.formElement}>
|
||||
<label className={cx(styles.label, styles.labelSmall)}>
|
||||
Speaker source
|
||||
</label>
|
||||
<DeviceSelector
|
||||
value={this.state.outputDeviceId}
|
||||
className={styles.select}
|
||||
kind="audiooutput"
|
||||
onChange={this.handleOutputChange} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.row}>
|
||||
<div className={styles.col}>
|
||||
<div className={styles.formElement}>
|
||||
<label className={cx(styles.label, styles.labelSmall)}>
|
||||
Your audio stream volume
|
||||
</label>
|
||||
<AudioStreamVolume
|
||||
deviceId={this.state.inputDeviceId}
|
||||
className={styles.audioMeter} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.col}>
|
||||
<label className={styles.label}> </label>
|
||||
<AudioTestContainer/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.containerLeftHalfContent}>
|
||||
<span className={styles.heading}>
|
||||
<FormattedMessage
|
||||
id="app.audio.audioSettings.microphoneSourceLabel"
|
||||
/>
|
||||
</span>
|
||||
<DeviceSelector
|
||||
className={styles.item}
|
||||
kind="audioinput"
|
||||
onChange={this.handleInputChange} />
|
||||
<span className={styles.heading}>
|
||||
<FormattedMessage
|
||||
id="app.audio.audioSettings.microphoneStreamLabel"
|
||||
/>
|
||||
</span>
|
||||
<AudioStreamVolume
|
||||
className={styles.item}
|
||||
deviceId={this.state.inputDeviceId} />
|
||||
</div>
|
||||
<div className={styles.containerRightHalfContent}>
|
||||
<span className={styles.heading}>
|
||||
<FormattedMessage
|
||||
id="app.audio.audioSettings.speakerSourceLabel"
|
||||
/>
|
||||
</span>
|
||||
<DeviceSelector
|
||||
className={styles.item}
|
||||
kind="audiooutput"
|
||||
onChange={this.handleOutputChange} />
|
||||
<AudioTestContainer />
|
||||
|
||||
<div className={styles.enterAudio}>
|
||||
<EnterAudioContainer isFullAudio={true}/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -43,22 +43,22 @@ class JoinAudio extends React.Component {
|
||||
const { intl } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.center}>
|
||||
<div className={styles.closeBtn}>
|
||||
<Button className={styles.closeBtn}
|
||||
label={intl.formatMessage(intlMessages.closeLabel)}
|
||||
icon={'close'}
|
||||
size={'lg'}
|
||||
circle={true}
|
||||
hideLabel={true}
|
||||
onClick={this.handleClose}
|
||||
/>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
</div>
|
||||
|
||||
<div className={styles.title}>
|
||||
<FormattedMessage
|
||||
id="app.audioModal.audioChoiceLabel"
|
||||
description="app.audioModal.audioChoiceDescription"
|
||||
defaultMessage="How would you like to join the audio?"
|
||||
/>
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.center}>
|
||||
<Button className={styles.audioBtn}
|
||||
@ -68,6 +68,9 @@ class JoinAudio extends React.Component {
|
||||
size={'jumbo'}
|
||||
onClick={this.openAudio}
|
||||
/>
|
||||
|
||||
<span className={styles.verticalLine}>
|
||||
</span>
|
||||
<Button className={styles.audioBtn}
|
||||
label={intl.formatMessage(intlMessages.listenOnlyLabel)}
|
||||
icon={'listen'}
|
||||
|
@ -1,159 +1,164 @@
|
||||
@import "../../stylesheets/variables/_all";
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
font-size: $font-size-large;
|
||||
padding-top: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
.closeBtn {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
background-color: #FFFFFF;
|
||||
border: none;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
i {
|
||||
color: $color-gray-light;
|
||||
}
|
||||
}
|
||||
|
||||
// Modifies the close button style
|
||||
Button.closeBtn span:first-child {
|
||||
color: $color-gray-light;
|
||||
background: none;
|
||||
background-color: $color-primary;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
Button.audioBtn {
|
||||
i{
|
||||
color: #3c5764;
|
||||
}
|
||||
}
|
||||
|
||||
// Modifies the audio button icon colour
|
||||
Button.audioBtn span:first-child {
|
||||
color: #25385D;
|
||||
border: 5px solid $color-white;
|
||||
background-color: $color-primary;
|
||||
color: #1b3c4b;
|
||||
background-color: #f1f8ff;
|
||||
box-shadow: none;
|
||||
border: 5px solid #f1f8ff;
|
||||
}
|
||||
|
||||
// When hovering over a button of class audioBtn, change the border colour of first span-child
|
||||
Button.audioBtn:hover span:first-child {
|
||||
border: 5px solid $color-primary;
|
||||
background-color: #f1f8ff;
|
||||
}
|
||||
|
||||
// Modifies the button label text
|
||||
Button.audioBtn span:last-child {
|
||||
color: $color-gray-dark;
|
||||
font-size: 30%;
|
||||
color: black;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
Button.audioBtn:first-of-type {
|
||||
margin-right: 70px;
|
||||
margin-right: 5%;
|
||||
}
|
||||
|
||||
Button.audioBtn:last-of-type {
|
||||
margin-left: 5%;
|
||||
}
|
||||
|
||||
.inner {
|
||||
padding: 10px;
|
||||
min-height: 350px;
|
||||
min-width: 500px;
|
||||
}
|
||||
|
||||
// Audio settings menu
|
||||
.half {
|
||||
width: 50%;
|
||||
float: left;
|
||||
padding-top: 30px;
|
||||
}
|
||||
|
||||
div.half label {
|
||||
font-size: 0.75em;
|
||||
font-weight: 600;
|
||||
color: $color-primary;
|
||||
margin-bottom: 5px;
|
||||
padding: 1em;
|
||||
min-height: 20rem;
|
||||
}
|
||||
|
||||
.backBtn {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
i {
|
||||
color: $color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.playSound {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.enterBtn {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.containerLeftHalf {
|
||||
width: 50%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.containerRightHalf {
|
||||
width: 50%;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.containerFull{
|
||||
width: 100%;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.containerLeftHalfContent {
|
||||
@extend .containerLeftHalf;
|
||||
@extend .row;
|
||||
}
|
||||
|
||||
.containerRightHalfContent {
|
||||
@extend .containerRightHalf;
|
||||
@extend .row;
|
||||
}
|
||||
|
||||
.row {
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: block;
|
||||
margin-bottom: $md-padding-y;
|
||||
width: 85%;
|
||||
margin-bottom: 2em;
|
||||
border-bottom: 3px solid;
|
||||
border-top:0px;
|
||||
border-left: 0px;
|
||||
border-right: 0px;
|
||||
outline:0px;
|
||||
border-color: $color-gray-dark;
|
||||
text-decoration-color: $color-gray-dark;
|
||||
}
|
||||
|
||||
|
||||
.title {
|
||||
color: $color-gray-dark;
|
||||
font-weight: 700;
|
||||
font-size: $font-size-large;
|
||||
margin-top: -1em;
|
||||
.topRow {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.audioNote {
|
||||
color: $color-text;
|
||||
display: inline-block;
|
||||
margin-top: 1.75em;
|
||||
margin-bottom: 2em;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-weight: 700;
|
||||
font-size: $font-size-small;
|
||||
display: inline-block;
|
||||
margin-bottom: .5em;
|
||||
.title {
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
color: black;
|
||||
font-weight: 400;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.joinButton {
|
||||
.form {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
flex-grow: 1;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.7rem;
|
||||
}
|
||||
|
||||
.col {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
margin-right: 1rem;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
padding-right: 0.1rem;
|
||||
padding-left: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.labelSmall {
|
||||
color: black;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.formElement {
|
||||
position: relative;
|
||||
left: 11em;
|
||||
top: 3em;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.select {
|
||||
background-color: #fff;
|
||||
border: 0;
|
||||
border-bottom: 0.1rem solid $color-gray-lighter;
|
||||
color: $color-gray-light;
|
||||
width: 100%;
|
||||
// appearance: none;
|
||||
height: 1.75rem;
|
||||
}
|
||||
|
||||
.audioMeter {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pullContentRight {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-flow: row;
|
||||
}
|
||||
|
||||
.verticalLine {
|
||||
color: #f3f6f9;
|
||||
border-left: 1px solid;
|
||||
height: 5rem;
|
||||
}
|
||||
|
||||
.enterAudio {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-right: 2rem;
|
||||
}
|
||||
|
||||
.chooseAudio {
|
||||
position:absolute;
|
||||
left:50%;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
|
@ -28,11 +28,24 @@ export default class Chat extends Component {
|
||||
|
||||
return (
|
||||
<section className={styles.chat}>
|
||||
|
||||
<header className={styles.header}>
|
||||
<Link className={styles.closeChat} to="/users">
|
||||
<Icon iconName="left_arrow" /> {title}
|
||||
</Link>
|
||||
<div className={styles.title}>
|
||||
<Link to="/users">
|
||||
<Icon iconName="left_arrow"/> {title}
|
||||
</Link>
|
||||
</div>
|
||||
<div className={styles.closeIcon}>
|
||||
{
|
||||
((this.props.chatID == 'public') ?
|
||||
null :
|
||||
<Link to="/users">
|
||||
<Icon iconName="close" onClick={() => actions.handleClosePrivateChat(chatID)}/>
|
||||
</Link>)
|
||||
}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<MessageList
|
||||
chatId={chatID}
|
||||
messages={messages}
|
||||
|
@ -95,6 +95,9 @@ export default injectIntl(createContainer(({ params, intl }) => {
|
||||
isChatLocked,
|
||||
scrollPosition,
|
||||
actions: {
|
||||
|
||||
handleClosePrivateChat: chatID => ChatService.closePrivateChat(chatID),
|
||||
|
||||
handleSendMessage: message => {
|
||||
let sentMessage = ChatService.sendMessage(chatID, message);
|
||||
ChatService.updateScrollPosition(chatID, null); //null so its scrolls to bottom
|
||||
|
@ -1,7 +1,7 @@
|
||||
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import _ from 'underscore';
|
||||
import _ from 'lodash';
|
||||
import styles from './styles';
|
||||
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import _ from 'underscore';
|
||||
import _ from 'lodash';
|
||||
|
||||
const propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
|
@ -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);
|
@ -4,8 +4,10 @@ import Meetings from '/imports/api/meetings';
|
||||
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import UnreadMessages from '/imports/ui/services/unread-messages';
|
||||
import Storage from '/imports/ui/services/storage/session';
|
||||
|
||||
import { callServer } from '/imports/ui/services/api';
|
||||
import _ from 'lodash';
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const GROUPING_MESSAGES_WINDOW = CHAT_CONFIG.grouping_messages_window;
|
||||
@ -20,6 +22,9 @@ const PUBLIC_CHAT_USERNAME = CHAT_CONFIG.public_username;
|
||||
|
||||
const ScrollCollection = new Mongo.Collection(null);
|
||||
|
||||
// session for closed chat list
|
||||
const CLOSED_CHAT_LIST_KEY = 'closedChatList';
|
||||
|
||||
/* TODO: Same map is done in the user-list/service we should share this someway */
|
||||
|
||||
const mapUser = (user) => ({
|
||||
@ -193,6 +198,13 @@ const sendMessage = (receiverID, message) => {
|
||||
from_color: 0,
|
||||
};
|
||||
|
||||
let currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY);
|
||||
|
||||
// Remove the chat that user send messages from the session.
|
||||
if (_.indexOf(currentClosedChats, receiver.id) > -1) {
|
||||
Storage.setItem(CLOSED_CHAT_LIST_KEY, _.without(currentClosedChats, receiver.id));
|
||||
}
|
||||
|
||||
callServer('sendChat', messagePayload);
|
||||
|
||||
return messagePayload;
|
||||
@ -215,6 +227,17 @@ const updateUnreadMessage = (receiverID, timestamp) => {
|
||||
return UnreadMessages.update(receiverID, timestamp);
|
||||
};
|
||||
|
||||
const closePrivateChat = (chatID) => {
|
||||
|
||||
let currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY) || [];
|
||||
|
||||
if (_.indexOf(currentClosedChats, chatID) < 0) {
|
||||
currentClosedChats.push(chatID);
|
||||
|
||||
Storage.setItem(CLOSED_CHAT_LIST_KEY, currentClosedChats);
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
getPublicMessages,
|
||||
getPrivateMessages,
|
||||
@ -226,4 +249,5 @@ export default {
|
||||
updateScrollPosition,
|
||||
updateUnreadMessage,
|
||||
sendMessage,
|
||||
closePrivateChat,
|
||||
};
|
||||
|
@ -6,23 +6,38 @@
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.closeChat {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-top: $lg-padding-x - $sm-padding-x;
|
||||
margin-bottom: $lg-padding-x;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
@extend %text-elipsis;
|
||||
width: 90%;
|
||||
|
||||
> [class^="icon-bbb-"],
|
||||
> [class*=" icon-bbb-"] {
|
||||
font-size: 85%;
|
||||
}
|
||||
}
|
||||
|
||||
[class='icon-bbb-left_arrow'] {
|
||||
padding-bottom: 5px;
|
||||
.closeIcon {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[class='icon-bbb-left_arrow'],
|
||||
[class='icon-bbb-close']{
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
@ -3,10 +3,19 @@ import { findDOMNode } from 'react-dom';
|
||||
import styles from './styles';
|
||||
import DropdownTrigger from './trigger/component';
|
||||
import DropdownContent from './content/component';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import cx from 'classnames';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
const FOCUSABLE_CHILDREN = `[tabindex]:not([tabindex="-1"]), a, input, button`;
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
close: {
|
||||
id: 'app.dropdown.close',
|
||||
defaultMessage: 'Close',
|
||||
},
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
/**
|
||||
* The dropdown needs a trigger and a content component as childrens
|
||||
@ -44,7 +53,7 @@ const defaultProps = {
|
||||
isOpen: false,
|
||||
};
|
||||
|
||||
export default class Dropdown extends Component {
|
||||
class Dropdown extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { isOpen: false, };
|
||||
@ -113,7 +122,7 @@ export default class Dropdown extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, className, style } = this.props;
|
||||
const { children, className, style, intl } = this.props;
|
||||
|
||||
let trigger = children.find(x => x.type === DropdownTrigger);
|
||||
let content = children.find(x => x.type === DropdownContent);
|
||||
@ -137,6 +146,14 @@ export default class Dropdown extends Component {
|
||||
<div style={style} className={cx(styles.dropdown, className)}>
|
||||
{trigger}
|
||||
{content}
|
||||
{ this.state.isOpen ?
|
||||
<Button
|
||||
className={styles.close}
|
||||
label={intl.formatMessage(intlMessages.close)}
|
||||
size={'lg'}
|
||||
color={'default'}
|
||||
onClick={this.handleHide}
|
||||
/> : null }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -144,3 +161,4 @@ export default class Dropdown extends Component {
|
||||
|
||||
Dropdown.propTypes = propTypes;
|
||||
Dropdown.defaultProps = defaultProps;
|
||||
export default injectIntl(Dropdown);
|
||||
|
@ -40,7 +40,9 @@ export default class DropdownContent extends Component {
|
||||
style={style}
|
||||
aria-expanded={this.props['aria-expanded']}
|
||||
className={cx(styles.content, styles[placementName], className)}>
|
||||
{boundChildren}
|
||||
<div className={styles.scrollable}>
|
||||
{boundChildren}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import styles from '../styles';
|
||||
import _ from 'underscore';
|
||||
import _ from 'lodash';
|
||||
import cx from 'classnames';
|
||||
|
||||
import Icon from '/imports/ui/components/icon/component';
|
||||
|
@ -10,6 +10,11 @@
|
||||
padding: ($line-height-computed / 2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@include mq($small-only) {
|
||||
font-size: $font-size-large * 1.1;
|
||||
padding: $line-height-computed;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -32,6 +37,11 @@
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
@include mq($small-only) {
|
||||
padding: ($line-height-computed / 1.5) 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
@import "../../stylesheets/variables/_all";
|
||||
@import "../../stylesheets/mixins/_scrollable";
|
||||
|
||||
$dropdown-bg: $color-white;
|
||||
$dropdown-color: $color-text;
|
||||
@ -18,7 +19,6 @@ $dropdown-caret-height: 8px;
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
|
||||
border: 1px solid rgba(0, 0, 0, .15);
|
||||
padding: $line-height-computed / 2;
|
||||
// min-width: 150px;
|
||||
z-index: 1000;
|
||||
|
||||
&:after, &:before {
|
||||
@ -35,10 +35,55 @@ $dropdown-caret-height: 8px;
|
||||
&[aria-expanded="true"] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@include mq($small-only) {
|
||||
z-index: 1015;
|
||||
border-radius: 0;
|
||||
background-color: #fff;
|
||||
box-shadow: none;
|
||||
position: fixed;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
border: 0 !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 0 $line-height-computed * 5.25 0 !important;
|
||||
transform: translateX(0) translateY(0) !important;
|
||||
|
||||
&:after, &:before {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scrollable {
|
||||
@include mq($small-only) {
|
||||
@include scrollbox-vertical();
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.trigger {}
|
||||
|
||||
.close {
|
||||
display: none;
|
||||
position: fixed;
|
||||
bottom: 1rem;
|
||||
z-index: 1011;
|
||||
font-size: $font-size-large * 1.1;
|
||||
width: calc(100% - #{($line-height-computed * 2)});
|
||||
left: $line-height-computed;
|
||||
box-shadow: 0 0 0 2rem #fff;
|
||||
|
||||
@include mq($small-only) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* Placements
|
||||
* ==========
|
||||
|
@ -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;
|
@ -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;
|
@ -1,9 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import { withRouter } from 'react-router';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import Modal from '/imports/ui/components/modal/component';
|
||||
import LocalStorage from '/imports/ui/services/storage/local.js';
|
||||
import { clearModal } from '/imports/ui/components/app/service';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
title: {
|
||||
@ -33,33 +31,19 @@ const intlMessages = defineMessages({
|
||||
});
|
||||
|
||||
class LeaveConfirmation extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleLeaveConfirmation = this.handleLeaveConfirmation.bind(this);
|
||||
this.handleCancleLogout = this.handleCancleLogout.bind(this);
|
||||
}
|
||||
|
||||
handleLeaveConfirmation() {
|
||||
Auth.completeLogout();
|
||||
}
|
||||
|
||||
handleCancleLogout() {
|
||||
clearModal();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl } = this.props;
|
||||
const { intl, router } = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={intl.formatMessage(intlMessages.title)}
|
||||
confirm={{
|
||||
callback: this.handleLeaveConfirmation,
|
||||
callback: () => router.push('/logout'),
|
||||
label: intl.formatMessage(intlMessages.confirmLabel),
|
||||
description: intl.formatMessage(intlMessages.confirmDesc),
|
||||
}}
|
||||
dismiss={{
|
||||
callback: this.handleCancleLogout,
|
||||
callback: () => null,
|
||||
label: intl.formatMessage(intlMessages.dismissLabel),
|
||||
description: intl.formatMessage(intlMessages.dismissDesc),
|
||||
}}>
|
||||
@ -69,4 +53,4 @@ class LeaveConfirmation extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
export default injectIntl(LeaveConfirmation);
|
||||
export default withRouter(injectIntl(LeaveConfirmation));
|
||||
|
@ -46,7 +46,10 @@ export default class Modal extends Component {
|
||||
|
||||
handleDismiss() {
|
||||
const { dismiss } = this.props;
|
||||
dismiss.callback(...arguments);
|
||||
if (dismiss && dismiss.callback) {
|
||||
dismiss.callback(...arguments);
|
||||
}
|
||||
|
||||
this.setState({ isOpen: false });
|
||||
clearModal();
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import _ from 'underscore';
|
||||
import _ from 'lodash';
|
||||
import cx from 'classnames';
|
||||
import styles from './styles.scss';
|
||||
|
||||
@ -7,7 +7,7 @@ import { showModal } from '/imports/ui/components/app/service';
|
||||
|
||||
import Button from '../button/component';
|
||||
import RecordingIndicator from './recording-indicator/component';
|
||||
import SettingsDropdown from './settings-dropdown/component';
|
||||
import SettingsDropdownContainer from './settings-dropdown/container';
|
||||
import Icon from '/imports/ui/components/icon/component';
|
||||
import BreakoutJoinConfirmation from '/imports/ui/components/breakout-join-confirmation/component';
|
||||
import Dropdown from '/imports/ui/components/dropdown/component';
|
||||
@ -85,7 +85,7 @@ class NavBar extends Component {
|
||||
<RecordingIndicator beingRecorded={beingRecorded}/>
|
||||
</div>
|
||||
<div className={styles.right}>
|
||||
<SettingsDropdown />
|
||||
<SettingsDropdownContainer />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -57,7 +57,7 @@ export default withRouter(createContainer(({ location, router }) => {
|
||||
};
|
||||
|
||||
const breakouts = Service.getBreakouts();
|
||||
const currentUserId = Auth.getCredentials().requesterUserId;
|
||||
const currentUserId = Auth.userID;
|
||||
|
||||
return {
|
||||
breakouts,
|
||||
|
@ -4,7 +4,7 @@ import Breakouts from '/imports/api/breakouts';
|
||||
const getBreakouts = () => Breakouts.find().fetch();
|
||||
|
||||
const getBreakoutJoinURL = (breakout) => {
|
||||
const currentUserId = Auth.getCredentials().requesterUserId;
|
||||
const currentUserId = Auth.userID;
|
||||
|
||||
if (breakout.users) {
|
||||
const user = breakout.users.find(user => user.userId === currentUserId);
|
||||
|
@ -53,44 +53,16 @@ const intlMessages = defineMessages({
|
||||
id: 'app.navBar.settingsDropdown.leaveSessionDesc',
|
||||
defaultMessage: 'Leave the meeting',
|
||||
},
|
||||
exitFullScreenDesc: {
|
||||
id: 'app.navBar.settingsDropdown.exitFullScreenDesc',
|
||||
defaultMessage: 'exit fullscreen mode',
|
||||
},
|
||||
exitFullScreenLabel: {
|
||||
id: 'app.navBar.settingsDropdown.exitFullScreenLabel',
|
||||
defaultMessage: 'Exit fullscreen',
|
||||
},
|
||||
});
|
||||
|
||||
const toggleFullScreen = () => {
|
||||
let element = document.documentElement;
|
||||
|
||||
if (document.fullscreenEnabled
|
||||
|| document.mozFullScreenEnabled
|
||||
|| document.webkitFullscreenEnabled) {
|
||||
|
||||
// If the page is already fullscreen, exit fullscreen
|
||||
if (document.fullscreenElement
|
||||
|| document.webkitFullscreenElement
|
||||
|| document.mozFullScreenElement
|
||||
|| document.msFullscreenElement) {
|
||||
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
} else if (document.mozCancelFullScreen) {
|
||||
document.mozCancelFullScreen();
|
||||
} else if (document.webkitExitFullscreen) {
|
||||
document.webkitExitFullscreen();
|
||||
}
|
||||
|
||||
// If the page is not currently fullscreen, make fullscreen
|
||||
} else {
|
||||
if (element.requestFullscreen) {
|
||||
element.requestFullscreen();
|
||||
} else if (element.mozRequestFullScreen) {
|
||||
element.mozRequestFullScreen();
|
||||
} else if (element.webkitRequestFullscreen) {
|
||||
element.webkitRequestFullscreen();
|
||||
} else if (element.msRequestFullscreen) {
|
||||
element.msRequestFullscreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const openSettings = () => showModal(<SettingsMenuContainer />);
|
||||
|
||||
const openAbout = () => showModal(<AboutContainer />);
|
||||
@ -103,7 +75,17 @@ class SettingsDropdown extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl } = this.props;
|
||||
|
||||
const { intl, isFullScreen } = this.props;
|
||||
|
||||
let fullScreenLabel = intl.formatMessage(intlMessages.fullscreenLabel);
|
||||
let fullScreenDesc = intl.formatMessage(intlMessages.fullscreenDesc);
|
||||
|
||||
if (isFullScreen) {
|
||||
fullScreenLabel = intl.formatMessage(intlMessages.exitFullScreenLabel);
|
||||
fullScreenDesc = intl.formatMessage(intlMessages.exitFullScreenDesc);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown ref="dropdown">
|
||||
<DropdownTrigger>
|
||||
@ -124,9 +106,9 @@ class SettingsDropdown extends Component {
|
||||
<DropdownList>
|
||||
<DropdownListItem
|
||||
icon="fullscreen"
|
||||
label={intl.formatMessage(intlMessages.fullscreenLabel)}
|
||||
description={intl.formatMessage(intlMessages.fullscreenDesc)}
|
||||
onClick={toggleFullScreen.bind(this)}
|
||||
label={fullScreenLabel}
|
||||
description={fullScreenDesc}
|
||||
onClick={this.props.handleToggleFullscreen}
|
||||
/>
|
||||
<DropdownListItem
|
||||
icon="more"
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
};
|
@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor';
|
||||
import { createContainer } from 'meteor/react-meteor-data';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import _ from 'underscore';
|
||||
import _ from 'lodash';
|
||||
import NavBarService from '../nav-bar/service';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import { humanizeSeconds } from '/imports/utils/humanizeSeconds';
|
||||
|
@ -7,8 +7,8 @@ let getSlideData = (params) => {
|
||||
const { currentSlideNum, presentationId } = params;
|
||||
|
||||
// Get userId and meetingId
|
||||
const userId = AuthSingleton.getCredentials().requesterUserId;
|
||||
const meetingId = AuthSingleton.getCredentials().meetingId;
|
||||
const userId = AuthSingleton.userID;
|
||||
const meetingId = AuthSingleton.meetingID;
|
||||
|
||||
// Find the user object of this specific meeting and userid
|
||||
const currentUser = Users.findOne({
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import React, { Component } from 'react';
|
||||
import Modal from '/imports/ui/components/modal/component';
|
||||
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
|
||||
|
||||
@ -9,10 +8,8 @@ import Application from '/imports/ui/components/settings/submenus/application/co
|
||||
import Participants from '/imports/ui/components/settings/submenus/participants/component';
|
||||
import Video from '/imports/ui/components/settings/submenus/video/component';
|
||||
|
||||
import Button from '../button/component';
|
||||
import Icon from '../icon/component';
|
||||
import styles from './styles';
|
||||
import cx from 'classnames';
|
||||
|
||||
const propTypes = {
|
||||
};
|
||||
|
@ -1,13 +1,13 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { createContainer } from 'meteor/react-meteor-data';
|
||||
import _ from 'underscore';
|
||||
import Settings from './component.jsx';
|
||||
import _ from 'lodash';
|
||||
import Settings from './component';
|
||||
import {
|
||||
getSettingsFor,
|
||||
updateSettings,
|
||||
getClosedCaptionLocales,
|
||||
getUserRoles,
|
||||
} from './service.js';
|
||||
} from './service';
|
||||
|
||||
class SettingsContainer extends Component {
|
||||
render() {
|
||||
|
@ -2,7 +2,7 @@ import Storage from '/imports/ui/services/storage/session';
|
||||
import Users from '/imports/api/users';
|
||||
import Captions from '/imports/api/captions';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import _ from 'underscore';
|
||||
import _ from 'lodash';
|
||||
|
||||
const updateSettings = (obj) => {
|
||||
Object.keys(obj).forEach(k => Storage.setItem(`settings_${k}`, obj[k]));
|
||||
|
@ -56,3 +56,13 @@
|
||||
padding-top: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.testAudioBtn {
|
||||
border: none;
|
||||
padding-left: 0;
|
||||
i {
|
||||
color: $color-primary;
|
||||
}
|
||||
background-color: transparent;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import BaseMenu from '../base/component';
|
||||
import styles from '../styles.scss';
|
||||
|
||||
@ -46,7 +46,7 @@ export default class AudioMenu extends BaseMenu {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.tabContent}>
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
<h3 className={styles.title}>Audio</h3>
|
||||
</div>
|
||||
|
@ -2,15 +2,20 @@ import Users from '/imports/api/users';
|
||||
import Chat from '/imports/api/chat';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import UnreadMessages from '/imports/ui/services/unread-messages';
|
||||
import Storage from '/imports/ui/services/storage/session';
|
||||
import { EMOJI_STATUSES } from '/imports/utils/statuses.js';
|
||||
|
||||
import { callServer } from '/imports/ui/services/api';
|
||||
import _ from 'lodash';
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const USER_CONFIG = Meteor.settings.public.user;
|
||||
const ROLE_MODERATOR = USER_CONFIG.role_moderator;
|
||||
const PRIVATE_CHAT_TYPE = CHAT_CONFIG.type_private;
|
||||
|
||||
// session for closed chat list
|
||||
const CLOSED_CHAT_LIST_KEY = 'closedChatList';
|
||||
|
||||
/* TODO: Same map is done in the chat/service we should share this someway */
|
||||
|
||||
const mapUser = user => ({
|
||||
@ -164,13 +169,12 @@ const getUsers = () => {
|
||||
.fetch();
|
||||
|
||||
return users
|
||||
.map(u => u.user)
|
||||
.map(mapUser)
|
||||
.sort(sortUsers);
|
||||
.map(u => u.user)
|
||||
.map(mapUser)
|
||||
.sort(sortUsers);
|
||||
};
|
||||
|
||||
const getOpenChats = chatID => {
|
||||
window.Users = Users;
|
||||
|
||||
let openChats = Chat
|
||||
.find({ 'message.chat_type': PRIVATE_CHAT_TYPE })
|
||||
@ -178,6 +182,7 @@ const getOpenChats = chatID => {
|
||||
.map(mapOpenChats);
|
||||
|
||||
let currentUserId = Auth.userID;
|
||||
|
||||
if (chatID) {
|
||||
openChats.push(chatID);
|
||||
}
|
||||
@ -193,6 +198,28 @@ const getOpenChats = chatID => {
|
||||
return op;
|
||||
});
|
||||
|
||||
let currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY) || [];
|
||||
let filteredChatList = [];
|
||||
|
||||
openChats.forEach((op) => {
|
||||
|
||||
// When a new private chat message is received, ensure the conversation view is restored.
|
||||
if (op.unreadCounter > 0) {
|
||||
if (_.indexOf(currentClosedChats, op.id) > -1) {
|
||||
Storage.setItem(CLOSED_CHAT_LIST_KEY, _.without(currentClosedChats, op.id));
|
||||
}
|
||||
}
|
||||
|
||||
// Compare openChats with session and push it into filteredChatList
|
||||
// if one of the openChat is not in session.
|
||||
// It will pass to openChats.
|
||||
if (_.indexOf(currentClosedChats, op.id) < 0) {
|
||||
filteredChatList.push(op);
|
||||
}
|
||||
});
|
||||
|
||||
openChats = filteredChatList;
|
||||
|
||||
openChats.push({
|
||||
id: 'public',
|
||||
name: 'Public Chat',
|
||||
|
@ -7,7 +7,7 @@ import { withRouter } from 'react-router';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import styles from './styles.scss';
|
||||
import cx from 'classnames';
|
||||
import _ from 'underscore';
|
||||
import _ from 'lodash';
|
||||
|
||||
import Dropdown from '/imports/ui/components/dropdown/component';
|
||||
import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component';
|
||||
|
@ -8,7 +8,7 @@ function callServer(name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const credentials = Auth.getCredentials();
|
||||
const credentials = Auth.credentials;
|
||||
|
||||
// slice off the first element. That is the function name but we already have that.
|
||||
const args = Array.prototype.slice.call(arguments, 1);
|
||||
|
@ -1,4 +1,8 @@
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
|
||||
import Storage from '/imports/ui/services/storage/session';
|
||||
|
||||
import Users from '/imports/api/users';
|
||||
import { callServer } from '/imports/ui/services/api';
|
||||
|
||||
class Auth {
|
||||
@ -6,11 +10,10 @@ class Auth {
|
||||
this._meetingID = Storage.getItem('meetingID');
|
||||
this._userID = Storage.getItem('userID');
|
||||
this._authToken = Storage.getItem('authToken');
|
||||
this._logoutURL = Storage.getItem('logoutURL');
|
||||
|
||||
if (!this._logoutURL) {
|
||||
this._setLogOut();
|
||||
}
|
||||
this._loggedIn = {
|
||||
value: false,
|
||||
tracker: new Tracker.Dependency,
|
||||
};
|
||||
}
|
||||
|
||||
get meetingID() {
|
||||
@ -40,22 +43,17 @@ class Auth {
|
||||
Storage.setItem('authToken', this._authToken);
|
||||
}
|
||||
|
||||
get logoutURL() {
|
||||
return this._logoutURL;
|
||||
get loggedIn() {
|
||||
this._loggedIn.tracker.depend();
|
||||
return this._loggedIn.value;
|
||||
}
|
||||
|
||||
set logoutURL(logoutURL) {
|
||||
this._logoutURL = logoutURL;
|
||||
Storage.setItem('logoutURL', this._logoutURL);
|
||||
set loggedIn(value) {
|
||||
this._loggedIn.value = value;
|
||||
this._loggedIn.tracker.changed();
|
||||
}
|
||||
|
||||
setCredentials(meeting, user, token) {
|
||||
this.meetingID = meeting;
|
||||
this.userID = user;
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
getCredentials() {
|
||||
get credentials() {
|
||||
return {
|
||||
meetingId: this.meetingID,
|
||||
requesterUserId: this.userID,
|
||||
@ -63,49 +61,114 @@ class Auth {
|
||||
};
|
||||
}
|
||||
|
||||
clearCredentials(callback) {
|
||||
set credentials(value) {
|
||||
throw 'Credentials are read-only';
|
||||
}
|
||||
|
||||
clearCredentials() {
|
||||
this.meetingID = null;
|
||||
this.userID = null;
|
||||
this.token = null;
|
||||
this.loggedIn = false;
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
return callback();
|
||||
return Promise.resolve(...arguments);
|
||||
};
|
||||
|
||||
logout() {
|
||||
if (!this.loggedIn) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
completeLogout() {
|
||||
let logoutURL = this.logoutURL;
|
||||
callServer('userLogout');
|
||||
|
||||
this.clearCredentials(() => {
|
||||
document.location.href = logoutURL;
|
||||
return new Promise((resolve, reject) => {
|
||||
callServer('userLogout', () => {
|
||||
this.fetchLogoutUrl()
|
||||
.then(this.clearCredentials)
|
||||
.then(resolve);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
_setLogOut() {
|
||||
let request;
|
||||
let handleLogoutUrlError;
|
||||
authenticate(meetingID, userID, token) {
|
||||
if (arguments.length) {
|
||||
this.meetingID = meetingID;
|
||||
this.userID = userID;
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
handleLogoutUrlError = function () {
|
||||
console.log('Error : could not find the logoutURL');
|
||||
this.logoutURL = document.location.hostname;
|
||||
};
|
||||
return this._subscribeToCurrentUser()
|
||||
.then(this._addObserverToValidatedField.bind(this));
|
||||
}
|
||||
|
||||
// obtain the logoutURL
|
||||
request = $.ajax({
|
||||
dataType: 'json',
|
||||
url: '/bigbluebutton/api/enter',
|
||||
_subscribeToCurrentUser() {
|
||||
const credentials = this.credentials;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
Tracker.autorun((c) => {
|
||||
setTimeout(() => {
|
||||
c.stop();
|
||||
reject('Authentication subscription timeout.');
|
||||
}, 2000);
|
||||
|
||||
const subscription = Meteor.subscribe('current-user', credentials);
|
||||
if (!subscription.ready()) return;
|
||||
|
||||
resolve(c);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
request.done(data => {
|
||||
if (data.response.logoutURL != null) {
|
||||
this.logoutURL = data.response.logoutURL;
|
||||
} else {
|
||||
return handleLogoutUrlError();
|
||||
}
|
||||
_addObserverToValidatedField(prevComp) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const validationTimeout = setTimeout(() => {
|
||||
this.clearCredentials();
|
||||
reject('Authentication timeout.');
|
||||
}, 2500);
|
||||
|
||||
const didValidate = () => {
|
||||
this.loggedIn = true;
|
||||
clearTimeout(validationTimeout);
|
||||
prevComp.stop();
|
||||
resolve();
|
||||
};
|
||||
|
||||
Tracker.autorun((c) => {
|
||||
const selector = { meetingId: this.meetingID, userId: this.userID };
|
||||
const query = Users.find(selector);
|
||||
|
||||
if (query.count() && query.fetch()[0].validated) {
|
||||
c.stop();
|
||||
didValidate();
|
||||
}
|
||||
|
||||
const handle = query.observeChanges({
|
||||
changed: (id, fields) => {
|
||||
if (id !== this.userID) return;
|
||||
|
||||
if (fields.validated === true) {
|
||||
c.stop();
|
||||
didValidate();
|
||||
}
|
||||
|
||||
if (fields.validated === false) {
|
||||
c.stop();
|
||||
this.clearCredentials();
|
||||
reject('Authentication failed.');
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const credentials = this.credentials;
|
||||
callServer('validateAuthToken', credentials);
|
||||
});
|
||||
}
|
||||
|
||||
return request.fail(() => handleLogoutUrlError());
|
||||
fetchLogoutUrl() {
|
||||
const url = `/bigbluebutton/api/enter`;
|
||||
|
||||
return fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => Promise.resolve(data.response.logoutURL));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import _ from 'underscore';
|
||||
import _ from 'lodash';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
import { EJSON } from 'meteor/ejson';
|
||||
|
||||
|
@ -9,7 +9,7 @@ $color-primary: #299AD5 !default;
|
||||
$color-success: #4DC0A2 !default;
|
||||
$color-danger: #EC6365 !default;
|
||||
|
||||
$color-background: #2A2D36 !default;
|
||||
$color-background: $color-gray-dark !default;
|
||||
|
||||
$color-text: #8A95A5 !default;
|
||||
$color-heading: #4E525E !default;
|
||||
|
@ -15,7 +15,6 @@
|
||||
"grunt-cli": "~1.2.0",
|
||||
"hiredis": "^0.5.0",
|
||||
"history": "~3.3.0",
|
||||
"image-size": "~0.5.0",
|
||||
"meteor-node-stubs": "^0.2.3",
|
||||
"node-sass": "~3.8.0",
|
||||
"probe-image-size": "~2.1.1",
|
||||
@ -32,10 +31,9 @@
|
||||
"react-tabs": "^0.8.2",
|
||||
"react-toggle": "^2.2.0",
|
||||
"redis": "^2.6.2",
|
||||
"underscore": "~1.8.3",
|
||||
"winston": "^2.3.1",
|
||||
"xml2js": "^0.4.17",
|
||||
"xmlhttprequest": "^1.8.0"
|
||||
"lodash": "~4.17.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^6.3.6",
|
||||
|
@ -23,3 +23,5 @@ app:
|
||||
|
||||
# Name displayed in the brower URL
|
||||
basename: '/html5client'
|
||||
|
||||
defaultLocale: 'en'
|
||||
|
@ -12,7 +12,6 @@
|
||||
"app.chat.titlePrivate": "Private Chat with {name}",
|
||||
"app.chat.partnerDisconnected": "{name} has left the meeting",
|
||||
"app.chat.moreMessages": "More messages below",
|
||||
"app.kickMessage": "You have been kicked out of the meeting",
|
||||
"app.presentation.presentationToolbar.prevSlideLabel": "Previous slide",
|
||||
"app.presentation.presentationToolbar.prevSlideDescrip": "Change the presentation to the previous slide",
|
||||
"app.presentation.presentationToolbar.nextSlideLabel": "Next slide",
|
||||
@ -37,6 +36,8 @@
|
||||
"app.navBar.settingsDropdown.settingsDesc": "Change the general settings",
|
||||
"app.navBar.settingsDropdown.aboutDesc": "Show information about the client",
|
||||
"app.navBar.settingsDropdown.leaveSessionDesc": "Leave the meeting",
|
||||
"app.navBar.settingsDropdown.exitFullScreenLabel": "Exit fullscreen",
|
||||
"app.navBar.settingsDropdown.exitFullScreenDesc": "Exit fullscreen mode",
|
||||
"app.leaveConfirmation.title": "Leave Session",
|
||||
"app.leaveConfirmation.message": "Do you want to leave this meeting?",
|
||||
"app.leaveConfirmation.confirmLabel": "Leave",
|
||||
@ -109,5 +110,11 @@
|
||||
"app.audio.audioSettings.speakerSourceLabel": "Speaker source",
|
||||
"app.audio.audioSettings.microphoneStreamLabel": "Your audio stream volume",
|
||||
"app.audio.listenOnly.backLabel": "Back",
|
||||
"app.audio.listenOnly.closeLabel": "Close"
|
||||
"app.audio.listenOnly.closeLabel": "Close",
|
||||
"app.error.kicked": "You have been kicked out of the meeting",
|
||||
"app.dropdown.close": "Close",
|
||||
"app.error.500": "Ops, something went wrong",
|
||||
"app.error.404": "Not found",
|
||||
"app.error.401": "Unauthorized",
|
||||
"app.error.403": "Forbidden"
|
||||
}
|
@ -5,7 +5,7 @@
|
||||
-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
|
||||
<metadata>
|
||||
Created by FontForge 20161004 at Fri Dec 30 11:22:51 2016
|
||||
Created by FontForge 20161004 at Thu Mar 23 10:01:15 2017
|
||||
By Ghazi
|
||||
Copyright (c) 2016, BlindSide Networks Inc.
|
||||
</metadata>
|
||||
@ -19,7 +19,7 @@ Copyright (c) 2016, BlindSide Networks Inc.
|
||||
panose-1="2 0 5 3 0 0 0 0 0 0"
|
||||
ascent="819"
|
||||
descent="-205"
|
||||
bbox="0 -205 1212 820"
|
||||
bbox="0 -205 1024 820"
|
||||
underline-thickness="51"
|
||||
underline-position="-102"
|
||||
unicode-range="U+0020-E932"
|
||||
@ -31,20 +31,20 @@ Copyright (c) 2016, BlindSide Networks Inc.
|
||||
<glyph glyph-name="space" unicode=" " horiz-adv-x="524"
|
||||
/>
|
||||
<glyph glyph-name="logout" unicode=""
|
||||
d="M160 -117h84c19 0 30 -14 30 -32c0 -22 -7 -34 -30 -34h-84c-83 0 -150 66 -150 150v670c0 83 67 150 150 150h84c16 0 30 -17 30 -32c0 -16 -9 -32 -30 -32h-84c-47 0 -87 -40 -87 -86v-667c0 -47 40 -87 87 -87zM676 -9c-6 -5 -14 -8 -22 -8c-7 0 -15 3 -21 8
|
||||
s-8 12 -8 19c0 9 5 20 12 27l233 233h-625c-22 0 -35 14 -35 30c0 17 13 30 35 30h625l-233 233c-7 7 -11 16 -11 25s4 17 11 23c6 5 14 8 21 8c8 0 16 -3 22 -9c14 -13 284 -283 310 -310c-30 -30 -272 -270 -314 -309z" />
|
||||
d="M160 -117h84c19 0 30 -14 30 -32c0 -16 -7 -34 -30 -34h-84c-83 0 -150 66 -150 150v670c0 83 67 150 150 150h84c16 0 30 -17 30 -32c0 -16 -9 -32 -30 -32h-84c-47 0 -87 -40 -87 -86v-667c0 -47 40 -87 87 -87zM676 -9c-6 -5 -14 -8 -22 -8c-7 0 -15 3 -21 8
|
||||
s-8 12 -8 19c0 9 5 20 12 27l233 233h-625c-22 0 -35 12 -35 30s13 30 35 30h625l-233 233c-7 7 -11 16 -11 25s4 17 11 23c6 5 14 8 21 8c8 0 16 -3 22 -9l310 -310c-30 -30 -314 -309 -314 -309z" />
|
||||
<glyph glyph-name="more" unicode=""
|
||||
d="M17 307c0 60 49 109 109 109c61 0 110 -49 110 -109s-49 -109 -110 -109c-60 0 -109 49 -109 109zM403 307c0 60 49 109 109 109s109 -49 109 -109s-49 -109 -109 -109s-109 49 -109 109zM788 307c0 60 49 109 110 109c60 0 109 -49 109 -109s-49 -109 -109 -109
|
||||
c-61 0 -110 49 -110 109z" />
|
||||
<glyph glyph-name="promote" unicode="" horiz-adv-x="1028"
|
||||
d="M993 601c10 -8 11 -18 11 -28v-522c0 -14 -10 -33 -24 -33h-4c0 0 -520 129 -529 129v0c0 -65 -58 -110 -119 -110c-65 0 -120 55 -120 120v44l-128 31v-9c0 -15 -18 -27 -32 -27c-17 0 -31 12 -31 29v184c0 14 17 28 31 28c18 0 32 -11 32 -28v-17l886 215
|
||||
c7 2 18 1 27 -6zM331 92c20 0 55 7 55 58l-113 27v-30c0 -31 27 -55 58 -55zM944 82v450l-864 -211v-31c287 -69 577 -139 864 -208z" />
|
||||
d="M993 601c10 -8 11 -18 11 -28v-523c0 -14 -5 -20 -11 -25c-10 -8 -14 -8 -17 -7l-529 129c0 -65 -58 -110 -119 -110c-65 0 -120 55 -120 120v44l-128 31v-9c0 -15 -18 -27 -32 -27c-17 0 -31 12 -31 29v184c0 14 17 28 31 28c18 0 32 -11 32 -28v-17l886 215
|
||||
c7 2 18 1 27 -6zM331 92c20 0 55 7 55 58l-113 27v-30c0 -31 27 -55 58 -55zM944 82v450l-864 -211v-31c287 -69 864 -208 864 -208z" />
|
||||
<glyph glyph-name="video_off" unicode=""
|
||||
d="M963 78l-236 106c-10 5 -20 17 -20 31v167c0 14 6 21 20 27l236 106c4 2 9 3 13 3c16 0 31 -14 31 -34v-378c0 -18 -15 -31 -31 -31c-4 0 -9 1 -13 3zM945 153v280l-174 -78v-123zM915 662l-212 -208v-410c0 -17 -14 -31 -31 -31h-409s-110 -110 -113 -112
|
||||
c-5 -4 -12 -6 -18 -6c-10 0 -20 5 -26 13c-4 5 -6 11 -6 17c0 9 5 19 13 27l61 61h-123c-17 0 -31 14 -31 31v526c0 17 14 30 31 30h625c17 0 31 -13 31 -30v-24l160 160c5 5 13 8 20 8c8 0 17 -4 22 -9c8 -8 13 -17 13 -26c0 -6 -2 -12 -7 -17zM81 74h155l406 407v58h-561
|
||||
v-465zM642 74v318l-314 -318h314z" />
|
||||
<glyph glyph-name="user" unicode=""
|
||||
d="M860 85c79 -55 123 -143 123 -239c0 -17 -14 -31 -31 -31h-880c-17 0 -31 14 -31 31c0 96 44 184 123 239c85 58 198 86 348 86s263 -28 348 -86zM106 -123v-4h812c-10 65 -44 120 -95 158c-75 51 -171 75 -311 75s-236 -21 -311 -72c-55 -38 -88 -96 -95 -157zM512 239
|
||||
d="M860 85c79 -55 123 -143 123 -239c0 -17 -14 -31 -31 -31h-880c-17 0 -31 14 -31 31c0 96 37 179 123 239c115 79 198 86 348 86s263 -28 348 -86zM106 -123v-4h812c-10 65 -44 120 -95 158c-75 51 -171 75 -311 75s-236 -21 -311 -72c-55 -38 -88 -96 -95 -157zM512 239
|
||||
c-147 0 -263 115 -263 273c0 151 112 290 263 290c154 0 263 -136 263 -290c0 -153 -120 -273 -263 -273zM307 515c0 -105 89 -211 202 -211s201 85 201 211c0 118 -78 222 -198 222c-114 0 -205 -110 -205 -222z" />
|
||||
<glyph glyph-name="up_arrow" unicode=""
|
||||
d="M659 502c8 -8 12 -18 12 -29c0 -9 -3 -18 -10 -25c-8 -8 -18 -13 -29 -13c-10 0 -20 4 -28 12l-221 222v-816c0 -20 -20 -39 -40 -39s-39 19 -39 39v816l-222 -222c-8 -8 -17 -11 -27 -11s-20 4 -28 11c-8 8 -12 17 -12 27s4 20 12 28l290 286c8 8 17 13 27 13
|
||||
@ -65,9 +65,9 @@ d="M58 -205c-24 0 -41 17 -41 41c0 10 3 20 10 27l396 449l-396 439c-8 8 -11 16 -11
|
||||
d="M973 802c20 0 34 -14 37 -34v-662c0 -20 -14 -35 -34 -35h-375l222 -201c8 -7 17 -20 17 -33c0 -6 -2 -12 -7 -18s-14 -8 -23 -8c-12 0 -24 4 -32 11l-232 209v-190c0 -19 -11 -36 -32 -36c-17 0 -33 16 -33 36v190c-20 -19 -196 -174 -236 -210c-6 -5 -16 -9 -27 -9
|
||||
c-9 0 -18 2 -23 9c-4 5 -6 11 -6 17c0 11 5 23 16 32l222 198h-376c-20 0 -34 14 -34 34v666c0 20 14 34 34 34h922zM940 136v427h-858v-427h858zM82 631h858v106h-858v-106z" />
|
||||
<glyph glyph-name="listen" unicode=""
|
||||
d="M1096 310c72 -51 116 -133 116 -218c0 -150 -119 -277 -273 -277c-17 0 -31 14 -31 31v485c0 17 14 31 31 31c31 0 61 -7 92 -17c0 227 -233 388 -416 388c-190 0 -393 -146 -417 -375c31 10 61 17 92 17c17 0 31 -13 31 -30v-485c0 -17 -14 -31 -31 -31
|
||||
c-89 0 -171 41 -222 113c-35 49 -52 104 -52 160c0 85 40 169 114 225c0 263 222 475 485 475c266 0 481 -222 481 -492zM256 -106v416c-89 -14 -160 -85 -174 -174c-2 -12 -3 -24 -3 -36c0 -99 73 -188 177 -206zM973 -123c89 17 160 85 174 177c2 12 3 24 3 36
|
||||
c0 100 -73 189 -177 207v-420z" />
|
||||
d="M911 310c62 -44 96 -109 96 -180c0 -123 -99 -229 -225 -229c-14 0 -24 10 -24 24v402c0 14 10 24 24 24c24 0 51 -6 75 -13c-10 170 -140 307 -311 324c-11 1 -21 1 -31 1c-178 0 -332 -135 -348 -315c24 7 51 14 75 14c14 0 24 -11 24 -24v-403c0 -14 -10 -24 -24 -24
|
||||
c-75 0 -143 34 -184 92c-30 41 -44 88 -44 134c0 71 33 142 95 187c7 219 185 393 403 396c222 -3 399 -184 399 -410zM215 -34v344c-72 -10 -133 -68 -143 -143c-2 -10 -3 -20 -3 -30c0 -81 60 -156 146 -171zM809 -51c75 17 133 71 143 150c2 10 3 20 3 30
|
||||
c0 81 -60 156 -146 171v-351z" />
|
||||
<glyph glyph-name="left_arrow" unicode=""
|
||||
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=""
|
||||
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Binary file not shown.
Binary file not shown.
0
record-and-playback/core/scripts/README
Executable file → Normal file
0
record-and-playback/core/scripts/README
Executable file → Normal file
0
record-and-playback/core/scripts/bigbluebutton.yml
Executable file → Normal file
0
record-and-playback/core/scripts/bigbluebutton.yml
Executable file → Normal file
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user