Merge remote-tracking branch 'upstream/v2.7.x-release' into 27-dev-apr24
This commit is contained in:
commit
b121fcbd87
4
.github/workflows/automated-tests.yml
vendored
4
.github/workflows/automated-tests.yml
vendored
@ -43,6 +43,7 @@ jobs:
|
||||
- run: ./build/setup.sh bbb-record-core
|
||||
- run: ./build/setup.sh bbb-web
|
||||
- run: ./build/setup.sh bbb-webrtc-sfu
|
||||
- run: ./build/setup.sh bbb-webrtc-recorder
|
||||
- run: ./build/setup.sh bigbluebutton
|
||||
- run: tar cvf artifacts.tar artifacts/
|
||||
- name: Archive packages
|
||||
@ -182,6 +183,7 @@ jobs:
|
||||
cp /etc/bigbluebutton/bigbluebutton-release configs/bigbluebutton-release
|
||||
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 /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 /etc/hosts /configs/hosts
|
||||
chmod a+r -R configs
|
||||
@ -197,4 +199,4 @@ jobs:
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: bbb-logs
|
||||
path: ./bbb-logs.tar.gz
|
||||
path: ./bbb-logs.tar.gz
|
||||
|
@ -12,7 +12,7 @@ stages:
|
||||
|
||||
# define which docker image to use for builds
|
||||
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.
|
||||
# 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-webhooks
|
||||
- bbb-webrtc-sfu
|
||||
- bbb-webrtc-recorder
|
||||
- freeswitch
|
||||
- bbb-pads
|
||||
- bbb-playback
|
||||
expire_in: 1h 30min
|
||||
|
||||
|
||||
# template job for build step
|
||||
.build_job:
|
||||
stage: build
|
||||
@ -170,6 +171,11 @@ bbb-webrtc-sfu-build:
|
||||
script:
|
||||
- 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:
|
||||
extends: .build_job
|
||||
script:
|
||||
@ -180,12 +186,12 @@ push_packages:
|
||||
stage: push packages
|
||||
script: build/push_packages.sh
|
||||
resource_group: push_packages
|
||||
|
||||
# uncomment the lines below if you want one final
|
||||
# "artifacts" dir with all packages (increases runtime, fills up space on gitlab server)
|
||||
|
||||
# uncomment the lines below if you want one final
|
||||
# "artifacts" dir with all packages (increases runtime, fills up space on gitlab server)
|
||||
#artifacts:
|
||||
# paths:
|
||||
# - artifacts/*
|
||||
# expire_in: 2 days
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -14,6 +14,7 @@ import org.bigbluebutton.SystemConfiguration
|
||||
import java.util.concurrent.TimeUnit
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.running.RunningMeeting
|
||||
import org.bigbluebutton.core.util.ColorPicker
|
||||
import org.bigbluebutton.core2.RunningMeetings
|
||||
import org.bigbluebutton.core2.message.senders.MsgBuilder
|
||||
import org.bigbluebutton.service.HealthzService
|
||||
@ -183,6 +184,9 @@ class BigBlueButtonActor(
|
||||
// Stop the meeting actor.
|
||||
context.stop(m.actorRef)
|
||||
}
|
||||
|
||||
//Remove ColorPicker idx of the meeting
|
||||
ColorPicker.reset(m.props.meetingProp.intId)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ package org.bigbluebutton.core.apps.users
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.models._
|
||||
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
|
||||
import org.bigbluebutton.core.util.ColorPicker
|
||||
import org.bigbluebutton.core2.message.senders.{ MsgBuilder, Sender }
|
||||
|
||||
trait RegisterUserReqMsgHdlr {
|
||||
@ -56,7 +57,7 @@ trait RegisterUserReqMsgHdlr {
|
||||
|
||||
val regUser = RegisteredUsers.create(msg.body.intUserId, msg.body.extUserId,
|
||||
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)
|
||||
|
||||
@ -89,7 +90,7 @@ trait RegisterUserReqMsgHdlr {
|
||||
val g = GuestApprovedVO(regUser.id, GuestStatus.ALLOW)
|
||||
UsersApp.approveOrRejectGuest(liveMeeting, outGW, g, SystemUser.ID)
|
||||
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)
|
||||
notifyModeratorsOfGuestWaiting(Vector(guest), liveMeeting.users2x, liveMeeting.props.meetingProp.intId)
|
||||
val notifyEvent = MsgBuilder.buildNotifyRoleInMeetingEvtMsg(
|
||||
|
@ -19,8 +19,8 @@ trait UserConnectedToGlobalAudioMsgHdlr {
|
||||
val header = BbbClientMsgHeader(UserJoinedVoiceConfToClientEvtMsg.NAME, props.meetingProp.intId, vu.intId)
|
||||
|
||||
val body = UserJoinedVoiceConfToClientEvtMsgBody(voiceConf = msg.header.voiceConf, intId = vu.intId, voiceUserId = vu.intId,
|
||||
callingWith = vu.callingWith, callerName = vu.callerName,
|
||||
callerNum = vu.callerNum, muted = true, talking = false, listenOnly = true)
|
||||
callingWith = vu.callingWith, callerName = vu.callerName, callerNum = vu.callerNum, color = vu.color,
|
||||
muted = true, talking = false, listenOnly = true)
|
||||
val event = UserJoinedVoiceConfToClientEvtMsg(header, body)
|
||||
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
|
||||
outGW.send(msgEvent)
|
||||
@ -36,6 +36,7 @@ trait UserConnectedToGlobalAudioMsgHdlr {
|
||||
callingWith = "flash",
|
||||
callerName = user.name,
|
||||
callerNum = user.name,
|
||||
color = user.color,
|
||||
muted = true,
|
||||
talking = false,
|
||||
listenOnly = true,
|
||||
|
@ -158,6 +158,7 @@ class UsersApp(
|
||||
with SelectRandomViewerReqMsgHdlr
|
||||
with AssignPresenterReqMsgHdlr
|
||||
with ChangeUserPinStateReqMsgHdlr
|
||||
with ChangeUserMobileFlagReqMsgHdlr
|
||||
with EjectUserFromMeetingCmdMsgHdlr
|
||||
with EjectUserFromMeetingSysMsgHdlr
|
||||
with MuteUserCmdMsgHdlr {
|
||||
|
@ -130,7 +130,7 @@ trait ValidateAuthTokenReqMsgHdlr extends HandlerHelpers {
|
||||
def sendAllVoiceUsersInMeeting(requesterId: String, voiceUsers: VoiceUsers, meetingId: String): Unit = {
|
||||
val vu = VoiceUsers.findAll(voiceUsers).map { u =>
|
||||
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)
|
||||
|
@ -14,7 +14,7 @@ trait SyncGetVoiceUsersMsgHdlr {
|
||||
def buildSyncGetVoiceUsersRespMsg(): BbbCommonEnvCoreMsg = {
|
||||
val voiceUsers = VoiceUsers.findAll(liveMeeting.voiceUsers).map { u =>
|
||||
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)
|
||||
|
@ -5,7 +5,7 @@ import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.running.{ LiveMeeting, MeetingActor, OutMsgRouter }
|
||||
import org.bigbluebutton.core2.message.senders.MsgBuilder
|
||||
import org.bigbluebutton.core.models._
|
||||
import org.bigbluebutton.core.apps.users.UsersApp
|
||||
import org.bigbluebutton.core.util.ColorPicker
|
||||
import org.bigbluebutton.core2.MeetingStatus2x
|
||||
|
||||
trait UserJoinedVoiceConfEvtMsgHdlr extends SystemConfiguration {
|
||||
@ -19,6 +19,8 @@ trait UserJoinedVoiceConfEvtMsgHdlr extends SystemConfiguration {
|
||||
val guestPolicy = GuestsWaiting.getGuestPolicy(liveMeeting.guestsWaiting)
|
||||
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 = {
|
||||
val moderators = Users2x.findAll(users).filter(p => p.role == Roles.MODERATOR_ROLE)
|
||||
moderators foreach { mod =>
|
||||
@ -32,7 +34,7 @@ trait UserJoinedVoiceConfEvtMsgHdlr extends SystemConfiguration {
|
||||
|
||||
def registerUserInRegisteredUsers() = {
|
||||
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)
|
||||
RegisteredUsers.add(liveMeeting.registeredUsers, regUser)
|
||||
}
|
||||
@ -48,9 +50,11 @@ trait UserJoinedVoiceConfEvtMsgHdlr extends SystemConfiguration {
|
||||
guestStatus = GuestStatus.WAIT,
|
||||
emoji = "none",
|
||||
pin = false,
|
||||
mobile = false,
|
||||
presenter = false,
|
||||
locked = MeetingStatus2x.getPermissions(liveMeeting.status).lockOnJoin,
|
||||
avatar = "",
|
||||
color = userColor,
|
||||
clientType = "",
|
||||
pickExempted = false,
|
||||
userLeftFlag = UserLeftFlag(false, 0)
|
||||
@ -60,7 +64,7 @@ trait UserJoinedVoiceConfEvtMsgHdlr extends SystemConfiguration {
|
||||
|
||||
def registerUserAsGuest() = {
|
||||
if (GuestsWaiting.findWithIntId(liveMeeting.guestsWaiting, msg.body.intId) == None) {
|
||||
val guest = GuestWaiting(msg.body.intId, msg.body.callerIdName, Roles.VIEWER_ROLE, true, "", true, System.currentTimeMillis())
|
||||
val guest = GuestWaiting(msg.body.intId, msg.body.callerIdName, Roles.VIEWER_ROLE, true, "", userColor, true, System.currentTimeMillis())
|
||||
GuestsWaiting.add(liveMeeting.guestsWaiting, guest)
|
||||
notifyModeratorsOfGuestWaiting(guest, liveMeeting.users2x, liveMeeting.props.meetingProp.intId)
|
||||
|
||||
@ -84,6 +88,7 @@ trait UserJoinedVoiceConfEvtMsgHdlr extends SystemConfiguration {
|
||||
msg.body.callingWith,
|
||||
msg.body.callerIdName,
|
||||
msg.body.callerIdNum,
|
||||
userColor,
|
||||
msg.body.muted,
|
||||
msg.body.talking,
|
||||
"freeswitch"
|
||||
|
@ -7,9 +7,10 @@ import org.bigbluebutton.core.bus.InternalEventBus
|
||||
import org.bigbluebutton.core2.MeetingStatus2x
|
||||
import org.bigbluebutton.core2.message.senders.MsgBuilder
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.running.{ LiveMeeting, MeetingActor, OutMsgRouter }
|
||||
import org.bigbluebutton.core.running.{LiveMeeting, MeetingActor, OutMsgRouter}
|
||||
import org.bigbluebutton.core.models._
|
||||
import org.bigbluebutton.core.apps.users.UsersApp
|
||||
import org.bigbluebutton.core.util.ColorPicker
|
||||
|
||||
object VoiceApp extends SystemConfiguration {
|
||||
|
||||
@ -164,6 +165,7 @@ object VoiceApp extends SystemConfiguration {
|
||||
cvu.callingWith,
|
||||
cvu.callerIdName,
|
||||
cvu.callerIdNum,
|
||||
ColorPicker.nextColor(liveMeeting.props.meetingProp.intId),
|
||||
cvu.muted,
|
||||
cvu.talking,
|
||||
cvu.calledInto
|
||||
@ -213,6 +215,7 @@ object VoiceApp extends SystemConfiguration {
|
||||
callingWith: String,
|
||||
callerIdName: String,
|
||||
callerIdNum: String,
|
||||
color: String,
|
||||
muted: Boolean,
|
||||
talking: Boolean,
|
||||
callingInto: String
|
||||
@ -240,6 +243,7 @@ object VoiceApp extends SystemConfiguration {
|
||||
voiceUserState.voiceUserId,
|
||||
voiceUserState.callerName,
|
||||
voiceUserState.callerNum,
|
||||
voiceUserState.color,
|
||||
voiceUserState.muted,
|
||||
voiceUserState.talking,
|
||||
voiceUserState.callingWith,
|
||||
@ -267,6 +271,7 @@ object VoiceApp extends SystemConfiguration {
|
||||
callingWith,
|
||||
callerIdName,
|
||||
callerIdNum,
|
||||
color,
|
||||
muted,
|
||||
talking,
|
||||
listenOnly = isListenOnly,
|
||||
|
@ -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)
|
||||
|
||||
object GuestPolicyType {
|
||||
|
@ -5,7 +5,7 @@ import org.bigbluebutton.core.domain.BreakoutRoom2x
|
||||
|
||||
object RegisteredUsers {
|
||||
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 = {
|
||||
new RegisteredUser(
|
||||
userId,
|
||||
@ -14,6 +14,7 @@ object RegisteredUsers {
|
||||
roles,
|
||||
token,
|
||||
avatar,
|
||||
color,
|
||||
guest,
|
||||
authenticated,
|
||||
guestStatus,
|
||||
@ -191,6 +192,7 @@ case class RegisteredUser(
|
||||
role: String,
|
||||
authToken: String,
|
||||
avatarURL: String,
|
||||
color: String,
|
||||
guest: Boolean,
|
||||
authed: Boolean,
|
||||
guestStatus: String,
|
||||
|
@ -107,6 +107,12 @@ object Users2x {
|
||||
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] = {
|
||||
for {
|
||||
_ <- users.remove(intId)
|
||||
@ -354,12 +360,14 @@ case class UserState(
|
||||
role: String,
|
||||
guest: Boolean,
|
||||
pin: Boolean,
|
||||
mobile: Boolean,
|
||||
authed: Boolean,
|
||||
guestStatus: String,
|
||||
emoji: String,
|
||||
locked: Boolean,
|
||||
presenter: Boolean,
|
||||
avatar: String,
|
||||
color: String,
|
||||
roleChangedOn: Long = System.currentTimeMillis(),
|
||||
lastActivityTime: Long = System.currentTimeMillis(),
|
||||
lastInactivityInspect: Long = 0,
|
||||
|
@ -174,6 +174,7 @@ case class VoiceUserState(
|
||||
callingWith: String,
|
||||
callerName: String,
|
||||
callerNum: String,
|
||||
color: String,
|
||||
muted: Boolean,
|
||||
talking: Boolean,
|
||||
listenOnly: Boolean,
|
||||
|
@ -109,6 +109,8 @@ class ReceivedJsonMsgHandlerActor(
|
||||
routeGenericMsg[UserActivitySignCmdMsg](envelope, jsonNode)
|
||||
case ChangeUserPinStateReqMsg.NAME =>
|
||||
routeGenericMsg[ChangeUserPinStateReqMsg](envelope, jsonNode)
|
||||
case ChangeUserMobileFlagReqMsg.NAME =>
|
||||
routeGenericMsg[ChangeUserMobileFlagReqMsg](envelope, jsonNode)
|
||||
case SelectRandomViewerReqMsg.NAME =>
|
||||
routeGenericMsg[SelectRandomViewerReqMsg](envelope, jsonNode)
|
||||
|
||||
|
@ -63,9 +63,11 @@ trait HandlerHelpers extends SystemConfiguration {
|
||||
guestStatus = regUser.guestStatus,
|
||||
emoji = "none",
|
||||
pin = false,
|
||||
mobile = false,
|
||||
presenter = false,
|
||||
locked = MeetingStatus2x.getPermissions(liveMeeting.status).lockOnJoin,
|
||||
avatar = regUser.avatarURL,
|
||||
color = regUser.color,
|
||||
clientType = clientType,
|
||||
pickExempted = false,
|
||||
userLeftFlag = UserLeftFlag(false, 0)
|
||||
|
@ -384,10 +384,11 @@ class MeetingActor(
|
||||
case m: RecordAndClearPreviousMarkersCmdMsg =>
|
||||
state = usersApp.handleRecordAndClearPreviousMarkersCmdMsg(m, state)
|
||||
updateUserLastActivity(m.body.setBy)
|
||||
case m: GetRecordingStatusReqMsg => usersApp.handleGetRecordingStatusReqMsg(m)
|
||||
case m: ChangeUserEmojiCmdMsg => handleChangeUserEmojiCmdMsg(m)
|
||||
case m: SelectRandomViewerReqMsg => usersApp.handleSelectRandomViewerReqMsg(m)
|
||||
case m: ChangeUserPinStateReqMsg => usersApp.handleChangeUserPinStateReqMsg(m)
|
||||
case m: GetRecordingStatusReqMsg => usersApp.handleGetRecordingStatusReqMsg(m)
|
||||
case m: ChangeUserEmojiCmdMsg => handleChangeUserEmojiCmdMsg(m)
|
||||
case m: SelectRandomViewerReqMsg => usersApp.handleSelectRandomViewerReqMsg(m)
|
||||
case m: ChangeUserPinStateReqMsg => usersApp.handleChangeUserPinStateReqMsg(m)
|
||||
case m: ChangeUserMobileFlagReqMsg => usersApp.handleChangeUserMobileFlagReqMsg(m)
|
||||
|
||||
// Client requested to eject user
|
||||
case m: EjectUserFromMeetingCmdMsg =>
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -13,5 +13,6 @@ object RandomStringGenerator {
|
||||
// Generate a random alphabnumeric string of length n
|
||||
def randomAlphanumericString(n: Int) =
|
||||
randomString("abcdefghijklmnopqrstuvwxyz0123456789")(n)
|
||||
|
||||
}
|
||||
|
||||
|
@ -77,6 +77,7 @@ class AnalyticsActor(val includeChat: Boolean) extends Actor with ActorLogging {
|
||||
case m: UserDisconnectedFromGlobalAudioMsg => logMessage(msg)
|
||||
case m: AssignPresenterReqMsg => logMessage(msg)
|
||||
case m: ChangeUserPinStateReqMsg => logMessage(msg)
|
||||
case m: ChangeUserMobileFlagReqMsg => logMessage(msg)
|
||||
case m: ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg => logMessage(msg)
|
||||
case m: ScreenshareRtmpBroadcastStoppedVoiceConfEvtMsg => logMessage(msg)
|
||||
case m: ScreenshareRtmpBroadcastStartedEvtMsg => logMessage(msg)
|
||||
|
@ -42,6 +42,7 @@ trait GuestsWaitingApprovedMsgHdlr extends HandlerHelpers with RightsManagementT
|
||||
"none",
|
||||
dialInUser.name,
|
||||
dialInUser.name,
|
||||
dialInUser.color,
|
||||
MeetingStatus2x.isMeetingMuted(liveMeeting.status),
|
||||
false,
|
||||
"freeswitch"
|
||||
|
@ -78,7 +78,7 @@ object MsgBuilder {
|
||||
val envelope = BbbCoreEnvelope(GetGuestsWaitingApprovalRespMsg.NAME, routing)
|
||||
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 event = GetGuestsWaitingApprovalRespMsg(header, body)
|
||||
|
||||
@ -90,7 +90,7 @@ object MsgBuilder {
|
||||
val envelope = BbbCoreEnvelope(GuestsWaitingForApprovalEvtMsg.NAME, routing)
|
||||
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 event = GuestsWaitingForApprovalEvtMsg(header, body)
|
||||
|
||||
|
@ -13,7 +13,7 @@ object UserJoinedMeetingEvtMsgBuilder {
|
||||
guestStatus = userState.guestStatus,
|
||||
emoji = userState.emoji,
|
||||
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)
|
||||
|
||||
val event = UserJoinedMeetingEvtMsg(meetingId, userState.intId, body)
|
||||
|
@ -20,13 +20,13 @@ trait FakeTestData {
|
||||
val guest1 = createUserVoiceAndCam(liveMeeting, Roles.VIEWER_ROLE, guest = true, authed = true, CallingWith.WEBRTC, muted = false,
|
||||
talking = false, listenOnly = false)
|
||||
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)
|
||||
|
||||
val guest2 = createUserVoiceAndCam(liveMeeting, Roles.VIEWER_ROLE, guest = true, authed = true, CallingWith.FLASH, muted = false,
|
||||
talking = false, listenOnly = false)
|
||||
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)
|
||||
|
||||
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 = {
|
||||
UserState(intId = regUser.id, extId = regUser.externId, name = regUser.name, role = regUser.role, pin = false,
|
||||
guest = regUser.guest, authed = regUser.authed, guestStatus = regUser.guestStatus,
|
||||
emoji = "none", locked = false, presenter = false, avatar = regUser.avatarURL, clientType = "unknown",
|
||||
mobile = false, guest = regUser.guest, authed = regUser.authed, guestStatus = regUser.guestStatus,
|
||||
emoji = "none", locked = false, presenter = false, avatar = regUser.avatarURL, color = "#ff6242", clientType = "unknown",
|
||||
pickExempted = false, userLeftFlag = UserLeftFlag(false, 0))
|
||||
}
|
||||
|
||||
|
@ -52,9 +52,10 @@ object FakeUserGenerator {
|
||||
val authToken = RandomStringGenerator.randomAlphanumericString(16)
|
||||
val avatarURL = "https://www." + RandomStringGenerator.randomAlphanumericString(32) + ".com/" +
|
||||
RandomStringGenerator.randomAlphanumericString(10) + ".png"
|
||||
val color = "#ff6242"
|
||||
|
||||
val ru = RegisteredUsers.create(userId = id, extId, name, role,
|
||||
authToken, avatarURL, guest, authed, guestStatus = GuestStatus.ALLOW, false, false)
|
||||
authToken, avatarURL, color, guest, authed, guestStatus = GuestStatus.ALLOW, false, false)
|
||||
RegisteredUsers.add(users, ru)
|
||||
ru
|
||||
}
|
||||
@ -64,7 +65,7 @@ object FakeUserGenerator {
|
||||
val voiceUserId = RandomStringGenerator.randomAlphanumericString(8)
|
||||
val lastFloorTime = System.currentTimeMillis().toString();
|
||||
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,
|
||||
@ -74,7 +75,7 @@ object FakeUserGenerator {
|
||||
val name = getRandomElement(firstNames, random) + " " + getRandomElement(lastNames, random)
|
||||
val lastFloorTime = System.currentTimeMillis().toString();
|
||||
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 = {
|
||||
|
@ -24,7 +24,7 @@ object TestDataGen {
|
||||
listenOnly: Boolean): VoiceUserState = {
|
||||
val voiceUserId = RandomStringGenerator.randomAlphanumericString(8)
|
||||
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,
|
||||
@ -32,7 +32,7 @@ object TestDataGen {
|
||||
val voiceUserId = RandomStringGenerator.randomAlphanumericString(8)
|
||||
val intId = "v_" + RandomStringGenerator.randomAlphanumericString(16)
|
||||
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 = {
|
||||
@ -43,8 +43,8 @@ object TestDataGen {
|
||||
def createUserFor(liveMeeting: LiveMeeting, regUser: RegisteredUser, presenter: Boolean): UserState = {
|
||||
val u = UserState(intId = regUser.id, extId = regUser.externId, name = regUser.name, role = regUser.role,
|
||||
guest = regUser.guest, authed = regUser.authed, guestStatus = regUser.guestStatus,
|
||||
emoji = "none", locked = false, presenter = false, avatar = regUser.avatarURL, clientType = "unknown",
|
||||
userLeftFlag = UserLeftFlag(false, 0))
|
||||
emoji = "none", locked = false, presenter = false, avatar = regUser.avatarURL, color = "#ff6242",
|
||||
clientType = "unknown", userLeftFlag = UserLeftFlag(false, 0))
|
||||
Users2x.add(liveMeeting.users2x, u)
|
||||
u
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ case class GetGuestsWaitingApprovalRespMsg(
|
||||
body: GetGuestsWaitingApprovalRespMsgBody
|
||||
) extends BbbCoreMsg
|
||||
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
|
||||
|
@ -88,11 +88,22 @@ case class UserJoinedMeetingEvtMsg(
|
||||
header: BbbClientMsgHeader,
|
||||
body: UserJoinedMeetingEvtMsgBody
|
||||
) extends BbbCoreMsg
|
||||
case class UserJoinedMeetingEvtMsgBody(intId: String, extId: String, name: String, role: String,
|
||||
guest: Boolean, authed: Boolean, guestStatus: String,
|
||||
emoji: String,
|
||||
pin: Boolean,
|
||||
presenter: Boolean, locked: Boolean, avatar: String, clientType: String)
|
||||
case class UserJoinedMeetingEvtMsgBody(
|
||||
intId: String,
|
||||
extId: String,
|
||||
name: 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.
|
||||
@ -189,6 +200,20 @@ object UserEmojiChangedEvtMsg { val NAME = "UserEmojiChangedEvtMsg" }
|
||||
case class UserEmojiChangedEvtMsg(header: BbbClientMsgHeader, body: UserEmojiChangedEvtMsgBody) extends BbbCoreMsg
|
||||
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" }
|
||||
case class AssignPresenterReqMsg(header: BbbClientMsgHeader, body: AssignPresenterReqMsgBody) extends StandardMsg
|
||||
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 GetVoiceUsersMeetingRespMsgBody(users: Vector[VoiceConfUser])
|
||||
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.
|
||||
|
@ -408,7 +408,7 @@ case class UserJoinedVoiceConfEvtMsgBody(voiceConf: String, voiceUserId: String,
|
||||
object UserJoinedVoiceConfToClientEvtMsg { val NAME = "UserJoinedVoiceConfToClientEvtMsg" }
|
||||
case class UserJoinedVoiceConfToClientEvtMsg(header: BbbClientMsgHeader, body: UserJoinedVoiceConfToClientEvtMsgBody) extends BbbCoreMsg
|
||||
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)
|
||||
|
||||
/**
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
set -ex
|
||||
RELEASE=4.0.1
|
||||
RELEASE=4.0.2
|
||||
cat <<MSG
|
||||
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}
|
||||
|
1
bbb-webrtc-recorder.placeholder.sh
Executable file
1
bbb-webrtc-recorder.placeholder.sh
Executable file
@ -0,0 +1 @@
|
||||
git clone --branch v0.2.0 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-recorder bbb-webrtc-recorder
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
HTML5_CONFIG=/usr/share/meteor/bundle/programs/server/assets/app/config/settings.yml
|
||||
BBB_WEB_CONFIG=$SERVLET_DIR/WEB-INF/classes/bigbluebutton.properties
|
||||
|
||||
HTML5_CONFIG=/etc/bigbluebutton/bbb-html5.yml
|
||||
if [ ! -f "${HTML5_CONFIG}" ]; then
|
||||
touch $HTML5_CONFIG
|
||||
fi
|
||||
|
||||
#
|
||||
# Enable Looging of the HTML5 client for debugging
|
||||
|
@ -154,6 +154,14 @@ get_bbb_web_config_value() {
|
||||
|
||||
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_ETC_CONFIG=/etc/bigbluebutton/bbb-html5.yml
|
||||
if [ -f $HTML5_ETC_CONFIG ]; then
|
||||
@ -407,16 +415,7 @@ display_bigbluebutton_status () {
|
||||
fi
|
||||
|
||||
if [ -f /usr/lib/systemd/system/bbb-html5.service ]; then
|
||||
units="$units mongod bbb-html5 bbb-webrtc-sfu kurento-media-server"
|
||||
|
||||
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
|
||||
units="$units mongod bbb-html5"
|
||||
|
||||
source /usr/share/meteor/bundle/bbb-html5-with-roles.conf
|
||||
|
||||
@ -433,6 +432,27 @@ display_bigbluebutton_status () {
|
||||
done
|
||||
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
|
||||
units="$units etherpad"
|
||||
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.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 "BigBlueButton's UDP port range is now $START_PORT-$END_PORT"
|
||||
echo
|
||||
@ -744,14 +767,18 @@ check_configuration() {
|
||||
# Look for properties with no values set
|
||||
#
|
||||
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
|
||||
if [ ! -f $file ]; then
|
||||
echo "# Error: File not found: $file"
|
||||
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 "# $(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
|
||||
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 " 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
|
||||
@ -1427,10 +1456,20 @@ if [ $CHECK ]; then
|
||||
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 " 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 " 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_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
|
||||
|
||||
if [ -n "$HTML5_CONFIG" ]; then
|
||||
|
@ -108,6 +108,14 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
background-color: rgba(66, 133, 244, 1) !important;
|
||||
color: #FFF !important;
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.fade-out {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
document.addEventListener('gesturestart', function (e) {
|
||||
@ -149,4 +157,5 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
<span id="destination"></span>
|
||||
<audio id="remote-media" autoplay>
|
||||
</audio>
|
||||
<div id="modals-container"></div>
|
||||
</body>
|
||||
|
@ -348,3 +348,9 @@
|
||||
.icon-bbb-closed_caption_stop:before {
|
||||
content: "\e966";
|
||||
}
|
||||
.icon-bbb-link:before {
|
||||
content: "\e967";
|
||||
}
|
||||
.icon-bbb-manage_layout:before {
|
||||
content: "\e968";
|
||||
}
|
||||
|
@ -38,3 +38,39 @@
|
||||
border: none;
|
||||
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;
|
||||
}
|
||||
|
@ -758,9 +758,18 @@ class SIPSession {
|
||||
|
||||
const setupRemoteMedia = () => {
|
||||
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.currentSession.sessionDescriptionHandler
|
||||
.peerConnection.getReceivers().forEach((receiver) => {
|
||||
if (receiver.track) {
|
||||
@ -792,22 +801,15 @@ class SIPSession {
|
||||
fsReady,
|
||||
},
|
||||
}, 'Audio call - check if ICE is finished and FreeSWITCH is ready');
|
||||
if (iceCompleted && fsReady) {
|
||||
|
||||
if (iceCompleted) {
|
||||
this.webrtcConnected = true;
|
||||
setupRemoteMedia();
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
if (fsReady) {
|
||||
this.callback({ status: this.baseCallStates.started, bridge: this.bridgeName });
|
||||
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
@ -1,15 +1,8 @@
|
||||
import stringHash from 'string-hash';
|
||||
import { check } from 'meteor/check';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import GuestUsers from '/imports/api/guest-users/';
|
||||
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) {
|
||||
const { guests } = body;
|
||||
check(guests, Array);
|
||||
@ -27,7 +20,6 @@ export default async function handleGuestsWaitingForApproval({ body }, meetingId
|
||||
meetingId,
|
||||
loginTime: guest.registeredOn,
|
||||
privateGuestLobbyMessage: '',
|
||||
color: COLOR_LIST[stringHash(guest.intId) % COLOR_LIST.length],
|
||||
});
|
||||
|
||||
if (insertedId) {
|
||||
|
@ -42,7 +42,7 @@ export default async function changeLayout(payload) {
|
||||
pushLayout: Boolean,
|
||||
presentationIsOpen: Boolean,
|
||||
isResizing: Boolean,
|
||||
cameraPosition: String,
|
||||
cameraPosition: Match.Maybe(String),
|
||||
focusedCamera: String,
|
||||
presentationVideoRate: Number,
|
||||
});
|
||||
|
@ -15,7 +15,7 @@ export default function setPushLayout(payload) {
|
||||
check(requesterUserId, String);
|
||||
|
||||
check(payload, {
|
||||
pushLayout: Boolean,
|
||||
pushLayout: Match.Maybe(Boolean),
|
||||
});
|
||||
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
|
||||
|
@ -8,6 +8,7 @@ import handleEmojiStatus from './handlers/emojiStatus';
|
||||
import handleChangeRole from './handlers/changeRole';
|
||||
import handleUserPinChanged from './handlers/userPinChanged';
|
||||
import handleUserInactivityInspect from './handlers/userInactivityInspect';
|
||||
import handleChangeMobileFlag from '/imports/api/users/server/handlers/changeMobileFlag';
|
||||
|
||||
RedisPubSub.on('PresenterAssignedEvtMsg', handlePresenterAssigned);
|
||||
RedisPubSub.on('UserJoinedMeetingEvtMsg', handleUserJoined);
|
||||
@ -15,6 +16,7 @@ RedisPubSub.on('UserLeftMeetingEvtMsg', handleRemoveUser);
|
||||
RedisPubSub.on('ValidateAuthTokenRespMsg', handleValidateAuthToken);
|
||||
RedisPubSub.on('UserEmojiChangedEvtMsg', handleEmojiStatus);
|
||||
RedisPubSub.on('UserRoleChangedEvtMsg', handleChangeRole);
|
||||
RedisPubSub.on('UserMobileFlagChangedEvtMsg', handleChangeMobileFlag);
|
||||
RedisPubSub.on('UserLeftFlagUpdatedEvtMsg', handleUserLeftFlagUpdated);
|
||||
RedisPubSub.on('UserPinStateChangedEvtMsg', handleUserPinChanged);
|
||||
RedisPubSub.on('UserInactivityInspectMsg', handleUserInactivityInspect);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -1,18 +1,27 @@
|
||||
import { check } from 'meteor/check';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import setMobile from '../modifiers/setMobile';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
|
||||
export default async function setMobileUser() {
|
||||
try {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'ChangeUserMobileFlagReqMsg';
|
||||
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
|
||||
const payload = {
|
||||
userId: requesterUserId,
|
||||
mobile: true,
|
||||
};
|
||||
|
||||
Logger.verbose(`Mobile user ${requesterUserId} from meeting ${meetingId}`);
|
||||
|
||||
await setMobile(meetingId, requesterUserId);
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
|
||||
} catch (err) {
|
||||
Logger.error(`Exception while invoking method setMobileUser ${err.stack}`);
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ export default async function addDialInUser(meetingId, voiceUser) {
|
||||
const USER_CONFIG = Meteor.settings.public.user;
|
||||
const ROLE_VIEWER = USER_CONFIG.role_viewer;
|
||||
|
||||
const { intId, callerName } = voiceUser;
|
||||
const { intId, callerName, color } = voiceUser;
|
||||
|
||||
const voiceOnlyUser = {
|
||||
intId,
|
||||
@ -23,6 +23,7 @@ export default async function addDialInUser(meetingId, voiceUser) {
|
||||
presenter: false,
|
||||
locked: false, // TODO
|
||||
avatar: '',
|
||||
color,
|
||||
pin: false,
|
||||
clientType: 'dial-in-user',
|
||||
};
|
||||
|
@ -4,18 +4,11 @@ import Users from '/imports/api/users';
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import VoiceUsers from '/imports/api/voice-users/';
|
||||
import addUserPsersistentData from '/imports/api/users-persistent-data/server/modifiers/addUserPersistentData';
|
||||
import stringHash from 'string-hash';
|
||||
import flat from 'flat';
|
||||
import { lowercaseTrim } from '/imports/utils/string-utils';
|
||||
|
||||
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) {
|
||||
const user = userData;
|
||||
|
||||
@ -34,6 +27,7 @@ export default async function addUser(meetingId, userData) {
|
||||
presenter: Boolean,
|
||||
locked: Boolean,
|
||||
avatar: String,
|
||||
color: String,
|
||||
pin: Boolean,
|
||||
clientType: String,
|
||||
});
|
||||
@ -46,14 +40,9 @@ export default async function addUser(meetingId, userData) {
|
||||
};
|
||||
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 = {
|
||||
meetingId,
|
||||
sortName: lowercaseTrim(user.name),
|
||||
color,
|
||||
speechLocale: '',
|
||||
mobile: false,
|
||||
breakoutProps: {
|
||||
@ -81,6 +70,7 @@ export default async function addUser(meetingId, userData) {
|
||||
intId: userId,
|
||||
callerName: user.name,
|
||||
callerNum: '',
|
||||
color: user.color,
|
||||
muted: false,
|
||||
talking: false,
|
||||
callingWith: '',
|
||||
|
@ -40,6 +40,7 @@ export default async function handleGetVoiceUsers({ body }, meetingId) {
|
||||
callerName: user.callerName,
|
||||
callerNum: user.callerNum,
|
||||
muted: user.muted,
|
||||
color: user.color,
|
||||
talking: user.talking,
|
||||
callingWith: user.callingWith,
|
||||
listenOnly: user.listenOnly,
|
||||
|
@ -3,7 +3,6 @@ import Users from '/imports/api/users';
|
||||
import addDialInUser from '/imports/api/users/server/modifiers/addDialInUser';
|
||||
import addVoiceUser from '../modifiers/addVoiceUser';
|
||||
|
||||
|
||||
export default async function handleJoinVoiceUser({ body }, meetingId) {
|
||||
const voiceUser = body;
|
||||
voiceUser.joined = true;
|
||||
@ -15,6 +14,7 @@ export default async function handleJoinVoiceUser({ body }, meetingId) {
|
||||
voiceUserId: String,
|
||||
callerName: String,
|
||||
callerNum: String,
|
||||
color: String,
|
||||
muted: Boolean,
|
||||
talking: Boolean,
|
||||
callingWith: String,
|
||||
|
@ -5,7 +5,6 @@ import removeVoiceUser from '../modifiers/removeVoiceUser';
|
||||
import updateVoiceUser from '../modifiers/updateVoiceUser';
|
||||
import addVoiceUser from '../modifiers/addVoiceUser';
|
||||
|
||||
|
||||
export default async function handleVoiceUsers({ header, body }) {
|
||||
const { voiceUsers } = body;
|
||||
const { meetingId } = header;
|
||||
@ -38,6 +37,7 @@ export default async function handleVoiceUsers({ header, body }) {
|
||||
intId: voice.intId,
|
||||
callerName: voice.callerName,
|
||||
callerNum: voice.callerNum,
|
||||
color: voice.color,
|
||||
muted: voice.muted,
|
||||
talking: voice.talking,
|
||||
callingWith: voice.callingWith,
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { check } from 'meteor/check';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import VoiceUsers from '/imports/api/voice-users';
|
||||
import Users from '/imports/api/users';
|
||||
import flat from 'flat';
|
||||
|
||||
export default async function addVoiceUser(meetingId, voiceUser) {
|
||||
@ -11,6 +10,7 @@ export default async function addVoiceUser(meetingId, voiceUser) {
|
||||
intId: String,
|
||||
callerName: String,
|
||||
callerNum: String,
|
||||
color: String,
|
||||
muted: Boolean,
|
||||
talking: Boolean,
|
||||
callingWith: String,
|
||||
@ -27,19 +27,12 @@ export default async function addVoiceUser(meetingId, voiceUser) {
|
||||
};
|
||||
|
||||
const modifier = {
|
||||
$set: Object.assign(
|
||||
{ meetingId, spoke: talking },
|
||||
flat(voiceUser),
|
||||
),
|
||||
};
|
||||
|
||||
const user = await Users.findOneAsync({ meetingId, userId: intId }, {
|
||||
fields: {
|
||||
color: 1,
|
||||
$set: {
|
||||
meetingId,
|
||||
spoke: talking,
|
||||
...flat(voiceUser),
|
||||
},
|
||||
});
|
||||
|
||||
if (user) modifier.$set.color = user.color;
|
||||
};
|
||||
|
||||
try {
|
||||
const { numberAffected } = await VoiceUsers.upsertAsync(selector, modifier);
|
||||
|
@ -2,8 +2,6 @@ import VoiceUsers from '/imports/api/voice-users';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import AuthTokenValidation, { ValidationStates } from '/imports/api/auth-token-validation';
|
||||
import ejectUserFromVoice from './methods/ejectUserFromVoice';
|
||||
import { debounce } from 'radash';
|
||||
|
||||
async function voiceUser() {
|
||||
const tokenValidation = await AuthTokenValidation
|
||||
@ -15,22 +13,8 @@ async function voiceUser() {
|
||||
}
|
||||
|
||||
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 });
|
||||
|
||||
this._session.socket.on('close', debounce({ delay: 100 }, onCloseConnection));
|
||||
return VoiceUsers.find({ meetingId });
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
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({
|
||||
title: {
|
||||
@ -38,7 +38,8 @@ const intlMessages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
const AboutComponent = ({ intl, settings }) => {
|
||||
const AboutComponent = (props) => {
|
||||
const { intl, settings, isOpen, onRequestClose, priority, } = props;
|
||||
const {
|
||||
html5ClientBuild,
|
||||
copyright,
|
||||
@ -54,20 +55,25 @@ const AboutComponent = ({ intl, settings }) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<ModalSimple
|
||||
data-test="aboutModalTitleLabel"
|
||||
title={intl.formatMessage(intlMessages.title)}
|
||||
dismiss={{
|
||||
label: intl.formatMessage(intlMessages.dismissLabel),
|
||||
description: intl.formatMessage(intlMessages.dismissDesc),
|
||||
}}
|
||||
{...{
|
||||
isOpen,
|
||||
onRequestClose,
|
||||
priority,
|
||||
}}
|
||||
>
|
||||
{`${intl.formatMessage(intlMessages.copyright)} ${copyright}`}
|
||||
<br />
|
||||
{`${intl.formatMessage(intlMessages.version)} ${html5ClientBuild}`}
|
||||
{displayBbbServerVersion ? showLabelVersion() : null}
|
||||
|
||||
</Modal>
|
||||
</ModalSimple>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages } from 'react-intl';
|
||||
import { withModalMounter } from '/imports/ui/components/common/modal/service';
|
||||
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
|
||||
import ExternalVideoModal from '/imports/ui/components/external-video-player/modal/container';
|
||||
import RandomUserSelectContainer from '/imports/ui/components/common/modal/random-user/container';
|
||||
@ -19,7 +18,6 @@ const propTypes = {
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
mountModal: PropTypes.func.isRequired,
|
||||
amIModerator: PropTypes.bool.isRequired,
|
||||
shortcuts: PropTypes.string,
|
||||
handleTakePresenter: PropTypes.func.isRequired,
|
||||
@ -109,22 +107,29 @@ class ActionsDropdown extends PureComponent {
|
||||
this.pollId = uniqueId('action-item-');
|
||||
this.takePresenterId = uniqueId('action-item-');
|
||||
this.selectUserRandId = uniqueId('action-item-');
|
||||
this.state = {
|
||||
isExternalVideoModalOpen: false,
|
||||
isRandomUserSelectModalOpen: false,
|
||||
isLayoutModalOpen: false,
|
||||
}
|
||||
|
||||
this.handleExternalVideoClick = this.handleExternalVideoClick.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) {
|
||||
const { amIPresenter: wasPresenter } = prevProps;
|
||||
const { amIPresenter: isPresenter, mountModal } = this.props;
|
||||
const { amIPresenter: isPresenter } = this.props;
|
||||
if (wasPresenter && !isPresenter) {
|
||||
mountModal(null);
|
||||
this.setExternalVideoModalIsOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
handleExternalVideoClick() {
|
||||
const { mountModal } = this.props;
|
||||
mountModal(<ExternalVideoModal />);
|
||||
this.setExternalVideoModalIsOpen(true);
|
||||
}
|
||||
|
||||
getAvailableActions() {
|
||||
@ -137,7 +142,6 @@ class ActionsDropdown extends PureComponent {
|
||||
isPollingEnabled,
|
||||
isSelectRandomUserEnabled,
|
||||
stopExternalVideoShare,
|
||||
mountModal,
|
||||
layoutContextDispatch,
|
||||
setMeetingLayout,
|
||||
setPushLayout,
|
||||
@ -216,7 +220,7 @@ class ActionsDropdown extends PureComponent {
|
||||
icon: "user",
|
||||
label: intl.formatMessage(intlMessages.selectRandUserLabel),
|
||||
key: this.selectUserRandId,
|
||||
onClick: () => mountModal(<RandomUserSelectContainer isSelectedUser={false} />),
|
||||
onClick: () => this.setRandomUserSelectModalIsOpen(true),
|
||||
dataTest: "selectRandomUser",
|
||||
})
|
||||
}
|
||||
@ -236,7 +240,7 @@ class ActionsDropdown extends PureComponent {
|
||||
icon: 'send',
|
||||
label: intl.formatMessage(intlMessages.layoutModal),
|
||||
key: 'layoutModal',
|
||||
onClick: () => mountModal(<LayoutModalContainer {...this.props} />),
|
||||
onClick: () => this.setLayoutModalIsOpen(true),
|
||||
dataTest: 'layoutModal',
|
||||
});
|
||||
}
|
||||
@ -281,6 +285,27 @@ class ActionsDropdown extends PureComponent {
|
||||
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() {
|
||||
const {
|
||||
intl,
|
||||
@ -290,8 +315,12 @@ class ActionsDropdown extends PureComponent {
|
||||
isDropdownOpen,
|
||||
isMobile,
|
||||
isRTL,
|
||||
isSelectRandomUserEnabled,
|
||||
} = this.props;
|
||||
|
||||
const { isExternalVideoModalOpen,
|
||||
isRandomUserSelectModalOpen, isLayoutModalOpen } = this.state;
|
||||
|
||||
const availableActions = this.getAvailableActions();
|
||||
const availablePresentations = this.makePresentationItems();
|
||||
const children = availablePresentations.length > 1 && amIPresenter
|
||||
@ -305,35 +334,43 @@ class ActionsDropdown extends PureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<BBBMenu
|
||||
customStyles={!isMobile ? customStyles : null}
|
||||
accessKey={OPEN_ACTIONS_AK}
|
||||
trigger={
|
||||
<Styled.HideDropdownButton
|
||||
open={isDropdownOpen}
|
||||
hideLabel
|
||||
aria-label={intl.formatMessage(intlMessages.actionsLabel)}
|
||||
data-test="actionsButton"
|
||||
label={intl.formatMessage(intlMessages.actionsLabel)}
|
||||
icon="plus"
|
||||
color="primary"
|
||||
size="lg"
|
||||
circle
|
||||
onClick={() => null}
|
||||
/>
|
||||
}
|
||||
actions={children}
|
||||
opts={{
|
||||
id: "actions-dropdown-menu",
|
||||
keepMounted: true,
|
||||
transitionDuration: 0,
|
||||
elevation: 3,
|
||||
getContentAnchorEl: null,
|
||||
fullwidth: "true",
|
||||
anchorOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' },
|
||||
transformOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' },
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<BBBMenu
|
||||
customStyles={!isMobile ? customStyles : null}
|
||||
accessKey={OPEN_ACTIONS_AK}
|
||||
trigger={
|
||||
<Styled.HideDropdownButton
|
||||
open={isDropdownOpen}
|
||||
hideLabel
|
||||
aria-label={intl.formatMessage(intlMessages.actionsLabel)}
|
||||
data-test="actionsButton"
|
||||
label={intl.formatMessage(intlMessages.actionsLabel)}
|
||||
icon="plus"
|
||||
color="primary"
|
||||
size="lg"
|
||||
circle
|
||||
onClick={() => null}
|
||||
/>
|
||||
}
|
||||
actions={children}
|
||||
opts={{
|
||||
id: "actions-dropdown-menu",
|
||||
keepMounted: true,
|
||||
transitionDuration: 0,
|
||||
elevation: 3,
|
||||
getContentAnchorEl: null,
|
||||
fullwidth: "true",
|
||||
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.defaultProps = defaultProps;
|
||||
|
||||
export default withShortcutHelper(withModalMounter(ActionsDropdown), 'openActions');
|
||||
export default withShortcutHelper(ActionsDropdown, 'openActions');
|
||||
|
@ -4,6 +4,7 @@ import deviceInfo from '/imports/utils/deviceInfo';
|
||||
import Styled from './styles';
|
||||
import ActionsDropdown from './actions-dropdown/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 AudioControlsContainer from '../audio/audio-controls/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';
|
||||
|
||||
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() {
|
||||
const {
|
||||
amIPresenter,
|
||||
@ -41,8 +56,10 @@ class ActionsBar extends PureComponent {
|
||||
setPushLayout,
|
||||
} = this.props;
|
||||
|
||||
const shouldShowOptionsButton = (isPresentationEnabled() && isThereCurrentPresentation)
|
||||
|| isSharingVideo || hasScreenshare || isSharedNotesPinned;
|
||||
const { isCaptionsReaderMenuModalOpen } = this.state;
|
||||
|
||||
const shouldShowOptionsButton = (isPresentationEnabled() && isThereCurrentPresentation)
|
||||
|| isSharingVideo || hasScreenshare || isSharedNotesPinned;
|
||||
return (
|
||||
<Styled.ActionsBar
|
||||
style={
|
||||
@ -71,7 +88,20 @@ class ActionsBar extends PureComponent {
|
||||
/>
|
||||
{isCaptionsAvailable
|
||||
? (
|
||||
<CaptionsButtonContainer {...{ intl }} />
|
||||
<>
|
||||
<CaptionsButtonContainer {...{ intl,
|
||||
setIsOpen: this.setCaptionsReaderMenuModalIsOpen,}} />
|
||||
{
|
||||
isCaptionsReaderMenuModalOpen ? <CaptionsReaderMenuContainer
|
||||
{...{
|
||||
onRequestClose: () => this.setCaptionsReaderMenuModalIsOpen(false),
|
||||
priority: "low",
|
||||
setIsOpen: this.setCaptionsReaderMenuModalIsOpen,
|
||||
isOpen: isCaptionsReaderMenuModalOpen,
|
||||
}}
|
||||
/> : null
|
||||
}
|
||||
</>
|
||||
)
|
||||
: null}
|
||||
{ !deviceInfo.isMobile
|
||||
|
@ -5,8 +5,7 @@ import { range } from '/imports/utils/array-utils';
|
||||
import deviceInfo from '/imports/utils/deviceInfo';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
import { Session } from 'meteor/session';
|
||||
import Modal from '/imports/ui/components/common/modal/fullscreen/component';
|
||||
import { withModalMounter } from '/imports/ui/components/common/modal/service';
|
||||
import ModalFullscreen from '/imports/ui/components/common/modal/fullscreen/component';
|
||||
import SortList from './sort-user-list/component';
|
||||
import Styled from './styles';
|
||||
import Icon from '/imports/ui/components/common/icon/component';
|
||||
@ -192,7 +191,6 @@ const propTypes = {
|
||||
getUsersNotJoined: PropTypes.func.isRequired,
|
||||
getBreakouts: PropTypes.func.isRequired,
|
||||
sendInvitation: PropTypes.func.isRequired,
|
||||
mountModal: PropTypes.func.isRequired,
|
||||
isBreakoutRecordable: PropTypes.bool,
|
||||
};
|
||||
|
||||
@ -411,10 +409,10 @@ class BreakoutRoom extends PureComponent {
|
||||
}
|
||||
|
||||
handleDismiss() {
|
||||
const { mountModal } = this.props;
|
||||
const { setIsOpen } = this.props;
|
||||
setPresentationVisibility('block');
|
||||
return new Promise((resolve) => {
|
||||
mountModal(null);
|
||||
setIsOpen(false);
|
||||
|
||||
this.setState({
|
||||
preventClosing: false,
|
||||
@ -1317,7 +1315,7 @@ class BreakoutRoom extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, isUpdate } = this.props;
|
||||
const { intl, isUpdate, isOpen, priority, setIsOpen, } = this.props;
|
||||
const {
|
||||
preventClosing,
|
||||
leastOneUserIsValid,
|
||||
@ -1330,7 +1328,7 @@ class BreakoutRoom extends PureComponent {
|
||||
const { isMobile } = deviceInfo;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<ModalFullscreen
|
||||
title={
|
||||
isUpdate
|
||||
? intl.formatMessage(intlMessages.updateTitle)
|
||||
@ -1357,16 +1355,21 @@ class BreakoutRoom extends PureComponent {
|
||||
: intl.formatMessage(intlMessages.dismissLabel),
|
||||
}}
|
||||
preventClosing={preventClosing}
|
||||
{...{
|
||||
isOpen,
|
||||
priority,
|
||||
setIsOpen,
|
||||
}}
|
||||
>
|
||||
<Styled.Content>
|
||||
{this.renderTitle()}
|
||||
{isMobile ? this.renderMobile() : this.renderDesktop()}
|
||||
</Styled.Content>
|
||||
</Modal>
|
||||
</ModalFullscreen>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BreakoutRoom.propTypes = propTypes;
|
||||
|
||||
export default withModalMounter(injectIntl(BreakoutRoom));
|
||||
export default injectIntl(BreakoutRoom);
|
||||
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
|
||||
|
||||
const propTypes = {
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
@ -47,7 +48,8 @@ const PresentationOptionsContainer = ({
|
||||
buttonType = 'desktop';
|
||||
}
|
||||
|
||||
const isThereCurrentPresentation = hasExternalVideo || hasScreenshare || hasPresentation || hasPinnedSharedNotes;
|
||||
const isThereCurrentPresentation = hasExternalVideo || hasScreenshare
|
||||
|| hasPresentation || hasPinnedSharedNotes;
|
||||
return (
|
||||
<Button
|
||||
icon={`${buttonType}${!presentationIsOpen ? '_off' : ''}`}
|
||||
@ -59,7 +61,12 @@ const PresentationOptionsContainer = ({
|
||||
hideLabel
|
||||
circle
|
||||
size="lg"
|
||||
onClick={() => setPresentationIsOpen(layoutContextDispatch, !presentationIsOpen)}
|
||||
onClick={() => {
|
||||
setPresentationIsOpen(layoutContextDispatch, !presentationIsOpen);
|
||||
if (!hasExternalVideo && !hasScreenshare && !hasPinnedSharedNotes) {
|
||||
Session.set('presentationLastState', !presentationIsOpen);
|
||||
}
|
||||
}}
|
||||
id="restore-presentation"
|
||||
ghost={!presentationIsOpen}
|
||||
disabled={!isThereCurrentPresentation}
|
||||
|
@ -1,11 +1,10 @@
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import deviceInfo from '/imports/utils/deviceInfo';
|
||||
import browserInfo from '/imports/utils/browserInfo';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import { notify } from '/imports/ui/services/notification';
|
||||
import { withModalMounter } from '/imports/ui/components/common/modal/service';
|
||||
import Styled from './styles';
|
||||
import ScreenshareBridgeService from '/imports/api/screenshare/client/bridge/service';
|
||||
import {
|
||||
@ -14,6 +13,7 @@ import {
|
||||
} from '/imports/ui/components/screenshare/service';
|
||||
import { SCREENSHARING_ERRORS } from '/imports/api/screenshare/client/bridge/errors';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
import { parsePayloads } from 'sdp-transform';
|
||||
|
||||
const { isMobile } = deviceInfo;
|
||||
const { isSafari, isTabletApp } = browserInfo;
|
||||
@ -117,8 +117,6 @@ const ScreenshareButton = ({
|
||||
isVideoBroadcasting,
|
||||
amIPresenter,
|
||||
isMeteorConnected,
|
||||
screenshareDataSavingSetting,
|
||||
mountModal,
|
||||
}) => {
|
||||
// This is the failure callback that will be passed to the /api/screenshare/kurento.js
|
||||
// script on the presenter's call
|
||||
@ -141,18 +139,19 @@ const ScreenshareButton = ({
|
||||
screenshareHasEnded();
|
||||
};
|
||||
|
||||
const renderScreenshareUnavailableModal = () => mountModal(
|
||||
const [isScreenshareUnavailableModalOpen, setScreenshareUnavailableModalIsOpen] = useState(false);
|
||||
|
||||
const RenderScreenshareUnavailableModal = (otherProps) =>
|
||||
<Styled.ScreenShareModal
|
||||
onRequestClose={() => mountModal(null)}
|
||||
hideBorder
|
||||
contentLabel={intl.formatMessage(intlMessages.screenShareUnavailable)}
|
||||
{...otherProps}
|
||||
>
|
||||
<Styled.Title>
|
||||
{intl.formatMessage(intlMessages.screenShareUnavailable)}
|
||||
</Styled.Title>
|
||||
<p>{intl.formatMessage(intlMessages.screenShareNotSupported)}</p>
|
||||
</Styled.ScreenShareModal>,
|
||||
);
|
||||
</Styled.ScreenShareModal>;
|
||||
|
||||
const screenshareLabel = intlMessages.desktopShareLabel;
|
||||
|
||||
@ -168,32 +167,46 @@ const ScreenshareButton = ({
|
||||
|
||||
const dataTest = isVideoBroadcasting ? 'stopScreenShare' : 'startScreenShare';
|
||||
|
||||
return shouldAllowScreensharing
|
||||
? (
|
||||
<Button
|
||||
disabled={(!isMeteorConnected && !isVideoBroadcasting)}
|
||||
icon={isVideoBroadcasting ? 'desktop' : 'desktop_off'}
|
||||
data-test={dataTest}
|
||||
label={intl.formatMessage(vLabel)}
|
||||
description={intl.formatMessage(vDescr)}
|
||||
color={isVideoBroadcasting ? 'primary' : 'default'}
|
||||
ghost={!isVideoBroadcasting}
|
||||
hideLabel
|
||||
circle
|
||||
size="lg"
|
||||
onClick={isVideoBroadcasting
|
||||
? screenshareHasEnded
|
||||
: () => {
|
||||
if (isSafari && !ScreenshareBridgeService.HAS_DISPLAY_MEDIA) {
|
||||
renderScreenshareUnavailableModal();
|
||||
} else {
|
||||
shareScreen(amIPresenter, handleFailure);
|
||||
}
|
||||
}}
|
||||
id={isVideoBroadcasting ? 'unshare-screen-button' : 'share-screen-button'}
|
||||
/>
|
||||
) : null;
|
||||
return <>
|
||||
{
|
||||
shouldAllowScreensharing
|
||||
? (
|
||||
<Button
|
||||
disabled={(!isMeteorConnected && !isVideoBroadcasting)}
|
||||
icon={isVideoBroadcasting ? 'desktop' : 'desktop_off'}
|
||||
data-test={dataTest}
|
||||
label={intl.formatMessage(vLabel)}
|
||||
description={intl.formatMessage(vDescr)}
|
||||
color={isVideoBroadcasting ? 'primary' : 'default'}
|
||||
ghost={!isVideoBroadcasting}
|
||||
hideLabel
|
||||
circle
|
||||
size="lg"
|
||||
onClick={isVideoBroadcasting
|
||||
? screenshareHasEnded
|
||||
: () => {
|
||||
if (isSafari && !ScreenshareBridgeService.HAS_DISPLAY_MEDIA) {
|
||||
setScreenshareUnavailableModalIsOpen(true);
|
||||
} else {
|
||||
shareScreen(amIPresenter, handleFailure);
|
||||
}
|
||||
}}
|
||||
id={isVideoBroadcasting ? 'unshare-screen-button' : 'share-screen-button'}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
isScreenshareUnavailableModalOpen ? <RenderScreenshareUnavailableModal
|
||||
{...{
|
||||
onRequestClose: () => setScreenshareUnavailableModalIsOpen(false),
|
||||
priority: "low",
|
||||
setIsOpen: setScreenshareUnavailableModalIsOpen,
|
||||
isOpen: isScreenshareUnavailableModalOpen,
|
||||
}}
|
||||
/> : null
|
||||
}
|
||||
</>
|
||||
};
|
||||
|
||||
ScreenshareButton.propTypes = propTypes;
|
||||
export default withModalMounter(injectIntl(memo(ScreenshareButton)));
|
||||
export default injectIntl(memo(ScreenshareButton));
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import { withModalMounter } from '/imports/ui/components/common/modal/service';
|
||||
import ScreenshareButton from './component';
|
||||
import { isScreenSharingEnabled } from '/imports/ui/services/features';
|
||||
import {
|
||||
@ -18,8 +17,8 @@ const ScreenshareButtonContainer = (props) => <ScreenshareButton {...props} />;
|
||||
* isMeteorConnected,
|
||||
* screenshareDataSavingSetting,
|
||||
*/
|
||||
export default withModalMounter(withTracker(() => ({
|
||||
export default withTracker(() => ({
|
||||
isVideoBroadcasting: isVideoBroadcasting(),
|
||||
screenshareDataSavingSetting: dataSavingSetting(),
|
||||
enabled: isScreenSharingEnabled(),
|
||||
}))(ScreenshareButtonContainer));
|
||||
}))(ScreenshareButtonContainer);
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 {
|
||||
jumboPaddingY,
|
||||
@ -9,7 +9,7 @@ import {
|
||||
} from '/imports/ui/stylesheets/styled-components/general';
|
||||
import { fontSizeLarge } from '/imports/ui/stylesheets/styled-components/typography';
|
||||
|
||||
const ScreenShareModal = styled(Modal)`
|
||||
const ScreenShareModal = styled(ModalSimple)`
|
||||
padding: ${jumboPaddingY};
|
||||
min-height: ${minModalHeight};
|
||||
text-align: center;
|
||||
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { defineMessages } from 'react-intl';
|
||||
|
||||
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 { Meteor } from 'meteor/meteor';
|
||||
@ -92,7 +92,7 @@ class ActivityCheck extends Component {
|
||||
const { responseDelay } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<ModalSimple
|
||||
hideBorder
|
||||
onRequestClose={handleInactivityDismiss}
|
||||
shouldCloseOnOverlayClick={false}
|
||||
@ -110,7 +110,7 @@ class ActivityCheck extends Component {
|
||||
size="lg"
|
||||
/>
|
||||
</Styled.ActivityModalContent>
|
||||
</Modal>
|
||||
</ModalSimple>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { throttle } from '/imports/utils/throttle';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Modal from 'react-modal';
|
||||
import ReactModal from 'react-modal';
|
||||
import browserInfo from '/imports/utils/browserInfo';
|
||||
import deviceInfo from '/imports/utils/deviceInfo';
|
||||
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 ToastContainer from '/imports/ui/components/common/toast/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 AudioContainer from '../audio/container';
|
||||
import ChatAlertContainer from '../chat/alert/container';
|
||||
@ -135,10 +134,16 @@ class App extends Component {
|
||||
super(props);
|
||||
this.state = {
|
||||
enableResize: !window.matchMedia(MOBILE_MEDIA).matches,
|
||||
isAudioModalOpen: false,
|
||||
isRandomUserSelectModalOpen: false,
|
||||
isVideoPreviewModalOpen: false,
|
||||
};
|
||||
|
||||
this.handleWindowResize = throttle(this.handleWindowResize).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(),
|
||||
50, { trailing: true, leading: true }).bind(this);
|
||||
@ -162,7 +167,7 @@ class App extends Component {
|
||||
value: isRTL,
|
||||
});
|
||||
|
||||
Modal.setAppElement('#app');
|
||||
ReactModal.setAppElement('#app');
|
||||
|
||||
const fontSize = isMobile() ? MOBILE_FONT_SIZE : DESKTOP_FONT_SIZE;
|
||||
document.getElementsByTagName('html')[0].style.fontSize = fontSize;
|
||||
@ -220,7 +225,6 @@ class App extends Component {
|
||||
notify,
|
||||
currentUserEmoji,
|
||||
intl,
|
||||
mountModal,
|
||||
deviceType,
|
||||
mountRandomUserModal,
|
||||
selectedLayout,
|
||||
@ -228,12 +232,11 @@ class App extends Component {
|
||||
layoutContextDispatch,
|
||||
numCameras,
|
||||
presentationIsOpen,
|
||||
ignorePollNotifications,
|
||||
} = this.props;
|
||||
|
||||
this.renderDarkMode();
|
||||
|
||||
if (mountRandomUserModal) mountModal(<RandomUserSelectContainer />);
|
||||
if (mountRandomUserModal) this.setRandomUserSelectModalIsOpen(true);
|
||||
|
||||
if (prevProps.currentUserEmoji.status !== currentUserEmoji.status) {
|
||||
const formattedEmojiStatus = intl.formatMessage({ id: `app.actionsBar.emojiMenu.${currentUserEmoji.status}Label` })
|
||||
@ -506,12 +509,26 @@ class App extends Component {
|
||||
setMeetingLayout,
|
||||
setPushLayout,
|
||||
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() {
|
||||
const {
|
||||
customStyle,
|
||||
@ -528,6 +545,7 @@ class App extends Component {
|
||||
darkTheme,
|
||||
} = this.props;
|
||||
|
||||
const { isAudioModalOpen, isRandomUserSelectModalOpen, isVideoPreviewModalOpen } = this.state;
|
||||
return (
|
||||
<>
|
||||
<Notifications />
|
||||
@ -571,7 +589,12 @@ class App extends Component {
|
||||
<UploaderContainer />
|
||||
<CaptionsSpeechContainer />
|
||||
<BreakoutRoomInvitation />
|
||||
<AudioContainer />
|
||||
<AudioContainer {...{
|
||||
isAudioModalOpen,
|
||||
setAudioModalIsOpen: this.setAudioModalIsOpen,
|
||||
isVideoPreviewModalOpen,
|
||||
setVideoPreviewModalIsOpen: this.setVideoPreviewModalIsOpen,
|
||||
}} />
|
||||
<ToastContainer rtl />
|
||||
{(audioAlertEnabled || pushAlertEnabled)
|
||||
&& (
|
||||
@ -583,11 +606,18 @@ class App extends Component {
|
||||
<StatusNotifier status="raiseHand" />
|
||||
<ManyWebcamsNotifier />
|
||||
<PollingContainer />
|
||||
<ModalContainer />
|
||||
<PadsSessionsContainer />
|
||||
{this.renderActionsBar()}
|
||||
{customStyleUrl ? <link rel="stylesheet" type="text/css" href={customStyleUrl} /> : null}
|
||||
{customStyle ? <link rel="stylesheet" type="text/css" href={`data:text/css;charset=UTF-8,${encodeURIComponent(customStyle)}`} /> : null}
|
||||
{isRandomUserSelectModalOpen ? <RandomUserSelectContainer
|
||||
{...{
|
||||
onRequestClose: () => this.setRandomUserSelectModalIsOpen(false),
|
||||
priority: "low",
|
||||
setIsOpen: this.setRandomUserSelectModalIsOpen,
|
||||
isOpen: isRandomUserSelectModalOpen,
|
||||
}}
|
||||
/> : null}
|
||||
</Styled.Layout>
|
||||
</>
|
||||
);
|
||||
|
@ -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 Auth from '/imports/ui/services/auth';
|
||||
import Users from '/imports/api/users';
|
||||
@ -31,8 +31,6 @@ import {
|
||||
validIOSVersion,
|
||||
} from './service';
|
||||
|
||||
import { withModalMounter, getModal } from '/imports/ui/components/common/modal/service';
|
||||
|
||||
import App from './component';
|
||||
|
||||
const CUSTOM_STYLE_URL = Meteor.settings.public.app.customStyleUrl;
|
||||
@ -116,10 +114,14 @@ const AppContainer = (props) => {
|
||||
|
||||
const prevRandomUser = usePrevious(randomlySelectedUser);
|
||||
|
||||
const mountRandomUserModal = !isPresenter
|
||||
&& !isEqual(prevRandomUser, randomlySelectedUser)
|
||||
&& randomlySelectedUser.length > 0
|
||||
&& !isModalOpen;
|
||||
const [mountRandomUserModal, setMountRandomUserModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMountRandomUserModal(!isPresenter
|
||||
&& !isEqual(prevRandomUser, randomlySelectedUser)
|
||||
&& randomlySelectedUser.length > 0
|
||||
&& !isModalOpen);
|
||||
}, [isPresenter, prevRandomUser, randomlySelectedUser, isModalOpen]);
|
||||
|
||||
const setPushLayout = () => {
|
||||
LayoutService.setPushLayout(pushLayout);
|
||||
@ -176,6 +178,7 @@ const AppContainer = (props) => {
|
||||
sidebarContentIsOpen,
|
||||
shouldShowPresentation,
|
||||
mountRandomUserModal,
|
||||
setMountRandomUserModal,
|
||||
isPresenter,
|
||||
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({
|
||||
removed(userData) {
|
||||
// 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),
|
||||
hideActionsBar: getFromUserSettings('bbb_hide_actions_bar', false),
|
||||
isModalOpen: !!getModal(),
|
||||
ignorePollNotifications: Session.get('ignorePollNotifications'),
|
||||
};
|
||||
})(AppContainer));
|
||||
})(AppContainer);
|
||||
|
@ -7,6 +7,7 @@ import InputStreamLiveSelectorContainer from './input-stream-live-selector/conta
|
||||
import MutedAlert from '/imports/ui/components/muted-alert/component';
|
||||
import Styled from './styles';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
import AudioModalContainer from '../audio-modal/container';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
joinAudio: {
|
||||
@ -30,7 +31,6 @@ const intlMessages = defineMessages({
|
||||
const propTypes = {
|
||||
shortcuts: PropTypes.objectOf(PropTypes.string).isRequired,
|
||||
handleToggleMuteMicrophone: PropTypes.func.isRequired,
|
||||
handleJoinAudio: PropTypes.func.isRequired,
|
||||
handleLeaveAudio: PropTypes.func.isRequired,
|
||||
disable: PropTypes.bool.isRequired,
|
||||
muted: PropTypes.bool.isRequired,
|
||||
@ -46,21 +46,28 @@ const propTypes = {
|
||||
class AudioControls extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isAudioModalOpen: false,
|
||||
};
|
||||
|
||||
this.renderButtonsAndStreamSelector = this.renderButtonsAndStreamSelector.bind(this);
|
||||
this.renderJoinLeaveButton = this.renderJoinLeaveButton.bind(this);
|
||||
this.setAudioModalIsOpen = this.setAudioModalIsOpen.bind(this);
|
||||
}
|
||||
|
||||
renderJoinButton() {
|
||||
const {
|
||||
handleJoinAudio,
|
||||
disable,
|
||||
intl,
|
||||
shortcuts,
|
||||
joinListenOnly,
|
||||
isConnected
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={handleJoinAudio}
|
||||
onClick={() => this.handleJoinAudio(joinListenOnly, isConnected)}
|
||||
disabled={disable}
|
||||
hideLabel
|
||||
aria-label={intl.formatMessage(intlMessages.joinAudio)}
|
||||
@ -119,6 +126,16 @@ class AudioControls extends PureComponent {
|
||||
return this.renderJoinButton();
|
||||
}
|
||||
|
||||
handleJoinAudio(joinListenOnly, isConnected) {
|
||||
(isConnected()
|
||||
? joinListenOnly()
|
||||
: this.setAudioModalIsOpen(true)
|
||||
)}
|
||||
|
||||
setAudioModalIsOpen(value) {
|
||||
this.setState({ isAudioModalOpen: value })
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
showMute,
|
||||
@ -130,6 +147,8 @@ class AudioControls extends PureComponent {
|
||||
isPresenter,
|
||||
} = this.props;
|
||||
|
||||
const { isAudioModalOpen } = this.state;
|
||||
|
||||
const MUTE_ALERT_CONFIG = Meteor.settings.public.app.mutedAlert;
|
||||
const { enabled: muteAlertEnabled } = MUTE_ALERT_CONFIG;
|
||||
|
||||
@ -144,6 +163,15 @@ class AudioControls extends PureComponent {
|
||||
{
|
||||
this.renderJoinLeaveButton()
|
||||
}
|
||||
{
|
||||
isAudioModalOpen ? <AudioModalContainer
|
||||
{...{
|
||||
priority: "low",
|
||||
setIsOpen: this.setAudioModalIsOpen,
|
||||
isOpen: isAudioModalOpen
|
||||
}}
|
||||
/> : null
|
||||
}
|
||||
</Styled.Container>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
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 lockContextContainer from '/imports/ui/components/lock-viewers/context/container';
|
||||
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 getFromUserSettings from '/imports/ui/services/users-settings';
|
||||
import AudioControls from './component';
|
||||
import AudioModalContainer from '../audio-modal/container';
|
||||
import {
|
||||
setUserSelectedMicrophone,
|
||||
setUserSelectedListenOnly,
|
||||
@ -65,7 +63,7 @@ const {
|
||||
|
||||
export default withUsersConsumer(
|
||||
lockContextContainer(
|
||||
withModalMounter(withTracker(({ mountModal, userLocks, users }) => {
|
||||
withTracker(({ userLocks, users }) => {
|
||||
const currentUser = users[Auth.meetingID][Auth.userID];
|
||||
const isViewer = currentUser.role === ROLE_VIEWER;
|
||||
const isPresenter = currentUser.presenter;
|
||||
@ -87,15 +85,13 @@ export default withUsersConsumer(
|
||||
talking: isTalking() && !isMuted(),
|
||||
isVoiceUser: isVoiceUser(),
|
||||
handleToggleMuteMicrophone: () => toggleMuteMicrophone(),
|
||||
handleJoinAudio: () => (isConnected()
|
||||
? joinListenOnly()
|
||||
: mountModal(<AudioModalContainer />)
|
||||
),
|
||||
joinListenOnly,
|
||||
handleLeaveAudio,
|
||||
inputStream: AudioManager.inputStream,
|
||||
isViewer,
|
||||
isPresenter,
|
||||
isConnected,
|
||||
});
|
||||
})(AudioControlsContainer)),
|
||||
})(AudioControlsContainer),
|
||||
),
|
||||
);
|
||||
|
@ -14,6 +14,7 @@ const AUDIO_INPUT = 'audioinput';
|
||||
const AUDIO_OUTPUT = 'audiooutput';
|
||||
const DEFAULT_DEVICE = 'default';
|
||||
const DEVICE_LABEL_MAX_LENGTH = 40;
|
||||
const SET_SINK_ID_SUPPORTED = 'setSinkId' in HTMLMediaElement.prototype;
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
changeAudioDevice: {
|
||||
@ -52,6 +53,10 @@ const intlMessages = defineMessages({
|
||||
id: 'app.audioNotification.deviceChangeFailed',
|
||||
description: 'Device change failed',
|
||||
},
|
||||
defaultOutputDeviceLabel: {
|
||||
id: 'app.audio.audioSettings.defaultOutputDeviceLabel',
|
||||
description: 'Default output device label',
|
||||
},
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
@ -263,8 +268,10 @@ class InputStreamLiveSelector extends Component {
|
||||
},
|
||||
];
|
||||
|
||||
const deviceList = (listLength > 0)
|
||||
? list.map((device, index) => (
|
||||
let deviceList = [];
|
||||
|
||||
if (listLength > 0) {
|
||||
deviceList = list.map((device, index) => (
|
||||
{
|
||||
key: `${device.deviceId}-${deviceKind}`,
|
||||
dataTest: `${deviceKind}-${index + 1}`,
|
||||
@ -273,8 +280,21 @@ class InputStreamLiveSelector extends Component {
|
||||
iconRight: (device.deviceId === currentDeviceId) ? 'check' : null,
|
||||
onClick: () => this.onDeviceListClick(device.deviceId, deviceKind, callback),
|
||||
}
|
||||
))
|
||||
: [
|
||||
));
|
||||
} else if (deviceKind === AUDIO_OUTPUT && !SET_SINK_ID_SUPPORTED && listLength === 0) {
|
||||
// If the browser doesn't support setSinkId, show the chosen output device
|
||||
// as a placeholder Default - like it's done in audio/device-selector
|
||||
deviceList = [
|
||||
{
|
||||
key: `defaultDeviceKey-${deviceKind}`,
|
||||
label: intl.formatMessage(intlMessages.defaultOutputDeviceLabel),
|
||||
customStyles: Styled.SelectedLabel,
|
||||
iconRight: 'check',
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
deviceList = [
|
||||
{
|
||||
key: `noDeviceFoundKey-${deviceKind}-`,
|
||||
label: listLength < 0
|
||||
@ -282,6 +302,8 @@ class InputStreamLiveSelector extends Component {
|
||||
: intl.formatMessage(intlMessages.noDeviceFound),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return listTitle.concat(deviceList);
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,6 @@ import AudioDial from '../audio-dial/component';
|
||||
import AudioAutoplayPrompt from '../autoplay/component';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import CaptionsSelectContainer from '/imports/ui/components/audio/captions/select/container';
|
||||
import { showModal } from '/imports/ui/components/common/modal/service';
|
||||
|
||||
const propTypes = {
|
||||
intl: PropTypes.shape({
|
||||
@ -177,7 +176,6 @@ class AudioModal extends Component {
|
||||
audioLocked,
|
||||
isUsingAudio,
|
||||
} = this.props;
|
||||
window.addEventListener("CLOSE_AUDIO_MODAL", this.handleCloseAudioModal);
|
||||
|
||||
if (!isUsingAudio) {
|
||||
if (forceListenOnlyAttendee || audioLocked) return this.handleJoinListenOnly();
|
||||
@ -212,7 +210,6 @@ class AudioModal extends Component {
|
||||
exitAudio();
|
||||
}
|
||||
if (resolve) resolve();
|
||||
window.removeEventListener("CLOSE_AUDIO_MODAL", this.handleCloseAudioModal);
|
||||
Session.set('audioModalIsOpen', false);
|
||||
}
|
||||
|
||||
@ -252,10 +249,6 @@ class AudioModal extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
handleCloseAudioModal = () => {
|
||||
showModal(null);
|
||||
}
|
||||
|
||||
handleGoToEchoTest() {
|
||||
const { AudioError } = this.props;
|
||||
const { MIC_ERROR } = AudioError;
|
||||
@ -599,6 +592,9 @@ class AudioModal extends Component {
|
||||
showPermissionsOvelay,
|
||||
closeModal,
|
||||
isIE,
|
||||
isOpen,
|
||||
priority,
|
||||
setIsOpen,
|
||||
} = this.props;
|
||||
|
||||
const { content } = this.state;
|
||||
@ -607,6 +603,7 @@ class AudioModal extends Component {
|
||||
<span>
|
||||
{showPermissionsOvelay ? <PermissionsOverlay closeModal={closeModal} /> : null}
|
||||
<Styled.AudioModal
|
||||
modalName="AUDIO"
|
||||
onRequestClose={closeModal}
|
||||
data-test="audioModal"
|
||||
contentLabel={intl.formatMessage(intlMessages.ariaModalTitle)}
|
||||
@ -619,6 +616,11 @@ class AudioModal extends Component {
|
||||
)
|
||||
: null
|
||||
}
|
||||
{...{
|
||||
setIsOpen,
|
||||
isOpen,
|
||||
priority,
|
||||
}}
|
||||
>
|
||||
{isIE ? (
|
||||
<Styled.BrowserWarning>
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import { withModalMounter } from '/imports/ui/components/common/modal/service';
|
||||
import browserInfo from '/imports/utils/browserInfo';
|
||||
import getFromUserSettings from '/imports/ui/services/users-settings';
|
||||
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 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 forceListenOnly = getFromUserSettings('bbb_force_listen_only', APP_CONFIG.forceListenOnly);
|
||||
const skipCheck = getFromUserSettings('bbb_skip_check_audio', APP_CONFIG.skipCheck);
|
||||
@ -65,7 +64,7 @@ export default lockContextContainer(withModalMounter(withTracker(({ userLocks })
|
||||
|
||||
return ({
|
||||
meetingIsBreakout,
|
||||
closeModal,
|
||||
closeModal: () => closeModal(() => setIsOpen(false)),
|
||||
joinMicrophone: (skipEchoTest) => joinMicrophone(skipEchoTest || skipCheck || skipCheckOnJoin),
|
||||
joinListenOnly,
|
||||
leaveEchoTest,
|
||||
@ -100,4 +99,4 @@ export default lockContextContainer(withModalMounter(withTracker(({ userLocks })
|
||||
isRTL,
|
||||
AudioError,
|
||||
});
|
||||
})(AudioModalContainer)));
|
||||
})(AudioModalContainer));
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { showModal } from '/imports/ui/components/common/modal/service';
|
||||
import Service from '../service';
|
||||
import Storage from '/imports/ui/services/storage/session';
|
||||
|
||||
@ -38,7 +37,7 @@ export const joinMicrophone = (skipEchoTest = false) => {
|
||||
});
|
||||
|
||||
return call.then(() => {
|
||||
window.dispatchEvent(new Event("CLOSE_AUDIO_MODAL"));
|
||||
document.dispatchEvent(new Event("CLOSE_MODAL_AUDIO"));
|
||||
}).catch((error) => {
|
||||
throw error;
|
||||
});
|
||||
@ -55,7 +54,7 @@ export const joinListenOnly = () => {
|
||||
// prop transitions to a state where it was handled OR the user opts
|
||||
// to close the modal.
|
||||
if (!Service.autoplayBlocked()) {
|
||||
window.dispatchEvent(new Event("CLOSE_AUDIO_MODAL"));
|
||||
document.dispatchEvent(new Event("CLOSE_MODAL_AUDIO"));
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
@ -72,11 +71,11 @@ export const leaveEchoTest = () => {
|
||||
return Service.exitAudio();
|
||||
};
|
||||
|
||||
export const closeModal = () => {
|
||||
export const closeModal = (callback) => {
|
||||
if (Service.isConnecting()) {
|
||||
Service.forceExitAudio();
|
||||
}
|
||||
showModal(null);
|
||||
callback();
|
||||
};
|
||||
|
||||
export default {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import styled, { css, keyframes } from 'styled-components';
|
||||
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 { colorPrimary } from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import {
|
||||
@ -90,7 +90,7 @@ const ConnectingAnimation = styled.span`
|
||||
}
|
||||
`;
|
||||
|
||||
const AudioModal = styled(Modal)`
|
||||
const AudioModal = styled(ModalSimple)`
|
||||
padding: 1rem;
|
||||
min-height: 20rem;
|
||||
`;
|
||||
|
@ -2,7 +2,6 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
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 Styled from './styles';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
@ -375,4 +374,4 @@ class AudioSettings extends React.Component {
|
||||
AudioSettings.propTypes = propTypes;
|
||||
AudioSettings.defaultProps = defaultProps;
|
||||
|
||||
export default withModalMounter(injectIntl(AudioSettings));
|
||||
export default injectIntl(AudioSettings);
|
||||
|
@ -2,7 +2,6 @@ import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import { Session } from 'meteor/session';
|
||||
import { withModalMounter } from '/imports/ui/components/common/modal/service';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import { range } from '/imports/utils/array-utils';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
@ -133,7 +132,24 @@ class AudioContainer extends PureComponent {
|
||||
}
|
||||
|
||||
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 autoJoin = getFromUserSettings('bbb_auto_join_audio', APP_CONFIG.autoJoin);
|
||||
const enableVideo = getFromUserSettings('bbb_enable_video', KURENTO_CONFIG.enableVideo);
|
||||
@ -174,14 +191,12 @@ export default lockContextContainer(withModalMounter(injectIntl(withTracker(({ m
|
||||
const userSelectedListenOnly = didUserSelectedListenOnly();
|
||||
const meetingIsBreakout = AppService.meetingIsBreakout();
|
||||
const hasBreakoutRooms = AppService.getBreakoutRooms().length > 0;
|
||||
const openAudioModal = () => new Promise((resolve) => {
|
||||
mountModal(<AudioModalContainer resolve={resolve} />);
|
||||
});
|
||||
const openAudioModal = () => setAudioModalIsOpen(true);
|
||||
|
||||
const openVideoPreviewModal = () => new Promise((resolve) => {
|
||||
if (userWebcam) return resolve();
|
||||
mountModal(<VideoPreviewContainer resolve={resolve} />);
|
||||
});
|
||||
const openVideoPreviewModal = () => {
|
||||
if (userWebcam) return;
|
||||
setVideoPreviewModalIsOpen(true);
|
||||
};
|
||||
|
||||
if (Service.isConnected() && !Service.isListenOnly()) {
|
||||
Service.updateAudioConstraints(microphoneConstraints);
|
||||
@ -208,11 +223,12 @@ export default lockContextContainer(withModalMounter(injectIntl(withTracker(({ m
|
||||
|
||||
return;
|
||||
}
|
||||
setTimeout(() => openAudioModal().then(() => {
|
||||
if (enableVideo && autoShareWebcam) {
|
||||
openVideoPreviewModal();
|
||||
setTimeout(() => {
|
||||
openAudioModal();
|
||||
if (enableVideo && autoShareWebcam) {
|
||||
openVideoPreviewModal();
|
||||
}
|
||||
}), 0);
|
||||
}, 0);
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -222,6 +238,8 @@ export default lockContextContainer(withModalMounter(injectIntl(withTracker(({ m
|
||||
meetingIsBreakout,
|
||||
userSelectedMicrophone,
|
||||
userSelectedListenOnly,
|
||||
isAudioModalOpen,
|
||||
setAudioModalIsOpen,
|
||||
init: async () => {
|
||||
await Service.init(messages, intl);
|
||||
const enableVideo = getFromUserSettings('bbb_enable_video', KURENTO_CONFIG.enableVideo);
|
||||
@ -234,7 +252,9 @@ export default lockContextContainer(withModalMounter(injectIntl(withTracker(({ m
|
||||
}
|
||||
Session.set('audioModalIsOpen', true);
|
||||
if (enableVideo && autoShareWebcam) {
|
||||
openAudioModal().then(() => { openVideoPreviewModal(); didMountAutoJoin = true; });
|
||||
openAudioModal()
|
||||
openVideoPreviewModal();
|
||||
didMountAutoJoin = true;
|
||||
} else if (!(
|
||||
userSelectedMicrophone
|
||||
&& userSelectedListenOnly
|
||||
@ -245,7 +265,7 @@ export default lockContextContainer(withModalMounter(injectIntl(withTracker(({ m
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
};
|
||||
})(AudioContainer))));
|
||||
})(AudioContainer)));
|
||||
|
||||
AudioContainer.propTypes = {
|
||||
hasBreakoutRooms: PropTypes.bool.isRequired,
|
||||
|
@ -129,13 +129,12 @@ class DeviceSelector extends Component {
|
||||
} = this.props;
|
||||
|
||||
const { options } = this.state;
|
||||
const { isSafari } = browserInfo;
|
||||
|
||||
let notFoundOption;
|
||||
|
||||
if (blocked) {
|
||||
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);
|
||||
notFoundOption = <option value="not-found">{defaultOutputDeviceLabel}</option>;
|
||||
} else {
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { 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 && `
|
||||
top: 8em;
|
||||
left: 22em;
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { withModalMounter } from '/imports/ui/components/common/modal/service';
|
||||
import Modal from '/imports/ui/components/common/modal/fullscreen/component';
|
||||
import ModalFullscreen from '/imports/ui/components/common/modal/fullscreen/component';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import PropTypes from 'prop-types';
|
||||
import AudioService from '../audio/service';
|
||||
@ -49,7 +48,6 @@ const propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
breakout: PropTypes.objectOf(Object).isRequired,
|
||||
getURL: PropTypes.func.isRequired,
|
||||
mountModal: PropTypes.func.isRequired,
|
||||
breakoutURL: PropTypes.string.isRequired,
|
||||
isFreeJoin: PropTypes.bool.isRequired,
|
||||
voiceUserJoined: PropTypes.bool.isRequired,
|
||||
@ -97,7 +95,7 @@ class BreakoutJoinConfirmation extends Component {
|
||||
handleJoinBreakoutConfirmation() {
|
||||
const {
|
||||
getURL,
|
||||
mountModal,
|
||||
setIsOpen,
|
||||
breakoutURL,
|
||||
isFreeJoin,
|
||||
voiceUserJoined,
|
||||
@ -133,7 +131,7 @@ class BreakoutJoinConfirmation extends Component {
|
||||
|
||||
Session.set('lastBreakoutIdOpened', selectValue);
|
||||
window.open(url);
|
||||
mountModal(null);
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
async fetchJoinURL(selectValue) {
|
||||
@ -201,11 +199,13 @@ class BreakoutJoinConfirmation extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, breakoutName, isFreeJoin } = this.props;
|
||||
const { intl, breakoutName, isFreeJoin, setIsOpen,
|
||||
isOpen, priority,
|
||||
} = this.props;
|
||||
const { waiting } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<ModalFullscreen
|
||||
title={intl.formatMessage(intlMessages.title)}
|
||||
confirm={{
|
||||
callback: this.handleJoinBreakoutConfirmation,
|
||||
@ -215,16 +215,22 @@ class BreakoutJoinConfirmation extends Component {
|
||||
disabled: waiting,
|
||||
}}
|
||||
dismiss={{
|
||||
callback: () => setIsOpen(false),
|
||||
label: intl.formatMessage(intlMessages.dismissLabel),
|
||||
description: intl.formatMessage(intlMessages.dismissDesc),
|
||||
}}
|
||||
{...{
|
||||
setIsOpen,
|
||||
isOpen,
|
||||
priority,
|
||||
}}
|
||||
>
|
||||
{ isFreeJoin ? this.renderSelectMeeting() : `${intl.formatMessage(intlMessages.message)} ${breakoutName}?`}
|
||||
</Modal>
|
||||
</ModalFullscreen>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withModalMounter(injectIntl(BreakoutJoinConfirmation));
|
||||
export default injectIntl(BreakoutJoinConfirmation);
|
||||
|
||||
BreakoutJoinConfirmation.propTypes = propTypes;
|
||||
|
@ -33,14 +33,13 @@ const requestJoinURL = (breakoutId) => {
|
||||
});
|
||||
};
|
||||
|
||||
export default withTracker(({ breakout, mountModal, breakoutName }) => {
|
||||
export default withTracker(({ breakout, breakoutName }) => {
|
||||
const isFreeJoin = breakout.freeJoin;
|
||||
const { breakoutId } = breakout;
|
||||
const url = getURL(breakoutId);
|
||||
|
||||
return {
|
||||
isFreeJoin,
|
||||
mountModal,
|
||||
breakoutName,
|
||||
breakoutURL: url,
|
||||
breakouts: breakoutService.getBreakouts(),
|
||||
|
@ -1,8 +1,7 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
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 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";
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
@ -27,6 +26,11 @@ const intlMessages = defineMessages({
|
||||
class BreakoutDropdown extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isCreateBreakoutRoomModalOpen: false,
|
||||
};
|
||||
this.setCreateBreakoutRoomModalIsOpen = this.setCreateBreakoutRoomModalIsOpen.bind(this);
|
||||
}
|
||||
|
||||
getAvailableActions() {
|
||||
@ -36,7 +40,6 @@ class BreakoutDropdown extends PureComponent {
|
||||
endAllBreakouts,
|
||||
isMeteorConnected,
|
||||
amIModerator,
|
||||
mountModal,
|
||||
} = this.props;
|
||||
|
||||
this.menuItems = [];
|
||||
@ -58,9 +61,7 @@ class BreakoutDropdown extends PureComponent {
|
||||
dataTest: 'openUpdateBreakoutUsersModal',
|
||||
label: intl.formatMessage(intlMessages.manageUsers),
|
||||
onClick: () => {
|
||||
mountModal(
|
||||
<CreateBreakoutRoomModal isUpdate />
|
||||
);
|
||||
this.setCreateBreakoutRoomModalIsOpen(true);
|
||||
}
|
||||
}
|
||||
);
|
||||
@ -82,12 +83,19 @@ class BreakoutDropdown extends PureComponent {
|
||||
return this.menuItems;
|
||||
}
|
||||
|
||||
setCreateBreakoutRoomModalIsOpen(value) {
|
||||
this.setState({
|
||||
isCreateBreakoutRoomModalOpen: value,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
intl,
|
||||
isRTL,
|
||||
} = this.props;
|
||||
|
||||
const { isCreateBreakoutRoomModalOpen } = this.state;
|
||||
return (
|
||||
<>
|
||||
<BBBMenu
|
||||
@ -112,9 +120,18 @@ class BreakoutDropdown extends PureComponent {
|
||||
}}
|
||||
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);
|
||||
|
@ -1,14 +1,12 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Session } from 'meteor/session';
|
||||
import { withModalMounter } from '/imports/ui/components/common/modal/service';
|
||||
import BreakoutJoinConfirmation from '/imports/ui/components/breakout-join-confirmation/container';
|
||||
import BreakoutJoinConfirmationContainer from '/imports/ui/components/breakout-join-confirmation/container';
|
||||
import BreakoutService from '../service';
|
||||
|
||||
const BREAKOUT_MODAL_DELAY = 200;
|
||||
|
||||
const propTypes = {
|
||||
mountModal: PropTypes.func.isRequired,
|
||||
lastBreakoutReceived: PropTypes.shape({
|
||||
breakoutUrlData: PropTypes.object.isRequired,
|
||||
}),
|
||||
@ -26,20 +24,17 @@ const defaultProps = {
|
||||
breakouts: [],
|
||||
};
|
||||
|
||||
const openBreakoutJoinConfirmation = (breakout, breakoutName, mountModal) => mountModal(
|
||||
<BreakoutJoinConfirmation
|
||||
breakout={breakout}
|
||||
breakoutName={breakoutName}
|
||||
/>,
|
||||
);
|
||||
|
||||
class BreakoutRoomInvitation extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
didSendBreakoutInvite: false,
|
||||
isBreakoutJoinConfirmationModalOpen: false,
|
||||
breakout: null,
|
||||
breakoutName: null,
|
||||
};
|
||||
this.setBreakoutJoinConfirmationModalIsOpen = this.setBreakoutJoinConfirmationModalIsOpen.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@ -101,23 +96,43 @@ class BreakoutRoomInvitation extends Component {
|
||||
|
||||
inviteUserToBreakout(breakout) {
|
||||
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
|
||||
// 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.
|
||||
setTimeout(() => {
|
||||
openBreakoutJoinConfirmation.call(this, breakout, breakout.name, mountModal);
|
||||
this.setState({
|
||||
breakout: breakout,
|
||||
breakoutName: breakout.name,
|
||||
})
|
||||
this.setBreakoutJoinConfirmationModalIsOpen(true);
|
||||
}, BREAKOUT_MODAL_DELAY);
|
||||
}
|
||||
|
||||
setBreakoutJoinConfirmationModalIsOpen(value) {
|
||||
this.setState({
|
||||
isBreakoutJoinConfirmationModalOpen: value,
|
||||
});
|
||||
}
|
||||
|
||||
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.defaultProps = defaultProps;
|
||||
|
||||
export default withModalMounter(BreakoutRoomInvitation);
|
||||
export default BreakoutRoomInvitation;
|
||||
|
@ -1,15 +1,13 @@
|
||||
import React from 'react';
|
||||
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 CaptionsReaderMenuContainer from '/imports/ui/components/captions/reader-menu/container';
|
||||
import CaptionButton from './component';
|
||||
|
||||
const Container = (props) => <CaptionButton {...props} />;
|
||||
|
||||
export default withModalMounter(withTracker(({ mountModal }) => ({
|
||||
export default withTracker(({ setIsOpen }) => ({
|
||||
isActive: Service.isCaptionsActive(),
|
||||
handleOnClick: () => (Service.isCaptionsActive()
|
||||
? Service.deactivateCaptions()
|
||||
: mountModal(<CaptionsReaderMenuContainer />)),
|
||||
}))(Container));
|
||||
: setIsOpen(true)),
|
||||
}))(Container);
|
||||
|
@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
import ColorPicker from "./color-picker/component";
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { withModalMounter } from '/imports/ui/components/common/modal/service';
|
||||
import Styled from './styles';
|
||||
|
||||
const DEFAULT_VALUE = 'select';
|
||||
@ -207,6 +206,8 @@ class ReaderMenu extends PureComponent {
|
||||
intl,
|
||||
ownedLocales,
|
||||
closeModal,
|
||||
isOpen,
|
||||
priority,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@ -231,6 +232,10 @@ class ReaderMenu extends PureComponent {
|
||||
onRequestClose={closeModal}
|
||||
hideBorder
|
||||
contentLabel={intl.formatMessage(intlMessages.title)}
|
||||
{...{
|
||||
isOpen,
|
||||
priority,
|
||||
}}
|
||||
>
|
||||
<Styled.Title>
|
||||
{intl.formatMessage(intlMessages.title)}
|
||||
@ -408,4 +413,4 @@ class ReaderMenu extends PureComponent {
|
||||
|
||||
ReaderMenu.propTypes = propTypes;
|
||||
|
||||
export default injectIntl(withModalMounter(ReaderMenu));
|
||||
export default injectIntl(ReaderMenu);
|
||||
|
@ -1,14 +1,13 @@
|
||||
import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import { withModalMounter } from '/imports/ui/components/common/modal/service';
|
||||
import ReaderMenu from './component';
|
||||
import CaptionsService from '/imports/ui/components/captions/service';
|
||||
|
||||
const ReaderMenuContainer = (props) => <ReaderMenu {...props} />;
|
||||
|
||||
export default withModalMounter(withTracker(({ mountModal }) => ({
|
||||
closeModal: () => mountModal(null),
|
||||
export default withTracker(({ setIsOpen }) => ({
|
||||
closeModal: () => setIsOpen(false),
|
||||
activateCaptions: (locale, settings) => CaptionsService.activateCaptions(locale, settings),
|
||||
getCaptionsSettings: () => CaptionsService.getCaptionsSettings(),
|
||||
ownedLocales: CaptionsService.getOwnedLocales(),
|
||||
}))(ReaderMenuContainer));
|
||||
}))(ReaderMenuContainer);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
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 {
|
||||
colorGrayDark,
|
||||
colorWhite,
|
||||
@ -10,7 +10,7 @@ import {
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import { borderSize, borderSizeLarge } from '/imports/ui/stylesheets/styled-components/general';
|
||||
|
||||
const ReaderMenuModal = styled(Modal)`
|
||||
const ReaderMenuModal = styled(ModalSimple)`
|
||||
padding: 1rem;
|
||||
`;
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { withModalMounter } from '/imports/ui/components/common/modal/service';
|
||||
import PropTypes from 'prop-types';
|
||||
import Service from '/imports/ui/components/captions/service';
|
||||
import LocalesDropdown from '/imports/ui/components/common/locales-dropdown/component';
|
||||
@ -44,7 +43,6 @@ const intlMessages = defineMessages({
|
||||
|
||||
const propTypes = {
|
||||
availableLocales: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
closeModal: PropTypes.func.isRequired,
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
@ -68,9 +66,9 @@ class WriterMenu extends PureComponent {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { closeModal } = this.props;
|
||||
const { setIsOpen } = this.props;
|
||||
|
||||
closeModal();
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
handleChange(event) {
|
||||
@ -79,7 +77,7 @@ class WriterMenu extends PureComponent {
|
||||
|
||||
handleStart() {
|
||||
const {
|
||||
closeModal,
|
||||
setIsOpen,
|
||||
layoutContextDispatch,
|
||||
} = this.props;
|
||||
|
||||
@ -95,24 +93,33 @@ class WriterMenu extends PureComponent {
|
||||
value: PANELS.CAPTIONS,
|
||||
});
|
||||
|
||||
closeModal();
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
intl,
|
||||
availableLocales,
|
||||
closeModal,
|
||||
isOpen,
|
||||
onRequestClose,
|
||||
priority,
|
||||
setIsOpen
|
||||
|
||||
} = this.props;
|
||||
|
||||
const { locale } = this.state;
|
||||
|
||||
return (
|
||||
<Styled.WriterMenuModal
|
||||
onRequestClose={closeModal}
|
||||
hideBorder
|
||||
contentLabel={intl.formatMessage(intlMessages.title)}
|
||||
title={intl.formatMessage(intlMessages.title)}
|
||||
{...{
|
||||
isOpen,
|
||||
onRequestClose,
|
||||
priority,
|
||||
setIsOpen
|
||||
}}
|
||||
>
|
||||
<Styled.Content>
|
||||
<span>
|
||||
@ -151,4 +158,4 @@ class WriterMenu extends PureComponent {
|
||||
|
||||
WriterMenu.propTypes = propTypes;
|
||||
|
||||
export default injectIntl(withModalMounter(WriterMenu));
|
||||
export default injectIntl(WriterMenu);
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React, { useContext } from 'react';
|
||||
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 WriterMenu from './component';
|
||||
import { layoutDispatch } from '../../layout/context';
|
||||
@ -20,7 +19,6 @@ const WriterMenuContainer = (props) => {
|
||||
return amIModerator && <WriterMenu {...{ layoutContextDispatch, ...props }} />;
|
||||
};
|
||||
|
||||
export default withModalMounter(withTracker(({ mountModal }) => ({
|
||||
closeModal: () => mountModal(null),
|
||||
export default withTracker(() => ({
|
||||
availableLocales: Service.getAvailableLocales(),
|
||||
}))(WriterMenuContainer));
|
||||
}))(WriterMenuContainer);
|
||||
|
@ -12,9 +12,9 @@ import {
|
||||
colorPrimary,
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
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;
|
||||
`;
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
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 { getDateString, uniqueId } from '/imports/utils/string-utils';
|
||||
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);
|
||||
|
@ -329,7 +329,6 @@ class TimeWindowList extends PureComponent {
|
||||
}}
|
||||
key="chat-list"
|
||||
data-test="chatMessages"
|
||||
aria-live="polite"
|
||||
ref={node => this.messageListWrapper = node}
|
||||
onCopy={(e) => { e.stopPropagation(); }}
|
||||
>
|
||||
|
@ -164,6 +164,7 @@ export default class ButtonBase extends React.Component {
|
||||
'iconRight',
|
||||
'isVisualEffects',
|
||||
'panning',
|
||||
'panSelected',
|
||||
];
|
||||
|
||||
return (
|
||||
|
@ -44,6 +44,7 @@ const EmojiButton = styled.button`
|
||||
overflow: hidden;
|
||||
z-index: 2;
|
||||
border: none;
|
||||
padding: 0;
|
||||
|
||||
[dir="rtl"] & {
|
||||
right: initial;
|
||||
|
@ -123,6 +123,8 @@ export default class Button extends BaseButton {
|
||||
'aria-label': ariaLabel,
|
||||
'aria-expanded': ariaExpanded,
|
||||
tooltipLabel,
|
||||
tooltipdelay,
|
||||
tooltipplacement,
|
||||
} = this.props;
|
||||
|
||||
const renderFuncName = circle ? 'renderCircle' : 'renderDefault';
|
||||
@ -132,6 +134,8 @@ export default class Button extends BaseButton {
|
||||
return (
|
||||
<TooltipContainer
|
||||
title={tooltipLabel || buttonLabel}
|
||||
delay={tooltipdelay}
|
||||
placement={tooltipplacement}
|
||||
>
|
||||
{this[renderFuncName]()}
|
||||
</TooltipContainer>
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import ModalSimple from '/imports/ui/components/common/modal/simple/component';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { withModalMounter } from '/imports/ui/components/common/modal/service';
|
||||
import FallbackView from '../fallback-view/component';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
@ -11,14 +10,17 @@ const intlMessages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
const FallbackModal = ({ error, intl, mountModal }) => (
|
||||
const FallbackModal = ({ error, intl }) => {
|
||||
return (
|
||||
<ModalSimple
|
||||
hideBorder
|
||||
onRequestClose={() => mountModal(null)}
|
||||
priority="medium"
|
||||
shouldShowCloseButton={false}
|
||||
contentLabel={intl.formatMessage(intlMessages.ariaTitle)}
|
||||
isOpen={!!error}
|
||||
>
|
||||
<FallbackView {...{ error }} />
|
||||
</ModalSimple>
|
||||
);
|
||||
)};
|
||||
|
||||
export default withModalMounter(injectIntl(FallbackModal));
|
||||
export default injectIntl(FallbackModal);
|
||||
|
@ -1,91 +1,39 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import Styled from './styles';
|
||||
import { registerTitleView, unregisterTitleView } from '/imports/utils/dom-utils';
|
||||
|
||||
const propTypes = {
|
||||
overlayClassName: PropTypes.string.isRequired,
|
||||
portalClassName: PropTypes.string.isRequired,
|
||||
contentLabel: PropTypes.string.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
};
|
||||
const BaseModal = (props) => {
|
||||
const { setIsOpen, modalName, children,
|
||||
isOpen, onRequestClose, className, overlayClassName,
|
||||
} = props;
|
||||
|
||||
const defaultProps = {
|
||||
overlayClassName: 'modalOverlay',
|
||||
contentLabel: 'Modal',
|
||||
isOpen: true,
|
||||
};
|
||||
const closeEventHandler = useCallback (() => {
|
||||
setIsOpen(false);
|
||||
} , []);
|
||||
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() {
|
||||
registerTitleView(this.props.contentLabel);
|
||||
}
|
||||
// Listen to close event on mount
|
||||
document.addEventListener(closeEventName, closeEventHandler);
|
||||
|
||||
componentWillUnmount() {
|
||||
unregisterTitleView();
|
||||
}
|
||||
// Remove listener on unmount
|
||||
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() {
|
||||
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}
|
||||
/>);
|
||||
}
|
||||
};
|
||||
export default { BaseModal };
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { defineMessages } from 'react-intl';
|
||||
import { withModalMounter } from '/imports/ui/components/common/modal/service';
|
||||
import PropTypes from 'prop-types';
|
||||
import Styled from './styles';
|
||||
|
||||
@ -39,7 +38,7 @@ class ConfirmationModal extends Component {
|
||||
render() {
|
||||
const {
|
||||
intl,
|
||||
mountModal,
|
||||
setIsOpen,
|
||||
onConfirm,
|
||||
title,
|
||||
titleMessageId,
|
||||
@ -51,6 +50,9 @@ class ConfirmationModal extends Component {
|
||||
confirmParam,
|
||||
disableConfirmButton,
|
||||
description,
|
||||
isOpen,
|
||||
onRequestClose,
|
||||
priority,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@ -61,9 +63,14 @@ class ConfirmationModal extends Component {
|
||||
|
||||
return (
|
||||
<Styled.ConfirmationModal
|
||||
onRequestClose={() => mountModal(null)}
|
||||
onRequestClose={() => setIsOpen(false)}
|
||||
contentLabel={title}
|
||||
title={title || intl.formatMessage({ id: titleMessageId }, { 0: titleMessageExtra })}
|
||||
{...{
|
||||
isOpen,
|
||||
onRequestClose,
|
||||
priority,
|
||||
}}
|
||||
>
|
||||
<Styled.Container>
|
||||
<Styled.Description>
|
||||
@ -92,12 +99,12 @@ class ConfirmationModal extends Component {
|
||||
data-test={confirmButtonDataTest}
|
||||
onClick={() => {
|
||||
onConfirm(confirmParam, checked);
|
||||
mountModal(null);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
/>
|
||||
<Styled.CancelButton
|
||||
label={intl.formatMessage(messages.noLabel)}
|
||||
onClick={() => mountModal(null)}
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
</Styled.Footer>
|
||||
</Styled.Container>
|
||||
@ -109,4 +116,4 @@ class ConfirmationModal extends Component {
|
||||
ConfirmationModal.propTypes = propTypes;
|
||||
ConfirmationModal.defaultProps = defaultProps;
|
||||
|
||||
export default withModalMounter(ConfirmationModal);
|
||||
export default ConfirmationModal;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
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 {
|
||||
smPaddingX,
|
||||
mdPaddingX,
|
||||
@ -10,7 +10,7 @@ import {
|
||||
import { colorGray } from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import { lineHeightBase } from '/imports/ui/stylesheets/styled-components/typography';
|
||||
|
||||
const ConfirmationModal = styled(Modal)`
|
||||
const ConfirmationModal = styled(ModalSimple)`
|
||||
padding: ${mdPaddingX};
|
||||
`;
|
||||
|
||||
|
@ -1,6 +0,0 @@
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import { getModal } from './service';
|
||||
|
||||
export default withTracker(() => ({
|
||||
modalComponent: getModal(),
|
||||
}))(({ modalComponent }) => modalComponent);
|
@ -1,7 +1,6 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { withModalState } from '../base/component';
|
||||
import Styled from './styles';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
@ -61,7 +60,7 @@ class ModalFullscreen extends PureComponent {
|
||||
}
|
||||
|
||||
handleAction(name) {
|
||||
const { confirm, dismiss, modalHide } = this.props;
|
||||
const { confirm, dismiss } = this.props;
|
||||
const { callback: callBackConfirm } = confirm;
|
||||
const { callback: callBackDismiss } = dismiss;
|
||||
|
||||
@ -78,7 +77,7 @@ class ModalFullscreen extends PureComponent {
|
||||
break;
|
||||
}
|
||||
|
||||
return modalHide(callback);
|
||||
return callback();
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -89,7 +88,7 @@ class ModalFullscreen extends PureComponent {
|
||||
dismiss,
|
||||
className,
|
||||
children,
|
||||
modalisOpen,
|
||||
isOpen,
|
||||
preventClosing,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
@ -103,7 +102,7 @@ class ModalFullscreen extends PureComponent {
|
||||
return (
|
||||
<Styled.FullscreenModal
|
||||
id="fsmodal"
|
||||
isOpen={modalisOpen || preventClosing}
|
||||
isOpen={isOpen || preventClosing}
|
||||
contentLabel={title}
|
||||
overlayClassName={"fullscreenModalOverlay"}
|
||||
{...otherProps}
|
||||
@ -147,4 +146,4 @@ class ModalFullscreen extends PureComponent {
|
||||
ModalFullscreen.propTypes = propTypes;
|
||||
ModalFullscreen.defaultProps = defaultProps;
|
||||
|
||||
export default withModalState(injectIntl(ModalFullscreen));
|
||||
export default injectIntl(ModalFullscreen);
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 Button from '/imports/ui/components/common/button/component';
|
||||
import {
|
||||
|
@ -3,7 +3,6 @@ import Styled from './styles';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
hideBorder: PropTypes.bool,
|
||||
headerPosition: PropTypes.string,
|
||||
shouldShowCloseButton: PropTypes.bool,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
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 Styled from './styles';
|
||||
|
||||
@ -42,7 +42,6 @@ const propTypes = {
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
mountModal: PropTypes.func.isRequired,
|
||||
numAvailableViewers: PropTypes.number.isRequired,
|
||||
randomUserReq: PropTypes.func.isRequired,
|
||||
};
|
||||
@ -123,11 +122,13 @@ class RandomUserSelect extends Component {
|
||||
keepModalOpen,
|
||||
toggleKeepModalOpen,
|
||||
intl,
|
||||
mountModal,
|
||||
setIsOpen,
|
||||
numAvailableViewers,
|
||||
currentUser,
|
||||
clearRandomlySelectedUser,
|
||||
mappedRandomlySelectedUsers,
|
||||
isOpen,
|
||||
priority,
|
||||
} = this.props;
|
||||
|
||||
const counter = SELECT_RANDOM_USER_COUNTDOWN ? this.state.count : 0;
|
||||
@ -189,17 +190,21 @@ class RandomUserSelect extends Component {
|
||||
}
|
||||
if (keepModalOpen) {
|
||||
return (
|
||||
<Modal
|
||||
<ModalSimple
|
||||
onRequestClose={() => {
|
||||
if (currentUser.presenter) clearRandomlySelectedUser();
|
||||
toggleKeepModalOpen();
|
||||
mountModal(null);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
contentLabel={intl.formatMessage(messages.ariaModalTitle)}
|
||||
title={title}
|
||||
{...{
|
||||
isOpen,
|
||||
priority,
|
||||
}}
|
||||
>
|
||||
{viewElement}
|
||||
</Modal>
|
||||
</ModalSimple>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
|
@ -3,7 +3,6 @@ import { withTracker } from 'meteor/react-meteor-data';
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import Users from '/imports/api/users';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import { withModalMounter } from '/imports/ui/components/common/modal/service';
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
import RandomUserSelect from './component';
|
||||
import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
|
||||
@ -56,12 +55,14 @@ const RandomUserSelectContainer = (props) => {
|
||||
if (randomlySelectedUser) {
|
||||
mappedRandomlySelectedUsers = randomlySelectedUser.map((ui) => {
|
||||
const selectedUser = users[Auth.meetingID][ui[0]];
|
||||
return [{
|
||||
userId: selectedUser.userId,
|
||||
avatar: selectedUser.avatar,
|
||||
color: selectedUser.color,
|
||||
name: selectedUser.name,
|
||||
}, ui[1]];
|
||||
if (selectedUser){
|
||||
return [{
|
||||
userId: selectedUser.userId,
|
||||
avatar: selectedUser.avatar,
|
||||
color: selectedUser.color,
|
||||
name: selectedUser.name,
|
||||
}, ui[1]];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -74,7 +75,7 @@ const RandomUserSelectContainer = (props) => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default withModalMounter(withTracker(({ mountModal }) => {
|
||||
export default withTracker(() => {
|
||||
const viewerPool = Users.find({
|
||||
meetingId: Auth.meetingID,
|
||||
presenter: { $ne: true },
|
||||
@ -96,11 +97,10 @@ export default withModalMounter(withTracker(({ mountModal }) => {
|
||||
const clearRandomlySelectedUser = () => (SELECT_RANDOM_USER_ENABLED ? makeCall('clearRandomlySelectedUser') : null);
|
||||
|
||||
return ({
|
||||
closeModal: () => mountModal(null),
|
||||
toggleKeepModalOpen,
|
||||
numAvailableViewers: viewerPool.length,
|
||||
randomUserReq,
|
||||
clearRandomlySelectedUser,
|
||||
randomlySelectedUser: meeting.randomlySelectedUser,
|
||||
});
|
||||
})(RandomUserSelectContainer));
|
||||
})(RandomUserSelectContainer);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user