Merge branch 'v3.0.x-release' into remove-unused-talking-indicator
This commit is contained in:
commit
889d72dd40
@ -113,6 +113,12 @@ case class EjectUserFromBreakoutInternalMsg(parentId: String, breakoutId: String
|
||||
*/
|
||||
case class CapturePresentationReqInternalMsg(userId: String, parentMeetingId: String, filename: String, allPages: Boolean = true) extends InMessage
|
||||
|
||||
/**
|
||||
* Sent to the same meeting to force a new presenter to the Pod
|
||||
* @param presenterId
|
||||
*/
|
||||
case class SetPresenterInDefaultPodInternalMsg(presenterId: String) extends InMessage
|
||||
|
||||
/**
|
||||
* Sent by breakout room to parent meeting to obtain padId
|
||||
* @param breakoutId
|
||||
|
@ -11,7 +11,7 @@ class PresentationPodHdlrs(implicit val context: ActorContext)
|
||||
with PresentationConversionCompletedSysPubMsgHdlr
|
||||
with PdfConversionInvalidErrorSysPubMsgHdlr
|
||||
with SetCurrentPagePubMsgHdlr
|
||||
with SetPresenterInPodReqMsgHdlr
|
||||
with SetPresenterInDefaultPodInternalMsgHdlr
|
||||
with RemovePresentationPubMsgHdlr
|
||||
with SetPresentationDownloadablePubMsgHdlr
|
||||
with PresentationConversionUpdatePubMsgHdlr
|
||||
|
@ -0,0 +1,68 @@
|
||||
package org.bigbluebutton.core.apps.presentationpod
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.api.SetPresenterInDefaultPodInternalMsg
|
||||
import org.bigbluebutton.core.apps.{ RightsManagementTrait }
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.domain.MeetingState2x
|
||||
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
|
||||
import org.bigbluebutton.core.models.{ PresentationPod, Users2x }
|
||||
|
||||
trait SetPresenterInDefaultPodInternalMsgHdlr {
|
||||
this: PresentationPodHdlrs =>
|
||||
|
||||
def handleSetPresenterInDefaultPodInternalMsg(
|
||||
msg: SetPresenterInDefaultPodInternalMsg, state: MeetingState2x,
|
||||
liveMeeting: LiveMeeting, bus: MessageBus
|
||||
): MeetingState2x = {
|
||||
// Swith presenter as default presenter pod has changed.
|
||||
log.info("Presenter pod change will trigger a presenter change")
|
||||
SetPresenterInPodActionHandler.handleAction(state, liveMeeting, bus.outGW, "", PresentationPod.DEFAULT_PRESENTATION_POD, msg.presenterId)
|
||||
}
|
||||
}
|
||||
|
||||
object SetPresenterInPodActionHandler extends RightsManagementTrait {
|
||||
def handleAction(
|
||||
state: MeetingState2x,
|
||||
liveMeeting: LiveMeeting,
|
||||
outGW: OutMsgRouter,
|
||||
assignedBy: String,
|
||||
podId: String,
|
||||
newPresenterId: String
|
||||
): MeetingState2x = {
|
||||
|
||||
def broadcastSetPresenterInPodRespMsg(podId: String, nextPresenterId: String, requesterId: String): Unit = {
|
||||
val routing = Routing.addMsgToClientRouting(
|
||||
MessageTypes.BROADCAST_TO_MEETING,
|
||||
liveMeeting.props.meetingProp.intId, requesterId
|
||||
)
|
||||
val envelope = BbbCoreEnvelope(SetPresenterInPodRespMsg.NAME, routing)
|
||||
val header = BbbClientMsgHeader(SetPresenterInPodRespMsg.NAME, liveMeeting.props.meetingProp.intId, requesterId)
|
||||
|
||||
val body = SetPresenterInPodRespMsgBody(podId, nextPresenterId)
|
||||
val event = SetPresenterInPodRespMsg(header, body)
|
||||
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
|
||||
outGW.send(msgEvent)
|
||||
}
|
||||
|
||||
val newState = for {
|
||||
user <- Users2x.findWithIntId(liveMeeting.users2x, newPresenterId)
|
||||
pod <- PresentationPodsApp.getPresentationPod(state, podId)
|
||||
} yield {
|
||||
if (pod.currentPresenter != "") {
|
||||
Users2x.removeUserFromPresenterGroup(liveMeeting.users2x, pod.currentPresenter)
|
||||
liveMeeting.users2x.addOldPresenter(pod.currentPresenter)
|
||||
}
|
||||
Users2x.addUserToPresenterGroup(liveMeeting.users2x, newPresenterId)
|
||||
val updatedPod = pod.setCurrentPresenter(newPresenterId)
|
||||
broadcastSetPresenterInPodRespMsg(pod.id, newPresenterId, assignedBy)
|
||||
val pods = state.presentationPodManager.addPod(updatedPod)
|
||||
state.update(pods)
|
||||
}
|
||||
|
||||
newState match {
|
||||
case Some(ns) => ns
|
||||
case None => state
|
||||
}
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
package org.bigbluebutton.core.apps.presentationpod
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.apps.users.AssignPresenterActionHandler
|
||||
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.domain.MeetingState2x
|
||||
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
|
||||
import org.bigbluebutton.core.models.{ PresentationPod, Users2x }
|
||||
|
||||
trait SetPresenterInPodReqMsgHdlr {
|
||||
this: PresentationPodHdlrs =>
|
||||
|
||||
def handle(
|
||||
msg: SetPresenterInPodReqMsg, state: MeetingState2x,
|
||||
liveMeeting: LiveMeeting, bus: MessageBus
|
||||
): MeetingState2x = {
|
||||
if (msg.body.podId == PresentationPod.DEFAULT_PRESENTATION_POD) {
|
||||
// Swith presenter as default presenter pod has changed.
|
||||
log.info("Presenter pod change will trigger a presenter change")
|
||||
AssignPresenterActionHandler.handleAction(liveMeeting, bus.outGW, msg.header.userId, msg.body.nextPresenterId)
|
||||
}
|
||||
SetPresenterInPodActionHandler.handleAction(state, liveMeeting, bus.outGW, msg.header.userId, msg.body.podId, msg.body.nextPresenterId)
|
||||
}
|
||||
}
|
||||
|
||||
object SetPresenterInPodActionHandler extends RightsManagementTrait {
|
||||
def handleAction(
|
||||
state: MeetingState2x,
|
||||
liveMeeting: LiveMeeting,
|
||||
outGW: OutMsgRouter,
|
||||
assignedBy: String,
|
||||
podId: String,
|
||||
newPresenterId: String
|
||||
): MeetingState2x = {
|
||||
|
||||
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, assignedBy)) {
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
val reason = "No permission to set presenter in presentation pod."
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, assignedBy, reason, outGW, liveMeeting)
|
||||
state
|
||||
} else {
|
||||
def broadcastSetPresenterInPodRespMsg(podId: String, nextPresenterId: String, requesterId: String): Unit = {
|
||||
val routing = Routing.addMsgToClientRouting(
|
||||
MessageTypes.BROADCAST_TO_MEETING,
|
||||
liveMeeting.props.meetingProp.intId, requesterId
|
||||
)
|
||||
val envelope = BbbCoreEnvelope(SetPresenterInPodRespMsg.NAME, routing)
|
||||
val header = BbbClientMsgHeader(SetPresenterInPodRespMsg.NAME, liveMeeting.props.meetingProp.intId, requesterId)
|
||||
|
||||
val body = SetPresenterInPodRespMsgBody(podId, nextPresenterId)
|
||||
val event = SetPresenterInPodRespMsg(header, body)
|
||||
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
|
||||
outGW.send(msgEvent)
|
||||
}
|
||||
|
||||
val newState = for {
|
||||
user <- Users2x.findWithIntId(liveMeeting.users2x, newPresenterId)
|
||||
pod <- PresentationPodsApp.getPresentationPod(state, podId)
|
||||
} yield {
|
||||
if (pod.currentPresenter != "") {
|
||||
Users2x.removeUserFromPresenterGroup(liveMeeting.users2x, pod.currentPresenter)
|
||||
liveMeeting.users2x.addOldPresenter(pod.currentPresenter)
|
||||
}
|
||||
Users2x.addUserToPresenterGroup(liveMeeting.users2x, newPresenterId)
|
||||
val updatedPod = pod.setCurrentPresenter(newPresenterId)
|
||||
broadcastSetPresenterInPodRespMsg(pod.id, newPresenterId, assignedBy)
|
||||
val pods = state.presentationPodManager.addPod(updatedPod)
|
||||
state.update(pods)
|
||||
}
|
||||
|
||||
newState match {
|
||||
case Some(ns) => ns
|
||||
case None => state
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,8 +2,11 @@ package org.bigbluebutton.core.apps.users
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.apps.RightsManagementTrait
|
||||
import org.bigbluebutton.core.models.{ UserState, Users2x }
|
||||
import org.bigbluebutton.core.apps.groupchats.GroupChatApp
|
||||
import org.bigbluebutton.core.db.ChatMessageDAO
|
||||
import org.bigbluebutton.core.models.{ GroupChatFactory, GroupChatMessage, Roles, UserState, Users2x }
|
||||
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
|
||||
import org.bigbluebutton.core2.MeetingStatus2x
|
||||
import org.bigbluebutton.core2.message.senders.MsgBuilder
|
||||
|
||||
trait ChangeUserAwayReqMsgHdlr extends RightsManagementTrait {
|
||||
@ -30,6 +33,8 @@ trait ChangeUserAwayReqMsgHdlr extends RightsManagementTrait {
|
||||
outGW.send(msgEventChange)
|
||||
}
|
||||
|
||||
val permissions = MeetingStatus2x.getPermissions(liveMeeting.status)
|
||||
|
||||
for {
|
||||
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId)
|
||||
newUserState <- Users2x.setUserAway(liveMeeting.users2x, user.intId, msg.body.away)
|
||||
@ -44,6 +49,14 @@ trait ChangeUserAwayReqMsgHdlr extends RightsManagementTrait {
|
||||
outGW.send(MsgBuilder.buildUserEmojiChangedEvtMsg(liveMeeting.props.meetingProp.intId, msg.body.userId, "none"))
|
||||
}
|
||||
|
||||
val msgMeta = Map(
|
||||
"away" -> msg.body.away
|
||||
)
|
||||
|
||||
if (!(user.role == Roles.VIEWER_ROLE && user.locked && permissions.disablePubChat) && ((user.away && !msg.body.away) || (!user.away && msg.body.away))) {
|
||||
ChatMessageDAO.insertSystemMsg(liveMeeting.props.meetingProp.intId, GroupChatApp.MAIN_PUBLIC_CHAT, "", GroupChatMessageType.USER_AWAY_STATUS_MSG, msgMeta, user.name)
|
||||
}
|
||||
|
||||
broadcast(newUserState, msg.body.away)
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +60,8 @@ trait RegisterUserReqMsgHdlr {
|
||||
|
||||
val regUser = RegisteredUsers.create(msg.body.intUserId, msg.body.extUserId,
|
||||
msg.body.name, msg.body.role, msg.body.authToken, msg.body.sessionToken,
|
||||
msg.body.avatarURL, ColorPicker.nextColor(liveMeeting.props.meetingProp.intId), msg.body.guest, msg.body.authed, guestStatus, msg.body.excludeFromDashboard, msg.body.customParameters, false)
|
||||
msg.body.avatarURL, ColorPicker.nextColor(liveMeeting.props.meetingProp.intId), msg.body.guest, msg.body.authed,
|
||||
guestStatus, msg.body.excludeFromDashboard, msg.body.enforceLayout, msg.body.customParameters, false)
|
||||
|
||||
checkUserConcurrentAccesses(regUser)
|
||||
RegisteredUsers.add(liveMeeting.registeredUsers, regUser, liveMeeting.props.meetingProp.intId)
|
||||
|
@ -2,15 +2,14 @@ package org.bigbluebutton.core.apps.users
|
||||
|
||||
import org.bigbluebutton.common2.msgs.UserJoinMeetingReqMsg
|
||||
import org.bigbluebutton.core.apps.breakout.BreakoutHdlrHelpers
|
||||
import org.bigbluebutton.core.db.{ UserStateDAO }
|
||||
import org.bigbluebutton.core.db.{ UserDAO, UserStateDAO }
|
||||
import org.bigbluebutton.core.domain.MeetingState2x
|
||||
import org.bigbluebutton.core.models.{ RegisteredUser, RegisteredUsers, Users2x, VoiceUsers }
|
||||
import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting, MeetingActor, OutMsgRouter }
|
||||
import org.bigbluebutton.core2.message.senders.MsgBuilder
|
||||
import org.bigbluebutton.core.models._
|
||||
import org.bigbluebutton.core.running._
|
||||
import org.bigbluebutton.core2.message.senders._
|
||||
|
||||
trait UserJoinMeetingReqMsgHdlr extends HandlerHelpers {
|
||||
this: MeetingActor =>
|
||||
|
||||
val liveMeeting: LiveMeeting
|
||||
val outGW: OutMsgRouter
|
||||
|
||||
@ -18,64 +17,136 @@ trait UserJoinMeetingReqMsgHdlr extends HandlerHelpers {
|
||||
log.info("Received user joined meeting. user {} meetingId={}", msg.body.userId, msg.header.meetingId)
|
||||
|
||||
Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId) match {
|
||||
case Some(reconnectingUser) =>
|
||||
if (reconnectingUser.userLeftFlag.left) {
|
||||
log.info("Resetting flag that user left meeting. user {}", msg.body.userId)
|
||||
// User has reconnected. Just reset it's flag. ralam Oct 23, 2018
|
||||
sendUserLeftFlagUpdatedEvtMsg(outGW, liveMeeting, msg.body.userId, false)
|
||||
Users2x.resetUserLeftFlag(liveMeeting.users2x, msg.body.userId)
|
||||
}
|
||||
|
||||
state
|
||||
case None =>
|
||||
// Check if maxParticipants has been reached
|
||||
// User are able to reenter if he already joined previously with the same extId
|
||||
val userHasJoinedAlready = RegisteredUsers.findWithUserId(msg.body.userId, liveMeeting.registeredUsers) match {
|
||||
case Some(regUser: RegisteredUser) => RegisteredUsers.checkUserExtIdHasJoined(regUser.externId, liveMeeting.registeredUsers)
|
||||
case None => false
|
||||
}
|
||||
val hasReachedMaxParticipants = liveMeeting.props.usersProp.maxUsers > 0 &&
|
||||
RegisteredUsers.numUniqueJoinedUsers(liveMeeting.registeredUsers) >= liveMeeting.props.usersProp.maxUsers &&
|
||||
userHasJoinedAlready == false
|
||||
|
||||
if (!hasReachedMaxParticipants) {
|
||||
val newState = userJoinMeeting(outGW, msg.body.authToken, msg.body.clientType, liveMeeting, state)
|
||||
|
||||
if (liveMeeting.props.meetingProp.isBreakout) {
|
||||
BreakoutHdlrHelpers.updateParentMeetingWithUsers(liveMeeting, eventBus)
|
||||
}
|
||||
|
||||
// Warn previous users that someone connected with same Id
|
||||
for {
|
||||
regUser <- RegisteredUsers.getRegisteredUserWithToken(msg.body.authToken, msg.body.userId,
|
||||
liveMeeting.registeredUsers)
|
||||
} yield {
|
||||
RegisteredUsers.findAllWithExternUserId(regUser.externId, liveMeeting.registeredUsers)
|
||||
.filter(u => u.id != regUser.id)
|
||||
.foreach { previousUser =>
|
||||
val notifyUserEvent = MsgBuilder.buildNotifyUserInMeetingEvtMsg(
|
||||
previousUser.id,
|
||||
liveMeeting.props.meetingProp.intId,
|
||||
"info",
|
||||
"promote",
|
||||
"app.mobileAppModal.userConnectedWithSameId",
|
||||
"Notification to warn that user connect again from other browser/device",
|
||||
Vector(regUser.name)
|
||||
)
|
||||
outGW.send(notifyUserEvent)
|
||||
}
|
||||
}
|
||||
|
||||
// fresh user joined (not due to reconnection). Clear (pop) the cached voice user
|
||||
VoiceUsers.recoverVoiceUser(liveMeeting.voiceUsers, msg.body.userId)
|
||||
UserStateDAO.updateExpired(msg.body.userId, false)
|
||||
|
||||
newState
|
||||
} else {
|
||||
log.info("Ignoring user {} attempt to join, once the meeting {} has reached max participants: {}", msg.body.userId, msg.header.meetingId, liveMeeting.props.usersProp.maxUsers)
|
||||
state
|
||||
}
|
||||
case Some(user) => handleUserReconnecting(user, msg, state)
|
||||
case None => handleUserJoining(msg, state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def handleUserJoining(msg: UserJoinMeetingReqMsg, state: MeetingState2x): MeetingState2x = {
|
||||
|
||||
val regUser = RegisteredUsers.getRegisteredUserWithToken(msg.body.authToken, msg.body.userId, liveMeeting.registeredUsers)
|
||||
log.info(s"Number of registered users [${RegisteredUsers.numRegisteredUsers(liveMeeting.registeredUsers)}]")
|
||||
|
||||
regUser.fold {
|
||||
handleFailedUserJoin(msg, "Invalid auth token.", EjectReasonCode.VALIDATE_TOKEN)
|
||||
state
|
||||
} { user =>
|
||||
val validationResult = for {
|
||||
_ <- checkIfUserGuestStatusIsAllowed(user)
|
||||
_ <- checkIfUserIsBanned(user)
|
||||
_ <- checkIfUserLoggedOut(user)
|
||||
_ <- validateMaxParticipants(user)
|
||||
} yield user
|
||||
|
||||
validationResult.fold(
|
||||
reason => handleFailedUserJoin(msg, reason._1, reason._2),
|
||||
validUser => handleSuccessfulUserJoin(msg, validUser)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def handleSuccessfulUserJoin(msg: UserJoinMeetingReqMsg, regUser: RegisteredUser) = {
|
||||
val newState = userJoinMeeting(outGW, msg.body.authToken, msg.body.clientType, liveMeeting, state)
|
||||
updateParentMeetingWithNewListOfUsers()
|
||||
notifyPreviousUsersWithSameExtId(regUser)
|
||||
clearCachedVoiceUser(regUser)
|
||||
clearExpiredUserState(regUser)
|
||||
invalidateUserGraphqlConnection(regUser)
|
||||
|
||||
newState
|
||||
}
|
||||
|
||||
private def handleFailedUserJoin(msg: UserJoinMeetingReqMsg, failReason: String, failReasonCode: String) = {
|
||||
log.info("Ignoring user {} attempt to join in meeting {}. Reason Code: {}, Reason Message: {}", msg.body.userId, msg.header.meetingId, failReasonCode, failReason)
|
||||
UserDAO.updateJoinError(msg.body.userId, failReasonCode, failReason)
|
||||
state
|
||||
}
|
||||
|
||||
private def handleUserReconnecting(user: UserState, msg: UserJoinMeetingReqMsg, state: MeetingState2x): MeetingState2x = {
|
||||
if (user.userLeftFlag.left) {
|
||||
resetUserLeftFlag(msg)
|
||||
}
|
||||
state
|
||||
}
|
||||
|
||||
private def resetUserLeftFlag(msg: UserJoinMeetingReqMsg) = {
|
||||
log.info("Resetting flag that user left meeting. user {}", msg.body.userId)
|
||||
sendUserLeftFlagUpdatedEvtMsg(outGW, liveMeeting, msg.body.userId, false)
|
||||
Users2x.resetUserLeftFlag(liveMeeting.users2x, msg.body.userId)
|
||||
}
|
||||
|
||||
private def validateMaxParticipants(regUser: RegisteredUser): Either[(String, String), Unit] = {
|
||||
val userHasJoinedAlready = RegisteredUsers.checkUserExtIdHasJoined(regUser.externId, liveMeeting.registeredUsers)
|
||||
val maxParticipants = liveMeeting.props.usersProp.maxUsers - 1
|
||||
|
||||
if (maxParticipants > 0 && //0 = no limit
|
||||
RegisteredUsers.numUniqueJoinedUsers(liveMeeting.registeredUsers) >= maxParticipants &&
|
||||
!userHasJoinedAlready) {
|
||||
Left(("The maximum number of participants allowed for this meeting has been reached.", EjectReasonCode.MAX_PARTICIPANTS))
|
||||
} else {
|
||||
Right(())
|
||||
}
|
||||
}
|
||||
|
||||
private def checkIfUserGuestStatusIsAllowed(user: RegisteredUser): Either[(String, String), Unit] = {
|
||||
if (user.guestStatus != GuestStatus.ALLOW) {
|
||||
Left(("User is not allowed to join", EjectReasonCode.PERMISSION_FAILED))
|
||||
} else {
|
||||
Right(())
|
||||
}
|
||||
}
|
||||
|
||||
private def checkIfUserIsBanned(user: RegisteredUser): Either[(String, String), Unit] = {
|
||||
if (user.banned) {
|
||||
Left(("Banned user rejoining", EjectReasonCode.BANNED_USER_REJOINING))
|
||||
} else {
|
||||
Right(())
|
||||
}
|
||||
}
|
||||
|
||||
private def checkIfUserLoggedOut(user: RegisteredUser): Either[(String, String), Unit] = {
|
||||
if (user.loggedOut) {
|
||||
Left(("User had logged out", EjectReasonCode.USER_LOGGED_OUT))
|
||||
} else {
|
||||
Right(())
|
||||
}
|
||||
}
|
||||
|
||||
private def updateParentMeetingWithNewListOfUsers() = {
|
||||
if (liveMeeting.props.meetingProp.isBreakout) {
|
||||
BreakoutHdlrHelpers.updateParentMeetingWithUsers(liveMeeting, eventBus)
|
||||
}
|
||||
}
|
||||
|
||||
private def notifyPreviousUsersWithSameExtId(regUser: RegisteredUser) = {
|
||||
RegisteredUsers.findAllWithExternUserId(regUser.externId, liveMeeting.registeredUsers)
|
||||
.filter(_.id != regUser.id)
|
||||
.foreach { previousUser =>
|
||||
sendUserConnectedNotification(previousUser, regUser, liveMeeting)
|
||||
}
|
||||
}
|
||||
|
||||
private def sendUserConnectedNotification(previousUser: RegisteredUser, newUser: RegisteredUser, liveMeeting: LiveMeeting) = {
|
||||
val notifyUserEvent = MsgBuilder.buildNotifyUserInMeetingEvtMsg(
|
||||
previousUser.id,
|
||||
liveMeeting.props.meetingProp.intId,
|
||||
"info",
|
||||
"promote",
|
||||
"app.mobileAppModal.userConnectedWithSameId",
|
||||
"Notification to warn that user connect again from other browser/device",
|
||||
Vector(newUser.name)
|
||||
)
|
||||
outGW.send(notifyUserEvent)
|
||||
}
|
||||
|
||||
private def clearCachedVoiceUser(regUser: RegisteredUser) =
|
||||
// fresh user joined (not due to reconnection). Clear (pop) the cached voice user
|
||||
VoiceUsers.recoverVoiceUser(liveMeeting.voiceUsers, regUser.id)
|
||||
|
||||
private def clearExpiredUserState(regUser: RegisteredUser) =
|
||||
UserStateDAO.updateExpired(regUser.id, false)
|
||||
|
||||
private def invalidateUserGraphqlConnection(regUser: RegisteredUser) =
|
||||
Sender.sendInvalidateUserGraphqlConnectionSysMsg(liveMeeting.props.meetingProp.intId, regUser.id, regUser.sessionToken, "user_joined", outGW)
|
||||
|
||||
}
|
||||
|
@ -2,12 +2,14 @@ package org.bigbluebutton.core.apps.users
|
||||
|
||||
import org.apache.pekko.actor.ActorContext
|
||||
import org.apache.pekko.event.Logging
|
||||
import org.bigbluebutton.Boot.eventBus
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.api.{SetPresenterInDefaultPodInternalMsg}
|
||||
import org.bigbluebutton.core.apps.ExternalVideoModel
|
||||
import org.bigbluebutton.core.bus.InternalEventBus
|
||||
import org.bigbluebutton.core.bus.{BigBlueButtonEvent, InternalEventBus}
|
||||
import org.bigbluebutton.core.models._
|
||||
import org.bigbluebutton.core.running.{LiveMeeting, OutMsgRouter}
|
||||
import org.bigbluebutton.core2.message.senders.{MsgBuilder, Sender}
|
||||
import org.bigbluebutton.core2.message.senders.{MsgBuilder}
|
||||
import org.bigbluebutton.core.apps.screenshare.ScreenshareApp2x
|
||||
import org.bigbluebutton.core.db.UserStateDAO
|
||||
|
||||
@ -67,8 +69,8 @@ object UsersApp {
|
||||
moderator <- Users2x.findModerator(liveMeeting.users2x)
|
||||
newPresenter <- Users2x.makePresenter(liveMeeting.users2x, moderator.intId)
|
||||
} yield {
|
||||
// println(s"automaticallyAssignPresenter: moderator=${moderator} newPresenter=${newPresenter.intId}");
|
||||
sendPresenterAssigned(outGW, meetingId, newPresenter.intId, newPresenter.name, newPresenter.intId)
|
||||
sendPresenterInPodReq(meetingId, newPresenter.intId)
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,6 +79,10 @@ object UsersApp {
|
||||
outGW.send(event)
|
||||
}
|
||||
|
||||
def sendPresenterInPodReq(meetingId: String, newPresenterIntId: String): Unit = {
|
||||
eventBus.publish(BigBlueButtonEvent(meetingId, SetPresenterInDefaultPodInternalMsg(newPresenterIntId)))
|
||||
}
|
||||
|
||||
def sendUserLeftMeetingToAllClients(outGW: OutMsgRouter, meetingId: String,
|
||||
userId: String, eject: Boolean = false, ejectedBy: String = "", reason: String = "", reasonCode: String = ""): Unit = {
|
||||
// send a user left event for the clients to update
|
||||
|
@ -2,6 +2,7 @@ package org.bigbluebutton.core.apps.users
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.bus.InternalEventBus
|
||||
import org.bigbluebutton.core.db.UserDAO
|
||||
import org.bigbluebutton.core.domain.MeetingState2x
|
||||
import org.bigbluebutton.core.models._
|
||||
import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting, OutMsgRouter }
|
||||
@ -15,103 +16,79 @@ trait ValidateAuthTokenReqMsgHdlr extends HandlerHelpers {
|
||||
val eventBus: InternalEventBus
|
||||
|
||||
def handleValidateAuthTokenReqMsg(msg: ValidateAuthTokenReqMsg, state: MeetingState2x): MeetingState2x = {
|
||||
log.debug("RECEIVED ValidateAuthTokenReqMsg msg {}", msg)
|
||||
log.debug(s"Received ValidateAuthTokenReqMsg msg $msg")
|
||||
|
||||
var failReason = "Invalid auth token."
|
||||
var failReasonCode = EjectReasonCode.VALIDATE_TOKEN
|
||||
val regUser = RegisteredUsers.getRegisteredUserWithToken(msg.body.authToken, msg.body.userId, liveMeeting.registeredUsers)
|
||||
log.info(s"Number of registered users [${RegisteredUsers.numRegisteredUsers(liveMeeting.registeredUsers)}]")
|
||||
|
||||
log.info("Number of registered users [{}]", RegisteredUsers.numRegisteredUsers(liveMeeting.registeredUsers))
|
||||
val regUser = RegisteredUsers.getRegisteredUserWithToken(msg.body.authToken, msg.body.userId,
|
||||
liveMeeting.registeredUsers)
|
||||
regUser match {
|
||||
case Some(u) =>
|
||||
// Check if maxParticipants has been reached
|
||||
// User are able to reenter if he already joined previously with the same extId
|
||||
val hasReachedMaxParticipants = liveMeeting.props.usersProp.maxUsers > 0 &&
|
||||
RegisteredUsers.numUniqueJoinedUsers(liveMeeting.registeredUsers) >= liveMeeting.props.usersProp.maxUsers &&
|
||||
RegisteredUsers.checkUserExtIdHasJoined(u.externId, liveMeeting.registeredUsers) == false
|
||||
regUser.fold {
|
||||
sendFailedValidateAuthTokenRespMsg(msg, "Invalid auth token.", EjectReasonCode.VALIDATE_TOKEN)
|
||||
} { user =>
|
||||
val validationResult = for {
|
||||
_ <- checkIfUserGuestStatusIsAllowed(user)
|
||||
_ <- checkIfUserIsBanned(user)
|
||||
_ <- checkIfUserLoggedOut(user)
|
||||
_ <- validateMaxParticipants(user)
|
||||
} yield user
|
||||
|
||||
// Check if banned user is rejoining.
|
||||
// Fail validation if ejected user is rejoining.
|
||||
// ralam april 21, 2020
|
||||
if (u.guestStatus == GuestStatus.ALLOW && !u.banned && !u.loggedOut && !hasReachedMaxParticipants) {
|
||||
userValidated(u, state)
|
||||
} else {
|
||||
if (u.banned) {
|
||||
failReason = "Banned user rejoining"
|
||||
failReasonCode = EjectReasonCode.BANNED_USER_REJOINING
|
||||
} else if (u.loggedOut) {
|
||||
failReason = "User had logged out"
|
||||
failReasonCode = EjectReasonCode.USER_LOGGED_OUT
|
||||
} else if (hasReachedMaxParticipants) {
|
||||
failReason = "The maximum number of participants allowed for this meeting has been reached."
|
||||
failReasonCode = EjectReasonCode.MAX_PARTICIPANTS
|
||||
}
|
||||
validateTokenFailed(
|
||||
outGW,
|
||||
meetingId = liveMeeting.props.meetingProp.intId,
|
||||
userId = msg.header.userId,
|
||||
authToken = msg.body.authToken,
|
||||
valid = false,
|
||||
waitForApproval = false,
|
||||
failReason,
|
||||
failReasonCode,
|
||||
state
|
||||
)
|
||||
}
|
||||
validationResult.fold(
|
||||
reason => sendFailedValidateAuthTokenRespMsg(msg, reason._1, reason._2),
|
||||
validUser => sendSuccessfulValidateAuthTokenRespMsg(validUser)
|
||||
)
|
||||
}
|
||||
|
||||
case None =>
|
||||
validateTokenFailed(
|
||||
outGW,
|
||||
meetingId = liveMeeting.props.meetingProp.intId,
|
||||
userId = msg.header.userId,
|
||||
authToken = msg.body.authToken,
|
||||
valid = false,
|
||||
waitForApproval = false,
|
||||
failReason,
|
||||
failReasonCode,
|
||||
state
|
||||
)
|
||||
state
|
||||
}
|
||||
|
||||
private def validateMaxParticipants(user: RegisteredUser): Either[(String, String), Unit] = {
|
||||
if (liveMeeting.props.usersProp.maxUsers > 0 &&
|
||||
RegisteredUsers.numUniqueJoinedUsers(liveMeeting.registeredUsers) >= liveMeeting.props.usersProp.maxUsers &&
|
||||
RegisteredUsers.checkUserExtIdHasJoined(user.externId, liveMeeting.registeredUsers) == false) {
|
||||
Left(("The maximum number of participants allowed for this meeting has been reached.", EjectReasonCode.MAX_PARTICIPANTS))
|
||||
} else {
|
||||
Right(())
|
||||
}
|
||||
}
|
||||
|
||||
def validateTokenFailed(
|
||||
outGW: OutMsgRouter,
|
||||
meetingId: String,
|
||||
userId: String,
|
||||
authToken: String,
|
||||
valid: Boolean,
|
||||
waitForApproval: Boolean,
|
||||
reason: String,
|
||||
reasonCode: String,
|
||||
state: MeetingState2x
|
||||
): MeetingState2x = {
|
||||
val event = MsgBuilder.buildValidateAuthTokenRespMsg(meetingId, userId, authToken, valid, waitForApproval, 0,
|
||||
0, reasonCode, reason)
|
||||
outGW.send(event)
|
||||
|
||||
// send a system message to force disconnection
|
||||
// Comment out as meteor will disconnect the client. Requested by Tiago (ralam apr 28, 2020)
|
||||
//Sender.sendDisconnectClientSysMsg(meetingId, userId, SystemUser.ID, reasonCode, outGW)
|
||||
|
||||
state
|
||||
private def checkIfUserGuestStatusIsAllowed(user: RegisteredUser): Either[(String, String), Unit] = {
|
||||
if (user.guestStatus != GuestStatus.ALLOW) {
|
||||
Left(("User is not allowed to join", EjectReasonCode.PERMISSION_FAILED))
|
||||
} else {
|
||||
Right(())
|
||||
}
|
||||
}
|
||||
|
||||
def sendValidateAuthTokenRespMsg(meetingId: String, userId: String, authToken: String,
|
||||
valid: Boolean, waitForApproval: Boolean, registeredOn: Long, authTokenValidatedOn: Long,
|
||||
reasonCode: String = EjectReasonCode.NOT_EJECT, reason: String = "User not ejected"): Unit = {
|
||||
val event = MsgBuilder.buildValidateAuthTokenRespMsg(meetingId, userId, authToken, valid, waitForApproval, registeredOn,
|
||||
authTokenValidatedOn, reasonCode, reason)
|
||||
private def checkIfUserIsBanned(user: RegisteredUser): Either[(String, String), Unit] = {
|
||||
if (user.banned) {
|
||||
Left(("Banned user rejoining", EjectReasonCode.BANNED_USER_REJOINING))
|
||||
} else {
|
||||
Right(())
|
||||
}
|
||||
}
|
||||
|
||||
private def checkIfUserLoggedOut(user: RegisteredUser): Either[(String, String), Unit] = {
|
||||
if (user.loggedOut) {
|
||||
Left(("User had logged out", EjectReasonCode.USER_LOGGED_OUT))
|
||||
} else {
|
||||
Right(())
|
||||
}
|
||||
}
|
||||
|
||||
private def sendFailedValidateAuthTokenRespMsg(msg: ValidateAuthTokenReqMsg, failReason: String, failReasonCode: String) = {
|
||||
UserDAO.updateJoinError(msg.body.userId, failReasonCode, failReason)
|
||||
|
||||
val event = MsgBuilder.buildValidateAuthTokenRespMsg(liveMeeting.props.meetingProp.intId, msg.header.userId, msg.body.authToken, false, false, 0,
|
||||
0, failReasonCode, failReason)
|
||||
outGW.send(event)
|
||||
}
|
||||
|
||||
def userValidated(user: RegisteredUser, state: MeetingState2x): MeetingState2x = {
|
||||
def sendSuccessfulValidateAuthTokenRespMsg(user: RegisteredUser) = {
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
val updatedUser = RegisteredUsers.updateUserLastAuthTokenValidated(liveMeeting.registeredUsers, user)
|
||||
|
||||
sendValidateAuthTokenRespMsg(meetingId, updatedUser.id, updatedUser.authToken, valid = true, waitForApproval = false, updatedUser.registeredOn, updatedUser.lastAuthTokenValidatedOn)
|
||||
state
|
||||
val event = MsgBuilder.buildValidateAuthTokenRespMsg(meetingId, updatedUser.id, updatedUser.authToken, true, false, updatedUser.registeredOn,
|
||||
updatedUser.lastAuthTokenValidatedOn, EjectReasonCode.NOT_EJECT, "User not ejected")
|
||||
outGW.send(event)
|
||||
}
|
||||
|
||||
def sendAllUsersInMeeting(requesterId: String): Unit = {
|
||||
|
@ -33,8 +33,8 @@ trait UserJoinedVoiceConfEvtMsgHdlr extends SystemConfiguration {
|
||||
|
||||
def registerUserInRegisteredUsers() = {
|
||||
val regUser = RegisteredUsers.create(msg.body.intId, msg.body.voiceUserId,
|
||||
msg.body.callerIdName, Roles.VIEWER_ROLE, "", "", "", userColor,
|
||||
true, true, GuestStatus.WAIT, true, Map(), false)
|
||||
msg.body.callerIdName, Roles.VIEWER_ROLE, msg.body.intId, "", "", userColor,
|
||||
true, true, GuestStatus.WAIT, true, "", Map(), false)
|
||||
RegisteredUsers.add(liveMeeting.registeredUsers, regUser, liveMeeting.props.meetingProp.intId)
|
||||
}
|
||||
|
||||
@ -57,7 +57,7 @@ trait UserJoinedVoiceConfEvtMsgHdlr extends SystemConfiguration {
|
||||
locked = MeetingStatus2x.getPermissions(liveMeeting.status).lockOnJoin,
|
||||
avatar = "",
|
||||
color = userColor,
|
||||
clientType = "",
|
||||
clientType = if (isDialInUser) "dial-in-user" else "",
|
||||
pickExempted = false,
|
||||
userLeftFlag = UserLeftFlag(false, 0)
|
||||
)
|
||||
|
@ -4,6 +4,7 @@ import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.models._
|
||||
import org.bigbluebutton.core.apps.users.UsersApp
|
||||
import org.bigbluebutton.core.apps.breakout.BreakoutHdlrHelpers
|
||||
import org.bigbluebutton.core.db.UserDAO
|
||||
import org.bigbluebutton.core.running.{ LiveMeeting, MeetingActor, OutMsgRouter }
|
||||
|
||||
trait UserLeftVoiceConfEvtMsgHdlr {
|
||||
@ -39,6 +40,7 @@ trait UserLeftVoiceConfEvtMsgHdlr {
|
||||
UsersApp.guestWaitingLeft(liveMeeting, user.intId, outGW)
|
||||
}
|
||||
Users2x.remove(liveMeeting.users2x, user.intId)
|
||||
UserDAO.delete(user.intId)
|
||||
VoiceApp.removeUserFromVoiceConf(liveMeeting, outGW, msg.body.voiceUserId)
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
package org.bigbluebutton.core.apps.voice
|
||||
|
||||
import org.apache.pekko.actor.{ ActorContext, ActorSystem, Cancellable }
|
||||
import org.apache.pekko.actor.{ActorContext, ActorSystem, Cancellable}
|
||||
import org.bigbluebutton.SystemConfiguration
|
||||
import org.bigbluebutton.LockSettingsUtil
|
||||
import org.bigbluebutton.core.apps.breakout.BreakoutHdlrHelpers
|
||||
@ -11,8 +11,10 @@ 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
|
||||
import org.bigbluebutton.core.db.{UserDAO, UserVoiceDAO}
|
||||
import org.bigbluebutton.core.util.ColorPicker
|
||||
import org.bigbluebutton.core.util.TimeUtil
|
||||
|
||||
import scala.collection.immutable.Map
|
||||
import scala.concurrent.duration._
|
||||
|
||||
@ -323,6 +325,8 @@ object VoiceApp extends SystemConfiguration {
|
||||
uuid
|
||||
)
|
||||
VoiceUsers.add(liveMeeting.voiceUsers, voiceUserState)
|
||||
UserVoiceDAO.update(voiceUserState)
|
||||
UserDAO.updateVoiceUserJoined(voiceUserState)
|
||||
|
||||
broadcastEvent(voiceUserState)
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
package org.bigbluebutton.core.apps.voice
|
||||
|
||||
import org.bigbluebutton.common2.msgs.{ BbbClientMsgHeader, BbbCommonEnvCoreMsg, BbbCoreEnvelope, MessageTypes, Routing, VoiceCallStateEvtMsg, VoiceCallStateEvtMsgBody, VoiceConfCallStateEvtMsg }
|
||||
import org.bigbluebutton.core.models.{ VoiceUserState, VoiceUsers }
|
||||
import org.bigbluebutton.core.db.{ UserVoiceConfStateDAO, UserVoiceDAO }
|
||||
import org.bigbluebutton.core.running.{ LiveMeeting, MeetingActor, OutMsgRouter }
|
||||
|
||||
trait VoiceConfCallStateEvtMsgHdlr {
|
||||
@ -38,5 +38,9 @@ trait VoiceConfCallStateEvtMsgHdlr {
|
||||
val event = VoiceCallStateEvtMsg(header, body)
|
||||
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
|
||||
outGW.send(msgEvent)
|
||||
|
||||
if (msg.body.userId.nonEmpty) {
|
||||
UserVoiceConfStateDAO.insertOrUpdate(msg.body.userId, msg.body.voiceConf, msg.body.callSession, msg.body.clientSession, msg.body.callState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -77,12 +77,12 @@ object MeetingDAO {
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => {
|
||||
DatabaseConnection.logger.debug(s"$rowsAffected row(s) inserted in Meeting table!")
|
||||
ChatDAO.insert(meetingProps.meetingProp.intId, GroupChatApp.createDefaultPublicGroupChat())
|
||||
MeetingUsersPoliciesDAO.insert(meetingProps.meetingProp.intId, meetingProps.usersProp)
|
||||
MeetingLockSettingsDAO.insert(meetingProps.meetingProp.intId, meetingProps.lockSettingsProps)
|
||||
MeetingMetadataDAO.insert(meetingProps.meetingProp.intId, meetingProps.metadataProp)
|
||||
MeetingRecordingPoliciesDAO.insert(meetingProps.meetingProp.intId, meetingProps.recordProp)
|
||||
MeetingVoiceDAO.insert(meetingProps.meetingProp.intId, meetingProps.voiceProp)
|
||||
ChatDAO.insert(meetingProps.meetingProp.intId, GroupChatApp.createDefaultPublicGroupChat())
|
||||
MeetingWelcomeDAO.insert(meetingProps.meetingProp.intId, meetingProps.welcomeProp)
|
||||
MeetingGroupDAO.insert(meetingProps.meetingProp.intId, meetingProps.groups)
|
||||
MeetingBreakoutDAO.insert(meetingProps.meetingProp.intId, meetingProps.breakoutProps)
|
||||
|
@ -19,7 +19,7 @@ class UserConnectionStatusDbTableDef(tag: Tag) extends Table[UserConnectionStatu
|
||||
val connectionAliveAt = column[Option[java.sql.Timestamp]]("connectionAliveAt")
|
||||
}
|
||||
|
||||
object UserConnectionStatusdDAO {
|
||||
object UserConnectionStatusDAO {
|
||||
|
||||
def insert(meetingId: String, userId: String) = {
|
||||
DatabaseConnection.db.run(
|
||||
|
@ -1,34 +1,37 @@
|
||||
package org.bigbluebutton.core.db
|
||||
import org.bigbluebutton.core.models.{RegisteredUser}
|
||||
import org.bigbluebutton.core.models.{RegisteredUser, VoiceUserState}
|
||||
import slick.jdbc.PostgresProfile.api._
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
case class UserDbModel(
|
||||
userId: String,
|
||||
extId: String,
|
||||
meetingId: String,
|
||||
name: String,
|
||||
role: String,
|
||||
avatar: String = "",
|
||||
color: String = "",
|
||||
sessionToken: String = "",
|
||||
authed: Boolean = false,
|
||||
joined: Boolean = false,
|
||||
banned: Boolean = false,
|
||||
loggedOut: Boolean = false,
|
||||
guest: Boolean,
|
||||
guestStatus: String,
|
||||
registeredOn: Long,
|
||||
excludeFromDashboard: Boolean,
|
||||
userId: String,
|
||||
extId: String,
|
||||
meetingId: String,
|
||||
name: String,
|
||||
role: String,
|
||||
avatar: String = "",
|
||||
color: String = "",
|
||||
sessionToken: String = "",
|
||||
authed: Boolean = false,
|
||||
joined: Boolean = false,
|
||||
joinErrorMessage: Option[String],
|
||||
joinErrorCode: Option[String],
|
||||
banned: Boolean = false,
|
||||
loggedOut: Boolean = false,
|
||||
guest: Boolean,
|
||||
guestStatus: String,
|
||||
registeredOn: Long,
|
||||
excludeFromDashboard: Boolean,
|
||||
enforceLayout: Option[String],
|
||||
)
|
||||
|
||||
|
||||
|
||||
class UserDbTableDef(tag: Tag) extends Table[UserDbModel](tag, None, "user") {
|
||||
override def * = (
|
||||
userId,extId,meetingId,name,role,avatar,color, sessionToken, authed,joined,banned,loggedOut,guest,guestStatus,registeredOn,excludeFromDashboard) <> (UserDbModel.tupled, UserDbModel.unapply)
|
||||
userId,extId,meetingId,name,role,avatar,color, sessionToken, authed,joined,joinErrorCode, joinErrorMessage, banned,loggedOut,guest,guestStatus,registeredOn,excludeFromDashboard, enforceLayout) <> (UserDbModel.tupled, UserDbModel.unapply)
|
||||
val userId = column[String]("userId", O.PrimaryKey)
|
||||
val extId = column[String]("extId")
|
||||
val meetingId = column[String]("meetingId")
|
||||
@ -39,12 +42,15 @@ class UserDbTableDef(tag: Tag) extends Table[UserDbModel](tag, None, "user") {
|
||||
val sessionToken = column[String]("sessionToken")
|
||||
val authed = column[Boolean]("authed")
|
||||
val joined = column[Boolean]("joined")
|
||||
val joinErrorCode = column[Option[String]]("joinErrorCode")
|
||||
val joinErrorMessage = column[Option[String]]("joinErrorMessage")
|
||||
val banned = column[Boolean]("banned")
|
||||
val loggedOut = column[Boolean]("loggedOut")
|
||||
val guest = column[Boolean]("guest")
|
||||
val guestStatus = column[String]("guestStatus")
|
||||
val registeredOn = column[Long]("registeredOn")
|
||||
val excludeFromDashboard = column[Boolean]("excludeFromDashboard")
|
||||
val enforceLayout = column[Option[String]]("enforceLayout")
|
||||
}
|
||||
|
||||
object UserDAO {
|
||||
@ -62,21 +68,27 @@ object UserDAO {
|
||||
sessionToken = regUser.sessionToken,
|
||||
authed = regUser.authed,
|
||||
joined = regUser.joined,
|
||||
joinErrorCode = None,
|
||||
joinErrorMessage = None,
|
||||
banned = regUser.banned,
|
||||
loggedOut = regUser.loggedOut,
|
||||
guest = regUser.guest,
|
||||
guestStatus = regUser.guestStatus,
|
||||
registeredOn = regUser.registeredOn,
|
||||
excludeFromDashboard = regUser.excludeFromDashboard
|
||||
excludeFromDashboard = regUser.excludeFromDashboard,
|
||||
enforceLayout = regUser.enforceLayout match {
|
||||
case "" => None
|
||||
case enforceLayout: String => Some(enforceLayout)
|
||||
}
|
||||
)
|
||||
)
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => {
|
||||
DatabaseConnection.logger.debug(s"$rowsAffected row(s) inserted in User table!")
|
||||
ChatUserDAO.insertUserPublicChat(meetingId, regUser.id)
|
||||
UserConnectionStatusdDAO.insert(meetingId, regUser.id)
|
||||
UserConnectionStatusDAO.insert(meetingId, regUser.id)
|
||||
UserCustomParameterDAO.insert(regUser.id, regUser.customParameters)
|
||||
UserClientSettingsDAO.insert(regUser.id, meetingId)
|
||||
ChatUserDAO.insertUserPublicChat(meetingId, regUser.id)
|
||||
}
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error inserting user: $e")
|
||||
}
|
||||
@ -94,18 +106,33 @@ object UserDAO {
|
||||
}
|
||||
}
|
||||
|
||||
def updateVoiceUserJoined(voiceUserState: VoiceUserState) = {
|
||||
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[UserDbTableDef]
|
||||
.filter(_.userId === voiceUserState.intId)
|
||||
.map(u => (u.guest, u.guestStatus, u.authed, u.joined))
|
||||
.update((false, "ALLOW", true, true))
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated on user voice table!")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error updating user voice: $e")
|
||||
}
|
||||
}
|
||||
|
||||
def updateJoinError(userId: String, joinErrorCode: String, joinErrorMessage: String) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[UserDbTableDef]
|
||||
.filter(_.userId === userId)
|
||||
.map(u => (u.joined, u.joinErrorCode, u.joinErrorMessage))
|
||||
.update((false, Some(joinErrorCode), Some(joinErrorMessage)))
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated on user (Joined) table!")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error updating user (Joined): $e")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def delete(intId: String) = {
|
||||
// DatabaseConnection.db.run(
|
||||
// TableQuery[UserDbTableDef]
|
||||
// .filter(_.userId === intId)
|
||||
// .delete
|
||||
// ).onComplete {
|
||||
// case Success(rowsAffected) => DatabaseConnection.logger.debug(s"User ${intId} deleted")
|
||||
// case Failure(e) => DatabaseConnection.logger.error(s"Error deleting user ${intId}: $e")
|
||||
// }
|
||||
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[UserDbTableDef]
|
||||
.filter(_.userId === intId)
|
||||
|
@ -0,0 +1,49 @@
|
||||
package org.bigbluebutton.core.db
|
||||
|
||||
import org.bigbluebutton.core.models.{ VoiceUserState }
|
||||
import slick.jdbc.PostgresProfile.api._
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.util.{Failure, Success }
|
||||
|
||||
case class UserVoiceConfStateDbModel(
|
||||
userId: String,
|
||||
voiceConf: String,
|
||||
voiceConfCallSession: String,
|
||||
voiceConfClientSession: String,
|
||||
voiceConfCallState: String,
|
||||
)
|
||||
|
||||
class UserVoiceConfStateDbTableDef(tag: Tag) extends Table[UserVoiceConfStateDbModel](tag, None, "user_voice") {
|
||||
override def * = (
|
||||
userId, voiceConf, voiceConfCallSession, voiceConfClientSession, voiceConfCallState
|
||||
) <> (UserVoiceConfStateDbModel.tupled, UserVoiceConfStateDbModel.unapply)
|
||||
val userId = column[String]("userId", O.PrimaryKey)
|
||||
val voiceConf = column[String]("voiceConf")
|
||||
val voiceConfCallSession = column[String]("voiceConfCallSession")
|
||||
val voiceConfClientSession = column[String]("voiceConfClientSession")
|
||||
val voiceConfCallState = column[String]("voiceConfCallState")
|
||||
}
|
||||
|
||||
object UserVoiceConfStateDAO {
|
||||
def insertOrUpdate(userId: String, voiceConf: String, voiceConfCallSession: String, clientSession: String, callState: String) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[UserVoiceConfStateDbTableDef].insertOrUpdate(
|
||||
UserVoiceConfStateDbModel(
|
||||
userId = userId,
|
||||
voiceConf = voiceConf,
|
||||
voiceConfCallSession = voiceConfCallSession,
|
||||
voiceConfClientSession = clientSession,
|
||||
voiceConfCallState = callState,
|
||||
)
|
||||
)
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => {
|
||||
DatabaseConnection.logger.debug(s"$rowsAffected row(s) inserted on user_voice table!")
|
||||
}
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error inserting voice: $e")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -7,27 +7,26 @@ import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.util.{Failure, Success }
|
||||
|
||||
case class UserVoiceDbModel(
|
||||
userId: String,
|
||||
voiceUserId: String,
|
||||
callerName: String,
|
||||
callerNum: String,
|
||||
callingWith: String,
|
||||
joined: Boolean,
|
||||
listenOnly: Boolean,
|
||||
muted: Boolean,
|
||||
spoke: Boolean,
|
||||
talking: Boolean,
|
||||
floor: Boolean,
|
||||
lastFloorTime: String,
|
||||
voiceConf: String,
|
||||
startTime: Option[Long],
|
||||
endTime: Option[Long],
|
||||
userId: String,
|
||||
voiceUserId: String,
|
||||
callerName: String,
|
||||
callerNum: String,
|
||||
callingWith: String,
|
||||
joined: Boolean,
|
||||
listenOnly: Boolean,
|
||||
muted: Boolean,
|
||||
spoke: Boolean,
|
||||
talking: Boolean,
|
||||
floor: Boolean,
|
||||
lastFloorTime: String,
|
||||
startTime: Option[Long],
|
||||
endTime: Option[Long],
|
||||
)
|
||||
|
||||
class UserVoiceDbTableDef(tag: Tag) extends Table[UserVoiceDbModel](tag, None, "user_voice") {
|
||||
override def * = (
|
||||
userId, voiceUserId, callerName, callerNum, callingWith, joined, listenOnly,
|
||||
muted, spoke, talking, floor, lastFloorTime, voiceConf, startTime, endTime
|
||||
muted, spoke, talking, floor, lastFloorTime, startTime, endTime
|
||||
) <> (UserVoiceDbModel.tupled, UserVoiceDbModel.unapply)
|
||||
val userId = column[String]("userId", O.PrimaryKey)
|
||||
val voiceUserId = column[String]("voiceUserId")
|
||||
@ -42,6 +41,9 @@ class UserVoiceDbTableDef(tag: Tag) extends Table[UserVoiceDbModel](tag, None, "
|
||||
val floor = column[Boolean]("floor")
|
||||
val lastFloorTime = column[String]("lastFloorTime")
|
||||
val voiceConf = column[String]("voiceConf")
|
||||
val voiceConfCallSession = column[String]("voiceConfCallSession")
|
||||
val voiceConfClientSession = column[String]("voiceConfClientSession")
|
||||
val voiceConfCallState = column[String]("voiceConfCallState")
|
||||
val startTime = column[Option[Long]]("startTime")
|
||||
val endTime = column[Option[Long]]("endTime")
|
||||
}
|
||||
@ -64,7 +66,6 @@ object UserVoiceDAO {
|
||||
talking = voiceUserState.talking,
|
||||
floor = voiceUserState.floor,
|
||||
lastFloorTime = voiceUserState.lastFloorTime,
|
||||
voiceConf = "",
|
||||
startTime = None,
|
||||
endTime = None
|
||||
)
|
||||
@ -85,7 +86,7 @@ object UserVoiceDAO {
|
||||
.update((voiceUserState.listenOnly, voiceUserState.muted, voiceUserState.floor, voiceUserState.lastFloorTime))
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated on user_voice table!")
|
||||
case Failure(e) => DatabaseConnection.logger.error(s"Error updating user: $e")
|
||||
case Failure(e) => DatabaseConnection.logger.error(s"Error updating user_voice: $e")
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,7 +114,19 @@ object UserVoiceDAO {
|
||||
}
|
||||
}
|
||||
|
||||
def deleteUser(userId: String) = {
|
||||
def delete(intId: String) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[UserDbTableDef]
|
||||
.filter(_.userId === intId)
|
||||
.map(u => (u.loggedOut))
|
||||
.update((true))
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated loggedOut=true on user table!")
|
||||
case Failure(e) => DatabaseConnection.logger.error(s"Error updating loggedOut=true user: $e")
|
||||
}
|
||||
}
|
||||
|
||||
def deleteUserVoice(userId: String) = {
|
||||
//Meteor sets this props instead of removing
|
||||
// muted: false
|
||||
// talking: false
|
||||
@ -124,10 +137,11 @@ object UserVoiceDAO {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[UserVoiceDbTableDef]
|
||||
.filter(_.userId === userId)
|
||||
.delete
|
||||
.map(u => (u.muted, u.talking, u.listenOnly, u.joined, u.spoke, u.startTime, u.endTime))
|
||||
.update((false, false, false, false, false, None, None))
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"Voice of user ${userId} deleted")
|
||||
case Failure(e) => DatabaseConnection.logger.error(s"Error deleting voice: $e")
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"Voice of user ${userId} deleted (joined=false)")
|
||||
case Failure(e) => DatabaseConnection.logger.error(s"Error deleting voice user: $e")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,8 @@ import org.bigbluebutton.core.domain.BreakoutRoom2x
|
||||
object RegisteredUsers {
|
||||
def create(userId: String, extId: String, name: String, roles: String,
|
||||
authToken: String, sessionToken: String, avatar: String, color: String, guest: Boolean, authenticated: Boolean,
|
||||
guestStatus: String, excludeFromDashboard: Boolean, customParameters: Map[String, String], loggedOut: Boolean): RegisteredUser = {
|
||||
guestStatus: String, excludeFromDashboard: Boolean, enforceLayout: String,
|
||||
customParameters: Map[String, String], loggedOut: Boolean): RegisteredUser = {
|
||||
new RegisteredUser(
|
||||
userId,
|
||||
extId,
|
||||
@ -25,6 +26,7 @@ object RegisteredUsers {
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
enforceLayout,
|
||||
customParameters,
|
||||
loggedOut,
|
||||
)
|
||||
@ -214,6 +216,7 @@ case class RegisteredUser(
|
||||
lastAuthTokenValidatedOn: Long,
|
||||
joined: Boolean,
|
||||
banned: Boolean,
|
||||
enforceLayout: String,
|
||||
customParameters: Map[String,String],
|
||||
loggedOut: Boolean,
|
||||
lastBreakoutRoom: BreakoutRoom2x = null,
|
||||
|
@ -39,7 +39,7 @@ object VoiceUsers {
|
||||
}
|
||||
|
||||
def removeWithIntId(users: VoiceUsers, intId: String): Option[VoiceUserState] = {
|
||||
UserVoiceDAO.deleteUser(intId)
|
||||
UserVoiceDAO.deleteUserVoice(intId)
|
||||
users.remove(intId)
|
||||
}
|
||||
|
||||
|
@ -346,8 +346,6 @@ class ReceivedJsonMsgHandlerActor(
|
||||
routeGenericMsg[CreateNewPresentationPodPubMsg](envelope, jsonNode)
|
||||
case RemovePresentationPodPubMsg.NAME =>
|
||||
routeGenericMsg[RemovePresentationPodPubMsg](envelope, jsonNode)
|
||||
case SetPresenterInPodReqMsg.NAME =>
|
||||
routeGenericMsg[SetPresenterInPodReqMsg](envelope, jsonNode)
|
||||
|
||||
// Caption
|
||||
case EditCaptionHistoryPubMsg.NAME =>
|
||||
|
@ -248,25 +248,26 @@ class MeetingActor(
|
||||
//=============================
|
||||
|
||||
// 2x messages
|
||||
case msg: BbbCommonEnvCoreMsg => handleBbbCommonEnvCoreMsg(msg)
|
||||
case msg: BbbCommonEnvCoreMsg => handleBbbCommonEnvCoreMsg(msg)
|
||||
|
||||
// Handling RegisterUserReqMsg as it is forwarded from BBBActor and
|
||||
// its type is not BbbCommonEnvCoreMsg
|
||||
case m: RegisterUserReqMsg => usersApp.handleRegisterUserReqMsg(m)
|
||||
case m: GetAllMeetingsReqMsg => handleGetAllMeetingsReqMsg(m)
|
||||
case m: GetRunningMeetingStateReqMsg => handleGetRunningMeetingStateReqMsg(m)
|
||||
case m: ValidateConnAuthTokenSysMsg => handleValidateConnAuthTokenSysMsg(m)
|
||||
case m: RegisterUserReqMsg => usersApp.handleRegisterUserReqMsg(m)
|
||||
case m: GetAllMeetingsReqMsg => handleGetAllMeetingsReqMsg(m)
|
||||
case m: GetRunningMeetingStateReqMsg => handleGetRunningMeetingStateReqMsg(m)
|
||||
case m: ValidateConnAuthTokenSysMsg => handleValidateConnAuthTokenSysMsg(m)
|
||||
|
||||
// Meeting
|
||||
case m: DestroyMeetingSysCmdMsg => handleDestroyMeetingSysCmdMsg(m)
|
||||
case m: DestroyMeetingSysCmdMsg => handleDestroyMeetingSysCmdMsg(m)
|
||||
|
||||
//======================================
|
||||
|
||||
//=======================================
|
||||
// internal messages
|
||||
case msg: MonitorNumberOfUsersInternalMsg => handleMonitorNumberOfUsers(msg)
|
||||
case msg: MonitorNumberOfUsersInternalMsg => handleMonitorNumberOfUsers(msg)
|
||||
case msg: SetPresenterInDefaultPodInternalMsg => state = presentationPodsApp.handleSetPresenterInDefaultPodInternalMsg(msg, state, liveMeeting, msgBus)
|
||||
|
||||
case msg: ExtendMeetingDuration => handleExtendMeetingDuration(msg)
|
||||
case msg: ExtendMeetingDuration => handleExtendMeetingDuration(msg)
|
||||
case msg: SendTimeRemainingAuditInternalMsg =>
|
||||
if (!liveMeeting.props.meetingProp.isBreakout) {
|
||||
// Update users of meeting remaining time.
|
||||
@ -531,7 +532,6 @@ class MeetingActor(
|
||||
case m: PresentationConversionCompletedSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
case m: PdfConversionInvalidErrorSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
case m: SetCurrentPagePubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
case m: SetPresenterInPodReqMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
case m: RemovePresentationPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
case m: SetPresentationDownloadablePubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
case m: PresentationConversionUpdateSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
|
@ -1,8 +1,8 @@
|
||||
package org.bigbluebutton.core2.message.senders
|
||||
|
||||
import org.bigbluebutton.common2.domain.DefaultProps
|
||||
import org.bigbluebutton.common2.msgs.{ BbbCommonEnvCoreMsg, BbbCoreEnvelope, BbbCoreHeaderWithMeetingId, MessageTypes, Routing, ValidateConnAuthTokenSysRespMsg, ValidateConnAuthTokenSysRespMsgBody, NotifyAllInMeetingEvtMsg, NotifyAllInMeetingEvtMsgBody, NotifyRoleInMeetingEvtMsg, NotifyRoleInMeetingEvtMsgBody, NotifyUserInMeetingEvtMsg, NotifyUserInMeetingEvtMsgBody, _ }
|
||||
import org.bigbluebutton.core.models.GuestWaiting
|
||||
import org.bigbluebutton.common2.msgs.{ BbbCommonEnvCoreMsg, BbbCoreEnvelope, BbbCoreHeaderWithMeetingId, MessageTypes, NotifyAllInMeetingEvtMsg, NotifyAllInMeetingEvtMsgBody, NotifyRoleInMeetingEvtMsg, NotifyRoleInMeetingEvtMsgBody, NotifyUserInMeetingEvtMsg, NotifyUserInMeetingEvtMsgBody, Routing, ValidateConnAuthTokenSysRespMsg, ValidateConnAuthTokenSysRespMsgBody, _ }
|
||||
import org.bigbluebutton.core.models.{ GuestWaiting, PresentationPod }
|
||||
|
||||
object MsgBuilder {
|
||||
def buildGuestPolicyChangedEvtMsg(meetingId: String, userId: String, policy: String, setBy: String): BbbCommonEnvCoreMsg = {
|
||||
|
@ -56,7 +56,7 @@ object FakeUserGenerator {
|
||||
val color = "#ff6242"
|
||||
|
||||
val ru = RegisteredUsers.create(userId = id, extId, name, role,
|
||||
authToken, sessionToken, avatarURL, color, guest, authed, guestStatus = GuestStatus.ALLOW, false, Map(), false)
|
||||
authToken, sessionToken, avatarURL, color, guest, authed, guestStatus = GuestStatus.ALLOW, false, "", Map(), false)
|
||||
RegisteredUsers.add(users, ru, meetingId)
|
||||
ru
|
||||
}
|
||||
|
@ -397,10 +397,33 @@ class LearningDashboardActor(
|
||||
user <- findUserByIntId(meeting, msg.body.userId)
|
||||
} yield {
|
||||
if (msg.body.reactionEmoji != "none") {
|
||||
val updatedUser = user.copy(reactions = user.reactions :+ Emoji(msg.body.reactionEmoji))
|
||||
val updatedMeeting = meeting.copy(users = meeting.users + (updatedUser.userKey -> updatedUser))
|
||||
//Ignore multiple Reactions to prevent flooding
|
||||
val hasSameReactionInLast30Seconds = user.reactions.filter(r => {
|
||||
System.currentTimeMillis() - r.sentOn < (30 * 1000) && r.name == msg.body.reactionEmoji
|
||||
}).length > 0
|
||||
|
||||
meetings += (updatedMeeting.intId -> updatedMeeting)
|
||||
if(!hasSameReactionInLast30Seconds) {
|
||||
val updatedUser = user.copy(reactions = user.reactions :+ Emoji(msg.body.reactionEmoji))
|
||||
val updatedMeeting = meeting.copy(users = meeting.users + (updatedUser.userKey -> updatedUser))
|
||||
meetings += (updatedMeeting.intId -> updatedMeeting)
|
||||
|
||||
//Convert Reactions to legacy Emoji (while LearningDashboard doesn't support Reactions)
|
||||
val emoji = msg.body.reactionEmoji.codePointAt(0) match {
|
||||
case 128515 => "happy"
|
||||
case 128528 => "neutral"
|
||||
case 128577 => "sad"
|
||||
case 128077 => "thumbsUp"
|
||||
case 128078 => "thumbsDown"
|
||||
case 128079 => "applause"
|
||||
case _ => "none"
|
||||
}
|
||||
|
||||
if (emoji != "none") {
|
||||
val updatedUserWithEmoji = updatedUser.copy(emojis = user.emojis :+ Emoji(emoji))
|
||||
val updatedMeetingWithEmoji = meeting.copy(users = meeting.users + (updatedUserWithEmoji.userKey -> updatedUserWithEmoji))
|
||||
meetings += (updatedMeeting.intId -> updatedMeetingWithEmoji)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,9 +13,10 @@ object TestDataGen {
|
||||
val sessionToken = RandomStringGenerator.randomAlphanumericString(16)
|
||||
val avatarURL = "https://www." + RandomStringGenerator.randomAlphanumericString(32) + ".com/" +
|
||||
RandomStringGenerator.randomAlphanumericString(10) + ".png"
|
||||
val color = "#ff6242"
|
||||
|
||||
val ru = RegisteredUsers.create(userId = id, extId, name, role,
|
||||
authToken, sessionToken, avatarURL, guest, authed, GuestStatus.ALLOW, false)
|
||||
authToken, sessionToken, avatarURL, color, guest, authed, GuestStatus.ALLOW, false, "", Map(), false)
|
||||
|
||||
RegisteredUsers.add(users, ru, meetingId = "test")
|
||||
ru
|
||||
|
@ -11,6 +11,7 @@ object GroupChatMessageType {
|
||||
val POLL = "poll"
|
||||
val BREAKOUTROOM_MOD_MSG = "breakoutRoomModeratorMsg"
|
||||
val PUBLIC_CHAT_HIST_CLEARED = "publicChatHistoryCleared"
|
||||
val USER_AWAY_STATUS_MSG = "userAwayStatusMsg"
|
||||
}
|
||||
|
||||
case class GroupChatUser(id: String, name: String = "", role: String = "VIEWER")
|
||||
|
@ -23,10 +23,6 @@ object SetCurrentPagePubMsg { val NAME = "SetCurrentPagePubMsg" }
|
||||
case class SetCurrentPagePubMsg(header: BbbClientMsgHeader, body: SetCurrentPagePubMsgBody) extends StandardMsg
|
||||
case class SetCurrentPagePubMsgBody(podId: String, presentationId: String, pageId: String)
|
||||
|
||||
object SetPresenterInPodReqMsg { val NAME = "SetPresenterInPodReqMsg" }
|
||||
case class SetPresenterInPodReqMsg(header: BbbClientMsgHeader, body: SetPresenterInPodReqMsgBody) extends StandardMsg
|
||||
case class SetPresenterInPodReqMsgBody(podId: String, nextPresenterId: String)
|
||||
|
||||
object RemovePresentationPubMsg { val NAME = "RemovePresentationPubMsg" }
|
||||
case class RemovePresentationPubMsg(header: BbbClientMsgHeader, body: RemovePresentationPubMsgBody) extends StandardMsg
|
||||
case class RemovePresentationPubMsgBody(podId: String, presentationId: String)
|
||||
|
@ -8,7 +8,7 @@ case class RegisterUserReqMsg(
|
||||
case class RegisterUserReqMsgBody(meetingId: String, intUserId: String, name: String, role: String,
|
||||
extUserId: String, authToken: String, sessionToken: String, avatarURL: String,
|
||||
guest: Boolean, authed: Boolean, guestStatus: String, excludeFromDashboard: Boolean,
|
||||
customParameters: Map[String, String])
|
||||
enforceLayout: String, customParameters: Map[String, String])
|
||||
|
||||
object UserRegisteredRespMsg { val NAME = "UserRegisteredRespMsg" }
|
||||
case class UserRegisteredRespMsg(
|
||||
|
@ -122,13 +122,19 @@ public class MeetingService implements MessageListener {
|
||||
public void registerUser(String meetingID, String internalUserId,
|
||||
String fullname, String role, String externUserID,
|
||||
String authToken, String sessionToken, String avatarURL, Boolean guest,
|
||||
Boolean authed, String guestStatus, Boolean excludeFromDashboard, Boolean leftGuestLobby, Map<String, String> customParameters) {
|
||||
handle(new RegisterUser(meetingID, internalUserId, fullname, role,
|
||||
externUserID, authToken, sessionToken, avatarURL, guest, authed, guestStatus, excludeFromDashboard, leftGuestLobby, customParameters));
|
||||
Boolean authed, String guestStatus, Boolean excludeFromDashboard, Boolean leftGuestLobby,
|
||||
String enforceLayout, Map<String, String> customParameters) {
|
||||
handle(
|
||||
new RegisterUser(meetingID, internalUserId, fullname, role,
|
||||
externUserID, authToken, sessionToken, avatarURL, guest, authed, guestStatus,
|
||||
excludeFromDashboard, leftGuestLobby, enforceLayout, customParameters
|
||||
)
|
||||
);
|
||||
|
||||
Meeting m = getMeeting(meetingID);
|
||||
if (m != null) {
|
||||
RegisteredUser ruser = new RegisteredUser(authToken, internalUserId, guestStatus, excludeFromDashboard, leftGuestLobby);
|
||||
RegisteredUser ruser = new RegisteredUser(authToken, internalUserId, guestStatus,
|
||||
excludeFromDashboard, leftGuestLobby, enforceLayout);
|
||||
m.userRegistered(ruser);
|
||||
}
|
||||
}
|
||||
@ -423,7 +429,7 @@ public class MeetingService implements MessageListener {
|
||||
gw.registerUser(message.meetingID,
|
||||
message.internalUserId, message.fullname, message.role,
|
||||
message.externUserID, message.authToken, message.sessionToken, message.avatarURL, message.guest,
|
||||
message.authed, message.guestStatus, message.excludeFromDashboard, message.customParameters);
|
||||
message.authed, message.guestStatus, message.excludeFromDashboard, message.enforceLayout, message.customParameters);
|
||||
}
|
||||
|
||||
public Meeting getMeeting(String meetingId) {
|
||||
|
@ -1152,6 +1152,11 @@ public class ParamsProcessorUtil {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean parentMeetingExists(String parentMeetingId) {
|
||||
Meeting meeting = ServiceUtils.findMeetingFromMeetingID(parentMeetingId);
|
||||
return meeting != null;
|
||||
}
|
||||
|
||||
/*************************************************
|
||||
* Setters
|
||||
************************************************/
|
||||
|
@ -2,11 +2,14 @@ package org.bigbluebutton.api;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
public final class Util {
|
||||
@ -20,6 +23,14 @@ public final class Util {
|
||||
throw new IllegalStateException("Utility class");
|
||||
}
|
||||
|
||||
public static String extractFilenameFromUrl(String preUploadedPresentation) throws MalformedURLException {
|
||||
URL url = new URL(preUploadedPresentation);
|
||||
String filename = FilenameUtils.getName(url.getPath());
|
||||
String extension = FilenameUtils.getExtension(url.getPath());
|
||||
if (extension == null || extension.isEmpty()) return null;
|
||||
return filename;
|
||||
}
|
||||
|
||||
public static boolean isMeetingIdValidFormat(String id) {
|
||||
Matcher matcher = MEETING_ID_PATTERN.matcher(id);
|
||||
if (matcher.matches()) {
|
||||
@ -51,7 +62,11 @@ public final class Util {
|
||||
}
|
||||
|
||||
public static String createNewFilename(String presId, String fileExt) {
|
||||
return presId + "." + fileExt;
|
||||
if (!fileExt.isEmpty()) {
|
||||
return presId + "." + fileExt;
|
||||
} else {
|
||||
return presId;
|
||||
}
|
||||
}
|
||||
|
||||
public static File createPresentationDir(String meetingId, String presentationDir, String presentationId) {
|
||||
|
@ -9,13 +9,16 @@ public class RegisteredUser {
|
||||
private Boolean excludeFromDashboard;
|
||||
private Long guestWaitedOn;
|
||||
private Boolean leftGuestLobby;
|
||||
private String enforceLayout;
|
||||
|
||||
public RegisteredUser(String authToken, String userId, String guestStatus, Boolean excludeFromDashboard, Boolean leftGuestLobby) {
|
||||
public RegisteredUser(String authToken, String userId, String guestStatus, Boolean excludeFromDashboard,
|
||||
Boolean leftGuestLobby, String enforceLayout) {
|
||||
this.authToken = authToken;
|
||||
this.userId = userId;
|
||||
this.guestStatus = guestStatus;
|
||||
this.excludeFromDashboard = excludeFromDashboard;
|
||||
this.leftGuestLobby = leftGuestLobby;
|
||||
this.enforceLayout = enforceLayout;
|
||||
|
||||
Long currentTimeMillis = System.currentTimeMillis();
|
||||
this.registeredOn = currentTimeMillis;
|
||||
@ -30,6 +33,10 @@ public class RegisteredUser {
|
||||
return guestStatus;
|
||||
}
|
||||
|
||||
public void setLeftGuestLobby(boolean bool) {
|
||||
this.leftGuestLobby = bool;
|
||||
}
|
||||
|
||||
public Boolean getLeftGuestLobby() {
|
||||
return leftGuestLobby;
|
||||
}
|
||||
@ -38,6 +45,14 @@ public class RegisteredUser {
|
||||
this.excludeFromDashboard = excludeFromDashboard;
|
||||
}
|
||||
|
||||
public String getEnforceLayout() {
|
||||
return enforceLayout;
|
||||
}
|
||||
|
||||
public void setEnforceLayout(String enforceLayout) {
|
||||
this.enforceLayout = enforceLayout;
|
||||
}
|
||||
|
||||
public Boolean getExcludeFromDashboard() {
|
||||
return excludeFromDashboard;
|
||||
}
|
||||
@ -46,9 +61,6 @@ public class RegisteredUser {
|
||||
this.guestWaitedOn = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public void setLeftGuestLobby(boolean bool) {
|
||||
this.leftGuestLobby = bool;
|
||||
}
|
||||
public Long getGuestWaitedOn() {
|
||||
return this.guestWaitedOn;
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ public class UserSession {
|
||||
public String welcome = null;
|
||||
public String logoutUrl = null;
|
||||
public String defaultLayout = "NOLAYOUT";
|
||||
public String enforceLayout = "";
|
||||
public String avatarURL;
|
||||
public String guestStatus = GuestPolicy.ALLOW;
|
||||
public String clientUrl = null;
|
||||
@ -130,6 +131,10 @@ public class UserSession {
|
||||
return defaultLayout;
|
||||
}
|
||||
|
||||
public String getEnforceLayout() {
|
||||
return enforceLayout;
|
||||
}
|
||||
|
||||
public String getAvatarURL() {
|
||||
return avatarURL;
|
||||
}
|
||||
|
@ -18,12 +18,13 @@ public class RegisterUser implements IMessage {
|
||||
public final String guestStatus;
|
||||
public final Boolean excludeFromDashboard;
|
||||
public final Boolean leftGuestLobby;
|
||||
public final String enforceLayout;
|
||||
public final Map<String, String> customParameters;
|
||||
|
||||
public RegisterUser(String meetingID, String internalUserId, String fullname, String role, String externUserID,
|
||||
String authToken, String sessionToken, String avatarURL, Boolean guest,
|
||||
Boolean authed, String guestStatus, Boolean excludeFromDashboard, Boolean leftGuestLobby,
|
||||
Map<String, String> customParameters) {
|
||||
String enforceLayout, Map<String, String> customParameters) {
|
||||
this.meetingID = meetingID;
|
||||
this.internalUserId = internalUserId;
|
||||
this.fullname = fullname;
|
||||
@ -37,6 +38,7 @@ public class RegisterUser implements IMessage {
|
||||
this.guestStatus = guestStatus;
|
||||
this.excludeFromDashboard = excludeFromDashboard;
|
||||
this.leftGuestLobby = leftGuestLobby;
|
||||
this.enforceLayout = enforceLayout;
|
||||
this.customParameters = customParameters;
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,8 @@ public interface IPublisherService {
|
||||
void endMeeting(String meetingId);
|
||||
void send(String channel, String message);
|
||||
void registerUser(String meetingID, String internalUserId, String fullname, String role, String externUserID,
|
||||
String authToken, String avatarURL, Boolean guest, Boolean excludeFromDashboard, Boolean authed);
|
||||
String authToken, String avatarURL, Boolean guest, Boolean excludeFromDashboard,
|
||||
String enforceLayout, Boolean authed);
|
||||
void sendKeepAlive(String system, Long bbbWebTimestamp, Long akkaAppsTimestamp);
|
||||
void sendStunTurnInfo(String meetingId, String internalUserId, Set<StunServer> stuns, Set<TurnEntry> turns);
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ public interface IBbbWebApiGWApp {
|
||||
void registerUser(String meetingID, String internalUserId, String fullname, String role,
|
||||
String externUserID, String authToken, String sessionToken, String avatarURL,
|
||||
Boolean guest, Boolean authed, String guestStatus, Boolean excludeFromDashboard,
|
||||
Map<String, String> customParameters);
|
||||
String enforceLayout, Map<String, String> customParameters);
|
||||
void guestWaitingLeft(String meetingID, String internalUserId);
|
||||
|
||||
void destroyMeeting(DestroyMeetingMessage msg);
|
||||
|
@ -1,5 +1,7 @@
|
||||
package org.bigbluebutton.presentation;
|
||||
|
||||
import org.bigbluebutton.api.domain.Extension;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static org.bigbluebutton.presentation.FileTypeConstants.*;
|
||||
@ -43,6 +45,15 @@ public class MimeTypeUtils {
|
||||
put(FileTypeConstants.SVG, Arrays.asList(SVG));
|
||||
}
|
||||
};
|
||||
|
||||
public String getExtensionBasedOnMimeType(String mimeType) {
|
||||
return EXTENSIONS_MIME.entrySet()
|
||||
.stream()
|
||||
.filter(entry -> entry.getValue().contains(mimeType))
|
||||
.map(Map.Entry::getKey)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public Boolean extensionMatchMimeType(String mimeType, String finalExtension) {
|
||||
finalExtension = finalExtension.toLowerCase();
|
||||
|
@ -108,14 +108,21 @@ public final class SupportedFileTypes {
|
||||
return "";
|
||||
}
|
||||
|
||||
public static String detectFileExtensionBasedOnMimeType(File pres) {
|
||||
String mimeType = detectMimeType(pres);
|
||||
return mimeTypeUtils.getExtensionBasedOnMimeType(mimeType);
|
||||
}
|
||||
|
||||
public static Boolean isPresentationMimeTypeValid(File pres, String fileExtension) {
|
||||
String mimeType = detectMimeType(pres);
|
||||
|
||||
if (mimeType.equals("")) {
|
||||
log.error("Not able to detect mimeType.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mimeTypeUtils.getValidMimeTypes().contains(mimeType)) {
|
||||
log.error("MimeType is not valid for this meeting, [{}]", mimeType);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -262,7 +262,7 @@ class BbbWebApiGWApp(
|
||||
role: String, extUserId: String, authToken: String, sessionToken: String,
|
||||
avatarURL: String, guest: java.lang.Boolean, authed: java.lang.Boolean,
|
||||
guestStatus: String, excludeFromDashboard: java.lang.Boolean,
|
||||
customParameters: java.util.Map[String, String]): Unit = {
|
||||
enforceLayout: String, customParameters: java.util.Map[String, String]): Unit = {
|
||||
|
||||
// meetingManagerActorRef ! new RegisterUser(meetingId = meetingId, intUserId = intUserId, name = name,
|
||||
// role = role, extUserId = extUserId, authToken = authToken, avatarURL = avatarURL,
|
||||
@ -271,7 +271,8 @@ class BbbWebApiGWApp(
|
||||
val regUser = new RegisterUser(meetingId = meetingId, intUserId = intUserId, name = name,
|
||||
role = role, extUserId = extUserId, authToken = authToken, sessionToken = sessionToken,
|
||||
avatarURL = avatarURL, guest = guest.booleanValue(), authed = authed.booleanValue(),
|
||||
guestStatus = guestStatus, excludeFromDashboard = excludeFromDashboard, customParameters = (customParameters).asScala.toMap)
|
||||
guestStatus = guestStatus, excludeFromDashboard = excludeFromDashboard, enforceLayout = enforceLayout,
|
||||
customParameters = (customParameters).asScala.toMap)
|
||||
|
||||
val event = MsgBuilder.buildRegisterUserRequestToAkkaApps(regUser)
|
||||
msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event))
|
||||
|
@ -12,6 +12,7 @@ import java.nio.charset.StandardCharsets
|
||||
import java.util.stream.Collectors
|
||||
import javax.imageio.ImageIO
|
||||
import scala.io.Source
|
||||
import scala.util.{ Failure, Success, Try, Using }
|
||||
import scala.xml.XML
|
||||
|
||||
object MsgBuilder {
|
||||
@ -49,7 +50,7 @@ object MsgBuilder {
|
||||
val body = RegisterUserReqMsgBody(meetingId = msg.meetingId, intUserId = msg.intUserId,
|
||||
name = msg.name, role = msg.role, extUserId = msg.extUserId, authToken = msg.authToken, sessionToken = msg.sessionToken,
|
||||
avatarURL = msg.avatarURL, guest = msg.guest, authed = msg.authed, guestStatus = msg.guestStatus,
|
||||
excludeFromDashboard = msg.excludeFromDashboard, customParameters = msg.customParameters)
|
||||
excludeFromDashboard = msg.excludeFromDashboard, enforceLayout = msg.enforceLayout, customParameters = msg.customParameters)
|
||||
val req = RegisterUserReqMsg(header, body)
|
||||
BbbCommonEnvCoreMsg(envelope, req)
|
||||
}
|
||||
@ -83,7 +84,12 @@ object MsgBuilder {
|
||||
|
||||
val urls = Map("thumb" -> thumbUrl, "text" -> txtUrl, "svg" -> svgUrl, "png" -> pngUrl)
|
||||
|
||||
try {
|
||||
val result = Using.Manager { use =>
|
||||
val contentUrl = new URL(txtUrl)
|
||||
val stream = use(new InputStreamReader(contentUrl.openStream(), StandardCharsets.UTF_8))
|
||||
val reader = use(new BufferedReader(stream))
|
||||
val content = reader.lines().collect(Collectors.joining("\n"))
|
||||
|
||||
val svgSource = Source.fromURL(new URL(svgUrl))
|
||||
val svgContent = svgSource.mkString
|
||||
svgSource.close()
|
||||
@ -100,10 +106,6 @@ object MsgBuilder {
|
||||
val width = w.toDouble
|
||||
val height = h.toDouble
|
||||
|
||||
val contentUrl = new URL(txtUrl)
|
||||
val reader = new BufferedReader(new InputStreamReader(contentUrl.openStream(), StandardCharsets.UTF_8))
|
||||
val content = reader.lines().collect(Collectors.joining("\n"))
|
||||
|
||||
PresentationPageConvertedVO(
|
||||
id = id,
|
||||
num = page,
|
||||
@ -113,7 +115,7 @@ object MsgBuilder {
|
||||
width = width,
|
||||
height = height
|
||||
)
|
||||
} catch {
|
||||
} recover {
|
||||
case e: Exception =>
|
||||
e.printStackTrace()
|
||||
PresentationPageConvertedVO(
|
||||
@ -124,6 +126,18 @@ object MsgBuilder {
|
||||
current = current
|
||||
)
|
||||
}
|
||||
|
||||
val presentationPage = result.getOrElse(
|
||||
PresentationPageConvertedVO(
|
||||
id = id,
|
||||
num = page,
|
||||
urls = urls,
|
||||
content = "",
|
||||
current = current
|
||||
)
|
||||
)
|
||||
|
||||
presentationPage
|
||||
}
|
||||
|
||||
def buildPresentationPageConvertedSysMsg(msg: DocPageGeneratedProgress): BbbCommonEnvCoreMsg = {
|
||||
|
@ -18,7 +18,7 @@ case class AddUserSession(token: String, session: UserSession)
|
||||
case class RegisterUser(meetingId: String, intUserId: String, name: String, role: String,
|
||||
extUserId: String, authToken: String, sessionToken: String, avatarURL: String,
|
||||
guest: Boolean, authed: Boolean, guestStatus: String, excludeFromDashboard: Boolean,
|
||||
customParameters: Map[String, String])
|
||||
enforceLayout: String, customParameters: Map[String, String])
|
||||
|
||||
case class CreateMeetingMsg(defaultProps: DefaultProps)
|
||||
|
||||
|
@ -29,7 +29,8 @@ trait ToAkkaAppsSendersTrait extends SystemConfiguration {
|
||||
val body = RegisterUserReqMsgBody(meetingId = msg.meetingId, intUserId = msg.intUserId,
|
||||
name = msg.name, role = msg.role, extUserId = msg.extUserId, authToken = msg.authToken,
|
||||
sessionToken = msg.sessionToken, avatarURL = msg.avatarURL, guest = msg.guest, authed = msg.authed,
|
||||
guestStatus = msg.guestStatus, excludeFromDashboard = msg.excludeFromDashboard, customParameters = msg.customParameters)
|
||||
guestStatus = msg.guestStatus, excludeFromDashboard = msg.excludeFromDashboard,
|
||||
enforceLayout = msg.enforceLayout, customParameters = msg.customParameters)
|
||||
val req = RegisterUserReqMsg(header, body)
|
||||
val message = BbbCommonEnvCoreMsg(envelope, req)
|
||||
sendToBus(message)
|
||||
|
@ -1,12 +0,0 @@
|
||||
package org.bigbluebutton.api2.meeting
|
||||
|
||||
case class UserSession2(authToken: String, internalUserId: String, conferencename: String,
|
||||
meetingID: String, externMeetingID: String, externUserID: String, fullname: String,
|
||||
role: String, conference: String, room: String, guest: Boolean = false,
|
||||
authed: Boolean = false, voicebridge: String, webvoiceconf: String,
|
||||
mode: String, record: String, welcome: String, logoutUrl: String,
|
||||
defaultLayout: String = "NOLAYOUT", avatarURL: String)
|
||||
|
||||
class UserSessions {
|
||||
|
||||
}
|
@ -30,6 +30,7 @@ function App() {
|
||||
const [sessionToken, setSessionToken] = useState(null);
|
||||
const [userId, setUserId] = useState(null);
|
||||
const [userName, setUserName] = useState(null);
|
||||
const [userAuthToken, setUserAuthToken] = useState(null);
|
||||
const [graphqlClient, setGraphqlClient] = useState(null);
|
||||
const [enterApiResponse, setEnterApiResponse] = useState('');
|
||||
|
||||
@ -46,6 +47,7 @@ function App() {
|
||||
if(json?.response?.internalUserID) {
|
||||
setUserId(json.response.internalUserID);
|
||||
setUserName(json.response.fullname);
|
||||
setUserAuthToken(json.response.authToken);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -110,7 +112,7 @@ function App() {
|
||||
<br />
|
||||
<PluginDataChannel userId={userId} />
|
||||
<br />
|
||||
<MyInfo />
|
||||
<MyInfo userAuthToken={userAuthToken} />
|
||||
<br />
|
||||
<PresPresentationUploadToken />
|
||||
<br />
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {gql, useMutation, useQuery, useSubscription} from '@apollo/client';
|
||||
import {gql, useMutation, useSubscription} from '@apollo/client';
|
||||
import React from "react";
|
||||
|
||||
export default function MyInfo() {
|
||||
export default function MyInfo({userAuthToken}) {
|
||||
|
||||
//where is not necessary once user can update only its own status
|
||||
//Hasura accepts "now()" as value to timestamp fields
|
||||
@ -20,17 +20,32 @@ export default function MyInfo() {
|
||||
updateUserClientEchoTestRunningAtMeAsNow();
|
||||
};
|
||||
|
||||
const [dispatchUserJoin] = useMutation(gql`
|
||||
mutation UserJoin($authToken: String!, $clientType: String!) {
|
||||
userJoin(
|
||||
authToken: $authToken,
|
||||
clientType: $clientType,
|
||||
)
|
||||
}
|
||||
`);
|
||||
const handleDispatchUserJoin = (authToken) => {
|
||||
dispatchUserJoin({
|
||||
variables: {
|
||||
authToken: authToken,
|
||||
clientType: 'HTML5',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const { loading, error, data } = useSubscription(
|
||||
gql`subscription {
|
||||
user_current {
|
||||
userId
|
||||
name
|
||||
meeting {
|
||||
name
|
||||
}
|
||||
echoTestRunningAt
|
||||
isRunningEchoTest
|
||||
joined
|
||||
joinErrorCode
|
||||
joinErrorMessage
|
||||
}
|
||||
}`
|
||||
);
|
||||
@ -45,23 +60,23 @@ export default function MyInfo() {
|
||||
{/*<th>Id</th>*/}
|
||||
<th>userId</th>
|
||||
<th>name</th>
|
||||
<th>Meeting</th>
|
||||
<th>echoTestRunningAt</th>
|
||||
<th>isRunningEchoTest</th>
|
||||
<th>joined</th>
|
||||
<th>joinErrorCode</th>
|
||||
<th>joinErrorMessage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.user_current.map((curr) => {
|
||||
console.log('meeting', curr);
|
||||
console.log('user_current', curr);
|
||||
return (
|
||||
<tr key={curr.userId}>
|
||||
<td>{curr.userId}</td>
|
||||
<td>{curr.name}</td>
|
||||
<td>{curr.meeting.name}</td>
|
||||
<td>{curr.echoTestRunningAt}
|
||||
<button onClick={() => handleUpdateUserEchoTestRunningAt()}>Set running now!</button>
|
||||
<td>{curr.joined ? 'Yes' : 'No'}
|
||||
{curr.joined ? '' : <button onClick={() => handleDispatchUserJoin(userAuthToken)}>Join Now!</button>}
|
||||
</td>
|
||||
<td>{curr.isRunningEchoTest ? 'Yes' : 'No'}</td>
|
||||
<td>{curr.joinErrorCode}</td>
|
||||
<td>{curr.joinErrorMessage}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
|
@ -230,12 +230,15 @@ CREATE TABLE "user" (
|
||||
"sessionToken" varchar(16),
|
||||
"authed" bool,
|
||||
"joined" bool,
|
||||
"joinErrorCode" varchar(50),
|
||||
"joinErrorMessage" varchar(400),
|
||||
"banned" bool,
|
||||
"loggedOut" bool, -- when user clicked Leave meeting button
|
||||
"guest" bool, --used for dialIn
|
||||
"guestStatus" varchar(50),
|
||||
"registeredOn" bigint,
|
||||
"excludeFromDashboard" bool,
|
||||
"enforceLayout" varchar(50),
|
||||
--columns of user state bellow
|
||||
"raiseHand" bool default false,
|
||||
"raiseHandTime" timestamp with time zone,
|
||||
@ -247,8 +250,8 @@ CREATE TABLE "user" (
|
||||
"guestLobbyMessage" text,
|
||||
"mobile" bool,
|
||||
"clientType" varchar(50),
|
||||
"disconnected" bool, -- this is the old leftFlag (that was renamed), set when the user just closed the client
|
||||
"expired" bool, -- when it is been some time the user is disconnected
|
||||
"disconnected" bool default false, -- this is the old leftFlag (that was renamed), set when the user just closed the client
|
||||
"expired" bool default false, -- when it is been some time the user is disconnected
|
||||
"ejected" bool,
|
||||
"ejectReason" varchar(255),
|
||||
"ejectReasonCode" varchar(50),
|
||||
@ -388,10 +391,13 @@ AS SELECT "user"."userId",
|
||||
-- "user"."guestStatus",
|
||||
"user"."mobile",
|
||||
"user"."clientType",
|
||||
"user"."enforceLayout",
|
||||
"user"."isDialIn",
|
||||
"user"."role",
|
||||
"user"."authed",
|
||||
"user"."joined",
|
||||
"user"."joinErrorCode",
|
||||
"user"."joinErrorMessage",
|
||||
"user"."disconnected",
|
||||
"user"."expired",
|
||||
"user"."ejected",
|
||||
@ -481,7 +487,7 @@ join meeting_welcome w USING("meetingId");
|
||||
|
||||
|
||||
CREATE TABLE "user_voice" (
|
||||
"userId" varchar(50) PRIMARY KEY NOT NULL REFERENCES "user"("userId") ON DELETE CASCADE,
|
||||
"userId" varchar(50) PRIMARY KEY NOT NULL REFERENCES "user"("userId") ON DELETE CASCADE,
|
||||
"voiceUserId" varchar(100),
|
||||
"callerName" varchar(100),
|
||||
"callerNum" varchar(100),
|
||||
@ -494,6 +500,9 @@ CREATE TABLE "user_voice" (
|
||||
"floor" boolean,
|
||||
"lastFloorTime" varchar(25),
|
||||
"voiceConf" varchar(100),
|
||||
"voiceConfCallSession" varchar(50),
|
||||
"voiceConfClientSession" varchar(10),
|
||||
"voiceConfCallState" varchar(30),
|
||||
"endTime" bigint,
|
||||
"startTime" bigint
|
||||
);
|
||||
@ -513,7 +522,8 @@ SELECT
|
||||
FROM "user" u
|
||||
JOIN "user_voice" ON "user_voice"."userId" = u."userId"
|
||||
LEFT JOIN "user_voice" user_talking ON (user_talking."userId" = u."userId" and user_talking."talking" IS TRUE)
|
||||
OR (user_talking."userId" = u."userId" and user_talking."hideTalkingIndicatorAt" > now());
|
||||
OR (user_talking."userId" = u."userId" and user_talking."hideTalkingIndicatorAt" > now())
|
||||
WHERE "user_voice"."joined" is true;
|
||||
|
||||
CREATE TABLE "user_camera" (
|
||||
"streamId" varchar(100) PRIMARY KEY,
|
||||
|
@ -23,3 +23,10 @@ type Mutation {
|
||||
): Boolean
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
userJoin(
|
||||
authToken: String!
|
||||
clientType: String!
|
||||
): Boolean
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,13 @@ actions:
|
||||
handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}'
|
||||
permissions:
|
||||
- role: bbb_client
|
||||
- name: userJoin
|
||||
definition:
|
||||
kind: synchronous
|
||||
handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}'
|
||||
permissions:
|
||||
- role: pre_join_bbb_client
|
||||
- role: bbb_client
|
||||
custom_types:
|
||||
enums: []
|
||||
input_objects: []
|
||||
|
@ -135,6 +135,7 @@ select_permissions:
|
||||
- away
|
||||
- banned
|
||||
- clientType
|
||||
- enforceLayout
|
||||
- color
|
||||
- disconnected
|
||||
- echoTestRunningAt
|
||||
@ -149,6 +150,8 @@ select_permissions:
|
||||
- isDialIn
|
||||
- isModerator
|
||||
- isRunningEchoTest
|
||||
- joinErrorCode
|
||||
- joinErrorMessage
|
||||
- joined
|
||||
- locked
|
||||
- loggedOut
|
||||
@ -158,16 +161,39 @@ select_permissions:
|
||||
- pinned
|
||||
- presenter
|
||||
- raiseHand
|
||||
- registeredAt
|
||||
- registeredOn
|
||||
- role
|
||||
- speechLocale
|
||||
- userId
|
||||
filter:
|
||||
_and:
|
||||
- meetingId:
|
||||
_eq: X-Hasura-MeetingId
|
||||
- userId:
|
||||
_eq: X-Hasura-UserId
|
||||
userId:
|
||||
_eq: X-Hasura-UserId
|
||||
- role: pre_join_bbb_client
|
||||
permission:
|
||||
columns:
|
||||
- authed
|
||||
- banned
|
||||
- color
|
||||
- disconnected
|
||||
- ejectReason
|
||||
- ejectReasonCode
|
||||
- ejected
|
||||
- expired
|
||||
- extId
|
||||
- guest
|
||||
- joinErrorCode
|
||||
- joinErrorMessage
|
||||
- joined
|
||||
- loggedOut
|
||||
- name
|
||||
- registeredAt
|
||||
- registeredOn
|
||||
- userId
|
||||
filter:
|
||||
userId:
|
||||
_eq: X-Hasura-UserId
|
||||
comment: ""
|
||||
update_permissions:
|
||||
- role: bbb_client
|
||||
permission:
|
||||
|
@ -36,6 +36,7 @@ select_permissions:
|
||||
- talking
|
||||
- userId
|
||||
- voiceConf
|
||||
- voiceConfCallState
|
||||
- voiceUserId
|
||||
filter:
|
||||
meetingId:
|
||||
|
@ -1,8 +1,8 @@
|
||||
- "!include public_v_caption.yaml"
|
||||
- "!include public_v_breakoutRoom.yaml"
|
||||
- "!include public_v_breakoutRoom_assignedUser.yaml"
|
||||
- "!include public_v_breakoutRoom_participant.yaml"
|
||||
- "!include public_v_breakoutRoom_user.yaml"
|
||||
- "!include public_v_caption.yaml"
|
||||
- "!include public_v_chat.yaml"
|
||||
- "!include public_v_chat_message_private.yaml"
|
||||
- "!include public_v_chat_message_public.yaml"
|
||||
|
@ -1 +1 @@
|
||||
FROM bigbluebutton/bbb-libreoffice:latest
|
||||
FROM bigbluebutton/bbb-libreoffice:7.6.2-20231020-161900
|
@ -1,21 +1,11 @@
|
||||
import Users from '/imports/api/users';
|
||||
import PresentationPods from '/imports/api/presentation-pods';
|
||||
import changePresenter from '/imports/api/users/server/modifiers/changePresenter';
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
function setPresenterInPodReqMsg(credentials) { // TODO-- switch to meetingId, etc
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'SetPresenterInPodReqMsg';
|
||||
|
||||
const { meetingId, requesterUserId, presenterId } = credentials;
|
||||
|
||||
const payload = {
|
||||
podId: 'DEFAULT_PRESENTATION_POD',
|
||||
nextPresenterId: presenterId,
|
||||
};
|
||||
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
|
||||
// TODO It will be removed soon
|
||||
Logger.debug(credentials);
|
||||
}
|
||||
|
||||
export default async function handlePresenterAssigned({ body }, meetingId) {
|
||||
|
5
bigbluebutton-html5/imports/ui/Types/hook.ts
Normal file
5
bigbluebutton-html5/imports/ui/Types/hook.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { FetchResult } from '@apollo/client';
|
||||
|
||||
export type GraphqlDataHookSubscriptionResponse<T> = FetchResult<T> & {
|
||||
loading: boolean;
|
||||
};
|
@ -277,6 +277,7 @@ class ActionsDropdown extends PureComponent {
|
||||
: intl.formatMessage(intlMessages.activateTimerStopwatchLabel),
|
||||
key: this.timerId,
|
||||
onClick: () => this.handleTimerClick(),
|
||||
dataTest: 'timerStopWatchFeature',
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,7 @@ import AudioCaptionsButtonContainer from '/imports/ui/components/audio/captions/
|
||||
import CaptionsReaderMenuContainer from '/imports/ui/components/captions/reader-menu/container';
|
||||
import ScreenshareButtonContainer from '/imports/ui/components/actions-bar/screenshare/container';
|
||||
import ReactionsButtonContainer from './reactions-button/container';
|
||||
import AudioControlsContainer from '../audio/audio-controls/container';
|
||||
import AudioControlsContainer from '../audio/audio-graphql/audio-controls/component';
|
||||
import JoinVideoOptionsContainer from '../video-provider/video-button/container';
|
||||
import PresentationOptionsContainer from './presentation-options/component';
|
||||
import RaiseHandDropdownContainer from './raise-hand/container';
|
||||
|
@ -4,11 +4,15 @@ import PropTypes from 'prop-types';
|
||||
import BBBMenu from '/imports/ui/components/common/menu/component';
|
||||
import UserReactionService from '/imports/ui/components/user-reaction/service';
|
||||
import UserListService from '/imports/ui/components/user-list/service';
|
||||
import { Emoji } from 'emoji-mart';
|
||||
import { convertRemToPixels } from '/imports/utils/dom-utils';
|
||||
import data from '@emoji-mart/data';
|
||||
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
|
||||
import { init } from 'emoji-mart';
|
||||
|
||||
import Styled from './styles';
|
||||
|
||||
const REACTIONS = Meteor.settings.public.userReaction.reactions;
|
||||
|
||||
const ReactionsButton = (props) => {
|
||||
const {
|
||||
intl,
|
||||
@ -16,10 +20,14 @@ const ReactionsButton = (props) => {
|
||||
userId,
|
||||
raiseHand,
|
||||
isMobile,
|
||||
shortcuts,
|
||||
currentUserReaction,
|
||||
autoCloseReactionsBar,
|
||||
} = props;
|
||||
|
||||
// initialize emoji-mart data, need for the new version
|
||||
init({ data });
|
||||
|
||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
@ -74,43 +82,20 @@ const ReactionsButton = (props) => {
|
||||
};
|
||||
|
||||
const emojiProps = {
|
||||
native: true,
|
||||
size: convertRemToPixels(1.5),
|
||||
padding: '4px',
|
||||
};
|
||||
|
||||
const reactions = [
|
||||
{
|
||||
id: 'smiley',
|
||||
native: '😃',
|
||||
},
|
||||
{
|
||||
id: 'neutral_face',
|
||||
native: '😐',
|
||||
},
|
||||
{
|
||||
id: 'slightly_frowning_face',
|
||||
native: '🙁',
|
||||
},
|
||||
{
|
||||
id: '+1',
|
||||
native: '👍',
|
||||
},
|
||||
{
|
||||
id: '-1',
|
||||
native: '👎',
|
||||
},
|
||||
{
|
||||
id: 'clap',
|
||||
native: '👏',
|
||||
},
|
||||
];
|
||||
const handReaction = {
|
||||
id: 'hand',
|
||||
native: '✋',
|
||||
};
|
||||
|
||||
let actions = [];
|
||||
|
||||
reactions.forEach(({ id, native }) => {
|
||||
REACTIONS.forEach(({ id, native }) => {
|
||||
actions.push({
|
||||
label: <Styled.ButtonWrapper active={currentUserReaction === native}><Emoji key={id} emoji={{ id }} {...emojiProps} /></Styled.ButtonWrapper>,
|
||||
label: <Styled.ButtonWrapper active={currentUserReaction === native}><em-emoji key={native} native={native} {...emojiProps} /></Styled.ButtonWrapper>,
|
||||
key: id,
|
||||
onClick: () => handleReactionSelect(native),
|
||||
customStyles: actionCustomStyles,
|
||||
@ -118,29 +103,29 @@ const ReactionsButton = (props) => {
|
||||
});
|
||||
|
||||
actions.push({
|
||||
label: <Styled.RaiseHandButtonWrapper isMobile={isMobile} data-test={raiseHand ? 'lowerHandBtn' : 'raiseHandBtn'} active={raiseHand}><Emoji key="hand" emoji={{ id: 'hand' }} {...emojiProps} />{RaiseHandButtonLabel()}</Styled.RaiseHandButtonWrapper>,
|
||||
label: <Styled.RaiseHandButtonWrapper accessKey={shortcuts.raisehand} isMobile={isMobile} data-test={raiseHand ? 'lowerHandBtn' : 'raiseHandBtn'} active={raiseHand}><em-emoji key={handReaction.id} native={handReaction.native} emoji={{ id: handReaction.id }} {...emojiProps} />{RaiseHandButtonLabel()}</Styled.RaiseHandButtonWrapper>,
|
||||
key: 'hand',
|
||||
onClick: () => handleRaiseHandButtonClick(),
|
||||
customStyles: {...actionCustomStyles, width: 'auto'},
|
||||
});
|
||||
|
||||
const icon = !raiseHand && currentUserReaction === 'none' ? 'hand' : null;
|
||||
const currentUserReactionEmoji = reactions.find(({ native }) => native === currentUserReaction);
|
||||
const currentUserReactionEmoji = REACTIONS.find(({ native }) => native === currentUserReaction);
|
||||
|
||||
let customIcon = null;
|
||||
|
||||
if (raiseHand) {
|
||||
customIcon = <Emoji key="hand" emoji={{ id: 'hand' }} {...emojiProps} />;
|
||||
customIcon = <em-emoji key={handReaction.id} native={handReaction.native} emoji={handReaction} {...emojiProps} />;
|
||||
} else {
|
||||
if (!icon) {
|
||||
customIcon = <Emoji key={currentUserReactionEmoji?.id} emoji={{ id: currentUserReactionEmoji?.id }} {...emojiProps} />;
|
||||
customIcon = <em-emoji key={currentUserReactionEmoji?.id} native={currentUserReactionEmoji?.native} emoji={{ id: currentUserReactionEmoji?.id }} {...emojiProps} />;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<BBBMenu
|
||||
trigger={(
|
||||
<Styled.ReactionsDropdown>
|
||||
<Styled.ReactionsDropdown id="interactionsButton">
|
||||
<Styled.RaiseHandButton
|
||||
data-test="reactionsButton"
|
||||
icon={icon}
|
||||
@ -193,4 +178,4 @@ const propTypes = {
|
||||
|
||||
ReactionsButton.propTypes = propTypes;
|
||||
|
||||
export default ReactionsButton;
|
||||
export default withShortcutHelper(ReactionsButton, ['raiseHand']);
|
||||
|
@ -101,28 +101,13 @@ const ReactionsDropdown = styled.div`
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.emoji-mart-bar {
|
||||
display: none;
|
||||
}
|
||||
.emoji-mart-search {
|
||||
display: none;
|
||||
}
|
||||
.emoji-mart-category[aria-label="Frequently Used"] {
|
||||
display: none;
|
||||
}
|
||||
.emoji-mart-category-label{
|
||||
display: none;
|
||||
}
|
||||
.emoji-mart{
|
||||
border: none;
|
||||
}
|
||||
@media(min-width: 600px) {
|
||||
.emoji-mart-scroll{
|
||||
overflow:hidden;
|
||||
padding: 0;
|
||||
height: 270px;
|
||||
width: 280px;
|
||||
}
|
||||
overflow: hidden;
|
||||
margin: 0.2em 0.2em 0.2em 0.2em;
|
||||
text-align: center;
|
||||
max-height: 270px;
|
||||
width: 270px;
|
||||
em-emoji {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -29,6 +29,7 @@ import WebcamContainer from '../webcam/container';
|
||||
import PresentationContainer from '../presentation/container';
|
||||
import ScreenshareContainer from '../screenshare/container';
|
||||
import ExternalVideoContainer from '../external-video-player/container';
|
||||
import EmojiRainContainer from '../emoji-rain/container';
|
||||
import Styled from './styles';
|
||||
import { DEVICE_TYPE, ACTIONS, SMALL_VIEWPORT_BREAKPOINT, PANELS } from '../layout/enums';
|
||||
import {
|
||||
@ -521,6 +522,7 @@ class App extends Component {
|
||||
setPushLayout,
|
||||
shouldShowScreenshare,
|
||||
shouldShowExternalVideo,
|
||||
enforceLayout,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@ -551,6 +553,7 @@ class App extends Component {
|
||||
setPushLayout,
|
||||
shouldShowScreenshare,
|
||||
shouldShowExternalVideo: !!shouldShowExternalVideo,
|
||||
enforceLayout,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@ -663,6 +666,7 @@ class App extends Component {
|
||||
<PadsSessionsContainer />
|
||||
<WakeLockContainer />
|
||||
{this.renderActionsBar()}
|
||||
<EmojiRainContainer />
|
||||
{customStyleUrl ? <link rel="stylesheet" type="text/css" href={customStyleUrl} /> : null}
|
||||
{customStyle ? <link rel="stylesheet" type="text/css" href={`data:text/css;charset=UTF-8,${encodeURIComponent(customStyle)}`} /> : null}
|
||||
{isRandomUserSelectModalOpen ? <RandomUserSelectContainer
|
||||
|
@ -22,6 +22,8 @@ import {
|
||||
layoutDispatch,
|
||||
} from '../layout/context';
|
||||
import { isEqual } from 'radash';
|
||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||
import { LAYOUT_TYPE } from '/imports/ui/components/layout/enums';
|
||||
|
||||
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
|
||||
|
||||
@ -147,6 +149,16 @@ const AppContainer = (props) => {
|
||||
MediaService.buildLayoutWhenPresentationAreaIsDisabled(layoutContextDispatch)
|
||||
});
|
||||
|
||||
const validateEnforceLayout = (currentUserData) => {
|
||||
const layoutTypes = Object.values(LAYOUT_TYPE);
|
||||
const enforceLayout = currentUserData?.enforceLayout;
|
||||
return enforceLayout && layoutTypes.includes(enforceLayout) ? enforceLayout : null;
|
||||
};
|
||||
|
||||
const { data: currentUserData } = useCurrentUser((user) => ({
|
||||
enforceLayout: user.enforceLayout,
|
||||
}));
|
||||
|
||||
return currentUserId
|
||||
? (
|
||||
<App
|
||||
@ -185,6 +197,7 @@ const AppContainer = (props) => {
|
||||
setMountRandomUserModal,
|
||||
isPresenter,
|
||||
numCameras: cameraDockInput.numCameras,
|
||||
enforceLayout: validateEnforceLayout(currentUserData),
|
||||
}}
|
||||
{...otherProps}
|
||||
/>
|
||||
|
@ -1,183 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import deviceInfo from '/imports/utils/deviceInfo';
|
||||
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
|
||||
import InputStreamLiveSelectorContainer from './input-stream-live-selector/container';
|
||||
import MutedAlert from '/imports/ui/components/muted-alert/component';
|
||||
import Styled from './styles';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
import AudioModalContainer from '../audio-modal/container';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
joinAudio: {
|
||||
id: 'app.audio.joinAudio',
|
||||
description: 'Join audio button label',
|
||||
},
|
||||
leaveAudio: {
|
||||
id: 'app.audio.leaveAudio',
|
||||
description: 'Leave audio button label',
|
||||
},
|
||||
muteAudio: {
|
||||
id: 'app.actionsBar.muteLabel',
|
||||
description: 'Mute audio button label',
|
||||
},
|
||||
unmuteAudio: {
|
||||
id: 'app.actionsBar.unmuteLabel',
|
||||
description: 'Unmute audio button label',
|
||||
},
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
shortcuts: PropTypes.objectOf(PropTypes.string).isRequired,
|
||||
handleToggleMuteMicrophone: PropTypes.func.isRequired,
|
||||
handleLeaveAudio: PropTypes.func.isRequired,
|
||||
disable: PropTypes.bool.isRequired,
|
||||
muted: PropTypes.bool.isRequired,
|
||||
showMute: PropTypes.bool.isRequired,
|
||||
inAudio: PropTypes.bool.isRequired,
|
||||
listenOnly: PropTypes.bool.isRequired,
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
talking: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
class AudioControls extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isAudioModalOpen: false,
|
||||
};
|
||||
|
||||
this.renderButtonsAndStreamSelector = this.renderButtonsAndStreamSelector.bind(this);
|
||||
this.renderJoinLeaveButton = this.renderJoinLeaveButton.bind(this);
|
||||
this.setAudioModalIsOpen = this.setAudioModalIsOpen.bind(this);
|
||||
}
|
||||
|
||||
renderJoinButton() {
|
||||
const {
|
||||
disable,
|
||||
intl,
|
||||
shortcuts,
|
||||
joinListenOnly,
|
||||
isConnected
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={() => this.handleJoinAudio(joinListenOnly, isConnected)}
|
||||
disabled={disable}
|
||||
hideLabel
|
||||
aria-label={intl.formatMessage(intlMessages.joinAudio)}
|
||||
label={intl.formatMessage(intlMessages.joinAudio)}
|
||||
data-test="joinAudio"
|
||||
color="default"
|
||||
ghost
|
||||
icon="no_audio"
|
||||
size="lg"
|
||||
circle
|
||||
accessKey={shortcuts.joinaudio}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderButtonsAndStreamSelector(_enableDynamicDeviceSelection) {
|
||||
const {
|
||||
handleLeaveAudio, handleToggleMuteMicrophone, muted, disable, talking,
|
||||
} = this.props;
|
||||
|
||||
const { isMobile } = deviceInfo;
|
||||
|
||||
return (
|
||||
<InputStreamLiveSelectorContainer {...{
|
||||
handleLeaveAudio,
|
||||
handleToggleMuteMicrophone,
|
||||
muted,
|
||||
disable,
|
||||
talking,
|
||||
isMobile,
|
||||
_enableDynamicDeviceSelection,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderJoinLeaveButton() {
|
||||
const {
|
||||
inAudio,
|
||||
} = this.props;
|
||||
|
||||
const { isMobile } = deviceInfo;
|
||||
|
||||
let { enableDynamicAudioDeviceSelection } = Meteor.settings.public.app;
|
||||
|
||||
if (typeof enableDynamicAudioDeviceSelection === 'undefined') {
|
||||
enableDynamicAudioDeviceSelection = true;
|
||||
}
|
||||
|
||||
const _enableDynamicDeviceSelection = enableDynamicAudioDeviceSelection && !isMobile;
|
||||
|
||||
if (inAudio) {
|
||||
return this.renderButtonsAndStreamSelector(_enableDynamicDeviceSelection);
|
||||
}
|
||||
|
||||
return this.renderJoinButton();
|
||||
}
|
||||
|
||||
handleJoinAudio(joinListenOnly, isConnected) {
|
||||
(isConnected()
|
||||
? joinListenOnly()
|
||||
: this.setAudioModalIsOpen(true)
|
||||
)}
|
||||
|
||||
setAudioModalIsOpen(value) {
|
||||
this.setState({ isAudioModalOpen: value })
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
showMute,
|
||||
muted,
|
||||
isVoiceUser,
|
||||
listenOnly,
|
||||
inputStream,
|
||||
isViewer,
|
||||
isPresenter,
|
||||
} = this.props;
|
||||
|
||||
const { isAudioModalOpen } = this.state;
|
||||
|
||||
const MUTE_ALERT_CONFIG = Meteor.settings.public.app.mutedAlert;
|
||||
const { enabled: muteAlertEnabled } = MUTE_ALERT_CONFIG;
|
||||
|
||||
return (
|
||||
<Styled.Container>
|
||||
{isVoiceUser && inputStream && muteAlertEnabled && !listenOnly && muted && showMute ? (
|
||||
<MutedAlert {...{
|
||||
muted, inputStream, isViewer, isPresenter,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{
|
||||
this.renderJoinLeaveButton()
|
||||
}
|
||||
{
|
||||
isAudioModalOpen ? <AudioModalContainer
|
||||
{...{
|
||||
priority: "low",
|
||||
setIsOpen: this.setAudioModalIsOpen,
|
||||
isOpen: isAudioModalOpen
|
||||
}}
|
||||
/> : null
|
||||
}
|
||||
</Styled.Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AudioControls.propTypes = propTypes;
|
||||
|
||||
export default withShortcutHelper(injectIntl(AudioControls), ['joinAudio',
|
||||
'leaveAudio', 'toggleMute']);
|
@ -1,104 +0,0 @@
|
||||
import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import AudioManager from '/imports/ui/services/audio-manager';
|
||||
import lockContextContainer from '/imports/ui/components/lock-viewers/context/container';
|
||||
import { withUsersConsumer } from '/imports/ui/components/components-data/users-context/context';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import Storage from '/imports/ui/services/storage/session';
|
||||
import getFromUserSettings from '/imports/ui/services/users-settings';
|
||||
import AudioControls from './component';
|
||||
import {
|
||||
setUserSelectedMicrophone,
|
||||
setUserSelectedListenOnly,
|
||||
} from '../audio-modal/service';
|
||||
import { layoutSelect } from '/imports/ui/components/layout/context';
|
||||
import AudioControlsContainerGraphql from '../audio-graphql/audio-controls/component';
|
||||
|
||||
import Service from '../service';
|
||||
import AppService from '/imports/ui/components/app/service';
|
||||
|
||||
const ROLE_VIEWER = Meteor.settings.public.user.role_viewer;
|
||||
const APP_CONFIG = Meteor.settings.public.app;
|
||||
|
||||
const AudioControlsContainer = (props) => {
|
||||
const { users, lockSettings, userLocks, children, ...newProps } = props;
|
||||
const isRTL = layoutSelect((i) => i.isRTL);
|
||||
return <AudioControls {...{ ...newProps, isRTL }} />;
|
||||
};
|
||||
|
||||
const handleLeaveAudio = () => {
|
||||
const meetingIsBreakout = AppService.meetingIsBreakout();
|
||||
|
||||
if (!meetingIsBreakout) {
|
||||
setUserSelectedMicrophone(false);
|
||||
setUserSelectedListenOnly(false);
|
||||
}
|
||||
|
||||
const skipOnFistJoin = getFromUserSettings(
|
||||
'bbb_skip_check_audio_on_first_join',
|
||||
APP_CONFIG.skipCheckOnJoin
|
||||
);
|
||||
if (skipOnFistJoin && !Storage.getItem('getEchoTest')) {
|
||||
Storage.setItem('getEchoTest', true);
|
||||
}
|
||||
|
||||
Service.forceExitAudio();
|
||||
logger.info(
|
||||
{
|
||||
logCode: 'audiocontrols_leave_audio',
|
||||
extraInfo: { logType: 'user_action' },
|
||||
},
|
||||
'audio connection closed by user'
|
||||
);
|
||||
};
|
||||
|
||||
const {
|
||||
isVoiceUser,
|
||||
isConnected,
|
||||
isListenOnly,
|
||||
isEchoTest,
|
||||
isMuted,
|
||||
isConnecting,
|
||||
isHangingUp,
|
||||
isTalking,
|
||||
toggleMuteMicrophone,
|
||||
joinListenOnly,
|
||||
} = Service;
|
||||
|
||||
withUsersConsumer(
|
||||
lockContextContainer(
|
||||
withTracker(({ userLocks, users }) => {
|
||||
const currentUser = users[Auth.meetingID][Auth.userID];
|
||||
const isViewer = currentUser.role === ROLE_VIEWER;
|
||||
const isPresenter = currentUser.presenter;
|
||||
const { status } = Service.getBreakoutAudioTransferStatus();
|
||||
|
||||
if (status === AudioManager.BREAKOUT_AUDIO_TRANSFER_STATES.RETURNING) {
|
||||
Service.setBreakoutAudioTransferStatus({
|
||||
status: AudioManager.BREAKOUT_AUDIO_TRANSFER_STATES.DISCONNECTED,
|
||||
});
|
||||
Service.recoverMicState();
|
||||
}
|
||||
|
||||
return {
|
||||
showMute: isConnected() && !isListenOnly() && !isEchoTest() && !userLocks.userMic,
|
||||
muted: isConnected() && !isListenOnly() && isMuted(),
|
||||
inAudio: isConnected() && !isEchoTest(),
|
||||
listenOnly: isConnected() && isListenOnly(),
|
||||
disable: isConnecting() || isHangingUp() || !Meteor.status().connected,
|
||||
talking: isTalking() && !isMuted(),
|
||||
isVoiceUser: isVoiceUser(),
|
||||
handleToggleMuteMicrophone: () => toggleMuteMicrophone(),
|
||||
joinListenOnly,
|
||||
handleLeaveAudio,
|
||||
inputStream: AudioManager.inputStream,
|
||||
isViewer,
|
||||
isPresenter,
|
||||
isConnected,
|
||||
};
|
||||
})(AudioControlsContainer)
|
||||
)
|
||||
);
|
||||
|
||||
export default AudioControlsContainerGraphql;
|
@ -1,501 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
import BBBMenu from '/imports/ui/components/common/menu/component';
|
||||
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
|
||||
|
||||
import Styled from './styles';
|
||||
|
||||
const AUDIO_INPUT = 'audioinput';
|
||||
const AUDIO_OUTPUT = 'audiooutput';
|
||||
const DEFAULT_DEVICE = 'default';
|
||||
const DEVICE_LABEL_MAX_LENGTH = 40;
|
||||
const SET_SINK_ID_SUPPORTED = 'setSinkId' in HTMLMediaElement.prototype;
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
changeAudioDevice: {
|
||||
id: 'app.audio.changeAudioDevice',
|
||||
description: 'Change audio device button label',
|
||||
},
|
||||
leaveAudio: {
|
||||
id: 'app.audio.leaveAudio',
|
||||
description: 'Leave audio dropdown item label',
|
||||
},
|
||||
loading: {
|
||||
id: 'app.audio.loading',
|
||||
description: 'Loading audio dropdown item label',
|
||||
},
|
||||
noDeviceFound: {
|
||||
id: 'app.audio.noDeviceFound',
|
||||
description: 'No device found',
|
||||
},
|
||||
microphones: {
|
||||
id: 'app.audio.microphones',
|
||||
description: 'Input audio dropdown item label',
|
||||
},
|
||||
speakers: {
|
||||
id: 'app.audio.speakers',
|
||||
description: 'Output audio dropdown item label',
|
||||
},
|
||||
muteAudio: {
|
||||
id: 'app.actionsBar.muteLabel',
|
||||
description: 'Mute audio button label',
|
||||
},
|
||||
unmuteAudio: {
|
||||
id: 'app.actionsBar.unmuteLabel',
|
||||
description: 'Unmute audio button label',
|
||||
},
|
||||
deviceChangeFailed: {
|
||||
id: 'app.audioNotification.deviceChangeFailed',
|
||||
description: 'Device change failed',
|
||||
},
|
||||
defaultOutputDeviceLabel: {
|
||||
id: 'app.audio.audioSettings.defaultOutputDeviceLabel',
|
||||
description: 'Default output device label',
|
||||
},
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
liveChangeInputDevice: PropTypes.func.isRequired,
|
||||
handleLeaveAudio: PropTypes.func.isRequired,
|
||||
liveChangeOutputDevice: PropTypes.func.isRequired,
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
shortcuts: PropTypes.objectOf(PropTypes.string).isRequired,
|
||||
currentInputDeviceId: PropTypes.string.isRequired,
|
||||
currentOutputDeviceId: PropTypes.string.isRequired,
|
||||
isListenOnly: PropTypes.bool.isRequired,
|
||||
isAudioConnected: PropTypes.bool.isRequired,
|
||||
_enableDynamicDeviceSelection: PropTypes.bool.isRequired,
|
||||
handleToggleMuteMicrophone: PropTypes.func.isRequired,
|
||||
muted: PropTypes.bool.isRequired,
|
||||
disable: PropTypes.bool.isRequired,
|
||||
talking: PropTypes.bool,
|
||||
notify: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
talking: false,
|
||||
};
|
||||
|
||||
class InputStreamLiveSelector extends Component {
|
||||
static truncateDeviceName(deviceName) {
|
||||
if (deviceName && (deviceName.length <= DEVICE_LABEL_MAX_LENGTH)) {
|
||||
return deviceName;
|
||||
}
|
||||
return `${deviceName.substring(0, DEVICE_LABEL_MAX_LENGTH - 3)}...`;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.updateDeviceList = this.updateDeviceList.bind(this);
|
||||
this.renderDeviceList = this.renderDeviceList.bind(this);
|
||||
this.renderListenOnlyButton = this.renderListenOnlyButton.bind(this);
|
||||
this.renderMuteToggleButton = this.renderMuteToggleButton.bind(this);
|
||||
this.renderButtonsWithSelectorDevice = this.renderButtonsWithSelectorDevice.bind(this);
|
||||
this.renderButtonsWithoutSelectorDevice = this.renderButtonsWithoutSelectorDevice.bind(this);
|
||||
this.state = {
|
||||
audioInputDevices: null,
|
||||
audioOutputDevices: null,
|
||||
selectedInputDeviceId: null,
|
||||
selectedOutputDeviceId: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateDeviceList().then(() => {
|
||||
navigator.mediaDevices
|
||||
.addEventListener('devicechange', this.updateDeviceList);
|
||||
this.setCurrentDevices();
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
navigator.mediaDevices.removeEventListener('devicechange',
|
||||
this.updateDeviceList);
|
||||
}
|
||||
|
||||
onDeviceListClick(deviceId, deviceKind, callback) {
|
||||
if (!deviceId) return;
|
||||
|
||||
const { intl, notify } = this.props;
|
||||
|
||||
if (deviceKind === AUDIO_INPUT) {
|
||||
callback(deviceId).then(() => {
|
||||
this.setState({ selectedInputDeviceId: deviceId });
|
||||
}).catch((error) => {
|
||||
notify(intl.formatMessage(intlMessages.deviceChangeFailed), true);
|
||||
});
|
||||
} else {
|
||||
callback(deviceId, true).then(() => {
|
||||
this.setState({ selectedOutputDeviceId: deviceId });
|
||||
}).catch((error) => {
|
||||
notify(intl.formatMessage(intlMessages.deviceChangeFailed), true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setCurrentDevices() {
|
||||
const {
|
||||
currentInputDeviceId,
|
||||
currentOutputDeviceId,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
audioInputDevices,
|
||||
audioOutputDevices,
|
||||
} = this.state;
|
||||
|
||||
if (!audioInputDevices
|
||||
|| !audioInputDevices[0]
|
||||
|| !audioOutputDevices
|
||||
|| !audioOutputDevices[0]) return;
|
||||
|
||||
const _currentInputDeviceId = audioInputDevices.find(
|
||||
(d) => d.deviceId === currentInputDeviceId,
|
||||
) ? currentInputDeviceId : audioInputDevices[0].deviceId;
|
||||
|
||||
const _currentOutputDeviceId = audioOutputDevices.find(
|
||||
(d) => d.deviceId === currentOutputDeviceId,
|
||||
) ? currentOutputDeviceId : audioOutputDevices[0].deviceId;
|
||||
|
||||
this.setState({
|
||||
selectedInputDeviceId: _currentInputDeviceId,
|
||||
selectedOutputDeviceId: _currentOutputDeviceId,
|
||||
});
|
||||
}
|
||||
|
||||
fallbackInputDevice(fallbackDevice) {
|
||||
if (!fallbackDevice || !fallbackDevice.deviceId) return;
|
||||
|
||||
const {
|
||||
liveChangeInputDevice,
|
||||
} = this.props;
|
||||
|
||||
logger.info({
|
||||
logCode: 'audio_device_live_selector',
|
||||
extraInfo: {
|
||||
userId: Auth.userID,
|
||||
meetingId: Auth.meetingID,
|
||||
},
|
||||
}, 'Current input device was removed. Fallback to default device');
|
||||
liveChangeInputDevice(fallbackDevice.deviceId).then(() => {
|
||||
this.setState({ selectedInputDeviceId: fallbackDevice.deviceId });
|
||||
}).catch((error) => {
|
||||
notify(intl.formatMessage(intlMessages.deviceChangeFailed), true);
|
||||
});
|
||||
}
|
||||
|
||||
fallbackOutputDevice(fallbackDevice) {
|
||||
if (!fallbackDevice || !fallbackDevice.deviceId) return;
|
||||
|
||||
const {
|
||||
liveChangeOutputDevice,
|
||||
} = this.props;
|
||||
|
||||
logger.info({
|
||||
logCode: 'audio_device_live_selector',
|
||||
extraInfo: {
|
||||
userId: Auth.userID,
|
||||
meetingId: Auth.meetingID,
|
||||
},
|
||||
}, 'Current output device was removed. Fallback to default device');
|
||||
liveChangeOutputDevice(fallbackDevice.deviceId, true).then(() => {
|
||||
this.setState({ selectedOutputDeviceId: fallbackDevice.deviceId });
|
||||
}).catch((error) => {
|
||||
notify(intl.formatMessage(intlMessages.deviceChangeFailed), true);
|
||||
});
|
||||
}
|
||||
|
||||
updateRemovedDevices(audioInputDevices, audioOutputDevices) {
|
||||
const {
|
||||
selectedInputDeviceId,
|
||||
selectedOutputDeviceId,
|
||||
} = this.state;
|
||||
|
||||
if (selectedInputDeviceId
|
||||
&& (selectedInputDeviceId !== DEFAULT_DEVICE)
|
||||
&& !audioInputDevices.find((d) => d.deviceId === selectedInputDeviceId)) {
|
||||
this.fallbackInputDevice(audioInputDevices[0]);
|
||||
}
|
||||
|
||||
if (selectedOutputDeviceId
|
||||
&& (selectedOutputDeviceId !== DEFAULT_DEVICE)
|
||||
&& !audioOutputDevices.find((d) => d.deviceId === selectedOutputDeviceId)) {
|
||||
this.fallbackOutputDevice(audioOutputDevices[0]);
|
||||
}
|
||||
}
|
||||
|
||||
updateDeviceList() {
|
||||
const {
|
||||
isAudioConnected,
|
||||
} = this.props;
|
||||
|
||||
return navigator.mediaDevices.enumerateDevices()
|
||||
.then((devices) => {
|
||||
const audioInputDevices = devices.filter((i) => i.kind === AUDIO_INPUT);
|
||||
const audioOutputDevices = devices.filter((i) => i.kind === AUDIO_OUTPUT);
|
||||
|
||||
this.setState({
|
||||
audioInputDevices,
|
||||
audioOutputDevices,
|
||||
});
|
||||
|
||||
if (isAudioConnected) {
|
||||
this.updateRemovedDevices(audioInputDevices, audioOutputDevices);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderDeviceList(deviceKind, list, callback, title, currentDeviceId) {
|
||||
const {
|
||||
intl,
|
||||
} = this.props;
|
||||
const listLength = list ? list.length : -1;
|
||||
const listTitle = [
|
||||
{
|
||||
key: `audioDeviceList-${deviceKind}`,
|
||||
label: title,
|
||||
iconRight: (deviceKind === 'audioinput') ? 'unmute' : 'volume_level_2',
|
||||
disabled: true,
|
||||
customStyles: Styled.DisabledLabel,
|
||||
},
|
||||
{
|
||||
key: 'separator-01',
|
||||
isSeparator: true,
|
||||
},
|
||||
];
|
||||
|
||||
let deviceList = [];
|
||||
|
||||
if (listLength > 0) {
|
||||
deviceList = list.map((device, index) => (
|
||||
{
|
||||
key: `${device.deviceId}-${deviceKind}`,
|
||||
dataTest: `${deviceKind}-${index + 1}`,
|
||||
label: InputStreamLiveSelector.truncateDeviceName(device.label),
|
||||
customStyles: (device.deviceId === currentDeviceId) && Styled.SelectedLabel,
|
||||
iconRight: (device.deviceId === currentDeviceId) ? 'check' : null,
|
||||
onClick: () => this.onDeviceListClick(device.deviceId, deviceKind, callback),
|
||||
}
|
||||
));
|
||||
} else if (deviceKind === AUDIO_OUTPUT && !SET_SINK_ID_SUPPORTED && listLength === 0) {
|
||||
// If the browser doesn't support setSinkId, show the chosen output device
|
||||
// as a placeholder Default - like it's done in audio/device-selector
|
||||
deviceList = [
|
||||
{
|
||||
key: `defaultDeviceKey-${deviceKind}`,
|
||||
label: intl.formatMessage(intlMessages.defaultOutputDeviceLabel),
|
||||
customStyles: Styled.SelectedLabel,
|
||||
iconRight: 'check',
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
deviceList = [
|
||||
{
|
||||
key: `noDeviceFoundKey-${deviceKind}-`,
|
||||
label: listLength < 0
|
||||
? intl.formatMessage(intlMessages.loading)
|
||||
: intl.formatMessage(intlMessages.noDeviceFound),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return listTitle.concat(deviceList);
|
||||
}
|
||||
|
||||
renderMuteToggleButton() {
|
||||
const {
|
||||
intl,
|
||||
shortcuts,
|
||||
handleToggleMuteMicrophone,
|
||||
muted,
|
||||
disable,
|
||||
talking,
|
||||
} = this.props;
|
||||
|
||||
const label = muted ? intl.formatMessage(intlMessages.unmuteAudio)
|
||||
: intl.formatMessage(intlMessages.muteAudio);
|
||||
|
||||
const { animations } = Settings.application;
|
||||
|
||||
return (
|
||||
<Styled.MuteToggleButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleToggleMuteMicrophone();
|
||||
}}
|
||||
disabled={disable}
|
||||
hideLabel
|
||||
label={label}
|
||||
aria-label={label}
|
||||
color={!muted ? 'primary' : 'default'}
|
||||
ghost={muted}
|
||||
icon={muted ? 'mute' : 'unmute'}
|
||||
size="lg"
|
||||
circle
|
||||
accessKey={shortcuts.togglemute}
|
||||
$talking={talking || undefined}
|
||||
animations={animations}
|
||||
data-test={muted ? 'unmuteMicButton' : 'muteMicButton'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderListenOnlyButton() {
|
||||
const {
|
||||
handleLeaveAudio,
|
||||
intl,
|
||||
shortcuts,
|
||||
isListenOnly,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Button
|
||||
aria-label={intl.formatMessage(intlMessages.leaveAudio)}
|
||||
label={intl.formatMessage(intlMessages.leaveAudio)}
|
||||
accessKey={shortcuts.leaveaudio}
|
||||
data-test="leaveListenOnly"
|
||||
hideLabel
|
||||
color="primary"
|
||||
icon={isListenOnly ? 'listen' : 'volume_level_2'}
|
||||
size="lg"
|
||||
circle
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleLeaveAudio();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderButtonsWithSelectorDevice() {
|
||||
const {
|
||||
audioInputDevices,
|
||||
audioOutputDevices,
|
||||
selectedInputDeviceId,
|
||||
selectedOutputDeviceId,
|
||||
} = this.state;
|
||||
|
||||
const {
|
||||
liveChangeInputDevice,
|
||||
handleLeaveAudio,
|
||||
liveChangeOutputDevice,
|
||||
intl,
|
||||
currentInputDeviceId,
|
||||
currentOutputDeviceId,
|
||||
isListenOnly,
|
||||
shortcuts,
|
||||
isMobile,
|
||||
} = this.props;
|
||||
|
||||
const inputDeviceList = !isListenOnly
|
||||
? this.renderDeviceList(
|
||||
AUDIO_INPUT,
|
||||
audioInputDevices,
|
||||
liveChangeInputDevice,
|
||||
intl.formatMessage(intlMessages.microphones),
|
||||
selectedInputDeviceId || currentInputDeviceId,
|
||||
) : [];
|
||||
|
||||
const outputDeviceList = this.renderDeviceList(
|
||||
AUDIO_OUTPUT,
|
||||
audioOutputDevices,
|
||||
liveChangeOutputDevice,
|
||||
intl.formatMessage(intlMessages.speakers),
|
||||
selectedOutputDeviceId || currentOutputDeviceId,
|
||||
false,
|
||||
);
|
||||
|
||||
const leaveAudioOption = {
|
||||
icon: 'logout',
|
||||
label: intl.formatMessage(intlMessages.leaveAudio),
|
||||
key: 'leaveAudioOption',
|
||||
dataTest: 'leaveAudio',
|
||||
customStyles: Styled.DangerColor,
|
||||
onClick: () => handleLeaveAudio(),
|
||||
};
|
||||
|
||||
const dropdownListComplete = inputDeviceList.concat(outputDeviceList)
|
||||
.concat({
|
||||
key: 'separator-02',
|
||||
isSeparator: true,
|
||||
})
|
||||
.concat(leaveAudioOption);
|
||||
const customStyles = { top: '-1rem' };
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isListenOnly ? (
|
||||
<span
|
||||
style={{ display: 'none' }}
|
||||
accessKey={shortcuts.leaveaudio}
|
||||
onClick={() => handleLeaveAudio()}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
<BBBMenu
|
||||
customStyles={!isMobile ? customStyles : null}
|
||||
trigger={(
|
||||
<>
|
||||
{isListenOnly
|
||||
? this.renderListenOnlyButton()
|
||||
: this.renderMuteToggleButton()}
|
||||
<Styled.AudioDropdown
|
||||
data-test="audioDropdownMenu"
|
||||
emoji="device_list_selector"
|
||||
label={intl.formatMessage(intlMessages.changeAudioDevice)}
|
||||
hideLabel
|
||||
tabIndex={0}
|
||||
rotate
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
actions={dropdownListComplete}
|
||||
opts={{
|
||||
id: 'audio-selector-dropdown-menu',
|
||||
keepMounted: true,
|
||||
transitionDuration: 0,
|
||||
elevation: 3,
|
||||
getcontentanchorel: null,
|
||||
fullwidth: 'true',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'center' },
|
||||
transformOrigin: { vertical: 'bottom', horizontal: 'center'},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderButtonsWithoutSelectorDevice() {
|
||||
const { isListenOnly } = this.props;
|
||||
return isListenOnly
|
||||
? this.renderListenOnlyButton()
|
||||
: (
|
||||
<>
|
||||
{this.renderMuteToggleButton()}
|
||||
{this.renderListenOnlyButton()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { _enableDynamicDeviceSelection } = this.props;
|
||||
|
||||
return _enableDynamicDeviceSelection
|
||||
? this.renderButtonsWithSelectorDevice()
|
||||
: this.renderButtonsWithoutSelectorDevice();
|
||||
}
|
||||
}
|
||||
|
||||
InputStreamLiveSelector.propTypes = propTypes;
|
||||
InputStreamLiveSelector.defaultProps = defaultProps;
|
||||
|
||||
export default withShortcutHelper(injectIntl(InputStreamLiveSelector),
|
||||
['leaveAudio', 'toggleMute']);
|
@ -1,23 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import InputStreamLiveSelector from './component';
|
||||
import Service from '../../service';
|
||||
|
||||
class InputStreamLiveSelectorContainer extends PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<InputStreamLiveSelector {...this.props} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTracker(({ handleLeaveAudio }) => ({
|
||||
isAudioConnected: Service.isConnected(),
|
||||
isListenOnly: Service.isListenOnly(),
|
||||
currentInputDeviceId: Service.inputDeviceId(),
|
||||
currentOutputDeviceId: Service.outputDeviceId(),
|
||||
liveChangeInputDevice: Service.liveChangeInputDevice,
|
||||
liveChangeOutputDevice: Service.changeOutputDevice,
|
||||
notify: Service.notify,
|
||||
handleLeaveAudio,
|
||||
}))(InputStreamLiveSelectorContainer);
|
@ -1,83 +0,0 @@
|
||||
import styled, { css, keyframes } from 'styled-components';
|
||||
import ButtonEmoji from '/imports/ui/components/common/button/button-emoji/ButtonEmoji';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
import {
|
||||
colorPrimary,
|
||||
colorDanger,
|
||||
colorGrayDark,
|
||||
colorOffWhite,
|
||||
colorWhite,
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
|
||||
const pulse = keyframes`
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 white;
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 0.5625rem transparent;
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 transparent;
|
||||
}
|
||||
`;
|
||||
|
||||
const AudioDropdown = styled(ButtonEmoji)`
|
||||
span {
|
||||
i {
|
||||
width: 0px !important;
|
||||
bottom: 1px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const MuteToggleButton = styled(Button)`
|
||||
|
||||
${({ ghost }) => ghost && `
|
||||
span {
|
||||
box-shadow: none;
|
||||
background-color: transparent !important;
|
||||
border-color: ${colorWhite} !important;
|
||||
}
|
||||
`}
|
||||
|
||||
${({ $talking }) => $talking && `
|
||||
border-radius: 50%;
|
||||
`}
|
||||
|
||||
${({ $talking, animations }) => $talking && animations && css`
|
||||
animation: ${pulse} 1s infinite ease-in;
|
||||
`}
|
||||
|
||||
${({ $talking, animations }) => $talking && !animations && css`
|
||||
& span {
|
||||
content: '';
|
||||
outline: none !important;
|
||||
background-clip: padding-box;
|
||||
box-shadow: 0 0 0 4px rgba(255,255,255,.5);
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const DangerColor = {
|
||||
color: colorDanger,
|
||||
paddingLeft: 12,
|
||||
};
|
||||
|
||||
const SelectedLabel = {
|
||||
color: colorPrimary,
|
||||
backgroundColor: colorOffWhite,
|
||||
};
|
||||
|
||||
const DisabledLabel = {
|
||||
color: colorGrayDark,
|
||||
fontWeight: 'bold',
|
||||
opacity: 1,
|
||||
};
|
||||
|
||||
export default {
|
||||
AudioDropdown,
|
||||
MuteToggleButton,
|
||||
DangerColor,
|
||||
SelectedLabel,
|
||||
DisabledLabel,
|
||||
};
|
@ -1,102 +0,0 @@
|
||||
import styled, { css, keyframes } from 'styled-components';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
|
||||
import { colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import { smPaddingX, smPaddingY } from '/imports/ui/stylesheets/styled-components/general';
|
||||
|
||||
const pulse = keyframes`
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 white;
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 0.5625rem transparent;
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 transparent;
|
||||
}
|
||||
`;
|
||||
|
||||
const LeaveButtonWithoutLiveStreamSelector = styled(Button)`
|
||||
${({ ghost }) => ghost && `
|
||||
span {
|
||||
background-color: transparent !important;
|
||||
border-color: ${colorWhite} !important;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const MuteToggleButton = styled(Button)`
|
||||
margin-right: ${smPaddingX};
|
||||
margin-left: 0;
|
||||
|
||||
@media ${smallOnly} {
|
||||
margin-right: ${smPaddingY};
|
||||
}
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin-left: ${smPaddingX};
|
||||
margin-right: 0;
|
||||
|
||||
@media ${smallOnly} {
|
||||
margin-left: ${smPaddingY};
|
||||
}
|
||||
}
|
||||
|
||||
${({ ghost }) => ghost && `
|
||||
span {
|
||||
background-color: transparent !important;
|
||||
border-color: ${colorWhite} !important;
|
||||
}
|
||||
`}
|
||||
|
||||
${({ talking }) => talking && `
|
||||
border-radius: 50%;
|
||||
`}
|
||||
|
||||
${({ talking, animations }) => talking && animations && css`
|
||||
animation: ${pulse} 1s infinite ease-in;
|
||||
`}
|
||||
|
||||
${({ talking, animations }) => talking && !animations && css`
|
||||
& span {
|
||||
content: '';
|
||||
outline: none !important;
|
||||
background-clip: padding-box;
|
||||
box-shadow: 0 0 0 4px rgba(255,255,255,.5);
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const Container = styled.span`
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
position: relative;
|
||||
|
||||
& > div {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
& > :last-child {
|
||||
margin-left: ${smPaddingX};
|
||||
margin-right: 0;
|
||||
|
||||
@media ${smallOnly} {
|
||||
margin-left: ${smPaddingY};
|
||||
}
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin-left: 0;
|
||||
margin-right: ${smPaddingX};
|
||||
|
||||
@media ${smallOnly} {
|
||||
margin-right: ${smPaddingY};
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default {
|
||||
LeaveButtonWithoutLiveStreamSelector,
|
||||
MuteToggleButton,
|
||||
Container,
|
||||
};
|
@ -113,7 +113,7 @@ const AudioControls: React.FC<AudioControlsProps> = ({
|
||||
};
|
||||
|
||||
export const AudioControlsContainer: React.FC = () => {
|
||||
const currentUser: Partial<User> = useCurrentUser((u: Partial<User>) => {
|
||||
const { data: currentUser } = useCurrentUser((u: Partial<User>) => {
|
||||
return {
|
||||
presenter: u.presenter,
|
||||
isModerator: u.isModerator,
|
||||
@ -122,7 +122,7 @@ export const AudioControlsContainer: React.FC = () => {
|
||||
};
|
||||
});
|
||||
|
||||
const currentMeeting: Partial<Meeting> = useMeeting((m: Partial<Meeting>) => {
|
||||
const { data: currentMeeting } = useMeeting((m: Partial<Meeting>) => {
|
||||
return {
|
||||
lockSettings: m.lockSettings,
|
||||
};
|
||||
|
@ -216,7 +216,7 @@ const InputStreamLiveSelector: React.FC<InputStreamLiveSelectorProps> = ({
|
||||
};
|
||||
|
||||
const InputStreamLiveSelectorContainer: React.FC = () => {
|
||||
const currentUser: Partial<User> = useCurrentUser((u: Partial<User>) => {
|
||||
const { data: currentUser } = useCurrentUser((u: Partial<User>) => {
|
||||
if (!u.voice) {
|
||||
return {
|
||||
presenter: u.presenter,
|
||||
@ -237,7 +237,7 @@ const InputStreamLiveSelectorContainer: React.FC = () => {
|
||||
};
|
||||
});
|
||||
|
||||
const currentMeeting: Partial<Meeting> = useMeeting((m: Partial<Meeting>) => {
|
||||
const { data: currentMeeting } = useMeeting((m: Partial<Meeting>) => {
|
||||
return {
|
||||
lockSettings: m?.lockSettings,
|
||||
isBreakout: m?.isBreakout,
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import BBBMenu from "/imports/ui/components/common/menu/component";
|
||||
import CreateBreakoutRoomContainer from '/imports/ui/components/actions-bar/create-breakout-room/container';
|
||||
import Trigger from "/imports/ui/components/common/control-header/right/component";
|
||||
import BBBMenu from '/imports/ui/components/common/menu/component';
|
||||
import CreateBreakoutRoomContainerGraphql from '/imports/ui/components/breakout-room/breakout-room-graphql/create-breakout-room/component';
|
||||
import Trigger from '/imports/ui/components/common/control-header/right/component';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
options: {
|
||||
@ -51,8 +51,8 @@ class BreakoutDropdown extends PureComponent {
|
||||
label: intl.formatMessage(intlMessages.manageDuration),
|
||||
onClick: () => {
|
||||
openBreakoutTimeManager();
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
this.menuItems.push(
|
||||
@ -62,8 +62,8 @@ class BreakoutDropdown extends PureComponent {
|
||||
label: intl.formatMessage(intlMessages.manageUsers),
|
||||
onClick: () => {
|
||||
this.setCreateBreakoutRoomModalIsOpen(true);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (amIModerator) {
|
||||
@ -75,8 +75,8 @@ class BreakoutDropdown extends PureComponent {
|
||||
disabled: !isMeteorConnected,
|
||||
onClick: () => {
|
||||
endAllBreakouts();
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -100,35 +100,39 @@ class BreakoutDropdown extends PureComponent {
|
||||
<>
|
||||
<BBBMenu
|
||||
trigger={
|
||||
<Trigger
|
||||
data-test="breakoutOptionsMenu"
|
||||
icon="more"
|
||||
label={intl.formatMessage(intlMessages.options)}
|
||||
aria-label={intl.formatMessage(intlMessages.options)}
|
||||
onClick={() => null}
|
||||
/>
|
||||
(
|
||||
<Trigger
|
||||
data-test="breakoutOptionsMenu"
|
||||
icon="more"
|
||||
label={intl.formatMessage(intlMessages.options)}
|
||||
aria-label={intl.formatMessage(intlMessages.options)}
|
||||
onClick={() => null}
|
||||
/>
|
||||
)
|
||||
}
|
||||
opts={{
|
||||
id: "breakoutroom-dropdown-menu",
|
||||
id: 'breakoutroom-dropdown-menu',
|
||||
keepMounted: true,
|
||||
transitionDuration: 0,
|
||||
elevation: 3,
|
||||
getcontentanchorel: null,
|
||||
fullwidth: "true",
|
||||
fullwidth: 'true',
|
||||
anchorOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' },
|
||||
transformOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' },
|
||||
}}
|
||||
actions={this.getAvailableActions()}
|
||||
/>
|
||||
{isCreateBreakoutRoomModalOpen ? <CreateBreakoutRoomContainer
|
||||
{...{
|
||||
isUpdate: true,
|
||||
onRequestClose: () => this.setCreateBreakoutRoomModalIsOpen(false),
|
||||
priority: "low",
|
||||
setIsOpen: this.setCreateBreakoutRoomModalIsOpen,
|
||||
isOpen: isCreateBreakoutRoomModalOpen
|
||||
}}
|
||||
/> : null}
|
||||
{isCreateBreakoutRoomModalOpen ? (
|
||||
<CreateBreakoutRoomContainerGraphql
|
||||
{...{
|
||||
isUpdate: true,
|
||||
onRequestClose: () => this.setCreateBreakoutRoomModalIsOpen(false),
|
||||
priority: 'low',
|
||||
setIsOpen: this.setCreateBreakoutRoomModalIsOpen,
|
||||
isOpen: isCreateBreakoutRoomModalOpen,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,115 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
import { uniqueId } from '/imports/utils/string-utils';
|
||||
import Styled from '../styles';
|
||||
import RoomUserList from './room-user-list/component';
|
||||
import { ChildComponentProps } from '../room-managment-state/types';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
nextLabel: {
|
||||
id: 'app.createBreakoutRoom.nextLabel',
|
||||
description: 'Next label',
|
||||
},
|
||||
backLabel: {
|
||||
id: 'app.audio.backLabel',
|
||||
description: 'Back label',
|
||||
},
|
||||
breakoutRoomDesc: {
|
||||
id: 'app.createBreakoutRoom.modalDesc',
|
||||
description: 'modal description',
|
||||
},
|
||||
addParticipantLabel: {
|
||||
id: 'app.createBreakoutRoom.addParticipantLabel',
|
||||
description: 'add Participant label',
|
||||
},
|
||||
breakoutRoomLabel: {
|
||||
id: 'app.createBreakoutRoom.breakoutRoomLabel',
|
||||
description: 'breakout room label',
|
||||
},
|
||||
});
|
||||
|
||||
const BreakoutRoomUserAssignmentMobile: React.FC<ChildComponentProps> = ({
|
||||
numberOfRooms,
|
||||
selectedRoom,
|
||||
setSelectedRoom,
|
||||
moveUser,
|
||||
rooms,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [layer, setLayer] = useState<1 | 2 | 3>(1);
|
||||
|
||||
const btnLevelId = useMemo(() => uniqueId('btn-set-level-'), []);
|
||||
const levelingButton = useMemo(() => {
|
||||
return (
|
||||
<Button
|
||||
color="primary"
|
||||
size="lg"
|
||||
label={layer === 1 ? intl.formatMessage(intlMessages.nextLabel)
|
||||
: intl.formatMessage(intlMessages.backLabel)}
|
||||
onClick={() => (layer === 1 ? setLayer(2) : setLayer(1))}
|
||||
key={btnLevelId}
|
||||
/>
|
||||
);
|
||||
}, [layer]);
|
||||
|
||||
const layerTwo = useMemo(() => {
|
||||
if (layer === 2) {
|
||||
return (
|
||||
<>
|
||||
<Styled.SubTitle>
|
||||
{intl.formatMessage(intlMessages.breakoutRoomDesc)}
|
||||
</Styled.SubTitle>
|
||||
<Styled.ListContainer>
|
||||
<span>
|
||||
{
|
||||
new Array(numberOfRooms).fill(1).map((_, idx) => (
|
||||
<Styled.RoomItem>
|
||||
<Styled.ItemTitle>
|
||||
{intl.formatMessage(intlMessages.breakoutRoomLabel, { 0: idx + 1 })}
|
||||
</Styled.ItemTitle>
|
||||
<Styled.ItemButton
|
||||
label={intl.formatMessage(intlMessages.addParticipantLabel)}
|
||||
size="lg"
|
||||
ghost
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setLayer(3);
|
||||
setSelectedRoom(idx + 1);
|
||||
}}
|
||||
/>
|
||||
</Styled.RoomItem>
|
||||
))
|
||||
}
|
||||
</span>
|
||||
</Styled.ListContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}, [layer, numberOfRooms]);
|
||||
|
||||
const layerThree = useMemo(() => {
|
||||
return (
|
||||
<RoomUserList
|
||||
confirm={() => setLayer(2)}
|
||||
selectedRoom={selectedRoom}
|
||||
rooms={rooms}
|
||||
moveUser={moveUser}
|
||||
/>
|
||||
);
|
||||
}, [selectedRoom, rooms]);
|
||||
const layers = {
|
||||
1: null,
|
||||
2: layerTwo,
|
||||
3: layerThree,
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{layers[layer]}
|
||||
{levelingButton}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BreakoutRoomUserAssignmentMobile;
|
@ -0,0 +1,95 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import Styled from './styles';
|
||||
import { BreakoutUser } from '../../room-managment-state/types';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
breakoutRoomLabel: {
|
||||
id: 'app.createBreakoutRoom.breakoutRoomLabel',
|
||||
description: 'breakout room label',
|
||||
},
|
||||
doneLabel: {
|
||||
id: 'app.createBreakoutRoom.doneLabel',
|
||||
description: 'done label',
|
||||
},
|
||||
});
|
||||
|
||||
interface RoomUserListProps {
|
||||
confirm: () => void;
|
||||
selectedRoom: number;
|
||||
rooms: {
|
||||
[key:number]: {
|
||||
id: number;
|
||||
name: string;
|
||||
users: BreakoutUser[];
|
||||
}
|
||||
}
|
||||
moveUser: (userId: string, fromRoomId: number, toRoomId: number) => void;
|
||||
}
|
||||
|
||||
const RoomUserList: React.FC<RoomUserListProps> = ({
|
||||
selectedRoom,
|
||||
rooms,
|
||||
moveUser,
|
||||
confirm,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const users = useMemo(() => {
|
||||
const userElements = Object.values(rooms).sort((a, b) => {
|
||||
if (a.id === selectedRoom) {
|
||||
return -1; // Move itemToMove to the front
|
||||
}
|
||||
if (b.id === selectedRoom) {
|
||||
return 1; // Move itemToMove to the front
|
||||
}
|
||||
return 0;
|
||||
}).map((room) => {
|
||||
return room.users.map((user) => {
|
||||
return (
|
||||
<Styled.SelectUserContainer id={user.userId} key={`breakout-user-${user.userId}`}>
|
||||
<Styled.Round>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`itemId${room.id}`}
|
||||
defaultChecked={selectedRoom === room.id}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.checked) {
|
||||
moveUser(user.userId, room.id, selectedRoom);
|
||||
} else {
|
||||
moveUser(user.userId, room.id, 0);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label htmlFor={`itemId${room.id}`}>
|
||||
<input type="hidden" id={`itemId${room.id}`} />
|
||||
</label>
|
||||
</Styled.Round>
|
||||
<Styled.TextName>
|
||||
{user.name}
|
||||
{((room.id !== selectedRoom) && room.id !== 0) ? `\t[${room.id}]` : ''}
|
||||
</Styled.TextName>
|
||||
</Styled.SelectUserContainer>
|
||||
);
|
||||
});
|
||||
}).flat();
|
||||
return userElements;
|
||||
}, [rooms, selectedRoom]);
|
||||
return (
|
||||
<Styled.SelectUserScreen>
|
||||
<Styled.Header>
|
||||
<Styled.Title>
|
||||
{intl.formatMessage(intlMessages.breakoutRoomLabel, { 0: selectedRoom })}
|
||||
</Styled.Title>
|
||||
<Styled.ButtonAdd
|
||||
size="md"
|
||||
label={intl.formatMessage(intlMessages.doneLabel)}
|
||||
color="primary"
|
||||
onClick={confirm}
|
||||
/>
|
||||
</Styled.Header>
|
||||
{users}
|
||||
</Styled.SelectUserScreen>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoomUserList;
|
@ -0,0 +1,133 @@
|
||||
import styled from 'styled-components';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
import { TextElipsis, TitleElipsis } from '/imports/ui/stylesheets/styled-components/placeholders';
|
||||
import Styled from '/imports/ui/components/actions-bar/create-breakout-room/styles';
|
||||
import { colorWhite, colorGrayLighter } from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import { borderSize } from '/imports/ui/stylesheets/styled-components/general';
|
||||
import { lineHeightComputed } from '/imports/ui/stylesheets/styled-components/typography';
|
||||
|
||||
const SelectUserContainer = styled.div`
|
||||
margin: 1.5rem 1rem;
|
||||
`;
|
||||
|
||||
const Round = styled.span`
|
||||
position: relative;
|
||||
|
||||
& > label {
|
||||
margin-top: -10px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
height: 28px;
|
||||
left: 0;
|
||||
right : auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 28px;
|
||||
|
||||
[dir="rtl"] & {
|
||||
left : auto;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
& > label:after {
|
||||
border: {
|
||||
style: solid;
|
||||
color: #fff;
|
||||
width: 2px;
|
||||
right: {
|
||||
style : none;
|
||||
}
|
||||
top: {
|
||||
style: none;
|
||||
}
|
||||
}
|
||||
content: "";
|
||||
height: 6px;
|
||||
left: 7px;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
transform: rotate(-45deg);
|
||||
width: 12px;
|
||||
|
||||
[dir="rtl"] & {
|
||||
border: {
|
||||
style: solid;
|
||||
color: #fff;
|
||||
width: 2px;
|
||||
left: {
|
||||
style : none;
|
||||
}
|
||||
top: {
|
||||
style: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > input[type="checkbox"] {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
& > input[type="checkbox"]:checked + label {
|
||||
background-color: #66bb6a;
|
||||
border-color: #66bb6a;
|
||||
}
|
||||
|
||||
& > input[type="checkbox"]:checked + label:after {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const TextName = styled(TextElipsis)`
|
||||
margin-left: 1.5rem;
|
||||
`;
|
||||
|
||||
const LockIcon = styled(Styled.LockIcon)`
|
||||
background:red;
|
||||
`;
|
||||
|
||||
const SelectUserScreen = styled.div`
|
||||
position: fixed;
|
||||
display: block;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
background-color: ${colorWhite};
|
||||
z-index: 1002;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
`;
|
||||
|
||||
const Header = styled.header`
|
||||
display: flex;
|
||||
padding: ${lineHeightComputed} 0;
|
||||
border-bottom: ${borderSize} solid ${colorGrayLighter};
|
||||
margin: 0 1rem 0 1rem;
|
||||
`;
|
||||
|
||||
const Title = styled(TitleElipsis)`
|
||||
align-content: flex-end;
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
`;
|
||||
// @ts-ignore - button is a JS component
|
||||
const ButtonAdd = styled(Button)`
|
||||
flex: 0 1 35%;
|
||||
`;
|
||||
|
||||
export default {
|
||||
SelectUserContainer,
|
||||
Round,
|
||||
TextName,
|
||||
LockIcon,
|
||||
SelectUserScreen,
|
||||
Header,
|
||||
Title,
|
||||
ButtonAdd,
|
||||
};
|
@ -0,0 +1,351 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { range } from '/imports/utils/array-utils';
|
||||
import Icon from '/imports/ui/components/common/icon/icon-ts/component';
|
||||
import Styled from '../styles';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import ManageRoomLabel from '../manage-room-label/component';
|
||||
import { ChildComponentProps } from '../room-managment-state/types';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
breakoutRoomTitle: {
|
||||
id: 'app.createBreakoutRoom.title',
|
||||
description: 'modal title',
|
||||
},
|
||||
breakoutRoomDesc: {
|
||||
id: 'app.createBreakoutRoom.modalDesc',
|
||||
description: 'modal description',
|
||||
},
|
||||
breakoutRoomUpdateDesc: {
|
||||
id: 'app.updateBreakoutRoom.modalDesc',
|
||||
description: 'update modal description',
|
||||
},
|
||||
cancelLabel: {
|
||||
id: 'app.updateBreakoutRoom.cancelLabel',
|
||||
description: 'used in the button that close update modal',
|
||||
},
|
||||
updateTitle: {
|
||||
id: 'app.updateBreakoutRoom.title',
|
||||
description: 'update breakout title',
|
||||
},
|
||||
updateConfirm: {
|
||||
id: 'app.updateBreakoutRoom.confirm',
|
||||
description: 'Update to breakout confirm button label',
|
||||
},
|
||||
resetUserRoom: {
|
||||
id: 'app.update.resetRoom',
|
||||
description: 'Reset user room button label',
|
||||
},
|
||||
confirmButton: {
|
||||
id: 'app.createBreakoutRoom.confirm',
|
||||
description: 'confirm button label',
|
||||
},
|
||||
dismissLabel: {
|
||||
id: 'app.presentationUploder.dismissLabel',
|
||||
description: 'used in the button that close modal',
|
||||
},
|
||||
numberOfRooms: {
|
||||
id: 'app.createBreakoutRoom.numberOfRooms',
|
||||
description: 'number of rooms label',
|
||||
},
|
||||
duration: {
|
||||
id: 'app.createBreakoutRoom.durationInMinutes',
|
||||
description: 'duration time label',
|
||||
},
|
||||
resetAssignments: {
|
||||
id: 'app.createBreakoutRoom.resetAssignments',
|
||||
description: 'reset assignments label',
|
||||
},
|
||||
resetAssignmentsDesc: {
|
||||
id: 'app.createBreakoutRoom.resetAssignmentsDesc',
|
||||
description: 'reset assignments label description',
|
||||
},
|
||||
randomlyAssign: {
|
||||
id: 'app.createBreakoutRoom.randomlyAssign',
|
||||
description: 'randomly assign label',
|
||||
},
|
||||
randomlyAssignDesc: {
|
||||
id: 'app.createBreakoutRoom.randomlyAssignDesc',
|
||||
description: 'randomly assign label description',
|
||||
},
|
||||
breakoutRoom: {
|
||||
id: 'app.createBreakoutRoom.room',
|
||||
description: 'breakout room',
|
||||
},
|
||||
freeJoinLabel: {
|
||||
id: 'app.createBreakoutRoom.freeJoin',
|
||||
description: 'free join label',
|
||||
},
|
||||
captureNotesLabel: {
|
||||
id: 'app.createBreakoutRoom.captureNotes',
|
||||
description: 'capture shared notes label',
|
||||
},
|
||||
captureSlidesLabel: {
|
||||
id: 'app.createBreakoutRoom.captureSlides',
|
||||
description: 'capture slides label',
|
||||
},
|
||||
captureNotesType: {
|
||||
id: 'app.notes.label',
|
||||
description: 'indicates notes have been captured',
|
||||
},
|
||||
captureSlidesType: {
|
||||
id: 'app.shortcut-help.whiteboard',
|
||||
description: 'indicates the whiteboard has been captured',
|
||||
},
|
||||
roomLabel: {
|
||||
id: 'app.createBreakoutRoom.room',
|
||||
description: 'Room label',
|
||||
},
|
||||
leastOneWarnBreakout: {
|
||||
id: 'app.createBreakoutRoom.leastOneWarnBreakout',
|
||||
description: 'warn message label',
|
||||
},
|
||||
notAssigned: {
|
||||
id: 'app.createBreakoutRoom.notAssigned',
|
||||
description: 'Not assigned label',
|
||||
},
|
||||
breakoutRoomLabel: {
|
||||
id: 'app.createBreakoutRoom.breakoutRoomLabel',
|
||||
description: 'breakout room label',
|
||||
},
|
||||
addParticipantLabel: {
|
||||
id: 'app.createBreakoutRoom.addParticipantLabel',
|
||||
description: 'add Participant label',
|
||||
},
|
||||
nextLabel: {
|
||||
id: 'app.createBreakoutRoom.nextLabel',
|
||||
description: 'Next label',
|
||||
},
|
||||
backLabel: {
|
||||
id: 'app.audio.backLabel',
|
||||
description: 'Back label',
|
||||
},
|
||||
minusRoomTime: {
|
||||
id: 'app.createBreakoutRoom.minusRoomTime',
|
||||
description: 'aria label for btn to decrease room time',
|
||||
},
|
||||
addRoomTime: {
|
||||
id: 'app.createBreakoutRoom.addRoomTime',
|
||||
description: 'aria label for btn to increase room time',
|
||||
},
|
||||
record: {
|
||||
id: 'app.createBreakoutRoom.record',
|
||||
description: 'label for checkbox to allow record',
|
||||
},
|
||||
roomTime: {
|
||||
id: 'app.createBreakoutRoom.roomTime',
|
||||
description: 'used to provide current room time for aria label',
|
||||
},
|
||||
numberOfRoomsIsValid: {
|
||||
id: 'app.createBreakoutRoom.numberOfRoomsError',
|
||||
description: 'Label an error message',
|
||||
},
|
||||
roomNameEmptyIsValid: {
|
||||
id: 'app.createBreakoutRoom.emptyRoomNameError',
|
||||
description: 'Label an error message',
|
||||
},
|
||||
roomNameDuplicatedIsValid: {
|
||||
id: 'app.createBreakoutRoom.duplicatedRoomNameError',
|
||||
description: 'Label an error message',
|
||||
},
|
||||
you: {
|
||||
id: 'app.userList.you',
|
||||
description: 'Text for identifying your user',
|
||||
},
|
||||
minimumDurationWarnBreakout: {
|
||||
id: 'app.createBreakoutRoom.minimumDurationWarnBreakout',
|
||||
description: 'minimum duration warning message label',
|
||||
},
|
||||
roomNameInputDesc: {
|
||||
id: 'app.createBreakoutRoom.roomNameInputDesc',
|
||||
description: 'aria description for room name change',
|
||||
},
|
||||
movedUserLabel: {
|
||||
id: 'app.createBreakoutRoom.movedUserLabel',
|
||||
description: 'screen reader alert when users are moved to rooms',
|
||||
},
|
||||
manageRooms: {
|
||||
id: 'app.createBreakoutRoom.manageRoomsLabel',
|
||||
description: 'Label for manage rooms',
|
||||
},
|
||||
sendInvitationToMods: {
|
||||
id: 'app.createBreakoutRoom.sendInvitationToMods',
|
||||
description: 'label for checkbox send invitation to moderators',
|
||||
},
|
||||
});
|
||||
|
||||
const isMe = (intId: string) => intId === Auth.userID;
|
||||
|
||||
const BreakoutRoomUserAssignment: React.FC<ChildComponentProps> = ({
|
||||
moveUser,
|
||||
rooms,
|
||||
getRoomName,
|
||||
changeRoomName,
|
||||
numberOfRooms,
|
||||
setSelectedId,
|
||||
randomlyAssign,
|
||||
resetRooms,
|
||||
users,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const dragStart = (ev: React.DragEvent<HTMLParagraphElement>) => {
|
||||
const paragraphElement = ev.target as HTMLParagraphElement;
|
||||
ev.dataTransfer.setData('text', paragraphElement.id);
|
||||
setSelectedId(paragraphElement.id);
|
||||
};
|
||||
|
||||
const dragEnd = () => {
|
||||
setSelectedId('');
|
||||
};
|
||||
|
||||
const allowDrop = (ev: React.DragEvent) => {
|
||||
ev.preventDefault();
|
||||
};
|
||||
|
||||
const drop = (roomNumber: number) => (ev: React.DragEvent) => {
|
||||
if (ev.preventDefault) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
const data = ev.dataTransfer.getData('text');
|
||||
const [userId, from] = data.split('-');
|
||||
moveUser(userId, Number(from), roomNumber);
|
||||
setSelectedId('');
|
||||
};
|
||||
|
||||
const hasNameDuplicated = (room: number) => {
|
||||
const roomName = rooms[room]?.name || '';
|
||||
return Object.values(rooms).filter((r) => r.name === roomName).length > 1;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (numberOfRooms) {
|
||||
resetRooms(numberOfRooms);
|
||||
}
|
||||
}, [numberOfRooms]);
|
||||
|
||||
const roomUserList = (room: number) => {
|
||||
if (rooms[room]) {
|
||||
return rooms[room].users.map((user) => {
|
||||
return (
|
||||
<Styled.RoomUserItem
|
||||
tabIndex={-1}
|
||||
id={`${user.userId}-${room}`}
|
||||
key={user.userId}
|
||||
draggable
|
||||
onDragStart={dragStart}
|
||||
onDragEnd={dragEnd}
|
||||
>
|
||||
<span>
|
||||
<span>{user.name}</span>
|
||||
<i>{(isMe(user.userId)) ? ` (${intl.formatMessage(intlMessages.you)})` : ''}</i>
|
||||
</span>
|
||||
{room !== 0
|
||||
? (
|
||||
<span
|
||||
key={`${user.userId}-${room}`}
|
||||
tabIndex={0}
|
||||
className="close"
|
||||
role="button"
|
||||
aria-label={intl.formatMessage(intlMessages.resetUserRoom)}
|
||||
onKeyDown={() => {
|
||||
moveUser(user.userId, room, 0);
|
||||
}}
|
||||
onClick={() => {
|
||||
moveUser(user.userId, room, 0);
|
||||
}}
|
||||
>
|
||||
<Icon iconName="close" />
|
||||
</span>
|
||||
) : null}
|
||||
</Styled.RoomUserItem>
|
||||
);
|
||||
});
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ManageRoomLabel
|
||||
onAssignReset={() => { resetRooms(0); }}
|
||||
onAssignRandomly={randomlyAssign}
|
||||
numberOfRoomsIsValid={numberOfRooms > 0}
|
||||
leastOneUserIsValid={rooms[0]?.users?.length < users.length}
|
||||
/>
|
||||
<Styled.ContentContainer>
|
||||
<Styled.Alert valid role="alert">
|
||||
<Styled.FreeJoinLabel>
|
||||
<Styled.BreakoutNameInput
|
||||
type="text"
|
||||
readOnly
|
||||
value={
|
||||
intl.formatMessage(intlMessages.notAssigned, { 0: 0 })
|
||||
}
|
||||
/>
|
||||
</Styled.FreeJoinLabel>
|
||||
<Styled.BreakoutBox
|
||||
hundred
|
||||
id="breakoutBox-0"
|
||||
onDrop={drop(0)}
|
||||
onDragOver={allowDrop}
|
||||
tabIndex={0}
|
||||
>
|
||||
{roomUserList(0)}
|
||||
</Styled.BreakoutBox>
|
||||
<Styled.SpanWarn data-test="warningNoUserAssigned" valid={rooms[0]?.users?.length < users.length}>
|
||||
{intl.formatMessage(intlMessages.leastOneWarnBreakout)}
|
||||
</Styled.SpanWarn>
|
||||
</Styled.Alert>
|
||||
<Styled.BoxContainer key="rooms-grid-" data-test="roomGrid">
|
||||
{
|
||||
range(1, numberOfRooms + 1).map((value) => (
|
||||
<div key={`room-${value}`}>
|
||||
<Styled.FreeJoinLabel>
|
||||
<Styled.RoomName
|
||||
type="text"
|
||||
maxLength={255}
|
||||
duplicated={hasNameDuplicated(value)}
|
||||
value={getRoomName(value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
changeRoomName(value, e.target.value);
|
||||
}}
|
||||
onBlur={(e: React.FocusEvent<HTMLInputElement>) => {
|
||||
changeRoomName(value, e.target.value);
|
||||
}}
|
||||
data-test={getRoomName(value).length === 0 ? `room-error-${value}` : `roomName-${value}`}
|
||||
/>
|
||||
<div aria-hidden id={`room-input-${value}`} className="sr-only">
|
||||
{intl.formatMessage(intlMessages.roomNameInputDesc)}
|
||||
</div>
|
||||
</Styled.FreeJoinLabel>
|
||||
<Styled.BreakoutBox
|
||||
id={`breakoutBox-${value}`}
|
||||
onDrop={drop(value)}
|
||||
onDragOver={allowDrop}
|
||||
hundred={false}
|
||||
tabIndex={0}
|
||||
>
|
||||
{roomUserList(value)}
|
||||
</Styled.BreakoutBox>
|
||||
{hasNameDuplicated(value) ? (
|
||||
<Styled.SpanWarn valid>
|
||||
{intl.formatMessage(intlMessages.roomNameDuplicatedIsValid)}
|
||||
</Styled.SpanWarn>
|
||||
) : null}
|
||||
{getRoomName(value).length === 0 ? (
|
||||
<Styled.SpanWarn valid aria-hidden id={`room-error-${value}`}>
|
||||
{intl.formatMessage(intlMessages.roomNameEmptyIsValid)}
|
||||
</Styled.SpanWarn>
|
||||
) : null}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</Styled.BoxContainer>
|
||||
</Styled.ContentContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BreakoutRoomUserAssignment;
|
@ -0,0 +1,566 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import ModalFullscreen from '/imports/ui/components/common/modal/fullscreen/component';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { range } from 'ramda';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { uniqueId } from '/imports/utils/string-utils';
|
||||
import { isImportPresentationWithAnnotationsFromBreakoutRoomsEnabled, isImportSharedNotesFromBreakoutRoomsEnabled } from '/imports/ui/services/features';
|
||||
import useMeeting from '/imports/ui/core/hooks/useMeeting';
|
||||
import { useLazyQuery, useQuery } from '@apollo/client';
|
||||
import Styled from './styles';
|
||||
import {
|
||||
getBreakouts,
|
||||
getBreakoutsResponse,
|
||||
getUser,
|
||||
getUserResponse,
|
||||
} from './queries';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import BreakoutRoomUserAssignment from './breakout-room-user-assignment/component';
|
||||
import deviceInfo from '/imports/utils/deviceInfo';
|
||||
import BreakoutRoomUserAssignmentMobile from './breakout-room-user-assignment-mobile/component';
|
||||
import RoomManagmentState from './room-managment-state/component';
|
||||
import {
|
||||
Rooms,
|
||||
RoomToWithSettings,
|
||||
BreakoutUser,
|
||||
moveUserRegistery,
|
||||
} from './room-managment-state/types';
|
||||
import { createBreakoutRoom, moveUser } from './service';
|
||||
|
||||
const BREAKOUT_LIM = Meteor.settings.public.app.breakouts.breakoutRoomLimit;
|
||||
const MIN_BREAKOUT_ROOMS = 2;
|
||||
const MAX_BREAKOUT_ROOMS = BREAKOUT_LIM > MIN_BREAKOUT_ROOMS ? BREAKOUT_LIM : MIN_BREAKOUT_ROOMS;
|
||||
const MIN_BREAKOUT_TIME = 5;
|
||||
const DEFAULT_BREAKOUT_TIME = 15;
|
||||
|
||||
interface CreateBreakoutRoomContainerProps {
|
||||
isOpen: boolean
|
||||
setIsOpen: (isOpen: boolean) => void
|
||||
priority: number,
|
||||
isUpdate?: boolean,
|
||||
}
|
||||
|
||||
interface CreateBreakoutRoomProps extends CreateBreakoutRoomContainerProps {
|
||||
isBreakoutRecordable: boolean,
|
||||
users: Array<BreakoutUser>,
|
||||
runningRooms: getBreakoutsResponse['breakoutRoom'],
|
||||
}
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
breakoutRoomTitle: {
|
||||
id: 'app.createBreakoutRoom.title',
|
||||
description: 'modal title',
|
||||
},
|
||||
breakoutRoomDesc: {
|
||||
id: 'app.createBreakoutRoom.modalDesc',
|
||||
description: 'modal description',
|
||||
},
|
||||
breakoutRoomUpdateDesc: {
|
||||
id: 'app.updateBreakoutRoom.modalDesc',
|
||||
description: 'update modal description',
|
||||
},
|
||||
cancelLabel: {
|
||||
id: 'app.updateBreakoutRoom.cancelLabel',
|
||||
description: 'used in the button that close update modal',
|
||||
},
|
||||
updateTitle: {
|
||||
id: 'app.updateBreakoutRoom.title',
|
||||
description: 'update breakout title',
|
||||
},
|
||||
updateConfirm: {
|
||||
id: 'app.updateBreakoutRoom.confirm',
|
||||
description: 'Update to breakout confirm button label',
|
||||
},
|
||||
resetUserRoom: {
|
||||
id: 'app.update.resetRoom',
|
||||
description: 'Reset user room button label',
|
||||
},
|
||||
confirmButton: {
|
||||
id: 'app.createBreakoutRoom.confirm',
|
||||
description: 'confirm button label',
|
||||
},
|
||||
dismissLabel: {
|
||||
id: 'app.presentationUploder.dismissLabel',
|
||||
description: 'used in the button that close modal',
|
||||
},
|
||||
numberOfRooms: {
|
||||
id: 'app.createBreakoutRoom.numberOfRooms',
|
||||
description: 'number of rooms label',
|
||||
},
|
||||
duration: {
|
||||
id: 'app.createBreakoutRoom.durationInMinutes',
|
||||
description: 'duration time label',
|
||||
},
|
||||
resetAssignments: {
|
||||
id: 'app.createBreakoutRoom.resetAssignments',
|
||||
description: 'reset assignments label',
|
||||
},
|
||||
resetAssignmentsDesc: {
|
||||
id: 'app.createBreakoutRoom.resetAssignmentsDesc',
|
||||
description: 'reset assignments label description',
|
||||
},
|
||||
randomlyAssign: {
|
||||
id: 'app.createBreakoutRoom.randomlyAssign',
|
||||
description: 'randomly assign label',
|
||||
},
|
||||
randomlyAssignDesc: {
|
||||
id: 'app.createBreakoutRoom.randomlyAssignDesc',
|
||||
description: 'randomly assign label description',
|
||||
},
|
||||
breakoutRoom: {
|
||||
id: 'app.createBreakoutRoom.room',
|
||||
description: 'breakout room',
|
||||
},
|
||||
freeJoinLabel: {
|
||||
id: 'app.createBreakoutRoom.freeJoin',
|
||||
description: 'free join label',
|
||||
},
|
||||
captureNotesLabel: {
|
||||
id: 'app.createBreakoutRoom.captureNotes',
|
||||
description: 'capture shared notes label',
|
||||
},
|
||||
captureSlidesLabel: {
|
||||
id: 'app.createBreakoutRoom.captureSlides',
|
||||
description: 'capture slides label',
|
||||
},
|
||||
captureNotesType: {
|
||||
id: 'app.notes.label',
|
||||
description: 'indicates notes have been captured',
|
||||
},
|
||||
captureSlidesType: {
|
||||
id: 'app.shortcut-help.whiteboard',
|
||||
description: 'indicates the whiteboard has been captured',
|
||||
},
|
||||
roomLabel: {
|
||||
id: 'app.createBreakoutRoom.room',
|
||||
description: 'Room label',
|
||||
},
|
||||
leastOneWarnBreakout: {
|
||||
id: 'app.createBreakoutRoom.leastOneWarnBreakout',
|
||||
description: 'warn message label',
|
||||
},
|
||||
notAssigned: {
|
||||
id: 'app.createBreakoutRoom.notAssigned',
|
||||
description: 'Not assigned label',
|
||||
},
|
||||
breakoutRoomLabel: {
|
||||
id: 'app.createBreakoutRoom.breakoutRoomLabel',
|
||||
description: 'breakout room label',
|
||||
},
|
||||
addParticipantLabel: {
|
||||
id: 'app.createBreakoutRoom.addParticipantLabel',
|
||||
description: 'add Participant label',
|
||||
},
|
||||
nextLabel: {
|
||||
id: 'app.createBreakoutRoom.nextLabel',
|
||||
description: 'Next label',
|
||||
},
|
||||
backLabel: {
|
||||
id: 'app.audio.backLabel',
|
||||
description: 'Back label',
|
||||
},
|
||||
minusRoomTime: {
|
||||
id: 'app.createBreakoutRoom.minusRoomTime',
|
||||
description: 'aria label for btn to decrease room time',
|
||||
},
|
||||
addRoomTime: {
|
||||
id: 'app.createBreakoutRoom.addRoomTime',
|
||||
description: 'aria label for btn to increase room time',
|
||||
},
|
||||
record: {
|
||||
id: 'app.createBreakoutRoom.record',
|
||||
description: 'label for checkbox to allow record',
|
||||
},
|
||||
roomTime: {
|
||||
id: 'app.createBreakoutRoom.roomTime',
|
||||
description: 'used to provide current room time for aria label',
|
||||
},
|
||||
numberOfRoomsIsValid: {
|
||||
id: 'app.createBreakoutRoom.numberOfRoomsError',
|
||||
description: 'Label an error message',
|
||||
},
|
||||
roomNameEmptyIsValid: {
|
||||
id: 'app.createBreakoutRoom.emptyRoomNameError',
|
||||
description: 'Label an error message',
|
||||
},
|
||||
roomNameDuplicatedIsValid: {
|
||||
id: 'app.createBreakoutRoom.duplicatedRoomNameError',
|
||||
description: 'Label an error message',
|
||||
},
|
||||
you: {
|
||||
id: 'app.userList.you',
|
||||
description: 'Text for identifying your user',
|
||||
},
|
||||
minimumDurationWarnBreakout: {
|
||||
id: 'app.createBreakoutRoom.minimumDurationWarnBreakout',
|
||||
description: 'minimum duration warning message label',
|
||||
},
|
||||
roomNameInputDesc: {
|
||||
id: 'app.createBreakoutRoom.roomNameInputDesc',
|
||||
description: 'aria description for room name change',
|
||||
},
|
||||
movedUserLabel: {
|
||||
id: 'app.createBreakoutRoom.movedUserLabel',
|
||||
description: 'screen reader alert when users are moved to rooms',
|
||||
},
|
||||
manageRooms: {
|
||||
id: 'app.createBreakoutRoom.manageRoomsLabel',
|
||||
description: 'Label for manage rooms',
|
||||
},
|
||||
sendInvitationToMods: {
|
||||
id: 'app.createBreakoutRoom.sendInvitationToMods',
|
||||
description: 'label for checkbox send invitation to moderators',
|
||||
},
|
||||
});
|
||||
|
||||
const CreateBreakoutRoom: React.FC<CreateBreakoutRoomProps> = ({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
priority,
|
||||
isUpdate = false,
|
||||
isBreakoutRecordable,
|
||||
users,
|
||||
runningRooms,
|
||||
}) => {
|
||||
const { isMobile } = deviceInfo;
|
||||
const intl = useIntl();
|
||||
|
||||
const [numberOfRoomsIsValid, setNumberOfRoomsIsValid] = React.useState(true);
|
||||
const [durationIsValid, setDurationIsValid] = React.useState(true);
|
||||
const [freeJoin, setFreeJoin] = React.useState(false);
|
||||
const [record, setRecord] = React.useState(false);
|
||||
const [captureSlides, setCaptureSlides] = React.useState(false);
|
||||
const [leastOneUserIsValid, setLeastOneUserIsValid] = React.useState(false);
|
||||
const [captureNotes, setCaptureNotes] = React.useState(false);
|
||||
const [inviteMods, setInviteMods] = React.useState(false);
|
||||
const [numberOfRooms, setNumberOfRooms] = React.useState(MIN_BREAKOUT_ROOMS);
|
||||
const [durationTime, setDurationTime] = React.useState(DEFAULT_BREAKOUT_TIME);
|
||||
|
||||
const roomsRef = React.useRef<Rooms>({});
|
||||
const moveRegisterRef = React.useRef<moveUserRegistery>({});
|
||||
|
||||
const setRoomsRef = (rooms: Rooms) => {
|
||||
roomsRef.current = rooms;
|
||||
};
|
||||
|
||||
const setMoveRegisterRef = (moveRegister: moveUserRegistery) => {
|
||||
moveRegisterRef.current = moveRegister;
|
||||
};
|
||||
|
||||
const checkboxCallbackFactory = (call: (value: boolean) => void) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { checked } = e.target;
|
||||
call(checked);
|
||||
};
|
||||
|
||||
const createRoom = () => {
|
||||
const rooms = roomsRef.current;
|
||||
const roomsArray: RoomToWithSettings[] = [];
|
||||
/* eslint no-restricted-syntax: "off" */
|
||||
for (let i = 0; i < numberOfRooms; i += 1) {
|
||||
const roomNumber = i + 1;
|
||||
if (rooms[roomNumber]) {
|
||||
const r = rooms[roomNumber];
|
||||
roomsArray.push({
|
||||
name: r.name,
|
||||
sequence: r.id,
|
||||
captureNotesFilename: `${r.name.replace(/\s/g, '_')}_${intl.formatMessage(intlMessages.captureNotesType)}`,
|
||||
captureSlidesFilename: `${r.name.replace(/\s/g, '_')}_${intl.formatMessage(intlMessages.captureSlidesType)}`,
|
||||
isDefaultName: r.name === intl.formatMessage(intlMessages.breakoutRoom, {
|
||||
0: r.id,
|
||||
}),
|
||||
users: r.users.map((u) => u.userId),
|
||||
freeJoin,
|
||||
shortName: r.name,
|
||||
});
|
||||
} else {
|
||||
const defaultName = intl.formatMessage(intlMessages.breakoutRoom, {
|
||||
0: roomNumber,
|
||||
});
|
||||
|
||||
roomsArray.push({
|
||||
name: defaultName,
|
||||
sequence: roomNumber,
|
||||
captureNotesFilename: `${defaultName.replace(/\s/g, '_')}_${intl.formatMessage(intlMessages.captureNotesType)}`,
|
||||
captureSlidesFilename: `${defaultName.replace(/\s/g, '_')}_${intl.formatMessage(intlMessages.captureSlidesType)}`,
|
||||
isDefaultName: true,
|
||||
freeJoin,
|
||||
shortName: defaultName,
|
||||
users: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
createBreakoutRoom(roomsArray, durationTime, record, captureSlides, captureNotes, inviteMods);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const userUpdate = () => {
|
||||
const userIds = Object.keys(moveRegisterRef.current);
|
||||
userIds.forEach((userId) => {
|
||||
const { fromRoomId, toRoomId } = moveRegisterRef.current[userId];
|
||||
if (fromRoomId !== toRoomId) {
|
||||
moveUser(userId, fromRoomId, toRoomId);
|
||||
}
|
||||
});
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const title = useMemo(() => (
|
||||
<Styled.SubTitle>
|
||||
{isUpdate
|
||||
? intl.formatMessage(intlMessages.breakoutRoomUpdateDesc)
|
||||
: intl.formatMessage(intlMessages.breakoutRoomDesc)}
|
||||
</Styled.SubTitle>
|
||||
), [isUpdate]);
|
||||
|
||||
const checkboxesInfo = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
allowed: true,
|
||||
htmlFor: 'freeJoinCheckbox',
|
||||
key: 'free-join-breakouts',
|
||||
id: 'freeJoinCheckbox',
|
||||
onChange: checkboxCallbackFactory((e: boolean) => {
|
||||
setFreeJoin(e);
|
||||
setLeastOneUserIsValid(true);
|
||||
}),
|
||||
label: intl.formatMessage(intlMessages.freeJoinLabel),
|
||||
},
|
||||
{
|
||||
allowed: isBreakoutRecordable,
|
||||
htmlFor: 'recordBreakoutCheckbox',
|
||||
key: 'record-breakouts',
|
||||
id: 'recordBreakoutCheckbox',
|
||||
onChange: checkboxCallbackFactory(setRecord),
|
||||
label: intl.formatMessage(intlMessages.record),
|
||||
},
|
||||
{
|
||||
allowed: isImportPresentationWithAnnotationsFromBreakoutRoomsEnabled(),
|
||||
htmlFor: 'captureSlidesBreakoutCheckbox',
|
||||
key: 'capture-slides-breakouts',
|
||||
id: 'captureSlidesBreakoutCheckbox',
|
||||
onChange: checkboxCallbackFactory(setCaptureSlides),
|
||||
label: intl.formatMessage(intlMessages.captureSlidesLabel),
|
||||
},
|
||||
{
|
||||
allowed: isImportSharedNotesFromBreakoutRoomsEnabled(),
|
||||
htmlFor: 'captureNotesBreakoutCheckbox',
|
||||
key: 'capture-notes-breakouts',
|
||||
id: 'captureNotesBreakoutCheckbox',
|
||||
onChange: checkboxCallbackFactory(setCaptureNotes),
|
||||
label: intl.formatMessage(intlMessages.captureNotesLabel),
|
||||
},
|
||||
{
|
||||
allowed: true,
|
||||
htmlFor: 'sendInvitationToAssignedModeratorsCheckbox',
|
||||
key: 'send-invitation-to-assigned-moderators-breakouts',
|
||||
id: 'sendInvitationToAssignedModeratorsCheckbox',
|
||||
onChange: checkboxCallbackFactory(setInviteMods),
|
||||
label: intl.formatMessage(intlMessages.sendInvitationToMods),
|
||||
},
|
||||
];
|
||||
}, [isBreakoutRecordable]);
|
||||
|
||||
const form = useMemo(() => {
|
||||
return (
|
||||
<React.Fragment key="breakout-form">
|
||||
<Styled.BreakoutSettings>
|
||||
<div>
|
||||
<Styled.FormLabel valid={numberOfRoomsIsValid} aria-hidden>
|
||||
{intl.formatMessage(intlMessages.numberOfRooms)}
|
||||
</Styled.FormLabel>
|
||||
<Styled.InputRooms
|
||||
id="numberOfRooms"
|
||||
name="numberOfRooms"
|
||||
valid={numberOfRoomsIsValid}
|
||||
value={numberOfRooms}
|
||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const { value } = e.target;
|
||||
setNumberOfRooms(Number.parseInt(value, 10));
|
||||
setNumberOfRoomsIsValid(true);
|
||||
}}
|
||||
aria-label={intl.formatMessage(intlMessages.numberOfRooms)}
|
||||
>
|
||||
{
|
||||
range(MIN_BREAKOUT_ROOMS, MAX_BREAKOUT_ROOMS + 1).map((item) => (<option key={uniqueId('value-')}>{item}</option>))
|
||||
}
|
||||
</Styled.InputRooms>
|
||||
</div>
|
||||
<Styled.DurationLabel valid={durationIsValid} htmlFor="breakoutRoomTime">
|
||||
<Styled.LabelText bold={false} aria-hidden>
|
||||
{intl.formatMessage(intlMessages.duration)}
|
||||
</Styled.LabelText>
|
||||
<Styled.DurationArea>
|
||||
<Styled.DurationInput
|
||||
type="number"
|
||||
min="1"
|
||||
value={durationTime}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = e.target;
|
||||
const v = Number.parseInt(value, 10);
|
||||
setDurationTime(v);
|
||||
setDurationIsValid(v >= MIN_BREAKOUT_TIME);
|
||||
}}
|
||||
onBlur={(e: React.FocusEvent<HTMLInputElement>) => {
|
||||
const { value } = e.target;
|
||||
const v = Number.parseInt(value, 10);
|
||||
setDurationTime((v && !(v <= 0)) ? v : MIN_BREAKOUT_TIME);
|
||||
setDurationIsValid(true);
|
||||
}}
|
||||
aria-label={intl.formatMessage(intlMessages.duration)}
|
||||
data-test="durationTime"
|
||||
/>
|
||||
</Styled.DurationArea>
|
||||
<Styled.SpanWarn data-test="minimumDurationWarnBreakout" valid={durationIsValid}>
|
||||
{
|
||||
intl.formatMessage(
|
||||
intlMessages.minimumDurationWarnBreakout,
|
||||
{ 0: MIN_BREAKOUT_TIME },
|
||||
)
|
||||
}
|
||||
</Styled.SpanWarn>
|
||||
</Styled.DurationLabel>
|
||||
<Styled.CheckBoxesContainer key="breakout-checkboxes">
|
||||
{checkboxesInfo
|
||||
.filter((item) => item.allowed)
|
||||
.map((item) => (
|
||||
<Styled.FreeJoinLabel htmlFor={item.htmlFor} key={item.key}>
|
||||
<Styled.FreeJoinCheckbox
|
||||
type="checkbox"
|
||||
id={item.id}
|
||||
onChange={item.onChange}
|
||||
aria-label={item.label}
|
||||
/>
|
||||
<span aria-hidden>{item.label}</span>
|
||||
</Styled.FreeJoinLabel>
|
||||
))}
|
||||
</Styled.CheckBoxesContainer>
|
||||
</Styled.BreakoutSettings>
|
||||
<Styled.SpanWarn valid={numberOfRoomsIsValid}>
|
||||
{intl.formatMessage(intlMessages.numberOfRoomsIsValid)}
|
||||
</Styled.SpanWarn>
|
||||
<span aria-hidden id="randomlyAssignDesc" className="sr-only">
|
||||
{intl.formatMessage(intlMessages.randomlyAssignDesc)}
|
||||
</span>
|
||||
<Styled.Separator />
|
||||
</React.Fragment>
|
||||
);
|
||||
}, [durationTime, durationIsValid, numberOfRooms, numberOfRoomsIsValid]);
|
||||
|
||||
return (
|
||||
<ModalFullscreen
|
||||
title={
|
||||
isUpdate
|
||||
? intl.formatMessage(intlMessages.updateTitle)
|
||||
: intl.formatMessage(intlMessages.breakoutRoomTitle)
|
||||
}
|
||||
confirm={
|
||||
{
|
||||
label: isUpdate
|
||||
? intl.formatMessage(intlMessages.updateConfirm)
|
||||
: intl.formatMessage(intlMessages.confirmButton),
|
||||
callback: isUpdate ? userUpdate : createRoom,
|
||||
disabled: !leastOneUserIsValid || !numberOfRoomsIsValid || !durationIsValid,
|
||||
}
|
||||
}
|
||||
dismiss={{
|
||||
label: isUpdate
|
||||
? intl.formatMessage(intlMessages.cancelLabel)
|
||||
: intl.formatMessage(intlMessages.dismissLabel),
|
||||
callback: () => setIsOpen(false),
|
||||
disabled: false,
|
||||
}}
|
||||
isOpen={isOpen}
|
||||
priority={priority}
|
||||
>
|
||||
<Styled.Content>
|
||||
{title}
|
||||
{form}
|
||||
<RoomManagmentState
|
||||
numberOfRooms={numberOfRooms}
|
||||
users={users}
|
||||
RendererComponent={isMobile ? BreakoutRoomUserAssignmentMobile : BreakoutRoomUserAssignment}
|
||||
runningRooms={runningRooms}
|
||||
setRoomsRef={setRoomsRef}
|
||||
setMoveRegisterRef={setMoveRegisterRef}
|
||||
setFormIsValid={setLeastOneUserIsValid}
|
||||
/>
|
||||
</Styled.Content>
|
||||
</ModalFullscreen>
|
||||
);
|
||||
};
|
||||
|
||||
const CreateBreakoutRoomContainer: React.FC<CreateBreakoutRoomContainerProps> = ({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
priority,
|
||||
isUpdate,
|
||||
}) => {
|
||||
const [fetchedBreakouts, setFetchedBreakouts] = React.useState(false);
|
||||
// isBreakoutRecordable - get from meeting breakout policies breakoutPolicies/record
|
||||
const {
|
||||
data: currentMeeting,
|
||||
} = useMeeting((m) => ({
|
||||
breakoutPolicies: m.breakoutPolicies,
|
||||
}));
|
||||
|
||||
const {
|
||||
data: usersData,
|
||||
loading: usersLoading,
|
||||
error: usersError,
|
||||
} = useQuery<getUserResponse>(getUser, {
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
|
||||
const [
|
||||
loadBreakouts,
|
||||
{
|
||||
data: breakoutsData,
|
||||
loading: breakoutsLoading,
|
||||
error: breakoutsError,
|
||||
},
|
||||
] = useLazyQuery<getBreakoutsResponse>(getBreakouts, {
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
|
||||
if (usersLoading || breakoutsLoading || !currentMeeting) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (true && !fetchedBreakouts) {
|
||||
loadBreakouts();
|
||||
setFetchedBreakouts(true);
|
||||
}
|
||||
|
||||
if (true && breakoutsLoading) return null;
|
||||
|
||||
if (usersError || breakoutsError) {
|
||||
logger.info('Error loading users', usersError);
|
||||
logger.info('Error loading breakouts', breakoutsError);
|
||||
return (
|
||||
<div>
|
||||
{JSON.stringify(usersError) || JSON.stringify(breakoutsError)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<CreateBreakoutRoom
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
priority={priority}
|
||||
isUpdate={isUpdate}
|
||||
isBreakoutRecordable={currentMeeting?.breakoutPolicies?.record ?? true}
|
||||
users={usersData?.user ?? []}
|
||||
runningRooms={breakoutsData?.breakoutRoom ?? []}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
CreateBreakoutRoomContainer.defaultProps = {
|
||||
isUpdate: false,
|
||||
};
|
||||
|
||||
CreateBreakoutRoom.defaultProps = {
|
||||
isUpdate: false,
|
||||
};
|
||||
|
||||
export default CreateBreakoutRoomContainer;
|
@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import Styled from '../styles';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
manageRooms: {
|
||||
id: 'app.createBreakoutRoom.manageRoomsLabel',
|
||||
description: 'Label for manage rooms',
|
||||
},
|
||||
resetAssignments: {
|
||||
id: 'app.createBreakoutRoom.resetAssignments',
|
||||
description: 'reset assignments label',
|
||||
},
|
||||
resetAssignmentsDesc: {
|
||||
id: 'app.createBreakoutRoom.resetAssignmentsDesc',
|
||||
description: 'reset assignments label description',
|
||||
},
|
||||
randomlyAssign: {
|
||||
id: 'app.createBreakoutRoom.randomlyAssign',
|
||||
description: 'randomly assign label',
|
||||
},
|
||||
randomlyAssignDesc: {
|
||||
id: 'app.createBreakoutRoom.randomlyAssignDesc',
|
||||
description: 'randomly assign label description',
|
||||
},
|
||||
});
|
||||
|
||||
interface ManageRoomLabelProps {
|
||||
numberOfRoomsIsValid: boolean;
|
||||
leastOneUserIsValid: boolean;
|
||||
onAssignReset: () => void;
|
||||
onAssignRandomly: () => void;
|
||||
}
|
||||
|
||||
const ManageRoomLabel: React.FC<ManageRoomLabelProps> = ({
|
||||
numberOfRoomsIsValid,
|
||||
leastOneUserIsValid,
|
||||
onAssignReset,
|
||||
onAssignRandomly,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<Styled.AssignBtnsContainer>
|
||||
<Styled.LabelText bold aria-hidden>
|
||||
{intl.formatMessage(intlMessages.manageRooms)}
|
||||
</Styled.LabelText>
|
||||
{leastOneUserIsValid ? (
|
||||
<Styled.AssignBtns
|
||||
data-test="resetAssignments"
|
||||
label={intl.formatMessage(intlMessages.resetAssignments)}
|
||||
aria-describedby="resetAssignmentsDesc"
|
||||
onClick={onAssignReset}
|
||||
size="sm"
|
||||
color="default"
|
||||
disabled={!numberOfRoomsIsValid}
|
||||
/>
|
||||
) : (
|
||||
<Styled.AssignBtns
|
||||
random
|
||||
data-test="randomlyAssign"
|
||||
label={intl.formatMessage(intlMessages.randomlyAssign)}
|
||||
aria-describedby="randomlyAssignDesc"
|
||||
onClick={onAssignRandomly}
|
||||
size="sm"
|
||||
color="default"
|
||||
/>
|
||||
)}
|
||||
</Styled.AssignBtnsContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageRoomLabel;
|
@ -0,0 +1,75 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { BreakoutUser } from './room-managment-state/types';
|
||||
|
||||
export interface getUserResponse {
|
||||
user: Array<BreakoutUser>;
|
||||
}
|
||||
|
||||
export interface breakoutRoom {
|
||||
sequence: number;
|
||||
name: string;
|
||||
breakoutRoomId: string;
|
||||
participants: Array<{
|
||||
user: {
|
||||
name: string;
|
||||
userId: string;
|
||||
isModerator: boolean;
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
||||
export interface getBreakoutsResponse {
|
||||
breakoutRoom: Array<breakoutRoom>
|
||||
}
|
||||
|
||||
export const getUser = gql`
|
||||
query getUser {
|
||||
user(
|
||||
order_by: [
|
||||
{role: asc},
|
||||
{raiseHandTime: asc_nulls_last},
|
||||
{awayTime: asc_nulls_last},
|
||||
{emojiTime: asc_nulls_last},
|
||||
{isDialIn: desc},
|
||||
{hasDrawPermissionOnCurrentPage: desc},
|
||||
{nameSortable: asc},
|
||||
{userId: asc}
|
||||
]) {
|
||||
userId
|
||||
name
|
||||
isModerator
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const getBreakouts = gql`
|
||||
query getBreakouts {
|
||||
breakoutRoom {
|
||||
sequence
|
||||
name
|
||||
breakoutRoomId
|
||||
participants {
|
||||
user {
|
||||
name
|
||||
userId
|
||||
isModerator
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const getBreakoutCount = gql`
|
||||
query getBreakoutCount {
|
||||
breakoutRoom_aggregate {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default {
|
||||
getUser,
|
||||
getBreakouts,
|
||||
};
|
@ -0,0 +1,218 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import {
|
||||
BreakoutUser,
|
||||
Rooms,
|
||||
ChildComponentProps,
|
||||
Room,
|
||||
moveUserRegistery,
|
||||
} from './types';
|
||||
import { breakoutRoom, getBreakoutsResponse } from '../queries';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
breakoutRoom: {
|
||||
id: 'app.createBreakoutRoom.room',
|
||||
description: 'breakout room',
|
||||
},
|
||||
notAssigned: {
|
||||
id: 'app.createBreakoutRoom.notAssigned',
|
||||
description: 'Not assigned label',
|
||||
},
|
||||
});
|
||||
|
||||
interface RoomManagmentStateProps {
|
||||
numberOfRooms: number;
|
||||
users: BreakoutUser[];
|
||||
RendererComponent: React.FC<ChildComponentProps>;
|
||||
runningRooms: getBreakoutsResponse['breakoutRoom'] | null;
|
||||
setFormIsValid: (isValid: boolean) => void;
|
||||
setRoomsRef: (rooms: Rooms) => void;
|
||||
setMoveRegisterRef: (moveRegister: moveUserRegistery) => void;
|
||||
}
|
||||
|
||||
const RoomManagmentState: React.FC<RoomManagmentStateProps> = ({
|
||||
numberOfRooms,
|
||||
users,
|
||||
RendererComponent,
|
||||
setFormIsValid,
|
||||
runningRooms,
|
||||
setRoomsRef,
|
||||
setMoveRegisterRef,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [selectedId, setSelectedId] = useState<string>('');
|
||||
const [selectedRoom, setSelectedRoom] = useState<number>(0);
|
||||
const [rooms, setRooms] = useState<Rooms>({});
|
||||
const [init, setInit] = useState<boolean>(false);
|
||||
const [movementRegistered, setMovementRegistered] = useState<moveUserRegistery>({});
|
||||
|
||||
const moveUser = (userId: string, from: number, to: number) => {
|
||||
const room = rooms[to];
|
||||
const roomFrom = rooms[Number(from)];
|
||||
|
||||
if (from === to) return null;
|
||||
setMovementRegistered({
|
||||
...movementRegistered,
|
||||
[userId]: {
|
||||
fromSequence: from,
|
||||
toSequence: to,
|
||||
toRoomId: runningRooms?.find((r) => r.sequence === to)?.breakoutRoomId,
|
||||
fromRoomId: runningRooms?.find((r) => r.sequence === from)?.breakoutRoomId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!rooms[to]) {
|
||||
const oldStateRooms = { ...rooms };
|
||||
oldStateRooms[from].users = (oldStateRooms[from].users.filter((user) => user.userId !== userId) || []);
|
||||
return setRooms({
|
||||
...oldStateRooms,
|
||||
[to]: {
|
||||
id: to,
|
||||
name: intl.formatMessage(intlMessages.breakoutRoom, {
|
||||
0: to,
|
||||
}),
|
||||
users: [users.find((user) => user.userId === userId)],
|
||||
} as Room,
|
||||
});
|
||||
}
|
||||
|
||||
return setRooms({
|
||||
...rooms,
|
||||
[to]: {
|
||||
...room,
|
||||
users: [
|
||||
...(room?.users ?? []),
|
||||
roomFrom?.users?.find((user) => user.userId === userId),
|
||||
],
|
||||
} as Room,
|
||||
[Number(from)]: {
|
||||
...roomFrom,
|
||||
users: roomFrom?.users.filter((user) => user.userId !== userId),
|
||||
} as Room,
|
||||
});
|
||||
};
|
||||
|
||||
const roomName = (room: number) => {
|
||||
const defaultName = intl.formatMessage(intlMessages.breakoutRoom, {
|
||||
0: room,
|
||||
});
|
||||
if (rooms[room]) {
|
||||
return rooms[room].name || defaultName;
|
||||
}
|
||||
return defaultName;
|
||||
};
|
||||
|
||||
const changeRoomName = (room: number, name: string) => {
|
||||
setRooms((prevRooms) => {
|
||||
const rooms = { ...prevRooms };
|
||||
if (!rooms[room]) {
|
||||
rooms[room] = {
|
||||
id: room,
|
||||
name: '',
|
||||
users: [],
|
||||
};
|
||||
}
|
||||
rooms[room].name = name;
|
||||
return rooms;
|
||||
});
|
||||
};
|
||||
|
||||
const randomlyAssign = () => {
|
||||
const withoutModerators = rooms[0].users.filter((user) => !user.isModerator);
|
||||
withoutModerators.forEach((user) => {
|
||||
const randomRoom = Math.floor(Math.random() * numberOfRooms) + 1;
|
||||
moveUser(user.userId, 0, randomRoom);
|
||||
});
|
||||
};
|
||||
|
||||
const resetRooms = (cap: number) => {
|
||||
const greterThanRooms = Object.keys(rooms).filter((room) => Number(room) > cap);
|
||||
greterThanRooms.forEach((room) => {
|
||||
if (rooms && rooms[Number(room)]) {
|
||||
setRooms((prevRooms) => ({
|
||||
...prevRooms,
|
||||
0: {
|
||||
...prevRooms[0],
|
||||
users: [
|
||||
...prevRooms[0].users,
|
||||
...rooms[Number(room)].users,
|
||||
],
|
||||
},
|
||||
[Number(room)]: {
|
||||
...prevRooms[Number(room)],
|
||||
users: [],
|
||||
},
|
||||
}));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (users && users.length > 0) {
|
||||
// set users to room 0
|
||||
setInit(true);
|
||||
setRooms((prevRooms: Rooms) => ({
|
||||
...(prevRooms ?? {}),
|
||||
0: {
|
||||
id: 0,
|
||||
name: intl.formatMessage(intlMessages.notAssigned, { 0: 0 }),
|
||||
users,
|
||||
},
|
||||
}));
|
||||
}
|
||||
}, [users]);
|
||||
|
||||
useEffect(() => {
|
||||
if (runningRooms && init) {
|
||||
runningRooms.forEach((r: breakoutRoom) => {
|
||||
r.participants.forEach((u) => {
|
||||
if (!rooms[r.sequence]?.users?.find((user) => user.userId === u.user.userId)) {
|
||||
moveUser(u.user.userId, 0, r.sequence);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [runningRooms, init]);
|
||||
|
||||
useEffect(() => {
|
||||
if (rooms) {
|
||||
setRoomsRef(rooms);
|
||||
if (!(rooms[0]?.users?.length === users.length)) {
|
||||
setFormIsValid(true);
|
||||
} else {
|
||||
setFormIsValid(false);
|
||||
}
|
||||
}
|
||||
}, [rooms]);
|
||||
|
||||
useEffect(() => {
|
||||
if (movementRegistered) {
|
||||
setMoveRegisterRef(movementRegistered);
|
||||
}
|
||||
}, [movementRegistered]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
init ? (
|
||||
<RendererComponent
|
||||
moveUser={moveUser}
|
||||
rooms={rooms}
|
||||
getRoomName={roomName}
|
||||
changeRoomName={changeRoomName}
|
||||
numberOfRooms={numberOfRooms}
|
||||
selectedId={selectedId ?? ''}
|
||||
setSelectedId={setSelectedId}
|
||||
selectedRoom={selectedRoom}
|
||||
setSelectedRoom={setSelectedRoom}
|
||||
randomlyAssign={randomlyAssign}
|
||||
resetRooms={resetRooms}
|
||||
users={users}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoomManagmentState;
|
@ -0,0 +1,48 @@
|
||||
import { User } from '/imports/ui/Types/user';
|
||||
|
||||
export type BreakoutUser = Pick<User, 'userId' | 'name' | 'isModerator'>;
|
||||
|
||||
export type Room = {
|
||||
id: number;
|
||||
name: string;
|
||||
users: BreakoutUser[];
|
||||
};
|
||||
|
||||
export interface moveUserRegistery {
|
||||
[userId: string]: {
|
||||
fromRoomId?: string;
|
||||
toRoomId?: string;
|
||||
fromSequence: number;
|
||||
toSequence: number;
|
||||
}
|
||||
}
|
||||
|
||||
export type RoomToWithSettings = {
|
||||
name: string;
|
||||
users: string[];
|
||||
captureNotesFilename: string;
|
||||
captureSlidesFilename: string;
|
||||
isDefaultName: boolean;
|
||||
freeJoin: boolean;
|
||||
sequence: number;
|
||||
shortName: string;
|
||||
};
|
||||
|
||||
export type Rooms = {
|
||||
[key: number]: Room;
|
||||
};
|
||||
|
||||
export type ChildComponentProps = {
|
||||
moveUser: (userId: string, fromRoomId: number, toRoomId: number) => void;
|
||||
rooms: Rooms;
|
||||
getRoomName: (roomId: number) => string;
|
||||
changeRoomName: (roomId: number, name: string) => void;
|
||||
numberOfRooms: number;
|
||||
selectedId: string;
|
||||
setSelectedId: (id: string) => void;
|
||||
selectedRoom: number;
|
||||
setSelectedRoom: (id: number) => void;
|
||||
randomlyAssign: () => void;
|
||||
resetRooms: (cap: number) => void;
|
||||
users: BreakoutUser[];
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import { RoomToWithSettings } from './room-managment-state/types';
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
|
||||
export const createBreakoutRoom = (
|
||||
rooms: RoomToWithSettings[],
|
||||
durationInMinutes: number,
|
||||
record: boolean = false,
|
||||
captureNotes: boolean = false,
|
||||
captureSlides: boolean = false,
|
||||
sendInviteToModerators: boolean = false,
|
||||
) => makeCall(
|
||||
'createBreakoutRoom',
|
||||
rooms,
|
||||
durationInMinutes,
|
||||
record,
|
||||
captureNotes,
|
||||
captureSlides,
|
||||
sendInviteToModerators,
|
||||
);
|
||||
|
||||
export const moveUser = (
|
||||
userId: string,
|
||||
fromBreakoutId: string | undefined,
|
||||
toBreakoutId: string | undefined,
|
||||
) => makeCall('moveUser', fromBreakoutId, toBreakoutId, userId);
|
||||
|
||||
export default {
|
||||
createBreakoutRoom,
|
||||
moveUser,
|
||||
};
|
@ -0,0 +1,383 @@
|
||||
import styled from 'styled-components';
|
||||
import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
|
||||
import { ScrollboxVertical } from '/imports/ui/stylesheets/styled-components/scrollable';
|
||||
import HoldButton from '/imports/ui/components/presentation/presentation-toolbar/zoom-tool/holdButton/component';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
import { FlexRow, FlexColumn } from '/imports/ui/stylesheets/styled-components/placeholders';
|
||||
import {
|
||||
colorDanger,
|
||||
colorGray,
|
||||
colorGrayLight,
|
||||
colorGrayLighter,
|
||||
colorWhite,
|
||||
colorPrimary,
|
||||
colorBlueLight,
|
||||
colorGrayLightest,
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import { fontSizeSmall, fontSizeBase, fontSizeSmaller } from '/imports/ui/stylesheets/styled-components/typography';
|
||||
import {
|
||||
borderRadius,
|
||||
borderSize,
|
||||
lgPaddingX,
|
||||
lgPaddingY,
|
||||
} from '/imports/ui/stylesheets/styled-components/general';
|
||||
|
||||
type withValidProp = {
|
||||
valid: boolean;
|
||||
};
|
||||
|
||||
type BreakoutBoxProps = {
|
||||
hundred: boolean;
|
||||
};
|
||||
|
||||
type RoomNameProps = {
|
||||
value: string;
|
||||
duplicated: boolean;
|
||||
maxLength: number;
|
||||
};
|
||||
|
||||
type LabelTextProps = {
|
||||
bold: boolean;
|
||||
};
|
||||
|
||||
const BoxContainer = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-gap: 1.6rem 1rem;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: 1rem;
|
||||
`;
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
grid-template-areas: "sidebar content";
|
||||
grid-gap: 1.5rem;
|
||||
`;
|
||||
|
||||
const Alert = styled.div<withValidProp>`
|
||||
grid-area: sidebar;
|
||||
margin-bottom: 2.5rem;
|
||||
${({ valid }) => valid === false && `
|
||||
position: relative;
|
||||
|
||||
& > * {
|
||||
border-color: ${colorDanger} !important;
|
||||
color: ${colorDanger};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const FreeJoinLabel = styled.label`
|
||||
font-size: ${fontSizeSmall};
|
||||
font-weight: bolder;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: ${fontSizeSmall};
|
||||
margin-bottom: 0.2rem;
|
||||
|
||||
& > * {
|
||||
margin: 0 .5rem 0 0;
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin: 0 0 0 .5rem;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const BreakoutNameInput = styled.input`
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
padding: .25rem .25rem .25rem 0;
|
||||
margin: 0;
|
||||
&::placeholder {
|
||||
color: ${colorGray};
|
||||
opacity: 1;
|
||||
}
|
||||
border: 1px solid ${colorGrayLightest};
|
||||
margin-bottom: 1rem;
|
||||
|
||||
${({ readOnly }) => readOnly && `
|
||||
cursor: default;
|
||||
`}
|
||||
`;
|
||||
|
||||
const BreakoutBox = styled(ScrollboxVertical)<BreakoutBoxProps>`
|
||||
width: 100%;
|
||||
height: 10rem;
|
||||
border: 1px solid ${colorGrayLightest};
|
||||
border-radius: ${borderRadius};
|
||||
padding: ${lgPaddingY} 0;
|
||||
|
||||
${({ hundred }) => hundred && `
|
||||
height: 100%;
|
||||
`}
|
||||
`;
|
||||
|
||||
const SpanWarn = styled.span<withValidProp>`
|
||||
${({ valid }) => valid && `
|
||||
display: none;
|
||||
`}
|
||||
|
||||
${({ valid }) => !valid && `
|
||||
margin: .25rem;
|
||||
position: absolute;
|
||||
font-size: ${fontSizeSmall};
|
||||
color: ${colorDanger};
|
||||
font-weight: 200;
|
||||
white-space: nowrap;
|
||||
`}
|
||||
`;
|
||||
|
||||
const RoomName = styled(BreakoutNameInput)<RoomNameProps>`
|
||||
${({ value }) => value.length === 0 && `
|
||||
border-color: ${colorDanger} !important;
|
||||
`}
|
||||
|
||||
${({ duplicated }) => duplicated && `
|
||||
border-color: ${colorDanger} !important;
|
||||
`}
|
||||
`;
|
||||
|
||||
const BreakoutSettings = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 2fr;
|
||||
grid-template-rows: 1fr;
|
||||
grid-gap: 2rem;
|
||||
|
||||
@media ${smallOnly} {
|
||||
grid-template-columns: 1fr ;
|
||||
grid-template-rows: 1fr 1fr 1fr;
|
||||
flex-direction: column;
|
||||
}
|
||||
`;
|
||||
|
||||
const FormLabel = styled.p<withValidProp>`
|
||||
color: ${colorGray};
|
||||
white-space: nowrap;
|
||||
margin-bottom: .5rem;
|
||||
|
||||
${({ valid }) => !valid && `
|
||||
color: ${colorDanger};
|
||||
`}
|
||||
`;
|
||||
|
||||
const InputRooms = styled.select<withValidProp>`
|
||||
background-color: ${colorWhite};
|
||||
color: ${colorGray};
|
||||
border: 1px solid ${colorGrayLighter};
|
||||
border-radius: ${borderRadius};
|
||||
width: 100%;
|
||||
padding-top: .25rem;
|
||||
padding-bottom: .25rem;
|
||||
padding: .25rem 0 .25rem .25rem;
|
||||
|
||||
${({ valid }) => !valid && `
|
||||
border-color: ${colorDanger} !important;
|
||||
`}
|
||||
`;
|
||||
|
||||
const DurationLabel = styled.label<withValidProp>`
|
||||
${({ valid }) => !valid && `
|
||||
& > * {
|
||||
border-color: ${colorDanger} !important;
|
||||
color: ${colorDanger};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const LabelText = styled.p<LabelTextProps>`
|
||||
color: ${colorGray};
|
||||
white-space: nowrap;
|
||||
margin-bottom: .5rem;
|
||||
|
||||
${({ bold }) => bold && `
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
`}
|
||||
`;
|
||||
|
||||
const DurationArea = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const DurationInput = styled.input`
|
||||
background-color: ${colorWhite};
|
||||
color: ${colorGray};
|
||||
border: 1px solid ${colorGrayLighter};
|
||||
border-radius: ${borderRadius};
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: .25rem;
|
||||
|
||||
|
||||
&::placeholder {
|
||||
color: ${colorGray};
|
||||
}
|
||||
`;
|
||||
|
||||
const HoldButtonWrapper = styled(HoldButton)`
|
||||
& > button > span {
|
||||
padding-bottom: ${borderSize};
|
||||
}
|
||||
|
||||
& > button > span > i {
|
||||
color: ${colorGray};
|
||||
width: ${lgPaddingX};
|
||||
height: ${lgPaddingX};
|
||||
font-size: 170% !important;
|
||||
}
|
||||
`;
|
||||
|
||||
const AssignBtnsContainer = styled.div`
|
||||
justify-items: center;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: baseline;
|
||||
margin-top: auto;
|
||||
`;
|
||||
// @ts-ignore - Button is a JS component
|
||||
const AssignBtns = styled(Button)`
|
||||
color: ${colorDanger};
|
||||
font-size: ${fontSizeSmall};
|
||||
white-space: nowrap;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
${({ random }) => random && `
|
||||
color: ${colorPrimary};
|
||||
`}
|
||||
`;
|
||||
|
||||
const CheckBoxesContainer = styled(FlexRow)`
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: flex-end;
|
||||
`;
|
||||
|
||||
const Separator = styled.div`
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
margin: 1rem 0;
|
||||
border: 1px solid ${colorGrayLightest};
|
||||
`;
|
||||
|
||||
const FreeJoinCheckbox = styled.input`
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
`;
|
||||
|
||||
const RoomUserItem = styled.p`
|
||||
margin: 0;
|
||||
padding: .25rem 0 .25rem .25rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
[dir="rtl"] & {
|
||||
padding: .25rem .25rem .25rem 0;
|
||||
}
|
||||
|
||||
span.close {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin-right: 5px;
|
||||
font-size: ${fontSizeSmaller};
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background-color: ${colorPrimary};
|
||||
color: ${colorWhite};
|
||||
}
|
||||
`;
|
||||
|
||||
const LockIcon = styled.span`
|
||||
float: right;
|
||||
margin-right: 1rem;
|
||||
|
||||
@media ${smallOnly} {
|
||||
margin-left: .5rem;
|
||||
margin-right: auto;
|
||||
float: left;
|
||||
}
|
||||
|
||||
&:after {
|
||||
font-family: 'bbb-icons' !important;
|
||||
content: '\\e926';
|
||||
color: ${colorGrayLight};
|
||||
}
|
||||
`;
|
||||
|
||||
const ListContainer = styled(FlexColumn)`
|
||||
justify-content: flex-start;
|
||||
`;
|
||||
|
||||
const RoomItem = styled.div`
|
||||
margin: 1rem 0 1rem 0;
|
||||
`;
|
||||
|
||||
const ItemTitle = styled.h2`
|
||||
margin: 0;
|
||||
color: ${colorBlueLight};
|
||||
`;
|
||||
|
||||
// @ts-ignore - Button is a JS component
|
||||
const ItemButton = styled(Button)`
|
||||
padding: 0;
|
||||
outline: none !important;
|
||||
|
||||
& > span {
|
||||
color: ${colorBlueLight};
|
||||
}
|
||||
`;
|
||||
|
||||
const WithError = styled.span`
|
||||
color: ${colorDanger};
|
||||
`;
|
||||
|
||||
const SubTitle = styled.p`
|
||||
font-size: ${fontSizeBase};
|
||||
text-align: justify;
|
||||
color: ${colorGray};
|
||||
`;
|
||||
|
||||
const Content = styled(FlexColumn)``;
|
||||
|
||||
export default {
|
||||
BoxContainer,
|
||||
Alert,
|
||||
FreeJoinLabel,
|
||||
BreakoutNameInput,
|
||||
BreakoutBox,
|
||||
SpanWarn,
|
||||
RoomName,
|
||||
BreakoutSettings,
|
||||
FormLabel,
|
||||
InputRooms,
|
||||
DurationLabel,
|
||||
LabelText,
|
||||
DurationArea,
|
||||
DurationInput,
|
||||
HoldButtonWrapper,
|
||||
AssignBtnsContainer,
|
||||
AssignBtns,
|
||||
CheckBoxesContainer,
|
||||
Separator,
|
||||
FreeJoinCheckbox,
|
||||
RoomUserItem,
|
||||
LockIcon,
|
||||
ListContainer,
|
||||
RoomItem,
|
||||
ItemTitle,
|
||||
ItemButton,
|
||||
WithError,
|
||||
SubTitle,
|
||||
Content,
|
||||
ContentContainer,
|
||||
};
|
@ -31,6 +31,7 @@ import { useMutation } from '@apollo/client';
|
||||
import { SEND_GROUP_CHAT_MSG } from './mutations';
|
||||
import Storage from '/imports/ui/services/storage/session';
|
||||
import { indexOf, without } from '/imports/utils/array-utils';
|
||||
import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
|
||||
|
||||
// @ts-ignore - temporary, while meteor exists in the project
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
@ -394,13 +395,13 @@ const ChatMessageFormContainer: React.FC = ({
|
||||
const intl = useIntl();
|
||||
const [showEmojiPicker, setShowEmojiPicker] = React.useState(false);
|
||||
const idChatOpen: string = layoutSelect((i: Layout) => i.idChatOpen);
|
||||
const chat = useChat((c: Partial<Chat>) => ({
|
||||
const { data: chat } = useChat((c: Partial<Chat>) => ({
|
||||
participant: c?.participant,
|
||||
chatId: c?.chatId,
|
||||
public: c?.public,
|
||||
}), idChatOpen) as Partial<Chat>;
|
||||
}), idChatOpen) as GraphqlDataHookSubscriptionResponse<Partial<Chat>>;
|
||||
|
||||
const currentUser = useCurrentUser((c) => ({
|
||||
const { data: currentUser } = useCurrentUser((c) => ({
|
||||
isModerator: c?.isModerator,
|
||||
locked: c?.locked,
|
||||
}));
|
||||
@ -409,7 +410,7 @@ const ChatMessageFormContainer: React.FC = ({
|
||||
? intl.formatMessage(messages.titlePrivate, { 0: chat?.participant?.name })
|
||||
: intl.formatMessage(messages.titlePublic);
|
||||
|
||||
const meeting = useMeeting((m) => ({
|
||||
const { data: meeting } = useMeeting((m) => ({
|
||||
lockSettings: m?.lockSettings,
|
||||
}));
|
||||
|
||||
|
@ -23,6 +23,7 @@ import { Message } from '/imports/ui/Types/message';
|
||||
import ChatPopupContainer from '../chat-popup/component';
|
||||
import { ChatEvents } from '/imports/ui/core/enums/chat';
|
||||
import { Layout } from '../../../layout/layoutTypes';
|
||||
import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
|
||||
|
||||
// @ts-ignore - temporary, while meteor exists in the project
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
@ -316,14 +317,14 @@ const ChatMessageListContainer: React.FC = () => {
|
||||
const idChatOpen = layoutSelect((i: Layout) => i.idChatOpen);
|
||||
const isPublicChat = idChatOpen === PUBLIC_CHAT_KEY;
|
||||
const chatId = !isPublicChat ? idChatOpen : PUBLIC_GROUP_CHAT_KEY;
|
||||
const currentChat = useChat((chat) => {
|
||||
const { data: currentChat } = useChat((chat) => {
|
||||
return {
|
||||
chatId: chat.chatId,
|
||||
totalMessages: chat.totalMessages,
|
||||
totalUnread: chat.totalUnread,
|
||||
lastSeenAt: chat.lastSeenAt,
|
||||
};
|
||||
}, chatId) as Partial<Chat>;
|
||||
}, chatId) as GraphqlDataHookSubscriptionResponse<Partial<Chat>>;
|
||||
|
||||
const [setMessageAsSeenMutation] = useMutation(LAST_SEEN_MUTATION);
|
||||
|
||||
|
@ -37,6 +37,14 @@ const intlMessages = defineMessages({
|
||||
id: 'app.chat.clearPublicChatMessage',
|
||||
description: 'message of when clear the public chat',
|
||||
},
|
||||
userAway: {
|
||||
id: 'app.chat.away',
|
||||
description: 'message when user is away',
|
||||
},
|
||||
userNotAway: {
|
||||
id: 'app.chat.notAway',
|
||||
description: 'message when user is no longer away',
|
||||
},
|
||||
});
|
||||
|
||||
function isInViewport(el: HTMLDivElement) {
|
||||
@ -137,6 +145,20 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
|
||||
/>
|
||||
),
|
||||
};
|
||||
case ChatMessageType.USER_AWAY_STATUS_MSG: {
|
||||
const { away } = JSON.parse(message.messageMetadata);
|
||||
return {
|
||||
name: message.senderName,
|
||||
color: '#0F70D7',
|
||||
isModerator: true,
|
||||
component: (
|
||||
<ChatMessageTextContent
|
||||
emphasizedMessage
|
||||
text={(away) ? intl.formatMessage(intlMessages.userAway) : intl.formatMessage(intlMessages.userNotAway)}
|
||||
/>
|
||||
),
|
||||
};
|
||||
}
|
||||
case ChatMessageType.TEXT:
|
||||
default:
|
||||
return {
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
import { Message } from '/imports/ui/Types/message';
|
||||
import ChatMessage from './chat-message/component';
|
||||
import { useCreateUseSubscription } from '/imports/ui/core/hooks/createUseSubscription';
|
||||
import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
|
||||
|
||||
// @ts-ignore - temporary, while meteor exists in the project
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
@ -76,22 +77,26 @@ const ChatListPageContainer: React.FC<ChatListPageContainerProps> = ({
|
||||
const variables = isPublicChat
|
||||
? defaultVariables : { ...defaultVariables, requestedChatId: chatId };
|
||||
|
||||
const useChatMessageSubscription = useCreateUseSubscription<Partial<Message>>(chatQuery, variables, true);
|
||||
const chatMessageData = useChatMessageSubscription((msg) => msg) as Array<Message>;
|
||||
const useChatMessageSubscription = useCreateUseSubscription<Message>(chatQuery, variables, true);
|
||||
const {
|
||||
data: chatMessageData,
|
||||
} = useChatMessageSubscription((msg) => msg) as GraphqlDataHookSubscriptionResponse<Message[]>;
|
||||
|
||||
if (chatMessageData.length > 0) {
|
||||
setLastSender(page, chatMessageData[chatMessageData.length - 1].user?.userId);
|
||||
}
|
||||
if (chatMessageData) {
|
||||
if (chatMessageData.length > 0 && chatMessageData[chatMessageData.length - 1].user?.userId) {
|
||||
setLastSender(page, chatMessageData[chatMessageData.length - 1].user?.userId);
|
||||
}
|
||||
|
||||
return (
|
||||
<ChatListPage
|
||||
messages={chatMessageData}
|
||||
lastSenderPreviousPage={lastSenderPreviousPage}
|
||||
page={page}
|
||||
markMessageAsSeen={markMessageAsSeen}
|
||||
scrollRef={scrollRef}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<ChatListPage
|
||||
messages={chatMessageData}
|
||||
lastSenderPreviousPage={lastSenderPreviousPage}
|
||||
page={page}
|
||||
markMessageAsSeen={markMessageAsSeen}
|
||||
scrollRef={scrollRef}
|
||||
/>
|
||||
);
|
||||
} return (<></>);
|
||||
};
|
||||
|
||||
export default ChatListPageContainer;
|
||||
|
@ -18,6 +18,7 @@ import { Layout } from '../../../layout/layoutTypes';
|
||||
import useChat from '/imports/ui/core/hooks/useChat';
|
||||
import useMeeting from '/imports/ui/core/hooks/useMeeting';
|
||||
import { Chat } from '/imports/ui/Types/chat';
|
||||
import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
|
||||
|
||||
const DEBUG_CONSOLE = false;
|
||||
|
||||
@ -111,7 +112,7 @@ const TypingIndicator: React.FC<TypingIndicatorProps> = ({
|
||||
const TypingIndicatorContainer: React.FC = () => {
|
||||
const idChatOpen: string = layoutSelect((i: Layout) => i.idChatOpen);
|
||||
const intl = useIntl();
|
||||
const currentUser = useCurrentUser((user: Partial<User>) => {
|
||||
const { data: currentUser } = useCurrentUser((user: Partial<User>) => {
|
||||
return {
|
||||
userId: user.userId,
|
||||
isModerator: user.isModerator,
|
||||
@ -120,15 +121,15 @@ const TypingIndicatorContainer: React.FC = () => {
|
||||
});
|
||||
// eslint-disable-next-line no-unused-expressions, no-console
|
||||
DEBUG_CONSOLE && console.log('TypingIndicatorContainer:currentUser', currentUser);
|
||||
const chat = useChat((c) => {
|
||||
const { data: chat } = useChat((c) => {
|
||||
return {
|
||||
participant: c?.participant,
|
||||
chatId: c?.chatId,
|
||||
public: c?.public,
|
||||
};
|
||||
}, idChatOpen) as Partial<Chat>;
|
||||
}, idChatOpen) as GraphqlDataHookSubscriptionResponse<Chat>;
|
||||
|
||||
const meeting = useMeeting((m) => ({
|
||||
const { data: meeting } = useMeeting((m) => ({
|
||||
lockSettings: m?.lockSettings,
|
||||
}));
|
||||
|
||||
|
@ -13,6 +13,7 @@ import useChat from '/imports/ui/core/hooks/useChat';
|
||||
import { Chat as ChatType } from '/imports/ui/Types/chat';
|
||||
import { layoutDispatch } from '/imports/ui/components/layout/context';
|
||||
import browserInfo from '/imports/utils/browserInfo';
|
||||
import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
|
||||
|
||||
interface ChatProps {
|
||||
|
||||
@ -43,16 +44,16 @@ const ChatContainer: React.FC = () => {
|
||||
const idChatOpen = layoutSelect((i: Layout) => i.idChatOpen);
|
||||
const sidebarContent = layoutSelectInput((i: Input) => i.sidebarContent);
|
||||
const layoutContextDispatch = layoutDispatch();
|
||||
const chats = useChat((chat) => {
|
||||
const { data: chats } = useChat((chat) => {
|
||||
return {
|
||||
chatId: chat.chatId,
|
||||
participant: chat.participant,
|
||||
};
|
||||
}) as Partial<ChatType>[];
|
||||
}) as GraphqlDataHookSubscriptionResponse<Partial<ChatType>[]>;
|
||||
|
||||
const [pendingChat, setPendingChat] = usePendingChat();
|
||||
|
||||
if (pendingChat) {
|
||||
if (pendingChat && chats) {
|
||||
const chat = chats.find((c) => {
|
||||
return c.participant?.userId === pendingChat;
|
||||
});
|
||||
|
@ -251,7 +251,7 @@ class MessageForm extends PureComponent {
|
||||
stopUserTyping,
|
||||
} = this.props;
|
||||
const { message } = this.state;
|
||||
let msg = message.trim();
|
||||
const msg = message.trim();
|
||||
|
||||
if (msg.length < minMessageLength) return;
|
||||
|
||||
@ -291,8 +291,6 @@ class MessageForm extends PureComponent {
|
||||
<Styled.EmojiPickerWrapper>
|
||||
<Styled.EmojiPicker
|
||||
onEmojiSelect={(emojiObject) => this.handleEmojiSelect(emojiObject)}
|
||||
showPreview={false}
|
||||
showSkinTones={false}
|
||||
/>
|
||||
</Styled.EmojiPickerWrapper>
|
||||
);
|
||||
|
@ -109,21 +109,11 @@ const EmojiButton = styled(Button)`
|
||||
`;
|
||||
|
||||
const EmojiPickerWrapper = styled.div`
|
||||
.emoji-mart {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
.emoji-mart-anchor {
|
||||
cursor: pointer;
|
||||
}
|
||||
.emoji-mart-emoji {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
.emoji-mart-category-list {
|
||||
span {
|
||||
cursor: pointer !important;
|
||||
display: inline-block !important;
|
||||
}
|
||||
em-emoji-picker {
|
||||
width: 100%;
|
||||
max-height: 300px;
|
||||
}
|
||||
padding-bottom: 5px;
|
||||
`;
|
||||
|
||||
const EmojiPicker = styled(EmojiPickerComponent)``;
|
||||
|
@ -1,26 +1,16 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl } from 'react-intl';
|
||||
import { Picker } from 'emoji-mart';
|
||||
import 'emoji-mart/css/emoji-mart.css';
|
||||
import data from '@emoji-mart/data';
|
||||
import Picker from '@emoji-mart/react';
|
||||
|
||||
const DISABLE_EMOJIS = Meteor.settings.public.chat.disableEmojis;
|
||||
const FREQUENT_SORT_ON_CLICK = Meteor.settings.public.chat.emojiPicker.frequentEmojiSortOnClick;
|
||||
|
||||
const propTypes = {
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
onEmojiSelect: PropTypes.func.isRequired,
|
||||
style: PropTypes.shape({}),
|
||||
showPreview: PropTypes.bool,
|
||||
showSkinTones: PropTypes.bool,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
style: null,
|
||||
showPreview: true,
|
||||
showSkinTones: true,
|
||||
};
|
||||
|
||||
const emojisToExclude = [
|
||||
@ -31,8 +21,6 @@ const EmojiPicker = (props) => {
|
||||
const {
|
||||
intl,
|
||||
onEmojiSelect,
|
||||
showPreview,
|
||||
showSkinTones,
|
||||
} = props;
|
||||
|
||||
const i18n = {
|
||||
@ -65,23 +53,19 @@ const EmojiPicker = (props) => {
|
||||
|
||||
return (
|
||||
<Picker
|
||||
emoji=""
|
||||
onSelect={(emojiObject, event) => onEmojiSelect(emojiObject, event)}
|
||||
enableFrequentEmojiSort={FREQUENT_SORT_ON_CLICK}
|
||||
native
|
||||
title=""
|
||||
data={data}
|
||||
onEmojiSelect={(emojiObject, event) => onEmojiSelect(emojiObject, event)}
|
||||
emojiSize={24}
|
||||
emojiTooltip
|
||||
i18n={i18n}
|
||||
showPreview={showPreview}
|
||||
showSkinTones={showSkinTones}
|
||||
useButton
|
||||
emojisToShowFilter={(emoji) => !emojisToExclude.includes(emoji.unified)}
|
||||
previewPosition="none"
|
||||
skinTonePosition="none"
|
||||
theme="light"
|
||||
dynamicWidth
|
||||
exceptEmojis={emojisToExclude}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
EmojiPicker.propTypes = propTypes;
|
||||
EmojiPicker.defaultProps = defaultProps;
|
||||
|
||||
export default injectIntl(EmojiPicker);
|
||||
|
@ -1,71 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl } from 'react-intl';
|
||||
import { Picker } from 'emoji-mart';
|
||||
import 'emoji-mart/css/emoji-mart.css';
|
||||
|
||||
const propTypes = {
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
onEmojiSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
};
|
||||
|
||||
const emojisToExclude = [
|
||||
// reactions
|
||||
'1F605', '1F61C', '1F604', '1F609', '1F602', '1F92A',
|
||||
'1F634', '1F62C', '1F633', '1F60D', '02665', '1F499',
|
||||
'1F615', '1F61F', '1F928', '1F644', '1F612', '1F621',
|
||||
'1F614', '1F625', '1F62D', '1F62F', '1F631', '1F630',
|
||||
'1F44F', '1F44C', '1F44D', '1F44E', '1F4AA', '1F44A',
|
||||
'1F64C', '1F64F', '1F440', '1F4A9', '1F921', '1F480',
|
||||
];
|
||||
|
||||
const inlineStyle = {
|
||||
margin: '.5rem 0',
|
||||
alignSelf: 'center',
|
||||
width: '100%',
|
||||
};
|
||||
|
||||
const ReactionsPicker = (props) => {
|
||||
const {
|
||||
intl,
|
||||
onEmojiSelect,
|
||||
style,
|
||||
} = props;
|
||||
|
||||
const i18n = {
|
||||
notfound: intl.formatMessage({ id: 'app.emojiPicker.notFound' }),
|
||||
clear: intl.formatMessage({ id: 'app.emojiPicker.clear' }),
|
||||
skintext: intl.formatMessage({ id: 'app.emojiPicker.skintext' }),
|
||||
categories: {
|
||||
reactions: intl.formatMessage({ id: 'app.emojiPicker.categories.reactions' }),
|
||||
},
|
||||
categorieslabel: intl.formatMessage({ id: 'app.emojiPicker.categories.label' }),
|
||||
};
|
||||
|
||||
return (
|
||||
<Picker
|
||||
onSelect={(emojiObject, event) => onEmojiSelect(emojiObject, event)}
|
||||
native
|
||||
emoji=""
|
||||
title=""
|
||||
emojiSize={30}
|
||||
emojiTooltip
|
||||
style={Object.assign(inlineStyle, style)}
|
||||
i18n={i18n}
|
||||
showPreview={false}
|
||||
showSkinTones={false}
|
||||
emojisToShowFilter={(emoji) => emojisToExclude.includes(emoji.unified)}
|
||||
/>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
ReactionsPicker.propTypes = propTypes;
|
||||
ReactionsPicker.defaultProps = defaultProps;
|
||||
|
||||
export default injectIntl(ReactionsPicker);
|
@ -0,0 +1,35 @@
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
const EmojiWrapper = styled.span`
|
||||
padding-top: 0.9em;
|
||||
padding-bottom: 0.1em;
|
||||
${({ selected }) => !selected && css`
|
||||
:hover {
|
||||
border-radius:100%;
|
||||
outline-color: transparent;
|
||||
outline-style:solid;
|
||||
box-shadow: 0 0 0 0.25em #eee;
|
||||
background-color: #eee;
|
||||
opacity: 0.75;
|
||||
}
|
||||
`}
|
||||
${({ selected }) => selected && css`
|
||||
em-emoji {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`}
|
||||
${({ selected, emoji }) => selected && selected !== emoji && css`
|
||||
opacity: 0.75;
|
||||
`}
|
||||
${({ selected, emoji }) => selected && selected === emoji && css`
|
||||
border-radius:100%;
|
||||
outline-color: transparent;
|
||||
outline-style:solid;
|
||||
box-shadow: 0 0 0 0.25em #eee;
|
||||
background-color: #eee;
|
||||
`}
|
||||
`;
|
||||
|
||||
export default {
|
||||
EmojiWrapper,
|
||||
};
|
@ -1,27 +0,0 @@
|
||||
.dropdownContent {
|
||||
section[class^="emoji-mart-search"] {
|
||||
display: none !important;
|
||||
}
|
||||
section[class^="emoji-mart emoji-mart-light"] {
|
||||
display: unset;
|
||||
border: unset;
|
||||
}
|
||||
div[class^="emoji-mart-bar"] {
|
||||
display: none !important;
|
||||
}
|
||||
section[aria-label^="Frequently Used"] {
|
||||
display: none !important;
|
||||
}
|
||||
div[class^="emoji-mart-category-label"] {
|
||||
display: none !important;
|
||||
}
|
||||
div[class^="emoji-mart-scroll"] {
|
||||
overflow-y: unset;
|
||||
height: unset;
|
||||
}
|
||||
ul[class^="emoji-mart-category-list"] {
|
||||
span {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import Service from './service';
|
||||
|
||||
const EmojiRain = ({ reactions }) => {
|
||||
const containerRef = useRef(null);
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
const EMOJI_SIZE = Meteor.settings.public.app.emojiRain.emojiSize;
|
||||
const NUMBER_OF_EMOJIS = Meteor.settings.public.app.emojiRain.numberOfEmojis;
|
||||
const EMOJI_RAIN_ENABLED = Meteor.settings.public.app.emojiRain.enabled;
|
||||
|
||||
const { animations } = Settings.application;
|
||||
|
||||
function createEmojiRain(emoji) {
|
||||
const coord = Service.getInteractionsButtonCoordenates();
|
||||
const flyingEmojis = [];
|
||||
|
||||
for (i = 0; i < NUMBER_OF_EMOJIS; i++) {
|
||||
const initialPosition = {
|
||||
x: coord.x + coord.width / 8,
|
||||
y: coord.y + coord.height / 5,
|
||||
};
|
||||
const endPosition = {
|
||||
x: Math.floor(Math.random() * 100) + coord.x - 100 / 2,
|
||||
y: Math.floor(Math.random() * 300) + coord.y / 2,
|
||||
};
|
||||
const scale = Math.floor(Math.random() * (8 - 4 + 1)) - 40;
|
||||
const sec = Math.floor(Math.random() * 1700) + 2000;
|
||||
|
||||
const shapeElement = document.createElement('svg');
|
||||
const emojiElement = document.createElement('text');
|
||||
emojiElement.setAttribute('x', '50%');
|
||||
emojiElement.setAttribute('y', '50%');
|
||||
emojiElement.innerHTML = emoji;
|
||||
|
||||
shapeElement.style.position = 'absolute';
|
||||
shapeElement.style.left = `${initialPosition.x}px`;
|
||||
shapeElement.style.top = `${initialPosition.y}px`;
|
||||
shapeElement.style.transform = `scaleX(0.${scale}) scaleY(0.${scale})`;
|
||||
shapeElement.style.transition = `${sec}ms`;
|
||||
shapeElement.style.fontSize = `${EMOJI_SIZE}em`;
|
||||
shapeElement.style.pointerEvents = 'none';
|
||||
|
||||
shapeElement.appendChild(emojiElement);
|
||||
containerRef.current.appendChild(shapeElement);
|
||||
|
||||
flyingEmojis.push({ shapeElement, endPosition });
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => setTimeout(() => flyingEmojis.forEach((emoji) => {
|
||||
const { shapeElement, endPosition } = emoji;
|
||||
shapeElement.style.left = `${endPosition.x}px`;
|
||||
shapeElement.style.top = `${endPosition.y}px`;
|
||||
shapeElement.style.transform = 'scaleX(0) scaleY(0)';
|
||||
}), 0));
|
||||
|
||||
setTimeout(() => {
|
||||
flyingEmojis.forEach((emoji) => emoji.shapeElement.remove());
|
||||
flyingEmojis.length = 0;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.hidden) {
|
||||
setIsAnimating(false);
|
||||
} else if (EMOJI_RAIN_ENABLED && animations) {
|
||||
setIsAnimating(true);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (EMOJI_RAIN_ENABLED && animations && !isAnimating && !document.hidden) {
|
||||
setIsAnimating(true);
|
||||
} else if (!animations) {
|
||||
setIsAnimating(false);
|
||||
}
|
||||
}, [EMOJI_RAIN_ENABLED, animations, isAnimating]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAnimating) {
|
||||
reactions.forEach((reaction) => {
|
||||
const currentTime = new Date().getTime();
|
||||
const secondsSinceCreated = (currentTime - reaction.creationDate.getTime()) / 1000;
|
||||
if (secondsSinceCreated <= 1 && (reaction.reaction !== 'none')) {
|
||||
createEmojiRain(reaction.reaction);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [isAnimating, reactions]);
|
||||
|
||||
const containerStyle = {
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
overflow: 'hidden',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 2,
|
||||
};
|
||||
|
||||
return <div ref={containerRef} style={containerStyle} />;
|
||||
};
|
||||
|
||||
export default EmojiRain;
|
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import EmojiRain from './component';
|
||||
import UserReaction from '/imports/api/user-reaction';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
|
||||
const EmojiRainContainer = (props) => <EmojiRain {...props} />;
|
||||
|
||||
export default withTracker(() => ({
|
||||
reactions: UserReaction.find({ meetingId: Auth.meetingID }).fetch(),
|
||||
}))(EmojiRainContainer);
|
@ -0,0 +1,9 @@
|
||||
const getInteractionsButtonCoordenates = () => {
|
||||
const el = document.getElementById('interactionsButton');
|
||||
const coordenada = el.getBoundingClientRect();
|
||||
return coordenada;
|
||||
};
|
||||
|
||||
export default {
|
||||
getInteractionsButtonCoordenates,
|
||||
};
|
@ -316,11 +316,11 @@ const ExternalVideoPlayerContainer: React.FC = () => {
|
||||
// @ts-ignore - temporary while hybrid (meteor+GraphQl)
|
||||
const isEchoTest = useReactiveVar(audioManager._isEchoTest.value) as boolean;
|
||||
|
||||
const currentUser = useCurrentUser((user) => ({
|
||||
const { data: currentUser } = useCurrentUser((user) => ({
|
||||
presenter: user.presenter,
|
||||
}));
|
||||
|
||||
const currentMeeting = useMeeting((m) => ({
|
||||
const { data: currentMeeting } = useMeeting((m) => ({
|
||||
externalVideo: m.externalVideo,
|
||||
}));
|
||||
|
||||
|
@ -43,6 +43,7 @@ const propTypes = {
|
||||
setPushLayout: PropTypes.func,
|
||||
shouldShowScreenshare: PropTypes.bool,
|
||||
shouldShowExternalVideo: PropTypes.bool,
|
||||
enforceLayout: PropTypes.string,
|
||||
};
|
||||
|
||||
class PushLayoutEngine extends React.Component {
|
||||
@ -63,10 +64,11 @@ class PushLayoutEngine extends React.Component {
|
||||
meetingPresentationIsOpen,
|
||||
shouldShowScreenshare,
|
||||
shouldShowExternalVideo,
|
||||
enforceLayout,
|
||||
} = this.props;
|
||||
|
||||
const userLayout = LAYOUT_TYPE[getFromUserSettings('bbb_change_layout', false)];
|
||||
Settings.application.selectedLayout = userLayout || meetingLayout;
|
||||
Settings.application.selectedLayout = enforceLayout || userLayout || meetingLayout;
|
||||
|
||||
let selectedLayout = Settings.application.selectedLayout;
|
||||
if (isMobile()) {
|
||||
@ -142,19 +144,22 @@ class PushLayoutEngine extends React.Component {
|
||||
selectedLayout,
|
||||
setMeetingLayout,
|
||||
setPushLayout,
|
||||
enforceLayout,
|
||||
} = this.props;
|
||||
|
||||
const meetingLayoutDidChange = meetingLayout !== prevProps.meetingLayout;
|
||||
const pushLayoutMeetingDidChange = pushLayoutMeeting !== prevProps.pushLayoutMeeting;
|
||||
const enforceLayoutDidChange = enforceLayout !== prevProps.enforceLayout;
|
||||
const shouldSwitchLayout = isPresenter
|
||||
? meetingLayoutDidChange
|
||||
: (meetingLayoutDidChange || pushLayoutMeetingDidChange) && pushLayoutMeeting;
|
||||
? meetingLayoutDidChange || enforceLayoutDidChange
|
||||
: ((meetingLayoutDidChange || pushLayoutMeetingDidChange) && pushLayoutMeeting) || enforceLayoutDidChange;
|
||||
|
||||
if (shouldSwitchLayout) {
|
||||
|
||||
let contextLayout = meetingLayout;
|
||||
let contextLayout = enforceLayout || meetingLayout;
|
||||
if (isMobile()) {
|
||||
contextLayout = meetingLayout === 'custom' ? 'smart' : meetingLayout;
|
||||
if (contextLayout === 'custom') {
|
||||
contextLayout = 'smart';
|
||||
}
|
||||
}
|
||||
|
||||
layoutContextDispatch({
|
||||
@ -170,7 +175,7 @@ class PushLayoutEngine extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
if (pushLayoutMeetingDidChange) {
|
||||
if (!enforceLayout && pushLayoutMeetingDidChange) {
|
||||
updateSettings({
|
||||
application: {
|
||||
...Settings.application,
|
||||
@ -244,6 +249,7 @@ class PushLayoutEngine extends React.Component {
|
||||
|| cameraIsResizing !== prevProps.cameraIsResizing
|
||||
|| cameraPosition !== prevProps.cameraPosition
|
||||
|| focusedCamera !== prevProps.focusedCamera
|
||||
|| enforceLayout !== prevProps.enforceLayout
|
||||
|| !equalDouble(presentationVideoRate, prevProps.presentationVideoRate);
|
||||
|
||||
if (pushLayout !== prevProps.pushLayout) { // push layout once after presenter toggles / special case where we set pushLayout to false in all viewers
|
||||
|
@ -294,7 +294,7 @@ const RecordingIndicatorContainer: React.FC = () => {
|
||||
error: meetingRecordingError,
|
||||
} = useSubscription<getMeetingRecordingData>(GET_MEETING_RECORDING_DATA);
|
||||
|
||||
const currentUser = useCurrentUser((user: Partial<User>) => ({
|
||||
const { data: currentUser } = useCurrentUser((user: Partial<User>) => ({
|
||||
userId: user.userId,
|
||||
isModerator: user.isModerator,
|
||||
voice: user.voice
|
||||
@ -305,7 +305,7 @@ const RecordingIndicatorContainer: React.FC = () => {
|
||||
: null,
|
||||
} as Partial<User>));
|
||||
|
||||
const currentMeeting = useMeeting((meeting) => ({
|
||||
const { data: currentMeeting } = useMeeting((meeting) => ({
|
||||
meetingId: meeting.meetingId,
|
||||
notifyRecordingIsOn: meeting.notifyRecordingIsOn,
|
||||
}));
|
||||
|
@ -170,7 +170,7 @@ const TalkingIndicator: React.FC<TalkingIndicatorProps> = ({
|
||||
const TalkingIndicatorContainer: React.FC = (() => {
|
||||
if (!enableTalkingIndicator) return () => null;
|
||||
return () => {
|
||||
const currentUser: Partial<User> = useCurrentUser((u: Partial<User>) => ({
|
||||
const { data: currentUser } = useCurrentUser((u: Partial<User>) => ({
|
||||
userId: u?.userId,
|
||||
isModerator: u?.isModerator,
|
||||
}));
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user