diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/HidePollResultReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/HidePollResultReqMsgHdlr.scala
deleted file mode 100755
index ecf2b445167d1f8289e6860405d2ee6813fdd994..0000000000000000000000000000000000000000
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/HidePollResultReqMsgHdlr.scala
+++ /dev/null
@@ -1,37 +0,0 @@
-package org.bigbluebutton.core.apps.polls
-
-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.apps.{ PermissionCheck, RightsManagementTrait }
-
-trait HidePollResultReqMsgHdlr extends RightsManagementTrait {
-  this: PollApp2x =>
-
-  def handle(msg: HidePollResultReqMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
-
-    def broadcastEvent(msg: HidePollResultReqMsg, hiddenPollId: String): Unit = {
-      val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, liveMeeting.props.meetingProp.intId, msg.header.userId)
-      val envelope = BbbCoreEnvelope(PollHideResultEvtMsg.NAME, routing)
-      val header = BbbClientMsgHeader(PollHideResultEvtMsg.NAME, liveMeeting.props.meetingProp.intId, msg.header.userId)
-
-      val body = PollHideResultEvtMsgBody(msg.header.userId, hiddenPollId)
-      val event = PollHideResultEvtMsg(header, body)
-      val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
-      bus.outGW.send(msgEvent)
-    }
-
-    if (permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
-      val meetingId = liveMeeting.props.meetingProp.intId
-      val reason = "No permission to hide poll result."
-      PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
-    } else {
-      for {
-        hiddenPollId <- Polls.handleHidePollResultReqMsg(msg.header.userId, msg.body.pollId, liveMeeting)
-      } yield {
-        broadcastEvent(msg, hiddenPollId)
-      }
-    }
-  }
-}
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 1df0e98f51710d9f30694c33103785954edac991..4d8fa41d1a26b668e369e49b4e1d8a5656e91772 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
@@ -5,7 +5,6 @@ import akka.event.Logging
 
 class PollApp2x(implicit val context: ActorContext)
     extends GetCurrentPollReqMsgHdlr
-    with HidePollResultReqMsgHdlr
     with RespondToPollReqMsgHdlr
     with ShowPollResultReqMsgHdlr
     with StartCustomPollReqMsgHdlr
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 bf2658dc25b4ce74ea8cfe53b1228c90b797bf47..9da5a3a79a2708fb7d0dadbc0363a70c23fcb92d 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
@@ -12,22 +12,34 @@ trait RespondToPollReqMsgHdlr {
   def handle(msg: RespondToPollReqMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
     log.debug("Received RespondToPollReqMsg {}", RespondToPollReqMsg)
 
-    def broadcastEvent(msg: RespondToPollReqMsg, stoppedPollId: String, presenterId: String, poll: SimplePollResultOutVO): Unit = {
+    def broadcastPollUpdatedEvent(msg: RespondToPollReqMsg, pollId: String, poll: SimplePollResultOutVO): Unit = {
       val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, liveMeeting.props.meetingProp.intId, msg.header.userId)
-      val envelope = BbbCoreEnvelope(UserRespondedToPollEvtMsg.NAME, routing)
-      val header = BbbClientMsgHeader(UserRespondedToPollEvtMsg.NAME, 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 = UserRespondedToPollEvtMsgBody(presenterId, stoppedPollId, poll)
-      val event = UserRespondedToPollEvtMsg(header, body)
+      val body = PollUpdatedEvtMsgBody(pollId, poll)
+      val event = PollUpdatedEvtMsg(header, body)
+      val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
+      bus.outGW.send(msgEvent)
+    }
+
+    def broadcastUserRespondedToPollRecordMsg(msg: RespondToPollReqMsg, pollId: String, answerId: Int): Unit = {
+      val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, liveMeeting.props.meetingProp.intId, msg.header.userId)
+      val envelope = BbbCoreEnvelope(UserRespondedToPollRecordMsg.NAME, routing)
+      val header = BbbClientMsgHeader(UserRespondedToPollRecordMsg.NAME, liveMeeting.props.meetingProp.intId, msg.header.userId)
+
+      val body = UserRespondedToPollRecordMsgBody(pollId, answerId)
+      val event = UserRespondedToPollRecordMsg(header, body)
       val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
       bus.outGW.send(msgEvent)
     }
 
     for {
-      (curPresenterId: String, pollId: String, updatedPoll: SimplePollResultOutVO) <- Polls.handleRespondToPollReqMsg(msg.header.userId, msg.body.pollId,
+      (pollId: String, updatedPoll: SimplePollResultOutVO) <- Polls.handleRespondToPollReqMsg(msg.header.userId, msg.body.pollId,
         msg.body.questionId, msg.body.answerId, liveMeeting)
     } yield {
-      broadcastEvent(msg, pollId, curPresenterId, updatedPoll)
+      broadcastPollUpdatedEvent(msg, pollId, updatedPoll)
+      broadcastUserRespondedToPollRecordMsg(msg, pollId, msg.body.answerId)
     }
   }
 }
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 9830d1a6d25edcd25dce49db71365c8caedbb2ae..3f63772d18e569fb0dca4aeb8c450b7aca74913d 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
@@ -79,15 +79,6 @@ object Polls {
     }
   }
 
-  def handleHidePollResultReqMsg(requesterId: String, pollId: String, lm: LiveMeeting): Option[String] = {
-    for {
-      poll <- getPoll(pollId, lm.polls)
-    } yield {
-      hidePollResult(pollId, lm.polls)
-      pollId
-    }
-  }
-
   def handleGetCurrentPollReqMsg(state: MeetingState2x, requesterId: String, lm: LiveMeeting): Option[PollVO] = {
     val poll = for {
       pod <- state.presentationPodManager.getDefaultPod()
@@ -111,14 +102,13 @@ object Polls {
   }
 
   def handleRespondToPollReqMsg(requesterId: String, pollId: String, questionId: Int, answerId: Int,
-                                lm: LiveMeeting): Option[(String, String, SimplePollResultOutVO)] = {
+                                lm: LiveMeeting): Option[(String, SimplePollResultOutVO)] = {
 
     for {
-      curPres <- Users2x.findPresenter(lm.users2x)
       poll <- getSimplePollResult(pollId, lm.polls)
       pvo <- handleRespondToPoll(poll, requesterId, pollId, questionId, answerId, lm)
     } yield {
-      (curPres.intId, pollId, pvo)
+      (pollId, pvo)
     }
 
   }
@@ -297,14 +287,6 @@ object Polls {
     pvo
   }
 
-  def hidePollResult(pollId: String, polls: Polls) {
-    polls.get(pollId) foreach {
-      p =>
-        p.hideResult()
-        polls.currentPoll = None
-    }
-  }
-
   def showPollResult(pollId: String, polls: Polls) {
     polls.get(pollId) foreach {
       p =>
@@ -460,7 +442,6 @@ class Poll(val id: String, val questions: Array[Question], val numRespondents: I
   private var _numResponders: Int = 0
 
   def showingResult() { _showResult = true }
-  def hideResult() { _showResult = false }
   def showResult(): Boolean = { _showResult }
   def start() { _started = true }
   def stop() { _stopped = true }
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 d4fbc00b1898eda7234f3b4cab119ff06c1fff15..915fb94ffdd863a49de22df9e20599ebab15f807 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
@@ -98,8 +98,6 @@ class ReceivedJsonMsgHandlerActor(
         routeGenericMsg[StopPollReqMsg](envelope, jsonNode)
       case ShowPollResultReqMsg.NAME =>
         routeGenericMsg[ShowPollResultReqMsg](envelope, jsonNode)
-      case HidePollResultReqMsg.NAME =>
-        routeGenericMsg[HidePollResultReqMsg](envelope, jsonNode)
       case GetCurrentPollReqMsg.NAME =>
         routeGenericMsg[GetCurrentPollReqMsg](envelope, jsonNode)
       case RespondToPollReqMsg.NAME =>
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/AbstractPollRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/AbstractPollRecordEvent.scala
new file mode 100755
index 0000000000000000000000000000000000000000..fcbbf714bcac6d0e018fae825b54d7f40a493678
--- /dev/null
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/AbstractPollRecordEvent.scala
@@ -0,0 +1,34 @@
+/**
+ * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
+ *
+ * Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
+ *
+ * This program is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation; either version 3.0 of the License, or (at your option) any later
+ * version.
+ *
+ * BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along
+ * with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.bigbluebutton.core.record.events
+
+trait AbstractPollRecordEvent extends RecordEvent {
+  import AbstractPollRecordEvent._
+
+  setModule("POLL")
+
+  def setPollId(pollId: String) {
+    eventMap.put(POLL_ID, pollId)
+  }
+}
+
+object AbstractPollRecordEvent {
+  protected final val POLL_ID = "pollId"
+}
\ No newline at end of file
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/PollStartedRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/PollStartedRecordEvent.scala
new file mode 100755
index 0000000000000000000000000000000000000000..a7ce1973ad151f4682390906e2b91990e9564489
--- /dev/null
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/PollStartedRecordEvent.scala
@@ -0,0 +1,42 @@
+/**
+ * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
+ *
+ * Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
+ *
+ * This program is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation; either version 3.0 of the License, or (at your option) any later
+ * version.
+ *
+ * BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along
+ * with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.bigbluebutton.core.record.events
+
+import org.bigbluebutton.common2.domain.SimpleAnswerOutVO
+import org.bigbluebutton.common2.util.JsonUtil
+
+class PollStartedRecordEvent extends AbstractPollRecordEvent {
+  import PollStartedRecordEvent._
+
+  setEvent("PollStartedRecordEvent")
+
+  def setUserId(userId: String) {
+    eventMap.put(USER_ID, userId)
+  }
+
+  def setAnswers(answers: Array[SimpleAnswerOutVO]) {
+    eventMap.put(ANSWERS, JsonUtil.toJson(answers))
+  }
+}
+
+object PollStartedRecordEvent {
+  protected final val USER_ID = "userId"
+  protected final val ANSWERS = "answers"
+}
\ No newline at end of file
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/PollStoppedRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/PollStoppedRecordEvent.scala
new file mode 100755
index 0000000000000000000000000000000000000000..216b7f5a46ec2886ed4a9df2c15bd18bf3c17c14
--- /dev/null
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/PollStoppedRecordEvent.scala
@@ -0,0 +1,24 @@
+/**
+ * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
+ *
+ * Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
+ *
+ * This program is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation; either version 3.0 of the License, or (at your option) any later
+ * version.
+ *
+ * BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along
+ * with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.bigbluebutton.core.record.events
+
+class PollStoppedRecordEvent extends AbstractPollRecordEvent {
+  setEvent("PollStoppedRecordEvent")
+}
\ No newline at end of file
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/UserRespondedToPollRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/UserRespondedToPollRecordEvent.scala
new file mode 100755
index 0000000000000000000000000000000000000000..ea955895b7a88215db241f9a7370b3cfdf285f7f
--- /dev/null
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/UserRespondedToPollRecordEvent.scala
@@ -0,0 +1,39 @@
+/**
+ * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
+ *
+ * Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
+ *
+ * This program is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation; either version 3.0 of the License, or (at your option) any later
+ * version.
+ *
+ * BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along
+ * with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.bigbluebutton.core.record.events
+
+class UserRespondedToPollRecordEvent extends AbstractPollRecordEvent {
+  import UserRespondedToPollRecordEvent._
+
+  setEvent("UserRespondedToPollRecordEvent")
+
+  def setUserId(userId: String) {
+    eventMap.put(USER_ID, userId)
+  }
+
+  def setAnswerId(answerId: Int) {
+    eventMap.put(ANSWER_ID, Integer.toString(answerId))
+  }
+}
+
+object UserRespondedToPollRecordEvent {
+  protected final val USER_ID = "userId"
+  protected final val ANSWER_ID = "answerId"
+}
\ No newline at end of file
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 260694d5ad0d39fff35d676be9db81924bcb86f3..3639db2d299ff8666f81132a21a505addb7c26b6 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
@@ -237,7 +237,6 @@ class MeetingActor(
       case m: StartCustomPollReqMsg => pollApp.handle(m, state, liveMeeting, msgBus) // passing state but not modifying it
       case m: StopPollReqMsg => pollApp.handle(m, state, liveMeeting, msgBus) // passing state but not modifying it
       case m: ShowPollResultReqMsg => pollApp.handle(m, state, liveMeeting, msgBus) // passing state but not modifying it
-      case m: HidePollResultReqMsg => pollApp.handle(m, liveMeeting, msgBus)
       case m: GetCurrentPollReqMsg => pollApp.handle(m, state, liveMeeting, msgBus) // passing state but not modifying it
       case m: RespondToPollReqMsg => pollApp.handle(m, liveMeeting, msgBus)
 
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/FromAkkaAppsMsgSenderActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/FromAkkaAppsMsgSenderActor.scala
index edfa5d2430351f11fef46471bf17a271a69b251f..31503a9f7ce74c23d3795052937d802246a9fcdd 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/FromAkkaAppsMsgSenderActor.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/FromAkkaAppsMsgSenderActor.scala
@@ -94,7 +94,15 @@ class FromAkkaAppsMsgSenderActor(msgSender: MessageSender)
         msgSender.send(fromAkkaAppsPresRedisChannel, json)
       //==================================================================
 
-      case _ => msgSender.send(fromAkkaAppsRedisChannel, json)
+      //==================================================================
+      // Some events are only intended for recording and shouldn't be 
+      // sent past akka-apps
+      // Poll Record Event
+      case UserRespondedToPollRecordMsg.NAME =>
+      //==================================================================
+
+      case _ =>
+        msgSender.send(fromAkkaAppsRedisChannel, json)
     }
   }
 }
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala
index c519c96cd8b2a87e0bc3890a171b4dbb7f0a9069..020e622e1b02ed6fdcf8767fd8863b1c89412d94 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala
@@ -99,6 +99,12 @@ class RedisRecorderActor(val system: ActorSystem)
       // Recording
       case m: RecordingChapterBreakSysMsg           => handleRecordingChapterBreakSysMsg(m)
 
+      // Poll
+      case m: PollStartedEvtMsg                     => handlePollStartedEvtMsg(m)
+      case m: UserRespondedToPollRecordMsg          => handleUserRespondedToPollRecordMsg(m)
+      case m: PollStoppedEvtMsg                     => handlePollStoppedEvtMsg(m)
+      case m: PollShowResultEvtMsg                  => handlePollShowResultEvtMsg(m)
+
       case _                                        => // message not to be recorded.
     }
   }
@@ -460,4 +466,36 @@ class RedisRecorderActor(val system: ActorSystem)
 
     record(msg.header.meetingId, ev.toMap)
   }
+
+  private def handlePollStartedEvtMsg(msg: PollStartedEvtMsg): Unit = {
+    val ev = new PollStartedRecordEvent()
+    ev.setPollId(msg.body.pollId)
+    ev.setAnswers(msg.body.poll.answers)
+
+    record(msg.header.meetingId, ev.toMap)
+  }
+
+  private def handleUserRespondedToPollRecordMsg(msg: UserRespondedToPollRecordMsg): Unit = {
+    val ev = new UserRespondedToPollRecordEvent()
+    ev.setPollId(msg.body.pollId)
+    ev.setUserId(msg.header.userId)
+    ev.setAnswerId(msg.body.answerId)
+
+    record(msg.header.meetingId, ev.toMap)
+  }
+
+  private def handlePollStoppedEvtMsg(msg: PollStoppedEvtMsg): Unit = {
+    pollStoppedRecordHelper(msg.header.meetingId, msg.body.pollId)
+  }
+
+  private def handlePollShowResultEvtMsg(msg: PollShowResultEvtMsg): Unit = {
+    pollStoppedRecordHelper(msg.header.meetingId, msg.body.pollId)
+  }
+
+  private def pollStoppedRecordHelper(meetingId: String, pollId: String): Unit = {
+    val ev = new PollStoppedRecordEvent()
+    ev.setPollId(pollId)
+
+    record(meetingId, ev.toMap)
+  }
 }
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 7b42199fc2fbd986b07bc5b18d876f633b04d748..f120999fce08376cd2fec5da5ffabd831631d614 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
@@ -12,14 +12,6 @@ object GetCurrentPollRespMsg { val NAME = "GetCurrentPollRespMsg" }
 case class GetCurrentPollRespMsg(header: BbbClientMsgHeader, body: GetCurrentPollRespMsgBody) extends BbbCoreMsg
 case class GetCurrentPollRespMsgBody(userId: String, hasPoll: Boolean, poll: Option[PollVO])
 
-object HidePollResultReqMsg { val NAME = "HidePollResultReqMsg" }
-case class HidePollResultReqMsg(header: BbbClientMsgHeader, body: HidePollResultReqMsgBody) extends StandardMsg
-case class HidePollResultReqMsgBody(requesterId: String, pollId: String)
-
-object PollHideResultEvtMsg { val NAME = "PollHideResultEvtMsg" }
-case class PollHideResultEvtMsg(header: BbbClientMsgHeader, body: PollHideResultEvtMsgBody) extends BbbCoreMsg
-case class PollHideResultEvtMsgBody(userId: String, pollId: String)
-
 object PollShowResultEvtMsg { val NAME = "PollShowResultEvtMsg" }
 case class PollShowResultEvtMsg(header: BbbClientMsgHeader, body: PollShowResultEvtMsgBody) extends BbbCoreMsg
 case class PollShowResultEvtMsgBody(userId: String, pollId: String, poll: SimplePollResultOutVO)
@@ -32,6 +24,14 @@ object PollStoppedEvtMsg { val NAME = "PollStoppedEvtMsg" }
 case class PollStoppedEvtMsg(header: BbbClientMsgHeader, body: PollStoppedEvtMsgBody) extends BbbCoreMsg
 case class PollStoppedEvtMsgBody(userId: String, pollId: String)
 
+object PollUpdatedEvtMsg { val NAME = "PollUpdatedEvtMsg" }
+case class PollUpdatedEvtMsg(header: BbbClientMsgHeader, body: PollUpdatedEvtMsgBody) extends BbbCoreMsg
+case class PollUpdatedEvtMsgBody(pollId: String, poll: SimplePollResultOutVO)
+
+object UserRespondedToPollRecordMsg { val NAME = "UserRespondedToPollRecordMsg" }
+case class UserRespondedToPollRecordMsg(header: BbbClientMsgHeader, body: UserRespondedToPollRecordMsgBody) extends BbbCoreMsg
+case class UserRespondedToPollRecordMsgBody(pollId: String, answerId: Int)
+
 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)
@@ -50,10 +50,4 @@ case class StartPollReqMsgBody(requesterId: String, pollId: String, pollType: St
 
 object StopPollReqMsg { val NAME = "StopPollReqMsg" }
 case class StopPollReqMsg(header: BbbClientMsgHeader, body: StopPollReqMsgBody) extends StandardMsg
-case class StopPollReqMsgBody(requesterId: String)
-
-object UserRespondedToPollEvtMsg { val NAME = "UserRespondedToPollEvtMsg" }
-case class UserRespondedToPollEvtMsg(header: BbbClientMsgHeader, body: UserRespondedToPollEvtMsgBody) extends BbbCoreMsg
-case class UserRespondedToPollEvtMsgBody(presenterId: String, pollId: String, poll: SimplePollResultOutVO)
-
-
+case class StopPollReqMsgBody(requesterId: String)
\ No newline at end of file
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 fa2e8eced8419a2565f453f1fdd9db0007689631..bb30401ef7e327de04e01acac00bf65b6d98bcd0 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
@@ -71,6 +71,9 @@ public class ParamsProcessorUtil {
     private int defaultNumDigitsForTelVoice;
     private String defaultClientUrl;
     private String defaultGuestWaitURL;
+    private String html5ClientUrl;
+    private Boolean moderatorsJoinViaHTML5Client;
+    private Boolean attendeesJoinViaHTML5Client;
     private String defaultAvatarURL;
     private String defaultConfigURL;
     private String defaultGuestPolicy;
@@ -500,7 +503,18 @@ public class ParamsProcessorUtil {
 
 	public String getDefaultGuestWaitURL() {
 		return defaultGuestWaitURL;
-		//return defaultServerUrl + "/guestWait";
+        }
+
+	public String getHTML5ClientUrl() {
+		return html5ClientUrl;
+	}
+
+	public Boolean getAttendeesJoinViaHTML5Client() {
+		return attendeesJoinViaHTML5Client;
+	}
+
+	public Boolean getModeratorsJoinViaHTML5Client() {
+		return moderatorsJoinViaHTML5Client;
 	}
 
 	public String getDefaultConfigXML() {
@@ -823,6 +837,18 @@ public class ParamsProcessorUtil {
 
 	public void setDefaultGuestWaitURL(String url) {
 		this.defaultGuestWaitURL = url;
+        }
+
+	public void setHtml5ClientUrl(String html5ClientUrl) {
+		this.html5ClientUrl = html5ClientUrl;
+	}
+
+	public void setModeratorsJoinViaHTML5Client(Boolean moderatorsJoinViaHTML5Client) {
+		this.moderatorsJoinViaHTML5Client = moderatorsJoinViaHTML5Client;
+	}
+
+	public void setAttendeesJoinViaHTML5Client(Boolean attendeesJoinViaHTML5Client) {
+		this.attendeesJoinViaHTML5Client = attendeesJoinViaHTML5Client;
 	}
 
 	public void setDefaultMeetingDuration(int defaultMeetingDuration) {
diff --git a/bigbluebutton-client/locale/en_US/bbbResources.properties b/bigbluebutton-client/locale/en_US/bbbResources.properties
index 81773877ad34c1f7dbaa593d3123bf58bb8f5b6d..bfb9e22c55f63f0f893d63a7532edbaa377f10c6 100755
--- a/bigbluebutton-client/locale/en_US/bbbResources.properties
+++ b/bigbluebutton-client/locale/en_US/bbbResources.properties
@@ -62,6 +62,7 @@ bbb.micSettings.webrtc.waitingforice = Connecting
 bbb.micSettings.webrtc.transferring = Transferring
 bbb.micSettings.webrtc.endingecho = Joining audio
 bbb.micSettings.webrtc.endedecho = Echo test ended.
+bbb.micPermissions.message.browserhttp = This server is not configured with SSL. As a result, {0} disables sharing of your microphone.
 bbb.micPermissions.firefox.title = Firefox Microphone Permissions
 bbb.micPermissions.firefox.message = Click Allow to give Firefox permission to use your microphone.
 bbb.micPermissions.chrome.title = Chrome Microphone Permissions
diff --git a/bigbluebutton-client/resources/prod/BigBlueButton.html b/bigbluebutton-client/resources/prod/BigBlueButton.html
index c0670b32c1774c62f023d3305faf4260898e7a47..00e1a24d794c710c2b55c52d4e2d685b319a13fc 100755
--- a/bigbluebutton-client/resources/prod/BigBlueButton.html
+++ b/bigbluebutton-client/resources/prod/BigBlueButton.html
@@ -194,15 +194,15 @@
     <div id="accessibile-progress" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" class="visually-hidden">0 %</div>
     <button id="enterFlash" type="button" class="visually-hidden" onclick="startFlashFocus();">Set focus to client</button>
     <div id="content">
-      <div id="altFlash"  style="width:50%; margin-left: auto; margin-right: auto; ">
-        You need Adobe Flash installed and enabled in order to use this client.
+      <div id="altFlash"  style="width:50%; margin-left: auto; margin-right: auto; font-family: sans-serif; text-align: center;">
+        <p style="font-weight: bold;">You need Adobe Flash installed and enabled in order to use this client.</p>
         <br/>
-        <div style="width:50%; margin-left: auto; margin-right: auto; ">
+        <div style="width:50%; margin-left: auto; margin-right: auto;">
           <a href="http://www.adobe.com/go/getflashplayer">
             <img src="get_flash_player.gif" alt="Get Adobe Flash player" />
           </a>
           <div id="html5Section" style="display:none">
-            <p style="margin-left:50px;" >OR</p>
+            <p>OR</p>
             <button type="button" onclick="html5();"><h3>Launch the HTML5 client instead</h3></button>
           </div>
         </div>
diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/events/ShortcutEvent.as b/bigbluebutton-client/src/org/bigbluebutton/main/events/ShortcutEvent.as
old mode 100644
new mode 100755
index c795de2182bea54f65656b5b1a332d7a34a27f63..a54db46ec2b78d174418a607a7f338180a28dc1f
--- a/bigbluebutton-client/src/org/bigbluebutton/main/events/ShortcutEvent.as
+++ b/bigbluebutton-client/src/org/bigbluebutton/main/events/ShortcutEvent.as
@@ -52,8 +52,6 @@ package org.bigbluebutton.main.events {
 		public static const FOCUS_SHARED_NOTES_WINDOW:String = 'FOCUS_SHARED_NOTES_WINDOW';
 		
 		public static const REMOTE_FOCUS_DESKTOP:String = 'REMOTE_FOCUS_DESKTOP';
-		public static const REMOTE_FOCUS_WEBCAM:String = 'REMOTE_FOCUS_WEBCAM';
-		// Remote focus microphone not necessary; audio options already hog focus
 		
 		public static const REMOTE_OPEN_SHORTCUT_WIN:String = 'REMOTE_OPEN_SHORTCUT_WIN';
 		public static const LOGOUT:String = 'LOGOUT';
diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/AudioSelectionWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/AudioSelectionWindow.mxml
index 96342523040b0e904a5674908f3b2cc516ce2b47..ff59f04db347a9ea6e248fa283c9ef997bf4b10e 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/main/views/AudioSelectionWindow.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/AudioSelectionWindow.mxml
@@ -35,6 +35,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 		<![CDATA[
 			import com.asfusion.mate.events.Dispatcher;
 			
+			import mx.controls.Alert;
+			
 			import org.as3commons.logging.api.ILogger;
 			import org.as3commons.logging.api.getClassLogger;
 			import org.bigbluebutton.core.Options;
@@ -78,11 +80,18 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 					vboxListen.percentWidth = 100;
 				}
 			}
+			
+			private function browserForcesHTTPS() : Boolean {
+				var result : Boolean = !BrowserCheck.isHttps() && ((BrowserCheck.isChrome() && BrowserCheck.browserMajorVersion >= "60") || (BrowserCheck.isOpera() && BrowserCheck.browserMajorVersion >= "47"));
+				return result;
+			}
 
 			private function onMicClick():void {
 				LOGGER.debug("AudioSelectionWindow - Share Microphone Clicked");
 				var dispatcher:Dispatcher = new Dispatcher();
-				if (BrowserCheck.isPuffin46AndAbove() || (!BrowserCheck.isHttps() && ((BrowserCheck.isChrome() && BrowserCheck.browserMajorVersion >= "60") || (BrowserCheck.isOpera() && BrowserCheck.browserMajorVersion >= "47")))) {
+				if (browserForcesHTTPS()) {
+					Alert.show(ResourceUtil.getInstance().getString("bbb.micPermissions.message.browserhttp", [BrowserCheck.browserName]));
+				} else if (BrowserCheck.isPuffin46AndAbove()) {
 					dispatcher.dispatchEvent(new UseFlashModeCommand());
 				} else {
 					var command:JoinVoiceConferenceCommand = new JoinVoiceConferenceCommand();
diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatTab.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatTab.mxml
index aa5c8d2336313ed5c3b342b847f8bcd9854f57e5..dcadaf14c871708a452bc0f0b14f0b33f6585071 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatTab.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatTab.mxml
@@ -527,6 +527,30 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
         }
       }
 
+      private function processChatMessage(message:String):ChatMessageVO {
+        var sanitizedMessage:String = ExternalInterface.call('parseURLs', (ChatUtil.cleanup(message)));
+        if (sanitizedMessage == null || sanitizedMessage.length < 1) {
+          return null;
+        }
+        
+        var cm:ChatMessageVO = new ChatMessageVO();
+        cm.fromUserId = UsersUtil.getMyUserID();
+        cm.fromUsername = UsersUtil.getMyUsername();
+        // get the color value from ColorPicker
+        cm.fromColor = cmpColorPicker.selectedColor.toString();
+        
+        // Get the current UTC time and the timezone for this sender.
+        // The receiver will have to convert this to local time.
+        var now:Date = new Date();
+        cm.fromTime = now.valueOf();
+        cm.fromTimezoneOffset = now.getTimezoneOffset();
+        
+        //cm.message = ChatUtil.parseURLs(ChatUtil.cleanup(message));
+        cm.message = sanitizedMessage;
+        
+        return cm;
+      }
+      
       private function handleTextInput(e:TextEvent):void {
         if ((e.text.length == 1) && (e.text.charCodeAt(0) == 10) /*ENTER-KEY*/ && messageCanBeSent) {
           sendMessages();
diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/polling/events/ShowPollResultEvent.as b/bigbluebutton-client/src/org/bigbluebutton/modules/polling/events/ShowPollResultEvent.as
index e5df9b040e039d5d0f6903ea0ef271b83fa70bd1..67bbdf3e84f0da8f2fa87418356ec851b227759a 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/modules/polling/events/ShowPollResultEvent.as
+++ b/bigbluebutton-client/src/org/bigbluebutton/modules/polling/events/ShowPollResultEvent.as
@@ -7,12 +7,9 @@ package org.bigbluebutton.modules.polling.events
     
     public static const SHOW_POLL_RESULT:String = "show poll result";
     
-    public var show: Boolean;
-    
-    public function ShowPollResultEvent(show:Boolean)
+    public function ShowPollResultEvent()
     {
       super(SHOW_POLL_RESULT, true, false);
-      this.show = show;
     }
   }
 }
\ No newline at end of file
diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/polling/service/IPollDataService.as b/bigbluebutton-client/src/org/bigbluebutton/modules/polling/service/IPollDataService.as
index e3b163703ae839928695679aaf67489d86636803..302baa615590aa05eed7e2452da877797b6c420a 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/modules/polling/service/IPollDataService.as
+++ b/bigbluebutton-client/src/org/bigbluebutton/modules/polling/service/IPollDataService.as
@@ -12,6 +12,6 @@ package org.bigbluebutton.modules.polling.service
     
     function votePoll(pollId:String, answerId:Number):void;
     
-    function showPollResult(pollId:String, show:Boolean):void;
+    function showPollResult(pollId:String):void;
   }
 }
\ No newline at end of file
diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/polling/service/MessageReceiver.as b/bigbluebutton-client/src/org/bigbluebutton/modules/polling/service/MessageReceiver.as
index e120058ec79c6f169e1c6fc598413c4db9a4bae6..ebb02dbbae3d1b520ca7d7af548f2d807b209c75 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/modules/polling/service/MessageReceiver.as
+++ b/bigbluebutton-client/src/org/bigbluebutton/modules/polling/service/MessageReceiver.as
@@ -51,8 +51,8 @@ package org.bigbluebutton.modules.polling.service
         case "PollStoppedEvtMsg":
           processor.handlePollStoppedMesage(message);
           break;
-        case "UserRespondedToPollEvtMsg":
-          processor.handlePollUserVotedMessage(message);
+        case "PollUpdatedEvtMsg":
+          processor.handlePollUpdatedMessage(message);
           break;
       }
     }
diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/polling/service/MessageSender.as b/bigbluebutton-client/src/org/bigbluebutton/modules/polling/service/MessageSender.as
index c59cf4d26e75847f76f453a59179326ccaab2683..53a17d3a4c1058d1fcdeb3a322c6ee54fd7b09ff 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/modules/polling/service/MessageSender.as
+++ b/bigbluebutton-client/src/org/bigbluebutton/modules/polling/service/MessageSender.as
@@ -98,11 +98,8 @@ package org.bigbluebutton.modules.polling.service
       );
     }
     
-    public function showPollResult(pollId:String, show:Boolean):void {
+    public function showPollResult(pollId:String):void {
       var messageName:String = "ShowPollResultReqMsg";
-      if (!show) {
-        messageName = "HidePollResultReqMsg";
-      }
 
       var message:Object = {
         header: {name: messageName, meetingId: UsersUtil.getInternalMeetingID(), userId: UsersUtil.getMyUserID()},
diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/polling/service/NetworkPollDataService.as b/bigbluebutton-client/src/org/bigbluebutton/modules/polling/service/NetworkPollDataService.as
index 07273dc2a752f478bff096b2efa28f328f01323b..c1f06710243688b54502492759a5071bc5da02bb 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/modules/polling/service/NetworkPollDataService.as
+++ b/bigbluebutton-client/src/org/bigbluebutton/modules/polling/service/NetworkPollDataService.as
@@ -33,9 +33,9 @@ package org.bigbluebutton.modules.polling.service
       sender.votePoll(pollId, answerId);
     }
     
-    public function showPollResult(pollId:String, show:Boolean):void
+    public function showPollResult(pollId:String):void
     {
-      sender.showPollResult(pollId, show);
+      sender.showPollResult(pollId);
     }
   }
 }
\ No newline at end of file
diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/polling/service/PollDataProcessor.as b/bigbluebutton-client/src/org/bigbluebutton/modules/polling/service/PollDataProcessor.as
index 76a85a3be32e394c2bfd731b3347ca05c2f7e2b6..09830da93bea980d4719572ed8abfd21fbe56616 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/modules/polling/service/PollDataProcessor.as
+++ b/bigbluebutton-client/src/org/bigbluebutton/modules/polling/service/PollDataProcessor.as
@@ -102,7 +102,7 @@ package org.bigbluebutton.modules.polling.service
       }
     }
     
-    public function handlePollUserVotedMessage(msg:Object):void {
+    public function handlePollUpdatedMessage(msg:Object):void {
       var pollId:String = msg.body.pollId;
       var poll:Object = msg.body.poll;
       var answers:Array = poll.answers as Array;
diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/polling/service/PollingService.as b/bigbluebutton-client/src/org/bigbluebutton/modules/polling/service/PollingService.as
index aef9ea0afa98bbc0553400095b9ff314f3e6c1dd..2ae88c9ce9b8b0dbff85f35da6ecc3d6de5d435e 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/modules/polling/service/PollingService.as
+++ b/bigbluebutton-client/src/org/bigbluebutton/modules/polling/service/PollingService.as
@@ -96,7 +96,7 @@ package org.bigbluebutton.modules.polling.service
     
     public function handleShowPollResultEvent(event:ShowPollResultEvent):void {
       var curPoll:SimplePoll = model.getCurrentPoll();
-      dataService.showPollResult(curPoll.id, event.show);
+      dataService.showPollResult(curPoll.id);
     }
     
 
diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/polling/views/PollResultsModal.as b/bigbluebutton-client/src/org/bigbluebutton/modules/polling/views/PollResultsModal.as
old mode 100644
new mode 100755
index d17267aa69c3efc3bdf44da5bb5f4e8c4e0fd49e..e66866b4b79d8049fdcfe335e17db03579964a3a
--- a/bigbluebutton-client/src/org/bigbluebutton/modules/polling/views/PollResultsModal.as
+++ b/bigbluebutton-client/src/org/bigbluebutton/modules/polling/views/PollResultsModal.as
@@ -176,7 +176,7 @@ package org.bigbluebutton.modules.polling.views
 		
 		private function handlePublishClick(e:MouseEvent):void {
 			var dispatcher:Dispatcher = new Dispatcher();
-			dispatcher.dispatchEvent(new ShowPollResultEvent(true));
+			dispatcher.dispatchEvent(new ShowPollResultEvent());
 			close();
 		}
 		
diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/ToolbarPopupButton.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/ToolbarPopupButton.mxml
index 468dff4a0c4f934c8ed51b5490706c3eeeee5a2a..dff2ecf6ffc08c5d08c8d9c15746cda5b37f444c 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/ToolbarPopupButton.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/ToolbarPopupButton.mxml
@@ -149,7 +149,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 
 			public function remoteClick(e:ShortcutEvent):void{
 				openPublishWindow();
-				dispatchEvent(new ShortcutEvent(ShortcutEvent.REMOTE_FOCUS_WEBCAM));
 			}
 			
 			public function publishingStatus(status:Number, camID:Number = -1):void {
@@ -186,6 +185,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 			}
 
 			private function openPublishWindow():void{
+				if (LiveMeeting.inst().me.disableMyCam) {
+					return;
+				}
 				if (browserForcesHTTPS()) {
 					return;
 				}
diff --git a/bigbluebutton-html5/client/main.html b/bigbluebutton-html5/client/main.html
old mode 100755
new mode 100644
index fb01946b07731e6b8c834346614839b865261b5b..8344297a488e8a26ce5370de565ce88f34071844
--- a/bigbluebutton-html5/client/main.html
+++ b/bigbluebutton-html5/client/main.html
@@ -11,6 +11,8 @@
     }
 
     body {
+      position: absolute;
+      height: 100%;
       font-family: 'Source Sans Pro', Arial, sans-serif;
       font-size: 1rem; /* 16px */
       background-color: #06172A;
@@ -21,7 +23,7 @@
     }
 
     #app {
-      height: 100vh;
+      height: 100%;
       width: 100vw;
       overflow: hidden;
     }
diff --git a/bigbluebutton-html5/imports/api/annotations/server/modifiers/clearAnnotations.js b/bigbluebutton-html5/imports/api/annotations/server/modifiers/clearAnnotations.js
index c543326780c7a7a8a4fb78ec23a629e87c2b27d6..c8af5d888c7ac81a2a13c0e39a53d8dcd486562a 100644
--- a/bigbluebutton-html5/imports/api/annotations/server/modifiers/clearAnnotations.js
+++ b/bigbluebutton-html5/imports/api/annotations/server/modifiers/clearAnnotations.js
@@ -21,15 +21,19 @@ export default function clearAnnotations(meetingId, whiteboardId, userId) {
       return Logger.error(`Removing Annotations from collection: ${err}`);
     }
 
-    if (!meetingId) {
-      return Logger.info('Cleared Annotations (all)');
+    if (userId) {
+      return Logger.info(`Cleared Annotations for userId=${userId} where whiteboard=${whiteboardId}`);
     }
 
-    if (userId) {
-      return Logger.info(`Removed Annotations for userId=${userId} where whiteboard=${whiteboardId}`);
+    if (whiteboardId) {
+      return Logger.info(`Cleared Annotations for whiteboard=${whiteboardId}`);
+    }
+
+    if (meetingId) {
+      return Logger.info(`Cleared Annotations (${meetingId})`);
     }
 
-    return Logger.info(`Removed Annotations where whiteboard=${whiteboardId}`);
+    return Logger.info('Cleared Annotations (all)');
   };
 
   return Annotations.remove(selector, cb);
diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
index 271b8e5a3634373776114af6a4b076535e3f9f75..4889aad28ad3dfa23a0f9dc8c8c3cb5e4d6bf910 100644
--- a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
+++ b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
@@ -56,7 +56,9 @@ export default class SIPBridge extends BaseAudioBridge {
     this.protocol = window.document.location.protocol;
     this.hostname = window.document.location.hostname;
 
-    const causes = window.SIP.C.causes;
+    const {
+      causes,
+    } = window.SIP.C;
 
     this.errorCodes = {
       [causes.REQUEST_TIMEOUT]: this.baseErrorCodes.REQUEST_TIMEOUT,
@@ -76,14 +78,14 @@ export default class SIPBridge extends BaseAudioBridge {
       this.callback = callback;
 
       return this.doCall({ callExtension, isListenOnly, inputStream })
-                 .catch((reason) => {
-                   callback({
-                     status: this.baseCallStates.failed,
-                     error: this.baseErrorCodes.GENERIC_ERROR,
-                     bridgeError: reason,
-                   });
-                   reject(reason);
-                 });
+        .catch((reason) => {
+          callback({
+            status: this.baseCallStates.failed,
+            error: this.baseErrorCodes.GENERIC_ERROR,
+            bridgeError: reason,
+          });
+          reject(reason);
+        });
     });
   }
 
@@ -108,9 +110,9 @@ export default class SIPBridge extends BaseAudioBridge {
     this.callOptions = options;
 
     return fetchStunTurnServers(sessionToken)
-                        .then(this.createUserAgent.bind(this))
-                        .then(this.inviteUserAgent.bind(this))
-                        .then(this.setupEventHandlers.bind(this));
+      .then(this.createUserAgent.bind(this))
+      .then(this.inviteUserAgent.bind(this))
+      .then(this.setupEventHandlers.bind(this));
   }
 
   transferCall(onTransferSuccess) {
@@ -123,7 +125,8 @@ export default class SIPBridge extends BaseAudioBridge {
         this.callback({
           status: this.baseCallStates.failed,
           error: this.baseErrorCodes.REQUEST_TIMEOUT,
-          bridgeError: 'Timeout on call transfer' });
+          bridgeError: 'Timeout on call transfer',
+        });
         reject(this.baseErrorCodes.REQUEST_TIMEOUT);
       }, CALL_TRANSFER_TIMEOUT);
 
