Merge pull request #5770 from ritzalam/merge-latest-20-branch

Merge latest 20 branch
This commit is contained in:
Richard Alam 2018-06-28 11:37:48 -04:00 committed by GitHub
commit 63d43da710
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 614 additions and 351 deletions

View File

@ -19,9 +19,20 @@ trait SyncGetUsersMeetingRespMsgHdlr {
val users = Users2x.findAll(liveMeeting.users2x)
val webUsers = users.map { u =>
WebUser(intId = u.intId, extId = u.extId, name = u.name, role = u.role,
guest = u.guest, authed = u.authed, guestStatus = u.guestStatus, emoji = u.emoji,
locked = u.locked, presenter = u.presenter, avatar = u.avatar)
WebUser(
intId = u.intId,
extId = u.extId,
name = u.name,
role = u.role,
guest = u.guest,
authed = u.authed,
guestStatus = u.guestStatus,
emoji = u.emoji,
locked = u.locked,
presenter = u.presenter,
avatar = u.avatar,
clientType = u.clientType
)
}
val body = SyncGetUsersMeetingRespMsgBody(webUsers)

View File

@ -14,7 +14,7 @@ trait UserJoinMeetingAfterReconnectReqMsgHdlr extends HandlerHelpers with Breako
def handleUserJoinMeetingAfterReconnectReqMsg(msg: UserJoinMeetingAfterReconnectReqMsg, state: MeetingState2x): MeetingState2x = {
val newState = userJoinMeeting(outGW, msg.body.authToken, liveMeeting, state)
val newState = userJoinMeeting(outGW, msg.body.authToken, msg.body.clientType, liveMeeting, state)
if (liveMeeting.props.meetingProp.isBreakout) {
updateParentMeetingWithUsers()
}

View File

@ -13,7 +13,7 @@ trait UserJoinMeetingReqMsgHdlr extends HandlerHelpers with BreakoutHdlrHelpers
val outGW: OutMsgRouter
def handleUserJoinMeetingReqMsg(msg: UserJoinMeetingReqMsg, state: MeetingState2x): MeetingState2x = {
val newState = userJoinMeeting(outGW, msg.body.authToken, liveMeeting, state)
val newState = userJoinMeeting(outGW, msg.body.authToken, msg.body.clientType, liveMeeting, state)
if (liveMeeting.props.meetingProp.isBreakout) {
updateParentMeetingWithUsers()

View File

@ -66,7 +66,7 @@ trait ValidateAuthTokenReqMsgHdlr extends HandlerHelpers {
val webUsers = users.map { u =>
WebUser(intId = u.intId, extId = u.extId, name = u.name, role = u.role,
guest = u.guest, authed = u.authed, guestStatus = u.guestStatus, emoji = u.emoji,
locked = u.locked, presenter = u.presenter, avatar = u.avatar)
locked = u.locked, presenter = u.presenter, avatar = u.avatar, clientType = u.clientType)
}
val event = MsgBuilder.buildGetUsersMeetingRespMsg(meetingId, requesterId, webUsers)

View File

@ -209,11 +209,22 @@ class Users2x {
case class OldPresenter(userId: String, changedPresenterOn: Long)
case class UserState(intId: String, extId: String, name: String, role: String,
guest: Boolean, authed: Boolean, guestStatus: String, emoji: String, locked: Boolean,
presenter: Boolean, avatar: String,
roleChangedOn: Long = System.currentTimeMillis(),
inactivityResponseOn: Long = TimeUtil.timeNowInMs())
case class UserState(
intId: String,
extId: String,
name: String,
role: String,
guest: Boolean,
authed: Boolean,
guestStatus: String,
emoji: String,
locked: Boolean,
presenter: Boolean,
avatar: String,
roleChangedOn: Long = System.currentTimeMillis(),
inactivityResponseOn: Long = TimeUtil.timeNowInMs(),
clientType: String
)
case class UserIdAndName(id: String, name: String)

View File

@ -26,7 +26,7 @@ trait HandlerHelpers extends SystemConfiguration {
outGW.send(event)
}
def userJoinMeeting(outGW: OutMsgRouter, authToken: String,
def userJoinMeeting(outGW: OutMsgRouter, authToken: String, clientType: String,
liveMeeting: LiveMeeting, state: MeetingState2x): MeetingState2x = {
val nu = for {
regUser <- RegisteredUsers.findWithToken(authToken, liveMeeting.registeredUsers)
@ -42,7 +42,8 @@ trait HandlerHelpers extends SystemConfiguration {
emoji = "none",
presenter = false,
locked = MeetingStatus2x.getPermissions(liveMeeting.status).lockOnJoin,
avatar = regUser.avatarURL
avatar = regUser.avatarURL,
clientType = clientType
)
}

View File

@ -10,8 +10,10 @@ object UserJoinedMeetingEvtMsgBuilder {
val body = UserJoinedMeetingEvtMsgBody(intId = userState.intId, extId = userState.extId, name = userState.name,
role = userState.role, guest = userState.guest, authed = userState.authed,
guestStatus = userState.guestStatus, emoji = userState.emoji,
presenter = userState.presenter, locked = userState.locked, avatar = userState.avatar)
guestStatus = userState.guestStatus,
emoji = userState.emoji,
presenter = userState.presenter, locked = userState.locked, avatar = userState.avatar,
clientType = userState.clientType)
val event = UserJoinedMeetingEvtMsg(meetingId, userState.intId, body)

View File

@ -68,7 +68,6 @@ trait FakeTestData {
def createFakeUser(liveMeeting: LiveMeeting, regUser: RegisteredUser): UserState = {
UserState(intId = regUser.id, extId = regUser.externId, name = regUser.name, role = regUser.role,
guest = regUser.guest, authed = regUser.authed, guestStatus = regUser.guestStatus,
emoji = "none", locked = false, presenter = false, avatar = regUser.avatarURL)
emoji = "none", locked = false, presenter = false, avatar = regUser.avatarURL, clientType = "unknown")
}
}

View File

@ -303,7 +303,7 @@ public class VideoTranscoder extends UntypedActor implements ProcessMonitorObser
ffmpeg = new FFmpegCommand();
ffmpeg.setFFmpegPath(FFMPEG_PATH);
ffmpeg.setInput(input);
ffmpeg.setProtocolWhitelist("file,udp,rtp");
ffmpeg.setLoglevel("quiet");
ffmpeg.setOutput(outputLive);
ffmpeg.addRtmpOutputConnectionParameter(meetingId);
@ -570,7 +570,7 @@ public class VideoTranscoder extends UntypedActor implements ProcessMonitorObser
if(currentFFmpegRestartNumber == MAX_RESTARTINGS_NUMBER) {
long timeInterval = System.currentTimeMillis() - lastFFmpegRestartTime;
if(timeInterval <= MIN_RESTART_TIME) {
System.out.println(" > Max number of ffmpeg restartings reached in " + timeInterval + " miliseconds for " + transcoderId + "'s Video Transcoder." +
System.out.println(" > Max number of ffmpeg restartings reached in " + timeInterval + " miliseconds for " + transcoderId + "'s Video Transcoder." +
" Not restating it anymore.");
return true;
}

View File

@ -29,6 +29,8 @@ public class FFmpegCommand {
private int frameRate;
private String frameSize;
private String protocolWhitelist;
public FFmpegCommand() {
this.args = new HashMap();
this.x264Params = new HashMap();
@ -82,6 +84,11 @@ public class FFmpegCommand {
comm.add(probeSize);
}
if(protocolWhitelist != null && !protocolWhitelist.isEmpty()) {
comm.add("-protocol_whitelist");
comm.add(protocolWhitelist);
}
buildRtmpInput();
comm.add("-i");
@ -323,6 +330,14 @@ public class FFmpegCommand {
this.frameSize = value;
}
/**
* Sets protocol elements to be whitelisted
* @param whitelist
*/
public void setProtocolWhitelist(String whitelist) {
this.protocolWhitelist = whitelist;
}
/**
* Add parameters for rtmp connections.
* The order of parameters is the order they are added

View File

@ -67,8 +67,9 @@ object UserJoinedMeetingEvtMsg {
case class UserJoinedMeetingEvtMsg(header: BbbClientMsgHeader,
body: UserJoinedMeetingEvtMsgBody) extends BbbCoreMsg
case class UserJoinedMeetingEvtMsgBody(intId: String, extId: String, name: String, role: String,
guest: Boolean, authed: Boolean, guestStatus: String, emoji: String,
presenter: Boolean, locked: Boolean, avatar: String)
guest: Boolean, authed: Boolean, guestStatus: String,
emoji: String,
presenter: Boolean, locked: Boolean, avatar: String, clientType: String)
/**
* Sent by client to get all users in a meeting.
@ -271,14 +272,14 @@ case class LogoutAndEndMeetingCmdMsgBody(userId: String)
object UserJoinMeetingReqMsg { val NAME = "UserJoinMeetingReqMsg" }
case class UserJoinMeetingReqMsg(header: BbbClientMsgHeader, body: UserJoinMeetingReqMsgBody) extends StandardMsg
case class UserJoinMeetingReqMsgBody(userId: String, authToken: String)
case class UserJoinMeetingReqMsgBody(userId: String, authToken: String, clientType: String)
/**
* Sent from Flash client to rejoin meeting after reconnection
*/
object UserJoinMeetingAfterReconnectReqMsg { val NAME = "UserJoinMeetingAfterReconnectReqMsg" }
case class UserJoinMeetingAfterReconnectReqMsg(header: BbbClientMsgHeader, body: UserJoinMeetingAfterReconnectReqMsgBody) extends StandardMsg
case class UserJoinMeetingAfterReconnectReqMsgBody(userId: String, authToken: String)
case class UserJoinMeetingAfterReconnectReqMsgBody(userId: String, authToken: String, clientType: String)
/**
* Sent from bbb-apps when user disconnects from Red5.
@ -306,8 +307,9 @@ object GetUsersMeetingRespMsg {
case class GetUsersMeetingRespMsg(header: BbbClientMsgHeader, body: GetUsersMeetingRespMsgBody) extends BbbCoreMsg
case class GetUsersMeetingRespMsgBody(users: Vector[WebUser])
case class WebUser(intId: String, extId: String, name: String, role: String,
guest: Boolean, authed: Boolean, guestStatus: String, emoji: String, locked: Boolean,
presenter: Boolean, avatar: String)
guest: Boolean, authed: Boolean, guestStatus: String,
emoji: String, locked: Boolean,
presenter: Boolean, avatar: String, clientType: String)
object SyncGetUsersMeetingRespMsg { val NAME = "SyncGetUsersMeetingRespMsg"}
case class SyncGetUsersMeetingRespMsg(header: BbbClientMsgHeader, body: SyncGetUsersMeetingRespMsgBody) extends BbbCoreMsg

View File

@ -650,8 +650,8 @@ public class MeetingService implements MessageListener {
}
User user = new User(message.userId, message.externalUserId,
message.name, message.role, message.avatarURL, message.guest,
message.guestStatus);
message.name, message.role, message.avatarURL, message.guest, message.guestStatus,
message.clientType);
m.userJoined(user);
m.setGuestStatusWithId(user.getInternalUserId(), message.guestStatus);
UserSession userSession = getUserSessionWithUserId(user.getInternalUserId());
@ -671,6 +671,7 @@ public class MeetingService implements MessageListener {
logData.put("guestStatus", user.getGuestStatus());
logData.put("event", "user_joined_message");
logData.put("description", "User joined the meeting.");
logData.put("clientType", user.getClientType());
Gson gson = new Gson();
String logStr = gson.toJson(logData);
@ -761,8 +762,14 @@ public class MeetingService implements MessageListener {
} else {
if (message.userId.startsWith("v_")) {
// A dial-in user joined the meeting. Dial-in users by convention has userId that starts with "v_".
User vuser = new User(message.userId, message.userId,
message.name, "DIAL-IN-USER", "no-avatar-url", true, "VOICE-USER");
User vuser = new User(message.userId,
message.userId,
message.name,
"DIAL-IN-USER",
"no-avatar-url",
true,
GuestPolicy.ALLOW,
"DIAL-IN");
vuser.setVoiceJoined(true);
m.userJoined(vuser);
}

View File

@ -36,12 +36,17 @@ public class User {
private String guestStatus;
private Boolean listeningOnly = false;
private Boolean voiceJoined = false;
private String clientType;
private List<String> streams;
public User(String internalUserId,
String externalUserId, String fullname,
String role, String avatarURL,
Boolean guest, String guestStatus) {
String externalUserId,
String fullname,
String role,
String avatarURL,
Boolean guest,
String guestStatus,
String clientType) {
this.internalUserId = internalUserId;
this.externalUserId = externalUserId;
this.fullname = fullname;
@ -51,6 +56,7 @@ public class User {
this.guestStatus = guestStatus;
this.status = new ConcurrentHashMap<String, String>();
this.streams = Collections.synchronizedList(new ArrayList<String>());
this.clientType = clientType;
}
public String getInternalUserId() {
@ -158,4 +164,9 @@ public class User {
public void setVoiceJoined(Boolean voiceJoined) {
this.voiceJoined = voiceJoined;
}
public String getClientType() {
return this.clientType;
}
}

View File

@ -9,9 +9,18 @@ public class UserJoined implements IMessage {
public final String avatarURL;
public final Boolean guest;
public final String guestStatus;
public final String clientType;
public UserJoined(String meetingId, String userId, String externalUserId, String name, String role,
String avatarURL, Boolean guest, String guestStatus) {
public UserJoined(String meetingId,
String userId,
String externalUserId,
String name,
String role,
String avatarURL,
Boolean guest,
String guestStatus,
String clientType) {
this.meetingId = meetingId;
this.userId = userId;
this.externalUserId = externalUserId;
@ -20,5 +29,6 @@ public class UserJoined implements IMessage {
this.avatarURL = avatarURL;
this.guest = guest;
this.guestStatus = guestStatus;
this.clientType = clientType;
}
}

View File

@ -92,7 +92,8 @@ class OldMeetingMsgHdlrActor(val olgMsgGW: OldMessageReceivedGW)
def handleUserJoinedMeetingEvtMsg(msg: UserJoinedMeetingEvtMsg): Unit = {
olgMsgGW.handle(new UserJoined(msg.header.meetingId, msg.body.intId,
msg.body.extId, msg.body.name, msg.body.role, msg.body.avatar, msg.body.guest,
msg.body.guestStatus))
msg.body.guestStatus,
msg.body.clientType))
}

View File

@ -34,6 +34,7 @@
<isListeningOnly>${att.isListeningOnly()?c}</isListeningOnly>
<hasJoinedVoice>${att.isVoiceJoined()?c}</hasJoinedVoice>
<hasVideo>${att.hasVideo()?c}</hasVideo>
<clientType>${att.getClientType()}</clientType>
<#if meeting.getUserCustomData(att.getExternalUserId())??>
<#assign ucd = meeting.getUserCustomData(att.getExternalUserId())>
<customdata>

View File

@ -39,6 +39,7 @@
<isListeningOnly>${att.isListeningOnly()?c}</isListeningOnly>
<hasJoinedVoice>${att.isVoiceJoined()?c}</hasJoinedVoice>
<hasVideo>${att.hasVideo()?c}</hasVideo>
<clientType>${att.getClientType()}</clientType>
<#if meeting.getUserCustomData(att.getExternalUserId())??>
<#assign ucd = meetingDetail.meeting.getUserCustomData(att.getExternalUserId())>
<customdata>

View File

@ -6,7 +6,6 @@ bin/
bin-debug/
bin-release/
client/
locale/.tx/
bbbResources.properties.*
asdoc/
hs_err_pid*

View File

@ -1496,7 +1496,7 @@ mx|Panel {
textDecoration : underline;
}
.presentationUploadFileFormatHintBoxStyle, .audioBroswerHintBoxStyle, .lockSettingsHintBoxStyle, .breakoutTipBox, .pollingTipBox {
.presentationUploadFileFormatHintBoxStyle, .audioBroswerHintBoxStyle, .lockSettingsHintBoxStyle, .breakoutTipBox, .pollingTipBox, .screenshareSelectHintBoxStyle {
backgroundColor : #CDD4DB;
horizontalAlign : center;
paddingTop : 8;
@ -1666,15 +1666,6 @@ mx|ScrollBar {
//------------------------------
*/
.screenshareSelectHintBoxStyle {
horizontalAlign : center;
paddingTop : 7;
paddingBottom : 7;
paddingLeft : 5;
paddingRight : 5;
verticalAlign : middle;
}
.screenshareTypeTitle {
color : #363B43;
fontSize : 14;

View File

@ -1,3 +1,2 @@
.tx
ru/

View File

@ -0,0 +1,10 @@
[main]
host = https://www.transifex.com
[bigbluebutton.bbbresourcesproperties]
file_filter = <lang>/bbbResources.properties
minimum_perc = 0
source_file = en_US/bbbResources.properties
source_lang = en
type = UNICODEPROPERTIES

View File

@ -893,6 +893,8 @@ bbb.lockSettings.feature=Feature
bbb.lockSettings.locked=Locked
bbb.lockSettings.lockOnJoin=Lock On Join
bbb.users.meeting.closewarning.text = Meeting is closing in a minute.
bbb.users.breakout.breakoutRooms = Breakout Rooms
bbb.users.breakout.updateBreakoutRooms = Update Breakout Rooms
bbb.users.breakout.timerForRoom.toolTip = Time left for this breakout room

View File

@ -241,7 +241,7 @@ bbb.users.emojiStatus.speakFaster = Èske ou ta vle tanpri pale pi vit?
bbb.users.emojiStatus.speakSlower =
bbb.users.emojiStatus.beRightBack = Mwen pral dwa tounen
bbb.presentation.title = Prezantasyon
bbb.presentation.titleWithPres = Prezantasyon :(0)
bbb.presentation.titleWithPres = Prezantasyon: {0}
bbb.presentation.quickLink.label = Prezantasyon Window
bbb.presentation.fitToWidth.toolTip = Anfòm Prezantasyon pou Lajè
bbb.presentation.fitToPage.toolTip = Anfòm Prezantasyon pou Paj

File diff suppressed because one or more lines are too long

View File

@ -21,11 +21,11 @@ package org.bigbluebutton.core {
import flash.events.TimerEvent;
import flash.utils.Dictionary;
import flash.utils.Timer;
import mx.controls.Alert;
import mx.controls.Label;
import mx.managers.PopUpManager;
import org.bigbluebutton.util.i18n.ResourceUtil;
public final class TimerUtil {
@ -43,7 +43,14 @@ package org.bigbluebutton.core {
var formattedTime:String = (Math.floor(remainingSeconds / 60)) + ":" + (remainingSeconds % 60 >= 10 ? "" : "0") + (remainingSeconds % 60);
label.text = formattedTime;
if (remainingSeconds < 60 && showMinuteWarning && !minuteWarningShown) {
minuteAlert = Alert.show(ResourceUtil.getInstance().getString('bbb.users.breakout.closewarning.text'));
// Check the label which timer is firing and display message accordingly.
var warnText: String = 'bbb.users.breakout.closewarning.text';
if (label.id == "breakoutTimeLabel") {
warnText = 'bbb.users.breakout.closewarning.text';
} else if (label.id == 'timeRemaining') {
warnText = 'bbb.users.meeting.closewarning.text';
}
minuteAlert = Alert.show(ResourceUtil.getInstance().getString(warnText));
minuteWarningShown = true;
}
});

View File

@ -199,6 +199,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
if (!timeRemaining.visible && e.timeLeftInSec <= 1800) {
timeRemaining.visible = true;
}
// The label.id is used to determine message to display. So make sure
// you change in the TimerUtil if you change the label.
TimerUtil.setCountDownTimer(timeRemaining, e.timeLeftInSec, true);
}

View File

@ -147,8 +147,6 @@ package org.bigbluebutton.modules.chat.model
cm.lastTime = prevCM.time;
cm.differentLastSenderAndTime = differentLastSenderAndTime(cm.lastTime, cm.time,
cm.senderId, cm.lastSenderId);
cm.sameLastSender = sameLastSender(cm.senderId, cm.lastSenderId);
cm.isModerator = isModerator(cm.senderId);
}
return cm

View File

@ -18,6 +18,9 @@
*/
package org.bigbluebutton.modules.chat.model {
import org.as3commons.lang.StringUtils;
import org.bigbluebutton.common.Role;
import org.bigbluebutton.core.UsersUtil;
import org.bigbluebutton.core.model.users.User2x;
import org.bigbluebutton.util.i18n.ResourceUtil;
public class ChatMessage {
@ -31,14 +34,30 @@ package org.bigbluebutton.modules.chat.model {
[Bindable] public var lastTime:String;
[Bindable] public var text:String;
[Bindable] public var differentLastSenderAndTime:Boolean;
[Bindable] public var sameLastSender:Boolean;
[Bindable] public var isModerator:Boolean;
// Stores the time (millis) when the sender sent the message.
public var fromTime:Number;
// Stores the timezone offset (minutes) of the sender.
public var fromTimezoneOffset:Number;
/*
// Stores what we display to the user. The converted fromTime and fromTimezoneOffset to local time.
[Bindable] public var senderTime:String;
*/
public function sameLastTime():Boolean {
return lastTime == time;
}
public function sameLastSender():Boolean {
return StringUtils.trimToEmpty(senderId) == StringUtils.trimToEmpty(lastSenderId);
}
public function isModerator():Boolean {
var user:User2x = UsersUtil.getUser(senderId);
return user && user.role == Role.MODERATOR
}
public function toString() : String {
var result:String;
var accName:String = (StringUtils.isBlank(name) ? ResourceUtil.getInstance().getString("bbb.chat.chatMessage.systemMessage") : name);
@ -64,4 +83,4 @@ package org.bigbluebutton.modules.chat.model {
return str.replace(pattern, "");
}
}
}
}

View File

@ -19,9 +19,12 @@
package org.bigbluebutton.modules.chat.views
{
import flash.display.Sprite;
import flash.events.Event;
import mx.controls.List;
import mx.controls.listClasses.IListItemRenderer;
import mx.events.CollectionEvent;
import mx.events.CollectionEventKind;
import org.as3commons.logging.api.ILogger;
import org.as3commons.logging.api.getClassLogger;
@ -66,5 +69,19 @@ package org.bigbluebutton.modules.chat.views
public function get verticalScrollAtMax():Boolean {
return verticalScrollPosition == maxVerticalScrollPosition;
}
override protected function collectionChangeHandler(event:Event):void {
var previousVScroll:Number = verticalScrollPosition;
super.collectionChangeHandler(event);
if (event is CollectionEvent) {
var cEvent:CollectionEvent = CollectionEvent(event);
if (cEvent.kind == CollectionEventKind.REFRESH) {
verticalScrollPosition = previousVScroll;
}
}
}
}
}

View File

@ -53,18 +53,30 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
protected function dataChangeHandler(event:FlexEvent):void {
// If you remove this some of the chat messages will fail to render
validateNow();
if (data != null) {
var sameSender:Boolean = data.sameLastSender();
var sameTime:Boolean = data.sameLastTime();
var isMod:Boolean = data.isModerator();
hbHeader.visible = hbHeader.includeInLayout = !sameSender || !sameTime;
lblName.visible = !sameSender;
lblName.styleName = (isMod ? 'chatMessageHeaderModerator' : '');
moderatorIcon.visible = lblName.visible && isMod;
lblTime.visible = !sameSender || !sameTime;
}
}
]]>
</fx:Script>
<mx:Canvas width="100%" id="hbHeader" styleName="chatMessageHeader" verticalScrollPolicy="off" horizontalScrollPolicy="off"
visible="{lblName.visible || lblTime.visible}" includeInLayout="{lblName.visible || lblTime.visible}">
<mx:Label id="lblName" text="{data.name}" visible="{!data.sameLastSender}"
verticalCenter="0" textAlign="left" left="0" maxWidth="{this.width - lblTime.width - moderatorIcon.width - 22}"
styleName="{data.isModerator ? 'chatMessageHeaderModerator' : ''}"/>
<mx:Image id="moderatorIcon" visible="{lblName.visible &amp;&amp; data.isModerator}"
visible="false" includeInLayout="false">
<mx:Label id="lblName" text="{data.name}" visible="false"
verticalCenter="0" textAlign="left" left="0" maxWidth="{this.width - lblTime.width - moderatorIcon.width - 22}" />
<mx:Image id="moderatorIcon" visible="false"
source="{getStyle('moderatorIcon')}" x="{lblName.width + 4}" verticalCenter="0"/>
<mx:Text id="lblTime" visible="{data.differentLastSenderAndTime}" htmlText="{data.time}" textAlign="right"
<mx:Text id="lblTime" visible="true" htmlText="{data.time}" textAlign="right"
verticalCenter="0"
right="4" />
</mx:Canvas>

View File

@ -89,7 +89,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
private static const LOGGER:ILogger = getClassLogger(ChatTab);
public var chatWithUserID:String;
public var chatWithUsername:String
public var chatId: String = null;
@ -259,9 +259,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
private function handleUserLeftEvent(event:UserLeftEvent):void {
var gc: GroupChat = LiveMeeting.inst().chats.findChatWithUser(event.userID);
if (gc != null && gc.id == chatId) {
displayUserHasLeftMessage();
addMessageAndScrollToEnd(createUserHasLeftMessage(), event.userID);
txtMsgArea.enabled = false;
scrollToEndOfMessage(event.userID);
}
}
@ -274,14 +273,13 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
private function handleUserJoinedEvent(event:UserJoinedEvent):void {
var gc: GroupChat = LiveMeeting.inst().chats.findChatWithUser(event.userID);
if (gc != null && gc.id == chatId) {
// Handle user joining so that the user can start to talk if the person rejoins
displayUserHasJoinedMessage();
addMessageAndScrollToEnd(createUserHasJoinedMessage(), event.userID);
txtMsgArea.enabled = true;
scrollToEndOfMessage(event.userID);
}
}
private function displayUserHasLeftMessage():void {
private var SPACE:String = " ";
private function createUserHasLeftMessage():ChatMessageVO {
var msg:ChatMessageVO = new ChatMessageVO();
msg.fromUserId = ChatModel.USER_LEFT_MSG;
msg.fromUsername = ChatModel.SPACE;
@ -289,13 +287,10 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
msg.fromTime = new Date().getTime();
msg.message = "<b><i>"+ResourceUtil.getInstance().getString('bbb.chat.private.userLeft')+"</b></i>";
var groupChat: GroupChat = LiveMeeting.inst().chats.getGroupChat(chatId);
if (groupChat != null) {
chatMessages.newChatMessage(msg);
}
return msg;
}
private function displayUserHasJoinedMessage():void {
private function createUserHasJoinedMessage():ChatMessageVO {
var msg:ChatMessageVO = new ChatMessageVO();
msg.fromUserId = ChatModel.USER_JOINED_MSG;
msg.fromUsername = ChatModel.SPACE;
@ -303,10 +298,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
msg.fromTime = new Date().getTime();
msg.message = "<b><i>"+ResourceUtil.getInstance().getString('bbb.chat.private.userJoined')+"</b></i>";
var groupChat: GroupChat = LiveMeeting.inst().chats.getGroupChat(chatId);
if (groupChat != null) {
chatMessages.newChatMessage(msg);
}
return msg;
}
public function focusToTextMessageArea():void {
@ -316,8 +308,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
private function handlePublicChatMessageEvent(event:PublicChatMessageEvent):void {
if (chatId == event.chatId && chatMessages != null) {
chatMessages.newChatMessage(event.msg);
scrollToEndOfMessage(event.msg.fromUserId);
addMessageAndScrollToEnd(event.msg, event.msg.fromUserId);
}
}
@ -336,23 +327,29 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
private function handlePrivateChatMessageEvent(event:PrivateChatMessageEvent):void {
if (chatId == event.chatId) {
displayChatHistory();
scrollToEndOfMessage(event.senderId);
}
// addMessageAndScrollToEnd(event.msg, event.msg.fromUserId);
}
}
public function handleFirstPrivateMessage(event:PrivateChatMessageEvent):void {
public function handleFirstPrivateMessage(event:PrivateChatMessageEvent):void {
handlePrivateChatMessageEvent(event);
}
public function scrollToEndOfMessage(userID:String):void {
public function addMessageAndScrollToEnd(message:ChatMessageVO, userId:String):void {
// Have to check if the vScroll is max before adding the new message
var vScrollMax:Boolean = chatMessagesList.verticalScrollAtMax;
chatMessages.newChatMessage(message);
scrollToEndOfMessage(userId, vScrollMax);
}
public function scrollToEndOfMessage(userID:String, precheckedVScroll:Boolean=false):void {
/**
* Trigger to force the scrollbar to show the last message.
*/
// @todo : scromm if
// 1 - I am the send of the last message
// 2 - If the scroll bar is at the bottom most
if (UsersUtil.isMe(userID) || (chatMessagesList.verticalScrollAtMax)) {
if (UsersUtil.isMe(userID) || precheckedVScroll || (chatMessagesList.verticalScrollAtMax)) {
if (scrollTimer != null) scrollTimer.start();
} else if (!scrollTimer.running) {
unreadMessagesBar.visible = unreadMessagesBar.includeInLayout = true;

View File

@ -122,6 +122,15 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
text="{ResourceUtil.getInstance().getString('bbb.screenshareSelection.title')}"
styleName="titleWindowStyle"
maxWidth="{this.width - 40}" />
<mx:Box id="webrtcProblemHintBox"
width="100%"
verticalScrollPolicy="off" horizontalScrollPolicy="off"
visible="false" includeInLayout="false"
styleName="screenshareSelectHintBoxStyle">
<mx:Text id="webrtcProblemHintLbl" width="100%" textAlign="center" link="onHelpLinkClicked(event)"/>
</mx:Box>
<mx:HBox width="100%" height="100%" styleName="screenshareSelectionsStyle" verticalAlign="middle" horizontalAlign="center">
<mx:VBox id="vboxWebrtc" horizontalAlign="center" verticalAlign="middle" paddingLeft="20" paddingRight="20">
<mx:Button id="btnWebrtc" buttonMode="true" styleName="btnScreenshareSelectStyle" width="140" height="140"
@ -142,13 +151,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
styleName="screenshareTypeTitle" />
</mx:VBox>
</mx:HBox>
<mx:Box id="webrtcProblemHintBox"
width="100%"
verticalScrollPolicy="off" horizontalScrollPolicy="off"
visible="false" includeInLayout="false"
styleName="screenshareSelectHintBoxStyle">
<mx:Text id="webrtcProblemHintLbl" width="100%" textAlign="center" styleName="screenshareSelectHintTextStyle" link="onHelpLinkClicked(event)"/>
</mx:Box>
</mx:VBox>
<mx:Button id="closeButton" click="onCancelClicked()" styleName="titleWindowCloseButton"
toolTip="{ResourceUtil.getInstance().getString('bbb.screenshareSelection.cancel')}"

View File

@ -42,7 +42,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
import org.bigbluebutton.main.events.ShortcutEvent;
import org.bigbluebutton.main.views.MainToolbar;
import org.bigbluebutton.modules.screenshare.events.RequestToStartSharing;
import org.bigbluebutton.modules.screenshare.events.RequestToStopSharing;
import org.bigbluebutton.modules.screenshare.events.ScreenshareSelectionWindowEvent;
import org.bigbluebutton.modules.screenshare.events.ShareWindowEvent;

View File

@ -417,7 +417,8 @@ package org.bigbluebutton.modules.users.services
var locked: Boolean = user.locked as Boolean;
var presenter: Boolean = user.presenter as Boolean;
var avatar: String = user.avatar as String;
// var clientType: String = user.clientType as String;
var user2x: User2x = new User2x();
user2x.intId = intId;
user2x.extId = extId;
@ -461,6 +462,7 @@ package org.bigbluebutton.modules.users.services
LiveMeeting.inst().me.locked = locked;
UsersUtil.applyLockSettings();
}
}
private function handleGetVoiceUsersMeetingRespMsg(msg:Object):void {

View File

@ -82,7 +82,7 @@ package org.bigbluebutton.modules.users.services
public function joinMeeting(): void {
var message:Object = {
header: {name: "UserJoinMeetingReqMsg", meetingId: UsersUtil.getInternalMeetingID(), userId: UsersUtil.getMyUserID()},
body: {userId: UsersUtil.getMyUserID(), authToken: LiveMeeting.inst().me.authToken}
body: {userId: UsersUtil.getMyUserID(), authToken: LiveMeeting.inst().me.authToken, clientType: "FLASH"}
};
var _nc:ConnectionManager = BBB.initConnectionManager();
@ -103,7 +103,7 @@ package org.bigbluebutton.modules.users.services
var message:Object = {
header: {name: "UserJoinMeetingAfterReconnectReqMsg", meetingId: UsersUtil.getInternalMeetingID(), userId: UsersUtil.getMyUserID()},
body: {userId: UsersUtil.getMyUserID(), authToken: LiveMeeting.inst().me.authToken}
body: {userId: UsersUtil.getMyUserID(), authToken: LiveMeeting.inst().me.authToken, clientType: "FLASH"}
};
var _nc:ConnectionManager = BBB.initConnectionManager();

View File

@ -494,12 +494,16 @@ $Id: $
}
private function handleRemainingTimeUpdate(event:BreakoutRoomEvent):void {
// The label.id is used to determine message to display. So make sure
// you change in the TimerUtil if you change the label.
TimerUtil.setCountDownTimer(breakoutTimeLabel, event.durationInMinutes);
}
private function breakoutRoomsListChangeListener(event:CollectionEvent):void {
if (breakoutRoomsList.length == 0) {
breakoutTimeLabel.text = "...";
// The label.id is used to determine message to display. So make sure
// you change in the TimerUtil if you change the label.
TimerUtil.stopTimer(breakoutTimeLabel.id);
// All breakout rooms were close we don't need to display the join URL alert anymore
removeJoinWindow();

View File

@ -707,9 +707,12 @@ if [ $SECRET ]; then
need_root
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\"$SECRET\"|g" /usr/local/bigbluebutton/bbb-webhooks/config_local.coffee
fi
if [ -f /usr/local/bigbluebutton/bbb-webhooks/config_local.coffee ]; then
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/config_local.js ]; then
sed -i "s|\(^[ \t]*config.bbb.sharedSecret[ =]*\).*|\1\"$SECRET\"|g" /usr/local/bigbluebutton/bbb-webhooks/config_local.js
fi
if [ -f /usr/local/bigbluebutton/bbb-webhooks/extra/post_catcher.js ]; then
sed -i "s|\(^[ \t]*var shared_secret[ =]*\)[^;]*|\1\"$SECRET\"|g" /usr/local/bigbluebutton/bbb-webhooks/extra/post_catcher.js
@ -891,26 +894,34 @@ check_configuration() {
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/.*server_name[ ]*//;s/;//;p}' | cut -d' ' -f1)
if [ -f /usr/lib/systemd/system/bbb-webhooks.service ]; then
WEBHOOKS_SECRET=$(cat /usr/local/bigbluebutton/bbb-webhooks/config_local.coffee | grep '^[ \t]*config.bbb.sharedSecret[ =]*' | cut -d '"' -f2)
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
WEBHOOKS_PROXY_PORT=$(cat /etc/bigbluebutton/nginx/webhooks.nginx | grep -v '#' | grep '^[ \t]*proxy_pass[ \t]*' | sed 's|.*http[s]\?://[^:]*:\([^;]*\);.*|\1|g')
WEBHOOKS_APP_PORT=$(cat /usr/local/bigbluebutton/bbb-webhooks/config_local.coffee | grep '^[ \t]*config.server.port[ =]*' | cut -d '=' -f2 | xargs)
if [ -f /usr/lib/systemd/system/bbb-webhooks.service ]; then
if [ -f /usr/local/bigbluebutton/bbb-webhooks/config_local.js ]; then
WEBHOOKS_SECRET=$(cat /usr/local/bigbluebutton/bbb-webhooks/config_local.js | grep '^[ \t]*config.bbb.sharedSecret[ =]*' | cut -d '"' -f2)
WEBHOOKS_CONF=/usr/local/bigbluebutton/bbb-webhooks/config_local.js
fi
if [ -f /usr/local/bigbluebutton/bbb-webhooks/config_local.coffee ]; then
WEBHOOKS_SECRET=$(cat /usr/local/bigbluebutton/bbb-webhooks/config_local.coffee | grep '^[ \t]*config.bbb.sharedSecret[ =]*' | cut -d '"' -f2)
WEBHOOKS_CONF=/usr/local/bigbluebutton/bbb-webhooks/config_local.coffee
fi
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 "# $WEBHOOKS_CONF = $WEBHOOKS_SECRET"
echo
fi
WEBHOOKS_PROXY_PORT=$(cat /etc/bigbluebutton/nginx/webhooks.nginx | grep -v '#' | grep '^[ \t]*proxy_pass[ \t]*' | sed 's|.*http[s]\?://[^:]*:\([^;]*\);.*|\1|g')
WEBHOOKS_APP_PORT=$(cat $WEBHOOKS_CONF | grep config.server.port | sed "s/.*config.server.port[ =\"]*//g" | sed 's/[;\"]*//g')
if [ "$WEBHOOKS_PROXY_PORT" != "$WEBHOOKS_APP_PORT" ]; then
echo "# Warning: Webhooks port mismatch: "
echo "# /etc/bigbluebutton/nginx/webhooks.nginx = $WEBHOOKS_PROXY_PORT"
echo "# $WEBHOOKS_CONF = $WEBHOOKS_APP_PORT"
echo
fi
fi
if [ "$WEBHOOKS_PROXY_PORT" != "$WEBHOOKS_APP_PORT" ]; then
echo "# Warning: Webhooks port mismatch: "
echo "# /etc/bigbluebutton/nginx/webhooks.nginx = $WEBHOOKS_PROXY_PORT"
echo "# /usr/local/bigbluebutton/bbb-webhooks/config_local.coffee = $WEBHOOKS_APP_PORT"
echo
fi
fi
if [ -f ${SERVLET_DIR}/lti/WEB-INF/classes/lti-config.properties ]; then
LTI_SECRET=$(cat ${SERVLET_DIR}/lti/WEB-INF/classes/lti-config.properties | grep -v '#' | tr -d '\r' | sed -n '/^bigbluebuttonSalt/{s/.*=//;p}')
@ -1185,22 +1196,27 @@ check_state() {
#
# Check if ffmpeg is installed, and whether it is a supported version
#
FFMPEG_VERSION=$(ffmpeg -version 2>/dev/null | grep ffmpeg | cut -d ' ' -f3)
FFMPEG_VERSION=$(ffmpeg -version 2>/dev/null | grep ffmpeg | cut -d ' ' -f3 | sed 's/--.*//g' | tr -d '\n')
case "$FFMPEG_VERSION" in
2.8.*)
# This is the current supported version; OK.
;;
'')
echo "# Warning: No ffmpeg version was found on the system"
echo "# Recording processing will not function"
echo
;;
*)
echo "# Warning: The installed ffmpeg version '${FFMPEG_VERSION}' is not supported"
echo "# Recording processing may not function correctly"
echo
;;
esac
4.0.*)
# This is the current supported version; OK.
;;
'')
echo "# Warning: No ffmpeg version was found on the system"
echo "# Recording processing will not function"
echo
;;
*)
echo "# Warning: The installed ffmpeg version '${FFMPEG_VERSION}' is not recommended."
echo "# Recommend you update to the 4.0.x version of ffmpeg. To upgrade, do the following"
echo "#"
echo "# sudo add-apt-repository ppa:jonathonf/ffmpeg-4"
echo "# sudo apt-get update"
echo "# sudo apt-get dist-upgrade"
echo "#"
echo
;;
esac
if [ -f /usr/share/red5/log/sip.log ]; then
@ -1590,6 +1606,7 @@ if [ $CHECK ]; then
echo
echo "/usr/local/bigbluebutton/core/scripts/bigbluebutton.yml (record and playback)"
echo " playback host: $PLAYBACK_IP"
echo " ffmpeg: $(ffmpeg -version 2>/dev/null | grep ffmpeg | cut -d ' ' -f3 | sed 's/--.*//g' | tr -d '\n')"
fi
if [ -f /usr/local/bigbluebutton/bbb-webrtc-sfu/config/default.yml ]; then
@ -1968,6 +1985,10 @@ if [ $CLEAN ]; then
rm -f /var/log/bbb-transcode-akka/*
fi
if [ -d /var/log/bbb-webrtc-sfu ]; then
rm -f /var/log/bbb-webrtc-sfu/*
fi
start_bigbluebutton
check_state
fi

View File

@ -262,7 +262,7 @@
<div class="row">
<div class="span twelve center">
<p>Copyright &copy; 2018 BigBlueButton Inc.<br>
<small>Version <a href="http://docs.bigbluebutton.org/">2.0-beta</a></small>
<small>Version <a href="http://docs.bigbluebutton.org/">2.0-RC1</a></small>
</p>
</div>
</div>

View File

@ -288,7 +288,7 @@
<div class="row">
<div class="span twelve center">
<p>Copyright &copy; 2018 BigBlueButton Inc.<br>
<small>Version <a href="http://docs.bigbluebutton.org/">2.0-beta</a></small>
<small>Version <a href="http://docs.bigbluebutton.org/">2.0-RC1</a></small>
</p>
</div>
</div>

View File

@ -17,6 +17,7 @@ export default function userJoin(meetingId, userId, authToken) {
const payload = {
userId,
authToken,
clientType: 'HTML5',
};
Logger.info(`User='${userId}' is joining meeting='${meetingId}' authToken='${authToken}' pt2`);

View File

@ -23,7 +23,7 @@ export default function userLeaving(credentials, userId, connectionId) {
const User = Users.findOne(selector);
if (!User) {
Logger.info(`Skipping userLeaving. Could not find ${userId} in ${meetingId}`);
return Logger.info(`Skipping userLeaving. Could not find ${userId} in ${meetingId}`);
}
// If the current user connection is not the same that triggered the leave we skip

View File

@ -29,6 +29,7 @@ export default function addUser(meetingId, user) {
presenter: Boolean,
locked: Boolean,
avatar: String,
clientType: String,
});
const userId = user.intId;

View File

@ -3,8 +3,6 @@ import Users from '/imports/api/users';
import Logger from '/imports/startup/server/logger';
import ejectUserFromVoice from '/imports/api/voice-users/server/methods/ejectUserFromVoice';
const CLIENT_TYPE_HTML = 'HTML5';
const clearAllSessions = (sessionUserId) => {
const serverSessions = Meteor.server.sessions;
Object.keys(serverSessions)
@ -44,7 +42,7 @@ export default function removeUser(meetingId, userId) {
meetingId,
}, userId);
return Logger.info(`Removed ${CLIENT_TYPE_HTML} user id=${userId} meeting=${meetingId}`);
return Logger.info(`Removed user id=${userId} meeting=${meetingId}`);
};
return Users.update(selector, modifier, cb);

View File

@ -38,7 +38,7 @@ export default function handleJoinVoiceUser({ body }, meetingId) {
const USER_CONFIG = Meteor.settings.public.user;
const ROLE_VIEWER = USER_CONFIG.role_viewer;
const modifier = {
const modifier = { // web (Users) representation of dial-in user
$set: {
meetingId,
connectionStatus: 'online',
@ -56,6 +56,7 @@ export default function handleJoinVoiceUser({ body }, meetingId) {
presenter: false,
locked: false, // TODO
avatar: '',
clientType: 'dial-in-user',
},
};

View File

@ -5,7 +5,7 @@ import { defineMessages, injectIntl, intlShape } from 'react-intl';
import Modal from 'react-modal';
import cx from 'classnames';
import Resizable from 're-resizable';
import browser from 'browser-detect';
import ToastContainer from '../toast/container';
import ModalContainer from '../modal/container';
import NotificationsBarContainer from '../notifications-bar/container';
@ -79,6 +79,11 @@ class App extends Component {
document.getElementsByTagName('html')[0].lang = locale;
document.getElementsByTagName('html')[0].style.fontSize = this.props.fontSize;
const BROWSER_RESULTS = browser();
const body = document.getElementsByTagName('body')[0];
body.classList.add(`browser-${BROWSER_RESULTS.name}`);
body.classList.add(`os-${BROWSER_RESULTS.os.split(' ').shift().toLowerCase()}`);
this.handleWindowResize();
window.addEventListener('resize', this.handleWindowResize, false);
}

View File

@ -1,9 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { defineMessages, intlShape, injectIntl } from 'react-intl';
import Button from '/imports/ui/components/button/component';
import { styles } from './styles';
import cx from 'classnames';
const intlMessages = defineMessages({
joinAudio: {
@ -29,7 +29,7 @@ const propTypes = {
handleJoinAudio: PropTypes.func.isRequired,
handleLeaveAudio: PropTypes.func.isRequired,
disable: PropTypes.bool.isRequired,
unmute: PropTypes.bool.isRequired,
unmute: PropTypes.bool,
mute: PropTypes.bool.isRequired,
join: PropTypes.bool.isRequired,
intl: intlShape.isRequired,
@ -38,6 +38,7 @@ const propTypes = {
const defaultProps = {
glow: false,
unmute: false,
};
const SHORTCUTS_CONFIG = Meteor.settings.public.app.shortcuts;

View File

@ -2,7 +2,6 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ModalBase from '/imports/ui/components/modal/base/component';
import Button from '/imports/ui/components/button/component';
import deviceInfo from '/imports/utils/deviceInfo';
import { defineMessages, injectIntl, intlShape } from 'react-intl';
import { styles } from './styles';
import PermissionsOverlay from '../permissions-overlay/component';
@ -10,7 +9,6 @@ import AudioSettings from '../audio-settings/component';
import EchoTest from '../echo-test/component';
import Help from '../help/component';
const propTypes = {
intl: intlShape.isRequired,
closeModal: PropTypes.func.isRequired,
@ -307,11 +305,12 @@ class AudioModal extends Component {
const {
isEchoTest,
intl,
isIOSChrome,
} = this.props;
const { content } = this.state;
if (deviceInfo.osType().isIOSChrome) {
if (isIOSChrome) {
return (
<div>
<div className={styles.warning}>!</div>
@ -357,7 +356,6 @@ class AudioModal extends Component {
handleBack={this.handleGoToAudioOptions}
handleRetry={this.handleRetryGoToEchoTest}
joinEchoTest={this.joinEchoTest}
exitAudio={this.exitAudio}
changeInputDevice={this.changeInputDevice}
changeOutputDevice={this.changeOutputDevice}
isConnecting={isConnecting}
@ -381,6 +379,7 @@ class AudioModal extends Component {
const {
intl,
showPermissionsOvelay,
isIOSChrome,
} = this.props;
const { content } = this.state;
@ -399,16 +398,13 @@ class AudioModal extends Component {
data-test="audioModalHeader"
className={styles.header}
>{
(!deviceInfo.osType().isIOSChrome ?
isIOSChrome ? null :
<h3 className={styles.title}>
{content ?
this.contents[content].title :
intl.formatMessage(intlMessages.audioChoiceLabel)}
</h3> : <h3 className={styles.title} />
)
</h3>
}
<Button
data-test="modalBaseCloseButton"
className={styles.closeBtn}

View File

@ -1,6 +1,7 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import { withModalMounter } from '/imports/ui/components/modal/service';
import browser from 'browser-detect';
import AudioModal from './component';
import Service from '../service';
@ -24,7 +25,7 @@ export default withModalMounter(withTracker(({ mountModal }) =>
}
reject(() => {
Service.exitAudio();
})
});
});
return call.then(() => {
@ -55,4 +56,5 @@ export default withModalMounter(withTracker(({ mountModal }) =>
joinFullAudioImmediately: !listenOnlyMode && skipCheck,
joinFullAudioEchoTest: !listenOnlyMode && !skipCheck,
forceListenOnlyAttendee: listenOnlyMode && forceListenOnly && !Service.isUserModerator(),
isIOSChrome: browser().name === 'crios',
}))(AudioModalContainer));

View File

@ -21,23 +21,23 @@ const intlMessages = defineMessages({
},
genericError: {
id: 'app.audioManager.genericError',
description: 'Generic error messsage',
description: 'Generic error message',
},
connectionError: {
id: 'app.audioManager.connectionError',
description: 'Connection error messsage',
description: 'Connection error message',
},
requestTimeout: {
id: 'app.audioManager.requestTimeout',
description: 'Request timeout error messsage',
description: 'Request timeout error message',
},
invalidTarget: {
id: 'app.audioManager.invalidTarget',
description: 'Invalid target error messsage',
description: 'Invalid target error message',
},
mediaError: {
id: 'app.audioManager.mediaError',
description: 'Media error messsage',
description: 'Media error message',
},
});
@ -71,6 +71,12 @@ export default withModalMounter(injectIntl(withTracker(({ mountModal, intl }) =>
Breakouts.find().observeChanges({
removed() {
// if the user joined a breakout room, the main room's audio was
// programmatically dropped to avoid interference. On breakout end,
// offer to rejoin main room audio only if the user is not in audio already
if (Service.isUsingAudio()) {
return;
}
setTimeout(() => openAudioModal(), 0);
},
});

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React from 'react';
import { injectIntl, intlShape, defineMessages } from 'react-intl';
import { styles } from './styles';
@ -17,52 +17,16 @@ const intlMessages = defineMessages({
},
});
class PermissionsOverlay extends Component {
constructor(props) {
super(props);
const broswerStyles = {
Chrome: {
top: '145px',
left: '380px',
},
Firefox: {
top: '210px',
left: '605px',
},
Safari: {
top: '100px',
left: '100px',
},
};
const browser = window.bowser.name;
this.state = {
styles: {
top: broswerStyles[browser].top,
left: broswerStyles[browser].left,
},
};
}
render() {
const {
intl,
} = this.props;
return (
<div className={styles.overlay}>
<div style={this.state.styles} className={styles.hint}>
{ intl.formatMessage(intlMessages.title) }
<small>
{ intl.formatMessage(intlMessages.hint) }
</small>
</div>
</div>
);
}
}
const PermissionsOverlay = ({ intl }) => (
<div className={styles.overlay}>
<div className={styles.hint}>
{ intl.formatMessage(intlMessages.title) }
<small>
{ intl.formatMessage(intlMessages.hint) }
</small>
</div>
</div>
);
PermissionsOverlay.propTypes = propTypes;

View File

@ -1,3 +1,44 @@
@mixin arrowIconStyle() {
&:after {
top: -50px;
left: -20px;
font-size: 20px;
-webkit-animation: bounce 2s infinite;
animation: bounce 2s infinite;
display: block;
font-family: 'bbb-icons';
content: "\E906";
position: relative;
}
:global(.browser-edge) &:after {
top: -50px;
left: -15.5em;
font-size: 20px;
-webkit-animation: bounceRotate 2s infinite;
animation: bounceRotate 2s infinite;
}
}
@mixin positionHint() {
:global(.browser-edge) & {
left: 50%;
bottom: 10%;
}
:global(.browser-firefox) & {
top: 210px;
left: 605px;
}
:global(.browser-chrome) & {
top: 145px;
left: 380px;
}
:global(.browser-safari) & {
top: 100px;
left: 100px;
}
}
.overlay {
position: fixed;
z-index: 1002;
@ -10,6 +51,8 @@
}
.hint {
@include positionHint();
position: absolute;
color: #fff;
font-size: 16px;
@ -25,17 +68,7 @@
opacity: .6;
}
&:after {
display: block;
font-family: 'bbb-icons';
content: "\E906";
position: relative;
top: -50px;
left: -20px;
font-size: 20px;
-webkit-animation: bounce 2s infinite;
animation: bounce 2s infinite;
}
@include arrowIconStyle();
}
@-webkit-keyframes bounce {
@ -80,6 +113,21 @@
}
}
@keyframes bounceRotate {
0%, 20%, 50%, 80%, 100% {
-ms-transform: translateY(0) rotate(180deg);
transform: translateY(0) rotate(180deg);
}
40% {
-ms-transform: translateY(10px) rotate(180deg);
transform: translateY(10px) rotate(180deg);
}
60% {
-ms-transform: translateY(5px) rotate(180deg);
transform: translateY(5px) rotate(180deg);
}
}
@keyframes fade-in {
0% {
opacity: 0;

View File

@ -2,7 +2,6 @@ import Users from '/imports/api/users';
import Auth from '/imports/ui/services/auth';
import AudioManager from '/imports/ui/services/audio-manager';
import Meetings from '/imports/api/meetings';
import VoiceUsers from '/imports/api/voice-users';
const init = (messages) => {
AudioManager.setAudioMessages(messages);
@ -30,9 +29,6 @@ const init = (messages) => {
AudioManager.init(userData);
};
const isVoiceUserTalking = () =>
VoiceUsers.findOne({ intId: Auth.userID }).talking;
export default {
init,
exitAudio: () => AudioManager.exitAudio(),
@ -44,8 +40,9 @@ export default {
changeInputDevice: inputDeviceId => AudioManager.changeInputDevice(inputDeviceId),
changeOutputDevice: outputDeviceId => AudioManager.changeOutputDevice(outputDeviceId),
isConnected: () => AudioManager.isConnected,
isTalking: () => isVoiceUserTalking(),
isTalking: () => AudioManager.isTalking,
isHangingUp: () => AudioManager.isHangingUp,
isUsingAudio: () => AudioManager.isUsingAudio(),
isWaitingPermissions: () => AudioManager.isWaitingPermissions,
isMuted: () => AudioManager.isMuted,
isConnecting: () => AudioManager.isConnecting,

View File

@ -83,19 +83,9 @@ const setRetrySeconds = (sec = 0) => {
}
};
const changeDocumentTitle = (sec) => {
if (sec >= 0) {
const affix = `(${humanizeSeconds(sec)}`;
const splitTitle = document.title.split(') ');
const title = splitTitle[1] || splitTitle[0];
document.title = [affix, title].join(') ');
}
};
const setTimeRemaining = (sec = 0) => {
if (sec !== timeRemaining) {
timeRemaining = sec;
changeDocumentTitle(sec);
timeRemainingDep.changed();
}
};

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import browser from 'browser-detect';
import Modal from '/imports/ui/components/modal/simple/component';
import deviceInfo from '/imports/utils/deviceInfo';
import _ from 'lodash';
import { styles } from './styles';
@ -77,23 +77,24 @@ const SHORTCUTS_CONFIG = Meteor.settings.public.app.shortcuts;
class ShortcutHelpComponent extends Component {
render() {
const { intl } = this.props;
const { isWindows, isLinux, isMac } = deviceInfo.osType();
const { isFirefox, isChrome, isIE } = deviceInfo.browserType();
const shortcuts = Object.values(SHORTCUTS_CONFIG);
const { name } = browser();
let accessMod = null;
if (isMac) {
accessMod = 'Control + Alt';
}
if (isWindows) {
accessMod = isIE ? 'Alt' : accessMod;
}
if (isWindows || isLinux) {
accessMod = isFirefox ? 'Alt + Shift' : accessMod;
accessMod = isChrome ? 'Alt' : accessMod;
switch (name) {
case 'chrome':
case 'edge':
accessMod = 'Alt';
break;
case 'firefox':
accessMod = 'Alt + Shift';
break;
case 'safari':
case 'crios':
case 'fxios':
accessMod = 'Control + Alt';
break;
}
return (

View File

@ -10,6 +10,7 @@ import Meetings from '/imports/api/meetings';
import Icon from '../icon/component';
import { styles } from './styles';
import AudioService from '../audio/service';
const intlMessages = defineMessages({
@ -41,7 +42,9 @@ class ToastContainer extends React.Component {
export default injectIntl(injectNotify(withTracker(({ notify, intl }) => {
Breakouts.find().observeChanges({
removed() {
notify(intl.formatMessage(intlMessages.toastBreakoutRoomEnded), 'info', 'rooms');
if (!AudioService.isUsingAudio()) {
notify(intl.formatMessage(intlMessages.toastBreakoutRoomEnded), 'info', 'rooms');
}
},
});

View File

@ -1,12 +1,12 @@
@import "/imports/ui/stylesheets/variables/palette";
@import "/imports/ui/stylesheets/variables/general";
@import "/imports/ui/stylesheets/mixins/_indicators";
/* Variables
* ==========
*/
$user-avatar-border: $color-gray-light;
$user-avatar-text: $color-white;
$user-indicators-offset: -5px;
$user-indicator-presenter-bg: $color-primary;
$user-indicator-voice-bg: $color-success;
$user-indicator-muted-bg: $color-danger;
$user-list-bg: $color-off-white;
@ -72,24 +72,14 @@ $user-color: currentColor; //picks the current color reference in the class
.presenter {
&:before {
content: "\00a0\e90b\00a0";
opacity: 1;
top: $user-indicators-offset;
left: $user-indicators-offset;
bottom: auto;
right: auto;
border-radius: 5px;
background-color: $user-indicator-presenter-bg;
padding: .425rem;
}
@include presenterIndicator();
}
.voice {
&:after {
content: "\00a0\e931\00a0";
background-color: $user-indicator-voice-bg;
opacity: 1;
width: 1.375rem;
height: 1.375rem;
top: 1.375rem;
left: 1.375rem;
}
@ -99,18 +89,19 @@ $user-color: currentColor; //picks the current color reference in the class
&:after {
content: "\00a0\e932\00a0";
background-color: $user-indicator-muted-bg;
opacity: 1;
}
}
.listenOnly {
&:after {
content: "\00a0\e90c\00a0";
opacity: 1;
}
}
.listenOnly, .muted, .voice {
@include indicatorStyles();
}
.content {
color: $user-avatar-text;
top: 50%;

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import RenderInBrowser from 'react-render-in-browser';
import AnnotationHelpers from '../helpers';
const DRAW_END = Meteor.settings.public.whiteboard.annotations.status.end;
@ -138,20 +139,19 @@ export default class TextDrawComponent extends Component {
renderViewerTextShape(results) {
const styles = TextDrawComponent.getViewerStyles(results);
const { isChrome, isEdge } = this.props.browserType;
return (
<g>
{ isChrome || isEdge ? null :
<clipPath id={this.props.annotation.id}>
<rect
x={results.x}
y={results.y}
width={results.width}
height={results.height}
/>
</clipPath>
}
<RenderInBrowser only firefox>
<clipPath id={this.props.annotation.id}>
<rect
x={results.x}
y={results.y}
width={results.width}
height={results.height}
/>
</clipPath>
</RenderInBrowser>
<foreignObject
clipPath={`url(#${this.props.annotation.id})`}
x={results.x}

View File

@ -1,6 +1,5 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import deviceInfo from '/imports/utils/deviceInfo';
import TextShapeService from './service';
import TextDrawComponent from './component';
@ -21,6 +20,5 @@ export default withTracker((params) => {
isActive,
setTextShapeValue: TextShapeService.setTextShapeValue,
resetTextShapeActiveId: TextShapeService.resetTextShapeActiveId,
browserType: deviceInfo.browserType(),
};
})(TextDrawContainer);

View File

@ -3,6 +3,9 @@ import PropTypes from 'prop-types';
import cx from 'classnames';
import { HEXToINTColor, INTToHEXColor } from '/imports/utils/hexInt';
import { defineMessages, injectIntl, intlShape } from 'react-intl';
import RenderInBrowser from 'react-render-in-browser';
import browser from 'browser-detect';
import { noop } from 'lodash';
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
import { styles } from './styles.scss';
import ToolbarMenuItem from './toolbar-menu-item/component';
@ -58,6 +61,8 @@ const intlMessages = defineMessages({
},
});
const runExceptInEdge = fn => (browser().name === 'edge' ? noop : fn);
class WhiteboardToolbar extends Component {
constructor() {
super();
@ -97,6 +102,8 @@ class WhiteboardToolbar extends Component {
this.handleColorChange = this.handleColorChange.bind(this);
this.handleMouseEnter = this.handleMouseEnter.bind(this);
this.handleMouseLeave = this.handleMouseLeave.bind(this);
this.componentDidMount = runExceptInEdge(this.componentDidMount);
this.componentDidUpdate = runExceptInEdge(this.componentDidUpdate);
}
componentWillMount() {
@ -174,7 +181,6 @@ class WhiteboardToolbar extends Component {
* 3. Switch from the Text tool to any other - trigger color and radius for thickness
* 4. Trigger initial animation for the icons
*/
// 1st case
if (this.state.colorSelected.value !== prevState.colorSelected.value) {
// 1st case b)
@ -186,13 +192,12 @@ class WhiteboardToolbar extends Component {
// 2nd case
} else if (this.state.thicknessSelected.value !== prevState.thicknessSelected.value) {
this.thicknessListIconRadius.beginElement();
// 3rd case
// 3rd case
} else if (this.state.annotationSelected.value !== 'text' &&
prevState.annotationSelected.value === 'text') {
prevState.annotationSelected.value === 'text') {
this.thicknessListIconRadius.beginElement();
this.thicknessListIconColor.beginElement();
}
// 4th case, initial animation is triggered in componentDidMount
}
@ -406,36 +411,41 @@ class WhiteboardToolbar extends Component {
renderThicknessItemIcon() {
return (
<svg className={styles.customSvgIcon} shapeRendering="geometricPrecision">
<circle
shapeRendering="geometricPrecision"
cx="50%"
cy="50%"
stroke="black"
strokeWidth="1"
>
<animate
ref={(ref) => { this.thicknessListIconColor = ref; }}
attributeName="fill"
attributeType="XML"
from={this.state.prevColorSelected.value}
to={this.state.colorSelected.value}
begin="indefinite"
dur={TRANSITION_DURATION}
repeatCount="0"
fill="freeze"
/>
<animate
ref={(ref) => { this.thicknessListIconRadius = ref; }}
attributeName="r"
attributeType="XML"
from={this.state.prevThicknessSelected.value}
to={this.state.thicknessSelected.value}
begin="indefinite"
dur={TRANSITION_DURATION}
repeatCount="0"
fill="freeze"
/>
</circle>
<RenderInBrowser only edge>
<circle cx="50%" cy="50%" r={this.state.thicknessSelected.value} stroke="black" strokeWidth="1" fill={this.state.colorSelected.value} />
</RenderInBrowser>
<RenderInBrowser except edge>
<circle
shapeRendering="geometricPrecision"
cx="50%"
cy="50%"
stroke="black"
strokeWidth="1"
>
<animate
ref={(ref) => { this.thicknessListIconColor = ref; }}
attributeName="fill"
attributeType="XML"
from={this.state.prevColorSelected.value}
to={this.state.colorSelected.value}
begin="indefinite"
dur={TRANSITION_DURATION}
repeatCount="0"
fill="freeze"
/>
<animate
ref={(ref) => { this.thicknessListIconRadius = ref; }}
attributeName="r"
attributeType="XML"
from={this.state.prevThicknessSelected.value}
to={this.state.thicknessSelected.value}
begin="indefinite"
dur={TRANSITION_DURATION}
repeatCount="0"
fill="freeze"
/>
</circle>
</RenderInBrowser>
</svg>
);
}
@ -474,19 +484,24 @@ class WhiteboardToolbar extends Component {
renderColorItemIcon() {
return (
<svg className={styles.customSvgIcon}>
<rect x="25%" y="25%" width="50%" height="50%" stroke="black" strokeWidth="1">
<animate
ref={(ref) => { this.colorListIconColor = ref; }}
attributeName="fill"
attributeType="XML"
from={this.state.prevColorSelected.value}
to={this.state.colorSelected.value}
begin="indefinite"
dur={TRANSITION_DURATION}
repeatCount="0"
fill="freeze"
/>
</rect>
<RenderInBrowser only edge>
<rect x="25%" y="25%" width="50%" height="50%" stroke="black" strokeWidth="1" fill={this.state.colorSelected.value} />
</RenderInBrowser>
<RenderInBrowser except edge>
<rect x="25%" y="25%" width="50%" height="50%" stroke="black" strokeWidth="1">
<animate
ref={(ref) => { this.colorListIconColor = ref; }}
attributeName="fill"
attributeType="XML"
from={this.state.prevColorSelected.value}
to={this.state.colorSelected.value}
begin="indefinite"
dur={TRANSITION_DURATION}
repeatCount="0"
fill="freeze"
/>
</rect>
</RenderInBrowser>
</svg>
);
}

View File

@ -30,6 +30,7 @@ class AudioManager {
isHangingUp: false,
isListenOnly: false,
isEchoTest: false,
isTalking: false,
isWaitingPermissions: false,
error: null,
outputDeviceId: null,
@ -164,6 +165,7 @@ class AudioManager {
if (!this.isConnected) return Promise.resolve();
this.isHangingUp = true;
this.isEchoTest = false;
return this.bridge.exitAudio();
}
@ -185,8 +187,17 @@ class AudioManager {
const query = VoiceUsers.find({ intId: Auth.userID });
this.muteHandle = query.observeChanges({
changed: (id, fields) => {
if (fields.muted === this.isMuted) return;
this.isMuted = fields.muted;
if (fields.muted !== undefined && fields.muted !== this.isMuted) {
this.isMuted = fields.muted;
}
if (fields.talking !== undefined && fields.talking !== this.isTalking) {
this.isTalking = fields.talking;
}
if (this.isMuted) {
this.isTalking = false;
}
},
});
}
@ -205,6 +216,7 @@ class AudioManager {
this.isConnected = false;
this.isConnecting = false;
this.isHangingUp = false;
this.isListenOnly = false;
if (this.inputStream) {
window.defaultInputStream.forEach(track => track.stop());
@ -258,6 +270,11 @@ class AudioManager {
return this.listenOnlyAudioContext.createMediaStreamDestination().stream;
}
isUsingAudio() {
return this.isConnected || this.isConnecting ||
this.isHangingUp || this.isEchoTest;
}
setDefaultInputDevice() {
return this.changeInputDevice();
}

View File

@ -0,0 +1,41 @@
@import "/imports/ui/stylesheets/variables/palette";
@import "/imports/ui/stylesheets/variables/general";
@mixin presenterIndicator() {
&:before {
opacity: 1;
top: $user-indicators-offset;
left: $user-indicators-offset;
bottom: auto;
right: auto;
border-radius: 5px;
background-color: $color-primary;
}
:global(.browser-chrome) &:before,
:global(.browser-firefox) &:before {
padding: .45rem;
}
:global(.browser-edge) &:before {
padding-top: $indicator-padding-top;
padding-left: $indicator-padding-left;
padding-right: $indicator-padding-right;
padding-bottom: $indicator-padding-bottom;
}
}
@mixin indicatorStyles() {
&:after {
opacity: 1;
width: 1.2rem;
height: 1.2rem;
}
:global(.browser-edge) &:after {
padding-top: $indicator-padding-top;
padding-left: $indicator-padding-left;
padding-right: $indicator-padding-right;
padding-bottom: $indicator-padding-bottom;
}
}

View File

@ -14,3 +14,14 @@ $lg-padding-y: 0.6rem;
$jumbo-padding-x: 3.025rem;
$jumbo-padding-y: 1.5rem;
//used to center presenter indicator icon in Chrome / Firefox
$indicator-padding: .425rem;
//used to center indicator icons in Edge
$indicator-padding-right: 1.2em;
$indicator-padding-left: 0.175em;
$indicator-padding-top: 0.7em;
$indicator-padding-bottom: 0.7em;
$user-indicators-offset: -5px;

View File

@ -12,25 +12,6 @@ const deviceInfo = {
isPhone: smallSide <= MAX_PHONE_SHORT_SIDE,
};
},
browserType() {
return {
// Uses features to determine browser
isChrome: !!window.chrome && !!window.chrome.webstore,
isFirefox: typeof InstallTrigger !== 'undefined',
isIE: 'ActiveXObject' in window,
isEdge: !document.documentMode && window.StyleMedia,
};
},
osType() {
return {
// Uses userAgent to determine operating system
isWindows: window.navigator.userAgent.indexOf('Windows') !== -1,
isMac: window.navigator.userAgent.indexOf('Mac') !== -1,
isLinux: window.navigator.userAgent.indexOf('Linux') !== -1,
isIOSChrome: navigator.userAgent.match('CriOS'),
};
},
};

View File

@ -401,6 +401,21 @@
"concat-map": "0.0.1"
}
},
"browser-detect": {
"version": "0.2.27",
"resolved": "https://registry.npmjs.org/browser-detect/-/browser-detect-0.2.27.tgz",
"integrity": "sha512-qjOSrFROblMbGhFbS1U7DkszptdRxAH7O9I3zZPT6oIbZKjhrudj+ZRuiQkuVtXs1/HEgMv+2zJuxZIsn/bLhQ==",
"requires": {
"core-js": "2.5.7"
},
"dependencies": {
"core-js": {
"version": "2.5.7",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz",
"integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw=="
}
}
},
"browserslist": {
"version": "2.11.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.11.3.tgz",
@ -4660,6 +4675,16 @@
"prop-types": "15.6.0"
}
},
"react-render-in-browser": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/react-render-in-browser/-/react-render-in-browser-1.0.0.tgz",
"integrity": "sha512-DnOYcGVfjcu13Em8Z/sNbgYSrL26NjCQhZNzOEMV3BJiZ5WfvWFqvI9P/MW2K8guAkuf+hBouQyZysJdqrVhKA==",
"requires": {
"prop-types": "15.6.0",
"react": "16.0.0",
"react-dom": "16.0.0"
}
},
"react-router": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-3.0.5.tgz",

View File

@ -29,6 +29,7 @@
"need to investigate"
],
"babel-runtime": "~6.26.0",
"browser-detect": "^0.2.27",
"classnames": "~2.2.5",
"clipboard": "~1.7.1",
"core-js": "~2.5.3",
@ -52,6 +53,7 @@
"react-dropzone": "~4.2.1",
"react-intl": "~2.4.0",
"react-modal": "~3.0.4",
"react-render-in-browser": "^1.0.0",
"react-router": "~3.0.2",
"react-tabs": "~2.1.0",
"react-toastify": "~2.1.2",

View File

@ -2132,6 +2132,7 @@ class ApiController {
isListeningOnly("${att.isListeningOnly()}")
hasJoinedVoice("${att.isVoiceJoined()}")
hasVideo("${att.hasVideo()}")
clientType() { mkp.yield("${att.clientType}") }
videoStreams() {
att.getStreams().each { s ->
streamName("${s}")

View File

@ -34,6 +34,7 @@
<isListeningOnly>${att.isListeningOnly()?c}</isListeningOnly>
<hasJoinedVoice>${att.isVoiceJoined()?c}</hasJoinedVoice>
<hasVideo>${att.hasVideo()?c}</hasVideo>
<clientType>${att.getClientType()}</clientType>
<#if meeting.getUserCustomData(att.getExternalUserId())??>
<#assign ucd = meeting.getUserCustomData(att.getExternalUserId())>
<customdata>

View File

@ -39,6 +39,7 @@
<isListeningOnly>${att.isListeningOnly()?c}</isListeningOnly>
<hasJoinedVoice>${att.isVoiceJoined()?c}</hasJoinedVoice>
<hasVideo>${att.hasVideo()?c}</hasVideo>
<clientType>${att.getClientType()}</clientType>
<#if meeting.getUserCustomData(att.getExternalUserId())??>
<#assign ucd = meetingDetail.meeting.getUserCustomData(att.getExternalUserId())>
<customdata>