Merge remote-tracking branch 'upstream/v2.7.x-release' into 27-dev-apr24

This commit is contained in:
Ramón Souza 2023-04-24 17:26:54 -03:00
commit b121fcbd87
342 changed files with 3943 additions and 1471 deletions

View File

@ -43,6 +43,7 @@ jobs:
- run: ./build/setup.sh bbb-record-core - run: ./build/setup.sh bbb-record-core
- run: ./build/setup.sh bbb-web - run: ./build/setup.sh bbb-web
- run: ./build/setup.sh bbb-webrtc-sfu - run: ./build/setup.sh bbb-webrtc-sfu
- run: ./build/setup.sh bbb-webrtc-recorder
- run: ./build/setup.sh bigbluebutton - run: ./build/setup.sh bigbluebutton
- run: tar cvf artifacts.tar artifacts/ - run: tar cvf artifacts.tar artifacts/
- name: Archive packages - name: Archive packages
@ -182,6 +183,7 @@ jobs:
cp /etc/bigbluebutton/bigbluebutton-release configs/bigbluebutton-release cp /etc/bigbluebutton/bigbluebutton-release configs/bigbluebutton-release
cp /etc/bigbluebutton/turn-stun-servers.xml configs/turn-stun-servers.xml cp /etc/bigbluebutton/turn-stun-servers.xml configs/turn-stun-servers.xml
cp /usr/local/bigbluebutton/bbb-webrtc-sfu/config/default.yml configs/bbb-webrtc-sfu-default.yml cp /usr/local/bigbluebutton/bbb-webrtc-sfu/config/default.yml configs/bbb-webrtc-sfu-default.yml
cp /etc/bbb-webrtc-recorder/bbb-webrtc-recorder.yml configs/bbb-webrtc-recorder-default.yml
cp /usr/share/bigbluebutton/nginx/sip.nginx configs/nginx_sip.nginx cp /usr/share/bigbluebutton/nginx/sip.nginx configs/nginx_sip.nginx
cp /etc/hosts /configs/hosts cp /etc/hosts /configs/hosts
chmod a+r -R configs chmod a+r -R configs
@ -197,4 +199,4 @@ jobs:
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: bbb-logs name: bbb-logs
path: ./bbb-logs.tar.gz path: ./bbb-logs.tar.gz

View File

@ -12,7 +12,7 @@ stages:
# define which docker image to use for builds # define which docker image to use for builds
default: default:
image: gitlab.senfcall.de:5050/senfcall-public/docker-bbb-build:v2022-12-29-grails-524 image: gitlab.senfcall.de:5050/senfcall-public/docker-bbb-build:v2023-04-18
# This stage uses git to find out since when each package has been unmodified. # This stage uses git to find out since when each package has been unmodified.
# it then checks an API endpoint on the package server to find out for which of # it then checks an API endpoint on the package server to find out for which of
@ -47,11 +47,12 @@ get_external_dependencies:
- bbb-etherpad - bbb-etherpad
- bbb-webhooks - bbb-webhooks
- bbb-webrtc-sfu - bbb-webrtc-sfu
- bbb-webrtc-recorder
- freeswitch - freeswitch
- bbb-pads - bbb-pads
- bbb-playback - bbb-playback
expire_in: 1h 30min expire_in: 1h 30min
# template job for build step # template job for build step
.build_job: .build_job:
stage: build stage: build
@ -170,6 +171,11 @@ bbb-webrtc-sfu-build:
script: script:
- build/setup-inside-docker.sh bbb-webrtc-sfu - build/setup-inside-docker.sh bbb-webrtc-sfu
bbb-webrtc-recorder-build:
extends: .build_job
script:
- build/setup-inside-docker.sh bbb-webrtc-recorder
bigbluebutton-build: bigbluebutton-build:
extends: .build_job extends: .build_job
script: script:
@ -180,12 +186,12 @@ push_packages:
stage: push packages stage: push packages
script: build/push_packages.sh script: build/push_packages.sh
resource_group: push_packages resource_group: push_packages
# uncomment the lines below if you want one final # uncomment the lines below if you want one final
# "artifacts" dir with all packages (increases runtime, fills up space on gitlab server) # "artifacts" dir with all packages (increases runtime, fills up space on gitlab server)
#artifacts: #artifacts:
# paths: # paths:
# - artifacts/* # - artifacts/*
# expire_in: 2 days # expire_in: 2 days

View File

@ -14,6 +14,7 @@ import org.bigbluebutton.SystemConfiguration
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import org.bigbluebutton.common2.msgs._ import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.running.RunningMeeting import org.bigbluebutton.core.running.RunningMeeting
import org.bigbluebutton.core.util.ColorPicker
import org.bigbluebutton.core2.RunningMeetings import org.bigbluebutton.core2.RunningMeetings
import org.bigbluebutton.core2.message.senders.MsgBuilder import org.bigbluebutton.core2.message.senders.MsgBuilder
import org.bigbluebutton.service.HealthzService import org.bigbluebutton.service.HealthzService
@ -183,6 +184,9 @@ class BigBlueButtonActor(
// Stop the meeting actor. // Stop the meeting actor.
context.stop(m.actorRef) context.stop(m.actorRef)
} }
//Remove ColorPicker idx of the meeting
ColorPicker.reset(m.props.meetingProp.intId)
} }
} }

View File

@ -0,0 +1,42 @@
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.running.{ LiveMeeting, OutMsgRouter }
trait ChangeUserMobileFlagReqMsgHdlr extends RightsManagementTrait {
this: UsersApp =>
val liveMeeting: LiveMeeting
val outGW: OutMsgRouter
def handleChangeUserMobileFlagReqMsg(msg: ChangeUserMobileFlagReqMsg): Unit = {
log.info("handleChangeUserMobileFlagReqMsg: mobile={} userId={}", msg.body.mobile, msg.body.userId)
def broadcastUserMobileChanged(user: UserState, mobile: Boolean): Unit = {
val routingChange = Routing.addMsgToClientRouting(
MessageTypes.BROADCAST_TO_MEETING,
liveMeeting.props.meetingProp.intId, user.intId
)
val envelopeChange = BbbCoreEnvelope(UserMobileFlagChangedEvtMsg.NAME, routingChange)
val headerChange = BbbClientMsgHeader(UserMobileFlagChangedEvtMsg.NAME, liveMeeting.props.meetingProp.intId,
user.intId)
val bodyChange = UserMobileFlagChangedEvtMsgBody(user.intId, mobile)
val eventChange = UserMobileFlagChangedEvtMsg(headerChange, bodyChange)
val msgEventChange = BbbCommonEnvCoreMsg(envelopeChange, eventChange)
outGW.send(msgEventChange)
}
for {
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId)
} yield {
if (user.mobile != msg.body.mobile) {
val userMobile = Users2x.setMobile(liveMeeting.users2x, user)
broadcastUserMobileChanged(userMobile, msg.body.mobile)
}
}
}
}

View File

