diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/PollApp2x.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/PollApp2x.scala index 02f5cf78749bf68a56d93175d1cb625c93fc8b12..31dbeb73fa8f0c96b827a05728cdc63e42ff12ff 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/PollApp2x.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/PollApp2x.scala @@ -6,6 +6,7 @@ import akka.event.Logging class PollApp2x(implicit val context: ActorContext) extends GetCurrentPollReqMsgHdlr with RespondToPollReqMsgHdlr + with RespondToTypedPollReqMsgHdlr with ShowPollResultReqMsgHdlr with StartCustomPollReqMsgHdlr with StartPollReqMsgHdlr 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 new file mode 100644 index 0000000000000000000000000000000000000000..b87f510dc2a6818e0d9d6ec6077049c87afbe78f --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/RespondToTypedPollReqMsgHdlr.scala @@ -0,0 +1,51 @@ +package org.bigbluebutton.core.apps.polls + +import org.bigbluebutton.common2.domain.SimplePollResultOutVO +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.bus.MessageBus +import org.bigbluebutton.core.models.Polls +import org.bigbluebutton.core.running.{ LiveMeeting } +import org.bigbluebutton.core.models.Users2x + +trait RespondToTypedPollReqMsgHdlr { + this: PollApp2x => + + def handle(msg: RespondToTypedPollReqMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { + log.debug("Received RespondToPollReqMsg {}", RespondToTypedPollReqMsg) + + def broadcastPollUpdatedEvent(msg: RespondToTypedPollReqMsg, pollId: String, poll: SimplePollResultOutVO): Unit = { + val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, liveMeeting.props.meetingProp.intId, msg.header.userId) + val envelope = BbbCoreEnvelope(PollUpdatedEvtMsg.NAME, routing) + val header = BbbClientMsgHeader(PollUpdatedEvtMsg.NAME, liveMeeting.props.meetingProp.intId, msg.header.userId) + + val body = PollUpdatedEvtMsgBody(pollId, poll) + val event = PollUpdatedEvtMsg(header, body) + val msgEvent = BbbCommonEnvCoreMsg(envelope, event) + bus.outGW.send(msgEvent) + } + + def broadcastUserRespondedToTypedPollRespMsg(msg: RespondToTypedPollReqMsg, pollId: String, answer: String, sendToId: String): Unit = { + val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, liveMeeting.props.meetingProp.intId, sendToId) + val envelope = BbbCoreEnvelope(UserRespondedToTypedPollRespMsg.NAME, routing) + val header = BbbClientMsgHeader(UserRespondedToTypedPollRespMsg.NAME, liveMeeting.props.meetingProp.intId, sendToId) + + val body = UserRespondedToTypedPollRespMsgBody(pollId, msg.header.userId, answer) + val event = UserRespondedToTypedPollRespMsg(header, body) + val msgEvent = BbbCommonEnvCoreMsg(envelope, event) + 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) + + for { + presenter <- Users2x.findPresenter(liveMeeting.users2x) + } yield { + broadcastUserRespondedToTypedPollRespMsg(msg, pollId, msg.body.answer, presenter.intId) + } + } + } +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/StartCustomPollReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/StartCustomPollReqMsgHdlr.scala index cf58f6109810e30dda9e7910dd2ce8eb030147fd..0065bc517a69240224262e9eea51b32f4d81fc76 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/StartCustomPollReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/StartCustomPollReqMsgHdlr.scala @@ -17,7 +17,7 @@ trait StartCustomPollReqMsgHdlr extends RightsManagementTrait { val envelope = BbbCoreEnvelope(PollStartedEvtMsg.NAME, routing) val header = BbbClientMsgHeader(PollStartedEvtMsg.NAME, liveMeeting.props.meetingProp.intId, msg.header.userId) - val body = PollStartedEvtMsgBody(msg.header.userId, poll.id, poll) + val body = PollStartedEvtMsgBody(msg.header.userId, poll.id, msg.body.pollType, msg.body.question, poll) val event = PollStartedEvtMsg(header, body) val msgEvent = BbbCommonEnvCoreMsg(envelope, event) bus.outGW.send(msgEvent) @@ -29,7 +29,7 @@ trait StartCustomPollReqMsgHdlr extends RightsManagementTrait { PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) } else { for { - pvo <- Polls.handleStartCustomPollReqMsg(state, msg.header.userId, msg.body.pollId, msg.body.pollType, msg.body.answers, liveMeeting) + pvo <- Polls.handleStartCustomPollReqMsg(state, msg.header.userId, msg.body.pollId, msg.body.pollType, msg.body.answers, msg.body.question, liveMeeting) } yield { broadcastEvent(msg, pvo) } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/StartPollReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/StartPollReqMsgHdlr.scala index c50e43335d79d95b022e7d837b792464d032796c..f01eaa74ac265d68de19f5348278d74570eb1b6f 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/StartPollReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/StartPollReqMsgHdlr.scala @@ -18,7 +18,7 @@ trait StartPollReqMsgHdlr extends RightsManagementTrait { val envelope = BbbCoreEnvelope(PollStartedEvtMsg.NAME, routing) val header = BbbClientMsgHeader(PollStartedEvtMsg.NAME, liveMeeting.props.meetingProp.intId, msg.header.userId) - val body = PollStartedEvtMsgBody(msg.header.userId, poll.id, poll) + val body = PollStartedEvtMsgBody(msg.header.userId, poll.id, msg.body.pollType, msg.body.question, poll) val event = PollStartedEvtMsg(header, body) val msgEvent = BbbCommonEnvCoreMsg(envelope, event) bus.outGW.send(msgEvent) @@ -30,7 +30,7 @@ trait StartPollReqMsgHdlr extends RightsManagementTrait { PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) } else { for { - pvo <- Polls.handleStartPollReqMsg(state, msg.header.userId, msg.body.pollId, msg.body.pollType, liveMeeting) + pvo <- Polls.handleStartPollReqMsg(state, msg.header.userId, msg.body.pollId, msg.body.pollType, msg.body.question, liveMeeting) } yield { broadcastEvent(msg, pvo) } 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 6bec9374607e1feca3773e4489efda00ae77cefd..99cc8f5bf2109a349975028c265c70b3871cdf2a 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 @@ -11,7 +11,7 @@ import org.bigbluebutton.core.running.LiveMeeting object Polls { - def handleStartPollReqMsg(state: MeetingState2x, userId: String, pollId: String, pollType: String, + def handleStartPollReqMsg(state: MeetingState2x, userId: String, pollId: String, pollType: String, question: String, lm: LiveMeeting): Option[SimplePollOutVO] = { def createPoll(stampedPollId: String): Option[Poll] = { @@ -156,8 +156,18 @@ object Polls { } + def handleRespondToTypedPollReqMsg(requesterId: String, pollId: String, questionId: Int, answer: String, + lm: LiveMeeting): Option[(String, SimplePollResultOutVO)] = { + for { + poll <- getSimplePollResult(pollId, lm.polls) + pvo <- handleRespondToTypedPoll(poll, requesterId, pollId, questionId, answer, lm) + } yield { + (pollId, pvo) + } + } + def handleStartCustomPollReqMsg(state: MeetingState2x, requesterId: String, pollId: String, pollType: String, - answers: Seq[String], lm: LiveMeeting): Option[SimplePollOutVO] = { + answers: Seq[String], question: String, lm: LiveMeeting): Option[SimplePollOutVO] = { def createPoll(stampedPollId: String): Option[Poll] = { val numRespondents: Int = Users2x.numUsers(lm.users2x) - 1 // subtract the presenter @@ -227,7 +237,17 @@ object Polls { } yield { updatedPoll } + } + + private def handleRespondToTypedPoll(poll: SimplePollResultOutVO, requesterId: String, pollId: String, questionId: Int, + answer: String, lm: LiveMeeting): Option[SimplePollResultOutVO] = { + addQuestionResponse(poll.id, questionId, answer, lm.polls) + for { + updatedPoll <- getSimplePollResult(poll.id, lm.polls) + } yield { + updatedPoll + } } private def pollResultToWhiteboardShape(result: SimplePollResultOutVO): scala.collection.immutable.Map[String, Object] = { @@ -363,6 +383,14 @@ object Polls { } } + def addQuestionResponse(pollId: String, questionID: Int, answer: String, polls: Polls) { + polls.polls.get(pollId) match { + case Some(p) => { + p.addQuestionResponse(questionID, answer) + } + case None => + } + } } object PollType { @@ -371,6 +399,7 @@ object PollType { val CustomPollType = "CUSTOM" val LetterPollType = "A-" val NumberPollType = "1-" + val ResponsePollType = "RP" } object PollFactory { @@ -379,19 +408,19 @@ object PollFactory { val NumberArray = Array("1", "2", "3", "4", "5", "6") private def processYesNoPollType(qType: String): Question = { - val answers = new Array[Answer](2) + val answers = new ArrayBuffer[Answer]; - answers(0) = new Answer(0, "Yes", Some("Yes")) - answers(1) = new Answer(1, "No", Some("No")) + answers += new Answer(0, "Yes", Some("Yes")) + answers += new Answer(1, "No", Some("No")) new Question(0, PollType.YesNoPollType, false, None, answers) } private def processTrueFalsePollType(qType: String): Question = { - val answers = new Array[Answer](2) + val answers = new ArrayBuffer[Answer]; - answers(0) = new Answer(0, "True", Some("True")) - answers(1) = new Answer(1, "False", Some("False")) + answers += new Answer(0, "True", Some("True")) + answers += new Answer(1, "False", Some("False")) new Question(0, PollType.TrueFalsePollType, false, None, answers) } @@ -403,10 +432,9 @@ object PollFactory { var questionOption: Option[Question] = None if (numQs > 0 && numQs <= 6) { - val answers = new Array[Answer](numQs) - + val answers = new ArrayBuffer[Answer]; for (i <- 0 until numQs) { - answers(i) = new Answer(i, LetterArray(i), Some(LetterArray(i))) + answers += new Answer(i, LetterArray(i), Some(LetterArray(i))) val question = new Question(0, PollType.LetterPollType, multiResponse, None, answers) questionOption = Some(question) } @@ -422,9 +450,9 @@ object PollFactory { var questionOption: Option[Question] = None if (numQs > 0 && numQs <= 6) { - val answers = new Array[Answer](numQs) + val answers = new ArrayBuffer[Answer]; for (i <- 0 until numQs) { - answers(i) = new Answer(i, NumberArray(i), Some(NumberArray(i))) + answers += new Answer(i, NumberArray(i), Some(NumberArray(i))) val question = new Question(0, PollType.NumberPollType, multiResponse, None, answers) questionOption = Some(question) } @@ -432,10 +460,10 @@ object PollFactory { questionOption } - private def buildAnswers(answers: Seq[String]): Array[Answer] = { - val ans = new Array[Answer](answers.length) + private def buildAnswers(answers: Seq[String]): ArrayBuffer[Answer] = { + val ans = new ArrayBuffer[Answer] for (i <- 0 until answers.length) { - ans(i) = new Answer(i, answers(i), Some(answers(i))) + ans += new Answer(i, answers(i), Some(answers(i))) } ans @@ -453,6 +481,16 @@ object PollFactory { questionOption } + private def processResponsePollType(qType: String): Option[Question] = { + var questionOption: Option[Question] = None + + val answers = new ArrayBuffer[Answer] + val question = new Question(0, PollType.ResponsePollType, false, None, answers) + questionOption = Some(question) + + questionOption + } + private def createQuestion(qType: String, answers: Option[Seq[String]]): Option[Question] = { val qt = qType.toUpperCase() @@ -468,6 +506,8 @@ object PollFactory { questionOption = processLetterPollType(qt, false) } else if (qt.startsWith(PollType.NumberPollType)) { questionOption = processNumberPollType(qt, false) + } else if (qt.startsWith(PollType.ResponsePollType)) { + questionOption = processResponsePollType(qt) } questionOption @@ -530,6 +570,14 @@ class Poll(val id: String, val questions: Array[Question], val numRespondents: I }) } + def addQuestionResponse(questionID: Int, answer: String) { + questions.foreach(q => { + if (q.id == questionID) { + q.addQuestionResponse(answer) + } + }) + } + def toPollVO(): PollVO = { val qvos = new ArrayBuffer[QuestionVO] questions.foreach(q => { @@ -548,7 +596,10 @@ class Poll(val id: String, val questions: Array[Question], val numRespondents: I } } -class Question(val id: Int, val questionType: String, val multiResponse: Boolean, val text: Option[String], val answers: Array[Answer]) { +class Question(val id: Int, val questionType: String, val multiResponse: Boolean, val text: Option[String], val answers: ArrayBuffer[Answer]) { + def addAnswer(text: String) { + answers += new Answer(answers.size, text, Some(text)) + } def clear() { answers.foreach(r => r.clear) @@ -568,6 +619,10 @@ class Question(val id: Int, val questionType: String, val multiResponse: Boolean }) } + def addQuestionResponse(answer: String) { + addAnswer(answer) + } + def toQuestionVO(): QuestionVO = { val rvos = new ArrayBuffer[AnswerVO] answers.foreach(answer => { 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 683f07d0b0aabcd85962600150d2e8d9751bb5d4..40c6a169169f6584085951c241b75d29b0348b13 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 @@ -117,6 +117,8 @@ class ReceivedJsonMsgHandlerActor( routeGenericMsg[GetCurrentPollReqMsg](envelope, jsonNode) case RespondToPollReqMsg.NAME => routeGenericMsg[RespondToPollReqMsg](envelope, jsonNode) + case RespondToTypedPollReqMsg.NAME => + routeGenericMsg[RespondToTypedPollReqMsg](envelope, jsonNode) // Webcam case UserBroadcastCamStartMsg.NAME => 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 8a1955c2e691dd618ca05957bee90d713673ab52..df25ea34720a879181ae87b1b182009e2d0ef245 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 @@ -278,9 +278,9 @@ class MeetingActor( private def handleBbbCommonEnvCoreMsg(msg: BbbCommonEnvCoreMsg): Unit = { msg.core match { - case m: ClientToServerLatencyTracerMsg => handleClientToServerLatencyTracerMsg(m) + case m: ClientToServerLatencyTracerMsg => handleClientToServerLatencyTracerMsg(m) case m: CheckRunningAndRecordingVoiceConfEvtMsg => handleCheckRunningAndRecordingVoiceConfEvtMsg(m) - case _ => handleMessageThatAffectsInactivity(msg) + case _ => handleMessageThatAffectsInactivity(msg) } } @@ -297,7 +297,7 @@ class MeetingActor( case m: UserBroadcastCamStartMsg => handleUserBroadcastCamStartMsg(m) case m: UserBroadcastCamStopMsg => handleUserBroadcastCamStopMsg(m) case m: UserJoinedVoiceConfEvtMsg => handleUserJoinedVoiceConfEvtMsg(m) - case m: LogoutAndEndMeetingCmdMsg => usersApp.handleLogoutAndEndMeetingCmdMsg(m, state) + case m: LogoutAndEndMeetingCmdMsg => usersApp.handleLogoutAndEndMeetingCmdMsg(m, state) case m: SetRecordingStatusCmdMsg => state = usersApp.handleSetRecordingStatusCmdMsg(m, state) updateUserLastActivity(m.body.setBy) @@ -346,6 +346,9 @@ class MeetingActor( case m: RespondToPollReqMsg => pollApp.handle(m, liveMeeting, msgBus) updateUserLastActivity(m.body.requesterId) + case m: RespondToTypedPollReqMsg => + pollApp.handle(m, liveMeeting, msgBus) + updateUserLastActivity(m.body.requesterId) // Breakout case m: BreakoutRoomsListMsg => state = handleBreakoutRoomsListMsg(m, state) diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PollsMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PollsMsgs.scala index 6c641d61a2b54fee4f1d25e39c6e3688fbadc449..efc6c00880b011ad5eeb9596739cb8c94c4090e5 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PollsMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PollsMsgs.scala @@ -16,7 +16,7 @@ case class PollShowResultEvtMsgBody(userId: String, pollId: String, poll: Simple object PollStartedEvtMsg { val NAME = "PollStartedEvtMsg" } case class PollStartedEvtMsg(header: BbbClientMsgHeader, body: PollStartedEvtMsgBody) extends BbbCoreMsg -case class PollStartedEvtMsgBody(userId: String, pollId: String, poll: SimplePollOutVO) +case class PollStartedEvtMsgBody(userId: String, pollId: String, pollType: String, question: String, poll: SimplePollOutVO) object PollStoppedEvtMsg { val NAME = "PollStoppedEvtMsg" } case class PollStoppedEvtMsg(header: BbbClientMsgHeader, body: PollStoppedEvtMsgBody) extends BbbCoreMsg @@ -34,21 +34,29 @@ object RespondToPollReqMsg { val NAME = "RespondToPollReqMsg" } case class RespondToPollReqMsg(header: BbbClientMsgHeader, body: RespondToPollReqMsgBody) extends StandardMsg case class RespondToPollReqMsgBody(requesterId: String, pollId: String, questionId: Int, answerId: Int) +object RespondToTypedPollReqMsg { val NAME = "RespondToTypedPollReqMsg" } +case class RespondToTypedPollReqMsg(header: BbbClientMsgHeader, body: RespondToTypedPollReqMsgBody) extends StandardMsg +case class RespondToTypedPollReqMsgBody(requesterId: String, pollId: String, questionId: Int, answer: String) + object UserRespondedToPollRespMsg { val NAME = "UserRespondedToPollRespMsg" } case class UserRespondedToPollRespMsg(header: BbbClientMsgHeader, body: UserRespondedToPollRespMsgBody) extends BbbCoreMsg case class UserRespondedToPollRespMsgBody(pollId: String, userId: String, answerId: Int) +object UserRespondedToTypedPollRespMsg { val NAME = "UserRespondedToTypedPollRespMsg" } +case class UserRespondedToTypedPollRespMsg(header: BbbClientMsgHeader, body: UserRespondedToTypedPollRespMsgBody) extends BbbCoreMsg +case class UserRespondedToTypedPollRespMsgBody(pollId: String, userId: String, answer: String) + object ShowPollResultReqMsg { val NAME = "ShowPollResultReqMsg" } case class ShowPollResultReqMsg(header: BbbClientMsgHeader, body: ShowPollResultReqMsgBody) extends StandardMsg case class ShowPollResultReqMsgBody(requesterId: String, pollId: String) object StartCustomPollReqMsg { val NAME = "StartCustomPollReqMsg" } case class StartCustomPollReqMsg(header: BbbClientMsgHeader, body: StartCustomPollReqMsgBody) extends StandardMsg -case class StartCustomPollReqMsgBody(requesterId: String, pollId: String, pollType: String, answers: Seq[String]) +case class StartCustomPollReqMsgBody(requesterId: String, pollId: String, pollType: String, answers: Seq[String], question: String) object StartPollReqMsg { val NAME = "StartPollReqMsg" } case class StartPollReqMsg(header: BbbClientMsgHeader, body: StartPollReqMsgBody) extends StandardMsg -case class StartPollReqMsgBody(requesterId: String, pollId: String, pollType: String) +case class StartPollReqMsgBody(requesterId: String, pollId: String, pollType: String, question: String) object StopPollReqMsg { val NAME = "StopPollReqMsg" } case class StopPollReqMsg(header: BbbClientMsgHeader, body: StopPollReqMsgBody) extends StandardMsg diff --git a/bigbluebutton-html5/imports/api/polls/server/eventHandlers.js b/bigbluebutton-html5/imports/api/polls/server/eventHandlers.js index a63a0e55bc8c49e520fce7699b350e0bf829adc2..13f8802f7c3cb65784e65462d124246626976f89 100644 --- a/bigbluebutton-html5/imports/api/polls/server/eventHandlers.js +++ b/bigbluebutton-html5/imports/api/polls/server/eventHandlers.js @@ -4,9 +4,11 @@ import handlePollStopped from './handlers/pollStopped'; import handlePollPublished from './handlers/pollPublished'; import handleUserVoted from './handlers/userVoted'; import handleUserResponded from './handlers/userResponded'; +import handleUserTypedResponse from './handlers/userTypedResponse'; RedisPubSub.on('PollShowResultEvtMsg', handlePollPublished); RedisPubSub.on('PollStartedEvtMsg', handlePollStarted); RedisPubSub.on('PollStoppedEvtMsg', handlePollStopped); RedisPubSub.on('PollUpdatedEvtMsg', handleUserVoted); RedisPubSub.on('UserRespondedToPollRespMsg', handleUserResponded); +RedisPubSub.on('UserRespondedToTypedPollRespMsg', handleUserTypedResponse); diff --git a/bigbluebutton-html5/imports/api/polls/server/handlers/pollStarted.js b/bigbluebutton-html5/imports/api/polls/server/handlers/pollStarted.js index 013dba40879556eef379ff8ac3534fcebb19e97f..e1ad34f9c37b9d017bf06f868f56577ad5b844ff 100644 --- a/bigbluebutton-html5/imports/api/polls/server/handlers/pollStarted.js +++ b/bigbluebutton-html5/imports/api/polls/server/handlers/pollStarted.js @@ -3,14 +3,17 @@ import addPoll from '../modifiers/addPoll'; import setPublishedPoll from '../../../meetings/server/modifiers/setPublishedPoll'; export default function pollStarted({ body }, meetingId) { - const { userId } = body; - const { poll } = body; + const { + userId, poll, pollType, question, + } = body; check(meetingId, String); check(userId, String); check(poll, Object); + check(pollType, String); + check(question, String); setPublishedPoll(meetingId, false); - return addPoll(meetingId, userId, poll); + return addPoll(meetingId, userId, poll, pollType, question); } diff --git a/bigbluebutton-html5/imports/api/polls/server/handlers/userTypedResponse.js b/bigbluebutton-html5/imports/api/polls/server/handlers/userTypedResponse.js new file mode 100644 index 0000000000000000000000000000000000000000..eceb10dfbf74dac9623f8a32c6384907e33d485d --- /dev/null +++ b/bigbluebutton-html5/imports/api/polls/server/handlers/userTypedResponse.js @@ -0,0 +1,34 @@ +import { check } from 'meteor/check'; +import Polls from '/imports/api/polls'; +import RedisPubSub from '/imports/startup/server/redis'; + +export default function userTypedResponse({ header, body }) { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'RespondToPollReqMsg'; + + const { pollId, userId, answer } = body; + const { meetingId } = header; + + check(pollId, String); + check(meetingId, String); + check(userId, String); + check(answer, String); + + const poll = Polls.findOne({ meetingId, id: pollId }); + + let answerId = 0; + poll.answers.forEach((a) => { + const { id, key } = a; + if (key === answer) answerId = id; + }); + + const payload = { + requesterId: userId, + pollId, + questionId: 0, + answerId, + }; + + return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, userId, payload); +} diff --git a/bigbluebutton-html5/imports/api/polls/server/methods.js b/bigbluebutton-html5/imports/api/polls/server/methods.js index 5096ad7a7091a0fb527055f66285e0a3ce00bb31..57d7a08246127feef64c5a335ff4f1de46ffd2ac 100644 --- a/bigbluebutton-html5/imports/api/polls/server/methods.js +++ b/bigbluebutton-html5/imports/api/polls/server/methods.js @@ -1,4 +1,5 @@ import { Meteor } from 'meteor/meteor'; +import publishTypedVote from './methods/publishTypedVote'; import publishVote from './methods/publishVote'; import publishPoll from './methods/publishPoll'; import startPoll from './methods/startPoll'; @@ -6,6 +7,7 @@ import stopPoll from './methods/stopPoll'; Meteor.methods({ publishVote, + publishTypedVote, publishPoll, startPoll, stopPoll, diff --git a/bigbluebutton-html5/imports/api/polls/server/methods/publishTypedVote.js b/bigbluebutton-html5/imports/api/polls/server/methods/publishTypedVote.js new file mode 100644 index 0000000000000000000000000000000000000000..ffd29cc537f691ceccee1b84f8d0f7e0c5f92d42 --- /dev/null +++ b/bigbluebutton-html5/imports/api/polls/server/methods/publishTypedVote.js @@ -0,0 +1,53 @@ +import RedisPubSub from '/imports/startup/server/redis'; +import { check } from 'meteor/check'; +import Polls from '/imports/api/polls'; +import { extractCredentials } from '/imports/api/common/server/helpers'; + +export default function publishTypedVote(id, pollAnswer) { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + let EVENT_NAME = 'RespondToTypedPollReqMsg'; + + const { meetingId, requesterUserId } = extractCredentials(this.userId); + + check(pollAnswer, String); + check(id, String); + + const activePoll = Polls.findOne({ meetingId, id }, { + fields: { + answers: 1, + }, + }); + + let existingAnsId = null; + activePoll.answers.forEach((a) => { + if (a.key === pollAnswer) existingAnsId = a.id; + }); + + if (existingAnsId !== null) { + check(existingAnsId, Number); + EVENT_NAME = 'RespondToPollReqMsg'; + + return RedisPubSub.publishUserMessage( + CHANNEL, + EVENT_NAME, + meetingId, + requesterUserId, + { + requesterId: requesterUserId, + pollId: id, + questionId: 0, + answerId: existingAnsId, + }, + ); + } + + const payload = { + requesterId: requesterUserId, + pollId: id, + questionId: 0, + answer: pollAnswer, + }; + + return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); +} diff --git a/bigbluebutton-html5/imports/api/polls/server/methods/startPoll.js b/bigbluebutton-html5/imports/api/polls/server/methods/startPoll.js index f6a08e3dd9045736783f28c8ef4ada7375048bbc..2fe305dd484f5eecb3dc4dae4e67141c25e33f58 100644 --- a/bigbluebutton-html5/imports/api/polls/server/methods/startPoll.js +++ b/bigbluebutton-html5/imports/api/polls/server/methods/startPoll.js @@ -2,7 +2,7 @@ import RedisPubSub from '/imports/startup/server/redis'; import { check } from 'meteor/check'; import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function startPoll(pollType, pollId, answers) { +export default function startPoll(pollType, pollId, question, answers) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; @@ -17,6 +17,7 @@ export default function startPoll(pollType, pollId, answers) { requesterId: requesterUserId, pollId: `${pollId}/${new Date().getTime()}`, pollType, + question, }; if (pollType === 'custom') { diff --git a/bigbluebutton-html5/imports/api/polls/server/modifiers/addPoll.js b/bigbluebutton-html5/imports/api/polls/server/modifiers/addPoll.js index 07d7457f552cc4505f9f2e3407699bf000db8fd5..df2d636081cbdea246cd6e444342c0c04e4f03f2 100644 --- a/bigbluebutton-html5/imports/api/polls/server/modifiers/addPoll.js +++ b/bigbluebutton-html5/imports/api/polls/server/modifiers/addPoll.js @@ -4,7 +4,7 @@ import Logger from '/imports/startup/server/logger'; import flat from 'flat'; import { check } from 'meteor/check'; -export default function addPoll(meetingId, requesterId, poll) { +export default function addPoll(meetingId, requesterId, poll, pollType, question = '') { check(requesterId, String); check(meetingId, String); check(poll, { @@ -37,6 +37,7 @@ export default function addPoll(meetingId, requesterId, poll) { { meetingId }, { requester: requesterId }, { users: userIds }, + { question, pollType }, flat(poll, { safe: true }), ); diff --git a/bigbluebutton-html5/imports/ui/components/poll/component.jsx b/bigbluebutton-html5/imports/ui/components/poll/component.jsx index e99af79f1733ae3cf857796d493e1d51f1c6794e..17f09438d9bf63270a546c33291753c05e8e79ab 100644 --- a/bigbluebutton-html5/imports/ui/components/poll/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/poll/component.jsx @@ -5,6 +5,7 @@ import PresentationUploaderContainer from '/imports/ui/components/presentation/p import { withModalMounter } from '/imports/ui/components/modal/service'; import _ from 'lodash'; import { Session } from 'meteor/session'; +import cx from 'classnames'; import Button from '/imports/ui/components/button/component'; import LiveResult from './live-result/component'; import { styles } from './styles.scss'; @@ -62,25 +63,73 @@ const intlMessages = defineMessages({ id: 'app.poll.tf', description: 'label for true / false poll', }, - yn: { - id: 'app.poll.yn', - description: 'label for Yes / No poll', - }, - a2: { - id: 'app.poll.a2', - description: 'label for A / B poll', - }, - a3: { - id: 'app.poll.a3', - description: 'label for A / B / C poll', - }, a4: { id: 'app.poll.a4', description: 'label for A / B / C / D poll', }, - a5: { - id: 'app.poll.a5', - description: 'label for A / B / C / D / E poll', + delete: { + id: 'app.poll.optionDelete.label', + description: '', + }, + pollPanelDesc: { + id: 'app.poll.panel.desc', + description: '', + }, + questionLabel: { + id: 'app.poll.question.label', + description: '', + }, + userResponse: { + id: 'app.poll.userResponse.label', + description: '', + }, + responseChoices: { + id: 'app.poll.responseChoices.label', + description: '', + }, + typedResponseDesc: { + id: 'app.poll.typedResponse.desc', + description: '', + }, + responseTypesLabel: { + id: 'app.poll.responseTypes.label', + description: '', + }, + addOptionLabel: { + id: 'app.poll.addItem.label', + description: '', + }, + startPollLabel: { + id: 'app.poll.start.label', + description: '', + }, + questionTitle: { + id: 'app.poll.question.title', + description: '', + }, + true: { + id: 'app.poll.answer.true', + description: '', + }, + false: { + id: 'app.poll.answer.false', + description: '', + }, + a: { + id: 'app.poll.answer.a', + description: '', + }, + b: { + id: 'app.poll.answer.b', + description: '', + }, + c: { + id: 'app.poll.answer.c', + description: '', + }, + d: { + id: 'app.poll.answer.d', + description: '', }, }); @@ -93,19 +142,16 @@ class Poll extends Component { super(props); this.state = { - customPollReq: false, isPolling: false, - customPollValues: [], + question: '', + optList: [], }; - this.inputEditor = []; - - this.toggleCustomFields = this.toggleCustomFields.bind(this); - this.renderQuickPollBtns = this.renderQuickPollBtns.bind(this); - this.renderCustomView = this.renderCustomView.bind(this); - this.renderInputFields = this.renderInputFields.bind(this); - this.handleInputChange = this.handleInputChange.bind(this); this.handleBackClick = this.handleBackClick.bind(this); + this.handleAddOption = this.handleAddOption.bind(this); + this.handleRemoveOption = this.handleRemoveOption.bind(this); + this.handleTextareaChange = this.handleTextareaChange.bind(this); + this.handleInputChange = this.handleInputChange.bind(this); } componentDidMount() { @@ -129,112 +175,105 @@ class Poll extends Component { } } - handleInputChange(index, event) { - // This regex will replace any instance of 2 or more consecutive white spaces - // with a single white space character. - const option = event.target.value.replace(/\s{2,}/g, ' ').trim(); - - this.inputEditor[index] = option === '' ? '' : option; - - this.setState({ customPollValues: this.inputEditor }); - } - handleBackClick() { const { stopPoll } = this.props; - Session.set('resetPollPanel', false); - - stopPoll(); - this.inputEditor = []; this.setState({ isPolling: false, - customPollValues: this.inputEditor, - }, document.activeElement.blur()); + }, () => { + stopPoll(); + Session.set('resetPollPanel', false); + document.activeElement.blur(); + }); } - toggleCustomFields() { - const { customPollReq } = this.state; - return this.setState({ customPollReq: !customPollReq }); + handleInputChange(e, index) { + const { optList } = this.state; + const { value } = e.target; + const list = [...optList]; + list[index] = { val: value }; + this.setState({ optList: list }); } - renderQuickPollBtns() { - const { - isMeteorConnected, pollTypes, startPoll, intl, - } = this.props; - - const btns = pollTypes.map((type) => { - if (type === 'custom') return false; - - const label = intl.formatMessage( - // regex removes the - to match the message id - intlMessages[type.replace(/-/g, '').toLowerCase()], - ); - - return ( - <Button - disabled={!isMeteorConnected} - label={label} - color="default" - className={styles.pollBtn} - data-test="pollBtn" - key={_.uniqueId('quick-poll-')} - onClick={() => { - Session.set('pollInitiated', true); - this.setState({ isPolling: true }, () => startPoll(type)); - }} - />); - }); + handleTextareaChange(e) { + this.setState({ question: e.target.value }); + } - return btns; + handleRemoveOption(index) { + const { optList } = this.state; + const list = [...optList]; + list.splice(index, 1); + this.setState({ optList: list }); } - renderCustomView() { - const { intl, startCustomPoll } = this.props; - const isDisabled = _.compact(this.inputEditor).length < 1; + handleAddOption() { + const { optList } = this.state; + this.setState({ optList: [...optList, { val: '' }] }); + } - return ( - <div className={styles.customInputWrapper}> - {this.renderInputFields()} - <Button - onClick={() => { - if (this.inputEditor.length > 0) { - Session.set('pollInitiated', true); - this.setState({ isPolling: true }, () => startCustomPoll('custom', _.compact(this.inputEditor))); - } - }} - label={intl.formatMessage(intlMessages.startCustomLabel)} - color="primary" - aria-disabled={isDisabled} - disabled={isDisabled} - className={styles.btn} - /> - </div> - ); + checkPollType() { + const { type, optList } = this.state; + let _type = type; + let pollString = ''; + let defaultMatch = null; + let isDefault = null; + + switch (_type) { + case 'A-': + pollString = optList.map(x => x.val).sort().join(''); + defaultMatch = pollString.match(/^(ABCDEFG)|(ABCDEF)|(ABCDE)|(ABCD)|(ABC)|(AB)$/gi); + isDefault = defaultMatch && pollString.length === defaultMatch[0].length; + _type = isDefault ? `${_type}${defaultMatch[0].length}` : 'custom'; + break; + case 'TF': + pollString = optList.map(x => x.val).join(''); + defaultMatch = pollString.match(/^(TRUEFALSE)|(FALSETRUE)$/gi); + isDefault = defaultMatch && pollString.length === defaultMatch[0].length; + if (!isDefault) _type = 'custom'; + break; + default: + break; + } + return _type; } - renderInputFields() { + renderInputs() { const { intl } = this.props; - const { customPollValues } = this.state; - let items = []; - - items = _.range(1, MAX_CUSTOM_FIELDS + 1).map((ele, index) => { - const id = index; + const { optList } = this.state; + return optList.map((o, i) => { + const pollOptionKey = `poll-option-${i}`; return ( - <div key={`custom-poll-${id}`} className={styles.pollInput}> + <div + key={pollOptionKey} + style={{ + display: 'flex', + justifyContent: 'spaceBetween', + marginBottom: '1rem', + }} + > <input - aria-label={intl.formatMessage( - intlMessages.ariaInputCount, { 0: id + 1, 1: MAX_CUSTOM_FIELDS }, - )} + type="text" + value={o.val} placeholder={intl.formatMessage(intlMessages.customPlaceholder)} - className={styles.input} - onChange={event => this.handleInputChange(id, event)} - defaultValue={customPollValues[id]} + className={styles.pollOption} + onChange={e => this.handleInputChange(e, i)} maxLength={MAX_INPUT_CHARS} /> + { i > 1 ? ( + <Button + className={styles.deleteBtn} + label={intl.formatMessage(intlMessages.delete)} + icon="delete" + hideLabel + circle + color="default" + onClick={() => { + this.handleRemoveOption(i); + }} + />) : <div style={{ width: '40px' }} /> + } </div> ); }); - - return items; } renderActivePollOptions() { @@ -267,29 +306,143 @@ class Poll extends Component { } renderPollOptions() { - const { isMeteorConnected, intl } = this.props; - const { customPollReq } = this.state; + const { type, optList, question } = this.state; + const { startPoll, startCustomPoll, intl } = this.props; + const defaultPoll = type === 'TF' || type === 'A-'; + + let hasVal = false; + optList.forEach((o) => { + if (o.val.length > 0) hasVal = true; + }); + + const disableStartPoll = (type === 'RP' && question.length === 0) || (!hasVal && type !== 'RP'); return ( <div> <div className={styles.instructions}> - {intl.formatMessage(intlMessages.quickPollInstruction)} + {intl.formatMessage(intlMessages.pollPanelDesc)} </div> - <div className={styles.grid}> - {this.renderQuickPollBtns()} + <div> + <h4>{intl.formatMessage(intlMessages.questionTitle)}</h4> + <textarea + className={styles.pollQuestion} + value={question} + onChange={e => this.handleTextareaChange(e)} + rows="4" + cols="35" + placeholder={intl.formatMessage(intlMessages.questionLabel)} + /> </div> - <div className={styles.instructions}> - {intl.formatMessage(intlMessages.customPollInstruction)} + <div> + <h4>{intl.formatMessage(intlMessages.responseTypesLabel)}</h4> + <div className={styles.responseType}> + <Button + label={intl.formatMessage(intlMessages.tf)} + color="default" + onClick={() => { + this.setState({ + type: 'TF', + optList: [ + { val: intl.formatMessage(intlMessages.true) }, + { val: intl.formatMessage(intlMessages.false) }, + ], + }); + }} + className={cx(styles.pBtn, { [styles.selectedBtn]: type === 'TF' })} + /> + <Button + label={intl.formatMessage(intlMessages.a4)} + color="default" + onClick={() => { + this.setState({ + type: 'A-', + optList: [ + { val: intl.formatMessage(intlMessages.a) }, + { val: intl.formatMessage(intlMessages.b) }, + { val: intl.formatMessage(intlMessages.c) }, + { val: intl.formatMessage(intlMessages.d) }, + ], + }); + }} + className={cx(styles.pBtn, { [styles.selectedBtn]: type === 'A-' })} + /> + </div> + <Button + label={intl.formatMessage(intlMessages.userResponse)} + color="default" + onClick={() => { this.setState({ type: 'RP' }); }} + className={cx(styles.pBtn, styles.fullWidth, { [styles.selectedBtn]: type === 'RP' })} + /> </div> - <Button - disabled={!isMeteorConnected} - className={styles.customBtn} - color="default" - onClick={this.toggleCustomFields} - label={intl.formatMessage(intlMessages.customPollLabel)} - aria-expanded={customPollReq} - /> - {!customPollReq ? null : this.renderCustomView()} + { type + && ( + <div> + <h4>{intl.formatMessage(intlMessages.responseChoices)}</h4> + { + type === 'RP' + && ( + <div> + <span>{intl.formatMessage(intlMessages.typedResponseDesc)}</span> + <div className={styles.exampleResponse}> + <div className={styles.exampleTitle} /> + <div className={styles.responseInput}> + <div className={styles.rInput} /> + </div> + </div> + </div> + ) + } + { + (defaultPoll || type === 'RP') + && ( + <div style={{ + display: 'flex', + flexFlow: 'column', + }} + > + {defaultPoll && this.renderInputs()} + {defaultPoll + && ( + <Button + className={styles.addItemBtn} + label={intl.formatMessage(intlMessages.addOptionLabel)} + color="default" + icon="add" + disabled={optList.length === MAX_CUSTOM_FIELDS} + onClick={() => this.handleAddOption()} + /> + ) + } + <Button + className={styles.startPollBtn} + label={intl.formatMessage(intlMessages.startPollLabel)} + disabled={disableStartPoll} + color="primary" + onClick={() => { + this.setState({ isPolling: true }, () => { + const verifiedPollType = this.checkPollType(); + const verifiedOptions = optList.map((o) => { + if (o.val.length > 0) return o.val; + return null; + }); + if (verifiedPollType === 'custom') { + startCustomPoll( + verifiedPollType, + question, + _.compact(verifiedOptions), + ); + } else { + startPoll(verifiedPollType, question); + } + }); + }} + /> + </div> + ) + } + </div> + ) + } </div> ); } @@ -346,18 +499,13 @@ class Poll extends Component { icon="left_arrow" aria-label={intl.formatMessage(intlMessages.hidePollDesc)} className={styles.hideBtn} - onClick={() => { - Session.set('openPanel', 'userlist'); - }} + onClick={() => { Session.set('openPanel', 'userlist'); }} /> - <Button label={intl.formatMessage(intlMessages.closeLabel)} aria-label={`${intl.formatMessage(intlMessages.closeLabel)} ${intl.formatMessage(intlMessages.pollPaneTitle)}`} onClick={() => { - if (currentPoll) { - stopPoll(); - } + if (currentPoll) stopPoll(); Session.set('openPanel', 'userlist'); Session.set('forcePollOpen', false); Session.set('pollInitiated', false); @@ -367,11 +515,8 @@ class Poll extends Component { size="sm" hideLabel /> - </header> - { - this.renderPollPanel() - } + {this.renderPollPanel()} </div> ); } diff --git a/bigbluebutton-html5/imports/ui/components/poll/container.jsx b/bigbluebutton-html5/imports/ui/components/poll/container.jsx index ef3d4f003b84298dbaedec41d82b36e54a2a95c6..0a2833157a3cc2743c69decdf74598616dfe2900 100644 --- a/bigbluebutton-html5/imports/ui/components/poll/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/poll/container.jsx @@ -22,9 +22,9 @@ export default withTracker(() => { const pollId = currentSlide ? currentSlide.id : PUBLIC_CHAT_KEY; - const startPoll = type => makeCall('startPoll', type, pollId); + const startPoll = (type, question = '') => makeCall('startPoll', type, pollId, question); - const startCustomPoll = (type, answers) => makeCall('startPoll', type, pollId, answers); + const startCustomPoll = (type, question = '', answers) => makeCall('startPoll', type, pollId, question, answers); const stopPoll = () => makeCall('stopPoll'); diff --git a/bigbluebutton-html5/imports/ui/components/poll/live-result/component.jsx b/bigbluebutton-html5/imports/ui/components/poll/live-result/component.jsx index 0aa6f2ace9fb8bc84bb35c61ec1e8962807ad0b4..df12b2f498da69cf54af288d0fba50686436069b 100644 --- a/bigbluebutton-html5/imports/ui/components/poll/live-result/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/poll/live-result/component.jsx @@ -186,7 +186,7 @@ class LiveResult extends PureComponent { {waiting ? <span className={styles.connectingAnimation} /> : null} </div> - {currentPoll + {currentPoll && currentPoll.answers.length > 0 ? ( <Button disabled={!isMeteorConnected} diff --git a/bigbluebutton-html5/imports/ui/components/poll/styles.scss b/bigbluebutton-html5/imports/ui/components/poll/styles.scss index f129c63e09414791a4900b495d34f6fde1788afe..4125467b183e2a0b3be2c4ea4c7d7ec75bf9a8a8 100644 --- a/bigbluebutton-html5/imports/ui/components/poll/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/poll/styles.scss @@ -6,6 +6,8 @@ --poll-column-amount: 2; --poll-blue: #1A73D4; --poll-header-offset: -0.875rem; + --poll-addItem-width: 6rem; + --poll-input-height: 2.5rem; } .closeBtn { @@ -91,7 +93,6 @@ } .instructions { - margin-top: var(--lg-padding-x); margin-bottom: var(--lg-padding-x); color: var(--color-text); } @@ -120,7 +121,7 @@ } .pollBtn:nth-child(odd) { - margin-right: var(--sm-padding-y); + margin-right: 1rem; margin-left: inherit; [dir="rtl"] & { @@ -201,16 +202,143 @@ font-size: var(--font-size-small); border: 1px solid var(--color-gray-lighter); border-radius: var(--border-radius); - padding: .3rem * 1 .3rem * 0.25; - padding-left: var(--lg-padding-y); - - [dir="rtl"] & { - padding: .3rem * 1 .3rem * 0.25; - padding-right:var(--lg-padding-y); - } } .noSlidePanelContainer { color: var(--color-gray-drak); text-align: center; -} \ No newline at end of file +} + +.responseType { + display: flex; + justify-content: space-between; + position: relative; + width: 100%; + height: var(--poll-input-height); + margin-bottom: var(--lg-padding-x); + + button { + position: relative; + width: 47%; + } +} + +.fullWidth { + width: 100%; +} + +.pollQuestion { + resize: none; +} + +.pollOption { + margin-right: 1rem; + + [dir="rtl"] & { + margin-right: 0; + margin-left: 1rem; + } +} + +.pollQuestion, +.pollOption { + @include inputFocus(var(--color-blue-light)); + + width: 100%; + color: var(--color-text); + -webkit-appearance: none; + padding: calc(var(--sm-padding-y) * 2) calc(var(--sm-padding-x) * 1); + border-radius: var(--border-radius); + font-size: var(--font-size-base); + border: 1px solid var(--color-gray-lighter); + box-shadow: 0 0 0 1px var(--color-gray-lighter); +} + +.exampleTitle { + background-color: var(--color-gray-lightest); + height: var(--sm-padding-x); + border-radius: var(--border-radius); + width: 75%; +} + +.exampleResponse { + border: var(--color-gray-light) solid 1px; + border-radius: var(--border-radius); + padding: 1rem; + margin-top: 1rem; + + .responseInput { + border: var(--color-gray-light) solid 1px; + border-radius: var(--border-radius); + margin-top: 1rem; + height: var(--poll-input-height); + padding-top: var(--sm-padding-x); + padding-left: var(--sm-padding-x); + position: relative; + } +} + +.rInput { + background-color: var(--color-gray-lightest); + height: var(--lg-padding-y); + border-radius: var(--border-radius); + width: 75%; +} + +.addItemBtn { + top: 1px; + position: relative; + display: block; + width: var(--poll-addItem-width); + padding-left: var(--md-padding-y); + padding-right: var(--md-padding-y); + + &:hover { + span { + opacity: 1; + } + } +} + +.startPollBtn { + position: relative; + width: 100%; + height: var(--poll-input-height); + margin-top: 1rem; + + &:hover { + span { + opacity: 1; + } + } +} + +.pBtn { + border: solid var(--color-gray-light) 1px; + height: var(--poll-input-height); + span { + &:hover { + opacity: 1; + } + } +} + +.deleteBtn { + position: relative; + i { + font-size: 150%; + } +} + +.selectedBtn { + background-color: var(--poll-blue); + color: var(--color-white); + + &:hover, + &:focus, + &:active { + background-color: var(--poll-blue) !important; + box-shadow: none !important; + color: var(--color-white) !important; + } +} diff --git a/bigbluebutton-html5/imports/ui/components/polling/component.jsx b/bigbluebutton-html5/imports/ui/components/polling/component.jsx index c0e7219a2e0c5d489de596adc565d91b9a585b99..e553ea4b15c13ab954ee829f9e69033cbb958e6e 100644 --- a/bigbluebutton-html5/imports/ui/components/polling/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/polling/component.jsx @@ -16,12 +16,28 @@ const intlMessages = defineMessages({ pollAnswerDesc: { id: 'app.polling.pollAnswerDesc', }, + pollQestionTitle: { + id: 'app.polling.pollQestionTitle', + }, + submitLabel: { + id: 'app.polling.submitLabel', + }, + submitAriaLabel: { + id: 'app.polling.submitAriaLabel', + }, + responsePlaceholder: { + id: 'app.polling.responsePlaceholder', + }, }); class Polling extends Component { constructor(props) { super(props); + this.state = { + typedAns: '', + }; + this.play = this.play.bind(this); } @@ -40,9 +56,15 @@ class Polling extends Component { intl, poll, handleVote, + handleTypedVote, pollAnswerIds, } = this.props; - const { stackOptions, answers } = poll; + + const { + typedAns, + } = this.state; + + const { stackOptions, answers, question } = poll; const pollAnswerStyles = { [styles.pollingAnswers]: true, [styles.removeColumns]: answers.length === 1, @@ -58,49 +80,88 @@ class Polling extends Component { })} role="alert" > - <div className={styles.pollingTitle}> - {intl.formatMessage(intlMessages.pollingTitleLabel)} - </div> - <div className={cx(pollAnswerStyles)}> - {poll.answers.map((pollAnswer) => { - const formattedMessageIndex = pollAnswer.key.toLowerCase(); - let label = pollAnswer.key; - if (pollAnswerIds[formattedMessageIndex]) { - label = intl.formatMessage(pollAnswerIds[formattedMessageIndex]); + {question.length > 0 && ( + <span className={styles.qHeader}> + <div className={styles.qTitle}>{intl.formatMessage(intlMessages.pollQestionTitle)}</div> + <div className={styles.qText}>{question}</div> + </span>) + } + { poll.pollType !== 'RP' + && ( + <span> + {question.length === 0 + && ( + <div className={styles.pollingTitle}> + {intl.formatMessage(intlMessages.pollingTitleLabel)} + </div> + ) } - return ( - <div - key={pollAnswer.id} - className={styles.pollButtonWrapper} - > - <Button - disabled={!isMeteorConnected} - className={styles.pollingButton} - color="primary" - size="md" - label={label} - key={pollAnswer.key} - onClick={() => handleVote(poll.pollId, pollAnswer)} - aria-labelledby={`pollAnswerLabel${pollAnswer.key}`} - aria-describedby={`pollAnswerDesc${pollAnswer.key}`} - /> - <div - className={styles.hidden} - id={`pollAnswerLabel${pollAnswer.key}`} - > - {intl.formatMessage(intlMessages.pollAnswerLabel, { 0: label })} - </div> - <div - className={styles.hidden} - id={`pollAnswerDesc${pollAnswer.key}`} - > - {intl.formatMessage(intlMessages.pollAnswerDesc, { 0: label })} - </div> - </div> - ); - })} - </div> + <div className={cx(pollAnswerStyles)}> + {poll.answers.map((pollAnswer) => { + const formattedMessageIndex = pollAnswer.key.toLowerCase(); + let label = pollAnswer.key; + if (pollAnswerIds[formattedMessageIndex]) { + label = intl.formatMessage(pollAnswerIds[formattedMessageIndex]); + } + + return ( + <div + key={pollAnswer.id} + className={styles.pollButtonWrapper} + > + <Button + disabled={!isMeteorConnected} + className={styles.pollingButton} + color="primary" + size="md" + label={label} + key={pollAnswer.key} + onClick={() => handleVote(poll.pollId, pollAnswer)} + aria-labelledby={`pollAnswerLabel${pollAnswer.key}`} + aria-describedby={`pollAnswerDesc${pollAnswer.key}`} + /> + <div + className={styles.hidden} + id={`pollAnswerLabel${pollAnswer.key}`} + > + {intl.formatMessage(intlMessages.pollAnswerLabel, { 0: label })} + </div> + <div + className={styles.hidden} + id={`pollAnswerDesc${pollAnswer.key}`} + > + {intl.formatMessage(intlMessages.pollAnswerDesc, { 0: label })} + </div> + </div> + ); + })} + </div> + </span> + ) + } + { poll.pollType === 'RP' + && ( + <div className={styles.typedResponseWrapper}> + <input + onChange={e => this.setState({ typedAns: e.target.value })} + type="text" + className={styles.typedResponseInput} + placeholder={intl.formatMessage(intlMessages.responsePlaceholder)} + /> + <Button + disabled={typedAns.length === 0} + color="primary" + size="sm" + label={intl.formatMessage(intlMessages.submitLabel)} + aria-label={intl.formatMessage(intlMessages.submitAriaLabel)} + onClick={() => { + handleTypedVote(poll.pollId, typedAns); + }} + /> + </div> + ) + } </div> </div>); } diff --git a/bigbluebutton-html5/imports/ui/components/polling/container.jsx b/bigbluebutton-html5/imports/ui/components/polling/container.jsx index 92353cba9d5e26a57087191b0e1e17cfd0309377..14f8455ce8eea77aceea510d14fa40a01c894d68 100644 --- a/bigbluebutton-html5/imports/ui/components/polling/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/polling/container.jsx @@ -16,6 +16,7 @@ const POLLING_ENABLED = Meteor.settings.public.poll.enabled; const PollingContainer = ({ pollExists, ...props }) => { const currentUser = Users.findOne({ userId: Auth.userID }, { fields: { presenter: 1 } }); const showPolling = pollExists && !currentUser.presenter && POLLING_ENABLED; + if (showPolling) { return ( <PollingComponent {...props} /> @@ -27,10 +28,13 @@ const PollingContainer = ({ pollExists, ...props }) => { PollingContainer.propTypes = propTypes; export default withTracker(() => { - const { pollExists, handleVote, poll } = PollingService.mapPolls(); + const { + pollExists, handleVote, poll, handleTypedVote, + } = PollingService.mapPolls(); return ({ pollExists, handleVote, + handleTypedVote, poll, pollAnswerIds: PollService.pollAnswerIds, isMeteorConnected: Meteor.status().connected, diff --git a/bigbluebutton-html5/imports/ui/components/polling/service.js b/bigbluebutton-html5/imports/ui/components/polling/service.js index 40c241ec099877c9d4f5019a707b307811ad0478..0a6accb7b8a5111acf10e0148b250c130cb533ba 100644 --- a/bigbluebutton-html5/imports/ui/components/polling/service.js +++ b/bigbluebutton-html5/imports/ui/components/polling/service.js @@ -26,13 +26,18 @@ const mapPolls = () => { poll: { answers: poll.answers, pollId: poll.id, + pollType: poll.pollType, stackOptions, + question: poll.question, }, pollExists: true, amIRequester, handleVote(pollId, answerId) { makeCall('publishVote', pollId, answerId.id); }, + handleTypedVote(pollId, answer) { + makeCall('publishTypedVote', pollId, answer); + }, }; }; diff --git a/bigbluebutton-html5/imports/ui/components/polling/styles.scss b/bigbluebutton-html5/imports/ui/components/polling/styles.scss index 0e806213fc28fbe5d71c1b57541ed31a19af07d8..1fc1c873548d76362c039929834acc7e99c59a70 100644 --- a/bigbluebutton-html5/imports/ui/components/polling/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/polling/styles.scss @@ -1,3 +1,4 @@ +@import "/imports/ui/stylesheets/mixins/focus"; @import "../../stylesheets/variables/breakpoints"; :root { @@ -5,7 +6,7 @@ --max-btn-width: 9em; --overlayIndex: 9999; --overlayOpacity: 0.349; - --pollIndex: 1016; + --poll-index: 1016; --poll-width: 18rem; } @@ -27,7 +28,7 @@ min-width: var(--poll-width); position: absolute; - z-index: var(--pollIndex); + z-index: var(--poll-index); border: 1px solid var(--color-off-white); border-radius: var(--border-radius); box-shadow: var(--color-gray-dark) 0px 0px var(--lg-padding-y); @@ -58,10 +59,10 @@ } .pollingTitle { - color: var(--color-text); white-space: nowrap; padding-bottom: var(--md-padding-y); padding-top: var(--md-padding-y); + font-size: var(--font-size-small); } .pollButtonWrapper { @@ -124,3 +125,37 @@ .hidden { display: none; } + +.qHeader { + text-align: left; + position: relative; + left: var(--sm-padding-y); +} + +.qTitle { + font-size: var(--font-size-small); +} + +.qText { + color: var(--color-text); + word-break: break-word; + font-size: var(--font-size-large); +} + +.typedResponseWrapper { + margin: var(--jumbo-padding-y) .5rem .5rem .5rem; + display: flex; + flex-flow: column; +} + +.typedResponseInput { + @include inputFocus(var(--color-blue-light)); + color: var(--color-text); + -webkit-appearance: none; + padding: calc(var(--sm-padding-y) * 2.5) calc(var(--sm-padding-x) * 1.25); + border-radius: var(--border-radius); + font-size: var(--font-size-base); + border: 1px solid var(--color-gray-lighter); + box-shadow: 0 0 0 1px var(--color-gray-lighter); + margin-bottom: 1rem; +} diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index 783ad8410b59ae3ba1b6a75d084c7e033969403a..d82b03a01b23eb74509334ef4c423020ec874806 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -225,6 +225,16 @@ "app.poll.customPlaceholder": "Add poll option", "app.poll.noPresentationSelected": "No presentation selected! Please select one.", "app.poll.clickHereToSelect": "Click here to select", + "app.poll.panel.desc" : "Fill out your polling details below.", + "app.poll.question.label" : "Write your question..", + "app.poll.userResponse.label" : "User Repsonse", + "app.poll.responseTypes.label" : "Response Types", + "app.poll.optionDelete.label" : "Delete", + "app.poll.responseChoices.label" : "Response Choices", + "app.poll.typedResponse.desc" : "Users will be presented with a text box to fill in their response.", + "app.poll.addItem.label" : "Add item", + "app.poll.question.title": "Ask a question", + "app.poll.start.label" : "Start Poll", "app.poll.t": "True", "app.poll.f": "False", "app.poll.tf": "True / False", @@ -247,6 +257,10 @@ "app.poll.liveResult.usersTitle": "Users", "app.poll.liveResult.responsesTitle": "Response", "app.polling.pollingTitle": "Polling options", + "app.polling.pollQestionTitle": "Polling Qestion", + "app.polling.submitLabel": "Submit", + "app.polling.submitAriaLabel": "Submit poll response", + "app.polling.responsePlaceholder": "Enter answer", "app.polling.pollAnswerLabel": "Poll answer {0}", "app.polling.pollAnswerDesc": "Select this option to vote for {0}", "app.failedMessage": "Apologies, trouble connecting to the server.",