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: CreateMeetingReqMsg => handleCreateMeetingReqMsg(m)
|
||||||
case m: RegisterUserReqMsg => handleRegisterUserReqMsg(m)
|
case m: RegisterUserReqMsg => handleRegisterUserReqMsg(m)
|
||||||
case m: EjectDuplicateUserReqMsg => handleEjectDuplicateUserReqMsg(m)
|
|
||||||
case m: GetAllMeetingsReqMsg => handleGetAllMeetingsReqMsg(m)
|
case m: GetAllMeetingsReqMsg => handleGetAllMeetingsReqMsg(m)
|
||||||
case m: GetRunningMeetingsReqMsg => handleGetRunningMeetingsReqMsg(m)
|
case m: GetRunningMeetingsReqMsg => handleGetRunningMeetingsReqMsg(m)
|
||||||
case m: CheckAlivePingSysMsg => handleCheckAlivePingSysMsg(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 = {
|
def handleCreateMeetingReqMsg(msg: CreateMeetingReqMsg): Unit = {
|
||||||
log.debug("RECEIVED CreateMeetingReqMsg msg {}", msg)
|
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
|
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
|
// DeskShare
|
||||||
case class DeskShareStartedRequest(conferenceName: String, callerId: String, callerIdName: String) extends InMessage
|
case class DeskShareStartedRequest(conferenceName: String, callerId: String, callerIdName: String) extends InMessage
|
||||||
case class DeskShareStoppedRequest(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,
|
isDefaultName: Boolean,
|
||||||
freeJoin: Boolean,
|
freeJoin: Boolean,
|
||||||
voiceConf: String,
|
voiceConf: String,
|
||||||
assignedUsers: Vector[String]
|
assignedUsers: Vector[String],
|
||||||
|
captureNotes: Boolean,
|
||||||
|
captureSlides: Boolean,
|
||||||
): BreakoutRoom2x = {
|
): 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
|
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.immutable.HashMap
|
||||||
import scala.collection.JavaConverters._
|
|
||||||
import org.bigbluebutton.common2.msgs.AnnotationVO
|
import org.bigbluebutton.common2.msgs.AnnotationVO
|
||||||
import org.bigbluebutton.core.apps.whiteboard.Whiteboard
|
import org.bigbluebutton.core.apps.whiteboard.Whiteboard
|
||||||
import org.bigbluebutton.SystemConfiguration
|
import org.bigbluebutton.SystemConfiguration
|
||||||
@ -24,86 +21,83 @@ class WhiteboardModel extends SystemConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private def createWhiteboard(wbId: String): Whiteboard = {
|
private def createWhiteboard(wbId: String): Whiteboard = {
|
||||||
new Whiteboard(
|
Whiteboard(
|
||||||
wbId,
|
wbId,
|
||||||
Array.empty[String],
|
Array.empty[String],
|
||||||
Array.empty[String],
|
Array.empty[String],
|
||||||
System.currentTimeMillis(),
|
System.currentTimeMillis(),
|
||||||
new HashMap[String, Map[String, AnnotationVO]]()
|
new HashMap[String, AnnotationVO]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private def getAnnotationsByUserId(wb: Whiteboard, id: String): Map[String, AnnotationVO] = {
|
private def deepMerge(test: Map[String, _], that: Map[String, _]): Map[String, _] =
|
||||||
wb.annotationsMap.get(id).getOrElse(Map[String, AnnotationVO]())
|
(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 wb = getWhiteboard(wbId)
|
||||||
val usersAnnotations = getAnnotationsByUserId(wb, userId)
|
var newAnnotationsMap = wb.annotationsMap
|
||||||
var newUserAnnotations = usersAnnotations
|
|
||||||
for (annotation <- annotations) {
|
for (annotation <- annotations) {
|
||||||
newUserAnnotations = newUserAnnotations + (annotation.id -> annotation)
|
val oldAnnotation = wb.annotationsMap.get(annotation.id)
|
||||||
println("Adding annotation to page [" + wb.id + "]. After numAnnotations=[" + newUserAnnotations.size + "].")
|
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)
|
val newWb = wb.copy(annotationsMap = newAnnotationsMap)
|
||||||
saveWhiteboard(newWb)
|
saveWhiteboard(newWb)
|
||||||
annotations
|
annotationsAdded
|
||||||
}
|
}
|
||||||
|
|
||||||
def getHistory(wbId: String): Array[AnnotationVO] = {
|
def getHistory(wbId: String): Array[AnnotationVO] = {
|
||||||
//wb.annotationsMap.values.flatten.toArray.sortBy(_.position);
|
|
||||||
val wb = getWhiteboard(wbId)
|
val wb = getWhiteboard(wbId)
|
||||||
var annotations = Array[AnnotationVO]()
|
wb.annotationsMap.values.toArray
|
||||||
// TODO: revisit this, probably there is a one-liner simple solution
|
|
||||||
wb.annotationsMap.values.foreach(
|
|
||||||
user => user.values.foreach(
|
|
||||||
annotation => annotations = annotations :+ annotation
|
|
||||||
)
|
|
||||||
)
|
|
||||||
annotations
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def clearWhiteboard(wbId: String, userId: String): Option[Boolean] = {
|
def deleteAnnotations(wbId: String, userId: String, annotationsIds: Array[String], isPresenter: Boolean, isModerator: Boolean): Array[String] = {
|
||||||
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] = {
|
|
||||||
var annotationsIdsRemoved = Array[String]()
|
var annotationsIdsRemoved = Array[String]()
|
||||||
val wb = getWhiteboard(wbId)
|
val wb = getWhiteboard(wbId)
|
||||||
|
var newAnnotationsMap = wb.annotationsMap
|
||||||
|
|
||||||
val usersAnnotations = getAnnotationsByUserId(wb, userId)
|
|
||||||
var newUserAnnotations = usersAnnotations
|
|
||||||
for (annotationId <- annotationsIds) {
|
for (annotationId <- annotationsIds) {
|
||||||
val annotation = usersAnnotations.get(annotationId)
|
val annotation = wb.annotationsMap.get(annotationId)
|
||||||
|
|
||||||
//not empty and annotation exists
|
if (!annotation.isEmpty) {
|
||||||
if (!usersAnnotations.isEmpty && !annotation.isEmpty) {
|
val hasPermission = isPresenter || isModerator || annotation.get.userId == userId
|
||||||
newUserAnnotations = newUserAnnotations - annotationId
|
if (hasPermission) {
|
||||||
println("Removing annotation on page [" + wb.id + "]. After numAnnotations=[" + newUserAnnotations.size + "].")
|
newAnnotationsMap -= annotationId
|
||||||
annotationsIdsRemoved = annotationsIdsRemoved :+ 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)
|
val newWb = wb.copy(annotationsMap = newAnnotationsMap)
|
||||||
saveWhiteboard(newWb)
|
saveWhiteboard(newWb)
|
||||||
annotationsIdsRemoved
|
annotationsIdsRemoved
|
||||||
|
@ -52,7 +52,7 @@ trait BreakoutRoomCreatedMsgHdlr {
|
|||||||
(redirectToHtml5JoinURL, redirectJoinURL) <- BreakoutHdlrHelpers.getRedirectUrls(liveMeeting, user, r.externalId, r.sequence.toString())
|
(redirectToHtml5JoinURL, redirectJoinURL) <- BreakoutHdlrHelpers.getRedirectUrls(liveMeeting, user, r.externalId, r.sequence.toString())
|
||||||
} yield (user -> redirectToHtml5JoinURL)
|
} 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)
|
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)
|
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)
|
val event = build(liveMeeting.props.meetingProp.intId, breakoutInfo)
|
||||||
outGW.send(event)
|
outGW.send(event)
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ trait BreakoutRoomsListMsgHdlr {
|
|||||||
breakoutModel <- state.breakout
|
breakoutModel <- state.breakout
|
||||||
} yield {
|
} yield {
|
||||||
val rooms = breakoutModel.rooms.values.toVector map { r =>
|
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()
|
val ready = breakoutModel.hasAllStarted()
|
||||||
broadcastEvent(rooms, ready)
|
broadcastEvent(rooms, ready)
|
||||||
|
@ -52,7 +52,7 @@ trait CreateBreakoutRoomsCmdMsgHdlr extends RightsManagementTrait {
|
|||||||
val (internalId, externalId) = BreakoutRoomsUtil.createMeetingIds(liveMeeting.props.meetingProp.intId, i)
|
val (internalId, externalId) = BreakoutRoomsUtil.createMeetingIds(liveMeeting.props.meetingProp.intId, i)
|
||||||
val voiceConf = BreakoutRoomsUtil.createVoiceConfId(liveMeeting.props.voiceProp.voiceConf, 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)
|
rooms = rooms + (breakout.id -> breakout)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,7 +70,9 @@ trait CreateBreakoutRoomsCmdMsgHdlr extends RightsManagementTrait {
|
|||||||
liveMeeting.props.password.moderatorPass,
|
liveMeeting.props.password.moderatorPass,
|
||||||
liveMeeting.props.password.viewerPass,
|
liveMeeting.props.password.viewerPass,
|
||||||
presId, presSlide, msg.body.record,
|
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)
|
val event = buildCreateBreakoutRoomSysCmdMsg(liveMeeting.props.meetingProp.intId, roomDetail)
|
||||||
|
@ -14,8 +14,8 @@ trait EndAllBreakoutRoomsMsgHdlr extends RightsManagementTrait {
|
|||||||
val outGW: OutMsgRouter
|
val outGW: OutMsgRouter
|
||||||
|
|
||||||
def handleEndAllBreakoutRoomsMsg(msg: EndAllBreakoutRoomsMsg, state: MeetingState2x): MeetingState2x = {
|
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)) {
|
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."
|
val reason = "No permission to end breakout rooms for meeting."
|
||||||
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)
|
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)
|
||||||
state
|
state
|
||||||
@ -24,11 +24,11 @@ trait EndAllBreakoutRoomsMsgHdlr extends RightsManagementTrait {
|
|||||||
model <- state.breakout
|
model <- state.breakout
|
||||||
} yield {
|
} yield {
|
||||||
model.rooms.values.foreach { room =>
|
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(
|
val notifyEvent = MsgBuilder.buildNotifyAllInMeetingEvtMsg(
|
||||||
liveMeeting.props.meetingProp.intId,
|
meetingId,
|
||||||
"info",
|
"info",
|
||||||
"rooms",
|
"rooms",
|
||||||
"app.toast.breakoutRoomEnded",
|
"app.toast.breakoutRoomEnded",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package org.bigbluebutton.core.apps.breakout
|
package org.bigbluebutton.core.apps.breakout
|
||||||
|
|
||||||
import org.bigbluebutton.core.api.EndBreakoutRoomInternalMsg
|
import org.bigbluebutton.core.api.{ CaptureSharedNotesReqInternalMsg, CapturePresentationReqInternalMsg, EndBreakoutRoomInternalMsg }
|
||||||
import org.bigbluebutton.core.bus.{ InternalEventBus }
|
import org.bigbluebutton.core.bus.{ BigBlueButtonEvent, InternalEventBus }
|
||||||
import org.bigbluebutton.core.running.{ BaseMeetingActor, HandlerHelpers, LiveMeeting, OutMsgRouter }
|
import org.bigbluebutton.core.running.{ BaseMeetingActor, HandlerHelpers, LiveMeeting, OutMsgRouter }
|
||||||
|
|
||||||
trait EndBreakoutRoomInternalMsgHdlr extends HandlerHelpers {
|
trait EndBreakoutRoomInternalMsgHdlr extends HandlerHelpers {
|
||||||
@ -12,6 +12,18 @@ trait EndBreakoutRoomInternalMsgHdlr extends HandlerHelpers {
|
|||||||
val eventBus: InternalEventBus
|
val eventBus: InternalEventBus
|
||||||
|
|
||||||
def handleEndBreakoutRoomInternalMsg(msg: EndBreakoutRoomInternalMsg): Unit = {
|
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)
|
log.info("Breakout room {} ended by parent meeting {}.", msg.breakoutId, msg.parentId)
|
||||||
sendEndMeetingDueToExpiry(msg.reason, eventBus, outGW, liveMeeting, "system")
|
sendEndMeetingDueToExpiry(msg.reason, eventBus, outGW, liveMeeting, "system")
|
||||||
}
|
}
|
||||||
|
@ -84,10 +84,12 @@ trait CreateGroupChatReqMsgHdlr extends SystemConfiguration {
|
|||||||
BbbCoreEnvelope(name, routing)
|
BbbCoreEnvelope(name, routing)
|
||||||
}
|
}
|
||||||
|
|
||||||
def makeBody(chatId: String,
|
def makeBody(
|
||||||
access: String, correlationId: String,
|
chatId: String,
|
||||||
createdBy: GroupChatUser, users: Vector[GroupChatUser],
|
access: String, correlationId: String,
|
||||||
msgs: Vector[GroupChatMsgToUser]): GroupChatCreatedEvtMsgBody = {
|
createdBy: GroupChatUser, users: Vector[GroupChatUser],
|
||||||
|
msgs: Vector[GroupChatMsgToUser]
|
||||||
|
): GroupChatCreatedEvtMsgBody = {
|
||||||
GroupChatCreatedEvtMsgBody(correlationId, chatId, createdBy,
|
GroupChatCreatedEvtMsgBody(correlationId, chatId, createdBy,
|
||||||
access, users, msgs)
|
access, users, msgs)
|
||||||
}
|
}
|
||||||
|
@ -45,27 +45,31 @@ trait RespondToPollReqMsgHdlr {
|
|||||||
bus.outGW.send(msgEvent)
|
bus.outGW.send(msgEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
if (Polls.checkUserResponded(msg.body.pollId, msg.header.userId, liveMeeting.polls) == false) {
|
||||||
(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 {
|
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 {
|
} yield {
|
||||||
|
broadcastPollUpdatedEvent(msg, pollId, updatedPoll)
|
||||||
for {
|
for {
|
||||||
answerId <- msg.body.answerIds
|
poll <- Polls.getPoll(pollId, liveMeeting.polls)
|
||||||
} yield {
|
} yield {
|
||||||
val answerText = poll.questions(0).answers.get(answerId).key
|
for {
|
||||||
broadcastUserRespondedToPollRecordMsg(msg, pollId, answerId, answerText, poll.isSecret)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
for {
|
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)
|
||||||
presenter <- Users2x.findPresenter(liveMeeting.users2x)
|
|
||||||
} yield {
|
|
||||||
broadcastUserRespondedToPollRespMsg(msg, pollId, msg.body.answerIds, presenter.intId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,17 +34,23 @@ trait RespondToTypedPollReqMsgHdlr {
|
|||||||
bus.outGW.send(msgEvent)
|
bus.outGW.send(msgEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
if (Polls.isResponsePollType(msg.body.pollId, liveMeeting.polls) &&
|
||||||
(pollId: String, updatedPoll: SimplePollResultOutVO) <- Polls.handleRespondToTypedPollReqMsg(msg.header.userId, msg.body.pollId,
|
Polls.checkUserResponded(msg.body.pollId, msg.header.userId, liveMeeting.polls) == false &&
|
||||||
msg.body.questionId, msg.body.answer, liveMeeting)
|
Polls.checkUserAddedQuestion(msg.body.pollId, msg.header.userId, liveMeeting.polls) == false) {
|
||||||
} yield {
|
|
||||||
broadcastPollUpdatedEvent(msg, pollId, updatedPoll)
|
|
||||||
|
|
||||||
for {
|
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 {
|
} 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
|
package org.bigbluebutton.core.apps.presentationpod
|
||||||
|
|
||||||
import org.bigbluebutton.common2.msgs._
|
import org.bigbluebutton.common2.msgs._
|
||||||
|
import org.bigbluebutton.core.api.{ CapturePresentationReqInternalMsg, CaptureSharedNotesReqInternalMsg }
|
||||||
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
|
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
|
||||||
import org.bigbluebutton.core.bus.MessageBus
|
import org.bigbluebutton.core.bus.MessageBus
|
||||||
import org.bigbluebutton.core.domain.MeetingState2x
|
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 meetingId = liveMeeting.props.meetingProp.intId
|
||||||
val userId = m.header.userId
|
val userId = m.userId
|
||||||
|
|
||||||
val presentationPods: Vector[PresentationPod] = state.presentationPodManager.getAllPresentationPodsInMeeting()
|
val presentationPods: Vector[PresentationPod] = state.presentationPodManager.getAllPresentationPodsInMeeting()
|
||||||
val currentPres: Option[PresentationInPod] = presentationPods.flatMap(_.getCurrentPresentation()).headOption
|
val currentPres: Option[PresentationInPod] = presentationPods.flatMap(_.getCurrentPresentation()).headOption
|
||||||
|
|
||||||
if (liveMeeting.props.meetingProp.disabledFeatures.contains("importPresentationWithAnnotationsFromBreakoutRooms")) {
|
if (liveMeeting.props.meetingProp.disabledFeatures.contains("importPresentationWithAnnotationsFromBreakoutRooms")) {
|
||||||
val reason = "Importing slides from breakout rooms disabled for this meeting."
|
log.error(s"Capturing breakout rooms slides disabled in meeting ${meetingId}.")
|
||||||
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)
|
|
||||||
} else if (currentPres.isEmpty) {
|
} else if (currentPres.isEmpty) {
|
||||||
log.error(s"No presentation set in meeting ${meetingId}")
|
log.error(s"No presentation set in meeting ${meetingId}")
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
val jobId: String = RandomStringGenerator.randomAlphanumericString(16);
|
val jobId: String = RandomStringGenerator.randomAlphanumericString(16);
|
||||||
val jobType = "PresentationWithAnnotationExportJob"
|
val jobType = "PresentationWithAnnotationExportJob"
|
||||||
val allPages: Boolean = m.body.allPages
|
val allPages: Boolean = m.allPages
|
||||||
val pageCount = currentPres.get.pages.size
|
val pageCount = currentPres.get.pages.size
|
||||||
|
|
||||||
val presId: String = PresentationPodsApp.getAllPresentationPodsInMeeting(state).flatMap(_.getCurrentPresentation.map(_.id)).mkString
|
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 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 currentPage: PresentationPage = PresentationInPod.getCurrentPage(currentPres.get).get
|
||||||
val pagesRange: List[Int] = if (allPages) (1 to pageCount).toList else List(currentPage.num)
|
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)
|
log.info("Received NewPresAnnFileAvailableMsg meetingId={} presId={} fileUrl={}", liveMeeting.props.meetingProp.intId, m.body.presId, m.body.fileURI)
|
||||||
|
|
||||||
bus.outGW.send(buildBroadcastNewPresAnnFileAvailable(m, liveMeeting))
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.common2.msgs._
|
||||||
import org.bigbluebutton.core.models._
|
import org.bigbluebutton.core.models._
|
||||||
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
|
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
|
||||||
import org.bigbluebutton.core2.message.senders.MsgBuilder
|
import org.bigbluebutton.core2.message.senders.{ MsgBuilder, Sender }
|
||||||
|
|
||||||
trait RegisterUserReqMsgHdlr {
|
trait RegisterUserReqMsgHdlr {
|
||||||
this: UsersApp =>
|
this: UsersApp =>
|
||||||
@ -22,12 +22,44 @@ trait RegisterUserReqMsgHdlr {
|
|||||||
val event = UserRegisteredRespMsg(header, body)
|
val event = UserRegisteredRespMsg(header, body)
|
||||||
BbbCommonEnvCoreMsg(envelope, event)
|
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 guestStatus = msg.body.guestStatus
|
||||||
|
|
||||||
val regUser = RegisteredUsers.create(msg.body.intUserId, msg.body.extUserId,
|
val regUser = RegisteredUsers.create(msg.body.intUserId, msg.body.extUserId,
|
||||||
msg.body.name, msg.body.role, msg.body.authToken,
|
msg.body.name, msg.body.role, msg.body.authToken,
|
||||||
msg.body.avatarURL, msg.body.guest, msg.body.authed, guestStatus, msg.body.excludeFromDashboard, false)
|
msg.body.avatarURL, msg.body.guest, msg.body.authed, guestStatus, msg.body.excludeFromDashboard, false)
|
||||||
|
|
||||||
|
checkUserConcurrentAccesses(regUser)
|
||||||
|
|
||||||
RegisteredUsers.add(liveMeeting.registeredUsers, regUser)
|
RegisteredUsers.add(liveMeeting.registeredUsers, regUser)
|
||||||
|
|
||||||
log.info("Register user success. meetingId=" + liveMeeting.props.meetingProp.intId
|
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.common2.msgs.UserJoinMeetingReqMsg
|
||||||
import org.bigbluebutton.core.apps.breakout.BreakoutHdlrHelpers
|
import org.bigbluebutton.core.apps.breakout.BreakoutHdlrHelpers
|
||||||
import org.bigbluebutton.core.domain.MeetingState2x
|
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 }
|
import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting, MeetingActor, OutMsgRouter }
|
||||||
|
|
||||||
trait UserJoinMeetingReqMsgHdlr extends HandlerHelpers {
|
trait UserJoinMeetingReqMsgHdlr extends HandlerHelpers {
|
||||||
@ -26,16 +26,31 @@ trait UserJoinMeetingReqMsgHdlr extends HandlerHelpers {
|
|||||||
|
|
||||||
state
|
state
|
||||||
case None =>
|
case None =>
|
||||||
val newState = userJoinMeeting(outGW, msg.body.authToken, msg.body.clientType, liveMeeting, state)
|
// Check if maxParticipants has been reached
|
||||||
|
// User are able to reenter if he already joined previously with the same extId
|
||||||
if (liveMeeting.props.meetingProp.isBreakout) {
|
val userHasJoinedAlready = RegisteredUsers.findWithUserId(msg.body.userId, liveMeeting.registeredUsers) match {
|
||||||
BreakoutHdlrHelpers.updateParentMeetingWithUsers(liveMeeting, eventBus)
|
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
|
if (!hasReachedMaxParticipants) {
|
||||||
VoiceUsers.recoverVoiceUser(liveMeeting.voiceUsers, msg.body.userId)
|
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 SelectRandomViewerReqMsgHdlr
|
||||||
with AssignPresenterReqMsgHdlr
|
with AssignPresenterReqMsgHdlr
|
||||||
with ChangeUserPinStateReqMsgHdlr
|
with ChangeUserPinStateReqMsgHdlr
|
||||||
with EjectDuplicateUserReqMsgHdlr
|
|
||||||
with EjectUserFromMeetingCmdMsgHdlr
|
with EjectUserFromMeetingCmdMsgHdlr
|
||||||
with EjectUserFromMeetingSysMsgHdlr
|
with EjectUserFromMeetingSysMsgHdlr
|
||||||
with MuteUserCmdMsgHdlr {
|
with MuteUserCmdMsgHdlr {
|
||||||
|
@ -5,7 +5,7 @@ import org.bigbluebutton.core.bus.InternalEventBus
|
|||||||
import org.bigbluebutton.core.domain.MeetingState2x
|
import org.bigbluebutton.core.domain.MeetingState2x
|
||||||
import org.bigbluebutton.core.models._
|
import org.bigbluebutton.core.models._
|
||||||
import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting, OutMsgRouter }
|
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 {
|
trait ValidateAuthTokenReqMsgHdlr extends HandlerHelpers {
|
||||||
this: UsersApp =>
|
this: UsersApp =>
|
||||||
@ -24,10 +24,16 @@ trait ValidateAuthTokenReqMsgHdlr extends HandlerHelpers {
|
|||||||
liveMeeting.registeredUsers)
|
liveMeeting.registeredUsers)
|
||||||
regUser match {
|
regUser match {
|
||||||
case Some(u) =>
|
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.
|
// Check if banned user is rejoining.
|
||||||
// Fail validation if ejected user is rejoining.
|
// Fail validation if ejected user is rejoining.
|
||||||
// ralam april 21, 2020
|
// 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)
|
userValidated(u, state)
|
||||||
} else {
|
} else {
|
||||||
if (u.banned) {
|
if (u.banned) {
|
||||||
@ -36,6 +42,9 @@ trait ValidateAuthTokenReqMsgHdlr extends HandlerHelpers {
|
|||||||
} else if (u.loggedOut) {
|
} else if (u.loggedOut) {
|
||||||
failReason = "User had logged out"
|
failReason = "User had logged out"
|
||||||
failReasonCode = EjectReasonCode.USER_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(
|
validateTokenFailed(
|
||||||
outGW,
|
outGW,
|
||||||
|
@ -28,11 +28,7 @@ trait ClearWhiteboardPubMsgHdlr extends RightsManagementTrait {
|
|||||||
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
|
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for {
|
log.error("Ignoring message ClearWhiteboardPubMsg since this functions is not available in the new Whiteboard")
|
||||||
fullClear <- clearWhiteboard(msg.body.whiteboardId, msg.header.userId, liveMeeting)
|
|
||||||
} yield {
|
|
||||||
broadcastEvent(msg, fullClear)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,14 +21,24 @@ trait DeleteWhiteboardAnnotationsPubMsgHdlr extends RightsManagementTrait {
|
|||||||
bus.outGW.send(msgEvent)
|
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)) {
|
if (isNonEjectionGracePeriodOver(msg.body.whiteboardId, msg.header.userId, liveMeeting)) {
|
||||||
val meetingId = liveMeeting.props.meetingProp.intId
|
val meetingId = liveMeeting.props.meetingProp.intId
|
||||||
val reason = "No permission to delete an annotation."
|
val reason = "No permission to delete an annotation."
|
||||||
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
|
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
|
||||||
}
|
}
|
||||||
} else {
|
} 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) {
|
if (!deletedAnnotations.isEmpty) {
|
||||||
broadcastEvent(msg, deletedAnnotations)
|
broadcastEvent(msg, deletedAnnotations)
|
||||||
}
|
}
|
||||||
|
@ -46,13 +46,18 @@ trait SendWhiteboardAnnotationsPubMsgHdlr extends RightsManagementTrait {
|
|||||||
PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId
|
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) {
|
if (isUserOneOfPermited || isUserAmongPresenters) {
|
||||||
println("============= Printing Sanitized annotations ============")
|
println("============= Printing Sanitized annotations ============")
|
||||||
for (annotation <- msg.body.annotations) {
|
for (annotation <- msg.body.annotations) {
|
||||||
printAnnotationInfo(annotation)
|
printAnnotationInfo(annotation)
|
||||||
}
|
}
|
||||||
println("============= Printed Sanitized annotations ============")
|
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)
|
broadcastEvent(msg, msg.body.whiteboardId, annotations, msg.body.html5InstanceId)
|
||||||
} else {
|
} else {
|
||||||
//val meetingId = liveMeeting.props.meetingProp.intId
|
//val meetingId = liveMeeting.props.meetingProp.intId
|
||||||
|
@ -11,7 +11,7 @@ case class Whiteboard(
|
|||||||
multiUser: Array[String],
|
multiUser: Array[String],
|
||||||
oldMultiUser: Array[String],
|
oldMultiUser: Array[String],
|
||||||
changedModeOn: Long,
|
changedModeOn: Long,
|
||||||
annotationsMap: Map[String, Map[String, AnnotationVO]]
|
annotationsMap: Map[String, AnnotationVO]
|
||||||
)
|
)
|
||||||
|
|
||||||
class WhiteboardApp2x(implicit val context: ActorContext)
|
class WhiteboardApp2x(implicit val context: ActorContext)
|
||||||
@ -24,9 +24,16 @@ class WhiteboardApp2x(implicit val context: ActorContext)
|
|||||||
|
|
||||||
val log = Logging(context.system, getClass)
|
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 + "]")
|
// 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] = {
|
def getWhiteboardAnnotations(whiteboardId: String, liveMeeting: LiveMeeting): Array[AnnotationVO] = {
|
||||||
@ -34,12 +41,15 @@ class WhiteboardApp2x(implicit val context: ActorContext)
|
|||||||
liveMeeting.wbModel.getHistory(whiteboardId)
|
liveMeeting.wbModel.getHistory(whiteboardId)
|
||||||
}
|
}
|
||||||
|
|
||||||
def clearWhiteboard(whiteboardId: String, requesterId: String, liveMeeting: LiveMeeting): Option[Boolean] = {
|
def deleteWhiteboardAnnotations(
|
||||||
liveMeeting.wbModel.clearWhiteboard(whiteboardId, requesterId)
|
whiteboardId: String,
|
||||||
}
|
requesterId: String,
|
||||||
|
annotationsIds: Array[String],
|
||||||
def deleteWhiteboardAnnotations(whiteboardId: String, requesterId: String, annotationsIds: Array[String], liveMeeting: LiveMeeting): Array[String] = {
|
liveMeeting: LiveMeeting,
|
||||||
liveMeeting.wbModel.deleteAnnotations(whiteboardId, requesterId, annotationsIds)
|
isPresenter: Boolean,
|
||||||
|
isModerator: Boolean
|
||||||
|
): Array[String] = {
|
||||||
|
liveMeeting.wbModel.deleteAnnotations(whiteboardId, requesterId, annotationsIds, isPresenter, isModerator)
|
||||||
}
|
}
|
||||||
|
|
||||||
def getWhiteboardAccess(whiteboardId: String, liveMeeting: LiveMeeting): Array[String] = {
|
def getWhiteboardAccess(whiteboardId: String, liveMeeting: LiveMeeting): Array[String] = {
|
||||||
|
@ -14,7 +14,9 @@ case class BreakoutRoom2x(
|
|||||||
users: Vector[BreakoutUser],
|
users: Vector[BreakoutUser],
|
||||||
voiceUsers: Vector[BreakoutVoiceUser],
|
voiceUsers: Vector[BreakoutVoiceUser],
|
||||||
startedOn: Option[Long],
|
startedOn: Option[Long],
|
||||||
started: Boolean
|
started: Boolean,
|
||||||
|
captureNotes: Boolean,
|
||||||
|
captureSlides: Boolean,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -10,8 +10,8 @@ object BreakoutRooms {
|
|||||||
def breakoutRoomsdurationInMinutes(status: BreakoutRooms, duration: Int) = status.breakoutRoomsdurationInMinutes = duration
|
def breakoutRoomsdurationInMinutes(status: BreakoutRooms, duration: Int) = status.breakoutRoomsdurationInMinutes = duration
|
||||||
|
|
||||||
def newBreakoutRoom(parentRoomId: String, id: String, externalMeetingId: String, name: String, sequence: Integer, freeJoin: Boolean,
|
def newBreakoutRoom(parentRoomId: String, id: String, externalMeetingId: String, name: String, sequence: Integer, freeJoin: Boolean,
|
||||||
voiceConfId: String, assignedUsers: Vector[String], breakoutRooms: BreakoutRooms): Option[BreakoutRoomVO] = {
|
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())
|
val brvo = new BreakoutRoomVO(id, externalMeetingId, name, parentRoomId, sequence, freeJoin, voiceConfId, assignedUsers, Vector(), captureNotes, captureSlides)
|
||||||
breakoutRooms.add(brvo)
|
breakoutRooms.add(brvo)
|
||||||
Some(brvo)
|
Some(brvo)
|
||||||
}
|
}
|
||||||
|
@ -112,7 +112,7 @@ object Polls {
|
|||||||
shape = pollResultToWhiteboardShape(result)
|
shape = pollResultToWhiteboardShape(result)
|
||||||
annot <- send(result, shape)
|
annot <- send(result, shape)
|
||||||
} yield {
|
} 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)
|
showPollResult(pollId, lm.polls)
|
||||||
(result, annot)
|
(result, annot)
|
||||||
}
|
}
|
||||||
@ -238,7 +238,7 @@ object Polls {
|
|||||||
private def handleRespondToTypedPoll(poll: SimplePollResultOutVO, requesterId: String, pollId: String, questionId: Int,
|
private def handleRespondToTypedPoll(poll: SimplePollResultOutVO, requesterId: String, pollId: String, questionId: Int,
|
||||||
answer: String, lm: LiveMeeting): Option[SimplePollResultOutVO] = {
|
answer: String, lm: LiveMeeting): Option[SimplePollResultOutVO] = {
|
||||||
|
|
||||||
addQuestionResponse(poll.id, questionId, answer, lm.polls)
|
addQuestionResponse(poll.id, questionId, answer, requesterId, lm.polls)
|
||||||
for {
|
for {
|
||||||
updatedPoll <- getSimplePollResult(poll.id, lm.polls)
|
updatedPoll <- getSimplePollResult(poll.id, lm.polls)
|
||||||
} yield {
|
} yield {
|
||||||
@ -355,6 +355,45 @@ object Polls {
|
|||||||
pvo
|
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) {
|
def showPollResult(pollId: String, polls: Polls) {
|
||||||
polls.get(pollId) foreach {
|
polls.get(pollId) foreach {
|
||||||
p =>
|
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 {
|
polls.polls.get(pollId) match {
|
||||||
case Some(p) => {
|
case Some(p) => {
|
||||||
p.addQuestionResponse(questionID, answer)
|
if (!p.getTypedPollResponders().contains(requesterId)) {
|
||||||
|
p.addTypedPollResponder(requesterId)
|
||||||
|
p.addQuestionResponse(questionID, answer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case None =>
|
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 _showResult: Boolean = false
|
||||||
private var _numResponders: Int = 0
|
private var _numResponders: Int = 0
|
||||||
private var _responders = new ArrayBuffer[Responder]()
|
private var _responders = new ArrayBuffer[Responder]()
|
||||||
|
private var _respondersTypedPoll = new ArrayBuffer[String]()
|
||||||
|
|
||||||
def showingResult() { _showResult = true }
|
def showingResult() { _showResult = true }
|
||||||
def showResult(): Boolean = { _showResult }
|
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 addResponder(responder: Responder) { _responders += (responder) }
|
||||||
def getResponders(): ArrayBuffer[Responder] = { return _responders }
|
def getResponders(): ArrayBuffer[Responder] = { return _responders }
|
||||||
|
def addTypedPollResponder(responderId: String) { _respondersTypedPoll += (responderId) }
|
||||||
|
def getTypedPollResponders(): ArrayBuffer[String] = { return _respondersTypedPoll }
|
||||||
|
|
||||||
def hasResponses(): Boolean = {
|
def hasResponses(): Boolean = {
|
||||||
questions.foreach(q => {
|
questions.foreach(q => {
|
||||||
|
@ -64,6 +64,14 @@ object RegisteredUsers {
|
|||||||
} yield user
|
} 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] = {
|
def add(users: RegisteredUsers, user: RegisteredUser): Vector[RegisteredUser] = {
|
||||||
|
|
||||||
findWithExternUserId(user.externId, users) match {
|
findWithExternUserId(user.externId, users) match {
|
||||||
|
@ -407,4 +407,5 @@ object EjectReasonCode {
|
|||||||
val USER_INACTIVITY = "user_inactivity_eject_reason"
|
val USER_INACTIVITY = "user_inactivity_eject_reason"
|
||||||
val BANNED_USER_REJOINING = "banned_user_rejoining_reason"
|
val BANNED_USER_REJOINING = "banned_user_rejoining_reason"
|
||||||
val USER_LOGGED_OUT = "user_logged_out_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
|
// 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.
|
// because the meeting actor might not have been created yet.
|
||||||
route[RegisterUserReqMsg](meetingManagerChannel, envelope, jsonNode)
|
route[RegisterUserReqMsg](meetingManagerChannel, envelope, jsonNode)
|
||||||
case EjectDuplicateUserReqMsg.NAME =>
|
|
||||||
route[EjectDuplicateUserReqMsg](meetingManagerChannel, envelope, jsonNode)
|
|
||||||
case UserJoinMeetingReqMsg.NAME =>
|
case UserJoinMeetingReqMsg.NAME =>
|
||||||
routeGenericMsg[UserJoinMeetingReqMsg](envelope, jsonNode)
|
routeGenericMsg[UserJoinMeetingReqMsg](envelope, jsonNode)
|
||||||
case UserJoinMeetingAfterReconnectReqMsg.NAME =>
|
case UserJoinMeetingAfterReconnectReqMsg.NAME =>
|
||||||
@ -175,6 +173,8 @@ class ReceivedJsonMsgHandlerActor(
|
|||||||
routePadMsg[PadPatchSysMsg](envelope, jsonNode)
|
routePadMsg[PadPatchSysMsg](envelope, jsonNode)
|
||||||
case PadUpdatePubMsg.NAME =>
|
case PadUpdatePubMsg.NAME =>
|
||||||
routeGenericMsg[PadUpdatePubMsg](envelope, jsonNode)
|
routeGenericMsg[PadUpdatePubMsg](envelope, jsonNode)
|
||||||
|
case PadCapturePubMsg.NAME =>
|
||||||
|
routePadMsg[PadCapturePubMsg](envelope, jsonNode)
|
||||||
|
|
||||||
// Voice
|
// Voice
|
||||||
case RecordingStartedVoiceConfEvtMsg.NAME =>
|
case RecordingStartedVoiceConfEvtMsg.NAME =>
|
||||||
@ -310,8 +310,6 @@ class ReceivedJsonMsgHandlerActor(
|
|||||||
routeGenericMsg[AssignPresenterReqMsg](envelope, jsonNode)
|
routeGenericMsg[AssignPresenterReqMsg](envelope, jsonNode)
|
||||||
case MakePresentationWithAnnotationDownloadReqMsg.NAME =>
|
case MakePresentationWithAnnotationDownloadReqMsg.NAME =>
|
||||||
routeGenericMsg[MakePresentationWithAnnotationDownloadReqMsg](envelope, jsonNode)
|
routeGenericMsg[MakePresentationWithAnnotationDownloadReqMsg](envelope, jsonNode)
|
||||||
case ExportPresentationWithAnnotationReqMsg.NAME =>
|
|
||||||
routeGenericMsg[ExportPresentationWithAnnotationReqMsg](envelope, jsonNode)
|
|
||||||
case NewPresAnnFileAvailableMsg.NAME =>
|
case NewPresAnnFileAvailableMsg.NAME =>
|
||||||
routeGenericMsg[NewPresAnnFileAvailableMsg](envelope, jsonNode)
|
routeGenericMsg[NewPresAnnFileAvailableMsg](envelope, jsonNode)
|
||||||
|
|
||||||
|
@ -226,7 +226,7 @@ trait HandlerHelpers extends SystemConfiguration {
|
|||||||
model <- state.breakout
|
model <- state.breakout
|
||||||
} yield {
|
} yield {
|
||||||
model.rooms.values.foreach { room =>
|
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
|
// Handling RegisterUserReqMsg as it is forwarded from BBBActor and
|
||||||
// its type is not BbbCommonEnvCoreMsg
|
// its type is not BbbCommonEnvCoreMsg
|
||||||
case m: RegisterUserReqMsg => usersApp.handleRegisterUserReqMsg(m)
|
case m: RegisterUserReqMsg => usersApp.handleRegisterUserReqMsg(m)
|
||||||
case m: EjectDuplicateUserReqMsg => usersApp.handleEjectDuplicateUserReqMsg(m)
|
|
||||||
case m: GetAllMeetingsReqMsg => handleGetAllMeetingsReqMsg(m)
|
case m: GetAllMeetingsReqMsg => handleGetAllMeetingsReqMsg(m)
|
||||||
case m: GetRunningMeetingStateReqMsg => handleGetRunningMeetingStateReqMsg(m)
|
case m: GetRunningMeetingStateReqMsg => handleGetRunningMeetingStateReqMsg(m)
|
||||||
case m: ValidateConnAuthTokenSysMsg => handleValidateConnAuthTokenSysMsg(m)
|
case m: ValidateConnAuthTokenSysMsg => handleValidateConnAuthTokenSysMsg(m)
|
||||||
@ -283,7 +282,8 @@ class MeetingActor(
|
|||||||
case msg: SendMessageToBreakoutRoomInternalMsg => state = handleSendMessageToBreakoutRoomInternalMsg(msg, state, liveMeeting, msgBus)
|
case msg: SendMessageToBreakoutRoomInternalMsg => state = handleSendMessageToBreakoutRoomInternalMsg(msg, state, liveMeeting, msgBus)
|
||||||
case msg: SendBreakoutTimeRemainingInternalMsg =>
|
case msg: SendBreakoutTimeRemainingInternalMsg =>
|
||||||
handleSendBreakoutTimeRemainingInternalMsg(msg)
|
handleSendBreakoutTimeRemainingInternalMsg(msg)
|
||||||
|
case msg: CapturePresentationReqInternalMsg => presentationPodsApp.handle(msg, state, liveMeeting, msgBus)
|
||||||
|
case msg: CaptureSharedNotesReqInternalMsg => presentationPodsApp.handle(msg, liveMeeting, msgBus)
|
||||||
case msg: SendRecordingTimerInternalMsg =>
|
case msg: SendRecordingTimerInternalMsg =>
|
||||||
state = usersApp.handleSendRecordingTimerInternalMsg(msg, state)
|
state = usersApp.handleSendRecordingTimerInternalMsg(msg, state)
|
||||||
|
|
||||||
@ -505,8 +505,8 @@ class MeetingActor(
|
|||||||
case m: PreuploadedPresentationsSysPubMsg => presentationApp2x.handle(m, liveMeeting, msgBus)
|
case m: PreuploadedPresentationsSysPubMsg => presentationApp2x.handle(m, liveMeeting, msgBus)
|
||||||
case m: AssignPresenterReqMsg => state = handlePresenterChange(m, state)
|
case m: AssignPresenterReqMsg => state = handlePresenterChange(m, state)
|
||||||
case m: MakePresentationWithAnnotationDownloadReqMsg => presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
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: NewPresAnnFileAvailableMsg => presentationPodsApp.handle(m, liveMeeting, msgBus)
|
||||||
|
case m: PadCapturePubMsg => presentationPodsApp.handle(m, liveMeeting, msgBus)
|
||||||
|
|
||||||
// Presentation Pods
|
// Presentation Pods
|
||||||
case m: CreateNewPresentationPodPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
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: StoreAnnotationsInRedisSysMsg => logMessage(msg)
|
||||||
// case m: StoreExportJobInRedisSysMsg => logMessage(msg)
|
// case m: StoreExportJobInRedisSysMsg => logMessage(msg)
|
||||||
case m: MakePresentationWithAnnotationDownloadReqMsg => logMessage(msg)
|
case m: MakePresentationWithAnnotationDownloadReqMsg => logMessage(msg)
|
||||||
case m: ExportPresentationWithAnnotationReqMsg => logMessage(msg)
|
|
||||||
case m: NewPresAnnFileAvailableMsg => logMessage(msg)
|
case m: NewPresAnnFileAvailableMsg => logMessage(msg)
|
||||||
case m: PresentationPageConversionStartedSysMsg => logMessage(msg)
|
case m: PresentationPageConversionStartedSysMsg => logMessage(msg)
|
||||||
case m: PresentationConversionEndedSysMsg => 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: PadUpdatedEvtMsg => logMessage(msg)
|
||||||
case m: PadUpdatePubMsg => logMessage(msg)
|
case m: PadUpdatePubMsg => logMessage(msg)
|
||||||
case m: PadUpdateCmdMsg => logMessage(msg)
|
case m: PadUpdateCmdMsg => logMessage(msg)
|
||||||
|
case m: PadCapturePubMsg => logMessage(msg)
|
||||||
|
|
||||||
case _ => // ignore message
|
case _ => // ignore message
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ trait AppsTestFixtures {
|
|||||||
val meetingLayout = ""
|
val meetingLayout = ""
|
||||||
|
|
||||||
val metadata: collection.immutable.Map[String, String] = Map("foo" -> "bar", "bar" -> "baz", "baz" -> "foo")
|
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,
|
val meetingProp = MeetingProp(name = meetingName, extId = externalMeetingId, intId = meetingId,
|
||||||
meetingCameraCap = meetingCameraCap,
|
meetingCameraCap = meetingCameraCap,
|
||||||
|
@ -27,7 +27,9 @@ case class BreakoutProps(
|
|||||||
freeJoin: Boolean,
|
freeJoin: Boolean,
|
||||||
breakoutRooms: Vector[String],
|
breakoutRooms: Vector[String],
|
||||||
record: Boolean,
|
record: Boolean,
|
||||||
privateChatEnabled: Boolean
|
privateChatEnabled: Boolean,
|
||||||
|
captureNotes: Boolean,
|
||||||
|
captureSlides: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
case class PasswordProp(moderatorPass: String, viewerPass: String, learningDashboardAccessToken: String)
|
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 VoiceProp(telVoice: String, voiceConf: String, dialNumber: String, muteOnStart: Boolean)
|
||||||
|
|
||||||
case class UsersProp(
|
case class UsersProp(
|
||||||
maxUsers: Int,
|
maxUsers: Int,
|
||||||
webcamsOnlyForModerator: Boolean,
|
maxUserConcurrentAccesses:Int,
|
||||||
userCameraCap: Int,
|
webcamsOnlyForModerator: Boolean,
|
||||||
guestPolicy: String,
|
userCameraCap: Int,
|
||||||
meetingLayout: String,
|
guestPolicy: String,
|
||||||
allowModsToUnmuteUsers: Boolean,
|
meetingLayout: String,
|
||||||
allowModsToEjectCameras: Boolean,
|
allowModsToUnmuteUsers: Boolean,
|
||||||
authenticatedGuest: Boolean
|
allowModsToEjectCameras: Boolean,
|
||||||
|
authenticatedGuest: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
case class MetadataProp(metadata: collection.immutable.Map[String, String])
|
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" }
|
object BreakoutRoomsListEvtMsg { val NAME = "BreakoutRoomsListEvtMsg" }
|
||||||
case class BreakoutRoomsListEvtMsg(header: BbbClientMsgHeader, body: BreakoutRoomsListEvtMsgBody) extends BbbCoreMsg
|
case class BreakoutRoomsListEvtMsg(header: BbbClientMsgHeader, body: BreakoutRoomsListEvtMsgBody) extends BbbCoreMsg
|
||||||
case class BreakoutRoomsListEvtMsgBody(meetingId: String, rooms: Vector[BreakoutRoomInfo], roomsReady: Boolean)
|
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" }
|
object BreakoutRoomsListMsg { val NAME = "BreakoutRoomsListMsg" }
|
||||||
case class BreakoutRoomsListMsg(header: BbbClientMsgHeader, body: BreakoutRoomsListMsgBody) extends StandardMsg
|
case class BreakoutRoomsListMsg(header: BbbClientMsgHeader, body: BreakoutRoomsListMsgBody) extends StandardMsg
|
||||||
@ -58,7 +58,9 @@ case class BreakoutRoomDetail(
|
|||||||
sourcePresentationId: String,
|
sourcePresentationId: String,
|
||||||
sourcePresentationSlide: Int,
|
sourcePresentationSlide: Int,
|
||||||
record: Boolean,
|
record: Boolean,
|
||||||
privateChatEnabled: Boolean
|
privateChatEnabled: Boolean,
|
||||||
|
captureNotes: Boolean,
|
||||||
|
captureSlides: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -66,7 +68,7 @@ case class BreakoutRoomDetail(
|
|||||||
*/
|
*/
|
||||||
object CreateBreakoutRoomsCmdMsg { val NAME = "CreateBreakoutRoomsCmdMsg" }
|
object CreateBreakoutRoomsCmdMsg { val NAME = "CreateBreakoutRoomsCmdMsg" }
|
||||||
case class CreateBreakoutRoomsCmdMsg(header: BbbClientMsgHeader, body: CreateBreakoutRoomsCmdMsgBody) extends StandardMsg
|
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])
|
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
|
// 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,
|
case class BreakoutRoomVO(id: String, externalId: String, name: String, parentId: String,
|
||||||
sequence: Int, freeJoin: Boolean, voiceConf: 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" }
|
object PadUpdateCmdMsg { val NAME = "PadUpdateCmdMsg" }
|
||||||
case class PadUpdateCmdMsg(header: BbbCoreHeaderWithMeetingId, body: PadUpdateCmdMsgBody) extends BbbCoreMsg
|
case class PadUpdateCmdMsg(header: BbbCoreHeaderWithMeetingId, body: PadUpdateCmdMsgBody) extends BbbCoreMsg
|
||||||
case class PadUpdateCmdMsgBody(groupId: String, name: String, text: String)
|
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 MakePresentationWithAnnotationDownloadReqMsg(header: BbbClientMsgHeader, body: MakePresentationWithAnnotationDownloadReqMsgBody) extends StandardMsg
|
||||||
case class MakePresentationWithAnnotationDownloadReqMsgBody(presId: String, allPages: Boolean, pages: List[Int])
|
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" }
|
object NewPresAnnFileAvailableMsg { val NAME = "NewPresAnnFileAvailableMsg" }
|
||||||
case class NewPresAnnFileAvailableMsg(header: BbbClientMsgHeader, body: NewPresAnnFileAvailableMsgBody) extends StandardMsg
|
case class NewPresAnnFileAvailableMsg(header: BbbClientMsgHeader, body: NewPresAnnFileAvailableMsgBody) extends StandardMsg
|
||||||
case class NewPresAnnFileAvailableMsgBody(fileURI: String, presId: String)
|
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 ------------
|
// ------------ bbb-common-web to akka-apps ------------
|
||||||
|
|
||||||
// ------------ akka-apps to client ------------
|
// ------------ akka-apps to client ------------
|
||||||
@ -40,4 +32,13 @@ case class PresenterUnassignedEvtMsgBody(intId: String, name: String, assignedBy
|
|||||||
object NewPresentationEvtMsg { val NAME = "NewPresentationEvtMsg" }
|
object NewPresentationEvtMsg { val NAME = "NewPresentationEvtMsg" }
|
||||||
case class NewPresentationEvtMsg(header: BbbClientMsgHeader, body: NewPresentationEvtMsgBody) extends BbbCoreMsg
|
case class NewPresentationEvtMsg(header: BbbClientMsgHeader, body: NewPresentationEvtMsgBody) extends BbbCoreMsg
|
||||||
case class NewPresentationEvtMsgBody(presentation: PresentationVO)
|
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 ------------
|
// ------------ akka-apps to client ------------
|
||||||
|
@ -1,13 +1,5 @@
|
|||||||
package org.bigbluebutton.common2.msgs
|
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" }
|
object RegisterUserReqMsg { val NAME = "RegisterUserReqMsg" }
|
||||||
case class RegisterUserReqMsg(
|
case class RegisterUserReqMsg(
|
||||||
header: BbbCoreHeaderWithMeetingId,
|
header: BbbCoreHeaderWithMeetingId,
|
||||||
|
@ -49,7 +49,7 @@ trait TestFixtures {
|
|||||||
meetingCameraCap = meetingCameraCap,
|
meetingCameraCap = meetingCameraCap,
|
||||||
maxPinnedCameras = maxPinnedCameras,
|
maxPinnedCameras = maxPinnedCameras,
|
||||||
isBreakout = isBreakout.booleanValue())
|
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,
|
val durationProps = DurationProps(duration = durationInMinutes, createdTime = createTime, createdDate = createDate,
|
||||||
meetingExpireIfNoUserJoinedInMinutes = meetingExpireIfNoUserJoinedInMinutes, meetingExpireWhenLastUserLeftInMinutes = meetingExpireWhenLastUserLeftInMinutes,
|
meetingExpireIfNoUserJoinedInMinutes = meetingExpireIfNoUserJoinedInMinutes, meetingExpireWhenLastUserLeftInMinutes = meetingExpireWhenLastUserLeftInMinutes,
|
||||||
|
@ -31,7 +31,7 @@ object Dependencies {
|
|||||||
val lang = "3.12.0"
|
val lang = "3.12.0"
|
||||||
val io = "2.11.0"
|
val io = "2.11.0"
|
||||||
val pool = "2.11.1"
|
val pool = "2.11.1"
|
||||||
val text = "1.9"
|
val text = "1.10.0"
|
||||||
|
|
||||||
// BigBlueButton
|
// BigBlueButton
|
||||||
val bbbCommons = "0.0.21-SNAPSHOT"
|
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_DESCRIPTION = "uploadExternalDescription";
|
||||||
public static final String UPLOAD_EXTERNAL_URL = "uploadExternalUrl";
|
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_ENABLED = "breakoutRoomsEnabled";
|
||||||
public static final String BREAKOUT_ROOMS_RECORD = "breakoutRoomsRecord";
|
public static final String BREAKOUT_ROOMS_RECORD = "breakoutRoomsRecord";
|
||||||
public static final String BREAKOUT_ROOMS_PRIVATE_CHAT_ENABLED = "breakoutRoomsPrivateChatEnabled";
|
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("meetingId", m.getExternalId());
|
||||||
breakoutMetadata.put("sequence", m.getSequence().toString());
|
breakoutMetadata.put("sequence", m.getSequence().toString());
|
||||||
breakoutMetadata.put("freeJoin", m.isFreeJoin().toString());
|
breakoutMetadata.put("freeJoin", m.isFreeJoin().toString());
|
||||||
|
breakoutMetadata.put("captureSlides", m.isCaptureSlides().toString());
|
||||||
|
breakoutMetadata.put("captureNotes", m.isCaptureNotes().toString());
|
||||||
breakoutMetadata.put("parentMeetingId", m.getParentMeetingId());
|
breakoutMetadata.put("parentMeetingId", m.getParentMeetingId());
|
||||||
storeService.recordBreakoutInfo(m.getInternalId(), breakoutMetadata);
|
storeService.recordBreakoutInfo(m.getInternalId(), breakoutMetadata);
|
||||||
}
|
}
|
||||||
@ -388,6 +390,8 @@ public class MeetingService implements MessageListener {
|
|||||||
if (m.isBreakout()) {
|
if (m.isBreakout()) {
|
||||||
logData.put("sequence", m.getSequence());
|
logData.put("sequence", m.getSequence());
|
||||||
logData.put("freeJoin", m.isFreeJoin());
|
logData.put("freeJoin", m.isFreeJoin());
|
||||||
|
logData.put("captureSlides", m.isCaptureSlides());
|
||||||
|
logData.put("captureNotes", m.isCaptureNotes());
|
||||||
logData.put("parentMeetingId", m.getParentMeetingId());
|
logData.put("parentMeetingId", m.getParentMeetingId());
|
||||||
}
|
}
|
||||||
logData.put("name", m.getName());
|
logData.put("name", m.getName());
|
||||||
@ -415,7 +419,7 @@ public class MeetingService implements MessageListener {
|
|||||||
m.getLearningDashboardAccessToken(), m.getCreateTime(),
|
m.getLearningDashboardAccessToken(), m.getCreateTime(),
|
||||||
formatPrettyDate(m.getCreateTime()), m.isBreakout(), m.getSequence(), m.isFreeJoin(), m.getMetadata(),
|
formatPrettyDate(m.getCreateTime()), m.isBreakout(), m.getSequence(), m.isFreeJoin(), m.getMetadata(),
|
||||||
m.getGuestPolicy(), m.getAuthenticatedGuest(), m.getMeetingLayout(), m.getWelcomeMessageTemplate(), m.getWelcomeMessage(), m.getModeratorOnlyMessage(),
|
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.getMeetingExpireIfNoUserJoinedInMinutes(), m.getMeetingExpireWhenLastUserLeftInMinutes(),
|
||||||
m.getUserInactivityInspectTimerInMinutes(), m.getUserInactivityThresholdInMinutes(),
|
m.getUserInactivityInspectTimerInMinutes(), m.getUserInactivityThresholdInMinutes(),
|
||||||
m.getUserActivitySignResponseDelayInMinutes(), m.getEndWhenNoModerator(), m.getEndWhenNoModeratorDelayInMinutes(),
|
m.getUserActivitySignResponseDelayInMinutes(), m.getEndWhenNoModerator(), m.getEndWhenNoModeratorDelayInMinutes(),
|
||||||
@ -434,33 +438,6 @@ public class MeetingService implements MessageListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void processRegisterUser(RegisterUser message) {
|
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,
|
gw.registerUser(message.meetingID,
|
||||||
message.internalUserId, message.fullname, message.role,
|
message.internalUserId, message.fullname, message.role,
|
||||||
message.externUserID, message.authToken, message.avatarURL, message.guest,
|
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.IS_BREAKOUT, "true");
|
||||||
params.put(ApiParams.SEQUENCE, message.sequence.toString());
|
params.put(ApiParams.SEQUENCE, message.sequence.toString());
|
||||||
params.put(ApiParams.FREE_JOIN, message.freeJoin.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.ATTENDEE_PW, message.viewerPassword);
|
||||||
params.put(ApiParams.MODERATOR_PW, message.moderatorPassword);
|
params.put(ApiParams.MODERATOR_PW, message.moderatorPassword);
|
||||||
params.put(ApiParams.DIAL_NUMBER, message.dialNumber);
|
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.name, message.role, message.avatarURL, message.guest, message.guestStatus,
|
||||||
message.clientType);
|
message.clientType);
|
||||||
|
|
||||||
if(m.getMaxUsers() > 0 && m.getUsers().size() >= m.getMaxUsers()) {
|
if(m.getMaxUsers() > 0 && m.countUniqueExtIds() >= m.getMaxUsers()) {
|
||||||
m.removeEnteredUser(user.getInternalUserId());
|
m.removeEnteredUser(user.getInternalUserId());
|
||||||
gw.ejectDuplicateUser(message.meetingId, user.getInternalUserId(), user.getFullname(), user.getExternalUserId());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,6 +66,8 @@ public class ParamsProcessorUtil {
|
|||||||
private String apiVersion;
|
private String apiVersion;
|
||||||
private boolean serviceEnabled = false;
|
private boolean serviceEnabled = false;
|
||||||
private String securitySalt;
|
private String securitySalt;
|
||||||
|
private String supportedChecksumAlgorithms;
|
||||||
|
private String checksumHash;
|
||||||
private int defaultMaxUsers = 20;
|
private int defaultMaxUsers = 20;
|
||||||
private String defaultWelcomeMessage;
|
private String defaultWelcomeMessage;
|
||||||
private String defaultWelcomeMessageFooter;
|
private String defaultWelcomeMessageFooter;
|
||||||
@ -106,6 +108,8 @@ public class ParamsProcessorUtil {
|
|||||||
|
|
||||||
private boolean defaultBreakoutRoomsEnabled = true;
|
private boolean defaultBreakoutRoomsEnabled = true;
|
||||||
private boolean defaultBreakoutRoomsRecord;
|
private boolean defaultBreakoutRoomsRecord;
|
||||||
|
private boolean defaultBreakoutRoomsCaptureSlides = false;
|
||||||
|
private boolean defaultBreakoutRoomsCaptureNotes = false;
|
||||||
private boolean defaultbreakoutRoomsPrivateChatEnabled;
|
private boolean defaultbreakoutRoomsPrivateChatEnabled;
|
||||||
|
|
||||||
private boolean defaultLockSettingsDisableCam;
|
private boolean defaultLockSettingsDisableCam;
|
||||||
@ -128,6 +132,8 @@ public class ParamsProcessorUtil {
|
|||||||
private Integer userInactivityThresholdInMinutes = 30;
|
private Integer userInactivityThresholdInMinutes = 30;
|
||||||
private Integer userActivitySignResponseDelayInMinutes = 5;
|
private Integer userActivitySignResponseDelayInMinutes = 5;
|
||||||
private Boolean defaultAllowDuplicateExtUserid = true;
|
private Boolean defaultAllowDuplicateExtUserid = true;
|
||||||
|
|
||||||
|
private Integer maxUserConcurrentAccesses = 0;
|
||||||
private Boolean defaultEndWhenNoModerator = false;
|
private Boolean defaultEndWhenNoModerator = false;
|
||||||
private Integer defaultEndWhenNoModeratorDelayInMinutes = 1;
|
private Integer defaultEndWhenNoModeratorDelayInMinutes = 1;
|
||||||
private Integer defaultHtml5InstanceId = 1;
|
private Integer defaultHtml5InstanceId = 1;
|
||||||
@ -275,7 +281,19 @@ public class ParamsProcessorUtil {
|
|||||||
breakoutRoomsPrivateChatEnabled = Boolean.parseBoolean(breakoutRoomsPrivateChatEnabledParam);
|
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) {
|
private LockSettingsParams processLockSettingsParams(Map<String, String> params) {
|
||||||
@ -680,6 +698,11 @@ public class ParamsProcessorUtil {
|
|||||||
|
|
||||||
int html5InstanceId = processHtml5InstanceId(params.get(ApiParams.HTML5_INSTANCE_ID));
|
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.
|
// Create the meeting with all passed in parameters.
|
||||||
Meeting meeting = new Meeting.Builder(externalMeetingId,
|
Meeting meeting = new Meeting.Builder(externalMeetingId,
|
||||||
internalMeetingId, createTime).withName(meetingName)
|
internalMeetingId, createTime).withName(meetingName)
|
||||||
@ -706,7 +729,7 @@ public class ParamsProcessorUtil {
|
|||||||
.withMeetingLayout(meetingLayout)
|
.withMeetingLayout(meetingLayout)
|
||||||
.withBreakoutRoomsParams(breakoutParams)
|
.withBreakoutRoomsParams(breakoutParams)
|
||||||
.withLockSettingsParams(lockSettingsParams)
|
.withLockSettingsParams(lockSettingsParams)
|
||||||
.withAllowDuplicateExtUserid(defaultAllowDuplicateExtUserid)
|
.withMaxUserConcurrentAccesses(maxUserConcurrentAccesses)
|
||||||
.withHTML5InstanceId(html5InstanceId)
|
.withHTML5InstanceId(html5InstanceId)
|
||||||
.withLearningDashboardCleanupDelayInMinutes(learningDashboardCleanupMins)
|
.withLearningDashboardCleanupDelayInMinutes(learningDashboardCleanupMins)
|
||||||
.withLearningDashboardAccessToken(learningDashboardAccessToken)
|
.withLearningDashboardAccessToken(learningDashboardAccessToken)
|
||||||
@ -742,6 +765,8 @@ public class ParamsProcessorUtil {
|
|||||||
if (isBreakout) {
|
if (isBreakout) {
|
||||||
meeting.setSequence(Integer.parseInt(params.get(ApiParams.SEQUENCE)));
|
meeting.setSequence(Integer.parseInt(params.get(ApiParams.SEQUENCE)));
|
||||||
meeting.setFreeJoin(Boolean.parseBoolean(params.get(ApiParams.FREE_JOIN)));
|
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);
|
meeting.setParentMeetingId(parentMeetingId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -978,11 +1003,39 @@ public class ParamsProcessorUtil {
|
|||||||
log.info("CHECKSUM={} length={}", checksum, checksum.length());
|
log.info("CHECKSUM={} length={}", checksum, checksum.length());
|
||||||
|
|
||||||
String data = apiCall + queryString + securitySalt;
|
String data = apiCall + queryString + securitySalt;
|
||||||
String cs = DigestUtils.sha1Hex(data);
|
|
||||||
if (checksum.length() == 64) {
|
int checksumLength = checksum.length();
|
||||||
cs = DigestUtils.sha256Hex(data);
|
String cs = null;
|
||||||
log.info("SHA256 {}", cs);
|
|
||||||
}
|
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)) {
|
if (cs == null || !cs.equals(checksum)) {
|
||||||
log.info("query string after checksum removed: [{}]", queryString);
|
log.info("query string after checksum removed: [{}]", queryString);
|
||||||
log.info("checksumError: query string checksum failed. our: [{}], client: [{}]", cs, checksum);
|
log.info("checksumError: query string checksum failed. our: [{}], client: [{}]", cs, checksum);
|
||||||
@ -1068,6 +1121,10 @@ public class ParamsProcessorUtil {
|
|||||||
this.securitySalt = securitySalt;
|
this.securitySalt = securitySalt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setSupportedChecksumAlgorithms(String supportedChecksumAlgorithms) { this.supportedChecksumAlgorithms = supportedChecksumAlgorithms; }
|
||||||
|
|
||||||
|
public void setChecksumHash(String checksumHash) { this.checksumHash = checksumHash; }
|
||||||
|
|
||||||
public void setDefaultMaxUsers(int defaultMaxUsers) {
|
public void setDefaultMaxUsers(int defaultMaxUsers) {
|
||||||
this.defaultMaxUsers = defaultMaxUsers;
|
this.defaultMaxUsers = defaultMaxUsers;
|
||||||
}
|
}
|
||||||
@ -1367,6 +1424,10 @@ public class ParamsProcessorUtil {
|
|||||||
this.defaultAllowDuplicateExtUserid = allow;
|
this.defaultAllowDuplicateExtUserid = allow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setMaxUserConcurrentAccesses(Integer maxUserConcurrentAccesses) {
|
||||||
|
this.maxUserConcurrentAccesses = maxUserConcurrentAccesses;
|
||||||
|
}
|
||||||
|
|
||||||
public void setEndWhenNoModerator(Boolean val) {
|
public void setEndWhenNoModerator(Boolean val) {
|
||||||
this.defaultEndWhenNoModerator = val;
|
this.defaultEndWhenNoModerator = val;
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,13 @@ package org.bigbluebutton.api.domain;
|
|||||||
public class BreakoutRoomsParams {
|
public class BreakoutRoomsParams {
|
||||||
public final Boolean record;
|
public final Boolean record;
|
||||||
public final Boolean privateChatEnabled;
|
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.record = record;
|
||||||
this.privateChatEnabled = privateChatEnabled;
|
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 String parentMeetingId = "bbb-none"; // Initialize so we don't send null in the json message.
|
||||||
private Integer sequence = 0;
|
private Integer sequence = 0;
|
||||||
private Boolean freeJoin = false;
|
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 createdTime = 0;
|
||||||
private long startTime = 0;
|
private long startTime = 0;
|
||||||
private long endTime = 0;
|
private long endTime = 0;
|
||||||
@ -109,7 +111,7 @@ public class Meeting {
|
|||||||
public final BreakoutRoomsParams breakoutRoomsParams;
|
public final BreakoutRoomsParams breakoutRoomsParams;
|
||||||
public final LockSettingsParams lockSettingsParams;
|
public final LockSettingsParams lockSettingsParams;
|
||||||
|
|
||||||
public final Boolean allowDuplicateExtUserid;
|
public final Integer maxUserConcurrentAccesses;
|
||||||
|
|
||||||
private String meetingEndedCallbackURL = "";
|
private String meetingEndedCallbackURL = "";
|
||||||
|
|
||||||
@ -163,7 +165,7 @@ public class Meeting {
|
|||||||
allowRequestsWithoutSession = builder.allowRequestsWithoutSession;
|
allowRequestsWithoutSession = builder.allowRequestsWithoutSession;
|
||||||
breakoutRoomsParams = builder.breakoutRoomsParams;
|
breakoutRoomsParams = builder.breakoutRoomsParams;
|
||||||
lockSettingsParams = builder.lockSettingsParams;
|
lockSettingsParams = builder.lockSettingsParams;
|
||||||
allowDuplicateExtUserid = builder.allowDuplicateExtUserid;
|
maxUserConcurrentAccesses = builder.maxUserConcurrentAccesses;
|
||||||
endWhenNoModerator = builder.endWhenNoModerator;
|
endWhenNoModerator = builder.endWhenNoModerator;
|
||||||
endWhenNoModeratorDelayInMinutes = builder.endWhenNoModeratorDelayInMinutes;
|
endWhenNoModeratorDelayInMinutes = builder.endWhenNoModeratorDelayInMinutes;
|
||||||
html5InstanceId = builder.html5InstanceId;
|
html5InstanceId = builder.html5InstanceId;
|
||||||
@ -197,6 +199,28 @@ public class Meeting {
|
|||||||
return users;
|
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) {
|
public void guestIsWaiting(String userId) {
|
||||||
RegisteredUser ruser = registeredUsers.get(userId);
|
RegisteredUser ruser = registeredUsers.get(userId);
|
||||||
if (ruser != null) {
|
if (ruser != null) {
|
||||||
@ -288,6 +312,22 @@ public class Meeting {
|
|||||||
this.freeJoin = freeJoin;
|
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() {
|
public Integer getDuration() {
|
||||||
return duration;
|
return duration;
|
||||||
}
|
}
|
||||||
@ -504,6 +544,10 @@ public class Meeting {
|
|||||||
return maxUsers;
|
return maxUsers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Integer getMaxUserConcurrentAccesses() {
|
||||||
|
return maxUserConcurrentAccesses;
|
||||||
|
}
|
||||||
|
|
||||||
public int getLogoutTimer() {
|
public int getLogoutTimer() {
|
||||||
return logoutTimer;
|
return logoutTimer;
|
||||||
}
|
}
|
||||||
@ -633,17 +677,6 @@ public class Meeting {
|
|||||||
return this.users.get(id);
|
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(){
|
public int getNumUsers(){
|
||||||
return this.users.size();
|
return this.users.size();
|
||||||
}
|
}
|
||||||
@ -843,7 +876,8 @@ public class Meeting {
|
|||||||
private String meetingLayout;
|
private String meetingLayout;
|
||||||
private BreakoutRoomsParams breakoutRoomsParams;
|
private BreakoutRoomsParams breakoutRoomsParams;
|
||||||
private LockSettingsParams lockSettingsParams;
|
private LockSettingsParams lockSettingsParams;
|
||||||
private Boolean allowDuplicateExtUserid;
|
|
||||||
|
private Integer maxUserConcurrentAccesses;
|
||||||
private Boolean endWhenNoModerator;
|
private Boolean endWhenNoModerator;
|
||||||
private Integer endWhenNoModeratorDelayInMinutes;
|
private Integer endWhenNoModeratorDelayInMinutes;
|
||||||
private int html5InstanceId;
|
private int html5InstanceId;
|
||||||
@ -1035,8 +1069,8 @@ public class Meeting {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder withAllowDuplicateExtUserid(Boolean allowDuplicateExtUserid) {
|
public Builder withMaxUserConcurrentAccesses(Integer maxUserConcurrentAccesses) {
|
||||||
this.allowDuplicateExtUserid = allowDuplicateExtUserid;
|
this.maxUserConcurrentAccesses = maxUserConcurrentAccesses;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,8 @@ public class CreateBreakoutRoom implements IMessage {
|
|||||||
public final Integer sourcePresentationSlide;
|
public final Integer sourcePresentationSlide;
|
||||||
public final Boolean record;
|
public final Boolean record;
|
||||||
public final Boolean privateChatEnabled;
|
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,
|
public CreateBreakoutRoom(String meetingId,
|
||||||
String parentMeetingId,
|
String parentMeetingId,
|
||||||
@ -35,7 +37,9 @@ public class CreateBreakoutRoom implements IMessage {
|
|||||||
String sourcePresentationId,
|
String sourcePresentationId,
|
||||||
Integer sourcePresentationSlide,
|
Integer sourcePresentationSlide,
|
||||||
Boolean record,
|
Boolean record,
|
||||||
Boolean privateChatEnabled) {
|
Boolean privateChatEnabled,
|
||||||
|
Boolean captureNotes,
|
||||||
|
Boolean captureSlides) {
|
||||||
this.meetingId = meetingId;
|
this.meetingId = meetingId;
|
||||||
this.parentMeetingId = parentMeetingId;
|
this.parentMeetingId = parentMeetingId;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
@ -52,5 +56,7 @@ public class CreateBreakoutRoom implements IMessage {
|
|||||||
this.sourcePresentationSlide = sourcePresentationSlide;
|
this.sourcePresentationSlide = sourcePresentationSlide;
|
||||||
this.record = record;
|
this.record = record;
|
||||||
this.privateChatEnabled = privateChatEnabled;
|
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;
|
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.MeetingEndedConstraint;
|
||||||
import org.bigbluebutton.api.model.constraint.MeetingExistsConstraint;
|
import org.bigbluebutton.api.model.constraint.MeetingExistsConstraint;
|
||||||
import org.bigbluebutton.api.model.constraint.UserSessionConstraint;
|
import org.bigbluebutton.api.model.constraint.UserSessionConstraint;
|
||||||
|
@ -20,6 +20,7 @@ public class GetChecksumValidator implements ConstraintValidator<GetChecksumCons
|
|||||||
@Override
|
@Override
|
||||||
public boolean isValid(GetChecksum checksum, ConstraintValidatorContext context) {
|
public boolean isValid(GetChecksum checksum, ConstraintValidatorContext context) {
|
||||||
String securitySalt = ServiceUtils.getValidationService().getSecuritySalt();
|
String securitySalt = ServiceUtils.getValidationService().getSecuritySalt();
|
||||||
|
String supportedChecksumAlgorithms = ServiceUtils.getValidationService().getSupportedChecksumAlgorithms();
|
||||||
|
|
||||||
if (securitySalt.isEmpty()) {
|
if (securitySalt.isEmpty()) {
|
||||||
log.warn("Security is disabled in this service. Make sure this is intentional.");
|
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 data = checksum.getApiCall() + queryStringWithoutChecksum + securitySalt;
|
||||||
String createdCheckSum = DigestUtils.sha1Hex(data);
|
|
||||||
|
|
||||||
if (providedChecksum.length() == 64) {
|
int checksumLength = providedChecksum.length();
|
||||||
log.debug("providedChecksum.length() == 64");
|
String createdCheckSum = null;
|
||||||
createdCheckSum = DigestUtils.sha256Hex(data);
|
|
||||||
log.info("SHA256 {}", createdCheckSum);
|
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)) {
|
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 securitySalt;
|
||||||
|
private String supportedChecksumAlgorithms;
|
||||||
private Boolean allowRequestsWithoutSession;
|
private Boolean allowRequestsWithoutSession;
|
||||||
|
|
||||||
private ValidatorFactory validatorFactory;
|
private ValidatorFactory validatorFactory;
|
||||||
@ -266,6 +267,9 @@ public class ValidationService {
|
|||||||
public void setSecuritySalt(String securitySalt) { this.securitySalt = securitySalt; }
|
public void setSecuritySalt(String securitySalt) { this.securitySalt = securitySalt; }
|
||||||
public String getSecuritySalt() { return securitySalt; }
|
public String getSecuritySalt() { return securitySalt; }
|
||||||
|
|
||||||
|
public void setSupportedChecksumAlgorithms(String supportedChecksumAlgorithms) { this.supportedChecksumAlgorithms = supportedChecksumAlgorithms; }
|
||||||
|
public String getSupportedChecksumAlgorithms() { return supportedChecksumAlgorithms; }
|
||||||
|
|
||||||
public void setAllowRequestsWithoutSession(Boolean allowRequestsWithoutSession) {
|
public void setAllowRequestsWithoutSession(Boolean allowRequestsWithoutSession) {
|
||||||
this.allowRequestsWithoutSession = allowRequestsWithoutSession;
|
this.allowRequestsWithoutSession = allowRequestsWithoutSession;
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ public interface IBbbWebApiGWApp {
|
|||||||
String moderatorPass, String viewerPass, String learningDashboardAccessToken, Long createTime,
|
String moderatorPass, String viewerPass, String learningDashboardAccessToken, Long createTime,
|
||||||
String createDate, Boolean isBreakout, Integer sequence, Boolean freejoin, Map<String, String> metadata,
|
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 guestPolicy, Boolean authenticatedGuest, String meetingLayout, String welcomeMsgTemplate, String welcomeMsg, String modOnlyMessage,
|
||||||
String dialNumber, Integer maxUsers,
|
String dialNumber, Integer maxUsers, Integer maxUserConcurrentAccesses,
|
||||||
Integer meetingExpireIfNoUserJoinedInMinutes,
|
Integer meetingExpireIfNoUserJoinedInMinutes,
|
||||||
Integer meetingExpireWhenLastUserLeftInMinutes,
|
Integer meetingExpireWhenLastUserLeftInMinutes,
|
||||||
Integer userInactivityInspectTimerInMinutes,
|
Integer userInactivityInspectTimerInMinutes,
|
||||||
@ -50,8 +50,6 @@ public interface IBbbWebApiGWApp {
|
|||||||
void registerUser(String meetingID, String internalUserId, String fullname, String role,
|
void registerUser(String meetingID, String internalUserId, String fullname, String role,
|
||||||
String externUserID, String authToken, String avatarURL,
|
String externUserID, String authToken, String avatarURL,
|
||||||
Boolean guest, Boolean authed, String guestStatus, Boolean excludeFromDashboard);
|
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 guestWaitingLeft(String meetingID, String internalUserId);
|
||||||
|
|
||||||
void destroyMeeting(DestroyMeetingMessage msg);
|
void destroyMeeting(DestroyMeetingMessage msg);
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
* @version $Id: $
|
* @version $Id: $
|
||||||
*/
|
*/
|
||||||
package org.bigbluebutton.presentation;
|
package org.bigbluebutton.presentation;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
|
@ -50,7 +50,6 @@ public class SwfSlidesGenerationProgressNotifier {
|
|||||||
maxUploadFileSize);
|
maxUploadFileSize);
|
||||||
messagingService.sendDocConversionMsg(progress);
|
messagingService.sendDocConversionMsg(progress);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendUploadFileTimedout(UploadedPresentation pres, int page) {
|
public void sendUploadFileTimedout(UploadedPresentation pres, int page) {
|
||||||
UploadFileTimedoutMessage errorMessage = new UploadFileTimedoutMessage(
|
UploadFileTimedoutMessage errorMessage = new UploadFileTimedoutMessage(
|
||||||
pres.getPodId(),
|
pres.getPodId(),
|
||||||
|
@ -131,7 +131,9 @@ class BbbWebApiGWApp(
|
|||||||
freeJoin: java.lang.Boolean,
|
freeJoin: java.lang.Boolean,
|
||||||
metadata: java.util.Map[String, String], guestPolicy: String, authenticatedGuest: java.lang.Boolean, meetingLayout: String,
|
metadata: java.util.Map[String, String], guestPolicy: String, authenticatedGuest: java.lang.Boolean, meetingLayout: String,
|
||||||
welcomeMsgTemplate: String, welcomeMsg: String, modOnlyMessage: 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,
|
meetingExpireIfNoUserJoinedInMinutes: java.lang.Integer,
|
||||||
meetingExpireWhenLastUserLeftInMinutes: java.lang.Integer,
|
meetingExpireWhenLastUserLeftInMinutes: java.lang.Integer,
|
||||||
userInactivityInspectTimerInMinutes: java.lang.Integer,
|
userInactivityInspectTimerInMinutes: java.lang.Integer,
|
||||||
@ -189,17 +191,23 @@ class BbbWebApiGWApp(
|
|||||||
freeJoin = freeJoin.booleanValue(),
|
freeJoin = freeJoin.booleanValue(),
|
||||||
breakoutRooms = Vector(),
|
breakoutRooms = Vector(),
|
||||||
record = breakoutParams.record.booleanValue(),
|
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,
|
val welcomeProp = WelcomeProp(welcomeMsgTemplate = welcomeMsgTemplate, welcomeMsg = welcomeMsg,
|
||||||
modOnlyMessage = modOnlyMessage)
|
modOnlyMessage = modOnlyMessage)
|
||||||
val voiceProp = VoiceProp(telVoice = voiceBridge, voiceConf = voiceBridge, dialNumber = dialNumber, muteOnStart = muteOnStart.booleanValue())
|
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(),
|
userCameraCap = userCameraCap.intValue(),
|
||||||
guestPolicy = guestPolicy, meetingLayout = meetingLayout, allowModsToUnmuteUsers = allowModsToUnmuteUsers.booleanValue(),
|
guestPolicy = guestPolicy, meetingLayout = meetingLayout, allowModsToUnmuteUsers = allowModsToUnmuteUsers.booleanValue(),
|
||||||
allowModsToEjectCameras = allowModsToEjectCameras.booleanValue(),
|
allowModsToEjectCameras = allowModsToEjectCameras.booleanValue(),
|
||||||
authenticatedGuest = authenticatedGuest.booleanValue())
|
authenticatedGuest = authenticatedGuest.booleanValue()
|
||||||
|
)
|
||||||
val metadataProp = MetadataProp(mapAsScalaMap(metadata).toMap)
|
val metadataProp = MetadataProp(mapAsScalaMap(metadata).toMap)
|
||||||
|
|
||||||
val lockSettingsProps = LockSettingsProps(
|
val lockSettingsProps = LockSettingsProps(
|
||||||
@ -261,11 +269,6 @@ class BbbWebApiGWApp(
|
|||||||
msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event))
|
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 = {
|
def guestWaitingLeft(meetingId: String, intUserId: String): Unit = {
|
||||||
val event = MsgBuilder.buildGuestWaitingLeftMsg(meetingId, intUserId)
|
val event = MsgBuilder.buildGuestWaitingLeftMsg(meetingId, intUserId)
|
||||||
msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event))
|
msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event))
|
||||||
|
@ -34,16 +34,6 @@ object MsgBuilder {
|
|||||||
BbbCommonEnvCoreMsg(envelope, req)
|
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 = {
|
def buildRegisterUserRequestToAkkaApps(msg: RegisterUser): BbbCommonEnvCoreMsg = {
|
||||||
val routing = collection.immutable.HashMap("sender" -> "bbb-web")
|
val routing = collection.immutable.HashMap("sender" -> "bbb-web")
|
||||||
val envelope = BbbCoreEnvelope(RegisterUserReqMsg.NAME, routing)
|
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,
|
name: String, sequence: Integer, freeJoin: Boolean, dialNumber: String,
|
||||||
voiceConfId: String, viewerPassword: String, moderatorPassword: String, duration: Int,
|
voiceConfId: String, viewerPassword: String, moderatorPassword: String, duration: Int,
|
||||||
sourcePresentationId: String, sourcePresentationSlide: 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 AddUserSession(token: String, session: UserSession)
|
||||||
case class RegisterUser(meetingId: String, intUserId: String, name: String, role: String,
|
case class RegisterUser(meetingId: String, intUserId: String, name: String, role: String,
|
||||||
|
@ -102,7 +102,9 @@ class OldMeetingMsgHdlrActor(val olgMsgGW: OldMessageReceivedGW)
|
|||||||
msg.body.room.sourcePresentationId,
|
msg.body.room.sourcePresentationId,
|
||||||
msg.body.room.sourcePresentationSlide,
|
msg.body.room.sourcePresentationSlide,
|
||||||
msg.body.room.record,
|
msg.body.room.record,
|
||||||
msg.body.room.privateChatEnabled
|
msg.body.room.privateChatEnabled,
|
||||||
|
msg.body.room.captureNotes,
|
||||||
|
msg.body.room.captureSlides,
|
||||||
))
|
))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,9 @@
|
|||||||
"imagemagick": "/usr/bin/convert",
|
"imagemagick": "/usr/bin/convert",
|
||||||
"pdftocairo": "/usr/bin/pdftocairo"
|
"pdftocairo": "/usr/bin/pdftocairo"
|
||||||
},
|
},
|
||||||
|
"captureNotes": {
|
||||||
|
"timeout": 5000
|
||||||
|
},
|
||||||
"collector": {
|
"collector": {
|
||||||
"pngWidthRasterizedSlides": 2560
|
"pngWidthRasterizedSlides": 2560
|
||||||
},
|
},
|
||||||
@ -25,6 +28,7 @@
|
|||||||
"msgName": "NewPresAnnFileAvailableMsg"
|
"msgName": "NewPresAnnFileAvailableMsg"
|
||||||
},
|
},
|
||||||
"bbbWebAPI": "http://127.0.0.1:8090",
|
"bbbWebAPI": "http://127.0.0.1:8090",
|
||||||
|
"bbbPadsAPI": "http://127.0.0.1:9002",
|
||||||
"redis": {
|
"redis": {
|
||||||
"host": "127.0.0.1",
|
"host": "127.0.0.1",
|
||||||
"port": 6379,
|
"port": 6379,
|
||||||
|
@ -1,24 +1,32 @@
|
|||||||
const Logger = require('../lib/utils/logger');
|
const Logger = require('../lib/utils/logger');
|
||||||
|
const axios = require('axios').default;
|
||||||
const config = require('../config');
|
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 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 jobId = workerData;
|
||||||
|
|
||||||
const logger = new Logger('presAnn Collector');
|
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) => {
|
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('message', resolve);
|
||||||
worker.on('error', reject);
|
worker.on('error', reject);
|
||||||
worker.on('exit', (code) => {
|
worker.on('exit', (code) => {
|
||||||
if (code !== 0) {
|
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 job = fs.readFileSync(path.join(dropbox, 'job'));
|
||||||
const exportJob = JSON.parse(job);
|
const exportJob = JSON.parse(job);
|
||||||
|
|
||||||
// Collect the annotations from Redis
|
async function collectAnnotationsFromRedis() {
|
||||||
(async () => {
|
|
||||||
const client = redis.createClient({
|
const client = redis.createClient({
|
||||||
host: config.redis.host,
|
host: config.redis.host,
|
||||||
port: config.redis.port,
|
port: config.redis.port,
|
||||||
@ -42,7 +49,7 @@ const exportJob = JSON.parse(job);
|
|||||||
|
|
||||||
await client.connect();
|
await client.connect();
|
||||||
|
|
||||||
const presAnn = await client.hGetAll(exportJob.jobId);
|
const presAnn = await client.hGetAll(jobId);
|
||||||
|
|
||||||
// Remove annotations from Redis
|
// Remove annotations from Redis
|
||||||
await client.del(jobId);
|
await client.del(jobId);
|
||||||
@ -95,8 +102,66 @@ const exportJob = JSON.parse(job);
|
|||||||
} else if (fs.existsSync(`${presFile}.jpeg`)) {
|
} else if (fs.existsSync(`${presFile}.jpeg`)) {
|
||||||
fs.copyFileSync(`${presFile}.jpeg`, path.join(dropbox, 'slide1.jpeg'));
|
fs.copyFileSync(`${presFile}.jpeg`, path.join(dropbox, 'slide1.jpeg'));
|
||||||
} else {
|
} 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 {workerData} = require('worker_threads');
|
||||||
|
|
||||||
const [jobType, jobId, filename_with_extension] = workerData;
|
const [jobType, jobId, filename] = workerData;
|
||||||
|
|
||||||
const logger = new Logger('presAnn Notifier Worker');
|
const logger = new Logger('presAnn Notifier Worker');
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ async function notifyMeetingActor() {
|
|||||||
|
|
||||||
const link = path.join(`${path.sep}bigbluebutton`, 'presentation',
|
const link = path.join(`${path.sep}bigbluebutton`, 'presentation',
|
||||||
exportJob.parentMeetingId, exportJob.parentMeetingId,
|
exportJob.parentMeetingId, exportJob.parentMeetingId,
|
||||||
exportJob.presId, 'pdf', jobId, filename_with_extension);
|
exportJob.presId, 'pdf', jobId, filename);
|
||||||
|
|
||||||
const notification = {
|
const notification = {
|
||||||
envelope: {
|
envelope: {
|
||||||
@ -59,27 +59,35 @@ async function notifyMeetingActor() {
|
|||||||
client.disconnect();
|
client.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Upload PDF to a BBB room */
|
/** Upload PDF to a BBB room
|
||||||
async function upload() {
|
* @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 callbackUrl = `${config.bbbWebAPI}/bigbluebutton/presentation/${exportJob.presentationUploadToken}/upload`;
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
const file = `${exportJob.presLocation}/pdfs/${jobId}/${filename_with_extension}`;
|
|
||||||
|
|
||||||
formData.append('conference', exportJob.parentMeetingId);
|
formData.append('conference', exportJob.parentMeetingId);
|
||||||
formData.append('pod_id', config.notifier.pod_id);
|
formData.append('pod_id', config.notifier.pod_id);
|
||||||
formData.append('is_downloadable', config.notifier.is_downloadable);
|
formData.append('is_downloadable', config.notifier.is_downloadable);
|
||||||
formData.append('temporaryPresentationId', jobId);
|
formData.append('temporaryPresentationId', jobId);
|
||||||
formData.append('fileUpload', fs.createReadStream(file));
|
formData.append('fileUpload', fs.createReadStream(filePath));
|
||||||
|
|
||||||
const res = await axios.post(callbackUrl, formData,
|
try {
|
||||||
{headers: formData.getHeaders()});
|
const res = await axios.post(callbackUrl, formData,
|
||||||
logger.info(`Upload of job ${exportJob.jobId} returned ${res.data}`);
|
{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') {
|
if (jobType == 'PresentationWithAnnotationDownloadJob') {
|
||||||
notifyMeetingActor();
|
notifyMeetingActor();
|
||||||
} else if (jobType == 'PresentationWithAnnotationExportJob') {
|
} 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 {
|
} else {
|
||||||
logger.error(`Notifier received unknown job type ${jobType}`);
|
logger.error(`Notifier received unknown job type ${jobType}`);
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ const cp = require('child_process');
|
|||||||
const {Worker, workerData} = require('worker_threads');
|
const {Worker, workerData} = require('worker_threads');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const sanitize = require('sanitize-filename');
|
const sanitize = require('sanitize-filename');
|
||||||
const {getStroke, getStrokePoints} = require('perfect-freehand');
|
const {getStrokePoints, getStrokeOutlinePoints} = require('perfect-freehand');
|
||||||
const probe = require('probe-image-size');
|
const probe = require('probe-image-size');
|
||||||
|
|
||||||
const jobId = workerData;
|
const jobId = workerData;
|
||||||
@ -193,17 +193,17 @@ function get_gap(dash, size) {
|
|||||||
function get_stroke_width(dash, size) {
|
function get_stroke_width(dash, size) {
|
||||||
switch (size) {
|
switch (size) {
|
||||||
case 'small': if (dash === 'draw') {
|
case 'small': if (dash === 'draw') {
|
||||||
return 1;
|
return 2;
|
||||||
} else {
|
} else {
|
||||||
return 4;
|
return 4;
|
||||||
}
|
}
|
||||||
case 'medium': if (dash === 'draw') {
|
case 'medium': if (dash === 'draw') {
|
||||||
return 1.75;
|
return 3.5;
|
||||||
} else {
|
} else {
|
||||||
return 6.25;
|
return 6.25;
|
||||||
}
|
}
|
||||||
case 'large': if (dash === 'draw') {
|
case 'large': if (dash === 'draw') {
|
||||||
return 2.5;
|
return 5;
|
||||||
} else {
|
} else {
|
||||||
return 8.5;
|
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) {
|
* Turns an array of points into a path of quadradic curves.
|
||||||
// Gets inner path of a stroke outline
|
* @param {Array} annotationPoints
|
||||||
// For solid, dashed, and dotted types
|
* @param {Boolean} closed - whether the path end and start should be connected (default)
|
||||||
const stroke = getStrokePoints(annotationPoints)
|
* @return {Array} - an SVG quadratic curve path
|
||||||
.map((strokePoint) => strokePoint.point);
|
*/
|
||||||
|
function getSvgPath(annotationPoints, closed = true) {
|
||||||
let [max_x, max_y] = [0, 0];
|
const svgPath = annotationPoints.reduce(
|
||||||
const inner_path = stroke.reduce(
|
|
||||||
(acc, [x0, y0], i, arr) => {
|
(acc, [x0, y0], i, arr) => {
|
||||||
if (!arr[i + 1]) return acc;
|
if (!arr[i + 1]) return acc;
|
||||||
const [x1, y1] = arr[i + 1];
|
const [x1, y1] = arr[i + 1];
|
||||||
if (x1 >= max_x) {
|
acc.push(x0.toFixed(2), y0.toFixed(2), ((x0 + x1) / 2).toFixed(2), ((y0 + y1) / 2).toFixed(2));
|
||||||
max_x = x1;
|
|
||||||
}
|
|
||||||
if (y1 >= max_y) {
|
|
||||||
max_y = y1;
|
|
||||||
}
|
|
||||||
acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2);
|
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
|
|
||||||
['M', ...stroke[0], 'Q'],
|
['M', ...annotationPoints[0], 'Q'],
|
||||||
);
|
);
|
||||||
|
|
||||||
return [inner_path, max_x, max_y];
|
if (closed) svgPath.push('Z');
|
||||||
}
|
return svgPath;
|
||||||
|
|
||||||
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];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function circleFromThreePoints(A, B, C) {
|
function circleFromThreePoints(A, B, C) {
|
||||||
@ -471,49 +436,94 @@ function overlay_arrow(svg, annotation) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function overlay_draw(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 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 thickness = get_stroke_width(dash, annotation.style.size);
|
||||||
const gap = get_gap(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 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 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
|
const rotation = rad_to_degree(annotation.rotation);
|
||||||
// when path start- and end points overlap
|
const [x, y] = annotation.point;
|
||||||
const shapeIsFilled =
|
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.style.isFilled &&
|
||||||
annotation.points.length > 3 &&
|
shapePointsLength > 3 &&
|
||||||
Math.round(distance(
|
Math.round(distance(
|
||||||
annotation.points[0][0],
|
shapePoints[0][0],
|
||||||
annotation.points[0][1],
|
shapePoints[0][1],
|
||||||
annotation.points[annotation.points.length - 1][0],
|
shapePoints[shapePointsLength - 1][0],
|
||||||
annotation.points[annotation.points.length - 1][1],
|
shapePoints[shapePointsLength - 1][1],
|
||||||
)) <= 2 * get_stroke_width('solid', 'small');
|
)) <= 2 * thickness;
|
||||||
|
|
||||||
if (shapeIsFilled) {
|
if (isShapeFilled) {
|
||||||
|
const shapeArea = strokePoints.map((strokePoint) => strokePoint.point);
|
||||||
svg.ele('path', {
|
svg.ele('path', {
|
||||||
style: `fill:${shapeFillColor};`,
|
style: `fill:${shapeFillColor};`,
|
||||||
d: getPath(annotation.points)[0] + 'Z',
|
d: getSvgPath(shapeArea),
|
||||||
transform: shapeTransform,
|
transform: shapeTransform,
|
||||||
}).up();
|
}).up();
|
||||||
}
|
}
|
||||||
|
|
||||||
svg.ele('path', {
|
if (isDashDraw) {
|
||||||
style: `stroke:${shapeColor};stroke-width:${thickness};fill:${fill};${stroke_dasharray}`,
|
const strokeOutlinePoints = getStrokeOutlinePoints(strokePoints, options);
|
||||||
d: calculated_path,
|
const svgPath = getSvgPath(strokeOutlinePoints);
|
||||||
transform: shapeTransform,
|
|
||||||
});
|
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) {
|
function overlay_ellipse(svg, annotation) {
|
||||||
@ -603,19 +613,23 @@ function overlay_shape_label(svg, annotation) {
|
|||||||
|
|
||||||
render_textbox(fontColor, font, fontSize, textAlign, text, id);
|
render_textbox(fontColor, font, fontSize, textAlign, text, id);
|
||||||
|
|
||||||
const dimensions = probe.sync(fs.readFileSync(path.join(dropbox, `text${id}.png`)));
|
const shape_label = path.join(dropbox, `text${id}.png`);
|
||||||
const labelWidth = dimensions.width / config.process.textScaleFactor;
|
|
||||||
const labelHeight = dimensions.height / config.process.textScaleFactor;
|
|
||||||
|
|
||||||
svg.ele('g', {
|
if (fs.existsSync(shape_label)) {
|
||||||
transform: `rotate(${rotation} ${label_center_x} ${label_center_y})`,
|
const dimensions = probe.sync(fs.readFileSync(shape_label));
|
||||||
}).ele('image', {
|
const labelWidth = dimensions.width / config.process.textScaleFactor;
|
||||||
'x': label_center_x - (labelWidth * x_offset),
|
const labelHeight = dimensions.height / config.process.textScaleFactor;
|
||||||
'y': label_center_y - (labelHeight * y_offset),
|
|
||||||
'width': labelWidth,
|
svg.ele('g', {
|
||||||
'height': labelHeight,
|
transform: `rotate(${rotation} ${label_center_x} ${label_center_y})`,
|
||||||
'xlink:href': `file://${dropbox}/text${id}.png`,
|
}).ele('image', {
|
||||||
}).up();
|
'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) {
|
function overlay_sticky(svg, annotation) {
|
||||||
@ -712,32 +726,30 @@ function overlay_text(svg, annotation) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function overlay_annotation(svg, currentAnnotation) {
|
function overlay_annotation(svg, currentAnnotation) {
|
||||||
if (currentAnnotation.childIndex >= 1) {
|
switch (currentAnnotation.type) {
|
||||||
switch (currentAnnotation.type) {
|
case 'arrow':
|
||||||
case 'arrow':
|
overlay_arrow(svg, currentAnnotation);
|
||||||
overlay_arrow(svg, currentAnnotation);
|
break;
|
||||||
break;
|
case 'draw':
|
||||||
case 'draw':
|
overlay_draw(svg, currentAnnotation);
|
||||||
overlay_draw(svg, currentAnnotation);
|
break;
|
||||||
break;
|
case 'ellipse':
|
||||||
case 'ellipse':
|
overlay_ellipse(svg, currentAnnotation);
|
||||||
overlay_ellipse(svg, currentAnnotation);
|
break;
|
||||||
break;
|
case 'rectangle':
|
||||||
case 'rectangle':
|
overlay_rectangle(svg, currentAnnotation);
|
||||||
overlay_rectangle(svg, currentAnnotation);
|
break;
|
||||||
break;
|
case 'sticky':
|
||||||
case 'sticky':
|
overlay_sticky(svg, currentAnnotation);
|
||||||
overlay_sticky(svg, currentAnnotation);
|
break;
|
||||||
break;
|
case 'triangle':
|
||||||
case 'triangle':
|
overlay_triangle(svg, currentAnnotation);
|
||||||
overlay_triangle(svg, currentAnnotation);
|
break;
|
||||||
break;
|
case 'text':
|
||||||
case 'text':
|
overlay_text(svg, currentAnnotation);
|
||||||
overlay_text(svg, currentAnnotation);
|
break;
|
||||||
break;
|
default:
|
||||||
default:
|
logger.info(`Unknown annotation type ${currentAnnotation.type}.`);
|
||||||
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 (
|
return (
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
@ -104,7 +142,16 @@ class PollsTable extends React.Component {
|
|||||||
.sort((a, b) => ((a.createdOn > b.createdOn) ? 1 : -1))
|
.sort((a, b) => ((a.createdOn > b.createdOn) ? 1 : -1))
|
||||||
.map((poll) => (
|
.map((poll) => (
|
||||||
<td className="px-4 py-3 text-sm text-center">
|
<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
|
{ poll.anonymous
|
||||||
? (
|
? (
|
||||||
<span title={intl.formatMessage({
|
<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
|
if [ ! -z "$STUN" ]; then
|
||||||
for i in $STUN; do
|
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')"
|
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
|
# stun is from the stun-client package, which is available on both bionic and focal
|
||||||
if stunclient --mode full --localport 30000 $STUN_SERVER | grep -q "fail\|Unable\ to\ resolve"; then
|
# 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 "#"
|
echo "#"
|
||||||
echo "# Warning: Failed to verify STUN server at $STUN_SERVER with command"
|
echo "# Warning: Failed to verify STUN server at $STUN_SERVER with command"
|
||||||
echo "#"
|
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 "#"
|
echo "#"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
@ -1,20 +1,27 @@
|
|||||||
import { check } from 'meteor/check';
|
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(meetingId, String);
|
||||||
check(whiteboardId, String);
|
check(whiteboardId, String);
|
||||||
check(annotation, Object);
|
check(annotation, Object);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
id, annotationInfo, wbId,
|
id, wbId,
|
||||||
} = annotation;
|
} = annotation;
|
||||||
|
|
||||||
|
let { annotationInfo } = annotation;
|
||||||
|
|
||||||
const selector = {
|
const selector = {
|
||||||
meetingId,
|
meetingId,
|
||||||
id,
|
id,
|
||||||
userId,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const oldAnnotation = Annotations.findOne(selector);
|
||||||
|
if (oldAnnotation) {
|
||||||
|
annotationInfo = _.merge(oldAnnotation.annotationInfo, annotationInfo)
|
||||||
|
}
|
||||||
|
|
||||||
const modifier = {
|
const modifier = {
|
||||||
$set: {
|
$set: {
|
||||||
whiteboardId,
|
whiteboardId,
|
||||||
|
@ -8,7 +8,7 @@ export default function addAnnotation(meetingId, whiteboardId, userId, annotatio
|
|||||||
check(whiteboardId, String);
|
check(whiteboardId, String);
|
||||||
check(annotation, Object);
|
check(annotation, Object);
|
||||||
|
|
||||||
const query = addAnnotationQuery(meetingId, whiteboardId, userId, annotation);
|
const query = addAnnotationQuery(meetingId, whiteboardId, userId, annotation, Annotations);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { insertedId } = Annotations.upsert(query.selector, query.modifier);
|
const { insertedId } = Annotations.upsert(query.selector, query.modifier);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import Settings from '/imports/ui/services/settings';
|
import Settings from '/imports/ui/services/settings';
|
||||||
import logger from '/imports/startup/client/logger';
|
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 AUDIO_SESSION_NUM_KEY = 'AudioSessionNumber';
|
||||||
const DEFAULT_INPUT_DEVICE_ID = '';
|
const DEFAULT_INPUT_DEVICE_ID = '';
|
||||||
@ -38,10 +38,10 @@ const getCurrentAudioSinkId = () => {
|
|||||||
return audioElement?.sinkId || DEFAULT_OUTPUT_DEVICE_ID;
|
return audioElement?.sinkId || DEFAULT_OUTPUT_DEVICE_ID;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStoredAudioInputDeviceId = () => Storage.getItem(INPUT_DEVICE_ID_KEY);
|
const getStoredAudioInputDeviceId = () => BBBStorage.getItem(INPUT_DEVICE_ID_KEY);
|
||||||
const getStoredAudioOutputDeviceId = () => Storage.getItem(OUTPUT_DEVICE_ID_KEY);
|
const getStoredAudioOutputDeviceId = () => BBBStorage.getItem(OUTPUT_DEVICE_ID_KEY);
|
||||||
const storeAudioInputDeviceId = (deviceId) => Storage.setItem(INPUT_DEVICE_ID_KEY, deviceId);
|
const storeAudioInputDeviceId = (deviceId) => BBBStorage.setItem(INPUT_DEVICE_ID_KEY, deviceId);
|
||||||
const storeAudioOutputDeviceId = (deviceId) => Storage.setItem(OUTPUT_DEVICE_ID_KEY, deviceId);
|
const storeAudioOutputDeviceId = (deviceId) => BBBStorage.setItem(OUTPUT_DEVICE_ID_KEY, deviceId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter constraints set in audioDeviceConstraints, based on
|
* 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 { extractCredentials } from '/imports/api/common/server/helpers';
|
||||||
import { check } from 'meteor/check';
|
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 REDIS_CONFIG = Meteor.settings.private.redis;
|
||||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||||
const BREAKOUT_LIM = Meteor.settings.public.app.breakouts.breakoutRoomLimit;
|
const BREAKOUT_LIM = Meteor.settings.public.app.breakouts.breakoutRoomLimit;
|
||||||
@ -24,6 +24,8 @@ export default function createBreakoutRoom(rooms, durationInMinutes, record = fa
|
|||||||
}
|
}
|
||||||
const payload = {
|
const payload = {
|
||||||
record,
|
record,
|
||||||
|
captureNotes,
|
||||||
|
captureSlides,
|
||||||
durationInMinutes,
|
durationInMinutes,
|
||||||
rooms,
|
rooms,
|
||||||
meetingId,
|
meetingId,
|
||||||
|
@ -74,6 +74,8 @@ export default function addMeeting(meeting) {
|
|||||||
parentId: String,
|
parentId: String,
|
||||||
record: Boolean,
|
record: Boolean,
|
||||||
privateChatEnabled: Boolean,
|
privateChatEnabled: Boolean,
|
||||||
|
captureNotes: Boolean,
|
||||||
|
captureSlides: Boolean,
|
||||||
},
|
},
|
||||||
meetingProp: {
|
meetingProp: {
|
||||||
intId: String,
|
intId: String,
|
||||||
@ -88,11 +90,12 @@ export default function addMeeting(meeting) {
|
|||||||
uploadExternalUrl: String,
|
uploadExternalUrl: String,
|
||||||
},
|
},
|
||||||
usersProp: {
|
usersProp: {
|
||||||
|
maxUsers: Number,
|
||||||
|
maxUserConcurrentAccesses: Number,
|
||||||
webcamsOnlyForModerator: Boolean,
|
webcamsOnlyForModerator: Boolean,
|
||||||
userCameraCap: Number,
|
userCameraCap: Number,
|
||||||
guestPolicy: String,
|
guestPolicy: String,
|
||||||
authenticatedGuest: Boolean,
|
authenticatedGuest: Boolean,
|
||||||
maxUsers: Number,
|
|
||||||
allowModsToUnmuteUsers: Boolean,
|
allowModsToUnmuteUsers: Boolean,
|
||||||
allowModsToEjectCameras: Boolean,
|
allowModsToEjectCameras: Boolean,
|
||||||
meetingLayout: String,
|
meetingLayout: String,
|
||||||
|
@ -6,6 +6,7 @@ import padUpdated from './handlers/padUpdated';
|
|||||||
import padContent from './handlers/padContent';
|
import padContent from './handlers/padContent';
|
||||||
import padTail from './handlers/padTail';
|
import padTail from './handlers/padTail';
|
||||||
import sessionDeleted from './handlers/sessionDeleted';
|
import sessionDeleted from './handlers/sessionDeleted';
|
||||||
|
import captureSharedNotes from './handlers/captureSharedNotes';
|
||||||
|
|
||||||
RedisPubSub.on('PadGroupCreatedRespMsg', groupCreated);
|
RedisPubSub.on('PadGroupCreatedRespMsg', groupCreated);
|
||||||
RedisPubSub.on('PadCreatedRespMsg', padCreated);
|
RedisPubSub.on('PadCreatedRespMsg', padCreated);
|
||||||
@ -14,3 +15,4 @@ RedisPubSub.on('PadUpdatedEvtMsg', padUpdated);
|
|||||||
RedisPubSub.on('PadContentEvtMsg', padContent);
|
RedisPubSub.on('PadContentEvtMsg', padContent);
|
||||||
RedisPubSub.on('PadTailEvtMsg', padTail);
|
RedisPubSub.on('PadTailEvtMsg', padTail);
|
||||||
RedisPubSub.on('PadSessionDeletedEvtMsg', sessionDeleted);
|
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,
|
meetingEndedReason,
|
||||||
meetingIsBreakout,
|
meetingIsBreakout,
|
||||||
subscriptionsReady,
|
subscriptionsReady,
|
||||||
User,
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if ((loading || !subscriptionsReady) && !meetingHasEnded && meetingExist) {
|
if ((loading || !subscriptionsReady) && !meetingHasEnded && meetingExist) {
|
||||||
return (<LoadingScreen>{loading}</LoadingScreen>);
|
return (<LoadingScreen>{loading}</LoadingScreen>);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (meetingIsBreakout && (ejected || userRemoved)) {
|
|
||||||
Base.setExitReason('removedFromBreakout').finally(() => {
|
|
||||||
Meteor.disconnect();
|
|
||||||
window.close();
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ejected) {
|
if (ejected) {
|
||||||
return (
|
return (
|
||||||
<MeetingEnded
|
<MeetingEnded
|
||||||
@ -281,21 +272,19 @@ class Base extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((meetingHasEnded || User?.loggedOut) && meetingIsBreakout) {
|
if (meetingHasEnded && meetingIsBreakout) {
|
||||||
const reason = meetingHasEnded ? 'breakoutEnded' : 'logout';
|
Base.setExitReason('breakoutEnded').finally(() => {
|
||||||
Base.setExitReason(reason).finally(() => {
|
|
||||||
Meteor.disconnect();
|
Meteor.disconnect();
|
||||||
window.close();
|
window.close();
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((meetingHasEnded && !meetingIsBreakout) || (codeError && User?.loggedOut)) {
|
if (meetingHasEnded && !meetingIsBreakout) {
|
||||||
return (
|
return (
|
||||||
<MeetingEnded
|
<MeetingEnded
|
||||||
code={codeError}
|
code={codeError}
|
||||||
endedReason={meetingEndedReason}
|
endedReason={meetingEndedReason}
|
||||||
ejectedReason={ejectedReason}
|
|
||||||
callback={() => Base.setExitReason('meetingEnded')}
|
callback={() => Base.setExitReason('meetingEnded')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -9,6 +9,7 @@ import _ from 'lodash';
|
|||||||
import { Session } from 'meteor/session';
|
import { Session } from 'meteor/session';
|
||||||
import Logger from '/imports/startup/client/logger';
|
import Logger from '/imports/startup/client/logger';
|
||||||
import { formatLocaleCode } from '/imports/utils/string-utils';
|
import { formatLocaleCode } from '/imports/utils/string-utils';
|
||||||
|
import Intl from '/imports/ui/services/locale';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
locale: PropTypes.string,
|
locale: PropTypes.string,
|
||||||
@ -66,6 +67,7 @@ class IntlStartup extends Component {
|
|||||||
const url = `./locale?locale=${locale}&init=${init}`;
|
const url = `./locale?locale=${locale}&init=${init}`;
|
||||||
const localesPath = 'locales';
|
const localesPath = 'locales';
|
||||||
|
|
||||||
|
Intl.fetching = true;
|
||||||
this.setState({ fetching: true }, () => {
|
this.setState({ fetching: true }, () => {
|
||||||
fetch(url)
|
fetch(url)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -138,6 +140,7 @@ class IntlStartup extends Component {
|
|||||||
|
|
||||||
const dasherizedLocale = normalizedLocale.replace('_', '-');
|
const dasherizedLocale = normalizedLocale.replace('_', '-');
|
||||||
const { language, formattedLocale } = formatLocaleCode(dasherizedLocale);
|
const { language, formattedLocale } = formatLocaleCode(dasherizedLocale);
|
||||||
|
Intl.setLocale(formattedLocale, mergedMessages);
|
||||||
|
|
||||||
this.setState({ messages: mergedMessages, fetching: false, normalizedLocale: dasherizedLocale }, () => {
|
this.setState({ messages: mergedMessages, fetching: false, normalizedLocale: dasherizedLocale }, () => {
|
||||||
Settings.application.locale = dasherizedLocale;
|
Settings.application.locale = dasherizedLocale;
|
||||||
|
@ -318,7 +318,7 @@ class ActionsDropdown extends PureComponent {
|
|||||||
}
|
}
|
||||||
actions={children}
|
actions={children}
|
||||||
opts={{
|
opts={{
|
||||||
id: "default-dropdown-menu",
|
id: "actions-dropdown-menu",
|
||||||
keepMounted: true,
|
keepMounted: true,
|
||||||
transitionDuration: 0,
|
transitionDuration: 0,
|
||||||
elevation: 3,
|
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 SortList from './sort-user-list/component';
|
||||||
import Styled from './styles';
|
import Styled from './styles';
|
||||||
import Icon from '/imports/ui/components/common/icon/component.jsx';
|
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;
|
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
|
||||||
|
|
||||||
@ -82,6 +84,14 @@ const intlMessages = defineMessages({
|
|||||||
id: 'app.createBreakoutRoom.freeJoin',
|
id: 'app.createBreakoutRoom.freeJoin',
|
||||||
description: 'free join label',
|
description: 'free join label',
|
||||||
},
|
},
|
||||||
|
captureNotesLabel: {
|
||||||
|
id: 'app.createBreakoutRoom.captureNotes',
|
||||||
|
description: 'capture shared notes label',
|
||||||
|
},
|
||||||
|
captureSlidesLabel: {
|
||||||
|
id: 'app.createBreakoutRoom.captureSlides',
|
||||||
|
description: 'capture slides label',
|
||||||
|
},
|
||||||
roomLabel: {
|
roomLabel: {
|
||||||
id: 'app.createBreakoutRoom.room',
|
id: 'app.createBreakoutRoom.room',
|
||||||
description: 'Room label',
|
description: 'Room label',
|
||||||
@ -150,6 +160,10 @@ const intlMessages = defineMessages({
|
|||||||
id: 'app.createBreakoutRoom.roomNameInputDesc',
|
id: 'app.createBreakoutRoom.roomNameInputDesc',
|
||||||
description: 'aria description for room name change',
|
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;
|
const BREAKOUT_LIM = Meteor.settings.public.app.breakouts.breakoutRoomLimit;
|
||||||
@ -200,6 +214,8 @@ class BreakoutRoom extends PureComponent {
|
|||||||
this.handleDismiss = this.handleDismiss.bind(this);
|
this.handleDismiss = this.handleDismiss.bind(this);
|
||||||
this.setInvitationConfig = this.setInvitationConfig.bind(this);
|
this.setInvitationConfig = this.setInvitationConfig.bind(this);
|
||||||
this.setRecord = this.setRecord.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.blurDurationTime = this.blurDurationTime.bind(this);
|
||||||
this.removeRoomUsers = this.removeRoomUsers.bind(this);
|
this.removeRoomUsers = this.removeRoomUsers.bind(this);
|
||||||
this.renderErrorMessages = this.renderErrorMessages.bind(this);
|
this.renderErrorMessages = this.renderErrorMessages.bind(this);
|
||||||
@ -220,6 +236,8 @@ class BreakoutRoom extends PureComponent {
|
|||||||
roomNameDuplicatedIsValid: true,
|
roomNameDuplicatedIsValid: true,
|
||||||
roomNameEmptyIsValid: true,
|
roomNameEmptyIsValid: true,
|
||||||
record: false,
|
record: false,
|
||||||
|
captureNotes: false,
|
||||||
|
captureSlides: false,
|
||||||
durationIsValid: true,
|
durationIsValid: true,
|
||||||
breakoutJoinedUsers: null,
|
breakoutJoinedUsers: null,
|
||||||
};
|
};
|
||||||
@ -310,6 +328,7 @@ class BreakoutRoom extends PureComponent {
|
|||||||
users.forEach((u, index) => {
|
users.forEach((u, index) => {
|
||||||
if (`roomUserItem-${u.userId}` === document.activeElement.id) {
|
if (`roomUserItem-${u.userId}` === document.activeElement.id) {
|
||||||
users[index].room = text.substr(text.length - 1).includes(')') ? 0 : parseInt(roomNumber, 10);
|
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,
|
users,
|
||||||
freeJoin,
|
freeJoin,
|
||||||
record,
|
record,
|
||||||
|
captureNotes,
|
||||||
|
captureSlides,
|
||||||
numberOfRoomsIsValid,
|
numberOfRoomsIsValid,
|
||||||
numberOfRooms,
|
numberOfRooms,
|
||||||
durationTime,
|
durationTime,
|
||||||
@ -430,7 +451,7 @@ class BreakoutRoom extends PureComponent {
|
|||||||
sequence: seq,
|
sequence: seq,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
createBreakoutRoom(rooms, durationTime, record);
|
createBreakoutRoom(rooms, durationTime, record, captureNotes, captureSlides);
|
||||||
Session.set('isUserListOpen', true);
|
Session.set('isUserListOpen', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -567,6 +588,14 @@ class BreakoutRoom extends PureComponent {
|
|||||||
this.setState({ record: e.target.checked });
|
this.setState({ record: e.target.checked });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCaptureNotes(e) {
|
||||||
|
this.setState({ captureNotes: e.target.checked });
|
||||||
|
}
|
||||||
|
|
||||||
|
setCaptureSlides(e) {
|
||||||
|
this.setState({ captureSlides: e.target.checked });
|
||||||
|
}
|
||||||
|
|
||||||
getUserByRoom(room) {
|
getUserByRoom(room) {
|
||||||
const { users } = this.state;
|
const { users } = this.state;
|
||||||
return users.filter((user) => user.room === room);
|
return users.filter((user) => user.room === room);
|
||||||
@ -602,17 +631,24 @@ class BreakoutRoom extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
changeUserRoom(userId, room) {
|
changeUserRoom(userId, room) {
|
||||||
|
const { intl } = this.props;
|
||||||
const { users, freeJoin } = this.state;
|
const { users, freeJoin } = this.state;
|
||||||
|
|
||||||
const idxUser = users.findIndex((user) => user.userId === userId.replace('roomUserItem-', ''));
|
const idxUser = users.findIndex((user) => user.userId === userId.replace('roomUserItem-', ''));
|
||||||
|
|
||||||
const usersCopy = [...users];
|
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({
|
this.setState({
|
||||||
users: usersCopy,
|
users: usersCopy,
|
||||||
leastOneUserIsValid: (this.getUserByRoom(0).length !== users.length || freeJoin),
|
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 (
|
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.Alert valid={leastOneUserIsValid} role="alert">
|
||||||
<Styled.FreeJoinLabel>
|
<Styled.FreeJoinLabel>
|
||||||
<Styled.BreakoutNameInput
|
<Styled.BreakoutNameInput
|
||||||
@ -797,7 +833,7 @@ class BreakoutRoom extends PureComponent {
|
|||||||
<Styled.BreakoutBox id="breakoutBox-0" onDrop={drop(0)} onDragOver={allowDrop} tabIndex={0}>
|
<Styled.BreakoutBox id="breakoutBox-0" onDrop={drop(0)} onDragOver={allowDrop} tabIndex={0}>
|
||||||
{this.renderUserItemByRoom(0)}
|
{this.renderUserItemByRoom(0)}
|
||||||
</Styled.BreakoutBox>
|
</Styled.BreakoutBox>
|
||||||
<Styled.SpanWarn valid={leastOneUserIsValid}>
|
<Styled.SpanWarn data-test="warningNoUserAssigned" valid={leastOneUserIsValid}>
|
||||||
{intl.formatMessage(intlMessages.leastOneWarnBreakout)}
|
{intl.formatMessage(intlMessages.leastOneWarnBreakout)}
|
||||||
</Styled.SpanWarn>
|
</Styled.SpanWarn>
|
||||||
</Styled.Alert>
|
</Styled.Alert>
|
||||||
@ -814,6 +850,7 @@ class BreakoutRoom extends PureComponent {
|
|||||||
onBlur={changeRoomName(value)}
|
onBlur={changeRoomName(value)}
|
||||||
aria-label={`${this.getRoomName(value)}`}
|
aria-label={`${this.getRoomName(value)}`}
|
||||||
aria-describedby={this.getRoomName(value).length === 0 ? `room-error-${value}` : `room-input-${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}
|
readOnly={isUpdate}
|
||||||
/>
|
/>
|
||||||
<div aria-hidden id={`room-input-${value}`} className="sr-only">
|
<div aria-hidden id={`room-input-${value}`} className="sr-only">
|
||||||
@ -885,6 +922,7 @@ class BreakoutRoom extends PureComponent {
|
|||||||
onChange={this.changeDurationTime}
|
onChange={this.changeDurationTime}
|
||||||
onBlur={this.blurDurationTime}
|
onBlur={this.blurDurationTime}
|
||||||
aria-label={intl.formatMessage(intlMessages.duration)}
|
aria-label={intl.formatMessage(intlMessages.duration)}
|
||||||
|
data-test="durationTime"
|
||||||
/>
|
/>
|
||||||
<Styled.HoldButtonWrapper
|
<Styled.HoldButtonWrapper
|
||||||
key="decrease-breakout-time"
|
key="decrease-breakout-time"
|
||||||
@ -902,6 +940,7 @@ class BreakoutRoom extends PureComponent {
|
|||||||
hideLabel
|
hideLabel
|
||||||
circle
|
circle
|
||||||
size="sm"
|
size="sm"
|
||||||
|
data-test="decreaseBreakoutTime"
|
||||||
/>
|
/>
|
||||||
</Styled.HoldButtonWrapper>
|
</Styled.HoldButtonWrapper>
|
||||||
<Styled.HoldButtonWrapper
|
<Styled.HoldButtonWrapper
|
||||||
@ -918,6 +957,7 @@ class BreakoutRoom extends PureComponent {
|
|||||||
hideLabel
|
hideLabel
|
||||||
circle
|
circle
|
||||||
size="sm"
|
size="sm"
|
||||||
|
data-test="increaseBreakoutTime"
|
||||||
/>
|
/>
|
||||||
</Styled.HoldButtonWrapper>
|
</Styled.HoldButtonWrapper>
|
||||||
</Styled.DurationArea>
|
</Styled.DurationArea>
|
||||||
@ -979,11 +1019,15 @@ class BreakoutRoom extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderCheckboxes() {
|
renderCheckboxes() {
|
||||||
const { intl, isUpdate, isBreakoutRecordable } = this.props;
|
const {
|
||||||
|
intl, isUpdate, isBreakoutRecordable,
|
||||||
|
} = this.props;
|
||||||
if (isUpdate) return null;
|
if (isUpdate) return null;
|
||||||
const {
|
const {
|
||||||
freeJoin,
|
freeJoin,
|
||||||
record,
|
record,
|
||||||
|
captureNotes,
|
||||||
|
captureSlides,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
return (
|
return (
|
||||||
<Styled.CheckBoxesContainer key="breakout-checkboxes">
|
<Styled.CheckBoxesContainer key="breakout-checkboxes">
|
||||||
@ -1013,6 +1057,38 @@ class BreakoutRoom extends PureComponent {
|
|||||||
</Styled.FreeJoinLabel>
|
</Styled.FreeJoinLabel>
|
||||||
) : null
|
) : 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>
|
</Styled.CheckBoxesContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { defineMessages } from 'react-intl';
|
import { defineMessages } from 'react-intl';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
@ -8,6 +8,7 @@ import { PANELS, ACTIONS } from '../../layout/enums';
|
|||||||
|
|
||||||
const POLL_SETTINGS = Meteor.settings.public.poll;
|
const POLL_SETTINGS = Meteor.settings.public.poll;
|
||||||
const MAX_CUSTOM_FIELDS = POLL_SETTINGS.maxCustom;
|
const MAX_CUSTOM_FIELDS = POLL_SETTINGS.maxCustom;
|
||||||
|
const CANCELED_POLL_DELAY = 250;
|
||||||
|
|
||||||
const intlMessages = defineMessages({
|
const intlMessages = defineMessages({
|
||||||
quickPollLabel: {
|
quickPollLabel: {
|
||||||
@ -34,6 +35,10 @@ const intlMessages = defineMessages({
|
|||||||
id: 'app.poll.abstention',
|
id: 'app.poll.abstention',
|
||||||
description: 'Poll Abstention option value',
|
description: 'Poll Abstention option value',
|
||||||
},
|
},
|
||||||
|
typedRespLabel: {
|
||||||
|
id: 'app.poll.userResponse.label',
|
||||||
|
description: 'quick poll typed response label',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
@ -44,172 +49,218 @@ const propTypes = {
|
|||||||
amIPresenter: PropTypes.bool.isRequired,
|
amIPresenter: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClickQuickPoll = (layoutContextDispatch) => {
|
const QuickPollDropdown = (props) => {
|
||||||
layoutContextDispatch({
|
const {
|
||||||
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
amIPresenter,
|
||||||
value: true,
|
intl,
|
||||||
});
|
parseCurrentSlideContent,
|
||||||
layoutContextDispatch({
|
startPoll,
|
||||||
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
stopPoll,
|
||||||
value: PANELS.POLL,
|
currentSlide,
|
||||||
});
|
activePoll,
|
||||||
Session.set('forcePollOpen', true);
|
className,
|
||||||
Session.set('pollInitiated', true);
|
layoutContextDispatch,
|
||||||
};
|
pollTypes,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const getAvailableQuickPolls = (
|
const parsedSlide = parseCurrentSlideContent(
|
||||||
slideId, parsedSlides, startPoll, pollTypes, layoutContextDispatch,
|
intl.formatMessage(intlMessages.yesOptionLabel),
|
||||||
) => {
|
intl.formatMessage(intlMessages.noOptionLabel),
|
||||||
const pollItemElements = parsedSlides.map((poll) => {
|
intl.formatMessage(intlMessages.abstentionOptionLabel),
|
||||||
const { poll: label } = poll;
|
intl.formatMessage(intlMessages.trueOptionLabel),
|
||||||
const { type } = poll;
|
intl.formatMessage(intlMessages.falseOptionLabel),
|
||||||
let itemLabel = label;
|
);
|
||||||
const letterAnswers = [];
|
|
||||||
|
|
||||||
if (type !== pollTypes.YesNo
|
const {
|
||||||
&& type !== pollTypes.YesNoAbstention
|
slideId, quickPollOptions, optionsWithLabels, pollQuestion,
|
||||||
&& type !== pollTypes.TrueFalse) {
|
} = parsedSlide;
|
||||||
const { options } = itemLabel;
|
|
||||||
itemLabel = options.join('/').replace(/[\n.)]/g, '');
|
const handleClickQuickPoll = (lCDispatch) => {
|
||||||
if (type === pollTypes.Custom) {
|
lCDispatch({
|
||||||
for (let i = 0; i < options.length; i += 1) {
|
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||||
const letterOption = options[i]?.replace(/[\r.)]/g, '').toUpperCase();
|
value: true,
|
||||||
if (letterAnswers.length < MAX_CUSTOM_FIELDS) {
|
});
|
||||||
letterAnswers.push(letterOption);
|
lCDispatch({
|
||||||
} else {
|
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||||
break;
|
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
|
// removes any whitespace from the label
|
||||||
itemLabel = itemLabel?.replace(/\s+/g, '').toUpperCase();
|
itemLabel = itemLabel?.replace(/\s+/g, '').toUpperCase();
|
||||||
|
|
||||||
const numChars = {
|
const numChars = {
|
||||||
1: 'A', 2: 'B', 3: 'C', 4: 'D', 5: 'E',
|
1: 'A', 2: 'B', 3: 'C', 4: 'D', 5: 'E',
|
||||||
};
|
};
|
||||||
itemLabel = itemLabel.split('').map((c) => {
|
itemLabel = itemLabel.split('').map((c) => {
|
||||||
if (numChars[c]) return numChars[c];
|
if (numChars[c]) return numChars[c];
|
||||||
return c;
|
return c;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown.DropdownListItem
|
<Dropdown.DropdownListItem
|
||||||
label={itemLabel}
|
label={itemLabel}
|
||||||
key={_.uniqueId('quick-poll-item')}
|
key={_.uniqueId('quick-poll-item')}
|
||||||
onClick={() => {
|
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);
|
handleClickQuickPoll(layoutContextDispatch);
|
||||||
startPoll(type, slideId, letterAnswers);
|
if (singlePollType === 'R-' || singlePollType === 'TF') {
|
||||||
}}
|
startPoll(singlePollType, currentSlide.id, answers, pollQuestion, multiResponse);
|
||||||
answers={letterAnswers}
|
} else {
|
||||||
/>
|
startPoll(
|
||||||
);
|
pollTypes.Custom,
|
||||||
});
|
currentSlide.id,
|
||||||
|
optionsWithLabels,
|
||||||
|
pollQuestion,
|
||||||
|
multiResponse,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, CANCELED_POLL_DELAY);
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
data-test="quickPollBtn"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
const sizes = [];
|
const usePollDropdown = quickPollOptions && quickPollOptions.length && quickPolls.length > 1;
|
||||||
return pollItemElements.filter((el) => {
|
let dropdown = null;
|
||||||
const { label } = el.props;
|
|
||||||
if (label.length === sizes[sizes.length - 1]) return false;
|
|
||||||
sizes.push(label.length);
|
|
||||||
return el;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
class QuickPollDropdown extends Component {
|
if (usePollDropdown) {
|
||||||
render() {
|
btn = (
|
||||||
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 = (
|
|
||||||
<Styled.QuickPollButton
|
<Styled.QuickPollButton
|
||||||
aria-label={intl.formatMessage(intlMessages.quickPollLabel)}
|
aria-label={intl.formatMessage(intlMessages.quickPollLabel)}
|
||||||
label={quickPollLabel}
|
label={quickPollLabel}
|
||||||
tooltipLabel={intl.formatMessage(intlMessages.quickPollLabel)}
|
tooltipLabel={intl.formatMessage(intlMessages.quickPollLabel)}
|
||||||
onClick={() => {
|
onClick={() => null}
|
||||||
handleClickQuickPoll(layoutContextDispatch);
|
|
||||||
startPoll(singlePollType, currentSlide.id, answers);
|
|
||||||
}}
|
|
||||||
size="lg"
|
size="lg"
|
||||||
disabled={!!activePoll}
|
|
||||||
data-test="quickPollBtn"
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const usePollDropdown = quickPollOptions && quickPollOptions.length && quickPolls.length > 1;
|
dropdown = (
|
||||||
let dropdown = null;
|
<Dropdown className={className}>
|
||||||
|
<Dropdown.DropdownTrigger tabIndex={0}>
|
||||||
if (usePollDropdown) {
|
{btn}
|
||||||
btn = (
|
</Dropdown.DropdownTrigger>
|
||||||
<Styled.QuickPollButton
|
<Dropdown.DropdownContent>
|
||||||
aria-label={intl.formatMessage(intlMessages.quickPollLabel)}
|
<Dropdown.DropdownList>
|
||||||
label={quickPollLabel}
|
{quickPolls}
|
||||||
tooltipLabel={intl.formatMessage(intlMessages.quickPollLabel)}
|
</Dropdown.DropdownList>
|
||||||
onClick={() => null}
|
</Dropdown.DropdownContent>
|
||||||
size="lg"
|
</Dropdown>
|
||||||
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
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return amIPresenter && usePollDropdown ? (
|
||||||
|
dropdown
|
||||||
|
) : (
|
||||||
|
btn
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
QuickPollDropdown.propTypes = propTypes;
|
QuickPollDropdown.propTypes = propTypes;
|
||||||
|
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { withTracker } from 'meteor/react-meteor-data';
|
import { withTracker } from 'meteor/react-meteor-data';
|
||||||
import { injectIntl } from 'react-intl';
|
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 QuickPollDropdown from './component';
|
||||||
import { layoutDispatch } from '../../layout/context';
|
import { layoutDispatch } from '../../layout/context';
|
||||||
import PollService from '/imports/ui/components/poll/service';
|
|
||||||
|
|
||||||
const QuickPollDropdownContainer = (props) => {
|
const QuickPollDropdownContainer = (props) => {
|
||||||
const layoutContextDispatch = layoutDispatch();
|
const layoutContextDispatch = layoutDispatch();
|
||||||
|
|
||||||
return <QuickPollDropdown {...{ layoutContextDispatch, ...props }} />;
|
return <QuickPollDropdown {...{ layoutContextDispatch, ...props }} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withTracker(() => ({
|
export default withTracker(() => ({
|
||||||
activePoll: Session.get('pollInitiated') || false,
|
activePoll: Session.get('pollInitiated') || false,
|
||||||
pollTypes: PollService.pollTypes,
|
pollTypes: PollService.pollTypes,
|
||||||
|
stopPoll: () => makeCall('stopPoll'),
|
||||||
}))(injectIntl(QuickPollDropdownContainer));
|
}))(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';
|
import Button from '/imports/ui/components/common/button/component';
|
||||||
|
|
||||||
const { isMobile } = deviceInfo;
|
const { isMobile } = deviceInfo;
|
||||||
const { isSafari, isMobileApp } = browserInfo;
|
const { isSafari, isTabletApp } = browserInfo;
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
intl: PropTypes.objectOf(Object).isRequired,
|
intl: PropTypes.objectOf(Object).isRequired,
|
||||||
@ -163,7 +163,7 @@ const ScreenshareButton = ({
|
|||||||
? intlMessages.stopDesktopShareDesc : intlMessages.desktopShareDesc;
|
? intlMessages.stopDesktopShareDesc : intlMessages.desktopShareDesc;
|
||||||
|
|
||||||
const shouldAllowScreensharing = enabled
|
const shouldAllowScreensharing = enabled
|
||||||
&& ( !isMobile || isMobileApp)
|
&& ( !isMobile || isTabletApp)
|
||||||
&& amIPresenter;
|
&& amIPresenter;
|
||||||
|
|
||||||
const dataTest = isVideoBroadcasting ? 'stopScreenShare' : 'startScreenShare';
|
const dataTest = isVideoBroadcasting ? 'stopScreenShare' : 'startScreenShare';
|
||||||
|
@ -63,7 +63,7 @@ export default {
|
|||||||
isBreakoutRecordable: () => Meetings.findOne({ meetingId: Auth.meetingID },
|
isBreakoutRecordable: () => Meetings.findOne({ meetingId: Auth.meetingID },
|
||||||
{ fields: { 'breakoutProps.record': 1 } }).breakoutProps.record,
|
{ fields: { 'breakoutProps.record': 1 } }).breakoutProps.record,
|
||||||
toggleRecording: () => makeCall('toggleRecording'),
|
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 }),
|
sendInvitation: (breakoutId, userId) => makeCall('requestJoinURL', { breakoutId, userId }),
|
||||||
breakoutJoinedUsers: () => Breakouts.find({
|
breakoutJoinedUsers: () => Breakouts.find({
|
||||||
joinedUsers: { $exists: true },
|
joinedUsers: { $exists: true },
|
||||||
|
@ -81,6 +81,8 @@ class AudioControls extends PureComponent {
|
|||||||
handleLeaveAudio, handleToggleMuteMicrophone, muted, disable, talking,
|
handleLeaveAudio, handleToggleMuteMicrophone, muted, disable, talking,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const { isMobile } = deviceInfo;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputStreamLiveSelectorContainer {...{
|
<InputStreamLiveSelectorContainer {...{
|
||||||
handleLeaveAudio,
|
handleLeaveAudio,
|
||||||
@ -88,6 +90,7 @@ class AudioControls extends PureComponent {
|
|||||||
muted,
|
muted,
|
||||||
disable,
|
disable,
|
||||||
talking,
|
talking,
|
||||||
|
isMobile,
|
||||||
_enableDynamicDeviceSelection,
|
_enableDynamicDeviceSelection,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -366,8 +366,8 @@ class InputStreamLiveSelector extends Component {
|
|||||||
currentInputDeviceId,
|
currentInputDeviceId,
|
||||||
currentOutputDeviceId,
|
currentOutputDeviceId,
|
||||||
isListenOnly,
|
isListenOnly,
|
||||||
isRTL,
|
|
||||||
shortcuts,
|
shortcuts,
|
||||||
|
isMobile,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const inputDeviceList = !isListenOnly
|
const inputDeviceList = !isListenOnly
|
||||||
@ -399,6 +399,7 @@ class InputStreamLiveSelector extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const dropdownListComplete = inputDeviceList.concat(outputDeviceList).concat(leaveAudioOption);
|
const dropdownListComplete = inputDeviceList.concat(outputDeviceList).concat(leaveAudioOption);
|
||||||
|
const customStyles = { top: '-1rem' };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -411,6 +412,7 @@ class InputStreamLiveSelector extends Component {
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<BBBMenu
|
<BBBMenu
|
||||||
|
customStyles={!isMobile ? customStyles : null}
|
||||||
trigger={(
|
trigger={(
|
||||||
<>
|
<>
|
||||||
{isListenOnly
|
{isListenOnly
|
||||||
@ -428,14 +430,14 @@ class InputStreamLiveSelector extends Component {
|
|||||||
)}
|
)}
|
||||||
actions={dropdownListComplete}
|
actions={dropdownListComplete}
|
||||||
opts={{
|
opts={{
|
||||||
id: 'default-dropdown-menu',
|
id: 'audio-selector-dropdown-menu',
|
||||||
keepMounted: true,
|
keepMounted: true,
|
||||||
transitionDuration: 0,
|
transitionDuration: 0,
|
||||||
elevation: 3,
|
elevation: 3,
|
||||||
getContentAnchorEl: null,
|
getContentAnchorEl: null,
|
||||||
fullwidth: 'true',
|
fullwidth: 'true',
|
||||||
anchorOrigin: { vertical: 'top', horizontal: isRTL ? 'left' : 'right' },
|
anchorOrigin: { vertical: 'top', horizontal: 'center' },
|
||||||
transformOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' },
|
transformOrigin: { vertical: 'bottom', horizontal: 'center'},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
@ -94,7 +94,7 @@ const ConnectingAnimation = styled.span`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const AudioModal = styled(Modal)`
|
const AudioModal = styled(Modal)`
|
||||||
padding: 1.5rem;
|
padding: 1rem;
|
||||||
min-height: 20rem;
|
min-height: 20rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -98,6 +98,7 @@ class AudioSettings extends React.Component {
|
|||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { inputDeviceId, outputDeviceId } = this.state;
|
const { inputDeviceId, outputDeviceId } = this.state;
|
||||||
|
|
||||||
|
Session.set('inEchoTest', true);
|
||||||
this._isMounted = true;
|
this._isMounted = true;
|
||||||
// Guarantee initial in/out devices are initialized on all ends
|
// Guarantee initial in/out devices are initialized on all ends
|
||||||
this.setInputDevice(inputDeviceId);
|
this.setInputDevice(inputDeviceId);
|
||||||
@ -107,6 +108,7 @@ class AudioSettings extends React.Component {
|
|||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
const { stream } = this.state;
|
const { stream } = this.state;
|
||||||
|
|
||||||
|
Session.set('inEchoTest', false);
|
||||||
this._mounted = false;
|
this._mounted = false;
|
||||||
|
|
||||||
if (stream) {
|
if (stream) {
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import React from 'react';
|
import React, { useRef, useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import Service from '/imports/ui/components/audio/captions/service';
|
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';
|
import Styled from './styles';
|
||||||
|
|
||||||
const intlMessages = defineMessages({
|
const intlMessages = defineMessages({
|
||||||
@ -13,18 +16,155 @@ const intlMessages = defineMessages({
|
|||||||
id: 'app.audio.captions.button.stop',
|
id: 'app.audio.captions.button.stop',
|
||||||
description: 'Stop audio captions',
|
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 = ({
|
const CaptionsButton = ({
|
||||||
intl,
|
intl,
|
||||||
active,
|
active,
|
||||||
|
isRTL,
|
||||||
enabled,
|
enabled,
|
||||||
|
currentSpeechLocale,
|
||||||
|
availableVoices,
|
||||||
|
isSupported,
|
||||||
|
isVoiceUser,
|
||||||
}) => {
|
}) => {
|
||||||
const onClick = () => Service.setAudioCaptions(!active);
|
|
||||||
|
|
||||||
if (!enabled) return null;
|
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
|
<Styled.ClosedCaptionToggleButton
|
||||||
icon={active ? 'closed_caption' : 'closed_caption_stop'}
|
icon={active ? 'closed_caption' : 'closed_caption_stop'}
|
||||||
label={intl.formatMessage(active ? intlMessages.stop : intlMessages.start)}
|
label={intl.formatMessage(active ? intlMessages.stop : intlMessages.start)}
|
||||||
@ -33,9 +173,42 @@ const CaptionsButton = ({
|
|||||||
hideLabel
|
hideLabel
|
||||||
circle
|
circle
|
||||||
size="lg"
|
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 = {
|
CaptionsButton.propTypes = {
|
||||||
@ -43,7 +216,12 @@ CaptionsButton.propTypes = {
|
|||||||
formatMessage: PropTypes.func.isRequired,
|
formatMessage: PropTypes.func.isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
active: PropTypes.bool.isRequired,
|
active: PropTypes.bool.isRequired,
|
||||||
|
isRTL: PropTypes.bool.isRequired,
|
||||||
enabled: 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);
|
export default injectIntl(CaptionsButton);
|
||||||
|
@ -2,10 +2,24 @@ import React from 'react';
|
|||||||
import { withTracker } from 'meteor/react-meteor-data';
|
import { withTracker } from 'meteor/react-meteor-data';
|
||||||
import Service from '/imports/ui/components/audio/captions/service';
|
import Service from '/imports/ui/components/audio/captions/service';
|
||||||
import Button from './component';
|
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} />;
|
const Container = (props) => <Button {...props} />;
|
||||||
|
|
||||||
export default withTracker(() => ({
|
export default withTracker(() => {
|
||||||
enabled: Service.hasAudioCaptions(),
|
const isRTL = document.documentElement.getAttribute('dir') === 'rtl';
|
||||||
active: Service.getAudioCaptions(),
|
const availableVoices = SpeechService.getSpeechVoices();
|
||||||
}))(Container);
|
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 styled from 'styled-components';
|
||||||
import Button from '/imports/ui/components/common/button/component';
|
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)`
|
const ClosedCaptionToggleButton = styled(Button)`
|
||||||
${({ ghost }) => ghost && `
|
${({ 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 {
|
export default {
|
||||||
ClosedCaptionToggleButton,
|
ClosedCaptionToggleButton,
|
||||||
|
SpanButtonWrapper,
|
||||||
|
TranscriptionToggle,
|
||||||
|
TitleLabel,
|
||||||
|
EnableTrascription,
|
||||||
|
DisableTrascription,
|
||||||
|
SelectedLabel,
|
||||||
};
|
};
|
||||||
|
@ -17,7 +17,6 @@ const intlMessages = defineMessages({
|
|||||||
manageUsers: {
|
manageUsers: {
|
||||||
id: 'app.breakout.dropdown.manageUsers',
|
id: 'app.breakout.dropdown.manageUsers',
|
||||||
description: 'Manage users label',
|
description: 'Manage users label',
|
||||||
defaultMessage: 'Manage Users',
|
|
||||||
},
|
},
|
||||||
destroy: {
|
destroy: {
|
||||||
id: 'app.breakout.dropdown.destroyAll',
|
id: 'app.breakout.dropdown.destroyAll',
|
||||||
@ -102,7 +101,7 @@ class BreakoutDropdown extends PureComponent {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
opts={{
|
opts={{
|
||||||
id: "default-dropdown-menu",
|
id: "breakoutroom-dropdown-menu",
|
||||||
keepMounted: true,
|
keepMounted: true,
|
||||||
transitionDuration: 0,
|
transitionDuration: 0,
|
||||||
elevation: 3,
|
elevation: 3,
|
||||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
|
|
||||||
|
|
||||||
const BreakoutRemainingTime = props => (
|
const BreakoutRemainingTime = props => (
|
||||||
<span data-test="breakoutRemainingTime">
|
<span data-test="timeRemaining">
|
||||||
{props.children}
|
{props.children}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
@ -68,7 +68,7 @@ class breakoutRemainingTimeContainer extends React.Component {
|
|||||||
<BreakoutRemainingTimeComponent>
|
<BreakoutRemainingTimeComponent>
|
||||||
<Text>{text}</Text>
|
<Text>{text}</Text>
|
||||||
<br />
|
<br />
|
||||||
<Time>{time}</Time>
|
<Time data-test="breakoutRemainingTime">{time}</Time>
|
||||||
</BreakoutRemainingTimeComponent>
|
</BreakoutRemainingTimeComponent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -429,11 +429,10 @@ class BreakoutRoom extends PureComponent {
|
|||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const { animations } = Settings.application;
|
const { animations } = Settings.application;
|
||||||
|
|
||||||
const roomItems = breakoutRooms.map((breakout) => (
|
const roomItems = breakoutRooms.map((breakout) => (
|
||||||
<Styled.BreakoutItems key={`breakoutRoomItems-${breakout.breakoutId}`} >
|
<Styled.BreakoutItems key={`breakoutRoomItems-${breakout.breakoutId}`} >
|
||||||
<Styled.Content key={`breakoutRoomList-${breakout.breakoutId}`}>
|
<Styled.Content key={`breakoutRoomList-${breakout.breakoutId}`}>
|
||||||
<Styled.BreakoutRoomListNameLabel aria-hidden>
|
<Styled.BreakoutRoomListNameLabel data-test={breakout.shortName} aria-hidden>
|
||||||
{breakout.isDefaultName
|
{breakout.isDefaultName
|
||||||
? intl.formatMessage(intlMessages.breakoutRoom, { 0: breakout.sequence })
|
? intl.formatMessage(intlMessages.breakoutRoom, { 0: breakout.sequence })
|
||||||
: breakout.shortName}
|
: breakout.shortName}
|
||||||
@ -454,7 +453,9 @@ class BreakoutRoom extends PureComponent {
|
|||||||
breakout.shortName,
|
breakout.shortName,
|
||||||
)}
|
)}
|
||||||
</Styled.Content>
|
</Styled.Content>
|
||||||
<Styled.JoinedUserNames>
|
<Styled.JoinedUserNames
|
||||||
|
data-test={`userNameBreakoutRoom-${breakout.shortName}`}
|
||||||
|
>
|
||||||
{breakout.joinedUsers
|
{breakout.joinedUsers
|
||||||
.sort(BreakoutRoom.sortById)
|
.sort(BreakoutRoom.sortById)
|
||||||
.filter((value, idx, arr) => !(value.userId === (arr[idx + 1] || {}).userId))
|
.filter((value, idx, arr) => !(value.userId === (arr[idx + 1] || {}).userId))
|
||||||
@ -467,7 +468,7 @@ class BreakoutRoom extends PureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Styled.BreakoutColumn>
|
<Styled.BreakoutColumn>
|
||||||
<Styled.BreakoutScrollableList>
|
<Styled.BreakoutScrollableList data-test="breakoutRoomList">
|
||||||
{roomItems}
|
{roomItems}
|
||||||
</Styled.BreakoutScrollableList>
|
</Styled.BreakoutScrollableList>
|
||||||
</Styled.BreakoutColumn>
|
</Styled.BreakoutColumn>
|
||||||
@ -518,6 +519,7 @@ class BreakoutRoom extends PureComponent {
|
|||||||
|
|
||||||
|
|
||||||
<Styled.EndButton
|
<Styled.EndButton
|
||||||
|
data-test="sendButtonDurationTime"
|
||||||
color="primary"
|
color="primary"
|
||||||
disabled={!isMeteorConnected}
|
disabled={!isMeteorConnected}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
@ -226,6 +226,9 @@ class MessageForm extends PureComponent {
|
|||||||
onChange={this.handleMessageChange}
|
onChange={this.handleMessageChange}
|
||||||
onKeyDown={this.handleMessageKeyDown}
|
onKeyDown={this.handleMessageKeyDown}
|
||||||
async
|
async
|
||||||
|
onPaste={(e) => { e.stopPropagation(); }}
|
||||||
|
onCut={(e) => { e.stopPropagation(); }}
|
||||||
|
onCopy={(e) => { e.stopPropagation(); }}
|
||||||
/>
|
/>
|
||||||
<Styled.SendButton
|
<Styled.SendButton
|
||||||
hideLabel
|
hideLabel
|
||||||
|
@ -67,6 +67,11 @@ const Content = styled.div`
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
font-size: ${fontSizeSmall};
|
font-size: ${fontSizeSmall};
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
padding: ${borderSize} ${borderSize} ${borderSize} 0;
|
||||||
|
|
||||||
|
[dir="rtl"] & {
|
||||||
|
padding: ${borderSize} 0 ${borderSize} ${borderSize};
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const BreakoutRoomListNameLabel = styled.span`
|
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';
|
import Modal from '/imports/ui/components/common/modal/simple/component';
|
||||||
|
|
||||||
const WriterMenuModal = styled(Modal)`
|
const WriterMenuModal = styled(Modal)`
|
||||||
padding: 1.5rem;
|
padding: 1rem;
|
||||||
min-height: 20rem;
|
min-height: 20rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -51,7 +51,6 @@ const Content = styled.div`
|
|||||||
const StartBtn = styled(Button)`
|
const StartBtn = styled(Button)`
|
||||||
align-self: center;
|
align-self: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
width: 40%;
|
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
|
@ -14,6 +14,7 @@ import { usePreviousValue } from '/imports/ui/components/utils/hooks';
|
|||||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||||
const PUBLIC_CHAT_CLEAR = CHAT_CONFIG.chat_clear;
|
const PUBLIC_CHAT_CLEAR = CHAT_CONFIG.chat_clear;
|
||||||
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
|
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
|
||||||
|
const POLL_RESULT_KEY = CHAT_CONFIG.system_messages_keys.chat_poll_result;
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
pushAlertEnabled: PropTypes.bool.isRequired,
|
pushAlertEnabled: PropTypes.bool.isRequired,
|
||||||
@ -56,6 +57,14 @@ const intlMessages = defineMessages({
|
|||||||
id: 'app.toast.chat.private',
|
id: 'app.toast.chat.private',
|
||||||
description: 'private chat toast message title',
|
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
|
const ALERT_INTERVAL = 5000; // 5 seconds
|
||||||
@ -168,6 +177,13 @@ const ChatAlert = (props) => {
|
|||||||
</Styled.PushMessageContent>
|
</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)) {
|
if (_.isEqual(prevUnreadMessages, unreadMessages)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -175,9 +191,15 @@ const ChatAlert = (props) => {
|
|||||||
return pushAlertEnabled
|
return pushAlertEnabled
|
||||||
? unreadMessages.map((timeWindow) => {
|
? unreadMessages.map((timeWindow) => {
|
||||||
const mappedMessage = Service.mapGroupMessage(timeWindow);
|
const mappedMessage = Service.mapGroupMessage(timeWindow);
|
||||||
const content = mappedMessage
|
|
||||||
? createMessage(mappedMessage.sender.name, mappedMessage.content.slice(-5))
|
let content = null;
|
||||||
: 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;
|
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);
|
max-height: calc(${fontSizeSmall} * 10);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const ContentMessagePoll = styled(ContentMessage)`
|
||||||
|
margin-top: ${fontSizeSmall};
|
||||||
|
`;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
PushMessageContent,
|
PushMessageContent,
|
||||||
UserNameMessage,
|
UserNameMessage,
|
||||||
ContentMessage,
|
ContentMessage,
|
||||||
|
ContentMessagePoll,
|
||||||
};
|
};
|
||||||
|
@ -144,7 +144,7 @@ class ChatDropdown extends PureComponent {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
opts={{
|
opts={{
|
||||||
id: 'default-dropdown-menu',
|
id: 'chat-options-dropdown-menu',
|
||||||
keepMounted: true,
|
keepMounted: true,
|
||||||
transitionDuration: 0,
|
transitionDuration: 0,
|
||||||
elevation: 3,
|
elevation: 3,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user