Merge remote-tracking branch 'upstream/v2.4.x-release' into v2.4.x-release
This commit is contained in:
commit
15e6da9b69
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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 }
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -174,7 +174,7 @@ object Users2x {
|
||||
newUser
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def hasPresenter(users: Users2x): Boolean = {
|
||||
findPresenter(users) match {
|
||||
case Some(p) => true
|
||||
|
@ -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 = {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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])
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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";
|
||||
|
@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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));
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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/
|
||||
```
|
||||
|
@ -1,11 +0,0 @@
|
||||
// craco.config.js
|
||||
module.exports = {
|
||||
style: {
|
||||
postcss: {
|
||||
plugins: [
|
||||
require('tailwindcss'),
|
||||
require('autoprefixer'),
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
location /learning-dashboard/ {
|
||||
location /learning-analytics-dashboard/ {
|
||||
alias /var/bigbluebutton/learning-dashboard/;
|
||||
autoindex off;
|
||||
}
|
||||
|
39112
bbb-learning-dashboard/package-lock.json
generated
39112
bbb-learning-dashboard/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
}
|
||||
}
|
||||
|
6
bbb-learning-dashboard/postcss.config.js
Normal file
6
bbb-learning-dashboard/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
@ -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>
|
||||
|
@ -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: {},
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -1 +1 @@
|
||||
BIGBLUEBUTTON_RELEASE=2.4.0
|
||||
BIGBLUEBUTTON_RELEASE=2.4.3
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -63,6 +63,7 @@ export default function addMeeting(meeting) {
|
||||
allowModsToUnmuteUsers: Boolean,
|
||||
allowModsToEjectCameras: Boolean,
|
||||
meetingLayout: String,
|
||||
virtualBackgroundsDisabled: Boolean,
|
||||
},
|
||||
durationProps: {
|
||||
createdTime: Number,
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
@ -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}`);
|
||||
}
|
||||
}
|
@ -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}`);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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 });
|
||||
|
@ -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',
|
||||
};
|
||||
|
||||
|
@ -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') {
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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"
|
||||
|
@ -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 {
|
||||
|
@ -67,6 +67,7 @@ const PresentationOptionsContainer = ({
|
||||
id="restore-presentation"
|
||||
ghost={isLayoutSwapped}
|
||||
disabled={!isThereCurrentPresentation}
|
||||
data-test={isLayoutSwapped ? 'restorePresentation' : 'minimizePresentation'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -172,6 +172,7 @@ class QuickPollDropdown extends Component {
|
||||
}}
|
||||
size="lg"
|
||||
disabled={!!activePoll}
|
||||
data-test="quickPollBtn"
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -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'}
|
||||
|
@ -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',
|
||||
|
@ -19,7 +19,6 @@
|
||||
|
||||
.layout {
|
||||
@extend %flex-column;
|
||||
background-color: #06172a;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
|
@ -199,6 +199,7 @@ class AudioControls extends PureComponent {
|
||||
size="lg"
|
||||
circle
|
||||
accessKey={shortcuts.togglemute}
|
||||
data-test="toggleMicrophoneButton"
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -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
|
||||
: (
|
||||
|
@ -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
|
||||
|
@ -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');
|
||||
|
@ -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');
|
||||
|
@ -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));
|
||||
|
@ -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}
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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 !== ''
|
||||
|
@ -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}>
|
||||
|
@ -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>
|
||||
|
@ -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() {
|
||||
|
@ -134,6 +134,7 @@ class ExternalVideoModal extends Component {
|
||||
label={intl.formatMessage(intlMessages.start)}
|
||||
onClick={this.startWatchingHandler}
|
||||
disabled={startDisabled}
|
||||
data-test="startNewVideo"
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user