Merge pull request #14 from gustavotrott/pr-19841

Add graphql data for Gladia transcriptions
This commit is contained in:
Anton Georgiev 2024-03-27 10:54:36 -04:00 committed by GitHub
commit 2126c5192c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
177 changed files with 1882 additions and 1757 deletions

1
.gitignore vendored
View File

@ -24,3 +24,4 @@ artifacts/*
bbb-presentation-video.zip
bbb-presentation-video
bbb-graphql-actions-adapter-server/
bigbluebutton-html5/public/locales/index.json

View File

@ -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() {

View File

@ -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)
}
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -165,7 +165,6 @@ class UsersApp(
with RecordAndClearPreviousMarkersCmdMsgHdlr
with SendRecordingTimerInternalMsgHdlr
with GetRecordingStatusReqMsgHdlr
with SelectRandomViewerReqMsgHdlr
with AssignPresenterReqMsgHdlr
with ChangeUserPinStateReqMsgHdlr
with ChangeUserMobileFlagReqMsgHdlr

View File

@ -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)

View File

@ -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")
}
}
}

View File

@ -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")
}
}
}

View File

@ -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")
}
}
}

View File

@ -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"
}

View File

@ -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 = ""
)

View File

@ -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 =>

View File

@ -73,7 +73,6 @@ trait HandlerHelpers extends SystemConfiguration {
avatar = regUser.avatarURL,
color = regUser.color,
clientType = clientType,
pickExempted = false,
userLeftFlag = UserLeftFlag(false, 0)
)
}

View File

@ -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)
}
}

View File

@ -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))
}
}

View File

@ -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
}

View File

@ -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)

View File

@ -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 };

View File

@ -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

View File

@ -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
}

View 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
}
}

View File

@ -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)

View File

@ -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

View File

@ -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!
}

View File

@ -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: []

View File

@ -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

View File

@ -22,4 +22,4 @@ select_permissions:
filter:
meetingId:
_eq: X-Hasura-MeetingId
comment: ""
comment: ""

View File

@ -17,8 +17,10 @@ select_permissions:
- disablePublicChat
- hasActiveLockSetting
- hideUserList
- hideViewersCursor
- hideViewersAnnotation
- hideViewersCursor
- lockOnJoin
- lockOnJoinConfigurable
- webcamsOnlyForModerator
filter:
meetingId:

View File

@ -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: ""

View File

@ -10,6 +10,7 @@ select_permissions:
- role: bbb_client
permission:
columns:
- contentType
- hasAudio
- screenshareConf
- screenshareId

View File

@ -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

View File

@ -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: ""

View File

@ -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"

View File

@ -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")
}

View File

@ -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

View File

@ -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 (

View File

@ -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>

View File

@ -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>
);
};

View 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>

View File

@ -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);

View File

@ -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);
}

View File

@ -1,3 +1,2 @@
import './eventHandlers';
import './methods';
import './publishers';

View File

@ -1,6 +0,0 @@
import { Meteor } from 'meteor/meteor';
import clearRandomlySelectedUser from './methods/clearRandomlySelectedUser';
Meteor.methods({
clearRandomlySelectedUser,
});

View File

@ -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}`);
}
}

View File

@ -206,7 +206,6 @@ export default async function addMeeting(meeting) {
layout: LAYOUT_TYPE[meetingLayout] || 'smart',
publishedPoll: false,
guestLobbyMessage: '',
randomlySelectedUser: [],
...flat(newMeeting, {
safe: true,
}),

View File

@ -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}`);
}
}

View File

@ -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;

View File

@ -66,6 +66,7 @@ const currentParameters = [
'bbb_hide_nav_bar',
'bbb_change_layout',
'bbb_direct_leave_button',
'bbb_default_layout',
];
function valueParser(val) {

View File

@ -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,

View File

@ -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]);

View File

@ -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}`);

View File

@ -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>;
}

View File

@ -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

View File

@ -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,

View File

@ -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) => {

View File

@ -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,

View File

@ -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(),

View File

@ -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 },

View File

@ -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>
</>
);

View File

@ -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,

View File

@ -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) => {

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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');
};

View File

@ -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,

View File

@ -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 {

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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,
};

View File

@ -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;

View File

@ -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) {

View File

@ -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}

View File

@ -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 = () => {

View File

@ -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}
/>
);
};

View File

@ -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>
);
};

View File

@ -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}
/>
);
};

View File

@ -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;
`}

View File

@ -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;
}

View File

@ -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;

View File

@ -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);
}
}, [

View File

@ -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]);

View File

@ -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={
[

View File

@ -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 = {

View File

@ -96,7 +96,7 @@ export const INITIAL_INPUT_STATE = {
browserHeight: 0,
},
genericComponent: {
hasGenericComponent: false,
genericComponentId: undefined,
width: 0,
height: 0,
browserWidth: 0,

View File

@ -366,7 +366,7 @@ const CamerasOnlyLayout = (props) => {
hasExternalVideo: false,
},
genericComponent: {
hasGenericComponent: false,
genericComponentId: undefined,
},
screenShare: {
hasScreenShare: false,

View File

@ -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;

View File

@ -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;

View File

@ -410,7 +410,7 @@ const ParticipantsAndChatOnlyLayout = (props) => {
height: 0,
},
genericComponent: {
hasGenericComponent: false,
genericComponentId: undefined,
width: 0,
height: 0,
},

View File

@ -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;

View File

@ -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,

View File

@ -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;

View File

@ -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;

View File

@ -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,
};

View File

@ -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