@@ -195,9 +198,9 @@ export default class SIPBridge extends BaseAudioBridge {
       let userAgent = new window.SIP.UA({
         uri: `sip:${encodeURIComponent(callerIdName)}@${hostname}`,
         wsServers: `${(protocol === 'https:' ? 'wss://' : 'ws://')}${hostname}/ws`,
-        // log: {
-        //   builtinEnabled: false,
-        // },
+        log: {
+          builtinEnabled: false,
+        },
         displayName: callerIdName,
         register: false,
         traceSip: true,
@@ -220,7 +223,8 @@ export default class SIPBridge extends BaseAudioBridge {
         this.callback({
           status: this.baseCallStates.failed,
           error: this.baseErrorCodes.CONNECTION_ERROR,
-          bridgeError: 'User Agent Disconnected' });
+          bridgeError: 'User Agent Disconnected',
+        });
         reject(this.baseErrorCodes.CONNECTION_ERROR);
       };
 
@@ -283,8 +287,8 @@ export default class SIPBridge extends BaseAudioBridge {
         }
 
         const mappedCause = cause in this.errorCodes ?
-                            this.errorCodes[cause] :
-                            this.baseErrorCodes.GENERIC_ERROR;
+          this.errorCodes[cause] :
+          this.baseErrorCodes.GENERIC_ERROR;
 
         return this.callback({
           status: this.baseCallStates.failed,
@@ -301,43 +305,43 @@ export default class SIPBridge extends BaseAudioBridge {
     });
   }
 
-  getMediaStream(constraints) {
-    return navigator.mediaDevices.getUserMedia(constraints).catch((err) => {
-      console.error(err);
-      throw new Error(this.baseErrorCodes.MEDIA_ERROR);
-    });
-  }
+  setDefaultInputDevice() {
+    const handleMediaSuccess = (mediaStream) => {
+      const deviceLabel = mediaStream.getAudioTracks()[0].label;
+      return navigator.mediaDevices.enumerateDevices().then((mediaDevices) => {
+        const device = mediaDevices.find(d => d.label === deviceLabel);
+        return this.changeInputDevice(device.deviceId);
+      });
+    };
 
-  async setDefaultInputDevice() {
-    const mediaStream = await this.getMediaStream({ audio: true });
-    const deviceLabel = mediaStream.getAudioTracks()[0].label;
-    const mediaDevices = await navigator.mediaDevices.enumerateDevices();
-    const device = mediaDevices.find(d => d.label === deviceLabel);
-    return this.changeInputDevice(device.deviceId);
+    return navigator.mediaDevices.getUserMedia({ audio: true }).then(handleMediaSuccess);
   }
 
-  async changeInputDevice(value) {
+  changeInputDevice(value) {
     const {
       media,
     } = this;
 
     if (media.inputDevice.audioContext) {
-      media.inputDevice.audioContext.close().then(() => {
+      const handleAudioContextCloseSuccess = () => {
         media.inputDevice.audioContext = null;
         media.inputDevice.scriptProcessor = null;
         media.inputDevice.source = null;
         return this.changeInputDevice(value);
-      });
+      };
+
+      return media.inputDevice.audioContext.close().then(handleAudioContextCloseSuccess);
     }
 
-    media.inputDevice.id = value;
     if ('AudioContext' in window) {
       media.inputDevice.audioContext = new window.AudioContext();
     } else {
       media.inputDevice.audioContext = new window.webkitAudioContext();
     }
+
+    media.inputDevice.id = value;
     media.inputDevice.scriptProcessor = media.inputDevice.audioContext
-                                              .createScriptProcessor(2048, 1, 1);
+      .createScriptProcessor(2048, 1, 1);
     media.inputDevice.source = null;
 
     const constraints = {
@@ -346,13 +350,17 @@ export default class SIPBridge extends BaseAudioBridge {
       },
     };
 
-    const mediaStream = await this.getMediaStream(constraints);
-    media.inputDevice.stream = mediaStream;
-    media.inputDevice.source = media.inputDevice.audioContext.createMediaStreamSource(mediaStream);
-    media.inputDevice.source.connect(media.inputDevice.scriptProcessor);
-    media.inputDevice.scriptProcessor.connect(media.inputDevice.audioContext.destination);
+    const handleMediaSuccess = (mediaStream) => {
+      media.inputDevice.stream = mediaStream;
+      media.inputDevice.source = media.inputDevice.audioContext
+        .createMediaStreamSource(mediaStream);
+      media.inputDevice.source.connect(media.inputDevice.scriptProcessor);
+      media.inputDevice.scriptProcessor.connect(media.inputDevice.audioContext.destination);
+
+      return this.media.inputDevice;
+    };
 
-    return this.media.inputDevice;
+    return navigator.mediaDevices.getUserMedia(constraints).then(handleMediaSuccess);
   }
 
   async changeOutputDevice(value) {
@@ -368,6 +376,6 @@ export default class SIPBridge extends BaseAudioBridge {
       }
     }
 
-    return this.media.outputDeviceId;
+    return this.media.outputDeviceId || value;
   }
 }
diff --git a/bigbluebutton-html5/imports/api/chat/server/handlers/chatPublicHistoryClear.js b/bigbluebutton-html5/imports/api/chat/server/handlers/chatPublicHistoryClear.js
index 47d861bac4ec979cb0ed8d614ed756842f956826..3f29aab4c1931619204c3d2b3015fbc0c6ff06b6 100644
--- a/bigbluebutton-html5/imports/api/chat/server/handlers/chatPublicHistoryClear.js
+++ b/bigbluebutton-html5/imports/api/chat/server/handlers/chatPublicHistoryClear.js
@@ -10,19 +10,20 @@ export default function publicHistoryClear({ header }, meetingId) {
   const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system;
 
   if (meetingId) {
-    Chat.remove({ meetingId, toUserId: PUBLIC_CHAT_USERID },
-      Logger.info(`Cleared Chats (${meetingId})`));
+    Chat.remove(
+      { meetingId, toUserId: PUBLIC_CHAT_USERID },
+      Logger.info(`Cleared Chats (${meetingId})`),
+    );
 
     addChat(meetingId, {
-      message: '<b><i>The public chat history was cleared by a moderator</i></b>',
+      message: CHAT_CONFIG.system_messages_keys.chat_clear,
       fromTime: new Date().getTime(),
       toUserId: PUBLIC_CHAT_USERID,
       toUsername: PUBLIC_CHAT_USERNAME,
       fromUserId: SYSTEM_CHAT_TYPE,
       fromUsername: '',
       fromColor: '',
-    },
-    );
+    });
   }
 
   return null;
diff --git a/bigbluebutton-html5/imports/api/users/server/methods/assignPresenter.js b/bigbluebutton-html5/imports/api/users/server/methods/assignPresenter.js
index 80dba64643971dd8958da9a66b8a7f4810a87c7c..8b4727e31cd468ce2a998bfb3caa59f82a8524b7 100644
--- a/bigbluebutton-html5/imports/api/users/server/methods/assignPresenter.js
+++ b/bigbluebutton-html5/imports/api/users/server/methods/assignPresenter.js
@@ -21,8 +21,7 @@ export default function assignPresenter(credentials, userId) {
   });
 
   if (!User) {
-    throw new Meteor.Error(
-      'user-not-found', 'You need a valid user to be able to set presenter');
+    throw new Meteor.Error('user-not-found', 'You need a valid user to be able to set presenter');
   }
 
   const payload = {
@@ -35,5 +34,5 @@ export default function assignPresenter(credentials, userId) {
   Logger.verbose(`User '${userId}' setted as presenter by '${
     requesterUserId}' from meeting '${meetingId}'`);
 
-  return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, userId, payload);
+  return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
 }
diff --git a/bigbluebutton-html5/imports/api/users/server/methods/kickUser.js b/bigbluebutton-html5/imports/api/users/server/methods/kickUser.js
index b5ab3b8a81f8774742c8f94b1cce79e5e4576918..b9aed12653043a833600551d435f4be75a96ccbf 100644
--- a/bigbluebutton-html5/imports/api/users/server/methods/kickUser.js
+++ b/bigbluebutton-html5/imports/api/users/server/methods/kickUser.js
@@ -18,5 +18,5 @@ export default function kickUser(credentials, userId) {
     ejectedBy: requesterUserId,
   };
 
-  return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, userId, payload);
+  return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
 }
diff --git a/bigbluebutton-html5/imports/api/voice-users/server/methods/muteToggle.js b/bigbluebutton-html5/imports/api/voice-users/server/methods/muteToggle.js
index 16f7fa82cc32e30a21fcc64a85c6eb8531322791..ea1d4dedd33eacfab09d102f91d291917435a43a 100644
--- a/bigbluebutton-html5/imports/api/voice-users/server/methods/muteToggle.js
+++ b/bigbluebutton-html5/imports/api/voice-users/server/methods/muteToggle.js
@@ -18,12 +18,7 @@ export default function muteToggle(credentials, userId) {
   check(meetingId, String);
   check(requesterUserId, String);
 
-  const payload = {
-    userId,
-    mutedBy: requesterUserId,
-  };
-
-  const user = Users.findOne({
+  const requester = Users.findOne({
     meetingId,
     userId: requesterUserId,
   });
@@ -32,20 +27,25 @@ export default function muteToggle(credentials, userId) {
     intId: userId,
   });
 
-  if (!user || !voiceUser) return;
-
-  const isListenOnly = voiceUser.listenOnly;
+  if (!requester || !voiceUser) return;
 
-  if (isListenOnly) return;
+  const { listenOnly, muted } = voiceUser;
+  if (listenOnly) return;
 
-  const isModerator = user.roles.includes(ROLE_MODERATOR.toLowerCase());
-  const isMuted = voiceUser.muted;
+  const isModerator = requester.roles.includes(ROLE_MODERATOR.toLowerCase());
   const isNotHimself = requesterUserId !== userId;
 
+  // the ability for a moderator to unmute other users is configurable (on/off)
   if (!ALLOW_MODERATOR_TO_UNMUTE_AUDIO &&
     isModerator &&
-    isMuted &&
+    muted &&
     isNotHimself) return;
 
-  return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, userId, payload);
+  const payload = {
+    userId,
+    mutedBy: requesterUserId,
+    mute: !muted,
+  };
+
+  RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
 }
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 4cfa5c6b62a1c0d0f394b60003679a81f057048d..0c71b45665cec3428751f1d6d33ba73eb1398463 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx
@@ -11,6 +11,7 @@ import DropdownListItem from '/imports/ui/components/dropdown/list/item/componen
 
 import PresentationUploaderContainer from '/imports/ui/components/presentation/presentation-uploader/container';
 import { withModalMounter } from '/imports/ui/components/modal/service';
