Merge pull request #5770 from ritzalam/merge-latest-20-branch
Merge latest 20 branch
This commit is contained in:
commit
63d43da710
@ -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)
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
1
bigbluebutton-client/.gitignore
vendored
1
bigbluebutton-client/.gitignore
vendored
@ -6,7 +6,6 @@ bin/
|
||||
bin-debug/
|
||||
bin-release/
|
||||
client/
|
||||
locale/.tx/
|
||||
bbbResources.properties.*
|
||||
asdoc/
|
||||
hs_err_pid*
|
||||
|
@ -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;
|
||||
|
1
bigbluebutton-client/locale/.gitignore
vendored
1
bigbluebutton-client/locale/.gitignore
vendored
@ -1,3 +1,2 @@
|
||||
.tx
|
||||
ru/
|
||||
|
||||
|
10
bigbluebutton-client/locale/.tx/config
Normal file
10
bigbluebutton-client/locale/.tx/config
Normal 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
|
||||
|
@ -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
|
||||
|
@ -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
@ -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;
|
||||
}
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 && 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>
|
||||
|
@ -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;
|
||||
|
@ -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')}"
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -262,7 +262,7 @@
|
||||
<div class="row">
|
||||
<div class="span twelve center">
|
||||
<p>Copyright © 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>
|
||||
|
@ -288,7 +288,7 @@
|
||||
<div class="row">
|
||||
<div class="span twelve center">
|
||||
<p>Copyright © 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>
|
||||
|
@ -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`);
|
||||
|
@ -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
|
||||
|
@ -29,6 +29,7 @@ export default function addUser(meetingId, user) {
|
||||
presenter: Boolean,
|
||||
locked: Boolean,
|
||||
avatar: String,
|
||||
clientType: String,
|
||||
});
|
||||
|
||||
const userId = user.intId;
|
||||
|
@ -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);
|
||||
|
@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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}
|
||||
|
@ -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));
|
||||
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
@ -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 (
|
||||
|
@ -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');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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%;
|
||||
|
@ -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}
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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'),
|
||||
};
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
25
bigbluebutton-html5/package-lock.json
generated
25
bigbluebutton-html5/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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}")
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user