Merge branch 'master' of github.com:bigbluebutton/bigbluebutton into audio-restructuring

This commit is contained in:
Anton Georgiev 2017-04-17 11:37:49 -04:00
commit 450d2f3435
102 changed files with 7700 additions and 674 deletions

View File

@ -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)

View File

@ -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 = {

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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');
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 B

View File

@ -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

View File

@ -60,6 +60,7 @@
enabledEchoCancel="true"
useWebRTCIfAvailable="true"
showPhoneOption="false"
showWebRTCStats="false"
echoTestApp="9196"
dependsOn="UsersModule"
/>

View File

@ -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>

File diff suppressed because it is too large Load Diff

View 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);

View File

@ -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':

View 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;
}
})();

View 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);
}
}

View File

@ -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);
}
}
}

View File

@ -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();

View File

@ -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">

View File

@ -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" />
<!--

View File

@ -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>

View File

@ -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");
}
}
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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) {

View File

@ -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 {

View File

@ -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

View File

@ -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>

View File

@ -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";
}

View File

@ -133,5 +133,4 @@ export default class SIPBridge extends BaseAudioBridge {
}
}, options.isListenOnly, st);
}
}

View File

@ -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`);
}

View File

@ -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})`));
};

View File

@ -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,
},
},
};

View File

@ -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,

View File

@ -57,7 +57,7 @@ export default function userLeaving(credentials, userId) {
}
};
return Users.update(selector, modifier, cb);
Users.update(selector, modifier, cb);
}
let payload = {

View File

@ -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',

View File

@ -45,9 +45,6 @@ Meteor.publish('users', function (credentials) {
const selector = {
meetingId,
'user.connection_status': {
$in: ['online', ''],
},
};
const options = {

View File

@ -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;

View 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',
},
});

View File

@ -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>

View File

@ -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>
);
}
}

View File

@ -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'
},
});

View File

@ -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 {

View File

@ -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{

View File

@ -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);

View File

@ -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({

View File

@ -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() {

View File

@ -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;

View File

@ -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',

View 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',
},
});

View File

@ -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);

View File

@ -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: {

View File

@ -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>
);
}

View 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;
}
}

View File

@ -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;
}

View File

@ -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}/>

View File

@ -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)

View File

@ -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);
};

View File

@ -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 = [];

View File

@ -12,7 +12,6 @@ const FOCUSABLE_CHILDREN = `[tabindex]:not([tabindex="-1"]), a, input, button`;
const intlMessages = defineMessages({
close: {
id: 'app.dropdown.close',
defaultMessage: 'Close',
},
});

View 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

View 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)} />;
}
}

View 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',
},
});

View File

@ -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',
},
});

View File

@ -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);

View File

@ -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,

View File

@ -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)}

View File

@ -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',

View File

@ -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',

View File

@ -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,
};

View File

@ -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>

View File

@ -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;

View 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;

View File

@ -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>
);

View File

@ -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)}
&nbsp;({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));

View File

@ -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',
},
};

View File

@ -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>
);

View 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;

View File

@ -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 };

View File

@ -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

View File

@ -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",

File diff suppressed because it is too large Load Diff

30
bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.svg Executable file → Normal file
View 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="&#xe901;"
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="&#xe933;"
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="&#xe934;"
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="&#xe935;"
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

Binary file not shown.

BIN
bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.woff Executable file → Normal file

Binary file not shown.

0
bigbluebutton-html5/public/svgs/bbb_audio_icon.svg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 806 B

After

Width:  |  Height:  |  Size: 806 B

View File

@ -86,7 +86,7 @@ public abstract class AbstractPageConverterHandler extends
return stderrBuilder.indexOf(value) > -1;
}
public Boolean isConversionSuccessfull() {
public Boolean isConversionSuccessful() {
return !exitedWithError();
}
}

View File

@ -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();
}

View File

@ -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();

View File

@ -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>

View File

@ -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>

View File

@ -1,4 +1,3 @@
<#-- GET_RECORDINGS FreeMarker XML template -->
<#compress>
<response>
<#-- Where code is a 'SUCCESS' or 'FAILED' String -->

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