diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonActor.scala index b0a3bba94e..8175128568 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonActor.scala @@ -74,7 +74,6 @@ class BigBlueButtonActor( case m: CreateMeetingReqMsg => handleCreateMeetingReqMsg(m) case m: RegisterUserReqMsg => handleRegisterUserReqMsg(m) - case m: EjectDuplicateUserReqMsg => handleEjectDuplicateUserReqMsg(m) case m: GetAllMeetingsReqMsg => handleGetAllMeetingsReqMsg(m) case m: GetRunningMeetingsReqMsg => handleGetRunningMeetingsReqMsg(m) case m: CheckAlivePingSysMsg => handleCheckAlivePingSysMsg(m) @@ -105,16 +104,6 @@ class BigBlueButtonActor( } } - def handleEjectDuplicateUserReqMsg(msg: EjectDuplicateUserReqMsg): Unit = { - log.debug("RECEIVED EjectDuplicateUserReqMsg msg {}", msg) - for { - m <- RunningMeetings.findWithId(meetings, msg.header.meetingId) - } yield { - log.debug("FORWARDING EjectDuplicateUserReqMsg") - m.actorRef forward (msg) - } - } - def handleCreateMeetingReqMsg(msg: CreateMeetingReqMsg): Unit = { log.debug("RECEIVED CreateMeetingReqMsg msg {}", msg) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/api/InMessages.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/api/InMessages.scala index 9130308268..1badd00c5d 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/api/InMessages.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/api/InMessages.scala @@ -104,6 +104,22 @@ case class SendMessageToBreakoutRoomInternalMsg(parentId: String, breakoutId: St */ case class EjectUserFromBreakoutInternalMsg(parentId: String, breakoutId: String, extUserId: String, ejectedBy: String, reason: String, reasonCode: String, ban: Boolean) extends InMessage +/** + * Sent by parent meeting to breakout room to import annotated slides. + * @param userId + * @param parentMeetingId + * @param allPages + */ +case class CapturePresentationReqInternalMsg(userId: String, parentMeetingId: String, allPages: Boolean = true) extends InMessage + +/** + * Sent by parent meeting to breakout room to import shared notes. + * @param parentMeetingId + * @param meetingName + * @param sequence + */ +case class CaptureSharedNotesReqInternalMsg(parentMeetingId: String, meetingName: String, sequence: Int) extends InMessage + // DeskShare case class DeskShareStartedRequest(conferenceName: String, callerId: String, callerIdName: String) extends InMessage case class DeskShareStoppedRequest(conferenceName: String, callerId: String, callerIdName: String) extends InMessage diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/BreakoutModel.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/BreakoutModel.scala index 5bb0e70d8d..f2046badee 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/BreakoutModel.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/BreakoutModel.scala @@ -13,9 +13,11 @@ object BreakoutModel { isDefaultName: Boolean, freeJoin: Boolean, voiceConf: String, - assignedUsers: Vector[String] + assignedUsers: Vector[String], + captureNotes: Boolean, + captureSlides: Boolean, ): BreakoutRoom2x = { - new BreakoutRoom2x(id, externalId, name, parentId, sequence, shortName, isDefaultName, freeJoin, voiceConf, assignedUsers, Vector(), Vector(), None, false) + new BreakoutRoom2x(id, externalId, name, parentId, sequence, shortName, isDefaultName, freeJoin, voiceConf, assignedUsers, Vector(), Vector(), None, false, captureNotes, captureSlides) } } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/WhiteboardModel.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/WhiteboardModel.scala index ff4fa5d096..f3355052ac 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/WhiteboardModel.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/WhiteboardModel.scala @@ -1,9 +1,6 @@ package org.bigbluebutton.core.apps -import org.bigbluebutton.core.util.jhotdraw.BezierWrapper -import scala.collection.immutable.List import scala.collection.immutable.HashMap -import scala.collection.JavaConverters._ import org.bigbluebutton.common2.msgs.AnnotationVO import org.bigbluebutton.core.apps.whiteboard.Whiteboard import org.bigbluebutton.SystemConfiguration @@ -24,86 +21,83 @@ class WhiteboardModel extends SystemConfiguration { } private def createWhiteboard(wbId: String): Whiteboard = { - new Whiteboard( + Whiteboard( wbId, Array.empty[String], Array.empty[String], System.currentTimeMillis(), - new HashMap[String, Map[String, AnnotationVO]]() + new HashMap[String, AnnotationVO] ) } - private def getAnnotationsByUserId(wb: Whiteboard, id: String): Map[String, AnnotationVO] = { - wb.annotationsMap.get(id).getOrElse(Map[String, AnnotationVO]()) - } + private def deepMerge(test: Map[String, _], that: Map[String, _]): Map[String, _] = + (for (k <- test.keys ++ that.keys) yield { + val newValue = + (test.get(k), that.get(k)) match { + case (Some(v), None) => v + case (None, Some(v)) => v + case (Some(v1), Some(v2)) => + if (v1.isInstanceOf[Map[String, _]] && v2.isInstanceOf[Map[String, _]]) + deepMerge(v1.asInstanceOf[Map[String, _]], v2.asInstanceOf[Map[String, _]]) + else v2 + case (_, _) => ??? + } + k -> newValue + }).toMap - def addAnnotations(wbId: String, userId: String, annotations: Array[AnnotationVO]): Array[AnnotationVO] = { + def addAnnotations(wbId: String, userId: String, annotations: Array[AnnotationVO], isPresenter: Boolean, isModerator: Boolean): Array[AnnotationVO] = { + var annotationsAdded = Array[AnnotationVO]() val wb = getWhiteboard(wbId) - val usersAnnotations = getAnnotationsByUserId(wb, userId) - var newUserAnnotations = usersAnnotations + var newAnnotationsMap = wb.annotationsMap for (annotation <- annotations) { - newUserAnnotations = newUserAnnotations + (annotation.id -> annotation) - println("Adding annotation to page [" + wb.id + "]. After numAnnotations=[" + newUserAnnotations.size + "].") + val oldAnnotation = wb.annotationsMap.get(annotation.id) + if (!oldAnnotation.isEmpty) { + val hasPermission = isPresenter || isModerator || oldAnnotation.get.userId == userId + if (hasPermission) { + val newAnnotation = oldAnnotation.get.copy(annotationInfo = deepMerge(oldAnnotation.get.annotationInfo, annotation.annotationInfo)) + newAnnotationsMap += (annotation.id -> newAnnotation) + annotationsAdded :+= annotation + println(s"Updated annotation onpage [${wb.id}]. After numAnnotations=[${newAnnotationsMap.size}].") + } else { + println(s"User $userId doesn't have permission to edit annotation ${annotation.id}, ignoring...") + } + } else if (annotation.annotationInfo.contains("type")) { + newAnnotationsMap += (annotation.id -> annotation) + annotationsAdded :+= annotation + println(s"Adding annotation to page [${wb.id}]. After numAnnotations=[${newAnnotationsMap.size}].") + } else { + println(s"New annotation [${annotation.id}] with no type, ignoring (probably received a remove message before and now the shape is incomplete, ignoring...") + } } - val newAnnotationsMap = wb.annotationsMap + (userId -> newUserAnnotations) val newWb = wb.copy(annotationsMap = newAnnotationsMap) saveWhiteboard(newWb) - annotations + annotationsAdded } def getHistory(wbId: String): Array[AnnotationVO] = { - //wb.annotationsMap.values.flatten.toArray.sortBy(_.position); val wb = getWhiteboard(wbId) - var annotations = Array[AnnotationVO]() - // TODO: revisit this, probably there is a one-liner simple solution - wb.annotationsMap.values.foreach( - user => user.values.foreach( - annotation => annotations = annotations :+ annotation - ) - ) - annotations + wb.annotationsMap.values.toArray } - def clearWhiteboard(wbId: String, userId: String): Option[Boolean] = { - var cleared: Option[Boolean] = None - - if (hasWhiteboard(wbId)) { - val wb = getWhiteboard(wbId) - - if (wb.multiUser.contains(userId)) { - if (wb.annotationsMap.contains(userId)) { - val newWb = wb.copy(annotationsMap = wb.annotationsMap - userId) - saveWhiteboard(newWb) - cleared = Some(false) - } - } else { - if (wb.annotationsMap.nonEmpty) { - val newWb = wb.copy(annotationsMap = new HashMap[String, Map[String, AnnotationVO]]()) - saveWhiteboard(newWb) - cleared = Some(true) - } - } - } - cleared - } - - def deleteAnnotations(wbId: String, userId: String, annotationsIds: Array[String]): Array[String] = { + def deleteAnnotations(wbId: String, userId: String, annotationsIds: Array[String], isPresenter: Boolean, isModerator: Boolean): Array[String] = { var annotationsIdsRemoved = Array[String]() val wb = getWhiteboard(wbId) + var newAnnotationsMap = wb.annotationsMap - val usersAnnotations = getAnnotationsByUserId(wb, userId) - var newUserAnnotations = usersAnnotations for (annotationId <- annotationsIds) { - val annotation = usersAnnotations.get(annotationId) + val annotation = wb.annotationsMap.get(annotationId) - //not empty and annotation exists - if (!usersAnnotations.isEmpty && !annotation.isEmpty) { - newUserAnnotations = newUserAnnotations - annotationId - println("Removing annotation on page [" + wb.id + "]. After numAnnotations=[" + newUserAnnotations.size + "].") - annotationsIdsRemoved = annotationsIdsRemoved :+ annotationId + if (!annotation.isEmpty) { + val hasPermission = isPresenter || isModerator || annotation.get.userId == userId + if (hasPermission) { + newAnnotationsMap -= annotationId + println("Removing annotation on page [" + wb.id + "]. After numAnnotations=[" + newAnnotationsMap.size + "].") + annotationsIdsRemoved :+= annotationId + } else { + println("User doesn't have permission to remove this annotation, ignoring...") + } } } - val newAnnotationsMap = wb.annotationsMap + (userId -> newUserAnnotations) val newWb = wb.copy(annotationsMap = newAnnotationsMap) saveWhiteboard(newWb) annotationsIdsRemoved diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/BreakoutRoomCreatedMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/BreakoutRoomCreatedMsgHdlr.scala index d77309a201..7f2b91fa17 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/BreakoutRoomCreatedMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/BreakoutRoomCreatedMsgHdlr.scala @@ -52,7 +52,7 @@ trait BreakoutRoomCreatedMsgHdlr { (redirectToHtml5JoinURL, redirectJoinURL) <- BreakoutHdlrHelpers.getRedirectUrls(liveMeeting, user, r.externalId, r.sequence.toString()) } yield (user -> redirectToHtml5JoinURL) - new BreakoutRoomInfo(r.name, r.externalId, r.id, r.sequence, r.shortName, r.isDefaultName, r.freeJoin, html5JoinUrls.toMap) + new BreakoutRoomInfo(r.name, r.externalId, r.id, r.sequence, r.shortName, r.isDefaultName, r.freeJoin, html5JoinUrls.toMap, r.captureNotes, r.captureSlides) } log.info("Sending breakout rooms list to {} with containing {} room(s)", liveMeeting.props.meetingProp.intId, breakoutRooms.length) @@ -79,7 +79,7 @@ trait BreakoutRoomCreatedMsgHdlr { BbbCommonEnvCoreMsg(envelope, event) } - val breakoutInfo = BreakoutRoomInfo(room.name, room.externalId, room.id, room.sequence, room.shortName, room.isDefaultName, room.freeJoin, Map()) + val breakoutInfo = BreakoutRoomInfo(room.name, room.externalId, room.id, room.sequence, room.shortName, room.isDefaultName, room.freeJoin, Map(), room.captureNotes, room.captureSlides) val event = build(liveMeeting.props.meetingProp.intId, breakoutInfo) outGW.send(event) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/BreakoutRoomsListMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/BreakoutRoomsListMsgHdlr.scala index 2eb2d87bbb..9a7efafbb7 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/BreakoutRoomsListMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/BreakoutRoomsListMsgHdlr.scala @@ -28,7 +28,7 @@ trait BreakoutRoomsListMsgHdlr { breakoutModel <- state.breakout } yield { val rooms = breakoutModel.rooms.values.toVector map { r => - new BreakoutRoomInfo(r.name, r.externalId, r.id, r.sequence, r.shortName, r.isDefaultName, r.freeJoin, Map()) + new BreakoutRoomInfo(r.name, r.externalId, r.id, r.sequence, r.shortName, r.isDefaultName, r.freeJoin, Map(), r.captureNotes, r.captureSlides) } val ready = breakoutModel.hasAllStarted() broadcastEvent(rooms, ready) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/CreateBreakoutRoomsCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/CreateBreakoutRoomsCmdMsgHdlr.scala index 6e26b76cfe..739a2f1edf 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/CreateBreakoutRoomsCmdMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/CreateBreakoutRoomsCmdMsgHdlr.scala @@ -52,7 +52,7 @@ trait CreateBreakoutRoomsCmdMsgHdlr extends RightsManagementTrait { val (internalId, externalId) = BreakoutRoomsUtil.createMeetingIds(liveMeeting.props.meetingProp.intId, i) val voiceConf = BreakoutRoomsUtil.createVoiceConfId(liveMeeting.props.voiceProp.voiceConf, i) - val breakout = BreakoutModel.create(parentId, internalId, externalId, room.name, room.sequence, room.shortName, room.isDefaultName, room.freeJoin, voiceConf, room.users) + val breakout = BreakoutModel.create(parentId, internalId, externalId, room.name, room.sequence, room.shortName, room.isDefaultName, room.freeJoin, voiceConf, room.users, msg.body.captureNotes, msg.body.captureSlides) rooms = rooms + (breakout.id -> breakout) } @@ -70,7 +70,9 @@ trait CreateBreakoutRoomsCmdMsgHdlr extends RightsManagementTrait { liveMeeting.props.password.moderatorPass, liveMeeting.props.password.viewerPass, presId, presSlide, msg.body.record, - liveMeeting.props.breakoutProps.privateChatEnabled + liveMeeting.props.breakoutProps.privateChatEnabled, + breakout.captureNotes, + breakout.captureSlides, ) val event = buildCreateBreakoutRoomSysCmdMsg(liveMeeting.props.meetingProp.intId, roomDetail) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/EndAllBreakoutRoomsMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/EndAllBreakoutRoomsMsgHdlr.scala index 417d38933d..30af0980c9 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/EndAllBreakoutRoomsMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/EndAllBreakoutRoomsMsgHdlr.scala @@ -14,8 +14,8 @@ trait EndAllBreakoutRoomsMsgHdlr extends RightsManagementTrait { val outGW: OutMsgRouter def handleEndAllBreakoutRoomsMsg(msg: EndAllBreakoutRoomsMsg, state: MeetingState2x): MeetingState2x = { + val meetingId = liveMeeting.props.meetingProp.intId if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) { - val meetingId = liveMeeting.props.meetingProp.intId val reason = "No permission to end breakout rooms for meeting." PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting) state @@ -24,11 +24,11 @@ trait EndAllBreakoutRoomsMsgHdlr extends RightsManagementTrait { model <- state.breakout } yield { model.rooms.values.foreach { room => - eventBus.publish(BigBlueButtonEvent(room.id, EndBreakoutRoomInternalMsg(props.breakoutProps.parentId, room.id, MeetingEndReason.BREAKOUT_ENDED_BY_MOD))) + eventBus.publish(BigBlueButtonEvent(room.id, EndBreakoutRoomInternalMsg(meetingId, room.id, MeetingEndReason.BREAKOUT_ENDED_BY_MOD))) } val notifyEvent = MsgBuilder.buildNotifyAllInMeetingEvtMsg( - liveMeeting.props.meetingProp.intId, + meetingId, "info", "rooms", "app.toast.breakoutRoomEnded", diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/EndBreakoutRoomInternalMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/EndBreakoutRoomInternalMsgHdlr.scala index 051d9720c4..2e13d83ad0 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/EndBreakoutRoomInternalMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/EndBreakoutRoomInternalMsgHdlr.scala @@ -1,7 +1,7 @@ package org.bigbluebutton.core.apps.breakout -import org.bigbluebutton.core.api.EndBreakoutRoomInternalMsg -import org.bigbluebutton.core.bus.{ InternalEventBus } +import org.bigbluebutton.core.api.{ CaptureSharedNotesReqInternalMsg, CapturePresentationReqInternalMsg, EndBreakoutRoomInternalMsg } +import org.bigbluebutton.core.bus.{ BigBlueButtonEvent, InternalEventBus } import org.bigbluebutton.core.running.{ BaseMeetingActor, HandlerHelpers, LiveMeeting, OutMsgRouter } trait EndBreakoutRoomInternalMsgHdlr extends HandlerHelpers { @@ -12,6 +12,18 @@ trait EndBreakoutRoomInternalMsgHdlr extends HandlerHelpers { val eventBus: InternalEventBus def handleEndBreakoutRoomInternalMsg(msg: EndBreakoutRoomInternalMsg): Unit = { + + if (liveMeeting.props.breakoutProps.captureSlides) { + val captureSlidesEvent = BigBlueButtonEvent(msg.breakoutId, CapturePresentationReqInternalMsg("system", msg.parentId)) + eventBus.publish(captureSlidesEvent) + } + + if (liveMeeting.props.breakoutProps.captureNotes) { + val meetingName: String = liveMeeting.props.meetingProp.name + val captureNotesEvent = BigBlueButtonEvent(msg.breakoutId, CaptureSharedNotesReqInternalMsg(msg.parentId, meetingName, liveMeeting.props.breakoutProps.sequence)) + eventBus.publish(captureNotesEvent) + } + log.info("Breakout room {} ended by parent meeting {}.", msg.breakoutId, msg.parentId) sendEndMeetingDueToExpiry(msg.reason, eventBus, outGW, liveMeeting, "system") } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/CreateGroupChatReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/CreateGroupChatReqMsgHdlr.scala index 479f3b7894..bd72755dbc 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/CreateGroupChatReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/CreateGroupChatReqMsgHdlr.scala @@ -84,10 +84,12 @@ trait CreateGroupChatReqMsgHdlr extends SystemConfiguration { BbbCoreEnvelope(name, routing) } - def makeBody(chatId: String, - access: String, correlationId: String, - createdBy: GroupChatUser, users: Vector[GroupChatUser], - msgs: Vector[GroupChatMsgToUser]): GroupChatCreatedEvtMsgBody = { + def makeBody( + chatId: String, + access: String, correlationId: String, + createdBy: GroupChatUser, users: Vector[GroupChatUser], + msgs: Vector[GroupChatMsgToUser] + ): GroupChatCreatedEvtMsgBody = { GroupChatCreatedEvtMsgBody(correlationId, chatId, createdBy, access, users, msgs) } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/RespondToPollReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/RespondToPollReqMsgHdlr.scala index a7763e9220..ee2b05e8c5 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/RespondToPollReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/RespondToPollReqMsgHdlr.scala @@ -45,27 +45,31 @@ trait RespondToPollReqMsgHdlr { bus.outGW.send(msgEvent) } - for { - (pollId: String, updatedPoll: SimplePollResultOutVO) <- Polls.handleRespondToPollReqMsg(msg.header.userId, msg.body.pollId, - msg.body.questionId, msg.body.answerIds, liveMeeting) - } yield { - broadcastPollUpdatedEvent(msg, pollId, updatedPoll) + if (Polls.checkUserResponded(msg.body.pollId, msg.header.userId, liveMeeting.polls) == false) { for { - poll <- Polls.getPoll(pollId, liveMeeting.polls) + (pollId: String, updatedPoll: SimplePollResultOutVO) <- Polls.handleRespondToPollReqMsg(msg.header.userId, msg.body.pollId, + msg.body.questionId, msg.body.answerIds, liveMeeting) } yield { + broadcastPollUpdatedEvent(msg, pollId, updatedPoll) for { - answerId <- msg.body.answerIds + poll <- Polls.getPoll(pollId, liveMeeting.polls) } yield { - val answerText = poll.questions(0).answers.get(answerId).key - broadcastUserRespondedToPollRecordMsg(msg, pollId, answerId, answerText, poll.isSecret) + for { + answerId <- msg.body.answerIds + } yield { + val answerText = poll.questions(0).answers.get(answerId).key + broadcastUserRespondedToPollRecordMsg(msg, pollId, answerId, answerText, poll.isSecret) + } + } + + for { + presenter <- Users2x.findPresenter(liveMeeting.users2x) + } yield { + broadcastUserRespondedToPollRespMsg(msg, pollId, msg.body.answerIds, presenter.intId) } } - - for { - presenter <- Users2x.findPresenter(liveMeeting.users2x) - } yield { - broadcastUserRespondedToPollRespMsg(msg, pollId, msg.body.answerIds, presenter.intId) - } + } else { + log.info("Ignoring typed answer from user {} once user already added an answer to this poll {} in meeting {}", msg.header.userId, msg.body.pollId, msg.header.meetingId) } } } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/RespondToTypedPollReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/RespondToTypedPollReqMsgHdlr.scala index 1b5187c684..420d18e8c5 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/RespondToTypedPollReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/RespondToTypedPollReqMsgHdlr.scala @@ -34,17 +34,23 @@ trait RespondToTypedPollReqMsgHdlr { bus.outGW.send(msgEvent) } - for { - (pollId: String, updatedPoll: SimplePollResultOutVO) <- Polls.handleRespondToTypedPollReqMsg(msg.header.userId, msg.body.pollId, - msg.body.questionId, msg.body.answer, liveMeeting) - } yield { - broadcastPollUpdatedEvent(msg, pollId, updatedPoll) - + if (Polls.isResponsePollType(msg.body.pollId, liveMeeting.polls) && + Polls.checkUserResponded(msg.body.pollId, msg.header.userId, liveMeeting.polls) == false && + Polls.checkUserAddedQuestion(msg.body.pollId, msg.header.userId, liveMeeting.polls) == false) { for { - presenter <- Users2x.findPresenter(liveMeeting.users2x) + (pollId: String, updatedPoll: SimplePollResultOutVO) <- Polls.handleRespondToTypedPollReqMsg(msg.header.userId, msg.body.pollId, + msg.body.questionId, msg.body.answer, liveMeeting) } yield { - broadcastUserRespondedToTypedPollRespMsg(msg, pollId, msg.body.answer, presenter.intId) + broadcastPollUpdatedEvent(msg, pollId, updatedPoll) + + for { + presenter <- Users2x.findPresenter(liveMeeting.users2x) + } yield { + broadcastUserRespondedToTypedPollRespMsg(msg, pollId, msg.body.answer, presenter.intId) + } } + } else { + log.info("Ignoring typed answer from user {} once user already added an answer to this poll {} in meeting {}", msg.header.userId, msg.body.pollId, msg.header.meetingId) } } } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationWithAnnotationsMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationWithAnnotationsMsgHdlr.scala index 118b2dd92c..751a9ff609 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationWithAnnotationsMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationWithAnnotationsMsgHdlr.scala @@ -1,6 +1,7 @@ package org.bigbluebutton.core.apps.presentationpod import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.api.{ CapturePresentationReqInternalMsg, CaptureSharedNotesReqInternalMsg } import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait } import org.bigbluebutton.core.bus.MessageBus import org.bigbluebutton.core.domain.MeetingState2x @@ -122,32 +123,27 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait { } } - def handle(m: ExportPresentationWithAnnotationReqMsg, state: MeetingState2x, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { + def handle(m: CapturePresentationReqInternalMsg, state: MeetingState2x, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { val meetingId = liveMeeting.props.meetingProp.intId - val userId = m.header.userId - + val userId = m.userId val presentationPods: Vector[PresentationPod] = state.presentationPodManager.getAllPresentationPodsInMeeting() val currentPres: Option[PresentationInPod] = presentationPods.flatMap(_.getCurrentPresentation()).headOption if (liveMeeting.props.meetingProp.disabledFeatures.contains("importPresentationWithAnnotationsFromBreakoutRooms")) { - val reason = "Importing slides from breakout rooms disabled for this meeting." - PermissionCheck.ejectUserForFailedPermission(meetingId, userId, reason, bus.outGW, liveMeeting) - } else if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, userId)) { - val reason = "No permission to export presentation." - PermissionCheck.ejectUserForFailedPermission(meetingId, userId, reason, bus.outGW, liveMeeting) + log.error(s"Capturing breakout rooms slides disabled in meeting ${meetingId}.") } else if (currentPres.isEmpty) { log.error(s"No presentation set in meeting ${meetingId}") } else { val jobId: String = RandomStringGenerator.randomAlphanumericString(16); val jobType = "PresentationWithAnnotationExportJob" - val allPages: Boolean = m.body.allPages + val allPages: Boolean = m.allPages val pageCount = currentPres.get.pages.size val presId: String = PresentationPodsApp.getAllPresentationPodsInMeeting(state).flatMap(_.getCurrentPresentation.map(_.id)).mkString val presLocation = List("var", "bigbluebutton", meetingId, meetingId, presId).mkString(File.separator, File.separator, ""); - val parentMeetingId: String = m.body.parentMeetingId + val parentMeetingId: String = m.parentMeetingId val currentPage: PresentationPage = PresentationInPod.getCurrentPage(currentPres.get).get val pagesRange: List[Int] = if (allPages) (1 to pageCount).toList else List(currentPage.num) @@ -183,7 +179,32 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait { log.info("Received NewPresAnnFileAvailableMsg meetingId={} presId={} fileUrl={}", liveMeeting.props.meetingProp.intId, m.body.presId, m.body.fileURI) bus.outGW.send(buildBroadcastNewPresAnnFileAvailable(m, liveMeeting)) - } + def handle(m: CaptureSharedNotesReqInternalMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { + val meetingId = liveMeeting.props.meetingProp.intId + val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, "not-used") + val envelope = BbbCoreEnvelope(PresentationPageConversionStartedEventMsg.NAME, routing) + val header = BbbClientMsgHeader(CaptureSharedNotesReqEvtMsg.NAME, meetingId, "not-used") + val body = CaptureSharedNotesReqEvtMsgBody(m.parentMeetingId, m.meetingName, m.sequence) + val event = CaptureSharedNotesReqEvtMsg(header, body) + + bus.outGW.send(BbbCommonEnvCoreMsg(envelope, event)) + } + + def handle(m: PadCapturePubMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { + + val userId: String = "system" + val jobId: String = RandomStringGenerator.randomAlphanumericString(16); + val jobType = "PadCaptureJob" + val filename = s"${m.body.meetingName}-notes" + val presentationUploadToken: String = PresentationPodsApp.generateToken("DEFAULT_PRESENTATION_POD", userId) + + bus.outGW.send(buildPresentationUploadTokenSysPubMsg(m.body.parentMeetingId, userId, presentationUploadToken, filename)) + + val exportJob = new ExportJob(jobId, jobType, filename, m.body.padId, "", true, List(m.body.sequence), m.body.parentMeetingId, presentationUploadToken) + val job = buildStoreExportJobInRedisSysMsg(exportJob, liveMeeting) + + bus.outGW.send(job) + } } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeLockSettingsInMeetingCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeLockSettingsInMeetingCmdMsgHdlr.scala index 00a8d59a83..a7e738bfed 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeLockSettingsInMeetingCmdMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeLockSettingsInMeetingCmdMsgHdlr.scala @@ -55,7 +55,7 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait { outGW.send(notifyEvent) LockSettingsUtil.enforceCamLockSettingsForAllUsers(liveMeeting, outGW) - + // Dial-in def buildLockMessage(meetingId: String, userId: String, lockedBy: String, locked: Boolean): BbbCommonEnvCoreMsg = { val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/EjectDuplicateUserReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/EjectDuplicateUserReqMsgHdlr.scala deleted file mode 100755 index f7111af84f..0000000000 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/EjectDuplicateUserReqMsgHdlr.scala +++ /dev/null @@ -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) - } - -} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/RegisterUserReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/RegisterUserReqMsgHdlr.scala index c4dae8b193..cee8251428 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/RegisterUserReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/RegisterUserReqMsgHdlr.scala @@ -3,7 +3,7 @@ package org.bigbluebutton.core.apps.users import org.bigbluebutton.common2.msgs._ import org.bigbluebutton.core.models._ import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter } -import org.bigbluebutton.core2.message.senders.MsgBuilder +import org.bigbluebutton.core2.message.senders.{ MsgBuilder, Sender } trait RegisterUserReqMsgHdlr { this: UsersApp => @@ -22,12 +22,44 @@ trait RegisterUserReqMsgHdlr { val event = UserRegisteredRespMsg(header, body) BbbCommonEnvCoreMsg(envelope, event) } + + def checkUserConcurrentAccesses(regUser: RegisteredUser): Unit = { + //Remove concurrent accesses over the limit + if (liveMeeting.props.usersProp.maxUserConcurrentAccesses > 0) { + val userConcurrentAccesses = RegisteredUsers.findAllWithExternUserId(regUser.externId, liveMeeting.registeredUsers) + .filter(u => !u.loggedOut) + .sortWith((u1, u2) => u1.registeredOn > u2.registeredOn) //Remove older first + + val userAvailableSlots = liveMeeting.props.usersProp.maxUserConcurrentAccesses - userConcurrentAccesses.length + if (userAvailableSlots <= 0) { + (liveMeeting.props.usersProp.maxUserConcurrentAccesses to userConcurrentAccesses.length) foreach { + idxUserToRemove => + { + val userToRemove = userConcurrentAccesses(idxUserToRemove - 1) + val meetingId = liveMeeting.props.meetingProp.intId + + log.info(s"User ${regUser.id} with extId=${regUser.externId} has ${userConcurrentAccesses.length} concurrent accesses and limit is ${liveMeeting.props.usersProp.maxUserConcurrentAccesses}. " + + s"Ejecting the oldest=${userToRemove.id} in meetingId=${meetingId}") + + val reason = "user ejected because of duplicate external userid" + UsersApp.ejectUserFromMeeting(outGW, liveMeeting, userToRemove.id, SystemUser.ID, reason, EjectReasonCode.DUPLICATE_USER, ban = false) + + // send a system message to force disconnection + Sender.sendDisconnectClientSysMsg(meetingId, userToRemove.id, SystemUser.ID, EjectReasonCode.DUPLICATE_USER, outGW) + } + } + } + } + } + val guestStatus = msg.body.guestStatus val regUser = RegisteredUsers.create(msg.body.intUserId, msg.body.extUserId, msg.body.name, msg.body.role, msg.body.authToken, msg.body.avatarURL, msg.body.guest, msg.body.authed, guestStatus, msg.body.excludeFromDashboard, false) + checkUserConcurrentAccesses(regUser) + RegisteredUsers.add(liveMeeting.registeredUsers, regUser) log.info("Register user success. meetingId=" + liveMeeting.props.meetingProp.intId diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserJoinMeetingReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserJoinMeetingReqMsgHdlr.scala index 9a47563d8f..044ebf920c 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserJoinMeetingReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserJoinMeetingReqMsgHdlr.scala @@ -3,7 +3,7 @@ package org.bigbluebutton.core.apps.users import org.bigbluebutton.common2.msgs.UserJoinMeetingReqMsg import org.bigbluebutton.core.apps.breakout.BreakoutHdlrHelpers import org.bigbluebutton.core.domain.MeetingState2x -import org.bigbluebutton.core.models.{ Users2x, VoiceUsers } +import org.bigbluebutton.core.models.{ RegisteredUser, RegisteredUsers, Users2x, VoiceUsers } import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting, MeetingActor, OutMsgRouter } trait UserJoinMeetingReqMsgHdlr extends HandlerHelpers { @@ -26,16 +26,31 @@ trait UserJoinMeetingReqMsgHdlr extends HandlerHelpers { state case None => - val newState = userJoinMeeting(outGW, msg.body.authToken, msg.body.clientType, liveMeeting, state) - - if (liveMeeting.props.meetingProp.isBreakout) { - BreakoutHdlrHelpers.updateParentMeetingWithUsers(liveMeeting, eventBus) + // Check if maxParticipants has been reached + // User are able to reenter if he already joined previously with the same extId + val userHasJoinedAlready = RegisteredUsers.findWithUserId(msg.body.userId, liveMeeting.registeredUsers) match { + case Some(regUser: RegisteredUser) => RegisteredUsers.checkUserExtIdHasJoined(regUser.externId, liveMeeting.registeredUsers) + case None => false } + val hasReachedMaxParticipants = liveMeeting.props.usersProp.maxUsers > 0 && + RegisteredUsers.numUniqueJoinedUsers(liveMeeting.registeredUsers) >= liveMeeting.props.usersProp.maxUsers && + userHasJoinedAlready == false - // fresh user joined (not due to reconnection). Clear (pop) the cached voice user - VoiceUsers.recoverVoiceUser(liveMeeting.voiceUsers, msg.body.userId) + if (!hasReachedMaxParticipants) { + val newState = userJoinMeeting(outGW, msg.body.authToken, msg.body.clientType, liveMeeting, state) - newState + if (liveMeeting.props.meetingProp.isBreakout) { + BreakoutHdlrHelpers.updateParentMeetingWithUsers(liveMeeting, eventBus) + } + + // fresh user joined (not due to reconnection). Clear (pop) the cached voice user + VoiceUsers.recoverVoiceUser(liveMeeting.voiceUsers, msg.body.userId) + + newState + } else { + log.info("Ignoring user {} attempt to join, once the meeting {} has reached max participants: {}", msg.body.userId, msg.header.meetingId, liveMeeting.props.usersProp.maxUsers) + state + } } } } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp.scala index 1e294fc5ae..5d597bbfb8 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp.scala @@ -158,7 +158,6 @@ class UsersApp( with SelectRandomViewerReqMsgHdlr with AssignPresenterReqMsgHdlr with ChangeUserPinStateReqMsgHdlr - with EjectDuplicateUserReqMsgHdlr with EjectUserFromMeetingCmdMsgHdlr with EjectUserFromMeetingSysMsgHdlr with MuteUserCmdMsgHdlr { diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ValidateAuthTokenReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ValidateAuthTokenReqMsgHdlr.scala index 9d818b367f..f9ae376609 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ValidateAuthTokenReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ValidateAuthTokenReqMsgHdlr.scala @@ -5,7 +5,7 @@ import org.bigbluebutton.core.bus.InternalEventBus import org.bigbluebutton.core.domain.MeetingState2x import org.bigbluebutton.core.models._ import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting, OutMsgRouter } -import org.bigbluebutton.core2.message.senders.{ MsgBuilder, Sender } +import org.bigbluebutton.core2.message.senders.{ MsgBuilder } trait ValidateAuthTokenReqMsgHdlr extends HandlerHelpers { this: UsersApp => @@ -24,10 +24,16 @@ trait ValidateAuthTokenReqMsgHdlr extends HandlerHelpers { liveMeeting.registeredUsers) regUser match { case Some(u) => + // Check if maxParticipants has been reached + // User are able to reenter if he already joined previously with the same extId + val hasReachedMaxParticipants = liveMeeting.props.usersProp.maxUsers > 0 && + RegisteredUsers.numUniqueJoinedUsers(liveMeeting.registeredUsers) >= liveMeeting.props.usersProp.maxUsers && + RegisteredUsers.checkUserExtIdHasJoined(u.externId, liveMeeting.registeredUsers) == false + // Check if banned user is rejoining. // Fail validation if ejected user is rejoining. // ralam april 21, 2020 - if (u.guestStatus == GuestStatus.ALLOW && !u.banned && !u.loggedOut) { + if (u.guestStatus == GuestStatus.ALLOW && !u.banned && !u.loggedOut && !hasReachedMaxParticipants) { userValidated(u, state) } else { if (u.banned) { @@ -36,6 +42,9 @@ trait ValidateAuthTokenReqMsgHdlr extends HandlerHelpers { } else if (u.loggedOut) { failReason = "User had logged out" failReasonCode = EjectReasonCode.USER_LOGGED_OUT + } else if (hasReachedMaxParticipants) { + failReason = "The maximum number of participants allowed for this meeting has been reached." + failReasonCode = EjectReasonCode.MAX_PARTICIPANTS } validateTokenFailed( outGW, diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/ClearWhiteboardPubMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/ClearWhiteboardPubMsgHdlr.scala index d1043ee610..b91ebe1f53 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/ClearWhiteboardPubMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/ClearWhiteboardPubMsgHdlr.scala @@ -28,11 +28,7 @@ trait ClearWhiteboardPubMsgHdlr extends RightsManagementTrait { PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) } } else { - for { - fullClear <- clearWhiteboard(msg.body.whiteboardId, msg.header.userId, liveMeeting) - } yield { - broadcastEvent(msg, fullClear) - } + log.error("Ignoring message ClearWhiteboardPubMsg since this functions is not available in the new Whiteboard") } } } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/DeleteWhiteboardAnnotationsPubMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/DeleteWhiteboardAnnotationsPubMsgHdlr.scala index f59e799a72..55bb258d4f 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/DeleteWhiteboardAnnotationsPubMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/DeleteWhiteboardAnnotationsPubMsgHdlr.scala @@ -21,14 +21,24 @@ trait DeleteWhiteboardAnnotationsPubMsgHdlr extends RightsManagementTrait { bus.outGW.send(msgEvent) } - if (filterWhiteboardMessage(msg.body.whiteboardId, msg.header.userId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) { + val isUserAmongPresenters = !permissionFailed( + PermissionCheck.GUEST_LEVEL, + PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId + ) + + val isUserModerator = !permissionFailed( + PermissionCheck.MOD_LEVEL, + PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId + ) + + if (filterWhiteboardMessage(msg.body.whiteboardId, msg.header.userId, liveMeeting) && !isUserAmongPresenters) { if (isNonEjectionGracePeriodOver(msg.body.whiteboardId, msg.header.userId, liveMeeting)) { val meetingId = liveMeeting.props.meetingProp.intId val reason = "No permission to delete an annotation." PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) } } else { - val deletedAnnotations = deleteWhiteboardAnnotations(msg.body.whiteboardId, msg.header.userId, msg.body.annotationsIds, liveMeeting) + val deletedAnnotations = deleteWhiteboardAnnotations(msg.body.whiteboardId, msg.header.userId, msg.body.annotationsIds, liveMeeting, isUserAmongPresenters, isUserModerator) if (!deletedAnnotations.isEmpty) { broadcastEvent(msg, deletedAnnotations) } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/SendWhiteboardAnnotationPubMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/SendWhiteboardAnnotationPubMsgHdlr.scala index ab5949e1f6..dc0242bf39 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/SendWhiteboardAnnotationPubMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/SendWhiteboardAnnotationPubMsgHdlr.scala @@ -46,13 +46,18 @@ trait SendWhiteboardAnnotationsPubMsgHdlr extends RightsManagementTrait { PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId ) + val isUserModerator = !permissionFailed( + PermissionCheck.MOD_LEVEL, + PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId + ) + if (isUserOneOfPermited || isUserAmongPresenters) { println("============= Printing Sanitized annotations ============") for (annotation <- msg.body.annotations) { printAnnotationInfo(annotation) } println("============= Printed Sanitized annotations ============") - val annotations = sendWhiteboardAnnotations(msg.body.whiteboardId, msg.header.userId, msg.body.annotations, liveMeeting) + val annotations = sendWhiteboardAnnotations(msg.body.whiteboardId, msg.header.userId, msg.body.annotations, liveMeeting, isUserAmongPresenters, isUserModerator) broadcastEvent(msg, msg.body.whiteboardId, annotations, msg.body.html5InstanceId) } else { //val meetingId = liveMeeting.props.meetingProp.intId diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/WhiteboardApp2x.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/WhiteboardApp2x.scala index 921f250bf1..17ed317397 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/WhiteboardApp2x.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/WhiteboardApp2x.scala @@ -11,7 +11,7 @@ case class Whiteboard( multiUser: Array[String], oldMultiUser: Array[String], changedModeOn: Long, - annotationsMap: Map[String, Map[String, AnnotationVO]] + annotationsMap: Map[String, AnnotationVO] ) class WhiteboardApp2x(implicit val context: ActorContext) @@ -24,9 +24,16 @@ class WhiteboardApp2x(implicit val context: ActorContext) val log = Logging(context.system, getClass) - def sendWhiteboardAnnotations(whiteboardId: String, requesterId: String, annotations: Array[AnnotationVO], liveMeeting: LiveMeeting): Array[AnnotationVO] = { + def sendWhiteboardAnnotations( + whiteboardId: String, + requesterId: String, + annotations: Array[AnnotationVO], + liveMeeting: LiveMeeting, + isPresenter: Boolean, + isModerator: Boolean + ): Array[AnnotationVO] = { // println("Received whiteboard annotation. status=[" + status + "], annotationType=[" + annotationType + "]") - liveMeeting.wbModel.addAnnotations(whiteboardId, requesterId, annotations) + liveMeeting.wbModel.addAnnotations(whiteboardId, requesterId, annotations, isPresenter, isModerator) } def getWhiteboardAnnotations(whiteboardId: String, liveMeeting: LiveMeeting): Array[AnnotationVO] = { @@ -34,12 +41,15 @@ class WhiteboardApp2x(implicit val context: ActorContext) liveMeeting.wbModel.getHistory(whiteboardId) } - def clearWhiteboard(whiteboardId: String, requesterId: String, liveMeeting: LiveMeeting): Option[Boolean] = { - liveMeeting.wbModel.clearWhiteboard(whiteboardId, requesterId) - } - - def deleteWhiteboardAnnotations(whiteboardId: String, requesterId: String, annotationsIds: Array[String], liveMeeting: LiveMeeting): Array[String] = { - liveMeeting.wbModel.deleteAnnotations(whiteboardId, requesterId, annotationsIds) + def deleteWhiteboardAnnotations( + whiteboardId: String, + requesterId: String, + annotationsIds: Array[String], + liveMeeting: LiveMeeting, + isPresenter: Boolean, + isModerator: Boolean + ): Array[String] = { + liveMeeting.wbModel.deleteAnnotations(whiteboardId, requesterId, annotationsIds, isPresenter, isModerator) } def getWhiteboardAccess(whiteboardId: String, liveMeeting: LiveMeeting): Array[String] = { diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/domain/BreakoutRoom2x.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/domain/BreakoutRoom2x.scala index 1c124bf980..f17e78143f 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/domain/BreakoutRoom2x.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/domain/BreakoutRoom2x.scala @@ -14,10 +14,12 @@ case class BreakoutRoom2x( users: Vector[BreakoutUser], voiceUsers: Vector[BreakoutVoiceUser], startedOn: Option[Long], - started: Boolean + started: Boolean, + captureNotes: Boolean, + captureSlides: Boolean, ) { } case class BreakoutUser(id: String, name: String) -case class BreakoutVoiceUser(id: String, extId: String, voiceUserId: String) \ No newline at end of file +case class BreakoutVoiceUser(id: String, extId: String, voiceUserId: String) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/BreakoutRooms.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/BreakoutRooms.scala index 1d82379414..2101e77fbc 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/BreakoutRooms.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/BreakoutRooms.scala @@ -10,8 +10,8 @@ object BreakoutRooms { def breakoutRoomsdurationInMinutes(status: BreakoutRooms, duration: Int) = status.breakoutRoomsdurationInMinutes = duration def newBreakoutRoom(parentRoomId: String, id: String, externalMeetingId: String, name: String, sequence: Integer, freeJoin: Boolean, - voiceConfId: String, assignedUsers: Vector[String], breakoutRooms: BreakoutRooms): Option[BreakoutRoomVO] = { - val brvo = new BreakoutRoomVO(id, externalMeetingId, name, parentRoomId, sequence, freeJoin, voiceConfId, assignedUsers, Vector()) + voiceConfId: String, assignedUsers: Vector[String], captureNotes: Boolean, captureSlides: Boolean, breakoutRooms: BreakoutRooms): Option[BreakoutRoomVO] = { + val brvo = new BreakoutRoomVO(id, externalMeetingId, name, parentRoomId, sequence, freeJoin, voiceConfId, assignedUsers, Vector(), captureNotes, captureSlides) breakoutRooms.add(brvo) Some(brvo) } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Polls.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Polls.scala index c38732ff7b..8ae8608693 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Polls.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Polls.scala @@ -112,7 +112,7 @@ object Polls { shape = pollResultToWhiteboardShape(result) annot <- send(result, shape) } yield { - lm.wbModel.addAnnotations(annot.wbId, requesterId, Array[AnnotationVO](annot)) + lm.wbModel.addAnnotations(annot.wbId, requesterId, Array[AnnotationVO](annot), false, false) showPollResult(pollId, lm.polls) (result, annot) } @@ -238,7 +238,7 @@ object Polls { private def handleRespondToTypedPoll(poll: SimplePollResultOutVO, requesterId: String, pollId: String, questionId: Int, answer: String, lm: LiveMeeting): Option[SimplePollResultOutVO] = { - addQuestionResponse(poll.id, questionId, answer, lm.polls) + addQuestionResponse(poll.id, questionId, answer, requesterId, lm.polls) for { updatedPoll <- getSimplePollResult(poll.id, lm.polls) } yield { @@ -355,6 +355,45 @@ object Polls { pvo } + def checkUserResponded(pollId: String, userId: String, polls: Polls): Boolean = { + polls.polls.get(pollId) match { + case Some(p) => { + if (p.getResponders().filter(p => p.userId == userId).length > 0) { + true + } else { + false + } + } + case None => false + } + } + + def checkUserAddedQuestion(pollId: String, userId: String, polls: Polls): Boolean = { + polls.polls.get(pollId) match { + case Some(p) => { + if (p.getTypedPollResponders().filter(responderId => responderId == userId).length > 0) { + true + } else { + false + } + } + case None => false + } + } + + def isResponsePollType(pollId: String, polls: Polls): Boolean = { + polls.polls.get(pollId) match { + case Some(p) => { + if (p.questions.filter(q => q.questionType == PollType.ResponsePollType).length > 0) { + true + } else { + false + } + } + case None => false + } + } + def showPollResult(pollId: String, polls: Polls) { polls.get(pollId) foreach { p => @@ -375,10 +414,13 @@ object Polls { } } - def addQuestionResponse(pollId: String, questionID: Int, answer: String, polls: Polls) { + def addQuestionResponse(pollId: String, questionID: Int, answer: String, requesterId: String, polls: Polls) { polls.polls.get(pollId) match { case Some(p) => { - p.addQuestionResponse(questionID, answer) + if (!p.getTypedPollResponders().contains(requesterId)) { + p.addTypedPollResponder(requesterId) + p.addQuestionResponse(questionID, answer) + } } case None => } @@ -545,6 +587,7 @@ class Poll(val id: String, val questions: Array[Question], val numRespondents: I private var _showResult: Boolean = false private var _numResponders: Int = 0 private var _responders = new ArrayBuffer[Responder]() + private var _respondersTypedPoll = new ArrayBuffer[String]() def showingResult() { _showResult = true } def showResult(): Boolean = { _showResult } @@ -561,6 +604,8 @@ class Poll(val id: String, val questions: Array[Question], val numRespondents: I def addResponder(responder: Responder) { _responders += (responder) } def getResponders(): ArrayBuffer[Responder] = { return _responders } + def addTypedPollResponder(responderId: String) { _respondersTypedPoll += (responderId) } + def getTypedPollResponders(): ArrayBuffer[String] = { return _respondersTypedPoll } def hasResponses(): Boolean = { questions.foreach(q => { diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/RegisteredUsers.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/RegisteredUsers.scala index 7679207c09..c92bd627e9 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/RegisteredUsers.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/RegisteredUsers.scala @@ -64,6 +64,14 @@ object RegisteredUsers { } yield user } + def checkUserExtIdHasJoined(externId: String, regUsers: RegisteredUsers): Boolean = { + regUsers.toVector.filter(_.externId == externId).filter(_.joined).length > 0 + } + + def numUniqueJoinedUsers(regUsers: RegisteredUsers): Int = { + regUsers.toVector.filter(_.joined).map(_.externId).distinct.length + } + def add(users: RegisteredUsers, user: RegisteredUser): Vector[RegisteredUser] = { findWithExternUserId(user.externId, users) match { diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Users2x.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Users2x.scala index dbbaaa19ad..b0e9c8498e 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Users2x.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Users2x.scala @@ -407,4 +407,5 @@ object EjectReasonCode { val USER_INACTIVITY = "user_inactivity_eject_reason" val BANNED_USER_REJOINING = "banned_user_rejoining_reason" val USER_LOGGED_OUT = "user_logged_out_reason" + val MAX_PARTICIPANTS = "max_participants_reason" } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala index 735d80f64c..b087cf60a8 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala @@ -65,8 +65,6 @@ class ReceivedJsonMsgHandlerActor( // Route via meeting manager as there is a race condition if we send directly to meeting // because the meeting actor might not have been created yet. route[RegisterUserReqMsg](meetingManagerChannel, envelope, jsonNode) - case EjectDuplicateUserReqMsg.NAME => - route[EjectDuplicateUserReqMsg](meetingManagerChannel, envelope, jsonNode) case UserJoinMeetingReqMsg.NAME => routeGenericMsg[UserJoinMeetingReqMsg](envelope, jsonNode) case UserJoinMeetingAfterReconnectReqMsg.NAME => @@ -175,6 +173,8 @@ class ReceivedJsonMsgHandlerActor( routePadMsg[PadPatchSysMsg](envelope, jsonNode) case PadUpdatePubMsg.NAME => routeGenericMsg[PadUpdatePubMsg](envelope, jsonNode) + case PadCapturePubMsg.NAME => + routePadMsg[PadCapturePubMsg](envelope, jsonNode) // Voice case RecordingStartedVoiceConfEvtMsg.NAME => @@ -310,8 +310,6 @@ class ReceivedJsonMsgHandlerActor( routeGenericMsg[AssignPresenterReqMsg](envelope, jsonNode) case MakePresentationWithAnnotationDownloadReqMsg.NAME => routeGenericMsg[MakePresentationWithAnnotationDownloadReqMsg](envelope, jsonNode) - case ExportPresentationWithAnnotationReqMsg.NAME => - routeGenericMsg[ExportPresentationWithAnnotationReqMsg](envelope, jsonNode) case NewPresAnnFileAvailableMsg.NAME => routeGenericMsg[NewPresAnnFileAvailableMsg](envelope, jsonNode) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/HandlerHelpers.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/HandlerHelpers.scala index 9a4a3212fc..eb852acc7d 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/HandlerHelpers.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/HandlerHelpers.scala @@ -226,7 +226,7 @@ trait HandlerHelpers extends SystemConfiguration { model <- state.breakout } yield { model.rooms.values.foreach { room => - eventBus.publish(BigBlueButtonEvent(room.id, EndBreakoutRoomInternalMsg(liveMeeting.props.breakoutProps.parentId, room.id, reason))) + eventBus.publish(BigBlueButtonEvent(room.id, EndBreakoutRoomInternalMsg(liveMeeting.props.meetingProp.intId, room.id, reason))) } } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index fcbd7236aa..6cab888390 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -250,7 +250,6 @@ class MeetingActor( // Handling RegisterUserReqMsg as it is forwarded from BBBActor and // its type is not BbbCommonEnvCoreMsg case m: RegisterUserReqMsg => usersApp.handleRegisterUserReqMsg(m) - case m: EjectDuplicateUserReqMsg => usersApp.handleEjectDuplicateUserReqMsg(m) case m: GetAllMeetingsReqMsg => handleGetAllMeetingsReqMsg(m) case m: GetRunningMeetingStateReqMsg => handleGetRunningMeetingStateReqMsg(m) case m: ValidateConnAuthTokenSysMsg => handleValidateConnAuthTokenSysMsg(m) @@ -283,7 +282,8 @@ class MeetingActor( case msg: SendMessageToBreakoutRoomInternalMsg => state = handleSendMessageToBreakoutRoomInternalMsg(msg, state, liveMeeting, msgBus) case msg: SendBreakoutTimeRemainingInternalMsg => handleSendBreakoutTimeRemainingInternalMsg(msg) - + case msg: CapturePresentationReqInternalMsg => presentationPodsApp.handle(msg, state, liveMeeting, msgBus) + case msg: CaptureSharedNotesReqInternalMsg => presentationPodsApp.handle(msg, liveMeeting, msgBus) case msg: SendRecordingTimerInternalMsg => state = usersApp.handleSendRecordingTimerInternalMsg(msg, state) @@ -505,8 +505,8 @@ class MeetingActor( case m: PreuploadedPresentationsSysPubMsg => presentationApp2x.handle(m, liveMeeting, msgBus) case m: AssignPresenterReqMsg => state = handlePresenterChange(m, state) case m: MakePresentationWithAnnotationDownloadReqMsg => presentationPodsApp.handle(m, state, liveMeeting, msgBus) - case m: ExportPresentationWithAnnotationReqMsg => presentationPodsApp.handle(m, state, liveMeeting, msgBus) case m: NewPresAnnFileAvailableMsg => presentationPodsApp.handle(m, liveMeeting, msgBus) + case m: PadCapturePubMsg => presentationPodsApp.handle(m, liveMeeting, msgBus) // Presentation Pods case m: CreateNewPresentationPodPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala index ca6bc9f171..be964dc2eb 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala @@ -117,7 +117,6 @@ class AnalyticsActor(val includeChat: Boolean) extends Actor with ActorLogging { // case m: StoreAnnotationsInRedisSysMsg => logMessage(msg) // case m: StoreExportJobInRedisSysMsg => logMessage(msg) case m: MakePresentationWithAnnotationDownloadReqMsg => logMessage(msg) - case m: ExportPresentationWithAnnotationReqMsg => logMessage(msg) case m: NewPresAnnFileAvailableMsg => logMessage(msg) case m: PresentationPageConversionStartedSysMsg => logMessage(msg) case m: PresentationConversionEndedSysMsg => logMessage(msg) @@ -201,6 +200,7 @@ class AnalyticsActor(val includeChat: Boolean) extends Actor with ActorLogging { case m: PadUpdatedEvtMsg => logMessage(msg) case m: PadUpdatePubMsg => logMessage(msg) case m: PadUpdateCmdMsg => logMessage(msg) + case m: PadCapturePubMsg => logMessage(msg) case _ => // ignore message } diff --git a/akka-bbb-apps/src/test/scala/org/bigbluebutton/core/AppsTestFixtures.scala b/akka-bbb-apps/src/test/scala/org/bigbluebutton/core/AppsTestFixtures.scala index edc54e7122..0cc46576ef 100755 --- a/akka-bbb-apps/src/test/scala/org/bigbluebutton/core/AppsTestFixtures.scala +++ b/akka-bbb-apps/src/test/scala/org/bigbluebutton/core/AppsTestFixtures.scala @@ -47,7 +47,7 @@ trait AppsTestFixtures { val meetingLayout = "" val metadata: collection.immutable.Map[String, String] = Map("foo" -> "bar", "bar" -> "baz", "baz" -> "foo") - val breakoutProps = BreakoutProps(parentId = parentMeetingId, sequence = sequence, freeJoin = false, breakoutRooms = Vector()) + val breakoutProps = BreakoutProps(parentId = parentMeetingId, sequence = sequence, freeJoin = false, captureNotes = false, captureSlides = false, breakoutRooms = Vector()) val meetingProp = MeetingProp(name = meetingName, extId = externalMeetingId, intId = meetingId, meetingCameraCap = meetingCameraCap, diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Meeting2x.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Meeting2x.scala index 213c7c131d..b80d64488d 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Meeting2x.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Meeting2x.scala @@ -27,7 +27,9 @@ case class BreakoutProps( freeJoin: Boolean, breakoutRooms: Vector[String], record: Boolean, - privateChatEnabled: Boolean + privateChatEnabled: Boolean, + captureNotes: Boolean, + captureSlides: Boolean, ) case class PasswordProp(moderatorPass: String, viewerPass: String, learningDashboardAccessToken: String) @@ -39,14 +41,15 @@ case class WelcomeProp(welcomeMsgTemplate: String, welcomeMsg: String, modOnlyMe case class VoiceProp(telVoice: String, voiceConf: String, dialNumber: String, muteOnStart: Boolean) case class UsersProp( - maxUsers: Int, - webcamsOnlyForModerator: Boolean, - userCameraCap: Int, - guestPolicy: String, - meetingLayout: String, - allowModsToUnmuteUsers: Boolean, - allowModsToEjectCameras: Boolean, - authenticatedGuest: Boolean + maxUsers: Int, + maxUserConcurrentAccesses:Int, + webcamsOnlyForModerator: Boolean, + userCameraCap: Int, + guestPolicy: String, + meetingLayout: String, + allowModsToUnmuteUsers: Boolean, + allowModsToEjectCameras: Boolean, + authenticatedGuest: Boolean ) case class MetadataProp(metadata: collection.immutable.Map[String, String]) diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/BreakoutMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/BreakoutMsgs.scala index 871606749b..61401e219e 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/BreakoutMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/BreakoutMsgs.scala @@ -13,7 +13,7 @@ case class BreakoutRoomJoinURLEvtMsgBody(parentId: String, breakoutId: String, e object BreakoutRoomsListEvtMsg { val NAME = "BreakoutRoomsListEvtMsg" } case class BreakoutRoomsListEvtMsg(header: BbbClientMsgHeader, body: BreakoutRoomsListEvtMsgBody) extends BbbCoreMsg case class BreakoutRoomsListEvtMsgBody(meetingId: String, rooms: Vector[BreakoutRoomInfo], roomsReady: Boolean) -case class BreakoutRoomInfo(name: String, externalId: String, breakoutId: String, sequence: Int, shortName: String, isDefaultName: Boolean, freeJoin: Boolean, html5JoinUrls: Map[String, String]) +case class BreakoutRoomInfo(name: String, externalId: String, breakoutId: String, sequence: Int, shortName: String, isDefaultName: Boolean, freeJoin: Boolean, html5JoinUrls: Map[String, String], captureNotes: Boolean, captureSlides: Boolean) object BreakoutRoomsListMsg { val NAME = "BreakoutRoomsListMsg" } case class BreakoutRoomsListMsg(header: BbbClientMsgHeader, body: BreakoutRoomsListMsgBody) extends StandardMsg @@ -58,7 +58,9 @@ case class BreakoutRoomDetail( sourcePresentationId: String, sourcePresentationSlide: Int, record: Boolean, - privateChatEnabled: Boolean + privateChatEnabled: Boolean, + captureNotes: Boolean, + captureSlides: Boolean, ) /** @@ -66,7 +68,7 @@ case class BreakoutRoomDetail( */ object CreateBreakoutRoomsCmdMsg { val NAME = "CreateBreakoutRoomsCmdMsg" } case class CreateBreakoutRoomsCmdMsg(header: BbbClientMsgHeader, body: CreateBreakoutRoomsCmdMsgBody) extends StandardMsg -case class CreateBreakoutRoomsCmdMsgBody(meetingId: String, durationInMinutes: Int, record: Boolean, rooms: Vector[BreakoutRoomMsgBody]) +case class CreateBreakoutRoomsCmdMsgBody(meetingId: String, durationInMinutes: Int, record: Boolean, captureNotes: Boolean, captureSlides: Boolean, rooms: Vector[BreakoutRoomMsgBody]) case class BreakoutRoomMsgBody(name: String, sequence: Int, shortName: String, isDefaultName: Boolean, freeJoin: Boolean, users: Vector[String]) // Sent by user to request ending all the breakout rooms @@ -123,5 +125,5 @@ case class BreakoutUserVO(id: String, name: String) case class BreakoutRoomVO(id: String, externalId: String, name: String, parentId: String, sequence: Int, freeJoin: Boolean, voiceConf: String, - assignedUsers: Vector[String], users: Vector[BreakoutUserVO]) + assignedUsers: Vector[String], users: Vector[BreakoutUserVO], captureNotes: Boolean, captureSlides: Boolean) diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PadsMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PadsMsgs.scala index d42255b7ab..3f628c72c3 100644 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PadsMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PadsMsgs.scala @@ -113,3 +113,8 @@ case class PadUpdatePubMsgBody(externalId: String, text: String) object PadUpdateCmdMsg { val NAME = "PadUpdateCmdMsg" } case class PadUpdateCmdMsg(header: BbbCoreHeaderWithMeetingId, body: PadUpdateCmdMsgBody) extends BbbCoreMsg case class PadUpdateCmdMsgBody(groupId: String, name: String, text: String) + +// pads -> apps +object PadCapturePubMsg { val NAME = "PadCapturePubMsg" } +case class PadCapturePubMsg(header: BbbCoreHeaderWithMeetingId, body: PadCapturePubMsgBody) extends PadStandardMsg +case class PadCapturePubMsgBody(parentMeetingId: String, breakoutId: String, padId: String, meetingName: String, sequence: Int) diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationMsgs.scala index b2ed8f7a01..9fc7de060f 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationMsgs.scala @@ -14,18 +14,10 @@ object MakePresentationWithAnnotationDownloadReqMsg { val NAME = "MakePresentati case class MakePresentationWithAnnotationDownloadReqMsg(header: BbbClientMsgHeader, body: MakePresentationWithAnnotationDownloadReqMsgBody) extends StandardMsg case class MakePresentationWithAnnotationDownloadReqMsgBody(presId: String, allPages: Boolean, pages: List[Int]) -object ExportPresentationWithAnnotationReqMsg { val NAME = "ExportPresentationWithAnnotationReqMsg" } -case class ExportPresentationWithAnnotationReqMsg(header: BbbClientMsgHeader, body: ExportPresentationWithAnnotationReqMsgBody) extends StandardMsg -case class ExportPresentationWithAnnotationReqMsgBody(parentMeetingId: String, allPages: Boolean) - object NewPresAnnFileAvailableMsg { val NAME = "NewPresAnnFileAvailableMsg" } case class NewPresAnnFileAvailableMsg(header: BbbClientMsgHeader, body: NewPresAnnFileAvailableMsgBody) extends StandardMsg case class NewPresAnnFileAvailableMsgBody(fileURI: String, presId: String) -object NewPresAnnFileAvailableEvtMsg { val NAME = "NewPresAnnFileAvailableEvtMsg" } -case class NewPresAnnFileAvailableEvtMsg(header: BbbClientMsgHeader, body: NewPresAnnFileAvailableEvtMsgBody) extends BbbCoreMsg -case class NewPresAnnFileAvailableEvtMsgBody(fileURI: String, presId: String) - // ------------ bbb-common-web to akka-apps ------------ // ------------ akka-apps to client ------------ @@ -40,4 +32,13 @@ case class PresenterUnassignedEvtMsgBody(intId: String, name: String, assignedBy object NewPresentationEvtMsg { val NAME = "NewPresentationEvtMsg" } case class NewPresentationEvtMsg(header: BbbClientMsgHeader, body: NewPresentationEvtMsgBody) extends BbbCoreMsg case class NewPresentationEvtMsgBody(presentation: PresentationVO) + +object NewPresAnnFileAvailableEvtMsg { val NAME = "NewPresAnnFileAvailableEvtMsg" } +case class NewPresAnnFileAvailableEvtMsg(header: BbbClientMsgHeader, body: NewPresAnnFileAvailableEvtMsgBody) extends BbbCoreMsg +case class NewPresAnnFileAvailableEvtMsgBody(fileURI: String, presId: String) + +object CaptureSharedNotesReqEvtMsg { val NAME = "CaptureSharedNotesReqEvtMsg" } +case class CaptureSharedNotesReqEvtMsg(header: BbbClientMsgHeader, body: CaptureSharedNotesReqEvtMsgBody) extends BbbCoreMsg +case class CaptureSharedNotesReqEvtMsgBody(parentMeetingId: String, meetingName: String, sequence: Int) + // ------------ akka-apps to client ------------ diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/UsersMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/UsersMsgs.scala index 33ccdfcc30..0bf6ef3984 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/UsersMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/UsersMsgs.scala @@ -1,13 +1,5 @@ package org.bigbluebutton.common2.msgs -object EjectDuplicateUserReqMsg { val NAME = "EjectDuplicateUserReqMsg" } -case class EjectDuplicateUserReqMsg( - header: BbbCoreHeaderWithMeetingId, - body: EjectDuplicateUserReqMsgBody -) extends BbbCoreMsg -case class EjectDuplicateUserReqMsgBody(meetingId: String, intUserId: String, name: String, - extUserId: String) - object RegisterUserReqMsg { val NAME = "RegisterUserReqMsg" } case class RegisterUserReqMsg( header: BbbCoreHeaderWithMeetingId, diff --git a/bbb-common-message/src/test/scala/org/bigbluebutton/common2/TestFixtures.scala b/bbb-common-message/src/test/scala/org/bigbluebutton/common2/TestFixtures.scala index b23b6d16f2..fc49fb7ba2 100755 --- a/bbb-common-message/src/test/scala/org/bigbluebutton/common2/TestFixtures.scala +++ b/bbb-common-message/src/test/scala/org/bigbluebutton/common2/TestFixtures.scala @@ -49,7 +49,7 @@ trait TestFixtures { meetingCameraCap = meetingCameraCap, maxPinnedCameras = maxPinnedCameras, isBreakout = isBreakout.booleanValue()) - val breakoutProps = BreakoutProps(parentId = parentMeetingId, sequence = sequence, freeJoin = false, breakoutRooms = Vector()) + val breakoutProps = BreakoutProps(parentId = parentMeetingId, sequence = sequence, freeJoin = false, captureNotes = false, captureSlides = false, breakoutRooms = Vector()) val durationProps = DurationProps(duration = durationInMinutes, createdTime = createTime, createdDate = createDate, meetingExpireIfNoUserJoinedInMinutes = meetingExpireIfNoUserJoinedInMinutes, meetingExpireWhenLastUserLeftInMinutes = meetingExpireWhenLastUserLeftInMinutes, diff --git a/bbb-common-web/project/Dependencies.scala b/bbb-common-web/project/Dependencies.scala index 192b33ea70..891ce546e0 100644 --- a/bbb-common-web/project/Dependencies.scala +++ b/bbb-common-web/project/Dependencies.scala @@ -31,7 +31,7 @@ object Dependencies { val lang = "3.12.0" val io = "2.11.0" val pool = "2.11.1" - val text = "1.9" + val text = "1.10.0" // BigBlueButton val bbbCommons = "0.0.21-SNAPSHOT" diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java index 0247eb40d5..365f9f514d 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java @@ -76,6 +76,8 @@ public class ApiParams { public static final String UPLOAD_EXTERNAL_DESCRIPTION = "uploadExternalDescription"; public static final String UPLOAD_EXTERNAL_URL = "uploadExternalUrl"; + public static final String BREAKOUT_ROOMS_CAPTURE_SLIDES = "breakoutRoomsCaptureSlides"; + public static final String BREAKOUT_ROOMS_CAPTURE_NOTES = "breakoutRoomsCaptureNotes"; public static final String BREAKOUT_ROOMS_ENABLED = "breakoutRoomsEnabled"; public static final String BREAKOUT_ROOMS_RECORD = "breakoutRoomsRecord"; public static final String BREAKOUT_ROOMS_PRIVATE_CHAT_ENABLED = "breakoutRoomsPrivateChatEnabled"; diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java index 42053ec701..06e5c6980f 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java @@ -377,6 +377,8 @@ public class MeetingService implements MessageListener { breakoutMetadata.put("meetingId", m.getExternalId()); breakoutMetadata.put("sequence", m.getSequence().toString()); breakoutMetadata.put("freeJoin", m.isFreeJoin().toString()); + breakoutMetadata.put("captureSlides", m.isCaptureSlides().toString()); + breakoutMetadata.put("captureNotes", m.isCaptureNotes().toString()); breakoutMetadata.put("parentMeetingId", m.getParentMeetingId()); storeService.recordBreakoutInfo(m.getInternalId(), breakoutMetadata); } @@ -388,6 +390,8 @@ public class MeetingService implements MessageListener { if (m.isBreakout()) { logData.put("sequence", m.getSequence()); logData.put("freeJoin", m.isFreeJoin()); + logData.put("captureSlides", m.isCaptureSlides()); + logData.put("captureNotes", m.isCaptureNotes()); logData.put("parentMeetingId", m.getParentMeetingId()); } logData.put("name", m.getName()); @@ -415,7 +419,7 @@ public class MeetingService implements MessageListener { m.getLearningDashboardAccessToken(), m.getCreateTime(), formatPrettyDate(m.getCreateTime()), m.isBreakout(), m.getSequence(), m.isFreeJoin(), m.getMetadata(), m.getGuestPolicy(), m.getAuthenticatedGuest(), m.getMeetingLayout(), m.getWelcomeMessageTemplate(), m.getWelcomeMessage(), m.getModeratorOnlyMessage(), - m.getDialNumber(), m.getMaxUsers(), + m.getDialNumber(), m.getMaxUsers(), m.getMaxUserConcurrentAccesses(), m.getMeetingExpireIfNoUserJoinedInMinutes(), m.getMeetingExpireWhenLastUserLeftInMinutes(), m.getUserInactivityInspectTimerInMinutes(), m.getUserInactivityThresholdInMinutes(), m.getUserActivitySignResponseDelayInMinutes(), m.getEndWhenNoModerator(), m.getEndWhenNoModeratorDelayInMinutes(), @@ -434,33 +438,6 @@ public class MeetingService implements MessageListener { } private void processRegisterUser(RegisterUser message) { - Meeting m = getMeeting(message.meetingID); - if (m != null) { - User prevUser = m.getUserWithExternalId(message.externUserID); - if (prevUser != null) { - Map logData = new HashMap<>(); - logData.put("meetingId", m.getInternalId()); - logData.put("externalMeetingId", m.getExternalId()); - logData.put("name", m.getName()); - logData.put("extUserId", prevUser.getExternalUserId()); - logData.put("intUserId", prevUser.getInternalUserId()); - logData.put("username", prevUser.getFullname()); - logData.put("logCode", "duplicate_user_with_external_userid"); - logData.put("description", "Duplicate user with external userid."); - - Gson gson = new Gson(); - String logStr = gson.toJson(logData); - log.info(" --analytics-- data={}", logStr); - - if (!m.allowDuplicateExtUserid) { - gw.ejectDuplicateUser(message.meetingID, - prevUser.getInternalUserId(), prevUser.getFullname(), - prevUser.getExternalUserId()); - } - - } - - } gw.registerUser(message.meetingID, message.internalUserId, message.fullname, message.role, message.externUserID, message.authToken, message.avatarURL, message.guest, @@ -661,6 +638,8 @@ public class MeetingService implements MessageListener { params.put(ApiParams.IS_BREAKOUT, "true"); params.put(ApiParams.SEQUENCE, message.sequence.toString()); params.put(ApiParams.FREE_JOIN, message.freeJoin.toString()); + params.put(ApiParams.BREAKOUT_ROOMS_CAPTURE_SLIDES, message.captureSlides.toString()); + params.put(ApiParams.BREAKOUT_ROOMS_CAPTURE_NOTES, message.captureNotes.toString()); params.put(ApiParams.ATTENDEE_PW, message.viewerPassword); params.put(ApiParams.MODERATOR_PW, message.moderatorPassword); params.put(ApiParams.DIAL_NUMBER, message.dialNumber); @@ -951,9 +930,8 @@ public class MeetingService implements MessageListener { message.name, message.role, message.avatarURL, message.guest, message.guestStatus, message.clientType); - if(m.getMaxUsers() > 0 && m.getUsers().size() >= m.getMaxUsers()) { + if(m.getMaxUsers() > 0 && m.countUniqueExtIds() >= m.getMaxUsers()) { m.removeEnteredUser(user.getInternalUserId()); - gw.ejectDuplicateUser(message.meetingId, user.getInternalUserId(), user.getFullname(), user.getExternalUserId()); return; } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java index aed41cfc44..f150cd5697 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java @@ -66,6 +66,8 @@ public class ParamsProcessorUtil { private String apiVersion; private boolean serviceEnabled = false; private String securitySalt; + private String supportedChecksumAlgorithms; + private String checksumHash; private int defaultMaxUsers = 20; private String defaultWelcomeMessage; private String defaultWelcomeMessageFooter; @@ -106,6 +108,8 @@ public class ParamsProcessorUtil { private boolean defaultBreakoutRoomsEnabled = true; private boolean defaultBreakoutRoomsRecord; + private boolean defaultBreakoutRoomsCaptureSlides = false; + private boolean defaultBreakoutRoomsCaptureNotes = false; private boolean defaultbreakoutRoomsPrivateChatEnabled; private boolean defaultLockSettingsDisableCam; @@ -128,6 +132,8 @@ public class ParamsProcessorUtil { private Integer userInactivityThresholdInMinutes = 30; private Integer userActivitySignResponseDelayInMinutes = 5; private Boolean defaultAllowDuplicateExtUserid = true; + + private Integer maxUserConcurrentAccesses = 0; private Boolean defaultEndWhenNoModerator = false; private Integer defaultEndWhenNoModeratorDelayInMinutes = 1; private Integer defaultHtml5InstanceId = 1; @@ -275,7 +281,19 @@ public class ParamsProcessorUtil { breakoutRoomsPrivateChatEnabled = Boolean.parseBoolean(breakoutRoomsPrivateChatEnabledParam); } - return new BreakoutRoomsParams(breakoutRoomsRecord, breakoutRoomsPrivateChatEnabled); + Boolean breakoutRoomsCaptureSlides = defaultBreakoutRoomsCaptureSlides; + String breakoutRoomsCaptureParam = params.get(ApiParams.BREAKOUT_ROOMS_CAPTURE_SLIDES); + if (!StringUtils.isEmpty(breakoutRoomsCaptureParam)) { + breakoutRoomsCaptureSlides = Boolean.parseBoolean(breakoutRoomsCaptureParam); + } + + Boolean breakoutRoomsCaptureNotes = defaultBreakoutRoomsCaptureNotes; + String breakoutRoomsCaptureNotesParam = params.get(ApiParams.BREAKOUT_ROOMS_CAPTURE_NOTES); + if (!StringUtils.isEmpty(breakoutRoomsCaptureNotesParam)) { + breakoutRoomsCaptureNotes = Boolean.parseBoolean(breakoutRoomsCaptureNotesParam); + } + + return new BreakoutRoomsParams(breakoutRoomsRecord, breakoutRoomsPrivateChatEnabled, breakoutRoomsCaptureNotes, breakoutRoomsCaptureSlides); } private LockSettingsParams processLockSettingsParams(Map params) { @@ -680,6 +698,11 @@ public class ParamsProcessorUtil { int html5InstanceId = processHtml5InstanceId(params.get(ApiParams.HTML5_INSTANCE_ID)); + if(defaultAllowDuplicateExtUserid == false) { + log.warn("[DEPRECATION] use `maxUserConcurrentAccesses=1` instead of `allowDuplicateExtUserid=false`"); + maxUserConcurrentAccesses = 1; + } + // Create the meeting with all passed in parameters. Meeting meeting = new Meeting.Builder(externalMeetingId, internalMeetingId, createTime).withName(meetingName) @@ -706,7 +729,7 @@ public class ParamsProcessorUtil { .withMeetingLayout(meetingLayout) .withBreakoutRoomsParams(breakoutParams) .withLockSettingsParams(lockSettingsParams) - .withAllowDuplicateExtUserid(defaultAllowDuplicateExtUserid) + .withMaxUserConcurrentAccesses(maxUserConcurrentAccesses) .withHTML5InstanceId(html5InstanceId) .withLearningDashboardCleanupDelayInMinutes(learningDashboardCleanupMins) .withLearningDashboardAccessToken(learningDashboardAccessToken) @@ -742,6 +765,8 @@ public class ParamsProcessorUtil { if (isBreakout) { meeting.setSequence(Integer.parseInt(params.get(ApiParams.SEQUENCE))); meeting.setFreeJoin(Boolean.parseBoolean(params.get(ApiParams.FREE_JOIN))); + meeting.setCaptureSlides(Boolean.parseBoolean(params.get(ApiParams.BREAKOUT_ROOMS_CAPTURE_SLIDES))); + meeting.setCaptureNotes(Boolean.parseBoolean(params.get(ApiParams.BREAKOUT_ROOMS_CAPTURE_NOTES))); meeting.setParentMeetingId(parentMeetingId); } @@ -978,11 +1003,39 @@ public class ParamsProcessorUtil { log.info("CHECKSUM={} length={}", checksum, checksum.length()); String data = apiCall + queryString + securitySalt; - String cs = DigestUtils.sha1Hex(data); - if (checksum.length() == 64) { - cs = DigestUtils.sha256Hex(data); - log.info("SHA256 {}", cs); - } + + int checksumLength = checksum.length(); + String cs = null; + + switch(checksumLength) { + case 40: + if(supportedChecksumAlgorithms.contains("sha1")) { + cs = DigestUtils.sha1Hex(data); + log.info("SHA1 {}", cs); + } + break; + case 64: + if(supportedChecksumAlgorithms.contains("sha256")) { + cs = DigestUtils.sha256Hex(data); + log.info("SHA256 {}", cs); + } + break; + case 96: + if(supportedChecksumAlgorithms.contains("sha384")) { + cs = DigestUtils.sha384Hex(data); + log.info("SHA384 {}", cs); + } + break; + case 128: + if(supportedChecksumAlgorithms.contains("sha512")) { + cs = DigestUtils.sha512Hex(data); + log.info("SHA512 {}", cs); + } + break; + default: + log.info("No algorithm could be found that matches the provided checksum length"); + } + if (cs == null || !cs.equals(checksum)) { log.info("query string after checksum removed: [{}]", queryString); log.info("checksumError: query string checksum failed. our: [{}], client: [{}]", cs, checksum); @@ -1068,6 +1121,10 @@ public class ParamsProcessorUtil { this.securitySalt = securitySalt; } + public void setSupportedChecksumAlgorithms(String supportedChecksumAlgorithms) { this.supportedChecksumAlgorithms = supportedChecksumAlgorithms; } + + public void setChecksumHash(String checksumHash) { this.checksumHash = checksumHash; } + public void setDefaultMaxUsers(int defaultMaxUsers) { this.defaultMaxUsers = defaultMaxUsers; } @@ -1367,6 +1424,10 @@ public class ParamsProcessorUtil { this.defaultAllowDuplicateExtUserid = allow; } + public void setMaxUserConcurrentAccesses(Integer maxUserConcurrentAccesses) { + this.maxUserConcurrentAccesses = maxUserConcurrentAccesses; + } + public void setEndWhenNoModerator(Boolean val) { this.defaultEndWhenNoModerator = val; } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/BreakoutRoomsParams.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/BreakoutRoomsParams.java index b5285273c1..d49cd6276d 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/BreakoutRoomsParams.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/BreakoutRoomsParams.java @@ -3,9 +3,13 @@ package org.bigbluebutton.api.domain; public class BreakoutRoomsParams { public final Boolean record; public final Boolean privateChatEnabled; + public final Boolean captureNotes; + public final Boolean captureSlides; - public BreakoutRoomsParams(Boolean record, Boolean privateChatEnabled) { + public BreakoutRoomsParams(Boolean record, Boolean privateChatEnabled, Boolean captureNotes, Boolean captureSlides) { this.record = record; this.privateChatEnabled = privateChatEnabled; + this.captureNotes = captureNotes; + this.captureSlides = captureSlides; } } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java index b348a69e0d..5b582a428d 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java @@ -42,7 +42,9 @@ public class Meeting { private String parentMeetingId = "bbb-none"; // Initialize so we don't send null in the json message. private Integer sequence = 0; private Boolean freeJoin = false; - private Integer duration = 0; + private Boolean captureSlides = false; + private Boolean captureNotes = false; + private Integer duration = 0; private long createdTime = 0; private long startTime = 0; private long endTime = 0; @@ -109,7 +111,7 @@ public class Meeting { public final BreakoutRoomsParams breakoutRoomsParams; public final LockSettingsParams lockSettingsParams; - public final Boolean allowDuplicateExtUserid; + public final Integer maxUserConcurrentAccesses; private String meetingEndedCallbackURL = ""; @@ -163,7 +165,7 @@ public class Meeting { allowRequestsWithoutSession = builder.allowRequestsWithoutSession; breakoutRoomsParams = builder.breakoutRoomsParams; lockSettingsParams = builder.lockSettingsParams; - allowDuplicateExtUserid = builder.allowDuplicateExtUserid; + maxUserConcurrentAccesses = builder.maxUserConcurrentAccesses; endWhenNoModerator = builder.endWhenNoModerator; endWhenNoModeratorDelayInMinutes = builder.endWhenNoModeratorDelayInMinutes; html5InstanceId = builder.html5InstanceId; @@ -197,6 +199,28 @@ public class Meeting { return users; } + public Integer countUniqueExtIds() { + List uniqueExtIds = new ArrayList(); + for (User user : users.values()) { + if(!uniqueExtIds.contains(user.getExternalUserId())) { + uniqueExtIds.add(user.getExternalUserId()); + } + } + + return uniqueExtIds.size(); + } + + public List getUsersWithExtId(String externalUserId) { + List usersWithExtId = new ArrayList(); + for (User user : users.values()) { + if(user.getExternalUserId().equals(externalUserId)) { + usersWithExtId.add(user.getInternalUserId()); + } + } + + return usersWithExtId; + } + public void guestIsWaiting(String userId) { RegisteredUser ruser = registeredUsers.get(userId); if (ruser != null) { @@ -288,6 +312,22 @@ public class Meeting { this.freeJoin = freeJoin; } + public Boolean isCaptureSlides() { + return captureSlides; + } + + public void setCaptureSlides(Boolean capture) { + this.captureSlides = captureSlides; + } + + public Boolean isCaptureNotes() { + return captureNotes; + } + + public void setCaptureNotes(Boolean capture) { + this.captureNotes = captureNotes; + } + public Integer getDuration() { return duration; } @@ -504,6 +544,10 @@ public class Meeting { return maxUsers; } + public Integer getMaxUserConcurrentAccesses() { + return maxUserConcurrentAccesses; + } + public int getLogoutTimer() { return logoutTimer; } @@ -633,17 +677,6 @@ public class Meeting { return this.users.get(id); } - public User getUserWithExternalId(String externalUserId) { - for (Map.Entry entry : users.entrySet()) { - User u = entry.getValue(); - if (u.getExternalUserId().equals(externalUserId)) { - return u; - } - } - return null; - } - - public int getNumUsers(){ return this.users.size(); } @@ -843,7 +876,8 @@ public class Meeting { private String meetingLayout; private BreakoutRoomsParams breakoutRoomsParams; private LockSettingsParams lockSettingsParams; - private Boolean allowDuplicateExtUserid; + + private Integer maxUserConcurrentAccesses; private Boolean endWhenNoModerator; private Integer endWhenNoModeratorDelayInMinutes; private int html5InstanceId; @@ -1035,8 +1069,8 @@ public class Meeting { return this; } - public Builder withAllowDuplicateExtUserid(Boolean allowDuplicateExtUserid) { - this.allowDuplicateExtUserid = allowDuplicateExtUserid; + public Builder withMaxUserConcurrentAccesses(Integer maxUserConcurrentAccesses) { + this.maxUserConcurrentAccesses = maxUserConcurrentAccesses; return this; } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/messaging/messages/CreateBreakoutRoom.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/messaging/messages/CreateBreakoutRoom.java index fc5a8ee5d2..fa05cbb8e7 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/messaging/messages/CreateBreakoutRoom.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/messaging/messages/CreateBreakoutRoom.java @@ -19,6 +19,8 @@ public class CreateBreakoutRoom implements IMessage { public final Integer sourcePresentationSlide; public final Boolean record; public final Boolean privateChatEnabled; + public final Boolean captureNotes; // Upload shared notes to main room after breakout room end + public final Boolean captureSlides; // Upload annotated breakout slides to main room after breakout room end public CreateBreakoutRoom(String meetingId, String parentMeetingId, @@ -35,7 +37,9 @@ public class CreateBreakoutRoom implements IMessage { String sourcePresentationId, Integer sourcePresentationSlide, Boolean record, - Boolean privateChatEnabled) { + Boolean privateChatEnabled, + Boolean captureNotes, + Boolean captureSlides) { this.meetingId = meetingId; this.parentMeetingId = parentMeetingId; this.name = name; @@ -52,5 +56,7 @@ public class CreateBreakoutRoom implements IMessage { this.sourcePresentationSlide = sourcePresentationSlide; this.record = record; this.privateChatEnabled = privateChatEnabled; + this.captureNotes = captureNotes; + this.captureSlides = captureSlides; } } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/model/constraint/MaxParticipantsConstraint.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/model/constraint/MaxParticipantsConstraint.java deleted file mode 100755 index db982421fc..0000000000 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/model/constraint/MaxParticipantsConstraint.java +++ /dev/null @@ -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[] payload() default {}; -} diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/model/request/GuestWait.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/model/request/GuestWait.java index 47da3b0acb..2e2e3aeecc 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/model/request/GuestWait.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/model/request/GuestWait.java @@ -1,6 +1,5 @@ package org.bigbluebutton.api.model.request; -import org.bigbluebutton.api.model.constraint.MaxParticipantsConstraint; import org.bigbluebutton.api.model.constraint.MeetingEndedConstraint; import org.bigbluebutton.api.model.constraint.MeetingExistsConstraint; import org.bigbluebutton.api.model.constraint.UserSessionConstraint; diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/model/validator/GetChecksumValidator.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/model/validator/GetChecksumValidator.java index 7ae69960da..9d5593e2bc 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/model/validator/GetChecksumValidator.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/model/validator/GetChecksumValidator.java @@ -20,6 +20,7 @@ public class GetChecksumValidator implements ConstraintValidator { - - @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; - } -} diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/service/ValidationService.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/ValidationService.java index 6c30a8b78f..cb571d30e6 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/service/ValidationService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/ValidationService.java @@ -55,6 +55,7 @@ public class ValidationService { } private String securitySalt; + private String supportedChecksumAlgorithms; private Boolean allowRequestsWithoutSession; private ValidatorFactory validatorFactory; @@ -266,6 +267,9 @@ public class ValidationService { public void setSecuritySalt(String securitySalt) { this.securitySalt = securitySalt; } public String getSecuritySalt() { return securitySalt; } + public void setSupportedChecksumAlgorithms(String supportedChecksumAlgorithms) { this.supportedChecksumAlgorithms = supportedChecksumAlgorithms; } + public String getSupportedChecksumAlgorithms() { return supportedChecksumAlgorithms; } + public void setAllowRequestsWithoutSession(Boolean allowRequestsWithoutSession) { this.allowRequestsWithoutSession = allowRequestsWithoutSession; } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api2/IBbbWebApiGWApp.java b/bbb-common-web/src/main/java/org/bigbluebutton/api2/IBbbWebApiGWApp.java index 6974c3e0a4..1da6e79d55 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api2/IBbbWebApiGWApp.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api2/IBbbWebApiGWApp.java @@ -26,7 +26,7 @@ public interface IBbbWebApiGWApp { String moderatorPass, String viewerPass, String learningDashboardAccessToken, Long createTime, String createDate, Boolean isBreakout, Integer sequence, Boolean freejoin, Map metadata, String guestPolicy, Boolean authenticatedGuest, String meetingLayout, String welcomeMsgTemplate, String welcomeMsg, String modOnlyMessage, - String dialNumber, Integer maxUsers, + String dialNumber, Integer maxUsers, Integer maxUserConcurrentAccesses, Integer meetingExpireIfNoUserJoinedInMinutes, Integer meetingExpireWhenLastUserLeftInMinutes, Integer userInactivityInspectTimerInMinutes, @@ -50,8 +50,6 @@ public interface IBbbWebApiGWApp { void registerUser(String meetingID, String internalUserId, String fullname, String role, String externUserID, String authToken, String avatarURL, Boolean guest, Boolean authed, String guestStatus, Boolean excludeFromDashboard); - void ejectDuplicateUser(String meetingID, String internalUserId, String fullname, - String externUserID); void guestWaitingLeft(String meetingID, String internalUserId); void destroyMeeting(DestroyMeetingMessage msg); diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/SvgImageCreator.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/SvgImageCreator.java index b6d491a9f7..ac07053327 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/SvgImageCreator.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/SvgImageCreator.java @@ -21,6 +21,7 @@ * @version $Id: $ */ package org.bigbluebutton.presentation; +import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException; diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/SwfSlidesGenerationProgressNotifier.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/SwfSlidesGenerationProgressNotifier.java index 684e20366a..66bfd59537 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/SwfSlidesGenerationProgressNotifier.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/SwfSlidesGenerationProgressNotifier.java @@ -50,7 +50,6 @@ public class SwfSlidesGenerationProgressNotifier { maxUploadFileSize); messagingService.sendDocConversionMsg(progress); } - public void sendUploadFileTimedout(UploadedPresentation pres, int page) { UploadFileTimedoutMessage errorMessage = new UploadFileTimedoutMessage( pres.getPodId(), diff --git a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/BbbWebApiGWApp.scala b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/BbbWebApiGWApp.scala index f528e569ea..2fb53c4cda 100755 --- a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/BbbWebApiGWApp.scala +++ b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/BbbWebApiGWApp.scala @@ -131,7 +131,9 @@ class BbbWebApiGWApp( freeJoin: java.lang.Boolean, metadata: java.util.Map[String, String], guestPolicy: String, authenticatedGuest: java.lang.Boolean, meetingLayout: String, welcomeMsgTemplate: String, welcomeMsg: String, modOnlyMessage: String, - dialNumber: String, maxUsers: java.lang.Integer, + dialNumber: String, + maxUsers: java.lang.Integer, + maxUserConcurrentAccesses: java.lang.Integer, meetingExpireIfNoUserJoinedInMinutes: java.lang.Integer, meetingExpireWhenLastUserLeftInMinutes: java.lang.Integer, userInactivityInspectTimerInMinutes: java.lang.Integer, @@ -189,17 +191,23 @@ class BbbWebApiGWApp( freeJoin = freeJoin.booleanValue(), breakoutRooms = Vector(), record = breakoutParams.record.booleanValue(), - privateChatEnabled = breakoutParams.privateChatEnabled.booleanValue() + privateChatEnabled = breakoutParams.privateChatEnabled.booleanValue(), + captureNotes = breakoutParams.captureNotes.booleanValue(), + captureSlides = breakoutParams.captureSlides.booleanValue(), ) val welcomeProp = WelcomeProp(welcomeMsgTemplate = welcomeMsgTemplate, welcomeMsg = welcomeMsg, modOnlyMessage = modOnlyMessage) val voiceProp = VoiceProp(telVoice = voiceBridge, voiceConf = voiceBridge, dialNumber = dialNumber, muteOnStart = muteOnStart.booleanValue()) - val usersProp = UsersProp(maxUsers = maxUsers.intValue(), webcamsOnlyForModerator = webcamsOnlyForModerator.booleanValue(), + val usersProp = UsersProp( + maxUsers = maxUsers.intValue(), + maxUserConcurrentAccesses = maxUserConcurrentAccesses, + webcamsOnlyForModerator = webcamsOnlyForModerator.booleanValue(), userCameraCap = userCameraCap.intValue(), guestPolicy = guestPolicy, meetingLayout = meetingLayout, allowModsToUnmuteUsers = allowModsToUnmuteUsers.booleanValue(), allowModsToEjectCameras = allowModsToEjectCameras.booleanValue(), - authenticatedGuest = authenticatedGuest.booleanValue()) + authenticatedGuest = authenticatedGuest.booleanValue() + ) val metadataProp = MetadataProp(mapAsScalaMap(metadata).toMap) val lockSettingsProps = LockSettingsProps( @@ -261,11 +269,6 @@ class BbbWebApiGWApp( msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event)) } - def ejectDuplicateUser(meetingId: String, intUserId: String, name: String, extUserId: String): Unit = { - val event = MsgBuilder.buildEjectDuplicateUserRequestToAkkaApps(meetingId, intUserId, name, extUserId) - msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event)) - } - def guestWaitingLeft(meetingId: String, intUserId: String): Unit = { val event = MsgBuilder.buildGuestWaitingLeftMsg(meetingId, intUserId) msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event)) diff --git a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/MsgBuilder.scala b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/MsgBuilder.scala index 9ff386411e..668d64f9f9 100755 --- a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/MsgBuilder.scala +++ b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/MsgBuilder.scala @@ -34,16 +34,6 @@ object MsgBuilder { BbbCommonEnvCoreMsg(envelope, req) } - def buildEjectDuplicateUserRequestToAkkaApps(meetingId: String, intUserId: String, name: String, extUserId: String): BbbCommonEnvCoreMsg = { - val routing = collection.immutable.HashMap("sender" -> "bbb-web") - val envelope = BbbCoreEnvelope(EjectDuplicateUserReqMsg.NAME, routing) - val header = BbbCoreHeaderWithMeetingId(EjectDuplicateUserReqMsg.NAME, meetingId) - val body = EjectDuplicateUserReqMsgBody(meetingId = meetingId, intUserId = intUserId, - name = name, extUserId = extUserId) - val req = EjectDuplicateUserReqMsg(header, body) - BbbCommonEnvCoreMsg(envelope, req) - } - def buildRegisterUserRequestToAkkaApps(msg: RegisterUser): BbbCommonEnvCoreMsg = { val routing = collection.immutable.HashMap("sender" -> "bbb-web") val envelope = BbbCoreEnvelope(RegisterUserReqMsg.NAME, routing) diff --git a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/MeetingsManagerActor.scala b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/MeetingsManagerActor.scala index 3e7732e1b0..0a1eaf7954 100755 --- a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/MeetingsManagerActor.scala +++ b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/MeetingsManagerActor.scala @@ -12,7 +12,7 @@ case class CreateBreakoutRoomMsg(meetingId: String, parentMeetingId: String, name: String, sequence: Integer, freeJoin: Boolean, dialNumber: String, voiceConfId: String, viewerPassword: String, moderatorPassword: String, duration: Int, sourcePresentationId: String, sourcePresentationSlide: Int, - record: Boolean) extends ApiMsg + record: Boolean, captureNotes: Boolean, captureSlides: Boolean) extends ApiMsg case class AddUserSession(token: String, session: UserSession) case class RegisterUser(meetingId: String, intUserId: String, name: String, role: String, diff --git a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/OldMeetingMsgHdlrActor.scala b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/OldMeetingMsgHdlrActor.scala index c624d538a7..d860a565d8 100755 --- a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/OldMeetingMsgHdlrActor.scala +++ b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/OldMeetingMsgHdlrActor.scala @@ -102,9 +102,11 @@ class OldMeetingMsgHdlrActor(val olgMsgGW: OldMessageReceivedGW) msg.body.room.sourcePresentationId, msg.body.room.sourcePresentationSlide, msg.body.room.record, - msg.body.room.privateChatEnabled + msg.body.room.privateChatEnabled, + msg.body.room.captureNotes, + msg.body.room.captureSlides, )) - + } def handleRecordingStatusChangedEvtMsg(msg: RecordingStatusChangedEvtMsg): Unit = { diff --git a/bbb-export-annotations/config/settings.json b/bbb-export-annotations/config/settings.json index 5cccc7aae1..baf78f7d59 100644 --- a/bbb-export-annotations/config/settings.json +++ b/bbb-export-annotations/config/settings.json @@ -10,6 +10,9 @@ "imagemagick": "/usr/bin/convert", "pdftocairo": "/usr/bin/pdftocairo" }, + "captureNotes": { + "timeout": 5000 + }, "collector": { "pngWidthRasterizedSlides": 2560 }, @@ -25,6 +28,7 @@ "msgName": "NewPresAnnFileAvailableMsg" }, "bbbWebAPI": "http://127.0.0.1:8090", + "bbbPadsAPI": "http://127.0.0.1:9002", "redis": { "host": "127.0.0.1", "port": 6379, diff --git a/bbb-export-annotations/workers/collector.js b/bbb-export-annotations/workers/collector.js index aaa28ccc02..da5c2d28a7 100644 --- a/bbb-export-annotations/workers/collector.js +++ b/bbb-export-annotations/workers/collector.js @@ -1,24 +1,32 @@ const Logger = require('../lib/utils/logger'); +const axios = require('axios').default; const config = require('../config'); -const fs = require('fs'); -const redis = require('redis'); -const {Worker, workerData} = require('worker_threads'); -const path = require('path'); const cp = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const redis = require('redis'); +const sanitize = require('sanitize-filename'); +const stream = require('stream'); +const {Worker, workerData} = require('worker_threads'); +const {promisify} = require('util'); + +const WorkerTypes = Object.freeze({ + Notifier: 'notifier', + Process: 'process', +}); const jobId = workerData; - const logger = new Logger('presAnn Collector'); -logger.info('Collecting job ' + jobId); +logger.info(`Collecting job ${jobId}`); -const kickOffProcessWorker = (jobId) => { +const kickOffWorker = (workerType, data) => { return new Promise((resolve, reject) => { - const worker = new Worker('./workers/process.js', {workerData: jobId}); + const worker = new Worker(`./workers/${workerType}.js`, {workerData: data}); worker.on('message', resolve); worker.on('error', reject); worker.on('exit', (code) => { if (code !== 0) { - reject(new Error(`Process Worker stopped with exit code ${code}`)); + reject(new Error(`Worker '${workerType}' stopped with exit code ${code}`)); } }); }); @@ -30,8 +38,7 @@ const dropbox = path.join(config.shared.presAnnDropboxDir, jobId); const job = fs.readFileSync(path.join(dropbox, 'job')); const exportJob = JSON.parse(job); -// Collect the annotations from Redis -(async () => { +async function collectAnnotationsFromRedis() { const client = redis.createClient({ host: config.redis.host, port: config.redis.port, @@ -42,7 +49,7 @@ const exportJob = JSON.parse(job); await client.connect(); - const presAnn = await client.hGetAll(exportJob.jobId); + const presAnn = await client.hGetAll(jobId); // Remove annotations from Redis await client.del(jobId); @@ -95,8 +102,66 @@ const exportJob = JSON.parse(job); } else if (fs.existsSync(`${presFile}.jpeg`)) { fs.copyFileSync(`${presFile}.jpeg`, path.join(dropbox, 'slide1.jpeg')); } else { - return logger.error(`Could not find presentation file ${exportJob.jobId}`); + return logger.error(`Could not find presentation file ${jobId}`); } - kickOffProcessWorker(exportJob.jobId); -})(); + kickOffWorker(WorkerTypes.Process, jobId); +} + +async function sleep(ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +/** Export shared notes via bbb-pads in the desired format + * @param {Integer} retries - Number of retries to get the shared notes +*/ +async function collectSharedNotes(retries) { + /** One of the following formats is supported: + etherpad / html / pdf / txt / doc / odf */ + + const padId = exportJob.presId; + const notesFormat = 'pdf'; + + const filename = `${sanitize(exportJob.filename.replace(/\s/g, '_'))}.${notesFormat}`; + const notes_endpoint = `${config.bbbPadsAPI}/p/${padId}/export/${notesFormat}`; + const filePath = path.join(dropbox, filename); + + const [sequence] = JSON.parse(exportJob.pages); + const timeout = (sequence - 1) * config.captureNotes.timeout; + + // Wait for the bbb-pads API to be available + await sleep(timeout); + + const finishedDownload = promisify(stream.finished); + const writer = fs.createWriteStream(filePath); + + try { + const response = await axios({ + method: 'GET', + url: notes_endpoint, + responseType: 'stream', + timeout: timeout, + }); + response.data.pipe(writer); + await finishedDownload(writer); + } catch (err) { + if (retries > 0) { + logger.info(`Retrying ${jobId} in ${timeout}ms...`); + return collectSharedNotes(retries - 1); + } else { + logger.error(`Could not download notes in job ${jobId}`); + return; + } + } + + kickOffWorker(WorkerTypes.Notifier, [exportJob.jobType, jobId, filename]); +} + +switch (exportJob.jobType) { + case 'PresentationWithAnnotationExportJob': return collectAnnotationsFromRedis(); + case 'PresentationWithAnnotationDownloadJob': return collectAnnotationsFromRedis(); + case 'PadCaptureJob': return collectSharedNotes(3); + default: return logger.error(`Unknown job type ${exportJob.jobType}`); +} diff --git a/bbb-export-annotations/workers/notifier.js b/bbb-export-annotations/workers/notifier.js index b33a3fb60c..94d7f10952 100644 --- a/bbb-export-annotations/workers/notifier.js +++ b/bbb-export-annotations/workers/notifier.js @@ -8,7 +8,7 @@ const path = require('path'); const {workerData} = require('worker_threads'); -const [jobType, jobId, filename_with_extension] = workerData; +const [jobType, jobId, filename] = workerData; const logger = new Logger('presAnn Notifier Worker'); @@ -30,7 +30,7 @@ async function notifyMeetingActor() { const link = path.join(`${path.sep}bigbluebutton`, 'presentation', exportJob.parentMeetingId, exportJob.parentMeetingId, - exportJob.presId, 'pdf', jobId, filename_with_extension); + exportJob.presId, 'pdf', jobId, filename); const notification = { envelope: { @@ -59,27 +59,35 @@ async function notifyMeetingActor() { client.disconnect(); } -/** Upload PDF to a BBB room */ -async function upload() { +/** Upload PDF to a BBB room + * @param {String} filePath - Absolute path to the file, including the extension +*/ +async function upload(filePath) { const callbackUrl = `${config.bbbWebAPI}/bigbluebutton/presentation/${exportJob.presentationUploadToken}/upload`; const formData = new FormData(); - const file = `${exportJob.presLocation}/pdfs/${jobId}/${filename_with_extension}`; - formData.append('conference', exportJob.parentMeetingId); formData.append('pod_id', config.notifier.pod_id); formData.append('is_downloadable', config.notifier.is_downloadable); formData.append('temporaryPresentationId', jobId); - formData.append('fileUpload', fs.createReadStream(file)); + formData.append('fileUpload', fs.createReadStream(filePath)); - const res = await axios.post(callbackUrl, formData, - {headers: formData.getHeaders()}); - logger.info(`Upload of job ${exportJob.jobId} returned ${res.data}`); + try { + const res = await axios.post(callbackUrl, formData, + {headers: formData.getHeaders()}); + logger.info(`Upload of job ${exportJob.jobId} returned ${res.data}`); + } catch (error) { + return logger.error(`Could upload job ${exportJob.jobId}: ${error}`); + } } if (jobType == 'PresentationWithAnnotationDownloadJob') { notifyMeetingActor(); } else if (jobType == 'PresentationWithAnnotationExportJob') { - upload(); + const filePath = `${exportJob.presLocation}/pdfs/${jobId}/${filename}`; + upload(filePath); +} else if (jobType == 'PadCaptureJob') { + const filePath = `${dropbox}/${filename}`; + upload(filePath); } else { logger.error(`Notifier received unknown job type ${jobType}`); } diff --git a/bbb-export-annotations/workers/process.js b/bbb-export-annotations/workers/process.js index 8fcac14f48..d1b0386603 100644 --- a/bbb-export-annotations/workers/process.js +++ b/bbb-export-annotations/workers/process.js @@ -6,7 +6,7 @@ const cp = require('child_process'); const {Worker, workerData} = require('worker_threads'); const path = require('path'); const sanitize = require('sanitize-filename'); -const {getStroke, getStrokePoints} = require('perfect-freehand'); +const {getStrokePoints, getStrokeOutlinePoints} = require('perfect-freehand'); const probe = require('probe-image-size'); const jobId = workerData; @@ -193,17 +193,17 @@ function get_gap(dash, size) { function get_stroke_width(dash, size) { switch (size) { case 'small': if (dash === 'draw') { - return 1; + return 2; } else { return 4; } case 'medium': if (dash === 'draw') { - return 1.75; + return 3.5; } else { return 6.25; } case 'large': if (dash === 'draw') { - return 2.5; + return 5; } else { return 8.5; } @@ -236,61 +236,26 @@ function text_size_to_px(size, scale = 1, isStickyNote = false) { } } -// Methods based on tldraw's utilities -function getPath(annotationPoints) { - // Gets inner path of a stroke outline - // For solid, dashed, and dotted types - const stroke = getStrokePoints(annotationPoints) - .map((strokePoint) => strokePoint.point); - - let [max_x, max_y] = [0, 0]; - const inner_path = stroke.reduce( +/** + * Turns an array of points into a path of quadradic curves. + * @param {Array} annotationPoints + * @param {Boolean} closed - whether the path end and start should be connected (default) + * @return {Array} - an SVG quadratic curve path + */ +function getSvgPath(annotationPoints, closed = true) { + const svgPath = annotationPoints.reduce( (acc, [x0, y0], i, arr) => { if (!arr[i + 1]) return acc; const [x1, y1] = arr[i + 1]; - if (x1 >= max_x) { - max_x = x1; - } - if (y1 >= max_y) { - max_y = y1; - } - acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2); + acc.push(x0.toFixed(2), y0.toFixed(2), ((x0 + x1) / 2).toFixed(2), ((y0 + y1) / 2).toFixed(2)); return acc; }, - ['M', ...stroke[0], 'Q'], + ['M', ...annotationPoints[0], 'Q'], ); - return [inner_path, max_x, max_y]; -} - -function getOutlinePath(annotationPoints) { - // Gets outline of a hand-drawn input, with pressure - const stroke = getStroke(annotationPoints, { - simulatePressure: true, - size: 8, - }); - - let [max_x, max_y] = [0, 0]; - const outline_path = stroke.reduce( - (acc, [x0, y0], i, arr) => { - const [x1, y1] = arr[(i + 1) % arr.length]; - if (x1 >= max_x) { - max_x = x1; - } - if (y1 >= max_y) { - max_y = y1; - } - acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2); - return acc; - }, - - ['M', ...stroke[0], 'Q'], - ); - - outline_path.push('Z'); - - return [outline_path, max_x, max_y]; + if (closed) svgPath.push('Z'); + return svgPath; } function circleFromThreePoints(A, B, C) { @@ -471,49 +436,94 @@ function overlay_arrow(svg, annotation) { } function overlay_draw(svg, annotation) { + const shapePoints = annotation.points; + const shapePointsLength = shapePoints.length; + + if (shapePointsLength < 2) return; + const dash = annotation.style.dash; - const [calculated_path, max_x, max_y] = (dash == 'draw') ? getOutlinePath(annotation.points) : getPath(annotation.points); + const isDashDraw = (dash == 'draw'); - if (!calculated_path.length) return; - - const shapeColor = color_to_hex(annotation.style.color); - const rotation = rad_to_degree(annotation.rotation); const thickness = get_stroke_width(dash, annotation.style.size); const gap = get_gap(dash, annotation.style.size); - - const [x, y] = annotation.point; - const stroke_dasharray = determine_dasharray(dash, gap); - const fill = (dash === 'draw') ? shapeColor : 'none'; + const shapeColor = color_to_hex(annotation.style.color); const shapeFillColor = color_to_hex(`fill-${annotation.style.color}`); - const shapeTransform = `translate(${x} ${y}), rotate(${rotation} ${max_x / 2} ${max_y / 2})`; + const fill = isDashDraw ? shapeColor : 'none'; - // Fill assuming solid, small pencil used - // when path start- and end points overlap - const shapeIsFilled = + const rotation = rad_to_degree(annotation.rotation); + const [x, y] = annotation.point; + const [width, height] = annotation.size; + const shapeTransform = `translate(${x} ${y}), rotate(${rotation} ${width / 2} ${height / 2})`; + + const simulatePressure = { + easing: (t) => Math.sin((t * Math.PI) / 2), + simulatePressure: true, + }; + + const realPressure = { + easing: (t) => t * t, + simulatePressure: false, + }; + + const options = { + size: 1 + thickness * 1.5, + thinning: 0.65, + streamline: 0.65, + smoothing: 0.65, + ...(shapePoints[1][2] === 0.5 ? simulatePressure : realPressure), + last: annotation.isComplete, + }; + + const strokePoints = getStrokePoints(shapePoints, options); + + // Fill when path start- and end points overlap + const isShapeFilled = annotation.style.isFilled && - annotation.points.length > 3 && + shapePointsLength > 3 && Math.round(distance( - annotation.points[0][0], - annotation.points[0][1], - annotation.points[annotation.points.length - 1][0], - annotation.points[annotation.points.length - 1][1], - )) <= 2 * get_stroke_width('solid', 'small'); + shapePoints[0][0], + shapePoints[0][1], + shapePoints[shapePointsLength - 1][0], + shapePoints[shapePointsLength - 1][1], + )) <= 2 * thickness; - if (shapeIsFilled) { + if (isShapeFilled) { + const shapeArea = strokePoints.map((strokePoint) => strokePoint.point); svg.ele('path', { style: `fill:${shapeFillColor};`, - d: getPath(annotation.points)[0] + 'Z', + d: getSvgPath(shapeArea), transform: shapeTransform, }).up(); } - svg.ele('path', { - style: `stroke:${shapeColor};stroke-width:${thickness};fill:${fill};${stroke_dasharray}`, - d: calculated_path, - transform: shapeTransform, - }); + if (isDashDraw) { + const strokeOutlinePoints = getStrokeOutlinePoints(strokePoints, options); + const svgPath = getSvgPath(strokeOutlinePoints); + + svg.ele('path', { + style: `fill:${fill};${stroke_dasharray}`, + d: svgPath, + transform: shapeTransform, + }); + } else { + const last = shapePoints[shapePointsLength - 1]; + + // Avoid single dots from not being drawn + if (strokePoints[0].point[0] == last[0] && strokePoints[0].point[1] == last[1]) { + strokePoints.push({point: last}); + } + + const solidPath = strokePoints.map((strokePoint) => strokePoint.point); + const svgPath = getSvgPath(solidPath, false); + + svg.ele('path', { + style: `stroke:${shapeColor};stroke-width:${thickness};fill:${fill};${stroke_dasharray}`, + d: svgPath, + transform: shapeTransform, + }); + } } function overlay_ellipse(svg, annotation) { @@ -603,19 +613,23 @@ function overlay_shape_label(svg, annotation) { render_textbox(fontColor, font, fontSize, textAlign, text, id); - const dimensions = probe.sync(fs.readFileSync(path.join(dropbox, `text${id}.png`))); - const labelWidth = dimensions.width / config.process.textScaleFactor; - const labelHeight = dimensions.height / config.process.textScaleFactor; + const shape_label = path.join(dropbox, `text${id}.png`); - svg.ele('g', { - transform: `rotate(${rotation} ${label_center_x} ${label_center_y})`, - }).ele('image', { - 'x': label_center_x - (labelWidth * x_offset), - 'y': label_center_y - (labelHeight * y_offset), - 'width': labelWidth, - 'height': labelHeight, - 'xlink:href': `file://${dropbox}/text${id}.png`, - }).up(); + if (fs.existsSync(shape_label)) { + const dimensions = probe.sync(fs.readFileSync(shape_label)); + const labelWidth = dimensions.width / config.process.textScaleFactor; + const labelHeight = dimensions.height / config.process.textScaleFactor; + + svg.ele('g', { + transform: `rotate(${rotation} ${label_center_x} ${label_center_y})`, + }).ele('image', { + 'x': label_center_x - (labelWidth * x_offset), + 'y': label_center_y - (labelHeight * y_offset), + 'width': labelWidth, + 'height': labelHeight, + 'xlink:href': `file://${dropbox}/text${id}.png`, + }).up(); + } } function overlay_sticky(svg, annotation) { @@ -712,32 +726,30 @@ function overlay_text(svg, annotation) { } function overlay_annotation(svg, currentAnnotation) { - if (currentAnnotation.childIndex >= 1) { - switch (currentAnnotation.type) { - case 'arrow': - overlay_arrow(svg, currentAnnotation); - break; - case 'draw': - overlay_draw(svg, currentAnnotation); - break; - case 'ellipse': - overlay_ellipse(svg, currentAnnotation); - break; - case 'rectangle': - overlay_rectangle(svg, currentAnnotation); - break; - case 'sticky': - overlay_sticky(svg, currentAnnotation); - break; - case 'triangle': - overlay_triangle(svg, currentAnnotation); - break; - case 'text': - overlay_text(svg, currentAnnotation); - break; - default: - logger.info(`Unknown annotation type ${currentAnnotation.type}.`); - } + switch (currentAnnotation.type) { + case 'arrow': + overlay_arrow(svg, currentAnnotation); + break; + case 'draw': + overlay_draw(svg, currentAnnotation); + break; + case 'ellipse': + overlay_ellipse(svg, currentAnnotation); + break; + case 'rectangle': + overlay_rectangle(svg, currentAnnotation); + break; + case 'sticky': + overlay_sticky(svg, currentAnnotation); + break; + case 'triangle': + overlay_triangle(svg, currentAnnotation); + break; + case 'text': + overlay_text(svg, currentAnnotation); + break; + default: + logger.info(`Unknown annotation type ${currentAnnotation.type}.`); } } diff --git a/bbb-learning-dashboard/src/components/PollsTable.jsx b/bbb-learning-dashboard/src/components/PollsTable.jsx index 8d60d406aa..a3aff8ed49 100644 --- a/bbb-learning-dashboard/src/components/PollsTable.jsx +++ b/bbb-learning-dashboard/src/components/PollsTable.jsx @@ -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 ( @@ -104,7 +142,16 @@ class PollsTable extends React.Component { .sort((a, b) => ((a.createdOn > b.createdOn) ? 1 : -1)) .map((poll) => ( @@ -379,12 +379,13 @@ const ShortcutHelpComponent = (props) => { {generalShortcutItems} - + ) } - - - + + + + @@ -393,9 +394,11 @@ const ShortcutHelpComponent = (props) => { {shortcutItems} - - - + + + + + @@ -404,7 +407,8 @@ const ShortcutHelpComponent = (props) => { {whiteboardShortcutItems} - + + diff --git a/bigbluebutton-html5/imports/ui/components/shortcut-help/styles.js b/bigbluebutton-html5/imports/ui/components/shortcut-help/styles.js index 49bedc80b1..6e9170a1e6 100644 --- a/bigbluebutton-html5/imports/ui/components/shortcut-help/styles.js +++ b/bigbluebutton-html5/imports/ui/components/shortcut-help/styles.js @@ -1,12 +1,13 @@ import styled from 'styled-components'; -import { borderSize, smPaddingX } from '/imports/ui/stylesheets/styled-components/general'; -import { colorGrayLighter } from '/imports/ui/stylesheets/styled-components/palette'; +import { smPaddingX } from '/imports/ui/stylesheets/styled-components/general'; +import { colorOffWhite, colorPrimary } from '/imports/ui/stylesheets/styled-components/palette'; import { Tabs } from 'react-tabs'; +import { ScrollboxVertical } from '/imports/ui/stylesheets/styled-components/scrollable'; +import StyledSettings from '../settings/styles'; import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints'; const KeyCell = styled.td` - border: ${borderSize} solid ${colorGrayLighter}; text-align: center; padding: ${smPaddingX}; margin: auto; @@ -15,16 +16,19 @@ const KeyCell = styled.td` `; const DescCell = styled.td` - border: ${borderSize} solid ${colorGrayLighter}; padding: ${smPaddingX}; margin: auto; `; const ShortcutTable = styled.table` - border: ${borderSize} solid ${colorGrayLighter}; border-collapse: collapse; margin: 0; width: 100%; + + > tbody > tr:nth-child(even) { + background-color: ${colorOffWhite}; + color: ${colorPrimary}; + } `; const SettingsTabs = styled(Tabs)` @@ -39,9 +43,24 @@ const SettingsTabs = styled(Tabs)` } `; +const TableWrapper = styled(ScrollboxVertical)` + height: 50vh; + width: 100%; +`; + +const TabPanel = styled(StyledSettings.SettingsTabPanel)` + margin-top: ${smPaddingX}; + + @media ${smallOnly} { + padding: 0; + } +`; + export default { KeyCell, DescCell, ShortcutTable, SettingsTabs, + TableWrapper, + TabPanel, }; diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx index 7b5a1fcd8c..5df01ec90b 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx @@ -15,6 +15,7 @@ const propTypes = { }; const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator; +const ALWAYS_SHOW_WAITING_ROOM = Meteor.settings.public.app.alwaysShowWaitingRoomUI; class UserContent extends PureComponent { render() { @@ -26,7 +27,7 @@ class UserContent extends PureComponent { compact, } = this.props; - const showWaitingRoom = (isGuestLobbyMessageEnabled && isWaitingRoomEnabled) + const showWaitingRoom = (ALWAYS_SHOW_WAITING_ROOM && isWaitingRoomEnabled) || pendingUsers.length > 0; return ( diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx index 0017d62731..49f1d5e36b 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx @@ -195,6 +195,7 @@ class UserParticipants extends Component { clearAllEmojiStatus, currentUser, meetingIsBreakout, + isMeetingMuteOnStart, } = this.props; const { isOpen, scrollArea } = this.state; @@ -214,6 +215,7 @@ class UserParticipants extends Component { users, clearAllEmojiStatus, meetingIsBreakout, + isMeetingMuteOnStart, }} /> ) : null diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/container.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/container.jsx index 5645bbd853..a89444fc2e 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/container.jsx @@ -54,7 +54,15 @@ export default withTracker(() => { const currentMeeting = Meetings.findOne({ meetingId: Auth.meetingID }, { fields: { lockSettingsProps: 1 } }); + const isMeetingMuteOnStart = () => { + const { voiceProp } = Meetings.findOne({ meetingId: Auth.meetingID }, + { fields: { 'voiceProp.muteOnStart': 1 } }); + const { muteOnStart } = voiceProp; + return muteOnStart; + }; + return ({ + isMeetingMuteOnStart: isMeetingMuteOnStart(), meetingIsBreakout: meetingIsBreakout(), videoUsers: VideoService.getUsersIdFromVideoStreams(), whiteboardUsers, diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx index f3afce8131..7165c38709 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx @@ -124,6 +124,10 @@ const intlMessages = defineMessages({ id: 'app.userList.userOptions.sortedLastName.heading', description: '', }, + newTab: { + id: 'app.modal.newTab', + description: 'label used in aria description', + } }); class UserOptions extends PureComponent { @@ -228,7 +232,7 @@ class UserOptions extends PureComponent { this.menuItems.push({ key: this.muteAllId, label: intl.formatMessage(intlMessages[isMeetingMuted ? 'unmuteAllLabel' : 'muteAllLabel']), - // description: intl.formatMessage(intlMessages[isMeetingMuted ? 'unmuteAllDesc' : 'muteAllDesc']), + description: intl.formatMessage(intlMessages[isMeetingMuted ? 'unmuteAllDesc' : 'muteAllDesc']), onClick: toggleMuteAllUsers, icon: isMeetingMuted ? 'unmute' : 'mute', }); @@ -237,7 +241,7 @@ class UserOptions extends PureComponent { this.menuItems.push({ key: this.muteId, label: intl.formatMessage(intlMessages.muteAllExceptPresenterLabel), - // description: intl.formatMessage(intlMessages.muteAllExceptPresenterDesc), + description: intl.formatMessage(intlMessages.muteAllExceptPresenterDesc), onClick: toggleMuteAllUsersExceptPresenter, icon: 'mute', }); @@ -246,7 +250,7 @@ class UserOptions extends PureComponent { this.menuItems.push({ key: this.lockId, label: intl.formatMessage(intlMessages.lockViewersLabel), - // description: intl.formatMessage(intlMessages.lockViewersDesc), + description: intl.formatMessage(intlMessages.lockViewersDesc), onClick: () => mountModal(), icon: 'lock', dataTest: 'lockViewersButton', @@ -257,7 +261,7 @@ class UserOptions extends PureComponent { key: this.guestPolicyId, icon: 'user', label: intl.formatMessage(intlMessages.guestPolicyLabel), - // description: intl.formatMessage(intlMessages.guestPolicyDesc), + description: intl.formatMessage(intlMessages.guestPolicyDesc), onClick: () => mountModal(), dataTest: 'guestPolicyLabel', }); @@ -278,7 +282,7 @@ class UserOptions extends PureComponent { this.menuItems.push({ key: this.clearStatusId, label: intl.formatMessage(intlMessages.clearAllLabel), - // description: intl.formatMessage(intlMessages.clearAllDesc), + description: intl.formatMessage(intlMessages.clearAllDesc), onClick: toggleStatus, icon: 'clear_status', divider: true, @@ -289,7 +293,7 @@ class UserOptions extends PureComponent { key: this.createBreakoutId, icon: 'rooms', label: intl.formatMessage(intlMessages.createBreakoutRoom), - // description: intl.formatMessage(intlMessages.createBreakoutRoomDesc), + description: intl.formatMessage(intlMessages.createBreakoutRoomDesc), onClick: this.onCreateBreakouts, dataTest: 'createBreakoutRooms', }); @@ -299,7 +303,7 @@ class UserOptions extends PureComponent { this.menuItems.push({ icon: 'closed_caption', label: intl.formatMessage(intlMessages.captionsLabel), - // description: intl.formatMessage(intlMessages.captionsDesc), + description: intl.formatMessage(intlMessages.captionsDesc), key: this.captionsId, onClick: this.handleCaptionsClick, }); @@ -310,7 +314,7 @@ class UserOptions extends PureComponent { icon: 'multi_whiteboard', iconRight: 'popout_window', label: intl.formatMessage(intlMessages.learningDashboardLabel), - description: intl.formatMessage(intlMessages.learningDashboardDesc), + description: `${intl.formatMessage(intlMessages.learningDashboardDesc)} ${intl.formatMessage(intlMessages.newTab)}`, key: this.learningDashboardId, onClick: () => { openLearningDashboardUrl(locale); }, dividerTop: true, @@ -341,7 +345,7 @@ class UserOptions extends PureComponent { )} actions={this.renderMenuItems()} opts={{ - id: "default-dropdown-menu", + id: "user-options-dropdown-menu", keepMounted: true, transitionDuration: 0, elevation: 3, diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/container.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/container.jsx index ec805a4845..4874329d81 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/container.jsx @@ -37,8 +37,9 @@ const UserOptionsContainer = withTracker((props) => { users, clearAllEmojiStatus, intl, + isMeetingMuteOnStart, } = props; - + const toggleStatus = () => { clearAllEmojiStatus(users); @@ -47,13 +48,6 @@ const UserOptionsContainer = withTracker((props) => { ); }; - const isMeetingMuteOnStart = () => { - const { voiceProp } = Meetings.findOne({ meetingId: Auth.meetingID }, - { fields: { 'voiceProp.muteOnStart': 1 } }); - const { muteOnStart } = voiceProp; - return muteOnStart; - }; - const getMeetingName = () => { const { meetingProp } = Meetings.findOne({ meetingId: Auth.meetingID }, { fields: { 'meetingProp.name': 1 } }); @@ -66,7 +60,7 @@ const UserOptionsContainer = withTracker((props) => { return { toggleMuteAllUsers: () => { UserListService.muteAllUsers(Auth.userID); - if (isMeetingMuteOnStart()) { + if (isMeetingMuteOnStart) { return meetingMuteDisabledLog(); } return logger.info({ @@ -76,7 +70,7 @@ const UserOptionsContainer = withTracker((props) => { }, toggleMuteAllUsersExceptPresenter: () => { UserListService.muteAllExceptPresenter(Auth.userID); - if (isMeetingMuteOnStart()) { + if (isMeetingMuteOnStart) { return meetingMuteDisabledLog(); } return logger.info({ @@ -85,7 +79,7 @@ const UserOptionsContainer = withTracker((props) => { }, 'moderator enabled meeting mute, all users muted except presenter'); }, toggleStatus, - isMeetingMuted: isMeetingMuteOnStart(), + isMeetingMuted: isMeetingMuteOnStart, amIModerator: ActionsBarService.amIModerator(), hasBreakoutRoom: UserListService.hasBreakoutRoom(), isBreakoutRecordable: ActionsBarService.isBreakoutRecordable(), diff --git a/bigbluebutton-html5/imports/ui/components/video-preview/component.jsx b/bigbluebutton-html5/imports/ui/components/video-preview/component.jsx index bfe7359767..a02f1b8720 100755 --- a/bigbluebutton-html5/imports/ui/components/video-preview/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-preview/component.jsx @@ -201,6 +201,14 @@ const intlMessages = defineMessages({ id: 'app.videoPreview.wholeImageBrightnessLabel', description: 'Whole image brightness label', }, + wholeImageBrightnessDesc: { + id: 'app.videoPreview.wholeImageBrightnessDesc', + description: 'Whole image brightness aria description', + }, + sliderDesc: { + id: 'app.videoPreview.sliderDesc', + description: 'Brightness slider aria description', + }, }); class VideoPreview extends Component { @@ -313,28 +321,6 @@ class VideoPreview extends Component { viewState: VIEW_STATES.found, }); this.displayPreview(); - - if (CAMERA_BRIGHTNESS_AVAILABLE) { - const setBrightnessInfo = () => { - const stream = this.currentVideoStream || {}; - const service = stream.virtualBgService || {}; - const { brightness = 100, wholeImageBrightness = false } = service; - this.setState({ brightness, wholeImageBrightness }); - }; - - if (!this.currentVideoStream.virtualBgService) { - this.startVirtualBackground( - this.currentVideoStream, - EFFECT_TYPES.NONE_TYPE - ).then((switched) => { - if (switched) { - setBrightnessInfo(); - } - }); - } else { - setBrightnessInfo(); - } - } }); } else { // There were no webcams coming from enumerateDevices. Throw an error. @@ -377,6 +363,30 @@ class VideoPreview extends Component { this._isMounted = false; } + startCameraBrightness() { + if (CAMERA_BRIGHTNESS_AVAILABLE) { + const setBrightnessInfo = () => { + const stream = this.currentVideoStream || {}; + const service = stream.virtualBgService || {}; + const { brightness = 100, wholeImageBrightness = false } = service; + this.setState({ brightness, wholeImageBrightness }); + }; + + if (!this.currentVideoStream.virtualBgService) { + this.startVirtualBackground( + this.currentVideoStream, + EFFECT_TYPES.NONE_TYPE, + ).then((switched) => { + if (switched) { + setBrightnessInfo(); + } + }); + } else { + setBrightnessInfo(); + } + } + } + handleSelectWebcam(event) { const webcamValue = event.target.value; @@ -466,9 +476,18 @@ class VideoPreview extends Component { } handleStartSharing() { - const { resolve, startSharing } = this.props; - const { webcamDeviceId, brightness } = this.state; - // Only streams that will be shared should be stored in the service. // If the store call returns false, we're duplicating stuff. So clean this one + const { + resolve, + startSharing, + } = this.props; + const { + webcamDeviceId, + selectedProfile, + brightness, + } = this.state; + + // Only streams that will be shared should be stored in the service. + // If the store call returns false, we're duplicating stuff. So clean this one // up because it's an impostor. if(!PreviewService.storeStream(webcamDeviceId, this.currentVideoStream)) { this.currentVideoStream.stop(); @@ -484,6 +503,8 @@ class VideoPreview extends Component { this.updateVirtualBackgroundInfo(); this.cleanupStreamAndVideo(); + PreviewService.changeProfile(selectedProfile); + PreviewService.changeWebcam(webcamDeviceId); startSharing(webcamDeviceId); if (resolve) resolve(); } @@ -607,7 +628,6 @@ class VideoPreview extends Component { } this.setState({ webcamDeviceId: actualDeviceId, }); - PreviewService.changeWebcam(actualDeviceId); } getInitialCameraStream(deviceId) { @@ -627,7 +647,6 @@ class VideoPreview extends Component { previewError: undefined, }); - PreviewService.changeProfile(profile.id); this.terminateCameraStream(this.currentVideoStream, webcamDeviceId); this.cleanupStreamAndVideo(); @@ -637,6 +656,7 @@ class VideoPreview extends Component { if (!this._isMounted) return this.terminateCameraStream(bbbVideoStream, deviceId); this.currentVideoStream = bbbVideoStream; + this.startCameraBrightness(); this.setState({ isStartSharingDisabled: false, }); @@ -801,7 +821,7 @@ class VideoPreview extends Component { {intl.formatMessage(intlMessages.brightness)} -
+
this.brightnessMarker = ref} @@ -818,6 +838,7 @@ class VideoPreview extends Component { min={0} max={200} value={brightness} + aria-describedBy={'brightness-slider-desc'} onChange={(e) => { const brightness = e.target.valueAsNumber; this.currentVideoStream.changeCameraBrightness(brightness); @@ -825,7 +846,10 @@ class VideoPreview extends Component { }} disabled={!isVirtualBackgroundSupported() || isStartSharingDisabled} /> - +
+ {intl.formatMessage(intlMessages.sliderDesc)} +
+ {'-100'} {'0'} {'100'} @@ -834,17 +858,14 @@ class VideoPreview extends Component { -
); diff --git a/bigbluebutton-html5/imports/ui/components/video-preview/service.js b/bigbluebutton-html5/imports/ui/components/video-preview/service.js index e57b8213a4..b14d94fa97 100755 --- a/bigbluebutton-html5/imports/ui/components/video-preview/service.js +++ b/bigbluebutton-html5/imports/ui/components/video-preview/service.js @@ -1,4 +1,5 @@ import Storage from '/imports/ui/services/storage/session'; +import BBBStorage from '/imports/ui/services/storage'; import getFromUserSettings from '/imports/ui/services/users-settings'; import MediaStreamUtils from '/imports/utils/media-stream-utils'; import VideoService from '/imports/ui/components/video-provider/service'; @@ -15,7 +16,8 @@ const CAMERA_PROFILES = Meteor.settings.public.kurento.cameraProfiles || []; const PREVIEW_CAMERA_PROFILES = CAMERA_PROFILES.filter(p => !p.hidden); const getDefaultProfile = () => { - return CAMERA_PROFILES.find(profile => profile.id === VideoService.getUserParameterProfile()) + return CAMERA_PROFILES.find(profile => profile.id === BBBStorage.getItem('WebcamProfileId')) + || CAMERA_PROFILES.find(profile => profile.id === VideoService.getUserParameterProfile()) || CAMERA_PROFILES.find(profile => profile.default) || CAMERA_PROFILES[0]; } @@ -221,11 +223,11 @@ export default { CAMERA_PROFILES, promiseTimeout, changeWebcam: (deviceId) => { - Session.set('WebcamDeviceId', deviceId); + BBBStorage.setItem('WebcamDeviceId', deviceId); }, - webcamDeviceId: () => Session.get('WebcamDeviceId'), + webcamDeviceId: () => BBBStorage.getItem('WebcamDeviceId'), changeProfile: (profileId) => { - Session.set('WebcamProfileId', profileId); + BBBStorage.setItem('WebcamProfileId', profileId); }, getSkipVideoPreview, storeStream, diff --git a/bigbluebutton-html5/imports/ui/components/video-preview/styles.js b/bigbluebutton-html5/imports/ui/components/video-preview/styles.js index b7c956a1d0..9c4defbd28 100644 --- a/bigbluebutton-html5/imports/ui/components/video-preview/styles.js +++ b/bigbluebutton-html5/imports/ui/components/video-preview/styles.js @@ -128,7 +128,7 @@ const Footer = styled.div` const Actions = styled.div` margin-left: auto; - margin-right: ${borderSizeLarge}; + margin-right: auto; [dir="rtl"] & { margin-right: auto; diff --git a/bigbluebutton-html5/imports/ui/components/video-preview/virtual-background/component.jsx b/bigbluebutton-html5/imports/ui/components/video-preview/virtual-background/component.jsx index 899040d327..67ae7e312d 100644 --- a/bigbluebutton-html5/imports/ui/components/video-preview/virtual-background/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-preview/virtual-background/component.jsx @@ -64,6 +64,10 @@ const intlMessages = defineMessages({ id: 'app.video.virtualBackground.camBgAriaDesc', description: 'Label for virtual background button aria', }, + customDesc: { + id: 'app.video.virtualBackground.button.customDesc', + description: 'Aria description for upload virtual background button', + }, background: { id: 'app.video.virtualBackground.background', description: 'Label for the background word', @@ -338,6 +342,7 @@ const VirtualBgSelector = ({ disabled={disabled} label={intl.formatMessage(intlMessages.removeLabel)} aria-label={intl.formatMessage(intlMessages.removeLabel)} + aria-describedby={`vr-cam-btn-${index + 1}`} data-test="removeCustomBackground" icon="close" size="sm" @@ -384,7 +389,7 @@ const VirtualBgSelector = ({ accept={MIME_TYPES_ALLOWED.join(', ')} />
- {intl.formatMessage(intlMessages.customLabel)} + {intl.formatMessage(intlMessages.customDesc)}
); diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/service.js b/bigbluebutton-html5/imports/ui/components/video-provider/service.js index 345dc48975..2996a3dca7 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/service.js +++ b/bigbluebutton-html5/imports/ui/components/video-provider/service.js @@ -14,6 +14,7 @@ import browserInfo from '/imports/utils/browserInfo'; import getFromUserSettings from '/imports/ui/services/users-settings'; import VideoPreviewService from '../video-preview/service'; import Storage from '/imports/ui/services/storage/session'; +import BBBStorage from '/imports/ui/services/storage'; import logger from '/imports/startup/client/logger'; import _ from 'lodash'; import { @@ -688,11 +689,11 @@ class VideoService { } getCameraProfile() { - const profileId = Session.get('WebcamProfileId') || ''; + const profileId = BBBStorage.getItem('WebcamProfileId') || ''; const cameraProfile = CAMERA_PROFILES.find(profile => profile.id === profileId) || CAMERA_PROFILES.find(profile => profile.default) || CAMERA_PROFILES[0]; - const deviceId = Session.get('WebcamDeviceId'); + const deviceId = BBBStorage.getItem('WebcamDeviceId'); if (deviceId) { cameraProfile.constraints = cameraProfile.constraints || {}; cameraProfile.constraints.deviceId = { exact: deviceId }; @@ -755,7 +756,7 @@ class VideoService { if (this.userParameterProfile === null) { this.userParameterProfile = getFromUserSettings( 'bbb_preferred_camera_profile', - (CAMERA_PROFILES.filter(i => i.default) || {}).id, + (CAMERA_PROFILES.find(i => i.default) || {}).id || null, ); } diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-button/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-button/component.jsx index bafc191f0d..327a28e502 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/video-button/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-button/component.jsx @@ -105,7 +105,7 @@ const JoinVideoButton = ({ }, JOIN_VIDEO_DELAY_MILLISECONDS); const handleOpenAdvancedOptions = (props) => { - mountVideoPreview(isMobileSharingCamera, props); + mountVideoPreview(isDesktopSharingCamera, props); }; const getMessageFromStatus = () => { @@ -146,18 +146,31 @@ const JoinVideoButton = ({ } if (actions.length === 0) return null; + const customStyles = { top: '-3.6rem' }; return ( )} actions={actions} + opts={{ + id: "video-dropdown-menu", + keepMounted: true, + transitionDuration: 0, + elevation: 3, + getContentAnchorEl: null, + fullwidth: "true", + anchorOrigin: { vertical: 'top', horizontal: 'center' }, + transformOrigin: { vertical: 'top', horizontal: 'center'}, + }} /> ); } diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/user-actions/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/user-actions/component.jsx index dd4f948d34..27b24eed04 100644 --- a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/user-actions/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/user-actions/component.jsx @@ -148,7 +148,7 @@ const UserActions = (props) => { )} actions={getAvailableActions()} opts={{ - id: 'default-dropdown-menu', + id: `webcam-${user?.userId}-dropdown-menu`, keepMounted: true, transitionDuration: 0, elevation: 3, diff --git a/bigbluebutton-html5/imports/ui/components/waiting-users/component.jsx b/bigbluebutton-html5/imports/ui/components/waiting-users/component.jsx index 16b3c144e1..48ff06a209 100755 --- a/bigbluebutton-html5/imports/ui/components/waiting-users/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/waiting-users/component.jsx @@ -126,7 +126,8 @@ const renderGuestUserItem = ( size="lg" ghost label={intl.formatMessage(intlMessages.privateMessageLabel)} - onClick={privateMessageVisible} + onClick={privateMessageVisible} + data-test="privateMessageGuest" /> ) : null} | @@ -137,6 +138,7 @@ const renderGuestUserItem = ( ghost label={intl.formatMessage(intlMessages.accept)} onClick={handleAccept} + data-test="acceptGuest" /> | { isGuestLobbyMessageEnabled ? ( + id={`privateMessage-${userId}`} + data-test="privateLobbyMessage"> { return cb(); }; - const renderButton = (message, { key, color, policy, action }) => ( + const renderButton = (message, { key, color, policy, action, dataTest }) => ( ); @@ -306,6 +311,7 @@ const WaitingUsers = (props) => { key: 'allow-everyone', color: 'primary', policy: 'ALWAYS_ACCEPT', + dataTest: 'allowEveryone', }, { messageId: intlMessages.denyEveryone, @@ -313,6 +319,7 @@ const WaitingUsers = (props) => { key: 'deny-everyone', color: 'danger', policy: 'ALWAYS_DENY', + dataTest: 'denyEveryone', }, ]; @@ -332,7 +339,7 @@ const WaitingUsers = (props) => { /> {isGuestLobbyMessageEnabled ? ( - + { } {allowRememberChoice ? ( - - diff --git a/bigbluebutton-html5/imports/ui/components/waiting-users/guest-policy/styles.js b/bigbluebutton-html5/imports/ui/components/waiting-users/guest-policy/styles.js index e834b44bcb..c6527e0c21 100644 --- a/bigbluebutton-html5/imports/ui/components/waiting-users/guest-policy/styles.js +++ b/bigbluebutton-html5/imports/ui/components/waiting-users/guest-policy/styles.js @@ -12,7 +12,7 @@ import Modal from '/imports/ui/components/common/modal/simple/component'; import Button from '/imports/ui/components/common/button/component'; const GuestPolicyModal = styled(Modal)` - padding: ${jumboPaddingY}; + padding: 1rem; `; const Container = styled.div` diff --git a/bigbluebutton-html5/imports/ui/components/webcam/drop-areas/styles.js b/bigbluebutton-html5/imports/ui/components/webcam/drop-areas/styles.js index 13f7f04e51..afe2368628 100644 --- a/bigbluebutton-html5/imports/ui/components/webcam/drop-areas/styles.js +++ b/bigbluebutton-html5/imports/ui/components/webcam/drop-areas/styles.js @@ -1,4 +1,5 @@ import styled from 'styled-components'; +import { colorWhite } from '/imports/ui/stylesheets/styled-components/palette'; const DropZoneArea = styled.div` position: absolute; @@ -6,7 +7,6 @@ const DropZoneArea = styled.div` -webkit-box-shadow: inset 0px 0px 0px 1px rgba(0, 0, 0, .2); -moz-box-shadow: inset 0px 0px 0px 1px rgba(0, 0, 0, .2); box-shadow: inset 0px 0px 0px 1px rgba(0, 0, 0, .2); - color: rgba(0, 0, 0, .2); font-weight: bold; font-family: sans-serif; display: flex; @@ -21,11 +21,11 @@ const DropZoneArea = styled.div` const DropZoneBg = styled.div` position: absolute; - background-color: rgba(0, 0, 0, .2); + background-color: rgba(0, 0, 0, .5); -webkit-box-shadow: inset 0px 0px 0px 1px #666; -moz-box-shadow: inset 0px 0px 0px 1px #666; box-shadow: inset 0px 0px 0px 1px #666; - color: rgba(0, 0, 0, .2); + color: ${colorWhite}; font-weight: bold; font-family: sans-serif; display: flex; diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index b2d88c7d79..2d268e0298 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -3,13 +3,9 @@ import _ from "lodash"; import { createGlobalStyle } from "styled-components"; import Cursors from "./cursors/container"; import { TldrawApp, Tldraw } from "@tldraw/tldraw"; -import { - ColorStyle, - DashStyle, - SizeStyle, - TDShapeType, -} from "@tldraw/tldraw"; import SlideCalcUtil, {HUNDRED_PERCENT} from '/imports/utils/slideCalcUtils'; +import { Utils } from "@tldraw/core"; +import Settings from '/imports/ui/services/settings'; function usePrevious(value) { const ref = React.useRef(); @@ -25,6 +21,28 @@ const findRemoved = (A, B) => { }); }; +// map different localeCodes from bbb to tldraw +const mapLanguage = (language) => { + switch(language) { + case 'fa-ir': + return 'fa'; + case 'it-it': + return 'it'; + case 'nb-no': + return 'no'; + case 'pl-pl': + return 'pl'; + case 'sv-se': + return 'sv'; + case 'uk-ua': + return 'uk'; + case 'zh-cn': + return 'zh-ch'; + default: + return language; + } +} + const SMALL_HEIGHT = 435; const SMALLEST_HEIGHT = 363; const TOOLBAR_SMALL = 28; @@ -37,15 +55,26 @@ const TldrawGlobalStyle = createGlobalStyle` display: none; } `} + ${({ hideCursor }) => hideCursor && ` + #canvas { + cursor: none; + } + `} + #TD-PrimaryTools-Image { + display: none; + } `; export default function Whiteboard(props) { const { isPresenter, + isModerator, removeShapes, initDefaultPages, persistShape, + notifyNotAllowedChange, shapes, + assets, currentUser, curPres, whiteboardId, @@ -54,7 +83,6 @@ export default function Whiteboard(props) { skipToSlide, slidePosition, curPageId, - svgUri, presentationWidth, presentationHeight, isViewersCursorLocked, @@ -63,9 +91,9 @@ export default function Whiteboard(props) { isRTL, fitToWidth, zoomValue, - width, - height, isPanning, + intl, + svgUri, } = props; const { pages, pageStates } = initDefaultPages(curPres?.pages.length || 1); @@ -79,12 +107,15 @@ export default function Whiteboard(props) { assets: {}, }); const [tldrawAPI, setTLDrawAPI] = React.useState(null); + const [history, setHistory] = React.useState(null); const [forcePanning, setForcePanning] = React.useState(false); const [zoom, setZoom] = React.useState(HUNDRED_PERCENT); const [isMounting, setIsMounting] = React.useState(true); const prevShapes = usePrevious(shapes); const prevSlidePosition = usePrevious(slidePosition); const prevFitToWidth = usePrevious(fitToWidth); + const prevSvgUri = usePrevious(svgUri); + const language = mapLanguage(Settings?.application?.locale?.toLowerCase() || 'en'); const calculateZoom = (width, height) => { let zoom = fitToWidth @@ -97,66 +128,137 @@ export default function Whiteboard(props) { return zoom; } + const hasShapeAccess = (id) => { + const owner = shapes[id]?.userId; + const isBackgroundShape = id?.includes('slide-background'); + const hasShapeAccess = !isBackgroundShape && ((owner && owner === currentUser?.userId) || !owner || isPresenter || isModerator); + return hasShapeAccess; + } + + const sendShapeChanges= (app, changedShapes, redo = false) => { + const invalidChange = Object.keys(changedShapes) + .find(id => !hasShapeAccess(id)); + if (invalidChange) { + notifyNotAllowedChange(intl); + // undo last command without persisting to not generate the onUndo/onRedo callback + if (!redo) { + const command = app.stack[app.pointer]; + app.pointer--; + return app.applyPatch(command.before, `undo`); + } else { + app.pointer++ + const command = app.stack[app.pointer] + return app.applyPatch(command.after, 'redo'); + } + }; + let deletedShapes = []; + Object.entries(changedShapes) + .forEach(([id, shape]) => { + if (!shape) deletedShapes.push(id); + else { + //checks to find any bindings assosiated with the changed shapes. + //If any, they may need to be updated as well. + const pageBindings = app.page.bindings; + if (pageBindings) { + Object.entries(pageBindings).map(([k,b]) => { + if (b.toId.includes(id)) { + const boundShape = app.getShape(b.fromId); + if (shapes[b.fromId] && !_.isEqual(boundShape, shapes[b.fromId])) { + const shapeBounds = app.getShapeBounds(b.fromId); + boundShape.size = [shapeBounds.width, shapeBounds.height]; + persistShape(boundShape, whiteboardId) + } + } + }) + } + if (!shape.id) { + // check it already exists (otherwise we need the full shape) + if (!shapes[id]) { + shape = app.getShape(id); + } + shape.id = id; + } + const shapeBounds = app.getShapeBounds(id); + const size = [shapeBounds.width, shapeBounds.height]; + if (!shapes[id] || (shapes[id] && !_.isEqual(shapes[id].size, size))) { + shape.size = size; + } + if (!shapes[id] || (shapes[id] && !shapes[id].userId)) shape.userId = currentUser?.userId; + persistShape(shape, whiteboardId); + } + }); + removeShapes(deletedShapes, whiteboardId); + } + const doc = React.useMemo(() => { const currentDoc = rDocument.current; let next = { ...currentDoc }; - let pageBindings = null; - let history = null; - let stack = null; let changed = false; if (next.pageStates[curPageId] && !_.isEqual(prevShapes, shapes)) { - // mergeDocument loses bindings and history, save it - pageBindings = tldrawAPI?.getPage(curPageId)?.bindings; - history = tldrawAPI?.history - stack = tldrawAPI?.stack + // set shapes as locked for those who aren't allowed to edit it + Object.entries(shapes).forEach(([shapeId, shape]) => { + if (!shape.isLocked && !hasShapeAccess(shapeId)) { + shape.isLocked = true; + } + }); + const removed = prevShapes && findRemoved(Object.keys(prevShapes),Object.keys((shapes))) + if (removed && removed.length > 0) { + tldrawAPI?.patchState( + { + document: { + pageStates: { + [curPageId]: { + selectedIds: tldrawAPI?.selectedIds?.filter(id => !removed.includes(id)) || [], + }, + }, + pages: { + [curPageId]: { + shapes: Object.fromEntries(removed.map((id) => [id, undefined])), + }, + }, + }, + }, + ); + } next.pages[curPageId].shapes = shapes; - changed = true; } - if (curPageId && next.pages[curPageId] && !next.pages[curPageId].shapes["slide-background-shape"]) { - next.assets[`slide-background-asset-${curPageId}`] = { - id: `slide-background-asset-${curPageId}`, - size: [slidePosition?.width || 0, slidePosition?.height || 0], - src: svgUri, - type: "image", - }; + if (curPageId && (!next.assets[`slide-background-asset-${curPageId}`]) || (svgUri && !_.isEqual(prevSvgUri, svgUri))) { + next.assets[`slide-background-asset-${curPageId}`] = assets[`slide-background-asset-${curPageId}`] + tldrawAPI?.patchState( + { + document: { + assets: assets + }, + }, + ); + } - next.pages[curPageId].shapes["slide-background-shape"] = { - assetId: `slide-background-asset-${curPageId}`, - childIndex: 0.5, - id: "slide-background-shape", - name: "Image", - type: TDShapeType.Image, - parentId: `${curPageId}`, - point: [0, 0], - isLocked: true, - size: [slidePosition?.width || 0, slidePosition?.height || 0], - style: { - dash: DashStyle.Draw, - size: SizeStyle.Medium, - color: ColorStyle.Blue, + if (changed && tldrawAPI) { + // merge patch manually (this improves performance and reduce side effects on fast updates) + const patch = { + document: { + pages: { + [curPageId]: { shapes: shapes } + }, }, }; - - changed = true; - } - - if (changed) { - if (pageBindings) next.pages[curPageId].bindings = pageBindings; - tldrawAPI?.mergeDocument(next); - if (tldrawAPI && history) tldrawAPI.history = history; - if (tldrawAPI && stack) tldrawAPI.stack = stack; + const prevState = tldrawAPI._state; + const nextState = Utils.deepMerge(tldrawAPI._state, patch); + const final = tldrawAPI.cleanup(nextState, prevState, patch, ''); + tldrawAPI._state = final; + tldrawAPI?.forceUpdate(); } // move poll result text to bottom right if (next.pages[curPageId]) { const pollResults = Object.entries(next.pages[curPageId].shapes) - .filter(([id, shape]) => shape.name.includes("poll-result")) + .filter(([id, shape]) => shape.name?.includes("poll-result")) for (const [id, shape] of pollResults) { if (_.isEqual(shape.point, [0, 0])) { const shapeBounds = tldrawAPI?.getShapeBounds(id); @@ -209,8 +311,11 @@ export default function Whiteboard(props) { if (cameraZoom && cameraZoom === 1) { tldrawAPI?.setCamera([slidePosition.x, slidePosition.y], newzoom); } else if (isMounting) { - if (!fitToWidth) { - setIsMounting(false); + setIsMounting(false); + const currentAspectRatio = Math.round((presentationWidth / presentationHeight) * 100) / 100; + const previousAspectRatio = Math.round((slidePosition.viewBoxWidth / slidePosition.viewBoxHeight) * 100) / 100; + // case where the presenter had fit-to-width enabled and he reloads the page + if (!fitToWidth && currentAspectRatio !== previousAspectRatio) { // wee need this to ensure tldraw updates the viewport size after re-mounting setTimeout(() => { tldrawAPI?.setCamera([slidePosition.x, slidePosition.y], newzoom, 'zoomed'); @@ -252,32 +357,34 @@ export default function Whiteboard(props) { } }, [zoomValue]); - // update zoom when presenter changes + // update zoom when presenter changes if the aspectRatio has changed React.useEffect(() => { if (tldrawAPI && isPresenter && curPageId && slidePosition && !isMounting) { const currentAspectRatio = Math.round((presentationWidth / presentationHeight) * 100) / 100; const previousAspectRatio = Math.round((slidePosition.viewBoxWidth / slidePosition.viewBoxHeight) * 100) / 100; - if (previousAspectRatio !== currentAspectRatio && fitToWidth) { - const zoom = calculateZoom(slidePosition.width, slidePosition.height) - tldrawAPI?.setCamera([0, 0], zoom); - const viewedRegionH = SlideCalcUtil.calcViewedRegionHeight(tldrawAPI?.viewport.width, slidePosition.height); - zoomSlide(parseInt(curPageId), podId, HUNDRED_PERCENT, viewedRegionH, 0, 0); - setZoom(HUNDRED_PERCENT); - zoomChanger(HUNDRED_PERCENT); - } else { - let viewedRegionW = SlideCalcUtil.calcViewedRegionWidth(tldrawAPI?.viewport.height, slidePosition.width); - let viewedRegionH = SlideCalcUtil.calcViewedRegionHeight(tldrawAPI?.viewport.width, slidePosition.height); - const camera = tldrawAPI?.getPageState()?.camera; - const zoomFitSlide = calculateZoom(slidePosition.width, slidePosition.height); - if (!fitToWidth && camera.zoom === zoomFitSlide) { - viewedRegionW = HUNDRED_PERCENT; - viewedRegionH = HUNDRED_PERCENT; - } - zoomSlide(parseInt(curPageId), podId, viewedRegionW, viewedRegionH, camera.point[0], camera.point[1]); - const zoomToolbar = Math.round((HUNDRED_PERCENT * camera.zoom) / zoomFitSlide * 100) / 100; - if (zoom !== zoomToolbar) { - setZoom(zoomToolbar); - zoomChanger(zoomToolbar); + if (previousAspectRatio !== currentAspectRatio) { + if (fitToWidth) { + const zoom = calculateZoom(slidePosition.width, slidePosition.height) + tldrawAPI?.setCamera([0, 0], zoom); + const viewedRegionH = SlideCalcUtil.calcViewedRegionHeight(tldrawAPI?.viewport.width, slidePosition.height); + zoomSlide(parseInt(curPageId), podId, HUNDRED_PERCENT, viewedRegionH, 0, 0); + setZoom(HUNDRED_PERCENT); + zoomChanger(HUNDRED_PERCENT); + } else if (!isMounting) { + let viewedRegionW = SlideCalcUtil.calcViewedRegionWidth(tldrawAPI?.viewport.height, slidePosition.width); + let viewedRegionH = SlideCalcUtil.calcViewedRegionHeight(tldrawAPI?.viewport.width, slidePosition.height); + const camera = tldrawAPI?.getPageState()?.camera; + const zoomFitSlide = calculateZoom(slidePosition.width, slidePosition.height); + if (!fitToWidth && camera.zoom === zoomFitSlide) { + viewedRegionW = HUNDRED_PERCENT; + viewedRegionH = HUNDRED_PERCENT; + } + zoomSlide(parseInt(curPageId), podId, viewedRegionW, viewedRegionH, camera.point[0], camera.point[1]); + const zoomToolbar = Math.round((HUNDRED_PERCENT * camera.zoom) / zoomFitSlide * 100) / 100; + if (zoom !== zoomToolbar) { + setZoom(zoomToolbar); + zoomChanger(zoomToolbar); + } } } } @@ -292,6 +399,7 @@ export default function Whiteboard(props) { const tdDelete = document.getElementById("TD-Delete"); const tdPrimaryTools = document.getElementById("TD-PrimaryTools"); const tdTools = document.getElementById("TD-Tools"); + if (tdToolsDots && tdDelete && tdPrimaryTools) { const size = props.height < SMALL_HEIGHT ? TOOLBAR_SMALL : TOOLBAR_LARGE; tdToolsDots.style.height = `${size}px`; @@ -326,8 +434,27 @@ export default function Whiteboard(props) { } }, [isPanning]); + React.useEffect(() => { + tldrawAPI?.setSetting('language', language); + }, [language]); + const onMount = (app) => { - app.setSetting('language', document.getElementsByTagName('html')[0]?.lang || 'en'); + const menu = document.getElementById("TD-Styles")?.parentElement; + if (menu) { + const MENU_OFFSET = `48px`; + menu.style.position = `relative`; + if (isRTL) { + menu.style.left = MENU_OFFSET; + } else { + menu.style.right = MENU_OFFSET; + } + + [...menu.children] + .sort((a,b)=> a?.id>b?.id?-1:1) + .forEach(n=> menu.appendChild(n)); + } + + app.setSetting('language', language); setTLDrawAPI(app); props.setTldrawAPI(app); // disable for non presenter that doesn't have multi user access @@ -335,47 +462,58 @@ export default function Whiteboard(props) { app.onPan = () => {}; app.setSelectedIds = () => {}; app.setHoveredId = () => {}; - } else { - // disable hover highlight for background slide shape - app.setHoveredId = (id) => { - if (id?.includes('slide-background')) return null; - app.patchState( - { - document: { - pageStates: { - [app.getPage()?.id]: { - hoveredId: id || [], - }, - }, - }, - }, - `set_hovered_id` - ); - }; - // disable selecting background slide shape - app.setSelectedIds = (ids) => { - ids = ids.filter(id => !id.includes('slide-background')) - app.patchState( - { - document: { - pageStates: { - [app.getPage()?.id]: { - selectedIds: ids || [], - }, - }, - }, - }, - `selected` - ); - }; } if (curPageId) { app.changePage(curPageId); + setIsMounting(true); + } + + if (history) { + app.replaceHistory(history); } }; const onPatch = (e, t, reason) => { + // don't allow select others shapes for editing if don't have permission + if (reason && reason.includes("set_editing_id")) { + if (!hasShapeAccess(e.pageState.editingId)) { + e.pageState.editingId = null; + } + } + // don't allow hover others shapes for editing if don't have permission + if (reason && reason.includes("set_hovered_id")) { + if (!hasShapeAccess(e.pageState.hoveredId)) { + e.pageState.hoveredId = null; + } + } + // don't allow select others shapes if don't have permission + if (reason && reason.includes("selected")) { + const validIds = []; + e.pageState.selectedIds.forEach(id => hasShapeAccess(id) && validIds.push(id)); + e.pageState.selectedIds = validIds; + e.patchState( + { + document: { + pageStates: { + [e.getPage()?.id]: { + selectedIds: validIds, + }, + }, + }, + } + ); + } + // don't allow selecting others shapes with ctrl (brush) + if (e?.session?.type === "brush" && e?.session?.status === "brushing") { + const validIds = []; + e.pageState.selectedIds.forEach(id => hasShapeAccess(id) && validIds.push(id)); + e.pageState.selectedIds = validIds; + if (!validIds.find(id => id === e.pageState.hoveredId)) { + e.pageState.hoveredId = undefined; + } + } + if (reason && isPresenter && (reason.includes("zoomed") || reason.includes("panned"))) { const camera = tldrawAPI.getPageState()?.camera; @@ -438,19 +576,90 @@ export default function Whiteboard(props) { } } - if (reason && reason === 'patched_shapes') { + if (reason && reason === 'patched_shapes' && e?.session?.type === "edit" && e?.session?.initialShape?.type === "text") { const patchedShape = e?.getShape(e?.getPageState()?.editingId); - if (patchedShape?.type === 'text') { + if (!shapes[patchedShape.id]) { + patchedShape.userId = currentUser?.userId; persistShape(patchedShape, whiteboardId); + } else { + const diff = { + id: patchedShape.id, + point: patchedShape.point, + text: patchedShape.text + } + persistShape(diff, whiteboardId); } } }; + const onUndo = (app) => { + if (app.currentPageId !== curPageId) { + if (isPresenter) { + // change slide for others + skipToSlide(Number.parseInt(app.currentPageId), podId) + } else { + // ignore, stay on same page + app.changePage(curPageId); + } + return; + } + const lastCommand = app.stack[app.pointer+1]; + const changedShapes = lastCommand?.before?.document?.pages[app.currentPageId]?.shapes; + if (changedShapes) { + sendShapeChanges(app, changedShapes, true); + } + }; + + const onRedo = (app) => { + if (app.currentPageId !== curPageId) { + if (isPresenter) { + // change slide for others + skipToSlide(Number.parseInt(app.currentPageId), podId) + } else { + // ignore, stay on same page + app.changePage(curPageId); + } + return; + } + const lastCommand = app.stack[app.pointer]; + const changedShapes = lastCommand?.after?.document?.pages[app.currentPageId]?.shapes; + if (changedShapes) { + sendShapeChanges(app, changedShapes); + } + }; + + const onCommand = (app, command, reason) => { + setHistory(app.history); + const changedShapes = command.after?.document?.pages[app.currentPageId]?.shapes; + if (!isMounting && app.currentPageId !== curPageId) { + // can happen then the "move to page action" is called, or using undo after changing a page + const newWhiteboardId = curPres.pages.find(page => page.num === Number.parseInt(app.currentPageId)).id; + //remove from previous page and persist on new + changedShapes && removeShapes(Object.keys(changedShapes), whiteboardId); + changedShapes && Object.entries(changedShapes) + .forEach(([id, shape]) => { + const shapeBounds = app.getShapeBounds(id); + shape.size = [shapeBounds.width, shapeBounds.height]; + persistShape(shape, newWhiteboardId); + }); + if (isPresenter) { + // change slide for others + skipToSlide(Number.parseInt(app.currentPageId), podId) + } else { + // ignore, stay on same page + app.changePage(curPageId); + } + } + else if (changedShapes) { + sendShapeChanges(app, changedShapes); + } + }; + const webcams = document.getElementById('cameraDock'); const dockPos = webcams?.getAttribute("data-position"); const editableWB = ( { - e?.selectedIds?.map(id => { - const shape = e.getShape(id); - persistShape(shape, whiteboardId); - const children = shape.children; - children && children.forEach(c => { - const childShape = e.getShape(c); - const shapeBounds = e.getShapeBounds(c); - childShape.size = [shapeBounds.width, shapeBounds.height]; - persistShape(childShape, whiteboardId) - }); - }) - const pageShapes = e.state.document.pages[e.getPage()?.id]?.shapes; - let shapesIdsToRemove = findRemoved(Object.keys(shapes), Object.keys(pageShapes)) - if (shapesIdsToRemove.length) { - // add a little delay, wee need to make sure children are updated first - setTimeout(() => removeShapes(shapesIdsToRemove, whiteboardId), 200); - } - }} - - onRedo={(e, s) => { - e?.selectedIds?.map(id => { - const shape = e.getShape(id); - persistShape(shape, whiteboardId); - const children = shape.children; - children && children.forEach(c => { - const childShape = e.getShape(c); - const shapeBounds = e.getShapeBounds(c); - childShape.size = [shapeBounds.width, shapeBounds.height]; - persistShape(childShape, whiteboardId) - }); - }); - const pageShapes = e.state.document.pages[e.getPage()?.id]?.shapes; - let shapesIdsToRemove = findRemoved(Object.keys(shapes), Object.keys(pageShapes)) - if (shapesIdsToRemove.length) { - // add a little delay, wee need to make sure children are updated first - setTimeout(() => removeShapes(shapesIdsToRemove, whiteboardId), 200); - } - }} - - onCommand={(e, s, g) => { - if (s?.id.includes('move_to_page')) { - let groupShapes = []; - let nonGroupShapes = []; - let movedShapes = {}; - e.selectedIds.forEach(id => { - const shape = e.getShape(id); - if (shape.type === 'group') - groupShapes.push(id); - else - nonGroupShapes.push(id); - movedShapes[id] = e.getShape(id); - }); - //remove shapes on origin page - let idsToRemove = nonGroupShapes.concat(groupShapes); - removeShapes(idsToRemove, whiteboardId); - //persist shapes for destination page - const newWhiteboardId = curPres.pages.find(page => page.num === Number.parseInt(e.getPage()?.id)).id; - let idsToInsert = groupShapes.concat(nonGroupShapes); - idsToInsert.forEach(id => { - persistShape(movedShapes[id], newWhiteboardId); - const children = movedShapes[id].children; - children && children.forEach(c => { - persistShape(e.getShape(c), newWhiteboardId) - }); - }); - if (isPresenter) { - // change slide for others - skipToSlide(Number.parseInt(e.getPage()?.id), podId) - } else { - // ignore, stay on same page - e.changePage(curPageId); - } - return; - } - - if (s?.id.includes('ungroup')) { - e?.selectedIds?.map(id => { - persistShape(e.getShape(id), whiteboardId); - }) - - // check for deleted shapes - const pageShapes = e.state.document.pages[e.getPage()?.id]?.shapes; - let shapesIdsToRemove = findRemoved(Object.keys(shapes), Object.keys(pageShapes)) - if (shapesIdsToRemove.length) { - // add a little delay, wee need to make sure children are updated first - setTimeout(() => removeShapes(shapesIdsToRemove, whiteboardId), 200); - } - return; - } - - const conditions = [ - "session:complete", "style", "updated_shapes", "duplicate", "stretch", - "align", "move", "delete", "create", "flip", "toggle", "group", "translate", - "transform_single", "arrow", "edit", "erase", "rotate", - ] - if (conditions.some(el => s?.id?.startsWith(el))) { - e.selectedIds.forEach(id => { - const shape = e.getShape(id); - const shapeBounds = e.getShapeBounds(id); - shape.size = [shapeBounds.width, shapeBounds.height]; - persistShape(shape, whiteboardId); - //checks to find any bindings assosiated with the selected shapes. - //If any, they need to be updated as well. - const pageBindings = e.bindings; - const boundShapes = {}; - if (pageBindings) { - Object.entries(pageBindings).map(([k,b]) => { - if (b.toId.includes(id)) { - boundShapes[b.fromId] = e.getShape(b.fromId); - } - }) - } - //persist shape(s) that was updated by the client and any shapes bound to it. - Object.entries(boundShapes).map(([k,bs]) => { - const shapeBounds = e.getShapeBounds(k); - bs.size = [shapeBounds.width, shapeBounds.height]; - persistShape(bs, whiteboardId) - }) - const children = e.getShape(id).children; - //also persist children of the selected shape (grouped shapes) - children && children.forEach(c => { - const shape = e.getShape(c); - const shapeBounds = e.getShapeBounds(c); - shape.size = [shapeBounds.width, shapeBounds.height]; - persistShape(shape, whiteboardId) - // also persist shapes that are bound to the children - if (pageBindings) { - Object.entries(pageBindings).map(([k,b]) => { - if (!(b.fromId in boundShapes) && b.toId.includes(c)) { - const shape = e.getShape(b.fromId); - persistShape(shape, whiteboardId) - boundShapes[b.fromId] = shape; - } - }) - } - }) - }); - // draw shapes - Object.entries(e.state.document.pages[e.getPage()?.id]?.shapes) - .filter(([k, s]) => s?.type === 'draw') - .forEach(([k, s]) => { - if (!prevShapes[k] && !k.includes('slide-background')) { - const shapeBounds = e.getShapeBounds(k); - s.size = [shapeBounds.width, shapeBounds.height]; - persistShape(s, whiteboardId); - } - }); - - // check for deleted shapes - const pageShapes = e.state.document.pages[e.getPage()?.id]?.shapes; - let shapesIdsToRemove = findRemoved(Object.keys(shapes), Object.keys(pageShapes)) - let groups = []; - let nonGroups = []; - // if we have groups, we need to make sure they are removed lastly - shapesIdsToRemove.forEach(shape => { - if (shapes[shape].type === 'group') { - groups.push(shape); - } else { - nonGroups.push(shape); - } - }); - if (shapesIdsToRemove.length) { - shapesIdsToRemove = nonGroups.concat(groups); - removeShapes(shapesIdsToRemove, whiteboardId); - } - } - }} + onUndo={onUndo} + onRedo={onRedo} + onCommand={onCommand} /> ); @@ -670,7 +714,10 @@ export default function Whiteboard(props) { isPanning={isPanning} > {hasWBAccess || isPresenter ? editableWB : readOnlyWB} - + ); diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx index 9986b4c3ba..b1b001ec0b 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx @@ -6,6 +6,14 @@ import { UsersContext } from "../components-data/users-context/context"; import Auth from "/imports/ui/services/auth"; import PresentationToolbarService from '../presentation/presentation-toolbar/service'; import { layoutSelect } from '../layout/context'; +import { + ColorStyle, + DashStyle, + SizeStyle, + TDShapeType, +} from "@tldraw/tldraw"; + +const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator; const WhiteboardContainer = (props) => { const usingUsersContext = useContext(UsersContext); @@ -15,13 +23,39 @@ const WhiteboardContainer = (props) => { const { users } = usingUsersContext; const currentUser = users[Auth.meetingID][Auth.userID]; const isPresenter = currentUser.presenter; - return + const isModerator = currentUser.role === ROLE_MODERATOR; + return }; -export default withTracker(({ whiteboardId, curPageId, intl, zoomChanger }) => { +export default withTracker(({ whiteboardId, curPageId, intl, zoomChanger, slidePosition, svgUri }) => { const shapes = Service.getShapes(whiteboardId, curPageId, intl); const curPres = Service.getCurrentPres(); + shapes["slide-background-shape"] = { + assetId: `slide-background-asset-${curPageId}`, + childIndex: -1, + id: "slide-background-shape", + name: "Image", + type: TDShapeType.Image, + parentId: `${curPageId}`, + point: [0, 0], + isLocked: true, + size: [slidePosition?.width || 0, slidePosition?.height || 0], + style: { + dash: DashStyle.Draw, + size: SizeStyle.Medium, + color: ColorStyle.Blue, + }, + }; + + const assets = {} + assets[`slide-background-asset-${curPageId}`] = { + id: `slide-background-asset-${curPageId}`, + size: [slidePosition?.width || 0, slidePosition?.height || 0], + src: svgUri, + type: "image", + }; + return { initDefaultPages: Service.initDefaultPages, persistShape: Service.persistShape, @@ -29,10 +63,12 @@ export default withTracker(({ whiteboardId, curPageId, intl, zoomChanger }) => { hasMultiUserAccess: Service.hasMultiUserAccess, changeCurrentSlide: Service.changeCurrentSlide, shapes: shapes, + assets: assets, curPres, removeShapes: Service.removeShapes, zoomSlide: PresentationToolbarService.zoomSlide, skipToSlide: PresentationToolbarService.skipToSlide, zoomChanger: zoomChanger, + notifyNotAllowedChange: Service.notifyNotAllowedChange, }; })(WhiteboardContainer); diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/cursors/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/cursors/component.jsx index a73623cc59..76702bbeb0 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/cursors/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/cursors/component.jsx @@ -1,346 +1,380 @@ -import * as React from "react"; -const XS_OFFSET = 8; -const SMALL_OFFSET = 18; -const XL_OFFSET = 85; -const BOTTOM_CAM_HANDLE_HEIGHT = 10; -const PRES_TOOLBAR_HEIGHT = 35; - -function usePrevious(value) { - const ref = React.useRef(); - React.useEffect(() => { - ref.current = value; - }, [value]); - return ref.current; -} - -const renderCursor = ( - name, - color, - x, - y, - currentPoint, - pageState, - isMultiUserActive, - owner = false, - -) => { - const z = !owner ? 2 : 1; - let _x = null; - let _y = null; - - if (!currentPoint) { - _x = (x + pageState?.camera?.point[0]) * pageState?.camera?.zoom; - _y = (y + pageState?.camera?.point[1]) * pageState?.camera?.zoom; - } - - return ( - <> -
- - {isMultiUserActive &&
- {name} -
} - - ); -}; - -const PositionLabel = (props) => { - const { - currentUser, - currentPoint, - pageState, - publishCursorUpdate, - whiteboardId, - pos, - isMultiUserActive, - } = props; - - const { name, color } = currentUser; - const prevCurrentPoint = usePrevious(currentPoint); - - React.useEffect(() => { - try { - const point = [pos.x, pos.y]; - publishCursorUpdate({ - xPercent: - point[0] / pageState?.camera?.zoom - pageState?.camera?.point[0], - yPercent: - point[1] / pageState?.camera?.zoom - pageState?.camera?.point[1], - whiteboardId, - }); - } catch (e) { - console.log(e); - } - }, [pos?.x, pos?.y]); - - return ( - <> -
- {renderCursor(name, color, pos.x, pos.y, currentPoint, props.pageState, isMultiUserActive(whiteboardId))} -
- - ); -}; - -export default function Cursors(props) { - let cursorWrapper = React.useRef(null); - const [active, setActive] = React.useState(false); - const [pos, setPos] = React.useState({ x: 0, y: 0 }); - const { - whiteboardId, - otherCursors, - currentUser, - tldrawAPI, - publishCursorUpdate, - children, - isViewersCursorLocked, - hasMultiUserAccess, - isMultiUserActive, - application, - isPanning, - } = props; - - const start = () => setActive(true); - - const end = () => { - if (whiteboardId) { - publishCursorUpdate({ - xPercent: -1.0, - yPercent: -1.0, - whiteboardId, - }); - }; - setActive(false); - } - - const moved = (event) => { - const { type, x, y } = event; - const nav = document.getElementById('Navbar'); - const getSibling = (el) => el?.previousSibling || null; - const panel = getSibling(nav); - const webcams = document.getElementById('cameraDock'); - const subPanel = panel && getSibling(panel); - const camPosition = document.getElementById('layout')?.getAttribute('data-cam-position') || null; - const sl = document.getElementById('layout')?.getAttribute('data-layout'); - const presentationContainer = document.querySelector('[data-test="presentationContainer"]'); - const presentation = document.getElementById('currentSlideText')?.parentElement; - let yOffset = 0; - let xOffset = 0; - const calcPresOffset = () => { - yOffset += (parseFloat(presentationContainer?.style?.height) - (parseFloat(presentation?.style?.height) + (currentUser.presenter ? PRES_TOOLBAR_HEIGHT : 0))) / 2; - xOffset += (parseFloat(presentationContainer?.style?.width) - parseFloat(presentation?.style?.width)) / 2; - } - // If the presentation container is the full screen element we don't need any offsets - const fsEl = document?.webkitFullscreenElement || document?.fullscreenElement; - if (fsEl?.getAttribute('data-test') === "presentationContainer") { - calcPresOffset(); - return setPos({ x: x - xOffset, y: y - yOffset }); - } - if (nav) yOffset += parseFloat(nav?.style?.height); - if (panel) xOffset += parseFloat(panel?.style?.width); - if (subPanel) xOffset += parseFloat(subPanel?.style?.width); - - // disable native tldraw eraser animation - const eraserLine = document.getElementsByClassName('tl-erase-line')[0]; - if (eraserLine) eraserLine.style.display = `none`; - - if (type === 'touchmove') { - calcPresOffset(); - !active && setActive(true); - return setPos({ x: event?.changedTouches[0]?.clientX - xOffset, y: event?.changedTouches[0]?.clientY - yOffset }); - } - - if (document?.documentElement?.dir === 'rtl') { - xOffset = 0; - if (presentationContainer && presentation) { - calcPresOffset(); - } - if (sl.includes('custom')) { - if (webcams) { - if (camPosition === 'contentTop' || !camPosition) { - yOffset += (parseFloat(webcams?.style?.height || 0) + BOTTOM_CAM_HANDLE_HEIGHT); - } - if (camPosition === 'contentBottom') { - yOffset -= BOTTOM_CAM_HANDLE_HEIGHT; - } - if (camPosition === 'contentRight') { - xOffset += (parseFloat(webcams?.style?.width || 0) + SMALL_OFFSET); - } - } - } - if (sl?.includes('smart')) { - if (panel || subPanel) { - const dockPos = webcams?.getAttribute("data-position"); - if (dockPos === 'contentTop') { - yOffset += (parseFloat(webcams?.style?.height || 0) + SMALL_OFFSET); - } - } - } - if (webcams && sl?.includes('videoFocus')) { - xOffset += parseFloat(nav?.style?.width); - yOffset += (parseFloat(panel?.style?.height || 0) - XL_OFFSET); - } - } else { - if (sl.includes('custom')) { - if (webcams) { - if (camPosition === 'contentTop' || !camPosition) { - yOffset += (parseFloat(webcams?.style?.height) || 0) + XS_OFFSET; - } - if (camPosition === 'contentBottom') { - yOffset -= BOTTOM_CAM_HANDLE_HEIGHT; - } - if (camPosition === 'contentLeft') { - xOffset += (parseFloat(webcams?.style?.width) || 0) + SMALL_OFFSET; - } - } - } - - if (sl.includes('smart')) { - if (panel || subPanel) { - const dockPos = webcams?.getAttribute("data-position"); - if (dockPos === 'contentLeft') { - xOffset += (parseFloat(webcams?.style?.width || 0) + SMALL_OFFSET); - } - if (dockPos === 'contentTop') { - yOffset += (parseFloat(webcams?.style?.height || 0) + SMALL_OFFSET); - } - } - if (!panel && !subPanel) { - if (webcams) { - xOffset = parseFloat(webcams?.style?.width || 0) + SMALL_OFFSET; - } - } - } - if (sl?.includes('videoFocus')) { - if (webcams) { - xOffset = parseFloat(subPanel?.style?.width); - yOffset = parseFloat(panel?.style?.height); - } - } - if (presentationContainer && presentation) { - calcPresOffset(); - } - } - - return setPos({ x: event.x - xOffset, y: event.y - yOffset }); - } - - React.useEffect(() => { - !cursorWrapper.hasOwnProperty("mouseenter") && - cursorWrapper?.addEventListener("mouseenter", start); - - !cursorWrapper.hasOwnProperty("mouseleave") && - cursorWrapper?.addEventListener("mouseleave", end); - - !cursorWrapper.hasOwnProperty("touchend") && - cursorWrapper?.addEventListener("touchend", end); - - !cursorWrapper.hasOwnProperty("mousemove") && - cursorWrapper?.addEventListener("mousemove", moved); - - !cursorWrapper.hasOwnProperty("touchmove") && - cursorWrapper?.addEventListener("touchmove", moved); - }, [cursorWrapper, whiteboardId]); - - React.useEffect(() => { - return () => { - if (cursorWrapper) { - cursorWrapper.removeEventListener('mouseenter', start); - cursorWrapper.removeEventListener('mouseleave', end); - cursorWrapper.removeEventListener('mousemove', moved); - cursorWrapper.removeEventListener('touchend', end); - cursorWrapper.removeEventListener('touchmove', moved); - } - } - }); - - const multiUserAccess = hasMultiUserAccess(whiteboardId, currentUser?.userId); - let cursorType = multiUserAccess || currentUser?.presenter ? "none" : "default"; - if (isPanning) cursorType = 'grab'; - - return ( - (cursorWrapper = r)}> -
- {(active && multiUserAccess || (active && currentUser?.presenter)) && ( - - )} - {children} -
- {otherCursors - .filter((c) => c?.xPercent && c?.yPercent) - .filter((c) => { - if ((isViewersCursorLocked && c?.role !== "VIEWER") || !isViewersCursorLocked || currentUser?.presenter) { - return c; - } - return null; - }) - .map((c) => { - if (c && currentUser.userId !== c?.userId) { - if (c.presenter) { - return renderCursor( - c?.userName, - "#C70039", - c?.xPercent, - c?.yPercent, - null, - tldrawAPI?.getPageState(), - isMultiUserActive(whiteboardId), - true - ); - } - - return hasMultiUserAccess(whiteboardId, c?.userId) && ( - renderCursor( - c?.userName, - "#AFE1AF", - c?.xPercent, - c?.yPercent, - null, - tldrawAPI?.getPageState(), - isMultiUserActive(whiteboardId), - true - ) - ); - } - })} -
- ); -} +import * as React from 'react'; + +const XS_OFFSET = 8; +const SMALL_OFFSET = 18; +const XL_OFFSET = 85; +const BOTTOM_CAM_HANDLE_HEIGHT = 10; +const PRES_TOOLBAR_HEIGHT = 35; + +const Cursor = (props) => { + const { + name, + color, + x, + y, + currentPoint, + pageState, + isMultiUserActive, + owner = false, + } = props; + + const z = !owner ? 2 : 1; + let _x = null; + let _y = null; + + if (!currentPoint) { + _x = (x + pageState?.camera?.point[0]) * pageState?.camera?.zoom; + _y = (y + pageState?.camera?.point[1]) * pageState?.camera?.zoom; + } + + return ( + <> +
+ + {isMultiUserActive && ( +
+ {name} +
+ )} + + ); +}; + +const PositionLabel = (props) => { + const { + currentUser, + currentPoint, + pageState, + publishCursorUpdate, + whiteboardId, + pos, + isMultiUserActive, + } = props; + + const { name, color, userId } = currentUser; + const { x, y } = pos; + + React.useEffect(() => { + try { + const point = [x, y]; + publishCursorUpdate({ + xPercent: + point[0] / pageState?.camera?.zoom - pageState?.camera?.point[0], + yPercent: + point[1] / pageState?.camera?.zoom - pageState?.camera?.point[1], + whiteboardId, + }); + } catch (e) { + console.log(e); + } + }, [x, y]); + + return ( + <> +
+ +
+ + ); +}; + +export default function Cursors(props) { + let cursorWrapper = React.useRef(null); + const [active, setActive] = React.useState(false); + const [pos, setPos] = React.useState({ x: 0, y: 0 }); + const { + whiteboardId, + otherCursors, + currentUser, + tldrawAPI, + publishCursorUpdate, + children, + isViewersCursorLocked, + hasMultiUserAccess, + isMultiUserActive, + isPanning, + } = props; + + const start = () => setActive(true); + + const end = () => { + if (whiteboardId) { + publishCursorUpdate({ + xPercent: -1.0, + yPercent: -1.0, + whiteboardId, + }); + } + setActive(false); + }; + + const moved = (event) => { + const { type, x, y } = event; + const nav = document.getElementById('Navbar'); + const getSibling = (el) => { + if (el?.previousSibling && !el?.previousSibling?.hasAttribute('data-test')) { + return el?.previousSibling; + } + return null; + }; + const panel = getSibling(nav); + const webcams = document.getElementById('cameraDock'); + const subPanel = panel && getSibling(panel); + const camPosition = document.getElementById('layout')?.getAttribute('data-cam-position') || null; + const sl = document.getElementById('layout')?.getAttribute('data-layout'); + const presentationContainer = document.querySelector('[data-test="presentationContainer"]'); + const presentation = document.getElementById('currentSlideText')?.parentElement; + const banners = document.querySelectorAll('[data-test="notificationBannerBar"]'); + let yOffset = 0; + let xOffset = 0; + const calcPresOffset = () => { + yOffset + += (parseFloat(presentationContainer?.style?.height) + - (parseFloat(presentation?.style?.height) + + (currentUser.presenter ? PRES_TOOLBAR_HEIGHT : 0)) + ) / 2; + xOffset + += (parseFloat(presentationContainer?.style?.width) + - parseFloat(presentation?.style?.width) + ) / 2; + }; + // If the presentation container is the full screen element we don't + // need any offsets + const { webkitFullscreenElement, fullscreenElement } = document; + const fsEl = webkitFullscreenElement || fullscreenElement; + if (fsEl?.getAttribute('data-test') === 'presentationContainer') { + calcPresOffset(); + return setPos({ x: x - xOffset, y: y - yOffset }); + } + if (nav) yOffset += parseFloat(nav?.style?.height); + if (panel) xOffset += parseFloat(panel?.style?.width); + if (subPanel) xOffset += parseFloat(subPanel?.style?.width); + + // disable native tldraw eraser animation + const eraserLine = document.getElementsByClassName('tl-erase-line')[0]; + if (eraserLine) eraserLine.style.display = 'none'; + + if (type === 'touchmove') { + calcPresOffset(); + if (!active) { + setActive(true); + } + const newX = event?.changedTouches[0]?.clientX - xOffset; + const newY = event?.changedTouches[0]?.clientY - yOffset; + return setPos({ x: newX, y: newY }); + } + + if (document?.documentElement?.dir === 'rtl') { + xOffset = 0; + if (presentationContainer && presentation) { + calcPresOffset(); + } + if (sl.includes('custom')) { + if (webcams) { + if (camPosition === 'contentTop' || !camPosition) { + yOffset += (parseFloat(webcams?.style?.height || 0) + BOTTOM_CAM_HANDLE_HEIGHT); + } + if (camPosition === 'contentBottom') { + yOffset -= BOTTOM_CAM_HANDLE_HEIGHT; + } + if (camPosition === 'contentRight') { + xOffset += (parseFloat(webcams?.style?.width || 0) + SMALL_OFFSET); + } + } + } + if (sl?.includes('smart')) { + if (panel || subPanel) { + const dockPos = webcams?.getAttribute('data-position'); + if (dockPos === 'contentTop') { + yOffset += (parseFloat(webcams?.style?.height || 0) + SMALL_OFFSET); + } + } + } + if (webcams && sl?.includes('videoFocus')) { + xOffset += parseFloat(nav?.style?.width); + yOffset += (parseFloat(panel?.style?.height || 0) - XL_OFFSET); + } + } else { + if (sl.includes('custom')) { + if (webcams) { + if (camPosition === 'contentTop' || !camPosition) { + yOffset += (parseFloat(webcams?.style?.height) || 0) + XS_OFFSET; + } + if (camPosition === 'contentBottom') { + yOffset -= BOTTOM_CAM_HANDLE_HEIGHT; + } + if (camPosition === 'contentLeft') { + xOffset += (parseFloat(webcams?.style?.width) || 0) + SMALL_OFFSET; + } + } + } + + if (sl.includes('smart')) { + if (panel || subPanel) { + const dockPos = webcams?.getAttribute('data-position'); + if (dockPos === 'contentLeft') { + xOffset += (parseFloat(webcams?.style?.width || 0) + SMALL_OFFSET); + } + if (dockPos === 'contentTop') { + yOffset += (parseFloat(webcams?.style?.height || 0) + SMALL_OFFSET); + } + } + if (!panel && !subPanel) { + if (webcams) { + xOffset = parseFloat(webcams?.style?.width || 0) + SMALL_OFFSET; + } + } + } + if (sl?.includes('videoFocus')) { + if (webcams) { + xOffset = parseFloat(subPanel?.style?.width); + yOffset = parseFloat(panel?.style?.height); + } + } + if (presentationContainer && presentation) { + calcPresOffset(); + } + } + + if (banners) { + banners.forEach((el) => { + yOffset += parseFloat(window.getComputedStyle(el).height); + }); + } + + return setPos({ x: event.x - xOffset, y: event.y - yOffset }); + }; + + React.useEffect(() => { + if (!Object.prototype.hasOwnProperty.call(cursorWrapper, 'mouseenter')) { + cursorWrapper?.addEventListener('mouseenter', start); + } + if (!Object.prototype.hasOwnProperty.call(cursorWrapper, 'mouseleave')) { + cursorWrapper?.addEventListener('mouseleave', end); + } + if (!Object.prototype.hasOwnProperty.call(cursorWrapper, 'touchend')) { + cursorWrapper?.addEventListener('touchend', end); + } + if (!Object.prototype.hasOwnProperty.call(cursorWrapper, 'mousemove')) { + cursorWrapper?.addEventListener('mousemove', moved); + } + if (!Object.prototype.hasOwnProperty.call(cursorWrapper, 'touchmove')) { + cursorWrapper?.addEventListener('touchmove', moved); + } + }, [cursorWrapper, whiteboardId]); + + React.useEffect(() => () => { + if (cursorWrapper) { + cursorWrapper.removeEventListener('mouseenter', start); + cursorWrapper.removeEventListener('mouseleave', end); + cursorWrapper.removeEventListener('mousemove', moved); + cursorWrapper.removeEventListener('touchend', end); + cursorWrapper.removeEventListener('touchmove', moved); + } + }); + + const multiUserAccess = hasMultiUserAccess(whiteboardId, currentUser?.userId); + let cursorType = multiUserAccess || currentUser?.presenter ? 'none' : 'default'; + if (isPanning) cursorType = 'grab'; + + return ( + { cursorWrapper = r; }}> +
+ {((active && multiUserAccess) || (active && currentUser?.presenter)) && ( + + )} + {children} +
+ {otherCursors + .filter((c) => c?.xPercent && c.xPercent !== -1.0 && c?.yPercent && c.yPercent !== -1.0) + .filter((c) => { + if ((isViewersCursorLocked && c?.role !== 'VIEWER') + || !isViewersCursorLocked + || currentUser?.presenter + ) { + return c; + } + return null; + }) + .map((c) => { + if (c && currentUser.userId !== c?.userId) { + if (c.presenter) { + return ( + + ); + } + + return hasMultiUserAccess(whiteboardId, c?.userId) + && ( + + ); + } + return null; + })} +
+ ); +} diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/service.js b/bigbluebutton-html5/imports/ui/components/whiteboard/service.js index e6ad5a65a1..4d625926be 100755 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/service.js +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/service.js @@ -7,6 +7,8 @@ import { makeCall } from '/imports/ui/services/api'; import PresentationService from '/imports/ui/components/presentation/service'; import PollService from '/imports/ui/components/poll/service'; import logger from '/imports/startup/client/logger'; +import { defineMessages } from 'react-intl'; +import { notify } from '/imports/ui/services/notification'; const Annotations = new Mongo.Collection(null); @@ -14,6 +16,13 @@ const UnsentAnnotations = new Mongo.Collection(null); const ANNOTATION_CONFIG = Meteor.settings.public.whiteboard.annotations; const DRAW_END = ANNOTATION_CONFIG.status.end; +const intlMessages = defineMessages({ + notifyNotAllowedChange: { + id: 'app.whiteboard.annotations.notAllowed', + description: 'Label shown in toast when the user make a change on a shape he doesnt have permission', + }, +}); + let annotationsStreamListener = null; const clearPreview = (annotation) => { @@ -32,7 +41,7 @@ function handleAddedAnnotation({ annotation, }) { const isOwn = Auth.meetingID === meetingId && Auth.userID === userId; - const query = addAnnotationQuery(meetingId, whiteboardId, userId, annotation); + const query = addAnnotationQuery(meetingId, whiteboardId, userId, annotation, Annotations); Annotations.upsert(query.selector, query.modifier); @@ -146,7 +155,12 @@ const sendAnnotation = (annotation) => { // reconnected. With this it will miss things if (!Meteor.status().connected) return; - annotationsQueue.push(annotation); + const index = annotationsQueue.findIndex(ann => ann.id === annotation.id); + if (index !== -1) { + annotationsQueue[index] = annotation; + } else { + annotationsQueue.push(annotation); + } if (!annotationsSenderIsRunning) setTimeout(proccessAnnotationsQueue, annotationsBufferTimeMin); }; @@ -295,7 +309,7 @@ const persistShape = (shape, whiteboardId) => { id: shape.id, annotationInfo: shape, wbId: whiteboardId, - userId: shape.userId ? shape.userId : Auth.userID, + userId: Auth.userID, }; sendAnnotation(annotation); @@ -343,8 +357,8 @@ const getShapes = (whiteboardId, curPageId, intl) => { dash: "draw" }, } + annotation.annotationInfo.questionType = false; } - annotation.annotationInfo.userId = annotation.userId; result[annotation.annotationInfo.id] = annotation.annotationInfo; }); return result; @@ -379,6 +393,10 @@ const initDefaultPages = (count = 1) => { return { pages, pageStates }; }; +const notifyNotAllowedChange = (intl) => { + if (intl) notify(intl.formatMessage(intlMessages.notifyNotAllowedChange), 'warning', 'whiteboard'); +}; + export { initDefaultPages, Annotations, @@ -402,4 +420,5 @@ export { removeShapes, changeCurrentSlide, clearFakeAnnotations, + notifyNotAllowedChange, }; diff --git a/bigbluebutton-html5/imports/ui/services/features/index.js b/bigbluebutton-html5/imports/ui/services/features/index.js index 141d7a5923..b214d41a1e 100644 --- a/bigbluebutton-html5/imports/ui/services/features/index.js +++ b/bigbluebutton-html5/imports/ui/services/features/index.js @@ -63,3 +63,7 @@ export function isDownloadPresentationWithAnnotationsEnabled() { export function isImportPresentationWithAnnotationsFromBreakoutRoomsEnabled() { return getDisabledFeatures().indexOf('importPresentationWithAnnotationsFromBreakoutRooms') === -1; } + +export function isImportSharedNotesFromBreakoutRoomsEnabled() { + return getDisabledFeatures().indexOf('importSharedNotesFromBreakoutRooms') === -1; +} diff --git a/bigbluebutton-html5/imports/ui/services/locale/index.js b/bigbluebutton-html5/imports/ui/services/locale/index.js new file mode 100644 index 0000000000..aba87832bb --- /dev/null +++ b/bigbluebutton-html5/imports/ui/services/locale/index.js @@ -0,0 +1,75 @@ +import { createIntl } from 'react-intl'; + +const FALLBACK_ON_EMPTY_STRING = Meteor.settings.public.app.fallbackOnEmptyLocaleString; + +/** + * Use this if you need any translation outside of React lifecyle. + */ +class BBBIntl { + _intl = { + tracker: new Tracker.Dependency(), + value: undefined, + }; + + _fetching = { + tracker: new Tracker.Dependency(), + value: true, + }; + + constructor({ fallback }) { + this._fallback = fallback; + } + + setLocale(locale, messages) { + this.intl = createIntl({ + locale, + messages, + fallbackOnEmptyString: this._fallback, + }); + + this.fetching = false; + } + + set fetching(value) { + this._fetching.value = value; + this._fetching.tracker.changed(); + } + + get fetching() { + this._fetching.tracker.depend(); + return this._fetching.value; + } + + set intl(value) { + this._intl.value = value; + this._intl.tracker.changed(); + } + + get intl() { + this._intl.tracker.depend(); + return this._intl.value; + } + + formatMessage(descriptor, values, options) { + return new Promise((resolve, reject) => { + try { + if (!this.fetching && this.intl) { + resolve(this.intl.formatMessage(descriptor, values, options)); + } else { + Tracker.autorun((c) => { + const { fetching, intl } = this; + + if (fetching || !intl) return; + + resolve(this.intl.formatMessage(descriptor, values, options)); + c.stop(); + }); + } + } catch (e) { + reject(e); + } + }); + } +} + +export default new BBBIntl({ fallback: FALLBACK_ON_EMPTY_STRING }); diff --git a/bigbluebutton-html5/imports/ui/services/mobile-app/index.js b/bigbluebutton-html5/imports/ui/services/mobile-app/index.js index 9f72ddb07f..edfe98d4b4 100644 --- a/bigbluebutton-html5/imports/ui/services/mobile-app/index.js +++ b/bigbluebutton-html5/imports/ui/services/mobile-app/index.js @@ -1,10 +1,13 @@ import browserInfo from '/imports/utils/browserInfo'; import logger from '/imports/startup/client/logger'; +import Auth from '/imports/ui/services/auth'; +import { fetchStunTurnServers } from '/imports/utils/fetchStunTurnServers'; + (function (){ // This function must be executed during the import time, that's why it's not exported to the caller component. // It's needed because it changes some functions provided by browser, and these functions are verified during // import time (like in ScreenshareBridgeService) - if(browserInfo.isMobileApp) { + if(browserInfo.isTabletApp) { logger.debug(`BBB-MOBILE - Mobile APP detected`); const WEBRTC_CALL_TYPE_FULL_AUDIO = 'full_audio'; @@ -30,9 +33,9 @@ import logger from '/imports/startup/client/logger'; const stackTrace = e.stack; logger.info(`BBB-MOBILE - detectWebRtcCallType (evaluating)`, {caller, peerConnection, stackTrace: stackTrace.split('\n'), detectWebRtcCallTypeEvaluations: peerConnection.detectWebRtcCallTypeEvaluations, args}); - // addTransceiver is the first call for screensharing and it has a startScreensharing in its stackTrace + // addEventListener is the first call for screensharing and it has a startScreensharing in its stackTrace if( peerConnection.detectWebRtcCallTypeEvaluations == 1) { - if(caller == 'addTransceiver' && stackTrace.indexOf('startScreensharing') !== -1) { + if(caller == 'addEventListener' && stackTrace.indexOf('startScreensharing') !== -1) { peerConnection.webRtcCallType = WEBRTC_CALL_TYPE_SCREEN_SHARE; // this uses mobile app broadcast upload extension } else if(caller == 'addEventListener' && stackTrace.indexOf('invite') !== -1) { peerConnection.webRtcCallType = WEBRTC_CALL_TYPE_FULL_AUDIO; // this uses mobile app webRTC @@ -131,7 +134,7 @@ import logger from '/imports/startup/client/logger'; const prototype = window.RTCPeerConnection.prototype; prototype.originalCreateOffer = prototype.createOffer; - prototype.createOffer = function (options) { + prototype.createOffer = async function (options) { const webRtcCallType = detectWebRtcCallType('createOffer', this); if(webRtcCallType === WEBRTC_CALL_TYPE_STANDARD){ @@ -139,10 +142,12 @@ import logger from '/imports/startup/client/logger'; } logger.info(`BBB-MOBILE - createOffer called`, {options}); + const stunTurn = await fetchStunTurnServers(Auth._authToken); + const createOfferMethod = (webRtcCallType === WEBRTC_CALL_TYPE_SCREEN_SHARE) ? 'createScreenShareOffer' : 'createFullAudioOffer'; - return new Promise( (resolve, reject) => { - callNativeMethod(createOfferMethod).then ( sdp => { + return await new Promise( (resolve, reject) => { + callNativeMethod(createOfferMethod, [stunTurn]).then ( sdp => { logger.info(`BBB-MOBILE - createOffer resolved`, {sdp}); // send offer to BBB code diff --git a/bigbluebutton-html5/imports/ui/services/settings/index.js b/bigbluebutton-html5/imports/ui/services/settings/index.js index 4c5a190337..dfe55d2957 100644 --- a/bigbluebutton-html5/imports/ui/services/settings/index.js +++ b/bigbluebutton-html5/imports/ui/services/settings/index.js @@ -1,7 +1,11 @@ -import Storage from '/imports/ui/services/storage/session'; +import {default as LocalStorage} from '/imports/ui/services/storage/local'; +import {default as SessionStorage} from '/imports/ui/services/storage/session'; + import _ from 'lodash'; import { makeCall } from '/imports/ui/services/api'; +const APP_CONFIG = Meteor.settings.public.app; + const SETTINGS = [ 'application', 'audio', @@ -55,6 +59,7 @@ class Settings { } loadChanged() { + const Storage = (APP_CONFIG.userSettingsStorage == 'local') ? LocalStorage : SessionStorage; const savedSettings = {}; SETTINGS.forEach((s) => { @@ -72,6 +77,7 @@ class Settings { } save(settings = CHANGED_SETTINGS) { + const Storage = (APP_CONFIG.userSettingsStorage == 'local') ? LocalStorage : SessionStorage; if (settings === CHANGED_SETTINGS) { Object.keys(this).forEach((k) => { const values = this[k].value; diff --git a/bigbluebutton-html5/imports/ui/services/storage/index.js b/bigbluebutton-html5/imports/ui/services/storage/index.js index 9e4d58919d..4b2c703c49 100644 --- a/bigbluebutton-html5/imports/ui/services/storage/index.js +++ b/bigbluebutton-html5/imports/ui/services/storage/index.js @@ -1,7 +1,8 @@ import Local from './local'; import Session from './session'; -export default { - Local, - Session, -}; +const APP_CONFIG = Meteor.settings.public.app; + +const BBBStorage = APP_CONFIG.userSettingsStorage === 'local' ? Local : Session; + +export default BBBStorage; diff --git a/bigbluebutton-html5/imports/utils/browserInfo.js b/bigbluebutton-html5/imports/utils/browserInfo.js index 98c4d69213..b6a65481cc 100755 --- a/bigbluebutton-html5/imports/utils/browserInfo.js +++ b/bigbluebutton-html5/imports/utils/browserInfo.js @@ -16,7 +16,7 @@ const isValidSafariVersion = Bowser.getParser(userAgent).satisfies({ safari: '>12', }); -const isMobileApp = !!(userAgent.match(/BBBMobile/i)); +const isTabletApp = !!(userAgent.match(/BigBlueButton-Tablet/i)); const browserInfo = { isChrome, @@ -27,7 +27,7 @@ const browserInfo = { browserName, versionNumber, isValidSafariVersion, - isMobileApp + isTabletApp }; export default browserInfo; diff --git a/bigbluebutton-html5/imports/utils/string-utils.js b/bigbluebutton-html5/imports/utils/string-utils.js index cc450a5760..8d63e78b1f 100644 --- a/bigbluebutton-html5/imports/utils/string-utils.js +++ b/bigbluebutton-html5/imports/utils/string-utils.js @@ -37,8 +37,15 @@ export const formatLocaleCode = (locale) => { return { language: formattedLocale?.split('-')[0], formattedLocale, - } -} + }; +}; + +export const safeMatch = (regex, content, defaultValue) => { + const regexLimit = 50000; + + if (content.length > regexLimit) return defaultValue; + return content.match(regex) || defaultValue; +}; export default { capitalizeFirstLetter, @@ -47,4 +54,5 @@ export default { escapeHtml, unescapeHtml, formatLocaleCode, + safeMatch, }; diff --git a/bigbluebutton-html5/package-lock.json b/bigbluebutton-html5/package-lock.json index fc22690248..4d41622280 100644 --- a/bigbluebutton-html5/package-lock.json +++ b/bigbluebutton-html5/package-lock.json @@ -423,6 +423,28 @@ } } }, + "@floating-ui/core": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.7.3.tgz", + "integrity": "sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg==" + }, + "@floating-ui/dom": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.5.4.tgz", + "integrity": "sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg==", + "requires": { + "@floating-ui/core": "^0.7.3" + } + }, + "@floating-ui/react-dom": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-0.7.2.tgz", + "integrity": "sha512-1T0sJcpHgX/u4I1OzIEhlcrvkUN8ln39nz7fMoE/2HDHrPiMFoOGR7++GYyfUmIQHkkrTinaeQsO3XWubjSvGg==", + "requires": { + "@floating-ui/dom": "^0.5.3", + "use-isomorphic-layout-effect": "^1.1.1" + } + }, "@formatjs/ecma402-abstract": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.12.0.tgz", @@ -728,155 +750,234 @@ "diff": "^5.0.0" } }, - "@radix-ui/popper": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/popper/-/popper-0.1.0.tgz", - "integrity": "sha512-uzYeElL3w7SeNMuQpXiFlBhTT+JyaNMCwDfjKkrzugEcYrf5n52PHqncNdQPUtR42hJh8V9FsqyEDbDxkeNjJQ==", - "requires": { - "@babel/runtime": "^7.13.10", - "csstype": "^3.0.4" - } - }, "@radix-ui/primitive": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-0.1.0.tgz", - "integrity": "sha512-tqxZKybwN5Fa3VzZry4G6mXAAb9aAqKmPtnVbZpL0vsBwvOHTBwsjHVPXylocYLwEtBY9SCe665bYnNB515uoA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz", + "integrity": "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==", "requires": { "@babel/runtime": "^7.13.10" } }, "@radix-ui/react-alert-dialog": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-0.1.7.tgz", - "integrity": "sha512-b0+TWr0VRWMWM7QcXvvcwbMGNzpTmvPBSBpYcoaD+QnVo3jdJt0k0bghwbYBuywzdyuRNUFf33xwah/57w09QA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.0.2.tgz", + "integrity": "sha512-0MtxV53FaEEBOKRgyLnEqHZKKDS5BldQ9oUBsKVXWI5FHbl2jp35qs+0aJET+K5hJDsc40kQUzP7g+wC7tqrqA==", "requires": { "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "0.1.0", - "@radix-ui/react-compose-refs": "0.1.0", - "@radix-ui/react-context": "0.1.1", - "@radix-ui/react-dialog": "0.1.7", - "@radix-ui/react-primitive": "0.1.4", - "@radix-ui/react-slot": "0.1.2" + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-dialog": "1.0.2", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-slot": "1.0.1" } }, "@radix-ui/react-arrow": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-0.1.4.tgz", - "integrity": "sha512-BB6XzAb7Ml7+wwpFdYVtZpK1BlMgqyafSQNGzhIpSZ4uXvXOHPlR5GP8M449JkeQzgQjv9Mp1AsJxFC0KuOtuA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.0.tgz", + "integrity": "sha512-1MUuv24HCdepi41+qfv125EwMuxgQ+U+h0A9K3BjCO/J8nVRREKHHpkD9clwfnjEDk9hgGzCnff4aUKCPiRepw==", "requires": { "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "0.1.4" + "@radix-ui/react-primitive": "1.0.0" + }, + "dependencies": { + "@radix-ui/react-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.0.tgz", + "integrity": "sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.0" + } + }, + "@radix-ui/react-slot": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.0.tgz", + "integrity": "sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + } + } } }, "@radix-ui/react-collection": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-0.1.4.tgz", - "integrity": "sha512-3muGI15IdgaDFjOcO7xX8a35HQRBRF6LH9pS6UCeZeRmbslkVeHyJRQr2rzICBUoX7zgIA0kXyMDbpQnJGyJTA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.0.tgz", + "integrity": "sha512-8i1pf5dKjnq90Z8udnnXKzdCEV3/FYrfw0n/b6NvB6piXEn3fO1bOh7HBcpG8XrnIXzxlYu2oCcR38QpyLS/mg==", "requires": { "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "0.1.0", - "@radix-ui/react-context": "0.1.1", - "@radix-ui/react-primitive": "0.1.4", - "@radix-ui/react-slot": "0.1.2" + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.0", + "@radix-ui/react-slot": "1.0.0" + }, + "dependencies": { + "@radix-ui/react-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.0.tgz", + "integrity": "sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.0" + } + }, + "@radix-ui/react-slot": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.0.tgz", + "integrity": "sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + } + } } }, "@radix-ui/react-compose-refs": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-0.1.0.tgz", - "integrity": "sha512-eyclbh+b77k+69Dk72q3694OHrn9B3QsoIRx7ywX341U9RK1ThgQjMFZoPtmZNQTksXHLNEiefR8hGVeFyInGg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz", + "integrity": "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==", "requires": { "@babel/runtime": "^7.13.10" } }, "@radix-ui/react-context": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-0.1.1.tgz", - "integrity": "sha512-PkyVX1JsLBioeu0jB9WvRpDBBLtLZohVDT3BB5CTSJqActma8S8030P57mWZb4baZifMvN7KKWPAA40UmWKkQg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.0.tgz", + "integrity": "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==", "requires": { "@babel/runtime": "^7.13.10" } }, "@radix-ui/react-context-menu": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-0.1.6.tgz", - "integrity": "sha512-0qa6ABaeqD+WYI+8iT0jH0QLLcV8Kv0xI+mZL4FFnG4ec9H0v+yngb5cfBBfs9e/KM8mDzFFpaeegqsQlLNqyQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-1.0.0.tgz", + "integrity": "sha512-JkwOgdXwErwEEpsmgu0Ob8zD3gzWS1brPXnNGPyZEtR6/EYyDgruQYKiihXVsCrPCdrNUHawop9I1+6JTdXPTA==", "requires": { "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "0.1.0", - "@radix-ui/react-context": "0.1.1", - "@radix-ui/react-menu": "0.1.6", - "@radix-ui/react-primitive": "0.1.4", - "@radix-ui/react-use-callback-ref": "0.1.0" + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-menu": "1.0.0", + "@radix-ui/react-primitive": "1.0.0", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-controllable-state": "1.0.0" + }, + "dependencies": { + "@radix-ui/react-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.0.tgz", + "integrity": "sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.0" + } + }, + "@radix-ui/react-slot": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.0.tgz", + "integrity": "sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + } + } } }, "@radix-ui/react-dialog": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-0.1.7.tgz", - "integrity": "sha512-jXt8srGhHBRvEr9jhEAiwwJzWCWZoGRJ030aC9ja/gkRJbZdy0iD3FwXf+Ff4RtsZyLUMHW7VUwFOlz3Ixe1Vw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.2.tgz", + "integrity": "sha512-EKxxp2WNSmUPkx4trtWNmZ4/vAYEg7JkAfa1HKBUnaubw9eHzf1Orr9B472lJYaYz327RHDrd4R95fsw7VR8DA==", "requires": { "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "0.1.0", - "@radix-ui/react-compose-refs": "0.1.0", - "@radix-ui/react-context": "0.1.1", - "@radix-ui/react-dismissable-layer": "0.1.5", - "@radix-ui/react-focus-guards": "0.1.0", - "@radix-ui/react-focus-scope": "0.1.4", - "@radix-ui/react-id": "0.1.5", - "@radix-ui/react-portal": "0.1.4", - "@radix-ui/react-presence": "0.1.2", - "@radix-ui/react-primitive": "0.1.4", - "@radix-ui/react-slot": "0.1.2", - "@radix-ui/react-use-controllable-state": "0.1.0", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-dismissable-layer": "1.0.2", + "@radix-ui/react-focus-guards": "1.0.0", + "@radix-ui/react-focus-scope": "1.0.1", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-portal": "1.0.1", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-slot": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.0", "aria-hidden": "^1.1.1", - "react-remove-scroll": "^2.4.0" + "react-remove-scroll": "2.5.5" + } + }, + "@radix-ui/react-direction": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.0.tgz", + "integrity": "sha512-2HV05lGUgYcA6xgLQ4BKPDmtL+QbIZYH5fCOTAOOcJ5O0QbWS3i9lKaurLzliYUDhORI2Qr3pyjhJh44lKA3rQ==", + "requires": { + "@babel/runtime": "^7.13.10" } }, "@radix-ui/react-dismissable-layer": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-0.1.5.tgz", - "integrity": "sha512-J+fYWijkX4M4QKwf9dtu1oC0U6e6CEl8WhBp3Ad23yz2Hia0XCo6Pk/mp5CAFy4QBtQedTSkhW05AdtSOEoajQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.2.tgz", + "integrity": "sha512-WjJzMrTWROozDqLB0uRWYvj4UuXsM/2L19EmQ3Au+IJWqwvwq9Bwd+P8ivo0Deg9JDPArR1I6MbWNi1CmXsskg==", "requires": { "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "0.1.0", - "@radix-ui/react-compose-refs": "0.1.0", - "@radix-ui/react-primitive": "0.1.4", - "@radix-ui/react-use-body-pointer-events": "0.1.1", - "@radix-ui/react-use-callback-ref": "0.1.0", - "@radix-ui/react-use-escape-keydown": "0.1.0" + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-escape-keydown": "1.0.2" } }, "@radix-ui/react-dropdown-menu": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-0.1.6.tgz", - "integrity": "sha512-RZhtzjWwJ4ZBN7D8ek4Zn+ilHzYuYta9yIxFnbC0pfqMnSi67IQNONo1tuuNqtFh9SRHacPKc65zo+kBBlxtdg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-1.0.0.tgz", + "integrity": "sha512-Ptben3TxPWrZLbInO7zjAK73kmjYuStsxfg6ujgt+EywJyREoibhZYnsSNqC+UiOtl4PdW/MOHhxVDtew5fouQ==", "requires": { "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "0.1.0", - "@radix-ui/react-compose-refs": "0.1.0", - "@radix-ui/react-context": "0.1.1", - "@radix-ui/react-id": "0.1.5", - "@radix-ui/react-menu": "0.1.6", - "@radix-ui/react-primitive": "0.1.4", - "@radix-ui/react-use-controllable-state": "0.1.0" + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-menu": "1.0.0", + "@radix-ui/react-primitive": "1.0.0", + "@radix-ui/react-use-controllable-state": "1.0.0" + }, + "dependencies": { + "@radix-ui/react-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.0.tgz", + "integrity": "sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.0" + } + }, + "@radix-ui/react-slot": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.0.tgz", + "integrity": "sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + } + } } }, "@radix-ui/react-focus-guards": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-0.1.0.tgz", - "integrity": "sha512-kRx/swAjEfBpQ3ns7J3H4uxpXuWCqN7MpALiSDOXiyo2vkWv0L9sxvbpZeTulINuE3CGMzicVMuNc/VWXjFKOg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.0.tgz", + "integrity": "sha512-UagjDk4ijOAnGu4WMUPj9ahi7/zJJqNZ9ZAiGPp7waUWJO0O1aWXi/udPphI0IUjvrhBsZJGSN66dR2dsueLWQ==", "requires": { "@babel/runtime": "^7.13.10" } }, "@radix-ui/react-focus-scope": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-0.1.4.tgz", - "integrity": "sha512-fbA4ES3H4Wkxp+OeLhvN6SwL7mXNn/aBtUf7DRYxY9+Akrf7dRxl2ck4lgcpPsSg3zSDsEwLcY+h5cmj5yvlug==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.1.tgz", + "integrity": "sha512-Ej2MQTit8IWJiS2uuujGUmxXjF/y5xZptIIQnyd2JHLwtV0R2j9NRVoRj/1j/gJ7e3REdaBw4Hjf4a1ImhkZcQ==", "requires": { "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "0.1.0", - "@radix-ui/react-primitive": "0.1.4", - "@radix-ui/react-use-callback-ref": "0.1.0" + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-callback-ref": "1.0.0" } }, "@radix-ui/react-icons": { @@ -885,242 +986,389 @@ "integrity": "sha512-xc3wQC59rsFylVbSusQCrrM+6695ppF730Q6yqzhRdqDcRNWIm2R6ngpzBoSOQMcwnq4p805F+Gr7xo4fmtN1A==" }, "@radix-ui/react-id": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-0.1.5.tgz", - "integrity": "sha512-IPc4H/63bes0IZ1GJJozSEkSWcDyhNGtKFWUpJ+XtaLyQ1X3x7Mf6fWwWhDcpqlYEP+5WtAvfqcyEsyjP+ZhBQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.0.tgz", + "integrity": "sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==", "requires": { "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "0.1.0" + "@radix-ui/react-use-layout-effect": "1.0.0" } }, "@radix-ui/react-menu": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-0.1.6.tgz", - "integrity": "sha512-ho3+bhpr3oAFkOBJ8VkUb1BcGoiZBB3OmcWPqa6i5RTUKrzNX/d6rauochu2xDlWjiRtpVuiAcsTVOeIC4FbYQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-1.0.0.tgz", + "integrity": "sha512-icW4C64T6nHh3Z4Q1fxO1RlSShouFF4UpUmPV8FLaJZfphDljannKErDuALDx4ClRLihAPZ9i+PrLNPoWS2DMA==", "requires": { "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "0.1.0", - "@radix-ui/react-collection": "0.1.4", - "@radix-ui/react-compose-refs": "0.1.0", - "@radix-ui/react-context": "0.1.1", - "@radix-ui/react-dismissable-layer": "0.1.5", - "@radix-ui/react-focus-guards": "0.1.0", - "@radix-ui/react-focus-scope": "0.1.4", - "@radix-ui/react-id": "0.1.5", - "@radix-ui/react-popper": "0.1.4", - "@radix-ui/react-portal": "0.1.4", - "@radix-ui/react-presence": "0.1.2", - "@radix-ui/react-primitive": "0.1.4", - "@radix-ui/react-roving-focus": "0.1.5", - "@radix-ui/react-use-callback-ref": "0.1.0", - "@radix-ui/react-use-direction": "0.1.0", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-collection": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-direction": "1.0.0", + "@radix-ui/react-dismissable-layer": "1.0.0", + "@radix-ui/react-focus-guards": "1.0.0", + "@radix-ui/react-focus-scope": "1.0.0", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-popper": "1.0.0", + "@radix-ui/react-portal": "1.0.0", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.0", + "@radix-ui/react-roving-focus": "1.0.0", + "@radix-ui/react-slot": "1.0.0", + "@radix-ui/react-use-callback-ref": "1.0.0", "aria-hidden": "^1.1.1", - "react-remove-scroll": "^2.4.0" + "react-remove-scroll": "2.5.4" + }, + "dependencies": { + "@radix-ui/react-dismissable-layer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.0.tgz", + "integrity": "sha512-n7kDRfx+LB1zLueRDvZ1Pd0bxdJWDUZNQ/GWoxDn2prnuJKRdxsjulejX/ePkOsLi2tTm6P24mDqlMSgQpsT6g==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-primitive": "1.0.0", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-escape-keydown": "1.0.0" + } + }, + "@radix-ui/react-focus-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.0.tgz", + "integrity": "sha512-C4SWtsULLGf/2L4oGeIHlvWQx7Rf+7cX/vKOAD2dXW0A1b5QXwi3wWeaEgW+wn+SEVrraMUk05vLU9fZZz5HbQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-primitive": "1.0.0", + "@radix-ui/react-use-callback-ref": "1.0.0" + } + }, + "@radix-ui/react-portal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.0.tgz", + "integrity": "sha512-a8qyFO/Xb99d8wQdu4o7qnigNjTPG123uADNecz0eX4usnQEj7o+cG4ZX4zkqq98NYekT7UoEQIjxBNWIFuqTA==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.0" + } + }, + "@radix-ui/react-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.0.tgz", + "integrity": "sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.0" + } + }, + "@radix-ui/react-slot": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.0.tgz", + "integrity": "sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + } + }, + "@radix-ui/react-use-escape-keydown": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.0.tgz", + "integrity": "sha512-JwfBCUIfhXRxKExgIqGa4CQsiMemo1Xt0W/B4ei3fpzpvPENKpMKQ8mZSB6Acj3ebrAEgi2xiQvcI1PAAodvyg==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.0" + } + }, + "react-remove-scroll": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.4.tgz", + "integrity": "sha512-xGVKJJr0SJGQVirVFAUZ2k1QLyO6m+2fy0l8Qawbp5Jgrv3DeLalrfMNBFSlmz5kriGGzsVBtGVnf4pTKIhhWA==", + "requires": { + "react-remove-scroll-bar": "^2.3.3", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + } + } } }, "@radix-ui/react-popover": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-0.1.6.tgz", - "integrity": "sha512-zQzgUqW4RQDb0ItAL1xNW4K4olUrkfV3jeEPs9rG+nsDQurO+W9TT+YZ9H1mmgAJqlthyv1sBRZGdBm4YjtD6Q==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.2.tgz", + "integrity": "sha512-4tqZEl9w95R5mlZ/sFdgBnfhCBOEPepLIurBA5kt/qaAhldJ1tNQd0ngr0ET0AHbPotT4mwxMPr7a+MA/wbK0g==", "requires": { "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "0.1.0", - "@radix-ui/react-compose-refs": "0.1.0", - "@radix-ui/react-context": "0.1.1", - "@radix-ui/react-dismissable-layer": "0.1.5", - "@radix-ui/react-focus-guards": "0.1.0", - "@radix-ui/react-focus-scope": "0.1.4", - "@radix-ui/react-id": "0.1.5", - "@radix-ui/react-popper": "0.1.4", - "@radix-ui/react-portal": "0.1.4", - "@radix-ui/react-presence": "0.1.2", - "@radix-ui/react-primitive": "0.1.4", - "@radix-ui/react-use-controllable-state": "0.1.0", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-dismissable-layer": "1.0.2", + "@radix-ui/react-focus-guards": "1.0.0", + "@radix-ui/react-focus-scope": "1.0.1", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-popper": "1.0.1", + "@radix-ui/react-portal": "1.0.1", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-slot": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.0", "aria-hidden": "^1.1.1", - "react-remove-scroll": "^2.4.0" + "react-remove-scroll": "2.5.5" + }, + "dependencies": { + "@radix-ui/react-arrow": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz", + "integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.1" + } + }, + "@radix-ui/react-popper": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.0.1.tgz", + "integrity": "sha512-J4Vj7k3k+EHNWgcKrE+BLlQfpewxA7Zd76h5I0bIa+/EqaIZ3DuwrbPj49O3wqN+STnXsBuxiHLiF0iU3yfovw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "0.7.2", + "@radix-ui/react-arrow": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-use-rect": "1.0.0", + "@radix-ui/react-use-size": "1.0.0", + "@radix-ui/rect": "1.0.0" + } + } } }, "@radix-ui/react-popper": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-0.1.4.tgz", - "integrity": "sha512-18gDYof97t8UQa7zwklG1Dr8jIdj3u+rVOQLzPi9f5i1YQak/pVGkaqw8aY+iDUknKKuZniTk/7jbAJUYlKyOw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.0.0.tgz", + "integrity": "sha512-k2dDd+1Wl0XWAMs9ZvAxxYsB9sOsEhrFQV4CINd7IUZf0wfdye4OHen9siwxvZImbzhgVeKTJi68OQmPRvVdMg==", "requires": { "@babel/runtime": "^7.13.10", - "@radix-ui/popper": "0.1.0", - "@radix-ui/react-arrow": "0.1.4", - "@radix-ui/react-compose-refs": "0.1.0", - "@radix-ui/react-context": "0.1.1", - "@radix-ui/react-primitive": "0.1.4", - "@radix-ui/react-use-rect": "0.1.1", - "@radix-ui/react-use-size": "0.1.1", - "@radix-ui/rect": "0.1.1" + "@floating-ui/react-dom": "0.7.2", + "@radix-ui/react-arrow": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-use-rect": "1.0.0", + "@radix-ui/react-use-size": "1.0.0", + "@radix-ui/rect": "1.0.0" + }, + "dependencies": { + "@radix-ui/react-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.0.tgz", + "integrity": "sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.0" + } + }, + "@radix-ui/react-slot": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.0.tgz", + "integrity": "sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + } + } } }, "@radix-ui/react-portal": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-0.1.4.tgz", - "integrity": "sha512-MO0wRy2eYRTZ/CyOri9NANCAtAtq89DEtg90gicaTlkCfdqCLEBsLb+/q66BZQTr3xX/Vq01nnVfc/TkCqoqvw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.1.tgz", + "integrity": "sha512-NY2vUWI5WENgAT1nfC6JS7RU5xRYBfjZVLq0HmgEN1Ezy3rk/UruMV4+Rd0F40PEaFC5SrLS1ixYvcYIQrb4Ig==", "requires": { "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "0.1.4", - "@radix-ui/react-use-layout-effect": "0.1.0" + "@radix-ui/react-primitive": "1.0.1" } }, "@radix-ui/react-presence": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-0.1.2.tgz", - "integrity": "sha512-3BRlFZraooIUfRlyN+b/Xs5hq1lanOOo/+3h6Pwu2GMFjkGKKa4Rd51fcqGqnVlbr3jYg+WLuGyAV4KlgqwrQw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.0.tgz", + "integrity": "sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==", "requires": { "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "0.1.0", - "@radix-ui/react-use-layout-effect": "0.1.0" + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0" } }, "@radix-ui/react-primitive": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-0.1.4.tgz", - "integrity": "sha512-6gSl2IidySupIMJFjYnDIkIWRyQdbu/AHK7rbICPani+LW4b0XdxBXc46og/iZvuwW8pjCS8I2SadIerv84xYA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.1.tgz", + "integrity": "sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA==", "requires": { "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "0.1.2" + "@radix-ui/react-slot": "1.0.1" } }, "@radix-ui/react-roving-focus": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-0.1.5.tgz", - "integrity": "sha512-ClwKPS5JZE+PaHCoW7eu1onvE61pDv4kO8W4t5Ra3qMFQiTJLZMdpBQUhksN//DaVygoLirz4Samdr5Y1x1FSA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.0.tgz", + "integrity": "sha512-lHvO4MhvoWpeNbiJAoyDsEtbKqP2jkkdwsMVJ3kfqbkC71J/aXE6Th6gkZA1xHEqSku+t+UgoDjvE7Z3gsBpcg==", "requires": { "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "0.1.0", - "@radix-ui/react-collection": "0.1.4", - "@radix-ui/react-compose-refs": "0.1.0", - "@radix-ui/react-context": "0.1.1", - "@radix-ui/react-id": "0.1.5", - "@radix-ui/react-primitive": "0.1.4", - "@radix-ui/react-use-callback-ref": "0.1.0", - "@radix-ui/react-use-controllable-state": "0.1.0" + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-collection": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-direction": "1.0.0", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-primitive": "1.0.0", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-controllable-state": "1.0.0" + }, + "dependencies": { + "@radix-ui/react-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.0.tgz", + "integrity": "sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.0" + } + }, + "@radix-ui/react-slot": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.0.tgz", + "integrity": "sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + } + } } }, "@radix-ui/react-slot": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-0.1.2.tgz", - "integrity": "sha512-ADkqfL+agEzEguU3yS26jfB50hRrwf7U4VTwAOZEmi/g+ITcBWe12yM46ueS/UCIMI9Py+gFUaAdxgxafFvY2Q==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz", + "integrity": "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==", "requires": { "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "0.1.0" + "@radix-ui/react-compose-refs": "1.0.0" } }, "@radix-ui/react-tooltip": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-0.1.7.tgz", - "integrity": "sha512-eiBUsVOHenZ0JR16tl970bB0DafJBz6mFgSGfIGIVpflFj0LIsIDiLMsYyvYdx1KwwsIUDTEZtxcPm/sWjPzqA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.2.tgz", + "integrity": "sha512-11gUlok2rv5mu+KBtxniOKKNKjqC/uTbgFHWoQdbF46vMV+zjDaBvCtVDK9+MTddlpmlisGPGvvojX7Qm0yr+g==", "requires": { "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "0.1.0", - "@radix-ui/react-compose-refs": "0.1.0", - "@radix-ui/react-context": "0.1.1", - "@radix-ui/react-id": "0.1.5", - "@radix-ui/react-popper": "0.1.4", - "@radix-ui/react-portal": "0.1.4", - "@radix-ui/react-presence": "0.1.2", - "@radix-ui/react-primitive": "0.1.4", - "@radix-ui/react-slot": "0.1.2", - "@radix-ui/react-use-controllable-state": "0.1.0", - "@radix-ui/react-use-escape-keydown": "0.1.0", - "@radix-ui/react-use-previous": "0.1.1", - "@radix-ui/react-use-rect": "0.1.1", - "@radix-ui/react-visually-hidden": "0.1.4" - } - }, - "@radix-ui/react-use-body-pointer-events": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-body-pointer-events/-/react-use-body-pointer-events-0.1.1.tgz", - "integrity": "sha512-R8leV2AWmJokTmERM8cMXFHWSiv/fzOLhG/JLmRBhLTAzOj37EQizssq4oW0Z29VcZy2tODMi9Pk/htxwb+xpA==", - "requires": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "0.1.0" + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-dismissable-layer": "1.0.2", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-popper": "1.0.1", + "@radix-ui/react-portal": "1.0.1", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-slot": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.0", + "@radix-ui/react-visually-hidden": "1.0.1" + }, + "dependencies": { + "@radix-ui/react-arrow": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz", + "integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.1" + } + }, + "@radix-ui/react-popper": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.0.1.tgz", + "integrity": "sha512-J4Vj7k3k+EHNWgcKrE+BLlQfpewxA7Zd76h5I0bIa+/EqaIZ3DuwrbPj49O3wqN+STnXsBuxiHLiF0iU3yfovw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "0.7.2", + "@radix-ui/react-arrow": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-use-rect": "1.0.0", + "@radix-ui/react-use-size": "1.0.0", + "@radix-ui/rect": "1.0.0" + } + } } }, "@radix-ui/react-use-callback-ref": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-0.1.0.tgz", - "integrity": "sha512-Va041McOFFl+aV+sejvl0BS2aeHx86ND9X/rVFmEFQKTXCp6xgUK0NGUAGcgBlIjnJSbMYPGEk1xKSSlVcN2Aw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", + "integrity": "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==", "requires": { "@babel/runtime": "^7.13.10" } }, "@radix-ui/react-use-controllable-state": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-0.1.0.tgz", - "integrity": "sha512-zv7CX/PgsRl46a52Tl45TwqwVJdmqnlQEQhaYMz/yBOD2sx2gCkCFSoF/z9mpnYWmS6DTLNTg5lIps3fV6EnXg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.0.tgz", + "integrity": "sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==", "requires": { "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "0.1.0" - } - }, - "@radix-ui/react-use-direction": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-direction/-/react-use-direction-0.1.0.tgz", - "integrity": "sha512-NajpY/An9TCPSfOVkgWIdXJV+VuWl67PxB6kOKYmtNAFHvObzIoh8o0n9sAuwSAyFCZVq211FEf9gvVDRhOyiA==", - "requires": { - "@babel/runtime": "^7.13.10" + "@radix-ui/react-use-callback-ref": "1.0.0" } }, "@radix-ui/react-use-escape-keydown": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-0.1.0.tgz", - "integrity": "sha512-tDLZbTGFmvXaazUXXv8kYbiCcbAE8yKgng9s95d8fCO+Eundv0Jngbn/hKPhDDs4jj9ChwRX5cDDnlaN+ugYYQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.2.tgz", + "integrity": "sha512-DXGim3x74WgUv+iMNCF+cAo8xUHHeqvjx8zs7trKf+FkQKPQXLk2sX7Gx1ysH7Q76xCpZuxIJE7HLPxRE+Q+GA==", "requires": { "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "0.1.0" + "@radix-ui/react-use-callback-ref": "1.0.0" } }, "@radix-ui/react-use-layout-effect": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-0.1.0.tgz", - "integrity": "sha512-+wdeS51Y+E1q1Wmd+1xSSbesZkpVj4jsg0BojCbopWvgq5iBvixw5vgemscdh58ep98BwUbsFYnrywFhV9yrVg==", - "requires": { - "@babel/runtime": "^7.13.10" - } - }, - "@radix-ui/react-use-previous": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-0.1.1.tgz", - "integrity": "sha512-O/ZgrDBr11dR8rhO59ED8s5zIXBRFi8MiS+CmFGfi7MJYdLbfqVOmQU90Ghf87aifEgWe6380LA69KBneaShAg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz", + "integrity": "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==", "requires": { "@babel/runtime": "^7.13.10" } }, "@radix-ui/react-use-rect": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-0.1.1.tgz", - "integrity": "sha512-kHNNXAsP3/PeszEmM/nxBBS9Jbo93sO+xuMTcRfwzXsmxT5gDXQzAiKbZQ0EecCPtJIzqvr7dlaQi/aP1PKYqQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.0.tgz", + "integrity": "sha512-TB7pID8NRMEHxb/qQJpvSt3hQU4sqNPM1VCTjTRjEOa7cEop/QMuq8S6fb/5Tsz64kqSvB9WnwsDHtjnrM9qew==", "requires": { "@babel/runtime": "^7.13.10", - "@radix-ui/rect": "0.1.1" + "@radix-ui/rect": "1.0.0" } }, "@radix-ui/react-use-size": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-0.1.1.tgz", - "integrity": "sha512-pTgWM5qKBu6C7kfKxrKPoBI2zZYZmp2cSXzpUiGM3qEBQlMLtYhaY2JXdXUCxz+XmD1YEjc8oRwvyfsD4AG4WA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.0.tgz", + "integrity": "sha512-imZ3aYcoYCKhhgNpkNDh/aTiU05qw9hX+HHI1QDBTyIlcFjgeFlKKySNGMwTp7nYFLQg/j0VA2FmCY4WPDDHMg==", "requires": { - "@babel/runtime": "^7.13.10" + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.0" } }, "@radix-ui/react-visually-hidden": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-0.1.4.tgz", - "integrity": "sha512-K/q6AEEzqeeEq/T0NPChvBqnwlp8Tl4NnQdrI/y8IOY7BRR+Ug0PEsVk6g48HJ7cA1//COugdxXXVVK/m0X1mA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.1.tgz", + "integrity": "sha512-K1hJcCMfWfiYUibRqf3V8r5Drpyf7rh44jnrwAbdvI5iCCijilBBeyQv9SKidYNZIopMdCyR9FnIjkHxHN0FcQ==", "requires": { "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "0.1.4" + "@radix-ui/react-primitive": "1.0.1" } }, "@radix-ui/rect": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-0.1.1.tgz", - "integrity": "sha512-g3hnE/UcOg7REdewduRPAK88EPuLZtaq7sA9ouu8S+YEtnyFRI16jgv6GZYe3VMoQLL1T171ebmEPtDjyxWLzw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.0.tgz", + "integrity": "sha512-d0O68AYy/9oeEy1DdC07bz1/ZXX+DqCskRd3i4JzLSTXwefzaepQrKjXC7aNM8lTHjFLDO0pDgaEiQ7jEk+HVg==", "requires": { "@babel/runtime": "^7.13.10" } @@ -1131,169 +1379,207 @@ "integrity": "sha512-9g9dWI4gsSVe8bNLlb+lMkBYsnIKCZTmvqvDG+Avnn69XfmHZKiaMrx7cgTaddq7aTPPmXiTsbFcUy0xgI4+wA==" }, "@tldraw/core": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@tldraw/core/-/core-1.15.0.tgz", - "integrity": "sha512-4A9Xyh/VQBCtzVxZF/a7pXkP6AbFqVE8LcD9gmKWnaSYW6haLhc/Ie1O477T5xKV2HMfWJ52x1dUEn3jOGFVCg==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@tldraw/core/-/core-1.20.0.tgz", + "integrity": "sha512-zOQMS0bVj9x1RCCu7GzjjKzu36P577/BCuA9HF4Q9GNyZocvdoqGe0hv82AbDQIO8ktDfFSKPUGdGtlno0n+Lw==", "requires": { - "@tldraw/intersect": "^1.7.1", - "@tldraw/vec": "^1.7.1", - "@use-gesture/react": "^10.2.14", - "mobx-react-lite": "^3.2.3", - "perfect-freehand": "^1.1.0", - "resize-observer-polyfill": "^1.5.1" + "@tldraw/intersect": "^1.8.0", + "@tldraw/vec": "^1.8.0", + "@use-gesture/react": "^10.2.19", + "perfect-freehand": "^1.1.0" } }, "@tldraw/intersect": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@tldraw/intersect/-/intersect-1.7.1.tgz", - "integrity": "sha512-azJvJv4selqiQemipQE9Z1uax0j5XzIoHXQuRL4nm4EFiuS3n6jTTyWr9aQN3f7GgQbhcseq/ynytxfo9df02w==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@tldraw/intersect/-/intersect-1.8.0.tgz", + "integrity": "sha512-0UarshNpyq2+O4o0xHMJIBgF0E630mes5CkMoO+D5xgYppSBIkeqYDcv0ujsmAhMKX1O6Y0ShuuHeflBEULUoQ==", "requires": { - "@tldraw/vec": "^1.7.0" + "@tldraw/vec": "^1.8.0" } }, "@tldraw/tldraw": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/@tldraw/tldraw/-/tldraw-1.20.0.tgz", - "integrity": "sha512-dVy/l7ceTRwEBzpFvmT2vqDCmbiAJtF55Jv6At9Y9oNDF+xT81dgXupHVw0wSiQRW3ZwD2hxPjnFWqi6uHBS+w==", + "version": "1.26.1", + "resolved": "https://registry.npmjs.org/@tldraw/tldraw/-/tldraw-1.26.1.tgz", + "integrity": "sha512-SHskYUuZxvL0ugYeBnQ8o7UAs1yhfx/IQsmnolryPqmcFDoZ1y5K5lnkSAclDliv2HTbl9jZ/p85Zul9T4i+CQ==", "requires": { - "@radix-ui/react-alert-dialog": "^0.1.7", - "@radix-ui/react-context-menu": "^0.1.6", - "@radix-ui/react-dialog": "^0.1.7", - "@radix-ui/react-dropdown-menu": "^0.1.6", + "@radix-ui/react-alert-dialog": "^1.0.0", + "@radix-ui/react-context-menu": "^1.0.0", + "@radix-ui/react-dialog": "^1.0.0", + "@radix-ui/react-dropdown-menu": "^1.0.0", "@radix-ui/react-icons": "^1.1.1", - "@radix-ui/react-popover": "^0.1.6", - "@radix-ui/react-tooltip": "^0.1.7", + "@radix-ui/react-popover": "^1.0.0", + "@radix-ui/react-tooltip": "^1.0.0", "@stitches/react": "^1.2.8", - "@tldraw/core": "^1.15.0", - "@tldraw/intersect": "^1.7.1", - "@tldraw/vec": "^1.7.1", - "idb-keyval": "^6.1.0", - "lz-string": "^1.4.4", - "perfect-freehand": "^1.1.0", + "@tldraw/core": "^1.20.0", + "@tldraw/intersect": "^1.8.0", + "@tldraw/vec": "^1.8.0", + "browser-fs-access": "^0.31.0", + "idb-keyval": "^6.2.0", + "perfect-freehand": "^1.2.0", "react-error-boundary": "^3.1.4", - "react-hotkeys-hook": "^3.4.4", - "react-intl": "^6.0.3", + "react-hotkeys-hook": "^3.4.7", + "react-intl": "^6.1.1", "tslib": "^2.4.0", - "zustand": "^3.6.9" + "zustand": "^4.1.1" }, "dependencies": { "@formatjs/ecma402-abstract": { - "version": "1.11.8", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.8.tgz", - "integrity": "sha512-fgLqyWlwmTEuqV/TSLEL/t9JOmHNLFvCdgzXB0jc2w+WOItPCOJ1T0eyN6fQBQKRPfSqqNlu+kWj7ijcOVTVVQ==", - "requires": { - "@formatjs/intl-localematcher": "0.2.28", - "tslib": "2.4.0" - } - }, - "@formatjs/fast-memoize": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-1.2.4.tgz", - "integrity": "sha512-9ARYoLR8AEzXvj2nYrOVHY/h1dDMDWGTnKDLXSISF1uoPakSmfcZuSqjiqZX2wRkEUimPxdwTu/agyozBtZRHA==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.13.0.tgz", + "integrity": "sha512-CQ8Ykd51jYD1n05dtoX6ns6B9n/+6ZAxnWUAonvHC4kkuAemROYBhHkEB4tm1uVrRlE7gLDqXkAnY51Y0pRCWQ==", "requires": { + "@formatjs/intl-localematcher": "0.2.31", "tslib": "2.4.0" + }, + "dependencies": { + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + } } }, "@formatjs/icu-messageformat-parser": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.4.tgz", - "integrity": "sha512-3PqMvKWV1oyok0BuiXUAHIaotdhdTJw6OICqCZbfUgKT+ZRwRWO4IlCgvXJeCITaKS5p+PY0XXKjf/vUyIpWjQ==", + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.10.tgz", + "integrity": "sha512-KkRMxhifWkRC45dhM9tqm0GXbb6NPYTGVYY3xx891IKc6p++DQrZTnmkVSNNO47OEERLfuP2KkPFPJBuu8z/wg==", "requires": { - "@formatjs/ecma402-abstract": "1.11.8", - "@formatjs/icu-skeleton-parser": "1.3.10", + "@formatjs/ecma402-abstract": "1.13.0", + "@formatjs/icu-skeleton-parser": "1.3.14", "tslib": "2.4.0" + }, + "dependencies": { + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + } } }, "@formatjs/icu-skeleton-parser": { - "version": "1.3.10", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.10.tgz", - "integrity": "sha512-kXJmtLDqFF5aLTf8IxdJXnhrIX1Qb4Qp3a9jqRecGDYfzOa9hMhi9U0nKyhrJJ4cXxBzptcgb+LWkyeHL6nlBQ==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.14.tgz", + "integrity": "sha512-7bv60HQQcBb3+TSj+45tOb/CHV5z1hOpwdtS50jsSBXfB+YpGhnoRsZxSRksXeCxMy6xn6tA6VY2601BrrK+OA==", "requires": { - "@formatjs/ecma402-abstract": "1.11.8", + "@formatjs/ecma402-abstract": "1.13.0", "tslib": "2.4.0" + }, + "dependencies": { + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + } } }, "@formatjs/intl": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-2.3.1.tgz", - "integrity": "sha512-f06qZ/ukpeN24gc01qFjh3P+r3FU/ikY4yG+fDJu6dPNvpUQzDy98lYogA1dr6ig2UtrnoEk3xncyFPL1e9cZw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-2.5.1.tgz", + "integrity": "sha512-P01ZGuDDlcN8bHHBCEHspJPvs8WJeO8SXlUIcVGWhS3IN5vUgz0QKUXcKBFnJbEHhONJ+azlObVwvlDKsE+kUg==", "requires": { - "@formatjs/ecma402-abstract": "1.11.8", - "@formatjs/fast-memoize": "1.2.4", - "@formatjs/icu-messageformat-parser": "2.1.4", - "@formatjs/intl-displaynames": "6.0.3", - "@formatjs/intl-listformat": "7.0.3", - "intl-messageformat": "10.1.1", + "@formatjs/ecma402-abstract": "1.13.0", + "@formatjs/fast-memoize": "1.2.6", + "@formatjs/icu-messageformat-parser": "2.1.10", + "@formatjs/intl-displaynames": "6.1.4", + "@formatjs/intl-listformat": "7.1.3", + "intl-messageformat": "10.2.1", "tslib": "2.4.0" + }, + "dependencies": { + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + } } }, "@formatjs/intl-displaynames": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-6.0.3.tgz", - "integrity": "sha512-Mxh6W1VOlmiEvO/QPBrBQHlXrIn5VxjJWyyEI0V7ZHNGl0ee8AjSlq7vIJG8GodRJqGUuutF6N3OB/6qFv0YWg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-6.1.4.tgz", + "integrity": "sha512-sEbziGLsWQo6nA8ZUBcsDRlZzPg+uMVjDmbTalgGqRWLbdXuxMldTYdaCK+UptyJhkmNVM/erz3csTiyqamXHQ==", "requires": { - "@formatjs/ecma402-abstract": "1.11.8", - "@formatjs/intl-localematcher": "0.2.28", + "@formatjs/ecma402-abstract": "1.13.0", + "@formatjs/intl-localematcher": "0.2.31", "tslib": "2.4.0" + }, + "dependencies": { + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + } } }, "@formatjs/intl-listformat": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.0.3.tgz", - "integrity": "sha512-ampNLRGZl/08epHa3i5sRmcHGLneC6JrknexbbgnexYFNSmJ6AbL/dCzgrQzw2Efl+5AZK7UbNFxcDYY3RePvw==", - "requires": { - "@formatjs/ecma402-abstract": "1.11.8", - "@formatjs/intl-localematcher": "0.2.28", - "tslib": "2.4.0" - } - }, - "@formatjs/intl-localematcher": { - "version": "0.2.28", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.28.tgz", - "integrity": "sha512-FLsc6Gifs1np/8HnCn/7Q+lHMmenrD5fuDhRT82yj0gi9O19kfaFwjQUw1gZsyILuRyT93GuzdifHj7TKRhBcw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.1.3.tgz", + "integrity": "sha512-rs0Kxl78PeRCedx2cmFoBqcun2Kf0bCQrF8ycna54sfePpDhMskvODWeI4G/xBioW01FjK7CJSvtJJ87hrr79A==", "requires": { + "@formatjs/ecma402-abstract": "1.13.0", + "@formatjs/intl-localematcher": "0.2.31", "tslib": "2.4.0" + }, + "dependencies": { + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + } } }, "intl-messageformat": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.1.1.tgz", - "integrity": "sha512-FeJne2oooYW6shLPbrqyjRX6hTELVrQ90Dn88z7NomLk/xZBCLxLPAkgaYaTQJBRBV78nZ933d8APHHkTQrD9Q==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.2.1.tgz", + "integrity": "sha512-1lrJG2qKzcC1TVzYu1VuB1yiY68LU5rwpbHa2THCzA67Vutkz7+1lv5U20K3Lz5RAiH78zxNztMEtchokMWv8A==", "requires": { - "@formatjs/ecma402-abstract": "1.11.8", - "@formatjs/fast-memoize": "1.2.4", - "@formatjs/icu-messageformat-parser": "2.1.4", + "@formatjs/ecma402-abstract": "1.13.0", + "@formatjs/fast-memoize": "1.2.6", + "@formatjs/icu-messageformat-parser": "2.1.10", "tslib": "2.4.0" + }, + "dependencies": { + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + } } }, "react-intl": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-6.0.5.tgz", - "integrity": "sha512-nDZ3BosuE8WdovcGxsrjj1aIgJZklSL5aORs5oah+5tLQTzUdOEstzJEYQPM+sxl1dkDOu7RCuw0z9oI9ENf9g==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-6.2.1.tgz", + "integrity": "sha512-hYxcSamgoA3Mvc55nwhTF1v15T0NUSkaV/EScMNVZXg0kRyaMAoNHkCi9/9H+TnXWNiWrcWH9bjlMlJwrG2V7g==", "requires": { - "@formatjs/ecma402-abstract": "1.11.8", - "@formatjs/icu-messageformat-parser": "2.1.4", - "@formatjs/intl": "2.3.1", - "@formatjs/intl-displaynames": "6.0.3", - "@formatjs/intl-listformat": "7.0.3", + "@formatjs/ecma402-abstract": "1.13.0", + "@formatjs/icu-messageformat-parser": "2.1.10", + "@formatjs/intl": "2.5.1", + "@formatjs/intl-displaynames": "6.1.4", + "@formatjs/intl-listformat": "7.1.3", "@types/hoist-non-react-statics": "^3.3.1", "@types/react": "16 || 17 || 18", "hoist-non-react-statics": "^3.3.2", - "intl-messageformat": "10.1.1", + "intl-messageformat": "10.2.1", "tslib": "2.4.0" + }, + "dependencies": { + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + } } }, "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" } } }, "@tldraw/vec": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@tldraw/vec/-/vec-1.7.1.tgz", - "integrity": "sha512-qM6Z9RvkLFFEzr91mmsA4HI14msyDgDDOu36csIzG5BYu2bFmEz5siQ8WntHgDtUjzJHP+VSSOTbAXhklEZHLA==" + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@tldraw/vec/-/vec-1.8.0.tgz", + "integrity": "sha512-GiS5Df3CzXY/fPBFcM0CKFERZfI4Cg1X33VPZX+NLo7Fwm/h9zu/aU24N1mG75Q9LuMnwKm7woxKr8BiUXGYCg==" }, "@types/hoist-non-react-statics": { "version": "3.3.1", @@ -1345,16 +1631,16 @@ "integrity": "sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==" }, "@use-gesture/core": { - "version": "10.2.17", - "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.2.17.tgz", - "integrity": "sha512-62hCybe4x6oGZ1/JA9gSYIdghV1FqxCdvYWt9SqCEAAikwT1OmVl2Q/Uu8CP636L57D+DfXtw6PWM+fdhr4oJQ==" + "version": "10.2.20", + "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.2.20.tgz", + "integrity": "sha512-4lFhHc8so4yIHkBEs641DnEsBxPyhJ5GEjB4PURFDH4p/FcZriH6w99knZgI63zN/MBFfylMyb8+PDuj6RIXKQ==" }, "@use-gesture/react": { - "version": "10.2.17", - "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.2.17.tgz", - "integrity": "sha512-Vfrp1KgdYn/kOEUAYNXtGBCl2dr38s3G6rru1TOPs+cVUjfNyNxvJK56grUyJ336N3rQLK8F9G7+FfrHuc3g/Q==", + "version": "10.2.20", + "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.2.20.tgz", + "integrity": "sha512-KnJq9ZSqprWA6uNhWTUHZqTCh+rfa0j8ehTzqeBhktUPrmTj7yVOBvEQ/vSFU/7d72cGgWSsJ0f5T6GQCHXnvg==", "requires": { - "@use-gesture/core": "10.2.17" + "@use-gesture/core": "10.2.20" } }, "acorn": { @@ -1468,18 +1754,11 @@ } }, "aria-hidden": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.1.3.tgz", - "integrity": "sha512-RhVWFtKH5BiGMycI72q2RAFMLQi8JP9bLuQXgR5a8Znp7P5KOIADSJeyfI8PCVxLEp067B2HbP5JIiI/PXIZeA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.1.tgz", + "integrity": "sha512-PN344VAf9j1EAi+jyVHOJ8XidQdPVssGco39eNcsGdM4wcsILtxrKLkbuiMfLWYROK1FjRQasMWCBttrhjnr6A==", "requires": { - "tslib": "^1.0.0" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - } + "tslib": "^2.0.0" } }, "aria-query": { @@ -1938,6 +2217,11 @@ "@browser-bunyan/levels": "^1.8.0" } }, + "browser-fs-access": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/browser-fs-access/-/browser-fs-access-0.31.1.tgz", + "integrity": "sha512-jMz9f56DkLM7LyA8wZYO7CtpoF3RdUk1/FXrnRNybgV0R5eqk/fgFWR0k5IMjPYgK4jmZecytP/UDO5WBi9Dhg==" + }, "browserslist": { "version": "4.20.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.2.tgz", @@ -4329,11 +4613,6 @@ "yallist": "^4.0.0" } }, - "lz-string": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", - "integrity": "sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==" - }, "makeup-screenreader-trap": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/makeup-screenreader-trap/-/makeup-screenreader-trap-0.0.5.tgz", @@ -5184,11 +5463,6 @@ "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.4.2.tgz", "integrity": "sha512-b4xQJYiH8sb0sEbfq/Ws3N77DEJtSihUFD1moeiz2jNoJ5B+mqJutt54ouO9iEfkp7Wk4jQDsVUOh7DPEW3wEw==" }, - "mobx-react-lite": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-3.4.0.tgz", - "integrity": "sha512-bRuZp3C0itgLKHu/VNxi66DN/XVkQG7xtoBVWxpvC5FhAqbOCP21+nPhULjnzEqd7xBMybp6KwytdUpZKEgpIQ==" - }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -5699,9 +5973,9 @@ "dev": true }, "perfect-freehand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/perfect-freehand/-/perfect-freehand-1.1.0.tgz", - "integrity": "sha512-nVWukMN9qlii1dQsQHVvfaNpeOAWVLgTZP6e/tFcU6cWlLo+6YdvfRGBL2u5pU11APlPbHeB0SpMcGA8ZjPgcQ==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/perfect-freehand/-/perfect-freehand-1.2.0.tgz", + "integrity": "sha512-h/0ikF1M3phW7CwpZ5MMvKnfpHficWoOEyr//KVNTxV4F6deRK1eYMtHyBKEAKFK0aXIEUK9oBvlF6PNXMDsAw==" }, "performance-now": { "version": "2.1.0", @@ -6165,9 +6439,9 @@ } }, "react-remove-scroll-bar": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.3.tgz", - "integrity": "sha512-i9GMNWwpz8XpUpQ6QlevUtFjHGqnPG4Hxs+wlIJntu/xcsZVEpJcIV71K3ZkqNy2q3GfgvkD7y6t/Sv8ofYSbw==", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz", + "integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==", "requires": { "react-style-singleton": "^2.2.1", "tslib": "^2.0.0" @@ -6351,11 +6625,6 @@ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true }, - "resize-observer-polyfill": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", - "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" - }, "resolve": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", @@ -7103,6 +7372,11 @@ "resolved": "https://registry.npmjs.org/use-context-selector/-/use-context-selector-1.3.7.tgz", "integrity": "sha512-O94hcN9UDAPTC4Fsm3p6Og5PVlhTEeKqxJX3HuBbVSuevOSPLDZxowFUmx49/fnu9jpgY83Nd3TALJVDRtYzdQ==" }, + "use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==" + }, "use-sidecar": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", @@ -7112,6 +7386,11 @@ "tslib": "^2.0.0" } }, + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==" + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -7282,9 +7561,12 @@ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" }, "zustand": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", - "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==" + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.1.4.tgz", + "integrity": "sha512-k2jVOlWo8p4R83mQ+/uyB8ILPO2PCJOf+QVjcL+1PbMCk1w5OoPYpAIxy9zd93FSfmJqoH6lGdwzzjwqJIRU5A==", + "requires": { + "use-sync-external-store": "1.2.0" + } } } } diff --git a/bigbluebutton-html5/package.json b/bigbluebutton-html5/package.json index 544d0bea6c..058eca32cf 100755 --- a/bigbluebutton-html5/package.json +++ b/bigbluebutton-html5/package.json @@ -33,8 +33,8 @@ "@jitsi/sdp-interop": "0.1.14", "@material-ui/core": "^4.12.4", "@mconf/bbb-diff": "^1.2.0", - "@tldraw/core": "1.15.0", - "@tldraw/tldraw": "1.20.0", + "@tldraw/core": "1.20.0", + "@tldraw/tldraw": "1.26.1", "autoprefixer": "^10.4.4", "axios": "^0.21.3", "babel-runtime": "~6.26.0", diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml index 7fb0977899..93add10313 100755 --- a/bigbluebutton-html5/private/config/settings.yml +++ b/bigbluebutton-html5/private/config/settings.yml @@ -57,6 +57,7 @@ public: allowUserLookup: false dynamicGuestPolicy: true enableGuestLobbyMessage: true + alwaysShowWaitingRoomUI: true enableLimitOfViewersInWebcam: false enableMultipleCameras: true # Allow users to open webcam video modal/preview when video is already @@ -144,6 +145,13 @@ public: enableNetworkStats: true # Enable the button to allow users to copy network stats to clipboard enableCopyNetworkStatsButton: true + # where should client settings be stored? if you run a single BBB server or + # a cluster with a reverse proxy in front of it, you may set this to 'local' + # See https://docs.bigbluebutton.org/admin/clusterproxy.html + # allowed values: + # 'session' -> settings are stored in browser sessionStorage + # 'local' -> settings are stored in browser localStorage + userSettingsStorage: session defaultSettings: application: animations: true diff --git a/bigbluebutton-html5/private/static/guest-wait/guest-wait.html b/bigbluebutton-html5/private/static/guest-wait/guest-wait.html index 22bb238761..e4c1f899fa 100755 --- a/bigbluebutton-html5/private/static/guest-wait/guest-wait.html +++ b/bigbluebutton-html5/private/static/guest-wait/guest-wait.html @@ -392,7 +392,7 @@
-

Please wait for a moderator to approve you joining the meeting.

+

Please wait for a moderator to approve you joining the meeting.

Calculating position in waiting queue

diff --git a/bigbluebutton-html5/public/locales/ar.json b/bigbluebutton-html5/public/locales/ar.json index a091308306..083a804fdf 100644 --- a/bigbluebutton-html5/public/locales/ar.json +++ b/bigbluebutton-html5/public/locales/ar.json @@ -230,6 +230,7 @@ "app.presentationUploder.conversion.generatedSlides": "الشرائح المنشأة...", "app.presentationUploder.conversion.generatingSvg": "إنشاء صور SVG ...", "app.presentationUploder.conversion.pageCountExceeded": "تجاوز عدد الصفحات الحد الأقصى {0}", + "app.presentationUploder.conversion.conversionTimeout": "لا يمكن معالجة الشريحة {0} خلال {1} من المحاولات.", "app.presentationUploder.conversion.officeDocConversionInvalid": "فشل في معالجة مستند المكتب. الرجاء تحميل بي دي اف بدلاً من ذلك.", "app.presentationUploder.conversion.officeDocConversionFailed": "فشل في معالجة مستند المكتب. الرجاء تحميل بي دي اف بدلاً من ذلك.", "app.presentationUploder.conversion.pdfHasBigPage": "لم نتمكن من تحويل ملف بي دي اف ، يرجى محاولة تحسينه. أقصى حجم للصفحة {0}", @@ -270,7 +271,7 @@ "app.poll.clickHereToSelect": "انقر هنا للاختيار", "app.poll.question.label" : "اكتب سؤالك ...", "app.poll.optionalQuestion.label" : "اكتب سؤالك (اختياري) ...", - "app.poll.userResponse.label" : "استجابة المستخدم", + "app.poll.userResponse.label" : "استجابة مكتوبة", "app.poll.responseTypes.label" : "أنواع الاستجابة", "app.poll.optionDelete.label" : "حذف", "app.poll.responseChoices.label" : "خيارات الاستجابة", @@ -599,8 +600,11 @@ "app.error.403": "لقد تمت إزالتك من المؤتمر", "app.error.404": "غير معثور عليه", "app.error.408": "المصادقة فشلت", + "app.error.409": "تضاد", "app.error.410": "انتهى الاجتماع", "app.error.500": "عفوا، حدث خطأ ما", + "app.error.503": "لقد تم قطع اتصالك", + "app.error.disconnected.rejoin": "يمكنك تحديث الصفحة للانضمام مرة أخرى.", "app.error.userLoggedOut": "المستخدم لديه رمز جلسة غير صالح بسبب تسجيل الخروج", "app.error.ejectedUser": "المستخدم لديه رمز جلسة غير صالح بسبب الطرد", "app.error.joinedAnotherWindow": "يبدو أن هذه الجلسة مفتوحة في نافذة متصفح أخر.", diff --git a/bigbluebutton-html5/public/locales/de.json b/bigbluebutton-html5/public/locales/de.json index 6744c78e5a..5268c522fc 100644 --- a/bigbluebutton-html5/public/locales/de.json +++ b/bigbluebutton-html5/public/locales/de.json @@ -230,6 +230,7 @@ "app.presentationUploder.conversion.generatedSlides": "Folien wurden generiert...", "app.presentationUploder.conversion.generatingSvg": "SVG-Bilder werden generiert...", "app.presentationUploder.conversion.pageCountExceeded": "Maximale Seitenanzahl von {0} Seiten wurde überschritten", + "app.presentationUploder.conversion.conversionTimeout": "Folie {0} konnte innerhalb von {1} Versuchen nicht verarbeitet werden.", "app.presentationUploder.conversion.officeDocConversionInvalid": "Die Verarbeitung des Office-Dokuments ist fehlgeschlagen, bitte eine PDF-Datei hochladen versuchen.", "app.presentationUploder.conversion.officeDocConversionFailed": "Die Verarbeitung des Office-Dokuments ist fehlgeschlagen, bitte eine PDF-Datei hochladen.", "app.presentationUploder.conversion.pdfHasBigPage": "Die PDF-Datei konnte nicht konvertiert werden, bitte versuchen Sie die Datei zu optimieren. Die maximale Seitenzahl beträgt {0} Seiten.", @@ -270,7 +271,7 @@ "app.poll.clickHereToSelect": "Zum Auswählen hier klicken", "app.poll.question.label" : "Eine Frage stellen...", "app.poll.optionalQuestion.label" : "Eine Frage stellen (optional)...", - "app.poll.userResponse.label" : "Teilnehmerantwort", + "app.poll.userResponse.label" : "Getippte Antwort", "app.poll.responseTypes.label" : "Antworttypen", "app.poll.optionDelete.label" : "Löschen", "app.poll.responseChoices.label" : "Antwortmöglichkeiten", @@ -599,8 +600,11 @@ "app.error.403": "Sie wurden aus der Konferenz entfernt", "app.error.404": "Nicht gefunden", "app.error.408": "Authentifizierung fehlgeschlagen", + "app.error.409": "Problem", "app.error.410": "Die Konferenz ist zu Ende", "app.error.500": "Ups, irgendwas ist schiefgelaufen", + "app.error.503": "Ihre Verbindung wurde unterbrochen", + "app.error.disconnected.rejoin": "Sie können die Seite aktualisieren, um sich wieder anzumelden.", "app.error.userLoggedOut": "Teilnehmer hat einen ungültigen Konferenz-Token, weil er sich ausgeloggt hat", "app.error.ejectedUser": "Teilnehmer hat einen ungültigen Konferenz-Token, weil er gesperrt wurde", "app.error.joinedAnotherWindow": "Diese Konferenz scheint in einem anderen Browserfenster geöffnet zu sein.", @@ -777,7 +781,7 @@ "app.video.securityError": "Medienunterstützung ist für das Dokument deaktiviert", "app.video.typeError": "Liste der angegebenen Bedingungen ist leer oder alle Bedingungen sind auf falsch gesetzt", "app.video.notFoundError": "Konnte keine Webcam finden. Bitte prüfen, dass eine Kamera angeschlossen ist und nicht von einem anderen Programm blockiert wird.", - "app.video.notAllowed": "Fehlende Berechtigung für die Webcamfreigabe. Bitte die Berechtigungen iim Browser prüfen", + "app.video.notAllowed": "Fehlende Berechtigung für die Webcamfreigabe. Bitte die Berechtigungen im Browser prüfen", "app.video.notSupportedError": "Webcam kann nur über eine sichere Verbindung freigegeben werden, bitte prüfen ob das SSL-Zertifikat gültig ist", "app.video.notReadableError": "Konnte nicht auf die Webcam zugreifen. Bitte prüfen, dass kein anderes Programm auf die Webcam zugreift", "app.video.timeoutError": "Der Browser hat nicht rechtzeitig reagiert.", @@ -909,7 +913,7 @@ "app.createBreakoutRoom.addRoomTime": "Gruppenraumzeit erhöhen auf", "app.createBreakoutRoom.addParticipantLabel": "+ Teilnehmer hinzufügen", "app.createBreakoutRoom.freeJoin": "Den Teilnehmern erlauben, sich selbst einen Gruppenraum auszusuchen.", - "app.createBreakoutRoom.leastOneWarnBreakout": "Jedem Gruppenraum muss wenigstens ein Teilnehmer zugeordnet sein.", + "app.createBreakoutRoom.leastOneWarnBreakout": "Wenigstens ein Teilnehmer muss einem Gruppenraum zugeordnet sein.", "app.createBreakoutRoom.minimumDurationWarnBreakout": "Die Mindestdauer für einen Gruppenraum beträgt {0} Minuten.", "app.createBreakoutRoom.modalDesc": "Tipp: Sie können die Teilnehmer per Drag-and-Drop einem bestimmten Gruppenraum zuweisen.", "app.createBreakoutRoom.roomTime": "{0} Minuten", diff --git a/bigbluebutton-html5/public/locales/en.json b/bigbluebutton-html5/public/locales/en.json index abdc986e2d..98e62237f2 100755 --- a/bigbluebutton-html5/public/locales/en.json +++ b/bigbluebutton-html5/public/locales/en.json @@ -197,7 +197,7 @@ "app.presentation.endSlideContent": "Slide content end", "app.presentation.changedSlideContent": "Presentation changed to slide: {0}", "app.presentation.emptySlideContent": "No content for current slide", - "app.presentation.options.fullscreen": "Fullscreen", + "app.presentation.options.fullscreen": "Fullscreen Presentation", "app.presentation.options.exitFullscreen": "Exit Fullscreen", "app.presentation.options.minimize": "Minimize", "app.presentation.options.snapshot": "Snapshot of current slide", @@ -311,7 +311,7 @@ "app.poll.clickHereToSelect": "Click here to select", "app.poll.question.label" : "Write your question...", "app.poll.optionalQuestion.label" : "Write your question (optional)...", - "app.poll.userResponse.label" : "User Response", + "app.poll.userResponse.label" : "Typed Response", "app.poll.responseTypes.label" : "Response Types", "app.poll.optionDelete.label" : "Delete", "app.poll.responseChoices.label" : "Response Choices", @@ -370,11 +370,11 @@ "app.muteWarning.disableMessage": "Mute alerts disabled until unmute", "app.muteWarning.tooltip": "Click to close and disable warning until next unmute", "app.navBar.settingsDropdown.optionsLabel": "Options", - "app.navBar.settingsDropdown.fullscreenLabel": "Make fullscreen", + "app.navBar.settingsDropdown.fullscreenLabel": "Fullscreen Application", "app.navBar.settingsDropdown.settingsLabel": "Settings", "app.navBar.settingsDropdown.aboutLabel": "About", "app.navBar.settingsDropdown.leaveSessionLabel": "Leave meeting", - "app.navBar.settingsDropdown.exitFullscreenLabel": "Exit fullscreen", + "app.navBar.settingsDropdown.exitFullscreenLabel": "Exit Fullscreen", "app.navBar.settingsDropdown.fullscreenDesc": "Make the settings menu fullscreen", "app.navBar.settingsDropdown.settingsDesc": "Change the general settings", "app.navBar.settingsDropdown.aboutDesc": "Show information about the client", @@ -554,6 +554,7 @@ "app.breakout.dropdown.manageDuration": "Change duration", "app.breakout.dropdown.destroyAll": "End breakout rooms", "app.breakout.dropdown.options": "Breakout Options", + "app.breakout.dropdown.manageUsers": "Manage users", "app.calculatingBreakoutTimeRemaining": "Calculating remaining time ...", "app.audioModal.ariaTitle": "Join audio modal", "app.audioModal.microphoneLabel": "Microphone", @@ -623,6 +624,9 @@ "app.audio.permissionsOverlay.hint": "We need you to allow us to use your media devices in order to join you to the voice conference :)", "app.audio.captions.button.start": "Start closed captions", "app.audio.captions.button.stop": "Stop closed captions", + "app.audio.captions.button.language": "Language", + "app.audio.captions.button.transcription": "Transcription", + "app.audio.captions.button.transcriptionSettings": "Transcription settings", "app.audio.captions.speech.title": "Automatic transcription", "app.audio.captions.speech.disabled": "Disabled", "app.audio.captions.speech.unsupported": "Your browser doesn't support speech recognition. Your audio won't be transcribed", @@ -643,6 +647,7 @@ "app.meeting.logout.ejectedFromMeeting": "You have been removed from the meeting", "app.meeting.logout.validateTokenFailedEjectReason": "Failed to validate authorization token", "app.meeting.logout.userInactivityEjectReason": "User inactive for too long", + "app.meeting.logout.maxParticipantsReached": "The maximum number of participants allowed for this meeting has been reached", "app.meeting-ended.rating.legendLabel": "Feedback rating", "app.meeting-ended.rating.starLabel": "Star", "app.modal.close": "Close", @@ -718,6 +723,8 @@ "app.toast.chat.public": "New Public Chat message", "app.toast.chat.private": "New Private Chat message", "app.toast.chat.system": "System", + "app.toast.chat.poll": "Poll Results", + "app.toast.chat.pollClick": "Poll results were published. Click here to see.", "app.toast.clearedEmoji.label": "Emoji status cleared", "app.toast.setEmoji.label": "Emoji status set to {0}", "app.toast.meetingMuteOn.label": "All users have been muted", @@ -839,7 +846,7 @@ "app.connection-status.next": "Next page", "app.connection-status.prev": "Previous page", "app.learning-dashboard.label": "Learning Analytics Dashboard", - "app.learning-dashboard.description": "Open dashboard with users activities", + "app.learning-dashboard.description": "Dashboard with users activities", "app.learning-dashboard.clickHereToOpen": "Open Learning Analytics Dashboard", "app.recording.startTitle": "Start recording", "app.recording.stopTitle": "Pause recording", @@ -875,6 +882,8 @@ "app.videoPreview.profileNotFoundLabel": "No supported camera profile", "app.videoPreview.brightness": "Brightness", "app.videoPreview.wholeImageBrightnessLabel": "Whole image", + "app.videoPreview.wholeImageBrightnessDesc": "Applies brighteness to stream and background image", + "app.videoPreview.sliderDesc": "Increase or decrease levels of brightness", "app.video.joinVideo": "Share webcam", "app.video.connecting": "Webcam sharing is starting ...", "app.video.leaveVideo": "Stop sharing webcam", @@ -928,6 +937,7 @@ "app.video.virtualBackground.errorOnRead": "Something went wrong when reading the file.", "app.video.virtualBackground.uploaded": "Uploaded", "app.video.virtualBackground.uploading": "Uploading...", + "app.video.virtualBackground.button.customDesc": "Adds a new virtual background image", "app.video.camCapReached": "You cannot share more cameras", "app.video.meetingCamCapReached": "Meeting reached it's simultaneous cameras limit", "app.video.dropZoneLabel": "Drop here", @@ -948,6 +958,7 @@ "app.whiteboard.annotations.poll": "Poll results were published", "app.whiteboard.annotations.pollResult": "Poll Result", "app.whiteboard.annotations.noResponses": "No responses", + "app.whiteboard.annotations.notAllowed": "You are not allowed to make this change", "app.whiteboard.toolbar.tools": "Tools", "app.whiteboard.toolbar.tools.hand": "Pan", "app.whiteboard.toolbar.tools.pencil": "Pencil", @@ -1032,6 +1043,8 @@ "app.createBreakoutRoom.addRoomTime": "Increase breakout room time to", "app.createBreakoutRoom.addParticipantLabel": "+ Add participant", "app.createBreakoutRoom.freeJoin": "Allow users to choose a breakout room to join", + "app.createBreakoutRoom.captureNotes": "Capture shared notes when breakout rooms end", + "app.createBreakoutRoom.captureSlides": "Capture whiteboard when breakout rooms end", "app.createBreakoutRoom.leastOneWarnBreakout": "You must place at least one user in a breakout room.", "app.createBreakoutRoom.minimumDurationWarnBreakout": "Minimum duration for a breakout room is {0} minutes.", "app.createBreakoutRoom.modalDesc": "Tip: You can drag-and-drop a user's name to assign them to a specific breakout room.", @@ -1044,11 +1057,13 @@ "app.createBreakoutRoom.setTimeCancel": "Cancel", "app.createBreakoutRoom.setTimeHigherThanMeetingTimeError": "The breakout rooms duration can't exceed the meeting remaining time.", "app.createBreakoutRoom.roomNameInputDesc": "Updates breakout room name", + "app.createBreakoutRoom.movedUserLabel": "Moved {0} to room {1}", "app.updateBreakoutRoom.modalDesc": "To update or invite a user, simply drag them into the desired room.", "app.updateBreakoutRoom.cancelLabel": "Cancel", "app.updateBreakoutRoom.title": "Update Breakout Rooms", "app.updateBreakoutRoom.confirm": "Apply", "app.updateBreakoutRoom.userChangeRoomNotification": "You were moved to room {0}.", + "app.smartMediaShare.externalVideo": "External video(s)", "app.update.resetRoom": "Reset user room", "app.externalVideo.start": "Share a new video", "app.externalVideo.title": "Share an external video", @@ -1082,6 +1097,8 @@ "app.layout.modal.keepPushingLayoutLabel": "Keep pushing to everyone", "app.layout.modal.pushLayoutLabel": "Push to everyone", "app.layout.modal.layoutToastLabel": "Layout settings changed", + "app.layout.modal.layoutSingular": "Layout", + "app.layout.modal.layoutBtnDesc": "Sets layout as selected option", "app.layout.style.custom": "Custom", "app.layout.style.smart": "Smart layout", "app.layout.style.presentationFocus": "Focus on presentation", diff --git a/bigbluebutton-html5/public/locales/pt_BR.json b/bigbluebutton-html5/public/locales/pt_BR.json index 8ed40ebbd2..b7c36b558e 100644 --- a/bigbluebutton-html5/public/locales/pt_BR.json +++ b/bigbluebutton-html5/public/locales/pt_BR.json @@ -825,6 +825,7 @@ "app.whiteboard.annotations.poll": "Os resultados da enquete foram publicados", "app.whiteboard.annotations.pollResult": "Resultado da Enquete", "app.whiteboard.annotations.noResponses": "Sem respostas", + "app.whiteboard.annotations.notAllowed": "Você não tem permissão para fazer essa alteração", "app.whiteboard.toolbar.tools": "Ferramentas", "app.whiteboard.toolbar.tools.hand": "Mover", "app.whiteboard.toolbar.tools.pencil": "Lápis", diff --git a/bigbluebutton-html5/public/locales/tr.json b/bigbluebutton-html5/public/locales/tr.json index d03b426601..8428e5a0f0 100644 --- a/bigbluebutton-html5/public/locales/tr.json +++ b/bigbluebutton-html5/public/locales/tr.json @@ -270,7 +270,7 @@ "app.poll.clickHereToSelect": "Seçmek için buraya tıklayın", "app.poll.question.label" : "Sorunuzu yazın...", "app.poll.optionalQuestion.label" : "Sorunuzu yazın (isteğe bağlı)...", - "app.poll.userResponse.label" : "Kullanıcı yanıtı", + "app.poll.userResponse.label" : "Yazılan yanıt", "app.poll.responseTypes.label" : "Yanıt türleri", "app.poll.optionDelete.label" : "Sil", "app.poll.responseChoices.label" : "Yanıt seçenekleri", diff --git a/bigbluebutton-tests/playwright/breakout/breakout.spec.js b/bigbluebutton-tests/playwright/breakout/breakout.spec.js index 86a9787db8..a117bb5447 100644 --- a/bigbluebutton-tests/playwright/breakout/breakout.spec.js +++ b/bigbluebutton-tests/playwright/breakout/breakout.spec.js @@ -3,38 +3,122 @@ const { Create } = require('./create'); const { Join } = require('./join'); test.describe.parallel('Breakout', () => { - test('Create Breakout room @ci', async ({ browser, context, page }) => { - const create = new Create(browser, context); - await create.initPages(page); - await create.create(); + + test.describe.parallel('Creating', () => { + test('Create Breakout room @ci', async ({ browser, context, page }) => { + const create = new Create(browser, context); + await create.initPages(page); + await create.create(); + }); + + test('Change number of rooms', async ({ browser, context, page }) => { + const create = new Create(browser, context); + await create.initPages(page); + await create.changeNumberOfRooms(); + }); + + test('Change duration time', async ({ browser, context, page }) => { + const create = new Create(browser, context); + await create.initPages(page); + await create.changeDurationTime(); + }); + + test('Change rooms name', async ({ browser, context, page }) => { + const create = new Create(browser, context); + await create.initPages(page); + await create.changeRoomsName(); + }); + + test('Remove and reset assignments', async ({ browser, context, page }) => { + const create = new Create(browser, context); + await create.initPages(page); + await create.removeAndResetAssignments(); + }); + + test('Drag and drop user in a room', async ({ browser, context, page }) => { + const create = new Create(browser, context); + await create.initPages(page); + await create.dragDropUserInRoom(); + }); }); - // https://docs.bigbluebutton.org/2.6/release-tests.html#moderators-creating-breakout-rooms-and-assiging-users-automated - test('Join Breakout room @ci', async ({ browser, context, page }) => { - const join = new Join(browser, context); - await join.initPages(page); - await join.create() - await join.joinRoom(); - }); + test.describe.parallel('After creating', () => { + // https://docs.bigbluebutton.org/2.6/release-tests.html#moderators-creating-breakout-rooms-and-assiging-users-automated + test('Join Breakout room @ci', async ({ browser, context, page }) => { + const join = new Join(browser, context); + await join.initPages(page); + await join.create() + await join.joinRoom(); + }); - test('Join Breakout room and share webcam', async ({ browser, context, page }) => { - const join = new Join(browser, context); - await join.initPages(page); - await join.create() - await join.joinAndShareWebcam(); - }); + test('Join Breakout room and share webcam', async ({ browser, context, page }) => { + const join = new Join(browser, context); + await join.initPages(page); + await join.create() + await join.joinAndShareWebcam(); + }); - test('Join Breakout room and share screen', async ({ browser, context, page }) => { - const join = new Join(browser, context); - await join.initPages(page); - await join.create(); - await join.joinAndShareScreen(); - }); + test('Join Breakout room and share screen', async ({ browser, context, page }) => { + const join = new Join(browser, context); + await join.initPages(page); + await join.create(); + await join.joinAndShareScreen(); + }); - test('Join Breakout room with Audio', async ({ browser, context, page }) => { - const join = new Join(browser, context); - await join.initPages(page); - await join.create(); - await join.joinWithAudio(); + test('Join Breakout room with Audio', async ({ browser, context, page }) => { + const join = new Join(browser, context); + await join.initPages(page); + await join.create(); + await join.joinWithAudio(); + }); + + test('Message to all rooms', async ({ browser, context, page }) => { + const join = new Join(browser, context); + await join.initPages(page); + await join.create(); + await join.messageToAllRooms(); + }); + + test('Change duration time', async ({ browser, context, page }) => { + const join = new Join(browser, context); + await join.initPages(page); + await join.create(); + await join.changeDurationTime(); + }); + + test('User name shows below rooms name', async ({ browser, context, page }) => { + const join = new Join(browser, context); + await join.initPages(page); + await join.create(); + await join.usernameShowsBelowRoomsName(); + }); + + test('Show breakout room time remaining', async ({ browser, context, page }) => { + const join = new Join(browser, context); + await join.initPages(page); + await join.create(); + await join.showBreakoutRoomTimeRemaining(); + }); + + test('End all breakout rooms', async ({ browser, context, page }) => { + const join = new Join(browser, context); + await join.initPages(page); + await join.create(); + await join.endAllBreakoutRooms(); + }); + + test('Invite user after creating rooms', async ({ browser, context, page }) => { + const join = new Join(browser, context); + await join.initPages(page); + await join.create(); + await join.inviteUserAfterCreatingRooms(); + }); + + test('Move user to another room', async ({ browser, context, page }) => { + const join = new Join(browser, context); + await join.initPages(page); + await join.create(); + await join.moveUserToOtherRoom(); + }); }); }); diff --git a/bigbluebutton-tests/playwright/breakout/create.js b/bigbluebutton-tests/playwright/breakout/create.js index 59a4272c0d..bb9a8694c3 100644 --- a/bigbluebutton-tests/playwright/breakout/create.js +++ b/bigbluebutton-tests/playwright/breakout/create.js @@ -1,6 +1,7 @@ const { MultiUsers } = require('../user/multiusers'); const e = require('../core/elements'); const { ELEMENT_WAIT_LONGER_TIME } = require('../core/constants'); +const { expect } = require('@playwright/test'); class Create extends MultiUsers { constructor(browser, context) { @@ -12,6 +13,7 @@ class Create extends MultiUsers { await this.modPage.waitAndClick(e.manageUsers); await this.modPage.waitAndClick(e.createBreakoutRooms); + //Randomly assignment await this.modPage.waitAndClick(e.randomlyAssign); await this.modPage.waitAndClick(e.modalConfirmButton, ELEMENT_WAIT_LONGER_TIME); @@ -19,6 +21,85 @@ class Create extends MultiUsers { await this.userPage.waitAndClick(e.modalDismissButton); await this.modPage.hasElement(e.breakoutRoomsItem); } + + async changeNumberOfRooms() { + await this.modPage.waitAndClick(e.manageUsers); + await this.modPage.waitAndClick(e.createBreakoutRooms); + + await this.modPage.waitAndClick(e.randomlyAssign); + await this.modPage.getLocator(e.selectNumberOfRooms).selectOption('7'); + await this.modPage.waitAndClick(e.modalConfirmButton, ELEMENT_WAIT_LONGER_TIME); + await this.modPage.waitAndClick(e.breakoutRoomsItem); + await this.modPage.checkElementCount(e.userNameBreakoutRoom7, 1); + } + + async changeDurationTime() { + await this.modPage.waitAndClick(e.manageUsers); + await this.modPage.waitAndClick(e.createBreakoutRooms); + await this.modPage.waitAndClick(e.randomlyAssign); + + //test minimum 5 minutes + await this.modPage.getLocator(e.durationTime).press('Backspace'); + await this.modPage.getLocator(e.durationTime).press('Backspace'); + await this.modPage.type(e.durationTime, '5'); + await this.modPage.waitAndClick(e.decreaseBreakoutTime); + await this.modPage.hasValue(e.durationTime, '5'); + + await this.modPage.getLocator(e.durationTime).press('Backspace'); + await this.modPage.type(e.durationTime, '15'); + await this.modPage.waitAndClick(e.increaseBreakoutTime); + await this.modPage.waitAndClick(e.modalConfirmButton, ELEMENT_WAIT_LONGER_TIME); + await this.modPage.waitAndClick(e.breakoutRoomsItem); + await this.modPage.hasText(e.breakoutRemainingTime, /15:[0-5][0-9]/); + } + + async changeRoomsName() { + await this.modPage.waitAndClick(e.manageUsers); + await this.modPage.waitAndClick(e.createBreakoutRooms); + await this.modPage.waitAndClick(e.randomlyAssign); + //Change room's name + await this.modPage.type(e.roomNameInput, 'Test'); + await this.modPage.waitAndClick(e.modalConfirmButton, ELEMENT_WAIT_LONGER_TIME); + await this.modPage.waitAndClick(e.breakoutRoomsItem); + await this.modPage.hasText(e.roomName1Test, /Test/); + } + + async removeAndResetAssignments() { + await this.modPage.waitAndClick(e.manageUsers); + await this.modPage.waitAndClick(e.createBreakoutRooms); + + //Reset assignments + await this.modPage.waitAndClick(e.randomlyAssign); + await this.modPage.hasText(e.breakoutBox1, /Attendee/); + await this.modPage.waitAndClick(e.resetAssignments); + await this.modPage.hasText(e.breakoutBox0, /Attendee/); + + //Remove specific assignment + await this.modPage.waitAndClick(e.randomlyAssign); + await this.modPage.dragDropSelector(e.moveUser, e.breakoutBox0); + await this.modPage.hasText(e.breakoutBox0, /Attendee/); + } + + async dragDropUserInRoom() { + await this.modPage.waitAndClick(e.manageUsers); + await this.modPage.waitAndClick(e.createBreakoutRooms); + + //testing no user assigned + await this.modPage.waitAndClick(e.modalConfirmButton); + await this.modPage.hasElement(e.warningNoUserAssigned); + + //await this.modPage.hasElementDisabled(e.modalConfirmButton); + const modalConfirmButton = await this.modPage.getLocator(e.modalConfirmButton); + await expect(modalConfirmButton, 'Getting error when trying to create a breakout room without designating any user.').toBeDisabled(); + + await this.modPage.dragDropSelector(e.userTest, e.breakoutBox1); + await this.modPage.hasText(e.breakoutBox1, /Attendee/); + await this.modPage.waitAndClick(e.modalConfirmButton, ELEMENT_WAIT_LONGER_TIME); + await this.userPage.waitAndClick(e.modalConfirmButton); + + await this.modPage.waitAndClick(e.breakoutRoomsItem); + await this.modPage.hasText(e.userNameBreakoutRoom, /Attendee/); + } } exports.Create = Create; diff --git a/bigbluebutton-tests/playwright/breakout/join.js b/bigbluebutton-tests/playwright/breakout/join.js index 9cdcb27881..971d9ae74f 100644 --- a/bigbluebutton-tests/playwright/breakout/join.js +++ b/bigbluebutton-tests/playwright/breakout/join.js @@ -3,6 +3,7 @@ const utilScreenShare = require('../screenshare/util'); const e = require('../core/elements'); const { ELEMENT_WAIT_LONGER_TIME } = require('../core/constants'); const { getSettings } = require('../core/settings'); +const { expect } = require('@playwright/test'); class Join extends Create { constructor(browser, context) { @@ -51,6 +52,93 @@ class Join extends Create { await breakoutUserPage.waitForSelector(e.talkingIndicator); await breakoutUserPage.hasElement(e.isTalking); } + + async messageToAllRooms() { + const breakoutUserPage = await this.joinRoom(); + await breakoutUserPage.hasElement(e.presentationTitle); + + await this.modPage.waitAndClick(e.breakoutRoomsItem); + await this.modPage.type(e.chatBox, "test"); + await this.modPage.waitAndClick(e.sendButton); + + await breakoutUserPage.hasElement(e.chatUserMessageText); + } + + async changeDurationTime() { + const breakoutUserPage = await this.joinRoom(); + await breakoutUserPage.hasElement(e.presentationTitle); + + await this.modPage.waitAndClick(e.breakoutRoomsItem); + await this.modPage.waitAndClick(e.breakoutOptionsMenu); + await this.modPage.waitAndClick(e.openBreakoutTimeManager); + await this.modPage.getLocator(e.inputSetTimeSelector).press('Backspace'); + await this.modPage.type(e.inputSetTimeSelector, '2'); + await this.modPage.waitAndClick(e.sendButtonDurationTime); + await this.modPage.hasText(e.breakoutRemainingTime, /[11-12]:[0-5][0-9]/); + + await breakoutUserPage.hasText(e.timeRemaining, /[11-12]:[0-5][0-9]/); + } + + async inviteUserAfterCreatingRooms() { + await this.modPage.waitAndClick(e.breakoutRoomsItem); + await this.modPage.waitAndClick(e.breakoutOptionsMenu); + await this.modPage.waitAndClick(e.openUpdateBreakoutUsersModal); + await this.modPage.dragDropSelector(e.userTest, e.breakoutBox1); + await this.modPage.hasText(e.breakoutBox1, /Attendee/); + await this.modPage.waitAndClick(e.modalConfirmButton); + + await this.userPage.hasElement(e.modalConfirmButton); + await this.userPage.waitAndClick(e.modalDismissButton); + } + + async usernameShowsBelowRoomsName() { + const breakoutUserPage = await this.joinRoom(); + await this.modPage.waitAndClick(e.breakoutRoomsItem); + await this.modPage.hasText(e.userNameBreakoutRoom, /Attendee/); + } + + async showBreakoutRoomTimeRemaining() { + const breakoutUserPage = await this.joinRoom(); + await breakoutUserPage.hasElement(e.presentationTitle); + + await this.modPage.waitAndClick(e.breakoutRoomsItem); + await this.modPage.waitAndClick(e.breakoutOptionsMenu); + await this.modPage.waitAndClick(e.openBreakoutTimeManager); + await this.modPage.getLocator(e.inputSetTimeSelector).press('Backspace'); + await this.modPage.type(e.inputSetTimeSelector, '2'); + await this.modPage.waitAndClick(e.sendButtonDurationTime); + await this.modPage.hasText(e.breakoutRemainingTime, /[11-12]:[0-5][0-9]/); + + await breakoutUserPage.hasText(e.timeRemaining,/[11-12]:[0-5][0-9]/); + } + + async endAllBreakoutRooms() { + await this.modPage.waitAndClick(e.breakoutRoomsItem); + await this.modPage.waitAndClick(e.breakoutOptionsMenu); + await this.modPage.waitAndClick(e.endAllBreakouts); + await this.modPage.wasRemoved(e.breakoutRoomsItem); + } + + async moveUserToOtherRoom() { + const breakoutUserPage = await this.joinRoom(); + await breakoutUserPage.hasElement(e.presentationTitle); + + await this.modPage.waitAndClick(e.breakoutRoomsItem); + await this.modPage.hasText(e.userNameBreakoutRoom, /Attendee/); + + await this.modPage.waitAndClick(e.breakoutOptionsMenu); + + await this.modPage.waitAndClick(e.openUpdateBreakoutUsersModal); + await this.modPage.dragDropSelector(e.moveUser, e.breakoutBox2); + await this.modPage.waitAndClick(e.modalConfirmButton); + + await this.userPage.waitForSelector(e.modalConfirmButton); + + await expect(breakoutUserPage.page.isClosed(), "Previous breakout room page did not close!").toBeTruthy(); + + await this.userPage.waitAndClick(e.modalConfirmButton); + await this.modPage.hasText(e.userNameBreakoutRoom2, /Attendee/); + } } exports.Join = Join; diff --git a/bigbluebutton-tests/playwright/chat/chat.js b/bigbluebutton-tests/playwright/chat/chat.js index e00eb8e927..2e2b221d2c 100644 --- a/bigbluebutton-tests/playwright/chat/chat.js +++ b/bigbluebutton-tests/playwright/chat/chat.js @@ -13,25 +13,22 @@ class Chat extends Page { async sendPublicMessage() { await openChat(this); - const message = this.getLocator(e.chatUserMessageText); - await expect(message).toHaveCount(0); + await this.checkElementCount(e.chatUserMessageText, 0); await this.type(e.chatBox, e.message); await this.waitAndClick(e.sendButton); - await this.waitForSelector(e.chatUserMessageText); - await expect(message).toHaveCount(1); + await this.checkElementCount(e.chatUserMessageText, 1); } async clearChat() { await openChat(this); - const message = this.getLocator(e.chatUserMessageText); await this.type(e.chatBox, e.message); await this.waitAndClick(e.sendButton); await this.waitForSelector(e.chatUserMessageText); // 1 message - await expect(message).toHaveCount(1); + await this.checkElementCount(e.chatUserMessageText, 1); // clear await this.waitAndClick(e.chatOptions); @@ -80,27 +77,25 @@ class Chat extends Page { async characterLimit() { await openChat(this); - const messageLocator = this.getLocator(e.chatUserMessageText); const { maxMessageLength } = getSettings(); await this.page.fill(e.chatBox, e.uniqueCharacterMessage.repeat(maxMessageLength)); await this.waitAndClick(e.sendButton); await this.waitForSelector(e.chatUserMessageText); - await expect(messageLocator).toHaveCount(1); + await this.checkElementCount(e.chatUserMessageText, 1); await this.page.fill(e.chatBox, e.uniqueCharacterMessage.repeat(maxMessageLength + 1)); await this.waitForSelector(e.typingIndicator); await this.waitAndClick(e.sendButton); await this.waitForSelector(e.chatUserMessageText); - await expect(messageLocator).toHaveCount(1); + await this.checkElementCount(e.chatUserMessageText, 1); } async emptyMessage() { await openChat(this); - const messageLocator = this.getLocator(e.chatUserMessageText); await this.waitAndClick(e.sendButton); - await expect(messageLocator).toHaveCount(0); + await this.checkElementCount(e.chatUserMessageText, 0); } // Emojis diff --git a/bigbluebutton-tests/playwright/connectionStatus/connectionStatus.js b/bigbluebutton-tests/playwright/connectionStatus/connectionStatus.js index 0e25947bad..8fd5c820ae 100644 --- a/bigbluebutton-tests/playwright/connectionStatus/connectionStatus.js +++ b/bigbluebutton-tests/playwright/connectionStatus/connectionStatus.js @@ -34,8 +34,7 @@ class ConnectionStatus extends MultiUsers { await this.modPage.hasElement(e.connectionStatusItemEmpty); await this.modPage.page.evaluate(() => window.dispatchEvent(new CustomEvent('socketstats', { detail: { rtt: 2000 } }))); await this.modPage.wasRemoved(e.connectionStatusItemEmpty); - const status = this.modPage.getLocator(e.connectionStatusItemUser); - await expect(status).toHaveCount(1); + await this.modPage.checkElementCount(e.connectionStatusItemUser, 1); } async linkToSettingsTest() { diff --git a/bigbluebutton-tests/playwright/core/constants.js b/bigbluebutton-tests/playwright/core/constants.js index 80102b4c57..715fabd5ba 100644 --- a/bigbluebutton-tests/playwright/core/constants.js +++ b/bigbluebutton-tests/playwright/core/constants.js @@ -3,7 +3,7 @@ const CI = process.env.CI === 'true'; // GLOBAL TESTS VARS exports.ELEMENT_WAIT_TIME = CI ? 10000 : 5000; exports.ELEMENT_WAIT_LONGER_TIME = CI ? 20000 : 10000; -exports.ELEMENT_WAIT_EXTRA_LONG_TIME = 15000; +exports.ELEMENT_WAIT_EXTRA_LONG_TIME = CI ? 30000 : 15000; exports.LOOP_INTERVAL = 1200; exports.USER_LIST_VLIST_BOTS_LISTENING = 50; diff --git a/bigbluebutton-tests/playwright/core/elements.js b/bigbluebutton-tests/playwright/core/elements.js index 27708cce1d..2bef8212c9 100644 --- a/bigbluebutton-tests/playwright/core/elements.js +++ b/bigbluebutton-tests/playwright/core/elements.js @@ -49,6 +49,7 @@ exports.muteMicButton = 'button[data-test="muteMicButton"]'; // Breakout exports.createBreakoutRooms = 'li[data-test="createBreakoutRooms"]'; exports.randomlyAssign = 'button[data-test="randomlyAssign"]'; +exports.resetAssignments = 'button[data-test="resetAssignments"]' exports.breakoutRoomsItem = 'div[data-test="breakoutRoomsItem"]'; exports.alreadyConnected = 'span[data-test="alreadyConnected"]'; exports.askJoinRoom1 = 'button[data-test="askToJoinRoom1"]'; @@ -56,6 +57,31 @@ exports.joinRoom1 = 'button[data-test="joinRoom1"]'; exports.allowChoiceRoom = 'input[id="freeJoinCheckbox"]'; exports.labelGeneratingURL = 'span[data-test="labelGeneratingURL"]'; exports.endBreakoutRoomsButton = 'button[data-test="endBreakoutRoomsButton"]'; +exports.durationTime = 'input[data-test="durationTime"]'; +exports.decreaseBreakoutTime = 'button[data-test="decreaseBreakoutTime"]'; +exports.increaseBreakoutTime = 'button[data-test="increaseBreakoutTime"]'; +exports.selectNumberOfRooms = 'select[id="numberOfRooms"]'; +exports.roomGrid = 'div[data-test="roomGrid"] >> input'; +exports.breakoutBox0 = 'div[id="breakoutBox-0"]'; +exports.breakoutBox1 = 'div[id="breakoutBox-1"]'; +exports.breakoutBox2 = 'div[id="breakoutBox-2"]'; +exports.breakoutOptionsMenu = 'button[data-test="breakoutOptionsMenu"]'; +exports.openUpdateBreakoutUsersModal = 'li[data-test="openUpdateBreakoutUsersModal"]'; +exports.userTest = 'div[id="breakoutBox-0"] >> p:nth-child(2)'; +exports.moveUser = 'div[id="breakoutBox-1"] >> p:nth-child(1)'; +exports.openBreakoutTimeManager = 'li[data-test="openBreakoutTimeManager"]'; +exports.inputSetTimeSelector = 'input[id="inputSetTimeSelector"]'; +exports.sendButtonDurationTime = 'button[data-test="sendButtonDurationTime"]'; +exports.breakoutRemainingTime = 'span[data-test="breakoutRemainingTime"]'; +exports.roomNameInput = 'input[data-test="roomName-1"]'; +exports.roomName1Test = 'span[data-test="Room 1Test"]'; +exports.userNameBreakoutRoom = 'div[data-test="userNameBreakoutRoom-Room 1"]'; +exports.userNameBreakoutRoom2 = 'div[data-test="userNameBreakoutRoom-Room 2"]'; +exports.userNameBreakoutRoom7 = 'div[data-test="userNameBreakoutRoom-Room 7"]'; +exports.endAllBreakouts = 'li[data-test="endAllBreakouts"]'; +exports.breakoutRoomList = 'div[data-test="breakoutRoomList"]'; +exports.warningNoUserAssigned = 'span[data-test="warningNoUserAssigned"]'; +exports.timeRemaining = 'span[data-test="timeRemaining"]'; // Chat exports.chatBox = 'textarea[id="message-input"]'; @@ -78,6 +104,20 @@ exports.typingIndicator = 'span[data-test="typingIndicator"]'; exports.chatUserMessageText = 'p[data-test="chatUserMessageText"]'; exports.chatClearMessageText = 'p[data-test="chatClearMessageText"]'; exports.chatWelcomeMessageText = 'p[data-test="chatWelcomeMessageText"]'; +exports.waitingUsersLobbyMessage = 'div[data-test="lobbyMessage"] >> textarea'; +exports.sendLobbyMessage = 'div[data-test="lobbyMessage"] >> button'; +exports.lobbyMessage = 'div[data-test="lobbyMessage"] >> p'; +exports.positionInWaitingQueue = 'div[id="positionInWaitingQueue"]'; +exports.allowEveryone = 'button[data-test="allowEveryone"]'; +exports.denyEveryone = 'button[data-test="denyEveryone"]'; +exports.guestMessage = 'p[data-test="guestMessage"]'; +exports.privateMessageGuest = 'button[data-test="privateMessageGuest"]'; +exports.acceptGuest = 'button[data-test="acceptGuest"]'; +exports.denyGuest = 'button[data-test="denyGuest"]'; +exports.inputPrivateLobbyMesssage = 'div[data-test="privateLobbyMessage"] >> textarea'; +exports.sendPrivateLobbyMessage = 'div[data-test="privateLobbyMessage"] >> button'; +exports.rememberCheckboxId = 'input[id="rememberCheckboxId"]'; +exports.welcomeMessage = 'h1[id="welcome-message"]'; // Emoji picker exports.emojiPickerButton = 'button[data-test="emojiPickerButton"]'; exports.frequentlyUsedEmoji = '👍'; diff --git a/bigbluebutton-tests/playwright/core/page.js b/bigbluebutton-tests/playwright/core/page.js index 285eed3920..d2c390106f 100644 --- a/bigbluebutton-tests/playwright/core/page.js +++ b/bigbluebutton-tests/playwright/core/page.js @@ -210,6 +210,20 @@ class Page { async up(key) { await this.page.keyboard.up(key); } + + async dragDropSelector(selector, position) { + await this.page.locator(selector).dragTo(this.page.locator(position)); + } + + async checkElementCount(selector, count) { + const locator = await this.page.locator(selector); + await expect(locator).toHaveCount(count); + } + + async hasValue(selector, value) { + const locator = await this.page.locator(selector); + await expect(locator).toHaveValue(value); + } } module.exports = exports = Page; diff --git a/bigbluebutton-tests/playwright/user/guestPolicy.js b/bigbluebutton-tests/playwright/user/guestPolicy.js index eec862035f..c181245016 100644 --- a/bigbluebutton-tests/playwright/user/guestPolicy.js +++ b/bigbluebutton-tests/playwright/user/guestPolicy.js @@ -2,17 +2,97 @@ const { MultiUsers } = require("./multiusers"); const e = require('../core/elements'); const { sleep } = require('../core/helpers'); const { setGuestPolicyOption } = require("./util"); +const { ELEMENT_WAIT_LONGER_TIME } = require("../core/constants"); class GuestPolicy extends MultiUsers { constructor(browser, context) { super(browser, context); } - async askModerator() { + async messageToGuestLobby() { await setGuestPolicyOption(this.modPage, e.askModerator); await sleep(500); await this.initUserPage(false); await this.modPage.hasElement(e.waitingUsersBtn); + + await this.modPage.waitAndClick(e.waitingUsersBtn); + await this.modPage.type(e.waitingUsersLobbyMessage, 'test'); + await this.modPage.waitAndClick(e.sendLobbyMessage); + await this.modPage.hasText(e.lobbyMessage, /test/); + } + + async allowEveryone() { + await setGuestPolicyOption(this.modPage, e.askModerator); + await sleep(500); + await this.initUserPage(false); + await this.userPage.hasText(e.guestMessage, /wait/); + await this.userPage.hasText(e.positionInWaitingQueue, /first/); + await this.modPage.waitAndClick(e.waitingUsersBtn); + await this.modPage.waitAndClick(e.allowEveryone); + + await this.userPage.hasText(e.guestMessage, /approved/, ELEMENT_WAIT_LONGER_TIME); + await this.modPage.hasElement(e.viewerAvatar, ELEMENT_WAIT_LONGER_TIME); + await this.userPage.hasElement(e.audioModal); + + } + + async denyEveryone() { + await setGuestPolicyOption(this.modPage, e.askModerator); + await sleep(500); + await this.initUserPage(false); + await this.modPage.waitAndClick(e.waitingUsersBtn); + await this.modPage.waitAndClick(e.denyEveryone); + + await this.userPage.hasText(e.guestMessage, /denied/, ELEMENT_WAIT_LONGER_TIME); + } + + async rememberChoice() { + await setGuestPolicyOption(this.modPage, e.askModerator); + await sleep(500); + await this.modPage.waitAndClick(e.waitingUsersBtn); + + await this.modPage.waitAndClick(e.rememberCheckboxId); + await this.modPage.hasElementEnabled(e.rememberCheckboxId); + await this.modPage.waitAndClick(e.denyEveryone); + + await this.initUserPage(false); + await this.userPage.hasElement(e.welcomeMessage); + } + + async messageToSpecificUser() { + await setGuestPolicyOption(this.modPage, e.askModerator); + await sleep(500); + await this.initUserPage(false); + await this.modPage.waitAndClick(e.waitingUsersBtn); + + await this.modPage.waitAndClick(e.privateMessageGuest); + await this.modPage.type(e.inputPrivateLobbyMesssage, 'test'); + await this.modPage.waitAndClick(e.sendPrivateLobbyMessage); + await this.userPage.hasText(e.guestMessage, /test/, ELEMENT_WAIT_LONGER_TIME); + } + + async acceptSpecificUser() { + await setGuestPolicyOption(this.modPage, e.askModerator); + await sleep(500); + await this.initUserPage(false); + await this.userPage.hasText(e.guestMessage, /wait/); + await this.userPage.hasText(e.positionInWaitingQueue, /first/); + await this.modPage.waitAndClick(e.waitingUsersBtn); + await this.modPage.waitAndClick(e.acceptGuest); + await this.userPage.hasText(e.guestMessage, /approved/, ELEMENT_WAIT_LONGER_TIME); + + await this.modPage.waitForSelector(e.viewerAvatar, ELEMENT_WAIT_LONGER_TIME); + await this.userPage.hasElement(e.audioModal); + } + + async denySpecificUser() { + await setGuestPolicyOption(this.modPage, e.askModerator); + await sleep(500); + await this.initUserPage(false); + await this.modPage.waitAndClick(e.waitingUsersBtn); + + await this.modPage.waitAndClick(e.denyGuest); + await this.userPage.hasText(e.guestMessage, /denied/, ELEMENT_WAIT_LONGER_TIME); } async alwaysAccept() { @@ -27,7 +107,7 @@ class GuestPolicy extends MultiUsers { await setGuestPolicyOption(this.modPage, e.alwaysDeny); await sleep(1500); await this.initUserPage(false); - await this.userPage.hasElement(e.joinMeetingDemoPage); + await this.userPage.hasElement(e.welcomeMessage); } } diff --git a/bigbluebutton-tests/playwright/user/lockViewers.js b/bigbluebutton-tests/playwright/user/lockViewers.js index 2688d9dab8..7e43fbabc1 100644 --- a/bigbluebutton-tests/playwright/user/lockViewers.js +++ b/bigbluebutton-tests/playwright/user/lockViewers.js @@ -72,8 +72,7 @@ class LockViewers extends MultiUsers { await this.modPage.type(e.chatBox, e.message); await this.modPage.waitAndClick(e.sendButton); await this.userPage.waitForSelector(e.chatUserMessageText); - const messagesCount = this.userPage.getLocator(e.chatUserMessageText); - await expect(messagesCount).toHaveCount(1); + await this.userPage.checkElementCount(e.chatUserMessageText, 1); } async lockSendPrivateChatMessages() { diff --git a/bigbluebutton-tests/playwright/user/multiusers.js b/bigbluebutton-tests/playwright/user/multiusers.js index 1af8b0f715..a681d89b80 100644 --- a/bigbluebutton-tests/playwright/user/multiusers.js +++ b/bigbluebutton-tests/playwright/user/multiusers.js @@ -80,14 +80,10 @@ class MultiUsers { } async userPresence() { - const firstUserOnModPage = this.modPage.getLocator(e.currentUser); - const secondUserOnModPage = this.modPage.getLocator(e.userListItem); - const firstUserOnUserPage = this.userPage.getLocator(e.currentUser); - const secondUserOnUserPage = this.userPage.getLocator(e.userListItem); - await expect(firstUserOnModPage).toHaveCount(1); - await expect(secondUserOnModPage).toHaveCount(1); - await expect(firstUserOnUserPage).toHaveCount(1); - await expect(secondUserOnUserPage).toHaveCount(1); + await this.modPage.checkElementCount(e.currentUser, 1); + await this.modPage.checkElementCount(e.userListItem, 1); + await this.userPage.checkElementCount(e.currentUser, 1); + await this.userPage.checkElementCount(e.userListItem, 1); } async makePresenter() { diff --git a/bigbluebutton-tests/playwright/user/user.spec.js b/bigbluebutton-tests/playwright/user/user.spec.js index e223e6f230..b529eae095 100644 --- a/bigbluebutton-tests/playwright/user/user.spec.js +++ b/bigbluebutton-tests/playwright/user/user.spec.js @@ -68,10 +68,49 @@ test.describe.parallel('User', () => { test.describe.parallel('Manage', () => { test.describe.parallel('Guest policy', () => { - test('ASK_MODERATOR', async ({ browser, context, page }) => { - const guestPolicy = new GuestPolicy(browser, context); - await guestPolicy.initModPage(page); - await guestPolicy.askModerator(); + test.describe.parallel('ASK_MODERATOR', () => { + // https://docs.bigbluebutton.org/2.6/release-tests.html#ask-moderator + test('Message to guest lobby', async ({ browser, context, page }) => { + const guestPolicy = new GuestPolicy(browser, context); + await guestPolicy.initModPage(page); + await guestPolicy.messageToGuestLobby(); + }); + test('Allow Everyone', async ({ browser, context, page }) => { + const guestPolicy = new GuestPolicy(browser, context); + await guestPolicy.initModPage(page); + await guestPolicy.allowEveryone(); + }); + test('Deny Everyone', async ({ browser, context, page }) => { + const guestPolicy = new GuestPolicy(browser, context); + await guestPolicy.initModPage(page); + await guestPolicy.denyEveryone(); + }); + + test('Remember choice', async ({ browser, context, page }) => { + const guestPolicy = new GuestPolicy(browser, context); + await guestPolicy.initModPage(page); + await guestPolicy.rememberChoice(); + }); + + test.describe.parallel('Actions to specific pending user', () => { + test('Message', async ({ browser, context, page }) => { + const guestPolicy = new GuestPolicy(browser, context); + await guestPolicy.initModPage(page); + await guestPolicy.messageToSpecificUser(); + }); + + test('Accept', async ({ browser, context, page }) => { + const guestPolicy = new GuestPolicy(browser, context); + await guestPolicy.initModPage(page); + await guestPolicy.acceptSpecificUser(); + }); + + test('Deny', async ({ browser, context, page }) => { + const guestPolicy = new GuestPolicy(browser, context); + await guestPolicy.initModPage(page); + await guestPolicy.denySpecificUser(); + }); + }); }); test('ALWAYS_ACCEPT', async ({ browser, context, page }) => { @@ -79,7 +118,7 @@ test.describe.parallel('User', () => { await guestPolicy.initModPage(page); await guestPolicy.alwaysAccept(); }); - + // https://docs.bigbluebutton.org/2.6/release-tests.html#always-deny test('ALWAYS_DENY', async ({ browser, context, page }) => { const guestPolicy = new GuestPolicy(browser, context); await guestPolicy.initModPage(page); diff --git a/bigbluebutton-web/build.gradle b/bigbluebutton-web/build.gradle index aa4f4832d1..2861af0cde 100755 --- a/bigbluebutton-web/build.gradle +++ b/bigbluebutton-web/build.gradle @@ -1,6 +1,6 @@ buildscript { repositories { - jcenter() + mavenCentral() mavenLocal() maven { url "https://repo1.maven.org/maven2" } maven { url "https://repo.grails.org/artifactory/core" } @@ -45,7 +45,7 @@ task copyWebInf(type: Copy) { processResources.dependsOn copyWebInf repositories { - jcenter() + mavenCentral() mavenLocal() maven { url "https://repo1.maven.org/maven2" } maven { url "https://repo.grails.org/artifactory/core" } @@ -87,20 +87,20 @@ dependencies { //--- BigBlueButton Dependencies Start - Transitive dependencies have to be re-defined below implementation "org.bigbluebutton:bbb-common-message_2.13:0.0.21-SNAPSHOT" implementation "org.bigbluebutton:bbb-common-web:0.0.3-SNAPSHOT" - implementation "io.lettuce:lettuce-core:6.1.5.RELEASE" + implementation "io.lettuce:lettuce-core:6.1.9.RELEASE" implementation "org.reactivestreams:reactive-streams:1.0.3" implementation "io.projectreactor:reactor-core:3.4.12" implementation "org.freemarker:freemarker:2.3.31" implementation "com.google.code.gson:gson:2.8.9" implementation "org.json:json:20211205" - implementation "com.zaxxer:nuprocess:2.0.2" + implementation "com.zaxxer:nuprocess:2.0.5" implementation "net.java.dev.jna:jna:5.10.0" // https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload implementation group: 'commons-fileupload', name: 'commons-fileupload', version: '1.4' implementation 'javax.validation:validation-api:2.0.1.Final' implementation "org.springframework.boot:spring-boot-starter-validation:${springVersion}" - implementation 'org.postgresql:postgresql:42.2.16' + implementation 'org.postgresql:postgresql:42.4.1' implementation 'org.hibernate:hibernate-core:5.6.1.Final' //--- BigBlueButton Dependencies End @@ -110,9 +110,9 @@ dependencies { testImplementation "org.grails:grails-gorm-testing-support" testImplementation "org.grails.plugins:geb" testImplementation "org.grails:grails-web-testing-support" - testRuntimeOnly "org.seleniumhq.selenium:selenium-chrome-driver:2.47.1" + testRuntimeOnly "org.seleniumhq.selenium:selenium-chrome-driver:4.0.0" testRuntimeOnly "org.seleniumhq.selenium:selenium-htmlunit-driver:2.47.1" - testRuntimeOnly "net.sourceforge.htmlunit:htmlunit:2.18" + testRuntimeOnly "net.sourceforge.htmlunit:htmlunit:2.63.0" testImplementation "com.github.javafaker:javafaker:0.12" } diff --git a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties old mode 100755 new mode 100644 index 2893ee83ef..b0a2f4cf12 --- a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties +++ b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties @@ -211,6 +211,12 @@ defaultWelcomeMessageFooter=This server is running + @@ -136,6 +137,7 @@ with BigBlueButton; if not, see . + @@ -188,7 +190,8 @@ with BigBlueButton; if not, see . - + + diff --git a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy index 46d7d89f86..7a0002d83b 100755 --- a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy +++ b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy @@ -924,6 +924,8 @@ class ApiController { breakoutRooms { record meeting.breakoutRoomsParams.record privateChatEnabled meeting.breakoutRoomsParams.privateChatEnabled + captureNotes meeting.breakoutRoomsParams.captureNotes + captureSlides meeting.breakoutRoomsParams.captureSlides } } customdata ( @@ -1295,7 +1297,11 @@ class ApiController { def Boolean isDownloadable = false; if (document.name != null && "default".equals(document.name)) { - downloadAndProcessDocument(presentationService.defaultUploadedPresentation, conf.getInternalId(), document.current /* default presentation */, '', false, true); + if(presentationService.defaultUploadedPresentation){ + downloadAndProcessDocument(presentationService.defaultUploadedPresentation, conf.getInternalId(), document.current /* default presentation */, '', false, true); + } else { + log.error "Default presentation could not be read, it is (" + presentationService.defaultUploadedPresentation + ")", "error" + } } else{ // Extracting all properties inside the xml if (!StringUtils.isEmpty(document.@removable.toString())) { @@ -1411,9 +1417,8 @@ class ApiController { def presId if (presFilename == "" || filenameExt == "") { - log.debug("Upload failed. Invalid filename " + presOrigFilename) - uploadFailReasons.add("invalid_filename") - uploadFailed = true + log.debug("presentation is null by default") + return } else { String presentationDir = presentationService.getPresentationDir() presId = Util.generatePresentationId(presFilename) @@ -1422,13 +1427,13 @@ class ApiController { def newFilename = Util.createNewFilename(presId, filenameExt) def newFilePath = uploadDir.absolutePath + File.separatorChar + newFilename - if (presDownloadService.savePresentation(meetingId, newFilePath, address)) { - pres = new File(newFilePath) - } else { + if(presDownloadService.savePresentation(meetingId, newFilePath, address)) pres = new File(newFilePath) + else { log.error("Failed to download presentation=[${address}], meeting=[${meetingId}], fileName=[${fileName}]") uploadFailReasons.add("failed_to_download_file") - uploadFailed = true + uploadFailed = true } + } else { log.error("Null presentation directory meeting=[${meetingId}], presentationDir=[${presentationDir}], presId=[${presId}]") uploadFailReasons.add("null_presentation_dir") @@ -1582,16 +1587,18 @@ class ApiController { Boolean rejoin = meeting.getUserById(us.internalUserId) != null; // Users that passed enter once, still not joined but somehow re-entered Boolean reenter = meeting.getEnteredUserById(us.internalUserId) != null; + // User are able to rejoin if he already joined previously with the same extId + Boolean userExtIdAlreadyJoined = meeting.getUsersWithExtId(us.externUserID).size() > 0 // Users that already joined the meeting - int joinedUsers = meeting.getUsers().size() + // It will count only unique users in order to avoid the same user from filling all slots + int joinedUniqueUsers = meeting.countUniqueExtIds() // Users that are entering the meeting int enteredUsers = meeting.getEnteredUsers().size() - log.info("Joined users - ${joinedUsers}") - log.info("Entered users - ${enteredUsers}") + log.info("Entered users - ${enteredUsers}. Joined users - ${joinedUniqueUsers}") - Boolean reachedMax = joinedUsers >= maxParticipants; - if (enabled && !rejoin && !reenter && reachedMax) { + Boolean reachedMax = joinedUniqueUsers >= maxParticipants; + if (enabled && !rejoin && !reenter && !userExtIdAlreadyJoined && reachedMax) { return true; } diff --git a/bigbluebutton-web/pres-checker/build.gradle b/bigbluebutton-web/pres-checker/build.gradle index eca2216635..f19440276f 100755 --- a/bigbluebutton-web/pres-checker/build.gradle +++ b/bigbluebutton-web/pres-checker/build.gradle @@ -15,7 +15,7 @@ task resolveDeps(type: Copy) { } repositories { - jcenter() + mavenCentral() mavenLocal() } diff --git a/build/packages-template/bbb-config/after-install.sh b/build/packages-template/bbb-config/after-install.sh index bb5cea48df..c1370e276f 100644 --- a/build/packages-template/bbb-config/after-install.sh +++ b/build/packages-template/bbb-config/after-install.sh @@ -130,7 +130,7 @@ if [ -d /var/mediasoup/screenshare ]; then fi sed -i 's/worker_connections 768/worker_connections 4000/g' /etc/nginx/nginx.conf - +echo 'limit_conn_zone $uri zone=ws_zone:5m;' > /etc/nginx/conf.d/html5-conn-limit.conf if grep -q "worker_rlimit_nofile" /etc/nginx/nginx.conf; then num=$(grep worker_rlimit_nofile /etc/nginx/nginx.conf | grep -o '[0-9]*') if [[ "$num" -lt 10000 ]]; then diff --git a/build/packages-template/bbb-freeswitch-core/audio.patch b/build/packages-template/bbb-freeswitch-core/audio.patch index 3424ee3ff5..32588d6749 100644 --- a/build/packages-template/bbb-freeswitch-core/audio.patch +++ b/build/packages-template/bbb-freeswitch-core/audio.patch @@ -1,13 +1,14 @@ --- src/mod/applications/mod_conference/mod_conference.c +++ src/mod/applications/mod_conference/mod_conference.c -@@ -2477,9 +2477,7 @@ SWITCH_STANDARD_APP(conference_function) +@@ -2476,9 +2476,7 @@ SWITCH_STANDARD_APP(conference_function) /* Run the conference loop */ do { - switch_media_flow_t audio_flow = switch_core_session_media_flow(session, SWITCH_MEDIA_TYPE_AUDIO); - -- if (switch_channel_test_flag(channel, CF_AUDIO) && (audio_flow == SWITCH_MEDIA_FLOW_SENDRECV || audio_flow == SWITCH_MEDIA_FLOW_RECVONLY)) { +- if (switch_channel_test_flag(channel, CF_AUDIO) && (audio_flow == SWITCH_MEDIA_FLOW_SENDRECV || audio_flow == SWITCH_MEDIA_FLOW_SENDONLY)) { + if (switch_channel_test_flag(channel, CF_AUDIO)) { conference_loop_output(&member); } else { - if (conference_utils_member_test_flag((&member), MFLAG_RUNNING) && switch_channel_ready(channel)) { + if (!conference_utils_member_test_flag(&member, MFLAG_ITHREAD)) { + diff --git a/build/packages-template/bbb-html5/bbb-html5.nginx b/build/packages-template/bbb-html5/bbb-html5.nginx index 82ff916813..45b03f982c 100644 --- a/build/packages-template/bbb-html5/bbb-html5.nginx +++ b/build/packages-template/bbb-html5/bbb-html5.nginx @@ -4,6 +4,7 @@ location @html5client { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; + limit_conn ws_zone 3; } location /html5client/locales { diff --git a/freeswitch.placeholder.sh b/freeswitch.placeholder.sh index 77d4115d90..cbc2e46488 100755 --- a/freeswitch.placeholder.sh +++ b/freeswitch.placeholder.sh @@ -2,5 +2,5 @@ mkdir freeswitch cd freeswitch git init git remote add origin https://github.com/signalwire/freeswitch.git -git fetch --depth 1 origin v1.10.7 +git fetch --depth 1 origin v1.10.8 git checkout FETCH_HEAD diff --git a/record-and-playback/presentation/scripts/publish/presentation.rb b/record-and-playback/presentation/scripts/publish/presentation.rb index 7625714565..1767ddaadc 100755 --- a/record-and-playback/presentation/scripts/publish/presentation.rb +++ b/record-and-playback/presentation/scripts/publish/presentation.rb @@ -31,6 +31,7 @@ require 'yaml' require 'builder' require 'fastimage' # require fastimage to get the image size of the slides (gem install fastimage) require 'json' +require "active_support" # This script lives in scripts/archive/steps while properties.yaml lives in scripts/ bbb_props = BigBlueButton.read_props @@ -607,12 +608,13 @@ def events_parse_tldraw_shape(shapes, event, current_presentation, current_slide prev_shape = nil if shape_id # If we have a shape ID, look up the previous shape by ID - prev_shape_pos = shapes.rindex { |s| s[:shade_id] == shape_id } + prev_shape_pos = shapes.rindex { |s| s[:id] == shape_id } prev_shape = prev_shape_pos ? shapes[prev_shape_pos] : nil end if prev_shape prev_shape[:out] = timestamp shape[:shape_unique_id] = prev_shape[:shape_unique_id] + shape[:shape_data] = prev_shape[:shape_data].deep_merge(shape[:shape_data]) else shape[:shape_unique_id] = @svg_shape_unique_id @svg_shape_unique_id += 1
- { getUserAnswer(user, poll).map((answer) =>

{answer}

) } + { 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

{answer}

; + }) } { poll.anonymous ? ( - - - - - - - - diff --git a/bbb-webrtc-sfu.placeholder.sh b/bbb-webrtc-sfu.placeholder.sh index f7867a1094..0e125e3f78 100755 --- a/bbb-webrtc-sfu.placeholder.sh +++ b/bbb-webrtc-sfu.placeholder.sh @@ -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 diff --git a/bigbluebutton-config/bin/bbb-conf b/bigbluebutton-config/bin/bbb-conf index eea01fbc5c..9ce130fd38 100755 --- a/bigbluebutton-config/bin/bbb-conf +++ b/bigbluebutton-config/bin/bbb-conf @@ -1249,19 +1249,34 @@ check_state() { if [ ! -z "$STUN" ]; then for i in $STUN; do STUN_SERVER="$(xmlstarlet sel -N x="http://www.springframework.org/schema/beans" -t -m "_:beans/_:bean[@id=\"$i\"]/_:constructor-arg[@index=\"0\"]" -v @value $TURN | sed 's/stun://g')" - if echo $STUN_SERVER | grep -q ':'; then - STUN_SERVER="$(echo $STUN_SERVER | sed 's/:.*//g') $(echo $STUN_SERVER | sed 's/.*://g')" - else - STUN_SERVER="$STUN_SERVER 3478" - fi - if which stunclient > /dev/null 2>&1; then - if stunclient --mode full --localport 30000 $STUN_SERVER | grep -q "fail\|Unable\ to\ resolve"; then + # stun is from the stun-client package, which is available on both bionic and focal + # stunclient is from the stuntman-client package, which is available on bionic but was removed from focal + if which stun > /dev/null 2>&1; then + # stun return codes, from its client.cxx + # low nibble: open (0), various STUN combinations (2-9), firewall (a), blocked (c), unknown (e), error (f) + # high nibble: hairpin (1) + stun $STUN_SERVER > /dev/null + if (( ($? & 0xf) > 9 )); then echo echo "#" echo "# Warning: Failed to verify STUN server at $STUN_SERVER with command" echo "#" - echo "# stunclient --mode full --localport 30000 $STUN_SERVER" + echo "# stun $STUN_SERVER" + echo "#" + fi + elif which stunclient > /dev/null 2>&1; then + if echo $STUN_SERVER | grep -q ':'; then + STUN_SERVER="$(echo $STUN_SERVER | sed 's/:.*//g') $(echo $STUN_SERVER | sed 's/.*://g')" + else + STUN_SERVER="$STUN_SERVER 3478" + fi + if stunclient $STUN_SERVER | grep -q "fail\|Unable\ to\ resolve"; then + echo + echo "#" + echo "# Warning: Failed to verify STUN server at $STUN_SERVER with command" + echo "#" + echo "# stunclient $STUN_SERVER" echo "#" fi fi diff --git a/bigbluebutton-html5/imports/api/annotations/addAnnotation.js b/bigbluebutton-html5/imports/api/annotations/addAnnotation.js index 6bdb8ea30c..d95a93b3a5 100755 --- a/bigbluebutton-html5/imports/api/annotations/addAnnotation.js +++ b/bigbluebutton-html5/imports/api/annotations/addAnnotation.js @@ -1,20 +1,27 @@ import { check } from 'meteor/check'; +import _ from "lodash"; -export default function addAnnotation(meetingId, whiteboardId, userId, annotation) { +export default function addAnnotation(meetingId, whiteboardId, userId, annotation, Annotations) { check(meetingId, String); check(whiteboardId, String); check(annotation, Object); const { - id, annotationInfo, wbId, + id, wbId, } = annotation; + let { annotationInfo } = annotation; + const selector = { meetingId, id, - userId, }; + const oldAnnotation = Annotations.findOne(selector); + if (oldAnnotation) { + annotationInfo = _.merge(oldAnnotation.annotationInfo, annotationInfo) + } + const modifier = { $set: { whiteboardId, diff --git a/bigbluebutton-html5/imports/api/annotations/server/modifiers/addAnnotation.js b/bigbluebutton-html5/imports/api/annotations/server/modifiers/addAnnotation.js index 4503da40b7..77b1205f4c 100755 --- a/bigbluebutton-html5/imports/api/annotations/server/modifiers/addAnnotation.js +++ b/bigbluebutton-html5/imports/api/annotations/server/modifiers/addAnnotation.js @@ -8,7 +8,7 @@ export default function addAnnotation(meetingId, whiteboardId, userId, annotatio check(whiteboardId, String); check(annotation, Object); - const query = addAnnotationQuery(meetingId, whiteboardId, userId, annotation); + const query = addAnnotationQuery(meetingId, whiteboardId, userId, annotation, Annotations); try { const { insertedId } = Annotations.upsert(query.selector, query.modifier); diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/service.js b/bigbluebutton-html5/imports/api/audio/client/bridge/service.js index d2c2ce443d..d795db4393 100644 --- a/bigbluebutton-html5/imports/api/audio/client/bridge/service.js +++ b/bigbluebutton-html5/imports/api/audio/client/bridge/service.js @@ -1,6 +1,6 @@ import Settings from '/imports/ui/services/settings'; import logger from '/imports/startup/client/logger'; -import Storage from '/imports/ui/services/storage/session'; +import BBBStorage from '/imports/ui/services/storage'; const AUDIO_SESSION_NUM_KEY = 'AudioSessionNumber'; const DEFAULT_INPUT_DEVICE_ID = ''; @@ -38,10 +38,10 @@ const getCurrentAudioSinkId = () => { return audioElement?.sinkId || DEFAULT_OUTPUT_DEVICE_ID; }; -const getStoredAudioInputDeviceId = () => Storage.getItem(INPUT_DEVICE_ID_KEY); -const getStoredAudioOutputDeviceId = () => Storage.getItem(OUTPUT_DEVICE_ID_KEY); -const storeAudioInputDeviceId = (deviceId) => Storage.setItem(INPUT_DEVICE_ID_KEY, deviceId); -const storeAudioOutputDeviceId = (deviceId) => Storage.setItem(OUTPUT_DEVICE_ID_KEY, deviceId); +const getStoredAudioInputDeviceId = () => BBBStorage.getItem(INPUT_DEVICE_ID_KEY); +const getStoredAudioOutputDeviceId = () => BBBStorage.getItem(OUTPUT_DEVICE_ID_KEY); +const storeAudioInputDeviceId = (deviceId) => BBBStorage.setItem(INPUT_DEVICE_ID_KEY, deviceId); +const storeAudioOutputDeviceId = (deviceId) => BBBStorage.setItem(OUTPUT_DEVICE_ID_KEY, deviceId); /** * Filter constraints set in audioDeviceConstraints, based on diff --git a/bigbluebutton-html5/imports/api/breakouts/server/methods/createBreakout.js b/bigbluebutton-html5/imports/api/breakouts/server/methods/createBreakout.js index 02694ca627..635c73d2cb 100644 --- a/bigbluebutton-html5/imports/api/breakouts/server/methods/createBreakout.js +++ b/bigbluebutton-html5/imports/api/breakouts/server/methods/createBreakout.js @@ -4,7 +4,7 @@ import Logger from '/imports/startup/server/logger'; import { extractCredentials } from '/imports/api/common/server/helpers'; import { check } from 'meteor/check'; -export default function createBreakoutRoom(rooms, durationInMinutes, record = false) { +export default function createBreakoutRoom(rooms, durationInMinutes, record = false, captureNotes = false, captureSlides = false) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const BREAKOUT_LIM = Meteor.settings.public.app.breakouts.breakoutRoomLimit; @@ -24,6 +24,8 @@ export default function createBreakoutRoom(rooms, durationInMinutes, record = fa } const payload = { record, + captureNotes, + captureSlides, durationInMinutes, rooms, meetingId, diff --git a/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js index 07f3fd339f..42827a17fc 100755 --- a/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js +++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js @@ -74,6 +74,8 @@ export default function addMeeting(meeting) { parentId: String, record: Boolean, privateChatEnabled: Boolean, + captureNotes: Boolean, + captureSlides: Boolean, }, meetingProp: { intId: String, @@ -88,11 +90,12 @@ export default function addMeeting(meeting) { uploadExternalUrl: String, }, usersProp: { + maxUsers: Number, + maxUserConcurrentAccesses: Number, webcamsOnlyForModerator: Boolean, userCameraCap: Number, guestPolicy: String, authenticatedGuest: Boolean, - maxUsers: Number, allowModsToUnmuteUsers: Boolean, allowModsToEjectCameras: Boolean, meetingLayout: String, diff --git a/bigbluebutton-html5/imports/api/pads/server/eventHandlers.js b/bigbluebutton-html5/imports/api/pads/server/eventHandlers.js index fb14a79f99..482ab80c0a 100644 --- a/bigbluebutton-html5/imports/api/pads/server/eventHandlers.js +++ b/bigbluebutton-html5/imports/api/pads/server/eventHandlers.js @@ -6,6 +6,7 @@ import padUpdated from './handlers/padUpdated'; import padContent from './handlers/padContent'; import padTail from './handlers/padTail'; import sessionDeleted from './handlers/sessionDeleted'; +import captureSharedNotes from './handlers/captureSharedNotes'; RedisPubSub.on('PadGroupCreatedRespMsg', groupCreated); RedisPubSub.on('PadCreatedRespMsg', padCreated); @@ -14,3 +15,4 @@ RedisPubSub.on('PadUpdatedEvtMsg', padUpdated); RedisPubSub.on('PadContentEvtMsg', padContent); RedisPubSub.on('PadTailEvtMsg', padTail); RedisPubSub.on('PadSessionDeletedEvtMsg', sessionDeleted); +RedisPubSub.on('CaptureSharedNotesReqEvtMsg', captureSharedNotes); diff --git a/bigbluebutton-html5/imports/api/pads/server/handlers/captureSharedNotes.js b/bigbluebutton-html5/imports/api/pads/server/handlers/captureSharedNotes.js new file mode 100644 index 0000000000..ff94033cee --- /dev/null +++ b/bigbluebutton-html5/imports/api/pads/server/handlers/captureSharedNotes.js @@ -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); +} diff --git a/bigbluebutton-html5/imports/api/pads/server/methods/padCapture.js b/bigbluebutton-html5/imports/api/pads/server/methods/padCapture.js new file mode 100644 index 0000000000..0723405543 --- /dev/null +++ b/bigbluebutton-html5/imports/api/pads/server/methods/padCapture.js @@ -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; + } +} diff --git a/bigbluebutton-html5/imports/startup/client/base.jsx b/bigbluebutton-html5/imports/startup/client/base.jsx index 9ca8112140..394936901f 100755 --- a/bigbluebutton-html5/imports/startup/client/base.jsx +++ b/bigbluebutton-html5/imports/startup/client/base.jsx @@ -256,21 +256,12 @@ class Base extends Component { meetingEndedReason, meetingIsBreakout, subscriptionsReady, - User, } = this.props; if ((loading || !subscriptionsReady) && !meetingHasEnded && meetingExist) { return ({loading}); } - if (meetingIsBreakout && (ejected || userRemoved)) { - Base.setExitReason('removedFromBreakout').finally(() => { - Meteor.disconnect(); - window.close(); - }); - return null; - } - if (ejected) { return ( { + if (meetingHasEnded && meetingIsBreakout) { + Base.setExitReason('breakoutEnded').finally(() => { Meteor.disconnect(); window.close(); }); return null; } - if ((meetingHasEnded && !meetingIsBreakout) || (codeError && User?.loggedOut)) { + if (meetingHasEnded && !meetingIsBreakout) { return ( Base.setExitReason('meetingEnded')} /> ); diff --git a/bigbluebutton-html5/imports/startup/client/intl.jsx b/bigbluebutton-html5/imports/startup/client/intl.jsx index 25a7bb4364..ad8087c5b9 100644 --- a/bigbluebutton-html5/imports/startup/client/intl.jsx +++ b/bigbluebutton-html5/imports/startup/client/intl.jsx @@ -9,6 +9,7 @@ import _ from 'lodash'; import { Session } from 'meteor/session'; import Logger from '/imports/startup/client/logger'; import { formatLocaleCode } from '/imports/utils/string-utils'; +import Intl from '/imports/ui/services/locale'; const propTypes = { locale: PropTypes.string, @@ -66,6 +67,7 @@ class IntlStartup extends Component { const url = `./locale?locale=${locale}&init=${init}`; const localesPath = 'locales'; + Intl.fetching = true; this.setState({ fetching: true }, () => { fetch(url) .then((response) => { @@ -138,6 +140,7 @@ class IntlStartup extends Component { const dasherizedLocale = normalizedLocale.replace('_', '-'); const { language, formattedLocale } = formatLocaleCode(dasherizedLocale); + Intl.setLocale(formattedLocale, mergedMessages); this.setState({ messages: mergedMessages, fetching: false, normalizedLocale: dasherizedLocale }, () => { Settings.application.locale = dasherizedLocale; diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx index 5aae817b71..822a529edb 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx @@ -318,7 +318,7 @@ class ActionsDropdown extends PureComponent { } actions={children} opts={{ - id: "default-dropdown-menu", + id: "actions-dropdown-menu", keepMounted: true, transitionDuration: 0, elevation: 3, diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx old mode 100755 new mode 100644 index c91ce95cd3..a3e688362e --- a/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx @@ -10,6 +10,8 @@ import { withModalMounter } from '/imports/ui/components/common/modal/service'; import SortList from './sort-user-list/component'; import Styled from './styles'; import Icon from '/imports/ui/components/common/icon/component.jsx'; +import { isImportSharedNotesFromBreakoutRoomsEnabled, isImportPresentationWithAnnotationsFromBreakoutRoomsEnabled } from '/imports/ui/services/features'; +import { addNewAlert } from '/imports/ui/components/screenreader-alert/service'; const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator; @@ -82,6 +84,14 @@ const intlMessages = defineMessages({ id: 'app.createBreakoutRoom.freeJoin', description: 'free join label', }, + captureNotesLabel: { + id: 'app.createBreakoutRoom.captureNotes', + description: 'capture shared notes label', + }, + captureSlidesLabel: { + id: 'app.createBreakoutRoom.captureSlides', + description: 'capture slides label', + }, roomLabel: { id: 'app.createBreakoutRoom.room', description: 'Room label', @@ -150,6 +160,10 @@ const intlMessages = defineMessages({ id: 'app.createBreakoutRoom.roomNameInputDesc', description: 'aria description for room name change', }, + movedUserLabel: { + id: 'app.createBreakoutRoom.movedUserLabel', + description: 'screen reader alert when users are moved to rooms', + } }); const BREAKOUT_LIM = Meteor.settings.public.app.breakouts.breakoutRoomLimit; @@ -200,6 +214,8 @@ class BreakoutRoom extends PureComponent { this.handleDismiss = this.handleDismiss.bind(this); this.setInvitationConfig = this.setInvitationConfig.bind(this); this.setRecord = this.setRecord.bind(this); + this.setCaptureNotes = this.setCaptureNotes.bind(this); + this.setCaptureSlides = this.setCaptureSlides.bind(this); this.blurDurationTime = this.blurDurationTime.bind(this); this.removeRoomUsers = this.removeRoomUsers.bind(this); this.renderErrorMessages = this.renderErrorMessages.bind(this); @@ -220,6 +236,8 @@ class BreakoutRoom extends PureComponent { roomNameDuplicatedIsValid: true, roomNameEmptyIsValid: true, record: false, + captureNotes: false, + captureSlides: false, durationIsValid: true, breakoutJoinedUsers: null, }; @@ -310,6 +328,7 @@ class BreakoutRoom extends PureComponent { users.forEach((u, index) => { if (`roomUserItem-${u.userId}` === document.activeElement.id) { users[index].room = text.substr(text.length - 1).includes(')') ? 0 : parseInt(roomNumber, 10); + this.changeUserRoom(u.userId, users[index].room); } }); } @@ -386,6 +405,8 @@ class BreakoutRoom extends PureComponent { users, freeJoin, record, + captureNotes, + captureSlides, numberOfRoomsIsValid, numberOfRooms, durationTime, @@ -430,7 +451,7 @@ class BreakoutRoom extends PureComponent { sequence: seq, })); - createBreakoutRoom(rooms, durationTime, record); + createBreakoutRoom(rooms, durationTime, record, captureNotes, captureSlides); Session.set('isUserListOpen', true); } @@ -567,6 +588,14 @@ class BreakoutRoom extends PureComponent { this.setState({ record: e.target.checked }); } + setCaptureNotes(e) { + this.setState({ captureNotes: e.target.checked }); + } + + setCaptureSlides(e) { + this.setState({ captureSlides: e.target.checked }); + } + getUserByRoom(room) { const { users } = this.state; return users.filter((user) => user.room === room); @@ -602,17 +631,24 @@ class BreakoutRoom extends PureComponent { } changeUserRoom(userId, room) { + const { intl } = this.props; const { users, freeJoin } = this.state; const idxUser = users.findIndex((user) => user.userId === userId.replace('roomUserItem-', '')); const usersCopy = [...users]; + let userName = null; - if (idxUser >= 0) usersCopy[idxUser].room = room; + if (idxUser >= 0) { + usersCopy[idxUser].room = room; + userName = usersCopy[idxUser].userName; + }; this.setState({ users: usersCopy, leastOneUserIsValid: (this.getUserByRoom(0).length !== users.length || freeJoin), + }, () => { + addNewAlert(intl.formatMessage(intlMessages.movedUserLabel, { 0: userName, 1: room })) }); } @@ -783,7 +819,7 @@ class BreakoutRoom extends PureComponent { }; return ( - { this.listOfUsers = r; }}> + { this.listOfUsers = r; }} data-test="roomGrid"> {this.renderUserItemByRoom(0)} - + {intl.formatMessage(intlMessages.leastOneWarnBreakout)} @@ -814,6 +850,7 @@ class BreakoutRoom extends PureComponent { onBlur={changeRoomName(value)} aria-label={`${this.getRoomName(value)}`} aria-describedby={this.getRoomName(value).length === 0 ? `room-error-${value}` : `room-input-${value}`} + data-test={this.getRoomName(value).length === 0 ? `room-error-${value}` : `roomName-${value}`} readOnly={isUpdate} />
@@ -885,6 +922,7 @@ class BreakoutRoom extends PureComponent { onChange={this.changeDurationTime} onBlur={this.blurDurationTime} aria-label={intl.formatMessage(intlMessages.duration)} + data-test="durationTime" /> @@ -979,11 +1019,15 @@ class BreakoutRoom extends PureComponent { } renderCheckboxes() { - const { intl, isUpdate, isBreakoutRecordable } = this.props; + const { + intl, isUpdate, isBreakoutRecordable, + } = this.props; if (isUpdate) return null; const { freeJoin, record, + captureNotes, + captureSlides, } = this.state; return ( @@ -1013,6 +1057,38 @@ class BreakoutRoom extends PureComponent { ) : null } + { + isImportPresentationWithAnnotationsFromBreakoutRoomsEnabled() ? ( + + + + {intl.formatMessage(intlMessages.captureSlidesLabel)} + + + ) : null + } + { + isImportSharedNotesFromBreakoutRoomsEnabled() ? ( + + + + {intl.formatMessage(intlMessages.captureNotesLabel)} + + + ) : null + } ); } diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx index 21ce5fa181..349bbc7f58 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { defineMessages } from 'react-intl'; import _ from 'lodash'; @@ -8,6 +8,7 @@ import { PANELS, ACTIONS } from '../../layout/enums'; const POLL_SETTINGS = Meteor.settings.public.poll; const MAX_CUSTOM_FIELDS = POLL_SETTINGS.maxCustom; +const CANCELED_POLL_DELAY = 250; const intlMessages = defineMessages({ quickPollLabel: { @@ -34,6 +35,10 @@ const intlMessages = defineMessages({ id: 'app.poll.abstention', description: 'Poll Abstention option value', }, + typedRespLabel: { + id: 'app.poll.userResponse.label', + description: 'quick poll typed response label', + }, }); const propTypes = { @@ -44,172 +49,218 @@ const propTypes = { amIPresenter: PropTypes.bool.isRequired, }; -const handleClickQuickPoll = (layoutContextDispatch) => { - layoutContextDispatch({ - type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN, - value: true, - }); - layoutContextDispatch({ - type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL, - value: PANELS.POLL, - }); - Session.set('forcePollOpen', true); - Session.set('pollInitiated', true); -}; +const QuickPollDropdown = (props) => { + const { + amIPresenter, + intl, + parseCurrentSlideContent, + startPoll, + stopPoll, + currentSlide, + activePoll, + className, + layoutContextDispatch, + pollTypes, + } = props; -const getAvailableQuickPolls = ( - slideId, parsedSlides, startPoll, pollTypes, layoutContextDispatch, -) => { - const pollItemElements = parsedSlides.map((poll) => { - const { poll: label } = poll; - const { type } = poll; - let itemLabel = label; - const letterAnswers = []; + const parsedSlide = parseCurrentSlideContent( + intl.formatMessage(intlMessages.yesOptionLabel), + intl.formatMessage(intlMessages.noOptionLabel), + intl.formatMessage(intlMessages.abstentionOptionLabel), + intl.formatMessage(intlMessages.trueOptionLabel), + intl.formatMessage(intlMessages.falseOptionLabel), + ); - if (type !== pollTypes.YesNo - && type !== pollTypes.YesNoAbstention - && type !== pollTypes.TrueFalse) { - const { options } = itemLabel; - itemLabel = options.join('/').replace(/[\n.)]/g, ''); - if (type === pollTypes.Custom) { - for (let i = 0; i < options.length; i += 1) { - const letterOption = options[i]?.replace(/[\r.)]/g, '').toUpperCase(); - if (letterAnswers.length < MAX_CUSTOM_FIELDS) { - letterAnswers.push(letterOption); - } else { - break; + const { + slideId, quickPollOptions, optionsWithLabels, pollQuestion, + } = parsedSlide; + + const handleClickQuickPoll = (lCDispatch) => { + lCDispatch({ + type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN, + value: true, + }); + lCDispatch({ + type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL, + value: PANELS.POLL, + }); + Session.set('forcePollOpen', true); + Session.set('pollInitiated', true); + }; + + const getAvailableQuickPolls = ( + slideId, parsedSlides, funcStartPoll, _pollTypes, _layoutContextDispatch, + ) => { + const pollItemElements = parsedSlides.map((poll) => { + const { poll: label } = poll; + const { type, poll: pollData } = poll; + let itemLabel = label; + const letterAnswers = []; + + if (type === 'R-') { + return ( + { + if (activePoll) { + stopPoll(); + } + setTimeout(() => { + handleClickQuickPoll(_layoutContextDispatch); + funcStartPoll(type, slideId, letterAnswers, pollData?.question); + }, CANCELED_POLL_DELAY); + }} + question={pollData?.question} + /> + ); + } + + if (type !== _pollTypes.YesNo + && type !== _pollTypes.YesNoAbstention + && type !== _pollTypes.TrueFalse) { + const { options } = itemLabel; + itemLabel = options.join('/').replace(/[\n.)]/g, ''); + if (type === _pollTypes.Custom) { + for (let i = 0; i < options.length; i += 1) { + const letterOption = options[i]?.replace(/[\r.)]/g, '').toUpperCase(); + if (letterAnswers.length < MAX_CUSTOM_FIELDS) { + letterAnswers.push(letterOption); + } else { + break; + } } } } - } - // removes any whitespace from the label - itemLabel = itemLabel?.replace(/\s+/g, '').toUpperCase(); + // removes any whitespace from the label + itemLabel = itemLabel?.replace(/\s+/g, '').toUpperCase(); - const numChars = { - 1: 'A', 2: 'B', 3: 'C', 4: 'D', 5: 'E', - }; - itemLabel = itemLabel.split('').map((c) => { - if (numChars[c]) return numChars[c]; - return c; - }).join(''); + const numChars = { + 1: 'A', 2: 'B', 3: 'C', 4: 'D', 5: 'E', + }; + itemLabel = itemLabel.split('').map((c) => { + if (numChars[c]) return numChars[c]; + return c; + }).join(''); - return ( - { + return ( + { + 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 = ( + { + if (activePoll) { + stopPoll(); + } + + setTimeout(() => { handleClickQuickPoll(layoutContextDispatch); - startPoll(type, slideId, letterAnswers); - }} - answers={letterAnswers} - /> - ); - }); + if (singlePollType === 'R-' || singlePollType === 'TF') { + startPoll(singlePollType, currentSlide.id, answers, pollQuestion, multiResponse); + } else { + startPoll( + pollTypes.Custom, + currentSlide.id, + optionsWithLabels, + pollQuestion, + multiResponse, + ); + } + }, CANCELED_POLL_DELAY); + }} + size="lg" + data-test="quickPollBtn" + /> + ); - const sizes = []; - return pollItemElements.filter((el) => { - const { label } = el.props; - if (label.length === sizes[sizes.length - 1]) return false; - sizes.push(label.length); - return el; - }); -}; + const usePollDropdown = quickPollOptions && quickPollOptions.length && quickPolls.length > 1; + let dropdown = null; -class QuickPollDropdown extends Component { - render() { - const { - amIPresenter, - intl, - parseCurrentSlideContent, - startPoll, - currentSlide, - activePoll, - className, - layoutContextDispatch, - pollTypes, - } = this.props; - - const parsedSlide = parseCurrentSlideContent( - intl.formatMessage(intlMessages.yesOptionLabel), - intl.formatMessage(intlMessages.noOptionLabel), - intl.formatMessage(intlMessages.abstentionOptionLabel), - intl.formatMessage(intlMessages.trueOptionLabel), - intl.formatMessage(intlMessages.falseOptionLabel), - ); - - const { slideId, quickPollOptions } = parsedSlide; - const quickPolls = getAvailableQuickPolls( - slideId, quickPollOptions, startPoll, pollTypes, layoutContextDispatch, - ); - - if (quickPollOptions.length === 0) return null; - - let answers = null; - let quickPollLabel = ''; - if (quickPolls.length > 0) { - const { props: pollProps } = quickPolls[0]; - quickPollLabel = pollProps.label; - answers = pollProps.answers; - } - - let singlePollType = null; - if (quickPolls.length === 1 && quickPollOptions.length) { - const { type } = quickPollOptions[0]; - singlePollType = type; - } - - let btn = ( + if (usePollDropdown) { + btn = ( { - handleClickQuickPoll(layoutContextDispatch); - startPoll(singlePollType, currentSlide.id, answers); - }} + onClick={() => null} size="lg" - disabled={!!activePoll} - data-test="quickPollBtn" /> ); - const usePollDropdown = quickPollOptions && quickPollOptions.length && quickPolls.length > 1; - let dropdown = null; - - if (usePollDropdown) { - btn = ( - null} - size="lg" - disabled={!!activePoll} - /> - ); - - dropdown = ( - - - {btn} - - - - {quickPolls} - - - - ); - } - - return amIPresenter && usePollDropdown ? ( - dropdown - ) : ( - btn + dropdown = ( + + + {btn} + + + + {quickPolls} + + + ); } -} + + return amIPresenter && usePollDropdown ? ( + dropdown + ) : ( + btn + ); +}; QuickPollDropdown.propTypes = propTypes; diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/container.jsx index 036f227698..7c19722424 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/container.jsx @@ -1,17 +1,18 @@ import React from 'react'; import { withTracker } from 'meteor/react-meteor-data'; import { injectIntl } from 'react-intl'; +import { makeCall } from '/imports/ui/services/api'; +import PollService from '/imports/ui/components/poll/service'; import QuickPollDropdown from './component'; import { layoutDispatch } from '../../layout/context'; -import PollService from '/imports/ui/components/poll/service'; const QuickPollDropdownContainer = (props) => { const layoutContextDispatch = layoutDispatch(); - return ; }; export default withTracker(() => ({ activePoll: Session.get('pollInitiated') || false, pollTypes: PollService.pollTypes, + stopPoll: () => makeCall('stopPoll'), }))(injectIntl(QuickPollDropdownContainer)); diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/screenshare/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/screenshare/component.jsx index 9ddb1107a5..8896a4448d 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/screenshare/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/screenshare/component.jsx @@ -16,7 +16,7 @@ import { SCREENSHARING_ERRORS } from '/imports/api/screenshare/client/bridge/err import Button from '/imports/ui/components/common/button/component'; const { isMobile } = deviceInfo; -const { isSafari, isMobileApp } = browserInfo; +const { isSafari, isTabletApp } = browserInfo; const propTypes = { intl: PropTypes.objectOf(Object).isRequired, @@ -163,7 +163,7 @@ const ScreenshareButton = ({ ? intlMessages.stopDesktopShareDesc : intlMessages.desktopShareDesc; const shouldAllowScreensharing = enabled - && ( !isMobile || isMobileApp) + && ( !isMobile || isTabletApp) && amIPresenter; const dataTest = isVideoBroadcasting ? 'stopScreenShare' : 'startScreenShare'; diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/service.js b/bigbluebutton-html5/imports/ui/components/actions-bar/service.js index 6cfda0e9f5..af21c5dd97 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/service.js +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/service.js @@ -63,7 +63,7 @@ export default { isBreakoutRecordable: () => Meetings.findOne({ meetingId: Auth.meetingID }, { fields: { 'breakoutProps.record': 1 } }).breakoutProps.record, toggleRecording: () => makeCall('toggleRecording'), - createBreakoutRoom: (rooms, durationInMinutes, record = false) => makeCall('createBreakoutRoom', rooms, durationInMinutes, record), + createBreakoutRoom: (rooms, durationInMinutes, record = false, captureNotes = false, captureSlides = false) => makeCall('createBreakoutRoom', rooms, durationInMinutes, record, captureNotes, captureSlides), sendInvitation: (breakoutId, userId) => makeCall('requestJoinURL', { breakoutId, userId }), breakoutJoinedUsers: () => Breakouts.find({ joinedUsers: { $exists: true }, diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx index 66981e3a4a..ce26e5e95b 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx @@ -81,6 +81,8 @@ class AudioControls extends PureComponent { handleLeaveAudio, handleToggleMuteMicrophone, muted, disable, talking, } = this.props; + const { isMobile } = deviceInfo; + return ( diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/input-stream-live-selector/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/input-stream-live-selector/component.jsx index a4feef0023..39bbff8ebd 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/input-stream-live-selector/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/input-stream-live-selector/component.jsx @@ -366,8 +366,8 @@ class InputStreamLiveSelector extends Component { currentInputDeviceId, currentOutputDeviceId, isListenOnly, - isRTL, shortcuts, + isMobile, } = this.props; const inputDeviceList = !isListenOnly @@ -399,6 +399,7 @@ class InputStreamLiveSelector extends Component { }; const dropdownListComplete = inputDeviceList.concat(outputDeviceList).concat(leaveAudioOption); + const customStyles = { top: '-1rem' }; return ( <> @@ -411,6 +412,7 @@ class InputStreamLiveSelector extends Component { /> ) : null} {isListenOnly @@ -428,14 +430,14 @@ class InputStreamLiveSelector extends Component { )} actions={dropdownListComplete} opts={{ - id: 'default-dropdown-menu', + id: 'audio-selector-dropdown-menu', keepMounted: true, transitionDuration: 0, elevation: 3, getContentAnchorEl: null, fullwidth: 'true', - anchorOrigin: { vertical: 'top', horizontal: isRTL ? 'left' : 'right' }, - transformOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' }, + anchorOrigin: { vertical: 'top', horizontal: 'center' }, + transformOrigin: { vertical: 'bottom', horizontal: 'center'}, }} /> diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.js b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.js index a2e9ed8c6e..ddfaad66ec 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.js +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.js @@ -94,7 +94,7 @@ const ConnectingAnimation = styled.span` `; const AudioModal = styled(Modal)` - padding: 1.5rem; + padding: 1rem; min-height: 20rem; `; diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-settings/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-settings/component.jsx index db7caeb84b..0897de1399 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-settings/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-settings/component.jsx @@ -98,6 +98,7 @@ class AudioSettings extends React.Component { componentDidMount() { const { inputDeviceId, outputDeviceId } = this.state; + Session.set('inEchoTest', true); this._isMounted = true; // Guarantee initial in/out devices are initialized on all ends this.setInputDevice(inputDeviceId); @@ -107,6 +108,7 @@ class AudioSettings extends React.Component { componentWillUnmount() { const { stream } = this.state; + Session.set('inEchoTest', false); this._mounted = false; if (stream) { diff --git a/bigbluebutton-html5/imports/ui/components/audio/captions/button/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/captions/button/component.jsx index d4014ba9d9..97fddd7eb4 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/captions/button/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/captions/button/component.jsx @@ -1,7 +1,10 @@ -import React from 'react'; +import React, { useRef, useEffect } from 'react'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl } from 'react-intl'; import Service from '/imports/ui/components/audio/captions/service'; +import SpeechService from '/imports/ui/components/audio/captions/speech/service'; +import ButtonEmoji from '/imports/ui/components/common/button/button-emoji/ButtonEmoji'; +import BBBMenu from '/imports/ui/components/common/menu/component'; import Styled from './styles'; const intlMessages = defineMessages({ @@ -13,18 +16,155 @@ const intlMessages = defineMessages({ id: 'app.audio.captions.button.stop', description: 'Stop audio captions', }, + transcriptionSettings: { + id: 'app.audio.captions.button.transcriptionSettings', + description: 'Audio captions settings modal', + }, + transcription: { + id: 'app.audio.captions.button.transcription', + description: 'Audio speech transcription label', + }, + transcriptionOn: { + id: 'app.switch.onLabel', + }, + transcriptionOff: { + id: 'app.switch.offLabel', + }, + language: { + id: 'app.audio.captions.button.language', + description: 'Audio speech recognition language label', + }, + 'de-DE': { + id: 'app.audio.captions.select.de-DE', + description: 'Audio speech recognition german language', + }, + 'en-US': { + id: 'app.audio.captions.select.en-US', + description: 'Audio speech recognition english language', + }, + 'es-ES': { + id: 'app.audio.captions.select.es-ES', + description: 'Audio speech recognition spanish language', + }, + 'fr-FR': { + id: 'app.audio.captions.select.fr-FR', + description: 'Audio speech recognition french language', + }, + 'hi-ID': { + id: 'app.audio.captions.select.hi-ID', + description: 'Audio speech recognition indian language', + }, + 'it-IT': { + id: 'app.audio.captions.select.it-IT', + description: 'Audio speech recognition italian language', + }, + 'ja-JP': { + id: 'app.audio.captions.select.ja-JP', + description: 'Audio speech recognition japanese language', + }, + 'pt-BR': { + id: 'app.audio.captions.select.pt-BR', + description: 'Audio speech recognition portuguese language', + }, + 'ru-RU': { + id: 'app.audio.captions.select.ru-RU', + description: 'Audio speech recognition russian language', + }, + 'zh-CN': { + id: 'app.audio.captions.select.zh-CN', + description: 'Audio speech recognition chinese language', + }, }); +const DEFAULT_LOCALE = 'en-US'; +const DISABLED = ''; + const CaptionsButton = ({ intl, active, + isRTL, enabled, + currentSpeechLocale, + availableVoices, + isSupported, + isVoiceUser, }) => { - const onClick = () => Service.setAudioCaptions(!active); - if (!enabled) return null; - return ( + const isTranscriptionDisabled = () => ( + currentSpeechLocale === DISABLED + ); + + const fallbackLocale = availableVoices.includes(navigator.language) + ? navigator.language : DEFAULT_LOCALE; + + const getSelectedLocaleValue = (isTranscriptionDisabled() ? fallbackLocale : currentSpeechLocale); + + const selectedLocale = useRef(getSelectedLocaleValue); + + useEffect(() => { + if (!isTranscriptionDisabled()) selectedLocale.current = getSelectedLocaleValue; + }, [currentSpeechLocale]); + + const shouldRenderChevron = isSupported && isVoiceUser; + + const getAvailableLocales = () => ( + availableVoices.map((availableVoice) => ( + { + icon: '', + label: intl.formatMessage(intlMessages[availableVoice]), + key: availableVoice, + iconRight: selectedLocale.current === availableVoice ? 'check' : null, + customStyles: (selectedLocale.current === availableVoice) && Styled.SelectedLabel, + disabled: isTranscriptionDisabled(), + dividerTop: availableVoice === availableVoices[0], + onClick: () => { + selectedLocale.current = availableVoice; + SpeechService.setSpeechLocale(selectedLocale.current); + }, + } + )) + ); + + const toggleTranscription = () => { + SpeechService.setSpeechLocale(isTranscriptionDisabled() ? selectedLocale.current : DISABLED); + }; + + const getAvailableLocalesList = () => ( + [{ + key: 'availableLocalesList', + label: intl.formatMessage(intlMessages.language), + customStyles: Styled.TitleLabel, + disabled: true, + dividerTop: false, + }, + ...getAvailableLocales(), + { + key: 'divider', + label: intl.formatMessage(intlMessages.transcription), + customStyles: Styled.TitleLabel, + disabled: true, + }, { + key: 'transcriptionStatus', + label: intl.formatMessage( + isTranscriptionDisabled() + ? intlMessages.transcriptionOn + : intlMessages.transcriptionOff, + ), + customStyles: isTranscriptionDisabled() + ? Styled.EnableTrascription : Styled.DisableTrascription, + disabled: false, + dividerTop: true, + onClick: toggleTranscription, + }] + ); + + const onToggleClick = (e) => { + e.stopPropagation(); + Service.setAudioCaptions(!active); + }; + + const startStopCaptionsButton = ( ); + + return ( + shouldRenderChevron + ? ( + + + { startStopCaptionsButton } + + + )} + 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' }, + }} + /> + + ) : startStopCaptionsButton + ); }; CaptionsButton.propTypes = { @@ -43,7 +216,12 @@ CaptionsButton.propTypes = { formatMessage: PropTypes.func.isRequired, }).isRequired, active: PropTypes.bool.isRequired, + isRTL: PropTypes.bool.isRequired, enabled: PropTypes.bool.isRequired, + currentSpeechLocale: PropTypes.string.isRequired, + availableVoices: PropTypes.arrayOf(PropTypes.string).isRequired, + isSupported: PropTypes.bool.isRequired, + isVoiceUser: PropTypes.bool.isRequired, }; export default injectIntl(CaptionsButton); diff --git a/bigbluebutton-html5/imports/ui/components/audio/captions/button/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/captions/button/container.jsx index b030380067..44a08b88c9 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/captions/button/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/captions/button/container.jsx @@ -2,10 +2,24 @@ import React from 'react'; import { withTracker } from 'meteor/react-meteor-data'; import Service from '/imports/ui/components/audio/captions/service'; import Button from './component'; +import SpeechService from '/imports/ui/components/audio/captions/speech/service'; +import AudioService from '/imports/ui/components/audio/service'; const Container = (props) =>
+ + + + + - - {this.renderSkipSlideOpts(numberOfSlides)} - - - + + - - } - { - - + + + this.handleSwitchWhiteboardMode(!multiUser)} - label={ + color="light" + disabled={!isMeteorConnected} + icon={multiUser ? 'multi_whiteboard' : 'whiteboard'} + size="md" + circle + onClick={() => this.handleSwitchWhiteboardMode(!multiUser)} + label={ multiUser ? intl.formatMessage(intlMessages.toolbarMultiUserOff) : intl.formatMessage(intlMessages.toolbarMultiUserOn) } - hideLabel - /> - {multiUser ? ( - {multiUserSize} - ) : ( - - )} - {!isMobile ? ( - - - - ) : null} - - + {multiUser ? ( + {multiUserSize} + ) : ( + + )} + {!isMobile ? ( + + + + ) : null} + + - - } + color="light" + disabled={!isMeteorConnected} + icon="fit_to_width" + size="md" + circle + onClick={fitToWidthHandler} + label={fitToWidth + ? intl.formatMessage(intlMessages.fitToPage) + : intl.formatMessage(intlMessages.fitToWidth)} + hideLabel + /> + ); } diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx index 03f0b08027..5f166d5710 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx @@ -43,12 +43,12 @@ export default withTracker((params) => { presentationId, } = params; - const startPoll = (type, id, answers) => { + const startPoll = (type, id, answers = [], question = '', multiResp = false) => { Session.set('openPanel', 'poll'); Session.set('forcePollOpen', true); window.dispatchEvent(new Event('panelChanged')); - makeCall('startPoll', PollService.pollTypes, type, id, false, '', false, answers); + makeCall('startPoll', PollService.pollTypes, type, id, false, question, multiResp, answers); }; return { diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/smart-video-share/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/smart-video-share/component.jsx new file mode 100644 index 0000000000..b39cd203e1 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/smart-video-share/component.jsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { defineMessages } from 'react-intl'; +import { safeMatch } from '/imports/utils/string-utils'; +import { isUrlValid, startWatching } from '/imports/ui/components/external-video-player/service'; +import BBBMenu from '/imports/ui/components/common/menu/component'; +import Styled from './styles'; + +const intlMessages = defineMessages({ + externalVideo: { + id: 'app.smartMediaShare.externalVideo', + }, +}); + +export const SmartMediaShare = (props) => { + const { + currentSlide, intl, isMobile, isRTL, + } = props; + const linkPatt = /(https?:\/\/[^\s]+)/gm; + const externalLinks = safeMatch(linkPatt, currentSlide?.content, false); + if (!externalLinks) return null; + + const actions = []; + + externalLinks.forEach((lnk) => { + if (isUrlValid(lnk)) { + actions.push({ + label: lnk, + onClick: () => startWatching(lnk), + }); + } + }); + + if (actions?.length === 0) return null; + + const customStyles = { top: '-1rem' }; + + return ( + null} + hideLabel + /> + )} + actions={actions} + opts={{ + id: 'external-video-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' }, + }} + /> + ); +}; + +export default SmartMediaShare; diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/smart-video-share/container.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/smart-video-share/container.jsx new file mode 100644 index 0000000000..0a28ce00fd --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/smart-video-share/container.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { withTracker } from 'meteor/react-meteor-data'; +import { SmartMediaShare } from './component'; + +import { layoutSelect } from '/imports/ui/components/layout/context'; +import { isMobile } from '/imports/ui/components/layout/utils'; + +const SmartMediaShareContainer = (props) => ( + +); + +export default withTracker(() => { + const isRTL = layoutSelect((i) => i.isRTL); + return { + isRTL, + isMobile: isMobile(), + }; +})(SmartMediaShareContainer); diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/smart-video-share/styles.js b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/smart-video-share/styles.js new file mode 100644 index 0000000000..b759ca836b --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/smart-video-share/styles.js @@ -0,0 +1,22 @@ +import styled from 'styled-components'; +import Button from '/imports/ui/components/common/button/component'; + +const QuickVideoButton = styled(Button)` + i { + font-size: 1rem; + padding-left: 20%; + right: 2px; + + [dir="rtl"] & { + -webkit-transform: scale(-1, 1); + -moz-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + -o-transform: scale(-1, 1); + transform: scale(-1, 1); + } + } +`; + +export default { + QuickVideoButton, +}; diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.js b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.js index b031a2731a..54fc86577f 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.js +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.js @@ -132,17 +132,19 @@ const SkipSlideSelect = styled.select` &:-moz-focusring { outline: none; } - + + &:focus, &:hover { outline: transparent; outline-style: dotted; outline-width: ${borderSize}; + background-color: #DCE4EC; + border-radius: 4px; } &:focus { - outline: transparent; - outline-width: ${borderSize}; outline-style: solid; + box-shadow: 0 0 0 1px #cdd6e0 !important; } `; diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/zoom-tool/styles.js b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/zoom-tool/styles.js index bbbdbc58ba..07ffcd80e7 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/zoom-tool/styles.js +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/zoom-tool/styles.js @@ -5,6 +5,7 @@ import { } from '/imports/ui/stylesheets/styled-components/palette'; import { whiteboardToolbarMargin, + borderSize, } from '/imports/ui/stylesheets/styled-components/general'; import Button from '/imports/ui/components/common/button/component'; @@ -22,11 +23,6 @@ const ResetZoomButton = styled(Button)` font-weight: 200; margin-left: ${whiteboardToolbarMargin}; margin-right: ${whiteboardToolbarMargin}; - - &:hover { - opacity: .8; - } - position: relative; color: ${toolbarButtonColor}; background-color: ${colorOffWhite}; @@ -34,9 +30,22 @@ const ResetZoomButton = styled(Button)` box-shadow: none !important; border: 0; + &:focus, + &:hover { + outline: transparent; + outline-style: dotted; + outline-width: ${borderSize}; + background-color: #DCE4EC; + border-radius: 4px; + } + + &:hover { + opacity: .8; + } + &:focus { - background-color: ${colorOffWhite}; - border: 0; + outline-style: solid; + box-shadow: 0 0 0 1px #cdd6e0 !important; } `; diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx index 5d096e66e4..e5cbeb9246 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx @@ -146,6 +146,10 @@ const intlMessages = defineMessages({ TIMEOUT: { id: 'app.presentationUploder.conversion.timeout', }, + CONVERSION_TIMEOUT: { + id:'app.presentationUploder.conversion.conversionTimeout', + description: 'warns the user that the presentation timed out in the back-end in specific page of the document', + }, GENERATING_THUMBNAIL: { id: 'app.presentationUploder.conversion.generatingThumbnail', description: 'indicatess that it is generating thumbnails', @@ -330,18 +334,17 @@ class PresentationUploader extends Component { let shouldUpdateState = isOpen && !prevProps.isOpen; const presState = Object.values({ - ...propPresentations, - ...presentations, + ...JSON.parse(JSON.stringify(propPresentations)), + ...JSON.parse(JSON.stringify(presentations)), }); if (propPresentations.length > prevPropPresentations.length) { shouldUpdateState = true; - const propsDiffs = propPresentations.filter(p => !prevPropPresentations.includes(p)) + const propsDiffs = propPresentations.filter(p => + !prevPropPresentations.some(presentation => p.id === presentation.id + || p.temporaryPresentationId === presentation.temporaryPresentationId)); propsDiffs.forEach(p => { const index = presState.findIndex(pres => { - if (p.isCurrent) { - pres.isCurrent = false; - } return pres.temporaryPresentationId === p.temporaryPresentationId || pres.id === p.id; }); if (index === -1) { diff --git a/bigbluebutton-html5/imports/ui/components/presentation/service.js b/bigbluebutton-html5/imports/ui/components/presentation/service.js index 3cfc720cdd..fbfe358018 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/service.js +++ b/bigbluebutton-html5/imports/ui/components/presentation/service.js @@ -1,6 +1,7 @@ import Presentations from '/imports/api/presentations'; import { Slides, SlidePositions } from '/imports/api/slides'; import PollService from '/imports/ui/components/poll/service'; +import { safeMatch } from '/imports/utils/string-utils'; const POLL_SETTINGS = Meteor.settings.public.poll; const MAX_CUSTOM_FIELDS = POLL_SETTINGS.maxCustom; @@ -83,9 +84,26 @@ const parseCurrentSlideContent = (yesValue, noValue, abstentionValue, trueValue, content, } = currentSlide; + const questionRegex = /.*?\?$/gm; + const question = safeMatch(questionRegex, content, ''); + + const doubleQuestionRegex = /\?{2}/gm; + const doubleQuestion = safeMatch(doubleQuestionRegex, content, false); + + const yesNoPatt = /.*(yes\/no|no\/yes).*/gm; + const hasYN = safeMatch(yesNoPatt, content, false); + const pollRegex = /[1-9A-Ia-i][.)].*/g; - let optionsPoll = content.match(pollRegex) || []; - if (optionsPoll) optionsPoll = optionsPoll.map((opt) => `\r${opt[0]}.`); + let optionsPoll = safeMatch(pollRegex, content, []); + const optionsWithLabels = []; + if (optionsPoll) { + optionsPoll = optionsPoll.map((opt) => { + const MAX_CHAR_LIMIT = 30; + const formattedOpt = opt.substring(0, MAX_CHAR_LIMIT); + optionsWithLabels.push(formattedOpt); + return `\r${opt[0]}.`; + }); + } optionsPoll.reduce((acc, currentValue) => { const lastElement = acc[acc.length - 1]; @@ -122,7 +140,9 @@ const parseCurrentSlideContent = (yesValue, noValue, abstentionValue, trueValue, return acc; }, []).filter(({ options, - }) => options.length > 1 && options.length < 10).forEach((poll) => { + }) => options.length > 1 && options.length < 10).forEach((p) => { + const poll = p; + if (doubleQuestion) poll.multiResp = true; if (poll.options.length <= 5 || MAX_CUSTOM_FIELDS <= 5) { const maxAnswer = poll.options.length > MAX_CUSTOM_FIELDS ? MAX_CUSTOM_FIELDS @@ -139,6 +159,15 @@ const parseCurrentSlideContent = (yesValue, noValue, abstentionValue, trueValue, } }); + if (question.length > 0 && optionsPoll.length === 0 && !doubleQuestion && !hasYN) { + quickPollOptions.push({ + type: 'R-', + poll: { + question: question[0], + }, + }); + } + if (quickPollOptions.length > 0) { content = content.replace(new RegExp(pollRegex), ''); } @@ -162,9 +191,13 @@ const parseCurrentSlideContent = (yesValue, noValue, abstentionValue, trueValue, poll, })); + const pollQuestion = (question?.length > 0 && question[0]?.replace(/ *\([^)]*\) */g, '')) || ''; + return { slideId: currentSlide.id, quickPollOptions, + optionsWithLabels, + pollQuestion, }; }; diff --git a/bigbluebutton-html5/imports/ui/components/presentation/styles.js b/bigbluebutton-html5/imports/ui/components/presentation/styles.js index bc9eb8993f..bc8778ca4c 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/styles.js +++ b/bigbluebutton-html5/imports/ui/components/presentation/styles.js @@ -125,7 +125,7 @@ const PresentationContainer = styled.div` left: 0; right: 0; bottom: 0; - z-index: 0; + z-index: 1; `; const Presentation = styled.div` diff --git a/bigbluebutton-html5/imports/ui/components/screenshare/service.js b/bigbluebutton-html5/imports/ui/components/screenshare/service.js index 5d1108a6c1..b99f5d63a0 100644 --- a/bigbluebutton-html5/imports/ui/components/screenshare/service.js +++ b/bigbluebutton-html5/imports/ui/components/screenshare/service.js @@ -131,8 +131,8 @@ const getVolume = () => KurentoBridge.getVolume(); const shouldEnableVolumeControl = () => VOLUME_CONTROL_ENABLED && screenshareHasAudio(); const attachLocalPreviewStream = (mediaElement) => { - const {isMobileApp} = browserInfo; - if (isMobileApp) { + const {isTabletApp} = browserInfo; + if (isTabletApp) { // We don't show preview for mobile app, as the stream is only available in native code return; } diff --git a/bigbluebutton-html5/imports/ui/components/settings/component.jsx b/bigbluebutton-html5/imports/ui/components/settings/component.jsx index 72570c2a05..f956762585 100644 --- a/bigbluebutton-html5/imports/ui/components/settings/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/settings/component.jsx @@ -268,7 +268,7 @@ class Settings extends Component { title={intl.formatMessage(intlMessages.SettingsLabel)} confirm={{ callback: () => { - this.updateSettings(current, intl.formatMessage(intlMessages.savedAlertLabel)); + this.updateSettings(current, intlMessages.savedAlertLabel); if (saved.application.locale !== current.application.locale) { const { language } = formatLocaleCode(saved.application.locale); diff --git a/bigbluebutton-html5/imports/ui/components/settings/service.js b/bigbluebutton-html5/imports/ui/components/settings/service.js index 366d9dd4c7..0941c7db6d 100644 --- a/bigbluebutton-html5/imports/ui/components/settings/service.js +++ b/bigbluebutton-html5/imports/ui/components/settings/service.js @@ -3,6 +3,7 @@ import Auth from '/imports/ui/services/auth'; import Settings from '/imports/ui/services/settings'; import {notify} from '/imports/ui/services/notification'; import GuestService from '/imports/ui/components/waiting-users/service'; +import Intl from '/imports/ui/services/locale'; const getUserRoles = () => { const user = Users.findOne({ @@ -30,18 +31,20 @@ const showGuestNotification = () => { const isKeepPushingLayoutEnabled = () => Meteor.settings.public.layout.showPushLayoutToggle; -const updateSettings = (obj, msg) => { +const updateSettings = (obj, msgDescriptor) => { Object.keys(obj).forEach(k => (Settings[k] = obj[k])); Settings.save(); - if (msg) { + if (msgDescriptor) { // prevents React state update on unmounted component setTimeout(() => { - notify( - msg, - 'info', - 'settings', - ); + Intl.formatMessage(msgDescriptor).then((txt) => { + notify( + txt, + 'info', + 'settings', + ); + }); }, 0); } }; diff --git a/bigbluebutton-html5/imports/ui/components/shortcut-help/component.jsx b/bigbluebutton-html5/imports/ui/components/shortcut-help/component.jsx index b9c08bf846..2e93db0811 100644 --- a/bigbluebutton-html5/imports/ui/components/shortcut-help/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/shortcut-help/component.jsx @@ -366,10 +366,10 @@ const ShortcutHelpComponent = (props) => { - + {!accessMod ?

{intl.formatMessage(intlMessages.accessKeyNotAvailable)}

: ( - +
{intl.formatMessage(intlMessages.functionLabel)}
{intl.formatMessage(intlMessages.functionLabel)}