Merge branch 'master' of github.com:bigbluebutton/bigbluebutton into audio-restructuring
@ -86,7 +86,7 @@ trait UsersApp {
|
||||
|
||||
def handleValidateAuthToken(msg: ValidateAuthToken) {
|
||||
log.info("Got ValidateAuthToken message. meetingId=" + msg.meetingID + " userId=" + msg.userId)
|
||||
usersModel.getRegisteredUserWithToken(msg.token) match {
|
||||
usersModel.getRegisteredUserWithToken(msg.token, msg.userId) match {
|
||||
case Some(u) =>
|
||||
{
|
||||
val replyTo = mProps.meetingID + '/' + msg.userId
|
||||
@ -319,7 +319,7 @@ trait UsersApp {
|
||||
def handleUserJoin(msg: UserJoining): Unit = {
|
||||
log.debug("Received user joined meeting. metingId=" + mProps.meetingID + " userId=" + msg.userID)
|
||||
|
||||
val regUser = usersModel.getRegisteredUserWithToken(msg.authToken)
|
||||
val regUser = usersModel.getRegisteredUserWithToken(msg.authToken, msg.userID)
|
||||
regUser foreach { ru =>
|
||||
log.debug("Found registered user. metingId=" + mProps.meetingID + " userId=" + msg.userID + " ru=" + ru)
|
||||
|
||||
|
@ -38,8 +38,21 @@ class UsersModel {
|
||||
regUsers += token -> regUser
|
||||
}
|
||||
|
||||
def getRegisteredUserWithToken(token: String): Option[RegisteredUser] = {
|
||||
regUsers.get(token)
|
||||
def getRegisteredUserWithToken(token: String, userId: String): Option[RegisteredUser] = {
|
||||
|
||||
def isSameUserId(ru: RegisteredUser, userId: String): Option[RegisteredUser] = {
|
||||
if (userId.startsWith(ru.id)) {
|
||||
Some(ru)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
ru <- regUsers.get(token)
|
||||
user <- isSameUserId(ru, userId)
|
||||
} yield user
|
||||
|
||||
}
|
||||
|
||||
def generateWebUserId: String = {
|
||||
|
@ -55,18 +55,19 @@ public class RegisterUserMessage implements IBigBlueButtonMessage {
|
||||
if (payload.has(Constants.MEETING_ID)
|
||||
&& payload.has(Constants.NAME)
|
||||
&& payload.has(Constants.ROLE)
|
||||
&& payload.has(Constants.USER_ID)
|
||||
&& payload.has(Constants.EXT_USER_ID)
|
||||
&& payload.has(Constants.AUTH_TOKEN)) {
|
||||
|
||||
String meetingID = payload.get(Constants.MEETING_ID).getAsString();
|
||||
String fullname = payload.get(Constants.NAME).getAsString();
|
||||
String role = payload.get(Constants.ROLE).getAsString();
|
||||
String userId = payload.get(Constants.USER_ID).getAsString();
|
||||
String externUserID = payload.get(Constants.EXT_USER_ID).getAsString();
|
||||
String authToken = payload.get(Constants.AUTH_TOKEN).getAsString();
|
||||
String avatarURL = payload.get(Constants.AVATAR_URL).getAsString();
|
||||
|
||||
//use externalUserId twice - once for external, once for internal
|
||||
return new RegisterUserMessage(meetingID, externUserID, fullname, role, externUserID, authToken, avatarURL);
|
||||
|
||||
return new RegisterUserMessage(meetingID, userId, fullname, role, externUserID, authToken, avatarURL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -195,4 +195,13 @@ public class RecordingMetadata {
|
||||
public Boolean hasError() {
|
||||
return processingError;
|
||||
}
|
||||
|
||||
public Integer calculateDuration() {
|
||||
if ((endTime == null) || (endTime == "") || (startTime == null) || (startTime == "")) return 0;
|
||||
|
||||
int start = (int) Math.ceil((Long.parseLong(startTime)) / 60000.0);
|
||||
int end = (int) Math.ceil((Long.parseLong(endTime)) / 60000.0);
|
||||
|
||||
return end - start;
|
||||
}
|
||||
}
|
||||
|
@ -47,6 +47,15 @@ public class RecordingMetadataPlayback {
|
||||
return duration;
|
||||
}
|
||||
|
||||
public Long calculateDuration() {
|
||||
if (duration > 0) {
|
||||
// convert to minutes
|
||||
return duration / 60000;
|
||||
} else {
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
|
||||
public void setExtensions(Extensions extensions) {
|
||||
this.extensions = extensions;
|
||||
}
|
||||
|
@ -1,78 +1,79 @@
|
||||
<#-- GET_RECORDINGS FreeMarker XML template -->
|
||||
<#compress>
|
||||
<response>
|
||||
<#-- Where code is a 'SUCCESS' or 'FAILED' String -->
|
||||
<returncode>${returnCode}</returncode>
|
||||
<meetingName>${meeting.getName()}</meetingName>
|
||||
<meetingID>${meeting.getExternalId()}</meetingID>
|
||||
<internalMeetingID>${meeting.getInternalId()}</internalMeetingID>
|
||||
<createTime>${meeting.getCreateTime()?c}</createTime>
|
||||
<createDate>${createdOn}</createDate>
|
||||
<voiceBridge>${meeting.getTelVoice()}</voiceBridge>
|
||||
<dialNumber>${meeting.getDialNumber()}</dialNumber>
|
||||
<attendeePW>${meeting.getViewerPassword()}</attendeePW>
|
||||
<moderatorPW>${meeting.getModeratorPassword()}</moderatorPW>
|
||||
<running>${meeting.isRunning()?c}</running>
|
||||
<duration>${meeting.getDuration()}</duration>
|
||||
<hasUserJoined>${meeting.hasUserJoined()?c}</hasUserJoined>
|
||||
<recording>${meeting.isRecord()?c}</recording>
|
||||
<hasBeenForciblyEnded>${meeting.isForciblyEnded()?c}</hasBeenForciblyEnded>
|
||||
<startTime>${meeting.getStartTime()?c}</startTime>
|
||||
<endTime>${meeting.getEndTime()}</endTime>
|
||||
<participantCount>${meeting.getNumUsers()}</participantCount>
|
||||
<listenerCount>${meeting.getNumListenOnly()}</listenerCount>
|
||||
<voiceParticipantCount>${meeting.getNumVoiceJoined()}</voiceParticipantCount>
|
||||
<videoCount>${meeting.getNumVideos()}</videoCount>
|
||||
<maxUsers>${meeting.getMaxUsers()}</maxUsers>
|
||||
<moderatorCount>${meeting.getNumModerators()}</moderatorCount>
|
||||
<attendees>
|
||||
<#list meeting.getUsers() as att>
|
||||
<#-- Where code is a 'SUCCESS' or 'FAILED' String -->
|
||||
<returncode>${returnCode}</returncode>
|
||||
<meetingName>${meeting.getName()?html}</meetingName>
|
||||
<meetingID>${meeting.getExternalId()?html}</meetingID>
|
||||
<internalMeetingID>${meeting.getInternalId()}</internalMeetingID>
|
||||
<createTime>${meeting.getCreateTime()?c}</createTime>
|
||||
<createDate>${createdOn}</createDate>
|
||||
<voiceBridge>${meeting.getTelVoice()}</voiceBridge>
|
||||
<dialNumber>${meeting.getDialNumber()}</dialNumber>
|
||||
<attendeePW>${meeting.getViewerPassword()?html}</attendeePW>
|
||||
<moderatorPW>${meeting.getModeratorPassword()?html}</moderatorPW>
|
||||
<running>${meeting.isRunning()?c}</running>
|
||||
<duration>${meeting.getDuration()}</duration>
|
||||
<hasUserJoined>${meeting.hasUserJoined()?c}</hasUserJoined>
|
||||
<recording>${meeting.isRecord()?c}</recording>
|
||||
<hasBeenForciblyEnded>${meeting.isForciblyEnded()?c}</hasBeenForciblyEnded>
|
||||
<startTime>${meeting.getStartTime()?c}</startTime>
|
||||
<endTime>${meeting.getEndTime()}</endTime>
|
||||
<participantCount>${meeting.getNumUsers()}</participantCount>
|
||||
<listenerCount>${meeting.getNumListenOnly()}</listenerCount>
|
||||
<voiceParticipantCount>${meeting.getNumVoiceJoined()}</voiceParticipantCount>
|
||||
<videoCount>${meeting.getNumVideos()}</videoCount>
|
||||
<maxUsers>${meeting.getMaxUsers()}</maxUsers>
|
||||
<moderatorCount>${meeting.getNumModerators()}</moderatorCount>
|
||||
<attendees>
|
||||
<#list meeting.getUsers() as att>
|
||||
<attendee>
|
||||
<userID>${att.getInternalUserId()}</userID>
|
||||
<fullName>${att.getFullname()}</fullName>
|
||||
<fullName>${att.getFullname()?html}</fullName>
|
||||
<role>${att.getRole()}</role>
|
||||
<isPresenter>${att.isPresenter()?c}</isPresenter>
|
||||
<isListeningOnly>${att.isListeningOnly()?c}</isListeningOnly>
|
||||
<hasJoinedVoice>${att.isVoiceJoined()?c}</hasJoinedVoice>
|
||||
<hasVideo>${att.hasVideo()?c}</hasVideo>
|
||||
<#if meeting.getUserCustomData(att.getExternalUserId())??>
|
||||
<#assign ucd = meeting.getUserCustomData(att.getExternalUserId())>
|
||||
<customdata>
|
||||
<#list ucd?keys as prop>
|
||||
<${prop}><![CDATA[${ucd[prop]}]]></${prop}>
|
||||
</#list>
|
||||
</customdata>
|
||||
</#if>
|
||||
</attendee>
|
||||
</#list>
|
||||
</attendees>
|
||||
<#assign m = meeting.getMetadata()>
|
||||
<metadata>
|
||||
<#list m?keys as prop>
|
||||
<${prop}><![CDATA[${m[prop]}]]></${prop}>
|
||||
</#list>
|
||||
</metadata>
|
||||
|
||||
<#if messageKey?has_content>
|
||||
<messageKey>${messageKey}</messageKey>
|
||||
<#assign ucd = meeting.getUserCustomData(att.getExternalUserId())>
|
||||
<customdata>
|
||||
<#list ucd?keys as prop>
|
||||
<${(prop)?html}>${(ucd[prop])?html}</${(prop)?html}>
|
||||
</#list>
|
||||
</customdata>
|
||||
</#if>
|
||||
</attendee>
|
||||
</#list>
|
||||
</attendees>
|
||||
<#assign m = meeting.getMetadata()>
|
||||
<metadata>
|
||||
<#list m?keys as prop>
|
||||
<${(prop)?html}>${(m[prop])?html}</${(prop)?html}>
|
||||
</#list>
|
||||
</metadata>
|
||||
|
||||
<#if message?has_content>
|
||||
<message>${message}</message>
|
||||
</#if>
|
||||
<#if messageKey?has_content>
|
||||
<messageKey>${messageKey}</messageKey>
|
||||
</#if>
|
||||
|
||||
<isBreakout>${meeting.isBreakout()?c}</isBreakout>
|
||||
<#if message?has_content>
|
||||
<message>${message}</message>
|
||||
</#if>
|
||||
|
||||
<#if meeting.isBreakout()>
|
||||
<parentMeetingID>${meeting.getParentMeetingId()}</parentMeetingID>
|
||||
<sequence>${meeting.getSequence()}</sequence>
|
||||
</#if>
|
||||
<isBreakout>${meeting.isBreakout()?c}</isBreakout>
|
||||
|
||||
<#list meeting.getBreakoutRooms()>
|
||||
<breakoutRooms>
|
||||
<#items as room>
|
||||
<breakout>${room}</breakout>
|
||||
</#items>
|
||||
</breakoutRooms>
|
||||
</#list>
|
||||
<#if meeting.isBreakout()>
|
||||
<parentMeetingID>${meeting.getParentMeetingId()}</parentMeetingID>
|
||||
<sequence>${meeting.getSequence()}</sequence>
|
||||
</#if>
|
||||
|
||||
</response>
|
||||
<#list meeting.getBreakoutRooms()>
|
||||
<breakoutRooms>
|
||||
<#items as room>
|
||||
<breakout>${room}</breakout>
|
||||
</#items>
|
||||
</breakoutRooms>
|
||||
</#list>
|
||||
|
||||
</response>
|
||||
</#compress>
|
@ -1,4 +1,4 @@
|
||||
<#-- GET_RECORDINGS FreeMarker XML template -->
|
||||
<#compress>
|
||||
<response>
|
||||
<#-- Where code is a 'SUCCESS' or 'FAILED' String -->
|
||||
<returncode>${returnCode}</returncode>
|
||||
@ -7,15 +7,15 @@
|
||||
<#items as meetingDetail>
|
||||
<#assign meeting = meetingDetail.getMeeting()>
|
||||
<meeting>
|
||||
<meetingName>${meeting.getName()}</meetingName>
|
||||
<meetingID>${meeting.getExternalId()}</meetingID>
|
||||
<meetingName>${meeting.getName()?html}</meetingName>
|
||||
<meetingID>${meeting.getExternalId()?html}</meetingID>
|
||||
<internalMeetingID>${meeting.getInternalId()}</internalMeetingID>
|
||||
<createTime>${meeting.getCreateTime()?c}</createTime>
|
||||
<createDate>${meetingDetail.getCreatedOn()}</createDate>
|
||||
<voiceBridge>${meeting.getTelVoice()}</voiceBridge>
|
||||
<dialNumber>${meeting.getDialNumber()}</dialNumber>
|
||||
<attendeePW>${meeting.getViewerPassword()}</attendeePW>
|
||||
<moderatorPW>${meeting.getModeratorPassword()}</moderatorPW>
|
||||
<attendeePW>${meeting.getViewerPassword()?html}</attendeePW>
|
||||
<moderatorPW>${meeting.getModeratorPassword()?html}</moderatorPW>
|
||||
<running>${meeting.isRunning()?c}</running>
|
||||
<duration>${meeting.getDuration()}</duration>
|
||||
<hasUserJoined>${meeting.hasUserJoined()?c}</hasUserJoined>
|
||||
@ -33,7 +33,7 @@
|
||||
<#list meetingDetail.meeting.getUsers() as att>
|
||||
<attendee>
|
||||
<userID>${att.getInternalUserId()}</userID>
|
||||
<fullName>${att.getFullname()}</fullName>
|
||||
<fullName>${att.getFullname()?html}</fullName>
|
||||
<role>${att.getRole()}</role>
|
||||
<isPresenter>${att.isPresenter()?c}</isPresenter>
|
||||
<isListeningOnly>${att.isListeningOnly()?c}</isListeningOnly>
|
||||
@ -43,7 +43,7 @@
|
||||
<#assign ucd = meetingDetail.meeting.getUserCustomData(att.getExternalUserId())>
|
||||
<customdata>
|
||||
<#list ucd?keys as prop>
|
||||
<${prop}><![CDATA[${ucd[prop]}]]></${prop}>
|
||||
<${(prop)?html}>${(ucd[prop])?html}</${(prop)?html}>
|
||||
</#list>
|
||||
</customdata>
|
||||
</#if>
|
||||
@ -53,7 +53,7 @@
|
||||
<#assign m = meetingDetail.meeting.getMetadata()>
|
||||
<metadata>
|
||||
<#list m?keys as prop>
|
||||
<${prop}><![CDATA[${m[prop]}]]></${prop}>
|
||||
<${(prop)?html}>${(m[prop])?html}</${(prop)?html}>
|
||||
</#list>
|
||||
</metadata>
|
||||
|
||||
@ -75,4 +75,5 @@
|
||||
</#items>
|
||||
</meetings>
|
||||
</#list>
|
||||
</response>
|
||||
</response>
|
||||
</#compress>
|
@ -3,11 +3,11 @@
|
||||
<#if r.getMeeting()??>
|
||||
<meetingID>${r.getMeeting().getId()?html}</meetingID>
|
||||
<externalMeetingID>${r.getMeeting().getExternalId()?html}</externalMeetingID>
|
||||
<name><![CDATA[${r.getMeeting().getName()}]]></name>
|
||||
<name>${r.getMeeting().getName()?html}</name>
|
||||
<isBreakout>${r.getMeeting().isBreakout()?c}</isBreakout>
|
||||
<#else>
|
||||
<meetingID>${r.getMeetingId()?html}</meetingID>
|
||||
<name><![CDATA[${r.getMeetingName()}]]></name>
|
||||
<name>${r.getMeetingName()?html}</name>
|
||||
<isBreakout>${r.isBreakout()?c}</isBreakout>
|
||||
</#if>
|
||||
|
||||
@ -38,7 +38,7 @@
|
||||
<#assign m = r.getMeta().get()>
|
||||
<metadata>
|
||||
<#list m?keys as prop>
|
||||
<${prop}><![CDATA[${(m[prop])!""}]]></${prop}>
|
||||
<${(prop)?html}>${((m[prop])?html)!""}</${(prop)?html}>
|
||||
</#list>
|
||||
</metadata>
|
||||
|
||||
@ -48,7 +48,12 @@
|
||||
<type>${pb.getFormat()}</type>
|
||||
<url>${pb.getLink()}</url>
|
||||
<processingTime>${pb.getProcessingTime()?c}</processingTime>
|
||||
<length>${pb.getDuration()?c}</length>
|
||||
|
||||
<#if pb.getDuration() == 0>
|
||||
<length>${r.calculateDuration()?c}</length>
|
||||
<#else>
|
||||
<length>${pb.calculateDuration()?c}</length>
|
||||
</#if>
|
||||
|
||||
<#if pb.getExtensions()??>
|
||||
<#if pb.getExtensions().getPreview()??>
|
||||
@ -57,11 +62,11 @@
|
||||
<#if prev.getImages()??>
|
||||
<#list prev.getImages()>
|
||||
<images>
|
||||
<#items as image>
|
||||
<#if image??>
|
||||
<image width="${image.getWidth()}" height="${image.getHeight()}" alt="${image.getAlt()}">${image.getValue()!"Link not found."}</image>
|
||||
</#if>
|
||||
</#items>
|
||||
<#items as image>
|
||||
<#if image??>
|
||||
<image width="${image.getWidth()}" height="${image.getHeight()}" alt="${image.getAlt()?html}">${image.getValue()!"Link not found."}</image>
|
||||
</#if>
|
||||
</#items>
|
||||
</images>
|
||||
</#list>
|
||||
</#if>
|
||||
|
@ -503,17 +503,24 @@ TitleWindow {
|
||||
|
||||
}
|
||||
|
||||
.presentationUploadFileFormatHintBoxStyle {
|
||||
.presentationUploadFileFormatHintBoxStyle, .audioBroswerHintBoxStyle {
|
||||
backgroundColor: #D4D4D4;
|
||||
dropShadowEnabled: false;
|
||||
paddingLeft: 10;
|
||||
paddingRight: 10
|
||||
paddingRight: 10;
|
||||
}
|
||||
|
||||
.presentationUploadFileFormatHintTextStyle {
|
||||
.presentationUploadFileFormatHintTextStyle, .audioBroswerHintTextStyle {
|
||||
fontWeight: bold;
|
||||
}
|
||||
|
||||
.audioBroswerHintBoxStyle {
|
||||
paddingLeft: 5;
|
||||
paddingRight: 5;
|
||||
paddingBottom : 8;
|
||||
paddingTop : 8;
|
||||
}
|
||||
|
||||
.desktopShareViewStyle {
|
||||
backgroundColor: #FFFFFF;
|
||||
paddingTop: 15;
|
||||
@ -1037,6 +1044,13 @@ AlertForm {
|
||||
refreshImage: Embed(source='assets/images/status_refresh.png');
|
||||
}
|
||||
|
||||
.webRTCAudioStatusStyle {
|
||||
strongAudioStatus: Embed(source='assets/images/strong_audio_status.png');
|
||||
almostStrongAudioStatus: Embed(source='assets/images/almost_strong_audio_status.png');
|
||||
almostWeakAudioStatus: Embed(source='assets/images/almost_weak_audio_status.png');
|
||||
weakAudioStatus: Embed(source='assets/images/weak_audio_status.png');
|
||||
}
|
||||
|
||||
.warningButtonStyle {
|
||||
icon: Embed('assets/images/status_warning_20.png');
|
||||
}
|
||||
|
After Width: | Height: | Size: 104 B |
After Width: | Height: | Size: 103 B |
After Width: | Height: | Size: 102 B |
After Width: | Height: | Size: 100 B |
@ -113,6 +113,10 @@ bbb.clientstatus.browser.message = Your browser ({0}) is not up-to-date. Recomme
|
||||
bbb.clientstatus.flash.title = Flash Player
|
||||
bbb.clientstatus.flash.message = Your Flash Player plugin ({0}) is out-of-date. Recommend updating to the latest version.
|
||||
bbb.clientstatus.webrtc.title = Audio
|
||||
bbb.clientstatus.webrtc.strongStatus = Your WebRTC audio connection is great
|
||||
bbb.clientstatus.webrtc.almostStrongStatus = Your WebRTC audio connection is fine
|
||||
bbb.clientstatus.webrtc.almostWeakStatus = Your WebRTC audio connection is bad
|
||||
bbb.clientstatus.webrtc.weakStatus = Maybe there is a problem with your WebRTC audio connection
|
||||
bbb.clientstatus.webrtc.message = Recommend using either Firefox or Chrome for better audio.
|
||||
bbb.window.minimizeBtn.toolTip = Minimize
|
||||
bbb.window.maximizeRestoreBtn.toolTip = Maximize
|
||||
|
@ -60,6 +60,7 @@
|
||||
enabledEchoCancel="true"
|
||||
useWebRTCIfAvailable="true"
|
||||
showPhoneOption="false"
|
||||
showWebRTCStats="false"
|
||||
echoTestApp="9196"
|
||||
dependsOn="UsersModule"
|
||||
/>
|
||||
|
@ -120,6 +120,10 @@
|
||||
|
||||
<script src="lib/bbb_api_bridge.js?v=VERSION" language="javascript"></script>
|
||||
<script src="lib/sip.js?v=VERSION" language="javascript"></script>
|
||||
|
||||
<script src="lib/adapter.js?v=VERSION" language="javascript"></script>
|
||||
<script src="lib/webrtc_stats_bridge.js?v=VERSION" language="javascript"></script>
|
||||
|
||||
<script src="lib/bbb_webrtc_bridge_sip.js?v=VERSION" language="javascript"></script>
|
||||
<script src="lib/weburl_regex.js?v=VERSION" language="javascript"></script>
|
||||
<script src="lib/jsnlog.min.js?v=VERSION" language="javascript"></script>
|
||||
|
3201
bigbluebutton-client/resources/prod/lib/adapter.js
Normal file
@ -504,7 +504,15 @@
|
||||
swfObj.webRTCMediaFail();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BBB.webRTCMonitorUpdate = function(result) {
|
||||
var swfObj = getSwfObj();
|
||||
if (swfObj) {
|
||||
swfObj.webRTCMonitorUpdate(result);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Third-party JS apps should use this to query if the BBB SWF file is ready to handle calls.
|
||||
BBB.isSwfClientReady = function() {
|
||||
return swfReady;
|
||||
@ -657,4 +665,3 @@
|
||||
|
||||
window.BBB = BBB;
|
||||
})(this);
|
||||
|
||||
|
@ -9,12 +9,15 @@ function webRTCCallback(message) {
|
||||
if (message.errorcode !== 1004) {
|
||||
message.cause = null;
|
||||
}
|
||||
monitorTracksStop();
|
||||
BBB.webRTCCallFailed(inEchoTest, message.errorcode, message.cause);
|
||||
break;
|
||||
case 'ended':
|
||||
monitorTracksStop();
|
||||
BBB.webRTCCallEnded(inEchoTest);
|
||||
break;
|
||||
case 'started':
|
||||
monitorTracksStart();
|
||||
BBB.webRTCCallStarted(inEchoTest);
|
||||
break;
|
||||
case 'connecting':
|
||||
|
297
bigbluebutton-client/resources/prod/lib/getStats.js
Normal file
@ -0,0 +1,297 @@
|
||||
// Last time updated at May 23, 2015, 08:32:23
|
||||
// Latest file can be found here: https://cdn.webrtc-experiment.com/getStats.js
|
||||
// Muaz Khan - www.MuazKhan.com
|
||||
// MIT License - www.WebRTC-Experiment.com/licence
|
||||
// Source Code - https://github.com/muaz-khan/getStats
|
||||
// ___________
|
||||
// getStats.js
|
||||
// an abstraction layer runs top over RTCPeerConnection.getStats API
|
||||
// cross-browser compatible solution
|
||||
// http://dev.w3.org/2011/webrtc/editor/webrtc.html#dom-peerconnection-getstats
|
||||
/*
|
||||
getStats(function(result) {
|
||||
result.connectionType.remote.ipAddress
|
||||
result.connectionType.remote.candidateType
|
||||
result.connectionType.transport
|
||||
});
|
||||
*/
|
||||
(function() {
|
||||
|
||||
var RTCPeerConnection;
|
||||
if (typeof webkitRTCPeerConnection !== 'undefined') {
|
||||
RTCPeerConnection = webkitRTCPeerConnection;
|
||||
}
|
||||
|
||||
if (isFirefox()) {
|
||||
RTCPeerConnection = mozRTCPeerConnection;
|
||||
}
|
||||
|
||||
if (typeof MediaStreamTrack === 'undefined') {
|
||||
MediaStreamTrack = {}; // todo?
|
||||
}
|
||||
|
||||
function isFirefox() {
|
||||
return typeof mozRTCPeerConnection !== 'undefined';
|
||||
}
|
||||
|
||||
function arrayAverage(array) {
|
||||
var sum = 0;
|
||||
for( var i = 0; i < array.length; i++ ){
|
||||
sum += array[i];
|
||||
}
|
||||
return sum/array.length;
|
||||
}
|
||||
|
||||
function getStats(mediaStreamTrack, callback, interval) {
|
||||
var peer = this;
|
||||
|
||||
if (arguments[0] instanceof RTCPeerConnection) {
|
||||
peer = arguments[0];
|
||||
mediaStreamTrack = arguments[1];
|
||||
callback = arguments[2];
|
||||
interval = arguments[3];
|
||||
|
||||
if (!(mediaStreamTrack instanceof MediaStreamTrack) && !!navigator.mozGetUserMedia) {
|
||||
throw '2nd argument is not instance of MediaStreamTrack.';
|
||||
}
|
||||
} else if (!(mediaStreamTrack instanceof MediaStreamTrack) && !!navigator.mozGetUserMedia) {
|
||||
throw '1st argument is not instance of MediaStreamTrack.';
|
||||
}
|
||||
|
||||
var globalObject = {
|
||||
audio: {},
|
||||
video: {}
|
||||
};
|
||||
|
||||
var nomore = false;
|
||||
|
||||
(function getPrivateStats() {
|
||||
var promise = _getStats(peer, mediaStreamTrack);
|
||||
promise.then(function(results) {
|
||||
var result = {
|
||||
audio: {},
|
||||
video: {},
|
||||
// TODO remove the raw results
|
||||
results: results,
|
||||
nomore: function() {
|
||||
nomore = true;
|
||||
}
|
||||
};
|
||||
|
||||
for (var key in results) {
|
||||
var res = results[key];
|
||||
|
||||
res.timestamp = typeof res.timestamp === 'object'? res.timestamp.getTime(): res.timestamp;
|
||||
res.packetsLost = typeof res.packetsLost === 'string'? parseInt(res.packetsLost): res.packetsLost;
|
||||
res.packetsReceived = typeof res.packetsReceived === 'string'? parseInt(res.packetsReceived): res.packetsReceived;
|
||||
res.bytesReceived = typeof res.bytesReceived === 'string'? parseInt(res.bytesReceived): res.bytesReceived;
|
||||
|
||||
if ((res.mediaType == 'audio' || (res.type == 'ssrc' && res.googCodecName == 'opus')) && typeof res.bytesSent !== 'undefined') {
|
||||
if (typeof globalObject.audio.prevSent === 'undefined') {
|
||||
globalObject.audio.prevSent = res;
|
||||
}
|
||||
|
||||
var bytes = res.bytesSent - globalObject.audio.prevSent.bytesSent;
|
||||
var diffTimestamp = res.timestamp - globalObject.audio.prevSent.timestamp;
|
||||
|
||||
var kilobytes = bytes / 1024;
|
||||
var kbitsSentPerSecond = (kilobytes * 8) / (diffTimestamp / 1000.0);
|
||||
|
||||
result.audio = merge(result.audio, {
|
||||
availableBandwidth: kilobytes,
|
||||
inputLevel: res.audioInputLevel,
|
||||
packetsSent: res.packetsSent,
|
||||
bytesSent: res.bytesSent,
|
||||
kbitsSentPerSecond: kbitsSentPerSecond
|
||||
});
|
||||
|
||||
globalObject.audio.prevSent = res;
|
||||
}
|
||||
if ((res.mediaType == 'audio' || (res.type == 'ssrc' && res.googCodecName == 'opus')) && typeof res.bytesReceived !== 'undefined') {
|
||||
if (typeof globalObject.audio.prevReceived === 'undefined') {
|
||||
globalObject.audio.prevReceived = res;
|
||||
globalObject.audio.consecutiveFlaws = 0;
|
||||
globalObject.audio.globalBitrateArray = [ ];
|
||||
}
|
||||
|
||||
var intervalPacketsLost = res.packetsLost - globalObject.audio.prevReceived.packetsLost;
|
||||
var intervalPacketsReceived = res.packetsReceived - globalObject.audio.prevReceived.packetsReceived;
|
||||
var intervalBytesReceived = res.bytesReceived - globalObject.audio.prevReceived.bytesReceived;
|
||||
var intervalLossRate = intervalPacketsLost + intervalPacketsReceived == 0? 1: intervalPacketsLost / (intervalPacketsLost + intervalPacketsReceived);
|
||||
var intervalBitrate = (intervalBytesReceived / interval) * 8;
|
||||
var globalBitrate = arrayAverage(globalObject.audio.globalBitrateArray);
|
||||
var intervalEstimatedLossRate;
|
||||
if (isFirefox()) {
|
||||
intervalEstimatedLossRate = Math.max(0, globalBitrate - intervalBitrate) / globalBitrate;
|
||||
} else {
|
||||
intervalEstimatedLossRate = intervalLossRate;
|
||||
}
|
||||
|
||||
var flaws = intervalPacketsLost;
|
||||
if (flaws > 0) {
|
||||
if (globalObject.audio.consecutiveFlaws > 0) {
|
||||
flaws *= 2;
|
||||
}
|
||||
++globalObject.audio.consecutiveFlaws;
|
||||
} else {
|
||||
globalObject.audio.consecutiveFlaws = 0;
|
||||
}
|
||||
var packetsLost = res.packetsLost + flaws;
|
||||
var r = (Math.max(0, res.packetsReceived - packetsLost) / res.packetsReceived) * 100;
|
||||
if (r > 100) {
|
||||
r = 100;
|
||||
}
|
||||
// https://freeswitch.org/stash/projects/FS/repos/freeswitch/browse/src/switch_rtp.c?at=refs%2Fheads%2Fv1.4#1671
|
||||
var mos = 1 + (0.035) * r + (0.000007) * r * (r-60) * (100-r);
|
||||
|
||||
var bytes = res.bytesReceived - globalObject.audio.prevReceived.bytesReceived;
|
||||
var diffTimestamp = res.timestamp - globalObject.audio.prevReceived.timestamp;
|
||||
var packetDuration = diffTimestamp / (res.packetsReceived - globalObject.audio.prevReceived.packetsReceived);
|
||||
var kilobytes = bytes / 1024;
|
||||
var kbitsReceivedPerSecond = (kilobytes * 8) / (diffTimestamp / 1000.0);
|
||||
|
||||
result.audio = merge(result.audio, {
|
||||
availableBandwidth: kilobytes,
|
||||
outputLevel: res.audioOutputLevel,
|
||||
packetsLost: res.packetsLost,
|
||||
jitter: typeof res.googJitterReceived !== 'undefined'? res.googJitterReceived: res.jitter,
|
||||
jitterBuffer: res.googJitterBufferMs,
|
||||
delay: res.googCurrentDelayMs,
|
||||
packetsReceived: res.packetsReceived,
|
||||
bytesReceived: res.bytesReceived,
|
||||
kbitsReceivedPerSecond: kbitsReceivedPerSecond,
|
||||
packetDuration: packetDuration,
|
||||
r: r,
|
||||
mos: mos,
|
||||
intervalLossRate: intervalLossRate,
|
||||
intervalEstimatedLossRate: intervalEstimatedLossRate,
|
||||
globalBitrate: globalBitrate
|
||||
});
|
||||
|
||||
globalObject.audio.prevReceived = res;
|
||||
globalObject.audio.globalBitrateArray.push(intervalBitrate);
|
||||
// 12 is the number of seconds we use to calculate the global bitrate
|
||||
if (globalObject.audio.globalBitrateArray.length > 12 / (interval / 1000)) {
|
||||
globalObject.audio.globalBitrateArray.shift();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO make it work on Firefox
|
||||
if (res.googCodecName == 'VP8') {
|
||||
if (!globalObject.video.prevBytesSent)
|
||||
globalObject.video.prevBytesSent = res.bytesSent;
|
||||
|
||||
var bytes = res.bytesSent - globalObject.video.prevBytesSent;
|
||||
globalObject.video.prevBytesSent = res.bytesSent;
|
||||
|
||||
var kilobytes = bytes / 1024;
|
||||
|
||||
result.video = merge(result.video, {
|
||||
availableBandwidth: kilobytes.toFixed(1),
|
||||
googFrameHeightInput: res.googFrameHeightInput,
|
||||
googFrameWidthInput: res.googFrameWidthInput,
|
||||
googCaptureQueueDelayMsPerS: res.googCaptureQueueDelayMsPerS,
|
||||
rtt: res.googRtt,
|
||||
packetsLost: res.packetsLost,
|
||||
packetsSent: res.packetsSent,
|
||||
googEncodeUsagePercent: res.googEncodeUsagePercent,
|
||||
googCpuLimitedResolution: res.googCpuLimitedResolution,
|
||||
googNacksReceived: res.googNacksReceived,
|
||||
googFrameRateInput: res.googFrameRateInput,
|
||||
googPlisReceived: res.googPlisReceived,
|
||||
googViewLimitedResolution: res.googViewLimitedResolution,
|
||||
googCaptureJitterMs: res.googCaptureJitterMs,
|
||||
googAvgEncodeMs: res.googAvgEncodeMs,
|
||||
googFrameHeightSent: res.googFrameHeightSent,
|
||||
googFrameRateSent: res.googFrameRateSent,
|
||||
googBandwidthLimitedResolution: res.googBandwidthLimitedResolution,
|
||||
googFrameWidthSent: res.googFrameWidthSent,
|
||||
googFirsReceived: res.googFirsReceived,
|
||||
bytesSent: res.bytesSent
|
||||
});
|
||||
}
|
||||
|
||||
if (res.type == 'VideoBwe') {
|
||||
result.video.bandwidth = {
|
||||
googActualEncBitrate: res.googActualEncBitrate,
|
||||
googAvailableSendBandwidth: res.googAvailableSendBandwidth,
|
||||
googAvailableReceiveBandwidth: res.googAvailableReceiveBandwidth,
|
||||
googRetransmitBitrate: res.googRetransmitBitrate,
|
||||
googTargetEncBitrate: res.googTargetEncBitrate,
|
||||
googBucketDelay: res.googBucketDelay,
|
||||
googTransmitBitrate: res.googTransmitBitrate
|
||||
};
|
||||
}
|
||||
|
||||
// res.googActiveConnection means either STUN or TURN is used.
|
||||
|
||||
if (res.type == 'googCandidatePair' && res.googActiveConnection == 'true') {
|
||||
result.connectionType = {
|
||||
local: {
|
||||
candidateType: res.googLocalCandidateType,
|
||||
ipAddress: res.googLocalAddress
|
||||
},
|
||||
remote: {
|
||||
candidateType: res.googRemoteCandidateType,
|
||||
ipAddress: res.googRemoteAddress
|
||||
},
|
||||
transport: res.googTransportType
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
callback(result);
|
||||
|
||||
// second argument checks to see, if target-user is still connected.
|
||||
if (!nomore) {
|
||||
typeof interval != undefined && interval && setTimeout(getPrivateStats, interval || 1000);
|
||||
}
|
||||
}, function(exception) {
|
||||
console.log("Promise rejected: " + exception.message);
|
||||
callback(null);
|
||||
});
|
||||
})();
|
||||
|
||||
// taken from http://blog.telenor.io/webrtc/2015/06/11/getstats-chrome-vs-firefox.html
|
||||
function _getStats(pc, selector) {
|
||||
if (navigator.mozGetUserMedia) {
|
||||
return pc.getStats(selector);
|
||||
}
|
||||
return new Promise(function(resolve, reject) {
|
||||
pc.getStats(function(response) {
|
||||
var standardReport = {};
|
||||
response.result().forEach(function(report) {
|
||||
var standardStats = {
|
||||
id: report.id,
|
||||
timestamp: report.timestamp,
|
||||
type: report.type
|
||||
};
|
||||
report.names().forEach(function(name) {
|
||||
standardStats[name] = report.stat(name);
|
||||
});
|
||||
standardReport[standardStats.id] = standardStats;
|
||||
});
|
||||
resolve(standardReport);
|
||||
}, selector, reject);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function merge(mergein, mergeto) {
|
||||
if (!mergein) mergein = {};
|
||||
if (!mergeto) return mergein;
|
||||
|
||||
for (var item in mergeto) {
|
||||
mergein[item] = mergeto[item];
|
||||
}
|
||||
return mergein;
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined'/* && !!module.exports*/) {
|
||||
module.exports = getStats;
|
||||
}
|
||||
|
||||
if(typeof window !== 'undefined') {
|
||||
window.getStats = getStats;
|
||||
}
|
||||
})();
|
305
bigbluebutton-client/resources/prod/lib/webrtc_stats_bridge.js
Normal file
@ -0,0 +1,305 @@
|
||||
var monitoredTracks = {};
|
||||
|
||||
function isFirefox() {
|
||||
return typeof mozRTCPeerConnection !== 'undefined';
|
||||
}
|
||||
|
||||
function arrayAverage(array) {
|
||||
var sum = 0;
|
||||
for( var i = 0; i < array.length; i++ ){
|
||||
sum += array[i];
|
||||
}
|
||||
return sum/array.length;
|
||||
}
|
||||
|
||||
function customGetStats(peer, mediaStreamTrack, callback, interval) {
|
||||
var globalObject = {
|
||||
audio: {}
|
||||
//audio: {},
|
||||
//video: {}
|
||||
};
|
||||
|
||||
var nomore = false;
|
||||
|
||||
(function getPrivateStats() {
|
||||
var promise = _getStats(peer, mediaStreamTrack);
|
||||
promise.then(function(results) {
|
||||
var result = {
|
||||
audio: {},
|
||||
//video: {},
|
||||
// TODO remove the raw results
|
||||
results: results,
|
||||
nomore: function() {
|
||||
nomore = true;
|
||||
}
|
||||
};
|
||||
|
||||
for (var key in results) {
|
||||
var res = results[key];
|
||||
|
||||
res.timestamp = typeof res.timestamp === 'object'? res.timestamp.getTime(): res.timestamp;
|
||||
res.packetsLost = typeof res.packetsLost === 'string'? parseInt(res.packetsLost): res.packetsLost;
|
||||
res.packetsReceived = typeof res.packetsReceived === 'string'? parseInt(res.packetsReceived): res.packetsReceived;
|
||||
res.bytesReceived = typeof res.bytesReceived === 'string'? parseInt(res.bytesReceived): res.bytesReceived;
|
||||
|
||||
if ((res.mediaType == 'audio' || (res.type == 'ssrc' && res.googCodecName == 'opus')) && typeof res.bytesSent !== 'undefined') {
|
||||
if (typeof globalObject.audio.prevSent === 'undefined') {
|
||||
globalObject.audio.prevSent = res;
|
||||
}
|
||||
|
||||
var bytes = res.bytesSent - globalObject.audio.prevSent.bytesSent;
|
||||
var diffTimestamp = res.timestamp - globalObject.audio.prevSent.timestamp;
|
||||
|
||||
var kilobytes = bytes / 1024;
|
||||
var kbitsSentPerSecond = (kilobytes * 8) / (diffTimestamp / 1000.0);
|
||||
|
||||
result.audio = merge(result.audio, {
|
||||
availableBandwidth: kilobytes,
|
||||
inputLevel: res.audioInputLevel,
|
||||
packetsSent: res.packetsSent,
|
||||
bytesSent: res.bytesSent,
|
||||
kbitsSentPerSecond: kbitsSentPerSecond
|
||||
});
|
||||
|
||||
globalObject.audio.prevSent = res;
|
||||
}
|
||||
if ((res.mediaType == 'audio' || (res.type == 'ssrc' && res.googCodecName == 'opus')) && typeof res.bytesReceived !== 'undefined') {
|
||||
if (typeof globalObject.audio.prevReceived === 'undefined') {
|
||||
globalObject.audio.prevReceived = res;
|
||||
globalObject.audio.consecutiveFlaws = 0;
|
||||
globalObject.audio.globalBitrateArray = [ ];
|
||||
}
|
||||
|
||||
var intervalPacketsLost = res.packetsLost - globalObject.audio.prevReceived.packetsLost;
|
||||
var intervalPacketsReceived = res.packetsReceived - globalObject.audio.prevReceived.packetsReceived;
|
||||
var intervalBytesReceived = res.bytesReceived - globalObject.audio.prevReceived.bytesReceived;
|
||||
var intervalLossRate = intervalPacketsLost + intervalPacketsReceived == 0? 1: intervalPacketsLost / (intervalPacketsLost + intervalPacketsReceived);
|
||||
var intervalBitrate = (intervalBytesReceived / interval) * 8;
|
||||
var globalBitrate = arrayAverage(globalObject.audio.globalBitrateArray);
|
||||
var intervalEstimatedLossRate;
|
||||
if (isFirefox()) {
|
||||
intervalEstimatedLossRate = Math.max(0, globalBitrate - intervalBitrate) / globalBitrate;
|
||||
} else {
|
||||
intervalEstimatedLossRate = intervalLossRate;
|
||||
}
|
||||
|
||||
var flaws = intervalPacketsLost;
|
||||
if (flaws > 0) {
|
||||
if (globalObject.audio.consecutiveFlaws > 0) {
|
||||
flaws *= 2;
|
||||
}
|
||||
++globalObject.audio.consecutiveFlaws;
|
||||
} else {
|
||||
globalObject.audio.consecutiveFlaws = 0;
|
||||
}
|
||||
var packetsLost = res.packetsLost + flaws;
|
||||
var r = (Math.max(0, res.packetsReceived - packetsLost) / res.packetsReceived) * 100;
|
||||
if (r > 100) {
|
||||
r = 100;
|
||||
}
|
||||
// https://freeswitch.org/stash/projects/FS/repos/freeswitch/browse/src/switch_rtp.c?at=refs%2Fheads%2Fv1.4#1671
|
||||
var mos = 1 + (0.035) * r + (0.000007) * r * (r-60) * (100-r);
|
||||
|
||||
var bytes = res.bytesReceived - globalObject.audio.prevReceived.bytesReceived;
|
||||
var diffTimestamp = res.timestamp - globalObject.audio.prevReceived.timestamp;
|
||||
var packetDuration = diffTimestamp / (res.packetsReceived - globalObject.audio.prevReceived.packetsReceived);
|
||||
var kilobytes = bytes / 1024;
|
||||
var kbitsReceivedPerSecond = (kilobytes * 8) / (diffTimestamp / 1000.0);
|
||||
|
||||
result.audio = merge(result.audio, {
|
||||
availableBandwidth: kilobytes,
|
||||
outputLevel: res.audioOutputLevel,
|
||||
packetsLost: res.packetsLost,
|
||||
jitter: typeof res.googJitterReceived !== 'undefined'? res.googJitterReceived: res.jitter,
|
||||
jitterBuffer: res.googJitterBufferMs,
|
||||
delay: res.googCurrentDelayMs,
|
||||
packetsReceived: res.packetsReceived,
|
||||
bytesReceived: res.bytesReceived,
|
||||
kbitsReceivedPerSecond: kbitsReceivedPerSecond,
|
||||
packetDuration: packetDuration,
|
||||
r: r,
|
||||
mos: mos,
|
||||
intervalLossRate: intervalLossRate,
|
||||
intervalEstimatedLossRate: intervalEstimatedLossRate,
|
||||
globalBitrate: globalBitrate
|
||||
});
|
||||
|
||||
globalObject.audio.prevReceived = res;
|
||||
globalObject.audio.globalBitrateArray.push(intervalBitrate);
|
||||
// 12 is the number of seconds we use to calculate the global bitrate
|
||||
if (globalObject.audio.globalBitrateArray.length > 12 / (interval / 1000)) {
|
||||
globalObject.audio.globalBitrateArray.shift();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// TODO make it work on Firefox
|
||||
if (res.googCodecName == 'VP8') {
|
||||
if (!globalObject.video.prevBytesSent)
|
||||
globalObject.video.prevBytesSent = res.bytesSent;
|
||||
|
||||
var bytes = res.bytesSent - globalObject.video.prevBytesSent;
|
||||
globalObject.video.prevBytesSent = res.bytesSent;
|
||||
|
||||
var kilobytes = bytes / 1024;
|
||||
|
||||
result.video = merge(result.video, {
|
||||
availableBandwidth: kilobytes.toFixed(1),
|
||||
googFrameHeightInput: res.googFrameHeightInput,
|
||||
googFrameWidthInput: res.googFrameWidthInput,
|
||||
googCaptureQueueDelayMsPerS: res.googCaptureQueueDelayMsPerS,
|
||||
rtt: res.googRtt,
|
||||
packetsLost: res.packetsLost,
|
||||
packetsSent: res.packetsSent,
|
||||
googEncodeUsagePercent: res.googEncodeUsagePercent,
|
||||
googCpuLimitedResolution: res.googCpuLimitedResolution,
|
||||
googNacksReceived: res.googNacksReceived,
|
||||
googFrameRateInput: res.googFrameRateInput,
|
||||
googPlisReceived: res.googPlisReceived,
|
||||
googViewLimitedResolution: res.googViewLimitedResolution,
|
||||
googCaptureJitterMs: res.googCaptureJitterMs,
|
||||
googAvgEncodeMs: res.googAvgEncodeMs,
|
||||
googFrameHeightSent: res.googFrameHeightSent,
|
||||
googFrameRateSent: res.googFrameRateSent,
|
||||
googBandwidthLimitedResolution: res.googBandwidthLimitedResolution,
|
||||
googFrameWidthSent: res.googFrameWidthSent,
|
||||
googFirsReceived: res.googFirsReceived,
|
||||
bytesSent: res.bytesSent
|
||||
});
|
||||
}
|
||||
|
||||
if (res.type == 'VideoBwe') {
|
||||
result.video.bandwidth = {
|
||||
googActualEncBitrate: res.googActualEncBitrate,
|
||||
googAvailableSendBandwidth: res.googAvailableSendBandwidth,
|
||||
googAvailableReceiveBandwidth: res.googAvailableReceiveBandwidth,
|
||||
googRetransmitBitrate: res.googRetransmitBitrate,
|
||||
googTargetEncBitrate: res.googTargetEncBitrate,
|
||||
googBucketDelay: res.googBucketDelay,
|
||||
googTransmitBitrate: res.googTransmitBitrate
|
||||
};
|
||||
}
|
||||
*/
|
||||
|
||||
// res.googActiveConnection means either STUN or TURN is used.
|
||||
|
||||
if (res.type == 'googCandidatePair' && res.googActiveConnection == 'true') {
|
||||
result.connectionType = {
|
||||
local: {
|
||||
candidateType: res.googLocalCandidateType,
|
||||
ipAddress: res.googLocalAddress
|
||||
},
|
||||
remote: {
|
||||
candidateType: res.googRemoteCandidateType,
|
||||
ipAddress: res.googRemoteAddress
|
||||
},
|
||||
transport: res.googTransportType
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
callback(result);
|
||||
|
||||
// second argument checks to see, if target-user is still connected.
|
||||
if (!nomore) {
|
||||
typeof interval != undefined && interval && setTimeout(getPrivateStats, interval || 1000);
|
||||
}
|
||||
}, function(exception) {
|
||||
console.log("Promise rejected: " + exception.message);
|
||||
callback(null);
|
||||
});
|
||||
})();
|
||||
|
||||
// taken from http://blog.telenor.io/webrtc/2015/06/11/getstats-chrome-vs-firefox.html
|
||||
function _getStats(pc, selector) {
|
||||
if (navigator.mozGetUserMedia) {
|
||||
return pc.getStats(selector);
|
||||
}
|
||||
return new Promise(function(resolve, reject) {
|
||||
pc.getStats(function(response) {
|
||||
var standardReport = {};
|
||||
response.result().forEach(function(report) {
|
||||
var standardStats = {
|
||||
id: report.id,
|
||||
timestamp: report.timestamp,
|
||||
type: report.type
|
||||
};
|
||||
report.names().forEach(function(name) {
|
||||
standardStats[name] = report.stat(name);
|
||||
});
|
||||
standardReport[standardStats.id] = standardStats;
|
||||
});
|
||||
resolve(standardReport);
|
||||
}, selector, reject);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function merge(mergein, mergeto) {
|
||||
if (!mergein) mergein = {};
|
||||
if (!mergeto) return mergein;
|
||||
|
||||
for (var item in mergeto) {
|
||||
mergein[item] = mergeto[item];
|
||||
}
|
||||
return mergein;
|
||||
}
|
||||
|
||||
function monitorTrackStart(peer, track, local) {
|
||||
if (! monitoredTracks.hasOwnProperty(track.id)) {
|
||||
monitoredTracks[track.id] = function() { console.log("Still didn't have any report for this track"); };
|
||||
customGetStats(
|
||||
peer,
|
||||
track,
|
||||
function(results) {
|
||||
if (results == null) {
|
||||
monitorTrackStop(track.id);
|
||||
} else {
|
||||
monitoredTracks[track.id] = results.nomore;
|
||||
results.audio.type = local? "local": "remote",
|
||||
delete results.results;
|
||||
BBB.webRTCMonitorUpdate(JSON.stringify(results));
|
||||
console.log(JSON.stringify(results));
|
||||
}
|
||||
},
|
||||
2000
|
||||
);
|
||||
} else {
|
||||
console.log("Already monitoring this track");
|
||||
}
|
||||
}
|
||||
|
||||
function monitorTrackStop(trackId) {
|
||||
monitoredTracks[trackId]();
|
||||
delete monitoredTracks[trackId];
|
||||
console.log("Track removed, monitoredTracks.length = " + Object.keys(monitoredTracks).length);
|
||||
}
|
||||
|
||||
function monitorTracksStart() {
|
||||
setTimeout( function() {
|
||||
if (currentSession == null) {
|
||||
console.log("Doing nothing because currentSession is null");
|
||||
return;
|
||||
}
|
||||
var peer = currentSession.mediaHandler.peerConnection;
|
||||
|
||||
for (var streamId = 0; streamId < peer.getLocalStreams().length; ++streamId) {
|
||||
for (var trackId = 0; trackId < peer.getLocalStreams()[streamId].getAudioTracks().length; ++trackId) {
|
||||
var track = peer.getLocalStreams()[streamId].getAudioTracks()[trackId];
|
||||
monitorTrackStart(peer, track, true);
|
||||
}
|
||||
}
|
||||
for (var streamId = 0; streamId < peer.getRemoteStreams().length; ++streamId) {
|
||||
for (var trackId = 0; trackId < peer.getRemoteStreams()[streamId].getAudioTracks().length; ++trackId) {
|
||||
var track = peer.getRemoteStreams()[streamId].getAudioTracks()[trackId];
|
||||
monitorTrackStart(peer, track, false);
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function monitorTracksStop() {
|
||||
for (var id in monitoredTracks) {
|
||||
monitorTrackStop(id);
|
||||
}
|
||||
}
|
@ -105,7 +105,7 @@ package org.bigbluebutton.main.api
|
||||
ExternalInterface.addCallback("webRTCMediaSuccess", handleWebRTCMediaSuccess);
|
||||
ExternalInterface.addCallback("webRTCMediaFail", handleWebRTCMediaFail);
|
||||
ExternalInterface.addCallback("getSessionToken", handleGetSessionToken);
|
||||
|
||||
ExternalInterface.addCallback("webRTCMonitorUpdate", handleWebRTCMonitorUpdate);
|
||||
}
|
||||
|
||||
// Tell out JS counterpart that we are ready.
|
||||
@ -444,5 +444,11 @@ package org.bigbluebutton.main.api
|
||||
private function handleWebRTCMediaFail():void {
|
||||
_dispatcher.dispatchEvent(new WebRTCMediaEvent(WebRTCMediaEvent.WEBRTC_MEDIA_FAIL));
|
||||
}
|
||||
|
||||
private function handleWebRTCMonitorUpdate(results:String):void {
|
||||
var e:BBBEvent = new BBBEvent(BBBEvent.WEBRTC_MONITOR_UPDATE_EVENT);
|
||||
e.payload.results = results;
|
||||
_dispatcher.dispatchEvent(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -54,6 +54,7 @@ package org.bigbluebutton.main.events {
|
||||
public static const RECONNECT_DESKSHARE_SUCCEEDED_EVENT:String = "RECONNECT_DESKSHARE_SUCCEEDED_EVENT";
|
||||
|
||||
public static const CANCEL_RECONNECTION_EVENT:String = "CANCEL_RECONNECTION_EVENT";
|
||||
public static const WEBRTC_MONITOR_UPDATE_EVENT:String = "WEBRTC_MONITOR_UPDATE_EVENT";
|
||||
|
||||
public var message:String;
|
||||
public var payload:Object = new Object();
|
||||
|
@ -76,6 +76,11 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
vboxListen.percentWidth = 50;
|
||||
}
|
||||
|
||||
if (!JSAPI.getInstance().isWebRTCAvailable()) {
|
||||
audioBrowserHint.visible = audioBrowserHint.includeInLayout = true;
|
||||
this.height += 72;
|
||||
}
|
||||
|
||||
// If Puffin browser is deteted and version is less than 4.6
|
||||
if (browserInfo[0] == "Puffin" && String(browserInfo[2]).substr(0, 3) < "4.6") {
|
||||
vruleListen.visible = vruleListen.includeInLayout = vboxMic.visible = vboxMic.includeInLayout = false;
|
||||
@ -139,6 +144,13 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
styleName="micSettingsWindowTitleStyle"
|
||||
width="100%" height="100%" />
|
||||
</mx:HBox>
|
||||
<mx:Box width="100%" height="56"
|
||||
verticalAlign="middle" horizontalAlign="center"
|
||||
verticalScrollPolicy="off" horizontalScrollPolicy="off"
|
||||
visible="false" includeInLayout="false"
|
||||
id="audioBrowserHint" styleName="audioBroswerHintBoxStyle">
|
||||
<mx:Text width="100%" textAlign="center" text="{ResourceUtil.getInstance().getString('bbb.clientstatus.webrtc.message')}" styleName="audioBroswerHintTextStyle"/>
|
||||
</mx:Box>
|
||||
<mx:HRule width="100%" />
|
||||
<mx:HBox width="100%" height="100%">
|
||||
<mx:VBox id="vboxMic" height="100%" width="30%" horizontalAlign="center" verticalAlign="middle">
|
||||
|
@ -380,7 +380,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
<common:TabIndexer id="quickLinksIndexer" startIndex="102" tabIndices="{[usersLinkBtn, webcamLinkButton, presentationLinkBtn, chatLinkBtn, captionLinkBtn]}"/>
|
||||
<common:TabIndexer id="buttonsIndexer" startIndex="{quickLinksIndexer.startIndex + numButtons + 10}"
|
||||
tabIndices="{[recordBtn, muteMeBtn, shortcutKeysBtn, helpBtn, btnLogout]}"/>
|
||||
tabIndices="{[recordBtn, muteMeBtn, webRTCAudioStatus, shortcutKeysBtn, helpBtn, btnLogout]}"/>
|
||||
|
||||
|
||||
<mx:HBox id="quickLinks" width="1" includeInLayout="false">
|
||||
@ -404,6 +404,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
<views:RecordButton id="recordBtn"/>
|
||||
<mx:VRule strokeWidth="2" height="100%" visible="{muteMeBtn.visible}" includeInLayout="{muteMeBtn.includeInLayout}"/>
|
||||
<views:MuteMeButton id="muteMeBtn" height="20"/>
|
||||
<views:WebRTCAudioStatus id="webRTCAudioStatus" height="20"/>
|
||||
<mx:Label id="meetingNameLbl" width="100%" minWidth="1" styleName="meetingNameLabelStyle" />
|
||||
|
||||
<!--
|
||||
|
@ -0,0 +1,111 @@
|
||||
<?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/>.
|
||||
|
||||
-->
|
||||
<mx:Canvas
|
||||
xmlns:mx="http://www.adobe.com/2006/mxml"
|
||||
xmlns:flc="flexlib.controls.*"
|
||||
xmlns:mate="http://mate.asfusion.com/"
|
||||
visible="false"
|
||||
includeInLayout="false"
|
||||
creationComplete="onCreationComplete()"
|
||||
styleName="webRTCAudioStatusStyle">
|
||||
|
||||
<mate:Listener type="{BBBEvent.WEBRTC_MONITOR_UPDATE_EVENT}" method="handleWebRTCMonitor" />
|
||||
<mate:Listener type="{WebRTCCallEvent.WEBRTC_CALL_STARTED}" method="handleWebRTCCallStartedEvent" />
|
||||
<mate:Listener type="{WebRTCCallEvent.WEBRTC_CALL_ENDED}" method="handleWebRTCCallEndedEvent" />
|
||||
|
||||
<mx:Script>
|
||||
<![CDATA[
|
||||
import org.as3commons.logging.api.ILogger;
|
||||
import org.as3commons.logging.api.getClassLogger;
|
||||
|
||||
import mx.collections.ArrayCollection;
|
||||
|
||||
import org.bigbluebutton.main.events.BBBEvent;
|
||||
import org.bigbluebutton.modules.phone.PhoneOptions;
|
||||
import org.bigbluebutton.modules.phone.events.WebRTCCallEvent;
|
||||
import org.bigbluebutton.util.i18n.ResourceUtil;
|
||||
|
||||
private static const LOGGER:ILogger = getClassLogger(WebRTCAudioStatus);
|
||||
|
||||
private var webRTCAudioStatusHistory:ArrayCollection = new ArrayCollection([0, 0]);
|
||||
|
||||
private function onCreationComplete():void {
|
||||
}
|
||||
|
||||
private function handleWebRTCCallStartedEvent(event:WebRTCCallEvent):void {
|
||||
var phoneOptions:PhoneOptions = new PhoneOptions();
|
||||
if (phoneOptions.showWebRTCStats) {
|
||||
this.visible = this.includeInLayout = true;
|
||||
}
|
||||
}
|
||||
|
||||
private function handleWebRTCCallEndedEvent(event:WebRTCCallEvent):void {
|
||||
var phoneOptions:PhoneOptions = new PhoneOptions();
|
||||
if (phoneOptions.showWebRTCStats) {
|
||||
this.visible = this.includeInLayout = false;
|
||||
}
|
||||
}
|
||||
|
||||
private function getRateFromHistory(rate: Number):Number {
|
||||
webRTCAudioStatusHistory.removeItemAt(webRTCAudioStatusHistory.length - 1);
|
||||
webRTCAudioStatusHistory.addItemAt(rate, 0);
|
||||
return Math.max.apply(null, webRTCAudioStatusHistory.toArray());
|
||||
}
|
||||
|
||||
private function audioMonitor(audio: Object):void {
|
||||
if (audio.hasOwnProperty("intervalEstimatedLossRate")) {
|
||||
var rate:Number = getRateFromHistory(audio.intervalEstimatedLossRate);
|
||||
switch (true) {
|
||||
case rate > 0.50:
|
||||
audioStatus.source = getStyle("weakAudioStatus");
|
||||
toolTip = ResourceUtil.getInstance().getString('bbb.clientstatus.webrtc.weakStatus');
|
||||
LOGGER.debug("Audio interval estimated loss rate: {0}", [rate]);
|
||||
break;
|
||||
case rate > 0.25:
|
||||
audioStatus.source = getStyle("almostWeakAudioStatus");
|
||||
toolTip = ResourceUtil.getInstance().getString('bbb.clientstatus.webrtc.almostWeakStatus');
|
||||
LOGGER.debug("Audio interval estimated loss rate: {0}", [rate]);
|
||||
break;
|
||||
case rate > 0.1:
|
||||
audioStatus.source = getStyle("almostStrongAudioStatus");
|
||||
toolTip = ResourceUtil.getInstance().getString('bbb.clientstatus.webrtc.almostStrongStatus');
|
||||
break;
|
||||
default:
|
||||
audioStatus.source = getStyle("strongAudioStatus");
|
||||
toolTip = ResourceUtil.getInstance().getString('bbb.clientstatus.webrtc.strongStatus');
|
||||
break;
|
||||
}
|
||||
validateNow();
|
||||
}
|
||||
}
|
||||
|
||||
public function handleWebRTCMonitor(e:BBBEvent):void {
|
||||
var results:String = e.payload.results;
|
||||
var object:Object = JSON.parse(results);
|
||||
if (object.hasOwnProperty("audio")) {
|
||||
audioMonitor(object.audio);
|
||||
}
|
||||
}
|
||||
|
||||
]]>
|
||||
</mx:Script>
|
||||
<mx:Image id="audioStatus"/>
|
||||
</mx:Canvas>
|
@ -49,6 +49,9 @@ package org.bigbluebutton.modules.phone
|
||||
|
||||
[Bindable]
|
||||
public var showPhoneOption:Boolean = false;
|
||||
|
||||
[Bindable]
|
||||
public var showWebRTCStats:Boolean = false;
|
||||
|
||||
public function PhoneOptions() {
|
||||
parseOptions();
|
||||
@ -87,6 +90,9 @@ package org.bigbluebutton.modules.phone
|
||||
if (vxml.@showPhoneOption != undefined) {
|
||||
showPhoneOption = (vxml.@showPhoneOption.toString().toUpperCase() == "TRUE");
|
||||
}
|
||||
if (vxml.@showWebRTCStats != undefined) {
|
||||
showWebRTCStats = (vxml.@showWebRTCStats.toString().toUpperCase() == "TRUE");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -70,10 +70,14 @@ package org.bigbluebutton.modules.whiteboard
|
||||
* Check if the presenter is starting a new text annotation without committing the last one.
|
||||
* If so, publish the last text annotation.
|
||||
*/
|
||||
if (currentlySelectedTextObject != null && currentlySelectedTextObject.status != TextObject.TEXT_PUBLISHED) {
|
||||
if (needToPublish()) {
|
||||
sendTextToServer(TextObject.TEXT_PUBLISHED, currentlySelectedTextObject);
|
||||
}
|
||||
}
|
||||
|
||||
public function needToPublish() : Boolean {
|
||||
return currentlySelectedTextObject != null && currentlySelectedTextObject.status != TextObject.TEXT_PUBLISHED;
|
||||
}
|
||||
|
||||
public function drawGraphic(event:WhiteboardUpdate):void{
|
||||
var o:Annotation = event.annotation;
|
||||
|
@ -21,7 +21,7 @@ package org.bigbluebutton.modules.whiteboard.models
|
||||
import flash.events.IEventDispatcher;
|
||||
|
||||
import mx.collections.ArrayCollection;
|
||||
import org.bigbluebutton.core.UsersUtil;
|
||||
|
||||
import org.as3commons.logging.api.ILogger;
|
||||
import org.as3commons.logging.api.getClassLogger;
|
||||
import org.bigbluebutton.modules.present.model.Page;
|
||||
|
@ -109,7 +109,7 @@ package org.bigbluebutton.modules.whiteboard.views {
|
||||
var tbWidth:Number = Math.abs(_mouseXMove - _mouseXDown);
|
||||
var tbHeight:Number = Math.abs(_mouseYMove - _mouseYDown);
|
||||
|
||||
if (tbHeight == 0 && tbWidth == 0) {
|
||||
if (tbHeight == 0 && tbWidth == 0 && !_wbCanvas.finishedTextEdit) {
|
||||
tbWidth = _singleClickWidth;
|
||||
tbHeight = _singleClickHeight;
|
||||
if (_mouseXDown + _singleClickWidth > _wbCanvas.width || _mouseYDown + _singleClickHeight > _wbCanvas.height) {
|
||||
|
@ -43,12 +43,14 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
public var model:WhiteboardCanvasModel;
|
||||
public var displayModel:WhiteboardCanvasDisplayModel;
|
||||
|
||||
public var finishedTextEdit : Boolean;
|
||||
|
||||
public var textToolbar:WhiteboardTextToolbar;
|
||||
private var bbbCanvas:IBbbCanvas;
|
||||
private var _xPosition:int;
|
||||
private var _yPosition:int;
|
||||
private var images:Images = new Images();
|
||||
|
||||
[Bindable] private var select_icon:Class = images.select_icon;
|
||||
[Bindable] private var pencil_icon:Class = images.pencil_icon;
|
||||
[Bindable] private var rectangle_icon:Class = images.square_icon;
|
||||
@ -100,12 +102,13 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
}
|
||||
|
||||
private function doMouseDown(event:Event):void {
|
||||
displayModel.doMouseDown(this.mouseX, this.mouseY);
|
||||
finishedTextEdit = displayModel.needToPublish();
|
||||
displayModel.doMouseDown(this.mouseX, this.mouseY);
|
||||
model.doMouseDown(this.mouseX, this.mouseY);
|
||||
event.stopPropagation(); // we want to stop the bubbling so slide doesn't move
|
||||
|
||||
stage.addEventListener(MouseEvent.MOUSE_UP, doMouseUp);
|
||||
stage.addEventListener(MouseEvent.MOUSE_MOVE, doMouseMove);
|
||||
stage.addEventListener(MouseEvent.MOUSE_MOVE, doMouseMove);
|
||||
}
|
||||
|
||||
private function doMouseMove(event:Event):void {
|
||||
|
@ -55,6 +55,7 @@
|
||||
# 2016-05-28 FFD Initial updates for 1.1-dev
|
||||
# 2016-08-15 GTR Archive more logs with zip option and show more applications with status
|
||||
# 2016-10-17 GTR Added redis to checked server components & added ownership check for video and freeswitch recording directories
|
||||
# 2017-04-08 FFD Cleanup for 1.1-beta
|
||||
|
||||
#set -x
|
||||
#set -e
|
||||
@ -118,7 +119,7 @@ if [ ! -f /var/www/bigbluebutton/client/conf/config.xml ]; then
|
||||
echo "# BigBlueButton does not appear to be installed. Could not"
|
||||
echo "# locate:"
|
||||
echo "#"
|
||||
echo "# /usr/share/red5/webapps/bigbluebutton/WEB-INF/red5-web.xml"
|
||||
echo "# /var/www/bigbluebutton/client/conf/config.xml"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@ -131,7 +132,7 @@ else
|
||||
fi
|
||||
|
||||
#
|
||||
# We're going to give ^bigbluebutton.web.logoutURL a default value so bbb-conf does not give a warning
|
||||
# We're going to give ^bigbluebutton.web.logoutURL a default value (if undefined) so bbb-conf does not give a warning
|
||||
#
|
||||
if [ -f $SERVLET_DIR/bigbluebutton/WEB-INF/classes/bigbluebutton.properties ]; then
|
||||
if cat $SERVLET_DIR/bigbluebutton/WEB-INF/classes/bigbluebutton.properties | grep -q ^bigbluebutton.web.logoutURL=$; then
|
||||
@ -154,7 +155,11 @@ else
|
||||
IP=$(echo "$(LANG=c ifconfig | awk -v RS="" '{gsub (/\n[ ]*inet /," ")}1' | grep ^et.* | head -n1 | sed 's/.*addr://g' | sed 's/ .*//g')$(LANG=c ifconfig | awk -v RS="" '{gsub (/\n[ ]*inet /," ")}1' | grep ^en.* | head -n1 | sed 's/.*addr://g' | sed 's/ .*//g')" | head -n1)
|
||||
fi
|
||||
|
||||
MEM=`free -m | grep Mem | awk '{ print $2}'`
|
||||
#
|
||||
# Calculate total memory on this server
|
||||
#
|
||||
MEM=`grep MemTotal /proc/meminfo | awk '{print $2}'`
|
||||
MEM=$((MEM/1000))
|
||||
|
||||
|
||||
#
|
||||
@ -212,13 +217,14 @@ usage() {
|
||||
echo
|
||||
echo "Configuration:"
|
||||
echo " --version Display BigBlueButton version (packages)"
|
||||
echo " --setip <host> Set IP/hostname for BigBlueButton"
|
||||
echo " --setip <IP/hostname> Set IP/hostname for BigBlueButton"
|
||||
echo " --setsecret <secret> Change the shared secret in bigbluebutton.properties"
|
||||
echo
|
||||
echo "Monitoring:"
|
||||
echo " --check Check configuration files and processes for problems"
|
||||
echo " --debug Scan the log files for error messages"
|
||||
echo " --watch Scan the log files for error messages every 2 seconds"
|
||||
echo " --network View network connections on 80 and 1935 by IP address"
|
||||
echo " --secret View the URL and shared secret for the server"
|
||||
echo " --lti View the URL and secret for LTI (if installed)"
|
||||
echo
|
||||
@ -229,10 +235,6 @@ usage() {
|
||||
echo " --clean Restart and clean all log files"
|
||||
echo " --status Display running status of components"
|
||||
echo " --zip Zip up log files for reporting an error"
|
||||
echo
|
||||
echo "Testing:"
|
||||
echo " --enablewebrtc Enables WebRTC audio in the server"
|
||||
echo " --disablewebrtc Disables WebRTC audio in the server"
|
||||
|
||||
echo
|
||||
}
|
||||
@ -397,15 +399,6 @@ start_bigbluebutton () {
|
||||
NGINX_IP=$(cat /etc/nginx/sites-available/bigbluebutton | grep -v '#' | sed -n '/server_name/{s/.*name[ ]*//;s/;//;p}' | cut -d' ' -f1)
|
||||
check_no_value server_name /etc/nginx/sites-available/bigbluebutton $NGINX_IP
|
||||
|
||||
#if ! nc -z -w 1 127.0.0.1 9123 > /dev/null; then
|
||||
# while ! nc -z -w 1 127.0.0.1 9123 > /dev/null; do
|
||||
# echo -n "."
|
||||
# sleep 1
|
||||
# done
|
||||
#fi
|
||||
|
||||
#sleep 5
|
||||
|
||||
#if ! wget http://$BBB_WEB/bigbluebutton/api -O - --quiet | grep -q SUCCESS; then
|
||||
# echo "Startup unsuccessful: could not connect to http://$BBB_WEB/bigbluebutton/api"
|
||||
# exit 1
|
||||
@ -470,6 +463,9 @@ display_bigbluebutton_status () {
|
||||
fi
|
||||
}
|
||||
|
||||
#
|
||||
# Depreciated -- WebRTC is always enabled by default
|
||||
#
|
||||
enable_webrtc(){
|
||||
# Set server ip address in FreeSWITCH
|
||||
sed -i "s@<X-PRE-PROCESS cmd=\"set\" data=\"local_ip_v4=.*\"/>@<X-PRE-PROCESS cmd=\"set\" data=\"local_ip_v4=$IP\"/>@g" $FREESWITCH_VARS
|
||||
@ -548,12 +544,6 @@ while [ $# -gt 0 ]; do
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ "$1" = "--setup-samba" -o "$1" = "-setup-samba" ]; then
|
||||
SAMBA=1
|
||||
shift
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ "$1" = "--version" -o "$1" = "-version" -o "$1" = "-v" ]; then
|
||||
VERSION=1
|
||||
shift
|
||||
@ -601,18 +591,6 @@ while [ $# -gt 0 ]; do
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ "$1" = "--enablewebrtc" -o "$1" = "-enablewebrtc" ]; then
|
||||
need_root
|
||||
enable_webrtc
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$1" = "--disablewebrtc" -o "$1" = "-disablewebrtc" ]; then
|
||||
need_root
|
||||
disable_webrtc
|
||||
exit 0
|
||||
fi
|
||||
|
||||
#
|
||||
# all other parameters requires at least 1 argument
|
||||
#
|
||||
@ -631,8 +609,8 @@ while [ $# -gt 0 ]; do
|
||||
fi
|
||||
|
||||
if [ "$1" = "--salt" -o "$1" = "-salt" -o "$1" = "--setsalt" -o "$1" = "--secret" -o "$1" = "-secret" -o "$1" = "--setsecret" ]; then
|
||||
SALT="${2}"
|
||||
if [ -z "$SALT" ]; then
|
||||
SECRET="${2}"
|
||||
if [ -z "$SECRET" ]; then
|
||||
BBB_WEB_URL=$(cat ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties | grep -v '#' | sed -n '/^bigbluebutton.web.serverURL/{s/.*=//;p}')
|
||||
SECRET=$(cat ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties | grep -v '#' | grep securitySalt | cut -d= -f2);
|
||||
echo
|
||||
@ -646,7 +624,7 @@ while [ $# -gt 0 ]; do
|
||||
fi
|
||||
|
||||
if [ "$1" = "--lti" -o "$1" = "-lti" ]; then
|
||||
if [ -z "$SALT" ]; then
|
||||
if [ -z "$SECRET" ]; then
|
||||
if [ -f ${SERVLET_DIR}/lti/WEB-INF/classes/lti-config.properties ]; then
|
||||
LTI_URL='http://'$(cat ${SERVLET_DIR}/lti/WEB-INF/classes/lti-config.properties | grep -v '#' | sed -n '/^ltiEndPoint/{s/^.*=//;p}')'/lti/tool'
|
||||
CUSTOMER=$(cat ${SERVLET_DIR}/lti/WEB-INF/classes/lti-config.properties | grep -v '#' | sed -n '/^ltiConsumer/{s/^.*=//;s/:.*//p}')
|
||||
@ -683,28 +661,30 @@ fi
|
||||
|
||||
|
||||
#
|
||||
# Set Security Salt
|
||||
# - Legacy
|
||||
# Set Shared Secret
|
||||
#
|
||||
|
||||
if [ $SALT ]; then
|
||||
if [ $SECRET ]; then
|
||||
need_root
|
||||
change_var_salt ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties securitySalt $SALT
|
||||
change_var_salt ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties securitySalt $SECRET
|
||||
|
||||
if [ -f /usr/local/bigbluebutton/bbb-webhooks/config_local.coffee ]; then
|
||||
sed -i "s|\(^[ \t]*config.bbb.sharedSecret[ =]*\).*|\1\"$SALT\"|g" /usr/local/bigbluebutton/bbb-webhooks/config_local.coffee
|
||||
sed -i "s|\(^[ \t]*config.bbb.sharedSecret[ =]*\).*|\1\"$SECRET\"|g" /usr/local/bigbluebutton/bbb-webhooks/config_local.coffee
|
||||
fi
|
||||
|
||||
if [ -f /usr/local/bigbluebutton/bbb-webhooks/extra/post_catcher.js ]; then
|
||||
sed -i "s|\(^[ \t]*var shared_secret[ =]*\)[^;]*|\1\"$SALT\"|g" /usr/local/bigbluebutton/bbb-webhooks/extra/post_catcher.js
|
||||
sed -i "s|\(^[ \t]*var shared_secret[ =]*\)[^;]*|\1\"$SECRET\"|g" /usr/local/bigbluebutton/bbb-webhooks/extra/post_catcher.js
|
||||
fi
|
||||
|
||||
if [ -f /usr/share/bbb-apps-akka/conf/application.conf ]; then
|
||||
sed -i "s/sharedSecret[ ]*=[ ]*\"[^\"]*\"/sharedSecret=\"$SALT\"/g" \
|
||||
sed -i "s/sharedSecret[ ]*=[ ]*\"[^\"]*\"/sharedSecret=\"$SECRET\"/g" \
|
||||
/usr/share/bbb-apps-akka/conf/application.conf
|
||||
fi
|
||||
|
||||
echo "Changed BigBlueButton's shared secret to $SALT"
|
||||
echo "Changed BigBlueButton's shared secret to $SECRET"
|
||||
echo
|
||||
echo "You must restart BigBlueButton for the changes to take effect"
|
||||
echo " sudo bbb-conf --restart"
|
||||
echo
|
||||
fi
|
||||
|
||||
@ -854,13 +834,13 @@ check_configuration() {
|
||||
#
|
||||
# Make sure the salt for the API matches the server
|
||||
#
|
||||
SALT_PROPERTIES=$(cat ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties | grep -v '#' | tr -d '\r' | sed -n '/securitySalt/{s/.*=//;p}')
|
||||
SALT_DEMO=$(cat ${SERVLET_DIR}/demo/bbb_api_conf.jsp | grep -v '^//' | tr -d '\r' | sed -n '/salt[ ]*=/{s/.*=[ ]*"//;s/".*//g;p}')
|
||||
SECRET_PROPERTIES=$(cat ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties | grep -v '#' | tr -d '\r' | sed -n '/securitySalt/{s/.*=//;p}')
|
||||
SECRET_DEMO=$(cat ${SERVLET_DIR}/demo/bbb_api_conf.jsp | grep -v '^//' | tr -d '\r' | sed -n '/salt[ ]*=/{s/.*=[ ]*"//;s/".*//g;p}')
|
||||
|
||||
if [ "$SALT_PROPERTIES" != "$SALT_DEMO" ]; then
|
||||
echo "# Warning: API Salt mismatch: "
|
||||
echo "# ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties = $SALT_PROPERTIES"
|
||||
echo "# ${SERVLET_DIR}/demo/bbb_api_conf.jsp = $SALT_DEMO"
|
||||
if [ "$SECRET_PROPERTIES" != "$SECRET_DEMO" ]; then
|
||||
echo "# Warning: API Shared Secret mismatch: "
|
||||
echo "# ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties = $SECRET_PROPERTIES"
|
||||
echo "# ${SERVLET_DIR}/demo/bbb_api_conf.jsp = $SECRET_DEMO"
|
||||
echo
|
||||
fi
|
||||
|
||||
@ -874,16 +854,16 @@ check_configuration() {
|
||||
fi
|
||||
fi
|
||||
|
||||
BBB_SALT=$(cat ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties | grep -v '#' | tr -d '\r' | sed -n '/securitySalt/{s/.*=//;p}')
|
||||
BBB_SECRET=$(cat ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties | grep -v '#' | tr -d '\r' | sed -n '/securitySalt/{s/.*=//;p}')
|
||||
NGINX_IP=$(cat /etc/nginx/sites-available/bigbluebutton | grep -v '#' | sed -n '/server_name/{s/.*name[ ]*//;s/;//;p}' | cut -d' ' -f1)
|
||||
|
||||
if [ -f /usr/local/bigbluebutton/bbb-webhooks/config_local.coffee ]; then
|
||||
WEBHOOKS_SALT=$(cat /usr/local/bigbluebutton/bbb-webhooks/config_local.coffee | grep '^[ \t]*config.bbb.sharedSecret[ =]*' | cut -d '"' -f2)
|
||||
WEBHOOKS_SECRET=$(cat /usr/local/bigbluebutton/bbb-webhooks/config_local.coffee | grep '^[ \t]*config.bbb.sharedSecret[ =]*' | cut -d '"' -f2)
|
||||
|
||||
if [ "$BBB_SALT" != "$WEBHOOKS_SALT" ]; then
|
||||
echo "# Warning: Webhooks API Salt mismatch: "
|
||||
echo "# ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties = $BBB_SALT"
|
||||
echo "# /usr/local/bigbluebutton/bbb-webhooks/config_local.coffee = $WEBHOOKS_SALT"
|
||||
if [ "$BBB_SECRET" != "$WEBHOOKS_SECRET" ]; then
|
||||
echo "# Warning: Webhooks API Shared Secret mismatch: "
|
||||
echo "# ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties = $BBB_SECRET"
|
||||
echo "# /usr/local/bigbluebutton/bbb-webhooks/config_local.coffee = $WEBHOOKS_SECRET"
|
||||
echo
|
||||
fi
|
||||
|
||||
@ -899,13 +879,13 @@ check_configuration() {
|
||||
fi
|
||||
|
||||
if [ -f ${SERVLET_DIR}/lti/WEB-INF/classes/lti-config.properties ]; then
|
||||
LTI_SALT=$(cat ${SERVLET_DIR}/lti/WEB-INF/classes/lti-config.properties | grep -v '#' | tr -d '\r' | sed -n '/^bigbluebuttonSalt/{s/.*=//;p}')
|
||||
BBB_SALT=$(cat ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties | grep -v '#' | tr -d '\r' | sed -n '/securitySalt/{s/.*=//;p}')
|
||||
LTI_SECRET=$(cat ${SERVLET_DIR}/lti/WEB-INF/classes/lti-config.properties | grep -v '#' | tr -d '\r' | sed -n '/^bigbluebuttonSalt/{s/.*=//;p}')
|
||||
BBB_SECRET=$(cat ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties | grep -v '#' | tr -d '\r' | sed -n '/securitySalt/{s/.*=//;p}')
|
||||
|
||||
if [ "$LTI_SALT" != "$BBB_SALT" ]; then
|
||||
if [ "$LTI_SECRET" != "$BBB_SECRET" ]; then
|
||||
echo "# Warning: LTI shared secret (salt) mismatch:"
|
||||
echo "# ${SERVLET_DIR}/lti/WEB-INF/classes/lti-config.properties = $LTI_SALT"
|
||||
echo "# ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties = $BBB_SALT"
|
||||
echo "# ${SERVLET_DIR}/lti/WEB-INF/classes/lti-config.properties = $LTI_SECRET"
|
||||
echo "# ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties = $BBB_SECRET"
|
||||
echo
|
||||
fi
|
||||
fi
|
||||
@ -1784,7 +1764,7 @@ if [ -n "$HOST" ]; then
|
||||
/usr/share/bbb-apps-akka/conf/application.conf
|
||||
sed -i "s/defaultPresentationURL[ ]*=[ ]*\"[^\"]*\"/defaultPresentationURL=\"${PROTOCOL_HTTP}:\/\/$HOST\/default.pdf\"/g" \
|
||||
/usr/share/bbb-apps-akka/conf/application.conf
|
||||
# XXX Temporary fix to ensure application.conf has the latest shared secret
|
||||
# Fix to ensure application.conf has the latest shared secret
|
||||
SECRET=$(cat ${SERVLET_DIR}/bigbluebutton/WEB-INF/classes/bigbluebutton.properties | grep -v '#' | grep securitySalt | cut -d= -f2);
|
||||
sed -i "s/sharedSecret[ ]*=[ ]*\"[^\"]*\"/sharedSecret=\"$SECRET\"/g" \
|
||||
/usr/share/bbb-apps-akka/conf/application.conf
|
||||
@ -1915,17 +1895,16 @@ if [ $CLEAN ]; then
|
||||
fi
|
||||
|
||||
if [ $NETWORK ]; then
|
||||
netstat -ant | egrep ":1935|:9123|:80\ " | egrep -v ":::|0.0.0.0" > /tmp/t_net
|
||||
netstat -ant | egrep ":1935|:80\ " | egrep -v ":::|0.0.0.0" > /tmp/t_net
|
||||
REMOTE=$(cat /tmp/t_net | cut -c 45-68 | cut -d ":" -f1 | sort | uniq)
|
||||
|
||||
if [ "$REMOTE" != "" ]; then
|
||||
echo -e "netstat\t\t\t80\t1935\t9123"
|
||||
echo -e "netstat\t\t\t80\t1935"
|
||||
for IP in $REMOTE ; do
|
||||
PORT_1935=$(cat /tmp/t_net | grep :1935 | cut -c 45-68 | cut -d ":" -f1 | grep $IP | wc -l)
|
||||
PORT_9123=$(cat /tmp/t_net | grep :9123 | cut -c 45-68 | cut -d ":" -f1 | grep $IP | wc -l )
|
||||
PORT_80=$(cat /tmp/t_net | grep :80 | cut -c 45-68 | cut -d ":" -f1 | grep $IP | wc -l )
|
||||
|
||||
echo -e "$IP\t\t$PORT_80\t$PORT_1935\t$PORT_9123"
|
||||
echo -e "$IP\t\t$PORT_80\t$PORT_1935"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
@ -43,7 +43,7 @@
|
||||
</style>
|
||||
</head>
|
||||
<body style="background-color: #2A2D33">
|
||||
<div id="app" role="application"></div>
|
||||
<div id="app" role="document"></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>
|
||||
|
@ -44,6 +44,9 @@
|
||||
.icon-bbb-fullscreen:before {
|
||||
content: "\e92a";
|
||||
}
|
||||
.icon-bbb-exit_fullscreen:before {
|
||||
content: "\e935";
|
||||
}
|
||||
.icon-bbb-settings:before {
|
||||
content: "\e92b";
|
||||
}
|
||||
@ -184,3 +187,9 @@
|
||||
.icon-bbb-thumbs_down:before {
|
||||
content: "\e92c";
|
||||
}
|
||||
.icon-bbb-send:before {
|
||||
content: "\e934";
|
||||
}
|
||||
.icon-bbb-about:before {
|
||||
content: "\e933";
|
||||
}
|
||||
|
@ -133,5 +133,4 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
}
|
||||
}, options.isListenOnly, st);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ export default function sendChat(credentials, message) {
|
||||
}
|
||||
|
||||
if (!isAllowedTo(actionName, credentials)
|
||||
&& message.from_userid !== requesterUserId) {
|
||||
&& message.from_userid !== requesterUserId) {
|
||||
throw new Meteor.Error('not-allowed', `You are not allowed to sendChat`);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,26 @@
|
||||
import Chat from '/imports/api/chat';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import { check } from 'meteor/check';
|
||||
import { BREAK_LINE } from '/imports/utils/lineEndings.js';
|
||||
|
||||
/**
|
||||
* Remove any system message from the user with userId.
|
||||
*
|
||||
* @param {string} meetingId
|
||||
* @param {string} userId
|
||||
*/
|
||||
export default function clearUserSystemMessages(meetingId, userId) {
|
||||
|
||||
check(meetingId, String);
|
||||
check(userId, String);
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
"message.from_userid": CHAT_CONFIG.type_system,
|
||||
"message.to_userid": userId,
|
||||
}
|
||||
|
||||
return Chat.remove(selector, Logger.info(`Removing system messages from: (${userId})`));
|
||||
};
|
@ -29,10 +29,12 @@ export default function updateVotes(poll, meetingId, requesterId) {
|
||||
|
||||
const modifier = {
|
||||
$set: {
|
||||
requester: requesterId,
|
||||
poll: {
|
||||
answers: answers,
|
||||
num_responders: numResponders,
|
||||
num_respondents: numRespondents,
|
||||
id,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -4,6 +4,7 @@ import Meetings from '/imports/api/meetings';
|
||||
import Users from '/imports/api/users';
|
||||
|
||||
import addChat from '/imports/api/chat/server/modifiers/addChat';
|
||||
import clearUserSystemMessages from '/imports/api/chat/server/modifiers/clearUserSystemMessages';
|
||||
|
||||
export default function handleValidateAuthToken({ payload }) {
|
||||
const meetingId = payload.meeting_id;
|
||||
@ -40,6 +41,7 @@ export default function handleValidateAuthToken({ payload }) {
|
||||
|
||||
if (numChanged) {
|
||||
if (validStatus) {
|
||||
clearUserSystemMessages(meetingId, userId);
|
||||
addWelcomeChatMessage(meetingId, userId);
|
||||
}
|
||||
|
||||
@ -57,8 +59,8 @@ const addWelcomeChatMessage = (meetingId, userId) => {
|
||||
const Meeting = Meetings.findOne({ meetingId });
|
||||
|
||||
let welcomeMessage = APP_CONFIG.defaultWelcomeMessage
|
||||
.concat(APP_CONFIG.defaultWelcomeMessageFooter)
|
||||
.replace(/%%CONFNAME%%/, Meeting.meetingName);
|
||||
.concat(APP_CONFIG.defaultWelcomeMessageFooter)
|
||||
.replace(/%%CONFNAME%%/, Meeting.meetingName);
|
||||
|
||||
const message = {
|
||||
chat_type: CHAT_CONFIG.type_system,
|
||||
|
0
bigbluebutton-html5/imports/api/users/server/methods/listenOnlyToggle.js
Executable file → Normal file
@ -57,7 +57,7 @@ export default function userLeaving(credentials, userId) {
|
||||
}
|
||||
};
|
||||
|
||||
return Users.update(selector, modifier, cb);
|
||||
Users.update(selector, modifier, cb);
|
||||
}
|
||||
|
||||
let payload = {
|
||||
|
@ -17,20 +17,6 @@ export default function removeUser(meetingId, userId) {
|
||||
|
||||
const User = Users.findOne(selector);
|
||||
|
||||
if (User && User.clientType !== CLIENT_TYPE_HTML) {
|
||||
const cb = (err, numChanged) => {
|
||||
if (err) {
|
||||
return Logger.error(`Removing user from collection: ${err}`);
|
||||
}
|
||||
|
||||
if (numChanged) {
|
||||
return Logger.info(`Removed user id=${userId} meeting=${meetingId}`);
|
||||
}
|
||||
};
|
||||
|
||||
return Users.remove(selector, cb);
|
||||
}
|
||||
|
||||
const modifier = {
|
||||
$set: {
|
||||
'user.connection_status': 'offline',
|
||||
|
@ -45,9 +45,6 @@ Meteor.publish('users', function (credentials) {
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
'user.connection_status': {
|
||||
$in: ['online', ''],
|
||||
},
|
||||
};
|
||||
|
||||
const options = {
|
||||
|
@ -132,6 +132,7 @@ export function isAllowedTo(action, credentials) {
|
||||
null != user &&
|
||||
authToken === user.authToken &&
|
||||
user.validated &&
|
||||
user.user.connection_status === 'online' &&
|
||||
'HTML5' === user.clientType &&
|
||||
null != user.user;
|
||||
|
||||
|
6
bigbluebutton-html5/imports/ui/components/about/component.jsx
Normal file → Executable file
@ -5,11 +5,9 @@ import Modal from '/imports/ui/components/modal/component';
|
||||
const intlMessages = defineMessages({
|
||||
title: {
|
||||
id: 'app.about.title',
|
||||
defaultMessage: 'About',
|
||||
},
|
||||
version: {
|
||||
id: 'app.about.version',
|
||||
defaultMessage: 'Client Build:',
|
||||
},
|
||||
copyright: {
|
||||
id: 'app.about.copyright',
|
||||
@ -17,19 +15,15 @@ const intlMessages = defineMessages({
|
||||
},
|
||||
confirmLabel: {
|
||||
id: 'app.about.confirmLabel',
|
||||
defaultMessage: 'OK',
|
||||
},
|
||||
confirmDesc: {
|
||||
id: 'app.about.confirmDesc',
|
||||
defaultMessage: 'OK',
|
||||
},
|
||||
dismissLabel: {
|
||||
id: 'app.about.dismissLabel',
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
dismissDesc: {
|
||||
id: 'app.about.dismissDesc',
|
||||
defaultMessage: 'Close about client information',
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -50,7 +50,10 @@ class ActionsDropdown extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl } = this.props;
|
||||
const { intl, isUserPresenter } = this.props;
|
||||
|
||||
if (!isUserPresenter) return null;
|
||||
|
||||
return (
|
||||
<Dropdown ref="dropdown">
|
||||
<DropdownTrigger>
|
||||
|
@ -10,51 +10,25 @@ export default class ActionsBar extends Component {
|
||||
super(props);
|
||||
}
|
||||
|
||||
renderForPresenter() {
|
||||
return (
|
||||
<div className={styles.actionsbar}>
|
||||
<div className={styles.left}>
|
||||
<ActionsDropdown />
|
||||
</div>
|
||||
<div className={styles.center}>
|
||||
<MuteAudioContainer />
|
||||
<JoinAudioOptionsContainer
|
||||
handleJoinAudio={this.props.handleOpenJoinAudio}
|
||||
handleCloseAudio={this.props.handleExitAudio}
|
||||
|
||||
/>
|
||||
{/*<JoinVideo />*/}
|
||||
<EmojiContainer />
|
||||
</div>
|
||||
<div className={styles.hidden}>
|
||||
<ActionsDropdown />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderForUser() {
|
||||
return (
|
||||
<div className={styles.actionsbar}>
|
||||
<div className={styles.center}>
|
||||
<MuteAudioContainer />
|
||||
<JoinAudioOptionsContainer
|
||||
handleJoinAudio={this.props.handleOpenJoinAudio}
|
||||
handleCloseAudio={this.props.handleExitAudio}
|
||||
|
||||
/>
|
||||
{/*<JoinVideo />*/}
|
||||
<EmojiContainer />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isUserPresenter } = this.props;
|
||||
|
||||
return isUserPresenter ?
|
||||
this.renderForPresenter() :
|
||||
this.renderForUser();
|
||||
return (
|
||||
<div className={styles.actionsbar}>
|
||||
<div className={styles.left}>
|
||||
<ActionsDropdown {...{isUserPresenter}}/>
|
||||
</div>
|
||||
<div className={styles.center}>
|
||||
<MuteAudioContainer />
|
||||
<JoinAudioOptionsContainer
|
||||
handleJoinAudio={this.props.handleOpenJoinAudio}
|
||||
handleCloseAudio={this.props.handleExitAudio}
|
||||
|
||||
/>
|
||||
{/*<JoinVideo />*/}
|
||||
<EmojiContainer />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ class EmojiMenu extends Component {
|
||||
<Button
|
||||
role="button"
|
||||
label={intl.formatMessage(intlMessages.statusTriggerLabel)}
|
||||
aria-label={intl.formatMessage(intlMessages.changeStatusLabel)}
|
||||
icon="hand"
|
||||
ghost={false}
|
||||
circle={true}
|
||||
@ -51,7 +52,7 @@ class EmojiMenu extends Component {
|
||||
icon="hand"
|
||||
label={intl.formatMessage(intlMessages.raiseLabel)}
|
||||
description={intl.formatMessage(intlMessages.raiseDesc)}
|
||||
onClick={() => actions.setEmojiHandler('hand')}
|
||||
onClick={() => actions.setEmojiHandler('raiseHand')}
|
||||
/>
|
||||
<DropdownListItem
|
||||
icon="happy"
|
||||
@ -63,7 +64,7 @@ class EmojiMenu extends Component {
|
||||
icon="undecided"
|
||||
label={intl.formatMessage(intlMessages.undecidedLabel)}
|
||||
description={intl.formatMessage(intlMessages.undecidedDesc)}
|
||||
onClick={() => actions.setEmojiHandler('undecided')}
|
||||
onClick={() => actions.setEmojiHandler('neutral')}
|
||||
/>
|
||||
<DropdownListItem
|
||||
icon="sad"
|
||||
@ -81,19 +82,19 @@ class EmojiMenu extends Component {
|
||||
icon="time"
|
||||
label={intl.formatMessage(intlMessages.awayLabel)}
|
||||
description={intl.formatMessage(intlMessages.awayDesc)}
|
||||
onClick={() => actions.setEmojiHandler('time')}
|
||||
onClick={() => actions.setEmojiHandler('away')}
|
||||
/>
|
||||
<DropdownListItem
|
||||
icon="thumbs_up"
|
||||
label={intl.formatMessage(intlMessages.thumbsupLabel)}
|
||||
description={intl.formatMessage(intlMessages.thumbsupDesc)}
|
||||
onClick={() => actions.setEmojiHandler('thumbs_up')}
|
||||
onClick={() => actions.setEmojiHandler('thumbsUp')}
|
||||
/>
|
||||
<DropdownListItem
|
||||
icon="thumbs_down"
|
||||
label={intl.formatMessage(intlMessages.thumbsdownLabel)}
|
||||
description={intl.formatMessage(intlMessages.thumbsdownDesc)}
|
||||
onClick={() => actions.setEmojiHandler('thumbs_down')}
|
||||
onClick={() => actions.setEmojiHandler('thumbsDown')}
|
||||
/>
|
||||
<DropdownListItem
|
||||
icon="applause"
|
||||
@ -101,7 +102,7 @@ class EmojiMenu extends Component {
|
||||
description={intl.formatMessage(intlMessages.applauseDesc)}
|
||||
onClick={() => actions.setEmojiHandler('applause')}
|
||||
/>
|
||||
<DropdownListSeparator/>
|
||||
<DropdownListSeparator />
|
||||
<DropdownListItem
|
||||
icon="clear_status"
|
||||
label={intl.formatMessage(intlMessages.clearLabel)}
|
||||
@ -118,107 +119,89 @@ class EmojiMenu extends Component {
|
||||
const intlMessages = defineMessages({
|
||||
statusTriggerLabel: {
|
||||
id: 'app.actionsBar.emojiMenu.statusTriggerLabel',
|
||||
defaultMessage: 'Status',
|
||||
},
|
||||
|
||||
// For away status
|
||||
awayLabel: {
|
||||
id: 'app.actionsBar.emojiMenu.awayLabel',
|
||||
defaultMessage: 'Away',
|
||||
},
|
||||
awayDesc: {
|
||||
id: 'app.actionsBar.emojiMenu.awayDesc',
|
||||
defaultMessage: 'Change your status to away',
|
||||
},
|
||||
|
||||
// For raise hand status
|
||||
raiseLabel: {
|
||||
id: 'app.actionsBar.emojiMenu.raiseLabel',
|
||||
defaultMessage: 'Raise',
|
||||
},
|
||||
raiseDesc: {
|
||||
id: 'app.actionsBar.emojiMenu.raiseDesc',
|
||||
defaultMessage: 'Raise your hand to ask a question',
|
||||
},
|
||||
|
||||
// For undecided status
|
||||
undecidedLabel: {
|
||||
id: 'app.actionsBar.emojiMenu.undecidedLabel',
|
||||
defaultMessage: 'Undecided',
|
||||
},
|
||||
undecidedDesc: {
|
||||
id: 'app.actionsBar.emojiMenu.undecidedDesc',
|
||||
defaultMessage: 'Change your status to undecided',
|
||||
},
|
||||
|
||||
// For confused status
|
||||
confusedLabel: {
|
||||
id: 'app.actionsBar.emojiMenu.confusedLabel',
|
||||
defaultMessage: 'Confused',
|
||||
},
|
||||
confusedDesc: {
|
||||
id: 'app.actionsBar.emojiMenu.confusedDesc',
|
||||
defaultMessage: 'Change your status to confused',
|
||||
},
|
||||
|
||||
// For sad status
|
||||
sadLabel: {
|
||||
id: 'app.actionsBar.emojiMenu.sadLabel',
|
||||
defaultMessage: 'Sad',
|
||||
},
|
||||
sadDesc: {
|
||||
id: 'app.actionsBar.emojiMenu.sadDesc',
|
||||
defaultMessage: 'Change your status to sad',
|
||||
},
|
||||
|
||||
// For happy status
|
||||
happyLabel: {
|
||||
id: 'app.actionsBar.emojiMenu.happyLabel',
|
||||
defaultMessage: 'Happy',
|
||||
},
|
||||
happyDesc: {
|
||||
id: 'app.actionsBar.emojiMenu.happyDesc',
|
||||
defaultMessage: 'Change your status to happy',
|
||||
},
|
||||
|
||||
// For confused status
|
||||
clearLabel: {
|
||||
id: 'app.actionsBar.emojiMenu.clearLabel',
|
||||
defaultMessage: 'Clear',
|
||||
},
|
||||
clearDesc: {
|
||||
id: 'app.actionsBar.emojiMenu.clearDesc',
|
||||
defaultMessage: 'Clear your status',
|
||||
},
|
||||
|
||||
// For applause status
|
||||
applauseLabel: {
|
||||
id: 'app.actionsBar.emojiMenu.applauseLabel',
|
||||
defaultMessage: 'Applause',
|
||||
},
|
||||
applauseDesc: {
|
||||
id: 'app.actionsBar.emojiMenu.applauseDesc',
|
||||
defaultMessage: 'Change your status to applause',
|
||||
},
|
||||
|
||||
// For thumbs up status
|
||||
thumbsupLabel: {
|
||||
id: 'app.actionsBar.emojiMenu.thumbsupLabel',
|
||||
defaultMessage: 'Thumbs up',
|
||||
},
|
||||
thumbsupDesc: {
|
||||
id: 'app.actionsBar.emojiMenu.thumbsupDesc',
|
||||
defaultMessage: 'Change your status to thumbs up',
|
||||
},
|
||||
|
||||
// For thumbs-down status
|
||||
thumbsdownLabel: {
|
||||
id: 'app.actionsBar.emojiMenu.thumbsdownLabel',
|
||||
defaultMessage: 'Thumbs down',
|
||||
},
|
||||
thumbsdownDesc: {
|
||||
id: 'app.actionsBar.emojiMenu.thumbsdownDesc',
|
||||
defaultMessage: 'Change your status to thumbs down',
|
||||
},
|
||||
changeStatusLabel: {
|
||||
id: 'app.actionsBar.changeStatusLabel'
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -5,13 +5,9 @@ import Users from '/imports/api/users';
|
||||
let isUserPresenter = () => {
|
||||
|
||||
// check if user is a presenter
|
||||
let isPresenter = Users.findOne({
|
||||
return Users.findOne({
|
||||
userId: AuthSingleton.userID,
|
||||
}).user.presenter;
|
||||
|
||||
return {
|
||||
isUserPresenter: isPresenter,
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
|
@ -1,36 +1,28 @@
|
||||
@import "../../stylesheets/variables/_all";
|
||||
|
||||
.actionsbar {
|
||||
.actionsbar,
|
||||
.left,
|
||||
.center {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.left,
|
||||
.right,
|
||||
.center,
|
||||
.hidden {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
.center {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
> * {
|
||||
margin: 0 $line-height-computed;
|
||||
}
|
||||
}
|
||||
|
||||
.left,
|
||||
.right,
|
||||
.hidden {
|
||||
flex: 0;
|
||||
.left{
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.center {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.circleGlow > :first-child{
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import NotificationsBarContainer from '../notifications-bar/container';
|
||||
import AudioNotificationContainer from '../audio/audio-notification/container';
|
||||
@ -9,6 +9,21 @@ import ChatNotificationContainer from '../chat/notification/container';
|
||||
import styles from './styles';
|
||||
import cx from 'classnames';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
userListLabel: {
|
||||
id: 'app.userlist.Label',
|
||||
},
|
||||
chatLabel: {
|
||||
id: 'app.chat.Label',
|
||||
},
|
||||
mediaLabel: {
|
||||
id: 'app.media.Label',
|
||||
},
|
||||
actionsbarLabel: {
|
||||
id: 'app.actionsBar.Label',
|
||||
},
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
init: PropTypes.func.isRequired,
|
||||
fontSize: PropTypes.string,
|
||||
@ -23,7 +38,7 @@ const defaultProps = {
|
||||
fontSize: '16px',
|
||||
};
|
||||
|
||||
export default class App extends Component {
|
||||
class App extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@ -63,7 +78,7 @@ export default class App extends Component {
|
||||
}
|
||||
|
||||
renderUserList() {
|
||||
let { userList } = this.props;
|
||||
let { userList, intl } = this.props;
|
||||
const { compactUserList } = this.state;
|
||||
|
||||
if (!userList) return;
|
||||
@ -75,44 +90,55 @@ export default class App extends Component {
|
||||
});
|
||||
|
||||
return (
|
||||
<nav className={cx(styles.userList, userListStyle)}>
|
||||
{userList}
|
||||
<nav
|
||||
className={cx(styles.userList, userListStyle)}
|
||||
aria-label={intl.formatMessage(intlMessages.userListLabel)}>
|
||||
{userList}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
renderChat() {
|
||||
const { chat } = this.props;
|
||||
const { chat, intl } = this.props;
|
||||
|
||||
if (!chat) return null;
|
||||
|
||||
return (
|
||||
<section className={styles.chat} role="log">
|
||||
{chat}
|
||||
<section
|
||||
className={styles.chat}
|
||||
role="region"
|
||||
aria-label={intl.formatMessage(intlMessages.chatLabel)}>
|
||||
{chat}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
renderMedia() {
|
||||
const { media } = this.props;
|
||||
const { media, intl } = this.props;
|
||||
|
||||
if (!media) return null;
|
||||
|
||||
return (
|
||||
<section className={styles.media}>
|
||||
{media}
|
||||
<section
|
||||
className={styles.media}
|
||||
role="region"
|
||||
aria-label={intl.formatMessage(intlMessages.mediaLabel)}>
|
||||
{media}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
renderActionsBar() {
|
||||
const { actionsbar } = this.props;
|
||||
const { actionsbar, intl } = this.props;
|
||||
|
||||
if (!actionsbar) return null;
|
||||
|
||||
return (
|
||||
<section className={styles.actionsbar}>
|
||||
{actionsbar}
|
||||
<section
|
||||
className={styles.actionsbar}
|
||||
role="region"
|
||||
aria-label={intl.formatMessage(intlMessages.actionsbarLabel)}>
|
||||
{actionsbar}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@ -144,3 +170,4 @@ export default class App extends Component {
|
||||
|
||||
App.propTypes = propTypes;
|
||||
App.defaultProps = defaultProps;
|
||||
export default injectIntl(App);
|
||||
|
@ -9,8 +9,6 @@ import {
|
||||
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';
|
||||
@ -48,10 +46,6 @@ class AppContainer extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
const init = () => {
|
||||
setDefaultSettings();
|
||||
};
|
||||
|
||||
export default withRouter(injectIntl(createContainer(({ router, intl, baseControls }) => {
|
||||
// Check if user is kicked out of the session
|
||||
Users.find({ userId: Auth.userID }).observeChanges({
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Breakouts from '/imports/api/breakouts';
|
||||
import SettingsService from '/imports/ui/components/settings/service';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import Auth from '/imports/ui/services/auth/index.js';
|
||||
|
||||
let currentModal = {
|
||||
component: null,
|
||||
@ -23,13 +24,13 @@ const clearModal = () => {
|
||||
};
|
||||
|
||||
const getCaptionsStatus = () => {
|
||||
const settings = SettingsService.getSettingsFor('cc');
|
||||
return settings ? settings.closedCaptions : false;
|
||||
const ccSettings = Settings.cc;
|
||||
return ccSettings ? ccSettings.enabled : false;
|
||||
};
|
||||
|
||||
const getFontSize = () => {
|
||||
const settings = SettingsService.getSettingsFor('application');
|
||||
return settings ? settings.fontSize : '16px';
|
||||
const applicationSettings = Settings.application;
|
||||
return applicationSettings ? applicationSettings.fontSize : '16px';
|
||||
};
|
||||
|
||||
function meetingIsBreakout() {
|
||||
|
@ -51,8 +51,6 @@ $bars-padding: $lg-padding-x - .45rem; // -.45 so user-list and chat title is al
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.content {
|
||||
@extend %full-page;
|
||||
|
||||
@ -149,11 +147,13 @@ $bars-padding: $lg-padding-x - .45rem; // -.45 so user-list and chat title is al
|
||||
|
||||
.media {
|
||||
@extend %full-page;
|
||||
flex: 1;
|
||||
order: 1;
|
||||
flex: 1 100%;
|
||||
order: 2;
|
||||
|
||||
@include mq($small-only) {
|
||||
padding-bottom: $actionsbar-height;
|
||||
margin-bottom: $actionsbar-height;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
@ -164,6 +164,7 @@ $bars-padding: $lg-padding-x - .45rem; // -.45 so user-list and chat title is al
|
||||
}
|
||||
|
||||
.actionsbar {
|
||||
flex: 1;
|
||||
padding: $bars-padding;
|
||||
position: relative;
|
||||
order: 3;
|
||||
|
@ -7,7 +7,6 @@ import { defineMessages, injectIntl } from 'react-intl';
|
||||
const intlMessages = defineMessages({
|
||||
joinAudio: {
|
||||
id: 'app.audio.joinAudio',
|
||||
defaultMessage: 'Join Audio',
|
||||
},
|
||||
leaveAudio: {
|
||||
id: 'app.audio.leaveAudio',
|
||||
|
6
bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/component.jsx
Normal file → Executable file
@ -7,27 +7,21 @@ import Modal from '/imports/ui/components/modal/component';
|
||||
const intlMessages = defineMessages({
|
||||
title: {
|
||||
id: 'app.breakoutJoinConfirmation.title',
|
||||
defaultMessage: 'Join Breakout Room',
|
||||
},
|
||||
message: {
|
||||
id: 'app.breakoutJoinConfirmation.message',
|
||||
defaultMessage: 'Do you want to join',
|
||||
},
|
||||
confirmLabel: {
|
||||
id: 'app.breakoutJoinConfirmation.confirmLabel',
|
||||
defaultMessage: 'Join',
|
||||
},
|
||||
confirmDesc: {
|
||||
id: 'app.breakoutJoinConfirmation.confirmDesc',
|
||||
defaultMessage: 'Join you to the Breakout Room',
|
||||
},
|
||||
dismissLabel: {
|
||||
id: 'app.breakoutJoinConfirmation.dismissLabel',
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
dismissDesc: {
|
||||
id: 'app.breakoutJoinConfirmation.dismissDesc',
|
||||
defaultMessage: 'Closes and rejects Joining the Breakout Room',
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -1,14 +1,20 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import styles from './styles';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import MessageForm from './message-form/component';
|
||||
import MessageList from './message-list/component';
|
||||
import Icon from '../icon/component';
|
||||
|
||||
const ELEMENT_ID = 'chat-messages';
|
||||
|
||||
export default class Chat extends Component {
|
||||
const intlMessages = defineMessages({
|
||||
closeChatLabel: {
|
||||
id: 'app.chat.closeChatLabel',
|
||||
},
|
||||
});
|
||||
|
||||
class Chat extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
@ -22,8 +28,10 @@ export default class Chat extends Component {
|
||||
scrollPosition,
|
||||
hasUnreadMessages,
|
||||
lastReadMessageTime,
|
||||
partnerIsLoggedOut,
|
||||
isChatLocked,
|
||||
actions,
|
||||
intl,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@ -31,8 +39,11 @@ export default class Chat extends Component {
|
||||
|
||||
<header className={styles.header}>
|
||||
<div className={styles.title}>
|
||||
<Link to="/users">
|
||||
<Icon iconName="left_arrow"/> {title}
|
||||
<Link
|
||||
to="/users"
|
||||
role="button"
|
||||
aria-label={intl.formatMessage(intlMessages.closeChatLabel, { title: title })}>
|
||||
<Icon iconName="left_arrow"/> {title}
|
||||
</Link>
|
||||
</div>
|
||||
<div className={styles.closeIcon}>
|
||||
@ -55,6 +66,7 @@ export default class Chat extends Component {
|
||||
handleScrollUpdate={actions.handleScrollUpdate}
|
||||
handleReadMessage={actions.handleReadMessage}
|
||||
lastReadMessageTime={lastReadMessageTime}
|
||||
partnerIsLoggedOut={partnerIsLoggedOut}
|
||||
/>
|
||||
<MessageForm
|
||||
disabled={isChatLocked}
|
||||
@ -67,3 +79,5 @@ export default class Chat extends Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(Chat);
|
||||
|
@ -54,30 +54,37 @@ export default injectIntl(createContainer(({ params, intl }) => {
|
||||
messages = ChatService.getPrivateMessages(chatID);
|
||||
}
|
||||
|
||||
if (messages && chatID !== PUBLIC_CHAT_KEY) {
|
||||
let userMessage = messages.find(m => m.sender !== null);
|
||||
let user = ChatService.getUser(chatID, '{{NAME}}');
|
||||
// TODO: Find out how to get the name of the user when logged out
|
||||
let user = ChatService.getUser(chatID, '{{NAME}}');
|
||||
|
||||
title = intl.formatMessage(intlMessages.titlePrivate, { name: user.name });
|
||||
chatName = user.name;
|
||||
let partnerIsLoggedOut = false;
|
||||
|
||||
if (user.isLoggedOut) {
|
||||
let time = Date.now();
|
||||
let id = `partner-disconnected-${time}`;
|
||||
let messagePartnerLoggedOut = {
|
||||
id: id,
|
||||
content: [{
|
||||
id: id,
|
||||
text: intl.formatMessage(intlMessages.partnerDisconnected, { name: user.name }),
|
||||
time: time,
|
||||
},],
|
||||
time: time,
|
||||
sender: null,
|
||||
};
|
||||
if (user) {
|
||||
partnerIsLoggedOut = !user.isOnline;
|
||||
|
||||
messages.push(messagePartnerLoggedOut);
|
||||
isChatLocked = true;
|
||||
if (messages && chatID !== PUBLIC_CHAT_KEY) {
|
||||
let userMessage = messages.find(m => m.sender !== null);
|
||||
let user = ChatService.getUser(chatID, '{{NAME}}');
|
||||
|
||||
title = intl.formatMessage(intlMessages.titlePrivate, { name: user.name });
|
||||
chatName = user.name;
|
||||
|
||||
if (!user.isOnline) {
|
||||
let time = Date.now();
|
||||
let id = `partner-disconnected-${time}`;
|
||||
let messagePartnerLoggedOut = {
|
||||
id,
|
||||
content: [{
|
||||
id,
|
||||
text: intl.formatMessage(intlMessages.partnerDisconnected, { name: user.name }),
|
||||
time,
|
||||
},],
|
||||
time,
|
||||
sender: null,
|
||||
};
|
||||
|
||||
messages.push(messagePartnerLoggedOut);
|
||||
isChatLocked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,6 +99,7 @@ export default injectIntl(createContainer(({ params, intl }) => {
|
||||
messages,
|
||||
lastReadMessageTime,
|
||||
hasUnreadMessages,
|
||||
partnerIsLoggedOut,
|
||||
isChatLocked,
|
||||
scrollPosition,
|
||||
actions: {
|
||||
|
@ -6,6 +6,7 @@ import styles from './styles';
|
||||
|
||||
import MessageFormActions from './message-form-actions/component';
|
||||
import TextareaAutosize from 'react-autosize-textarea';
|
||||
import Button from '../../button/component';
|
||||
|
||||
const propTypes = {
|
||||
};
|
||||
@ -16,17 +17,14 @@ const defaultProps = {
|
||||
const messages = defineMessages({
|
||||
submitLabel: {
|
||||
id: 'app.chat.submitLabel',
|
||||
defaultMessage: 'Send Message',
|
||||
description: 'Chat submit button label',
|
||||
},
|
||||
inputLabel: {
|
||||
id: 'app.chat.inputLabel',
|
||||
defaultMessage: 'Message input for chat {name}',
|
||||
description: 'Chat message input label',
|
||||
},
|
||||
inputPlaceholder: {
|
||||
id: 'app.chat.inputPlaceholder',
|
||||
defaultMessage: 'Message {name}',
|
||||
description: 'Chat message input placeholder',
|
||||
},
|
||||
});
|
||||
@ -42,9 +40,12 @@ class MessageForm extends Component {
|
||||
this.handleMessageChange = this.handleMessageChange.bind(this);
|
||||
this.handleMessageKeyDown = this.handleMessageKeyDown.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
|
||||
}
|
||||
|
||||
handleMessageKeyDown(e) {
|
||||
|
||||
//TODO Prevent send message pressing enter on mobile and/or virtual keyboard
|
||||
if (e.keyCode === 13 && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
|
||||
@ -104,9 +105,9 @@ class MessageForm extends Component {
|
||||
<TextareaAutosize
|
||||
className={styles.input}
|
||||
id="message-input"
|
||||
placeholder={ intl.formatMessage(messages.inputPlaceholder, { name: chatName }) }
|
||||
placeholder={intl.formatMessage(messages.inputPlaceholder, { name: chatName })}
|
||||
aria-controls={this.props.chatAreaId}
|
||||
aria-label={ intl.formatMessage(messages.inputLabel, { name: chatTitle }) }
|
||||
aria-label={intl.formatMessage(messages.inputLabel, { name: chatTitle })}
|
||||
autoCorrect="off"
|
||||
autoComplete="off"
|
||||
spellCheck="true"
|
||||
@ -115,13 +116,16 @@ class MessageForm extends Component {
|
||||
onChange={this.handleMessageChange}
|
||||
onKeyDown={this.handleMessageKeyDown}
|
||||
/>
|
||||
<input
|
||||
ref="btnSubmit"
|
||||
className={'sr-only'}
|
||||
<Button
|
||||
className={styles.sendButton}
|
||||
aria-label={intl.formatMessage(messages.submitLabel)}
|
||||
type="submit"
|
||||
disabled={disabled}
|
||||
value={ intl.formatMessage(messages.submitLabel) }
|
||||
/>
|
||||
label={intl.formatMessage(messages.submitLabel)}
|
||||
hideLabel={true}
|
||||
icon={"send"}
|
||||
onClick={()=>{}}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
13
bigbluebutton-html5/imports/ui/components/chat/message-form/styles.scss
Executable file → Normal file
@ -42,7 +42,7 @@
|
||||
border: $border-size solid $color-gray-lighter;
|
||||
background-clip: padding-box;
|
||||
margin: 0;
|
||||
color: $color-gray;
|
||||
color: $color-text;
|
||||
-webkit-appearance: none;
|
||||
box-shadow: none;
|
||||
outline: 0;
|
||||
@ -61,3 +61,14 @@
|
||||
background-color: fade-out($color-gray-lighter, .75);
|
||||
}
|
||||
}
|
||||
|
||||
.sendButton {
|
||||
@extend .input;
|
||||
border-left: 0px;
|
||||
min-width: 1rem;
|
||||
max-width: 2.5rem;
|
||||
|
||||
> [class*=" icon-bbb-"] {
|
||||
color: $color-gray-light;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import _ from 'lodash';
|
||||
@ -14,7 +13,6 @@ const propTypes = {
|
||||
const intlMessages = defineMessages({
|
||||
moreMessages: {
|
||||
id: 'app.chat.moreMessages',
|
||||
defaultMessage: 'More messages below',
|
||||
description: 'Chat message when the user has unread messages below the scroll',
|
||||
},
|
||||
});
|
||||
@ -112,13 +110,24 @@ class MessageList extends Component {
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (this.props.chatId !== nextProps.chatId
|
||||
|| this.props.hasUnreadMessages !== nextProps.hasUnreadMessages
|
||||
|| this.props.messages.length !== nextProps.messages.length
|
||||
|| !_.isEqual(this.props.messages, nextProps.messages)) {
|
||||
return true;
|
||||
const {
|
||||
chatId,
|
||||
hasUnreadMessages,
|
||||
partnerIsLoggedOut,
|
||||
} = this.props;
|
||||
|
||||
const switchingCorrespondent = chatId !== nextProps.chatId;
|
||||
const hasNewUnreadMessages = hasUnreadMessages !== nextProps.hasUnreadMessages;
|
||||
|
||||
// check if the messages include <user has left the meeting>
|
||||
const lastMessage = nextProps.messages[nextProps.messages.length - 1];
|
||||
if (lastMessage) {
|
||||
const userLeftIsDisplayed = lastMessage.id.includes('partner-disconnected');
|
||||
if (!(partnerIsLoggedOut && userLeftIsDisplayed)) return true;
|
||||
}
|
||||
|
||||
if (switchingCorrespondent || hasNewUnreadMessages) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@ const defaultProps = {
|
||||
export default class MessageListItem extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -41,9 +42,9 @@ export default class MessageListItem extends Component {
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.meta}>
|
||||
<div className={!user.isLoggedOut ? styles.name : styles.logout}>
|
||||
<div className={user.isOnline ? styles.name : styles.logout}>
|
||||
<span>{user.name}</span>
|
||||
{user.isLoggedOut ? <span className={styles.offline}>(offline)</span> : null}
|
||||
{user.isOnline ? null : <span className={styles.offline}>(offline)</span>}
|
||||
</div>
|
||||
<time className={styles.time} dateTime={dateTime}>
|
||||
<FormattedTime value={dateTime}/>
|
||||
|
@ -4,7 +4,7 @@ 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';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
|
||||
class ChatNotificationContainer extends Component {
|
||||
constructor(props) {
|
||||
@ -30,7 +30,7 @@ class ChatNotificationContainer extends Component {
|
||||
}
|
||||
|
||||
export default createContainer(({ currentChatID }) => {
|
||||
const AppSettings = SettingsService.getSettingsFor('application');
|
||||
const AppSettings = Settings.application;
|
||||
|
||||
const unreadMessagesCount = UserListService.getOpenChats()
|
||||
.map(chat => chat.unreadCounter)
|
||||
|
@ -38,24 +38,11 @@ const mapUser = (user) => ({
|
||||
isModerator: user.role === 'MODERATOR',
|
||||
isCurrent: user.userid === Auth.userID,
|
||||
isVoiceUser: user.voiceUser.joined,
|
||||
isOnline: user.connection_status === 'online',
|
||||
isMuted: user.voiceUser.muted,
|
||||
isListenOnly: user.listenOnly,
|
||||
isSharingWebcam: user.webcam_stream.length,
|
||||
isLocked: user.locked,
|
||||
isLoggedOut: false,
|
||||
});
|
||||
|
||||
const loggedOutUser = (userID, userName) => ({
|
||||
id: userID,
|
||||
name: userName,
|
||||
emoji: {
|
||||
status: 'none',
|
||||
},
|
||||
isPresenter: false,
|
||||
isModerator: false,
|
||||
isCurrent: false,
|
||||
isVoiceUser: false,
|
||||
isLoggedOut: true,
|
||||
});
|
||||
|
||||
const mapMessage = (messagePayload) => {
|
||||
@ -97,7 +84,7 @@ const reduceMessages = (previous, current, index, array) => {
|
||||
// with the last one
|
||||
|
||||
if (lastPayload.from_userid === currentPayload.from_userid
|
||||
&& (currentPayload.from_time - lastPayload.from_time) <= GROUPING_MESSAGES_WINDOW) {
|
||||
&& (currentPayload.from_time - lastPayload.from_time) <= GROUPING_MESSAGES_WINDOW) {
|
||||
lastMessage.content.push(current.content.pop());
|
||||
return previous;
|
||||
} else {
|
||||
@ -107,20 +94,20 @@ const reduceMessages = (previous, current, index, array) => {
|
||||
|
||||
const getUser = (userID, userName) => {
|
||||
const user = Users.findOne({ userId: userID });
|
||||
if (user) {
|
||||
return mapUser(user.user);
|
||||
} else {
|
||||
return loggedOutUser(userID, userName);
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
return mapUser(user.user);
|
||||
|
||||
};
|
||||
|
||||
const getPublicMessages = () => {
|
||||
let publicMessages = Chats.find({
|
||||
'message.chat_type': { $in: [PUBLIC_CHAT_TYPE, SYSTEM_CHAT_TYPE] },
|
||||
}, {
|
||||
sort: ['message.from_time'],
|
||||
})
|
||||
.fetch();
|
||||
sort: ['message.from_time'],
|
||||
})
|
||||
.fetch();
|
||||
|
||||
return publicMessages
|
||||
.reduce(reduceMessages, [])
|
||||
@ -135,8 +122,8 @@ const getPrivateMessages = (userID) => {
|
||||
{ 'message.from_userid': userID },
|
||||
],
|
||||
}, {
|
||||
sort: ['message.from_time'],
|
||||
}).fetch();
|
||||
sort: ['message.from_time'],
|
||||
}).fetch();
|
||||
|
||||
return messages.reduce(reduceMessages, []).map(mapMessage);
|
||||
};
|
||||
|
@ -1,13 +1,13 @@
|
||||
import Captions from '/imports/api/captions';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import Storage from '/imports/ui/services/storage/session';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
|
||||
let getCCData = () => {
|
||||
const meetingID = Auth.meetingID;
|
||||
|
||||
const ccSettings = Storage.getItem('settings_cc');
|
||||
const ccSettings = Settings.cc;
|
||||
|
||||
let CCEnabled = ccSettings.closedCaptions;
|
||||
let CCEnabled = ccSettings.enabled;
|
||||
|
||||
//associative array that keeps locales with arrays of string objects related to those locales
|
||||
let captions = [];
|
||||
|
@ -12,7 +12,6 @@ const FOCUSABLE_CHILDREN = `[tabindex]:not([tabindex="-1"]), a, input, button`;
|
||||
const intlMessages = defineMessages({
|
||||
close: {
|
||||
id: 'app.dropdown.close',
|
||||
defaultMessage: 'Close',
|
||||
},
|
||||
});
|
||||
|
||||
|
6
bigbluebutton-html5/imports/ui/components/dropdown/list/item/component.jsx
Normal file → Executable file
@ -2,7 +2,6 @@ import React, { Component, PropTypes } from 'react';
|
||||
import styles from '../styles';
|
||||
import _ from 'lodash';
|
||||
import cx from 'classnames';
|
||||
|
||||
import Icon from '/imports/ui/components/icon/component';
|
||||
|
||||
const propTypes = {
|
||||
@ -29,9 +28,8 @@ export default class DropdownListItem extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { label, description, children,
|
||||
injectRef, tabIndex, onClick, onKeyDown,
|
||||
className, style, } = this.props;
|
||||
const { label, description, children, injectRef, tabIndex, onClick, onKeyDown,
|
||||
className, style, separator, intl} = this.props;
|
||||
|
||||
return (
|
||||
<li
|
||||
|
2
bigbluebutton-html5/imports/ui/components/dropdown/list/separator/component.jsx
Normal file → Executable file
@ -5,6 +5,6 @@ import cx from 'classnames';
|
||||
export default class DropdownListSeparator extends Component {
|
||||
render() {
|
||||
const { style, className } = this.props;
|
||||
return <li style={style} className={cx(styles.separator, className)} role="separator" />;
|
||||
return <li style={style} className={cx(styles.separator, className)} />;
|
||||
}
|
||||
}
|
||||
|
4
bigbluebutton-html5/imports/ui/components/error-screen/component.jsx
Normal file → Executable file
@ -6,19 +6,15 @@ 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',
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -6,27 +6,21 @@ import Modal from '/imports/ui/components/modal/component';
|
||||
const intlMessages = defineMessages({
|
||||
title: {
|
||||
id: 'app.leaveConfirmation.title',
|
||||
defaultMessage: 'Leave Session',
|
||||
},
|
||||
message: {
|
||||
id: 'app.leaveConfirmation.message',
|
||||
defaultMessage: 'Do you want to leave this meeting?',
|
||||
},
|
||||
confirmLabel: {
|
||||
id: 'app.leaveConfirmation.confirmLabel',
|
||||
defaultMessage: 'Leave',
|
||||
},
|
||||
confirmDesc: {
|
||||
id: 'app.leaveConfirmation.confirmDesc',
|
||||
defaultMessage: 'Logs you out of the meeting',
|
||||
},
|
||||
dismissLabel: {
|
||||
id: 'app.leaveConfirmation.dismissLabel',
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
dismissDesc: {
|
||||
id: 'app.leaveConfirmation.dismissDesc',
|
||||
defaultMessage: 'Closes and rejects the leave confirmation',
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -2,9 +2,7 @@ import React, { Component, PropTypes } from 'react';
|
||||
import _ from 'lodash';
|
||||
import cx from 'classnames';
|
||||
import styles from './styles.scss';
|
||||
|
||||
import { showModal } from '/imports/ui/components/app/service';
|
||||
|
||||
import Button from '../button/component';
|
||||
import RecordingIndicator from './recording-indicator/component';
|
||||
import SettingsDropdownContainer from './settings-dropdown/container';
|
||||
@ -15,7 +13,14 @@ import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component';
|
||||
import DropdownContent from '/imports/ui/components/dropdown/content/component';
|
||||
import DropdownList from '/imports/ui/components/dropdown/list/component';
|
||||
import DropdownListItem from '/imports/ui/components/dropdown/list/item/component';
|
||||
import DropdownListSeparator from '/imports/ui/components/dropdown/list/separator/component';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
toggleUserListLabel: {
|
||||
id: 'app.navBar.userListToggleBtnLabel',
|
||||
},
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
presentationTitle: PropTypes.string.isRequired,
|
||||
@ -61,7 +66,7 @@ class NavBar extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { hasUnreadMessages, beingRecorded } = this.props;
|
||||
const { hasUnreadMessages, beingRecorded, isExpanded, intl } = this.props;
|
||||
|
||||
let toggleBtnClasses = {};
|
||||
toggleBtnClasses[styles.btn] = true;
|
||||
@ -75,12 +80,13 @@ class NavBar extends Component {
|
||||
ghost={true}
|
||||
circle={true}
|
||||
hideLabel={true}
|
||||
label={'Toggle User-List'}
|
||||
label={intl.formatMessage(intlMessages.toggleUserListLabel)}
|
||||
icon={'user'}
|
||||
className={cx(toggleBtnClasses)}
|
||||
aria-expanded={isExpanded}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.center}>
|
||||
<div className={styles.center} role="banner">
|
||||
{this.renderPresentationTitle()}
|
||||
<RecordingIndicator beingRecorded={beingRecorded}/>
|
||||
</div>
|
||||
@ -168,5 +174,4 @@ class NavBar extends Component {
|
||||
|
||||
NavBar.propTypes = propTypes;
|
||||
NavBar.defaultProps = defaultProps;
|
||||
|
||||
export default NavBar;
|
||||
export default injectIntl(NavBar);
|
||||
|
@ -1,14 +1,12 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { createContainer } from 'meteor/react-meteor-data';
|
||||
import { withRouter } from 'react-router';
|
||||
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import userListService from '../user-list/service';
|
||||
import ChatService from '../chat/service';
|
||||
import Service from './service';
|
||||
import { meetingIsBreakout } from '/imports/ui/components/app/service';
|
||||
|
||||
import NavBar from './component';
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
@ -29,6 +27,7 @@ class NavBarContainer extends Component {
|
||||
}
|
||||
|
||||
export default withRouter(createContainer(({ location, router }) => {
|
||||
|
||||
let meetingTitle;
|
||||
let meetingRecorded;
|
||||
|
||||
@ -59,7 +58,10 @@ export default withRouter(createContainer(({ location, router }) => {
|
||||
const breakouts = Service.getBreakouts();
|
||||
const currentUserId = Auth.userID;
|
||||
|
||||
let isExpanded = location.pathname.indexOf('/users') !== -1;
|
||||
|
||||
return {
|
||||
isExpanded,
|
||||
breakouts,
|
||||
currentUserId,
|
||||
meetingId,
|
||||
|
@ -19,47 +19,36 @@ import DropdownListSeparator from '/imports/ui/components/dropdown/list/separato
|
||||
const intlMessages = defineMessages({
|
||||
optionsLabel: {
|
||||
id: 'app.navBar.settingsDropdown.optionsLabel',
|
||||
defaultMessage: 'Options',
|
||||
},
|
||||
fullscreenLabel: {
|
||||
id: 'app.navBar.settingsDropdown.fullscreenLabel',
|
||||
defaultMessage: 'Make fullscreen',
|
||||
},
|
||||
settingsLabel: {
|
||||
id: 'app.navBar.settingsDropdown.settingsLabel',
|
||||
defaultMessage: 'Open settings',
|
||||
},
|
||||
aboutLabel: {
|
||||
id: 'app.navBar.settingsDropdown.aboutLabel',
|
||||
defaultMessage: 'About',
|
||||
},
|
||||
aboutDesc: {
|
||||
id: 'app.navBar.settingsDropdown.aboutDesc',
|
||||
defaultMessage: 'About',
|
||||
},
|
||||
leaveSessionLabel: {
|
||||
id: 'app.navBar.settingsDropdown.leaveSessionLabel',
|
||||
defaultMessage: 'Logout',
|
||||
},
|
||||
fullscreenDesc: {
|
||||
id: 'app.navBar.settingsDropdown.fullscreenDesc',
|
||||
defaultMessage: 'Make the settings menu fullscreen',
|
||||
},
|
||||
settingsDesc: {
|
||||
id: 'app.navBar.settingsDropdown.settingsDesc',
|
||||
defaultMessage: 'Change the general settings',
|
||||
},
|
||||
leaveSessionDesc: {
|
||||
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',
|
||||
},
|
||||
});
|
||||
|
||||
@ -80,10 +69,12 @@ class SettingsDropdown extends Component {
|
||||
|
||||
let fullScreenLabel = intl.formatMessage(intlMessages.fullscreenLabel);
|
||||
let fullScreenDesc = intl.formatMessage(intlMessages.fullscreenDesc);
|
||||
let fullScreenIcon = 'fullscreen';
|
||||
|
||||
if (isFullScreen) {
|
||||
fullScreenLabel = intl.formatMessage(intlMessages.exitFullScreenLabel);
|
||||
fullScreenDesc = intl.formatMessage(intlMessages.exitFullScreenDesc);
|
||||
fullScreenIcon = 'exit_fullscreen';
|
||||
}
|
||||
|
||||
return (
|
||||
@ -105,18 +96,19 @@ class SettingsDropdown extends Component {
|
||||
<DropdownContent placement="bottom right">
|
||||
<DropdownList>
|
||||
<DropdownListItem
|
||||
icon="fullscreen"
|
||||
icon={fullScreenIcon}
|
||||
label={fullScreenLabel}
|
||||
description={fullScreenDesc}
|
||||
onClick={this.props.handleToggleFullscreen}
|
||||
/>
|
||||
<DropdownListItem
|
||||
icon="more"
|
||||
icon="settings"
|
||||
label={intl.formatMessage(intlMessages.settingsLabel)}
|
||||
description={intl.formatMessage(intlMessages.settingsDesc)}
|
||||
onClick={openSettings.bind(this)}
|
||||
/>
|
||||
<DropdownListItem
|
||||
icon="about"
|
||||
label={intl.formatMessage(intlMessages.aboutLabel)}
|
||||
description={intl.formatMessage(intlMessages.aboutDesc)}
|
||||
onClick={openAbout.bind(this)}
|
||||
|
@ -6,6 +6,7 @@ import ClosedCaptions from '/imports/ui/components/settings/submenus/closed-capt
|
||||
import Application from '/imports/ui/components/settings/submenus/application/container';
|
||||
import Participants from '/imports/ui/components/settings/submenus/participants/component';
|
||||
import Video from '/imports/ui/components/settings/submenus/video/component';
|
||||
import _ from 'lodash';
|
||||
|
||||
import Icon from '../icon/component';
|
||||
import styles from './styles';
|
||||
@ -24,10 +25,10 @@ export default class Settings extends Component {
|
||||
|
||||
this.state = {
|
||||
current: {
|
||||
video,
|
||||
application,
|
||||
cc,
|
||||
participants,
|
||||
video: _.clone(video),
|
||||
application: _.clone(application),
|
||||
cc: _.clone(cc),
|
||||
participants: _.clone(participants),
|
||||
},
|
||||
saved: {
|
||||
video: _.clone(video),
|
||||
@ -38,7 +39,7 @@ export default class Settings extends Component {
|
||||
selectedTab: 0,
|
||||
};
|
||||
|
||||
this.handleSettingsApply = props.updateSettings;
|
||||
this.updateSettings = props.updateSettings;
|
||||
this.handleUpdateSettings = this.handleUpdateSettings.bind(this);
|
||||
this.handleSelectTab = this.handleSelectTab.bind(this);
|
||||
}
|
||||
@ -53,7 +54,7 @@ export default class Settings extends Component {
|
||||
title="Settings"
|
||||
confirm={{
|
||||
callback: (() => {
|
||||
this.handleSettingsApply(this.state.current);
|
||||
this.updateSettings(this.state.current);
|
||||
}),
|
||||
label: 'Save',
|
||||
description: 'Saves the changes and close the settings menu',
|
||||
|
@ -1,11 +1,12 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { createContainer } from 'meteor/react-meteor-data';
|
||||
import Settings from './component';
|
||||
import SettingsService from '/imports/ui/services/settings';
|
||||
|
||||
import {
|
||||
getSettingsFor,
|
||||
updateSettings,
|
||||
getClosedCaptionLocales,
|
||||
getUserRoles,
|
||||
updateSettings,
|
||||
} from './service';
|
||||
|
||||
class SettingsContainer extends Component {
|
||||
@ -18,11 +19,11 @@ class SettingsContainer extends Component {
|
||||
|
||||
export default createContainer(() => {
|
||||
return {
|
||||
audio: getSettingsFor('audio'),
|
||||
video: getSettingsFor('video'),
|
||||
application: getSettingsFor('application'),
|
||||
cc: getSettingsFor('cc'),
|
||||
participants: getSettingsFor('participants'),
|
||||
audio: SettingsService.audio,
|
||||
video: SettingsService.video,
|
||||
application: SettingsService.application,
|
||||
cc: SettingsService.cc,
|
||||
participants: SettingsService.participants,
|
||||
updateSettings,
|
||||
locales: getClosedCaptionLocales(),
|
||||
isModerator: getUserRoles() === 'MODERATOR',
|
||||
|
@ -1,18 +1,8 @@
|
||||
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 'lodash';
|
||||
|
||||
const updateSettings = (obj) => {
|
||||
Object.keys(obj).forEach(k => Storage.setItem(`settings_${k}`, obj[k]));
|
||||
};
|
||||
|
||||
const getSettingsFor = (key) => {
|
||||
const setting = Storage.getItem(`settings_${key}`);
|
||||
|
||||
return setting;
|
||||
};
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
|
||||
const getClosedCaptionLocales = () => {
|
||||
//list of unique locales in the Captions Collection
|
||||
@ -34,61 +24,13 @@ const getUserRoles = () => {
|
||||
return user.role;
|
||||
};
|
||||
|
||||
const setDefaultSettings = () => {
|
||||
const defaultSettings = {
|
||||
application: {
|
||||
chatAudioNotifications: false,
|
||||
chatPushNotifications: false,
|
||||
fontSize: '16px',
|
||||
},
|
||||
audio: {
|
||||
inputDeviceId: undefined,
|
||||
outputDeviceId: undefined,
|
||||
},
|
||||
video: {
|
||||
viewParticipantsWebcams: true,
|
||||
},
|
||||
cc: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
fontColor: '#000000',
|
||||
closedCaptions: false,
|
||||
fontFamily: 'Calibri',
|
||||
fontSize: -1,
|
||||
locale: undefined,
|
||||
takeOwnership: false,
|
||||
},
|
||||
participants: {
|
||||
muteAll: false,
|
||||
lockAll: false,
|
||||
lockAll: false,
|
||||
microphone: false,
|
||||
publicChat: false,
|
||||
privateChat: false,
|
||||
layout: false,
|
||||
},
|
||||
};
|
||||
|
||||
const savedSettings = {
|
||||
application: getSettingsFor('application'),
|
||||
audio: getSettingsFor('audio'),
|
||||
video: getSettingsFor('video'),
|
||||
cc: getSettingsFor('cc'),
|
||||
participants: getSettingsFor('participants'),
|
||||
};
|
||||
|
||||
let settings = {};
|
||||
|
||||
Object.keys(defaultSettings).forEach(key => {
|
||||
settings[key] = _.extend(defaultSettings[key], savedSettings[key]);
|
||||
});
|
||||
|
||||
updateSettings(settings);
|
||||
const updateSettings = (obj) => {
|
||||
Object.keys(obj).forEach(k => Settings[k] = obj[k]);
|
||||
Settings.save();
|
||||
};
|
||||
|
||||
export {
|
||||
updateSettings,
|
||||
getSettingsFor,
|
||||
getClosedCaptionLocales,
|
||||
getUserRoles,
|
||||
setDefaultSettings,
|
||||
updateSettings,
|
||||
};
|
||||
|
@ -28,7 +28,7 @@ export default class ClosedCaptionsMenu extends BaseMenu {
|
||||
settings: {
|
||||
backgroundColor: props.settings ? props.settings.backgroundColor : '#f3f6f9',
|
||||
fontColor: props.settings ? props.settings.fontColor : '#000000',
|
||||
closedCaptions: props.settings ? props.settings.closedCaptions : false,
|
||||
enabled: props.settings ? props.settings.enabled : false,
|
||||
fontFamily: props.settings ? props.settings.fontFamily : 'Calibri',
|
||||
fontSize: props.settings ? props.settings.fontSize : -1,
|
||||
locale: props.settings ? props.settings.locale : -1,
|
||||
@ -99,8 +99,8 @@ export default class ClosedCaptionsMenu extends BaseMenu {
|
||||
<div className={cx(styles.formElement, styles.pullContentRight)}>
|
||||
<Toggle
|
||||
icons={false}
|
||||
defaultChecked={this.state.settings.closedCaptions}
|
||||
onChange={() => this.handleToggle('closedCaptions')} />
|
||||
defaultChecked={this.state.settings.enabled}
|
||||
onChange={() => this.handleToggle('enabled')} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -10,6 +10,7 @@ const propTypes = {
|
||||
isPresenter: React.PropTypes.bool.isRequired,
|
||||
isVoiceUser: React.PropTypes.bool.isRequired,
|
||||
isModerator: React.PropTypes.bool.isRequired,
|
||||
isOnline: React.PropTypes.bool.isRequired,
|
||||
image: React.PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
@ -23,7 +24,7 @@ export default class UserAvatar extends Component {
|
||||
user,
|
||||
} = this.props;
|
||||
|
||||
const avatarColor = !user.isLoggedOut ? generateColor(user.name) : '#fff';
|
||||
const avatarColor = user.isOnline ? generateColor(user.name) : '#fff';
|
||||
|
||||
let avatarStyles = {
|
||||
backgroundColor: avatarColor,
|
||||
@ -31,8 +32,8 @@ export default class UserAvatar extends Component {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={!user.isLoggedOut ? styles.userAvatar : styles.userLogout}
|
||||
style={avatarStyles}>
|
||||
<div className={user.isOnline ? styles.userAvatar : styles.userLogout}
|
||||
style={avatarStyles} aria-hidden="true">
|
||||
<span>
|
||||
{this.renderAvatarContent()}
|
||||
</span>
|
||||
@ -48,7 +49,28 @@ export default class UserAvatar extends Component {
|
||||
let content = user.name.slice(0, 2);
|
||||
|
||||
if (user.emoji.status !== 'none') {
|
||||
content = <Icon iconName={user.emoji.status}/>;
|
||||
let iconEmoji = undefined;
|
||||
|
||||
switch (user.emoji.status) {
|
||||
case 'thumbsUp':
|
||||
iconEmoji = 'thumbs_up';
|
||||
break;
|
||||
case 'thumbsDown':
|
||||
iconEmoji = 'thumbs_down';
|
||||
break;
|
||||
case 'raiseHand':
|
||||
iconEmoji = 'hand';
|
||||
break;
|
||||
case 'away':
|
||||
iconEmoji = 'time';
|
||||
break;
|
||||
case 'neutral':
|
||||
iconEmoji = 'undecided';
|
||||
break;
|
||||
default:
|
||||
iconEmoji = user.emoji.status;
|
||||
}
|
||||
content = <Icon iconName={iconEmoji}/>;
|
||||
}
|
||||
|
||||
return content;
|
||||
|
1
bigbluebutton-html5/imports/ui/components/user-avatar/styles.scss
Executable file → Normal file
@ -19,6 +19,7 @@ $moderator-bg: $color-primary;
|
||||
flex-shrink: 0;
|
||||
line-height: 2.2rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
|
@ -10,7 +10,12 @@ import { defineMessages, injectIntl } from 'react-intl';
|
||||
const intlMessages = defineMessages({
|
||||
titlePublic: {
|
||||
id: 'app.chat.titlePublic',
|
||||
defaultMessage: "Public Chat",
|
||||
},
|
||||
unreadPlural: {
|
||||
id: 'app.userlist.chatlistitem.unreadPlural',
|
||||
},
|
||||
unreadSingular: {
|
||||
id: 'app.userlist.chatlistitem.unreadSingular',
|
||||
},
|
||||
});
|
||||
|
||||
@ -38,9 +43,11 @@ class ChatListItem extends Component {
|
||||
} = this.props;
|
||||
|
||||
const linkPath = [PRIVATE_CHAT_PATH, chat.id].join('');
|
||||
const isCurrentChat = chat.id === openChat;
|
||||
let isSingleMessage = chat.unreadCounter === 1;
|
||||
|
||||
let linkClasses = {};
|
||||
linkClasses[styles.active] = chat.id === openChat;
|
||||
linkClasses[styles.active] = isCurrentChat;
|
||||
|
||||
if (chat.name === 'Public Chat') {
|
||||
chat.name = intl.formatMessage(intlMessages.titlePublic);
|
||||
@ -48,16 +55,26 @@ class ChatListItem extends Component {
|
||||
|
||||
return (
|
||||
<li className={cx(styles.chatListItem, linkClasses)}>
|
||||
<Link to={linkPath} className={styles.chatListItemLink}>
|
||||
{chat.icon ? this.renderChatIcon() : this.renderChatAvatar()}
|
||||
<div className={styles.chatName}>
|
||||
{!compact ? <h3 className={styles.chatNameMain}>{chat.name}</h3> : null }
|
||||
</div>
|
||||
{(chat.unreadCounter > 0) ?
|
||||
<div className={styles.unreadMessages}>
|
||||
<p className={styles.unreadMessagesText}>{chat.unreadCounter}</p>
|
||||
<Link
|
||||
to={linkPath}
|
||||
className={styles.chatListItemLink}
|
||||
role="button"
|
||||
aria-expanded={isCurrentChat}>
|
||||
{chat.icon ? this.renderChatIcon() : this.renderChatAvatar()}
|
||||
<div className={styles.chatName}>
|
||||
{!compact ? <span className={styles.chatNameMain}>{chat.name}</span> : null }
|
||||
</div>
|
||||
: null}
|
||||
{(chat.unreadCounter > 0) ?
|
||||
<div
|
||||
className={styles.unreadMessages}
|
||||
aria-label={isSingleMessage
|
||||
? intl.formatMessage(intlMessages.unreadSingular, { count: chat.unreadCounter })
|
||||
: intl.formatMessage(intlMessages.unreadPlural, { count: chat.unreadCounter })}>
|
||||
<div className={styles.unreadMessagesText} aria-hidden="true">
|
||||
{chat.unreadCounter}
|
||||
</div>
|
||||
</div>
|
||||
: null}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
|
@ -2,9 +2,8 @@ import React, { Component, PropTypes } from 'react';
|
||||
import { withRouter } from 'react-router';
|
||||
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
|
||||
import styles from './styles.scss';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import cx from 'classnames';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import UserListItem from './user-list-item/component.jsx';
|
||||
import ChatListItem from './chat-list-item/component.jsx';
|
||||
|
||||
@ -43,16 +42,14 @@ class UserList extends Component {
|
||||
}
|
||||
|
||||
renderHeader() {
|
||||
const { intl } = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.header}>
|
||||
{
|
||||
!this.state.compact ?
|
||||
<h2 className={styles.headerTitle}>
|
||||
<FormattedMessage
|
||||
id="app.userlist.participantsTitle"
|
||||
description="Title for the Header"
|
||||
defaultMessage="Participants"
|
||||
/>
|
||||
{intl.formatMessage(intlMessages.participantsTitle)}
|
||||
</h2> : null
|
||||
}
|
||||
</div>
|
||||
@ -72,6 +69,7 @@ class UserList extends Component {
|
||||
const {
|
||||
openChats,
|
||||
openChat,
|
||||
intl,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@ -79,11 +77,7 @@ class UserList extends Component {
|
||||
{
|
||||
!this.state.compact ?
|
||||
<h3 className={styles.smallTitle}>
|
||||
<FormattedMessage
|
||||
id="app.userlist.messagesTitle"
|
||||
description="Title for the messages list"
|
||||
defaultMessage="Messages"
|
||||
/>
|
||||
{intl.formatMessage(intlMessages.messagesTitle)}
|
||||
</h3> : <hr className={styles.separator}></hr>
|
||||
}
|
||||
<div className={styles.scrollableList}>
|
||||
@ -117,6 +111,7 @@ class UserList extends Component {
|
||||
userActions,
|
||||
compact,
|
||||
isBreakoutRoom,
|
||||
intl,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@ -124,11 +119,7 @@ class UserList extends Component {
|
||||
{
|
||||
!this.state.compact ?
|
||||
<h3 className={styles.smallTitle}>
|
||||
<FormattedMessage
|
||||
id="app.userlist.usersTitle"
|
||||
description="Title for the Users list"
|
||||
defaultMessage="Users"
|
||||
/>
|
||||
{intl.formatMessage(intlMessages.usersTitle)}
|
||||
({users.length})
|
||||
</h3> : <hr className={styles.separator}></hr>
|
||||
}
|
||||
@ -159,5 +150,20 @@ class UserList extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
usersTitle: {
|
||||
id: 'app.userlist.usersTitle',
|
||||
description: 'Title for the Header',
|
||||
},
|
||||
messagesTitle: {
|
||||
id: 'app.userlist.messagesTitle',
|
||||
description: 'Title for the messages list',
|
||||
},
|
||||
participantsTitle: {
|
||||
id: 'app.userlist.participantsTitle',
|
||||
description: 'Title for the Users list',
|
||||
},
|
||||
});
|
||||
|
||||
UserList.propTypes = propTypes;
|
||||
export default withRouter(UserList);
|
||||
export default withRouter(injectIntl(UserList));
|
||||
|
@ -34,14 +34,14 @@ const mapUser = user => ({
|
||||
isListenOnly: user.listenOnly,
|
||||
isSharingWebcam: user.webcam_stream.length,
|
||||
isPhoneUser: user.phone_user,
|
||||
isLoggedOut: !user ? true : false,
|
||||
isOnline: true
|
||||
});
|
||||
|
||||
const mapOpenChats = chat => {
|
||||
let currentUserId = Auth.userID;
|
||||
return chat.message.from_userid !== Auth.userID
|
||||
? chat.message.from_userid
|
||||
: chat.message.to_userid;
|
||||
? chat.message.from_userid
|
||||
: chat.message.to_userid;
|
||||
};
|
||||
|
||||
const sortUsersByName = (a, b) => {
|
||||
@ -165,21 +165,21 @@ const userFindSorting = {
|
||||
|
||||
const getUsers = () => {
|
||||
let users = Users
|
||||
.find({}, userFindSorting)
|
||||
.fetch();
|
||||
.find({ "user.connection_status": 'online' }, userFindSorting)
|
||||
.fetch();
|
||||
|
||||
return users
|
||||
.map(u => u.user)
|
||||
.map(mapUser)
|
||||
.sort(sortUsers);
|
||||
.map(u => u.user)
|
||||
.map(mapUser)
|
||||
.sort(sortUsers);
|
||||
};
|
||||
|
||||
const getOpenChats = chatID => {
|
||||
|
||||
let openChats = Chat
|
||||
.find({ 'message.chat_type': PRIVATE_CHAT_TYPE })
|
||||
.fetch()
|
||||
.map(mapOpenChats);
|
||||
.find({ 'message.chat_type': PRIVATE_CHAT_TYPE })
|
||||
.fetch()
|
||||
.map(mapOpenChats);
|
||||
|
||||
let currentUserId = Auth.userID;
|
||||
|
||||
@ -190,13 +190,13 @@ const getOpenChats = chatID => {
|
||||
openChats = _.uniq(openChats);
|
||||
|
||||
openChats = Users
|
||||
.find({ 'user.userid': { $in: openChats } })
|
||||
.map(u => u.user)
|
||||
.map(mapUser)
|
||||
.map(op => {
|
||||
op.unreadCounter = UnreadMessages.count(op.id);
|
||||
return op;
|
||||
});
|
||||
.find({ 'user.userid': { $in: openChats } })
|
||||
.map(u => u.user)
|
||||
.map(mapUser)
|
||||
.map(op => {
|
||||
op.unreadCounter = UnreadMessages.count(op.id);
|
||||
return op;
|
||||
});
|
||||
|
||||
let currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY) || [];
|
||||
let filteredChatList = [];
|
||||
@ -228,7 +228,7 @@ const getOpenChats = chatID => {
|
||||
});
|
||||
|
||||
return openChats
|
||||
.sort(sortChats);
|
||||
.sort(sortChats);
|
||||
};
|
||||
|
||||
getCurrentUser = () => {
|
||||
@ -261,12 +261,12 @@ const userActions = {
|
||||
},
|
||||
mute: {
|
||||
label: 'Mute Audio',
|
||||
handler: user=> callServer('muteUser', user.id),
|
||||
handler: user => callServer('muteUser', user.id),
|
||||
icon: 'audio_off',
|
||||
},
|
||||
unmute: {
|
||||
label: 'Unmute Audio',
|
||||
handler: user=> callServer('unmuteUser', user.id),
|
||||
handler: user => callServer('unmuteUser', user.id),
|
||||
icon: 'audio_on',
|
||||
},
|
||||
};
|
||||
|
@ -40,12 +40,10 @@ const messages = defineMessages({
|
||||
presenter: {
|
||||
id: 'app.userlist.presenter',
|
||||
description: 'Text for identifying presenter user',
|
||||
defaultMessage: 'Presenter',
|
||||
},
|
||||
you: {
|
||||
id: 'app.userlist.you',
|
||||
description: 'Text for identifying your user',
|
||||
defaultMessage: 'You',
|
||||
},
|
||||
});
|
||||
|
||||
@ -83,6 +81,9 @@ class UserListItem extends Component {
|
||||
|
||||
this.state = {
|
||||
isActionsOpen: false,
|
||||
dropdownOffset: 0,
|
||||
dropdownDirection: 'top',
|
||||
dropdownVisible: false,
|
||||
};
|
||||
|
||||
this.handleScroll = this.handleScroll.bind(this);
|
||||
@ -123,7 +124,8 @@ class UserListItem extends Component {
|
||||
// if currentUser is a moderator, allow kicking other users
|
||||
let allowedToKick = currentUser.isModerator && !user.isCurrent && !isBreakoutRoom;
|
||||
|
||||
let allowedToSetPresenter = (currentUser.isModerator || currentUser.isPresenter) && !user.isPresenter;
|
||||
let allowedToSetPresenter =
|
||||
(currentUser.isModerator || currentUser.isPresenter) && !user.isPresenter;
|
||||
|
||||
return _.compact([
|
||||
(allowedToChatPrivately ? this.renderUserAction(openChat, router, user) : null),
|
||||
@ -135,21 +137,82 @@ class UserListItem extends Component {
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
this.checkDropdownDirection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the dropdown is visible, if so, check if should be draw on top or bottom direction.
|
||||
*/
|
||||
checkDropdownDirection() {
|
||||
if (this.isDropdownActivedByUser()) {
|
||||
const dropdown = findDOMNode(this.refs.dropdown);
|
||||
const dropdownTrigger = dropdown.children[0];
|
||||
const dropdownContent = dropdown.children[1];
|
||||
|
||||
const scrollContainer = dropdown.parentElement.parentElement;
|
||||
|
||||
let nextState = {
|
||||
dropdownVisible: true,
|
||||
};
|
||||
|
||||
const isDropdownVisible =
|
||||
this.checkIfDropdownIsVisible(dropdownContent.offsetTop, dropdownContent.offsetHeight);
|
||||
|
||||
if (!isDropdownVisible) {
|
||||
const offsetPageTop =
|
||||
(dropdownTrigger.offsetTop + dropdownTrigger.offsetHeight - scrollContainer.scrollTop);
|
||||
|
||||
nextState.dropdownOffset = window.innerHeight - offsetPageTop;
|
||||
nextState.dropdownDirection = 'bottom';
|
||||
}
|
||||
|
||||
this.setState(nextState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the dropdown is visible and is opened by the user
|
||||
*
|
||||
* @return True if is visible and opened by the user.
|
||||
*/
|
||||
isDropdownActivedByUser() {
|
||||
const { isActionsOpen, dropdownVisible } = this.state;
|
||||
return isActionsOpen && !dropdownVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the content fit on the screen, false otherwise.
|
||||
*
|
||||
* @param {number} contentOffSetTop
|
||||
* @param {number} contentOffsetHeight
|
||||
* @return True if the content fit on the screen, false otherwise.
|
||||
*/
|
||||
checkIfDropdownIsVisible(contentOffSetTop, contentOffsetHeight) {
|
||||
return (contentOffSetTop + contentOffsetHeight) < window.innerHeight;
|
||||
}
|
||||
|
||||
|
||||
onActionsShow() {
|
||||
const dropdown = findDOMNode(this.refs.dropdown);
|
||||
const scrollContainer = dropdown.parentElement.parentElement;
|
||||
const dropdownTrigger = dropdown.children[0];
|
||||
|
||||
this.setState({
|
||||
contentTop: `${dropdown.offsetTop - dropdown.parentElement.parentElement.scrollTop}px`,
|
||||
isActionsOpen: true,
|
||||
active: true,
|
||||
dropdownVisible: false,
|
||||
dropdownOffset: dropdownTrigger.offsetTop - scrollContainer.scrollTop,
|
||||
dropdownDirection: 'top',
|
||||
});
|
||||
|
||||
findDOMNode(this).parentElement.addEventListener('scroll', this.handleScroll, false);
|
||||
scrollContainer.addEventListener('scroll', this.handleScroll, false);
|
||||
}
|
||||
|
||||
onActionsHide() {
|
||||
this.setState({
|
||||
active: false,
|
||||
isActionsOpen: false,
|
||||
dropdownVisible: false,
|
||||
});
|
||||
|
||||
findDOMNode(this).parentElement.removeEventListener('scroll', this.handleScroll, false);
|
||||
@ -162,10 +225,12 @@ class UserListItem extends Component {
|
||||
|
||||
let userItemContentsStyle = {};
|
||||
userItemContentsStyle[styles.userItemContentsCompact] = compact;
|
||||
userItemContentsStyle[styles.active] = this.state.active;
|
||||
userItemContentsStyle[styles.active] = this.state.isActionsOpen;
|
||||
|
||||
return (
|
||||
<li
|
||||
role="button"
|
||||
aria-haspopup="true"
|
||||
className={cx(styles.userListItem, userItemContentsStyle)}>
|
||||
{this.renderUserContents()}
|
||||
</li>
|
||||
@ -180,7 +245,7 @@ class UserListItem extends Component {
|
||||
let actions = this.getAvailableActions();
|
||||
let contents = (
|
||||
<div tabIndex={0} className={styles.userItemContents}>
|
||||
<UserAvatar user={user}/>
|
||||
<UserAvatar user={user} />
|
||||
{this.renderUserName()}
|
||||
{this.renderUserIcons()}
|
||||
</div>
|
||||
@ -190,10 +255,12 @@ class UserListItem extends Component {
|
||||
return contents;
|
||||
}
|
||||
|
||||
const { dropdownOffset, dropdownDirection, dropdownVisible, } = this.state;
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
isOpen={this.state.isActionsOpen}
|
||||
ref="dropdown"
|
||||
isOpen={this.state.isActionsOpen}
|
||||
onShow={this.onActionsShow}
|
||||
onHide={this.onActionsHide}
|
||||
className={styles.dropdown}>
|
||||
@ -202,10 +269,11 @@ class UserListItem extends Component {
|
||||
</DropdownTrigger>
|
||||
<DropdownContent
|
||||
style={{
|
||||
top: this.state.contentTop,
|
||||
visibility: dropdownVisible ? 'visible' : 'hidden',
|
||||
[dropdownDirection]: `${dropdownOffset}px`,
|
||||
}}
|
||||
className={styles.dropdownContent}
|
||||
placement="right top">
|
||||
placement={`right ${dropdownDirection}`}>
|
||||
|
||||
<DropdownList>
|
||||
{
|
||||
@ -215,7 +283,7 @@ class UserListItem extends Component {
|
||||
key={_.uniqueId('action-header')}
|
||||
label={user.name}
|
||||
style={{ fontWeight: 600 }}
|
||||
defaultMessage={user.name}/>),
|
||||
defaultMessage={user.name} />),
|
||||
(<DropdownListSeparator key={_.uniqueId('action-separator')} />),
|
||||
].concat(actions)
|
||||
}
|
||||
@ -249,12 +317,12 @@ class UserListItem extends Component {
|
||||
|
||||
return (
|
||||
<div className={styles.userName}>
|
||||
<h3 className={styles.userNameMain}>
|
||||
<span className={styles.userNameMain}>
|
||||
{user.name}
|
||||
</h3>
|
||||
<p className={styles.userNameSub}>
|
||||
</span>
|
||||
<span className={styles.userNameSub}>
|
||||
{userNameSub}
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -294,16 +362,16 @@ class UserListItem extends Component {
|
||||
{
|
||||
user.isSharingWebcam ?
|
||||
<span className={styles.userIconsContainer}>
|
||||
<Icon iconName='video'/>
|
||||
<Icon iconName='video' />
|
||||
</span>
|
||||
: null
|
||||
}
|
||||
{
|
||||
audioChatIcon ?
|
||||
<span className={cx(audioIconClassnames)}>
|
||||
<Icon iconName={audioChatIcon}/>
|
||||
</span>
|
||||
: null
|
||||
<span className={cx(audioIconClassnames)}>
|
||||
<Icon iconName={audioChatIcon} />
|
||||
</span>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
56
bigbluebutton-html5/imports/ui/services/settings/index.js
Normal file
@ -0,0 +1,56 @@
|
||||
import Storage from '/imports/ui/services/storage/session';
|
||||
import _ from 'lodash';
|
||||
|
||||
const SETTINGS = [
|
||||
'application',
|
||||
'audio',
|
||||
'video',
|
||||
'cc',
|
||||
'participants',
|
||||
];
|
||||
|
||||
class Settings {
|
||||
constructor(defaultValues = {}) {
|
||||
SETTINGS.forEach(p => {
|
||||
const privateProp = `_${p}`;
|
||||
this[privateProp] = {
|
||||
tracker: new Tracker.Dependency,
|
||||
value: undefined,
|
||||
};
|
||||
|
||||
Object.defineProperty(this, p, {
|
||||
get: () => {
|
||||
this[privateProp].tracker.depend();
|
||||
return this[privateProp].value;
|
||||
},
|
||||
|
||||
set: v => {
|
||||
this[privateProp].value = v;
|
||||
this[privateProp].tracker.changed();
|
||||
},
|
||||
});
|
||||
});
|
||||
this.setDefault(defaultValues);
|
||||
}
|
||||
|
||||
setDefault(defaultValues) {
|
||||
const savedSettings = {};
|
||||
|
||||
SETTINGS.forEach(s => {
|
||||
savedSettings[s] = Storage.getItem(`settings_${s}`);
|
||||
});
|
||||
|
||||
Object.keys(defaultValues).forEach(key => {
|
||||
this[key] = _.extend(defaultValues[key], savedSettings[key]);
|
||||
});
|
||||
|
||||
this.save();
|
||||
};
|
||||
|
||||
save() {
|
||||
Object.keys(this).forEach(k => Storage.setItem(`settings${k}`, this[k].value));
|
||||
}
|
||||
}
|
||||
|
||||
const SettingsSingleton = new Settings(Meteor.settings.public.app.defaultSettings);
|
||||
export default SettingsSingleton;
|
@ -1,4 +1,4 @@
|
||||
const EMOJI_STATUSES = ['time', 'hand', 'undecided', 'confused', 'sad',
|
||||
'happy', 'applause', 'thumbs_up', 'thumbs_down'];
|
||||
const EMOJI_STATUSES = ['away', 'raiseHand', 'neutral', 'confused', 'sad',
|
||||
'happy', 'applause', 'thumbsUp', 'thumbsDown'];
|
||||
|
||||
export { EMOJI_STATUSES };
|
||||
|
@ -25,3 +25,30 @@ app:
|
||||
basename: '/html5client'
|
||||
|
||||
defaultLocale: 'en'
|
||||
#default settings for session storage
|
||||
defaultSettings:
|
||||
application:
|
||||
chatAudioNotifications: false
|
||||
chatPushNotifications: false
|
||||
fontSize: "16px"
|
||||
audio:
|
||||
inputDeviceId: undefined
|
||||
outputDeviceId: undefined
|
||||
video:
|
||||
viewParticipantsWebcams: true
|
||||
cc:
|
||||
backgroundColor: "#FFFFFF"
|
||||
fontColor: "#000000"
|
||||
enabled: false
|
||||
fontFamily: "Calibri"
|
||||
fontSize: '16px'
|
||||
# locale: undefined
|
||||
takeOwnership: false
|
||||
participants:
|
||||
muteAll: false
|
||||
lockAll: false
|
||||
lockAll: false
|
||||
microphone: false
|
||||
publicChat: false
|
||||
privateChat: false
|
||||
layout: false
|
||||
|
@ -5,13 +5,19 @@
|
||||
"app.userlist.messagesTitle": "Messages",
|
||||
"app.userlist.presenter": "Presenter",
|
||||
"app.userlist.you": "You",
|
||||
"app.userlist.Label": "User List",
|
||||
"app.chat.submitLabel": "Send Message",
|
||||
"app.chat.inputLabel": "Message input for chat {name}",
|
||||
"app.chat.inputPlaceholder": "Message {name}",
|
||||
"app.chat.titlePublic": "Public Chat",
|
||||
"app.chat.titlePrivate": "Private Chat with {name}",
|
||||
"app.chat.partnerDisconnected": "{name} has left the meeting",
|
||||
"app.chat.closeChatLabel": "Close {title}",
|
||||
"app.chat.moreMessages": "More messages below",
|
||||
"app.userlist.chatlistitem.unreadSingular": "{count} New Message",
|
||||
"app.userlist.chatlistitem.unreadPlural": "{count} New Messages",
|
||||
"app.chat.Label": "Chat",
|
||||
"app.media.Label": "Media",
|
||||
"app.presentation.presentationToolbar.prevSlideLabel": "Previous slide",
|
||||
"app.presentation.presentationToolbar.prevSlideDescrip": "Change the presentation to the previous slide",
|
||||
"app.presentation.presentationToolbar.nextSlideLabel": "Next slide",
|
||||
@ -38,6 +44,7 @@
|
||||
"app.navBar.settingsDropdown.leaveSessionDesc": "Leave the meeting",
|
||||
"app.navBar.settingsDropdown.exitFullScreenLabel": "Exit fullscreen",
|
||||
"app.navBar.settingsDropdown.exitFullScreenDesc": "Exit fullscreen mode",
|
||||
"app.navBar.userListToggleBtnLabel": "User List Toggle",
|
||||
"app.leaveConfirmation.title": "Leave Session",
|
||||
"app.leaveConfirmation.message": "Do you want to leave this meeting?",
|
||||
"app.leaveConfirmation.confirmLabel": "Leave",
|
||||
@ -51,9 +58,11 @@
|
||||
"app.about.confirmDesc": "OK",
|
||||
"app.about.dismissLabel": "Cancel",
|
||||
"app.about.dismissDesc": "Close about client information",
|
||||
"app.actionsBar.changeStatusLabel": "Change Status",
|
||||
"app.actionsBar.muteLabel": "Mute",
|
||||
"app.actionsBar.camOffLabel": "Cam Off",
|
||||
"app.actionsBar.raiseLabel": "Raise",
|
||||
"app.actionsBar.Label": "Actions Bar",
|
||||
"app.actionsBar.actionsDropdown.actionsLabel": "Actions",
|
||||
"app.actionsBar.actionsDropdown.presentationLabel": "Upload a presentation",
|
||||
"app.actionsBar.actionsDropdown.initPollLabel": "Initiate a poll",
|
||||
|
2844
bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.sfd
Normal file
30
bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.svg
Executable file → Normal file
@ -5,9 +5,9 @@
|
||||
-->
|
||||
<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 Thu Mar 23 10:01:15 2017
|
||||
By Ghazi
|
||||
Copyright (c) 2016, BlindSide Networks Inc.
|
||||
Created by FontForge 20161003 at Tue Apr 11 11:56:37 2017
|
||||
By Tyler Copeland
|
||||
Copyright (c) 2016, BigBlueButton Inc.
|
||||
</metadata>
|
||||
<defs>
|
||||
<font id="BigBlueButton" horiz-adv-x="1024" >
|
||||
@ -19,10 +19,10 @@ 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 1024 820"
|
||||
bbox="-0.0117188 -205 1031.18 826.183"
|
||||
underline-thickness="51"
|
||||
underline-position="-102"
|
||||
unicode-range="U+0020-E932"
|
||||
unicode-range="U+0020-E935"
|
||||
/>
|
||||
<missing-glyph horiz-adv-x="1048"
|
||||
/>
|
||||
@ -218,5 +218,25 @@ c28 -28 63 -42 102 -42s77 14 102 42zM371 272c0 -18 7 -43 7 -43l282 283v92c0 39 -
|
||||
<glyph glyph-name="application" unicode=""
|
||||
d="M952 679c31 0 55 -24 55 -55v-634c0 -31 -24 -55 -55 -55h-876c-34 0 -58 24 -58 55v634c3 31 27 55 58 55h876zM946 618h-863v-82h863v82zM83 0h863v471h-863v-471zM246 577c0 13 11 24 24 24s24 -11 24 -24s-11 -24 -24 -24s-24 11 -24 24zM148 577c0 13 11 24 24 24
|
||||
s24 -11 24 -24s-11 -24 -24 -24s-24 11 -24 24zM342 577c0 13 11 24 24 24s24 -11 24 -24s-11 -24 -24 -24s-24 11 -24 24z" />
|
||||
<glyph glyph-name="about" unicode=""
|
||||
d="M512 -205c-282.624 0 -512 229.376 -512 512s229.376 512 512 512s512 -229.376 512 -512s-229.376 -512 -512 -512zM512 754.829c-247.39 0 -448.171 -200.78 -448.171 -448.171c0 -247.39 200.781 -448.17 448.171 -448.17c247.391 0 448.171 200.78 448.171 448.17
|
||||
v0.341797c-0.375977 247.014 -201.156 447.642 -448.171 447.829zM521.898 429.539c-28.2617 0 -51.2002 22.9375 -51.2002 51.1992c0 28.2627 22.9385 51.2002 51.2002 51.2002c0.385742 0.0107422 1.01074 0.0195312 1.39551 0.0195312
|
||||
c27.1318 0 49.1523 -22.0195 49.1523 -49.1514c0 -0.476562 -0.0136719 -1.25 -0.0302734 -1.72656c0.00292969 -0.209961 0.00585938 -0.551758 0.00585938 -0.761719c0 -10.6777 -5.9668 -25.6279 -13.3184 -33.3721
|
||||
c-8.22852 -9.16309 -24.8965 -16.9619 -37.2051 -17.4072zM567.296 68.0664h-89.4297v307.2h88.4062z" />
|
||||
<glyph glyph-name="send" unicode=""
|
||||
d="M40.2773 -143.219c-9.67285 0.0253906 -22.8232 5.83594 -29.3545 12.9707c-6.03613 6.37598 -10.9346 18.6758 -10.9346 27.4561c0 5.56836 2.14551 14.0645 4.79004 18.9648l212.651 401.067l-211.286 379.903c-2.91504 5.11719 -5.28125 14.0508 -5.28125 19.9395
|
||||
c0 22.2334 18.0439 40.2773 40.2773 40.2773c4.77734 0 12.1895 -1.59277 16.5459 -3.55469l942.08 -409.601c13.4932 -5.76855 24.4434 -22.3604 24.4434 -37.0342c0 -14.6748 -10.9502 -31.2666 -24.4434 -37.0352l-943.104 -409.6
|
||||
c-4.30469 -1.98438 -11.6445 -3.66699 -16.3838 -3.75488zM78.165 682.467l192.513 -345.088c2.71387 -4.97656 4.91699 -13.6172 4.91699 -19.2852c0 -5.66895 -2.20312 -14.3086 -4.91699 -19.2861l-194.901 -367.274l868.011 375.467zM977.237 292.664v0z
|
||||
M34.1338 702.264v0z" />
|
||||
<glyph glyph-name="exit_fullscreen" unicode=""
|
||||
d="M627.371 440.12c-1.42578 3.33984 -2.64941 8.99805 -2.73145 12.6289v208.214c3.57422 14.2607 18.4072 25.835 33.1094 25.835s29.5352 -11.5742 33.1094 -25.835v-128.342l276.821 276.821c5.47266 9.24121 18.6299 16.7402 29.3691 16.7402
|
||||
c18.8418 0 34.1338 -15.292 34.1338 -34.1338c0 -10.7393 -7.49902 -23.8965 -16.7402 -29.3691l-276.821 -276.821h128.342c2.25488 0.56543 5.97266 1.02441 8.29785 1.02441c18.8418 0 34.1328 -15.292 34.1328 -34.1338c0 -18.8408 -15.291 -34.1328 -34.1328 -34.1328
|
||||
c-2.3252 0 -6.04297 0.458984 -8.29785 1.02344h-208.214c-11.9795 0.320312 -25.5889 9.49512 -30.3779 20.4805zM56.3203 -195.442c-4.44043 -2.62988 -12.2334 -4.76465 -17.3936 -4.76465c-18.8418 0 -34.1338 15.292 -34.1338 34.1338
|
||||
c0 5.16016 2.13477 12.9531 4.76465 17.3936l276.821 277.845h-128.342c-2.25488 -0.56543 -5.97266 -1.02344 -8.29785 -1.02344c-18.8418 0 -34.1328 15.291 -34.1328 34.1328s15.291 34.1338 34.1328 34.1338c2.3252 0 6.04297 -0.458984 8.29785 -1.02441h207.189
|
||||
c18.8418 0 34.1338 -15.292 34.1338 -34.1328v-208.214c0.564453 -2.25488 1.02344 -5.97266 1.02344 -8.29785c0 -18.8418 -15.292 -34.1328 -34.1328 -34.1328c-18.8418 0 -34.1338 15.291 -34.1338 34.1328c0 2.3252 0.458984 6.04297 1.02441 8.29785v128.342z
|
||||
M286.379 485.858l-276.821 276.821c-2.62988 4.44043 -4.76465 12.2334 -4.76465 17.3936c0 18.8418 15.292 34.1338 34.1338 34.1338c5.16016 0 12.9531 -2.13477 17.3936 -4.76465l277.845 -276.821v128.342c3.57422 14.2607 18.4072 25.835 33.1094 25.835
|
||||
s29.5352 -11.5742 33.1094 -25.835v-207.189c0 -18.8418 -15.292 -34.1338 -34.1328 -34.1338h-208.214c-14.2607 3.57422 -25.835 18.4072 -25.835 33.1094s11.5742 29.5352 25.835 33.1094h128.342zM657.749 -80.0723c-17.0752 0.65332 -31.7549 15.0283 -32.7676 32.0859
|
||||
v208.213c0 18.8418 15.292 34.1338 34.1328 34.1338h208.214c2.25488 0.564453 5.97266 1.02344 8.29785 1.02344c18.8418 0 34.1338 -15.292 34.1338 -34.1328c0 -18.8418 -15.292 -34.1338 -34.1338 -34.1338c-2.3252 0 -6.04297 0.458984 -8.29785 1.02441h-128.683
|
||||
l276.821 -276.821c2.62988 -4.44043 4.76367 -12.2334 4.76367 -17.3936c0 -18.8418 -15.291 -34.1338 -34.1328 -34.1338c-5.16113 0 -12.9531 2.13477 -17.3936 4.76465l-276.821 276.821v-127.317c0 -18.8418 -15.292 -34.1338 -34.1338 -34.1338z" />
|
||||
</font>
|
||||
</defs></svg>
|
||||
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 35 KiB |
BIN
bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.ttf
Executable file → Normal file
BIN
bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.woff
Executable file → Normal file
0
bigbluebutton-html5/public/svgs/bbb_audio_icon.svg
Executable file → Normal file
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
0
bigbluebutton-html5/public/svgs/bbb_headphones_icon.svg
Executable file → Normal file
Before Width: | Height: | Size: 806 B After Width: | Height: | Size: 806 B |
@ -86,7 +86,7 @@ public abstract class AbstractPageConverterHandler extends
|
||||
return stderrBuilder.indexOf(value) > -1;
|
||||
}
|
||||
|
||||
public Boolean isConversionSuccessfull() {
|
||||
public Boolean isConversionSuccessful() {
|
||||
return !exitedWithError();
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ public class Pdf2SwfPageConverterHandler extends AbstractPageConverterHandler {
|
||||
private static String IMAGE_TAG_PATTERN = "\\d+\\s" + IMAGE_TAG_OUTPUT;
|
||||
|
||||
@Override
|
||||
public Boolean isConversionSuccessfull() {
|
||||
public Boolean isConversionSuccessful() {
|
||||
return !exitedWithError();
|
||||
}
|
||||
|
||||
|
@ -99,18 +99,23 @@ public class Pdf2SwfPageConverter implements PageConverter {
|
||||
log.debug("Pdf2Swf conversion duration: {} sec", (pdf2SwfEnd - pdf2SwfStart)/1000);
|
||||
|
||||
File destFile = new File(dest);
|
||||
if (pHandler.isConversionSuccessfull() && destFile.exists()
|
||||
if (pHandler.isConversionSuccessful() && destFile.exists()
|
||||
&& pHandler.numberOfPlacements() < placementsThreshold
|
||||
&& pHandler.numberOfTextTags() < defineTextThreshold
|
||||
&& pHandler.numberOfImageTags() < imageTagThreshold) {
|
||||
return true;
|
||||
} else {
|
||||
// We need t delete the destination file as we are starting a new conversion process
|
||||
if (destFile.exists()) {
|
||||
destFile.delete();
|
||||
}
|
||||
|
||||
Map<String, Object> logData = new HashMap<String, Object>();
|
||||
logData.put("meetingId", pres.getMeetingId());
|
||||
logData.put("presId", pres.getId());
|
||||
logData.put("filename", pres.getName());
|
||||
logData.put("page", page);
|
||||
logData.put("convertSuccess", pHandler.isConversionSuccessfull());
|
||||
logData.put("convertSuccess", pHandler.isConversionSuccessful());
|
||||
logData.put("fileExists", destFile.exists());
|
||||
logData.put("numObjectTags", pHandler.numberOfPlacements());
|
||||
logData.put("numTextTags", pHandler.numberOfTextTags());
|
||||
@ -203,7 +208,7 @@ public class Pdf2SwfPageConverter implements PageConverter {
|
||||
tempPdfPage.delete();
|
||||
tempPng.delete();
|
||||
|
||||
boolean doneSwf = pSwfHandler.isConversionSuccessfull();
|
||||
boolean doneSwf = pSwfHandler.isConversionSuccessful();
|
||||
|
||||
long convertEnd = System.currentTimeMillis();
|
||||
|
||||
|
@ -1,16 +1,16 @@
|
||||
<#-- GET_RECORDINGS FreeMarker XML template -->
|
||||
<#compress>
|
||||
<response>
|
||||
<#-- Where code is a 'SUCCESS' or 'FAILED' String -->
|
||||
<returncode>${returnCode}</returncode>
|
||||
<meetingName>${meeting.getName()}</meetingName>
|
||||
<meetingID>${meeting.getExternalId()}</meetingID>
|
||||
<meetingName>${meeting.getName()?html}</meetingName>
|
||||
<meetingID>${meeting.getExternalId()?html}</meetingID>
|
||||
<internalMeetingID>${meeting.getInternalId()}</internalMeetingID>
|
||||
<createTime>${meeting.getCreateTime()?c}</createTime>
|
||||
<createDate>${createdOn}</createDate>
|
||||
<voiceBridge>${meeting.getTelVoice()}</voiceBridge>
|
||||
<dialNumber>${meeting.getDialNumber()}</dialNumber>
|
||||
<attendeePW>${meeting.getViewerPassword()}</attendeePW>
|
||||
<moderatorPW>${meeting.getModeratorPassword()}</moderatorPW>
|
||||
<attendeePW>${meeting.getViewerPassword()?html}</attendeePW>
|
||||
<moderatorPW>${meeting.getModeratorPassword()?html}</moderatorPW>
|
||||
<running>${meeting.isRunning()?c}</running>
|
||||
<duration>${meeting.getDuration()}</duration>
|
||||
<hasUserJoined>${meeting.hasUserJoined()?c}</hasUserJoined>
|
||||
@ -28,7 +28,7 @@
|
||||
<#list meeting.getUsers() as att>
|
||||
<attendee>
|
||||
<userID>${att.getInternalUserId()}</userID>
|
||||
<fullName>${att.getFullname()}</fullName>
|
||||
<fullName>${att.getFullname()?html}</fullName>
|
||||
<role>${att.getRole()}</role>
|
||||
<isPresenter>${att.isPresenter()?c}</isPresenter>
|
||||
<isListeningOnly>${att.isListeningOnly()?c}</isListeningOnly>
|
||||
@ -38,7 +38,7 @@
|
||||
<#assign ucd = meeting.getUserCustomData(att.getExternalUserId())>
|
||||
<customdata>
|
||||
<#list ucd?keys as prop>
|
||||
<${prop}><![CDATA[${ucd[prop]}]]></${prop}>
|
||||
<${(prop)?html}>${(ucd[prop])?html}</${(prop)?html}>
|
||||
</#list>
|
||||
</customdata>
|
||||
</#if>
|
||||
@ -48,7 +48,7 @@
|
||||
<#assign m = meeting.getMetadata()>
|
||||
<metadata>
|
||||
<#list m?keys as prop>
|
||||
<${prop}><![CDATA[${m[prop]}]]></${prop}>
|
||||
<${(prop)?html}>${(m[prop])?html}</${(prop)?html}>
|
||||
</#list>
|
||||
</metadata>
|
||||
|
||||
@ -75,4 +75,5 @@
|
||||
</breakoutRooms>
|
||||
</#list>
|
||||
|
||||
</response>
|
||||
</response>
|
||||
</#compress>
|
@ -1,4 +1,4 @@
|
||||
<#-- GET_RECORDINGS FreeMarker XML template -->
|
||||
<#compress>
|
||||
<response>
|
||||
<#-- Where code is a 'SUCCESS' or 'FAILED' String -->
|
||||
<returncode>${returnCode}</returncode>
|
||||
@ -7,15 +7,15 @@
|
||||
<#items as meetingDetail>
|
||||
<#assign meeting = meetingDetail.getMeeting()>
|
||||
<meeting>
|
||||
<meetingName>${meeting.getName()}</meetingName>
|
||||
<meetingID>${meeting.getExternalId()}</meetingID>
|
||||
<meetingName>${meeting.getName()?html}</meetingName>
|
||||
<meetingID>${meeting.getExternalId()?html}</meetingID>
|
||||
<internalMeetingID>${meeting.getInternalId()}</internalMeetingID>
|
||||
<createTime>${meeting.getCreateTime()?c}</createTime>
|
||||
<createDate>${meetingDetail.getCreatedOn()}</createDate>
|
||||
<voiceBridge>${meeting.getTelVoice()}</voiceBridge>
|
||||
<dialNumber>${meeting.getDialNumber()}</dialNumber>
|
||||
<attendeePW>${meeting.getViewerPassword()}</attendeePW>
|
||||
<moderatorPW>${meeting.getModeratorPassword()}</moderatorPW>
|
||||
<attendeePW>${meeting.getViewerPassword()?html}</attendeePW>
|
||||
<moderatorPW>${meeting.getModeratorPassword()?html}</moderatorPW>
|
||||
<running>${meeting.isRunning()?c}</running>
|
||||
<duration>${meeting.getDuration()}</duration>
|
||||
<hasUserJoined>${meeting.hasUserJoined()?c}</hasUserJoined>
|
||||
@ -33,7 +33,7 @@
|
||||
<#list meetingDetail.meeting.getUsers() as att>
|
||||
<attendee>
|
||||
<userID>${att.getInternalUserId()}</userID>
|
||||
<fullName>${att.getFullname()}</fullName>
|
||||
<fullName>${att.getFullname()?html}</fullName>
|
||||
<role>${att.getRole()}</role>
|
||||
<isPresenter>${att.isPresenter()?c}</isPresenter>
|
||||
<isListeningOnly>${att.isListeningOnly()?c}</isListeningOnly>
|
||||
@ -43,7 +43,7 @@
|
||||
<#assign ucd = meetingDetail.meeting.getUserCustomData(att.getExternalUserId())>
|
||||
<customdata>
|
||||
<#list ucd?keys as prop>
|
||||
<${prop}><![CDATA[${ucd[prop]}]]></${prop}>
|
||||
<${(prop)?html}>${(ucd[prop])?html}</${(prop)?html}>
|
||||
</#list>
|
||||
</customdata>
|
||||
</#if>
|
||||
@ -53,7 +53,7 @@
|
||||
<#assign m = meetingDetail.meeting.getMetadata()>
|
||||
<metadata>
|
||||
<#list m?keys as prop>
|
||||
<${prop}><![CDATA[${m[prop]}]]></${prop}>
|
||||
<${(prop)?html}>${(m[prop])?html}</${(prop)?html}>
|
||||
</#list>
|
||||
</metadata>
|
||||
|
||||
@ -75,4 +75,5 @@
|
||||
</#items>
|
||||
</meetings>
|
||||
</#list>
|
||||
</response>
|
||||
</response>
|
||||
</#compress>
|
@ -1,4 +1,3 @@
|
||||
<#-- GET_RECORDINGS FreeMarker XML template -->
|
||||
<#compress>
|
||||
<response>
|
||||
<#-- Where code is a 'SUCCESS' or 'FAILED' String -->
|
||||
|