Merge 2.7 into Develop

This commit is contained in:
Gustavo Trott 2023-06-26 17:21:01 -03:00
commit 5681e88c60
248 changed files with 2812 additions and 1486 deletions

View File

@ -45,6 +45,7 @@ jobs:
- run: ./build/setup.sh bbb-web
- run: ./build/setup.sh bbb-webrtc-sfu
- run: ./build/setup.sh bbb-webrtc-recorder
- run: ./build/setup.sh bbb-transcription-controller
- run: ./build/setup.sh bigbluebutton
- run: tar cvf artifacts.tar artifacts/
- name: Archive packages

View File

@ -12,7 +12,7 @@ stages:
# define which docker image to use for builds
default:
image: bigbluebutton/bbb-build:2023-04-25
image: bigbluebutton/bbb-build:bbb27-2023-06-13-java17
# This stage uses git to find out since when each package has been unmodified.
# it then checks an API endpoint on the package server to find out for which of
@ -51,6 +51,7 @@ get_external_dependencies:
- freeswitch
- bbb-pads
- bbb-playback
- bbb-transcription-controller
expire_in: 1h 30min
# template job for build step
@ -181,6 +182,11 @@ bbb-webrtc-recorder-build:
script:
- build/setup-inside-docker.sh bbb-webrtc-recorder
bbb-transcription-controller-build:
extends: .build_job
script:
- build/setup-inside-docker.sh bbb-transcription-controller
bigbluebutton-build:
extends: .build_job
script:

View File

@ -18,7 +18,7 @@ val compileSettings = Seq(
"-Xlint",
"-Ywarn-dead-code",
"-language:_",
"-target:11",
"-release:17",
"-encoding", "UTF-8"
),
javacOptions ++= List(
@ -76,4 +76,4 @@ daemonGroup in Linux := group
javaOptions in Universal ++= Seq("-J-Xms130m", "-J-Xmx256m", "-Dconfig.file=/etc/bigbluebutton/bbb-apps-akka.conf", "-Dlogback.configurationFile=conf/logback.xml")
debianPackageDependencies in Debian ++= Seq("java11-runtime-headless", "bash")
debianPackageDependencies in Debian ++= Seq("java17-runtime-headless", "bash")

View File

@ -11,11 +11,11 @@ trait UpdateTranscriptPubMsgHdlr {
def handle(msg: UpdateTranscriptPubMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
val meetingId = liveMeeting.props.meetingProp.intId
def broadcastEvent(userId: String, transcriptId: String, transcript: String, locale: String): Unit = {
def broadcastEvent(userId: String, transcriptId: String, transcript: String, locale: String, result: Boolean): Unit = {
val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, meetingId, "nodeJSapp")
val envelope = BbbCoreEnvelope(TranscriptUpdatedEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(TranscriptUpdatedEvtMsg.NAME, meetingId, userId)
val body = TranscriptUpdatedEvtMsgBody(transcriptId, transcript, locale)
val body = TranscriptUpdatedEvtMsgBody(transcriptId, transcript, locale, result)
val event = TranscriptUpdatedEvtMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
@ -67,7 +67,8 @@ trait UpdateTranscriptPubMsgHdlr {
msg.header.userId,
msg.body.transcriptId,
transcript,
msg.body.locale
msg.body.locale,
msg.body.result,
)
}
}

View File

@ -148,10 +148,9 @@ trait MakePresentationDownloadReqMsgHdlr extends RightsManagementTrait {
val exportJob: ExportJob = new ExportJob(jobId, JobTypes.DOWNLOAD, "annotated_slides", presId, presLocation, allPages, pagesRange, meetingId, "");
val storeAnnotationPages: List[PresentationPageForExport] = getPresentationPagesForExport(pagesRange, pageCount, presId, currentPres, liveMeeting);
val annotationCount: Int = storeAnnotationPages.map(_.annotations.size).sum
val isOriginalPresentationType = m.body.typeOfExport == "Original"
if (!isOriginalPresentationType && annotationCount > 0) {
if (!isOriginalPresentationType) {
// Send Export Job to Redis
val job = buildStoreExportJobInRedisSysMsg(exportJob, liveMeeting)
bus.outGW.send(job)
@ -159,9 +158,7 @@ trait MakePresentationDownloadReqMsgHdlr extends RightsManagementTrait {
// Send Annotations to Redis
val annotations = StoredAnnotations(jobId, presId, storeAnnotationPages)
bus.outGW.send(buildStoreAnnotationsInRedisSysMsg(annotations, liveMeeting))
} else if (!isOriginalPresentationType && annotationCount == 0) {
log.error("There are no annotations for presentation with Id {}... Ignoring", presId)
} else if (isOriginalPresentationType) {
} else {
// Return existing uploaded file directly
val convertedFileName = currentPres.get.filenameConverted
val filename = if (convertedFileName == "") currentPres.get.name else convertedFileName

View File

@ -17,11 +17,8 @@ trait GetScreenBroadcastPermissionReqMsgHdlr {
for {
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId)
} yield {
if (props.meetingProp.disabledFeatures.contains("screenshare")) {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "Screen sharing is disabled for this meeting."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)
} else if (permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
if (permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL,
liveMeeting.users2x, msg.header.userId)) {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to share the screen."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)

View File

@ -33,31 +33,41 @@ trait ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr {
ScreenshareModel.isBroadcastingRTMP(liveMeeting.screenshareModel) +
" URL:" + ScreenshareModel.getRTMPBroadcastingUrl(liveMeeting.screenshareModel))
// only valid if not broadcasting yet
if (!ScreenshareModel.isBroadcastingRTMP(liveMeeting.screenshareModel)) {
// Stop external video if it's running
ExternalVideoModel.stop(bus.outGW, liveMeeting)
ScreenshareModel.setRTMPBroadcastingUrl(liveMeeting.screenshareModel, msg.body.stream)
ScreenshareModel.broadcastingRTMPStarted(liveMeeting.screenshareModel)
ScreenshareModel.setScreenshareVideoWidth(liveMeeting.screenshareModel, msg.body.vidWidth)
ScreenshareModel.setScreenshareVideoHeight(liveMeeting.screenshareModel, msg.body.vidHeight)
ScreenshareModel.setVoiceConf(liveMeeting.screenshareModel, msg.body.voiceConf)
ScreenshareModel.setScreenshareConf(liveMeeting.screenshareModel, msg.body.screenshareConf)
ScreenshareModel.setTimestamp(liveMeeting.screenshareModel, msg.body.timestamp)
ScreenshareModel.setHasAudio(liveMeeting.screenshareModel, msg.body.hasAudio)
ScreenshareModel.setContentType(liveMeeting.screenshareModel, msg.body.contentType)
log.info("START broadcast ALLOWED when isBroadcastingRTMP=false")
ScreenshareDAO.insert(liveMeeting.props.meetingProp.intId, liveMeeting.screenshareModel)
// Notify viewers in the meeting that there's an rtmp stream to view
val msgEvent = broadcastEvent(msg.body.voiceConf, msg.body.screenshareConf, msg.body.stream,
msg.body.vidWidth, msg.body.vidHeight, msg.body.timestamp, msg.body.hasAudio, msg.body.contentType)
bus.outGW.send(msgEvent)
if (msg.body.contentType == "camera" && liveMeeting.props.meetingProp.disabledFeatures.contains("cameraAsContent")) {
log.error(
"Camera as a content is disabled for meeting {}, meetingID = {}",
liveMeeting.props.meetingProp.name, liveMeeting.props.meetingProp.intId
)
} else if (msg.body.contentType == "screenshare" && liveMeeting.props.meetingProp.disabledFeatures.contains("screenshare")) {
val meetingId = liveMeeting.props.meetingProp.intId
log.error("Screen sharing is disabled for this meeting, meetingID = {}", meetingId)
} else {
log.info("START broadcast NOT ALLOWED when isBroadcastingRTMP=true")
// only valid if not broadcasting yet
if (!ScreenshareModel.isBroadcastingRTMP(liveMeeting.screenshareModel)) {
// Stop external video if it's running
ExternalVideoModel.stop(bus.outGW, liveMeeting)
ScreenshareModel.setRTMPBroadcastingUrl(liveMeeting.screenshareModel, msg.body.stream)
ScreenshareModel.broadcastingRTMPStarted(liveMeeting.screenshareModel)
ScreenshareModel.setScreenshareVideoWidth(liveMeeting.screenshareModel, msg.body.vidWidth)
ScreenshareModel.setScreenshareVideoHeight(liveMeeting.screenshareModel, msg.body.vidHeight)
ScreenshareModel.setVoiceConf(liveMeeting.screenshareModel, msg.body.voiceConf)
ScreenshareModel.setScreenshareConf(liveMeeting.screenshareModel, msg.body.screenshareConf)
ScreenshareModel.setTimestamp(liveMeeting.screenshareModel, msg.body.timestamp)
ScreenshareModel.setHasAudio(liveMeeting.screenshareModel, msg.body.hasAudio)
ScreenshareModel.setContentType(liveMeeting.screenshareModel, msg.body.contentType)
log.info("START broadcast ALLOWED when isBroadcastingRTMP=false")
ScreenshareDAO.insert(liveMeeting.props.meetingProp.intId, liveMeeting.screenshareModel)
// Notify viewers in the meeting that there's an rtmp stream to view
val msgEvent = broadcastEvent(msg.body.voiceConf, msg.body.screenshareConf, msg.body.stream,
msg.body.vidWidth, msg.body.vidHeight, msg.body.timestamp, msg.body.hasAudio, msg.body.contentType)
bus.outGW.send(msgEvent)
} else {
log.info("START broadcast NOT ALLOWED when isBroadcastingRTMP=true")
}
}
}

View File

@ -30,16 +30,23 @@ trait ActivateTimerReqMsgHdlr extends RightsManagementTrait {
bus.outGW.send(msgEvent)
}
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) &&
permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "You need to be the presenter or moderator to activate timer"
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
val isTimerFeatureEnabled: Boolean = !liveMeeting.props.meetingProp.disabledFeatures.contains("timer")
if (!isTimerFeatureEnabled) {
log.error("Timer feature is disabled for meeting {}, meetingId={}", liveMeeting.props.meetingProp.name,
liveMeeting.props.meetingProp.intId)
} else {
TimerModel.reset(liveMeeting.timerModel)
TimerModel.setIsActive(liveMeeting.timerModel, true)
TimerDAO.update(liveMeeting.props.meetingProp.intId, liveMeeting.timerModel)
broadcastEvent(msg.body.stopwatch, msg.body.running, msg.body.time, msg.body.accumulated, msg.body.track)
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) &&
permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "You need to be the presenter or moderator to activate timer"
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
} else {
TimerModel.reset(liveMeeting.timerModel)
TimerModel.setIsActive(liveMeeting.timerModel, true)
TimerDAO.update(liveMeeting.props.meetingProp.intId, liveMeeting.timerModel)
broadcastEvent(msg.body.stopwatch, msg.body.running, msg.body.time, msg.body.accumulated, msg.body.track)
}
}
}
}

View File

@ -28,4 +28,4 @@ trait TimerEndedPubMsgHdlr extends RightsManagementTrait {
TimerDAO.update(liveMeeting.props.meetingProp.intId, liveMeeting.timerModel)
broadcastEvent()
}
}
}

View File

@ -0,0 +1,50 @@
package org.bigbluebutton.core.apps.users
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.apps.RightsManagementTrait
import org.bigbluebutton.core.models.{ UserState, Users2x }
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
import org.bigbluebutton.core2.message.senders.MsgBuilder
trait ChangeUserAwayReqMsgHdlr extends RightsManagementTrait {
this: UsersApp =>
val liveMeeting: LiveMeeting
val outGW: OutMsgRouter
def handleChangeUserAwayReqMsg(msg: ChangeUserAwayReqMsg): Unit = {
log.info("handleChangeUserAwayReqMsg: away={} userId={}", msg.body.away, msg.body.userId)
def broadcast(user: UserState, away: Boolean): Unit = {
val routingChange = Routing.addMsgToClientRouting(
MessageTypes.BROADCAST_TO_MEETING,
liveMeeting.props.meetingProp.intId, user.intId
)
val envelopeChange = BbbCoreEnvelope(UserAwayChangedEvtMsg.NAME, routingChange)
val headerChange = BbbClientMsgHeader(UserAwayChangedEvtMsg.NAME, liveMeeting.props.meetingProp.intId,
user.intId)
val bodyChange = UserAwayChangedEvtMsgBody(user.intId, away)
val eventChange = UserAwayChangedEvtMsg(headerChange, bodyChange)
val msgEventChange = BbbCommonEnvCoreMsg(envelopeChange, eventChange)
outGW.send(msgEventChange)
}
for {
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId)
newUserState <- Users2x.setUserAway(liveMeeting.users2x, user.intId, msg.body.away)
} yield {
if (msg.body.away && user.emoji == "") {
Users2x.setEmojiStatus(liveMeeting.users2x, msg.body.userId, "away")
outGW.send(MsgBuilder.buildUserEmojiChangedEvtMsg(liveMeeting.props.meetingProp.intId, msg.body.userId, "away"))
}
if (msg.body.away == false && user.emoji == "away") {
Users2x.setEmojiStatus(liveMeeting.users2x, msg.body.userId, "none")
outGW.send(MsgBuilder.buildUserEmojiChangedEvtMsg(liveMeeting.props.meetingProp.intId, msg.body.userId, "none"))
}
broadcast(newUserState, msg.body.away)
}
}
}

View File

@ -4,6 +4,7 @@ import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.models.Users2x
import org.bigbluebutton.core.running.{ BaseMeetingActor, LiveMeeting, OutMsgRouter }
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
import org.bigbluebutton.core2.message.senders.MsgBuilder
trait ChangeUserEmojiCmdMsgHdlr extends RightsManagementTrait {
this: BaseMeetingActor =>
@ -37,7 +38,18 @@ trait ChangeUserEmojiCmdMsgHdlr extends RightsManagementTrait {
for {
uvo <- Users2x.setEmojiStatus(liveMeeting.users2x, msg.body.userId, msg.body.emoji)
} yield {
sendUserEmojiChangedEvtMsg(outGW, liveMeeting.props.meetingProp.intId, msg.body.userId, msg.body.emoji)
outGW.send(MsgBuilder.buildUserEmojiChangedEvtMsg(liveMeeting.props.meetingProp.intId, msg.body.userId, msg.body.emoji))
if (initialEmojiState == "raiseHand" || nextEmojiState == "raiseHand") {
Users2x.setUserRaiseHand(liveMeeting.users2x, msg.body.userId, msg.body.emoji == "raiseHand")
outGW.send(MsgBuilder.buildUserRaiseHandChangedEvtMsg(liveMeeting.props.meetingProp.intId, msg.body.userId, msg.body.emoji == "raiseHand"))
}
if (initialEmojiState == "away" || nextEmojiState == "away") {
Users2x.setUserAway(liveMeeting.users2x, msg.body.userId, msg.body.emoji == "away")
outGW.send(MsgBuilder.buildUserAwayChangedEvtMsg(liveMeeting.props.meetingProp.intId, msg.body.userId, msg.body.emoji == "away"))
}
}
} else {
val meetingId = liveMeeting.props.meetingProp.intId
@ -46,13 +58,4 @@ trait ChangeUserEmojiCmdMsgHdlr extends RightsManagementTrait {
}
}
def sendUserEmojiChangedEvtMsg(outGW: OutMsgRouter, meetingId: String, userId: String, emoji: String): Unit = {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId)
val envelope = BbbCoreEnvelope(UserEmojiChangedEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(UserEmojiChangedEvtMsg.NAME, meetingId, userId)
val body = UserEmojiChangedEvtMsgBody(userId, emoji)
val event = UserEmojiChangedEvtMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
outGW.send(msgEvent)
}
}

View File

@ -0,0 +1,74 @@
package org.bigbluebutton.core.apps.users
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
import org.bigbluebutton.core.models.{ UserState, Users2x }
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
import org.bigbluebutton.core2.message.senders.MsgBuilder
trait ChangeUserRaiseHandReqMsgHdlr extends RightsManagementTrait {
this: UsersApp =>
val liveMeeting: LiveMeeting
val outGW: OutMsgRouter
def handleChangeUserRaiseHandReqMsg(msg: ChangeUserRaiseHandReqMsg): Unit = {
log.info("handleChangeUserRaiseHandReqMsg: raiseHand={} userId={}", msg.body.raiseHand, msg.body.userId)
def broadcast(user: UserState, raiseHand: Boolean): Unit = {
val routingChange = Routing.addMsgToClientRouting(
MessageTypes.BROADCAST_TO_MEETING,
liveMeeting.props.meetingProp.intId, user.intId
)
val envelopeChange = BbbCoreEnvelope(UserRaiseHandChangedEvtMsg.NAME, routingChange)
val headerChange = BbbClientMsgHeader(UserRaiseHandChangedEvtMsg.NAME, liveMeeting.props.meetingProp.intId,
user.intId)
val bodyChange = UserRaiseHandChangedEvtMsgBody(user.intId, raiseHand)
val eventChange = UserRaiseHandChangedEvtMsg(headerChange, bodyChange)
val msgEventChange = BbbCommonEnvCoreMsg(envelopeChange, eventChange)
outGW.send(msgEventChange)
}
val isUserSettingOwnProps = (msg.header.userId == msg.body.userId)
val isUserModerator = !permissionFailed(
PermissionCheck.MOD_LEVEL,
PermissionCheck.VIEWER_LEVEL,
liveMeeting.users2x,
msg.header.userId
)
val isUserPresenter = !permissionFailed(
PermissionCheck.VIEWER_LEVEL,
PermissionCheck.PRESENTER_LEVEL,
liveMeeting.users2x,
msg.header.userId
)
if (isUserSettingOwnProps || isUserModerator || isUserPresenter) {
for {
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId)
newUserState <- Users2x.setUserRaiseHand(liveMeeting.users2x, user.intId, msg.body.raiseHand)
} yield {
if (msg.body.raiseHand && user.emoji == "") {
Users2x.setEmojiStatus(liveMeeting.users2x, msg.body.userId, "raiseHand")
outGW.send(MsgBuilder.buildUserEmojiChangedEvtMsg(liveMeeting.props.meetingProp.intId, msg.body.userId, "raiseHand"))
}
if (msg.body.raiseHand == false && user.emoji == "raiseHand") {
Users2x.setEmojiStatus(liveMeeting.users2x, msg.body.userId, "none")
outGW.send(MsgBuilder.buildUserEmojiChangedEvtMsg(liveMeeting.props.meetingProp.intId, msg.body.userId, "none"))
}
broadcast(newUserState, msg.body.raiseHand)
}
} else {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to change user raiseHand prop."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)
}
}
}

View File

@ -0,0 +1,41 @@
package org.bigbluebutton.core.apps.users
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.apps.RightsManagementTrait
import org.bigbluebutton.core.models.{ UserState, Users2x }
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
trait ChangeUserReactionEmojiReqMsgHdlr extends RightsManagementTrait {
this: UsersApp =>
val liveMeeting: LiveMeeting
val outGW: OutMsgRouter
def handleChangeUserReactionEmojiReqMsg(msg: ChangeUserReactionEmojiReqMsg): Unit = {
log.info("handleChangeUserReactionEmojiReqMsg: reactionEmoji={} userId={}", msg.body.reactionEmoji, msg.body.userId)
def broadcast(user: UserState, reactionEmoji: String): Unit = {
val routingChange = Routing.addMsgToClientRouting(
MessageTypes.BROADCAST_TO_MEETING,
liveMeeting.props.meetingProp.intId, user.intId
)
val envelopeChange = BbbCoreEnvelope(UserReactionEmojiChangedEvtMsg.NAME, routingChange)
val headerChange = BbbClientMsgHeader(UserReactionEmojiChangedEvtMsg.NAME, liveMeeting.props.meetingProp.intId,
user.intId)
val bodyChange = UserReactionEmojiChangedEvtMsgBody(user.intId, reactionEmoji)
val eventChange = UserReactionEmojiChangedEvtMsg(headerChange, bodyChange)
val msgEventChange = BbbCommonEnvCoreMsg(envelopeChange, eventChange)
outGW.send(msgEventChange)
}
for {
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId)
newUserState <- Users2x.setReactionEmoji(liveMeeting.users2x, user.intId, msg.body.reactionEmoji)
} yield {
if (user.reactionEmoji != msg.body.reactionEmoji) {
broadcast(newUserState, msg.body.reactionEmoji)
}
}
}
}

View File

