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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,8 +17,8 @@ case class MeetingProp(
isBreakout: Boolean,
disabledFeatures: Vector[String],
notifyRecordingIsOn: Boolean,
uploadExternalDescription: String,
uploadExternalUrl: String,
presentationUploadExternalDescription: String,
presentationUploadExternalUrl: String,
)
case class BreakoutProps(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,67 @@
package org.bigbluebutton.presentation;
import java.util.*;
import static org.bigbluebutton.presentation.FileTypeConstants.*;
public class MimeTypeUtils {
private static final String XLS = "application/vnd.ms-excel";
private static final String XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
private static final String DOC = "application/msword";
private static final String DOCX = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
private static final String PPT = "application/vnd.ms-powerpoint";
private static final String PPTX = "application/vnd.openxmlformats-officedocument.presentationml.presentation";
private static final String ODT = "application/vnd.oasis.opendocument.text";
private static final String RTF = "application/rtf";
private static final String TXT = "text/plain";
private static final String ODS = "application/vnd.oasis.opendocument.spreadsheet";
private static final String ODP = "application/vnd.oasis.opendocument.presentation";
private static final String PDF = "application/pdf";
private static final String JPEG = "image/jpeg";
private static final String PNG = "image/png";
private static final String SVG = "image/svg+xml";
private static final HashMap<String,String> EXTENSIONS_MIME = new HashMap<String,String>(16) {
{
// Add all the supported files
put(FileTypeConstants.XLS, XLS);
put(FileTypeConstants.XLSX, XLSX);
put(FileTypeConstants.DOC, DOC);
put(FileTypeConstants.DOCX, DOCX);
put(FileTypeConstants.PPT, PPT);
put(FileTypeConstants.PPTX, PPTX);
put(FileTypeConstants.ODT, ODT);
put(FileTypeConstants.RTF, RTF);
put(FileTypeConstants.TXT, TXT);
put(FileTypeConstants.ODS, ODS);
put(FileTypeConstants.ODP, ODP);
put(FileTypeConstants.PDF, PDF);
put(FileTypeConstants.JPG, JPEG);
put(FileTypeConstants.JPEG, JPEG);
put(FileTypeConstants.PNG, PNG);
put(FileTypeConstants.SVG, SVG);
}
};
public Boolean extensionMatchMimeType(String mimeType, String finalExtension) {
if(EXTENSIONS_MIME.containsKey(finalExtension.toLowerCase()) &&
EXTENSIONS_MIME.get(finalExtension.toLowerCase()).equalsIgnoreCase(mimeType)) {
return true;
} else if(EXTENSIONS_MIME.containsKey(finalExtension.toLowerCase() + 'x') &&
EXTENSIONS_MIME.get(finalExtension.toLowerCase() + 'x').equalsIgnoreCase(mimeType)) {
//Exception for MS Office files named with old extension but using internally the new mime type
//e.g. a file named with extension `ppt` but has the content of a `pptx`
return true;
}
return false;
}
public List<String> getValidMimeTypes() {
List<String> validMimeTypes = Arrays.asList(XLS, XLSX,
DOC, DOCX, PPT, PPTX, ODT, RTF, TXT, ODS, ODP,
PDF, JPEG, PNG, SVG
);
return validMimeTypes;
}
}

View File

@ -19,8 +19,15 @@
package org.bigbluebutton.presentation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.bigbluebutton.presentation.FileTypeConstants.*;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Collections;
@ -28,6 +35,9 @@ import java.util.Collections;
@SuppressWarnings("serial")
public final class SupportedFileTypes {
private static Logger log = LoggerFactory.getLogger(SupportedFileTypes.class);
private static MimeTypeUtils mimeTypeUtils = new MimeTypeUtils();
private static final List<String> SUPPORTED_FILE_LIST = Collections.unmodifiableList(new ArrayList<String>(15) {
{
// Add all the supported files
@ -76,4 +86,56 @@ public final class SupportedFileTypes {
public static boolean isImageFile(String fileExtension) {
return IMAGE_FILE_LIST.contains(fileExtension.toLowerCase());
}
/*
* It was tested native java methods to detect mimetypes, such as:
* - URLConnection.guessContentTypeFromStream(InputStream is);
* - Files.probeContentType(Path path);
* - FileNameMap fileNameMap.getContentTypeFor(String file.getName());
* - MimetypesFileTypeMap fileTypeMap.getContentType(File file);
* But none of them was as successful as the linux based command
*/
public static String detectMimeType(File pres) {
String mimeType = "";
if (pres != null && pres.isFile()){
try {
ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command("bash", "-c", "file -b --mime-type " + pres.getAbsolutePath());
Process process = processBuilder.start();
StringBuilder output = new StringBuilder();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
output.append(line + "\n");
}
int exitVal = process.waitFor();
if (exitVal == 0) {
mimeType = output.toString().trim();
} else {
log.error("Error while executing command {} for file {}, error: {}",
process.toString(), pres.getAbsolutePath(), process.getErrorStream());
}
} catch (IOException e) {
log.error("Could not read file [{}]", pres.getAbsolutePath(), e.getMessage());
} catch (InterruptedException e) {
log.error("Flow interrupted for file [{}]", pres.getAbsolutePath(), e.getMessage());
}
}
return mimeType;
}
public static Boolean isPresentationMimeTypeValid(File pres, String fileExtension) {
String mimeType = detectMimeType(pres);
if(mimeType == null || mimeType == "") return false;
if(!mimeTypeUtils.getValidMimeTypes().contains(mimeType)) return false;
if(!mimeTypeUtils.extensionMatchMimeType(mimeType, fileExtension)) {
log.error("File with extension [{}] doesn't match with mimeType [{}].", fileExtension, mimeType);
return false;
}
return true;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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