Merge branch 'v2.6.x-release' into capture-slides-upload-toast

This commit is contained in:
Daniel Petri Rocha 2022-12-22 21:18:14 +01:00 committed by GitHub
commit a8657ff0ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
199 changed files with 5561 additions and 11787 deletions

View File

@ -13,7 +13,7 @@ jobs:
build-install-and-test: build-install-and-test:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- run: ./build/get_external_dependencies.sh - run: ./build/get_external_dependencies.sh
- run: ./build/setup.sh bbb-apps-akka - run: ./build/setup.sh bbb-apps-akka
- run: ./build/setup.sh bbb-config - run: ./build/setup.sh bbb-config

View File

@ -6,8 +6,13 @@ on:
- opened - opened
- synchronize - synchronize
permissions:
contents: read
jobs: jobs:
main: main:
permissions:
pull-requests: write # for eps1lon/actions-label-merge-conflict to label PRs
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check for dirty pull requests - name: Check for dirty pull requests

View File

@ -11,7 +11,7 @@ stages:
# define which docker image to use for builds # define which docker image to use for builds
default: default:
image: gitlab.senfcall.de:5050/senfcall-public/docker-bbb-build:v2022-07-12 image: gitlab.senfcall.de:5050/senfcall-public/docker-bbb-build:v2022-12-08-meteor-290
# This stage uses git to find out since when each package has been unmodified. # This stage uses git to find out since when each package has been unmodified.
# it then checks an API endpoint on the package server to find out for which of # it then checks an API endpoint on the package server to find out for which of

View File

