Merge branch 'v2.6.x-release' into capture-slides-upload-toast
This commit is contained in:
commit
a8657ff0ed
2
.github/workflows/automated-tests.yml
vendored
2
.github/workflows/automated-tests.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
build-install-and-test:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- run: ./build/get_external_dependencies.sh
|
||||
- run: ./build/setup.sh bbb-apps-akka
|
||||
- run: ./build/setup.sh bbb-config
|
||||
|
5
.github/workflows/check-merge-conflict.yml
vendored
5
.github/workflows/check-merge-conflict.yml
vendored
@ -6,8 +6,13 @@ on:
|
||||
- opened
|
||||
- synchronize
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
main:
|
||||
permissions:
|
||||
pull-requests: write # for eps1lon/actions-label-merge-conflict to label PRs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check for dirty pull requests
|
||||
|
@ -11,7 +11,7 @@ stages:
|
||||
|
||||
# define which docker image to use for builds
|
||||
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.
|
||||
# it then checks an API endpoint on the package server to find out for which of
|
||||
|
@ -18,7 +18,7 @@ val compileSettings = Seq(
|
||||
"-Xlint",
|
||||
"-Ywarn-dead-code",
|
||||
"-language:_",
|
||||
"-target:jvm-1.11",
|
||||
"-target:11",
|
||||
"-encoding", "UTF-8"
|
||||
),
|
||||
javacOptions ++= List(
|
||||
@ -48,7 +48,7 @@ lazy val bbbAppsAkka = (project in file(".")).settings(name := "bbb-apps-akka",
|
||||
// Config file is in ./.scalariform.conf
|
||||
scalariformAutoformat := true
|
||||
|
||||
scalaVersion := "2.13.4"
|
||||
scalaVersion := "2.13.9"
|
||||
//-----------
|
||||
// Packaging
|
||||
//
|
||||
|
@ -7,7 +7,7 @@ object Dependencies {
|
||||
|
||||
object Versions {
|
||||
// Scala
|
||||
val scala = "2.13.4"
|
||||
val scala = "2.13.9"
|
||||
val junit = "4.12"
|
||||
val junitInterface = "0.11"
|
||||
val scalactic = "3.0.8"
|
||||
@ -26,7 +26,7 @@ object Dependencies {
|
||||
val codec = "1.15"
|
||||
|
||||
// BigBlueButton
|
||||
val bbbCommons = "0.0.21-SNAPSHOT"
|
||||
val bbbCommons = "0.0.22-SNAPSHOT"
|
||||
|
||||
// Test
|
||||
val scalaTest = "3.2.11"
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -26,7 +26,8 @@ class PresentationPodHdlrs(implicit val context: ActorContext)
|
||||
with PresentationPageConvertedSysMsgHdlr
|
||||
with PresentationPageConversionStartedSysMsgHdlr
|
||||
with PresentationConversionEndedSysMsgHdlr
|
||||
with PresentationUploadedFileTimeoutErrorPubMsgHdlr {
|
||||
with PresentationUploadedFileTimeoutErrorPubMsgHdlr
|
||||
with PresentationHasInvalidMimeTypeErrorPubMsgHdlr {
|
||||
|
||||
val log = Logging(context.system, getClass)
|
||||
}
|
||||
|
@ -13,6 +13,12 @@ import java.io.File
|
||||
trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
|
||||
this: PresentationPodHdlrs =>
|
||||
|
||||
object JobTypes {
|
||||
val DOWNLOAD = "PresentationWithAnnotationDownloadJob"
|
||||
val CAPTURE_PRESENTATION = "PresentationWithAnnotationExportJob"
|
||||
val CAPTURE_NOTES = "PadCaptureJob"
|
||||
}
|
||||
|
||||
def buildStoreAnnotationsInRedisSysMsg(annotations: StoredAnnotations, liveMeeting: LiveMeeting): BbbCommonEnvCoreMsg = {
|
||||
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
|
||||
val envelope = BbbCoreEnvelope(StoreAnnotationsInRedisSysMsg.NAME, routing)
|
||||
@ -111,7 +117,6 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
|
||||
log.error(s"Presentation ${presId} not found in meeting ${meetingId}")
|
||||
} else {
|
||||
|
||||
val jobType: String = "PresentationWithAnnotationDownloadJob"
|
||||
val jobId: String = RandomStringGenerator.randomAlphanumericString(16);
|
||||
val allPages: Boolean = m.body.allPages
|
||||
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 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);
|
||||
|
||||
// Send Export Job to Redis
|
||||
@ -147,7 +152,6 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
|
||||
} else {
|
||||
|
||||
val jobId: String = s"${meetingId}-slides" // Used as the temporaryPresentationId upon upload
|
||||
val jobType = "PresentationWithAnnotationExportJob"
|
||||
val allPages: Boolean = m.allPages
|
||||
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
|
||||
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);
|
||||
|
||||
// Send Export Job to Redis
|
||||
@ -201,13 +205,12 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
|
||||
|
||||
val userId: String = "system"
|
||||
val jobId: String = s"${m.body.breakoutId}-notes" // Used as the temporaryPresentationId upon upload
|
||||
val jobType = "PadCaptureJob"
|
||||
val filename = m.body.filename
|
||||
val presentationUploadToken: String = PresentationPodsApp.generateToken("DEFAULT_PRESENTATION_POD", userId)
|
||||
|
||||
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)
|
||||
|
||||
bus.outGW.send(job)
|
||||
|
@ -42,6 +42,17 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait {
|
||||
|
||||
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 (settings.disableCam) {
|
||||
val notifyEvent = MsgBuilder.buildNotifyAllInMeetingEvtMsg(
|
||||
@ -55,24 +66,6 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait {
|
||||
outGW.send(notifyEvent)
|
||||
|
||||
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 {
|
||||
val notifyEvent = MsgBuilder.buildNotifyAllInMeetingEvtMsg(
|
||||
liveMeeting.props.meetingProp.intId,
|
||||
@ -97,8 +90,12 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait {
|
||||
Vector()
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
|
||||
// Apply lock settings when disableMic from false to true.
|
||||
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)
|
||||
}
|
||||
}
|
||||
LockSettingsUtil.enforceLockSettingsForAllVoiceUsers(liveMeeting, outGW)
|
||||
} else {
|
||||
val notifyEvent = MsgBuilder.buildNotifyAllInMeetingEvtMsg(
|
||||
|
@ -288,6 +288,8 @@ class ReceivedJsonMsgHandlerActor(
|
||||
routeGenericMsg[PreuploadedPresentationsSysPubMsg](envelope, jsonNode)
|
||||
case PresentationUploadedFileTooLargeErrorSysPubMsg.NAME =>
|
||||
routeGenericMsg[PresentationUploadedFileTooLargeErrorSysPubMsg](envelope, jsonNode)
|
||||
case PresentationHasInvalidMimeTypeErrorSysPubMsg.NAME =>
|
||||
routeGenericMsg[PresentationHasInvalidMimeTypeErrorSysPubMsg](envelope, jsonNode)
|
||||
case PresentationUploadedFileTimeoutErrorSysPubMsg.NAME =>
|
||||
routeGenericMsg[PresentationUploadedFileTimeoutErrorSysPubMsg](envelope, jsonNode)
|
||||
case PresentationConversionUpdateSysPubMsg.NAME =>
|
||||
|
@ -523,6 +523,7 @@ class MeetingActor(
|
||||
case m: SetPresentationDownloadablePubMsg => 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: PresentationHasInvalidMimeTypeErrorSysPubMsg => 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: PresentationPageCountErrorSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
|
@ -85,6 +85,7 @@ case class PresentationSlide(
|
||||
presentationId: String,
|
||||
pageNum: Long,
|
||||
setOn: Long = System.currentTimeMillis(),
|
||||
presentationName: String,
|
||||
)
|
||||
|
||||
|
||||
@ -189,7 +190,7 @@ class LearningDashboardActor(
|
||||
for {
|
||||
page <- msg.body.presentation.pages.find(p => p.current == true)
|
||||
} 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)
|
||||
page <- presentation.pages.find(p => p.id == msg.body.pageId)
|
||||
} 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)
|
||||
} yield {
|
||||
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);
|
||||
if(presPreviousSlides.length > 0) {
|
||||
//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 {
|
||||
//If none page was showed yet, set the current page (page 1 by default)
|
||||
for {
|
||||
@ -231,20 +232,20 @@ class LearningDashboardActor(
|
||||
presentation <- presentations.get(msg.body.presentationId)
|
||||
page <- presentation.pages.find(s => s.current == true)
|
||||
} 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 {
|
||||
meeting <- meetings.values.find(m => m.intId == meetingId)
|
||||
} yield {
|
||||
if (meeting.presentationSlides.length == 0 ||
|
||||
meeting.presentationSlides.last.presentationId != presentationId ||
|
||||
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)
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ val compileSettings = Seq(
|
||||
"-Xlint",
|
||||
"-Ywarn-dead-code",
|
||||
"-language:_",
|
||||
"-target:jvm-1.11",
|
||||
"-target:11",
|
||||
"-encoding", "UTF-8"
|
||||
),
|
||||
javacOptions ++= List(
|
||||
@ -27,7 +27,7 @@ val compileSettings = Seq(
|
||||
)
|
||||
)
|
||||
|
||||
scalaVersion := "2.13.4"
|
||||
scalaVersion := "2.13.9"
|
||||
|
||||
resolvers += Resolver.sonatypeRepo("releases")
|
||||
|
||||
|
@ -7,7 +7,7 @@ object Dependencies {
|
||||
|
||||
object Versions {
|
||||
// Scala
|
||||
val scala = "2.13.4"
|
||||
val scala = "2.13.9"
|
||||
val junitInterface = "0.11"
|
||||
val scalactic = "3.0.8"
|
||||
|
||||
@ -21,8 +21,8 @@ object Dependencies {
|
||||
val codec = "1.15"
|
||||
|
||||
// BigBlueButton
|
||||
val bbbCommons = "0.0.21-SNAPSHOT"
|
||||
val bbbFsesl = "0.0.8-SNAPSHOT"
|
||||
val bbbCommons = "0.0.22-SNAPSHOT"
|
||||
val bbbFsesl = "0.0.9-SNAPSHOT"
|
||||
|
||||
// Test
|
||||
val scalaTest = "3.2.11"
|
||||
|
@ -1,7 +1,7 @@
|
||||
import org.bigbluebutton.build._
|
||||
|
||||
|
||||
version := "0.0.21-SNAPSHOT"
|
||||
version := "0.0.22-SNAPSHOT"
|
||||
|
||||
val compileSettings = Seq(
|
||||
organization := "org.bigbluebutton",
|
||||
@ -12,7 +12,7 @@ val compileSettings = Seq(
|
||||
"-Xlint",
|
||||
"-Ywarn-dead-code",
|
||||
"-language:_",
|
||||
"-target:jvm-1.11",
|
||||
"-target:11",
|
||||
"-encoding", "UTF-8"
|
||||
),
|
||||
javacOptions ++= List(
|
||||
@ -55,7 +55,7 @@ scalariformAutoformat := true
|
||||
// Do not append Scala versions to the generated artifacts
|
||||
//crossPaths := false
|
||||
|
||||
scalaVersion := "2.13.4"
|
||||
scalaVersion := "2.13.9"
|
||||
|
||||
// This forbids including Scala related libraries into the dependency
|
||||
//autoScalaLibrary := false
|
||||
|
@ -7,7 +7,7 @@ object Dependencies {
|
||||
|
||||
object Versions {
|
||||
// Scala
|
||||
val scala = "2.13.4"
|
||||
val scala = "2.13.9"
|
||||
val junit = "4.12"
|
||||
val junitInterface = "0.11"
|
||||
val scalactic = "3.0.8"
|
||||
|
@ -17,8 +17,8 @@ case class MeetingProp(
|
||||
isBreakout: Boolean,
|
||||
disabledFeatures: Vector[String],
|
||||
notifyRecordingIsOn: Boolean,
|
||||
uploadExternalDescription: String,
|
||||
uploadExternalUrl: String,
|
||||
presentationUploadExternalDescription: String,
|
||||
presentationUploadExternalUrl: String,
|
||||
)
|
||||
|
||||
case class BreakoutProps(
|
||||
|
@ -166,6 +166,23 @@ case class PresentationUploadedFileTooLargeErrorSysPubMsgBody(
|
||||
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" }
|
||||
case class PresentationUploadedFileTimeoutErrorSysPubMsg(
|
||||
header: BbbClientMsgHeader,
|
||||
@ -237,6 +254,13 @@ object PresentationUploadedFileTooLargeErrorEvtMsg { val NAME = "PresentationUpl
|
||||
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)
|
||||
|
||||
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" }
|
||||
case class PresentationUploadedFileTimeoutErrorEvtMsg(header: BbbClientMsgHeader, body: PresentationUploadedFileTimeoutErrorEvtMsgBody) extends BbbCoreMsg
|
||||
case class PresentationUploadedFileTimeoutErrorEvtMsgBody(podId: String, meetingId: String, presentationName: String,
|
||||
|
@ -11,7 +11,7 @@ val compileSettings = Seq(
|
||||
"-Xlint",
|
||||
"-Ywarn-dead-code",
|
||||
"-language:_",
|
||||
"-target:jvm-1.11",
|
||||
"-target:11",
|
||||
"-encoding", "UTF-8"
|
||||
),
|
||||
javacOptions ++= List(
|
||||
@ -40,7 +40,7 @@ lazy val commonWeb = (project in file(".")).settings(name := "bbb-common-web", l
|
||||
// Config file is in ./.scalariform.conf
|
||||
scalariformAutoformat := true
|
||||
|
||||
scalaVersion := "2.13.4"
|
||||
scalaVersion := "2.13.9"
|
||||
//-----------
|
||||
// Packaging
|
||||
//
|
||||
|
@ -7,7 +7,7 @@ object Dependencies {
|
||||
|
||||
object Versions {
|
||||
// Scala
|
||||
val scala = "2.13.4"
|
||||
val scala = "2.13.9"
|
||||
val junit = "4.12"
|
||||
val junitInterface = "0.11"
|
||||
val scalactic = "3.0.8"
|
||||
@ -34,7 +34,7 @@ object Dependencies {
|
||||
val text = "1.10.0"
|
||||
|
||||
// BigBlueButton
|
||||
val bbbCommons = "0.0.21-SNAPSHOT"
|
||||
val bbbCommons = "0.0.22-SNAPSHOT"
|
||||
|
||||
// Test
|
||||
val scalaTest = "3.2.11"
|
||||
|
@ -73,8 +73,8 @@ public class ApiParams {
|
||||
public static final String DISABLED_FEATURES = "disabledFeatures";
|
||||
public static final String NOTIFY_RECORDING_IS_ON = "notifyRecordingIsOn";
|
||||
|
||||
public static final String UPLOAD_EXTERNAL_DESCRIPTION = "uploadExternalDescription";
|
||||
public static final String UPLOAD_EXTERNAL_URL = "uploadExternalUrl";
|
||||
public static final String PRESENTATION_UPLOAD_EXTERNAL_DESCRIPTION = "presentationUploadExternalDescription";
|
||||
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_NOTES = "breakoutRoomsCaptureNotes";
|
||||
|
@ -419,7 +419,7 @@ public class MeetingService implements MessageListener {
|
||||
m.getMuteOnStart(), m.getAllowModsToUnmuteUsers(), m.getAllowModsToEjectCameras(), m.getMeetingKeepEvents(),
|
||||
m.breakoutRoomsParams, m.lockSettingsParams, m.getHtml5InstanceId(),
|
||||
m.getGroups(), m.getDisabledFeatures(), m.getNotifyRecordingIsOn(),
|
||||
m.getUploadExternalDescription(), m.getUploadExternalUrl());
|
||||
m.getPresentationUploadExternalDescription(), m.getPresentationUploadExternalUrl());
|
||||
}
|
||||
|
||||
private String formatPrettyDate(Long timestamp) {
|
||||
|
@ -47,6 +47,7 @@ import org.bigbluebutton.api.domain.BreakoutRoomsParams;
|
||||
import org.bigbluebutton.api.domain.LockSettingsParams;
|
||||
import org.bigbluebutton.api.domain.Meeting;
|
||||
import org.bigbluebutton.api.domain.Group;
|
||||
import org.bigbluebutton.api.service.ServiceUtils;
|
||||
import org.bigbluebutton.api.util.ParamsUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -80,6 +81,7 @@ public class ParamsProcessorUtil {
|
||||
private String defaultHTML5ClientUrl;
|
||||
private String defaultGuestWaitURL;
|
||||
private Boolean allowRequestsWithoutSession = false;
|
||||
private Integer defaultHttpSessionTimeout = 14400;
|
||||
private Boolean useDefaultAvatar = false;
|
||||
private String defaultAvatarURL;
|
||||
private String defaultGuestPolicy;
|
||||
@ -103,8 +105,8 @@ public class ParamsProcessorUtil {
|
||||
private boolean defaultKeepEvents = false;
|
||||
private Boolean useDefaultLogo;
|
||||
private String defaultLogoURL;
|
||||
private String defaultUploadExternalDescription = "";
|
||||
private String defaultUploadExternalUrl = "";
|
||||
private String defaultPresentationUploadExternalDescription = "";
|
||||
private String defaultPresentationUploadExternalUrl = "";
|
||||
|
||||
private boolean defaultBreakoutRoomsEnabled = true;
|
||||
private boolean defaultBreakoutRoomsRecord;
|
||||
@ -638,14 +640,14 @@ public class ParamsProcessorUtil {
|
||||
guestPolicy = params.get(ApiParams.GUEST_POLICY);
|
||||
}
|
||||
|
||||
String uploadExternalDescription = defaultUploadExternalDescription;
|
||||
if (!StringUtils.isEmpty(params.get(ApiParams.UPLOAD_EXTERNAL_DESCRIPTION))) {
|
||||
uploadExternalDescription = params.get(ApiParams.UPLOAD_EXTERNAL_DESCRIPTION);
|
||||
String presentationUploadExternalDescription = defaultPresentationUploadExternalDescription;
|
||||
if (!StringUtils.isEmpty(params.get(ApiParams.PRESENTATION_UPLOAD_EXTERNAL_DESCRIPTION))) {
|
||||
presentationUploadExternalDescription = params.get(ApiParams.PRESENTATION_UPLOAD_EXTERNAL_DESCRIPTION);
|
||||
}
|
||||
|
||||
String uploadExternalUrl = defaultUploadExternalUrl;
|
||||
if (!StringUtils.isEmpty(params.get(ApiParams.UPLOAD_EXTERNAL_URL))) {
|
||||
uploadExternalUrl = params.get(ApiParams.UPLOAD_EXTERNAL_URL);
|
||||
String presentationUploadExternalUrl = defaultPresentationUploadExternalUrl;
|
||||
if (!StringUtils.isEmpty(params.get(ApiParams.PRESENTATION_UPLOAD_EXTERNAL_URL))) {
|
||||
presentationUploadExternalUrl = params.get(ApiParams.PRESENTATION_UPLOAD_EXTERNAL_URL);
|
||||
}
|
||||
|
||||
String meetingLayout = defaultMeetingLayout;
|
||||
@ -685,7 +687,7 @@ public class ParamsProcessorUtil {
|
||||
String parentMeetingId = "";
|
||||
if (isBreakout) {
|
||||
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
|
||||
// meeting, the shared timestamp and the sequence number
|
||||
String timeStamp = StringUtils.substringAfter(internalMeetingId, "-");
|
||||
@ -736,8 +738,8 @@ public class ParamsProcessorUtil {
|
||||
.withGroups(groups)
|
||||
.withDisabledFeatures(listOfDisabledFeatures)
|
||||
.withNotifyRecordingIsOn(notifyRecordingIsOn)
|
||||
.withUploadExternalDescription(uploadExternalDescription)
|
||||
.withUploadExternalUrl(uploadExternalUrl)
|
||||
.withPresentationUploadExternalDescription(presentationUploadExternalDescription)
|
||||
.withPresentationUploadExternalUrl(presentationUploadExternalUrl)
|
||||
.build();
|
||||
|
||||
if (!StringUtils.isEmpty(params.get(ApiParams.MODERATOR_ONLY_MESSAGE))) {
|
||||
@ -845,6 +847,14 @@ public class ParamsProcessorUtil {
|
||||
return allowRequestsWithoutSession;
|
||||
}
|
||||
|
||||
public Integer getDefaultHttpSessionTimeout() {
|
||||
return defaultHttpSessionTimeout;
|
||||
}
|
||||
|
||||
public void setDefaultHttpSessionTimeout(Integer value) {
|
||||
this.defaultHttpSessionTimeout = value;
|
||||
}
|
||||
|
||||
public String getDefaultLogoutUrl() {
|
||||
if ((StringUtils.isEmpty(defaultLogoutUrl)) || "default".equalsIgnoreCase(defaultLogoutUrl)) {
|
||||
return defaultServerUrl;
|
||||
@ -1444,12 +1454,12 @@ public class ParamsProcessorUtil {
|
||||
this.defaultNotifyRecordingIsOn = notifyRecordingIsOn;
|
||||
}
|
||||
|
||||
public void setUploadExternalDescription(String uploadExternalDescription) {
|
||||
this.defaultUploadExternalDescription = uploadExternalDescription;
|
||||
public void setPresentationUploadExternalDescription(String presentationUploadExternalDescription) {
|
||||
this.defaultPresentationUploadExternalDescription = presentationUploadExternalDescription;
|
||||
}
|
||||
|
||||
public void setUploadExternalUrl(String uploadExternalUrl) {
|
||||
this.defaultUploadExternalUrl = uploadExternalUrl;
|
||||
public void setPresentationUploadExternalUrl(String presentationUploadExternalUrl) {
|
||||
this.defaultPresentationUploadExternalUrl = presentationUploadExternalUrl;
|
||||
}
|
||||
|
||||
public void setBbbVersion(String version) {
|
||||
|
@ -97,8 +97,8 @@ public class Meeting {
|
||||
private Boolean allowRequestsWithoutSession = false;
|
||||
private Boolean allowModsToEjectCameras = false;
|
||||
private Boolean meetingKeepEvents;
|
||||
private String uploadExternalDescription;
|
||||
private String uploadExternalUrl;
|
||||
private String presentationUploadExternalDescription;
|
||||
private String presentationUploadExternalUrl;
|
||||
|
||||
private Integer meetingExpireIfNoUserJoinedInMinutes = 5;
|
||||
private Integer meetingExpireWhenLastUserLeftInMinutes = 1;
|
||||
@ -123,8 +123,8 @@ public class Meeting {
|
||||
intMeetingId = builder.internalId;
|
||||
disabledFeatures = builder.disabledFeatures;
|
||||
notifyRecordingIsOn = builder.notifyRecordingIsOn;
|
||||
uploadExternalDescription = builder.uploadExternalDescription;
|
||||
uploadExternalUrl = builder.uploadExternalUrl;
|
||||
presentationUploadExternalDescription = builder.presentationUploadExternalDescription;
|
||||
presentationUploadExternalUrl = builder.presentationUploadExternalUrl;
|
||||
if (builder.viewerPass == null){
|
||||
viewerPass = "";
|
||||
} else {
|
||||
@ -424,11 +424,11 @@ public class Meeting {
|
||||
return notifyRecordingIsOn;
|
||||
}
|
||||
|
||||
public String getUploadExternalDescription() {
|
||||
return uploadExternalDescription;
|
||||
public String getPresentationUploadExternalDescription() {
|
||||
return presentationUploadExternalDescription;
|
||||
}
|
||||
public String getUploadExternalUrl() {
|
||||
return uploadExternalUrl;
|
||||
public String getPresentationUploadExternalUrl() {
|
||||
return presentationUploadExternalUrl;
|
||||
}
|
||||
|
||||
public String getWelcomeMessageTemplate() {
|
||||
@ -863,8 +863,8 @@ public class Meeting {
|
||||
private String learningDashboardAccessToken;
|
||||
private ArrayList<String> disabledFeatures;
|
||||
private Boolean notifyRecordingIsOn;
|
||||
private String uploadExternalDescription;
|
||||
private String uploadExternalUrl;
|
||||
private String presentationUploadExternalDescription;
|
||||
private String presentationUploadExternalUrl;
|
||||
private int duration;
|
||||
private String webVoice;
|
||||
private String telVoice;
|
||||
@ -993,13 +993,13 @@ public class Meeting {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withUploadExternalDescription(String d) {
|
||||
this.uploadExternalDescription = d;
|
||||
public Builder withPresentationUploadExternalDescription(String d) {
|
||||
this.presentationUploadExternalDescription = d;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withUploadExternalUrl(String u) {
|
||||
this.uploadExternalUrl = u;
|
||||
public Builder withPresentationUploadExternalUrl(String u) {
|
||||
this.presentationUploadExternalUrl = u;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -25,8 +25,8 @@ public class CreateMeetingMessage {
|
||||
public final String learningDashboardAccessToken;
|
||||
public final ArrayList<String> disabledFeatures;
|
||||
public final Boolean notifyRecordingIsOn;
|
||||
public final String uploadExternalDescription;
|
||||
public final String uploadExternalUrl;
|
||||
public final String presentationUploadExternalDescription;
|
||||
public final String presentationUploadExternalUrl;
|
||||
public final Long createTime;
|
||||
public final String createDate;
|
||||
public final Map<String, String> metadata;
|
||||
@ -38,8 +38,8 @@ public class CreateMeetingMessage {
|
||||
String viewerPass, String learningDashboardAccessToken,
|
||||
ArrayList<String> disabledFeatures,
|
||||
Boolean notifyRecordingIsOn,
|
||||
String uploadExternalDescription,
|
||||
String uploadExternalUrl,
|
||||
String presentationUploadExternalDescription,
|
||||
String presentationUploadExternalUrl,
|
||||
Long createTime, String createDate, Map<String, String> metadata) {
|
||||
this.id = id;
|
||||
this.externalId = externalId;
|
||||
@ -58,8 +58,8 @@ public class CreateMeetingMessage {
|
||||
this.learningDashboardAccessToken = learningDashboardAccessToken;
|
||||
this.disabledFeatures = disabledFeatures;
|
||||
this.notifyRecordingIsOn = notifyRecordingIsOn;
|
||||
this.uploadExternalDescription = uploadExternalDescription;
|
||||
this.uploadExternalUrl = uploadExternalUrl;
|
||||
this.presentationUploadExternalDescription = presentationUploadExternalDescription;
|
||||
this.presentationUploadExternalUrl = presentationUploadExternalUrl;
|
||||
this.createTime = createTime;
|
||||
this.createDate = createDate;
|
||||
this.metadata = metadata;
|
||||
|
@ -18,7 +18,7 @@ public class ParamsUtil {
|
||||
public static final String INVALID_CHARS = ",";
|
||||
|
||||
public static String stripControlChars(String text) {
|
||||
return text.replaceAll("\\p{Cc}", "");
|
||||
return text.replaceAll("\\p{Cc}", "").trim();
|
||||
}
|
||||
|
||||
public static String escapeHTMLTags(String value) {
|
||||
|
@ -43,8 +43,8 @@ public interface IBbbWebApiGWApp {
|
||||
ArrayList<Group> groups,
|
||||
ArrayList<String> disabledFeatures,
|
||||
Boolean notifyRecordingIsOn,
|
||||
String uploadExternalDescription,
|
||||
String uploadExternalUrl);
|
||||
String presentationUploadExternalDescription,
|
||||
String presentationUploadExternalUrl);
|
||||
|
||||
void registerUser(String meetingID, String internalUserId, String fullname, String role,
|
||||
String externUserID, String authToken, String avatarURL,
|
||||
|
@ -21,4 +21,5 @@ package org.bigbluebutton.presentation;
|
||||
|
||||
public interface DocumentConversionService {
|
||||
void processDocument(UploadedPresentation pres);
|
||||
void sendDocConversionFailedOnMimeType(UploadedPresentation pres, String fileMime, String fileExtension);
|
||||
}
|
||||
|
@ -19,15 +19,19 @@
|
||||
|
||||
package org.bigbluebutton.presentation;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.bigbluebutton.api2.IBbbWebApiGWApp;
|
||||
import org.bigbluebutton.presentation.imp.*;
|
||||
import org.bigbluebutton.presentation.messages.DocConversionRequestReceived;
|
||||
import org.bigbluebutton.presentation.messages.DocInvalidMimeType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import static org.bigbluebutton.presentation.Util.deleteDirectoryFromFileHandlingErrors;
|
||||
|
||||
public class DocumentConversionServiceImp implements DocumentConversionService {
|
||||
private static Logger log = LoggerFactory.getLogger(DocumentConversionServiceImp.class);
|
||||
|
||||
@ -93,6 +97,9 @@ public class DocumentConversionServiceImp implements DocumentConversionService {
|
||||
}
|
||||
|
||||
} else {
|
||||
File presentationFile = pres.getUploadedFile();
|
||||
deleteDirectoryFromFileHandlingErrors(presentationFile);
|
||||
|
||||
Map<String, Object> logData = new HashMap<String, Object>();
|
||||
logData = new HashMap<String, Object>();
|
||||
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) {
|
||||
if (! pres.isConversionStarted()) {
|
||||
Map<String, Object> logData = new HashMap<String, Object>();
|
||||
|
@ -39,5 +39,6 @@ public final class FileTypeConstants {
|
||||
public static final String JPG = "jpg";
|
||||
public static final String JPEG = "jpeg";
|
||||
public static final String PNG = "png";
|
||||
public static final String SVG = "svg";
|
||||
private FileTypeConstants() {} // Prevent instantiation
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -19,8 +19,15 @@
|
||||
|
||||
package org.bigbluebutton.presentation;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
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.List;
|
||||
import java.util.Collections;
|
||||
@ -28,6 +35,9 @@ import java.util.Collections;
|
||||
@SuppressWarnings("serial")
|
||||
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) {
|
||||
{
|
||||
// Add all the supported files
|
||||
@ -76,4 +86,56 @@ public final class SupportedFileTypes {
|
||||
public static boolean isImageFile(String fileExtension) {
|
||||
return IMAGE_FILE_LIST.contains(fileExtension.toLowerCase());
|
||||
}
|
||||
|
||||
/*
|
||||
* It was tested native java methods to detect mimetypes, such as:
|
||||
* - URLConnection.guessContentTypeFromStream(InputStream is);
|
||||
* - Files.probeContentType(Path path);
|
||||
* - FileNameMap fileNameMap.getContentTypeFor(String file.getName());
|
||||
* - MimetypesFileTypeMap fileTypeMap.getContentType(File file);
|
||||
* But none of them was as successful as the linux based command
|
||||
*/
|
||||
public static String detectMimeType(File pres) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -19,9 +19,14 @@
|
||||
|
||||
package org.bigbluebutton.presentation;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public final class Util {
|
||||
private static Logger log = LoggerFactory.getLogger(Util.class);
|
||||
|
||||
public static void deleteDirectory(File directory) {
|
||||
/**
|
||||
@ -40,4 +45,20 @@ public final class Util {
|
||||
// Now that the directory is empty. Delete it.
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -35,6 +35,8 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import static org.bigbluebutton.presentation.Util.deleteDirectoryFromFileHandlingErrors;
|
||||
|
||||
public abstract class Office2PdfPageConverter {
|
||||
private static Logger log = LoggerFactory.getLogger(Office2PdfPageConverter.class);
|
||||
|
||||
@ -95,6 +97,7 @@ public abstract class Office2PdfPageConverter {
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
deleteDirectoryFromFileHandlingErrors(presentationFile);
|
||||
Map<String, Object> logData = new HashMap<>();
|
||||
logData.put("meetingId", pres.getMeetingId());
|
||||
logData.put("presId", pres.getId());
|
||||
|
@ -50,6 +50,20 @@ public class SlidesGenerationProgressNotifier {
|
||||
maxUploadFileSize);
|
||||
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) {
|
||||
UploadFileTimedoutMessage errorMessage = new UploadFileTimedoutMessage(
|
||||
pres.getPodId(),
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -150,8 +150,8 @@ class BbbWebApiGWApp(
|
||||
groups: java.util.ArrayList[Group],
|
||||
disabledFeatures: java.util.ArrayList[String],
|
||||
notifyRecordingIsOn: java.lang.Boolean,
|
||||
uploadExternalDescription: String,
|
||||
uploadExternalUrl: String): Unit = {
|
||||
presentationUploadExternalDescription: String,
|
||||
presentationUploadExternalUrl: String): Unit = {
|
||||
|
||||
val disabledFeaturesAsVector: Vector[String] = disabledFeatures.asScala.toVector
|
||||
|
||||
@ -164,8 +164,8 @@ class BbbWebApiGWApp(
|
||||
isBreakout = isBreakout.booleanValue(),
|
||||
disabledFeaturesAsVector,
|
||||
notifyRecordingIsOn,
|
||||
uploadExternalDescription,
|
||||
uploadExternalUrl
|
||||
presentationUploadExternalDescription,
|
||||
presentationUploadExternalUrl
|
||||
)
|
||||
|
||||
val durationProps = DurationProps(
|
||||
@ -346,6 +346,9 @@ class BbbWebApiGWApp(
|
||||
} else if (msg.isInstanceOf[UploadFileTimedoutMessage]) {
|
||||
val event = MsgBuilder.buildPresentationUploadedFileTimedoutErrorSysMsg(msg.asInstanceOf[UploadFileTimedoutMessage])
|
||||
msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event))
|
||||
} else if (msg.isInstanceOf[DocInvalidMimeType]) {
|
||||
val event = MsgBuilder.buildPresentationHasInvalidMimeType(msg.asInstanceOf[DocInvalidMimeType])
|
||||
msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -285,6 +285,19 @@ object MsgBuilder {
|
||||
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 = {
|
||||
val routing = collection.immutable.HashMap("sender" -> "bbb-web")
|
||||
val envelope = BbbCoreEnvelope(PresentationUploadedFileTimeoutErrorSysPubMsg.NAME, routing)
|
||||
|
1
bbb-export-annotations/.npmrc
Normal file
1
bbb-export-annotations/.npmrc
Normal file
@ -0,0 +1 @@
|
||||
engine-strict=true
|
@ -1,3 +1,13 @@
|
||||
let _ = require('lodash');
|
||||
let fs = require('fs');
|
||||
const settings = require('./settings');
|
||||
const LOCAL_SETTINGS_FILE_PATH = '/etc/bigbluebutton/bbb-export-annotations.json';
|
||||
|
||||
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;
|
||||
|
@ -26,6 +26,7 @@
|
||||
"msgName": "NewPresAnnFileAvailableMsg"
|
||||
},
|
||||
"bbbWebAPI": "http://127.0.0.1:8090",
|
||||
"bbbWebPublicAPI": "/bigbluebutton/",
|
||||
"bbbPadsAPI": "http://127.0.0.1:9002",
|
||||
"redis": {
|
||||
"host": "127.0.0.1",
|
||||
|
@ -1,13 +1,13 @@
|
||||
const config = require('../../config');
|
||||
|
||||
const { level } = config.log;
|
||||
const {level} = config.log;
|
||||
const trace = level.toLowerCase() === 'trace';
|
||||
const debug = trace || level.toLowerCase() === 'debug';
|
||||
|
||||
const date = () => new Date().toISOString();
|
||||
|
||||
const parse = (messages) => {
|
||||
return messages.map(message => {
|
||||
return messages.map((message) => {
|
||||
if (typeof message === 'object') return JSON.stringify(message);
|
||||
|
||||
return message;
|
||||
|
88
bbb-export-annotations/lib/utils/message-builder.js
Normal file
88
bbb-export-annotations/lib/utils/message-builder.js
Normal 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,
|
||||
};
|
15
bbb-export-annotations/package-lock.json
generated
15
bbb-export-annotations/package-lock.json
generated
@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"axios": "^0.26.0",
|
||||
"form-data": "^4.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"perfect-freehand": "^1.0.16",
|
||||
"probe-image-size": "^7.2.3",
|
||||
"redis": "^4.0.3",
|
||||
@ -19,6 +20,10 @@
|
||||
"devDependencies": {
|
||||
"eslint": "^8.20.0",
|
||||
"eslint-config-google": "^0.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.16.0",
|
||||
"npm": "^8.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc": {
|
||||
@ -920,6 +925,11 @@
|
||||
"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": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
@ -2044,6 +2054,11 @@
|
||||
"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": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
|
@ -9,6 +9,7 @@
|
||||
"dependencies": {
|
||||
"axios": "^0.26.0",
|
||||
"form-data": "^4.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"perfect-freehand": "^1.0.16",
|
||||
"probe-image-size": "^7.2.3",
|
||||
"redis": "^4.0.3",
|
||||
@ -18,5 +19,9 @@
|
||||
"devDependencies": {
|
||||
"eslint": "^8.20.0",
|
||||
"eslint-config-google": "^0.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.16.0",
|
||||
"npm": "^8.5.0"
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ const redis = require('redis');
|
||||
const sanitize = require('sanitize-filename');
|
||||
const stream = require('stream');
|
||||
const WorkerStarter = require('../lib/utils/worker-starter');
|
||||
const {PresAnnStatusMsg} = require('../lib/utils/message-builder');
|
||||
const {workerData} = require('worker_threads');
|
||||
const {promisify} = require('util');
|
||||
|
||||
@ -55,29 +56,7 @@ async function collectAnnotationsFromRedis() {
|
||||
const pdfFile = `${presFile}.pdf`;
|
||||
|
||||
// Message to display conversion progress toast
|
||||
const statusUpdate = {
|
||||
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,
|
||||
},
|
||||
},
|
||||
};
|
||||
const statusUpdate = new PresAnnStatusMsg(exportJob);
|
||||
|
||||
if (fs.existsSync(pdfFile)) {
|
||||
for (const p of pages) {
|
||||
@ -103,35 +82,32 @@ async function collectAnnotationsFromRedis() {
|
||||
try {
|
||||
cp.spawnSync(config.shared.pdftocairo, extract_png_from_pdf, {shell: false});
|
||||
} catch (error) {
|
||||
const error_reason = `PDFtoCairo failed extracting slide ${pageNumber}`;
|
||||
logger.error(`${error_reason} in job ${jobId}: ${error.message}`);
|
||||
statusUpdate.core.body.status = error_reason;
|
||||
statusUpdate.core.body.error = true;
|
||||
logger.error(`PDFtoCairo failed extracting slide ${pageNumber} in job ${jobId}: ${error.message}`);
|
||||
statusUpdate.setError();
|
||||
}
|
||||
|
||||
statusUpdate.core.body.pageNumber = pageNumber;
|
||||
statusUpdate.envelope.timestamp = (new Date()).getTime();
|
||||
await client.publish(config.redis.channels.publish, JSON.stringify(statusUpdate));
|
||||
statusUpdate.core.body.error = false;
|
||||
await client.publish(config.redis.channels.publish, statusUpdate.build(pageNumber));
|
||||
}
|
||||
// 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 {
|
||||
statusUpdate.core.body.error = true;
|
||||
await client.publish(config.redis.channels.publish, JSON.stringify(statusUpdate));
|
||||
if (fs.existsSync(`${presFile}.png`)) {
|
||||
// PNG file available
|
||||
fs.copyFileSync(`${presFile}.png`, path.join(dropbox, 'slide1.png'));
|
||||
} 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(`Presentation file missing for job ${exportJob.jobId}`);
|
||||
return logger.error(`No PDF, PNG or JPEG file available for job ${jobId}`);
|
||||
}
|
||||
|
||||
await client.publish(config.redis.channels.publish, statusUpdate.build());
|
||||
}
|
||||
|
||||
client.disconnect();
|
||||
|
||||
const process = new WorkerStarter({jobId, statusUpdate});
|
||||
const process = new WorkerStarter({jobId});
|
||||
process.process();
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ const FormData = require('form-data');
|
||||
const redis = require('redis');
|
||||
const axios = require('axios').default;
|
||||
const path = require('path');
|
||||
const {NewPresAnnFileAvailableMsg} = require('../lib/utils/message-builder');
|
||||
|
||||
const {workerData} = require('worker_threads');
|
||||
const [jobType, jobId, filename] = [workerData.jobType, workerData.jobId, workerData.filename];
|
||||
@ -27,34 +28,14 @@ async function notifyMeetingActor() {
|
||||
await client.connect();
|
||||
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.presId, 'pdf', jobId, filename);
|
||||
|
||||
const notification = {
|
||||
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,
|
||||
},
|
||||
},
|
||||
};
|
||||
const notification = new NewPresAnnFileAvailableMsg(exportJob, link);
|
||||
|
||||
logger.info(`Annotated PDF available at ${link}`);
|
||||
await client.publish(config.redis.channels.publish,
|
||||
JSON.stringify(notification));
|
||||
await client.publish(config.redis.channels.publish, notification.build());
|
||||
client.disconnect();
|
||||
}
|
||||
|
||||
|
@ -10,14 +10,16 @@ const sanitize = require('sanitize-filename');
|
||||
const {getStrokePoints, getStrokeOutlinePoints} = require('perfect-freehand');
|
||||
const probe = require('probe-image-size');
|
||||
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');
|
||||
logger.info('Processing PDF for job ' + jobId);
|
||||
statusUpdate.core.body.status = 'PROCESSING';
|
||||
|
||||
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
|
||||
function align_to_pango(alignment) {
|
||||
@ -157,10 +159,8 @@ function render_textbox(textColor, font, fontSize, textAlign, text, id, textBoxW
|
||||
try {
|
||||
cp.spawnSync(config.shared.imagemagick, commands, {shell: false});
|
||||
} catch (error) {
|
||||
const error_reason = 'ImageMagick failed to render textbox';
|
||||
logger.error(`${error_reason} in job ${jobId}: ${error.message}`);
|
||||
statusUpdate.core.body.status = error_reason;
|
||||
statusUpdate.core.body.error = true;
|
||||
logger.error(`ImageMagick failed to render textbox in job ${jobId}: ${error.message}`);
|
||||
statusUpdate.setError();
|
||||
}
|
||||
}
|
||||
|
||||
@ -596,7 +596,7 @@ function overlay_shape_label(svg, annotation) {
|
||||
const fontSize = text_size_to_px(annotation.style.size, annotation.style.scale);
|
||||
const textAlign = 'center';
|
||||
const text = annotation.label;
|
||||
const id = annotation.id;
|
||||
const id = sanitize(annotation.id);
|
||||
const rotation = rad_to_degree(annotation.rotation);
|
||||
|
||||
const [shape_width, shape_height] = annotation.size;
|
||||
@ -641,7 +641,7 @@ function overlay_sticky(svg, annotation) {
|
||||
|
||||
const textColor = '#0d0d0d'; // For sticky notes
|
||||
const text = annotation.text;
|
||||
const id = annotation.id;
|
||||
const id = sanitize(annotation.id);
|
||||
|
||||
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 textAlign = align_to_pango(annotation.style.textAlign);
|
||||
const text = annotation.text;
|
||||
const id = annotation.id;
|
||||
const id = sanitize(annotation.id);
|
||||
|
||||
const rotation = rad_to_degree(annotation.rotation);
|
||||
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));
|
||||
|
||||
// 1. Get the job
|
||||
const job = fs.readFileSync(path.join(dropbox, 'job'));
|
||||
const exportJob = JSON.parse(job);
|
||||
|
||||
// 2. Get the annotations
|
||||
// Get the annotations
|
||||
const annotations = fs.readFileSync(path.join(dropbox, 'whiteboard'));
|
||||
const whiteboard = JSON.parse(annotations);
|
||||
const pages = JSON.parse(whiteboard.pages);
|
||||
const ghostScriptInput = [];
|
||||
|
||||
// 3. Convert annotations to SVG
|
||||
// Convert annotations to SVG
|
||||
for (const currentSlide of pages) {
|
||||
const bgImagePath = path.join(dropbox, `slide${currentSlide.page}`);
|
||||
const svgBackgroundSlide = path.join(exportJob.presLocation,
|
||||
@ -854,14 +850,7 @@ async function process_presentation_annotations() {
|
||||
}
|
||||
});
|
||||
|
||||
// Dimensions converted to a pixel size which,
|
||||
// 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.
|
||||
|
||||
// Scale slide back to its original size
|
||||
const convertAnnotatedSlide = [
|
||||
SVGfile,
|
||||
'--output-width', to_px(slideWidth),
|
||||
@ -873,15 +862,11 @@ async function process_presentation_annotations() {
|
||||
cp.spawnSync(config.shared.cairosvg, convertAnnotatedSlide, {shell: false});
|
||||
} catch (error) {
|
||||
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;
|
||||
statusUpdate.envelope.timestamp = (new Date()).getTime();
|
||||
|
||||
await client.publish(config.redis.channels.publish, JSON.stringify(statusUpdate));
|
||||
await client.publish(config.redis.channels.publish, statusUpdate.build(currentSlide.page));
|
||||
ghostScriptInput.push(PDFfile);
|
||||
statusUpdate.core.body.error = false;
|
||||
}
|
||||
|
||||
// Create PDF output directory if it doesn't exist
|
||||
@ -903,11 +888,7 @@ async function process_presentation_annotations() {
|
||||
try {
|
||||
cp.spawnSync(config.shared.ghostscript, mergePDFs, {shell: false});
|
||||
} catch (error) {
|
||||
const error_reason = 'GhostScript failed to merge PDFs';
|
||||
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));
|
||||
return logger.error(`GhostScript failed to merge PDFs in job ${jobId}: ${error.message}`);
|
||||
}
|
||||
|
||||
// Launch Notifier Worker depending on job type
|
||||
|
@ -2,7 +2,7 @@ import org.bigbluebutton.build._
|
||||
|
||||
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(
|
||||
organization := "org.bigbluebutton",
|
||||
@ -13,7 +13,7 @@ val compileSettings = Seq(
|
||||
"-Xlint",
|
||||
"-Ywarn-dead-code",
|
||||
"-language:_",
|
||||
"-target:jvm-1.11",
|
||||
"-target:11",
|
||||
"-encoding", "UTF-8"
|
||||
),
|
||||
javacOptions ++= List(
|
||||
@ -52,7 +52,7 @@ crossPaths := false
|
||||
// This forbids including Scala related libraries into the dependency
|
||||
autoScalaLibrary := false
|
||||
|
||||
scalaVersion := "2.13.4"
|
||||
scalaVersion := "2.13.9"
|
||||
|
||||
publishTo := Some(Resolver.file("file", new File(Path.userHome.absolutePath + "/.m2/repository")))
|
||||
|
||||
|
@ -7,7 +7,7 @@ object Dependencies {
|
||||
|
||||
object Versions {
|
||||
// Scala
|
||||
val scala = "2.13.4"
|
||||
val scala = "2.13.9"
|
||||
|
||||
// Libraries
|
||||
val netty = "3.2.10.Final"
|
||||
|
11768
bbb-learning-dashboard/package-lock.json
generated
11768
bbb-learning-dashboard/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,19 +9,14 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.15.0",
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@mui/material": "^5.10.13",
|
||||
"@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-dom": "^17.0.2",
|
||||
"react-intl": "^5.20.6",
|
||||
"typescript": "^4.3.5",
|
||||
"web-vitals": "^1.1.2"
|
||||
"react-scripts": "^5.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
@ -48,6 +43,7 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.15.0",
|
||||
"autoprefixer": "^10.4.1",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
@ -57,7 +53,6 @@
|
||||
"eslint-plugin-react": "^7.24.0",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"postcss": "^8.4.5",
|
||||
"react-scripts": "^5.0.0",
|
||||
"tailwindcss": "^3.0.11"
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,17 @@
|
||||
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 './bbb-icons.css';
|
||||
import {
|
||||
FormattedMessage, FormattedDate, injectIntl, FormattedTime,
|
||||
} from 'react-intl';
|
||||
import { emojiConfigs } from './services/EmojiService';
|
||||
import Card from './components/Card';
|
||||
import CardBody from './components/Card';
|
||||
import UsersTable from './components/UsersTable';
|
||||
import UserDetails from './components/UserDetails/component';
|
||||
import { UserDetailsContext } from './components/UserDetails/context';
|
||||
@ -14,6 +20,13 @@ import PollsTable from './components/PollsTable';
|
||||
import ErrorMessage from './components/ErrorMessage';
|
||||
import { makeUserCSVData, tsToHHmmss } from './services/UserService';
|
||||
|
||||
const TABS = {
|
||||
OVERVIEW: 0,
|
||||
OVERVIEW_ACTIVITY_SCORE: 1,
|
||||
TIMELINE: 2,
|
||||
POLLING: 3,
|
||||
};
|
||||
|
||||
class App extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -21,7 +34,7 @@ class App extends React.Component {
|
||||
loading: true,
|
||||
invalidSessionCount: 0,
|
||||
activitiesJson: {},
|
||||
tab: 'overview',
|
||||
tab: 0,
|
||||
meetingId: '',
|
||||
learningDashboardAccessToken: '',
|
||||
ldAccessTokenCopied: false,
|
||||
@ -170,14 +183,13 @@ class App extends React.Component {
|
||||
invalidSessionCount: 0,
|
||||
lastUpdated: Date.now(),
|
||||
});
|
||||
document.title = `Learning Dashboard - ${json.name}`;
|
||||
this.updateModalUser();
|
||||
}).catch(() => {
|
||||
this.setState({ loading: false, invalidSessionCount: invalidSessionCount + 1 });
|
||||
});
|
||||
} else if (sessionToken !== '') {
|
||||
const url = new URL('/bigbluebutton/api/learningDashboard', window.location);
|
||||
fetch(`${url}?sessionToken=${sessionToken}`)
|
||||
fetch(`${url}?sessionToken=${sessionToken}`, { credentials: 'include' })
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
if (json.response.returncode === 'SUCCESS') {
|
||||
@ -188,7 +200,6 @@ class App extends React.Component {
|
||||
invalidSessionCount: 0,
|
||||
lastUpdated: Date.now(),
|
||||
});
|
||||
document.title = `Learning Dashboard - ${jsonData.name}`;
|
||||
this.updateModalUser();
|
||||
} else {
|
||||
// When meeting is ended the sessionToken stop working, check for new cookies
|
||||
@ -215,7 +226,7 @@ class App extends React.Component {
|
||||
} = this.state;
|
||||
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() {
|
||||
if (activitiesJson && activitiesJson.users) {
|
||||
@ -311,6 +322,11 @@ class App extends React.Component {
|
||||
|
||||
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 (
|
||||
<div className="mx-10">
|
||||
<div className="flex flex-col sm:flex-row items-start justify-between pb-3">
|
||||
@ -366,24 +382,25 @@ class App extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div aria-hidden="true" className="cursor-pointer" onClick={() => { this.setState({ tab: 'overview' }); }}>
|
||||
<Card
|
||||
<TabsUnstyled
|
||||
defaultValue={0}
|
||||
onChange={(e, v) => {
|
||||
this.setState({ tab: v });
|
||||
}}
|
||||
>
|
||||
<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">
|
||||
<Card>
|
||||
<CardContent classes={{ root: '!p-0' }}>
|
||||
<CardBody
|
||||
name={
|
||||
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' })
|
||||
}
|
||||
number={Object
|
||||
.values(activitiesJson.users || {})
|
||||
.filter((u) => activitiesJson.endedOn > 0
|
||||
|| Object.values(u.intIds)[Object.values(u.intIds).length - 1].leftOn === 0)
|
||||
.length}
|
||||
cardClass={tab === 'overview' ? 'border-pink-500' : 'hover:border-pink-500 border-white'}
|
||||
number={usersCount}
|
||||
cardClass={tab === TABS.OVERVIEW ? 'border-pink-500' : 'hover:border-pink-500 border-white'}
|
||||
iconClass="bg-pink-50 text-pink-500"
|
||||
onClick={() => {
|
||||
this.setState({ tab: 'overview' });
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -399,16 +416,20 @@ class App extends React.Component {
|
||||
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>
|
||||
</CardBody>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div aria-hidden="true" className="cursor-pointer" onClick={() => { this.setState({ tab: 'overview_activityscore' }); }}>
|
||||
<Card
|
||||
</TabUnstyled>
|
||||
<TabUnstyled className="rounded focus:outline-none focus:ring focus:ring-green-500 ring-offset-2">
|
||||
<Card>
|
||||
<CardContent classes={{ root: '!p-0' }}>
|
||||
<CardBody
|
||||
name={intl.formatMessage({ id: 'app.learningDashboard.indicators.activityScore', defaultMessage: 'Activity Score' })}
|
||||
number={intl.formatNumber((getAverageActivityScore() || 0), {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 1,
|
||||
})}
|
||||
cardClass={tab === 'overview_activityscore' ? 'border-green-500' : 'hover:border-green-500 border-white'}
|
||||
cardClass={tab === TABS.OVERVIEW_ACTIVITY_SCORE ? 'border-green-500' : 'hover:border-green-500 border-white'}
|
||||
iconClass="bg-green-200 text-green-500"
|
||||
>
|
||||
<svg
|
||||
@ -431,23 +452,31 @@ class App extends React.Component {
|
||||
d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z"
|
||||
/>
|
||||
</svg>
|
||||
</CardBody>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div aria-hidden="true" className="cursor-pointer" onClick={() => { this.setState({ tab: 'status_timeline' }); }}>
|
||||
<Card
|
||||
</TabUnstyled>
|
||||
<TabUnstyled className="rounded focus:outline-none focus:ring focus:ring-purple-500 ring-offset-2">
|
||||
<Card>
|
||||
<CardContent classes={{ root: '!p-0' }}>
|
||||
<CardBody
|
||||
name={intl.formatMessage({ id: 'app.learningDashboard.indicators.timeline', defaultMessage: 'Timeline' })}
|
||||
number={totalOfEmojis()}
|
||||
cardClass={tab === 'status_timeline' ? 'border-purple-500' : 'hover:border-purple-500 border-white'}
|
||||
cardClass={tab === TABS.TIMELINE ? 'border-purple-500' : 'hover:border-purple-500 border-white'}
|
||||
iconClass="bg-purple-200 text-purple-500"
|
||||
>
|
||||
{this.fetchMostUsedEmojis()}
|
||||
</CardBody>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div aria-hidden="true" className="cursor-pointer" onClick={() => { this.setState({ tab: 'polling' }); }}>
|
||||
<Card
|
||||
</TabUnstyled>
|
||||
<TabUnstyled className="rounded focus:outline-none focus:ring focus:ring-blue-500 ring-offset-2">
|
||||
<Card>
|
||||
<CardContent classes={{ root: '!p-0' }}>
|
||||
<CardBody
|
||||
name={intl.formatMessage({ id: 'app.learningDashboard.indicators.polls', defaultMessage: 'Polls' })}
|
||||
number={Object.values(activitiesJson.polls || {}).length}
|
||||
cardClass={tab === 'polling' ? 'border-blue-500' : 'hover:border-blue-500 border-white'}
|
||||
cardClass={tab === TABS.POLLING ? 'border-blue-500' : 'hover:border-blue-500 border-white'}
|
||||
iconClass="bg-blue-100 text-blue-500"
|
||||
>
|
||||
<svg
|
||||
@ -464,49 +493,69 @@ class App extends React.Component {
|
||||
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>
|
||||
</CardBody>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="block my-2 pr-2 text-xl font-semibold">
|
||||
{ tab === 'overview' || tab === 'overview_activityscore'
|
||||
? <FormattedMessage id="app.learningDashboard.usersTable.title" defaultMessage="Overview" />
|
||||
: null }
|
||||
{ tab === 'status_timeline'
|
||||
? <FormattedMessage id="app.learningDashboard.statusTimelineTable.title" defaultMessage="Timeline" />
|
||||
: null }
|
||||
{ tab === 'polling'
|
||||
? <FormattedMessage id="app.learningDashboard.pollsTable.title" defaultMessage="Polls" />
|
||||
: null }
|
||||
</h1>
|
||||
</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">
|
||||
{ (tab === 'overview' || tab === 'overview_activityscore')
|
||||
? (
|
||||
<UsersTable
|
||||
allUsers={activitiesJson.users}
|
||||
totalOfActivityTime={totalOfActivity()}
|
||||
totalOfPolls={Object.values(activitiesJson.polls || {}).length}
|
||||
tab={tab}
|
||||
tab="overview"
|
||||
/>
|
||||
)
|
||||
: null }
|
||||
{ (tab === 'status_timeline')
|
||||
? (
|
||||
</div>
|
||||
</div>
|
||||
</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
|
||||
allUsers={activitiesJson.users}
|
||||
slides={activitiesJson.presentationSlides}
|
||||
meetingId={activitiesJson.intId}
|
||||
/>
|
||||
)
|
||||
: null }
|
||||
{ tab === 'polling'
|
||||
? <PollsTable polls={activitiesJson.polls} allUsers={activitiesJson.users} />
|
||||
: null }
|
||||
</div>
|
||||
</div>
|
||||
</TabPanelUnstyled>
|
||||
<TabPanelUnstyled value={3}>
|
||||
<h2 className="block my-2 pr-2 text-xl font-semibold">
|
||||
<FormattedMessage id="app.learningDashboard.pollsTable.title" defaultMessage="Polls" />
|
||||
</h2>
|
||||
<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} />
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<p>
|
||||
{
|
||||
|
@ -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();
|
||||
});
|
@ -40,7 +40,7 @@ function Card(props) {
|
||||
+ ` ${cardClass}`
|
||||
}
|
||||
>
|
||||
<div className="w-70">
|
||||
<div className="w-70 text-left rtl:text-right">
|
||||
<p className="text-lg font-semibold text-gray-700">
|
||||
{ number }
|
||||
</p>
|
||||
|
@ -12,9 +12,9 @@ function ErrorMessage(props) {
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</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}
|
||||
</h1>
|
||||
</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -375,7 +375,7 @@ const PollsTable = (props) => {
|
||||
sx={{
|
||||
'& .MuiDataGrid-columnHeaders': {
|
||||
backgroundColor: 'rgb(243 244 246/var(--tw-bg-opacity))',
|
||||
color: 'rgb(107 114 128/1)',
|
||||
color: 'rgb(55 65 81/1)',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '.025em',
|
||||
minHeight: '40.5px !important',
|
||||
|
@ -1,8 +1,27 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
||||
import { emojiConfigs, filterUserEmojis } from '../services/EmojiService';
|
||||
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 {
|
||||
componentDidMount() {
|
||||
// This code is needed to prevent emojis from overflowing.
|
||||
@ -162,7 +181,7 @@ class StatusTable extends React.Component {
|
||||
return (
|
||||
<table className="w-full">
|
||||
<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'}`}>
|
||||
<FormattedMessage id="app.learningDashboard.user" defaultMessage="User" />
|
||||
</th>
|
||||
@ -190,7 +209,7 @@ class StatusTable extends React.Component {
|
||||
const { slide, start, end } = period;
|
||||
const padding = isRTL ? 'paddingLeft' : 'paddingRight';
|
||||
const URLPrefix = `/bigbluebutton/presentation/${meetingId}/${meetingId}`;
|
||||
const { presentationId, pageNum } = slide || {};
|
||||
const { presentationId, pageNum, presentationName } = slide || {};
|
||||
return (
|
||||
<td
|
||||
style={{
|
||||
@ -199,29 +218,28 @@ class StatusTable extends React.Component {
|
||||
>
|
||||
{ slide && (
|
||||
<div className="flex">
|
||||
<div
|
||||
className="my-4"
|
||||
aria-label={tsToHHmmss(start - periods[0].start)}
|
||||
>
|
||||
<div className="my-4">
|
||||
<a
|
||||
href={`${URLPrefix}/${presentationId}/svg/${pageNum}`}
|
||||
className="block border-2 border-gray-300"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-describedby={`thumb-desc-${presentationId}`}
|
||||
>
|
||||
<img
|
||||
src={`${URLPrefix}/${presentationId}/thumbnail/${pageNum}`}
|
||||
alt={intl.formatMessage({
|
||||
id: 'app.learningDashboard.statusTimelineTable.thumbnail',
|
||||
defaultMessage: 'Presentation thumbnail',
|
||||
})}
|
||||
alt={`${intl.formatMessage(intlMessages.thumbnail)} - ${intl.formatMessage(intlMessages.presentation)} ${presentationName} - ${intl.formatMessage(intlMessages.pageNumber)} ${pageNum}`}
|
||||
style={{
|
||||
maxWidth: '150px',
|
||||
width: '150px',
|
||||
height: 'auto',
|
||||
whiteSpace: 'pre-line',
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
|
@ -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="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
|
||||
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"
|
||||
style={{
|
||||
right: `calc(${document.dir === 'ltr' ? userEndOffsetTime : userStartOffsetTime}% + 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)))
|
||||
.toISOString()
|
||||
.substring(11, 19) }
|
||||
</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 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="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">
|
||||
<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" />
|
||||
@ -465,7 +473,7 @@ const UserDatailsComponent = (props) => {
|
||||
</div>
|
||||
<div className="bg-white shadow rounded">
|
||||
<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">
|
||||
<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>
|
||||
|
@ -165,7 +165,7 @@ class UsersTable extends React.Component {
|
||||
return (
|
||||
<table className="w-full">
|
||||
<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
|
||||
className={`px-3.5 2xl:px-4 py-3 col-text-left ${tab === 'overview' ? 'cursor-pointer' : ''}`}
|
||||
onClick={() => { if (tab === 'overview') this.toggleOrder('userOrder'); }}
|
||||
|
@ -3,7 +3,6 @@ import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import { UserDetailsProvider } from './components/UserDetails/context';
|
||||
|
||||
const RTL_LANGUAGES = ['ar', 'dv', 'fa', 'he'];
|
||||
@ -83,5 +82,3 @@ class Dashboard extends React.Component {
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
ReactDOM.render(<Dashboard />, rootElement);
|
||||
|
||||
reportWebVitals();
|
||||
|
@ -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;
|
@ -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';
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -1 +1 @@
|
||||
BIGBLUEBUTTON_RELEASE=2.6.0-beta.1
|
||||
BIGBLUEBUTTON_RELEASE=2.6.0-beta.4
|
||||
|
@ -832,21 +832,6 @@ check_configuration() {
|
||||
echo "# is not owned by $BBB_USER"
|
||||
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')
|
||||
if [ "$CHECK_STUN" == "stun.freeswitch.org" ]; then
|
||||
echo
|
||||
@ -1370,7 +1355,6 @@ if [ $CHECK ]; then
|
||||
echo "$BBB_WEB_CONFIG (bbb-web)"
|
||||
echo " bigbluebutton.web.serverURL: $(get_bbb_web_config_value bigbluebutton.web.serverURL)"
|
||||
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
|
||||
|
@ -5,20 +5,20 @@
|
||||
|
||||
meteor-base@1.5.1
|
||||
mobile-experience@1.1.0
|
||||
mongo@1.15.0
|
||||
reactive-var@1.0.11
|
||||
mongo@1.16.3
|
||||
reactive-var@1.0.12
|
||||
|
||||
standard-minifier-css@1.8.1
|
||||
standard-minifier-js@2.8.0
|
||||
standard-minifier-css@1.8.3
|
||||
standard-minifier-js@2.8.1
|
||||
es5-shim@4.8.0
|
||||
ecmascript@0.16.2
|
||||
ecmascript@0.16.4
|
||||
shell-server@0.5.0
|
||||
|
||||
static-html@1.3.2
|
||||
react-meteor-data
|
||||
session@1.2.0
|
||||
tracker@1.2.0
|
||||
check@1.3.1
|
||||
session@1.2.1
|
||||
tracker@1.2.1
|
||||
check@1.3.2
|
||||
|
||||
rocketchat:streamer
|
||||
meteortesting:mocha
|
||||
|
@ -1 +1 @@
|
||||
METEOR@2.7.3
|
||||
METEOR@2.9.0
|
||||
|
@ -1,6 +1,6 @@
|
||||
allow-deny@1.1.1
|
||||
autoupdate@1.8.0
|
||||
babel-compiler@7.9.0
|
||||
babel-compiler@7.10.1
|
||||
babel-runtime@1.5.1
|
||||
base64@1.0.12
|
||||
binary-heap@1.0.11
|
||||
@ -9,21 +9,21 @@ boilerplate-generator@1.7.1
|
||||
caching-compiler@1.2.2
|
||||
caching-html-compiler@1.2.1
|
||||
callback-hook@1.4.0
|
||||
check@1.3.1
|
||||
ddp@1.4.0
|
||||
ddp-client@2.5.0
|
||||
check@1.3.2
|
||||
ddp@1.4.1
|
||||
ddp-client@2.6.1
|
||||
ddp-common@1.4.0
|
||||
ddp-server@2.5.0
|
||||
diff-sequence@1.1.1
|
||||
ddp-server@2.6.0
|
||||
diff-sequence@1.1.2
|
||||
dynamic-import@0.7.2
|
||||
ecmascript@0.16.2
|
||||
ecmascript@0.16.4
|
||||
ecmascript-runtime@0.8.0
|
||||
ecmascript-runtime-client@0.12.1
|
||||
ecmascript-runtime-server@0.11.0
|
||||
ejson@1.1.2
|
||||
ejson@1.1.3
|
||||
es5-shim@4.8.0
|
||||
fetch@0.1.1
|
||||
geojson-utils@1.0.10
|
||||
fetch@0.1.2
|
||||
geojson-utils@1.0.11
|
||||
hot-code-push@1.0.4
|
||||
html-tools@1.1.3
|
||||
htmljs@1.1.1
|
||||
@ -33,46 +33,46 @@ inter-process-messaging@0.1.1
|
||||
launch-screen@1.3.0
|
||||
lmieulet:meteor-coverage@4.1.0
|
||||
logging@1.3.1
|
||||
meteor@1.10.0
|
||||
meteor@1.10.3
|
||||
meteor-base@1.5.1
|
||||
meteortesting:browser-tests@1.3.5
|
||||
meteortesting:mocha@2.0.3
|
||||
meteortesting:mocha-core@8.1.2
|
||||
minifier-css@1.6.0
|
||||
minifier-js@2.7.4
|
||||
minimongo@1.8.0
|
||||
minifier-css@1.6.2
|
||||
minifier-js@2.7.5
|
||||
minimongo@1.9.1
|
||||
mobile-experience@1.1.0
|
||||
mobile-status-bar@1.1.0
|
||||
modern-browsers@0.1.8
|
||||
modules@0.18.0
|
||||
modules-runtime@0.13.0
|
||||
mongo@1.15.0
|
||||
modern-browsers@0.1.9
|
||||
modules@0.19.0
|
||||
modules-runtime@0.13.1
|
||||
mongo@1.16.3
|
||||
mongo-decimal@0.1.3
|
||||
mongo-dev-server@1.1.0
|
||||
mongo-id@1.0.8
|
||||
npm-mongo@4.3.1
|
||||
npm-mongo@4.12.1
|
||||
ordered-dict@1.1.0
|
||||
promise@0.12.0
|
||||
random@1.2.0
|
||||
promise@0.12.2
|
||||
random@1.2.1
|
||||
react-fast-refresh@0.2.3
|
||||
react-meteor-data@2.5.1
|
||||
reactive-dict@1.3.0
|
||||
reactive-var@1.0.11
|
||||
reactive-dict@1.3.1
|
||||
reactive-var@1.0.12
|
||||
reload@1.3.1
|
||||
retry@1.1.0
|
||||
rocketchat:streamer@1.1.0
|
||||
routepolicy@1.1.1
|
||||
session@1.2.0
|
||||
session@1.2.1
|
||||
shell-server@0.5.0
|
||||
socket-stream-client@0.5.0
|
||||
spacebars-compiler@1.3.1
|
||||
standard-minifier-css@1.8.1
|
||||
standard-minifier-js@2.8.0
|
||||
standard-minifier-css@1.8.3
|
||||
standard-minifier-js@2.8.1
|
||||
static-html@1.3.2
|
||||
templating-tools@1.2.2
|
||||
tracker@1.2.0
|
||||
typescript@4.5.4
|
||||
underscore@1.0.10
|
||||
tracker@1.2.1
|
||||
typescript@4.6.4
|
||||
underscore@1.0.11
|
||||
url@1.3.2
|
||||
webapp@1.13.1
|
||||
webapp-hashing@1.1.0
|
||||
webapp@1.13.2
|
||||
webapp-hashing@1.1.1
|
||||
|
@ -27,74 +27,52 @@ import Users, { CurrentUser } from '/imports/api/users';
|
||||
import { Slides, SlidePositions } from '/imports/api/slides';
|
||||
|
||||
// Custom Publishers
|
||||
export const localCurrentPollSync = new AbstractCollection(CurrentPoll, CurrentPoll);
|
||||
export const localCurrentUserSync = new AbstractCollection(CurrentUser, CurrentUser);
|
||||
export const localSlidesSync = new AbstractCollection(Slides, Slides);
|
||||
export const localSlidePositionsSync = new AbstractCollection(SlidePositions, SlidePositions);
|
||||
export const localPollsSync = new AbstractCollection(Polls, Polls);
|
||||
export const localPresentationsSync = new AbstractCollection(Presentations, Presentations);
|
||||
export const localPresentationPodsSync = new AbstractCollection(PresentationPods, PresentationPods);
|
||||
export const localPresentationUploadTokenSync = new AbstractCollection(PresentationUploadToken, PresentationUploadToken);
|
||||
export const localScreenshareSync = new AbstractCollection(Screenshare, Screenshare);
|
||||
export const localUserInfosSync = new AbstractCollection(UserInfos, UserInfos);
|
||||
export const localUsersPersistentDataSync = new AbstractCollection(UsersPersistentData, UsersPersistentData);
|
||||
export const localUserSettingsSync = new AbstractCollection(UserSettings, UserSettings);
|
||||
export const localVideoStreamsSync = new AbstractCollection(VideoStreams, VideoStreams);
|
||||
export const localVoiceUsersSync = new AbstractCollection(VoiceUsers, VoiceUsers);
|
||||
export const localWhiteboardMultiUserSync = new AbstractCollection(WhiteboardMultiUser, WhiteboardMultiUser);
|
||||
export const localGroupChatSync = new AbstractCollection(GroupChat, GroupChat);
|
||||
export const localConnectionStatusSync = new AbstractCollection(ConnectionStatus, ConnectionStatus);
|
||||
export const localCaptionsSync = new AbstractCollection(Captions, Captions);
|
||||
export const localPadsSync = new AbstractCollection(Pads, Pads);
|
||||
export const localPadsSessionsSync = new AbstractCollection(PadsSessions, PadsSessions);
|
||||
export const localPadsUpdatesSync = new AbstractCollection(PadsUpdates, PadsUpdates);
|
||||
export const localAuthTokenValidationSync = new AbstractCollection(AuthTokenValidation, AuthTokenValidation);
|
||||
export const localAnnotationsSync = new AbstractCollection(Annotations, Annotations);
|
||||
export const localRecordMeetingsSync = new AbstractCollection(RecordMeetings, RecordMeetings);
|
||||
export const localExternalVideoMeetingsSync = new AbstractCollection(ExternalVideoMeetings, ExternalVideoMeetings);
|
||||
export const localMeetingTimeRemainingSync = new AbstractCollection(MeetingTimeRemaining, MeetingTimeRemaining);
|
||||
export const localUsersTypingSync = new AbstractCollection(UsersTyping, UsersTyping);
|
||||
export const localBreakoutsSync = new AbstractCollection(Breakouts, Breakouts);
|
||||
export const localBreakoutsHistorySync = new AbstractCollection(BreakoutsHistory, BreakoutsHistory);
|
||||
export const localGuestUsersSync = new AbstractCollection(guestUsers, guestUsers);
|
||||
export const localMeetingsSync = new AbstractCollection(Meetings, Meetings);
|
||||
export const localUsersSync = new AbstractCollection(Users, Users);
|
||||
export const localNotificationsSync = new AbstractCollection(Notifications, Notifications);
|
||||
export const localCollectionRegistry = {
|
||||
localCurrentPollSync: new AbstractCollection(CurrentPoll, CurrentPoll),
|
||||
localCurrentUserSync: new AbstractCollection(CurrentUser, CurrentUser),
|
||||
localSlidesSync: new AbstractCollection(Slides, Slides),
|
||||
localSlidePositionsSync: new AbstractCollection(SlidePositions, SlidePositions),
|
||||
localPollsSync: new AbstractCollection(Polls, Polls),
|
||||
localPresentationsSync: new AbstractCollection(Presentations, Presentations),
|
||||
localPresentationPodsSync: new AbstractCollection(PresentationPods, PresentationPods),
|
||||
localPresentationUploadTokenSync: new AbstractCollection(
|
||||
PresentationUploadToken,
|
||||
PresentationUploadToken,
|
||||
),
|
||||
localScreenshareSync: new AbstractCollection(Screenshare, Screenshare),
|
||||
localUserInfosSync: new AbstractCollection(UserInfos, UserInfos),
|
||||
localUsersPersistentDataSync: new AbstractCollection(UsersPersistentData, UsersPersistentData),
|
||||
localUserSettingsSync: new AbstractCollection(UserSettings, UserSettings),
|
||||
localVideoStreamsSync: new AbstractCollection(VideoStreams, VideoStreams),
|
||||
localVoiceUsersSync: new AbstractCollection(VoiceUsers, VoiceUsers),
|
||||
localWhiteboardMultiUserSync: new AbstractCollection(WhiteboardMultiUser, WhiteboardMultiUser),
|
||||
localGroupChatSync: new AbstractCollection(GroupChat, GroupChat),
|
||||
localConnectionStatusSync: new AbstractCollection(ConnectionStatus, ConnectionStatus),
|
||||
localCaptionsSync: new AbstractCollection(Captions, Captions),
|
||||
localPadsSync: new AbstractCollection(Pads, Pads),
|
||||
localPadsSessionsSync: new AbstractCollection(PadsSessions, PadsSessions),
|
||||
localPadsUpdatesSync: new AbstractCollection(PadsUpdates, PadsUpdates),
|
||||
localAuthTokenValidationSync: new AbstractCollection(AuthTokenValidation, AuthTokenValidation),
|
||||
localAnnotationsSync: new AbstractCollection(Annotations, Annotations),
|
||||
localRecordMeetingsSync: new AbstractCollection(RecordMeetings, RecordMeetings),
|
||||
localExternalVideoMeetingsSync: new AbstractCollection(
|
||||
ExternalVideoMeetings,
|
||||
ExternalVideoMeetings,
|
||||
),
|
||||
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 = () => {
|
||||
localCurrentPollSync.setupListeners();
|
||||
localCurrentUserSync.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();
|
||||
Object.values(localCollectionRegistry).forEach((localCollection) => {
|
||||
localCollection.setupListeners();
|
||||
});
|
||||
};
|
||||
|
||||
export default collectionMirrorInitializer;
|
||||
|
@ -127,7 +127,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
</head>
|
||||
<body style="background-color: #06172A">
|
||||
<div id="aria-polite-alert" aria-live="polite" aria-atomic="false" class="sr-only"></div>
|
||||
<main>
|
||||
<div id="app" role="document">
|
||||
</main>
|
||||
</div>
|
||||
<span id="destination"></span>
|
||||
<audio id="remote-media" autoplay>
|
||||
|
@ -2,6 +2,7 @@ import Breakouts from '/imports/api/breakouts';
|
||||
import updateUserBreakoutRoom from '/imports/api/users-persistent-data/server/modifiers/updateUserBreakoutRoom';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import { check } from 'meteor/check';
|
||||
import { lowercaseTrim } from '/imports/utils/string-utils';
|
||||
|
||||
export default function joinedUsersChanged({ body }) {
|
||||
check(body, Object);
|
||||
@ -21,7 +22,7 @@ export default function joinedUsersChanged({ body }) {
|
||||
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 = {
|
||||
$set: {
|
||||
joinedUsers: usersMapped,
|
||||
|
@ -86,8 +86,8 @@ export default function addMeeting(meeting) {
|
||||
name: String,
|
||||
disabledFeatures: Array,
|
||||
notifyRecordingIsOn: Boolean,
|
||||
uploadExternalDescription: String,
|
||||
uploadExternalUrl: String,
|
||||
presentationUploadExternalDescription: String,
|
||||
presentationUploadExternalUrl: String,
|
||||
},
|
||||
usersProp: {
|
||||
maxUsers: Number,
|
||||
|
@ -11,6 +11,7 @@ RedisPubSub.on('PdfConversionInvalidErrorEvtMsg', handlePresentationConversionUp
|
||||
RedisPubSub.on('PresentationPageGeneratedEvtMsg', handlePresentationConversionUpdate);
|
||||
RedisPubSub.on('PresentationPageCountErrorEvtMsg', handlePresentationConversionUpdate);
|
||||
RedisPubSub.on('PresentationUploadedFileTimeoutErrorEvtMsg', handlePresentationConversionUpdate);
|
||||
RedisPubSub.on('PresentationHasInvalidMimeTypeErrorEvtMsg', handlePresentationConversionUpdate);
|
||||
RedisPubSub.on('PresentationConversionUpdateEvtMsg', handlePresentationConversionUpdate);
|
||||
RedisPubSub.on('PresentationUploadedFileTooLargeErrorEvtMsg', handlePresentationConversionUpdate);
|
||||
RedisPubSub.on('PresentationConversionCompletedEvtMsg', handlePresentationAdded);
|
||||
|
@ -13,6 +13,7 @@ const PDF_HAS_BIG_PAGE_KEY = 'PDF_HAS_BIG_PAGE';
|
||||
const GENERATED_SLIDE_KEY = 'GENERATED_SLIDE';
|
||||
const FILE_TOO_LARGE_KEY = 'FILE_TOO_LARGE';
|
||||
const CONVERSION_TIMEOUT_KEY = "CONVERSION_TIMEOUT";
|
||||
const IVALID_MIME_TYPE_KEY = "IVALID_MIME_TYPE";
|
||||
// const GENERATING_THUMBNAIL_KEY = 'GENERATING_THUMBNAIL';
|
||||
// const GENERATED_THUMBNAIL_KEY = 'GENERATED_THUMBNAIL';
|
||||
// const GENERATING_TEXTFILES_KEY = 'GENERATING_TEXTFILES';
|
||||
@ -50,6 +51,10 @@ export default function handlePresentationConversionUpdate({ body }, meetingId)
|
||||
statusModifier['conversion.maxFileSize'] = body.maxFileSize;
|
||||
case UNSUPPORTED_DOCUMENT_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 PAGE_COUNT_FAILED_KEY:
|
||||
case PAGE_COUNT_EXCEEDED_KEY:
|
||||
|
@ -6,6 +6,7 @@ import VoiceUsers from '/imports/api/voice-users/';
|
||||
import addUserPsersistentData from '/imports/api/users-persistent-data/server/modifiers/addUserPersistentData';
|
||||
import stringHash from 'string-hash';
|
||||
import flat from 'flat';
|
||||
import { lowercaseTrim } from '/imports/utils/string-utils';
|
||||
|
||||
import addVoiceUser from '/imports/api/voice-users/server/modifiers/addVoiceUser';
|
||||
|
||||
@ -51,7 +52,7 @@ export default function addUser(meetingId, userData) {
|
||||
|
||||
const userInfos = {
|
||||
meetingId,
|
||||
sortName: user.name.trim().toLowerCase(),
|
||||
sortName: lowercaseTrim(user.name),
|
||||
color,
|
||||
speechLocale: '',
|
||||
mobile: false,
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
} from '/imports/api/video-streams/server/helpers';
|
||||
import VoiceUsers from '/imports/api/voice-users/';
|
||||
import Users from '/imports/api/users/';
|
||||
import { lowercaseTrim } from '/imports/utils/string-utils';
|
||||
|
||||
const BASE_FLOOR_TIME = "0";
|
||||
|
||||
@ -40,6 +41,7 @@ export default function sharedWebcam(meetingId, userId, stream) {
|
||||
$set: {
|
||||
stream,
|
||||
name,
|
||||
sortName: lowercaseTrim(name),
|
||||
lastFloorTime,
|
||||
floor,
|
||||
pin,
|
||||
|
@ -171,7 +171,7 @@ class Base extends Component {
|
||||
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 (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)) {
|
||||
|
@ -723,8 +723,7 @@ class BreakoutRoom extends PureComponent {
|
||||
}
|
||||
|
||||
populateWithLastBreakouts(lastBreakouts) {
|
||||
const { getBreakoutUserWasIn, intl } = this.props;
|
||||
const { users } = this.state;
|
||||
const { getBreakoutUserWasIn, users, intl } = this.props;
|
||||
|
||||
const changedNames = [];
|
||||
lastBreakouts.forEach((breakout) => {
|
||||
|
@ -40,13 +40,6 @@ const Alert = styled.div`
|
||||
color: ${colorDanger};
|
||||
}
|
||||
`}
|
||||
|
||||
grid-row: span 3;
|
||||
|
||||
& > div {
|
||||
height: 25.2rem;
|
||||
max-height: 25.2rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const FreeJoinLabel = styled.label`
|
||||
@ -85,8 +78,7 @@ const BreakoutNameInput = styled.input`
|
||||
|
||||
const BreakoutBox = styled(ScrollboxVertical)`
|
||||
width: 100%;
|
||||
min-height: 6rem;
|
||||
max-height: 8rem;
|
||||
height: 21rem;
|
||||
border: 1px solid ${colorGrayLightest};
|
||||
border-radius: ${borderRadius};
|
||||
padding: ${lgPaddingY} 0;
|
||||
|
46
bigbluebutton-html5/imports/ui/components/app/component.jsx
Executable file → Normal file
46
bigbluebutton-html5/imports/ui/components/app/component.jsx
Executable file → Normal file
@ -30,7 +30,7 @@ import PresentationAreaContainer from '../presentation/presentation-area/contain
|
||||
import ScreenshareContainer from '../screenshare/container';
|
||||
import ExternalVideoContainer from '../external-video-player/container';
|
||||
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 {
|
||||
isMobile, isTablet, isTabletPortrait, isTabletLandscape, isDesktop,
|
||||
} from '../layout/utils';
|
||||
@ -47,13 +47,16 @@ import Notifications from '../notifications/container';
|
||||
import GlobalStyles from '/imports/ui/stylesheets/styled-components/globalStyles';
|
||||
import ActionsBarContainer from '../actions-bar/container';
|
||||
import PushLayoutEngine from '../layout/push-layout/pushLayoutEngine';
|
||||
import AudioService from '/imports/ui/components/audio/service';
|
||||
import NotesContainer from '/imports/ui/components/notes/container';
|
||||
import DEFAULT_VALUES from '../layout/defaultValues';
|
||||
|
||||
const MOBILE_MEDIA = 'only screen and (max-width: 40em)';
|
||||
const APP_CONFIG = Meteor.settings.public.app;
|
||||
const DESKTOP_FONT_SIZE = APP_CONFIG.desktopFontSize;
|
||||
const MOBILE_FONT_SIZE = APP_CONFIG.mobileFontSize;
|
||||
const LAYOUT_CONFIG = Meteor.settings.public.layout;
|
||||
const CONFIRMATION_ON_LEAVE = Meteor.settings.public.app.askForConfirmationOnLeave;
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
userListLabel: {
|
||||
@ -195,6 +198,16 @@ class App extends Component {
|
||||
window.ondragover = (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');
|
||||
|
||||
ConnectionStatusService.startRoundTripTime();
|
||||
@ -210,6 +223,12 @@ class App extends Component {
|
||||
mountModal,
|
||||
deviceType,
|
||||
mountRandomUserModal,
|
||||
selectedLayout,
|
||||
sidebarContentIsOpen,
|
||||
layoutContextDispatch,
|
||||
numCameras,
|
||||
presentationIsOpen,
|
||||
ignorePollNotifications,
|
||||
} = this.props;
|
||||
|
||||
this.renderDarkMode();
|
||||
@ -243,10 +262,35 @@ class App extends Component {
|
||||
}
|
||||
|
||||
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() {
|
||||
window.removeEventListener('resize', this.handleWindowResize, false);
|
||||
window.onbeforeunload = null;
|
||||
ConnectionStatusService.stopRoundTripTime();
|
||||
}
|
||||
|
||||
|
@ -168,6 +168,7 @@ const AppContainer = (props) => {
|
||||
shouldShowPresentation,
|
||||
mountRandomUserModal,
|
||||
isPresenter,
|
||||
numCameras: cameraDockInput.numCameras,
|
||||
}}
|
||||
{...otherProps}
|
||||
/>
|
||||
@ -310,5 +311,6 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
|
||||
hidePresentation: getFromUserSettings('bbb_hide_presentation', LAYOUT_CONFIG.hidePresentation),
|
||||
hideActionsBar: getFromUserSettings('bbb_hide_actions_bar', false),
|
||||
isModalOpen: !!getModal(),
|
||||
ignorePollNotifications: Session.get('ignorePollNotifications'),
|
||||
};
|
||||
})(AppContainer)));
|
||||
|
@ -71,6 +71,21 @@ const init = (messages, intl) => {
|
||||
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 voiceUser = VoiceUsers.findOne({ intId: Auth.userID },
|
||||
{ fields: { joined: 1 } });
|
||||
@ -133,6 +148,7 @@ export default {
|
||||
updateAudioConstraints:
|
||||
(constraints) => AudioManager.updateAudioConstraints(constraints),
|
||||
recoverMicState,
|
||||
muteMicrophone: () => muteMicrophone(),
|
||||
isReconnecting: () => AudioManager.isReconnecting,
|
||||
setBreakoutAudioTransferStatus: (status) => AudioManager
|
||||
.setBreakoutAudioTransferStatus(status),
|
||||
|
@ -115,14 +115,15 @@ class MessageForm extends PureComponent {
|
||||
maxMessageLength,
|
||||
} = this.props;
|
||||
|
||||
const message = e.target.value;
|
||||
let message = e.target.value;
|
||||
let error = null;
|
||||
|
||||
if (message.length > maxMessageLength) {
|
||||
error = intl.formatMessage(
|
||||
messages.errorMaxMessageLength,
|
||||
{ 0: message.length - maxMessageLength },
|
||||
{ 0: maxMessageLength },
|
||||
);
|
||||
message = message.substring(0, maxMessageLength);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
|
@ -74,11 +74,7 @@ class LiveCaptions extends PureComponent {
|
||||
<div style={captionStyles}>
|
||||
{clear ? '' : data}
|
||||
</div>
|
||||
<div
|
||||
style={visuallyHidden}
|
||||
aria-atomic
|
||||
aria-live="polite"
|
||||
>
|
||||
<div style={visuallyHidden}>
|
||||
{clear ? '' : data}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -10,6 +10,7 @@ import { stripTags, unescapeHtml } from '/imports/utils/string-utils';
|
||||
import Service from '../service';
|
||||
import Styled from './styles';
|
||||
import { usePreviousValue } from '/imports/ui/components/utils/hooks';
|
||||
import { Session } from 'meteor/session';
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const PUBLIC_CHAT_CLEAR = CHAT_CONFIG.chat_clear;
|
||||
@ -193,9 +194,11 @@ const ChatAlert = (props) => {
|
||||
const mappedMessage = Service.mapGroupMessage(timeWindow);
|
||||
|
||||
let content = null;
|
||||
let isPollResult = false;
|
||||
if (mappedMessage) {
|
||||
if (mappedMessage.id.includes(POLL_RESULT_KEY)) {
|
||||
content = createPollMessage();
|
||||
isPollResult = true;
|
||||
} else {
|
||||
content = createMessage(mappedMessage.sender.name, mappedMessage.content.slice(-5));
|
||||
}
|
||||
@ -218,10 +221,22 @@ const ChatAlert = (props) => {
|
||||
: <span>{intl.formatMessage(intlMessages.appToastChatPrivate)}</span>
|
||||
}
|
||||
onOpen={
|
||||
() => setUnreadMessages(newUnreadMessages)
|
||||
() => {
|
||||
if (isPollResult) {
|
||||
Session.set('ignorePollNotifications', true);
|
||||
}
|
||||
|
||||
setUnreadMessages(newUnreadMessages);
|
||||
}
|
||||
}
|
||||
onClose={
|
||||
() => setUnreadMessages(newUnreadMessages)
|
||||
() => {
|
||||
if (isPollResult) {
|
||||
Session.set('ignorePollNotifications', false);
|
||||
}
|
||||
|
||||
setUnreadMessages(newUnreadMessages);
|
||||
}
|
||||
}
|
||||
alertDuration={timeWindow.durationDiff}
|
||||
layoutContextDispatch={layoutContextDispatch}
|
||||
|
@ -227,8 +227,9 @@ class MessageForm extends PureComponent {
|
||||
if (message.length > maxMessageLength) {
|
||||
error = intl.formatMessage(
|
||||
messages.errorMaxMessageLength,
|
||||
{ 0: message.length - maxMessageLength },
|
||||
{ 0: maxMessageLength },
|
||||
);
|
||||
message = message.substring(0, maxMessageLength);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
|
@ -46,7 +46,7 @@ class LocalesDropdown extends PureComponent {
|
||||
|
||||
render() {
|
||||
const {
|
||||
value, handleChange, elementId, selectMessage,
|
||||
value, handleChange, elementId, selectMessage, ariaLabel,
|
||||
} = this.props;
|
||||
const defaultLocale = value || DEFAULT_VALUE;
|
||||
|
||||
@ -57,6 +57,7 @@ class LocalesDropdown extends PureComponent {
|
||||
id={elementId}
|
||||
onChange={handleChange}
|
||||
value={defaultLocale}
|
||||
aria-label={ariaLabel||''}
|
||||
>
|
||||
<option disabled key={DEFAULT_KEY} value={DEFAULT_VALUE}>
|
||||
{selectMessage}
|
||||
|
@ -6,7 +6,7 @@ import Styled from './styles';
|
||||
|
||||
const messages = defineMessages({
|
||||
yesLabel: {
|
||||
id: 'app.endMeeting.yesLabel',
|
||||
id: 'app.confirmationModal.yesLabel',
|
||||
description: 'confirm button label',
|
||||
},
|
||||
noLabel: {
|
||||
@ -46,6 +46,7 @@ class ConfirmationModal extends Component {
|
||||
titleMessageExtra,
|
||||
checkboxMessageId,
|
||||
confirmButtonColor,
|
||||
confirmButtonLabel,
|
||||
confirmButtonDataTest,
|
||||
confirmParam,
|
||||
disableConfirmButton,
|
||||
@ -86,7 +87,7 @@ class ConfirmationModal extends Component {
|
||||
<Styled.Footer>
|
||||
<Styled.ConfirmationButton
|
||||
color={confirmButtonColor}
|
||||
label={intl.formatMessage(messages.yesLabel)}
|
||||
label={confirmButtonLabel ? confirmButtonLabel : intl.formatMessage(messages.yesLabel)}
|
||||
disabled={disableConfirmButton}
|
||||
data-test={confirmButtonDataTest}
|
||||
onClick={() => {
|
||||
|
@ -47,6 +47,7 @@ class ModalSimple extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
intl,
|
||||
title,
|
||||
hideBorder,
|
||||
@ -79,6 +80,7 @@ class ModalSimple extends Component {
|
||||
|
||||
return (
|
||||
<Styled.SimpleModal
|
||||
id="simpleModal"
|
||||
isOpen={modalisOpen}
|
||||
className={className}
|
||||
onRequestClose={handleRequestClose}
|
||||
|
@ -158,8 +158,7 @@ class ConnectionStatusComponent extends PureComponent {
|
||||
|
||||
this.help = Service.getHelp();
|
||||
this.state = {
|
||||
selectedTab: '1',
|
||||
dataPage: '1',
|
||||
selectedTab: 0,
|
||||
dataSaving: props.dataSaving,
|
||||
hasNetworkData: false,
|
||||
copyButtonText: intl.formatMessage(intlMessages.copy),
|
||||
@ -187,6 +186,7 @@ class ConnectionStatusComponent extends PureComponent {
|
||||
this.audioDownloadLabel = intl.formatMessage(intlMessages.audioDownloadRate);
|
||||
this.videoUploadLabel = intl.formatMessage(intlMessages.videoUploadRate);
|
||||
this.videoDownloadLabel = intl.formatMessage(intlMessages.videoDownloadRate);
|
||||
this.handleSelectTab = this.handleSelectTab.bind(this);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
@ -197,12 +197,24 @@ class ConnectionStatusComponent extends PureComponent {
|
||||
Meteor.clearInterval(this.rateInterval);
|
||||
}
|
||||
|
||||
handleSelectTab(tab) {
|
||||
this.setState({
|
||||
selectedTab: tab,
|
||||
});
|
||||
}
|
||||
|
||||
handleDataSavingChange(key) {
|
||||
const { dataSaving } = this.state;
|
||||
dataSaving[key] = !dataSaving[key];
|
||||
this.setState(dataSaving);
|
||||
}
|
||||
|
||||
setButtonMessage(msg) {
|
||||
this.setState({
|
||||
copyButtonText: msg,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start monitoring the network data.
|
||||
* @return {Promise} A Promise that resolves when process started.
|
||||
@ -262,6 +274,43 @@ class ConnectionStatusComponent extends PureComponent {
|
||||
}, 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() {
|
||||
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() {
|
||||
const {
|
||||
connectionStatus,
|
||||
@ -335,7 +338,7 @@ class ConnectionStatusComponent extends PureComponent {
|
||||
if (isConnectionStatusEmpty(connectionStatus)) return this.renderEmpty();
|
||||
|
||||
let connections = connectionStatus;
|
||||
if (selectedTab === '2') {
|
||||
if (selectedTab === 2) {
|
||||
connections = connections.filter(conn => conn.you);
|
||||
if (isConnectionStatusEmpty(connections)) return this.renderEmpty();
|
||||
}
|
||||
@ -345,7 +348,7 @@ class ConnectionStatusComponent extends PureComponent {
|
||||
|
||||
return (
|
||||
<Styled.Item
|
||||
key={index}
|
||||
key={`${conn?.name}-${dateTime}`}
|
||||
last={(index + 1) === connections.length}
|
||||
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 (
|
||||
<Styled.NetworkDataContainer data-test="networkDataContainer">
|
||||
<Styled.Prev>
|
||||
<Styled.ButtonLeft
|
||||
role="button"
|
||||
disabled={dataPage === '1'}
|
||||
aria-label={`${intl.formatMessage(intlMessages.prev)} ${intl.formatMessage(intlMessages.ariaTitle)}`}
|
||||
onClick={handlePaginationClick.bind(this, 'prev')}
|
||||
<Styled.NetworkDataContainer
|
||||
data-test="networkDataContainer"
|
||||
tabIndex={0}
|
||||
>
|
||||
<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="M15 19l-7-7 7-7"
|
||||
/>
|
||||
</Styled.Chevron>
|
||||
</Styled.ButtonLeft>
|
||||
</Styled.Prev>
|
||||
<Styled.Helper page={dataPage}>
|
||||
<Styled.HelperWrapper>
|
||||
<Styled.Helper>
|
||||
<ConnectionStatusHelper closeModal={() => closeModal(dataSaving, intl)} />
|
||||
</Styled.Helper>
|
||||
<Styled.NetworkDataContent page={dataPage}>
|
||||
</Styled.HelperWrapper>
|
||||
<Styled.NetworkDataContent>
|
||||
<Styled.DataColumn>
|
||||
<Styled.NetworkData>
|
||||
<div>{`${audioUploadLabel}`}</div>
|
||||
@ -582,28 +559,6 @@ class ConnectionStatusComponent extends PureComponent {
|
||||
</Styled.NetworkData>
|
||||
</Styled.DataColumn>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@ -619,9 +574,7 @@ class ConnectionStatusComponent extends PureComponent {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { intl } = this.props;
|
||||
|
||||
const { hasNetworkData } = this.state;
|
||||
const { hasNetworkData, copyButtonText } = this.state;
|
||||
return (
|
||||
<Styled.CopyContainer aria-live="polite">
|
||||
<Styled.Copy
|
||||
@ -632,68 +585,12 @@ class ConnectionStatusComponent extends PureComponent {
|
||||
onKeyPress={this.copyNetworkData.bind(this)}
|
||||
tabIndex={0}
|
||||
>
|
||||
{this.state.copyButtonText}
|
||||
{copyButtonText}
|
||||
</Styled.Copy>
|
||||
</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() {
|
||||
const {
|
||||
closeModal,
|
||||
@ -715,18 +612,43 @@ class ConnectionStatusComponent extends PureComponent {
|
||||
{intl.formatMessage(intlMessages.title)}
|
||||
</Styled.Title>
|
||||
</Styled.Header>
|
||||
{this.renderNavigation()}
|
||||
<Styled.Main>
|
||||
<Styled.Body>
|
||||
{selectedTab === '1'
|
||||
? this.renderNetworkData()
|
||||
: this.renderConnections()
|
||||
|
||||
<Styled.ConnectionTabs
|
||||
onSelect={this.handleSelectTab}
|
||||
selectedIndex={selectedTab}
|
||||
>
|
||||
<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>
|
||||
{selectedTab === '1' &&
|
||||
this.renderCopyDataButton()
|
||||
</Styled.ConnectionTabList>
|
||||
<Styled.ConnectionTabPanel selectedClassName="is-selected">
|
||||
<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.ConnectionStatusModal>
|
||||
);
|
||||
|
@ -7,12 +7,13 @@ import {
|
||||
colorGrayLabel,
|
||||
colorGrayLightest,
|
||||
colorPrimary,
|
||||
colorWhite,
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import {
|
||||
smPaddingX,
|
||||
smPaddingY,
|
||||
mdPaddingY,
|
||||
lgPaddingY,
|
||||
lgPaddingX,
|
||||
titlePositionLeft,
|
||||
mdPaddingX,
|
||||
borderSizeLarge,
|
||||
@ -26,7 +27,14 @@ import {
|
||||
hasPhoneDimentions,
|
||||
mediumDown,
|
||||
hasPhoneWidth,
|
||||
smallOnly,
|
||||
} 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`
|
||||
display: flex;
|
||||
@ -169,10 +177,22 @@ const Label = styled.span`
|
||||
margin-bottom: ${lgPaddingY};
|
||||
`;
|
||||
|
||||
const NetworkDataContainer = styled.div`
|
||||
const NetworkDataContainer = styled(ScrollboxVertical)`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
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} {
|
||||
justify-content: space-between;
|
||||
@ -202,6 +222,8 @@ const CopyContainer = styled.div`
|
||||
|
||||
const ConnectionStatusModal = styled(Modal)`
|
||||
padding: 1rem;
|
||||
height: 28rem;
|
||||
|
||||
`;
|
||||
|
||||
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`
|
||||
width: 12.5rem;
|
||||
height: 100%;
|
||||
@ -271,12 +306,7 @@ const Helper = styled.div`
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
@media ${mediumDown} {
|
||||
${({ page }) => page === '1'
|
||||
? 'display: flex;'
|
||||
: 'display: none;'}
|
||||
}
|
||||
padding: .5rem;
|
||||
`;
|
||||
|
||||
const NetworkDataContent = styled.div`
|
||||
@ -286,9 +316,9 @@ const NetworkDataContent = styled.div`
|
||||
flex-grow: 1;
|
||||
|
||||
@media ${mediumDown} {
|
||||
${({ page }) => page === '2'
|
||||
? 'display: flex;'
|
||||
: 'display: none;'}
|
||||
flex: none;
|
||||
width: 100%;
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
`;
|
||||
|
||||
@ -302,132 +332,103 @@ const DataColumn = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const Main = styled.div`
|
||||
height: 19.5rem;
|
||||
const ConnectionTabs = styled(Tabs)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-flow: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
@media ${smallOnly} {
|
||||
width: 100%;
|
||||
flex-flow: column;
|
||||
}
|
||||
`;
|
||||
|
||||
const Body = styled.div`
|
||||
padding: ${jumboPaddingY} 0;
|
||||
const ConnectionTabList = styled(TabList)`
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
margin: 0;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const Navigation = styled.div`
|
||||
display: flex;
|
||||
margin-bottom: .5rem;
|
||||
border: none;
|
||||
border-bottom: 1px solid ${colorOffWhite};
|
||||
user-select: none;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
padding: 0;
|
||||
width: calc(100% / 3);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& :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;
|
||||
@media ${smallOnly} {
|
||||
width: 100%;
|
||||
flex-flow: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media ${hasPhoneWidth} {
|
||||
margin: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const Next = styled(Prev)`
|
||||
margin: 0 .25rem 0 .5rem;
|
||||
|
||||
@media ${hasPhoneWidth} {
|
||||
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);
|
||||
const ConnectionTabPanel = styled(TabPanel)`
|
||||
display: none;
|
||||
margin: 0 0 0 1rem;
|
||||
height: 13rem;
|
||||
|
||||
[dir="rtl"] & {
|
||||
left: calc(50%);
|
||||
margin: 0 1rem 0 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const ButtonRight = styled(Button)`
|
||||
right: calc(50% - 2rem);
|
||||
|
||||
[dir="rtl"] & {
|
||||
right: calc(50%);
|
||||
}
|
||||
`;
|
||||
|
||||
const Chevron = styled.svg`
|
||||
&.is-selected {
|
||||
display: flex;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
flex-flow: column;
|
||||
}
|
||||
|
||||
@media ${smallOnly} {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const ConnectionTabSelector = styled(Tab)`
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
font-size: 0.9rem;
|
||||
flex: 0 0 auto;
|
||||
justify-content: flex-start;
|
||||
border: none !important;
|
||||
padding: ${mdPaddingY} ${mdPaddingX};
|
||||
|
||||
border-radius: .2rem;
|
||||
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"] & {
|
||||
transform: rotate(180deg);
|
||||
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,
|
||||
Helper,
|
||||
NetworkDataContent,
|
||||
Main,
|
||||
Body,
|
||||
Navigation,
|
||||
FullName,
|
||||
DataColumn,
|
||||
Prev,
|
||||
Next,
|
||||
ButtonLeft,
|
||||
ButtonRight,
|
||||
Chevron,
|
||||
HelperWrapper,
|
||||
ConnectionTabs,
|
||||
ConnectionTabList,
|
||||
ConnectionTabSelector,
|
||||
ConnectionTabPanel,
|
||||
};
|
||||
|
@ -20,6 +20,10 @@ const intlMessages = defineMessages({
|
||||
id: 'app.endMeeting.contentWarning',
|
||||
description: 'end meeting content warning',
|
||||
},
|
||||
confirmButtonLabel: {
|
||||
id: 'app.endMeeting.yesLabel',
|
||||
description: 'end meeting confirm button label',
|
||||
},
|
||||
});
|
||||
|
||||
const { warnAboutUnsavedContentOnMeetingEnd } = Meteor.settings.public.app;
|
||||
@ -58,6 +62,7 @@ class EndMeetingComponent extends PureComponent {
|
||||
description={description}
|
||||
confirmButtonColor="danger"
|
||||
confirmButtonDataTest="confirmEndMeeting"
|
||||
confirmButtonLabel={intl.formatMessage(intlMessages.confirmButtonLabel)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -202,6 +202,7 @@ const CustomLayout = (props) => {
|
||||
}, INITIAL_INPUT_STATE),
|
||||
});
|
||||
}
|
||||
Session.set('layoutReady', true);
|
||||
throttledCalculatesLayout();
|
||||
};
|
||||
|
||||
|
@ -142,6 +142,7 @@ const PresentationFocusLayout = (props) => {
|
||||
}, INITIAL_INPUT_STATE),
|
||||
});
|
||||
}
|
||||
Session.set('layoutReady', true);
|
||||
throttledCalculatesLayout();
|
||||
};
|
||||
|
||||
|
@ -138,6 +138,7 @@ const SmartLayout = (props) => {
|
||||
}, INITIAL_INPUT_STATE),
|
||||
});
|
||||
}
|
||||
Session.set('layoutReady', true);
|
||||
throttledCalculatesLayout();
|
||||
};
|
||||
|
||||
|
@ -149,6 +149,7 @@ const VideoFocusLayout = (props) => {
|
||||
),
|
||||
});
|
||||
}
|
||||
Session.set('layoutReady', true);
|
||||
throttledCalculatesLayout();
|
||||
};
|
||||
|
||||
|
32
bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx
Executable file → Normal file
32
bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx
Executable file → Normal file
@ -11,7 +11,7 @@ import BBBMenu from '/imports/ui/components/common/menu/component';
|
||||
import ShortcutHelpComponent from '/imports/ui/components/shortcut-help/component';
|
||||
import withShortcutHelper from '/imports/ui/components/shortcut-help/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 browserInfo from '/imports/utils/browserInfo';
|
||||
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) {
|
||||
const customStyles = { color: colorDanger };
|
||||
|
||||
this.menuItems.push(
|
||||
{
|
||||
key: 'list-item-logout',
|
||||
@ -315,12 +301,26 @@ class SettingsDropdown extends PureComponent {
|
||||
icon: 'logout',
|
||||
label: intl.formatMessage(intlMessages.leaveSessionLabel),
|
||||
description: intl.formatMessage(intlMessages.leaveSessionDesc),
|
||||
customStyles,
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -45,6 +45,7 @@ class TalkingIndicator extends PureComponent {
|
||||
talkers,
|
||||
amIModerator,
|
||||
moreThanMaxIndicators,
|
||||
users,
|
||||
} = this.props;
|
||||
if (!talkers) return null;
|
||||
|
||||
@ -58,9 +59,13 @@ class TalkingIndicator extends PureComponent {
|
||||
callerName,
|
||||
} = talkers[`${id}`];
|
||||
|
||||
const user = users[id];
|
||||
|
||||
const name = user?.name ?? callerName;
|
||||
|
||||
const ariaLabel = intl.formatMessage(talking
|
||||
? intlMessages.isTalking : intlMessages.wasTalking, {
|
||||
0: callerName,
|
||||
0: name,
|
||||
});
|
||||
|
||||
let icon = talking ? 'unmute' : 'blank';
|
||||
@ -68,7 +73,7 @@ class TalkingIndicator extends PureComponent {
|
||||
|
||||
return (
|
||||
<Styled.TalkingIndicatorWrapper
|
||||
key={_.uniqueId(`${callerName}-`)}
|
||||
key={_.uniqueId(`${name}-`)}
|
||||
muted={muted}
|
||||
talking={talking}
|
||||
floor={floor}
|
||||
@ -84,11 +89,11 @@ class TalkingIndicator extends PureComponent {
|
||||
$spoke={!talking || undefined}
|
||||
$muted={muted}
|
||||
$isViewer={!amIModerator || undefined}
|
||||
key={_.uniqueId(`${callerName}-`)}
|
||||
key={_.uniqueId(`${name}-`)}
|
||||
onClick={() => this.handleMuteUser(id)}
|
||||
label={callerName}
|
||||
label={name}
|
||||
tooltipLabel={!muted && amIModerator
|
||||
? `${intl.formatMessage(intlMessages.muteLabel)} ${callerName}`
|
||||
? `${intl.formatMessage(intlMessages.muteLabel)} ${name}`
|
||||
: null}
|
||||
data-test={talking ? 'isTalking' : 'wasTalking'}
|
||||
aria-label={ariaLabel}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user