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:
|
build-install-and-test:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- run: ./build/get_external_dependencies.sh
|
- run: ./build/get_external_dependencies.sh
|
||||||
- run: ./build/setup.sh bbb-apps-akka
|
- run: ./build/setup.sh bbb-apps-akka
|
||||||
- run: ./build/setup.sh bbb-config
|
- run: ./build/setup.sh bbb-config
|
||||||
|
5
.github/workflows/check-merge-conflict.yml
vendored
5
.github/workflows/check-merge-conflict.yml
vendored
@ -6,8 +6,13 @@ on:
|
|||||||
- opened
|
- opened
|
||||||
- synchronize
|
- synchronize
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
main:
|
main:
|
||||||
|
permissions:
|
||||||
|
pull-requests: write # for eps1lon/actions-label-merge-conflict to label PRs
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check for dirty pull requests
|
- name: Check for dirty pull requests
|
||||||
|
@ -11,7 +11,7 @@ stages:
|
|||||||
|
|
||||||
# define which docker image to use for builds
|
# define which docker image to use for builds
|
||||||
default:
|
default:
|
||||||
image: gitlab.senfcall.de:5050/senfcall-public/docker-bbb-build:v2022-07-12
|
image: gitlab.senfcall.de:5050/senfcall-public/docker-bbb-build:v2022-12-08-meteor-290
|
||||||
|
|
||||||
# This stage uses git to find out since when each package has been unmodified.
|
# This stage uses git to find out since when each package has been unmodified.
|
||||||
# it then checks an API endpoint on the package server to find out for which of
|
# it then checks an API endpoint on the package server to find out for which of
|
||||||
|
@ -18,7 +18,7 @@ val compileSettings = Seq(
|
|||||||
"-Xlint",
|
"-Xlint",
|
||||||
"-Ywarn-dead-code",
|
"-Ywarn-dead-code",
|
||||||
"-language:_",
|
"-language:_",
|
||||||
"-target:jvm-1.11",
|
"-target:11",
|
||||||
"-encoding", "UTF-8"
|
"-encoding", "UTF-8"
|
||||||
),
|
),
|
||||||
javacOptions ++= List(
|
javacOptions ++= List(
|
||||||
@ -48,7 +48,7 @@ lazy val bbbAppsAkka = (project in file(".")).settings(name := "bbb-apps-akka",
|
|||||||
// Config file is in ./.scalariform.conf
|
// Config file is in ./.scalariform.conf
|
||||||
scalariformAutoformat := true
|
scalariformAutoformat := true
|
||||||
|
|
||||||
scalaVersion := "2.13.4"
|
scalaVersion := "2.13.9"
|
||||||
//-----------
|
//-----------
|
||||||
// Packaging
|
// Packaging
|
||||||
//
|
//
|
||||||
|
@ -7,7 +7,7 @@ object Dependencies {
|
|||||||
|
|
||||||
object Versions {
|
object Versions {
|
||||||
// Scala
|
// Scala
|
||||||
val scala = "2.13.4"
|
val scala = "2.13.9"
|
||||||
val junit = "4.12"
|
val junit = "4.12"
|
||||||
val junitInterface = "0.11"
|
val junitInterface = "0.11"
|
||||||
val scalactic = "3.0.8"
|
val scalactic = "3.0.8"
|
||||||
@ -26,7 +26,7 @@ object Dependencies {
|
|||||||
val codec = "1.15"
|
val codec = "1.15"
|
||||||
|
|
||||||
// BigBlueButton
|
// BigBlueButton
|
||||||
val bbbCommons = "0.0.21-SNAPSHOT"
|
val bbbCommons = "0.0.22-SNAPSHOT"
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
val scalaTest = "3.2.11"
|
val scalaTest = "3.2.11"
|
||||||
|
@ -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 PresentationPageConvertedSysMsgHdlr
|
||||||
with PresentationPageConversionStartedSysMsgHdlr
|
with PresentationPageConversionStartedSysMsgHdlr
|
||||||
with PresentationConversionEndedSysMsgHdlr
|
with PresentationConversionEndedSysMsgHdlr
|
||||||
with PresentationUploadedFileTimeoutErrorPubMsgHdlr {
|
with PresentationUploadedFileTimeoutErrorPubMsgHdlr
|
||||||
|
with PresentationHasInvalidMimeTypeErrorPubMsgHdlr {
|
||||||
|
|
||||||
val log = Logging(context.system, getClass)
|
val log = Logging(context.system, getClass)
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,12 @@ import java.io.File
|
|||||||
trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
|
trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
|
||||||
this: PresentationPodHdlrs =>
|
this: PresentationPodHdlrs =>
|
||||||
|
|
||||||
|
object JobTypes {
|
||||||
|
val DOWNLOAD = "PresentationWithAnnotationDownloadJob"
|
||||||
|
val CAPTURE_PRESENTATION = "PresentationWithAnnotationExportJob"
|
||||||
|
val CAPTURE_NOTES = "PadCaptureJob"
|
||||||
|
}
|
||||||
|
|
||||||
def buildStoreAnnotationsInRedisSysMsg(annotations: StoredAnnotations, liveMeeting: LiveMeeting): BbbCommonEnvCoreMsg = {
|
def buildStoreAnnotationsInRedisSysMsg(annotations: StoredAnnotations, liveMeeting: LiveMeeting): BbbCommonEnvCoreMsg = {
|
||||||
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
|
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
|
||||||
val envelope = BbbCoreEnvelope(StoreAnnotationsInRedisSysMsg.NAME, routing)
|
val envelope = BbbCoreEnvelope(StoreAnnotationsInRedisSysMsg.NAME, routing)
|
||||||
@ -111,7 +117,6 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
|
|||||||
log.error(s"Presentation ${presId} not found in meeting ${meetingId}")
|
log.error(s"Presentation ${presId} not found in meeting ${meetingId}")
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
val jobType: String = "PresentationWithAnnotationDownloadJob"
|
|
||||||
val jobId: String = RandomStringGenerator.randomAlphanumericString(16);
|
val jobId: String = RandomStringGenerator.randomAlphanumericString(16);
|
||||||
val allPages: Boolean = m.body.allPages
|
val allPages: Boolean = m.body.allPages
|
||||||
val pageCount = currentPres.get.pages.size
|
val pageCount = currentPres.get.pages.size
|
||||||
@ -120,7 +125,7 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
|
|||||||
val pages: List[Int] = m.body.pages // Desired presentation pages for export
|
val pages: List[Int] = m.body.pages // Desired presentation pages for export
|
||||||
val pagesRange: List[Int] = if (allPages) (1 to pageCount).toList else pages
|
val pagesRange: List[Int] = if (allPages) (1 to pageCount).toList else pages
|
||||||
|
|
||||||
val exportJob: ExportJob = new ExportJob(jobId, jobType, "annotated_slides", presId, presLocation, allPages, pagesRange, meetingId, "");
|
val exportJob: ExportJob = new ExportJob(jobId, JobTypes.DOWNLOAD, "annotated_slides", presId, presLocation, allPages, pagesRange, meetingId, "");
|
||||||
val storeAnnotationPages: List[PresentationPageForExport] = getPresentationPagesForExport(pagesRange, pageCount, presId, currentPres, liveMeeting);
|
val storeAnnotationPages: List[PresentationPageForExport] = getPresentationPagesForExport(pagesRange, pageCount, presId, currentPres, liveMeeting);
|
||||||
|
|
||||||
// Send Export Job to Redis
|
// Send Export Job to Redis
|
||||||
@ -147,7 +152,6 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
|
|||||||
} else {
|
} else {
|
||||||
|
|
||||||
val jobId: String = s"${meetingId}-slides" // Used as the temporaryPresentationId upon upload
|
val jobId: String = s"${meetingId}-slides" // Used as the temporaryPresentationId upon upload
|
||||||
val jobType = "PresentationWithAnnotationExportJob"
|
|
||||||
val allPages: Boolean = m.allPages
|
val allPages: Boolean = m.allPages
|
||||||
val pageCount = currentPres.get.pages.size
|
val pageCount = currentPres.get.pages.size
|
||||||
|
|
||||||
@ -164,7 +168,7 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
|
|||||||
// Informs bbb-web about the token so that when we use it to upload the presentation, it is able to look it up in the list of tokens
|
// Informs bbb-web about the token so that when we use it to upload the presentation, it is able to look it up in the list of tokens
|
||||||
bus.outGW.send(buildPresentationUploadTokenSysPubMsg(parentMeetingId, userId, presentationUploadToken, filename))
|
bus.outGW.send(buildPresentationUploadTokenSysPubMsg(parentMeetingId, userId, presentationUploadToken, filename))
|
||||||
|
|
||||||
val exportJob: ExportJob = new ExportJob(jobId, jobType, filename, presId, presLocation, allPages, pagesRange, parentMeetingId, presentationUploadToken)
|
val exportJob: ExportJob = new ExportJob(jobId, JobTypes.CAPTURE_PRESENTATION, filename, presId, presLocation, allPages, pagesRange, parentMeetingId, presentationUploadToken)
|
||||||
val storeAnnotationPages: List[PresentationPageForExport] = getPresentationPagesForExport(pagesRange, pageCount, presId, currentPres, liveMeeting);
|
val storeAnnotationPages: List[PresentationPageForExport] = getPresentationPagesForExport(pagesRange, pageCount, presId, currentPres, liveMeeting);
|
||||||
|
|
||||||
// Send Export Job to Redis
|
// Send Export Job to Redis
|
||||||
@ -201,13 +205,12 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
|
|||||||
|
|
||||||
val userId: String = "system"
|
val userId: String = "system"
|
||||||
val jobId: String = s"${m.body.breakoutId}-notes" // Used as the temporaryPresentationId upon upload
|
val jobId: String = s"${m.body.breakoutId}-notes" // Used as the temporaryPresentationId upon upload
|
||||||
val jobType = "PadCaptureJob"
|
|
||||||
val filename = m.body.filename
|
val filename = m.body.filename
|
||||||
val presentationUploadToken: String = PresentationPodsApp.generateToken("DEFAULT_PRESENTATION_POD", userId)
|
val presentationUploadToken: String = PresentationPodsApp.generateToken("DEFAULT_PRESENTATION_POD", userId)
|
||||||
|
|
||||||
bus.outGW.send(buildPresentationUploadTokenSysPubMsg(m.body.parentMeetingId, userId, presentationUploadToken, filename))
|
bus.outGW.send(buildPresentationUploadTokenSysPubMsg(m.body.parentMeetingId, userId, presentationUploadToken, filename))
|
||||||
|
|
||||||
val exportJob = new ExportJob(jobId, jobType, filename, m.body.padId, "", true, List(), m.body.parentMeetingId, presentationUploadToken)
|
val exportJob = new ExportJob(jobId, JobTypes.CAPTURE_NOTES, filename, m.body.padId, "", true, List(), m.body.parentMeetingId, presentationUploadToken)
|
||||||
val job = buildStoreExportJobInRedisSysMsg(exportJob, liveMeeting)
|
val job = buildStoreExportJobInRedisSysMsg(exportJob, liveMeeting)
|
||||||
|
|
||||||
bus.outGW.send(job)
|
bus.outGW.send(job)
|
||||||
|
@ -42,6 +42,17 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait {
|
|||||||
|
|
||||||
MeetingStatus2x.setPermissions(liveMeeting.status, settings)
|
MeetingStatus2x.setPermissions(liveMeeting.status, settings)
|
||||||
|
|
||||||
|
// Dial-in
|
||||||
|
def buildLockMessage(meetingId: String, userId: String, lockedBy: String, locked: Boolean): BbbCommonEnvCoreMsg = {
|
||||||
|
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId)
|
||||||
|
val envelope = BbbCoreEnvelope(UserLockedInMeetingEvtMsg.NAME, routing)
|
||||||
|
val body = UserLockedInMeetingEvtMsgBody(userId, locked, lockedBy)
|
||||||
|
val header = BbbClientMsgHeader(UserLockedInMeetingEvtMsg.NAME, meetingId, userId)
|
||||||
|
val event = UserLockedInMeetingEvtMsg(header, body)
|
||||||
|
|
||||||
|
BbbCommonEnvCoreMsg(envelope, event)
|
||||||
|
}
|
||||||
|
|
||||||
if (oldPermissions.disableCam != settings.disableCam) {
|
if (oldPermissions.disableCam != settings.disableCam) {
|
||||||
if (settings.disableCam) {
|
if (settings.disableCam) {
|
||||||
val notifyEvent = MsgBuilder.buildNotifyAllInMeetingEvtMsg(
|
val notifyEvent = MsgBuilder.buildNotifyAllInMeetingEvtMsg(
|
||||||
@ -55,24 +66,6 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait {
|
|||||||
outGW.send(notifyEvent)
|
outGW.send(notifyEvent)
|
||||||
|
|
||||||
LockSettingsUtil.enforceCamLockSettingsForAllUsers(liveMeeting, outGW)
|
LockSettingsUtil.enforceCamLockSettingsForAllUsers(liveMeeting, outGW)
|
||||||
|
|
||||||
// Dial-in
|
|
||||||
def buildLockMessage(meetingId: String, userId: String, lockedBy: String, locked: Boolean): BbbCommonEnvCoreMsg = {
|
|
||||||
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId)
|
|
||||||
val envelope = BbbCoreEnvelope(UserLockedInMeetingEvtMsg.NAME, routing)
|
|
||||||
val body = UserLockedInMeetingEvtMsgBody(userId, locked, lockedBy)
|
|
||||||
val header = BbbClientMsgHeader(UserLockedInMeetingEvtMsg.NAME, meetingId, userId)
|
|
||||||
val event = UserLockedInMeetingEvtMsg(header, body)
|
|
||||||
|
|
||||||
BbbCommonEnvCoreMsg(envelope, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
VoiceUsers.findAll(liveMeeting.voiceUsers) foreach { vu =>
|
|
||||||
if (vu.intId.startsWith(IntIdPrefixType.DIAL_IN)) { // only Dial-in users need this
|
|
||||||
val eventExplicitLock = buildLockMessage(liveMeeting.props.meetingProp.intId, vu.intId, msg.body.setBy, settings.disableMic)
|
|
||||||
outGW.send(eventExplicitLock)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
val notifyEvent = MsgBuilder.buildNotifyAllInMeetingEvtMsg(
|
val notifyEvent = MsgBuilder.buildNotifyAllInMeetingEvtMsg(
|
||||||
liveMeeting.props.meetingProp.intId,
|
liveMeeting.props.meetingProp.intId,
|
||||||
@ -97,8 +90,12 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait {
|
|||||||
Vector()
|
Vector()
|
||||||
)
|
)
|
||||||
outGW.send(notifyEvent)
|
outGW.send(notifyEvent)
|
||||||
|
VoiceUsers.findAll(liveMeeting.voiceUsers) foreach { vu =>
|
||||||
// Apply lock settings when disableMic from false to true.
|
if (vu.intId.startsWith(IntIdPrefixType.DIAL_IN)) { // only Dial-in users need this
|
||||||
|
val eventExplicitLock = buildLockMessage(liveMeeting.props.meetingProp.intId, vu.intId, msg.body.setBy, settings.disableMic)
|
||||||
|
outGW.send(eventExplicitLock)
|
||||||
|
}
|
||||||
|
}
|
||||||
LockSettingsUtil.enforceLockSettingsForAllVoiceUsers(liveMeeting, outGW)
|
LockSettingsUtil.enforceLockSettingsForAllVoiceUsers(liveMeeting, outGW)
|
||||||
} else {
|
} else {
|
||||||
val notifyEvent = MsgBuilder.buildNotifyAllInMeetingEvtMsg(
|
val notifyEvent = MsgBuilder.buildNotifyAllInMeetingEvtMsg(
|
||||||
|
@ -288,6 +288,8 @@ class ReceivedJsonMsgHandlerActor(
|
|||||||
routeGenericMsg[PreuploadedPresentationsSysPubMsg](envelope, jsonNode)
|
routeGenericMsg[PreuploadedPresentationsSysPubMsg](envelope, jsonNode)
|
||||||
case PresentationUploadedFileTooLargeErrorSysPubMsg.NAME =>
|
case PresentationUploadedFileTooLargeErrorSysPubMsg.NAME =>
|
||||||
routeGenericMsg[PresentationUploadedFileTooLargeErrorSysPubMsg](envelope, jsonNode)
|
routeGenericMsg[PresentationUploadedFileTooLargeErrorSysPubMsg](envelope, jsonNode)
|
||||||
|
case PresentationHasInvalidMimeTypeErrorSysPubMsg.NAME =>
|
||||||
|
routeGenericMsg[PresentationHasInvalidMimeTypeErrorSysPubMsg](envelope, jsonNode)
|
||||||
case PresentationUploadedFileTimeoutErrorSysPubMsg.NAME =>
|
case PresentationUploadedFileTimeoutErrorSysPubMsg.NAME =>
|
||||||
routeGenericMsg[PresentationUploadedFileTimeoutErrorSysPubMsg](envelope, jsonNode)
|
routeGenericMsg[PresentationUploadedFileTimeoutErrorSysPubMsg](envelope, jsonNode)
|
||||||
case PresentationConversionUpdateSysPubMsg.NAME =>
|
case PresentationConversionUpdateSysPubMsg.NAME =>
|
||||||
|
@ -523,6 +523,7 @@ class MeetingActor(
|
|||||||
case m: SetPresentationDownloadablePubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
case m: SetPresentationDownloadablePubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||||
case m: PresentationConversionUpdateSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
case m: PresentationConversionUpdateSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||||
case m: PresentationUploadedFileTooLargeErrorSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
case m: PresentationUploadedFileTooLargeErrorSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||||
|
case m: PresentationHasInvalidMimeTypeErrorSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||||
case m: PresentationUploadedFileTimeoutErrorSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
case m: PresentationUploadedFileTimeoutErrorSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||||
case m: PresentationPageGeneratedSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
case m: PresentationPageGeneratedSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||||
case m: PresentationPageCountErrorSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
case m: PresentationPageCountErrorSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||||
|
@ -85,6 +85,7 @@ case class PresentationSlide(
|
|||||||
presentationId: String,
|
presentationId: String,
|
||||||
pageNum: Long,
|
pageNum: Long,
|
||||||
setOn: Long = System.currentTimeMillis(),
|
setOn: Long = System.currentTimeMillis(),
|
||||||
|
presentationName: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -189,7 +190,7 @@ class LearningDashboardActor(
|
|||||||
for {
|
for {
|
||||||
page <- msg.body.presentation.pages.find(p => p.current == true)
|
page <- msg.body.presentation.pages.find(p => p.current == true)
|
||||||
} yield {
|
} yield {
|
||||||
this.setPresentationSlide(meeting.intId, msg.body.presentation.id,page.num)
|
this.setPresentationSlide(meeting.intId, msg.body.presentation.id,page.num, msg.body.presentation.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -202,7 +203,7 @@ class LearningDashboardActor(
|
|||||||
presentation <- presentations.get(msg.body.presentationId)
|
presentation <- presentations.get(msg.body.presentationId)
|
||||||
page <- presentation.pages.find(p => p.id == msg.body.pageId)
|
page <- presentation.pages.find(p => p.id == msg.body.pageId)
|
||||||
} yield {
|
} yield {
|
||||||
this.setPresentationSlide(meeting.intId, msg.body.presentationId,page.num)
|
this.setPresentationSlide(meeting.intId, msg.body.presentationId,page.num, presentation.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,7 +212,7 @@ class LearningDashboardActor(
|
|||||||
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)
|
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)
|
||||||
} yield {
|
} yield {
|
||||||
if(meeting.presentationSlides.last.presentationId == msg.body.presentationId) {
|
if(meeting.presentationSlides.last.presentationId == msg.body.presentationId) {
|
||||||
this.setPresentationSlide(meeting.intId, "",0)
|
this.setPresentationSlide(meeting.intId, "",0, "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -223,7 +224,7 @@ class LearningDashboardActor(
|
|||||||
val presPreviousSlides: Vector[PresentationSlide] = meeting.presentationSlides.filter(p => p.presentationId == msg.body.presentationId);
|
val presPreviousSlides: Vector[PresentationSlide] = meeting.presentationSlides.filter(p => p.presentationId == msg.body.presentationId);
|
||||||
if(presPreviousSlides.length > 0) {
|
if(presPreviousSlides.length > 0) {
|
||||||
//Set last page showed for this presentation
|
//Set last page showed for this presentation
|
||||||
this.setPresentationSlide(meeting.intId, msg.body.presentationId,presPreviousSlides.last.pageNum)
|
this.setPresentationSlide(meeting.intId, msg.body.presentationId,presPreviousSlides.last.pageNum, presPreviousSlides.last.presentationName)
|
||||||
} else {
|
} else {
|
||||||
//If none page was showed yet, set the current page (page 1 by default)
|
//If none page was showed yet, set the current page (page 1 by default)
|
||||||
for {
|
for {
|
||||||
@ -231,20 +232,20 @@ class LearningDashboardActor(
|
|||||||
presentation <- presentations.get(msg.body.presentationId)
|
presentation <- presentations.get(msg.body.presentationId)
|
||||||
page <- presentation.pages.find(s => s.current == true)
|
page <- presentation.pages.find(s => s.current == true)
|
||||||
} yield {
|
} yield {
|
||||||
this.setPresentationSlide(meeting.intId, msg.body.presentationId,page.num)
|
this.setPresentationSlide(meeting.intId, msg.body.presentationId,page.num, presentation.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def setPresentationSlide(meetingId: String, presentationId: String, pageNum: Long) {
|
private def setPresentationSlide(meetingId: String, presentationId: String, pageNum: Long, presentationName: String) {
|
||||||
for {
|
for {
|
||||||
meeting <- meetings.values.find(m => m.intId == meetingId)
|
meeting <- meetings.values.find(m => m.intId == meetingId)
|
||||||
} yield {
|
} yield {
|
||||||
if (meeting.presentationSlides.length == 0 ||
|
if (meeting.presentationSlides.length == 0 ||
|
||||||
meeting.presentationSlides.last.presentationId != presentationId ||
|
meeting.presentationSlides.last.presentationId != presentationId ||
|
||||||
meeting.presentationSlides.last.pageNum != pageNum) {
|
meeting.presentationSlides.last.pageNum != pageNum) {
|
||||||
val updatedMeeting = meeting.copy(presentationSlides = meeting.presentationSlides :+ PresentationSlide(presentationId, pageNum))
|
val updatedMeeting = meeting.copy(presentationSlides = meeting.presentationSlides :+ PresentationSlide(presentationId, pageNum, presentationName = presentationName))
|
||||||
|
|
||||||
meetings += (updatedMeeting.intId -> updatedMeeting)
|
meetings += (updatedMeeting.intId -> updatedMeeting)
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ val compileSettings = Seq(
|
|||||||
"-Xlint",
|
"-Xlint",
|
||||||
"-Ywarn-dead-code",
|
"-Ywarn-dead-code",
|
||||||
"-language:_",
|
"-language:_",
|
||||||
"-target:jvm-1.11",
|
"-target:11",
|
||||||
"-encoding", "UTF-8"
|
"-encoding", "UTF-8"
|
||||||
),
|
),
|
||||||
javacOptions ++= List(
|
javacOptions ++= List(
|
||||||
@ -27,7 +27,7 @@ val compileSettings = Seq(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
scalaVersion := "2.13.4"
|
scalaVersion := "2.13.9"
|
||||||
|
|
||||||
resolvers += Resolver.sonatypeRepo("releases")
|
resolvers += Resolver.sonatypeRepo("releases")
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ object Dependencies {
|
|||||||
|
|
||||||
object Versions {
|
object Versions {
|
||||||
// Scala
|
// Scala
|
||||||
val scala = "2.13.4"
|
val scala = "2.13.9"
|
||||||
val junitInterface = "0.11"
|
val junitInterface = "0.11"
|
||||||
val scalactic = "3.0.8"
|
val scalactic = "3.0.8"
|
||||||
|
|
||||||
@ -21,8 +21,8 @@ object Dependencies {
|
|||||||
val codec = "1.15"
|
val codec = "1.15"
|
||||||
|
|
||||||
// BigBlueButton
|
// BigBlueButton
|
||||||
val bbbCommons = "0.0.21-SNAPSHOT"
|
val bbbCommons = "0.0.22-SNAPSHOT"
|
||||||
val bbbFsesl = "0.0.8-SNAPSHOT"
|
val bbbFsesl = "0.0.9-SNAPSHOT"
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
val scalaTest = "3.2.11"
|
val scalaTest = "3.2.11"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import org.bigbluebutton.build._
|
import org.bigbluebutton.build._
|
||||||
|
|
||||||
|
|
||||||
version := "0.0.21-SNAPSHOT"
|
version := "0.0.22-SNAPSHOT"
|
||||||
|
|
||||||
val compileSettings = Seq(
|
val compileSettings = Seq(
|
||||||
organization := "org.bigbluebutton",
|
organization := "org.bigbluebutton",
|
||||||
@ -12,7 +12,7 @@ val compileSettings = Seq(
|
|||||||
"-Xlint",
|
"-Xlint",
|
||||||
"-Ywarn-dead-code",
|
"-Ywarn-dead-code",
|
||||||
"-language:_",
|
"-language:_",
|
||||||
"-target:jvm-1.11",
|
"-target:11",
|
||||||
"-encoding", "UTF-8"
|
"-encoding", "UTF-8"
|
||||||
),
|
),
|
||||||
javacOptions ++= List(
|
javacOptions ++= List(
|
||||||
@ -55,7 +55,7 @@ scalariformAutoformat := true
|
|||||||
// Do not append Scala versions to the generated artifacts
|
// Do not append Scala versions to the generated artifacts
|
||||||
//crossPaths := false
|
//crossPaths := false
|
||||||
|
|
||||||
scalaVersion := "2.13.4"
|
scalaVersion := "2.13.9"
|
||||||
|
|
||||||
// This forbids including Scala related libraries into the dependency
|
// This forbids including Scala related libraries into the dependency
|
||||||
//autoScalaLibrary := false
|
//autoScalaLibrary := false
|
||||||
|
@ -7,7 +7,7 @@ object Dependencies {
|
|||||||
|
|
||||||
object Versions {
|
object Versions {
|
||||||
// Scala
|
// Scala
|
||||||
val scala = "2.13.4"
|
val scala = "2.13.9"
|
||||||
val junit = "4.12"
|
val junit = "4.12"
|
||||||
val junitInterface = "0.11"
|
val junitInterface = "0.11"
|
||||||
val scalactic = "3.0.8"
|
val scalactic = "3.0.8"
|
||||||
|
@ -9,16 +9,16 @@ case class DurationProps(duration: Int, createdTime: Long, createdDate: String,
|
|||||||
endWhenNoModerator: Boolean, endWhenNoModeratorDelayInMinutes: Int)
|
endWhenNoModerator: Boolean, endWhenNoModeratorDelayInMinutes: Int)
|
||||||
|
|
||||||
case class MeetingProp(
|
case class MeetingProp(
|
||||||
name: String,
|
name: String,
|
||||||
extId: String,
|
extId: String,
|
||||||
intId: String,
|
intId: String,
|
||||||
meetingCameraCap: Int,
|
meetingCameraCap: Int,
|
||||||
maxPinnedCameras: Int,
|
maxPinnedCameras: Int,
|
||||||
isBreakout: Boolean,
|
isBreakout: Boolean,
|
||||||
disabledFeatures: Vector[String],
|
disabledFeatures: Vector[String],
|
||||||
notifyRecordingIsOn: Boolean,
|
notifyRecordingIsOn: Boolean,
|
||||||
uploadExternalDescription: String,
|
presentationUploadExternalDescription: String,
|
||||||
uploadExternalUrl: String,
|
presentationUploadExternalUrl: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
case class BreakoutProps(
|
case class BreakoutProps(
|
||||||
|
@ -166,6 +166,23 @@ case class PresentationUploadedFileTooLargeErrorSysPubMsgBody(
|
|||||||
maxFileSize: Int
|
maxFileSize: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
object PresentationHasInvalidMimeTypeErrorSysPubMsg { val NAME = "PresentationHasInvalidMimeTypeErrorSysPubMsg" }
|
||||||
|
case class PresentationHasInvalidMimeTypeErrorSysPubMsg(
|
||||||
|
header: BbbClientMsgHeader,
|
||||||
|
body: PresentationHasInvalidMimeTypeErrorSysPubMsgBody
|
||||||
|
) extends StandardMsg
|
||||||
|
case class PresentationHasInvalidMimeTypeErrorSysPubMsgBody(
|
||||||
|
podId: String,
|
||||||
|
meetingId: String,
|
||||||
|
presentationName: String,
|
||||||
|
temporaryPresentationId: String,
|
||||||
|
presentationId: String,
|
||||||
|
messageKey: String,
|
||||||
|
fileMime: String,
|
||||||
|
fileExtension: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
object PresentationUploadedFileTimeoutErrorSysPubMsg { val NAME = "PresentationUploadedFileTimeoutErrorSysPubMsg" }
|
object PresentationUploadedFileTimeoutErrorSysPubMsg { val NAME = "PresentationUploadedFileTimeoutErrorSysPubMsg" }
|
||||||
case class PresentationUploadedFileTimeoutErrorSysPubMsg(
|
case class PresentationUploadedFileTimeoutErrorSysPubMsg(
|
||||||
header: BbbClientMsgHeader,
|
header: BbbClientMsgHeader,
|
||||||
@ -237,6 +254,13 @@ object PresentationUploadedFileTooLargeErrorEvtMsg { val NAME = "PresentationUpl
|
|||||||
case class PresentationUploadedFileTooLargeErrorEvtMsg(header: BbbClientMsgHeader, body: PresentationUploadedFileTooLargeErrorEvtMsgBody) extends BbbCoreMsg
|
case class PresentationUploadedFileTooLargeErrorEvtMsg(header: BbbClientMsgHeader, body: PresentationUploadedFileTooLargeErrorEvtMsgBody) extends BbbCoreMsg
|
||||||
case class PresentationUploadedFileTooLargeErrorEvtMsgBody(podId: String, messageKey: String, code: String, presentationName: String, presentationToken: String, fileSize: Int, maxFileSize: Int)
|
case class PresentationUploadedFileTooLargeErrorEvtMsgBody(podId: String, messageKey: String, code: String, presentationName: String, presentationToken: String, fileSize: Int, maxFileSize: Int)
|
||||||
|
|
||||||
|
object PresentationHasInvalidMimeTypeErrorEvtMsg { val NAME = "PresentationHasInvalidMimeTypeErrorEvtMsg" }
|
||||||
|
case class PresentationHasInvalidMimeTypeErrorEvtMsg(header: BbbClientMsgHeader, body: PresentationHasInvalidMimeTypeErrorEvtMsgBody) extends BbbCoreMsg
|
||||||
|
case class PresentationHasInvalidMimeTypeErrorEvtMsgBody(podId: String, meetingId: String, presentationName: String,
|
||||||
|
temporaryPresentationId: String, presentationId: String,
|
||||||
|
messageKey: String, fileMime: String, fileExtension: String,
|
||||||
|
)
|
||||||
|
|
||||||
object PresentationUploadedFileTimeoutErrorEvtMsg { val NAME = "PresentationUploadedFileTimeoutErrorEvtMsg" }
|
object PresentationUploadedFileTimeoutErrorEvtMsg { val NAME = "PresentationUploadedFileTimeoutErrorEvtMsg" }
|
||||||
case class PresentationUploadedFileTimeoutErrorEvtMsg(header: BbbClientMsgHeader, body: PresentationUploadedFileTimeoutErrorEvtMsgBody) extends BbbCoreMsg
|
case class PresentationUploadedFileTimeoutErrorEvtMsg(header: BbbClientMsgHeader, body: PresentationUploadedFileTimeoutErrorEvtMsgBody) extends BbbCoreMsg
|
||||||
case class PresentationUploadedFileTimeoutErrorEvtMsgBody(podId: String, meetingId: String, presentationName: String,
|
case class PresentationUploadedFileTimeoutErrorEvtMsgBody(podId: String, meetingId: String, presentationName: String,
|
||||||
|
@ -11,7 +11,7 @@ val compileSettings = Seq(
|
|||||||
"-Xlint",
|
"-Xlint",
|
||||||
"-Ywarn-dead-code",
|
"-Ywarn-dead-code",
|
||||||
"-language:_",
|
"-language:_",
|
||||||
"-target:jvm-1.11",
|
"-target:11",
|
||||||
"-encoding", "UTF-8"
|
"-encoding", "UTF-8"
|
||||||
),
|
),
|
||||||
javacOptions ++= List(
|
javacOptions ++= List(
|
||||||
@ -40,7 +40,7 @@ lazy val commonWeb = (project in file(".")).settings(name := "bbb-common-web", l
|
|||||||
// Config file is in ./.scalariform.conf
|
// Config file is in ./.scalariform.conf
|
||||||
scalariformAutoformat := true
|
scalariformAutoformat := true
|
||||||
|
|
||||||
scalaVersion := "2.13.4"
|
scalaVersion := "2.13.9"
|
||||||
//-----------
|
//-----------
|
||||||
// Packaging
|
// Packaging
|
||||||
//
|
//
|
||||||
|
@ -7,7 +7,7 @@ object Dependencies {
|
|||||||
|
|
||||||
object Versions {
|
object Versions {
|
||||||
// Scala
|
// Scala
|
||||||
val scala = "2.13.4"
|
val scala = "2.13.9"
|
||||||
val junit = "4.12"
|
val junit = "4.12"
|
||||||
val junitInterface = "0.11"
|
val junitInterface = "0.11"
|
||||||
val scalactic = "3.0.8"
|
val scalactic = "3.0.8"
|
||||||
@ -34,7 +34,7 @@ object Dependencies {
|
|||||||
val text = "1.10.0"
|
val text = "1.10.0"
|
||||||
|
|
||||||
// BigBlueButton
|
// BigBlueButton
|
||||||
val bbbCommons = "0.0.21-SNAPSHOT"
|
val bbbCommons = "0.0.22-SNAPSHOT"
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
val scalaTest = "3.2.11"
|
val scalaTest = "3.2.11"
|
||||||
|
@ -73,8 +73,8 @@ public class ApiParams {
|
|||||||
public static final String DISABLED_FEATURES = "disabledFeatures";
|
public static final String DISABLED_FEATURES = "disabledFeatures";
|
||||||
public static final String NOTIFY_RECORDING_IS_ON = "notifyRecordingIsOn";
|
public static final String NOTIFY_RECORDING_IS_ON = "notifyRecordingIsOn";
|
||||||
|
|
||||||
public static final String UPLOAD_EXTERNAL_DESCRIPTION = "uploadExternalDescription";
|
public static final String PRESENTATION_UPLOAD_EXTERNAL_DESCRIPTION = "presentationUploadExternalDescription";
|
||||||
public static final String UPLOAD_EXTERNAL_URL = "uploadExternalUrl";
|
public static final String PRESENTATION_UPLOAD_EXTERNAL_URL = "presentationUploadExternalUrl";
|
||||||
|
|
||||||
public static final String BREAKOUT_ROOMS_CAPTURE_SLIDES = "breakoutRoomsCaptureSlides";
|
public static final String BREAKOUT_ROOMS_CAPTURE_SLIDES = "breakoutRoomsCaptureSlides";
|
||||||
public static final String BREAKOUT_ROOMS_CAPTURE_NOTES = "breakoutRoomsCaptureNotes";
|
public static final String BREAKOUT_ROOMS_CAPTURE_NOTES = "breakoutRoomsCaptureNotes";
|
||||||
|
@ -419,7 +419,7 @@ public class MeetingService implements MessageListener {
|
|||||||
m.getMuteOnStart(), m.getAllowModsToUnmuteUsers(), m.getAllowModsToEjectCameras(), m.getMeetingKeepEvents(),
|
m.getMuteOnStart(), m.getAllowModsToUnmuteUsers(), m.getAllowModsToEjectCameras(), m.getMeetingKeepEvents(),
|
||||||
m.breakoutRoomsParams, m.lockSettingsParams, m.getHtml5InstanceId(),
|
m.breakoutRoomsParams, m.lockSettingsParams, m.getHtml5InstanceId(),
|
||||||
m.getGroups(), m.getDisabledFeatures(), m.getNotifyRecordingIsOn(),
|
m.getGroups(), m.getDisabledFeatures(), m.getNotifyRecordingIsOn(),
|
||||||
m.getUploadExternalDescription(), m.getUploadExternalUrl());
|
m.getPresentationUploadExternalDescription(), m.getPresentationUploadExternalUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
private String formatPrettyDate(Long timestamp) {
|
private String formatPrettyDate(Long timestamp) {
|
||||||
|
@ -47,6 +47,7 @@ import org.bigbluebutton.api.domain.BreakoutRoomsParams;
|
|||||||
import org.bigbluebutton.api.domain.LockSettingsParams;
|
import org.bigbluebutton.api.domain.LockSettingsParams;
|
||||||
import org.bigbluebutton.api.domain.Meeting;
|
import org.bigbluebutton.api.domain.Meeting;
|
||||||
import org.bigbluebutton.api.domain.Group;
|
import org.bigbluebutton.api.domain.Group;
|
||||||
|
import org.bigbluebutton.api.service.ServiceUtils;
|
||||||
import org.bigbluebutton.api.util.ParamsUtil;
|
import org.bigbluebutton.api.util.ParamsUtil;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@ -80,6 +81,7 @@ public class ParamsProcessorUtil {
|
|||||||
private String defaultHTML5ClientUrl;
|
private String defaultHTML5ClientUrl;
|
||||||
private String defaultGuestWaitURL;
|
private String defaultGuestWaitURL;
|
||||||
private Boolean allowRequestsWithoutSession = false;
|
private Boolean allowRequestsWithoutSession = false;
|
||||||
|
private Integer defaultHttpSessionTimeout = 14400;
|
||||||
private Boolean useDefaultAvatar = false;
|
private Boolean useDefaultAvatar = false;
|
||||||
private String defaultAvatarURL;
|
private String defaultAvatarURL;
|
||||||
private String defaultGuestPolicy;
|
private String defaultGuestPolicy;
|
||||||
@ -103,8 +105,8 @@ public class ParamsProcessorUtil {
|
|||||||
private boolean defaultKeepEvents = false;
|
private boolean defaultKeepEvents = false;
|
||||||
private Boolean useDefaultLogo;
|
private Boolean useDefaultLogo;
|
||||||
private String defaultLogoURL;
|
private String defaultLogoURL;
|
||||||
private String defaultUploadExternalDescription = "";
|
private String defaultPresentationUploadExternalDescription = "";
|
||||||
private String defaultUploadExternalUrl = "";
|
private String defaultPresentationUploadExternalUrl = "";
|
||||||
|
|
||||||
private boolean defaultBreakoutRoomsEnabled = true;
|
private boolean defaultBreakoutRoomsEnabled = true;
|
||||||
private boolean defaultBreakoutRoomsRecord;
|
private boolean defaultBreakoutRoomsRecord;
|
||||||
@ -638,14 +640,14 @@ public class ParamsProcessorUtil {
|
|||||||
guestPolicy = params.get(ApiParams.GUEST_POLICY);
|
guestPolicy = params.get(ApiParams.GUEST_POLICY);
|
||||||
}
|
}
|
||||||
|
|
||||||
String uploadExternalDescription = defaultUploadExternalDescription;
|
String presentationUploadExternalDescription = defaultPresentationUploadExternalDescription;
|
||||||
if (!StringUtils.isEmpty(params.get(ApiParams.UPLOAD_EXTERNAL_DESCRIPTION))) {
|
if (!StringUtils.isEmpty(params.get(ApiParams.PRESENTATION_UPLOAD_EXTERNAL_DESCRIPTION))) {
|
||||||
uploadExternalDescription = params.get(ApiParams.UPLOAD_EXTERNAL_DESCRIPTION);
|
presentationUploadExternalDescription = params.get(ApiParams.PRESENTATION_UPLOAD_EXTERNAL_DESCRIPTION);
|
||||||
}
|
}
|
||||||
|
|
||||||
String uploadExternalUrl = defaultUploadExternalUrl;
|
String presentationUploadExternalUrl = defaultPresentationUploadExternalUrl;
|
||||||
if (!StringUtils.isEmpty(params.get(ApiParams.UPLOAD_EXTERNAL_URL))) {
|
if (!StringUtils.isEmpty(params.get(ApiParams.PRESENTATION_UPLOAD_EXTERNAL_URL))) {
|
||||||
uploadExternalUrl = params.get(ApiParams.UPLOAD_EXTERNAL_URL);
|
presentationUploadExternalUrl = params.get(ApiParams.PRESENTATION_UPLOAD_EXTERNAL_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
String meetingLayout = defaultMeetingLayout;
|
String meetingLayout = defaultMeetingLayout;
|
||||||
@ -685,7 +687,7 @@ public class ParamsProcessorUtil {
|
|||||||
String parentMeetingId = "";
|
String parentMeetingId = "";
|
||||||
if (isBreakout) {
|
if (isBreakout) {
|
||||||
internalMeetingId = params.get(ApiParams.MEETING_ID);
|
internalMeetingId = params.get(ApiParams.MEETING_ID);
|
||||||
parentMeetingId = params.get(ApiParams.PARENT_MEETING_ID);
|
parentMeetingId = ServiceUtils.findMeetingFromMeetingID(params.get(ApiParams.PARENT_MEETING_ID)).getInternalId();
|
||||||
// We rebuild the the external meeting using the has of the parent
|
// We rebuild the the external meeting using the has of the parent
|
||||||
// meeting, the shared timestamp and the sequence number
|
// meeting, the shared timestamp and the sequence number
|
||||||
String timeStamp = StringUtils.substringAfter(internalMeetingId, "-");
|
String timeStamp = StringUtils.substringAfter(internalMeetingId, "-");
|
||||||
@ -736,8 +738,8 @@ public class ParamsProcessorUtil {
|
|||||||
.withGroups(groups)
|
.withGroups(groups)
|
||||||
.withDisabledFeatures(listOfDisabledFeatures)
|
.withDisabledFeatures(listOfDisabledFeatures)
|
||||||
.withNotifyRecordingIsOn(notifyRecordingIsOn)
|
.withNotifyRecordingIsOn(notifyRecordingIsOn)
|
||||||
.withUploadExternalDescription(uploadExternalDescription)
|
.withPresentationUploadExternalDescription(presentationUploadExternalDescription)
|
||||||
.withUploadExternalUrl(uploadExternalUrl)
|
.withPresentationUploadExternalUrl(presentationUploadExternalUrl)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
if (!StringUtils.isEmpty(params.get(ApiParams.MODERATOR_ONLY_MESSAGE))) {
|
if (!StringUtils.isEmpty(params.get(ApiParams.MODERATOR_ONLY_MESSAGE))) {
|
||||||
@ -845,6 +847,14 @@ public class ParamsProcessorUtil {
|
|||||||
return allowRequestsWithoutSession;
|
return allowRequestsWithoutSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Integer getDefaultHttpSessionTimeout() {
|
||||||
|
return defaultHttpSessionTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDefaultHttpSessionTimeout(Integer value) {
|
||||||
|
this.defaultHttpSessionTimeout = value;
|
||||||
|
}
|
||||||
|
|
||||||
public String getDefaultLogoutUrl() {
|
public String getDefaultLogoutUrl() {
|
||||||
if ((StringUtils.isEmpty(defaultLogoutUrl)) || "default".equalsIgnoreCase(defaultLogoutUrl)) {
|
if ((StringUtils.isEmpty(defaultLogoutUrl)) || "default".equalsIgnoreCase(defaultLogoutUrl)) {
|
||||||
return defaultServerUrl;
|
return defaultServerUrl;
|
||||||
@ -1444,12 +1454,12 @@ public class ParamsProcessorUtil {
|
|||||||
this.defaultNotifyRecordingIsOn = notifyRecordingIsOn;
|
this.defaultNotifyRecordingIsOn = notifyRecordingIsOn;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUploadExternalDescription(String uploadExternalDescription) {
|
public void setPresentationUploadExternalDescription(String presentationUploadExternalDescription) {
|
||||||
this.defaultUploadExternalDescription = uploadExternalDescription;
|
this.defaultPresentationUploadExternalDescription = presentationUploadExternalDescription;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUploadExternalUrl(String uploadExternalUrl) {
|
public void setPresentationUploadExternalUrl(String presentationUploadExternalUrl) {
|
||||||
this.defaultUploadExternalUrl = uploadExternalUrl;
|
this.defaultPresentationUploadExternalUrl = presentationUploadExternalUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setBbbVersion(String version) {
|
public void setBbbVersion(String version) {
|
||||||
|
@ -97,8 +97,8 @@ public class Meeting {
|
|||||||
private Boolean allowRequestsWithoutSession = false;
|
private Boolean allowRequestsWithoutSession = false;
|
||||||
private Boolean allowModsToEjectCameras = false;
|
private Boolean allowModsToEjectCameras = false;
|
||||||
private Boolean meetingKeepEvents;
|
private Boolean meetingKeepEvents;
|
||||||
private String uploadExternalDescription;
|
private String presentationUploadExternalDescription;
|
||||||
private String uploadExternalUrl;
|
private String presentationUploadExternalUrl;
|
||||||
|
|
||||||
private Integer meetingExpireIfNoUserJoinedInMinutes = 5;
|
private Integer meetingExpireIfNoUserJoinedInMinutes = 5;
|
||||||
private Integer meetingExpireWhenLastUserLeftInMinutes = 1;
|
private Integer meetingExpireWhenLastUserLeftInMinutes = 1;
|
||||||
@ -123,8 +123,8 @@ public class Meeting {
|
|||||||
intMeetingId = builder.internalId;
|
intMeetingId = builder.internalId;
|
||||||
disabledFeatures = builder.disabledFeatures;
|
disabledFeatures = builder.disabledFeatures;
|
||||||
notifyRecordingIsOn = builder.notifyRecordingIsOn;
|
notifyRecordingIsOn = builder.notifyRecordingIsOn;
|
||||||
uploadExternalDescription = builder.uploadExternalDescription;
|
presentationUploadExternalDescription = builder.presentationUploadExternalDescription;
|
||||||
uploadExternalUrl = builder.uploadExternalUrl;
|
presentationUploadExternalUrl = builder.presentationUploadExternalUrl;
|
||||||
if (builder.viewerPass == null){
|
if (builder.viewerPass == null){
|
||||||
viewerPass = "";
|
viewerPass = "";
|
||||||
} else {
|
} else {
|
||||||
@ -424,11 +424,11 @@ public class Meeting {
|
|||||||
return notifyRecordingIsOn;
|
return notifyRecordingIsOn;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUploadExternalDescription() {
|
public String getPresentationUploadExternalDescription() {
|
||||||
return uploadExternalDescription;
|
return presentationUploadExternalDescription;
|
||||||
}
|
}
|
||||||
public String getUploadExternalUrl() {
|
public String getPresentationUploadExternalUrl() {
|
||||||
return uploadExternalUrl;
|
return presentationUploadExternalUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getWelcomeMessageTemplate() {
|
public String getWelcomeMessageTemplate() {
|
||||||
@ -863,8 +863,8 @@ public class Meeting {
|
|||||||
private String learningDashboardAccessToken;
|
private String learningDashboardAccessToken;
|
||||||
private ArrayList<String> disabledFeatures;
|
private ArrayList<String> disabledFeatures;
|
||||||
private Boolean notifyRecordingIsOn;
|
private Boolean notifyRecordingIsOn;
|
||||||
private String uploadExternalDescription;
|
private String presentationUploadExternalDescription;
|
||||||
private String uploadExternalUrl;
|
private String presentationUploadExternalUrl;
|
||||||
private int duration;
|
private int duration;
|
||||||
private String webVoice;
|
private String webVoice;
|
||||||
private String telVoice;
|
private String telVoice;
|
||||||
@ -993,13 +993,13 @@ public class Meeting {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder withUploadExternalDescription(String d) {
|
public Builder withPresentationUploadExternalDescription(String d) {
|
||||||
this.uploadExternalDescription = d;
|
this.presentationUploadExternalDescription = d;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder withUploadExternalUrl(String u) {
|
public Builder withPresentationUploadExternalUrl(String u) {
|
||||||
this.uploadExternalUrl = u;
|
this.presentationUploadExternalUrl = u;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,8 +25,8 @@ public class CreateMeetingMessage {
|
|||||||
public final String learningDashboardAccessToken;
|
public final String learningDashboardAccessToken;
|
||||||
public final ArrayList<String> disabledFeatures;
|
public final ArrayList<String> disabledFeatures;
|
||||||
public final Boolean notifyRecordingIsOn;
|
public final Boolean notifyRecordingIsOn;
|
||||||
public final String uploadExternalDescription;
|
public final String presentationUploadExternalDescription;
|
||||||
public final String uploadExternalUrl;
|
public final String presentationUploadExternalUrl;
|
||||||
public final Long createTime;
|
public final Long createTime;
|
||||||
public final String createDate;
|
public final String createDate;
|
||||||
public final Map<String, String> metadata;
|
public final Map<String, String> metadata;
|
||||||
@ -38,8 +38,8 @@ public class CreateMeetingMessage {
|
|||||||
String viewerPass, String learningDashboardAccessToken,
|
String viewerPass, String learningDashboardAccessToken,
|
||||||
ArrayList<String> disabledFeatures,
|
ArrayList<String> disabledFeatures,
|
||||||
Boolean notifyRecordingIsOn,
|
Boolean notifyRecordingIsOn,
|
||||||
String uploadExternalDescription,
|
String presentationUploadExternalDescription,
|
||||||
String uploadExternalUrl,
|
String presentationUploadExternalUrl,
|
||||||
Long createTime, String createDate, Map<String, String> metadata) {
|
Long createTime, String createDate, Map<String, String> metadata) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.externalId = externalId;
|
this.externalId = externalId;
|
||||||
@ -58,8 +58,8 @@ public class CreateMeetingMessage {
|
|||||||
this.learningDashboardAccessToken = learningDashboardAccessToken;
|
this.learningDashboardAccessToken = learningDashboardAccessToken;
|
||||||
this.disabledFeatures = disabledFeatures;
|
this.disabledFeatures = disabledFeatures;
|
||||||
this.notifyRecordingIsOn = notifyRecordingIsOn;
|
this.notifyRecordingIsOn = notifyRecordingIsOn;
|
||||||
this.uploadExternalDescription = uploadExternalDescription;
|
this.presentationUploadExternalDescription = presentationUploadExternalDescription;
|
||||||
this.uploadExternalUrl = uploadExternalUrl;
|
this.presentationUploadExternalUrl = presentationUploadExternalUrl;
|
||||||
this.createTime = createTime;
|
this.createTime = createTime;
|
||||||
this.createDate = createDate;
|
this.createDate = createDate;
|
||||||
this.metadata = metadata;
|
this.metadata = metadata;
|
||||||
|
@ -18,7 +18,7 @@ public class ParamsUtil {
|
|||||||
public static final String INVALID_CHARS = ",";
|
public static final String INVALID_CHARS = ",";
|
||||||
|
|
||||||
public static String stripControlChars(String text) {
|
public static String stripControlChars(String text) {
|
||||||
return text.replaceAll("\\p{Cc}", "");
|
return text.replaceAll("\\p{Cc}", "").trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String escapeHTMLTags(String value) {
|
public static String escapeHTMLTags(String value) {
|
||||||
|
@ -43,8 +43,8 @@ public interface IBbbWebApiGWApp {
|
|||||||
ArrayList<Group> groups,
|
ArrayList<Group> groups,
|
||||||
ArrayList<String> disabledFeatures,
|
ArrayList<String> disabledFeatures,
|
||||||
Boolean notifyRecordingIsOn,
|
Boolean notifyRecordingIsOn,
|
||||||
String uploadExternalDescription,
|
String presentationUploadExternalDescription,
|
||||||
String uploadExternalUrl);
|
String presentationUploadExternalUrl);
|
||||||
|
|
||||||
void registerUser(String meetingID, String internalUserId, String fullname, String role,
|
void registerUser(String meetingID, String internalUserId, String fullname, String role,
|
||||||
String externUserID, String authToken, String avatarURL,
|
String externUserID, String authToken, String avatarURL,
|
||||||
|
@ -21,4 +21,5 @@ package org.bigbluebutton.presentation;
|
|||||||
|
|
||||||
public interface DocumentConversionService {
|
public interface DocumentConversionService {
|
||||||
void processDocument(UploadedPresentation pres);
|
void processDocument(UploadedPresentation pres);
|
||||||
|
void sendDocConversionFailedOnMimeType(UploadedPresentation pres, String fileMime, String fileExtension);
|
||||||
}
|
}
|
||||||
|
@ -19,15 +19,19 @@
|
|||||||
|
|
||||||
package org.bigbluebutton.presentation;
|
package org.bigbluebutton.presentation;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.bigbluebutton.api2.IBbbWebApiGWApp;
|
import org.bigbluebutton.api2.IBbbWebApiGWApp;
|
||||||
import org.bigbluebutton.presentation.imp.*;
|
import org.bigbluebutton.presentation.imp.*;
|
||||||
import org.bigbluebutton.presentation.messages.DocConversionRequestReceived;
|
import org.bigbluebutton.presentation.messages.DocConversionRequestReceived;
|
||||||
|
import org.bigbluebutton.presentation.messages.DocInvalidMimeType;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
|
|
||||||
|
import static org.bigbluebutton.presentation.Util.deleteDirectoryFromFileHandlingErrors;
|
||||||
|
|
||||||
public class DocumentConversionServiceImp implements DocumentConversionService {
|
public class DocumentConversionServiceImp implements DocumentConversionService {
|
||||||
private static Logger log = LoggerFactory.getLogger(DocumentConversionServiceImp.class);
|
private static Logger log = LoggerFactory.getLogger(DocumentConversionServiceImp.class);
|
||||||
|
|
||||||
@ -93,6 +97,9 @@ public class DocumentConversionServiceImp implements DocumentConversionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
File presentationFile = pres.getUploadedFile();
|
||||||
|
deleteDirectoryFromFileHandlingErrors(presentationFile);
|
||||||
|
|
||||||
Map<String, Object> logData = new HashMap<String, Object>();
|
Map<String, Object> logData = new HashMap<String, Object>();
|
||||||
logData = new HashMap<String, Object>();
|
logData = new HashMap<String, Object>();
|
||||||
logData.put("podId", pres.getPodId());
|
logData.put("podId", pres.getPodId());
|
||||||
@ -124,6 +131,11 @@ public class DocumentConversionServiceImp implements DocumentConversionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void sendDocConversionFailedOnMimeType(UploadedPresentation pres, String fileMime,
|
||||||
|
String fileExtension) {
|
||||||
|
notifier.sendInvalidMimeTypeMessage(pres, fileMime, fileExtension);
|
||||||
|
}
|
||||||
|
|
||||||
private void sendDocConversionRequestReceived(UploadedPresentation pres) {
|
private void sendDocConversionRequestReceived(UploadedPresentation pres) {
|
||||||
if (! pres.isConversionStarted()) {
|
if (! pres.isConversionStarted()) {
|
||||||
Map<String, Object> logData = new HashMap<String, Object>();
|
Map<String, Object> logData = new HashMap<String, Object>();
|
||||||
|
@ -39,5 +39,6 @@ public final class FileTypeConstants {
|
|||||||
public static final String JPG = "jpg";
|
public static final String JPG = "jpg";
|
||||||
public static final String JPEG = "jpeg";
|
public static final String JPEG = "jpeg";
|
||||||
public static final String PNG = "png";
|
public static final String PNG = "png";
|
||||||
|
public static final String SVG = "svg";
|
||||||
private FileTypeConstants() {} // Prevent instantiation
|
private FileTypeConstants() {} // Prevent instantiation
|
||||||
}
|
}
|
||||||
|
@ -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,15 +19,25 @@
|
|||||||
|
|
||||||
package org.bigbluebutton.presentation;
|
package org.bigbluebutton.presentation;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import static org.bigbluebutton.presentation.FileTypeConstants.*;
|
import static org.bigbluebutton.presentation.FileTypeConstants.*;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
||||||
@SuppressWarnings("serial")
|
@SuppressWarnings("serial")
|
||||||
public final class SupportedFileTypes {
|
public final class SupportedFileTypes {
|
||||||
|
|
||||||
|
private static Logger log = LoggerFactory.getLogger(SupportedFileTypes.class);
|
||||||
|
private static MimeTypeUtils mimeTypeUtils = new MimeTypeUtils();
|
||||||
|
|
||||||
private static final List<String> SUPPORTED_FILE_LIST = Collections.unmodifiableList(new ArrayList<String>(15) {
|
private static final List<String> SUPPORTED_FILE_LIST = Collections.unmodifiableList(new ArrayList<String>(15) {
|
||||||
{
|
{
|
||||||
// Add all the supported files
|
// Add all the supported files
|
||||||
@ -76,4 +86,56 @@ public final class SupportedFileTypes {
|
|||||||
public static boolean isImageFile(String fileExtension) {
|
public static boolean isImageFile(String fileExtension) {
|
||||||
return IMAGE_FILE_LIST.contains(fileExtension.toLowerCase());
|
return IMAGE_FILE_LIST.contains(fileExtension.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* It was tested native java methods to detect mimetypes, such as:
|
||||||
|
* - URLConnection.guessContentTypeFromStream(InputStream is);
|
||||||
|
* - Files.probeContentType(Path path);
|
||||||
|
* - FileNameMap fileNameMap.getContentTypeFor(String file.getName());
|
||||||
|
* - MimetypesFileTypeMap fileTypeMap.getContentType(File file);
|
||||||
|
* But none of them was as successful as the linux based command
|
||||||
|
*/
|
||||||
|
public static String detectMimeType(File pres) {
|
||||||
|
String mimeType = "";
|
||||||
|
if (pres != null && pres.isFile()){
|
||||||
|
try {
|
||||||
|
ProcessBuilder processBuilder = new ProcessBuilder();
|
||||||
|
processBuilder.command("bash", "-c", "file -b --mime-type " + pres.getAbsolutePath());
|
||||||
|
Process process = processBuilder.start();
|
||||||
|
StringBuilder output = new StringBuilder();
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
output.append(line + "\n");
|
||||||
|
}
|
||||||
|
int exitVal = process.waitFor();
|
||||||
|
if (exitVal == 0) {
|
||||||
|
mimeType = output.toString().trim();
|
||||||
|
} else {
|
||||||
|
log.error("Error while executing command {} for file {}, error: {}",
|
||||||
|
process.toString(), pres.getAbsolutePath(), process.getErrorStream());
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Could not read file [{}]", pres.getAbsolutePath(), e.getMessage());
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
log.error("Flow interrupted for file [{}]", pres.getAbsolutePath(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mimeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Boolean isPresentationMimeTypeValid(File pres, String fileExtension) {
|
||||||
|
String mimeType = detectMimeType(pres);
|
||||||
|
|
||||||
|
if(mimeType == null || mimeType == "") return false;
|
||||||
|
|
||||||
|
if(!mimeTypeUtils.getValidMimeTypes().contains(mimeType)) return false;
|
||||||
|
|
||||||
|
if(!mimeTypeUtils.extensionMatchMimeType(mimeType, fileExtension)) {
|
||||||
|
log.error("File with extension [{}] doesn't match with mimeType [{}].", fileExtension, mimeType);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,10 +19,15 @@
|
|||||||
|
|
||||||
package org.bigbluebutton.presentation;
|
package org.bigbluebutton.presentation;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
public final class Util {
|
public final class Util {
|
||||||
|
private static Logger log = LoggerFactory.getLogger(Util.class);
|
||||||
|
|
||||||
public static void deleteDirectory(File directory) {
|
public static void deleteDirectory(File directory) {
|
||||||
/**
|
/**
|
||||||
* Go through each directory and check if it's not empty.
|
* Go through each directory and check if it's not empty.
|
||||||
@ -40,4 +45,20 @@ public final class Util {
|
|||||||
// Now that the directory is empty. Delete it.
|
// Now that the directory is empty. Delete it.
|
||||||
directory.delete();
|
directory.delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void deleteDirectoryFromFileHandlingErrors(File presentationFile) {
|
||||||
|
if ( presentationFile != null ){
|
||||||
|
Path presDir = presentationFile.toPath().getParent();
|
||||||
|
try {
|
||||||
|
File presFileDir = new File(presDir.toString());
|
||||||
|
if (presFileDir.exists()) {
|
||||||
|
deleteDirectory(presFileDir);
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("Error while trying to delete directory {}", presDir.toString(), ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,8 @@ import org.slf4j.LoggerFactory;
|
|||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
|
|
||||||
|
import static org.bigbluebutton.presentation.Util.deleteDirectoryFromFileHandlingErrors;
|
||||||
|
|
||||||
public abstract class Office2PdfPageConverter {
|
public abstract class Office2PdfPageConverter {
|
||||||
private static Logger log = LoggerFactory.getLogger(Office2PdfPageConverter.class);
|
private static Logger log = LoggerFactory.getLogger(Office2PdfPageConverter.class);
|
||||||
|
|
||||||
@ -95,6 +97,7 @@ public abstract class Office2PdfPageConverter {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
deleteDirectoryFromFileHandlingErrors(presentationFile);
|
||||||
Map<String, Object> logData = new HashMap<>();
|
Map<String, Object> logData = new HashMap<>();
|
||||||
logData.put("meetingId", pres.getMeetingId());
|
logData.put("meetingId", pres.getMeetingId());
|
||||||
logData.put("presId", pres.getId());
|
logData.put("presId", pres.getId());
|
||||||
|
@ -50,6 +50,20 @@ public class SlidesGenerationProgressNotifier {
|
|||||||
maxUploadFileSize);
|
maxUploadFileSize);
|
||||||
messagingService.sendDocConversionMsg(progress);
|
messagingService.sendDocConversionMsg(progress);
|
||||||
}
|
}
|
||||||
|
public void sendInvalidMimeTypeMessage(UploadedPresentation pres, String fileMime, String fileExtension) {
|
||||||
|
DocInvalidMimeType invalidMimeType = new DocInvalidMimeType(
|
||||||
|
pres.getPodId(),
|
||||||
|
pres.getMeetingId(),
|
||||||
|
pres.getId(),
|
||||||
|
pres.getTemporaryPresentationId(),
|
||||||
|
pres.getName(),
|
||||||
|
pres.getAuthzToken(),
|
||||||
|
"IVALID_MIME_TYPE",
|
||||||
|
fileMime,
|
||||||
|
fileExtension
|
||||||
|
);
|
||||||
|
messagingService.sendDocConversionMsg(invalidMimeType);
|
||||||
|
}
|
||||||
public void sendUploadFileTimedout(UploadedPresentation pres, int page) {
|
public void sendUploadFileTimedout(UploadedPresentation pres, int page) {
|
||||||
UploadFileTimedoutMessage errorMessage = new UploadFileTimedoutMessage(
|
UploadFileTimedoutMessage errorMessage = new UploadFileTimedoutMessage(
|
||||||
pres.getPodId(),
|
pres.getPodId(),
|
||||||
|
@ -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],
|
groups: java.util.ArrayList[Group],
|
||||||
disabledFeatures: java.util.ArrayList[String],
|
disabledFeatures: java.util.ArrayList[String],
|
||||||
notifyRecordingIsOn: java.lang.Boolean,
|
notifyRecordingIsOn: java.lang.Boolean,
|
||||||
uploadExternalDescription: String,
|
presentationUploadExternalDescription: String,
|
||||||
uploadExternalUrl: String): Unit = {
|
presentationUploadExternalUrl: String): Unit = {
|
||||||
|
|
||||||
val disabledFeaturesAsVector: Vector[String] = disabledFeatures.asScala.toVector
|
val disabledFeaturesAsVector: Vector[String] = disabledFeatures.asScala.toVector
|
||||||
|
|
||||||
@ -164,8 +164,8 @@ class BbbWebApiGWApp(
|
|||||||
isBreakout = isBreakout.booleanValue(),
|
isBreakout = isBreakout.booleanValue(),
|
||||||
disabledFeaturesAsVector,
|
disabledFeaturesAsVector,
|
||||||
notifyRecordingIsOn,
|
notifyRecordingIsOn,
|
||||||
uploadExternalDescription,
|
presentationUploadExternalDescription,
|
||||||
uploadExternalUrl
|
presentationUploadExternalUrl
|
||||||
)
|
)
|
||||||
|
|
||||||
val durationProps = DurationProps(
|
val durationProps = DurationProps(
|
||||||
@ -346,6 +346,9 @@ class BbbWebApiGWApp(
|
|||||||
} else if (msg.isInstanceOf[UploadFileTimedoutMessage]) {
|
} else if (msg.isInstanceOf[UploadFileTimedoutMessage]) {
|
||||||
val event = MsgBuilder.buildPresentationUploadedFileTimedoutErrorSysMsg(msg.asInstanceOf[UploadFileTimedoutMessage])
|
val event = MsgBuilder.buildPresentationUploadedFileTimedoutErrorSysMsg(msg.asInstanceOf[UploadFileTimedoutMessage])
|
||||||
msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event))
|
msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event))
|
||||||
|
} else if (msg.isInstanceOf[DocInvalidMimeType]) {
|
||||||
|
val event = MsgBuilder.buildPresentationHasInvalidMimeType(msg.asInstanceOf[DocInvalidMimeType])
|
||||||
|
msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,6 +285,19 @@ object MsgBuilder {
|
|||||||
BbbCommonEnvCoreMsg(envelope, req)
|
BbbCommonEnvCoreMsg(envelope, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def buildPresentationHasInvalidMimeType(msg: DocInvalidMimeType): BbbCommonEnvCoreMsg = {
|
||||||
|
val routing = collection.immutable.HashMap("sender" -> "bbb-web")
|
||||||
|
val envelope = BbbCoreEnvelope(PresentationHasInvalidMimeTypeErrorSysPubMsg.NAME, routing)
|
||||||
|
val header = BbbClientMsgHeader(PresentationHasInvalidMimeTypeErrorSysPubMsg.NAME, msg.meetingId, "not-used")
|
||||||
|
|
||||||
|
val body = PresentationHasInvalidMimeTypeErrorSysPubMsgBody(podId = msg.podId, presentationName = msg.filename,
|
||||||
|
temporaryPresentationId = msg.temporaryPresentationId, presentationId = msg.presId, meetingId = msg.meetingId,
|
||||||
|
messageKey = msg.messageKey, fileMime = msg.fileMime, fileExtension = msg.fileExtension)
|
||||||
|
|
||||||
|
val req = PresentationHasInvalidMimeTypeErrorSysPubMsg(header, body)
|
||||||
|
BbbCommonEnvCoreMsg(envelope, req)
|
||||||
|
}
|
||||||
|
|
||||||
def buildPresentationUploadedFileTimedoutErrorSysMsg(msg: UploadFileTimedoutMessage): BbbCommonEnvCoreMsg = {
|
def buildPresentationUploadedFileTimedoutErrorSysMsg(msg: UploadFileTimedoutMessage): BbbCommonEnvCoreMsg = {
|
||||||
val routing = collection.immutable.HashMap("sender" -> "bbb-web")
|
val routing = collection.immutable.HashMap("sender" -> "bbb-web")
|
||||||
val envelope = BbbCoreEnvelope(PresentationUploadedFileTimeoutErrorSysPubMsg.NAME, routing)
|
val envelope = BbbCoreEnvelope(PresentationUploadedFileTimeoutErrorSysPubMsg.NAME, routing)
|
||||||
|
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 settings = require('./settings');
|
||||||
|
const LOCAL_SETTINGS_FILE_PATH = '/etc/bigbluebutton/bbb-export-annotations.json';
|
||||||
|
|
||||||
const config = settings;
|
const config = settings;
|
||||||
|
|
||||||
|
if (fs.existsSync(LOCAL_SETTINGS_FILE_PATH)) {
|
||||||
|
const local_config = JSON.parse(fs.readFileSync(LOCAL_SETTINGS_FILE_PATH));
|
||||||
|
_.mergeWith(config, local_config, (a, b) => (_.isArray(b) ? b : undefined));
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
"msgName": "NewPresAnnFileAvailableMsg"
|
"msgName": "NewPresAnnFileAvailableMsg"
|
||||||
},
|
},
|
||||||
"bbbWebAPI": "http://127.0.0.1:8090",
|
"bbbWebAPI": "http://127.0.0.1:8090",
|
||||||
|
"bbbWebPublicAPI": "/bigbluebutton/",
|
||||||
"bbbPadsAPI": "http://127.0.0.1:9002",
|
"bbbPadsAPI": "http://127.0.0.1:9002",
|
||||||
"redis": {
|
"redis": {
|
||||||
"host": "127.0.0.1",
|
"host": "127.0.0.1",
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
const config = require('../../config');
|
const config = require('../../config');
|
||||||
|
|
||||||
const { level } = config.log;
|
const {level} = config.log;
|
||||||
const trace = level.toLowerCase() === 'trace';
|
const trace = level.toLowerCase() === 'trace';
|
||||||
const debug = trace || level.toLowerCase() === 'debug';
|
const debug = trace || level.toLowerCase() === 'debug';
|
||||||
|
|
||||||
const date = () => new Date().toISOString();
|
const date = () => new Date().toISOString();
|
||||||
|
|
||||||
const parse = (messages) => {
|
const parse = (messages) => {
|
||||||
return messages.map(message => {
|
return messages.map((message) => {
|
||||||
if (typeof message === 'object') return JSON.stringify(message);
|
if (typeof message === 'object') return JSON.stringify(message);
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
|
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": {
|
"dependencies": {
|
||||||
"axios": "^0.26.0",
|
"axios": "^0.26.0",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"perfect-freehand": "^1.0.16",
|
"perfect-freehand": "^1.0.16",
|
||||||
"probe-image-size": "^7.2.3",
|
"probe-image-size": "^7.2.3",
|
||||||
"redis": "^4.0.3",
|
"redis": "^4.0.3",
|
||||||
@ -19,6 +20,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.20.0",
|
"eslint": "^8.20.0",
|
||||||
"eslint-config-google": "^0.14.0"
|
"eslint-config-google": "^0.14.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^16.16.0",
|
||||||
|
"npm": "^8.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/eslintrc": {
|
"node_modules/@eslint/eslintrc": {
|
||||||
@ -920,6 +925,11 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||||
|
},
|
||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
@ -2044,6 +2054,11 @@
|
|||||||
"type-check": "~0.4.0"
|
"type-check": "~0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||||
|
},
|
||||||
"lodash.merge": {
|
"lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.26.0",
|
"axios": "^0.26.0",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"perfect-freehand": "^1.0.16",
|
"perfect-freehand": "^1.0.16",
|
||||||
"probe-image-size": "^7.2.3",
|
"probe-image-size": "^7.2.3",
|
||||||
"redis": "^4.0.3",
|
"redis": "^4.0.3",
|
||||||
@ -18,5 +19,9 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.20.0",
|
"eslint": "^8.20.0",
|
||||||
"eslint-config-google": "^0.14.0"
|
"eslint-config-google": "^0.14.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^16.16.0",
|
||||||
|
"npm": "^8.5.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ const redis = require('redis');
|
|||||||
const sanitize = require('sanitize-filename');
|
const sanitize = require('sanitize-filename');
|
||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
const WorkerStarter = require('../lib/utils/worker-starter');
|
const WorkerStarter = require('../lib/utils/worker-starter');
|
||||||
|
const {PresAnnStatusMsg} = require('../lib/utils/message-builder');
|
||||||
const {workerData} = require('worker_threads');
|
const {workerData} = require('worker_threads');
|
||||||
const {promisify} = require('util');
|
const {promisify} = require('util');
|
||||||
|
|
||||||
@ -55,29 +56,7 @@ async function collectAnnotationsFromRedis() {
|
|||||||
const pdfFile = `${presFile}.pdf`;
|
const pdfFile = `${presFile}.pdf`;
|
||||||
|
|
||||||
// Message to display conversion progress toast
|
// Message to display conversion progress toast
|
||||||
const statusUpdate = {
|
const statusUpdate = new PresAnnStatusMsg(exportJob);
|
||||||
envelope: {
|
|
||||||
name: config.log.msgName,
|
|
||||||
routing: {
|
|
||||||
sender: exportJob.module,
|
|
||||||
},
|
|
||||||
timestamp: (new Date()).getTime(),
|
|
||||||
},
|
|
||||||
core: {
|
|
||||||
header: {
|
|
||||||
name: config.log.msgName,
|
|
||||||
meetingId: exportJob.parentMeetingId,
|
|
||||||
userId: '',
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
presId: exportJob.presId,
|
|
||||||
pageNumber: 1,
|
|
||||||
totalPages: pages.length,
|
|
||||||
status: 'COLLECTING',
|
|
||||||
error: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (fs.existsSync(pdfFile)) {
|
if (fs.existsSync(pdfFile)) {
|
||||||
for (const p of pages) {
|
for (const p of pages) {
|
||||||
@ -103,35 +82,32 @@ async function collectAnnotationsFromRedis() {
|
|||||||
try {
|
try {
|
||||||
cp.spawnSync(config.shared.pdftocairo, extract_png_from_pdf, {shell: false});
|
cp.spawnSync(config.shared.pdftocairo, extract_png_from_pdf, {shell: false});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const error_reason = `PDFtoCairo failed extracting slide ${pageNumber}`;
|
logger.error(`PDFtoCairo failed extracting slide ${pageNumber} in job ${jobId}: ${error.message}`);
|
||||||
logger.error(`${error_reason} in job ${jobId}: ${error.message}`);
|
statusUpdate.setError();
|
||||||
statusUpdate.core.body.status = error_reason;
|
|
||||||
statusUpdate.core.body.error = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
statusUpdate.core.body.pageNumber = pageNumber;
|
await client.publish(config.redis.channels.publish, statusUpdate.build(pageNumber));
|
||||||
statusUpdate.envelope.timestamp = (new Date()).getTime();
|
|
||||||
await client.publish(config.redis.channels.publish, JSON.stringify(statusUpdate));
|
|
||||||
statusUpdate.core.body.error = false;
|
|
||||||
}
|
}
|
||||||
// If PNG file already available
|
|
||||||
} else if (fs.existsSync(`${presFile}.png`)) {
|
|
||||||
fs.copyFileSync(`${presFile}.png`, path.join(dropbox, 'slide1.png'));
|
|
||||||
await client.publish(config.redis.channels.publish, JSON.stringify(statusUpdate));
|
|
||||||
// If JPEG file available
|
|
||||||
} else if (fs.existsSync(`${presFile}.jpeg`)) {
|
|
||||||
fs.copyFileSync(`${presFile}.jpeg`, path.join(dropbox, 'slide1.jpeg'));
|
|
||||||
await client.publish(config.redis.channels.publish, JSON.stringify(statusUpdate));
|
|
||||||
} else {
|
} else {
|
||||||
statusUpdate.core.body.error = true;
|
if (fs.existsSync(`${presFile}.png`)) {
|
||||||
await client.publish(config.redis.channels.publish, JSON.stringify(statusUpdate));
|
// PNG file available
|
||||||
client.disconnect();
|
fs.copyFileSync(`${presFile}.png`, path.join(dropbox, 'slide1.png'));
|
||||||
return logger.error(`Presentation file missing for job ${exportJob.jobId}`);
|
} else if (fs.existsSync(`${presFile}.jpeg`)) {
|
||||||
|
// JPEG file available
|
||||||
|
fs.copyFileSync(`${presFile}.jpeg`, path.join(dropbox, 'slide1.jpeg'));
|
||||||
|
await client.publish(config.redis.channels.publish, statusUpdate.build());
|
||||||
|
} else {
|
||||||
|
await client.publish(config.redis.channels.publish, statusUpdate.build());
|
||||||
|
client.disconnect();
|
||||||
|
return logger.error(`No PDF, PNG or JPEG file available for job ${jobId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.publish(config.redis.channels.publish, statusUpdate.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
client.disconnect();
|
client.disconnect();
|
||||||
|
|
||||||
const process = new WorkerStarter({jobId, statusUpdate});
|
const process = new WorkerStarter({jobId});
|
||||||
process.process();
|
process.process();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ const FormData = require('form-data');
|
|||||||
const redis = require('redis');
|
const redis = require('redis');
|
||||||
const axios = require('axios').default;
|
const axios = require('axios').default;
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const {NewPresAnnFileAvailableMsg} = require('../lib/utils/message-builder');
|
||||||
|
|
||||||
const {workerData} = require('worker_threads');
|
const {workerData} = require('worker_threads');
|
||||||
const [jobType, jobId, filename] = [workerData.jobType, workerData.jobId, workerData.filename];
|
const [jobType, jobId, filename] = [workerData.jobType, workerData.jobId, workerData.filename];
|
||||||
@ -27,34 +28,14 @@ async function notifyMeetingActor() {
|
|||||||
await client.connect();
|
await client.connect();
|
||||||
client.on('error', (err) => logger.info('Redis Client Error', err));
|
client.on('error', (err) => logger.info('Redis Client Error', err));
|
||||||
|
|
||||||
const link = path.join(`${path.sep}bigbluebutton`, 'presentation',
|
const link = config.bbbWebPublicAPI + path.join('presentation',
|
||||||
exportJob.parentMeetingId, exportJob.parentMeetingId,
|
exportJob.parentMeetingId, exportJob.parentMeetingId,
|
||||||
exportJob.presId, 'pdf', jobId, filename);
|
exportJob.presId, 'pdf', jobId, filename);
|
||||||
|
|
||||||
const notification = {
|
const notification = new NewPresAnnFileAvailableMsg(exportJob, link);
|
||||||
envelope: {
|
|
||||||
name: config.notifier.msgName,
|
|
||||||
routing: {
|
|
||||||
sender: exportJob.module,
|
|
||||||
},
|
|
||||||
timestamp: (new Date()).getTime(),
|
|
||||||
},
|
|
||||||
core: {
|
|
||||||
header: {
|
|
||||||
name: config.notifier.msgName,
|
|
||||||
meetingId: exportJob.parentMeetingId,
|
|
||||||
userId: '',
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
fileURI: link,
|
|
||||||
presId: exportJob.presId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.info(`Annotated PDF available at ${link}`);
|
logger.info(`Annotated PDF available at ${link}`);
|
||||||
await client.publish(config.redis.channels.publish,
|
await client.publish(config.redis.channels.publish, notification.build());
|
||||||
JSON.stringify(notification));
|
|
||||||
client.disconnect();
|
client.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,14 +10,16 @@ const sanitize = require('sanitize-filename');
|
|||||||
const {getStrokePoints, getStrokeOutlinePoints} = require('perfect-freehand');
|
const {getStrokePoints, getStrokeOutlinePoints} = require('perfect-freehand');
|
||||||
const probe = require('probe-image-size');
|
const probe = require('probe-image-size');
|
||||||
const redis = require('redis');
|
const redis = require('redis');
|
||||||
|
const {PresAnnStatusMsg} = require('../lib/utils/message-builder');
|
||||||
|
|
||||||
const [jobId, statusUpdate] = [workerData.jobId, workerData.statusUpdate];
|
const jobId = workerData.jobId;
|
||||||
|
|
||||||
const logger = new Logger('presAnn Process Worker');
|
const logger = new Logger('presAnn Process Worker');
|
||||||
logger.info('Processing PDF for job ' + jobId);
|
logger.info('Processing PDF for job ' + jobId);
|
||||||
statusUpdate.core.body.status = 'PROCESSING';
|
|
||||||
|
|
||||||
const dropbox = path.join(config.shared.presAnnDropboxDir, jobId);
|
const dropbox = path.join(config.shared.presAnnDropboxDir, jobId);
|
||||||
|
const job = fs.readFileSync(path.join(dropbox, 'job'));
|
||||||
|
const exportJob = JSON.parse(job);
|
||||||
|
const statusUpdate = new PresAnnStatusMsg(exportJob, PresAnnStatusMsg.EXPORT_STATUSES.PROCESSING);
|
||||||
|
|
||||||
// General utilities for rendering SVGs resembling Tldraw as much as possible
|
// General utilities for rendering SVGs resembling Tldraw as much as possible
|
||||||
function align_to_pango(alignment) {
|
function align_to_pango(alignment) {
|
||||||
@ -157,10 +159,8 @@ function render_textbox(textColor, font, fontSize, textAlign, text, id, textBoxW
|
|||||||
try {
|
try {
|
||||||
cp.spawnSync(config.shared.imagemagick, commands, {shell: false});
|
cp.spawnSync(config.shared.imagemagick, commands, {shell: false});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const error_reason = 'ImageMagick failed to render textbox';
|
logger.error(`ImageMagick failed to render textbox in job ${jobId}: ${error.message}`);
|
||||||
logger.error(`${error_reason} in job ${jobId}: ${error.message}`);
|
statusUpdate.setError();
|
||||||
statusUpdate.core.body.status = error_reason;
|
|
||||||
statusUpdate.core.body.error = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -596,7 +596,7 @@ function overlay_shape_label(svg, annotation) {
|
|||||||
const fontSize = text_size_to_px(annotation.style.size, annotation.style.scale);
|
const fontSize = text_size_to_px(annotation.style.size, annotation.style.scale);
|
||||||
const textAlign = 'center';
|
const textAlign = 'center';
|
||||||
const text = annotation.label;
|
const text = annotation.label;
|
||||||
const id = annotation.id;
|
const id = sanitize(annotation.id);
|
||||||
const rotation = rad_to_degree(annotation.rotation);
|
const rotation = rad_to_degree(annotation.rotation);
|
||||||
|
|
||||||
const [shape_width, shape_height] = annotation.size;
|
const [shape_width, shape_height] = annotation.size;
|
||||||
@ -641,7 +641,7 @@ function overlay_sticky(svg, annotation) {
|
|||||||
|
|
||||||
const textColor = '#0d0d0d'; // For sticky notes
|
const textColor = '#0d0d0d'; // For sticky notes
|
||||||
const text = annotation.text;
|
const text = annotation.text;
|
||||||
const id = annotation.id;
|
const id = sanitize(annotation.id);
|
||||||
|
|
||||||
render_textbox(textColor, font, fontSize, textAlign, text, id, textBoxWidth);
|
render_textbox(textColor, font, fontSize, textAlign, text, id, textBoxWidth);
|
||||||
|
|
||||||
@ -701,7 +701,7 @@ function overlay_text(svg, annotation) {
|
|||||||
const fontSize = text_size_to_px(annotation.style.size, annotation.style.scale);
|
const fontSize = text_size_to_px(annotation.style.size, annotation.style.scale);
|
||||||
const textAlign = align_to_pango(annotation.style.textAlign);
|
const textAlign = align_to_pango(annotation.style.textAlign);
|
||||||
const text = annotation.text;
|
const text = annotation.text;
|
||||||
const id = annotation.id;
|
const id = sanitize(annotation.id);
|
||||||
|
|
||||||
const rotation = rad_to_degree(annotation.rotation);
|
const rotation = rad_to_degree(annotation.rotation);
|
||||||
const [textBox_x, textBox_y] = annotation.point;
|
const [textBox_x, textBox_y] = annotation.point;
|
||||||
@ -787,17 +787,13 @@ async function process_presentation_annotations() {
|
|||||||
|
|
||||||
client.on('error', (err) => logger.info('Redis Client Error', err));
|
client.on('error', (err) => logger.info('Redis Client Error', err));
|
||||||
|
|
||||||
// 1. Get the job
|
// Get the annotations
|
||||||
const job = fs.readFileSync(path.join(dropbox, 'job'));
|
|
||||||
const exportJob = JSON.parse(job);
|
|
||||||
|
|
||||||
// 2. Get the annotations
|
|
||||||
const annotations = fs.readFileSync(path.join(dropbox, 'whiteboard'));
|
const annotations = fs.readFileSync(path.join(dropbox, 'whiteboard'));
|
||||||
const whiteboard = JSON.parse(annotations);
|
const whiteboard = JSON.parse(annotations);
|
||||||
const pages = JSON.parse(whiteboard.pages);
|
const pages = JSON.parse(whiteboard.pages);
|
||||||
const ghostScriptInput = [];
|
const ghostScriptInput = [];
|
||||||
|
|
||||||
// 3. Convert annotations to SVG
|
// Convert annotations to SVG
|
||||||
for (const currentSlide of pages) {
|
for (const currentSlide of pages) {
|
||||||
const bgImagePath = path.join(dropbox, `slide${currentSlide.page}`);
|
const bgImagePath = path.join(dropbox, `slide${currentSlide.page}`);
|
||||||
const svgBackgroundSlide = path.join(exportJob.presLocation,
|
const svgBackgroundSlide = path.join(exportJob.presLocation,
|
||||||
@ -854,14 +850,7 @@ async function process_presentation_annotations() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dimensions converted to a pixel size which,
|
// Scale slide back to its original size
|
||||||
// when converted to points, will yield the desired
|
|
||||||
// dimension in pixels when read without conversion
|
|
||||||
|
|
||||||
// e.g. say the background SVG dimensions are set to 1920x1080 pt
|
|
||||||
// Resize output to 2560x1440 px so that the SVG
|
|
||||||
// generates with the original size in pt.
|
|
||||||
|
|
||||||
const convertAnnotatedSlide = [
|
const convertAnnotatedSlide = [
|
||||||
SVGfile,
|
SVGfile,
|
||||||
'--output-width', to_px(slideWidth),
|
'--output-width', to_px(slideWidth),
|
||||||
@ -873,15 +862,11 @@ async function process_presentation_annotations() {
|
|||||||
cp.spawnSync(config.shared.cairosvg, convertAnnotatedSlide, {shell: false});
|
cp.spawnSync(config.shared.cairosvg, convertAnnotatedSlide, {shell: false});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Processing slide ${currentSlide.page} failed for job ${jobId}: ${error.message}`);
|
logger.error(`Processing slide ${currentSlide.page} failed for job ${jobId}: ${error.message}`);
|
||||||
statusUpdate.core.body.error = true;
|
statusUpdate.setError();
|
||||||
}
|
}
|
||||||
|
|
||||||
statusUpdate.core.body.pageNumber = currentSlide.page;
|
await client.publish(config.redis.channels.publish, statusUpdate.build(currentSlide.page));
|
||||||
statusUpdate.envelope.timestamp = (new Date()).getTime();
|
|
||||||
|
|
||||||
await client.publish(config.redis.channels.publish, JSON.stringify(statusUpdate));
|
|
||||||
ghostScriptInput.push(PDFfile);
|
ghostScriptInput.push(PDFfile);
|
||||||
statusUpdate.core.body.error = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create PDF output directory if it doesn't exist
|
// Create PDF output directory if it doesn't exist
|
||||||
@ -903,11 +888,7 @@ async function process_presentation_annotations() {
|
|||||||
try {
|
try {
|
||||||
cp.spawnSync(config.shared.ghostscript, mergePDFs, {shell: false});
|
cp.spawnSync(config.shared.ghostscript, mergePDFs, {shell: false});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const error_reason = 'GhostScript failed to merge PDFs';
|
return logger.error(`GhostScript failed to merge PDFs in job ${jobId}: ${error.message}`);
|
||||||
logger.error(`${error_reason} in job ${jobId}: ${error.message}`);
|
|
||||||
statusUpdate.core.body.status = error_reason;
|
|
||||||
statusUpdate.core.body.error = true;
|
|
||||||
await client.publish(config.redis.channels.publish, JSON.stringify(statusUpdate));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Launch Notifier Worker depending on job type
|
// Launch Notifier Worker depending on job type
|
||||||
|
@ -2,7 +2,7 @@ import org.bigbluebutton.build._
|
|||||||
|
|
||||||
description := "BigBlueButton custom FS-ESL client built on top of FS-ESL Java library."
|
description := "BigBlueButton custom FS-ESL client built on top of FS-ESL Java library."
|
||||||
|
|
||||||
version := "0.0.8-SNAPSHOT"
|
version := "0.0.9-SNAPSHOT"
|
||||||
|
|
||||||
val compileSettings = Seq(
|
val compileSettings = Seq(
|
||||||
organization := "org.bigbluebutton",
|
organization := "org.bigbluebutton",
|
||||||
@ -13,7 +13,7 @@ val compileSettings = Seq(
|
|||||||
"-Xlint",
|
"-Xlint",
|
||||||
"-Ywarn-dead-code",
|
"-Ywarn-dead-code",
|
||||||
"-language:_",
|
"-language:_",
|
||||||
"-target:jvm-1.11",
|
"-target:11",
|
||||||
"-encoding", "UTF-8"
|
"-encoding", "UTF-8"
|
||||||
),
|
),
|
||||||
javacOptions ++= List(
|
javacOptions ++= List(
|
||||||
@ -52,7 +52,7 @@ crossPaths := false
|
|||||||
// This forbids including Scala related libraries into the dependency
|
// This forbids including Scala related libraries into the dependency
|
||||||
autoScalaLibrary := false
|
autoScalaLibrary := false
|
||||||
|
|
||||||
scalaVersion := "2.13.4"
|
scalaVersion := "2.13.9"
|
||||||
|
|
||||||
publishTo := Some(Resolver.file("file", new File(Path.userHome.absolutePath + "/.m2/repository")))
|
publishTo := Some(Resolver.file("file", new File(Path.userHome.absolutePath + "/.m2/repository")))
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ object Dependencies {
|
|||||||
|
|
||||||
object Versions {
|
object Versions {
|
||||||
// Scala
|
// Scala
|
||||||
val scala = "2.13.4"
|
val scala = "2.13.9"
|
||||||
|
|
||||||
// Libraries
|
// Libraries
|
||||||
val netty = "3.2.10.Final"
|
val netty = "3.2.10.Final"
|
||||||
|
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": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.15.0",
|
|
||||||
"@emotion/react": "^11.10.5",
|
"@emotion/react": "^11.10.5",
|
||||||
"@emotion/styled": "^11.10.5",
|
"@emotion/styled": "^11.10.5",
|
||||||
"@mui/material": "^5.10.13",
|
"@mui/material": "^5.10.13",
|
||||||
"@mui/x-data-grid": "^5.17.10",
|
"@mui/x-data-grid": "^5.17.10",
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
|
||||||
"@testing-library/react": "^11.2.7",
|
|
||||||
"@testing-library/user-event": "^12.8.3",
|
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-intl": "^5.20.6",
|
"react-intl": "^5.20.6",
|
||||||
"typescript": "^4.3.5",
|
"react-scripts": "^5.0.0"
|
||||||
"web-vitals": "^1.1.2"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
@ -48,6 +43,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.15.0",
|
||||||
"autoprefixer": "^10.4.1",
|
"autoprefixer": "^10.4.1",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"eslint-config-airbnb": "^18.2.1",
|
"eslint-config-airbnb": "^18.2.1",
|
||||||
@ -57,7 +53,6 @@
|
|||||||
"eslint-plugin-react": "^7.24.0",
|
"eslint-plugin-react": "^7.24.0",
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
"eslint-plugin-react-hooks": "^4.2.0",
|
||||||
"postcss": "^8.4.5",
|
"postcss": "^8.4.5",
|
||||||
"react-scripts": "^5.0.0",
|
|
||||||
"tailwindcss": "^3.0.11"
|
"tailwindcss": "^3.0.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import Card from '@mui/material/Card';
|
||||||
|
import CardContent from '@mui/material/CardContent';
|
||||||
|
import TabUnstyled from '@mui/base/TabUnstyled';
|
||||||
|
import TabsListUnstyled from '@mui/base/TabsListUnstyled';
|
||||||
|
import TabPanelUnstyled from '@mui/base/TabPanelUnstyled';
|
||||||
|
import TabsUnstyled from '@mui/base/TabsUnstyled';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import './bbb-icons.css';
|
import './bbb-icons.css';
|
||||||
import {
|
import {
|
||||||
FormattedMessage, FormattedDate, injectIntl, FormattedTime,
|
FormattedMessage, FormattedDate, injectIntl, FormattedTime,
|
||||||
} from 'react-intl';
|
} from 'react-intl';
|
||||||
import { emojiConfigs } from './services/EmojiService';
|
import { emojiConfigs } from './services/EmojiService';
|
||||||
import Card from './components/Card';
|
import CardBody from './components/Card';
|
||||||
import UsersTable from './components/UsersTable';
|
import UsersTable from './components/UsersTable';
|
||||||
import UserDetails from './components/UserDetails/component';
|
import UserDetails from './components/UserDetails/component';
|
||||||
import { UserDetailsContext } from './components/UserDetails/context';
|
import { UserDetailsContext } from './components/UserDetails/context';
|
||||||
@ -14,6 +20,13 @@ import PollsTable from './components/PollsTable';
|
|||||||
import ErrorMessage from './components/ErrorMessage';
|
import ErrorMessage from './components/ErrorMessage';
|
||||||
import { makeUserCSVData, tsToHHmmss } from './services/UserService';
|
import { makeUserCSVData, tsToHHmmss } from './services/UserService';
|
||||||
|
|
||||||
|
const TABS = {
|
||||||
|
OVERVIEW: 0,
|
||||||
|
OVERVIEW_ACTIVITY_SCORE: 1,
|
||||||
|
TIMELINE: 2,
|
||||||
|
POLLING: 3,
|
||||||
|
};
|
||||||
|
|
||||||
class App extends React.Component {
|
class App extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -21,7 +34,7 @@ class App extends React.Component {
|
|||||||
loading: true,
|
loading: true,
|
||||||
invalidSessionCount: 0,
|
invalidSessionCount: 0,
|
||||||
activitiesJson: {},
|
activitiesJson: {},
|
||||||
tab: 'overview',
|
tab: 0,
|
||||||
meetingId: '',
|
meetingId: '',
|
||||||
learningDashboardAccessToken: '',
|
learningDashboardAccessToken: '',
|
||||||
ldAccessTokenCopied: false,
|
ldAccessTokenCopied: false,
|
||||||
@ -170,14 +183,13 @@ class App extends React.Component {
|
|||||||
invalidSessionCount: 0,
|
invalidSessionCount: 0,
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
});
|
});
|
||||||
document.title = `Learning Dashboard - ${json.name}`;
|
|
||||||
this.updateModalUser();
|
this.updateModalUser();
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
this.setState({ loading: false, invalidSessionCount: invalidSessionCount + 1 });
|
this.setState({ loading: false, invalidSessionCount: invalidSessionCount + 1 });
|
||||||
});
|
});
|
||||||
} else if (sessionToken !== '') {
|
} else if (sessionToken !== '') {
|
||||||
const url = new URL('/bigbluebutton/api/learningDashboard', window.location);
|
const url = new URL('/bigbluebutton/api/learningDashboard', window.location);
|
||||||
fetch(`${url}?sessionToken=${sessionToken}`)
|
fetch(`${url}?sessionToken=${sessionToken}`, { credentials: 'include' })
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((json) => {
|
.then((json) => {
|
||||||
if (json.response.returncode === 'SUCCESS') {
|
if (json.response.returncode === 'SUCCESS') {
|
||||||
@ -188,7 +200,6 @@ class App extends React.Component {
|
|||||||
invalidSessionCount: 0,
|
invalidSessionCount: 0,
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
});
|
});
|
||||||
document.title = `Learning Dashboard - ${jsonData.name}`;
|
|
||||||
this.updateModalUser();
|
this.updateModalUser();
|
||||||
} else {
|
} else {
|
||||||
// When meeting is ended the sessionToken stop working, check for new cookies
|
// When meeting is ended the sessionToken stop working, check for new cookies
|
||||||
@ -215,7 +226,7 @@ class App extends React.Component {
|
|||||||
} = this.state;
|
} = this.state;
|
||||||
const { intl } = this.props;
|
const { intl } = this.props;
|
||||||
|
|
||||||
document.title = `${intl.formatMessage({ id: 'app.learningDashboard.dashboardTitle', defaultMessage: 'Learning Dashboard' })} - ${activitiesJson.name}`;
|
document.title = `${intl.formatMessage({ id: 'app.learningDashboard.bigbluebuttonTitle', defaultMessage: 'BigBlueButton' })} - ${intl.formatMessage({ id: 'app.learningDashboard.dashboardTitle', defaultMessage: 'Learning Analytics Dashboard' })} - ${activitiesJson.name}`;
|
||||||
|
|
||||||
function totalOfEmojis() {
|
function totalOfEmojis() {
|
||||||
if (activitiesJson && activitiesJson.users) {
|
if (activitiesJson && activitiesJson.users) {
|
||||||
@ -311,6 +322,11 @@ class App extends React.Component {
|
|||||||
|
|
||||||
if (loading === false && getErrorMessage() !== '') return <ErrorMessage message={getErrorMessage()} />;
|
if (loading === false && getErrorMessage() !== '') return <ErrorMessage message={getErrorMessage()} />;
|
||||||
|
|
||||||
|
const usersCount = Object.values(activitiesJson.users || {})
|
||||||
|
.filter((u) => activitiesJson.endedOn > 0
|
||||||
|
|| Object.values(u.intIds)[Object.values(u.intIds).length - 1].leftOn === 0)
|
||||||
|
.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-10">
|
<div className="mx-10">
|
||||||
<div className="flex flex-col sm:flex-row items-start justify-between pb-3">
|
<div className="flex flex-col sm:flex-row items-start justify-between pb-3">
|
||||||
@ -366,147 +382,180 @@ class App extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
|
<TabsUnstyled
|
||||||
<div aria-hidden="true" className="cursor-pointer" onClick={() => { this.setState({ tab: 'overview' }); }}>
|
defaultValue={0}
|
||||||
<Card
|
onChange={(e, v) => {
|
||||||
name={
|
this.setState({ tab: v });
|
||||||
activitiesJson.endedOn === 0
|
}}
|
||||||
? intl.formatMessage({ id: 'app.learningDashboard.indicators.usersOnline', defaultMessage: 'Active Users' })
|
>
|
||||||
: intl.formatMessage({ id: 'app.learningDashboard.indicators.usersTotal', defaultMessage: 'Total Number Of Users' })
|
<TabsListUnstyled className="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
|
||||||
}
|
<TabUnstyled className="rounded focus:outline-none focus:ring focus:ring-pink-500 ring-offset-2">
|
||||||
number={Object
|
<Card>
|
||||||
.values(activitiesJson.users || {})
|
<CardContent classes={{ root: '!p-0' }}>
|
||||||
.filter((u) => activitiesJson.endedOn > 0
|
<CardBody
|
||||||
|| Object.values(u.intIds)[Object.values(u.intIds).length - 1].leftOn === 0)
|
name={
|
||||||
.length}
|
activitiesJson.endedOn === 0
|
||||||
cardClass={tab === 'overview' ? 'border-pink-500' : 'hover:border-pink-500 border-white'}
|
? intl.formatMessage({ id: 'app.learningDashboard.indicators.usersOnline', defaultMessage: 'Active Users' })
|
||||||
iconClass="bg-pink-50 text-pink-500"
|
: intl.formatMessage({ id: 'app.learningDashboard.indicators.usersTotal', defaultMessage: 'Total Number Of Users' })
|
||||||
onClick={() => {
|
}
|
||||||
this.setState({ tab: 'overview' });
|
number={usersCount}
|
||||||
}}
|
cardClass={tab === TABS.OVERVIEW ? 'border-pink-500' : 'hover:border-pink-500 border-white'}
|
||||||
>
|
iconClass="bg-pink-50 text-pink-500"
|
||||||
<svg
|
>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<svg
|
||||||
className="h-6 w-6"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
className="h-6 w-6"
|
||||||
viewBox="0 0 24 24"
|
fill="none"
|
||||||
stroke="currentColor"
|
viewBox="0 0 24 24"
|
||||||
>
|
stroke="currentColor"
|
||||||
<path
|
>
|
||||||
strokeLinecap="round"
|
<path
|
||||||
strokeLinejoin="round"
|
strokeLinecap="round"
|
||||||
strokeWidth="2"
|
strokeLinejoin="round"
|
||||||
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
|
strokeWidth="2"
|
||||||
/>
|
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
|
||||||
</svg>
|
/>
|
||||||
</Card>
|
</svg>
|
||||||
</div>
|
</CardBody>
|
||||||
<div aria-hidden="true" className="cursor-pointer" onClick={() => { this.setState({ tab: 'overview_activityscore' }); }}>
|
</CardContent>
|
||||||
<Card
|
</Card>
|
||||||
name={intl.formatMessage({ id: 'app.learningDashboard.indicators.activityScore', defaultMessage: 'Activity Score' })}
|
</TabUnstyled>
|
||||||
number={intl.formatNumber((getAverageActivityScore() || 0), {
|
<TabUnstyled className="rounded focus:outline-none focus:ring focus:ring-green-500 ring-offset-2">
|
||||||
minimumFractionDigits: 0,
|
<Card>
|
||||||
maximumFractionDigits: 1,
|
<CardContent classes={{ root: '!p-0' }}>
|
||||||
})}
|
<CardBody
|
||||||
cardClass={tab === 'overview_activityscore' ? 'border-green-500' : 'hover:border-green-500 border-white'}
|
name={intl.formatMessage({ id: 'app.learningDashboard.indicators.activityScore', defaultMessage: 'Activity Score' })}
|
||||||
iconClass="bg-green-200 text-green-500"
|
number={intl.formatNumber((getAverageActivityScore() || 0), {
|
||||||
>
|
minimumFractionDigits: 0,
|
||||||
<svg
|
maximumFractionDigits: 1,
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
})}
|
||||||
className="h-6 w-6"
|
cardClass={tab === TABS.OVERVIEW_ACTIVITY_SCORE ? 'border-green-500' : 'hover:border-green-500 border-white'}
|
||||||
fill="none"
|
iconClass="bg-green-200 text-green-500"
|
||||||
viewBox="0 0 24 24"
|
>
|
||||||
stroke="currentColor"
|
<svg
|
||||||
>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<path
|
className="h-6 w-6"
|
||||||
strokeLinecap="round"
|
fill="none"
|
||||||
strokeLinejoin="round"
|
viewBox="0 0 24 24"
|
||||||
strokeWidth="2"
|
stroke="currentColor"
|
||||||
d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z"
|
>
|
||||||
/>
|
<path
|
||||||
<path
|
strokeLinecap="round"
|
||||||
strokeLinecap="round"
|
strokeLinejoin="round"
|
||||||
strokeLinejoin="round"
|
strokeWidth="2"
|
||||||
strokeWidth="2"
|
d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z"
|
||||||
d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z"
|
/>
|
||||||
/>
|
<path
|
||||||
</svg>
|
strokeLinecap="round"
|
||||||
</Card>
|
strokeLinejoin="round"
|
||||||
</div>
|
strokeWidth="2"
|
||||||
<div aria-hidden="true" className="cursor-pointer" onClick={() => { this.setState({ tab: 'status_timeline' }); }}>
|
d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z"
|
||||||
<Card
|
/>
|
||||||
name={intl.formatMessage({ id: 'app.learningDashboard.indicators.timeline', defaultMessage: 'Timeline' })}
|
</svg>
|
||||||
number={totalOfEmojis()}
|
</CardBody>
|
||||||
cardClass={tab === 'status_timeline' ? 'border-purple-500' : 'hover:border-purple-500 border-white'}
|
</CardContent>
|
||||||
iconClass="bg-purple-200 text-purple-500"
|
</Card>
|
||||||
>
|
</TabUnstyled>
|
||||||
{this.fetchMostUsedEmojis()}
|
<TabUnstyled className="rounded focus:outline-none focus:ring focus:ring-purple-500 ring-offset-2">
|
||||||
</Card>
|
<Card>
|
||||||
</div>
|
<CardContent classes={{ root: '!p-0' }}>
|
||||||
<div aria-hidden="true" className="cursor-pointer" onClick={() => { this.setState({ tab: 'polling' }); }}>
|
<CardBody
|
||||||
<Card
|
name={intl.formatMessage({ id: 'app.learningDashboard.indicators.timeline', defaultMessage: 'Timeline' })}
|
||||||
name={intl.formatMessage({ id: 'app.learningDashboard.indicators.polls', defaultMessage: 'Polls' })}
|
number={totalOfEmojis()}
|
||||||
number={Object.values(activitiesJson.polls || {}).length}
|
cardClass={tab === TABS.TIMELINE ? 'border-purple-500' : 'hover:border-purple-500 border-white'}
|
||||||
cardClass={tab === 'polling' ? 'border-blue-500' : 'hover:border-blue-500 border-white'}
|
iconClass="bg-purple-200 text-purple-500"
|
||||||
iconClass="bg-blue-100 text-blue-500"
|
>
|
||||||
>
|
{this.fetchMostUsedEmojis()}
|
||||||
<svg
|
</CardBody>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
</CardContent>
|
||||||
className="h-6 w-6"
|
</Card>
|
||||||
fill="none"
|
</TabUnstyled>
|
||||||
viewBox="0 0 24 24"
|
<TabUnstyled className="rounded focus:outline-none focus:ring focus:ring-blue-500 ring-offset-2">
|
||||||
stroke="currentColor"
|
<Card>
|
||||||
>
|
<CardContent classes={{ root: '!p-0' }}>
|
||||||
<path
|
<CardBody
|
||||||
strokeLinecap="round"
|
name={intl.formatMessage({ id: 'app.learningDashboard.indicators.polls', defaultMessage: 'Polls' })}
|
||||||
strokeLinejoin="round"
|
number={Object.values(activitiesJson.polls || {}).length}
|
||||||
strokeWidth="2"
|
cardClass={tab === TABS.POLLING ? 'border-blue-500' : 'hover:border-blue-500 border-white'}
|
||||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"
|
iconClass="bg-blue-100 text-blue-500"
|
||||||
/>
|
>
|
||||||
</svg>
|
<svg
|
||||||
</Card>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</div>
|
className="h-6 w-6"
|
||||||
</div>
|
fill="none"
|
||||||
<h1 className="block my-2 pr-2 text-xl font-semibold">
|
viewBox="0 0 24 24"
|
||||||
{ tab === 'overview' || tab === 'overview_activityscore'
|
stroke="currentColor"
|
||||||
? <FormattedMessage id="app.learningDashboard.usersTable.title" defaultMessage="Overview" />
|
>
|
||||||
: null }
|
<path
|
||||||
{ tab === 'status_timeline'
|
strokeLinecap="round"
|
||||||
? <FormattedMessage id="app.learningDashboard.statusTimelineTable.title" defaultMessage="Timeline" />
|
strokeLinejoin="round"
|
||||||
: null }
|
strokeWidth="2"
|
||||||
{ tab === 'polling'
|
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"
|
||||||
? <FormattedMessage id="app.learningDashboard.pollsTable.title" defaultMessage="Polls" />
|
/>
|
||||||
: null }
|
</svg>
|
||||||
</h1>
|
</CardBody>
|
||||||
<div className="w-full overflow-hidden rounded-md shadow-xs border-2 border-gray-100">
|
</CardContent>
|
||||||
<div className="w-full overflow-x-auto">
|
</Card>
|
||||||
{ (tab === 'overview' || tab === 'overview_activityscore')
|
</TabUnstyled>
|
||||||
? (
|
</TabsListUnstyled>
|
||||||
|
<TabPanelUnstyled value={0}>
|
||||||
|
<h2 className="block my-2 pr-2 text-xl font-semibold">
|
||||||
|
<FormattedMessage id="app.learningDashboard.usersTable.title" defaultMessage="Overview" />
|
||||||
|
</h2>
|
||||||
|
<div className="w-full overflow-hidden rounded-md shadow-xs border-2 border-gray-100">
|
||||||
|
<div className="w-full overflow-x-auto">
|
||||||
<UsersTable
|
<UsersTable
|
||||||
allUsers={activitiesJson.users}
|
allUsers={activitiesJson.users}
|
||||||
totalOfActivityTime={totalOfActivity()}
|
totalOfActivityTime={totalOfActivity()}
|
||||||
totalOfPolls={Object.values(activitiesJson.polls || {}).length}
|
totalOfPolls={Object.values(activitiesJson.polls || {}).length}
|
||||||
tab={tab}
|
tab="overview"
|
||||||
/>
|
/>
|
||||||
)
|
</div>
|
||||||
: null }
|
</div>
|
||||||
{ (tab === 'status_timeline')
|
</TabPanelUnstyled>
|
||||||
? (
|
<TabPanelUnstyled value={1}>
|
||||||
|
<h2 className="block my-2 pr-2 text-xl font-semibold">
|
||||||
|
<FormattedMessage id="app.learningDashboard.usersTable.title" defaultMessage="Overview" />
|
||||||
|
</h2>
|
||||||
|
<div className="w-full overflow-hidden rounded-md shadow-xs border-2 border-gray-100">
|
||||||
|
<div className="w-full overflow-x-auto">
|
||||||
|
<UsersTable
|
||||||
|
allUsers={activitiesJson.users}
|
||||||
|
totalOfActivityTime={totalOfActivity()}
|
||||||
|
totalOfPolls={Object.values(activitiesJson.polls || {}).length}
|
||||||
|
tab="overview_activityscore"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabPanelUnstyled>
|
||||||
|
<TabPanelUnstyled value={2}>
|
||||||
|
<h2 className="block my-2 pr-2 text-xl font-semibold">
|
||||||
|
<FormattedMessage id="app.learningDashboard.statusTimelineTable.title" defaultMessage="Timeline" />
|
||||||
|
</h2>
|
||||||
|
<div className="w-full overflow-hidden rounded-md shadow-xs border-2 border-gray-100">
|
||||||
|
<div className="w-full overflow-x-auto">
|
||||||
<StatusTable
|
<StatusTable
|
||||||
allUsers={activitiesJson.users}
|
allUsers={activitiesJson.users}
|
||||||
slides={activitiesJson.presentationSlides}
|
slides={activitiesJson.presentationSlides}
|
||||||
meetingId={activitiesJson.intId}
|
meetingId={activitiesJson.intId}
|
||||||
/>
|
/>
|
||||||
)
|
</div>
|
||||||
: null }
|
</div>
|
||||||
{ tab === 'polling'
|
</TabPanelUnstyled>
|
||||||
? <PollsTable polls={activitiesJson.polls} allUsers={activitiesJson.users} />
|
<TabPanelUnstyled value={3}>
|
||||||
: null }
|
<h2 className="block my-2 pr-2 text-xl font-semibold">
|
||||||
<UserDetails dataJson={activitiesJson} />
|
<FormattedMessage id="app.learningDashboard.pollsTable.title" defaultMessage="Polls" />
|
||||||
</div>
|
</h2>
|
||||||
</div>
|
<div className="w-full overflow-hidden rounded-md shadow-xs border-2 border-gray-100">
|
||||||
|
<div className="w-full overflow-x-auto">
|
||||||
|
<PollsTable polls={activitiesJson.polls} allUsers={activitiesJson.users} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabPanelUnstyled>
|
||||||
|
</TabsUnstyled>
|
||||||
|
<UserDetails dataJson={activitiesJson} />
|
||||||
<hr className="my-8" />
|
<hr className="my-8" />
|
||||||
<div className="flex justify-between pb-8 text-xs text-gray-700 dark:text-gray-400 whitespace-nowrap flex-col sm:flex-row">
|
<div className="flex justify-between pb-8 text-xs text-gray-800 dark:text-gray-400 whitespace-nowrap flex-col sm:flex-row">
|
||||||
<div className="flex flex-col justify-center mb-4 sm:mb-0">
|
<div className="flex flex-col justify-center mb-4 sm:mb-0">
|
||||||
<p>
|
<p>
|
||||||
{
|
{
|
||||||
|
@ -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}`
|
+ ` ${cardClass}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="w-70">
|
<div className="w-70 text-left rtl:text-right">
|
||||||
<p className="text-lg font-semibold text-gray-700">
|
<p className="text-lg font-semibold text-gray-700">
|
||||||
{ number }
|
{ number }
|
||||||
</p>
|
</p>
|
||||||
|
@ -12,9 +12,9 @@ function ErrorMessage(props) {
|
|||||||
clipRule="evenodd"
|
clipRule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<h1 className="text-xl font-semibold text-gray-700 dark:text-gray-200">
|
<h3 className="text-xl font-semibold text-gray-700 dark:text-gray-200">
|
||||||
{message}
|
{message}
|
||||||
</h1>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -375,7 +375,7 @@ const PollsTable = (props) => {
|
|||||||
sx={{
|
sx={{
|
||||||
'& .MuiDataGrid-columnHeaders': {
|
'& .MuiDataGrid-columnHeaders': {
|
||||||
backgroundColor: 'rgb(243 244 246/var(--tw-bg-opacity))',
|
backgroundColor: 'rgb(243 244 246/var(--tw-bg-opacity))',
|
||||||
color: 'rgb(107 114 128/1)',
|
color: 'rgb(55 65 81/1)',
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
letterSpacing: '.025em',
|
letterSpacing: '.025em',
|
||||||
minHeight: '40.5px !important',
|
minHeight: '40.5px !important',
|
||||||
|
@ -1,8 +1,27 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
||||||
import { emojiConfigs, filterUserEmojis } from '../services/EmojiService';
|
import { emojiConfigs, filterUserEmojis } from '../services/EmojiService';
|
||||||
import UserAvatar from './UserAvatar';
|
import UserAvatar from './UserAvatar';
|
||||||
|
|
||||||
|
const intlMessages = defineMessages({
|
||||||
|
thumbnail: {
|
||||||
|
id: 'app.learningDashboard.statusTimelineTable.thumbnail',
|
||||||
|
defaultMessage: 'Presentation thumbnail',
|
||||||
|
},
|
||||||
|
presentation: {
|
||||||
|
id: 'app.learningDashboard.statusTimelineTable.presentation',
|
||||||
|
defaultMessage: 'Presentation',
|
||||||
|
},
|
||||||
|
pageNumber: {
|
||||||
|
id: 'app.learningDashboard.statusTimelineTable.pageNumber',
|
||||||
|
defaultMessage: 'Page',
|
||||||
|
},
|
||||||
|
setAt: {
|
||||||
|
id: 'app.learningDashboard.statusTimelineTable.setAt',
|
||||||
|
defaultMessage: 'Set at',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
class StatusTable extends React.Component {
|
class StatusTable extends React.Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
// This code is needed to prevent emojis from overflowing.
|
// This code is needed to prevent emojis from overflowing.
|
||||||
@ -162,7 +181,7 @@ class StatusTable extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="text-xs font-semibold tracking-wide text-gray-500 uppercase border-b bg-gray-100">
|
<tr className="text-xs font-semibold tracking-wide text-gray-700 uppercase border-b bg-gray-100">
|
||||||
<th className={`z-30 bg-inherit px-4 py-3 col-text-left sticky ${isRTL ? 'right-0' : 'left-0'}`}>
|
<th className={`z-30 bg-inherit px-4 py-3 col-text-left sticky ${isRTL ? 'right-0' : 'left-0'}`}>
|
||||||
<FormattedMessage id="app.learningDashboard.user" defaultMessage="User" />
|
<FormattedMessage id="app.learningDashboard.user" defaultMessage="User" />
|
||||||
</th>
|
</th>
|
||||||
@ -190,7 +209,7 @@ class StatusTable extends React.Component {
|
|||||||
const { slide, start, end } = period;
|
const { slide, start, end } = period;
|
||||||
const padding = isRTL ? 'paddingLeft' : 'paddingRight';
|
const padding = isRTL ? 'paddingLeft' : 'paddingRight';
|
||||||
const URLPrefix = `/bigbluebutton/presentation/${meetingId}/${meetingId}`;
|
const URLPrefix = `/bigbluebutton/presentation/${meetingId}/${meetingId}`;
|
||||||
const { presentationId, pageNum } = slide || {};
|
const { presentationId, pageNum, presentationName } = slide || {};
|
||||||
return (
|
return (
|
||||||
<td
|
<td
|
||||||
style={{
|
style={{
|
||||||
@ -199,29 +218,28 @@ class StatusTable extends React.Component {
|
|||||||
>
|
>
|
||||||
{ slide && (
|
{ slide && (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div
|
<div className="my-4">
|
||||||
className="my-4"
|
|
||||||
aria-label={tsToHHmmss(start - periods[0].start)}
|
|
||||||
>
|
|
||||||
<a
|
<a
|
||||||
href={`${URLPrefix}/${presentationId}/svg/${pageNum}`}
|
href={`${URLPrefix}/${presentationId}/svg/${pageNum}`}
|
||||||
className="block border-2 border-gray-300"
|
className="block border-2 border-gray-300"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
|
aria-describedby={`thumb-desc-${presentationId}`}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={`${URLPrefix}/${presentationId}/thumbnail/${pageNum}`}
|
src={`${URLPrefix}/${presentationId}/thumbnail/${pageNum}`}
|
||||||
alt={intl.formatMessage({
|
alt={`${intl.formatMessage(intlMessages.thumbnail)} - ${intl.formatMessage(intlMessages.presentation)} ${presentationName} - ${intl.formatMessage(intlMessages.pageNumber)} ${pageNum}`}
|
||||||
id: 'app.learningDashboard.statusTimelineTable.thumbnail',
|
|
||||||
defaultMessage: 'Presentation thumbnail',
|
|
||||||
})}
|
|
||||||
style={{
|
style={{
|
||||||
maxWidth: '150px',
|
maxWidth: '150px',
|
||||||
width: '150px',
|
width: '150px',
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
|
whiteSpace: 'pre-line',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
<p id={`thumb-desc-${presentationId}`} className="absolute w-0 h-0 p-0 border-0 m-0 overflow-hidden">
|
||||||
|
{`${intl.formatMessage(intlMessages.thumbnail)} - ${intl.formatMessage(intlMessages.presentation)} ${presentationName} - ${intl.formatMessage(intlMessages.pageNumber)} ${pageNum} - ${intl.formatMessage(intlMessages.setAt)} ${start}`}
|
||||||
|
</p>
|
||||||
<div className="text-xs text-center mt-1 text-gray-500">{tsToHHmmss(slide.setOn - periods[0].start)}</div>
|
<div className="text-xs text-center mt-1 text-gray-500">{tsToHHmmss(slide.setOn - periods[0].start)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -347,17 +347,25 @@ const UserDatailsComponent = (props) => {
|
|||||||
<div className="h-6 relative before:bg-gray-500 before:absolute before:w-[10px] before:h-[10px] before:rounded-full before:left-0 before:top-[calc(50%-5px)] after:bg-gray-500 after:absolute after:w-[10px] after:h-[10px] after:rounded-full after:right-0 after:top-[calc(50%-5px)]">
|
<div className="h-6 relative before:bg-gray-500 before:absolute before:w-[10px] before:h-[10px] before:rounded-full before:left-0 before:top-[calc(50%-5px)] after:bg-gray-500 after:absolute after:w-[10px] after:h-[10px] after:rounded-full after:right-0 after:top-[calc(50%-5px)]">
|
||||||
<div className="bg-gray-500 [--line-height:2px] h-[var(--line-height)] absolute top-[calc(50%-var(--line-height)/2)] left-[10px] right-[10px] rounded-2xl" />
|
<div className="bg-gray-500 [--line-height:2px] h-[var(--line-height)] absolute top-[calc(50%-var(--line-height)/2)] left-[10px] right-[10px] rounded-2xl" />
|
||||||
<div
|
<div
|
||||||
|
role="progressbar"
|
||||||
className="ltr:bg-gradient-to-br rtl:bg-gradient-to-bl from-green-100 to-green-600 absolute h-full rounded-2xl text-right rtl:text-left text-ellipsis overflow-hidden"
|
className="ltr:bg-gradient-to-br rtl:bg-gradient-to-bl from-green-100 to-green-600 absolute h-full rounded-2xl text-right rtl:text-left text-ellipsis overflow-hidden"
|
||||||
style={{
|
style={{
|
||||||
right: `calc(${document.dir === 'ltr' ? userEndOffsetTime : userStartOffsetTime}% + 10px)`,
|
right: `calc(${document.dir === 'ltr' ? userEndOffsetTime : userStartOffsetTime}% + 10px)`,
|
||||||
left: `calc(${document.dir === 'ltr' ? userStartOffsetTime : userEndOffsetTime}% + 10px)`,
|
left: `calc(${document.dir === 'ltr' ? userStartOffsetTime : userEndOffsetTime}% + 10px)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="mx-3 inline-block text-white">
|
<div
|
||||||
|
aria-describedby={`online-indicator-desc-${user.userKey}`}
|
||||||
|
aria-label={intl.formatMessage({ id: 'app.learningDashboard.usersTable.colOnline', defaultMessage: 'Online time' })}
|
||||||
|
className="mx-3 inline-block text-white"
|
||||||
|
>
|
||||||
{ new Date(getSumOfTime(Object.values(user.intIds)))
|
{ new Date(getSumOfTime(Object.values(user.intIds)))
|
||||||
.toISOString()
|
.toISOString()
|
||||||
.substring(11, 19) }
|
.substring(11, 19) }
|
||||||
</div>
|
</div>
|
||||||
|
<p id={`online-indicator-desc-${user.userKey}`} className="absolute w-0 h-0 p-0 border-0 m-0 overflow-hidden">
|
||||||
|
{`${intl.formatMessage({ id: 'app.learningDashboard.userDetails.onlineIndicator', defaultMessage: '{0} online time' }, { 0: user.name })} ${new Date(getSumOfTime(Object.values(user.intIds))).toISOString().substring(11, 19)}`}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row justify-between font-light text-gray-700">
|
<div className="flex flex-row justify-between font-light text-gray-700">
|
||||||
@ -414,7 +422,7 @@ const UserDatailsComponent = (props) => {
|
|||||||
<>
|
<>
|
||||||
<div className="bg-white shadow rounded mb-4 table w-full">
|
<div className="bg-white shadow rounded mb-4 table w-full">
|
||||||
<div className="p-6 text-lg flex items-center">
|
<div className="p-6 text-lg flex items-center">
|
||||||
<div className="p-2 rounded-full bg-green-200 text-green-500">
|
<div className="p-2 rounded-full bg-green-100 text-green-700">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z" />
|
||||||
@ -465,7 +473,7 @@ const UserDatailsComponent = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="bg-white shadow rounded">
|
<div className="bg-white shadow rounded">
|
||||||
<div className="p-6 text-lg flex items-center">
|
<div className="p-6 text-lg flex items-center">
|
||||||
<div className="p-2 rounded-full bg-blue-100 text-blue-500">
|
<div className="p-2 rounded-full bg-blue-100 text-blue-700">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -165,7 +165,7 @@ class UsersTable extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b bg-gray-100">
|
<tr className="text-xs font-semibold tracking-wide text-left text-gray-700 uppercase border-b bg-gray-100">
|
||||||
<th
|
<th
|
||||||
className={`px-3.5 2xl:px-4 py-3 col-text-left ${tab === 'overview' ? 'cursor-pointer' : ''}`}
|
className={`px-3.5 2xl:px-4 py-3 col-text-left ${tab === 'overview' ? 'cursor-pointer' : ''}`}
|
||||||
onClick={() => { if (tab === 'overview') this.toggleOrder('userOrder'); }}
|
onClick={() => { if (tab === 'overview') this.toggleOrder('userOrder'); }}
|
||||||
|
@ -3,7 +3,6 @@ import ReactDOM from 'react-dom';
|
|||||||
import './index.css';
|
import './index.css';
|
||||||
import { IntlProvider } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import reportWebVitals from './reportWebVitals';
|
|
||||||
import { UserDetailsProvider } from './components/UserDetails/context';
|
import { UserDetailsProvider } from './components/UserDetails/context';
|
||||||
|
|
||||||
const RTL_LANGUAGES = ['ar', 'dv', 'fa', 'he'];
|
const RTL_LANGUAGES = ['ar', 'dv', 'fa', 'he'];
|
||||||
@ -83,5 +82,3 @@ class Dashboard extends React.Component {
|
|||||||
|
|
||||||
const rootElement = document.getElementById('root');
|
const rootElement = document.getElementById('root');
|
||||||
ReactDOM.render(<Dashboard />, rootElement);
|
ReactDOM.render(<Dashboard />, rootElement);
|
||||||
|
|
||||||
reportWebVitals();
|
|
||||||
|
@ -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"
|
echo "# is not owned by $BBB_USER"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "$HTML5_CONFIG" ]; then
|
|
||||||
SVG_IMAGES_REQUIRED=$(cat $BBB_WEB_CONFIG | grep -v '#' | sed -n '/^svgImagesRequired/{s/.*=//;p}')
|
|
||||||
if [ "$SVG_IMAGES_REQUIRED" != "true" ]; then
|
|
||||||
echo
|
|
||||||
echo "# Warning: You have the HTML5 client installed but in"
|
|
||||||
echo "#"
|
|
||||||
echo "# $BBB_WEB_CONFIG"
|
|
||||||
echo "#"
|
|
||||||
echo "# the setting for svgImagesRequired is false. To fix, run the commnad"
|
|
||||||
echo "#"
|
|
||||||
echo "# sed -i 's/^svgImagesRequired=.*/svgImagesRequired=true/' $BBB_WEB_CONFIG "
|
|
||||||
echo "#"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
CHECK_STUN=$(xmlstarlet sel -t -m '//X-PRE-PROCESS[@cmd="set" and starts-with(@data, "external_rtp_ip=")]' -v @data $FREESWITCH_VARS | sed 's/external_rtp_ip=stun://g')
|
CHECK_STUN=$(xmlstarlet sel -t -m '//X-PRE-PROCESS[@cmd="set" and starts-with(@data, "external_rtp_ip=")]' -v @data $FREESWITCH_VARS | sed 's/external_rtp_ip=stun://g')
|
||||||
if [ "$CHECK_STUN" == "stun.freeswitch.org" ]; then
|
if [ "$CHECK_STUN" == "stun.freeswitch.org" ]; then
|
||||||
echo
|
echo
|
||||||
@ -1370,7 +1355,6 @@ if [ $CHECK ]; then
|
|||||||
echo "$BBB_WEB_CONFIG (bbb-web)"
|
echo "$BBB_WEB_CONFIG (bbb-web)"
|
||||||
echo " bigbluebutton.web.serverURL: $(get_bbb_web_config_value bigbluebutton.web.serverURL)"
|
echo " bigbluebutton.web.serverURL: $(get_bbb_web_config_value bigbluebutton.web.serverURL)"
|
||||||
echo " defaultGuestPolicy: $(get_bbb_web_config_value defaultGuestPolicy)"
|
echo " defaultGuestPolicy: $(get_bbb_web_config_value defaultGuestPolicy)"
|
||||||
echo " svgImagesRequired: $(get_bbb_web_config_value svgImagesRequired)"
|
|
||||||
echo " defaultMeetingLayout: $(get_bbb_web_config_value defaultMeetingLayout)"
|
echo " defaultMeetingLayout: $(get_bbb_web_config_value defaultMeetingLayout)"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
|
@ -5,20 +5,20 @@
|
|||||||
|
|
||||||
meteor-base@1.5.1
|
meteor-base@1.5.1
|
||||||
mobile-experience@1.1.0
|
mobile-experience@1.1.0
|
||||||
mongo@1.15.0
|
mongo@1.16.3
|
||||||
reactive-var@1.0.11
|
reactive-var@1.0.12
|
||||||
|
|
||||||
standard-minifier-css@1.8.1
|
standard-minifier-css@1.8.3
|
||||||
standard-minifier-js@2.8.0
|
standard-minifier-js@2.8.1
|
||||||
es5-shim@4.8.0
|
es5-shim@4.8.0
|
||||||
ecmascript@0.16.2
|
ecmascript@0.16.4
|
||||||
shell-server@0.5.0
|
shell-server@0.5.0
|
||||||
|
|
||||||
static-html@1.3.2
|
static-html@1.3.2
|
||||||
react-meteor-data
|
react-meteor-data
|
||||||
session@1.2.0
|
session@1.2.1
|
||||||
tracker@1.2.0
|
tracker@1.2.1
|
||||||
check@1.3.1
|
check@1.3.2
|
||||||
|
|
||||||
rocketchat:streamer
|
rocketchat:streamer
|
||||||
meteortesting:mocha
|
meteortesting:mocha
|
||||||
|
@ -1 +1 @@
|
|||||||
METEOR@2.7.3
|
METEOR@2.9.0
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
allow-deny@1.1.1
|
allow-deny@1.1.1
|
||||||
autoupdate@1.8.0
|
autoupdate@1.8.0
|
||||||
babel-compiler@7.9.0
|
babel-compiler@7.10.1
|
||||||
babel-runtime@1.5.1
|
babel-runtime@1.5.1
|
||||||
base64@1.0.12
|
base64@1.0.12
|
||||||
binary-heap@1.0.11
|
binary-heap@1.0.11
|
||||||
@ -9,21 +9,21 @@ boilerplate-generator@1.7.1
|
|||||||
caching-compiler@1.2.2
|
caching-compiler@1.2.2
|
||||||
caching-html-compiler@1.2.1
|
caching-html-compiler@1.2.1
|
||||||
callback-hook@1.4.0
|
callback-hook@1.4.0
|
||||||
check@1.3.1
|
check@1.3.2
|
||||||
ddp@1.4.0
|
ddp@1.4.1
|
||||||
ddp-client@2.5.0
|
ddp-client@2.6.1
|
||||||
ddp-common@1.4.0
|
ddp-common@1.4.0
|
||||||
ddp-server@2.5.0
|
ddp-server@2.6.0
|
||||||
diff-sequence@1.1.1
|
diff-sequence@1.1.2
|
||||||
dynamic-import@0.7.2
|
dynamic-import@0.7.2
|
||||||
ecmascript@0.16.2
|
ecmascript@0.16.4
|
||||||
ecmascript-runtime@0.8.0
|
ecmascript-runtime@0.8.0
|
||||||
ecmascript-runtime-client@0.12.1
|
ecmascript-runtime-client@0.12.1
|
||||||
ecmascript-runtime-server@0.11.0
|
ecmascript-runtime-server@0.11.0
|
||||||
ejson@1.1.2
|
ejson@1.1.3
|
||||||
es5-shim@4.8.0
|
es5-shim@4.8.0
|
||||||
fetch@0.1.1
|
fetch@0.1.2
|
||||||
geojson-utils@1.0.10
|
geojson-utils@1.0.11
|
||||||
hot-code-push@1.0.4
|
hot-code-push@1.0.4
|
||||||
html-tools@1.1.3
|
html-tools@1.1.3
|
||||||
htmljs@1.1.1
|
htmljs@1.1.1
|
||||||
@ -33,46 +33,46 @@ inter-process-messaging@0.1.1
|
|||||||
launch-screen@1.3.0
|
launch-screen@1.3.0
|
||||||
lmieulet:meteor-coverage@4.1.0
|
lmieulet:meteor-coverage@4.1.0
|
||||||
logging@1.3.1
|
logging@1.3.1
|
||||||
meteor@1.10.0
|
meteor@1.10.3
|
||||||
meteor-base@1.5.1
|
meteor-base@1.5.1
|
||||||
meteortesting:browser-tests@1.3.5
|
meteortesting:browser-tests@1.3.5
|
||||||
meteortesting:mocha@2.0.3
|
meteortesting:mocha@2.0.3
|
||||||
meteortesting:mocha-core@8.1.2
|
meteortesting:mocha-core@8.1.2
|
||||||
minifier-css@1.6.0
|
minifier-css@1.6.2
|
||||||
minifier-js@2.7.4
|
minifier-js@2.7.5
|
||||||
minimongo@1.8.0
|
minimongo@1.9.1
|
||||||
mobile-experience@1.1.0
|
mobile-experience@1.1.0
|
||||||
mobile-status-bar@1.1.0
|
mobile-status-bar@1.1.0
|
||||||
modern-browsers@0.1.8
|
modern-browsers@0.1.9
|
||||||
modules@0.18.0
|
modules@0.19.0
|
||||||
modules-runtime@0.13.0
|
modules-runtime@0.13.1
|
||||||
mongo@1.15.0
|
mongo@1.16.3
|
||||||
mongo-decimal@0.1.3
|
mongo-decimal@0.1.3
|
||||||
mongo-dev-server@1.1.0
|
mongo-dev-server@1.1.0
|
||||||
mongo-id@1.0.8
|
mongo-id@1.0.8
|
||||||
npm-mongo@4.3.1
|
npm-mongo@4.12.1
|
||||||
ordered-dict@1.1.0
|
ordered-dict@1.1.0
|
||||||
promise@0.12.0
|
promise@0.12.2
|
||||||
random@1.2.0
|
random@1.2.1
|
||||||
react-fast-refresh@0.2.3
|
react-fast-refresh@0.2.3
|
||||||
react-meteor-data@2.5.1
|
react-meteor-data@2.5.1
|
||||||
reactive-dict@1.3.0
|
reactive-dict@1.3.1
|
||||||
reactive-var@1.0.11
|
reactive-var@1.0.12
|
||||||
reload@1.3.1
|
reload@1.3.1
|
||||||
retry@1.1.0
|
retry@1.1.0
|
||||||
rocketchat:streamer@1.1.0
|
rocketchat:streamer@1.1.0
|
||||||
routepolicy@1.1.1
|
routepolicy@1.1.1
|
||||||
session@1.2.0
|
session@1.2.1
|
||||||
shell-server@0.5.0
|
shell-server@0.5.0
|
||||||
socket-stream-client@0.5.0
|
socket-stream-client@0.5.0
|
||||||
spacebars-compiler@1.3.1
|
spacebars-compiler@1.3.1
|
||||||
standard-minifier-css@1.8.1
|
standard-minifier-css@1.8.3
|
||||||
standard-minifier-js@2.8.0
|
standard-minifier-js@2.8.1
|
||||||
static-html@1.3.2
|
static-html@1.3.2
|
||||||
templating-tools@1.2.2
|
templating-tools@1.2.2
|
||||||
tracker@1.2.0
|
tracker@1.2.1
|
||||||
typescript@4.5.4
|
typescript@4.6.4
|
||||||
underscore@1.0.10
|
underscore@1.0.11
|
||||||
url@1.3.2
|
url@1.3.2
|
||||||
webapp@1.13.1
|
webapp@1.13.2
|
||||||
webapp-hashing@1.1.0
|
webapp-hashing@1.1.1
|
||||||
|
@ -27,74 +27,52 @@ import Users, { CurrentUser } from '/imports/api/users';
|
|||||||
import { Slides, SlidePositions } from '/imports/api/slides';
|
import { Slides, SlidePositions } from '/imports/api/slides';
|
||||||
|
|
||||||
// Custom Publishers
|
// Custom Publishers
|
||||||
export const localCurrentPollSync = new AbstractCollection(CurrentPoll, CurrentPoll);
|
export const localCollectionRegistry = {
|
||||||
export const localCurrentUserSync = new AbstractCollection(CurrentUser, CurrentUser);
|
localCurrentPollSync: new AbstractCollection(CurrentPoll, CurrentPoll),
|
||||||
export const localSlidesSync = new AbstractCollection(Slides, Slides);
|
localCurrentUserSync: new AbstractCollection(CurrentUser, CurrentUser),
|
||||||
export const localSlidePositionsSync = new AbstractCollection(SlidePositions, SlidePositions);
|
localSlidesSync: new AbstractCollection(Slides, Slides),
|
||||||
export const localPollsSync = new AbstractCollection(Polls, Polls);
|
localSlidePositionsSync: new AbstractCollection(SlidePositions, SlidePositions),
|
||||||
export const localPresentationsSync = new AbstractCollection(Presentations, Presentations);
|
localPollsSync: new AbstractCollection(Polls, Polls),
|
||||||
export const localPresentationPodsSync = new AbstractCollection(PresentationPods, PresentationPods);
|
localPresentationsSync: new AbstractCollection(Presentations, Presentations),
|
||||||
export const localPresentationUploadTokenSync = new AbstractCollection(PresentationUploadToken, PresentationUploadToken);
|
localPresentationPodsSync: new AbstractCollection(PresentationPods, PresentationPods),
|
||||||
export const localScreenshareSync = new AbstractCollection(Screenshare, Screenshare);
|
localPresentationUploadTokenSync: new AbstractCollection(
|
||||||
export const localUserInfosSync = new AbstractCollection(UserInfos, UserInfos);
|
PresentationUploadToken,
|
||||||
export const localUsersPersistentDataSync = new AbstractCollection(UsersPersistentData, UsersPersistentData);
|
PresentationUploadToken,
|
||||||
export const localUserSettingsSync = new AbstractCollection(UserSettings, UserSettings);
|
),
|
||||||
export const localVideoStreamsSync = new AbstractCollection(VideoStreams, VideoStreams);
|
localScreenshareSync: new AbstractCollection(Screenshare, Screenshare),
|
||||||
export const localVoiceUsersSync = new AbstractCollection(VoiceUsers, VoiceUsers);
|
localUserInfosSync: new AbstractCollection(UserInfos, UserInfos),
|
||||||
export const localWhiteboardMultiUserSync = new AbstractCollection(WhiteboardMultiUser, WhiteboardMultiUser);
|
localUsersPersistentDataSync: new AbstractCollection(UsersPersistentData, UsersPersistentData),
|
||||||
export const localGroupChatSync = new AbstractCollection(GroupChat, GroupChat);
|
localUserSettingsSync: new AbstractCollection(UserSettings, UserSettings),
|
||||||
export const localConnectionStatusSync = new AbstractCollection(ConnectionStatus, ConnectionStatus);
|
localVideoStreamsSync: new AbstractCollection(VideoStreams, VideoStreams),
|
||||||
export const localCaptionsSync = new AbstractCollection(Captions, Captions);
|
localVoiceUsersSync: new AbstractCollection(VoiceUsers, VoiceUsers),
|
||||||
export const localPadsSync = new AbstractCollection(Pads, Pads);
|
localWhiteboardMultiUserSync: new AbstractCollection(WhiteboardMultiUser, WhiteboardMultiUser),
|
||||||
export const localPadsSessionsSync = new AbstractCollection(PadsSessions, PadsSessions);
|
localGroupChatSync: new AbstractCollection(GroupChat, GroupChat),
|
||||||
export const localPadsUpdatesSync = new AbstractCollection(PadsUpdates, PadsUpdates);
|
localConnectionStatusSync: new AbstractCollection(ConnectionStatus, ConnectionStatus),
|
||||||
export const localAuthTokenValidationSync = new AbstractCollection(AuthTokenValidation, AuthTokenValidation);
|
localCaptionsSync: new AbstractCollection(Captions, Captions),
|
||||||
export const localAnnotationsSync = new AbstractCollection(Annotations, Annotations);
|
localPadsSync: new AbstractCollection(Pads, Pads),
|
||||||
export const localRecordMeetingsSync = new AbstractCollection(RecordMeetings, RecordMeetings);
|
localPadsSessionsSync: new AbstractCollection(PadsSessions, PadsSessions),
|
||||||
export const localExternalVideoMeetingsSync = new AbstractCollection(ExternalVideoMeetings, ExternalVideoMeetings);
|
localPadsUpdatesSync: new AbstractCollection(PadsUpdates, PadsUpdates),
|
||||||
export const localMeetingTimeRemainingSync = new AbstractCollection(MeetingTimeRemaining, MeetingTimeRemaining);
|
localAuthTokenValidationSync: new AbstractCollection(AuthTokenValidation, AuthTokenValidation),
|
||||||
export const localUsersTypingSync = new AbstractCollection(UsersTyping, UsersTyping);
|
localAnnotationsSync: new AbstractCollection(Annotations, Annotations),
|
||||||
export const localBreakoutsSync = new AbstractCollection(Breakouts, Breakouts);
|
localRecordMeetingsSync: new AbstractCollection(RecordMeetings, RecordMeetings),
|
||||||
export const localBreakoutsHistorySync = new AbstractCollection(BreakoutsHistory, BreakoutsHistory);
|
localExternalVideoMeetingsSync: new AbstractCollection(
|
||||||
export const localGuestUsersSync = new AbstractCollection(guestUsers, guestUsers);
|
ExternalVideoMeetings,
|
||||||
export const localMeetingsSync = new AbstractCollection(Meetings, Meetings);
|
ExternalVideoMeetings,
|
||||||
export const localUsersSync = new AbstractCollection(Users, Users);
|
),
|
||||||
export const localNotificationsSync = new AbstractCollection(Notifications, Notifications);
|
localMeetingTimeRemainingSync: new AbstractCollection(MeetingTimeRemaining, MeetingTimeRemaining),
|
||||||
|
localUsersTypingSync: new AbstractCollection(UsersTyping, UsersTyping),
|
||||||
|
localBreakoutsSync: new AbstractCollection(Breakouts, Breakouts),
|
||||||
|
localBreakoutsHistorySync: new AbstractCollection(BreakoutsHistory, BreakoutsHistory),
|
||||||
|
localGuestUsersSync: new AbstractCollection(guestUsers, guestUsers),
|
||||||
|
localMeetingsSync: new AbstractCollection(Meetings, Meetings),
|
||||||
|
localUsersSync: new AbstractCollection(Users, Users),
|
||||||
|
localNotificationsSync: new AbstractCollection(Notifications, Notifications),
|
||||||
|
};
|
||||||
|
|
||||||
const collectionMirrorInitializer = () => {
|
const collectionMirrorInitializer = () => {
|
||||||
localCurrentPollSync.setupListeners();
|
Object.values(localCollectionRegistry).forEach((localCollection) => {
|
||||||
localCurrentUserSync.setupListeners();
|
localCollection.setupListeners();
|
||||||
localSlidesSync.setupListeners();
|
});
|
||||||
localSlidePositionsSync.setupListeners();
|
|
||||||
localPollsSync.setupListeners();
|
|
||||||
localPresentationsSync.setupListeners();
|
|
||||||
localPresentationPodsSync.setupListeners();
|
|
||||||
localPresentationUploadTokenSync.setupListeners();
|
|
||||||
localScreenshareSync.setupListeners();
|
|
||||||
localUserInfosSync.setupListeners();
|
|
||||||
localUsersPersistentDataSync.setupListeners();
|
|
||||||
localUserSettingsSync.setupListeners();
|
|
||||||
localVideoStreamsSync.setupListeners();
|
|
||||||
localVoiceUsersSync.setupListeners();
|
|
||||||
localWhiteboardMultiUserSync.setupListeners();
|
|
||||||
localGroupChatSync.setupListeners();
|
|
||||||
localConnectionStatusSync.setupListeners();
|
|
||||||
localCaptionsSync.setupListeners();
|
|
||||||
localPadsSync.setupListeners();
|
|
||||||
localPadsSessionsSync.setupListeners();
|
|
||||||
localPadsUpdatesSync.setupListeners();
|
|
||||||
localAuthTokenValidationSync.setupListeners();
|
|
||||||
localAnnotationsSync.setupListeners();
|
|
||||||
localRecordMeetingsSync.setupListeners();
|
|
||||||
localExternalVideoMeetingsSync.setupListeners();
|
|
||||||
localMeetingTimeRemainingSync.setupListeners();
|
|
||||||
localUsersTypingSync.setupListeners();
|
|
||||||
localBreakoutsSync.setupListeners();
|
|
||||||
localBreakoutsHistorySync.setupListeners();
|
|
||||||
localGuestUsersSync.setupListeners();
|
|
||||||
localMeetingsSync.setupListeners();
|
|
||||||
localUsersSync.setupListeners();
|
|
||||||
localNotificationsSync.setupListeners();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default collectionMirrorInitializer;
|
export default collectionMirrorInitializer;
|
||||||
|
@ -127,7 +127,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
|||||||
</head>
|
</head>
|
||||||
<body style="background-color: #06172A">
|
<body style="background-color: #06172A">
|
||||||
<div id="aria-polite-alert" aria-live="polite" aria-atomic="false" class="sr-only"></div>
|
<div id="aria-polite-alert" aria-live="polite" aria-atomic="false" class="sr-only"></div>
|
||||||
<div id="app" role="document">
|
<main>
|
||||||
|
<div id="app" role="document">
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<span id="destination"></span>
|
<span id="destination"></span>
|
||||||
<audio id="remote-media" autoplay>
|
<audio id="remote-media" autoplay>
|
||||||
|
@ -2,6 +2,7 @@ import Breakouts from '/imports/api/breakouts';
|
|||||||
import updateUserBreakoutRoom from '/imports/api/users-persistent-data/server/modifiers/updateUserBreakoutRoom';
|
import updateUserBreakoutRoom from '/imports/api/users-persistent-data/server/modifiers/updateUserBreakoutRoom';
|
||||||
import Logger from '/imports/startup/server/logger';
|
import Logger from '/imports/startup/server/logger';
|
||||||
import { check } from 'meteor/check';
|
import { check } from 'meteor/check';
|
||||||
|
import { lowercaseTrim } from '/imports/utils/string-utils';
|
||||||
|
|
||||||
export default function joinedUsersChanged({ body }) {
|
export default function joinedUsersChanged({ body }) {
|
||||||
check(body, Object);
|
check(body, Object);
|
||||||
@ -21,7 +22,7 @@ export default function joinedUsersChanged({ body }) {
|
|||||||
breakoutId,
|
breakoutId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const usersMapped = users.map(user => ({ userId: user.id, name: user.name }));
|
const usersMapped = users.map(user => ({ userId: user.id, name: user.name, sortName: lowercaseTrim(user.name) }));
|
||||||
const modifier = {
|
const modifier = {
|
||||||
$set: {
|
$set: {
|
||||||
joinedUsers: usersMapped,
|
joinedUsers: usersMapped,
|
||||||
|
@ -86,8 +86,8 @@ export default function addMeeting(meeting) {
|
|||||||
name: String,
|
name: String,
|
||||||
disabledFeatures: Array,
|
disabledFeatures: Array,
|
||||||
notifyRecordingIsOn: Boolean,
|
notifyRecordingIsOn: Boolean,
|
||||||
uploadExternalDescription: String,
|
presentationUploadExternalDescription: String,
|
||||||
uploadExternalUrl: String,
|
presentationUploadExternalUrl: String,
|
||||||
},
|
},
|
||||||
usersProp: {
|
usersProp: {
|
||||||
maxUsers: Number,
|
maxUsers: Number,
|
||||||
|
@ -11,6 +11,7 @@ RedisPubSub.on('PdfConversionInvalidErrorEvtMsg', handlePresentationConversionUp
|
|||||||
RedisPubSub.on('PresentationPageGeneratedEvtMsg', handlePresentationConversionUpdate);
|
RedisPubSub.on('PresentationPageGeneratedEvtMsg', handlePresentationConversionUpdate);
|
||||||
RedisPubSub.on('PresentationPageCountErrorEvtMsg', handlePresentationConversionUpdate);
|
RedisPubSub.on('PresentationPageCountErrorEvtMsg', handlePresentationConversionUpdate);
|
||||||
RedisPubSub.on('PresentationUploadedFileTimeoutErrorEvtMsg', handlePresentationConversionUpdate);
|
RedisPubSub.on('PresentationUploadedFileTimeoutErrorEvtMsg', handlePresentationConversionUpdate);
|
||||||
|
RedisPubSub.on('PresentationHasInvalidMimeTypeErrorEvtMsg', handlePresentationConversionUpdate);
|
||||||
RedisPubSub.on('PresentationConversionUpdateEvtMsg', handlePresentationConversionUpdate);
|
RedisPubSub.on('PresentationConversionUpdateEvtMsg', handlePresentationConversionUpdate);
|
||||||
RedisPubSub.on('PresentationUploadedFileTooLargeErrorEvtMsg', handlePresentationConversionUpdate);
|
RedisPubSub.on('PresentationUploadedFileTooLargeErrorEvtMsg', handlePresentationConversionUpdate);
|
||||||
RedisPubSub.on('PresentationConversionCompletedEvtMsg', handlePresentationAdded);
|
RedisPubSub.on('PresentationConversionCompletedEvtMsg', handlePresentationAdded);
|
||||||
|
@ -13,6 +13,7 @@ const PDF_HAS_BIG_PAGE_KEY = 'PDF_HAS_BIG_PAGE';
|
|||||||
const GENERATED_SLIDE_KEY = 'GENERATED_SLIDE';
|
const GENERATED_SLIDE_KEY = 'GENERATED_SLIDE';
|
||||||
const FILE_TOO_LARGE_KEY = 'FILE_TOO_LARGE';
|
const FILE_TOO_LARGE_KEY = 'FILE_TOO_LARGE';
|
||||||
const CONVERSION_TIMEOUT_KEY = "CONVERSION_TIMEOUT";
|
const CONVERSION_TIMEOUT_KEY = "CONVERSION_TIMEOUT";
|
||||||
|
const IVALID_MIME_TYPE_KEY = "IVALID_MIME_TYPE";
|
||||||
// const GENERATING_THUMBNAIL_KEY = 'GENERATING_THUMBNAIL';
|
// const GENERATING_THUMBNAIL_KEY = 'GENERATING_THUMBNAIL';
|
||||||
// const GENERATED_THUMBNAIL_KEY = 'GENERATED_THUMBNAIL';
|
// const GENERATED_THUMBNAIL_KEY = 'GENERATED_THUMBNAIL';
|
||||||
// const GENERATING_TEXTFILES_KEY = 'GENERATING_TEXTFILES';
|
// const GENERATING_TEXTFILES_KEY = 'GENERATING_TEXTFILES';
|
||||||
@ -50,6 +51,10 @@ export default function handlePresentationConversionUpdate({ body }, meetingId)
|
|||||||
statusModifier['conversion.maxFileSize'] = body.maxFileSize;
|
statusModifier['conversion.maxFileSize'] = body.maxFileSize;
|
||||||
case UNSUPPORTED_DOCUMENT_KEY:
|
case UNSUPPORTED_DOCUMENT_KEY:
|
||||||
case OFFICE_DOC_CONVERSION_FAILED_KEY:
|
case OFFICE_DOC_CONVERSION_FAILED_KEY:
|
||||||
|
case IVALID_MIME_TYPE_KEY:
|
||||||
|
statusModifier['conversion.error'] = true;
|
||||||
|
statusModifier['conversion.fileMime'] = body.fileMime;
|
||||||
|
statusModifier['conversion.fileExtension'] = body.fileExtension;
|
||||||
case OFFICE_DOC_CONVERSION_INVALID_KEY:
|
case OFFICE_DOC_CONVERSION_INVALID_KEY:
|
||||||
case PAGE_COUNT_FAILED_KEY:
|
case PAGE_COUNT_FAILED_KEY:
|
||||||
case PAGE_COUNT_EXCEEDED_KEY:
|
case PAGE_COUNT_EXCEEDED_KEY:
|
||||||
|
@ -6,6 +6,7 @@ import VoiceUsers from '/imports/api/voice-users/';
|
|||||||
import addUserPsersistentData from '/imports/api/users-persistent-data/server/modifiers/addUserPersistentData';
|
import addUserPsersistentData from '/imports/api/users-persistent-data/server/modifiers/addUserPersistentData';
|
||||||
import stringHash from 'string-hash';
|
import stringHash from 'string-hash';
|
||||||
import flat from 'flat';
|
import flat from 'flat';
|
||||||
|
import { lowercaseTrim } from '/imports/utils/string-utils';
|
||||||
|
|
||||||
import addVoiceUser from '/imports/api/voice-users/server/modifiers/addVoiceUser';
|
import addVoiceUser from '/imports/api/voice-users/server/modifiers/addVoiceUser';
|
||||||
|
|
||||||
@ -51,7 +52,7 @@ export default function addUser(meetingId, userData) {
|
|||||||
|
|
||||||
const userInfos = {
|
const userInfos = {
|
||||||
meetingId,
|
meetingId,
|
||||||
sortName: user.name.trim().toLowerCase(),
|
sortName: lowercaseTrim(user.name),
|
||||||
color,
|
color,
|
||||||
speechLocale: '',
|
speechLocale: '',
|
||||||
mobile: false,
|
mobile: false,
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
} from '/imports/api/video-streams/server/helpers';
|
} from '/imports/api/video-streams/server/helpers';
|
||||||
import VoiceUsers from '/imports/api/voice-users/';
|
import VoiceUsers from '/imports/api/voice-users/';
|
||||||
import Users from '/imports/api/users/';
|
import Users from '/imports/api/users/';
|
||||||
|
import { lowercaseTrim } from '/imports/utils/string-utils';
|
||||||
|
|
||||||
const BASE_FLOOR_TIME = "0";
|
const BASE_FLOOR_TIME = "0";
|
||||||
|
|
||||||
@ -40,6 +41,7 @@ export default function sharedWebcam(meetingId, userId, stream) {
|
|||||||
$set: {
|
$set: {
|
||||||
stream,
|
stream,
|
||||||
name,
|
name,
|
||||||
|
sortName: lowercaseTrim(name),
|
||||||
lastFloorTime,
|
lastFloorTime,
|
||||||
floor,
|
floor,
|
||||||
pin,
|
pin,
|
||||||
|
@ -171,7 +171,7 @@ class Base extends Component {
|
|||||||
HTML.classList.add('animationsDisabled');
|
HTML.classList.add('animationsDisabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sidebarContentPanel === PANELS.NONE || Session.equals('subscriptionsReady', true)) {
|
if (Session.equals('layoutReady', true) && (sidebarContentPanel === PANELS.NONE || Session.equals('subscriptionsReady', true))) {
|
||||||
if (!checkedUserSettings) {
|
if (!checkedUserSettings) {
|
||||||
if (getFromUserSettings('bbb_show_participants_on_login', Meteor.settings.public.layout.showParticipantsOnLogin) && !deviceInfo.isPhone) {
|
if (getFromUserSettings('bbb_show_participants_on_login', Meteor.settings.public.layout.showParticipantsOnLogin) && !deviceInfo.isPhone) {
|
||||||
if (isChatEnabled() && getFromUserSettings('bbb_show_public_chat_on_login', !Meteor.settings.public.chat.startClosed)) {
|
if (isChatEnabled() && getFromUserSettings('bbb_show_public_chat_on_login', !Meteor.settings.public.chat.startClosed)) {
|
||||||
|
@ -723,8 +723,7 @@ class BreakoutRoom extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
populateWithLastBreakouts(lastBreakouts) {
|
populateWithLastBreakouts(lastBreakouts) {
|
||||||
const { getBreakoutUserWasIn, intl } = this.props;
|
const { getBreakoutUserWasIn, users, intl } = this.props;
|
||||||
const { users } = this.state;
|
|
||||||
|
|
||||||
const changedNames = [];
|
const changedNames = [];
|
||||||
lastBreakouts.forEach((breakout) => {
|
lastBreakouts.forEach((breakout) => {
|
||||||
|
@ -40,13 +40,6 @@ const Alert = styled.div`
|
|||||||
color: ${colorDanger};
|
color: ${colorDanger};
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
|
|
||||||
grid-row: span 3;
|
|
||||||
|
|
||||||
& > div {
|
|
||||||
height: 25.2rem;
|
|
||||||
max-height: 25.2rem;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const FreeJoinLabel = styled.label`
|
const FreeJoinLabel = styled.label`
|
||||||
@ -85,8 +78,7 @@ const BreakoutNameInput = styled.input`
|
|||||||
|
|
||||||
const BreakoutBox = styled(ScrollboxVertical)`
|
const BreakoutBox = styled(ScrollboxVertical)`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 6rem;
|
height: 21rem;
|
||||||
max-height: 8rem;
|
|
||||||
border: 1px solid ${colorGrayLightest};
|
border: 1px solid ${colorGrayLightest};
|
||||||
border-radius: ${borderRadius};
|
border-radius: ${borderRadius};
|
||||||
padding: ${lgPaddingY} 0;
|
padding: ${lgPaddingY} 0;
|
||||||
|
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 ScreenshareContainer from '../screenshare/container';
|
||||||
import ExternalVideoContainer from '../external-video-player/container';
|
import ExternalVideoContainer from '../external-video-player/container';
|
||||||
import Styled from './styles';
|
import Styled from './styles';
|
||||||
import { DEVICE_TYPE, ACTIONS, SMALL_VIEWPORT_BREAKPOINT } from '../layout/enums';
|
import { DEVICE_TYPE, ACTIONS, SMALL_VIEWPORT_BREAKPOINT, PANELS } from '../layout/enums';
|
||||||
import {
|
import {
|
||||||
isMobile, isTablet, isTabletPortrait, isTabletLandscape, isDesktop,
|
isMobile, isTablet, isTabletPortrait, isTabletLandscape, isDesktop,
|
||||||
} from '../layout/utils';
|
} from '../layout/utils';
|
||||||
@ -47,13 +47,16 @@ import Notifications from '../notifications/container';
|
|||||||
import GlobalStyles from '/imports/ui/stylesheets/styled-components/globalStyles';
|
import GlobalStyles from '/imports/ui/stylesheets/styled-components/globalStyles';
|
||||||
import ActionsBarContainer from '../actions-bar/container';
|
import ActionsBarContainer from '../actions-bar/container';
|
||||||
import PushLayoutEngine from '../layout/push-layout/pushLayoutEngine';
|
import PushLayoutEngine from '../layout/push-layout/pushLayoutEngine';
|
||||||
|
import AudioService from '/imports/ui/components/audio/service';
|
||||||
import NotesContainer from '/imports/ui/components/notes/container';
|
import NotesContainer from '/imports/ui/components/notes/container';
|
||||||
|
import DEFAULT_VALUES from '../layout/defaultValues';
|
||||||
|
|
||||||
const MOBILE_MEDIA = 'only screen and (max-width: 40em)';
|
const MOBILE_MEDIA = 'only screen and (max-width: 40em)';
|
||||||
const APP_CONFIG = Meteor.settings.public.app;
|
const APP_CONFIG = Meteor.settings.public.app;
|
||||||
const DESKTOP_FONT_SIZE = APP_CONFIG.desktopFontSize;
|
const DESKTOP_FONT_SIZE = APP_CONFIG.desktopFontSize;
|
||||||
const MOBILE_FONT_SIZE = APP_CONFIG.mobileFontSize;
|
const MOBILE_FONT_SIZE = APP_CONFIG.mobileFontSize;
|
||||||
const LAYOUT_CONFIG = Meteor.settings.public.layout;
|
const LAYOUT_CONFIG = Meteor.settings.public.layout;
|
||||||
|
const CONFIRMATION_ON_LEAVE = Meteor.settings.public.app.askForConfirmationOnLeave;
|
||||||
|
|
||||||
const intlMessages = defineMessages({
|
const intlMessages = defineMessages({
|
||||||
userListLabel: {
|
userListLabel: {
|
||||||
@ -195,6 +198,16 @@ class App extends Component {
|
|||||||
window.ondragover = (e) => { e.preventDefault(); };
|
window.ondragover = (e) => { e.preventDefault(); };
|
||||||
window.ondrop = (e) => { e.preventDefault(); };
|
window.ondrop = (e) => { e.preventDefault(); };
|
||||||
|
|
||||||
|
if (CONFIRMATION_ON_LEAVE) {
|
||||||
|
window.onbeforeunload = (event) => {
|
||||||
|
AudioService.muteMicrophone();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
event.returnValue = '';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (deviceInfo.isMobile) makeCall('setMobileUser');
|
if (deviceInfo.isMobile) makeCall('setMobileUser');
|
||||||
|
|
||||||
ConnectionStatusService.startRoundTripTime();
|
ConnectionStatusService.startRoundTripTime();
|
||||||
@ -210,6 +223,12 @@ class App extends Component {
|
|||||||
mountModal,
|
mountModal,
|
||||||
deviceType,
|
deviceType,
|
||||||
mountRandomUserModal,
|
mountRandomUserModal,
|
||||||
|
selectedLayout,
|
||||||
|
sidebarContentIsOpen,
|
||||||
|
layoutContextDispatch,
|
||||||
|
numCameras,
|
||||||
|
presentationIsOpen,
|
||||||
|
ignorePollNotifications,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
this.renderDarkMode();
|
this.renderDarkMode();
|
||||||
@ -243,10 +262,35 @@ class App extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (deviceType === null || prevProps.deviceType !== deviceType) this.throttledDeviceType();
|
if (deviceType === null || prevProps.deviceType !== deviceType) this.throttledDeviceType();
|
||||||
|
|
||||||
|
if (
|
||||||
|
selectedLayout !== prevProps.selectedLayout
|
||||||
|
&& selectedLayout?.toLowerCase?.()?.includes?.('focus')
|
||||||
|
&& !sidebarContentIsOpen
|
||||||
|
&& deviceType !== DEVICE_TYPE.MOBILE
|
||||||
|
&& numCameras > 0
|
||||||
|
&& presentationIsOpen
|
||||||
|
) {
|
||||||
|
setTimeout(() => {
|
||||||
|
layoutContextDispatch({
|
||||||
|
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||||
|
value: true,
|
||||||
|
});
|
||||||
|
layoutContextDispatch({
|
||||||
|
type: ACTIONS.SET_ID_CHAT_OPEN,
|
||||||
|
value: DEFAULT_VALUES.idChatOpen,
|
||||||
|
});
|
||||||
|
layoutContextDispatch({
|
||||||
|
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||||
|
value: PANELS.CHAT,
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
window.removeEventListener('resize', this.handleWindowResize, false);
|
window.removeEventListener('resize', this.handleWindowResize, false);
|
||||||
|
window.onbeforeunload = null;
|
||||||
ConnectionStatusService.stopRoundTripTime();
|
ConnectionStatusService.stopRoundTripTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,6 +168,7 @@ const AppContainer = (props) => {
|
|||||||
shouldShowPresentation,
|
shouldShowPresentation,
|
||||||
mountRandomUserModal,
|
mountRandomUserModal,
|
||||||
isPresenter,
|
isPresenter,
|
||||||
|
numCameras: cameraDockInput.numCameras,
|
||||||
}}
|
}}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
@ -310,5 +311,6 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
|
|||||||
hidePresentation: getFromUserSettings('bbb_hide_presentation', LAYOUT_CONFIG.hidePresentation),
|
hidePresentation: getFromUserSettings('bbb_hide_presentation', LAYOUT_CONFIG.hidePresentation),
|
||||||
hideActionsBar: getFromUserSettings('bbb_hide_actions_bar', false),
|
hideActionsBar: getFromUserSettings('bbb_hide_actions_bar', false),
|
||||||
isModalOpen: !!getModal(),
|
isModalOpen: !!getModal(),
|
||||||
|
ignorePollNotifications: Session.get('ignorePollNotifications'),
|
||||||
};
|
};
|
||||||
})(AppContainer)));
|
})(AppContainer)));
|
||||||
|
@ -71,6 +71,21 @@ const init = (messages, intl) => {
|
|||||||
return AudioManager.init(userData, audioEventHandler);
|
return AudioManager.init(userData, audioEventHandler);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const muteMicrophone = () => {
|
||||||
|
const user = VoiceUsers.findOne({
|
||||||
|
meetingId: Auth.meetingID, intId: Auth.userID,
|
||||||
|
}, { fields: { muted: 1 } });
|
||||||
|
|
||||||
|
if (!user.muted) {
|
||||||
|
logger.info({
|
||||||
|
logCode: 'audiomanager_mute_audio',
|
||||||
|
extraInfo: { logType: 'user_action' },
|
||||||
|
}, 'User wants to leave conference. Microphone muted');
|
||||||
|
AudioManager.setSenderTrackEnabled(false);
|
||||||
|
makeCall('toggleVoice');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const isVoiceUser = () => {
|
const isVoiceUser = () => {
|
||||||
const voiceUser = VoiceUsers.findOne({ intId: Auth.userID },
|
const voiceUser = VoiceUsers.findOne({ intId: Auth.userID },
|
||||||
{ fields: { joined: 1 } });
|
{ fields: { joined: 1 } });
|
||||||
@ -133,6 +148,7 @@ export default {
|
|||||||
updateAudioConstraints:
|
updateAudioConstraints:
|
||||||
(constraints) => AudioManager.updateAudioConstraints(constraints),
|
(constraints) => AudioManager.updateAudioConstraints(constraints),
|
||||||
recoverMicState,
|
recoverMicState,
|
||||||
|
muteMicrophone: () => muteMicrophone(),
|
||||||
isReconnecting: () => AudioManager.isReconnecting,
|
isReconnecting: () => AudioManager.isReconnecting,
|
||||||
setBreakoutAudioTransferStatus: (status) => AudioManager
|
setBreakoutAudioTransferStatus: (status) => AudioManager
|
||||||
.setBreakoutAudioTransferStatus(status),
|
.setBreakoutAudioTransferStatus(status),
|
||||||
|
@ -115,14 +115,15 @@ class MessageForm extends PureComponent {
|
|||||||
maxMessageLength,
|
maxMessageLength,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const message = e.target.value;
|
let message = e.target.value;
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
if (message.length > maxMessageLength) {
|
if (message.length > maxMessageLength) {
|
||||||
error = intl.formatMessage(
|
error = intl.formatMessage(
|
||||||
messages.errorMaxMessageLength,
|
messages.errorMaxMessageLength,
|
||||||
{ 0: message.length - maxMessageLength },
|
{ 0: maxMessageLength },
|
||||||
);
|
);
|
||||||
|
message = message.substring(0, maxMessageLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -74,11 +74,7 @@ class LiveCaptions extends PureComponent {
|
|||||||
<div style={captionStyles}>
|
<div style={captionStyles}>
|
||||||
{clear ? '' : data}
|
{clear ? '' : data}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div style={visuallyHidden}>
|
||||||
style={visuallyHidden}
|
|
||||||
aria-atomic
|
|
||||||
aria-live="polite"
|
|
||||||
>
|
|
||||||
{clear ? '' : data}
|
{clear ? '' : data}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,6 +10,7 @@ import { stripTags, unescapeHtml } from '/imports/utils/string-utils';
|
|||||||
import Service from '../service';
|
import Service from '../service';
|
||||||
import Styled from './styles';
|
import Styled from './styles';
|
||||||
import { usePreviousValue } from '/imports/ui/components/utils/hooks';
|
import { usePreviousValue } from '/imports/ui/components/utils/hooks';
|
||||||
|
import { Session } from 'meteor/session';
|
||||||
|
|
||||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||||
const PUBLIC_CHAT_CLEAR = CHAT_CONFIG.chat_clear;
|
const PUBLIC_CHAT_CLEAR = CHAT_CONFIG.chat_clear;
|
||||||
@ -193,9 +194,11 @@ const ChatAlert = (props) => {
|
|||||||
const mappedMessage = Service.mapGroupMessage(timeWindow);
|
const mappedMessage = Service.mapGroupMessage(timeWindow);
|
||||||
|
|
||||||
let content = null;
|
let content = null;
|
||||||
|
let isPollResult = false;
|
||||||
if (mappedMessage) {
|
if (mappedMessage) {
|
||||||
if (mappedMessage.id.includes(POLL_RESULT_KEY)) {
|
if (mappedMessage.id.includes(POLL_RESULT_KEY)) {
|
||||||
content = createPollMessage();
|
content = createPollMessage();
|
||||||
|
isPollResult = true;
|
||||||
} else {
|
} else {
|
||||||
content = createMessage(mappedMessage.sender.name, mappedMessage.content.slice(-5));
|
content = createMessage(mappedMessage.sender.name, mappedMessage.content.slice(-5));
|
||||||
}
|
}
|
||||||
@ -218,10 +221,22 @@ const ChatAlert = (props) => {
|
|||||||
: <span>{intl.formatMessage(intlMessages.appToastChatPrivate)}</span>
|
: <span>{intl.formatMessage(intlMessages.appToastChatPrivate)}</span>
|
||||||
}
|
}
|
||||||
onOpen={
|
onOpen={
|
||||||
() => setUnreadMessages(newUnreadMessages)
|
() => {
|
||||||
|
if (isPollResult) {
|
||||||
|
Session.set('ignorePollNotifications', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
setUnreadMessages(newUnreadMessages);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onClose={
|
onClose={
|
||||||
() => setUnreadMessages(newUnreadMessages)
|
() => {
|
||||||
|
if (isPollResult) {
|
||||||
|
Session.set('ignorePollNotifications', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
setUnreadMessages(newUnreadMessages);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
alertDuration={timeWindow.durationDiff}
|
alertDuration={timeWindow.durationDiff}
|
||||||
layoutContextDispatch={layoutContextDispatch}
|
layoutContextDispatch={layoutContextDispatch}
|
||||||
|
@ -227,8 +227,9 @@ class MessageForm extends PureComponent {
|
|||||||
if (message.length > maxMessageLength) {
|
if (message.length > maxMessageLength) {
|
||||||
error = intl.formatMessage(
|
error = intl.formatMessage(
|
||||||
messages.errorMaxMessageLength,
|
messages.errorMaxMessageLength,
|
||||||
{ 0: message.length - maxMessageLength },
|
{ 0: maxMessageLength },
|
||||||
);
|
);
|
||||||
|
message = message.substring(0, maxMessageLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -46,7 +46,7 @@ class LocalesDropdown extends PureComponent {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
value, handleChange, elementId, selectMessage,
|
value, handleChange, elementId, selectMessage, ariaLabel,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const defaultLocale = value || DEFAULT_VALUE;
|
const defaultLocale = value || DEFAULT_VALUE;
|
||||||
|
|
||||||
@ -57,6 +57,7 @@ class LocalesDropdown extends PureComponent {
|
|||||||
id={elementId}
|
id={elementId}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
value={defaultLocale}
|
value={defaultLocale}
|
||||||
|
aria-label={ariaLabel||''}
|
||||||
>
|
>
|
||||||
<option disabled key={DEFAULT_KEY} value={DEFAULT_VALUE}>
|
<option disabled key={DEFAULT_KEY} value={DEFAULT_VALUE}>
|
||||||
{selectMessage}
|
{selectMessage}
|
||||||
|
@ -6,7 +6,7 @@ import Styled from './styles';
|
|||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
yesLabel: {
|
yesLabel: {
|
||||||
id: 'app.endMeeting.yesLabel',
|
id: 'app.confirmationModal.yesLabel',
|
||||||
description: 'confirm button label',
|
description: 'confirm button label',
|
||||||
},
|
},
|
||||||
noLabel: {
|
noLabel: {
|
||||||
@ -46,6 +46,7 @@ class ConfirmationModal extends Component {
|
|||||||
titleMessageExtra,
|
titleMessageExtra,
|
||||||
checkboxMessageId,
|
checkboxMessageId,
|
||||||
confirmButtonColor,
|
confirmButtonColor,
|
||||||
|
confirmButtonLabel,
|
||||||
confirmButtonDataTest,
|
confirmButtonDataTest,
|
||||||
confirmParam,
|
confirmParam,
|
||||||
disableConfirmButton,
|
disableConfirmButton,
|
||||||
@ -86,7 +87,7 @@ class ConfirmationModal extends Component {
|
|||||||
<Styled.Footer>
|
<Styled.Footer>
|
||||||
<Styled.ConfirmationButton
|
<Styled.ConfirmationButton
|
||||||
color={confirmButtonColor}
|
color={confirmButtonColor}
|
||||||
label={intl.formatMessage(messages.yesLabel)}
|
label={confirmButtonLabel ? confirmButtonLabel : intl.formatMessage(messages.yesLabel)}
|
||||||
disabled={disableConfirmButton}
|
disabled={disableConfirmButton}
|
||||||
data-test={confirmButtonDataTest}
|
data-test={confirmButtonDataTest}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -47,6 +47,7 @@ class ModalSimple extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
id,
|
||||||
intl,
|
intl,
|
||||||
title,
|
title,
|
||||||
hideBorder,
|
hideBorder,
|
||||||
@ -79,6 +80,7 @@ class ModalSimple extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Styled.SimpleModal
|
<Styled.SimpleModal
|
||||||
|
id="simpleModal"
|
||||||
isOpen={modalisOpen}
|
isOpen={modalisOpen}
|
||||||
className={className}
|
className={className}
|
||||||
onRequestClose={handleRequestClose}
|
onRequestClose={handleRequestClose}
|
||||||
|
@ -158,8 +158,7 @@ class ConnectionStatusComponent extends PureComponent {
|
|||||||
|
|
||||||
this.help = Service.getHelp();
|
this.help = Service.getHelp();
|
||||||
this.state = {
|
this.state = {
|
||||||
selectedTab: '1',
|
selectedTab: 0,
|
||||||
dataPage: '1',
|
|
||||||
dataSaving: props.dataSaving,
|
dataSaving: props.dataSaving,
|
||||||
hasNetworkData: false,
|
hasNetworkData: false,
|
||||||
copyButtonText: intl.formatMessage(intlMessages.copy),
|
copyButtonText: intl.formatMessage(intlMessages.copy),
|
||||||
@ -187,6 +186,7 @@ class ConnectionStatusComponent extends PureComponent {
|
|||||||
this.audioDownloadLabel = intl.formatMessage(intlMessages.audioDownloadRate);
|
this.audioDownloadLabel = intl.formatMessage(intlMessages.audioDownloadRate);
|
||||||
this.videoUploadLabel = intl.formatMessage(intlMessages.videoUploadRate);
|
this.videoUploadLabel = intl.formatMessage(intlMessages.videoUploadRate);
|
||||||
this.videoDownloadLabel = intl.formatMessage(intlMessages.videoDownloadRate);
|
this.videoDownloadLabel = intl.formatMessage(intlMessages.videoDownloadRate);
|
||||||
|
this.handleSelectTab = this.handleSelectTab.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
@ -197,12 +197,24 @@ class ConnectionStatusComponent extends PureComponent {
|
|||||||
Meteor.clearInterval(this.rateInterval);
|
Meteor.clearInterval(this.rateInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleSelectTab(tab) {
|
||||||
|
this.setState({
|
||||||
|
selectedTab: tab,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
handleDataSavingChange(key) {
|
handleDataSavingChange(key) {
|
||||||
const { dataSaving } = this.state;
|
const { dataSaving } = this.state;
|
||||||
dataSaving[key] = !dataSaving[key];
|
dataSaving[key] = !dataSaving[key];
|
||||||
this.setState(dataSaving);
|
this.setState(dataSaving);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setButtonMessage(msg) {
|
||||||
|
this.setState({
|
||||||
|
copyButtonText: msg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start monitoring the network data.
|
* Start monitoring the network data.
|
||||||
* @return {Promise} A Promise that resolves when process started.
|
* @return {Promise} A Promise that resolves when process started.
|
||||||
@ -262,6 +274,43 @@ class ConnectionStatusComponent extends PureComponent {
|
|||||||
}, NETWORK_MONITORING_INTERVAL_MS);
|
}, NETWORK_MONITORING_INTERVAL_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
displaySettingsStatus(status) {
|
||||||
|
const { intl } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Styled.ToggleLabel>
|
||||||
|
{status ? intl.formatMessage(intlMessages.on)
|
||||||
|
: intl.formatMessage(intlMessages.off)}
|
||||||
|
</Styled.ToggleLabel>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy network data to clipboard
|
||||||
|
* @return {Promise} A Promise that is resolved after data is copied.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async copyNetworkData() {
|
||||||
|
const { intl } = this.props;
|
||||||
|
const {
|
||||||
|
networkData,
|
||||||
|
hasNetworkData,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
if (!hasNetworkData) return;
|
||||||
|
|
||||||
|
this.setButtonMessage(intl.formatMessage(intlMessages.copied));
|
||||||
|
|
||||||
|
const data = JSON.stringify(networkData, null, 2);
|
||||||
|
|
||||||
|
await navigator.clipboard.writeText(data);
|
||||||
|
|
||||||
|
this.copyNetworkDataTimeout = setTimeout(() => {
|
||||||
|
this.setButtonMessage(intl.formatMessage(intlMessages.copy));
|
||||||
|
}, MIN_TIMEOUT);
|
||||||
|
}
|
||||||
|
|
||||||
renderEmpty() {
|
renderEmpty() {
|
||||||
const { intl } = this.props;
|
const { intl } = this.props;
|
||||||
|
|
||||||
@ -278,52 +327,6 @@ class ConnectionStatusComponent extends PureComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
displaySettingsStatus(status) {
|
|
||||||
const { intl } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Styled.ToggleLabel>
|
|
||||||
{status ? intl.formatMessage(intlMessages.on)
|
|
||||||
: intl.formatMessage(intlMessages.off)}
|
|
||||||
</Styled.ToggleLabel>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setButtonMessage(msg) {
|
|
||||||
this.setState({
|
|
||||||
copyButtonText: msg,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy network data to clipboard
|
|
||||||
* @param {Object} e Event object from click event
|
|
||||||
* @return {Promise} A Promise that is resolved after data is copied.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async copyNetworkData(e) {
|
|
||||||
const { intl } = this.props;
|
|
||||||
const {
|
|
||||||
networkData,
|
|
||||||
hasNetworkData,
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
if (!hasNetworkData) return;
|
|
||||||
|
|
||||||
const { target: copyButton } = e;
|
|
||||||
|
|
||||||
this.setButtonMessage(intl.formatMessage(intlMessages.copied));
|
|
||||||
|
|
||||||
const data = JSON.stringify(networkData, null, 2);
|
|
||||||
|
|
||||||
await navigator.clipboard.writeText(data);
|
|
||||||
|
|
||||||
this.copyNetworkDataTimeout = setTimeout(() => {
|
|
||||||
this.setButtonMessage(intl.formatMessage(intlMessages.copy));
|
|
||||||
}, MIN_TIMEOUT);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderConnections() {
|
renderConnections() {
|
||||||
const {
|
const {
|
||||||
connectionStatus,
|
connectionStatus,
|
||||||
@ -335,7 +338,7 @@ class ConnectionStatusComponent extends PureComponent {
|
|||||||
if (isConnectionStatusEmpty(connectionStatus)) return this.renderEmpty();
|
if (isConnectionStatusEmpty(connectionStatus)) return this.renderEmpty();
|
||||||
|
|
||||||
let connections = connectionStatus;
|
let connections = connectionStatus;
|
||||||
if (selectedTab === '2') {
|
if (selectedTab === 2) {
|
||||||
connections = connections.filter(conn => conn.you);
|
connections = connections.filter(conn => conn.you);
|
||||||
if (isConnectionStatusEmpty(connections)) return this.renderEmpty();
|
if (isConnectionStatusEmpty(connections)) return this.renderEmpty();
|
||||||
}
|
}
|
||||||
@ -345,7 +348,7 @@ class ConnectionStatusComponent extends PureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Styled.Item
|
<Styled.Item
|
||||||
key={index}
|
key={`${conn?.name}-${dateTime}`}
|
||||||
last={(index + 1) === connections.length}
|
last={(index + 1) === connections.length}
|
||||||
data-test="connectionStatusItemUser"
|
data-test="connectionStatusItemUser"
|
||||||
>
|
>
|
||||||
@ -507,43 +510,17 @@ class ConnectionStatusComponent extends PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePaginationClick(action) {
|
|
||||||
if (action === 'next') {
|
|
||||||
this.setState({ dataPage: '2' });
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.setState({ dataPage: '1' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Styled.NetworkDataContainer data-test="networkDataContainer">
|
<Styled.NetworkDataContainer
|
||||||
<Styled.Prev>
|
data-test="networkDataContainer"
|
||||||
<Styled.ButtonLeft
|
tabIndex={0}
|
||||||
role="button"
|
>
|
||||||
disabled={dataPage === '1'}
|
<Styled.HelperWrapper>
|
||||||
aria-label={`${intl.formatMessage(intlMessages.prev)} ${intl.formatMessage(intlMessages.ariaTitle)}`}
|
<Styled.Helper>
|
||||||
onClick={handlePaginationClick.bind(this, 'prev')}
|
<ConnectionStatusHelper closeModal={() => closeModal(dataSaving, intl)} />
|
||||||
>
|
</Styled.Helper>
|
||||||
<Styled.Chevron
|
</Styled.HelperWrapper>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<Styled.NetworkDataContent>
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M15 19l-7-7 7-7"
|
|
||||||
/>
|
|
||||||
</Styled.Chevron>
|
|
||||||
</Styled.ButtonLeft>
|
|
||||||
</Styled.Prev>
|
|
||||||
<Styled.Helper page={dataPage}>
|
|
||||||
<ConnectionStatusHelper closeModal={() => closeModal(dataSaving, intl)} />
|
|
||||||
</Styled.Helper>
|
|
||||||
<Styled.NetworkDataContent page={dataPage}>
|
|
||||||
<Styled.DataColumn>
|
<Styled.DataColumn>
|
||||||
<Styled.NetworkData>
|
<Styled.NetworkData>
|
||||||
<div>{`${audioUploadLabel}`}</div>
|
<div>{`${audioUploadLabel}`}</div>
|
||||||
@ -582,28 +559,6 @@ class ConnectionStatusComponent extends PureComponent {
|
|||||||
</Styled.NetworkData>
|
</Styled.NetworkData>
|
||||||
</Styled.DataColumn>
|
</Styled.DataColumn>
|
||||||
</Styled.NetworkDataContent>
|
</Styled.NetworkDataContent>
|
||||||
<Styled.Next>
|
|
||||||
<Styled.ButtonRight
|
|
||||||
role="button"
|
|
||||||
disabled={dataPage === '2'}
|
|
||||||
aria-label={`${intl.formatMessage(intlMessages.next)} ${intl.formatMessage(intlMessages.ariaTitle)}`}
|
|
||||||
onClick={handlePaginationClick.bind(this, 'next')}
|
|
||||||
>
|
|
||||||
<Styled.Chevron
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M9 5l7 7-7 7"
|
|
||||||
/>
|
|
||||||
</Styled.Chevron>
|
|
||||||
</Styled.ButtonRight>
|
|
||||||
</Styled.Next>
|
|
||||||
</Styled.NetworkDataContainer>
|
</Styled.NetworkDataContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -619,81 +574,23 @@ class ConnectionStatusComponent extends PureComponent {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { intl } = this.props;
|
const { hasNetworkData, copyButtonText } = this.state;
|
||||||
|
|
||||||
const { hasNetworkData } = this.state;
|
|
||||||
return (
|
return (
|
||||||
<Styled.CopyContainer aria-live="polite">
|
<Styled.CopyContainer aria-live="polite">
|
||||||
<Styled.Copy
|
<Styled.Copy
|
||||||
disabled={!hasNetworkData}
|
disabled={!hasNetworkData}
|
||||||
role="button"
|
role="button"
|
||||||
data-test="copyStats"
|
data-test="copyStats"
|
||||||
onClick={this.copyNetworkData.bind(this)}
|
onClick={this.copyNetworkData.bind(this)}
|
||||||
onKeyPress={this.copyNetworkData.bind(this)}
|
onKeyPress={this.copyNetworkData.bind(this)}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
{this.state.copyButtonText}
|
{copyButtonText}
|
||||||
</Styled.Copy>
|
</Styled.Copy>
|
||||||
</Styled.CopyContainer>
|
</Styled.CopyContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The navigation bar.
|
|
||||||
* @returns {Object} The component to be renderized.
|
|
||||||
*/
|
|
||||||
renderNavigation() {
|
|
||||||
const { intl } = this.props;
|
|
||||||
|
|
||||||
const handleTabClick = (event) => {
|
|
||||||
const activeTabElement = document.querySelector('.activeConnectionStatusTab');
|
|
||||||
const { target } = event;
|
|
||||||
|
|
||||||
if (activeTabElement) {
|
|
||||||
activeTabElement.classList.remove('activeConnectionStatusTab');
|
|
||||||
}
|
|
||||||
|
|
||||||
target.classList.add('activeConnectionStatusTab');
|
|
||||||
this.setState({
|
|
||||||
selectedTab: target.dataset.tab,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Styled.Navigation>
|
|
||||||
<div
|
|
||||||
data-tab="1"
|
|
||||||
className="activeConnectionStatusTab"
|
|
||||||
onClick={handleTabClick}
|
|
||||||
onKeyDown={handleTabClick}
|
|
||||||
role="button"
|
|
||||||
>
|
|
||||||
{intl.formatMessage(intlMessages.connectionStats)}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
data-tab="2"
|
|
||||||
onClick={handleTabClick}
|
|
||||||
onKeyDown={handleTabClick}
|
|
||||||
role="button"
|
|
||||||
>
|
|
||||||
{intl.formatMessage(intlMessages.myLogs)}
|
|
||||||
</div>
|
|
||||||
{Service.isModerator()
|
|
||||||
&& (
|
|
||||||
<div
|
|
||||||
data-tab="3"
|
|
||||||
onClick={handleTabClick}
|
|
||||||
onKeyDown={handleTabClick}
|
|
||||||
role="button"
|
|
||||||
>
|
|
||||||
{intl.formatMessage(intlMessages.sessionLogs)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</Styled.Navigation>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
closeModal,
|
closeModal,
|
||||||
@ -715,18 +612,43 @@ class ConnectionStatusComponent extends PureComponent {
|
|||||||
{intl.formatMessage(intlMessages.title)}
|
{intl.formatMessage(intlMessages.title)}
|
||||||
</Styled.Title>
|
</Styled.Title>
|
||||||
</Styled.Header>
|
</Styled.Header>
|
||||||
{this.renderNavigation()}
|
|
||||||
<Styled.Main>
|
<Styled.ConnectionTabs
|
||||||
<Styled.Body>
|
onSelect={this.handleSelectTab}
|
||||||
{selectedTab === '1'
|
selectedIndex={selectedTab}
|
||||||
? this.renderNetworkData()
|
>
|
||||||
: this.renderConnections()
|
<Styled.ConnectionTabList>
|
||||||
|
<Styled.ConnectionTabSelector selectedClassName="is-selected">
|
||||||
|
<span id="connection-status-tab">{intl.formatMessage(intlMessages.title)}</span>
|
||||||
|
</Styled.ConnectionTabSelector>
|
||||||
|
<Styled.ConnectionTabSelector selectedClassName="is-selected">
|
||||||
|
<span id="my-logs-tab">{intl.formatMessage(intlMessages.myLogs)}</span>
|
||||||
|
</Styled.ConnectionTabSelector>
|
||||||
|
{Service.isModerator()
|
||||||
|
&& (
|
||||||
|
<Styled.ConnectionTabSelector selectedClassName="is-selected">
|
||||||
|
<span id="session-logs-tab">{intl.formatMessage(intlMessages.sessionLogs)}</span>
|
||||||
|
</Styled.ConnectionTabSelector>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
</Styled.Body>
|
</Styled.ConnectionTabList>
|
||||||
{selectedTab === '1' &&
|
<Styled.ConnectionTabPanel selectedClassName="is-selected">
|
||||||
this.renderCopyDataButton()
|
<div>
|
||||||
|
{this.renderNetworkData()}
|
||||||
|
{this.renderCopyDataButton()}
|
||||||
|
</div>
|
||||||
|
</Styled.ConnectionTabPanel>
|
||||||
|
<Styled.ConnectionTabPanel selectedClassName="is-selected">
|
||||||
|
<div>{this.renderConnections()}</div>
|
||||||
|
</Styled.ConnectionTabPanel>
|
||||||
|
{Service.isModerator()
|
||||||
|
&& (
|
||||||
|
<Styled.ConnectionTabPanel selectedClassName="is-selected">
|
||||||
|
<div>{this.renderConnections()}</div>
|
||||||
|
</Styled.ConnectionTabPanel>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
</Styled.Main>
|
</Styled.ConnectionTabs>
|
||||||
</Styled.Container>
|
</Styled.Container>
|
||||||
</Styled.ConnectionStatusModal>
|
</Styled.ConnectionStatusModal>
|
||||||
);
|
);
|
||||||
|
@ -7,12 +7,13 @@ import {
|
|||||||
colorGrayLabel,
|
colorGrayLabel,
|
||||||
colorGrayLightest,
|
colorGrayLightest,
|
||||||
colorPrimary,
|
colorPrimary,
|
||||||
|
colorWhite,
|
||||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||||
import {
|
import {
|
||||||
smPaddingX,
|
smPaddingX,
|
||||||
smPaddingY,
|
smPaddingY,
|
||||||
|
mdPaddingY,
|
||||||
lgPaddingY,
|
lgPaddingY,
|
||||||
lgPaddingX,
|
|
||||||
titlePositionLeft,
|
titlePositionLeft,
|
||||||
mdPaddingX,
|
mdPaddingX,
|
||||||
borderSizeLarge,
|
borderSizeLarge,
|
||||||
@ -26,7 +27,14 @@ import {
|
|||||||
hasPhoneDimentions,
|
hasPhoneDimentions,
|
||||||
mediumDown,
|
mediumDown,
|
||||||
hasPhoneWidth,
|
hasPhoneWidth,
|
||||||
|
smallOnly,
|
||||||
} from '/imports/ui/stylesheets/styled-components/breakpoints';
|
} from '/imports/ui/stylesheets/styled-components/breakpoints';
|
||||||
|
import {
|
||||||
|
ScrollboxVertical,
|
||||||
|
} from '/imports/ui/stylesheets/styled-components/scrollable';
|
||||||
|
import {
|
||||||
|
Tab, Tabs, TabList, TabPanel,
|
||||||
|
} from 'react-tabs';
|
||||||
|
|
||||||
const Item = styled.div`
|
const Item = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -169,11 +177,23 @@ const Label = styled.span`
|
|||||||
margin-bottom: ${lgPaddingY};
|
margin-bottom: ${lgPaddingY};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const NetworkDataContainer = styled.div`
|
const NetworkDataContainer = styled(ScrollboxVertical)`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow: auto;
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
|
padding-bottom: 1.25rem;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(0,0,0,.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media ${mediumDown} {
|
@media ${mediumDown} {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
@ -202,6 +222,8 @@ const CopyContainer = styled.div`
|
|||||||
|
|
||||||
const ConnectionStatusModal = styled(Modal)`
|
const ConnectionStatusModal = styled(Modal)`
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
height: 28rem;
|
||||||
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
@ -262,6 +284,19 @@ const Copy = styled.span`
|
|||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const HelperWrapper = styled.div`
|
||||||
|
min-width: 12.5rem;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
@media ${mediumDown} {
|
||||||
|
flex: none;
|
||||||
|
width: 100%;
|
||||||
|
scroll-snap-align: start;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const Helper = styled.div`
|
const Helper = styled.div`
|
||||||
width: 12.5rem;
|
width: 12.5rem;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -271,12 +306,7 @@ const Helper = styled.div`
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding: .5rem;
|
||||||
@media ${mediumDown} {
|
|
||||||
${({ page }) => page === '1'
|
|
||||||
? 'display: flex;'
|
|
||||||
: 'display: none;'}
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const NetworkDataContent = styled.div`
|
const NetworkDataContent = styled.div`
|
||||||
@ -286,9 +316,9 @@ const NetworkDataContent = styled.div`
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
@media ${mediumDown} {
|
@media ${mediumDown} {
|
||||||
${({ page }) => page === '2'
|
flex: none;
|
||||||
? 'display: flex;'
|
width: 100%;
|
||||||
: 'display: none;'}
|
scroll-snap-align: start;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -302,132 +332,103 @@ const DataColumn = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Main = styled.div`
|
const ConnectionTabs = styled(Tabs)`
|
||||||
height: 19.5rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-flow: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
@media ${smallOnly} {
|
||||||
|
width: 100%;
|
||||||
|
flex-flow: column;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Body = styled.div`
|
const ConnectionTabList = styled(TabList)`
|
||||||
padding: ${jumboPaddingY} 0;
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
flex-grow: 1;
|
margin-bottom: .5rem;
|
||||||
overflow: auto;
|
|
||||||
position: relative;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Navigation = styled.div`
|
|
||||||
display: flex;
|
|
||||||
border: none;
|
border: none;
|
||||||
border-bottom: 1px solid ${colorOffWhite};
|
padding: 0;
|
||||||
user-select: none;
|
width: calc(100% / 3);
|
||||||
overflow-y: auto;
|
|
||||||
scrollbar-width: none;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
@media ${smallOnly} {
|
||||||
display: none;
|
width: 100%;
|
||||||
}
|
flex-flow: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
& :not(:last-child) {
|
|
||||||
margin: 0;
|
|
||||||
margin-right: ${lgPaddingX};
|
|
||||||
}
|
|
||||||
|
|
||||||
.activeConnectionStatusTab {
|
|
||||||
border: none;
|
|
||||||
border-bottom: 2px solid ${colorPrimary};
|
|
||||||
color: ${colorPrimary};
|
|
||||||
}
|
|
||||||
|
|
||||||
& * {
|
|
||||||
cursor: pointer;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
[dir="rtl"] & {
|
|
||||||
& :not(:last-child) {
|
|
||||||
margin: 0;
|
|
||||||
margin-left: ${lgPaddingX};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Prev = styled.div`
|
|
||||||
display: none;
|
|
||||||
margin: 0 .5rem 0 .25rem;
|
|
||||||
|
|
||||||
@media ${mediumDown} {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media ${hasPhoneWidth} {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Next = styled(Prev)`
|
const ConnectionTabPanel = styled(TabPanel)`
|
||||||
margin: 0 .25rem 0 .5rem;
|
display: none;
|
||||||
|
margin: 0 0 0 1rem;
|
||||||
@media ${hasPhoneWidth} {
|
height: 13rem;
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Button = styled.button`
|
|
||||||
flex: 0;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
color: inherit;
|
|
||||||
border-radius: 50%;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: .5;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: .75;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media ${hasPhoneWidth} {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
padding: .25rem;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ButtonLeft = styled(Button)`
|
|
||||||
left: calc(50% - 2rem);
|
|
||||||
|
|
||||||
[dir="rtl"] & {
|
[dir="rtl"] & {
|
||||||
left: calc(50%);
|
margin: 0 1rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-selected {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media ${smallOnly} {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ButtonRight = styled(Button)`
|
const ConnectionTabSelector = styled(Tab)`
|
||||||
right: calc(50% - 2rem);
|
|
||||||
|
|
||||||
[dir="rtl"] & {
|
|
||||||
right: calc(50%);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Chevron = styled.svg`
|
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 1rem;
|
flex-flow: row;
|
||||||
height: 1rem;
|
font-size: 0.9rem;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
justify-content: flex-start;
|
||||||
|
border: none !important;
|
||||||
|
padding: ${mdPaddingY} ${mdPaddingX};
|
||||||
|
|
||||||
[dir="rtl"] & {
|
border-radius: .2rem;
|
||||||
transform: rotate(180deg);
|
cursor: pointer;
|
||||||
|
margin-bottom: ${smPaddingY};
|
||||||
|
align-items: center;
|
||||||
|
flex-grow: 0;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
& > span {
|
||||||
|
min-width: 0;
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media ${smallOnly} {
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0 ${smPaddingX} 0 0;
|
||||||
|
& > i {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] & {
|
||||||
|
margin: 0 0 0 ${smPaddingX};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
border-bottom: 2px solid ${colorWhite};
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-selected {
|
||||||
|
border: none;
|
||||||
|
color: ${colorPrimary};
|
||||||
|
|
||||||
|
span {
|
||||||
|
border-bottom: 2px solid ${colorPrimary};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -461,14 +462,11 @@ export default {
|
|||||||
Copy,
|
Copy,
|
||||||
Helper,
|
Helper,
|
||||||
NetworkDataContent,
|
NetworkDataContent,
|
||||||
Main,
|
|
||||||
Body,
|
|
||||||
Navigation,
|
|
||||||
FullName,
|
FullName,
|
||||||
DataColumn,
|
DataColumn,
|
||||||
Prev,
|
HelperWrapper,
|
||||||
Next,
|
ConnectionTabs,
|
||||||
ButtonLeft,
|
ConnectionTabList,
|
||||||
ButtonRight,
|
ConnectionTabSelector,
|
||||||
Chevron,
|
ConnectionTabPanel,
|
||||||
};
|
};
|
||||||
|
@ -20,6 +20,10 @@ const intlMessages = defineMessages({
|
|||||||
id: 'app.endMeeting.contentWarning',
|
id: 'app.endMeeting.contentWarning',
|
||||||
description: 'end meeting content warning',
|
description: 'end meeting content warning',
|
||||||
},
|
},
|
||||||
|
confirmButtonLabel: {
|
||||||
|
id: 'app.endMeeting.yesLabel',
|
||||||
|
description: 'end meeting confirm button label',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { warnAboutUnsavedContentOnMeetingEnd } = Meteor.settings.public.app;
|
const { warnAboutUnsavedContentOnMeetingEnd } = Meteor.settings.public.app;
|
||||||
@ -58,6 +62,7 @@ class EndMeetingComponent extends PureComponent {
|
|||||||
description={description}
|
description={description}
|
||||||
confirmButtonColor="danger"
|
confirmButtonColor="danger"
|
||||||
confirmButtonDataTest="confirmEndMeeting"
|
confirmButtonDataTest="confirmEndMeeting"
|
||||||
|
confirmButtonLabel={intl.formatMessage(intlMessages.confirmButtonLabel)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -202,6 +202,7 @@ const CustomLayout = (props) => {
|
|||||||
}, INITIAL_INPUT_STATE),
|
}, INITIAL_INPUT_STATE),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Session.set('layoutReady', true);
|
||||||
throttledCalculatesLayout();
|
throttledCalculatesLayout();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -142,6 +142,7 @@ const PresentationFocusLayout = (props) => {
|
|||||||
}, INITIAL_INPUT_STATE),
|
}, INITIAL_INPUT_STATE),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Session.set('layoutReady', true);
|
||||||
throttledCalculatesLayout();
|
throttledCalculatesLayout();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -138,6 +138,7 @@ const SmartLayout = (props) => {
|
|||||||
}, INITIAL_INPUT_STATE),
|
}, INITIAL_INPUT_STATE),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Session.set('layoutReady', true);
|
||||||
throttledCalculatesLayout();
|
throttledCalculatesLayout();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -149,6 +149,7 @@ const VideoFocusLayout = (props) => {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Session.set('layoutReady', true);
|
||||||
throttledCalculatesLayout();
|
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 ShortcutHelpComponent from '/imports/ui/components/shortcut-help/component';
|
||||||
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
|
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
|
||||||
import FullscreenService from '/imports/ui/components/common/fullscreen-button/service';
|
import FullscreenService from '/imports/ui/components/common/fullscreen-button/service';
|
||||||
import { colorDanger } from '/imports/ui/stylesheets/styled-components/palette';
|
import { colorDanger, colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
|
||||||
import Styled from './styles';
|
import Styled from './styles';
|
||||||
import browserInfo from '/imports/utils/browserInfo';
|
import browserInfo from '/imports/utils/browserInfo';
|
||||||
import deviceInfo from '/imports/utils/deviceInfo';
|
import deviceInfo from '/imports/utils/deviceInfo';
|
||||||
@ -293,21 +293,7 @@ class SettingsDropdown extends PureComponent {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (allowedToEndMeeting && isMeteorConnected) {
|
|
||||||
this.menuItems.push(
|
|
||||||
{
|
|
||||||
key: 'list-item-end-meeting',
|
|
||||||
icon: 'application',
|
|
||||||
label: intl.formatMessage(intlMessages.endMeetingLabel),
|
|
||||||
description: intl.formatMessage(intlMessages.endMeetingDesc),
|
|
||||||
onClick: () => mountModal(<EndMeetingConfirmationContainer />),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allowLogoutSetting && isMeteorConnected) {
|
if (allowLogoutSetting && isMeteorConnected) {
|
||||||
const customStyles = { color: colorDanger };
|
|
||||||
|
|
||||||
this.menuItems.push(
|
this.menuItems.push(
|
||||||
{
|
{
|
||||||
key: 'list-item-logout',
|
key: 'list-item-logout',
|
||||||
@ -315,12 +301,26 @@ class SettingsDropdown extends PureComponent {
|
|||||||
icon: 'logout',
|
icon: 'logout',
|
||||||
label: intl.formatMessage(intlMessages.leaveSessionLabel),
|
label: intl.formatMessage(intlMessages.leaveSessionLabel),
|
||||||
description: intl.formatMessage(intlMessages.leaveSessionDesc),
|
description: intl.formatMessage(intlMessages.leaveSessionDesc),
|
||||||
customStyles,
|
|
||||||
onClick: () => this.leaveSession(),
|
onClick: () => this.leaveSession(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (allowedToEndMeeting && isMeteorConnected) {
|
||||||
|
const customStyles = { background: colorDanger, color: colorWhite };
|
||||||
|
|
||||||
|
this.menuItems.push(
|
||||||
|
{
|
||||||
|
key: 'list-item-end-meeting',
|
||||||
|
icon: 'application',
|
||||||
|
label: intl.formatMessage(intlMessages.endMeetingLabel),
|
||||||
|
description: intl.formatMessage(intlMessages.endMeetingDesc),
|
||||||
|
customStyles,
|
||||||
|
onClick: () => mountModal(<EndMeetingConfirmationContainer />),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return this.menuItems;
|
return this.menuItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,6 +45,7 @@ class TalkingIndicator extends PureComponent {
|
|||||||
talkers,
|
talkers,
|
||||||
amIModerator,
|
amIModerator,
|
||||||
moreThanMaxIndicators,
|
moreThanMaxIndicators,
|
||||||
|
users,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
if (!talkers) return null;
|
if (!talkers) return null;
|
||||||
|
|
||||||
@ -58,9 +59,13 @@ class TalkingIndicator extends PureComponent {
|
|||||||
callerName,
|
callerName,
|
||||||
} = talkers[`${id}`];
|
} = talkers[`${id}`];
|
||||||
|
|
||||||
|
const user = users[id];
|
||||||
|
|
||||||
|
const name = user?.name ?? callerName;
|
||||||
|
|
||||||
const ariaLabel = intl.formatMessage(talking
|
const ariaLabel = intl.formatMessage(talking
|
||||||
? intlMessages.isTalking : intlMessages.wasTalking, {
|
? intlMessages.isTalking : intlMessages.wasTalking, {
|
||||||
0: callerName,
|
0: name,
|
||||||
});
|
});
|
||||||
|
|
||||||
let icon = talking ? 'unmute' : 'blank';
|
let icon = talking ? 'unmute' : 'blank';
|
||||||
@ -68,7 +73,7 @@ class TalkingIndicator extends PureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Styled.TalkingIndicatorWrapper
|
<Styled.TalkingIndicatorWrapper
|
||||||
key={_.uniqueId(`${callerName}-`)}
|
key={_.uniqueId(`${name}-`)}
|
||||||
muted={muted}
|
muted={muted}
|
||||||
talking={talking}
|
talking={talking}
|
||||||
floor={floor}
|
floor={floor}
|
||||||
@ -84,11 +89,11 @@ class TalkingIndicator extends PureComponent {
|
|||||||
$spoke={!talking || undefined}
|
$spoke={!talking || undefined}
|
||||||
$muted={muted}
|
$muted={muted}
|
||||||
$isViewer={!amIModerator || undefined}
|
$isViewer={!amIModerator || undefined}
|
||||||
key={_.uniqueId(`${callerName}-`)}
|
key={_.uniqueId(`${name}-`)}
|
||||||
onClick={() => this.handleMuteUser(id)}
|
onClick={() => this.handleMuteUser(id)}
|
||||||
label={callerName}
|
label={name}
|
||||||
tooltipLabel={!muted && amIModerator
|
tooltipLabel={!muted && amIModerator
|
||||||
? `${intl.formatMessage(intlMessages.muteLabel)} ${callerName}`
|
? `${intl.formatMessage(intlMessages.muteLabel)} ${name}`
|
||||||
: null}
|
: null}
|
||||||
data-test={talking ? 'isTalking' : 'wasTalking'}
|
data-test={talking ? 'isTalking' : 'wasTalking'}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user