@ -18,7 +18,7 @@ val compileSettings = Seq(
"-Xlint", "-Xlint",
"-Ywarn-dead-code", "-Ywarn-dead-code",
"-language:_", "-language:_",
"-target:jvm-1.11", "-target:11",
"-encoding", "UTF-8" "-encoding", "UTF-8"
), ),
javacOptions ++= List( javacOptions ++= List(
@ -48,7 +48,7 @@ lazy val bbbAppsAkka = (project in file(".")).settings(name := "bbb-apps-akka",
// Config file is in ./.scalariform.conf // Config file is in ./.scalariform.conf
scalariformAutoformat := true scalariformAutoformat := true
scalaVersion := "2.13.4" scalaVersion := "2.13.9"
//----------- //-----------
// Packaging // Packaging
// //

View File

@ -7,7 +7,7 @@ object Dependencies {
object Versions { object Versions {
// Scala // Scala
val scala = "2.13.4" val scala = "2.13.9"
val junit = "4.12" val junit = "4.12"
val junitInterface = "0.11" val junitInterface = "0.11"
val scalactic = "3.0.8" val scalactic = "3.0.8"
@ -26,7 +26,7 @@ object Dependencies {
val codec = "1.15" val codec = "1.15"
// BigBlueButton // BigBlueButton
val bbbCommons = "0.0.21-SNAPSHOT" val bbbCommons = "0.0.22-SNAPSHOT"
// Test // Test
val scalaTest = "3.2.11" val scalaTest = "3.2.11"

View File

@ -0,0 +1,38 @@
package org.bigbluebutton.core.apps.presentationpod
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.running.LiveMeeting
trait PresentationHasInvalidMimeTypeErrorPubMsgHdlr {
this: PresentationPodHdlrs =>
def handle(
msg: PresentationHasInvalidMimeTypeErrorSysPubMsg, state: MeetingState2x,
liveMeeting: LiveMeeting, bus: MessageBus
): MeetingState2x = {
def broadcastEvent(msg: PresentationHasInvalidMimeTypeErrorSysPubMsg): Unit = {
val routing = Routing.addMsgToClientRouting(
MessageTypes.BROADCAST_TO_MEETING,
liveMeeting.props.meetingProp.intId, msg.header.userId
)
val envelope = BbbCoreEnvelope(PresentationHasInvalidMimeTypeErrorEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(
PresentationHasInvalidMimeTypeErrorEvtMsg.NAME,
liveMeeting.props.meetingProp.intId, msg.header.userId
)
val body = PresentationHasInvalidMimeTypeErrorEvtMsgBody(msg.body.podId, msg.body.meetingId,
msg.body.presentationName, msg.body.temporaryPresentationId,
msg.body.presentationId, msg.body.messageKey, msg.body.fileMime, msg.body.fileExtension)
val event = PresentationHasInvalidMimeTypeErrorEvtMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
bus.outGW.send(msgEvent)
}
broadcastEvent(msg)
state
}
}

View File

@ -26,7 +26,8 @@ class PresentationPodHdlrs(implicit val context: ActorContext)
with PresentationPageConvertedSysMsgHdlr with PresentationPageConvertedSysMsgHdlr
with PresentationPageConversionStartedSysMsgHdlr with PresentationPageConversionStartedSysMsgHdlr
with PresentationConversionEndedSysMsgHdlr with PresentationConversionEndedSysMsgHdlr
with PresentationUploadedFileTimeoutErrorPubMsgHdlr { with PresentationUploadedFileTimeoutErrorPubMsgHdlr
with PresentationHasInvalidMimeTypeErrorPubMsgHdlr {
val log = Logging(context.system, getClass) val log = Logging(context.system, getClass)
} }

View File

@ -13,6 +13,12 @@ import java.io.File
trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait { trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
this: PresentationPodHdlrs => this: PresentationPodHdlrs =>
object JobTypes {
val DOWNLOAD = "PresentationWithAnnotationDownloadJob"
val CAPTURE_PRESENTATION = "PresentationWithAnnotationExportJob"
val CAPTURE_NOTES = "PadCaptureJob"
}
def buildStoreAnnotationsInRedisSysMsg(annotations: StoredAnnotations, liveMeeting: LiveMeeting): BbbCommonEnvCoreMsg = { def buildStoreAnnotationsInRedisSysMsg(annotations: StoredAnnotations, liveMeeting: LiveMeeting): BbbCommonEnvCoreMsg = {
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka") val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
val envelope = BbbCoreEnvelope(StoreAnnotationsInRedisSysMsg.NAME, routing) val envelope = BbbCoreEnvelope(StoreAnnotationsInRedisSysMsg.NAME, routing)
@ -111,7 +117,6 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
log.error(s"Presentation ${presId} not found in meeting ${meetingId}") log.error(s"Presentation ${presId} not found in meeting ${meetingId}")
} else { } else {
val jobType: String = "PresentationWithAnnotationDownloadJob"
val jobId: String = RandomStringGenerator.randomAlphanumericString(16); val jobId: String = RandomStringGenerator.randomAlphanumericString(16);
val allPages: Boolean = m.body.allPages val allPages: Boolean = m.body.allPages
val pageCount = currentPres.get.pages.size val pageCount = currentPres.get.pages.size
@ -120,7 +125,7 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
val pages: List[Int] = m.body.pages // Desired presentation pages for export val pages: List[Int] = m.body.pages // Desired presentation pages for export
val pagesRange: List[Int] = if (allPages) (1 to pageCount).toList else pages val pagesRange: List[Int] = if (allPages) (1 to pageCount).toList else pages
val exportJob: ExportJob = new ExportJob(jobId, jobType, "annotated_slides", presId, presLocation, allPages, pagesRange, meetingId, ""); 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 storeAnnotationPages: List[PresentationPageForExport] = getPresentationPagesForExport(pagesRange, pageCount, presId, currentPres, liveMeeting);
// Send Export Job to Redis // Send Export Job to Redis
@ -147,7 +152,6 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
} else { } else {
val jobId: String = s"${meetingId}-slides" // Used as the temporaryPresentationId upon upload val jobId: String = s"${meetingId}-slides" // Used as the temporaryPresentationId upon upload
val jobType = "PresentationWithAnnotationExportJob"
val allPages: Boolean = m.allPages val allPages: Boolean = m.allPages
val pageCount = currentPres.get.pages.size val pageCount = currentPres.get.pages.size
@ -164,7 +168,7 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
// Informs bbb-web about the token so that when we use it to upload the presentation, it is able to look it up in the list of tokens // Informs bbb-web about the token so that when we use it to upload the presentation, it is able to look it up in the list of tokens
bus.outGW.send(buildPresentationUploadTokenSysPubMsg(parentMeetingId, userId, presentationUploadToken, filename)) bus.outGW.send(buildPresentationUploadTokenSysPubMsg(parentMeetingId, userId, presentationUploadToken, filename))
val exportJob: ExportJob = new ExportJob(jobId, jobType, filename, presId, presLocation, allPages, pagesRange, parentMeetingId, presentationUploadToken) val exportJob: ExportJob = new ExportJob(jobId, JobTypes.CAPTURE_PRESENTATION, filename, presId, presLocation, allPages, pagesRange, parentMeetingId, presentationUploadToken)
val storeAnnotationPages: List[PresentationPageForExport] = getPresentationPagesForExport(pagesRange, pageCount, presId, currentPres, liveMeeting); val storeAnnotationPages: List[PresentationPageForExport] = getPresentationPagesForExport(pagesRange, pageCount, presId, currentPres, liveMeeting);
// Send Export Job to Redis // Send Export Job to Redis
@ -201,13 +205,12 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
val userId: String = "system" val userId: String = "system"
val jobId: String = s"${m.body.breakoutId}-notes" // Used as the temporaryPresentationId upon upload val jobId: String = s"${m.body.breakoutId}-notes" // Used as the temporaryPresentationId upon upload
val jobType = "PadCaptureJob"
val filename = m.body.filename val filename = m.body.filename
val presentationUploadToken: String = PresentationPodsApp.generateToken("DEFAULT_PRESENTATION_POD", userId) val presentationUploadToken: String = PresentationPodsApp.generateToken("DEFAULT_PRESENTATION_POD", userId)
bus.outGW.send(buildPresentationUploadTokenSysPubMsg(m.body.parentMeetingId, userId, presentationUploadToken, filename)) bus.outGW.send(buildPresentationUploadTokenSysPubMsg(m.body.parentMeetingId, userId, presentationUploadToken, filename))
val exportJob = new ExportJob(jobId, jobType, filename, m.body.padId, "", true, List(), m.body.parentMeetingId, presentationUploadToken) val exportJob = new ExportJob(jobId, JobTypes.CAPTURE_NOTES, filename, m.body.padId, "", true, List(), m.body.parentMeetingId, presentationUploadToken)
val job = buildStoreExportJobInRedisSysMsg(exportJob, liveMeeting) val job = buildStoreExportJobInRedisSysMsg(exportJob, liveMeeting)
bus.outGW.send(job) bus.outGW.send(job)

View File

@ -42,6 +42,17 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait {
MeetingStatus2x.setPermissions(liveMeeting.status, settings) MeetingStatus2x.setPermissions(liveMeeting.status, settings)
// Dial-in
def buildLockMessage(meetingId: String, userId: String, lockedBy: String, locked: Boolean): BbbCommonEnvCoreMsg = {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId)
val envelope = BbbCoreEnvelope(UserLockedInMeetingEvtMsg.NAME, routing)
val body = UserLockedInMeetingEvtMsgBody(userId, locked, lockedBy)
val header = BbbClientMsgHeader(UserLockedInMeetingEvtMsg.NAME, meetingId, userId)
val event = UserLockedInMeetingEvtMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
if (oldPermissions.disableCam != settings.disableCam) { if (oldPermissions.disableCam != settings.disableCam) {
if (settings.disableCam) { if (settings.disableCam) {
val notifyEvent = MsgBuilder.buildNotifyAllInMeetingEvtMsg( val notifyEvent = MsgBuilder.buildNotifyAllInMeetingEvtMsg(
@ -55,24 +66,6 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait {
outGW.send(notifyEvent) outGW.send(notifyEvent)
LockSettingsUtil.enforceCamLockSettingsForAllUsers(liveMeeting, outGW) LockSettingsUtil.enforceCamLockSettingsForAllUsers(liveMeeting, outGW)
// Dial-in
def buildLockMessage(meetingId: String, userId: String, lockedBy: String, locked: Boolean): BbbCommonEnvCoreMsg = {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId)
val envelope = BbbCoreEnvelope(UserLockedInMeetingEvtMsg.NAME, routing)
val body = UserLockedInMeetingEvtMsgBody(userId, locked, lockedBy)
val header = BbbClientMsgHeader(UserLockedInMeetingEvtMsg.NAME, meetingId, userId)
val event = UserLockedInMeetingEvtMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
VoiceUsers.findAll(liveMeeting.voiceUsers) foreach { vu =>
if (vu.intId.startsWith(IntIdPrefixType.DIAL_IN)) { // only Dial-in users need this
val eventExplicitLock = buildLockMessage(liveMeeting.props.meetingProp.intId, vu.intId, msg.body.setBy, settings.disableMic)
outGW.send(eventExplicitLock)
}
}
} else { } else {
val notifyEvent = MsgBuilder.buildNotifyAllInMeetingEvtMsg( val notifyEvent = MsgBuilder.buildNotifyAllInMeetingEvtMsg(
liveMeeting.props.meetingProp.intId, liveMeeting.props.meetingProp.intId,
@ -97,8 +90,12 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait {
Vector() Vector()
) )
outGW.send(notifyEvent) outGW.send(notifyEvent)
VoiceUsers.findAll(liveMeeting.voiceUsers) foreach { vu =>
// Apply lock settings when disableMic from false to true. if (vu.intId.startsWith(IntIdPrefixType.DIAL_IN)) { // only Dial-in users need this
val eventExplicitLock = buildLockMessage(liveMeeting.props.meetingProp.intId, vu.intId, msg.body.setBy, settings.disableMic)
outGW.send(eventExplicitLock)
}
}
LockSettingsUtil.enforceLockSettingsForAllVoiceUsers(liveMeeting, outGW) LockSettingsUtil.enforceLockSettingsForAllVoiceUsers(liveMeeting, outGW)
} else { } else {
val notifyEvent = MsgBuilder.buildNotifyAllInMeetingEvtMsg( val notifyEvent = MsgBuilder.buildNotifyAllInMeetingEvtMsg(

View File

@ -288,6 +288,8 @@ class ReceivedJsonMsgHandlerActor(
routeGenericMsg[PreuploadedPresentationsSysPubMsg](envelope, jsonNode) routeGenericMsg[PreuploadedPresentationsSysPubMsg](envelope, jsonNode)
case PresentationUploadedFileTooLargeErrorSysPubMsg.NAME => case PresentationUploadedFileTooLargeErrorSysPubMsg.NAME =>
routeGenericMsg[PresentationUploadedFileTooLargeErrorSysPubMsg](envelope, jsonNode) routeGenericMsg[PresentationUploadedFileTooLargeErrorSysPubMsg](envelope, jsonNode)
case PresentationHasInvalidMimeTypeErrorSysPubMsg.NAME =>
routeGenericMsg[PresentationHasInvalidMimeTypeErrorSysPubMsg](envelope, jsonNode)
case PresentationUploadedFileTimeoutErrorSysPubMsg.NAME => case PresentationUploadedFileTimeoutErrorSysPubMsg.NAME =>
routeGenericMsg[PresentationUploadedFileTimeoutErrorSysPubMsg](envelope, jsonNode) routeGenericMsg[PresentationUploadedFileTimeoutErrorSysPubMsg](envelope, jsonNode)
case PresentationConversionUpdateSysPubMsg.NAME => case PresentationConversionUpdateSysPubMsg.NAME =>

View File

@ -523,6 +523,7 @@ class MeetingActor(
case m: SetPresentationDownloadablePubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) case m: SetPresentationDownloadablePubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
case m: PresentationConversionUpdateSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) case m: PresentationConversionUpdateSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
case m: PresentationUploadedFileTooLargeErrorSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) case m: PresentationUploadedFileTooLargeErrorSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
case m: PresentationHasInvalidMimeTypeErrorSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
case m: PresentationUploadedFileTimeoutErrorSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) case m: PresentationUploadedFileTimeoutErrorSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
case m: PresentationPageGeneratedSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) case m: PresentationPageGeneratedSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
case m: PresentationPageCountErrorSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) case m: PresentationPageCountErrorSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)

View File

@ -85,6 +85,7 @@ case class PresentationSlide(
presentationId: String, presentationId: String,
pageNum: Long, pageNum: Long,
setOn: Long = System.currentTimeMillis(), setOn: Long = System.currentTimeMillis(),
presentationName: String,
) )
@ -189,7 +190,7 @@ class LearningDashboardActor(
for { for {
page <- msg.body.presentation.pages.find(p => p.current == true) page <- msg.body.presentation.pages.find(p => p.current == true)
} yield { } yield {
this.setPresentationSlide(meeting.intId, msg.body.presentation.id,page.num) this.setPresentationSlide(meeting.intId, msg.body.presentation.id,page.num, msg.body.presentation.name)
} }
} }
} }
@ -202,7 +203,7 @@ class LearningDashboardActor(
presentation <- presentations.get(msg.body.presentationId) presentation <- presentations.get(msg.body.presentationId)
page <- presentation.pages.find(p => p.id == msg.body.pageId) page <- presentation.pages.find(p => p.id == msg.body.pageId)
} yield { } yield {
this.setPresentationSlide(meeting.intId, msg.body.presentationId,page.num) this.setPresentationSlide(meeting.intId, msg.body.presentationId,page.num, presentation.name)
} }
} }
@ -211,7 +212,7 @@ class LearningDashboardActor(
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId) meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)
} yield { } yield {
if(meeting.presentationSlides.last.presentationId == msg.body.presentationId) { if(meeting.presentationSlides.last.presentationId == msg.body.presentationId) {
this.setPresentationSlide(meeting.intId, "",0) this.setPresentationSlide(meeting.intId, "",0, "")
} }
} }
} }
@ -223,7 +224,7 @@ class LearningDashboardActor(
val presPreviousSlides: Vector[PresentationSlide] = meeting.presentationSlides.filter(p => p.presentationId == msg.body.presentationId); val presPreviousSlides: Vector[PresentationSlide] = meeting.presentationSlides.filter(p => p.presentationId == msg.body.presentationId);
if(presPreviousSlides.length > 0) { if(presPreviousSlides.length > 0) {
//Set last page showed for this presentation //Set last page showed for this presentation
this.setPresentationSlide(meeting.intId, msg.body.presentationId,presPreviousSlides.last.pageNum) this.setPresentationSlide(meeting.intId, msg.body.presentationId,presPreviousSlides.last.pageNum, presPreviousSlides.last.presentationName)
} else { } else {
//If none page was showed yet, set the current page (page 1 by default) //If none page was showed yet, set the current page (page 1 by default)
for { for {
@ -231,20 +232,20 @@ class LearningDashboardActor(
presentation <- presentations.get(msg.body.presentationId) presentation <- presentations.get(msg.body.presentationId)
page <- presentation.pages.find(s => s.current == true) page <- presentation.pages.find(s => s.current == true)
} yield { } yield {
this.setPresentationSlide(meeting.intId, msg.body.presentationId,page.num) this.setPresentationSlide(meeting.intId, msg.body.presentationId,page.num, presentation.name)
} }
} }
} }
} }
private def setPresentationSlide(meetingId: String, presentationId: String, pageNum: Long) { private def setPresentationSlide(meetingId: String, presentationId: String, pageNum: Long, presentationName: String) {
for { for {
meeting <- meetings.values.find(m => m.intId == meetingId) meeting <- meetings.values.find(m => m.intId == meetingId)
} yield { } yield {
if (meeting.presentationSlides.length == 0 || if (meeting.presentationSlides.length == 0 ||
meeting.presentationSlides.last.presentationId != presentationId || meeting.presentationSlides.last.presentationId != presentationId ||
meeting.presentationSlides.last.pageNum != pageNum) { meeting.presentationSlides.last.pageNum != pageNum) {
val updatedMeeting = meeting.copy(presentationSlides = meeting.presentationSlides :+ PresentationSlide(presentationId, pageNum)) val updatedMeeting = meeting.copy(presentationSlides = meeting.presentationSlides :+ PresentationSlide(presentationId, pageNum, presentationName = presentationName))
meetings += (updatedMeeting.intId -> updatedMeeting) meetings += (updatedMeeting.intId -> updatedMeeting)
} }

View File

@ -18,7 +18,7 @@ val compileSettings = Seq(
"-Xlint", "-Xlint",
"-Ywarn-dead-code", "-Ywarn-dead-code",
"-language:_", "-language:_",
"-target:jvm-1.11", "-target:11",
"-encoding", "UTF-8" "-encoding", "UTF-8"
), ),
javacOptions ++= List( javacOptions ++= List(
@ -27,7 +27,7 @@ val compileSettings = Seq(
) )
) )
scalaVersion := "2.13.4" scalaVersion := "2.13.9"
resolvers += Resolver.sonatypeRepo("releases") resolvers += Resolver.sonatypeRepo("releases")

View File

@ -7,7 +7,7 @@ object Dependencies {
object Versions { object Versions {
// Scala // Scala
val scala = "2.13.4" val scala = "2.13.9"
val junitInterface = "0.11" val junitInterface = "0.11"
val scalactic = "3.0.8" val scalactic = "3.0.8"
@ -21,8 +21,8 @@ object Dependencies {
val codec = "1.15" val codec = "1.15"
// BigBlueButton // BigBlueButton
val bbbCommons = "0.0.21-SNAPSHOT" val bbbCommons = "0.0.22-SNAPSHOT"
val bbbFsesl = "0.0.8-SNAPSHOT" val bbbFsesl = "0.0.9-SNAPSHOT"
// Test // Test
val scalaTest = "3.2.11" val scalaTest = "3.2.11"

View File

@ -1,7 +1,7 @@
import org.bigbluebutton.build._ import org.bigbluebutton.build._
version := "0.0.21-SNAPSHOT" version := "0.0.22-SNAPSHOT"
val compileSettings = Seq( val compileSettings = Seq(
organization := "org.bigbluebutton", organization := "org.bigbluebutton",
@ -12,7 +12,7 @@ val compileSettings = Seq(
"-Xlint", "-Xlint",
"-Ywarn-dead-code", "-Ywarn-dead-code",
"-language:_", "-language:_",
"-target:jvm-1.11", "-target:11",
"-encoding", "UTF-8" "-encoding", "UTF-8"
), ),
javacOptions ++= List( javacOptions ++= List(
@ -55,7 +55,7 @@ scalariformAutoformat := true
// Do not append Scala versions to the generated artifacts // Do not append Scala versions to the generated artifacts
//crossPaths := false //crossPaths := false
scalaVersion := "2.13.4" scalaVersion := "2.13.9"
// This forbids including Scala related libraries into the dependency // This forbids including Scala related libraries into the dependency
//autoScalaLibrary := false //autoScalaLibrary := false

View File

@ -7,7 +7,7 @@ object Dependencies {
object Versions { object Versions {
// Scala // Scala
val scala = "2.13.4" val scala = "2.13.9"
val junit = "4.12" val junit = "4.12"
val junitInterface = "0.11" val junitInterface = "0.11"
val scalactic = "3.0.8" val scalactic = "3.0.8"

View File

@ -9,16 +9,16 @@ case class DurationProps(duration: Int, createdTime: Long, createdDate: String,
endWhenNoModerator: Boolean, endWhenNoModeratorDelayInMinutes: Int) endWhenNoModerator: Boolean, endWhenNoModeratorDelayInMinutes: Int)
case class MeetingProp( case class MeetingProp(
name: String, name: String,
extId: String, extId: String,
intId: String, intId: String,
meetingCameraCap: Int, meetingCameraCap: Int,
maxPinnedCameras: Int, maxPinnedCameras: Int,
isBreakout: Boolean, isBreakout: Boolean,
disabledFeatures: Vector[String], disabledFeatures: Vector[String],
notifyRecordingIsOn: Boolean, notifyRecordingIsOn: Boolean,
uploadExternalDescription: String, presentationUploadExternalDescription: String,
uploadExternalUrl: String, presentationUploadExternalUrl: String,
) )
case class BreakoutProps( case class BreakoutProps(

View File

@ -166,6 +166,23 @@ case class PresentationUploadedFileTooLargeErrorSysPubMsgBody(
maxFileSize: Int maxFileSize: Int
) )
object PresentationHasInvalidMimeTypeErrorSysPubMsg { val NAME = "PresentationHasInvalidMimeTypeErrorSysPubMsg" }
case class PresentationHasInvalidMimeTypeErrorSysPubMsg(
header: BbbClientMsgHeader,
body: PresentationHasInvalidMimeTypeErrorSysPubMsgBody
) extends StandardMsg
case class PresentationHasInvalidMimeTypeErrorSysPubMsgBody(
podId: String,
meetingId: String,
presentationName: String,
temporaryPresentationId: String,
presentationId: String,
messageKey: String,
fileMime: String,
fileExtension: String,
)
object PresentationUploadedFileTimeoutErrorSysPubMsg { val NAME = "PresentationUploadedFileTimeoutErrorSysPubMsg" } object PresentationUploadedFileTimeoutErrorSysPubMsg { val NAME = "PresentationUploadedFileTimeoutErrorSysPubMsg" }
case class PresentationUploadedFileTimeoutErrorSysPubMsg( case class PresentationUploadedFileTimeoutErrorSysPubMsg(
header: BbbClientMsgHeader, header: BbbClientMsgHeader,
@ -237,6 +254,13 @@ object PresentationUploadedFileTooLargeErrorEvtMsg { val NAME = "PresentationUpl
case class PresentationUploadedFileTooLargeErrorEvtMsg(header: BbbClientMsgHeader, body: PresentationUploadedFileTooLargeErrorEvtMsgBody) extends BbbCoreMsg case class PresentationUploadedFileTooLargeErrorEvtMsg(header: BbbClientMsgHeader, body: PresentationUploadedFileTooLargeErrorEvtMsgBody) extends BbbCoreMsg
case class PresentationUploadedFileTooLargeErrorEvtMsgBody(podId: String, messageKey: String, code: String, presentationName: String, presentationToken: String, fileSize: Int, maxFileSize: Int) case class PresentationUploadedFileTooLargeErrorEvtMsgBody(podId: String, messageKey: String, code: String, presentationName: String, presentationToken: String, fileSize: Int, maxFileSize: Int)
object PresentationHasInvalidMimeTypeErrorEvtMsg { val NAME = "PresentationHasInvalidMimeTypeErrorEvtMsg" }
case class PresentationHasInvalidMimeTypeErrorEvtMsg(header: BbbClientMsgHeader, body: PresentationHasInvalidMimeTypeErrorEvtMsgBody) extends BbbCoreMsg
case class PresentationHasInvalidMimeTypeErrorEvtMsgBody(podId: String, meetingId: String, presentationName: String,
temporaryPresentationId: String, presentationId: String,
messageKey: String, fileMime: String, fileExtension: String,
)
object PresentationUploadedFileTimeoutErrorEvtMsg { val NAME = "PresentationUploadedFileTimeoutErrorEvtMsg" } object PresentationUploadedFileTimeoutErrorEvtMsg { val NAME = "PresentationUploadedFileTimeoutErrorEvtMsg" }
case class PresentationUploadedFileTimeoutErrorEvtMsg(header: BbbClientMsgHeader, body: PresentationUploadedFileTimeoutErrorEvtMsgBody) extends BbbCoreMsg case class PresentationUploadedFileTimeoutErrorEvtMsg(header: BbbClientMsgHeader, body: PresentationUploadedFileTimeoutErrorEvtMsgBody) extends BbbCoreMsg
case class PresentationUploadedFileTimeoutErrorEvtMsgBody(podId: String, meetingId: String, presentationName: String, case class PresentationUploadedFileTimeoutErrorEvtMsgBody(podId: String, meetingId: String, presentationName: String,

View File

@ -11,7 +11,7 @@ val compileSettings = Seq(
"-Xlint", "-Xlint",
"-Ywarn-dead-code", "-Ywarn-dead-code",
"-language:_", "-language:_",
"-target:jvm-1.11", "-target:11",
"-encoding", "UTF-8" "-encoding", "UTF-8"
), ),
javacOptions ++= List( javacOptions ++= List(
@ -40,7 +40,7 @@ lazy val commonWeb = (project in file(".")).settings(name := "bbb-common-web", l
// Config file is in ./.scalariform.conf // Config file is in ./.scalariform.conf
scalariformAutoformat := true scalariformAutoformat := true
scalaVersion := "2.13.4" scalaVersion := "2.13.9"
//----------- //-----------
// Packaging // Packaging
// //

View File

@ -7,7 +7,7 @@ object Dependencies {
object Versions { object Versions {
// Scala // Scala
val scala = "2.13.4" val scala = "2.13.9"
val junit = "4.12" val junit = "4.12"
val junitInterface = "0.11" val junitInterface = "0.11"
val scalactic = "3.0.8" val scalactic = "3.0.8"
@ -34,7 +34,7 @@ object Dependencies {
val text = "1.10.0" val text = "1.10.0"
// BigBlueButton // BigBlueButton
val bbbCommons = "0.0.21-SNAPSHOT" val bbbCommons = "0.0.22-SNAPSHOT"
// Test // Test
val scalaTest = "3.2.11" val scalaTest = "3.2.11"

View File

@ -73,8 +73,8 @@ public class ApiParams {
public static final String DISABLED_FEATURES = "disabledFeatures"; public static final String DISABLED_FEATURES = "disabledFeatures";
public static final String NOTIFY_RECORDING_IS_ON = "notifyRecordingIsOn"; public static final String NOTIFY_RECORDING_IS_ON = "notifyRecordingIsOn";
public static final String UPLOAD_EXTERNAL_DESCRIPTION = "uploadExternalDescription"; public static final String PRESENTATION_UPLOAD_EXTERNAL_DESCRIPTION = "presentationUploadExternalDescription";
public static final String UPLOAD_EXTERNAL_URL = "uploadExternalUrl"; public static final String PRESENTATION_UPLOAD_EXTERNAL_URL = "presentationUploadExternalUrl";
public static final String BREAKOUT_ROOMS_CAPTURE_SLIDES = "breakoutRoomsCaptureSlides"; public static final String BREAKOUT_ROOMS_CAPTURE_SLIDES = "breakoutRoomsCaptureSlides";
public static final String BREAKOUT_ROOMS_CAPTURE_NOTES = "breakoutRoomsCaptureNotes"; public static final String BREAKOUT_ROOMS_CAPTURE_NOTES = "breakoutRoomsCaptureNotes";

View File

@ -419,7 +419,7 @@ public class MeetingService implements MessageListener {
m.getMuteOnStart(), m.getAllowModsToUnmuteUsers(), m.getAllowModsToEjectCameras(), m.getMeetingKeepEvents(), m.getMuteOnStart(), m.getAllowModsToUnmuteUsers(), m.getAllowModsToEjectCameras(), m.getMeetingKeepEvents(),
m.breakoutRoomsParams, m.lockSettingsParams, m.getHtml5InstanceId(), m.breakoutRoomsParams, m.lockSettingsParams, m.getHtml5InstanceId(),
m.getGroups(), m.getDisabledFeatures(), m.getNotifyRecordingIsOn(), m.getGroups(), m.getDisabledFeatures(), m.getNotifyRecordingIsOn(),
m.getUploadExternalDescription(), m.getUploadExternalUrl()); m.getPresentationUploadExternalDescription(), m.getPresentationUploadExternalUrl());
} }
private String formatPrettyDate(Long timestamp) { private String formatPrettyDate(Long timestamp) {

View File

@ -47,6 +47,7 @@ import org.bigbluebutton.api.domain.BreakoutRoomsParams;
import org.bigbluebutton.api.domain.LockSettingsParams; import org.bigbluebutton.api.domain.LockSettingsParams;
import org.bigbluebutton.api.domain.Meeting; import org.bigbluebutton.api.domain.Meeting;
import org.bigbluebutton.api.domain.Group; import org.bigbluebutton.api.domain.Group;
import org.bigbluebutton.api.service.ServiceUtils;
import org.bigbluebutton.api.util.ParamsUtil; import org.bigbluebutton.api.util.ParamsUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -80,6 +81,7 @@ public class ParamsProcessorUtil {
private String defaultHTML5ClientUrl; private String defaultHTML5ClientUrl;
private String defaultGuestWaitURL; private String defaultGuestWaitURL;
private Boolean allowRequestsWithoutSession = false; private Boolean allowRequestsWithoutSession = false;
private Integer defaultHttpSessionTimeout = 14400;
private Boolean useDefaultAvatar = false; private Boolean useDefaultAvatar = false;
private String defaultAvatarURL; private String defaultAvatarURL;
private String defaultGuestPolicy; private String defaultGuestPolicy;
@ -103,8 +105,8 @@ public class ParamsProcessorUtil {
private boolean defaultKeepEvents = false; private boolean defaultKeepEvents = false;
private Boolean useDefaultLogo; private Boolean useDefaultLogo;
private String defaultLogoURL; private String defaultLogoURL;
private String defaultUploadExternalDescription = ""; private String defaultPresentationUploadExternalDescription = "";
private String defaultUploadExternalUrl = ""; private String defaultPresentationUploadExternalUrl = "";
private boolean defaultBreakoutRoomsEnabled = true; private boolean defaultBreakoutRoomsEnabled = true;
private boolean defaultBreakoutRoomsRecord; private boolean defaultBreakoutRoomsRecord;
@ -638,14 +640,14 @@ public class ParamsProcessorUtil {
guestPolicy = params.get(ApiParams.GUEST_POLICY); guestPolicy = params.get(ApiParams.GUEST_POLICY);
} }
String uploadExternalDescription = defaultUploadExternalDescription; String presentationUploadExternalDescription = defaultPresentationUploadExternalDescription;
if (!StringUtils.isEmpty(params.get(ApiParams.UPLOAD_EXTERNAL_DESCRIPTION))) { if (!StringUtils.isEmpty(params.get(ApiParams.PRESENTATION_UPLOAD_EXTERNAL_DESCRIPTION))) {
uploadExternalDescription = params.get(ApiParams.UPLOAD_EXTERNAL_DESCRIPTION); presentationUploadExternalDescription = params.get(ApiParams.PRESENTATION_UPLOAD_EXTERNAL_DESCRIPTION);
} }
String uploadExternalUrl = defaultUploadExternalUrl; String presentationUploadExternalUrl = defaultPresentationUploadExternalUrl;
if (!StringUtils.isEmpty(params.get(ApiParams.UPLOAD_EXTERNAL_URL))) { if (!StringUtils.isEmpty(params.get(ApiParams.PRESENTATION_UPLOAD_EXTERNAL_URL))) {
uploadExternalUrl = params.get(ApiParams.UPLOAD_EXTERNAL_URL); presentationUploadExternalUrl = params.get(ApiParams.PRESENTATION_UPLOAD_EXTERNAL_URL);
} }
String meetingLayout = defaultMeetingLayout; String meetingLayout = defaultMeetingLayout;
@ -685,7 +687,7 @@ public class ParamsProcessorUtil {
String parentMeetingId = ""; String parentMeetingId = "";
if (isBreakout) { if (isBreakout) {
internalMeetingId = params.get(ApiParams.MEETING_ID); internalMeetingId = params.get(ApiParams.MEETING_ID);
parentMeetingId = params.get(ApiParams.PARENT_MEETING_ID); parentMeetingId = ServiceUtils.findMeetingFromMeetingID(params.get(ApiParams.PARENT_MEETING_ID)).getInternalId();
// We rebuild the the external meeting using the has of the parent // We rebuild the the external meeting using the has of the parent
// meeting, the shared timestamp and the sequence number // meeting, the shared timestamp and the sequence number
String timeStamp = StringUtils.substringAfter(internalMeetingId, "-"); String timeStamp = StringUtils.substringAfter(internalMeetingId, "-");
@ -736,8 +738,8 @@ public class ParamsProcessorUtil {
.withGroups(groups) .withGroups(groups)
.withDisabledFeatures(listOfDisabledFeatures) .withDisabledFeatures(listOfDisabledFeatures)
.withNotifyRecordingIsOn(notifyRecordingIsOn) .withNotifyRecordingIsOn(notifyRecordingIsOn)
.withUploadExternalDescription(uploadExternalDescription) .withPresentationUploadExternalDescription(presentationUploadExternalDescription)
.withUploadExternalUrl(uploadExternalUrl) .withPresentationUploadExternalUrl(presentationUploadExternalUrl)
.build(); .build();
if (!StringUtils.isEmpty(params.get(ApiParams.MODERATOR_ONLY_MESSAGE))) { if (!StringUtils.isEmpty(params.get(ApiParams.MODERATOR_ONLY_MESSAGE))) {
@ -845,6 +847,14 @@ public class ParamsProcessorUtil {
return allowRequestsWithoutSession; return allowRequestsWithoutSession;
} }
public Integer getDefaultHttpSessionTimeout() {
return defaultHttpSessionTimeout;
}
public void setDefaultHttpSessionTimeout(Integer value) {
this.defaultHttpSessionTimeout = value;
}
public String getDefaultLogoutUrl() { public String getDefaultLogoutUrl() {
if ((StringUtils.isEmpty(defaultLogoutUrl)) || "default".equalsIgnoreCase(defaultLogoutUrl)) { if ((StringUtils.isEmpty(defaultLogoutUrl)) || "default".equalsIgnoreCase(defaultLogoutUrl)) {
return defaultServerUrl; return defaultServerUrl;
@ -1444,12 +1454,12 @@ public class ParamsProcessorUtil {
this.defaultNotifyRecordingIsOn = notifyRecordingIsOn; this.defaultNotifyRecordingIsOn = notifyRecordingIsOn;
} }
public void setUploadExternalDescription(String uploadExternalDescription) { public void setPresentationUploadExternalDescription(String presentationUploadExternalDescription) {
this.defaultUploadExternalDescription = uploadExternalDescription; this.defaultPresentationUploadExternalDescription = presentationUploadExternalDescription;
} }
public void setUploadExternalUrl(String uploadExternalUrl) { public void setPresentationUploadExternalUrl(String presentationUploadExternalUrl) {
this.defaultUploadExternalUrl = uploadExternalUrl; this.defaultPresentationUploadExternalUrl = presentationUploadExternalUrl;
} }
public void setBbbVersion(String version) { public void setBbbVersion(String version) {

View File

@ -97,8 +97,8 @@ public class Meeting {
private Boolean allowRequestsWithoutSession = false; private Boolean allowRequestsWithoutSession = false;
private Boolean allowModsToEjectCameras = false; private Boolean allowModsToEjectCameras = false;
private Boolean meetingKeepEvents; private Boolean meetingKeepEvents;
private String uploadExternalDescription; private String presentationUploadExternalDescription;
private String uploadExternalUrl; private String presentationUploadExternalUrl;
private Integer meetingExpireIfNoUserJoinedInMinutes = 5; private Integer meetingExpireIfNoUserJoinedInMinutes = 5;
private Integer meetingExpireWhenLastUserLeftInMinutes = 1; private Integer meetingExpireWhenLastUserLeftInMinutes = 1;
@ -123,8 +123,8 @@ public class Meeting {
intMeetingId = builder.internalId; intMeetingId = builder.internalId;
disabledFeatures = builder.disabledFeatures; disabledFeatures = builder.disabledFeatures;
notifyRecordingIsOn = builder.notifyRecordingIsOn; notifyRecordingIsOn = builder.notifyRecordingIsOn;
uploadExternalDescription = builder.uploadExternalDescription; presentationUploadExternalDescription = builder.presentationUploadExternalDescription;
uploadExternalUrl = builder.uploadExternalUrl; presentationUploadExternalUrl = builder.presentationUploadExternalUrl;
if (builder.viewerPass == null){ if (builder.viewerPass == null){
viewerPass = ""; viewerPass = "";
} else { } else {
@ -424,11 +424,11 @@ public class Meeting {
return notifyRecordingIsOn; return notifyRecordingIsOn;
} }
public String getUploadExternalDescription() { public String getPresentationUploadExternalDescription() {
return uploadExternalDescription; return presentationUploadExternalDescription;
} }
public String getUploadExternalUrl() { public String getPresentationUploadExternalUrl() {
return uploadExternalUrl; return presentationUploadExternalUrl;
} }
public String getWelcomeMessageTemplate() { public String getWelcomeMessageTemplate() {
@ -863,8 +863,8 @@ public class Meeting {
private String learningDashboardAccessToken; private String learningDashboardAccessToken;
private ArrayList<String> disabledFeatures; private ArrayList<String> disabledFeatures;
private Boolean notifyRecordingIsOn; private Boolean notifyRecordingIsOn;
private String uploadExternalDescription; private String presentationUploadExternalDescription;
private String uploadExternalUrl; private String presentationUploadExternalUrl;
private int duration; private int duration;
private String webVoice; private String webVoice;
private String telVoice; private String telVoice;
@ -993,13 +993,13 @@ public class Meeting {
return this; return this;
} }
public Builder withUploadExternalDescription(String d) { public Builder withPresentationUploadExternalDescription(String d) {
this.uploadExternalDescription = d; this.presentationUploadExternalDescription = d;
return this; return this;
} }
public Builder withUploadExternalUrl(String u) { public Builder withPresentationUploadExternalUrl(String u) {
this.uploadExternalUrl = u; this.presentationUploadExternalUrl = u;
return this; return this;
} }

View File

@ -25,8 +25,8 @@ public class CreateMeetingMessage {
public final String learningDashboardAccessToken; public final String learningDashboardAccessToken;
public final ArrayList<String> disabledFeatures; public final ArrayList<String> disabledFeatures;
public final Boolean notifyRecordingIsOn; public final Boolean notifyRecordingIsOn;
public final String uploadExternalDescription; public final String presentationUploadExternalDescription;
public final String uploadExternalUrl; public final String presentationUploadExternalUrl;
public final Long createTime; public final Long createTime;
public final String createDate; public final String createDate;
public final Map<String, String> metadata; public final Map<String, String> metadata;
@ -38,8 +38,8 @@ public class CreateMeetingMessage {
String viewerPass, String learningDashboardAccessToken, String viewerPass, String learningDashboardAccessToken,
ArrayList<String> disabledFeatures, ArrayList<String> disabledFeatures,
Boolean notifyRecordingIsOn, Boolean notifyRecordingIsOn,
String uploadExternalDescription, String presentationUploadExternalDescription,
String uploadExternalUrl, String presentationUploadExternalUrl,
Long createTime, String createDate, Map<String, String> metadata) { Long createTime, String createDate, Map<String, String> metadata) {
this.id = id; this.id = id;
this.externalId = externalId; this.externalId = externalId;
@ -58,8 +58,8 @@ public class CreateMeetingMessage {
this.learningDashboardAccessToken = learningDashboardAccessToken; this.learningDashboardAccessToken = learningDashboardAccessToken;
this.disabledFeatures = disabledFeatures; this.disabledFeatures = disabledFeatures;
this.notifyRecordingIsOn = notifyRecordingIsOn; this.notifyRecordingIsOn = notifyRecordingIsOn;
this.uploadExternalDescription = uploadExternalDescription; this.presentationUploadExternalDescription = presentationUploadExternalDescription;
this.uploadExternalUrl = uploadExternalUrl; this.presentationUploadExternalUrl = presentationUploadExternalUrl;
this.createTime = createTime; this.createTime = createTime;
this.createDate = createDate; this.createDate = createDate;
this.metadata = metadata; this.metadata = metadata;

View File

@ -18,7 +18,7 @@ public class ParamsUtil {
public static final String INVALID_CHARS = ","; public static final String INVALID_CHARS = ",";
public static String stripControlChars(String text) { public static String stripControlChars(String text) {
return text.replaceAll("\\p{Cc}", ""); return text.replaceAll("\\p{Cc}", "").trim();
} }
public static String escapeHTMLTags(String value) { public static String escapeHTMLTags(String value) {

View File

@ -43,8 +43,8 @@ public interface IBbbWebApiGWApp {
ArrayList<Group> groups, ArrayList<Group> groups,
ArrayList<String> disabledFeatures, ArrayList<String> disabledFeatures,
Boolean notifyRecordingIsOn, Boolean notifyRecordingIsOn,
String uploadExternalDescription, String presentationUploadExternalDescription,
String uploadExternalUrl); String presentationUploadExternalUrl);
void registerUser(String meetingID, String internalUserId, String fullname, String role, void registerUser(String meetingID, String internalUserId, String fullname, String role,
String externUserID, String authToken, String avatarURL, String externUserID, String authToken, String avatarURL,

View File

@ -21,4 +21,5 @@ package org.bigbluebutton.presentation;
public interface DocumentConversionService { public interface DocumentConversionService {
void processDocument(UploadedPresentation pres); void processDocument(UploadedPresentation pres);
void sendDocConversionFailedOnMimeType(UploadedPresentation pres, String fileMime, String fileExtension);
} }

View File

@ -19,15 +19,19 @@
package org.bigbluebutton.presentation; package org.bigbluebutton.presentation;
import java.io.File;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.bigbluebutton.api2.IBbbWebApiGWApp; import org.bigbluebutton.api2.IBbbWebApiGWApp;
import org.bigbluebutton.presentation.imp.*; import org.bigbluebutton.presentation.imp.*;
import org.bigbluebutton.presentation.messages.DocConversionRequestReceived; import org.bigbluebutton.presentation.messages.DocConversionRequestReceived;
import org.bigbluebutton.presentation.messages.DocInvalidMimeType;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.google.gson.Gson; import com.google.gson.Gson;
import static org.bigbluebutton.presentation.Util.deleteDirectoryFromFileHandlingErrors;
public class DocumentConversionServiceImp implements DocumentConversionService { public class DocumentConversionServiceImp implements DocumentConversionService {
private static Logger log = LoggerFactory.getLogger(DocumentConversionServiceImp.class); private static Logger log = LoggerFactory.getLogger(DocumentConversionServiceImp.class);
@ -93,6 +97,9 @@ public class DocumentConversionServiceImp implements DocumentConversionService {
} }
} else { } else {
File presentationFile = pres.getUploadedFile();
deleteDirectoryFromFileHandlingErrors(presentationFile);
Map<String, Object> logData = new HashMap<String, Object>(); Map<String, Object> logData = new HashMap<String, Object>();
logData = new HashMap<String, Object>(); logData = new HashMap<String, Object>();
logData.put("podId", pres.getPodId()); logData.put("podId", pres.getPodId());
@ -124,6 +131,11 @@ public class DocumentConversionServiceImp implements DocumentConversionService {
} }
} }
public void sendDocConversionFailedOnMimeType(UploadedPresentation pres, String fileMime,
String fileExtension) {
notifier.sendInvalidMimeTypeMessage(pres, fileMime, fileExtension);
}
private void sendDocConversionRequestReceived(UploadedPresentation pres) { private void sendDocConversionRequestReceived(UploadedPresentation pres) {
if (! pres.isConversionStarted()) { if (! pres.isConversionStarted()) {
Map<String, Object> logData = new HashMap<String, Object>(); Map<String, Object> logData = new HashMap<String, Object>();

View File

@ -39,5 +39,6 @@ public final class FileTypeConstants {
public static final String JPG = "jpg"; public static final String JPG = "jpg";
public static final String JPEG = "jpeg"; public static final String JPEG = "jpeg";
public static final String PNG = "png"; public static final String PNG = "png";
public static final String SVG = "svg";
private FileTypeConstants() {} // Prevent instantiation private FileTypeConstants() {} // Prevent instantiation
} }

View File

@ -0,0 +1,67 @@
package org.bigbluebutton.presentation;
import java.util.*;
import static org.bigbluebutton.presentation.FileTypeConstants.*;
public class MimeTypeUtils {
private static final String XLS = "application/vnd.ms-excel";
private static final String XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
private static final String DOC = "application/msword";
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 ODT = "application/vnd.oasis.opendocument.text";
private static final String RTF = "application/rtf";
private static final String TXT = "text/plain";
private static final String ODS = "application/vnd.oasis.opendocument.spreadsheet";
private static final String ODP = "application/vnd.oasis.opendocument.presentation";
private static final String PDF = "application/pdf";
private static final String JPEG = "image/jpeg";
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) {
{
// 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);
}
};
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;
}
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
);
return validMimeTypes;
}
}

View File

@ -19,15 +19,25 @@
package org.bigbluebutton.presentation; package org.bigbluebutton.presentation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.bigbluebutton.presentation.FileTypeConstants.*; import static org.bigbluebutton.presentation.FileTypeConstants.*;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Collections; import java.util.Collections;
@SuppressWarnings("serial") @SuppressWarnings("serial")
public final class SupportedFileTypes { public final class SupportedFileTypes {
private static Logger log = LoggerFactory.getLogger(SupportedFileTypes.class);
private static MimeTypeUtils mimeTypeUtils = new MimeTypeUtils();
private static final List<String> SUPPORTED_FILE_LIST = Collections.unmodifiableList(new ArrayList<String>(15) { private static final List<String> SUPPORTED_FILE_LIST = Collections.unmodifiableList(new ArrayList<String>(15) {
{ {
// Add all the supported files // Add all the supported files
@ -76,4 +86,56 @@ public final class SupportedFileTypes {
public static boolean isImageFile(String fileExtension) { public static boolean isImageFile(String fileExtension) {
return IMAGE_FILE_LIST.contains(fileExtension.toLowerCase()); 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) {
String mimeType = "";
if (pres != null && pres.isFile()){
try {
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) {
mimeType = output.toString().trim();
} else {
log.error("Error while executing command {} for file {}, error: {}",
process.toString(), pres.getAbsolutePath(), process.getErrorStream());
}
} catch (IOException e) {
log.error("Could not read file [{}]", pres.getAbsolutePath(), e.getMessage());
} catch (InterruptedException e) {
log.error("Flow interrupted for file [{}]", pres.getAbsolutePath(), e.getMessage());
}
}
return mimeType;
}
public static Boolean isPresentationMimeTypeValid(File pres, String fileExtension) {
String mimeType = detectMimeType(pres);
if(mimeType == null || mimeType == "") return false;
if(!mimeTypeUtils.getValidMimeTypes().contains(mimeType)) return false;
if(!mimeTypeUtils.extensionMatchMimeType(mimeType, fileExtension)) {
log.error("File with extension [{}] doesn't match with mimeType [{}].", fileExtension, mimeType);
return false;
}
return true;
}
} }

View File

@ -19,10 +19,15 @@
package org.bigbluebutton.presentation; package org.bigbluebutton.presentation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File; import java.io.File;
import java.nio.file.Path;
public final class Util { public final class Util {
private static Logger log = LoggerFactory.getLogger(Util.class);
public static void deleteDirectory(File directory) { public static void deleteDirectory(File directory) {
/** /**
* Go through each directory and check if it's not empty. * Go through each directory and check if it's not empty.
@ -40,4 +45,20 @@ public final class Util {
// Now that the directory is empty. Delete it. // Now that the directory is empty. Delete it.
directory.delete(); directory.delete();
} }
public static void deleteDirectoryFromFileHandlingErrors(File presentationFile) {
if ( presentationFile != null ){
Path presDir = presentationFile.toPath().getParent();
try {
File presFileDir = new File(presDir.toString());
if (presFileDir.exists()) {
deleteDirectory(presFileDir);
}
} catch (Exception ex) {
log.error("Error while trying to delete directory {}", presDir.toString(), ex);
}
}
}
} }

View File

@ -35,6 +35,8 @@ import org.slf4j.LoggerFactory;
import com.google.gson.Gson; import com.google.gson.Gson;
import static org.bigbluebutton.presentation.Util.deleteDirectoryFromFileHandlingErrors;
public abstract class Office2PdfPageConverter { public abstract class Office2PdfPageConverter {
private static Logger log = LoggerFactory.getLogger(Office2PdfPageConverter.class); private static Logger log = LoggerFactory.getLogger(Office2PdfPageConverter.class);
@ -95,6 +97,7 @@ public abstract class Office2PdfPageConverter {
return false; return false;
} }
} catch (Exception e) { } catch (Exception e) {
deleteDirectoryFromFileHandlingErrors(presentationFile);
Map<String, Object> logData = new HashMap<>(); Map<String, Object> logData = new HashMap<>();
logData.put("meetingId", pres.getMeetingId()); logData.put("meetingId", pres.getMeetingId());
logData.put("presId", pres.getId()); logData.put("presId", pres.getId());

View File

@ -50,6 +50,20 @@ public class SlidesGenerationProgressNotifier {
maxUploadFileSize); maxUploadFileSize);
messagingService.sendDocConversionMsg(progress); messagingService.sendDocConversionMsg(progress);
} }
public void sendInvalidMimeTypeMessage(UploadedPresentation pres, String fileMime, String fileExtension) {
DocInvalidMimeType invalidMimeType = new DocInvalidMimeType(
pres.getPodId(),
pres.getMeetingId(),
pres.getId(),
pres.getTemporaryPresentationId(),
pres.getName(),
pres.getAuthzToken(),
"IVALID_MIME_TYPE",
fileMime,
fileExtension
);
messagingService.sendDocConversionMsg(invalidMimeType);
}
public void sendUploadFileTimedout(UploadedPresentation pres, int page) { public void sendUploadFileTimedout(UploadedPresentation pres, int page) {
UploadFileTimedoutMessage errorMessage = new UploadFileTimedoutMessage( UploadFileTimedoutMessage errorMessage = new UploadFileTimedoutMessage(
pres.getPodId(), pres.getPodId(),

View File

@ -0,0 +1,34 @@
package org.bigbluebutton.presentation.messages;
public class DocInvalidMimeType implements IDocConversionMsg{
public final String podId;
public final String meetingId;
public final String presId;
public final String temporaryPresentationId;
public final String filename;
public final String authzToken;
public final String messageKey;
public final String fileMime;
public final String fileExtension;
public DocInvalidMimeType( String podId,
String meetingId,
String presId,
String temporaryPresentationId,
String filename,
String authzToken,
String messageKey,
String fileMime,
String fileExtension) {
this.podId = podId;
this.meetingId = meetingId;
this.presId = presId;
this.temporaryPresentationId = temporaryPresentationId;
this.filename = filename;
this.authzToken = authzToken;
this.messageKey = messageKey;
this.fileMime = fileMime;
this.fileExtension = fileExtension;
}
}

View File

@ -150,8 +150,8 @@ class BbbWebApiGWApp(
groups: java.util.ArrayList[Group], groups: java.util.ArrayList[Group],
disabledFeatures: java.util.ArrayList[String], disabledFeatures: java.util.ArrayList[String],
notifyRecordingIsOn: java.lang.Boolean, notifyRecordingIsOn: java.lang.Boolean,
uploadExternalDescription: String, presentationUploadExternalDescription: String,
uploadExternalUrl: String): Unit = { presentationUploadExternalUrl: String): Unit = {
val disabledFeaturesAsVector: Vector[String] = disabledFeatures.asScala.toVector val disabledFeaturesAsVector: Vector[String] = disabledFeatures.asScala.toVector
@ -164,8 +164,8 @@ class BbbWebApiGWApp(
isBreakout = isBreakout.booleanValue(), isBreakout = isBreakout.booleanValue(),
disabledFeaturesAsVector, disabledFeaturesAsVector,
notifyRecordingIsOn, notifyRecordingIsOn,
uploadExternalDescription, presentationUploadExternalDescription,
uploadExternalUrl presentationUploadExternalUrl
) )
val durationProps = DurationProps( val durationProps = DurationProps(
@ -346,6 +346,9 @@ class BbbWebApiGWApp(
} else if (msg.isInstanceOf[UploadFileTimedoutMessage]) { } else if (msg.isInstanceOf[UploadFileTimedoutMessage]) {
val event = MsgBuilder.buildPresentationUploadedFileTimedoutErrorSysMsg(msg.asInstanceOf[UploadFileTimedoutMessage]) val event = MsgBuilder.buildPresentationUploadedFileTimedoutErrorSysMsg(msg.asInstanceOf[UploadFileTimedoutMessage])
msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event)) msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event))
} else if (msg.isInstanceOf[DocInvalidMimeType]) {
val event = MsgBuilder.buildPresentationHasInvalidMimeType(msg.asInstanceOf[DocInvalidMimeType])
msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event))
} }
} }

View File

@ -285,6 +285,19 @@ object MsgBuilder {
BbbCommonEnvCoreMsg(envelope, req) BbbCommonEnvCoreMsg(envelope, req)
} }
def buildPresentationHasInvalidMimeType(msg: DocInvalidMimeType): BbbCommonEnvCoreMsg = {
val routing = collection.immutable.HashMap("sender" -> "bbb-web")
val envelope = BbbCoreEnvelope(PresentationHasInvalidMimeTypeErrorSysPubMsg.NAME, routing)
val header = BbbClientMsgHeader(PresentationHasInvalidMimeTypeErrorSysPubMsg.NAME, msg.meetingId, "not-used")
val body = PresentationHasInvalidMimeTypeErrorSysPubMsgBody(podId = msg.podId, presentationName = msg.filename,
temporaryPresentationId = msg.temporaryPresentationId, presentationId = msg.presId, meetingId = msg.meetingId,
messageKey = msg.messageKey, fileMime = msg.fileMime, fileExtension = msg.fileExtension)
val req = PresentationHasInvalidMimeTypeErrorSysPubMsg(header, body)
BbbCommonEnvCoreMsg(envelope, req)
}
def buildPresentationUploadedFileTimedoutErrorSysMsg(msg: UploadFileTimedoutMessage): BbbCommonEnvCoreMsg = { def buildPresentationUploadedFileTimedoutErrorSysMsg(msg: UploadFileTimedoutMessage): BbbCommonEnvCoreMsg = {
val routing = collection.immutable.HashMap("sender" -> "bbb-web") val routing = collection.immutable.HashMap("sender" -> "bbb-web")
val envelope = BbbCoreEnvelope(PresentationUploadedFileTimeoutErrorSysPubMsg.NAME, routing) val envelope = BbbCoreEnvelope(PresentationUploadedFileTimeoutErrorSysPubMsg.NAME, routing)

View File

@ -0,0 +1 @@
engine-strict=true

View File

@ -1,3 +1,13 @@
let _ = require('lodash');
let fs = require('fs');
const settings = require('./settings'); const settings = require('./settings');
const LOCAL_SETTINGS_FILE_PATH = '/etc/bigbluebutton/bbb-export-annotations.json';
const config = settings; const config = settings;
if (fs.existsSync(LOCAL_SETTINGS_FILE_PATH)) {
const local_config = JSON.parse(fs.readFileSync(LOCAL_SETTINGS_FILE_PATH));
_.mergeWith(config, local_config, (a, b) => (_.isArray(b) ? b : undefined));
}
module.exports = config; module.exports = config;

View File

@ -26,6 +26,7 @@
"msgName": "NewPresAnnFileAvailableMsg" "msgName": "NewPresAnnFileAvailableMsg"
}, },
"bbbWebAPI": "http://127.0.0.1:8090", "bbbWebAPI": "http://127.0.0.1:8090",
"bbbWebPublicAPI": "/bigbluebutton/",
"bbbPadsAPI": "http://127.0.0.1:9002", "bbbPadsAPI": "http://127.0.0.1:9002",
"redis": { "redis": {
"host": "127.0.0.1", "host": "127.0.0.1",

View File

@ -1,13 +1,13 @@
const config = require('../../config'); const config = require('../../config');
const { level } = config.log; const {level} = config.log;
const trace = level.toLowerCase() === 'trace'; const trace = level.toLowerCase() === 'trace';
const debug = trace || level.toLowerCase() === 'debug'; const debug = trace || level.toLowerCase() === 'debug';
const date = () => new Date().toISOString(); const date = () => new Date().toISOString();
const parse = (messages) => { const parse = (messages) => {
return messages.map(message => { return messages.map((message) => {
if (typeof message === 'object') return JSON.stringify(message); if (typeof message === 'object') return JSON.stringify(message);
return message; return message;

View File

@ -0,0 +1,88 @@
const config = require('../../config');
const EXPORT_STATUSES = Object.freeze({
COLLECTING: 'COLLECTING',
PROCESSING: 'PROCESSING',
});
class PresAnnStatusMsg {
constructor(exportJob, status = EXPORT_STATUSES.COLLECTING) {
this.message = {
envelope: {
name: config.log.msgName,
routing: {
sender: exportJob.module,
},
timestamp: (new Date()).getTime(),
},
core: {
header: {
name: config.log.msgName,
meetingId: exportJob.parentMeetingId,
userId: '',
},
body: {
presId: exportJob.presId,
pageNumber: 1,
totalPages: JSON.parse(exportJob.pages).length,
status,
error: false,
},
},
};
}
build = (pageNumber = 1) => {
this.message.core.body.pageNumber = pageNumber;
this.message.envelope.timestamp = (new Date()).getTime();
const event = JSON.stringify(this.message);
this.message.core.body.error = false;
return event;
};
setError = (error = true) => {
this.message.core.body.error = error;
};
setStatus = (status) => {
this.message.core.body.status = status;
};
static get EXPORT_STATUSES() {
return EXPORT_STATUSES;
}
};
class NewPresAnnFileAvailableMsg {
constructor(exportJob, link) {
this.message = {
envelope: {
name: config.notifier.msgName,
routing: {
sender: exportJob.module,
},
timestamp: (new Date()).getTime(),
},
core: {
header: {
name: config.notifier.msgName,
meetingId: exportJob.parentMeetingId,
userId: '',
},
body: {
fileURI: link,
presId: exportJob.presId,
},
},
};
}
build = () => {
return JSON.stringify(this.message);
};
};
module.exports = {
PresAnnStatusMsg,
NewPresAnnFileAvailableMsg,
};

View File

@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"axios": "^0.26.0", "axios": "^0.26.0",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"lodash": "^4.17.21",
"perfect-freehand": "^1.0.16", "perfect-freehand": "^1.0.16",
"probe-image-size": "^7.2.3", "probe-image-size": "^7.2.3",
"redis": "^4.0.3", "redis": "^4.0.3",
@ -19,6 +20,10 @@
"devDependencies": { "devDependencies": {
"eslint": "^8.20.0", "eslint": "^8.20.0",
"eslint-config-google": "^0.14.0" "eslint-config-google": "^0.14.0"
},
"engines": {
"node": "^16.16.0",
"npm": "^8.5.0"
} }
}, },
"node_modules/@eslint/eslintrc": { "node_modules/@eslint/eslintrc": {
@ -920,6 +925,11 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.merge": { "node_modules/lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@ -2044,6 +2054,11 @@
"type-check": "~0.4.0" "type-check": "~0.4.0"
} }
}, },
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash.merge": { "lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",

View File

@ -9,6 +9,7 @@
"dependencies": { "dependencies": {
"axios": "^0.26.0", "axios": "^0.26.0",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"lodash": "^4.17.21",
"perfect-freehand": "^1.0.16", "perfect-freehand": "^1.0.16",
"probe-image-size": "^7.2.3", "probe-image-size": "^7.2.3",
"redis": "^4.0.3", "redis": "^4.0.3",
@ -18,5 +19,9 @@
"devDependencies": { "devDependencies": {
"eslint": "^8.20.0", "eslint": "^8.20.0",
"eslint-config-google": "^0.14.0" "eslint-config-google": "^0.14.0"
},
"engines": {
"node": "^16.16.0",
"npm": "^8.5.0"
} }
} }

View File

@ -8,6 +8,7 @@ const redis = require('redis');
const sanitize = require('sanitize-filename'); const sanitize = require('sanitize-filename');
const stream = require('stream'); const stream = require('stream');
const WorkerStarter = require('../lib/utils/worker-starter'); const WorkerStarter = require('../lib/utils/worker-starter');
const {PresAnnStatusMsg} = require('../lib/utils/message-builder');
const {workerData} = require('worker_threads'); const {workerData} = require('worker_threads');
const {promisify} = require('util'); const {promisify} = require('util');
@ -55,29 +56,7 @@ async function collectAnnotationsFromRedis() {
const pdfFile = `${presFile}.pdf`; const pdfFile = `${presFile}.pdf`;
// Message to display conversion progress toast // Message to display conversion progress toast
const statusUpdate = { const statusUpdate = new PresAnnStatusMsg(exportJob);
envelope: {
name: config.log.msgName,
routing: {
sender: exportJob.module,
},
timestamp: (new Date()).getTime(),
},
core: {
header: {
name: config.log.msgName,
meetingId: exportJob.parentMeetingId,
userId: '',
},
body: {
presId: exportJob.presId,
pageNumber: 1,
totalPages: pages.length,
status: 'COLLECTING',
error: false,
},
},
};
if (fs.existsSync(pdfFile)) { if (fs.existsSync(pdfFile)) {
for (const p of pages) { for (const p of pages) {
@ -103,35 +82,32 @@ async function collectAnnotationsFromRedis() {
try { try {
cp.spawnSync(config.shared.pdftocairo, extract_png_from_pdf, {shell: false}); cp.spawnSync(config.shared.pdftocairo, extract_png_from_pdf, {shell: false});
} catch (error) { } catch (error) {
const error_reason = `PDFtoCairo failed extracting slide ${pageNumber}`; logger.error(`PDFtoCairo failed extracting slide ${pageNumber} in job ${jobId}: ${error.message}`);
logger.error(`${error_reason} in job ${jobId}: ${error.message}`); statusUpdate.setError();
statusUpdate.core.body.status = error_reason;
statusUpdate.core.body.error = true;
} }
statusUpdate.core.body.pageNumber = pageNumber; await client.publish(config.redis.channels.publish, statusUpdate.build(pageNumber));
statusUpdate.envelope.timestamp = (new Date()).getTime();
await client.publish(config.redis.channels.publish, JSON.stringify(statusUpdate));
statusUpdate.core.body.error = false;
} }
// If PNG file already available
} else if (fs.existsSync(`${presFile}.png`)) {
fs.copyFileSync(`${presFile}.png`, path.join(dropbox, 'slide1.png'));
await client.publish(config.redis.channels.publish, JSON.stringify(statusUpdate));
// If JPEG file available
} else if (fs.existsSync(`${presFile}.jpeg`)) {
fs.copyFileSync(`${presFile}.jpeg`, path.join(dropbox, 'slide1.jpeg'));
await client.publish(config.redis.channels.publish, JSON.stringify(statusUpdate));
} else { } else {
statusUpdate.core.body.error = true; if (fs.existsSync(`${presFile}.png`)) {
await client.publish(config.redis.channels.publish, JSON.stringify(statusUpdate)); // PNG file available
client.disconnect(); fs.copyFileSync(`${presFile}.png`, path.join(dropbox, 'slide1.png'));
return logger.error(`Presentation file missing for job ${exportJob.jobId}`); } else if (fs.existsSync(`${presFile}.jpeg`)) {
// JPEG file available
fs.copyFileSync(`${presFile}.jpeg`, path.join(dropbox, 'slide1.jpeg'));
await client.publish(config.redis.channels.publish, statusUpdate.build());
} else {
await client.publish(config.redis.channels.publish, statusUpdate.build());
client.disconnect();
return logger.error(`No PDF, PNG or JPEG file available for job ${jobId}`);
}
await client.publish(config.redis.channels.publish, statusUpdate.build());
} }
client.disconnect(); client.disconnect();
const process = new WorkerStarter({jobId, statusUpdate}); const process = new WorkerStarter({jobId});
process.process(); process.process();
} }

View File

@ -5,6 +5,7 @@ const FormData = require('form-data');
const redis = require('redis'); const redis = require('redis');
const axios = require('axios').default; const axios = require('axios').default;
const path = require('path'); const path = require('path');
const {NewPresAnnFileAvailableMsg} = require('../lib/utils/message-builder');
const {workerData} = require('worker_threads'); const {workerData} = require('worker_threads');
const [jobType, jobId, filename] = [workerData.jobType, workerData.jobId, workerData.filename]; const [jobType, jobId, filename] = [workerData.jobType, workerData.jobId, workerData.filename];
@ -27,34 +28,14 @@ async function notifyMeetingActor() {
await client.connect(); await client.connect();
client.on('error', (err) => logger.info('Redis Client Error', err)); client.on('error', (err) => logger.info('Redis Client Error', err));
const link = path.join(`${path.sep}bigbluebutton`, 'presentation', const link = config.bbbWebPublicAPI + path.join('presentation',
exportJob.parentMeetingId, exportJob.parentMeetingId, exportJob.parentMeetingId, exportJob.parentMeetingId,
exportJob.presId, 'pdf', jobId, filename); exportJob.presId, 'pdf', jobId, filename);
const notification = { const notification = new NewPresAnnFileAvailableMsg(exportJob, link);
envelope: {
name: config.notifier.msgName,
routing: {
sender: exportJob.module,
},
timestamp: (new Date()).getTime(),
},
core: {
header: {
name: config.notifier.msgName,
meetingId: exportJob.parentMeetingId,
userId: '',
},
body: {
fileURI: link,
presId: exportJob.presId,
},
},
};
logger.info(`Annotated PDF available at ${link}`); logger.info(`Annotated PDF available at ${link}`);
await client.publish(config.redis.channels.publish, await client.publish(config.redis.channels.publish, notification.build());
JSON.stringify(notification));
client.disconnect(); client.disconnect();
} }

View File

@ -10,14 +10,16 @@ const sanitize = require('sanitize-filename');
const {getStrokePoints, getStrokeOutlinePoints} = require('perfect-freehand'); const {getStrokePoints, getStrokeOutlinePoints} = require('perfect-freehand');
const probe = require('probe-image-size'); const probe = require('probe-image-size');
const redis = require('redis'); const redis = require('redis');
const {PresAnnStatusMsg} = require('../lib/utils/message-builder');
const [jobId, statusUpdate] = [workerData.jobId, workerData.statusUpdate]; const jobId = workerData.jobId;
const logger = new Logger('presAnn Process Worker'); const logger = new Logger('presAnn Process Worker');
logger.info('Processing PDF for job ' + jobId); logger.info('Processing PDF for job ' + jobId);
statusUpdate.core.body.status = 'PROCESSING';
const dropbox = path.join(config.shared.presAnnDropboxDir, jobId); const dropbox = path.join(config.shared.presAnnDropboxDir, jobId);
const job = fs.readFileSync(path.join(dropbox, 'job'));
const exportJob = JSON.parse(job);
const statusUpdate = new PresAnnStatusMsg(exportJob, PresAnnStatusMsg.EXPORT_STATUSES.PROCESSING);
// General utilities for rendering SVGs resembling Tldraw as much as possible // General utilities for rendering SVGs resembling Tldraw as much as possible
function align_to_pango(alignment) { function align_to_pango(alignment) {
@ -157,10 +159,8 @@ function render_textbox(textColor, font, fontSize, textAlign, text, id, textBoxW
try { try {
cp.spawnSync(config.shared.imagemagick, commands, {shell: false}); cp.spawnSync(config.shared.imagemagick, commands, {shell: false});
} catch (error) { } catch (error) {
const error_reason = 'ImageMagick failed to render textbox'; logger.error(`ImageMagick failed to render textbox in job ${jobId}: ${error.message}`);
logger.error(`${error_reason} in job ${jobId}: ${error.message}`); statusUpdate.setError();
statusUpdate.core.body.status = error_reason;
statusUpdate.core.body.error = true;
} }
} }
@ -596,7 +596,7 @@ function overlay_shape_label(svg, annotation) {
const fontSize = text_size_to_px(annotation.style.size, annotation.style.scale); const fontSize = text_size_to_px(annotation.style.size, annotation.style.scale);
const textAlign = 'center'; const textAlign = 'center';
const text = annotation.label; const text = annotation.label;
const id = annotation.id; const id = sanitize(annotation.id);
const rotation = rad_to_degree(annotation.rotation); const rotation = rad_to_degree(annotation.rotation);
const [shape_width, shape_height] = annotation.size; const [shape_width, shape_height] = annotation.size;
@ -641,7 +641,7 @@ function overlay_sticky(svg, annotation) {
const textColor = '#0d0d0d'; // For sticky notes const textColor = '#0d0d0d'; // For sticky notes
const text = annotation.text; const text = annotation.text;
const id = annotation.id; const id = sanitize(annotation.id);
render_textbox(textColor, font, fontSize, textAlign, text, id, textBoxWidth); render_textbox(textColor, font, fontSize, textAlign, text, id, textBoxWidth);
@ -701,7 +701,7 @@ function overlay_text(svg, annotation) {
const fontSize = text_size_to_px(annotation.style.size, annotation.style.scale); const fontSize = text_size_to_px(annotation.style.size, annotation.style.scale);
const textAlign = align_to_pango(annotation.style.textAlign); const textAlign = align_to_pango(annotation.style.textAlign);
const text = annotation.text; const text = annotation.text;
const id = annotation.id; const id = sanitize(annotation.id);
const rotation = rad_to_degree(annotation.rotation); const rotation = rad_to_degree(annotation.rotation);
const [textBox_x, textBox_y] = annotation.point; const [textBox_x, textBox_y] = annotation.point;
@ -787,17 +787,13 @@ async function process_presentation_annotations() {
client.on('error', (err) => logger.info('Redis Client Error', err)); client.on('error', (err) => logger.info('Redis Client Error', err));
// 1. Get the job // Get the annotations
const job = fs.readFileSync(path.join(dropbox, 'job'));
const exportJob = JSON.parse(job);
// 2. Get the annotations
const annotations = fs.readFileSync(path.join(dropbox, 'whiteboard')); const annotations = fs.readFileSync(path.join(dropbox, 'whiteboard'));
const whiteboard = JSON.parse(annotations); const whiteboard = JSON.parse(annotations);
const pages = JSON.parse(whiteboard.pages); const pages = JSON.parse(whiteboard.pages);
const ghostScriptInput = []; const ghostScriptInput = [];
// 3. Convert annotations to SVG // Convert annotations to SVG
for (const currentSlide of pages) { for (const currentSlide of pages) {
const bgImagePath = path.join(dropbox, `slide${currentSlide.page}`); const bgImagePath = path.join(dropbox, `slide${currentSlide.page}`);
const svgBackgroundSlide = path.join(exportJob.presLocation, const svgBackgroundSlide = path.join(exportJob.presLocation,
@ -854,14 +850,7 @@ async function process_presentation_annotations() {
} }
}); });
// Dimensions converted to a pixel size which, // Scale slide back to its original size
// when converted to points, will yield the desired
// dimension in pixels when read without conversion
// e.g. say the background SVG dimensions are set to 1920x1080 pt
// Resize output to 2560x1440 px so that the SVG
// generates with the original size in pt.
const convertAnnotatedSlide = [ const convertAnnotatedSlide = [
SVGfile, SVGfile,
'--output-width', to_px(slideWidth), '--output-width', to_px(slideWidth),
@ -873,15 +862,11 @@ async function process_presentation_annotations() {
cp.spawnSync(config.shared.cairosvg, convertAnnotatedSlide, {shell: false}); cp.spawnSync(config.shared.cairosvg, convertAnnotatedSlide, {shell: false});
} catch (error) { } catch (error) {
logger.error(`Processing slide ${currentSlide.page} failed for job ${jobId}: ${error.message}`); logger.error(`Processing slide ${currentSlide.page} failed for job ${jobId}: ${error.message}`);
statusUpdate.core.body.error = true; statusUpdate.setError();
} }
statusUpdate.core.body.pageNumber = currentSlide.page; await client.publish(config.redis.channels.publish, statusUpdate.build(currentSlide.page));
statusUpdate.envelope.timestamp = (new Date()).getTime();
await client.publish(config.redis.channels.publish, JSON.stringify(statusUpdate));
ghostScriptInput.push(PDFfile); ghostScriptInput.push(PDFfile);
statusUpdate.core.body.error = false;
} }
// Create PDF output directory if it doesn't exist // Create PDF output directory if it doesn't exist
@ -903,11 +888,7 @@ async function process_presentation_annotations() {
try { try {
cp.spawnSync(config.shared.ghostscript, mergePDFs, {shell: false}); cp.spawnSync(config.shared.ghostscript, mergePDFs, {shell: false});
} catch (error) { } catch (error) {
const error_reason = 'GhostScript failed to merge PDFs'; return logger.error(`GhostScript failed to merge PDFs in job ${jobId}: ${error.message}`);
logger.error(`${error_reason} in job ${jobId}: ${error.message}`);
statusUpdate.core.body.status = error_reason;
statusUpdate.core.body.error = true;
await client.publish(config.redis.channels.publish, JSON.stringify(statusUpdate));
} }
// Launch Notifier Worker depending on job type // Launch Notifier Worker depending on job type

View File

@ -2,7 +2,7 @@ import org.bigbluebutton.build._
description := "BigBlueButton custom FS-ESL client built on top of FS-ESL Java library." description := "BigBlueButton custom FS-ESL client built on top of FS-ESL Java library."
version := "0.0.8-SNAPSHOT" version := "0.0.9-SNAPSHOT"
val compileSettings = Seq( val compileSettings = Seq(
organization := "org.bigbluebutton", organization := "org.bigbluebutton",
@ -13,7 +13,7 @@ val compileSettings = Seq(
"-Xlint", "-Xlint",
"-Ywarn-dead-code", "-Ywarn-dead-code",
"-language:_", "-language:_",
"-target:jvm-1.11", "-target:11",
"-encoding", "UTF-8" "-encoding", "UTF-8"
), ),
javacOptions ++= List( javacOptions ++= List(
@ -52,7 +52,7 @@ crossPaths := false
// This forbids including Scala related libraries into the dependency // This forbids including Scala related libraries into the dependency
autoScalaLibrary := false autoScalaLibrary := false
scalaVersion := "2.13.4" scalaVersion := "2.13.9"
publishTo := Some(Resolver.file("file", new File(Path.userHome.absolutePath + "/.m2/repository"))) publishTo := Some(Resolver.file("file", new File(Path.userHome.absolutePath + "/.m2/repository")))

View File

@ -7,7 +7,7 @@ object Dependencies {
object Versions { object Versions {
// Scala // Scala
val scala = "2.13.4" val scala = "2.13.9"
// Libraries // Libraries
val netty = "3.2.10.Final" val netty = "3.2.10.Final"

File diff suppressed because it is too large Load Diff

View File

@ -9,19 +9,14 @@
] ]
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.15.0",
"@emotion/react": "^11.10.5", "@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5", "@emotion/styled": "^11.10.5",
"@mui/material": "^5.10.13", "@mui/material": "^5.10.13",
"@mui/x-data-grid": "^5.17.10", "@mui/x-data-grid": "^5.17.10",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-intl": "^5.20.6", "react-intl": "^5.20.6",
"typescript": "^4.3.5", "react-scripts": "^5.0.0"
"web-vitals": "^1.1.2"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
@ -48,6 +43,7 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.15.0",
"autoprefixer": "^10.4.1", "autoprefixer": "^10.4.1",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-config-airbnb": "^18.2.1", "eslint-config-airbnb": "^18.2.1",
@ -57,7 +53,6 @@
"eslint-plugin-react": "^7.24.0", "eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-react-hooks": "^4.2.0",
"postcss": "^8.4.5", "postcss": "^8.4.5",
"react-scripts": "^5.0.0",
"tailwindcss": "^3.0.11" "tailwindcss": "^3.0.11"
} }
} }

View File

@ -1,11 +1,17 @@
import React from 'react'; import React from 'react';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import TabUnstyled from '@mui/base/TabUnstyled';
import TabsListUnstyled from '@mui/base/TabsListUnstyled';
import TabPanelUnstyled from '@mui/base/TabPanelUnstyled';
import TabsUnstyled from '@mui/base/TabsUnstyled';
import './App.css'; import './App.css';
import './bbb-icons.css'; import './bbb-icons.css';
import { import {
FormattedMessage, FormattedDate, injectIntl, FormattedTime, FormattedMessage, FormattedDate, injectIntl, FormattedTime,
} from 'react-intl'; } from 'react-intl';
import { emojiConfigs } from './services/EmojiService'; import { emojiConfigs } from './services/EmojiService';
import Card from './components/Card'; import CardBody from './components/Card';
import UsersTable from './components/UsersTable'; import UsersTable from './components/UsersTable';
import UserDetails from './components/UserDetails/component'; import UserDetails from './components/UserDetails/component';
import { UserDetailsContext } from './components/UserDetails/context'; import { UserDetailsContext } from './components/UserDetails/context';
@ -14,6 +20,13 @@ import PollsTable from './components/PollsTable';
import ErrorMessage from './components/ErrorMessage'; import ErrorMessage from './components/ErrorMessage';
import { makeUserCSVData, tsToHHmmss } from './services/UserService'; import { makeUserCSVData, tsToHHmmss } from './services/UserService';
const TABS = {
OVERVIEW: 0,
OVERVIEW_ACTIVITY_SCORE: 1,
TIMELINE: 2,
POLLING: 3,
};
class App extends React.Component { class App extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -21,7 +34,7 @@ class App extends React.Component {
loading: true, loading: true,
invalidSessionCount: 0, invalidSessionCount: 0,
activitiesJson: {}, activitiesJson: {},
tab: 'overview', tab: 0,
meetingId: '', meetingId: '',
learningDashboardAccessToken: '', learningDashboardAccessToken: '',
ldAccessTokenCopied: false, ldAccessTokenCopied: false,
@ -170,14 +183,13 @@ class App extends React.Component {
invalidSessionCount: 0, invalidSessionCount: 0,
lastUpdated: Date.now(), lastUpdated: Date.now(),
}); });
document.title = `Learning Dashboard - ${json.name}`;
this.updateModalUser(); this.updateModalUser();
}).catch(() => { }).catch(() => {
this.setState({ loading: false, invalidSessionCount: invalidSessionCount + 1 }); this.setState({ loading: false, invalidSessionCount: invalidSessionCount + 1 });
}); });
} else if (sessionToken !== '') { } else if (sessionToken !== '') {
const url = new URL('/bigbluebutton/api/learningDashboard', window.location); const url = new URL('/bigbluebutton/api/learningDashboard', window.location);
fetch(`${url}?sessionToken=${sessionToken}`) fetch(`${url}?sessionToken=${sessionToken}`, { credentials: 'include' })
.then((response) => response.json()) .then((response) => response.json())
.then((json) => { .then((json) => {
if (json.response.returncode === 'SUCCESS') { if (json.response.returncode === 'SUCCESS') {
@ -188,7 +200,6 @@ class App extends React.Component {
invalidSessionCount: 0, invalidSessionCount: 0,
lastUpdated: Date.now(), lastUpdated: Date.now(),
}); });
document.title = `Learning Dashboard - ${jsonData.name}`;
this.updateModalUser(); this.updateModalUser();
} else { } else {
// When meeting is ended the sessionToken stop working, check for new cookies // When meeting is ended the sessionToken stop working, check for new cookies
@ -215,7 +226,7 @@ class App extends React.Component {
} = this.state; } = this.state;
const { intl } = this.props; const { intl } = this.props;
document.title = `${intl.formatMessage({ id: 'app.learningDashboard.dashboardTitle', defaultMessage: 'Learning Dashboard' })} - ${activitiesJson.name}`; document.title = `${intl.formatMessage({ id: 'app.learningDashboard.bigbluebuttonTitle', defaultMessage: 'BigBlueButton' })} - ${intl.formatMessage({ id: 'app.learningDashboard.dashboardTitle', defaultMessage: 'Learning Analytics Dashboard' })} - ${activitiesJson.name}`;
function totalOfEmojis() { function totalOfEmojis() {
if (activitiesJson && activitiesJson.users) { if (activitiesJson && activitiesJson.users) {
@ -311,6 +322,11 @@ class App extends React.Component {
if (loading === false && getErrorMessage() !== '') return <ErrorMessage message={getErrorMessage()} />; if (loading === false && getErrorMessage() !== '') return <ErrorMessage message={getErrorMessage()} />;
const usersCount = Object.values(activitiesJson.users || {})
.filter((u) => activitiesJson.endedOn > 0
|| Object.values(u.intIds)[Object.values(u.intIds).length - 1].leftOn === 0)
.length;
return ( return (
<div className="mx-10"> <div className="mx-10">
<div className="flex flex-col sm:flex-row items-start justify-between pb-3"> <div className="flex flex-col sm:flex-row items-start justify-between pb-3">
@ -366,147 +382,180 @@ class App extends React.Component {
</div> </div>
</div> </div>
<div className="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4"> <TabsUnstyled
<div aria-hidden="true" className="cursor-pointer" onClick={() => { this.setState({ tab: 'overview' }); }}> defaultValue={0}
<Card onChange={(e, v) => {
name={ this.setState({ tab: v });
activitiesJson.endedOn === 0 }}
? intl.formatMessage({ id: 'app.learningDashboard.indicators.usersOnline', defaultMessage: 'Active Users' }) >
: intl.formatMessage({ id: 'app.learningDashboard.indicators.usersTotal', defaultMessage: 'Total Number Of Users' }) <TabsListUnstyled className="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
} <TabUnstyled className="rounded focus:outline-none focus:ring focus:ring-pink-500 ring-offset-2">
number={Object <Card>
.values(activitiesJson.users || {}) <CardContent classes={{ root: '!p-0' }}>
.filter((u) => activitiesJson.endedOn > 0 <CardBody
|| Object.values(u.intIds)[Object.values(u.intIds).length - 1].leftOn === 0) name={
.length} activitiesJson.endedOn === 0
cardClass={tab === 'overview' ? 'border-pink-500' : 'hover:border-pink-500 border-white'} ? intl.formatMessage({ id: 'app.learningDashboard.indicators.usersOnline', defaultMessage: 'Active Users' })
iconClass="bg-pink-50 text-pink-500" : intl.formatMessage({ id: 'app.learningDashboard.indicators.usersTotal', defaultMessage: 'Total Number Of Users' })
onClick={() => { }
this.setState({ tab: 'overview' }); number={usersCount}
}} cardClass={tab === TABS.OVERVIEW ? 'border-pink-500' : 'hover:border-pink-500 border-white'}
> iconClass="bg-pink-50 text-pink-500"
<svg >
xmlns="http://www.w3.org/2000/svg" <svg
className="h-6 w-6" xmlns="http://www.w3.org/2000/svg"
fill="none" className="h-6 w-6"
viewBox="0 0 24 24" fill="none"
stroke="currentColor" viewBox="0 0 24 24"
> stroke="currentColor"
<path >
strokeLinecap="round" <path
strokeLinejoin="round" strokeLinecap="round"
strokeWidth="2" strokeLinejoin="round"
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" strokeWidth="2"
/> d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
</svg> />
</Card> </svg>
</div> </CardBody>
<div aria-hidden="true" className="cursor-pointer" onClick={() => { this.setState({ tab: 'overview_activityscore' }); }}> </CardContent>
<Card </Card>
name={intl.formatMessage({ id: 'app.learningDashboard.indicators.activityScore', defaultMessage: 'Activity Score' })} </TabUnstyled>
number={intl.formatNumber((getAverageActivityScore() || 0), { <TabUnstyled className="rounded focus:outline-none focus:ring focus:ring-green-500 ring-offset-2">
minimumFractionDigits: 0, <Card>
maximumFractionDigits: 1, <CardContent classes={{ root: '!p-0' }}>
})} <CardBody
cardClass={tab === 'overview_activityscore' ? 'border-green-500' : 'hover:border-green-500 border-white'} name={intl.formatMessage({ id: 'app.learningDashboard.indicators.activityScore', defaultMessage: 'Activity Score' })}
iconClass="bg-green-200 text-green-500" number={intl.formatNumber((getAverageActivityScore() || 0), {
> minimumFractionDigits: 0,
<svg maximumFractionDigits: 1,
xmlns="http://www.w3.org/2000/svg" })}
className="h-6 w-6" cardClass={tab === TABS.OVERVIEW_ACTIVITY_SCORE ? 'border-green-500' : 'hover:border-green-500 border-white'}
fill="none" iconClass="bg-green-200 text-green-500"
viewBox="0 0 24 24" >
stroke="currentColor" <svg
> xmlns="http://www.w3.org/2000/svg"
<path className="h-6 w-6"
strokeLinecap="round" fill="none"
strokeLinejoin="round" viewBox="0 0 24 24"
strokeWidth="2" stroke="currentColor"
d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z" >
/> <path
<path strokeLinecap="round"
strokeLinecap="round" strokeLinejoin="round"
strokeLinejoin="round" strokeWidth="2"
strokeWidth="2" d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z"
d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z" />
/> <path
</svg> strokeLinecap="round"
</Card> strokeLinejoin="round"
</div> strokeWidth="2"
<div aria-hidden="true" className="cursor-pointer" onClick={() => { this.setState({ tab: 'status_timeline' }); }}> d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z"
<Card />
name={intl.formatMessage({ id: 'app.learningDashboard.indicators.timeline', defaultMessage: 'Timeline' })} </svg>
number={totalOfEmojis()} </CardBody>
cardClass={tab === 'status_timeline' ? 'border-purple-500' : 'hover:border-purple-500 border-white'} </CardContent>
iconClass="bg-purple-200 text-purple-500" </Card>
> </TabUnstyled>
{this.fetchMostUsedEmojis()} <TabUnstyled className="rounded focus:outline-none focus:ring focus:ring-purple-500 ring-offset-2">
</Card> <Card>
</div> <CardContent classes={{ root: '!p-0' }}>
<div aria-hidden="true" className="cursor-pointer" onClick={() => { this.setState({ tab: 'polling' }); }}> <CardBody
<Card name={intl.formatMessage({ id: 'app.learningDashboard.indicators.timeline', defaultMessage: 'Timeline' })}
name={intl.formatMessage({ id: 'app.learningDashboard.indicators.polls', defaultMessage: 'Polls' })} number={totalOfEmojis()}
number={Object.values(activitiesJson.polls || {}).length} cardClass={tab === TABS.TIMELINE ? 'border-purple-500' : 'hover:border-purple-500 border-white'}
cardClass={tab === 'polling' ? 'border-blue-500' : 'hover:border-blue-500 border-white'} iconClass="bg-purple-200 text-purple-500"
iconClass="bg-blue-100 text-blue-500" >
> {this.fetchMostUsedEmojis()}
<svg </CardBody>
xmlns="http://www.w3.org/2000/svg" </CardContent>
className="h-6 w-6" </Card>
fill="none" </TabUnstyled>
viewBox="0 0 24 24" <TabUnstyled className="rounded focus:outline-none focus:ring focus:ring-blue-500 ring-offset-2">
stroke="currentColor" <Card>
> <CardContent classes={{ root: '!p-0' }}>
<path <CardBody
strokeLinecap="round" name={intl.formatMessage({ id: 'app.learningDashboard.indicators.polls', defaultMessage: 'Polls' })}
strokeLinejoin="round" number={Object.values(activitiesJson.polls || {}).length}
strokeWidth="2" cardClass={tab === TABS.POLLING ? 'border-blue-500' : 'hover:border-blue-500 border-white'}
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" iconClass="bg-blue-100 text-blue-500"
/> >
</svg> <svg
</Card> xmlns="http://www.w3.org/2000/svg"
</div> className="h-6 w-6"
</div> fill="none"
<h1 className="block my-2 pr-2 text-xl font-semibold"> viewBox="0 0 24 24"
{ tab === 'overview' || tab === 'overview_activityscore' stroke="currentColor"
? <FormattedMessage id="app.learningDashboard.usersTable.title" defaultMessage="Overview" /> >
: null } <path
{ tab === 'status_timeline' strokeLinecap="round"
? <FormattedMessage id="app.learningDashboard.statusTimelineTable.title" defaultMessage="Timeline" /> strokeLinejoin="round"
: null } strokeWidth="2"
{ tab === 'polling' d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"
? <FormattedMessage id="app.learningDashboard.pollsTable.title" defaultMessage="Polls" /> />
: null } </svg>
</h1> </CardBody>
<div className="w-full overflow-hidden rounded-md shadow-xs border-2 border-gray-100"> </CardContent>
<div className="w-full overflow-x-auto"> </Card>
{ (tab === 'overview' || tab === 'overview_activityscore') </TabUnstyled>
? ( </TabsListUnstyled>
<TabPanelUnstyled value={0}>
<h2 className="block my-2 pr-2 text-xl font-semibold">
<FormattedMessage id="app.learningDashboard.usersTable.title" defaultMessage="Overview" />
</h2>
<div className="w-full overflow-hidden rounded-md shadow-xs border-2 border-gray-100">
<div className="w-full overflow-x-auto">
<UsersTable <UsersTable
allUsers={activitiesJson.users} allUsers={activitiesJson.users}
totalOfActivityTime={totalOfActivity()} totalOfActivityTime={totalOfActivity()}
totalOfPolls={Object.values(activitiesJson.polls || {}).length} totalOfPolls={Object.values(activitiesJson.polls || {}).length}
tab={tab} tab="overview"
/> />
) </div>
: null } </div>
{ (tab === 'status_timeline') </TabPanelUnstyled>
? ( <TabPanelUnstyled value={1}>
<h2 className="block my-2 pr-2 text-xl font-semibold">
<FormattedMessage id="app.learningDashboard.usersTable.title" defaultMessage="Overview" />
</h2>
<div className="w-full overflow-hidden rounded-md shadow-xs border-2 border-gray-100">
<div className="w-full overflow-x-auto">
<UsersTable
allUsers={activitiesJson.users}
totalOfActivityTime={totalOfActivity()}
totalOfPolls={Object.values(activitiesJson.polls || {}).length}
tab="overview_activityscore"
/>
</div>
</div>
</TabPanelUnstyled>
<TabPanelUnstyled value={2}>
<h2 className="block my-2 pr-2 text-xl font-semibold">
<FormattedMessage id="app.learningDashboard.statusTimelineTable.title" defaultMessage="Timeline" />
</h2>
<div className="w-full overflow-hidden rounded-md shadow-xs border-2 border-gray-100">
<div className="w-full overflow-x-auto">
<StatusTable <StatusTable
allUsers={activitiesJson.users} allUsers={activitiesJson.users}
slides={activitiesJson.presentationSlides} slides={activitiesJson.presentationSlides}
meetingId={activitiesJson.intId} meetingId={activitiesJson.intId}
/> />
) </div>
: null } </div>
{ tab === 'polling' </TabPanelUnstyled>
? <PollsTable polls={activitiesJson.polls} allUsers={activitiesJson.users} /> <TabPanelUnstyled value={3}>
: null } <h2 className="block my-2 pr-2 text-xl font-semibold">
<UserDetails dataJson={activitiesJson} /> <FormattedMessage id="app.learningDashboard.pollsTable.title" defaultMessage="Polls" />
</div> </h2>
</div> <div className="w-full overflow-hidden rounded-md shadow-xs border-2 border-gray-100">
<div className="w-full overflow-x-auto">
<PollsTable polls={activitiesJson.polls} allUsers={activitiesJson.users} />
</div>
</div>
</TabPanelUnstyled>
</TabsUnstyled>
<UserDetails dataJson={activitiesJson} />
<hr className="my-8" /> <hr className="my-8" />
<div className="flex justify-between pb-8 text-xs text-gray-700 dark:text-gray-400 whitespace-nowrap flex-col sm:flex-row"> <div className="flex justify-between pb-8 text-xs text-gray-800 dark:text-gray-400 whitespace-nowrap flex-col sm:flex-row">
<div className="flex flex-col justify-center mb-4 sm:mb-0"> <div className="flex flex-col justify-center mb-4 sm:mb-0">
<p> <p>
{ {

View File

@ -1,8 +0,0 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@ -40,7 +40,7 @@ function Card(props) {
+ ` ${cardClass}` + ` ${cardClass}`
} }
> >
<div className="w-70"> <div className="w-70 text-left rtl:text-right">
<p className="text-lg font-semibold text-gray-700"> <p className="text-lg font-semibold text-gray-700">
{ number } { number }
</p> </p>

View File

@ -12,9 +12,9 @@ function ErrorMessage(props) {
clipRule="evenodd" clipRule="evenodd"
/> />
</svg> </svg>
<h1 className="text-xl font-semibold text-gray-700 dark:text-gray-200"> <h3 className="text-xl font-semibold text-gray-700 dark:text-gray-200">
{message} {message}
</h1> </h3>
</div> </div>
); );
} }

View File

@ -375,7 +375,7 @@ const PollsTable = (props) => {
sx={{ sx={{
'& .MuiDataGrid-columnHeaders': { '& .MuiDataGrid-columnHeaders': {
backgroundColor: 'rgb(243 244 246/var(--tw-bg-opacity))', backgroundColor: 'rgb(243 244 246/var(--tw-bg-opacity))',
color: 'rgb(107 114 128/1)', color: 'rgb(55 65 81/1)',
textTransform: 'uppercase', textTransform: 'uppercase',
letterSpacing: '.025em', letterSpacing: '.025em',
minHeight: '40.5px !important', minHeight: '40.5px !important',

View File

@ -1,8 +1,27 @@
import React from 'react'; import React from 'react';
import { FormattedMessage, injectIntl } from 'react-intl'; import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
import { emojiConfigs, filterUserEmojis } from '../services/EmojiService'; import { emojiConfigs, filterUserEmojis } from '../services/EmojiService';
import UserAvatar from './UserAvatar'; import UserAvatar from './UserAvatar';
const intlMessages = defineMessages({
thumbnail: {
id: 'app.learningDashboard.statusTimelineTable.thumbnail',
defaultMessage: 'Presentation thumbnail',
},
presentation: {
id: 'app.learningDashboard.statusTimelineTable.presentation',
defaultMessage: 'Presentation',
},
pageNumber: {
id: 'app.learningDashboard.statusTimelineTable.pageNumber',
defaultMessage: 'Page',
},
setAt: {
id: 'app.learningDashboard.statusTimelineTable.setAt',
defaultMessage: 'Set at',
},
});
class StatusTable extends React.Component { class StatusTable extends React.Component {
componentDidMount() { componentDidMount() {
// This code is needed to prevent emojis from overflowing. // This code is needed to prevent emojis from overflowing.
@ -162,7 +181,7 @@ class StatusTable extends React.Component {
return ( return (
<table className="w-full"> <table className="w-full">
<thead> <thead>
<tr className="text-xs font-semibold tracking-wide text-gray-500 uppercase border-b bg-gray-100"> <tr className="text-xs font-semibold tracking-wide text-gray-700 uppercase border-b bg-gray-100">
<th className={`z-30 bg-inherit px-4 py-3 col-text-left sticky ${isRTL ? 'right-0' : 'left-0'}`}> <th className={`z-30 bg-inherit px-4 py-3 col-text-left sticky ${isRTL ? 'right-0' : 'left-0'}`}>
<FormattedMessage id="app.learningDashboard.user" defaultMessage="User" /> <FormattedMessage id="app.learningDashboard.user" defaultMessage="User" />
</th> </th>
@ -190,7 +209,7 @@ class StatusTable extends React.Component {
const { slide, start, end } = period; const { slide, start, end } = period;
const padding = isRTL ? 'paddingLeft' : 'paddingRight'; const padding = isRTL ? 'paddingLeft' : 'paddingRight';
const URLPrefix = `/bigbluebutton/presentation/${meetingId}/${meetingId}`; const URLPrefix = `/bigbluebutton/presentation/${meetingId}/${meetingId}`;
const { presentationId, pageNum } = slide || {}; const { presentationId, pageNum, presentationName } = slide || {};
return ( return (
<td <td
style={{ style={{
@ -199,29 +218,28 @@ class StatusTable extends React.Component {
> >
{ slide && ( { slide && (
<div className="flex"> <div className="flex">
<div <div className="my-4">
className="my-4"
aria-label={tsToHHmmss(start - periods[0].start)}
>
<a <a
href={`${URLPrefix}/${presentationId}/svg/${pageNum}`} href={`${URLPrefix}/${presentationId}/svg/${pageNum}`}
className="block border-2 border-gray-300" className="block border-2 border-gray-300"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
aria-describedby={`thumb-desc-${presentationId}`}
> >
<img <img
src={`${URLPrefix}/${presentationId}/thumbnail/${pageNum}`} src={`${URLPrefix}/${presentationId}/thumbnail/${pageNum}`}
alt={intl.formatMessage({ alt={`${intl.formatMessage(intlMessages.thumbnail)} - ${intl.formatMessage(intlMessages.presentation)} ${presentationName} - ${intl.formatMessage(intlMessages.pageNumber)} ${pageNum}`}
id: 'app.learningDashboard.statusTimelineTable.thumbnail',
defaultMessage: 'Presentation thumbnail',
})}
style={{ style={{
maxWidth: '150px', maxWidth: '150px',
width: '150px', width: '150px',
height: 'auto', height: 'auto',
whiteSpace: 'pre-line',
}} }}
/> />
</a> </a>
<p id={`thumb-desc-${presentationId}`} className="absolute w-0 h-0 p-0 border-0 m-0 overflow-hidden">
{`${intl.formatMessage(intlMessages.thumbnail)} - ${intl.formatMessage(intlMessages.presentation)} ${presentationName} - ${intl.formatMessage(intlMessages.pageNumber)} ${pageNum} - ${intl.formatMessage(intlMessages.setAt)} ${start}`}
</p>
<div className="text-xs text-center mt-1 text-gray-500">{tsToHHmmss(slide.setOn - periods[0].start)}</div> <div className="text-xs text-center mt-1 text-gray-500">{tsToHHmmss(slide.setOn - periods[0].start)}</div>
</div> </div>
</div> </div>

View File

@ -347,17 +347,25 @@ const UserDatailsComponent = (props) => {
<div className="h-6 relative before:bg-gray-500 before:absolute before:w-[10px] before:h-[10px] before:rounded-full before:left-0 before:top-[calc(50%-5px)] after:bg-gray-500 after:absolute after:w-[10px] after:h-[10px] after:rounded-full after:right-0 after:top-[calc(50%-5px)]"> <div className="h-6 relative before:bg-gray-500 before:absolute before:w-[10px] before:h-[10px] before:rounded-full before:left-0 before:top-[calc(50%-5px)] after:bg-gray-500 after:absolute after:w-[10px] after:h-[10px] after:rounded-full after:right-0 after:top-[calc(50%-5px)]">
<div className="bg-gray-500 [--line-height:2px] h-[var(--line-height)] absolute top-[calc(50%-var(--line-height)/2)] left-[10px] right-[10px] rounded-2xl" /> <div className="bg-gray-500 [--line-height:2px] h-[var(--line-height)] absolute top-[calc(50%-var(--line-height)/2)] left-[10px] right-[10px] rounded-2xl" />
<div <div
role="progressbar"
className="ltr:bg-gradient-to-br rtl:bg-gradient-to-bl from-green-100 to-green-600 absolute h-full rounded-2xl text-right rtl:text-left text-ellipsis overflow-hidden" className="ltr:bg-gradient-to-br rtl:bg-gradient-to-bl from-green-100 to-green-600 absolute h-full rounded-2xl text-right rtl:text-left text-ellipsis overflow-hidden"
style={{ style={{
right: `calc(${document.dir === 'ltr' ? userEndOffsetTime : userStartOffsetTime}% + 10px)`, right: `calc(${document.dir === 'ltr' ? userEndOffsetTime : userStartOffsetTime}% + 10px)`,
left: `calc(${document.dir === 'ltr' ? userStartOffsetTime : userEndOffsetTime}% + 10px)`, left: `calc(${document.dir === 'ltr' ? userStartOffsetTime : userEndOffsetTime}% + 10px)`,
}} }}
> >
<div className="mx-3 inline-block text-white"> <div
aria-describedby={`online-indicator-desc-${user.userKey}`}
aria-label={intl.formatMessage({ id: 'app.learningDashboard.usersTable.colOnline', defaultMessage: 'Online time' })}
className="mx-3 inline-block text-white"
>
{ new Date(getSumOfTime(Object.values(user.intIds))) { new Date(getSumOfTime(Object.values(user.intIds)))
.toISOString() .toISOString()
.substring(11, 19) } .substring(11, 19) }
</div> </div>
<p id={`online-indicator-desc-${user.userKey}`} className="absolute w-0 h-0 p-0 border-0 m-0 overflow-hidden">
{`${intl.formatMessage({ id: 'app.learningDashboard.userDetails.onlineIndicator', defaultMessage: '{0} online time' }, { 0: user.name })} ${new Date(getSumOfTime(Object.values(user.intIds))).toISOString().substring(11, 19)}`}
</p>
</div> </div>
</div> </div>
<div className="flex flex-row justify-between font-light text-gray-700"> <div className="flex flex-row justify-between font-light text-gray-700">
@ -414,7 +422,7 @@ const UserDatailsComponent = (props) => {
<> <>
<div className="bg-white shadow rounded mb-4 table w-full"> <div className="bg-white shadow rounded mb-4 table w-full">
<div className="p-6 text-lg flex items-center"> <div className="p-6 text-lg flex items-center">
<div className="p-2 rounded-full bg-green-200 text-green-500"> <div className="p-2 rounded-full bg-green-100 text-green-700">
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z" />
@ -465,7 +473,7 @@ const UserDatailsComponent = (props) => {
</div> </div>
<div className="bg-white shadow rounded"> <div className="bg-white shadow rounded">
<div className="p-6 text-lg flex items-center"> <div className="p-6 text-lg flex items-center">
<div className="p-2 rounded-full bg-blue-100 text-blue-500"> <div className="p-2 rounded-full bg-blue-100 text-blue-700">
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg> </svg>

View File

@ -165,7 +165,7 @@ class UsersTable extends React.Component {
return ( return (
<table className="w-full"> <table className="w-full">
<thead> <thead>
<tr className="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b bg-gray-100"> <tr className="text-xs font-semibold tracking-wide text-left text-gray-700 uppercase border-b bg-gray-100">
<th <th
className={`px-3.5 2xl:px-4 py-3 col-text-left ${tab === 'overview' ? 'cursor-pointer' : ''}`} className={`px-3.5 2xl:px-4 py-3 col-text-left ${tab === 'overview' ? 'cursor-pointer' : ''}`}
onClick={() => { if (tab === 'overview') this.toggleOrder('userOrder'); }} onClick={() => { if (tab === 'overview') this.toggleOrder('userOrder'); }}

View File

@ -3,7 +3,6 @@ import ReactDOM from 'react-dom';
import './index.css'; import './index.css';
import { IntlProvider } from 'react-intl'; import { IntlProvider } from 'react-intl';
import App from './App'; import App from './App';
import reportWebVitals from './reportWebVitals';
import { UserDetailsProvider } from './components/UserDetails/context'; import { UserDetailsProvider } from './components/UserDetails/context';
const RTL_LANGUAGES = ['ar', 'dv', 'fa', 'he']; const RTL_LANGUAGES = ['ar', 'dv', 'fa', 'he'];
@ -83,5 +82,3 @@ class Dashboard extends React.Component {
const rootElement = document.getElementById('root'); const rootElement = document.getElementById('root');
ReactDOM.render(<Dashboard />, rootElement); ReactDOM.render(<Dashboard />, rootElement);
reportWebVitals();

View File

@ -1,15 +0,0 @@
const reportWebVitals = (onPerfEntry) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({
getCLS, getFID, getFCP, getLCP, getTTFB,
}) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@ -1,5 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

View File

@ -1 +1 @@
git clone --branch v5.0.0-alpha.3 --depth 1 https://github.com/bigbluebutton/bbb-playback bbb-playback git clone --branch v5.0.0-beta.1 --depth 1 https://github.com/bigbluebutton/bbb-playback bbb-playback

View File

@ -1 +1 @@
git clone --branch v2.9.4 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu git clone --branch v2.9.5 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu

View File

@ -1 +1 @@
BIGBLUEBUTTON_RELEASE=2.6.0-beta.1 BIGBLUEBUTTON_RELEASE=2.6.0-beta.4

View File

@ -832,21 +832,6 @@ check_configuration() {
echo "# is not owned by $BBB_USER" echo "# is not owned by $BBB_USER"
fi fi
if [ -n "$HTML5_CONFIG" ]; then
SVG_IMAGES_REQUIRED=$(cat $BBB_WEB_CONFIG | grep -v '#' | sed -n '/^svgImagesRequired/{s/.*=//;p}')
if [ "$SVG_IMAGES_REQUIRED" != "true" ]; then
echo
echo "# Warning: You have the HTML5 client installed but in"
echo "#"
echo "# $BBB_WEB_CONFIG"
echo "#"
echo "# the setting for svgImagesRequired is false. To fix, run the commnad"
echo "#"
echo "# sed -i 's/^svgImagesRequired=.*/svgImagesRequired=true/' $BBB_WEB_CONFIG "
echo "#"
fi
fi
CHECK_STUN=$(xmlstarlet sel -t -m '//X-PRE-PROCESS[@cmd="set" and starts-with(@data, "external_rtp_ip=")]' -v @data $FREESWITCH_VARS | sed 's/external_rtp_ip=stun://g') CHECK_STUN=$(xmlstarlet sel -t -m '//X-PRE-PROCESS[@cmd="set" and starts-with(@data, "external_rtp_ip=")]' -v @data $FREESWITCH_VARS | sed 's/external_rtp_ip=stun://g')
if [ "$CHECK_STUN" == "stun.freeswitch.org" ]; then if [ "$CHECK_STUN" == "stun.freeswitch.org" ]; then
echo echo
@ -1370,7 +1355,6 @@ if [ $CHECK ]; then
echo "$BBB_WEB_CONFIG (bbb-web)" echo "$BBB_WEB_CONFIG (bbb-web)"
echo " bigbluebutton.web.serverURL: $(get_bbb_web_config_value bigbluebutton.web.serverURL)" echo " bigbluebutton.web.serverURL: $(get_bbb_web_config_value bigbluebutton.web.serverURL)"
echo " defaultGuestPolicy: $(get_bbb_web_config_value defaultGuestPolicy)" echo " defaultGuestPolicy: $(get_bbb_web_config_value defaultGuestPolicy)"
echo " svgImagesRequired: $(get_bbb_web_config_value svgImagesRequired)"
echo " defaultMeetingLayout: $(get_bbb_web_config_value defaultMeetingLayout)" echo " defaultMeetingLayout: $(get_bbb_web_config_value defaultMeetingLayout)"
echo echo

View File

@ -5,20 +5,20 @@
meteor-base@1.5.1 meteor-base@1.5.1
mobile-experience@1.1.0 mobile-experience@1.1.0
mongo@1.15.0 mongo@1.16.3
reactive-var@1.0.11 reactive-var@1.0.12
standard-minifier-css@1.8.1 standard-minifier-css@1.8.3
standard-minifier-js@2.8.0 standard-minifier-js@2.8.1
es5-shim@4.8.0 es5-shim@4.8.0
ecmascript@0.16.2 ecmascript@0.16.4
shell-server@0.5.0 shell-server@0.5.0
static-html@1.3.2 static-html@1.3.2
react-meteor-data react-meteor-data
session@1.2.0 session@1.2.1
tracker@1.2.0 tracker@1.2.1
check@1.3.1 check@1.3.2
rocketchat:streamer rocketchat:streamer
meteortesting:mocha meteortesting:mocha

View File

@ -1 +1 @@
METEOR@2.7.3 METEOR@2.9.0

View File

@ -1,6 +1,6 @@
allow-deny@1.1.1 allow-deny@1.1.1
autoupdate@1.8.0 autoupdate@1.8.0
babel-compiler@7.9.0 babel-compiler@7.10.1
babel-runtime@1.5.1 babel-runtime@1.5.1
base64@1.0.12 base64@1.0.12
binary-heap@1.0.11 binary-heap@1.0.11
@ -9,21 +9,21 @@ boilerplate-generator@1.7.1
caching-compiler@1.2.2 caching-compiler@1.2.2
caching-html-compiler@1.2.1 caching-html-compiler@1.2.1
callback-hook@1.4.0 callback-hook@1.4.0
check@1.3.1 check@1.3.2
ddp@1.4.0 ddp@1.4.1
ddp-client@2.5.0 ddp-client@2.6.1
ddp-common@1.4.0 ddp-common@1.4.0
ddp-server@2.5.0 ddp-server@2.6.0
diff-sequence@1.1.1 diff-sequence@1.1.2
dynamic-import@0.7.2 dynamic-import@0.7.2
ecmascript@0.16.2 ecmascript@0.16.4
ecmascript-runtime@0.8.0 ecmascript-runtime@0.8.0
ecmascript-runtime-client@0.12.1 ecmascript-runtime-client@0.12.1
ecmascript-runtime-server@0.11.0 ecmascript-runtime-server@0.11.0
ejson@1.1.2 ejson@1.1.3
es5-shim@4.8.0 es5-shim@4.8.0
fetch@0.1.1 fetch@0.1.2
geojson-utils@1.0.10 geojson-utils@1.0.11
hot-code-push@1.0.4 hot-code-push@1.0.4
html-tools@1.1.3 html-tools@1.1.3
htmljs@1.1.1 htmljs@1.1.1
@ -33,46 +33,46 @@ inter-process-messaging@0.1.1
launch-screen@1.3.0 launch-screen@1.3.0
lmieulet:meteor-coverage@4.1.0 lmieulet:meteor-coverage@4.1.0
logging@1.3.1 logging@1.3.1
meteor@1.10.0 meteor@1.10.3
meteor-base@1.5.1 meteor-base@1.5.1
meteortesting:browser-tests@1.3.5 meteortesting:browser-tests@1.3.5
meteortesting:mocha@2.0.3 meteortesting:mocha@2.0.3
meteortesting:mocha-core@8.1.2 meteortesting:mocha-core@8.1.2
minifier-css@1.6.0 minifier-css@1.6.2
minifier-js@2.7.4 minifier-js@2.7.5
minimongo@1.8.0 minimongo@1.9.1
mobile-experience@1.1.0 mobile-experience@1.1.0
mobile-status-bar@1.1.0 mobile-status-bar@1.1.0
modern-browsers@0.1.8 modern-browsers@0.1.9
modules@0.18.0 modules@0.19.0
modules-runtime@0.13.0 modules-runtime@0.13.1
mongo@1.15.0 mongo@1.16.3
mongo-decimal@0.1.3 mongo-decimal@0.1.3
mongo-dev-server@1.1.0 mongo-dev-server@1.1.0
mongo-id@1.0.8 mongo-id@1.0.8
npm-mongo@4.3.1 npm-mongo@4.12.1
ordered-dict@1.1.0 ordered-dict@1.1.0
promise@0.12.0 promise@0.12.2
random@1.2.0 random@1.2.1
react-fast-refresh@0.2.3 react-fast-refresh@0.2.3
react-meteor-data@2.5.1 react-meteor-data@2.5.1
reactive-dict@1.3.0 reactive-dict@1.3.1
reactive-var@1.0.11 reactive-var@1.0.12
reload@1.3.1 reload@1.3.1
retry@1.1.0 retry@1.1.0
rocketchat:streamer@1.1.0 rocketchat:streamer@1.1.0
routepolicy@1.1.1 routepolicy@1.1.1
session@1.2.0 session@1.2.1
shell-server@0.5.0 shell-server@0.5.0
socket-stream-client@0.5.0 socket-stream-client@0.5.0
spacebars-compiler@1.3.1 spacebars-compiler@1.3.1
standard-minifier-css@1.8.1 standard-minifier-css@1.8.3
standard-minifier-js@2.8.0 standard-minifier-js@2.8.1
static-html@1.3.2 static-html@1.3.2
templating-tools@1.2.2 templating-tools@1.2.2
tracker@1.2.0 tracker@1.2.1
typescript@4.5.4 typescript@4.6.4
underscore@1.0.10 underscore@1.0.11
url@1.3.2 url@1.3.2
webapp@1.13.1 webapp@1.13.2
webapp-hashing@1.1.0 webapp-hashing@1.1.1

View File

@ -27,74 +27,52 @@ import Users, { CurrentUser } from '/imports/api/users';
import { Slides, SlidePositions } from '/imports/api/slides'; import { Slides, SlidePositions } from '/imports/api/slides';
// Custom Publishers // Custom Publishers
export const localCurrentPollSync = new AbstractCollection(CurrentPoll, CurrentPoll); export const localCollectionRegistry = {
export const localCurrentUserSync = new AbstractCollection(CurrentUser, CurrentUser); localCurrentPollSync: new AbstractCollection(CurrentPoll, CurrentPoll),
export const localSlidesSync = new AbstractCollection(Slides, Slides); localCurrentUserSync: new AbstractCollection(CurrentUser, CurrentUser),
export const localSlidePositionsSync = new AbstractCollection(SlidePositions, SlidePositions); localSlidesSync: new AbstractCollection(Slides, Slides),
export const localPollsSync = new AbstractCollection(Polls, Polls); localSlidePositionsSync: new AbstractCollection(SlidePositions, SlidePositions),
export const localPresentationsSync = new AbstractCollection(Presentations, Presentations); localPollsSync: new AbstractCollection(Polls, Polls),
export const localPresentationPodsSync = new AbstractCollection(PresentationPods, PresentationPods); localPresentationsSync: new AbstractCollection(Presentations, Presentations),
export const localPresentationUploadTokenSync = new AbstractCollection(PresentationUploadToken, PresentationUploadToken); localPresentationPodsSync: new AbstractCollection(PresentationPods, PresentationPods),
export const localScreenshareSync = new AbstractCollection(Screenshare, Screenshare); localPresentationUploadTokenSync: new AbstractCollection(
export const localUserInfosSync = new AbstractCollection(UserInfos, UserInfos); PresentationUploadToken,
export const localUsersPersistentDataSync = new AbstractCollection(UsersPersistentData, UsersPersistentData); PresentationUploadToken,
export const localUserSettingsSync = new AbstractCollection(UserSettings, UserSettings); ),
export const localVideoStreamsSync = new AbstractCollection(VideoStreams, VideoStreams); localScreenshareSync: new AbstractCollection(Screenshare, Screenshare),
export const localVoiceUsersSync = new AbstractCollection(VoiceUsers, VoiceUsers); localUserInfosSync: new AbstractCollection(UserInfos, UserInfos),
export const localWhiteboardMultiUserSync = new AbstractCollection(WhiteboardMultiUser, WhiteboardMultiUser); localUsersPersistentDataSync: new AbstractCollection(UsersPersistentData, UsersPersistentData),
export const localGroupChatSync = new AbstractCollection(GroupChat, GroupChat); localUserSettingsSync: new AbstractCollection(UserSettings, UserSettings),
export const localConnectionStatusSync = new AbstractCollection(ConnectionStatus, ConnectionStatus); localVideoStreamsSync: new AbstractCollection(VideoStreams, VideoStreams),
export const localCaptionsSync = new AbstractCollection(Captions, Captions); localVoiceUsersSync: new AbstractCollection(VoiceUsers, VoiceUsers),
export const localPadsSync = new AbstractCollection(Pads, Pads); localWhiteboardMultiUserSync: new AbstractCollection(WhiteboardMultiUser, WhiteboardMultiUser),
export const localPadsSessionsSync = new AbstractCollection(PadsSessions, PadsSessions); localGroupChatSync: new AbstractCollection(GroupChat, GroupChat),
export const localPadsUpdatesSync = new AbstractCollection(PadsUpdates, PadsUpdates); localConnectionStatusSync: new AbstractCollection(ConnectionStatus, ConnectionStatus),
export const localAuthTokenValidationSync = new AbstractCollection(AuthTokenValidation, AuthTokenValidation); localCaptionsSync: new AbstractCollection(Captions, Captions),
export const localAnnotationsSync = new AbstractCollection(Annotations, Annotations); localPadsSync: new AbstractCollection(Pads, Pads),
export const localRecordMeetingsSync = new AbstractCollection(RecordMeetings, RecordMeetings); localPadsSessionsSync: new AbstractCollection(PadsSessions, PadsSessions),
export const localExternalVideoMeetingsSync = new AbstractCollection(ExternalVideoMeetings, ExternalVideoMeetings); localPadsUpdatesSync: new AbstractCollection(PadsUpdates, PadsUpdates),
export const localMeetingTimeRemainingSync = new AbstractCollection(MeetingTimeRemaining, MeetingTimeRemaining); localAuthTokenValidationSync: new AbstractCollection(AuthTokenValidation, AuthTokenValidation),
export const localUsersTypingSync = new AbstractCollection(UsersTyping, UsersTyping); localAnnotationsSync: new AbstractCollection(Annotations, Annotations),
export const localBreakoutsSync = new AbstractCollection(Breakouts, Breakouts); localRecordMeetingsSync: new AbstractCollection(RecordMeetings, RecordMeetings),
export const localBreakoutsHistorySync = new AbstractCollection(BreakoutsHistory, BreakoutsHistory); localExternalVideoMeetingsSync: new AbstractCollection(
export const localGuestUsersSync = new AbstractCollection(guestUsers, guestUsers); ExternalVideoMeetings,
export const localMeetingsSync = new AbstractCollection(Meetings, Meetings); ExternalVideoMeetings,
export const localUsersSync = new AbstractCollection(Users, Users); ),
export const localNotificationsSync = new AbstractCollection(Notifications, Notifications); localMeetingTimeRemainingSync: new AbstractCollection(MeetingTimeRemaining, MeetingTimeRemaining),
localUsersTypingSync: new AbstractCollection(UsersTyping, UsersTyping),
localBreakoutsSync: new AbstractCollection(Breakouts, Breakouts),
localBreakoutsHistorySync: new AbstractCollection(BreakoutsHistory, BreakoutsHistory),
localGuestUsersSync: new AbstractCollection(guestUsers, guestUsers),
localMeetingsSync: new AbstractCollection(Meetings, Meetings),
localUsersSync: new AbstractCollection(Users, Users),
localNotificationsSync: new AbstractCollection(Notifications, Notifications),
};
const collectionMirrorInitializer = () => { const collectionMirrorInitializer = () => {
localCurrentPollSync.setupListeners(); Object.values(localCollectionRegistry).forEach((localCollection) => {
localCurrentUserSync.setupListeners(); localCollection.setupListeners();
localSlidesSync.setupListeners(); });
localSlidePositionsSync.setupListeners();
localPollsSync.setupListeners();
localPresentationsSync.setupListeners();
localPresentationPodsSync.setupListeners();
localPresentationUploadTokenSync.setupListeners();
localScreenshareSync.setupListeners();
localUserInfosSync.setupListeners();
localUsersPersistentDataSync.setupListeners();
localUserSettingsSync.setupListeners();
localVideoStreamsSync.setupListeners();
localVoiceUsersSync.setupListeners();
localWhiteboardMultiUserSync.setupListeners();
localGroupChatSync.setupListeners();
localConnectionStatusSync.setupListeners();
localCaptionsSync.setupListeners();
localPadsSync.setupListeners();
localPadsSessionsSync.setupListeners();
localPadsUpdatesSync.setupListeners();
localAuthTokenValidationSync.setupListeners();
localAnnotationsSync.setupListeners();
localRecordMeetingsSync.setupListeners();
localExternalVideoMeetingsSync.setupListeners();
localMeetingTimeRemainingSync.setupListeners();
localUsersTypingSync.setupListeners();
localBreakoutsSync.setupListeners();
localBreakoutsHistorySync.setupListeners();
localGuestUsersSync.setupListeners();
localMeetingsSync.setupListeners();
localUsersSync.setupListeners();
localNotificationsSync.setupListeners();
}; };
export default collectionMirrorInitializer; export default collectionMirrorInitializer;

View File

@ -127,7 +127,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
</head> </head>
<body style="background-color: #06172A"> <body style="background-color: #06172A">
<div id="aria-polite-alert" aria-live="polite" aria-atomic="false" class="sr-only"></div> <div id="aria-polite-alert" aria-live="polite" aria-atomic="false" class="sr-only"></div>
<div id="app" role="document"> <main>
<div id="app" role="document">
</main>
</div> </div>
<span id="destination"></span> <span id="destination"></span>
<audio id="remote-media" autoplay> <audio id="remote-media" autoplay>

View File

@ -2,6 +2,7 @@ import Breakouts from '/imports/api/breakouts';
import updateUserBreakoutRoom from '/imports/api/users-persistent-data/server/modifiers/updateUserBreakoutRoom'; import updateUserBreakoutRoom from '/imports/api/users-persistent-data/server/modifiers/updateUserBreakoutRoom';
import Logger from '/imports/startup/server/logger'; import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check'; import { check } from 'meteor/check';
import { lowercaseTrim } from '/imports/utils/string-utils';
export default function joinedUsersChanged({ body }) { export default function joinedUsersChanged({ body }) {
check(body, Object); check(body, Object);
@ -21,7 +22,7 @@ export default function joinedUsersChanged({ body }) {
breakoutId, breakoutId,
}; };
const usersMapped = users.map(user => ({ userId: user.id, name: user.name })); const usersMapped = users.map(user => ({ userId: user.id, name: user.name, sortName: lowercaseTrim(user.name) }));
const modifier = { const modifier = {
$set: { $set: {
joinedUsers: usersMapped, joinedUsers: usersMapped,

View File

@ -86,8 +86,8 @@ export default function addMeeting(meeting) {
name: String, name: String,
disabledFeatures: Array, disabledFeatures: Array,
notifyRecordingIsOn: Boolean, notifyRecordingIsOn: Boolean,
uploadExternalDescription: String, presentationUploadExternalDescription: String,
uploadExternalUrl: String, presentationUploadExternalUrl: String,
}, },
usersProp: { usersProp: {
maxUsers: Number, maxUsers: Number,

View File

@ -11,6 +11,7 @@ RedisPubSub.on('PdfConversionInvalidErrorEvtMsg', handlePresentationConversionUp
RedisPubSub.on('PresentationPageGeneratedEvtMsg', handlePresentationConversionUpdate); RedisPubSub.on('PresentationPageGeneratedEvtMsg', handlePresentationConversionUpdate);
RedisPubSub.on('PresentationPageCountErrorEvtMsg', handlePresentationConversionUpdate); RedisPubSub.on('PresentationPageCountErrorEvtMsg', handlePresentationConversionUpdate);
RedisPubSub.on('PresentationUploadedFileTimeoutErrorEvtMsg', handlePresentationConversionUpdate); RedisPubSub.on('PresentationUploadedFileTimeoutErrorEvtMsg', handlePresentationConversionUpdate);
RedisPubSub.on('PresentationHasInvalidMimeTypeErrorEvtMsg', handlePresentationConversionUpdate);
RedisPubSub.on('PresentationConversionUpdateEvtMsg', handlePresentationConversionUpdate); RedisPubSub.on('PresentationConversionUpdateEvtMsg', handlePresentationConversionUpdate);
RedisPubSub.on('PresentationUploadedFileTooLargeErrorEvtMsg', handlePresentationConversionUpdate); RedisPubSub.on('PresentationUploadedFileTooLargeErrorEvtMsg', handlePresentationConversionUpdate);
RedisPubSub.on('PresentationConversionCompletedEvtMsg', handlePresentationAdded); RedisPubSub.on('PresentationConversionCompletedEvtMsg', handlePresentationAdded);

View File

@ -13,6 +13,7 @@ const PDF_HAS_BIG_PAGE_KEY = 'PDF_HAS_BIG_PAGE';
const GENERATED_SLIDE_KEY = 'GENERATED_SLIDE'; const GENERATED_SLIDE_KEY = 'GENERATED_SLIDE';
const FILE_TOO_LARGE_KEY = 'FILE_TOO_LARGE'; const FILE_TOO_LARGE_KEY = 'FILE_TOO_LARGE';
const CONVERSION_TIMEOUT_KEY = "CONVERSION_TIMEOUT"; const CONVERSION_TIMEOUT_KEY = "CONVERSION_TIMEOUT";
const IVALID_MIME_TYPE_KEY = "IVALID_MIME_TYPE";
// const GENERATING_THUMBNAIL_KEY = 'GENERATING_THUMBNAIL'; // const GENERATING_THUMBNAIL_KEY = 'GENERATING_THUMBNAIL';
// const GENERATED_THUMBNAIL_KEY = 'GENERATED_THUMBNAIL'; // const GENERATED_THUMBNAIL_KEY = 'GENERATED_THUMBNAIL';
// const GENERATING_TEXTFILES_KEY = 'GENERATING_TEXTFILES'; // const GENERATING_TEXTFILES_KEY = 'GENERATING_TEXTFILES';
@ -50,6 +51,10 @@ export default function handlePresentationConversionUpdate({ body }, meetingId)
statusModifier['conversion.maxFileSize'] = body.maxFileSize; statusModifier['conversion.maxFileSize'] = body.maxFileSize;
case UNSUPPORTED_DOCUMENT_KEY: case UNSUPPORTED_DOCUMENT_KEY:
case OFFICE_DOC_CONVERSION_FAILED_KEY: case OFFICE_DOC_CONVERSION_FAILED_KEY:
case IVALID_MIME_TYPE_KEY:
statusModifier['conversion.error'] = true;
statusModifier['conversion.fileMime'] = body.fileMime;
statusModifier['conversion.fileExtension'] = body.fileExtension;
case OFFICE_DOC_CONVERSION_INVALID_KEY: case OFFICE_DOC_CONVERSION_INVALID_KEY:
case PAGE_COUNT_FAILED_KEY: case PAGE_COUNT_FAILED_KEY:
case PAGE_COUNT_EXCEEDED_KEY: case PAGE_COUNT_EXCEEDED_KEY:

View File

@ -6,6 +6,7 @@ import VoiceUsers from '/imports/api/voice-users/';
import addUserPsersistentData from '/imports/api/users-persistent-data/server/modifiers/addUserPersistentData'; import addUserPsersistentData from '/imports/api/users-persistent-data/server/modifiers/addUserPersistentData';
import stringHash from 'string-hash'; import stringHash from 'string-hash';
import flat from 'flat'; import flat from 'flat';
import { lowercaseTrim } from '/imports/utils/string-utils';
import addVoiceUser from '/imports/api/voice-users/server/modifiers/addVoiceUser'; import addVoiceUser from '/imports/api/voice-users/server/modifiers/addVoiceUser';
@ -51,7 +52,7 @@ export default function addUser(meetingId, userData) {
const userInfos = { const userInfos = {
meetingId, meetingId,
sortName: user.name.trim().toLowerCase(), sortName: lowercaseTrim(user.name),
color, color,
speechLocale: '', speechLocale: '',
mobile: false, mobile: false,

View File

@ -7,6 +7,7 @@ import {
} from '/imports/api/video-streams/server/helpers'; } from '/imports/api/video-streams/server/helpers';
import VoiceUsers from '/imports/api/voice-users/'; import VoiceUsers from '/imports/api/voice-users/';
import Users from '/imports/api/users/'; import Users from '/imports/api/users/';
import { lowercaseTrim } from '/imports/utils/string-utils';
const BASE_FLOOR_TIME = "0"; const BASE_FLOOR_TIME = "0";
@ -40,6 +41,7 @@ export default function sharedWebcam(meetingId, userId, stream) {
$set: { $set: {
stream, stream,
name, name,
sortName: lowercaseTrim(name),
lastFloorTime, lastFloorTime,
floor, floor,
pin, pin,

View File

@ -171,7 +171,7 @@ class Base extends Component {
HTML.classList.add('animationsDisabled'); HTML.classList.add('animationsDisabled');
} }
if (sidebarContentPanel === PANELS.NONE || Session.equals('subscriptionsReady', true)) { if (Session.equals('layoutReady', true) && (sidebarContentPanel === PANELS.NONE || Session.equals('subscriptionsReady', true))) {
if (!checkedUserSettings) { if (!checkedUserSettings) {
if (getFromUserSettings('bbb_show_participants_on_login', Meteor.settings.public.layout.showParticipantsOnLogin) && !deviceInfo.isPhone) { if (getFromUserSettings('bbb_show_participants_on_login', Meteor.settings.public.layout.showParticipantsOnLogin) && !deviceInfo.isPhone) {
if (isChatEnabled() && getFromUserSettings('bbb_show_public_chat_on_login', !Meteor.settings.public.chat.startClosed)) { if (isChatEnabled() && getFromUserSettings('bbb_show_public_chat_on_login', !Meteor.settings.public.chat.startClosed)) {

View File

@ -723,8 +723,7 @@ class BreakoutRoom extends PureComponent {
} }
populateWithLastBreakouts(lastBreakouts) { populateWithLastBreakouts(lastBreakouts) {
const { getBreakoutUserWasIn, intl } = this.props; const { getBreakoutUserWasIn, users, intl } = this.props;
const { users } = this.state;
const changedNames = []; const changedNames = [];
lastBreakouts.forEach((breakout) => { lastBreakouts.forEach((breakout) => {

View File

@ -40,13 +40,6 @@ const Alert = styled.div`
color: ${colorDanger}; color: ${colorDanger};
} }
`} `}
grid-row: span 3;
& > div {
height: 25.2rem;
max-height: 25.2rem;
}
`; `;
const FreeJoinLabel = styled.label` const FreeJoinLabel = styled.label`
@ -85,8 +78,7 @@ const BreakoutNameInput = styled.input`
const BreakoutBox = styled(ScrollboxVertical)` const BreakoutBox = styled(ScrollboxVertical)`
width: 100%; width: 100%;
min-height: 6rem; height: 21rem;
max-height: 8rem;
border: 1px solid ${colorGrayLightest}; border: 1px solid ${colorGrayLightest};
border-radius: ${borderRadius}; border-radius: ${borderRadius};
padding: ${lgPaddingY} 0; padding: ${lgPaddingY} 0;

View File

@ -30,7 +30,7 @@ import PresentationAreaContainer from '../presentation/presentation-area/contain
import ScreenshareContainer from '../screenshare/container'; import ScreenshareContainer from '../screenshare/container';
import ExternalVideoContainer from '../external-video-player/container'; import ExternalVideoContainer from '../external-video-player/container';
import Styled from './styles'; import Styled from './styles';
import { DEVICE_TYPE, ACTIONS, SMALL_VIEWPORT_BREAKPOINT } from '../layout/enums'; import { DEVICE_TYPE, ACTIONS, SMALL_VIEWPORT_BREAKPOINT, PANELS } from '../layout/enums';
import { import {
isMobile, isTablet, isTabletPortrait, isTabletLandscape, isDesktop, isMobile, isTablet, isTabletPortrait, isTabletLandscape, isDesktop,
} from '../layout/utils'; } from '../layout/utils';
@ -47,13 +47,16 @@ import Notifications from '../notifications/container';
import GlobalStyles from '/imports/ui/stylesheets/styled-components/globalStyles'; import GlobalStyles from '/imports/ui/stylesheets/styled-components/globalStyles';
import ActionsBarContainer from '../actions-bar/container'; import ActionsBarContainer from '../actions-bar/container';
import PushLayoutEngine from '../layout/push-layout/pushLayoutEngine'; import PushLayoutEngine from '../layout/push-layout/pushLayoutEngine';
import AudioService from '/imports/ui/components/audio/service';
import NotesContainer from '/imports/ui/components/notes/container'; import NotesContainer from '/imports/ui/components/notes/container';
import DEFAULT_VALUES from '../layout/defaultValues';
const MOBILE_MEDIA = 'only screen and (max-width: 40em)'; const MOBILE_MEDIA = 'only screen and (max-width: 40em)';
const APP_CONFIG = Meteor.settings.public.app; const APP_CONFIG = Meteor.settings.public.app;
const DESKTOP_FONT_SIZE = APP_CONFIG.desktopFontSize; const DESKTOP_FONT_SIZE = APP_CONFIG.desktopFontSize;
const MOBILE_FONT_SIZE = APP_CONFIG.mobileFontSize; const MOBILE_FONT_SIZE = APP_CONFIG.mobileFontSize;
const LAYOUT_CONFIG = Meteor.settings.public.layout; const LAYOUT_CONFIG = Meteor.settings.public.layout;
const CONFIRMATION_ON_LEAVE = Meteor.settings.public.app.askForConfirmationOnLeave;
const intlMessages = defineMessages({ const intlMessages = defineMessages({
userListLabel: { userListLabel: {
@ -195,6 +198,16 @@ class App extends Component {
window.ondragover = (e) => { e.preventDefault(); }; window.ondragover = (e) => { e.preventDefault(); };
window.ondrop = (e) => { e.preventDefault(); }; window.ondrop = (e) => { e.preventDefault(); };
if (CONFIRMATION_ON_LEAVE) {
window.onbeforeunload = (event) => {
AudioService.muteMicrophone();
event.stopImmediatePropagation();
event.preventDefault();
// eslint-disable-next-line no-param-reassign
event.returnValue = '';
};
}
if (deviceInfo.isMobile) makeCall('setMobileUser'); if (deviceInfo.isMobile) makeCall('setMobileUser');
ConnectionStatusService.startRoundTripTime(); ConnectionStatusService.startRoundTripTime();
@ -210,6 +223,12 @@ class App extends Component {
mountModal, mountModal,
deviceType, deviceType,
mountRandomUserModal, mountRandomUserModal,
selectedLayout,
sidebarContentIsOpen,
layoutContextDispatch,
numCameras,
presentationIsOpen,
ignorePollNotifications,
} = this.props; } = this.props;
this.renderDarkMode(); this.renderDarkMode();
@ -243,10 +262,35 @@ class App extends Component {
} }
if (deviceType === null || prevProps.deviceType !== deviceType) this.throttledDeviceType(); if (deviceType === null || prevProps.deviceType !== deviceType) this.throttledDeviceType();
if (
selectedLayout !== prevProps.selectedLayout
&& selectedLayout?.toLowerCase?.()?.includes?.('focus')
&& !sidebarContentIsOpen
&& deviceType !== DEVICE_TYPE.MOBILE
&& numCameras > 0
&& presentationIsOpen
) {
setTimeout(() => {
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
value: true,
});
layoutContextDispatch({
type: ACTIONS.SET_ID_CHAT_OPEN,
value: DEFAULT_VALUES.idChatOpen,
});
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
value: PANELS.CHAT,
});
}, 0);
}
} }
componentWillUnmount() { componentWillUnmount() {
window.removeEventListener('resize', this.handleWindowResize, false); window.removeEventListener('resize', this.handleWindowResize, false);
window.onbeforeunload = null;
ConnectionStatusService.stopRoundTripTime(); ConnectionStatusService.stopRoundTripTime();
} }

View File

@ -168,6 +168,7 @@ const AppContainer = (props) => {
shouldShowPresentation, shouldShowPresentation,
mountRandomUserModal, mountRandomUserModal,
isPresenter, isPresenter,
numCameras: cameraDockInput.numCameras,
}} }}
{...otherProps} {...otherProps}
/> />
@ -310,5 +311,6 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
hidePresentation: getFromUserSettings('bbb_hide_presentation', LAYOUT_CONFIG.hidePresentation), hidePresentation: getFromUserSettings('bbb_hide_presentation', LAYOUT_CONFIG.hidePresentation),
hideActionsBar: getFromUserSettings('bbb_hide_actions_bar', false), hideActionsBar: getFromUserSettings('bbb_hide_actions_bar', false),
isModalOpen: !!getModal(), isModalOpen: !!getModal(),
ignorePollNotifications: Session.get('ignorePollNotifications'),
}; };
})(AppContainer))); })(AppContainer)));

View File

@ -71,6 +71,21 @@ const init = (messages, intl) => {
return AudioManager.init(userData, audioEventHandler); return AudioManager.init(userData, audioEventHandler);
}; };
const muteMicrophone = () => {
const user = VoiceUsers.findOne({
meetingId: Auth.meetingID, intId: Auth.userID,
}, { fields: { muted: 1 } });
if (!user.muted) {
logger.info({
logCode: 'audiomanager_mute_audio',
extraInfo: { logType: 'user_action' },
}, 'User wants to leave conference. Microphone muted');
AudioManager.setSenderTrackEnabled(false);
makeCall('toggleVoice');
}
};
const isVoiceUser = () => { const isVoiceUser = () => {
const voiceUser = VoiceUsers.findOne({ intId: Auth.userID }, const voiceUser = VoiceUsers.findOne({ intId: Auth.userID },
{ fields: { joined: 1 } }); { fields: { joined: 1 } });
@ -133,6 +148,7 @@ export default {
updateAudioConstraints: updateAudioConstraints:
(constraints) => AudioManager.updateAudioConstraints(constraints), (constraints) => AudioManager.updateAudioConstraints(constraints),
recoverMicState, recoverMicState,
muteMicrophone: () => muteMicrophone(),
isReconnecting: () => AudioManager.isReconnecting, isReconnecting: () => AudioManager.isReconnecting,
setBreakoutAudioTransferStatus: (status) => AudioManager setBreakoutAudioTransferStatus: (status) => AudioManager
.setBreakoutAudioTransferStatus(status), .setBreakoutAudioTransferStatus(status),

View File

@ -115,14 +115,15 @@ class MessageForm extends PureComponent {
maxMessageLength, maxMessageLength,
} = this.props; } = this.props;
const message = e.target.value; let message = e.target.value;
let error = null; let error = null;
if (message.length > maxMessageLength) { if (message.length > maxMessageLength) {
error = intl.formatMessage( error = intl.formatMessage(
messages.errorMaxMessageLength, messages.errorMaxMessageLength,
{ 0: message.length - maxMessageLength }, { 0: maxMessageLength },
); );
message = message.substring(0, maxMessageLength);
} }
this.setState({ this.setState({

View File

@ -74,11 +74,7 @@ class LiveCaptions extends PureComponent {
<div style={captionStyles}> <div style={captionStyles}>
{clear ? '' : data} {clear ? '' : data}
</div> </div>
<div <div style={visuallyHidden}>
style={visuallyHidden}
aria-atomic
aria-live="polite"
>
{clear ? '' : data} {clear ? '' : data}
</div> </div>
</div> </div>

View File

@ -10,6 +10,7 @@ import { stripTags, unescapeHtml } from '/imports/utils/string-utils';
import Service from '../service'; import Service from '../service';
import Styled from './styles'; import Styled from './styles';
import { usePreviousValue } from '/imports/ui/components/utils/hooks'; import { usePreviousValue } from '/imports/ui/components/utils/hooks';
import { Session } from 'meteor/session';
const CHAT_CONFIG = Meteor.settings.public.chat; const CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_CHAT_CLEAR = CHAT_CONFIG.chat_clear; const PUBLIC_CHAT_CLEAR = CHAT_CONFIG.chat_clear;
@ -193,9 +194,11 @@ const ChatAlert = (props) => {
const mappedMessage = Service.mapGroupMessage(timeWindow); const mappedMessage = Service.mapGroupMessage(timeWindow);
let content = null; let content = null;
let isPollResult = false;
if (mappedMessage) { if (mappedMessage) {
if (mappedMessage.id.includes(POLL_RESULT_KEY)) { if (mappedMessage.id.includes(POLL_RESULT_KEY)) {
content = createPollMessage(); content = createPollMessage();
isPollResult = true;
} else { } else {
content = createMessage(mappedMessage.sender.name, mappedMessage.content.slice(-5)); content = createMessage(mappedMessage.sender.name, mappedMessage.content.slice(-5));
} }
@ -218,10 +221,22 @@ const ChatAlert = (props) => {
: <span>{intl.formatMessage(intlMessages.appToastChatPrivate)}</span> : <span>{intl.formatMessage(intlMessages.appToastChatPrivate)}</span>
} }
onOpen={ onOpen={
() => setUnreadMessages(newUnreadMessages) () => {
if (isPollResult) {
Session.set('ignorePollNotifications', true);
}
setUnreadMessages(newUnreadMessages);
}
} }
onClose={ onClose={
() => setUnreadMessages(newUnreadMessages) () => {
if (isPollResult) {
Session.set('ignorePollNotifications', false);
}
setUnreadMessages(newUnreadMessages);
}
} }
alertDuration={timeWindow.durationDiff} alertDuration={timeWindow.durationDiff}
layoutContextDispatch={layoutContextDispatch} layoutContextDispatch={layoutContextDispatch}

View File

@ -227,8 +227,9 @@ class MessageForm extends PureComponent {
if (message.length > maxMessageLength) { if (message.length > maxMessageLength) {
error = intl.formatMessage( error = intl.formatMessage(
messages.errorMaxMessageLength, messages.errorMaxMessageLength,
{ 0: message.length - maxMessageLength }, { 0: maxMessageLength },
); );
message = message.substring(0, maxMessageLength);
} }
this.setState({ this.setState({

View File

@ -46,7 +46,7 @@ class LocalesDropdown extends PureComponent {
render() { render() {
const { const {
value, handleChange, elementId, selectMessage, value, handleChange, elementId, selectMessage, ariaLabel,
} = this.props; } = this.props;
const defaultLocale = value || DEFAULT_VALUE; const defaultLocale = value || DEFAULT_VALUE;
@ -57,6 +57,7 @@ class LocalesDropdown extends PureComponent {
id={elementId} id={elementId}
onChange={handleChange} onChange={handleChange}
value={defaultLocale} value={defaultLocale}
aria-label={ariaLabel||''}
> >
<option disabled key={DEFAULT_KEY} value={DEFAULT_VALUE}> <option disabled key={DEFAULT_KEY} value={DEFAULT_VALUE}>
{selectMessage} {selectMessage}

View File

@ -6,7 +6,7 @@ import Styled from './styles';
const messages = defineMessages({ const messages = defineMessages({
yesLabel: { yesLabel: {
id: 'app.endMeeting.yesLabel', id: 'app.confirmationModal.yesLabel',
description: 'confirm button label', description: 'confirm button label',
}, },
noLabel: { noLabel: {
@ -46,6 +46,7 @@ class ConfirmationModal extends Component {
titleMessageExtra, titleMessageExtra,
checkboxMessageId, checkboxMessageId,
confirmButtonColor, confirmButtonColor,
confirmButtonLabel,
confirmButtonDataTest, confirmButtonDataTest,
confirmParam, confirmParam,
disableConfirmButton, disableConfirmButton,
@ -86,7 +87,7 @@ class ConfirmationModal extends Component {
<Styled.Footer> <Styled.Footer>
<Styled.ConfirmationButton <Styled.ConfirmationButton
color={confirmButtonColor} color={confirmButtonColor}
label={intl.formatMessage(messages.yesLabel)} label={confirmButtonLabel ? confirmButtonLabel : intl.formatMessage(messages.yesLabel)}
disabled={disableConfirmButton} disabled={disableConfirmButton}
data-test={confirmButtonDataTest} data-test={confirmButtonDataTest}
onClick={() => { onClick={() => {

View File

@ -47,6 +47,7 @@ class ModalSimple extends Component {
render() { render() {
const { const {
id,
intl, intl,
title, title,
hideBorder, hideBorder,
@ -79,6 +80,7 @@ class ModalSimple extends Component {
return ( return (
<Styled.SimpleModal <Styled.SimpleModal
id="simpleModal"
isOpen={modalisOpen} isOpen={modalisOpen}
className={className} className={className}
onRequestClose={handleRequestClose} onRequestClose={handleRequestClose}

View File

@ -158,8 +158,7 @@ class ConnectionStatusComponent extends PureComponent {
this.help = Service.getHelp(); this.help = Service.getHelp();
this.state = { this.state = {
selectedTab: '1', selectedTab: 0,
dataPage: '1',
dataSaving: props.dataSaving, dataSaving: props.dataSaving,
hasNetworkData: false, hasNetworkData: false,
copyButtonText: intl.formatMessage(intlMessages.copy), copyButtonText: intl.formatMessage(intlMessages.copy),
@ -187,6 +186,7 @@ class ConnectionStatusComponent extends PureComponent {
this.audioDownloadLabel = intl.formatMessage(intlMessages.audioDownloadRate); this.audioDownloadLabel = intl.formatMessage(intlMessages.audioDownloadRate);
this.videoUploadLabel = intl.formatMessage(intlMessages.videoUploadRate); this.videoUploadLabel = intl.formatMessage(intlMessages.videoUploadRate);
this.videoDownloadLabel = intl.formatMessage(intlMessages.videoDownloadRate); this.videoDownloadLabel = intl.formatMessage(intlMessages.videoDownloadRate);
this.handleSelectTab = this.handleSelectTab.bind(this);
} }
async componentDidMount() { async componentDidMount() {
@ -197,12 +197,24 @@ class ConnectionStatusComponent extends PureComponent {
Meteor.clearInterval(this.rateInterval); Meteor.clearInterval(this.rateInterval);
} }
handleSelectTab(tab) {
this.setState({
selectedTab: tab,
});
}
handleDataSavingChange(key) { handleDataSavingChange(key) {
const { dataSaving } = this.state; const { dataSaving } = this.state;
dataSaving[key] = !dataSaving[key]; dataSaving[key] = !dataSaving[key];
this.setState(dataSaving); this.setState(dataSaving);
} }
setButtonMessage(msg) {
this.setState({
copyButtonText: msg,
});
}
/** /**
* Start monitoring the network data. * Start monitoring the network data.
* @return {Promise} A Promise that resolves when process started. * @return {Promise} A Promise that resolves when process started.
@ -262,6 +274,43 @@ class ConnectionStatusComponent extends PureComponent {
}, NETWORK_MONITORING_INTERVAL_MS); }, NETWORK_MONITORING_INTERVAL_MS);
} }
displaySettingsStatus(status) {
const { intl } = this.props;
return (
<Styled.ToggleLabel>
{status ? intl.formatMessage(intlMessages.on)
: intl.formatMessage(intlMessages.off)}
</Styled.ToggleLabel>
);
}
/**
* Copy network data to clipboard
* @return {Promise} A Promise that is resolved after data is copied.
*
*
*/
async copyNetworkData() {
const { intl } = this.props;
const {
networkData,
hasNetworkData,
} = this.state;
if (!hasNetworkData) return;
this.setButtonMessage(intl.formatMessage(intlMessages.copied));
const data = JSON.stringify(networkData, null, 2);
await navigator.clipboard.writeText(data);
this.copyNetworkDataTimeout = setTimeout(() => {
this.setButtonMessage(intl.formatMessage(intlMessages.copy));
}, MIN_TIMEOUT);
}
renderEmpty() { renderEmpty() {
const { intl } = this.props; const { intl } = this.props;
@ -278,52 +327,6 @@ class ConnectionStatusComponent extends PureComponent {
); );
} }
displaySettingsStatus(status) {
const { intl } = this.props;
return (
<Styled.ToggleLabel>
{status ? intl.formatMessage(intlMessages.on)
: intl.formatMessage(intlMessages.off)}
</Styled.ToggleLabel>
);
}
setButtonMessage(msg) {
this.setState({
copyButtonText: msg,
});
}
/**
* Copy network data to clipboard
* @param {Object} e Event object from click event
* @return {Promise} A Promise that is resolved after data is copied.
*
*
*/
async copyNetworkData(e) {
const { intl } = this.props;
const {
networkData,
hasNetworkData,
} = this.state;
if (!hasNetworkData) return;
const { target: copyButton } = e;
this.setButtonMessage(intl.formatMessage(intlMessages.copied));
const data = JSON.stringify(networkData, null, 2);
await navigator.clipboard.writeText(data);
this.copyNetworkDataTimeout = setTimeout(() => {
this.setButtonMessage(intl.formatMessage(intlMessages.copy));
}, MIN_TIMEOUT);
}
renderConnections() { renderConnections() {
const { const {
connectionStatus, connectionStatus,
@ -335,7 +338,7 @@ class ConnectionStatusComponent extends PureComponent {
if (isConnectionStatusEmpty(connectionStatus)) return this.renderEmpty(); if (isConnectionStatusEmpty(connectionStatus)) return this.renderEmpty();
let connections = connectionStatus; let connections = connectionStatus;
if (selectedTab === '2') { if (selectedTab === 2) {
connections = connections.filter(conn => conn.you); connections = connections.filter(conn => conn.you);
if (isConnectionStatusEmpty(connections)) return this.renderEmpty(); if (isConnectionStatusEmpty(connections)) return this.renderEmpty();
} }
@ -345,7 +348,7 @@ class ConnectionStatusComponent extends PureComponent {
return ( return (
<Styled.Item <Styled.Item
key={index} key={`${conn?.name}-${dateTime}`}
last={(index + 1) === connections.length} last={(index + 1) === connections.length}
data-test="connectionStatusItemUser" data-test="connectionStatusItemUser"
> >
@ -507,43 +510,17 @@ class ConnectionStatusComponent extends PureComponent {
} }
} }
function handlePaginationClick(action) {
if (action === 'next') {
this.setState({ dataPage: '2' });
}
else {
this.setState({ dataPage: '1' });
}
}
return ( return (
<Styled.NetworkDataContainer data-test="networkDataContainer"> <Styled.NetworkDataContainer
<Styled.Prev> data-test="networkDataContainer"
<Styled.ButtonLeft tabIndex={0}
role="button" >
disabled={dataPage === '1'} <Styled.HelperWrapper>
aria-label={`${intl.formatMessage(intlMessages.prev)} ${intl.formatMessage(intlMessages.ariaTitle)}`} <Styled.Helper>
onClick={handlePaginationClick.bind(this, 'prev')} <ConnectionStatusHelper closeModal={() => closeModal(dataSaving, intl)} />
> </Styled.Helper>
<Styled.Chevron </Styled.HelperWrapper>
xmlns="http://www.w3.org/2000/svg" <Styled.NetworkDataContent>
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</Styled.Chevron>
</Styled.ButtonLeft>
</Styled.Prev>
<Styled.Helper page={dataPage}>
<ConnectionStatusHelper closeModal={() => closeModal(dataSaving, intl)} />
</Styled.Helper>
<Styled.NetworkDataContent page={dataPage}>
<Styled.DataColumn> <Styled.DataColumn>
<Styled.NetworkData> <Styled.NetworkData>
<div>{`${audioUploadLabel}`}</div> <div>{`${audioUploadLabel}`}</div>
@ -582,28 +559,6 @@ class ConnectionStatusComponent extends PureComponent {
</Styled.NetworkData> </Styled.NetworkData>
</Styled.DataColumn> </Styled.DataColumn>
</Styled.NetworkDataContent> </Styled.NetworkDataContent>
<Styled.Next>
<Styled.ButtonRight
role="button"
disabled={dataPage === '2'}
aria-label={`${intl.formatMessage(intlMessages.next)} ${intl.formatMessage(intlMessages.ariaTitle)}`}
onClick={handlePaginationClick.bind(this, 'next')}
>
<Styled.Chevron
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</Styled.Chevron>
</Styled.ButtonRight>
</Styled.Next>
</Styled.NetworkDataContainer> </Styled.NetworkDataContainer>
); );
} }
@ -619,81 +574,23 @@ class ConnectionStatusComponent extends PureComponent {
return null; return null;
} }
const { intl } = this.props; const { hasNetworkData, copyButtonText } = this.state;
const { hasNetworkData } = this.state;
return ( return (
<Styled.CopyContainer aria-live="polite"> <Styled.CopyContainer aria-live="polite">
<Styled.Copy <Styled.Copy
disabled={!hasNetworkData} disabled={!hasNetworkData}
role="button" role="button"
data-test="copyStats" data-test="copyStats"
onClick={this.copyNetworkData.bind(this)} onClick={this.copyNetworkData.bind(this)}
onKeyPress={this.copyNetworkData.bind(this)} onKeyPress={this.copyNetworkData.bind(this)}
tabIndex={0} tabIndex={0}
> >
{this.state.copyButtonText} {copyButtonText}
</Styled.Copy> </Styled.Copy>
</Styled.CopyContainer> </Styled.CopyContainer>
); );
} }
/**
* The navigation bar.
* @returns {Object} The component to be renderized.
*/
renderNavigation() {
const { intl } = this.props;
const handleTabClick = (event) => {
const activeTabElement = document.querySelector('.activeConnectionStatusTab');
const { target } = event;
if (activeTabElement) {
activeTabElement.classList.remove('activeConnectionStatusTab');
}
target.classList.add('activeConnectionStatusTab');
this.setState({
selectedTab: target.dataset.tab,
});
}
return (
<Styled.Navigation>
<div
data-tab="1"
className="activeConnectionStatusTab"
onClick={handleTabClick}
onKeyDown={handleTabClick}
role="button"
>
{intl.formatMessage(intlMessages.connectionStats)}
</div>
<div
data-tab="2"
onClick={handleTabClick}
onKeyDown={handleTabClick}
role="button"
>
{intl.formatMessage(intlMessages.myLogs)}
</div>
{Service.isModerator()
&& (
<div
data-tab="3"
onClick={handleTabClick}
onKeyDown={handleTabClick}
role="button"
>
{intl.formatMessage(intlMessages.sessionLogs)}
</div>
)
}
</Styled.Navigation>
);
}
render() { render() {
const { const {
closeModal, closeModal,
@ -715,18 +612,43 @@ class ConnectionStatusComponent extends PureComponent {
{intl.formatMessage(intlMessages.title)} {intl.formatMessage(intlMessages.title)}
</Styled.Title> </Styled.Title>
</Styled.Header> </Styled.Header>
{this.renderNavigation()}
<Styled.Main> <Styled.ConnectionTabs
<Styled.Body> onSelect={this.handleSelectTab}
{selectedTab === '1' selectedIndex={selectedTab}
? this.renderNetworkData() >
: this.renderConnections() <Styled.ConnectionTabList>
<Styled.ConnectionTabSelector selectedClassName="is-selected">
<span id="connection-status-tab">{intl.formatMessage(intlMessages.title)}</span>
</Styled.ConnectionTabSelector>
<Styled.ConnectionTabSelector selectedClassName="is-selected">
<span id="my-logs-tab">{intl.formatMessage(intlMessages.myLogs)}</span>
</Styled.ConnectionTabSelector>
{Service.isModerator()
&& (
<Styled.ConnectionTabSelector selectedClassName="is-selected">
<span id="session-logs-tab">{intl.formatMessage(intlMessages.sessionLogs)}</span>
</Styled.ConnectionTabSelector>
)
} }
</Styled.Body> </Styled.ConnectionTabList>
{selectedTab === '1' && <Styled.ConnectionTabPanel selectedClassName="is-selected">
this.renderCopyDataButton() <div>
{this.renderNetworkData()}
{this.renderCopyDataButton()}
</div>
</Styled.ConnectionTabPanel>
<Styled.ConnectionTabPanel selectedClassName="is-selected">
<div>{this.renderConnections()}</div>
</Styled.ConnectionTabPanel>
{Service.isModerator()
&& (
<Styled.ConnectionTabPanel selectedClassName="is-selected">
<div>{this.renderConnections()}</div>
</Styled.ConnectionTabPanel>
)
} }
</Styled.Main> </Styled.ConnectionTabs>
</Styled.Container> </Styled.Container>
</Styled.ConnectionStatusModal> </Styled.ConnectionStatusModal>
); );

View File

@ -7,12 +7,13 @@ import {
colorGrayLabel, colorGrayLabel,
colorGrayLightest, colorGrayLightest,
colorPrimary, colorPrimary,
colorWhite,
} from '/imports/ui/stylesheets/styled-components/palette'; } from '/imports/ui/stylesheets/styled-components/palette';
import { import {
smPaddingX, smPaddingX,
smPaddingY, smPaddingY,
mdPaddingY,
lgPaddingY, lgPaddingY,
lgPaddingX,
titlePositionLeft, titlePositionLeft,
mdPaddingX, mdPaddingX,
borderSizeLarge, borderSizeLarge,
@ -26,7 +27,14 @@ import {
hasPhoneDimentions, hasPhoneDimentions,
mediumDown, mediumDown,
hasPhoneWidth, hasPhoneWidth,
smallOnly,
} from '/imports/ui/stylesheets/styled-components/breakpoints'; } from '/imports/ui/stylesheets/styled-components/breakpoints';
import {
ScrollboxVertical,
} from '/imports/ui/stylesheets/styled-components/scrollable';
import {
Tab, Tabs, TabList, TabPanel,
} from 'react-tabs';
const Item = styled.div` const Item = styled.div`
display: flex; display: flex;
@ -169,11 +177,23 @@ const Label = styled.span`
margin-bottom: ${lgPaddingY}; margin-bottom: ${lgPaddingY};
`; `;
const NetworkDataContainer = styled.div` const NetworkDataContainer = styled(ScrollboxVertical)`
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex; display: flex;
flex-wrap: nowrap;
overflow: auto;
scroll-snap-type: x mandatory;
padding-bottom: 1.25rem;
&:focus {
outline: none;
&::-webkit-scrollbar-thumb {
background: rgba(0,0,0,.5);
}
}
@media ${mediumDown} { @media ${mediumDown} {
justify-content: space-between; justify-content: space-between;
} }
@ -202,6 +222,8 @@ const CopyContainer = styled.div`
const ConnectionStatusModal = styled(Modal)` const ConnectionStatusModal = styled(Modal)`
padding: 1rem; padding: 1rem;
height: 28rem;
`; `;
const Container = styled.div` const Container = styled.div`
@ -262,6 +284,19 @@ const Copy = styled.span`
`} `}
`; `;
const HelperWrapper = styled.div`
min-width: 12.5rem;
height: 100%;
@media ${mediumDown} {
flex: none;
width: 100%;
scroll-snap-align: start;
display: flex;
justify-content: center;
}
`;
const Helper = styled.div` const Helper = styled.div`
width: 12.5rem; width: 12.5rem;
height: 100%; height: 100%;
@ -271,12 +306,7 @@ const Helper = styled.div`
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: .5rem;
@media ${mediumDown} {
${({ page }) => page === '1'
? 'display: flex;'
: 'display: none;'}
}
`; `;
const NetworkDataContent = styled.div` const NetworkDataContent = styled.div`
@ -286,9 +316,9 @@ const NetworkDataContent = styled.div`
flex-grow: 1; flex-grow: 1;
@media ${mediumDown} { @media ${mediumDown} {
${({ page }) => page === '2' flex: none;
? 'display: flex;' width: 100%;
: 'display: none;'} scroll-snap-align: start;
} }
`; `;
@ -302,132 +332,103 @@ const DataColumn = styled.div`
} }
`; `;
const Main = styled.div` const ConnectionTabs = styled(Tabs)`
height: 19.5rem;
display: flex; display: flex;
flex-direction: column; flex-flow: column;
justify-content: flex-start;
@media ${smallOnly} {
width: 100%;
flex-flow: column;
}
`; `;
const Body = styled.div` const ConnectionTabList = styled(TabList)`
padding: ${jumboPaddingY} 0; display: flex;
flex-flow: row;
margin: 0; margin: 0;
flex-grow: 1; margin-bottom: .5rem;
overflow: auto;
position: relative;
`;
const Navigation = styled.div`
display: flex;
border: none; border: none;
border-bottom: 1px solid ${colorOffWhite}; padding: 0;
user-select: none; width: calc(100% / 3);
overflow-y: auto;
scrollbar-width: none;
&::-webkit-scrollbar { @media ${smallOnly} {
display: none; width: 100%;
} flex-flow: row;
flex-wrap: wrap;
& :not(:last-child) {
margin: 0;
margin-right: ${lgPaddingX};
}
.activeConnectionStatusTab {
border: none;
border-bottom: 2px solid ${colorPrimary};
color: ${colorPrimary};
}
& * {
cursor: pointer;
white-space: nowrap;
}
[dir="rtl"] & {
& :not(:last-child) {
margin: 0;
margin-left: ${lgPaddingX};
}
}
`;
const Prev = styled.div`
display: none;
margin: 0 .5rem 0 .25rem;
@media ${mediumDown} {
display: flex;
flex-direction: column;
justify-content: center; justify-content: center;
} }
@media ${hasPhoneWidth} {
margin: 0;
}
`; `;
const Next = styled(Prev)` const ConnectionTabPanel = styled(TabPanel)`
margin: 0 .25rem 0 .5rem; display: none;
margin: 0 0 0 1rem;
@media ${hasPhoneWidth} { height: 13rem;
margin: 0;
}
`;
const Button = styled.button`
flex: 0;
margin: 0;
padding: 0;
border: none;
background: none;
color: inherit;
border-radius: 50%;
cursor: pointer;
&:disabled {
cursor: not-allowed;
opacity: .5;
}
&:hover {
opacity: .75;
}
&:focus {
outline: none;
}
@media ${hasPhoneWidth} {
position: absolute;
bottom: 0;
padding: .25rem;
}
`;
const ButtonLeft = styled(Button)`
left: calc(50% - 2rem);
[dir="rtl"] & { [dir="rtl"] & {
left: calc(50%); margin: 0 1rem 0 0;
}
&.is-selected {
display: flex;
flex-flow: column;
}
@media ${smallOnly} {
width: 100%;
margin: 0;
padding-left: 1rem;
padding-right: 1rem;
} }
`; `;
const ButtonRight = styled(Button)` const ConnectionTabSelector = styled(Tab)`
right: calc(50% - 2rem);
[dir="rtl"] & {
right: calc(50%);
}
`;
const Chevron = styled.svg`
display: flex; display: flex;
width: 1rem; flex-flow: row;
height: 1rem; font-size: 0.9rem;
flex: 0 0 auto;
justify-content: flex-start;
border: none !important;
padding: ${mdPaddingY} ${mdPaddingX};
[dir="rtl"] & { border-radius: .2rem;
transform: rotate(180deg); cursor: pointer;
margin-bottom: ${smPaddingY};
align-items: center;
flex-grow: 0;
min-width: 0;
& > span {
min-width: 0;
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@media ${smallOnly} {
max-width: 100%;
margin: 0 ${smPaddingX} 0 0;
& > i {
display: none;
}
[dir="rtl"] & {
margin: 0 0 0 ${smPaddingX};
}
}
span {
border-bottom: 2px solid ${colorWhite};
}
&.is-selected {
border: none;
color: ${colorPrimary};
span {
border-bottom: 2px solid ${colorPrimary};
}
} }
`; `;
@ -461,14 +462,11 @@ export default {
Copy, Copy,
Helper, Helper,
NetworkDataContent, NetworkDataContent,
Main,
Body,
Navigation,
FullName, FullName,
DataColumn, DataColumn,
Prev, HelperWrapper,
Next, ConnectionTabs,
ButtonLeft, ConnectionTabList,
ButtonRight, ConnectionTabSelector,
Chevron, ConnectionTabPanel,
}; };

View File

@ -20,6 +20,10 @@ const intlMessages = defineMessages({
id: 'app.endMeeting.contentWarning', id: 'app.endMeeting.contentWarning',
description: 'end meeting content warning', description: 'end meeting content warning',
}, },
confirmButtonLabel: {
id: 'app.endMeeting.yesLabel',
description: 'end meeting confirm button label',
},
}); });
const { warnAboutUnsavedContentOnMeetingEnd } = Meteor.settings.public.app; const { warnAboutUnsavedContentOnMeetingEnd } = Meteor.settings.public.app;
@ -58,6 +62,7 @@ class EndMeetingComponent extends PureComponent {
description={description} description={description}
confirmButtonColor="danger" confirmButtonColor="danger"
confirmButtonDataTest="confirmEndMeeting" confirmButtonDataTest="confirmEndMeeting"
confirmButtonLabel={intl.formatMessage(intlMessages.confirmButtonLabel)}
/> />
); );
} }

View File

@ -202,6 +202,7 @@ const CustomLayout = (props) => {
}, INITIAL_INPUT_STATE), }, INITIAL_INPUT_STATE),
}); });
} }
Session.set('layoutReady', true);
throttledCalculatesLayout(); throttledCalculatesLayout();
}; };

View File

@ -142,6 +142,7 @@ const PresentationFocusLayout = (props) => {
}, INITIAL_INPUT_STATE), }, INITIAL_INPUT_STATE),
}); });
} }
Session.set('layoutReady', true);
throttledCalculatesLayout(); throttledCalculatesLayout();
}; };

View File

@ -138,6 +138,7 @@ const SmartLayout = (props) => {
}, INITIAL_INPUT_STATE), }, INITIAL_INPUT_STATE),
}); });
} }
Session.set('layoutReady', true);
throttledCalculatesLayout(); throttledCalculatesLayout();
}; };

View File

@ -149,6 +149,7 @@ const VideoFocusLayout = (props) => {
), ),
}); });
} }
Session.set('layoutReady', true);
throttledCalculatesLayout(); throttledCalculatesLayout();
}; };

View File

@ -11,7 +11,7 @@ import BBBMenu from '/imports/ui/components/common/menu/component';
import ShortcutHelpComponent from '/imports/ui/components/shortcut-help/component'; import ShortcutHelpComponent from '/imports/ui/components/shortcut-help/component';
import withShortcutHelper from '/imports/ui/components/shortcut-help/service'; import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
import FullscreenService from '/imports/ui/components/common/fullscreen-button/service'; import FullscreenService from '/imports/ui/components/common/fullscreen-button/service';
import { colorDanger } from '/imports/ui/stylesheets/styled-components/palette'; import { colorDanger, colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
import Styled from './styles'; import Styled from './styles';
import browserInfo from '/imports/utils/browserInfo'; import browserInfo from '/imports/utils/browserInfo';
import deviceInfo from '/imports/utils/deviceInfo'; import deviceInfo from '/imports/utils/deviceInfo';
@ -293,21 +293,7 @@ class SettingsDropdown extends PureComponent {
}, },
); );
if (allowedToEndMeeting && isMeteorConnected) {
this.menuItems.push(
{
key: 'list-item-end-meeting',
icon: 'application',
label: intl.formatMessage(intlMessages.endMeetingLabel),
description: intl.formatMessage(intlMessages.endMeetingDesc),
onClick: () => mountModal(<EndMeetingConfirmationContainer />),
},
);
}
if (allowLogoutSetting && isMeteorConnected) { if (allowLogoutSetting && isMeteorConnected) {
const customStyles = { color: colorDanger };
this.menuItems.push( this.menuItems.push(
{ {
key: 'list-item-logout', key: 'list-item-logout',
@ -315,12 +301,26 @@ class SettingsDropdown extends PureComponent {
icon: 'logout', icon: 'logout',
label: intl.formatMessage(intlMessages.leaveSessionLabel), label: intl.formatMessage(intlMessages.leaveSessionLabel),
description: intl.formatMessage(intlMessages.leaveSessionDesc), description: intl.formatMessage(intlMessages.leaveSessionDesc),
customStyles,
onClick: () => this.leaveSession(), onClick: () => this.leaveSession(),
}, },
); );
} }
if (allowedToEndMeeting && isMeteorConnected) {
const customStyles = { background: colorDanger, color: colorWhite };
this.menuItems.push(
{
key: 'list-item-end-meeting',
icon: 'application',
label: intl.formatMessage(intlMessages.endMeetingLabel),
description: intl.formatMessage(intlMessages.endMeetingDesc),
customStyles,
onClick: () => mountModal(<EndMeetingConfirmationContainer />),
},
);
}
return this.menuItems; return this.menuItems;
} }

View File

@ -45,6 +45,7 @@ class TalkingIndicator extends PureComponent {
talkers, talkers,
amIModerator, amIModerator,
moreThanMaxIndicators, moreThanMaxIndicators,
users,
} = this.props; } = this.props;
if (!talkers) return null; if (!talkers) return null;
@ -58,9 +59,13 @@ class TalkingIndicator extends PureComponent {
callerName, callerName,
} = talkers[`${id}`]; } = talkers[`${id}`];
const user = users[id];
const name = user?.name ?? callerName;
const ariaLabel = intl.formatMessage(talking const ariaLabel = intl.formatMessage(talking
? intlMessages.isTalking : intlMessages.wasTalking, { ? intlMessages.isTalking : intlMessages.wasTalking, {
0: callerName, 0: name,
}); });
let icon = talking ? 'unmute' : 'blank'; let icon = talking ? 'unmute' : 'blank';
@ -68,7 +73,7 @@ class TalkingIndicator extends PureComponent {
return ( return (
<Styled.TalkingIndicatorWrapper <Styled.TalkingIndicatorWrapper
key={_.uniqueId(`${callerName}-`)} key={_.uniqueId(`${name}-`)}
muted={muted} muted={muted}
talking={talking} talking={talking}
floor={floor} floor={floor}
@ -84,11 +89,11 @@ class TalkingIndicator extends PureComponent {
$spoke={!talking || undefined} $spoke={!talking || undefined}
$muted={muted} $muted={muted}
$isViewer={!amIModerator || undefined} $isViewer={!amIModerator || undefined}
key={_.uniqueId(`${callerName}-`)} key={_.uniqueId(`${name}-`)}
onClick={() => this.handleMuteUser(id)} onClick={() => this.handleMuteUser(id)}
label={callerName} label={name}
tooltipLabel={!muted && amIModerator tooltipLabel={!muted && amIModerator
? `${intl.formatMessage(intlMessages.muteLabel)} ${callerName}` ? `${intl.formatMessage(intlMessages.muteLabel)} ${name}`
: null} : null}
data-test={talking ? 'isTalking' : 'wasTalking'} data-test={talking ? 'isTalking' : 'wasTalking'}
aria-label={ariaLabel} aria-label={ariaLabel}

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