Merge branch 'v2.6.x-release' into issue-15891
This commit is contained in:
commit
26c8d31197
@ -74,7 +74,6 @@ class BigBlueButtonActor(
|
||||
|
||||
case m: CreateMeetingReqMsg => handleCreateMeetingReqMsg(m)
|
||||
case m: RegisterUserReqMsg => handleRegisterUserReqMsg(m)
|
||||
case m: EjectDuplicateUserReqMsg => handleEjectDuplicateUserReqMsg(m)
|
||||
case m: GetAllMeetingsReqMsg => handleGetAllMeetingsReqMsg(m)
|
||||
case m: GetRunningMeetingsReqMsg => handleGetRunningMeetingsReqMsg(m)
|
||||
case m: CheckAlivePingSysMsg => handleCheckAlivePingSysMsg(m)
|
||||
@ -105,16 +104,6 @@ class BigBlueButtonActor(
|
||||
}
|
||||
}
|
||||
|
||||
def handleEjectDuplicateUserReqMsg(msg: EjectDuplicateUserReqMsg): Unit = {
|
||||
log.debug("RECEIVED EjectDuplicateUserReqMsg msg {}", msg)
|
||||
for {
|
||||
m <- RunningMeetings.findWithId(meetings, msg.header.meetingId)
|
||||
} yield {
|
||||
log.debug("FORWARDING EjectDuplicateUserReqMsg")
|
||||
m.actorRef forward (msg)
|
||||
}
|
||||
}
|
||||
|
||||
def handleCreateMeetingReqMsg(msg: CreateMeetingReqMsg): Unit = {
|
||||
log.debug("RECEIVED CreateMeetingReqMsg msg {}", msg)
|
||||
|
||||
|
@ -104,6 +104,22 @@ case class SendMessageToBreakoutRoomInternalMsg(parentId: String, breakoutId: St
|
||||
*/
|
||||
case class EjectUserFromBreakoutInternalMsg(parentId: String, breakoutId: String, extUserId: String, ejectedBy: String, reason: String, reasonCode: String, ban: Boolean) extends InMessage
|
||||
|
||||
/**
|
||||
* Sent by parent meeting to breakout room to import annotated slides.
|
||||
* @param userId
|
||||
* @param parentMeetingId
|
||||
* @param allPages
|
||||
*/
|
||||
case class CapturePresentationReqInternalMsg(userId: String, parentMeetingId: String, allPages: Boolean = true) extends InMessage
|
||||
|
||||
/**
|
||||
* Sent by parent meeting to breakout room to import shared notes.
|
||||
* @param parentMeetingId
|
||||
* @param meetingName
|
||||
* @param sequence
|
||||
*/
|
||||
case class CaptureSharedNotesReqInternalMsg(parentMeetingId: String, meetingName: String, sequence: Int) extends InMessage
|
||||
|
||||
// DeskShare
|
||||
case class DeskShareStartedRequest(conferenceName: String, callerId: String, callerIdName: String) extends InMessage
|
||||
case class DeskShareStoppedRequest(conferenceName: String, callerId: String, callerIdName: String) extends InMessage
|
||||
|
@ -13,9 +13,11 @@ object BreakoutModel {
|
||||
isDefaultName: Boolean,
|
||||
freeJoin: Boolean,
|
||||
voiceConf: String,
|
||||
assignedUsers: Vector[String]
|
||||
assignedUsers: Vector[String],
|
||||
captureNotes: Boolean,
|
||||
captureSlides: Boolean,
|
||||
): BreakoutRoom2x = {
|
||||
new BreakoutRoom2x(id, externalId, name, parentId, sequence, shortName, isDefaultName, freeJoin, voiceConf, assignedUsers, Vector(), Vector(), None, false)
|
||||
new BreakoutRoom2x(id, externalId, name, parentId, sequence, shortName, isDefaultName, freeJoin, voiceConf, assignedUsers, Vector(), Vector(), None, false, captureNotes, captureSlides)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,9 +1,6 @@
|
||||
package org.bigbluebutton.core.apps
|
||||
|
||||
import org.bigbluebutton.core.util.jhotdraw.BezierWrapper
|
||||
import scala.collection.immutable.List
|
||||
import scala.collection.immutable.HashMap
|
||||
import scala.collection.JavaConverters._
|
||||
import org.bigbluebutton.common2.msgs.AnnotationVO
|
||||
import org.bigbluebutton.core.apps.whiteboard.Whiteboard
|
||||
import org.bigbluebutton.SystemConfiguration
|
||||
@ -24,86 +21,83 @@ class WhiteboardModel extends SystemConfiguration {
|
||||
}
|
||||
|
||||
private def createWhiteboard(wbId: String): Whiteboard = {
|
||||
new Whiteboard(
|
||||
Whiteboard(
|
||||
wbId,
|
||||
Array.empty[String],
|
||||
Array.empty[String],
|
||||
System.currentTimeMillis(),
|
||||
new HashMap[String, Map[String, AnnotationVO]]()
|
||||
new HashMap[String, AnnotationVO]
|
||||
)
|
||||
}
|
||||
|
||||
private def getAnnotationsByUserId(wb: Whiteboard, id: String): Map[String, AnnotationVO] = {
|
||||
wb.annotationsMap.get(id).getOrElse(Map[String, AnnotationVO]())
|
||||
}
|
||||
private def deepMerge(test: Map[String, _], that: Map[String, _]): Map[String, _] =
|
||||
(for (k <- test.keys ++ that.keys) yield {
|
||||
val newValue =
|
||||
(test.get(k), that.get(k)) match {
|
||||
case (Some(v), None) => v
|
||||
case (None, Some(v)) => v
|
||||
case (Some(v1), Some(v2)) =>
|
||||
if (v1.isInstanceOf[Map[String, _]] && v2.isInstanceOf[Map[String, _]])
|
||||
deepMerge(v1.asInstanceOf[Map[String, _]], v2.asInstanceOf[Map[String, _]])
|
||||
else v2
|
||||
case (_, _) => ???
|
||||
}
|
||||
k -> newValue
|
||||
}).toMap
|
||||
|
||||
def addAnnotations(wbId: String, userId: String, annotations: Array[AnnotationVO]): Array[AnnotationVO] = {
|
||||
def addAnnotations(wbId: String, userId: String, annotations: Array[AnnotationVO], isPresenter: Boolean, isModerator: Boolean): Array[AnnotationVO] = {
|
||||
var annotationsAdded = Array[AnnotationVO]()
|
||||
val wb = getWhiteboard(wbId)
|
||||
val usersAnnotations = getAnnotationsByUserId(wb, userId)
|
||||
var newUserAnnotations = usersAnnotations
|
||||
var newAnnotationsMap = wb.annotationsMap
|
||||
for (annotation <- annotations) {
|
||||
newUserAnnotations = newUserAnnotations + (annotation.id -> annotation)
|
||||
println("Adding annotation to page [" + wb.id + "]. After numAnnotations=[" + newUserAnnotations.size + "].")
|
||||
val oldAnnotation = wb.annotationsMap.get(annotation.id)
|
||||
if (!oldAnnotation.isEmpty) {
|
||||
val hasPermission = isPresenter || isModerator || oldAnnotation.get.userId == userId
|
||||
if (hasPermission) {
|
||||
val newAnnotation = oldAnnotation.get.copy(annotationInfo = deepMerge(oldAnnotation.get.annotationInfo, annotation.annotationInfo))
|
||||
newAnnotationsMap += (annotation.id -> newAnnotation)
|
||||
annotationsAdded :+= annotation
|
||||
println(s"Updated annotation onpage [${wb.id}]. After numAnnotations=[${newAnnotationsMap.size}].")
|
||||
} else {
|
||||
println(s"User $userId doesn't have permission to edit annotation ${annotation.id}, ignoring...")
|
||||
}
|
||||
} else if (annotation.annotationInfo.contains("type")) {
|
||||
newAnnotationsMap += (annotation.id -> annotation)
|
||||
annotationsAdded :+= annotation
|
||||
println(s"Adding annotation to page [${wb.id}]. After numAnnotations=[${newAnnotationsMap.size}].")
|
||||
} else {
|
||||
println(s"New annotation [${annotation.id}] with no type, ignoring (probably received a remove message before and now the shape is incomplete, ignoring...")
|
||||
}
|
||||
}
|
||||
val newAnnotationsMap = wb.annotationsMap + (userId -> newUserAnnotations)
|
||||
val newWb = wb.copy(annotationsMap = newAnnotationsMap)
|
||||
saveWhiteboard(newWb)
|
||||
annotations
|
||||
annotationsAdded
|
||||
}
|
||||
|
||||
def getHistory(wbId: String): Array[AnnotationVO] = {
|
||||
//wb.annotationsMap.values.flatten.toArray.sortBy(_.position);
|
||||
val wb = getWhiteboard(wbId)
|
||||
var annotations = Array[AnnotationVO]()
|
||||
// TODO: revisit this, probably there is a one-liner simple solution
|
||||
wb.annotationsMap.values.foreach(
|
||||
user => user.values.foreach(
|
||||
annotation => annotations = annotations :+ annotation
|
||||
)
|
||||
)
|
||||
annotations
|
||||
wb.annotationsMap.values.toArray
|
||||
}
|
||||
|
||||
def clearWhiteboard(wbId: String, userId: String): Option[Boolean] = {
|
||||
var cleared: Option[Boolean] = None
|
||||
|
||||
if (hasWhiteboard(wbId)) {
|
||||
val wb = getWhiteboard(wbId)
|
||||
|
||||
if (wb.multiUser.contains(userId)) {
|
||||
if (wb.annotationsMap.contains(userId)) {
|
||||
val newWb = wb.copy(annotationsMap = wb.annotationsMap - userId)
|
||||
saveWhiteboard(newWb)
|
||||
cleared = Some(false)
|
||||
}
|
||||
} else {
|
||||
if (wb.annotationsMap.nonEmpty) {
|
||||
val newWb = wb.copy(annotationsMap = new HashMap[String, Map[String, AnnotationVO]]())
|
||||
saveWhiteboard(newWb)
|
||||
cleared = Some(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
cleared
|
||||
}
|
||||
|
||||
def deleteAnnotations(wbId: String, userId: String, annotationsIds: Array[String]): Array[String] = {
|
||||
def deleteAnnotations(wbId: String, userId: String, annotationsIds: Array[String], isPresenter: Boolean, isModerator: Boolean): Array[String] = {
|
||||
var annotationsIdsRemoved = Array[String]()
|
||||
val wb = getWhiteboard(wbId)
|
||||
var newAnnotationsMap = wb.annotationsMap
|
||||
|
||||
val usersAnnotations = getAnnotationsByUserId(wb, userId)
|
||||
var newUserAnnotations = usersAnnotations
|
||||
for (annotationId <- annotationsIds) {
|
||||
val annotation = usersAnnotations.get(annotationId)
|
||||
val annotation = wb.annotationsMap.get(annotationId)
|
||||
|
||||
//not empty and annotation exists
|
||||
if (!usersAnnotations.isEmpty && !annotation.isEmpty) {
|
||||
newUserAnnotations = newUserAnnotations - annotationId
|
||||
println("Removing annotation on page [" + wb.id + "]. After numAnnotations=[" + newUserAnnotations.size + "].")
|
||||
annotationsIdsRemoved = annotationsIdsRemoved :+ annotationId
|
||||
if (!annotation.isEmpty) {
|
||||
val hasPermission = isPresenter || isModerator || annotation.get.userId == userId
|
||||
if (hasPermission) {
|
||||
newAnnotationsMap -= annotationId
|
||||
println("Removing annotation on page [" + wb.id + "]. After numAnnotations=[" + newAnnotationsMap.size + "].")
|
||||
annotationsIdsRemoved :+= annotationId
|
||||
} else {
|
||||
println("User doesn't have permission to remove this annotation, ignoring...")
|
||||
}
|
||||
}
|
||||
}
|
||||
val newAnnotationsMap = wb.annotationsMap + (userId -> newUserAnnotations)
|
||||
val newWb = wb.copy(annotationsMap = newAnnotationsMap)
|
||||
saveWhiteboard(newWb)
|
||||
annotationsIdsRemoved
|
||||
|
@ -52,7 +52,7 @@ trait BreakoutRoomCreatedMsgHdlr {
|
||||
(redirectToHtml5JoinURL, redirectJoinURL) <- BreakoutHdlrHelpers.getRedirectUrls(liveMeeting, user, r.externalId, r.sequence.toString())
|
||||
} yield (user -> redirectToHtml5JoinURL)
|
||||
|
||||
new BreakoutRoomInfo(r.name, r.externalId, r.id, r.sequence, r.shortName, r.isDefaultName, r.freeJoin, html5JoinUrls.toMap)
|
||||
new BreakoutRoomInfo(r.name, r.externalId, r.id, r.sequence, r.shortName, r.isDefaultName, r.freeJoin, html5JoinUrls.toMap, r.captureNotes, r.captureSlides)
|
||||
}
|
||||
|
||||
log.info("Sending breakout rooms list to {} with containing {} room(s)", liveMeeting.props.meetingProp.intId, breakoutRooms.length)
|
||||
@ -79,7 +79,7 @@ trait BreakoutRoomCreatedMsgHdlr {
|
||||
BbbCommonEnvCoreMsg(envelope, event)
|
||||
}
|
||||
|
||||
val breakoutInfo = BreakoutRoomInfo(room.name, room.externalId, room.id, room.sequence, room.shortName, room.isDefaultName, room.freeJoin, Map())
|
||||
val breakoutInfo = BreakoutRoomInfo(room.name, room.externalId, room.id, room.sequence, room.shortName, room.isDefaultName, room.freeJoin, Map(), room.captureNotes, room.captureSlides)
|
||||
val event = build(liveMeeting.props.meetingProp.intId, breakoutInfo)
|
||||
outGW.send(event)
|
||||
|
||||
|
@ -28,7 +28,7 @@ trait BreakoutRoomsListMsgHdlr {
|
||||
breakoutModel <- state.breakout
|
||||
} yield {
|
||||
val rooms = breakoutModel.rooms.values.toVector map { r =>
|
||||
new BreakoutRoomInfo(r.name, r.externalId, r.id, r.sequence, r.shortName, r.isDefaultName, r.freeJoin, Map())
|
||||
new BreakoutRoomInfo(r.name, r.externalId, r.id, r.sequence, r.shortName, r.isDefaultName, r.freeJoin, Map(), r.captureNotes, r.captureSlides)
|
||||
}
|
||||
val ready = breakoutModel.hasAllStarted()
|
||||
broadcastEvent(rooms, ready)
|
||||
|
@ -52,7 +52,7 @@ trait CreateBreakoutRoomsCmdMsgHdlr extends RightsManagementTrait {
|
||||
val (internalId, externalId) = BreakoutRoomsUtil.createMeetingIds(liveMeeting.props.meetingProp.intId, i)
|
||||
val voiceConf = BreakoutRoomsUtil.createVoiceConfId(liveMeeting.props.voiceProp.voiceConf, i)
|
||||
|
||||
val breakout = BreakoutModel.create(parentId, internalId, externalId, room.name, room.sequence, room.shortName, room.isDefaultName, room.freeJoin, voiceConf, room.users)
|
||||
val breakout = BreakoutModel.create(parentId, internalId, externalId, room.name, room.sequence, room.shortName, room.isDefaultName, room.freeJoin, voiceConf, room.users, msg.body.captureNotes, msg.body.captureSlides)
|
||||
rooms = rooms + (breakout.id -> breakout)
|
||||
}
|
||||
|
||||
@ -70,7 +70,9 @@ trait CreateBreakoutRoomsCmdMsgHdlr extends RightsManagementTrait {
|
||||
liveMeeting.props.password.moderatorPass,
|
||||
liveMeeting.props.password.viewerPass,
|
||||
presId, presSlide, msg.body.record,
|
||||
liveMeeting.props.breakoutProps.privateChatEnabled
|
||||
liveMeeting.props.breakoutProps.privateChatEnabled,
|
||||
breakout.captureNotes,
|
||||
breakout.captureSlides,
|
||||
)
|
||||
|
||||
val event = buildCreateBreakoutRoomSysCmdMsg(liveMeeting.props.meetingProp.intId, roomDetail)
|
||||
|
@ -14,8 +14,8 @@ trait EndAllBreakoutRoomsMsgHdlr extends RightsManagementTrait {
|
||||
val outGW: OutMsgRouter
|
||||
|
||||
def handleEndAllBreakoutRoomsMsg(msg: EndAllBreakoutRoomsMsg, state: MeetingState2x): MeetingState2x = {
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
val reason = "No permission to end breakout rooms for meeting."
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)
|
||||
state
|
||||
@ -24,11 +24,11 @@ trait EndAllBreakoutRoomsMsgHdlr extends RightsManagementTrait {
|
||||
model <- state.breakout
|
||||
} yield {
|
||||
model.rooms.values.foreach { room =>
|
||||
eventBus.publish(BigBlueButtonEvent(room.id, EndBreakoutRoomInternalMsg(props.breakoutProps.parentId, room.id, MeetingEndReason.BREAKOUT_ENDED_BY_MOD)))
|
||||
eventBus.publish(BigBlueButtonEvent(room.id, EndBreakoutRoomInternalMsg(meetingId, room.id, MeetingEndReason.BREAKOUT_ENDED_BY_MOD)))
|
||||
}
|
||||
|
||||
val notifyEvent = MsgBuilder.buildNotifyAllInMeetingEvtMsg(
|
||||
liveMeeting.props.meetingProp.intId,
|
||||
meetingId,
|
||||
"info",
|
||||
"rooms",
|
||||
"app.toast.breakoutRoomEnded",
|
||||
|
@ -1,7 +1,7 @@
|
||||
package org.bigbluebutton.core.apps.breakout
|
||||
|
||||
import org.bigbluebutton.core.api.EndBreakoutRoomInternalMsg
|
||||
import org.bigbluebutton.core.bus.{ InternalEventBus }
|
||||
import org.bigbluebutton.core.api.{ CaptureSharedNotesReqInternalMsg, CapturePresentationReqInternalMsg, EndBreakoutRoomInternalMsg }
|
||||
import org.bigbluebutton.core.bus.{ BigBlueButtonEvent, InternalEventBus }
|
||||
import org.bigbluebutton.core.running.{ BaseMeetingActor, HandlerHelpers, LiveMeeting, OutMsgRouter }
|
||||
|
||||
trait EndBreakoutRoomInternalMsgHdlr extends HandlerHelpers {
|
||||
@ -12,6 +12,18 @@ trait EndBreakoutRoomInternalMsgHdlr extends HandlerHelpers {
|
||||
val eventBus: InternalEventBus
|
||||
|
||||
def handleEndBreakoutRoomInternalMsg(msg: EndBreakoutRoomInternalMsg): Unit = {
|
||||
|
||||
if (liveMeeting.props.breakoutProps.captureSlides) {
|
||||
val captureSlidesEvent = BigBlueButtonEvent(msg.breakoutId, CapturePresentationReqInternalMsg("system", msg.parentId))
|
||||
eventBus.publish(captureSlidesEvent)
|
||||
}
|
||||
|
||||
if (liveMeeting.props.breakoutProps.captureNotes) {
|
||||
val meetingName: String = liveMeeting.props.meetingProp.name
|
||||
val captureNotesEvent = BigBlueButtonEvent(msg.breakoutId, CaptureSharedNotesReqInternalMsg(msg.parentId, meetingName, liveMeeting.props.breakoutProps.sequence))
|
||||
eventBus.publish(captureNotesEvent)
|
||||
}
|
||||
|
||||
log.info("Breakout room {} ended by parent meeting {}.", msg.breakoutId, msg.parentId)
|
||||
sendEndMeetingDueToExpiry(msg.reason, eventBus, outGW, liveMeeting, "system")
|
||||
}
|
||||
|
@ -84,10 +84,12 @@ trait CreateGroupChatReqMsgHdlr extends SystemConfiguration {
|
||||
BbbCoreEnvelope(name, routing)
|
||||
}
|
||||
|
||||
def makeBody(chatId: String,
|
||||
access: String, correlationId: String,
|
||||
createdBy: GroupChatUser, users: Vector[GroupChatUser],
|
||||
msgs: Vector[GroupChatMsgToUser]): GroupChatCreatedEvtMsgBody = {
|
||||
def makeBody(
|
||||
chatId: String,
|
||||
access: String, correlationId: String,
|
||||
createdBy: GroupChatUser, users: Vector[GroupChatUser],
|
||||
msgs: Vector[GroupChatMsgToUser]
|
||||
): GroupChatCreatedEvtMsgBody = {
|
||||
GroupChatCreatedEvtMsgBody(correlationId, chatId, createdBy,
|
||||
access, users, msgs)
|
||||
}
|
||||
|
@ -45,27 +45,31 @@ trait RespondToPollReqMsgHdlr {
|
||||
bus.outGW.send(msgEvent)
|
||||
}
|
||||
|
||||
for {
|
||||
(pollId: String, updatedPoll: SimplePollResultOutVO) <- Polls.handleRespondToPollReqMsg(msg.header.userId, msg.body.pollId,
|
||||
msg.body.questionId, msg.body.answerIds, liveMeeting)
|
||||
} yield {
|
||||
broadcastPollUpdatedEvent(msg, pollId, updatedPoll)
|
||||
if (Polls.checkUserResponded(msg.body.pollId, msg.header.userId, liveMeeting.polls) == false) {
|
||||
for {
|
||||
poll <- Polls.getPoll(pollId, liveMeeting.polls)
|
||||
(pollId: String, updatedPoll: SimplePollResultOutVO) <- Polls.handleRespondToPollReqMsg(msg.header.userId, msg.body.pollId,
|
||||
msg.body.questionId, msg.body.answerIds, liveMeeting)
|
||||
} yield {
|
||||
broadcastPollUpdatedEvent(msg, pollId, updatedPoll)
|
||||
for {
|
||||
answerId <- msg.body.answerIds
|
||||
poll <- Polls.getPoll(pollId, liveMeeting.polls)
|
||||
} yield {
|
||||
val answerText = poll.questions(0).answers.get(answerId).key
|
||||
broadcastUserRespondedToPollRecordMsg(msg, pollId, answerId, answerText, poll.isSecret)
|
||||
for {
|
||||
answerId <- msg.body.answerIds
|
||||
} yield {
|
||||
val answerText = poll.questions(0).answers.get(answerId).key
|
||||
broadcastUserRespondedToPollRecordMsg(msg, pollId, answerId, answerText, poll.isSecret)
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
presenter <- Users2x.findPresenter(liveMeeting.users2x)
|
||||
} yield {
|
||||
broadcastUserRespondedToPollRespMsg(msg, pollId, msg.body.answerIds, presenter.intId)
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
presenter <- Users2x.findPresenter(liveMeeting.users2x)
|
||||
} yield {
|
||||
broadcastUserRespondedToPollRespMsg(msg, pollId, msg.body.answerIds, presenter.intId)
|
||||
}
|
||||
} else {
|
||||
log.info("Ignoring typed answer from user {} once user already added an answer to this poll {} in meeting {}", msg.header.userId, msg.body.pollId, msg.header.meetingId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,17 +34,23 @@ trait RespondToTypedPollReqMsgHdlr {
|
||||
bus.outGW.send(msgEvent)
|
||||
}
|
||||
|
||||
for {
|
||||
(pollId: String, updatedPoll: SimplePollResultOutVO) <- Polls.handleRespondToTypedPollReqMsg(msg.header.userId, msg.body.pollId,
|
||||
msg.body.questionId, msg.body.answer, liveMeeting)
|
||||
} yield {
|
||||
broadcastPollUpdatedEvent(msg, pollId, updatedPoll)
|
||||
|
||||
if (Polls.isResponsePollType(msg.body.pollId, liveMeeting.polls) &&
|
||||
Polls.checkUserResponded(msg.body.pollId, msg.header.userId, liveMeeting.polls) == false &&
|
||||
Polls.checkUserAddedQuestion(msg.body.pollId, msg.header.userId, liveMeeting.polls) == false) {
|
||||
for {
|
||||
presenter <- Users2x.findPresenter(liveMeeting.users2x)
|
||||
(pollId: String, updatedPoll: SimplePollResultOutVO) <- Polls.handleRespondToTypedPollReqMsg(msg.header.userId, msg.body.pollId,
|
||||
msg.body.questionId, msg.body.answer, liveMeeting)
|
||||
} yield {
|
||||
broadcastUserRespondedToTypedPollRespMsg(msg, pollId, msg.body.answer, presenter.intId)
|
||||
broadcastPollUpdatedEvent(msg, pollId, updatedPoll)
|
||||
|
||||
for {
|
||||
presenter <- Users2x.findPresenter(liveMeeting.users2x)
|
||||
} yield {
|
||||
broadcastUserRespondedToTypedPollRespMsg(msg, pollId, msg.body.answer, presenter.intId)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.info("Ignoring typed answer from user {} once user already added an answer to this poll {} in meeting {}", msg.header.userId, msg.body.pollId, msg.header.meetingId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.bigbluebutton.core.apps.presentationpod
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.api.{ CapturePresentationReqInternalMsg, CaptureSharedNotesReqInternalMsg }
|
||||
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.domain.MeetingState2x
|
||||
@ -122,32 +123,27 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
|
||||
}
|
||||
}
|
||||
|
||||
def handle(m: ExportPresentationWithAnnotationReqMsg, state: MeetingState2x, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
|
||||
def handle(m: CapturePresentationReqInternalMsg, state: MeetingState2x, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
|
||||
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
val userId = m.header.userId
|
||||
|
||||
val userId = m.userId
|
||||
val presentationPods: Vector[PresentationPod] = state.presentationPodManager.getAllPresentationPodsInMeeting()
|
||||
val currentPres: Option[PresentationInPod] = presentationPods.flatMap(_.getCurrentPresentation()).headOption
|
||||
|
||||
if (liveMeeting.props.meetingProp.disabledFeatures.contains("importPresentationWithAnnotationsFromBreakoutRooms")) {
|
||||
val reason = "Importing slides from breakout rooms disabled for this meeting."
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, userId, reason, bus.outGW, liveMeeting)
|
||||
} else if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, userId)) {
|
||||
val reason = "No permission to export presentation."
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, userId, reason, bus.outGW, liveMeeting)
|
||||
log.error(s"Capturing breakout rooms slides disabled in meeting ${meetingId}.")
|
||||
} else if (currentPres.isEmpty) {
|
||||
log.error(s"No presentation set in meeting ${meetingId}")
|
||||
} else {
|
||||
|
||||
val jobId: String = RandomStringGenerator.randomAlphanumericString(16);
|
||||
val jobType = "PresentationWithAnnotationExportJob"
|
||||
val allPages: Boolean = m.body.allPages
|
||||
val allPages: Boolean = m.allPages
|
||||
val pageCount = currentPres.get.pages.size
|
||||
|
||||
val presId: String = PresentationPodsApp.getAllPresentationPodsInMeeting(state).flatMap(_.getCurrentPresentation.map(_.id)).mkString
|
||||
val presLocation = List("var", "bigbluebutton", meetingId, meetingId, presId).mkString(File.separator, File.separator, "");
|
||||
val parentMeetingId: String = m.body.parentMeetingId
|
||||
val parentMeetingId: String = m.parentMeetingId
|
||||
|
||||
val currentPage: PresentationPage = PresentationInPod.getCurrentPage(currentPres.get).get
|
||||
val pagesRange: List[Int] = if (allPages) (1 to pageCount).toList else List(currentPage.num)
|
||||
@ -183,7 +179,32 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
|
||||
log.info("Received NewPresAnnFileAvailableMsg meetingId={} presId={} fileUrl={}", liveMeeting.props.meetingProp.intId, m.body.presId, m.body.fileURI)
|
||||
|
||||
bus.outGW.send(buildBroadcastNewPresAnnFileAvailable(m, liveMeeting))
|
||||
|
||||
}
|
||||
|
||||
def handle(m: CaptureSharedNotesReqInternalMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, "not-used")
|
||||
val envelope = BbbCoreEnvelope(PresentationPageConversionStartedEventMsg.NAME, routing)
|
||||
val header = BbbClientMsgHeader(CaptureSharedNotesReqEvtMsg.NAME, meetingId, "not-used")
|
||||
val body = CaptureSharedNotesReqEvtMsgBody(m.parentMeetingId, m.meetingName, m.sequence)
|
||||
val event = CaptureSharedNotesReqEvtMsg(header, body)
|
||||
|
||||
bus.outGW.send(BbbCommonEnvCoreMsg(envelope, event))
|
||||
}
|
||||
|
||||
def handle(m: PadCapturePubMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
|
||||
|
||||
val userId: String = "system"
|
||||
val jobId: String = RandomStringGenerator.randomAlphanumericString(16);
|
||||
val jobType = "PadCaptureJob"
|
||||
val filename = s"${m.body.meetingName}-notes"
|
||||
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.sequence), m.body.parentMeetingId, presentationUploadToken)
|
||||
val job = buildStoreExportJobInRedisSysMsg(exportJob, liveMeeting)
|
||||
|
||||
bus.outGW.send(job)
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ 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)
|
||||
|
@ -1,26 +0,0 @@
|
||||
package org.bigbluebutton.core.apps.users
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.models.{ EjectReasonCode, SystemUser }
|
||||
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
|
||||
import org.bigbluebutton.core2.message.senders.Sender
|
||||
|
||||
trait EjectDuplicateUserReqMsgHdlr {
|
||||
this: UsersApp =>
|
||||
|
||||
val liveMeeting: LiveMeeting
|
||||
val outGW: OutMsgRouter
|
||||
|
||||
def handleEjectDuplicateUserReqMsg(msg: EjectDuplicateUserReqMsg) {
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
val userId = msg.body.intUserId
|
||||
val ejectedBy = SystemUser.ID
|
||||
|
||||
val reason = "user ejected because of duplicate external userid"
|
||||
UsersApp.ejectUserFromMeeting(outGW, liveMeeting, userId, ejectedBy, reason, EjectReasonCode.DUPLICATE_USER, ban = false)
|
||||
|
||||
// send a system message to force disconnection
|
||||
Sender.sendDisconnectClientSysMsg(meetingId, userId, ejectedBy, EjectReasonCode.DUPLICATE_USER, outGW)
|
||||
}
|
||||
|
||||
}
|
@ -3,7 +3,7 @@ package org.bigbluebutton.core.apps.users
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.models._
|
||||
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
|
||||
import org.bigbluebutton.core2.message.senders.MsgBuilder
|
||||
import org.bigbluebutton.core2.message.senders.{ MsgBuilder, Sender }
|
||||
|
||||
trait RegisterUserReqMsgHdlr {
|
||||
this: UsersApp =>
|
||||
@ -22,12 +22,44 @@ trait RegisterUserReqMsgHdlr {
|
||||
val event = UserRegisteredRespMsg(header, body)
|
||||
BbbCommonEnvCoreMsg(envelope, event)
|
||||
}
|
||||
|
||||
def checkUserConcurrentAccesses(regUser: RegisteredUser): Unit = {
|
||||
//Remove concurrent accesses over the limit
|
||||
if (liveMeeting.props.usersProp.maxUserConcurrentAccesses > 0) {
|
||||
val userConcurrentAccesses = RegisteredUsers.findAllWithExternUserId(regUser.externId, liveMeeting.registeredUsers)
|
||||
.filter(u => !u.loggedOut)
|
||||
.sortWith((u1, u2) => u1.registeredOn > u2.registeredOn) //Remove older first
|
||||
|
||||
val userAvailableSlots = liveMeeting.props.usersProp.maxUserConcurrentAccesses - userConcurrentAccesses.length
|
||||
if (userAvailableSlots <= 0) {
|
||||
(liveMeeting.props.usersProp.maxUserConcurrentAccesses to userConcurrentAccesses.length) foreach {
|
||||
idxUserToRemove =>
|
||||
{
|
||||
val userToRemove = userConcurrentAccesses(idxUserToRemove - 1)
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
|
||||
log.info(s"User ${regUser.id} with extId=${regUser.externId} has ${userConcurrentAccesses.length} concurrent accesses and limit is ${liveMeeting.props.usersProp.maxUserConcurrentAccesses}. " +
|
||||
s"Ejecting the oldest=${userToRemove.id} in meetingId=${meetingId}")
|
||||
|
||||
val reason = "user ejected because of duplicate external userid"
|
||||
UsersApp.ejectUserFromMeeting(outGW, liveMeeting, userToRemove.id, SystemUser.ID, reason, EjectReasonCode.DUPLICATE_USER, ban = false)
|
||||
|
||||
// send a system message to force disconnection
|
||||
Sender.sendDisconnectClientSysMsg(meetingId, userToRemove.id, SystemUser.ID, EjectReasonCode.DUPLICATE_USER, outGW)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val guestStatus = msg.body.guestStatus
|
||||
|
||||
val regUser = RegisteredUsers.create(msg.body.intUserId, msg.body.extUserId,
|
||||
msg.body.name, msg.body.role, msg.body.authToken,
|
||||
msg.body.avatarURL, msg.body.guest, msg.body.authed, guestStatus, msg.body.excludeFromDashboard, false)
|
||||
|
||||
checkUserConcurrentAccesses(regUser)
|
||||
|
||||
RegisteredUsers.add(liveMeeting.registeredUsers, regUser)
|
||||
|
||||
log.info("Register user success. meetingId=" + liveMeeting.props.meetingProp.intId
|
||||
|
@ -3,7 +3,7 @@ package org.bigbluebutton.core.apps.users
|
||||
import org.bigbluebutton.common2.msgs.UserJoinMeetingReqMsg
|
||||
import org.bigbluebutton.core.apps.breakout.BreakoutHdlrHelpers
|
||||
import org.bigbluebutton.core.domain.MeetingState2x
|
||||
import org.bigbluebutton.core.models.{ Users2x, VoiceUsers }
|
||||
import org.bigbluebutton.core.models.{ RegisteredUser, RegisteredUsers, Users2x, VoiceUsers }
|
||||
import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting, MeetingActor, OutMsgRouter }
|
||||
|
||||
trait UserJoinMeetingReqMsgHdlr extends HandlerHelpers {
|
||||
@ -26,16 +26,31 @@ trait UserJoinMeetingReqMsgHdlr extends HandlerHelpers {
|
||||
|
||||
state
|
||||
case None =>
|
||||
val newState = userJoinMeeting(outGW, msg.body.authToken, msg.body.clientType, liveMeeting, state)
|
||||
|
||||
if (liveMeeting.props.meetingProp.isBreakout) {
|
||||
BreakoutHdlrHelpers.updateParentMeetingWithUsers(liveMeeting, eventBus)
|
||||
// Check if maxParticipants has been reached
|
||||
// User are able to reenter if he already joined previously with the same extId
|
||||
val userHasJoinedAlready = RegisteredUsers.findWithUserId(msg.body.userId, liveMeeting.registeredUsers) match {
|
||||
case Some(regUser: RegisteredUser) => RegisteredUsers.checkUserExtIdHasJoined(regUser.externId, liveMeeting.registeredUsers)
|
||||
case None => false
|
||||
}
|
||||
val hasReachedMaxParticipants = liveMeeting.props.usersProp.maxUsers > 0 &&
|
||||
RegisteredUsers.numUniqueJoinedUsers(liveMeeting.registeredUsers) >= liveMeeting.props.usersProp.maxUsers &&
|
||||
userHasJoinedAlready == false
|
||||
|
||||
// fresh user joined (not due to reconnection). Clear (pop) the cached voice user
|
||||
VoiceUsers.recoverVoiceUser(liveMeeting.voiceUsers, msg.body.userId)
|
||||
if (!hasReachedMaxParticipants) {
|
||||
val newState = userJoinMeeting(outGW, msg.body.authToken, msg.body.clientType, liveMeeting, state)
|
||||
|
||||
newState
|
||||
if (liveMeeting.props.meetingProp.isBreakout) {
|
||||
BreakoutHdlrHelpers.updateParentMeetingWithUsers(liveMeeting, eventBus)
|
||||
}
|
||||
|
||||
// fresh user joined (not due to reconnection). Clear (pop) the cached voice user
|
||||
VoiceUsers.recoverVoiceUser(liveMeeting.voiceUsers, msg.body.userId)
|
||||
|
||||
newState
|
||||
} else {
|
||||
log.info("Ignoring user {} attempt to join, once the meeting {} has reached max participants: {}", msg.body.userId, msg.header.meetingId, liveMeeting.props.usersProp.maxUsers)
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -158,7 +158,6 @@ class UsersApp(
|
||||
with SelectRandomViewerReqMsgHdlr
|
||||
with AssignPresenterReqMsgHdlr
|
||||
with ChangeUserPinStateReqMsgHdlr
|
||||
with EjectDuplicateUserReqMsgHdlr
|
||||
with EjectUserFromMeetingCmdMsgHdlr
|
||||
with EjectUserFromMeetingSysMsgHdlr
|
||||
with MuteUserCmdMsgHdlr {
|
||||
|
@ -5,7 +5,7 @@ import org.bigbluebutton.core.bus.InternalEventBus
|
||||
import org.bigbluebutton.core.domain.MeetingState2x
|
||||
import org.bigbluebutton.core.models._
|
||||
import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting, OutMsgRouter }
|
||||
import org.bigbluebutton.core2.message.senders.{ MsgBuilder, Sender }
|
||||
import org.bigbluebutton.core2.message.senders.{ MsgBuilder }
|
||||
|
||||
trait ValidateAuthTokenReqMsgHdlr extends HandlerHelpers {
|
||||
this: UsersApp =>
|
||||
@ -24,10 +24,16 @@ trait ValidateAuthTokenReqMsgHdlr extends HandlerHelpers {
|
||||
liveMeeting.registeredUsers)
|
||||
regUser match {
|
||||
case Some(u) =>
|
||||
// Check if maxParticipants has been reached
|
||||
// User are able to reenter if he already joined previously with the same extId
|
||||
val hasReachedMaxParticipants = liveMeeting.props.usersProp.maxUsers > 0 &&
|
||||
RegisteredUsers.numUniqueJoinedUsers(liveMeeting.registeredUsers) >= liveMeeting.props.usersProp.maxUsers &&
|
||||
RegisteredUsers.checkUserExtIdHasJoined(u.externId, liveMeeting.registeredUsers) == false
|
||||
|
||||
// Check if banned user is rejoining.
|
||||
// Fail validation if ejected user is rejoining.
|
||||
// ralam april 21, 2020
|
||||
if (u.guestStatus == GuestStatus.ALLOW && !u.banned && !u.loggedOut) {
|
||||
if (u.guestStatus == GuestStatus.ALLOW && !u.banned && !u.loggedOut && !hasReachedMaxParticipants) {
|
||||
userValidated(u, state)
|
||||
} else {
|
||||
if (u.banned) {
|
||||
@ -36,6 +42,9 @@ trait ValidateAuthTokenReqMsgHdlr extends HandlerHelpers {
|
||||
} else if (u.loggedOut) {
|
||||
failReason = "User had logged out"
|
||||
failReasonCode = EjectReasonCode.USER_LOGGED_OUT
|
||||
} else if (hasReachedMaxParticipants) {
|
||||
failReason = "The maximum number of participants allowed for this meeting has been reached."
|
||||
failReasonCode = EjectReasonCode.MAX_PARTICIPANTS
|
||||
}
|
||||
validateTokenFailed(
|
||||
outGW,
|
||||
|
@ -28,11 +28,7 @@ trait ClearWhiteboardPubMsgHdlr extends RightsManagementTrait {
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
|
||||
}
|
||||
} else {
|
||||
for {
|
||||
fullClear <- clearWhiteboard(msg.body.whiteboardId, msg.header.userId, liveMeeting)
|
||||
} yield {
|
||||
broadcastEvent(msg, fullClear)
|
||||
}
|
||||
log.error("Ignoring message ClearWhiteboardPubMsg since this functions is not available in the new Whiteboard")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,14 +21,24 @@ trait DeleteWhiteboardAnnotationsPubMsgHdlr extends RightsManagementTrait {
|
||||
bus.outGW.send(msgEvent)
|
||||
}
|
||||
|
||||
if (filterWhiteboardMessage(msg.body.whiteboardId, msg.header.userId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
|
||||
val isUserAmongPresenters = !permissionFailed(
|
||||
PermissionCheck.GUEST_LEVEL,
|
||||
PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId
|
||||
)
|
||||
|
||||
val isUserModerator = !permissionFailed(
|
||||
PermissionCheck.MOD_LEVEL,
|
||||
PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId
|
||||
)
|
||||
|
||||
if (filterWhiteboardMessage(msg.body.whiteboardId, msg.header.userId, liveMeeting) && !isUserAmongPresenters) {
|
||||
if (isNonEjectionGracePeriodOver(msg.body.whiteboardId, msg.header.userId, liveMeeting)) {
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
val reason = "No permission to delete an annotation."
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
|
||||
}
|
||||
} else {
|
||||
val deletedAnnotations = deleteWhiteboardAnnotations(msg.body.whiteboardId, msg.header.userId, msg.body.annotationsIds, liveMeeting)
|
||||
val deletedAnnotations = deleteWhiteboardAnnotations(msg.body.whiteboardId, msg.header.userId, msg.body.annotationsIds, liveMeeting, isUserAmongPresenters, isUserModerator)
|
||||
if (!deletedAnnotations.isEmpty) {
|
||||
broadcastEvent(msg, deletedAnnotations)
|
||||
}
|
||||
|
@ -46,13 +46,18 @@ trait SendWhiteboardAnnotationsPubMsgHdlr extends RightsManagementTrait {
|
||||
PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId
|
||||
)
|
||||
|
||||
val isUserModerator = !permissionFailed(
|
||||
PermissionCheck.MOD_LEVEL,
|
||||
PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId
|
||||
)
|
||||
|
||||
if (isUserOneOfPermited || isUserAmongPresenters) {
|
||||
println("============= Printing Sanitized annotations ============")
|
||||
for (annotation <- msg.body.annotations) {
|
||||
printAnnotationInfo(annotation)
|
||||
}
|
||||
println("============= Printed Sanitized annotations ============")
|
||||
val annotations = sendWhiteboardAnnotations(msg.body.whiteboardId, msg.header.userId, msg.body.annotations, liveMeeting)
|
||||
val annotations = sendWhiteboardAnnotations(msg.body.whiteboardId, msg.header.userId, msg.body.annotations, liveMeeting, isUserAmongPresenters, isUserModerator)
|
||||
broadcastEvent(msg, msg.body.whiteboardId, annotations, msg.body.html5InstanceId)
|
||||
} else {
|
||||
//val meetingId = liveMeeting.props.meetingProp.intId
|
||||
|
@ -11,7 +11,7 @@ case class Whiteboard(
|
||||
multiUser: Array[String],
|
||||
oldMultiUser: Array[String],
|
||||
changedModeOn: Long,
|
||||
annotationsMap: Map[String, Map[String, AnnotationVO]]
|
||||
annotationsMap: Map[String, AnnotationVO]
|
||||
)
|
||||
|
||||
class WhiteboardApp2x(implicit val context: ActorContext)
|
||||
@ -24,9 +24,16 @@ class WhiteboardApp2x(implicit val context: ActorContext)
|
||||
|
||||
val log = Logging(context.system, getClass)
|
||||
|
||||
def sendWhiteboardAnnotations(whiteboardId: String, requesterId: String, annotations: Array[AnnotationVO], liveMeeting: LiveMeeting): Array[AnnotationVO] = {
|
||||
def sendWhiteboardAnnotations(
|
||||
whiteboardId: String,
|
||||
requesterId: String,
|
||||
annotations: Array[AnnotationVO],
|
||||
liveMeeting: LiveMeeting,
|
||||
isPresenter: Boolean,
|
||||
isModerator: Boolean
|
||||
): Array[AnnotationVO] = {
|
||||
// println("Received whiteboard annotation. status=[" + status + "], annotationType=[" + annotationType + "]")
|
||||
liveMeeting.wbModel.addAnnotations(whiteboardId, requesterId, annotations)
|
||||
liveMeeting.wbModel.addAnnotations(whiteboardId, requesterId, annotations, isPresenter, isModerator)
|
||||
}
|
||||
|
||||
def getWhiteboardAnnotations(whiteboardId: String, liveMeeting: LiveMeeting): Array[AnnotationVO] = {
|
||||
@ -34,12 +41,15 @@ class WhiteboardApp2x(implicit val context: ActorContext)
|
||||
liveMeeting.wbModel.getHistory(whiteboardId)
|
||||
}
|
||||
|
||||
def clearWhiteboard(whiteboardId: String, requesterId: String, liveMeeting: LiveMeeting): Option[Boolean] = {
|
||||
liveMeeting.wbModel.clearWhiteboard(whiteboardId, requesterId)
|
||||
}
|
||||
|
||||
def deleteWhiteboardAnnotations(whiteboardId: String, requesterId: String, annotationsIds: Array[String], liveMeeting: LiveMeeting): Array[String] = {
|
||||
liveMeeting.wbModel.deleteAnnotations(whiteboardId, requesterId, annotationsIds)
|
||||
def deleteWhiteboardAnnotations(
|
||||
whiteboardId: String,
|
||||
requesterId: String,
|
||||
annotationsIds: Array[String],
|
||||
liveMeeting: LiveMeeting,
|
||||
isPresenter: Boolean,
|
||||
isModerator: Boolean
|
||||
): Array[String] = {
|
||||
liveMeeting.wbModel.deleteAnnotations(whiteboardId, requesterId, annotationsIds, isPresenter, isModerator)
|
||||
}
|
||||
|
||||
def getWhiteboardAccess(whiteboardId: String, liveMeeting: LiveMeeting): Array[String] = {
|
||||
|
@ -14,10 +14,12 @@ case class BreakoutRoom2x(
|
||||
users: Vector[BreakoutUser],
|
||||
voiceUsers: Vector[BreakoutVoiceUser],
|
||||
startedOn: Option[Long],
|
||||
started: Boolean
|
||||
started: Boolean,
|
||||
captureNotes: Boolean,
|
||||
captureSlides: Boolean,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
case class BreakoutUser(id: String, name: String)
|
||||
case class BreakoutVoiceUser(id: String, extId: String, voiceUserId: String)
|
||||
case class BreakoutVoiceUser(id: String, extId: String, voiceUserId: String)
|
||||
|
@ -10,8 +10,8 @@ object BreakoutRooms {
|
||||
def breakoutRoomsdurationInMinutes(status: BreakoutRooms, duration: Int) = status.breakoutRoomsdurationInMinutes = duration
|
||||
|
||||
def newBreakoutRoom(parentRoomId: String, id: String, externalMeetingId: String, name: String, sequence: Integer, freeJoin: Boolean,
|
||||
voiceConfId: String, assignedUsers: Vector[String], breakoutRooms: BreakoutRooms): Option[BreakoutRoomVO] = {
|
||||
val brvo = new BreakoutRoomVO(id, externalMeetingId, name, parentRoomId, sequence, freeJoin, voiceConfId, assignedUsers, Vector())
|
||||
voiceConfId: String, assignedUsers: Vector[String], captureNotes: Boolean, captureSlides: Boolean, breakoutRooms: BreakoutRooms): Option[BreakoutRoomVO] = {
|
||||
val brvo = new BreakoutRoomVO(id, externalMeetingId, name, parentRoomId, sequence, freeJoin, voiceConfId, assignedUsers, Vector(), captureNotes, captureSlides)
|
||||
breakoutRooms.add(brvo)
|
||||
Some(brvo)
|
||||
}
|
||||
|
@ -112,7 +112,7 @@ object Polls {
|
||||
shape = pollResultToWhiteboardShape(result)
|
||||
annot <- send(result, shape)
|
||||
} yield {
|
||||
lm.wbModel.addAnnotations(annot.wbId, requesterId, Array[AnnotationVO](annot))
|
||||
lm.wbModel.addAnnotations(annot.wbId, requesterId, Array[AnnotationVO](annot), false, false)
|
||||
showPollResult(pollId, lm.polls)
|
||||
(result, annot)
|
||||
}
|
||||
@ -238,7 +238,7 @@ object Polls {
|
||||
private def handleRespondToTypedPoll(poll: SimplePollResultOutVO, requesterId: String, pollId: String, questionId: Int,
|
||||
answer: String, lm: LiveMeeting): Option[SimplePollResultOutVO] = {
|
||||
|
||||
addQuestionResponse(poll.id, questionId, answer, lm.polls)
|
||||
addQuestionResponse(poll.id, questionId, answer, requesterId, lm.polls)
|
||||
for {
|
||||
updatedPoll <- getSimplePollResult(poll.id, lm.polls)
|
||||
} yield {
|
||||
@ -355,6 +355,45 @@ object Polls {
|
||||
pvo
|
||||
}
|
||||
|
||||
def checkUserResponded(pollId: String, userId: String, polls: Polls): Boolean = {
|
||||
polls.polls.get(pollId) match {
|
||||
case Some(p) => {
|
||||
if (p.getResponders().filter(p => p.userId == userId).length > 0) {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
case None => false
|
||||
}
|
||||
}
|
||||
|
||||
def checkUserAddedQuestion(pollId: String, userId: String, polls: Polls): Boolean = {
|
||||
polls.polls.get(pollId) match {
|
||||
case Some(p) => {
|
||||
if (p.getTypedPollResponders().filter(responderId => responderId == userId).length > 0) {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
case None => false
|
||||
}
|
||||
}
|
||||
|
||||
def isResponsePollType(pollId: String, polls: Polls): Boolean = {
|
||||
polls.polls.get(pollId) match {
|
||||
case Some(p) => {
|
||||
if (p.questions.filter(q => q.questionType == PollType.ResponsePollType).length > 0) {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
case None => false
|
||||
}
|
||||
}
|
||||
|
||||
def showPollResult(pollId: String, polls: Polls) {
|
||||
polls.get(pollId) foreach {
|
||||
p =>
|
||||
@ -375,10 +414,13 @@ object Polls {
|
||||
}
|
||||
}
|
||||
|
||||
def addQuestionResponse(pollId: String, questionID: Int, answer: String, polls: Polls) {
|
||||
def addQuestionResponse(pollId: String, questionID: Int, answer: String, requesterId: String, polls: Polls) {
|
||||
polls.polls.get(pollId) match {
|
||||
case Some(p) => {
|
||||
p.addQuestionResponse(questionID, answer)
|
||||
if (!p.getTypedPollResponders().contains(requesterId)) {
|
||||
p.addTypedPollResponder(requesterId)
|
||||
p.addQuestionResponse(questionID, answer)
|
||||
}
|
||||
}
|
||||
case None =>
|
||||
}
|
||||
@ -545,6 +587,7 @@ class Poll(val id: String, val questions: Array[Question], val numRespondents: I
|
||||
private var _showResult: Boolean = false
|
||||
private var _numResponders: Int = 0
|
||||
private var _responders = new ArrayBuffer[Responder]()
|
||||
private var _respondersTypedPoll = new ArrayBuffer[String]()
|
||||
|
||||
def showingResult() { _showResult = true }
|
||||
def showResult(): Boolean = { _showResult }
|
||||
@ -561,6 +604,8 @@ class Poll(val id: String, val questions: Array[Question], val numRespondents: I
|
||||
|
||||
def addResponder(responder: Responder) { _responders += (responder) }
|
||||
def getResponders(): ArrayBuffer[Responder] = { return _responders }
|
||||
def addTypedPollResponder(responderId: String) { _respondersTypedPoll += (responderId) }
|
||||
def getTypedPollResponders(): ArrayBuffer[String] = { return _respondersTypedPoll }
|
||||
|
||||
def hasResponses(): Boolean = {
|
||||
questions.foreach(q => {
|
||||
|
@ -64,6 +64,14 @@ object RegisteredUsers {
|
||||
} yield user
|
||||
}
|
||||
|
||||
def checkUserExtIdHasJoined(externId: String, regUsers: RegisteredUsers): Boolean = {
|
||||
regUsers.toVector.filter(_.externId == externId).filter(_.joined).length > 0
|
||||
}
|
||||
|
||||
def numUniqueJoinedUsers(regUsers: RegisteredUsers): Int = {
|
||||
regUsers.toVector.filter(_.joined).map(_.externId).distinct.length
|
||||
}
|
||||
|
||||
def add(users: RegisteredUsers, user: RegisteredUser): Vector[RegisteredUser] = {
|
||||
|
||||
findWithExternUserId(user.externId, users) match {
|
||||
|
@ -407,4 +407,5 @@ object EjectReasonCode {
|
||||
val USER_INACTIVITY = "user_inactivity_eject_reason"
|
||||
val BANNED_USER_REJOINING = "banned_user_rejoining_reason"
|
||||
val USER_LOGGED_OUT = "user_logged_out_reason"
|
||||
val MAX_PARTICIPANTS = "max_participants_reason"
|
||||
}
|
||||
|
@ -65,8 +65,6 @@ class ReceivedJsonMsgHandlerActor(
|
||||
// Route via meeting manager as there is a race condition if we send directly to meeting
|
||||
// because the meeting actor might not have been created yet.
|
||||
route[RegisterUserReqMsg](meetingManagerChannel, envelope, jsonNode)
|
||||
case EjectDuplicateUserReqMsg.NAME =>
|
||||
route[EjectDuplicateUserReqMsg](meetingManagerChannel, envelope, jsonNode)
|
||||
case UserJoinMeetingReqMsg.NAME =>
|
||||
routeGenericMsg[UserJoinMeetingReqMsg](envelope, jsonNode)
|
||||
case UserJoinMeetingAfterReconnectReqMsg.NAME =>
|
||||
@ -175,6 +173,8 @@ class ReceivedJsonMsgHandlerActor(
|
||||
routePadMsg[PadPatchSysMsg](envelope, jsonNode)
|
||||
case PadUpdatePubMsg.NAME =>
|
||||
routeGenericMsg[PadUpdatePubMsg](envelope, jsonNode)
|
||||
case PadCapturePubMsg.NAME =>
|
||||
routePadMsg[PadCapturePubMsg](envelope, jsonNode)
|
||||
|
||||
// Voice
|
||||
case RecordingStartedVoiceConfEvtMsg.NAME =>
|
||||
@ -310,8 +310,6 @@ class ReceivedJsonMsgHandlerActor(
|
||||
routeGenericMsg[AssignPresenterReqMsg](envelope, jsonNode)
|
||||
case MakePresentationWithAnnotationDownloadReqMsg.NAME =>
|
||||
routeGenericMsg[MakePresentationWithAnnotationDownloadReqMsg](envelope, jsonNode)
|
||||
case ExportPresentationWithAnnotationReqMsg.NAME =>
|
||||
routeGenericMsg[ExportPresentationWithAnnotationReqMsg](envelope, jsonNode)
|
||||
case NewPresAnnFileAvailableMsg.NAME =>
|
||||
routeGenericMsg[NewPresAnnFileAvailableMsg](envelope, jsonNode)
|
||||
|
||||
|
@ -226,7 +226,7 @@ trait HandlerHelpers extends SystemConfiguration {
|
||||
model <- state.breakout
|
||||
} yield {
|
||||
model.rooms.values.foreach { room =>
|
||||
eventBus.publish(BigBlueButtonEvent(room.id, EndBreakoutRoomInternalMsg(liveMeeting.props.breakoutProps.parentId, room.id, reason)))
|
||||
eventBus.publish(BigBlueButtonEvent(room.id, EndBreakoutRoomInternalMsg(liveMeeting.props.meetingProp.intId, room.id, reason)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -250,7 +250,6 @@ class MeetingActor(
|
||||
// Handling RegisterUserReqMsg as it is forwarded from BBBActor and
|
||||
// its type is not BbbCommonEnvCoreMsg
|
||||
case m: RegisterUserReqMsg => usersApp.handleRegisterUserReqMsg(m)
|
||||
case m: EjectDuplicateUserReqMsg => usersApp.handleEjectDuplicateUserReqMsg(m)
|
||||
case m: GetAllMeetingsReqMsg => handleGetAllMeetingsReqMsg(m)
|
||||
case m: GetRunningMeetingStateReqMsg => handleGetRunningMeetingStateReqMsg(m)
|
||||
case m: ValidateConnAuthTokenSysMsg => handleValidateConnAuthTokenSysMsg(m)
|
||||
@ -283,7 +282,8 @@ class MeetingActor(
|
||||
case msg: SendMessageToBreakoutRoomInternalMsg => state = handleSendMessageToBreakoutRoomInternalMsg(msg, state, liveMeeting, msgBus)
|
||||
case msg: SendBreakoutTimeRemainingInternalMsg =>
|
||||
handleSendBreakoutTimeRemainingInternalMsg(msg)
|
||||
|
||||
case msg: CapturePresentationReqInternalMsg => presentationPodsApp.handle(msg, state, liveMeeting, msgBus)
|
||||
case msg: CaptureSharedNotesReqInternalMsg => presentationPodsApp.handle(msg, liveMeeting, msgBus)
|
||||
case msg: SendRecordingTimerInternalMsg =>
|
||||
state = usersApp.handleSendRecordingTimerInternalMsg(msg, state)
|
||||
|
||||
@ -505,8 +505,8 @@ class MeetingActor(
|
||||
case m: PreuploadedPresentationsSysPubMsg => presentationApp2x.handle(m, liveMeeting, msgBus)
|
||||
case m: AssignPresenterReqMsg => state = handlePresenterChange(m, state)
|
||||
case m: MakePresentationWithAnnotationDownloadReqMsg => presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
case m: ExportPresentationWithAnnotationReqMsg => presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
case m: NewPresAnnFileAvailableMsg => presentationPodsApp.handle(m, liveMeeting, msgBus)
|
||||
case m: PadCapturePubMsg => presentationPodsApp.handle(m, liveMeeting, msgBus)
|
||||
|
||||
// Presentation Pods
|
||||
case m: CreateNewPresentationPodPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
|
@ -117,7 +117,6 @@ class AnalyticsActor(val includeChat: Boolean) extends Actor with ActorLogging {
|
||||
// case m: StoreAnnotationsInRedisSysMsg => logMessage(msg)
|
||||
// case m: StoreExportJobInRedisSysMsg => logMessage(msg)
|
||||
case m: MakePresentationWithAnnotationDownloadReqMsg => logMessage(msg)
|
||||
case m: ExportPresentationWithAnnotationReqMsg => logMessage(msg)
|
||||
case m: NewPresAnnFileAvailableMsg => logMessage(msg)
|
||||
case m: PresentationPageConversionStartedSysMsg => logMessage(msg)
|
||||
case m: PresentationConversionEndedSysMsg => logMessage(msg)
|
||||
@ -201,6 +200,7 @@ class AnalyticsActor(val includeChat: Boolean) extends Actor with ActorLogging {
|
||||
case m: PadUpdatedEvtMsg => logMessage(msg)
|
||||
case m: PadUpdatePubMsg => logMessage(msg)
|
||||
case m: PadUpdateCmdMsg => logMessage(msg)
|
||||
case m: PadCapturePubMsg => logMessage(msg)
|
||||
|
||||
case _ => // ignore message
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ trait AppsTestFixtures {
|
||||
val meetingLayout = ""
|
||||
|
||||
val metadata: collection.immutable.Map[String, String] = Map("foo" -> "bar", "bar" -> "baz", "baz" -> "foo")
|
||||
val breakoutProps = BreakoutProps(parentId = parentMeetingId, sequence = sequence, freeJoin = false, breakoutRooms = Vector())
|
||||
val breakoutProps = BreakoutProps(parentId = parentMeetingId, sequence = sequence, freeJoin = false, captureNotes = false, captureSlides = false, breakoutRooms = Vector())
|
||||
|
||||
val meetingProp = MeetingProp(name = meetingName, extId = externalMeetingId, intId = meetingId,
|
||||
meetingCameraCap = meetingCameraCap,
|
||||
|
@ -27,7 +27,9 @@ case class BreakoutProps(
|
||||
freeJoin: Boolean,
|
||||
breakoutRooms: Vector[String],
|
||||
record: Boolean,
|
||||
privateChatEnabled: Boolean
|
||||
privateChatEnabled: Boolean,
|
||||
captureNotes: Boolean,
|
||||
captureSlides: Boolean,
|
||||
)
|
||||
|
||||
case class PasswordProp(moderatorPass: String, viewerPass: String, learningDashboardAccessToken: String)
|
||||
@ -39,14 +41,15 @@ case class WelcomeProp(welcomeMsgTemplate: String, welcomeMsg: String, modOnlyMe
|
||||
case class VoiceProp(telVoice: String, voiceConf: String, dialNumber: String, muteOnStart: Boolean)
|
||||
|
||||
case class UsersProp(
|
||||
maxUsers: Int,
|
||||
webcamsOnlyForModerator: Boolean,
|
||||
userCameraCap: Int,
|
||||
guestPolicy: String,
|
||||
meetingLayout: String,
|
||||
allowModsToUnmuteUsers: Boolean,
|
||||
allowModsToEjectCameras: Boolean,
|
||||
authenticatedGuest: Boolean
|
||||
maxUsers: Int,
|
||||
maxUserConcurrentAccesses:Int,
|
||||
webcamsOnlyForModerator: Boolean,
|
||||
userCameraCap: Int,
|
||||
guestPolicy: String,
|
||||
meetingLayout: String,
|
||||
allowModsToUnmuteUsers: Boolean,
|
||||
allowModsToEjectCameras: Boolean,
|
||||
authenticatedGuest: Boolean
|
||||
)
|
||||
|
||||
case class MetadataProp(metadata: collection.immutable.Map[String, String])
|
||||
|
@ -13,7 +13,7 @@ case class BreakoutRoomJoinURLEvtMsgBody(parentId: String, breakoutId: String, e
|
||||
object BreakoutRoomsListEvtMsg { val NAME = "BreakoutRoomsListEvtMsg" }
|
||||
case class BreakoutRoomsListEvtMsg(header: BbbClientMsgHeader, body: BreakoutRoomsListEvtMsgBody) extends BbbCoreMsg
|
||||
case class BreakoutRoomsListEvtMsgBody(meetingId: String, rooms: Vector[BreakoutRoomInfo], roomsReady: Boolean)
|
||||
case class BreakoutRoomInfo(name: String, externalId: String, breakoutId: String, sequence: Int, shortName: String, isDefaultName: Boolean, freeJoin: Boolean, html5JoinUrls: Map[String, String])
|
||||
case class BreakoutRoomInfo(name: String, externalId: String, breakoutId: String, sequence: Int, shortName: String, isDefaultName: Boolean, freeJoin: Boolean, html5JoinUrls: Map[String, String], captureNotes: Boolean, captureSlides: Boolean)
|
||||
|
||||
object BreakoutRoomsListMsg { val NAME = "BreakoutRoomsListMsg" }
|
||||
case class BreakoutRoomsListMsg(header: BbbClientMsgHeader, body: BreakoutRoomsListMsgBody) extends StandardMsg
|
||||
@ -58,7 +58,9 @@ case class BreakoutRoomDetail(
|
||||
sourcePresentationId: String,
|
||||
sourcePresentationSlide: Int,
|
||||
record: Boolean,
|
||||
privateChatEnabled: Boolean
|
||||
privateChatEnabled: Boolean,
|
||||
captureNotes: Boolean,
|
||||
captureSlides: Boolean,
|
||||
)
|
||||
|
||||
/**
|
||||
@ -66,7 +68,7 @@ case class BreakoutRoomDetail(
|
||||
*/
|
||||
object CreateBreakoutRoomsCmdMsg { val NAME = "CreateBreakoutRoomsCmdMsg" }
|
||||
case class CreateBreakoutRoomsCmdMsg(header: BbbClientMsgHeader, body: CreateBreakoutRoomsCmdMsgBody) extends StandardMsg
|
||||
case class CreateBreakoutRoomsCmdMsgBody(meetingId: String, durationInMinutes: Int, record: Boolean, rooms: Vector[BreakoutRoomMsgBody])
|
||||
case class CreateBreakoutRoomsCmdMsgBody(meetingId: String, durationInMinutes: Int, record: Boolean, captureNotes: Boolean, captureSlides: Boolean, rooms: Vector[BreakoutRoomMsgBody])
|
||||
case class BreakoutRoomMsgBody(name: String, sequence: Int, shortName: String, isDefaultName: Boolean, freeJoin: Boolean, users: Vector[String])
|
||||
|
||||
// Sent by user to request ending all the breakout rooms
|
||||
@ -123,5 +125,5 @@ case class BreakoutUserVO(id: String, name: String)
|
||||
|
||||
case class BreakoutRoomVO(id: String, externalId: String, name: String, parentId: String,
|
||||
sequence: Int, freeJoin: Boolean, voiceConf: String,
|
||||
assignedUsers: Vector[String], users: Vector[BreakoutUserVO])
|
||||
assignedUsers: Vector[String], users: Vector[BreakoutUserVO], captureNotes: Boolean, captureSlides: Boolean)
|
||||
|
||||
|
@ -113,3 +113,8 @@ case class PadUpdatePubMsgBody(externalId: String, text: String)
|
||||
object PadUpdateCmdMsg { val NAME = "PadUpdateCmdMsg" }
|
||||
case class PadUpdateCmdMsg(header: BbbCoreHeaderWithMeetingId, body: PadUpdateCmdMsgBody) extends BbbCoreMsg
|
||||
case class PadUpdateCmdMsgBody(groupId: String, name: String, text: String)
|
||||
|
||||
// pads -> apps
|
||||
object PadCapturePubMsg { val NAME = "PadCapturePubMsg" }
|
||||
case class PadCapturePubMsg(header: BbbCoreHeaderWithMeetingId, body: PadCapturePubMsgBody) extends PadStandardMsg
|
||||
case class PadCapturePubMsgBody(parentMeetingId: String, breakoutId: String, padId: String, meetingName: String, sequence: Int)
|
||||
|
@ -14,18 +14,10 @@ object MakePresentationWithAnnotationDownloadReqMsg { val NAME = "MakePresentati
|
||||
case class MakePresentationWithAnnotationDownloadReqMsg(header: BbbClientMsgHeader, body: MakePresentationWithAnnotationDownloadReqMsgBody) extends StandardMsg
|
||||
case class MakePresentationWithAnnotationDownloadReqMsgBody(presId: String, allPages: Boolean, pages: List[Int])
|
||||
|
||||
object ExportPresentationWithAnnotationReqMsg { val NAME = "ExportPresentationWithAnnotationReqMsg" }
|
||||
case class ExportPresentationWithAnnotationReqMsg(header: BbbClientMsgHeader, body: ExportPresentationWithAnnotationReqMsgBody) extends StandardMsg
|
||||
case class ExportPresentationWithAnnotationReqMsgBody(parentMeetingId: String, allPages: Boolean)
|
||||
|
||||
object NewPresAnnFileAvailableMsg { val NAME = "NewPresAnnFileAvailableMsg" }
|
||||
case class NewPresAnnFileAvailableMsg(header: BbbClientMsgHeader, body: NewPresAnnFileAvailableMsgBody) extends StandardMsg
|
||||
case class NewPresAnnFileAvailableMsgBody(fileURI: String, presId: String)
|
||||
|
||||
object NewPresAnnFileAvailableEvtMsg { val NAME = "NewPresAnnFileAvailableEvtMsg" }
|
||||
case class NewPresAnnFileAvailableEvtMsg(header: BbbClientMsgHeader, body: NewPresAnnFileAvailableEvtMsgBody) extends BbbCoreMsg
|
||||
case class NewPresAnnFileAvailableEvtMsgBody(fileURI: String, presId: String)
|
||||
|
||||
// ------------ bbb-common-web to akka-apps ------------
|
||||
|
||||
// ------------ akka-apps to client ------------
|
||||
@ -40,4 +32,13 @@ case class PresenterUnassignedEvtMsgBody(intId: String, name: String, assignedBy
|
||||
object NewPresentationEvtMsg { val NAME = "NewPresentationEvtMsg" }
|
||||
case class NewPresentationEvtMsg(header: BbbClientMsgHeader, body: NewPresentationEvtMsgBody) extends BbbCoreMsg
|
||||
case class NewPresentationEvtMsgBody(presentation: PresentationVO)
|
||||
|
||||
object NewPresAnnFileAvailableEvtMsg { val NAME = "NewPresAnnFileAvailableEvtMsg" }
|
||||
case class NewPresAnnFileAvailableEvtMsg(header: BbbClientMsgHeader, body: NewPresAnnFileAvailableEvtMsgBody) extends BbbCoreMsg
|
||||
case class NewPresAnnFileAvailableEvtMsgBody(fileURI: String, presId: String)
|
||||
|
||||
object CaptureSharedNotesReqEvtMsg { val NAME = "CaptureSharedNotesReqEvtMsg" }
|
||||
case class CaptureSharedNotesReqEvtMsg(header: BbbClientMsgHeader, body: CaptureSharedNotesReqEvtMsgBody) extends BbbCoreMsg
|
||||
case class CaptureSharedNotesReqEvtMsgBody(parentMeetingId: String, meetingName: String, sequence: Int)
|
||||
|
||||
// ------------ akka-apps to client ------------
|
||||
|
@ -1,13 +1,5 @@
|
||||
package org.bigbluebutton.common2.msgs
|
||||
|
||||
object EjectDuplicateUserReqMsg { val NAME = "EjectDuplicateUserReqMsg" }
|
||||
case class EjectDuplicateUserReqMsg(
|
||||
header: BbbCoreHeaderWithMeetingId,
|
||||
body: EjectDuplicateUserReqMsgBody
|
||||
) extends BbbCoreMsg
|
||||
case class EjectDuplicateUserReqMsgBody(meetingId: String, intUserId: String, name: String,
|
||||
extUserId: String)
|
||||
|
||||
object RegisterUserReqMsg { val NAME = "RegisterUserReqMsg" }
|
||||
case class RegisterUserReqMsg(
|
||||
header: BbbCoreHeaderWithMeetingId,
|
||||
|
@ -49,7 +49,7 @@ trait TestFixtures {
|
||||
meetingCameraCap = meetingCameraCap,
|
||||
maxPinnedCameras = maxPinnedCameras,
|
||||
isBreakout = isBreakout.booleanValue())
|
||||
val breakoutProps = BreakoutProps(parentId = parentMeetingId, sequence = sequence, freeJoin = false, breakoutRooms = Vector())
|
||||
val breakoutProps = BreakoutProps(parentId = parentMeetingId, sequence = sequence, freeJoin = false, captureNotes = false, captureSlides = false, breakoutRooms = Vector())
|
||||
|
||||
val durationProps = DurationProps(duration = durationInMinutes, createdTime = createTime, createdDate = createDate,
|
||||
meetingExpireIfNoUserJoinedInMinutes = meetingExpireIfNoUserJoinedInMinutes, meetingExpireWhenLastUserLeftInMinutes = meetingExpireWhenLastUserLeftInMinutes,
|
||||
|
@ -31,7 +31,7 @@ object Dependencies {
|
||||
val lang = "3.12.0"
|
||||
val io = "2.11.0"
|
||||
val pool = "2.11.1"
|
||||
val text = "1.9"
|
||||
val text = "1.10.0"
|
||||
|
||||
// BigBlueButton
|
||||
val bbbCommons = "0.0.21-SNAPSHOT"
|
||||
|
@ -76,6 +76,8 @@ public class ApiParams {
|
||||
public static final String UPLOAD_EXTERNAL_DESCRIPTION = "uploadExternalDescription";
|
||||
public static final String UPLOAD_EXTERNAL_URL = "uploadExternalUrl";
|
||||
|
||||
public static final String BREAKOUT_ROOMS_CAPTURE_SLIDES = "breakoutRoomsCaptureSlides";
|
||||
public static final String BREAKOUT_ROOMS_CAPTURE_NOTES = "breakoutRoomsCaptureNotes";
|
||||
public static final String BREAKOUT_ROOMS_ENABLED = "breakoutRoomsEnabled";
|
||||
public static final String BREAKOUT_ROOMS_RECORD = "breakoutRoomsRecord";
|
||||
public static final String BREAKOUT_ROOMS_PRIVATE_CHAT_ENABLED = "breakoutRoomsPrivateChatEnabled";
|
||||
|
@ -377,6 +377,8 @@ public class MeetingService implements MessageListener {
|
||||
breakoutMetadata.put("meetingId", m.getExternalId());
|
||||
breakoutMetadata.put("sequence", m.getSequence().toString());
|
||||
breakoutMetadata.put("freeJoin", m.isFreeJoin().toString());
|
||||
breakoutMetadata.put("captureSlides", m.isCaptureSlides().toString());
|
||||
breakoutMetadata.put("captureNotes", m.isCaptureNotes().toString());
|
||||
breakoutMetadata.put("parentMeetingId", m.getParentMeetingId());
|
||||
storeService.recordBreakoutInfo(m.getInternalId(), breakoutMetadata);
|
||||
}
|
||||
@ -388,6 +390,8 @@ public class MeetingService implements MessageListener {
|
||||
if (m.isBreakout()) {
|
||||
logData.put("sequence", m.getSequence());
|
||||
logData.put("freeJoin", m.isFreeJoin());
|
||||
logData.put("captureSlides", m.isCaptureSlides());
|
||||
logData.put("captureNotes", m.isCaptureNotes());
|
||||
logData.put("parentMeetingId", m.getParentMeetingId());
|
||||
}
|
||||
logData.put("name", m.getName());
|
||||
@ -415,7 +419,7 @@ public class MeetingService implements MessageListener {
|
||||
m.getLearningDashboardAccessToken(), m.getCreateTime(),
|
||||
formatPrettyDate(m.getCreateTime()), m.isBreakout(), m.getSequence(), m.isFreeJoin(), m.getMetadata(),
|
||||
m.getGuestPolicy(), m.getAuthenticatedGuest(), m.getMeetingLayout(), m.getWelcomeMessageTemplate(), m.getWelcomeMessage(), m.getModeratorOnlyMessage(),
|
||||
m.getDialNumber(), m.getMaxUsers(),
|
||||
m.getDialNumber(), m.getMaxUsers(), m.getMaxUserConcurrentAccesses(),
|
||||
m.getMeetingExpireIfNoUserJoinedInMinutes(), m.getMeetingExpireWhenLastUserLeftInMinutes(),
|
||||
m.getUserInactivityInspectTimerInMinutes(), m.getUserInactivityThresholdInMinutes(),
|
||||
m.getUserActivitySignResponseDelayInMinutes(), m.getEndWhenNoModerator(), m.getEndWhenNoModeratorDelayInMinutes(),
|
||||
@ -434,33 +438,6 @@ public class MeetingService implements MessageListener {
|
||||
}
|
||||
|
||||
private void processRegisterUser(RegisterUser message) {
|
||||
Meeting m = getMeeting(message.meetingID);
|
||||
if (m != null) {
|
||||
User prevUser = m.getUserWithExternalId(message.externUserID);
|
||||
if (prevUser != null) {
|
||||
Map<String, Object> logData = new HashMap<>();
|
||||
logData.put("meetingId", m.getInternalId());
|
||||
logData.put("externalMeetingId", m.getExternalId());
|
||||
logData.put("name", m.getName());
|
||||
logData.put("extUserId", prevUser.getExternalUserId());
|
||||
logData.put("intUserId", prevUser.getInternalUserId());
|
||||
logData.put("username", prevUser.getFullname());
|
||||
logData.put("logCode", "duplicate_user_with_external_userid");
|
||||
logData.put("description", "Duplicate user with external userid.");
|
||||
|
||||
Gson gson = new Gson();
|
||||
String logStr = gson.toJson(logData);
|
||||
log.info(" --analytics-- data={}", logStr);
|
||||
|
||||
if (!m.allowDuplicateExtUserid) {
|
||||
gw.ejectDuplicateUser(message.meetingID,
|
||||
prevUser.getInternalUserId(), prevUser.getFullname(),
|
||||
prevUser.getExternalUserId());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
gw.registerUser(message.meetingID,
|
||||
message.internalUserId, message.fullname, message.role,
|
||||
message.externUserID, message.authToken, message.avatarURL, message.guest,
|
||||
@ -661,6 +638,8 @@ public class MeetingService implements MessageListener {
|
||||
params.put(ApiParams.IS_BREAKOUT, "true");
|
||||
params.put(ApiParams.SEQUENCE, message.sequence.toString());
|
||||
params.put(ApiParams.FREE_JOIN, message.freeJoin.toString());
|
||||
params.put(ApiParams.BREAKOUT_ROOMS_CAPTURE_SLIDES, message.captureSlides.toString());
|
||||
params.put(ApiParams.BREAKOUT_ROOMS_CAPTURE_NOTES, message.captureNotes.toString());
|
||||
params.put(ApiParams.ATTENDEE_PW, message.viewerPassword);
|
||||
params.put(ApiParams.MODERATOR_PW, message.moderatorPassword);
|
||||
params.put(ApiParams.DIAL_NUMBER, message.dialNumber);
|
||||
@ -951,9 +930,8 @@ public class MeetingService implements MessageListener {
|
||||
message.name, message.role, message.avatarURL, message.guest, message.guestStatus,
|
||||
message.clientType);
|
||||
|
||||
if(m.getMaxUsers() > 0 && m.getUsers().size() >= m.getMaxUsers()) {
|
||||
if(m.getMaxUsers() > 0 && m.countUniqueExtIds() >= m.getMaxUsers()) {
|
||||
m.removeEnteredUser(user.getInternalUserId());
|
||||
gw.ejectDuplicateUser(message.meetingId, user.getInternalUserId(), user.getFullname(), user.getExternalUserId());
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -66,6 +66,8 @@ public class ParamsProcessorUtil {
|
||||
private String apiVersion;
|
||||
private boolean serviceEnabled = false;
|
||||
private String securitySalt;
|
||||
private String supportedChecksumAlgorithms;
|
||||
private String checksumHash;
|
||||
private int defaultMaxUsers = 20;
|
||||
private String defaultWelcomeMessage;
|
||||
private String defaultWelcomeMessageFooter;
|
||||
@ -106,6 +108,8 @@ public class ParamsProcessorUtil {
|
||||
|
||||
private boolean defaultBreakoutRoomsEnabled = true;
|
||||
private boolean defaultBreakoutRoomsRecord;
|
||||
private boolean defaultBreakoutRoomsCaptureSlides = false;
|
||||
private boolean defaultBreakoutRoomsCaptureNotes = false;
|
||||
private boolean defaultbreakoutRoomsPrivateChatEnabled;
|
||||
|
||||
private boolean defaultLockSettingsDisableCam;
|
||||
@ -128,6 +132,8 @@ public class ParamsProcessorUtil {
|
||||
private Integer userInactivityThresholdInMinutes = 30;
|
||||
private Integer userActivitySignResponseDelayInMinutes = 5;
|
||||
private Boolean defaultAllowDuplicateExtUserid = true;
|
||||
|
||||
private Integer maxUserConcurrentAccesses = 0;
|
||||
private Boolean defaultEndWhenNoModerator = false;
|
||||
private Integer defaultEndWhenNoModeratorDelayInMinutes = 1;
|
||||
private Integer defaultHtml5InstanceId = 1;
|
||||
@ -275,7 +281,19 @@ public class ParamsProcessorUtil {
|
||||
breakoutRoomsPrivateChatEnabled = Boolean.parseBoolean(breakoutRoomsPrivateChatEnabledParam);
|
||||
}
|
||||
|
||||
return new BreakoutRoomsParams(breakoutRoomsRecord, breakoutRoomsPrivateChatEnabled);
|
||||
Boolean breakoutRoomsCaptureSlides = defaultBreakoutRoomsCaptureSlides;
|
||||
String breakoutRoomsCaptureParam = params.get(ApiParams.BREAKOUT_ROOMS_CAPTURE_SLIDES);
|
||||
if (!StringUtils.isEmpty(breakoutRoomsCaptureParam)) {
|
||||
breakoutRoomsCaptureSlides = Boolean.parseBoolean(breakoutRoomsCaptureParam);
|
||||
}
|
||||
|
||||
Boolean breakoutRoomsCaptureNotes = defaultBreakoutRoomsCaptureNotes;
|
||||
String breakoutRoomsCaptureNotesParam = params.get(ApiParams.BREAKOUT_ROOMS_CAPTURE_NOTES);
|
||||
if (!StringUtils.isEmpty(breakoutRoomsCaptureNotesParam)) {
|
||||
breakoutRoomsCaptureNotes = Boolean.parseBoolean(breakoutRoomsCaptureNotesParam);
|
||||
}
|
||||
|
||||
return new BreakoutRoomsParams(breakoutRoomsRecord, breakoutRoomsPrivateChatEnabled, breakoutRoomsCaptureNotes, breakoutRoomsCaptureSlides);
|
||||
}
|
||||
|
||||
private LockSettingsParams processLockSettingsParams(Map<String, String> params) {
|
||||
@ -680,6 +698,11 @@ public class ParamsProcessorUtil {
|
||||
|
||||
int html5InstanceId = processHtml5InstanceId(params.get(ApiParams.HTML5_INSTANCE_ID));
|
||||
|
||||
if(defaultAllowDuplicateExtUserid == false) {
|
||||
log.warn("[DEPRECATION] use `maxUserConcurrentAccesses=1` instead of `allowDuplicateExtUserid=false`");
|
||||
maxUserConcurrentAccesses = 1;
|
||||
}
|
||||
|
||||
// Create the meeting with all passed in parameters.
|
||||
Meeting meeting = new Meeting.Builder(externalMeetingId,
|
||||
internalMeetingId, createTime).withName(meetingName)
|
||||
@ -706,7 +729,7 @@ public class ParamsProcessorUtil {
|
||||
.withMeetingLayout(meetingLayout)
|
||||
.withBreakoutRoomsParams(breakoutParams)
|
||||
.withLockSettingsParams(lockSettingsParams)
|
||||
.withAllowDuplicateExtUserid(defaultAllowDuplicateExtUserid)
|
||||
.withMaxUserConcurrentAccesses(maxUserConcurrentAccesses)
|
||||
.withHTML5InstanceId(html5InstanceId)
|
||||
.withLearningDashboardCleanupDelayInMinutes(learningDashboardCleanupMins)
|
||||
.withLearningDashboardAccessToken(learningDashboardAccessToken)
|
||||
@ -742,6 +765,8 @@ public class ParamsProcessorUtil {
|
||||
if (isBreakout) {
|
||||
meeting.setSequence(Integer.parseInt(params.get(ApiParams.SEQUENCE)));
|
||||
meeting.setFreeJoin(Boolean.parseBoolean(params.get(ApiParams.FREE_JOIN)));
|
||||
meeting.setCaptureSlides(Boolean.parseBoolean(params.get(ApiParams.BREAKOUT_ROOMS_CAPTURE_SLIDES)));
|
||||
meeting.setCaptureNotes(Boolean.parseBoolean(params.get(ApiParams.BREAKOUT_ROOMS_CAPTURE_NOTES)));
|
||||
meeting.setParentMeetingId(parentMeetingId);
|
||||
}
|
||||
|
||||
@ -978,11 +1003,39 @@ public class ParamsProcessorUtil {
|
||||
log.info("CHECKSUM={} length={}", checksum, checksum.length());
|
||||
|
||||
String data = apiCall + queryString + securitySalt;
|
||||
String cs = DigestUtils.sha1Hex(data);
|
||||
if (checksum.length() == 64) {
|
||||
cs = DigestUtils.sha256Hex(data);
|
||||
log.info("SHA256 {}", cs);
|
||||
}
|
||||
|
||||
int checksumLength = checksum.length();
|
||||
String cs = null;
|
||||
|
||||
switch(checksumLength) {
|
||||
case 40:
|
||||
if(supportedChecksumAlgorithms.contains("sha1")) {
|
||||
cs = DigestUtils.sha1Hex(data);
|
||||
log.info("SHA1 {}", cs);
|
||||
}
|
||||
break;
|
||||
case 64:
|
||||
if(supportedChecksumAlgorithms.contains("sha256")) {
|
||||
cs = DigestUtils.sha256Hex(data);
|
||||
log.info("SHA256 {}", cs);
|
||||
}
|
||||
break;
|
||||
case 96:
|
||||
if(supportedChecksumAlgorithms.contains("sha384")) {
|
||||
cs = DigestUtils.sha384Hex(data);
|
||||
log.info("SHA384 {}", cs);
|
||||
}
|
||||
break;
|
||||
case 128:
|
||||
if(supportedChecksumAlgorithms.contains("sha512")) {
|
||||
cs = DigestUtils.sha512Hex(data);
|
||||
log.info("SHA512 {}", cs);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
log.info("No algorithm could be found that matches the provided checksum length");
|
||||
}
|
||||
|
||||
if (cs == null || !cs.equals(checksum)) {
|
||||
log.info("query string after checksum removed: [{}]", queryString);
|
||||
log.info("checksumError: query string checksum failed. our: [{}], client: [{}]", cs, checksum);
|
||||
@ -1068,6 +1121,10 @@ public class ParamsProcessorUtil {
|
||||
this.securitySalt = securitySalt;
|
||||
}
|
||||
|
||||
public void setSupportedChecksumAlgorithms(String supportedChecksumAlgorithms) { this.supportedChecksumAlgorithms = supportedChecksumAlgorithms; }
|
||||
|
||||
public void setChecksumHash(String checksumHash) { this.checksumHash = checksumHash; }
|
||||
|
||||
public void setDefaultMaxUsers(int defaultMaxUsers) {
|
||||
this.defaultMaxUsers = defaultMaxUsers;
|
||||
}
|
||||
@ -1367,6 +1424,10 @@ public class ParamsProcessorUtil {
|
||||
this.defaultAllowDuplicateExtUserid = allow;
|
||||
}
|
||||
|
||||
public void setMaxUserConcurrentAccesses(Integer maxUserConcurrentAccesses) {
|
||||
this.maxUserConcurrentAccesses = maxUserConcurrentAccesses;
|
||||
}
|
||||
|
||||
public void setEndWhenNoModerator(Boolean val) {
|
||||
this.defaultEndWhenNoModerator = val;
|
||||
}
|
||||
|
@ -3,9 +3,13 @@ package org.bigbluebutton.api.domain;
|
||||
public class BreakoutRoomsParams {
|
||||
public final Boolean record;
|
||||
public final Boolean privateChatEnabled;
|
||||
public final Boolean captureNotes;
|
||||
public final Boolean captureSlides;
|
||||
|
||||
public BreakoutRoomsParams(Boolean record, Boolean privateChatEnabled) {
|
||||
public BreakoutRoomsParams(Boolean record, Boolean privateChatEnabled, Boolean captureNotes, Boolean captureSlides) {
|
||||
this.record = record;
|
||||
this.privateChatEnabled = privateChatEnabled;
|
||||
this.captureNotes = captureNotes;
|
||||
this.captureSlides = captureSlides;
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,9 @@ public class Meeting {
|
||||
private String parentMeetingId = "bbb-none"; // Initialize so we don't send null in the json message.
|
||||
private Integer sequence = 0;
|
||||
private Boolean freeJoin = false;
|
||||
private Integer duration = 0;
|
||||
private Boolean captureSlides = false;
|
||||
private Boolean captureNotes = false;
|
||||
private Integer duration = 0;
|
||||
private long createdTime = 0;
|
||||
private long startTime = 0;
|
||||
private long endTime = 0;
|
||||
@ -109,7 +111,7 @@ public class Meeting {
|
||||
public final BreakoutRoomsParams breakoutRoomsParams;
|
||||
public final LockSettingsParams lockSettingsParams;
|
||||
|
||||
public final Boolean allowDuplicateExtUserid;
|
||||
public final Integer maxUserConcurrentAccesses;
|
||||
|
||||
private String meetingEndedCallbackURL = "";
|
||||
|
||||
@ -163,7 +165,7 @@ public class Meeting {
|
||||
allowRequestsWithoutSession = builder.allowRequestsWithoutSession;
|
||||
breakoutRoomsParams = builder.breakoutRoomsParams;
|
||||
lockSettingsParams = builder.lockSettingsParams;
|
||||
allowDuplicateExtUserid = builder.allowDuplicateExtUserid;
|
||||
maxUserConcurrentAccesses = builder.maxUserConcurrentAccesses;
|
||||
endWhenNoModerator = builder.endWhenNoModerator;
|
||||
endWhenNoModeratorDelayInMinutes = builder.endWhenNoModeratorDelayInMinutes;
|
||||
html5InstanceId = builder.html5InstanceId;
|
||||
@ -197,6 +199,28 @@ public class Meeting {
|
||||
return users;
|
||||
}
|
||||
|
||||
public Integer countUniqueExtIds() {
|
||||
List<String> uniqueExtIds = new ArrayList<String>();
|
||||
for (User user : users.values()) {
|
||||
if(!uniqueExtIds.contains(user.getExternalUserId())) {
|
||||
uniqueExtIds.add(user.getExternalUserId());
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueExtIds.size();
|
||||
}
|
||||
|
||||
public List<String> getUsersWithExtId(String externalUserId) {
|
||||
List<String> usersWithExtId = new ArrayList<String>();
|
||||
for (User user : users.values()) {
|
||||
if(user.getExternalUserId().equals(externalUserId)) {
|
||||
usersWithExtId.add(user.getInternalUserId());
|
||||
}
|
||||
}
|
||||
|
||||
return usersWithExtId;
|
||||
}
|
||||
|
||||
public void guestIsWaiting(String userId) {
|
||||
RegisteredUser ruser = registeredUsers.get(userId);
|
||||
if (ruser != null) {
|
||||
@ -288,6 +312,22 @@ public class Meeting {
|
||||
this.freeJoin = freeJoin;
|
||||
}
|
||||
|
||||
public Boolean isCaptureSlides() {
|
||||
return captureSlides;
|
||||
}
|
||||
|
||||
public void setCaptureSlides(Boolean capture) {
|
||||
this.captureSlides = captureSlides;
|
||||
}
|
||||
|
||||
public Boolean isCaptureNotes() {
|
||||
return captureNotes;
|
||||
}
|
||||
|
||||
public void setCaptureNotes(Boolean capture) {
|
||||
this.captureNotes = captureNotes;
|
||||
}
|
||||
|
||||
public Integer getDuration() {
|
||||
return duration;
|
||||
}
|
||||
@ -504,6 +544,10 @@ public class Meeting {
|
||||
return maxUsers;
|
||||
}
|
||||
|
||||
public Integer getMaxUserConcurrentAccesses() {
|
||||
return maxUserConcurrentAccesses;
|
||||
}
|
||||
|
||||
public int getLogoutTimer() {
|
||||
return logoutTimer;
|
||||
}
|
||||
@ -633,17 +677,6 @@ public class Meeting {
|
||||
return this.users.get(id);
|
||||
}
|
||||
|
||||
public User getUserWithExternalId(String externalUserId) {
|
||||
for (Map.Entry<String, User> entry : users.entrySet()) {
|
||||
User u = entry.getValue();
|
||||
if (u.getExternalUserId().equals(externalUserId)) {
|
||||
return u;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public int getNumUsers(){
|
||||
return this.users.size();
|
||||
}
|
||||
@ -843,7 +876,8 @@ public class Meeting {
|
||||
private String meetingLayout;
|
||||
private BreakoutRoomsParams breakoutRoomsParams;
|
||||
private LockSettingsParams lockSettingsParams;
|
||||
private Boolean allowDuplicateExtUserid;
|
||||
|
||||
private Integer maxUserConcurrentAccesses;
|
||||
private Boolean endWhenNoModerator;
|
||||
private Integer endWhenNoModeratorDelayInMinutes;
|
||||
private int html5InstanceId;
|
||||
@ -1035,8 +1069,8 @@ public class Meeting {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withAllowDuplicateExtUserid(Boolean allowDuplicateExtUserid) {
|
||||
this.allowDuplicateExtUserid = allowDuplicateExtUserid;
|
||||
public Builder withMaxUserConcurrentAccesses(Integer maxUserConcurrentAccesses) {
|
||||
this.maxUserConcurrentAccesses = maxUserConcurrentAccesses;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,8 @@ public class CreateBreakoutRoom implements IMessage {
|
||||
public final Integer sourcePresentationSlide;
|
||||
public final Boolean record;
|
||||
public final Boolean privateChatEnabled;
|
||||
public final Boolean captureNotes; // Upload shared notes to main room after breakout room end
|
||||
public final Boolean captureSlides; // Upload annotated breakout slides to main room after breakout room end
|
||||
|
||||
public CreateBreakoutRoom(String meetingId,
|
||||
String parentMeetingId,
|
||||
@ -35,7 +37,9 @@ public class CreateBreakoutRoom implements IMessage {
|
||||
String sourcePresentationId,
|
||||
Integer sourcePresentationSlide,
|
||||
Boolean record,
|
||||
Boolean privateChatEnabled) {
|
||||
Boolean privateChatEnabled,
|
||||
Boolean captureNotes,
|
||||
Boolean captureSlides) {
|
||||
this.meetingId = meetingId;
|
||||
this.parentMeetingId = parentMeetingId;
|
||||
this.name = name;
|
||||
@ -52,5 +56,7 @@ public class CreateBreakoutRoom implements IMessage {
|
||||
this.sourcePresentationSlide = sourcePresentationSlide;
|
||||
this.record = record;
|
||||
this.privateChatEnabled = privateChatEnabled;
|
||||
this.captureNotes = captureNotes;
|
||||
this.captureSlides = captureSlides;
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +0,0 @@
|
||||
package org.bigbluebutton.api.model.constraint;
|
||||
|
||||
import org.bigbluebutton.api.model.validator.MaxParticipantsValidator;
|
||||
|
||||
import javax.validation.Constraint;
|
||||
import javax.validation.Payload;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.FIELD;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@Constraint(validatedBy = { MaxParticipantsValidator.class })
|
||||
@Target(FIELD)
|
||||
@Retention(RUNTIME)
|
||||
public @interface MaxParticipantsConstraint {
|
||||
|
||||
String key() default "maxParticipantsReached";
|
||||
String message() default "The maximum number of participants for the meeting has been reached";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
package org.bigbluebutton.api.model.request;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.MaxParticipantsConstraint;
|
||||
import org.bigbluebutton.api.model.constraint.MeetingEndedConstraint;
|
||||
import org.bigbluebutton.api.model.constraint.MeetingExistsConstraint;
|
||||
import org.bigbluebutton.api.model.constraint.UserSessionConstraint;
|
||||
|
@ -20,6 +20,7 @@ public class GetChecksumValidator implements ConstraintValidator<GetChecksumCons
|
||||
@Override
|
||||
public boolean isValid(GetChecksum checksum, ConstraintValidatorContext context) {
|
||||
String securitySalt = ServiceUtils.getValidationService().getSecuritySalt();
|
||||
String supportedChecksumAlgorithms = ServiceUtils.getValidationService().getSupportedChecksumAlgorithms();
|
||||
|
||||
if (securitySalt.isEmpty()) {
|
||||
log.warn("Security is disabled in this service. Make sure this is intentional.");
|
||||
@ -41,12 +42,37 @@ public class GetChecksumValidator implements ConstraintValidator<GetChecksumCons
|
||||
}
|
||||
|
||||
String data = checksum.getApiCall() + queryStringWithoutChecksum + securitySalt;
|
||||
String createdCheckSum = DigestUtils.sha1Hex(data);
|
||||
|
||||
if (providedChecksum.length() == 64) {
|
||||
log.debug("providedChecksum.length() == 64");
|
||||
createdCheckSum = DigestUtils.sha256Hex(data);
|
||||
log.info("SHA256 {}", createdCheckSum);
|
||||
int checksumLength = providedChecksum.length();
|
||||
String createdCheckSum = null;
|
||||
|
||||
switch(checksumLength) {
|
||||
case 40:
|
||||
if(supportedChecksumAlgorithms.contains("sha1")) {
|
||||
createdCheckSum = DigestUtils.sha1Hex(data);
|
||||
log.info("SHA1 {}", createdCheckSum);
|
||||
}
|
||||
break;
|
||||
case 64:
|
||||
if(supportedChecksumAlgorithms.contains("sha256")) {
|
||||
createdCheckSum = DigestUtils.sha256Hex(data);
|
||||
log.info("SHA256 {}", createdCheckSum);
|
||||
}
|
||||
break;
|
||||
case 96:
|
||||
if(supportedChecksumAlgorithms.contains("sha384")) {
|
||||
createdCheckSum = DigestUtils.sha384Hex(data);
|
||||
log.info("SHA384 {}", createdCheckSum);
|
||||
}
|
||||
break;
|
||||
case 128:
|
||||
if(supportedChecksumAlgorithms.contains("sha512")) {
|
||||
createdCheckSum = DigestUtils.sha512Hex(data);
|
||||
log.info("SHA512 {}", createdCheckSum);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
log.info("No algorithm could be found that matches the provided checksum length");
|
||||
}
|
||||
|
||||
if (createdCheckSum == null || !createdCheckSum.equals(providedChecksum)) {
|
||||
|
@ -1,50 +0,0 @@
|
||||
package org.bigbluebutton.api.model.validator;
|
||||
|
||||
import org.bigbluebutton.api.MeetingService;
|
||||
import org.bigbluebutton.api.domain.Meeting;
|
||||
import org.bigbluebutton.api.domain.UserSession;
|
||||
import org.bigbluebutton.api.model.constraint.MaxParticipantsConstraint;
|
||||
import org.bigbluebutton.api.service.ServiceUtils;
|
||||
|
||||
import javax.validation.ConstraintValidator;
|
||||
import javax.validation.ConstraintValidatorContext;
|
||||
|
||||
public class MaxParticipantsValidator implements ConstraintValidator<MaxParticipantsConstraint, String> {
|
||||
|
||||
@Override
|
||||
public void initialize(MaxParticipantsConstraint constraintAnnotation) {}
|
||||
|
||||
@Override
|
||||
public boolean isValid(String sessionToken, ConstraintValidatorContext constraintValidatorContext) {
|
||||
|
||||
if(sessionToken == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MeetingService meetingService = ServiceUtils.getMeetingService();
|
||||
UserSession userSession = meetingService.getUserSessionWithAuthToken(sessionToken);
|
||||
|
||||
if(userSession == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Meeting meeting = meetingService.getMeeting(userSession.meetingID);
|
||||
|
||||
if(meeting == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int maxParticipants = meeting.getMaxUsers();
|
||||
boolean enabled = maxParticipants > 0;
|
||||
boolean rejoin = meeting.getUserById(userSession.internalUserId) != null;
|
||||
boolean reenter = meeting.getEnteredUserById(userSession.internalUserId) != null;
|
||||
int joinedUsers = meeting.getUsers().size();
|
||||
|
||||
boolean reachedMax = joinedUsers >= maxParticipants;
|
||||
if(enabled && !rejoin && !reenter && reachedMax) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -55,6 +55,7 @@ public class ValidationService {
|
||||
}
|
||||
|
||||
private String securitySalt;
|
||||
private String supportedChecksumAlgorithms;
|
||||
private Boolean allowRequestsWithoutSession;
|
||||
|
||||
private ValidatorFactory validatorFactory;
|
||||
@ -266,6 +267,9 @@ public class ValidationService {
|
||||
public void setSecuritySalt(String securitySalt) { this.securitySalt = securitySalt; }
|
||||
public String getSecuritySalt() { return securitySalt; }
|
||||
|
||||
public void setSupportedChecksumAlgorithms(String supportedChecksumAlgorithms) { this.supportedChecksumAlgorithms = supportedChecksumAlgorithms; }
|
||||
public String getSupportedChecksumAlgorithms() { return supportedChecksumAlgorithms; }
|
||||
|
||||
public void setAllowRequestsWithoutSession(Boolean allowRequestsWithoutSession) {
|
||||
this.allowRequestsWithoutSession = allowRequestsWithoutSession;
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ public interface IBbbWebApiGWApp {
|
||||
String moderatorPass, String viewerPass, String learningDashboardAccessToken, Long createTime,
|
||||
String createDate, Boolean isBreakout, Integer sequence, Boolean freejoin, Map<String, String> metadata,
|
||||
String guestPolicy, Boolean authenticatedGuest, String meetingLayout, String welcomeMsgTemplate, String welcomeMsg, String modOnlyMessage,
|
||||
String dialNumber, Integer maxUsers,
|
||||
String dialNumber, Integer maxUsers, Integer maxUserConcurrentAccesses,
|
||||
Integer meetingExpireIfNoUserJoinedInMinutes,
|
||||
Integer meetingExpireWhenLastUserLeftInMinutes,
|
||||
Integer userInactivityInspectTimerInMinutes,
|
||||
@ -50,8 +50,6 @@ public interface IBbbWebApiGWApp {
|
||||
void registerUser(String meetingID, String internalUserId, String fullname, String role,
|
||||
String externUserID, String authToken, String avatarURL,
|
||||
Boolean guest, Boolean authed, String guestStatus, Boolean excludeFromDashboard);
|
||||
void ejectDuplicateUser(String meetingID, String internalUserId, String fullname,
|
||||
String externUserID);
|
||||
void guestWaitingLeft(String meetingID, String internalUserId);
|
||||
|
||||
void destroyMeeting(DestroyMeetingMessage msg);
|
||||
|
@ -21,6 +21,7 @@
|
||||
* @version $Id: $
|
||||
*/
|
||||
package org.bigbluebutton.presentation;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
|
@ -50,7 +50,6 @@ public class SwfSlidesGenerationProgressNotifier {
|
||||
maxUploadFileSize);
|
||||
messagingService.sendDocConversionMsg(progress);
|
||||
}
|
||||
|
||||
public void sendUploadFileTimedout(UploadedPresentation pres, int page) {
|
||||
UploadFileTimedoutMessage errorMessage = new UploadFileTimedoutMessage(
|
||||
pres.getPodId(),
|
||||
|
@ -131,7 +131,9 @@ class BbbWebApiGWApp(
|
||||
freeJoin: java.lang.Boolean,
|
||||
metadata: java.util.Map[String, String], guestPolicy: String, authenticatedGuest: java.lang.Boolean, meetingLayout: String,
|
||||
welcomeMsgTemplate: String, welcomeMsg: String, modOnlyMessage: String,
|
||||
dialNumber: String, maxUsers: java.lang.Integer,
|
||||
dialNumber: String,
|
||||
maxUsers: java.lang.Integer,
|
||||
maxUserConcurrentAccesses: java.lang.Integer,
|
||||
meetingExpireIfNoUserJoinedInMinutes: java.lang.Integer,
|
||||
meetingExpireWhenLastUserLeftInMinutes: java.lang.Integer,
|
||||
userInactivityInspectTimerInMinutes: java.lang.Integer,
|
||||
@ -189,17 +191,23 @@ class BbbWebApiGWApp(
|
||||
freeJoin = freeJoin.booleanValue(),
|
||||
breakoutRooms = Vector(),
|
||||
record = breakoutParams.record.booleanValue(),
|
||||
privateChatEnabled = breakoutParams.privateChatEnabled.booleanValue()
|
||||
privateChatEnabled = breakoutParams.privateChatEnabled.booleanValue(),
|
||||
captureNotes = breakoutParams.captureNotes.booleanValue(),
|
||||
captureSlides = breakoutParams.captureSlides.booleanValue(),
|
||||
)
|
||||
|
||||
val welcomeProp = WelcomeProp(welcomeMsgTemplate = welcomeMsgTemplate, welcomeMsg = welcomeMsg,
|
||||
modOnlyMessage = modOnlyMessage)
|
||||
val voiceProp = VoiceProp(telVoice = voiceBridge, voiceConf = voiceBridge, dialNumber = dialNumber, muteOnStart = muteOnStart.booleanValue())
|
||||
val usersProp = UsersProp(maxUsers = maxUsers.intValue(), webcamsOnlyForModerator = webcamsOnlyForModerator.booleanValue(),
|
||||
val usersProp = UsersProp(
|
||||
maxUsers = maxUsers.intValue(),
|
||||
maxUserConcurrentAccesses = maxUserConcurrentAccesses,
|
||||
webcamsOnlyForModerator = webcamsOnlyForModerator.booleanValue(),
|
||||
userCameraCap = userCameraCap.intValue(),
|
||||
guestPolicy = guestPolicy, meetingLayout = meetingLayout, allowModsToUnmuteUsers = allowModsToUnmuteUsers.booleanValue(),
|
||||
allowModsToEjectCameras = allowModsToEjectCameras.booleanValue(),
|
||||
authenticatedGuest = authenticatedGuest.booleanValue())
|
||||
authenticatedGuest = authenticatedGuest.booleanValue()
|
||||
)
|
||||
val metadataProp = MetadataProp(mapAsScalaMap(metadata).toMap)
|
||||
|
||||
val lockSettingsProps = LockSettingsProps(
|
||||
@ -261,11 +269,6 @@ class BbbWebApiGWApp(
|
||||
msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event))
|
||||
}
|
||||
|
||||
def ejectDuplicateUser(meetingId: String, intUserId: String, name: String, extUserId: String): Unit = {
|
||||
val event = MsgBuilder.buildEjectDuplicateUserRequestToAkkaApps(meetingId, intUserId, name, extUserId)
|
||||
msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event))
|
||||
}
|
||||
|
||||
def guestWaitingLeft(meetingId: String, intUserId: String): Unit = {
|
||||
val event = MsgBuilder.buildGuestWaitingLeftMsg(meetingId, intUserId)
|
||||
msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event))
|
||||
|
@ -34,16 +34,6 @@ object MsgBuilder {
|
||||
BbbCommonEnvCoreMsg(envelope, req)
|
||||
}
|
||||
|
||||
def buildEjectDuplicateUserRequestToAkkaApps(meetingId: String, intUserId: String, name: String, extUserId: String): BbbCommonEnvCoreMsg = {
|
||||
val routing = collection.immutable.HashMap("sender" -> "bbb-web")
|
||||
val envelope = BbbCoreEnvelope(EjectDuplicateUserReqMsg.NAME, routing)
|
||||
val header = BbbCoreHeaderWithMeetingId(EjectDuplicateUserReqMsg.NAME, meetingId)
|
||||
val body = EjectDuplicateUserReqMsgBody(meetingId = meetingId, intUserId = intUserId,
|
||||
name = name, extUserId = extUserId)
|
||||
val req = EjectDuplicateUserReqMsg(header, body)
|
||||
BbbCommonEnvCoreMsg(envelope, req)
|
||||
}
|
||||
|
||||
def buildRegisterUserRequestToAkkaApps(msg: RegisterUser): BbbCommonEnvCoreMsg = {
|
||||
val routing = collection.immutable.HashMap("sender" -> "bbb-web")
|
||||
val envelope = BbbCoreEnvelope(RegisterUserReqMsg.NAME, routing)
|
||||
|
@ -12,7 +12,7 @@ case class CreateBreakoutRoomMsg(meetingId: String, parentMeetingId: String,
|
||||
name: String, sequence: Integer, freeJoin: Boolean, dialNumber: String,
|
||||
voiceConfId: String, viewerPassword: String, moderatorPassword: String, duration: Int,
|
||||
sourcePresentationId: String, sourcePresentationSlide: Int,
|
||||
record: Boolean) extends ApiMsg
|
||||
record: Boolean, captureNotes: Boolean, captureSlides: Boolean) extends ApiMsg
|
||||
|
||||
case class AddUserSession(token: String, session: UserSession)
|
||||
case class RegisterUser(meetingId: String, intUserId: String, name: String, role: String,
|
||||
|
@ -102,9 +102,11 @@ class OldMeetingMsgHdlrActor(val olgMsgGW: OldMessageReceivedGW)
|
||||
msg.body.room.sourcePresentationId,
|
||||
msg.body.room.sourcePresentationSlide,
|
||||
msg.body.room.record,
|
||||
msg.body.room.privateChatEnabled
|
||||
msg.body.room.privateChatEnabled,
|
||||
msg.body.room.captureNotes,
|
||||
msg.body.room.captureSlides,
|
||||
))
|
||||
|
||||
|
||||
}
|
||||
|
||||
def handleRecordingStatusChangedEvtMsg(msg: RecordingStatusChangedEvtMsg): Unit = {
|
||||
|
@ -10,6 +10,9 @@
|
||||
"imagemagick": "/usr/bin/convert",
|
||||
"pdftocairo": "/usr/bin/pdftocairo"
|
||||
},
|
||||
"captureNotes": {
|
||||
"timeout": 5000
|
||||
},
|
||||
"collector": {
|
||||
"pngWidthRasterizedSlides": 2560
|
||||
},
|
||||
@ -25,6 +28,7 @@
|
||||
"msgName": "NewPresAnnFileAvailableMsg"
|
||||
},
|
||||
"bbbWebAPI": "http://127.0.0.1:8090",
|
||||
"bbbPadsAPI": "http://127.0.0.1:9002",
|
||||
"redis": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 6379,
|
||||
|
@ -1,24 +1,32 @@
|
||||
const Logger = require('../lib/utils/logger');
|
||||
const axios = require('axios').default;
|
||||
const config = require('../config');
|
||||
const fs = require('fs');
|
||||
const redis = require('redis');
|
||||
const {Worker, workerData} = require('worker_threads');
|
||||
const path = require('path');
|
||||
const cp = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const redis = require('redis');
|
||||
const sanitize = require('sanitize-filename');
|
||||
const stream = require('stream');
|
||||
const {Worker, workerData} = require('worker_threads');
|
||||
const {promisify} = require('util');
|
||||
|
||||
const WorkerTypes = Object.freeze({
|
||||
Notifier: 'notifier',
|
||||
Process: 'process',
|
||||
});
|
||||
|
||||
const jobId = workerData;
|
||||
|
||||
const logger = new Logger('presAnn Collector');
|
||||
logger.info('Collecting job ' + jobId);
|
||||
logger.info(`Collecting job ${jobId}`);
|
||||
|
||||
const kickOffProcessWorker = (jobId) => {
|
||||
const kickOffWorker = (workerType, data) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = new Worker('./workers/process.js', {workerData: jobId});
|
||||
const worker = new Worker(`./workers/${workerType}.js`, {workerData: data});
|
||||
worker.on('message', resolve);
|
||||
worker.on('error', reject);
|
||||
worker.on('exit', (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`Process Worker stopped with exit code ${code}`));
|
||||
reject(new Error(`Worker '${workerType}' stopped with exit code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -30,8 +38,7 @@ const dropbox = path.join(config.shared.presAnnDropboxDir, jobId);
|
||||
const job = fs.readFileSync(path.join(dropbox, 'job'));
|
||||
const exportJob = JSON.parse(job);
|
||||
|
||||
// Collect the annotations from Redis
|
||||
(async () => {
|
||||
async function collectAnnotationsFromRedis() {
|
||||
const client = redis.createClient({
|
||||
host: config.redis.host,
|
||||
port: config.redis.port,
|
||||
@ -42,7 +49,7 @@ const exportJob = JSON.parse(job);
|
||||
|
||||
await client.connect();
|
||||
|
||||
const presAnn = await client.hGetAll(exportJob.jobId);
|
||||
const presAnn = await client.hGetAll(jobId);
|
||||
|
||||
// Remove annotations from Redis
|
||||
await client.del(jobId);
|
||||
@ -95,8 +102,66 @@ const exportJob = JSON.parse(job);
|
||||
} else if (fs.existsSync(`${presFile}.jpeg`)) {
|
||||
fs.copyFileSync(`${presFile}.jpeg`, path.join(dropbox, 'slide1.jpeg'));
|
||||
} else {
|
||||
return logger.error(`Could not find presentation file ${exportJob.jobId}`);
|
||||
return logger.error(`Could not find presentation file ${jobId}`);
|
||||
}
|
||||
|
||||
kickOffProcessWorker(exportJob.jobId);
|
||||
})();
|
||||
kickOffWorker(WorkerTypes.Process, jobId);
|
||||
}
|
||||
|
||||
async function sleep(ms) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
/** Export shared notes via bbb-pads in the desired format
|
||||
* @param {Integer} retries - Number of retries to get the shared notes
|
||||
*/
|
||||
async function collectSharedNotes(retries) {
|
||||
/** One of the following formats is supported:
|
||||
etherpad / html / pdf / txt / doc / odf */
|
||||
|
||||
const padId = exportJob.presId;
|
||||
const notesFormat = 'pdf';
|
||||
|
||||
const filename = `${sanitize(exportJob.filename.replace(/\s/g, '_'))}.${notesFormat}`;
|
||||
const notes_endpoint = `${config.bbbPadsAPI}/p/${padId}/export/${notesFormat}`;
|
||||
const filePath = path.join(dropbox, filename);
|
||||
|
||||
const [sequence] = JSON.parse(exportJob.pages);
|
||||
const timeout = (sequence - 1) * config.captureNotes.timeout;
|
||||
|
||||
// Wait for the bbb-pads API to be available
|
||||
await sleep(timeout);
|
||||
|
||||
const finishedDownload = promisify(stream.finished);
|
||||
const writer = fs.createWriteStream(filePath);
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
method: 'GET',
|
||||
url: notes_endpoint,
|
||||
responseType: 'stream',
|
||||
timeout: timeout,
|
||||
});
|
||||
response.data.pipe(writer);
|
||||
await finishedDownload(writer);
|
||||
} catch (err) {
|
||||
if (retries > 0) {
|
||||
logger.info(`Retrying ${jobId} in ${timeout}ms...`);
|
||||
return collectSharedNotes(retries - 1);
|
||||
} else {
|
||||
logger.error(`Could not download notes in job ${jobId}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
kickOffWorker(WorkerTypes.Notifier, [exportJob.jobType, jobId, filename]);
|
||||
}
|
||||
|
||||
switch (exportJob.jobType) {
|
||||
case 'PresentationWithAnnotationExportJob': return collectAnnotationsFromRedis();
|
||||
case 'PresentationWithAnnotationDownloadJob': return collectAnnotationsFromRedis();
|
||||
case 'PadCaptureJob': return collectSharedNotes(3);
|
||||
default: return logger.error(`Unknown job type ${exportJob.jobType}`);
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ const path = require('path');
|
||||
|
||||
const {workerData} = require('worker_threads');
|
||||
|
||||
const [jobType, jobId, filename_with_extension] = workerData;
|
||||
const [jobType, jobId, filename] = workerData;
|
||||
|
||||
const logger = new Logger('presAnn Notifier Worker');
|
||||
|
||||
@ -30,7 +30,7 @@ async function notifyMeetingActor() {
|
||||
|
||||
const link = path.join(`${path.sep}bigbluebutton`, 'presentation',
|
||||
exportJob.parentMeetingId, exportJob.parentMeetingId,
|
||||
exportJob.presId, 'pdf', jobId, filename_with_extension);
|
||||
exportJob.presId, 'pdf', jobId, filename);
|
||||
|
||||
const notification = {
|
||||
envelope: {
|
||||
@ -59,27 +59,35 @@ async function notifyMeetingActor() {
|
||||
client.disconnect();
|
||||
}
|
||||
|
||||
/** Upload PDF to a BBB room */
|
||||
async function upload() {
|
||||
/** Upload PDF to a BBB room
|
||||
* @param {String} filePath - Absolute path to the file, including the extension
|
||||
*/
|
||||
async function upload(filePath) {
|
||||
const callbackUrl = `${config.bbbWebAPI}/bigbluebutton/presentation/${exportJob.presentationUploadToken}/upload`;
|
||||
const formData = new FormData();
|
||||
const file = `${exportJob.presLocation}/pdfs/${jobId}/${filename_with_extension}`;
|
||||
|
||||
formData.append('conference', exportJob.parentMeetingId);
|
||||
formData.append('pod_id', config.notifier.pod_id);
|
||||
formData.append('is_downloadable', config.notifier.is_downloadable);
|
||||
formData.append('temporaryPresentationId', jobId);
|
||||
formData.append('fileUpload', fs.createReadStream(file));
|
||||
formData.append('fileUpload', fs.createReadStream(filePath));
|
||||
|
||||
const res = await axios.post(callbackUrl, formData,
|
||||
{headers: formData.getHeaders()});
|
||||
logger.info(`Upload of job ${exportJob.jobId} returned ${res.data}`);
|
||||
try {
|
||||
const res = await axios.post(callbackUrl, formData,
|
||||
{headers: formData.getHeaders()});
|
||||
logger.info(`Upload of job ${exportJob.jobId} returned ${res.data}`);
|
||||
} catch (error) {
|
||||
return logger.error(`Could upload job ${exportJob.jobId}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (jobType == 'PresentationWithAnnotationDownloadJob') {
|
||||
notifyMeetingActor();
|
||||
} else if (jobType == 'PresentationWithAnnotationExportJob') {
|
||||
upload();
|
||||
const filePath = `${exportJob.presLocation}/pdfs/${jobId}/${filename}`;
|
||||
upload(filePath);
|
||||
} else if (jobType == 'PadCaptureJob') {
|
||||
const filePath = `${dropbox}/${filename}`;
|
||||
upload(filePath);
|
||||
} else {
|
||||
logger.error(`Notifier received unknown job type ${jobType}`);
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ const cp = require('child_process');
|
||||
const {Worker, workerData} = require('worker_threads');
|
||||
const path = require('path');
|
||||
const sanitize = require('sanitize-filename');
|
||||
const {getStroke, getStrokePoints} = require('perfect-freehand');
|
||||
const {getStrokePoints, getStrokeOutlinePoints} = require('perfect-freehand');
|
||||
const probe = require('probe-image-size');
|
||||
|
||||
const jobId = workerData;
|
||||
@ -193,17 +193,17 @@ function get_gap(dash, size) {
|
||||
function get_stroke_width(dash, size) {
|
||||
switch (size) {
|
||||
case 'small': if (dash === 'draw') {
|
||||
return 1;
|
||||
return 2;
|
||||
} else {
|
||||
return 4;
|
||||
}
|
||||
case 'medium': if (dash === 'draw') {
|
||||
return 1.75;
|
||||
return 3.5;
|
||||
} else {
|
||||
return 6.25;
|
||||
}
|
||||
case 'large': if (dash === 'draw') {
|
||||
return 2.5;
|
||||
return 5;
|
||||
} else {
|
||||
return 8.5;
|
||||
}
|
||||
@ -236,61 +236,26 @@ function text_size_to_px(size, scale = 1, isStickyNote = false) {
|
||||
}
|
||||
}
|
||||
|
||||
// Methods based on tldraw's utilities
|
||||
function getPath(annotationPoints) {
|
||||
// Gets inner path of a stroke outline
|
||||
// For solid, dashed, and dotted types
|
||||
const stroke = getStrokePoints(annotationPoints)
|
||||
.map((strokePoint) => strokePoint.point);
|
||||
|
||||
let [max_x, max_y] = [0, 0];
|
||||
const inner_path = stroke.reduce(
|
||||
/**
|
||||
* Turns an array of points into a path of quadradic curves.
|
||||
* @param {Array} annotationPoints
|
||||
* @param {Boolean} closed - whether the path end and start should be connected (default)
|
||||
* @return {Array} - an SVG quadratic curve path
|
||||
*/
|
||||
function getSvgPath(annotationPoints, closed = true) {
|
||||
const svgPath = annotationPoints.reduce(
|
||||
(acc, [x0, y0], i, arr) => {
|
||||
if (!arr[i + 1]) return acc;
|
||||
const [x1, y1] = arr[i + 1];
|
||||
if (x1 >= max_x) {
|
||||
max_x = x1;
|
||||
}
|
||||
if (y1 >= max_y) {
|
||||
max_y = y1;
|
||||
}
|
||||
acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2);
|
||||
acc.push(x0.toFixed(2), y0.toFixed(2), ((x0 + x1) / 2).toFixed(2), ((y0 + y1) / 2).toFixed(2));
|
||||
return acc;
|
||||
},
|
||||
|
||||
['M', ...stroke[0], 'Q'],
|
||||
['M', ...annotationPoints[0], 'Q'],
|
||||
);
|
||||
|
||||
return [inner_path, max_x, max_y];
|
||||
}
|
||||
|
||||
function getOutlinePath(annotationPoints) {
|
||||
// Gets outline of a hand-drawn input, with pressure
|
||||
const stroke = getStroke(annotationPoints, {
|
||||
simulatePressure: true,
|
||||
size: 8,
|
||||
});
|
||||
|
||||
let [max_x, max_y] = [0, 0];
|
||||
const outline_path = stroke.reduce(
|
||||
(acc, [x0, y0], i, arr) => {
|
||||
const [x1, y1] = arr[(i + 1) % arr.length];
|
||||
if (x1 >= max_x) {
|
||||
max_x = x1;
|
||||
}
|
||||
if (y1 >= max_y) {
|
||||
max_y = y1;
|
||||
}
|
||||
acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2);
|
||||
return acc;
|
||||
},
|
||||
|
||||
['M', ...stroke[0], 'Q'],
|
||||
);
|
||||
|
||||
outline_path.push('Z');
|
||||
|
||||
return [outline_path, max_x, max_y];
|
||||
if (closed) svgPath.push('Z');
|
||||
return svgPath;
|
||||
}
|
||||
|
||||
function circleFromThreePoints(A, B, C) {
|
||||
@ -471,49 +436,94 @@ function overlay_arrow(svg, annotation) {
|
||||
}
|
||||
|
||||
function overlay_draw(svg, annotation) {
|
||||
const shapePoints = annotation.points;
|
||||
const shapePointsLength = shapePoints.length;
|
||||
|
||||
if (shapePointsLength < 2) return;
|
||||
|
||||
const dash = annotation.style.dash;
|
||||
const [calculated_path, max_x, max_y] = (dash == 'draw') ? getOutlinePath(annotation.points) : getPath(annotation.points);
|
||||
const isDashDraw = (dash == 'draw');
|
||||
|
||||
if (!calculated_path.length) return;
|
||||
|
||||
const shapeColor = color_to_hex(annotation.style.color);
|
||||
const rotation = rad_to_degree(annotation.rotation);
|
||||
const thickness = get_stroke_width(dash, annotation.style.size);
|
||||
const gap = get_gap(dash, annotation.style.size);
|
||||
|
||||
const [x, y] = annotation.point;
|
||||
|
||||
const stroke_dasharray = determine_dasharray(dash, gap);
|
||||
const fill = (dash === 'draw') ? shapeColor : 'none';
|
||||
|
||||
const shapeColor = color_to_hex(annotation.style.color);
|
||||
const shapeFillColor = color_to_hex(`fill-${annotation.style.color}`);
|
||||
const shapeTransform = `translate(${x} ${y}), rotate(${rotation} ${max_x / 2} ${max_y / 2})`;
|
||||
const fill = isDashDraw ? shapeColor : 'none';
|
||||
|
||||
// Fill assuming solid, small pencil used
|
||||
// when path start- and end points overlap
|
||||
const shapeIsFilled =
|
||||
const rotation = rad_to_degree(annotation.rotation);
|
||||
const [x, y] = annotation.point;
|
||||
const [width, height] = annotation.size;
|
||||
const shapeTransform = `translate(${x} ${y}), rotate(${rotation} ${width / 2} ${height / 2})`;
|
||||
|
||||
const simulatePressure = {
|
||||
easing: (t) => Math.sin((t * Math.PI) / 2),
|
||||
simulatePressure: true,
|
||||
};
|
||||
|
||||
const realPressure = {
|
||||
easing: (t) => t * t,
|
||||
simulatePressure: false,
|
||||
};
|
||||
|
||||
const options = {
|
||||
size: 1 + thickness * 1.5,
|
||||
thinning: 0.65,
|
||||
streamline: 0.65,
|
||||
smoothing: 0.65,
|
||||
...(shapePoints[1][2] === 0.5 ? simulatePressure : realPressure),
|
||||
last: annotation.isComplete,
|
||||
};
|
||||
|
||||
const strokePoints = getStrokePoints(shapePoints, options);
|
||||
|
||||
// Fill when path start- and end points overlap
|
||||
const isShapeFilled =
|
||||
annotation.style.isFilled &&
|
||||
annotation.points.length > 3 &&
|
||||
shapePointsLength > 3 &&
|
||||
Math.round(distance(
|
||||
annotation.points[0][0],
|
||||
annotation.points[0][1],
|
||||
annotation.points[annotation.points.length - 1][0],
|
||||
annotation.points[annotation.points.length - 1][1],
|
||||
)) <= 2 * get_stroke_width('solid', 'small');
|
||||
shapePoints[0][0],
|
||||
shapePoints[0][1],
|
||||
shapePoints[shapePointsLength - 1][0],
|
||||
shapePoints[shapePointsLength - 1][1],
|
||||
)) <= 2 * thickness;
|
||||
|
||||
if (shapeIsFilled) {
|
||||
if (isShapeFilled) {
|
||||
const shapeArea = strokePoints.map((strokePoint) => strokePoint.point);
|
||||
svg.ele('path', {
|
||||
style: `fill:${shapeFillColor};`,
|
||||
d: getPath(annotation.points)[0] + 'Z',
|
||||
d: getSvgPath(shapeArea),
|
||||
transform: shapeTransform,
|
||||
}).up();
|
||||
}
|
||||
|
||||
svg.ele('path', {
|
||||
style: `stroke:${shapeColor};stroke-width:${thickness};fill:${fill};${stroke_dasharray}`,
|
||||
d: calculated_path,
|
||||
transform: shapeTransform,
|
||||
});
|
||||
if (isDashDraw) {
|
||||
const strokeOutlinePoints = getStrokeOutlinePoints(strokePoints, options);
|
||||
const svgPath = getSvgPath(strokeOutlinePoints);
|
||||
|
||||
svg.ele('path', {
|
||||
style: `fill:${fill};${stroke_dasharray}`,
|
||||
d: svgPath,
|
||||
transform: shapeTransform,
|
||||
});
|
||||
} else {
|
||||
const last = shapePoints[shapePointsLength - 1];
|
||||
|
||||
// Avoid single dots from not being drawn
|
||||
if (strokePoints[0].point[0] == last[0] && strokePoints[0].point[1] == last[1]) {
|
||||
strokePoints.push({point: last});
|
||||
}
|
||||
|
||||
const solidPath = strokePoints.map((strokePoint) => strokePoint.point);
|
||||
const svgPath = getSvgPath(solidPath, false);
|
||||
|
||||
svg.ele('path', {
|
||||
style: `stroke:${shapeColor};stroke-width:${thickness};fill:${fill};${stroke_dasharray}`,
|
||||
d: svgPath,
|
||||
transform: shapeTransform,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function overlay_ellipse(svg, annotation) {
|
||||
@ -603,19 +613,23 @@ function overlay_shape_label(svg, annotation) {
|
||||
|
||||
render_textbox(fontColor, font, fontSize, textAlign, text, id);
|
||||
|
||||
const dimensions = probe.sync(fs.readFileSync(path.join(dropbox, `text${id}.png`)));
|
||||
const labelWidth = dimensions.width / config.process.textScaleFactor;
|
||||
const labelHeight = dimensions.height / config.process.textScaleFactor;
|
||||
const shape_label = path.join(dropbox, `text${id}.png`);
|
||||
|
||||
svg.ele('g', {
|
||||
transform: `rotate(${rotation} ${label_center_x} ${label_center_y})`,
|
||||
}).ele('image', {
|
||||
'x': label_center_x - (labelWidth * x_offset),
|
||||
'y': label_center_y - (labelHeight * y_offset),
|
||||
'width': labelWidth,
|
||||
'height': labelHeight,
|
||||
'xlink:href': `file://${dropbox}/text${id}.png`,
|
||||
}).up();
|
||||
if (fs.existsSync(shape_label)) {
|
||||
const dimensions = probe.sync(fs.readFileSync(shape_label));
|
||||
const labelWidth = dimensions.width / config.process.textScaleFactor;
|
||||
const labelHeight = dimensions.height / config.process.textScaleFactor;
|
||||
|
||||
svg.ele('g', {
|
||||
transform: `rotate(${rotation} ${label_center_x} ${label_center_y})`,
|
||||
}).ele('image', {
|
||||
'x': label_center_x - (labelWidth * x_offset),
|
||||
'y': label_center_y - (labelHeight * y_offset),
|
||||
'width': labelWidth,
|
||||
'height': labelHeight,
|
||||
'xlink:href': `file://${dropbox}/text${id}.png`,
|
||||
}).up();
|
||||
}
|
||||
}
|
||||
|
||||
function overlay_sticky(svg, annotation) {
|
||||
@ -712,32 +726,30 @@ function overlay_text(svg, annotation) {
|
||||
}
|
||||
|
||||
function overlay_annotation(svg, currentAnnotation) {
|
||||
if (currentAnnotation.childIndex >= 1) {
|
||||
switch (currentAnnotation.type) {
|
||||
case 'arrow':
|
||||
overlay_arrow(svg, currentAnnotation);
|
||||
break;
|
||||
case 'draw':
|
||||
overlay_draw(svg, currentAnnotation);
|
||||
break;
|
||||
case 'ellipse':
|
||||
overlay_ellipse(svg, currentAnnotation);
|
||||
break;
|
||||
case 'rectangle':
|
||||
overlay_rectangle(svg, currentAnnotation);
|
||||
break;
|
||||
case 'sticky':
|
||||
overlay_sticky(svg, currentAnnotation);
|
||||
break;
|
||||
case 'triangle':
|
||||
overlay_triangle(svg, currentAnnotation);
|
||||
break;
|
||||
case 'text':
|
||||
overlay_text(svg, currentAnnotation);
|
||||
break;
|
||||
default:
|
||||
logger.info(`Unknown annotation type ${currentAnnotation.type}.`);
|
||||
}
|
||||
switch (currentAnnotation.type) {
|
||||
case 'arrow':
|
||||
overlay_arrow(svg, currentAnnotation);
|
||||
break;
|
||||
case 'draw':
|
||||
overlay_draw(svg, currentAnnotation);
|
||||
break;
|
||||
case 'ellipse':
|
||||
overlay_ellipse(svg, currentAnnotation);
|
||||
break;
|
||||
case 'rectangle':
|
||||
overlay_rectangle(svg, currentAnnotation);
|
||||
break;
|
||||
case 'sticky':
|
||||
overlay_sticky(svg, currentAnnotation);
|
||||
break;
|
||||
case 'triangle':
|
||||
overlay_triangle(svg, currentAnnotation);
|
||||
break;
|
||||
case 'text':
|
||||
overlay_text(svg, currentAnnotation);
|
||||
break;
|
||||
default:
|
||||
logger.info(`Unknown annotation type ${currentAnnotation.type}.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,6 +51,44 @@ class PollsTable extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
// Here we count each poll vote in order to find out the most common answer.
|
||||
const pollVotesCount = Object.keys(polls || {}).reduce((prevPollVotesCount, pollId) => {
|
||||
const currPollVotesCount = { ...prevPollVotesCount };
|
||||
currPollVotesCount[pollId] = {};
|
||||
|
||||
if (polls[pollId].anonymous) {
|
||||
polls[pollId].anonymousAnswers.forEach((answer) => {
|
||||
const answerLowerCase = answer.toLowerCase();
|
||||
if (currPollVotesCount[pollId][answerLowerCase] === undefined) {
|
||||
currPollVotesCount[pollId][answerLowerCase] = 1;
|
||||
} else {
|
||||
currPollVotesCount[pollId][answerLowerCase] += 1;
|
||||
}
|
||||
});
|
||||
|
||||
return currPollVotesCount;
|
||||
}
|
||||
|
||||
Object.values(allUsers).forEach((currUser) => {
|
||||
if (currUser.answers[pollId] !== undefined) {
|
||||
const userAnswers = Array.isArray(currUser.answers[pollId])
|
||||
? currUser.answers[pollId]
|
||||
: [currUser.answers[pollId]];
|
||||
|
||||
userAnswers.forEach((answer) => {
|
||||
const answerLowerCase = answer.toLowerCase();
|
||||
if (currPollVotesCount[pollId][answerLowerCase] === undefined) {
|
||||
currPollVotesCount[pollId][answerLowerCase] = 1;
|
||||
} else {
|
||||
currPollVotesCount[pollId][answerLowerCase] += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return currPollVotesCount;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
@ -104,7 +142,16 @@ class PollsTable extends React.Component {
|
||||
.sort((a, b) => ((a.createdOn > b.createdOn) ? 1 : -1))
|
||||
.map((poll) => (
|
||||
<td className="px-4 py-3 text-sm text-center">
|
||||
{ getUserAnswer(user, poll).map((answer) => <p>{answer}</p>) }
|
||||
{ getUserAnswer(user, poll).map((answer) => {
|
||||
const answersSorted = Object
|
||||
.entries(pollVotesCount[poll?.pollId])
|
||||
.sort(([, countA], [, countB]) => countB - countA);
|
||||
const isMostCommonAnswer = (
|
||||
answersSorted[0]?.[0]?.toLowerCase() === answer?.toLowerCase()
|
||||
&& answersSorted[0]?.[1] > 1
|
||||
);
|
||||
return <p className={isMostCommonAnswer ? 'font-bold' : ''}>{answer}</p>;
|
||||
}) }
|
||||
{ poll.anonymous
|
||||
? (
|
||||
<span title={intl.formatMessage({
|
||||
|
@ -1 +1 @@
|
||||
git clone --branch v1.3.0 --depth 1 https://github.com/bigbluebutton/bbb-pads bbb-pads
|
||||
git clone --branch v1.3.2 --depth 1 https://github.com/bigbluebutton/bbb-pads bbb-pads
|
||||
|
@ -1 +1 @@
|
||||
git clone --branch v5.0.0-alpha.2 --depth 1 https://github.com/bigbluebutton/bbb-playback bbb-playback
|
||||
git clone --branch v5.0.0-alpha.3 --depth 1 https://github.com/bigbluebutton/bbb-playback bbb-playback
|
||||
|
@ -1,9 +0,0 @@
|
||||
<include>
|
||||
<extension name="bbb_sp_call" continue="true">
|
||||
<condition field="network_addr" expression="${domain}" break="on-false">
|
||||
<action application="set" data="bbb_authorized=true"/>
|
||||
<action application="transfer" data="${destination_number} XML default"/>
|
||||
</condition>
|
||||
</extension>
|
||||
</include>
|
||||
|
@ -1 +1 @@
|
||||
git clone --branch v2.9.1 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu
|
||||
git clone --branch v2.9.2 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu
|
||||
|
@ -1249,19 +1249,34 @@ check_state() {
|
||||
if [ ! -z "$STUN" ]; then
|
||||
for i in $STUN; do
|
||||
STUN_SERVER="$(xmlstarlet sel -N x="http://www.springframework.org/schema/beans" -t -m "_:beans/_:bean[@id=\"$i\"]/_:constructor-arg[@index=\"0\"]" -v @value $TURN | sed 's/stun://g')"
|
||||
if echo $STUN_SERVER | grep -q ':'; then
|
||||
STUN_SERVER="$(echo $STUN_SERVER | sed 's/:.*//g') $(echo $STUN_SERVER | sed 's/.*://g')"
|
||||
else
|
||||
STUN_SERVER="$STUN_SERVER 3478"
|
||||
fi
|
||||
|
||||
if which stunclient > /dev/null 2>&1; then
|
||||
if stunclient --mode full --localport 30000 $STUN_SERVER | grep -q "fail\|Unable\ to\ resolve"; then
|
||||
# stun is from the stun-client package, which is available on both bionic and focal
|
||||
# stunclient is from the stuntman-client package, which is available on bionic but was removed from focal
|
||||
if which stun > /dev/null 2>&1; then
|
||||
# stun return codes, from its client.cxx
|
||||
# low nibble: open (0), various STUN combinations (2-9), firewall (a), blocked (c), unknown (e), error (f)
|
||||
# high nibble: hairpin (1)
|
||||
stun $STUN_SERVER > /dev/null
|
||||
if (( ($? & 0xf) > 9 )); then
|
||||
echo
|
||||
echo "#"
|
||||
echo "# Warning: Failed to verify STUN server at $STUN_SERVER with command"
|
||||
echo "#"
|
||||
echo "# stunclient --mode full --localport 30000 $STUN_SERVER"
|
||||
echo "# stun $STUN_SERVER"
|
||||
echo "#"
|
||||
fi
|
||||
elif which stunclient > /dev/null 2>&1; then
|
||||
if echo $STUN_SERVER | grep -q ':'; then
|
||||
STUN_SERVER="$(echo $STUN_SERVER | sed 's/:.*//g') $(echo $STUN_SERVER | sed 's/.*://g')"
|
||||
else
|
||||
STUN_SERVER="$STUN_SERVER 3478"
|
||||
fi
|
||||
if stunclient $STUN_SERVER | grep -q "fail\|Unable\ to\ resolve"; then
|
||||
echo
|
||||
echo "#"
|
||||
echo "# Warning: Failed to verify STUN server at $STUN_SERVER with command"
|
||||
echo "#"
|
||||
echo "# stunclient $STUN_SERVER"
|
||||
echo "#"
|
||||
fi
|
||||
fi
|
||||
|
@ -1,20 +1,27 @@
|
||||
import { check } from 'meteor/check';
|
||||
import _ from "lodash";
|
||||
|
||||
export default function addAnnotation(meetingId, whiteboardId, userId, annotation) {
|
||||
export default function addAnnotation(meetingId, whiteboardId, userId, annotation, Annotations) {
|
||||
check(meetingId, String);
|
||||
check(whiteboardId, String);
|
||||
check(annotation, Object);
|
||||
|
||||
const {
|
||||
id, annotationInfo, wbId,
|
||||
id, wbId,
|
||||
} = annotation;
|
||||
|
||||
let { annotationInfo } = annotation;
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
id,
|
||||
userId,
|
||||
};
|
||||
|
||||
const oldAnnotation = Annotations.findOne(selector);
|
||||
if (oldAnnotation) {
|
||||
annotationInfo = _.merge(oldAnnotation.annotationInfo, annotationInfo)
|
||||
}
|
||||
|
||||
const modifier = {
|
||||
$set: {
|
||||
whiteboardId,
|
||||
|
@ -8,7 +8,7 @@ export default function addAnnotation(meetingId, whiteboardId, userId, annotatio
|
||||
check(whiteboardId, String);
|
||||
check(annotation, Object);
|
||||
|
||||
const query = addAnnotationQuery(meetingId, whiteboardId, userId, annotation);
|
||||
const query = addAnnotationQuery(meetingId, whiteboardId, userId, annotation, Annotations);
|
||||
|
||||
try {
|
||||
const { insertedId } = Annotations.upsert(query.selector, query.modifier);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import Storage from '/imports/ui/services/storage/session';
|
||||
import BBBStorage from '/imports/ui/services/storage';
|
||||
|
||||
const AUDIO_SESSION_NUM_KEY = 'AudioSessionNumber';
|
||||
const DEFAULT_INPUT_DEVICE_ID = '';
|
||||
@ -38,10 +38,10 @@ const getCurrentAudioSinkId = () => {
|
||||
return audioElement?.sinkId || DEFAULT_OUTPUT_DEVICE_ID;
|
||||
};
|
||||
|
||||
const getStoredAudioInputDeviceId = () => Storage.getItem(INPUT_DEVICE_ID_KEY);
|
||||
const getStoredAudioOutputDeviceId = () => Storage.getItem(OUTPUT_DEVICE_ID_KEY);
|
||||
const storeAudioInputDeviceId = (deviceId) => Storage.setItem(INPUT_DEVICE_ID_KEY, deviceId);
|
||||
const storeAudioOutputDeviceId = (deviceId) => Storage.setItem(OUTPUT_DEVICE_ID_KEY, deviceId);
|
||||
const getStoredAudioInputDeviceId = () => BBBStorage.getItem(INPUT_DEVICE_ID_KEY);
|
||||
const getStoredAudioOutputDeviceId = () => BBBStorage.getItem(OUTPUT_DEVICE_ID_KEY);
|
||||
const storeAudioInputDeviceId = (deviceId) => BBBStorage.setItem(INPUT_DEVICE_ID_KEY, deviceId);
|
||||
const storeAudioOutputDeviceId = (deviceId) => BBBStorage.setItem(OUTPUT_DEVICE_ID_KEY, deviceId);
|
||||
|
||||
/**
|
||||
* Filter constraints set in audioDeviceConstraints, based on
|
||||
|
@ -4,7 +4,7 @@ import Logger from '/imports/startup/server/logger';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
import { check } from 'meteor/check';
|
||||
|
||||
export default function createBreakoutRoom(rooms, durationInMinutes, record = false) {
|
||||
export default function createBreakoutRoom(rooms, durationInMinutes, record = false, captureNotes = false, captureSlides = false) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const BREAKOUT_LIM = Meteor.settings.public.app.breakouts.breakoutRoomLimit;
|
||||
@ -24,6 +24,8 @@ export default function createBreakoutRoom(rooms, durationInMinutes, record = fa
|
||||
}
|
||||
const payload = {
|
||||
record,
|
||||
captureNotes,
|
||||
captureSlides,
|
||||
durationInMinutes,
|
||||
rooms,
|
||||
meetingId,
|
||||
|
@ -74,6 +74,8 @@ export default function addMeeting(meeting) {
|
||||
parentId: String,
|
||||
record: Boolean,
|
||||
privateChatEnabled: Boolean,
|
||||
captureNotes: Boolean,
|
||||
captureSlides: Boolean,
|
||||
},
|
||||
meetingProp: {
|
||||
intId: String,
|
||||
@ -88,11 +90,12 @@ export default function addMeeting(meeting) {
|
||||
uploadExternalUrl: String,
|
||||
},
|
||||
usersProp: {
|
||||
maxUsers: Number,
|
||||
maxUserConcurrentAccesses: Number,
|
||||
webcamsOnlyForModerator: Boolean,
|
||||
userCameraCap: Number,
|
||||
guestPolicy: String,
|
||||
authenticatedGuest: Boolean,
|
||||
maxUsers: Number,
|
||||
allowModsToUnmuteUsers: Boolean,
|
||||
allowModsToEjectCameras: Boolean,
|
||||
meetingLayout: String,
|
||||
|
@ -6,6 +6,7 @@ import padUpdated from './handlers/padUpdated';
|
||||
import padContent from './handlers/padContent';
|
||||
import padTail from './handlers/padTail';
|
||||
import sessionDeleted from './handlers/sessionDeleted';
|
||||
import captureSharedNotes from './handlers/captureSharedNotes';
|
||||
|
||||
RedisPubSub.on('PadGroupCreatedRespMsg', groupCreated);
|
||||
RedisPubSub.on('PadCreatedRespMsg', padCreated);
|
||||
@ -14,3 +15,4 @@ RedisPubSub.on('PadUpdatedEvtMsg', padUpdated);
|
||||
RedisPubSub.on('PadContentEvtMsg', padContent);
|
||||
RedisPubSub.on('PadTailEvtMsg', padTail);
|
||||
RedisPubSub.on('PadSessionDeletedEvtMsg', sessionDeleted);
|
||||
RedisPubSub.on('CaptureSharedNotesReqEvtMsg', captureSharedNotes);
|
||||
|
@ -0,0 +1,15 @@
|
||||
import { check } from 'meteor/check';
|
||||
import padCapture from '../methods/padCapture';
|
||||
|
||||
export default function captureSharedNotes({ body }, meetingId) {
|
||||
check(body, Object);
|
||||
check(meetingId, String);
|
||||
|
||||
const { parentMeetingId, meetingName, sequence } = body;
|
||||
|
||||
check(parentMeetingId, String);
|
||||
check(meetingName, String);
|
||||
check(sequence, Number);
|
||||
|
||||
padCapture(meetingId, parentMeetingId, meetingName, sequence);
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
import { check } from 'meteor/check';
|
||||
import Pads from '/imports/api/pads';
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default function padCapture(meetingId, parentMeetingId, meetingName, sequence) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'PadCapturePubMsg';
|
||||
const EXTERNAL_ID = Meteor.settings.public.notes.id;
|
||||
try {
|
||||
check(meetingId, String);
|
||||
check(parentMeetingId, String);
|
||||
check(meetingName, String);
|
||||
check(sequence, Number);
|
||||
|
||||
const pad = Pads.findOne(
|
||||
{
|
||||
meetingId,
|
||||
externalId: EXTERNAL_ID,
|
||||
},
|
||||
{
|
||||
fields: {
|
||||
padId: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const payload = {
|
||||
parentMeetingId,
|
||||
breakoutId: meetingId,
|
||||
padId: pad.padId,
|
||||
meetingName,
|
||||
sequence,
|
||||
};
|
||||
|
||||
Logger.info(`Sending PadCapturePubMsg for meetingId=${meetingId} parentMeetingId=${parentMeetingId} padId=${pad.padId}`);
|
||||
|
||||
if (pad && pad.padId) {
|
||||
return RedisPubSub.publishMeetingMessage(CHANNEL, EVENT_NAME, parentMeetingId, payload);
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (err) {
|
||||
Logger.error(`Exception while invoking method padCapture ${err.stack}`);
|
||||
return null;
|
||||
}
|
||||
}
|
@ -256,21 +256,12 @@ class Base extends Component {
|
||||
meetingEndedReason,
|
||||
meetingIsBreakout,
|
||||
subscriptionsReady,
|
||||
User,
|
||||
} = this.props;
|
||||
|
||||
if ((loading || !subscriptionsReady) && !meetingHasEnded && meetingExist) {
|
||||
return (<LoadingScreen>{loading}</LoadingScreen>);
|
||||
}
|
||||
|
||||
if (meetingIsBreakout && (ejected || userRemoved)) {
|
||||
Base.setExitReason('removedFromBreakout').finally(() => {
|
||||
Meteor.disconnect();
|
||||
window.close();
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
if (ejected) {
|
||||
return (
|
||||
<MeetingEnded
|
||||
@ -281,21 +272,19 @@ class Base extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if ((meetingHasEnded || User?.loggedOut) && meetingIsBreakout) {
|
||||
const reason = meetingHasEnded ? 'breakoutEnded' : 'logout';
|
||||
Base.setExitReason(reason).finally(() => {
|
||||
if (meetingHasEnded && meetingIsBreakout) {
|
||||
Base.setExitReason('breakoutEnded').finally(() => {
|
||||
Meteor.disconnect();
|
||||
window.close();
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((meetingHasEnded && !meetingIsBreakout) || (codeError && User?.loggedOut)) {
|
||||
if (meetingHasEnded && !meetingIsBreakout) {
|
||||
return (
|
||||
<MeetingEnded
|
||||
code={codeError}
|
||||
endedReason={meetingEndedReason}
|
||||
ejectedReason={ejectedReason}
|
||||
callback={() => Base.setExitReason('meetingEnded')}
|
||||
/>
|
||||
);
|
||||
|
@ -9,6 +9,7 @@ import _ from 'lodash';
|
||||
import { Session } from 'meteor/session';
|
||||
import Logger from '/imports/startup/client/logger';
|
||||
import { formatLocaleCode } from '/imports/utils/string-utils';
|
||||
import Intl from '/imports/ui/services/locale';
|
||||
|
||||
const propTypes = {
|
||||
locale: PropTypes.string,
|
||||
@ -66,6 +67,7 @@ class IntlStartup extends Component {
|
||||
const url = `./locale?locale=${locale}&init=${init}`;
|
||||
const localesPath = 'locales';
|
||||
|
||||
Intl.fetching = true;
|
||||
this.setState({ fetching: true }, () => {
|
||||
fetch(url)
|
||||
.then((response) => {
|
||||
@ -138,6 +140,7 @@ class IntlStartup extends Component {
|
||||
|
||||
const dasherizedLocale = normalizedLocale.replace('_', '-');
|
||||
const { language, formattedLocale } = formatLocaleCode(dasherizedLocale);
|
||||
Intl.setLocale(formattedLocale, mergedMessages);
|
||||
|
||||
this.setState({ messages: mergedMessages, fetching: false, normalizedLocale: dasherizedLocale }, () => {
|
||||
Settings.application.locale = dasherizedLocale;
|
||||
|
@ -318,7 +318,7 @@ class ActionsDropdown extends PureComponent {
|
||||
}
|
||||
actions={children}
|
||||
opts={{
|
||||
id: "default-dropdown-menu",
|
||||
id: "actions-dropdown-menu",
|
||||
keepMounted: true,
|
||||
transitionDuration: 0,
|
||||
elevation: 3,
|
||||
|
86
bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx
Executable file → Normal file
86
bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx
Executable file → Normal file
@ -10,6 +10,8 @@ import { withModalMounter } from '/imports/ui/components/common/modal/service';
|
||||
import SortList from './sort-user-list/component';
|
||||
import Styled from './styles';
|
||||
import Icon from '/imports/ui/components/common/icon/component.jsx';
|
||||
import { isImportSharedNotesFromBreakoutRoomsEnabled, isImportPresentationWithAnnotationsFromBreakoutRoomsEnabled } from '/imports/ui/services/features';
|
||||
import { addNewAlert } from '/imports/ui/components/screenreader-alert/service';
|
||||
|
||||
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
|
||||
|
||||
@ -82,6 +84,14 @@ const intlMessages = defineMessages({
|
||||
id: 'app.createBreakoutRoom.freeJoin',
|
||||
description: 'free join label',
|
||||
},
|
||||
captureNotesLabel: {
|
||||
id: 'app.createBreakoutRoom.captureNotes',
|
||||
description: 'capture shared notes label',
|
||||
},
|
||||
captureSlidesLabel: {
|
||||
id: 'app.createBreakoutRoom.captureSlides',
|
||||
description: 'capture slides label',
|
||||
},
|
||||
roomLabel: {
|
||||
id: 'app.createBreakoutRoom.room',
|
||||
description: 'Room label',
|
||||
@ -150,6 +160,10 @@ const intlMessages = defineMessages({
|
||||
id: 'app.createBreakoutRoom.roomNameInputDesc',
|
||||
description: 'aria description for room name change',
|
||||
},
|
||||
movedUserLabel: {
|
||||
id: 'app.createBreakoutRoom.movedUserLabel',
|
||||
description: 'screen reader alert when users are moved to rooms',
|
||||
}
|
||||
});
|
||||
|
||||
const BREAKOUT_LIM = Meteor.settings.public.app.breakouts.breakoutRoomLimit;
|
||||
@ -200,6 +214,8 @@ class BreakoutRoom extends PureComponent {
|
||||
this.handleDismiss = this.handleDismiss.bind(this);
|
||||
this.setInvitationConfig = this.setInvitationConfig.bind(this);
|
||||
this.setRecord = this.setRecord.bind(this);
|
||||
this.setCaptureNotes = this.setCaptureNotes.bind(this);
|
||||
this.setCaptureSlides = this.setCaptureSlides.bind(this);
|
||||
this.blurDurationTime = this.blurDurationTime.bind(this);
|
||||
this.removeRoomUsers = this.removeRoomUsers.bind(this);
|
||||
this.renderErrorMessages = this.renderErrorMessages.bind(this);
|
||||
@ -220,6 +236,8 @@ class BreakoutRoom extends PureComponent {
|
||||
roomNameDuplicatedIsValid: true,
|
||||
roomNameEmptyIsValid: true,
|
||||
record: false,
|
||||
captureNotes: false,
|
||||
captureSlides: false,
|
||||
durationIsValid: true,
|
||||
breakoutJoinedUsers: null,
|
||||
};
|
||||
@ -310,6 +328,7 @@ class BreakoutRoom extends PureComponent {
|
||||
users.forEach((u, index) => {
|
||||
if (`roomUserItem-${u.userId}` === document.activeElement.id) {
|
||||
users[index].room = text.substr(text.length - 1).includes(')') ? 0 : parseInt(roomNumber, 10);
|
||||
this.changeUserRoom(u.userId, users[index].room);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -386,6 +405,8 @@ class BreakoutRoom extends PureComponent {
|
||||
users,
|
||||
freeJoin,
|
||||
record,
|
||||
captureNotes,
|
||||
captureSlides,
|
||||
numberOfRoomsIsValid,
|
||||
numberOfRooms,
|
||||
durationTime,
|
||||
@ -430,7 +451,7 @@ class BreakoutRoom extends PureComponent {
|
||||
sequence: seq,
|
||||
}));
|
||||
|
||||
createBreakoutRoom(rooms, durationTime, record);
|
||||
createBreakoutRoom(rooms, durationTime, record, captureNotes, captureSlides);
|
||||
Session.set('isUserListOpen', true);
|
||||
}
|
||||
|
||||
@ -567,6 +588,14 @@ class BreakoutRoom extends PureComponent {
|
||||
this.setState({ record: e.target.checked });
|
||||
}
|
||||
|
||||
setCaptureNotes(e) {
|
||||
this.setState({ captureNotes: e.target.checked });
|
||||
}
|
||||
|
||||
setCaptureSlides(e) {
|
||||
this.setState({ captureSlides: e.target.checked });
|
||||
}
|
||||
|
||||
getUserByRoom(room) {
|
||||
const { users } = this.state;
|
||||
return users.filter((user) => user.room === room);
|
||||
@ -602,17 +631,24 @@ class BreakoutRoom extends PureComponent {
|
||||
}
|
||||
|
||||
changeUserRoom(userId, room) {
|
||||
const { intl } = this.props;
|
||||
const { users, freeJoin } = this.state;
|
||||
|
||||
const idxUser = users.findIndex((user) => user.userId === userId.replace('roomUserItem-', ''));
|
||||
|
||||
const usersCopy = [...users];
|
||||
let userName = null;
|
||||
|
||||
if (idxUser >= 0) usersCopy[idxUser].room = room;
|
||||
if (idxUser >= 0) {
|
||||
usersCopy[idxUser].room = room;
|
||||
userName = usersCopy[idxUser].userName;
|
||||
};
|
||||
|
||||
this.setState({
|
||||
users: usersCopy,
|
||||
leastOneUserIsValid: (this.getUserByRoom(0).length !== users.length || freeJoin),
|
||||
}, () => {
|
||||
addNewAlert(intl.formatMessage(intlMessages.movedUserLabel, { 0: userName, 1: room }))
|
||||
});
|
||||
}
|
||||
|
||||
@ -783,7 +819,7 @@ class BreakoutRoom extends PureComponent {
|
||||
};
|
||||
|
||||
return (
|
||||
<Styled.BoxContainer key="rooms-grid-" ref={(r) => { this.listOfUsers = r; }}>
|
||||
<Styled.BoxContainer key="rooms-grid-" ref={(r) => { this.listOfUsers = r; }} data-test="roomGrid">
|
||||
<Styled.Alert valid={leastOneUserIsValid} role="alert">
|
||||
<Styled.FreeJoinLabel>
|
||||
<Styled.BreakoutNameInput
|
||||
@ -797,7 +833,7 @@ class BreakoutRoom extends PureComponent {
|
||||
<Styled.BreakoutBox id="breakoutBox-0" onDrop={drop(0)} onDragOver={allowDrop} tabIndex={0}>
|
||||
{this.renderUserItemByRoom(0)}
|
||||
</Styled.BreakoutBox>
|
||||
<Styled.SpanWarn valid={leastOneUserIsValid}>
|
||||
<Styled.SpanWarn data-test="warningNoUserAssigned" valid={leastOneUserIsValid}>
|
||||
{intl.formatMessage(intlMessages.leastOneWarnBreakout)}
|
||||
</Styled.SpanWarn>
|
||||
</Styled.Alert>
|
||||
@ -814,6 +850,7 @@ class BreakoutRoom extends PureComponent {
|
||||
onBlur={changeRoomName(value)}
|
||||
aria-label={`${this.getRoomName(value)}`}
|
||||
aria-describedby={this.getRoomName(value).length === 0 ? `room-error-${value}` : `room-input-${value}`}
|
||||
data-test={this.getRoomName(value).length === 0 ? `room-error-${value}` : `roomName-${value}`}
|
||||
readOnly={isUpdate}
|
||||
/>
|
||||
<div aria-hidden id={`room-input-${value}`} className="sr-only">
|
||||
@ -885,6 +922,7 @@ class BreakoutRoom extends PureComponent {
|
||||
onChange={this.changeDurationTime}
|
||||
onBlur={this.blurDurationTime}
|
||||
aria-label={intl.formatMessage(intlMessages.duration)}
|
||||
data-test="durationTime"
|
||||
/>
|
||||
<Styled.HoldButtonWrapper
|
||||
key="decrease-breakout-time"
|
||||
@ -902,6 +940,7 @@ class BreakoutRoom extends PureComponent {
|
||||
hideLabel
|
||||
circle
|
||||
size="sm"
|
||||
data-test="decreaseBreakoutTime"
|
||||
/>
|
||||
</Styled.HoldButtonWrapper>
|
||||
<Styled.HoldButtonWrapper
|
||||
@ -918,6 +957,7 @@ class BreakoutRoom extends PureComponent {
|
||||
hideLabel
|
||||
circle
|
||||
size="sm"
|
||||
data-test="increaseBreakoutTime"
|
||||
/>
|
||||
</Styled.HoldButtonWrapper>
|
||||
</Styled.DurationArea>
|
||||
@ -979,11 +1019,15 @@ class BreakoutRoom extends PureComponent {
|
||||
}
|
||||
|
||||
renderCheckboxes() {
|
||||
const { intl, isUpdate, isBreakoutRecordable } = this.props;
|
||||
const {
|
||||
intl, isUpdate, isBreakoutRecordable,
|
||||
} = this.props;
|
||||
if (isUpdate) return null;
|
||||
const {
|
||||
freeJoin,
|
||||
record,
|
||||
captureNotes,
|
||||
captureSlides,
|
||||
} = this.state;
|
||||
return (
|
||||
<Styled.CheckBoxesContainer key="breakout-checkboxes">
|
||||
@ -1013,6 +1057,38 @@ class BreakoutRoom extends PureComponent {
|
||||
</Styled.FreeJoinLabel>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
isImportPresentationWithAnnotationsFromBreakoutRoomsEnabled() ? (
|
||||
<Styled.FreeJoinLabel htmlFor="captureSlidesBreakoutCheckbox" key="capture-slides-breakouts">
|
||||
<Styled.FreeJoinCheckbox
|
||||
id="captureSlidesBreakoutCheckbox"
|
||||
type="checkbox"
|
||||
onChange={this.setCaptureSlides}
|
||||
checked={captureSlides}
|
||||
aria-label={intl.formatMessage(intlMessages.captureSlidesLabel)}
|
||||
/>
|
||||
<span aria-hidden>
|
||||
{intl.formatMessage(intlMessages.captureSlidesLabel)}
|
||||
</span>
|
||||
</Styled.FreeJoinLabel>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
isImportSharedNotesFromBreakoutRoomsEnabled() ? (
|
||||
<Styled.FreeJoinLabel htmlFor="captureNotesBreakoutCheckbox" key="capture-notes-breakouts">
|
||||
<Styled.FreeJoinCheckbox
|
||||
id="captureNotesBreakoutCheckbox"
|
||||
type="checkbox"
|
||||
onChange={this.setCaptureNotes}
|
||||
checked={captureNotes}
|
||||
aria-label={intl.formatMessage(intlMessages.captureNotesLabel)}
|
||||
/>
|
||||
<span aria-hidden>
|
||||
{intl.formatMessage(intlMessages.captureNotesLabel)}
|
||||
</span>
|
||||
</Styled.FreeJoinLabel>
|
||||
) : null
|
||||
}
|
||||
</Styled.CheckBoxesContainer>
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages } from 'react-intl';
|
||||
import _ from 'lodash';
|
||||
@ -8,6 +8,7 @@ import { PANELS, ACTIONS } from '../../layout/enums';
|
||||
|
||||
const POLL_SETTINGS = Meteor.settings.public.poll;
|
||||
const MAX_CUSTOM_FIELDS = POLL_SETTINGS.maxCustom;
|
||||
const CANCELED_POLL_DELAY = 250;
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
quickPollLabel: {
|
||||
@ -34,6 +35,10 @@ const intlMessages = defineMessages({
|
||||
id: 'app.poll.abstention',
|
||||
description: 'Poll Abstention option value',
|
||||
},
|
||||
typedRespLabel: {
|
||||
id: 'app.poll.userResponse.label',
|
||||
description: 'quick poll typed response label',
|
||||
},
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
@ -44,172 +49,218 @@ const propTypes = {
|
||||
amIPresenter: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const handleClickQuickPoll = (layoutContextDispatch) => {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||
value: true,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||
value: PANELS.POLL,
|
||||
});
|
||||
Session.set('forcePollOpen', true);
|
||||
Session.set('pollInitiated', true);
|
||||
};
|
||||
const QuickPollDropdown = (props) => {
|
||||
const {
|
||||
amIPresenter,
|
||||
intl,
|
||||
parseCurrentSlideContent,
|
||||
startPoll,
|
||||
stopPoll,
|
||||
currentSlide,
|
||||
activePoll,
|
||||
className,
|
||||
layoutContextDispatch,
|
||||
pollTypes,
|
||||
} = props;
|
||||
|
||||
const getAvailableQuickPolls = (
|
||||
slideId, parsedSlides, startPoll, pollTypes, layoutContextDispatch,
|
||||
) => {
|
||||
const pollItemElements = parsedSlides.map((poll) => {
|
||||
const { poll: label } = poll;
|
||||
const { type } = poll;
|
||||
let itemLabel = label;
|
||||
const letterAnswers = [];
|
||||
const parsedSlide = parseCurrentSlideContent(
|
||||
intl.formatMessage(intlMessages.yesOptionLabel),
|
||||
intl.formatMessage(intlMessages.noOptionLabel),
|
||||
intl.formatMessage(intlMessages.abstentionOptionLabel),
|
||||
intl.formatMessage(intlMessages.trueOptionLabel),
|
||||
intl.formatMessage(intlMessages.falseOptionLabel),
|
||||
);
|
||||
|
||||
if (type !== pollTypes.YesNo
|
||||
&& type !== pollTypes.YesNoAbstention
|
||||
&& type !== pollTypes.TrueFalse) {
|
||||
const { options } = itemLabel;
|
||||
itemLabel = options.join('/').replace(/[\n.)]/g, '');
|
||||
if (type === pollTypes.Custom) {
|
||||
for (let i = 0; i < options.length; i += 1) {
|
||||
const letterOption = options[i]?.replace(/[\r.)]/g, '').toUpperCase();
|
||||
if (letterAnswers.length < MAX_CUSTOM_FIELDS) {
|
||||
letterAnswers.push(letterOption);
|
||||
} else {
|
||||
break;
|
||||
const {
|
||||
slideId, quickPollOptions, optionsWithLabels, pollQuestion,
|
||||
} = parsedSlide;
|
||||
|
||||
const handleClickQuickPoll = (lCDispatch) => {
|
||||
lCDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||
value: true,
|
||||
});
|
||||
lCDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||
value: PANELS.POLL,
|
||||
});
|
||||
Session.set('forcePollOpen', true);
|
||||
Session.set('pollInitiated', true);
|
||||
};
|
||||
|
||||
const getAvailableQuickPolls = (
|
||||
slideId, parsedSlides, funcStartPoll, _pollTypes, _layoutContextDispatch,
|
||||
) => {
|
||||
const pollItemElements = parsedSlides.map((poll) => {
|
||||
const { poll: label } = poll;
|
||||
const { type, poll: pollData } = poll;
|
||||
let itemLabel = label;
|
||||
const letterAnswers = [];
|
||||
|
||||
if (type === 'R-') {
|
||||
return (
|
||||
<Dropdown.DropdownListItem
|
||||
label={intl.formatMessage(intlMessages.typedRespLabel)}
|
||||
key={_.uniqueId('quick-poll-item')}
|
||||
onClick={() => {
|
||||
if (activePoll) {
|
||||
stopPoll();
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleClickQuickPoll(_layoutContextDispatch);
|
||||
funcStartPoll(type, slideId, letterAnswers, pollData?.question);
|
||||
}, CANCELED_POLL_DELAY);
|
||||
}}
|
||||
question={pollData?.question}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (type !== _pollTypes.YesNo
|
||||
&& type !== _pollTypes.YesNoAbstention
|
||||
&& type !== _pollTypes.TrueFalse) {
|
||||
const { options } = itemLabel;
|
||||
itemLabel = options.join('/').replace(/[\n.)]/g, '');
|
||||
if (type === _pollTypes.Custom) {
|
||||
for (let i = 0; i < options.length; i += 1) {
|
||||
const letterOption = options[i]?.replace(/[\r.)]/g, '').toUpperCase();
|
||||
if (letterAnswers.length < MAX_CUSTOM_FIELDS) {
|
||||
letterAnswers.push(letterOption);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// removes any whitespace from the label
|
||||
itemLabel = itemLabel?.replace(/\s+/g, '').toUpperCase();
|
||||
// removes any whitespace from the label
|
||||
itemLabel = itemLabel?.replace(/\s+/g, '').toUpperCase();
|
||||
|
||||
const numChars = {
|
||||
1: 'A', 2: 'B', 3: 'C', 4: 'D', 5: 'E',
|
||||
};
|
||||
itemLabel = itemLabel.split('').map((c) => {
|
||||
if (numChars[c]) return numChars[c];
|
||||
return c;
|
||||
}).join('');
|
||||
const numChars = {
|
||||
1: 'A', 2: 'B', 3: 'C', 4: 'D', 5: 'E',
|
||||
};
|
||||
itemLabel = itemLabel.split('').map((c) => {
|
||||
if (numChars[c]) return numChars[c];
|
||||
return c;
|
||||
}).join('');
|
||||
|
||||
return (
|
||||
<Dropdown.DropdownListItem
|
||||
label={itemLabel}
|
||||
key={_.uniqueId('quick-poll-item')}
|
||||
onClick={() => {
|
||||
return (
|
||||
<Dropdown.DropdownListItem
|
||||
label={itemLabel}
|
||||
key={_.uniqueId('quick-poll-item')}
|
||||
onClick={() => {
|
||||
if (activePoll) {
|
||||
stopPoll();
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleClickQuickPoll(_layoutContextDispatch);
|
||||
funcStartPoll(type, slideId, letterAnswers, pollQuestion, pollData?.multiResp);
|
||||
}, CANCELED_POLL_DELAY);
|
||||
}}
|
||||
answers={letterAnswers}
|
||||
multiResp={pollData?.multiResp}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const sizes = [];
|
||||
return pollItemElements.filter((el) => {
|
||||
const { label } = el.props;
|
||||
if (label.length === sizes[sizes.length - 1]) return false;
|
||||
sizes.push(label.length);
|
||||
return el;
|
||||
});
|
||||
};
|
||||
|
||||
const quickPolls = getAvailableQuickPolls(
|
||||
slideId, quickPollOptions, startPoll, pollTypes, layoutContextDispatch,
|
||||
);
|
||||
|
||||
if (quickPollOptions.length === 0) return null;
|
||||
|
||||
let answers = null;
|
||||
let question = '';
|
||||
let quickPollLabel = '';
|
||||
let multiResponse = false;
|
||||
|
||||
if (quickPolls.length > 0) {
|
||||
const { props: pollProps } = quickPolls[0];
|
||||
quickPollLabel = pollProps?.label;
|
||||
answers = pollProps?.answers;
|
||||
question = pollProps?.question;
|
||||
multiResponse = pollProps?.multiResp;
|
||||
}
|
||||
|
||||
let singlePollType = null;
|
||||
if (quickPolls.length === 1 && quickPollOptions.length) {
|
||||
const { type } = quickPollOptions[0];
|
||||
singlePollType = type;
|
||||
}
|
||||
|
||||
let btn = (
|
||||
<Styled.QuickPollButton
|
||||
aria-label={intl.formatMessage(intlMessages.quickPollLabel)}
|
||||
label={quickPollLabel}
|
||||
tooltipLabel={intl.formatMessage(intlMessages.quickPollLabel)}
|
||||
onClick={() => {
|
||||
if (activePoll) {
|
||||
stopPoll();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
handleClickQuickPoll(layoutContextDispatch);
|
||||
startPoll(type, slideId, letterAnswers);
|
||||
}}
|
||||
answers={letterAnswers}
|
||||
/>
|
||||
);
|
||||
});
|
||||
if (singlePollType === 'R-' || singlePollType === 'TF') {
|
||||
startPoll(singlePollType, currentSlide.id, answers, pollQuestion, multiResponse);
|
||||
} else {
|
||||
startPoll(
|
||||
pollTypes.Custom,
|
||||
currentSlide.id,
|
||||
optionsWithLabels,
|
||||
pollQuestion,
|
||||
multiResponse,
|
||||
);
|
||||
}
|
||||
}, CANCELED_POLL_DELAY);
|
||||
}}
|
||||
size="lg"
|
||||
data-test="quickPollBtn"
|
||||
/>
|
||||
);
|
||||
|
||||
const sizes = [];
|
||||
return pollItemElements.filter((el) => {
|
||||
const { label } = el.props;
|
||||
if (label.length === sizes[sizes.length - 1]) return false;
|
||||
sizes.push(label.length);
|
||||
return el;
|
||||
});
|
||||
};
|
||||
const usePollDropdown = quickPollOptions && quickPollOptions.length && quickPolls.length > 1;
|
||||
let dropdown = null;
|
||||
|
||||
class QuickPollDropdown extends Component {
|
||||
render() {
|
||||
const {
|
||||
amIPresenter,
|
||||
intl,
|
||||
parseCurrentSlideContent,
|
||||
startPoll,
|
||||
currentSlide,
|
||||
activePoll,
|
||||
className,
|
||||
layoutContextDispatch,
|
||||
pollTypes,
|
||||
} = this.props;
|
||||
|
||||
const parsedSlide = parseCurrentSlideContent(
|
||||
intl.formatMessage(intlMessages.yesOptionLabel),
|
||||
intl.formatMessage(intlMessages.noOptionLabel),
|
||||
intl.formatMessage(intlMessages.abstentionOptionLabel),
|
||||
intl.formatMessage(intlMessages.trueOptionLabel),
|
||||
intl.formatMessage(intlMessages.falseOptionLabel),
|
||||
);
|
||||
|
||||
const { slideId, quickPollOptions } = parsedSlide;
|
||||
const quickPolls = getAvailableQuickPolls(
|
||||
slideId, quickPollOptions, startPoll, pollTypes, layoutContextDispatch,
|
||||
);
|
||||
|
||||
if (quickPollOptions.length === 0) return null;
|
||||
|
||||
let answers = null;
|
||||
let quickPollLabel = '';
|
||||
if (quickPolls.length > 0) {
|
||||
const { props: pollProps } = quickPolls[0];
|
||||
quickPollLabel = pollProps.label;
|
||||
answers = pollProps.answers;
|
||||
}
|
||||
|
||||
let singlePollType = null;
|
||||
if (quickPolls.length === 1 && quickPollOptions.length) {
|
||||
const { type } = quickPollOptions[0];
|
||||
singlePollType = type;
|
||||
}
|
||||
|
||||
let btn = (
|
||||
if (usePollDropdown) {
|
||||
btn = (
|
||||
<Styled.QuickPollButton
|
||||
aria-label={intl.formatMessage(intlMessages.quickPollLabel)}
|
||||
label={quickPollLabel}
|
||||
tooltipLabel={intl.formatMessage(intlMessages.quickPollLabel)}
|
||||
onClick={() => {
|
||||
handleClickQuickPoll(layoutContextDispatch);
|
||||
startPoll(singlePollType, currentSlide.id, answers);
|
||||
}}
|
||||
onClick={() => null}
|
||||
size="lg"
|
||||
disabled={!!activePoll}
|
||||
data-test="quickPollBtn"
|
||||
/>
|
||||
);
|
||||
|
||||
const usePollDropdown = quickPollOptions && quickPollOptions.length && quickPolls.length > 1;
|
||||
let dropdown = null;
|
||||
|
||||
if (usePollDropdown) {
|
||||
btn = (
|
||||
<Styled.QuickPollButton
|
||||
aria-label={intl.formatMessage(intlMessages.quickPollLabel)}
|
||||
label={quickPollLabel}
|
||||
tooltipLabel={intl.formatMessage(intlMessages.quickPollLabel)}
|
||||
onClick={() => null}
|
||||
size="lg"
|
||||
disabled={!!activePoll}
|
||||
/>
|
||||
);
|
||||
|
||||
dropdown = (
|
||||
<Dropdown className={className}>
|
||||
<Dropdown.DropdownTrigger tabIndex={0}>
|
||||
{btn}
|
||||
</Dropdown.DropdownTrigger>
|
||||
<Dropdown.DropdownContent>
|
||||
<Dropdown.DropdownList>
|
||||
{quickPolls}
|
||||
</Dropdown.DropdownList>
|
||||
</Dropdown.DropdownContent>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
return amIPresenter && usePollDropdown ? (
|
||||
dropdown
|
||||
) : (
|
||||
btn
|
||||
dropdown = (
|
||||
<Dropdown className={className}>
|
||||
<Dropdown.DropdownTrigger tabIndex={0}>
|
||||
{btn}
|
||||
</Dropdown.DropdownTrigger>
|
||||
<Dropdown.DropdownContent>
|
||||
<Dropdown.DropdownList>
|
||||
{quickPolls}
|
||||
</Dropdown.DropdownList>
|
||||
</Dropdown.DropdownContent>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return amIPresenter && usePollDropdown ? (
|
||||
dropdown
|
||||
) : (
|
||||
btn
|
||||
);
|
||||
};
|
||||
|
||||
QuickPollDropdown.propTypes = propTypes;
|
||||
|
||||
|
@ -1,17 +1,18 @@
|
||||
import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import { injectIntl } from 'react-intl';
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
import PollService from '/imports/ui/components/poll/service';
|
||||
import QuickPollDropdown from './component';
|
||||
import { layoutDispatch } from '../../layout/context';
|
||||
import PollService from '/imports/ui/components/poll/service';
|
||||
|
||||
const QuickPollDropdownContainer = (props) => {
|
||||
const layoutContextDispatch = layoutDispatch();
|
||||
|
||||
return <QuickPollDropdown {...{ layoutContextDispatch, ...props }} />;
|
||||
};
|
||||
|
||||
export default withTracker(() => ({
|
||||
activePoll: Session.get('pollInitiated') || false,
|
||||
pollTypes: PollService.pollTypes,
|
||||
stopPoll: () => makeCall('stopPoll'),
|
||||
}))(injectIntl(QuickPollDropdownContainer));
|
||||
|
@ -16,7 +16,7 @@ import { SCREENSHARING_ERRORS } from '/imports/api/screenshare/client/bridge/err
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
|
||||
const { isMobile } = deviceInfo;
|
||||
const { isSafari, isMobileApp } = browserInfo;
|
||||
const { isSafari, isTabletApp } = browserInfo;
|
||||
|
||||
const propTypes = {
|
||||
intl: PropTypes.objectOf(Object).isRequired,
|
||||
@ -163,7 +163,7 @@ const ScreenshareButton = ({
|
||||
? intlMessages.stopDesktopShareDesc : intlMessages.desktopShareDesc;
|
||||
|
||||
const shouldAllowScreensharing = enabled
|
||||
&& ( !isMobile || isMobileApp)
|
||||
&& ( !isMobile || isTabletApp)
|
||||
&& amIPresenter;
|
||||
|
||||
const dataTest = isVideoBroadcasting ? 'stopScreenShare' : 'startScreenShare';
|
||||
|
@ -63,7 +63,7 @@ export default {
|
||||
isBreakoutRecordable: () => Meetings.findOne({ meetingId: Auth.meetingID },
|
||||
{ fields: { 'breakoutProps.record': 1 } }).breakoutProps.record,
|
||||
toggleRecording: () => makeCall('toggleRecording'),
|
||||
createBreakoutRoom: (rooms, durationInMinutes, record = false) => makeCall('createBreakoutRoom', rooms, durationInMinutes, record),
|
||||
createBreakoutRoom: (rooms, durationInMinutes, record = false, captureNotes = false, captureSlides = false) => makeCall('createBreakoutRoom', rooms, durationInMinutes, record, captureNotes, captureSlides),
|
||||
sendInvitation: (breakoutId, userId) => makeCall('requestJoinURL', { breakoutId, userId }),
|
||||
breakoutJoinedUsers: () => Breakouts.find({
|
||||
joinedUsers: { $exists: true },
|
||||
|
@ -81,6 +81,8 @@ class AudioControls extends PureComponent {
|
||||
handleLeaveAudio, handleToggleMuteMicrophone, muted, disable, talking,
|
||||
} = this.props;
|
||||
|
||||
const { isMobile } = deviceInfo;
|
||||
|
||||
return (
|
||||
<InputStreamLiveSelectorContainer {...{
|
||||
handleLeaveAudio,
|
||||
@ -88,6 +90,7 @@ class AudioControls extends PureComponent {
|
||||
muted,
|
||||
disable,
|
||||
talking,
|
||||
isMobile,
|
||||
_enableDynamicDeviceSelection,
|
||||
}}
|
||||
/>
|
||||
|
@ -366,8 +366,8 @@ class InputStreamLiveSelector extends Component {
|
||||
currentInputDeviceId,
|
||||
currentOutputDeviceId,
|
||||
isListenOnly,
|
||||
isRTL,
|
||||
shortcuts,
|
||||
isMobile,
|
||||
} = this.props;
|
||||
|
||||
const inputDeviceList = !isListenOnly
|
||||
@ -399,6 +399,7 @@ class InputStreamLiveSelector extends Component {
|
||||
};
|
||||
|
||||
const dropdownListComplete = inputDeviceList.concat(outputDeviceList).concat(leaveAudioOption);
|
||||
const customStyles = { top: '-1rem' };
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -411,6 +412,7 @@ class InputStreamLiveSelector extends Component {
|
||||
/>
|
||||
) : null}
|
||||
<BBBMenu
|
||||
customStyles={!isMobile ? customStyles : null}
|
||||
trigger={(
|
||||
<>
|
||||
{isListenOnly
|
||||
@ -428,14 +430,14 @@ class InputStreamLiveSelector extends Component {
|
||||
)}
|
||||
actions={dropdownListComplete}
|
||||
opts={{
|
||||
id: 'default-dropdown-menu',
|
||||
id: 'audio-selector-dropdown-menu',
|
||||
keepMounted: true,
|
||||
transitionDuration: 0,
|
||||
elevation: 3,
|
||||
getContentAnchorEl: null,
|
||||
fullwidth: 'true',
|
||||
anchorOrigin: { vertical: 'top', horizontal: isRTL ? 'left' : 'right' },
|
||||
transformOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' },
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'center' },
|
||||
transformOrigin: { vertical: 'bottom', horizontal: 'center'},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
@ -94,7 +94,7 @@ const ConnectingAnimation = styled.span`
|
||||
`;
|
||||
|
||||
const AudioModal = styled(Modal)`
|
||||
padding: 1.5rem;
|
||||
padding: 1rem;
|
||||
min-height: 20rem;
|
||||
`;
|
||||
|
||||
|
@ -98,6 +98,7 @@ class AudioSettings extends React.Component {
|
||||
componentDidMount() {
|
||||
const { inputDeviceId, outputDeviceId } = this.state;
|
||||
|
||||
Session.set('inEchoTest', true);
|
||||
this._isMounted = true;
|
||||
// Guarantee initial in/out devices are initialized on all ends
|
||||
this.setInputDevice(inputDeviceId);
|
||||
@ -107,6 +108,7 @@ class AudioSettings extends React.Component {
|
||||
componentWillUnmount() {
|
||||
const { stream } = this.state;
|
||||
|
||||
Session.set('inEchoTest', false);
|
||||
this._mounted = false;
|
||||
|
||||
if (stream) {
|
||||
|
@ -1,7 +1,10 @@
|
||||
import React from 'react';
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Service from '/imports/ui/components/audio/captions/service';
|
||||
import SpeechService from '/imports/ui/components/audio/captions/speech/service';
|
||||
import ButtonEmoji from '/imports/ui/components/common/button/button-emoji/ButtonEmoji';
|
||||
import BBBMenu from '/imports/ui/components/common/menu/component';
|
||||
import Styled from './styles';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
@ -13,18 +16,155 @@ const intlMessages = defineMessages({
|
||||
id: 'app.audio.captions.button.stop',
|
||||
description: 'Stop audio captions',
|
||||
},
|
||||
transcriptionSettings: {
|
||||
id: 'app.audio.captions.button.transcriptionSettings',
|
||||
description: 'Audio captions settings modal',
|
||||
},
|
||||
transcription: {
|
||||
id: 'app.audio.captions.button.transcription',
|
||||
description: 'Audio speech transcription label',
|
||||
},
|
||||
transcriptionOn: {
|
||||
id: 'app.switch.onLabel',
|
||||
},
|
||||
transcriptionOff: {
|
||||
id: 'app.switch.offLabel',
|
||||
},
|
||||
language: {
|
||||
id: 'app.audio.captions.button.language',
|
||||
description: 'Audio speech recognition language label',
|
||||
},
|
||||
'de-DE': {
|
||||
id: 'app.audio.captions.select.de-DE',
|
||||
description: 'Audio speech recognition german language',
|
||||
},
|
||||
'en-US': {
|
||||
id: 'app.audio.captions.select.en-US',
|
||||
description: 'Audio speech recognition english language',
|
||||
},
|
||||
'es-ES': {
|
||||
id: 'app.audio.captions.select.es-ES',
|
||||
description: 'Audio speech recognition spanish language',
|
||||
},
|
||||
'fr-FR': {
|
||||
id: 'app.audio.captions.select.fr-FR',
|
||||
description: 'Audio speech recognition french language',
|
||||
},
|
||||
'hi-ID': {
|
||||
id: 'app.audio.captions.select.hi-ID',
|
||||
description: 'Audio speech recognition indian language',
|
||||
},
|
||||
'it-IT': {
|
||||
id: 'app.audio.captions.select.it-IT',
|
||||
description: 'Audio speech recognition italian language',
|
||||
},
|
||||
'ja-JP': {
|
||||
id: 'app.audio.captions.select.ja-JP',
|
||||
description: 'Audio speech recognition japanese language',
|
||||
},
|
||||
'pt-BR': {
|
||||
id: 'app.audio.captions.select.pt-BR',
|
||||
description: 'Audio speech recognition portuguese language',
|
||||
},
|
||||
'ru-RU': {
|
||||
id: 'app.audio.captions.select.ru-RU',
|
||||
description: 'Audio speech recognition russian language',
|
||||
},
|
||||
'zh-CN': {
|
||||
id: 'app.audio.captions.select.zh-CN',
|
||||
description: 'Audio speech recognition chinese language',
|
||||
},
|
||||
});
|
||||
|
||||
const DEFAULT_LOCALE = 'en-US';
|
||||
const DISABLED = '';
|
||||
|
||||
const CaptionsButton = ({
|
||||
intl,
|
||||
active,
|
||||
isRTL,
|
||||
enabled,
|
||||
currentSpeechLocale,
|
||||
availableVoices,
|
||||
isSupported,
|
||||
isVoiceUser,
|
||||
}) => {
|
||||
const onClick = () => Service.setAudioCaptions(!active);
|
||||
|
||||
if (!enabled) return null;
|
||||
|
||||
return (
|
||||
const isTranscriptionDisabled = () => (
|
||||
currentSpeechLocale === DISABLED
|
||||
);
|
||||
|
||||
const fallbackLocale = availableVoices.includes(navigator.language)
|
||||
? navigator.language : DEFAULT_LOCALE;
|
||||
|
||||
const getSelectedLocaleValue = (isTranscriptionDisabled() ? fallbackLocale : currentSpeechLocale);
|
||||
|
||||
const selectedLocale = useRef(getSelectedLocaleValue);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTranscriptionDisabled()) selectedLocale.current = getSelectedLocaleValue;
|
||||
}, [currentSpeechLocale]);
|
||||
|
||||
const shouldRenderChevron = isSupported && isVoiceUser;
|
||||
|
||||
const getAvailableLocales = () => (
|
||||
availableVoices.map((availableVoice) => (
|
||||
{
|
||||
icon: '',
|
||||
label: intl.formatMessage(intlMessages[availableVoice]),
|
||||
key: availableVoice,
|
||||
iconRight: selectedLocale.current === availableVoice ? 'check' : null,
|
||||
customStyles: (selectedLocale.current === availableVoice) && Styled.SelectedLabel,
|
||||
disabled: isTranscriptionDisabled(),
|
||||
dividerTop: availableVoice === availableVoices[0],
|
||||
onClick: () => {
|
||||
selectedLocale.current = availableVoice;
|
||||
SpeechService.setSpeechLocale(selectedLocale.current);
|
||||
},
|
||||
}
|
||||
))
|
||||
);
|
||||
|
||||
const toggleTranscription = () => {
|
||||
SpeechService.setSpeechLocale(isTranscriptionDisabled() ? selectedLocale.current : DISABLED);
|
||||
};
|
||||
|
||||
const getAvailableLocalesList = () => (
|
||||
[{
|
||||
key: 'availableLocalesList',
|
||||
label: intl.formatMessage(intlMessages.language),
|
||||
customStyles: Styled.TitleLabel,
|
||||
disabled: true,
|
||||
dividerTop: false,
|
||||
},
|
||||
...getAvailableLocales(),
|
||||
{
|
||||
key: 'divider',
|
||||
label: intl.formatMessage(intlMessages.transcription),
|
||||
customStyles: Styled.TitleLabel,
|
||||
disabled: true,
|
||||
}, {
|
||||
key: 'transcriptionStatus',
|
||||
label: intl.formatMessage(
|
||||
isTranscriptionDisabled()
|
||||
? intlMessages.transcriptionOn
|
||||
: intlMessages.transcriptionOff,
|
||||
),
|
||||
customStyles: isTranscriptionDisabled()
|
||||
? Styled.EnableTrascription : Styled.DisableTrascription,
|
||||
disabled: false,
|
||||
dividerTop: true,
|
||||
onClick: toggleTranscription,
|
||||
}]
|
||||
);
|
||||
|
||||
const onToggleClick = (e) => {
|
||||
e.stopPropagation();
|
||||
Service.setAudioCaptions(!active);
|
||||
};
|
||||
|
||||
const startStopCaptionsButton = (
|
||||
<Styled.ClosedCaptionToggleButton
|
||||
icon={active ? 'closed_caption' : 'closed_caption_stop'}
|
||||
label={intl.formatMessage(active ? intlMessages.stop : intlMessages.start)}
|
||||
@ -33,9 +173,42 @@ const CaptionsButton = ({
|
||||
hideLabel
|
||||
circle
|
||||
size="lg"
|
||||
onClick={onClick}
|
||||
onClick={onToggleClick}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
shouldRenderChevron
|
||||
? (
|
||||
<Styled.SpanButtonWrapper>
|
||||
<BBBMenu
|
||||
trigger={(
|
||||
<>
|
||||
{ startStopCaptionsButton }
|
||||
<ButtonEmoji
|
||||
emoji="device_list_selector"
|
||||
hideLabel
|
||||
label={intl.formatMessage(intlMessages.transcriptionSettings)}
|
||||
tabIndex={0}
|
||||
rotate
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
actions={getAvailableLocalesList()}
|
||||
opts={{
|
||||
id: 'default-dropdown-menu',
|
||||
keepMounted: true,
|
||||
transitionDuration: 0,
|
||||
elevation: 3,
|
||||
getContentAnchorEl: null,
|
||||
fullwidth: 'true',
|
||||
anchorOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' },
|
||||
transformOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' },
|
||||
}}
|
||||
/>
|
||||
</Styled.SpanButtonWrapper>
|
||||
) : startStopCaptionsButton
|
||||
);
|
||||
};
|
||||
|
||||
CaptionsButton.propTypes = {
|
||||
@ -43,7 +216,12 @@ CaptionsButton.propTypes = {
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
active: PropTypes.bool.isRequired,
|
||||
isRTL: PropTypes.bool.isRequired,
|
||||
enabled: PropTypes.bool.isRequired,
|
||||
currentSpeechLocale: PropTypes.string.isRequired,
|
||||
availableVoices: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
isSupported: PropTypes.bool.isRequired,
|
||||
isVoiceUser: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CaptionsButton);
|
||||
|
@ -2,10 +2,24 @@ import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import Service from '/imports/ui/components/audio/captions/service';
|
||||
import Button from './component';
|
||||
import SpeechService from '/imports/ui/components/audio/captions/speech/service';
|
||||
import AudioService from '/imports/ui/components/audio/service';
|
||||
|
||||
const Container = (props) => <Button {...props} />;
|
||||
|
||||
export default withTracker(() => ({
|
||||
enabled: Service.hasAudioCaptions(),
|
||||
active: Service.getAudioCaptions(),
|
||||
}))(Container);
|
||||
export default withTracker(() => {
|
||||
const isRTL = document.documentElement.getAttribute('dir') === 'rtl';
|
||||
const availableVoices = SpeechService.getSpeechVoices();
|
||||
const currentSpeechLocale = SpeechService.getSpeechLocale();
|
||||
const isSupported = availableVoices.length > 0;
|
||||
const isVoiceUser = AudioService.isVoiceUser();
|
||||
return {
|
||||
isRTL,
|
||||
enabled: Service.hasAudioCaptions(),
|
||||
active: Service.getAudioCaptions(),
|
||||
currentSpeechLocale,
|
||||
availableVoices,
|
||||
isSupported,
|
||||
isVoiceUser,
|
||||
};
|
||||
})(Container);
|
||||
|
@ -1,6 +1,13 @@
|
||||
import styled from 'styled-components';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
import { colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import Toggle from '/imports/ui/components/common/switch/component';
|
||||
import {
|
||||
colorWhite,
|
||||
colorPrimary,
|
||||
colorOffWhite,
|
||||
colorDangerDark,
|
||||
colorSuccess,
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
|
||||
const ClosedCaptionToggleButton = styled(Button)`
|
||||
${({ ghost }) => ghost && `
|
||||
@ -15,6 +22,40 @@ const ClosedCaptionToggleButton = styled(Button)`
|
||||
`}
|
||||
`;
|
||||
|
||||
const SpanButtonWrapper = styled.span`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const TranscriptionToggle = styled(Toggle)`
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
padding-left: 1em;
|
||||
`;
|
||||
|
||||
const TitleLabel = {
|
||||
fontWeight: 'bold',
|
||||
opacity: 1,
|
||||
};
|
||||
|
||||
const EnableTrascription = {
|
||||
color: colorSuccess,
|
||||
};
|
||||
|
||||
const DisableTrascription = {
|
||||
color: colorDangerDark,
|
||||
};
|
||||
|
||||
const SelectedLabel = {
|
||||
color: colorPrimary,
|
||||
backgroundColor: colorOffWhite,
|
||||
};
|
||||
|
||||
export default {
|
||||
ClosedCaptionToggleButton,
|
||||
SpanButtonWrapper,
|
||||
TranscriptionToggle,
|
||||
TitleLabel,
|
||||
EnableTrascription,
|
||||
DisableTrascription,
|
||||
SelectedLabel,
|
||||
};
|
||||
|
@ -17,7 +17,6 @@ const intlMessages = defineMessages({
|
||||
manageUsers: {
|
||||
id: 'app.breakout.dropdown.manageUsers',
|
||||
description: 'Manage users label',
|
||||
defaultMessage: 'Manage Users',
|
||||
},
|
||||
destroy: {
|
||||
id: 'app.breakout.dropdown.destroyAll',
|
||||
@ -102,7 +101,7 @@ class BreakoutDropdown extends PureComponent {
|
||||
/>
|
||||
}
|
||||
opts={{
|
||||
id: "default-dropdown-menu",
|
||||
id: "breakoutroom-dropdown-menu",
|
||||
keepMounted: true,
|
||||
transitionDuration: 0,
|
||||
elevation: 3,
|
||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
|
||||
const BreakoutRemainingTime = props => (
|
||||
<span data-test="breakoutRemainingTime">
|
||||
<span data-test="timeRemaining">
|
||||
{props.children}
|
||||
</span>
|
||||
);
|
||||
|
@ -68,7 +68,7 @@ class breakoutRemainingTimeContainer extends React.Component {
|
||||
<BreakoutRemainingTimeComponent>
|
||||
<Text>{text}</Text>
|
||||
<br />
|
||||
<Time>{time}</Time>
|
||||
<Time data-test="breakoutRemainingTime">{time}</Time>
|
||||
</BreakoutRemainingTimeComponent>
|
||||
);
|
||||
}
|
||||
|
@ -429,11 +429,10 @@ class BreakoutRoom extends PureComponent {
|
||||
} = this.state;
|
||||
|
||||
const { animations } = Settings.application;
|
||||
|
||||
const roomItems = breakoutRooms.map((breakout) => (
|
||||
<Styled.BreakoutItems key={`breakoutRoomItems-${breakout.breakoutId}`} >
|
||||
<Styled.Content key={`breakoutRoomList-${breakout.breakoutId}`}>
|
||||
<Styled.BreakoutRoomListNameLabel aria-hidden>
|
||||
<Styled.BreakoutRoomListNameLabel data-test={breakout.shortName} aria-hidden>
|
||||
{breakout.isDefaultName
|
||||
? intl.formatMessage(intlMessages.breakoutRoom, { 0: breakout.sequence })
|
||||
: breakout.shortName}
|
||||
@ -454,7 +453,9 @@ class BreakoutRoom extends PureComponent {
|
||||
breakout.shortName,
|
||||
)}
|
||||
</Styled.Content>
|
||||
<Styled.JoinedUserNames>
|
||||
<Styled.JoinedUserNames
|
||||
data-test={`userNameBreakoutRoom-${breakout.shortName}`}
|
||||
>
|
||||
{breakout.joinedUsers
|
||||
.sort(BreakoutRoom.sortById)
|
||||
.filter((value, idx, arr) => !(value.userId === (arr[idx + 1] || {}).userId))
|
||||
@ -467,7 +468,7 @@ class BreakoutRoom extends PureComponent {
|
||||
|
||||
return (
|
||||
<Styled.BreakoutColumn>
|
||||
<Styled.BreakoutScrollableList>
|
||||
<Styled.BreakoutScrollableList data-test="breakoutRoomList">
|
||||
{roomItems}
|
||||
</Styled.BreakoutScrollableList>
|
||||
</Styled.BreakoutColumn>
|
||||
@ -518,6 +519,7 @@ class BreakoutRoom extends PureComponent {
|
||||
|
||||
|
||||
<Styled.EndButton
|
||||
data-test="sendButtonDurationTime"
|
||||
color="primary"
|
||||
disabled={!isMeteorConnected}
|
||||
size="sm"
|
||||
|
@ -226,6 +226,9 @@ class MessageForm extends PureComponent {
|
||||
onChange={this.handleMessageChange}
|
||||
onKeyDown={this.handleMessageKeyDown}
|
||||
async
|
||||
onPaste={(e) => { e.stopPropagation(); }}
|
||||
onCut={(e) => { e.stopPropagation(); }}
|
||||
onCopy={(e) => { e.stopPropagation(); }}
|
||||
/>
|
||||
<Styled.SendButton
|
||||
hideLabel
|
||||
|
@ -67,6 +67,11 @@ const Content = styled.div`
|
||||
justify-content: space-between;
|
||||
font-size: ${fontSizeSmall};
|
||||
font-weight: bold;
|
||||
padding: ${borderSize} ${borderSize} ${borderSize} 0;
|
||||
|
||||
[dir="rtl"] & {
|
||||
padding: ${borderSize} 0 ${borderSize} ${borderSize};
|
||||
}
|
||||
`;
|
||||
|
||||
const BreakoutRoomListNameLabel = styled.span`
|
||||
|
@ -14,7 +14,7 @@ import Button from '/imports/ui/components/common/button/component';
|
||||
import Modal from '/imports/ui/components/common/modal/simple/component';
|
||||
|
||||
const WriterMenuModal = styled(Modal)`
|
||||
padding: 1.5rem;
|
||||
padding: 1rem;
|
||||
min-height: 20rem;
|
||||
`;
|
||||
|
||||
@ -51,7 +51,6 @@ const Content = styled.div`
|
||||
const StartBtn = styled(Button)`
|
||||
align-self: center;
|
||||
margin: 0;
|
||||
width: 40%;
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
|
@ -14,6 +14,7 @@ import { usePreviousValue } from '/imports/ui/components/utils/hooks';
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const PUBLIC_CHAT_CLEAR = CHAT_CONFIG.chat_clear;
|
||||
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
|
||||
const POLL_RESULT_KEY = CHAT_CONFIG.system_messages_keys.chat_poll_result;
|
||||
|
||||
const propTypes = {
|
||||
pushAlertEnabled: PropTypes.bool.isRequired,
|
||||
@ -56,6 +57,14 @@ const intlMessages = defineMessages({
|
||||
id: 'app.toast.chat.private',
|
||||
description: 'private chat toast message title',
|
||||
},
|
||||
pollResults: {
|
||||
id: 'app.toast.chat.poll',
|
||||
description: 'chat toast message for polls',
|
||||
},
|
||||
pollResultsClick: {
|
||||
id: 'app.toast.chat.pollClick',
|
||||
description: 'chat toast click message for polls',
|
||||
},
|
||||
});
|
||||
|
||||
const ALERT_INTERVAL = 5000; // 5 seconds
|
||||
@ -168,6 +177,13 @@ const ChatAlert = (props) => {
|
||||
</Styled.PushMessageContent>
|
||||
);
|
||||
|
||||
const createPollMessage = () => (
|
||||
<Styled.PushMessageContent>
|
||||
<Styled.UserNameMessage>{intl.formatMessage(intlMessages.pollResults)}</Styled.UserNameMessage>
|
||||
<Styled.ContentMessagePoll>{intl.formatMessage(intlMessages.pollResultsClick)}</Styled.ContentMessagePoll>
|
||||
</Styled.PushMessageContent>
|
||||
);
|
||||
|
||||
if (_.isEqual(prevUnreadMessages, unreadMessages)) {
|
||||
return null;
|
||||
}
|
||||
@ -175,9 +191,15 @@ const ChatAlert = (props) => {
|
||||
return pushAlertEnabled
|
||||
? unreadMessages.map((timeWindow) => {
|
||||
const mappedMessage = Service.mapGroupMessage(timeWindow);
|
||||
const content = mappedMessage
|
||||
? createMessage(mappedMessage.sender.name, mappedMessage.content.slice(-5))
|
||||
: null;
|
||||
|
||||
let content = null;
|
||||
if (mappedMessage) {
|
||||
if (mappedMessage.id.includes(POLL_RESULT_KEY)) {
|
||||
content = createPollMessage();
|
||||
} else {
|
||||
content = createMessage(mappedMessage.sender.name, mappedMessage.content.slice(-5));
|
||||
}
|
||||
}
|
||||
|
||||
const messageChatId = mappedMessage.chatId === 'MAIN-PUBLIC-GROUP-CHAT' ? PUBLIC_CHAT_ID : mappedMessage.chatId;
|
||||
|
||||
|
@ -38,8 +38,13 @@ const ContentMessage = styled.div`
|
||||
max-height: calc(${fontSizeSmall} * 10);
|
||||
`;
|
||||
|
||||
const ContentMessagePoll = styled(ContentMessage)`
|
||||
margin-top: ${fontSizeSmall};
|
||||
`;
|
||||
|
||||
export default {
|
||||
PushMessageContent,
|
||||
UserNameMessage,
|
||||
ContentMessage,
|
||||
ContentMessagePoll,
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user