+import styles from '../styles';
 
 const propTypes = {
   isUserPresenter: PropTypes.bool.isRequired,
@@ -57,11 +58,12 @@ class ActionsDropdown extends Component {
     if (!isUserPresenter) return null;
 
     return (
-      <Dropdown ref={(ref) => { this._dropdown = ref; }}>
-        <DropdownTrigger tabIndex={0}>
+      <Dropdown ref={(ref) => { this._dropdown = ref; }} >
+        <DropdownTrigger tabIndex={0} >
           <Button
+            className={styles.button}
             label={intl.formatMessage(intlMessages.actionsLabel)}
-            icon="add"
+            icon="plus"
             color="primary"
             size="lg"
             circle
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
index ad405b15fd069caf4e9ba80d590cf90fb0bdf962..74482af8407f3fe4d6d71c3eb91d014b6a897779 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
@@ -1,11 +1,14 @@
 import React from 'react';
 import styles from './styles.scss';
-import EmojiContainer from './emoji-menu/container';
+import EmojiSelect from './emoji-select/component';
 import ActionsDropdown from './actions-dropdown/component';
 import AudioControlsContainer from '../audio/audio-controls/container';
 
 const ActionsBar = ({
   isUserPresenter,
+  emojiList,
+  emojiSelected,
+  handleEmojiChange,
 }) => (
   <div className={styles.actionsbar}>
     <div className={styles.left}>
@@ -14,7 +17,7 @@ const ActionsBar = ({
     <div className={styles.center}>
       <AudioControlsContainer />
       {/* <JoinVideo /> */}
-      <EmojiContainer />
+      <EmojiSelect options={emojiList} selected={emojiSelected} onChange={handleEmojiChange} />
     </div>
   </div>
 );
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx
index 1037966d1dd4b3a3801fb819d34f28d170cbd464..943d138689e941a85ebf25c44ffb0dffbb5f31a6 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx
@@ -1,28 +1,13 @@
 import React from 'react';
 import { createContainer } from 'meteor/react-meteor-data';
-import { withModalMounter } from '/imports/ui/components/modal/service';
 import ActionsBar from './component';
 import Service from './service';
-import AudioService from '../audio/service';
 
-import AudioModal from '../audio/audio-modal/component';
+const ActionsBarContainer = props => <ActionsBar {...props} />;
 
-const ActionsBarContainer = ({ children, ...props }) => (
-  <ActionsBar {...props}>
-    {children}
-  </ActionsBar>
-);
-
-export default withModalMounter(createContainer(({ mountModal }) => {
-  const isPresenter = Service.isUserPresenter();
-
-  const handleExitAudio = () => AudioService.exitAudio();
-  const handleOpenJoinAudio = () =>
-    mountModal(<AudioModal handleJoinListenOnly={AudioService.joinListenOnly} />);
-
-  return {
-    isUserPresenter: isPresenter,
-    handleExitAudio,
-    handleOpenJoinAudio,
-  };
-}, ActionsBarContainer));
+export default createContainer(() => ({
+  isUserPresenter: Service.isUserPresenter(),
+  emojiList: Service.getEmojiList(),
+  emojiSelected: Service.getEmoji(),
+  handleEmojiChange: Service.setEmoji,
+}), ActionsBarContainer);
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx
deleted file mode 100644
index c14930d45945edd8d73097b0b0ca7debfbfef5fb..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx
+++ /dev/null
@@ -1,226 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { defineMessages, intlShape, injectIntl } from 'react-intl';
-import { EMOJI_NORMALIZE } from '/imports/utils/statuses';
-
-import Button from '/imports/ui/components/button/component';
-import Dropdown from '/imports/ui/components/dropdown/component';
-import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component';
-import DropdownContent from '/imports/ui/components/dropdown/content/component';
-import DropdownList from '/imports/ui/components/dropdown/list/component';
-import DropdownListItem from '/imports/ui/components/dropdown/list/item/component';
-import DropdownListSeparator from '/imports/ui/components/dropdown/list/separator/component';
-import styles from './styles';
-
-const intlMessages = defineMessages({
-  statusTriggerLabel: {
-    id: 'app.actionsBar.emojiMenu.statusTriggerLabel',
-    description: 'Emoji status button label',
-  },
-  awayLabel: {
-    id: 'app.actionsBar.emojiMenu.awayLabel',
-    description: 'Away emoji label',
-  },
-  awayDesc: {
-    id: 'app.actionsBar.emojiMenu.awayDesc',
-    description: 'adds context to away option',
-  },
-  raiseLabel: {
-    id: 'app.actionsBar.emojiMenu.raiseLabel',
-    description: 'raise hand emoji label',
-  },
-  raiseDesc: {
-    id: 'app.actionsBar.emojiMenu.raiseDesc',
-    description: 'adds context to raise hand option',
-  },
-  undecidedLabel: {
-    id: 'app.actionsBar.emojiMenu.undecidedLabel',
-    description: 'undecided emoji label',
-  },
-  undecidedDesc: {
-    id: 'app.actionsBar.emojiMenu.undecidedDesc',
-    description: 'adds context to undecided option',
-  },
-  confusedLabel: {
-    id: 'app.actionsBar.emojiMenu.confusedLabel',
-    description: 'confused emoji label',
-  },
-  confusedDesc: {
-    id: 'app.actionsBar.emojiMenu.confusedDesc',
-    description: 'adds context to confused option',
-  },
-  sadLabel: {
-    id: 'app.actionsBar.emojiMenu.sadLabel',
-    description: 'sad emoji label',
-  },
-  sadDesc: {
-    id: 'app.actionsBar.emojiMenu.sadDesc',
-    description: 'adds context to sad option',
-  },
-  happyLabel: {
-    id: 'app.actionsBar.emojiMenu.happyLabel',
-    description: 'happy emoji label',
-  },
-  happyDesc: {
-    id: 'app.actionsBar.emojiMenu.happyDesc',
-    description: 'adds context to happy option',
-  },
-  clearLabel: {
-    id: 'app.actionsBar.emojiMenu.clearLabel',
-    description: 'confused emoji label',
-  },
-  clearDesc: {
-    id: 'app.actionsBar.emojiMenu.clearDesc',
-    description: 'adds context to clear status option',
-  },
-  applauseLabel: {
-    id: 'app.actionsBar.emojiMenu.applauseLabel',
-    description: 'applause emoji label',
-  },
-  applauseDesc: {
-    id: 'app.actionsBar.emojiMenu.applauseDesc',
-    description: 'adds context to applause option',
-  },
-  thumbsUpLabel: {
-    id: 'app.actionsBar.emojiMenu.thumbsUpLabel',
-    description: 'thumbs up emoji label',
-  },
-  thumbsUpDesc: {
-    id: 'app.actionsBar.emojiMenu.thumbsUpDesc',
-    description: 'adds context to thumbs up option',
-  },
-  thumbsDownLabel: {
-    id: 'app.actionsBar.emojiMenu.thumbsDownLabel',
-    description: 'thumbs down emoji label',
-  },
-  thumbsDownDesc: {
-    id: 'app.actionsBar.emojiMenu.thumbsDownDesc',
-    description: 'adds context to thumbs down option',
-  },
-  changeStatusLabel: {
-    id: 'app.actionsBar.changeStatusLabel',
-    description: 'Aria-label for emoji status button',
-  },
-  currentStatusDesc: {
-    id: 'app.actionsBar.currentStatusDesc',
-    description: 'Aria description for status button',
-  },
-});
-
-const propTypes = {
-  // Emoji status of the current user
-  intl: intlShape.isRequired,
-  userEmojiStatus: PropTypes.string.isRequired,
-  actions: PropTypes.object.isRequired,
-};
-
-const EmojiMenu = ({
- userEmojiStatus,
- actions,
- intl,
-}) => (
-  <Dropdown autoFocus>
-    <DropdownTrigger tabIndex={0}>
-      <Button
-        className={styles.button}
-        role="button"
-        label={intl.formatMessage(intlMessages.statusTriggerLabel)}
-        aria-label={intl.formatMessage(intlMessages.changeStatusLabel)}
-        aria-describedby="currentStatus"
-        icon={userEmojiStatus in EMOJI_NORMALIZE ?
-          EMOJI_NORMALIZE[userEmojiStatus] : 'hand'}
-        ghost={false}
-        circle
-        hideLabel={false}
-        color="primary"
-        size="lg"
-
-        // FIXME: Without onClick react proptypes keep warning
-        // even after the DropdownTrigger inject an onClick handler
-        onClick={() => null}
-      >
-        <div id="currentStatus" hidden>
-          { intl.formatMessage(intlMessages.currentStatusDesc, { 0: userEmojiStatus }) }
-        </div>
-      </Button>
-    </DropdownTrigger>
-    <DropdownContent placement="top left">
-      <DropdownList>
-        <DropdownListItem
-          icon="hand"
-          label={intl.formatMessage(intlMessages.raiseLabel)}
-          description={intl.formatMessage(intlMessages.raiseDesc)}
-          onClick={() => actions.setEmojiHandler('raiseHand')}
-          tabIndex={-1}
-        />
-        <DropdownListItem
-          icon="happy"
-          label={intl.formatMessage(intlMessages.happyLabel)}
-          description={intl.formatMessage(intlMessages.happyDesc)}
-          onClick={() => actions.setEmojiHandler('happy')}
-          tabIndex={-1}
-        />
-        <DropdownListItem
-          icon="undecided"
-          label={intl.formatMessage(intlMessages.undecidedLabel)}
-          description={intl.formatMessage(intlMessages.undecidedDesc)}
-          onClick={() => actions.setEmojiHandler('neutral')}
-          tabIndex={-1}
-        />
-        <DropdownListItem
-          icon="sad"
-          label={intl.formatMessage(intlMessages.sadLabel)}
-          description={intl.formatMessage(intlMessages.sadDesc)}
-          onClick={() => actions.setEmojiHandler('sad')}
-          tabIndex={-1}
-        />
-        <DropdownListItem
-          icon="confused"
-          label={intl.formatMessage(intlMessages.confusedLabel)}
-          description={intl.formatMessage(intlMessages.confusedDesc)}
-          onClick={() => actions.setEmojiHandler('confused')}
-          tabIndex={-1}
-        />
-        <DropdownListItem
-          icon="time"
-          label={intl.formatMessage(intlMessages.awayLabel)}
-          description={intl.formatMessage(intlMessages.awayDesc)}
-          onClick={() => actions.setEmojiHandler('away')}
-          tabIndex={-1}
-        />
-        <DropdownListItem
-          icon="thumbs_up"
-          label={intl.formatMessage(intlMessages.thumbsUpLabel)}
-          description={intl.formatMessage(intlMessages.thumbsUpDesc)}
-          onClick={() => actions.setEmojiHandler('thumbsUp')}
-          tabIndex={-1}
-        />
-        <DropdownListItem
-          icon="thumbs_down"
-          label={intl.formatMessage(intlMessages.thumbsDownLabel)}
-          description={intl.formatMessage(intlMessages.thumbsDownDesc)}
-          onClick={() => actions.setEmojiHandler('thumbsDown')}
-          tabIndex={-1}
-        />
-        <DropdownListItem
-          icon="applause"
-          label={intl.formatMessage(intlMessages.applauseLabel)}
-          description={intl.formatMessage(intlMessages.applauseDesc)}
-          onClick={() => actions.setEmojiHandler('applause')}
-          tabIndex={-1}
-        />
-        <DropdownListSeparator />
-        <DropdownListItem
-          icon="clear_status"
-          label={intl.formatMessage(intlMessages.clearLabel)}
-          description={intl.formatMessage(intlMessages.clearDesc)}
-          onClick={() => actions.setEmojiHandler('none')}
-          tabIndex={-1}
-        />
-      </DropdownList>
-    </DropdownContent>
-  </Dropdown>
-);
-
-EmojiMenu.propTypes = propTypes;
-export default injectIntl(EmojiMenu);
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/container.jsx
deleted file mode 100644
index b394889f69b91bc1bddc7e92bdceb17b94da40f0..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/container.jsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { createContainer } from 'meteor/react-meteor-data';
-
-import EmojiService from './service';
-import EmojiMenu from './component';
-
-const propTypes = {
-  // Emoji status of the current user
-  userEmojiStatus: PropTypes.string.isRequired,
-  actions: PropTypes.object.isRequired,
-};
-
-const EmojiContainer = ({ userEmojiStatus, actions }) => (
-  <EmojiMenu userEmojiStatus={userEmojiStatus} actions={actions} />
-);
-
-export default createContainer(() => {
-  const data = EmojiService.getEmojiData();
-
-  const {
-    userEmojiStatus,
-    credentials,
-  } = data;
-
-  const { requesterUserId: userId } = credentials;
-
-  return {
-    userEmojiStatus,
-    actions: {
-      setEmojiHandler: (status) => {
-        EmojiService.setEmoji(userId, status);
-      },
-    },
-  };
-}, EmojiContainer);
-
-EmojiContainer.propTypes = propTypes;
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/service.js b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/service.js
deleted file mode 100644
index 544785851fca75a9aedce1adb915f24297c41b0e..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/service.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import Auth from '/imports/ui/services/auth/index.js';
-import Users from '/imports/api/users';
-import { makeCall } from '/imports/ui/services/api/index.js';
-
-const getEmojiData = () => {
-  // Get userId and meetingId
-  const credentials = Auth.credentials;
-  const { requesterUserId: userId, meetingId } = credentials;
-
-  // Find the Emoji Status of this specific meeting and userid
-  const userEmojiStatus = Users.findOne({
-    meetingId,
-    userId,
-  }).emoji;
-
-  return {
-    userEmojiStatus,
-    credentials,
-  };
-};
-
-// Below doesn't even need to receieve credentials
-const setEmoji = (toRaiseUserId, status) => {
-  makeCall('setEmojiStatus', toRaiseUserId, status);
-};
-
-export default {
-  getEmojiData,
-  setEmoji,
-};
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/styles.scss b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/styles.scss
deleted file mode 100644
index 136d7cad950af9b4c99c08fd6eb35d83fa0f0172..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/styles.scss
+++ /dev/null
@@ -1,9 +0,0 @@
-.button {
-  &:focus {
-    outline: none !important;
-  }
-
-  span:first-child {
-    box-shadow: 0 2px 5px 0 rgb(0, 0, 0);
-  }
-}
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-select/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-select/component.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..f0644270e4c87c558f76a53f6d48be1a9de4eb03
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-select/component.jsx
@@ -0,0 +1,99 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { defineMessages, intlShape, injectIntl } from 'react-intl';
+
+import Button from '/imports/ui/components/button/component';
+import Dropdown from '/imports/ui/components/dropdown/component';
+import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component';
+import DropdownContent from '/imports/ui/components/dropdown/content/component';
+import DropdownList from '/imports/ui/components/dropdown/list/component';
+import DropdownListItem from '/imports/ui/components/dropdown/list/item/component';
+import DropdownListSeparator from '/imports/ui/components/dropdown/list/separator/component';
+import styles from '../styles';
+
+const intlMessages = defineMessages({
+  statusTriggerLabel: {
+    id: 'app.actionsBar.emojiMenu.statusTriggerLabel',
+    description: 'Emoji status button label',
+  },
+  changeStatusLabel: {
+    id: 'app.actionsBar.changeStatusLabel',
+    description: 'Aria-label for emoji status button',
+  },
+  currentStatusDesc: {
+    id: 'app.actionsBar.currentStatusDesc',
+    description: 'Aria description for status button',
+  },
+});
+
+const propTypes = {
+  intl: intlShape.isRequired,
+  options: PropTypes.objectOf(PropTypes.string).isRequired,
+  selected: PropTypes.string.isRequired,
+  onChange: PropTypes.func.isRequired,
+};
+
+const EmojiSelect = ({
+  intl,
+  options,
+  selected,
+  onChange,
+}) => {
+  const statuses = Object.keys(options);
+  const lastStatus = statuses.pop();
+
+  return (
+    <Dropdown autoFocus>
+      <DropdownTrigger tabIndex={0}>
+        <Button
+          className={styles.button}
+          label={intl.formatMessage(intlMessages.statusTriggerLabel)}
+          aria-label={intl.formatMessage(intlMessages.changeStatusLabel)}
+          aria-describedby="currentStatus"
+          icon={options[selected !== lastStatus ? selected : statuses[1]]}
+          ghost={false}
+          hideLabel={false}
+          circle
+          size="lg"
+          color="primary"
+          onClick={() => null}
+        >
+          <div id="currentStatus" hidden>
+            { intl.formatMessage(intlMessages.currentStatusDesc, { 0: selected }) }
+          </div>
+        </Button>
+      </DropdownTrigger>
+      <DropdownContent placement="top left">
+        <DropdownList>
+          {
+            statuses.map(status => (
+              <DropdownListItem
+                key={status}
+                className={status === selected ? styles.emojiSelected : null}
+                icon={options[status]}
+                label={intl.formatMessage({ id: `app.actionsBar.emojiMenu.${status}Label` })}
+                description={intl.formatMessage({ id: `app.actionsBar.emojiMenu.${status}Desc` })}
+                onClick={() => onChange(status)}
+                tabIndex={-1}
+              />
+            ))
+            .concat(
+              <DropdownListSeparator key={-1} />,
+              <DropdownListItem
+                key={lastStatus}
+                icon={options[lastStatus]}
+                label={intl.formatMessage({ id: `app.actionsBar.emojiMenu.${lastStatus}Label` })}
+                description={intl.formatMessage({ id: `app.actionsBar.emojiMenu.${lastStatus}Desc` })}
+                onClick={() => onChange(lastStatus)}
+                tabIndex={-1}
+              />,
+            )
+          }
+        </DropdownList>
+      </DropdownContent>
+    </Dropdown>
+  );
+};
+
+EmojiSelect.propTypes = propTypes;
+export default injectIntl(EmojiSelect);
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/service.js b/bigbluebutton-html5/imports/ui/components/actions-bar/service.js
index 87491710917b5d993064844542bb70760687491f..0c065ac72f9b065f492016d5bfb2b704549a068d 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/service.js
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/service.js
@@ -1,30 +1,11 @@
 import Auth from '/imports/ui/services/auth';
 import Users from '/imports/api/users';
 import { makeCall } from '/imports/ui/services/api';
-import VoiceUsers from '/imports/api/voice-users';
-
-const isUserPresenter = () => Users.findOne({
-  userId: Auth.userID,
-}).presenter;
-
-const toggleSelfVoice = () => makeCall('toggleSelfVoice');
-
-const getVoiceUserData = () => {
-  const userId = Auth.userID;
-  const voiceUser = VoiceUsers.findOne({ intId: userId });
-
-  const { muted, joined, talking, listenOnly } = voiceUser;
-
-  return ({
-    isInAudio: joined,
-    isMuted: muted,
-    isTalking: talking,
-    listenOnly,
-  });
-};
+import { EMOJI_STATUSES } from '/imports/utils/statuses';
 
 export default {
-  isUserPresenter,
-  toggleSelfVoice,
-  getVoiceUserData,
+  isUserPresenter: () => Users.findOne({ userId: Auth.userID }).presenter,
+  getEmoji: () => Users.findOne({ userId: Auth.userID }).emoji,
+  setEmoji: status => makeCall('setEmojiStatus', Auth.userID, status),
+  getEmojiList: () => EMOJI_STATUSES,
 };
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss b/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss
index dbea4fdf5f5e29ee97d4a60e1b58ce12d6a4e679..968f74547b7783ae93a2f47f7de1c8b7cd883d0a 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss
@@ -24,3 +24,19 @@
 .center {
   align-items: center;
 }
+
+.button {
+  &:focus {
+    outline: none !important;
+  }
+
+  span:first-child {
+    box-shadow: 0 2px 5px 0 rgb(0, 0, 0);
+  }
+}
+
+.emojiSelected {
+  span, i::before{
+    color: $color-primary;
+  }
+}
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 3d6a95c3277c2ea25cd03f02a202b0d82a3d7499..e10ca43f675987f34f0cbcd14eef30896068b0b5 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx
@@ -2,6 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import Button from '/imports/ui/components/button/component';
 import styles from './styles';
+import cx from 'classnames';
 
 const propTypes = {
   handleToggleMuteMicrophone: PropTypes.func.isRequired,
@@ -20,12 +21,13 @@ const AudioControls = ({
   mute,
   unmute,
   disable,
+  glow,
   join,
 }) => (
   <span className={styles.container}>
     {mute ?
       <Button
-        className={styles.button}
+        className={glow ? cx(styles.button, styles.glow) : styles.button}
         onClick={handleToggleMuteMicrophone}
         disabled={disable}
         label={unmute ? 'Unmute' : 'Mute'}
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx
index 63504178b3a490018099b45a90c7280ddec7b2af..83c595cb064c76983fdebc287c9a320e824485de 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx
@@ -13,6 +13,7 @@ export default withModalMounter(createContainer(({ mountModal }) =>
      unmute: Service.isConnected() && !Service.isListenOnly() && Service.isMuted(),
      join: Service.isConnected() && !Service.isEchoTest(),
      disable: Service.isConnecting() || Service.isHangingUp(),
+     glow: Service.isTalking() && !Service.isMuted(),
      handleToggleMuteMicrophone: () => Service.toggleMuteMicrophone(),
      handleJoinAudio: () => mountModal(<AudioModalContainer />),
      handleLeaveAudio: () => Service.exitAudio(),
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/styles.scss b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/styles.scss
index b29874dc65d7e6eb7d5baafffd8e17ed996b035c..4b5d13f9fd037a273a45065fa787db35198a81e1 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/styles.scss
@@ -19,4 +19,10 @@
   &:focus {
     outline: none !important;
   }
+
+  &.glow {
+    span:first-child{
+      box-shadow: 0 0 0 1px #ffffff, 0 0 0 1px;
+    }
+  }
 }
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx
index c063971081b4552ebb0d6cc77a70dbe7db82889f..012a4caf8536274488ac776e7f130a21967d1555 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx
@@ -4,8 +4,10 @@ import ModalBase from '/imports/ui/components/modal/base/component';
 import Button from '/imports/ui/components/button/component';
 import { defineMessages, injectIntl, intlShape } from 'react-intl';
 import styles from './styles';
+import PermissionsOverlay from '../permissions-overlay/component';
 import AudioSettings from '../audio-settings/component';
 import EchoTest from '../echo-test/component';
+import Help from '../help/component';
 
 const propTypes = {
   intl: intlShape.isRequired,
@@ -54,6 +56,10 @@ const intlMessages = defineMessages({
     id: 'app.audioModal.settingsTitle',
     description: 'Title for the audio modal',
   },
+  helpTitle: {
+    id: 'app.audioModal.helpTitle',
+    description: 'Title for the audio help',
+  },
   connecting: {
     id: 'app.audioModal.connecting',
     description: 'Message for audio connecting',
@@ -87,8 +93,8 @@ class AudioModal extends Component {
     this.handleGoToAudioSettings = this.handleGoToAudioSettings.bind(this);
     this.handleGoToEchoTest = this.handleGoToEchoTest.bind(this);
     this.handleJoinMicrophone = this.handleJoinMicrophone.bind(this);
+    this.handleJoinListenOnly = this.handleJoinListenOnly.bind(this);
     this.closeModal = closeModal;
-    this.handleJoinListenOnly = joinListenOnly;
     this.joinEchoTest = joinEchoTest;
     this.exitAudio = exitAudio;
     this.leaveEchoTest = leaveEchoTest;
@@ -104,6 +110,10 @@ class AudioModal extends Component {
         title: intl.formatMessage(intlMessages.settingsTitle),
         component: () => this.renderAudioSettings(),
       },
+      help: {
+        title: intl.formatMessage(intlMessages.helpTitle),
+        component: () => this.renderHelp(),
+      }
     };
   }
 
@@ -132,10 +142,36 @@ class AudioModal extends Component {
   }
 
   handleGoToEchoTest() {
-    this.joinEchoTest().then(() => {
+    const {
+      inputDeviceId,
+      outputDeviceId,
+    } = this.props;
+
+    return this.joinEchoTest().then(() => {
+      console.log(inputDeviceId, outputDeviceId);
       this.setState({
         content: 'echoTest',
       });
+    }).catch(err => {
+      if (err.type === 'MEDIA_ERROR') {
+        this.setState({
+          content: 'help',
+        });
+      }
+    });
+  }
+
+  handleJoinListenOnly() {
+    const {
+      joinListenOnly,
+    } = this.props;
+
+    return joinListenOnly().catch(err => {
+      if (err.type === 'MEDIA_ERROR') {
+        this.setState({
+          content: 'help',
+        });
+      }
     });
   }
 
@@ -153,7 +189,7 @@ class AudioModal extends Component {
     } = this.props;
 
     return (
-      <span>
+      <span className={styles.audioOptions}>
         <Button
           className={styles.audioBtn}
           label={intl.formatMessage(intlMessages.microphoneLabel)}
@@ -240,10 +276,19 @@ class AudioModal extends Component {
     );
   }
 
+  renderHelp() {
+    return (
+      <Help
+        handleBack={this.handleGoToAudioOptions}
+      />
+    );
+  }
+
   render() {
     const {
       intl,
       isConnecting,
+      showPermissionsOvelay,
     } = this.props;
 
     const {
@@ -251,32 +296,35 @@ class AudioModal extends Component {
     } = this.state;
 
     return (
-      <ModalBase
-        overlayClassName={styles.overlay}
-        className={styles.modal}
-        onRequestClose={this.closeModal}
-      >
-        { isConnecting ? null :
-        <header className={styles.header}>
-          <h3 className={styles.title}>
-            { content ?
-              this.contents[content].title :
-              intl.formatMessage(intlMessages.audioChoiceLabel)}
-          </h3>
-          <Button
-            className={styles.closeBtn}
-            label={intl.formatMessage(intlMessages.closeLabel)}
-            icon={'close'}
-            size={'md'}
-            hideLabel
-            onClick={this.closeModal}
-          />
-        </header>
-        }
-        <div className={styles.content}>
-          { this.renderContent() }
-        </div>
-      </ModalBase>
+      <span>
+        { showPermissionsOvelay ? <PermissionsOverlay /> : null}
+        <ModalBase
+          overlayClassName={styles.overlay}
+          className={styles.modal}
+          onRequestClose={this.closeModal}
+        >
+          { isConnecting ? null :
+          <header className={styles.header}>
+            <h3 className={styles.title}>
+              { content ?
+                this.contents[content].title :
+                intl.formatMessage(intlMessages.audioChoiceLabel)}
+            </h3>
+            <Button
+              className={styles.closeBtn}
+              label={intl.formatMessage(intlMessages.closeLabel)}
+              icon={'close'}
+              size={'md'}
+              hideLabel
+              onClick={this.closeModal}
+            />
+          </header>
+          }
+          <div className={styles.content}>
+            { this.renderContent() }
+          </div>
+        </ModalBase>
+      </span>
     );
   }
 }
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx
index 27d11d6302d0e06e17f3c5af452fcdee54f83380..543fe3e91b61dea0ff57fd3d54b2d16e9dc87144 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx
@@ -37,4 +37,5 @@ export default withModalMounter(createContainer(({ mountModal }) =>
      isEchoTest: Service.isEchoTest(),
      inputDeviceId: Service.inputDeviceId(),
      outputDeviceId: Service.outputDeviceId(),
+     showPermissionsOvelay: Service.isWaitingPermissions(),
    }), AudioModalContainer));
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.scss b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.scss
index 1b36d01d9ea954fe9a3783122d7b298f487e6306..606dbad0eb2e5d6aaf44943b0b9f2f4d597bbcd6 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.scss
@@ -9,6 +9,7 @@
 }
 
 .content {
+  flex-grow: 1;
   display: flex;
   justify-content: center;
   padding: 0;
@@ -25,6 +26,11 @@
   }
 }
 
+.audioOptions {
+  margin-top: auto;
+  margin-bottom: auto;
+}
+
 .overlay {
   @extend .overlay;
 }
@@ -102,6 +108,8 @@
 }
 
 .connecting {
+  margin-top: auto;
+  margin-bottom: auto;
   font-size: 2rem;
 }
 
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 6e6534192d45e5c9ba88eb4976acb4243f38b2ca..848caa5e1c79e4d266c34d87f9d0f2ebd4060760 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-settings/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-settings/component.jsx
@@ -10,7 +10,6 @@ import styles from './styles';
 
 const propTypes = {
   intl: intlShape.isRequired,
-  exitAudio: PropTypes.func.isRequired,
   changeInputDevice: PropTypes.func.isRequired,
   changeOutputDevice: PropTypes.func.isRequired,
   handleBack: PropTypes.func.isRequired,
diff --git a/bigbluebutton-html5/imports/ui/components/audio/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/container.jsx
index fbf0ea116c9cfd9d9cdbb38c30af8d33182f6167..c44556f1c6052d4efc4b2327df2169337e7a26ac 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/container.jsx
@@ -71,6 +71,7 @@ export default withModalMounter(injectIntl(createContainer(({ mountModal, intl }
       CONNECTION_ERROR: intl.formatMessage(intlMessages.connectionError),
       REQUEST_TIMEOUT: intl.formatMessage(intlMessages.requestTimeout),
       INVALID_TARGET: intl.formatMessage(intlMessages.invalidTarget),
+      MEDIA_ERROR: intl.formatMessage(intlMessages.mediaError),
     },
   };
 
diff --git a/bigbluebutton-html5/imports/ui/components/audio/echo-test/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/echo-test/component.jsx
index ffc72587bb2d4735ef46a0545c2fbcc38dc282f6..a437d2de5876bbd94db87ca7931e5d5aae9e1ca4 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/echo-test/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/echo-test/component.jsx
@@ -35,7 +35,7 @@ class EchoTest extends Component {
     } = this.props;
 
     return (
-      <span>
+      <span className={styles.echoTest}>
         <Button
           className={styles.button}
           label={intl.formatMessage(intlMessages.yes)}
diff --git a/bigbluebutton-html5/imports/ui/components/audio/echo-test/styles.scss b/bigbluebutton-html5/imports/ui/components/audio/echo-test/styles.scss
index b196de52294feecebd641dbb0a58e310eee4ed47..114fd69c354a5b8a43f3b43131a12b766f30cea7 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/echo-test/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/audio/echo-test/styles.scss
@@ -1,5 +1,10 @@
 @import "/imports/ui/stylesheets/variables/_all";
 
+.echoTest {
+  margin-top: auto;
+  margin-bottom: auto;
+}
+
 .button {
   &:focus {
     outline: none !important;
diff --git a/bigbluebutton-html5/imports/ui/components/audio/help/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/help/component.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..1e36cc2b9cbf00a120a94a8473f9f58f1cc47292
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/audio/help/component.jsx
@@ -0,0 +1,44 @@
+import React, { Component } from 'react';
+import Button from '/imports/ui/components/button/component';
+import { injectIntl, defineMessages } from 'react-intl';
+import styles from './styles';
+
+const intlMessages = defineMessages({
+  descriptionHelp: {
+    id: 'app.audioModal.helpText',
+    description: 'Text decription for the audio help',
+  },
+  backLabel: {
+    id: 'app.audio.backLabel',
+    description: 'audio settings back button label',
+  }
+})
+
+class Help extends Component {
+  render() {
+    const {
+      intl,
+      handleBack,
+    } = this.props;
+
+    return (
+      <span className={styles.help}>
+        <div className={styles.text}>
+          { intl.formatMessage(intlMessages.descriptionHelp) }
+        </div>
+        <div className={styles.enterAudio}>
+          <Button
+            className={styles.backBtn}
+            label={intl.formatMessage(intlMessages.backLabel)}
+            size={'md'}
+            color={'primary'}
+            onClick={handleBack}
+            ghost
+          />
+        </div>
+      </span>
+    )
+  }
+};
+
+export default injectIntl(Help);
diff --git a/bigbluebutton-html5/imports/ui/components/audio/help/styles.scss b/bigbluebutton-html5/imports/ui/components/audio/help/styles.scss
new file mode 100644
index 0000000000000000000000000000000000000000..43f81fcff866e9ab7d51edf7a596354f920a8593
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/audio/help/styles.scss
@@ -0,0 +1,27 @@
+@import "/imports/ui/stylesheets/variables/_all";
+
+.help {
+  display: flex;
+  flex-flow: column;
+}
+
+.text {
+  text-align: center;
+  margin-top: auto;
+  margin-bottom: auto;
+}
+
+.backBtn {
+  margin-right: 0.5rem;
+  border: none;
+
+  @include mq($small-only) {
+    margin-right: auto;
+  }
+}
+
+.enterAudio {
+  margin-top: 1.5rem;
+  display: flex;
+  justify-content: flex-end;
+}
diff --git a/bigbluebutton-html5/imports/ui/components/audio/permissions-overlay/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/permissions-overlay/component.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..a086055ea7f25db818f29bf96fc51941c4bc0e6e
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/audio/permissions-overlay/component.jsx
@@ -0,0 +1,65 @@
+import React, { Component } from 'react';
+import { injectIntl, intlShape, defineMessages } from 'react-intl';
+import styles from './styles';
+
+const propTypes = {
+  intl: intlShape.isRequired,
+};
+
+const intlMessages = defineMessages({
+  title: {
+    id: 'app.audio.permissionsOverlay.title',
+    description: 'Title for the overlay',
+  },
+  hint: {
+    id: 'app.audio.permissionsOverlay.hint',
+    description: 'Hint for the overlay',
+  },
+});
+
+class PermissionsOverlay extends Component {
+  constructor(props) {
+    super(props);
+
+    const broswerStyles = {
+      Chrome: {
+        top: '145px',
+        left: '380px',
+      },
+      Firefox: {
+        top: '210px',
+        left: '605px',
+      },
+    };
+
+    const browser = window.bowser.name;
+
+    this.state = {
+      styles: {
+        top: broswerStyles[browser].top,
+        left: broswerStyles[browser].left,
+      },
+    };
+  }
+
+  render() {
+    const {
+      intl,
+    } = this.props;
+
+    return (
+      <div className={styles.overlay}>
+        <div style={this.state.styles} className={styles.hint}>
+          { intl.formatMessage(intlMessages.title) }
+          <small>
+            { intl.formatMessage(intlMessages.hint) }
+          </small>
+        </div>
+      </div>
+    );
+  }
+}
+
+PermissionsOverlay.propTypes = propTypes;
+
+export default injectIntl(PermissionsOverlay);
diff --git a/bigbluebutton-html5/imports/ui/components/audio/permissions-overlay/styles.scss b/bigbluebutton-html5/imports/ui/components/audio/permissions-overlay/styles.scss
new file mode 100644
index 0000000000000000000000000000000000000000..b468129cf9972e919db1690e62f39500017c2c63
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/audio/permissions-overlay/styles.scss
@@ -0,0 +1,90 @@
+.overlay {
+  position: fixed;
+  z-index: 1002;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  background-color: rgba(0, 0, 0, .85);
+  animation: fade-in .5s ease-in;
+}
+
+.hint {
+  position: absolute;
+  color: #fff;
+  font-size: 16px;
+  font-weight: 400;
+  line-height: 18px;
+  width: 300px;
+
+  small {
+    display: block;
+    font-size: 12px;
+    line-height: 14px;
+    margin-top: 3px;
+    opacity: .6;
+  }
+
+  &:after {
+    display: block;
+    font-family: 'bbb-icons';
+    content: "\E906";
+    position: relative;
+    top: -50px;
+    left: -20px;
+    font-size: 20px;
+    -webkit-animation: bounce 2s infinite;
+    animation: bounce 2s infinite;
+  }
+}
+
+@-webkit-keyframes bounce {
+	0%, 20%, 50%, 80%, 100% {
+    -webkit-transform: translateY(0);
+    transform: translateY(0);
+  }
+	40% {
+    -webkit-transform: translateY(10px);
+    transform: translateY(10px);
+  }
+	60% {
+    -webkit-transform: translateY(5px);
+    transform: translateY(5px);
+  }
+}
+
+@-moz-keyframes bounce {
+	0%, 20%, 50%, 80%, 100% {
+    transform: translateY(0);
+  }
+	40% {
+    transform: translateY(10px);
+  }
+	60% {
+    transform: translateY(5px);
+  }
+}
+
+@keyframes bounce {
+	0%, 20%, 50%, 80%, 100% {
+    -ms-transform: translateY(0);
+    transform: translateY(0);
+  }
+	40% {
+    -ms-transform: translateY(10px);
+    transform: translateY(10px);
+  }
+	60% {
+    -ms-transform: translateY(5px);
+    transform: translateY(5px);
+  }
+}
+
+@keyframes fade-in {
+  0% {
+    opacity: 0;
+  }
+	100% {
+    opacity: 1;
+  }
+}
diff --git a/bigbluebutton-html5/imports/ui/components/audio/service.js b/bigbluebutton-html5/imports/ui/components/audio/service.js
index 694d302f3f3a695861ea6045983257137de9ca56..81fc11b73f6d7bfa975765acfe7f99bf2853b8eb 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/service.js
+++ b/bigbluebutton-html5/imports/ui/components/audio/service.js
@@ -2,8 +2,10 @@ import Users from '/imports/api/users';
 import Auth from '/imports/ui/services/auth';
 import AudioManager from '/imports/ui/services/audio-manager';
 import Meetings from '/imports/api/meetings';
+import VoiceUsers from '/imports/api/voice-users';
 
 const init = (messages) => {
+  if (AudioManager.initialized) return;
   const meetingId = Auth.meetingID;
   const userId = Auth.userID;
   const sessionToken = Auth.sessionToken;
@@ -27,6 +29,9 @@ const init = (messages) => {
   AudioManager.init(userData, messages);
 };
 
+const isVoiceUserTalking = () =>
+  VoiceUsers.findOne({ intId: Auth.userID }).talking;
+
 export default {
   init,
   exitAudio: () => AudioManager.exitAudio(),
@@ -38,7 +43,9 @@ export default {
   changeInputDevice: inputDeviceId => AudioManager.changeInputDevice(inputDeviceId),
   changeOutputDevice: outputDeviceId => AudioManager.changeOutputDevice(outputDeviceId),
   isConnected: () => AudioManager.isConnected,
+  isTalking: () => isVoiceUserTalking(),
   isHangingUp: () => AudioManager.isHangingUp,
+  isWaitingPermissions: () => AudioManager.isWaitingPermissions,
   isMuted: () => AudioManager.isMuted,
   isConnecting: () => AudioManager.isConnecting,
   isListenOnly: () => AudioManager.isListenOnly,
diff --git a/bigbluebutton-html5/imports/ui/components/button/base/component.jsx b/bigbluebutton-html5/imports/ui/components/button/base/component.jsx
index 122b155b7fcc749c867d60b7ff9afa180a913f1c..260048608ee24e4ba796df57975d8666247b257b 100644
--- a/bigbluebutton-html5/imports/ui/components/button/base/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/button/base/component.jsx
@@ -1,4 +1,4 @@
-import React, { Component } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 
 const propTypes = {
@@ -24,13 +24,47 @@ const propTypes = {
    * Defines the button click handler
    * @defaultValue undefined
    */
-  onClick: PropTypes.func.isRequired,
+  onClick: (props, propName, componentName) => {
+    if (!props.onClick && !props.onMouseDown && !props.onMouseUp) {
+      return new Error('One of props \'onClick\' or \'onMouseDown\' or' +
+        ` 'onMouseUp' was not specified in '${componentName}'.`);
+    }
+
+    return null;
+  },
+  onMouseDown: (props, propName, componentName) => {
+    if (!props.onClick && !props.onMouseDown && !props.onMouseUp) {
+      return new Error('One of props \'onClick\' or \'onMouseDown\' or' +
+        ` 'onMouseUp' was not specified in '${componentName}'.`);
+    }
+
+    return null;
+  },
+  onMouseUp: (props, propName, componentName) => {
+    if (!props.onClick && !props.onMouseDown && !props.onMouseUp) {
+      return new Error('One of props \'onClick\' or \'onMouseDown\' or' +
+        ` 'onMouseUp' was not specified in '${componentName}'.`);
+    }
+
+    return null;
+  },
+
+  onKeyPress: PropTypes.func,
+  onKeyDown: PropTypes.func,
+  onKeyUp: PropTypes.func,
+  setRef: PropTypes.func,
 };
 
 const defaultProps = {
   disabled: false,
   tagName: 'button',
-  role: 'button',
+  onClick: undefined,
+  onMouseDown: undefined,
+  onMouseUp: undefined,
+  onKeyPress: undefined,
+  onKeyDown: undefined,
+  onKeyUp: undefined,
+  setRef: undefined,
 };
 
 /**
@@ -40,7 +74,7 @@ const defaultProps = {
  * keyboard users to comply with ARIA standards.
  */
 
-export default class ButtonBase extends Component {
+export default class ButtonBase extends React.Component {
   constructor(props) {
     super(props);
 
@@ -60,36 +94,38 @@ export default class ButtonBase extends Component {
     if (!this.props.disabled && typeof eventHandler === 'function') {
       return eventHandler(...args);
     }
+
+    return null;
   }
 
   // Define Mouse Event Handlers
-  internalClickHandler(event) {
-    return this.validateDisabled(this.props.onClick, ...arguments);
+  internalClickHandler(...args) {
+    return this.validateDisabled(this.props.onClick, ...args);
   }
 
-  internalDoubleClickHandler(event) {
-    return this.validateDisabled(this.props.onDoubleClick, ...arguments);
+  internalDoubleClickHandler(...args) {
+    return this.validateDisabled(this.props.onDoubleClick, ...args);
   }
 
-  internalMouseDownHandler(event) {
-    return this.validateDisabled(this.props.onMouseDown, ...arguments);
+  internalMouseDownHandler(...args) {
+    return this.validateDisabled(this.props.onMouseDown, ...args);
   }
 
-  internalMouseUpHandler() {
-    return this.validateDisabled(this.props.onMouseUp, ...arguments);
+  internalMouseUpHandler(...args) {
+    return this.validateDisabled(this.props.onMouseUp, ...args);
   }
 
   // Define Keyboard Event Handlers
-  internalKeyPressHandler() {
-    return this.validateDisabled(this.props.onKeyPress, ...arguments);
+  internalKeyPressHandler(...args) {
+    return this.validateDisabled(this.props.onKeyPress, ...args);
   }
 
-  internalKeyDownHandler() {
-    return this.validateDisabled(this.props.onKeyDown, ...arguments);
+  internalKeyDownHandler(...args) {
+    return this.validateDisabled(this.props.onKeyDown, ...args);
   }
 
-  internalKeyUpHandler() {
-    return this.validateDisabled(this.props.onKeyUp, ...arguments);
+  internalKeyUpHandler(...args) {
+    return this.validateDisabled(this.props.onKeyUp, ...args);
   }
 
   render() {
@@ -111,8 +147,12 @@ export default class ButtonBase extends Component {
     delete remainingProps.onKeyDown;
     delete remainingProps.onKeyUp;
 
+    // Delete setRef callback if it exists
+    delete remainingProps.setRef;
+
     return (
       <Component
+        ref={this.props.setRef}
         aria-label={this.props.label}
         aria-disabled={this.props.disabled}
 
diff --git a/bigbluebutton-html5/imports/ui/components/button/styles.scss b/bigbluebutton-html5/imports/ui/components/button/styles.scss
index 8c18e8777fc792774f0c8beb72c171398360b87c..265380ea764a600c07ac93ccc8bf5ee214d2829a 100644
--- a/bigbluebutton-html5/imports/ui/components/button/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/button/styles.scss
@@ -133,6 +133,9 @@ $btn-jumbo-padding: $jumbo-padding-y $jumbo-padding-x;
   & + .button  {
     margin-left: $btn-spacing;
   }
+  &:hover {
+    opacity: .5;
+  }
 }
 
 .hideLabel {
@@ -157,6 +160,9 @@ $btn-jumbo-padding: $jumbo-padding-y $jumbo-padding-x;
   & + .label {
     margin-left: $btn-spacing;
   }
+  .buttonWrapper:hover & {
+    opacity: .75;
+  }
 }
 
 /* Colors
diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/styles.scss b/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/styles.scss
index 1580de38878962d472c5a9a89e4f8ed649464ed4..a2cf678fe342b76f42676786c2057b651beb0c34 100644
--- a/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/styles.scss
@@ -7,21 +7,25 @@ $square-side-length: 1.56rem;
   flex: 0 0;
   margin-top: auto;
 
-  span {
+  cursor: pointer;
+
+  span:first-child {
     width: $square-side-length;
     height: $square-side-length;
   }
 
+
   i {
     color: $color-gray-dark !important;
     top: $icon-offset;
     left: $icon-offset;
-  }   
+  }
 
   &:hover,
   &:focus {
-    span {
+    > span:first-child {
       background-color: transparent !important;
     }
   }
+
 }
diff --git a/bigbluebutton-html5/imports/ui/components/chat/container.jsx b/bigbluebutton-html5/imports/ui/components/chat/container.jsx
index fcfd36f6fcbac8b9f42cc0bff9dbfbd04da1c397..088af86a6cdb495cde3446ad9cea55315b20b3b2 100644
--- a/bigbluebutton-html5/imports/ui/components/chat/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/chat/container.jsx
@@ -6,9 +6,13 @@ import ChatService from './service';
 
 const CHAT_CONFIG = Meteor.settings.public.chat;
 const PUBLIC_CHAT_KEY = CHAT_CONFIG.public_id;
-
+const CHAT_CLEAR = CHAT_CONFIG.system_messages_keys.chat_clear;
 
 const intlMessages = defineMessages({
+  [CHAT_CLEAR]: {
+    id: 'app.chat.clearPublicChatMessage',
+    description: 'message of when clear the public chat',
+  },
   titlePublic: {
     id: 'app.chat.titlePublic',
     description: 'Public chat title',
@@ -37,46 +41,52 @@ export default injectIntl(createContainer(({ params, intl }) => {
   let isChatLocked = ChatService.isChatLocked(chatID);
   let title = intl.formatMessage(intlMessages.titlePublic);
   let chatName = title;
+  let partnerIsLoggedOut = false;
+  let systemMessageIntl = {};
 
   if (chatID === PUBLIC_CHAT_KEY) {
     messages = ChatService.reduceAndMapMessages((ChatService.getPublicMessages()));
   } else {
     messages = ChatService.getPrivateMessages(chatID);
-  }
-
-  const user = ChatService.getUser(chatID, '{{NAME}}');
-
-  let partnerIsLoggedOut = false;
-
-  if (user) {
+    const user = ChatService.getUser(chatID);
+    chatName = user.name;
+    systemMessageIntl = { 0: user.name };
+    title = intl.formatMessage(intlMessages.titlePrivate, systemMessageIntl);
     partnerIsLoggedOut = !user.isOnline;
 
-    if (messages && chatID !== PUBLIC_CHAT_KEY) {
-      const chatUser = ChatService.getUser(chatID, '{{NAME}}');
-
-      title = intl.formatMessage(intlMessages.titlePrivate, { 0: chatUser.name });
-      chatName = chatUser.name;
-
-      if (!chatUser.isOnline) {
-        const time = Date.now();
-        const id = `partner-disconnected-${time}`;
-        const messagePartnerLoggedOut = {
+    if (partnerIsLoggedOut) {
+      const time = Date.now();
+      const id = `partner-disconnected-${time}`;
+      const messagePartnerLoggedOut = {
+        id,
+        content: [{
           id,
-          content: [{
-            id,
-            text: intl.formatMessage(intlMessages.partnerDisconnected, { 0: chatUser.name }),
-            time,
-          }],
+          text: 'partnerDisconnected',
           time,
-          sender: null,
-        };
+        }],
+        time,
+        sender: null,
+      };
 
-        messages.push(messagePartnerLoggedOut);
-        isChatLocked = true;
-      }
+      messages.push(messagePartnerLoggedOut);
+      isChatLocked = true;
     }
   }
 
+  messages = messages.map((message) => {
+    if (message.sender) return message;
+
+    return {
+      ...message,
+      content: message.content.map(content => ({
+        ...content,
+        text: content.text in intlMessages ?
+          `<b><i>${intl.formatMessage(intlMessages[content.text], systemMessageIntl)}</i></b>` : content.text,
+      })),
+    };
+  });
+
+
   const scrollPosition = ChatService.getScrollPosition(chatID);
   const hasUnreadMessages = ChatService.hasUnreadMessages(chatID);
   const lastReadMessageTime = ChatService.lastReadMessageTime(chatID);
diff --git a/bigbluebutton-html5/imports/ui/components/chat/message-form/styles.scss b/bigbluebutton-html5/imports/ui/components/chat/message-form/styles.scss
index 1716537bd8f954bec630ef1da204bd0aea2d3b47..caebbfd4d10ae68f6a78755598643c56084422c8 100644
--- a/bigbluebutton-html5/imports/ui/components/chat/message-form/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/chat/message-form/styles.scss
@@ -44,7 +44,7 @@
 
 .input {
   @include inputFocus($color-blue-light);
-  
+
   flex: 1;
   background: #fff;
   background-clip: padding-box;
@@ -72,8 +72,8 @@
 
 .sendButton {
   margin-left: $sm-padding-x;
-  align-self: flex-end;
-
+  align-self: center;
+  font-size: 0.9rem;
   i {
     font-size: 115% !important;
   }
diff --git a/bigbluebutton-html5/imports/ui/components/chat/message-list/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/message-list/component.jsx
index 5c7b1384be9a90f8069a52d2fe4b5a493a9b9e06..089d6d92fcd80421c287de8bd2c91fe26d81eeba 100644
--- a/bigbluebutton-html5/imports/ui/components/chat/message-list/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/chat/message-list/component.jsx
@@ -7,7 +7,7 @@ import styles from './styles';
 import MessageListItem from './message-list-item/component';
 
 const propTypes = {
-  messages: PropTypes.array.isRequired,
+  messages: PropTypes.arrayOf(PropTypes.object).isRequired,
 };
 
 const intlMessages = defineMessages({
@@ -144,7 +144,7 @@ class MessageList extends Component {
       return (
         <Button
           className={styles.unreadButton}
-          size={'sm'}
+          size="sm"
           label={intl.formatMessage(intlMessages.moreMessages)}
           onClick={() => this.scrollTo()}
         />
@@ -156,9 +156,7 @@ class MessageList extends Component {
 
   render() {
     const { messages, intl } = this.props;
-
-    const isEmpty = messages.length == 0;
-
+    const isEmpty = messages.length === 0;
     return (
       <div className={styles.messageListWrapper}>
         <div
diff --git a/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/styles.scss b/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/styles.scss
index 9f6f665d8329aacf411394be0c42f40ac5cb4a56..bafdf69f8ebbbc71862237e75696f50d9d32de8c 100644
--- a/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/styles.scss
@@ -33,14 +33,6 @@
 
   .message {
     color: $color-gray;
-
-
-    // hide the <br> we dont want from the default WelcomeMessage
-    br:first-child,
-    br:last-child,
-    br + br {
-      display: none;
-    }
   }
 }
 
diff --git a/bigbluebutton-html5/imports/ui/components/chat/notification/audio-notification/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/notification/audio-notification/component.jsx
index 5001a439fb4ec38e1a9f12a1d8112260d650a9c7..128997988c4030623c500da7b98396597a175846 100644
--- a/bigbluebutton-html5/imports/ui/components/chat/notification/audio-notification/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/chat/notification/audio-notification/component.jsx
@@ -42,7 +42,6 @@ class ChatAudioNotification extends React.Component {
   render() {
     return null;
   }
-
 }
 ChatAudioNotification.propTypes = propTypes;
 
diff --git a/bigbluebutton-html5/imports/ui/components/chat/notification/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/notification/component.jsx
index 50ebd65438e9ec4ad0a4f8fe143cdc76edb03f21..8dbe2179f1b8ae6feba0366d84eba9482f62ae18 100644
--- a/bigbluebutton-html5/imports/ui/components/chat/notification/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/chat/notification/component.jsx
@@ -33,10 +33,10 @@ class ChatNotification extends Component {
 
     const notifiedToClear = {};
     openChats
-     .filter(c => c.unreadCounter === 0)
-     .forEach((c) => {
-       notifiedToClear[c.id] = 0;
-     });
+      .filter(c => c.unreadCounter === 0)
+      .forEach((c) => {
+        notifiedToClear[c.id] = 0;
+      });
 
     this.setState(({ notified }) => ({
       notified: {
@@ -82,8 +82,7 @@ class ChatNotification extends Component {
                   },
                 }));
               }}
-            />),
-          )
+            />))
         }
       </span>
     );
diff --git a/bigbluebutton-html5/imports/ui/components/chat/notification/push-notification/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/notification/push-notification/component.jsx
index 0ee49592563fb0766c5cd8b6ffb10de77fd66619..b74528f4a78e766dbc7b571a90688b5f2c03949e 100644
--- a/bigbluebutton-html5/imports/ui/components/chat/notification/push-notification/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/chat/notification/push-notification/component.jsx
@@ -41,12 +41,12 @@ class ChatPushNotification extends React.Component {
       onOpen,
     } = this.props;
 
-    const message = intl.formatMessage(
-      count > 1 ?
+    const message = intl.formatMessage(count > 1 ?
       intlMessages.appToastChatPlural :
       intlMessages.appToastChatSigular, {
-        0: count,
-        1: name });
+      0: count,
+      1: name,
+    });
 
     return notify(message, 'info', 'chat', { onOpen });
   }
diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/list/separator/component.jsx b/bigbluebutton-html5/imports/ui/components/dropdown/list/separator/component.jsx
index be4f3442c1fbea35a0a121dd800713264bd0093e..de3f7bdfdd186ad502486b6147dffdc09c580d2f 100644
--- a/bigbluebutton-html5/imports/ui/components/dropdown/list/separator/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/dropdown/list/separator/component.jsx
@@ -1,8 +1,21 @@
 import React from 'react';
+import PropTypes from 'prop-types';
 import cx from 'classnames';
 import styles from '../styles';
 
-const DropdownListSeparator = (props, { style, className }) => (
-  <li style={style} className={cx(styles.separator, className)} />);
+const DropdownListSeparator = ({ style, className }) =>
+  (
+    <li style={style} className={cx(styles.separator, className)} />
+  );
+
+DropdownListSeparator.propTypes = {
+  style: PropTypes.shape({}),
+  className: PropTypes.string,
+};
+
+DropdownListSeparator.defaultProps = {
+  style: null,
+  className: null,
+};
 
 export default DropdownListSeparator;
diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/list/styles.scss b/bigbluebutton-html5/imports/ui/components/dropdown/list/styles.scss
index 4bf98597adf42a7f573edd0fbf85d0c002b9bb7e..17a7005996f69bf55b13c85011a7871b26783b00 100644
--- a/bigbluebutton-html5/imports/ui/components/dropdown/list/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/dropdown/list/styles.scss
@@ -31,10 +31,9 @@ $item-border-focus: $color-blue-lighter;
   display: flex;
   flex: 1 1 100%;
   height: 1px;
+  min-height: 1px;
   background-color: $color-gray-lighter;
   padding: 0;
-  margin-left: -($line-height-computed / 2);
-  margin-right: -($line-height-computed / 2);
   margin-top: $line-height-computed * .5;
   margin-bottom: $line-height-computed * .5;
 }
diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/styles.scss b/bigbluebutton-html5/imports/ui/components/dropdown/styles.scss
index e10a8bec06f9f340caee2590767999a4e4f36417..f03b3628dd5d6efe49e5470a6b8f2e5e8ec1de79 100644
--- a/bigbluebutton-html5/imports/ui/components/dropdown/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/dropdown/styles.scss
@@ -167,6 +167,7 @@ $dropdown-caret-height: 8px;
 
 %horz-center-caret {
   &:after, &:before {
+    left: 50%;
     margin-left: -($dropdown-caret-width);
   }
 }
diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/styles.scss b/bigbluebutton-html5/imports/ui/components/nav-bar/styles.scss
index 48ea6b771e74ff36608a91be320d7eca2e783102..64a5fd3c4634c80058e2fbf0cb13d4a1e987b6fb 100644
--- a/bigbluebutton-html5/imports/ui/components/nav-bar/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/nav-bar/styles.scss
@@ -48,10 +48,10 @@
     border-radius: 50%;
     width: 12px;
     height: 12px;
-    bottom: 2px;
+    bottom: $border-size;
     right: 3px;
     background-color: $color-danger;
-    border: 2px solid;
+    border: $border-size solid $color-gray-dark;
   }
 }
 
diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/component.jsx
index e8dff5b4f1967f78a7171c60283b289feedc9609..1d4fa36e4f0216f325e11c156779fff43f887606 100644
--- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/component.jsx
@@ -18,6 +18,17 @@ export default class PresentationOverlay extends Component {
     // id of the setInterval()
     this.intervalId = 0;
 
+    // Mobile Firefox has a bug where e.preventDefault on touchstart doesn't prevent
+    // onmousedown from triggering right after. Thus we have to track it manually.
+    // In case if it's fixed one day - there is another issue, React one.
+    // https://github.com/facebook/react/issues/9809
+    // Check it to figure if you can add onTouchStart in render(), or should use raw DOM api
+    this.touchStarted = false;
+
+    this.handleTouchStart = this.handleTouchStart.bind(this);
+    this.handleTouchMove = this.handleTouchMove.bind(this);
+    this.handleTouchEnd = this.handleTouchEnd.bind(this);
+    this.handleTouchCancel = this.handleTouchCancel.bind(this);
     this.mouseMoveHandler = this.mouseMoveHandler.bind(this);
     this.checkCursor = this.checkCursor.bind(this);
     this.mouseEnterHandler = this.mouseEnterHandler.bind(this);
@@ -67,7 +78,77 @@ export default class PresentationOverlay extends Component {
     return point;
   }
 
+
+  handleTouchStart(event) {
+    // to prevent default behavior (scrolling) on devices (in Safari), when you draw a text box
+    event.preventDefault();
+
+    window.addEventListener('touchend', this.handleTouchEnd, { passive: false });
+    window.addEventListener('touchmove', this.handleTouchMove, { passive: false });
+    window.addEventListener('touchcancel', this.handleTouchCancel, true);
+
+    this.touchStarted = true;
+
+    const { clientX, clientY } = event.changedTouches[0];
+    this.currentClientX = clientX;
+    this.currentClientY = clientY;
+
+    const intervalId = setInterval(this.checkCursor, CURSOR_INTERVAL);
+    this.intervalId = intervalId;
+  }
+
+  handleTouchMove(event) {
+    const { clientX, clientY } = event.changedTouches[0];
+
+    this.currentClientX = clientX;
+    this.currentClientY = clientY;
+  }
+
+  handleTouchEnd(event) {
+    event.preventDefault();
+
+    // touch ended, removing the interval
+    clearInterval(this.intervalId);
+    this.intervalId = 0;
+
+    // resetting the touchStarted flag
+    this.touchStarted = false;
+
+    // setting the coords to negative values and send the last message (the cursor will disappear)
+    this.currentClientX = -1;
+    this.currentClientY = -1;
+    this.checkCursor();
+
+    window.removeEventListener('touchend', this.handleTouchEnd, { passive: false });
+    window.removeEventListener('touchmove', this.handleTouchMove, { passive: false });
+    window.removeEventListener('touchcancel', this.handleTouchCancel, true);
+  }
+
+  handleTouchCancel(event) {
+    event.preventDefault();
+
+    // touch was cancelled, removing the interval
+    clearInterval(this.intervalId);
+    this.intervalId = 0;
+
+    // resetting the touchStarted flag
+    this.touchStarted = false;
+
+    // setting the coords to negative values and send the last message (the cursor will disappear)
+    this.currentClientX = -1;
+    this.currentClientY = -1;
+    this.checkCursor();
+
+    window.removeEventListener('touchend', this.handleTouchEnd, { passive: false });
+    window.removeEventListener('touchmove', this.handleTouchMove, { passive: false });
+    window.removeEventListener('touchcancel', this.handleTouchCancel, true);
+  }
+
   mouseMoveHandler(event) {
+    if (this.touchStarted) {
+      return;
+    }
+
     // for the case where you change settings in one of the lists (which are displayed on the slide)
     // the mouse starts pointing to the slide right away and mouseEnter doesn't fire
     // so we call it manually here
@@ -75,11 +156,15 @@ export default class PresentationOverlay extends Component {
       this.mouseEnterHandler();
     }
 
-    this.currentClientX = event.nativeEvent.clientX;
-    this.currentClientY = event.nativeEvent.clientY;
+    this.currentClientX = event.clientX;
+    this.currentClientY = event.clientY;
   }
 
   mouseEnterHandler() {
+    if (this.touchStarted) {
+      return;
+    }
+
     const intervalId = setInterval(this.checkCursor, CURSOR_INTERVAL);
     this.intervalId = intervalId;
   }
@@ -105,10 +190,11 @@ export default class PresentationOverlay extends Component {
         height={this.props.slideHeight}
       >
         <div
+          onTouchStart={this.handleTouchStart}
           onMouseOut={this.mouseOutHandler}
           onMouseEnter={this.mouseEnterHandler}
           onMouseMove={this.mouseMoveHandler}
-          style={{ width: '100%', height: '100%' }}
+          style={{ width: '100%', height: '100%', touchAction: 'none' }}
         >
           {this.props.children}
         </div>
diff --git a/bigbluebutton-html5/imports/ui/components/settings/component.jsx b/bigbluebutton-html5/imports/ui/components/settings/component.jsx
index f442ddc4b9df23efc6404951f4162f600aa082ed..f0f50d876667d3d262e60382e1ca93968f60ce8a 100644
--- a/bigbluebutton-html5/imports/ui/components/settings/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/settings/component.jsx
@@ -142,12 +142,12 @@ class Settings extends Component {
             <Icon iconName="user" className={styles.icon} />
             <span id="ccTab">{intl.formatMessage(intlMessages.closecaptionTabLabel)}</span>
           </Tab>
-          {/* { isModerator ?*/}
-          {/* <Tab className={styles.tabSelector} aria-labelledby="usersTab">*/}
-          {/* <Icon iconName="user" className={styles.icon} />*/}
-          {/* <span id="usersTab">{intl.formatMessage(intlMessages.usersTabLabel)}</span>*/}
-          {/* </Tab>*/}
-          {/* : null }*/}
+          {/* { isModerator ? */}
+          {/* <Tab className={styles.tabSelector} aria-labelledby="usersTab"> */}
+          {/* <Icon iconName="user" className={styles.icon} /> */}
+          {/* <span id="usersTab">{intl.formatMessage(intlMessages.usersTabLabel)}</span> */}
+          {/* </Tab> */}
+          {/* : null } */}
         </TabList>
         <TabPanel className={styles.tabPanel}>
           <Application
@@ -169,14 +169,14 @@ class Settings extends Component {
             locales={this.props.locales}
           />
         </TabPanel>
-        {/* { isModerator ?*/}
-        {/* <TabPanel className={styles.tabPanel}>*/}
-        {/* <Participants*/}
-        {/* settings={this.state.current.participants}*/}
-        {/* handleUpdateSettings={this.handleUpdateSettings}*/}
-        {/* />*/}
-        {/* </TabPanel>*/}
-        {/* : null }*/}
+        {/* { isModerator ? */}
+        {/* <TabPanel className={styles.tabPanel}> */}
+        {/* <Participants */}
+        {/* settings={this.state.current.participants} */}
+        {/* handleUpdateSettings={this.handleUpdateSettings} */}
+        {/* /> */}
+        {/* </TabPanel> */}
+        {/* : null } */}
       </Tabs>
     );
   }
@@ -206,7 +206,6 @@ class Settings extends Component {
       </Modal>
     );
   }
-
 }
 
 Settings.propTypes = propTypes;
diff --git a/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx b/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx
index 0c1600336b5c536388e105adf581da1d06c87347..650b646ddda0267b3eed20cd364bf3425c274b22 100644
--- a/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx
@@ -146,7 +146,7 @@ class ApplicationMenu extends BaseMenu {
                   icons={false}
                   defaultChecked={this.state.settings.chatAudioNotifications}
                   onChange={() => this.handleToggle('chatAudioNotifications')}
-                  ariaLabelledBy={'audioNotify'}
+                  ariaLabelledBy="audioNotify"
                   ariaLabel={intl.formatMessage(intlMessages.audioNotifyLabel)}
                 />
               </div>
@@ -167,7 +167,7 @@ class ApplicationMenu extends BaseMenu {
                   icons={false}
                   defaultChecked={this.state.settings.chatPushNotifications}
                   onChange={() => this.handleToggle('chatPushNotifications')}
-                  ariaLabelledBy={'pushNotify'}
+                  ariaLabelledBy="pushNotify"
                   ariaLabel={intl.formatMessage(intlMessages.pushNotifyLabel)}
                 />
               </div>
@@ -183,23 +183,22 @@ class ApplicationMenu extends BaseMenu {
             </div>
             <div className={styles.col}>
               <label aria-labelledby="changeLangLabel" className={cx(styles.formElement, styles.pullContentRight)}>
-                <select
-                  defaultValue={this.formatLocale(this.state.settings.locale)}
-                  className={styles.select}
-                  onChange={this.handleSelectChange.bind(this, 'locale', availableLocales)}
-                >
-                  <option disabled>
-                    { availableLocales &&
-                        availableLocales.length ?
-                        intl.formatMessage(intlMessages.languageOptionLabel) :
-                        intl.formatMessage(intlMessages.noLocaleOptionLabel) }
-                  </option>
-                  {availableLocales ? availableLocales.map((locale, index) =>
-                    (<option key={index} value={locale.locale}>
-                      {locale.name}
-                    </option>),
-                  ) : null }
-                </select>
+      { availableLocales && availableLocales.length > 0 ?
+                  <select
+                    defaultValue={this.formatLocale(this.state.settings.locale)}
+                    className={styles.select}
+                    onChange={this.handleSelectChange.bind(this, 'locale', availableLocales)}
+                  >
+                    <option disabled>
+                      { intl.formatMessage(intlMessages.languageOptionLabel) }
+                    </option>
+                    { availableLocales.map((locale, index) =>
+                      (<option key={index} value={locale.locale}>
+                        {locale.name}
+                      </option>),
+                    ) }
+                  </select>
+                : null }
               </label>
               <div
                 id="changeLangLabel"
@@ -229,8 +228,8 @@ class ApplicationMenu extends BaseMenu {
                   <div className={styles.col}>
                     <Button
                       onClick={() => this.handleIncreaseFontSize()}
-                      color={'primary'}
-                      icon={'add'}
+                      color="primary"
+                      icon="add"
                       circle
                       hideLabel
                       label={intl.formatMessage(intlMessages.increaseFontBtnLabel)}
@@ -239,8 +238,8 @@ class ApplicationMenu extends BaseMenu {
                   <div className={styles.col}>
                     <Button
                       onClick={() => this.handleDecreaseFontSize()}
-                      color={'primary'}
-                      icon={'substract'}
+                      color="primary"
+                      icon="substract"
                       circle
                       hideLabel
                       label={intl.formatMessage(intlMessages.decreaseFontBtnLabel)}
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx
index 86831ac621e4bd49f36c46b5799f4ae6e03d0fcd..8600181af8daa7a6a7d5bfaf1fc379d794c2e529 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/component.jsx
@@ -25,6 +25,7 @@ const intlMessages = defineMessages({
 
 const CHAT_CONFIG = Meteor.settings.public.chat;
 const PRIVATE_CHAT_PATH = CHAT_CONFIG.path_route;
+const CLOSED_CHAT_PATH = 'users/';
 
 const propTypes = {
   chat: PropTypes.shape({
@@ -53,11 +54,12 @@ const ChatListItem = (props) => {
     intl,
     tabIndex,
     isPublicChat,
-    } = props;
+    location,
+  } = props;
 
-  const linkPath = [PRIVATE_CHAT_PATH, chat.id].join('');
+  let linkPath = [PRIVATE_CHAT_PATH, chat.id].join('');
+  linkPath = location.pathname.includes(linkPath) ? CLOSED_CHAT_PATH : linkPath;
   const isCurrentChat = chat.id === openChat;
-
   const linkClasses = {};
   linkClasses[styles.active] = isCurrentChat;
 
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/service.js b/bigbluebutton-html5/imports/ui/components/user-list/service.js
index e7fcc717dff622701e7d7e9d118f4719093b1032..740b0d84c34f4b7efaf258745acb0a56ef50416c 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/service.js
+++ b/bigbluebutton-html5/imports/ui/components/user-list/service.js
@@ -5,7 +5,7 @@ import Auth from '/imports/ui/services/auth';
 import UnreadMessages from '/imports/ui/services/unread-messages';
 import Storage from '/imports/ui/services/storage/session';
 import mapUser from '/imports/ui/services/user/mapUser';
-import { EMOJI_STATUSES, EMOJI_NORMALIZE } from '/imports/utils/statuses';
+import { EMOJI_STATUSES } from '/imports/utils/statuses';
 import { makeCall } from '/imports/ui/services/api';
 import _ from 'lodash';
 
@@ -253,7 +253,7 @@ const getCurrentUser = () => {
 };
 
 const normalizeEmojiName = emoji => (
-  emoji in EMOJI_NORMALIZE ? EMOJI_NORMALIZE[emoji] : emoji
+  emoji in EMOJI_STATUSES ? EMOJI_STATUSES[emoji] : emoji
 );
 
 const isMeetingLocked = (id) => {
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/styles.scss b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/styles.scss
index 9b724cfd156f363791772857d91be369be2da3c6..acbc8846aa7aafc2bc2c48c2af9f26519e5d2c7d 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/styles.scss
@@ -10,10 +10,29 @@
 }
 
 .scrollableList {
+  pointer-events: none;
   @include elementFocus($list-item-bg-hover);
   @include scrollbox-vertical($user-list-bg);
 }
 
 .list {
   margin-left: $md-padding-y;
+  pointer-events: all;
+}
+
+.smallTitle {
+  @extend .smallTitle;
+}
+
+.separator {
+  margin: 1rem auto;
+  width: 2.2rem;
+  border: 0;
+  border-top: 1px solid $color-gray-lighter;
+}
+
+.chatsList {
+  @extend .lists;
+  overflow: hidden;
+  flex-shrink: 1;
 }
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-messages/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-messages/component.jsx
index 79b487bf4887c73266b7abbe4a88f3ab09b53cfd..7d07842213a8da2ff5aa741f3564f9529b20bad6 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-messages/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-messages/component.jsx
@@ -3,7 +3,7 @@ import { TransitionGroup, CSSTransition } from 'react-transition-group';
 import PropTypes from 'prop-types';
 import { defineMessages } from 'react-intl';
 import cx from 'classnames';
-import styles from './styles';
+import styles from '/imports/ui/components/user-list/user-list-content/styles';
 import ChatListItem from './../../chat-list-item/component';
 
 const propTypes = {
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-messages/styles.scss b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-messages/styles.scss
deleted file mode 100644
index 99cd30139cd05b1e4aef9e8144d88e818fdf31f3..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-messages/styles.scss
+++ /dev/null
@@ -1,28 +0,0 @@
-@import "/imports/ui/components/user-list/styles.scss";
-@import "/imports/ui/stylesheets/mixins/focus";
-
-.smallTitle {
-  @extend .smallTitle;
-}
-
-.scrollableList {
-  @include elementFocus($list-item-bg-hover);
-  @include scrollbox-vertical($user-list-bg);
-}
-
-.separator {
-  margin: 1rem auto;
-  width: 2.2rem;
-  border: 0;
-  border-top: 1px solid $color-gray-lighter;
-}
-
-.chatsList {
-  @extend .lists;
-  overflow: hidden;
-  flex-shrink: 1;
-}
-
-.list {
-  margin-left: $md-padding-y;
-}
diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/annotations/text/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/annotations/text/component.jsx
index c7c0e963bbe06c6f613fed441993f74f61c7e31b..a35217a1220666195221499febcf51512d2f9e44 100644
--- a/bigbluebutton-html5/imports/ui/components/whiteboard/annotations/text/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/whiteboard/annotations/text/component.jsx
@@ -55,6 +55,19 @@ export default class TextDrawComponent extends Component {
   }
 
   componentDidMount() {
+    // iOS doesn't show the keyboard if the input field was focused by event NOT invoked by a user
+    // by it still technically moves the focus there
+    // that's why we have a separate case for iOS - we don't focus here automatically
+    // but we focus on the next "tap" invoked by a user
+    const iOS = ['iPad', 'iPhone', 'iPod'].indexOf(navigator.platform) >= 0;
+
+    // unsupported Firefox condition (not iOS though) can be removed when FF 59 is released
+    // see https://bugzilla.mozilla.org/show_bug.cgi?id=1409113
+    const unsupportedFirefox = navigator.userAgent.indexOf('Firefox/57') !== -1
+                            || navigator.userAgent.indexOf('Firefox/58') !== -1;
+
+    if (iOS || unsupportedFirefox) { return; }
+
     if (this.props.isActive && this.props.annotation.status !== DRAW_END) {
       this.handleFocus();
     }
diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/component.jsx
index 436ec2d928b153fed801bb0ca2789f74b4142ebb..afd48151ad422840f59d147df7d86998e780d42d 100644
--- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/component.jsx
@@ -6,7 +6,6 @@ import PencilDrawListener from './pencil-draw-listener/component';
 import PanZoomDrawListener from './pan-zoom-draw-listener/component';
 
 export default class WhiteboardOverlay extends Component {
-
   // a function to transform a screen point to svg point
   // accepts and returns a point of type SvgPoint and an svg object
   static coordinateTransform(screenPoint, someSvgObject) {
@@ -48,11 +47,11 @@ export default class WhiteboardOverlay extends Component {
 
   // this function receives an event from the mouse event attached to the window
   // it transforms the coordinate to the main svg coordinate system
-  getTransformedSvgPoint(event) {
+  getTransformedSvgPoint(clientX, clientY) {
     const svgObject = this.props.getSvgRef();
     const svgPoint = svgObject.createSVGPoint();
-    svgPoint.x = event.clientX;
-    svgPoint.y = event.clientY;
+    svgPoint.x = clientX;
+    svgPoint.y = clientY;
     const transformedSvgPoint = WhiteboardOverlay.coordinateTransform(svgPoint, svgObject);
 
     return transformedSvgPoint;
@@ -84,10 +83,11 @@ export default class WhiteboardOverlay extends Component {
 
   // this function receives a transformed svg coordinate and checks if it's not out of bounds
   checkIfOutOfBounds(point) {
-    const { viewBoxX, viewBoxY, viewBoxWidth, viewBoxHeight } = this.props;
+    const {
+      viewBoxX, viewBoxY, viewBoxWidth, viewBoxHeight,
+    } = this.props;
 
-    let x = point.x;
-    let y = point.y;
+    let { x, y } = point;
 
     // set this flag to true if either x or y are out of bounds
     let shouldUnselect = false;
@@ -120,7 +120,8 @@ export default class WhiteboardOverlay extends Component {
   }
 
   render() {
-    const { drawSettings,
+    const {
+      drawSettings,
       userId,
       whiteboardId,
       sendAnnotation,
diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/pencil-draw-listener/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/pencil-draw-listener/component.jsx
index 814b5b035a6b96376997c1dd8a2d257590f68fb2..50937ae453251923082c483eba3a11c571590894 100644
--- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/pencil-draw-listener/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/pencil-draw-listener/component.jsx
@@ -22,8 +22,13 @@ export default class PencilDrawListener extends Component {
     this.mouseDownHandler = this.mouseDownHandler.bind(this);
     this.mouseMoveHandler = this.mouseMoveHandler.bind(this);
     this.mouseUpHandler = this.mouseUpHandler.bind(this);
+    this.resetState = this.resetState.bind(this);
     this.sendLastMessage = this.sendLastMessage.bind(this);
     this.sendCoordinates = this.sendCoordinates.bind(this);
+    this.handleTouchStart = this.handleTouchStart.bind(this);
+    this.handleTouchMove = this.handleTouchMove.bind(this);
+    this.handleTouchEnd = this.handleTouchEnd.bind(this);
+    this.handleTouchCancel = this.handleTouchCancel.bind(this);
   }
 
   componentDidMount() {
@@ -38,41 +43,31 @@ export default class PencilDrawListener extends Component {
     this.sendLastMessage();
   }
 
-  // main mouse down handler
-  mouseDownHandler(event) {
-    if (!this.isDrawing) {
-      window.addEventListener('mouseup', this.mouseUpHandler);
-      window.addEventListener('mousemove', this.mouseMoveHandler, true);
-      this.isDrawing = true;
-
-      const {
-        getTransformedSvgPoint,
-        generateNewShapeId,
-        svgCoordinateToPercentages,
-      } = this.props.actions;
+  commonDrawStartHandler(clientX, clientY) {
+    // changing isDrawing to true
+    this.isDrawing = true;
 
-      // sending the first message
-      let transformedSvgPoint = getTransformedSvgPoint(event);
+    const {
+      getTransformedSvgPoint,
+      generateNewShapeId,
+      svgCoordinateToPercentages,
+    } = this.props.actions;
 
-      // transforming svg coordinate to percentages relative to the slide width/height
-      transformedSvgPoint = svgCoordinateToPercentages(transformedSvgPoint);
+    // sending the first message
+    let transformedSvgPoint = getTransformedSvgPoint(clientX, clientY);
 
-      // sending the first message
-      const _points = [transformedSvgPoint.x, transformedSvgPoint.y];
-      this.handleDrawPencil(_points, DRAW_START, generateNewShapeId());
+    // transforming svg coordinate to percentages relative to the slide width/height
+    transformedSvgPoint = svgCoordinateToPercentages(transformedSvgPoint);
 
-      // All the DRAW_UPDATE messages will be send on timer by sendCoordinates func
-      this.intervalId = setInterval(this.sendCoordinates, MESSAGE_FREQUENCY);
+    // sending the first message
+    const _points = [transformedSvgPoint.x, transformedSvgPoint.y];
+    this.handleDrawPencil(_points, DRAW_START, generateNewShapeId());
 
-    // if you switch to a different window using Alt+Tab while mouse is down and release it
-    // it wont catch mouseUp and will keep tracking the movements. Thus we need this check.
-    } else {
-      this.sendLastMessage();
-    }
+    // All the DRAW_UPDATE messages will be send on timer by sendCoordinates func
+    this.intervalId = setInterval(this.sendCoordinates, MESSAGE_FREQUENCY);
   }
 
-  // main mouse move handler
-  mouseMoveHandler(event) {
+  commonDrawMoveHandler(clientX, clientY) {
     if (this.isDrawing) {
       const {
         checkIfOutOfBounds,
@@ -81,7 +76,7 @@ export default class PencilDrawListener extends Component {
       } = this.props.actions;
 
       // get the transformed svg coordinate
-      let transformedSvgPoint = getTransformedSvgPoint(event);
+      let transformedSvgPoint = getTransformedSvgPoint(clientX, clientY);
 
       // check if it's out of bounds
       transformedSvgPoint = checkIfOutOfBounds(transformedSvgPoint);
@@ -95,6 +90,58 @@ export default class PencilDrawListener extends Component {
     }
   }
 
+  handleTouchStart(event) {
+    event.preventDefault();
+    if (!this.isDrawing) {
+      window.addEventListener('touchend', this.handleTouchEnd, { passive: false });
+      window.addEventListener('touchmove', this.handleTouchMove, { passive: false });
+      window.addEventListener('touchcancel', this.handleTouchCancel, true);
+
+      const { clientX, clientY } = event.changedTouches[0];
+      this.commonDrawStartHandler(clientX, clientY);
+
+    // if you switch to a different window using Alt+Tab while mouse is down and release it
+    // it wont catch mouseUp and will keep tracking the movements. Thus we need this check.
+    } else {
+      this.sendLastMessage();
+    }
+  }
+
+  handleTouchMove(event) {
+    const { clientX, clientY } = event.changedTouches[0];
+    this.commonDrawMoveHandler(clientX, clientY);
+  }
+
+  handleTouchEnd() {
+    this.sendLastMessage();
+  }
+
+  handleTouchCancel() {
+    this.sendLastMessage();
+  }
+
+  // main mouse down handler
+  mouseDownHandler(event) {
+    if (!this.isDrawing) {
+      window.addEventListener('mouseup', this.mouseUpHandler);
+      window.addEventListener('mousemove', this.mouseMoveHandler, true);
+
+      const { clientX, clientY } = event;
+      this.commonDrawStartHandler(clientX, clientY);
+
+    // if you switch to a different window using Alt+Tab while mouse is down and release it
+    // it wont catch mouseUp and will keep tracking the movements. Thus we need this check.
+    } else {
+      this.sendLastMessage();
+    }
+  }
+
+  // main mouse move handler
+  mouseMoveHandler(event) {
+    const { clientX, clientY } = event;
+    this.commonDrawMoveHandler(clientX, clientY);
+  }
+
   // main mouse up handler
   mouseUpHandler() {
     this.sendLastMessage();
@@ -153,21 +200,30 @@ export default class PencilDrawListener extends Component {
         getCurrentShapeId(),
         [Math.round(physicalSlideWidth), Math.round(physicalSlideHeight)],
       );
-
-      // resetting the current info
-      this.points = [];
-      this.isDrawing = false;
-      window.removeEventListener('mouseup', this.mouseUpHandler);
-      window.removeEventListener('mousemove', this.mouseMoveHandler, true);
+      this.resetState();
     }
   }
 
+  resetState() {
+    // resetting the current info
+    this.points = [];
+    this.isDrawing = false;
+    // mouseup and mousemove are removed on desktop
+    window.removeEventListener('mouseup', this.mouseUpHandler);
+    window.removeEventListener('mousemove', this.mouseMoveHandler, true);
+    // touchend, touchmove and touchcancel are removed on devices
+    window.removeEventListener('touchend', this.handleTouchEnd, { passive: false });
+    window.removeEventListener('touchmove', this.handleTouchMove, { passive: false });
+    window.removeEventListener('touchcancel', this.handleTouchCancel, true);
+  }
+
   render() {
     return (
       <div
+        onTouchStart={this.handleTouchStart}
         role="presentation"
         className={styles.pencil}
-        style={{ width: '100%', height: '100%' }}
+        style={{ width: '100%', height: '100%', touchAction: 'none' }}
         onMouseDown={this.mouseDownHandler}
       />
     );
@@ -175,7 +231,7 @@ export default class PencilDrawListener extends Component {
 }
 
 PencilDrawListener.propTypes = {
-    // Defines a whiteboard id, which needed to publish an annotation message
+  // Defines a whiteboard id, which needed to publish an annotation message
   whiteboardId: PropTypes.string.isRequired,
   // Defines a user id, which needed to publish an annotation message
   userId: PropTypes.string.isRequired,
diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/shape-draw-listener/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/shape-draw-listener/component.jsx
index fd8ceaa8b93539c42316cbc18274506c1827af21..6907d264b9a7c45ea3a4e4d152961b6fd342befa 100644
--- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/shape-draw-listener/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/shape-draw-listener/component.jsx
@@ -40,6 +40,10 @@ export default class ShapeDrawListener extends Component {
     this.resetState = this.resetState.bind(this);
     this.sendLastMessage = this.sendLastMessage.bind(this);
     this.sendCoordinates = this.sendCoordinates.bind(this);
+    this.handleTouchStart = this.handleTouchStart.bind(this);
+    this.handleTouchMove = this.handleTouchMove.bind(this);
+    this.handleTouchEnd = this.handleTouchEnd.bind(this);
+    this.handleTouchCancel = this.handleTouchCancel.bind(this);
   }
 
   componentDidMount() {
@@ -54,16 +58,7 @@ export default class ShapeDrawListener extends Component {
     this.sendLastMessage();
   }
 
-  // main mouse down handler
-  handleMouseDown(event) {
-    // Sometimes when you Alt+Tab while drawing it can happen that your mouse is up,
-    // but the browser didn't catch it. So check it here.
-    if (this.isDrawing) {
-      return this.sendLastMessage();
-    }
-
-    window.addEventListener('mouseup', this.handleMouseUp);
-    window.addEventListener('mousemove', this.handleMouseMove, true);
+  commonDrawStartHandler(clientX, clientY) {
     this.isDrawing = true;
 
     const {
@@ -73,7 +68,7 @@ export default class ShapeDrawListener extends Component {
     } = this.props.actions;
 
     // sending the first message
-    let transformedSvgPoint = getTransformedSvgPoint(event);
+    let transformedSvgPoint = getTransformedSvgPoint(clientX, clientY);
 
     // transforming svg coordinate to percentages relative to the slide width/height
     transformedSvgPoint = svgCoordinateToPercentages(transformedSvgPoint);
@@ -97,12 +92,9 @@ export default class ShapeDrawListener extends Component {
 
     // All the messages will be send on timer by sendCoordinates func
     this.intervalId = setInterval(this.sendCoordinates, MESSAGE_FREQUENCY);
-
-    return true;
   }
 
-  // main mouse move handler
-  handleMouseMove(event) {
+  commonDrawMoveHandler(clientX, clientY) {
     if (!this.isDrawing) {
       return;
     }
@@ -114,7 +106,7 @@ export default class ShapeDrawListener extends Component {
     } = this.props.actions;
 
     // get the transformed svg coordinate
-    let transformedSvgPoint = getTransformedSvgPoint(event);
+    let transformedSvgPoint = getTransformedSvgPoint(clientX, clientY);
 
     // check if it's out of bounds
     transformedSvgPoint = checkIfOutOfBounds(transformedSvgPoint);
@@ -126,6 +118,58 @@ export default class ShapeDrawListener extends Component {
     this.currentCoordinate = transformedSvgPoint;
   }
 
+  handleTouchStart(event) {
+    event.preventDefault();
+
+    if (!this.isDrawing) {
+      window.addEventListener('touchend', this.handleTouchEnd, { passive: false });
+      window.addEventListener('touchmove', this.handleTouchMove, { passive: false });
+      window.addEventListener('touchcancel', this.handleTouchCancel, true);
+
+      const { clientX, clientY } = event.changedTouches[0];
+      this.commonDrawStartHandler(clientX, clientY);
+
+    // if you switch to a different window using Alt+Tab while mouse is down and release it
+    // it wont catch mouseUp and will keep tracking the movements. Thus we need this check.
+    } else {
+      this.sendLastMessage();
+    }
+  }
+
+  handleTouchMove(event) {
+    const { clientX, clientY } = event.changedTouches[0];
+    this.commonDrawMoveHandler(clientX, clientY);
+  }
+
+  handleTouchEnd() {
+    this.sendLastMessage();
+  }
+
+  handleTouchCancel() {
+    this.sendLastMessage();
+  }
+
+  // main mouse down handler
+  handleMouseDown(event) {
+    // Sometimes when you Alt+Tab while drawing it can happen that your mouse is up,
+    // but the browser didn't catch it. So check it here.
+    if (this.isDrawing) {
+      return this.sendLastMessage();
+    }
+
+    window.addEventListener('mouseup', this.handleMouseUp);
+    window.addEventListener('mousemove', this.handleMouseMove, true);
+
+    const { clientX, clientY } = event;
+    return this.commonDrawStartHandler(clientX, clientY);
+  }
+
+  // main mouse move handler
+  handleMouseMove(event) {
+    const { clientX, clientY } = event;
+    this.commonDrawMoveHandler(clientX, clientY);
+  }
+
   // main mouse up handler
   handleMouseUp() {
     this.sendLastMessage();
@@ -190,6 +234,10 @@ export default class ShapeDrawListener extends Component {
     // resetting the current drawing state
     window.removeEventListener('mouseup', this.handleMouseUp);
     window.removeEventListener('mousemove', this.handleMouseMove, true);
+    // touchend, touchmove and touchcancel are removed on devices
+    window.removeEventListener('touchend', this.handleTouchEnd, { passive: false });
+    window.removeEventListener('touchmove', this.handleTouchMove, { passive: false });
+    window.removeEventListener('touchcancel', this.handleTouchCancel, true);
     this.isDrawing = false;
     this.currentStatus = undefined;
     this.initialCoordinate = {
@@ -238,12 +286,13 @@ export default class ShapeDrawListener extends Component {
   }
 
   render() {
-    const tool = this.props.drawSettings.tool;
+    const { tool } = this.props.drawSettings;
     return (
       <div
+        onTouchStart={this.handleTouchStart}
         role="presentation"
         className={styles[tool]}
-        style={{ width: '100%', height: '100%' }}
+        style={{ width: '100%', height: '100%', touchAction: 'none' }}
         onMouseDown={this.handleMouseDown}
       />
     );
diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/text-draw-listener/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/text-draw-listener/component.jsx
index bf29ccac2d77482291fe83e5b48af38ac65fc2b2..1e12e94cde29485aea82c7ae80967b4d81ce4234 100644
--- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/text-draw-listener/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/text-draw-listener/component.jsx
@@ -39,11 +39,23 @@ export default class TextDrawListener extends Component {
     // current text shape status, it may change between DRAW_START, DRAW_UPDATE, DRAW_END
     this.currentStatus = '';
 
+    // Mobile Firefox has a bug where e.preventDefault on touchstart doesn't prevent
+    // onmousedown from triggering right after. Thus we have to track it manually.
+    // In case if it's fixed one day - there is another issue, React one.
+    // https://github.com/facebook/react/issues/9809
+    // Check it to figure if you can add onTouchStart in render(), or should use raw DOM api
+    this.hasBeenTouchedRecently = false;
+
     this.handleMouseDown = this.handleMouseDown.bind(this);
     this.handleMouseMove = this.handleMouseMove.bind(this);
     this.handleMouseUp = this.handleMouseUp.bind(this);
     this.resetState = this.resetState.bind(this);
     this.sendLastMessage = this.sendLastMessage.bind(this);
+    this.handleTouchStart = this.handleTouchStart.bind(this);
+    this.handleTouchMove = this.handleTouchMove.bind(this);
+    this.handleTouchEnd = this.handleTouchEnd.bind(this);
+    this.handleTouchCancel = this.handleTouchCancel.bind(this);
+    this.checkTextAreaFocus = this.checkTextAreaFocus.bind(this);
   }
 
   componentDidMount() {
@@ -92,46 +104,85 @@ export default class TextDrawListener extends Component {
 
   componentWillUnmount() {
     window.removeEventListener('beforeunload', this.sendLastMessage);
-    window.removeEventListener('mouseup', this.handleMouseUp);
-    window.removeEventListener('mousemove', this.handleMouseMove, true);
-
     // sending the last message on componentDidUnmount
     // for example in case when you switched a tool while drawing text shape
     this.sendLastMessage();
   }
 
-  // main mouse down handler
-  handleMouseDown(event) {
-    this.mouseDownText(event);
+  // checks if the input textarea is focused or not, and if not - moves focus there
+  // returns false if text area wasn't focused
+  // returns true if textarea was focused
+  // currently used only with iOS devices
+  checkTextAreaFocus() {
+    const { getCurrentShapeId } = this.props.actions;
+    const textarea = document.getElementById(getCurrentShapeId());
+
+    if (document.activeElement === textarea) {
+      return true;
+    }
+    textarea.focus();
+    return false;
   }
 
-  // main mouse up handler
-  handleMouseUp(event) {
-    window.removeEventListener('mouseup', this.handleMouseUp);
-    window.removeEventListener('mousemove', this.handleMouseMove, true);
-    this.mouseUpText(event);
+  handleTouchStart(event) {
+    this.hasBeenTouchedRecently = true;
+    setTimeout(() => { this.hasBeenTouchedRecently = false; }, 500);
+    // to prevent default behavior (scrolling) on devices (in Safari), when you draw a text box
+    event.preventDefault();
+
+
+    // if our current drawing state is not drawing the box and not writing the text
+    if (!this.state.isDrawing && !this.state.isWritingText) {
+      window.addEventListener('touchend', this.handleTouchEnd, { passive: false });
+      window.addEventListener('touchmove', this.handleTouchMove, { passive: false });
+      window.addEventListener('touchcancel', this.handleTouchCancel, true);
+
+      const { clientX, clientY } = event.changedTouches[0];
+      this.commonDrawStartHandler(clientX, clientY);
+
+    // this case is specifically for iOS, since text shape is working in 3 steps there:
+    // touch to draw a box -> tap to focus -> tap to publish
+    } else if (!this.state.isDrawing && this.state.isWritingText && !this.checkTextAreaFocus()) {
+
+    // if you switch to a different window using Alt+Tab while mouse is down and release it
+    // it wont catch mouseUp and will keep tracking the movements. Thus we need this check.
+    } else {
+      this.sendLastMessage();
+    }
   }
 
-  // main mouse move handler
-  handleMouseMove(event) {
-    this.mouseMoveText(event);
+  handleTouchMove(event) {
+    const { clientX, clientY } = event.changedTouches[0];
+    this.commonDrawMoveHandler(clientX, clientY);
   }
 
-  mouseDownText(event) {
+  handleTouchEnd() {
+    window.removeEventListener('touchend', this.handleTouchEnd, { passive: false });
+    window.removeEventListener('touchmove', this.handleTouchMove, { passive: false });
+    window.removeEventListener('touchcancel', this.handleTouchCancel, true);
+    this.commonDrawEndHandler();
+  }
+
+  handleTouchCancel() {
+    window.removeEventListener('touchend', this.handleTouchEnd, { passive: false });
+    window.removeEventListener('touchmove', this.handleTouchMove, { passive: false });
+    window.removeEventListener('touchcancel', this.handleTouchCancel, true);
+    this.commonDrawEndHandler();
+  }
+
+  // main mouse down handler
+  handleMouseDown(event) {
+    if (this.hasBeenTouchedRecently) {
+      return;
+    }
+
     // if our current drawing state is not drawing the box and not writing the text
     if (!this.state.isDrawing && !this.state.isWritingText) {
       window.addEventListener('mouseup', this.handleMouseUp);
       window.addEventListener('mousemove', this.handleMouseMove, true);
 
-      // saving initial X and Y coordinates for further displaying of the textarea
-      this.initialX = event.nativeEvent.offsetX;
-      this.initialY = event.nativeEvent.offsetY;
-
-      this.setState({
-        textBoxX: event.nativeEvent.offsetX,
-        textBoxY: event.nativeEvent.offsetY,
-        isDrawing: true,
-      });
+      const { clientX, clientY } = event;
+      this.commonDrawStartHandler(clientX, clientY);
 
     // second case is when a user finished writing the text and publishes the final result
     } else {
@@ -140,6 +191,35 @@ export default class TextDrawListener extends Component {
     }
   }
 
+  // main mouse move handler
+  handleMouseMove(event) {
+    const { clientX, clientY } = event;
+    this.commonDrawMoveHandler(clientX, clientY);
+  }
+
+  // main mouse up handler
+  handleMouseUp() {
+    window.removeEventListener('mouseup', this.handleMouseUp);
+    window.removeEventListener('mousemove', this.handleMouseMove, true);
+    this.commonDrawEndHandler();
+  }
+
+  commonDrawStartHandler(clientX, clientY) {
+    const { getTransformedSvgPoint } = this.props.actions;
+
+    const transformedSvgPoint = getTransformedSvgPoint(clientX, clientY);
+
+    // saving initial X and Y coordinates for further displaying of the textarea
+    this.initialX = transformedSvgPoint.x;
+    this.initialY = transformedSvgPoint.y;
+
+    this.setState({
+      textBoxX: transformedSvgPoint.x,
+      textBoxY: transformedSvgPoint.y,
+      isDrawing: true,
+    });
+  }
+
   sendLastMessage() {
     if (!this.state.isWritingText) {
       return;
@@ -161,6 +241,14 @@ export default class TextDrawListener extends Component {
   }
 
   resetState() {
+    // resetting the current drawing state
+    window.removeEventListener('mouseup', this.handleMouseUp);
+    window.removeEventListener('mousemove', this.handleMouseMove, true);
+    // touchend, touchmove and touchcancel are removed on devices
+    window.removeEventListener('touchend', this.handleTouchEnd, { passive: false });
+    window.removeEventListener('touchmove', this.handleTouchMove, { passive: false });
+    window.removeEventListener('touchcancel', this.handleTouchCancel, true);
+
     // resetting the text shape session values
     this.props.actions.resetTextShapeSession();
     // resetting the current state
@@ -182,11 +270,11 @@ export default class TextDrawListener extends Component {
     });
   }
 
-  mouseMoveText(event) {
+  commonDrawMoveHandler(clientX, clientY) {
     const { checkIfOutOfBounds, getTransformedSvgPoint } = this.props.actions;
 
     // get the transformed svg coordinate
-    let transformedSvgPoint = getTransformedSvgPoint(event);
+    let transformedSvgPoint = getTransformedSvgPoint(clientX, clientY);
 
     // check if it's out of bounds
     transformedSvgPoint = checkIfOutOfBounds(transformedSvgPoint);
@@ -197,9 +285,9 @@ export default class TextDrawListener extends Component {
 
     // calculating the width and height of the displayed text box
     const width = transformedSvgPoint.x > this.initialX ?
-        transformedSvgPoint.x - this.initialX : this.initialX - transformedSvgPoint.x;
+      transformedSvgPoint.x - this.initialX : this.initialX - transformedSvgPoint.x;
     const height = transformedSvgPoint.y > this.initialY ?
-        transformedSvgPoint.y - this.initialY : this.initialY - transformedSvgPoint.y;
+      transformedSvgPoint.y - this.initialY : this.initialY - transformedSvgPoint.y;
 
     this.setState({
       textBoxWidth: width,
@@ -210,13 +298,14 @@ export default class TextDrawListener extends Component {
   }
 
 
-  mouseUpText() {
+  commonDrawEndHandler() {
     // TODO - find if the size is large enough to display the text area
     if (!this.state.isDrawing && this.state.isWritingText) {
       return;
     }
 
-    const { generateNewShapeId,
+    const {
+      generateNewShapeId,
       getCurrentShapeId,
       setTextShapeActiveId,
     } = this.props.actions;
@@ -284,8 +373,9 @@ export default class TextDrawListener extends Component {
       <div
         role="presentation"
         className={styles.text}
-        style={{ width: '100%', height: '100%' }}
+        style={{ width: '100%', height: '100%', touchAction: 'none' }}
         onMouseDown={this.handleMouseDown}
+        onTouchStart={this.handleTouchStart}
       >
         {this.state.isDrawing ?
           <svg
diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/toolbar-menu-item/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/toolbar-menu-item/component.jsx
index bb782817ff3824f4f1c4d27cb5c9cd70db921996..03888407293c5a2fa3a27c50bf8fd8668d50a781 100644
--- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/toolbar-menu-item/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/toolbar-menu-item/component.jsx
@@ -1,16 +1,51 @@
 import React, { Component } from 'react';
 import PropTypes from 'prop-types';
 import Button from '/imports/ui/components/button/component';
+import _ from 'lodash';
 import styles from '../styles';
 
 export default class ToolbarMenuItem extends Component {
   constructor() {
     super();
 
-    this.handleItemClick = this.handleItemClick.bind(this);
+    this.handleTouchStart = this.handleTouchStart.bind(this);
+    this.handleOnMouseUp = this.handleOnMouseUp.bind(this);
+    this.setRef = this.setRef.bind(this);
   }
 
-  handleItemClick() {
+  // generating a unique ref string for the toolbar-item
+  componentWillMount() {
+    this.uniqueRef = _.uniqueId('toolbar-menu-item');
+  }
+
+  componentDidMount() {
+    // adding and removing touchstart events can be done via standard React way
+    // by passing onTouchStart={this.funcName} once they stop triggering mousedown events
+    // see https://github.com/facebook/react/issues/9809
+    this[this.uniqueRef].addEventListener('touchstart', this.handleTouchStart);
+  }
+
+  componentWillUnmount() {
+    this[this.uniqueRef].removeEventListener('touchstart', this.handleTouchStart);
+  }
+
+  setRef(ref) {
+    this[this.uniqueRef] = ref;
+  }
+
+  // we have to use touchStart and on mouseUp in order to be able to use the toolbar
+  // with the text shape on mobile devices
+  // (using the toolbar while typing text shouldn't move focus out of the textarea)
+  handleTouchStart(event) {
+    event.preventDefault();
+    const { objectToReturn, onItemClick } = this.props;
+    // if there is a submenu name, then pass it to onClick
+    // if not - it's probably "Undo", "Clear All", "Multi-user", etc.
+    // in the second case we'll pass undefined and it will work fine anyway
+    onItemClick(objectToReturn);
+  }
+
+  handleOnMouseUp() {
     const { objectToReturn, onItemClick } = this.props;
     // if there is a submenu name, then pass it to onClick
     // if not - it's probably "Undo", "Clear All", "Multi-user", etc.
@@ -24,14 +59,15 @@ export default class ToolbarMenuItem extends Component {
         <Button
           hideLabel
           role="button"
-          color={'default'}
-          size={'md'}
+          color="default"
+          size="md"
           label={this.props.label}
           icon={this.props.icon ? this.props.icon : null}
           customIcon={this.props.customIcon ? this.props.customIcon : null}
-          onClick={this.handleItemClick}
+          onMouseUp={this.handleOnMouseUp}
           onBlur={this.props.onBlur}
           className={this.props.className}
+          setRef={this.setRef}
         />
         {this.props.children}
       </div>
diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/toolbar-submenu-item/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/toolbar-submenu-item/component.jsx
index 2ac4c999d8bf26bce8e0e4ccf5382c94a00ce0d7..f66e1d58e2d301664cd94eccc813aea8112a373a 100644
--- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/toolbar-submenu-item/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/toolbar-submenu-item/component.jsx
@@ -1,20 +1,51 @@
 import React, { Component } from 'react';
 import PropTypes from 'prop-types';
 import Button from '/imports/ui/components/button/component';
+import _ from 'lodash';
 import styles from '../styles';
 
 export default class ToolbarSubmenuItem extends Component {
   constructor() {
     super();
 
-    this.handleItemClick = this.handleItemClick.bind(this);
+    this.handleTouchStart = this.handleTouchStart.bind(this);
+    this.handleOnMouseUp = this.handleOnMouseUp.bind(this);
+    this.setRef = this.setRef.bind(this);
   }
 
-  handleItemClick() {
+  // generating a unique ref string for the toolbar-item
+  componentWillMount() {
+    this.uniqueRef = _.uniqueId('toolbar-submenu-item');
+  }
+
+  componentDidMount() {
+    // adding and removing touchstart events can be done via standard React way
+    // by passing onTouchStart={this.funcName} once they stop triggering mousedown events
+    // see https://github.com/facebook/react/issues/9809
+    this[this.uniqueRef].addEventListener('touchstart', this.handleTouchStart);
+  }
+
+  componentWillUnmount() {
+    this[this.uniqueRef].removeEventListener('touchstart', this.handleTouchStart);
+  }
+
+  setRef(ref) {
+    this[this.uniqueRef] = ref;
+  }
+
+  // we have to use touchStart and on mouseUp in order to be able to use the toolbar
+  // with the text shape on mobile devices
+  // (using the toolbar while typing text shouldn't move focus out of the textarea)
+  handleTouchStart(event) {
+    event.preventDefault();
+    const { objectToReturn, onItemClick } = this.props;
+    // returning the selected object
+    onItemClick(objectToReturn);
+  }
+
+  handleOnMouseUp() {
     const { objectToReturn, onItemClick } = this.props;
-    // if there is a submenu name, then pass it to onClick
-    // if not - it's probably "Undo", "Clear All", "Multi-user", etc.
-    // in the second case we'll pass undefined and it will work fine anyway
+    // returning the selected object
     onItemClick(objectToReturn);
   }
 
@@ -24,13 +55,14 @@ export default class ToolbarSubmenuItem extends Component {
         <Button
           hideLabel
           role="button"
-          color={'default'}
-          size={'md'}
+          color="default"
+          size="md"
           label={this.props.label}
           icon={this.props.icon}
           customIcon={this.props.customIcon}
-          onClick={this.handleItemClick}
+          onMouseUp={this.handleOnMouseUp}
           className={this.props.className}
+          setRef={this.setRef}
         />
       </div>
     );
diff --git a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
index 2480edb5b44bae70e1cdfaf9b2172610af188c62..6bab22deee57027321f94b3e51a063af57b471a7 100644
--- a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
+++ b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
@@ -1,6 +1,8 @@
 import { Tracker } from 'meteor/tracker';
 import { makeCall } from '/imports/ui/services/api';
 import VertoBridge from '/imports/api/audio/client/bridge/verto';
+import Auth from '/imports/ui/services/auth';
+import VoiceUsers from '/imports/api/voice-users';
 import SIPBridge from '/imports/api/audio/client/bridge/sip';
 import { notify } from '/imports/ui/services/notification';
 
@@ -27,15 +29,26 @@ class AudioManager {
       isHangingUp: false,
       isListenOnly: false,
       isEchoTest: false,
+      isWaitingPermissions: false,
       error: null,
       outputDeviceId: null,
     });
+
+    const query = VoiceUsers.find({ intId: Auth.userID });
+
+    query.observeChanges({
+      changed: (id, fields) => {
+        if (fields.muted === this.isMuted) return;
+        this.isMuted = fields.muted;
+      },
+    });
   }
 
   init(userData, messages) {
     this.bridge = USE_SIP ? new SIPBridge(userData) : new VertoBridge(userData);
     this.userData = userData;
     this.messages = messages;
+    this.initialized = true;
   }
 
   defineProperties(obj) {
@@ -65,28 +78,46 @@ class AudioManager {
       isEchoTest,
     } = options;
 
-    if (!this.devicesInitialized) {
-      this.setDefaultInputDevice();
-      this.changeOutputDevice('default');
-      this.devicesInitialized = true;
-    }
+    const permissionsTimeout = setTimeout(() => {
+      this.isWaitingPermissions = true;
+    }, 100);
 
-    this.isConnecting = true;
-    this.isMuted = false;
-    this.error = null;
-    this.isListenOnly = isListenOnly || false;
-    this.isEchoTest = isEchoTest || false;
-
-    const callOptions = {
-      isListenOnly: this.isListenOnly,
-      extension: isEchoTest ? ECHO_TEST_NUMBER : null,
-      inputStream: this.isListenOnly ? this.createListenOnlyStream() : this.inputStream,
+    const doCall = () => {
+      clearTimeout(permissionsTimeout);
+      this.isWaitingPermissions = false;
+      this.devicesInitialized = true;
+      this.isConnecting = true;
+      this.isMuted = false;
+      this.error = null;
+      this.isListenOnly = isListenOnly || false;
+      this.isEchoTest = isEchoTest || false;
+
+      const callOptions = {
+        isListenOnly: this.isListenOnly,
+        extension: isEchoTest ? ECHO_TEST_NUMBER : null,
+        inputStream: this.isListenOnly ? this.createListenOnlyStream() : this.inputStream,
+      };
+      return this.bridge.joinAudio(callOptions, this.callStateCallback.bind(this));
     };
 
-    return this.bridge.joinAudio(callOptions, this.callStateCallback.bind(this));
+    if (this.devicesInitialized) return doCall();
+
+    return Promise.all([
+      this.setDefaultInputDevice(),
+      this.setDefaultOutputDevice(),
+    ]).then(doCall)
+      .catch((err) => {
+        clearTimeout(permissionsTimeout);
+        this.isWaitingPermissions = false;
+        this.error = err;
+        this.notify(err.message);
+        return Promise.reject(err);
+      });
   }
 
   exitAudio() {
+    if (!this.isConnected) return Promise.resolve();
+
     this.isHangingUp = true;
     return this.bridge.exitAudio();
   }
@@ -97,9 +128,7 @@ class AudioManager {
   }
 
   toggleMuteMicrophone() {
-    makeCall('toggleSelfVoice').then(() => {
-      this.onToggleMicrophoneMute();
-    });
+    makeCall('toggleSelfVoice');
   }
 
   onAudioJoin() {
@@ -128,10 +157,6 @@ class AudioManager {
     this.isEchoTest = false;
   }
 
-  onToggleMicrophoneMute() {
-    this.isMuted = !this.isMuted;
-  }
-
   callStateCallback(response) {
     return new Promise((resolve) => {
       const {
@@ -166,27 +191,40 @@ class AudioManager {
     }
 
     this.listenOnlyAudioContext = window.AudioContext ?
-                                  new window.AudioContext() :
-                                  new window.webkitAudioContext();
+      new window.AudioContext() :
+      new window.webkitAudioContext();
 
     return this.listenOnlyAudioContext.createMediaStreamDestination().stream;
   }
 
   setDefaultInputDevice() {
-    this.changeInputDevice();
+    return this.changeInputDevice();
   }
 
-  async changeInputDevice(deviceId) {
-    try {
-      if (!deviceId) {
-        this.inputDevice = await await this.bridge.setDefaultInputDevice();
-        return;
-      }
-      this.inputDevice = await this.bridge.changeInputDevice(deviceId);
-    } catch(err) {
-      this.error = err;
-      this.notify('There was a problem getting the media devices');
+  setDefaultOutputDevice() {
+    return this.changeOutputDevice('default');
+  }
+
+  changeInputDevice(deviceId) {
+    const handleChangeInputDeviceSuccess = (inputDevice) => {
+      this.inputDevice = inputDevice;
+      return Promise.resolve(inputDevice);
+    };
+
+    const handleChangeInputDeviceError = () =>
+      Promise.reject({
+        type: 'MEDIA_ERROR',
+        message: this.messages.error.MEDIA_ERROR,
+      });
+
+    if (!deviceId) {
+      return this.bridge.setDefaultInputDevice()
+        .then(handleChangeInputDeviceSuccess)
+        .catch(handleChangeInputDeviceError);
     }
+    return this.bridge.changeInputDevice(deviceId)
+      .then(handleChangeInputDeviceSuccess)
+      .catch(handleChangeInputDeviceError);
   }
 
   async changeOutputDevice(deviceId) {
@@ -216,9 +254,11 @@ class AudioManager {
   }
 
   notify(message) {
-    notify(message,
-           this.error ? 'error' : 'info',
-           this.isListenOnly ? 'audio_on' : 'unmute');
+    notify(
+      message,
+      this.error ? 'error' : 'info',
+      this.isListenOnly ? 'audio_on' : 'unmute',
+    );
   }
 }
 
diff --git a/bigbluebutton-html5/imports/utils/statuses.js b/bigbluebutton-html5/imports/utils/statuses.js
index a0f5b575f535195c49fac0b30327c50327d0e5c7..9aa785f63997e224beb0e8ab4492bdfbc6833410 100644
--- a/bigbluebutton-html5/imports/utils/statuses.js
+++ b/bigbluebutton-html5/imports/utils/statuses.js
@@ -1,17 +1,5 @@
-const EMOJI_STATUSES = {
-  away: 'away',
-  raiseHand: 'raiseHand',
-  neutral: 'neutral',
-  confused: 'confused',
-  sad: 'sad',
-  happy: 'happy',
-  applause: 'applause',
-  thumbsUp: 'thumbsUp',
-  thumbsDown: 'thumbsDown',
-  none: 'none',
-};
-
-const EMOJI_NORMALIZE = {
+export const EMOJI_STATUSES = {
+  // name: icon
   away: 'time',
   raiseHand: 'hand',
   neutral: 'undecided',
@@ -21,6 +9,7 @@ const EMOJI_NORMALIZE = {
   applause: 'applause',
   thumbsUp: 'thumbs_up',
   thumbsDown: 'thumbs_down',
+  none: 'clear_status',
 };
 
-export { EMOJI_STATUSES, EMOJI_NORMALIZE };
+export default { EMOJI_STATUSES };
diff --git a/bigbluebutton-html5/private/config/public/chat.yaml b/bigbluebutton-html5/private/config/public/chat.yaml
index 8b291511391c9e37a0499b45078feb8006f88c53..81bc370d11c44b28707c34f2b75b30e6438a1437 100644
--- a/bigbluebutton-html5/private/config/public/chat.yaml
+++ b/bigbluebutton-html5/private/config/public/chat.yaml
@@ -17,3 +17,5 @@ chat:
   storage_key: 'UNREAD_CHATS'
   # Chat paths
   path_route: 'users/chat/'
+  system_messages_keys:
+    chat_clear: 'PUBLIC_CHAT_CLEAR'
diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json
index e77f22eb6617fd8bebe18c1332d489ea1ee7f6bc..da0db9ce2a17e369c09af49c13be26f9fddaeec7 100644
--- a/bigbluebutton-html5/private/locales/en.json
+++ b/bigbluebutton-html5/private/locales/en.json
@@ -1,6 +1,8 @@
 {
     "app.home.greeting": "Welcome {0}! Your presentation will begin shortly...",
     "app.chat.submitLabel": "Send Message",
+    "app.chat.errorMinMessageLength": "The message is {0} characters(s) too short",
+    "app.chat.errorMaxMessageLength": "The message is {0} characters(s) too long",
     "app.chat.inputLabel": "Message input for chat {0}",
     "app.chat.inputPlaceholder": "Message {0}",
     "app.chat.titlePublic": "Public Chat",
@@ -15,6 +17,7 @@
     "app.chat.dropdown.save": "Save",
     "app.chat.label": "Chat",
     "app.chat.emptyLogLabel": "Chat log empty",
+    "app.chat.clearPublicChatMessage": "The public chat history was cleared by a moderator",
     "app.userList.usersTitle": "Users",
     "app.userList.participantsTitle": "Participants",
     "app.userList.messagesTitle": "Messages",
@@ -172,18 +175,18 @@
     "app.actionsBar.emojiMenu.statusTriggerLabel": "Status",
     "app.actionsBar.emojiMenu.awayLabel": "Away",
     "app.actionsBar.emojiMenu.awayDesc": "Change your status to away",
-    "app.actionsBar.emojiMenu.raiseLabel": "Raise",
-    "app.actionsBar.emojiMenu.raiseDesc": "Raise your hand to ask a question",
-    "app.actionsBar.emojiMenu.undecidedLabel": "Undecided",
-    "app.actionsBar.emojiMenu.undecidedDesc": "Change your status to undecided",
+    "app.actionsBar.emojiMenu.raiseHandLabel": "Raise",
+    "app.actionsBar.emojiMenu.raiseHandDesc": "Raise your hand to ask a question",
+    "app.actionsBar.emojiMenu.neutralLabel": "Undecided",
+    "app.actionsBar.emojiMenu.neutralDesc": "Change your status to undecided",
     "app.actionsBar.emojiMenu.confusedLabel": "Confused",
     "app.actionsBar.emojiMenu.confusedDesc": "Change your status to confused",
     "app.actionsBar.emojiMenu.sadLabel": "Sad",
     "app.actionsBar.emojiMenu.sadDesc": "Change your status to sad",
     "app.actionsBar.emojiMenu.happyLabel": "Happy",
     "app.actionsBar.emojiMenu.happyDesc": "Change your status to happy",
-    "app.actionsBar.emojiMenu.clearLabel": "Clear",
-    "app.actionsBar.emojiMenu.clearDesc": "Clear your status",
+    "app.actionsBar.emojiMenu.noneLabel": "Clear",
+    "app.actionsBar.emojiMenu.noneDesc": "Clear your status",
     "app.actionsBar.emojiMenu.applauseLabel": "Applaud",
     "app.actionsBar.emojiMenu.applauseDesc": "Change your status to applause",
     "app.actionsBar.emojiMenu.thumbsUpLabel": "Thumbs up",
@@ -223,6 +226,8 @@
     "app.audioModal.no": "No",
     "app.audioModal.echoTestTitle": "This is a private echo test. Speak a few words. Did you hear audio?",
     "app.audioModal.settingsTitle": "Change your audio settings",
+    "app.audioModal.helpTitle": "There was an issue with your media devices",
+    "app.audioModal.helpText": "Did you give BigBlueButton permission to access your microphone? Note that a dialog should appear when you try to join audio, asking for your media device permissions, please accept that in order to join the audio conference. If that is not the case, try changing your microphone permissions in your browser's settings.",
     "app.audioModal.connecting": "Connecting",
     "app.audioModal.connectingEchoTest": "Connecting to echo test",
     "app.audioManager.joinedAudio": "You have joined the audio conference",
@@ -232,7 +237,7 @@
     "app.audioManager.connectionError": "Error: Connection error",
     "app.audioManager.requestTimeout": "Error: There was a timeout in the request",
     "app.audioManager.invalidTarget": "Error: Tried to request something to an invalid target",
-    "app.audioManager.mediaError": "Error: There was an error getting your media devices",
+    "app.audioManager.mediaError": "Error: There was an issue getting your media devices",
     "app.audio.joinAudio": "Join Audio",
     "app.audio.leaveAudio": "Leave Audio",
     "app.audio.enterSessionLabel": "Enter Session",
@@ -246,6 +251,8 @@
     "app.audio.audioSettings.retryLabel": "Retry",
     "app.audio.listenOnly.backLabel": "Back",
     "app.audio.listenOnly.closeLabel": "Close",
+    "app.audio.permissionsOverlay.title": "Allow BigBlueButton to use your Media Devices",
+    "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.error.kicked": "You have been kicked out of the meeting",
     "app.error.meeting.ended": "You have logged out of the conference",
     "app.dropdown.close": "Close",
diff --git a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties
index 69c3a41d2e4103a3c367539cbdec69b424c6b254..d94d5b185ff06de5078a3c902722c5da8b33a6ed 100755
--- a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties
+++ b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties
@@ -168,10 +168,20 @@ bigbluebutton.web.serverURL=http://10.130.218.89
 # If "default", it returns to bigbluebutton.web.serverURL
 bigbluebutton.web.logoutURL=default
 
-# The url of the BigBlueButton client. User's will be redirected here when
+# The url of the BigBlueButton client. Users will be redirected here when
 # successfully joining the meeting.
 defaultClientUrl=${bigbluebutton.web.serverURL}/client/BigBlueButton.html
-#defaultClientUrl=http://192.168.0.235/3rd-party.html
+
+# Force all attendees to join the meeting using the HTML5 client
+attendeesJoinViaHTML5Client=false
+
+# Force all moderators to join the meeting using the HTML5 client
+moderatorsJoinViaHTML5Client=false
+
+# The url of the BigBlueButton HTML5 client. Users will be redirected here when
+# successfully joining the meeting.
+html5ClientUrl=${bigbluebutton.web.serverURL}/html5client/join
+
 
 # The url for where the guest will poll if approved to join or not.
 defaultGuestWaitURL=${bigbluebutton.web.serverURL}/client/guest-wait.html
diff --git a/bigbluebutton-web/grails-app/conf/spring/resources.xml b/bigbluebutton-web/grails-app/conf/spring/resources.xml
index 6cc4edd98055f435bd292a7e1d4509860f51dd1b..1d9affec0cb88fbd2b7ca5b8dd8e52a7e8e81d63 100755
--- a/bigbluebutton-web/grails-app/conf/spring/resources.xml
+++ b/bigbluebutton-web/grails-app/conf/spring/resources.xml
@@ -123,6 +123,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
         <property name="defaultNumDigitsForTelVoice" value="${defaultNumDigitsForTelVoice}"/>
         <property name="defaultClientUrl" value="${defaultClientUrl}"/>
         <property name="defaultGuestWaitURL" value="${defaultGuestWaitURL}"/>
+        <property name="html5ClientUrl" value="${html5ClientUrl}"/>
+        <property name="moderatorsJoinViaHTML5Client" value="${moderatorsJoinViaHTML5Client}"/>
+        <property name="attendeesJoinViaHTML5Client" value="${attendeesJoinViaHTML5Client}"/>
         <property name="defaultMeetingDuration" value="${defaultMeetingDuration}"/>
         <property name="disableRecordingDefault" value="${disableRecordingDefault}"/>
         <property name="autoStartRecording" value="${autoStartRecording}"/>
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 f525304aca305a3b1606408c5f109ba1f868d361..36e96116ca4dd15e7fa8ad8d00d4532f338868e6 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
@@ -263,6 +263,11 @@ class ApiController {
       authenticated = Boolean.parseBoolean(params.auth)
     }
 
+    Boolean joinViaHtml5 = false;
+    if (!StringUtils.isEmpty(params.joinViaHtml5)) {
+      joinViaHtml5 = Boolean.parseBoolean(params.joinViaHtml5)
+    }
+
     // Do we have a name for the user joining? If none, complain.
     if(!StringUtils.isEmpty(params.fullName)) {
       params.fullName = StringUtils.strip(params.fullName);
@@ -505,6 +510,27 @@ class ApiController {
     boolean redirectClient = true;
     String clientURL = paramsProcessorUtil.getDefaultClientUrl();
 
+    // server-wide configuration:
+    // Depending on configuration, prefer the HTML5 client over Flash for moderators
+    if (paramsProcessorUtil.getModeratorsJoinViaHTML5Client() && role == ROLE_MODERATOR) {
+      clientURL = paramsProcessorUtil.getHTML5ClientUrl();
+    }
+
+    // Depending on configuration, prefer the HTML5 client over Flash for attendees
+    if (paramsProcessorUtil.getAttendeesJoinViaHTML5Client() && role == ROLE_ATTENDEE) {
+      clientURL = paramsProcessorUtil.getHTML5ClientUrl();
+    }
+
+    // single client join configuration:
+    // Depending on configuration, prefer the HTML5 client over Flash client
+    if (joinViaHtml5) {
+      clientURL = paramsProcessorUtil.getHTML5ClientUrl();
+    } else {
+      if(!StringUtils.isEmpty(params.clientURL)){
+        clientURL = params.clientURL;
+      }
+    }
+
     if(! StringUtils.isEmpty(params.redirect)) {
       try{
         redirectClient = Boolean.parseBoolean(params.redirect);
@@ -513,9 +539,6 @@ class ApiController {
       }
     }
 
-    if(!StringUtils.isEmpty(params.clientURL)){
-      clientURL = params.clientURL;
-    }
 
     String msgKey = "successfullyJoined"
     String msgValue = "You have joined successfully."