Merge pull request #14 from gustavotrott/pr-19841
Add graphql data for Gladia transcriptions
This commit is contained in:
commit
2126c5192c
1
.gitignore
vendored
1
.gitignore
vendored
@ -24,3 +24,4 @@ artifacts/*
|
||||
bbb-presentation-video.zip
|
||||
bbb-presentation-video
|
||||
bbb-graphql-actions-adapter-server/
|
||||
bigbluebutton-html5/public/locales/index.json
|
||||
|
@ -14,6 +14,7 @@ import org.bigbluebutton.SystemConfiguration
|
||||
import java.util.concurrent.TimeUnit
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.db.{ DatabaseConnection, MeetingDAO }
|
||||
import org.bigbluebutton.core.domain.MeetingEndReason
|
||||
import org.bigbluebutton.core.running.RunningMeeting
|
||||
import org.bigbluebutton.core.util.ColorPicker
|
||||
import org.bigbluebutton.core2.RunningMeetings
|
||||
@ -57,6 +58,9 @@ class BigBlueButtonActor(
|
||||
override def preStart() {
|
||||
bbbMsgBus.subscribe(self, meetingManagerChannel)
|
||||
DatabaseConnection.initialize()
|
||||
|
||||
//Terminate all previous meetings, as they will not function following the akka-apps restart
|
||||
MeetingDAO.setAllMeetingsEnded(MeetingEndReason.ENDED_DUE_TO_SERVICE_INTERRUPTION, "system")
|
||||
}
|
||||
|
||||
override def postStop() {
|
||||
|
@ -2,6 +2,7 @@ package org.bigbluebutton.core.apps.audiocaptions
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.db.UserTranscriptionErrorDAO
|
||||
import org.bigbluebutton.core.models.AudioCaptions
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
|
||||
@ -23,5 +24,8 @@ trait TranscriptionProviderErrorMsgHdlr {
|
||||
}
|
||||
|
||||
broadcastEvent(msg.header.userId, msg.body.errorCode, msg.body.errorMessage)
|
||||
|
||||
UserTranscriptionErrorDAO.insert(msg.header.userId, msg.header.meetingId, msg.body.errorCode, msg.body.errorMessage)
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,13 @@
|
||||
package org.bigbluebutton.core.apps.audiocaptions
|
||||
|
||||
import org.bigbluebutton.ClientSettings.getConfigPropertyValueByPathAsStringOrElse
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.db.CaptionDAO
|
||||
import org.bigbluebutton.core.models.{AudioCaptions, Users2x}
|
||||
import org.bigbluebutton.core.models.{AudioCaptions, UserState, Users2x}
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
|
||||
import java.sql.Timestamp
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
trait UpdateTranscriptPubMsgHdlr {
|
||||
this: AudioCaptionsApp2x =>
|
||||
@ -25,6 +26,17 @@ trait UpdateTranscriptPubMsgHdlr {
|
||||
bus.outGW.send(msgEvent)
|
||||
}
|
||||
|
||||
def sendPadUpdatePubMsg(userId: String, defaultPad: String, text: String, transcript: Boolean): Unit = {
|
||||
val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, meetingId, "nodeJSapp")
|
||||
val envelope = BbbCoreEnvelope(PadUpdatePubMsg.NAME, routing)
|
||||
val header = BbbClientMsgHeader(PadUpdatePubMsg.NAME, meetingId, userId)
|
||||
val body = PadUpdatePubMsgBody(defaultPad, text, transcript)
|
||||
val event = PadUpdatePubMsg(header, body)
|
||||
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
|
||||
|
||||
bus.outGW.send(msgEvent)
|
||||
}
|
||||
|
||||
// Adapt to the current captions' recording process
|
||||
def editTranscript(
|
||||
userId: String,
|
||||
@ -80,6 +92,28 @@ trait UpdateTranscriptPubMsgHdlr {
|
||||
msg.body.locale,
|
||||
msg.body.result,
|
||||
)
|
||||
|
||||
if(msg.body.result) {
|
||||
val userName = Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId).get match {
|
||||
case u: UserState => u.name
|
||||
case _ => "???"
|
||||
}
|
||||
|
||||
val now = LocalDateTime.now()
|
||||
val formatter = DateTimeFormatter.ofPattern("HH:mm:ss")
|
||||
val formattedTime = now.format(formatter)
|
||||
|
||||
val userSpoke = s"\n $userName ($formattedTime): $transcript"
|
||||
|
||||
val defaultPad = getConfigPropertyValueByPathAsStringOrElse(
|
||||
liveMeeting.clientSettings,
|
||||
"public.captions.defaultPad",
|
||||
alternativeValue = ""
|
||||
)
|
||||
|
||||
sendPadUpdatePubMsg(msg.header.userId, defaultPad, userSpoke, transcript = true)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import org.bigbluebutton.core.running.OutMsgRouter
|
||||
import org.bigbluebutton.core2.MeetingStatus2x
|
||||
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
|
||||
import org.bigbluebutton.core.db.LayoutDAO
|
||||
import org.bigbluebutton.core2.message.senders.{ MsgBuilder }
|
||||
|
||||
trait BroadcastLayoutMsgHdlr extends RightsManagementTrait {
|
||||
this: LayoutApp2x =>
|
||||
@ -60,5 +61,18 @@ trait BroadcastLayoutMsgHdlr extends RightsManagementTrait {
|
||||
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
|
||||
|
||||
outGW.send(msgEvent)
|
||||
|
||||
if (body.pushLayout) {
|
||||
val notifyEvent = MsgBuilder.buildNotifyUserInMeetingEvtMsg(
|
||||
fromUserId,
|
||||
liveMeeting.props.meetingProp.intId,
|
||||
"info",
|
||||
"user",
|
||||
"app.layoutUpdate.label",
|
||||
"Notification to when the presenter changes size of cams",
|
||||
Vector()
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,63 +0,0 @@
|
||||
package org.bigbluebutton.core.apps.users
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
|
||||
import org.bigbluebutton.core.models.{ UserState, Users2x }
|
||||
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
|
||||
import org.bigbluebutton.core2.MeetingStatus2x
|
||||
import org.bigbluebutton.SystemConfiguration
|
||||
import scala.util.Random
|
||||
|
||||
trait SelectRandomViewerReqMsgHdlr extends RightsManagementTrait {
|
||||
this: UsersApp =>
|
||||
|
||||
val outGW: OutMsgRouter
|
||||
|
||||
def handleSelectRandomViewerReqMsg(msg: SelectRandomViewerReqMsg): Unit = {
|
||||
log.debug("Received SelectRandomViewerReqMsg {}", SelectRandomViewerReqMsg)
|
||||
|
||||
def broadcastEvent(msg: SelectRandomViewerReqMsg, users: Vector[String], choice: String): Unit = {
|
||||
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, liveMeeting.props.meetingProp.intId, msg.header.userId)
|
||||
val envelope = BbbCoreEnvelope(SelectRandomViewerRespMsg.NAME, routing)
|
||||
val header = BbbClientMsgHeader(SelectRandomViewerRespMsg.NAME, liveMeeting.props.meetingProp.intId, msg.header.userId)
|
||||
|
||||
val body = SelectRandomViewerRespMsgBody(msg.header.userId, users, choice)
|
||||
val event = SelectRandomViewerRespMsg(header, body)
|
||||
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
|
||||
outGW.send(msgEvent)
|
||||
}
|
||||
|
||||
if (permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
val reason = "No permission to select random user."
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)
|
||||
} else {
|
||||
val users = Users2x.getRandomlyPickableUsers(liveMeeting.users2x, false)
|
||||
|
||||
val usersPicked = Users2x.getRandomlyPickableUsers(liveMeeting.users2x, reduceDuplicatedPick)
|
||||
|
||||
val randNum = new scala.util.Random
|
||||
var pickedUser = if (usersPicked.size == 0) "" else usersPicked(randNum.nextInt(usersPicked.size)).intId
|
||||
|
||||
if (reduceDuplicatedPick) {
|
||||
if (usersPicked.size <= 1) {
|
||||
// Initialise the exemption
|
||||
val usersToUnexempt = Users2x.findAll(liveMeeting.users2x)
|
||||
usersToUnexempt foreach { u =>
|
||||
Users2x.setUserExempted(liveMeeting.users2x, u.intId, false)
|
||||
}
|
||||
if (usersPicked.size == 0) {
|
||||
// Pick again
|
||||
val usersRepicked = Users2x.getRandomlyPickableUsers(liveMeeting.users2x, reduceDuplicatedPick)
|
||||
pickedUser = if (usersRepicked.size == 0) "" else usersRepicked(randNum.nextInt(usersRepicked.size)).intId
|
||||
Users2x.setUserExempted(liveMeeting.users2x, pickedUser, true)
|
||||
}
|
||||
} else if (usersPicked.size > 1) {
|
||||
Users2x.setUserExempted(liveMeeting.users2x, pickedUser, true)
|
||||
}
|
||||
}
|
||||
val userIds = users.map { case (v) => v.intId }
|
||||
broadcastEvent(msg, userIds, pickedUser)
|
||||
}
|
||||
}
|
||||
}
|
@ -165,7 +165,6 @@ class UsersApp(
|
||||
with RecordAndClearPreviousMarkersCmdMsgHdlr
|
||||
with SendRecordingTimerInternalMsgHdlr
|
||||
with GetRecordingStatusReqMsgHdlr
|
||||
with SelectRandomViewerReqMsgHdlr
|
||||
with AssignPresenterReqMsgHdlr
|
||||
with ChangeUserPinStateReqMsgHdlr
|
||||
with ChangeUserMobileFlagReqMsgHdlr
|
||||
|
@ -58,7 +58,6 @@ trait UserJoinedVoiceConfEvtMsgHdlr extends SystemConfiguration {
|
||||
avatar = "",
|
||||
color = userColor,
|
||||
clientType = if (isDialInUser) "dial-in-user" else "",
|
||||
pickExempted = false,
|
||||
userLeftFlag = UserLeftFlag(false, 0)
|
||||
)
|
||||
Users2x.add(liveMeeting.users2x, newUser)
|
||||
|
@ -25,6 +25,8 @@ case class MeetingDbModel(
|
||||
bannerColor: Option[String],
|
||||
createdTime: Long,
|
||||
durationInSeconds: Int,
|
||||
endWhenNoModerator: Boolean,
|
||||
endWhenNoModeratorDelayInMinutes: Int,
|
||||
endedAt: Option[java.sql.Timestamp],
|
||||
endedReasonCode: Option[String],
|
||||
endedBy: Option[String],
|
||||
@ -49,6 +51,8 @@ class MeetingDbTableDef(tag: Tag) extends Table[MeetingDbModel](tag, None, "meet
|
||||
bannerColor,
|
||||
createdTime,
|
||||
durationInSeconds,
|
||||
endWhenNoModerator,
|
||||
endWhenNoModeratorDelayInMinutes,
|
||||
endedAt,
|
||||
endedReasonCode,
|
||||
endedBy
|
||||
@ -70,6 +74,8 @@ class MeetingDbTableDef(tag: Tag) extends Table[MeetingDbModel](tag, None, "meet
|
||||
val bannerColor = column[Option[String]]("bannerColor")
|
||||
val createdTime = column[Long]("createdTime")
|
||||
val durationInSeconds = column[Int]("durationInSeconds")
|
||||
val endWhenNoModerator = column[Boolean]("endWhenNoModerator")
|
||||
val endWhenNoModeratorDelayInMinutes = column[Int]("endWhenNoModeratorDelayInMinutes")
|
||||
val endedAt = column[Option[java.sql.Timestamp]]("endedAt")
|
||||
val endedReasonCode = column[Option[String]]("endedReasonCode")
|
||||
val endedBy = column[Option[String]]("endedBy")
|
||||
@ -106,6 +112,8 @@ object MeetingDAO {
|
||||
},
|
||||
createdTime = meetingProps.durationProps.createdTime,
|
||||
durationInSeconds = meetingProps.durationProps.duration * 60,
|
||||
endWhenNoModerator = meetingProps.durationProps.endWhenNoModerator,
|
||||
endWhenNoModeratorDelayInMinutes = meetingProps.durationProps.endWhenNoModeratorDelayInMinutes,
|
||||
endedAt = None,
|
||||
endedReasonCode = None,
|
||||
endedBy = None
|
||||
@ -182,4 +190,27 @@ object MeetingDAO {
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error updating endedAt=now() Meeting: $e")
|
||||
}
|
||||
}
|
||||
|
||||
def setAllMeetingsEnded(endedReasonCode: String, endedBy: String) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[MeetingDbTableDef]
|
||||
.filter(_.endedAt.isEmpty)
|
||||
.map(a => (a.endedAt, a.endedReasonCode, a.endedBy))
|
||||
.update(
|
||||
(
|
||||
Some(new java.sql.Timestamp(System.currentTimeMillis())),
|
||||
Some(endedReasonCode),
|
||||
endedBy match {
|
||||
case "" => None
|
||||
case c => Some(c)
|
||||
}
|
||||
)
|
||||
)
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated all-meetings endedAt=now() on Meeting table!")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error updating all-meetings endedAt=now() on Meeting table: $e")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -26,11 +26,15 @@ case class UserStateDbModel(
|
||||
pinned: Boolean = false,
|
||||
locked: Boolean = false,
|
||||
speechLocale: String,
|
||||
inactivityWarningDisplay: Boolean = false,
|
||||
inactivityWarningTimeoutSecs: Option[Long],
|
||||
)
|
||||
|
||||
class UserStateDbTableDef(tag: Tag) extends Table[UserStateDbModel](tag, None, "user") {
|
||||
override def * = (
|
||||
userId,emoji,away,raiseHand,guestStatus,guestStatusSetByModerator,guestLobbyMessage,mobile,clientType,disconnected,expired,ejected,ejectReason,ejectReasonCode,ejectedByModerator,presenter,pinned,locked,speechLocale) <> (UserStateDbModel.tupled, UserStateDbModel.unapply)
|
||||
userId,emoji,away,raiseHand,guestStatus,guestStatusSetByModerator,guestLobbyMessage,mobile,clientType,disconnected,
|
||||
expired,ejected,ejectReason,ejectReasonCode,ejectedByModerator,presenter,pinned,locked,speechLocale,
|
||||
inactivityWarningDisplay, inactivityWarningTimeoutSecs) <> (UserStateDbModel.tupled, UserStateDbModel.unapply)
|
||||
val userId = column[String]("userId", O.PrimaryKey)
|
||||
val emoji = column[String]("emoji")
|
||||
val away = column[Boolean]("away")
|
||||
@ -50,6 +54,8 @@ class UserStateDbTableDef(tag: Tag) extends Table[UserStateDbModel](tag, None, "
|
||||
val pinned = column[Boolean]("pinned")
|
||||
val locked = column[Boolean]("locked")
|
||||
val speechLocale = column[String]("speechLocale")
|
||||
val inactivityWarningDisplay = column[Boolean]("inactivityWarningDisplay")
|
||||
val inactivityWarningTimeoutSecs = column[Option[Long]]("inactivityWarningTimeoutSecs")
|
||||
}
|
||||
|
||||
object UserStateDAO {
|
||||
@ -119,4 +125,21 @@ object UserStateDAO {
|
||||
}
|
||||
}
|
||||
|
||||
def updateInactivityWarning(intId: String, inactivityWarningDisplay: Boolean, inactivityWarningTimeoutSecs: Long) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[UserStateDbTableDef]
|
||||
.filter(_.userId === intId)
|
||||
.map(u => (u.inactivityWarningDisplay, u.inactivityWarningTimeoutSecs))
|
||||
.update((inactivityWarningDisplay,
|
||||
inactivityWarningTimeoutSecs match {
|
||||
case 0 => None
|
||||
case timeout: Long => Some(timeout)
|
||||
case _ => None
|
||||
}))
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated inactivityWarningDisplay on user table!")
|
||||
case Failure(e) => DatabaseConnection.logger.error(s"Error updating inactivityWarningDisplay user: $e")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,48 @@
|
||||
package org.bigbluebutton.core.db
|
||||
|
||||
import org.bigbluebutton.core.models.{ VoiceUserState }
|
||||
import slick.jdbc.PostgresProfile.api._
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.util.{ Failure, Success }
|
||||
|
||||
case class UserTranscriptionErrorDbModel(
|
||||
userId: String,
|
||||
meetingId: String,
|
||||
errorCode: String,
|
||||
errorMessage: String,
|
||||
lastUpdatedAt: java.sql.Timestamp = new java.sql.Timestamp(System.currentTimeMillis())
|
||||
)
|
||||
|
||||
class UserTranscriptionErrorDbTableDef(tag: Tag) extends Table[UserTranscriptionErrorDbModel](tag, None, "user_transcriptionError") {
|
||||
override def * = (
|
||||
userId, meetingId, errorCode, errorMessage, lastUpdatedAt
|
||||
) <> (UserTranscriptionErrorDbModel.tupled, UserTranscriptionErrorDbModel.unapply)
|
||||
val userId = column[String]("userId", O.PrimaryKey)
|
||||
val meetingId = column[String]("meetingId")
|
||||
val errorCode = column[String]("errorCode")
|
||||
val errorMessage = column[String]("errorMessage")
|
||||
val lastUpdatedAt = column[java.sql.Timestamp]("lastUpdatedAt")
|
||||
}
|
||||
|
||||
object UserTranscriptionErrorDAO {
|
||||
def insert(userId: String, meetingId: String, errorCode: String, errorMessage: String) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[UserTranscriptionErrorDbTableDef].insertOrUpdate(
|
||||
UserTranscriptionErrorDbModel(
|
||||
userId = userId,
|
||||
meetingId = meetingId,
|
||||
errorCode = errorCode,
|
||||
errorMessage = errorMessage,
|
||||
lastUpdatedAt = new java.sql.Timestamp(System.currentTimeMillis()),
|
||||
)
|
||||
)
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => {
|
||||
DatabaseConnection.logger.debug(s"$rowsAffected row(s) inserted on user_transcriptionError table!")
|
||||
}
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error inserting user_transcriptionError: $e")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -42,4 +42,5 @@ object MeetingEndReason {
|
||||
val BREAKOUT_ENDED_BY_MOD = "BREAKOUT_ENDED_BY_MOD"
|
||||
val ENDED_DUE_TO_NO_AUTHED_USER = "ENDED_DUE_TO_NO_AUTHED_USER"
|
||||
val ENDED_DUE_TO_NO_MODERATOR = "ENDED_DUE_TO_NO_MODERATOR"
|
||||
val ENDED_DUE_TO_SERVICE_INTERRUPTION = "ENDED_DUE_TO_SERVICE_INTERRUPTION"
|
||||
}
|
||||
|
@ -78,15 +78,6 @@ object Users2x {
|
||||
users.toVector.filter(u => !u.presenter)
|
||||
}
|
||||
|
||||
def getRandomlyPickableUsers(users: Users2x, reduceDup: Boolean): Vector[UserState] = {
|
||||
|
||||
if (reduceDup) {
|
||||
users.toVector.filter(u => !u.presenter && u.role != Roles.MODERATOR_ROLE && !u.userLeftFlag.left && !u.pickExempted)
|
||||
} else {
|
||||
users.toVector.filter(u => !u.presenter && u.role != Roles.MODERATOR_ROLE && !u.userLeftFlag.left)
|
||||
}
|
||||
}
|
||||
|
||||
def findViewers(users: Users2x): Vector[UserState] = {
|
||||
users.toVector.filter(u => u.role == Roles.VIEWER_ROLE)
|
||||
}
|
||||
@ -98,6 +89,19 @@ object Users2x {
|
||||
def updateLastUserActivity(users: Users2x, u: UserState): UserState = {
|
||||
val newUserState = modify(u)(_.lastActivityTime).setTo(System.currentTimeMillis())
|
||||
users.save(newUserState)
|
||||
|
||||
//Reset inactivity warning
|
||||
if (u.lastInactivityInspect != 0) {
|
||||
resetLastInactivityInspect(users, newUserState)
|
||||
} else {
|
||||
newUserState
|
||||
}
|
||||
}
|
||||
|
||||
def resetLastInactivityInspect(users: Users2x, u: UserState): UserState = {
|
||||
val newUserState = modify(u)(_.lastInactivityInspect).setTo(0)
|
||||
users.save(newUserState)
|
||||
UserStateDAO.updateInactivityWarning(u.intId, inactivityWarningDisplay = false, 0)
|
||||
newUserState
|
||||
}
|
||||
|
||||
@ -241,16 +245,6 @@ object Users2x {
|
||||
}
|
||||
}
|
||||
|
||||
def setUserExempted(users: Users2x, intId: String, exempted: Boolean): Option[UserState] = {
|
||||
for {
|
||||
u <- findWithIntId(users, intId)
|
||||
} yield {
|
||||
val newUser = u.modify(_.pickExempted).setTo(exempted)
|
||||
users.save(newUser)
|
||||
newUser
|
||||
}
|
||||
}
|
||||
|
||||
def setUserSpeechLocale(users: Users2x, intId: String, locale: String): Option[UserState] = {
|
||||
for {
|
||||
u <- findWithIntId(users, intId)
|
||||
@ -435,7 +429,6 @@ case class UserState(
|
||||
lastActivityTime: Long = System.currentTimeMillis(),
|
||||
lastInactivityInspect: Long = 0,
|
||||
clientType: String,
|
||||
pickExempted: Boolean,
|
||||
userLeftFlag: UserLeftFlag,
|
||||
speechLocale: String = ""
|
||||
)
|
||||
|
@ -119,8 +119,6 @@ class ReceivedJsonMsgHandlerActor(
|
||||
routeGenericMsg[SetUserSpeechLocaleReqMsg](envelope, jsonNode)
|
||||
case SetUserSpeechOptionsReqMsg.NAME =>
|
||||
routeGenericMsg[SetUserSpeechOptionsReqMsg](envelope, jsonNode)
|
||||
case SelectRandomViewerReqMsg.NAME =>
|
||||
routeGenericMsg[SelectRandomViewerReqMsg](envelope, jsonNode)
|
||||
|
||||
// Poll
|
||||
case StartCustomPollReqMsg.NAME =>
|
||||
|
@ -73,7 +73,6 @@ trait HandlerHelpers extends SystemConfiguration {
|
||||
avatar = regUser.avatarURL,
|
||||
color = regUser.color,
|
||||
clientType = clientType,
|
||||
pickExempted = false,
|
||||
userLeftFlag = UserLeftFlag(false, 0)
|
||||
)
|
||||
}
|
||||
|
@ -402,13 +402,12 @@ class MeetingActor(
|
||||
case m: UserReactionTimeExpiredCmdMsg => handleUserReactionTimeExpiredCmdMsg(m)
|
||||
case m: ClearAllUsersEmojiCmdMsg => handleClearAllUsersEmojiCmdMsg(m)
|
||||
case m: ClearAllUsersReactionCmdMsg => handleClearAllUsersReactionCmdMsg(m)
|
||||
case m: SelectRandomViewerReqMsg => usersApp.handleSelectRandomViewerReqMsg(m)
|
||||
case m: ChangeUserPinStateReqMsg => usersApp.handleChangeUserPinStateReqMsg(m)
|
||||
case m: ChangeUserMobileFlagReqMsg => usersApp.handleChangeUserMobileFlagReqMsg(m)
|
||||
case m: UserConnectionAliveReqMsg => usersApp.handleUserConnectionAliveReqMsg(m)
|
||||
case m: UserConnectionUpdateRttReqMsg => usersApp.handleUserConnectionUpdateRttReqMsg(m)
|
||||
case m: SetUserSpeechLocaleReqMsg => usersApp.handleSetUserSpeechLocaleReqMsg(m)
|
||||
case m: SetUserSpeechOptionsReqMsg => usersApp.handleSetUserSpeechOptionsReqMsg(m)
|
||||
case m: SetUserSpeechOptionsReqMsg => usersApp.handleSetUserSpeechOptionsReqMsg(m)
|
||||
|
||||
// Client requested to eject user
|
||||
case m: EjectUserFromMeetingCmdMsg =>
|
||||
@ -986,6 +985,7 @@ class MeetingActor(
|
||||
|
||||
val secsToDisconnect = TimeUnit.MILLISECONDS.toSeconds(expiryTracker.userActivitySignResponseDelayInMs);
|
||||
Sender.sendUserInactivityInspectMsg(liveMeeting.props.meetingProp.intId, u.intId, secsToDisconnect, outGW)
|
||||
UserStateDAO.updateInactivityWarning(u.intId, inactivityWarningDisplay = true, secsToDisconnect)
|
||||
updateUserLastInactivityInspect(u.intId)
|
||||
}
|
||||
}
|
||||
|
@ -69,8 +69,7 @@ trait FakeTestData {
|
||||
UserState(intId = regUser.id, extId = regUser.externId, name = regUser.name, role = regUser.role, pin = false,
|
||||
mobile = false, guest = regUser.guest, authed = regUser.authed, guestStatus = regUser.guestStatus,
|
||||
emoji = "none", reactionEmoji = "none", raiseHand = false, away = false, locked = false, presenter = false,
|
||||
avatar = regUser.avatarURL, color = "#ff6242", clientType = "unknown",
|
||||
pickExempted = false, userLeftFlag = UserLeftFlag(false, 0))
|
||||
avatar = regUser.avatarURL, color = "#ff6242", clientType = "unknown", userLeftFlag = UserLeftFlag(false, 0))
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ object TestDataGen {
|
||||
guest = regUser.guest, authed = regUser.authed, guestStatus = regUser.guestStatus,
|
||||
emoji = "none", reactionEmoji = "none", raiseHand = false, away = false, pin = false, mobile = false,
|
||||
locked = false, presenter = false, avatar = regUser.avatarURL, color = "#ff6242",
|
||||
clientType = "unknown", pickExempted = false, userLeftFlag = UserLeftFlag(false, 0))
|
||||
clientType = "unknown", userLeftFlag = UserLeftFlag(false, 0))
|
||||
Users2x.add(liveMeeting.users2x, u)
|
||||
u
|
||||
}
|
||||
|
@ -525,20 +525,6 @@ object UserActivitySignCmdMsg { val NAME = "UserActivitySignCmdMsg" }
|
||||
case class UserActivitySignCmdMsg(header: BbbClientMsgHeader, body: UserActivitySignCmdMsgBody) extends StandardMsg
|
||||
case class UserActivitySignCmdMsgBody(userId: String)
|
||||
|
||||
/**
|
||||
* Sent from client to randomly select a viewer
|
||||
*/
|
||||
object SelectRandomViewerReqMsg { val NAME = "SelectRandomViewerReqMsg" }
|
||||
case class SelectRandomViewerReqMsg(header: BbbClientMsgHeader, body: SelectRandomViewerReqMsgBody) extends StandardMsg
|
||||
case class SelectRandomViewerReqMsgBody(requestedBy: String)
|
||||
|
||||
/**
|
||||
* Response to request for a random viewer
|
||||
*/
|
||||
object SelectRandomViewerRespMsg { val NAME = "SelectRandomViewerRespMsg" }
|
||||
case class SelectRandomViewerRespMsg(header: BbbClientMsgHeader, body: SelectRandomViewerRespMsgBody) extends StandardMsg
|
||||
case class SelectRandomViewerRespMsgBody(requestedBy: String, userIds: Vector[String], choice: String)
|
||||
|
||||
object SetUserSpeechLocaleReqMsg { val NAME = "SetUserSpeechLocaleReqMsg" }
|
||||
case class SetUserSpeechLocaleReqMsg(header: BbbClientMsgHeader, body: SetUserSpeechLocaleReqMsgBody) extends StandardMsg
|
||||
case class SetUserSpeechLocaleReqMsgBody(locale: String, provider: String)
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { RedisMessage } from '../types';
|
||||
import {throwErrorIfNotPresenter} from "../imports/validation";
|
||||
|
||||
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {
|
||||
throwErrorIfNotPresenter(sessionVariables);
|
||||
const eventName = `SelectRandomViewerReqMsg`;
|
||||
const eventName = `UserActivitySignCmdMsg`;
|
||||
|
||||
const routing = {
|
||||
meetingId: sessionVariables['x-hasura-meetingid'] as String,
|
||||
@ -17,7 +15,7 @@ export default function buildRedisMessage(sessionVariables: Record<string, unkno
|
||||
};
|
||||
|
||||
const body = {
|
||||
requestedBy: routing.userId
|
||||
userId: routing.userId
|
||||
};
|
||||
|
||||
return { eventName, routing, header, body };
|
@ -3,7 +3,7 @@ BBB_GRAPHQL_MIDDLEWARE_LISTEN_PORT=8378
|
||||
BBB_GRAPHQL_MIDDLEWARE_REDIS_ADDRESS=127.0.0.1:6379
|
||||
BBB_GRAPHQL_MIDDLEWARE_REDIS_PASSWORD=
|
||||
BBB_GRAPHQL_MIDDLEWARE_HASURA_WS=ws://127.0.0.1:8080/v1/graphql
|
||||
BBB_GRAPHQL_MIDDLEWARE_RATE_LIMIT_IN_MS=50
|
||||
BBB_GRAPHQL_MIDDLEWARE_MAX_CONN_PER_SECOND=10
|
||||
|
||||
# If you are running a cluster proxy setup, you need to configure the Origin of
|
||||
# the frontend. See https://docs.bigbluebutton.org/administration/cluster-proxy
|
||||
|
@ -2,12 +2,12 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/iMDT/bbb-graphql-middleware/internal/common"
|
||||
"github.com/iMDT/bbb-graphql-middleware/internal/msgpatch"
|
||||
"github.com/iMDT/bbb-graphql-middleware/internal/websrv"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/time/rate"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
@ -53,20 +53,23 @@ func main() {
|
||||
}
|
||||
|
||||
//Define new Connections Rate Limit
|
||||
rateLimitInMs := 50
|
||||
if envRateLimitInMs := os.Getenv("BBB_GRAPHQL_MIDDLEWARE_RATE_LIMIT_IN_MS"); envRateLimitInMs != "" {
|
||||
if envRateLimitInMsAsInt, err := strconv.Atoi(envRateLimitInMs); err == nil {
|
||||
rateLimitInMs = envRateLimitInMsAsInt
|
||||
maxConnPerSecond := 10
|
||||
if envMaxConnPerSecond := os.Getenv("BBB_GRAPHQL_MIDDLEWARE_MAX_CONN_PER_SECOND"); envMaxConnPerSecond != "" {
|
||||
if envMaxConnPerSecondAsInt, err := strconv.Atoi(envMaxConnPerSecond); err == nil {
|
||||
maxConnPerSecond = envMaxConnPerSecondAsInt
|
||||
}
|
||||
}
|
||||
limiterInterval := rate.NewLimiter(rate.Every(time.Duration(rateLimitInMs)*time.Millisecond), 1)
|
||||
rateLimiter := common.NewCustomRateLimiter(maxConnPerSecond)
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 120*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := limiterInterval.Wait(ctx); err != nil {
|
||||
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
|
||||
if err := rateLimiter.Wait(ctx); err != nil {
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
http.Error(w, "Request cancelled or rate limit exceeded", http.StatusTooManyRequests)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
64
bbb-graphql-middleware/internal/common/CustomRateLimiter.go
Normal file
64
bbb-graphql-middleware/internal/common/CustomRateLimiter.go
Normal file
@ -0,0 +1,64 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CustomRateLimiter struct {
|
||||
tokens chan struct{}
|
||||
requestQueue chan context.Context
|
||||
}
|
||||
|
||||
func NewCustomRateLimiter(requestsPerSecond int) *CustomRateLimiter {
|
||||
rl := &CustomRateLimiter{
|
||||
tokens: make(chan struct{}, requestsPerSecond),
|
||||
requestQueue: make(chan context.Context, 20000), // Adjust the size accordingly
|
||||
}
|
||||
|
||||
go rl.refillTokens(requestsPerSecond)
|
||||
go rl.processQueue()
|
||||
|
||||
return rl
|
||||
}
|
||||
|
||||
func (rl *CustomRateLimiter) refillTokens(requestsPerSecond int) {
|
||||
ticker := time.NewTicker(time.Second / time.Duration(requestsPerSecond))
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
// Try to add a token, skip if full
|
||||
select {
|
||||
case rl.tokens <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *CustomRateLimiter) processQueue() {
|
||||
for ctx := range rl.requestQueue {
|
||||
select {
|
||||
case <-rl.tokens:
|
||||
if ctx.Err() == nil {
|
||||
// Token acquired and context not cancelled, proceed
|
||||
// Simulate processing by calling a dummy function
|
||||
// processRequest() or similar
|
||||
}
|
||||
case <-ctx.Done():
|
||||
// Context cancelled, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *CustomRateLimiter) Wait(ctx context.Context) error {
|
||||
rl.requestQueue <- ctx
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Request cancelled
|
||||
return ctx.Err()
|
||||
case <-rl.tokens:
|
||||
// Acquired token, proceed
|
||||
return nil
|
||||
}
|
||||
}
|
@ -67,10 +67,13 @@ func ConnectionHandler(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
msgpatch.RemoveConnCacheDir(browserConnectionId)
|
||||
BrowserConnectionsMutex.Lock()
|
||||
sessionTokenRemoved := BrowserConnections[browserConnectionId].SessionToken
|
||||
delete(BrowserConnections, browserConnectionId)
|
||||
_, bcExists := BrowserConnections[browserConnectionId]
|
||||
if bcExists {
|
||||
sessionTokenRemoved := BrowserConnections[browserConnectionId].SessionToken
|
||||
delete(BrowserConnections, browserConnectionId)
|
||||
go SendUserGraphqlConnectionClosedSysMsg(sessionTokenRemoved, browserConnectionId)
|
||||
}
|
||||
BrowserConnectionsMutex.Unlock()
|
||||
go SendUserGraphqlConnectionClosedSysMsg(sessionTokenRemoved, browserConnectionId)
|
||||
|
||||
log.Infof("connection removed")
|
||||
}()
|
||||
@ -135,7 +138,71 @@ func ConnectionHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func InvalidateSessionTokenConnections(sessionTokenToInvalidate string) {
|
||||
BrowserConnectionsMutex.RLock()
|
||||
connectionsToProcess := make([]*common.BrowserConnection, 0)
|
||||
for _, browserConnection := range BrowserConnections {
|
||||
if browserConnection.SessionToken == sessionTokenToInvalidate {
|
||||
connectionsToProcess = append(connectionsToProcess, browserConnection)
|
||||
}
|
||||
}
|
||||
BrowserConnectionsMutex.RUnlock()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, browserConnection := range connectionsToProcess {
|
||||
wg.Add(1)
|
||||
go func(bc *common.BrowserConnection) {
|
||||
defer wg.Done()
|
||||
invalidateBrowserConnection(bc, sessionTokenToInvalidate)
|
||||
}(browserConnection)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func invalidateBrowserConnection(bc *common.BrowserConnection, sessionToken string) {
|
||||
if bc.HasuraConnection == nil {
|
||||
return // If there's no Hasura connection, there's nothing to invalidate.
|
||||
}
|
||||
|
||||
hasuraConnectionId := bc.HasuraConnection.Id
|
||||
|
||||
// Send message to stop receiving new messages from the browser.
|
||||
bc.HasuraConnection.FreezeMsgFromBrowserChan.Send(true)
|
||||
|
||||
// Wait until there are no active mutations.
|
||||
for iterationCount := 0; iterationCount < 20; iterationCount++ {
|
||||
activeMutationFound := false
|
||||
bc.ActiveSubscriptionsMutex.RLock()
|
||||
for _, subscription := range bc.ActiveSubscriptions {
|
||||
if subscription.Type == common.Mutation {
|
||||
activeMutationFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
bc.ActiveSubscriptionsMutex.RUnlock()
|
||||
|
||||
if !activeMutationFound {
|
||||
break // Exit the loop if no active mutations are found.
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond) // Wait a bit before checking again.
|
||||
}
|
||||
|
||||
log.Debugf("Processing invalidate request for sessionToken %v (hasura connection %v)", sessionToken, hasuraConnectionId)
|
||||
|
||||
// Cancel the Hasura connection context to clean up resources.
|
||||
if bc.HasuraConnection != nil && bc.HasuraConnection.ContextCancelFunc != nil {
|
||||
bc.HasuraConnection.ContextCancelFunc()
|
||||
}
|
||||
|
||||
log.Debugf("Processed invalidate request for sessionToken %v (hasura connection %v)", sessionToken, hasuraConnectionId)
|
||||
|
||||
// Send a reconnection confirmation message
|
||||
go SendUserGraphqlReconnectionForcedEvtMsg(sessionToken)
|
||||
}
|
||||
|
||||
func InvalidateSessionTokenConnectionsB(sessionTokenToInvalidate string) {
|
||||
BrowserConnectionsMutex.RLock()
|
||||
for _, browserConnection := range BrowserConnections {
|
||||
hasuraConnectionId := browserConnection.HasuraConnection.Id
|
||||
|
||||
if browserConnection.SessionToken == sessionTokenToInvalidate {
|
||||
if browserConnection.HasuraConnection != nil {
|
||||
//Send message to force stop receiving new messages from the browser
|
||||
@ -159,9 +226,10 @@ func InvalidateSessionTokenConnections(sessionTokenToInvalidate string) {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
hasuraConnectionId := browserConnection.HasuraConnection.Id
|
||||
log.Debugf("Processing invalidate request for sessionToken %v (hasura connection %v)", sessionTokenToInvalidate, hasuraConnectionId)
|
||||
browserConnection.HasuraConnection.ContextCancelFunc()
|
||||
if browserConnection.HasuraConnection != nil {
|
||||
browserConnection.HasuraConnection.ContextCancelFunc()
|
||||
}
|
||||
log.Debugf("Processed invalidate request for sessionToken %v (hasura connection %v)", sessionTokenToInvalidate, hasuraConnectionId)
|
||||
|
||||
go SendUserGraphqlReconnectionForcedEvtMsg(browserConnection.SessionToken)
|
||||
|
@ -26,6 +26,8 @@ create table "meeting" (
|
||||
"bannerColor" varchar(50),
|
||||
"createdTime" bigint,
|
||||
"durationInSeconds" integer,
|
||||
"endWhenNoModerator" boolean,
|
||||
"endWhenNoModeratorDelayInMinutes" integer,
|
||||
"endedAt" timestamp with time zone,
|
||||
"endedReasonCode" varchar(200),
|
||||
"endedBy" varchar(50)
|
||||
@ -126,16 +128,16 @@ create view "v_meeting_voiceSettings" as select * from meeting_voice;
|
||||
|
||||
create table "meeting_usersPolicies" (
|
||||
"meetingId" varchar(100) primary key references "meeting"("meetingId") ON DELETE CASCADE,
|
||||
"maxUsers" integer,
|
||||
"maxUsers" integer,
|
||||
"maxUserConcurrentAccesses" integer,
|
||||
"webcamsOnlyForModerator" boolean,
|
||||
"userCameraCap" integer,
|
||||
"guestPolicy" varchar(100),
|
||||
"guestLobbyMessage" text,
|
||||
"meetingLayout" varchar(100),
|
||||
"allowModsToUnmuteUsers" boolean,
|
||||
"allowModsToEjectCameras" boolean,
|
||||
"authenticatedGuest" boolean
|
||||
"webcamsOnlyForModerator" boolean,
|
||||
"userCameraCap" integer,
|
||||
"guestPolicy" varchar(100),
|
||||
"guestLobbyMessage" text,
|
||||
"meetingLayout" varchar(100),
|
||||
"allowModsToUnmuteUsers" boolean,
|
||||
"allowModsToEjectCameras" boolean,
|
||||
"authenticatedGuest" boolean
|
||||
);
|
||||
create index "idx_meeting_usersPolicies_meetingId" on "meeting_usersPolicies"("meetingId");
|
||||
|
||||
@ -156,6 +158,20 @@ SELECT "meeting_usersPolicies"."meetingId",
|
||||
FROM "meeting_usersPolicies"
|
||||
JOIN "meeting" using("meetingId");
|
||||
|
||||
create table "meeting_metadata" (
|
||||
"meetingId" varchar(100) references "meeting"("meetingId") ON DELETE CASCADE,
|
||||
"name" varchar(100),
|
||||
"value" varchar(100),
|
||||
CONSTRAINT "meeting_metadata_pkey" PRIMARY KEY ("meetingId","name")
|
||||
);
|
||||
create index "idx_meeting_metadata_meetingId" on "meeting_metadata"("meetingId");
|
||||
|
||||
CREATE OR REPLACE VIEW "v_meeting_metadata" AS
|
||||
SELECT "meeting_metadata"."meetingId",
|
||||
"meeting_metadata"."name",
|
||||
"meeting_metadata"."value"
|
||||
FROM "meeting_metadata";
|
||||
|
||||
create table "meeting_lockSettings" (
|
||||
"meetingId" varchar(100) primary key references "meeting"("meetingId") ON DELETE CASCADE,
|
||||
"disableCam" boolean,
|
||||
@ -182,6 +198,8 @@ SELECT
|
||||
mls."hideUserList",
|
||||
mls."hideViewersCursor",
|
||||
mls."hideViewersAnnotation",
|
||||
mls."lockOnJoin",
|
||||
mls."lockOnJoinConfigurable",
|
||||
mup."webcamsOnlyForModerator",
|
||||
CASE WHEN
|
||||
mls."disableCam" IS TRUE THEN TRUE
|
||||
@ -266,11 +284,13 @@ CREATE TABLE "user" (
|
||||
"ejected" bool,
|
||||
"ejectReason" varchar(255),
|
||||
"ejectReasonCode" varchar(50),
|
||||
"ejectedByModerator" varchar(50) references "user"("userId") ON DELETE SET NULL,
|
||||
"ejectedByModerator" varchar(50),
|
||||
"presenter" bool,
|
||||
"pinned" bool,
|
||||
"locked" bool,
|
||||
"speechLocale" varchar(255),
|
||||
"inactivityWarningDisplay" bool default FALSE,
|
||||
"inactivityWarningTimeoutSecs" numeric,
|
||||
"hasDrawPermissionOnCurrentPage" bool default FALSE,
|
||||
"echoTestRunningAt" timestamp with time zone
|
||||
);
|
||||
@ -427,7 +447,9 @@ AS SELECT "user"."userId",
|
||||
"user"."echoTestRunningAt",
|
||||
CASE WHEN "user"."echoTestRunningAt" > current_timestamp - INTERVAL '3 seconds' THEN TRUE ELSE FALSE END "isRunningEchoTest",
|
||||
CASE WHEN "user"."role" = 'MODERATOR' THEN true ELSE false END "isModerator",
|
||||
CASE WHEN "user"."joined" IS true AND "user"."expired" IS false AND "user"."loggedOut" IS false AND "user"."ejected" IS NOT TRUE THEN true ELSE false END "isOnline"
|
||||
CASE WHEN "user"."joined" IS true AND "user"."expired" IS false AND "user"."loggedOut" IS false AND "user"."ejected" IS NOT TRUE THEN true ELSE false END "isOnline",
|
||||
"user"."inactivityWarningDisplay",
|
||||
"user"."inactivityWarningTimeoutSecs"
|
||||
FROM "user";
|
||||
|
||||
--This view will be used by Meteor to validate if the provided authToken is valid
|
||||
@ -848,6 +870,20 @@ FROM "user" u
|
||||
JOIN "user_reaction" ur ON u."userId" = ur."userId" AND "expiresAt" > current_timestamp
|
||||
GROUP BY u."meetingId", ur."userId";
|
||||
|
||||
CREATE TABLE "user_transcriptionError"(
|
||||
"userId" varchar(50) PRIMARY KEY REFERENCES "user"("userId") ON DELETE CASCADE,
|
||||
"meetingId" varchar(100) references "meeting"("meetingId") ON DELETE CASCADE,
|
||||
"errorCode" varchar(255),
|
||||
"errorMessage" text,
|
||||
"lastUpdatedAt" timestamp with time zone DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX "idx_user_transcriptionError_meetingId" ON "user_transcriptionError"("meetingId");
|
||||
CREATE INDEX "idx_user_transcriptionError_userId" ON "user_transcriptionError"("userId");
|
||||
|
||||
create view "v_user_transcriptionError" as select * from "user_transcriptionError";
|
||||
|
||||
|
||||
|
||||
|
||||
create view "v_meeting" as
|
||||
|
@ -355,10 +355,6 @@ type Mutation {
|
||||
): Boolean
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
randomViewerPick: Boolean
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
sharedNotesCreateSession(
|
||||
sharedNotesExtId: String!
|
||||
@ -446,6 +442,10 @@ type Mutation {
|
||||
userLeaveMeeting: Boolean
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
userSendActivitySign: Boolean
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
userSetAway(
|
||||
away: Boolean!
|
||||
@ -562,3 +562,4 @@ input GuestUserApprovalStatus {
|
||||
guest: String!
|
||||
status: String!
|
||||
}
|
||||
|
||||
|
@ -306,12 +306,6 @@ actions:
|
||||
handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}'
|
||||
permissions:
|
||||
- role: bbb_client
|
||||
- name: randomViewerPick
|
||||
definition:
|
||||
kind: synchronous
|
||||
handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}'
|
||||
permissions:
|
||||
- role: bbb_client
|
||||
- name: sharedNotesCreateSession
|
||||
definition:
|
||||
kind: synchronous
|
||||
@ -403,6 +397,12 @@ actions:
|
||||
handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}'
|
||||
permissions:
|
||||
- role: bbb_client
|
||||
- name: userSendActivitySign
|
||||
definition:
|
||||
kind: synchronous
|
||||
handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}'
|
||||
permissions:
|
||||
- role: bbb_client
|
||||
- name: userSetAway
|
||||
definition:
|
||||
kind: synchronous
|
||||
@ -504,4 +504,5 @@ custom_types:
|
||||
input_objects:
|
||||
- name: BreakoutRoom
|
||||
- name: GuestUserApprovalStatus
|
||||
objects: []
|
||||
scalars: []
|
||||
|
@ -134,6 +134,15 @@ array_relationships:
|
||||
remote_table:
|
||||
name: v_meeting_group
|
||||
schema: public
|
||||
- name: metadata
|
||||
using:
|
||||
manual_configuration:
|
||||
column_mapping:
|
||||
meetingId: meetingId
|
||||
insertion_order: null
|
||||
remote_table:
|
||||
name: v_meeting_metadata
|
||||
schema: public
|
||||
- name: polls
|
||||
using:
|
||||
manual_configuration:
|
||||
@ -154,6 +163,8 @@ select_permissions:
|
||||
- customLogoUrl
|
||||
- disabledFeatures
|
||||
- durationInSeconds
|
||||
- endWhenNoModerator
|
||||
- endWhenNoModeratorDelayInMinutes
|
||||
- ended
|
||||
- endedAt
|
||||
- endedBy
|
||||
|
@ -22,4 +22,4 @@ select_permissions:
|
||||
filter:
|
||||
meetingId:
|
||||
_eq: X-Hasura-MeetingId
|
||||
comment: ""
|
||||
comment: ""
|
||||
|
@ -17,8 +17,10 @@ select_permissions:
|
||||
- disablePublicChat
|
||||
- hasActiveLockSetting
|
||||
- hideUserList
|
||||
- hideViewersCursor
|
||||
- hideViewersAnnotation
|
||||
- hideViewersCursor
|
||||
- lockOnJoin
|
||||
- lockOnJoinConfigurable
|
||||
- webcamsOnlyForModerator
|
||||
filter:
|
||||
meetingId:
|
||||
|
@ -0,0 +1,18 @@
|
||||
table:
|
||||
name: v_meeting_metadata
|
||||
schema: public
|
||||
configuration:
|
||||
column_config: {}
|
||||
custom_column_names: {}
|
||||
custom_name: meeting_metadata
|
||||
custom_root_fields: {}
|
||||
select_permissions:
|
||||
- role: bbb_client
|
||||
permission:
|
||||
columns:
|
||||
- name
|
||||
- value
|
||||
filter:
|
||||
meetingId:
|
||||
_eq: X-Hasura-MeetingId
|
||||
comment: ""
|
@ -10,6 +10,7 @@ select_permissions:
|
||||
- role: bbb_client
|
||||
permission:
|
||||
columns:
|
||||
- contentType
|
||||
- hasAudio
|
||||
- screenshareConf
|
||||
- screenshareId
|
||||
|
@ -70,6 +70,15 @@ object_relationships:
|
||||
remote_table:
|
||||
name: v_sharedNotes_session
|
||||
schema: public
|
||||
- name: transcriptionError
|
||||
using:
|
||||
manual_configuration:
|
||||
column_mapping:
|
||||
userId: userId
|
||||
insertion_order: null
|
||||
remote_table:
|
||||
name: v_user_transcriptionError
|
||||
schema: public
|
||||
- name: userClientSettings
|
||||
using:
|
||||
manual_configuration:
|
||||
@ -168,6 +177,8 @@ select_permissions:
|
||||
- registeredOn
|
||||
- role
|
||||
- speechLocale
|
||||
- inactivityWarningDisplay
|
||||
- inactivityWarningTimeoutSecs
|
||||
- userId
|
||||
filter:
|
||||
userId:
|
||||
@ -212,4 +223,4 @@ update_permissions:
|
||||
_eq: X-Hasura-MeetingId
|
||||
- userId:
|
||||
_eq: X-Hasura-UserId
|
||||
check: null
|
||||
check: null
|
||||
|
@ -0,0 +1,19 @@
|
||||
table:
|
||||
name: v_user_transcriptionError
|
||||
schema: public
|
||||
configuration:
|
||||
column_config: {}
|
||||
custom_column_names: {}
|
||||
custom_name: user_transcriptionError
|
||||
custom_root_fields: {}
|
||||
select_permissions:
|
||||
- role: bbb_client
|
||||
permission:
|
||||
columns:
|
||||
- errorCode
|
||||
- errorMessage
|
||||
- lastUpdatedAt
|
||||
filter:
|
||||
userId:
|
||||
_eq: X-Hasura-UserId
|
||||
comment: ""
|
@ -18,6 +18,7 @@
|
||||
- "!include public_v_meeting_group.yaml"
|
||||
- "!include public_v_meeting_learningDashboard.yaml"
|
||||
- "!include public_v_meeting_lockSettings.yaml"
|
||||
- "!include public_v_meeting_metadata.yaml"
|
||||
- "!include public_v_meeting_recording.yaml"
|
||||
- "!include public_v_meeting_recordingPolicies.yaml"
|
||||
- "!include public_v_meeting_usersPolicies.yaml"
|
||||
@ -53,6 +54,7 @@
|
||||
- "!include public_v_user_reaction.yaml"
|
||||
- "!include public_v_user_reaction_current.yaml"
|
||||
- "!include public_v_user_ref.yaml"
|
||||
- "!include public_v_user_transcriptionError.yaml"
|
||||
- "!include public_v_user_typing_private.yaml"
|
||||
- "!include public_v_user_typing_public.yaml"
|
||||
- "!include public_v_user_voice.yaml"
|
||||
|
@ -22,10 +22,12 @@
|
||||
query: |
|
||||
query clientStartupSettings {
|
||||
meeting_clientSettings {
|
||||
skipMeteorConnection: clientSettingsJson(path: "$.public.app.skipMeteorConnection")
|
||||
askForFeedbackOnLogout: clientSettingsJson(path: "$.public.app.askForFeedbackOnLogout")
|
||||
allowDefaultLogoutUrl: clientSettingsJson(path: "$.public.app.allowDefaultLogoutUrl")
|
||||
learningDashboardBase: clientSettingsJson(path: "$.public.app.learningDashboardBase")
|
||||
fallbackLocale: clientSettingsJson(path: "$.public.app.defaultSettings.application.fallbackLocale")
|
||||
overrideLocale: clientSettingsJson(path: "$.public.app.defaultSettings.application.overrideLocale")
|
||||
fallbackOnEmptyString: clientSettingsJson(path: "$.public.app.fallbackOnEmptyLocaleString")
|
||||
clientLog: clientSettingsJson(path: "$.public.clientLog")
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
git clone --branch v2.13.2 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu
|
||||
git clone --branch v2.14.0-beta.0 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu
|
||||
|
@ -4,22 +4,15 @@ import React, {
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import { LoadingContext } from '/imports/ui/components/common/loading-screen/loading-screen-HOC/component';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
|
||||
const MeetingClientLazy = React.lazy(() => import('./meetingClient'));
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
loadingClientLabel: {
|
||||
id: 'app.meeting.loadingClient',
|
||||
description: 'loading client label',
|
||||
},
|
||||
});
|
||||
|
||||
const ClientStartup: React.FC = () => {
|
||||
const loadingContextInfo = useContext(LoadingContext);
|
||||
const intl = useIntl();
|
||||
useEffect(() => {
|
||||
loadingContextInfo.setLoading(true, intl.formatMessage(intlMessages.loadingClientLabel));
|
||||
logger.info('Loading client');
|
||||
loadingContextInfo.setLoading(true, '4/4');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
@ -12,8 +12,10 @@ import LocatedErrorBoundary from '/imports/ui/components/common/error-boundary/l
|
||||
import StartupDataFetch from '/imports/ui/components/connection-manager/startup-data-fetch/component';
|
||||
import UserGrapQlMiniMongoAdapter from '/imports/ui/components/components-data/userGrapQlMiniMongoAdapter/component';
|
||||
import VoiceUserGrapQlMiniMongoAdapter from '/imports/ui/components/components-data/voiceUserGraphQlMiniMongoAdapter/component';
|
||||
import MeetingGrapQlMiniMongoAdapter from '/imports/ui/components/components-data/meetingGrapQlMiniMongoAdapter/component';
|
||||
|
||||
const Main: React.FC = () => {
|
||||
// Meteor.disconnect();
|
||||
return (
|
||||
<StartupDataFetch>
|
||||
<ErrorBoundary Fallback={ErrorScreen}>
|
||||
@ -26,6 +28,7 @@ const Main: React.FC = () => {
|
||||
<SettingsLoader />
|
||||
<UserGrapQlMiniMongoAdapter />
|
||||
<VoiceUserGrapQlMiniMongoAdapter />
|
||||
<MeetingGrapQlMiniMongoAdapter />
|
||||
</PresenceManager>
|
||||
</ConnectionManager>
|
||||
</LocatedErrorBoundary>
|
||||
|
@ -56,8 +56,8 @@ const Startup = () => {
|
||||
if (disableWebsocketFallback) {
|
||||
Meteor.connection._stream._sockjsProtocolsWhitelist = function () { return ['websocket']; };
|
||||
|
||||
Meteor.disconnect();
|
||||
Meteor.reconnect();
|
||||
// Meteor.disconnect();
|
||||
// Meteor.reconnect();
|
||||
}
|
||||
}, []);
|
||||
// Logs all uncaught exceptions to the client logger
|
||||
@ -84,16 +84,14 @@ const Startup = () => {
|
||||
|
||||
return (
|
||||
<ContextProviders>
|
||||
<>
|
||||
<PresenceAdapter>
|
||||
<Subscriptions>
|
||||
<IntlAdapter>
|
||||
<Base />
|
||||
</IntlAdapter>
|
||||
</Subscriptions>
|
||||
</PresenceAdapter>
|
||||
<UsersAdapter />
|
||||
</>
|
||||
<PresenceAdapter>
|
||||
<Subscriptions>
|
||||
<IntlAdapter>
|
||||
<Base />
|
||||
</IntlAdapter>
|
||||
</Subscriptions>
|
||||
</PresenceAdapter>
|
||||
<UsersAdapter />
|
||||
</ContextProviders>
|
||||
);
|
||||
};
|
||||
|
40
bigbluebutton-html5/footer
Normal file
40
bigbluebutton-html5/footer
Normal file
@ -0,0 +1,40 @@
|
||||
<script type="text/javascript">
|
||||
const settings = {
|
||||
"meteorRelease": "METEOR@2.13",
|
||||
"gitCommitHash": "0318597bfe093e809bbdf4ade59fb5f19ae15e04",
|
||||
"meteorEnv": {
|
||||
"NODE_ENV": "production",
|
||||
"AUTOUPDATE_VERSION": "",
|
||||
"TEST_METADATA": "{}"
|
||||
},
|
||||
"PUBLIC_SETTINGS": {},
|
||||
"ROOT_URL": "http://127.0.0.1/html5client",
|
||||
"ROOT_URL_PATH_PREFIX": "/html5client",
|
||||
"reactFastRefreshEnabled": true,
|
||||
"autoupdate": {
|
||||
"versions": {
|
||||
"web.browser": {
|
||||
"version": "129e1e0e93dfd823c36328bae6f4cd73293d08dae",
|
||||
"versionRefreshable": "1ca60b72d8873b367dd36c2874baad4c7d7cdd364",
|
||||
"versionNonRefreshable": "1a2c0b155ba4282801e28e325ab6e2330f82b435e",
|
||||
"versionReplaceable": "11952018619999f014765d73c14db1f446971e849"
|
||||
},
|
||||
"web.browser.legacy": {
|
||||
"version": "465de24d63ed3a853861b20ddfd2336b1a69f3f6",
|
||||
"versionRefreshable": "ca60b72d8873b367dd36c2874baad4c7d7cdd364",
|
||||
"versionNonRefreshable": "c089304b8954fb096aecb20aed4b0dfa2c29561c",
|
||||
"versionReplaceable": "1952018619999f014765d73c14db1f446971e849"
|
||||
}
|
||||
},
|
||||
"autoupdateVersion": null,
|
||||
"autoupdateVersionRefreshable": null,
|
||||
"autoupdateVersionCordova": null,
|
||||
"appId": "jrnkwdjvicqgy6gtl8"
|
||||
},
|
||||
"appId": "jrnkwdjvicqgy6gtl8",
|
||||
"isModern": true,
|
||||
};
|
||||
__meteor_runtime_config__ = JSON.parse(decodeURIComponent(`${encodeURIComponent(JSON.stringify(settings))}`));
|
||||
</script>
|
||||
<script type="text/javascript" src="/html5client/placeholder?meteor_js_resource=true"></script>
|
||||
<script>Object.values(Meteor.connection._subscriptions).find(subs => subs.name === "meteor_autoupdate_clientVersions").stop()</script>
|
@ -11,7 +11,6 @@ import handleRecordingStatusChange from './handlers/recordingStatusChange';
|
||||
import handleRecordingTimerChange from './handlers/recordingTimerChange';
|
||||
import handleTimeRemainingUpdate from './handlers/timeRemainingUpdate';
|
||||
import handleChangeWebcamOnlyModerator from './handlers/webcamOnlyModerator';
|
||||
import handleSelectRandomViewer from './handlers/selectRandomViewer';
|
||||
import handleBroadcastLayout from './handlers/broadcastLayout';
|
||||
import handleNotifyAllInMeetingEvtMsg from './handlers/handleNotifyAllInMeetingEvtMsg';
|
||||
import handleNotifyUserInMeeting from './handlers/handleNotifyUserInMeeting';
|
||||
@ -31,7 +30,7 @@ RedisPubSub.on('GetLockSettingsRespMsg', handleMeetingLocksChange);
|
||||
RedisPubSub.on('GuestPolicyChangedEvtMsg', handleGuestPolicyChanged);
|
||||
RedisPubSub.on('GuestLobbyMessageChangedEvtMsg', handleGuestLobbyMessageChanged);
|
||||
RedisPubSub.on('MeetingTimeRemainingUpdateEvtMsg', handleTimeRemainingUpdate);
|
||||
RedisPubSub.on('SelectRandomViewerRespMsg', handleSelectRandomViewer);
|
||||
|
||||
RedisPubSub.on('BroadcastLayoutEvtMsg', handleBroadcastLayout);
|
||||
RedisPubSub.on('NotifyAllInMeetingEvtMsg', handleNotifyAllInMeetingEvtMsg);
|
||||
RedisPubSub.on('NotifyUserInMeetingEvtMsg', handleNotifyUserInMeeting);
|
||||
|
@ -1,14 +0,0 @@
|
||||
import { check } from 'meteor/check';
|
||||
import updateRandomViewer from '../modifiers/updateRandomViewer';
|
||||
|
||||
export default async function randomlySelectedUser({ header, body }) {
|
||||
const { userIds, choice, requestedBy } = body;
|
||||
const { meetingId } = header;
|
||||
|
||||
check(meetingId, String);
|
||||
check(requestedBy, String);
|
||||
check(userIds, Array);
|
||||
check(choice, String);
|
||||
|
||||
await updateRandomViewer(meetingId, userIds, choice, requestedBy);
|
||||
}
|
@ -1,3 +1,2 @@
|
||||
import './eventHandlers';
|
||||
import './methods';
|
||||
import './publishers';
|
||||
|
@ -1,6 +0,0 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import clearRandomlySelectedUser from './methods/clearRandomlySelectedUser';
|
||||
|
||||
Meteor.methods({
|
||||
clearRandomlySelectedUser,
|
||||
});
|
@ -1,30 +0,0 @@
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
import { check } from 'meteor/check';
|
||||
|
||||
export default async function clearRandomlySelectedUser() {
|
||||
try {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
};
|
||||
|
||||
const modifier = {
|
||||
$set: {
|
||||
randomlySelectedUser: [],
|
||||
},
|
||||
};
|
||||
|
||||
const numberAffected = await Meetings.updateAsync(selector, modifier);
|
||||
if (numberAffected) {
|
||||
Logger.info(`Cleared randomly selected user from meeting=${meetingId} by id=${requesterUserId}`);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Exception while invoking method clearRandomlySelectedUser ${err.stack}`);
|
||||
}
|
||||
}
|
@ -206,7 +206,6 @@ export default async function addMeeting(meeting) {
|
||||
layout: LAYOUT_TYPE[meetingLayout] || 'smart',
|
||||
publishedPoll: false,
|
||||
guestLobbyMessage: '',
|
||||
randomlySelectedUser: [],
|
||||
...flat(newMeeting, {
|
||||
safe: true,
|
||||
}),
|
||||
|
@ -1,143 +0,0 @@
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import { check } from 'meteor/check';
|
||||
|
||||
const SELECT_RANDOM_USER_COUNTDOWN = Meteor.settings.public.selectRandomUser.countdown;
|
||||
|
||||
// Time intervals in milliseconds
|
||||
// for iteration in animation.
|
||||
const intervals = [0, 200, 450, 750, 1100, 1500];
|
||||
|
||||
// Used to toggle to the first value of intervals to
|
||||
// differenciare whether this function has been called
|
||||
let updateIndicator = true;
|
||||
|
||||
// A finction that toggles
|
||||
// the first interval on each call
|
||||
function toggleIndicator() {
|
||||
if (updateIndicator) {
|
||||
intervals[0] = 1;
|
||||
} else {
|
||||
intervals[0] = 0;
|
||||
}
|
||||
updateIndicator = !updateIndicator;
|
||||
}
|
||||
|
||||
function getFiveRandom(userList, userIds) {
|
||||
let IDs = userIds.slice();
|
||||
for (let i = 0; i < intervals.length - 1; i += 1) {
|
||||
if (IDs.length === 0) { // we used up all the options
|
||||
IDs = userIds.slice(); // start over
|
||||
let userId = IDs.splice(0, 1);
|
||||
if (userList[userList.length] === [userId, intervals[i]]) {
|
||||
// If we start over with the one we finished, change it
|
||||
IDs.push(userId);
|
||||
userId = IDs.splice(0, 1);
|
||||
}
|
||||
userList.push([userId, intervals[i]]);
|
||||
} else {
|
||||
const userId = IDs.splice(Math.floor(Math.random() * IDs.length), 1);
|
||||
userList.push([userId, intervals[i]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All possible combinations of 3 elements
|
||||
// to speed up randomizing
|
||||
const optionsFor3 = [
|
||||
[0, 1, 2],
|
||||
[0, 2, 1],
|
||||
[1, 2, 0],
|
||||
[1, 0, 2],
|
||||
[2, 0, 1],
|
||||
[2, 1, 0],
|
||||
];
|
||||
|
||||
export default async function updateRandomUser(meetingId, userIds, choice, requesterId) {
|
||||
check(meetingId, String);
|
||||
check(userIds, Array);
|
||||
check(choice, String);
|
||||
check(requesterId, String);
|
||||
|
||||
let userList = [];
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
};
|
||||
|
||||
toggleIndicator();
|
||||
|
||||
const numberOfUsers = userIds.length;
|
||||
|
||||
if (choice === '') {
|
||||
// no viewer
|
||||
userList = [
|
||||
[requesterId, intervals[0]],
|
||||
[requesterId, 0],
|
||||
[requesterId, 0],
|
||||
[requesterId, 0],
|
||||
[requesterId, 0],
|
||||
[requesterId, 0],
|
||||
];
|
||||
} else if (numberOfUsers === 1) { // If user is only one, obviously it is the chosen one
|
||||
userList = [
|
||||
[userIds[0], intervals[0]],
|
||||
[userIds[0], 0],
|
||||
[userIds[0], 0],
|
||||
[userIds[0], 0],
|
||||
[userIds[0], 0],
|
||||
[userIds[0], 0],
|
||||
];
|
||||
} else if (!SELECT_RANDOM_USER_COUNTDOWN) {
|
||||
// If animation is disabled, we only care about the chosen one
|
||||
userList = [
|
||||
[choice, intervals[0]],
|
||||
[choice, 0],
|
||||
[choice, 0],
|
||||
[choice, 0],
|
||||
[choice, 0],
|
||||
[choice, 0],
|
||||
];
|
||||
} else if (numberOfUsers === 2) { // If there are only two users, we can just chow them in turns
|
||||
const IDs = userIds.slice();
|
||||
IDs.splice(choice, 1);
|
||||
userList = [
|
||||
[IDs[0], intervals[0]],
|
||||
[choice, intervals[1]],
|
||||
[IDs[0], intervals[2]],
|
||||
[choice, intervals[3]],
|
||||
[IDs[0], intervals[4]],
|
||||
[choice, intervals[5]],
|
||||
];
|
||||
} else if (numberOfUsers === 3) {
|
||||
// If there are 3 users, the number of combinations is small, so we'll use that
|
||||
const option = Math.floor(Math.random() * 6);
|
||||
const order = optionsFor3[option];
|
||||
userList = [
|
||||
[userIds[order[0]], intervals[0]],
|
||||
[userIds[order[1]], intervals[1]],
|
||||
[userIds[order[2]], intervals[2]],
|
||||
[userIds[order[0]], intervals[3]],
|
||||
[userIds[order[1]], intervals[4]],
|
||||
[choice, intervals[5]],
|
||||
];
|
||||
} else { // We generate 5 users randomly, just for animation, and last one is the chosen one
|
||||
getFiveRandom(userList, userIds);
|
||||
userList.push([choice, intervals[intervals.length]]);
|
||||
}
|
||||
|
||||
const modifier = {
|
||||
$set: {
|
||||
randomlySelectedUser: userList,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const { insertedId } = await Meetings.upsertAsync(selector, modifier);
|
||||
if (insertedId) {
|
||||
Logger.info(`Set randomly selected userId and interval = ${userList} by requesterId=${requesterId} in meeitingId=${meetingId}`);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Setting randomly selected userId and interval = ${userList} by requesterId=${requesterId} in meetingId=${meetingId}`);
|
||||
}
|
||||
}
|
@ -22,7 +22,7 @@ const {
|
||||
const HAS_DISPLAY_MEDIA = (typeof navigator.getDisplayMedia === 'function'
|
||||
|| (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function'));
|
||||
|
||||
const getConferenceBridge = () => Meetings.findOne().voiceProp.voiceConf;
|
||||
const getConferenceBridge = () => Meetings.findOne().voiceSettings.voiceConf;
|
||||
|
||||
const normalizeGetDisplayMediaError = (error) => {
|
||||
return SCREENSHARING_ERRORS[error.name] || SCREENSHARING_ERRORS.GetDisplayMediaGenericError;
|
||||
|
@ -66,6 +66,7 @@ const currentParameters = [
|
||||
'bbb_hide_nav_bar',
|
||||
'bbb_change_layout',
|
||||
'bbb_direct_leave_button',
|
||||
'bbb_default_layout',
|
||||
];
|
||||
|
||||
function valueParser(val) {
|
||||
|
@ -181,14 +181,14 @@ class Base extends Component {
|
||||
if (!checkedUserSettings) {
|
||||
const showAnimationsDefault = getFromUserSettings(
|
||||
'bbb_show_animations_default',
|
||||
Meteor.settings.public.app.defaultSettings.application.animations
|
||||
window.meetingClientSettings.public.app.defaultSettings.application.animations
|
||||
);
|
||||
|
||||
Settings.application.animations = showAnimationsDefault;
|
||||
Settings.save(setLocalSettings);
|
||||
|
||||
if (getFromUserSettings('bbb_show_participants_on_login', Meteor.settings.public.layout.showParticipantsOnLogin) && !deviceInfo.isPhone) {
|
||||
if (isChatEnabled() && getFromUserSettings('bbb_show_public_chat_on_login', !Meteor.settings.public.chat.startClosed)) {
|
||||
if (getFromUserSettings('bbb_show_participants_on_login', window.meetingClientSettings.public.layout.showParticipantsOnLogin) && !deviceInfo.isPhone) {
|
||||
if (isChatEnabled() && getFromUserSettings('bbb_show_public_chat_on_login', !window.meetingClientSettings.public.chat.startClosed)) {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_NAVIGATION_IS_OPEN,
|
||||
value: true,
|
||||
@ -274,7 +274,6 @@ class Base extends Component {
|
||||
subscriptionsReady,
|
||||
userWasEjected,
|
||||
} = this.props;
|
||||
|
||||
if ((loading || !subscriptionsReady) && !meetingHasEnded && meetingExist) {
|
||||
return (<LoadingScreen>{loading}</LoadingScreen>);
|
||||
}
|
||||
@ -314,7 +313,6 @@ class Base extends Component {
|
||||
}
|
||||
return (<MeetingEnded code={codeError} callback={this.setUserExitReason} endedReason="logout" />);
|
||||
}
|
||||
|
||||
return (<AppContainer {...this.props} />);
|
||||
}
|
||||
|
||||
@ -363,6 +361,7 @@ const BaseContainer = (props) => {
|
||||
};
|
||||
|
||||
export default withTracker(() => {
|
||||
const clientSettings = JSON.parse(sessionStorage.getItem('clientStartupSettings') || '{}')
|
||||
const {
|
||||
animations,
|
||||
} = Settings.application;
|
||||
@ -390,15 +389,15 @@ export default withTracker(() => {
|
||||
loggedOut: 1,
|
||||
meetingId: 1,
|
||||
userId: 1,
|
||||
inactivityCheck: 1,
|
||||
responseDelay: 1,
|
||||
currentConnectionId: 1,
|
||||
connectionIdUpdateTime: 1,
|
||||
inactivityWarningDisplay: 1,
|
||||
inactivityWarningTimeoutSecs: 1,
|
||||
};
|
||||
const User = Users.findOne({ userId: credentials.requesterUserId }, { fields });
|
||||
const meeting = Meetings.findOne({ meetingId }, {
|
||||
fields: {
|
||||
meetingEnded: 1,
|
||||
ended: 1,
|
||||
meetingEndedReason: 1,
|
||||
meetingProp: 1,
|
||||
},
|
||||
@ -437,10 +436,10 @@ export default withTracker(() => {
|
||||
User,
|
||||
isMeteorConnected: Meteor.status().connected,
|
||||
meetingExist: !!meeting,
|
||||
meetingHasEnded: !!meeting && meeting.meetingEnded,
|
||||
meetingHasEnded: !!meeting && meeting.ended,
|
||||
meetingEndedReason,
|
||||
meetingIsBreakout: AppService.meetingIsBreakout(),
|
||||
subscriptionsReady: Session.get('subscriptionsReady'),
|
||||
subscriptionsReady: Session.get('subscriptionsReady') || clientSettings.skipMeteorConnection,
|
||||
loggedIn,
|
||||
codeError,
|
||||
usersVideo,
|
||||
|
@ -4,13 +4,6 @@ import { LoadingContext } from '/imports/ui/components/common/loading-screen/loa
|
||||
import useCurrentLocale from '/imports/ui/core/local-states/useCurrentLocale';
|
||||
import logger from './logger';
|
||||
|
||||
interface LocaleEndpointResponse {
|
||||
defaultLocale: string;
|
||||
fallbackOnEmptyLocaleString: boolean;
|
||||
normalizedLocale: string;
|
||||
regionDefaultLocale: string;
|
||||
}
|
||||
|
||||
interface LocaleJson {
|
||||
[key: string]: string;
|
||||
}
|
||||
@ -42,6 +35,57 @@ const buildFetchLocale = (locale: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
const fetchLocaleOptions = (locale: string, init: boolean, localesList: string[] = []) => {
|
||||
const clientSettings = JSON.parse(sessionStorage.getItem('clientStartupSettings') || '{}');
|
||||
const fallback = clientSettings.fallbackLocale;
|
||||
const override = clientSettings.overrideLocale;
|
||||
const browserLocale = override && init ? override.split(/[-_]/g) : locale.split(/[-_]/g);
|
||||
const defaultLanguage = clientSettings.fallbackLocale;
|
||||
const fallbackOnEmptyString = clientSettings.fallbackOnEmptyLocaleString;
|
||||
|
||||
let localeFile = fallback;
|
||||
let normalizedLocale: string = '';
|
||||
|
||||
const usableLocales = localesList
|
||||
.map((file) => file.replace('.json', ''))
|
||||
.reduce((locales: string[], locale: string) => (locale.match(browserLocale[0])
|
||||
? [...locales, locale]
|
||||
: locales), []);
|
||||
|
||||
const regionDefault = usableLocales.find((locale: string) => browserLocale[0] === locale);
|
||||
|
||||
if (browserLocale.length > 1) {
|
||||
// browser asks for specific locale
|
||||
normalizedLocale = `${browserLocale[0]}_${browserLocale[1]?.toUpperCase()}`;
|
||||
|
||||
const normDefault = usableLocales.find((locale) => normalizedLocale === locale);
|
||||
if (normDefault) {
|
||||
localeFile = normDefault;
|
||||
} else if (regionDefault) {
|
||||
localeFile = regionDefault;
|
||||
} else {
|
||||
const specFallback = usableLocales.find((locale) => browserLocale[0] === locale.split('_')[0]);
|
||||
if (specFallback) localeFile = specFallback;
|
||||
}
|
||||
} else {
|
||||
// browser asks for region default locale
|
||||
// eslint-disable-next-line no-lonely-if
|
||||
if (regionDefault && localeFile === fallback && regionDefault !== localeFile) {
|
||||
localeFile = regionDefault;
|
||||
} else {
|
||||
const normFallback = usableLocales.find((locale) => browserLocale[0] === locale.split('_')[0]);
|
||||
if (normFallback) localeFile = normFallback;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
normalizedLocale: localeFile,
|
||||
regionDefaultLocale: (regionDefault && regionDefault !== localeFile) ? regionDefault : '',
|
||||
defaultLocale: defaultLanguage,
|
||||
fallbackOnEmptyLocaleString: fallbackOnEmptyString,
|
||||
};
|
||||
};
|
||||
|
||||
const IntlLoader: React.FC<IntlLoaderProps> = ({
|
||||
children,
|
||||
currentLocale,
|
||||
@ -55,17 +99,11 @@ const IntlLoader: React.FC<IntlLoaderProps> = ({
|
||||
const [fallbackOnEmptyLocaleString, setFallbackOnEmptyLocaleString] = React.useState(false);
|
||||
|
||||
const fetchLocalizedMessages = useCallback((locale: string, init: boolean) => {
|
||||
const url = `./locale?locale=${locale}&init=${init}`;
|
||||
setFetching(true);
|
||||
// fetch localized messages
|
||||
fetch(url)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
loadingContextInfo.setLoading(false, '');
|
||||
throw new Error('unable to fetch localized messages');
|
||||
}
|
||||
return response.json();
|
||||
}).then((data: LocaleEndpointResponse) => {
|
||||
buildFetchLocale('index')
|
||||
.then((resp) => {
|
||||
const data = fetchLocaleOptions(locale, init, resp as string[]);
|
||||
|
||||
const {
|
||||
defaultLocale,
|
||||
regionDefaultLocale,
|
||||
@ -92,7 +130,6 @@ const IntlLoader: React.FC<IntlLoaderProps> = ({
|
||||
const mergedLocale = foundLocales
|
||||
.reduce((acc, locale: LocaleJson) => Object.assign(acc, locale), {});
|
||||
const replacedLocale = normalizedLocale.replace('_', '-');
|
||||
setFetching(false);
|
||||
setNormalizedLocale(replacedLocale);
|
||||
setCurrentLocale(replacedLocale);
|
||||
setMessages(mergedLocale);
|
||||
@ -103,6 +140,10 @@ const IntlLoader: React.FC<IntlLoaderProps> = ({
|
||||
loadingContextInfo.setLoading(false, '');
|
||||
throw new Error(error);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
loadingContextInfo.setLoading(false, '');
|
||||
throw new Error('unable to fetch localized messages');
|
||||
});
|
||||
}, []);
|
||||
|
||||
@ -119,7 +160,7 @@ const IntlLoader: React.FC<IntlLoaderProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (fetching) {
|
||||
loadingContextInfo.setLoading(true, 'Fetching locale');
|
||||
logger.info('Fetching locale');
|
||||
}
|
||||
}, [fetching]);
|
||||
|
||||
|
@ -186,12 +186,10 @@ Meteor.startup(() => {
|
||||
CDN=${CDN_URL}\n`, APP_CONFIG);
|
||||
});
|
||||
|
||||
|
||||
const generateLocaleOptions = () => {
|
||||
try {
|
||||
Logger.warn('Calculating aggregateLocales (heavy)');
|
||||
|
||||
|
||||
// remove duplicated locales (always remove more generic if same name)
|
||||
const tempAggregateLocales = AVAILABLE_LOCALES
|
||||
.map(file => file.replace('.json', ''))
|
||||
@ -212,6 +210,18 @@ const generateLocaleOptions = () => {
|
||||
|
||||
Logger.warn(`Total locales: ${tempAggregateLocales.length}`, tempAggregateLocales);
|
||||
|
||||
const filePath = `${applicationRoot}/index.json`;
|
||||
const jsContent = JSON.stringify(AVAILABLE_LOCALES, null, 2);
|
||||
|
||||
// Write JSON data to a file
|
||||
fs.writeFile(filePath, jsContent, (err) => {
|
||||
if (err) {
|
||||
Logger.error('Error writing file:', err);
|
||||
} else {
|
||||
Logger.info(`JSON data has been written to ${filePath}`);
|
||||
}
|
||||
});
|
||||
|
||||
return tempAggregateLocales;
|
||||
} catch (e) {
|
||||
Logger.error(`'Could not process locales error: ${e}`);
|
||||
|
@ -9,6 +9,13 @@ export interface LockSettings {
|
||||
hideViewersCursor: boolean;
|
||||
meetingId: boolean;
|
||||
webcamsOnlyForModerator: boolean;
|
||||
lockOnJoin: boolean;
|
||||
lockOnJoinConfigurable: boolean;
|
||||
}
|
||||
|
||||
export interface groups {
|
||||
groupId: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface WelcomeSettings {
|
||||
@ -93,6 +100,11 @@ export interface ComponentsFlags {
|
||||
hasTimer: boolean;
|
||||
}
|
||||
|
||||
export interface Metadata {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface Meeting {
|
||||
createdTime: number;
|
||||
disabledFeatures: Array<string>;
|
||||
@ -114,4 +126,8 @@ export interface Meeting {
|
||||
breakoutPolicies: BreakoutPolicies;
|
||||
externalVideo: ExternalVideo;
|
||||
componentsFlags: ComponentsFlags;
|
||||
endWhenNoModerator: boolean;
|
||||
endWhenNoModeratorDelayInMinutes: number;
|
||||
metadata: Array<Metadata>;
|
||||
groups: Array<groups>;
|
||||
}
|
||||
|
@ -20,7 +20,6 @@ export interface Public {
|
||||
media: Media
|
||||
stats: Stats
|
||||
presentation: Presentation
|
||||
selectRandomUser: SelectRandomUser
|
||||
user: User
|
||||
whiteboard: Whiteboard
|
||||
clientLog: ClientLog
|
||||
@ -664,11 +663,6 @@ export interface UploadValidMimeType {
|
||||
mime: string
|
||||
}
|
||||
|
||||
export interface SelectRandomUser {
|
||||
enabled: boolean
|
||||
countdown: boolean
|
||||
}
|
||||
|
||||
export interface User {
|
||||
role_moderator: string
|
||||
role_viewer: string
|
||||
|
@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
|
||||
import { defineMessages } from 'react-intl';
|
||||
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
|
||||
import ExternalVideoModal from '/imports/ui/components/external-video-player/external-video-player-graphql/modal/component';
|
||||
import RandomUserSelectContainer from '/imports/ui/components/common/modal/random-user/container';
|
||||
import LayoutModalContainer from '/imports/ui/components/layout/modal/container';
|
||||
import BBBMenu from '/imports/ui/components/common/menu/component';
|
||||
import { ActionButtonDropdownItemType } from 'bigbluebutton-html-plugin-sdk/dist/cjs/extensible-areas/action-button-dropdown-item/enums';
|
||||
@ -102,14 +101,6 @@ const intlMessages = defineMessages({
|
||||
id: 'app.actionsBar.actionsDropdown.stopShareExternalVideo',
|
||||
description: 'Stop sharing external video button',
|
||||
},
|
||||
selectRandUserLabel: {
|
||||
id: 'app.actionsBar.actionsDropdown.selectRandUserLabel',
|
||||
description: 'Label for selecting a random user',
|
||||
},
|
||||
selectRandUserDesc: {
|
||||
id: 'app.actionsBar.actionsDropdown.selectRandUserDesc',
|
||||
description: 'Description for select random user option',
|
||||
},
|
||||
layoutModal: {
|
||||
id: 'app.actionsBar.actionsDropdown.layoutModal',
|
||||
description: 'Label for layouts selection button',
|
||||
@ -137,7 +128,6 @@ class ActionsDropdown extends PureComponent {
|
||||
this.selectUserRandId = uniqueId('action-item-');
|
||||
this.state = {
|
||||
isExternalVideoModalOpen: false,
|
||||
isRandomUserSelectModalOpen: false,
|
||||
isLayoutModalOpen: false,
|
||||
isCameraAsContentModalOpen: false,
|
||||
};
|
||||
@ -145,7 +135,6 @@ class ActionsDropdown extends PureComponent {
|
||||
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);
|
||||
this.setCameraAsContentModalIsOpen = this.setCameraAsContentModalIsOpen.bind(this);
|
||||
this.setPropsToPassModal = this.setPropsToPassModal.bind(this);
|
||||
@ -182,7 +171,6 @@ class ActionsDropdown extends PureComponent {
|
||||
handleTakePresenter,
|
||||
isSharingVideo,
|
||||
isPollingEnabled,
|
||||
isSelectRandomUserEnabled,
|
||||
stopExternalVideoShare,
|
||||
isTimerActive,
|
||||
isTimerEnabled,
|
||||
@ -262,16 +250,6 @@ class ActionsDropdown extends PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
if (amIPresenter && isSelectRandomUserEnabled) {
|
||||
actions.push({
|
||||
icon: 'user',
|
||||
label: intl.formatMessage(intlMessages.selectRandUserLabel),
|
||||
key: this.selectUserRandId,
|
||||
onClick: () => this.setRandomUserSelectModalIsOpen(true),
|
||||
dataTest: 'selectRandomUser',
|
||||
});
|
||||
}
|
||||
|
||||
if (amIModerator && isTimerEnabled && isTimerFeatureEnabled) {
|
||||
actions.push({
|
||||
icon: 'time',
|
||||
@ -378,10 +356,6 @@ class ActionsDropdown extends PureComponent {
|
||||
this.setState({ isExternalVideoModalOpen: value });
|
||||
}
|
||||
|
||||
setRandomUserSelectModalIsOpen(value) {
|
||||
this.setState({ isRandomUserSelectModalOpen: value });
|
||||
}
|
||||
|
||||
setLayoutModalIsOpen(value) {
|
||||
this.setState({ isLayoutModalOpen: value });
|
||||
}
|
||||
@ -420,13 +394,11 @@ class ActionsDropdown extends PureComponent {
|
||||
isDropdownOpen,
|
||||
isMobile,
|
||||
isRTL,
|
||||
isSelectRandomUserEnabled,
|
||||
propsToPassModal,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isExternalVideoModalOpen,
|
||||
isRandomUserSelectModalOpen,
|
||||
isLayoutModalOpen,
|
||||
isCameraAsContentModalOpen,
|
||||
} = this.state;
|
||||
@ -480,14 +452,6 @@ class ActionsDropdown extends PureComponent {
|
||||
'low',
|
||||
ExternalVideoModal,
|
||||
)}
|
||||
{amIPresenter && isSelectRandomUserEnabled
|
||||
? this.renderModal(
|
||||
isRandomUserSelectModalOpen,
|
||||
this.setRandomUserSelectModalIsOpen,
|
||||
'low',
|
||||
RandomUserSelectContainer,
|
||||
)
|
||||
: null}
|
||||
{this.renderModal(
|
||||
isLayoutModalOpen,
|
||||
this.setLayoutModalIsOpen,
|
||||
|
@ -13,7 +13,7 @@ import { TIMER_ACTIVATE, TIMER_DEACTIVATE } from '../../timer/mutations';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import { PRESENTATION_SET_CURRENT } from '../../presentation/mutations';
|
||||
|
||||
const TIMER_CONFIG = Meteor.settings.public.timer;
|
||||
const TIMER_CONFIG = window.meetingClientSettings.public.timer;
|
||||
const MILLI_IN_MINUTE = 60000;
|
||||
|
||||
const ActionsDropdownContainer = (props) => {
|
||||
|
@ -114,7 +114,6 @@ class ActionsBar extends PureComponent {
|
||||
isCaptionsAvailable,
|
||||
isMeteorConnected,
|
||||
isPollingEnabled,
|
||||
isSelectRandomUserEnabled,
|
||||
isRaiseHandButtonCentered,
|
||||
isThereCurrentPresentation,
|
||||
allowExternalVideo,
|
||||
@ -151,7 +150,6 @@ class ActionsBar extends PureComponent {
|
||||
amIPresenter,
|
||||
amIModerator,
|
||||
isPollingEnabled,
|
||||
isSelectRandomUserEnabled,
|
||||
allowExternalVideo,
|
||||
intl,
|
||||
isSharingVideo,
|
||||
|
@ -77,7 +77,6 @@ const ActionsBarContainer = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const SELECT_RANDOM_USER_ENABLED = window.meetingClientSettings.public.selectRandomUser.enabled;
|
||||
const RAISE_HAND_BUTTON_ENABLED = window.meetingClientSettings.public.app.raiseHandActionButton.enabled;
|
||||
const RAISE_HAND_BUTTON_CENTERED = window.meetingClientSettings.public.app.raiseHandActionButton.centered;
|
||||
|
||||
@ -89,7 +88,7 @@ const isReactionsButtonEnabled = () => {
|
||||
};
|
||||
|
||||
export default withTracker(() => ({
|
||||
enableVideo: getFromUserSettings('bbb_enable_video', Meteor.settings.public.kurento.enableVideo),
|
||||
enableVideo: getFromUserSettings('bbb_enable_video', window.meetingClientSettings.public.kurento.enableVideo),
|
||||
setPresentationIsOpen: MediaService.setPresentationIsOpen,
|
||||
isSharedNotesPinned: Service.isSharedNotesPinned(),
|
||||
hasScreenshare: isScreenBroadcasting(),
|
||||
@ -99,7 +98,6 @@ export default withTracker(() => ({
|
||||
isTimerEnabled: TimerService.isEnabled(),
|
||||
isMeteorConnected: Meteor.status().connected,
|
||||
isPollingEnabled: isPollingEnabled() && isPresentationEnabled(),
|
||||
isSelectRandomUserEnabled: SELECT_RANDOM_USER_ENABLED,
|
||||
isRaiseHandButtonEnabled: RAISE_HAND_BUTTON_ENABLED,
|
||||
isRaiseHandButtonCentered: RAISE_HAND_BUTTON_CENTERED,
|
||||
isReactionsButtonEnabled: isReactionsButtonEnabled(),
|
||||
|
@ -34,8 +34,8 @@ const isMe = (intId) => intId === Auth.userID;
|
||||
|
||||
export default {
|
||||
isMe,
|
||||
meetingName: () => Meetings.findOne({ meetingId: Auth.meetingID },
|
||||
{ fields: { 'meetingProp.name': 1 } }).meetingProp.name,
|
||||
meetingName: () => Meetings.findOne({ meetingId: Auth.meetingID},
|
||||
{ fields: { name: 1 } }).name,
|
||||
users: () => Users.find({
|
||||
meetingId: Auth.meetingID,
|
||||
clientType: { $ne: DIAL_IN_USER },
|
||||
|
@ -22,7 +22,6 @@ import ManyWebcamsNotifier from '/imports/ui/components/video-provider/many-user
|
||||
import AudioCaptionsSpeechContainer from '/imports/ui/components/audio/captions/speech/container';
|
||||
import UploaderContainer from '/imports/ui/components/presentation/presentation-uploader/container';
|
||||
import CaptionsSpeechContainer from '/imports/ui/components/captions/speech/container';
|
||||
import RandomUserSelectContainer from '/imports/ui/components/common/modal/random-user/container';
|
||||
import ScreenReaderAlertContainer from '../screenreader-alert/container';
|
||||
import WebcamContainer from '../webcam/container';
|
||||
import PresentationContainer from '../presentation/container';
|
||||
@ -149,7 +148,6 @@ class App extends Component {
|
||||
this.state = {
|
||||
enableResize: !window.matchMedia(MOBILE_MEDIA).matches,
|
||||
isAudioModalOpen: false,
|
||||
isRandomUserSelectModalOpen: false,
|
||||
isVideoPreviewModalOpen: false,
|
||||
presentationFitToWidth: false,
|
||||
};
|
||||
@ -161,7 +159,7 @@ class App extends Component {
|
||||
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(),
|
||||
@ -246,7 +244,6 @@ class App extends Component {
|
||||
currentUserRaiseHand,
|
||||
intl,
|
||||
deviceType,
|
||||
mountRandomUserModal,
|
||||
selectedLayout,
|
||||
sidebarContentIsOpen,
|
||||
layoutContextDispatch,
|
||||
@ -258,8 +255,6 @@ class App extends Component {
|
||||
|
||||
this.renderDarkMode();
|
||||
|
||||
if (mountRandomUserModal) this.setRandomUserSelectModalIsOpen(true);
|
||||
|
||||
if (prevProps.currentUserEmoji.status !== currentUserEmoji.status
|
||||
&& currentUserEmoji.status !== 'raiseHand'
|
||||
&& currentUserEmoji.status !== 'away'
|
||||
@ -476,12 +471,12 @@ class App extends Component {
|
||||
renderActivityCheck() {
|
||||
const { User } = this.props;
|
||||
|
||||
const { inactivityCheck, responseDelay } = User;
|
||||
const { inactivityWarningDisplay, inactivityWarningTimeoutSecs } = User;
|
||||
|
||||
return (inactivityCheck ? (
|
||||
return (inactivityWarningDisplay ? (
|
||||
<ActivityCheckContainer
|
||||
inactivityCheck={inactivityCheck}
|
||||
responseDelay={responseDelay}
|
||||
inactivityCheck={inactivityWarningDisplay}
|
||||
responseDelay={inactivityWarningTimeoutSecs}
|
||||
/>
|
||||
) : null);
|
||||
}
|
||||
@ -578,7 +573,7 @@ class App extends Component {
|
||||
this.setState({isVideoPreviewModalOpen: value});
|
||||
}
|
||||
|
||||
setRandomUserSelectModalIsOpen(value) {
|
||||
setRandomUserSelectModalIsOpen(value) {
|
||||
const {setMountRandomUserModal} = this.props;
|
||||
this.setState({isRandomUserSelectModalOpen: value});
|
||||
setMountRandomUserModal(false);
|
||||
@ -592,7 +587,6 @@ class App extends Component {
|
||||
pushAlertEnabled,
|
||||
shouldShowPresentation,
|
||||
shouldShowScreenshare,
|
||||
shouldShowExternalVideo,
|
||||
shouldShowSharedNotes,
|
||||
isPresenter,
|
||||
selectedLayout,
|
||||
@ -600,11 +594,11 @@ class App extends Component {
|
||||
darkTheme,
|
||||
intl,
|
||||
isModerator,
|
||||
genericComponentId,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isAudioModalOpen,
|
||||
isRandomUserSelectModalOpen,
|
||||
isVideoPreviewModalOpen,
|
||||
presentationFitToWidth,
|
||||
} = this.state;
|
||||
@ -635,11 +629,7 @@ class App extends Component {
|
||||
<WebcamContainer isLayoutSwapped={!presentationIsOpen} layoutType={selectedLayout} />
|
||||
<ExternalVideoPlayerContainer />
|
||||
<GenericComponentContainer
|
||||
{...{
|
||||
shouldShowScreenshare,
|
||||
shouldShowSharedNotes,
|
||||
shouldShowExternalVideo,
|
||||
}}
|
||||
genericComponentId={genericComponentId}
|
||||
/>
|
||||
{shouldShowPresentation ? <PresentationContainer setPresentationFitToWidth={this.setPresentationFitToWidth} fitToWidth={presentationFitToWidth} darkTheme={darkTheme} presentationIsOpen={presentationIsOpen} layoutType={selectedLayout} /> : null}
|
||||
{shouldShowScreenshare ? <ScreenshareContainer isLayoutSwapped={!presentationIsOpen} isPresenter={isPresenter} /> : null}
|
||||
@ -680,14 +670,6 @@ class App extends Component {
|
||||
<EmojiRainContainer />
|
||||
{customStyleUrl ? <link rel="stylesheet" type="text/css" href={customStyleUrl} /> : null}
|
||||
{customStyle ? <link rel="stylesheet" type="text/css" href={`data:text/css;charset=UTF-8,${encodeURIComponent(customStyle)}`} /> : null}
|
||||
{isRandomUserSelectModalOpen ? <RandomUserSelectContainer
|
||||
{...{
|
||||
onRequestClose: () => this.setRandomUserSelectModalIsOpen(false),
|
||||
priority: "low",
|
||||
setIsOpen: this.setRandomUserSelectModalIsOpen,
|
||||
isOpen: isRandomUserSelectModalOpen,
|
||||
}}
|
||||
/> : null}
|
||||
</Styled.Layout>
|
||||
</>
|
||||
);
|
||||
|
@ -65,7 +65,6 @@ const AppContainer = (props) => {
|
||||
shouldShowScreenshare: propsShouldShowScreenshare,
|
||||
shouldShowSharedNotes,
|
||||
presentationRestoreOnUpdate,
|
||||
randomlySelectedUser,
|
||||
isModalOpen,
|
||||
meetingLayout,
|
||||
meetingLayoutUpdatedAt,
|
||||
@ -142,17 +141,6 @@ const AppContainer = (props) => {
|
||||
}
|
||||
presentationVideoRate = parseFloat(presentationVideoRate.toFixed(2));
|
||||
|
||||
const prevRandomUser = usePrevious(randomlySelectedUser);
|
||||
|
||||
const [mountRandomUserModal, setMountRandomUserModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMountRandomUserModal(!isPresenter
|
||||
&& !isEqual(prevRandomUser, randomlySelectedUser)
|
||||
&& randomlySelectedUser.length > 0
|
||||
&& !isModalOpen);
|
||||
}, [isPresenter, prevRandomUser, randomlySelectedUser, isModalOpen]);
|
||||
|
||||
const setPushLayout = () => {
|
||||
setSyncWithPresenterLayout({
|
||||
variables: {
|
||||
@ -189,7 +177,7 @@ const AppContainer = (props) => {
|
||||
|
||||
const shouldShowExternalVideo = isExternalVideoEnabled() && isSharingVideo;
|
||||
|
||||
const shouldShowGenericComponent = genericComponent.hasGenericComponent;
|
||||
const shouldShowGenericComponent = !!genericComponent.genericComponentId;
|
||||
|
||||
const validateEnforceLayout = (currentUser) => {
|
||||
const layoutTypes = Object.values(LAYOUT_TYPE);
|
||||
@ -197,12 +185,11 @@ const AppContainer = (props) => {
|
||||
return enforceLayout && layoutTypes.includes(enforceLayout) ? enforceLayout : null;
|
||||
};
|
||||
|
||||
const shouldShowScreenshare = propsShouldShowScreenshare
|
||||
const shouldShowScreenshare = propsShouldShowScreenshare
|
||||
&& (viewScreenshare || isPresenter);
|
||||
const shouldShowPresentation = (!shouldShowScreenshare && !shouldShowSharedNotes
|
||||
const shouldShowPresentation = (!shouldShowScreenshare && !shouldShowSharedNotes
|
||||
&& !shouldShowExternalVideo && !shouldShowGenericComponent
|
||||
&& (presentationIsOpen || presentationRestoreOnUpdate)) && isPresentationEnabled();
|
||||
|
||||
return currentUserId
|
||||
? (
|
||||
<App
|
||||
@ -237,8 +224,6 @@ const AppContainer = (props) => {
|
||||
sidebarContentPanel,
|
||||
sidebarContentIsOpen,
|
||||
shouldShowExternalVideo,
|
||||
mountRandomUserModal,
|
||||
setMountRandomUserModal,
|
||||
isPresenter,
|
||||
numCameras: cameraDockInput.numCameras,
|
||||
enforceLayout: validateEnforceLayout(currentUserData),
|
||||
@ -249,6 +234,7 @@ const AppContainer = (props) => {
|
||||
setMobileUser,
|
||||
toggleVoice,
|
||||
setLocalSettings,
|
||||
genericComponentId: genericComponent.genericComponentId,
|
||||
}}
|
||||
{...otherProps}
|
||||
/>
|
||||
@ -268,7 +254,7 @@ const currentUserEmoji = (currentUser) => (currentUser
|
||||
);
|
||||
|
||||
export default withTracker(() => {
|
||||
Users.find({ userId: Auth.userID, meetingId: Auth.meetingID }).observe({
|
||||
Users.find({ userId: Auth.userID, }).observe({
|
||||
removed(userData) {
|
||||
// wait 3secs (before endMeeting), client will try to authenticate again
|
||||
const delayForReconnection = userData.ejected ? 0 : 3000;
|
||||
@ -300,6 +286,7 @@ export default withTracker(() => {
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
const currentMeeting = Meetings.findOne({ meetingId: Auth.meetingID },
|
||||
{
|
||||
fields: {
|
||||
@ -311,17 +298,23 @@ export default withTracker(() => {
|
||||
randomlySelectedUser,
|
||||
} = currentMeeting;
|
||||
|
||||
const meetingLayoutObj = LayoutMeetings.findOne({ meetingId: Auth.meetingID }) || {};
|
||||
const meetingLayoutObj = Meetings
|
||||
.findOne({ meetingId: Auth.meetingID }) || {};
|
||||
|
||||
const { layout } = meetingLayoutObj;
|
||||
|
||||
const {
|
||||
layout: meetingLayout,
|
||||
pushLayout: pushLayoutMeeting,
|
||||
layoutUpdatedAt: meetingLayoutUpdatedAt,
|
||||
presentationIsOpen: meetingPresentationIsOpen,
|
||||
isResizing: isMeetingLayoutResizing,
|
||||
cameraPosition: meetingLayoutCameraPosition,
|
||||
focusedCamera: meetingLayoutFocusedCamera,
|
||||
currentLayoutType: meetingLayout,
|
||||
propagateLayout: pushLayoutMeeting,
|
||||
cameraDockIsResizing: isMeetingLayoutResizing,
|
||||
cameraDockPlacement: meetingLayoutCameraPosition,
|
||||
presentationVideoRate: meetingLayoutVideoRate,
|
||||
} = meetingLayoutObj;
|
||||
cameraWithFocus: meetingLayoutFocusedCamera,
|
||||
} = layout;
|
||||
|
||||
const meetingLayoutUpdatedAt = new Date(layout.updatedAt).getTime();
|
||||
|
||||
const meetingPresentationIsOpen = !layout.presentationMinimized;
|
||||
|
||||
const UserInfo = UserInfos.find({
|
||||
meetingId: Auth.meetingID,
|
||||
@ -355,7 +348,6 @@ export default withTracker(() => {
|
||||
currentUserEmoji: currentUserEmoji(currentUser),
|
||||
currentUserAway: currentUser.away,
|
||||
currentUserRaiseHand: currentUser.raiseHand,
|
||||
randomlySelectedUser,
|
||||
currentUserId: currentUser?.userId,
|
||||
meetingLayout,
|
||||
meetingLayoutUpdatedAt,
|
||||
|
@ -17,9 +17,9 @@ export const getBreakoutRooms = () => Breakouts.find().fetch();
|
||||
export function meetingIsBreakout() {
|
||||
const meeting = Meetings.findOne(
|
||||
{ meetingId: Auth.meetingID },
|
||||
{ fields: { 'meetingProp.isBreakout': 1 } }
|
||||
{ fields: { isBreakout: 1 } },
|
||||
);
|
||||
return meeting && meeting.meetingProp.isBreakout;
|
||||
return meeting && meeting.isBreakout;
|
||||
}
|
||||
|
||||
export const setDarkTheme = (value) => {
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { unique } from 'radash';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import { setAudioCaptionEnable } from '/imports/ui/core/local-states/useAudioCaptionEnable';
|
||||
import { isLiveTranscriptionEnabled } from '/imports/ui/services/features';
|
||||
|
||||
const CONFIG = Meteor.settings.public.app.audioCaptions;
|
||||
const CONFIG = window.meetingClientSettings.public.app.audioCaptions;
|
||||
const PROVIDER = CONFIG.provider;
|
||||
const LANGUAGES = CONFIG.language.available;
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { isAudioTranscriptionEnabled } from '../service';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import deviceInfo from '/imports/utils/deviceInfo';
|
||||
@ -9,7 +8,7 @@ import { Session } from 'meteor/session';
|
||||
import { throttle } from '/imports/utils/throttle';
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
|
||||
const CONFIG = Meteor.settings.public.app.audioCaptions;
|
||||
const CONFIG = window.meetingClientSettings.public.app.audioCaptions;
|
||||
const LANGUAGES = CONFIG.language.available;
|
||||
const VALID_ENVIRONMENT = !deviceInfo.isMobile || CONFIG.mobile;
|
||||
const THROTTLE_TIMEOUT = 2000;
|
||||
|
@ -30,14 +30,14 @@ export default lockContextContainer(withTracker(({ userLocks, setIsOpen }) => {
|
||||
const skipCheck = getFromUserSettings('bbb_skip_check_audio', APP_CONFIG.skipCheck);
|
||||
const skipCheckOnJoin = getFromUserSettings('bbb_skip_check_audio_on_first_join', APP_CONFIG.skipCheckOnJoin);
|
||||
const autoJoin = getFromUserSettings('bbb_auto_join_audio', APP_CONFIG.autoJoin);
|
||||
const meeting = Meetings.findOne({ meetingId: Auth.meetingID }, { fields: { voiceProp: 1 } });
|
||||
const meeting = Meetings.findOne({ meetingId: Auth.meetingID }, { fields: { voiceSettings: 1 } });
|
||||
const getEchoTest = Storage.getItem('getEchoTest');
|
||||
|
||||
let formattedDialNum = '';
|
||||
let formattedTelVoice = '';
|
||||
let combinedDialInNum = '';
|
||||
if (meeting && meeting.voiceProp) {
|
||||
const { dialNumber, telVoice } = meeting.voiceProp;
|
||||
if (meeting && meeting.voiceSettings) {
|
||||
const { dialNumber, telVoice } = meeting.voiceSettings;
|
||||
if (invalidDialNumbers.indexOf(dialNumber) < 0) {
|
||||
formattedDialNum = dialNumber;
|
||||
formattedTelVoice = telVoice;
|
||||
|
@ -53,8 +53,8 @@ const init = (messages, intl, toggleVoice) => {
|
||||
const { sessionToken } = Auth;
|
||||
const User = Users.findOne({ userId }, { fields: { name: 1 } });
|
||||
const username = User.name;
|
||||
const Meeting = Meetings.findOne({ meetingId: Auth.meetingID }, { fields: { 'voiceProp.voiceConf': 1 } });
|
||||
const voiceBridge = Meeting.voiceProp.voiceConf;
|
||||
const Meeting = Meetings.findOne({ meetingId: Auth.meetingID }, { fields: { 'voiceSettings.voiceConf': 1 } });
|
||||
const voiceBridge = Meeting.voiceSettings.voiceConf;
|
||||
|
||||
// FIX ME
|
||||
const microphoneLockEnforced = false;
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { useSubscription } from '@apollo/client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { isEqual } from 'radash';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { layoutSelect, layoutSelectInput, layoutDispatch } from '/imports/ui/components/layout/context';
|
||||
@ -55,7 +54,7 @@ const intlMessages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
|
||||
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
|
||||
const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id;
|
||||
|
||||
|
@ -1,10 +1,9 @@
|
||||
import AudioService from '/imports/ui/components/audio/service';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
const playAlertSound = () => {
|
||||
AudioService.playAlertSound(`${Meteor.settings.public.app.cdn
|
||||
+ Meteor.settings.public.app.basename
|
||||
+ Meteor.settings.public.app.instanceId}`
|
||||
AudioService.playAlertSound(`${window.meetingClientSettings.public.app.cdn
|
||||
+ window.meetingClientSettings.public.app.basename
|
||||
+ window.meetingClientSettings.public.app.instanceId}`
|
||||
+ '/resources/sounds/notify.mp3');
|
||||
};
|
||||
|
||||
|
@ -7,6 +7,16 @@ import React, {
|
||||
} from 'react';
|
||||
import { makeVar, useMutation } from '@apollo/client';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import ChatPopupContainer from '/imports/ui/components/chat/chat-graphql/chat-popup/component';
|
||||
import useChat from '/imports/ui/core/hooks/useChat';
|
||||
import useIntersectionObserver from '/imports/ui/hooks/useIntersectionObserver';
|
||||
import { Chat } from '/imports/ui/Types/chat';
|
||||
import { ChatEvents } from '/imports/ui/core/enums/chat';
|
||||
import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
|
||||
import { layoutSelect } from '/imports/ui/components/layout/context';
|
||||
import { Layout } from '/imports/ui/components/layout/layoutTypes';
|
||||
import { Message } from '/imports/ui/Types/message';
|
||||
import ChatListPage from './page/component';
|
||||
import LAST_SEEN_MUTATION from './queries';
|
||||
import {
|
||||
ButtonLoadMore,
|
||||
@ -15,17 +25,9 @@ import {
|
||||
UnreadButton,
|
||||
ChatMessages,
|
||||
} from './styles';
|
||||
import { layoutSelect } from '../../../layout/context';
|
||||
import ChatListPage from './page/component';
|
||||
import useChat from '/imports/ui/core/hooks/useChat';
|
||||
import { Chat } from '/imports/ui/Types/chat';
|
||||
import { Message } from '/imports/ui/Types/message';
|
||||
import ChatPopupContainer from '../chat-popup/component';
|
||||
import { ChatEvents } from '/imports/ui/core/enums/chat';
|
||||
import { Layout } from '../../../layout/layoutTypes';
|
||||
import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
|
||||
import useReactiveRef from '/imports/ui/hooks/useReactiveRef';
|
||||
import useStickyScroll from '/imports/ui/hooks/useStickyScroll';
|
||||
|
||||
// @ts-ignore - temporary, while meteor exists in the project
|
||||
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
|
||||
const PUBLIC_CHAT_KEY = CHAT_CONFIG.public_id;
|
||||
const PUBLIC_GROUP_CHAT_KEY = CHAT_CONFIG.public_group_id;
|
||||
@ -44,7 +46,6 @@ const intlMessages = defineMessages({
|
||||
});
|
||||
|
||||
interface ChatListProps {
|
||||
|
||||
totalUnread: number;
|
||||
totalPages: number;
|
||||
chatId: string;
|
||||
@ -59,6 +60,7 @@ interface ChatListProps {
|
||||
) => void;
|
||||
lastSeenAt: string;
|
||||
}
|
||||
|
||||
const isElement = (el: unknown): el is HTMLElement => {
|
||||
return el instanceof HTMLElement;
|
||||
};
|
||||
@ -117,15 +119,31 @@ const ChatMessageList: React.FC<ChatListProps> = ({
|
||||
isRTL,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const messageListRef = React.useRef<HTMLDivElement>(null);
|
||||
const contentRef = React.useRef<HTMLDivElement>(null);
|
||||
// I used a ref here because I don't want to re-render the component when the last sender changes
|
||||
const lastSenderPerPage = React.useRef<Map<number, string>>(new Map());
|
||||
const messagesEndRef = React.useRef<HTMLDivElement>(null);
|
||||
const messagesEndObserverRef = React.useRef<IntersectionObserver | null>(null);
|
||||
const sentinelRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const {
|
||||
ref: messageListRef,
|
||||
current: currentMessageList,
|
||||
} = useReactiveRef<HTMLDivElement>(null);
|
||||
const [userLoadedBackUntilPage, setUserLoadedBackUntilPage] = useState<number | null>(null);
|
||||
const [lastMessageCreatedAt, setLastMessageCreatedAt] = useState<string>('');
|
||||
const [followingTail, setFollowingTail] = React.useState(true);
|
||||
const {
|
||||
childRefProxy: sentinelRefProxy,
|
||||
intersecting: isSentinelVisible,
|
||||
parentRefProxy: messageListRefProxy,
|
||||
} = useIntersectionObserver(messageListRef, sentinelRef);
|
||||
const {
|
||||
startObserving,
|
||||
stopObserving,
|
||||
} = useStickyScroll(currentMessageList);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSentinelVisible) startObserving(); else stopObserving();
|
||||
toggleFollowingTail(isSentinelVisible);
|
||||
}, [isSentinelVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
setter({
|
||||
@ -135,6 +153,7 @@ const ChatMessageList: React.FC<ChatListProps> = ({
|
||||
chatIdVar(chatId);
|
||||
setLastMessageCreatedAt('');
|
||||
}, [chatId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (lastMessageCreatedAt !== '') {
|
||||
setMessageAsSeenMutation({
|
||||
@ -160,29 +179,21 @@ const ChatMessageList: React.FC<ChatListProps> = ({
|
||||
}
|
||||
}, [lastMessageCreatedAt, chatId]);
|
||||
|
||||
const setScrollToTailEventHandler = (el: HTMLDivElement) => {
|
||||
if (Math.abs(el.scrollHeight - el.clientHeight - el.scrollTop) === 0) {
|
||||
if (isElement(contentRef.current)) {
|
||||
toggleFollowingTail(true);
|
||||
}
|
||||
} else if (isElement(contentRef.current)) {
|
||||
toggleFollowingTail(false);
|
||||
}
|
||||
const setScrollToTailEventHandler = () => {
|
||||
toggleFollowingTail(isSentinelVisible);
|
||||
};
|
||||
|
||||
const toggleFollowingTail = (toggle: boolean) => {
|
||||
setFollowingTail(toggle);
|
||||
if (toggle) {
|
||||
if (isElement(contentRef.current)) {
|
||||
scrollObserver.observe(contentRef.current as HTMLDivElement);
|
||||
setFollowingTail(true);
|
||||
if (isElement(contentRef.current)) {
|
||||
if (toggle) {
|
||||
scrollObserver.observe(contentRef.current);
|
||||
} else {
|
||||
if (userLoadedBackUntilPage === null) {
|
||||
setUserLoadedBackUntilPage(Math.max(totalPages - 2, 0));
|
||||
}
|
||||
scrollObserver.unobserve(contentRef.current);
|
||||
}
|
||||
} else if (isElement(contentRef.current)) {
|
||||
if (userLoadedBackUntilPage === null) {
|
||||
setUserLoadedBackUntilPage(Math.max(totalPages - 2, 0));
|
||||
}
|
||||
scrollObserver.unobserve(contentRef.current as HTMLDivElement);
|
||||
setFollowingTail(false);
|
||||
}
|
||||
};
|
||||
|
||||
@ -196,7 +207,9 @@ const ChatMessageList: React.FC<ChatListProps> = ({
|
||||
key="unread-messages"
|
||||
label={intl.formatMessage(intlMessages.moreMessages)}
|
||||
onClick={() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
if (sentinelRef.current) {
|
||||
sentinelRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@ -206,12 +219,8 @@ const ChatMessageList: React.FC<ChatListProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
const scrollToTailEventHandler = () => {
|
||||
if (scrollObserver && contentRef.current) {
|
||||
scrollObserver.observe(contentRef.current as HTMLDivElement);
|
||||
if (isElement(messageListRef.current)) {
|
||||
messageListRef.current.scrollTop = messageListRef.current.scrollHeight + messageListRef.current.clientHeight;
|
||||
}
|
||||
setFollowingTail(true);
|
||||
if (isElement(sentinelRef.current)) {
|
||||
sentinelRef.current.scrollIntoView();
|
||||
}
|
||||
};
|
||||
|
||||
@ -220,7 +229,7 @@ const ChatMessageList: React.FC<ChatListProps> = ({
|
||||
return () => {
|
||||
window.removeEventListener(ChatEvents.SENT_MESSAGE, scrollToTailEventHandler);
|
||||
};
|
||||
}, [contentRef.current]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (followingTail) {
|
||||
@ -229,63 +238,28 @@ const ChatMessageList: React.FC<ChatListProps> = ({
|
||||
}, [followingTail]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isElement(contentRef.current)) {
|
||||
toggleFollowingTail(true);
|
||||
if (isElement(sentinelRef.current)) {
|
||||
sentinelRef.current.scrollIntoView();
|
||||
}
|
||||
|
||||
if (isElement(messageListRef.current)) {
|
||||
messageListRef.current.scrollTop = messageListRef.current.scrollHeight;
|
||||
}
|
||||
|
||||
return () => {
|
||||
toggleFollowingTail(false);
|
||||
};
|
||||
}, [contentRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!messageListRef.current || !messagesEndRef.current) return;
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
const { isIntersecting, target } = entry;
|
||||
if (isIntersecting && target === messagesEndRef.current) {
|
||||
toggleFollowingTail(true);
|
||||
}
|
||||
});
|
||||
}, {
|
||||
root: messageListRef.current,
|
||||
threshold: 1.0,
|
||||
});
|
||||
if (messagesEndObserverRef.current) {
|
||||
messagesEndObserverRef.current.disconnect();
|
||||
}
|
||||
messagesEndObserverRef.current = observer;
|
||||
observer.observe(messagesEndRef.current);
|
||||
}, [messagesEndRef.current, messageListRef.current]);
|
||||
}, []);
|
||||
|
||||
const firstPageToLoad = userLoadedBackUntilPage !== null
|
||||
? userLoadedBackUntilPage : Math.max(totalPages - 2, 0);
|
||||
? userLoadedBackUntilPage
|
||||
: Math.max(totalPages - 2, 0);
|
||||
const pagesToLoad = (totalPages - firstPageToLoad) || 1;
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
[
|
||||
<MessageListWrapper key="message-list-wrapper" id="chat-list">
|
||||
<MessageList
|
||||
ref={messageListRef}
|
||||
onWheel={(e) => {
|
||||
if (e.deltaY < 0) {
|
||||
if (isElement(contentRef.current) && followingTail) {
|
||||
toggleFollowingTail(false);
|
||||
}
|
||||
} else if (e.deltaY > 0) {
|
||||
setScrollToTailEventHandler(messageListRef.current as HTMLDivElement);
|
||||
}
|
||||
}}
|
||||
ref={messageListRefProxy}
|
||||
onMouseUp={() => {
|
||||
setScrollToTailEventHandler(messageListRef.current as HTMLDivElement);
|
||||
setScrollToTailEventHandler();
|
||||
}}
|
||||
onTouchEnd={() => {
|
||||
setScrollToTailEventHandler(messageListRef.current as HTMLDivElement);
|
||||
setScrollToTailEventHandler();
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
@ -331,7 +305,13 @@ const ChatMessageList: React.FC<ChatListProps> = ({
|
||||
})
|
||||
}
|
||||
</ChatMessages>
|
||||
<div ref={messagesEndRef} />
|
||||
<div
|
||||
ref={sentinelRefProxy}
|
||||
style={{
|
||||
height: 1,
|
||||
background: 'none',
|
||||
}}
|
||||
/>
|
||||
</MessageList>
|
||||
</MessageListWrapper>,
|
||||
renderUnreadNotification,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
import { colorGrayDark } from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import { borderSize } from '/imports/ui/stylesheets/styled-components/general';
|
||||
import { smPaddingY } from '/imports/ui/stylesheets/styled-components/general';
|
||||
import { fontSizeSmaller, fontSizeBase } from '/imports/ui/stylesheets/styled-components/typography';
|
||||
|
||||
const SingleTyper = styled.span`
|
||||
@ -38,13 +38,15 @@ const TypingIndicator = styled.span`
|
||||
`;
|
||||
|
||||
const TypingIndicatorWrapper = styled.div`
|
||||
font-size: calc(${fontSizeBase} * .75);
|
||||
color: ${colorGrayDark};
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
padding: ${borderSize} 0;
|
||||
height: 1.5rem;
|
||||
max-height: 1.5rem;
|
||||
font-size: calc(${fontSizeBase} * .75);
|
||||
color: ${colorGrayDark};
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
padding: ${smPaddingY} 0;
|
||||
height: 1.5rem;
|
||||
max-height: 1.5rem;
|
||||
line-height: 1;
|
||||
overflow-y: hidden;
|
||||
`;
|
||||
|
||||
export default {
|
||||
|
@ -124,7 +124,7 @@ const reduceAndDontMapGroupMessages = (messages) => (messages
|
||||
const isChatLocked = (receiverID) => {
|
||||
const isPublic = receiverID === PUBLIC_CHAT_ID;
|
||||
const meeting = Meetings.findOne({ meetingId: Auth.meetingID },
|
||||
{ fields: { 'lockSettingsProps.disablePublicChat': 1, 'lockSettingsProps.disablePrivateChat': 1 } });
|
||||
{ fields: { 'lockSettings.disablePublicChat': 1, 'lockSettings.disablePrivateChat': 1 } });
|
||||
const user = Users.findOne({ meetingId: Auth.meetingID, userId: Auth.userID },
|
||||
{ fields: { locked: 1, role: 1 } });
|
||||
const receiver = Users.findOne({ meetingId: Auth.meetingID, userId: receiverID },
|
||||
@ -136,13 +136,13 @@ const isChatLocked = (receiverID) => {
|
||||
return !isPublic;
|
||||
}
|
||||
|
||||
if (meeting.lockSettingsProps !== undefined) {
|
||||
if (meeting.lockSettings !== undefined) {
|
||||
if (user.locked && user.role !== ROLE_MODERATOR) {
|
||||
if (isPublic) {
|
||||
return meeting.lockSettingsProps.disablePublicChat;
|
||||
return meeting.lockSettings.disablePublicChat;
|
||||
}
|
||||
return !isReceiverModerator
|
||||
&& meeting.lockSettingsProps.disablePrivateChat;
|
||||
&& meeting.lockSettings.disablePrivateChat;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,216 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import ModalSimple from '/imports/ui/components/common/modal/simple/component';
|
||||
import AudioService from '/imports/ui/components/audio/service';
|
||||
import Styled from './styles';
|
||||
|
||||
const SELECT_RANDOM_USER_COUNTDOWN = window.meetingClientSettings.public.selectRandomUser.countdown;
|
||||
|
||||
const messages = defineMessages({
|
||||
noViewers: {
|
||||
id: 'app.modal.randomUser.noViewers.description',
|
||||
description: 'Label displayed when no viewers are avaiable',
|
||||
},
|
||||
selected: {
|
||||
id: 'app.modal.randomUser.selected.description',
|
||||
description: 'Label shown to the selected user',
|
||||
},
|
||||
randUserTitle: {
|
||||
id: 'app.modal.randomUser.title',
|
||||
description: 'Modal title label',
|
||||
},
|
||||
whollbeSelected: {
|
||||
id: 'app.modal.randomUser.who',
|
||||
description: 'Label shown during the selection',
|
||||
},
|
||||
onlyOneViewerTobeSelected: {
|
||||
id: 'app.modal.randomUser.alone',
|
||||
description: 'Label shown when only one viewer to be selected',
|
||||
},
|
||||
reselect: {
|
||||
id: 'app.modal.randomUser.reselect.label',
|
||||
description: 'select new random user button label',
|
||||
},
|
||||
ariaModalTitle: {
|
||||
id: 'app.modal.randomUser.ariaLabel.title',
|
||||
description: 'modal title displayed to screen reader',
|
||||
},
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
numAvailableViewers: PropTypes.number.isRequired,
|
||||
randomUserReq: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class RandomUserSelect extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
if (props.currentUser.presenter) {
|
||||
props.randomUserReq();
|
||||
}
|
||||
|
||||
if (SELECT_RANDOM_USER_COUNTDOWN) {
|
||||
this.state = {
|
||||
count: 0,
|
||||
};
|
||||
this.play = this.play.bind(this);
|
||||
}
|
||||
}
|
||||
|
||||
iterateSelection() {
|
||||
if (this.props.mappedRandomlySelectedUsers.length > 1) {
|
||||
const that = this;
|
||||
setTimeout(delay(that.props.mappedRandomlySelectedUsers, 1), that.props.mappedRandomlySelectedUsers[1][1]);
|
||||
function delay(arr, num) {
|
||||
that.setState({
|
||||
count: num,
|
||||
});
|
||||
if (num < that.props.mappedRandomlySelectedUsers.length - 1) {
|
||||
setTimeout(() => { delay(arr, num + 1); }, arr[num + 1][1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { keepModalOpen, toggleKeepModalOpen, currentUser } = this.props;
|
||||
|
||||
if (currentUser.presenter && !keepModalOpen) {
|
||||
toggleKeepModalOpen();
|
||||
}
|
||||
|
||||
if (SELECT_RANDOM_USER_COUNTDOWN && !currentUser.presenter) {
|
||||
this.iterateSelection();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (SELECT_RANDOM_USER_COUNTDOWN) {
|
||||
if (this.props.currentUser.presenter && this.state.count === 0) {
|
||||
this.iterateSelection();
|
||||
}
|
||||
|
||||
if ((prevState.count !== this.state.count) && this.props.keepModalOpen) {
|
||||
this.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
play() {
|
||||
AudioService.playAlertSound(`${window.meetingClientSettings.public.app.cdn
|
||||
+ window.meetingClientSettings.public.app.basename
|
||||
+ window.meetingClientSettings.public.app.instanceId}`
|
||||
+ '/resources/sounds/Poll.mp3');
|
||||
}
|
||||
|
||||
reselect() {
|
||||
if (SELECT_RANDOM_USER_COUNTDOWN) {
|
||||
this.setState({
|
||||
count: 0,
|
||||
});
|
||||
}
|
||||
this.props.randomUserReq();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
keepModalOpen,
|
||||
toggleKeepModalOpen,
|
||||
intl,
|
||||
setIsOpen,
|
||||
numAvailableViewers,
|
||||
currentUser,
|
||||
clearRandomlySelectedUser,
|
||||
mappedRandomlySelectedUsers,
|
||||
isOpen,
|
||||
priority,
|
||||
} = this.props;
|
||||
|
||||
const counter = SELECT_RANDOM_USER_COUNTDOWN ? this.state.count : 0;
|
||||
if (mappedRandomlySelectedUsers.length < counter + 1) return null;
|
||||
|
||||
const selectedUser = SELECT_RANDOM_USER_COUNTDOWN ? mappedRandomlySelectedUsers[counter][0] :
|
||||
mappedRandomlySelectedUsers[mappedRandomlySelectedUsers.length - 1][0];
|
||||
const countDown = SELECT_RANDOM_USER_COUNTDOWN ?
|
||||
mappedRandomlySelectedUsers.length - this.state.count - 1 : 0;
|
||||
|
||||
let viewElement;
|
||||
let title;
|
||||
|
||||
const amISelectedUser = currentUser.userId === selectedUser.userId;
|
||||
if (numAvailableViewers < 1 || (currentUser.presenter && amISelectedUser)) { // there's no viewers to select from,
|
||||
// or when you are the presenter but selected, which happens when the presenter ability is passed to somebody
|
||||
// and people are entering and leaving the meeting
|
||||
// display modal informing presenter that there's no viewers to select from
|
||||
viewElement = (
|
||||
<Styled.ModalViewContainer>
|
||||
<div data-test="noViewersSelectedMessage">
|
||||
{intl.formatMessage(messages.noViewers)}
|
||||
</div>
|
||||
</Styled.ModalViewContainer>
|
||||
);
|
||||
title = intl.formatMessage(messages.randUserTitle);
|
||||
} else { // viewers are available
|
||||
if (!selectedUser) return null; // rendering triggered before selectedUser is available
|
||||
|
||||
// display modal with random user selection
|
||||
viewElement = (
|
||||
<Styled.ModalViewContainer>
|
||||
<Styled.ModalAvatar aria-hidden style={{ backgroundColor: `${selectedUser.color}` }}>
|
||||
{selectedUser.name.slice(0, 2)}
|
||||
</Styled.ModalAvatar>
|
||||
<Styled.SelectedUserName data-test="selectedUserName">
|
||||
{selectedUser.name}
|
||||
</Styled.SelectedUserName>
|
||||
{currentUser.presenter
|
||||
&& countDown === 0
|
||||
&& (
|
||||
<Styled.SelectButton
|
||||
label={intl.formatMessage(messages.reselect)}
|
||||
color="primary"
|
||||
size="md"
|
||||
onClick={() => this.reselect()}
|
||||
data-test="selectAgainRadomUser"
|
||||
/>
|
||||
)}
|
||||
</Styled.ModalViewContainer>
|
||||
);
|
||||
title = countDown == 0
|
||||
? amISelectedUser
|
||||
? `${intl.formatMessage(messages.selected)}`
|
||||
: numAvailableViewers == 1 && currentUser.presenter
|
||||
? `${intl.formatMessage(messages.onlyOneViewerTobeSelected)}`
|
||||
: `${intl.formatMessage(messages.randUserTitle)}`
|
||||
: `${intl.formatMessage(messages.whollbeSelected)} ${countDown}`;
|
||||
}
|
||||
if (keepModalOpen) {
|
||||
return (
|
||||
<ModalSimple
|
||||
onRequestClose={() => {
|
||||
if (currentUser.presenter) clearRandomlySelectedUser();
|
||||
toggleKeepModalOpen();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
contentLabel={intl.formatMessage(messages.ariaModalTitle)}
|
||||
title={title}
|
||||
{...{
|
||||
isOpen,
|
||||
priority,
|
||||
}}
|
||||
>
|
||||
{viewElement}
|
||||
</ModalSimple>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RandomUserSelect.propTypes = propTypes;
|
||||
export default injectIntl(RandomUserSelect);
|
@ -1,116 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
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 { makeCall } from '/imports/ui/services/api';
|
||||
import RandomUserSelect from './component';
|
||||
import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import { PICK_RANDOM_VIEWER } from '/imports/ui/core/graphql/mutations/userMutations';
|
||||
|
||||
const SELECT_RANDOM_USER_ENABLED = window.meetingClientSettings.public.selectRandomUser.enabled;
|
||||
|
||||
// A value that is used by component to remember
|
||||
// whether it should be open or closed after a render
|
||||
let keepModalOpen = true;
|
||||
|
||||
// A value that stores the previous indicator
|
||||
let updateIndicator = 1;
|
||||
|
||||
const toggleKeepModalOpen = () => { keepModalOpen = ! keepModalOpen; };
|
||||
|
||||
const RandomUserSelectContainer = (props) => {
|
||||
const usingUsersContext = useContext(UsersContext);
|
||||
const { users } = usingUsersContext;
|
||||
const { randomlySelectedUser } = props;
|
||||
const { data: currentUserData } = useCurrentUser((user) => ({
|
||||
presenter: user.presenter,
|
||||
}));
|
||||
|
||||
const [pickRandomViewer] = useMutation(PICK_RANDOM_VIEWER);
|
||||
|
||||
const randomUserReq = () => (SELECT_RANDOM_USER_ENABLED ? pickRandomViewer() : null);
|
||||
|
||||
if (!users || !currentUserData) return null;
|
||||
|
||||
let mappedRandomlySelectedUsers = [];
|
||||
|
||||
const currentUser = {
|
||||
userId: Auth.userID,
|
||||
presenter: currentUserData?.presenter,
|
||||
};
|
||||
|
||||
try {
|
||||
if (!currentUser.presenter // this functionality does not bother presenter
|
||||
&& (!keepModalOpen) // we only ween a change if modal has been closed before
|
||||
&& (randomlySelectedUser[0][1] !== updateIndicator)// if they are different, a user was generated
|
||||
) { keepModalOpen = true; } // reopen modal
|
||||
if (!currentUser.presenter) { updateIndicator = randomlySelectedUser[0][1]; } // keep indicator up to date
|
||||
} catch (err) {
|
||||
logger.error({
|
||||
logCode: 'Random_USer_Error',
|
||||
extraInfo: {
|
||||
stackTrace: err,
|
||||
},
|
||||
},
|
||||
'\nIssue in Random User Select container caused by back-end crash'
|
||||
+ '\nValue of 6 randomly selected users was passed as '
|
||||
+ `{${randomlySelectedUser}}`
|
||||
+ '\nHowever, it is handled.'
|
||||
+ '\nError message:'
|
||||
+ `\n${err}`);
|
||||
}
|
||||
|
||||
if (randomlySelectedUser) {
|
||||
mappedRandomlySelectedUsers = randomlySelectedUser.map((ui) => {
|
||||
const selectedUser = users[Auth.meetingID][ui[0]];
|
||||
if (selectedUser){
|
||||
return [{
|
||||
userId: selectedUser.userId,
|
||||
avatar: selectedUser.avatar,
|
||||
color: selectedUser.color,
|
||||
name: selectedUser.name,
|
||||
}, ui[1]];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<RandomUserSelect
|
||||
{...props}
|
||||
mappedRandomlySelectedUsers={mappedRandomlySelectedUsers}
|
||||
currentUser={currentUser}
|
||||
keepModalOpen={keepModalOpen}
|
||||
randomUserReq={randomUserReq}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default withTracker(() => {
|
||||
const viewerPool = Users.find({
|
||||
meetingId: Auth.meetingID,
|
||||
presenter: { $ne: true },
|
||||
role: { $eq: 'VIEWER' },
|
||||
}, {
|
||||
fields: {
|
||||
userId: 1,
|
||||
},
|
||||
}).fetch();
|
||||
|
||||
const meeting = Meetings.findOne({ meetingId: Auth.meetingID }, {
|
||||
fields: {
|
||||
randomlySelectedUser: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const clearRandomlySelectedUser = () => (SELECT_RANDOM_USER_ENABLED ? makeCall('clearRandomlySelectedUser') : null);
|
||||
|
||||
return ({
|
||||
toggleKeepModalOpen,
|
||||
numAvailableViewers: viewerPool.length,
|
||||
clearRandomlySelectedUser,
|
||||
randomlySelectedUser: meeting.randomlySelectedUser,
|
||||
});
|
||||
})(RandomUserSelectContainer);
|
@ -1,57 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
import {
|
||||
fontSizeXXL,
|
||||
headingsFontWeight,
|
||||
} from '/imports/ui/stylesheets/styled-components/typography';
|
||||
import {
|
||||
mdPaddingX,
|
||||
smPaddingX,
|
||||
} from '/imports/ui/stylesheets/styled-components/general';
|
||||
|
||||
const ModalViewContainer = styled.div`
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
|
||||
& > div {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const ModalAvatar = styled.div`
|
||||
height: 6rem;
|
||||
width: 6rem;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: white;
|
||||
font-size: ${fontSizeXXL};
|
||||
font-weight: 400;
|
||||
margin-bottom: ${smPaddingX};
|
||||
text-transform: capitalize;
|
||||
`;
|
||||
|
||||
const SelectedUserName = styled.div`
|
||||
margin-bottom: ${mdPaddingX};
|
||||
font-weight: ${headingsFontWeight};
|
||||
font-size: 2rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const SelectButton = styled(Button)`
|
||||
margin-bottom: ${mdPaddingX};
|
||||
`;
|
||||
|
||||
export default {
|
||||
ModalViewContainer,
|
||||
ModalAvatar,
|
||||
SelectedUserName,
|
||||
SelectButton,
|
||||
};
|
@ -0,0 +1,23 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useCreateUseSubscription } from '/imports/ui/core/hooks/createUseSubscription';
|
||||
import MEETING_SUBSCRIPTION from '/imports/ui/core/graphql/queries/meetingSubscription';
|
||||
import { Meeting } from '/imports/ui/Types/meeting';
|
||||
import Meetings from '/imports/api/meetings';
|
||||
|
||||
const MeetingGrapQlMiniMongoAdapter: React.FC = () => {
|
||||
const meetingSubscription = useCreateUseSubscription<Meeting>(MEETING_SUBSCRIPTION, {}, true);
|
||||
const {
|
||||
data: meetingData,
|
||||
} = meetingSubscription();
|
||||
|
||||
useEffect(() => {
|
||||
if (meetingData) {
|
||||
const meeting = JSON.parse(JSON.stringify(meetingData[0]));
|
||||
const { meetingId } = meeting;
|
||||
Meetings.upsert({ meetingId }, meeting);
|
||||
}
|
||||
}, [meetingData]);
|
||||
return null;
|
||||
};
|
||||
|
||||
export default MeetingGrapQlMiniMongoAdapter;
|
@ -4,8 +4,8 @@ import {
|
||||
import { WebSocketLink } from '@apollo/client/link/ws';
|
||||
import { SubscriptionClient } from 'subscriptions-transport-ws';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { LoadingContext } from '/imports/ui/components/common/loading-screen/loading-screen-HOC/component';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
|
||||
interface ConnectionManagerProps {
|
||||
children: React.ReactNode;
|
||||
@ -37,11 +37,13 @@ const ConnectionManager: React.FC<ConnectionManagerProps> = ({ children }): Reac
|
||||
loadingContextInfo.setLoading(false, '');
|
||||
throw new Error('Error fetching GraphQL URL: '.concat(error.message || ''));
|
||||
});
|
||||
loadingContextInfo.setLoading(true, 'Fetching GraphQL URL');
|
||||
logger.info('Fetching GraphQL URL');
|
||||
loadingContextInfo.setLoading(true, '1/4');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadingContextInfo.setLoading(true, 'Connecting to GraphQL server');
|
||||
logger.info('Connecting to GraphQL server');
|
||||
loadingContextInfo.setLoading(true, '2/4');
|
||||
if (graphqlUrl) {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const sessionToken = urlParams.get('sessionToken');
|
||||
@ -56,6 +58,7 @@ const ConnectionManager: React.FC<ConnectionManagerProps> = ({ children }): Reac
|
||||
const subscription = new SubscriptionClient(graphqlUrl, {
|
||||
reconnect: true,
|
||||
timeout: 30000,
|
||||
minTimeout: 30000,
|
||||
connectionParams: {
|
||||
headers: {
|
||||
'X-Session-Token': sessionToken,
|
||||
@ -82,7 +85,7 @@ const ConnectionManager: React.FC<ConnectionManagerProps> = ({ children }): Reac
|
||||
client = new ApolloClient({
|
||||
link: wsLink,
|
||||
cache: new InMemoryCache(),
|
||||
connectToDevTools: Meteor.isDevelopment,
|
||||
connectToDevTools: true,
|
||||
});
|
||||
setApolloClient(client);
|
||||
} catch (error) {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Session } from 'meteor/session';
|
||||
import { ErrorScreen } from '../../error-screen/component';
|
||||
import LoadingScreen from '../../common/loading-screen/component';
|
||||
|
||||
@ -69,6 +70,10 @@ const StartupDataFetch: React.FC<StartupDataFetchProps> = ({
|
||||
setSettingsFetched(true);
|
||||
clearTimeout(timeoutRef.current);
|
||||
setLoading(false);
|
||||
}).catch(() => {
|
||||
Session.set('errorMessageDescription', 'meeting_ended');
|
||||
setError('Error fetching startup data');
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
@ -79,6 +84,7 @@ const StartupDataFetch: React.FC<StartupDataFetchProps> = ({
|
||||
? (
|
||||
<ErrorScreen
|
||||
endedReason={error}
|
||||
code={403}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
|
@ -5,9 +5,9 @@ import Meetings from '/imports/api/meetings';
|
||||
const getMeetingTitle = () => {
|
||||
const meeting = Meetings.findOne({
|
||||
meetingId: Auth.meetingID,
|
||||
}, { fields: { 'meetingProp.name': 1, 'breakoutProps.sequence': 1 } });
|
||||
}, { fields: { name: 1, 'breakoutPolicies.sequence': 1 } });
|
||||
|
||||
return meeting.meetingProp.name;
|
||||
return meeting.name;
|
||||
};
|
||||
|
||||
const getUsers = () => {
|
||||
|
@ -3,7 +3,6 @@ import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import ReactPlayer from 'react-player';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import audioManager from '/imports/ui/services/audio-manager';
|
||||
import { Session } from 'meteor/session';
|
||||
import { useReactiveVar, useMutation } from '@apollo/client';
|
||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||
import { ExternalVideoVolumeCommandsEnum } from 'bigbluebutton-html-plugin-sdk/dist/cjs/ui-commands/external-video/volume/enums';
|
||||
@ -13,6 +12,8 @@ import * as PluginSdk from 'bigbluebutton-html-plugin-sdk';
|
||||
import { UI_DATA_LISTENER_SUBSCRIBED } from 'bigbluebutton-html-plugin-sdk/dist/cjs/ui-data-hooks/consts';
|
||||
import { ExternalVideoVolumeUiDataNames } from 'bigbluebutton-html-plugin-sdk';
|
||||
import { ExternalVideoVolumeUiDataPayloads } from 'bigbluebutton-html-plugin-sdk/dist/cjs/ui-data-hooks/external-video/volume/types';
|
||||
import MediaService from '/imports/ui/components/media/service';
|
||||
import NotesService from '/imports/ui/components/notes/service';
|
||||
|
||||
import useMeeting from '/imports/ui/core/hooks/useMeeting';
|
||||
import {
|
||||
@ -32,7 +33,7 @@ import { uniqueId } from '/imports/utils/string-utils';
|
||||
import useTimeSync from '/imports/ui/core/local-states/useTimeSync';
|
||||
import ExternalVideoPlayerToolbar from './toolbar/component';
|
||||
import deviceInfo from '/imports/utils/deviceInfo';
|
||||
import { ACTIONS } from '../../layout/enums';
|
||||
import { ACTIONS, PRESENTATION_AREA } from '../../layout/enums';
|
||||
import { EXTERNAL_VIDEO_UPDATE } from '../mutations';
|
||||
|
||||
import PeerTube from '../custom-players/peertube';
|
||||
@ -71,9 +72,10 @@ interface ExternalVideoPlayerProps {
|
||||
playing: boolean;
|
||||
playerPlaybackRate: number;
|
||||
currentTime: number;
|
||||
layoutContextDispatch: ReturnType<typeof layoutDispatch>;
|
||||
key: string;
|
||||
setKey: (key: string) => void;
|
||||
shouldShowSharedNotes(): boolean;
|
||||
pinSharedNotes(pinned: boolean): void;
|
||||
}
|
||||
|
||||
// @ts-ignore - PeerTubePlayer is not typed
|
||||
@ -93,9 +95,10 @@ const ExternalVideoPlayer: React.FC<ExternalVideoPlayerProps> = ({
|
||||
playerPlaybackRate,
|
||||
currentTime,
|
||||
isEchoTest,
|
||||
layoutContextDispatch,
|
||||
key,
|
||||
setKey,
|
||||
shouldShowSharedNotes,
|
||||
pinSharedNotes,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
@ -219,16 +222,6 @@ const ExternalVideoPlayer: React.FC<ExternalVideoPlayerProps> = ({
|
||||
setAutoPlayBlocked(true);
|
||||
}, AUTO_PLAY_BLOCK_DETECTION_TIMEOUT_SECONDS * 1000);
|
||||
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_HAS_EXTERNAL_VIDEO,
|
||||
value: true,
|
||||
});
|
||||
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_PRESENTATION_IS_OPEN,
|
||||
value: true,
|
||||
});
|
||||
|
||||
const handleExternalVideoVolumeSet = ((
|
||||
event: CustomEvent<SetExternalVideoVolumeCommandArguments>,
|
||||
) => setVolume(event.detail.volume)) as EventListener;
|
||||
@ -250,6 +243,16 @@ const ExternalVideoPlayer: React.FC<ExternalVideoPlayerProps> = ({
|
||||
}
|
||||
}, [playerRef.current]);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldShowSharedNotes()) {
|
||||
pinSharedNotes(false);
|
||||
return () => {
|
||||
pinSharedNotes(true);
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}, []);
|
||||
|
||||
// --- Plugin related code ---;
|
||||
const internalPlayer = playerRef.current?.getInternalPlayer ? playerRef.current?.getInternalPlayer() : null;
|
||||
if (internalPlayer && internalPlayer?.isMuted
|
||||
@ -423,21 +426,20 @@ const ExternalVideoPlayerContainer: React.FC = () => {
|
||||
useEffect(() => {
|
||||
if (!currentMeeting?.externalVideo?.externalVideoUrl && !theresNoExternalVideo.current) {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_HAS_EXTERNAL_VIDEO,
|
||||
value: false,
|
||||
type: ACTIONS.SET_PILE_CONTENT_FOR_PRESENTATION_AREA,
|
||||
value: {
|
||||
content: PRESENTATION_AREA.EXTERNAL_VIDEO,
|
||||
open: false,
|
||||
},
|
||||
});
|
||||
theresNoExternalVideo.current = true;
|
||||
} else if (currentMeeting?.externalVideo?.externalVideoUrl && theresNoExternalVideo.current) {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_PRESENTATION_IS_OPEN,
|
||||
value: Session.get('presentationLastState'),
|
||||
});
|
||||
} else if (currentMeeting?.externalVideo?.externalVideoUrl) {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_HAS_EXTERNAL_VIDEO,
|
||||
value: true,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_PRESENTATION_IS_OPEN,
|
||||
value: true,
|
||||
type: ACTIONS.SET_PILE_CONTENT_FOR_PRESENTATION_AREA,
|
||||
value: {
|
||||
content: PRESENTATION_AREA.EXTERNAL_VIDEO,
|
||||
open: true,
|
||||
},
|
||||
});
|
||||
theresNoExternalVideo.current = false;
|
||||
}
|
||||
@ -505,7 +507,8 @@ const ExternalVideoPlayerContainer: React.FC = () => {
|
||||
const playerUpdatedAt = currentMeeting.externalVideo?.updatedAt ?? Date.now();
|
||||
const playerUpdatedAtDate = new Date(playerUpdatedAt);
|
||||
const currentDate = new Date(Date.now() + timeSync);
|
||||
const currentTime = (((currentDate.getTime() - playerUpdatedAtDate.getTime()) / 1000)
|
||||
const isPaused = !currentMeeting.externalVideo?.playerPlaying ?? false;
|
||||
const currentTime = isPaused ? playerCurrentTime : (((currentDate.getTime() - playerUpdatedAtDate.getTime()) / 1000)
|
||||
+ playerCurrentTime) * playerPlaybackRate;
|
||||
const isPresenter = currentUser.presenter ?? false;
|
||||
|
||||
@ -519,12 +522,13 @@ const ExternalVideoPlayerContainer: React.FC = () => {
|
||||
playing={currentMeeting.externalVideo?.playerPlaying ?? false}
|
||||
playerPlaybackRate={currentMeeting.externalVideo?.playerPlaybackRate ?? 1}
|
||||
isResizing={isResizing}
|
||||
layoutContextDispatch={layoutContextDispatch}
|
||||
fullscreenContext={fullscreenContext}
|
||||
externalVideo={externalVideo}
|
||||
currentTime={isPresenter ? playerCurrentTime : currentTime}
|
||||
key={key}
|
||||
setKey={setKey}
|
||||
shouldShowSharedNotes={MediaService.shouldShowSharedNotes}
|
||||
pinSharedNotes={NotesService.pinSharedNotes}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,52 +1,27 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import { GenericComponent } from 'bigbluebutton-html-plugin-sdk';
|
||||
import React from 'react';
|
||||
import * as Styled from './styles';
|
||||
import { GenericComponentProps } from './types';
|
||||
import { EXTERNAL_VIDEO_STOP } from '../external-video-player/mutations';
|
||||
import NotesService from '/imports/ui/components/notes/service';
|
||||
import GenericComponentItem from './generic-component-item/component';
|
||||
import { screenshareHasEnded } from '../screenshare/service';
|
||||
|
||||
const mapGenericComponentItems = (
|
||||
genericComponents: GenericComponent[],
|
||||
) => genericComponents.map((genericComponent) => (
|
||||
<GenericComponentItem
|
||||
key={genericComponent.id}
|
||||
renderFunction={genericComponent.contentFunction}
|
||||
/>
|
||||
));
|
||||
|
||||
const GenericComponentContent: React.FC<GenericComponentProps> = ({
|
||||
isResizing,
|
||||
genericComponent,
|
||||
genericComponentLayoutInformation,
|
||||
renderFunctionComponents,
|
||||
hasExternalVideoOnLayout,
|
||||
isSharedNotesPinned,
|
||||
hasScreenShareOnLayout,
|
||||
genericComponentId,
|
||||
}) => {
|
||||
const [stopExternalVideoShare] = useMutation(EXTERNAL_VIDEO_STOP);
|
||||
|
||||
const {
|
||||
height,
|
||||
width,
|
||||
top,
|
||||
left,
|
||||
right,
|
||||
} = genericComponent;
|
||||
} = genericComponentLayoutInformation;
|
||||
|
||||
const isMinimized = width === 0 && height === 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (hasExternalVideoOnLayout) stopExternalVideoShare();
|
||||
if (isSharedNotesPinned) NotesService.pinSharedNotes(false);
|
||||
if (hasScreenShareOnLayout) screenshareHasEnded();
|
||||
}, []);
|
||||
|
||||
const numberOfTiles = renderFunctionComponents.length;
|
||||
const componentToRender = renderFunctionComponents.filter((g) => genericComponentId === g.id);
|
||||
return (
|
||||
<Styled.Container
|
||||
numberOfTiles={numberOfTiles}
|
||||
style={{
|
||||
height,
|
||||
width,
|
||||
@ -57,7 +32,10 @@ const GenericComponentContent: React.FC<GenericComponentProps> = ({
|
||||
isResizing={isResizing}
|
||||
isMinimized={isMinimized}
|
||||
>
|
||||
{mapGenericComponentItems(renderFunctionComponents)}
|
||||
<GenericComponentItem
|
||||
key={componentToRender[0]?.id}
|
||||
renderFunction={componentToRender[0]?.contentFunction}
|
||||
/>
|
||||
</Styled.Container>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import React, { useContext, useRef } from 'react';
|
||||
import * as PluginSdk from 'bigbluebutton-html-plugin-sdk';
|
||||
|
||||
import {
|
||||
@ -7,63 +7,76 @@ import {
|
||||
layoutSelectOutput,
|
||||
} from '../layout/context';
|
||||
import {
|
||||
DispatcherFunction,
|
||||
GenericComponent as GenericComponentFromLayout,
|
||||
Input,
|
||||
Output,
|
||||
} from '../layout/layoutTypes';
|
||||
import { PluginsContext } from '../components-data/plugin-context/context';
|
||||
import { GenericComponentContainerProps } from './types';
|
||||
import { ACTIONS } from '../layout/enums';
|
||||
import GenericComponentContent from './component';
|
||||
import { ACTIONS, PRESENTATION_AREA } from '../layout/enums';
|
||||
import getDifferenceBetweenLists from './utils';
|
||||
import { GenericComponentContainerProps } from './types';
|
||||
|
||||
const GenericComponentContainer: React.FC<GenericComponentContainerProps> = (props: GenericComponentContainerProps) => {
|
||||
const {
|
||||
shouldShowScreenshare,
|
||||
shouldShowSharedNotes,
|
||||
shouldShowExternalVideo,
|
||||
} = props;
|
||||
|
||||
const hasExternalVideoOnLayout: boolean = layoutSelectInput((i: Input) => i.externalVideo.hasExternalVideo);
|
||||
const hasScreenShareOnLayout: boolean = layoutSelectInput((i: Input) => i.screenShare.hasScreenShare);
|
||||
const isSharedNotesPinned: boolean = layoutSelectInput((i: Input) => i.sharedNotes.isPinned);
|
||||
const { genericComponentId } = props;
|
||||
|
||||
const previousPluginGenericComponents = useRef<PluginSdk.GenericComponent[]>([]);
|
||||
const {
|
||||
pluginsExtensibleAreasAggregatedState,
|
||||
} = useContext(PluginsContext);
|
||||
const layoutContextDispatch: DispatcherFunction = layoutDispatch();
|
||||
let genericComponentExtensibleArea = [] as PluginSdk.GenericComponent[];
|
||||
|
||||
if (pluginsExtensibleAreasAggregatedState.genericComponents) {
|
||||
genericComponentExtensibleArea = [
|
||||
...pluginsExtensibleAreasAggregatedState.genericComponents as PluginSdk.GenericComponent[],
|
||||
];
|
||||
}
|
||||
useEffect(() => {
|
||||
if (shouldShowScreenshare || shouldShowSharedNotes || shouldShowExternalVideo) {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_HAS_GENERIC_COMPONENT,
|
||||
value: false,
|
||||
const [
|
||||
genericComponentsAdded,
|
||||
genericComponentsRemoved,
|
||||
] = getDifferenceBetweenLists(previousPluginGenericComponents.current, genericComponentExtensibleArea);
|
||||
if (genericComponentsAdded.length > 0 || genericComponentsRemoved.length > 0) {
|
||||
previousPluginGenericComponents.current = [...genericComponentExtensibleArea];
|
||||
genericComponentsAdded.forEach((g) => {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_PILE_CONTENT_FOR_PRESENTATION_AREA,
|
||||
value: {
|
||||
content: PRESENTATION_AREA.GENERIC_COMPONENT,
|
||||
open: true,
|
||||
genericComponentId: g.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
genericComponentsRemoved.forEach((g) => {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_PILE_CONTENT_FOR_PRESENTATION_AREA,
|
||||
value: {
|
||||
content: PRESENTATION_AREA.GENERIC_COMPONENT,
|
||||
open: false,
|
||||
genericComponentId: g.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [
|
||||
shouldShowScreenshare,
|
||||
shouldShowSharedNotes,
|
||||
shouldShowExternalVideo,
|
||||
]);
|
||||
}
|
||||
|
||||
const genericComponentLayoutInformation: GenericComponentFromLayout = layoutSelectOutput(
|
||||
(i: Output) => i.genericComponent,
|
||||
);
|
||||
|
||||
const genericComponent: GenericComponentFromLayout = layoutSelectOutput((i: Output) => i.genericComponent);
|
||||
const hasGenericComponentOnLayout: boolean = layoutSelectInput((i: Input) => i.genericComponent.hasGenericComponent);
|
||||
const cameraDock = layoutSelectInput((i: Input) => i.cameraDock);
|
||||
const { isResizing } = cameraDock;
|
||||
|
||||
const layoutContextDispatch = layoutDispatch();
|
||||
if (!hasGenericComponentOnLayout || !genericComponentExtensibleArea) return null;
|
||||
if (!genericComponentExtensibleArea
|
||||
|| genericComponentExtensibleArea.length === 0
|
||||
|| !genericComponentId) return null;
|
||||
return (
|
||||
<GenericComponentContent
|
||||
hasExternalVideoOnLayout={hasExternalVideoOnLayout}
|
||||
isSharedNotesPinned={isSharedNotesPinned}
|
||||
hasScreenShareOnLayout={hasScreenShareOnLayout}
|
||||
genericComponentId={genericComponentId}
|
||||
renderFunctionComponents={genericComponentExtensibleArea}
|
||||
isResizing={isResizing}
|
||||
genericComponent={genericComponent}
|
||||
genericComponentLayoutInformation={genericComponentLayoutInformation}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
type ContainerProps = {
|
||||
numberOfTiles: number;
|
||||
isResizing: boolean;
|
||||
isMinimized: boolean;
|
||||
};
|
||||
@ -12,11 +11,6 @@ export const Container = styled.div<ContainerProps>`
|
||||
background: var(--color-black);
|
||||
z-index: 5;
|
||||
display: grid;
|
||||
${({ numberOfTiles }) => {
|
||||
if (numberOfTiles > 1) return 'grid-template-columns: 1fr 1fr;';
|
||||
return 'grid-template-columns: 1fr';
|
||||
}}
|
||||
grid-template-columns: 1fr 1fr;
|
||||
${({ isResizing }) => isResizing && `
|
||||
pointer-events: none;
|
||||
`}
|
||||
|
@ -2,16 +2,11 @@ import { GenericComponent } from 'bigbluebutton-html-plugin-sdk';
|
||||
import { GenericComponent as GenericComponentLayout } from '../layout/layoutTypes';
|
||||
|
||||
export interface GenericComponentContainerProps {
|
||||
shouldShowScreenshare: boolean ;
|
||||
shouldShowSharedNotes: boolean ;
|
||||
shouldShowExternalVideo: boolean ;
|
||||
genericComponentId: string;
|
||||
}
|
||||
|
||||
export interface GenericComponentProps {
|
||||
isResizing: boolean;
|
||||
genericComponent: GenericComponentLayout;
|
||||
genericComponentId: string;
|
||||
genericComponentLayoutInformation: GenericComponentLayout;
|
||||
renderFunctionComponents: GenericComponent[];
|
||||
hasExternalVideoOnLayout: boolean;
|
||||
isSharedNotesPinned: boolean;
|
||||
hasScreenShareOnLayout: boolean;
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
import { difference } from 'ramda';
|
||||
|
||||
const getDifferenceBetweenLists = <T>(previousState: T[], currentState: T[]): T[][] => {
|
||||
const added = difference(currentState, previousState);
|
||||
const removed = difference(previousState, currentState);
|
||||
return [added, removed];
|
||||
};
|
||||
|
||||
export default getDifferenceBetweenLists;
|
@ -36,6 +36,11 @@ const CustomUsersSettings: React.FC<CustomUsersSettingsProps> = ({
|
||||
const { parameter, value } = uc;
|
||||
return { [parameter]: value };
|
||||
});
|
||||
const clientSettings = JSON.parse(sessionStorage.getItem('clientStartupSettings') || '{}');
|
||||
if (clientSettings.skipMeteorConnection) {
|
||||
setAllowToRender(true);
|
||||
return;
|
||||
}
|
||||
sendToServer(filteredData);
|
||||
}
|
||||
}, [
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useMutation, useQuery, useSubscription } from '@apollo/client';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import React, { useContext, useEffect, useRef } from 'react';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
// @ts-ignore - type avaible only to server package
|
||||
import { DDP } from 'meteor/ddp-client';
|
||||
@ -64,6 +64,7 @@ const PresenceManager: React.FC<PresenceManagerProps> = ({
|
||||
const [dispatchUserJoin] = useMutation(userJoinMutation);
|
||||
const timeoutRef = React.useRef<ReturnType<typeof setTimeout>>();
|
||||
const loadingContextInfo = useContext(LoadingContext);
|
||||
const clientSettingsRef = useRef(JSON.parse(sessionStorage.getItem('clientStartupSettings') || '{}'));
|
||||
|
||||
useEffect(() => {
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
@ -71,9 +72,11 @@ const PresenceManager: React.FC<PresenceManagerProps> = ({
|
||||
throw new Error('Authentication timeout');
|
||||
}, connectionTimeout);
|
||||
|
||||
DDP.onReconnect(() => {
|
||||
Meteor.callAsync('validateConnection', authToken, meetingId, userId);
|
||||
});
|
||||
if (!clientSettingsRef.current.skipMeteorConnection) {
|
||||
DDP.onReconnect(() => {
|
||||
Meteor.callAsync('validateConnection', authToken, meetingId, userId);
|
||||
});
|
||||
}
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const sessionToken = urlParams.get('sessionToken') as string;
|
||||
@ -120,9 +123,13 @@ const PresenceManager: React.FC<PresenceManagerProps> = ({
|
||||
useEffect(() => {
|
||||
if (joined) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
Meteor.callAsync('validateConnection', authToken, meetingId, userId).then(() => {
|
||||
if (clientSettingsRef.current.skipMeteorConnection) {
|
||||
setAllowToRender(true);
|
||||
});
|
||||
} else {
|
||||
Meteor.callAsync('validateConnection', authToken, meetingId, userId).then(() => {
|
||||
setAllowToRender(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [joined]);
|
||||
|
||||
|
@ -1,10 +1,11 @@
|
||||
import React, { useReducer } from 'react';
|
||||
import React, { useEffect, useReducer, useRef } from 'react';
|
||||
import { createContext, useContextSelector } from 'use-context-selector';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ACTIONS } from '/imports/ui/components/layout/enums';
|
||||
import { equals } from 'ramda';
|
||||
import { ACTIONS, PRESENTATION_AREA } from '/imports/ui/components/layout/enums';
|
||||
import DEFAULT_VALUES from '/imports/ui/components/layout/defaultValues';
|
||||
import { INITIAL_INPUT_STATE, INITIAL_OUTPUT_STATE } from './initState';
|
||||
import useUpdatePresentationAreaContent from '/imports/ui/components/plugins-engine/ui-data-hooks/layout/presentation-area/utils';
|
||||
import useUpdatePresentationAreaContentForPlugin from '/imports/ui/components/plugins-engine/ui-data-hooks/layout/presentation-area/utils';
|
||||
|
||||
// variable to debug in console log
|
||||
const debug = false;
|
||||
@ -28,7 +29,16 @@ const providerPropTypes = {
|
||||
|
||||
const LayoutContextSelector = createContext();
|
||||
|
||||
const initPresentationAreaContentActions = [{
|
||||
type: ACTIONS.SET_PILE_CONTENT_FOR_PRESENTATION_AREA,
|
||||
value: {
|
||||
content: PRESENTATION_AREA.WHITEBOARD_OPEN,
|
||||
open: true,
|
||||
},
|
||||
}];
|
||||
|
||||
const initState = {
|
||||
presentationAreaContentActions: initPresentationAreaContentActions,
|
||||
deviceType: null,
|
||||
isRTL: false,
|
||||
layoutType: DEFAULT_VALUES.layoutType,
|
||||
@ -1189,7 +1199,7 @@ const reducer = (state, action) => {
|
||||
// GENERIC COMPONENT
|
||||
case ACTIONS.SET_HAS_GENERIC_COMPONENT: {
|
||||
const { genericComponent } = state.input;
|
||||
if (genericComponent.hasGenericComponent === action.value) {
|
||||
if (genericComponent.genericComponentId === action.value) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
@ -1198,7 +1208,7 @@ const reducer = (state, action) => {
|
||||
...state.input,
|
||||
genericComponent: {
|
||||
...genericComponent,
|
||||
hasGenericComponent: action.value,
|
||||
genericComponentId: action.value,
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -1284,16 +1294,144 @@ const reducer = (state, action) => {
|
||||
},
|
||||
};
|
||||
}
|
||||
case ACTIONS.SET_PILE_CONTENT_FOR_PRESENTATION_AREA: {
|
||||
const { presentationAreaContentActions } = state;
|
||||
if (action.value.open) {
|
||||
presentationAreaContentActions.push(action);
|
||||
} else {
|
||||
const indexOfOpenedContent = presentationAreaContentActions.findIndex((p) => {
|
||||
if (action.value.content === PRESENTATION_AREA.GENERIC_COMPONENT) {
|
||||
return (
|
||||
p.value.content === action.value.content
|
||||
&& p.value.open
|
||||
&& p.value.genericComponentId === action.value.genericComponentId
|
||||
);
|
||||
}
|
||||
return (
|
||||
p.value.content === action.value.content && p.value.open
|
||||
);
|
||||
});
|
||||
if (
|
||||
indexOfOpenedContent !== -1
|
||||
) presentationAreaContentActions.splice(indexOfOpenedContent, 1);
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
presentationAreaContentActions,
|
||||
};
|
||||
}
|
||||
default: {
|
||||
throw new Error('Unexpected action');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updatePresentationAreaContent = (
|
||||
layoutContextState,
|
||||
previousPresentationAreaContentActions,
|
||||
layoutContextDispatch,
|
||||
) => {
|
||||
const {
|
||||
presentationAreaContentActions: currentPresentationAreaContentActions,
|
||||
} = layoutContextState;
|
||||
if (!equals(
|
||||
currentPresentationAreaContentActions,
|
||||
previousPresentationAreaContentActions.current,
|
||||
)) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
previousPresentationAreaContentActions.current = currentPresentationAreaContentActions.slice(0);
|
||||
const lastIndex = currentPresentationAreaContentActions.length - 1;
|
||||
const lastPresentationContentInPile = currentPresentationAreaContentActions[lastIndex];
|
||||
switch (lastPresentationContentInPile.value.content) {
|
||||
case PRESENTATION_AREA.GENERIC_COMPONENT: {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_NOTES_IS_PINNED,
|
||||
value: !lastPresentationContentInPile.value.open,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_HAS_GENERIC_COMPONENT,
|
||||
value: lastPresentationContentInPile.value.genericComponentId,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case PRESENTATION_AREA.PINNED_NOTES: {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_HAS_GENERIC_COMPONENT,
|
||||
value: undefined,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_NOTES_IS_PINNED,
|
||||
value: lastPresentationContentInPile.value.open,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case PRESENTATION_AREA.EXTERNAL_VIDEO: {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_HAS_GENERIC_COMPONENT,
|
||||
value: undefined,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_HAS_EXTERNAL_VIDEO,
|
||||
value: lastPresentationContentInPile.value.open,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case PRESENTATION_AREA.SCREEN_SHARE: {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_HAS_GENERIC_COMPONENT,
|
||||
value: undefined,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_HAS_SCREEN_SHARE,
|
||||
value: lastPresentationContentInPile.value.open,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case PRESENTATION_AREA.WHITEBOARD_OPEN: {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_HAS_SCREEN_SHARE,
|
||||
value: !lastPresentationContentInPile.value.open,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_HAS_EXTERNAL_VIDEO,
|
||||
value: !lastPresentationContentInPile.value.open,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_HAS_GENERIC_COMPONENT,
|
||||
value: undefined,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_PRESENTATION_IS_OPEN,
|
||||
value: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const LayoutContextProvider = (props) => {
|
||||
const previousPresentationAreaContentActions = useRef(
|
||||
[{
|
||||
type: ACTIONS.SET_PILE_CONTENT_FOR_PRESENTATION_AREA,
|
||||
value: {
|
||||
content: PRESENTATION_AREA.WHITEBOARD_OPEN,
|
||||
open: true,
|
||||
},
|
||||
}],
|
||||
);
|
||||
const [layoutContextState, layoutContextDispatch] = useReducer(reducer, initState);
|
||||
const { children } = props;
|
||||
useUpdatePresentationAreaContent(layoutContextState);
|
||||
useEffect(() => {
|
||||
updatePresentationAreaContent(
|
||||
layoutContextState,
|
||||
previousPresentationAreaContentActions,
|
||||
layoutContextDispatch,
|
||||
);
|
||||
}, [layoutContextState]);
|
||||
useUpdatePresentationAreaContentForPlugin(layoutContextState);
|
||||
return (
|
||||
<LayoutContextSelector.Provider value={
|
||||
[
|
||||
|
@ -112,6 +112,16 @@ export const ACTIONS = {
|
||||
|
||||
SET_SHARED_NOTES_OUTPUT: 'setSharedNotesOutput',
|
||||
SET_NOTES_IS_PINNED: 'setNotesIsPinned',
|
||||
|
||||
SET_PILE_CONTENT_FOR_PRESENTATION_AREA: 'setPileContentForPresentationArea',
|
||||
};
|
||||
|
||||
export const PRESENTATION_AREA = {
|
||||
GENERIC_COMPONENT: 'genericComponent',
|
||||
PINNED_NOTES: 'pinnedNotes',
|
||||
EXTERNAL_VIDEO: 'externalVideo',
|
||||
SCREEN_SHARE: 'screenShare',
|
||||
WHITEBOARD_OPEN: 'whiteboardOpen',
|
||||
};
|
||||
|
||||
export const PANELS = {
|
||||
|
@ -96,7 +96,7 @@ export const INITIAL_INPUT_STATE = {
|
||||
browserHeight: 0,
|
||||
},
|
||||
genericComponent: {
|
||||
hasGenericComponent: false,
|
||||
genericComponentId: undefined,
|
||||
width: 0,
|
||||
height: 0,
|
||||
browserWidth: 0,
|
||||
|
@ -366,7 +366,7 @@ const CamerasOnlyLayout = (props) => {
|
||||
hasExternalVideo: false,
|
||||
},
|
||||
genericComponent: {
|
||||
hasGenericComponent: false,
|
||||
genericComponentId: undefined,
|
||||
},
|
||||
screenShare: {
|
||||
hasScreenShare: false,
|
||||
|
@ -165,7 +165,7 @@ const CustomLayout = (props) => {
|
||||
hasExternalVideo: input.externalVideo.hasExternalVideo,
|
||||
},
|
||||
genericComponent: {
|
||||
hasGenericComponent: input.genericComponent.hasGenericComponent
|
||||
genericComponentId: input.genericComponent.genericComponentId,
|
||||
},
|
||||
screenShare: {
|
||||
hasScreenShare: input.screenShare.hasScreenShare,
|
||||
@ -209,7 +209,7 @@ const CustomLayout = (props) => {
|
||||
hasExternalVideo: input.externalVideo.hasExternalVideo,
|
||||
},
|
||||
genericComponent: {
|
||||
hasGenericComponent: input.genericComponent.hasGenericComponent
|
||||
genericComponentId: input.genericComponent.genericComponentId,
|
||||
},
|
||||
screenShare: {
|
||||
hasScreenShare: input.screenShare.hasScreenShare,
|
||||
@ -231,14 +231,13 @@ const CustomLayout = (props) => {
|
||||
const calculatesSidebarContentHeight = (cameraDockHeight) => {
|
||||
const { isOpen, slidesLength } = presentationInput;
|
||||
const { hasExternalVideo } = externalVideoInput;
|
||||
const { hasGenericComponent } = genericComponentInput;
|
||||
const { genericComponentId } = genericComponentInput;
|
||||
const { hasScreenShare } = screenShareInput;
|
||||
const { isPinned: isSharedNotesPinned } = sharedNotesInput;
|
||||
|
||||
const hasPresentation = isPresentationEnabled() && slidesLength !== 0;
|
||||
const isGeneralMediaOff =
|
||||
!hasPresentation && !hasExternalVideo
|
||||
&& !hasScreenShare && !isSharedNotesPinned && !hasGenericComponent;
|
||||
const isGeneralMediaOff = !hasPresentation && !hasExternalVideo
|
||||
&& !hasScreenShare && !isSharedNotesPinned && !genericComponentId;
|
||||
|
||||
let sidebarContentHeight = 0;
|
||||
if (sidebarContentInput.isOpen) {
|
||||
@ -410,7 +409,7 @@ const CustomLayout = (props) => {
|
||||
const calculatesMediaBounds = (sidebarNavWidth, sidebarContentWidth, cameraDockBounds) => {
|
||||
const { isOpen, slidesLength } = presentationInput;
|
||||
const { hasExternalVideo } = externalVideoInput;
|
||||
const { hasGenericComponent } = genericComponentInput;
|
||||
const { genericComponentId } = genericComponentInput;
|
||||
const { hasScreenShare } = screenShareInput;
|
||||
const { isPinned: isSharedNotesPinned } = sharedNotesInput;
|
||||
|
||||
@ -424,9 +423,8 @@ const CustomLayout = (props) => {
|
||||
const { camerasMargin } = DEFAULT_VALUES;
|
||||
|
||||
const hasPresentation = isPresentationEnabled() && slidesLength !== 0;
|
||||
const isGeneralMediaOff =
|
||||
!hasPresentation && !hasExternalVideo &&
|
||||
!hasScreenShare && !isSharedNotesPinned && !hasGenericComponent;
|
||||
const isGeneralMediaOff = !hasPresentation && !hasExternalVideo
|
||||
&& !hasScreenShare && !isSharedNotesPinned && !genericComponentId;
|
||||
|
||||
if (!isOpen || isGeneralMediaOff) {
|
||||
mediaBounds.width = 0;
|
||||
|
@ -55,7 +55,7 @@ const LayoutEngine = ({ layoutType }) => {
|
||||
const baseCameraDockBounds = (mediaAreaBounds, sidebarSize) => {
|
||||
const { isOpen, slidesLength } = presentationInput;
|
||||
const { hasExternalVideo } = externalVideoInput;
|
||||
const { hasGenericComponent } = genericComponentInput;
|
||||
const { genericComponentId } = genericComponentInput;
|
||||
const { hasScreenShare } = screenShareInput;
|
||||
const { isPinned: isSharedNotesPinned } = sharedNotesInput;
|
||||
|
||||
@ -72,7 +72,7 @@ const LayoutEngine = ({ layoutType }) => {
|
||||
const hasPresentation = isPresentationEnabled() && slidesLength !== 0;
|
||||
const isGeneralMediaOff = !hasPresentation
|
||||
&& !hasExternalVideo && !hasScreenShare
|
||||
&& !isSharedNotesPinned && !hasGenericComponent;
|
||||
&& !isSharedNotesPinned && !genericComponentId;
|
||||
|
||||
if (!isOpen || isGeneralMediaOff) {
|
||||
cameraDockBounds.width = mediaAreaBounds.width;
|
||||
|
@ -410,7 +410,7 @@ const ParticipantsAndChatOnlyLayout = (props) => {
|
||||
height: 0,
|
||||
},
|
||||
genericComponent: {
|
||||
hasGenericComponent: false,
|
||||
genericComponentId: undefined,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
|
@ -108,7 +108,7 @@ const PresentationFocusLayout = (props) => {
|
||||
hasExternalVideo: input.externalVideo.hasExternalVideo,
|
||||
},
|
||||
genericComponent: {
|
||||
hasGenericComponent: input.genericComponent.hasGenericComponent,
|
||||
genericComponentId: input.genericComponent.genericComponentId,
|
||||
},
|
||||
screenShare: {
|
||||
hasScreenShare: input.screenShare.hasScreenShare,
|
||||
@ -149,7 +149,7 @@ const PresentationFocusLayout = (props) => {
|
||||
hasExternalVideo: input.externalVideo.hasExternalVideo,
|
||||
},
|
||||
genericComponent: {
|
||||
hasGenericComponent: input.genericComponent.hasGenericComponent,
|
||||
genericComponentId: input.genericComponent.genericComponentId,
|
||||
},
|
||||
screenShare: {
|
||||
hasScreenShare: input.screenShare.hasScreenShare,
|
||||
@ -168,14 +168,13 @@ const PresentationFocusLayout = (props) => {
|
||||
const calculatesSidebarContentHeight = () => {
|
||||
const { isOpen, slidesLength } = presentationInput;
|
||||
const { hasExternalVideo } = externalVideoInput;
|
||||
const { hasGenericComponent } = genericComponentInput;
|
||||
const { genericComponentId } = genericComponentInput;
|
||||
const { hasScreenShare } = screenShareInput;
|
||||
const { isPinned: isSharedNotesPinned } = sharedNotesInput;
|
||||
|
||||
const hasPresentation = isPresentationEnabled() && slidesLength !== 0;
|
||||
const isGeneralMediaOff =
|
||||
!hasPresentation && !hasExternalVideo &&
|
||||
!hasScreenShare && !isSharedNotesPinned && !hasGenericComponent;
|
||||
const isGeneralMediaOff = !hasPresentation && !hasExternalVideo
|
||||
&& !hasScreenShare && !isSharedNotesPinned && !genericComponentId;
|
||||
|
||||
const { navBarHeight, sidebarContentMinHeight } = DEFAULT_VALUES;
|
||||
let height = 0;
|
||||
|
@ -358,7 +358,7 @@ const PresentationOnlyLayout = (props) => {
|
||||
hasExternalVideo: input.externalVideo.hasExternalVideo,
|
||||
},
|
||||
genericComponent: {
|
||||
hasGenericComponent: input.genericComponent.hasGenericComponent,
|
||||
genericComponentId: input.genericComponent.genericComponentId,
|
||||
},
|
||||
screenShare: {
|
||||
hasScreenShare: input.screenShare.hasScreenShare,
|
||||
|
@ -104,7 +104,7 @@ const SmartLayout = (props) => {
|
||||
hasExternalVideo: externalVideoInput.hasExternalVideo,
|
||||
},
|
||||
genericComponent: {
|
||||
hasGenericComponent: genericComponentInput.hasGenericComponent,
|
||||
genericComponentId: genericComponentInput.genericComponentId,
|
||||
},
|
||||
screenShare: {
|
||||
hasScreenShare: screenShareInput.hasScreenShare,
|
||||
@ -148,7 +148,7 @@ const SmartLayout = (props) => {
|
||||
hasExternalVideo: externalVideoInput.hasExternalVideo,
|
||||
},
|
||||
genericComponent: {
|
||||
hasGenericComponent: genericComponentInput.hasGenericComponent,
|
||||
genericComponentId: genericComponentInput.genericComponentId,
|
||||
},
|
||||
screenShare: {
|
||||
hasScreenShare: screenShareInput.hasScreenShare,
|
||||
@ -288,14 +288,13 @@ const SmartLayout = (props) => {
|
||||
const calculatesMediaBounds = (mediaAreaBounds, slideSize, sidebarSize, screenShareSize) => {
|
||||
const { isOpen, slidesLength } = presentationInput;
|
||||
const { hasExternalVideo } = externalVideoInput;
|
||||
const { hasGenericComponent } = genericComponentInput;
|
||||
const { genericComponentId } = genericComponentInput;
|
||||
const { hasScreenShare } = screenShareInput;
|
||||
const { isPinned: isSharedNotesPinned } = sharedNotesInput;
|
||||
|
||||
const hasPresentation = isPresentationEnabled() && slidesLength !== 0;
|
||||
const isGeneralMediaOff =
|
||||
!hasPresentation && !hasExternalVideo &&
|
||||
!hasScreenShare && !isSharedNotesPinned && !hasGenericComponent;
|
||||
const isGeneralMediaOff = !hasPresentation && !hasExternalVideo
|
||||
&& !hasScreenShare && !isSharedNotesPinned && !genericComponentId;
|
||||
|
||||
const mediaBounds = {};
|
||||
const { element: fullscreenElement } = fullscreen;
|
||||
@ -329,7 +328,7 @@ const SmartLayout = (props) => {
|
||||
|
||||
if (cameraDockInput.numCameras > 0 && !cameraDockInput.isDragging) {
|
||||
if (mediaContentSize.width !== 0 && mediaContentSize.height !== 0
|
||||
&& !hasExternalVideo && !hasGenericComponent) {
|
||||
&& !hasExternalVideo && !genericComponentId) {
|
||||
if (mediaContentSize.width < mediaAreaBounds.width && !isMobile) {
|
||||
if (mediaContentSize.width < mediaAreaBounds.width * 0.8) {
|
||||
mediaBounds.width = mediaContentSize.width;
|
||||
|
@ -111,7 +111,7 @@ const VideoFocusLayout = (props) => {
|
||||
hasExternalVideo: input.externalVideo.hasExternalVideo,
|
||||
},
|
||||
genericComponent: {
|
||||
hasGenericComponent: input.genericComponent.hasGenericComponent,
|
||||
genericComponentId: input.genericComponent.genericComponentId,
|
||||
},
|
||||
screenShare: {
|
||||
hasScreenShare: input.screenShare.hasScreenShare,
|
||||
@ -152,7 +152,7 @@ const VideoFocusLayout = (props) => {
|
||||
hasExternalVideo: input.externalVideo.hasExternalVideo,
|
||||
},
|
||||
genericComponent: {
|
||||
hasGenericComponent: input.genericComponent.hasGenericComponent,
|
||||
genericComponentId: input.genericComponent.genericComponentId,
|
||||
},
|
||||
screenShare: {
|
||||
hasScreenShare: input.screenShare.hasScreenShare,
|
||||
@ -171,15 +171,14 @@ const VideoFocusLayout = (props) => {
|
||||
const calculatesSidebarContentHeight = () => {
|
||||
const { isOpen, slidesLength } = presentationInput;
|
||||
const { hasExternalVideo } = externalVideoInput;
|
||||
const { hasGenericComponent } = genericComponentInput;
|
||||
const { genericComponentId } = genericComponentInput;
|
||||
const { hasScreenShare } = screenShareInput;
|
||||
const { isPinned: isSharedNotesPinned } = sharedNotesInput;
|
||||
|
||||
const navBarHeight = calculatesNavbarHeight();
|
||||
const hasPresentation = isPresentationEnabled() && slidesLength !== 0;
|
||||
const isGeneralMediaOff =
|
||||
!hasPresentation && !hasExternalVideo &&
|
||||
!hasScreenShare && !isSharedNotesPinned && !hasGenericComponent;
|
||||
const isGeneralMediaOff = !hasPresentation && !hasExternalVideo
|
||||
&& !hasScreenShare && !isSharedNotesPinned && !genericComponentId;
|
||||
|
||||
let minHeight = 0;
|
||||
let height = 0;
|
||||
|
@ -11,6 +11,15 @@ interface ActionBar {
|
||||
zIndex?: number
|
||||
}
|
||||
|
||||
interface PresentationAreaContentActions {
|
||||
type: string,
|
||||
value: {
|
||||
content: string,
|
||||
open: boolean,
|
||||
genericComponentId?: string;
|
||||
},
|
||||
}
|
||||
|
||||
interface ResizableEdge {
|
||||
bottom: boolean;
|
||||
left: boolean;
|
||||
@ -65,7 +74,7 @@ export interface ExternalVideo {
|
||||
}
|
||||
|
||||
export interface GenericComponent {
|
||||
hasGenericComponent?: boolean;
|
||||
genericComponentId?: string;
|
||||
browserHeight?: number;
|
||||
browserWidth?: number;
|
||||
height: number;
|
||||
@ -258,6 +267,7 @@ interface Output {
|
||||
}
|
||||
|
||||
interface Layout {
|
||||
presentationAreaContentActions: PresentationAreaContentActions[];
|
||||
deviceType: string;
|
||||
fontSize: number;
|
||||
fullscreen: Fullscreen;
|
||||
@ -268,4 +278,16 @@ interface Layout {
|
||||
output: Output;
|
||||
}
|
||||
|
||||
export { Input, Layout, Output };
|
||||
interface ActionForDispatcher {
|
||||
type: string;
|
||||
value: object;
|
||||
}
|
||||
|
||||
type DispatcherFunction = (action: ActionForDispatcher) => void;
|
||||
|
||||
export {
|
||||
Input,
|
||||
Layout,
|
||||
Output,
|
||||
DispatcherFunction,
|
||||
};
|
||||
|
@ -21,6 +21,7 @@ const LayoutModalComponent = (props) => {
|
||||
} = props;
|
||||
|
||||
const [selectedLayout, setSelectedLayout] = useState(application.selectedLayout);
|
||||
const [updateAllUsed, setUpdateAllUsed] = useState(false);
|
||||
|
||||
const BASE_NAME = window.meetingClientSettings.public.app.cdn + window.meetingClientSettings.public.app.basename;
|
||||
|
||||
@ -44,6 +45,14 @@ const LayoutModalComponent = (props) => {
|
||||
id: 'app.layout.modal.layoutLabel',
|
||||
description: 'Layout label',
|
||||
},
|
||||
layoutToastLabelAuto: {
|
||||
id: 'app.layout.modal.layoutToastLabelAuto',
|
||||
description: 'Layout toast label',
|
||||
},
|
||||
layoutToastLabelAutoOff: {
|
||||
id: 'app.layout.modal.layoutToastLabelAutoOff',
|
||||
description: 'Layout toast label',
|
||||
},
|
||||
layoutToastLabel: {
|
||||
id: 'app.layout.modal.layoutToastLabel',
|
||||
description: 'Layout toast label',
|
||||
@ -83,6 +92,15 @@ const LayoutModalComponent = (props) => {
|
||||
application:
|
||||
{ ...application, selectedLayout, pushLayout: updateAll },
|
||||
};
|
||||
if ((isModerator || isPresenter) && updateAll) {
|
||||
updateSettings(obj, intlMessages.layoutToastLabelAuto);
|
||||
setUpdateAllUsed(true);
|
||||
} else if ((isModerator || isPresenter) && !updateAll && !updateAllUsed) {
|
||||
updateSettings(obj, intlMessages.layoutToastLabelAutoOff);
|
||||
setUpdateAllUsed(false);
|
||||
} else {
|
||||
updateSettings(obj, intlMessages.layoutToastLabel);
|
||||
}
|
||||
updateSettings(obj, intlMessages.layoutToastLabel, setLocalSettings);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user