@ -22,9 +22,10 @@ trait ClearAllUsersEmojiCmdMsgHdlr extends RightsManagementTrait {
if (isUserModerator) {
for {
user <- Users2x.findAll(liveMeeting.users2x)
if user.emoji.equals("raiseHand") || user.emoji.equals("away") || user.emoji.equals("notAway")
} yield {
Users2x.setEmojiStatus(liveMeeting.users2x, user.intId, "none")
Users2x.setUserAway(liveMeeting.users2x, user.intId, false)
Users2x.setUserRaiseHand(liveMeeting.users2x, user.intId, false)
}
sendClearedAllUsersEmojiEvtMsg(outGW, liveMeeting.props.meetingProp.intId, msg.header.userId)
} else {

View File

@ -0,0 +1,41 @@
package org.bigbluebutton.core.apps.users
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.models.{ UserState, Users2x }
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
import org.bigbluebutton.core.domain.MeetingState2x
trait SetUserSpeechLocaleMsgHdlr extends RightsManagementTrait {
this: UsersApp =>
val liveMeeting: LiveMeeting
val outGW: OutMsgRouter
def handleSetUserSpeechLocaleReqMsg(msg: SetUserSpeechLocaleReqMsg): Unit = {
log.info("handleSetUserSpeechLocaleReqMsg: locale={} provider={} userId={}", msg.body.locale, msg.body.provider, msg.header.userId)
def broadcastUserSpeechLocaleChanged(user: UserState, locale: String, provider: String): Unit = {
val routingChange = Routing.addMsgToClientRouting(
MessageTypes.BROADCAST_TO_MEETING,
liveMeeting.props.meetingProp.intId, user.intId
)
val envelopeChange = BbbCoreEnvelope(UserSpeechLocaleChangedEvtMsg.NAME, routingChange)
val headerChange = BbbClientMsgHeader(UserSpeechLocaleChangedEvtMsg.NAME, liveMeeting.props.meetingProp.intId, user.intId)
val bodyChange = UserSpeechLocaleChangedEvtMsgBody(locale, provider)
val eventChange = UserSpeechLocaleChangedEvtMsg(headerChange, bodyChange)
val msgEventChange = BbbCommonEnvCoreMsg(envelopeChange, eventChange)
outGW.send(msgEventChange)
}
for {
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId)
} yield {
var changeLocale: Option[UserState] = None;
changeLocale = Users2x.setUserSpeechLocale(liveMeeting.users2x, msg.header.userId, msg.body.locale)
broadcastUserSpeechLocaleChanged(user, msg.body.locale, msg.body.provider)
}
}
}

View File

@ -12,11 +12,8 @@ trait UserReactionTimeExpiredCmdMsgHdlr extends RightsManagementTrait {
def handleUserReactionTimeExpiredCmdMsg(msg: UserReactionTimeExpiredCmdMsg) {
val isNodeUser = msg.header.userId.equals("nodeJSapp")
val currentEmojiState = Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId).get.emoji
if (isNodeUser && (!currentEmojiState.equals("raiseHand") && !currentEmojiState.equals("away") && !currentEmojiState.equals("notAway"))) {
Users2x.setEmojiStatus(liveMeeting.users2x, msg.body.userId, "none")
if (isNodeUser) {
Users2x.setReactionEmoji(liveMeeting.users2x, msg.body.userId, "none")
}
}
}

View File

@ -149,6 +149,7 @@ class UsersApp(
with GetUsersMeetingReqMsgHdlr
with RegisterUserReqMsgHdlr
with ChangeUserRoleCmdMsgHdlr
with SetUserSpeechLocaleMsgHdlr
with SyncGetUsersMeetingRespMsgHdlr
with LogoutAndEndMeetingCmdMsgHdlr
with SetRecordingStatusCmdMsgHdlr
@ -159,6 +160,9 @@ class UsersApp(
with AssignPresenterReqMsgHdlr
with ChangeUserPinStateReqMsgHdlr
with ChangeUserMobileFlagReqMsgHdlr
with ChangeUserReactionEmojiReqMsgHdlr
with ChangeUserRaiseHandReqMsgHdlr
with ChangeUserAwayReqMsgHdlr
with EjectUserFromMeetingCmdMsgHdlr
with EjectUserFromMeetingSysMsgHdlr
with MuteUserCmdMsgHdlr {

View File

@ -48,6 +48,9 @@ trait UserJoinedVoiceConfEvtMsgHdlr extends SystemConfiguration {
authed = true,
guestStatus = GuestStatus.WAIT,
emoji = "none",
reactionEmoji = "none",
raiseHand = false,
away = false,
pin = false,
mobile = false,
presenter = false,

View File

@ -189,11 +189,43 @@ object Users2x {
u <- findWithIntId(users, intId)
} yield {
val newUser = u.modify(_.emoji).setTo(emoji)
users.save(newUser)
UserDAO.update(newUser)
newUser
}
}
def setReactionEmoji(users: Users2x, intId: String, reactionEmoji: String): Option[UserState] = {
for {
u <- findWithIntId(users, intId)
} yield {
val newUser = u.modify(_.reactionEmoji).setTo(reactionEmoji)
.modify(_.reactionChangedOn).setTo(System.currentTimeMillis())
users.save(newUser)
newUser
}
}
def setUserRaiseHand(users: Users2x, intId: String, raiseHand: Boolean): Option[UserState] = {
for {
u <- findWithIntId(users, intId)
} yield {
val newUserState = u.modify(_.away).setTo(raiseHand)
users.save(newUserState)
newUserState
}
}
def setUserAway(users: Users2x, intId: String, away: Boolean): Option[UserState] = {
for {
u <- findWithIntId(users, intId)
} yield {
val newUserState = u.modify(_.away).setTo(away)
users.save(newUserState)
newUserState
}
}
def setUserLocked(users: Users2x, intId: String, locked: Boolean): Option[UserState] = {
for {
@ -216,6 +248,16 @@ object Users2x {
}
}
def setUserSpeechLocale(users: Users2x, intId: String, locale: String): Option[UserState] = {
for {
u <- findWithIntId(users, intId)
} yield {
val newUser = u.modify(_.speechLocale).setTo(locale)
users.save(newUser)
newUser
}
}
def hasPresenter(users: Users2x): Boolean = {
findPresenter(users) match {
case Some(p) => true
@ -377,6 +419,10 @@ case class UserState(
authed: Boolean,
guestStatus: String,
emoji: String,
reactionEmoji: String,
reactionChangedOn: Long = 0,
raiseHand: Boolean,
away: Boolean,
locked: Boolean,
presenter: Boolean,
avatar: String,
@ -386,7 +432,8 @@ case class UserState(
lastInactivityInspect: Long = 0,
clientType: String,
pickExempted: Boolean,
userLeftFlag: UserLeftFlag
userLeftFlag: UserLeftFlag,
speechLocale: String = ""
)
case class UserIdAndName(id: String, name: String)

View File

@ -111,6 +111,8 @@ class ReceivedJsonMsgHandlerActor(
routeGenericMsg[ChangeUserPinStateReqMsg](envelope, jsonNode)
case ChangeUserMobileFlagReqMsg.NAME =>
routeGenericMsg[ChangeUserMobileFlagReqMsg](envelope, jsonNode)
case SetUserSpeechLocaleReqMsg.NAME =>
routeGenericMsg[SetUserSpeechLocaleReqMsg](envelope, jsonNode)
case SelectRandomViewerReqMsg.NAME =>
routeGenericMsg[SelectRandomViewerReqMsg](envelope, jsonNode)
@ -250,8 +252,14 @@ class ReceivedJsonMsgHandlerActor(
case UserLeaveReqMsg.NAME =>
routeGenericMsg[UserLeaveReqMsg](envelope, jsonNode)
case ChangeUserRaiseHandReqMsg.NAME =>
routeGenericMsg[ChangeUserRaiseHandReqMsg](envelope, jsonNode)
case ChangeUserAwayReqMsg.NAME =>
routeGenericMsg[ChangeUserAwayReqMsg](envelope, jsonNode)
case ChangeUserEmojiCmdMsg.NAME =>
routeGenericMsg[ChangeUserEmojiCmdMsg](envelope, jsonNode)
case ChangeUserReactionEmojiReqMsg.NAME =>
routeGenericMsg[ChangeUserReactionEmojiReqMsg](envelope, jsonNode)
case UserReactionTimeExpiredCmdMsg.NAME =>
routeGenericMsg[UserReactionTimeExpiredCmdMsg](envelope, jsonNode)
case ClearAllUsersEmojiCmdMsg.NAME =>

View File

@ -63,6 +63,9 @@ trait HandlerHelpers extends SystemConfiguration {
authed = regUser.authed,
guestStatus = regUser.guestStatus,
emoji = "none",
reactionEmoji = "none",
raiseHand = false,
away = false,
pin = false,
mobile = false,
presenter = false,

View File

@ -390,11 +390,15 @@ class MeetingActor(
updateUserLastActivity(m.body.setBy)
case m: GetRecordingStatusReqMsg => usersApp.handleGetRecordingStatusReqMsg(m)
case m: ChangeUserEmojiCmdMsg => handleChangeUserEmojiCmdMsg(m)
case m: ChangeUserReactionEmojiReqMsg => usersApp.handleChangeUserReactionEmojiReqMsg(m)
case m: ChangeUserRaiseHandReqMsg => usersApp.handleChangeUserRaiseHandReqMsg(m)
case m: ChangeUserAwayReqMsg => usersApp.handleChangeUserAwayReqMsg(m)
case m: UserReactionTimeExpiredCmdMsg => handleUserReactionTimeExpiredCmdMsg(m)
case m: ClearAllUsersEmojiCmdMsg => handleClearAllUsersEmojiCmdMsg(m)
case m: SelectRandomViewerReqMsg => usersApp.handleSelectRandomViewerReqMsg(m)
case m: ChangeUserPinStateReqMsg => usersApp.handleChangeUserPinStateReqMsg(m)
case m: ChangeUserMobileFlagReqMsg => usersApp.handleChangeUserMobileFlagReqMsg(m)
case m: SetUserSpeechLocaleReqMsg => usersApp.handleSetUserSpeechLocaleReqMsg(m)
// Client requested to eject user
case m: EjectUserFromMeetingCmdMsg =>

View File

@ -600,4 +600,32 @@ object MsgBuilder {
BbbCommonEnvCoreMsg(envelope, event)
}
def buildUserEmojiChangedEvtMsg(meetingId: String, userId: String, emoji: String) = {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId)
val envelope = BbbCoreEnvelope(UserEmojiChangedEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(UserEmojiChangedEvtMsg.NAME, meetingId, userId)
val body = UserEmojiChangedEvtMsgBody(userId, emoji)
val event = UserEmojiChangedEvtMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
def buildUserAwayChangedEvtMsg(meetingId: String, userId: String, away: Boolean) = {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId)
val envelope = BbbCoreEnvelope(UserAwayChangedEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(UserAwayChangedEvtMsg.NAME, meetingId, userId)
val body = UserAwayChangedEvtMsgBody(userId, away)
val event = UserAwayChangedEvtMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
def buildUserRaiseHandChangedEvtMsg(meetingId: String, userId: String, raiseHand: Boolean) = {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId)
val envelope = BbbCoreEnvelope(UserRaiseHandChangedEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(UserRaiseHandChangedEvtMsg.NAME, meetingId, userId)
val body = UserRaiseHandChangedEvtMsgBody(userId, raiseHand)
val event = UserRaiseHandChangedEvtMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
}

View File

@ -12,6 +12,9 @@ object UserJoinedMeetingEvtMsgBuilder {
role = userState.role, guest = userState.guest, authed = userState.authed,
guestStatus = userState.guestStatus,
emoji = userState.emoji,
reactionEmoji = userState.reactionEmoji,
raiseHand = userState.raiseHand,
away = userState.away,
pin = userState.pin,
presenter = userState.presenter, locked = userState.locked, avatar = userState.avatar, color = userState.color,
clientType = userState.clientType)

View File

@ -68,7 +68,8 @@ trait FakeTestData {
def createFakeUser(liveMeeting: LiveMeeting, regUser: RegisteredUser): UserState = {
UserState(intId = regUser.id, extId = regUser.externId, name = regUser.name, role = regUser.role, pin = false,
mobile = false, guest = regUser.guest, authed = regUser.authed, guestStatus = regUser.guestStatus,
emoji = "none", locked = false, presenter = false, avatar = regUser.avatarURL, color = "#ff6242", clientType = "unknown",
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))
}

View File

@ -39,6 +39,7 @@ case class User(
answers: Map[String,Vector[String]] = Map(),
talk: Talk = Talk(),
emojis: Vector[Emoji] = Vector(),
reactions: Vector[Emoji] = Vector(),
webcams: Vector[Webcam] = Vector(),
totalOfMessages: Long = 0,
)
@ -140,6 +141,9 @@ class LearningDashboardActor(
case m: UserLeaveReqMsg => handleUserLeaveReqMsg(m)
case m: UserLeftMeetingEvtMsg => handleUserLeftMeetingEvtMsg(m)
case m: UserEmojiChangedEvtMsg => handleUserEmojiChangedEvtMsg(m)
case m: UserAwayChangedEvtMsg => handleUserAwayChangedEvtMsg(m)
case m: UserRaiseHandChangedEvtMsg => handleUserRaiseHandChangedEvtMsg(m)
case m: UserReactionEmojiChangedEvtMsg => handleUserReactionEmojiChangedEvtMsg(m)
case m: UserRoleChangedEvtMsg => handleUserRoleChangedEvtMsg(m)
case m: UserBroadcastCamStartedEvtMsg => handleUserBroadcastCamStartedEvtMsg(m)
case m: UserBroadcastCamStoppedEvtMsg => handleUserBroadcastCamStoppedEvtMsg(m)
@ -350,7 +354,7 @@ class LearningDashboardActor(
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)
user <- findUserByIntId(meeting, msg.body.userId)
} yield {
if (msg.body.emoji != "none") {
if (msg.body.emoji != "none" && msg.body.emoji != "raiseHand" && msg.body.emoji != "away") {
val updatedUser = user.copy(emojis = user.emojis :+ Emoji(msg.body.emoji))
val updatedMeeting = meeting.copy(users = meeting.users + (updatedUser.userKey -> updatedUser))
@ -359,6 +363,48 @@ class LearningDashboardActor(
}
}
private def handleUserRaiseHandChangedEvtMsg(msg: UserRaiseHandChangedEvtMsg): Unit = {
for {
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)
user <- findUserByIntId(meeting, msg.body.userId)
} yield {
if (msg.body.raiseHand) {
val updatedUser = user.copy(emojis = user.emojis :+ Emoji("raiseHand"))
val updatedMeeting = meeting.copy(users = meeting.users + (updatedUser.userKey -> updatedUser))
meetings += (updatedMeeting.intId -> updatedMeeting)
}
}
}
private def handleUserAwayChangedEvtMsg(msg: UserAwayChangedEvtMsg): Unit = {
for {
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)
user <- findUserByIntId(meeting, msg.body.userId)
} yield {
if (msg.body.away) {
val updatedUser = user.copy(emojis = user.emojis :+ Emoji("away"))
val updatedMeeting = meeting.copy(users = meeting.users + (updatedUser.userKey -> updatedUser))
meetings += (updatedMeeting.intId -> updatedMeeting)
}
}
}
private def handleUserReactionEmojiChangedEvtMsg(msg: UserReactionEmojiChangedEvtMsg): Unit = {
for {
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)
user <- findUserByIntId(meeting, msg.body.userId)
} yield {
if (msg.body.reactionEmoji != "none") {
val updatedUser = user.copy(reactions = user.reactions :+ Emoji(msg.body.reactionEmoji))
val updatedMeeting = meeting.copy(users = meeting.users + (updatedUser.userKey -> updatedUser))
meetings += (updatedMeeting.intId -> updatedMeeting)
}
}
}
private def handleUserRoleChangedEvtMsg(msg: UserRoleChangedEvtMsg) {
for {
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)

View File

@ -44,8 +44,9 @@ object TestDataGen {
def createUserFor(liveMeeting: LiveMeeting, regUser: RegisteredUser, presenter: Boolean): UserState = {
val u = UserState(intId = regUser.id, extId = regUser.externId, name = regUser.name, role = regUser.role,
guest = regUser.guest, authed = regUser.authed, guestStatus = regUser.guestStatus,
emoji = "none", locked = false, presenter = false, avatar = regUser.avatarURL, color = "#ff6242",
clientType = "unknown", userLeftFlag = UserLeftFlag(false, 0))
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))
Users2x.add(liveMeeting.users2x, u)
u
}

View File

@ -18,7 +18,7 @@ val compileSettings = Seq(
"-Xlint",
"-Ywarn-dead-code",
"-language:_",
"-target:11",
"-release:17",
"-encoding", "UTF-8"
),
javacOptions ++= List(
@ -77,4 +77,4 @@ daemonGroup in Linux := group
javaOptions in Universal ++= Seq("-J-Xms130m", "-J-Xmx256m", "-Dconfig.file=/etc/bigbluebutton/bbb-fsesl-akka.conf", "-Dlogback.configurationFile=conf/logback.xml")
debianPackageDependencies in Debian ++= Seq("java11-runtime-headless", "bash", "bbb-freeswitch-core")
debianPackageDependencies in Debian ++= Seq("java17-runtime-headless", "bash", "bbb-freeswitch-core")

View File

@ -12,7 +12,7 @@ val compileSettings = Seq(
"-Xlint",
"-Ywarn-dead-code",
"-language:_",
"-target:11",
"-release:17",
"-encoding", "UTF-8"
),
javacOptions ++= List(

View File

@ -9,10 +9,11 @@ case class UpdateTranscriptPubMsgBody(
end: Int,
text: String,
transcript: String,
locale: String
locale: String,
result: Boolean,
)
// Out messages
object TranscriptUpdatedEvtMsg { val NAME = "TranscriptUpdatedEvtMsg" }
case class TranscriptUpdatedEvtMsg(header: BbbClientMsgHeader, body: TranscriptUpdatedEvtMsgBody) extends BbbCoreMsg
case class TranscriptUpdatedEvtMsgBody(transcriptId: String, transcript: String, locale: String)
case class TranscriptUpdatedEvtMsgBody(transcriptId: String, transcript: String, locale: String, result: Boolean)

View File

@ -90,20 +90,23 @@ case class UserJoinedMeetingEvtMsg(
body: UserJoinedMeetingEvtMsgBody
) extends BbbCoreMsg
case class UserJoinedMeetingEvtMsgBody(
intId: String,
extId: String,
name: String,
role: String,
guest: Boolean,
authed: Boolean,
guestStatus: String,
emoji: String,
pin: Boolean,
presenter: Boolean,
locked: Boolean,
avatar: String,
color: String,
clientType: String
intId: String,
extId: String,
name: String,
role: String,
guest: Boolean,
authed: Boolean,
guestStatus: String,
emoji: String,
reactionEmoji: String,
raiseHand: Boolean,
away: Boolean,
pin: Boolean,
presenter: Boolean,
locked: Boolean,
avatar: String,
color: String,
clientType: String
)
/**
@ -206,6 +209,48 @@ object UserEmojiChangedEvtMsg { val NAME = "UserEmojiChangedEvtMsg" }
case class UserEmojiChangedEvtMsg(header: BbbClientMsgHeader, body: UserEmojiChangedEvtMsgBody) extends BbbCoreMsg
case class UserEmojiChangedEvtMsgBody(userId: String, emoji: String)
/**
* Sent from client about a user changing RaiseHand.
*/
object ChangeUserRaiseHandReqMsg { val NAME = "ChangeUserRaiseHandReqMsg" }
case class ChangeUserRaiseHandReqMsg(header: BbbClientMsgHeader, body: ChangeUserRaiseHandReqMsgBody) extends StandardMsg
case class ChangeUserRaiseHandReqMsgBody(userId: String, raiseHand: Boolean)
/**
* Sent to all clients about a user changing RaiseHand.
*/
object UserRaiseHandChangedEvtMsg { val NAME = "UserRaiseHandChangedEvtMsg" }
case class UserRaiseHandChangedEvtMsg(header: BbbClientMsgHeader, body: UserRaiseHandChangedEvtMsgBody) extends BbbCoreMsg
case class UserRaiseHandChangedEvtMsgBody(userId: String, raiseHand: Boolean)
/**
* Sent from client about a user changing Away.
*/
object ChangeUserAwayReqMsg { val NAME = "ChangeUserAwayReqMsg" }
case class ChangeUserAwayReqMsg(header: BbbClientMsgHeader, body: ChangeUserAwayReqMsgBody) extends StandardMsg
case class ChangeUserAwayReqMsgBody(userId: String, away: Boolean)
/**
* Sent to all clients about a user changing Away.
*/
object UserAwayChangedEvtMsg { val NAME = "UserAwayChangedEvtMsg" }
case class UserAwayChangedEvtMsg(header: BbbClientMsgHeader, body: UserAwayChangedEvtMsgBody) extends BbbCoreMsg
case class UserAwayChangedEvtMsgBody(userId: String, away: Boolean)
/**
* Sent from client about a user changing ReactionEmoji.
*/
object ChangeUserReactionEmojiReqMsg { val NAME = "ChangeUserReactionEmojiReqMsg" }
case class ChangeUserReactionEmojiReqMsg(header: BbbClientMsgHeader, body: ChangeUserReactionEmojiReqMsgBody) extends StandardMsg
case class ChangeUserReactionEmojiReqMsgBody(userId: String, reactionEmoji: String)
/**
* Sent to all clients about a user changing ReactionEmoji.
*/
object UserReactionEmojiChangedEvtMsg { val NAME = "UserReactionEmojiChangedEvtMsg" }
case class UserReactionEmojiChangedEvtMsg(header: BbbClientMsgHeader, body: UserReactionEmojiChangedEvtMsgBody) extends BbbCoreMsg
case class UserReactionEmojiChangedEvtMsgBody(userId: String, reactionEmoji: String)
/**
* Sent from meteor about a user reaction's expiration.
*/
@ -465,3 +510,11 @@ case class SelectRandomViewerReqMsgBody(requestedBy: String)
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)
object UserSpeechLocaleChangedEvtMsg { val NAME = "UserSpeechLocaleChangedEvtMsg" }
case class UserSpeechLocaleChangedEvtMsg(header: BbbClientMsgHeader, body: UserSpeechLocaleChangedEvtMsgBody) extends BbbCoreMsg
case class UserSpeechLocaleChangedEvtMsgBody(locale: String, provider: String)

View File

@ -11,7 +11,7 @@ val compileSettings = Seq(
"-Xlint",
"-Ywarn-dead-code",
"-language:_",
"-target:11",
"-release:17",
"-encoding", "UTF-8"
),
javacOptions ++= List(
@ -110,5 +110,7 @@ libraryDependencies ++= Seq(
"org.hibernate" % "hibernate-core" % "5.6.1.Final",
"org.flywaydb" % "flyway-core" % "7.8.2",
"com.zaxxer" % "HikariCP" % "4.0.3",
"commons-validator" % "commons-validator" % "1.7"
"commons-validator" % "commons-validator" % "1.7",
"org.apache.tika" % "tika-core" % "2.8.0",
"org.apache.tika" % "tika-parsers-standard-package" % "2.8.0"
)

View File

@ -23,6 +23,7 @@ object Dependencies {
// Office and document conversion
val apachePoi = "5.1.0"
val nuProcess = "2.0.6"
val tika = "2.8.0"
// Server
val servlet = "4.0.1"
@ -56,6 +57,7 @@ object Dependencies {
val poiXml = "org.apache.poi" % "poi-ooxml" % Versions.apachePoi
val nuProcess = "com.zaxxer" % "nuprocess" % Versions.nuProcess
val tika = "org.apache.tika" % "tika-core" % Versions.tika
val servletApi = "javax.servlet" % "javax.servlet-api" % Versions.servlet
@ -93,6 +95,7 @@ object Dependencies {
Compile.apacheHttpAsync,
Compile.poiXml,
Compile.nuProcess,
Compile.tika,
Compile.servletApi,
Compile.apacheLang,
Compile.apacheIo,

View File

@ -48,7 +48,7 @@ public class LearningDashboardService {
File jsonFile = this.getJsonDataFile(meetingId,learningDashboardAccessToken);
FileOutputStream fileOutput = new FileOutputStream(jsonFile);
fileOutput.write(activityJson.getBytes());
fileOutput.write(activityJson.getBytes("UTF-8"));
fileOutput.close();

View File

@ -11,6 +11,8 @@ public class MimeTypeUtils {
private static final String DOCX = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
private static final String PPT = "application/vnd.ms-powerpoint";
private static final String PPTX = "application/vnd.openxmlformats-officedocument.presentationml.presentation";
private static final String TIKA_MSOFFICE = "application/x-tika-msoffice";
private static final String TIKA_MSOFFICE_X = "application/x-tika-ooxml";
private static final String ODT = "application/vnd.oasis.opendocument.text";
private static final String RTF = "application/rtf";
private static final String TXT = "text/plain";
@ -21,46 +23,45 @@ public class MimeTypeUtils {
private static final String PNG = "image/png";
private static final String SVG = "image/svg+xml";
private static final HashMap<String,String> EXTENSIONS_MIME = new HashMap<String,String>(16) {
private static final HashMap<String, List<String>> EXTENSIONS_MIME = new HashMap<String, List<String>>(16) {
{
// Add all the supported files
put(FileTypeConstants.XLS, XLS);
put(FileTypeConstants.XLSX, XLSX);
put(FileTypeConstants.DOC, DOC);
put(FileTypeConstants.DOCX, DOCX);
put(FileTypeConstants.PPT, PPT);
put(FileTypeConstants.PPTX, PPTX);
put(FileTypeConstants.ODT, ODT);
put(FileTypeConstants.RTF, RTF);
put(FileTypeConstants.TXT, TXT);
put(FileTypeConstants.ODS, ODS);
put(FileTypeConstants.ODP, ODP);
put(FileTypeConstants.PDF, PDF);
put(FileTypeConstants.JPG, JPEG);
put(FileTypeConstants.JPEG, JPEG);
put(FileTypeConstants.PNG, PNG);
put(FileTypeConstants.SVG, SVG);
put(FileTypeConstants.DOC, Arrays.asList(DOC, DOCX, TIKA_MSOFFICE, TIKA_MSOFFICE_X));
put(FileTypeConstants.XLS, Arrays.asList(XLS, XLSX, TIKA_MSOFFICE, TIKA_MSOFFICE_X));
put(FileTypeConstants.PPT, Arrays.asList(PPT, PPTX, TIKA_MSOFFICE, TIKA_MSOFFICE_X));
put(FileTypeConstants.DOCX, Arrays.asList(DOC, DOCX, TIKA_MSOFFICE, TIKA_MSOFFICE_X));
put(FileTypeConstants.PPTX, Arrays.asList(PPT, PPTX, TIKA_MSOFFICE, TIKA_MSOFFICE_X));
put(FileTypeConstants.XLSX, Arrays.asList(XLS, XLSX, TIKA_MSOFFICE, TIKA_MSOFFICE_X));
put(FileTypeConstants.ODT, Arrays.asList(ODT));
put(FileTypeConstants.RTF, Arrays.asList(RTF));
put(FileTypeConstants.TXT, Arrays.asList(TXT));
put(FileTypeConstants.ODS, Arrays.asList(ODS));
put(FileTypeConstants.ODP, Arrays.asList(ODP));
put(FileTypeConstants.PDF, Arrays.asList(PDF));
put(FileTypeConstants.JPG, Arrays.asList(JPEG));
put(FileTypeConstants.JPEG, Arrays.asList(JPEG));
put(FileTypeConstants.PNG, Arrays.asList(PNG));
put(FileTypeConstants.SVG, Arrays.asList(SVG));
}
};
public Boolean extensionMatchMimeType(String mimeType, String finalExtension) {
if(EXTENSIONS_MIME.containsKey(finalExtension.toLowerCase()) &&
EXTENSIONS_MIME.get(finalExtension.toLowerCase()).equalsIgnoreCase(mimeType)) {
return true;
} else if(EXTENSIONS_MIME.containsKey(finalExtension.toLowerCase() + 'x') &&
EXTENSIONS_MIME.get(finalExtension.toLowerCase() + 'x').equalsIgnoreCase(mimeType)) {
//Exception for MS Office files named with old extension but using internally the new mime type
//e.g. a file named with extension `ppt` but has the content of a `pptx`
return true;
}
finalExtension = finalExtension.toLowerCase();
if (EXTENSIONS_MIME.containsKey(finalExtension)) {
for (String validMimeType : EXTENSIONS_MIME.get(finalExtension)) {
if (validMimeType.equalsIgnoreCase(mimeType)) {
return true;
}
}
}
return false;
}
public List<String> getValidMimeTypes() {
List<String> validMimeTypes = Arrays.asList(XLS, XLSX,
DOC, DOCX, PPT, PPTX, ODT, RTF, TXT, ODS, ODP,
PDF, JPEG, PNG, SVG
PDF, JPEG, PNG, SVG, TIKA_MSOFFICE, TIKA_MSOFFICE_X
);
return validMimeTypes;
}

View File

@ -24,11 +24,11 @@ import org.slf4j.LoggerFactory;
import static org.bigbluebutton.presentation.FileTypeConstants.*;
import java.io.BufferedReader;
import org.apache.tika.Tika;
import java.io.File;
import java.io.InputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
import java.util.Collections;
@ -71,7 +71,7 @@ public final class SupportedFileTypes {
}
/*
* Returns if the office file is supported.
* Returns if the Office file is supported.
*/
public static boolean isOfficeFile(String fileExtension) {
return OFFICE_FILE_LIST.contains(fileExtension.toLowerCase());
@ -82,62 +82,27 @@ public final class SupportedFileTypes {
}
/*
* Returns if the iamge file is supported
* Returns if the image file is supported
*/
public static boolean isImageFile(String fileExtension) {
return IMAGE_FILE_LIST.contains(fileExtension.toLowerCase());
}
/*
* It was tested native java methods to detect mimetypes, such as:
* - URLConnection.guessContentTypeFromStream(InputStream is);
* - Files.probeContentType(Path path);
* - FileNameMap fileNameMap.getContentTypeFor(String file.getName());
* - MimetypesFileTypeMap fileTypeMap.getContentType(File file);
* But none of them was as successful as the linux based command
*/
public static String detectMimeType(File pres) {
try {
if (pres == null) throw new NullPointerException("Presentation is null");
if (!pres.isFile()) throw new RuntimeException("Presentation is not a file");
ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command("bash", "-c", "file -b --mime-type \"" + pres.getAbsolutePath() + "\"");
Process process = processBuilder.start();
StringBuilder output = new StringBuilder();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
output.append(line + "\n");
}
int exitVal = process.waitFor();
if (exitVal == 0) {
return output.toString().trim();
} else {
String executedCommand = processBuilder.command().toArray(new String[0])[2];
//Read error stream
BufferedReader stdError = new BufferedReader(new InputStreamReader(process.getErrorStream()));
StringBuilder errorString = new StringBuilder();
while (stdError.ready()) {
errorString.append(stdError.readLine());
if (stdError.ready()) {
errorString.append("\n");
}
}
log.error("Error while executing command '{}': {}", executedCommand, errorString);
if (exitVal == 127) {
// 127 - command not found
// use Java method to detect in this case (based on file name)
return URLConnection.getFileNameMap().getContentTypeFor(pres.getAbsolutePath());
} else {
throw new RuntimeException(errorString.toString());
}
}
} catch (Exception e) {
log.error("Error while executing detectMimeType: {}", e.getMessage());
if (pres == null) {
log.error("Presentation is null");
return "";
}
if (!pres.isFile()) {
log.error("Presentation is not a file");
return "";
}
try (InputStream presStream = new FileInputStream(pres)) {
return (new Tika()).detect(presStream);
} catch (IOException e) {
log.error("Error while executing detectMimeType: {}", e);
}
return "";
@ -146,7 +111,7 @@ public final class SupportedFileTypes {
public static Boolean isPresentationMimeTypeValid(File pres, String fileExtension) {
String mimeType = detectMimeType(pres);
if (mimeType == null || mimeType.equals("")) {
if (mimeType.equals("")) {
return false;
}

View File

@ -58,7 +58,7 @@ public class SlidesGenerationProgressNotifier {
pres.getTemporaryPresentationId(),
pres.getName(),
pres.getAuthzToken(),
"IVALID_MIME_TYPE",
"INVALID_MIME_TYPE",
fileMime,
fileExtension
);

View File

@ -88,6 +88,8 @@ class ReceivedJsonMsgHdlrActor(val msgFromAkkaAppsEventBus: MsgFromAkkaAppsEvent
route[UserRoleChangedEvtMsg](envelope, jsonNode)
case UserLockedInMeetingEvtMsg.NAME =>
route[UserLockedInMeetingEvtMsg](envelope, jsonNode)
case UserSpeechLocaleChangedEvtMsg.NAME =>
route[UserSpeechLocaleChangedEvtMsg](envelope, jsonNode)
case CreateBreakoutRoomSysCmdMsg.NAME =>
route[CreateBreakoutRoomSysCmdMsg](envelope, jsonNode)
case PresentationUploadTokenSysPubMsg.NAME =>

View File

@ -22,8 +22,8 @@
"eslint-config-google": "^0.14.0"
},
"engines": {
"node": "^16.16.0",
"npm": "^8.5.0"
"node": ">=16",
"npm": ">=8.5"
}
},
"node_modules/@eslint/eslintrc": {

View File

@ -13,7 +13,7 @@ val compileSettings = Seq(
"-Xlint",
"-Ywarn-dead-code",
"-language:_",
"-target:11",
"-release:17",
"-encoding", "UTF-8"
),
javacOptions ++= List(

View File

@ -37,7 +37,7 @@ systemctl restart nginx
#wget https://graphql-engine-cdn.hasura.io/server/latest/linux-amd64 -O /usr/local/bin/hasura-graphql-engine
#chmod +x /usr/local/bin/hasura-graphql-engine
git clone --branch v2.27.0 https://github.com/iMDT/hasura-graphql-engine.git
git clone --branch v2.28.0 https://github.com/iMDT/hasura-graphql-engine.git
cat hasura-graphql-engine/hasura-graphql.part-a* > hasura-graphql
rm -rf hasura-graphql-engine/
chmod +x hasura-graphql

View File

@ -0,0 +1 @@
git clone --branch v0.1.0 --depth 1 https://github.com/bigbluebutton/bbb-transcription-controller bbb-transcription-controller

View File

@ -14,6 +14,7 @@
<load module="mod_commands"/>
<load module="mod_conference"/>
<load module="mod_dptools"/>
<load module="mod_audio_fork"/>
<!-- Dialplan Interfaces -->
<load module="mod_dialplan_xml"/>

View File

@ -476,11 +476,15 @@ display_bigbluebutton_status () {
units="$units bbb-rap-starter"
fi
if [ -f /usr/lib/systemd/system/bbb-transcription-controller.service ]; then
units="$units bbb-transcription-controller"
fi
if systemctl list-units --full -all | grep -q $TOMCAT_USER.service; then
TOMCAT_SERVICE=$TOMCAT_USER
fi
line='——————————————————————►'
line='—————————————————————————————►'
for unit in $units; do
status=$(systemctl is-active "$unit")
if [ "$status" = "active" ]; then
@ -1706,7 +1710,9 @@ if [ -n "$HOST" ]; then
sudo yq e -i ".freeswitch.esl_password = \"$ESL_PASSWORD\"" /etc/bigbluebutton/bbb-webrtc-sfu/production.yml
sudo xmlstarlet edit --inplace --update 'configuration/settings//param[@name="password"]/@value' --value $ESL_PASSWORD /opt/freeswitch/etc/freeswitch/autoload_configs/event_socket.conf.xml
if [ -f /usr/local/bigbluebutton/bbb-transcription-controller/config/default.yml ]; then
sudo yq w -i /usr/local/bigbluebutton/bbb-transcription-controller/config/default.yml freeswitch.esl_password "$ESL_PASSWORD"
fi
echo "Restarting BigBlueButton $BIGBLUEBUTTON_RELEASE ..."
stop_bigbluebutton

View File

@ -3,7 +3,7 @@ import RedisPubSub from '/imports/startup/server/redis';
import { extractCredentials } from '/imports/api/common/server/helpers';
import Logger from '/imports/startup/server/logger';
export default function updateTranscript(transcriptId, start, end, text, transcript, locale) {
export default function updateTranscript(transcriptId, start, end, text, transcript, locale, isFinal) {
try {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
@ -19,6 +19,7 @@ export default function updateTranscript(transcriptId, start, end, text, transcr
check(text, String);
check(transcript, String);
check(locale, String);
check(isFinal, Boolean);
// Ignore irrelevant updates
if (start !== -1 && end !== -1) {
@ -29,6 +30,7 @@ export default function updateTranscript(transcriptId, start, end, text, transcr
text,
transcript,
locale,
result: isFinal,
};
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);

View File

@ -12,6 +12,7 @@ import {
filterValidIceCandidates,
analyzeSdp,
logSelectedCandidate,
forceDisableStereo,
} from '/imports/utils/sdpUtils';
import { Tracker } from 'meteor/tracker';
import VoiceCallStates from '/imports/api/voice-call-states';
@ -25,6 +26,7 @@ import {
filterSupportedConstraints,
doGUM,
} from '/imports/api/audio/client/bridge/service';
import SpeechService from '/imports/ui/components/audio/captions/speech/service';
const MEDIA = Meteor.settings.public.media;
const MEDIA_TAG = MEDIA.mediaTag;
@ -708,12 +710,25 @@ class SIPSession {
const target = SIP.UserAgent.makeURI(`sip:${callExtension}@${hostname}`);
const matchConstraints = getAudioConstraints({ deviceId: this.inputDeviceId });
const sessionDescriptionHandlerModifiers = [];
const iceModifiers = [
filterValidIceCandidates.bind(this, this.validIceCandidates),
];
if (!SIPJS_ALLOW_MDNS) iceModifiers.push(stripMDnsCandidates);
// The current Vosk provider does not support stereo when transcribing
// microphone streams, so we need to make sure it is forcefully disabled
// via SDP munging. Having it disabled on server side FS _does not suffice_
// because the stereo parameter is client-mandated (ie replicated in the
// answer)
if (SpeechService.stereoUnsupported()) {
logger.debug({
logCode: 'sipjs_transcription_disable_stereo',
}, 'Transcription provider does not support stereo, forcing stereo=0');
sessionDescriptionHandlerModifiers.push(forceDisableStereo);
}
const inviterOptions = {
sessionDescriptionHandlerOptions: {
constraints: {
@ -724,6 +739,7 @@ class SIPSession {
},
iceGatheringTimeout: ICE_GATHERING_TIMEOUT,
},
sessionDescriptionHandlerModifiers,
sessionDescriptionHandlerModifiersPostICEGathering: iceModifiers,
delegate: {
onSessionDescriptionHandler:

View File

@ -16,7 +16,7 @@ import { initCaptions } from '/imports/api/captions/server/helpers';
import { addAnnotationsStreamer } from '/imports/api/annotations/server/streamer';
import { addCursorStreamer } from '/imports/api/cursor/server/streamer';
import { addExternalVideoStreamer } from '/imports/api/external-videos/server/streamer';
import { addUserReactionsObserver } from '/imports/api/user-reaction/server/helpers';
import addUserReactionsObserver from '/imports/api/user-reaction/server/helpers';
import { LAYOUT_TYPE } from '/imports/ui/components/layout/enums';
const addExternalVideo = async (meetingId) => {

View File

@ -12,8 +12,8 @@ const PAGE_COUNT_EXCEEDED_KEY = 'PAGE_COUNT_EXCEEDED';
const PDF_HAS_BIG_PAGE_KEY = 'PDF_HAS_BIG_PAGE';
const GENERATED_SLIDE_KEY = 'GENERATED_SLIDE';
const FILE_TOO_LARGE_KEY = 'FILE_TOO_LARGE';
const CONVERSION_TIMEOUT_KEY = "CONVERSION_TIMEOUT";
const IVALID_MIME_TYPE_KEY = "IVALID_MIME_TYPE";
const CONVERSION_TIMEOUT_KEY = 'CONVERSION_TIMEOUT';
const INVALID_MIME_TYPE_KEY = 'INVALID_MIME_TYPE';
const NO_CONTENT = '204';
// const GENERATING_THUMBNAIL_KEY = 'GENERATING_THUMBNAIL';
// const GENERATED_THUMBNAIL_KEY = 'GENERATED_THUMBNAIL';
@ -52,7 +52,7 @@ export default async function handlePresentationConversionUpdate({ body }, meeti
statusModifier['conversion.maxFileSize'] = body.maxFileSize;
case UNSUPPORTED_DOCUMENT_KEY:
case OFFICE_DOC_CONVERSION_FAILED_KEY:
case IVALID_MIME_TYPE_KEY:
case INVALID_MIME_TYPE_KEY:
statusModifier['conversion.error'] = true;
statusModifier['conversion.fileMime'] = body.fileMime;
statusModifier['conversion.fileExtension'] = body.fileExtension;

View File

@ -1,6 +1,4 @@
import RedisPubSub from '/imports/startup/server/redis';
import handleSetUserReaction from './handlers/setUserReaction';
import handleClearUsersEmoji from './handlers/clearUsersEmoji';
RedisPubSub.on('UserEmojiChangedEvtMsg', handleSetUserReaction);
RedisPubSub.on('ClearedAllUsersEmojiEvtMsg', handleClearUsersEmoji);
RedisPubSub.on('UserReactionEmojiChangedEvtMsg', handleSetUserReaction);

View File

@ -1,15 +1,7 @@
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import UsersReaction from '/imports/api/users';
import addUserReaction from '../modifiers/addUserReaction';
import addUserEmoji from '../modifiers/addUserEmoji';
export default function handleSetUserReaction({ body }, meetingId) {
const { userId, emoji } = body;
const { userId, reactionEmoji } = body;
if (emoji == 'none' || emoji == 'raiseHand' || emoji == 'away' || emoji == 'notAway') {
addUserEmoji(meetingId, userId, emoji);
} else {
addUserReaction(meetingId, userId, emoji);
}
addUserReaction(meetingId, userId, reactionEmoji);
}

View File

@ -3,19 +3,7 @@ import UserReactions from '/imports/api/user-reaction';
import Logger from '/imports/startup/server/logger';
const expireSeconds = Meteor.settings.public.userReaction.expire;
const expireMilliseconds = expireSeconds * 1000
const addUserReactionsObserver = (meetingId) => {
const meetingUserReactions = UserReactions.find({ meetingId });
return meetingUserReactions.observe({
removed(document) {
const isExpirationTriggeredRemoval = (Date.now() - Date.parse(document.creationDate)) >= expireMilliseconds
if (isExpirationTriggeredRemoval) {
notifyExpiredReaction(meetingId, document.userId);
}
}
})
}
const expireMilliseconds = expireSeconds * 1000;
const notifyExpiredReaction = (meetingId, userId) => {
try {
@ -40,8 +28,18 @@ const notifyExpiredReaction = (meetingId, userId) => {
} catch (err) {
Logger.error(`Exception while invoking method resetUserReaction ${err.stack}`);
}
}
};
export {
addUserReactionsObserver,
};
const addUserReactionsObserver = (meetingId) => {
const meetingUserReactions = UserReactions.find({ meetingId });
return meetingUserReactions.observe({
removed(document) {
const isExpirationTriggeredRemoval = (Date.now() - Date.parse(document.creationDate)) >= expireMilliseconds;
if (isExpirationTriggeredRemoval) {
notifyExpiredReaction(meetingId, document.userId);
}
},
});
};
export default addUserReactionsObserver;

View File

@ -1,8 +1,6 @@
import { Meteor } from 'meteor/meteor';
import setUserReaction from './methods/setUserReaction';
import clearAllUsersEmoji from './methods/clearAllUsersEmoji';
Meteor.methods({
setUserReaction,
clearAllUsersEmoji,
});

View File

@ -4,25 +4,25 @@ import RedisPubSub from '/imports/startup/server/redis';
import Logger from '/imports/startup/server/logger';
import { extractCredentials } from '/imports/api/common/server/helpers';
export default function setUserReaction(emoji, userId = undefined) {
export default function setUserReaction(reactionEmoji, userId = undefined) {
try {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'ChangeUserEmojiCmdMsg';
const EVENT_NAME = 'ChangeUserReactionEmojiReqMsg';
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(emoji, String);
check(reactionEmoji, String);
const payload = {
emoji,
userId: userId ? userId : requesterUserId,
reactionEmoji,
userId: userId || requesterUserId,
};
Logger.verbose('User emoji status updated', {
emoji, requesterUserId, meetingId,
Logger.verbose('User reactionEmoji status updated', {
reactionEmoji, requesterUserId, meetingId,
});
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);

View File

@ -23,6 +23,9 @@ export default async function addUserPersistentData(user) {
waitingForAcceptance: Match.Maybe(Boolean),
guestStatus: String,
emoji: String,
reactionEmoji: String,
raiseHand: Boolean,
away: Boolean,
presenter: Boolean,
locked: Boolean,
avatar: String,

View File

@ -4,17 +4,27 @@ import handleUserJoined from './handlers/userJoined';
import handleUserLeftFlagUpdated from './handlers/userLeftFlagUpdated';
import handleValidateAuthToken from './handlers/validateAuthToken';
import handlePresenterAssigned from './handlers/presenterAssigned';
import handleEmojiStatus from './handlers/emojiStatus';
import handleChangeRole from './handlers/changeRole';
import handleUserPinChanged from './handlers/userPinChanged';
import handleUserInactivityInspect from './handlers/userInactivityInspect';
import handleChangeMobileFlag from '/imports/api/users/server/handlers/changeMobileFlag';
import handleChangeMobileFlag from './handlers/changeMobileFlag';
import handleChangeRaiseHand from './handlers/changeRaiseHand';
import handleAway from './handlers/changeAway';
import handleClearUsersEmoji from './handlers/clearUsersEmoji';
import handleUserSpeechLocaleChanged from './handlers/userSpeechLocaleChanged';
RedisPubSub.on('PresenterAssignedEvtMsg', handlePresenterAssigned);
RedisPubSub.on('UserJoinedMeetingEvtMsg', handleUserJoined);
RedisPubSub.on('UserLeftMeetingEvtMsg', handleRemoveUser);
RedisPubSub.on('ValidateAuthTokenRespMsg', handleValidateAuthToken);
RedisPubSub.on('UserEmojiChangedEvtMsg', handleEmojiStatus);
RedisPubSub.on('ClearedAllUsersEmojiEvtMsg', handleClearUsersEmoji);
RedisPubSub.on('UserRaiseHandChangedEvtMsg', handleChangeRaiseHand);
RedisPubSub.on('UserAwayChangedEvtMsg', handleAway);
RedisPubSub.on('UserRoleChangedEvtMsg', handleChangeRole);
RedisPubSub.on('UserMobileFlagChangedEvtMsg', handleChangeMobileFlag);
RedisPubSub.on('UserLeftFlagUpdatedEvtMsg', handleUserLeftFlagUpdated);
RedisPubSub.on('UserPinStateChangedEvtMsg', handleUserPinChanged);
RedisPubSub.on('UserInactivityInspectMsg', handleUserInactivityInspect);
RedisPubSub.on('UserSpeechLocaleChangedEvtMsg', handleUserSpeechLocaleChanged);

View File

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

View File

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

View File

@ -1,9 +1,9 @@
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import Users from '/imports/api/users';
import sendStatusChatMsg from './sendStatusChatMsg';
export default function handleEmojiStatus(meetingId, userId, emoji) {
export default async function handleEmojiStatus({ body }, meetingId) {
const { userId, emoji } = body;
check(userId, String);
check(emoji, String);
@ -21,11 +21,7 @@ export default function handleEmojiStatus(meetingId, userId, emoji) {
};
try {
// must be called before modifying the users collection, because it
// needs to be consulted in order to know the previous emoji
sendStatusChatMsg(meetingId, userId, emoji);
const numberAffected = Users.update(selector, modifier);
const numberAffected = await Users.updateAsync(selector, modifier);
if (numberAffected) {
Logger.info(`Assigned user emoji status ${emoji} id=${userId} meeting=${meetingId}`);

View File

@ -0,0 +1,32 @@
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import Users from '/imports/api/users';
export default async function handleReactionEmoji({ body }, meetingId) {
const { userId, reactionEmoji } = body;
aaa
check(userId, String);
check(reactionEmoji, String);
const selector = {
meetingId,
userId,
};
const modifier = {
$set: {
reactionEmojiTime: (new Date()).getTime(),
reactionEmoji,
},
};
try {
const numberAffected = await Users.updateAsync(selector, modifier);
if (numberAffected) {
Logger.info(`Assigned user rectionEmoji ${reactionEmoji} id=${userId} meeting=${meetingId}`);
}
} catch (err) {
Logger.error(`Assigning user reactionEmoji: ${err}`);
}
}

View File

@ -0,0 +1,13 @@
import { check } from 'meteor/check';
import updateSpeechLocale from '../modifiers/updateSpeechLocale';
export default function handleUserSpeechLocaleChanged({ body, header }, meetingId) {
const { locale } = body;
const { userId } = header;
check(meetingId, String);
check(userId, String);
check(locale, String);
return updateSpeechLocale(meetingId, userId, locale);
}

View File

@ -2,6 +2,9 @@ import { Meteor } from 'meteor/meteor';
import validateAuthToken from './methods/validateAuthToken';
import setSpeechLocale from './methods/setSpeechLocale';
import setMobileUser from './methods/setMobileUser';
import setEmojiStatus from './methods/setEmojiStatus';
import changeAway from './methods/changeAway';
import changeRaiseHand from './methods/changeRaiseHand';
import assignPresenter from './methods/assignPresenter';
import changeRole from './methods/changeRole';
import removeUser from './methods/removeUser';
@ -12,10 +15,15 @@ import userLeftMeeting from './methods/userLeftMeeting';
import changePin from './methods/changePin';
import setRandomUser from './methods/setRandomUser';
import setExitReason from './methods/setExitReason';
import clearAllUsersEmoji from './methods/clearAllUsersEmoji';
Meteor.methods({
setSpeechLocale,
setMobileUser,
setEmojiStatus,
clearAllUsersEmoji,
changeAway,
changeRaiseHand,
assignPresenter,
changeRole,
removeUser,

View File

@ -0,0 +1,31 @@
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import { extractCredentials } from '/imports/api/common/server/helpers';
import RedisPubSub from '/imports/startup/server/redis';
export default async function changeAway(away) {
try {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'ChangeUserAwayReqMsg';
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(away, Boolean);
const payload = {
userId: requesterUserId,
away,
};
Logger.verbose('Updated away status for user', {
meetingId, requesterUserId, away,
});
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
} catch (err) {
Logger.error(`Exception while invoking method changeAway ${err.stack}`);
}
}

View File

@ -0,0 +1,31 @@
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import { extractCredentials } from '/imports/api/common/server/helpers';
import RedisPubSub from '/imports/startup/server/redis';
export default async function changeRaiseHand(raiseHand, userId = undefined) {
try {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'ChangeUserRaiseHandReqMsg';
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(raiseHand, Boolean);
const payload = {
userId: userId || requesterUserId,
raiseHand,
};
Logger.verbose('Updated raiseHand status for user', {
meetingId, requesterUserId, raiseHand,
});
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
} catch (err) {
Logger.error(`Exception while invoking method changeRaiseHand ${err.stack}`);
}
}

View File

@ -5,13 +5,12 @@ import Users from '/imports/api/users';
import addSystemMsg from '/imports/api/group-chat-msg/server/modifiers/addSystemMsg';
const ROLE_VIEWER = Meteor.settings.public.user.role_viewer;
const CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id;
const CHAT_USER_STATUS_MESSAGE = CHAT_CONFIG.system_messages_keys.chat_status_message;
const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system;
export default function sendStatusChatMsg(meetingId, userId, emoji) {
export default function sendAwayStatusChatMsg(meetingId, userId, newAwayStatus) {
const user = Users.findOne(
{ meetingId, userId },
{
@ -19,7 +18,7 @@ export default function sendStatusChatMsg(meetingId, userId, emoji) {
name: 1,
role: 1,
locked: 1,
emoji: 1,
away: 1,
},
},
);
@ -44,9 +43,9 @@ export default function sendStatusChatMsg(meetingId, userId, emoji) {
// Send message if previous emoji or actual emoji is 'away'
let status;
if (user.emoji === 'away') {
if (user.away && !newAwayStatus) {
status = 'notAway';
} else if (emoji === 'away') {
} else if (!user.away && newAwayStatus) {
status = 'away';
} else {
return null;

View File

@ -0,0 +1,33 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import RedisPubSub from '/imports/startup/server/redis';
import Logger from '/imports/startup/server/logger';
import { extractCredentials } from '/imports/api/common/server/helpers';
export default function setEmojiStatus(userId, status) {
try {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'ChangeUserEmojiCmdMsg';
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(userId, String);
check(status, String);
const payload = {
emoji: status,
userId,
};
Logger.verbose('User emoji status updated', {
userId, status, requesterUserId, meetingId,
});
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
} catch (err) {
Logger.error(`Exception while invoking method setEmojiStatus ${err.stack}`);
}
}

View File

@ -1,20 +1,30 @@
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import updateSpeechLocale from '../modifiers/updateSpeechLocale';
import RedisPubSub from '/imports/startup/server/redis';
import { extractCredentials } from '/imports/api/common/server/helpers';
const LANGUAGES = Meteor.settings.public.app.audioCaptions.language.available;
export default async function setSpeechLocale(locale) {
export default function setSpeechLocale(locale, provider) {
try {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'SetUserSpeechLocaleReqMsg';
check(meetingId, String);
check(requesterUserId, String);
check(locale, String);
check(provider, String);
const payload = {
locale,
provider: provider !== 'webspeech' ? provider : '',
};
if (LANGUAGES.includes(locale) || locale === '') {
await updateSpeechLocale(meetingId, requesterUserId, locale);
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
}
} catch (err) {
Logger.error(`Exception while invoking method setSpeechLocale ${err.stack}`);

View File

@ -24,6 +24,9 @@ export default async function addUser(meetingId, userData) {
waitingForAcceptance: Match.Maybe(Boolean),
guestStatus: String,
emoji: String,
reactionEmoji: String,
raiseHand: Boolean,
away: Boolean,
presenter: Boolean,
locked: Boolean,
avatar: String,

View File

@ -0,0 +1,30 @@
import Logger from '/imports/startup/server/logger';
import Users from '/imports/api/users';
import sendAwayStatusChatMsg from '../methods/sendAwayStatusChatMsg';
export default async function changeAway(meetingId, userId, away) {
const selector = {
meetingId,
userId,
};
const modifier = {
$set: {
away,
awayTime: away ? (new Date()).getTime() : 0,
},
};
try {
// must be called before modifying the users collection, because it
// needs to be consulted in order to know the previous emoji
sendAwayStatusChatMsg(meetingId, userId, away);
const numberAffected = await Users.updateAsync(selector, modifier);
if (numberAffected) {
Logger.info(`Assigned away=${away} user id=${userId} meeting=${meetingId}`);
}
} catch (err) {
Logger.error(`Assigning away user: ${err}`);
}
}

View File

@ -0,0 +1,26 @@
import Logger from '/imports/startup/server/logger';
import Users from '/imports/api/users';
export default async function changeRaiseHand(meetingId, userId, raiseHand) {
const selector = {
meetingId,
userId,
};
const modifier = {
$set: {
raiseHand,
raiseHandTime: raiseHand ? (new Date()).getTime() : 0,
},
};
try {
const numberAffected = await Users.updateAsync(selector, modifier);
if (numberAffected) {
Logger.info(`Assigned raiseHand=${raiseHand} user id=${userId} meeting=${meetingId}`);
}
} catch (err) {
Logger.error(`Assigning raiseHand user: ${err}`);
}
}

View File

@ -12,7 +12,11 @@ export default function clearUsersEmoji(meetingId) {
const numberAffected = Users.update(selector, {
$set: {
emojiTime: (new Date()).getTime(),
emoji: "none",
emoji: 'none',
awayTime: 0,
away: false,
raiseHandTime: 0,
raiseHand: false,
},
}, { multi: true });

View File

@ -29,11 +29,6 @@ process.on('uncaughtException', (err) => {
process.exit(1);
});
process.on('uncaughtException', (err) => {
Logger.error(`uncaughtException: ${err}`);
process.exit(1);
});
const formatMemoryUsage = (data) => `${Math.round(data / 1024 / 1024 * 100) / 100} MB`
const serverHealth = () => {

View File

@ -11,11 +11,7 @@ import TimerService from '/imports/ui/components/timer/service';
import { colorPrimary } from '/imports/ui/stylesheets/styled-components/palette';
import { PANELS, ACTIONS, LAYOUT_TYPE } from '../../layout/enums';
import { uniqueId } from '/imports/utils/string-utils';
import {
isPresentationEnabled,
isLayoutsEnabled,
isTimerFeatureEnabled,
} from '/imports/ui/services/features';
import { isPresentationEnabled, isLayoutsEnabled } from '/imports/ui/services/features';
import VideoPreviewContainer from '/imports/ui/components/video-preview/container';
import { screenshareHasEnded } from '/imports/ui/components/screenshare/service';
@ -35,6 +31,8 @@ const propTypes = {
setMeetingLayout: PropTypes.func.isRequired,
setPushLayout: PropTypes.func.isRequired,
showPushLayout: PropTypes.bool.isRequired,
isTimerFeatureEnabled: PropTypes.bool.isRequired,
isCameraAsContentEnabled: PropTypes.bool.isRequired,
};
const defaultProps = {
@ -191,35 +189,30 @@ class ActionsDropdown extends PureComponent {
isMobile,
hasCameraAsContent,
isCameraAsContentEnabled,
isTimerFeatureEnabled,
} = this.props;
const {
pollBtnLabel,
presentationLabel,
takePresenter,
} = intlMessages;
const { pollBtnLabel, presentationLabel, takePresenter } = intlMessages;
const {
formatMessage,
} = intl;
const { formatMessage } = intl;
const actions = [];
if (amIPresenter && isPresentationEnabled()) {
actions.push({
icon: "upload",
dataTest: "managePresentations",
icon: 'upload',
dataTest: 'managePresentations',
label: formatMessage(presentationLabel),
key: this.presentationItemId,
onClick: handlePresentationClick,
dividerTop: this.props?.presentations?.length > 1 ? true : false,
})
});
}
if (amIPresenter && isPollingEnabled) {
actions.push({
icon: "polling",
dataTest: "polling",
icon: 'polling',
dataTest: 'polling',
label: formatMessage(pollBtnLabel),
key: this.pollId,
onClick: () => {
@ -236,12 +229,12 @@ class ActionsDropdown extends PureComponent {
});
Session.set('forcePollOpen', true);
},
})
});
}
if (!amIPresenter && amIModerator) {
actions.push({
icon: "presentation",
icon: 'presentation',
label: formatMessage(takePresenter),
key: this.takePresenterId,
onClick: () => handleTakePresenter(),
@ -250,29 +243,31 @@ class ActionsDropdown extends PureComponent {
if (amIPresenter && allowExternalVideo) {
actions.push({
icon: !isSharingVideo ? "external-video" : "external-video_off",
label: !isSharingVideo ? intl.formatMessage(intlMessages.startExternalVideoLabel)
icon: !isSharingVideo ? 'external-video' : 'external-video_off',
label: !isSharingVideo
? intl.formatMessage(intlMessages.startExternalVideoLabel)
: intl.formatMessage(intlMessages.stopExternalVideoLabel),
key: "external-video",
key: 'external-video',
onClick: isSharingVideo ? stopExternalVideoShare : this.handleExternalVideoClick,
dataTest: "shareExternalVideo",
})
dataTest: 'shareExternalVideo',
});
}
if (amIPresenter && isSelectRandomUserEnabled) {
actions.push({
icon: "user",
icon: 'user',
label: intl.formatMessage(intlMessages.selectRandUserLabel),
key: this.selectUserRandId,
onClick: () => this.setRandomUserSelectModalIsOpen(true),
dataTest: "selectRandomUser",
})
dataTest: 'selectRandomUser',
});
}
if (amIModerator && isTimerEnabled && isTimerFeatureEnabled()) {
if (amIModerator && isTimerEnabled && isTimerFeatureEnabled) {
actions.push({
icon: 'time',
label: isTimerActive ? intl.formatMessage(intlMessages.deactivateTimerLabel)
label: isTimerActive
? intl.formatMessage(intlMessages.deactivateTimerLabel)
: intl.formatMessage(intlMessages.activateTimerLabel),
key: this.timerId,
onClick: () => this.handleTimerClick(),
@ -289,7 +284,7 @@ class ActionsDropdown extends PureComponent {
});
}
if (isLayoutsEnabled()){
if (isLayoutsEnabled()) {
actions.push({
icon: 'send',
label: intl.formatMessage(intlMessages.layoutModal),
@ -309,9 +304,9 @@ class ActionsDropdown extends PureComponent {
onClick: hasCameraAsContent
? screenshareHasEnded
: () => {
screenshareHasEnded();
this.setCameraAsContentModalIsOpen(true);
},
screenshareHasEnded();
this.setCameraAsContentModalIsOpen(true);
},
});
}
@ -319,11 +314,7 @@ class ActionsDropdown extends PureComponent {
}
makePresentationItems() {
const {
presentations,
setPresentation,
podIds,
} = this.props;
const { presentations, setPresentation, podIds } = this.props;
if (!podIds || podIds.length < 1) return [];
@ -332,57 +323,60 @@ class ActionsDropdown extends PureComponent {
const { podId } = podIds[0];
const presentationItemElements = presentations
.sort((a, b) => (a.name.localeCompare(b.name)))
.sort((a, b) => a.name.localeCompare(b.name))
.map((p) => {
const customStyles = { color: colorPrimary };
return (
{
customStyles: p.current ? customStyles : null,
icon: "file",
iconRight: p.current ? 'check' : null,
selected: p.current ? true : false,
label: p.name,
description: "uploaded presentation file",
key: `uploaded-presentation-${p.id}`,
onClick: () => {
setPresentation(p.id, podId);
},
}
);
return {
customStyles: p.current ? customStyles : null,
icon: 'file',
iconRight: p.current ? 'check' : null,
selected: p.current ? true : false,
label: p.name,
description: 'uploaded presentation file',
key: `uploaded-presentation-${p.id}`,
onClick: () => {
setPresentation(p.id, podId);
},
};
});
return presentationItemElements;
}
setExternalVideoModalIsOpen(value) {
this.setState({isExternalVideoModalOpen: value});
this.setState({ isExternalVideoModalOpen: value });
}
setRandomUserSelectModalIsOpen(value) {
this.setState({isRandomUserSelectModalOpen: value});
this.setState({ isRandomUserSelectModalOpen: value });
}
setLayoutModalIsOpen(value) {
this.setState({isLayoutModalOpen: value});
this.setState({ isLayoutModalOpen: value });
}
setCameraAsContentModalIsOpen(value) {
this.setState({isCameraAsContentModalOpen: value});
this.setState({ isCameraAsContentModalOpen: value });
}
setPropsToPassModal(value) {
this.setState({propsToPassModal: value});
this.setState({ propsToPassModal: value });
}
setForceOpen(value){
this.setState({forceOpen: value});
setForceOpen(value) {
this.setState({ forceOpen: value });
}
renderModal(isOpen, setIsOpen, priority, Component) {
return isOpen ? <Component
{...{
onRequestClose: () => setIsOpen(false),
priority,
setIsOpen,
isOpen
}}
/> : null
return isOpen ? (
<Component
{...{
onRequestClose: () => setIsOpen(false),
priority,
setIsOpen,
isOpen,
}}
/>
) : null;
}
render() {
@ -408,12 +402,12 @@ class ActionsDropdown extends PureComponent {
const availableActions = this.getAvailableActions();
const availablePresentations = this.makePresentationItems();
const children = availablePresentations.length > 1 && amIPresenter
? availablePresentations.concat(availableActions) : availableActions;
? availablePresentations.concat(availableActions)
: availableActions;
const customStyles = { top: '-1rem' };
if (availableActions.length === 0
|| !isMeteorConnected) {
if (availableActions.length === 0 || !isMeteorConnected) {
return null;
}
@ -438,24 +432,41 @@ class ActionsDropdown extends PureComponent {
}
actions={children}
opts={{
id: "actions-dropdown-menu",
id: 'actions-dropdown-menu',
keepMounted: true,
transitionDuration: 0,
elevation: 3,
getcontentanchorel: null,
fullwidth: "true",
fullwidth: 'true',
anchorOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' },
transformOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' },
}}
/>
{this.renderModal(isExternalVideoModalOpen, this.setExternalVideoModalIsOpen, "low",
ExternalVideoModal)}
{(amIPresenter && isSelectRandomUserEnabled) ? this.renderModal(isRandomUserSelectModalOpen, this.setRandomUserSelectModalIsOpen,
"low", RandomUserSelectContainer) : null }
{this.renderModal(isLayoutModalOpen, this.setLayoutModalIsOpen,
"low", LayoutModalContainer)}
{this.renderModal(isCameraAsContentModalOpen, this.setCameraAsContentModalIsOpen,
'low', () => (
{this.renderModal(
isExternalVideoModalOpen,
this.setExternalVideoModalIsOpen,
'low',
ExternalVideoModal,
)}
{amIPresenter && isSelectRandomUserEnabled
? this.renderModal(
isRandomUserSelectModalOpen,
this.setRandomUserSelectModalIsOpen,
'low',
RandomUserSelectContainer,
)
: null}
{this.renderModal(
isLayoutModalOpen,
this.setLayoutModalIsOpen,
'low',
LayoutModalContainer,
)}
{this.renderModal(
isCameraAsContentModalOpen,
this.setCameraAsContentModalIsOpen,
'low',
() => (
<VideoPreviewContainer
cameraAsContent
amIPresenter
@ -470,7 +481,8 @@ class ActionsDropdown extends PureComponent {
}}
{...propsToPassModal}
/>
))}
)
)}
</>
);
}

View File

@ -6,6 +6,7 @@ import PresentationPodService from '/imports/ui/components/presentation-pod/serv
import ActionsDropdown from './component';
import { layoutSelectInput, layoutDispatch, layoutSelect } from '../../layout/context';
import { SMALL_VIEWPORT_BREAKPOINT } from '../../layout/enums';
import { isCameraAsContentEnabled, isTimerFeatureEnabled } from '/imports/ui/services/features';
const ActionsDropdownContainer = (props) => {
const sidebarContent = layoutSelectInput((i) => i.sidebarContent);
@ -16,27 +17,27 @@ const ActionsDropdownContainer = (props) => {
const isRTL = layoutSelect((i) => i.isRTL);
return (
<ActionsDropdown {...{
layoutContextDispatch,
sidebarContent,
sidebarNavigation,
isMobile,
isRTL,
...props,
}}
<ActionsDropdown
{...{
layoutContextDispatch,
sidebarContent,
sidebarNavigation,
isMobile,
isRTL,
...props,
}}
/>
);
};
const ENABLE_CAMERA_AS_CONTENT = Meteor.settings.public.app.enableCameraAsContent;
export default withTracker(() => {
const presentations = Presentations.find({ 'conversion.done': true }).fetch();
return ({
return {
presentations,
isTimerFeatureEnabled: isTimerFeatureEnabled(),
isDropdownOpen: Session.get('dropdownOpen'),
setPresentation: PresentationUploaderService.setPresentation,
podIds: PresentationPodService.getPresentationPodIds(),
isCameraAsContentEnabled: ENABLE_CAMERA_AS_CONTENT,
});
isCameraAsContentEnabled: isCameraAsContentEnabled(),
};
})(ActionsDropdownContainer);

View File

@ -30,11 +30,13 @@ class ActionsBar extends PureComponent {
}
renderRaiseHand() {
const { isInteractionsButtonEnabled, isRaiseHandButtonEnabled } = this.props;
const {
isInteractionsButtonEnabled, isRaiseHandButtonEnabled, setEmojiStatus, currentUser, intl,
} = this.props;
return (<>
{isInteractionsButtonEnabled ? <InteractionsButtonContainer /> :
isRaiseHandButtonEnabled ? <RaiseHandDropdownContainer {...{setEmojiStatus, currentUser, intl}}/>
{isInteractionsButtonEnabled ? <InteractionsButtonContainer />
: isRaiseHandButtonEnabled ? <RaiseHandDropdownContainer {...{ setEmojiStatus, currentUser, intl }} />
: null}
</>);
}
@ -106,7 +108,7 @@ class ActionsBar extends PureComponent {
{isCaptionsAvailable
? (
<>
<CaptionsButtonContainer {...{ intl,
<CaptionsButtonContainer {...{ intl,
setIsOpen: this.setCaptionsReaderMenuModalIsOpen,}} />
{
isCaptionsReaderMenuModalOpen ? <CaptionsReaderMenuContainer

View File

@ -12,7 +12,13 @@ import Styled from '../styles';
const InteractionsButton = (props) => {
const {
userId, emoji, intl, isMobile, isRTL,
userId,
emoji,
away,
raiseHand,
intl,
isMobile,
isRTL,
} = props;
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
@ -76,12 +82,11 @@ const InteractionsButton = (props) => {
key: 'raise-hand',
dataTest: 'raise-hand',
icon: 'hand',
label:
emoji === 'raiseHand'
? intl.formatMessage(intlMessages.notRaiseHandLabel)
: intl.formatMessage(intlMessages.raiseHandLabel),
label: raiseHand
? intl.formatMessage(intlMessages.notRaiseHandLabel)
: intl.formatMessage(intlMessages.raiseHandLabel),
onClick: () => {
UserListService.setEmojiStatus(userId, emoji === 'raiseHand' ? 'none' : 'raiseHand');
UserListService.setUserRaiseHand(userId, !raiseHand);
},
});
@ -141,11 +146,11 @@ const InteractionsButton = (props) => {
);
const handlePresent = () => {
UserListService.setEmojiStatus(userId, 'none');
UserListService.setUserAway(userId, false);
};
const handleAFK = () => {
UserListService.setEmojiStatus(userId, 'away');
UserListService.setUserAway(userId, true);
};
const buttonStatus = () => (
@ -155,18 +160,18 @@ const InteractionsButton = (props) => {
onClick={() => handlePresent()}
id="btn"
icon="user"
disabled={emoji !== 'away'}
disabled={!away}
size="md"
color={emoji !== 'away' ? 'primary' : 'default'}
color={!away ? 'primary' : 'default'}
/>
<Button
label={intl.formatMessage(intlMessages.awayLabel)}
onClick={() => handleAFK()}
id="btn"
icon="clear_status"
disabled={emoji === 'away'}
disabled={away}
size="md"
color={emoji === 'away' ? 'primary' : 'default'}
color={away ? 'primary' : 'default'}
/>
</Styled.ButtonContainer>
);
@ -176,7 +181,7 @@ const InteractionsButton = (props) => {
return intl.formatMessage(intlMessages.interactionsAdvancedButton);
}
return emoji === 'raiseHand'
return raiseHand
? intl.formatMessage(intlMessages.notRaiseHandLabel)
: intl.formatMessage(intlMessages.raiseHandLabel);
};
@ -187,7 +192,7 @@ const InteractionsButton = (props) => {
}
event.stopPropagation();
UserListService.setEmojiStatus(userId, emoji === 'raiseHand' ? 'none' : 'raiseHand');
UserListService.setUserRaiseHand(userId, !raiseHand);
};
return (
@ -199,10 +204,10 @@ const InteractionsButton = (props) => {
icon="hand"
label={handleButtonLabel()}
description="Interactions"
ghost={emoji !== 'raiseHand'}
ghost={!raiseHand}
onKeyPress={() => {}}
onClick={handleInteractionsButtonClick}
color={emoji === 'raiseHand' ? 'primary' : 'default'}
color={raiseHand ? 'primary' : 'default'}
hideLabel
circle
size="lg"

View File

@ -27,6 +27,8 @@ export default injectIntl(withTracker(() => {
return {
userId: currentUser.userId,
emoji: currentUser.emoji,
away: currentUser.away,
raiseHand: currentUser.raiseHand,
};
})(InteractionsButtonContainer));

View File

@ -27,15 +27,17 @@ const intlMessages = defineMessages({
id: 'app.actionsBar.emojiMenu.noneLabel',
description: 'label for status clearing',
},
raiseHandLabel: {
id: 'app.actionsBar.emojiMenu.raiseHandLabel',
description: 'label for raise hand status',
},
lowerHandLabel: {
id: 'app.actionsBar.emojiMenu.lowerHandLabel',
description: 'label for lower hand',
},
});
class RaiseHandDropdown extends PureComponent {
constructor(props) {
super(props);
this.state = {
isHandRaised: false,
};
}
getAvailableActions() {
const {
@ -43,7 +45,6 @@ class RaiseHandDropdown extends PureComponent {
getEmojiList,
setEmojiStatus,
intl,
currentUser,
} = this.props;
const actions = [];
@ -54,11 +55,6 @@ class RaiseHandDropdown extends PureComponent {
key: s,
label: intl.formatMessage({ id: `app.actionsBar.emojiMenu.${s}Label` }),
onClick: () => {
if (currentUser.emoji === 'raiseHand') {
this.setState({
isHandRaised: true,
});
}
setEmojiStatus(userId, s);
},
icon: getEmojiList[s],
@ -75,26 +71,24 @@ class RaiseHandDropdown extends PureComponent {
shortcuts,
} = this.props;
const {
isHandRaised,
} = this.state;
const label = currentUser.emoji !== 'raiseHand' && currentUser.emoji !== 'none'
? intlMessages.clearStatusLabel
: {id: `app.actionsBar.emojiMenu.${
currentUser.emoji === 'raiseHand'
? 'lowerHandLabel'
: 'raiseHandLabel'
}`,
};
let btnLabel;
let btnEmoji;
if (currentUser.emoji === 'none') {
btnEmoji = 'raiseHand';
btnLabel = intlMessages.raiseHandLabel;
} else if (currentUser.emoji === 'raiseHand') {
btnEmoji = 'none';
btnLabel = intlMessages.lowerHandLabel;
} else {
btnEmoji = 'none';
btnLabel = intlMessages.clearStatusLabel;
}
return (
<Button
icon={EMOJI_STATUSES[currentUser.emoji === 'none'
? 'raiseHand' : currentUser.emoji]}
label={intl.formatMessage(
label,
)}
label={intl.formatMessage(btnLabel)}
accessKey={shortcuts.raisehand}
color={currentUser.emoji !== 'none' ? 'primary' : 'default'}
data-test={currentUser.emoji === 'raiseHand' ? 'lowerHandLabel' : 'raiseHandLabel'}
@ -105,21 +99,7 @@ class RaiseHandDropdown extends PureComponent {
size="lg"
onClick={(e) => {
e.stopPropagation();
if (currentUser.emoji !== 'none'
&& currentUser.emoji !== 'raiseHand') {
setEmojiStatus(
currentUser.userId,
isHandRaised ? 'raiseHand' : 'none',
);
} else {
this.setState({
isHandRaised: false,
});
setEmojiStatus(
currentUser.userId,
currentUser.emoji === 'raiseHand' ? 'none' : 'raiseHand',
);
}
setEmojiStatus(currentUser.userId, btnEmoji);
}}
/>
);

View File

@ -52,7 +52,14 @@ export default {
amIModerator,
isMe,
currentUser: () => Users.findOne({ meetingId: Auth.meetingID, userId: Auth.userID },
{ fields: { userId: 1, emoji: 1 } }),
{
fields: {
userId: 1,
emoji: 1,
away: 1,
raiseHand: 1,
},
}),
meetingName: () => Meetings.findOne({ meetingId: Auth.meetingID },
{ fields: { 'meetingProp.name': 1 } }).meetingProp.name,
users: () => Users.find({

View File

@ -17,7 +17,7 @@ import NotificationsBarContainer from '../notifications-bar/container';
import AudioContainer from '../audio/container';
import ChatAlertContainer from '../chat/alert/container';
import BannerBarContainer from '/imports/ui/components/banner-bar/container';
import StatusNotifier from '/imports/ui/components/status-notifier/container';
import RaiseHandNotifier from '/imports/ui/components/raisehand-notifier/container';
import ManyWebcamsNotifier from '/imports/ui/components/video-provider/many-users-notify/container';
import AudioCaptionsSpeechContainer from '/imports/ui/components/audio/captions/speech/container';
import UploaderContainer from '/imports/ui/components/presentation/presentation-uploader/container';
@ -87,6 +87,14 @@ const intlMessages = defineMessages({
id: 'app.toast.setEmoji.lowerHand',
description: 'toast message for lowered hand notification',
},
away: {
id: 'app.toast.setEmoji.away',
description: 'toast message for set away notification',
},
notAway: {
id: 'app.toast.setEmoji.notAway',
description: 'toast message for remove away notification',
},
meetingMuteOn: {
id: 'app.toast.meetingMuteOn.label',
description: 'message used when meeting has been muted',
@ -223,6 +231,8 @@ class App extends Component {
const {
notify,
currentUserEmoji,
currentUserAway,
currentUserRaiseHand,
intl,
deviceType,
mountRandomUserModal,
@ -237,30 +247,42 @@ class App extends Component {
if (mountRandomUserModal) this.setRandomUserSelectModalIsOpen(true);
if (prevProps.currentUserEmoji.status !== currentUserEmoji.status) {
if (prevProps.currentUserEmoji.status !== currentUserEmoji.status
&& currentUserEmoji.status !== 'raiseHand'
&& currentUserEmoji.status !== 'away'
) {
const formattedEmojiStatus = intl.formatMessage({ id: `app.actionsBar.emojiMenu.${currentUserEmoji.status}Label` })
|| currentUserEmoji.status;
const raisedHand = currentUserEmoji.status === 'raiseHand';
let statusLabel = '';
if (currentUserEmoji.status === 'none') {
statusLabel = prevProps.currentUserEmoji.status === 'raiseHand'
? intl.formatMessage(intlMessages.loweredHand)
: intl.formatMessage(intlMessages.clearedEmoji);
notify(
intl.formatMessage(intlMessages.clearedEmoji),
'info',
'clear_status',
);
} else {
statusLabel = raisedHand
? intl.formatMessage(intlMessages.raisedHand)
: intl.formatMessage(intlMessages.setEmoji, ({ 0: formattedEmojiStatus }));
notify(
intl.formatMessage(intlMessages.setEmoji, ({ 0: formattedEmojiStatus })),
'info',
'user',
);
}
}
notify(
statusLabel,
'info',
currentUserEmoji.status === 'none'
? 'clear_status'
: 'user',
);
if (prevProps.currentUserAway !== currentUserAway) {
if (currentUserAway === true) {
notify(intl.formatMessage(intlMessages.away), 'info', 'user');
} else {
notify(intl.formatMessage(intlMessages.notAway), 'info', 'clear_status');
}
}
if (prevProps.currentUserRaiseHand !== currentUserRaiseHand) {
if (currentUserRaiseHand === true) {
notify(intl.formatMessage(intlMessages.raisedHand), 'info', 'user');
} else {
notify(intl.formatMessage(intlMessages.loweredHand), 'info', 'clear_status');
}
}
if (deviceType === null || prevProps.deviceType !== deviceType) this.throttledDeviceType();
@ -521,7 +543,7 @@ class App extends Component {
setAudioModalIsOpen(value) {
this.setState({isAudioModalOpen: value});
}
setVideoPreviewModalIsOpen(value) {
this.setState({isVideoPreviewModalOpen: value});
}
@ -606,14 +628,14 @@ class App extends Component {
pushAlertEnabled={pushAlertEnabled}
/>
)}
<StatusNotifier status="raiseHand" />
<RaiseHandNotifier />
<ManyWebcamsNotifier />
<PollingContainer />
<PadsSessionsContainer />
{this.renderActionsBar()}
{customStyleUrl ? <link rel="stylesheet" type="text/css" href={customStyleUrl} /> : null}
{customStyle ? <link rel="stylesheet" type="text/css" href={`data:text/css;charset=UTF-8,${encodeURIComponent(customStyle)}`} /> : null}
{isRandomUserSelectModalOpen ? <RandomUserSelectContainer
{isRandomUserSelectModalOpen ? <RandomUserSelectContainer
{...{
onRequestClose: () => this.setRandomUserSelectModalIsOpen(false),
priority: "low",

View File

@ -226,7 +226,7 @@ export default withTracker(() => {
{
fields:
{
approved: 1, emoji: 1, userId: 1, presenter: 1, role: 1,
approved: 1, emoji: 1, raiseHand: 1, away: 1, userId: 1, presenter: 1, role: 1,
},
},
);
@ -288,6 +288,8 @@ export default withTracker(() => {
isPhone: deviceInfo.isPhone,
isRTL: document.documentElement.getAttribute('dir') === 'rtl',
currentUserEmoji: currentUserEmoji(currentUser),
currentUserAway: currentUser.away,
currentUserRaiseHand: currentUser.raiseHand,
randomlySelectedUser,
currentUserId: currentUser?.userId,
isPresenter,

View File

@ -600,7 +600,7 @@ class AudioModal extends Component {
const { content } = this.state;
return (
<span>
<>
{showPermissionsOvelay ? <PermissionsOverlay closeModal={closeModal} /> : null}
<Styled.AudioModal
modalName="AUDIO"
@ -638,7 +638,7 @@ class AudioModal extends Component {
{this.renderContent()}
</Styled.Content>
</Styled.AudioModal>
</span>
</>
);
}
}

View File

@ -13,6 +13,8 @@ import { unique } from 'radash';
const THROTTLE_TIMEOUT = 1000;
const CONFIG = Meteor.settings.public.app.audioCaptions;
const ENABLED = CONFIG.enabled;
const PROVIDER = CONFIG.provider;
const LANGUAGES = CONFIG.language.available;
const VALID_ENVIRONMENT = !deviceInfo.isMobile || CONFIG.mobile;
@ -32,15 +34,17 @@ const setSpeechVoices = () => {
setSpeechVoices();
const getSpeechVoices = () => {
const voices = Session.get('speechVoices') || [];
if (!isWebSpeechApi()) return LANGUAGES;
const voices = Session.get('speechVoices') || [];
return voices.filter((v) => LANGUAGES.includes(v));
};
const setSpeechLocale = (value) => {
const voices = getSpeechVoices();
if (voices.includes(value) || value === '') {
makeCall('setSpeechLocale', value);
makeCall('setSpeechLocale', value, CONFIG.provider);
} else {
logger.error({
logCode: 'captions_speech_locale',
@ -51,7 +55,7 @@ const setSpeechLocale = (value) => {
const useFixedLocale = () => isEnabled() && CONFIG.language.forceLocale;
const initSpeechRecognition = () => {
if (!isEnabled()) return null;
if (!isEnabled() || !isWebSpeechApi()) return null;
if (hasSpeechRecognitionSupport()) {
// Effectivate getVoices
setSpeechVoices();
@ -77,7 +81,7 @@ const initSpeechRecognition = () => {
let prevId = '';
let prevTranscript = '';
const updateTranscript = (id, transcript, locale) => {
const updateTranscript = (id, transcript, locale, isFinal) => {
// If it's a new sentence
if (id !== prevId) {
prevId = id;
@ -98,7 +102,7 @@ const updateTranscript = (id, transcript, locale) => {
// Stores current transcript as previous
prevTranscript = transcript;
makeCall('updateTranscript', id, start, end, text, transcript, locale);
makeCall('updateTranscript', id, start, end, text, transcript, locale, isFinal);
};
const throttledTranscriptUpdate = throttle(updateTranscript, THROTTLE_TIMEOUT, {
@ -107,12 +111,12 @@ const throttledTranscriptUpdate = throttle(updateTranscript, THROTTLE_TIMEOUT, {
});
const updateInterimTranscript = (id, transcript, locale) => {
throttledTranscriptUpdate(id, transcript, locale);
throttledTranscriptUpdate(id, transcript, locale, false);
};
const updateFinalTranscript = (id, transcript, locale) => {
throttledTranscriptUpdate.cancel();
updateTranscript(id, transcript, locale);
updateTranscript(id, transcript, locale, true);
};
const getSpeechLocale = (userId = Auth.userID) => {
@ -129,7 +133,15 @@ const isLocaleValid = (locale) => LANGUAGES.includes(locale);
const isEnabled = () => isLiveTranscriptionEnabled();
const isActive = () => isEnabled() && hasSpeechRecognitionSupport() && hasSpeechLocale();
const isWebSpeechApi = () => PROVIDER === 'webspeech';
const isVosk = () => PROVIDER === 'vosk';
const isWhispering = () => PROVIDER === 'whisper';
const isDeepSpeech = () => PROVIDER === 'deepSpeech'
const isActive = () => isEnabled() && ((isWebSpeechApi() && hasSpeechLocale()) || isVosk() || isWhispering() || isDeepSpeech());
const getStatus = () => {
const active = isActive();
@ -156,6 +168,8 @@ const getLocale = () => {
return locale;
};
const stereoUnsupported = () => isActive() && isVosk() && !!getSpeechLocale();
export default {
LANGUAGES,
hasSpeechRecognitionSupport,
@ -172,4 +186,5 @@ export default {
getStatus,
generateId,
useFixedLocale,
stereoUnsupported,
};

View File

@ -19,6 +19,7 @@ const WriterMenuContainer = (props) => {
return amIModerator && <WriterMenu {...{ layoutContextDispatch, ...props }} />;
};
export default withTracker(() => ({
export default withTracker(({ setIsOpen }) => ({
closeModal: () => setIsOpen(false),
availableLocales: Service.getAvailableLocales(),
}))(WriterMenuContainer);

View File

@ -55,9 +55,9 @@ const intlMessages = defineMessages({
id: 'app.presentationUploader.export.originalLabel',
description: 'Label to identify original presentation exported',
},
annotated: {
id: 'app.presentationUploader.export.withAnnotationsLabel',
description: 'Label to identify annotated presentation exported',
currentState: {
id: 'app.presentationUploader.export.inCurrentStateLabel',
description: 'Label to identify in current state presentation exported',
},
});
@ -337,11 +337,11 @@ const removePackagedClassAttribute = (classnames, attribute) => {
};
const getExportedPresentationString = (fileURI, filename, intl, typeOfExport) => {
const intlTypeOfExport = typeOfExport === 'Original' ? intlMessages.original : intlMessages.annotated;
const intlTypeOfExport = typeOfExport === 'Original' ? intlMessages.original : intlMessages.currentState;
const warningIcon = '<i class="icon-bbb-warning"></i>';
const label = `<span>${intl.formatMessage(intlMessages.download)}</span>`;
const notAccessibleWarning = `<span title="${intl.formatMessage(intlMessages.notAccessibleWarning)}">${warningIcon}</span>`;
const link = `<a aria-label="${intl.formatMessage(intlMessages.notAccessibleWarning)}" href=${fileURI} type="application/pdf" rel="noopener, noreferrer" download>${label}&nbsp;${notAccessibleWarning}</a>`;
const link = `<a aria-label="${intl.formatMessage(intlMessages.notAccessibleWarning)}" href=${fileURI} type="application/pdf" target="_blank" rel="noopener, noreferrer" download>${label}&nbsp;${notAccessibleWarning}</a>`;
const name = `<span>${filename} (${intl.formatMessage(intlTypeOfExport)})</span>`;
return `${name}</br>${link}`;
};

View File

@ -1,7 +1,7 @@
import styled from 'styled-components';
import {
colorWhite,
colorPrimary
colorPrimary,
} from '/imports/ui/stylesheets/styled-components/palette';
import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
import { mdPaddingX } from '/imports/ui/stylesheets/styled-components/general';
@ -52,6 +52,9 @@ const Chat = styled.div`
@media ${smallOnly} {
transform: none !important;
&.no-padding {
padding: 0;
}
}
`;

View File

@ -1,17 +1,25 @@
import React, { useCallback, useEffect } from 'react';
import Styled from './styles';
const BaseModal = (props) => {
const { setIsOpen, modalName, children,
isOpen, onRequestClose, className, overlayClassName,
const BaseModal = (props) => {
const {
setIsOpen,
modalName,
children,
isOpen,
onRequestClose,
className,
overlayClassName,
dataTest,
priority,
} = props;
const closeEventHandler = useCallback (() => {
setIsOpen(false);
} , []);
useEffect( () => {
const closeEventHandler = useCallback(() => {
setIsOpen(false);
}, []);
useEffect(() => {
// Only add event listener if name is specified
if(!modalName) return;
if (!modalName) return;
const closeEventName = `CLOSE_MODAL_${modalName.toUpperCase()}`;
@ -23,18 +31,25 @@ const BaseModal = (props) => {
document.removeEventListener(closeEventName, closeEventHandler);
};
}, []);
const priority = props.priority ? props.priority : "low"
return (<Styled.BaseModal
portalClassName={`modal-${priority}`}
parentSelector={()=>document.querySelector('#modals-container')}
isOpen={isOpen}
onRequestClose={onRequestClose}
className={className}
overlayClassName={overlayClassName}
shouldReturnFocusAfterClose={false}
>
{children}
</Styled.BaseModal>
)}
const priorityValue = priority || 'low';
return (
<Styled.BaseModal
portalClassName={`modal-${priorityValue}`}
parentSelector={() => document.querySelector('#modals-container')}
isOpen={isOpen}
onRequestClose={onRequestClose}
className={className}
overlayClassName={overlayClassName}
shouldReturnFocusAfterClose={false}
data={{
test: dataTest,
}}
{...props}
>
{children}
</Styled.BaseModal>
);
};
export default { BaseModal };

View File

@ -84,9 +84,7 @@ class ModalSimple extends Component {
className={className}
onRequestClose={handleRequestClose}
contentLabel={title || contentLabel}
data={{
test: dataTest ?? null,
}}
dataTest={dataTest}
{...otherProps}
>
<Styled.Header

View File

@ -7,7 +7,6 @@ import { ACTIONS, CAMERADOCK_POSITION, PANELS } from '../enums';
import Storage from '/imports/ui/services/storage/session';
import { defaultsDeep } from '/imports/utils/array-utils';
import { isPresentationEnabled } from '/imports/ui/services/features';
import Draggable from 'react-draggable';
const windowWidth = () => window.document.documentElement.clientWidth;
const windowHeight = () => window.document.documentElement.clientHeight;
@ -78,8 +77,7 @@ const CustomLayout = (props) => {
const calculatesDropAreas = (sidebarNavWidth, sidebarContentWidth, cameraDockBounds) => {
const { height: actionBarHeight } = calculatesActionbarHeight();
const mediaAreaHeight = windowHeight()
- (DEFAULT_VALUES.navBarHeight + actionBarHeight);
const mediaAreaHeight = windowHeight() - (DEFAULT_VALUES.navBarHeight + actionBarHeight);
const mediaAreaWidth = windowWidth() - (sidebarNavWidth + sidebarContentWidth);
const DROP_ZONE_DEFAUL_SIZE = 100;
const dropZones = {};
@ -97,16 +95,13 @@ const CustomLayout = (props) => {
dropZones[CAMERADOCK_POSITION.CONTENT_RIGHT] = {
top: DEFAULT_VALUES.navBarHeight + DROP_ZONE_DEFAUL_SIZE,
left: !isRTL ? windowWidth() - DROP_ZONE_DEFAUL_SIZE : 0,
height: mediaAreaHeight
- (2 * DROP_ZONE_DEFAUL_SIZE),
height: mediaAreaHeight - 2 * DROP_ZONE_DEFAUL_SIZE,
width: DROP_ZONE_DEFAUL_SIZE,
zIndex: cameraDockBounds.zIndex,
};
dropZones[CAMERADOCK_POSITION.CONTENT_BOTTOM] = {
top: DEFAULT_VALUES.navBarHeight
+ mediaAreaHeight
- DROP_ZONE_DEFAUL_SIZE,
top: DEFAULT_VALUES.navBarHeight + mediaAreaHeight - DROP_ZONE_DEFAUL_SIZE,
left: !isRTL ? sidebarSize : null,
right: isRTL ? sidebarSize : null,
width: mediaAreaWidth,
@ -118,8 +113,7 @@ const CustomLayout = (props) => {
top: DEFAULT_VALUES.navBarHeight + DROP_ZONE_DEFAUL_SIZE,
left: !isRTL ? sidebarSize : null,
right: isRTL ? sidebarSize : null,
height: mediaAreaHeight
- (2 * DROP_ZONE_DEFAUL_SIZE),
height: mediaAreaHeight - 2 * DROP_ZONE_DEFAUL_SIZE,
width: DROP_ZONE_DEFAUL_SIZE,
zIndex: cameraDockBounds.zIndex,
};
@ -137,82 +131,90 @@ const CustomLayout = (props) => {
};
const init = () => {
const { sidebarContentPanel } = sidebarContentInput;
if (isMobile) {
layoutContextDispatch({
type: ACTIONS.SET_LAYOUT_INPUT,
value: defaultsDeep({
sidebarNavigation: {
isOpen: false,
sidebarNavPanel: sidebarNavigationInput.sidebarNavPanel,
},
sidebarContent: {
isOpen: false,
sidebarContentPanel: sidebarContentInput.sidebarContentPanel,
},
sidebarContentHorizontalResizer: {
isOpen: false,
},
presentation: {
isOpen: presentationInput.isOpen,
slidesLength: presentationInput.slidesLength,
currentSlide: {
...presentationInput.currentSlide,
value: defaultsDeep(
{
sidebarNavigation: {
isOpen:
input.sidebarNavigation.isOpen || sidebarContentPanel !== PANELS.NONE || false,
sidebarNavPanel: sidebarNavigationInput.sidebarNavPanel,
},
sidebarContent: {
isOpen: sidebarContentPanel !== PANELS.NONE,
sidebarContentPanel: sidebarContentInput.sidebarContentPanel,
},
sidebarContentHorizontalResizer: {
isOpen: false,
},
presentation: {
isOpen: presentationInput.isOpen,
slidesLength: presentationInput.slidesLength,
currentSlide: {
...presentationInput.currentSlide,
},
},
cameraDock: {
numCameras: cameraDockInput.numCameras,
},
externalVideo: {
hasExternalVideo: input.externalVideo.hasExternalVideo,
},
screenShare: {
hasScreenShare: input.screenShare.hasScreenShare,
width: input.screenShare.width,
height: input.screenShare.height,
},
sharedNotes: {
isPinned: sharedNotesInput.isPinned,
},
},
cameraDock: {
numCameras: cameraDockInput.numCameras,
},
externalVideo: {
hasExternalVideo: input.externalVideo.hasExternalVideo,
},
screenShare: {
hasScreenShare: input.screenShare.hasScreenShare,
width: input.screenShare.width,
height: input.screenShare.height,
},
sharedNotes: {
isPinned: sharedNotesInput.isPinned,
},
}, INITIAL_INPUT_STATE),
INITIAL_INPUT_STATE
),
});
} else {
const { sidebarContentPanel } = sidebarContentInput;
layoutContextDispatch({
type: ACTIONS.SET_LAYOUT_INPUT,
value: defaultsDeep({
sidebarNavigation: {
isOpen: input.sidebarNavigation.isOpen || sidebarContentPanel !== PANELS.NONE || false,
},
sidebarContent: {
isOpen: sidebarContentPanel !== PANELS.NONE,
sidebarContentPanel,
},
sidebarContentHorizontalResizer: {
isOpen: false,
},
presentation: {
isOpen: presentationInput.isOpen,
slidesLength: presentationInput.slidesLength,
currentSlide: {
...presentationInput.currentSlide,
value: defaultsDeep(
{
sidebarNavigation: {
isOpen:
input.sidebarNavigation.isOpen || sidebarContentPanel !== PANELS.NONE || false,
},
sidebarContent: {
isOpen: sidebarContentPanel !== PANELS.NONE,
sidebarContentPanel,
},
sidebarContentHorizontalResizer: {
isOpen: false,
},
presentation: {
isOpen: presentationInput.isOpen,
slidesLength: presentationInput.slidesLength,
currentSlide: {
...presentationInput.currentSlide,
},
},
cameraDock: {
numCameras: cameraDockInput.numCameras,
},
externalVideo: {
hasExternalVideo: input.externalVideo.hasExternalVideo,
},
screenShare: {
hasScreenShare: input.screenShare.hasScreenShare,
width: input.screenShare.width,
height: input.screenShare.height,
},
sharedNotes: {
isPinned: sharedNotesInput.isPinned,
},
},
cameraDock: {
numCameras: cameraDockInput.numCameras,
},
externalVideo: {
hasExternalVideo: input.externalVideo.hasExternalVideo,
},
screenShare: {
hasScreenShare: input.screenShare.hasScreenShare,
width: input.screenShare.width,
height: input.screenShare.height,
},
sharedNotes: {
isPinned: sharedNotesInput.isPinned,
},
}, INITIAL_INPUT_STATE),
INITIAL_INPUT_STATE
),
});
}
Session.set('layoutReady', true);
@ -225,16 +227,20 @@ const CustomLayout = (props) => {
const { hasScreenShare } = screenShareInput;
const { isPinned: isSharedNotesPinned } = sharedNotesInput;
const hasPresentation = isPresentationEnabled() && slidesLength !== 0
const isGeneralMediaOff = !hasPresentation && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned;
const hasPresentation = isPresentationEnabled() && slidesLength !== 0;
const isGeneralMediaOff =
!hasPresentation && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned;
let sidebarContentHeight = 0;
if (sidebarContentInput.isOpen) {
if (isMobile) {
sidebarContentHeight = windowHeight() - DEFAULT_VALUES.navBarHeight;
} else if (cameraDockInput.numCameras > 0
&& cameraDockInput.position === CAMERADOCK_POSITION.SIDEBAR_CONTENT_BOTTOM
&& isOpen && !isGeneralMediaOff) {
} else if (
cameraDockInput.numCameras > 0 &&
cameraDockInput.position === CAMERADOCK_POSITION.SIDEBAR_CONTENT_BOTTOM &&
isOpen &&
!isGeneralMediaOff
) {
sidebarContentHeight = windowHeight() - cameraDockHeight;
} else {
sidebarContentHeight = windowHeight();
@ -282,14 +288,13 @@ const CustomLayout = (props) => {
const stoppedResizing = prevIsResizing && !isResizing;
if (stoppedResizing) {
const isCameraTopOrBottom = cameraDockInput.position === CAMERADOCK_POSITION.CONTENT_TOP
|| cameraDockInput.position === CAMERADOCK_POSITION.CONTENT_BOTTOM;
const isCameraTopOrBottom =
cameraDockInput.position === CAMERADOCK_POSITION.CONTENT_TOP ||
cameraDockInput.position === CAMERADOCK_POSITION.CONTENT_BOTTOM;
Storage.setItem('webcamSize', {
width: isCameraTopOrBottom || isCameraSidebar
? lastWidth : cameraDockInput.width,
height: isCameraTopOrBottom || isCameraSidebar
? cameraDockInput.height : lastHeight,
width: isCameraTopOrBottom || isCameraSidebar ? lastWidth : cameraDockInput.width,
height: isCameraTopOrBottom || isCameraSidebar ? cameraDockInput.height : lastHeight,
});
const updatedLastSize = Storage.getItem('webcamSize');
@ -300,14 +305,14 @@ const CustomLayout = (props) => {
if (isCameraTop || isCameraBottom) {
if ((lastHeight === 0 && !isResizing) || (isCameraTop && isMobile)) {
cameraDockHeight = min(
max((mediaAreaBounds.height * 0.2), cameraDockMinHeight),
(mediaAreaBounds.height - cameraDockMinHeight),
max(mediaAreaBounds.height * 0.2, cameraDockMinHeight),
mediaAreaBounds.height - cameraDockMinHeight
);
} else {
const height = isResizing ? cameraDockInput.height : lastHeight;
cameraDockHeight = min(
max(height, cameraDockMinHeight),
(mediaAreaBounds.height - cameraDockMinHeight),
mediaAreaBounds.height - cameraDockMinHeight
);
}
@ -322,7 +327,7 @@ const CustomLayout = (props) => {
cameraDockBounds.maxHeight = mediaAreaBounds.height * 0.8;
if (isCameraBottom) {
cameraDockBounds.top += (mediaAreaBounds.height - cameraDockHeight);
cameraDockBounds.top += mediaAreaBounds.height - cameraDockHeight;
}
return cameraDockBounds;
@ -331,14 +336,14 @@ const CustomLayout = (props) => {
if (isCameraLeft || isCameraRight) {
if (lastWidth === 0 && !isResizing) {
cameraDockWidth = min(
max((mediaAreaBounds.width * 0.2), cameraDockMinWidth),
(mediaAreaBounds.width - cameraDockMinWidth),
max(mediaAreaBounds.width * 0.2, cameraDockMinWidth),
mediaAreaBounds.width - cameraDockMinWidth
);
} else {
const width = isResizing ? cameraDockInput.width : lastWidth;
cameraDockWidth = min(
max(width, cameraDockMinWidth),
(mediaAreaBounds.width - cameraDockMinWidth),
mediaAreaBounds.width - cameraDockMinWidth
);
}
@ -346,9 +351,8 @@ const CustomLayout = (props) => {
cameraDockBounds.minWidth = cameraDockMinWidth;
cameraDockBounds.width = cameraDockWidth;
cameraDockBounds.maxWidth = mediaAreaBounds.width * 0.8;
cameraDockBounds.presenterMaxWidth = mediaAreaBounds.width
- presentationToolbarMinWidth
- camerasMargin;
cameraDockBounds.presenterMaxWidth =
mediaAreaBounds.width - presentationToolbarMinWidth - camerasMargin;
cameraDockBounds.minHeight = cameraDockMinHeight;
cameraDockBounds.height = mediaAreaBounds.height;
cameraDockBounds.maxHeight = mediaAreaBounds.height;
@ -356,12 +360,12 @@ const CustomLayout = (props) => {
cameraDockBounds.height -= 20;
if (isCameraRight) {
const sizeValue = (mediaAreaBounds.left + mediaAreaBounds.width) - cameraDockWidth;
const sizeValue = mediaAreaBounds.left + mediaAreaBounds.width - cameraDockWidth;
cameraDockBounds.left = !isRTL ? sizeValue - camerasMargin : 0;
cameraDockBounds.right = isRTL ? sizeValue + sidebarSize - camerasMargin : null;
} else if (isCameraLeft) {
cameraDockBounds.left = mediaAreaBounds.left + camerasMargin;
cameraDockBounds.right = isRTL ? sidebarSize + (camerasMargin * 2) : null;
cameraDockBounds.right = isRTL ? sidebarSize + camerasMargin * 2 : null;
}
return cameraDockBounds;
@ -370,14 +374,14 @@ const CustomLayout = (props) => {
if (isCameraSidebar) {
if (lastHeight === 0 && !isResizing) {
cameraDockHeight = min(
max((windowHeight() * 0.2), cameraDockMinHeight),
(windowHeight() - cameraDockMinHeight),
max(windowHeight() * 0.2, cameraDockMinHeight),
windowHeight() - cameraDockMinHeight
);
} else {
const height = isResizing ? cameraDockInput.height : lastHeight;
cameraDockHeight = min(
max(height, cameraDockMinHeight),
(windowHeight() - cameraDockMinHeight),
windowHeight() - cameraDockMinHeight
);
}
@ -401,15 +405,16 @@ const CustomLayout = (props) => {
const { isPinned: isSharedNotesPinned } = sharedNotesInput;
const { height: actionBarHeight } = calculatesActionbarHeight();
const mediaAreaHeight = windowHeight()
- (DEFAULT_VALUES.navBarHeight + actionBarHeight + bannerAreaHeight());
const mediaAreaHeight =
windowHeight() - (DEFAULT_VALUES.navBarHeight + actionBarHeight + bannerAreaHeight());
const mediaAreaWidth = windowWidth() - (sidebarNavWidth + sidebarContentWidth);
const mediaBounds = {};
const { element: fullscreenElement } = fullscreen;
const { navBarHeight, camerasMargin } = DEFAULT_VALUES;
const hasPresentation = isPresentationEnabled() && slidesLength !== 0
const isGeneralMediaOff = !hasPresentation && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned;
const hasPresentation = isPresentationEnabled() && slidesLength !== 0;
const isGeneralMediaOff =
!hasPresentation && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned;
if (!isOpen || isGeneralMediaOff) {
mediaBounds.width = 0;
@ -421,7 +426,11 @@ const CustomLayout = (props) => {
return mediaBounds;
}
if (fullscreenElement === 'Presentation' || fullscreenElement === 'Screenshare' || fullscreenElement === 'ExternalVideo') {
if (
fullscreenElement === 'Presentation' ||
fullscreenElement === 'Screenshare' ||
fullscreenElement === 'ExternalVideo'
) {
mediaBounds.width = windowWidth();
mediaBounds.height = windowHeight();
mediaBounds.top = 0;
@ -438,17 +447,18 @@ const CustomLayout = (props) => {
case CAMERADOCK_POSITION.CONTENT_TOP: {
mediaBounds.width = mediaAreaWidth;
mediaBounds.height = mediaAreaHeight - cameraDockBounds.height - camerasMargin;
mediaBounds.top = navBarHeight + cameraDockBounds.height + camerasMargin + bannerAreaHeight();
mediaBounds.top =
navBarHeight + cameraDockBounds.height + camerasMargin + bannerAreaHeight();
mediaBounds.left = !isRTL ? sidebarSize : null;
mediaBounds.right = isRTL ? sidebarSize : null;
break;
}
case CAMERADOCK_POSITION.CONTENT_RIGHT: {
mediaBounds.width = mediaAreaWidth - cameraDockBounds.width - (camerasMargin * 2);
mediaBounds.width = mediaAreaWidth - cameraDockBounds.width - camerasMargin * 2;
mediaBounds.height = mediaAreaHeight;
mediaBounds.top = navBarHeight + bannerAreaHeight();
mediaBounds.left = !isRTL ? sidebarSize : null;
mediaBounds.right = isRTL ? sidebarSize - (camerasMargin * 2) : null;
mediaBounds.right = isRTL ? sidebarSize - camerasMargin * 2 : null;
break;
}
case CAMERADOCK_POSITION.CONTENT_BOTTOM: {
@ -460,11 +470,11 @@ const CustomLayout = (props) => {
break;
}
case CAMERADOCK_POSITION.CONTENT_LEFT: {
mediaBounds.width = mediaAreaWidth - cameraDockBounds.width - (camerasMargin * 2);
mediaBounds.width = mediaAreaWidth - cameraDockBounds.width - camerasMargin * 2;
mediaBounds.height = mediaAreaHeight;
mediaBounds.top = navBarHeight + bannerAreaHeight();
const sizeValue = sidebarNavWidth
+ sidebarContentWidth + mediaAreaWidth - mediaBounds.width;
const sizeValue =
sidebarNavWidth + sidebarContentWidth + mediaAreaWidth - mediaBounds.width;
mediaBounds.left = !isRTL ? sizeValue : null;
mediaBounds.right = isRTL ? sidebarSize : null;
break;
@ -491,7 +501,7 @@ const CustomLayout = (props) => {
}
return mediaBounds;
}
};
const calculatesLayout = () => {
const {
@ -513,16 +523,27 @@ const CustomLayout = (props) => {
const sidebarContentWidth = calculatesSidebarContentWidth();
const sidebarNavBounds = calculatesSidebarNavBounds();
const sidebarContentBounds = calculatesSidebarContentBounds(sidebarNavWidth.width);
const mediaAreaBounds = calculatesMediaAreaBounds(sidebarNavWidth.width, sidebarContentWidth.width);
const mediaAreaBounds = calculatesMediaAreaBounds(
sidebarNavWidth.width,
sidebarContentWidth.width
);
const navbarBounds = calculatesNavbarBounds(mediaAreaBounds);
const actionbarBounds = calculatesActionbarBounds(mediaAreaBounds);
const cameraDockBounds = calculatesCameraDockBounds(
sidebarNavWidth.width, sidebarContentWidth.width, mediaAreaBounds,
sidebarNavWidth.width,
sidebarContentWidth.width,
mediaAreaBounds
);
const dropZoneAreas = calculatesDropAreas(
sidebarNavWidth.width,
sidebarContentWidth.width,
cameraDockBounds
);
const dropZoneAreas = calculatesDropAreas(sidebarNavWidth.width, sidebarContentWidth.width, cameraDockBounds);
const sidebarContentHeight = calculatesSidebarContentHeight(cameraDockBounds.height);
const mediaBounds = calculatesMediaBounds(
sidebarNavWidth.width, sidebarContentWidth.width, cameraDockBounds,
sidebarNavWidth.width,
sidebarContentWidth.width,
cameraDockBounds
);
const sidebarSize = sidebarContentWidth.width + sidebarNavWidth.width;
const { height: actionBarHeight } = calculatesActionbarHeight();
@ -530,7 +551,7 @@ const CustomLayout = (props) => {
let horizontalCameraDiff = 0;
if (cameraPosition === CAMERADOCK_POSITION.CONTENT_LEFT) {
horizontalCameraDiff = cameraDockBounds.width + (camerasMargin * 2);
horizontalCameraDiff = cameraDockBounds.width + camerasMargin * 2;
}
if (cameraPosition === CAMERADOCK_POSITION.CONTENT_RIGHT) {
@ -568,9 +589,9 @@ const CustomLayout = (props) => {
layoutContextDispatch({
type: ACTIONS.SET_CAPTIONS_OUTPUT,
value: {
left: !isRTL ? (sidebarSize + captionsMargin) : null,
right: isRTL ? (sidebarSize + captionsMargin) : null,
maxWidth: mediaAreaBounds.width - (captionsMargin * 2),
left: !isRTL ? sidebarSize + captionsMargin : null,
right: isRTL ? sidebarSize + captionsMargin : null,
maxWidth: mediaAreaBounds.width - captionsMargin * 2,
},
});
@ -655,14 +676,17 @@ const CustomLayout = (props) => {
tabOrder: 4,
isDraggable: !isMobile && !isTablet && presentationInput.isOpen,
resizableEdge: {
top: (input.cameraDock.position === CAMERADOCK_POSITION.CONTENT_BOTTOM)
|| (input.cameraDock.position === CAMERADOCK_POSITION.SIDEBAR_CONTENT_BOTTOM
&& input.sidebarContent.isOpen),
right: (!isRTL && input.cameraDock.position === CAMERADOCK_POSITION.CONTENT_LEFT)
|| (isRTL && input.cameraDock.position === CAMERADOCK_POSITION.CONTENT_RIGHT),
top:
input.cameraDock.position === CAMERADOCK_POSITION.CONTENT_BOTTOM ||
(input.cameraDock.position === CAMERADOCK_POSITION.SIDEBAR_CONTENT_BOTTOM &&
input.sidebarContent.isOpen),
right:
(!isRTL && input.cameraDock.position === CAMERADOCK_POSITION.CONTENT_LEFT) ||
(isRTL && input.cameraDock.position === CAMERADOCK_POSITION.CONTENT_RIGHT),
bottom: input.cameraDock.position === CAMERADOCK_POSITION.CONTENT_TOP,
left: (!isRTL && input.cameraDock.position === CAMERADOCK_POSITION.CONTENT_RIGHT)
|| (isRTL && input.cameraDock.position === CAMERADOCK_POSITION.CONTENT_LEFT),
left:
(!isRTL && input.cameraDock.position === CAMERADOCK_POSITION.CONTENT_RIGHT) ||
(isRTL && input.cameraDock.position === CAMERADOCK_POSITION.CONTENT_LEFT),
},
zIndex: cameraDockBounds.zIndex,
focusedId: input.cameraDock.focusedId,
@ -682,7 +706,7 @@ const CustomLayout = (props) => {
height: mediaBounds.height,
top: mediaBounds.top,
left: mediaBounds.left,
right: isRTL ? (mediaBounds.right + horizontalCameraDiff) : null,
right: isRTL ? mediaBounds.right + horizontalCameraDiff : null,
tabOrder: DEFAULT_VALUES.presentationTabOrder,
isResizable: false,
zIndex: mediaBounds.zIndex,
@ -696,7 +720,7 @@ const CustomLayout = (props) => {
height: mediaBounds.height,
top: mediaBounds.top,
left: mediaBounds.left,
right: isRTL ? (mediaBounds.right + horizontalCameraDiff) : null,
right: isRTL ? mediaBounds.right + horizontalCameraDiff : null,
zIndex: mediaBounds.zIndex,
},
});
@ -708,7 +732,7 @@ const CustomLayout = (props) => {
height: mediaBounds.height,
top: mediaBounds.top,
left: mediaBounds.left,
right: isRTL ? (mediaBounds.right + horizontalCameraDiff) : null,
right: isRTL ? mediaBounds.right + horizontalCameraDiff : null,
},
});
@ -719,7 +743,7 @@ const CustomLayout = (props) => {
height: mediaBounds.height,
top: mediaBounds.top,
left: mediaBounds.left,
right: isRTL ? (mediaBounds.right + horizontalCameraDiff) : null,
right: isRTL ? mediaBounds.right + horizontalCameraDiff : null,
},
});
};
@ -727,4 +751,4 @@ const CustomLayout = (props) => {
return null;
};
export default CustomLayout;
export default CustomLayout;

View File

@ -76,76 +76,82 @@ const PresentationFocusLayout = (props) => {
}, [input, deviceType, isRTL, fontSize, fullscreen]);
const init = () => {
const { sidebarContentPanel } = sidebarContentInput;
if (isMobile) {
layoutContextDispatch({
type: ACTIONS.SET_LAYOUT_INPUT,
value: defaultsDeep({
sidebarNavigation: {
isOpen: false,
sidebarNavPanel: sidebarNavigationInput.sidebarNavPanel,
},
sidebarContent: {
isOpen: false,
sidebarContentPanel: sidebarContentInput.sidebarContentPanel,
},
SidebarContentHorizontalResizer: {
isOpen: false,
},
presentation: {
isOpen: presentationInput.isOpen,
slidesLength: presentationInput.slidesLength,
currentSlide: {
...presentationInput.currentSlide,
value: defaultsDeep(
{
sidebarNavigation: {
isOpen:
input.sidebarNavigation.isOpen || sidebarContentPanel !== PANELS.NONE || false,
},
sidebarContent: {
isOpen: sidebarContentPanel !== PANELS.NONE,
sidebarContentPanel,
},
SidebarContentHorizontalResizer: {
isOpen: false,
},
presentation: {
isOpen: presentationInput.isOpen,
slidesLength: presentationInput.slidesLength,
currentSlide: {
...presentationInput.currentSlide,
},
},
cameraDock: {
numCameras: cameraDockInput.numCameras,
},
externalVideo: {
hasExternalVideo: input.externalVideo.hasExternalVideo,
},
screenShare: {
hasScreenShare: input.screenShare.hasScreenShare,
width: input.screenShare.width,
height: input.screenShare.height,
},
},
cameraDock: {
numCameras: cameraDockInput.numCameras,
},
externalVideo: {
hasExternalVideo: input.externalVideo.hasExternalVideo,
},
screenShare: {
hasScreenShare: input.screenShare.hasScreenShare,
width: input.screenShare.width,
height: input.screenShare.height,
},
}, INITIAL_INPUT_STATE),
INITIAL_INPUT_STATE
),
});
} else {
const { sidebarContentPanel } = sidebarContentInput;
layoutContextDispatch({
type: ACTIONS.SET_LAYOUT_INPUT,
value: defaultsDeep({
sidebarNavigation: {
isOpen: input.sidebarNavigation.isOpen || sidebarContentPanel !== PANELS.NONE || false,
},
sidebarContent: {
isOpen: sidebarContentPanel !== PANELS.NONE,
sidebarContentPanel,
},
SidebarContentHorizontalResizer: {
isOpen: false,
},
presentation: {
isOpen: presentationInput.isOpen,
slidesLength: presentationInput.slidesLength,
currentSlide: {
...presentationInput.currentSlide,
value: defaultsDeep(
{
sidebarNavigation: {
isOpen:
input.sidebarNavigation.isOpen || sidebarContentPanel !== PANELS.NONE || false,
},
sidebarContent: {
isOpen: sidebarContentPanel !== PANELS.NONE,
sidebarContentPanel,
},
SidebarContentHorizontalResizer: {
isOpen: false,
},
presentation: {
isOpen: presentationInput.isOpen,
slidesLength: presentationInput.slidesLength,
currentSlide: {
...presentationInput.currentSlide,
},
},
cameraDock: {
numCameras: cameraDockInput.numCameras,
},
externalVideo: {
hasExternalVideo: input.externalVideo.hasExternalVideo,
},
screenShare: {
hasScreenShare: input.screenShare.hasScreenShare,
width: input.screenShare.width,
height: input.screenShare.height,
},
},
cameraDock: {
numCameras: cameraDockInput.numCameras,
},
externalVideo: {
hasExternalVideo: input.externalVideo.hasExternalVideo,
},
screenShare: {
hasScreenShare: input.screenShare.hasScreenShare,
width: input.screenShare.width,
height: input.screenShare.height,
},
}, INITIAL_INPUT_STATE),
INITIAL_INPUT_STATE
),
});
}
Session.set('layoutReady', true);
@ -158,13 +164,11 @@ const PresentationFocusLayout = (props) => {
const { hasScreenShare } = screenShareInput;
const { isPinned: isSharedNotesPinned } = sharedNotesInput;
const hasPresentation = isPresentationEnabled() && slidesLength !== 0
const isGeneralMediaOff = !hasPresentation && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned;
const hasPresentation = isPresentationEnabled() && slidesLength !== 0;
const isGeneralMediaOff =
!hasPresentation && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned;
const {
navBarHeight,
sidebarContentMinHeight,
} = DEFAULT_VALUES;
const { navBarHeight, sidebarContentMinHeight } = DEFAULT_VALUES;
let height = 0;
let minHeight = 0;
let maxHeight = 0;
@ -175,10 +179,9 @@ const PresentationFocusLayout = (props) => {
maxHeight = height;
} else if (cameraDockInput.numCameras > 0 && isOpen && !isGeneralMediaOff) {
if (sidebarContentInput.height === 0) {
height = (windowHeight() * 0.75) - bannerAreaHeight();
height = windowHeight() * 0.75 - bannerAreaHeight();
} else {
height = min(max(sidebarContentInput.height, sidebarContentMinHeight),
windowHeight());
height = min(max(sidebarContentInput.height, sidebarContentMinHeight), windowHeight());
}
minHeight = windowHeight() * 0.25 - bannerAreaHeight();
maxHeight = windowHeight() * 0.75 - bannerAreaHeight();
@ -200,7 +203,7 @@ const PresentationFocusLayout = (props) => {
mediaAreaBounds,
sidebarNavWidth,
sidebarContentWidth,
sidebarContentHeight,
sidebarContentHeight
) => {
const { baseCameraDockBounds } = props;
const sidebarSize = sidebarNavWidth + sidebarContentWidth;
@ -231,15 +234,16 @@ const PresentationFocusLayout = (props) => {
} else {
if (cameraDockInput.height === 0) {
cameraDockHeight = min(
max((windowHeight() - sidebarContentHeight), cameraDockMinHeight),
(windowHeight() - cameraDockMinHeight),
max(windowHeight() - sidebarContentHeight, cameraDockMinHeight),
windowHeight() - cameraDockMinHeight
);
const bannerAreaDiff = windowHeight() - sidebarContentHeight - cameraDockHeight - bannerAreaHeight();
const bannerAreaDiff =
windowHeight() - sidebarContentHeight - cameraDockHeight - bannerAreaHeight();
cameraDockHeight += bannerAreaDiff;
} else {
cameraDockHeight = min(
max(cameraDockInput.height, cameraDockMinHeight),
(windowHeight() - cameraDockMinHeight),
windowHeight() - cameraDockMinHeight
);
}
cameraDockBounds.top = windowHeight() - cameraDockHeight - bannerAreaHeight();
@ -260,7 +264,11 @@ const PresentationFocusLayout = (props) => {
const mediaBounds = {};
const { element: fullscreenElement } = fullscreen;
if (fullscreenElement === 'Presentation' || fullscreenElement === 'Screenshare' || fullscreenElement === 'ExternalVideo') {
if (
fullscreenElement === 'Presentation' ||
fullscreenElement === 'Screenshare' ||
fullscreenElement === 'ExternalVideo'
) {
mediaBounds.width = windowWidth();
mediaBounds.height = windowHeight();
mediaBounds.top = 0;
@ -304,7 +312,8 @@ const PresentationFocusLayout = (props) => {
const sidebarNavBounds = calculatesSidebarNavBounds();
const sidebarContentBounds = calculatesSidebarContentBounds(sidebarNavWidth.width);
const mediaAreaBounds = calculatesMediaAreaBounds(
sidebarNavWidth.width, sidebarContentWidth.width,
sidebarNavWidth.width,
sidebarContentWidth.width
);
const navbarBounds = calculatesNavbarBounds(mediaAreaBounds);
const actionbarBounds = calculatesActionbarBounds(mediaAreaBounds);
@ -316,7 +325,7 @@ const PresentationFocusLayout = (props) => {
mediaAreaBounds,
sidebarNavWidth.width,
sidebarContentWidth.width,
sidebarContentHeight.height,
sidebarContentHeight.height
);
const { isOpen } = presentationInput;
@ -351,9 +360,9 @@ const PresentationFocusLayout = (props) => {
layoutContextDispatch({
type: ACTIONS.SET_CAPTIONS_OUTPUT,
value: {
left: !isRTL ? (sidebarSize + captionsMargin) : null,
right: isRTL ? (sidebarSize + captionsMargin) : null,
maxWidth: mediaAreaBounds.width - (captionsMargin * 2),
left: !isRTL ? sidebarSize + captionsMargin : null,
right: isRTL ? sidebarSize + captionsMargin : null,
maxWidth: mediaAreaBounds.width - captionsMargin * 2,
},
});
@ -502,4 +511,4 @@ const PresentationFocusLayout = (props) => {
return null;
};
export default PresentationFocusLayout;
export default PresentationFocusLayout;

View File

@ -41,8 +41,10 @@ const SmartLayout = (props) => {
const prevDeviceType = usePrevious(deviceType);
const throttledCalculatesLayout = throttle(() => calculatesLayout(),
50, { trailing: true, leading: true });
const throttledCalculatesLayout = throttle(() => calculatesLayout(), 50, {
trailing: true,
leading: true,
});
useEffect(() => {
window.addEventListener('resize', () => {
@ -69,82 +71,89 @@ const SmartLayout = (props) => {
}, [input, deviceType, isRTL, fontSize, fullscreen]);
const init = () => {
const { sidebarContentPanel } = sidebarContentInput;
if (isMobile) {
layoutContextDispatch({
type: ACTIONS.SET_LAYOUT_INPUT,
value: defaultsDeep({
sidebarNavigation: {
isOpen: false,
sidebarNavPanel: sidebarNavigationInput.sidebarNavPanel,
},
sidebarContent: {
isOpen: false,
sidebarContentPanel: sidebarContentInput.sidebarContentPanel,
},
SidebarContentHorizontalResizer: {
isOpen: false,
},
presentation: {
isOpen: presentationInput.isOpen,
slidesLength: presentationInput.slidesLength,
currentSlide: {
...presentationInput.currentSlide,
value: defaultsDeep(
{
sidebarNavigation: {
isOpen:
input.sidebarNavigation.isOpen || sidebarContentPanel !== PANELS.NONE || false,
},
sidebarContent: {
isOpen: sidebarContentPanel !== PANELS.NONE,
sidebarContentPanel,
},
SidebarContentHorizontalResizer: {
isOpen: false,
},
presentation: {
isOpen: presentationInput.isOpen,
slidesLength: presentationInput.slidesLength,
currentSlide: {
...presentationInput.currentSlide,
},
},
cameraDock: {
numCameras: cameraDockInput.numCameras,
},
externalVideo: {
hasExternalVideo: externalVideoInput.hasExternalVideo,
},
screenShare: {
hasScreenShare: screenShareInput.hasScreenShare,
width: screenShareInput.width,
height: screenShareInput.height,
},
sharedNotes: {
isPinned: sharedNotesInput.isPinned,
},
},
cameraDock: {
numCameras: cameraDockInput.numCameras,
},
externalVideo: {
hasExternalVideo: externalVideoInput.hasExternalVideo,
},
screenShare: {
hasScreenShare: screenShareInput.hasScreenShare,
width: screenShareInput.width,
height: screenShareInput.height,
},
sharedNotes: {
isPinned: sharedNotesInput.isPinned,
},
}, INITIAL_INPUT_STATE),
INITIAL_INPUT_STATE
),
});
} else {
const { sidebarContentPanel } = sidebarContentInput;
layoutContextDispatch({
type: ACTIONS.SET_LAYOUT_INPUT,
value: defaultsDeep({
sidebarNavigation: {
isOpen: input.sidebarNavigation.isOpen || sidebarContentPanel !== PANELS.NONE || false,
},
sidebarContent: {
isOpen: sidebarContentPanel !== PANELS.NONE,
sidebarContentPanel,
},
SidebarContentHorizontalResizer: {
isOpen: false,
},
presentation: {
isOpen: presentationInput.isOpen,
slidesLength: presentationInput.slidesLength,
currentSlide: {
...presentationInput.currentSlide,
value: defaultsDeep(
{
sidebarNavigation: {
isOpen:
input.sidebarNavigation.isOpen || sidebarContentPanel !== PANELS.NONE || false,
},
sidebarContent: {
isOpen: sidebarContentPanel !== PANELS.NONE,
sidebarContentPanel,
},
SidebarContentHorizontalResizer: {
isOpen: false,
},
presentation: {
isOpen: presentationInput.isOpen,
slidesLength: presentationInput.slidesLength,
currentSlide: {
...presentationInput.currentSlide,
},
},
cameraDock: {
numCameras: cameraDockInput.numCameras,
},
externalVideo: {
hasExternalVideo: externalVideoInput.hasExternalVideo,
},
screenShare: {
hasScreenShare: screenShareInput.hasScreenShare,
width: screenShareInput.width,
height: screenShareInput.height,
},
sharedNotes: {
isPinned: sharedNotesInput.isPinned,
},
},
cameraDock: {
numCameras: cameraDockInput.numCameras,
},
externalVideo: {
hasExternalVideo: externalVideoInput.hasExternalVideo,
},
screenShare: {
hasScreenShare: screenShareInput.hasScreenShare,
width: screenShareInput.width,
height: screenShareInput.height,
},
sharedNotes: {
isPinned: sharedNotesInput.isPinned,
},
}, INITIAL_INPUT_STATE),
INITIAL_INPUT_STATE
),
});
}
Session.set('layoutReady', true);
@ -181,13 +190,14 @@ const SmartLayout = (props) => {
cameraDockBounds.isCameraHorizontal = false;
const mediaBoundsWidth = (mediaBounds.width > presentationToolbarMinWidth && !isMobile)
? mediaBounds.width
: presentationToolbarMinWidth;
const mediaBoundsWidth =
mediaBounds.width > presentationToolbarMinWidth && !isMobile
? mediaBounds.width
: presentationToolbarMinWidth;
cameraDockBounds.top = navBarHeight;
cameraDockBounds.left = mediaAreaBounds.left;
cameraDockBounds.right = isRTL ? sidebarSize + (camerasMargin * 2) : null;
cameraDockBounds.right = isRTL ? sidebarSize + camerasMargin * 2 : null;
cameraDockBounds.zIndex = 1;
if (mediaBounds.width < mediaAreaBounds.width) {
@ -197,7 +207,7 @@ const SmartLayout = (props) => {
cameraDockBounds.height = mediaAreaBounds.height;
cameraDockBounds.maxHeight = mediaAreaBounds.height;
cameraDockBounds.left += camerasMargin;
cameraDockBounds.width -= (camerasMargin * 2);
cameraDockBounds.width -= camerasMargin * 2;
cameraDockBounds.isCameraHorizontal = true;
cameraDockBounds.position = CAMERADOCK_POSITION.CONTENT_LEFT;
// button size in vertical position
@ -208,7 +218,7 @@ const SmartLayout = (props) => {
cameraDockBounds.height = mediaAreaBounds.height - mediaBounds.height;
cameraDockBounds.maxHeight = mediaAreaBounds.height * 0.8;
cameraDockBounds.top += camerasMargin;
cameraDockBounds.height -= (camerasMargin * 2);
cameraDockBounds.height -= camerasMargin * 2;
cameraDockBounds.position = CAMERADOCK_POSITION.CONTENT_TOP;
}
@ -265,16 +275,17 @@ const SmartLayout = (props) => {
width: screeShareWidth,
height: screeShareHeight,
};
}
};
const calculatesMediaBounds = (mediaAreaBounds, slideSize, sidebarSize, screenShareSize) => {
const { isOpen, slidesLength } = presentationInput;
const { hasExternalVideo } = externalVideoInput;
const { hasScreenShare } = screenShareInput;
const { isPinned: isSharedNotesPinned } = sharedNotesInput;
const hasPresentation = isPresentationEnabled() && slidesLength !== 0
const isGeneralMediaOff = !hasPresentation && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned;
const hasPresentation = isPresentationEnabled() && slidesLength !== 0;
const isGeneralMediaOff =
!hasPresentation && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned;
const mediaBounds = {};
const { element: fullscreenElement } = fullscreen;
@ -289,7 +300,11 @@ const SmartLayout = (props) => {
return mediaBounds;
}
if (fullscreenElement === 'Presentation' || fullscreenElement === 'Screenshare' || fullscreenElement === 'ExternalVideo') {
if (
fullscreenElement === 'Presentation' ||
fullscreenElement === 'Screenshare' ||
fullscreenElement === 'ExternalVideo'
) {
mediaBounds.width = windowWidth();
mediaBounds.height = windowHeight();
mediaBounds.top = 0;
@ -304,26 +319,24 @@ const SmartLayout = (props) => {
if (cameraDockInput.numCameras > 0 && !cameraDockInput.isDragging) {
if (mediaContentSize.width !== 0 && mediaContentSize.height !== 0 && !hasExternalVideo) {
if (mediaContentSize.width < mediaAreaBounds.width && !isMobile) {
if (mediaContentSize.width < (mediaAreaBounds.width * 0.8)) {
if (mediaContentSize.width < mediaAreaBounds.width * 0.8) {
mediaBounds.width = mediaContentSize.width;
} else {
mediaBounds.width = mediaAreaBounds.width * 0.8;
}
mediaBounds.height = mediaAreaBounds.height;
mediaBounds.top = mediaAreaBounds.top;
const sizeValue = mediaAreaBounds.left
+ (mediaAreaBounds.width - mediaBounds.width);
const sizeValue = mediaAreaBounds.left + (mediaAreaBounds.width - mediaBounds.width);
mediaBounds.left = !isRTL ? sizeValue : null;
mediaBounds.right = isRTL ? sidebarSize : null;
} else {
if (mediaContentSize.height < (mediaAreaBounds.height * 0.8)) {
if (mediaContentSize.height < mediaAreaBounds.height * 0.8) {
mediaBounds.height = mediaContentSize.height;
} else {
mediaBounds.height = mediaAreaBounds.height * 0.8;
}
mediaBounds.width = mediaAreaBounds.width;
mediaBounds.top = mediaAreaBounds.top
+ (mediaAreaBounds.height - mediaBounds.height);
mediaBounds.top = mediaAreaBounds.top + (mediaAreaBounds.height - mediaBounds.height);
const sizeValue = mediaAreaBounds.left;
mediaBounds.left = !isRTL ? sizeValue : null;
mediaBounds.right = isRTL ? sidebarSize : null;
@ -331,8 +344,7 @@ const SmartLayout = (props) => {
} else {
mediaBounds.width = mediaAreaBounds.width;
mediaBounds.height = mediaAreaBounds.height * 0.8;
mediaBounds.top = mediaAreaBounds.top
+ (mediaAreaBounds.height - mediaBounds.height);
mediaBounds.top = mediaAreaBounds.top + (mediaAreaBounds.height - mediaBounds.height);
const sizeValue = mediaAreaBounds.left;
mediaBounds.left = !isRTL ? sizeValue : null;
mediaBounds.right = isRTL ? sidebarSize : null;
@ -370,7 +382,10 @@ const SmartLayout = (props) => {
const sidebarContentHeight = calculatesSidebarContentHeight();
const sidebarNavBounds = calculatesSidebarNavBounds();
const sidebarContentBounds = calculatesSidebarContentBounds(sidebarNavWidth.width);
const mediaAreaBounds = calculatesMediaAreaBounds(sidebarNavWidth.width, sidebarContentWidth.width);
const mediaAreaBounds = calculatesMediaAreaBounds(
sidebarNavWidth.width,
sidebarContentWidth.width
);
const navbarBounds = calculatesNavbarBounds(mediaAreaBounds);
const actionbarBounds = calculatesActionbarBounds(mediaAreaBounds);
const slideSize = calculatesSlideSize(mediaAreaBounds);
@ -380,11 +395,11 @@ const SmartLayout = (props) => {
mediaAreaBounds,
slideSize,
sidebarSize,
screenShareSize,
screenShareSize
);
const cameraDockBounds = calculatesCameraDockBounds(mediaAreaBounds, mediaBounds, sidebarSize);
const horizontalCameraDiff = cameraDockBounds.isCameraHorizontal
? cameraDockBounds.width + (camerasMargin * 2)
? cameraDockBounds.width + camerasMargin * 2
: 0;
layoutContextDispatch({
@ -418,9 +433,9 @@ const SmartLayout = (props) => {
layoutContextDispatch({
type: ACTIONS.SET_CAPTIONS_OUTPUT,
value: {
left: !isRTL ? (sidebarSize + captionsMargin) : null,
right: isRTL ? (sidebarSize + captionsMargin) : null,
maxWidth: mediaAreaBounds.width - (captionsMargin * 2),
left: !isRTL ? sidebarSize + captionsMargin : null,
right: isRTL ? sidebarSize + captionsMargin : null,
maxWidth: mediaAreaBounds.width - captionsMargin * 2,
},
});
@ -522,7 +537,7 @@ const SmartLayout = (props) => {
height: mediaBounds.height,
top: mediaBounds.top,
left: mediaBounds.left,
right: isRTL ? (mediaBounds.right + horizontalCameraDiff) : null,
right: isRTL ? mediaBounds.right + horizontalCameraDiff : null,
tabOrder: DEFAULT_VALUES.presentationTabOrder,
isResizable: false,
zIndex: mediaBounds.zIndex,
@ -536,7 +551,7 @@ const SmartLayout = (props) => {
height: mediaBounds.height,
top: mediaBounds.top,
left: mediaBounds.left,
right: isRTL ? (mediaBounds.right + horizontalCameraDiff) : null,
right: isRTL ? mediaBounds.right + horizontalCameraDiff : null,
zIndex: mediaBounds.zIndex,
},
});
@ -548,7 +563,7 @@ const SmartLayout = (props) => {
height: mediaBounds.height,
top: mediaBounds.top,
left: mediaBounds.left,
right: isRTL ? (mediaBounds.right + horizontalCameraDiff) : null,
right: isRTL ? mediaBounds.right + horizontalCameraDiff : null,
},
});
@ -559,7 +574,7 @@ const SmartLayout = (props) => {
height: mediaBounds.height,
top: mediaBounds.top,
left: mediaBounds.left,
right: isRTL ? (mediaBounds.right + horizontalCameraDiff) : null,
right: isRTL ? mediaBounds.right + horizontalCameraDiff : null,
},
});
};
@ -567,4 +582,4 @@ const SmartLayout = (props) => {
return null;
};
export default SmartLayout;
export default SmartLayout;

View File

@ -4,7 +4,7 @@ import {
layoutDispatch,
layoutSelect,
layoutSelectInput,
layoutSelectOutput
layoutSelectOutput,
} from '/imports/ui/components/layout/context';
import DEFAULT_VALUES from '/imports/ui/components/layout/defaultValues';
import { INITIAL_INPUT_STATE } from '/imports/ui/components/layout/initState';
@ -49,8 +49,10 @@ const VideoFocusLayout = (props) => {
const prevDeviceType = usePrevious(deviceType);
const throttledCalculatesLayout = throttle(() => calculatesLayout(),
50, { trailing: true, leading: true });
const throttledCalculatesLayout = throttle(() => calculatesLayout(), 50, {
trailing: true,
leading: true,
});
useEffect(() => {
window.addEventListener('resize', () => {
@ -77,53 +79,15 @@ const VideoFocusLayout = (props) => {
}, [input, deviceType, isRTL, fontSize, fullscreen]);
const init = () => {
const { sidebarContentPanel } = sidebarContentInput;
if (isMobile) {
layoutContextDispatch({
type: ACTIONS.SET_LAYOUT_INPUT,
value: defaultsDeep(
{
sidebarNavigation: {
isOpen: false,
sidebarNavPanel: sidebarNavigationInput.sidebarNavPanel,
},
sidebarContent: {
isOpen: false,
sidebarContentPanel: sidebarContentInput.sidebarContentPanel,
},
SidebarContentHorizontalResizer: {
isOpen: false,
},
presentation: {
isOpen: presentationInput.isOpen,
slidesLength: presentationInput.slidesLength,
currentSlide: {
...presentationInput.currentSlide,
},
},
cameraDock: {
numCameras: cameraDockInput.numCameras,
},
externalVideo: {
hasExternalVideo: input.externalVideo.hasExternalVideo,
},
screenShare: {
hasScreenShare: input.screenShare.hasScreenShare,
width: input.screenShare.width,
height: input.screenShare.height,
},
},
INITIAL_INPUT_STATE,
),
});
} else {
const { sidebarContentPanel } = sidebarContentInput;
layoutContextDispatch({
type: ACTIONS.SET_LAYOUT_INPUT,
value: defaultsDeep(
{
sidebarNavigation: {
isOpen: input.sidebarNavigation.isOpen || sidebarContentPanel !== PANELS.NONE || false,
isOpen:
input.sidebarNavigation.isOpen || sidebarContentPanel !== PANELS.NONE || false,
},
sidebarContent: {
isOpen: sidebarContentPanel !== PANELS.NONE,
@ -151,7 +115,45 @@ const VideoFocusLayout = (props) => {
height: input.screenShare.height,
},
},
INITIAL_INPUT_STATE,
INITIAL_INPUT_STATE
),
});
} else {
layoutContextDispatch({
type: ACTIONS.SET_LAYOUT_INPUT,
value: defaultsDeep(
{
sidebarNavigation: {
isOpen:
input.sidebarNavigation.isOpen || sidebarContentPanel !== PANELS.NONE || false,
},
sidebarContent: {
isOpen: sidebarContentPanel !== PANELS.NONE,
sidebarContentPanel,
},
SidebarContentHorizontalResizer: {
isOpen: false,
},
presentation: {
isOpen: presentationInput.isOpen,
slidesLength: presentationInput.slidesLength,
currentSlide: {
...presentationInput.currentSlide,
},
},
cameraDock: {
numCameras: cameraDockInput.numCameras,
},
externalVideo: {
hasExternalVideo: input.externalVideo.hasExternalVideo,
},
screenShare: {
hasScreenShare: input.screenShare.hasScreenShare,
width: input.screenShare.width,
height: input.screenShare.height,
},
},
INITIAL_INPUT_STATE
),
});
}
@ -165,8 +167,9 @@ const VideoFocusLayout = (props) => {
const { hasScreenShare } = screenShareInput;
const { isPinned: isSharedNotesPinned } = sharedNotesInput;
const hasPresentation = isPresentationEnabled() && slidesLength !== 0
const isGeneralMediaOff = !hasPresentation && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned;
const hasPresentation = isPresentationEnabled() && slidesLength !== 0;
const isGeneralMediaOff =
!hasPresentation && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned;
let minHeight = 0;
let height = 0;
@ -250,12 +253,16 @@ const VideoFocusLayout = (props) => {
cameraDockBounds,
sidebarNavWidth,
sidebarContentWidth,
sidebarContentHeight,
sidebarContentHeight
) => {
const mediaBounds = {};
const { element: fullscreenElement } = fullscreen;
if (fullscreenElement === 'Presentation' || fullscreenElement === 'Screenshare' || fullscreenElement === 'ExternalVideo') {
if (
fullscreenElement === 'Presentation' ||
fullscreenElement === 'Screenshare' ||
fullscreenElement === 'ExternalVideo'
) {
mediaBounds.width = windowWidth();
mediaBounds.height = windowHeight();
mediaBounds.top = 0;
@ -307,7 +314,8 @@ const VideoFocusLayout = (props) => {
const sidebarNavBounds = calculatesSidebarNavBounds();
const sidebarContentBounds = calculatesSidebarContentBounds(sidebarNavWidth.width);
const mediaAreaBounds = calculatesMediaAreaBounds(
sidebarNavWidth.width, sidebarContentWidth.width,
sidebarNavWidth.width,
sidebarContentWidth.width
);
const navbarBounds = calculatesNavbarBounds(mediaAreaBounds);
const actionbarBounds = calculatesActionbarBounds(mediaAreaBounds);
@ -319,7 +327,7 @@ const VideoFocusLayout = (props) => {
cameraDockBounds,
sidebarNavWidth.width,
sidebarContentWidth.width,
sidebarContentHeight.height,
sidebarContentHeight.height
);
layoutContextDispatch({
@ -353,9 +361,9 @@ const VideoFocusLayout = (props) => {
layoutContextDispatch({
type: ACTIONS.SET_CAPTIONS_OUTPUT,
value: {
left: !isRTL ? (sidebarSize + captionsMargin) : null,
right: isRTL ? (sidebarSize + captionsMargin) : null,
maxWidth: mediaAreaBounds.width - (captionsMargin * 2),
left: !isRTL ? sidebarSize + captionsMargin : null,
right: isRTL ? sidebarSize + captionsMargin : null,
maxWidth: mediaAreaBounds.width - captionsMargin * 2,
},
});
@ -513,4 +521,4 @@ const VideoFocusLayout = (props) => {
return null;
};
export default VideoFocusLayout;
export default VideoFocusLayout;

View File

@ -2,13 +2,14 @@ import Presentations from '/imports/api/presentations';
import { isScreenBroadcasting, isCameraAsContentBroadcasting } from '/imports/ui/components/screenshare/service';
import Settings from '/imports/ui/services/settings';
import getFromUserSettings from '/imports/ui/services/users-settings';
import { isExternalVideoEnabled, isScreenSharingEnabled } from '/imports/ui/services/features';
import {
isExternalVideoEnabled, isScreenSharingEnabled, isCameraAsContentEnabled, isPresentationEnabled,
} from '/imports/ui/services/features';
import { ACTIONS } from '../layout/enums';
import UserService from '/imports/ui/components/user-list/service';
import NotesService from '/imports/ui/components/notes/service';
import { getVideoUrl } from '/imports/ui/components/external-video-player/service';
import VideoStreams from '/imports/api/video-streams';
import { isPresentationEnabled } from '/imports/ui/services/features';
import Auth from '/imports/ui/services/auth/index';
const LAYOUT_CONFIG = Meteor.settings.public.layout;
@ -31,7 +32,8 @@ function shouldShowWhiteboard() {
function shouldShowScreenshare() {
const { viewScreenshare } = Settings.dataSaving;
return isScreenSharingEnabled() && (viewScreenshare || UserService.isUserPresenter())
return (isScreenSharingEnabled() || isCameraAsContentEnabled())
&& (viewScreenshare || UserService.isUserPresenter())
&& (isScreenBroadcasting() || isCameraAsContentBroadcasting());
}

View File

@ -63,7 +63,7 @@ const TalkingIndicatorButton = styled(Button)`
font-size: ${fontSizeXS};
}
[dir="rtl"] & {
[dir='rtl'] & {
margin-left: ${talkerMarginSm};
}
}
@ -84,7 +84,7 @@ const TalkingIndicatorButton = styled(Button)`
font-size: ${fontSizeXS};
}
[dir="rtl"] & {
[dir='rtl'] & {
right: calc(${talkerMarginSm} * -1);
}
}
@ -93,7 +93,8 @@ const TalkingIndicatorButton = styled(Button)`
opacity: 1;
}
${({ $spoke }) => $spoke && `
${({ $spoke }) => $spoke
&& `
opacity: ${spokeOpacity};
[dir="rtl"] & {
@ -101,7 +102,8 @@ const TalkingIndicatorButton = styled(Button)`
}
`}
${({ $muted }) => $muted && `
${({ $muted }) => $muted
&& `
cursor: default;
i {
@ -109,7 +111,8 @@ const TalkingIndicatorButton = styled(Button)`
}
`}
${({ $isViewer }) => $isViewer && `
${({ $isViewer }) => $isViewer
&& `
cursor: default;
`}
`;
@ -118,19 +121,16 @@ const CCIcon = styled(Icon)`
align-self: center;
color: ${colorWhite};
margin: 0 ${borderRadius};
font-size: calc(${fontSizeSmall} * .85);
opacity: ${({ muted, talking }) => ((muted || !talking) && `${spokeOpacity};`)
|| '1;'};
font-size: calc(${fontSizeSmall} * 0.85);
opacity: ${({ muted, talking }) => ((muted || !talking) && `${spokeOpacity};`) || '1;'};
`;
const TalkingIndicatorWrapper = styled.div`
border-radius: ${talkerBorderRadius} ${talkerBorderRadius};
display: flex;
margin: 0 ${borderRadius};
opacity: ${({ muted, talking }) => ((muted || !talking) && `${spokeOpacity};`)
|| '1;'};
background: ${({ muted, talking, floor }) => ((muted || !talking || !floor) && `${colorBackground};`)
|| `${colorSuccess}`}
opacity: ${({ muted, talking }) => ((muted || !talking) && `${spokeOpacity};`) || '1;'};
background: ${({ muted, talking, floor }) => ((muted || !talking || !floor) && `${colorBackground};`) || `${colorSuccess}`};
`;
const Hidden = styled.div`
@ -141,7 +141,6 @@ const IsTalkingWrapper = styled.div`
display: flex;
flex-direction: row;
position: relative;
margin-top: ${talkerMarginSm};
overflow: hidden;
`;

View File

@ -65,6 +65,7 @@ const Notes = ({
shouldShowSharedNotesOnPresentationArea,
}) => {
const [shouldRenderNotes, setShouldRenderNotes] = useState(false);
const { isChrome } = browserInfo;
const isOnMediaArea = area === 'media';
const style = isOnMediaArea ? {
@ -73,9 +74,9 @@ const Notes = ({
} : {};
const isHidden = (isOnMediaArea && (style.width === 0 || style.height === 0))
|| (!isToSharedNotesBeShow
&& !sidebarContentToIgnoreDelay.includes(sidebarContent.sidebarContentPanel))
|| shouldShowSharedNotesOnPresentationArea;
|| (!isToSharedNotesBeShow
&& !sidebarContentToIgnoreDelay.includes(sidebarContent.sidebarContentPanel))
|| shouldShowSharedNotesOnPresentationArea;
if (isHidden && !isOnMediaArea) {
style.padding = 0;
@ -89,7 +90,7 @@ const Notes = ({
timoutRef = setTimeout(() => {
setShouldRenderNotes(false);
}, (sidebarContentToIgnoreDelay.includes(sidebarContent.sidebarContentPanel)
|| shouldShowSharedNotesOnPresentationArea)
|| shouldShowSharedNotesOnPresentationArea)
? 0 : DELAY_UNMOUNT_SHARED_NOTES);
}
return () => clearTimeout(timoutRef);
@ -115,7 +116,6 @@ const Notes = ({
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
value: false,
});
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
value: PANELS.NONE,
@ -141,17 +141,14 @@ const Notes = ({
value: Session.get('presentationLastState'),
});
};
}
if(shouldShowSharedNotesOnPresentationArea) {
} else {
if (shouldShowSharedNotesOnPresentationArea) {
layoutContextDispatch({
type: ACTIONS.SET_NOTES_IS_PINNED,
value: true,
});
layoutContextDispatch({
type: ACTIONS.SET_PRESENTATION_IS_OPEN,
value: true,
});
}
}
}, []);
const renderHeaderOnMedia = () => {
@ -171,7 +168,11 @@ const Notes = ({
};
return (shouldRenderNotes || shouldShowSharedNotesOnPresentationArea) && (
<Styled.Notes data-test="notes" isChrome={isChrome} style={style}>
<Styled.Notes
data-test="notes"
isChrome={isChrome}
style={style}
>
{!isOnMediaArea ? (
<Header
leftButtonProps={{

View File

@ -21,6 +21,9 @@ const Notes = styled.div`
@media ${smallOnly} {
transform: none !important;
&.no-padding {
padding: 0;
}
}
`;

View File

@ -19,6 +19,7 @@ import { colorContentBackground } from '/imports/ui/stylesheets/styled-component
import browserInfo from '/imports/utils/browserInfo';
import { addNewAlert } from '../screenreader-alert/service';
import { clearCursors } from '/imports/ui/components/whiteboard/cursors/service';
import { debounce } from 'radash';
const intlMessages = defineMessages({
presentationLabel: {
@ -85,13 +86,13 @@ class Presentation extends PureComponent {
this.getSvgRef = this.getSvgRef.bind(this);
this.setFitToWidth = this.setFitToWidth.bind(this);
this.zoomChanger = this.zoomChanger.bind(this);
this.zoomChanger = debounce({ delay: 200 }, this.zoomChanger.bind(this));
this.updateLocalPosition = this.updateLocalPosition.bind(this);
this.panAndZoomChanger = this.panAndZoomChanger.bind(this);
this.fitToWidthHandler = this.fitToWidthHandler.bind(this);
this.onFullscreenChange = this.onFullscreenChange.bind(this);
this.getPresentationSizesAvailable = this.getPresentationSizesAvailable.bind(this);
this.handleResize = this.handleResize.bind(this);
this.handleResize = debounce({ delay: 200 }, this.handleResize.bind(this));
this.setTldrawAPI = this.setTldrawAPI.bind(this);
this.setIsPanning = this.setIsPanning.bind(this);
this.setIsToolbarVisible = this.setIsToolbarVisible.bind(this);

View File

@ -54,7 +54,7 @@ const intlMessages = defineMessages({
id: 'app.presentationUploder.upload.413',
description: 'error that file exceed the size limit',
},
IVALID_MIME_TYPE: {
INVALID_MIME_TYPE: {
id: 'app.presentationUploder.conversion.invalidMimeType',
description: 'warns user that the file\'s mime type is not supported or it doesn\'t match the extension',
},
@ -151,7 +151,7 @@ function renderPresentationItemStatus(item, intl) {
case 'PDF_HAS_BIG_PAGE':
constraint['0'] = (item.conversion.bigPageSize / 1000 / 1000).toFixed(2);
break;
case 'IVALID_MIME_TYPE':
case 'INVALID_MIME_TYPE':
constraint['0'] = item.conversion.fileExtension;
constraint['1'] = item.conversion.fileMime;
break;

View File

@ -15,9 +15,9 @@ const intlMessages = defineMessages({
id: 'app.presentationUploader.disableOriginalPresentationDownload',
description: 'Send original presentation to chat',
},
sendAnnotatedDocument: {
id: 'app.presentationUploader.exportAnnotatedPresentation',
description: 'Send presentation to chat with annotations label',
sendCurrentStateDocument: {
id: 'app.presentationUploader.exportCurrentStatePresentation',
description: 'Send presentation to chat in the current state label',
},
copySuccess: {
id: 'app.chat.copySuccess',
@ -53,7 +53,6 @@ const propTypes = {
uploadTimestamp: PropTypes.number.isRequired,
}).isRequired,
closeModal: PropTypes.func.isRequired,
hasAnnotations: PropTypes.bool.isRequired,
isRTL: PropTypes.bool.isRequired,
disabled: PropTypes.bool.isRequired,
};
@ -77,7 +76,6 @@ class PresentationDownloadDropdown extends PureComponent {
isDownloadable,
item,
closeModal,
hasAnnotations,
} = this.props;
this.menuItems = [];
@ -91,69 +89,51 @@ class PresentationDownloadDropdown extends PureComponent {
};
if (!isDownloadable) {
this.menuItems.push(
{
key: this.actionsKey[0],
dataTest: 'enableOriginalPresentationDownload',
label: intl.formatMessage(intlMessages.enableOriginalPresentationDownload),
onClick: () => toggleDownloadOriginalPresentation(true),
},
);
this.menuItems.push({
key: this.actionsKey[0],
dataTest: 'enableOriginalPresentationDownload',
label: intl.formatMessage(intlMessages.enableOriginalPresentationDownload),
onClick: () => toggleDownloadOriginalPresentation(true),
});
} else {
this.menuItems.push(
{
key: this.actionsKey[0],
dataTest: 'disableOriginalPresentationDownload',
label: intl.formatMessage(intlMessages.disableOriginalPresentationDownload),
onClick: () => toggleDownloadOriginalPresentation(false),
},
);
this.menuItems.push({
key: this.actionsKey[0],
dataTest: 'disableOriginalPresentationDownload',
label: intl.formatMessage(intlMessages.disableOriginalPresentationDownload),
onClick: () => toggleDownloadOriginalPresentation(false),
});
}
if (hasAnnotations) {
this.menuItems.push(
{
key: this.actionsKey[1],
id: 'sendAnnotatedDocument',
dataTest: 'sendAnnotatedDocument',
label: intl.formatMessage(intlMessages.sendAnnotatedDocument),
onClick: () => {
closeModal();
handleDownloadingOfPresentation('Annotated');
},
},
);
}
this.menuItems.push({
key: this.actionsKey[1],
id: 'sendCurrentStateDocument',
dataTest: 'sendCurrentStateDocument',
label: intl.formatMessage(intlMessages.sendCurrentStateDocument),
onClick: () => {
closeModal();
handleDownloadingOfPresentation('Annotated');
},
});
return this.menuItems;
}
render() {
const {
intl,
isRTL,
disabled,
} = this.props;
const { intl, isRTL, disabled } = this.props;
const customStyles = { zIndex: 9999 };
return (
<PresentationDownloadDropdownWrapper
disabled={disabled}
>
<PresentationDownloadDropdownWrapper disabled={disabled}>
<BBBMenu
customStyles={customStyles}
trigger={
(
<Trigger
data-test="presentationOptionsDownload"
icon="more"
label={intl.formatMessage(intlMessages.options)}
aria-label={intl.formatMessage(intlMessages.options)}
onClick={() => null}
/>
)
}
trigger={(
<Trigger
data-test="presentationOptionsDownload"
icon="more"
label={intl.formatMessage(intlMessages.options)}
aria-label={intl.formatMessage(intlMessages.options)}
onClick={() => null}
/>
)}
opts={{
id: 'presentation-download-dropdown',
keepMounted: true,

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