Merge remote-tracking branch 'upstream/v2.4.x-release' into v2.4.x-release

This commit is contained in:
Fred Dixon 2022-02-05 10:00:47 -06:00
commit 15e6da9b69
248 changed files with 7995 additions and 37479 deletions

View File

@ -51,7 +51,7 @@ trait SystemConfiguration {
lazy val endMeetingWhenNoMoreAuthedUsersAfterMinutes = Try(config.getInt("apps.endMeetingWhenNoMoreAuthedUsersAfterMinutes")).getOrElse(2)
lazy val reduceDuplicatedPick = Try(config.getBoolean("apps.reduceDuplicatedPick")).getOrElse(false)
// Redis server configuration
lazy val redisHost = Try(config.getString("redis.host")).getOrElse("127.0.0.1")
lazy val redisPort = Try(config.getInt("redis.port")).getOrElse(6379)

View File

@ -91,24 +91,6 @@ object ScreenshareModel {
def getHasAudio(status: ScreenshareModel): Boolean = {
status.hasAudio
}
def stop(outGW: OutMsgRouter, liveMeeting: LiveMeeting): Unit = {
if (isBroadcastingRTMP(liveMeeting.screenshareModel)) {
this.resetDesktopSharingParams(liveMeeting.screenshareModel)
val event = MsgBuilder.buildStopScreenshareRtmpBroadcastEvtMsg(
liveMeeting.props.meetingProp.intId,
getVoiceConf(liveMeeting.screenshareModel),
getScreenshareConf(liveMeeting.screenshareModel),
getRTMPBroadcastingUrl(liveMeeting.screenshareModel),
getScreenshareVideoWidth(liveMeeting.screenshareModel),
getScreenshareVideoHeight(liveMeeting.screenshareModel),
getTimestamp(liveMeeting.screenshareModel)
)
outGW.send(event)
}
}
}
class ScreenshareModel {

View File

@ -249,12 +249,15 @@ class WhiteboardModel extends SystemConfiguration {
def getWhiteboardAccess(wbId: String): Array[String] = getWhiteboard(wbId).multiUser
def isNonEjectionGracePeriodOver(wbId: String, userId: String): Boolean = {
val wb = getWhiteboard(wbId)
val lastChange = System.currentTimeMillis() - wb.changedModeOn
!(wb.oldMultiUser.contains(userId) && lastChange < 5000)
}
def hasWhiteboardAccess(wbId: String, userId: String): Boolean = {
val wb = getWhiteboard(wbId)
wb.multiUser.contains(userId) || {
val lastChange = System.currentTimeMillis() - wb.changedModeOn
wb.oldMultiUser.contains(userId) && lastChange < 5000
}
wb.multiUser.contains(userId)
}
def getChangedModeOn(wbId: String): Long = getWhiteboard(wbId).changedModeOn

View File

@ -1,9 +1,10 @@
package org.bigbluebutton.core.apps.externalvideo
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.apps.{ ExternalVideoModel, PermissionCheck, RightsManagementTrait, ScreenshareModel }
import org.bigbluebutton.core.apps.{ ExternalVideoModel, PermissionCheck, RightsManagementTrait }
import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.running.LiveMeeting
import org.bigbluebutton.core.apps.screenshare.ScreenshareApp2x.{ requestBroadcastStop }
trait StartExternalVideoPubMsgHdlr extends RightsManagementTrait {
this: ExternalVideoApp2x =>
@ -29,8 +30,9 @@ trait StartExternalVideoPubMsgHdlr extends RightsManagementTrait {
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
} else {
//Stop ScreenShare if it's running
ScreenshareModel.stop(bus.outGW, liveMeeting)
// Request a screen broadcast stop (goes to SFU, comes back through
// ScreenshareRtmpBroadcastStoppedVoiceConfEvtMsg)
requestBroadcastStop(bus.outGW, liveMeeting)
ExternalVideoModel.setURL(liveMeeting.externalVideoModel, msg.body.externalVideoUrl)
broadcastEvent(msg)

View File

@ -39,15 +39,7 @@ trait SendGroupChatMessageMsgHdlr {
}
}
// Check if this message was sent while the lock settings was being changed.
val isDelayedMessage = System.currentTimeMillis() - MeetingStatus2x.getPermissionsChangedOn(liveMeeting.status) < 5000
if (applyPermissionCheck && chatLocked && !isDelayedMessage) {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to send a message to this group chat."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
state
} else {
if (!(applyPermissionCheck && chatLocked)) {
def makeHeader(name: String, meetingId: String, userId: String): BbbClientMsgHeader = {
BbbClientMsgHeader(name, meetingId, userId)
}
@ -90,7 +82,7 @@ trait SendGroupChatMessageMsgHdlr {
case Some(ns) => ns
case None => state
}
}
} else { state }
}
}

View File

@ -23,12 +23,12 @@ trait RespondToTypedPollReqMsgHdlr {
bus.outGW.send(msgEvent)
}
def broadcastUserRespondedToTypedPollRespMsg(msg: RespondToTypedPollReqMsg, pollId: String, answer: String, sendToId: String): Unit = {
def broadcastUserRespondedToTypedPollRespMsg(msg: RespondToTypedPollReqMsg, pollId: String, answerId: Int, sendToId: String): Unit = {
val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, liveMeeting.props.meetingProp.intId, sendToId)
val envelope = BbbCoreEnvelope(UserRespondedToTypedPollRespMsg.NAME, routing)
val header = BbbClientMsgHeader(UserRespondedToTypedPollRespMsg.NAME, liveMeeting.props.meetingProp.intId, sendToId)
val body = UserRespondedToTypedPollRespMsgBody(pollId, msg.header.userId, answer)
val body = UserRespondedToTypedPollRespMsgBody(pollId, msg.header.userId, answerId)
val event = UserRespondedToTypedPollRespMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
bus.outGW.send(msgEvent)
@ -43,7 +43,8 @@ trait RespondToTypedPollReqMsgHdlr {
for {
presenter <- Users2x.findPresenter(liveMeeting.users2x)
} yield {
broadcastUserRespondedToTypedPollRespMsg(msg, pollId, msg.body.answer, presenter.intId)
val answerId = (updatedPoll.answers find (ans => ans.key == msg.body.answer)).get.id
broadcastUserRespondedToTypedPollRespMsg(msg, pollId, answerId, presenter.intId)
}
}
}

View File

@ -2,13 +2,49 @@ package org.bigbluebutton.core.apps.screenshare
import akka.actor.ActorContext
import akka.event.Logging
import org.bigbluebutton.core.apps.ScreenshareModel
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
import org.bigbluebutton.core2.message.senders.MsgBuilder
object ScreenshareApp2x {
def requestBroadcastStop(outGW: OutMsgRouter, liveMeeting: LiveMeeting): Unit = {
if (ScreenshareModel.isBroadcastingRTMP(liveMeeting.screenshareModel)) {
val event = MsgBuilder.buildScreenBroadcastStopSysMsg(
liveMeeting.props.meetingProp.intId,
ScreenshareModel.getVoiceConf(liveMeeting.screenshareModel),
ScreenshareModel.getRTMPBroadcastingUrl(liveMeeting.screenshareModel),
)
outGW.send(event)
}
}
def broadcastStopped(outGW: OutMsgRouter, liveMeeting: LiveMeeting): Unit = {
if (ScreenshareModel.isBroadcastingRTMP(liveMeeting.screenshareModel)) {
ScreenshareModel.resetDesktopSharingParams(liveMeeting.screenshareModel)
val event = MsgBuilder.buildStopScreenshareRtmpBroadcastEvtMsg(
liveMeeting.props.meetingProp.intId,
ScreenshareModel.getVoiceConf(liveMeeting.screenshareModel),
ScreenshareModel.getScreenshareConf(liveMeeting.screenshareModel),
ScreenshareModel.getRTMPBroadcastingUrl(liveMeeting.screenshareModel),
ScreenshareModel.getScreenshareVideoWidth(liveMeeting.screenshareModel),
ScreenshareModel.getScreenshareVideoHeight(liveMeeting.screenshareModel),
ScreenshareModel.getTimestamp(liveMeeting.screenshareModel)
)
outGW.send(event)
}
}
}
class ScreenshareApp2x(implicit val context: ActorContext)
extends ScreenshareStartedVoiceConfEvtMsgHdlr
with ScreenshareStoppedVoiceConfEvtMsgHdlr
with GetScreenshareStatusReqMsgHdlr
with ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr
with ScreenshareRtmpBroadcastStoppedVoiceConfEvtMsgHdlr {
with ScreenshareRtmpBroadcastStoppedVoiceConfEvtMsgHdlr
with SyncGetScreenshareInfoRespMsgHdlr {
val log = Logging(context.system, getClass)

View File

@ -5,32 +5,16 @@ import org.bigbluebutton.core.apps.ScreenshareModel
import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.running.LiveMeeting
import org.bigbluebutton.core2.message.senders.MsgBuilder
import org.bigbluebutton.core.apps.screenshare.ScreenshareApp2x.{ broadcastStopped }
trait ScreenshareRtmpBroadcastStoppedVoiceConfEvtMsgHdlr {
this: ScreenshareApp2x =>
def handle(msg: ScreenshareRtmpBroadcastStoppedVoiceConfEvtMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
log.info("handleScreenshareRTMPBroadcastStoppedRequest: isBroadcastingRTMP=" +
ScreenshareModel.isBroadcastingRTMP(liveMeeting.screenshareModel) + " URL:" +
ScreenshareModel.getRTMPBroadcastingUrl(liveMeeting.screenshareModel))
// only valid if currently broadcasting
if (ScreenshareModel.isBroadcastingRTMP(liveMeeting.screenshareModel)) {
log.info("STOP broadcast ALLOWED when isBroadcastingRTMP=true")
ScreenshareModel.broadcastingRTMPStopped(liveMeeting.screenshareModel)
// notify viewers that RTMP broadcast stopped
val msgEvent = MsgBuilder.buildStopScreenshareRtmpBroadcastEvtMsg(
liveMeeting.props.meetingProp.intId,
msg.body.voiceConf, msg.body.screenshareConf, msg.body.stream,
msg.body.vidWidth, msg.body.vidHeight, msg.body.timestamp
)
bus.outGW.send(msgEvent)
} else {
log.info("STOP broadcast NOT ALLOWED when isBroadcastingRTMP=false")
}
broadcastStopped(bus.outGW, liveMeeting)
}
}

View File

@ -0,0 +1,37 @@
package org.bigbluebutton.core.apps.screenshare
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.apps.ScreenshareModel
import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.running.LiveMeeting
trait SyncGetScreenshareInfoRespMsgHdlr {
this: ScreenshareApp2x =>
def handleSyncGetScreenshareInfoRespMsg(liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
val routing = Routing.addMsgToHtml5InstanceIdRouting(
liveMeeting.props.meetingProp.intId,
liveMeeting.props.systemProps.html5InstanceId.toString
)
val envelope = BbbCoreEnvelope(SyncGetScreenshareInfoRespMsg.NAME, routing)
val header = BbbClientMsgHeader(
SyncGetScreenshareInfoRespMsg.NAME,
liveMeeting.props.meetingProp.intId,
"nodeJSapp"
)
val body = SyncGetScreenshareInfoRespMsgBody(
ScreenshareModel.isBroadcastingRTMP(liveMeeting.screenshareModel),
ScreenshareModel.getVoiceConf(liveMeeting.screenshareModel),
ScreenshareModel.getScreenshareConf(liveMeeting.screenshareModel),
ScreenshareModel.getRTMPBroadcastingUrl(liveMeeting.screenshareModel),
ScreenshareModel.getScreenshareVideoWidth(liveMeeting.screenshareModel),
ScreenshareModel.getScreenshareVideoHeight(liveMeeting.screenshareModel),
ScreenshareModel.getTimestamp(liveMeeting.screenshareModel),
ScreenshareModel.getHasAudio(liveMeeting.screenshareModel)
)
val event = SyncGetScreenshareInfoRespMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
bus.outGW.send(msgEvent)
}
}

View File

@ -7,6 +7,7 @@ import org.bigbluebutton.core.models.{ PresentationPod, UserState, Users2x }
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.apps.screenshare.ScreenshareApp2x.{ requestBroadcastStop }
trait AssignPresenterReqMsgHdlr extends RightsManagementTrait {
this: UsersApp =>
@ -72,6 +73,9 @@ object AssignPresenterActionHandler extends RightsManagementTrait {
if (oldPres.intId != newPresenterId) {
// Stop external video if it's running
ExternalVideoModel.stop(outGW, liveMeeting)
// Request a screen broadcast stop (goes to SFU, comes back through
// ScreenshareRtmpBroadcastStoppedVoiceConfEvtMsg)
requestBroadcastStop(outGW, liveMeeting)
Users2x.makeNotPresenter(liveMeeting.users2x, oldPres.intId)
broadcastOldPresenterChange(oldPres)

View File

@ -41,9 +41,7 @@ trait ChangeUserRoleCmdMsgHdlr extends RightsManagementTrait {
val event = buildUserRoleChangedEvtMsg(liveMeeting.props.meetingProp.intId, msg.body.userId,
msg.body.changedBy, Roles.VIEWER_ROLE)
if (newUvo.locked) {
LockSettingsUtil.enforceCamLockSettingsForAllUsers(liveMeeting, outGW)
}
LockSettingsUtil.enforceCamLockSettingsForAllUsers(liveMeeting, outGW)
outGW.send(event)
}

View File

@ -22,6 +22,7 @@ trait GetCamBroadcastPermissionReqMsgHdlr {
if (!user.userLeftFlag.left
&& liveMeeting.props.meetingProp.intId == msg.body.meetingId
&& msg.body.streamId.startsWith(msg.header.userId)
&& (applyPermissionCheck && !camBroadcastLocked)) {
allowed = true
}
@ -30,6 +31,7 @@ trait GetCamBroadcastPermissionReqMsgHdlr {
val event = MsgBuilder.buildGetCamBroadcastPermissionRespMsg(
liveMeeting.props.meetingProp.intId,
msg.body.userId,
msg.body.streamId,
msg.body.sfuSessionId,
allowed
)

View File

@ -22,7 +22,6 @@ trait RegisterUserReqMsgHdlr {
val event = UserRegisteredRespMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
val guestStatus = msg.body.guestStatus
val regUser = RegisteredUsers.create(msg.body.intUserId, msg.body.extUserId,

View File

@ -50,7 +50,7 @@ trait SelectRandomViewerReqMsgHdlr extends RightsManagementTrait {
Users2x.setUserExempted(liveMeeting.users2x, pickedUser, true)
}
}
val userIds = users.map { case (v) => v.intId }
broadcastEvent(msg, userIds, pickedUser)
}

View File

@ -11,7 +11,7 @@ trait UserConnectedToGlobalAudioMsgHdlr {
def handleUserConnectedToGlobalAudioMsg(msg: UserConnectedToGlobalAudioMsg) {
log.info("Handling UserConnectedToGlobalAudio: meetingId=" + props.meetingProp.intId + " userId=" + msg.body.userId)
def broadcastEvent(vu: VoiceUserState): Unit = {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, props.meetingProp.intId,
vu.intId)

View File

@ -2,9 +2,11 @@ package org.bigbluebutton.core.apps.voice
import org.bigbluebutton.SystemConfiguration
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.models.VoiceUsers
import org.bigbluebutton.core.running.{ LiveMeeting, MeetingActor, OutMsgRouter }
import org.bigbluebutton.core2.message.senders.MsgBuilder
import org.bigbluebutton.core.models._
import org.bigbluebutton.core.apps.users.UsersApp
import org.bigbluebutton.core2.MeetingStatus2x
trait UserJoinedVoiceConfEvtMsgHdlr extends SystemConfiguration {
this: MeetingActor =>
@ -13,17 +15,57 @@ trait UserJoinedVoiceConfEvtMsgHdlr extends SystemConfiguration {
val outGW: OutMsgRouter
def handleUserJoinedVoiceConfEvtMsg(msg: UserJoinedVoiceConfEvtMsg): Unit = {
log.info("Received user joined voice conference " + msg)
if (VoiceUsers.isCallerBanned(msg.body.callerIdNum, liveMeeting.voiceUsers)) {
log.info("Ejecting banned voice user " + msg)
val event = MsgBuilder.buildEjectUserFromVoiceConfSysMsg(
props.meetingProp.intId,
props.voiceProp.voiceConf,
msg.body.voiceUserId
)
val guestPolicy = GuestsWaiting.getGuestPolicy(liveMeeting.guestsWaiting)
def notifyModeratorsOfGuestWaiting(guest: GuestWaiting, users: Users2x, meetingId: String): Unit = {
val moderators = Users2x.findAll(users).filter(p => p.role == Roles.MODERATOR_ROLE)
moderators foreach { mod =>
val event = MsgBuilder.buildGuestsWaitingForApprovalEvtMsg(meetingId, mod.intId, Vector(guest))
outGW.send(event)
}
// bbb-html should only listen for this single message
val event = MsgBuilder.buildGuestsWaitingForApprovalEvtMsg(meetingId, "nodeJSapp", Vector(guest))
outGW.send(event)
} else {
}
def registerUserInRegisteredUsers() = {
val regUser = RegisteredUsers.create(msg.body.intId, msg.body.voiceUserId,
msg.body.callerIdName, Roles.VIEWER_ROLE, "",
"", true, true, GuestStatus.WAIT, true, false)
RegisteredUsers.add(liveMeeting.registeredUsers, regUser)
}
def registerUserInUsers2x() = {
val newUser = UserState(
intId = msg.body.intId,
extId = msg.body.voiceUserId,
name = msg.body.callerIdName,
role = Roles.VIEWER_ROLE,
guest = true,
authed = true,
guestStatus = GuestStatus.WAIT,
emoji = "none",
pin = false,
presenter = false,
locked = MeetingStatus2x.getPermissions(liveMeeting.status).lockOnJoin,
avatar = "",
clientType = "",
pickExempted = false,
userLeftFlag = UserLeftFlag(false, 0)
)
Users2x.add(liveMeeting.users2x, newUser)
}
def registerUserAsGuest() = {
if (GuestsWaiting.findWithIntId(liveMeeting.guestsWaiting, msg.body.intId) == None) {
val guest = GuestWaiting(msg.body.intId, msg.body.callerIdName, Roles.VIEWER_ROLE, true, "", true, System.currentTimeMillis())
GuestsWaiting.add(liveMeeting.guestsWaiting, guest)
notifyModeratorsOfGuestWaiting(guest, liveMeeting.users2x, liveMeeting.props.meetingProp.intId)
}
}
def letUserEnter() = {
VoiceApp.handleUserJoinedVoiceConfEvtMsg(
liveMeeting,
outGW,
@ -39,5 +81,30 @@ trait UserJoinedVoiceConfEvtMsgHdlr extends SystemConfiguration {
"freeswitch"
)
}
//Firs of all we check whether the user is banned from the meeting
if (VoiceUsers.isCallerBanned(msg.body.callerIdNum, liveMeeting.voiceUsers)) {
log.info("Ejecting banned voice user " + msg)
val event = MsgBuilder.buildEjectUserFromVoiceConfSysMsg(
props.meetingProp.intId,
props.voiceProp.voiceConf,
msg.body.voiceUserId
)
outGW.send(event)
} else {
if (msg.body.intId.startsWith("v_")) { // Dial-in user (v_*)
registerUserInRegisteredUsers()
registerUserInUsers2x()
}
guestPolicy match {
case GuestPolicy(policy, setBy) => {
policy match {
case GuestPolicyType.ALWAYS_ACCEPT => letUserEnter()
case GuestPolicyType.ALWAYS_DENY => VoiceApp.removeUserFromVoiceConf(liveMeeting, outGW, msg.body.voiceUserId)
case GuestPolicyType.ASK_MODERATOR => registerUserAsGuest()
}
}
}
}
}
}

View File

@ -2,13 +2,14 @@ package org.bigbluebutton.core.apps.voice
import org.bigbluebutton.SystemConfiguration
import org.bigbluebutton.LockSettingsUtil
import org.bigbluebutton.common2.msgs.{ BbbClientMsgHeader, BbbCommonEnvCoreMsg, BbbCoreEnvelope, ConfVoiceUser, MessageTypes, Routing, UserJoinedVoiceConfToClientEvtMsg, UserJoinedVoiceConfToClientEvtMsgBody, UserLeftVoiceConfToClientEvtMsg, UserLeftVoiceConfToClientEvtMsgBody, UserMutedVoiceEvtMsg, UserMutedVoiceEvtMsgBody }
import org.bigbluebutton.core.apps.breakout.BreakoutHdlrHelpers
import org.bigbluebutton.core.bus.InternalEventBus
import org.bigbluebutton.core.models.{ Users2x, VoiceUserState, VoiceUsers }
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
import org.bigbluebutton.core2.MeetingStatus2x
import org.bigbluebutton.core2.message.senders.MsgBuilder
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.running.{ LiveMeeting, MeetingActor, OutMsgRouter }
import org.bigbluebutton.core.models._
import org.bigbluebutton.core.apps.users.UsersApp
object VoiceApp extends SystemConfiguration {
@ -152,20 +153,22 @@ object VoiceApp extends SystemConfiguration {
}
}
case None =>
handleUserJoinedVoiceConfEvtMsg(
liveMeeting,
outGW,
eventBus,
liveMeeting.props.voiceProp.voiceConf,
cvu.intId,
cvu.voiceUserId,
cvu.callingWith,
cvu.callerIdName,
cvu.callerIdNum,
cvu.muted,
cvu.talking,
cvu.calledInto
)
if (!cvu.intId.startsWith("v_")) {
handleUserJoinedVoiceConfEvtMsg(
liveMeeting,
outGW,
eventBus,
liveMeeting.props.voiceProp.voiceConf,
cvu.intId,
cvu.voiceUserId,
cvu.callingWith,
cvu.callerIdName,
cvu.callerIdNum,
cvu.muted,
cvu.talking,
cvu.calledInto
)
}
}
}
@ -304,6 +307,17 @@ object VoiceApp extends SystemConfiguration {
}
def removeUserFromVoiceConf(
liveMeeting: LiveMeeting,
outGW: OutMsgRouter,
voiceUserId: String,
): Unit = {
val guest = GuestApprovedVO(voiceUserId, GuestStatus.DENY)
UsersApp.approveOrRejectGuest(liveMeeting, outGW, guest, SystemUser.ID)
val event = MsgBuilder.buildEjectUserFromVoiceConfSysMsg(liveMeeting.props.meetingProp.intId, liveMeeting.props.voiceProp.voiceConf, voiceUserId)
outGW.send(event)
}
def handleUserLeftVoiceConfEvtMsg(
liveMeeting: LiveMeeting,
outGW: OutMsgRouter,

View File

@ -22,9 +22,11 @@ trait ClearWhiteboardPubMsgHdlr extends RightsManagementTrait {
}
if (filterWhiteboardMessage(msg.body.whiteboardId, msg.header.userId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to clear the whiteboard."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
if (isNonEjectionGracePeriodOver(msg.body.whiteboardId, msg.header.userId, liveMeeting)) {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to clear the whiteboard."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
}
} else {
for {
fullClear <- clearWhiteboard(msg.body.whiteboardId, msg.header.userId, liveMeeting)

View File

@ -21,10 +21,12 @@ trait ModifyWhiteboardAccessPubMsgHdlr extends RightsManagementTrait {
bus.outGW.send(msgEvent)
}
if (permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to modify access to the whiteboard."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
if (filterWhiteboardMessage(msg.body.whiteboardId, msg.header.userId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
if (isNonEjectionGracePeriodOver(msg.body.whiteboardId, msg.header.userId, liveMeeting)) {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to modify access to the whiteboard."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
}
} else {
modifyWhiteboardAccess(msg.body.whiteboardId, msg.body.multiUser, liveMeeting)
broadcastEvent(msg)

View File

@ -22,9 +22,11 @@ trait UndoWhiteboardPubMsgHdlr extends RightsManagementTrait {
}
if (filterWhiteboardMessage(msg.body.whiteboardId, msg.header.userId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to undo an annotation."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
if (isNonEjectionGracePeriodOver(msg.body.whiteboardId, msg.header.userId, liveMeeting)) {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to undo an annotation."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
}
} else {
for {
lastAnnotation <- undoWhiteboard(msg.body.whiteboardId, msg.header.userId, liveMeeting)

View File

@ -78,4 +78,8 @@ class WhiteboardApp2x(implicit val context: ActorContext)
// mode was changed. (ralam nov 22, 2017)
!liveMeeting.wbModel.hasWhiteboardAccess(whiteboardId, userId)
}
def isNonEjectionGracePeriodOver(wbId: String, userId: String, liveMeeting: LiveMeeting): Boolean = {
liveMeeting.wbModel.isNonEjectionGracePeriodOver(wbId, userId)
}
}

View File

@ -174,7 +174,7 @@ object Users2x {
newUser
}
}
def hasPresenter(users: Users2x): Boolean = {
findPresenter(users) match {
case Some(p) => true

View File

@ -255,7 +255,6 @@ class MeetingActor(
// Handling RegisterUserReqMsg as it is forwarded from BBBActor and
// its type is not BbbCommonEnvCoreMsg
case m: RegisterUserReqMsg => usersApp.handleRegisterUserReqMsg(m)
case m: EjectDuplicateUserReqMsg => usersApp.handleEjectDuplicateUserReqMsg(m)
case m: GetAllMeetingsReqMsg => handleGetAllMeetingsReqMsg(m)
case m: GetRunningMeetingStateReqMsg => handleGetRunningMeetingStateReqMsg(m)
@ -693,7 +692,8 @@ class MeetingActor(
// sync all lock settings
handleSyncGetLockSettingsMsg(state, liveMeeting, msgBus)
// TODO send all screen sharing info
// send all screen sharing info
screenshareApp2x.handleSyncGetScreenshareInfoRespMsg(liveMeeting, msgBus)
}
def handleGetAllMeetingsReqMsg(msg: GetAllMeetingsReqMsg): Unit = {

View File

@ -22,10 +22,12 @@ trait EjectUserFromVoiceCmdMsgHdlr extends RightsManagementTrait {
for {
u <- VoiceUsers.findWithIntId(liveMeeting.voiceUsers, msg.body.userId)
} yield {
log.info("Ejecting user from voice. meetingId=" + props.meetingProp.intId + " userId=" + u.intId)
VoiceUsers.ban(liveMeeting.voiceUsers, u)
log.info("Ejecting user from voice. meetingId=" + props.meetingProp.intId + " userId=" + u.intId + " ban=" + msg.body.banUser)
val event = MsgBuilder.buildEjectUserFromVoiceConfSysMsg(props.meetingProp.intId, props.voiceProp.voiceConf, u.voiceUserId)
outGW.send(event)
if (msg.body.banUser) {
VoiceUsers.ban(liveMeeting.voiceUsers, u)
}
}
}
}

View File

@ -2,7 +2,9 @@ package org.bigbluebutton.core2.message.handlers.guests
import org.bigbluebutton.common2.msgs.{ GuestApprovedVO, GuestsWaitingApprovedMsg }
import org.bigbluebutton.core.apps.users.UsersApp
import org.bigbluebutton.core.apps.voice.VoiceApp
import org.bigbluebutton.core.models._
import org.bigbluebutton.core.bus.InternalEventBus
import org.bigbluebutton.core.running.{ BaseMeetingActor, HandlerHelpers, LiveMeeting, OutMsgRouter }
import org.bigbluebutton.core2.message.senders.MsgBuilder
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
@ -12,6 +14,7 @@ trait GuestsWaitingApprovedMsgHdlr extends HandlerHelpers with RightsManagementT
val liveMeeting: LiveMeeting
val outGW: OutMsgRouter
val eventBus: InternalEventBus
def handleGuestsWaitingApprovedMsg(msg: GuestsWaitingApprovedMsg): Unit = {
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
@ -24,6 +27,32 @@ trait GuestsWaitingApprovedMsgHdlr extends HandlerHelpers with RightsManagementT
// Remove guest from waiting list
_ <- GuestsWaiting.remove(liveMeeting.guestsWaiting, g.guest)
} yield {
if (g.guest.startsWith("v_")) {
Users2x.findWithIntId(liveMeeting.users2x, g.guest) match {
case Some(dialInUser) =>
if (g.status == GuestStatus.ALLOW) {
VoiceApp.handleUserJoinedVoiceConfEvtMsg(
liveMeeting,
outGW,
eventBus,
liveMeeting.props.voiceProp.voiceConf,
g.guest,
dialInUser.extId,
"none",
dialInUser.name,
dialInUser.name,
false,
false,
"freeswitch"
)
} else {
VoiceApp.removeUserFromVoiceConf(liveMeeting, outGW, dialInUser.extId)
val event = MsgBuilder.buildEjectUserFromVoiceConfSysMsg(liveMeeting.props.meetingProp.intId, liveMeeting.props.voiceProp.voiceConf, g.guest)
outGW.send(event)
}
case None => None
}
}
UsersApp.approveOrRejectGuest(liveMeeting, outGW, g, msg.body.approvedBy)
}
}

View File

@ -505,6 +505,7 @@ object MsgBuilder {
def buildGetCamBroadcastPermissionRespMsg(
meetingId: String,
userId: String,
streamId: String,
sfuSessionId: String,
allowed: Boolean
): BbbCommonEnvCoreMsg = {
@ -515,6 +516,7 @@ object MsgBuilder {
val body = GetCamBroadcastPermissionRespMsgBody(
meetingId,
userId,
streamId,
sfuSessionId,
allowed
)
@ -610,4 +612,18 @@ object MsgBuilder {
BbbCommonEnvCoreMsg(envelope, event)
}
def buildScreenBroadcastStopSysMsg(
meetingId: String,
voiceConf: String,
streamId: String
): BbbCommonEnvCoreMsg = {
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
val envelope = BbbCoreEnvelope(ScreenBroadcastStopSysMsg.NAME, routing)
val body = ScreenBroadcastStopSysMsgBody(meetingId, voiceConf, streamId)
val header = BbbCoreBaseHeader(ScreenBroadcastStopSysMsg.NAME)
val event = ScreenBroadcastStopSysMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
}

View File

@ -42,6 +42,8 @@ trait AppsTestFixtures {
val allowModsToUnmuteUsers = false
val allowModsToEjectCameras = false
val authenticatedGuest = false
val meetingLayout = ""
val virtualBackgroundsEnabled = false
val red5DeskShareIPTestFixture = "127.0.0.1"
val red5DeskShareAppTestFixtures = "red5App"
@ -61,7 +63,8 @@ trait AppsTestFixtures {
modOnlyMessage = modOnlyMessage)
val voiceProp = VoiceProp(telVoice = voiceConfId, voiceConf = voiceConfId, dialNumber = dialNumber, muteOnStart = muteOnStart)
val usersProp = UsersProp(maxUsers = maxUsers, webcamsOnlyForModerator = webcamsOnlyForModerator,
guestPolicy = guestPolicy, allowModsToUnmuteUsers = allowModsToUnmuteUsers, allowModsToEjectCameras = allowModsToEjectCameras, authenticatedGuest = authenticatedGuest)
guestPolicy = guestPolicy, allowModsToUnmuteUsers = allowModsToUnmuteUsers, allowModsToEjectCameras = allowModsToEjectCameras,
authenticatedGuest = authenticatedGuest, meetingLayout = meetingLayout, virtualBackgroundsEnabled = virtualBackgroundsEnabled)
val metadataProp = new MetadataProp(metadata)
val defaultProps = DefaultProps(meetingProp, breakoutProps, durationProps, password, recordProp, welcomeProp, voiceProp,

View File

@ -28,7 +28,16 @@ case class WelcomeProp(welcomeMsgTemplate: String, welcomeMsg: String, modOnlyMe
case class VoiceProp(telVoice: String, voiceConf: String, dialNumber: String, muteOnStart: Boolean)
case class UsersProp(maxUsers: Int, webcamsOnlyForModerator: Boolean, guestPolicy: String, meetingLayout: String, allowModsToUnmuteUsers: Boolean, allowModsToEjectCameras: Boolean, authenticatedGuest: Boolean)
case class UsersProp(
maxUsers: Int,
webcamsOnlyForModerator: Boolean,
guestPolicy: String,
meetingLayout: String,
allowModsToUnmuteUsers: Boolean,
allowModsToEjectCameras: Boolean,
authenticatedGuest: Boolean,
virtualBackgroundsDisabled: Boolean
)
case class MetadataProp(metadata: collection.immutable.Map[String, String])

View File

@ -44,7 +44,7 @@ case class UserRespondedToPollRespMsgBody(pollId: String, userId: String, answer
object UserRespondedToTypedPollRespMsg { val NAME = "UserRespondedToTypedPollRespMsg" }
case class UserRespondedToTypedPollRespMsg(header: BbbClientMsgHeader, body: UserRespondedToTypedPollRespMsgBody) extends BbbCoreMsg
case class UserRespondedToTypedPollRespMsgBody(pollId: String, userId: String, answer: String)
case class UserRespondedToTypedPollRespMsgBody(pollId: String, userId: String, answerId: Int)
object ShowPollResultReqMsg { val NAME = "ShowPollResultReqMsg" }
case class ShowPollResultReqMsg(header: BbbClientMsgHeader, body: ShowPollResultReqMsgBody) extends StandardMsg

View File

@ -39,6 +39,25 @@ case class ScreenshareRtmpBroadcastStartedEvtMsgBody(voiceConf: String, screensh
stream: String, vidWidth: Int, vidHeight: Int,
timestamp: String, hasAudio: Boolean)
/**
* Sync screenshare state with bbb-html5
*/
object SyncGetScreenshareInfoRespMsg { val NAME = "SyncGetScreenshareInfoRespMsg" }
case class SyncGetScreenshareInfoRespMsg(
header: BbbClientMsgHeader,
body: SyncGetScreenshareInfoRespMsgBody
) extends BbbCoreMsg
case class SyncGetScreenshareInfoRespMsgBody(
isBroadcasting: Boolean,
voiceConf: String,
screenshareConf: String,
stream: String,
vidWidth: Int,
vidHeight: Int,
timestamp: String,
hasAudio: Boolean
)
/**
* Send by FS that RTMP stream has stopped.
*/
@ -166,6 +185,21 @@ case class GetScreenSubscribePermissionRespMsgBody(
sfuSessionId: String,
allowed: Boolean
)
/**
* Sent to bbb-webrtc-sfu to tear down screen stream #streamId
*/
object ScreenBroadcastStopSysMsg { val NAME = "ScreenBroadcastStopSysMsg" }
case class ScreenBroadcastStopSysMsg(
header: BbbCoreBaseHeader,
body: ScreenBroadcastStopSysMsgBody
) extends BbbCoreMsg
case class ScreenBroadcastStopSysMsgBody(
meetingId: String,
voiceConf: String,
streamId: String
)
/**
* Sent to FS to eject all users from the voice conference.
*/
@ -184,7 +218,7 @@ case class EjectUserFromVoiceCmdMsg(
header: BbbClientMsgHeader,
body: EjectUserFromVoiceCmdMsgBody
) extends StandardMsg
case class EjectUserFromVoiceCmdMsgBody(userId: String, ejectedBy: String)
case class EjectUserFromVoiceCmdMsgBody(userId: String, ejectedBy: String, banUser: Boolean)
/**
* Sent by client to mute all users except presenters in the voice conference.

View File

@ -61,6 +61,7 @@ case class GetCamBroadcastPermissionReqMsg(
case class GetCamBroadcastPermissionReqMsgBody(
meetingId: String,
userId: String,
streamId: String,
sfuSessionId: String
)
@ -74,6 +75,7 @@ case class GetCamBroadcastPermissionRespMsg(
case class GetCamBroadcastPermissionRespMsgBody(
meetingId: String,
userId: String,
streamId: String,
sfuSessionId: String,
allowed: Boolean
)

View File

@ -46,6 +46,7 @@ public class ApiParams {
public static final String MUTE_ON_START = "muteOnStart";
public static final String MEETING_KEEP_EVENTS = "meetingKeepEvents";
public static final String ALLOW_MODS_TO_UNMUTE_USERS = "allowModsToUnmuteUsers";
public static final String ALLOW_REQUESTS_WITHOUT_SESSION = "allowRequestsWithoutSession";
public static final String ALLOW_MODS_TO_EJECT_CAMERAS = "allowModsToEjectCameras";
public static final String NAME = "name";
public static final String PARENT_MEETING_ID = "parentMeetingID";
@ -58,6 +59,7 @@ public class ApiParams {
public static final String WEB_VOICE = "webVoice";
public static final String LEARNING_DASHBOARD_ENABLED = "learningDashboardEnabled";
public static final String LEARNING_DASHBOARD_CLEANUP_DELAY_IN_MINUTES = "learningDashboardCleanupDelayInMinutes";
public static final String VIRTUAL_BACKGROUNDS_DISABLED = "virtualBackgroundsDisabled";
public static final String WEBCAMS_ONLY_FOR_MODERATOR = "webcamsOnlyForModerator";
public static final String WELCOME = "welcome";
public static final String HTML5_INSTANCE_ID = "html5InstanceId";

View File

@ -23,6 +23,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileOutputStream;
import java.util.Calendar;
public class LearningDashboardService {
private static Logger log = LoggerFactory.getLogger(LearningDashboardService.class);
@ -58,6 +59,10 @@ public class LearningDashboardService {
}
public void removeJsonDataFile(String meetingId, int cleanUpDelayMinutes) {
Calendar cleanUpDelayCalendar = Calendar.getInstance();
cleanUpDelayCalendar.add(Calendar.MINUTE, cleanUpDelayMinutes);
//Delay `cleanUpDelayMinutes` then moderators can open the Dashboard before files has been removed
new java.util.Timer().schedule(
new java.util.TimerTask() {
@ -67,8 +72,7 @@ public class LearningDashboardService {
LearningDashboardService.deleteDirectory(ldMeetingFilesDir);
log.info("Learning Dashboard files removed for meeting {}.",meetingId);
}
},
(cleanUpDelayMinutes * 60) * 1000
}, cleanUpDelayCalendar.getTime()
);
}

View File

@ -178,6 +178,17 @@ public class MeetingService implements MessageListener {
return sessions.get(token);
}
public Boolean getAllowRequestsWithoutSession(String token) {
UserSession us = getUserSessionWithAuthToken(token);
if (us == null) {
return false;
} else {
Meeting meeting = getMeeting(us.meetingID);
if (meeting == null || meeting.isForciblyEnded()) return false;
return meeting.getAllowRequestsWithoutSession();
}
}
public UserSession removeUserSessionWithAuthToken(String token) {
UserSession user = sessions.remove(token);
if (user != null) {
@ -418,7 +429,7 @@ public class MeetingService implements MessageListener {
m.getUserActivitySignResponseDelayInMinutes(), m.getEndWhenNoModerator(), m.getEndWhenNoModeratorDelayInMinutes(),
m.getMuteOnStart(), m.getAllowModsToUnmuteUsers(), m.getAllowModsToEjectCameras(), m.getMeetingKeepEvents(),
m.breakoutRoomsParams,
m.lockSettingsParams, m.getHtml5InstanceId());
m.lockSettingsParams, m.getHtml5InstanceId(), m.getVirtualBackgroundsDisabled());
}
private String formatPrettyDate(Long timestamp) {
@ -753,6 +764,41 @@ public class MeetingService implements MessageListener {
}
}
public Map<String, String> getUserCustomData(
Meeting meeting,
String externUserID,
Map<String, String> params) {
Map<String, String> resp = paramsProcessorUtil.getUserCustomData(params);
// If is breakout room, merge with user's parent meeting userdata
if (meeting.isBreakout()) {
String parentMeetingId = meeting.getParentMeetingId();
Meeting parentMeeting = getMeeting(parentMeetingId);
if (parentMeeting != null) {
// Get parent meeting user's internal id from it's breakout external id
// parentUserInternalId-breakoutRoomNumber
String parentUserId = externUserID.split("-")[0];
User parentUser = parentMeeting.getUserById(parentUserId);
if (parentUser != null) {
// Custom data is stored indexed by user's external id
Map<String, Object> customData = parentMeeting.getUserCustomData(parentUser.getExternalUserId());
if (customData != null) {
for (String key : customData.keySet()) {
if (!resp.containsKey(key)) {
resp.put(key, String.valueOf(customData.get(key)));
}
}
}
}
}
}
return resp;
}
private void meetingStarted(MeetingStarted message) {
Meeting m = getMeeting(message.meetingId);
if (m != null) {

View File

@ -72,7 +72,7 @@ public class ParamsProcessorUtil {
private int defaultNumDigitsForTelVoice;
private String defaultHTML5ClientUrl;
private String defaultGuestWaitURL;
private Boolean allowRequestsWithoutSession;
private Boolean allowRequestsWithoutSession = false;
private Boolean useDefaultAvatar = false;
private String defaultAvatarURL;
private String defaultGuestPolicy;
@ -460,6 +460,14 @@ public class ParamsProcessorUtil {
learningDashboardAccessToken = RandomStringUtils.randomAlphanumeric(12).toLowerCase();
}
// Check if VirtualBackgrounds is disabled
boolean virtualBackgroundsDisabled = false;
if (!StringUtils.isEmpty(params.get(ApiParams.VIRTUAL_BACKGROUNDS_DISABLED))) {
virtualBackgroundsDisabled = Boolean.valueOf(params.get(ApiParams.VIRTUAL_BACKGROUNDS_DISABLED));
}
boolean webcamsOnlyForMod = webcamsOnlyForModerator;
if (!StringUtils.isEmpty(params.get(ApiParams.WEBCAMS_ONLY_FOR_MODERATOR))) {
try {
@ -552,6 +560,7 @@ public class ParamsProcessorUtil {
.withWelcomeMessage(welcomeMessage).isBreakout(isBreakout)
.withGuestPolicy(guestPolicy)
.withAuthenticatedGuest(authenticatedGuest)
.withAllowRequestsWithoutSession(allowRequestsWithoutSession)
.withMeetingLayout(meetingLayout)
.withBreakoutRoomsParams(breakoutParams)
.withLockSettingsParams(lockSettingsParams)
@ -560,6 +569,7 @@ public class ParamsProcessorUtil {
.withLearningDashboardEnabled(learningDashboardEn)
.withLearningDashboardCleanupDelayInMinutes(learningDashboardCleanupMins)
.withLearningDashboardAccessToken(learningDashboardAccessToken)
.withVirtualBackgroundsDisabled(virtualBackgroundsDisabled)
.build();
if (!StringUtils.isEmpty(params.get(ApiParams.MODERATOR_ONLY_MESSAGE))) {
@ -624,6 +634,10 @@ public class ParamsProcessorUtil {
}
meeting.setAllowModsToUnmuteUsers(allowModsToUnmuteUsers);
if (!StringUtils.isEmpty(params.get(ApiParams.ALLOW_REQUESTS_WITHOUT_SESSION))) {
meeting.setAllowRequestsWithoutSession(Boolean.parseBoolean(params.get(ApiParams.ALLOW_REQUESTS_WITHOUT_SESSION)));
}
Boolean allowModsToEjectCameras = defaultAllowModsToEjectCameras;
if (!StringUtils.isEmpty(params.get(ApiParams.ALLOW_MODS_TO_EJECT_CAMERAS))) {
allowModsToEjectCameras = Boolean.parseBoolean(params.get(ApiParams.ALLOW_MODS_TO_EJECT_CAMERAS));

View File

@ -54,6 +54,7 @@ public class Meeting {
private Boolean learningDashboardEnabled;
private int learningDashboardCleanupDelayInMinutes;
private String learningDashboardAccessToken;
private Boolean virtualBackgroundsDisabled;
private String welcomeMsgTemplate;
private String welcomeMsg;
private String modOnlyMessage = "";
@ -86,6 +87,7 @@ public class Meeting {
private String customCopyright = "";
private Boolean muteOnStart = false;
private Boolean allowModsToUnmuteUsers = false;
private Boolean allowRequestsWithoutSession = false;
private Boolean allowModsToEjectCameras = false;
private Boolean meetingKeepEvents;
@ -113,6 +115,7 @@ public class Meeting {
viewerPass = builder.viewerPass;
moderatorPass = builder.moderatorPass;
learningDashboardEnabled = builder.learningDashboardEnabled;
virtualBackgroundsDisabled = builder.virtualBackgroundsDisabled;
learningDashboardCleanupDelayInMinutes = builder.learningDashboardCleanupDelayInMinutes;
learningDashboardAccessToken = builder.learningDashboardAccessToken;
maxUsers = builder.maxUsers;
@ -136,6 +139,7 @@ public class Meeting {
isBreakout = builder.isBreakout;
guestPolicy = builder.guestPolicy;
authenticatedGuest = builder.authenticatedGuest;
allowRequestsWithoutSession = builder.allowRequestsWithoutSession;
meetingLayout = builder.meetingLayout;
breakoutRoomsParams = builder.breakoutRoomsParams;
lockSettingsParams = builder.lockSettingsParams;
@ -345,6 +349,10 @@ public class Meeting {
return learningDashboardAccessToken;
}
public Boolean getVirtualBackgroundsDisabled() {
return virtualBackgroundsDisabled;
}
public String getWelcomeMessageTemplate() {
return welcomeMsgTemplate;
}
@ -515,6 +523,14 @@ public class Meeting {
return allowModsToUnmuteUsers;
}
public void setAllowRequestsWithoutSession(Boolean value) {
allowRequestsWithoutSession = value;
}
public Boolean getAllowRequestsWithoutSession() {
return allowRequestsWithoutSession;
}
public void setAllowModsToEjectCameras(Boolean value) {
allowModsToEjectCameras = value;
}
@ -744,6 +760,7 @@ public class Meeting {
private Boolean learningDashboardEnabled;
private int learningDashboardCleanupDelayInMinutes;
private String learningDashboardAccessToken;
private Boolean virtualBackgroundsDisabled;
private int duration;
private String webVoice;
private String telVoice;
@ -760,6 +777,7 @@ public class Meeting {
private boolean isBreakout;
private String guestPolicy;
private Boolean authenticatedGuest;
private Boolean allowRequestsWithoutSession;
private String meetingLayout;
private BreakoutRoomsParams breakoutRoomsParams;
private LockSettingsParams lockSettingsParams;
@ -849,6 +867,11 @@ public class Meeting {
return this;
}
public Builder withVirtualBackgroundsDisabled(Boolean d) {
this.virtualBackgroundsDisabled = d;
return this;
}
public Builder withWelcomeMessage(String w) {
welcomeMsg = w;
return this;
@ -904,6 +927,11 @@ public class Meeting {
return this;
}
public Builder withAllowRequestsWithoutSession(Boolean value) {
allowRequestsWithoutSession = value;
return this;
}
public Builder withMeetingLayout(String layout) {
meetingLayout = layout;
return this;

View File

@ -34,7 +34,7 @@ public interface IBbbWebApiGWApp {
Boolean keepEvents,
BreakoutRoomsParams breakoutParams,
LockSettingsParams lockSettingsParams,
Integer html5InstanceId);
Integer html5InstanceId, Boolean virtualBackgroundsDisabled);
void registerUser(String meetingID, String internalUserId, String fullname, String role,
String externUserID, String authToken, String avatarURL,

View File

@ -144,7 +144,8 @@ class BbbWebApiGWApp(
keepEvents: java.lang.Boolean,
breakoutParams: BreakoutRoomsParams,
lockSettingsParams: LockSettingsParams,
html5InstanceId: java.lang.Integer): Unit = {
html5InstanceId: java.lang.Integer,
virtualBackgroundsDisabled: java.lang.Boolean): Unit = {
val meetingProp = MeetingProp(name = meetingName, extId = extMeetingId, intId = meetingId,
isBreakout = isBreakout.booleanValue(), learningDashboardEnabled = learningDashboardEnabled.booleanValue())
@ -180,7 +181,7 @@ class BbbWebApiGWApp(
val usersProp = UsersProp(maxUsers = maxUsers.intValue(), webcamsOnlyForModerator = webcamsOnlyForModerator.booleanValue(),
guestPolicy = guestPolicy, meetingLayout = meetingLayout, allowModsToUnmuteUsers = allowModsToUnmuteUsers.booleanValue(),
allowModsToEjectCameras = allowModsToEjectCameras.booleanValue(),
authenticatedGuest = authenticatedGuest.booleanValue())
authenticatedGuest = authenticatedGuest.booleanValue(), virtualBackgroundsDisabled = virtualBackgroundsDisabled.booleanValue())
val metadataProp = MetadataProp(mapAsScalaMap(metadata).toMap)
val screenshareProps = ScreenshareProps(
screenshareConf = voiceBridge + screenshareConfSuffix,

View File

@ -1,14 +1,16 @@
Learning Dashboard will be accessible through https://yourdomain/learning-dashboard
Learning Analytics Dashboard will be accessible through https://yourdomain/learning-analytics-dashboard
# Dev Instructions
## Prepare destination directory
```
mkdir /var/bigbluebutton/learning-dashboard
chown bigbluebutton /var/bigbluebutton/learning-dashboard/
```
## Build instructions
```
cd bbb-learning-dashboard
rm -r node_modules
@ -18,6 +20,7 @@ cp -r build/* /var/bigbluebutton/learning-dashboard
```
## Update nginx config
```
cp bbb-learning-dashboard/learning-dashboard.nginx /etc/bigbluebutton/nginx/
```

View File

@ -1,11 +0,0 @@
// craco.config.js
module.exports = {
style: {
postcss: {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
],
},
},
};

View File

@ -1,4 +1,4 @@
location /learning-dashboard/ {
location /learning-analytics-dashboard/ {
alias /var/bigbluebutton/learning-dashboard/;
autoindex off;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "learning-dashboard",
"homepage": "/learning-dashboard/",
"name": "learning-analytics-dashboard",
"homepage": "/learning-analytics-dashboard/",
"version": "0.1.0",
"private": true,
"lint-staged": {
@ -10,21 +10,20 @@
},
"dependencies": {
"@babel/core": "^7.15.0",
"@craco/craco": "^5.9.0",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-intl": "^5.20.6",
"react-scripts": "^4.0.3",
"react-scripts": "^5.0.0",
"typescript": "^4.3.5",
"web-vitals": "^1.1.2"
},
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"eslintConfig": {
@ -46,7 +45,7 @@
]
},
"devDependencies": {
"autoprefixer": "^9.8.6",
"autoprefixer": "^10.4.1",
"eslint": "^7.32.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-airbnb-base": "^14.2.1",
@ -54,7 +53,7 @@
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.2.0",
"postcss": "^7.0.36",
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.7"
"postcss": "^8.4.5",
"tailwindcss": "^3.0.11"
}
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -15,7 +15,7 @@ function Card(props) {
{ name }
</p>
</div>
<div className={`p-3 mr-4 text-orange-500 rounded-full ${iconClass}`}>
<div className={`p-3 mr-4 rounded-full ${iconClass || 'text-orange-500'}`}>
{ children }
</div>
</div>

View File

@ -1,6 +1,6 @@
module.exports = {
purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'],
darkMode: false, // or 'media' or 'class'
content: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'],
darkMode: 'media', // or 'media' or 'class'
theme: {
extend: {},
},

View File

@ -1 +1 @@
git clone --branch v2.6.4 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu
git clone --branch v2.6.9 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu

View File

@ -1 +1 @@
BIGBLUEBUTTON_RELEASE=2.4.0
BIGBLUEBUTTON_RELEASE=2.4.3

View File

@ -35,6 +35,11 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
background-color: #06172A;
}
/* language-specific font */
body.lang-fa {
font-family: Tahoma, 'Source Sans Pro', Arial, sans-serif;
}
:-webkit-full-screen {
background-color: inherit;
}
@ -101,19 +106,30 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<script src="compatibility/tflite-simd.js?v=VERSION" language="javascript"></script>
<script src="compatibility/tflite.js?v=VERSION" language="javascript"></script>
<!-- fonts -->
<link rel="preload" href="fonts/BbbIcons/bbb-icons.woff?j1ntjp" as="font" crossorigin="anonymous"/>
<link rel="preload" href="fonts/SourceSansPro/SourceSansPro-Light.woff" as="font" crossorigin="anonymous"/>
<link rel="preload" href="fonts/SourceSansPro/SourceSansPro-Regular.woff" as="font" crossorigin="anonymous"/>
<link rel="preload" href="fonts/SourceSansPro/SourceSansPro-Semibold.woff" as="font" crossorigin="anonymous"/>
<link rel="preload" href="fonts/SourceSansPro/SourceSansPro-Bold.woff" as="font" crossorigin="anonymous"/>
<link rel="preload" href="fonts/SourceSansPro/SourceSansPro-LightItalic.woff" as="font" crossorigin="anonymous"/>
<link rel="preload" href="fonts/SourceSansPro/SourceSansPro-Italic.woff" as="font" crossorigin="anonymous"/>
<link rel="preload" href="fonts/SourceSansPro/SourceSansPro-SemiboldItalic.woff" as="font" crossorigin="anonymous"/>
<link rel="preload" href="fonts/SourceSansPro/SourceSansPro-BoldItalic.woff" as="font" crossorigin="anonymous"/>
<link rel="preload" href="fonts/BbbIcons/bbb-icons.woff?v=VERSION" as="font" crossorigin="anonymous"/>
<link rel="preload" href="fonts/SourceSansPro/SourceSansPro-Light.woff?v=VERSION" as="font" crossorigin="anonymous"/>
<link rel="preload" href="fonts/SourceSansPro/SourceSansPro-Regular.woff?v=VERSION" as="font" crossorigin="anonymous"/>
<link rel="preload" href="fonts/SourceSansPro/SourceSansPro-Semibold.woff?v=VERSION" as="font" crossorigin="anonymous"/>
<link rel="preload" href="fonts/SourceSansPro/SourceSansPro-Bold.woff?v=VERSION" as="font" crossorigin="anonymous"/>
<link rel="preload" href="fonts/SourceSansPro/SourceSansPro-LightItalic.woff?v=VERSION" as="font" crossorigin="anonymous"/>
<link rel="preload" href="fonts/SourceSansPro/SourceSansPro-Italic.woff?v=VERSION" as="font" crossorigin="anonymous"/>
<link rel="preload" href="fonts/SourceSansPro/SourceSansPro-SemiboldItalic.woff?v=VERSION" as="font" crossorigin="anonymous"/>
<link rel="preload" href="fonts/SourceSansPro/SourceSansPro-BoldItalic.woff?v=VERSION" as="font" crossorigin="anonymous"/>
<style>
@font-face {
font-family: 'bbb-icons';
src: url('fonts/BbbIcons/bbb-icons.woff?v=VERSION') format('woff');
font-weight: normal;
font-style: normal;
}
</style>
<!-- fonts -->
</head>
<body style="background-color: #06172A">
<div id="app" role="document"></div>
<div id="aria-polite-alert" aria-live="polite" aria-atomic="false" class="sr-only"></div>
<div id="app" role="document">
</div>
<span id="destination"></span>
<audio id="remote-media" autoplay>
</audio>

View File

@ -1,10 +1,3 @@
@font-face {
font-family: 'bbb-icons';
src: url('/fonts/BbbIcons/bbb-icons.woff?j1ntjp') format('woff');
font-weight: normal;
font-style: normal;
}
[class^="icon-bbb-"], [class*=" icon-bbb-"] {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: 'bbb-icons' !important;

View File

@ -31,9 +31,9 @@ const IPV4_FALLBACK_DOMAIN = Meteor.settings.public.app.ipv4FallbackDomain;
const CALL_CONNECT_TIMEOUT = 20000;
const ICE_NEGOTIATION_TIMEOUT = 20000;
const AUDIO_SESSION_NUM_KEY = 'AudioSessionNumber';
const USER_AGENT_RECONNECTION_ATTEMPTS = 3;
const USER_AGENT_RECONNECTION_DELAY_MS = 5000;
const USER_AGENT_CONNECTION_TIMEOUT_MS = 5000;
const USER_AGENT_RECONNECTION_ATTEMPTS = MEDIA.audioReconnectionAttempts || 3;
const USER_AGENT_RECONNECTION_DELAY_MS = MEDIA.audioReconnectionDelay || 5000;
const USER_AGENT_CONNECTION_TIMEOUT_MS = MEDIA.audioConnectionTimeout || 5000;
const ICE_GATHERING_TIMEOUT = MEDIA.iceGatheringTimeout || 5000;
const BRIDGE_NAME = 'sip';
const WEBSOCKET_KEEP_ALIVE_INTERVAL = MEDIA.websocketKeepAliveInterval || 0;

View File

@ -17,6 +17,7 @@ export default function upsertValidationState(meetingId, userId, validationStatu
};
try {
AuthTokenValidation.remove({ meetingId, userId, connectionId: { $ne: connectionId } });
const { numberAffected } = AuthTokenValidation.upsert(selector, modifier);
if (numberAffected) {

View File

@ -1,6 +1,7 @@
import ConnectionStatus from '/imports/api/connection-status';
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import changeHasConnectionStatus from '/imports/api/users-persistent-data/server/modifiers/changeHasConnectionStatus';
export default function updateConnectionStatus(meetingId, userId, level) {
check(meetingId, String);
@ -24,6 +25,7 @@ export default function updateConnectionStatus(meetingId, userId, level) {
const { numberAffected } = ConnectionStatus.upsert(selector, modifier);
if (numberAffected) {
changeHasConnectionStatus(true, userId, meetingId);
Logger.verbose(`Updated connection status meetingId=${meetingId} userId=${userId} level=${level}`);
}
} catch (err) {

View File

@ -2,6 +2,7 @@ import { Match, check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import { GroupChatMsg } from '/imports/api/group-chat-msg';
import { BREAK_LINE } from '/imports/utils/lineEndings';
import changeHasMessages from '/imports/api/users-persistent-data/server/modifiers/changeHasMessages';
export function parseMessage(message) {
let parsedMessage = message || '';
@ -46,6 +47,7 @@ export default function addGroupChatMsg(meetingId, chatId, msg) {
const insertedId = GroupChatMsg.insert(msgDocument);
if (insertedId) {
changeHasMessages(true, sender.id, meetingId);
Logger.info(`Added group-chat-msg msgId=${msg.id} chatId=${chatId} meetingId=${meetingId}`);
}
} catch (err) {

View File

@ -63,6 +63,7 @@ export default function addMeeting(meeting) {
allowModsToUnmuteUsers: Boolean,
allowModsToEjectCameras: Boolean,
meetingLayout: String,
virtualBackgroundsDisabled: Boolean,
},
durationProps: {
createdTime: Number,

View File

@ -1,5 +1,4 @@
import { check } from 'meteor/check';
import Polls from '/imports/api/polls';
import RedisPubSub from '/imports/startup/server/redis';
export default function userTypedResponse({ header, body }) {
@ -7,21 +6,13 @@ export default function userTypedResponse({ header, body }) {
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'RespondToPollReqMsg';
const { pollId, userId, answer } = body;
const { pollId, userId, answerId } = body;
const { meetingId } = header;
check(pollId, String);
check(meetingId, String);
check(userId, String);
check(answer, String);
const poll = Polls.findOne({ meetingId, id: pollId });
let answerId = 0;
poll.answers.forEach((a) => {
const { id, key } = a;
if (key === answer) answerId = id;
});
check(answerId, Number);
const payload = {
requesterId: userId,

View File

@ -1,6 +1,8 @@
import RedisPubSub from '/imports/startup/server/redis';
import handleScreenshareStarted from './handlers/screenshareStarted';
import handleScreenshareStopped from './handlers/screenshareStopped';
import handleScreenshareSync from './handlers/screenshareSync';
RedisPubSub.on('ScreenshareRtmpBroadcastStartedEvtMsg', handleScreenshareStarted);
RedisPubSub.on('ScreenshareRtmpBroadcastStoppedEvtMsg', handleScreenshareStopped);
RedisPubSub.on('SyncGetScreenshareInfoRespMsg', handleScreenshareSync);

View File

@ -0,0 +1,19 @@
import { check } from 'meteor/check';
import addScreenshare from '../modifiers/addScreenshare';
import clearScreenshare from '../modifiers/clearScreenshare';
export default function handleScreenshareSync({ body }, meetingId) {
check(meetingId, String);
check(body, Object);
const { isBroadcasting, screenshareConf } = body;
check(screenshareConf, String);
check(isBroadcasting, Boolean);
if (!isBroadcasting) {
return clearScreenshare(meetingId, screenshareConf);
}
return addScreenshare(meetingId, body);
}

View File

@ -0,0 +1,25 @@
import Logger from '/imports/startup/server/logger';
import UsersPersistentData from '/imports/api/users-persistent-data';
export default function changeHasConnectionStatus(hasConnectionStatus, userId, meetingId) {
const selector = {
meetingId,
userId,
};
const modifier = {
$set: {
'shouldPersist.hasConnectionStatus': hasConnectionStatus,
},
};
try {
const numberAffected = UsersPersistentData.update(selector, modifier);
if (numberAffected) {
Logger.info(`Changed hasConnectionStatus=${hasConnectionStatus} id=${userId} meeting=${meetingId}`);
}
} catch (err) {
Logger.error(`Change hasConnectionStatus error: ${err}`);
}
}

View File

@ -0,0 +1,25 @@
import Logger from '/imports/startup/server/logger';
import UsersPersistentData from '/imports/api/users-persistent-data';
export default function changeHasMessages(hasMessages, userId, meetingId) {
const selector = {
meetingId,
userId,
};
const modifier = {
$set: {
'shouldPersist.hasMessages': hasMessages,
},
};
try {
const numberAffected = UsersPersistentData.update(selector, modifier);
if (numberAffected) {
Logger.info(`Changed hasMessages=${hasMessages} id=${userId} meeting=${meetingId}`);
}
} catch (err) {
Logger.error(`Change hasMessages error: ${err}`);
}
}

View File

@ -2,6 +2,9 @@ import UsersPersistentData from '/imports/api/users-persistent-data';
import { Meteor } from 'meteor/meteor';
import { extractCredentials } from '/imports/api/common/server/helpers';
import { check } from 'meteor/check';
import Users from '/imports/api/users';
const ROLE_VIEWER = Meteor.settings.public.user.role_viewer;
function usersPersistentData() {
if (!this.userId) {
@ -16,6 +19,23 @@ function usersPersistentData() {
meetingId,
};
const User = Users.findOne({ userId: requesterUserId, meetingId }, { fields: { role: 1 } });
if (!!User && User.role === ROLE_VIEWER) {
// viewers are allowed to see other users' data if:
// user is logged in or user sent a message in chat
const viewerSelector = {
meetingId,
$or: [
{
'shouldPersist.hasMessages': true,
},
{
loggedOut: false,
},
],
};
return UsersPersistentData.find(viewerSelector);
}
return UsersPersistentData.find(selector);
}

View File

@ -2,7 +2,6 @@ import { Meteor } from 'meteor/meteor';
import UserSettings from '/imports/api/users-settings';
import Logger from '/imports/startup/server/logger';
import AuthTokenValidation, { ValidationStates } from '/imports/api/auth-token-validation';
import User from '/imports/api/users';
function userSettings() {
const tokenValidation = AuthTokenValidation.findOne({ connectionId: this.connection.id });
@ -14,34 +13,6 @@ function userSettings() {
const { meetingId, userId } = tokenValidation;
const currentUser = User.findOne({ userId, meetingId });
if (currentUser && currentUser?.breakoutProps?.isBreakoutUser) {
const { parentId } = currentUser.breakoutProps;
const [externalId] = currentUser.extId.split('-');
const mainRoomUserSettings = UserSettings.find({ meetingId: parentId, userId: externalId });
mainRoomUserSettings.map(({ setting, value }) => ({
meetingId,
setting,
userId,
value,
})).forEach((doc) => {
const selector = {
meetingId,
setting: doc.setting,
};
UserSettings.upsert(selector, doc);
});
Logger.debug('Publishing UserSettings', { meetingId, userId });
return UserSettings.find({ meetingId, userId });
}
Logger.debug('Publishing UserSettings', { meetingId, userId });
return UserSettings.find({ meetingId, userId });

View File

@ -1,7 +1,6 @@
import { check } from 'meteor/check';
import addUser from '/imports/api/users/server/modifiers/addUser';
export default function addDialInUser(meetingId, voiceUser) {
check(meetingId, String);
check(voiceUser, Object);
@ -16,7 +15,7 @@ export default function addDialInUser(meetingId, voiceUser) {
extId: intId, // TODO
name: callerName,
role: ROLE_VIEWER.toLowerCase(),
guest: false,
guest: true,
authed: true,
waitingForAcceptance: false,
guestStatus: 'ALLOW',
@ -24,6 +23,7 @@ export default function addDialInUser(meetingId, voiceUser) {
presenter: false,
locked: false, // TODO
avatar: '',
pin: false,
clientType: 'dial-in-user',
};

View File

@ -5,6 +5,8 @@ import Logger from '/imports/startup/server/logger';
import setloggedOutStatus from '/imports/api/users-persistent-data/server/modifiers/setloggedOutStatus';
import clearUserInfoForRequester from '/imports/api/users-infos/server/modifiers/clearUserInfoForRequester';
import ClientConnections from '/imports/startup/server/ClientConnections';
import UsersPersistentData from '/imports/api/users-persistent-data';
import VoiceUsers from '/imports/api/voice-users/';
const clearAllSessions = (sessionUserId) => {
const serverSessions = Meteor.server.sessions;
@ -35,7 +37,15 @@ export default function removeUser(meetingId, userId) {
clearUserInfoForRequester(meetingId, userId);
const currentUser = UsersPersistentData.findOne({ userId, meetingId });
const hasMessages = currentUser?.shouldPersist?.hasMessages;
const hasConnectionStatus = currentUser?.shouldPersist?.hasConnectionStatus;
if (!hasMessages && !hasConnectionStatus) {
UsersPersistentData.remove(selector);
}
Users.remove(selector);
VoiceUsers.remove({ intId: userId, meetingId });
}
if (!process.env.BBB_HTML5_ROLE || process.env.BBB_HTML5_ROLE === 'frontend') {

View File

@ -4,7 +4,7 @@ import RedisPubSub from '/imports/startup/server/redis';
import { extractCredentials } from '/imports/api/common/server/helpers';
import Logger from '/imports/startup/server/logger';
export default function ejectUserFromVoice(userId) {
export default function ejectUserFromVoice(userId, banUser) {
try {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
@ -15,10 +15,12 @@ export default function ejectUserFromVoice(userId) {
check(meetingId, String);
check(requesterUserId, String);
check(userId, String);
check(banUser, Boolean);
const payload = {
userId,
ejectedBy: requesterUserId,
banUser,
};
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);

View File

@ -16,6 +16,7 @@ const propTypes = {
};
const DEFAULT_LANGUAGE = Meteor.settings.public.app.defaultSettings.application.fallbackLocale;
const CLIENT_VERSION = Meteor.settings.public.app.html5ClientBuild;
const RTL_LANGUAGES = ['ar', 'dv', 'fa', 'he'];
const LARGE_FONT_LANGUAGES = ['te', 'km'];
@ -73,7 +74,7 @@ class IntlStartup extends Component {
})
.then(({ normalizedLocale, regionDefaultLocale }) => {
const fetchFallbackMessages = new Promise((resolve, reject) => {
fetch(`${localesPath}/${DEFAULT_LANGUAGE}.json`)
fetch(`${localesPath}/${DEFAULT_LANGUAGE}.json?v=${CLIENT_VERSION}`)
.then((response) => {
if (!response.ok) {
return reject();
@ -86,7 +87,7 @@ class IntlStartup extends Component {
if (!regionDefaultLocale) {
return resolve(false);
}
fetch(`${localesPath}/${regionDefaultLocale}.json`)
fetch(`${localesPath}/${regionDefaultLocale}.json?v=${CLIENT_VERSION}`)
.then((response) => {
if (!response.ok) {
return resolve(false);
@ -104,7 +105,7 @@ class IntlStartup extends Component {
if (!normalizedLocale || normalizedLocale === DEFAULT_LANGUAGE || normalizedLocale === regionDefaultLocale) {
return resolve(false);
}
fetch(`${localesPath}/${normalizedLocale}.json`)
fetch(`${localesPath}/${normalizedLocale}.json?v=${CLIENT_VERSION}`)
.then((response) => {
if (!response.ok) {
return resolve(false);

View File

@ -49,7 +49,7 @@ class MeetingMessageQueue {
constructor(eventEmitter, asyncMessages = [], redisDebugEnabled = false) {
this.asyncMessages = asyncMessages;
this.emitter = eventEmitter;
this.queue = queue({ autostart: true });
this.queue = queue({ autostart: true, concurrency: 1 });
this.redisDebugEnabled = redisDebugEnabled;
this.handleTask = this.handleTask.bind(this);

View File

@ -142,7 +142,7 @@ class ActionsDropdown extends PureComponent {
if (amIPresenter && !hidePresentation) {
actions.push({
icon: "presentation",
dataTest: "uploadPresentation",
dataTest: "managePresentations",
label: formatMessage(presentationLabel),
key: this.presentationItemId,
onClick: handlePresentationClick,
@ -189,6 +189,7 @@ class ActionsDropdown extends PureComponent {
: intl.formatMessage(intlMessages.stopExternalVideoLabel),
key: "external-video",
onClick: isSharingVideo ? stopExternalVideoShare : this.handleExternalVideoClick,
dataTest: "shareExternalVideo",
})
}
@ -272,6 +273,7 @@ class ActionsDropdown extends PureComponent {
className={isDropdownOpen ? styles.hideDropdownButton : ''}
hideLabel
aria-label={intl.formatMessage(intlMessages.actionsLabel)}
data-test="actionsButton"
label={intl.formatMessage(intlMessages.actionsLabel)}
icon="plus"
color="primary"

View File

@ -356,6 +356,7 @@ input[type="number"]::-webkit-outer-spin-button, input[type="number"]::-webkit-i
.checkBoxesContainer {
@extend %flex-row;
margin-top: 2rem;
margin-bottom: 0.25rem;
}
.withError {

View File

@ -67,6 +67,7 @@ const PresentationOptionsContainer = ({
id="restore-presentation"
ghost={isLayoutSwapped}
disabled={!isThereCurrentPresentation}
data-test={isLayoutSwapped ? 'restorePresentation' : 'minimizePresentation'}
/>
);
};

View File

@ -172,6 +172,7 @@ class QuickPollDropdown extends Component {
}}
size="lg"
disabled={!!activePoll}
data-test="quickPollBtn"
/>
);

View File

@ -169,13 +169,16 @@ const ScreenshareButton = ({
&& !isMobile
&& amIPresenter;
const dataTest = !screenshareDataSavingSetting ? 'screenshareLocked'
: isVideoBroadcasting ? 'stopScreenShare' : 'startScreenShare';
return shouldAllowScreensharing
? (
<Button
className={cx(isVideoBroadcasting || styles.btn)}
disabled={(!isMeteorConnected && !isVideoBroadcasting) || !screenshareDataSavingSetting}
icon={isVideoBroadcasting ? 'desktop' : 'desktop_off'}
data-test={isVideoBroadcasting ? 'stopScreenShare' : 'startScreenShare'}
data-test={dataTest}
label={intl.formatMessage(vLabel)}
description={intl.formatMessage(vDescr)}
color={isVideoBroadcasting ? 'primary' : 'default'}

View File

@ -197,6 +197,8 @@ class App extends Component {
body.classList.add(`os-${osName.split(' ').shift().toLowerCase()}`);
body.classList.add(`lang-${locale.split('-')[0]}`);
if (!validIOSVersion()) {
notify(
intl.formatMessage(intlMessages.iOSWarning), 'error', 'warning',

View File

@ -19,7 +19,6 @@
.layout {
@extend %flex-column;
background-color: #06172a;
}
.navbar {

View File

@ -199,6 +199,7 @@ class AudioControls extends PureComponent {
size="lg"
circle
accessKey={shortcuts.togglemute}
data-test="toggleMicrophoneButton"
/>
);

View File

@ -178,6 +178,8 @@ class AudioModal extends Component {
if (forceListenOnlyAttendee || audioLocked) return this.handleJoinListenOnly();
if (joinFullAudioImmediately && !listenOnlyMode) return this.handleJoinMicrophone();
if (!listenOnlyMode) return this.handleGoToEchoTest();
}
return false;
}
@ -382,13 +384,14 @@ class AudioModal extends Component {
return (
<div>
<span className={styles.audioOptions}>
<span className={styles.audioOptions} data-test="audioModalOptions">
{!showMicrophone && !isMobileNative
&& (
<>
<Button
className={styles.audioBtn}
label={intl.formatMessage(intlMessages.microphoneLabel)}
data-test="microphoneBtn"
aria-describedby="mic-description"
icon="unmute"
circle
@ -411,6 +414,7 @@ class AudioModal extends Component {
<Button
className={styles.audioBtn}
label={intl.formatMessage(intlMessages.listenOnlyLabel)}
data-test="listenOnlyBtn"
aria-describedby="listenOnly-description"
icon="listen"
circle
@ -569,6 +573,7 @@ class AudioModal extends Component {
className={styles.modal}
onRequestClose={closeModal}
hideBorder
data-test="audioModal"
contentLabel={intl.formatMessage(intlMessages.ariaModalTitle)}
>
{isIE ? (
@ -586,10 +591,7 @@ class AudioModal extends Component {
{
!this.skipAudioOptions()
? (
<header
data-test="audioModalHeader"
className={styles.header}
>
<header className={styles.header}>
{
isIOSChrome ? null
: (

View File

@ -64,6 +64,7 @@ class EchoTest extends Component {
className={styles.button}
label={intl.formatMessage(intlMessages.confirmLabel)}
aria-label={intl.formatMessage(intlMessages.confirmAriaLabel)}
data-test="echoYesBtn"
icon="thumbs_up"
disabled={disabled}
circle

View File

@ -41,7 +41,9 @@ class AuthenticatedHandler extends Component {
AuthenticatedHandler.addReconnectObservable();
const setReason = (reason) => {
logger.error({
const log = reason.error === 403 ? 'warn' : 'error';
logger[log]({
logCode: 'authenticatedhandlercomponent_setreason',
extraInfo: { reason },
}, 'Encountered error while trying to authenticate');

View File

@ -96,6 +96,7 @@ class BreakoutRoom extends PureComponent {
super(props);
this.renderBreakoutRooms = this.renderBreakoutRooms.bind(this);
this.getBreakoutURL = this.getBreakoutURL.bind(this);
this.hasBreakoutUrl = this.hasBreakoutUrl.bind(this);
this.getBreakoutLabel = this.getBreakoutLabel.bind(this);
this.renderDuration = this.renderDuration.bind(this);
this.transferUserToBreakoutRoom = this.transferUserToBreakoutRoom.bind(this);
@ -186,17 +187,24 @@ class BreakoutRoom extends PureComponent {
return null;
}
getBreakoutLabel(breakoutId) {
const { intl, getBreakoutRoomUrl } = this.props;
hasBreakoutUrl(breakoutId) {
const { getBreakoutRoomUrl } = this.props;
const { requestedBreakoutId, generated } = this.state;
const breakoutRoomUrlData = getBreakoutRoomUrl(breakoutId);
if (generated && requestedBreakoutId === breakoutId) {
return intl.formatMessage(intlMessages.breakoutJoin);
if ((generated && requestedBreakoutId === breakoutId) || breakoutRoomUrlData) {
return true;
}
if (breakoutRoomUrlData) {
return false;
}
getBreakoutLabel(breakoutId) {
const { intl } = this.props;
const hasBreakoutUrl = this.hasBreakoutUrl(breakoutId)
if (hasBreakoutUrl) {
return intl.formatMessage(intlMessages.breakoutJoin);
}
@ -279,6 +287,10 @@ class BreakoutRoom extends PureComponent {
const stateBreakoutId = _stateBreakoutId || currentAudioTransferBreakoutId;
const moderatorJoinedAudio = isMicrophoneUser && amIModerator;
const disable = waiting && requestedBreakoutId !== breakoutId;
const breakoutShortname = this.props.breakoutRooms[number - 1]?.shortName;
const hasBreakoutUrl = this.hasBreakoutUrl(breakoutId);
const dataTest = `${hasBreakoutUrl ? 'join' : 'askToJoin'}${breakoutShortname.replace(' ', '')}`;
const audioAction = joinedAudioOnly || isInBreakoutAudioTransfer
? () => {
setBreakoutAudioTransferStatus({
@ -286,7 +298,7 @@ class BreakoutRoom extends PureComponent {
status: AudioManager.BREAKOUT_AUDIO_TRANSFER_STATES.RETURNING,
});
this.returnBackToMeeeting(breakoutId);
return logger.debug({
return logger.info({
logCode: 'breakoutroom_return_main_audio',
extraInfo: { logType: 'user_action' },
}, 'Returning to main audio (breakout room audio closed)');
@ -297,7 +309,7 @@ class BreakoutRoom extends PureComponent {
status: AudioManager.BREAKOUT_AUDIO_TRANSFER_STATES.CONNECTED,
});
this.transferUserToBreakoutRoom(breakoutId);
return logger.debug({
return logger.info({
logCode: 'breakoutroom_join_audio_from_main_room',
extraInfo: { logType: 'user_action' },
}, 'joining breakout room audio (main room audio closed)');
@ -307,21 +319,21 @@ class BreakoutRoom extends PureComponent {
{
isUserInBreakoutRoom(joinedUsers)
? (
<span className={styles.alreadyConnected}>
<span className={styles.alreadyConnected} data-test="alreadyConnected">
{intl.formatMessage(intlMessages.alreadyConnected)}
</span>
)
: (
<Button
label={this.getBreakoutLabel(breakoutId)}
data-test="breakoutJoin"
aria-label={`${this.getBreakoutLabel(breakoutId)} ${this.props.breakoutRooms[number - 1]?.shortName }`}
data-test={dataTest}
aria-label={`${this.getBreakoutLabel(breakoutId)} ${breakoutShortname}`}
onClick={() => {
this.getBreakoutURL(breakoutId);
// leave main room's audio,
// and stops video and screenshare when joining a breakout room
exitAudio();
logger.debug({
logger.info({
logCode: 'breakoutroom_join',
extraInfo: { logType: 'user_action' },
}, 'joining breakout room closed audio in the main room');

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { Meteor } from 'meteor/meteor';
import { defineMessages, injectIntl } from 'react-intl';
import _ from 'lodash';
import injectNotify from '/imports/ui/components/toast/inject-notify/component';
import AudioService from '/imports/ui/components/audio/service';
import ChatPushAlert from './push-alert/component';
import Service from '../service';
@ -45,6 +46,14 @@ const intlMessages = defineMessages({
id: 'app.chat.clearPublicChatMessage',
description: 'message of when clear the public chat',
},
publicChatMsg: {
id: 'app.toast.chat.public',
description: 'public chat toast message title',
},
privateChatMsg: {
id: 'app.toast.chat.private',
description: 'private chat toast message title',
},
});
const ALERT_INTERVAL = 5000; // 5 seconds
@ -59,6 +68,8 @@ const ChatAlert = (props) => {
unreadMessagesByChat,
intl,
layoutContextDispatch,
chatsTracker,
notify,
} = props;
const [unreadMessagesCount, setUnreadMessagesCount] = useState(0);
@ -94,6 +105,35 @@ const ChatAlert = (props) => {
}
}, [pushAlertEnabled]);
useEffect(() => {
const keys = Object.keys(chatsTracker);
keys.forEach((key) => {
if (chatsTracker[key]?.shouldNotify) {
if (audioAlertEnabled) {
AudioService.playAlertSound(`${Meteor.settings.public.app.cdn
+ Meteor.settings.public.app.basename
+ Meteor.settings.public.app.instanceId}`
+ '/resources/sounds/notify.mp3');
}
if (pushAlertEnabled) {
notify(
key === 'MAIN-PUBLIC-GROUP-CHAT'
? intl.formatMessage(intlMessages.publicChatMsg)
: intl.formatMessage(intlMessages.privateChatMsg),
'info',
'chat',
{ autoClose: 3000 },
<div>
<div style={{ fontWeight: 700 }}>{chatsTracker[key].lastSender}</div>
<div>{chatsTracker[key].content}</div>
</div>,
true,
);
}
}
});
}, [chatsTracker]);
useEffect(() => {
if (pushAlertEnabled) {
const alertsObject = unreadMessagesByChat;
@ -198,4 +238,4 @@ const ChatAlert = (props) => {
ChatAlert.propTypes = propTypes;
ChatAlert.defaultProps = defaultProps;
export default injectIntl(ChatAlert);
export default injectNotify(injectIntl(ChatAlert));

View File

@ -1,5 +1,7 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import logger from '/imports/startup/client/logger';
import Auth from '/imports/ui/services/auth';
import ChatAlert from './component';
import LayoutContext from '../../layout/context';
import { PANELS } from '../../layout/enums';
@ -16,6 +18,15 @@ const propTypes = {
pushAlertEnabled: PropTypes.bool.isRequired,
};
// custom hook for getting previous value
function usePrevious(value) {
const ref = React.useRef();
React.useEffect(() => {
ref.current = value;
});
return ref.current;
}
const ChatAlertContainer = (props) => {
const layoutContext = useContext(LayoutContext);
const { layoutContextState, layoutContextDispatch } = layoutContext;
@ -54,9 +65,46 @@ const ChatAlertContainer = (props) => {
})
: null;
const chatsTracker = {};
if (usingChatContext.chats) {
const chatsActive = Object.entries(usingChatContext.chats);
chatsActive.forEach((c) => {
try {
if (c[0] === idChat || (c[0] === 'MAIN-PUBLIC-GROUP-CHAT' && idChat === 'public')) {
chatsTracker[c[0]] = {};
chatsTracker[c[0]].lastSender = users[Auth.meetingID][c[1]?.lastSender]?.name;
if (c[1]?.posJoinMessages || c[1]?.messageGroups) {
const m = Object.entries(c[1]?.posJoinMessages || c[1]?.messageGroups);
chatsTracker[c[0]].count = m?.length;
if (m[m.length - 1]) {
chatsTracker[c[0]].content = m[m.length - 1][1]?.message;
}
}
}
} catch (e) {
logger.error({
logCode: 'chat_alert_component_error',
}, 'Error : ', e.error);
}
});
const prevTracker = usePrevious(chatsTracker);
if (prevTracker) {
const keys = Object.keys(prevTracker);
keys.forEach((key) => {
if (chatsTracker[key]?.count > prevTracker[key]?.count) {
chatsTracker[key].shouldNotify = true;
}
});
}
}
return (
<ChatAlert
{...props}
chatsTracker={chatsTracker}
layoutContextDispatch={layoutContextDispatch}
unreadMessagesCountByChat={unreadMessagesCountByChat}
unreadMessagesByChat={unreadMessagesByChat}

View File

@ -58,10 +58,11 @@ const Chat = (props) => {
const HIDE_CHAT_AK = shortcuts.hideprivatechat;
const CLOSE_CHAT_AK = shortcuts.closeprivatechat;
const isPublicChat = chatID === PUBLIC_CHAT_ID;
ChatLogger.debug('ChatComponent::render', props);
return (
<div
data-test={chatID !== PUBLIC_CHAT_ID ? 'privateChat' : 'publicChat'}
data-test={isPublicChat ? 'publicChat' : 'privateChat'}
className={styles.chat}
>
<header className={styles.header}>
@ -86,13 +87,14 @@ const Chat = (props) => {
}}
aria-label={intl.formatMessage(intlMessages.hideChatLabel, { 0: title })}
accessKey={chatID !== 'public' ? HIDE_CHAT_AK : null}
data-test={isPublicChat ? 'hidePublicChat' : 'hidePrivateChat'}
label={title}
icon="left_arrow"
className={styles.hideBtn}
/>
</div>
{
chatID !== PUBLIC_CHAT_ID
!isPublicChat
? (
<Button
icon="close"

View File

@ -103,7 +103,7 @@ class TypingIndicator extends PureComponent {
return (
<div className={cx(style)}>
<span className={styles.typingIndicator}>{error || typingElement}</span>
<span className={styles.typingIndicator} data-test="typingIndicator">{error || typingElement}</span>
</div>
);
}

View File

@ -55,6 +55,7 @@ class TimeWindowList extends PureComponent {
this.userScrolledBack = false;
this.handleScrollUpdate = _.debounce(this.handleScrollUpdate.bind(this), 150);
this.rowRender = this.rowRender.bind(this);
this.forceCacheUpdate = this.forceCacheUpdate.bind(this);
this.systemMessagesResized = {};
this.state = {
@ -183,6 +184,13 @@ class TimeWindowList extends PureComponent {
}
}
forceCacheUpdate(index) {
if (index >= 0) {
this.cache.clear(index);
this.listRef.recomputeRowHeights(index);
}
}
rowRender({
index,
parent,
@ -220,6 +228,9 @@ class TimeWindowList extends PureComponent {
scrollArea={scrollArea}
dispatch={dispatch}
chatId={chatId}
height={style.height}
index={index}
forceCacheUpdate={this.forceCacheUpdate}
/>
</span>
</CellMeasurer>

View File

@ -39,12 +39,6 @@
margin: 0 0 0 auto;
padding: 0 0 0 var(--md-padding-x);
}
&:after {
content: "";
display: block;
height: var(--md-padding-x);
}
}
.unreadButton {

View File

@ -57,7 +57,24 @@ const intlMessages = defineMessages({
});
class TimeWindowChatItem extends PureComponent {
constructor(props) {
super(props);
this.state = {
forcedUpdateCount: 0,
};
}
componentDidUpdate(prevProps, prevState) {
const { height, forceCacheUpdate, systemMessage, index } = this.props;
const elementHeight = this.itemRef ? this.itemRef.clientHeight : null;
if (systemMessage && elementHeight && height !== 'auto' && elementHeight !== height && this.state.forcedUpdateCount < 10) {
// forceCacheUpdate() internally calls forceUpdate(), so we need a stop flag
// and cannot rely on shouldComponentUpdate() and other comparisons.
forceCacheUpdate(index);
this.setState(({ forcedUpdateCount }) => ({ forcedUpdateCount: forcedUpdateCount + 1 }));
}
ChatLogger.debug('TimeWindowChatItem::componentDidUpdate::props', { ...this.props }, { ...prevProps });
ChatLogger.debug('TimeWindowChatItem::componentDidUpdate::state', { ...this.state }, { ...prevState });
}
@ -86,7 +103,10 @@ class TimeWindowChatItem extends PureComponent {
}
return (
<div className={styles.item} key={`time-window-chat-item-${messageKey}`}>
<div
className={styles.item}
key={`time-window-chat-item-${messageKey}`}
ref={element => this.itemRef = element} >
<div className={styles.messages}>
{messages.map(message => (
message.text !== ''

View File

@ -458,7 +458,7 @@ class ConnectionStatusComponent extends PureComponent {
}
return (
<div className={styles.networkDataContainer}>
<div className={styles.networkDataContainer} data-test="networkDataContainer">
<div className={styles.networkData}>
{`${audioLabel}: ${audioCurrentUploadRate} k`}
</div>
@ -528,6 +528,7 @@ class ConnectionStatusComponent extends PureComponent {
onRequestClose={() => closeModal(dataSaving, intl)}
hideBorder
contentLabel={intl.formatMessage(intlMessages.ariaTitle)}
data-test="connectionStatusModal"
>
<div className={styles.container}>
<div className={styles.header}>

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import logger from '/imports/startup/client/logger';
const propTypes = {
children: PropTypes.element.isRequired,
@ -17,6 +18,10 @@ class ErrorBoundary extends Component {
error,
errorInfo,
});
logger.error({
logCode: 'Error_Boundary_wrapper',
extraInfo: { error, errorInfo },
}, 'generic error boundary logger');
}
render() {
@ -29,7 +34,7 @@ class ErrorBoundary extends Component {
ErrorBoundary.propTypes = propTypes;
export const withErrorBoundary = (WrappedComponent, FallbackComponent) => props => (
export const withErrorBoundary = (WrappedComponent, FallbackComponent) => (props) => (
<ErrorBoundary Fallback={FallbackComponent}>
<WrappedComponent {...props} />
</ErrorBoundary>

View File

@ -56,9 +56,10 @@ const defaultProps = {
class ErrorScreen extends PureComponent {
componentDidMount() {
const { code } = this.props;
const log = code === 403 ? 'warn' : 'error';
AudioManager.exitAudio();
Meteor.disconnect();
logger.error({ logCode: 'startup_client_usercouldnotlogin_error' }, `User could not log in HTML5, hit ${code}`);
logger[log]({ logCode: 'startup_client_usercouldnotlogin_error' }, `User could not log in HTML5, hit ${code}`);
}
render() {

View File

@ -134,6 +134,7 @@ class ExternalVideoModal extends Component {
label={intl.formatMessage(intlMessages.start)}
onClick={this.startWatchingHandler}
disabled={startDisabled}
data-test="startNewVideo"
/>
</div>
</Modal>

View File

@ -20,7 +20,7 @@
position: relative;
bottom: 3.5em;
left: 1em;
padding: 0.25rem 0.5rem;
min-width: 200px;
background-color: rgba(0,0,0,0.5);
border-radius: 32px;

View File

@ -165,7 +165,7 @@ class CustomLayout extends Component {
type: ACTIONS.SET_LAYOUT_INPUT,
value: _.defaultsDeep({
sidebarNavigation: {
isOpen: input.sidebarNavigation.isOpen || false,
isOpen: input.sidebarNavigation.isOpen || sidebarContentPanel !== PANELS.NONE || false,
},
sidebarContent: {
isOpen: sidebarContentPanel !== PANELS.NONE,
@ -450,9 +450,9 @@ class CustomLayout extends Component {
if (!isOpen) {
cameraDockBounds.width = mediaAreaBounds.width;
cameraDockBounds.maxWidth = mediaAreaBounds.width;
cameraDockBounds.height = mediaAreaBounds.height;
cameraDockBounds.height = mediaAreaBounds.height - bannerAreaHeight;
cameraDockBounds.maxHeight = mediaAreaBounds.height;
cameraDockBounds.top = DEFAULT_VALUES.navBarHeight;
cameraDockBounds.top = DEFAULT_VALUES.navBarHeight + bannerAreaHeight;
cameraDockBounds.left = !isRTL ? mediaAreaBounds.left : 0;
cameraDockBounds.right = isRTL ? sidebarSize : null;
} else {

View File

@ -3,7 +3,12 @@ import { throttle, defaultsDeep } from 'lodash';
import { LayoutContextFunc } from '/imports/ui/components/layout/context';
import DEFAULT_VALUES from '/imports/ui/components/layout/defaultValues';
import { INITIAL_INPUT_STATE } from '/imports/ui/components/layout/initState';
import { DEVICE_TYPE, ACTIONS, PANELS } from '/imports/ui/components/layout/enums';
import {
DEVICE_TYPE,
ACTIONS,
PANELS,
CAMERADOCK_POSITION,
} from '/imports/ui/components/layout/enums';
const windowWidth = () => window.document.documentElement.clientWidth;
const windowHeight = () => window.document.documentElement.clientHeight;
@ -99,7 +104,7 @@ class PresentationFocusLayout extends Component {
type: ACTIONS.SET_LAYOUT_INPUT,
value: defaultsDeep({
sidebarNavigation: {
isOpen: input.sidebarNavigation.isOpen || false,
isOpen: input.sidebarNavigation.isOpen || sidebarContentPanel !== PANELS.NONE || false,
},
sidebarContent: {
isOpen: sidebarContentPanel !== PANELS.NONE,
@ -395,9 +400,9 @@ class PresentationFocusLayout extends Component {
if (!isOpen) {
cameraDockBounds.width = mediaAreaBounds.width;
cameraDockBounds.maxWidth = mediaAreaBounds.width;
cameraDockBounds.height = mediaAreaBounds.height;
cameraDockBounds.height = mediaAreaBounds.height - this.bannerAreaHeight();
cameraDockBounds.maxHeight = mediaAreaBounds.height;
cameraDockBounds.top = DEFAULT_VALUES.navBarHeight;
cameraDockBounds.top = DEFAULT_VALUES.navBarHeight + this.bannerAreaHeight();
cameraDockBounds.left = !isRTL ? mediaAreaBounds.left : 0;
cameraDockBounds.right = isRTL ? sidebarSize : null;
} else {
@ -616,6 +621,7 @@ class PresentationFocusLayout extends Component {
type: ACTIONS.SET_CAMERA_DOCK_OUTPUT,
value: {
display: input.cameraDock.numCameras > 0,
position: CAMERADOCK_POSITION.SIDEBAR_CONTENT_BOTTOM,
minWidth: cameraDockBounds.minWidth,
width: cameraDockBounds.width,
maxWidth: cameraDockBounds.maxWidth,

View File

@ -101,7 +101,7 @@ class SmartLayout extends Component {
type: ACTIONS.SET_LAYOUT_INPUT,
value: _.defaultsDeep({
sidebarNavigation: {
isOpen: input.sidebarNavigation.isOpen || false,
isOpen: input.sidebarNavigation.isOpen || sidebarContentPanel !== PANELS.NONE || false,
},
sidebarContent: {
isOpen: sidebarContentPanel !== PANELS.NONE,

View File

@ -104,7 +104,7 @@ class VideoFocusLayout extends Component {
value: defaultsDeep(
{
sidebarNavigation: {
isOpen: input.sidebarNavigation.isOpen || false,
isOpen: input.sidebarNavigation.isOpen || sidebarContentPanel !== PANELS.NONE || false,
},
sidebarContent: {
isOpen: sidebarContentPanel !== PANELS.NONE,
@ -400,23 +400,26 @@ class VideoFocusLayout extends Component {
if (!isOpen) {
cameraDockBounds.width = mediaAreaBounds.width;
cameraDockBounds.maxWidth = mediaAreaBounds.width;
cameraDockBounds.height = mediaAreaBounds.height;
cameraDockBounds.height = mediaAreaBounds.height - this.bannerAreaHeight();
cameraDockBounds.maxHeight = mediaAreaBounds.height;
cameraDockBounds.top = DEFAULT_VALUES.navBarHeight;
cameraDockBounds.top = DEFAULT_VALUES.navBarHeight + this.bannerAreaHeight();
cameraDockBounds.left = !isRTL ? mediaAreaBounds.left : 0;
cameraDockBounds.right = isRTL ? sidebarSize : null;
} else {
const mobileCameraHeight = mediaAreaBounds.height * 0.7 - this.bannerAreaHeight();
const cameraHeight = mediaAreaBounds.height - this.bannerAreaHeight();
if (deviceType === DEVICE_TYPE.MOBILE) {
cameraDockBounds.minHeight = mediaAreaBounds.height * 0.7;
cameraDockBounds.height = mediaAreaBounds.height * 0.7;
cameraDockBounds.maxHeight = mediaAreaBounds.height * 0.7;
cameraDockBounds.minHeight = mobileCameraHeight;
cameraDockBounds.height = mobileCameraHeight;
cameraDockBounds.maxHeight = mobileCameraHeight;
} else {
cameraDockBounds.minHeight = mediaAreaBounds.height;
cameraDockBounds.height = mediaAreaBounds.height;
cameraDockBounds.maxHeight = mediaAreaBounds.height;
cameraDockBounds.minHeight = cameraHeight;
cameraDockBounds.height = cameraHeight;
cameraDockBounds.maxHeight = cameraHeight;
}
cameraDockBounds.top = DEFAULT_VALUES.navBarHeight;
cameraDockBounds.top = DEFAULT_VALUES.navBarHeight + this.bannerAreaHeight();
cameraDockBounds.left = !isRTL ? mediaAreaBounds.left : null;
cameraDockBounds.right = isRTL ? sidebarSize : null;
cameraDockBounds.minWidth = mediaAreaBounds.width;
@ -474,6 +477,11 @@ class VideoFocusLayout extends Component {
mediaBounds.top = sidebarContentHeight + this.bannerAreaHeight();
mediaBounds.width = sidebarContentWidth;
mediaBounds.zIndex = 1;
} else if (!input.presentation.isOpen) {
mediaBounds.width = 0;
mediaBounds.height = 0;
mediaBounds.top = 0;
mediaBounds.left = 0;
} else {
mediaBounds.height = mediaAreaBounds.height;
mediaBounds.width = mediaAreaBounds.width;

View File

@ -71,6 +71,7 @@ const FALLBACK = 'fallback';
const READY = 'ready';
const supportedBrowsers = ['Chrome', 'Firefox', 'Safari', 'Opera', 'Microsoft Edge', 'Yandex Browser'];
const DEFAULT_LANGUAGE = Meteor.settings.public.app.defaultSettings.application.fallbackLocale;
const CLIENT_VERSION = Meteor.settings.public.app.html5ClientBuild;
export default class Legacy extends Component {
constructor(props) {
@ -93,7 +94,7 @@ export default class Legacy extends Component {
return response.json();
})
.then(({ normalizedLocale, regionDefaultLocale }) => {
fetch(`${localesPath}/${DEFAULT_LANGUAGE}.json`)
fetch(`${localesPath}/${DEFAULT_LANGUAGE}.json?v=${CLIENT_VERSION}`)
.then((response) => {
if (!response.ok) {
return Promise.reject();
@ -102,7 +103,7 @@ export default class Legacy extends Component {
})
.then((messages) => {
if (regionDefaultLocale !== '') {
fetch(`${localesPath}/${regionDefaultLocale}.json`)
fetch(`${localesPath}/${regionDefaultLocale}.json?v=${CLIENT_VERSION}`)
.then((response) => {
if (!response.ok) {
return Promise.resolve();
@ -116,7 +117,7 @@ export default class Legacy extends Component {
}
if (normalizedLocale && normalizedLocale !== DEFAULT_LANGUAGE && normalizedLocale !== regionDefaultLocale) {
fetch(`${localesPath}/${normalizedLocale}.json`)
fetch(`${localesPath}/${normalizedLocale}.json?v=${CLIENT_VERSION}`)
.then((response) => {
if (!response.ok) {
return Promise.reject();

View File

@ -202,6 +202,7 @@ class LockViewersComponent extends Component {
ariaLabel={intl.formatMessage(intlMessages.webcamLabel)}
showToggleLabel={showToggleLabel}
invertColors={invertColors}
data-test="lockShareWebcam"
/>
</div>
</div>
@ -226,6 +227,7 @@ class LockViewersComponent extends Component {
ariaLabel={intl.formatMessage(intlMessages.otherViewersWebcamLabel)}
showToggleLabel={showToggleLabel}
invertColors={invertColors}
data-test="lockSeeOtherViewersWebcam"
/>
</div>
</div>
@ -250,6 +252,7 @@ class LockViewersComponent extends Component {
ariaLabel={intl.formatMessage(intlMessages.microphoneLable)}
showToggleLabel={showToggleLabel}
invertColors={invertColors}
data-test="lockShareMicrophone"
/>
</div>
</div>
@ -277,6 +280,7 @@ class LockViewersComponent extends Component {
ariaLabel={intl.formatMessage(intlMessages.publicChatLabel)}
showToggleLabel={showToggleLabel}
invertColors={invertColors}
data-test="lockPublicChat"
/>
</div>
</div>
@ -301,6 +305,7 @@ class LockViewersComponent extends Component {
ariaLabel={intl.formatMessage(intlMessages.privateChatLable)}
showToggleLabel={showToggleLabel}
invertColors={invertColors}
data-test="lockPrivateChat"
/>
</div>
</div>
@ -330,6 +335,7 @@ class LockViewersComponent extends Component {
ariaLabel={intl.formatMessage(intlMessages.notesLabel)}
showToggleLabel={showToggleLabel}
invertColors={invertColors}
data-test="lockEditSharedNotes"
/>
</div>
</div>
@ -357,6 +363,7 @@ class LockViewersComponent extends Component {
ariaLabel={intl.formatMessage(intlMessages.userListLabel)}
showToggleLabel={showToggleLabel}
invertColors={invertColors}
data-test="lockUserList"
/>
</div>
</div>
@ -366,10 +373,12 @@ class LockViewersComponent extends Component {
<div className={styles.footer}>
<div className={styles.actions}>
<Button
className={styles.buttonCancel}
label={intl.formatMessage(intlMessages.buttonCancel)}
onClick={closeModal}
/>
<Button
className={styles.buttonApply}
color="primary"
label={intl.formatMessage(intlMessages.buttonApply)}
onClick={() => {
@ -377,6 +386,7 @@ class LockViewersComponent extends Component {
updateWebcamsOnlyForModerator(usersProp.webcamsOnlyForModerator);
closeModal();
}}
data-test="applyLockSettings"
/>
</div>
</div>

View File

@ -138,6 +138,14 @@
margin: var(--sm-padding-x) var(--modal-margin) 0;
}
.buttonCancel {
margin: 0 0.25rem;
}
.buttonApply {
margin: 0 0.25rem;
}
.actions {
margin-left: auto;
margin-right: 0;

View File

@ -15,6 +15,7 @@ import Users from '/imports/api/users';
import Meetings from '/imports/api/meetings';
import AudioManager from '/imports/ui/services/audio-manager';
import { meetingIsBreakout } from '/imports/ui/components/app/service';
import Storage from '/imports/ui/services/storage/session';
const intlMessage = defineMessages({
410: {
@ -156,6 +157,8 @@ class MeetingEnded extends PureComponent {
this.getEndingMessage = this.getEndingMessage.bind(this);
AudioManager.exitAudio();
Storage.removeItem('getEchoTest');
Storage.removeItem('isFirstJoin');
Meteor.disconnect();
}

View File

@ -94,7 +94,7 @@ class BBBMenu extends React.Component {
render() {
const { anchorEl } = this.state;
const { trigger, intl, wide, classes } = this.props;
const { trigger, intl, wide, classes, dataTest } = this.props;
const actionsItems = this.makeMenuItems();
const menuClasses = classes || [];
menuClasses.push(styles.menu);
@ -125,6 +125,7 @@ class BBBMenu extends React.Component {
onClose={this.handleClose}
className={menuClasses.join(' ')}
style={{ zIndex: 9999 }}
data-test={dataTest}
>
{actionsItems}
{anchorEl && window.innerWidth < MAX_WIDTH &&
@ -182,4 +183,6 @@ BBBMenu.propTypes = {
onCloseCallback: PropTypes.func,
wide: PropTypes.bool,
dataTest: PropTypes.string,
};

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