Merge branch 'v2.5.x-release' into default-nginx-config
This commit is contained in:
commit
fd5d3cbfac
@ -1,2 +0,0 @@
|
||||
Dockerfile
|
||||
|
1
akka-bbb-apps/.gitignore
vendored
1
akka-bbb-apps/.gitignore
vendored
@ -48,4 +48,5 @@ lib_managed/
|
||||
.cache
|
||||
bin/
|
||||
src/main/resources/
|
||||
.bsp/
|
||||
|
||||
|
@ -1,24 +0,0 @@
|
||||
FROM bbb-common-message AS builder
|
||||
|
||||
ARG COMMON_VERSION=0.0.1-SNAPSHOT
|
||||
|
||||
COPY . /source
|
||||
|
||||
RUN cd /source \
|
||||
&& find -name build.sbt -exec sed -i "s|\(.*org.bigbluebutton.*bbb-common-message[^\"]*\"[ ]*%[ ]*\)\"[^\"]*\"\(.*\)|\1\"$COMMON_VERSION\"\2|g" {} \; \
|
||||
&& sbt compile
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install fakeroot
|
||||
|
||||
RUN cd /source \
|
||||
&& sbt debian:packageBin
|
||||
|
||||
# FROM ubuntu:16.04
|
||||
FROM openjdk:8-jre-slim-stretch
|
||||
|
||||
COPY --from=builder /source/target/*.deb /root/
|
||||
|
||||
RUN dpkg -i /root/*.deb
|
||||
|
||||
CMD ["/usr/share/bbb-apps-akka/bin/bbb-apps-akka"]
|
@ -15,12 +15,13 @@ trait PresentationConversionCompletedSysPubMsgHdlr {
|
||||
): MeetingState2x = {
|
||||
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
val temporaryPresentationId = msg.body.presentation.temporaryPresentationId
|
||||
|
||||
val newState = for {
|
||||
pod <- PresentationPodsApp.getPresentationPod(state, msg.body.podId)
|
||||
pres <- pod.getPresentation(msg.body.presentation.id)
|
||||
} yield {
|
||||
val presVO = PresentationPodsApp.translatePresentationToPresentationVO(pres)
|
||||
val presVO = PresentationPodsApp.translatePresentationToPresentationVO(pres, temporaryPresentationId)
|
||||
|
||||
PresentationSender.broadcastPresentationConversionCompletedEvtMsg(
|
||||
bus,
|
||||
|
@ -58,7 +58,7 @@ object PresentationPodsApp {
|
||||
)
|
||||
}
|
||||
|
||||
PresentationVO(p.id, p.name, p.current,
|
||||
PresentationVO(p.id, "", p.name, p.current,
|
||||
pages.toVector, p.downloadable, p.removable)
|
||||
}
|
||||
|
||||
@ -74,7 +74,7 @@ object PresentationPodsApp {
|
||||
state.update(podManager)
|
||||
}
|
||||
|
||||
def translatePresentationToPresentationVO(pres: PresentationInPod): PresentationVO = {
|
||||
def translatePresentationToPresentationVO(pres: PresentationInPod, temporaryPresentationId: String): PresentationVO = {
|
||||
val pages = pres.pages.values.map { page =>
|
||||
PageVO(
|
||||
id = page.id,
|
||||
@ -90,7 +90,7 @@ object PresentationPodsApp {
|
||||
heightRatio = page.heightRatio
|
||||
)
|
||||
}
|
||||
PresentationVO(pres.id, pres.name, pres.current, pages.toVector, pres.downloadable, pres.removable)
|
||||
PresentationVO(pres.id, temporaryPresentationId, pres.name, pres.current, pages.toVector, pres.downloadable, pres.removable)
|
||||
}
|
||||
|
||||
def setCurrentPresentationInPod(state: MeetingState2x, podId: String, nextCurrentPresId: String): Option[PresentationPod] = {
|
||||
|
@ -19,7 +19,7 @@ trait PresentationUploadTokenReqMsgHdlr extends RightsManagementTrait {
|
||||
val envelope = BbbCoreEnvelope(PresentationUploadTokenPassRespMsg.NAME, routing)
|
||||
val header = BbbClientMsgHeader(PresentationUploadTokenPassRespMsg.NAME, liveMeeting.props.meetingProp.intId, msg.header.userId)
|
||||
|
||||
val body = PresentationUploadTokenPassRespMsgBody(msg.body.podId, token, msg.body.filename)
|
||||
val body = PresentationUploadTokenPassRespMsgBody(msg.body.podId, token, msg.body.filename, msg.body.tmpPresId)
|
||||
val event = PresentationUploadTokenPassRespMsg(header, body)
|
||||
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
|
||||
bus.outGW.send(msgEvent)
|
||||
|
@ -50,7 +50,6 @@ trait SelectRandomViewerReqMsgHdlr extends RightsManagementTrait {
|
||||
Users2x.setUserExempted(liveMeeting.users2x, pickedUser, true)
|
||||
}
|
||||
}
|
||||
|
||||
val userIds = users.map { case (v) => v.intId }
|
||||
broadcastEvent(msg, userIds, pickedUser)
|
||||
}
|
||||
|
@ -15,12 +15,12 @@ trait UserJoinMeetingAfterReconnectReqMsgHdlr extends HandlerHelpers with UserJo
|
||||
|
||||
def handleUserJoinMeetingAfterReconnectReqMsg(msg: UserJoinMeetingAfterReconnectReqMsg, state: MeetingState2x): MeetingState2x = {
|
||||
log.info("Received user joined after reconnecting. user {} meetingId={}", msg.body.userId, msg.header.meetingId)
|
||||
|
||||
Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId) match {
|
||||
case Some(reconnectingUser) =>
|
||||
if (reconnectingUser.userLeftFlag.left) {
|
||||
log.info("Resetting flag that user left meeting. user {}", msg.body.userId)
|
||||
// User has reconnected. Just reset it's flag. ralam Oct 23, 2018
|
||||
sendUserLeftFlagUpdatedEvtMsg(outGW, liveMeeting, msg.body.userId, false)
|
||||
Users2x.resetUserLeftFlag(liveMeeting.users2x, msg.body.userId)
|
||||
}
|
||||
state
|
||||
|
@ -2,8 +2,8 @@ package org.bigbluebutton.core.apps.users
|
||||
|
||||
import org.bigbluebutton.common2.msgs.UserJoinMeetingReqMsg
|
||||
import org.bigbluebutton.core.apps.breakout.BreakoutHdlrHelpers
|
||||
import org.bigbluebutton.core.models.{ Users2x, VoiceUsers }
|
||||
import org.bigbluebutton.core.domain.MeetingState2x
|
||||
import org.bigbluebutton.core.models.{Users2x, VoiceUsers}
|
||||
import org.bigbluebutton.core.running.{HandlerHelpers, LiveMeeting, MeetingActor, OutMsgRouter}
|
||||
|
||||
trait UserJoinMeetingReqMsgHdlr extends HandlerHelpers {
|
||||
@ -20,8 +20,10 @@ trait UserJoinMeetingReqMsgHdlr extends HandlerHelpers {
|
||||
if (reconnectingUser.userLeftFlag.left) {
|
||||
log.info("Resetting flag that user left meeting. user {}", msg.body.userId)
|
||||
// User has reconnected. Just reset it's flag. ralam Oct 23, 2018
|
||||
sendUserLeftFlagUpdatedEvtMsg(outGW, liveMeeting, msg.body.userId, false)
|
||||
Users2x.resetUserLeftFlag(liveMeeting.users2x, msg.body.userId)
|
||||
}
|
||||
|
||||
state
|
||||
case None =>
|
||||
val newState = userJoinMeeting(outGW, msg.body.authToken, msg.body.clientType, liveMeeting, state)
|
||||
|
@ -3,9 +3,9 @@ package org.bigbluebutton.core.apps.users
|
||||
import org.bigbluebutton.common2.msgs.UserLeaveReqMsg
|
||||
import org.bigbluebutton.core.domain.MeetingState2x
|
||||
import org.bigbluebutton.core.models.{ RegisteredUsers, Users2x }
|
||||
import org.bigbluebutton.core.running.{ MeetingActor, OutMsgRouter }
|
||||
import org.bigbluebutton.core.running.{ HandlerHelpers, MeetingActor, OutMsgRouter }
|
||||
|
||||
trait UserLeaveReqMsgHdlr {
|
||||
trait UserLeaveReqMsgHdlr extends HandlerHelpers {
|
||||
this: MeetingActor =>
|
||||
|
||||
val outGW: OutMsgRouter
|
||||
@ -19,6 +19,8 @@ trait UserLeaveReqMsgHdlr {
|
||||
// Just flag that user has left as the user might be reconnecting.
|
||||
// An audit will remove this user if it hasn't rejoined after a certain period of time.
|
||||
// ralam oct 23, 2018
|
||||
sendUserLeftFlagUpdatedEvtMsg(outGW, liveMeeting, msg.body.userId, true)
|
||||
|
||||
Users2x.setUserLeftFlag(liveMeeting.users2x, msg.body.userId)
|
||||
}
|
||||
if (msg.body.loggedOut) {
|
||||
|
@ -2,6 +2,7 @@ package org.bigbluebutton.core.models
|
||||
|
||||
import com.softwaremill.quicklens._
|
||||
import org.bigbluebutton.core.util.TimeUtil
|
||||
import org.bigbluebutton.core2.message.senders.MsgBuilder
|
||||
|
||||
object Users2x {
|
||||
def findWithIntId(users: Users2x, intId: String): Option[UserState] = {
|
||||
|
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.core.record.events
|
||||
|
||||
class MeetingConfigurationEvent extends StarterConfigurationEvent {
|
||||
import MeetingConfigurationEvent._
|
||||
|
||||
setEvent("MeetingConfigurationEvent")
|
||||
|
||||
def setWebcamsOnlyForModerator(webcamsOnlyForModerator: Boolean) {
|
||||
eventMap.put(WEBCAMS_ONLY_FOR_MODERATOR, webcamsOnlyForModerator.toString)
|
||||
}
|
||||
}
|
||||
|
||||
object MeetingConfigurationEvent {
|
||||
val WEBCAMS_ONLY_FOR_MODERATOR = "webcamsOnlyForModerator"
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.core.record.events
|
||||
|
||||
trait StarterConfigurationEvent extends RecordEvent {
|
||||
setModule("CONFIG")
|
||||
}
|
@ -25,6 +25,20 @@ trait HandlerHelpers extends SystemConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
def sendUserLeftFlagUpdatedEvtMsg(
|
||||
outGW: OutMsgRouter,
|
||||
liveMeeting: LiveMeeting,
|
||||
intId: String,
|
||||
leftFlag: Boolean
|
||||
): Unit = {
|
||||
for {
|
||||
u <- Users2x.findWithIntId(liveMeeting.users2x, intId)
|
||||
} yield {
|
||||
val userLeftFlagMeetingEvent = MsgBuilder.buildUserLeftFlagUpdatedEvtMsg(liveMeeting.props.meetingProp.intId, u.intId, leftFlag)
|
||||
outGW.send(userLeftFlagMeetingEvent)
|
||||
}
|
||||
}
|
||||
|
||||
def userJoinMeeting(outGW: OutMsgRouter, authToken: String, clientType: String,
|
||||
liveMeeting: LiveMeeting, state: MeetingState2x): MeetingState2x = {
|
||||
|
||||
|
@ -286,6 +286,16 @@ object MsgBuilder {
|
||||
BbbCommonEnvCoreMsg(envelope, event)
|
||||
}
|
||||
|
||||
def buildUserLeftFlagUpdatedEvtMsg(meetingId: String, userId: String, userLeftFlag: Boolean): BbbCommonEnvCoreMsg = {
|
||||
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId)
|
||||
val envelope = BbbCoreEnvelope(UserLeftFlagUpdatedEvtMsg.NAME, routing)
|
||||
val header = BbbClientMsgHeader(UserLeftFlagUpdatedEvtMsg.NAME, meetingId, userId)
|
||||
val body = UserLeftFlagUpdatedEvtMsgBody(userId, userLeftFlag)
|
||||
val event = UserLeftFlagUpdatedEvtMsg(header, body)
|
||||
|
||||
BbbCommonEnvCoreMsg(envelope, event)
|
||||
}
|
||||
|
||||
def buildUserInactivityInspectMsg(meetingId: String, userId: String, responseDelay: Long): BbbCommonEnvCoreMsg = {
|
||||
val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, meetingId, userId)
|
||||
val envelope = BbbCoreEnvelope(UserInactivityInspectMsg.NAME, routing)
|
||||
|
@ -116,6 +116,7 @@ class RedisRecorderActor(
|
||||
case m: RecordStatusResetSysMsg => handleRecordStatusResetSysMsg(m)
|
||||
case m: WebcamsOnlyForModeratorChangedEvtMsg => handleWebcamsOnlyForModeratorChangedEvtMsg(m)
|
||||
case m: MeetingEndingEvtMsg => handleEndAndKickAllSysMsg(m)
|
||||
case m: MeetingCreatedEvtMsg => handleStarterConfigurations(m)
|
||||
|
||||
// Recording
|
||||
case m: RecordingChapterBreakSysMsg => handleRecordingChapterBreakSysMsg(m)
|
||||
@ -622,4 +623,10 @@ class RedisRecorderActor(
|
||||
log.error("recording database is not available.")
|
||||
}
|
||||
|
||||
private def handleStarterConfigurations(msg: MeetingCreatedEvtMsg): Unit = {
|
||||
val ev = new MeetingConfigurationEvent()
|
||||
ev.setWebcamsOnlyForModerator(msg.body.props.usersProp.webcamsOnlyForModerator)
|
||||
record(msg.body.props.meetingProp.intId, ev.toMap().asJava)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,26 +0,0 @@
|
||||
FROM bbb-fsesl-client AS builder
|
||||
|
||||
ARG COMMON_VERSION=0.0.1-SNAPSHOT
|
||||
|
||||
COPY . /source
|
||||
|
||||
RUN cd /source \
|
||||
&& find -name build.sbt -exec sed -i "s|\(.*org.bigbluebutton.*bbb-common-message[^\"]*\"[ ]*%[ ]*\)\"[^\"]*\"\(.*\)|\1\"$COMMON_VERSION\"\2|g" {} \; \
|
||||
&& find -name build.sbt -exec sed -i "s|\(.*org.bigbluebutton.*bbb-fsesl-client[^\"]*\"[ ]*%[ ]*\)\"[^\"]*\"\(.*\)|\1\"$COMMON_VERSION\"\2|g" {} \; \
|
||||
&& sbt compile
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install fakeroot
|
||||
|
||||
RUN cd /source \
|
||||
&& sbt debian:packageBin
|
||||
|
||||
FROM openjdk:8-jre-slim-stretch
|
||||
|
||||
COPY --from=builder /source/target/*.deb /root/
|
||||
|
||||
RUN dpkg -i /root/*.deb
|
||||
|
||||
COPY wait-for-it.sh /usr/local/bin/
|
||||
|
||||
CMD ["/usr/share/bbb-fsesl-akka/bin/bbb-fsesl-akka"]
|
1
bbb-common-message/.gitignore
vendored
1
bbb-common-message/.gitignore
vendored
@ -53,4 +53,5 @@ akka-patterns-store/
|
||||
lib_managed/
|
||||
.cache
|
||||
bin/
|
||||
.bsp/
|
||||
|
||||
|
@ -1,13 +0,0 @@
|
||||
FROM sbt:0.13.8
|
||||
|
||||
ARG COMMON_VERSION
|
||||
|
||||
COPY . /bbb-common-message
|
||||
|
||||
RUN cd /bbb-common-message \
|
||||
&& sed -i "s|\(version := \)\".*|\1\"$COMMON_VERSION\"|g" build.sbt \
|
||||
&& echo 'publishTo := Some(Resolver.file("file", new File(Path.userHome.absolutePath+"/.m2/repository")))' | tee -a build.sbt \
|
||||
&& sbt compile \
|
||||
&& sbt publish \
|
||||
&& sbt publishLocal
|
||||
|
@ -1,6 +1,6 @@
|
||||
package org.bigbluebutton.common2.domain
|
||||
|
||||
case class PresentationVO(id: String, name: String, current: Boolean = false,
|
||||
case class PresentationVO(id: String, temporaryPresentationId: String, name: String, current: Boolean = false,
|
||||
pages: Vector[PageVO], downloadable: Boolean, removable: Boolean)
|
||||
|
||||
case class PageVO(id: String, num: Int, thumbUri: String = "", swfUri: String,
|
||||
|
@ -13,7 +13,7 @@ case class RemovePresentationPodPubMsgBody(podId: String)
|
||||
|
||||
object PresentationUploadTokenReqMsg { val NAME = "PresentationUploadTokenReqMsg" }
|
||||
case class PresentationUploadTokenReqMsg(header: BbbClientMsgHeader, body: PresentationUploadTokenReqMsgBody) extends StandardMsg
|
||||
case class PresentationUploadTokenReqMsgBody(podId: String, filename: String)
|
||||
case class PresentationUploadTokenReqMsgBody(podId: String, filename: String, tmpPresId: String)
|
||||
|
||||
object GetAllPresentationPodsReqMsg { val NAME = "GetAllPresentationPodsReqMsg" }
|
||||
case class GetAllPresentationPodsReqMsg(header: BbbClientMsgHeader, body: GetAllPresentationPodsReqMsgBody) extends StandardMsg
|
||||
@ -115,6 +115,7 @@ case class PresentationConversionRequestReceivedSysMsg(
|
||||
case class PresentationConversionRequestReceivedSysMsgBody(
|
||||
podId: String,
|
||||
presentationId: String,
|
||||
temporaryPresentationId: String,
|
||||
current: Boolean,
|
||||
presName: String,
|
||||
downloadable: Boolean,
|
||||
@ -181,7 +182,7 @@ case class PdfConversionInvalidErrorEvtMsgBody(podId: String, messageKey: String
|
||||
|
||||
object PresentationUploadTokenPassRespMsg { val NAME = "PresentationUploadTokenPassRespMsg" }
|
||||
case class PresentationUploadTokenPassRespMsg(header: BbbClientMsgHeader, body: PresentationUploadTokenPassRespMsgBody) extends StandardMsg
|
||||
case class PresentationUploadTokenPassRespMsgBody(podId: String, authzToken: String, filename: String)
|
||||
case class PresentationUploadTokenPassRespMsgBody(podId: String, authzToken: String, filename: String, tmpPresId: String)
|
||||
|
||||
object PresentationUploadTokenFailRespMsg { val NAME = "PresentationUploadTokenFailRespMsg" }
|
||||
case class PresentationUploadTokenFailRespMsg(header: BbbClientMsgHeader, body: PresentationUploadTokenFailRespMsgBody) extends StandardMsg
|
||||
|
@ -70,6 +70,20 @@ case class UserLeftMeetingEvtMsg(
|
||||
) extends BbbCoreMsg
|
||||
case class UserLeftMeetingEvtMsgBody(intId: String, eject: Boolean, ejectedBy: String, reason: String, reasonCode: String)
|
||||
|
||||
object UserLeftFlagUpdatedEvtMsg {
|
||||
val NAME = "UserLeftFlagUpdatedEvtMsg"
|
||||
def apply(meetingId: String, userId: String, body: UserLeftFlagUpdatedEvtMsgBody): UserLeftFlagUpdatedEvtMsg = {
|
||||
val header = BbbClientMsgHeader(UserLeftFlagUpdatedEvtMsg.NAME, meetingId, userId)
|
||||
UserLeftFlagUpdatedEvtMsg(header, body)
|
||||
}
|
||||
}
|
||||
|
||||
case class UserLeftFlagUpdatedEvtMsg(
|
||||
header: BbbClientMsgHeader,
|
||||
body: UserLeftFlagUpdatedEvtMsgBody
|
||||
) extends BbbCoreMsg
|
||||
case class UserLeftFlagUpdatedEvtMsgBody(intId: String, userLeftFlag: Boolean)
|
||||
|
||||
object UserJoinedMeetingEvtMsg {
|
||||
val NAME = "UserJoinedMeetingEvtMsg"
|
||||
def apply(meetingId: String, userId: String, body: UserJoinedMeetingEvtMsgBody): UserJoinedMeetingEvtMsg = {
|
||||
|
1
bbb-common-web/.gitignore
vendored
1
bbb-common-web/.gitignore
vendored
@ -53,4 +53,5 @@ akka-patterns-store/
|
||||
lib_managed/
|
||||
.cache
|
||||
bin/
|
||||
.bsp/
|
||||
|
||||
|
@ -1,13 +0,0 @@
|
||||
FROM bbb-common-message
|
||||
|
||||
ARG COMMON_VERSION
|
||||
|
||||
COPY . /bbb-common-web
|
||||
|
||||
RUN cd /bbb-common-web \
|
||||
&& sed -i "s|\(version := \)\".*|\1\"$COMMON_VERSION\"|g" build.sbt \
|
||||
&& find -name build.sbt -exec sed -i "s|\(.*org.bigbluebutton.*bbb-common-message[^\"]*\"[ ]*%[ ]*\)\"[^\"]*\"\(.*\)|\1\"$COMMON_VERSION\"\2|g" {} \; \
|
||||
&& echo 'publishTo := Some(Resolver.file("file", new File(Path.userHome.absolutePath+"/.m2/repository")))' | tee -a build.sbt \
|
||||
&& sbt compile \
|
||||
&& sbt publish \
|
||||
&& sbt publishLocal
|
@ -60,6 +60,7 @@ import org.bigbluebutton.api.messaging.converters.messages.DeletedRecordingMessa
|
||||
import org.bigbluebutton.api.messaging.messages.*;
|
||||
import org.bigbluebutton.api2.IBbbWebApiGWApp;
|
||||
import org.bigbluebutton.api2.domain.UploadedTrack;
|
||||
import org.bigbluebutton.common2.msgs.MeetingCreatedEvtMsg;
|
||||
import org.bigbluebutton.common2.redis.RedisStorageService;
|
||||
import org.bigbluebutton.presentation.PresentationUrlDownloadService;
|
||||
import org.bigbluebutton.presentation.imp.SwfSlidesGenerationProgressNotifier;
|
||||
|
@ -2,6 +2,7 @@ package org.bigbluebutton.api;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@ -45,7 +46,8 @@ public final class Util {
|
||||
|
||||
public static String generatePresentationId(String presFilename) {
|
||||
long timestamp = System.currentTimeMillis();
|
||||
return DigestUtils.sha1Hex(presFilename) + "-" + timestamp;
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
return DigestUtils.sha1Hex(presFilename + uuid) + "-" + timestamp;
|
||||
}
|
||||
|
||||
public static String createNewFilename(String presId, String fileExt) {
|
||||
|
@ -49,8 +49,8 @@ public class Meeting {
|
||||
private boolean forciblyEnded = false;
|
||||
private String telVoice;
|
||||
private String webVoice;
|
||||
private String moderatorPass;
|
||||
private String viewerPass;
|
||||
private String moderatorPass = "";
|
||||
private String viewerPass = "";
|
||||
private int learningDashboardCleanupDelayInMinutes;
|
||||
private String learningDashboardAccessToken;
|
||||
private ArrayList<String> disabledFeatures;
|
||||
@ -115,9 +115,17 @@ public class Meeting {
|
||||
name = builder.name;
|
||||
extMeetingId = builder.externalId;
|
||||
intMeetingId = builder.internalId;
|
||||
viewerPass = builder.viewerPass;
|
||||
moderatorPass = builder.moderatorPass;
|
||||
disabledFeatures = builder.disabledFeatures;
|
||||
if (builder.viewerPass == null){
|
||||
viewerPass = "";
|
||||
} else {
|
||||
viewerPass = builder.viewerPass;
|
||||
}
|
||||
if (builder.moderatorPass == null){
|
||||
moderatorPass = "";
|
||||
} else {
|
||||
moderatorPass = builder.moderatorPass;
|
||||
}
|
||||
learningDashboardCleanupDelayInMinutes = builder.learningDashboardCleanupDelayInMinutes;
|
||||
learningDashboardAccessToken = builder.learningDashboardAccessToken;
|
||||
maxUsers = builder.maxUsers;
|
||||
|
@ -1,22 +0,0 @@
|
||||
package org.bigbluebutton.api.model.constraint;
|
||||
|
||||
import org.bigbluebutton.api.model.validator.ModeratorPasswordValidator;
|
||||
|
||||
import javax.validation.Constraint;
|
||||
import javax.validation.Payload;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.TYPE;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@Constraint(validatedBy = ModeratorPasswordValidator.class)
|
||||
@Target(TYPE)
|
||||
@Retention(RUNTIME)
|
||||
public @interface ModeratorPasswordConstraint {
|
||||
|
||||
String key() default "invalidPassword";
|
||||
String message() default "The supplied moderator password is incorrect";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
@ -29,7 +29,6 @@ public class EndMeeting extends RequestWithChecksum<EndMeeting.Params> {
|
||||
private String meetingID;
|
||||
|
||||
@PasswordConstraint
|
||||
@NotEmpty(message = "You must provide the moderator password")
|
||||
private String password;
|
||||
|
||||
@Valid
|
||||
|
@ -38,7 +38,6 @@ public class JoinMeeting extends RequestWithChecksum<JoinMeeting.Params> {
|
||||
private String fullName;
|
||||
|
||||
@PasswordConstraint
|
||||
@NotEmpty(key = "invalidPassword", message = "You must provide either the moderator or attendee password")
|
||||
private String password;
|
||||
|
||||
@IsBooleanConstraint(message = "Guest must be a boolean value (true or false)")
|
||||
|
@ -1,6 +1,3 @@
|
||||
package org.bigbluebutton.api.model.shared;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.ModeratorPasswordConstraint;
|
||||
|
||||
@ModeratorPasswordConstraint
|
||||
public class ModeratorPassword extends Password {}
|
||||
|
@ -7,7 +7,6 @@ public abstract class Password {
|
||||
@NotEmpty(message = "You must provide the meeting ID")
|
||||
protected String meetingID;
|
||||
|
||||
@NotEmpty(message = "You must provide the password for the call")
|
||||
protected String password;
|
||||
|
||||
public String getMeetingID() {
|
||||
|
@ -36,18 +36,10 @@ public class JoinPasswordValidator implements ConstraintValidator<JoinPasswordCo
|
||||
String attendeePassword = meeting.getViewerPassword();
|
||||
String providedPassword = joinPassword.getPassword();
|
||||
|
||||
if(providedPassword == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
log.info("Moderator password: {}", moderatorPassword);
|
||||
log.info("Attendee password: {}", attendeePassword);
|
||||
log.info("Provided password: {}", providedPassword);
|
||||
|
||||
if(!providedPassword.equals(moderatorPassword) && !providedPassword.equals(attendeePassword)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1,52 +0,0 @@
|
||||
package org.bigbluebutton.api.model.validator;
|
||||
|
||||
import org.bigbluebutton.api.domain.Meeting;
|
||||
import org.bigbluebutton.api.model.constraint.ModeratorPasswordConstraint;
|
||||
import org.bigbluebutton.api.model.shared.ModeratorPassword;
|
||||
import org.bigbluebutton.api.service.ServiceUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.validation.ConstraintValidator;
|
||||
import javax.validation.ConstraintValidatorContext;
|
||||
|
||||
public class ModeratorPasswordValidator implements ConstraintValidator<ModeratorPasswordConstraint, ModeratorPassword> {
|
||||
|
||||
private static Logger log = LoggerFactory.getLogger(ModeratorPasswordValidator.class);
|
||||
|
||||
|
||||
@Override
|
||||
public void initialize(ModeratorPasswordConstraint constraintAnnotation) {}
|
||||
|
||||
@Override
|
||||
public boolean isValid(ModeratorPassword moderatorPassword, ConstraintValidatorContext context) {
|
||||
log.info("Validating password {} for meeting with ID {}",
|
||||
moderatorPassword.getPassword(), moderatorPassword.getMeetingID());
|
||||
|
||||
if(moderatorPassword.getMeetingID() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Meeting meeting = ServiceUtils.findMeetingFromMeetingID(moderatorPassword.getMeetingID());
|
||||
|
||||
if(meeting == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String actualPassword = meeting.getModeratorPassword();
|
||||
String providedPassword = moderatorPassword.getPassword();
|
||||
|
||||
if(providedPassword == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
log.info("Actual password: {}", actualPassword);
|
||||
log.info("Provided password: {}", providedPassword);
|
||||
|
||||
if(!providedPassword.equals(actualPassword)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -19,16 +19,12 @@ public class PasswordValidator implements ConstraintValidator<PasswordConstraint
|
||||
@Override
|
||||
public boolean isValid(String password, ConstraintValidatorContext context) {
|
||||
log.info("Validating password [{}]", password);
|
||||
|
||||
if(password == null || password.equals("")) {
|
||||
log.info("Provided password is either null or an empty string");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (password != null && !password.isEmpty()){
|
||||
if (password.length() < 2 || password.length() > 64) {
|
||||
log.info("Passwords must be between 2 and 64 characters in length");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -148,6 +148,7 @@ public class DocumentConversionServiceImp implements DocumentConversionService {
|
||||
pres.getPodId(),
|
||||
pres.getMeetingId(),
|
||||
pres.getId(),
|
||||
pres.getTemporaryPresentationId(),
|
||||
pres.getName(),
|
||||
pres.getAuthzToken(),
|
||||
pres.isDownloadable(),
|
||||
|
@ -26,6 +26,7 @@ public final class UploadedPresentation {
|
||||
private final String podId;
|
||||
private final String meetingId;
|
||||
private final String id;
|
||||
private final String temporaryPresentationId;
|
||||
private final String name;
|
||||
private final boolean uploadFailed;
|
||||
private final ArrayList<String> uploadFailReason;
|
||||
@ -44,6 +45,7 @@ public final class UploadedPresentation {
|
||||
public UploadedPresentation(String podId,
|
||||
String meetingId,
|
||||
String id,
|
||||
String temporaryPresentationId,
|
||||
String name,
|
||||
String baseUrl,
|
||||
Boolean current,
|
||||
@ -53,6 +55,7 @@ public final class UploadedPresentation {
|
||||
this.podId = podId;
|
||||
this.meetingId = meetingId;
|
||||
this.id = id;
|
||||
this.temporaryPresentationId = temporaryPresentationId;
|
||||
this.name = name;
|
||||
this.baseUrl = baseUrl;
|
||||
this.isDownloadable = false;
|
||||
@ -62,6 +65,19 @@ public final class UploadedPresentation {
|
||||
this.uploadFailReason = uploadFailReason;
|
||||
}
|
||||
|
||||
public UploadedPresentation(String podId,
|
||||
String meetingId,
|
||||
String id,
|
||||
String name,
|
||||
String baseUrl,
|
||||
Boolean current,
|
||||
String authzToken,
|
||||
Boolean uploadFailed,
|
||||
ArrayList<String> uploadFailReason) {
|
||||
this(podId, meetingId, id, "", name, baseUrl,
|
||||
current, authzToken, uploadFailed, uploadFailReason);
|
||||
}
|
||||
|
||||
public File getUploadedFile() {
|
||||
return uploadedFile;
|
||||
}
|
||||
@ -82,6 +98,10 @@ public final class UploadedPresentation {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getTemporaryPresentationId() {
|
||||
return temporaryPresentationId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
@ -85,7 +85,7 @@ public class SwfSlidesGenerationProgressNotifier {
|
||||
}
|
||||
|
||||
DocPageCompletedProgress progress = new DocPageCompletedProgress(pres.getPodId(), pres.getMeetingId(),
|
||||
pres.getId(), pres.getId(),
|
||||
pres.getId(), pres.getTemporaryPresentationId(), pres.getId(),
|
||||
pres.getName(), "notUsedYet", "notUsedYet",
|
||||
pres.isDownloadable(), pres.isRemovable(), ConversionMessageConstants.CONVERSION_COMPLETED_KEY,
|
||||
pres.getNumberOfPages(), generateBasePresUrl(pres), pres.isCurrent());
|
||||
|
@ -4,6 +4,7 @@ public class DocConversionRequestReceived 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 Boolean downloadable;
|
||||
@ -13,6 +14,7 @@ public class DocConversionRequestReceived implements IDocConversionMsg {
|
||||
public DocConversionRequestReceived(String podId,
|
||||
String meetingId,
|
||||
String presId,
|
||||
String temporaryPresentationId,
|
||||
String filename,
|
||||
String authzToken,
|
||||
Boolean downloadable,
|
||||
@ -21,6 +23,7 @@ public class DocConversionRequestReceived implements IDocConversionMsg {
|
||||
this.podId = podId;
|
||||
this.meetingId = meetingId;
|
||||
this.presId = presId;
|
||||
this.temporaryPresentationId = temporaryPresentationId;
|
||||
this.filename = filename;
|
||||
this.authzToken = authzToken;
|
||||
this.downloadable = downloadable;
|
||||
|
@ -4,6 +4,7 @@ public class DocPageCompletedProgress implements IDocConversionMsg {
|
||||
public final String podId;
|
||||
public final String meetingId;
|
||||
public final String presId;
|
||||
public final String temporaryPresentationId;
|
||||
public final String presInstance;
|
||||
public final String filename;
|
||||
public final String uploaderId;
|
||||
@ -15,13 +16,14 @@ public class DocPageCompletedProgress implements IDocConversionMsg {
|
||||
public final String presBaseUrl;
|
||||
public final Boolean current;
|
||||
|
||||
public DocPageCompletedProgress(String podId, String meetingId, String presId, String presInstance,
|
||||
public DocPageCompletedProgress(String podId, String meetingId, String presId, String temporaryPresentationId, String presInstance,
|
||||
String filename, String uploaderId, String authzToken,
|
||||
Boolean downloadable, Boolean removable, String key,
|
||||
Integer numPages, String presBaseUrl, Boolean current) {
|
||||
this.podId = podId;
|
||||
this.meetingId = meetingId;
|
||||
this.presId = presId;
|
||||
this.temporaryPresentationId = temporaryPresentationId;
|
||||
this.presInstance = presInstance;
|
||||
this.filename = filename;
|
||||
this.uploaderId = uploaderId;
|
||||
|
@ -157,7 +157,7 @@ object MsgBuilder {
|
||||
val header = BbbClientMsgHeader(PresentationConversionCompletedSysPubMsg.NAME, msg.meetingId, msg.authzToken)
|
||||
|
||||
val pages = generatePresentationPages(msg.presId, msg.numPages.intValue(), msg.presBaseUrl)
|
||||
val presentation = PresentationVO(msg.presId, msg.filename,
|
||||
val presentation = PresentationVO(msg.presId, msg.temporaryPresentationId, msg.filename,
|
||||
current = msg.current.booleanValue(), pages.values.toVector, msg.downloadable.booleanValue(), msg.removable.booleanValue())
|
||||
|
||||
val body = PresentationConversionCompletedSysPubMsgBody(podId = msg.podId, messageKey = msg.key,
|
||||
@ -228,6 +228,7 @@ object MsgBuilder {
|
||||
val body = PresentationConversionRequestReceivedSysMsgBody(
|
||||
podId = msg.podId,
|
||||
presentationId = msg.presId,
|
||||
temporaryPresentationId = msg.temporaryPresentationId,
|
||||
current = msg.current,
|
||||
presName = msg.filename,
|
||||
downloadable = msg.downloadable,
|
||||
|
@ -1,13 +0,0 @@
|
||||
FROM bbb-common-message
|
||||
|
||||
ARG COMMON_VERSION
|
||||
|
||||
COPY . /bbb-fsesl-client
|
||||
|
||||
RUN cd /bbb-fsesl-client \
|
||||
&& sed -i "s|\(version := \)\".*|\1\"$COMMON_VERSION\"|g" build.sbt \
|
||||
&& find -name build.sbt -exec sed -i "s|\(.*org.bigbluebutton.*bbb-common-message[^\"]*\"[ ]*%[ ]*\)\"[^\"]*\"\(.*\)|\1\"$COMMON_VERSION\"\2|g" {} \; \
|
||||
&& echo 'publishTo := Some(Resolver.file("file", new File(Path.userHome.absolutePath+"/.m2/repository")))' | tee -a build.sbt \
|
||||
&& sbt compile \
|
||||
&& sbt publish \
|
||||
&& sbt publishLocal
|
1372
bbb-learning-dashboard/package-lock.json
generated
1372
bbb-learning-dashboard/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -49,7 +49,7 @@ class App extends React.Component {
|
||||
|
||||
downloadButton.setAttribute('disabled', 'true');
|
||||
downloadButton.style.cursor = 'not-allowed';
|
||||
link.setAttribute('href', `data:application/octet-stream,${encodeURIComponent(data)}`);
|
||||
link.setAttribute('href', `data:text/csv;charset=UTF-8,${encodeURIComponent(data)}`);
|
||||
link.setAttribute('download', filename);
|
||||
link.style.display = 'none';
|
||||
document.body.appendChild(link);
|
||||
@ -87,14 +87,19 @@ class App extends React.Component {
|
||||
const cDecoded = decodeURIComponent(document.cookie);
|
||||
const cArr = cDecoded.split('; ');
|
||||
cArr.forEach((val) => {
|
||||
if (val.indexOf(`${cookieName}=`) === 0) learningDashboardAccessToken = val.substring((`${cookieName}=`).length);
|
||||
if (val.indexOf(`${cookieName}=`) === 0) {
|
||||
learningDashboardAccessToken = val.substring((`${cookieName}=`).length);
|
||||
}
|
||||
});
|
||||
|
||||
// Extend AccessToken lifetime by 7d (in each access)
|
||||
if (learningDashboardAccessToken !== '') {
|
||||
const cookieExpiresDate = new Date();
|
||||
cookieExpiresDate.setTime(cookieExpiresDate.getTime() + (3600000 * 24 * 7));
|
||||
document.cookie = `ld-${meetingId}=${learningDashboardAccessToken}; expires=${cookieExpiresDate.toGMTString()}; path=/;SameSite=None;Secure`;
|
||||
const value = `ld-${meetingId}=${learningDashboardAccessToken};`;
|
||||
const expire = `expires=${cookieExpiresDate.toGMTString()};`;
|
||||
const args = 'path=/;SameSite=None;Secure';
|
||||
document.cookie = `${value} ${expire} ${args}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,12 @@ function Card(props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex items-start justify-between p-3 bg-white rounded shadow border-l-4 ${cardClass}`}>
|
||||
<div
|
||||
className={
|
||||
'flex items-start justify-between p-3 bg-white rounded shadow border-l-4'
|
||||
+ ` ${cardClass}`
|
||||
}
|
||||
>
|
||||
<div className="w-70">
|
||||
<p className="text-lg font-semibold text-gray-700">
|
||||
{ number }
|
||||
|
@ -104,6 +104,61 @@ class StatusTable extends React.Component {
|
||||
|
||||
const isRTL = document.dir === 'rtl';
|
||||
|
||||
function makeLineThrough(userPeriod, period) {
|
||||
const { registeredOn, leftOn } = userPeriod;
|
||||
const boundaryLeft = period.start;
|
||||
const boundaryRight = period.end;
|
||||
const interval = period.end - period.start;
|
||||
let roundedLeft = registeredOn >= boundaryLeft
|
||||
&& registeredOn <= boundaryRight ? 'rounded-l' : '';
|
||||
let roundedRight = leftOn >= boundaryLeft
|
||||
&& leftOn <= boundaryRight ? 'rounded-r' : '';
|
||||
let offsetLeft = 0;
|
||||
let offsetRight = 0;
|
||||
if (registeredOn >= boundaryLeft && registeredOn <= boundaryRight) {
|
||||
offsetLeft = ((registeredOn - boundaryLeft) * 100) / interval;
|
||||
}
|
||||
if (leftOn >= boundaryLeft && leftOn <= boundaryRight) {
|
||||
offsetRight = ((boundaryRight - leftOn) * 100) / interval;
|
||||
}
|
||||
let width = '';
|
||||
if (offsetLeft === 0 && offsetRight >= 99) {
|
||||
width = 'w-1.5';
|
||||
}
|
||||
if (offsetRight === 0 && offsetLeft >= 99) {
|
||||
width = 'w-1.5';
|
||||
}
|
||||
if (offsetLeft && offsetRight) {
|
||||
const variation = offsetLeft - offsetRight;
|
||||
if (variation > -1 && variation < 1) {
|
||||
width = 'w-1.5';
|
||||
}
|
||||
}
|
||||
if (isRTL) {
|
||||
const aux = roundedRight;
|
||||
|
||||
if (roundedLeft !== '') roundedRight = 'rounded-r';
|
||||
else roundedRight = '';
|
||||
|
||||
if (aux !== '') roundedLeft = 'rounded-l';
|
||||
else roundedLeft = '';
|
||||
}
|
||||
const redress = '(0.375rem / 2)';
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
'h-1.5 bg-gray-200 absolute inset-x-0 z-10'
|
||||
+ ` ${width} ${roundedLeft} ${roundedRight}`
|
||||
}
|
||||
style={{
|
||||
top: `calc(50% - ${redress})`,
|
||||
left: `${isRTL ? offsetRight : offsetLeft}%`,
|
||||
right: `${isRTL ? offsetLeft : offsetRight}%`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
@ -134,6 +189,8 @@ class StatusTable extends React.Component {
|
||||
{ periods.map((period) => {
|
||||
const { slide, start, end } = period;
|
||||
const padding = isRTL ? 'paddingLeft' : 'paddingRight';
|
||||
const URLPrefix = `/bigbluebutton/presentation/${meetingId}/${meetingId}`;
|
||||
const { presentationId, pageNum } = slide || {};
|
||||
return (
|
||||
<td
|
||||
style={{
|
||||
@ -147,13 +204,13 @@ class StatusTable extends React.Component {
|
||||
aria-label={tsToHHmmss(start - periods[0].start)}
|
||||
>
|
||||
<a
|
||||
href={`/bigbluebutton/presentation/${meetingId}/${meetingId}/${slide.presentationId}/svg/${slide.pageNum}`}
|
||||
href={`${URLPrefix}/${presentationId}/svg/${pageNum}`}
|
||||
className="block border-2 border-gray-300"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<img
|
||||
src={`/bigbluebutton/presentation/${meetingId}/${meetingId}/${slide.presentationId}/thumbnail/${slide.pageNum}`}
|
||||
src={`${URLPrefix}/${presentationId}/thumbnail/${pageNum}`}
|
||||
alt={intl.formatMessage({
|
||||
id: 'app.learningDashboard.statusTimelineTable.thumbnail',
|
||||
defaultMessage: 'Presentation thumbnail',
|
||||
@ -216,57 +273,9 @@ class StatusTable extends React.Component {
|
||||
{ (registeredOn >= boundaryLeft && registeredOn <= boundaryRight)
|
||||
|| (leftOn >= boundaryLeft && leftOn <= boundaryRight)
|
||||
|| (boundaryLeft > registeredOn && boundaryRight < leftOn)
|
||||
|| (boundaryLeft >= registeredOn && leftOn === 0) ? (
|
||||
(function makeLineThrough() {
|
||||
let roundedLeft = registeredOn >= boundaryLeft
|
||||
&& registeredOn <= boundaryRight ? 'rounded-l' : '';
|
||||
let roundedRight = leftOn >= boundaryLeft
|
||||
&& leftOn <= boundaryRight ? 'rounded-r' : '';
|
||||
let offsetLeft = 0;
|
||||
let offsetRight = 0;
|
||||
if (registeredOn >= boundaryLeft
|
||||
&& registeredOn <= boundaryRight) {
|
||||
offsetLeft = ((registeredOn - boundaryLeft) * 100)
|
||||
/ interval;
|
||||
}
|
||||
if (leftOn >= boundaryLeft && leftOn <= boundaryRight) {
|
||||
offsetRight = ((boundaryRight - leftOn) * 100) / interval;
|
||||
}
|
||||
let width = '';
|
||||
if (offsetLeft === 0 && offsetRight >= 99) {
|
||||
width = 'w-1.5';
|
||||
}
|
||||
if (offsetRight === 0 && offsetLeft >= 99) {
|
||||
width = 'w-1.5';
|
||||
}
|
||||
if (offsetLeft && offsetRight) {
|
||||
const variation = offsetLeft - offsetRight;
|
||||
if (variation > -1 && variation < 1) {
|
||||
width = 'w-1.5';
|
||||
}
|
||||
}
|
||||
if (isRTL) {
|
||||
const aux = roundedRight;
|
||||
|
||||
if (roundedLeft !== '') roundedRight = 'rounded-r';
|
||||
else roundedRight = '';
|
||||
|
||||
if (aux !== '') roundedLeft = 'rounded-l';
|
||||
else roundedLeft = '';
|
||||
}
|
||||
const redress = '(0.375rem / 2)';
|
||||
return (
|
||||
<div
|
||||
className={`h-1.5 ${width} bg-gray-200 absolute inset-x-0 z-10 ${roundedLeft} ${roundedRight}`}
|
||||
style={{
|
||||
top: `calc(50% - ${redress})`,
|
||||
left: `${isRTL ? offsetRight : offsetLeft}%`,
|
||||
right: `${isRTL ? offsetLeft : offsetRight}%`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})()
|
||||
) : null }
|
||||
|| (boundaryLeft >= registeredOn && leftOn === 0)
|
||||
? makeLineThrough(userPeriod, period)
|
||||
: null }
|
||||
{ userEmojisInPeriod.map((emoji) => {
|
||||
const offset = ((emoji.sentOn - period.start) * 100)
|
||||
/ (interval);
|
||||
@ -285,7 +294,12 @@ class StatusTable extends React.Component {
|
||||
defaultMessage: emojiConfigs[emoji.name].defaultMessage,
|
||||
})}
|
||||
>
|
||||
<i className={`${emojiConfigs[emoji.name].icon} text-sm bbb-icon-timeline`} />
|
||||
<i
|
||||
className={
|
||||
'text-sm bbb-icon-timeline'
|
||||
+ ` ${emojiConfigs[emoji.name].icon}`
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}) }
|
||||
|
@ -176,6 +176,11 @@ const UserDatailsComponent = (props) => {
|
||||
if (hasDraw) mostCommonAnswer = null;
|
||||
}
|
||||
|
||||
const capitalizeFirstLetter = (text) => (
|
||||
String.fromCharCode(text.charCodeAt(0) - 32)
|
||||
+ text.substring(1)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-6 flex flex-row justify-between items-center">
|
||||
<div className="min-w-[40%] text-ellipsis">{question}</div>
|
||||
@ -211,11 +216,11 @@ const UserDatailsComponent = (props) => {
|
||||
<div
|
||||
className="min-w-[40%] text-ellipsis text-center overflow-hidden"
|
||||
title={mostCommonAnswer
|
||||
? `${String.fromCharCode(mostCommonAnswer.charCodeAt(0) - 32)}${mostCommonAnswer.substring(1)}`
|
||||
? capitalizeFirstLetter(mostCommonAnswer)
|
||||
: null}
|
||||
>
|
||||
{ mostCommonAnswer
|
||||
? `${String.fromCharCode(mostCommonAnswer.charCodeAt(0) - 32)}${mostCommonAnswer.substring(1)}`
|
||||
? capitalizeFirstLetter(mostCommonAnswer)
|
||||
: intl.formatMessage({
|
||||
id: 'app.learningDashboard.usersTable.notAvailable',
|
||||
defaultMessage: 'N/A',
|
||||
@ -233,7 +238,7 @@ const UserDatailsComponent = (props) => {
|
||||
<div className="min-w-[20%] text-ellipsis overflow-hidden">{category}</div>
|
||||
<div className="min-w-[60%] grow text-center text-sm">
|
||||
<div className="mb-2">
|
||||
{ (function () {
|
||||
{ (function getAverage() {
|
||||
if (average >= 0 && category === 'Talk Time') return tsToHHmmss(average);
|
||||
if (average >= 0 && category !== 'Talk Time') return <FormattedNumber value={average} minimumFractionDigits="0" maximumFractionDigits="1" />;
|
||||
return <FormattedMessage id="app.learningDashboard.usersTable.notAvailable" defaultMessage="N/A" />;
|
||||
|
@ -1 +1 @@
|
||||
git clone --branch v1.0.2 --depth 1 https://github.com/bigbluebutton/bbb-pads bbb-pads
|
||||
git clone --branch v1.2.0 --depth 1 https://github.com/bigbluebutton/bbb-pads bbb-pads
|
||||
|
@ -1 +1 @@
|
||||
git clone --branch v3.4.0 --depth 1 https://github.com/bigbluebutton/bbb-playback bbb-playback
|
||||
git clone --branch v4.0.0 --depth 1 https://github.com/bigbluebutton/bbb-playback bbb-playback
|
||||
|
@ -3,7 +3,7 @@
|
||||
<condition field="${bbb_authorized}" expression="true" break="on-false"/>
|
||||
<condition field="${sip_via_protocol}" expression="^wss?$"/>
|
||||
<condition field="destination_number" expression="^(\d{5,11})$">
|
||||
<action application="jitterbuffer" data="60:120"/>
|
||||
<action application="jitterbuffer" data="120"/>
|
||||
<action application="answer"/>
|
||||
<action application="conference" data="$1@cdquality"/>
|
||||
</condition>
|
||||
@ -11,7 +11,7 @@
|
||||
<extension name="bbb_conferences">
|
||||
<condition field="${bbb_authorized}" expression="true" break="on-false"/>
|
||||
<condition field="destination_number" expression="^(\d{5,11})$">
|
||||
<action application="jitterbuffer" data="60:120"/>
|
||||
<action application="jitterbuffer" data="120"/>
|
||||
<action application="answer"/>
|
||||
<action application="conference" data="$1@cdquality"/>
|
||||
</condition>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<extension name="ECHO_TO_CONFERENCE">
|
||||
<condition field="${bbb_from_echo}" expression="true" break="on-false"/>
|
||||
<condition field="destination_number" expression="^(ECHO_TO_CONFERENCE)$">
|
||||
<action application="jitterbuffer" data="60:120"/>
|
||||
<action application="jitterbuffer" data="120"/>
|
||||
<action application="answer"/>
|
||||
<action application="conference" data="${vbridge}@cdquality"/>
|
||||
</condition>
|
||||
|
@ -1 +1 @@
|
||||
git clone --branch v2.8.0-alpha.1 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu
|
||||
git clone --branch v2.8.1 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu
|
||||
|
@ -1 +1 @@
|
||||
BIGBLUEBUTTON_RELEASE=2.5.0-beta.1
|
||||
BIGBLUEBUTTON_RELEASE=2.5.0-rc.2
|
||||
|
@ -658,8 +658,10 @@ fi
|
||||
|
||||
if [[ $SECRET ]]; then
|
||||
need_root
|
||||
if get_properties_value securitySalt "$BBB_WEB_ETC_CONFIG" > /dev/null ; then
|
||||
change_var_salt "$BBB_WEB_ETC_CONFIG" securitySalt "$SECRET"
|
||||
|
||||
echo "Assigning secret in $BBB_WEB_ETC_CONFIG"
|
||||
if [ -f "$BBB_WEB_ETC_CONFIG" ] && grep "^securitySalt" "$BBB_WEB_ETC_CONFIG" > /dev/null ; then
|
||||
change_var_value "$BBB_WEB_ETC_CONFIG" securitySalt "$SECRET"
|
||||
else
|
||||
echo "securitySalt=$SECRET" >> "$BBB_WEB_ETC_CONFIG"
|
||||
fi
|
||||
@ -1296,19 +1298,6 @@ check_state() {
|
||||
done
|
||||
fi
|
||||
|
||||
stunServerAddress=$(cat /etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini | sed -n '/^stunServerAddress/{s/.*=//;p}')
|
||||
stunServerPort=$(cat /etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini | sed -n '/^stunServerPort/{s/.*=//;p}')
|
||||
if [ ! -z "$stunServerAddress" ]; then
|
||||
if stunclient --mode full --localport 30000 $stunServerAddress $stunServerPort | grep -q "fail\|Unable\ to\ resolve"; then
|
||||
echo
|
||||
echo "#"
|
||||
echo "# Warning: Failed to verify STUN server at $stunServerAddress:$stunServerPort with command"
|
||||
echo "#"
|
||||
echo "# stunclient --mode full --localport 30000 $stunServerAddress $stunServerPort"
|
||||
echo "#"
|
||||
fi
|
||||
fi
|
||||
|
||||
BBB_LOG="/var/log/bigbluebutton"
|
||||
if [ "$(stat -c "%U %G" $BBB_LOG)" != "bigbluebutton bigbluebutton" ]; then
|
||||
echo
|
||||
@ -1476,14 +1465,6 @@ if [ $CHECK ]; then
|
||||
done
|
||||
fi
|
||||
|
||||
stunServerAddress=$(cat /etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini | sed -n '/^stunServerAddress/{s/.*=//;p}')
|
||||
stunServerPort=$(cat /etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini | sed -n '/^stunServerPort/{s/.*=//;p}')
|
||||
if [ ! -z "$stunServerAddress" ]; then
|
||||
echo
|
||||
echo "/etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini (STUN Server)"
|
||||
echo " stun: $stunServerAddress:$stunServerPort"
|
||||
fi
|
||||
|
||||
check_state
|
||||
echo
|
||||
|
||||
@ -1633,7 +1614,12 @@ if [ -n "$HOST" ]; then
|
||||
if [ -f "$BBB_WEB_ETC_CONFIG" ] && grep "bigbluebutton.web.serverURL" "$BBB_WEB_ETC_CONFIG" > /dev/null ; then
|
||||
change_var_value "$BBB_WEB_ETC_CONFIG" bigbluebutton.web.serverURL "$PROTOCOL://$HOST"
|
||||
else
|
||||
echo "bigbluebutton.web.serverURL=$PROTOCOL://$HOST" > "$BBB_WEB_ETC_CONFIG"
|
||||
echo "bigbluebutton.web.serverURL=$PROTOCOL://$HOST" >> "$BBB_WEB_ETC_CONFIG"
|
||||
fi
|
||||
|
||||
# Populate /etc/bigbluebutton/bbb-web.properites with the shared secret
|
||||
if ! grep -q "^securitySalt" "$BBB_WEB_ETC_CONFIG"; then
|
||||
echo "securitySalt=$(get_bbb_web_config_value securitySalt)" >> "$BBB_WEB_ETC_CONFIG"
|
||||
fi
|
||||
|
||||
|
||||
|
@ -1,2 +0,0 @@
|
||||
Dockerfile
|
||||
Dockerfile.dev
|
@ -8,10 +8,10 @@ mobile-experience@1.1.0
|
||||
mongo@1.14.6
|
||||
reactive-var@1.0.11
|
||||
|
||||
standard-minifier-css@1.7.4
|
||||
standard-minifier-css@1.8.1
|
||||
standard-minifier-js@2.8.0
|
||||
es5-shim@4.8.0
|
||||
ecmascript@0.16.1
|
||||
ecmascript@0.16.2
|
||||
shell-server@0.5.0
|
||||
|
||||
static-html@1.3.2
|
||||
|
@ -1 +1 @@
|
||||
METEOR@2.6.1
|
||||
METEOR@2.7.1
|
||||
|
@ -1,6 +1,6 @@
|
||||
allow-deny@1.1.1
|
||||
autoupdate@1.8.0
|
||||
babel-compiler@7.8.1
|
||||
babel-compiler@7.9.0
|
||||
babel-runtime@1.5.0
|
||||
base64@1.0.12
|
||||
binary-heap@1.0.11
|
||||
@ -16,11 +16,11 @@ ddp-common@1.4.0
|
||||
ddp-server@2.5.0
|
||||
diff-sequence@1.1.1
|
||||
dynamic-import@0.7.2
|
||||
ecmascript@0.16.1
|
||||
ecmascript@0.16.2
|
||||
ecmascript-runtime@0.8.0
|
||||
ecmascript-runtime-client@0.12.1
|
||||
ecmascript-runtime-server@0.11.0
|
||||
ejson@1.1.1
|
||||
ejson@1.1.2
|
||||
es5-shim@4.8.0
|
||||
fetch@0.1.1
|
||||
geojson-utils@1.0.10
|
||||
@ -39,13 +39,13 @@ 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.3
|
||||
minifier-js@2.7.4
|
||||
minimongo@1.8.0
|
||||
mobile-experience@1.1.0
|
||||
mobile-status-bar@1.1.0
|
||||
modern-browsers@0.1.7
|
||||
modules@0.18.0
|
||||
modules-runtime@0.12.0
|
||||
modules-runtime@0.13.0
|
||||
mongo@1.14.6
|
||||
mongo-decimal@0.1.2
|
||||
mongo-dev-server@1.1.0
|
||||
@ -54,7 +54,7 @@ npm-mongo@4.3.1
|
||||
ordered-dict@1.1.0
|
||||
promise@0.12.0
|
||||
random@1.2.0
|
||||
react-fast-refresh@0.2.2
|
||||
react-fast-refresh@0.2.3
|
||||
react-meteor-data@2.4.0
|
||||
reactive-dict@1.3.0
|
||||
reactive-var@1.0.11
|
||||
@ -66,12 +66,12 @@ session@1.2.0
|
||||
shell-server@0.5.0
|
||||
socket-stream-client@0.4.0
|
||||
spacebars-compiler@1.3.0
|
||||
standard-minifier-css@1.7.4
|
||||
standard-minifier-css@1.8.1
|
||||
standard-minifier-js@2.8.0
|
||||
static-html@1.3.2
|
||||
templating-tools@1.2.1
|
||||
tracker@1.2.0
|
||||
typescript@4.4.1
|
||||
typescript@4.5.4
|
||||
underscore@1.0.10
|
||||
url@1.3.2
|
||||
webapp@1.13.1
|
||||
|
@ -1,38 +0,0 @@
|
||||
FROM node:8
|
||||
|
||||
RUN set -x \
|
||||
&& curl -sL https://install.meteor.com | sed s/--progress-bar/-sL/g | /bin/sh \
|
||||
&& useradd -m -G users -s /bin/bash meteor
|
||||
|
||||
RUN apt-get update && apt-get -y install jq
|
||||
|
||||
COPY . /source
|
||||
|
||||
RUN cd /source \
|
||||
&& mv docker-entrypoint.sh /usr/local/bin/ \
|
||||
&& chown -R meteor:meteor . \
|
||||
&& mkdir /app \
|
||||
&& chown -R meteor:meteor /app
|
||||
|
||||
USER meteor
|
||||
|
||||
RUN cd /source \
|
||||
&& meteor npm install \
|
||||
&& meteor build --directory /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
|
||||
RUN cd /app/bundle/programs/server \
|
||||
&& npm install \
|
||||
&& npm cache clear --force
|
||||
|
||||
WORKDIR /app/bundle
|
||||
|
||||
ENV MONGO_URL=mongodb://mongo:27017/html5client \
|
||||
PORT=3000 \
|
||||
ROOT_URL=http://localhost:3000 \
|
||||
METEOR_SETTINGS_MODIFIER=.
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["docker-entrypoint.sh"]
|
@ -1,24 +0,0 @@
|
||||
FROM node:8
|
||||
|
||||
COPY . /source
|
||||
|
||||
RUN set -x \
|
||||
&& curl -sL https://install.meteor.com | sed s/--progress-bar/-sL/g | /bin/sh \
|
||||
&& useradd -m -G users -s /bin/bash meteor \
|
||||
&& chown -R meteor:meteor /source
|
||||
|
||||
USER meteor
|
||||
|
||||
RUN cd /source \
|
||||
&& meteor npm install
|
||||
|
||||
WORKDIR /source
|
||||
|
||||
ENV MONGO_URL=mongodb://mongo:27017/html5client \
|
||||
PORT=3000 \
|
||||
ROOT_URL=http://localhost:3000
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["npm", "start"]
|
||||
|
@ -1,5 +0,0 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
export METEOR_SETTINGS=` jq "${METEOR_SETTINGS_MODIFIER}" ./programs/server/assets/app/config/settings.yml `
|
||||
|
||||
exec node main.js
|
@ -1,4 +1,9 @@
|
||||
import { PrometheusAgent, METRIC_NAMES } from '/imports/startup/server/prom-metrics/index.js'
|
||||
|
||||
// Round-trip time helper
|
||||
export default function voidConnection() {
|
||||
export default function voidConnection(previousRtt) {
|
||||
if (previousRtt) {
|
||||
PrometheusAgent.observe(METRIC_NAMES.METEOR_RTT, previousRtt/1000);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
@ -139,10 +139,13 @@ export default function addMeeting(meeting) {
|
||||
const sanitizeTextInChat = original => SanitizeHTML(original, {
|
||||
allowedTags: ['a', 'b', 'br', 'i', 'img', 'li', 'small', 'span', 'strong', 'u', 'ul'],
|
||||
allowedAttributes: {
|
||||
a: ['href', 'name', 'target'],
|
||||
a: ['href', 'target'],
|
||||
img: ['src', 'width', 'height'],
|
||||
},
|
||||
allowedSchemes: ['https'],
|
||||
allowedSchemesByTag: {
|
||||
a: ['https', 'mailto', 'tel']
|
||||
}
|
||||
});
|
||||
|
||||
const sanitizedWelcomeText = sanitizeTextInChat(welcomeMsg);
|
||||
@ -153,6 +156,8 @@ export default function addMeeting(meeting) {
|
||||
|
||||
const insertBlankTarget = (s, i) => `${s.substr(0, i)} target="_blank"${s.substr(i)}`;
|
||||
const linkWithoutTarget = new RegExp('<a href="(.*?)">', 'g');
|
||||
|
||||
do {
|
||||
linkWithoutTarget.test(welcomeMsg);
|
||||
|
||||
if (linkWithoutTarget.lastIndex > 0) {
|
||||
@ -160,7 +165,9 @@ export default function addMeeting(meeting) {
|
||||
welcomeMsg,
|
||||
linkWithoutTarget.lastIndex - 1,
|
||||
);
|
||||
linkWithoutTarget.lastIndex = linkWithoutTarget.lastIndex - 1;
|
||||
}
|
||||
} while (linkWithoutTarget.lastIndex > 0);
|
||||
|
||||
newMeeting.welcomeProp.welcomeMsg = welcomeMsg;
|
||||
|
||||
|
@ -1,23 +1,26 @@
|
||||
import { check } from 'meteor/check';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import PresentationUploadToken from '/imports/api/presentation-upload-token';
|
||||
import Presentations from '/imports/api/presentations';
|
||||
|
||||
export default function handlePresentationUploadTokenPass({ body, header }, meetingId) {
|
||||
check(body, Object);
|
||||
|
||||
const { userId } = header;
|
||||
const { podId, authzToken, filename } = body;
|
||||
const { podId, authzToken, filename, tmpPresId } = body;
|
||||
|
||||
check(userId, String);
|
||||
check(podId, String);
|
||||
check(authzToken, String);
|
||||
check(filename, String);
|
||||
check(tmpPresId, String)
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
podId,
|
||||
userId,
|
||||
filename,
|
||||
tmpPresId,
|
||||
};
|
||||
|
||||
const modifier = {
|
||||
@ -26,6 +29,7 @@ export default function handlePresentationUploadTokenPass({ body, header }, meet
|
||||
userId,
|
||||
filename,
|
||||
authzToken,
|
||||
tmpPresId,
|
||||
failed: false,
|
||||
used: false,
|
||||
};
|
||||
|
@ -3,7 +3,7 @@ import { check } from 'meteor/check';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default function requestPresentationUploadToken(podId, filename) {
|
||||
export default function requestPresentationUploadToken(podId, filename, tmpPresId) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'PresentationUploadTokenReqMsg';
|
||||
@ -15,10 +15,12 @@ export default function requestPresentationUploadToken(podId, filename) {
|
||||
check(requesterUserId, String);
|
||||
check(podId, String);
|
||||
check(filename, String);
|
||||
check(tmpPresId, String);
|
||||
|
||||
const payload = {
|
||||
podId,
|
||||
filename,
|
||||
tmpPresId
|
||||
};
|
||||
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
|
||||
|
@ -4,7 +4,7 @@ import PresentationUploadToken from '/imports/api/presentation-upload-token';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import AuthTokenValidation, { ValidationStates } from '/imports/api/auth-token-validation';
|
||||
|
||||
function presentationUploadToken(podId, filename) {
|
||||
function presentationUploadToken(podId, filename, tmpPresId) {
|
||||
const tokenValidation = AuthTokenValidation.findOne({ connectionId: this.connection.id });
|
||||
|
||||
if (!tokenValidation || tokenValidation.validationStatus !== ValidationStates.VALIDATED) {
|
||||
@ -16,12 +16,14 @@ function presentationUploadToken(podId, filename) {
|
||||
|
||||
check(podId, String);
|
||||
check(filename, String);
|
||||
check(tmpPresId, String);
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
podId,
|
||||
userId,
|
||||
filename,
|
||||
tmpPresId,
|
||||
};
|
||||
|
||||
Logger.debug('Publishing PresentationUploadToken', { meetingId, userId });
|
||||
|
@ -34,6 +34,7 @@ export default function addPresentation(meetingId, podId, presentation) {
|
||||
id: String,
|
||||
name: String,
|
||||
current: Boolean,
|
||||
temporaryPresentationId: String,
|
||||
pages: [
|
||||
{
|
||||
id: String,
|
||||
|
@ -26,10 +26,10 @@ export default function addUserPersistentData(user) {
|
||||
locked: Boolean,
|
||||
avatar: String,
|
||||
clientType: String,
|
||||
left: Boolean,
|
||||
effectiveConnectionType: null,
|
||||
});
|
||||
|
||||
|
||||
const {
|
||||
intId,
|
||||
extId,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import handleRemoveUser from './handlers/removeUser';
|
||||
import handleUserJoined from './handlers/userJoined';
|
||||
import handleUserLeftFlagUpdated from './handlers/userLeftFlagUpdated';
|
||||
import handleValidateAuthToken from './handlers/validateAuthToken';
|
||||
import handlePresenterAssigned from './handlers/presenterAssigned';
|
||||
import handleEmojiStatus from './handlers/emojiStatus';
|
||||
@ -14,5 +15,6 @@ RedisPubSub.on('UserLeftMeetingEvtMsg', handleRemoveUser);
|
||||
RedisPubSub.on('ValidateAuthTokenRespMsg', handleValidateAuthToken);
|
||||
RedisPubSub.on('UserEmojiChangedEvtMsg', handleEmojiStatus);
|
||||
RedisPubSub.on('UserRoleChangedEvtMsg', handleChangeRole);
|
||||
RedisPubSub.on('UserLeftFlagUpdatedEvtMsg', handleUserLeftFlagUpdated);
|
||||
RedisPubSub.on('UserPinStateChangedEvtMsg', handleUserPinChanged);
|
||||
RedisPubSub.on('UserInactivityInspectMsg', handleUserInactivityInspect);
|
||||
|
@ -0,0 +1,13 @@
|
||||
import { check } from 'meteor/check';
|
||||
|
||||
import userLeftFlag from '../modifiers/userLeftFlagUpdated';
|
||||
|
||||
export default function handleUserLeftFlag({ body }, meetingId) {
|
||||
const user = body;
|
||||
check(user, {
|
||||
intId: String,
|
||||
userLeftFlag: Boolean,
|
||||
});
|
||||
|
||||
userLeftFlag(meetingId, user.intId, user.userLeftFlag);
|
||||
}
|
@ -62,6 +62,7 @@ export default function addUser(meetingId, userData) {
|
||||
inactivityCheck: false,
|
||||
responseDelay: 0,
|
||||
loggedOut: false,
|
||||
left: false,
|
||||
...flat(user),
|
||||
};
|
||||
|
||||
|
@ -19,6 +19,7 @@ export default function createDummyUser(meetingId, userId, authToken) {
|
||||
authToken,
|
||||
clientType: 'HTML5',
|
||||
validated: null,
|
||||
left: false,
|
||||
};
|
||||
|
||||
try {
|
||||
|
@ -0,0 +1,24 @@
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import Users from '/imports/api/users';
|
||||
|
||||
export default function userLeftFlagUpdated(meetingId, userId, left) {
|
||||
const selector = {
|
||||
meetingId,
|
||||
userId,
|
||||
};
|
||||
|
||||
const modifier = {
|
||||
$set: {
|
||||
left,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const numberAffected = Users.update(selector, modifier);
|
||||
if (numberAffected) {
|
||||
Logger.info(`Updated user ${userId} with left flag as ${left}`);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Changed user role: ${err}`);
|
||||
}
|
||||
}
|
@ -60,6 +60,7 @@ function users() {
|
||||
{ meetingId },
|
||||
],
|
||||
intId: { $exists: true },
|
||||
left: false,
|
||||
};
|
||||
|
||||
const User = Users.findOne({ userId, meetingId }, { fields: { role: 1 } });
|
||||
|
@ -4,7 +4,6 @@ import Langmap from 'langmap';
|
||||
import fs from 'fs';
|
||||
import Users from '/imports/api/users';
|
||||
import './settings';
|
||||
import { lookup as lookupUserAgent } from 'useragent';
|
||||
import { check } from 'meteor/check';
|
||||
import Logger from './logger';
|
||||
import Redis from './redis';
|
||||
@ -144,7 +143,8 @@ Meteor.startup(() => {
|
||||
Meteor.onMessage(event => {
|
||||
const { method } = event;
|
||||
if (method) {
|
||||
PrometheusAgent.increment(METRIC_NAMES.METEOR_METHODS, { methodName: method });
|
||||
const methodName = method.includes('stream-cursor') ? 'stream-cursor' : method;
|
||||
PrometheusAgent.increment(METRIC_NAMES.METEOR_METHODS, { methodName });
|
||||
}
|
||||
});
|
||||
|
||||
@ -303,20 +303,6 @@ WebApp.connectHandlers.use('/feedback', (req, res) => {
|
||||
}));
|
||||
});
|
||||
|
||||
WebApp.connectHandlers.use('/useragent', (req, res) => {
|
||||
const userAgent = req.headers['user-agent'];
|
||||
let response = 'No user agent found in header';
|
||||
if (userAgent) {
|
||||
response = lookupUserAgent(userAgent).toString();
|
||||
}
|
||||
|
||||
Logger.info(`The requesting user agent is ${response}`);
|
||||
|
||||
// res.setHeader('Content-Type', 'application/json');
|
||||
res.writeHead(200);
|
||||
res.end(response);
|
||||
});
|
||||
|
||||
WebApp.connectHandlers.use('/guestWait', (req, res) => {
|
||||
if (!guestWaitHtml) {
|
||||
try {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { createLogger, format, transports } from 'winston';
|
||||
import WinstonPromTransport from './prom-metrics/winstonPromTransport';
|
||||
|
||||
const LOG_CONFIG = Meteor?.settings?.private?.serverLog || {};
|
||||
const { level } = LOG_CONFIG;
|
||||
@ -20,6 +21,10 @@ const Logger = createLogger({
|
||||
handleExceptions: true,
|
||||
level,
|
||||
}),
|
||||
// export error logs to prometheus
|
||||
new WinstonPromTransport({
|
||||
level: 'error',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
|
@ -1,34 +1,60 @@
|
||||
const {
|
||||
Counter,
|
||||
Gauge,
|
||||
Histogram
|
||||
} = require('prom-client');
|
||||
|
||||
const METRICS_PREFIX = 'html5_'
|
||||
const METRIC_NAMES = {
|
||||
METEOR_METHODS: 'meteorMethods',
|
||||
}
|
||||
|
||||
const buildFrontendMetrics = () => {
|
||||
return {
|
||||
[METRIC_NAMES.METEOR_METHODS]: new Counter({
|
||||
name: `${METRICS_PREFIX}meteor_methods`,
|
||||
help: 'Total number of meteor methods processed in html5',
|
||||
labelNames: ['methodName', 'role', 'instanceId'],
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
const buildBackendMetrics = () => {
|
||||
// TODO add relevant backend metrics
|
||||
return {}
|
||||
METEOR_ERRORS_TOTAL: 'meteorErrorsTotal',
|
||||
METEOR_RTT: 'meteorRtt',
|
||||
REDIS_MESSAGE_QUEUE: 'redisMessageQueue',
|
||||
REDIS_PAYLOAD_SIZE: 'redisPayloadSize',
|
||||
REDIS_PROCESSING_TIME: 'redisProcessingTime'
|
||||
}
|
||||
|
||||
let METRICS;
|
||||
const buildMetrics = () => {
|
||||
if (METRICS == null) {
|
||||
const isFrontend = (!process.env.BBB_HTML5_ROLE || process.env.BBB_HTML5_ROLE === 'frontend');
|
||||
const isBackend = (!process.env.BBB_HTML5_ROLE || process.env.BBB_HTML5_ROLE === 'backend');
|
||||
if (isFrontend) METRICS = buildFrontendMetrics();
|
||||
if (isBackend) METRICS = { ...METRICS, ...buildBackendMetrics()}
|
||||
METRICS = {
|
||||
[METRIC_NAMES.METEOR_METHODS]: new Counter({
|
||||
name: `${METRICS_PREFIX}meteor_methods`,
|
||||
help: 'Total number of meteor methods processed in html5',
|
||||
labelNames: ['methodName', 'role', 'instanceId'],
|
||||
}),
|
||||
|
||||
[METRIC_NAMES.METEOR_ERRORS_TOTAL]: new Counter({
|
||||
name: `${METRICS_PREFIX}meteor_errors_total`,
|
||||
help: 'Total number of errors logs in meteor',
|
||||
labelNames: ['errorMessage', 'role', 'instanceId'],
|
||||
}),
|
||||
|
||||
[METRIC_NAMES.METEOR_RTT]: new Histogram({
|
||||
name: `${METRICS_PREFIX}meteor_rtt_seconds`,
|
||||
help: 'Round-trip time of meteor client-server connections in seconds',
|
||||
buckets: [0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.75, 1, 1.5, 2, 2.5, 5],
|
||||
labelNames: ['role', 'instanceId'],
|
||||
}),
|
||||
|
||||
[METRIC_NAMES.REDIS_MESSAGE_QUEUE]: new Gauge({
|
||||
name: `${METRICS_PREFIX}redis_message_queue`,
|
||||
help: 'Message queue size in redis',
|
||||
labelNames: ['meetingId', 'role', 'instanceId'],
|
||||
}),
|
||||
|
||||
[METRIC_NAMES.REDIS_PAYLOAD_SIZE]: new Histogram({
|
||||
name: `${METRICS_PREFIX}redis_payload_size`,
|
||||
help: 'Redis events payload size',
|
||||
labelNames: ['eventName', 'role', 'instanceId'],
|
||||
}),
|
||||
|
||||
[METRIC_NAMES.REDIS_PROCESSING_TIME]: new Histogram({
|
||||
name: `${METRICS_PREFIX}redis_processing_time`,
|
||||
help: 'Redis events processing time in milliseconds',
|
||||
labelNames: ['eventName', 'role', 'instanceId'],
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
return METRICS;
|
||||
|
@ -81,6 +81,16 @@ class PrometheusScrapeAgent {
|
||||
metric.set(labelsObject, value)
|
||||
}
|
||||
}
|
||||
|
||||
observe(metricName, value, labelsObject) {
|
||||
if (!this.started) return;
|
||||
|
||||
const metric = this.metrics[metricName];
|
||||
if (metric) {
|
||||
labelsObject = { ...labelsObject, ...this.roleAndInstanceLabels };
|
||||
metric.observe(labelsObject, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default PrometheusScrapeAgent;
|
||||
|
@ -0,0 +1,19 @@
|
||||
const Transport = require('winston-transport');
|
||||
import { PrometheusAgent, METRIC_NAMES } from './index.js'
|
||||
|
||||
module.exports = class WinstonPromTransport extends Transport {
|
||||
constructor(opts) {
|
||||
super(opts);
|
||||
|
||||
}
|
||||
|
||||
log(info, callback) {
|
||||
setImmediate(() => {
|
||||
this.emit('logged', info);
|
||||
});
|
||||
|
||||
PrometheusAgent.increment(METRIC_NAMES.METEOR_ERRORS_TOTAL, { errorMessage: info.message });
|
||||
|
||||
callback();
|
||||
}
|
||||
};
|
@ -5,11 +5,13 @@ import { check } from 'meteor/check';
|
||||
import Logger from './logger';
|
||||
import Metrics from './metrics';
|
||||
import queue from 'queue';
|
||||
import { PrometheusAgent, METRIC_NAMES } from './prom-metrics/index.js'
|
||||
|
||||
// Fake meetingId used for messages that have no meetingId
|
||||
const NO_MEETING_ID = '_';
|
||||
|
||||
const { queueMetrics } = Meteor.settings.private.redis.metrics;
|
||||
const { collectRedisMetrics: PROM_METRICS_ENABLED } = Meteor.settings.private.prometheus;
|
||||
|
||||
const makeEnvelope = (channel, eventName, header, body, routing) => {
|
||||
const envelope = {
|
||||
@ -78,6 +80,16 @@ class MeetingMessageQueue {
|
||||
}
|
||||
|
||||
const queueLength = this.queue.length;
|
||||
|
||||
if (PROM_METRICS_ENABLED) {
|
||||
const dataLength = JSON.stringify(data).length;
|
||||
const currentTimestamp = Date.now();
|
||||
const processTime = currentTimestamp - beginHandleTimestamp;
|
||||
PrometheusAgent.observe(METRIC_NAMES.REDIS_PROCESSING_TIME, processTime, { eventName });
|
||||
PrometheusAgent.observe(METRIC_NAMES.REDIS_PAYLOAD_SIZE, dataLength, { eventName });
|
||||
meetingId && PrometheusAgent.set(METRIC_NAMES.REDIS_MESSAGE_QUEUE, queueLength, { meetingId });
|
||||
}
|
||||
|
||||
if (queueLength > 100) {
|
||||
Logger.warn(`Redis: MeetingMessageQueue for meetingId=${meetingId} has queue size=${queueLength} `);
|
||||
}
|
||||
|
@ -470,6 +470,7 @@ class BreakoutRoom extends PureComponent {
|
||||
.filter((user) => !stateUsersId.includes(user.userId))
|
||||
.map((user) => ({
|
||||
userId: user.userId,
|
||||
extId: user.extId,
|
||||
userName: user.name,
|
||||
isModerator: user.role === ROLE_MODERATOR,
|
||||
room: 0,
|
||||
@ -611,7 +612,8 @@ class BreakoutRoom extends PureComponent {
|
||||
}
|
||||
|
||||
populateWithLastBreakouts(lastBreakouts) {
|
||||
const { getBreakoutUserWasIn, users, intl } = this.props;
|
||||
const { getBreakoutUserWasIn, intl } = this.props;
|
||||
const { users } = this.state;
|
||||
|
||||
const changedNames = [];
|
||||
lastBreakouts.forEach((breakout) => {
|
||||
|
@ -44,6 +44,7 @@ import Settings from '/imports/ui/services/settings';
|
||||
import LayoutService from '/imports/ui/components/layout/service';
|
||||
import { registerTitleView } from '/imports/utils/dom-utils';
|
||||
import GlobalStyles from '/imports/ui/stylesheets/styled-components/globalStyles';
|
||||
import MediaService from '/imports/ui/components/media/service';
|
||||
|
||||
const MOBILE_MEDIA = 'only screen and (max-width: 40em)';
|
||||
const APP_CONFIG = Meteor.settings.public.app;
|
||||
@ -156,6 +157,9 @@ class App extends Component {
|
||||
settingsLayout,
|
||||
isRTL,
|
||||
hidePresentation,
|
||||
autoSwapLayout,
|
||||
shouldShowScreenshare,
|
||||
shouldShowExternalVideo,
|
||||
} = this.props;
|
||||
const { browserName } = browserInfo;
|
||||
const { osName } = deviceInfo;
|
||||
@ -167,11 +171,18 @@ class App extends Component {
|
||||
value: isRTL,
|
||||
});
|
||||
|
||||
const presentationOpen = !(autoSwapLayout || hidePresentation)
|
||||
|| shouldShowExternalVideo || shouldShowScreenshare;
|
||||
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_PRESENTATION_IS_OPEN,
|
||||
value: !hidePresentation,
|
||||
value: presentationOpen,
|
||||
});
|
||||
|
||||
if (!presentationOpen && !MediaService.getSwapLayout()) {
|
||||
MediaService.setSwapLayout(layoutContextDispatch);
|
||||
}
|
||||
|
||||
Modal.setAppElement('#app');
|
||||
|
||||
const fontSize = isMobile() ? MOBILE_FONT_SIZE : DESKTOP_FONT_SIZE;
|
||||
|
@ -230,6 +230,7 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
|
||||
Meteor.settings.public.presentation.restoreOnUpdate,
|
||||
),
|
||||
hidePresentation: getFromUserSettings('bbb_hide_presentation', LAYOUT_CONFIG.hidePresentation),
|
||||
autoSwapLayout: getFromUserSettings('bbb_auto_swap_layout', LAYOUT_CONFIG.autoSwapLayout),
|
||||
hideActionsBar: getFromUserSettings('bbb_hide_actions_bar', false),
|
||||
isModalOpen: !!getModal(),
|
||||
};
|
||||
|
@ -41,7 +41,7 @@ const handleLeaveAudio = () => {
|
||||
Storage.setItem('getEchoTest', true);
|
||||
}
|
||||
|
||||
Service.exitAudio();
|
||||
Service.forceExitAudio();
|
||||
logger.info({
|
||||
logCode: 'audiocontrols_leave_audio',
|
||||
extraInfo: { logType: 'user_action' },
|
||||
|
@ -267,28 +267,7 @@ class AudioModal extends Component {
|
||||
disableActions: false,
|
||||
});
|
||||
}).catch((err) => {
|
||||
const { type } = err;
|
||||
switch (type) {
|
||||
case 'MEDIA_ERROR':
|
||||
this.setState({
|
||||
content: 'help',
|
||||
errCode: 0,
|
||||
disableActions: false,
|
||||
});
|
||||
break;
|
||||
case 'CONNECTION_ERROR':
|
||||
this.setState({
|
||||
errCode: 0,
|
||||
disableActions: false,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
this.setState({
|
||||
errCode: 0,
|
||||
disableActions: false,
|
||||
});
|
||||
break;
|
||||
}
|
||||
this.handleJoinMicrophoneError(err);
|
||||
});
|
||||
}
|
||||
|
||||
@ -342,7 +321,29 @@ class AudioModal extends Component {
|
||||
this.setState({
|
||||
disableActions: false,
|
||||
});
|
||||
}).catch(this.handleGoToAudioOptions);
|
||||
}).catch((err) => {
|
||||
this.handleJoinMicrophoneError(err);
|
||||
});
|
||||
}
|
||||
|
||||
handleJoinMicrophoneError(err) {
|
||||
const { type } = err;
|
||||
switch (type) {
|
||||
case 'MEDIA_ERROR':
|
||||
this.setState({
|
||||
content: 'help',
|
||||
errCode: 0,
|
||||
disableActions: false,
|
||||
});
|
||||
break;
|
||||
case 'CONNECTION_ERROR':
|
||||
default:
|
||||
this.setState({
|
||||
errCode: 0,
|
||||
disableActions: false,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setContent(content) {
|
||||
|
@ -14,6 +14,8 @@ import { screenshareHasEnded } from '/imports/ui/components/screenshare/service'
|
||||
import AudioManager from '/imports/ui/services/audio-manager';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import BreakoutDropdown from '/imports/ui/components/breakout-room/breakout-dropdown/component';
|
||||
import Users from '/imports/api/users';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
breakoutTitle: {
|
||||
@ -282,7 +284,8 @@ class BreakoutRoom extends PureComponent {
|
||||
amIPresenter,
|
||||
intl,
|
||||
isUserInBreakoutRoom,
|
||||
exitAudio,
|
||||
forceExitAudio,
|
||||
rejoinAudio,
|
||||
setBreakoutAudioTransferStatus,
|
||||
getBreakoutAudioTransferStatus,
|
||||
} = this.props;
|
||||
@ -349,7 +352,7 @@ class BreakoutRoom extends PureComponent {
|
||||
this.getBreakoutURL(breakoutId);
|
||||
// leave main room's audio,
|
||||
// and stops video and screenshare when joining a breakout room
|
||||
exitAudio();
|
||||
forceExitAudio();
|
||||
logger.info({
|
||||
logCode: 'breakoutroom_join',
|
||||
extraInfo: { logType: 'user_action' },
|
||||
@ -357,6 +360,31 @@ class BreakoutRoom extends PureComponent {
|
||||
VideoService.storeDeviceIds();
|
||||
VideoService.exitVideo();
|
||||
if (amIPresenter) screenshareHasEnded();
|
||||
|
||||
Tracker.autorun((c) => {
|
||||
const selector = {
|
||||
meetingId: breakoutId,
|
||||
};
|
||||
|
||||
const query = Users.find(selector, {
|
||||
fields: {
|
||||
loggedOut: 1,
|
||||
extId: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const observeLogOut = (user) => {
|
||||
if (user?.loggedOut && user?.extId?.startsWith(Auth.userID)) {
|
||||
rejoinAudio();
|
||||
c.stop();
|
||||
}
|
||||
}
|
||||
|
||||
query.observe({
|
||||
added: observeLogOut,
|
||||
changed: observeLogOut,
|
||||
});
|
||||
});
|
||||
}}
|
||||
disabled={disable}
|
||||
/>
|
||||
|
@ -7,6 +7,11 @@ import Service from './service';
|
||||
import { layoutDispatch } from '../layout/context';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
|
||||
import {
|
||||
didUserSelectedMicrophone,
|
||||
didUserSelectedListenOnly,
|
||||
} from '/imports/ui/components/audio/audio-modal/service';
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
|
||||
const BreakoutContainer = (props) => {
|
||||
const layoutContextDispatch = layoutDispatch();
|
||||
@ -45,6 +50,30 @@ export default withTracker((props) => {
|
||||
getBreakoutAudioTransferStatus,
|
||||
} = AudioService;
|
||||
|
||||
const logUserCouldNotRejoinAudio = () => {
|
||||
logger.warn({
|
||||
logCode: 'mainroom_audio_rejoin',
|
||||
extraInfo: { logType: 'user_action' },
|
||||
}, 'leaving breakout room couldn\'t rejoin audio in the main room');
|
||||
};
|
||||
|
||||
const rejoinAudio = () => {
|
||||
if (didUserSelectedMicrophone()) {
|
||||
AudioManager.joinMicrophone().then(() => {
|
||||
makeCall('toggleVoice', null, true).catch(() => {
|
||||
AudioManager.forceExitAudio();
|
||||
logUserCouldNotRejoinAudio();
|
||||
});
|
||||
}).catch(() => {
|
||||
logUserCouldNotRejoinAudio();
|
||||
});
|
||||
} else if (didUserSelectedListenOnly()) {
|
||||
AudioManager.joinListenOnly().catch(() => {
|
||||
logUserCouldNotRejoinAudio();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...props,
|
||||
breakoutRooms,
|
||||
@ -61,7 +90,8 @@ export default withTracker((props) => {
|
||||
amIModerator: amIModerator(),
|
||||
isMeteorConnected,
|
||||
isUserInBreakoutRoom,
|
||||
exitAudio: () => AudioManager.exitAudio(),
|
||||
forceExitAudio: () => AudioManager.forceExitAudio(),
|
||||
rejoinAudio,
|
||||
isReconnecting,
|
||||
setBreakoutAudioTransferStatus,
|
||||
getBreakoutAudioTransferStatus,
|
||||
|
@ -68,8 +68,6 @@ const ChatAlert = (props) => {
|
||||
unreadMessagesByChat,
|
||||
intl,
|
||||
layoutContextDispatch,
|
||||
chatsTracker,
|
||||
notify,
|
||||
} = props;
|
||||
|
||||
const [unreadMessagesCount, setUnreadMessagesCount] = useState(0);
|
||||
@ -105,35 +103,6 @@ const ChatAlert = (props) => {
|
||||
}
|
||||
}, [pushAlertEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
const keys = Object.keys(chatsTracker);
|
||||
keys.forEach((key) => {
|
||||
if (chatsTracker[key]?.shouldNotify) {
|
||||
if (audioAlertEnabled) {
|
||||
AudioService.playAlertSound(`${Meteor.settings.public.app.cdn
|
||||
+ Meteor.settings.public.app.basename
|
||||
+ Meteor.settings.public.app.instanceId}`
|
||||
+ '/resources/sounds/notify.mp3');
|
||||
}
|
||||
if (pushAlertEnabled) {
|
||||
notify(
|
||||
key === 'MAIN-PUBLIC-GROUP-CHAT'
|
||||
? intl.formatMessage(intlMessages.publicChatMsg)
|
||||
: intl.formatMessage(intlMessages.privateChatMsg),
|
||||
'info',
|
||||
'chat',
|
||||
{ autoClose: 3000 },
|
||||
<div>
|
||||
<div style={{ fontWeight: 700 }}>{chatsTracker[key].lastSender}</div>
|
||||
<div dangerouslySetInnerHTML={{ __html: chatsTracker[key].content }} />
|
||||
</div>,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [chatsTracker]);
|
||||
|
||||
useEffect(() => {
|
||||
if (pushAlertEnabled) {
|
||||
const alertsObject = unreadMessagesByChat;
|
||||
|
@ -1,7 +1,5 @@
|
||||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import ChatAlert from './component';
|
||||
import { layoutSelect, layoutSelectInput, layoutDispatch } from '../../layout/context';
|
||||
import { PANELS } from '../../layout/enums';
|
||||
@ -18,15 +16,6 @@ const propTypes = {
|
||||
pushAlertEnabled: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
// custom hook for getting previous value
|
||||
function usePrevious(value) {
|
||||
const ref = React.useRef();
|
||||
React.useEffect(() => {
|
||||
ref.current = value;
|
||||
});
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
const ChatAlertContainer = (props) => {
|
||||
const idChatOpen = layoutSelect((i) => i.idChatOpen);
|
||||
const sidebarContent = layoutSelectInput((i) => i.sidebarContent);
|
||||
@ -65,47 +54,9 @@ const ChatAlertContainer = (props) => {
|
||||
})
|
||||
: null;
|
||||
|
||||
const chatsTracker = {};
|
||||
|
||||
if (usingChatContext.chats) {
|
||||
const chatsActive = Object.entries(usingChatContext.chats);
|
||||
chatsActive.forEach((c) => {
|
||||
try {
|
||||
if (c[0] === idChat || (c[0] === 'MAIN-PUBLIC-GROUP-CHAT' && idChat === 'public')) {
|
||||
chatsTracker[c[0]] = {};
|
||||
if (c[1]?.posJoinMessages || c[1]?.messageGroups) {
|
||||
const m = Object.entries(c[1]?.posJoinMessages || c[1]?.messageGroups);
|
||||
const sameUserCount = m.filter((message) => message[1]?.sender === Auth.userID).length;
|
||||
if (m[m.length - 1] && m[m.length - 1][1]?.sender !== Auth.userID) {
|
||||
chatsTracker[c[0]].lastSender = users[Auth.meetingID][c[1]?.lastSender]?.name;
|
||||
chatsTracker[c[0]].content = m[m.length - 1][1]?.message;
|
||||
chatsTracker[c[0]].count = m?.length - sameUserCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error({
|
||||
logCode: 'chat_alert_component_error',
|
||||
}, 'Error : ', e.error);
|
||||
}
|
||||
});
|
||||
|
||||
const prevTracker = usePrevious(chatsTracker);
|
||||
|
||||
if (prevTracker) {
|
||||
const keys = Object.keys(prevTracker);
|
||||
keys.forEach((key) => {
|
||||
if (chatsTracker[key]?.count > (prevTracker[key]?.count || 0)) {
|
||||
chatsTracker[key].shouldNotify = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ChatAlert
|
||||
{...props}
|
||||
chatsTracker={chatsTracker}
|
||||
layoutContextDispatch={layoutContextDispatch}
|
||||
unreadMessagesCountByChat={unreadMessagesCountByChat}
|
||||
unreadMessagesByChat={unreadMessagesByChat}
|
||||
|
@ -66,10 +66,10 @@ class TimeWindowChatItem extends PureComponent {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { height, forceCacheUpdate, systemMessage, index } = this.props;
|
||||
const { height, forceCacheUpdate, index } = this.props;
|
||||
const elementHeight = this.itemRef ? this.itemRef.clientHeight : null;
|
||||
|
||||
if (systemMessage && elementHeight && height !== 'auto' && elementHeight !== height && this.state.forcedUpdateCount < 10) {
|
||||
if (elementHeight && height !== 'auto' && elementHeight !== height && this.state.forcedUpdateCount < 10) {
|
||||
// forceCacheUpdate() internally calls forceUpdate(), so we need a stop flag
|
||||
// and cannot rely on shouldComponentUpdate() and other comparisons.
|
||||
forceCacheUpdate(index);
|
||||
@ -158,7 +158,10 @@ class TimeWindowChatItem extends PureComponent {
|
||||
const emphasizedText = messageFromModerator && CHAT_EMPHASIZE_TEXT && chatId === CHAT_PUBLIC_ID;
|
||||
|
||||
return (
|
||||
<Styled.Item key={`time-window-${messageKey}`}>
|
||||
<Styled.Item
|
||||
key={`time-window-${messageKey}`}
|
||||
ref={element => this.itemRef = element}
|
||||
>
|
||||
<Styled.Wrapper isSystemSender={isSystemSender}>
|
||||
<Styled.AvatarWrapper>
|
||||
<UserAvatar
|
||||
@ -280,11 +283,7 @@ class TimeWindowChatItem extends PureComponent {
|
||||
return this.renderSystemMessage();
|
||||
}
|
||||
|
||||
return (
|
||||
<Styled.Item>
|
||||
{this.renderMessageItem()}
|
||||
</Styled.Item>
|
||||
);
|
||||
return this.renderMessageItem();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -101,6 +101,7 @@ const Meta = styled.div`
|
||||
flex: 1;
|
||||
flex-flow: row;
|
||||
line-height: 1.35;
|
||||
align-items: baseline;
|
||||
`;
|
||||
|
||||
const Name = styled.div`
|
||||
|
@ -33,6 +33,15 @@ class BBBMenu extends React.Component {
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const { anchorEl } = this.state;
|
||||
if (this.props.open === false && anchorEl) {
|
||||
this.setState({ anchorEl: null });
|
||||
} else if (this.props.open === true && !anchorEl) {
|
||||
this.setState({ anchorEl: this.anchorElRef });
|
||||
}
|
||||
}
|
||||
|
||||
handleClick(event) {
|
||||
this.setState({ anchorEl: event.currentTarget });
|
||||
};
|
||||
@ -129,6 +138,7 @@ class BBBMenu extends React.Component {
|
||||
this.handleClick(e);
|
||||
}}
|
||||
accessKey={this.props?.accessKey}
|
||||
ref={(ref) => this.anchorElRef = ref}
|
||||
>
|
||||
{trigger}
|
||||
</div>
|
||||
|
@ -71,7 +71,9 @@ class ConfirmationModal extends Component {
|
||||
</Styled.Title>
|
||||
</Styled.Header>
|
||||
<Styled.Description>
|
||||
<span dangerouslySetInnerHTML={{ __html: description }} />
|
||||
<Styled.DescriptionText>
|
||||
{description}
|
||||
</Styled.DescriptionText>
|
||||
{ hasCheckbox ? (
|
||||
<label htmlFor="confirmationCheckbox" key="confirmation-checkbox">
|
||||
<Styled.Checkbox
|
||||
|
@ -55,6 +55,10 @@ const Description = styled.div`
|
||||
margin-bottom: ${jumboPaddingY};
|
||||
`;
|
||||
|
||||
const DescriptionText = styled.span`
|
||||
white-space: pre-line;
|
||||
`;
|
||||
|
||||
const Checkbox = styled.input`
|
||||
position: relative;
|
||||
top: 0.134rem;
|
||||
@ -85,6 +89,7 @@ export default {
|
||||
Header,
|
||||
Title,
|
||||
Description,
|
||||
DescriptionText,
|
||||
Checkbox,
|
||||
Footer,
|
||||
ConfirmationButton,
|
||||
|
@ -69,6 +69,7 @@ class Switch extends Toggle {
|
||||
hasFocus={hasFocus}
|
||||
disabled={disabled}
|
||||
animations={animations}
|
||||
isRTL={document.getElementsByTagName('html')[0].dir === 'rtl'}
|
||||
/>
|
||||
|
||||
<Styled.ScreenreaderInput
|
||||
|
@ -1,4 +1,4 @@
|
||||
import styled from 'styled-components';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { borderSize } from '/imports/ui/stylesheets/styled-components/general';
|
||||
import { colorDanger, colorSuccess } from '/imports/ui/stylesheets/styled-components/palette';
|
||||
|
||||
@ -121,7 +121,7 @@ const ToggleTrackX = styled.div`
|
||||
const ToggleThumb = styled.div`
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
left: ${({ isRTL }) => isRTL ? '2.6rem' : '1px'};
|
||||
width: 1.35rem;
|
||||
height: 1.35rem;
|
||||
border-radius: 50%;
|
||||
@ -129,16 +129,12 @@ const ToggleThumb = styled.div`
|
||||
box-sizing: border-box;
|
||||
box-shadow: 2px 0px 10px -1px rgba(0,0,0,0.4);
|
||||
|
||||
[dir="rtl"] & {
|
||||
left: 2.6rem;
|
||||
}
|
||||
|
||||
${({ animations }) => animations && `
|
||||
transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1) 0ms;
|
||||
`}
|
||||
|
||||
${({ checked }) => checked && `
|
||||
left: 2.1rem;
|
||||
${({ checked }) => checked && css`
|
||||
left: ${({ isRTL }) => isRTL ? '1px' : '2.1rem' };
|
||||
box-shadow: -2px 0px 10px -1px rgba(0,0,0,0.4);
|
||||
`}
|
||||
|
||||
|
@ -29,6 +29,7 @@ const intlMessages = defineMessages({
|
||||
});
|
||||
|
||||
let stats = -1;
|
||||
let lastRtt = null;
|
||||
const statsDep = new Tracker.Dependency();
|
||||
|
||||
let statsTimeout = null;
|
||||
@ -111,11 +112,12 @@ const addConnectionStatus = (level, type, value) => {
|
||||
|
||||
const fetchRoundTripTime = () => {
|
||||
const t0 = Date.now();
|
||||
makeCall('voidConnection').then(() => {
|
||||
makeCall('voidConnection', lastRtt).then(() => {
|
||||
const tf = Date.now();
|
||||
const rtt = tf - t0;
|
||||
const event = new CustomEvent('socketstats', { detail: { rtt } });
|
||||
window.dispatchEvent(event);
|
||||
lastRtt = rtt;
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -46,7 +46,8 @@ class EndMeetingComponent extends PureComponent {
|
||||
: intl.formatMessage(intlMessages.endMeetingNoUserDescription);
|
||||
|
||||
if (warnAboutUnsavedContentOnMeetingEnd) {
|
||||
description += `<p>${intl.formatMessage(intlMessages.contentWarning)}</p>`;
|
||||
// the double breakline it to put one empty line between the descriptions
|
||||
description += `\n\n${intl.formatMessage(intlMessages.contentWarning)}`;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -13,6 +13,7 @@ import deviceInfo from '/imports/utils/deviceInfo';
|
||||
|
||||
import logger from '/imports/startup/client/logger';
|
||||
|
||||
import Subtitles from './subtitles/component';
|
||||
import VolumeSlider from './volume-slider/component';
|
||||
import ReloadButton from '/imports/ui/components/reload-button/component';
|
||||
import FullscreenButtonContainer from '/imports/ui/components/common/fullscreen-button/container';
|
||||
@ -34,6 +35,12 @@ const intlMessages = defineMessages({
|
||||
fullscreenLabel: {
|
||||
id: 'app.externalVideo.fullscreenLabel',
|
||||
},
|
||||
subtitlesOn: {
|
||||
id: 'app.externalVideo.subtitlesOn',
|
||||
},
|
||||
subtitlesOff: {
|
||||
id: 'app.externalVideo.subtitlesOff',
|
||||
},
|
||||
});
|
||||
|
||||
const SYNC_INTERVAL_SECONDS = 5;
|
||||
@ -69,18 +76,22 @@ class VideoPlayer extends Component {
|
||||
this.throttleTimeout = null;
|
||||
|
||||
this.state = {
|
||||
subtitlesOn: false,
|
||||
muted: false,
|
||||
playing: false,
|
||||
autoPlayBlocked: false,
|
||||
volume: 1,
|
||||
playbackRate: 1,
|
||||
key: 0,
|
||||
played:0,
|
||||
loaded:0,
|
||||
};
|
||||
|
||||
this.hideVolume = {
|
||||
Vimeo: true,
|
||||
Facebook: true,
|
||||
ArcPlayer: true,
|
||||
//YouTube: true,
|
||||
};
|
||||
|
||||
this.opts = {
|
||||
@ -113,6 +124,7 @@ class VideoPlayer extends Component {
|
||||
rel: 0,
|
||||
ecver: 2,
|
||||
controls: isPresenter ? 1 : 0,
|
||||
cc_lang_pref: document.getElementsByTagName('html')[0].lang.substring(0, 2),
|
||||
},
|
||||
},
|
||||
peertube: {
|
||||
@ -145,6 +157,7 @@ class VideoPlayer extends Component {
|
||||
this.getMuted = this.getMuted.bind(this);
|
||||
this.setPlaybackRate = this.setPlaybackRate.bind(this);
|
||||
this.onBeforeUnload = this.onBeforeUnload.bind(this);
|
||||
this.toggleSubtitle = this.toggleSubtitle.bind(this);
|
||||
|
||||
this.mobileHoverSetTimeout = null;
|
||||
}
|
||||
@ -234,6 +247,20 @@ class VideoPlayer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
toggleSubtitle() {
|
||||
this.setState((state) => {
|
||||
return { subtitlesOn: !state.subtitlesOn };
|
||||
}, () => {
|
||||
const { subtitlesOn } = this.state;
|
||||
const { isPresenter } = this.props;
|
||||
if (!isPresenter && subtitlesOn) {
|
||||
this?.player?.getInternalPlayer()?.setOption('captions', 'reload', true);
|
||||
} else {
|
||||
this?.player?.getInternalPlayer()?.unloadModule('captions');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleOnReady() {
|
||||
const { hasPlayedBefore, playerIsReady } = this;
|
||||
|
||||
@ -301,12 +328,16 @@ class VideoPlayer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleOnProgress() {
|
||||
handleOnProgress(data) {
|
||||
const { mutedByEchoTest } = this.state;
|
||||
|
||||
const volume = this.getCurrentVolume();
|
||||
const muted = this.getMuted();
|
||||
|
||||
const { played, loaded } = data;
|
||||
|
||||
this.setState({played, loaded});
|
||||
|
||||
if (!mutedByEchoTest) {
|
||||
this.setState({ volume, muted });
|
||||
}
|
||||
@ -380,10 +411,10 @@ class VideoPlayer extends Component {
|
||||
}
|
||||
|
||||
getMuted() {
|
||||
const { mutedByEchoTest } = this.state;
|
||||
const { mutedByEchoTest, muted } = this.state;
|
||||
const intPlayer = this.player && this.player.getInternalPlayer();
|
||||
|
||||
return intPlayer && intPlayer.isMuted && intPlayer.isMuted() && !mutedByEchoTest;
|
||||
return (intPlayer && intPlayer.isMuted && intPlayer.isMuted?.() && !mutedByEchoTest) || muted;
|
||||
}
|
||||
|
||||
autoPlayBlockDetected() {
|
||||
@ -549,7 +580,7 @@ class VideoPlayer extends Component {
|
||||
|
||||
const {
|
||||
playing, playbackRate, mutedByEchoTest, autoPlayBlocked,
|
||||
volume, muted, key, showHoverToolBar,
|
||||
volume, muted, key, showHoverToolBar, played, loaded, subtitlesOn
|
||||
} = this.state;
|
||||
|
||||
// This looks weird, but I need to get this nested player
|
||||
@ -578,6 +609,7 @@ class VideoPlayer extends Component {
|
||||
width,
|
||||
pointerEvents: isResizing ? 'none' : 'inherit',
|
||||
display: isMinimized && 'none',
|
||||
background: 'var(--color-black)',
|
||||
}}
|
||||
>
|
||||
<Styled.VideoPlayerWrapper
|
||||
@ -617,10 +649,7 @@ class VideoPlayer extends Component {
|
||||
!isPresenter
|
||||
? [
|
||||
(
|
||||
<Styled.HoverToolbar
|
||||
toolbarStyle={toolbarStyle}
|
||||
key="hover-toolbar-external-video"
|
||||
>
|
||||
<Styled.HoverToolbar key="hover-toolbar-external-video">
|
||||
<VolumeSlider
|
||||
hideVolume={this.hideVolume[playerName]}
|
||||
volume={volume}
|
||||
@ -628,12 +657,32 @@ class VideoPlayer extends Component {
|
||||
onMuted={this.handleOnMuted}
|
||||
onVolumeChanged={this.handleVolumeChanged}
|
||||
/>
|
||||
|
||||
<Styled.ButtonsWrapper>
|
||||
<ReloadButton
|
||||
handleReload={this.handleReload}
|
||||
label={intl.formatMessage(intlMessages.refreshLabel)}
|
||||
/>
|
||||
{playerName === 'YouTube' && (
|
||||
<Subtitles
|
||||
toggleSubtitle={this.toggleSubtitle}
|
||||
label={subtitlesOn
|
||||
? intl.formatMessage(intlMessages.subtitlesOn)
|
||||
: intl.formatMessage(intlMessages.subtitlesOff)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Styled.ButtonsWrapper>
|
||||
{this.renderFullscreenButton()}
|
||||
|
||||
<Styled.ProgressBar>
|
||||
<Styled.Loaded
|
||||
style={{ width: loaded * 100 + '%' }}
|
||||
>
|
||||
<Styled.Played
|
||||
style={{ width: played * 100 / loaded + '%'}}
|
||||
/>
|
||||
</Styled.Loaded>
|
||||
</Styled.ProgressBar>
|
||||
</Styled.HoverToolbar>
|
||||
),
|
||||
(deviceInfo.isMobile && playing) && (
|
||||
|
@ -73,10 +73,47 @@ const HoverToolbar = styled.div`
|
||||
`}
|
||||
`;
|
||||
|
||||
const ProgressBar = styled.div`
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
height: 5px;
|
||||
width: 100%;
|
||||
|
||||
background-color: transparent;
|
||||
`;
|
||||
|
||||
const Loaded = styled.div`
|
||||
height: 100%;
|
||||
background-color: gray;
|
||||
`;
|
||||
|
||||
const Played = styled.div`
|
||||
height: 100%;
|
||||
background-color: #DF2721;
|
||||
`;
|
||||
|
||||
const ButtonsWrapper = styled.div`
|
||||
position: absolute;
|
||||
right: auto;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
display: flex;
|
||||
|
||||
[dir="rtl"] & {
|
||||
right: 0;
|
||||
left : auto;
|
||||
}
|
||||
`;
|
||||
|
||||
export default {
|
||||
VideoPlayerWrapper,
|
||||
AutoPlayWarning,
|
||||
VideoPlayer,
|
||||
MobileControlsOverlay,
|
||||
HoverToolbar,
|
||||
ProgressBar,
|
||||
Loaded,
|
||||
Played,
|
||||
ButtonsWrapper,
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user