@ -3,6 +3,7 @@ package org.bigbluebutton.core.apps.users
import org.bigbluebutton.common2.msgs._ import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.models._ import org.bigbluebutton.core.models._
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter } import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
import org.bigbluebutton.core.util.ColorPicker
import org.bigbluebutton.core2.message.senders.{ MsgBuilder, Sender } import org.bigbluebutton.core2.message.senders.{ MsgBuilder, Sender }
trait RegisterUserReqMsgHdlr { trait RegisterUserReqMsgHdlr {
@ -56,7 +57,7 @@ trait RegisterUserReqMsgHdlr {
val regUser = RegisteredUsers.create(msg.body.intUserId, msg.body.extUserId, val regUser = RegisteredUsers.create(msg.body.intUserId, msg.body.extUserId,
msg.body.name, msg.body.role, msg.body.authToken, msg.body.name, msg.body.role, msg.body.authToken,
msg.body.avatarURL, msg.body.guest, msg.body.authed, guestStatus, msg.body.excludeFromDashboard, false) msg.body.avatarURL, ColorPicker.nextColor(liveMeeting.props.meetingProp.intId), msg.body.guest, msg.body.authed, guestStatus, msg.body.excludeFromDashboard, false)
checkUserConcurrentAccesses(regUser) checkUserConcurrentAccesses(regUser)
@ -89,7 +90,7 @@ trait RegisterUserReqMsgHdlr {
val g = GuestApprovedVO(regUser.id, GuestStatus.ALLOW) val g = GuestApprovedVO(regUser.id, GuestStatus.ALLOW)
UsersApp.approveOrRejectGuest(liveMeeting, outGW, g, SystemUser.ID) UsersApp.approveOrRejectGuest(liveMeeting, outGW, g, SystemUser.ID)
case GuestStatus.WAIT => case GuestStatus.WAIT =>
val guest = GuestWaiting(regUser.id, regUser.name, regUser.role, regUser.guest, regUser.avatarURL, regUser.authed, regUser.registeredOn) val guest = GuestWaiting(regUser.id, regUser.name, regUser.role, regUser.guest, regUser.avatarURL, regUser.color, regUser.authed, regUser.registeredOn)
addGuestToWaitingForApproval(guest, liveMeeting.guestsWaiting) addGuestToWaitingForApproval(guest, liveMeeting.guestsWaiting)
notifyModeratorsOfGuestWaiting(Vector(guest), liveMeeting.users2x, liveMeeting.props.meetingProp.intId) notifyModeratorsOfGuestWaiting(Vector(guest), liveMeeting.users2x, liveMeeting.props.meetingProp.intId)
val notifyEvent = MsgBuilder.buildNotifyRoleInMeetingEvtMsg( val notifyEvent = MsgBuilder.buildNotifyRoleInMeetingEvtMsg(

View File

@ -19,8 +19,8 @@ trait UserConnectedToGlobalAudioMsgHdlr {
val header = BbbClientMsgHeader(UserJoinedVoiceConfToClientEvtMsg.NAME, props.meetingProp.intId, vu.intId) val header = BbbClientMsgHeader(UserJoinedVoiceConfToClientEvtMsg.NAME, props.meetingProp.intId, vu.intId)
val body = UserJoinedVoiceConfToClientEvtMsgBody(voiceConf = msg.header.voiceConf, intId = vu.intId, voiceUserId = vu.intId, val body = UserJoinedVoiceConfToClientEvtMsgBody(voiceConf = msg.header.voiceConf, intId = vu.intId, voiceUserId = vu.intId,
callingWith = vu.callingWith, callerName = vu.callerName, callingWith = vu.callingWith, callerName = vu.callerName, callerNum = vu.callerNum, color = vu.color,
callerNum = vu.callerNum, muted = true, talking = false, listenOnly = true) muted = true, talking = false, listenOnly = true)
val event = UserJoinedVoiceConfToClientEvtMsg(header, body) val event = UserJoinedVoiceConfToClientEvtMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event) val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
outGW.send(msgEvent) outGW.send(msgEvent)
@ -36,6 +36,7 @@ trait UserConnectedToGlobalAudioMsgHdlr {
callingWith = "flash", callingWith = "flash",
callerName = user.name, callerName = user.name,
callerNum = user.name, callerNum = user.name,
color = user.color,
muted = true, muted = true,
talking = false, talking = false,
listenOnly = true, listenOnly = true,

View File

@ -158,6 +158,7 @@ class UsersApp(
with SelectRandomViewerReqMsgHdlr with SelectRandomViewerReqMsgHdlr
with AssignPresenterReqMsgHdlr with AssignPresenterReqMsgHdlr
with ChangeUserPinStateReqMsgHdlr with ChangeUserPinStateReqMsgHdlr
with ChangeUserMobileFlagReqMsgHdlr
with EjectUserFromMeetingCmdMsgHdlr with EjectUserFromMeetingCmdMsgHdlr
with EjectUserFromMeetingSysMsgHdlr with EjectUserFromMeetingSysMsgHdlr
with MuteUserCmdMsgHdlr { with MuteUserCmdMsgHdlr {

View File

@ -130,7 +130,7 @@ trait ValidateAuthTokenReqMsgHdlr extends HandlerHelpers {
def sendAllVoiceUsersInMeeting(requesterId: String, voiceUsers: VoiceUsers, meetingId: String): Unit = { def sendAllVoiceUsersInMeeting(requesterId: String, voiceUsers: VoiceUsers, meetingId: String): Unit = {
val vu = VoiceUsers.findAll(voiceUsers).map { u => val vu = VoiceUsers.findAll(voiceUsers).map { u =>
VoiceConfUser(intId = u.intId, voiceUserId = u.voiceUserId, callingWith = u.callingWith, callerName = u.callerName, VoiceConfUser(intId = u.intId, voiceUserId = u.voiceUserId, callingWith = u.callingWith, callerName = u.callerName,
callerNum = u.callerNum, muted = u.muted, talking = u.talking, listenOnly = u.listenOnly) callerNum = u.callerNum, color = u.color, muted = u.muted, talking = u.talking, listenOnly = u.listenOnly)
} }
val event = MsgBuilder.buildGetVoiceUsersMeetingRespMsg(meetingId, requesterId, vu) val event = MsgBuilder.buildGetVoiceUsersMeetingRespMsg(meetingId, requesterId, vu)

View File

@ -14,7 +14,7 @@ trait SyncGetVoiceUsersMsgHdlr {
def buildSyncGetVoiceUsersRespMsg(): BbbCommonEnvCoreMsg = { def buildSyncGetVoiceUsersRespMsg(): BbbCommonEnvCoreMsg = {
val voiceUsers = VoiceUsers.findAll(liveMeeting.voiceUsers).map { u => val voiceUsers = VoiceUsers.findAll(liveMeeting.voiceUsers).map { u =>
VoiceConfUser(intId = u.intId, voiceUserId = u.voiceUserId, callingWith = u.callingWith, callerName = u.callerName, VoiceConfUser(intId = u.intId, voiceUserId = u.voiceUserId, callingWith = u.callingWith, callerName = u.callerName,
callerNum = u.callerNum, muted = u.muted, talking = u.talking, listenOnly = u.listenOnly) callerNum = u.callerNum, color = u.color, muted = u.muted, talking = u.talking, listenOnly = u.listenOnly)
} }
val routing = Routing.addMsgToHtml5InstanceIdRouting(liveMeeting.props.meetingProp.intId, liveMeeting.props.systemProps.html5InstanceId.toString) val routing = Routing.addMsgToHtml5InstanceIdRouting(liveMeeting.props.meetingProp.intId, liveMeeting.props.systemProps.html5InstanceId.toString)

View File

@ -5,7 +5,7 @@ import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.running.{ LiveMeeting, MeetingActor, OutMsgRouter } import org.bigbluebutton.core.running.{ LiveMeeting, MeetingActor, OutMsgRouter }
import org.bigbluebutton.core2.message.senders.MsgBuilder import org.bigbluebutton.core2.message.senders.MsgBuilder
import org.bigbluebutton.core.models._ import org.bigbluebutton.core.models._
import org.bigbluebutton.core.apps.users.UsersApp import org.bigbluebutton.core.util.ColorPicker
import org.bigbluebutton.core2.MeetingStatus2x import org.bigbluebutton.core2.MeetingStatus2x
trait UserJoinedVoiceConfEvtMsgHdlr extends SystemConfiguration { trait UserJoinedVoiceConfEvtMsgHdlr extends SystemConfiguration {
@ -19,6 +19,8 @@ trait UserJoinedVoiceConfEvtMsgHdlr extends SystemConfiguration {
val guestPolicy = GuestsWaiting.getGuestPolicy(liveMeeting.guestsWaiting) val guestPolicy = GuestsWaiting.getGuestPolicy(liveMeeting.guestsWaiting)
val isDialInUser = msg.body.intId.startsWith(IntIdPrefixType.DIAL_IN) val isDialInUser = msg.body.intId.startsWith(IntIdPrefixType.DIAL_IN)
val userColor = ColorPicker.nextColor(liveMeeting.props.meetingProp.intId)
def notifyModeratorsOfGuestWaiting(guest: GuestWaiting, users: Users2x, meetingId: String): Unit = { def notifyModeratorsOfGuestWaiting(guest: GuestWaiting, users: Users2x, meetingId: String): Unit = {
val moderators = Users2x.findAll(users).filter(p => p.role == Roles.MODERATOR_ROLE) val moderators = Users2x.findAll(users).filter(p => p.role == Roles.MODERATOR_ROLE)
moderators foreach { mod => moderators foreach { mod =>
@ -32,7 +34,7 @@ trait UserJoinedVoiceConfEvtMsgHdlr extends SystemConfiguration {
def registerUserInRegisteredUsers() = { def registerUserInRegisteredUsers() = {
val regUser = RegisteredUsers.create(msg.body.intId, msg.body.voiceUserId, val regUser = RegisteredUsers.create(msg.body.intId, msg.body.voiceUserId,
msg.body.callerIdName, Roles.VIEWER_ROLE, "", msg.body.callerIdName, Roles.VIEWER_ROLE, "", userColor,
"", true, true, GuestStatus.WAIT, true, false) "", true, true, GuestStatus.WAIT, true, false)
RegisteredUsers.add(liveMeeting.registeredUsers, regUser) RegisteredUsers.add(liveMeeting.registeredUsers, regUser)
} }
@ -48,9 +50,11 @@ trait UserJoinedVoiceConfEvtMsgHdlr extends SystemConfiguration {
guestStatus = GuestStatus.WAIT, guestStatus = GuestStatus.WAIT,
emoji = "none", emoji = "none",
pin = false, pin = false,
mobile = false,
presenter = false, presenter = false,
locked = MeetingStatus2x.getPermissions(liveMeeting.status).lockOnJoin, locked = MeetingStatus2x.getPermissions(liveMeeting.status).lockOnJoin,
avatar = "", avatar = "",
color = userColor,
clientType = "", clientType = "",
pickExempted = false, pickExempted = false,
userLeftFlag = UserLeftFlag(false, 0) userLeftFlag = UserLeftFlag(false, 0)
@ -60,7 +64,7 @@ trait UserJoinedVoiceConfEvtMsgHdlr extends SystemConfiguration {
def registerUserAsGuest() = { def registerUserAsGuest() = {
if (GuestsWaiting.findWithIntId(liveMeeting.guestsWaiting, msg.body.intId) == None) { if (GuestsWaiting.findWithIntId(liveMeeting.guestsWaiting, msg.body.intId) == None) {
val guest = GuestWaiting(msg.body.intId, msg.body.callerIdName, Roles.VIEWER_ROLE, true, "", true, System.currentTimeMillis()) val guest = GuestWaiting(msg.body.intId, msg.body.callerIdName, Roles.VIEWER_ROLE, true, "", userColor, true, System.currentTimeMillis())
GuestsWaiting.add(liveMeeting.guestsWaiting, guest) GuestsWaiting.add(liveMeeting.guestsWaiting, guest)
notifyModeratorsOfGuestWaiting(guest, liveMeeting.users2x, liveMeeting.props.meetingProp.intId) notifyModeratorsOfGuestWaiting(guest, liveMeeting.users2x, liveMeeting.props.meetingProp.intId)
@ -84,6 +88,7 @@ trait UserJoinedVoiceConfEvtMsgHdlr extends SystemConfiguration {
msg.body.callingWith, msg.body.callingWith,
msg.body.callerIdName, msg.body.callerIdName,
msg.body.callerIdNum, msg.body.callerIdNum,
userColor,
msg.body.muted, msg.body.muted,
msg.body.talking, msg.body.talking,
"freeswitch" "freeswitch"

View File

@ -7,9 +7,10 @@ import org.bigbluebutton.core.bus.InternalEventBus
import org.bigbluebutton.core2.MeetingStatus2x import org.bigbluebutton.core2.MeetingStatus2x
import org.bigbluebutton.core2.message.senders.MsgBuilder import org.bigbluebutton.core2.message.senders.MsgBuilder
import org.bigbluebutton.common2.msgs._ import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.running.{ LiveMeeting, MeetingActor, OutMsgRouter } import org.bigbluebutton.core.running.{LiveMeeting, MeetingActor, OutMsgRouter}
import org.bigbluebutton.core.models._ import org.bigbluebutton.core.models._
import org.bigbluebutton.core.apps.users.UsersApp import org.bigbluebutton.core.apps.users.UsersApp
import org.bigbluebutton.core.util.ColorPicker
object VoiceApp extends SystemConfiguration { object VoiceApp extends SystemConfiguration {
@ -164,6 +165,7 @@ object VoiceApp extends SystemConfiguration {
cvu.callingWith, cvu.callingWith,
cvu.callerIdName, cvu.callerIdName,
cvu.callerIdNum, cvu.callerIdNum,
ColorPicker.nextColor(liveMeeting.props.meetingProp.intId),
cvu.muted, cvu.muted,
cvu.talking, cvu.talking,
cvu.calledInto cvu.calledInto
@ -213,6 +215,7 @@ object VoiceApp extends SystemConfiguration {
callingWith: String, callingWith: String,
callerIdName: String, callerIdName: String,
callerIdNum: String, callerIdNum: String,
color: String,
muted: Boolean, muted: Boolean,
talking: Boolean, talking: Boolean,
callingInto: String callingInto: String
@ -240,6 +243,7 @@ object VoiceApp extends SystemConfiguration {
voiceUserState.voiceUserId, voiceUserState.voiceUserId,
voiceUserState.callerName, voiceUserState.callerName,
voiceUserState.callerNum, voiceUserState.callerNum,
voiceUserState.color,
voiceUserState.muted, voiceUserState.muted,
voiceUserState.talking, voiceUserState.talking,
voiceUserState.callingWith, voiceUserState.callingWith,
@ -267,6 +271,7 @@ object VoiceApp extends SystemConfiguration {
callingWith, callingWith,
callerIdName, callerIdName,
callerIdNum, callerIdNum,
color,
muted, muted,
talking, talking,
listenOnly = isListenOnly, listenOnly = isListenOnly,

View File

@ -68,7 +68,7 @@ class GuestsWaiting {
} }
} }
case class GuestWaiting(intId: String, name: String, role: String, guest: Boolean, avatar: String, authenticated: Boolean, registeredOn: Long) case class GuestWaiting(intId: String, name: String, role: String, guest: Boolean, avatar: String, color: String, authenticated: Boolean, registeredOn: Long)
case class GuestPolicy(policy: String, setBy: String) case class GuestPolicy(policy: String, setBy: String)
object GuestPolicyType { object GuestPolicyType {

View File

@ -5,7 +5,7 @@ import org.bigbluebutton.core.domain.BreakoutRoom2x
object RegisteredUsers { object RegisteredUsers {
def create(userId: String, extId: String, name: String, roles: String, def create(userId: String, extId: String, name: String, roles: String,
token: String, avatar: String, guest: Boolean, authenticated: Boolean, token: String, avatar: String, color: String, guest: Boolean, authenticated: Boolean,
guestStatus: String, excludeFromDashboard: Boolean, loggedOut: Boolean): RegisteredUser = { guestStatus: String, excludeFromDashboard: Boolean, loggedOut: Boolean): RegisteredUser = {
new RegisteredUser( new RegisteredUser(
userId, userId,
@ -14,6 +14,7 @@ object RegisteredUsers {
roles, roles,
token, token,
avatar, avatar,
color,
guest, guest,
authenticated, authenticated,
guestStatus, guestStatus,
@ -191,6 +192,7 @@ case class RegisteredUser(
role: String, role: String,
authToken: String, authToken: String,
avatarURL: String, avatarURL: String,
color: String,
guest: Boolean, guest: Boolean,
authed: Boolean, authed: Boolean,
guestStatus: String, guestStatus: String,

View File

@ -107,6 +107,12 @@ object Users2x {
newUserState newUserState
} }
def setMobile(users: Users2x, u: UserState): UserState = {
val newUserState = modify(u)(_.mobile).setTo(true)
users.save(newUserState)
newUserState
}
def ejectFromMeeting(users: Users2x, intId: String): Option[UserState] = { def ejectFromMeeting(users: Users2x, intId: String): Option[UserState] = {
for { for {
_ <- users.remove(intId) _ <- users.remove(intId)
@ -354,12 +360,14 @@ case class UserState(
role: String, role: String,
guest: Boolean, guest: Boolean,
pin: Boolean, pin: Boolean,
mobile: Boolean,
authed: Boolean, authed: Boolean,
guestStatus: String, guestStatus: String,
emoji: String, emoji: String,
locked: Boolean, locked: Boolean,
presenter: Boolean, presenter: Boolean,
avatar: String, avatar: String,
color: String,
roleChangedOn: Long = System.currentTimeMillis(), roleChangedOn: Long = System.currentTimeMillis(),
lastActivityTime: Long = System.currentTimeMillis(), lastActivityTime: Long = System.currentTimeMillis(),
lastInactivityInspect: Long = 0, lastInactivityInspect: Long = 0,

View File

@ -174,6 +174,7 @@ case class VoiceUserState(
callingWith: String, callingWith: String,
callerName: String, callerName: String,
callerNum: String, callerNum: String,
color: String,
muted: Boolean, muted: Boolean,
talking: Boolean, talking: Boolean,
listenOnly: Boolean, listenOnly: Boolean,

View File

@ -109,6 +109,8 @@ class ReceivedJsonMsgHandlerActor(
routeGenericMsg[UserActivitySignCmdMsg](envelope, jsonNode) routeGenericMsg[UserActivitySignCmdMsg](envelope, jsonNode)
case ChangeUserPinStateReqMsg.NAME => case ChangeUserPinStateReqMsg.NAME =>
routeGenericMsg[ChangeUserPinStateReqMsg](envelope, jsonNode) routeGenericMsg[ChangeUserPinStateReqMsg](envelope, jsonNode)
case ChangeUserMobileFlagReqMsg.NAME =>
routeGenericMsg[ChangeUserMobileFlagReqMsg](envelope, jsonNode)
case SelectRandomViewerReqMsg.NAME => case SelectRandomViewerReqMsg.NAME =>
routeGenericMsg[SelectRandomViewerReqMsg](envelope, jsonNode) routeGenericMsg[SelectRandomViewerReqMsg](envelope, jsonNode)

View File

@ -63,9 +63,11 @@ trait HandlerHelpers extends SystemConfiguration {
guestStatus = regUser.guestStatus, guestStatus = regUser.guestStatus,
emoji = "none", emoji = "none",
pin = false, pin = false,
mobile = false,
presenter = false, presenter = false,
locked = MeetingStatus2x.getPermissions(liveMeeting.status).lockOnJoin, locked = MeetingStatus2x.getPermissions(liveMeeting.status).lockOnJoin,
avatar = regUser.avatarURL, avatar = regUser.avatarURL,
color = regUser.color,
clientType = clientType, clientType = clientType,
pickExempted = false, pickExempted = false,
userLeftFlag = UserLeftFlag(false, 0) userLeftFlag = UserLeftFlag(false, 0)

View File

@ -384,10 +384,11 @@ class MeetingActor(
case m: RecordAndClearPreviousMarkersCmdMsg => case m: RecordAndClearPreviousMarkersCmdMsg =>
state = usersApp.handleRecordAndClearPreviousMarkersCmdMsg(m, state) state = usersApp.handleRecordAndClearPreviousMarkersCmdMsg(m, state)
updateUserLastActivity(m.body.setBy) updateUserLastActivity(m.body.setBy)
case m: GetRecordingStatusReqMsg => usersApp.handleGetRecordingStatusReqMsg(m) case m: GetRecordingStatusReqMsg => usersApp.handleGetRecordingStatusReqMsg(m)
case m: ChangeUserEmojiCmdMsg => handleChangeUserEmojiCmdMsg(m) case m: ChangeUserEmojiCmdMsg => handleChangeUserEmojiCmdMsg(m)
case m: SelectRandomViewerReqMsg => usersApp.handleSelectRandomViewerReqMsg(m) case m: SelectRandomViewerReqMsg => usersApp.handleSelectRandomViewerReqMsg(m)
case m: ChangeUserPinStateReqMsg => usersApp.handleChangeUserPinStateReqMsg(m) case m: ChangeUserPinStateReqMsg => usersApp.handleChangeUserPinStateReqMsg(m)
case m: ChangeUserMobileFlagReqMsg => usersApp.handleChangeUserMobileFlagReqMsg(m)
// Client requested to eject user // Client requested to eject user
case m: EjectUserFromMeetingCmdMsg => case m: EjectUserFromMeetingCmdMsg =>

View File

@ -0,0 +1,19 @@
package org.bigbluebutton.core.util
object ColorPicker {
private val colors = List("#7b1fa2", "#6a1b9a", "#4a148c", "#5e35b1", "#512da8", "#4527a0", "#311b92",
"#3949ab", "#303f9f", "#283593", "#1a237e", "#1976d2", "#1565c0", "#0d47a1", "#0277bd", "#01579b")
private var meetingCurrIdx: Map[String, Int] = Map()
def nextColor(meetingId: String): String = {
val currentIdx = meetingCurrIdx.getOrElse(meetingId, 0)
val color = colors(currentIdx)
meetingCurrIdx += meetingId -> (currentIdx + 1) % colors.length
color
}
def reset(meetingId: String): Unit = {
meetingCurrIdx -= meetingId
}
}

View File

@ -13,5 +13,6 @@ object RandomStringGenerator {
// Generate a random alphabnumeric string of length n // Generate a random alphabnumeric string of length n
def randomAlphanumericString(n: Int) = def randomAlphanumericString(n: Int) =
randomString("abcdefghijklmnopqrstuvwxyz0123456789")(n) randomString("abcdefghijklmnopqrstuvwxyz0123456789")(n)
} }

View File

@ -77,6 +77,7 @@ class AnalyticsActor(val includeChat: Boolean) extends Actor with ActorLogging {
case m: UserDisconnectedFromGlobalAudioMsg => logMessage(msg) case m: UserDisconnectedFromGlobalAudioMsg => logMessage(msg)
case m: AssignPresenterReqMsg => logMessage(msg) case m: AssignPresenterReqMsg => logMessage(msg)
case m: ChangeUserPinStateReqMsg => logMessage(msg) case m: ChangeUserPinStateReqMsg => logMessage(msg)
case m: ChangeUserMobileFlagReqMsg => logMessage(msg)
case m: ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg => logMessage(msg) case m: ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg => logMessage(msg)
case m: ScreenshareRtmpBroadcastStoppedVoiceConfEvtMsg => logMessage(msg) case m: ScreenshareRtmpBroadcastStoppedVoiceConfEvtMsg => logMessage(msg)
case m: ScreenshareRtmpBroadcastStartedEvtMsg => logMessage(msg) case m: ScreenshareRtmpBroadcastStartedEvtMsg => logMessage(msg)

View File

@ -42,6 +42,7 @@ trait GuestsWaitingApprovedMsgHdlr extends HandlerHelpers with RightsManagementT
"none", "none",
dialInUser.name, dialInUser.name,
dialInUser.name, dialInUser.name,
dialInUser.color,
MeetingStatus2x.isMeetingMuted(liveMeeting.status), MeetingStatus2x.isMeetingMuted(liveMeeting.status),
false, false,
"freeswitch" "freeswitch"

View File

@ -78,7 +78,7 @@ object MsgBuilder {
val envelope = BbbCoreEnvelope(GetGuestsWaitingApprovalRespMsg.NAME, routing) val envelope = BbbCoreEnvelope(GetGuestsWaitingApprovalRespMsg.NAME, routing)
val header = BbbClientMsgHeader(GetGuestsWaitingApprovalRespMsg.NAME, meetingId, userId) val header = BbbClientMsgHeader(GetGuestsWaitingApprovalRespMsg.NAME, meetingId, userId)
val guestsWaiting = guests.map(g => GuestWaitingVO(g.intId, g.name, g.role, g.guest, g.avatar, g.authenticated, g.registeredOn)) val guestsWaiting = guests.map(g => GuestWaitingVO(g.intId, g.name, g.role, g.guest, g.avatar, g.color, g.authenticated, g.registeredOn))
val body = GetGuestsWaitingApprovalRespMsgBody(guestsWaiting) val body = GetGuestsWaitingApprovalRespMsgBody(guestsWaiting)
val event = GetGuestsWaitingApprovalRespMsg(header, body) val event = GetGuestsWaitingApprovalRespMsg(header, body)
@ -90,7 +90,7 @@ object MsgBuilder {
val envelope = BbbCoreEnvelope(GuestsWaitingForApprovalEvtMsg.NAME, routing) val envelope = BbbCoreEnvelope(GuestsWaitingForApprovalEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(GuestsWaitingForApprovalEvtMsg.NAME, meetingId, userId) val header = BbbClientMsgHeader(GuestsWaitingForApprovalEvtMsg.NAME, meetingId, userId)
val guestsWaiting = guests.map(g => GuestWaitingVO(g.intId, g.name, g.role, g.guest, g.avatar, g.authenticated, g.registeredOn)) val guestsWaiting = guests.map(g => GuestWaitingVO(g.intId, g.name, g.role, g.guest, g.avatar, g.color, g.authenticated, g.registeredOn))
val body = GuestsWaitingForApprovalEvtMsgBody(guestsWaiting) val body = GuestsWaitingForApprovalEvtMsgBody(guestsWaiting)
val event = GuestsWaitingForApprovalEvtMsg(header, body) val event = GuestsWaitingForApprovalEvtMsg(header, body)

View File

@ -13,7 +13,7 @@ object UserJoinedMeetingEvtMsgBuilder {
guestStatus = userState.guestStatus, guestStatus = userState.guestStatus,
emoji = userState.emoji, emoji = userState.emoji,
pin = userState.pin, pin = userState.pin,
presenter = userState.presenter, locked = userState.locked, avatar = userState.avatar, presenter = userState.presenter, locked = userState.locked, avatar = userState.avatar, color = userState.color,
clientType = userState.clientType) clientType = userState.clientType)
val event = UserJoinedMeetingEvtMsg(meetingId, userState.intId, body) val event = UserJoinedMeetingEvtMsg(meetingId, userState.intId, body)

View File

@ -20,13 +20,13 @@ trait FakeTestData {
val guest1 = createUserVoiceAndCam(liveMeeting, Roles.VIEWER_ROLE, guest = true, authed = true, CallingWith.WEBRTC, muted = false, val guest1 = createUserVoiceAndCam(liveMeeting, Roles.VIEWER_ROLE, guest = true, authed = true, CallingWith.WEBRTC, muted = false,
talking = false, listenOnly = false) talking = false, listenOnly = false)
Users2x.add(liveMeeting.users2x, guest1) Users2x.add(liveMeeting.users2x, guest1)
val guestWait1 = GuestWaiting(guest1.intId, guest1.name, guest1.role, guest1.guest, "", guest1.authed, System.currentTimeMillis()) val guestWait1 = GuestWaiting(guest1.intId, guest1.name, guest1.role, guest1.guest, "", "#ff6242", guest1.authed, System.currentTimeMillis())
GuestsWaiting.add(liveMeeting.guestsWaiting, guestWait1) GuestsWaiting.add(liveMeeting.guestsWaiting, guestWait1)
val guest2 = createUserVoiceAndCam(liveMeeting, Roles.VIEWER_ROLE, guest = true, authed = true, CallingWith.FLASH, muted = false, val guest2 = createUserVoiceAndCam(liveMeeting, Roles.VIEWER_ROLE, guest = true, authed = true, CallingWith.FLASH, muted = false,
talking = false, listenOnly = false) talking = false, listenOnly = false)
Users2x.add(liveMeeting.users2x, guest2) Users2x.add(liveMeeting.users2x, guest2)
val guestWait2 = GuestWaiting(guest2.intId, guest2.name, guest2.role, guest2.guest, "", guest2.authed, System.currentTimeMillis()) val guestWait2 = GuestWaiting(guest2.intId, guest2.name, guest2.role, guest2.guest, "", "#ff6242", guest2.authed, System.currentTimeMillis())
GuestsWaiting.add(liveMeeting.guestsWaiting, guestWait2) GuestsWaiting.add(liveMeeting.guestsWaiting, guestWait2)
val vu1 = FakeUserGenerator.createFakeVoiceOnlyUser(CallingWith.PHONE, muted = false, talking = false, listenOnly = false) val vu1 = FakeUserGenerator.createFakeVoiceOnlyUser(CallingWith.PHONE, muted = false, talking = false, listenOnly = false)
@ -67,8 +67,8 @@ trait FakeTestData {
def createFakeUser(liveMeeting: LiveMeeting, regUser: RegisteredUser): UserState = { def createFakeUser(liveMeeting: LiveMeeting, regUser: RegisteredUser): UserState = {
UserState(intId = regUser.id, extId = regUser.externId, name = regUser.name, role = regUser.role, pin = false, UserState(intId = regUser.id, extId = regUser.externId, name = regUser.name, role = regUser.role, pin = false,
guest = regUser.guest, authed = regUser.authed, guestStatus = regUser.guestStatus, mobile = false, guest = regUser.guest, authed = regUser.authed, guestStatus = regUser.guestStatus,
emoji = "none", locked = false, presenter = false, avatar = regUser.avatarURL, clientType = "unknown", emoji = "none", locked = false, presenter = false, avatar = regUser.avatarURL, color = "#ff6242", clientType = "unknown",
pickExempted = false, userLeftFlag = UserLeftFlag(false, 0)) pickExempted = false, userLeftFlag = UserLeftFlag(false, 0))
} }

View File

@ -52,9 +52,10 @@ object FakeUserGenerator {
val authToken = RandomStringGenerator.randomAlphanumericString(16) val authToken = RandomStringGenerator.randomAlphanumericString(16)
val avatarURL = "https://www." + RandomStringGenerator.randomAlphanumericString(32) + ".com/" + val avatarURL = "https://www." + RandomStringGenerator.randomAlphanumericString(32) + ".com/" +
RandomStringGenerator.randomAlphanumericString(10) + ".png" RandomStringGenerator.randomAlphanumericString(10) + ".png"
val color = "#ff6242"
val ru = RegisteredUsers.create(userId = id, extId, name, role, val ru = RegisteredUsers.create(userId = id, extId, name, role,
authToken, avatarURL, guest, authed, guestStatus = GuestStatus.ALLOW, false, false) authToken, avatarURL, color, guest, authed, guestStatus = GuestStatus.ALLOW, false, false)
RegisteredUsers.add(users, ru) RegisteredUsers.add(users, ru)
ru ru
} }
@ -64,7 +65,7 @@ object FakeUserGenerator {
val voiceUserId = RandomStringGenerator.randomAlphanumericString(8) val voiceUserId = RandomStringGenerator.randomAlphanumericString(8)
val lastFloorTime = System.currentTimeMillis().toString(); val lastFloorTime = System.currentTimeMillis().toString();
VoiceUserState(intId = user.id, voiceUserId = voiceUserId, callingWith, callerName = user.name, VoiceUserState(intId = user.id, voiceUserId = voiceUserId, callingWith, callerName = user.name,
callerNum = user.name, muted, talking, listenOnly, "freeswitch", System.currentTimeMillis(), floor, lastFloorTime) callerNum = user.name, "#ff6242", muted, talking, listenOnly, "freeswitch", System.currentTimeMillis(), floor, lastFloorTime)
} }
def createFakeVoiceOnlyUser(callingWith: String, muted: Boolean, talking: Boolean, def createFakeVoiceOnlyUser(callingWith: String, muted: Boolean, talking: Boolean,
@ -74,7 +75,7 @@ object FakeUserGenerator {
val name = getRandomElement(firstNames, random) + " " + getRandomElement(lastNames, random) val name = getRandomElement(firstNames, random) + " " + getRandomElement(lastNames, random)
val lastFloorTime = System.currentTimeMillis().toString(); val lastFloorTime = System.currentTimeMillis().toString();
VoiceUserState(intId, voiceUserId = voiceUserId, callingWith, callerName = name, VoiceUserState(intId, voiceUserId = voiceUserId, callingWith, callerName = name,
callerNum = name, muted, talking, listenOnly, "freeswitch", System.currentTimeMillis(), floor, lastFloorTime) callerNum = name, "#ff6242", muted, talking, listenOnly, "freeswitch", System.currentTimeMillis(), floor, lastFloorTime)
} }
def createFakeWebcamStreamFor(userId: String, subscribers: Set[String]): WebcamStream = { def createFakeWebcamStreamFor(userId: String, subscribers: Set[String]): WebcamStream = {

View File

@ -24,7 +24,7 @@ object TestDataGen {
listenOnly: Boolean): VoiceUserState = { listenOnly: Boolean): VoiceUserState = {
val voiceUserId = RandomStringGenerator.randomAlphanumericString(8) val voiceUserId = RandomStringGenerator.randomAlphanumericString(8)
VoiceUserState(intId = user.id, voiceUserId = voiceUserId, callingWith, callerName = user.name, VoiceUserState(intId = user.id, voiceUserId = voiceUserId, callingWith, callerName = user.name,
callerNum = user.name, muted, talking, listenOnly) callerNum = user.name, "#ff6242", muted, talking, listenOnly)
} }
def createFakeVoiceOnlyUser(callingWith: String, muted: Boolean, talking: Boolean, def createFakeVoiceOnlyUser(callingWith: String, muted: Boolean, talking: Boolean,
@ -32,7 +32,7 @@ object TestDataGen {
val voiceUserId = RandomStringGenerator.randomAlphanumericString(8) val voiceUserId = RandomStringGenerator.randomAlphanumericString(8)
val intId = "v_" + RandomStringGenerator.randomAlphanumericString(16) val intId = "v_" + RandomStringGenerator.randomAlphanumericString(16)
VoiceUserState(intId, voiceUserId = voiceUserId, callingWith, callerName = name, VoiceUserState(intId, voiceUserId = voiceUserId, callingWith, callerName = name,
callerNum = name, muted, talking, listenOnly) callerNum = name, "#ff6242", muted, talking, listenOnly)
} }
def createFakeWebcamStreamFor(userId: String, subscribers: Set[String]): WebcamStream = { def createFakeWebcamStreamFor(userId: String, subscribers: Set[String]): WebcamStream = {
@ -43,8 +43,8 @@ object TestDataGen {
def createUserFor(liveMeeting: LiveMeeting, regUser: RegisteredUser, presenter: Boolean): UserState = { def createUserFor(liveMeeting: LiveMeeting, regUser: RegisteredUser, presenter: Boolean): UserState = {
val u = UserState(intId = regUser.id, extId = regUser.externId, name = regUser.name, role = regUser.role, val u = UserState(intId = regUser.id, extId = regUser.externId, name = regUser.name, role = regUser.role,
guest = regUser.guest, authed = regUser.authed, guestStatus = regUser.guestStatus, guest = regUser.guest, authed = regUser.authed, guestStatus = regUser.guestStatus,
emoji = "none", locked = false, presenter = false, avatar = regUser.avatarURL, clientType = "unknown", emoji = "none", locked = false, presenter = false, avatar = regUser.avatarURL, color = "#ff6242",
userLeftFlag = UserLeftFlag(false, 0)) clientType = "unknown", userLeftFlag = UserLeftFlag(false, 0))
Users2x.add(liveMeeting.users2x, u) Users2x.add(liveMeeting.users2x, u)
u u
} }

View File

@ -19,7 +19,7 @@ case class GetGuestsWaitingApprovalRespMsg(
body: GetGuestsWaitingApprovalRespMsgBody body: GetGuestsWaitingApprovalRespMsgBody
) extends BbbCoreMsg ) extends BbbCoreMsg
case class GetGuestsWaitingApprovalRespMsgBody(guests: Vector[GuestWaitingVO]) case class GetGuestsWaitingApprovalRespMsgBody(guests: Vector[GuestWaitingVO])
case class GuestWaitingVO(intId: String, name: String, role: String, guest: Boolean, avatar: String, authenticated: Boolean, registeredOn: Long) case class GuestWaitingVO(intId: String, name: String, role: String, guest: Boolean, avatar: String, color: String, authenticated: Boolean, registeredOn: Long)
/** /**
* Message sent to client for list of guest waiting for approval. This is sent when * Message sent to client for list of guest waiting for approval. This is sent when

View File

@ -88,11 +88,22 @@ case class UserJoinedMeetingEvtMsg(
header: BbbClientMsgHeader, header: BbbClientMsgHeader,
body: UserJoinedMeetingEvtMsgBody body: UserJoinedMeetingEvtMsgBody
) extends BbbCoreMsg ) extends BbbCoreMsg
case class UserJoinedMeetingEvtMsgBody(intId: String, extId: String, name: String, role: String, case class UserJoinedMeetingEvtMsgBody(
guest: Boolean, authed: Boolean, guestStatus: String, intId: String,
emoji: String, extId: String,
pin: Boolean, name: String,
presenter: Boolean, locked: Boolean, avatar: String, clientType: String) role: String,
guest: Boolean,
authed: Boolean,
guestStatus: String,
emoji: String,
pin: Boolean,
presenter: Boolean,
locked: Boolean,
avatar: String,
color: String,
clientType: String
)
/** /**
* Sent by client to get all users in a meeting. * Sent by client to get all users in a meeting.
@ -189,6 +200,20 @@ object UserEmojiChangedEvtMsg { val NAME = "UserEmojiChangedEvtMsg" }
case class UserEmojiChangedEvtMsg(header: BbbClientMsgHeader, body: UserEmojiChangedEvtMsgBody) extends BbbCoreMsg case class UserEmojiChangedEvtMsg(header: BbbClientMsgHeader, body: UserEmojiChangedEvtMsgBody) extends BbbCoreMsg
case class UserEmojiChangedEvtMsgBody(userId: String, emoji: String) case class UserEmojiChangedEvtMsgBody(userId: String, emoji: String)
/**
* Sent from client about a user mobile flag.
*/
object ChangeUserMobileFlagReqMsg { val NAME = "ChangeUserMobileFlagReqMsg" }
case class ChangeUserMobileFlagReqMsg(header: BbbClientMsgHeader, body: ChangeUserMobileFlagReqMsgBody) extends StandardMsg
case class ChangeUserMobileFlagReqMsgBody(userId: String, mobile: Boolean)
/**
* Sent to all clients about a user mobile flag.
*/
object UserMobileFlagChangedEvtMsg { val NAME = "UserMobileFlagChangedEvtMsg" }
case class UserMobileFlagChangedEvtMsg(header: BbbClientMsgHeader, body: UserMobileFlagChangedEvtMsgBody) extends BbbCoreMsg
case class UserMobileFlagChangedEvtMsgBody(userId: String, mobile: Boolean)
object AssignPresenterReqMsg { val NAME = "AssignPresenterReqMsg" } object AssignPresenterReqMsg { val NAME = "AssignPresenterReqMsg" }
case class AssignPresenterReqMsg(header: BbbClientMsgHeader, body: AssignPresenterReqMsgBody) extends StandardMsg case class AssignPresenterReqMsg(header: BbbClientMsgHeader, body: AssignPresenterReqMsgBody) extends StandardMsg
case class AssignPresenterReqMsgBody(requesterId: String, newPresenterId: String, newPresenterName: String, assignedBy: String) case class AssignPresenterReqMsgBody(requesterId: String, newPresenterId: String, newPresenterName: String, assignedBy: String)
@ -348,7 +373,7 @@ object GetVoiceUsersMeetingRespMsg {
case class GetVoiceUsersMeetingRespMsg(header: BbbClientMsgHeader, body: GetVoiceUsersMeetingRespMsgBody) extends BbbCoreMsg case class GetVoiceUsersMeetingRespMsg(header: BbbClientMsgHeader, body: GetVoiceUsersMeetingRespMsgBody) extends BbbCoreMsg
case class GetVoiceUsersMeetingRespMsgBody(users: Vector[VoiceConfUser]) case class GetVoiceUsersMeetingRespMsgBody(users: Vector[VoiceConfUser])
case class VoiceConfUser(intId: String, voiceUserId: String, callingWith: String, callerName: String, case class VoiceConfUser(intId: String, voiceUserId: String, callingWith: String, callerName: String,
callerNum: String, muted: Boolean, talking: Boolean, listenOnly: Boolean) callerNum: String, color: String, muted: Boolean, talking: Boolean, listenOnly: Boolean)
/** /**
* Sent from client to add user to the presenter group of a meeting. * Sent from client to add user to the presenter group of a meeting.

View File

@ -408,7 +408,7 @@ case class UserJoinedVoiceConfEvtMsgBody(voiceConf: String, voiceUserId: String,
object UserJoinedVoiceConfToClientEvtMsg { val NAME = "UserJoinedVoiceConfToClientEvtMsg" } object UserJoinedVoiceConfToClientEvtMsg { val NAME = "UserJoinedVoiceConfToClientEvtMsg" }
case class UserJoinedVoiceConfToClientEvtMsg(header: BbbClientMsgHeader, body: UserJoinedVoiceConfToClientEvtMsgBody) extends BbbCoreMsg case class UserJoinedVoiceConfToClientEvtMsg(header: BbbClientMsgHeader, body: UserJoinedVoiceConfToClientEvtMsgBody) extends BbbCoreMsg
case class UserJoinedVoiceConfToClientEvtMsgBody(voiceConf: String, intId: String, voiceUserId: String, callerName: String, case class UserJoinedVoiceConfToClientEvtMsgBody(voiceConf: String, intId: String, voiceUserId: String, callerName: String,
callerNum: String, muted: Boolean, callerNum: String, color: String, muted: Boolean,
talking: Boolean, callingWith: String, listenOnly: Boolean) talking: Boolean, callingWith: String, listenOnly: Boolean)
/** /**

View File

@ -1,6 +1,6 @@
#!/bin/sh #!/bin/sh
set -ex set -ex
RELEASE=4.0.1 RELEASE=4.0.2
cat <<MSG cat <<MSG
This tool downloads prebuilt packages built on Github Actions This tool downloads prebuilt packages built on Github Actions
The corresponding source can be browsed at https://github.com/bigbluebutton/bbb-presentation-video/tree/${RELEASE} The corresponding source can be browsed at https://github.com/bigbluebutton/bbb-presentation-video/tree/${RELEASE}

View File

@ -0,0 +1 @@
git clone --branch v0.2.0 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-recorder bbb-webrtc-recorder

View File

@ -1 +1 @@
git clone --branch v2.10.0-alpha.1 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu git clone --branch v2.10.0-alpha.2 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu

View File

@ -44,9 +44,10 @@ fi
HOST=$(cat $SERVLET_DIR/WEB-INF/classes/bigbluebutton.properties $tmpfile $BBB_WEB_ETC_CONFIG | grep -v '#' | sed -n '/^bigbluebutton.web.serverURL/{s/.*\///;p}' | tail -n 1) HOST=$(cat $SERVLET_DIR/WEB-INF/classes/bigbluebutton.properties $tmpfile $BBB_WEB_ETC_CONFIG | grep -v '#' | sed -n '/^bigbluebutton.web.serverURL/{s/.*\///;p}' | tail -n 1)
HTML5_CONFIG=/usr/share/meteor/bundle/programs/server/assets/app/config/settings.yml HTML5_CONFIG=/etc/bigbluebutton/bbb-html5.yml
BBB_WEB_CONFIG=$SERVLET_DIR/WEB-INF/classes/bigbluebutton.properties if [ ! -f "${HTML5_CONFIG}" ]; then
touch $HTML5_CONFIG
fi
# #
# Enable Looging of the HTML5 client for debugging # Enable Looging of the HTML5 client for debugging

View File

@ -154,6 +154,14 @@ get_bbb_web_config_value() {
RECORD_CONFIG=/usr/local/bigbluebutton/core/scripts/bigbluebutton.yml RECORD_CONFIG=/usr/local/bigbluebutton/core/scripts/bigbluebutton.yml
WEBRTC_RECORDER_DEFAULT_CONFIG=/etc/bbb-webrtc-recorder/bbb-webrtc-recorder.yml
WEBRTC_RECORDER_ETC_CONFIG=/etc/bigbluebutton/bbb-webrtc-recorder.yml
if [ -f $WEBRTC_RECORDER_ETC_CONFIG ]; then
WEBRTC_RECORDER_CONFIG=$(yq m -x $WEBRTC_RECORDER_DEFAULT_CONFIG $WEBRTC_RECORDER_ETC_CONFIG)
else
WEBRTC_RECORDER_CONFIG=$(yq r $WEBRTC_RECORDER_DEFAULT_CONFIG)
fi
HTML5_DEFAULT_CONFIG=/usr/share/meteor/bundle/programs/server/assets/app/config/settings.yml HTML5_DEFAULT_CONFIG=/usr/share/meteor/bundle/programs/server/assets/app/config/settings.yml
HTML5_ETC_CONFIG=/etc/bigbluebutton/bbb-html5.yml HTML5_ETC_CONFIG=/etc/bigbluebutton/bbb-html5.yml
if [ -f $HTML5_ETC_CONFIG ]; then if [ -f $HTML5_ETC_CONFIG ]; then
@ -407,16 +415,7 @@ display_bigbluebutton_status () {
fi fi
if [ -f /usr/lib/systemd/system/bbb-html5.service ]; then if [ -f /usr/lib/systemd/system/bbb-html5.service ]; then
units="$units mongod bbb-html5 bbb-webrtc-sfu kurento-media-server" units="$units mongod bbb-html5"
for i in `seq 8888 8890`; do
# check if multi-kurento setup is configured
if [ -f /usr/lib/systemd/system/kurento-media-server-${i}.service ]; then
if systemctl is-enabled kurento-media-server-${i}.service > /dev/null; then
units="$units kurento-media-server-${i}"
fi
fi
done
source /usr/share/meteor/bundle/bbb-html5-with-roles.conf source /usr/share/meteor/bundle/bbb-html5-with-roles.conf
@ -433,6 +432,27 @@ display_bigbluebutton_status () {
done done
fi fi
if [ -f /usr/lib/systemd/system/bbb-webrtc-sfu.service ]; then
units="$units bbb-webrtc-sfu"
fi
if [ -f /usr/lib/systemd/system/bbb-webrtc-recorder.service ]; then
units="$units bbb-webrtc-recorder"
fi
if [ -f /usr/lib/systemd/system/kurento-media-server.service ]; then
units="$units kurento-media-server"
fi
for i in `seq 8888 8890`; do
# check if multi-kurento setup is configured
if [ -f /usr/lib/systemd/system/kurento-media-server-${i}.service ]; then
if systemctl is-enabled kurento-media-server-${i}.service > /dev/null; then
units="$units kurento-media-server-${i}"
fi
fi
done
if [ -f /usr/share/etherpad-lite/settings.json ]; then if [ -f /usr/share/etherpad-lite/settings.json ]; then
units="$units etherpad" units="$units etherpad"
fi fi
@ -706,6 +726,9 @@ if [[ $PORT_RANGE ]]; then
yq w -i $WEBRTC_SFU_ETC_CONFIG mediasoup.worker.rtcMinPort $START_PORT yq w -i $WEBRTC_SFU_ETC_CONFIG mediasoup.worker.rtcMinPort $START_PORT
yq w -i $WEBRTC_SFU_ETC_CONFIG mediasoup.worker.rtcMaxPort $END_PORT yq w -i $WEBRTC_SFU_ETC_CONFIG mediasoup.worker.rtcMaxPort $END_PORT
yq w -i $WEBRTC_RECORDER_DEFAULT_CONFIG webrtc.rtcMinPort $START_PORT
yq w -i $WEBRTC_RECORDER_DEFAULT_CONFIG webrtc.rtcMaxPort $END_PORT
echo echo
echo "BigBlueButton's UDP port range is now $START_PORT-$END_PORT" echo "BigBlueButton's UDP port range is now $START_PORT-$END_PORT"
echo echo
@ -744,14 +767,18 @@ check_configuration() {
# Look for properties with no values set # Look for properties with no values set
# #
CONFIG_FILES="$SERVLET_DIR/WEB-INF/classes/bigbluebutton.properties" CONFIG_FILES="$SERVLET_DIR/WEB-INF/classes/bigbluebutton.properties"
ignore_configs_args=()
ignore_configs_args+=(-e "redis.pass")
ignore_configs_args+=(-e "redisPassword")
ignore_configs_args+=(-e "disabledFeatures")
for file in $CONFIG_FILES ; do for file in $CONFIG_FILES ; do
if [ ! -f $file ]; then if [ ! -f $file ]; then
echo "# Error: File not found: $file" echo "# Error: File not found: $file"
else else
if cat $file | grep -v redis.pass | grep -v redisPassword | grep -v ^# | grep -q "^[^=]*=[ ]*$"; then if cat $file | grep -v "${ignore_configs_args[@]}" | grep -v ^# | grep -q "^[^=]*=[ ]*$"; then
echo "# The following properties in $file have no value:" echo "# The following properties in $file have no value:"
echo "# $(grep '^[^=#]*=[ ]*$' $file | grep -v redis.pass | grep -v redisPassword | sed 's/=//g')" echo "# $(grep '^[^=#]*=[ ]*$' $file | grep -v "${ignore_configs_args[@]}" | sed 's/=//g')"
fi fi
fi fi
done done
@ -1393,6 +1420,8 @@ if [ $CHECK ]; then
echo " kurento: $(awk -F '=' '{if (! ($0 ~ /^;/) && $0 ~ /minPort/) print $2}' /etc/kurento/modules/kurento/BaseRtpEndpoint.conf.ini)-$(awk -F '=' '{if (! ($0 ~ /^;/) && $0 ~ /maxPort/) print $2}' /etc/kurento/modules/kurento/BaseRtpEndpoint.conf.ini)" echo " kurento: $(awk -F '=' '{if (! ($0 ~ /^;/) && $0 ~ /minPort/) print $2}' /etc/kurento/modules/kurento/BaseRtpEndpoint.conf.ini)-$(awk -F '=' '{if (! ($0 ~ /^;/) && $0 ~ /maxPort/) print $2}' /etc/kurento/modules/kurento/BaseRtpEndpoint.conf.ini)"
echo " bbb-webrtc-sfu: $(echo "$WEBRTC_SFU_CONFIG" | yq r - mediasoup.worker.rtcMinPort)-$(echo "$WEBRTC_SFU_CONFIG" | yq r - mediasoup.worker.rtcMaxPort)" echo " bbb-webrtc-sfu: $(echo "$WEBRTC_SFU_CONFIG" | yq r - mediasoup.worker.rtcMinPort)-$(echo "$WEBRTC_SFU_CONFIG" | yq r - mediasoup.worker.rtcMaxPort)"
echo " bbb-webrtc-recorder: $(echo "$WEBRTC_RECORDER_CONFIG" | yq r - webrtc.rtcMinPort)-$(echo "$WEBRTC_RECORDER_CONFIG" | yq r - webrtc.rtcMaxPort)"
# if [ -f ${LTI_DIR}/WEB-INF/classes/lti-config.properties ]; then # if [ -f ${LTI_DIR}/WEB-INF/classes/lti-config.properties ]; then
@ -1427,10 +1456,20 @@ if [ $CHECK ]; then
echo " kurento.ip: $(echo "$WEBRTC_SFU_CONFIG" | yq r - kurento[0].ip)" echo " kurento.ip: $(echo "$WEBRTC_SFU_CONFIG" | yq r - kurento[0].ip)"
echo " kurento.url: $(echo "$WEBRTC_SFU_CONFIG" | yq r - kurento[0].url)" echo " kurento.url: $(echo "$WEBRTC_SFU_CONFIG" | yq r - kurento[0].url)"
echo " freeswitch.sip_ip: $(echo "$WEBRTC_SFU_CONFIG" | yq r - freeswitch.sip_ip)" echo " freeswitch.sip_ip: $(echo "$WEBRTC_SFU_CONFIG" | yq r - freeswitch.sip_ip)"
echo " recordingAdapter: $(echo "$WEBRTC_SFU_CONFIG" | yq r - recordingAdapter)"
echo " recordScreenSharing: $(echo "$WEBRTC_SFU_CONFIG" | yq r - recordScreenSharing)" echo " recordScreenSharing: $(echo "$WEBRTC_SFU_CONFIG" | yq r - recordScreenSharing)"
echo " recordWebcams: $(echo "$WEBRTC_SFU_CONFIG" | yq r - recordWebcams)" echo " recordWebcams: $(echo "$WEBRTC_SFU_CONFIG" | yq r - recordWebcams)"
echo " codec_video_main: $(echo "$WEBRTC_SFU_CONFIG" | yq r - conference-media-specs.codec_video_main)" echo " codec_video_main: $(echo "$WEBRTC_SFU_CONFIG" | yq r - conference-media-specs.codec_video_main)"
echo " codec_video_content: $(echo "$WEBRTC_SFU_CONFIG" | yq r - conference-media-specs.codec_video_content)" echo " codec_video_content: $(echo "$WEBRTC_SFU_CONFIG" | yq r - conference-media-specs.codec_video_content)"
fi
if [ -n "$WEBRTC_RECORDER_CONFIG" ]; then
echo
echo "/etc/bbb-webrtc-recorder/bbb-webrtc-recorder.yml (bbb-webrtc-recorder)"
echo "/etc/bigbluebutton/bbb-webrtc-recorder.yml (bbb-webrtc-recorder - override)"
echo " debug: $(echo "$WEBRTC_RECORDER_CONFIG" | yq r - debug)"
echo " recorder.directory: $(echo "$WEBRTC_RECORDER_CONFIG" | yq r - recorder.directory)"
fi fi
if [ -n "$HTML5_CONFIG" ]; then if [ -n "$HTML5_CONFIG" ]; then

View File

@ -108,6 +108,14 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
background-color: rgba(66, 133, 244, 1) !important; background-color: rgba(66, 133, 244, 1) !important;
color: #FFF !important; color: #FFF !important;
} }
.fade-in {
opacity: 1 !important;
}
.fade-out {
opacity: 0 !important;
}
</style> </style>
<script> <script>
document.addEventListener('gesturestart', function (e) { document.addEventListener('gesturestart', function (e) {
@ -149,4 +157,5 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<span id="destination"></span> <span id="destination"></span>
<audio id="remote-media" autoplay> <audio id="remote-media" autoplay>
</audio> </audio>
<div id="modals-container"></div>
</body> </body>

View File

@ -348,3 +348,9 @@
.icon-bbb-closed_caption_stop:before { .icon-bbb-closed_caption_stop:before {
content: "\e966"; content: "\e966";
} }
.icon-bbb-link:before {
content: "\e967";
}
.icon-bbb-manage_layout:before {
content: "\e968";
}

View File

@ -38,3 +38,39 @@
border: none; border: none;
background-color: transparent; background-color: transparent;
} }
/* Prevents that an element within app shows over a modal */
#app {
position: relative;
z-index: 1000 !important;
}
.modal-low {
z-index: 1001;
}
.modal-medium {
z-index: 1002;
}
.modal-high {
z-index: 1003;
}
/* Within a same priority, hide all but first (FIFO) */
.modal-low ~ .modal-low,
.modal-medium ~ .modal-medium,
.modal-high ~ .modal-high {
display: none;
}
/* Hide all low priority modals when a medium or high priority modals are displayed */
#modals-container:has(.modal-medium) .modal-low,
#modals-container:has(.modal-high) .modal-low {
display: none;
}
/* Hide all medium priority modals when a high priority modal is displayed */
#modals-container:has(.modal-high) .modal-medium {
display: none;
}

View File

@ -758,9 +758,18 @@ class SIPSession {
const setupRemoteMedia = () => { const setupRemoteMedia = () => {
const mediaElement = document.querySelector(MEDIA_TAG); const mediaElement = document.querySelector(MEDIA_TAG);
const { sdp } = this.currentSession.sessionDescriptionHandler
.peerConnection.remoteDescription;
logger.info({
logCode: 'sip_js_session_setup_remote_media',
extraInfo: {
callerIdName: this.user.callerIdName,
sdp,
},
}, 'Audio call - setup remote media');
this.remoteStream = new MediaStream(); this.remoteStream = new MediaStream();
this.currentSession.sessionDescriptionHandler this.currentSession.sessionDescriptionHandler
.peerConnection.getReceivers().forEach((receiver) => { .peerConnection.getReceivers().forEach((receiver) => {
if (receiver.track) { if (receiver.track) {
@ -792,22 +801,15 @@ class SIPSession {
fsReady, fsReady,
}, },
}, 'Audio call - check if ICE is finished and FreeSWITCH is ready'); }, 'Audio call - check if ICE is finished and FreeSWITCH is ready');
if (iceCompleted && fsReady) {
if (iceCompleted) {
this.webrtcConnected = true; this.webrtcConnected = true;
setupRemoteMedia(); setupRemoteMedia();
}
const { sdp } = this.currentSession.sessionDescriptionHandler if (fsReady) {
.peerConnection.remoteDescription;
logger.info({
logCode: 'sip_js_session_setup_remote_media',
extraInfo: {
callerIdName: this.user.callerIdName,
sdp,
},
}, 'Audio call - setup remote media');
this.callback({ status: this.baseCallStates.started, bridge: this.bridgeName }); this.callback({ status: this.baseCallStates.started, bridge: this.bridgeName });
resolve(); resolve();
} }
}; };

View File

@ -1,15 +1,8 @@
import stringHash from 'string-hash';
import { check } from 'meteor/check'; import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger'; import Logger from '/imports/startup/server/logger';
import GuestUsers from '/imports/api/guest-users/'; import GuestUsers from '/imports/api/guest-users/';
import updatePositionInWaitingQueue from '../methods/updatePositionInWaitingQueue'; import updatePositionInWaitingQueue from '../methods/updatePositionInWaitingQueue';
const COLOR_LIST = [
'#7b1fa2', '#6a1b9a', '#4a148c', '#5e35b1', '#512da8', '#4527a0',
'#311b92', '#3949ab', '#303f9f', '#283593', '#1a237e', '#1976d2', '#1565c0',
'#0d47a1', '#0277bd', '#01579b',
];
export default async function handleGuestsWaitingForApproval({ body }, meetingId) { export default async function handleGuestsWaitingForApproval({ body }, meetingId) {
const { guests } = body; const { guests } = body;
check(guests, Array); check(guests, Array);
@ -27,7 +20,6 @@ export default async function handleGuestsWaitingForApproval({ body }, meetingId
meetingId, meetingId,
loginTime: guest.registeredOn, loginTime: guest.registeredOn,
privateGuestLobbyMessage: '', privateGuestLobbyMessage: '',
color: COLOR_LIST[stringHash(guest.intId) % COLOR_LIST.length],
}); });
if (insertedId) { if (insertedId) {

View File

@ -42,7 +42,7 @@ export default async function changeLayout(payload) {
pushLayout: Boolean, pushLayout: Boolean,
presentationIsOpen: Boolean, presentationIsOpen: Boolean,
isResizing: Boolean, isResizing: Boolean,
cameraPosition: String, cameraPosition: Match.Maybe(String),
focusedCamera: String, focusedCamera: String,
presentationVideoRate: Number, presentationVideoRate: Number,
}); });

View File

@ -15,7 +15,7 @@ export default function setPushLayout(payload) {
check(requesterUserId, String); check(requesterUserId, String);
check(payload, { check(payload, {
pushLayout: Boolean, pushLayout: Match.Maybe(Boolean),
}); });
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);

View File

@ -8,6 +8,7 @@ import handleEmojiStatus from './handlers/emojiStatus';
import handleChangeRole from './handlers/changeRole'; import handleChangeRole from './handlers/changeRole';
import handleUserPinChanged from './handlers/userPinChanged'; import handleUserPinChanged from './handlers/userPinChanged';
import handleUserInactivityInspect from './handlers/userInactivityInspect'; import handleUserInactivityInspect from './handlers/userInactivityInspect';
import handleChangeMobileFlag from '/imports/api/users/server/handlers/changeMobileFlag';
RedisPubSub.on('PresenterAssignedEvtMsg', handlePresenterAssigned); RedisPubSub.on('PresenterAssignedEvtMsg', handlePresenterAssigned);
RedisPubSub.on('UserJoinedMeetingEvtMsg', handleUserJoined); RedisPubSub.on('UserJoinedMeetingEvtMsg', handleUserJoined);
@ -15,6 +16,7 @@ RedisPubSub.on('UserLeftMeetingEvtMsg', handleRemoveUser);
RedisPubSub.on('ValidateAuthTokenRespMsg', handleValidateAuthToken); RedisPubSub.on('ValidateAuthTokenRespMsg', handleValidateAuthToken);
RedisPubSub.on('UserEmojiChangedEvtMsg', handleEmojiStatus); RedisPubSub.on('UserEmojiChangedEvtMsg', handleEmojiStatus);
RedisPubSub.on('UserRoleChangedEvtMsg', handleChangeRole); RedisPubSub.on('UserRoleChangedEvtMsg', handleChangeRole);
RedisPubSub.on('UserMobileFlagChangedEvtMsg', handleChangeMobileFlag);
RedisPubSub.on('UserLeftFlagUpdatedEvtMsg', handleUserLeftFlagUpdated); RedisPubSub.on('UserLeftFlagUpdatedEvtMsg', handleUserLeftFlagUpdated);
RedisPubSub.on('UserPinStateChangedEvtMsg', handleUserPinChanged); RedisPubSub.on('UserPinStateChangedEvtMsg', handleUserPinChanged);
RedisPubSub.on('UserInactivityInspectMsg', handleUserInactivityInspect); RedisPubSub.on('UserInactivityInspectMsg', handleUserInactivityInspect);

View File

@ -0,0 +1,13 @@
import { check } from 'meteor/check';
import setMobile from '/imports/api/users/server/modifiers/setMobile';
export default async function handleChangeMobileFlag(payload, meetingId) {
check(payload.body, Object);
check(meetingId, String);
const { userId: requesterUserId, mobile } = payload.body;
if (mobile) {
await setMobile(meetingId, requesterUserId);
}
}

View File

@ -1,18 +1,27 @@
import { check } from 'meteor/check'; import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger'; import Logger from '/imports/startup/server/logger';
import setMobile from '../modifiers/setMobile';
import { extractCredentials } from '/imports/api/common/server/helpers'; import { extractCredentials } from '/imports/api/common/server/helpers';
import RedisPubSub from '/imports/startup/server/redis';
export default async function setMobileUser() { export default async function setMobileUser() {
try { try {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'ChangeUserMobileFlagReqMsg';
const { meetingId, requesterUserId } = extractCredentials(this.userId); const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String); check(meetingId, String);
check(requesterUserId, String); check(requesterUserId, String);
const payload = {
userId: requesterUserId,
mobile: true,
};
Logger.verbose(`Mobile user ${requesterUserId} from meeting ${meetingId}`); Logger.verbose(`Mobile user ${requesterUserId} from meeting ${meetingId}`);
await setMobile(meetingId, requesterUserId); RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
} catch (err) { } catch (err) {
Logger.error(`Exception while invoking method setMobileUser ${err.stack}`); Logger.error(`Exception while invoking method setMobileUser ${err.stack}`);
} }

View File

@ -8,7 +8,7 @@ export default async function addDialInUser(meetingId, voiceUser) {
const USER_CONFIG = Meteor.settings.public.user; const USER_CONFIG = Meteor.settings.public.user;
const ROLE_VIEWER = USER_CONFIG.role_viewer; const ROLE_VIEWER = USER_CONFIG.role_viewer;
const { intId, callerName } = voiceUser; const { intId, callerName, color } = voiceUser;
const voiceOnlyUser = { const voiceOnlyUser = {
intId, intId,
@ -23,6 +23,7 @@ export default async function addDialInUser(meetingId, voiceUser) {
presenter: false, presenter: false,
locked: false, // TODO locked: false, // TODO
avatar: '', avatar: '',
color,
pin: false, pin: false,
clientType: 'dial-in-user', clientType: 'dial-in-user',
}; };

View File

@ -4,18 +4,11 @@ import Users from '/imports/api/users';
import Meetings from '/imports/api/meetings'; import Meetings from '/imports/api/meetings';
import VoiceUsers from '/imports/api/voice-users/'; import VoiceUsers from '/imports/api/voice-users/';
import addUserPsersistentData from '/imports/api/users-persistent-data/server/modifiers/addUserPersistentData'; import addUserPsersistentData from '/imports/api/users-persistent-data/server/modifiers/addUserPersistentData';
import stringHash from 'string-hash';
import flat from 'flat'; import flat from 'flat';
import { lowercaseTrim } from '/imports/utils/string-utils'; import { lowercaseTrim } from '/imports/utils/string-utils';
import addVoiceUser from '/imports/api/voice-users/server/modifiers/addVoiceUser'; import addVoiceUser from '/imports/api/voice-users/server/modifiers/addVoiceUser';
const COLOR_LIST = [
'#7b1fa2', '#6a1b9a', '#4a148c', '#5e35b1', '#512da8', '#4527a0',
'#311b92', '#3949ab', '#303f9f', '#283593', '#1a237e', '#1976d2', '#1565c0',
'#0d47a1', '#0277bd', '#01579b',
];
export default async function addUser(meetingId, userData) { export default async function addUser(meetingId, userData) {
const user = userData; const user = userData;
@ -34,6 +27,7 @@ export default async function addUser(meetingId, userData) {
presenter: Boolean, presenter: Boolean,
locked: Boolean, locked: Boolean,
avatar: String, avatar: String,
color: String,
pin: Boolean, pin: Boolean,
clientType: String, clientType: String,
}); });
@ -46,14 +40,9 @@ export default async function addUser(meetingId, userData) {
}; };
const Meeting = await Meetings.findOneAsync({ meetingId }); const Meeting = await Meetings.findOneAsync({ meetingId });
/* While the akka-apps dont generate a color we just pick one
from a list based on the userId */
const color = COLOR_LIST[stringHash(user.intId) % COLOR_LIST.length];
const userInfos = { const userInfos = {
meetingId, meetingId,
sortName: lowercaseTrim(user.name), sortName: lowercaseTrim(user.name),
color,
speechLocale: '', speechLocale: '',
mobile: false, mobile: false,
breakoutProps: { breakoutProps: {
@ -81,6 +70,7 @@ export default async function addUser(meetingId, userData) {
intId: userId, intId: userId,
callerName: user.name, callerName: user.name,
callerNum: '', callerNum: '',
color: user.color,
muted: false, muted: false,
talking: false, talking: false,
callingWith: '', callingWith: '',

View File

@ -40,6 +40,7 @@ export default async function handleGetVoiceUsers({ body }, meetingId) {
callerName: user.callerName, callerName: user.callerName,
callerNum: user.callerNum, callerNum: user.callerNum,
muted: user.muted, muted: user.muted,
color: user.color,
talking: user.talking, talking: user.talking,
callingWith: user.callingWith, callingWith: user.callingWith,
listenOnly: user.listenOnly, listenOnly: user.listenOnly,

View File

@ -3,7 +3,6 @@ import Users from '/imports/api/users';
import addDialInUser from '/imports/api/users/server/modifiers/addDialInUser'; import addDialInUser from '/imports/api/users/server/modifiers/addDialInUser';
import addVoiceUser from '../modifiers/addVoiceUser'; import addVoiceUser from '../modifiers/addVoiceUser';
export default async function handleJoinVoiceUser({ body }, meetingId) { export default async function handleJoinVoiceUser({ body }, meetingId) {
const voiceUser = body; const voiceUser = body;
voiceUser.joined = true; voiceUser.joined = true;
@ -15,6 +14,7 @@ export default async function handleJoinVoiceUser({ body }, meetingId) {
voiceUserId: String, voiceUserId: String,
callerName: String, callerName: String,
callerNum: String, callerNum: String,
color: String,
muted: Boolean, muted: Boolean,
talking: Boolean, talking: Boolean,
callingWith: String, callingWith: String,

View File

@ -5,7 +5,6 @@ import removeVoiceUser from '../modifiers/removeVoiceUser';
import updateVoiceUser from '../modifiers/updateVoiceUser'; import updateVoiceUser from '../modifiers/updateVoiceUser';
import addVoiceUser from '../modifiers/addVoiceUser'; import addVoiceUser from '../modifiers/addVoiceUser';
export default async function handleVoiceUsers({ header, body }) { export default async function handleVoiceUsers({ header, body }) {
const { voiceUsers } = body; const { voiceUsers } = body;
const { meetingId } = header; const { meetingId } = header;
@ -38,6 +37,7 @@ export default async function handleVoiceUsers({ header, body }) {
intId: voice.intId, intId: voice.intId,
callerName: voice.callerName, callerName: voice.callerName,
callerNum: voice.callerNum, callerNum: voice.callerNum,
color: voice.color,
muted: voice.muted, muted: voice.muted,
talking: voice.talking, talking: voice.talking,
callingWith: voice.callingWith, callingWith: voice.callingWith,

View File

@ -1,7 +1,6 @@
import { check } from 'meteor/check'; import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger'; import Logger from '/imports/startup/server/logger';
import VoiceUsers from '/imports/api/voice-users'; import VoiceUsers from '/imports/api/voice-users';
import Users from '/imports/api/users';
import flat from 'flat'; import flat from 'flat';
export default async function addVoiceUser(meetingId, voiceUser) { export default async function addVoiceUser(meetingId, voiceUser) {
@ -11,6 +10,7 @@ export default async function addVoiceUser(meetingId, voiceUser) {
intId: String, intId: String,
callerName: String, callerName: String,
callerNum: String, callerNum: String,
color: String,
muted: Boolean, muted: Boolean,
talking: Boolean, talking: Boolean,
callingWith: String, callingWith: String,
@ -27,19 +27,12 @@ export default async function addVoiceUser(meetingId, voiceUser) {
}; };
const modifier = { const modifier = {
$set: Object.assign( $set: {
{ meetingId, spoke: talking }, meetingId,
flat(voiceUser), spoke: talking,
), ...flat(voiceUser),
};
const user = await Users.findOneAsync({ meetingId, userId: intId }, {
fields: {
color: 1,
}, },
}); };
if (user) modifier.$set.color = user.color;
try { try {
const { numberAffected } = await VoiceUsers.upsertAsync(selector, modifier); const { numberAffected } = await VoiceUsers.upsertAsync(selector, modifier);

View File

@ -2,8 +2,6 @@ import VoiceUsers from '/imports/api/voice-users';
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
import Logger from '/imports/startup/server/logger'; import Logger from '/imports/startup/server/logger';
import AuthTokenValidation, { ValidationStates } from '/imports/api/auth-token-validation'; import AuthTokenValidation, { ValidationStates } from '/imports/api/auth-token-validation';
import ejectUserFromVoice from './methods/ejectUserFromVoice';
import { debounce } from 'radash';
async function voiceUser() { async function voiceUser() {
const tokenValidation = await AuthTokenValidation const tokenValidation = await AuthTokenValidation
@ -15,22 +13,8 @@ async function voiceUser() {
} }
const { meetingId, userId: requesterUserId } = tokenValidation; const { meetingId, userId: requesterUserId } = tokenValidation;
const onCloseConnection = Meteor.bindEnvironment(async () => {
try {
// I used user because voiceUser is the function's name
const User = await VoiceUsers.findOneAsync({ meetingId, requesterUserId });
if (User) {
await ejectUserFromVoice(requesterUserId);
}
} catch (e) {
Logger.error(`Exception while executing ejectUserFromVoice for ${requesterUserId}: ${e}`);
}
});
Logger.debug('Publishing Voice User', { meetingId, requesterUserId }); Logger.debug('Publishing Voice User', { meetingId, requesterUserId });
this._session.socket.on('close', debounce({ delay: 100 }, onCloseConnection));
return VoiceUsers.find({ meetingId }); return VoiceUsers.find({ meetingId });
} }

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import Modal from '/imports/ui/components/common/modal/simple/component'; import ModalSimple from '/imports/ui/components/common/modal/simple/component';
const intlMessages = defineMessages({ const intlMessages = defineMessages({
title: { title: {
@ -38,7 +38,8 @@ const intlMessages = defineMessages({
}, },
}); });
const AboutComponent = ({ intl, settings }) => { const AboutComponent = (props) => {
const { intl, settings, isOpen, onRequestClose, priority, } = props;
const { const {
html5ClientBuild, html5ClientBuild,
copyright, copyright,
@ -54,20 +55,25 @@ const AboutComponent = ({ intl, settings }) => {
); );
return ( return (
<Modal <ModalSimple
data-test="aboutModalTitleLabel" data-test="aboutModalTitleLabel"
title={intl.formatMessage(intlMessages.title)} title={intl.formatMessage(intlMessages.title)}
dismiss={{ dismiss={{
label: intl.formatMessage(intlMessages.dismissLabel), label: intl.formatMessage(intlMessages.dismissLabel),
description: intl.formatMessage(intlMessages.dismissDesc), description: intl.formatMessage(intlMessages.dismissDesc),
}} }}
{...{
isOpen,
onRequestClose,
priority,
}}
> >
{`${intl.formatMessage(intlMessages.copyright)} ${copyright}`} {`${intl.formatMessage(intlMessages.copyright)} ${copyright}`}
<br /> <br />
{`${intl.formatMessage(intlMessages.version)} ${html5ClientBuild}`} {`${intl.formatMessage(intlMessages.version)} ${html5ClientBuild}`}
{displayBbbServerVersion ? showLabelVersion() : null} {displayBbbServerVersion ? showLabelVersion() : null}
</Modal> </ModalSimple>
); );
}; };

View File

@ -1,7 +1,6 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { defineMessages } from 'react-intl'; import { defineMessages } from 'react-intl';
import { withModalMounter } from '/imports/ui/components/common/modal/service';
import withShortcutHelper from '/imports/ui/components/shortcut-help/service'; import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
import ExternalVideoModal from '/imports/ui/components/external-video-player/modal/container'; import ExternalVideoModal from '/imports/ui/components/external-video-player/modal/container';
import RandomUserSelectContainer from '/imports/ui/components/common/modal/random-user/container'; import RandomUserSelectContainer from '/imports/ui/components/common/modal/random-user/container';
@ -19,7 +18,6 @@ const propTypes = {
intl: PropTypes.shape({ intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired, formatMessage: PropTypes.func.isRequired,
}).isRequired, }).isRequired,
mountModal: PropTypes.func.isRequired,
amIModerator: PropTypes.bool.isRequired, amIModerator: PropTypes.bool.isRequired,
shortcuts: PropTypes.string, shortcuts: PropTypes.string,
handleTakePresenter: PropTypes.func.isRequired, handleTakePresenter: PropTypes.func.isRequired,
@ -109,22 +107,29 @@ class ActionsDropdown extends PureComponent {
this.pollId = uniqueId('action-item-'); this.pollId = uniqueId('action-item-');
this.takePresenterId = uniqueId('action-item-'); this.takePresenterId = uniqueId('action-item-');
this.selectUserRandId = uniqueId('action-item-'); this.selectUserRandId = uniqueId('action-item-');
this.state = {
isExternalVideoModalOpen: false,
isRandomUserSelectModalOpen: false,
isLayoutModalOpen: false,
}
this.handleExternalVideoClick = this.handleExternalVideoClick.bind(this); this.handleExternalVideoClick = this.handleExternalVideoClick.bind(this);
this.makePresentationItems = this.makePresentationItems.bind(this); this.makePresentationItems = this.makePresentationItems.bind(this);
this.setExternalVideoModalIsOpen = this.setExternalVideoModalIsOpen.bind(this);
this.setRandomUserSelectModalIsOpen = this.setRandomUserSelectModalIsOpen.bind(this);
this.setLayoutModalIsOpen = this.setLayoutModalIsOpen.bind(this);
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { amIPresenter: wasPresenter } = prevProps; const { amIPresenter: wasPresenter } = prevProps;
const { amIPresenter: isPresenter, mountModal } = this.props; const { amIPresenter: isPresenter } = this.props;
if (wasPresenter && !isPresenter) { if (wasPresenter && !isPresenter) {
mountModal(null); this.setExternalVideoModalIsOpen(false);
} }
} }
handleExternalVideoClick() { handleExternalVideoClick() {
const { mountModal } = this.props; this.setExternalVideoModalIsOpen(true);
mountModal(<ExternalVideoModal />);
} }
getAvailableActions() { getAvailableActions() {
@ -137,7 +142,6 @@ class ActionsDropdown extends PureComponent {
isPollingEnabled, isPollingEnabled,
isSelectRandomUserEnabled, isSelectRandomUserEnabled,
stopExternalVideoShare, stopExternalVideoShare,
mountModal,
layoutContextDispatch, layoutContextDispatch,
setMeetingLayout, setMeetingLayout,
setPushLayout, setPushLayout,
@ -216,7 +220,7 @@ class ActionsDropdown extends PureComponent {
icon: "user", icon: "user",
label: intl.formatMessage(intlMessages.selectRandUserLabel), label: intl.formatMessage(intlMessages.selectRandUserLabel),
key: this.selectUserRandId, key: this.selectUserRandId,
onClick: () => mountModal(<RandomUserSelectContainer isSelectedUser={false} />), onClick: () => this.setRandomUserSelectModalIsOpen(true),
dataTest: "selectRandomUser", dataTest: "selectRandomUser",
}) })
} }
@ -236,7 +240,7 @@ class ActionsDropdown extends PureComponent {
icon: 'send', icon: 'send',
label: intl.formatMessage(intlMessages.layoutModal), label: intl.formatMessage(intlMessages.layoutModal),
key: 'layoutModal', key: 'layoutModal',
onClick: () => mountModal(<LayoutModalContainer {...this.props} />), onClick: () => this.setLayoutModalIsOpen(true),
dataTest: 'layoutModal', dataTest: 'layoutModal',
}); });
} }
@ -281,6 +285,27 @@ class ActionsDropdown extends PureComponent {
return presentationItemElements; return presentationItemElements;
} }
setExternalVideoModalIsOpen(value) {
this.setState({isExternalVideoModalOpen: value});
}
setRandomUserSelectModalIsOpen(value) {
this.setState({isRandomUserSelectModalOpen: value});
}
setLayoutModalIsOpen(value) {
this.setState({isLayoutModalOpen: value});
}
renderModal(isOpen, setIsOpen, priority, Component) {
return isOpen ? <Component
{...{
onRequestClose: () => setIsOpen(false),
priority,
setIsOpen,
isOpen
}}
/> : null
}
render() { render() {
const { const {
intl, intl,
@ -290,8 +315,12 @@ class ActionsDropdown extends PureComponent {
isDropdownOpen, isDropdownOpen,
isMobile, isMobile,
isRTL, isRTL,
isSelectRandomUserEnabled,
} = this.props; } = this.props;
const { isExternalVideoModalOpen,
isRandomUserSelectModalOpen, isLayoutModalOpen } = this.state;
const availableActions = this.getAvailableActions(); const availableActions = this.getAvailableActions();
const availablePresentations = this.makePresentationItems(); const availablePresentations = this.makePresentationItems();
const children = availablePresentations.length > 1 && amIPresenter const children = availablePresentations.length > 1 && amIPresenter
@ -305,35 +334,43 @@ class ActionsDropdown extends PureComponent {
} }
return ( return (
<BBBMenu <>
customStyles={!isMobile ? customStyles : null} <BBBMenu
accessKey={OPEN_ACTIONS_AK} customStyles={!isMobile ? customStyles : null}
trigger={ accessKey={OPEN_ACTIONS_AK}
<Styled.HideDropdownButton trigger={
open={isDropdownOpen} <Styled.HideDropdownButton
hideLabel open={isDropdownOpen}
aria-label={intl.formatMessage(intlMessages.actionsLabel)} hideLabel
data-test="actionsButton" aria-label={intl.formatMessage(intlMessages.actionsLabel)}
label={intl.formatMessage(intlMessages.actionsLabel)} data-test="actionsButton"
icon="plus" label={intl.formatMessage(intlMessages.actionsLabel)}
color="primary" icon="plus"
size="lg" color="primary"
circle size="lg"
onClick={() => null} circle
/> onClick={() => null}
} />
actions={children} }
opts={{ actions={children}
id: "actions-dropdown-menu", opts={{
keepMounted: true, id: "actions-dropdown-menu",
transitionDuration: 0, keepMounted: true,
elevation: 3, transitionDuration: 0,
getContentAnchorEl: null, elevation: 3,
fullwidth: "true", getContentAnchorEl: null,
anchorOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' }, fullwidth: "true",
transformOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' }, anchorOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' },
}} transformOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' },
/> }}
/>
{this.renderModal(isExternalVideoModalOpen, this.setExternalVideoModalIsOpen, "low",
ExternalVideoModal)}
{(amIPresenter && isSelectRandomUserEnabled) ? this.renderModal(isRandomUserSelectModalOpen, this.setRandomUserSelectModalIsOpen,
"low", RandomUserSelectContainer) : null }
{this.renderModal(isLayoutModalOpen, this.setLayoutModalIsOpen,
"low", LayoutModalContainer)}
</>
); );
} }
} }
@ -341,4 +378,4 @@ class ActionsDropdown extends PureComponent {
ActionsDropdown.propTypes = propTypes; ActionsDropdown.propTypes = propTypes;
ActionsDropdown.defaultProps = defaultProps; ActionsDropdown.defaultProps = defaultProps;
export default withShortcutHelper(withModalMounter(ActionsDropdown), 'openActions'); export default withShortcutHelper(ActionsDropdown, 'openActions');

View File

@ -4,6 +4,7 @@ import deviceInfo from '/imports/utils/deviceInfo';
import Styled from './styles'; import Styled from './styles';
import ActionsDropdown from './actions-dropdown/container'; import ActionsDropdown from './actions-dropdown/container';
import AudioCaptionsButtonContainer from '/imports/ui/components/audio/captions/button/container'; import AudioCaptionsButtonContainer from '/imports/ui/components/audio/captions/button/container';
import CaptionsReaderMenuContainer from '/imports/ui/components/captions/reader-menu/container';
import ScreenshareButtonContainer from '/imports/ui/components/actions-bar/screenshare/container'; import ScreenshareButtonContainer from '/imports/ui/components/actions-bar/screenshare/container';
import AudioControlsContainer from '../audio/audio-controls/container'; import AudioControlsContainer from '../audio/audio-controls/container';
import JoinVideoOptionsContainer from '../video-provider/video-button/container'; import JoinVideoOptionsContainer from '../video-provider/video-button/container';
@ -12,6 +13,20 @@ import RaiseHandDropdownContainer from './raise-hand/container';
import { isPresentationEnabled } from '/imports/ui/services/features'; import { isPresentationEnabled } from '/imports/ui/services/features';
class ActionsBar extends PureComponent { class ActionsBar extends PureComponent {
constructor(props) {
super(props);
this.state = {
isCaptionsReaderMenuModalOpen: false,
};
this.setCaptionsReaderMenuModalIsOpen = this.setCaptionsReaderMenuModalIsOpen.bind(this);
}
setCaptionsReaderMenuModalIsOpen(value) {
this.setState({ isCaptionsReaderMenuModalOpen: value })
}
render() { render() {
const { const {
amIPresenter, amIPresenter,
@ -41,8 +56,10 @@ class ActionsBar extends PureComponent {
setPushLayout, setPushLayout,
} = this.props; } = this.props;
const shouldShowOptionsButton = (isPresentationEnabled() && isThereCurrentPresentation) const { isCaptionsReaderMenuModalOpen } = this.state;
|| isSharingVideo || hasScreenshare || isSharedNotesPinned;
const shouldShowOptionsButton = (isPresentationEnabled() && isThereCurrentPresentation)
|| isSharingVideo || hasScreenshare || isSharedNotesPinned;
return ( return (
<Styled.ActionsBar <Styled.ActionsBar
style={ style={
@ -71,7 +88,20 @@ class ActionsBar extends PureComponent {
/> />
{isCaptionsAvailable {isCaptionsAvailable
? ( ? (
<CaptionsButtonContainer {...{ intl }} /> <>
<CaptionsButtonContainer {...{ intl,
setIsOpen: this.setCaptionsReaderMenuModalIsOpen,}} />
{
isCaptionsReaderMenuModalOpen ? <CaptionsReaderMenuContainer
{...{
onRequestClose: () => this.setCaptionsReaderMenuModalIsOpen(false),
priority: "low",
setIsOpen: this.setCaptionsReaderMenuModalIsOpen,
isOpen: isCaptionsReaderMenuModalOpen,
}}
/> : null
}
</>
) )
: null} : null}
{ !deviceInfo.isMobile { !deviceInfo.isMobile

View File

@ -5,8 +5,7 @@ import { range } from '/imports/utils/array-utils';
import deviceInfo from '/imports/utils/deviceInfo'; import deviceInfo from '/imports/utils/deviceInfo';
import Button from '/imports/ui/components/common/button/component'; import Button from '/imports/ui/components/common/button/component';
import { Session } from 'meteor/session'; import { Session } from 'meteor/session';
import Modal from '/imports/ui/components/common/modal/fullscreen/component'; import ModalFullscreen from '/imports/ui/components/common/modal/fullscreen/component';
import { withModalMounter } from '/imports/ui/components/common/modal/service';
import SortList from './sort-user-list/component'; import SortList from './sort-user-list/component';
import Styled from './styles'; import Styled from './styles';
import Icon from '/imports/ui/components/common/icon/component'; import Icon from '/imports/ui/components/common/icon/component';
@ -192,7 +191,6 @@ const propTypes = {
getUsersNotJoined: PropTypes.func.isRequired, getUsersNotJoined: PropTypes.func.isRequired,
getBreakouts: PropTypes.func.isRequired, getBreakouts: PropTypes.func.isRequired,
sendInvitation: PropTypes.func.isRequired, sendInvitation: PropTypes.func.isRequired,
mountModal: PropTypes.func.isRequired,
isBreakoutRecordable: PropTypes.bool, isBreakoutRecordable: PropTypes.bool,
}; };
@ -411,10 +409,10 @@ class BreakoutRoom extends PureComponent {
} }
handleDismiss() { handleDismiss() {
const { mountModal } = this.props; const { setIsOpen } = this.props;
setPresentationVisibility('block'); setPresentationVisibility('block');
return new Promise((resolve) => { return new Promise((resolve) => {
mountModal(null); setIsOpen(false);
this.setState({ this.setState({
preventClosing: false, preventClosing: false,
@ -1317,7 +1315,7 @@ class BreakoutRoom extends PureComponent {
} }
render() { render() {
const { intl, isUpdate } = this.props; const { intl, isUpdate, isOpen, priority, setIsOpen, } = this.props;
const { const {
preventClosing, preventClosing,
leastOneUserIsValid, leastOneUserIsValid,
@ -1330,7 +1328,7 @@ class BreakoutRoom extends PureComponent {
const { isMobile } = deviceInfo; const { isMobile } = deviceInfo;
return ( return (
<Modal <ModalFullscreen
title={ title={
isUpdate isUpdate
? intl.formatMessage(intlMessages.updateTitle) ? intl.formatMessage(intlMessages.updateTitle)
@ -1357,16 +1355,21 @@ class BreakoutRoom extends PureComponent {
: intl.formatMessage(intlMessages.dismissLabel), : intl.formatMessage(intlMessages.dismissLabel),
}} }}
preventClosing={preventClosing} preventClosing={preventClosing}
{...{
isOpen,
priority,
setIsOpen,
}}
> >
<Styled.Content> <Styled.Content>
{this.renderTitle()} {this.renderTitle()}
{isMobile ? this.renderMobile() : this.renderDesktop()} {isMobile ? this.renderMobile() : this.renderDesktop()}
</Styled.Content> </Styled.Content>
</Modal> </ModalFullscreen>
); );
} }
} }
BreakoutRoom.propTypes = propTypes; BreakoutRoom.propTypes = propTypes;
export default withModalMounter(injectIntl(BreakoutRoom)); export default injectIntl(BreakoutRoom);

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import Button from '/imports/ui/components/common/button/component'; import Button from '/imports/ui/components/common/button/component';
const propTypes = { const propTypes = {
intl: PropTypes.shape({ intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired, formatMessage: PropTypes.func.isRequired,
@ -47,7 +48,8 @@ const PresentationOptionsContainer = ({
buttonType = 'desktop'; buttonType = 'desktop';
} }
const isThereCurrentPresentation = hasExternalVideo || hasScreenshare || hasPresentation || hasPinnedSharedNotes; const isThereCurrentPresentation = hasExternalVideo || hasScreenshare
|| hasPresentation || hasPinnedSharedNotes;
return ( return (
<Button <Button
icon={`${buttonType}${!presentationIsOpen ? '_off' : ''}`} icon={`${buttonType}${!presentationIsOpen ? '_off' : ''}`}
@ -59,7 +61,12 @@ const PresentationOptionsContainer = ({
hideLabel hideLabel
circle circle
size="lg" size="lg"
onClick={() => setPresentationIsOpen(layoutContextDispatch, !presentationIsOpen)} onClick={() => {
setPresentationIsOpen(layoutContextDispatch, !presentationIsOpen);
if (!hasExternalVideo && !hasScreenshare && !hasPinnedSharedNotes) {
Session.set('presentationLastState', !presentationIsOpen);
}
}}
id="restore-presentation" id="restore-presentation"
ghost={!presentationIsOpen} ghost={!presentationIsOpen}
disabled={!isThereCurrentPresentation} disabled={!isThereCurrentPresentation}

View File

@ -1,11 +1,10 @@
import React, { memo } from 'react'; import React, { memo, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import deviceInfo from '/imports/utils/deviceInfo'; import deviceInfo from '/imports/utils/deviceInfo';
import browserInfo from '/imports/utils/browserInfo'; import browserInfo from '/imports/utils/browserInfo';
import logger from '/imports/startup/client/logger'; import logger from '/imports/startup/client/logger';
import { notify } from '/imports/ui/services/notification'; import { notify } from '/imports/ui/services/notification';
import { withModalMounter } from '/imports/ui/components/common/modal/service';
import Styled from './styles'; import Styled from './styles';
import ScreenshareBridgeService from '/imports/api/screenshare/client/bridge/service'; import ScreenshareBridgeService from '/imports/api/screenshare/client/bridge/service';
import { import {
@ -14,6 +13,7 @@ import {
} from '/imports/ui/components/screenshare/service'; } from '/imports/ui/components/screenshare/service';
import { SCREENSHARING_ERRORS } from '/imports/api/screenshare/client/bridge/errors'; import { SCREENSHARING_ERRORS } from '/imports/api/screenshare/client/bridge/errors';
import Button from '/imports/ui/components/common/button/component'; import Button from '/imports/ui/components/common/button/component';
import { parsePayloads } from 'sdp-transform';
const { isMobile } = deviceInfo; const { isMobile } = deviceInfo;
const { isSafari, isTabletApp } = browserInfo; const { isSafari, isTabletApp } = browserInfo;
@ -117,8 +117,6 @@ const ScreenshareButton = ({
isVideoBroadcasting, isVideoBroadcasting,
amIPresenter, amIPresenter,
isMeteorConnected, isMeteorConnected,
screenshareDataSavingSetting,
mountModal,
}) => { }) => {
// This is the failure callback that will be passed to the /api/screenshare/kurento.js // This is the failure callback that will be passed to the /api/screenshare/kurento.js
// script on the presenter's call // script on the presenter's call
@ -141,18 +139,19 @@ const ScreenshareButton = ({
screenshareHasEnded(); screenshareHasEnded();
}; };
const renderScreenshareUnavailableModal = () => mountModal( const [isScreenshareUnavailableModalOpen, setScreenshareUnavailableModalIsOpen] = useState(false);
const RenderScreenshareUnavailableModal = (otherProps) =>
<Styled.ScreenShareModal <Styled.ScreenShareModal
onRequestClose={() => mountModal(null)}
hideBorder hideBorder
contentLabel={intl.formatMessage(intlMessages.screenShareUnavailable)} contentLabel={intl.formatMessage(intlMessages.screenShareUnavailable)}
{...otherProps}
> >
<Styled.Title> <Styled.Title>
{intl.formatMessage(intlMessages.screenShareUnavailable)} {intl.formatMessage(intlMessages.screenShareUnavailable)}
</Styled.Title> </Styled.Title>
<p>{intl.formatMessage(intlMessages.screenShareNotSupported)}</p> <p>{intl.formatMessage(intlMessages.screenShareNotSupported)}</p>
</Styled.ScreenShareModal>, </Styled.ScreenShareModal>;
);
const screenshareLabel = intlMessages.desktopShareLabel; const screenshareLabel = intlMessages.desktopShareLabel;
@ -168,32 +167,46 @@ const ScreenshareButton = ({
const dataTest = isVideoBroadcasting ? 'stopScreenShare' : 'startScreenShare'; const dataTest = isVideoBroadcasting ? 'stopScreenShare' : 'startScreenShare';
return shouldAllowScreensharing return <>
? ( {
<Button shouldAllowScreensharing
disabled={(!isMeteorConnected && !isVideoBroadcasting)} ? (
icon={isVideoBroadcasting ? 'desktop' : 'desktop_off'} <Button
data-test={dataTest} disabled={(!isMeteorConnected && !isVideoBroadcasting)}
label={intl.formatMessage(vLabel)} icon={isVideoBroadcasting ? 'desktop' : 'desktop_off'}
description={intl.formatMessage(vDescr)} data-test={dataTest}
color={isVideoBroadcasting ? 'primary' : 'default'} label={intl.formatMessage(vLabel)}
ghost={!isVideoBroadcasting} description={intl.formatMessage(vDescr)}
hideLabel color={isVideoBroadcasting ? 'primary' : 'default'}
circle ghost={!isVideoBroadcasting}
size="lg" hideLabel
onClick={isVideoBroadcasting circle
? screenshareHasEnded size="lg"
: () => { onClick={isVideoBroadcasting
if (isSafari && !ScreenshareBridgeService.HAS_DISPLAY_MEDIA) { ? screenshareHasEnded
renderScreenshareUnavailableModal(); : () => {
} else { if (isSafari && !ScreenshareBridgeService.HAS_DISPLAY_MEDIA) {
shareScreen(amIPresenter, handleFailure); setScreenshareUnavailableModalIsOpen(true);
} } else {
}} shareScreen(amIPresenter, handleFailure);
id={isVideoBroadcasting ? 'unshare-screen-button' : 'share-screen-button'} }
/> }}
) : null; id={isVideoBroadcasting ? 'unshare-screen-button' : 'share-screen-button'}
/>
) : null
}
{
isScreenshareUnavailableModalOpen ? <RenderScreenshareUnavailableModal
{...{
onRequestClose: () => setScreenshareUnavailableModalIsOpen(false),
priority: "low",
setIsOpen: setScreenshareUnavailableModalIsOpen,
isOpen: isScreenshareUnavailableModalOpen,
}}
/> : null
}
</>
}; };
ScreenshareButton.propTypes = propTypes; ScreenshareButton.propTypes = propTypes;
export default withModalMounter(injectIntl(memo(ScreenshareButton))); export default injectIntl(memo(ScreenshareButton));

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { withTracker } from 'meteor/react-meteor-data'; import { withTracker } from 'meteor/react-meteor-data';
import { withModalMounter } from '/imports/ui/components/common/modal/service';
import ScreenshareButton from './component'; import ScreenshareButton from './component';
import { isScreenSharingEnabled } from '/imports/ui/services/features'; import { isScreenSharingEnabled } from '/imports/ui/services/features';
import { import {
@ -18,8 +17,8 @@ const ScreenshareButtonContainer = (props) => <ScreenshareButton {...props} />;
* isMeteorConnected, * isMeteorConnected,
* screenshareDataSavingSetting, * screenshareDataSavingSetting,
*/ */
export default withModalMounter(withTracker(() => ({ export default withTracker(() => ({
isVideoBroadcasting: isVideoBroadcasting(), isVideoBroadcasting: isVideoBroadcasting(),
screenshareDataSavingSetting: dataSavingSetting(), screenshareDataSavingSetting: dataSavingSetting(),
enabled: isScreenSharingEnabled(), enabled: isScreenSharingEnabled(),
}))(ScreenshareButtonContainer)); }))(ScreenshareButtonContainer);

View File

@ -1,5 +1,5 @@
import styled from 'styled-components'; import styled from 'styled-components';
import Modal from '/imports/ui/components/common/modal/simple/component'; import ModalSimple from '/imports/ui/components/common/modal/simple/component';
import { colorGrayDark } from '/imports/ui/stylesheets/styled-components/palette'; import { colorGrayDark } from '/imports/ui/stylesheets/styled-components/palette';
import { import {
jumboPaddingY, jumboPaddingY,
@ -9,7 +9,7 @@ import {
} from '/imports/ui/stylesheets/styled-components/general'; } from '/imports/ui/stylesheets/styled-components/general';
import { fontSizeLarge } from '/imports/ui/stylesheets/styled-components/typography'; import { fontSizeLarge } from '/imports/ui/stylesheets/styled-components/typography';
const ScreenShareModal = styled(Modal)` const ScreenShareModal = styled(ModalSimple)`
padding: ${jumboPaddingY}; padding: ${jumboPaddingY};
min-height: ${minModalHeight}; min-height: ${minModalHeight};
text-align: center; text-align: center;

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { defineMessages } from 'react-intl'; import { defineMessages } from 'react-intl';
import Button from '/imports/ui/components/common/button/component'; import Button from '/imports/ui/components/common/button/component';
import Modal from '/imports/ui/components/common/modal/simple/component'; import ModalSimple from '/imports/ui/components/common/modal/simple/component';
import { makeCall } from '/imports/ui/services/api'; import { makeCall } from '/imports/ui/services/api';
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
@ -92,7 +92,7 @@ class ActivityCheck extends Component {
const { responseDelay } = this.state; const { responseDelay } = this.state;
return ( return (
<Modal <ModalSimple
hideBorder hideBorder
onRequestClose={handleInactivityDismiss} onRequestClose={handleInactivityDismiss}
shouldCloseOnOverlayClick={false} shouldCloseOnOverlayClick={false}
@ -110,7 +110,7 @@ class ActivityCheck extends Component {
size="lg" size="lg"
/> />
</Styled.ActivityModalContent> </Styled.ActivityModalContent>
</Modal> </ModalSimple>
); );
} }
} }

View File

@ -2,7 +2,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { throttle } from '/imports/utils/throttle'; import { throttle } from '/imports/utils/throttle';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import Modal from 'react-modal'; import ReactModal from 'react-modal';
import browserInfo from '/imports/utils/browserInfo'; import browserInfo from '/imports/utils/browserInfo';
import deviceInfo from '/imports/utils/deviceInfo'; import deviceInfo from '/imports/utils/deviceInfo';
import PollingContainer from '/imports/ui/components/polling/container'; import PollingContainer from '/imports/ui/components/polling/container';
@ -13,7 +13,6 @@ import BreakoutRoomInvitation from '/imports/ui/components/breakout-room/invitat
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
import ToastContainer from '/imports/ui/components/common/toast/container'; import ToastContainer from '/imports/ui/components/common/toast/container';
import PadsSessionsContainer from '/imports/ui/components/pads/sessions/container'; import PadsSessionsContainer from '/imports/ui/components/pads/sessions/container';
import ModalContainer from '/imports/ui/components/common/modal/container';
import NotificationsBarContainer from '../notifications-bar/container'; import NotificationsBarContainer from '../notifications-bar/container';
import AudioContainer from '../audio/container'; import AudioContainer from '../audio/container';
import ChatAlertContainer from '../chat/alert/container'; import ChatAlertContainer from '../chat/alert/container';
@ -135,10 +134,16 @@ class App extends Component {
super(props); super(props);
this.state = { this.state = {
enableResize: !window.matchMedia(MOBILE_MEDIA).matches, enableResize: !window.matchMedia(MOBILE_MEDIA).matches,
isAudioModalOpen: false,
isRandomUserSelectModalOpen: false,
isVideoPreviewModalOpen: false,
}; };
this.handleWindowResize = throttle(this.handleWindowResize).bind(this); this.handleWindowResize = throttle(this.handleWindowResize).bind(this);
this.shouldAriaHide = this.shouldAriaHide.bind(this); this.shouldAriaHide = this.shouldAriaHide.bind(this);
this.setAudioModalIsOpen = this.setAudioModalIsOpen.bind(this);
this.setRandomUserSelectModalIsOpen = this.setRandomUserSelectModalIsOpen.bind(this);
this.setVideoPreviewModalIsOpen = this.setVideoPreviewModalIsOpen.bind(this);
this.throttledDeviceType = throttle(() => this.setDeviceType(), this.throttledDeviceType = throttle(() => this.setDeviceType(),
50, { trailing: true, leading: true }).bind(this); 50, { trailing: true, leading: true }).bind(this);
@ -162,7 +167,7 @@ class App extends Component {
value: isRTL, value: isRTL,
}); });
Modal.setAppElement('#app'); ReactModal.setAppElement('#app');
const fontSize = isMobile() ? MOBILE_FONT_SIZE : DESKTOP_FONT_SIZE; const fontSize = isMobile() ? MOBILE_FONT_SIZE : DESKTOP_FONT_SIZE;
document.getElementsByTagName('html')[0].style.fontSize = fontSize; document.getElementsByTagName('html')[0].style.fontSize = fontSize;
@ -220,7 +225,6 @@ class App extends Component {
notify, notify,
currentUserEmoji, currentUserEmoji,
intl, intl,
mountModal,
deviceType, deviceType,
mountRandomUserModal, mountRandomUserModal,
selectedLayout, selectedLayout,
@ -228,12 +232,11 @@ class App extends Component {
layoutContextDispatch, layoutContextDispatch,
numCameras, numCameras,
presentationIsOpen, presentationIsOpen,
ignorePollNotifications,
} = this.props; } = this.props;
this.renderDarkMode(); this.renderDarkMode();
if (mountRandomUserModal) mountModal(<RandomUserSelectContainer />); if (mountRandomUserModal) this.setRandomUserSelectModalIsOpen(true);
if (prevProps.currentUserEmoji.status !== currentUserEmoji.status) { if (prevProps.currentUserEmoji.status !== currentUserEmoji.status) {
const formattedEmojiStatus = intl.formatMessage({ id: `app.actionsBar.emojiMenu.${currentUserEmoji.status}Label` }) const formattedEmojiStatus = intl.formatMessage({ id: `app.actionsBar.emojiMenu.${currentUserEmoji.status}Label` })
@ -506,12 +509,26 @@ class App extends Component {
setMeetingLayout, setMeetingLayout,
setPushLayout, setPushLayout,
shouldShowScreenshare, shouldShowScreenshare,
shouldShowExternalVideo, shouldShowExternalVideo: !!shouldShowExternalVideo,
}} }}
/> />
); );
} }
setAudioModalIsOpen(value) {
this.setState({isAudioModalOpen: value});
}
setVideoPreviewModalIsOpen(value) {
this.setState({isVideoPreviewModalOpen: value});
}
setRandomUserSelectModalIsOpen(value) {
const {setMountRandomUserModal} = this.props;
this.setState({isRandomUserSelectModalOpen: value});
setMountRandomUserModal(false);
}
render() { render() {
const { const {
customStyle, customStyle,
@ -528,6 +545,7 @@ class App extends Component {
darkTheme, darkTheme,
} = this.props; } = this.props;
const { isAudioModalOpen, isRandomUserSelectModalOpen, isVideoPreviewModalOpen } = this.state;
return ( return (
<> <>
<Notifications /> <Notifications />
@ -571,7 +589,12 @@ class App extends Component {
<UploaderContainer /> <UploaderContainer />
<CaptionsSpeechContainer /> <CaptionsSpeechContainer />
<BreakoutRoomInvitation /> <BreakoutRoomInvitation />
<AudioContainer /> <AudioContainer {...{
isAudioModalOpen,
setAudioModalIsOpen: this.setAudioModalIsOpen,
isVideoPreviewModalOpen,
setVideoPreviewModalIsOpen: this.setVideoPreviewModalIsOpen,
}} />
<ToastContainer rtl /> <ToastContainer rtl />
{(audioAlertEnabled || pushAlertEnabled) {(audioAlertEnabled || pushAlertEnabled)
&& ( && (
@ -583,11 +606,18 @@ class App extends Component {
<StatusNotifier status="raiseHand" /> <StatusNotifier status="raiseHand" />
<ManyWebcamsNotifier /> <ManyWebcamsNotifier />
<PollingContainer /> <PollingContainer />
<ModalContainer />
<PadsSessionsContainer /> <PadsSessionsContainer />
{this.renderActionsBar()} {this.renderActionsBar()}
{customStyleUrl ? <link rel="stylesheet" type="text/css" href={customStyleUrl} /> : null} {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} {customStyle ? <link rel="stylesheet" type="text/css" href={`data:text/css;charset=UTF-8,${encodeURIComponent(customStyle)}`} /> : null}
{isRandomUserSelectModalOpen ? <RandomUserSelectContainer
{...{
onRequestClose: () => this.setRandomUserSelectModalIsOpen(false),
priority: "low",
setIsOpen: this.setRandomUserSelectModalIsOpen,
isOpen: isRandomUserSelectModalOpen,
}}
/> : null}
</Styled.Layout> </Styled.Layout>
</> </>
); );

View File

@ -1,4 +1,4 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { withTracker } from 'meteor/react-meteor-data'; import { withTracker } from 'meteor/react-meteor-data';
import Auth from '/imports/ui/services/auth'; import Auth from '/imports/ui/services/auth';
import Users from '/imports/api/users'; import Users from '/imports/api/users';
@ -31,8 +31,6 @@ import {
validIOSVersion, validIOSVersion,
} from './service'; } from './service';
import { withModalMounter, getModal } from '/imports/ui/components/common/modal/service';
import App from './component'; import App from './component';
const CUSTOM_STYLE_URL = Meteor.settings.public.app.customStyleUrl; const CUSTOM_STYLE_URL = Meteor.settings.public.app.customStyleUrl;
@ -116,10 +114,14 @@ const AppContainer = (props) => {
const prevRandomUser = usePrevious(randomlySelectedUser); const prevRandomUser = usePrevious(randomlySelectedUser);
const mountRandomUserModal = !isPresenter const [mountRandomUserModal, setMountRandomUserModal] = useState(false);
&& !isEqual(prevRandomUser, randomlySelectedUser)
&& randomlySelectedUser.length > 0 useEffect(() => {
&& !isModalOpen; setMountRandomUserModal(!isPresenter
&& !isEqual(prevRandomUser, randomlySelectedUser)
&& randomlySelectedUser.length > 0
&& !isModalOpen);
}, [isPresenter, prevRandomUser, randomlySelectedUser, isModalOpen]);
const setPushLayout = () => { const setPushLayout = () => {
LayoutService.setPushLayout(pushLayout); LayoutService.setPushLayout(pushLayout);
@ -176,6 +178,7 @@ const AppContainer = (props) => {
sidebarContentIsOpen, sidebarContentIsOpen,
shouldShowPresentation, shouldShowPresentation,
mountRandomUserModal, mountRandomUserModal,
setMountRandomUserModal,
isPresenter, isPresenter,
numCameras: cameraDockInput.numCameras, numCameras: cameraDockInput.numCameras,
}} }}
@ -196,7 +199,7 @@ const currentUserEmoji = (currentUser) => (currentUser
} }
); );
export default withModalMounter(withTracker(() => { export default withTracker(() => {
Users.find({ userId: Auth.userID, meetingId: Auth.meetingID }).observe({ Users.find({ userId: Auth.userID, meetingId: Auth.meetingID }).observe({
removed(userData) { removed(userData) {
// wait 3secs (before endMeeting), client will try to authenticate again // wait 3secs (before endMeeting), client will try to authenticate again
@ -315,7 +318,6 @@ export default withModalMounter(withTracker(() => {
), ),
hidePresentationOnJoin: getFromUserSettings('bbb_hide_presentation_on_join', LAYOUT_CONFIG.hidePresentationOnJoin), hidePresentationOnJoin: getFromUserSettings('bbb_hide_presentation_on_join', LAYOUT_CONFIG.hidePresentationOnJoin),
hideActionsBar: getFromUserSettings('bbb_hide_actions_bar', false), hideActionsBar: getFromUserSettings('bbb_hide_actions_bar', false),
isModalOpen: !!getModal(),
ignorePollNotifications: Session.get('ignorePollNotifications'), ignorePollNotifications: Session.get('ignorePollNotifications'),
}; };
})(AppContainer)); })(AppContainer);

View File

@ -7,6 +7,7 @@ import InputStreamLiveSelectorContainer from './input-stream-live-selector/conta
import MutedAlert from '/imports/ui/components/muted-alert/component'; import MutedAlert from '/imports/ui/components/muted-alert/component';
import Styled from './styles'; import Styled from './styles';
import Button from '/imports/ui/components/common/button/component'; import Button from '/imports/ui/components/common/button/component';
import AudioModalContainer from '../audio-modal/container';
const intlMessages = defineMessages({ const intlMessages = defineMessages({
joinAudio: { joinAudio: {
@ -30,7 +31,6 @@ const intlMessages = defineMessages({
const propTypes = { const propTypes = {
shortcuts: PropTypes.objectOf(PropTypes.string).isRequired, shortcuts: PropTypes.objectOf(PropTypes.string).isRequired,
handleToggleMuteMicrophone: PropTypes.func.isRequired, handleToggleMuteMicrophone: PropTypes.func.isRequired,
handleJoinAudio: PropTypes.func.isRequired,
handleLeaveAudio: PropTypes.func.isRequired, handleLeaveAudio: PropTypes.func.isRequired,
disable: PropTypes.bool.isRequired, disable: PropTypes.bool.isRequired,
muted: PropTypes.bool.isRequired, muted: PropTypes.bool.isRequired,
@ -46,21 +46,28 @@ const propTypes = {
class AudioControls extends PureComponent { class AudioControls extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = {
isAudioModalOpen: false,
};
this.renderButtonsAndStreamSelector = this.renderButtonsAndStreamSelector.bind(this); this.renderButtonsAndStreamSelector = this.renderButtonsAndStreamSelector.bind(this);
this.renderJoinLeaveButton = this.renderJoinLeaveButton.bind(this); this.renderJoinLeaveButton = this.renderJoinLeaveButton.bind(this);
this.setAudioModalIsOpen = this.setAudioModalIsOpen.bind(this);
} }
renderJoinButton() { renderJoinButton() {
const { const {
handleJoinAudio,
disable, disable,
intl, intl,
shortcuts, shortcuts,
joinListenOnly,
isConnected
} = this.props; } = this.props;
return ( return (
<Button <Button
onClick={handleJoinAudio} onClick={() => this.handleJoinAudio(joinListenOnly, isConnected)}
disabled={disable} disabled={disable}
hideLabel hideLabel
aria-label={intl.formatMessage(intlMessages.joinAudio)} aria-label={intl.formatMessage(intlMessages.joinAudio)}
@ -119,6 +126,16 @@ class AudioControls extends PureComponent {
return this.renderJoinButton(); return this.renderJoinButton();
} }
handleJoinAudio(joinListenOnly, isConnected) {
(isConnected()
? joinListenOnly()
: this.setAudioModalIsOpen(true)
)}
setAudioModalIsOpen(value) {
this.setState({ isAudioModalOpen: value })
}
render() { render() {
const { const {
showMute, showMute,
@ -130,6 +147,8 @@ class AudioControls extends PureComponent {
isPresenter, isPresenter,
} = this.props; } = this.props;
const { isAudioModalOpen } = this.state;
const MUTE_ALERT_CONFIG = Meteor.settings.public.app.mutedAlert; const MUTE_ALERT_CONFIG = Meteor.settings.public.app.mutedAlert;
const { enabled: muteAlertEnabled } = MUTE_ALERT_CONFIG; const { enabled: muteAlertEnabled } = MUTE_ALERT_CONFIG;
@ -144,6 +163,15 @@ class AudioControls extends PureComponent {
{ {
this.renderJoinLeaveButton() this.renderJoinLeaveButton()
} }
{
isAudioModalOpen ? <AudioModalContainer
{...{
priority: "low",
setIsOpen: this.setAudioModalIsOpen,
isOpen: isAudioModalOpen
}}
/> : null
}
</Styled.Container> </Styled.Container>
); );
} }

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { withTracker } from 'meteor/react-meteor-data'; import { withTracker } from 'meteor/react-meteor-data';
import { withModalMounter } from '/imports/ui/components/common/modal/service';
import AudioManager from '/imports/ui/services/audio-manager'; import AudioManager from '/imports/ui/services/audio-manager';
import lockContextContainer from '/imports/ui/components/lock-viewers/context/container'; import lockContextContainer from '/imports/ui/components/lock-viewers/context/container';
import { withUsersConsumer } from '/imports/ui/components/components-data/users-context/context'; import { withUsersConsumer } from '/imports/ui/components/components-data/users-context/context';
@ -9,7 +8,6 @@ import Auth from '/imports/ui/services/auth';
import Storage from '/imports/ui/services/storage/session'; import Storage from '/imports/ui/services/storage/session';
import getFromUserSettings from '/imports/ui/services/users-settings'; import getFromUserSettings from '/imports/ui/services/users-settings';
import AudioControls from './component'; import AudioControls from './component';
import AudioModalContainer from '../audio-modal/container';
import { import {
setUserSelectedMicrophone, setUserSelectedMicrophone,
setUserSelectedListenOnly, setUserSelectedListenOnly,
@ -65,7 +63,7 @@ const {
export default withUsersConsumer( export default withUsersConsumer(
lockContextContainer( lockContextContainer(
withModalMounter(withTracker(({ mountModal, userLocks, users }) => { withTracker(({ userLocks, users }) => {
const currentUser = users[Auth.meetingID][Auth.userID]; const currentUser = users[Auth.meetingID][Auth.userID];
const isViewer = currentUser.role === ROLE_VIEWER; const isViewer = currentUser.role === ROLE_VIEWER;
const isPresenter = currentUser.presenter; const isPresenter = currentUser.presenter;
@ -87,15 +85,13 @@ export default withUsersConsumer(
talking: isTalking() && !isMuted(), talking: isTalking() && !isMuted(),
isVoiceUser: isVoiceUser(), isVoiceUser: isVoiceUser(),
handleToggleMuteMicrophone: () => toggleMuteMicrophone(), handleToggleMuteMicrophone: () => toggleMuteMicrophone(),
handleJoinAudio: () => (isConnected() joinListenOnly,
? joinListenOnly()
: mountModal(<AudioModalContainer />)
),
handleLeaveAudio, handleLeaveAudio,
inputStream: AudioManager.inputStream, inputStream: AudioManager.inputStream,
isViewer, isViewer,
isPresenter, isPresenter,
isConnected,
}); });
})(AudioControlsContainer)), })(AudioControlsContainer),
), ),
); );

View File

@ -14,6 +14,7 @@ const AUDIO_INPUT = 'audioinput';
const AUDIO_OUTPUT = 'audiooutput'; const AUDIO_OUTPUT = 'audiooutput';
const DEFAULT_DEVICE = 'default'; const DEFAULT_DEVICE = 'default';
const DEVICE_LABEL_MAX_LENGTH = 40; const DEVICE_LABEL_MAX_LENGTH = 40;
const SET_SINK_ID_SUPPORTED = 'setSinkId' in HTMLMediaElement.prototype;
const intlMessages = defineMessages({ const intlMessages = defineMessages({
changeAudioDevice: { changeAudioDevice: {
@ -52,6 +53,10 @@ const intlMessages = defineMessages({
id: 'app.audioNotification.deviceChangeFailed', id: 'app.audioNotification.deviceChangeFailed',
description: 'Device change failed', description: 'Device change failed',
}, },
defaultOutputDeviceLabel: {
id: 'app.audio.audioSettings.defaultOutputDeviceLabel',
description: 'Default output device label',
},
}); });
const propTypes = { const propTypes = {
@ -263,8 +268,10 @@ class InputStreamLiveSelector extends Component {
}, },
]; ];
const deviceList = (listLength > 0) let deviceList = [];
? list.map((device, index) => (
if (listLength > 0) {
deviceList = list.map((device, index) => (
{ {
key: `${device.deviceId}-${deviceKind}`, key: `${device.deviceId}-${deviceKind}`,
dataTest: `${deviceKind}-${index + 1}`, dataTest: `${deviceKind}-${index + 1}`,
@ -273,8 +280,21 @@ class InputStreamLiveSelector extends Component {
iconRight: (device.deviceId === currentDeviceId) ? 'check' : null, iconRight: (device.deviceId === currentDeviceId) ? 'check' : null,
onClick: () => this.onDeviceListClick(device.deviceId, deviceKind, callback), 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}-`, key: `noDeviceFoundKey-${deviceKind}-`,
label: listLength < 0 label: listLength < 0
@ -282,6 +302,8 @@ class InputStreamLiveSelector extends Component {
: intl.formatMessage(intlMessages.noDeviceFound), : intl.formatMessage(intlMessages.noDeviceFound),
}, },
]; ];
}
return listTitle.concat(deviceList); return listTitle.concat(deviceList);
} }

View File

@ -13,7 +13,6 @@ import AudioDial from '../audio-dial/component';
import AudioAutoplayPrompt from '../autoplay/component'; import AudioAutoplayPrompt from '../autoplay/component';
import Settings from '/imports/ui/services/settings'; import Settings from '/imports/ui/services/settings';
import CaptionsSelectContainer from '/imports/ui/components/audio/captions/select/container'; import CaptionsSelectContainer from '/imports/ui/components/audio/captions/select/container';
import { showModal } from '/imports/ui/components/common/modal/service';
const propTypes = { const propTypes = {
intl: PropTypes.shape({ intl: PropTypes.shape({
@ -177,7 +176,6 @@ class AudioModal extends Component {
audioLocked, audioLocked,
isUsingAudio, isUsingAudio,
} = this.props; } = this.props;
window.addEventListener("CLOSE_AUDIO_MODAL", this.handleCloseAudioModal);
if (!isUsingAudio) { if (!isUsingAudio) {
if (forceListenOnlyAttendee || audioLocked) return this.handleJoinListenOnly(); if (forceListenOnlyAttendee || audioLocked) return this.handleJoinListenOnly();
@ -212,7 +210,6 @@ class AudioModal extends Component {
exitAudio(); exitAudio();
} }
if (resolve) resolve(); if (resolve) resolve();
window.removeEventListener("CLOSE_AUDIO_MODAL", this.handleCloseAudioModal);
Session.set('audioModalIsOpen', false); Session.set('audioModalIsOpen', false);
} }
@ -252,10 +249,6 @@ class AudioModal extends Component {
}); });
} }
handleCloseAudioModal = () => {
showModal(null);
}
handleGoToEchoTest() { handleGoToEchoTest() {
const { AudioError } = this.props; const { AudioError } = this.props;
const { MIC_ERROR } = AudioError; const { MIC_ERROR } = AudioError;
@ -599,6 +592,9 @@ class AudioModal extends Component {
showPermissionsOvelay, showPermissionsOvelay,
closeModal, closeModal,
isIE, isIE,
isOpen,
priority,
setIsOpen,
} = this.props; } = this.props;
const { content } = this.state; const { content } = this.state;
@ -607,6 +603,7 @@ class AudioModal extends Component {
<span> <span>
{showPermissionsOvelay ? <PermissionsOverlay closeModal={closeModal} /> : null} {showPermissionsOvelay ? <PermissionsOverlay closeModal={closeModal} /> : null}
<Styled.AudioModal <Styled.AudioModal
modalName="AUDIO"
onRequestClose={closeModal} onRequestClose={closeModal}
data-test="audioModal" data-test="audioModal"
contentLabel={intl.formatMessage(intlMessages.ariaModalTitle)} contentLabel={intl.formatMessage(intlMessages.ariaModalTitle)}
@ -619,6 +616,11 @@ class AudioModal extends Component {
) )
: null : null
} }
{...{
setIsOpen,
isOpen,
priority,
}}
> >
{isIE ? ( {isIE ? (
<Styled.BrowserWarning> <Styled.BrowserWarning>

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { withTracker } from 'meteor/react-meteor-data'; import { withTracker } from 'meteor/react-meteor-data';
import { withModalMounter } from '/imports/ui/components/common/modal/service';
import browserInfo from '/imports/utils/browserInfo'; import browserInfo from '/imports/utils/browserInfo';
import getFromUserSettings from '/imports/ui/services/users-settings'; import getFromUserSettings from '/imports/ui/services/users-settings';
import AudioModal from './component'; import AudioModal from './component';
@ -25,7 +24,7 @@ const APP_CONFIG = Meteor.settings.public.app;
const invalidDialNumbers = ['0', '613-555-1212', '613-555-1234', '0000']; const invalidDialNumbers = ['0', '613-555-1212', '613-555-1234', '0000'];
const isRTL = document.documentElement.getAttribute('dir') === 'rtl'; const isRTL = document.documentElement.getAttribute('dir') === 'rtl';
export default lockContextContainer(withModalMounter(withTracker(({ userLocks }) => { export default lockContextContainer(withTracker(({ userLocks, setIsOpen }) => {
const listenOnlyMode = getFromUserSettings('bbb_listen_only_mode', APP_CONFIG.listenOnlyMode); const listenOnlyMode = getFromUserSettings('bbb_listen_only_mode', APP_CONFIG.listenOnlyMode);
const forceListenOnly = getFromUserSettings('bbb_force_listen_only', APP_CONFIG.forceListenOnly); const forceListenOnly = getFromUserSettings('bbb_force_listen_only', APP_CONFIG.forceListenOnly);
const skipCheck = getFromUserSettings('bbb_skip_check_audio', APP_CONFIG.skipCheck); const skipCheck = getFromUserSettings('bbb_skip_check_audio', APP_CONFIG.skipCheck);
@ -65,7 +64,7 @@ export default lockContextContainer(withModalMounter(withTracker(({ userLocks })
return ({ return ({
meetingIsBreakout, meetingIsBreakout,
closeModal, closeModal: () => closeModal(() => setIsOpen(false)),
joinMicrophone: (skipEchoTest) => joinMicrophone(skipEchoTest || skipCheck || skipCheckOnJoin), joinMicrophone: (skipEchoTest) => joinMicrophone(skipEchoTest || skipCheck || skipCheckOnJoin),
joinListenOnly, joinListenOnly,
leaveEchoTest, leaveEchoTest,
@ -100,4 +99,4 @@ export default lockContextContainer(withModalMounter(withTracker(({ userLocks })
isRTL, isRTL,
AudioError, AudioError,
}); });
})(AudioModalContainer))); })(AudioModalContainer));

View File

@ -1,4 +1,3 @@
import { showModal } from '/imports/ui/components/common/modal/service';
import Service from '../service'; import Service from '../service';
import Storage from '/imports/ui/services/storage/session'; import Storage from '/imports/ui/services/storage/session';
@ -38,7 +37,7 @@ export const joinMicrophone = (skipEchoTest = false) => {
}); });
return call.then(() => { return call.then(() => {
window.dispatchEvent(new Event("CLOSE_AUDIO_MODAL")); document.dispatchEvent(new Event("CLOSE_MODAL_AUDIO"));
}).catch((error) => { }).catch((error) => {
throw error; throw error;
}); });
@ -55,7 +54,7 @@ export const joinListenOnly = () => {
// prop transitions to a state where it was handled OR the user opts // prop transitions to a state where it was handled OR the user opts
// to close the modal. // to close the modal.
if (!Service.autoplayBlocked()) { if (!Service.autoplayBlocked()) {
window.dispatchEvent(new Event("CLOSE_AUDIO_MODAL")); document.dispatchEvent(new Event("CLOSE_MODAL_AUDIO"));
} }
resolve(); resolve();
}); });
@ -72,11 +71,11 @@ export const leaveEchoTest = () => {
return Service.exitAudio(); return Service.exitAudio();
}; };
export const closeModal = () => { export const closeModal = (callback) => {
if (Service.isConnecting()) { if (Service.isConnecting()) {
Service.forceExitAudio(); Service.forceExitAudio();
} }
showModal(null); callback();
}; };
export default { export default {

View File

@ -1,6 +1,6 @@
import styled, { css, keyframes } from 'styled-components'; import styled, { css, keyframes } from 'styled-components';
import Button from '/imports/ui/components/common/button/component'; import Button from '/imports/ui/components/common/button/component';
import Modal from '/imports/ui/components/common/modal/simple/component'; import ModalSimple from '/imports/ui/components/common/modal/simple/component';
import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints'; import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
import { colorPrimary } from '/imports/ui/stylesheets/styled-components/palette'; import { colorPrimary } from '/imports/ui/stylesheets/styled-components/palette';
import { import {
@ -90,7 +90,7 @@ const ConnectingAnimation = styled.span`
} }
`; `;
const AudioModal = styled(Modal)` const AudioModal = styled(ModalSimple)`
padding: 1rem; padding: 1rem;
min-height: 20rem; min-height: 20rem;
`; `;

View File

@ -2,7 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import Button from '/imports/ui/components/common/button/component'; import Button from '/imports/ui/components/common/button/component';
import { withModalMounter } from '/imports/ui/components/common/modal/service';
import AudioTestContainer from '/imports/ui/components/audio/audio-test/container'; import AudioTestContainer from '/imports/ui/components/audio/audio-test/container';
import Styled from './styles'; import Styled from './styles';
import logger from '/imports/startup/client/logger'; import logger from '/imports/startup/client/logger';
@ -375,4 +374,4 @@ class AudioSettings extends React.Component {
AudioSettings.propTypes = propTypes; AudioSettings.propTypes = propTypes;
AudioSettings.defaultProps = defaultProps; AudioSettings.defaultProps = defaultProps;
export default withModalMounter(injectIntl(AudioSettings)); export default injectIntl(AudioSettings);

View File

@ -2,7 +2,6 @@ import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withTracker } from 'meteor/react-meteor-data'; import { withTracker } from 'meteor/react-meteor-data';
import { Session } from 'meteor/session'; import { Session } from 'meteor/session';
import { withModalMounter } from '/imports/ui/components/common/modal/service';
import { injectIntl, defineMessages } from 'react-intl'; import { injectIntl, defineMessages } from 'react-intl';
import { range } from '/imports/utils/array-utils'; import { range } from '/imports/utils/array-utils';
import Auth from '/imports/ui/services/auth'; import Auth from '/imports/ui/services/auth';
@ -133,7 +132,24 @@ class AudioContainer extends PureComponent {
} }
render() { render() {
return null; const { isAudioModalOpen, setAudioModalIsOpen,
setVideoPreviewModalIsOpen, isVideoPreviewModalOpen } = this.props;
return <>
{isAudioModalOpen ? <AudioModalContainer
{...{
priority: "low",
setIsOpen: setAudioModalIsOpen,
isOpen: isAudioModalOpen
}}
/> : null}
{isVideoPreviewModalOpen ? <VideoPreviewContainer
{...{
priority: "low",
setIsOpen: setVideoPreviewModalIsOpen,
isOpen: isVideoPreviewModalOpen
}}
/> : null}
</>;
} }
} }
@ -163,7 +179,8 @@ const messages = {
}, },
}; };
export default lockContextContainer(withModalMounter(injectIntl(withTracker(({ mountModal, intl, userLocks }) => { export default lockContextContainer(injectIntl(withTracker(({ intl, userLocks, isAudioModalOpen, setAudioModalIsOpen,
setVideoPreviewModalIsOpen, isVideoPreviewModalOpen }) => {
const { microphoneConstraints } = Settings.application; const { microphoneConstraints } = Settings.application;
const autoJoin = getFromUserSettings('bbb_auto_join_audio', APP_CONFIG.autoJoin); const autoJoin = getFromUserSettings('bbb_auto_join_audio', APP_CONFIG.autoJoin);
const enableVideo = getFromUserSettings('bbb_enable_video', KURENTO_CONFIG.enableVideo); const enableVideo = getFromUserSettings('bbb_enable_video', KURENTO_CONFIG.enableVideo);
@ -174,14 +191,12 @@ export default lockContextContainer(withModalMounter(injectIntl(withTracker(({ m
const userSelectedListenOnly = didUserSelectedListenOnly(); const userSelectedListenOnly = didUserSelectedListenOnly();
const meetingIsBreakout = AppService.meetingIsBreakout(); const meetingIsBreakout = AppService.meetingIsBreakout();
const hasBreakoutRooms = AppService.getBreakoutRooms().length > 0; const hasBreakoutRooms = AppService.getBreakoutRooms().length > 0;
const openAudioModal = () => new Promise((resolve) => { const openAudioModal = () => setAudioModalIsOpen(true);
mountModal(<AudioModalContainer resolve={resolve} />);
});
const openVideoPreviewModal = () => new Promise((resolve) => { const openVideoPreviewModal = () => {
if (userWebcam) return resolve(); if (userWebcam) return;
mountModal(<VideoPreviewContainer resolve={resolve} />); setVideoPreviewModalIsOpen(true);
}); };
if (Service.isConnected() && !Service.isListenOnly()) { if (Service.isConnected() && !Service.isListenOnly()) {
Service.updateAudioConstraints(microphoneConstraints); Service.updateAudioConstraints(microphoneConstraints);
@ -208,11 +223,12 @@ export default lockContextContainer(withModalMounter(injectIntl(withTracker(({ m
return; return;
} }
setTimeout(() => openAudioModal().then(() => { setTimeout(() => {
if (enableVideo && autoShareWebcam) { openAudioModal();
openVideoPreviewModal(); if (enableVideo && autoShareWebcam) {
openVideoPreviewModal();
} }
}), 0); }, 0);
}, },
}); });
} }
@ -222,6 +238,8 @@ export default lockContextContainer(withModalMounter(injectIntl(withTracker(({ m
meetingIsBreakout, meetingIsBreakout,
userSelectedMicrophone, userSelectedMicrophone,
userSelectedListenOnly, userSelectedListenOnly,
isAudioModalOpen,
setAudioModalIsOpen,
init: async () => { init: async () => {
await Service.init(messages, intl); await Service.init(messages, intl);
const enableVideo = getFromUserSettings('bbb_enable_video', KURENTO_CONFIG.enableVideo); const enableVideo = getFromUserSettings('bbb_enable_video', KURENTO_CONFIG.enableVideo);
@ -234,7 +252,9 @@ export default lockContextContainer(withModalMounter(injectIntl(withTracker(({ m
} }
Session.set('audioModalIsOpen', true); Session.set('audioModalIsOpen', true);
if (enableVideo && autoShareWebcam) { if (enableVideo && autoShareWebcam) {
openAudioModal().then(() => { openVideoPreviewModal(); didMountAutoJoin = true; }); openAudioModal()
openVideoPreviewModal();
didMountAutoJoin = true;
} else if (!( } else if (!(
userSelectedMicrophone userSelectedMicrophone
&& userSelectedListenOnly && userSelectedListenOnly
@ -245,7 +265,7 @@ export default lockContextContainer(withModalMounter(injectIntl(withTracker(({ m
return Promise.resolve(true); return Promise.resolve(true);
}, },
}; };
})(AudioContainer)))); })(AudioContainer)));
AudioContainer.propTypes = { AudioContainer.propTypes = {
hasBreakoutRooms: PropTypes.bool.isRequired, hasBreakoutRooms: PropTypes.bool.isRequired,

View File

@ -129,13 +129,12 @@ class DeviceSelector extends Component {
} = this.props; } = this.props;
const { options } = this.state; const { options } = this.state;
const { isSafari } = browserInfo;
let notFoundOption; let notFoundOption;
if (blocked) { if (blocked) {
notFoundOption = <option value="finding">{intl.formatMessage(intlMessages.findingDevicesLabel)}</option>; notFoundOption = <option value="finding">{intl.formatMessage(intlMessages.findingDevicesLabel)}</option>;
} else if (kind === 'audiooutput' && isSafari) { } else if (kind === 'audiooutput' && !('setSinkId' in HTMLMediaElement.prototype)) {
const defaultOutputDeviceLabel = intl.formatMessage(intlMessages.defaultOutputDeviceLabel); const defaultOutputDeviceLabel = intl.formatMessage(intlMessages.defaultOutputDeviceLabel);
notFoundOption = <option value="not-found">{defaultOutputDeviceLabel}</option>; notFoundOption = <option value="not-found">{defaultOutputDeviceLabel}</option>;
} else { } else {

View File

@ -1,5 +1,5 @@
import styled, { css, keyframes } from 'styled-components'; import styled, { css, keyframes } from 'styled-components';
import Modal from '/imports/ui/components/common/modal/simple/component'; import ModalSimple from '/imports/ui/components/common/modal/simple/component';
import { colorBlack } from '/imports/ui/stylesheets/styled-components/palette'; import { colorBlack } from '/imports/ui/stylesheets/styled-components/palette';
import { jumboPaddingX } from '/imports/ui/stylesheets/styled-components/general'; import { jumboPaddingX } from '/imports/ui/stylesheets/styled-components/general';
@ -22,7 +22,7 @@ const bounce = keyframes`
} }
`; `;
const PermissionsOverlayModal = styled(Modal)` const PermissionsOverlayModal = styled(ModalSimple)`
${({ isFirefox }) => isFirefox && ` ${({ isFirefox }) => isFirefox && `
top: 8em; top: 8em;
left: 22em; left: 22em;

View File

@ -1,7 +1,6 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import { withModalMounter } from '/imports/ui/components/common/modal/service'; import ModalFullscreen from '/imports/ui/components/common/modal/fullscreen/component';
import Modal from '/imports/ui/components/common/modal/fullscreen/component';
import logger from '/imports/startup/client/logger'; import logger from '/imports/startup/client/logger';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import AudioService from '../audio/service'; import AudioService from '../audio/service';
@ -49,7 +48,6 @@ const propTypes = {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
breakout: PropTypes.objectOf(Object).isRequired, breakout: PropTypes.objectOf(Object).isRequired,
getURL: PropTypes.func.isRequired, getURL: PropTypes.func.isRequired,
mountModal: PropTypes.func.isRequired,
breakoutURL: PropTypes.string.isRequired, breakoutURL: PropTypes.string.isRequired,
isFreeJoin: PropTypes.bool.isRequired, isFreeJoin: PropTypes.bool.isRequired,
voiceUserJoined: PropTypes.bool.isRequired, voiceUserJoined: PropTypes.bool.isRequired,
@ -97,7 +95,7 @@ class BreakoutJoinConfirmation extends Component {
handleJoinBreakoutConfirmation() { handleJoinBreakoutConfirmation() {
const { const {
getURL, getURL,
mountModal, setIsOpen,
breakoutURL, breakoutURL,
isFreeJoin, isFreeJoin,
voiceUserJoined, voiceUserJoined,
@ -133,7 +131,7 @@ class BreakoutJoinConfirmation extends Component {
Session.set('lastBreakoutIdOpened', selectValue); Session.set('lastBreakoutIdOpened', selectValue);
window.open(url); window.open(url);
mountModal(null); setIsOpen(false);
} }
async fetchJoinURL(selectValue) { async fetchJoinURL(selectValue) {
@ -201,11 +199,13 @@ class BreakoutJoinConfirmation extends Component {
} }
render() { render() {
const { intl, breakoutName, isFreeJoin } = this.props; const { intl, breakoutName, isFreeJoin, setIsOpen,
isOpen, priority,
} = this.props;
const { waiting } = this.state; const { waiting } = this.state;
return ( return (
<Modal <ModalFullscreen
title={intl.formatMessage(intlMessages.title)} title={intl.formatMessage(intlMessages.title)}
confirm={{ confirm={{
callback: this.handleJoinBreakoutConfirmation, callback: this.handleJoinBreakoutConfirmation,
@ -215,16 +215,22 @@ class BreakoutJoinConfirmation extends Component {
disabled: waiting, disabled: waiting,
}} }}
dismiss={{ dismiss={{
callback: () => setIsOpen(false),
label: intl.formatMessage(intlMessages.dismissLabel), label: intl.formatMessage(intlMessages.dismissLabel),
description: intl.formatMessage(intlMessages.dismissDesc), description: intl.formatMessage(intlMessages.dismissDesc),
}} }}
{...{
setIsOpen,
isOpen,
priority,
}}
> >
{ isFreeJoin ? this.renderSelectMeeting() : `${intl.formatMessage(intlMessages.message)} ${breakoutName}?`} { isFreeJoin ? this.renderSelectMeeting() : `${intl.formatMessage(intlMessages.message)} ${breakoutName}?`}
</Modal> </ModalFullscreen>
); );
} }
} }
export default withModalMounter(injectIntl(BreakoutJoinConfirmation)); export default injectIntl(BreakoutJoinConfirmation);
BreakoutJoinConfirmation.propTypes = propTypes; BreakoutJoinConfirmation.propTypes = propTypes;

View File

@ -33,14 +33,13 @@ const requestJoinURL = (breakoutId) => {
}); });
}; };
export default withTracker(({ breakout, mountModal, breakoutName }) => { export default withTracker(({ breakout, breakoutName }) => {
const isFreeJoin = breakout.freeJoin; const isFreeJoin = breakout.freeJoin;
const { breakoutId } = breakout; const { breakoutId } = breakout;
const url = getURL(breakoutId); const url = getURL(breakoutId);
return { return {
isFreeJoin, isFreeJoin,
mountModal,
breakoutName, breakoutName,
breakoutURL: url, breakoutURL: url,
breakouts: breakoutService.getBreakouts(), breakouts: breakoutService.getBreakouts(),

View File

@ -1,8 +1,7 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import { withModalMounter } from '/imports/ui/components/common/modal/service';
import BBBMenu from "/imports/ui/components/common/menu/component"; import BBBMenu from "/imports/ui/components/common/menu/component";
import CreateBreakoutRoomModal from '/imports/ui/components/actions-bar/create-breakout-room/container'; import CreateBreakoutRoomContainer from '/imports/ui/components/actions-bar/create-breakout-room/container';
import Trigger from "/imports/ui/components/common/control-header/right/component"; import Trigger from "/imports/ui/components/common/control-header/right/component";
const intlMessages = defineMessages({ const intlMessages = defineMessages({
@ -27,6 +26,11 @@ const intlMessages = defineMessages({
class BreakoutDropdown extends PureComponent { class BreakoutDropdown extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = {
isCreateBreakoutRoomModalOpen: false,
};
this.setCreateBreakoutRoomModalIsOpen = this.setCreateBreakoutRoomModalIsOpen.bind(this);
} }
getAvailableActions() { getAvailableActions() {
@ -36,7 +40,6 @@ class BreakoutDropdown extends PureComponent {
endAllBreakouts, endAllBreakouts,
isMeteorConnected, isMeteorConnected,
amIModerator, amIModerator,
mountModal,
} = this.props; } = this.props;
this.menuItems = []; this.menuItems = [];
@ -58,9 +61,7 @@ class BreakoutDropdown extends PureComponent {
dataTest: 'openUpdateBreakoutUsersModal', dataTest: 'openUpdateBreakoutUsersModal',
label: intl.formatMessage(intlMessages.manageUsers), label: intl.formatMessage(intlMessages.manageUsers),
onClick: () => { onClick: () => {
mountModal( this.setCreateBreakoutRoomModalIsOpen(true);
<CreateBreakoutRoomModal isUpdate />
);
} }
} }
); );
@ -82,12 +83,19 @@ class BreakoutDropdown extends PureComponent {
return this.menuItems; return this.menuItems;
} }
setCreateBreakoutRoomModalIsOpen(value) {
this.setState({
isCreateBreakoutRoomModalOpen: value,
});
}
render() { render() {
const { const {
intl, intl,
isRTL, isRTL,
} = this.props; } = this.props;
const { isCreateBreakoutRoomModalOpen } = this.state;
return ( return (
<> <>
<BBBMenu <BBBMenu
@ -112,9 +120,18 @@ class BreakoutDropdown extends PureComponent {
}} }}
actions={this.getAvailableActions()} actions={this.getAvailableActions()}
/> />
{isCreateBreakoutRoomModalOpen ? <CreateBreakoutRoomContainer
{...{
isUpdate: true,
onRequestClose: () => this.setCreateBreakoutRoomModalIsOpen(false),
priority: "low",
setIsOpen: this.setCreateBreakoutRoomModalIsOpen,
isOpen: isCreateBreakoutRoomModalOpen
}}
/> : null}
</> </>
); );
} }
} }
export default withModalMounter(injectIntl(BreakoutDropdown)); export default injectIntl(BreakoutDropdown);

View File

@ -1,14 +1,12 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Session } from 'meteor/session'; import { Session } from 'meteor/session';
import { withModalMounter } from '/imports/ui/components/common/modal/service'; import BreakoutJoinConfirmationContainer from '/imports/ui/components/breakout-join-confirmation/container';
import BreakoutJoinConfirmation from '/imports/ui/components/breakout-join-confirmation/container';
import BreakoutService from '../service'; import BreakoutService from '../service';
const BREAKOUT_MODAL_DELAY = 200; const BREAKOUT_MODAL_DELAY = 200;
const propTypes = { const propTypes = {
mountModal: PropTypes.func.isRequired,
lastBreakoutReceived: PropTypes.shape({ lastBreakoutReceived: PropTypes.shape({
breakoutUrlData: PropTypes.object.isRequired, breakoutUrlData: PropTypes.object.isRequired,
}), }),
@ -26,20 +24,17 @@ const defaultProps = {
breakouts: [], breakouts: [],
}; };
const openBreakoutJoinConfirmation = (breakout, breakoutName, mountModal) => mountModal(
<BreakoutJoinConfirmation
breakout={breakout}
breakoutName={breakoutName}
/>,
);
class BreakoutRoomInvitation extends Component { class BreakoutRoomInvitation extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
didSendBreakoutInvite: false, didSendBreakoutInvite: false,
isBreakoutJoinConfirmationModalOpen: false,
breakout: null,
breakoutName: null,
}; };
this.setBreakoutJoinConfirmationModalIsOpen = this.setBreakoutJoinConfirmationModalIsOpen.bind(this);
} }
componentDidMount() { componentDidMount() {
@ -101,23 +96,43 @@ class BreakoutRoomInvitation extends Component {
inviteUserToBreakout(breakout) { inviteUserToBreakout(breakout) {
Session.set('lastBreakoutIdInvited', breakout.breakoutId); Session.set('lastBreakoutIdInvited', breakout.breakoutId);
const {
mountModal,
} = this.props;
// There's a race condition on page load with modals. Only one modal can be shown at a // There's a race condition on page load with modals. Only one modal can be shown at a
// time and new ones overwrite old ones. We delay the opening of the breakout modal // time and new ones overwrite old ones. We delay the opening of the breakout modal
// because it should always be on top if breakouts are running. // because it should always be on top if breakouts are running.
setTimeout(() => { setTimeout(() => {
openBreakoutJoinConfirmation.call(this, breakout, breakout.name, mountModal); this.setState({
breakout: breakout,
breakoutName: breakout.name,
})
this.setBreakoutJoinConfirmationModalIsOpen(true);
}, BREAKOUT_MODAL_DELAY); }, BREAKOUT_MODAL_DELAY);
} }
setBreakoutJoinConfirmationModalIsOpen(value) {
this.setState({
isBreakoutJoinConfirmationModalOpen: value,
});
}
render() { render() {
return null; const { isBreakoutJoinConfirmationModalOpen, breakout, breakoutName } = this.state;
return (<>
{isBreakoutJoinConfirmationModalOpen ? <BreakoutJoinConfirmationContainer
breakout={breakout}
breakoutName={breakoutName}
{...{
onRequestClose: () => this.setBreakoutJoinConfirmationModalIsOpen(false),
priority: "medium",
setIsOpen: this.setBreakoutJoinConfirmationModalIsOpen,
isOpen: isBreakoutJoinConfirmationModalOpen
}}
/> : null}
</>
);
} }
} }
BreakoutRoomInvitation.propTypes = propTypes; BreakoutRoomInvitation.propTypes = propTypes;
BreakoutRoomInvitation.defaultProps = defaultProps; BreakoutRoomInvitation.defaultProps = defaultProps;
export default withModalMounter(BreakoutRoomInvitation); export default BreakoutRoomInvitation;

View File

@ -1,15 +1,13 @@
import React from 'react'; import React from 'react';
import { withTracker } from 'meteor/react-meteor-data'; import { withTracker } from 'meteor/react-meteor-data';
import { withModalMounter } from '/imports/ui/components/common/modal/service';
import Service from '/imports/ui/components/captions/service'; import Service from '/imports/ui/components/captions/service';
import CaptionsReaderMenuContainer from '/imports/ui/components/captions/reader-menu/container';
import CaptionButton from './component'; import CaptionButton from './component';
const Container = (props) => <CaptionButton {...props} />; const Container = (props) => <CaptionButton {...props} />;
export default withModalMounter(withTracker(({ mountModal }) => ({ export default withTracker(({ setIsOpen }) => ({
isActive: Service.isCaptionsActive(), isActive: Service.isCaptionsActive(),
handleOnClick: () => (Service.isCaptionsActive() handleOnClick: () => (Service.isCaptionsActive()
? Service.deactivateCaptions() ? Service.deactivateCaptions()
: mountModal(<CaptionsReaderMenuContainer />)), : setIsOpen(true)),
}))(Container)); }))(Container);

View File

@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import Button from '/imports/ui/components/common/button/component'; import Button from '/imports/ui/components/common/button/component';
import ColorPicker from "./color-picker/component"; import ColorPicker from "./color-picker/component";
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import { withModalMounter } from '/imports/ui/components/common/modal/service';
import Styled from './styles'; import Styled from './styles';
const DEFAULT_VALUE = 'select'; const DEFAULT_VALUE = 'select';
@ -207,6 +206,8 @@ class ReaderMenu extends PureComponent {
intl, intl,
ownedLocales, ownedLocales,
closeModal, closeModal,
isOpen,
priority,
} = this.props; } = this.props;
const { const {
@ -231,6 +232,10 @@ class ReaderMenu extends PureComponent {
onRequestClose={closeModal} onRequestClose={closeModal}
hideBorder hideBorder
contentLabel={intl.formatMessage(intlMessages.title)} contentLabel={intl.formatMessage(intlMessages.title)}
{...{
isOpen,
priority,
}}
> >
<Styled.Title> <Styled.Title>
{intl.formatMessage(intlMessages.title)} {intl.formatMessage(intlMessages.title)}
@ -408,4 +413,4 @@ class ReaderMenu extends PureComponent {
ReaderMenu.propTypes = propTypes; ReaderMenu.propTypes = propTypes;
export default injectIntl(withModalMounter(ReaderMenu)); export default injectIntl(ReaderMenu);

View File

@ -1,14 +1,13 @@
import React from 'react'; import React from 'react';
import { withTracker } from 'meteor/react-meteor-data'; import { withTracker } from 'meteor/react-meteor-data';
import { withModalMounter } from '/imports/ui/components/common/modal/service';
import ReaderMenu from './component'; import ReaderMenu from './component';
import CaptionsService from '/imports/ui/components/captions/service'; import CaptionsService from '/imports/ui/components/captions/service';
const ReaderMenuContainer = (props) => <ReaderMenu {...props} />; const ReaderMenuContainer = (props) => <ReaderMenu {...props} />;
export default withModalMounter(withTracker(({ mountModal }) => ({ export default withTracker(({ setIsOpen }) => ({
closeModal: () => mountModal(null), closeModal: () => setIsOpen(false),
activateCaptions: (locale, settings) => CaptionsService.activateCaptions(locale, settings), activateCaptions: (locale, settings) => CaptionsService.activateCaptions(locale, settings),
getCaptionsSettings: () => CaptionsService.getCaptionsSettings(), getCaptionsSettings: () => CaptionsService.getCaptionsSettings(),
ownedLocales: CaptionsService.getOwnedLocales(), ownedLocales: CaptionsService.getOwnedLocales(),
}))(ReaderMenuContainer)); }))(ReaderMenuContainer);

View File

@ -1,6 +1,6 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints'; import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
import Modal from '/imports/ui/components/common/modal/simple/component'; import ModalSimple from '/imports/ui/components/common/modal/simple/component';
import { import {
colorGrayDark, colorGrayDark,
colorWhite, colorWhite,
@ -10,7 +10,7 @@ import {
} from '/imports/ui/stylesheets/styled-components/palette'; } from '/imports/ui/stylesheets/styled-components/palette';
import { borderSize, borderSizeLarge } from '/imports/ui/stylesheets/styled-components/general'; import { borderSize, borderSizeLarge } from '/imports/ui/stylesheets/styled-components/general';
const ReaderMenuModal = styled(Modal)` const ReaderMenuModal = styled(ModalSimple)`
padding: 1rem; padding: 1rem;
`; `;

View File

@ -1,6 +1,5 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import { withModalMounter } from '/imports/ui/components/common/modal/service';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Service from '/imports/ui/components/captions/service'; import Service from '/imports/ui/components/captions/service';
import LocalesDropdown from '/imports/ui/components/common/locales-dropdown/component'; import LocalesDropdown from '/imports/ui/components/common/locales-dropdown/component';
@ -44,7 +43,6 @@ const intlMessages = defineMessages({
const propTypes = { const propTypes = {
availableLocales: PropTypes.arrayOf(PropTypes.object).isRequired, availableLocales: PropTypes.arrayOf(PropTypes.object).isRequired,
closeModal: PropTypes.func.isRequired,
intl: PropTypes.shape({ intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired, formatMessage: PropTypes.func.isRequired,
}).isRequired, }).isRequired,
@ -68,9 +66,9 @@ class WriterMenu extends PureComponent {
} }
componentWillUnmount() { componentWillUnmount() {
const { closeModal } = this.props; const { setIsOpen } = this.props;
closeModal(); setIsOpen(false);
} }
handleChange(event) { handleChange(event) {
@ -79,7 +77,7 @@ class WriterMenu extends PureComponent {
handleStart() { handleStart() {
const { const {
closeModal, setIsOpen,
layoutContextDispatch, layoutContextDispatch,
} = this.props; } = this.props;
@ -95,24 +93,33 @@ class WriterMenu extends PureComponent {
value: PANELS.CAPTIONS, value: PANELS.CAPTIONS,
}); });
closeModal(); setIsOpen(false);
} }
render() { render() {
const { const {
intl, intl,
availableLocales, availableLocales,
closeModal, isOpen,
onRequestClose,
priority,
setIsOpen
} = this.props; } = this.props;
const { locale } = this.state; const { locale } = this.state;
return ( return (
<Styled.WriterMenuModal <Styled.WriterMenuModal
onRequestClose={closeModal}
hideBorder hideBorder
contentLabel={intl.formatMessage(intlMessages.title)} contentLabel={intl.formatMessage(intlMessages.title)}
title={intl.formatMessage(intlMessages.title)} title={intl.formatMessage(intlMessages.title)}
{...{
isOpen,
onRequestClose,
priority,
setIsOpen
}}
> >
<Styled.Content> <Styled.Content>
<span> <span>
@ -151,4 +158,4 @@ class WriterMenu extends PureComponent {
WriterMenu.propTypes = propTypes; WriterMenu.propTypes = propTypes;
export default injectIntl(withModalMounter(WriterMenu)); export default injectIntl(WriterMenu);

View File

@ -1,6 +1,5 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { withTracker } from 'meteor/react-meteor-data'; import { withTracker } from 'meteor/react-meteor-data';
import { withModalMounter } from '/imports/ui/components/common/modal/service';
import Service from '/imports/ui/components/captions/service'; import Service from '/imports/ui/components/captions/service';
import WriterMenu from './component'; import WriterMenu from './component';
import { layoutDispatch } from '../../layout/context'; import { layoutDispatch } from '../../layout/context';
@ -20,7 +19,6 @@ const WriterMenuContainer = (props) => {
return amIModerator && <WriterMenu {...{ layoutContextDispatch, ...props }} />; return amIModerator && <WriterMenu {...{ layoutContextDispatch, ...props }} />;
}; };
export default withModalMounter(withTracker(({ mountModal }) => ({ export default withTracker(() => ({
closeModal: () => mountModal(null),
availableLocales: Service.getAvailableLocales(), availableLocales: Service.getAvailableLocales(),
}))(WriterMenuContainer)); }))(WriterMenuContainer);

View File

@ -12,9 +12,9 @@ import {
colorPrimary, colorPrimary,
} from '/imports/ui/stylesheets/styled-components/palette'; } from '/imports/ui/stylesheets/styled-components/palette';
import Button from '/imports/ui/components/common/button/component'; import Button from '/imports/ui/components/common/button/component';
import Modal from '/imports/ui/components/common/modal/simple/component'; import ModalSimple from '/imports/ui/components/common/modal/simple/component';
const WriterMenuModal = styled(Modal)` const WriterMenuModal = styled(ModalSimple)`
min-height: 20rem; min-height: 20rem;
`; `;

View File

@ -1,6 +1,5 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import { withModalMounter } from '/imports/ui/components/common/modal/service';
import BBBMenu from '/imports/ui/components/common/menu/component'; import BBBMenu from '/imports/ui/components/common/menu/component';
import { getDateString, uniqueId } from '/imports/utils/string-utils'; import { getDateString, uniqueId } from '/imports/utils/string-utils';
import Trigger from '/imports/ui/components/common/control-header/right/component'; import Trigger from '/imports/ui/components/common/control-header/right/component';
@ -159,4 +158,4 @@ class ChatDropdown extends PureComponent {
} }
} }
export default withModalMounter(injectIntl(ChatDropdown)); export default injectIntl(ChatDropdown);

View File

@ -329,7 +329,6 @@ class TimeWindowList extends PureComponent {
}} }}
key="chat-list" key="chat-list"
data-test="chatMessages" data-test="chatMessages"
aria-live="polite"
ref={node => this.messageListWrapper = node} ref={node => this.messageListWrapper = node}
onCopy={(e) => { e.stopPropagation(); }} onCopy={(e) => { e.stopPropagation(); }}
> >

View File

@ -164,6 +164,7 @@ export default class ButtonBase extends React.Component {
'iconRight', 'iconRight',
'isVisualEffects', 'isVisualEffects',
'panning', 'panning',
'panSelected',
]; ];
return ( return (

View File

@ -44,6 +44,7 @@ const EmojiButton = styled.button`
overflow: hidden; overflow: hidden;
z-index: 2; z-index: 2;
border: none; border: none;
padding: 0;
[dir="rtl"] & { [dir="rtl"] & {
right: initial; right: initial;

View File

@ -123,6 +123,8 @@ export default class Button extends BaseButton {
'aria-label': ariaLabel, 'aria-label': ariaLabel,
'aria-expanded': ariaExpanded, 'aria-expanded': ariaExpanded,
tooltipLabel, tooltipLabel,
tooltipdelay,
tooltipplacement,
} = this.props; } = this.props;
const renderFuncName = circle ? 'renderCircle' : 'renderDefault'; const renderFuncName = circle ? 'renderCircle' : 'renderDefault';
@ -132,6 +134,8 @@ export default class Button extends BaseButton {
return ( return (
<TooltipContainer <TooltipContainer
title={tooltipLabel || buttonLabel} title={tooltipLabel || buttonLabel}
delay={tooltipdelay}
placement={tooltipplacement}
> >
{this[renderFuncName]()} {this[renderFuncName]()}
</TooltipContainer> </TooltipContainer>

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import ModalSimple from '/imports/ui/components/common/modal/simple/component'; import ModalSimple from '/imports/ui/components/common/modal/simple/component';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import { withModalMounter } from '/imports/ui/components/common/modal/service';
import FallbackView from '../fallback-view/component'; import FallbackView from '../fallback-view/component';
const intlMessages = defineMessages({ const intlMessages = defineMessages({
@ -11,14 +10,17 @@ const intlMessages = defineMessages({
}, },
}); });
const FallbackModal = ({ error, intl, mountModal }) => ( const FallbackModal = ({ error, intl }) => {
return (
<ModalSimple <ModalSimple
hideBorder hideBorder
onRequestClose={() => mountModal(null)} priority="medium"
shouldShowCloseButton={false}
contentLabel={intl.formatMessage(intlMessages.ariaTitle)} contentLabel={intl.formatMessage(intlMessages.ariaTitle)}
isOpen={!!error}
> >
<FallbackView {...{ error }} /> <FallbackView {...{ error }} />
</ModalSimple> </ModalSimple>
); )};
export default withModalMounter(injectIntl(FallbackModal)); export default injectIntl(FallbackModal);

View File

@ -1,91 +1,39 @@
import React, { Component } from 'react'; import React, { useCallback, useEffect } from 'react';
import PropTypes from 'prop-types';
import Styled from './styles'; import Styled from './styles';
import { registerTitleView, unregisterTitleView } from '/imports/utils/dom-utils';
const propTypes = { const BaseModal = (props) => {
overlayClassName: PropTypes.string.isRequired, const { setIsOpen, modalName, children,
portalClassName: PropTypes.string.isRequired, isOpen, onRequestClose, className, overlayClassName,
contentLabel: PropTypes.string.isRequired, } = props;
isOpen: PropTypes.bool.isRequired,
};
const defaultProps = { const closeEventHandler = useCallback (() => {
overlayClassName: 'modalOverlay', setIsOpen(false);
contentLabel: 'Modal', } , []);
isOpen: true, useEffect( () => {
}; // Only add event listener if name is specified
if(!modalName) return;
export default class ModalBase extends Component { const closeEventName = `CLOSE_MODAL_${modalName.toUpperCase()}`;
componentDidMount() { // Listen to close event on mount
registerTitleView(this.props.contentLabel); document.addEventListener(closeEventName, closeEventHandler);
}
componentWillUnmount() { // Remove listener on unmount
unregisterTitleView(); return () => {
} document.removeEventListener(closeEventName, closeEventHandler);
};
}, []);
const priority = props.priority ? props.priority : "low"
return (<Styled.BaseModal
portalClassName={`modal-${priority}`}
parentSelector={()=>document.querySelector('#modals-container')}
isOpen={isOpen}
onRequestClose={onRequestClose}
className={className}
overlayClassName={overlayClassName}
>
{children}
</Styled.BaseModal>
)}
render() { export default { BaseModal };
const {
isOpen,
'data-test': dataTest,
} = this.props;
if (!isOpen) return null;
return (
<Styled.BaseModal
{...this.props}
parentSelector={() => {
if (document.fullscreenElement &&
document.fullscreenElement.nodeName &&
document.fullscreenElement.nodeName.toLowerCase() === 'div')
return document.fullscreenElement;
else return document.body;
}}
data={{
test: dataTest ?? null,
}}
>
{this.props.children}
</Styled.BaseModal>
);
}
}
ModalBase.propTypes = propTypes;
ModalBase.defaultProps = defaultProps;
export const withModalState = ComponentToWrap =>
class ModalStateWrapper extends Component {
constructor(props) {
super(props);
this.state = {
isOpen: true,
};
this.hide = this.hide.bind(this);
this.show = this.show.bind(this);
}
hide(cb = () => { }) {
Promise.resolve(cb())
.then(() => this.setState({ isOpen: false }));
}
show(cb = () => { }) {
Promise.resolve(cb())
.then(() => this.setState({ isOpen: true }));
}
render() {
return (<ComponentToWrap
{...this.props}
modalHide={this.hide}
modalShow={this.show}
modalisOpen={this.state.isOpen}
/>);
}
};

View File

@ -1,6 +1,5 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { defineMessages } from 'react-intl'; import { defineMessages } from 'react-intl';
import { withModalMounter } from '/imports/ui/components/common/modal/service';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Styled from './styles'; import Styled from './styles';
@ -39,7 +38,7 @@ class ConfirmationModal extends Component {
render() { render() {
const { const {
intl, intl,
mountModal, setIsOpen,
onConfirm, onConfirm,
title, title,
titleMessageId, titleMessageId,
@ -51,6 +50,9 @@ class ConfirmationModal extends Component {
confirmParam, confirmParam,
disableConfirmButton, disableConfirmButton,
description, description,
isOpen,
onRequestClose,
priority,
} = this.props; } = this.props;
const { const {
@ -61,9 +63,14 @@ class ConfirmationModal extends Component {
return ( return (
<Styled.ConfirmationModal <Styled.ConfirmationModal
onRequestClose={() => mountModal(null)} onRequestClose={() => setIsOpen(false)}
contentLabel={title} contentLabel={title}
title={title || intl.formatMessage({ id: titleMessageId }, { 0: titleMessageExtra })} title={title || intl.formatMessage({ id: titleMessageId }, { 0: titleMessageExtra })}
{...{
isOpen,
onRequestClose,
priority,
}}
> >
<Styled.Container> <Styled.Container>
<Styled.Description> <Styled.Description>
@ -92,12 +99,12 @@ class ConfirmationModal extends Component {
data-test={confirmButtonDataTest} data-test={confirmButtonDataTest}
onClick={() => { onClick={() => {
onConfirm(confirmParam, checked); onConfirm(confirmParam, checked);
mountModal(null); setIsOpen(false);
}} }}
/> />
<Styled.CancelButton <Styled.CancelButton
label={intl.formatMessage(messages.noLabel)} label={intl.formatMessage(messages.noLabel)}
onClick={() => mountModal(null)} onClick={() => setIsOpen(false)}
/> />
</Styled.Footer> </Styled.Footer>
</Styled.Container> </Styled.Container>
@ -109,4 +116,4 @@ class ConfirmationModal extends Component {
ConfirmationModal.propTypes = propTypes; ConfirmationModal.propTypes = propTypes;
ConfirmationModal.defaultProps = defaultProps; ConfirmationModal.defaultProps = defaultProps;
export default withModalMounter(ConfirmationModal); export default ConfirmationModal;

View File

@ -1,6 +1,6 @@
import styled from 'styled-components'; import styled from 'styled-components';
import Button from '/imports/ui/components/common/button/component'; import Button from '/imports/ui/components/common/button/component';
import Modal from '/imports/ui/components/common/modal/simple/component'; import ModalSimple from '/imports/ui/components/common/modal/simple/component';
import { import {
smPaddingX, smPaddingX,
mdPaddingX, mdPaddingX,
@ -10,7 +10,7 @@ import {
import { colorGray } from '/imports/ui/stylesheets/styled-components/palette'; import { colorGray } from '/imports/ui/stylesheets/styled-components/palette';
import { lineHeightBase } from '/imports/ui/stylesheets/styled-components/typography'; import { lineHeightBase } from '/imports/ui/stylesheets/styled-components/typography';
const ConfirmationModal = styled(Modal)` const ConfirmationModal = styled(ModalSimple)`
padding: ${mdPaddingX}; padding: ${mdPaddingX};
`; `;

View File

@ -1,6 +0,0 @@
import { withTracker } from 'meteor/react-meteor-data';
import { getModal } from './service';
export default withTracker(() => ({
modalComponent: getModal(),
}))(({ modalComponent }) => modalComponent);

View File

@ -1,7 +1,6 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import { withModalState } from '../base/component';
import Styled from './styles'; import Styled from './styles';
const intlMessages = defineMessages({ const intlMessages = defineMessages({
@ -61,7 +60,7 @@ class ModalFullscreen extends PureComponent {
} }
handleAction(name) { handleAction(name) {
const { confirm, dismiss, modalHide } = this.props; const { confirm, dismiss } = this.props;
const { callback: callBackConfirm } = confirm; const { callback: callBackConfirm } = confirm;
const { callback: callBackDismiss } = dismiss; const { callback: callBackDismiss } = dismiss;
@ -78,7 +77,7 @@ class ModalFullscreen extends PureComponent {
break; break;
} }
return modalHide(callback); return callback();
} }
render() { render() {
@ -89,7 +88,7 @@ class ModalFullscreen extends PureComponent {
dismiss, dismiss,
className, className,
children, children,
modalisOpen, isOpen,
preventClosing, preventClosing,
...otherProps ...otherProps
} = this.props; } = this.props;
@ -103,7 +102,7 @@ class ModalFullscreen extends PureComponent {
return ( return (
<Styled.FullscreenModal <Styled.FullscreenModal
id="fsmodal" id="fsmodal"
isOpen={modalisOpen || preventClosing} isOpen={isOpen || preventClosing}
contentLabel={title} contentLabel={title}
overlayClassName={"fullscreenModalOverlay"} overlayClassName={"fullscreenModalOverlay"}
{...otherProps} {...otherProps}
@ -147,4 +146,4 @@ class ModalFullscreen extends PureComponent {
ModalFullscreen.propTypes = propTypes; ModalFullscreen.propTypes = propTypes;
ModalFullscreen.defaultProps = defaultProps; ModalFullscreen.defaultProps = defaultProps;
export default withModalState(injectIntl(ModalFullscreen)); export default injectIntl(ModalFullscreen);

View File

@ -1,5 +1,5 @@
import styled from 'styled-components'; import styled from 'styled-components';
import Styled from '../base/styles'; import Styled from '../base/component';
import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints'; import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
import Button from '/imports/ui/components/common/button/component'; import Button from '/imports/ui/components/common/button/component';
import { import {

View File

@ -3,7 +3,6 @@ import Styled from './styles';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
const propTypes = { const propTypes = {
children: PropTypes.node.isRequired,
hideBorder: PropTypes.bool, hideBorder: PropTypes.bool,
headerPosition: PropTypes.string, headerPosition: PropTypes.string,
shouldShowCloseButton: PropTypes.bool, shouldShowCloseButton: PropTypes.bool,

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Modal from '/imports/ui/components/common/modal/simple/component'; import ModalSimple from '/imports/ui/components/common/modal/simple/component';
import AudioService from '/imports/ui/components/audio/service'; import AudioService from '/imports/ui/components/audio/service';
import Styled from './styles'; import Styled from './styles';
@ -42,7 +42,6 @@ const propTypes = {
intl: PropTypes.shape({ intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired, formatMessage: PropTypes.func.isRequired,
}).isRequired, }).isRequired,
mountModal: PropTypes.func.isRequired,
numAvailableViewers: PropTypes.number.isRequired, numAvailableViewers: PropTypes.number.isRequired,
randomUserReq: PropTypes.func.isRequired, randomUserReq: PropTypes.func.isRequired,
}; };
@ -123,11 +122,13 @@ class RandomUserSelect extends Component {
keepModalOpen, keepModalOpen,
toggleKeepModalOpen, toggleKeepModalOpen,
intl, intl,
mountModal, setIsOpen,
numAvailableViewers, numAvailableViewers,
currentUser, currentUser,
clearRandomlySelectedUser, clearRandomlySelectedUser,
mappedRandomlySelectedUsers, mappedRandomlySelectedUsers,
isOpen,
priority,
} = this.props; } = this.props;
const counter = SELECT_RANDOM_USER_COUNTDOWN ? this.state.count : 0; const counter = SELECT_RANDOM_USER_COUNTDOWN ? this.state.count : 0;
@ -189,17 +190,21 @@ class RandomUserSelect extends Component {
} }
if (keepModalOpen) { if (keepModalOpen) {
return ( return (
<Modal <ModalSimple
onRequestClose={() => { onRequestClose={() => {
if (currentUser.presenter) clearRandomlySelectedUser(); if (currentUser.presenter) clearRandomlySelectedUser();
toggleKeepModalOpen(); toggleKeepModalOpen();
mountModal(null); setIsOpen(false);
}} }}
contentLabel={intl.formatMessage(messages.ariaModalTitle)} contentLabel={intl.formatMessage(messages.ariaModalTitle)}
title={title} title={title}
{...{
isOpen,
priority,
}}
> >
{viewElement} {viewElement}
</Modal> </ModalSimple>
); );
} else { } else {
return null; return null;

View File

@ -3,7 +3,6 @@ import { withTracker } from 'meteor/react-meteor-data';
import Meetings from '/imports/api/meetings'; import Meetings from '/imports/api/meetings';
import Users from '/imports/api/users'; import Users from '/imports/api/users';
import Auth from '/imports/ui/services/auth'; import Auth from '/imports/ui/services/auth';
import { withModalMounter } from '/imports/ui/components/common/modal/service';
import { makeCall } from '/imports/ui/services/api'; import { makeCall } from '/imports/ui/services/api';
import RandomUserSelect from './component'; import RandomUserSelect from './component';
import { UsersContext } from '/imports/ui/components/components-data/users-context/context'; import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
@ -56,12 +55,14 @@ const RandomUserSelectContainer = (props) => {
if (randomlySelectedUser) { if (randomlySelectedUser) {
mappedRandomlySelectedUsers = randomlySelectedUser.map((ui) => { mappedRandomlySelectedUsers = randomlySelectedUser.map((ui) => {
const selectedUser = users[Auth.meetingID][ui[0]]; const selectedUser = users[Auth.meetingID][ui[0]];
return [{ if (selectedUser){
userId: selectedUser.userId, return [{
avatar: selectedUser.avatar, userId: selectedUser.userId,
color: selectedUser.color, avatar: selectedUser.avatar,
name: selectedUser.name, color: selectedUser.color,
}, ui[1]]; name: selectedUser.name,
}, ui[1]];
}
}); });
} }
@ -74,7 +75,7 @@ const RandomUserSelectContainer = (props) => {
/> />
); );
}; };
export default withModalMounter(withTracker(({ mountModal }) => { export default withTracker(() => {
const viewerPool = Users.find({ const viewerPool = Users.find({
meetingId: Auth.meetingID, meetingId: Auth.meetingID,
presenter: { $ne: true }, presenter: { $ne: true },
@ -96,11 +97,10 @@ export default withModalMounter(withTracker(({ mountModal }) => {
const clearRandomlySelectedUser = () => (SELECT_RANDOM_USER_ENABLED ? makeCall('clearRandomlySelectedUser') : null); const clearRandomlySelectedUser = () => (SELECT_RANDOM_USER_ENABLED ? makeCall('clearRandomlySelectedUser') : null);
return ({ return ({
closeModal: () => mountModal(null),
toggleKeepModalOpen, toggleKeepModalOpen,
numAvailableViewers: viewerPool.length, numAvailableViewers: viewerPool.length,
randomUserReq, randomUserReq,
clearRandomlySelectedUser, clearRandomlySelectedUser,
randomlySelectedUser: meeting.randomlySelectedUser, randomlySelectedUser: meeting.randomlySelectedUser,
}); });
})(RandomUserSelectContainer)); })(RandomUserSelectContainer);

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